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