5.4. Pixels lezen en schrijven¶
De meeste bewerkingen op een afbeelding verbergen hun werk per pixel binnen een enkele methodeaanroep, waarbij de lussen die elke pixel aanraken op native snelheid verlopen. Er zijn echter gevallen waarin applicatiecode rechtstreeks een specifieke pixel wil aanraken: om te lezen wat zich op een bepaalde positie bevindt, om er een nieuwe waarde naar te schrijven, om een enkel punt te bemonsteren voor een kalibratiestap, of om een waarde op een bekende locatie te debuggen. De image-module biedt dat toegangsniveau via twee adresseringsvormen, die elk passen bij een andere manier van denken over waar een pixel zich bevindt.
5.4.1. Adresseren op coördinaat¶
De meest natuurlijke vorm is degene waarvoor Coördinaten al de woordenschat heeft ontwikkeld: benoem een pixel met zijn cartesische (x, y). get_pixel() neemt (x, y) en geeft de waarde op die positie terug; set_pixel() neemt diezelfde (x, y) samen met een waarde en schrijft die.
Wat die aanroepen teruggeven of accepteren, hangt af van het formaat van de afbeelding. Grijswaarden-, binaire en Bayer-afbeeldingen dragen één enkele waarde per pixel – een helderheid voor grijswaarden, een 0 of 1 voor binair, een enkele kleurkanaalbemonstering voor Bayer – dus geeft get_pixel() een enkel geheel getal terug. RGB565 draagt drie kleurkanalen verpakt in 16 bits, en get_pixel pakt ze standaard uit in een (r, g, b) tuple, waarbij elk kanaal wordt afgebeeld op het bereik 0 – 255.
Het standaardgedrag kan aan beide kanten worden omgedraaid. Het meegeven van rgbtuple=False aan get_pixel op een RGB565-afbeelding valt terug op het ruwe 16-bits verpakte woord – dezelfde vorm die de lineaire index teruggeeft, en de efficiënte vorm wanneer de applicatie dezelfde verpakte waarde meteen weer gaat terugschrijven. Het meegeven van rgbtuple=True op een afbeelding met één kanaal doet het tegenovergestelde: de opgeslagen waarde wordt vóór het teruggeven omgezet in een RGB888-tuple, waarbij Bayer-afbeeldingen een directe debayer-stap doorlopen. Het argument bestaat zodat aanroepende code pixels kan opvragen in een uniforme kleurruimte, ongeacht hoe de onderliggende afbeelding ze opslaat.
Gecomprimeerde afbeeldingen – JPEG en PNG – worden niet ondersteund door get_pixel of set_pixel. Hun bytes vertegenwoordigen geen pixels op bekende posities, en de methoden geven een fout in plaats van een waarde terug te geven die niets zou betekenen.
In de praktijk zien de patronen er als volgt uit:
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
Als de opgevraagde (x, y) buiten de afbeelding ligt, geeft get_pixel None terug en doet set_pixel niets. Dat is bewust vergevingsgezind: veel algoritmen lopen dicht langs de randen van een afbeelding en indexeren kortstondig posities buiten bereik, en een stille no-op is minder verstorend dan elke keer een uitzondering.
5.4.2. Adresseren op lineaire index¶
De andere vorm is om pixels te adresseren via hun positie in de onderliggende buffer. Herinner je de indeling van de buffer: pixels worden rij voor rij opgeslagen, eerst alle pixels van de bovenste rij, dan alle van de volgende rij, enzovoort tot aan de onderste. Die ordening betekent dat elke pixel een enkele gehele index heeft die telt vanaf 0 linksboven en bij elke rij op zijn beurt oploopt. De pixel op coördinaat (x, y) heeft lineaire index y * width + x.
Pixels worden zowel geadresseerd via cartesische (x, y) als via een lineaire index die de buffer rij voor rij doorloopt, van links naar rechts.¶
De image-module stelt die index beschikbaar via de gewone Python-subscriptnotatie: img[i] leest de pixel op lineaire index i, img[i] = value schrijft er een. Wat de indexvorm teruggeeft is de ruwe opgeslagen waarde voor het formaat, niet de uitgepakte tuple die get_pixel() standaard teruggeeft. Dat onderscheid is van belang omdat het eerder gekozen formaat bepaalt hoe de ruwe waarde eruitziet:
Grijswaarden- en Bayer-pixels komen terug als 8-bits gehele getallen.
RGB565- en YUV422-pixels komen terug als 16-bits gehele getallen – het verpakte woord.
Binaire pixels komen terug als
0of1.JPEG- en PNG-pixels komen terug als 8-bits gehele getallen, één byte per keer van de gecomprimeerde stroom. Die waarden zijn ondoorzichtig – het zijn stukjes van een gecomprimeerde codering in plaats van pixels in enige gewone zin.
De indexvorm past bij code die al in termen van bufferoffsets denkt: een lus die elke pixel eenmaal doorloopt, een algoritme dat per keer een rij moet overslaan, of een stuk code dat tussen bufferindelingen vertaalt. Code die in termen van x- en y-coördinaten denkt, is beter gediend met get_pixel en set_pixel; de twee vormen adresseren dezelfde pixels via verschillende mentale modellen.
De Image is ook itereerbaar. for v in img: doorloopt de buffer in dezelfde rij-georiënteerde volgorde en levert de ruwe waarden één pixel per keer op, en len(img) is het aantal pixels voor ongecomprimeerde formaten of het aantal bytes voor gecomprimeerde stromen.
5.4.3. Waarom Python per pixel het trage pad is¶
Een praktische opmerking waar het eerlijk over te zijn is. Een afbeelding pixel voor pixel doorlopen vanuit Python is traag. Een grijswaardenafbeelding van 320 × 240 bevat 76.800 pixels; get_pixel() op elk daarvan aanroepen in een for-lus voert miljoenen MicroPython-bytecode-instructies uit om werk te doen dat een gelijkwaardige native methode in een paar honderd microseconden zou kunnen voltooien. Dat is geen kleine factor. Het is het verschil tussen een script dat frames in realtime verwerkt en een script dat ver onder de framesnelheid van de camera voortkruipt.
Bijna elke methode op het Image-oppervlak bestaat omdat er een snellere, native versie van een veelvoorkomend patroon per pixel is. Een lus die twee afbeeldingen bij elkaar optelt wordt een enkele native aanroep. Een lus die elke pixel gladstrijkt door deze te middelen met zijn buren wordt een tweede. Een lus die elke pixel classificeert tegen een drempelwaarde wordt een derde. De taak van de applicatie is meestal om te herkennen welke methode voor de hele afbeelding overeenkomt met het werk dat de lus zou hebben gedaan, en daarnaar te grijpen in plaats van de lus met de hand te schrijven.
Lezen en schrijven op pixelniveau zijn nog steeds het juiste gereedschap wanneer niets anders past – een specifieke meting terug in de buffer plaatsen, één positie bemonsteren voor een kalibratiestap, een waarde op een bekende locatie debuggen. Het punt is dat ze het trage pad zijn, gebruikt wanneer de methoden voor de hele afbeelding niet de vorm hebben die de applicatie nodig heeft, en niet als de standaardmanier om op pixels te werken.