5.4. Citirea și scrierea pixelilor¶
Majoritatea operațiilor asupra unei imagini își ascund munca per-pixel în interiorul unui singur apel de metodă, unde buclele care parcurg fiecare pixel se execută la viteză nativă. Există însă cazuri în care codul aplicației dorește să acceseze direct un anumit pixel: pentru a citi ce se află la o anumită poziție, pentru a scrie o valoare nouă într-unul, pentru a eșantiona un singur punct pentru o etapă de calibrare sau pentru a depana o valoare la o locație cunoscută. Modulul image expune acest nivel de acces prin două forme de adresare, fiecare potrivindu-se unui mod diferit de a gândi despre locul unde se află un pixel.
5.4.1. Adresarea după coordonate¶
Cea mai naturală formă este cea pentru care Coordonatele au dezvoltat deja vocabularul: numirea unui pixel prin coordonatele sale carteziene (x, y). get_pixel() primește (x, y) și returnează valoarea de la acea poziție; set_pixel() primește aceleași (x, y) împreună cu o valoare și o scrie.
Ce returnează sau acceptă aceste apeluri depinde de formatul imaginii. Imaginile în tonuri de gri, binare și Bayer poartă o singură valoare per pixel – o luminozitate pentru tonuri de gri, un 0 sau 1 pentru binare, un singur eșantion de canal de culoare pentru Bayer – așa că get_pixel() returnează un singur întreg. RGB565 poartă trei canale de culoare împachetate în 16 biți, iar get_pixel le despachetează în mod implicit într-un tuplu (r, g, b), fiecare canal fiind mapat în intervalul 0 – 255.
Comportamentul implicit poate fi inversat la oricare dintre capete. Transmiterea rgbtuple=False către get_pixel pe o imagine RGB565 revine la cuvântul brut împachetat pe 16 biți – aceeași formă pe care o returnează indexul liniar și forma eficientă atunci când aplicația urmează să scrie aceeași valoare împachetată direct înapoi. Transmiterea rgbtuple=True pe o imagine cu un singur canal face opusul: valoarea stocată este convertită într-un tuplu RGB888 înainte de returnare, imaginile Bayer trecând printr-o etapă de debayering pe loc. Argumentul există pentru ca codul apelant să poată solicita pixeli într-un spațiu de culoare uniform, indiferent de modul în care imaginea subiacentă îi stochează.
Imaginile comprimate – JPEG și PNG – nu sunt acceptate de get_pixel sau set_pixel. Octeții lor nu reprezintă pixeli la poziții cunoscute, iar metodele generează o eroare în loc să returneze o valoare care nu ar însemna nimic.
În practică, tiparele arată astfel:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
Dacă coordonatele (x, y) solicitate se află în afara imaginii, get_pixel returnează None, iar set_pixel nu face nimic. Acest lucru este îngăduitor prin concepție: mulți algoritmi parcurg zonele apropiate de marginile unei imagini și indexează pentru scurt timp poziții din afara intervalului, iar o operație nulă silențioasă este mai puțin perturbatoare decât o excepție de fiecare dată când se întâmplă acest lucru.
5.4.2. Adresarea după index liniar¶
Cealaltă formă este adresarea pixelilor după poziția lor în tamponul (buffer) subiacent. Amintiți-vă structura tamponului: pixelii sunt stocați rând cu rând, mai întâi toți pixelii rândului de sus, apoi toți cei ai rândului următor și așa mai departe până jos. Acest aranjament înseamnă că fiecare pixel are un singur index întreg care numără de la 0 în colțul din stânga-sus și crește de-a lungul fiecărui rând pe rând. Pixelul de la coordonata (x, y) are indexul liniar y * width + x.
Pixelii sunt adresați atât prin coordonate carteziene (x, y), cât și printr-un index liniar care parcurge tamponul rând cu rând, de la stânga la dreapta.¶
Modulul image expune acest index prin notația obișnuită de subscript din Python: img[i] citește pixelul de la indexul liniar i, iar img[i] = value scrie unul. Ce returnează forma cu index este valoarea brută stocată pentru format, nu tuplul despachetat pe care get_pixel() îl returnează implicit. Această distincție contează deoarece formatul ales mai devreme decide cum arată valoarea brută:
Pixelii în tonuri de gri și Bayer revin ca întregi pe 8 biți.
Pixelii RGB565 și YUV422 revin ca întregi pe 16 biți – cuvântul împachetat.
Pixelii binari revin ca
0sau1.Pixelii JPEG și PNG revin ca întregi pe 8 biți, câte un octet din fluxul comprimat la un moment dat. Acele valori sunt opace – sunt bucăți dintr-o codificare comprimată, mai degrabă decât pixeli în vreun sens obișnuit.
Forma cu index se potrivește codului care gândește deja în termeni de deplasări în tampon: o buclă care parcurge fiecare pixel o singură dată, un algoritm care trebuie să sară cu câte un rând odată sau o porțiune de cod care traduce între structuri de tampon. Codul care gândește în termeni de coordonate x și y este mai bine deservit de get_pixel și set_pixel; cele două forme adresează aceiași pixeli prin modele mentale diferite.
Image este, de asemenea, iterabil. for v in img: parcurge tamponul în aceeași ordine de tip rând-major, producând valorile brute câte un pixel la un moment dat, iar len(img) este numărul de pixeli pentru formatele necomprimate sau numărul de octeți pentru fluxurile comprimate.
5.4.3. De ce procesarea per-pixel în Python este calea lentă¶
O observație practică despre care merită să fim sinceri. Parcurgerea unei imagini câte un pixel pe rând din Python este lentă. O imagine în tonuri de gri de 320 × 240 conține 76.800 de pixeli; apelarea get_pixel() pe fiecare dintre ei într-o buclă for execută milioane de instrucțiuni bytecode MicroPython pentru a face o muncă pe care o metodă nativă echivalentă ar putea-o finaliza în câteva sute de microsecunde. Acesta nu este un factor mic. Este diferența dintre un script care procesează cadre în timp real și unul care se târăște cu mult sub rata de cadre a camerei.
Aproape fiecare metodă de pe suprafața Image există pentru că există o versiune nativă, mai rapidă, a unui tipar comun per-pixel. O buclă care adună două imagini devine un singur apel nativ. O buclă care netezește fiecare pixel mediind-l cu vecinii săi devine altul. O buclă care clasifică fiecare pixel în raport cu un prag devine al treilea. Sarcina aplicației, de cele mai multe ori, este să recunoască ce metodă pe întreaga imagine se potrivește muncii pe care ar fi făcut-o bucla și să apeleze la aceasta în loc să scrie bucla manual.
Citirea și scrierea la nivel de pixel rămân instrumentul potrivit atunci când nimic altceva nu se potrivește – inserarea unei măsurători specifice înapoi în tampon, eșantionarea unei poziții pentru o etapă de calibrare, depanarea unei valori la o locație cunoscută. Ideea este că acestea reprezintă calea lentă, folosită atunci când metodele pe întreaga imagine nu au forma de care are nevoie aplicația, nu ca mod implicit de a opera asupra pixelilor.