5.1. Image-objektet

En bildbehandlingsalgoritm går igenom en bild en pixel i taget. Vid varje position gör den något enkelt – läser ett värde, jämför det mot ett tröskelvärde, kombinerar det med motsvarande pixel i en andra bild, skriver tillbaka ett resultat. Upprepat över en hel bildruta är dessa enkla beslut per pixel det som kantdetektering, blobspårning, QR-koddekodning och alla andra klassiska maskinseendetekniker är uppbyggda av. För att utföra det arbetet effektivt måste algoritmen veta var varje pixel ligger i minnet, vad varje pixels värde faktiskt betyder och vilken del av bilden den ska titta på. image.Image är objektet som organiserar den informationen.

Vision Sensors slutade i det ögonblick då csi.CSI.snapshot() returnerar. Vad än kamerasidans maskineri gjorde för att producera den fångade bildrutan är redan klart; applikationen har Image i handen och behöver veta vad den ska göra med den.

5.1.1. Bufferten och dess egenskaper

Inuti Image finns en pekare till ett sammanhängande block av byte i RAM och en liten rubrik som bär tre metadata: bildens bredd i pixlar, dess höjd i pixlar och pixelformatet som byten är i. Byten är pixlarna själva, lagrade i radordning – alla pixlar i den översta raden först, sedan alla i den andra raden och så vidare ned till botten. Egenskaperna beskriver hur man läser dem.

Bredd och höjd är enkla heltalsantal. Pixelformatet är den mer intressanta egenskapen, eftersom det bestämmer hur många byte varje pixel tar och vad dessa byte kodar. En gråskalebild bär en byte per pixel som håller ett ljusstyrkevärde. En RGB565-bild bär två byte per pixel som håller röda, gröna och blå fält packade i ett 16-bitars ord. En Bayer-bild bär en byte per pixel, men varje pixel samplas genom ett av tre färgfilter som väljs av dess position i mosaiken. Vision Sensors räknade upp hela katalogen; det som spelar roll här är att exakt ett av dessa format är inställt på varje Image, och valet styr aritmetiken för byte per pixel och betydelsen av varje enskild byte i bufferten.

Med en pekare till bufferten, bredden, höjden och formatet faller varje annan egenskap som en algoritm kan vilja ha ut som en kort beräkning. Byten som börjar pixel (x, y) sitter vid offset (y * width + x) * bytes_per_pixel från buffertens början. Det totala antalet byte är width * height * bytes_per_pixel. Adressen till nästa rad nedåt är exakt width * bytes_per_pixel byte efter början på den aktuella. Image exponerar de tre egenskaperna genom enkla metodanrop – width(), height(), format() – plus den härledda size genom size(). Metoder på andra ställen i modulen använder dessa värden för att göra offsetaritmetiken själva; applikationskod behöver sällan göra det.

En ruta märkt image.Image -- Python-omslag längst upp, med en pil som pekar nedåt märkt "references" till två staplade rutor -- en tunn rubrikruta som håller bredd, höjd och pixelformat, och en tjockare pixelbuffertruta med en rad små celler som representerar enskilda pixlar. En bildtext nedanför noterar att bufferten lever på heapen som standard och i bildbufferten när copy_to_fb är true.

En Image är ett litet Python-omslag som pekar på ett sammanhängande minnesblock: en rubrik som bär bredden, höjden och pixelformatet, följt av själva pixelbufferten.

5.1.2. Var bufferten kommer ifrån

Standardberättelsen genom hela detta kapitel är den som Vision Sensors redan täckte: en fångad bildruta anländer från snapshot, byten sitter i kamerans bildbuffert, och den returnerade Image pekar på dem. Tre andra sätt att erhålla en dyker upp regelbundet, och vart och ett innebär något annorlunda om var bufferten hamnar.

Att läsa in från en fil ser ut som att skicka en sökväg till konstruktorn: image.Image("/sdcard/saved.jpg"). Modulen läser filen in i en nyallokerad buffert på Python-heapen. BMP-, PGM- och PPM-filer avkodas på vägen in och den resulterande Image bär ett okomprimerat pixelformat. JPEG- och PNG-filer förblir komprimerade – Image bär formatet JPEG eller PNG, och bufferten håller filens byteström i stort sett oförändrad. För att göra något arbete på pixelnivå på en komprimerad bild konverterar applikationen den genom to_rgb565() eller to_grayscale() först, och den konverteringen är där dekomprimeringen – och motsvarande heapsvällning, där en 30 KB JPEG kan bli 600 KB RGB565 – faktiskt sker. Att läsa in från fil är mest användbart under utveckling, när en algoritm behöver testas mot en känd referensbildruta som lagras tillsammans med skriptet.

Att bygga en från grunden är canvasfallet: image.Image(320, 240, image.RGB565) ber modulen att allokera så många byte i det formatet, nollställa innehållet och lämna tillbaka omslaget. Pixlarna betyder ännu ingenting – de är alla noll – men den tomma bilden är arbetshästen för en handfull återkommande mönster: referensbildrutor som en aktuell bildruta subtraheras mot, canvasar på vilka grafiköverlägg komponeras, binära buffertar som fylls i och används som masker.

Att konstruera från en ndarray bryggar i den andra riktningen, från vilken numerisk beräkning som helst tillbaka in i image-modulen. Att skicka en float32 ulab.numpy.ndarray till konstruktorn producerar en Image vars dimensioner matchar ndarrayen – en tvåaxlig form (h, w) blir en gråskalebild, en treaxlig form (h, w, 3) blir RGB565 – med flytvärdena skalade från 0.0255.0 in i heltalspixelområdet. En neuronnätsvärmekarta, en numerisk array av vilket slag som helst, allt som produceras av ml eller ulab blir något som rit- och inspektionssidan av image-modulen kan använda.

Alla fyra källor lämnar tillbaka samma sorts Image. Kod som använder det returnerade objektet behöver aldrig hålla reda på var det kom ifrån.

5.1.3. Två vyer över byten

För det mesta behandlar applikationskod en Image som ett typat bildobjekt – en sak med namngivna metoder. Den andra halvan av berättelsen är att samma objekt också, transparent, framträder som en platt sekvens av byte för vilket MicroPython-API som helst som tar ett bytes-argument. Byten är inte en kopia av bufferten; de är en direkt vy av den.

Det arrangemanget är vad som gör det till en enradare att skicka ut en fångad bildruta från kameran. Att hasha den, skicka den över en seriell port, vidarebefordra den till ett nätverksuttag – inget av det behöver ett separat steg för att ”konvertera bilden till byte”:

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

Den byteliknande vyn är skrivskyddad som standard, med avsikt. Bildbuffertar är stora och ibland delade mellan lager av bildbehandlingsstacken, så att ge en oförsiktig buf[0] = 0 någonstans djupt i en anropsstack makten att tyst korrumpera en är en alltför vass kant att lämna exponerad. När läs- och skrivåtkomst på bytenivå är vad applikationen faktiskt behöver – att skriva ett kalibreringsvärde till en känd offset, till exempel – returnerar bytearray() en separat, uttryckligen läs- och skrivbar vy över samma minne, som skyltar avsikten vid anropsplatsen.

5.1.4. Var bufferten lever

Pixelbuffertar är tillräckligt stora för att var de sitter i RAM spelar roll. En QQVGA RGB565-bildruta är 160 × 120 × 2 = 38 400 byte; en VGA RGB565-bildruta är 614 400 byte; en 224 × 224 RGB565-indata som en neuronnätsklassificerare kanske konsumerar är ungefär 100 KB. Python-heapen på de minsta kamerorna kan vara bara några tiotals kilobyte när väl körtiden har startat. Att hålla mer än en bildruta eller två av bilddata på heapen skulle tränga ut allt annat från den.

Vägen ut är att bildbuffertar mestadels inte lever på Python-heapen. De lever i den dedikerade regionen av RAM som Vision Sensors introducerade som bildbufferten – samma minne som kamerans DMA skriver fångade bildrutor in i och som IDE-förhandsvisningen läser färdiga bildrutor ut ur. De flesta operationer på en Image modifierar sin källa på plats: algoritmen läser pixlar, beslutar, skriver tillbaka nya värden, och ingen separat resultatbild allokeras. De operationer som gör producerar ett separat resultat – formatkonverteringar och en handfull andra – kan ombes att placera det resultatet i bildbufferten genom nyckelordsargumentet copy_to_fb. copy_to_fb=True gör två saker på en gång: det lägger resultatbilden i bildbufferten i stället för på heapen (kringgår heaptrycket) och det gör resultatet till nästa bildruta som IDE-förhandsvisningen kommer att visa. Att fästa copy_to_fb=True på det sista steget i en pipeline, titta på resultatet dyka upp på skärmen och iterera därifrån är ett av de mest användbara felsökningsidiomen i bildbehandling.

Med ett omslag som håller en märkt buffert, fyra sätt att få en till existens, två vyer över dess byte och en omkopplare som beslutar var nya landar, är Image inte längre ett mysterium. De återstående grundläggande frågorna – hur en pixelposition namnges, vad varje pixel faktiskt håller, hur man avgränsar en operation till en del av en – är byggda ovanpå den.