5.1. Objekt Image

Algoritmus pro zpracování obrazu prochází obrazem po jednom pixelu. V každé pozici provede něco jednoduchého – přečte hodnotu, porovná ji s prahem, zkombinuje ji s odpovídajícím pixelem druhého obrazu, zapíše výsledek zpět. Opakovány přes celý snímek jsou tato jednoduchá per-pixelová rozhodnutí tím, z čeho je vystavěna detekce hran, sledování blobů, dekódování QR Code i každá další klasická technika počítačového vidění. Aby tuto práci prováděl efektivně, musí algoritmus vědět, kde každý pixel leží v paměti, co hodnota každého pixelu vlastně znamená a na kterou část obrazu se má dívat. image.Image je objekt, který tyto informace organizuje.

Kapitola Vision Sensors skončila v okamžiku, kdy csi.CSI.snapshot() vrací výsledek. Cokoli, co stroj na straně kamery udělal pro vytvoření zachyceného snímku, je již hotovo; aplikace má Image v ruce a potřebuje vědět, co s ním.

5.1.1. Buffer a jeho vlastnosti

Uvnitř objektu Image je ukazatel na souvislý blok bajtů v paměti RAM a malá hlavička nesoucí tři údaje metadat: šířku obrazu v pixelech, jeho výšku v pixelech a pixelový formát, ve kterém jsou bajty uloženy. Bajty jsou samotné pixely, uložené v pořadí po řádcích (row-major) – nejprve všechny pixely horního řádku, poté všechny pixely druhého řádku a tak dále až dolů k poslednímu. Vlastnosti popisují, jak je číst.

Šířka a výška jsou prosté celočíselné počty. Pixelový formát je zajímavější vlastnost, protože určuje, kolik bajtů zabírá každý pixel a co tyto bajty kódují. Obraz ve stupních šedi nese jeden bajt na pixel obsahující hodnotu jasu. Obraz RGB565 nese dva bajty na pixel obsahující červenou, zelenou a modrou složku zabalené do 16bitového slova. Bayerův obraz nese jeden bajt na pixel, ale každý pixel je vzorkován přes jeden ze tří barevných filtrů zvolených podle jeho pozice v mozaice. Vision Sensors vyjmenovaly celý katalog; zde je podstatné to, že na každém objektu Image je nastaven právě jeden z těchto formátů a tato volba řídí výpočet bajtů na pixel i význam každého jednotlivého bajtu v bufferu.

S ukazatelem na buffer, šířkou, výškou a formátem vyplyne každá další vlastnost, kterou by algoritmus mohl chtít, jako krátký výpočet. Bajt, kterým začíná pixel (x, y), leží na offsetu (y * width + x) * bytes_per_pixel od začátku bufferu. Celkový počet bajtů je width * height * bytes_per_pixel. Adresa následujícího řádku níže je přesně width * bytes_per_pixel bajtů za začátkem toho aktuálního. Image zpřístupňuje tyto tři vlastnosti prostřednictvím prostých volání metod – width(), height(), format() – a navíc odvozenou hodnotu size přes size(). Metody jinde v modulu používají tyto hodnoty k provádění výpočtu offsetů samy; aplikační kód to musí dělat jen zřídka.

Rámeček s popiskem image.Image -- Python wrapper nahoře, se šipkou směřující dolů označenou "references" ke dvěma na sebe naskládaným rámečkům -- tenký rámeček hlavičky obsahující šířku, výšku a pixelový formát a silnější rámeček pixelového bufferu s řadou malých buněk představujících jednotlivé pixely. Popisek pod nimi poznamenává, že buffer ve výchozím stavu žije v haldě a ve frame bufferu, když je copy_to_fb true.

Image je malý Python wrapper, který ukazuje na souvislý blok paměti: hlavičku nesoucí šířku, výšku a pixelový formát, za níž následuje samotný pixelový buffer.

5.1.2. Odkud buffer pochází

Výchozím příběhem v celé této kapitole je ten, který Vision Sensors již pokryly: zachycený snímek přichází z snapshot, bajty leží ve frame bufferu kamery a vrácený Image na ně ukazuje. Pravidelně se objevují tři další způsoby, jak nějaký získat, a každý z nich znamená něco jiného o tom, kde buffer skončí.

Načtení ze souboru vypadá jako předání cesty konstruktoru: image.Image("/sdcard/saved.jpg"). Modul načte soubor do čerstvě alokovaného bufferu na haldě Pythonu. Soubory BMP, PGM a PPM se cestou dovnitř dekódují a výsledný Image nese nekomprimovaný pixelový formát. Soubory JPEG a PNG zůstávají komprimované – Image nese formát JPEG nebo PNG a buffer drží bajtový proud souboru v podstatě beze změny. Aby aplikace mohla provádět jakoukoli práci na úrovni pixelů s komprimovaným obrazem, převede jej nejprve přes to_rgb565() nebo to_grayscale() a v tomto převodu je místo, kde dekomprese – a odpovídající nafouknutí haldy, kde se 30 KB JPEG může stát 600 KB RGB565 – skutečně probíhá. Načítání ze souboru je nejužitečnější během vývoje, když je třeba algoritmus otestovat oproti známému referenčnímu snímku uloženému spolu se skriptem.

Vytvoření od nuly je případ plátna: image.Image(320, 240, image.RGB565) požádá modul, aby alokoval daný počet bajtů v daném formátu, vynuloval obsah a vrátil wrapper. Pixely zatím nic neznamenají – jsou všechny nulové – ale prázdný obraz je tahounem pro hrstku opakujících se vzorů: referenční snímky, od nichž se odečítá aktuální snímek, plátna, na nichž se skládají grafické překryvy, binární buffery, které se naplní a použijí jako masky.

Konstrukce z ndarray překlenuje opačný směr, z jakéhokoli numerického výpočtu zpět do modulu image. Předání float32 ulab.numpy.ndarray konstruktoru vytvoří Image, jehož rozměry odpovídají ndarray – dvouosý tvar (h, w) se stane obrazem ve stupních šedi, tříosý tvar (h, w, 3) se stane RGB565 – s hodnotami float škálovanými z 0.0255.0 do celočíselného rozsahu pixelů. Teplotní mapa (heatmap) neuronové sítě, číselné pole jakéhokoli druhu, cokoli vytvořené modulem ml nebo ulab se stane něčím, co může kreslicí a inspekční strana modulu image použít.

Všechny čtyři zdroje vracejí stejný druh objektu Image. Kód, který používá vrácený objekt, nikdy nemusí sledovat, odkud pochází.

5.1.3. Dva pohledy na bajty

Aplikační kód většinu času zachází s objektem Image jako s typovaným obrazovým objektem – věcí s pojmenovanými metodami. Druhá polovina příběhu je, že tentýž objekt se také transparentně jeví jako plochá sekvence bajtů pro jakékoli MicroPython API, které přijímá argument bytes. Tyto bajty nejsou kopií bufferu; jsou jeho přímým pohledem.

Toto uspořádání je tím, co dělá z odeslání zachyceného snímku z kamery záležitost jednoho řádku. Výpočet hashe, odeslání přes sériový port, přeposlání do síťového socketu – žádná z těchto operací nepotřebuje samostatný krok „převeď obraz na bajty“:

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

Bajtový pohled je ve výchozím stavu jen pro čtení, a to záměrně. Obrazové buffery jsou velké a někdy sdílené mezi vrstvami obrazového zásobníku, takže dát náhodnému buf[0] = 0 někde hluboko v zásobníku volání moc tiše jeden z nich poškodit by byla příliš ostrá hrana, než aby zůstala odhalená. Když je přístup na úrovni bajtů pro čtení i zápis tím, co aplikace skutečně potřebuje – například zápis kalibrační hodnoty do známého offsetu – bytearray() vrací samostatný, explicitně čtecí i zapisovatelný pohled na tutéž paměť, čímž v místě volání signalizuje záměr.

5.1.4. Kde buffer žije

Pixelové buffery jsou dostatečně velké na to, aby záleželo na tom, kde sedí v paměti RAM. Snímek QQVGA RGB565 má 160 × 120 × 2 = 38 400 bajtů; snímek VGA RGB565 má 614 400 bajtů; vstup 224 × 224 RGB565, který by mohl spotřebovat klasifikátor neuronové sítě, má kolem 100 KB. Halda Pythonu na nejmenších kamerách může mít po naběhnutí běhového prostředí jen pár desítek kilobajtů. Držet více než snímek nebo dva obrazových dat na haldě by z ní vytlačilo všechno ostatní.

Východiskem je, že obrazové buffery většinou nežijí na haldě Pythonu. Žijí ve vyhrazené oblasti paměti RAM, kterou Vision Sensors představily jako frame buffer – tutéž paměť, do níž DMA kamery zapisuje zachycené snímky a z níž náhled v IDE čte dokončené snímky. Většina operací na objektu Image modifikuje svůj zdroj na místě: algoritmus přečte pixely, rozhodne, zapíše nové hodnoty zpět a žádný samostatný výsledný obraz se nealokuje. Operace, které opravdu produkují samostatný výsledek – převody formátu a hrstka dalších – lze požádat, aby tento výsledek umístily do frame bufferu pomocí klíčového argumentu copy_to_fb. copy_to_fb=True dělá dvě věci najednou: umístí výsledný obraz do frame bufferu místo na haldu (čímž obejde tlak na haldu) a učiní z výsledku další snímek, který náhled v IDE zobrazí. Přidání copy_to_fb=True k poslednímu kroku roury, sledování, jak se výsledek objeví na obrazovce, a iterování odtud je jedním z nejužitečnějších ladicích idiomů ve zpracování obrazu.

S wrapperem držícím označený buffer, čtyřmi způsoby, jak jej přivést na svět, dvěma pohledy na jeho bajty a přepínačem rozhodujícím, kde nové buffery přistanou, už Image není záhadou. Zbývající základní otázky – jak se pojmenovává pozice pixelu, co každý pixel vlastně drží, jak omezit operaci na část obrazu – jsou postaveny na něm.