5.9. Operacje arytmetyczne

Rodzina funkcji rysowania z poprzedniej sekcji maluje na obrazie. Rodzina funkcji arytmetycznych łączy dwa obrazy w trzeci – dodając ich wartości pikseli, odejmując jeden od drugiego, biorąc minimum lub maksimum na każdej pozycji. Ten niewielki zestaw operacji arytmetycznych na pikselach jest tym, na czym opierają się różnicowanie ramek, odejmowanie tła, składanie ekspozycji oraz garść innych klasycznych wzorców.

Rodzina operacji arytmetycznych w klasie Image jest na tyle mała, że można ją wyliczyć od razu w całości:

  • add()self + other na każdy piksel, przycięte do maksimum formatu.

  • sub()self - other na każdy piksel, przycięte do 0 od dołu.

  • rsub()other - self na każdy piksel, przycięte do 0 (ta sama arytmetyka co sub z odwróconymi operandami).

  • min() – minimum z dwóch wartości na każdy piksel.

  • max() – maksimum na każdy piksel.

  • difference()|self - other| na każdy piksel, różnica bezwzględna.

Plus dwie pokrewne operacje na pojedynczym obrazie:

  • invert() – zastępuje każdy piksel wartością 255 - pixel (lub odpowiednikiem maksimum dla danego formatu).

  • negate() – alias dla invert().

Dwa poziome paski gradientu u góry reprezentujące obrazy źródłowe A i B -- A przechodzi od ciemnego do jasnego od lewej do prawej, B przechodzi od jasnego do ciemnego od lewej do prawej. Poniżej nich pięć pasków gradientu reprezentujących wynik każdej operacji parami zastosowanej do A i B: A.add(B) wygląda na jednolicie biały, ponieważ każda pozycja sumuje się powyżej 255 i jest przycinana; A.sub(B) jest zerowy na lewej połowie i rozjaśnia się ku prawej; A.difference(B) pokazuje kształt litery V, jasny na każdym końcu i ciemny pośrodku; A.min(B) jest ciemny na końcach i jaśniejszy pośrodku; A.max(B) jest jasny na końcach i szary pośrodku.

Dwa gradienty źródłowe A i B oraz wynik każdej operacji parami zastosowanej do nich. Każda operacja wykonuje się pozycja po pozycji – to, co pojawia się w wyniku w danym miejscu, zależy wyłącznie od dwóch pikseli źródłowych w tym miejscu.

5.9.1. Dwie formy operanda

Każda z metod dwuobrazowych przyjmuje dla swojego drugiego operanda jedną z dwóch form:

  • Inny obraz Image o tych samych wymiarach. Arytmetyka wykonuje się pozycja po pozycji – wynik w (x, y) jest operacją zastosowaną do pikseli źródłowych w (x, y) obu obrazów.

  • Wartość skalarna – liczba całkowita dla skali szarości, krotka (r, g, b) dla RGB565. Ten sam skalar stosuje się na każdej pozycji.

Forma skalarna jest przydatna, gdy aplikacja chce przesunąć każdy piksel o stałą wartość. img.add(40) rozjaśnia cały obraz o 40; img.sub((20, 20, 20)) przyciemnia każdy piksel o 20 na kanał; img.max(50) podnosi każdy piksel poniżej 50 do 50 i pozostawia resztę bez zmian – rodzaj operacji, która zamienia niemal czarny próg sensora w płaską ciemną szarość, na której mogą pracować kolejne etapy.

5.9.2. Przycinanie

Wartości pikseli pozostają w zakresie formatu przez każdą operację. Dla 8-bitowego kanału oznacza to 0255: wszystko, co przekroczyłoby 255, jest przycinane z powrotem do 255, a wszystko, co spadłoby poniżej 0, jest podnoszone do 0. Nie ma zawijania.

Ten wybór ma znaczenie w praktyce. add rozjaśniający piksele nigdy nie wytwarza nagłego artefaktu przyciemnienia na jasnym końcu, gdzie obliczenia w przeciwnym razie przepełniłyby się; sub przyciemniający piksele nigdy nie wytwarza nagłego artefaktu rozjaśnienia na ciemnym końcu, gdzie w przeciwnym razie nastąpiłoby niedomiarowe przepełnienie. Wyniki pozostają wizualnie sensowne kosztem pewnej utraty informacji na nasyconych skrajnościach.

Przycinanie jest też powodem, dla którego sub i rsub zwracają różne wyniki. img_a.sub(img_b) daje tę część a, która jest jaśniejsza niż b, i zero wszędzie indziej; img_a.rsub(img_b) daje tę część b, która jest jaśniejsza niż a. Każda z nich jest przydatna do jednostronnego wykrywania zmian – jeśli aplikacja interesuje się tylko pikselami, które stały się jaśniejsze, albo tylko pikselami, które stały się ciemniejsze – ale żadna nie ujmuje wszystkich zmian między dwiema ramkami.

5.9.3. Operacja różnicy

Do dwustronnego wykrywania zmian należy sięgnąć po difference(), która oblicza |self - other| na każdej pozycji – różnicę bezwzględną, bez znaku. Każdy piksel, który zmienił się w którymkolwiek kierunku, pojawia się jako wartość niezerowa w wyniku, a wielkość jest proporcjonalna do tego, jak bardzo zmienił się w danym miejscu.

Ta właściwość – niezerowa dokładnie tam, gdzie dwa obrazy się różnią – czyni difference koniem roboczym wykrywania zmian klatka po klatce. Ramka referencyjna zapisana przy starcie i świeży zrzut, przepuszczone przez difference, dają obraz, którego niezerowe piksele oznaczają każdą pozycję, w której coś w scenie się poruszyło lub zmieniło jasność.

5.9.4. Ograniczanie zakresu za pomocą maski

Wszystkie metody arytmetyczne przyjmują argument słowa kluczowego mask wprowadzony na stronie obszarów i masek. Gdy maska jest przekazana, operacja wykonuje się tylko na pozycjach, gdzie maska jest niezerowa; wszędzie indziej obraz docelowy pozostaje nietknięty.

Taka kompozycja pojawia się w dwóch wzorcach. Pierwszy to ograniczanie operacji do znanego obszaru: na przykład dodawanie dwóch ramek tylko wewnątrz ramki ograniczającej wykrytego znacznika. Drugi to budowanie ramki złożonej kawałek po kawałku – min po sekwencji ramek wewnątrz maski pierwszego planu, max po tej samej sekwencji wewnątrz maski dopełniającej – tego rodzaju wzorzec.

5.9.5. W miejscu i zachowując dane wejściowe

Wszystkie metody arytmetyczne stosują się do konwencji działania ustalonej wcześniej: każda modyfikuje obraz źródłowy w miejscu i zwraca ten sam obraz na potrzeby łańcuchowania. Piksele źródła przepadają po wywołaniu – zostają zastąpione wynikiem operacji wobec tego, co przekazano jako drugi operand.

Gdy aplikacja musi zachować oba dane wejściowe, bezpiecznym wzorcem jest najpierw skopiowanie jednego z nich:

diff = current.copy()       # leaves current intact
diff.difference(reference)  # diff now holds the absolute difference

Ten wzorzec – skopiuj, potem operuj – jest kręgosłupem każdego potoku różnicowania ramek, w którym ramka referencyjna musi przetrwać porównanie, aby można ją było ponownie wykorzystać na kolejnej przechwyconej ramce.

Dzięki sześciu operacjom łączącym, dwóm operacjom na pojedynczym obrazie, koniowi roboczemu różnicy bezwzględnej oraz słowu kluczowemu maski do ograniczania zakresu, zestaw narzędzi arytmetyki na pikselach obejmuje kombinacje jasności i kanałów, których potrzebuje klasyczna wizja maszynowa. Pozostałe narzędzia przypominające arytmetykę z powierzchni działają bit po bicie, a nie wartość po wartości.