5.3. Formaty pikseli¶
Algorytm wykrywający krawędzie oczekuje, że każdy piksel przechowuje wartość jasności. Algorytm śledzący kolorowy obiekt oczekuje, że każdy piksel niesie informację o kolorze. Algorytm wykonujący domknięcie morfologiczne oczekuje, że każdy piksel jest albo włączony, albo wyłączony. Format pikseli niesiony przez Image – jeden z wyliczonych w katalogu Vision Sensors – jest tym, co czyni te oczekiwania sprawdzalnymi z góry: format mówi z wyprzedzeniem, w jakiej formie są piksele i które algorytmy mogą w związku z tym działać na nich bez kroku konwersji.
Ta strona dotyczy tego, jak to ograniczenie sprawdza się w praktyce. To, który format jest właściwym wyborem, zależy od tego, co zamierza robić potok, a metody konwersji między formatami są sposobem, w jaki potok potrzebujący więcej niż jednego z nich łączy poszczególne etapy.
Pięć nieskompresowanych formatów pikseli i sposób, w jaki upakowane są ich bajty. JPEG i PNG nie zostały tu narysowane, ponieważ są skompresowanymi strumieniami o zmiennej długości, a nie siatkami pikseli o stałym rozmiarze.¶
5.3.1. Koń pociągowy: skala szarości¶
Większość klasycznej wizji maszynowej sprowadza się do pracy z wartościami jasności. Wykrywanie krawędzi, dopasowywanie wzorców, dekodowanie AprilTag, estymacja przepływu optycznego, operatory morfologiczne, analiza plam (blob) – wszystkie one, na poziomie, na którym działają algorytmy, patrzą na to, jak jasny jest każdy piksel i jak ta jasność wypada w porównaniu z jasnością sąsiednich pikseli. Kolor sceny jest często przydatny aplikacji, która je wywołuje, ale same algorytmy go nie potrzebują.
Format skali szarości przekazuje algorytmom dokładnie to, bez żadnego narzutu. Jeden bajt na piksel przechowuje wartość jasności od 0 (czerń) do 255 (biel). Format ma połowę rozmiaru RGB565 i YUV422 oraz jedną trzecią rozmiaru RGB888, więc każda operacja przetwarza mniej danych – zarówno szybciej, jak i z mniejszym obciążeniem bufora. W mniejszych kamerach, gdzie bufor ramki konkuruje o RAM z resztą skryptu, ta różnica w zajmowanym miejscu może decydować o tym, czy potok w ogóle się zmieści. Jeśli kolor nie jest wskazówką, której potrzebuje algorytm, skala szarości jest właściwą odpowiedzią.
5.3.2. Kolor poprzez RGB565¶
Gdy kolor jest wskazówką – śledzenie kolorowego znacznika, odróżnianie czerwonych jabłek od zielonych, wybieranie elementu interfejsu po jego odcieniu – dwa bajty na piksel zapewniają wystarczającą ilość koloru dla rodzajów klasyfikacji wykonywanych przez algorytmy. RGB565 jest domyślnym formatem koloru w kamerze i tym, którego oczekują metody świadome koloru.
Renderowanie opisanej ramki – rysowanie ramek wykrywania, zapisywanie tekstu diagnostycznego, wyświetlanie ramki na ekranie lub przesyłanie jej do zdalnej przeglądarki – również naturalnie wymaga RGB565. Podgląd w IDE, wbudowane kontrolery wyświetlaczy i większość docelowych miejsc sieciowych albo bezpośrednio korzystają z tego formatu, albo tanio z niego konwertują.
5.3.3. Bayer jako format przechowywania¶
Obraz Bayer to surowe wyjście sensora, zanim ISP poddał je debajeryzacji do gotowej reprezentacji koloru. Każdy piksel to jeden bajt przechowujący pojedynczy kanał koloru – ten, który przepuścił filtr koloru znajdujący się na tej pozycji w mozaice. To sprawia, że obraz Bayer ma ten sam rozmiar co obraz w skali szarości i jedną trzecią rozmiaru RGB888, co pokrywa się z tym, do czego Bayer jest faktycznie przydatny: przechowywania wielu ramek naraz, gdy wiążącym ograniczeniem jest RAM.
Haczyk polega na tym, że algorytmy w module image nie operują bezpośrednio na obrazach Bayer. Bez debajeryzacji żaden piksel nie niesie wystarczającej ilości informacji, by samodzielnie ocenić kolor, a wzorce, których szukają algorytmy – krawędzie, narożniki, plamy (blob) – byłyby zniekształcone przez mozaikę. Jedynymi sposobami odczytu lub modyfikacji obrazu Bayer są get_pixel() i set_pixel(); wszystko inne oczekuje gotowej reprezentacji.
Wynikający z tego wzorzec polega na przechowywaniu ramek jako Bayer tak długo, jak muszą one czekać w kolejce, i konwertowaniu każdej z nich na skalę szarości lub RGB565 w momencie, gdy faktycznie zaczyna się jej przetwarzanie. Konwersja kosztuje cykle CPU, ale oszczędza RAM, który w przeciwnym razie byłby zajęty przez gotowe ramki przez cały czas życia aplikacji.
Informacja
Jedynymi operacjami modułu image działającymi bezpośrednio na pikselach Bayer są get_pixel(), set_pixel() oraz ścieżka kodowania JPEG zasilająca podgląd w IDE lub zdalną przeglądarkę. Rysowanie, analiza i filtrowanie wymagają najpierw konwersji do skali szarości, RGB565 lub formatu binarnego.
5.3.4. YUV422 dla potoków, które chcą obu¶
YUV422 rozdziela informację każdego piksela na kanał luminancji (Y) i dwa kanały chrominancji (U i V) oraz poddaje chrominancję podpróbkowaniu, tak że sąsiednie pary pikseli współdzielą pojedyncze U i pojedyncze V. Liczba bajtów na piksel wynosi średnio dwa – tyle samo co RGB565 – ale są one ułożone tak, że kanał Y jest już ciągłym 8-bitowym obrazem w skali szarości znajdującym się pod znanymi przesunięciami w buforze.
Taki układ jest dokładnie tym, czego potrzebuje potok, gdy część jego etapów to praca w skali szarości, a część wymaga koloru. Bezpośredni odczyt wartości Y dla etapów w skali szarości pomija koszt jawnej konwersji; kanały U i V są dostępne, gdy późniejszy etap faktycznie potrzebuje koloru. Poza tym konkretnym wzorcem RGB565 jest zwykle prostszym wyborem dla koloru, a skala szarości prostszym wyborem dla pracy wyłącznie na jasności – wartość YUV422 wynika z tego, że jest dobry w obu zadaniach jednocześnie.
Informacja
Moduł image operuje na YUV422 w bardziej ograniczony sposób niż na skali szarości, RGB565 czy formacie binarnym – bezpośrednie odczyty kanału Y dla pracy w skali szarości oraz ścieżka kodowania JPEG zasilająca podgląd w IDE lub zdalną przeglądarkę. Metody świadome koloru oczekują RGB565; ramki YUV422 wymagają jawnej konwersji przed analizą koloru lub rysowaniem.
5.3.5. Format binarny, maski i wyjście progowane¶
Obraz binarny to jeden bit na piksel: każdy piksel to albo 0, albo 1. Format rzadko pojawia się jako przechwycenie z sensora; zamiast tego występuje jako naturalne wyjście progowania (gdzie test koloru lub jasności klasyfikuje każdy piksel do „tak, pasuje” lub „nie, nie pasuje”) oraz jako naturalne wejście dla operacji morfologicznych i dla argumentu mask, który akceptuje wiele metod.
Praktyczną zaletą tego formatu jest jego rozmiar. Obraz binarny zajmuje jedną ósmą tego, co obraz w skali szarości, więc przenoszenie dużej maski – wyboru per piksel, których pozycji ma dotknąć jakaś operacja w dół potoku – jest tanie. Fakt, że wiele operacji akceptuje obraz binarny jako argument słowem kluczowym mask=, jest drugą stroną tego samego medalu: format jest mały, a łączenie binarnego wyjścia jednego etapu z wejściem maski innego jest częstym wzorcem potoku.
5.3.6. JPEG i PNG na granicy¶
Obiekty Image typu JPEG i PNG różnią się od pozostałych w katalogu. Nie są siatkami pikseli; są skompresowanymi strumieniami bajtów, które kodują dane pikseli w formie, której operacje na poziomie pikseli nie potrafią odczytać. Wywołanie get_pixel() na JPEG nie zwraca piksela na danej pozycji; piksel nie znajduje się rozpakowany nigdzie w buforze, by metoda mogła go pobrać.
JPEG i PNG pojawiają się na granicy przetwarzania obrazu, gdzie dane pikseli opuszczają kamerę lub do niej wchodzą w postaci skompresowanej. Zapisanie ramki na dysk jako JPEG utrzymuje mały rozmiar pliku; wysłanie ramki przez sieć jako JPEG utrzymuje tanią transmisję; wczytanie ramki referencyjnej z pliku JPEG pozwala jej leżeć na dysku w znacznie mniejszej formie niż surowe piksele. Dla każdego z tych przypadków użycia skompresowana reprezentacja jest właściwą odpowiedzią. Aby jednak wykonać jakiekolwiek faktyczne przetwarzanie na JPEG, aplikacja najpierw konwertuje go do formatu nadającego się do pracy – i to właśnie podczas tej konwersji skompresowane bajty są rozwijane w piksele i faktycznie dochodzi do gwałtownego wzrostu bufora (30 KB JPEG może stać się 600 KB RGB565).
5.3.7. Konwersja między formatami¶
Ścieżka konwersji jest tym, co zszywa różne formaty w jeden potok. Pięć metod klasy Image przyjmuje istniejący obraz i zwraca nowy w innym formacie:
to_grayscale()tworzy obraz o jednym bajcie na piksel, format, którego chcą klasyczne algorytmy.to_rgb565()tworzy dwubajtowy na piksel format koloru, którym posługują się zarówno metody świadome koloru, jak i podgląd w IDE.to_bitmap()tworzy jednobitowy obraz binarny, format akceptowany przez morfologię i argumentymask.to_jpeg()tworzy obraz skompresowany w JPEG, odpowiedni do zapisu lub transmisji.to_png()tworzy obraz skompresowany w PNG, gdy kodowanie bezstratne jest preferowane nad mniejszymi plikami JPEG.
Każda konwersja domyślnie działa w miejscu: bufor obrazu źródłowego jest nadpisywany przekonwertowanym wynikiem, a oryginalne piksele źródła znikają po powrocie z wywołania. To najtańsza opcja zarówno pod względem CPU, jak i pamięci, i jest właściwą odpowiedzią, gdy ramka źródłowa nie będzie już do niczego potrzebna.
Gdy źródło jest nadal potrzebne – gdy późniejszy etap potoku musi zobaczyć oryginalną ramkę – dwa argumenty słowem kluczowym przesłaniają domyślne działanie w miejscu. copy=True przydziela osobny bufor dla przekonwertowanego obrazu na stercie Pythona i pozostawia źródło nienaruszone. copy_to_fb=True wykonuje tę samą alokację, ale umieszcza ją w buforze ramki zamiast na stercie – po co sięga aplikacja, gdy przekonwertowany obraz musi trafić do podglądu w IDE, ponieważ IDE odczytuje z bufora ramki.
Dwie dodatkowe metody tworzą obrazy RGB565 pokolorowane przez paletę zamiast prostej konwersji. to_rainbow() odwzorowuje każdą jednokanałową wartość wejściową na kolor wzdłuż płynnego gradientu przebiegającego przez widmo widzialne. to_ironbow() odwzorowuje każdą wartość wejściową na nieliniową paletę kamery termowizyjnej, przebiegającą od czerni przez ciemne czerwienie i pomarańcze aż do bieli. Obie są narzędziami wizualizacji, a nie pomiaru; celem jest uczynienie obrazu jednokanałowego, którego surowe wartości byłyby w przeciwnym razie niewidoczne dla oka, czytelnym na pierwszy rzut oka.
5.3.8. Rozmiar bufora¶
Jeszcze jeden szczegół dotyczący formatów, o którym warto powiedzieć wprost. size() zawsze raportuje rozmiar bufora bajtów, a nie liczbę pikseli. Dla formatów nieskompresowanych wynika to bezpośrednio z wymiarów i liczby bajtów na piksel: width * height * bytes_per_pixel. Dla JPEG i PNG jest to rozmiar skompresowanego strumienia, który zmienia się od ramki do ramki w zależności od tego, co zawiera scena. Kod przydzielający bufory na podstawie budżetów bajtowych używa size() w pierwszym przypadku; kod strumieniujący skompresowane ramki z kamery odczytuje go po każdej kompresji, aby wiedzieć, ile bajtów faktycznie zawiera strumień.