5.4. Läsa och skriva pixlar

De flesta operationer på en bild döljer sitt arbete per pixel inuti ett enda metodanrop, där looparna som rör varje pixel sker i inbyggd hastighet. Det finns dock fall där applikationskoden vill röra en specifik pixel direkt: för att läsa vad som finns på en viss position, för att skriva ett nytt värde till en, för att ta ett stickprov på en enskild punkt för ett kalibreringssteg, eller för att felsöka ett värde på en känd plats. Modulen image ger den nivån av åtkomst genom två adresseringsformer, där var och en passar ett olika sätt att tänka kring var en pixel finns.

5.4.1. Adressering med koordinat

Den mest naturliga formen är den som Koordinater redan utvecklat vokabulären för: namnge en pixel med dess kartesiska (x, y). get_pixel() tar (x, y) och returnerar värdet på den positionen; set_pixel() tar samma (x, y) tillsammans med ett värde och skriver det.

Vad dessa anrop returnerar eller accepterar beror på bildens format. Bilder i gråskala, binärt format och Bayer bär ett enda värde per pixel – en ljusstyrka för gråskala, en 0 eller 1 för binärt, ett enskilt färgkanalsstickprov för Bayer – så get_pixel() returnerar ett enda heltal. RGB565 bär tre färgkanaler packade i 16 bitar, och get_pixel packar som standard upp dem till en (r, g, b)-tupel, där varje kanal mappas in i intervallet 0255.

Standardbeteendet kan vändas i båda ändar. Att skicka rgbtuple=False till get_pixel på en RGB565-bild faller tillbaka på det råa 16-bitars packade ordet – samma form som det linjära indexet returnerar, och den effektiva formen när applikationen ska skriva tillbaka samma packade värde direkt. Att skicka rgbtuple=True på en enkanalsbild gör motsatsen: det lagrade värdet konverteras till en RGB888-tupel innan det returneras, där Bayer-bilder går igenom ett debayer-steg på plats. Argumentet finns till så att anropande kod kan be om pixlar i ett enhetligt färgrum oavsett hur den underliggande bilden lagrar dem.

Komprimerade bilder – JPEG och PNG – stöds inte av get_pixel eller set_pixel. Deras byte representerar inte pixlar på kända positioner, och metoderna ger upphov till ett fel i stället för att returnera ett värde som inte skulle betyda någonting.

I praktiken ser mönstren ut så här:

v = img.get_pixel(40, 30)            # grayscale: int 0..255
img.set_pixel(40, 30, 255)           # write white

r, g, b = img.get_pixel(40, 30)      # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0))   # write red

Om de begärda (x, y) ligger utanför bilden returnerar get_pixel None och set_pixel gör ingenting. Det är förlåtande med flit: många algoritmer rör sig nära kanterna på en bild och indexerar kortvarigt positioner utanför intervallet, och en tyst no-op är mindre störande än ett undantag varje gång det händer.

5.4.2. Adressering med linjärt index

Den andra formen är att adressera pixlar med deras position i den underliggande bufferten. Erinra dig buffertens layout: pixlar lagras rad för rad, alla den översta radens pixlar först, sedan alla nästa rads, och så vidare ner till botten. Det arrangemanget innebär att varje pixel har ett enda heltalsindex som räknar från 0 uppe till vänster och ökar längs varje rad i tur och ordning. Pixeln på koordinaten (x, y) har det linjära indexet y * width + x.

Ett rutnät med celler på 4 gånger 3. Varje cell bär ett stort linjärt index från 0 i det övre vänstra hörnet genom 11 i det nedre högra, plus en liten (x, y)-tupel under. Kolumnerna är märkta x lika med 0, 1, 2, 3 längst upp; raderna är märkta y lika med 0, 1, 2 längs vänsterkanten. En bildtext under anger relationen: linjärt index lika med y gånger bredd plus x.

Pixlar adresseras både med kartesiska (x, y) och med ett linjärt index som vandrar genom bufferten rad för rad, från vänster till höger.

Modulen image ger åtkomst till det indexet genom vanlig Python-subskriptnotation: img[i] läser pixeln på det linjära indexet i, img[i] = value skriver en. Vad indexformen returnerar är det råa lagrade värdet för formatet, inte den uppackade tupel som get_pixel() returnerar som standard. Den distinktionen är viktig eftersom formatet som valdes tidigare avgör hur det råa värdet ser ut:

  • Pixlar i gråskala och Bayer kommer tillbaka som 8-bitars heltal.

  • Pixlar i RGB565 och YUV422 kommer tillbaka som 16-bitars heltal – det packade ordet.

  • Binära pixlar kommer tillbaka som 0 eller 1.

  • Pixlar i JPEG och PNG kommer tillbaka som 8-bitars heltal, en byte i taget av den komprimerade strömmen. Dessa värden är ogenomskinliga – de är delar av en komprimerad kodning snarare än pixlar i någon vanlig mening.

Indexformen passar kod som redan tänker i termer av buffertoffset: en loop som vandrar genom varje pixel en gång, en algoritm som behöver hoppa en rad i taget, eller en kod som översätter mellan buffertlayouter. Kod som tänker i termer av x- och y-koordinater betjänas bättre av get_pixel och set_pixel; de två formerna adresserar samma pixlar genom olika mentala modeller.

Image är också itererbar. for v in img: vandrar genom bufferten i samma radvisa ordning och ger de råa värdena en pixel i taget, och len(img) är pixelantalet för okomprimerade format eller byteantalet för komprimerade strömmar.

5.4.3. Varför Python per pixel är den långsamma vägen

En praktisk anmärkning som det är värt att vara ärlig om. Att vandra genom en bild en pixel i taget från Python är långsamt. En gråskalebild på 320 × 240 rymmer 76 800 pixlar; att anropa get_pixel() på var och en av dem i en for-loop kör miljontals MicroPython-bytekodinstruktioner för att utföra arbete som en motsvarande inbyggd metod skulle kunna slutföra på några hundra mikrosekunder. Det är inte en liten faktor. Det är skillnaden mellan ett skript som behandlar bildrutor i realtid och ett som kryper fram långt under kamerans bildrutefrekvens.

Nästan varje metod på Image-ytan finns till för att det finns en snabbare, inbyggd version av ett vanligt mönster per pixel. En loop som adderar två bilder blir ett enda inbyggt anrop. En loop som jämnar ut varje pixel genom att medelvärdesbilda den med dess grannar blir ett annat. En loop som klassificerar varje pixel mot ett tröskelvärde blir ett tredje. Applikationens uppgift är, för det mesta, att känna igen vilken helbildsmetod som matchar arbetet loopen skulle ha utfört, och ta till den i stället för att skriva loopen för hand.

Läsning och skrivning på pixelnivå är fortfarande rätt verktyg när inget annat passar – att lappa tillbaka en specifik mätning i bufferten, att ta ett stickprov på en position för ett kalibreringssteg, att felsöka ett värde på en känd plats. Poängen är att de är den långsamma vägen, som används när helbildsmetoderna inte har den form applikationen behöver, inte som standardsättet att operera på pixlar.