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
shapeedtypeprima di elaborare un array. - Usa
uint8per memorizzare immagini standard, ma preferiscifloat32oint16durante i calcoli. - Limita i valori con
np.clipprima 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
- NumPy Quickstart: introduzione ufficiale agli array e alle operazioni fondamentali.
- Documentazione di np.clip: limitazione dei valori in un intervallo.
- Documentazione di astype: conversione del tipo degli array.
- Documentazione di copy: creazione di copie indipendenti di un array.
