5.1. Az Image objektum¶
A képfeldolgozó algoritmus képpontonként halad végig a képen. Minden pozícióban valami egyszerűt végez – beolvas egy értéket, összehasonlítja egy küszöbértékkel, kombinálja egy másik kép megfelelő képpontjával, majd visszaír egy eredményt. Egy teljes képkockán át megismételve ezekből az egyszerű, képpontonkénti döntésekből épül fel az éldetektálás, a foltkövetés, a QR-kód-dekódolás és minden más klasszikus számítógépes látási technika. Ahhoz, hogy ezt a munkát hatékonyan elvégezze, az algoritmusnak tudnia kell, hol helyezkedik el az egyes képpontok a memóriában, mit jelent valójában az egyes képpontok értéke, és a kép mely részét kell vizsgálnia. Az image.Image az az objektum, amely ezeket az információkat rendszerezi.
A Vision Sensors abban a pillanatban ért véget, amikor a csi.CSI.snapshot() visszatér. Bármit is tett a kamera oldali gépezet a rögzített képkocka előállításáért, az már megtörtént; az alkalmazás kezében van az Image, és tudnia kell, mit kezdjen vele.
5.1.1. A puffer és tulajdonságai¶
Az Image belsejében egy mutató található, amely a RAM-ban lévő, egybefüggő bájttömbre mutat, valamint egy kis fejléc, amely három metaadatot hordoz: a kép szélességét képpontban, a magasságát képpontban és a bájtok képpontformátumát. A bájtok maguk a képpontok, sorfolytonos (row-major) sorrendben tárolva – előbb a legfelső sor összes képpontja, majd a második sor összes képpontja, és így tovább lefelé egészen az aljáig. A tulajdonságok írják le, hogyan kell ezeket olvasni.
A szélesség és a magasság egyszerű egész számú darabszám. A képpontformátum az érdekesebb tulajdonság, mivel ez határozza meg, hogy egy képpont hány bájtot foglal el, és hogy ezek a bájtok mit kódolnak. Egy szürkeárnyalatos kép képpontonként egy bájtot hordoz, amely fényerőértéket tartalmaz. Egy RGB565 kép képpontonként két bájtot hordoz, amelyek egy 16 bites szóba csomagolt piros, zöld és kék mezőket tartalmaznak. Egy Bayer kép képpontonként egy bájtot hordoz, de minden képpontot a mozaikban elfoglalt pozíciója alapján kiválasztott három színszűrő egyikén keresztül mintáznak. A Vision Sensors felsorolta a teljes katalógust; itt az számít, hogy minden Image objektumon pontosan egy ilyen formátum van beállítva, és ez a választás vezérli a képpontonkénti bájtszámítást, valamint a pufferben lévő bármely egyetlen bájt jelentését.
A pufferre mutató mutatóval, a szélességgel, a magassággal és a formátummal minden más tulajdonság, amelyre egy algoritmusnak szüksége lehet, rövid számításként adódik. A (x, y) képpontot kezdő bájt a puffer elejétől számított (y * width + x) * bytes_per_pixel eltolásnál található. A teljes bájtszám width * height * bytes_per_pixel. A következő sor lefelé pontosan width * bytes_per_pixel bájttal az aktuális kezdete után helyezkedik el. Az Image a három tulajdonságot egyszerű metódushívásokon keresztül teszi elérhetővé – width(), height(), format() –, valamint a származtatott size értéket a size() révén. A modul máshol lévő metódusai ezeket az értékeket használják, hogy maguk végezzék el az eltolásszámítást; az alkalmazáskódnak erre ritkán van szüksége.
Az Image egy kis Python wrapper, amely egy egybefüggő memóriablokkra mutat: egy fejléc, amely a szélességet, a magasságot és a képpontformátumot hordozza, majd maga a képpont-puffer.¶
5.1.2. Honnan származik a puffer¶
Az alapértelmezett történet ebben a fejezetben végig az, amelyet a Vision Sensors már lefedett: egy rögzített képkocka érkezik a snapshot hívásból, a bájtok a kamera képkocka-pufferében vannak, és a visszaadott Image rájuk mutat. Három másik mód is rendszeresen felmerül, amellyel ilyenhez juthatunk, és mindegyik mást jelent abból a szempontból, hogy hol köt ki a puffer.
A fájlból való betöltés úgy néz ki, mint egy útvonal átadása a konstruktornak: image.Image("/sdcard/saved.jpg"). A modul beolvassa a fájlt egy frissen lefoglalt pufferbe a Python kupacon (heap). A BMP, PGM és PPM fájlok dekódolásra kerülnek beolvasás közben, és az eredményül kapott Image tömörítetlen képpontformátumot hordoz. A JPEG és PNG fájlok tömörítve maradnak – az Image a JPEG vagy a PNG formátumot hordozza, és a puffer lényegében változatlanul tárolja a fájl bájtfolyamát. Ahhoz, hogy bármilyen képpontszintű munkát végezzünk egy tömörített képen, az alkalmazás először átalakítja azt a to_rgb565() vagy a to_grayscale() segítségével, és ezen átalakítás során történik meg ténylegesen a kicsomagolás – és az ennek megfelelő kupacfelfúvódás, ahol egy 30 KB-os JPEG-ből 600 KB-nyi RGB565 válhat. A fájlból való betöltés leginkább a fejlesztés során hasznos, amikor egy algoritmust egy ismert, a szkript mellett tárolt referencia-képkockával szemben kell tesztelni.
A nulláról való felépítés a vászon esete: az image.Image(320, 240, image.RGB565) arra kéri a modult, hogy foglaljon le annyi bájtot az adott formátumban, nullázza ki a tartalmát, és adja vissza a wrappert. A képpontok még nem jelentenek semmit – mind nullák –, de az üres kép a sokoldalú munkaeszköz néhány visszatérő mintához: referencia-képkockák, amelyekből az aktuális képkockát kivonjuk, vászonok, amelyekre grafikus rávetítéseket komponálunk, bináris pufferek, amelyeket feltöltünk és maszkként használunk.
Az ndarray-ból való konstruálás a másik irányba hidal át, bármilyen numerikus számítástól vissza a kép modulba. Egy float32 típusú ulab.numpy.ndarray átadása a konstruktornak olyan Image objektumot hoz létre, amelynek méretei megegyeznek az ndarray méreteivel – egy két tengelyű (h, w) alak szürkeárnyalatos képpé válik, egy három tengelyű (h, w, 3) alak RGB565 képpé –, ahol a lebegőpontos értékek a 0.0 – 255.0 tartományból az egész képpont-tartományba skálázódnak. Egy neurális hálózati hőtérkép, bármilyen numerikus tömb, bármi, amit az ml vagy az ulab állít elő, olyasvalamivé válik, amit a kép modul rajzolási és vizsgálati oldala fel tud használni.
Mind a négy forrás ugyanolyan típusú Image objektumot ad vissza. A visszaadott objektumot használó kódnak soha nem kell nyomon követnie, honnan származik.
5.1.3. Két nézet a bájtok felett¶
Az alkalmazáskód az Image objektumot többnyire típusos képobjektumként kezeli – egy nevesített metódusokkal rendelkező dolognak. A történet másik fele az, hogy ugyanaz az objektum átlátszóan, lapos bájtsorozatként is megjelenik bármely olyan MicroPython API számára, amely bytes argumentumot vár. A bájtok nem a puffer másolatai; közvetlen nézetek arra.
Ez az elrendezés teszi egysorossá egy rögzített képkocka kamerából való kiküldését. A kivonatolása (hashelés), soros porton való elküldése, hálózati socketre való továbbítása – ezek egyikéhez sincs szükség külön „a kép bájtokká alakítása” lépésre:
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
A bájtszerű nézet alapértelmezés szerint csak olvasható, méghozzá szándékosan. A képpufferek nagyok, és néha megosztottak a képalkotó verem rétegei között, ezért túl éles perem lenne fedezetlenül hagyni, ha egy hívásverem mélyén lévő, gondatlan buf[0] = 0 képes lenne csendben megrongálni egyet. Amikor az alkalmazásnak ténylegesen olvasható-írható bájtszintű hozzáférésre van szüksége – például egy kalibrációs érték beírására egy ismert eltolásra –, a bytearray() egy különálló, kifejezetten olvasható-írható nézetet ad vissza ugyanazon memória felett, jelezve a szándékot a hívás helyén.
5.1.4. Hol él a puffer¶
A képpufferek elég nagyok ahhoz, hogy számítson, hol helyezkednek el a RAM-ban. Egy QQVGA RGB565 képkocka 160 × 120 × 2 = 38 400 bájt; egy VGA RGB565 képkocka 614 400 bájt; egy 224 × 224 RGB565 bemenet, amelyet egy neurális hálózati osztályozó felhasználhat, körülbelül 100 KB. A legkisebb kamerákon a Python kupac (heap) a futtatókörnyezet elindulása után csak néhány tucat kilobájt lehet. Ha egy-két képkockánál több képadatot tartanánk a kupacon, az mindent mást leszorítana róla.
A megoldás az, hogy a képpufferek többnyire nem a Python kupacon (heap) élnek. A RAM dedikált területén élnek, amelyet a Vision Sensors frame buffer (képkocka-puffer) néven vezetett be – ugyanaz a memória, amelybe a kamera DMA-ja a rögzített képkockákat írja, és amelyből az IDE előnézete a kész képkockákat olvassa. Az Image objektumon végzett legtöbb művelet helyben módosítja a forrását: az algoritmus beolvassa a képpontokat, döntést hoz, új értékeket ír vissza, és nem foglalódik le külön eredménykép. Azok a műveletek, amelyek valóban külön eredményt állítanak elő – a formátumkonverziók és néhány másik –, megkérhetők arra, hogy ezt az eredményt a képkocka-pufferben helyezzék el a copy_to_fb kulcsszavas argumentum révén. A copy_to_fb=True két dolgot tesz egyszerre: az eredményképet a kupac helyett a képkocka-pufferbe teszi (megkerülve a kupacnyomást), és az eredményt teszi a következő képkockává, amelyet az IDE előnézete megjelenít. Egy folyamat utolsó lépésére ráakasztani a copy_to_fb=True argumentumot, figyelni, ahogy az eredmény megjelenik a képernyőn, és onnan iterálni a képfeldolgozás egyik leghasznosabb hibakeresési fogása.
Egy címkézett puffert tartó wrapperrel, négy módszerrel, amellyel ilyet létrehozhatunk, két nézettel a bájtjai felett, és egy kapcsolóval, amely eldönti, hová kerülnek az újak, az Image többé nem rejtély. A megmaradó alapozó kérdések – hogyan nevezzük meg egy képpont pozícióját, mit tartalmaz valójában az egyes képpontok, hogyan korlátozzuk egy műveletet egy kép egy részére – erre épülnek.