5.1. Objekt Image

Algoritam za obradu slike prolazi kroz sliku jedan po jedan piksel. Na svakom položaju radi nešto jednostavno – čita vrijednost, uspoređuje je s pragom, kombinira je s odgovarajućim pikselom druge slike, zapisuje rezultat natrag. Ponovljene preko cijele sličice, te jednostavne odluke po pikselu ono su iz čega su izgrađeni detekcija rubova, praćenje mrlja, dekodiranje QR-kodova i svaka druga klasična tehnika računalnog vida. Da bi taj posao obavio učinkovito, algoritam mora znati gdje svaki piksel leži u memoriji, što vrijednost svakog piksela zapravo znači i koji dio slike treba promatrati. image.Image je objekt koji organizira te informacije.

Vision Sensors završili su u trenutku kad csi.CSI.snapshot() vraća rezultat. Što god je mehanizam na strani kamere učinio kako bi proizveo snimljenu sličicu već je gotovo; aplikacija drži Image u ruci i treba znati što s njim učiniti.

5.1.1. Međuspremnik i njegova svojstva

Unutar Image nalazi se pokazivač na neprekinuti blok bajtova u RAM-u i malo zaglavlje koje nosi tri dijela metapodataka: širinu slike u pikselima, njezinu visinu u pikselima i format piksela u kojem su bajtovi. Bajtovi su sami pikseli, pohranjeni u poretku po redovima – najprije svi pikseli gornjeg reda, zatim svi pikseli drugog reda i tako dalje sve do dna. Svojstva opisuju kako ih čitati.

Širina i visina su obične cjelobrojne vrijednosti. Format piksela zanimljivije je svojstvo jer određuje koliko bajtova zauzima svaki piksel i što ti bajtovi kodiraju. Slika u sivim tonovima nosi jedan bajt po pikselu koji drži vrijednost svjetline. RGB565 slika nosi dva bajta po pikselu koji drže polja crvene, zelene i plave upakirana u 16-bitnu riječ. Bayer slika nosi jedan bajt po pikselu, ali svaki piksel uzorkovan je kroz jedan od tri filtra boja odabran prema njegovom položaju u mozaiku. Vision Sensors nabrojali su cijeli katalog; ovdje je važno da je točno jedan od tih formata postavljen na svakom Image, a izbor pokreće aritmetiku bajtova po pikselu i značenje svakog pojedinog bajta u međuspremniku.

S pokazivačem na međuspremnik, širinom, visinom i formatom, svako drugo svojstvo koje bi algoritam mogao htjeti proizlazi kao kratak izračun. Bajt kojim počinje piksel (x, y) nalazi se na pomaku (y * width + x) * bytes_per_pixel od početka međuspremnika. Ukupan broj bajtova je width * height * bytes_per_pixel. Adresa sljedećeg reda niže točno je width * bytes_per_pixel bajtova nakon početka trenutnog. Image izlaže tri svojstva kroz obične pozive metoda – width(), height(), format() – plus izvedeni size kroz size(). Metode drugdje u modulu koriste te vrijednosti kako bi same izračunale pomak; aplikacijski kod to rijetko mora.

Okvir s oznakom image.Image -- Python omotač na vrhu, sa strelicom koja pokazuje prema dolje s oznakom "references" prema dvama složenim okvirima -- tankom okviru zaglavlja koji drži širinu, visinu i format piksela te debljem okviru međuspremnika piksela s redom malih ćelija koje predstavljaju pojedine piksele. Natpis ispod napominje da međuspremnik prema zadanim postavkama živi na gomili (heap) a u međuspremniku slike kada je copy_to_fb postavljen na true.

Image je mali Python omotač koji pokazuje na neprekinuti blok memorije: zaglavlje koje nosi širinu, visinu i format piksela, nakon čega slijedi sam međuspremnik piksela.

5.1.2. Odakle dolazi međuspremnik

Zadana priča kroz ovo poglavlje ona je koju su Vision Sensors već obradili: snimljena sličica stiže iz snapshot, bajtovi se nalaze u međuspremniku slike kamere, a vraćeni Image pokazuje na njih. Redovito se pojavljuju tri druga načina dobivanja jednog, a svaki podrazumijeva nešto drukčije o tome gdje međuspremnik završava.

Učitavanje iz datoteke izgleda kao predaja putanje konstruktoru: image.Image("/sdcard/saved.jpg"). Modul čita datoteku u svježe alocirani međuspremnik na Python gomili (heap). BMP, PGM i PPM datoteke dekodiraju se pri ulazu, a rezultirajući Image nosi nekomprimirani format piksela. JPEG i PNG datoteke ostaju komprimirane – Image nosi format JPEG ili PNG, a međuspremnik drži tok bajtova datoteke u biti nepromijenjen. Da bi obavila bilo kakav rad na razini piksela na komprimiranoj slici, aplikacija je najprije pretvara kroz to_rgb565() ili to_grayscale(), a ta je pretvorba mjesto gdje se dekompresija – i odgovarajuće naduvavanje gomile, gdje 30 KB JPEG-a može postati 600 KB RGB565 – zapravo događa. Učitavanje iz datoteke najkorisnije je tijekom razvoja, kada algoritam treba testirati naspram poznate referentne sličice pohranjene uz skriptu.

Izgradnja jednog od nule slučaj je platna: image.Image(320, 240, image.RGB565) traži od modula da alocira toliko bajtova u tom formatu, postavi sadržaj na nulu i preda omotač natrag. Pikseli još ništa ne znače – svi su nula – ali prazna slika radni je konj za nekoliko ponavljajućih obrazaca: referentne sličice naspram kojih se oduzima trenutna sličica, platna na kojima se sastavljaju grafički preklopi, binarni međuspremnici koji se ispunjavaju i koriste kao maske.

Konstruiranje iz ndarray premošćuje u drugom smjeru, od bilo kojeg numeričkog izračuna natrag u modul image. Predaja float32 ulab.numpy.ndarray konstruktoru proizvodi Image čije dimenzije odgovaraju ndarray-u – dvoosni oblik (h, w) postaje slika u sivim tonovima, troosni oblik (h, w, 3) postaje RGB565 – s vrijednostima s pomičnim zarezom skaliranima iz 0.0255.0 u cjelobrojni raspon piksela. Toplinska karta neuronske mreže, numeričko polje bilo koje vrste, bilo što proizvedeno modulom ml ili ulab postaje nešto što strana crtanja i pregleda modula image može koristiti.

Sva četiri izvora vraćaju istu vrstu Image. Kod koji koristi vraćeni objekt nikada ne mora pratiti odakle je došao.

5.1.3. Dva pogleda na bajtove

Većinu vremena aplikacijski kod tretira Image kao tipizirani objekt slike – stvar s imenovanim metodama. Druga polovica priče jest da se isti objekt također pojavljuje, transparentno, kao ravni niz bajtova svakom MicroPython API-ju koji uzima bytes argument. Bajtovi nisu kopija međuspremnika; oni su izravni pogled na njega.

Taj raspored čini izguravanje snimljene sličice iz kamere stvari od jednog retka. Heširanje, slanje preko serijskog porta, prosljeđivanje mrežnoj utičnici – nijedno od toga ne treba zasebni korak „pretvori sliku u bajtove”:

import csi
import hashlib

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QQVGA)

img = csi0.snapshot()
uart.write(img)              # transmits the raw pixel bytes
hashlib.sha256(img)          # hashes the same bytes
sock.send(img)               # sends them over a socket

Pogled nalik bajtovima prema zadanim postavkama je samo za čitanje, namjerno. Međuspremnici slika su veliki i ponekad dijeljeni između slojeva slikovnog snopa, pa davanje slučajnom buf[0] = 0 negdje duboko u stogu poziva moći da jedan tiho ošteti previše je oštar rub da bi se ostavio izložen. Kada je čitanje-pisanje pristupa na razini bajtova ono što aplikacija zapravo treba – na primjer, upisivanje kalibracijske vrijednosti na poznati pomak – bytearray() vraća zaseban, eksplicitno pogled za čitanje i pisanje nad istom memorijom, naznačujući namjeru na mjestu poziva.

5.1.4. Gdje međuspremnik živi

Međuspremnici piksela dovoljno su veliki da je važno gdje sjede u RAM-u. QQVGA RGB565 sličica iznosi 160 × 120 × 2 = 38.400 bajtova; VGA RGB565 sličica iznosi 614.400 bajtova; 224 × 224 RGB565 ulaz koji bi klasifikator neuronske mreže mogao konzumirati iznosi oko 100 KB. Python gomila (heap) na najmanjim kamerama može biti tek nekoliko desetaka kilobajta nakon što se izvođenje pokrene. Držanje više od jedne ili dvije sličice slikovnih podataka na gomili istisnulo bi sve ostalo s nje.

Izlaz je u tome da međuspremnici slika uglavnom ne žive na Python gomili. Žive u namjenskoj regiji RAM-a koju su Vision Sensors predstavili kao međuspremnik slike – istu memoriju u koju DMA kamere upisuje snimljene sličice i iz koje IDE pregled čita gotove sličice. Većina operacija nad Image mijenja svoj izvor na mjestu: algoritam čita piksele, odlučuje, upisuje nove vrijednosti natrag i ne alocira se zasebna rezultatna slika. Operacije koje zaista proizvode zaseban rezultat – pretvorbe formata i nekolicina drugih – mogu se zatražiti da taj rezultat smjeste u međuspremnik slike kroz argument ključne riječi copy_to_fb. copy_to_fb=True radi dvije stvari odjednom: stavlja rezultatnu sliku u međuspremnik slike umjesto na gomilu (zaobilazeći pritisak na gomilu) i čini rezultat sljedećom sličicom koju će IDE pregled prikazati. Dodavanje copy_to_fb=True na završni korak cjevovoda, promatranje kako se rezultat pojavljuje na zaslonu i iteriranje odatle jedan je od najkorisnijih obrazaca otklanjanja pogrešaka u obradi slike.

S omotačem koji drži označeni međuspremnik, četiri načina da ga se dovede u postojanje, dva pogleda na njegove bajtove i prekidačem koji odlučuje gdje novi završavaju, Image više nije misterij. Preostala temeljna pitanja – kako se imenuje položaj piksela, što svaki piksel zapravo drži, kako ograničiti operaciju na njegov dio – izgrađena su povrh njega.