Elaborare immagini in scala di grigi con NumPy

by theArchitect
SHARE
Elaborare immagini in scala di grigi con NumPy
© Guida-HTML5.it

Introduzione

NumPy viene spesso presentato come una libreria per eseguire calcoli numerici su vettori e matrici. Un’applicazione molto concreta consiste nell’elaborazione di immagini, soprattutto quando lavoriamo con immagini in scala di grigi. In questo caso ogni immagine può essere rappresentata da una matrice bidimensionale: ogni elemento della matrice corrisponde a un pixel.

Il valore di un pixel indica la sua intensità luminosa. Nella rappresentazione più comune:

  • 0 rappresenta il nero;
  • 255 rappresenta il bianco;
  • i valori compresi tra 0 e 255 rappresentano diverse tonalità di grigio.

Questo modello permette di applicare operazioni molto utili usando semplici espressioni NumPy: aumentare il contrasto, invertire i colori, ritagliare una zona dell’immagine, ridimensionare una matrice tramite campionamento e calcolare una sfocatura elementare.

Per mantenere l’esempio autonomo, non useremo file immagine esterni. Creeremo direttamente una piccola immagine artificiale sotto forma di array NumPy. Il metodo è però applicabile anche ad array ottenuti da librerie come Pillow, OpenCV o scikit-image.

Codice completo

import numpy as np

# Creiamo un´immagine artificiale di 8 righe e 10 colonne.
# dtype=np.uint8 è adatto ai pixel con valori da 0 a 255.
immagine = np.array([
    [0,   0,   0,   0,   0,   0,   0,   0,   0,   0],
    [0,  30,  30, 30,  30, 30, 30,  30,  30,  0],
    [0,  30,  80, 80,  80, 80, 80,  80,  30,  0],
    [0,  30,  80, 150, 150, 150, 150, 80,  30,  0],
    [0,  30,  80, 150, 255, 255, 150, 80,  30,  0],
    [0,  30,  80, 150, 150, 150, 150, 80,  30,  0],
    [0,  30,  80,  80,  80,  80,  80,  80,  30,  0],
    [0,   0,   0,   0,   0,   0,   0,   0,   0,   0]
], dtype=np.uint8)

print("Dimensioni:", immagine.shape)
print("Valore minimo:", immagine.min())
print("Valore massimo:", immagine.max())

# Inversione dei colori.
# Convertiamo temporaneamente a un tipo con segno
# per evitare problemi con l´aritmetica su uint8.
invertita = 255 - immagine.astype(np.int16)

# Aumento del contrasto usando una trasformazione lineare.
# I valori inferiori a 60 vengono avvicinati allo zero,
# mentre quelli superiori vengono amplificati.
contrasto = (immagine.astype(np.float32) - 60) * 1.5
contrasto = np.clip(contrasto, 0, 255).astype(np.uint8)

# Ritaglio della parte centrale dell´immagine.
ritaglio = immagine[2:6, 2:8]

# Riduzione semplice: prendiamo un pixel ogni due righe e colonne.
ridotta = immagine[::2, ::2]

# Creiamo una sfocatura elementare tramite una finestra 3x3.
# Per semplicità lasciamo invariato il bordo.
sfocata = immagine.copy()

for riga in range(1, immagine.shape[0] - 1):
    for colonna in range(1, immagine.shape[1] - 1):
        finestra = immagine[riga - 1:riga + 2, colonna - 1:colonna + 2]
        sfocata[riga, colonna] = int(finestra.mean())

print("Ritaglio centrale:")
print(ritaglio)

print("Immagine ridotta:")
print(ridotta)

print("Immagine sfocata:")
print(sfocata)

Spiegazione

Rappresentare un’immagine come matrice

La variabile immagine è un array bidimensionale. La proprietà shape restituisce il numero di righe e colonne. Per esempio, una forma (8, 10) indica un’immagine alta 8 pixel e larga 10 pixel.

Il tipo uint8 occupa un solo byte per elemento e può contenere valori da 0 a 255. È quindi una scelta naturale per i pixel, ma richiede attenzione nelle operazioni matematiche. Se eseguiamo un calcolo che produce valori negativi direttamente su uint8, possiamo ottenere risultati inattesi a causa dell’overflow.

Invertire i colori

L’inversione di un pixel si ottiene sottraendo il suo valore da 255:

pixel_invertito = 255 - pixel

Grazie alle operazioni vettoriali, non è necessario attraversare ogni elemento con un ciclo Python. NumPy applica automaticamente la sottrazione a tutta la matrice. Un pixel nero diventa bianco, mentre un pixel con valore 80 diventa 175.

Modificare il contrasto

La trasformazione:

nuovo_valore = (vecchio_valore - 60) * 1.5

sposta e amplifica i valori. La funzione np.clip è importante perché limita il risultato nell’intervallo valido per un’immagine a 8 bit:

np.clip(valori, 0, 255)

Prima della conversione finale usiamo float32, così possiamo gestire correttamente valori decimali e negativi. Solo dopo il calcolo convertiamo nuovamente il risultato in uint8.

Ritagliare e ridurre un’immagine

Il ritaglio centrale usa la sintassi:

immagine[2:6, 2:8]

Il primo intervallo seleziona le righe, il secondo le colonne. L’espressione immagine[::2, ::2] seleziona invece una riga e una colonna ogni due elementi, producendo una versione più piccola. Questa è una riduzione semplice, non un ridimensionamento di qualità fotografica, ma è utile per comprendere il campionamento di una matrice.

Una sfocatura elementare

Per ogni pixel interno estraiamo una finestra 3x3 e calcoliamo la media dei suoi valori. Il pixel centrale viene quindi sostituito dalla media dei pixel vicini. Questo riduce variazioni improvvise e rumore, anche se per immagini reali esistono tecniche più sofisticate e filtri ottimizzati.

Best practice

  • Controlla sempre shape e dtype prima di elaborare un array.
  • Usa uint8 per memorizzare immagini standard, ma preferisci float32 o int16 durante i calcoli.
  • Limita i valori con np.clip prima della conversione finale.
  • Preferisci operazioni vettoriali NumPy ai cicli Python quando possibile.
  • Quando modifichi un’immagine, crea una copia con array.copy() se vuoi conservare l’originale.
  • Ricorda che una matrice in scala di grigi ha due dimensioni, mentre un’immagine RGB normalmente ha forma (altezza, larghezza, 3).
  • Per immagini grandi, evita di creare inutilmente molte copie: possono consumare rapidamente memoria.

Riepilogo

Un’immagine in scala di grigi può essere elaborata con NumPy come una normale matrice numerica. Abbiamo visto come rappresentare i pixel, invertire i colori, aumentare il contrasto, ritagliare una regione, ridurre la risoluzione e costruire una semplice sfocatura.

Il vantaggio principale è combinare codice compatto e calcolo efficiente. Una volta compreso il collegamento tra pixel e array, è possibile passare a problemi più avanzati: analisi di immagini mediche, riconoscimento di forme, filtri convolutivi e preparazione di dati per modelli di machine learning.

Approfondisci con risorse ufficiali

SHARE