Un QR code stampato su un cartellino, con al centro un disegno di un’ape, continua a portare il telefono all’URL del produttore. Lo stesso vale per i QR dei wallet, dei biglietti del treno, delle confezioni alimentari, dove al posto dell’ape c’è un logo aziendale. A vista è un’anomalia: una macchia di inchiostro estraneo occulta una porzione del codice, eppure il decoder non sbaglia. Non è un trucco di stampa, è un teorema del 1960.
Sei pagine pubblicate sul Journal of the SIAM da Irving Reed e Gustave Solomon, due ricercatori americani, hanno descritto una famiglia di codici lineari su campi finiti che permette di ricostruire un messaggio anche quando una parte dei byte trasmessi arriva sbagliata o cancellata. La specifica QR ne incorpora una variante operativa su GF(28), il campo a 256 elementi. Nel formato Version 6 livello H si parla di quattro blocchi Reed-Solomon indipendenti, 172 byte di codeword totali, 112 di parità, fino al 32,5% dei byte recuperabile per costruzione.
▶” frameborder=”0″ allow=”accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture” allowfullscreen title=”Vedi il video”>
L’articolo prende un singolo QR code, ne smonta lo strato di correzione errori, misura in laboratorio il punto in cui un’occlusione centrale fa effettivamente collassare la decodifica, e mostra come la stessa matematica sia il substrato silenzioso di CD audio, DVD, comunicazioni satellitari delle sonde Voyager, NAND flash, ADSL e oggi della QR code art generata con modelli di diffusione.
Anatomia di un QR Version 6
Un QR code è una matrice di moduli, i quadratini neri o bianchi visibili a occhio nudo. La dimensione varia in funzione della version: dalla 1 (21×21 moduli) alla 40 (177×177). Codificando l’URL https://pinperepette.github.io/signal.pirate/ al livello di correzione massimo, la libreria qrcode sceglie automaticamente la Version 6: 41 moduli per lato, 1.681 moduli totali.
Non tutti i moduli portano dati. Una parte non trascurabile fa da impalcatura geometrica che serve al decoder per trovare il codice nell’immagine, raddrizzare la prospettiva e calibrare la griglia:
-
Tre finder pattern agli angoli (alto-sinistra, alto-destra, basso-sinistra), 7×7 moduli ciascuno con separatori, per 192 moduli complessivi. Sono i quadrati concentrici che il telefono riconosce in pochi millisecondi.
-
Un alignment pattern centrale, 5×5 moduli, che corregge distorsioni residue di prospettiva.
-
Due timing pattern, le righe a scacchi che collegano i finder, 60 moduli.
-
Bit di format info, 30 moduli codificati con BCH(15,5) e replicati due volte per ridondanza.
Quello che resta è l’area dati, raggruppata a blocchi di 8 moduli che formano i byte di codeword. Per la Version 6 sono 172 byte. Il significato di quei 172 byte cambia al variare del livello di correzione errori, fissato dalla specifica ISO/IEC 18004 in quattro varianti L, M, Q, H:
| Livello | Byte dati (k) | Byte parità (n-k) | Blocchi RS | Correggibili | % byte |
|---|---|---|---|---|---|
| L | 136 | 36 | 2 x (86,68) | 18 | 10,5% |
| M | 108 | 64 | 4 x (43,27) | 32 | 18,6% |
| Q | 76 | 96 | 4 x (43,19) | 48 | 27,9% |
| H | 60 | 112 | 4 x (43,15) | 56 | 32,5% |
I QR con logo al centro lavorano quasi sempre a livello H. Quattro blocchi indipendenti, ognuno con 15 byte di dati e 28 byte di parità, ciascuno capace di correggere fino a 14 byte sbagliati. Sommati: 56 byte su 172, esattamente il 32,5%. Quei 112 byte di parità sono lo spazio dove vive il resto della discussione.
Quattro polinomi indipendenti reggono i 60 byte di dati di un QR livello H.
Il problema formale: r = c + e
Il telefono inquadra il codice. La camera scatta una foto a colori, la converte in scala di grigi, la binarizza con una soglia adattiva, ritrova la matrice di moduli grazie ai finder pattern, raddrizza la prospettiva. È computer vision, e non è il tema centrale. Alla fine di quel processo il telefono ha un vettore di 172 byte, chiamato r. Il vettore originale, quello che il tipografo voleva trasmettere, è c. Tra i due c’è un polinomio di errore e tale che:
r = c + e in GF(2^8)^172
Le componenti non nulle di e sono i byte sbagliati. Posizioni e valori sono entrambi ignoti. Le cause concrete sono molte: il logo disegnato sopra, una stampa imperfetta, un riflesso sul cellophane, una foto storta. Per il decoder è rumore indistinguibile.
La domanda della teoria dei codici si riformula come problema combinatorio. Esiste un sottoinsieme C ⊂ GF(2^8)^172 di codeword ammessi tale che ogni vettore ricevuto r con al massimo t byte sbagliati abbia un solo c ∈ C più vicino, da cui ricostruire univocamente l’originale? Se sì, il codice ha capacità correttiva t. La risposta storica è data dai codici lineari, e Reed-Solomon è la versione che satura il miglior compromesso possibile fra dati e parità.
Distanza di Hamming. Per due vettori
u, vinGF(2^8)^n, la distanzad(u,v)è il numero di posizioni in cui differiscono. La distanza minima di un codiceCè il minimo did(c1, c2)perc1, c2 ∈ Cdistinti. Se la distanza minima vale2t+1, il decoder può correggere fino aterrori per nearest-neighbor. La distanza minima è tutto.
GF(28): un campo finito di 256 elementi
Per costruire un codice lineare su byte serve un’algebra in cui ogni byte non nullo abbia un inverso moltiplicativo. Gli interi modulo 256 non funzionano: 2 × 128 = 256 ≡ 0 (mod 256), quindi 2 non è invertibile. Lo stesso vale per ogni divisore di 256. L’anello Z/256Z non è un campo.
La costruzione corretta è un campo di Galois di 256 elementi, GF(2^8). Si prende l’insieme dei polinomi di grado al massimo 7 a coefficienti in GF(2), si quoziente rispetto a un polinomio irriducibile di grado 8. La specifica QR fissa:
p(x) = x^8 + x^4 + x^3 + x^2 + 1 in esadecimale: 0x11D
Ogni byte b7 b6 ... b1 b0 rappresenta il polinomio b7·x^7 + b6·x^6 + ... + b1·x + b0. A questo punto il byte smette di comportarsi come un numero normale. Niente +1, +2, niente riporti, niente intuizione decimale. Restano tre operazioni:
-
Addizione: XOR bit a bit. In caratteristica 2 la somma è la sua stessa inversa:
a + a = 0. -
Moltiplicazione: prodotto polinomiale (con XOR), ridotto modulo
p(x). -
Inverso: ogni elemento non nullo si scrive come
α^kperα = 2e qualchek ∈ {0, 1, ..., 254}. L’inverso diα^kèα^(255−k).
Le implementazioni reali precalcolano due tabelle: exp[k] = α^k e log[α^k] = k. Le moltiplicazioni diventano somme di logaritmi modulo 255. Il costo passa da una decina di cicli a due lookup e una somma.
# Tabelle exp/log in GF(2^8), generatore alpha=2, primitivo 0x11D
def build_tables():
exp = [0] * 512
log = [0] * 256
x = 1
for i in range(255):
exp[i] = x
log[x] = i
x <<= 1
if x & 0x100:
x ^= 0x11D
for i in range(255, 512):
exp[i] = exp[i - 255]
return exp, log
def gf_mul(a, b):
if a == 0 or b == 0: return 0
return exp[log[a] + log[b]]
Verifica canonica: gf_mul(0x53, 0xCA) = 0x01. Quindi 0xCA è l’inverso moltiplicativo di 0x53 in GF(2^8). Si controlla anche a mano espandendo i prodotti polinomiali e riducendo modulo 0x11D. Sono otto operazioni di XOR, niente di trascendente.
La codifica: il messaggio è un polinomio
L’idea originale di Reed e Solomon, riformulata in versione moderna, è rappresentare il messaggio come un polinomio a coefficienti in GF(2^8) e costruire il codeword come un polinomio più lungo che è divisibile per un polinomio generatore noto a priori.
Sia n la lunghezza del codeword in byte, k il numero di byte di dati, 2t = n − k il numero di byte di parità. Per il blocco di QR Version 6 livello H: n=43, k=15, 2t=28. Il polinomio generatore è definito come prodotto di fattori lineari nelle prime potenze di α:
g(x) = (x − α^0)(x − α^1) ··· (x − α^(2t−1))
Il messaggio m(x) di grado k−1 = 14 ha come coefficienti i 15 byte di dati. La codifica costruisce un codeword c(x) di grado n−1 = 42:
c(x) = m(x) · x^(2t) + [ m(x) · x^(2t) mod g(x) ]
È un encoding sistematico: i primi k coefficienti sono i dati originali, gli ultimi 2t sono la parità. Per costruzione c(α^i) = 0 per ogni i = 0, 1, ..., 2t−1. Tradotto in algebra lineare: i codeword vivono in un sottospazio di GF(2^8)^n di dimensione k, e quei 2t annullamenti sono il fulcro di tutta la decodifica.
In codice il calcolo è una divisione polinomiale lunga, riga per riga sui coefficienti, con XOR al posto della sottrazione. Costo O(k · 2t) operazioni in GF(2^8): per un blocco (43, 15) sono 420 moltiplicazioni e altrettanti XOR. Su un microcontrollore qualsiasi gira in microsecondi.
# Encoder Reed-Solomon sistematico su GF(2^8)
def rs_generator_poly(nsym):
g = [1]
for i in range(nsym):
g = gf_poly_mul(g, [1, exp[i]])
return g
def rs_encode(msg, nsym):
gen = rs_generator_poly(nsym)
out = list(msg) + [0] * nsym
for i in range(len(msg)):
coef = out[i]
if coef != 0:
for j in range(1, len(gen)):
out[i + j] ^= gf_mul(gen[j], coef)
return list(msg) + out[len(msg):]
Singleton: perché esiste proprio il 32,5%
Il livello H del QR code, secondo la vulgata, “recupera circa il 30%”. Quel numero non è una scelta ingegneristica, è un teorema. Si chiama limite di Singleton, dimostrato da Richard Singleton nel 1964. Per ogni codice lineare (n, k) a coefficienti in qualunque campo finito vale:
d_min ≤ n − k + 1
La dimostrazione è un argomento di conteggio. Proiettando un codice lineare sui suoi primi k−1 coefficienti si ottengono 256^(k−1) possibili proiezioni. I codeword sono 256^k, e ne stiamo proiettando ognuno in uno spazio più piccolo: per il principio dei cassetti, almeno due codeword distinti hanno la stessa proiezione, e differiscono solo sugli ultimi n − k + 1 coefficienti. Tre righe.
Reed-Solomon satura il limite: vale l’uguaglianza d_min(RS) = n − k + 1. È un codice MDS (Maximum Distance Separable), espressione tecnica che significa “non si può fare meglio di così con un codice lineare”. Per il blocco (43, 15) di Version 6 livello H: d_min = 43 − 15 + 1 = 29. La capacità correttiva senza informazione preliminare sulle posizioni degli errori è:
t = ⌊(d_min − 1) / 2⌋ = ⌊28 / 2⌋ = 14
Quattro blocchi indipendenti su Version 6 H portano a 4 × 14 = 56 byte di errori correggibili su 172 byte totali. La frazione è 56/172 = 0,3256, il 32,5% che la specifica arrotonda a “circa 30%”. Non esiste codice lineare su GF(2^8) di parametri (43, 15) che corregga più di 14 errori. Quel numero non si può aumentare per via algebrica.
Quattro livelli, quattro scelte di k. I livelli L, M, Q, H non sono codici diversi: sono tutti Reed-Solomon su
GF(2^8), con lo stesson(fissato dalla version) ekdiverso. Ridurreksignifica pagare meno dati in cambio di più parità, e quindi untpiù grande. Il livello H paga la metà dei byte di dati per il doppio degli errori correggibili. Il prezzo è lineare: cambia solo dove si decide di tagliare, non come la matematica reagisce.
Quattro passi per decodificare ciò che non si conosce
Costruito il codice, resta da capire come si recupera c(x) da r(x) quando non si sa né dove né quanto ci si è sbagliati. Il telefono ha r(x) = c(x) + e(x), non conosce nulla di e(x), e sfrutta solo il fatto che c(α^i) = 0 per i primi 2t esponenti. La decodifica classica è una pipeline a quattro stadi: sindromi, Berlekamp-Massey, Chien search, algoritmo di Forney.
Sindromi
Si valuta il polinomio ricevuto nelle prime 2t potenze di α:
S_i = r(α^i) = c(α^i) + e(α^i) = e(α^i), i = 1, ..., 2t
Le sindromi dipendono solo da e(x), non da c(x): il messaggio originale, qualunque esso sia, sparisce dalla formula. Se tutte le S_i sono nulle, si conclude e = 0 e r = c. Altrimenti le 2t sindromi formano un sistema di equazioni non lineari nelle posizioni e nei valori degli errori, da risolvere indirettamente.
Polinomio locatore degli errori
Sia v ≤ t il numero effettivo di errori in e(x), siano j_1, ..., j_v le loro posizioni. Si definisce il polinomio locatore degli errori come polinomio di grado v le cui radici sono α^(−j_1), ..., α^(−j_v):
σ(x) = Π (1 − α^(j_i) · x), i = 1, ..., v
L’algoritmo di Berlekamp-Massey ricostruisce σ(x) dalle 2t sindromi. Lavora iterativamente: a ogni passo calcola la discrepanza fra una sindrome attesa e quella ricevuta, e aggiorna σ in modo da annullare la discrepanza. Il risultato è il più corto LFSR (linear feedback shift register) che genera la sequenza di sindromi. Costo: O(t^2) operazioni in GF(2^8). Esiste una formulazione equivalente via algoritmo di Euclide esteso applicato a S(x) e x^(2t): stesso costo asintotico, geometria diversa.
# Berlekamp-Massey su GF(2^8): ricostruisce sigma(x) dalle sindromi
def berlekamp_massey(syn):
sigma, B = [1], [1]
L, m, b = 0, 1, 1
for n in range(len(syn)):
delta = syn[n]
for i in range(1, L + 1):
delta ^= gf_mul(sigma[i], syn[n - i])
if delta == 0:
m += 1
continue
scale = gf_mul(delta, gf_inv(b))
new_sigma = list(sigma)
for i, c in enumerate(B):
while len(new_sigma) <= i + m:
new_sigma.append(0)
new_sigma[i + m] ^= gf_mul(scale, c)
if 2 * L <= n:
B, b = list(sigma), delta
L = n + 1 - L
m = 1
else:
m += 1
sigma = new_sigma
return sigma
Chien search e algoritmo di Forney
Le radici di σ(x) si trovano per enumerazione: per ogni j = 0, 1, ..., n−1 si calcola σ(α^(−j)). I valori di j per cui il risultato è zero sono le posizioni degli errori. Nessuna fattorizzazione esplicita, solo valutazioni successive che condividono i monomi. Costo O(n · v).
Resta da calcolare i valori degli errori, ora che si conoscono le posizioni. Si definisce il polinomio valutatore Ω(x) = [ S(x) · σ(x) ] mod x^(2t) e il valore di errore in posizione j_i si ricava da:
e_(j_i) = − Ω(α^(−j_i)) / σ'(α^(−j_i)) · α^(j_i)
In caratteristica 2 vale 1 + 1 = 0 nel campo, quindi quando si deriva σ(x) ogni coefficiente di indice pari sparisce e restano solo i monomi di grado dispari. La derivata formale si calcola in tre righe. Sottratto e da r si recupera c. I primi k=15 byte di ogni blocco sono il messaggio originale. Concatenati i quattro blocchi si ottengono i 60 byte di dati, e la decodifica UTF-8 restituisce https://pinperepette.github.io/signal.pirate/.
Le sindromi sopravvivono al messaggio: dipendono solo dall’errore, non da ciò che si voleva trasmettere.
Interleaving: perché il danno geometrico non è il danno algebrico
Singleton garantisce che il blocco RS(43, 15) corregga fino a 14 errori. Un logo al centro di un QR, però, non produce “14 errori sparsi”: occulta una regione contigua di moduli. Se i quattro blocchi RS fossero scritti in fila sulla matrice (prima il blocco 1, poi il 2, eccetera), il logo colpirebbe in pieno uno o due blocchi e li distruggerebbe oltre il limite correggibile. Gli altri due resterebbero intatti, e il decoder fallirebbe lo stesso, perché mancherebbe un blocco intero.
La specifica QR risolve il problema con l’interleaving. La sequenza fisica dei 172 byte di codeword sulla matrice non è “blocco 1, blocco 2, blocco 3, blocco 4” in fila. È intrecciata: il byte fisico in posizione i appartiene al blocco logico i mod 4. I 43 byte del primo blocco sono sparsi sulle posizioni {0, 4, 8, 12, ...}, il secondo sulle posizioni {1, 5, 9, 13, ...}, e così via.
La conseguenza è che un danno spaziale concentrato che occulta N byte fisici consecutivi si distribuisce in N/4 byte mancanti per ciascun blocco logico. Ogni blocco corregge fino a 14 errori, quindi l’occlusione funziona finché:
N/4 ≤ 14 ⇔ N ≤ 56
È lo stesso 56 di prima, ottenuto questa volta dalla geometria invece che dall’algebra. Non è una coincidenza. L’interleaving è stato progettato a posteriori (e poi standardizzato) per far coincidere il limite algebrico di Singleton con il limite pratico delle macchie reali. Il livello H del QR code esiste esattamente per ospitare un logo in mezzo. Un produttore di etichette industriali e un’agenzia di branding usano la stessa proprietà matematica per due motivi diversi.
Il caso medio è più clemente del caso peggiore. Singleton garantisce 14 errori per blocco nel caso peggiore, quando posizioni e valori sono scelti da un avversario. Per errori distribuiti uniformemente, la decodifica riesce con probabilità alta anche oltre la soglia, ma senza garanzia formale. Per macchie concentrate, l’interleaving lavora a favore del decoder: il caso peggiore non si materializza quasi mai.
Il laboratorio: dove cade davvero il cliff
Per misurare dove si rompe effettivamente la decodifica si genera un QR per https://pinperepette.github.io/signal.pirate/ a livello H e ci si disegna sopra un’ape stilizzata con primitive PIL: corpo giallo ellittico, due strisce nere, ali bianche, antenne, un occhio nero. L’ape sta dentro un cerchio bianco contornato che fa da zona logo pulita. Poi si fa uno sweep della dimensione lineare dell’ape dal 25% al 70% del lato del QR, verificando la decodifica con pyzbar a ogni passo.
# sweep dimensione ape vs decodifica
from qrcode import QRCode
from qrcode.constants import ERROR_CORRECT_H
from pyzbar.pyzbar import decode
qr = QRCode(error_correction=ERROR_CORRECT_H, box_size=20, border=4)
qr.add_data("https://pinperepette.github.io/signal.pirate/")
qr.make(fit=True)
base = qr.make_image(fill_color="black", back_color="white").convert("RGB")
for frac in (0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60):
img = overlay_bee(base, fraction=frac)
res = decode(img)
ok = bool(res) and res[0].data.decode() == URL
print(f"ape al {int(frac*100)}% lineare -> {'ok' if ok else 'FAIL'}")
Sweep fine al livello H, ape al centro con dimensione crescente, 20 prove per ogni size con posizione randomizzata di ±15 px per misurare stabilità, decoder pyzbar e cv2.QRCodeDetector in parallelo:
| Ape lineare | Area occlusa | pyzbar (20 prove) | cv2 (20 prove) | Stato |
|---|---|---|---|---|
| 30% | 9,0% | 20/20 OK | 20/20 OK | robusto |
| 35% | 12,2% | 20/20 OK | 20/20 OK | robusto |
| 40% | 16,0% | 20/20 OK | 20/20 OK | robusto |
| 42% | 17,6% | 20/20 OK | 20/20 OK | al margine |
| 44% | 19,4% | 13/20 | 20/20 OK | instabile |
| 45% | 20,2% | 9/20 | 20/20 OK | cliff |
| 46% | 21,2% | 1/20 | 18/20 | quasi morto |
| 48% | 23,0% | 0/20 | 0/20 | morto |
| 50% | 25,0% | 0/20 | 0/20 | morto |
| 55%+ | 30%+ | 0/20 | 0/20 | morto |
Prima di leggere il cliff, una nota di calcolo che vale oro: “lineare” non significa “area”. Un’ape al 45% del lato non copre il 45% del QR, ne copre il quadrato:
area occlusa = (lato)^2 = 0,45^2 = 20,2%
Il numero da confrontare con Singleton è l’area, non il lato. Per questo un logo che a occhio sembra enorme coesiste senza contraddizioni con “il livello H corregge il 30%”: un’ape al 45% lineare sta già sotto al 32,5% di Singleton, perché l’area occlusa è 20,2%, non 45%. Tutta la confusione sui QR con logo “esagerato” che funzionano lo stesso nasce esattamente qui.
Il secondo punto è che il cliff empirico cade prima del 32,5% teorico. La transizione da “robusto” a “morto” avviene in 4 punti percentuali di lato, fra il 42% e il 46% lineare. Roba di mezzo centimetro in un QR di 5 cm. La curva è quasi una step function, non una sigmoide morbida.
Il terzo punto riguarda i decoder: pyzbar e cv2 muoiono in tempi diversi. Al 46% lineare, pyzbar (basato su libzbar, decoder strict) è già al 5% di probabilità di successo, ma cv2 (la libreria che gira sotto Apple Camera e Google Lens) tiene ancora al 90%. cv2 ha tolleranza maggiore sui pattern strutturali, fa più lavoro di binarizzazione adattiva, perdona ape leggermente fuori centro. Il “QR funziona col telefono” vive proprio in quei due punti percentuali di gap fra i due decoder.
Quarto e ultimo punto: la posizione conta quanto la dimensione. Stessa ape al 44% lineare (la zona instabile del cliff), spostata in giro per il QR:
| Posizione | Cosa colpisce | pyzbar | cv2 |
|---|---|---|---|
| centro | alignment pattern | 10/20 | 20/20 |
| offset 200 px a sinistra | solo dati | 20/20 | 20/20 |
| offset 200 px sopra | timing pattern sfiorato | 19/20 | 0/20 |
| offset 100 px alto-sx | finder top-left | 0/20 | 0/20 |
| offset 100 px alto-dx | finder top-right | 0/20 | 0/20 |
| offset 100 px sotto | finder bottom-left | 0/20 | 0/20 |
Stessa identica ape, stesso identico QR: spostata in alto a sinistra muore istantaneamente, spostata 200 px a sinistra in zona dati pura decoda 20 volte su 20. La differenza fra “funziona sempre” e “non funziona mai” è di 30 pixel in alto a sinistra. C’è anche un caso bizzarro: spostata sopra-centro, pyzbar tiene (19/20) e cv2 muore (0/20). Stesso pixel, due decoder, due verdetti opposti. Sono motori diversi che inciampano su angoli diversi.
L’esperimento parallelo sul rumore sparso conferma la stessa lezione: 50 prove per soglia, ogni prova flippa un sottoinsieme casuale di moduli, anche strutturali. La probabilità di decodifica per tutti i quattro livelli L/M/Q/H scende sotto 0,5 già al 3,3% di moduli flippati. Su 2.025 moduli totali, 67 flip random bastano a uccidere il QR. Il motivo è meccanico: ogni flip ha il 14,7% di probabilità di colpire uno dei 297 moduli strutturali, e basta un solo finder rotto perché il decoder rinunci.
La lezione tecnica. Il 32,5% del livello H esiste solo nel mondo platonico dove gli errori sono distribuiti uniformemente sui 1.728 moduli dati, e nessuno tocca i 297 strutturali. Nel mondo reale, fatto di logo, macchie, graffi, foto storte, il cliff cade nettamente prima. Per il livello H su Version 6 il cliff effettivo è intorno al 20% di area centrale, ben sotto il 32,5% teorico. Reed-Solomon protegge il messaggio, non il foglio. Singleton vale solo per Reed-Solomon, e Reed-Solomon vale solo per la zona dati.
Quando il margine diventa una tela: la QR code art
Il margine fra l’area occlusa da un logo normale (intorno al 20%) e il limite teorico (32,5%) non è grande, ma qualcuno ha deciso di spenderlo tutto non per nascondere un marchio, bensì per fare arte. Il risultato è un genere visivo nato attorno al 2023: QR code art, immagini che a occhio sembrano dipinti, fotografie o scene fantasy, ma che inquadrate con un telefono scansionano regolarmente.
Se ne sono visti in giro parecchi: gatti dove la pelliccia è il pattern del QR, samurai con l’armatura fatta di moduli scuri e chiari, città viste dall’alto dove le case formano il codice. Sembrano impossibili. Non lo sono. Sono Reed-Solomon che lavora al limite del suo teorema, con una mano che spinge sulla parte estetica.
La pipeline: Stable Diffusion + ControlNet
La tecnica standard usa due reti neurali in tandem. La prima è Stable Diffusion, un modello di diffusion che parte da rumore gaussiano e in 30 passi lo trasforma in un’immagine guidata da un prompt testuale (per esempio “a pirate captain with a red bandana”). Il prompt influenza il sampling tramite cross-attention con CLIP. Senza altro intervento, l’output non ha nulla a che fare con un QR.
La seconda è ControlNet, una rete di condizionamento aggiuntiva. Prende come input un’immagine guida (nel caso QR un codice puro generato con la libreria qrcode) e a ogni passo di denoising inietta un bias nel latente di Stable Diffusion: dove la guida è scura, spinge i pixel verso valori scuri; dove è chiara, verso valori chiari. L’intensità di questa spinta è il parametro controlnet_conditioning_scale, tipicamente fra 1.0 e 1.6.
Per i QR esiste un ControlNet specializzato, QR Code Monster di monster-labs, addestrato su un dataset di immagini-QR che decodificano. Implicitamente, la rete ha imparato a stare sotto il 32,5% di errore. Non conosce il teorema di Singleton, ma il loss di training l’ha forzata a rispettarlo statisticamente: ogni immagine del dataset era una prova empirica del teorema, e il gradiente l’ha incorporata nei pesi.
Singleton come priore di rete. Da un punto di vista bayesiano, ControlNet QR Monster ha un priore implicito sulla soglia di errore. Quando genera, non massimizza la similarità con il QR (sarebbe un QR puro), ma massimizza la qualità estetica vincolata al fatto che la decodifica funzioni. Quel vincolo nascosto è il teorema di Singleton, mediato da migliaia di esempi di training.
Refinement chirurgico: forzare solo la struttura
Per controllare meglio la palette e gli elementi del soggetto conviene partire da un’immagine di riferimento invece che da un prompt testuale puro. Si usa StableDiffusionControlNetImg2ImgPipeline: SD parte dal latente codificato dell’init image, e ControlNet impone la struttura QR sopra. Il parametro strength controlla quanto SD si allontana dall’init (0.0 = init intatto, 1.0 = generazione da zero).
# SD 1.5 + ControlNet QR Monster, in modalita' img2img su CPU
controlnet = ControlNetModel.from_pretrained(
"monster-labs/control_v1p_sd15_qrcode_monster")
pipe = StableDiffusionControlNetImg2ImgPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", controlnet=controlnet).to("cpu")
init = Image.open("pirata-source.jpg").resize((768, 768))
qr = build_qr("https://pinperepette.github.io/signal.pirate/", 768)
img = pipe(prompt="cartoon pirate with eye patch and skull",
image=init, control_image=qr,
strength=0.92,
controlnet_conditioning_scale=1.65,
num_inference_steps=30).images[0]
Tempi su CPU a 48 thread: circa 14 minuti per immagine a 768×768. Memoria: picco 6-8 GB. Modelli scaricati una volta sola (circa 6 GB) e cachati in ~/.cache/huggingface/. Il sample che esce dalla pipeline pura, però, spesso cade fuori dalla regione decodificabile: il decoder rifiuta di leggerlo. QR Monster è addestrato a stare sotto Singleton in distribuzione, non in garanzia per il singolo seed. Si può rigenerare con altri seed e parametri, oppure portare il sample dentro al teorema con un passaggio deterministico.
Un QR non è un blocco uniforme di moduli liberi. Una parte è strutturale: serve al decoder per trovare il codice nell’immagine e calibrare la griglia. Senza quei moduli, anche un sample con i dati perfetti è invisibile. I moduli strutturali sono i tre finder pattern, il timing pattern, il pattern di allineamento centrale, il dark module in posizione (4V+9, 8), e i 30 bit di format info codificati BCH(15,5) e replicati. In tutto 297 moduli su 2.025 (14,7%). I rimanenti 1.728 sono area dati, dove vale Reed-Solomon.
La pipeline di refinement sovrappone il sample AI alla griglia del QR canonico. Per ogni modulo strutturale si applica un push forte verso il valore corretto. Per i moduli dati si pesa diversamente in base alla luminanza media del patch: se la media combacia con il valore canonico il modulo riceve un nudge leggero (preserva texture), altrimenti riceve un push più deciso (correzione). Valori operativi: f_s = 0,75 sui strutturali, f_w = 0,85 sui dati sbagliati, f_r = 0,20 sui dati corretti. Il refinement gira in RGB (non grayscale): per scurire si moltiplica il pixel per (1 − f), per schiarire si applica p + (255 − p) · f. Così la luminanza diventa QR-compliant ma la tinta del soggetto sopravvive. Al risultato finale si applica un blur gaussiano σ = 1.5 per ammorbidire i bordi e si aggiunge una quiet zone bianca di 80 px.
Il refinement non è un trucco contrario al teorema, è il teorema in azione. Singleton stabilisce che quali 56 byte si possono sbagliare nei dati è libero, ma i 297 moduli strutturali non sono codeword: appartengono alla cornice, e ogni decoder li pretende esatti. Reed-Solomon non li tocca, e nemmeno questo passaggio. Sui dati il refinement si limita a riportare la frazione di errore sotto il budget. Tutte le tecniche commerciali di QR art usano qualche variante di questa separazione fra struttura e dati, anche se nessuno la documenta apertamente.
Dove vive davvero la stessa matematica
Il paper di Reed e Solomon — “Polynomial Codes Over Certain Finite Fields”, sei pagine, Journal of the SIAM, 1960 — sta dietro a buona parte dei sistemi di memorizzazione e trasmissione costruiti dopo. Una mappa breve:
-
CD audio (1982). Codice CIRC (Cross-Interleaved Reed-Solomon), concatenazione di RS(32, 28) e RS(28, 24) su
GF(2^8), con interleaving profondo a quattro frame. Sopporta burst di errori fino a 4.000 bit, equivalenti a un graffio lineare di 2,5 mm sulla superficie del disco. -
DVD. Codice prodotto bidimensionale: RS(208, 192) sulle colonne e RS(182, 172) sulle righe di una matrice 192×172. Errori isolati corretti dal codice di riga, burst su una colonna corretti dal codice di colonna.
-
DVB-S satellitare. RS(204, 188) come outer code, codice convoluzionale rate 1/2 come inner. La concatenazione raggiunge prestazioni vicine al limite di Shannon su canale gaussiano.
-
Voyager 1 e 2 (1977). RS(255, 223) concatenato con Viterbi rate 1/2, constraint length 7. Voyager 2 ha trasmesso dati scientifici da Nettuno (4,5 ore-luce dalla Terra) con BER al ricevitore
5 × 10^-3e BER residuo post-decodifica10^-15. Quel10^-15è Reed-Solomon. -
NAND flash. Le prime generazioni usavano RS, le attuali sono passate a BCH o LDPC. Lo schema è cambiato perché gli errori della NAND sono diventati troppi e troppo correlati, e RS non è più ottimale. Ma per quindici anni ha retto.
-
ADSL e DSL. RS configurabile, parametri negoziati al link-up. Lo stesso codice che corregge il graffio sul CD corregge l’interferenza elettrica sul doppino di rame.
Sei pagine, due autori, una scelta algebrica precisa: polinomi su un campo finito di 256 elementi. Quasi ogni dispositivo che memorizza o trasmette informazione digitale dopo il 1982 contiene una versione operativa di quelle sei pagine.
Quello che il cartellino di un’arnia ha in comune con la Voyager
Il quadro complessivo che esce dal laboratorio è meno spettacolare di quanto la narrativa “il QR con il logo dentro che funziona per magia” suggerisce, e più interessante. Il livello H è una scelta deliberata di trade-off: si dimezzano i byte di dati utili (da 136 a 60, su Version 6) per pagarsi la possibilità di occludere il centro della matrice con un logo. Il livello H esiste per quello. Il marketing aziendale, l’apicoltore che etichetta le arnie e il produttore di biglietti del treno sfruttano la stessa proprietà matematica per ragioni diverse, senza nemmeno saperlo.
Il dato di laboratorio aggiunge un dettaglio che la specifica non racconta. Il cliff empirico cade prima del limite teorico: circa il 20% di area centrale, contro il 32,5% di Singleton. La differenza è il prezzo dei moduli strutturali, che non sono Reed-Solomon e che ogni decoder pretende esatti. Lo si vede nei test sulla posizione del logo: spostare un’ape di 30 pixel verso un finder pattern fa crollare la probabilità di decodifica da 100% a 0%. La cornice del codice non è negoziabile, e nessun teorema la protegge.
Il punto culturale è che Reed-Solomon è il pezzo di matematica più usato e meno citato dell’informatica applicata. Sta nei CD del 1982, nei DVD, nei dischi rigidi, nelle NAND flash di prima generazione, nelle linee ADSL, nei pacchetti DVB satellitari, nei segnali della Voyager 2 che arrivano da Nettuno con un errore residuo di 10^-15, e oggi nei QR code che entrano in pubblicità con dentro qualunque cosa, dalla mascotte aziendale al pirata generato da Stable Diffusion. Sei pagine pubblicate sessantacinque anni fa hanno tenuto in piedi il rumore di fondo dell’industria digitale.
Il messaggio non è nei byte. È in un polinomio di grado 42 che si rifiuta di morire.
#Adessonews seleziona nella rete articoli di particolare interesse.
Se vuoi leggere l’articolo completo clicca sul seguente link
Andrea Amani
Source link






