MostlyQR

Czy zdjęciowe kody QR naprawdę się skanują? Wyniki naszych testów

Zdjęciowy kod QR wydaje się gorzej skanować. Sprawdziliśmy go prawdziwym dekoderem — rozmytego, pomniejszonego i odczytanego tak, jak robi to czytnik.

Max Čelar··Drukowanie i skanowanie

Co nowego tutaj: Wyniki dekodowania naszej własnej implementacji zdjęciowych kodów QR, zmierzone jsQR wobec zwykłego kodu kontrolnego: zero z 4,225 modułów źle odczytanych przy każdej aperturze czytnika od 0.10 do 0.45 modułu, identyczna tolerancja 3-krotnego pomniejszenia i tolerancja na rozmycie 4–5 pikseli wobec 6 pikseli dla zwykłego kodu — konkretny, niewielki koszt zamiany kodu w obraz.

Kod QR, który jest fotografią, wygląda na kompromis: ładnie, ale traci się część skanów. Ta intuicja jest trafna dla większości implementacji, a błędna co do tego, jak to musi działać — a “część skanów” to liczba, której nikt nie publikuje.

Oto nasze liczby, z zestawu testów, który musiał przejść, zanim w ogóle zdecydowaliśmy się wypuścić tę funkcję.

# Czym jest zdjęciowy kod QR, a czym nie jest

Dwie bardzo różne rzeczy nazywa się tak samo.

Kod QR AI — ten z modeli dyfuzyjnych — generuje obraz na podstawie kodu QR, a potem ma nadzieję, że wynik nadal się zdekoduje. Czasem tak się dzieje. Awaria jest niewidoczna, dopóki ktoś w ciemnym barze nie skieruje na niego starego, trzyletniego telefonu.

Kod ze zdjęciem zrasterowanym metodą dyfuzji błędu to drugie podejście i to ono jest tutaj mierzone. Obraz jest redukowany do jednego bitu — czarny albo biały dla każdego subpiksela — z jednym kluczowym ograniczeniem: w każdym środku modułu rastrowanie jest wymuszone do wartości, której żąda koder, a błąd, jaki to wymuszenie tworzy (do około 95% wartości piksela, dla białego modułu w ciemnym obszarze), jest przenoszony na sąsiednie subpiksele, które jeszcze nie zostały progowane. Obraz składa się z przestrzeni między środkami modułów.

Ta różnica to cały sens tego artykułu. Oznacza to, że kod nie wydaje swojej korekcji błędów na dekorację: każdy moduł nadal niesie wartość z kodera, więc pełna redundancja jest nienaruszona w przypadku zagięć, odblasków, kciuka na jednym rogu — dokładnie tego, do czego korekcja błędów właściwie służy.

# Test

Wszystko poniżej można odtworzyć z naszego repozytorium, offline:

  • Koder: nasz własny packages/qr, korekcja błędów H, dolny próg gęstości minVersion: 12 → wersja symbolu 12, 65 modułów w poprzek, 10 pikseli na moduł, strefa ciszy o szerokości 4 modułów.
  • Style: halftone (prawdziwie zrastrowane zdjęcie) i mosaic (rysowane są tylko moduły, o rozmiarze zależnym od lokalnej ciemności) — w porównaniu z zwykłym, niezrastrowanym kodem jako kontrolą — ten sam ładunek, ta sama wersja, ta sama skala.
  • Obrazy: proceduralne obrazy testowe, w tym gradient diagonalny (pojawia się każdy odcień) i jednolita szarość średnia, czyli najgorszy przypadek, bo każdy wymuszony punkt kłóci się z otoczeniem.
  • Dekoder: jsQR 1.4.0, prawdziwy dekoder QR, podany rzeczywiste piksele.
  • Degradacje: zmniejszenie metodą najbliższego sąsiada, rozmycie uśredniające z ponownym progowaniem (nieostry aparat) oraz odczyt przez aperturę.

Zestaw, na którym to się opiera, to packages/qr/test/dither.test.js. Przeszedł 39/39 testów 31 sierpnia 2026 r., a kod, którego nie da się zdekodować, to nie ostrzeżenie, lecz niezaliczony test — ta zasada jest powodem, dla którego funkcja ma taki, a nie inny kształt. Tabele poniżej są generowane przez draft/mostlyqr/seo/benchmarks/photo.js, który uruchamia te same style przez te same degradacje; nic tutaj nie zostało przepisane ręcznie.

# Zero źle odczytanych modułów przy każdej aperturze

Najważniejszy pomiar to nie “czy się zdekodował”, lecz “czy czytnik zobaczył właściwą wartość w każdej komórce”, bo kod, który dekoduje się tylko dlatego, że korekcja błędów go uratowała, to kod, który zawiedzie, gdy coś innego pójdzie nie tak.

Zgodny czytnik nie pobiera próbki z jednego piksela na moduł. Uśrednia mały okrągły aperturę w środku każdego modułu i progowuje tę średnią. Zrobiliśmy to samo, dla promieni apertury 0.10, 0.25, 0.36 i 0.45 modułu (0.5 oznaczałoby cały moduł) i policzyliśmy, ile z 4,225 modułów zostałoby odczytanych błędnie:

Styl Źle odczytane moduły, apertura 0.10 / 0.25 / 0.36 / 0.45
zwykły kod (kontrola) 0 / 0 / 0 / 0
zdjęcie mozaikowe 0 / 0 / 0 / 0
zdjęcie w półtonach (gradient) 0 / 0 / 0 / 0
zdjęcie w półtonach (jednolita szarość, najgorszy przypadek) 0 / 0 / 0 / 0

Ani jeden moduł, w żadnym stylu, przy żadnej aperturze, nawet w najgorszym przypadku obrazu. Kod ze zdjęciem i zwykły kod przedstawiają czytnikowi te same wartości. Korekcja błędów jest nienaruszona i nadal w pełni dostępna.

# Koszt: mniej więcej jeden krok rozmycia

Nienaruszona korekcja błędów nie oznacza identycznego obrazu. Rastrowanie zmienia wygląd pikseli między środkami, a mocno nieostry aparat uśrednia te piksele razem. Dlatego każdy styl pchnęliśmy aż do awarii:

Styl Maks. zmniejszenie, które się jeszcze zdekodowało Maks. promień rozmycia, który się jeszcze zdekodował
zwykły kod (kontrola) 3× 6 px
zdjęcie mozaikowe 3× 5 px
zdjęcie w półtonach (jednolita szarość) 3× 6 px
zdjęcie w półtonach (gradient) 3× 4 px

Zmniejszenie — kod wyrenderowany mniejszy niż natywnie — jest identyczne we wszystkich stylach: każdy przeszedł 3× i każdy po raz pierwszy zawiódł przy 4×. (Zmniejszenie metodą najbliższego sąsiada nie jest monotoniczne — niektóre znów się zdekodowały przy 5×, gdy siatka próbkowania akurat trafiła lepiej — dlatego podajemy pierwszą awarię, a nie ostatni sukces.)

Rozmycie to miejsce, gdzie pojawia się koszt, i to tylko dla stylu półtonowego na najtrudniejszym obrazie: o jedną trzecią mniejszy margines niż w zwykłym kodzie kontrolnym. Półtony jednolitej szarości dokładnie odpowiadały zwykłemu kodowi, więc ten koszt zależy od obrazu, a nie od samej techniki. Prościej mówiąc: kod ze zdjęciem wymaga, by aparat był mniej więcej o jeden krok bliżej ostrości niż w przypadku zwykłego kodu. To realny koszt i niewielki, i jest uczciwą odpowiedzią na pytanie “czy się skanują?” — nie “tak” i nie “nie”, lecz “tak, z około jednym krokiem mniejszą tolerancją ostrości”.

Styl mozaikowy plasuje się pomiędzy, co ma sens: rysuje tylko moduły, więc między środkami jest mniej tuszu, który rozmycie mogłoby rozmazać.

# Dlaczego gęstość to właściwy wymóg

Pierwsza wersja tej funkcji była technicznie poprawna i wyglądała źle, z powodu, który warto znać, jeśli używasz jakiegokolwiek narzędzia do kodów zdjęciowych:

https://mqr.sh/AB12CD przy korekcji błędów H — czyli tej, którą wymusza kod zdjęciowy — koduje się w siatkę 29 modułów. To 841 komórek na całą fotografię, a wynik wygląda jak szum, a nie jak obraz: ludzie wyciągają wniosek, że technika nie działa, choć prawdziwy problem jest taki, że obraz nie ma gdzie się zmieścić. Wymuszenie gęstszego symbolu to naprawia:

Minimalna wersja Symbol Moduły w poprzek Komórki dostępne dla obrazu
brak (tyle, ile wymaga ładunek) v3 29 841
8 v8 49 2,401
12 (nasz domyślny) v12 65 4,225

Pięć razy więcej komórek przy wersji 12. Każde poważne narzędzie do kodów zdjęciowych wymusza to samo z tego samego powodu, a dolny próg nigdy nie może zmniejszyć symbolu poniżej tego, czego faktycznie wymaga ładunek — dane zawsze mają pierwszeństwo przed gęstością.

Konsekwencja dla druku jest bezpośrednia i to właśnie powód, dla którego ten artykuł znajduje się w tym samym klastrze co ile modułów potrzebuje twój ładunek: kod o 65 modułach potrzebuje 73 szerokości modułu papieru wraz ze strefą ciszy. Przy module 0.5mm to 36.5mm — więc kod ze zdjęciem to plakat, menu, panel opakowania albo ekran. Nie jest to wizytówka.

# Czego test nie obejmuje

Mówiąc wprost, bo benchmark bez podanych ograniczeń jest gorszy niż żaden:

  • Jeden dekoder, syntetyczne piksele. jsQR to prawdziwy dekoder, ale nie jest stosem aparatu telefonu, a wejściem jest czysty wyrenderowany obraz, a nie zdjęcie wydrukowanego przedmiotu.
  • Brak druku, brak odblasków, brak perspektywy, brak zgnieceń. Każde z nich zjada margines, a kod ze zdjęciem i zwykły kod zjadają go różnie.
  • Brak koloru. To kody czarno-białe, 1-bitowe.
  • Nasza własna implementacja. Nic tutaj nie dotyczy niczyich kodów ze zdjęciami ani kodów AI-art. Jeśli nadpisują moduły i polegają na korekcji błędów, by naprawić szkody, zachowają się zupełnie inaczej niż to, a uczciwy test to ten opisany powyżej, uruchomiony na nich.

# Kiedy go użyć

  • Tak: plakaty, menu pod szkłem, panele opakowań, ekrany, grafika na wystawy, wszystko drukowane w dużym formacie.
  • Prawdopodobnie: okładka albumu, obwoluta książki, duża etykieta produktu — przetestuj to.
  • Nie: wizytówki, składane ulotki papierowe, wszystko mniejsze niż 35mm, wszystko skanowane przy słabym świetle z daleka.

Jeśli jednak chcesz nie całej fotografii, a swojego logo w środku zwykłego kodu, to inna technika z dużo mniejszym budżetem — zmierzyliśmy też jak duże może być logo i odpowiedź jest mniejsza, niż sądzi internet.

Kody ze zdjęciami MostlyQR to implementacja zmierzona tutaj. Ten sam rdzeń rastrowania działa w podglądzie w przeglądarce i w renderowanym po stronie serwera PNG, więc to, co widzisz w kreatorze, dostaje drukarnia. Notatka w kreatorze mówi “najlepiej na ekranach i dużych wydrukach — kod ze zdjęciem trudniej odczytać na małym lub pogniecionym papierze”, co po tych liczbach wydaje się właściwą rzeczą do powiedzenia.

Często zadawane pytania

Czy zdjęciowe kody QR skanują się niezawodnie?

W naszych testach tak — z jednym mierzalnym kosztem. Gdy czytamy nasze zdjęciowe kody tak, jak robi to zgodny czytnik, uśredniając okrągłą aperturę w środku każdego modułu, nie został źle odczytany ani jeden z 4,225 modułów przy żadnej testowanej aperturze. Tolerowały to samo 3-krotne pomniejszenie co zwykły kod. Jedyna zmierzona różnica dotyczyła rozogniskowania i tylko na najtrudniejszym obrazie: zwykły kod kontrolny wytrzymał rozmycie o promieniu 6 pikseli, styl mozaikowy 5, a styl półtonowy 4 na gradiencie (6 na jednolitej szarości). To mniej więcej jeden krok ostrości aparatu, a nie inna kategoria niezawodności.

Czy artystyczne kody QR zużywają korekcję błędów?

Wiele implementacji tak, i dlatego ta technika ma złą opinię. Nasza nie. Zamiast nadpisywać całe moduły danymi obrazu i liczyć na to, że Reed-Solomon naprawi różnicę, wymusza punkt w środku każdego modułu na wartość wymaganą przez koder i rozprasza powstały błąd na otaczające podpiksele. Obraz jest opłacany z przestrzeni między środkami modułów, więc pełny budżet korekcji błędów pozostaje na rzeczywiste warunki — zagniecenia, odblaski i kciuk na rogu.

Czy mogę wydrukować zdjęciowy kod QR na ulotce?

Nie polecamy. Nasz własny kreator pisze „najlepiej na ekranach i dużych wydrukach” i tak jest: zdjęciowy kod potrzebuje gęstego symbolu (65 modułów w poniższym teście, wobec 29 dla tego samego krótkiego linku bez wymuszenia), więc przy rozmiarze ulotki każdy moduł bardzo szybko robi się bardzo mały. Plakaty, menu pod szkłem, panele opakowań i ekrany to właściwe miejsca. Papierowa ulotka, która ląduje w kieszeni, już nie.

Dlaczego mój zdjęciowy kod QR wygląda jak szum, a nie jak obraz?

Prawie zawsze dlatego, że symbol jest zbyt gruby. Krótki link przy korekcji błędów H koduje się do siatki 29 modułów — 841 komórek, zdecydowanie za mało, by przenieść rozpoznawalny obraz, więc obrazowi nie ma gdzie się zmieścić. Pomaga gęstszy symbol: wersja 8 daje 49 modułów, a wersja 12 — 65 (4,225 komórek, pięć razy więcej), dlatego nasz kreator domyślnie używa wersji 12 dla zdjęciowych kodów. Dane zawsze wygrywają, więc dłuższy ładunek nadal dostanie większy symbol.

Źródła
  1. jsQR — the decoder used in the test
  2. The published description of the dithered-QR technique this implementation was built from
  3. QR Code standardization (ISO/IEC 18004) — Denso Wave
Więcej w tym temacie