forensic-graphology / src /font_analysis.py
Fabio Antonini
First implementation
c7ccdd9
import cv2
import numpy as np
import pytesseract
from .preprocessing import ImagePreprocessor
class FontAnalyzer:
"""
Classe per l'analisi dei font e il riconoscimento del tipo di inchiostro.
Implementa funzionalità per identificare i font utilizzati nei documenti
e analizzare le caratteristiche dell'inchiostro.
"""
def __init__(self):
"""Inizializza l'analizzatore di font."""
self.preprocessor = ImagePreprocessor()
def detect_text_regions(self, image):
"""
Rileva le regioni di testo in un'immagine.
Args:
image (numpy.ndarray): Immagine di input
Returns:
list: Lista di rettangoli (x, y, w, h) che contengono testo
"""
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image
# Applica soglia per binarizzare l'immagine
binary = self.preprocessor.threshold_image(gray, method='adaptive')
# Applica operazioni morfologiche per connettere i componenti del testo
kernel = np.ones((5, 1), np.uint8) # Kernel rettangolare orizzontale
dilated = cv2.dilate(binary, kernel, iterations=2)
# Trova i contorni
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Filtra i contorni per dimensione
text_regions = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# Filtra i contorni troppo piccoli
if w > 20 and h > 8 and w > h: # Probabilmente testo
text_regions.append((x, y, w, h))
return text_regions
def extract_text(self, image, text_regions=None):
"""
Estrae il testo da un'immagine utilizzando OCR.
Args:
image (numpy.ndarray): Immagine di input
text_regions (list, optional): Lista di regioni di testo (x, y, w, h)
Returns:
dict: Dizionario con il testo estratto e le informazioni sulle regioni
"""
# Se non sono fornite regioni di testo, rileva automaticamente
if text_regions is None:
text_regions = self.detect_text_regions(image)
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image
# Prepara il risultato
result = {
'full_text': '',
'regions': []
}
# Estrai il testo da ciascuna regione
for i, (x, y, w, h) in enumerate(text_regions):
# Estrai la regione
roi = gray[y:y+h, x:x+w]
# Applica miglioramenti all'immagine per OCR
roi = cv2.resize(roi, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
roi = cv2.GaussianBlur(roi, (5, 5), 0)
roi = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
# Esegui OCR
text = pytesseract.image_to_string(roi, config='--psm 6')
# Aggiungi al risultato
if text.strip():
result['full_text'] += text + '\n'
result['regions'].append({
'id': i,
'bbox': (x, y, w, h),
'text': text.strip()
})
return result
def analyze_font(self, image, text_regions=None):
"""
Analizza i font presenti in un'immagine.
Args:
image (numpy.ndarray): Immagine di input
text_regions (list, optional): Lista di regioni di testo (x, y, w, h)
Returns:
dict: Dizionario con le informazioni sui font
"""
# Se non sono fornite regioni di testo, rileva automaticamente
if text_regions is None:
text_regions = self.detect_text_regions(image)
# Converti in scala di grigi se necessario
if len(image.shape) > 2:
gray = self.preprocessor.convert_to_grayscale(image)
else:
gray = image
# Prepara il risultato
result = {
'regions': []
}
# Analizza ciascuna regione
for i, (x, y, w, h) in enumerate(text_regions):
# Estrai la regione
roi = gray[y:y+h, x:x+w]
# Applica miglioramenti all'immagine per OCR
roi = cv2.resize(roi, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
roi = cv2.GaussianBlur(roi, (5, 5), 0)
roi = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
# Esegui OCR con output dettagliato
ocr_data = pytesseract.image_to_data(roi, output_type=pytesseract.Output.DICT)
# Analizza le caratteristiche del font
font_info = self._analyze_font_characteristics(roi, ocr_data)
# Aggiungi al risultato
result['regions'].append({
'id': i,
'bbox': (x, y, w, h),
'font_info': font_info
})
return result
def _analyze_font_characteristics(self, image, ocr_data):
"""
Analizza le caratteristiche del font in una regione di testo.
Args:
image (numpy.ndarray): Immagine della regione di testo
ocr_data (dict): Dati OCR dalla regione
Returns:
dict: Caratteristiche del font
"""
# Inizializza le caratteristiche
font_info = {
'is_serif': False,
'is_monospaced': False,
'is_bold': False,
'is_italic': False,
'font_size': 0,
'confidence': 0,
'possible_fonts': []
}
# Estrai le caratteristiche dai dati OCR
if 'conf' in ocr_data and len(ocr_data['conf']) > 0:
# Calcola la confidenza media
valid_conf = [float(conf) for conf in ocr_data['conf'] if conf != '-1']
if valid_conf:
font_info['confidence'] = sum(valid_conf) / len(valid_conf)
# Analizza la spaziatura per determinare se è monospaced
if 'text' in ocr_data and 'left' in ocr_data and len(ocr_data['text']) > 1:
# Filtra solo le parole valide
valid_indices = [i for i, text in enumerate(ocr_data['text']) if text.strip()]
if len(valid_indices) > 1:
# Calcola le distanze tra le parole
lefts = [ocr_data['left'][i] for i in valid_indices]
widths = [ocr_data['width'][i] for i in valid_indices]
# Calcola la deviazione standard delle larghezze dei caratteri
char_widths = []
for i in valid_indices:
if ocr_data['text'][i] and len(ocr_data['text'][i]) > 0:
char_width = ocr_data['width'][i] / len(ocr_data['text'][i])
char_widths.append(char_width)
if char_widths:
std_dev = np.std(char_widths)
mean_width = np.mean(char_widths)
# Se la deviazione standard è bassa rispetto alla media, è probabilmente monospaced
if std_dev / mean_width < 0.1:
font_info['is_monospaced'] = True
# Analizza l'immagine per determinare se è serif o sans-serif
# Questo è un approccio semplificato basato sul conteggio dei pixel
binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Calcola il numero di pixel bianchi (testo) e neri (sfondo)
white_pixels = cv2.countNonZero(binary)
total_pixels = binary.shape[0] * binary.shape[1]
black_pixels = total_pixels - white_pixels
# Calcola la densità del testo
text_density = white_pixels / total_pixels if total_pixels > 0 else 0
# Applica operazioni morfologiche per rilevare caratteristiche serif
kernel = np.ones((2, 2), np.uint8)
eroded = cv2.erode(binary, kernel, iterations=1)
# Calcola la differenza tra l'immagine originale e quella erosa
diff = cv2.subtract(binary, eroded)
# Conta i pixel nella differenza
diff_pixels = cv2.countNonZero(diff)
# Calcola il rapporto tra i pixel di differenza e i pixel bianchi originali
serif_ratio = diff_pixels / white_pixels if white_pixels > 0 else 0
# Se il rapporto è alto, è probabilmente serif
if serif_ratio > 0.2:
font_info['is_serif'] = True
# Stima la dimensione del font
if 'height' in ocr_data and len(ocr_data['height']) > 0:
valid_heights = [h for h in ocr_data['height'] if h > 0]
if valid_heights:
font_info['font_size'] = sum(valid_heights) / len(valid_heights) / 2 # Approssimazione
# Determina se è grassetto
if text_density > 0.4: # Soglia arbitraria
font_info['is_bold'] = True
# Determina se è corsivo
# Questo richiederebbe un'analisi più complessa dell'inclinazione dei caratteri
# Per ora, utilizziamo un'euristica basata sui dati OCR
if 'text' in ocr_data and 'left' in ocr_data and 'width' in ocr_data:
# Calcola l'inclinazione media dei caratteri
# Questo è un approccio semplificato
font_info['is_italic'] = False # Implementazione semplificata
# Suggerisci possibili font
if font_info['is_serif'] and font_info['is_monospaced']:
font_info['possible_fonts'] = ['Courier', 'Courier New', 'Consolas']
elif font_info['is_serif'] and not font_info['is_monospaced']:
if font_info['is_bold']:
font_info['possible_fonts'] = ['Times New Roman Bold', 'Georgia Bold', 'Garamond Bold']
else:
font_info['possible_fonts'] = ['Times New Roman', 'Georgia', 'Garamond']
elif not font_info['is_serif'] and font_info['is_monospaced']:
font_info['possible_fonts'] = ['Monaco', 'Menlo', 'Lucida Console']
else: # sans-serif, non-monospaced
if font_info['is_bold']:
font_info['possible_fonts'] = ['Arial Bold', 'Helvetica Bold', 'Calibri Bold']
else:
font_info['possible_fonts'] = ['Arial', 'Helvetica', 'Calibri']
return font_info
def analyze_ink(self, image):
"""
Analizza il tipo di inchiostro utilizzato in un'immagine.
Args:
image (numpy.ndarray): Immagine di input
Returns:
dict: Informazioni sul tipo di inchiostro
"""
# 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 per un'analisi migliore del colore
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Estrai i canali HSV
h, s, v = cv2.split(hsv)
# Crea una maschera per isolare l'inchiostro (pixel scuri)
_, ink_mask = cv2.threshold(v, 150, 255, cv2.THRESH_BINARY_INV)
# Applica la maschera ai canali HSV
h_ink = cv2.bitwise_and(h, h, mask=ink_mask)
s_ink = cv2.bitwise_and(s, s, mask=ink_mask)
# Calcola le statistiche dei canali HSV per l'inchiostro
h_values = h_ink[ink_mask > 0]
s_values = s_ink[ink_mask > 0]
v_values = 255 - v[ink_mask > 0] # Inverti V per ottenere l'intensità dell'inchiostro
# Se non ci sono pixel di inchiostro, restituisci un risultato predefinito
if len(h_values) == 0:
return {
'ink_type': 'unknown',
'ink_color': 'unknown',
'is_printed': False,
'confidence': 0,
'details': {
'hue_mean': 0,
'saturation_mean': 0,
'value_mean': 0,
'hue_std': 0,
'saturation_std': 0,
'value_std': 0,
'ink_coverage': 0
}
}
# Calcola le statistiche
hue_mean = np.mean(h_values)
saturation_mean = np.mean(s_values)
value_mean = np.mean(v_values)
hue_std = np.std(h_values)
saturation_std = np.std(s_values)
value_std = np.std(v_values)
# Calcola la copertura dell'inchiostro
ink_coverage = np.count_nonzero(ink_mask) / (ink_mask.shape[0] * ink_mask.shape[1])
# Determina il colore dell'inchiostro
ink_color = self._determine_ink_color(hue_mean, saturation_mean, value_mean)
# Determina se è stampato o scritto a mano
is_printed = self._is_printed_ink(value_std, saturation_std, ink_coverage)
# Determina il tipo di inchiostro
ink_type, confidence = self._determine_ink_type(
hue_mean, saturation_mean, value_mean,
hue_std, saturation_std, value_std,
ink_coverage, is_printed
)
return {
'ink_type': ink_type,
'ink_color': ink_color,
'is_printed': is_printed,
'confidence': confidence,
'details': {
'hue_mean': float(hue_mean),
'saturation_mean': float(saturation_mean),
'value_mean': float(value_mean),
'hue_std': float(hue_std),
'saturation_std': float(saturation_std),
'value_std': float(value_std),
'ink_coverage': float(ink_coverage)
}
}
def _determine_ink_color(self, hue_mean, saturation_mean, value_mean):
"""
Determina il colore dell'inchiostro in base ai valori HSV.
Args:
hue_mean (float): Media del canale H
saturation_mean (float): Media del canale S
value_mean (float): Media del canale V
Returns:
str: Nome del colore dell'inchiostro
"""
# Se la saturazione è bassa, è probabilmente nero o grigio
if saturation_mean < 50:
if value_mean > 200:
return 'black'
else:
return 'gray'
# Altrimenti, determina il colore in base alla tonalità
if 0 <= hue_mean < 30 or 330 <= hue_mean <= 360:
return 'red'
elif 30 <= hue_mean < 90:
return 'yellow'
elif 90 <= hue_mean < 150:
return 'green'
elif 150 <= hue_mean < 210:
return 'cyan'
elif 210 <= hue_mean < 270:
return 'blue'
elif 270 <= hue_mean < 330:
return 'magenta'
else:
return 'unknown'
def _is_printed_ink(self, value_std, saturation_std, ink_coverage):
"""
Determina se l'inchiostro è stampato o scritto a mano.
Args:
value_std (float): Deviazione standard del canale V
saturation_std (float): Deviazione standard del canale S
ink_coverage (float): Percentuale di copertura dell'inchiostro
Returns:
bool: True se l'inchiostro è probabilmente stampato, False altrimenti
"""
# L'inchiostro stampato tende ad avere una deviazione standard più bassa
# e una copertura più uniforme
if value_std < 30 and saturation_std < 20:
return True
# Se la copertura è molto alta, è probabilmente stampato
if ink_coverage > 0.4:
return True
return False
def _determine_ink_type(self, hue_mean, saturation_mean, value_mean,
hue_std, saturation_std, value_std,
ink_coverage, is_printed):
"""
Determina il tipo di inchiostro in base alle statistiche HSV.
Args:
hue_mean (float): Media del canale H
saturation_mean (float): Media del canale S
value_mean (float): Media del canale V
hue_std (float): Deviazione standard del canale H
saturation_std (float): Deviazione standard del canale S
value_std (float): Deviazione standard del canale V
ink_coverage (float): Percentuale di copertura dell'inchiostro
is_printed (bool): Se l'inchiostro è stampato o scritto a mano
Returns:
tuple: (tipo_inchiostro, confidenza)
"""
if is_printed:
# Inchiostro stampato
if saturation_mean < 30 and value_mean > 200:
return 'laser_printer', 0.8
elif saturation_mean < 50:
return 'inkjet_printer', 0.7
else:
return 'color_printer', 0.6
else:
# Inchiostro scritto a mano
if saturation_mean < 30 and value_mean > 200:
# Penna a sfera (biro)
return 'ballpoint_pen', 0.7
elif saturation_mean > 100 and value_std > 40:
# Pennarello
return 'marker', 0.8
elif value_mean < 150 and value_std < 30:
# Penna stilografica
return 'fountain_pen', 0.6
elif saturation_mean < 50 and value_mean < 180:
# Matita
return 'pencil', 0.7
else:
return 'unknown_pen', 0.4