5.6. Rysowanie kształtów i tekstu

Algorytm, który podejmuje jakąś decyzję dotyczącą obrazu, często wymaga, aby ta decyzja była widoczna. Detektor plam (blob) znajduje obszar, na którym zależy aplikacji; aplikacja chce, by ten obszar został narysowany na ramce, tak aby operator – lub deweloper uruchamiający skrypt – mógł zobaczyć, co znaleziono. Transformacja współrzędnych odwzorowuje pozycję wejściową na wyjściową; jej debugowanie zwykle oznacza zaznaczenie obu pozycji na tym samym obrazie. Podgląd w IDE odczytuje to, co znajduje się w buforze ramki w chwili odpytywania, więc najprostszym sposobem uwidocznienia wyniku algorytmu jest zapisanie adnotacji bezpośrednio do bufora ramki. Rodzina metod rysowania w klasie Image to zestaw narzędzi przeznaczony dokładnie do tej pracy.

5.6.1. Prymitywy

Każda metoda rysowania umieszcza na obrazie jeden konkretny rodzaj znaku. Katalog jest niewielki i trzyma się blisko prymitywów geometrycznych, których adnotacja faktycznie potrzebuje:

  • draw_line() – prosty odcinek linii między dwoma punktami końcowymi.

  • draw_rectangle() – prostokąt wyrównany do osi, pusty lub wypełniony.

  • draw_circle() – okrąg wokół środka, pusty lub wypełniony.

  • draw_ellipse() – elipsa o dowolnym obrocie.

  • draw_cross() – znak plus w punkcie, zwyczajowe oznaczenie centroidu lub celu kliknięcia.

  • draw_arrow() – strzałka od punktu początkowego do punktu końcowego.

  • draw_edges() – cztery boki dowolnego czworokąta, na podstawie czterech punktów narożnych; naturalny sposób na obrysowanie wykrytego tagu lub obszaru zniekształconego perspektywicznie.

  • draw_string() – tekst z wbudowanej czcionki bitmapowej.

Każda z tych metod modyfikuje obraz źródłowy w miejscu i zwraca ten sam obraz na potrzeby łączenia wywołań, zgodnie z konwencją metod operujących ustaloną wcześniej.

Siatka małych paneli, z których każdy pokazuje jeden z ośmiu prymitywów rysowania zastosowany raz. Każdy panel zawiera linię, prostokąt, okrąg, elipsę, krzyżyk, strzałkę, czworokąt lub krótki ciąg tekstowy, a pod spodem znajduje się etykieta z nazwą metody, która go utworzyła.

Osiem prymitywów rysowania, po jednym na panel. Każda metoda tworzy jeden rodzaj znaku.

5.6.2. Kolor

Każda metoda rysowania przyjmuje argument color, który decyduje o tym, jaką wartość zapisać do każdego malowanego piksela. Forma, jaką przyjmuje ten argument, zależy od formatu obrazu. Dla obrazu RGB565 naturalną formą jest krotka (r, g, b), w której każdy kanał mieści się w zakresie 0255; moduł upakowuje ją do 16-bitowego słowa RGB565 przed zapisem. Dla obrazu w skali szarości naturalną formą jest pojedyncza liczba całkowita jasności od 0 (czerń) do 255 (biel). Metody przyjmują również surową przechowywaną wartość danego formatu – 16-bitowe upakowane słowo dla RGB565, 8-bitową liczbę całkowitą dla skali szarości – co jest wydajną formą wtedy, gdy kolor został obliczony gdzie indziej i jest już w formie przechowywanej.

Pominięcie argumentu color maluje na biało. Ta wartość domyślna jest wygodna przy pracy w skali szarości, gdzie biel jest wartością maksymalną i czytelnie odznacza się na większości teł. W przypadku nakładek diagnostycznych RGB565 jest niemal zawsze błędna: zieleń lub czerwień zwykle lepiej odznacza się na tle sceny, jaką kamera rzeczywiście widzi, a jawnie podany kolor komunikuje intencję.

5.6.3. Grubość i wypełnienie

Większość metod geometrycznych przyjmuje dwie flagi, które decydują o sposobie rysowania znaku:

  • thickness=N ustawia szerokość linii w pikselach. Wartością domyślną jest 1, co odpowiada większości nakładek; większa wartość przydaje się, gdy adnotacja musi pozostać widoczna na ruchliwej scenie lub po tym, jak kolejny etap potoku dalej zmodyfikuje obraz.

  • fill=True przełącza znak z obrysu na wypełniony, malując każdy wewnętrzny piksel wybranym kolorem. Wartością domyślną jest False.

Te flagi nie dotyczą prymitywów, które nie mają wnętrza do wypełnienia – linii, krzyżyka, strzałki, czworokąta – gdzie znaczenie ma jedynie thickness.

5.6.4. Rysowanie tekstu

draw_string() wypisuje znaki z wbudowanej czcionki bitmapowej o rozmiarze 8 na 10 pikseli. x i y to lewy górny róg komórki pierwszego znaku, text to ciąg do narysowania, a color podlega tej samej konwencji co metody geometryczne. Czcionka obejmuje pełny zakres drukowalnych znaków ASCII i nie ma kerningu – każdy znak zajmuje tę samą komórkę o szerokości 8 pikseli – co ułatwia pozycjonowanie wyniku.

img.draw_string(10, 10, "blobs: 3", color=(0, 255, 0))

Ciąg może zawierać znaki nowego wiersza (\n); każdy znak nowego wiersza przenosi kolejny znak na początek nowego wiersza, dziesięć pikseli poniżej poprzedniego. Argument scale rysuje każdy znak w większym rozmiarze, mnożąc go przez współczynnik zmiennoprzecinkowy, a x_spacing i y_spacing dodają odstępy wokół każdego znaku. Niewielki zestaw flag obrotu / odbicia lustrzanego / odwrócenia stosuje się do całego ciągu albo do każdego znaku z osobna – to wystarczająca kontrola, by rozmieścić tekst pod kątem lub wzdłuż niepoziomej krawędzi, gdy układ tego wymaga.

5.6.5. Czyszczenie płótna

Jedna metoda z tej rodziny nie rysuje żadnego konkretnego znaku. Po prostu zeruje każdy piksel obrazu:

  • clear() – zeruje każdy piksel, opcjonalnie z ograniczeniem do ROI lub w zakresie wyznaczonym przez maskę.

clear() jest właściwym rozwiązaniem, gdy aplikacja komponuje adnotację od zera w każdej ramce – zaczyna od czarnego płótna, rysuje nowe adnotacje i przekazuje wynik do wyświetlacza – zamiast nakładać je na przechwyconą ramkę. Jest to także najtańszy sposób przygotowania obrazu roboczego do użycia jako bufor maski.

Świeżo zaalokowany obraz jest już wyzerowany przez konstruktor, więc clear() ma znaczenie zwłaszcza dla buforów wykorzystywanych ponownie między ramkami.