5.3. Formate de pixeli

Un algoritm care detectează muchii se așteaptă ca fiecare pixel să conțină o valoare de luminozitate. Un algoritm care urmărește un obiect colorat se așteaptă ca fiecare pixel să poarte culoare. Un algoritm care execută închidere morfologică se așteaptă ca fiecare pixel să fie fie activ, fie inactiv. Formatul de pixeli pe care îl poartă o Image – unul dintre cele enumerate în catalogul de senzori de viziune – este ceea ce face ca aceste așteptări să poată fi verificate dinainte: formatul spune, în avans, în ce formă se află pixelii și ce algoritmi pot deci rula asupra lor fără un pas de conversie.

Această pagină tratează modul în care această constrângere se manifestă în practică. Care format este alegerea corectă depinde de ceea ce va face pipeline-ul, iar metodele de conversie între formate sunt modul prin care un pipeline care are nevoie de mai multe dintre ele înlănțuie etapele.

O stivă verticală de cinci fâșii etichetate ce ilustrează dispunerea octeților. BINARY arată un octet împărțit în opt celule de câte un singur bit, marcat "8 pixels per byte". GRAYSCALE arată trei celule etichetate de câte un octet, fiecare marcată "1 pixel". RGB565 arată doi octeți adiacenți cu câmpurile de biți RRRRR GGGGGG BBBBB etichetate "1 pixel". YUV422 arată patru celule de octeți etichetate Y0, U, Y1, V marcate "2 pixels". BAYER arată două rânduri de câte patru celule de octeți etichetate: R G R G pe rândul de sus, G B G B pe rândul de jos.

Cele cinci formate de pixeli necomprimate și modul în care li se împachetează octeții. JPEG și PNG nu sunt desenate aici deoarece sunt fluxuri comprimate de lungime variabilă, nu grile de pixeli de dimensiune fixă.

5.3.1. Calul de bătaie: tonurile de gri

Cea mai mare parte a viziunii artificiale clasice se reduce la lucrul cu valori de luminozitate. Detectarea muchiilor, potrivirea șabloanelor, decodarea AprilTag, estimarea fluxului optic, operatorii morfologici, analiza blob-urilor – toate, la nivelul la care operează algoritmii, se uită la cât de luminos este fiecare pixel și la modul în care luminozitatea se compară cu cea a pixelilor învecinați. Culoarea scenei este adesea utilă aplicației care îi apelează, dar algoritmii înșiși nu au nevoie de ea.

Formatul în tonuri de gri oferă algoritmilor exact acest lucru, fără supraîncărcare. Un octet pe pixel conține o valoare de luminozitate de la 0 (negru) până la 255 (alb). Formatul are jumătate din dimensiunea RGB565 și YUV422 și o treime din dimensiunea RGB888, astfel încât fiecare operație parcurge mai puține date – atât mai rapid, cât și cu mai puțină presiune asupra tamponului (buffer). Pe camerele mai mici, unde tamponul de cadre (frame buffer) concurează cu restul scriptului pentru RAM, această diferență de amprentă poate fi ceea ce decide dacă un pipeline încape măcar. Dacă nu culoarea este indiciul de care are nevoie algoritmul, tonurile de gri sunt răspunsul corect.

5.3.2. Culoare prin RGB565

Când culoarea este indiciul – urmărirea unui marcaj colorat, distingerea merelor roșii de cele verzi, identificarea unui element de interfață după nuanța sa – doi octeți pe pixel cumpără suficientă culoare pentru tipurile de clasificare pe care le realizează algoritmii. RGB565 este formatul de culoare implicit pe cameră și cel pe care îl așteaptă metodele care lucrează cu culoare.

Randarea unui cadru adnotat – desenarea casetelor de detectare, scrierea de text de diagnosticare, afișarea cadrului pe un ecran sau trimiterea lui către un vizualizator la distanță – impune de asemenea în mod natural RGB565. Previzualizarea din IDE, controlerele de afișaj de pe placă și majoritatea destinațiilor de rețea fie consumă formatul direct, fie convertesc din el ieftin.

5.3.3. Bayer ca format de stocare

O imagine Bayer este ieșirea brută a senzorului, înainte ca ISP-ul să o fi debayerizat într-o reprezentare de culoare finită. Fiecare pixel este un octet care conține un singur canal de culoare – cel pe care l-a lăsat să treacă filtrul de culoare din poziția respectivă din mozaic. Acest lucru face ca o imagine Bayer să aibă aceeași dimensiune ca o imagine în tonuri de gri și o treime din dimensiunea RGB888, ceea ce se aliniază cu scopul pentru care Bayer este de fapt util: stocarea mai multor cadre deodată când RAM-ul este constrângerea limitativă.

Capcana este că algoritmii din modulul image nu operează direct asupra imaginilor Bayer. Fără debayerizare, niciun pixel nu poartă suficientă informație pentru a face o judecată de culoare de unul singur, iar tiparele pe care le caută algoritmii – muchii, colțuri, blob-uri – ar fi distorsionate de mozaic. Singurele modalități de a citi sau modifica o imagine Bayer sunt get_pixel() și set_pixel(); orice altceva așteaptă o reprezentare finită.

Tiparul care rezultă este de a stoca cadrele ca Bayer atâta timp cât trebuie să stea într-o coadă și de a converti fiecare în tonuri de gri sau RGB565 chiar în momentul în care procesarea sa începe efectiv. Conversia costă cicluri de CPU, dar economisește RAM-ul care altfel ar fi blocat ținând cadre finite pe toată durata de viață a aplicației.

Notă

Singurele operații ale modulului image direct asupra pixelilor Bayer sunt get_pixel(), set_pixel() și calea de codare JPEG care alimentează previzualizarea din IDE sau un vizualizator la distanță. Desenarea, analiza și filtrarea necesită toate conversia prealabilă în tonuri de gri, RGB565 sau binar.

5.3.4. YUV422 pentru pipeline-urile care le vor pe ambele

YUV422 separă informația fiecărui pixel într-un canal de luminanță (Y) și două canale de crominanță (U și V), și subeșantionează crominanța astfel încât perechile de pixeli adiacenți să partajeze un singur U și un singur V. Octeții pe pixel se mediază la doi – la fel ca RGB565 – dar sunt dispuși astfel încât canalul Y să fie deja o imagine continuă în tonuri de gri pe 8 biți, situată la deplasamente cunoscute în tampon (buffer).

Această dispunere este exact ceea ce dorește un pipeline atunci când unele dintre etapele sale sunt lucru în tonuri de gri, iar altele au nevoie de culoare. Citirea directă a valorilor Y pentru etapele în tonuri de gri evită costul unei conversii explicite; canalele U și V sunt acolo când o etapă ulterioară are de fapt nevoie de culoare. În afara acestui tipar specific, RGB565 este de obicei alegerea mai simplă pentru culoare, iar tonurile de gri sunt alegerea mai simplă pentru lucrul exclusiv cu luminozitate – valoarea lui YUV422 vine din faptul că este bun la ambele în același timp.

Notă

Modulul image operează asupra YUV422 într-un mod mai limitat decât asupra tonurilor de gri, RGB565 sau binar – citiri directe ale canalului Y pentru lucrul în tonuri de gri și calea de codare JPEG care alimentează previzualizarea din IDE sau un vizualizator la distanță. Metodele care lucrează cu culoare așteaptă RGB565; cadrele YUV422 necesită o conversie explicită înainte de analiza de culoare sau desenare.

5.3.5. Binar, măști și ieșire cu prag aplicat

O imagine binară este un bit pe pixel: fiecare pixel este fie 0, fie 1. Formatul apare rareori ca o captură de senzor; în schimb, apare ca ieșire naturală a aplicării pragului (unde un test de culoare sau de luminozitate clasifică fiecare pixel în „da, se potrivește” sau „nu, nu se potrivește”) și ca intrare naturală pentru operațiile morfologice și pentru argumentul mask pe care îl acceptă multe metode.

Avantajul practic al formatului este dimensiunea sa. O imagine binară are o optime din amprenta unei imagini în tonuri de gri, astfel încât purtarea unei măști mari – o alegere per pixel a pozițiilor pe care ar trebui să le atingă o operație ulterioară – este ieftină. Faptul că multe operații acceptă o imagine binară ca argument cu cuvânt-cheie mask= este cealaltă față a aceleiași idei: formatul este mic, iar înlănțuirea ieșirii binare a unei etape în intrarea de mască a alteia este un tipar de pipeline frecvent.

5.3.6. JPEG și PNG la graniță

Obiectele Image JPEG și PNG sunt diferite de celelalte din catalog. Ele nu sunt grile de pixeli; sunt fluxuri de octeți comprimate care codează date de pixeli într-o formă pe care operațiile la nivel de pixel nu o pot citi. Apelarea get_pixel() asupra unui JPEG nu returnează pixelul de la o poziție; pixelul nu se află despachetat nicăieri în tampon (buffer) pentru ca metoda să îl preia.

JPEG și PNG apar la granița procesării imaginii, unde datele de pixeli părăsesc sau intră în cameră în formă comprimată. Salvarea unui cadru pe disc ca JPEG păstrează fișierul mic; trimiterea unui cadru printr-o rețea ca JPEG păstrează transmisia ieftină; încărcarea unui cadru de referință dintr-un fișier JPEG îi permite să stea pe disc într-o formă mult mai mică decât ar fi pixelii bruți. Pentru oricare dintre aceste cazuri de utilizare, reprezentarea comprimată este răspunsul corect. Pentru a face însă orice procesare efectivă asupra unui JPEG, aplicația îl convertește mai întâi într-un format utilizabil – iar acea conversie este locul unde octeții comprimați sunt expandați în pixeli și unde se produce de fapt umflarea tamponului (un JPEG de 30 KB poate deveni 600 KB de RGB565).

5.3.7. Conversia între formate

Calea de conversie este ceea ce coase formate diferite într-un singur pipeline. Cinci metode ale clasei Image iau o imagine existentă și returnează una nouă într-un format diferit:

  • to_grayscale() produce o imagine cu un singur octet pe pixel, formatul pe care îl vor algoritmii clasici.

  • to_rgb565() produce formatul de culoare cu doi octeți pe pixel pe care îl vorbesc atât metodele care lucrează cu culoare, cât și previzualizarea din IDE.

  • to_bitmap() produce o imagine binară de un bit, formatul pe care îl acceptă morfologia și argumentele mask.

  • to_jpeg() produce o imagine comprimată JPEG potrivită pentru salvare sau transmisie.

  • to_png() produce o imagine comprimată PNG atunci când codarea fără pierderi este preferată în locul fișierelor mai mici ale JPEG.

Fiecare conversie rulează în loc în mod implicit: tamponul imaginii sursă este suprascris cu rezultatul convertit, iar pixelii originali ai sursei dispar după ce apelul revine. Aceasta este opțiunea cea mai ieftină atât pentru CPU, cât și pentru memorie, și este răspunsul corect atunci când cadrul sursă nu va mai fi necesar pentru altceva.

Când sursa este încă necesară – când o etapă ulterioară a pipeline-ului trebuie să vadă cadrul original – două argumente cu cuvânt-cheie suprascriu comportamentul implicit în loc. copy=True alocă un tampon separat pentru imaginea convertită pe heap-ul Python și lasă sursa intactă. copy_to_fb=True face aceeași alocare, dar o pune în tamponul de cadre (frame buffer) în loc de heap – ceea ce alege o aplicație atunci când imaginea convertită trebuie să ajungă în previzualizarea din IDE, deoarece IDE-ul citește din tamponul de cadre (frame buffer).

Alte două metode produc imagini RGB565 colorate printr-o paletă în loc de o conversie directă. to_rainbow() mapează fiecare valoare de intrare cu un singur canal la o culoare de-a lungul unui gradient lin care parcurge spectrul vizibil. to_ironbow() mapează fiecare valoare de intrare la paleta neliniară de imagistică termică care merge de la negru, prin roșuri și portocalii întunecate, până la alb. Ambele sunt instrumente de vizualizare, nu de măsurare; scopul este de a face o imagine cu un singur canal, ale cărei valori brute ar fi altfel invizibile pentru ochi, lizibilă dintr-o privire.

5.3.8. Dimensiunea tamponului

Un ultim detaliu despre formate care merită făcut explicit. size() raportează întotdeauna dimensiunea tamponului de octeți, nu numărul de pixeli. Pentru formatele necomprimate aceasta decurge direct din dimensiuni și din numărul de octeți pe pixel: width * height * bytes_per_pixel. Pentru JPEG și PNG este dimensiunea fluxului comprimat, care variază de la cadru la cadru în funcție de ceea ce conține scena. Codul care alocă tampoane din bugete de octeți folosește size() pentru primul caz; codul care transmite cadre comprimate din cameră îl citește după fiecare comprimare pentru a ști câți octeți conține efectiv fluxul.