5.9. Operazioni aritmetiche

La famiglia di disegno della sezione precedente dipinge all’interno di un’immagine. La famiglia aritmetica combina due immagini in una terza – sommando i valori dei loro pixel, sottraendo l’uno dall’altro, prendendo il minimo o il massimo in ogni posizione. Quel piccolo insieme di operazioni aritmetiche pixel per pixel è ciò su cui si basano il frame differencing, la sottrazione dello sfondo, lo stacking dell’esposizione e una manciata di altri pattern classici.

La famiglia aritmetica sulla classe Image è abbastanza piccola da poter essere elencata tutta in una volta:

  • add()self + other per pixel, limitato al massimo del formato.

  • sub()self - other per pixel, limitato a 0 in basso.

  • rsub()other - self per pixel, limitato a 0 (la stessa aritmetica di sub con gli operandi invertiti).

  • min() – minimo per pixel dei due valori.

  • max() – massimo per pixel.

  • difference()|self - other| per pixel, la differenza assoluta.

Più due operazioni correlate su una singola immagine:

  • invert() – sostituisce ogni pixel con 255 - pixel (o il massimo equivalente per il formato).

  • negate() – un alias per invert().

Due barre di gradiente orizzontali in alto che rappresentano le immagini sorgenti A e B -- A va da scuro a luminoso da sinistra a destra, B va da luminoso a scuro da sinistra a destra. Sotto di esse, cinque barre di gradiente che rappresentano il risultato di ogni operazione a coppie applicata ad A e B: A.add(B) appare uniformemente bianco perché ogni posizione somma oltre 255 e viene limitata; A.sub(B) è zero nella metà sinistra e diventa più luminoso verso destra; A.difference(B) mostra una forma a V, luminosa alle estremità e scura al centro; A.min(B) è scuro alle estremità e più luminoso al centro; A.max(B) è luminoso alle estremità e grigio al centro.

Due gradienti sorgente A e B, e il risultato di ogni operazione a coppie applicata ad essi. Ogni operazione viene eseguita posizione per posizione – ciò che appare nel risultato in una qualsiasi posizione dipende solo dai due pixel sorgente in quella posizione.

5.9.1. Due forme di operando

Ciascuno dei metodi a due immagini accetta entrambe le forme per il suo secondo operando:

  • Un’altra Image delle stesse dimensioni. L’aritmetica viene eseguita posizione per posizione – il risultato in (x, y) è l’operazione applicata ai pixel sorgente in (x, y) di entrambe le immagini.

  • Un valore scalare – un intero per la scala di grigi, una tupla (r, g, b) per RGB565. Lo stesso scalare viene applicato in ogni posizione.

La forma scalare è utile quando l’applicazione vuole spostare ogni pixel di una quantità costante. img.add(40) schiarisce l’intera immagine di 40; img.sub((20, 20, 20)) scurisce ogni pixel di 20 per canale; img.max(50) solleva qualsiasi pixel sotto 50 fino a 50 e lascia inalterati gli altri – il tipo di operazione che trasforma un fondo del sensore quasi nero in un grigio scuro uniforme su cui le fasi successive possono lavorare.

5.9.2. Clipping

I valori dei pixel rimangono all’interno dell’intervallo del formato attraverso ogni operazione. Per un canale a 8 bit ciò significa 0255: qualsiasi cosa avrebbe ecceduto oltre 255 viene limitata a 255, e qualsiasi cosa sarebbe scesa sotto 0 viene limitata a 0. Non c’è wrap-around.

Quella scelta è importante nella pratica. add che schiarisce i pixel non produce mai un improvviso artefatto di scurimento all’estremità luminosa dove la matematica andrebbe altrimenti in overflow; sub che scurisce i pixel non produce mai un improvviso artefatto di schiarimento all’estremità scura dove andrebbe altrimenti in underflow. I risultati rimangono visivamente significativi al costo di una certa perdita di informazione agli estremi saturati.

Il clipping è anche il motivo per cui sub e rsub restituiscono risultati diversi tra loro. img_a.sub(img_b) fornisce la parte di a che è più luminosa di b e zero ovunque altrove; img_a.rsub(img_b) fornisce la parte di b che è più luminosa di a. Entrambi sono utili per il rilevamento unilaterale delle variazioni – se all’applicazione interessano solo i pixel che sono diventati più luminosi, o solo i pixel che sono diventati più scuri – ma nessuno dei due cattura tutte le variazioni tra due frame.

5.9.3. L’operazione di differenza

Per il rilevamento bilaterale delle variazioni, l’operazione a cui ricorrere è difference(), che calcola |self - other| in ogni posizione – la differenza assoluta, senza segno. Ogni pixel che è cambiato in entrambe le direzioni appare come un valore diverso da zero nel risultato, con la magnitudine proporzionale a quanto è cambiato in quella posizione.

Quella proprietà – diversa da zero esattamente dove le due immagini differiscono – è ciò che rende difference il cavallo di battaglia del rilevamento delle variazioni frame per frame. Un frame di riferimento memorizzato all’avvio e una cattura fresca, passati attraverso difference, producono un’immagine i cui pixel diversi da zero contrassegnano ogni posizione in cui qualcosa nella scena si è mosso o ha cambiato luminosità.

5.9.4. Delimitazione con mask

Tutti i metodi aritmetici accettano l’argomento keyword mask introdotto nella pagina sulle regioni e le maschere. Quando viene passata una maschera, l’operazione viene eseguita solo nelle posizioni in cui la maschera è diversa da zero; ovunque altrove l’immagine di destinazione viene lasciata inalterata.

Quella composizione compare in due pattern. Il primo è vincolare un’operazione a un’area nota: sommare due frame insieme solo all’interno della bounding box di un marcatore rilevato, per esempio. Il secondo è costruire un frame composito pezzo per pezzo – min su una sequenza di frame all’interno di una maschera in primo piano, max sulla stessa sequenza all’interno della maschera complementare – quel tipo di pattern.

5.9.5. Sul posto, e preservando gli input

I metodi aritmetici seguono tutti la convenzione operativa stabilita in precedenza: ciascuno modifica l’immagine sorgente sul posto e restituisce la stessa immagine per il concatenamento. I pixel della sorgente non ci sono più dopo la chiamata – sostituiti dal risultato dell’operazione rispetto a ciò che è stato passato come secondo operando.

Quando l’applicazione ha bisogno di preservare entrambi gli input, il pattern sicuro è copiarne prima uno:

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

Quel pattern – copia, poi opera – è la spina dorsale di qualsiasi pipeline di frame differencing, dove il frame di riferimento deve sopravvivere al confronto in modo da poter essere riutilizzato sul successivo frame catturato.

Con sei operazioni di combinazione, due operazioni su singola immagine, un cavallo di battaglia per la differenza assoluta e l’argomento keyword mask per la delimitazione, il toolkit di aritmetica sui pixel copre le combinazioni di luminosità e canale di cui ha bisogno la visione artificiale classica. I restanti strumenti simili all’aritmetica nella loro superficie lavorano bit per bit anziché valore per valore.