forensic-graphology / src /image_enhancer.py
Fabio Antonini
First implementation
c7ccdd9
import cv2
import numpy as np
import matplotlib.pyplot as plt
from .preprocessing import ImagePreprocessor
class ImageEnhancer:
"""
Classe per l'elaborazione avanzata delle immagini di firme e documenti.
Implementa funzionalità per migliorare la qualità delle immagini,
evidenziare dettagli e applicare filtri speciali per l'analisi forense.
"""
def __init__(self):
"""Inizializza l'enhancer di immagini."""
self.preprocessor = ImagePreprocessor()
def enhance_contrast(self, image, method='clahe'):
"""
Migliora il contrasto di un'immagine.
Args:
image (numpy.ndarray): Immagine di input
method (str): Metodo di miglioramento ('clahe', 'histogram_eq', 'adaptive')
Returns:
numpy.ndarray: Immagine con contrasto migliorato
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
if method == 'clahe':
# Contrast Limited Adaptive Histogram Equalization
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
elif method == 'histogram_eq':
# Equalizzazione dell'istogramma globale
enhanced = cv2.equalizeHist(gray)
elif method == 'adaptive':
# Miglioramento adattivo del contrasto
enhanced = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
else:
raise ValueError(f"Metodo di miglioramento del contrasto non supportato: {method}")
return enhanced
def sharpen_image(self, image, kernel_size=3, strength=1.0):
"""
Applica un filtro di sharpening all'immagine.
Args:
image (numpy.ndarray): Immagine di input
kernel_size (int): Dimensione del kernel
strength (float): Intensità dell'effetto di sharpening
Returns:
numpy.ndarray: Immagine affilata
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Applica un filtro gaussiano per ridurre il rumore
blurred = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# Calcola la maschera di sharpening (immagine originale - immagine sfocata)
mask = cv2.subtract(gray, blurred)
# Applica la maschera all'immagine originale
sharpened = cv2.addWeighted(gray, 1.0, mask, strength, 0)
return sharpened
def apply_edge_detection(self, image, method='canny'):
"""
Applica un rilevatore di bordi all'immagine.
Args:
image (numpy.ndarray): Immagine di input
method (str): Metodo di rilevamento bordi ('canny', 'sobel', 'laplacian')
Returns:
numpy.ndarray: Immagine con bordi rilevati
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Applica un filtro gaussiano per ridurre il rumore
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
if method == 'canny':
# Rilevatore di bordi Canny
edges = cv2.Canny(blurred, 50, 150)
elif method == 'sobel':
# Rilevatore di bordi Sobel
sobelx = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
# Calcola il gradiente
magnitude = cv2.magnitude(sobelx, sobely)
# Normalizza e converti in uint8
edges = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
elif method == 'laplacian':
# Rilevatore di bordi Laplaciano
laplacian = cv2.Laplacian(blurred, cv2.CV_64F)
# Normalizza e converti in uint8
edges = cv2.normalize(laplacian, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
else:
raise ValueError(f"Metodo di rilevamento bordi non supportato: {method}")
return edges
def highlight_pressure_points(self, image, threshold=50):
"""
Evidenzia i punti di pressione in una firma.
Args:
image (numpy.ndarray): Immagine di input
threshold (int): Soglia per considerare un punto come punto di pressione
Returns:
numpy.ndarray: Immagine con punti di pressione evidenziati
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Inverti l'immagine (testo bianco su sfondo nero)
gray_inv = cv2.bitwise_not(gray)
# Applica una soglia per isolare il testo
_, binary = cv2.threshold(gray_inv, threshold, 255, cv2.THRESH_BINARY)
# Crea un'immagine a colori per la visualizzazione
result = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
# Applica una mappa di colori per evidenziare i punti di pressione
# Più scuro è il pixel, maggiore è la pressione
for i in range(gray.shape[0]):
for j in range(gray.shape[1]):
if binary[i, j] > 0:
# Calcola l'intensità normalizzata (0-1)
intensity = gray_inv[i, j] / 255.0
# Applica una mappa di colori (blu -> verde -> rosso)
if intensity < 0.33:
# Blu (bassa pressione)
result[i, j] = [255, 0, 0]
elif intensity < 0.66:
# Verde (media pressione)
result[i, j] = [0, 255, 0]
else:
# Rosso (alta pressione)
result[i, j] = [0, 0, 255]
return result
def extract_profile(self, image, direction='horizontal'):
"""
Estrae il profilo di un'immagine in una direzione specifica.
Args:
image (numpy.ndarray): Immagine di input
direction (str): Direzione del profilo ('horizontal', 'vertical')
Returns:
numpy.ndarray: Profilo estratto
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Inverti l'immagine (testo bianco su sfondo nero)
gray_inv = cv2.bitwise_not(gray)
if direction == 'horizontal':
# Somma i pixel per ogni riga
profile = np.sum(gray_inv, axis=1)
elif direction == 'vertical':
# Somma i pixel per ogni colonna
profile = np.sum(gray_inv, axis=0)
else:
raise ValueError(f"Direzione del profilo non supportata: {direction}")
# Normalizza il profilo
if np.max(profile) > 0:
profile = profile / np.max(profile)
return profile
def visualize_profile(self, image, save_path=None):
"""
Visualizza i profili orizzontale e verticale di un'immagine.
Args:
image (numpy.ndarray): Immagine di input
save_path (str, optional): Percorso dove salvare l'immagine
Returns:
matplotlib.figure.Figure: Figura con la visualizzazione
"""
# Estrai i profili
h_profile = self.extract_profile(image, direction='horizontal')
v_profile = self.extract_profile(image, direction='vertical')
# Crea una figura con più sottografici
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
# Immagine originale
if len(image.shape) > 2:
axs[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
else:
axs[0].imshow(image, cmap='gray')
axs[0].set_title('Immagine Originale')
axs[0].axis('off')
# Profilo orizzontale
axs[1].plot(h_profile, range(len(h_profile)), 'b-')
axs[1].invert_yaxis() # Inverti l'asse y per corrispondere all'immagine
axs[1].set_title('Profilo Orizzontale')
axs[1].set_xlabel('Intensità Normalizzata')
axs[1].set_ylabel('Riga')
# Profilo verticale
axs[2].plot(v_profile, 'r-')
axs[2].set_title('Profilo Verticale')
axs[2].set_xlabel('Colonna')
axs[2].set_ylabel('Intensità Normalizzata')
plt.tight_layout()
# Salva l'immagine se richiesto
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
return fig
def apply_color_filter(self, image, color_range):
"""
Applica un filtro di colore all'immagine.
Args:
image (numpy.ndarray): Immagine di input (BGR)
color_range (dict): Intervallo di colori in formato HSV
{'lower': [h_min, s_min, v_min], 'upper': [h_max, s_max, v_max]}
Returns:
numpy.ndarray: Immagine filtrata
"""
# Verifica che l'immagine sia a colori
if len(image.shape) < 3:
# Converti in BGR se è in scala di grigi
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Converti in HSV
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Crea una maschera per il colore specificato
lower = np.array(color_range['lower'])
upper = np.array(color_range['upper'])
mask = cv2.inRange(hsv, lower, upper)
# Applica la maschera all'immagine originale
filtered = cv2.bitwise_and(image, image, mask=mask)
return filtered
def extract_stamp(self, image):
"""
Estrae i timbri da un'immagine.
Args:
image (numpy.ndarray): Immagine di input (BGR)
Returns:
tuple: (immagine_originale_senza_timbri, timbri_estratti)
"""
# Verifica che l'immagine sia a colori
if len(image.shape) < 3:
# Converti in BGR se è in scala di grigi
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Definisci intervalli di colore per i timbri comuni
color_ranges = [
# Blu (timbri comuni)
{'lower': [100, 50, 50], 'upper': [140, 255, 255]},
# Rosso (timbri comuni)
{'lower': [0, 50, 50], 'upper': [10, 255, 255]},
# Rosso (parte alta dello spettro HSV)
{'lower': [170, 50, 50], 'upper': [180, 255, 255]},
# Viola (alcuni timbri ufficiali)
{'lower': [140, 50, 50], 'upper': [170, 255, 255]}
]
# Converti in HSV
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Crea una maschera combinata per tutti i colori
combined_mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)
for color_range in color_ranges:
lower = np.array(color_range['lower'])
upper = np.array(color_range['upper'])
mask = cv2.inRange(hsv, lower, upper)
combined_mask = cv2.bitwise_or(combined_mask, mask)
# Applica operazioni morfologiche per migliorare la maschera
kernel = np.ones((5, 5), np.uint8)
combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel)
combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel)
# Estrai i timbri
stamps = cv2.bitwise_and(image, image, mask=combined_mask)
# Crea un'immagine senza timbri
inv_mask = cv2.bitwise_not(combined_mask)
image_without_stamps = cv2.bitwise_and(image, image, mask=inv_mask)
return image_without_stamps, stamps
def convert_to_grayscale_enhanced(self, image, method='weighted'):
"""
Converte un'immagine a colori in scala di grigi con metodi avanzati.
Args:
image (numpy.ndarray): Immagine di input (BGR)
method (str): Metodo di conversione ('weighted', 'luminosity', 'desaturation', 'decomposition')
Returns:
numpy.ndarray: Immagine in scala di grigi
"""
# Verifica che l'immagine sia a colori
if len(image.shape) < 3:
return image.copy()
if method == 'weighted':
# Metodo standard (ponderato)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
elif method == 'luminosity':
# Metodo della luminosità (pesi personalizzati)
b, g, r = cv2.split(image)
gray = np.uint8(0.07 * b + 0.72 * g + 0.21 * r)
elif method == 'desaturation':
# Metodo della desaturazione (media di min e max)
b, g, r = cv2.split(image)
min_val = np.minimum(np.minimum(r, g), b)
max_val = np.maximum(np.maximum(r, g), b)
gray = np.uint8((min_val + max_val) / 2)
elif method == 'decomposition':
# Metodo della decomposizione (massimo dei canali)
b, g, r = cv2.split(image)
gray = np.maximum(np.maximum(r, g), b)
else:
raise ValueError(f"Metodo di conversione in scala di grigi non supportato: {method}")
return gray
def apply_emboss_effect(self, image, direction='top-left'):
"""
Applica un effetto di rilievo all'immagine.
Args:
image (numpy.ndarray): Immagine di input
direction (str): Direzione della luce ('top-left', 'top-right', 'bottom-left', 'bottom-right')
Returns:
numpy.ndarray: Immagine con effetto di rilievo
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Definisci il kernel in base alla direzione
if direction == 'top-left':
kernel = np.array([[-1, -1, 0],
[-1, 0, 1],
[0, 1, 1]])
elif direction == 'top-right':
kernel = np.array([[0, -1, -1],
[1, 0, -1],
[1, 1, 0]])
elif direction == 'bottom-left':
kernel = np.array([[0, 1, 1],
[-1, 0, 1],
[-1, -1, 0]])
elif direction == 'bottom-right':
kernel = np.array([[1, 1, 0],
[1, 0, -1],
[0, -1, -1]])
else:
raise ValueError(f"Direzione non supportata: {direction}")
# Applica il filtro
embossed = cv2.filter2D(gray, -1, kernel)
# Aggiungi 128 per spostare i valori nel range medio
embossed = cv2.add(embossed, 128)
return embossed
def create_signature_heatmap(self, image, kernel_size=15):
"""
Crea una mappa di calore della firma per evidenziare le aree di maggiore intensità.
Args:
image (numpy.ndarray): Immagine di input
kernel_size (int): Dimensione del kernel per il filtro gaussiano
Returns:
numpy.ndarray: Mappa di calore della firma
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image.copy()
# Inverti l'immagine (testo bianco su sfondo nero)
gray_inv = cv2.bitwise_not(gray)
# Applica un filtro gaussiano per creare l'effetto di calore
heatmap = cv2.GaussianBlur(gray_inv, (kernel_size, kernel_size), 0)
# Normalizza la mappa di calore
heatmap = cv2.normalize(heatmap, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
# Applica una mappa di colori
heatmap_color = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
# Crea una maschera per isolare la firma
_, mask = cv2.threshold(gray_inv, 10, 255, cv2.THRESH_BINARY)
# Dilata la maschera per includere le aree circostanti
kernel = np.ones((5, 5), np.uint8)
mask_dilated = cv2.dilate(mask, kernel, iterations=2)
# Applica la maschera alla mappa di calore
result = cv2.bitwise_and(heatmap_color, heatmap_color, mask=mask_dilated)
# Crea un'immagine di sfondo bianco
background = np.ones_like(image) * 255
if len(background.shape) < 3:
background = cv2.cvtColor(background, cv2.COLOR_GRAY2BGR)
# Combina lo sfondo con la mappa di calore
mask_dilated_3ch = cv2.cvtColor(mask_dilated, cv2.COLOR_GRAY2BGR) / 255.0
result = background * (1 - mask_dilated_3ch) + result * mask_dilated_3ch
return result.astype(np.uint8)
def enhance_signature(self, image):
"""
Applica una serie di miglioramenti a un'immagine di firma.
Args:
image (numpy.ndarray): Immagine di input
Returns:
dict: Dizionario con diverse versioni migliorate della firma
"""
# Carica l'immagine se è un percorso file
if isinstance(image, str):
image = self.preprocessor.load_image(image)
# Converti in scala di grigi
gray = self.preprocessor.convert_to_grayscale(image)
# Migliora il contrasto
contrast_enhanced = self.enhance_contrast(gray, method='clahe')
# Applica sharpening
sharpened = self.sharpen_image(gray, kernel_size=3, strength=1.5)
# Rileva i bordi
edges = self.apply_edge_detection(gray, method='canny')
# Evidenzia i punti di pressione
pressure_points = self.highlight_pressure_points(gray)
# Applica effetto di rilievo
embossed = self.apply_emboss_effect(gray)
# Crea una mappa di calore
heatmap = self.create_signature_heatmap(gray)
return {
'original': image,
'grayscale': gray,
'contrast_enhanced': contrast_enhanced,
'sharpened': sharpened,
'edges': edges,
'pressure_points': pressure_points,
'embossed': embossed,
'heatmap': heatmap
}