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.
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ściminVersion: 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.
- Jak duże może być logo na kodzie QR? Zmierzyliśmy„30% korekcji błędów” to nie budżet na logo. Wybijaliśmy rosnące dziury w kodach na każdym poziomie EC, aż dekoder się poddał, i zapisaliśmy punkt załamania.
- Jak mały może być wydrukowany kod QR? Rozmiary, wyliczoneNie ma jednego minimalnego rozmiaru kodu QR. Pięć prawdziwych ładunków przepuściliśmy przez nasz koder, by policzyć moduły, a potem zamieniliśmy je na milimetry.