5.1. Image-objekti

Kuvankäsittelyalgoritmi kulkee kuvan läpi pikseli kerrallaan. Kussakin kohdassa se tekee jotain yksinkertaista – lukee arvon, vertaa sitä kynnysarvoon, yhdistää sen toisen kuvan vastaavaan pikseliin tai kirjoittaa tuloksen takaisin. Koko kehyksen yli toistettuna nämä yksinkertaiset pikselikohtaiset päätökset ovat se, mistä reunantunnistus, blob-seuranta, QR-koodien purku ja kaikki muut klassiset konenäkötekniikat rakentuvat. Tämän työn tekemiseksi tehokkaasti algoritmin täytyy tietää, missä kukin pikseli sijaitsee muistissa, mitä kunkin pikselin arvo todella tarkoittaa ja mihin osaan kuvaa sen tulisi kohdistua. image.Image on objekti, joka järjestää tämän tiedon.

Vision Sensors päättyi hetkeen, jolloin csi.CSI.snapshot() palauttaa. Mitä tahansa kameran puolen koneisto tekikin tuottaakseen kaapatun kehyksen, on jo tehty; sovelluksella on Image kädessään ja sen täytyy tietää, mitä sillä tehdään.

5.1.1. Puskuri ja sen ominaisuudet

Image-objektin sisällä on osoitin yhtenäiseen tavulohkoon RAM-muistissa sekä pieni otsake, joka kantaa kolmea metatietoa: kuvan leveyden pikseleinä, sen korkeuden pikseleinä ja pikseliformaatin, jossa tavut ovat. Tavut ovat itse pikseleitä, tallennettuina riviensisäisessä järjestyksessä – ensin kaikki ylimmän rivin pikselit, sitten kaikki toisen rivin pikselit ja niin edelleen aina alimmalle riville saakka. Ominaisuudet kuvaavat, miten ne luetaan.

Leveys ja korkeus ovat tavallisia kokonaislukuja. Pikseliformaatti on mielenkiintoisempi ominaisuus, koska se määrittää, montako tavua kukin pikseli vie ja mitä nuo tavut koodaavat. Harmaasävykuvassa on yksi tavu pikseliä kohti, joka sisältää kirkkausarvon. RGB565-kuvassa on kaksi tavua pikseliä kohti, jotka sisältävät punaisen, vihreän ja sinisen kentät pakattuina 16-bittiseen sanaan. Bayer-kuvassa on yksi tavu pikseliä kohti, mutta kukin pikseli näytteistetään yhden kolmesta värisuotimesta läpi, joka valitaan sen sijainnin mukaan mosaiikissa. Vision Sensors luetteli koko valikoiman; tässä olennaista on, että täsmälleen yksi näistä formaateista on asetettu jokaiselle Image-objektille, ja valinta ohjaa tavua-per-pikseli-laskentaa sekä minkä tahansa yksittäisen tavun merkitystä puskurissa.

Kun käytössä on osoitin puskuriin, leveys, korkeus ja formaatti, jokainen muu ominaisuus, jota algoritmi saattaa tarvita, seuraa lyhyenä laskutoimituksena. Tavu, joka aloittaa pikselin (x, y), sijaitsee puskurin alusta laskettuna siirtymässä (y * width + x) * bytes_per_pixel. Tavujen kokonaismäärä on width * height * bytes_per_pixel. Seuraavan rivin osoite on täsmälleen width * bytes_per_pixel tavua nykyisen rivin alun jälkeen. Image paljastaa nämä kolme ominaisuutta tavallisten metodikutsujen kautta – width(), height(), format() – sekä johdetun size-arvon metodilla size(). Muut moduulin metodit käyttävät näitä arvoja tehdäkseen siirtymälaskennan itse; sovelluskoodin tarvitsee harvoin tehdä niin.

Laatikko nimeltä image.Image -- Python-kääre ylimpänä, ja siitä alaspäin osoittava nuoli nimeltä "references" kahteen päällekkäiseen laatikkoon -- ohut otsakelaatikko, joka pitää sisällään leveyden, korkeuden ja pikseliformaatin, sekä paksumpi pikselipuskurilaatikko, jossa on rivi pieniä soluja, jotka edustavat yksittäisiä pikseleitä. Alla oleva kuvateksti huomauttaa, että puskuri sijaitsee oletuksena keossa ja kehyspuskurissa, kun copy_to_fb on tosi.

Image on pieni Python-kääre, joka osoittaa yhtenäiseen muistilohkoon: otsake, joka kantaa leveyttä, korkeutta ja pikseliformaattia, ja sen perässä itse pikselipuskuri.

5.1.2. Mistä puskuri tulee

Tämän luvun läpi kulkeva oletustarina on se, jonka Vision Sensors jo kävi läpi: kaapattu kehys saapuu snapshot-kutsusta, tavut ovat kameran kehyspuskurissa, ja palautettu Image osoittaa niihin. Kolme muuta tapaa hankkia sellainen tulee säännöllisesti vastaan, ja kukin niistä merkitsee jotain erilaista sen suhteen, missä puskuri lopulta päätyy.

Tiedostosta lataaminen näyttää siltä, että konstruktorille annetaan polku: image.Image("/sdcard/saved.jpg"). Moduuli lukee tiedoston tuoreeseen, Python-keolle varattuun puskuriin. BMP-, PGM- ja PPM-tiedostot puretaan sisääntulon yhteydessä, ja syntyvä Image kantaa pakkaamatonta pikseliformaattia. JPEG- ja PNG-tiedostot pysyvät pakattuina – Image kantaa formaattia JPEG tai PNG, ja puskuri sisältää tiedoston tavuvirran olennaisesti muuttumattomana. Tehdäkseen mitä tahansa pikselitason työtä pakatulle kuvalle sovellus muuntaa sen ensin metodilla to_rgb565() tai to_grayscale(), ja juuri tuossa muunnoksessa purkaminen – ja vastaava keon paisuminen, jossa 30 KB:n JPEG voi muuttua 600 KB:ksi RGB565:ta – todella tapahtuu. Tiedostosta lataaminen on hyödyllisintä kehityksen aikana, kun algoritmia täytyy testata tunnettua referenssikehystä vasten, joka on tallennettu skriptin viereen.

Tyhjästä rakentaminen on kankaan tapaus: image.Image(320, 240, image.RGB565) pyytää moduulia varaamaan tuon määrän tavuja kyseisessä formaatissa, nollaamaan sisällön ja antamaan käärekohteen takaisin. Pikselit eivät vielä tarkoita mitään – ne ovat kaikki nollia – mutta tyhjä kuva on työjuhta kourallisessa toistuvia kuvioita: referenssikehykset, joista nykyinen kehys vähennetään, kankaat, joille grafiikkapäällykset kootaan, sekä binääripuskurit, jotka täytetään ja joita käytetään maskeina.

ndarray-rakenteesta rakentaminen toimii sillanrakentajana toiseen suuntaan, mistä tahansa numeerisesta laskennasta takaisin image-moduuliin. float32-tyyppisen ulab.numpy.ndarray-rakenteen antaminen konstruktorille tuottaa Image-objektin, jonka mitat vastaavat ndarray-rakennetta – kaksiakselinen (h, w) -muoto muuttuu harmaasävykuvaksi, kolmiakselinen (h, w, 3) -muoto muuttuu RGB565:ksi – liukulukuarvoilla skaalattuina väliltä 0.0255.0 kokonaislukupikselien alueelle. Neuroverkon lämpökartta, minkä tahansa tyyppinen numeerinen taulukko, mikä tahansa ml- tai ulab-moduulin tuottama muuttuu joksikin, mitä image-moduulin piirto- ja tarkastelupuoli voi käyttää.

Kaikki neljä lähdettä antavat takaisin samanlaisen Image-objektin. Palautettua objektia käyttävän koodin ei koskaan tarvitse seurata, mistä se tuli.

5.1.3. Kaksi näkymää tavuihin

Suurimman osan ajasta sovelluskoodi kohtelee Image-objektia tyypitettynä kuvaobjektina – asiana, jolla on nimettyjä metodeja. Tarinan toinen puoli on se, että sama objekti esiintyy myös, läpinäkyvästi, litteänä tavujonona mille tahansa MicroPython-API:lle, joka ottaa bytes-argumentin. Tavut eivät ole kopio puskurista; ne ovat suora näkymä siihen.

Tämä järjestely on se, mikä tekee kaapatun kehyksen työntämisestä ulos kamerasta yhden rivin tehtävän. Sen hajauttaminen, lähettäminen sarjaportin kautta, edelleenvälittäminen verkkosokettiin – mikään näistä ei tarvitse erillistä ”muunna kuva tavuiksi” -vaihetta:

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

Tavumainen näkymä on oletuksena vain luku -näkymä, tarkoituksella. Kuvapuskurit ovat suuria ja toisinaan jaettu kuvantamispinon kerrosten välillä, joten antaa huolimattomalle buf[0] = 0 -lauseelle jossain syvällä kutsupinossa valta hiljaa turmella sellainen on liian terävä reuna jätettäväksi paljaaksi. Kun luku-kirjoitus-tason tavukäyttö on se, mitä sovellus todella tarvitsee – esimerkiksi kalibrointiarvon kirjoittaminen tunnettuun siirtymään – bytearray() palauttaa erillisen, eksplisiittisesti luku-kirjoitus-näkymän samaan muistiin, viitoittaen aikomuksen kutsukohdassa.

5.1.4. Missä puskuri sijaitsee

Pikselipuskurit ovat tarpeeksi suuria, että sillä, missä ne sijaitsevat RAM-muistissa, on merkitystä. QQVGA RGB565 -kehys on 160 × 120 × 2 = 38 400 tavua; VGA RGB565 -kehys on 614 400 tavua; 224 × 224 RGB565 -syöte, jonka neuroverkkoluokitin saattaa kuluttaa, on noin 100 KB. Python-keko pienimmissä kameroissa voi olla vain muutaman kymmenen kilotavun suuruinen sen jälkeen, kun ajoympäristö on käynnistynyt. Useamman kuin yhden tai kahden kehyksen kuvadatan pitäminen keossa tunkisi kaiken muun pois sieltä.

Ulospääsy on se, että kuvapuskurit pääosin eivät sijaitse Python-keossa. Ne sijaitsevat siinä RAM-muistin omistetussa alueessa, jonka Vision Sensors esitteli kehyspuskurina – samassa muistissa, johon kameran DMA kirjoittaa kaapatut kehykset ja josta IDE:n esikatselu lukee valmiit kehykset. Useimmat Image-objektin toiminnot muokkaavat lähdettään paikan päällä: algoritmi lukee pikselit, tekee päätöksen, kirjoittaa uudet arvot takaisin, eikä erillistä tuloskuvaa varata. Toiminnot, jotka kuitenkin tuottavat erillisen tuloksen – formaattimuunnokset ja kourallinen muita – voidaan pyytää sijoittamaan tuo tulos kehyspuskuriin copy_to_fb-avainsana-argumentin kautta. copy_to_fb=True tekee kaksi asiaa kerralla: se sijoittaa tuloskuvan kehyspuskuriin keon sijaan (kiertäen keon paineen) ja tekee tuloksesta seuraavan kehyksen, jonka IDE:n esikatselu näyttää. copy_to_fb=True:n liittäminen liukuhihnan viimeiseen vaiheeseen, tuloksen ilmestymisen seuraaminen näytöllä ja siitä iterointi on yksi hyödyllisimmistä virheenjäljitysidiomeista kuvankäsittelyssä.

Kun käytössä on kääre, joka pitää sisällään nimetyn puskurin, neljä tapaa saada sellainen olemassaoloon, kaksi näkymää sen tavuihin ja kytkin, joka päättää mihin uudet päätyvät, Image ei ole enää mysteeri. Jäljellä olevat perustavanlaatuiset kysymykset – miten pikselin sijainti nimetään, mitä kukin pikseli todella sisältää, miten toiminto rajataan sen osaan – rakentuvat sen päälle.