MostlyQR

Les codes QR photo fonctionnent-ils vraiment ? Nos résultats de test de décodage

Un code QR qui est une photographie semble devoir scanner plus mal. Nous avons mesuré le nôtre avec un vrai décodeur — flou, réduit et lu comme le ferait un lecteur.

Max Čelar··Impression et numérisation

Quoi de neuf ici : Résultats de test de décodage pour notre implémentation de code QR photo, mesurés avec jsQR face à un code de contrôle ordinaire : zéro des 4 225 modules mal lus à chaque ouverture de lecteur de 0,10 à 0,45 modules, tolérance de réduction d'échelle 3x identique, et tolérance de flou de 4-5 pixels face aux 6 du code ordinaire — le coût spécifique et minime de transformer un code en image.

Un code QR qui est une photographie a l’air de devoir être un compromis : joli, mais vous perdez des lectures. Cette intuition est juste pour la plupart des implémentations et fausse sur la façon dont cela doit fonctionner — et « quelques lectures perdues » n’est pas un chiffre que quiconque ne publie.

Voici les nôtres, issus de la suite de tests qui devait passer avant que nous ne mettions la fonctionnalité en ligne.

# Ce qu’est un code QR photo et ce qu’il n’est pas

Deux choses très différentes portent le même nom.

L’art QR par IA — le type diffusion-model — génère une image conditionnée par un code QR, puis espère que le résultat se décode toujours. Parfois c’est le cas. Le mode de défaillance est invisible jusqu’à ce que quelqu’un dans un bar sombre pointe un téléphone de trois ans dessus.

Un code photo en treillis est l’autre approche et celle mesurée ici. L’image est réduite à un bit — noir ou blanc par sous-pixel — par diffusion d’erreur, avec une contrainte cruciale : à chaque centre de module, le treillis est forcé à la valeur demandée par l’encodeur, et l’erreur qui en résulte (jusqu’à environ 95 % de la valeur d’un pixel, pour un module blanc dans une région sombre) est poussée dans les sous-pixels voisins qui n’ont pas encore été seuillés. L’image est faite de l’espace entre les centres de module.

Cette distinction est tout l’article. Cela signifie que le code ne dépense pas sa correction d’erreur pour la décoration : chaque module porte toujours la valeur demandée par l’encodeur, de sorte que toute la redondance reste intacte pour les plis, les reflets, un pouce sur un coin — les choses pour lesquelles la correction d’erreur existe réellement.

# Le test

Tout ce qui suit est reproductible depuis notre dépôt, hors ligne :

  • Encodeur : notre propre packages/qr, correction d’erreur H, plancher de densité minVersion: 12 → version de symbole 12, 65 modules en travers, 10 pixels par module, zone silencieuse de 4 modules.
  • Styles : halftone (une véritable photographie tramée) et mosaic (seuls les modules sont dessinés, dimensionnés par la noirceur locale), contre un code simple non tramé comme contrôle — même charge, même version, même échelle.
  • Images : images de test procédurales, incluant un dégradé diagonal (chaque ton apparaît) et un gris moyen plat, qui est le pire cas car chaque point forcé n’est pas d’accord avec ses alentours.
  • Décodeur : jsQR 1.4.0, un vrai décodeur QR, donné les pixels réels.
  • Dégradations : réduction à l’interpolation du plus proche voisin, flou gaussien suivi d’un re-seuillage (une caméra défocalisée), et une lecture d’ouverture.

La suite sur laquelle ceci est basé est packages/qr/test/dither.test.js. Elle a tourné 39/39 passing le 2026-08-31, et un code qui échoue à se décoder est un test échouant plutôt qu’un avertissement — cette règle est pourquoi la fonctionnalité existe dans la forme qu’elle a. Les tableaux ci-dessous sont imprimés par draft/mostlyqr/seo/benchmarks/photo.js, qui exécute les mêmes styles à travers les mêmes dégradations ; rien ici n’a été retranscrit à la main.

# Zéro module mal lu, à chaque ouverture

La mesure qui compte le plus n’est pas « s’est-il décodé » mais « le lecteur a-t-il vu la bonne valeur dans chaque cellule », car un code qui se décode seulement parce que la correction d’erreur l’a sauvé est un code qui échouera quand quelque chose d’autre s’effondre.

Un lecteur conforme n’échantillonne pas un pixel par module. Il fait la moyenne d’une petite ouverture circulaire à chaque centre de module et seuille cette moyenne. Nous avons donc fait la même chose, à des rayons d’ouverture de 0,10, 0,25, 0,36 et 0,45 modules (0,5 serait le module entier), et compté combien des 4 225 modules seraient lus incorrectement :

Style Modules mal lus, ouverture 0.10 / 0.25 / 0.36 / 0.45
code simple (contrôle) 0 / 0 / 0 / 0
photo mosaic 0 / 0 / 0 / 0
photo halftone (dégradé) 0 / 0 / 0 / 0
photo halftone (gris moyen plat, pire cas) 0 / 0 / 0 / 0

Pas un module, dans aucun style, à aucune ouverture, y compris l’image du pire cas. Le code photo et le code simple présentent au lecteur les mêmes valeurs. La correction d’erreur est intacte et reste entièrement disponible.

# Ce qu’il en coûte : environ une étape de flou

La correction d’erreur intacte ne signifie pas identique. Le treillis change effectivement à quoi les pixels ressemblent entre les centres, et une caméra mal défocalisée fait la moyenne de ces pixels ensemble. Nous avons donc poussé chaque style jusqu’à ce qu’il se casse :

Style Réduction maximale qui se décode toujours Rayon de flou maximal qui se décode toujours
code simple (contrôle) 6 px
photo mosaic 5 px
photo halftone (gris moyen plat) 6 px
photo halftone (dégradé) 4 px

La réduction d’échelle — le code rendu plus petit qu’en natif — est identique sur tous les styles : chacun a survécu à 3× et chacun a échoué pour la première fois à 4×. (La réduction à l’interpolation du plus proche voisin n’est pas monotone — certains d’entre eux se sont décodés à nouveau à 5×, où la grille d’échantillonnage s’était avérée plus bienveillante — donc le chiffre que nous signalons est le premier échec, pas le dernier succès.)

Le flou est où le coût s’affiche, et seulement pour le style halftone sur l’image la plus difficile : un tiers moins de marge que le contrôle simple. Le halftone d’un champ gris moyen plat correspondait exactement au code simple, c’est donc un coût dépendant de l’image plutôt qu’une propriété de la technique. En termes simples : un code photo nécessite que l’appareil photo soit environ une étape plus proche de la mise au point qu’un code simple. C’est un vrai coût et c’est un petit coût, et c’est la réponse honnête à « se scannent-ils ? » — ni « oui » ni « non » mais « oui, avec environ une étape moins de tolérance de mise au point ».

Le style mosaic se situe entre les deux, ce qui a du sens : il ne dessine que les modules, il y a donc moins d’encre entre les centres pour qu’un flou s’étale.

# Pourquoi la densité est la vraie exigence

La première version de cette fonctionnalité était techniquement correcte et s’avérait incorrecte, pour une raison importante à connaître si vous utilisez un outil QR photo :

https://mqr.sh/AB12CD à la correction d’erreur H — qui est ce qu’un code photo force — encode en une grille de 29 modules. C’est 841 cellules pour une photographie entière, et le résultat se lit comme du bruit plutôt que comme une image : les gens concluent que la technique est cassée quand le vrai problème est qu’il n’y a nulle part pour que l’image vive. Forcer un symbole plus dense le corrige :

Version minimale Symbole Modules en travers Cellules disponibles pour l’image
aucune (ce que la charge en a besoin) v3 29 841
8 v8 49 2,401
12 (notre défaut) v12 65 4,225

Cinq fois plus de cellules à la version 12. Chaque outil image-QR sérieux force cela pour la même raison, et le plancher ne doit jamais réduire un symbole en dessous de ce que la charge en a réellement besoin — les données gagnent sur la densité, toujours.

La conséquence pour l’impression est directe, et c’est la raison pour laquelle cet article se situe dans le même cluster que combien de modules votre charge en a besoin : un code de 65 modules a besoin de 73 largeurs de module de papier incluant sa zone silencieuse. À 0,5 mm de module, c’est 36,5 mm — donc un code photo est une affiche, un menu, un panneau d’emballage ou un écran. Ce n’est pas une carte de visite.

# Ce que le test ne couvre pas

Énoncé clairement, car un benchmark avec des limites non énoncées est pire que rien :

  • Un décodeur, pixels synthétiques. jsQR est un vrai décodeur mais ce n’est pas une pile de caméra téléphonique, et l’entrée est une sortie proprement rendue plutôt qu’une photographie d’une chose imprimée.
  • Pas d’impression, pas de reflet, pas de perspective, pas de froissement. Chacun de ceux-ci coûte une marge, et ils coûtent un code photo et un code simple différemment.
  • Pas de couleur. Il s’agit de codes noir et blanc 1 bit.
  • Notre propre implémentation. Rien ici ne dit rien sur les codes QR photo ou art-IA de quiconque d’autre. S’ils écrasent les modules et s’appuient sur la correction d’erreur pour réparer les dégâts, ils se comporteront complètement différemment de ceci, et le test équitable est celui ci-dessus exécuté contre eux.

# Quand en utiliser un

  • Oui : affiches, menus sous verre, panneaux d’emballage, écrans, graphiques d’exposition, tout imprimé grand.
  • Probablement : une pochette d’album, une jaquette de livre, une grande étiquette de produit — testez-la.
  • Non : cartes de visite, tracts en papier qui se plient, quoi que ce soit en dessous d’environ 35 mm, quoi que ce soit scanné en mauvaise lumière à distance.

Et si ce que vous voulez n’est pas une photographie entière mais votre logo au milieu d’un code autrement ordinaire, c’est une technique différente avec un budget beaucoup plus petit — nous avons mesuré à quel point un logo peut devenir grand aussi, et la réponse est plus petit que l’internet le pense.

Les codes photo de MostlyQR sont l’implémentation mesurée ici. Le même noyau de treillis fonctionne dans l’aperçu du navigateur et dans le PNG rendu par le serveur, donc ce que vous voyez dans le générateur est ce que l’imprimante obtient. La note du générateur dit « meilleur sur les écrans et les grands tirages — un code photo est une lecture plus difficile sur papier petit ou froissé », qui après ces chiffres semble être la bonne chose à dire.

Questions fréquemment posées

Les codes QR photo se lisent-ils de manière fiable ?

Dans nos propres tests, oui — avec un coût mesurable. En lisant nos codes photo comme le ferait un lecteur conforme, en moyennant une ouverture circulaire au centre de chaque module, pas un seul des 4 225 modules n'a été mal lu à chaque ouverture que nous avons testée. Ils ont toléré la même réduction d'échelle 3x qu'un code ordinaire. La seule différence mesurée était la défocalisation, et seulement sur l'image la plus difficile : le contrôle ordinaire a survécu à un rayon de flou de 6 pixels, le style mosaïque 5, et le style demi-teinte 4 sur un dégradé (6 sur gris uni). C'est à peu près une étape de mise au point de caméra, pas une catégorie de fiabilité différente.

Les codes QR artistiques consomment-ils la correction d'erreur ?

Beaucoup d'implémentations le font, ce qui explique pourquoi la technique a mauvaise réputation. La nôtre ne le fait pas. Au lieu de réécrire des modules entiers avec des données d'image et de compter sur Reed-Solomon pour réparer la différence, elle force un point au centre de chaque module à la valeur que l'encodeur demande et diffuse l'erreur résultante dans les sous-pixels environnants. L'image est payée à partir de l'espace entre les centres des modules, donc l'intégralité du budget de correction d'erreur est toujours disponible pour le monde réel — pour les plis, les reflets et un doigt sur le coin.

Puis-je imprimer un code QR photo sur un prospectus ?

Nous ne le ferions pas. Notre propre générateur dit « optimal sur écrans et grands tirages », et nous le pensons vraiment : un code photo a besoin d'un symbole dense (65 modules dans le test ci-dessous, contre 29 pour le même lien court non forcé), donc à la taille d'un prospectus chaque module devient très petit très rapidement. Les affiches, les menus sous verre, les panneaux d'emballage et les écrans sont les bons endroits. Un prospectus papier qui se plie dans une poche ne l'est pas.

Pourquoi mon code QR photo ressemble-t-il à du bruit au lieu d'une image ?

Presque toujours parce que le symbole est trop grossier. Un lien court avec correction d'erreur H s'encode en une grille de 29 modules — 841 cellules, bien trop peu pour contenir une image reconnaissable, donc l'image n'a nulle part où vivre. Forcer un symbole plus dense le règle : la version 8 donne 49 modules et la version 12 donne 65 (4 225 cellules, cinq fois plus), et notre générateur utilise par défaut la version 12 pour les codes photo pour exactement cette raison. Les données gagnent toujours, donc une charge utile plus longue obtient toujours un symbole plus grand.

Sources
  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
Plus sur ce sujet