# Elaborazione delle immagini: convoluzione, Bayer, istogramma

> Le tre tecniche dietro Image Processing Lab: la convoluzione 2D con kernel, il pattern Bayer dei sensori CMOS col demosaicing, e istogramma ed esposizione.

Pubblicato: 2026-06-24
Aggiornato: 2026-08-25
Ambito: software
Riferimento normativo: Elaborazione digitale delle immagini <https://en.wikipedia.org/wiki/Digital_image_processing>
Repository: <https://github.com/stefanofante/arducam-evk-gui>

Pagina: <https://www.stline.it/wiki/image-processing-lab/>

---

## Cosa copre questa wiki

Le tre tecniche dietro lo strumento [Image Processing Lab](/tools/image-processing-lab/): la **convoluzione** con kernel, il **demosaicing** di un sensore **Bayer** e la lettura di **istogramma** ed **esposizione**. Sono i mattoni elementari di qualsiasi pipeline di immagine, dal software desktop alla camera embedded.

## La convoluzione 2D

La convoluzione è l'operazione di base dei filtri spaziali. Una piccola matrice di pesi — il **kernel** — scorre su ogni pixel; il valore di uscita è la somma pesata del pixel e dei suoi vicini. Ma qui va fatta una distinzione che quasi tutta la letteratura applicativa salta, e che questo strumento ha il dovere di dichiarare, perché sui kernel antisimmetrici cambia il risultato. Con un kernel `K` di lato dispari (a = (lato − 1)/2) e un'immagine `I` esistono due operazioni:

$$
\underbrace{O(x,y)=\sum_{j=-a}^{a}\sum_{i=-a}^{a} K(i,j)\,I(x+i,\;y+j)}_{\text{correlazione}} \qquad \underbrace{O(x,y)=\sum_{j=-a}^{a}\sum_{i=-a}^{a} K(i,j)\,I(x-i,\;y-j)}_{\text{convoluzione}}
$$

Differiscono per il **segno degli indici**: la convoluzione ribalta il kernel, la correlazione no. Questo strumento — come la maggior parte delle librerie di image processing, OpenCV compresa nella sua `filter2D` — implementa la **correlazione**, e la chiama convoluzione per abitudine consolidata.

Sui kernel **simmetrici** (blur, gaussiana, Laplaciano 3×3) le due coincidono esattamente e la distinzione è accademica. Sui kernel **antisimmetrici** no. Su una rampa orizzontale, il Sobel X dà:

```
kernel Sobel X       [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
correlazione (qui)   168  208  208  208  208  208  208  168
convoluzione vera     88   48   48   48   48   48   48   88
```

I due risultati sono **speculari rispetto all'offset 128**: 208 − 128 = +80, 128 − 48 = +80. Il modulo del gradiente è identico, il **segno è invertito** — cioè un bordo che sale viene letto come un bordo che scende. Per la magnitudo (√(Gx² + Gy²)) non cambia nulla; per la **direzione** del gradiente, per il flusso ottico, per qualunque uso in cui il segno conta, cambia tutto. Se serve la convoluzione vera basta ruotare il kernel di 180°.

I tre canali R, G, B si filtrano separatamente. Due dettagli pratici cambiano il risultato:

- **Bordi.** Dove il kernel sborda fuori dall'immagine, le coordinate vengono *clampate* (si estende il pixel di bordo). Esistono anche modalità a specchio o periodiche.
- **Normalizzazione.** Dividere per la somma dei coefficienti mantiene costante la luminanza media: indispensabile per i blur. I kernel a somma zero (bordi, gradienti) non si normalizzano; per renderli visibili si somma un *offset* (tipicamente 128), che porta lo zero al grigio medio.

## Kernel notevoli

| Kernel | Somma | Effetto |
| --- | --- | --- |
| Identità | 1 | nessun effetto (centro 1) |
| Box / Gaussian blur | >0 | media il vicinato → riduce rumore e dettaglio |
| Sharpen | 1 | centro alto, bordi negativi → esalta il contrasto locale |
| Laplaciano | 0 | derivata seconda → contorni in ogni direzione |
| Sobel X / Y | 0 | gradiente direzionale → bordi verticali / orizzontali |
| Emboss | 1 | rilievo illuminato di lato |

I due operatori di **Sobel** stimano le derivate orizzontale e verticale; combinati danno il **modulo del gradiente**, che è il cuore del rilevamento dei contorni:

$$
|\nabla I| = \sqrt{G_x^{2} + G_y^{2}}
$$

### Tre kernel 3×3 scritti per esteso

```
sharpen         Sobel X          box blur (×1/9)
 0 -1  0        -1  0  1          1 1 1
-1  5 -1        -2  0  2          1 1 1
 0 -1  0        -1  0  1          1 1 1
```

Lo **sharpen** ha somma 1 (conserva la luminanza) ma centro alto e bordi negativi: amplifica la differenza fra un pixel e i suoi vicini. Il **Sobel X** ha somma 0 e risponde solo dove l'intensità cambia in orizzontale (il Sobel Y è la sua trasposta). Il **box blur**, normalizzato a 1/9, è la media pura del vicinato.

## Il sensore Bayer e il demosaicing

Un sensore CMOS a colori non misura RGB in ogni pixel. Davanti ai fotositi c'è una matrice di filtri colore (**CFA**, *Color Filter Array*) disposta secondo il **pattern Bayer**: una griglia 2×2 ripetuta con **due verdi**, un rosso e un blu. I verdi sono il doppio perché l'occhio umano è più sensibile al verde, che porta gran parte dell'informazione di luminanza. `RGGB`, `BGGR`, `GRBG` e `GBRG` sono le quattro fasi dello stesso schema.

Poiché ogni pixel registra un solo canale, gli altri due vanno ricostruiti: è il **demosaicing**.

- **Nearest neighbor** — copia il campione dello stesso colore più vicino. Velocissimo, ma con scalettature e blocchi 2×2 visibili.
- **Bilineare** — media i vicini dello stesso colore. Più morbido, ma sui bordi netti può generare due artefatti tipici: lo *zipper* (zig-zag lungo i contorni) e il *color fringing* (frange colorate).

Gli algoritmi che usano davvero le fotocamere sono più sofisticati: invece di mediare alla cieca, riconoscono la direzione dei contorni e interpolano *lungo* i bordi anziché *attraverso*, riducendo gli artefatti.

### Quanta risoluzione costa il mosaico

Il conto è semplice e spiega gli artefatti del demosaicing meglio di qualunque descrizione qualitativa. Su un blocco 2×2 RGGB ogni canale è campionato a una frazione diversa:

| Canale | Campioni su un blocco 2×2 | Passo effettivo | Nyquist (cicli/pixel) |
|---|---|---|---|
| R e B | 1 / 4 = 25 % | 2,00 px | 0,250 |
| G | 2 / 4 = 50 % | 1,41 px | 0,354 |
| sensore monocromatico | 4 / 4 = 100 % | 1,00 px | 0,500 |

Il verde è campionato al doppio degli altri (è il canale su cui si appoggia la luminanza percepita) ma su un reticolo **diagonale**, quindi il suo passo effettivo è √2 pixel e la Nyquist è 0,354 cicli/pixel invece di 0,5. Rosso e blu stanno a 0,25, metà di un sensore monocromatico.

Da qui vengono gli artefatti tipici: un dettaglio fine e **colorato** oltre 0,25 cicli/pixel è sotto-campionato in R e B, e il demosaicing può solo interpolare — producendo *zipper* sui bordi e *false colours* sulle tessiture regolari. È anche il motivo per cui davanti a un sensore Bayer si mette un filtro **anti-aliasing ottico**: rinunciare a un po' di nitidezza costa meno che avere moiré colorato. Un sensore monocromatico, non avendo mosaico, non ha né il problema né il filtro — ed è la ragione per cui in molte applicazioni di visione industriale si sceglie il bianco e nero.

## Istogramma, luma ed esposizione

L'**istogramma** conta quanti pixel cadono in ciascun livello (0–255), separatamente per i tre canali e per una quarta grandezza che merita di essere chiamata col suo nome: la **luma**. Non è la luminanza, e la differenza non è terminologica.

$$
\underbrace{Y' = 0{,}299\,R' + 0{,}587\,G' + 0{,}114\,B'}_{\text{luma, su RGB gamma-encoded}} \qquad \underbrace{Y = 0{,}2126\,R + 0{,}7152\,G + 0{,}0722\,B}_{\text{luminanza, su RGB lineare}}
$$

La **luma Y′** si calcola sui valori RGB **così come stanno nel file**, cioè gamma-encoded: è una scorciatoia nata nella televisione analogica, dove fare la somma pesata prima della correzione di gamma costava un circuito in meno. La **luminanza Y** è una grandezza fotometrica: richiede di **linearizzare** prima i tre canali (per sRGB, l'EOTF con l'esponente 2,4 e il tratto lineare vicino a zero) e usa i pesi del primario Rec.709.

Quanto sono diverse? Su un grigio, per niente. Su un colore saturo, molto:

| Colore | Luma Y′ (601) | Luminanza Y relativa | Y′ del grigio di pari luminanza | Scarto |
|---|---|---|---|---|
| rosso puro | 76,2 | 21,3 % | 127,1 | +50,9 |
| verde puro | 149,7 | 71,5 % | 219,9 | +70,2 |
| blu puro | 29,1 | 7,2 % | 76,0 | +46,9 |
| giallo | 225,9 | 92,8 % | 246,7 | +20,8 |
| ciano | 178,8 | 78,7 % | 229,5 | +50,7 |
| magenta | 105,3 | 28,5 % | 145,4 | +40,1 |
| grigio 128 | 128,0 | 21,6 % | 128,0 | 0 |
| bianco | 255,0 | 100,0 % | 255,0 | 0 |

La colonna «scarto» è la lettura più diretta: un **rosso puro ha luma 76** ma la stessa luminanza di un **grigio 127**. Cinquantuno livelli su 255 di differenza, cioè il rosso saturo appare nell'istogramma della luma a un quinto della scala mentre in luminanza sta a metà. Sul verde puro lo scarto arriva a 70 livelli. Solo la diagonale dei grigi è esatta.

Nella stessa tabella si legge un'altra cosa che sorprende: il **grigio 128, il «mezzo» della scala digitale, vale il 21,6 % della luminanza del bianco**, non il 50 %. È la gamma, e è il motivo per cui «schiarire del 50 %» ha significati diversi a seconda dello spazio in cui si lavora.

C'è poi una terza ambiguità, indipendente dalla gamma: i pesi. Rec.601 (0,299 / 0,587 / 0,114) sono quelli della televisione a definizione standard; Rec.709, usato per l'HD, pesa diversamente (0,2126 / 0,7152 / 0,0722). Applicati agli stessi pixel gamma-encoded:

| Colore | Y′ con pesi 601 | Y′ con pesi 709 | Differenza |
|---|---|---|---|
| rosso puro | 76,2 | 54,2 | −22,0 |
| verde puro | 149,7 | 182,4 | +32,7 |
| blu puro | 29,1 | 18,4 | −10,7 |
| giallo | 225,9 | 236,6 | +10,7 |
| ciano | 178,8 | 200,8 | +22,0 |
| magenta | 105,3 | 72,6 | −32,7 |
| grigio 128 | 128,0 | 128,0 | 0 |
| bianco | 255,0 | 255,0 | 0 |

Fino a **33 livelli** di differenza sul verde e sul magenta — e nessuna delle due è «sbagliata», sono due convenzioni per due sistemi diversi. Questo strumento usa **Rec.601 su valori gamma-encoded**, cioè luma Y′ in senso stretto: perfettamente adatto al suo scopo (valutare esposizione e clipping in fase di acquisizione), da non confondere con una misura fotometrica.

La forma dell'istogramma racconta l'immagine: addensamento a sinistra → scena scura; a destra → scena chiara; distribuzione ampia → buon contrasto. Quando i conteggi si schiacciano contro 0 o 255 c'è **clipping**: il dettaglio nelle ombre o nelle alte luci è perso e non si recupera in post. È il controllo che si fa in fase di acquisizione per impostare correttamente esposizione e guadagno del sensore — *prima* di elaborare l'immagine.

## Dal sensore al codice

Queste non sono nozioni astratte: sono le operazioni quotidiane della messa a punto di una **camera embedded**. Nel bring-up di un sensore CMOS si legge l'istogramma per tarare l'esposizione, si valuta la qualità del demosaicing e si applica un filtro di nitidezza o di riduzione del rumore. Lo strumento da cui nasce questo tool, [`arducam-evk-gui`](https://github.com/stefanofante/arducam-evk-gui), serve proprio a portare in vita un sensore a livello di registri e a ispezionarne l'immagine.

## Limiti

Il tool è **dimostrativo**: gli algoritmi sono didattici, non quelli di un ISP reale; l'immagine viene ridimensionata per restare reattiva; il mosaico Bayer è *simulato* a partire da un'immagine RGB già compressa, non un RAW di sensore; la convoluzione lavora a precisione intera 8-bit con clamp.

## Riferimenti

- B. E. Bayer, brevetto del *Color Filter Array* (1976) — l'origine del pattern Bayer.
- I. Sobel, operatore di gradiente per il rilevamento dei bordi.
- ITU-R BT.601 / BT.709 — coefficienti di luminanza.
- Provalo: [Image Processing Lab](/tools/image-processing-lab/).
