forensic-graphology / src /preprocessing.py
Fabio Antonini
First implementation
c7ccdd9
import cv2
import numpy as np
import os
from PIL import Image
import fitz # PyMuPDF
class ImagePreprocessor:
"""
Classe per l'acquisizione e pre-elaborazione delle immagini di firme e documenti.
Implementa funzionalità di base come la conversione in scala di grigi,
normalizzazione, scontorno dei timbri, ecc.
"""
def __init__(self):
"""Inizializza il preprocessore di immagini."""
pass
def load_image(self, image_path):
"""
Carica un'immagine da un percorso file.
Args:
image_path (str): Percorso dell'immagine da caricare
Returns:
numpy.ndarray: Immagine caricata in formato BGR
"""
if not os.path.exists(image_path):
raise FileNotFoundError(f"Il file {image_path} non esiste")
# Controlla l'estensione del file
_, ext = os.path.splitext(image_path)
ext = ext.lower()
if ext == '.pdf':
return self.extract_image_from_pdf(image_path)
else:
# Carica l'immagine usando OpenCV
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Impossibile caricare l'immagine {image_path}")
return image
def extract_image_from_pdf(self, pdf_path, page_num=0):
"""
Estrae un'immagine da un file PDF.
Args:
pdf_path (str): Percorso del file PDF
page_num (int): Numero di pagina da cui estrarre l'immagine (default: 0)
Returns:
numpy.ndarray: Immagine estratta in formato BGR
"""
# Apri il documento PDF
doc = fitz.open(pdf_path)
# Controlla se il numero di pagina è valido
if page_num >= len(doc):
raise ValueError(f"Il PDF ha {len(doc)} pagine, ma è stata richiesta la pagina {page_num}")
# Ottieni la pagina
page = doc.load_page(page_num)
# Renderizza la pagina come immagine
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # Fattore di scala 2 per migliore qualità
# Converti in formato immagine
img_data = pix.samples
# Crea un array numpy dall'immagine
img_array = np.frombuffer(img_data, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)
# Se l'immagine è in formato RGB, converti in BGR per OpenCV
if pix.n == 3:
img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
return img_array
def convert_to_grayscale(self, image):
"""
Converte un'immagine in scala di grigi.
Args:
image (numpy.ndarray): Immagine di input in formato BGR
Returns:
numpy.ndarray: Immagine in scala di grigi
"""
if len(image.shape) == 2:
# L'immagine è già in scala di grigi
return image
return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
def normalize_image(self, image):
"""
Normalizza un'immagine per migliorare contrasto e luminosità.
Args:
image (numpy.ndarray): Immagine di input (scala di grigi o BGR)
Returns:
numpy.ndarray: Immagine normalizzata
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.convert_to_grayscale(image)
else:
gray = image
# Applica equalizzazione dell'istogramma
return cv2.equalizeHist(gray)
def detect_and_extract_stamps(self, image, lower_color=None, upper_color=None):
"""
Rileva e estrae i timbri da un'immagine utilizzando il filtraggio del colore.
Args:
image (numpy.ndarray): Immagine di input in formato BGR
lower_color (numpy.ndarray, optional): Limite inferiore del colore in formato HSV
upper_color (numpy.ndarray, optional): Limite superiore del colore in formato HSV
Returns:
tuple: (immagine_originale_senza_timbri, maschera_timbri, timbri_estratti)
"""
# Valori predefiniti per rilevare timbri blu (comuni nei documenti)
if lower_color is None:
lower_color = np.array([100, 50, 50]) # Blu in HSV
if upper_color is None:
upper_color = np.array([140, 255, 255])
# Converti l'immagine in HSV
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Crea una maschera per il colore specificato
mask = cv2.inRange(hsv, lower_color, upper_color)
# Applica operazioni morfologiche per migliorare la maschera
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# Estrai i timbri
stamps = cv2.bitwise_and(image, image, mask=mask)
# Crea un'immagine senza timbri
inv_mask = cv2.bitwise_not(mask)
image_without_stamps = cv2.bitwise_and(image, image, mask=inv_mask)
return image_without_stamps, mask, stamps
def threshold_image(self, image, method='adaptive'):
"""
Applica una soglia all'immagine per binarizzarla.
Args:
image (numpy.ndarray): Immagine in scala di grigi
method (str): Metodo di soglia ('simple', 'adaptive', 'otsu')
Returns:
numpy.ndarray: Immagine binaria
"""
if len(image.shape) > 2:
gray = self.convert_to_grayscale(image)
else:
gray = image
if method == 'simple':
_, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
elif method == 'adaptive':
binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2)
elif method == 'otsu':
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
else:
raise ValueError(f"Metodo di soglia non supportato: {method}")
return binary
def resize_image(self, image, width=None, height=None, keep_aspect_ratio=True):
"""
Ridimensiona un'immagine a una larghezza o altezza specificata.
Args:
image (numpy.ndarray): Immagine di input
width (int, optional): Larghezza desiderata
height (int, optional): Altezza desiderata
keep_aspect_ratio (bool): Mantiene il rapporto d'aspetto originale
Returns:
numpy.ndarray: Immagine ridimensionata
"""
if width is None and height is None:
return image
h, w = image.shape[:2]
if keep_aspect_ratio:
if width is None:
aspect_ratio = height / float(h)
dim = (int(w * aspect_ratio), height)
else:
aspect_ratio = width / float(w)
dim = (width, int(h * aspect_ratio))
else:
dim = (width if width is not None else w, height if height is not None else h)
return cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
def denoise_image(self, image, method='gaussian'):
"""
Applica un filtro di riduzione del rumore all'immagine.
Args:
image (numpy.ndarray): Immagine di input
method (str): Metodo di denoising ('gaussian', 'median', 'bilateral')
Returns:
numpy.ndarray: Immagine filtrata
"""
if method == 'gaussian':
return cv2.GaussianBlur(image, (5, 5), 0)
elif method == 'median':
return cv2.medianBlur(image, 5)
elif method == 'bilateral':
if len(image.shape) > 2:
return cv2.bilateralFilter(image, 9, 75, 75)
else:
# Per immagini in scala di grigi, convertiamo temporaneamente in BGR
temp = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
temp = cv2.bilateralFilter(temp, 9, 75, 75)
return cv2.cvtColor(temp, cv2.COLOR_BGR2GRAY)
else:
raise ValueError(f"Metodo di denoising non supportato: {method}")
def preprocess_signature(self, image_path, resize_width=800):
"""
Pipeline completa di pre-elaborazione per le firme.
Args:
image_path (str): Percorso dell'immagine della firma
resize_width (int): Larghezza a cui ridimensionare l'immagine
Returns:
dict: Dizionario contenente le diverse fasi di pre-elaborazione
"""
# Carica l'immagine
original = self.load_image(image_path)
# Ridimensiona l'immagine
resized = self.resize_image(original, width=resize_width)
# Converti in scala di grigi
gray = self.convert_to_grayscale(resized)
# Normalizza l'immagine
normalized = self.normalize_image(gray)
# Applica denoising
denoised = self.denoise_image(normalized, method='bilateral')
# Applica soglia
binary = self.threshold_image(denoised, method='adaptive')
# Restituisci tutte le fasi di pre-elaborazione
return {
'original': original,
'resized': resized,
'grayscale': gray,
'normalized': normalized,
'denoised': denoised,
'binary': binary
}