Introduzione all’Elaborazione delle Immagini con Python

'Introduzione all'Elaborazione delle Immagini con Python' can be condensed to 'Introduzione all'Elaborazione Immagini in Python'

Episodio 2: Miglioramenti dell’immagine, Parte 3: Manipolazione dell’istogramma

Tecniche di manipolazione dell'istogramma. Foto dell'autore

Benvenuti alla terza parte del secondo episodio della nostra serie di elaborazione delle immagini! Nelle parti precedenti della serie, abbiamo discusso delle tecniche di trasformata di Fourier e bilanciamento del bianco, e ora esploreremo un’altra tecnica eccitante chiamata Manipolazione dell’istogramma.

Se sei come me, potresti chiederti come si possa manipolare un istogramma. Voglio dire, un istogramma non è semplicemente un grafico che mostra la distribuzione dei valori dei pixel in un’immagine? Beh, si scopre che manipolando l’istogramma possiamo regolare il contrasto e la luminosità di un’immagine, il che può migliorarne notevolmente l’aspetto visivo.

Quindi, immergiamoci nel mondo della manipolazione dell’istogramma e scopriamo come possiamo migliorare il contrasto e la luminosità delle nostre immagini utilizzando diverse tecniche di manipolazione dell’istogramma. Queste tecniche possono essere utilizzate per migliorare la visibilità degli oggetti in immagini con poca luce, per migliorare i dettagli di un’immagine e per correggere immagini sovraesposte o sottoesposte.

Iniziamo importando le librerie Python pertinenti:

Passaggio 1: Importa le librerie, quindi carica e mostra l’immagine

import numpy as npimport matplotlib.pyplot as pltfrom skimage.color import rgb2grayfrom skimage.exposure import histogram, cumulative_distributionfrom skimage import filtersfrom skimage.color import rgb2hsv, rgb2gray, rgb2yuvfrom skimage import color, exposure, transformfrom skimage.exposure import histogram, cumulative_distribution

# Carica l'immagine e rimuovi il canale alfa o di opacità (trasparenza)dark_image = imread('plasma_ball.png')[:,:,:3]# Visualizza l'immagineplt.figure(figsize=(10, 10))plt.title('Immagine originale: Plasma Ball')plt.imshow(dark_image)plt.show()
Output n. 1: Foto dell'autore

Passaggio 2: Verifica le statistiche dei canali e traccia l’istogramma dell’immagine

def calc_color_overcast(image):    # Calcola l'overcast di colore per ogni canale    red_channel = image[:, :, 0]    green_channel = image[:, :, 1]    blue_channel = image[:, :, 2]    # Crea un dataframe per memorizzare i risultati    channel_stats = pd.DataFrame(columns=['Media', 'Deviazione standard', 'Minimo', 'Mediana',                                           'P_80', 'P_90', 'P_99', 'Massimo'])    # Calcola e memorizza le statistiche per ogni canale di colore    for channel, name in zip([red_channel, green_channel, blue_channel],                              ['Rosso', 'Verde', 'Blu']):        media = np.mean(channel)        deviazione_standard = np.std(channel)        minimo = np.min(channel)        mediana = np.median(channel)        p_80 = np.percentile(channel, 80)        p_90 = np.percentile(channel, 90)        p_99 = np.percentile(channel, 99)        massimo = np.max(channel)        channel_stats.loc[name] = [media, deviazione_standard, minimo, mediana, p_80, p_90, p_99, massimo]    return channel_stats

# Questa è la stessa funzione dell'episodio precedente-Parte 2 (Dai un'occhiata!)calc_color_overcast(dark_image)
Output n. 2: DataFrame di Pandas delle statistiche del canale dell'immagine. Foto dell'autore.
# Traccia l'istogramma del'immagine dark_image_intensity = img_as_ubyte(rgb2gray(dark_image))freq, bins = histogram(dark_image_intensity)plt.step(bins, freq*1.0/freq.sum())plt.xlabel('valore di intensità')plt.ylabel('frazione di pixel');
Output #3: Istogramma dei valori di intensità dell'immagine. Foto dell'autore.

Sembra non ci sia un’evidente copertura nuvolosa, ma l’intensità media dei pixel sembra estremamente bassa, confermando l’immagine scura e la sottoesposizione nella visualizzazione dell’immagine. L’istogramma mostra che la maggior parte dei pixel ha valori di bassa intensità, il che ha senso poiché valori di bassa intensità dei pixel significano che la maggior parte dei pixel è molto scura o nera in un’immagine. Possiamo applicare varie tecniche di manipolazione dell’istogramma all’immagine per migliorare il suo contrasto.

Passo 3: Esplorare varie tecniche di manipolazione dell’istogramma

Prima di immergerci nelle diverse tecniche di manipolazione dell’istogramma, cerchiamo di capire la tecnica di manipolazione dell’istogramma comunemente utilizzata chiamata equalizzazione dell’istogramma.

L’equalizzazione dell’istogramma è una tecnica che ridistribuisce le intensità dei pixel in un’immagine per rendere l’istogramma più uniforme. Una distribuzione non uniforme delle intensità dei pixel può risultare in un’immagine con basso contrasto e dettaglio, rendendo difficile distinguere oggetti o caratteristiche all’interno dell’immagine. Rendendo più uniforme la distribuzione delle intensità dei pixel, si migliora il contrasto dell’immagine, rendendo più facile percepire dettagli e caratteristiche.

Un modo per ottenere una distribuzione uniforme delle intensità dei pixel è rendere lineare la funzione di distribuzione cumulativa (CDF) dell’immagine. Questo perché una CDF lineare implica che ogni valore di intensità del pixel ha la stessa probabilità di verificarsi nell’immagine. Una CDF non lineare, d’altra parte, implica che alcuni valori di intensità del pixel si verificano più frequentemente di altri, risultando in una distribuzione non uniforme delle intensità dei pixel. Rendendo lineare la CDF, possiamo rendere più uniforme la distribuzione delle intensità dei pixel e migliorare il contrasto dell’immagine.

def plot_cdf(image):    """    Traccia la funzione di distribuzione cumulativa di un'immagine.        Parametri:    image (ndarray): Immagine di input.    """        # Converti l'immagine in scala di grigi se necessario    if len(image.shape) == 3:        image = rgb2gray(image[:,:,:3])        # Calcola la funzione di distribuzione cumulativa    intensity = np.round(image * 255).astype(np.uint8)    freq, bins = cumulative_distribution(intensity)        # Traccia le CDF effettive e target    target_bins = np.arange(256)    target_freq = np.linspace(0, 1, len(target_bins))    plt.step(bins, freq, c='b', label='CDF effettiva')    plt.plot(target_bins, target_freq, c='r', label='CDF target')        # Traccia un esempio di lookup    example_intensity = 50    example_target = np.interp(freq[example_intensity], target_freq, target_bins)    plt.plot([example_intensity, example_intensity, target_bins[-11], target_bins[-11]],             [0, freq[example_intensity], freq[example_intensity], 0],              'k--',              label=f'Esempio di lookup ({example_intensity} -> {example_target:.0f})')        # Personalizza il grafico    plt.legend()    plt.xlim(0, 255)    plt.ylim(0, 1)    plt.xlabel('Valori di intensità')    plt.ylabel('Frazione cumulativa dei pixel')    plt.title('Funzione di distribuzione cumulativa')        return freq, bins, target_freq, target_bins

dark_image = imread('plasma_ball.png')freq, bins, target_freq, target_bins = plot_cdf(dark_image);
Output #4: Grafico CDF

Il codice calcola la funzione di distribuzione cumulativa (CDF) dell’immagine scura e quindi definisce una CDF target basata su una distribuzione lineare. Successivamente, traccia la CDF effettiva dell’immagine scura in blu e la CDF target (lineare) in rosso. Viene anche tracciata un’esempio di lookup di un valore di intensità, che mostra che la CDF effettiva è 50 nell’esempio e vogliamo che sia 230.

# Esempio di conversione dei valori di intensità dal valore effettivo di 50 al valore target di 230dark_image_230 = dark_image_intensity.copy()dark_image_230[dark_image_230==50] = 230plt.figure(figsize=(10,10))plt.imshow(dark_image_230,cmap='gray');
Output #5: Esempio di risultato dell'immagine in scala di grigi basato sulla conversione del valore effettivo dell'intensità (50) al valore target (230). Foto dell'autore.

Dopo aver ottenuto la CDF target, il passo successivo è calcolare i valori di intensità da utilizzare per sostituire le intensità di pixel originali. Ciò viene fatto utilizzando l’interpolazione per creare una tabella di ricerca.

# Mostra il risultato dopo aver sostituito tutti i valori effettivi con i valori targetnew_vals = np.interp(freq, target_freq, target_bins)dark_image_eq = img_as_ubyte(new_vals[img_as_ubyte(rgb2gray(dark_image[:,:,:3]))].astype(int))plt.figure(figsize=(10,10))plt.imshow(dark_image_eq, cmap='gray');
Output #6: Esempio di risultato dell'immagine in scala di grigi basato sulla conversione di tutti i valori effettivi dell'intensità in valori target. Foto dell'autore.

La funzione np.interp() calcola i valori di intensità da utilizzare per sostituire le intensità di pixel originali interpolando tra le CDF effettive e target. I valori di intensità risultanti vengono quindi utilizzati per sostituire le intensità di pixel originali utilizzando l’indicizzazione di NumPy. Infine, l’immagine equalizzata risultante viene visualizzata utilizzando imshow() in cmap= gray.

Ora che ho mostrato il tipo più semplice di manipolazione dell’istogramma, proviamo diversi tipi di CDF e tecniche e vediamo quale tecnica è adatta per un’immagine data:

def custom_rgb_adjustment(image, target_freq):    target_bins = np.arange(256)    freq_bins = [cumulative_distribution(image[:, :, i]) for i in range(3)]    adjusted_channels = []    # Frequenze con frequenza minima    padded_freqs = []    for i in range(len(freq_bins)):        if len(freq_bins[i][0]) < 256:            frequencies = list(freq_bins[i][0])            min_pad = [min(frequencies)] * (256 - len(frequencies))            frequencies = min_pad + frequencies        else:            frequencies = freq_bins[i][0]        padded_freqs.append(np.array(frequencies))    for n in range(3):        interpolation = np.interp(padded_freqs[n], target_freq, target_bins)        adjusted_channel = img_as_ubyte(interpolation[image[:, :, n]].astype(int))        adjusted_channels.append([adjusted_channel])    adjusted_image = np.dstack((adjusted_channels[0][0], adjusted_channels[1][0], adjusted_channels[2][0]))    return adjusted_image

# Lineartarget_bins = np.arange(256)# Sigmoiddef sigmoid_cdf(x, a=1):    return (1 + np.tanh(a * x)) / 2# Exponentialdef exponential_cdf(x, alpha=1):    return 1 - np.exp(-alpha * x)# Powerdef power_law_cdf(x, alpha=1):    return x ** alpha# Altre tecniche:def adaptive_histogram_equalization(image, clip_limit=0.03, tile_size=(8, 8)):    clahe = exposure.equalize_adapthist(        image, clip_limit=clip_limit, nbins=256, kernel_size=(tile_size[0], tile_size[1]))    return clahedef gamma_correction(image, gamma=1.0):    corrected_image = exposure.adjust_gamma(image, gamma)    return corrected_imagedef contrast_stretching_percentile(image, lower_percentile=5, upper_percentile=95):    in_range = tuple(np.percentile(image, (lower_percentile, upper_percentile)))    stretched_image = exposure.rescale_intensity(image, in_range)    return stretched_imagedef unsharp_masking(image, radius=5, amount=1.0):    blurred_image = filters.gaussian(image, sigma=radius, multichannel=True)    sharpened_image = (image + (image - blurred_image) * amount).clip(0, 1)    return sharpened_imagedef equalize_hist_rgb(image):    equalized_image = exposure.equalize_hist(image)    return equalized_imagedef equalize_hist_hsv(image):    hsv_image = color.rgb2hsv(image[:,:,:3])    hsv_image[:, :, 2] = exposure.equalize_hist(hsv_image[:, :, 2])    hsv_adjusted = color.hsv2rgb(hsv_image)    return hsv_adjusteddef equalize_hist_yuv(image):    yuv_image = color.rgb2yuv(image[:,:,:3])    yuv_image[:, :, 0] = exposure.equalize_hist(yuv_image[:, :, 0])    yuv_adjusted = color.yuv2rgb(yuv_image)    return yuv_adjusted

# Salva ogni tecnica in una variabilelinear = custom_rgb_adjustment(dark_image, np.linspace(0, 1, len(target_bins)))sigmoid = custom_rgb_adjustment(dark_image, sigmoid_cdf((target_bins - 128) / 64, a=1))exponential = custom_rgb_adjustment(dark_image, exponential_cdf(target_bins / 255, alpha=3))power = custom_rgb_adjustment(dark_image, power_law_cdf(target_bins / 255, alpha=2))clahe_image = adaptive_histogram_equalization(    dark_image, clip_limit=0.09, tile_size=(50, 50))gamma_corrected_image = gamma_correction(dark_image, gamma=0.4)sharpened_image = unsharp_masking(dark_image, radius=10, amount=-0.98)cs_image = contrast_stretching_percentile(dark_image, 0, 70)equalized_rgb = equalize_hist_rgb(dark_image)equalized_hsv = equalize_hist_hsv(dark_image)equalized_yuv = equalize_hist_yuv(dark_image)

# Plotfig, axes = plt.subplots(3, 4, figsize=(20, 20))# Immagine originaleaxes[0, 0].imshow(dark_image)axes[0, 0].set_title('Immagine Originale', fontsize=20)# Equalizzazione dell'istogramma: RGB Adjustedaxes[0, 1].imshow(equalized_rgb)axes[0, 1].set_title('RGB Adjusted', fontsize=20)# HSV Adjustedaxes[0, 2].imshow(equalized_hsv)axes[0, 2].set_title('HSV Adjusted', fontsize=20)# YUV Adjustedaxes[0, 3].imshow(equalized_yuv)axes[0, 3].set_title('YUV Adjusted', fontsize=20)# CDF Lineareas[1, 0].imshow(linear)axes[1, 0].set_title('Lineare', fontsize=20)# CDF Sigmoidareas[1, 1].imshow(sigmoid)axes[1, 1].set_title('Sigmoid', fontsize=20)# CDF Esponenzialeareas[1, 2].imshow(exponential)axes[1, 2].set_title('Esponenziale', fontsize=20)# CDF Powerareas[1, 3].imshow(power)axes[1, 3].set_title('Power', fontsize=20)# Stretching del contrastoareas[2, 0].imshow(cs_image)axes[2, 0].set_title('Stretching del Contrast', fontsize=20)# Equalizzazione dell'istogramma adattiva (CLAHE)areas[2, 1].imshow(clahe_image)axes[2, 1].set_title('Equalizzazione dell\'Istogramma Adattiva', fontsize=20)# Correzione Gammaareas[2, 2].imshow(gamma_corrected_image)axes[2, 2].set_title('Correzione Gamma', fontsize=20)# Unsharp Maskingareas[2, 3].imshow(sharpened_image)axes[2, 3].set_title('Unsharp Masking', fontsize=20)# Rimuovi le etichette e gli assi per gli assi in axes.flatten():    ax.set_xticks([])    ax.set_yticks([])plt.tight_layout()plt.show()
Output #7: Plot delle immagini corrette utilizzando varie tecniche di manipolazione dell'istogramma. Foto dell'autore.

Ci sono molte modalità/tecniche per correggere un’immagine in RGB, ma la maggior parte di esse richiede un’aggiustamento manuale dei parametri. L’output n. 7 mostra un plot delle immagini corrette generate utilizzando varie tecniche di manipolazione dell’istogramma.

Le tecniche di regolazione HSV, esponenziale, stretching del contrasto e maschera non nitida sembrano tutte soddisfacenti. Tieni presente che i risultati varieranno in base all’immagine originale utilizzata. A seconda dell’immagine specifica, puoi sperimentare con diversi valori dei parametri per ottenere la qualità dell’immagine desiderata.

Le tecniche di manipolazione dell’istogramma possono migliorare notevolmente il contrasto e l’aspetto generale di un’immagine. Tuttavia, è importante utilizzarle con cura, poiché possono anche introdurre artefatti e dare un aspetto innaturale se utilizzate in modo eccessivo, come evidente da alcune delle tecniche utilizzate nell’output n. 7 (ad esempio, equalizzazione dell’istogramma adattiva con uno sfondo granuloso e bordi sovraemphasized).

A differenza dell’immagine scura utilizzata sopra, ho anche provato ad eseguire i miei codici, con gli stessi valori dei parametri, su una immagine luminosa. Osserviamo cosa è successo qui:

Output n. 8: Foto originale di Johen Redman su Unsplash, altre immagini elaborate dall'autore.

Come avrai notato, la maggior parte delle tecniche che hanno funzionato bene per l’immagine scura non hanno funzionato bene per l’immagine luminosa. Tecniche come regolazione HSV, esponenziale e maschera non nitida hanno dato risultati peggiori e hanno aggiunto artefatti o rumore all’immagine. Ciò potrebbe essere dovuto al fatto che queste tecniche possono migliorare o amplificare la luminosità esistente nell’immagine, portando a sovraesposizione o all’aggiunta di artefatti o rumore.

Tuttavia, è bene sapere che lo stretching del contrasto, nonostante renda alcune parti più luminose dell’immagine originale come previsto, dal momento che lo stretching del contrasto espande letteralmente la gamma dei valori dei pixel in un’immagine per aumentare il contrasto complessivo, fornisce una soluzione più flessibile che può essere utilizzata sia per immagini luminose che scure.

Conclusioni

In questo episodio, ci siamo addentrati nel mondo dell’elaborazione delle immagini, esplorando varie tecniche di miglioramento delle immagini. Abbiamo trattato la Trasformata di Fourier (Parte 1), gli algoritmi di bilanciamento del bianco (Parte 2) e le tecniche di manipolazione dell’istogramma (Parte 3, questa parte), insieme al codice Python pertinente utilizzando la libreria skimage.

In definitiva, la scelta delle tecniche di miglioramento delle immagini più adatte dipende dall’immagine specifica e dai requisiti di qualità dell’output. Come buona pratica, sperimenta con più tecniche di miglioramento delle immagini e regola diversi valori dei parametri per ottenere la qualità desiderata dell’immagine. Spero che questa esplorazione ti abbia aiutato a comprendere meglio l’impatto delle varie tecniche di miglioramento delle immagini.

Mentre continuiamo questo emozionante viaggio nell’elaborazione delle immagini, c’è ancora molto da imparare ed esplorare. Rimani sintonizzato per il prossimo episodio della mia serie Introduzione all’Elaborazione delle Immagini con Python, in cui discuterò tecniche e applicazioni ancora più avanzate!