Fabio Antonini commited on
Commit
c7ccdd9
·
1 Parent(s): 41d92cf

First implementation

Browse files
README_forensic_graphology.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Forensic Graphology Application
2
+
3
+ This application provides tools for forensic graphology analysis, including signature comparison, font analysis, ink recognition, and document measurement.
4
+
5
+ ## Features
6
+
7
+ - Image preprocessing and enhancement
8
+ - Signature comparison and verification
9
+ - Font and ink analysis
10
+ - Document measurement and profiling
11
+ - Machine learning for anomaly detection
12
+ - RAG system for document consultation
13
+
14
+ ## How to run
15
+
16
+ ```bash
17
+ pip install -r requirements.txt
18
+ python app.py
19
+ ```
20
+
21
+ ## Deployment on Hugging Face Spaces
22
+
23
+ This application is designed to be deployed on Hugging Face Spaces.
24
+
25
+ title: Forensic Graphology Application
26
+ emoji: 🔍
27
+ colorFrom: indigo
28
+ colorTo: purple
29
+ sdk: gradio
30
+ sdk_version: 5.22.0
31
+ app_file: app.py
32
+ pinned: false
33
+ license: mit
app.py ADDED
@@ -0,0 +1,807 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import numpy as np
4
+ import cv2
5
+ import matplotlib.pyplot as plt
6
+ import tempfile
7
+ from PIL import Image
8
+ import torch
9
+ import time
10
+ import json
11
+
12
+ # Importa i moduli dell'applicazione
13
+ from src.preprocessing import ImagePreprocessor
14
+ from src.signature_analysis import SignatureAnalyzer
15
+ from src.font_analysis import FontAnalyzer
16
+ from src.measurement import MeasurementTool
17
+ from src.image_enhancer import ImageEnhancer
18
+ from src.ml_models import SignatureFeatureExtractor, AnomalyDetector, SignatureVerifier
19
+ from src.rag_system import DocumentProcessor, VectorStore, RAGSystem
20
+
21
+ # Definisci le directory di lavoro
22
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
23
+ UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
24
+ RESULTS_DIR = os.path.join(BASE_DIR, "results")
25
+ MODELS_DIR = os.path.join(BASE_DIR, "models")
26
+ VECTOR_STORE_DIR = os.path.join(BASE_DIR, "vector_store")
27
+
28
+ # Crea le directory se non esistono
29
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
30
+ os.makedirs(RESULTS_DIR, exist_ok=True)
31
+ os.makedirs(MODELS_DIR, exist_ok=True)
32
+ os.makedirs(VECTOR_STORE_DIR, exist_ok=True)
33
+
34
+ # Inizializza i componenti dell'applicazione
35
+ preprocessor = ImagePreprocessor()
36
+ signature_analyzer = SignatureAnalyzer()
37
+ font_analyzer = FontAnalyzer()
38
+ measurement_tool = MeasurementTool()
39
+ image_enhancer = ImageEnhancer()
40
+
41
+ # Inizializza il sistema RAG
42
+ rag_system = RAGSystem(
43
+ upload_dir=UPLOAD_DIR,
44
+ vector_store_dir=VECTOR_STORE_DIR,
45
+ use_local_model=True,
46
+ model_name="google/flan-t5-small"
47
+ )
48
+
49
+ # Inizializza i modelli di machine learning
50
+ # Nota: questi verranno caricati solo quando necessario
51
+ anomaly_detector = None
52
+ signature_verifier = None
53
+
54
+ # Funzione per salvare un'immagine temporanea
55
+ def save_temp_image(image):
56
+ if image is None:
57
+ return None
58
+
59
+ # Crea un file temporaneo
60
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png", dir=UPLOAD_DIR)
61
+ temp_path = temp_file.name
62
+ temp_file.close()
63
+
64
+ # Salva l'immagine
65
+ if isinstance(image, np.ndarray):
66
+ cv2.imwrite(temp_path, image)
67
+ elif isinstance(image, Image.Image):
68
+ image.save(temp_path)
69
+
70
+ return temp_path
71
+
72
+ # Funzione per convertire una figura matplotlib in un'immagine
73
+ def fig_to_image(fig):
74
+ # Salva la figura in un file temporaneo
75
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png", dir=RESULTS_DIR)
76
+ temp_path = temp_file.name
77
+ temp_file.close()
78
+
79
+ # Salva la figura
80
+ fig.savefig(temp_path, dpi=300, bbox_inches='tight')
81
+ plt.close(fig)
82
+
83
+ # Carica l'immagine
84
+ image = cv2.imread(temp_path)
85
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
86
+
87
+ return image, temp_path
88
+
89
+ # Funzione per pre-elaborare un'immagine
90
+ def preprocess_image(image):
91
+ if image is None:
92
+ return None, "Nessuna immagine fornita."
93
+
94
+ try:
95
+ # Salva l'immagine temporaneamente
96
+ temp_path = save_temp_image(image)
97
+
98
+ # Pre-elabora l'immagine
99
+ processed = preprocessor.preprocess_signature(temp_path)
100
+
101
+ # Crea un'immagine di output con tutte le fasi di pre-elaborazione
102
+ h, w = processed['original'].shape[:2]
103
+ output = np.zeros((h * 2, w * 3, 3), dtype=np.uint8)
104
+
105
+ # Converti le immagini in RGB se necessario
106
+ original_rgb = cv2.cvtColor(processed['original'], cv2.COLOR_BGR2RGB)
107
+
108
+ # Converti le immagini in scala di grigi in RGB
109
+ grayscale_rgb = cv2.cvtColor(processed['grayscale'], cv2.COLOR_GRAY2RGB)
110
+ normalized_rgb = cv2.cvtColor(processed['normalized'], cv2.COLOR_GRAY2RGB)
111
+ denoised_rgb = cv2.cvtColor(processed['denoised'], cv2.COLOR_GRAY2RGB)
112
+ binary_rgb = cv2.cvtColor(processed['binary'], cv2.COLOR_GRAY2RGB)
113
+
114
+ # Ridimensiona le immagini se necessario
115
+ original_resized = cv2.resize(original_rgb, (w, h))
116
+ grayscale_resized = cv2.resize(grayscale_rgb, (w, h))
117
+ normalized_resized = cv2.resize(normalized_rgb, (w, h))
118
+ denoised_resized = cv2.resize(denoised_rgb, (w, h))
119
+ binary_resized = cv2.resize(binary_rgb, (w, h))
120
+
121
+ # Inserisci le immagini nell'output
122
+ output[0:h, 0:w] = original_resized
123
+ output[0:h, w:2*w] = grayscale_resized
124
+ output[0:h, 2*w:3*w] = normalized_resized
125
+ output[h:2*h, 0:w] = denoised_resized
126
+ output[h:2*h, w:2*w] = binary_resized
127
+
128
+ # Aggiungi etichette
129
+ font = cv2.FONT_HERSHEY_SIMPLEX
130
+ cv2.putText(output, "Originale", (10, 30), font, 1, (255, 255, 255), 2)
131
+ cv2.putText(output, "Scala di Grigi", (w + 10, 30), font, 1, (255, 255, 255), 2)
132
+ cv2.putText(output, "Normalizzata", (2*w + 10, 30), font, 1, (255, 255, 255), 2)
133
+ cv2.putText(output, "Denoised", (10, h + 30), font, 1, (255, 255, 255), 2)
134
+ cv2.putText(output, "Binaria", (w + 10, h + 30), font, 1, (255, 255, 255), 2)
135
+
136
+ # Salva l'immagine di output
137
+ output_path = os.path.join(RESULTS_DIR, f"preprocessed_{os.path.basename(temp_path)}")
138
+ cv2.imwrite(output_path, cv2.cvtColor(output, cv2.COLOR_RGB2BGR))
139
+
140
+ return output, f"Pre-elaborazione completata. Risultati salvati in {output_path}"
141
+ except Exception as e:
142
+ return None, f"Errore durante la pre-elaborazione: {str(e)}"
143
+
144
+ # Funzione per confrontare due firme
145
+ def compare_signatures(image1, image2):
146
+ if image1 is None or image2 is None:
147
+ return None, "Fornire entrambe le immagini delle firme."
148
+
149
+ try:
150
+ # Salva le immagini temporaneamente
151
+ temp_path1 = save_temp_image(image1)
152
+ temp_path2 = save_temp_image(image2)
153
+
154
+ # Confronta le firme
155
+ comparison_result = signature_analyzer.compare_signatures(temp_path1, temp_path2)
156
+
157
+ # Visualizza il confronto
158
+ fig = signature_analyzer.visualize_comparison(comparison_result)
159
+
160
+ # Converti la figura in un'immagine
161
+ output_image, output_path = fig_to_image(fig)
162
+
163
+ # Genera un report testuale
164
+ report = signature_analyzer.generate_comparison_report(comparison_result)
165
+
166
+ # Salva il report
167
+ report_path = os.path.join(RESULTS_DIR, f"comparison_report_{int(time.time())}.txt")
168
+ with open(report_path, 'w') as f:
169
+ f.write(report)
170
+
171
+ return output_image, f"Confronto completato. Punteggio di similarità: {comparison_result['combined_score']:.2f}%\n\n{report}"
172
+ except Exception as e:
173
+ return None, f"Errore durante il confronto delle firme: {str(e)}"
174
+
175
+ # Funzione per analizzare il font e l'inchiostro
176
+ def analyze_font_and_ink(image):
177
+ if image is None:
178
+ return None, "Nessuna immagine fornita."
179
+
180
+ try:
181
+ # Salva l'immagine temporaneamente
182
+ temp_path = save_temp_image(image)
183
+
184
+ # Carica l'immagine
185
+ img = preprocessor.load_image(temp_path)
186
+
187
+ # Rileva le regioni di testo
188
+ text_regions = font_analyzer.detect_text_regions(img)
189
+
190
+ # Estrai il testo
191
+ text_result = font_analyzer.extract_text(img, text_regions)
192
+
193
+ # Analizza il font
194
+ font_result = font_analyzer.analyze_font(img, text_regions)
195
+
196
+ # Analizza l'inchiostro
197
+ ink_result = font_analyzer.analyze_ink(img)
198
+
199
+ # Crea un'immagine di output
200
+ output = img.copy()
201
+
202
+ # Disegna i rettangoli delle regioni di testo
203
+ for i, (x, y, w, h) in enumerate(text_regions):
204
+ cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 2)
205
+ cv2.putText(output, f"Testo {i+1}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
206
+
207
+ # Converti in RGB per la visualizzazione
208
+ output_rgb = cv2.cvtColor(output, cv2.COLOR_BGR2RGB)
209
+
210
+ # Prepara il report
211
+ report = "ANALISI DEL FONT E DELL'INCHIOSTRO\n"
212
+ report += "=" * 50 + "\n\n"
213
+
214
+ # Aggiungi il testo estratto
215
+ report += "TESTO ESTRATTO:\n"
216
+ report += text_result['full_text'] + "\n\n"
217
+
218
+ # Aggiungi l'analisi del font
219
+ report += "ANALISI DEL FONT:\n"
220
+ for i, region in enumerate(font_result['regions']):
221
+ font_info = region['font_info']
222
+ report += f"Regione {i+1}:\n"
223
+ report += f"- Tipo: {'Serif' if font_info['is_serif'] else 'Sans-serif'}\n"
224
+ report += f"- Monospaced: {'Sì' if font_info['is_monospaced'] else 'No'}\n"
225
+ report += f"- Grassetto: {'Sì' if font_info['is_bold'] else 'No'}\n"
226
+ report += f"- Corsivo: {'Sì' if font_info['is_italic'] else 'No'}\n"
227
+ report += f"- Dimensione stimata: {font_info['font_size']:.1f} pt\n"
228
+ report += f"- Confidenza: {font_info['confidence']:.1f}%\n"
229
+ report += f"- Font possibili: {', '.join(font_info['possible_fonts'])}\n\n"
230
+
231
+ # Aggiungi l'analisi dell'inchiostro
232
+ report += "ANALISI DELL'INCHIOSTRO:\n"
233
+ report += f"- Tipo: {ink_result['ink_type']}\n"
234
+ report += f"- Colore: {ink_result['ink_color']}\n"
235
+ report += f"- Stampato: {'Sì' if ink_result['is_printed'] else 'No'}\n"
236
+ report += f"- Confidenza: {ink_result['confidence']:.1f}%\n\n"
237
+
238
+ report += "DETTAGLI TECNICI:\n"
239
+ report += f"- Tonalità media (H): {ink_result['details']['hue_mean']:.1f}\n"
240
+ report += f"- Saturazione media (S): {ink_result['details']['saturation_mean']:.1f}\n"
241
+ report += f"- Valore medio (V): {ink_result['details']['value_mean']:.1f}\n"
242
+ report += f"- Deviazione standard tonalità: {ink_result['details']['hue_std']:.1f}\n"
243
+ report += f"- Deviazione standard saturazione: {ink_result['details']['saturation_std']:.1f}\n"
244
+ report += f"- Deviazione standard valore: {ink_result['details']['value_std']:.1f}\n"
245
+ report += f"- Copertura inchiostro: {ink_result['details']['ink_coverage']*100:.1f}%\n"
246
+
247
+ # Salva il report
248
+ report_path = os.path.join(RESULTS_DIR, f"font_ink_analysis_{int(time.time())}.txt")
249
+ with open(report_path, 'w') as f:
250
+ f.write(report)
251
+
252
+ return output_rgb, report
253
+ except Exception as e:
254
+ return None, f"Errore durante l'analisi del font e dell'inchiostro: {str(e)}"
255
+
256
+ # Funzione per misurare e profilare un documento
257
+ def measure_document(image):
258
+ if image is None:
259
+ return None, "Nessuna immagine fornita."
260
+
261
+ try:
262
+ # Salva l'immagine temporaneamente
263
+ temp_path = save_temp_image(image)
264
+
265
+ # Carica l'immagine
266
+ img = preprocessor.load_image(temp_path)
267
+
268
+ # Genera il report di misurazione
269
+ measurements = measurement_tool.generate_measurement_report(img)
270
+
271
+ # Visualizza le misurazioni
272
+ fig = measurement_tool.visualize_measurements(img, measurements)
273
+
274
+ # Converti la figura in un'immagine
275
+ output_image, output_path = fig_to_image(fig)
276
+
277
+ # Crea un righello digitale
278
+ ruler_image = measurement_tool.create_digital_ruler(img)
279
+ ruler_path = os.path.join(RESULTS_DIR, f"ruler_{os.path.basename(temp_path)}")
280
+ cv2.imwrite(ruler_path, ruler_image)
281
+
282
+ # Prepara il report
283
+ report = "REPORT DI MISURAZIONE DEL DOCUMENTO\n"
284
+ report += "=" * 50 + "\n\n"
285
+
286
+ # Aggiungi le misurazioni delle linee
287
+ report += "SPAZIO TRA LE LINEE:\n"
288
+ report += f"- Numero di linee: {measurements['line_spacing']['line_count']}\n"
289
+ report += f"- Spazio medio: {measurements['line_spacing']['average_spacing']:.1f} pixel\n"
290
+ report += f"- Deviazione standard: {measurements['line_spacing']['spacing_std']:.1f} pixel\n\n"
291
+
292
+ # Aggiungi le misurazioni delle parole
293
+ report += "SPAZIO TRA LE PAROLE:\n"
294
+ report += f"- Numero di parole: {measurements['word_spacing']['word_count']}\n"
295
+ report += f"- Spazio medio: {measurements['word_spacing']['average_spacing']:.1f} pixel\n"
296
+ report += f"- Deviazione standard: {measurements['word_spacing']['spacing_std']:.1f} pixel\n\n"
297
+
298
+ # Aggiungi i margini
299
+ report += "MARGINI:\n"
300
+ report += f"- Superiore: {measurements['margins']['top']} pixel\n"
301
+ report += f"- Inferiore: {measurements['margins']['bottom']} pixel\n"
302
+ report += f"- Sinistro: {measurements['margins']['left']} pixel\n"
303
+ report += f"- Destro: {measurements['margins']['right']} pixel\n\n"
304
+
305
+ # Aggiungi l'inclinazione dei caratteri
306
+ report += "INCLINAZIONE DEI CARATTERI:\n"
307
+ report += f"- Inclinazione media: {measurements['character_slant']['average_slant']:.1f} gradi\n"
308
+ report += f"- Deviazione standard: {measurements['character_slant']['slant_std']:.1f} gradi\n\n"
309
+
310
+ # Aggiungi il profilo di pressione
311
+ report += "PROFILO DI PRESSIONE:\n"
312
+ report += f"- Pressione media: {measurements['pressure_profile']['average_pressure']:.1f}\n"
313
+ report += f"- Deviazione standard: {measurements['pressure_profile']['pressure_std']:.1f}\n"
314
+
315
+ # Salva il report
316
+ report_path = os.path.join(RESULTS_DIR, f"measurement_report_{int(time.time())}.txt")
317
+ with open(report_path, 'w') as f:
318
+ f.write(report)
319
+
320
+ return output_image, report
321
+ except Exception as e:
322
+ return None, f"Errore durante la misurazione del documento: {str(e)}"
323
+
324
+ # Funzione per migliorare un'immagine
325
+ def enhance_image(image, enhancement_type):
326
+ if image is None:
327
+ return None, "Nessuna immagine fornita."
328
+
329
+ try:
330
+ # Salva l'immagine temporaneamente
331
+ temp_path = save_temp_image(image)
332
+
333
+ # Carica l'immagine
334
+ img = preprocessor.load_image(temp_path)
335
+
336
+ # Applica il miglioramento selezionato
337
+ if enhancement_type == "contrast":
338
+ enhanced = image_enhancer.enhance_contrast(img, method='clahe')
339
+ title = "Miglioramento del Contrasto"
340
+ elif enhancement_type == "sharpen":
341
+ enhanced = image_enhancer.sharpen_image(img, strength=1.5)
342
+ title = "Sharpening dell'Immagine"
343
+ elif enhancement_type == "edges":
344
+ enhanced = image_enhancer.apply_edge_detection(img, method='canny')
345
+ title = "Rilevamento dei Bordi"
346
+ elif enhancement_type == "pressure":
347
+ enhanced = image_enhancer.highlight_pressure_points(img)
348
+ title = "Evidenziazione Punti di Pressione"
349
+ elif enhancement_type == "emboss":
350
+ enhanced = image_enhancer.apply_emboss_effect(img)
351
+ title = "Effetto Rilievo"
352
+ elif enhancement_type == "heatmap":
353
+ enhanced = image_enhancer.create_signature_heatmap(img)
354
+ title = "Mappa di Calore della Firma"
355
+ elif enhancement_type == "all":
356
+ # Applica tutti i miglioramenti
357
+ enhancements = image_enhancer.enhance_signature(img)
358
+
359
+ # Crea un'immagine di output con tutti i miglioramenti
360
+ h, w = enhancements['original'].shape[:2]
361
+ output = np.zeros((h * 2, w * 4, 3), dtype=np.uint8)
362
+
363
+ # Converti le immagini in RGB se necessario
364
+ original_rgb = cv2.cvtColor(enhancements['original'], cv2.COLOR_BGR2RGB)
365
+
366
+ # Converti le immagini in scala di grigi in RGB
367
+ grayscale_rgb = cv2.cvtColor(enhancements['grayscale'], cv2.COLOR_GRAY2RGB)
368
+ contrast_rgb = cv2.cvtColor(enhancements['contrast_enhanced'], cv2.COLOR_GRAY2RGB)
369
+ sharpened_rgb = cv2.cvtColor(enhancements['sharpened'], cv2.COLOR_GRAY2RGB)
370
+ edges_rgb = cv2.cvtColor(enhancements['edges'], cv2.COLOR_GRAY2RGB)
371
+ embossed_rgb = cv2.cvtColor(enhancements['embossed'], cv2.COLOR_GRAY2RGB)
372
+
373
+ # Inserisci le immagini nell'output
374
+ output[0:h, 0:w] = original_rgb
375
+ output[0:h, w:2*w] = grayscale_rgb
376
+ output[0:h, 2*w:3*w] = contrast_rgb
377
+ output[0:h, 3*w:4*w] = sharpened_rgb
378
+ output[h:2*h, 0:w] = edges_rgb
379
+ output[h:2*h, w:2*w] = embossed_rgb
380
+ output[h:2*h, 2*w:3*w] = enhancements['pressure_points']
381
+ output[h:2*h, 3*w:4*w] = enhancements['heatmap']
382
+
383
+ # Aggiungi etichette
384
+ font = cv2.FONT_HERSHEY_SIMPLEX
385
+ cv2.putText(output, "Originale", (10, 30), font, 1, (255, 255, 255), 2)
386
+ cv2.putText(output, "Scala di Grigi", (w + 10, 30), font, 1, (255, 255, 255), 2)
387
+ cv2.putText(output, "Contrasto", (2*w + 10, 30), font, 1, (255, 255, 255), 2)
388
+ cv2.putText(output, "Sharpening", (3*w + 10, 30), font, 1, (255, 255, 255), 2)
389
+ cv2.putText(output, "Bordi", (10, h + 30), font, 1, (255, 255, 255), 2)
390
+ cv2.putText(output, "Rilievo", (w + 10, h + 30), font, 1, (255, 255, 255), 2)
391
+ cv2.putText(output, "Punti di Pressione", (2*w + 10, h + 30), font, 1, (255, 255, 255), 2)
392
+ cv2.putText(output, "Mappa di Calore", (3*w + 10, h + 30), font, 1, (255, 255, 255), 2)
393
+
394
+ enhanced = output
395
+ title = "Tutti i Miglioramenti"
396
+ else:
397
+ return None, f"Tipo di miglioramento non supportato: {enhancement_type}"
398
+
399
+ # Salva l'immagine migliorata
400
+ output_path = os.path.join(RESULTS_DIR, f"{enhancement_type}_{os.path.basename(temp_path)}")
401
+
402
+ # Converti in BGR per il salvataggio se necessario
403
+ if len(enhanced.shape) == 3 and enhanced.shape[2] == 3:
404
+ cv2.imwrite(output_path, cv2.cvtColor(enhanced, cv2.COLOR_RGB2BGR))
405
+ else:
406
+ cv2.imwrite(output_path, enhanced)
407
+
408
+ # Converti in RGB per la visualizzazione se necessario
409
+ if len(enhanced.shape) == 2:
410
+ enhanced_rgb = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB)
411
+ elif enhanced.shape[2] == 3:
412
+ enhanced_rgb = enhanced
413
+ else:
414
+ enhanced_rgb = enhanced
415
+
416
+ return enhanced_rgb, f"{title} completato. Risultato salvato in {output_path}"
417
+ except Exception as e:
418
+ return None, f"Errore durante il miglioramento dell'immagine: {str(e)}"
419
+
420
+ # Funzione per rilevare anomalie in una firma
421
+ def detect_anomalies(image, model_path=None):
422
+ global anomaly_detector
423
+
424
+ if image is None:
425
+ return "Nessuna immagine fornita."
426
+
427
+ try:
428
+ # Salva l'immagine temporaneamente
429
+ temp_path = save_temp_image(image)
430
+
431
+ # Inizializza il rilevatore di anomalie se non è già stato fatto
432
+ if anomaly_detector is None:
433
+ anomaly_detector = AnomalyDetector()
434
+
435
+ # Carica il modello se specificato
436
+ if model_path and os.path.exists(model_path):
437
+ anomaly_detector.load_model(model_path)
438
+ else:
439
+ # Cerca un modello nella directory dei modelli
440
+ model_files = [f for f in os.listdir(MODELS_DIR) if f.endswith('.joblib') and 'anomaly' in f]
441
+ if model_files:
442
+ model_path = os.path.join(MODELS_DIR, model_files[0])
443
+ anomaly_detector.load_model(model_path)
444
+ else:
445
+ return "Nessun modello di rilevamento anomalie trovato. Addestrare un modello prima di utilizzare questa funzione."
446
+
447
+ # Estrai caratteristiche dalla firma
448
+ feature_extractor = SignatureFeatureExtractor()
449
+ features = feature_extractor.extract_features(temp_path)
450
+
451
+ # Rileva anomalie
452
+ result = anomaly_detector.predict(features=features)
453
+
454
+ # Prepara il report
455
+ report = "RILEVAMENTO ANOMALIE NELLA FIRMA\n"
456
+ report += "=" * 50 + "\n\n"
457
+
458
+ report += f"RISULTATO: {'ANOMALIA RILEVATA' if result['is_anomaly'] else 'FIRMA NORMALE'}\n\n"
459
+
460
+ report += f"Punteggio di anomalia: {result['anomaly_score']:.4f}\n"
461
+ report += f"Confidenza: {result['confidence']:.2f}%\n\n"
462
+
463
+ report += "INTERPRETAZIONE:\n"
464
+ if result['is_anomaly']:
465
+ report += "La firma presenta caratteristiche anomale rispetto al modello di riferimento.\n"
466
+ report += "Potrebbe trattarsi di una firma falsa o di una variazione significativa rispetto alle firme autentiche.\n"
467
+ else:
468
+ report += "La firma presenta caratteristiche coerenti con il modello di riferimento.\n"
469
+ report += "È probabile che si tratti di una firma autentica.\n"
470
+
471
+ report += "\nNOTA: Questo risultato è basato su un modello statistico e deve essere interpretato da un esperto di grafologia forense."
472
+
473
+ return report
474
+ except Exception as e:
475
+ return f"Errore durante il rilevamento delle anomalie: {str(e)}"
476
+
477
+ # Funzione per verificare due firme
478
+ def verify_signatures(image1, image2, model_path=None):
479
+ global signature_verifier
480
+
481
+ if image1 is None or image2 is None:
482
+ return "Fornire entrambe le immagini delle firme."
483
+
484
+ try:
485
+ # Salva le immagini temporaneamente
486
+ temp_path1 = save_temp_image(image1)
487
+ temp_path2 = save_temp_image(image2)
488
+
489
+ # Inizializza il verificatore di firme se non è già stato fatto
490
+ if signature_verifier is None:
491
+ signature_verifier = SignatureVerifier()
492
+
493
+ # Carica il modello se specificato
494
+ if model_path and os.path.exists(model_path):
495
+ signature_verifier.load_model(model_path)
496
+ else:
497
+ # Cerca un modello nella directory dei modelli
498
+ model_files = [f for f in os.listdir(MODELS_DIR) if f.endswith('.pth') and 'verifier' in f]
499
+ if model_files:
500
+ model_path = os.path.join(MODELS_DIR, model_files[0])
501
+ signature_verifier.load_model(model_path)
502
+ else:
503
+ return "Nessun modello di verifica firme trovato. Addestrare un modello prima di utilizzare questa funzione."
504
+
505
+ # Verifica le firme
506
+ result = signature_verifier.verify(temp_path1, temp_path2)
507
+
508
+ # Prepara il report
509
+ report = "VERIFICA DELLE FIRME\n"
510
+ report += "=" * 50 + "\n\n"
511
+
512
+ report += f"RISULTATO: {'STESSA PERSONA' if result['is_same_person'] else 'PERSONE DIVERSE'}\n\n"
513
+
514
+ report += f"Probabilità: {result['probability']:.4f}\n"
515
+ report += f"Confidenza: {result['confidence']:.2f}%\n\n"
516
+
517
+ report += "INTERPRETAZIONE:\n"
518
+ if result['is_same_person']:
519
+ report += "Le due firme sono probabilmente della stessa persona.\n"
520
+ report += f"Il modello ha una confidenza del {result['confidence']:.2f}% in questa valutazione.\n"
521
+ else:
522
+ report += "Le due firme sono probabilmente di persone diverse.\n"
523
+ report += f"Il modello ha una confidenza del {result['confidence']:.2f}% in questa valutazione.\n"
524
+
525
+ report += "\nNOTA: Questo risultato è basato su un modello di deep learning e deve essere interpretato da un esperto di grafologia forense."
526
+
527
+ return report
528
+ except Exception as e:
529
+ return f"Errore durante la verifica delle firme: {str(e)}"
530
+
531
+ # Funzione per caricare un documento nel sistema RAG
532
+ def upload_document(file):
533
+ if file is None:
534
+ return "Nessun file fornito."
535
+
536
+ try:
537
+ # Elabora e memorizza il documento
538
+ result = rag_system.process_and_store_document(file)
539
+
540
+ if result['success']:
541
+ return f"Documento '{result['filename']}' caricato e indicizzato con successo.\n\n" + \
542
+ f"ID documento: {result['document_id']}\n" + \
543
+ f"Numero di chunk: {result['chunk_count']}"
544
+ else:
545
+ return f"Errore durante il caricamento del documento: {result['error']}"
546
+ except Exception as e:
547
+ return f"Errore durante il caricamento del documento: {str(e)}"
548
+
549
+ # Funzione per eseguire una query sul sistema RAG
550
+ def query_rag(query_text):
551
+ if not query_text:
552
+ return "Nessuna query fornita."
553
+
554
+ try:
555
+ # Esegui la query
556
+ result = rag_system.query(query_text)
557
+
558
+ if result['success']:
559
+ # Prepara la risposta
560
+ response = f"RISPOSTA:\n{result['response']}\n\n"
561
+
562
+ # Aggiungi i riferimenti
563
+ response += "RIFERIMENTI:\n"
564
+ for ref in result['references']:
565
+ response += f"[{ref['id']}] {ref['filename']} (chunk {ref['chunk_id']+1}/{ref['chunk_total']})\n"
566
+ response += f" Snippet: {ref['snippet']}\n\n"
567
+
568
+ return response
569
+ else:
570
+ return f"Errore durante l'esecuzione della query: {result['error']}"
571
+ except Exception as e:
572
+ return f"Errore durante l'esecuzione della query: {str(e)}"
573
+
574
+ # Funzione per ottenere la lista dei documenti nel sistema RAG
575
+ def get_document_list():
576
+ try:
577
+ # Ottieni la lista dei documenti
578
+ documents = rag_system.get_document_list()
579
+
580
+ if not documents:
581
+ return "Nessun documento trovato nel sistema."
582
+
583
+ # Prepara la risposta
584
+ response = "DOCUMENTI NEL SISTEMA:\n"
585
+ response += "=" * 50 + "\n\n"
586
+
587
+ for i, doc in enumerate(documents):
588
+ response += f"[{i+1}] {doc['filename']}\n"
589
+ response += f" ID: {doc['document_id']}\n"
590
+ response += f" Numero di chunk: {doc['chunk_total']}\n"
591
+ response += f" Data di elaborazione: {doc['processing_date']}\n\n"
592
+
593
+ return response
594
+ except Exception as e:
595
+ return f"Errore durante il recupero della lista dei documenti: {str(e)}"
596
+
597
+ # Funzione per eliminare un documento dal sistema RAG
598
+ def delete_document(document_id):
599
+ if not document_id:
600
+ return "Nessun ID documento fornito."
601
+
602
+ try:
603
+ # Elimina il documento
604
+ result = rag_system.vector_store.delete_document(document_id)
605
+
606
+ if result['success']:
607
+ return f"Documento con ID '{document_id}' eliminato con successo."
608
+ else:
609
+ return f"Errore durante l'eliminazione del documento: {result['error']}"
610
+ except Exception as e:
611
+ return f"Errore durante l'eliminazione del documento: {str(e)}"
612
+
613
+ # Crea l'interfaccia Gradio
614
+ def create_interface():
615
+ # Crea i tab per le diverse funzionalità
616
+ with gr.Blocks(title="Grafologia Forense") as app:
617
+ gr.Markdown("# Applicazione di Grafologia Forense")
618
+ gr.Markdown("Questa applicazione fornisce strumenti per l'analisi forense di firme e documenti.")
619
+
620
+ with gr.Tabs():
621
+ # Tab per la pre-elaborazione delle immagini
622
+ with gr.Tab("Pre-elaborazione"):
623
+ with gr.Row():
624
+ with gr.Column():
625
+ preprocess_input = gr.Image(label="Immagine da pre-elaborare", type="numpy")
626
+ preprocess_button = gr.Button("Pre-elabora")
627
+ with gr.Column():
628
+ preprocess_output = gr.Image(label="Risultato della pre-elaborazione")
629
+ preprocess_text = gr.Textbox(label="Output", lines=5)
630
+
631
+ preprocess_button.click(
632
+ fn=preprocess_image,
633
+ inputs=[preprocess_input],
634
+ outputs=[preprocess_output, preprocess_text]
635
+ )
636
+
637
+ # Tab per la comparazione di firme
638
+ with gr.Tab("Comparazione Firme"):
639
+ with gr.Row():
640
+ with gr.Column():
641
+ compare_input1 = gr.Image(label="Firma 1", type="numpy")
642
+ compare_input2 = gr.Image(label="Firma 2", type="numpy")
643
+ compare_button = gr.Button("Confronta")
644
+ with gr.Column():
645
+ compare_output = gr.Image(label="Risultato del confronto")
646
+ compare_text = gr.Textbox(label="Report", lines=10)
647
+
648
+ compare_button.click(
649
+ fn=compare_signatures,
650
+ inputs=[compare_input1, compare_input2],
651
+ outputs=[compare_output, compare_text]
652
+ )
653
+
654
+ # Tab per l'analisi di font e inchiostro
655
+ with gr.Tab("Analisi Font e Inchiostro"):
656
+ with gr.Row():
657
+ with gr.Column():
658
+ font_input = gr.Image(label="Immagine da analizzare", type="numpy")
659
+ font_button = gr.Button("Analizza")
660
+ with gr.Column():
661
+ font_output = gr.Image(label="Regioni di testo rilevate")
662
+ font_text = gr.Textbox(label="Report", lines=15)
663
+
664
+ font_button.click(
665
+ fn=analyze_font_and_ink,
666
+ inputs=[font_input],
667
+ outputs=[font_output, font_text]
668
+ )
669
+
670
+ # Tab per la misurazione e profilazione
671
+ with gr.Tab("Misurazione e Profilazione"):
672
+ with gr.Row():
673
+ with gr.Column():
674
+ measure_input = gr.Image(label="Documento da misurare", type="numpy")
675
+ measure_button = gr.Button("Misura")
676
+ with gr.Column():
677
+ measure_output = gr.Image(label="Risultato della misurazione")
678
+ measure_text = gr.Textbox(label="Report", lines=15)
679
+
680
+ measure_button.click(
681
+ fn=measure_document,
682
+ inputs=[measure_input],
683
+ outputs=[measure_output, measure_text]
684
+ )
685
+
686
+ # Tab per il miglioramento delle immagini
687
+ with gr.Tab("Miglioramento Immagini"):
688
+ with gr.Row():
689
+ with gr.Column():
690
+ enhance_input = gr.Image(label="Immagine da migliorare", type="numpy")
691
+ enhance_type = gr.Radio(
692
+ label="Tipo di miglioramento",
693
+ choices=["contrast", "sharpen", "edges", "pressure", "emboss", "heatmap", "all"],
694
+ value="contrast"
695
+ )
696
+ enhance_button = gr.Button("Migliora")
697
+ with gr.Column():
698
+ enhance_output = gr.Image(label="Risultato del miglioramento")
699
+ enhance_text = gr.Textbox(label="Output", lines=5)
700
+
701
+ enhance_button.click(
702
+ fn=enhance_image,
703
+ inputs=[enhance_input, enhance_type],
704
+ outputs=[enhance_output, enhance_text]
705
+ )
706
+
707
+ # Tab per il machine learning
708
+ with gr.Tab("Machine Learning"):
709
+ with gr.Tabs():
710
+ # Subtab per il rilevamento di anomalie
711
+ with gr.Tab("Rilevamento Anomalie"):
712
+ with gr.Row():
713
+ with gr.Column():
714
+ anomaly_input = gr.Image(label="Firma da analizzare", type="numpy")
715
+ anomaly_button = gr.Button("Rileva Anomalie")
716
+ with gr.Column():
717
+ anomaly_text = gr.Textbox(label="Report", lines=15)
718
+
719
+ anomaly_button.click(
720
+ fn=detect_anomalies,
721
+ inputs=[anomaly_input],
722
+ outputs=[anomaly_text]
723
+ )
724
+
725
+ # Subtab per la verifica delle firme
726
+ with gr.Tab("Verifica Firme"):
727
+ with gr.Row():
728
+ with gr.Column():
729
+ verify_input1 = gr.Image(label="Firma 1", type="numpy")
730
+ verify_input2 = gr.Image(label="Firma 2", type="numpy")
731
+ verify_button = gr.Button("Verifica")
732
+ with gr.Column():
733
+ verify_text = gr.Textbox(label="Report", lines=15)
734
+
735
+ verify_button.click(
736
+ fn=verify_signatures,
737
+ inputs=[verify_input1, verify_input2],
738
+ outputs=[verify_text]
739
+ )
740
+
741
+ # Tab per il sistema RAG
742
+ with gr.Tab("Sistema RAG"):
743
+ with gr.Tabs():
744
+ # Subtab per il caricamento dei documenti
745
+ with gr.Tab("Caricamento Documenti"):
746
+ with gr.Row():
747
+ with gr.Column():
748
+ upload_input = gr.File(label="Documento da caricare")
749
+ upload_button = gr.Button("Carica")
750
+ with gr.Column():
751
+ upload_text = gr.Textbox(label="Output", lines=5)
752
+
753
+ upload_button.click(
754
+ fn=upload_document,
755
+ inputs=[upload_input],
756
+ outputs=[upload_text]
757
+ )
758
+
759
+ # Subtab per le query
760
+ with gr.Tab("Query"):
761
+ with gr.Row():
762
+ with gr.Column():
763
+ query_input = gr.Textbox(label="Query", lines=3)
764
+ query_button = gr.Button("Esegui Query")
765
+ with gr.Column():
766
+ query_text = gr.Textbox(label="Risposta", lines=15)
767
+
768
+ query_button.click(
769
+ fn=query_rag,
770
+ inputs=[query_input],
771
+ outputs=[query_text]
772
+ )
773
+
774
+ # Subtab per la gestione dei documenti
775
+ with gr.Tab("Gestione Documenti"):
776
+ with gr.Row():
777
+ with gr.Column():
778
+ list_button = gr.Button("Lista Documenti")
779
+ delete_input = gr.Textbox(label="ID Documento da eliminare")
780
+ delete_button = gr.Button("Elimina Documento")
781
+ with gr.Column():
782
+ doc_text = gr.Textbox(label="Output", lines=15)
783
+
784
+ list_button.click(
785
+ fn=get_document_list,
786
+ inputs=[],
787
+ outputs=[doc_text]
788
+ )
789
+
790
+ delete_button.click(
791
+ fn=delete_document,
792
+ inputs=[delete_input],
793
+ outputs=[doc_text]
794
+ )
795
+
796
+ return app
797
+
798
+ # Funzione principale
799
+ def main():
800
+ # Crea l'interfaccia
801
+ app = create_interface()
802
+
803
+ # Avvia l'applicazione
804
+ app.launch(share=True)
805
+
806
+ if __name__ == "__main__":
807
+ main()
docs/technical_docs.md ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentazione Tecnica - Applicazione di Grafologia Forense
2
+
3
+ ## Architettura del Sistema
4
+
5
+ L'applicazione di Grafologia Forense è strutturata in moduli indipendenti che lavorano insieme per fornire un'analisi completa di firme e documenti. L'architettura è basata su Python con un'interfaccia utente Gradio.
6
+
7
+ ### Struttura delle Directory
8
+
9
+ ```
10
+ forensic_graphology/
11
+ ├── app.py # Punto di ingresso dell'applicazione
12
+ ├── requirements.txt # Dipendenze Python
13
+ ├── README.md # Documentazione generale
14
+ ├── hf-space.yaml # Configurazione per Hugging Face Spaces
15
+ ├── src/ # Codice sorgente
16
+ │ ├── preprocessing.py # Pre-elaborazione delle immagini
17
+ │ ├── signature_analysis.py # Analisi delle firme
18
+ │ ├── font_analysis.py # Analisi di font e inchiostro
19
+ │ ├── measurement.py # Strumenti di misurazione
20
+ │ ├── image_enhancer.py # Miglioramento delle immagini
21
+ │ ├── ml_models.py # Modelli di machine learning
22
+ │ └── rag_system.py # Sistema RAG
23
+ ├── models/ # Directory per i modelli addestrati
24
+ ├── uploads/ # Directory per i file caricati
25
+ ├── results/ # Directory per i risultati generati
26
+ ├── vector_store/ # Directory per il vector store
27
+ └── docs/ # Documentazione
28
+ ├── user_guide.md # Guida utente
29
+ └── technical_docs.md # Documentazione tecnica
30
+ ```
31
+
32
+ ## Moduli Principali
33
+
34
+ ### 1. Preprocessing (preprocessing.py)
35
+
36
+ Questo modulo gestisce la pre-elaborazione delle immagini di firme e documenti.
37
+
38
+ **Classi principali:**
39
+ - `ImagePreprocessor`: Classe per la pre-elaborazione delle immagini
40
+
41
+ **Metodi principali:**
42
+ - `load_image(image_path)`: Carica un'immagine da un percorso
43
+ - `convert_to_grayscale(image)`: Converte un'immagine in scala di grigi
44
+ - `normalize_image(image)`: Normalizza un'immagine
45
+ - `denoise_image(image)`: Riduce il rumore in un'immagine
46
+ - `binarize_image(image)`: Converte un'immagine in bianco e nero
47
+ - `preprocess_signature(image_path)`: Applica tutte le fasi di pre-elaborazione a un'immagine di firma
48
+
49
+ ### 2. Signature Analysis (signature_analysis.py)
50
+
51
+ Questo modulo fornisce funzionalità per l'analisi e la comparazione di firme.
52
+
53
+ **Classi principali:**
54
+ - `SignatureAnalyzer`: Classe per l'analisi delle firme
55
+
56
+ **Metodi principali:**
57
+ - `extract_features_orb(image)`: Estrae caratteristiche ORB da un'immagine
58
+ - `extract_signature_metrics(image)`: Estrae metriche grafometriche da una firma
59
+ - `compare_signatures(image1_path, image2_path)`: Confronta due firme
60
+ - `visualize_comparison(comparison_result)`: Visualizza il risultato del confronto
61
+ - `generate_comparison_report(comparison_result)`: Genera un report testuale del confronto
62
+
63
+ ### 3. Font Analysis (font_analysis.py)
64
+
65
+ Questo modulo analizza il tipo di font e l'inchiostro utilizzato nei documenti.
66
+
67
+ **Classi principali:**
68
+ - `FontAnalyzer`: Classe per l'analisi di font e inchiostro
69
+
70
+ **Metodi principali:**
71
+ - `detect_text_regions(image)`: Rileva le regioni di testo in un'immagine
72
+ - `extract_text(image, regions)`: Estrae il testo dalle regioni rilevate
73
+ - `analyze_font(image, regions)`: Analizza il tipo di font
74
+ - `analyze_ink(image)`: Analizza il tipo di inchiostro
75
+
76
+ ### 4. Measurement (measurement.py)
77
+
78
+ Questo modulo fornisce strumenti per la misurazione di vari aspetti dei documenti.
79
+
80
+ **Classi principali:**
81
+ - `MeasurementTool`: Classe per la misurazione dei documenti
82
+
83
+ **Metodi principali:**
84
+ - `measure_line_spacing(image)`: Misura lo spazio tra le linee
85
+ - `measure_word_spacing(image)`: Misura lo spazio tra le parole
86
+ - `measure_margins(image)`: Misura i margini del documento
87
+ - `measure_character_slant(image)`: Misura l'inclinazione dei caratteri
88
+ - `create_digital_ruler(image)`: Crea un righello digitale
89
+ - `generate_measurement_report(image)`: Genera un report completo di misurazione
90
+
91
+ ### 5. Image Enhancer (image_enhancer.py)
92
+
93
+ Questo modulo fornisce funzionalità per il miglioramento delle immagini.
94
+
95
+ **Classi principali:**
96
+ - `ImageEnhancer`: Classe per il miglioramento delle immagini
97
+
98
+ **Metodi principali:**
99
+ - `enhance_contrast(image, method)`: Migliora il contrasto di un'immagine
100
+ - `sharpen_image(image, kernel_size, strength)`: Applica un filtro di sharpening
101
+ - `apply_edge_detection(image, method)`: Applica un rilevatore di bordi
102
+ - `highlight_pressure_points(image)`: Evidenzia i punti di pressione
103
+ - `apply_emboss_effect(image)`: Applica un effetto di rilievo
104
+ - `create_signature_heatmap(image)`: Crea una mappa di calore della firma
105
+
106
+ ### 6. Machine Learning Models (ml_models.py)
107
+
108
+ Questo modulo implementa modelli di machine learning per l'analisi delle firme.
109
+
110
+ **Classi principali:**
111
+ - `SignatureFeatureExtractor`: Estrae caratteristiche dalle firme
112
+ - `AnomalyDetector`: Rileva anomalie nelle firme usando Isolation Forest
113
+ - `SignatureVerifier`: Verifica l'autenticità delle firme usando una rete siamese
114
+ - `SiameseNetwork`: Implementazione della rete neurale siamese
115
+
116
+ **Metodi principali:**
117
+ - `extract_features(image_path)`: Estrae caratteristiche da un'immagine di firma
118
+ - `fit(signatures_df)`: Addestra il modello di rilevamento anomalie
119
+ - `predict(signature_path)`: Predice se una firma è anomala
120
+ - `verify(image_path1, image_path2)`: Verifica se due firme sono della stessa persona
121
+
122
+ ### 7. RAG System (rag_system.py)
123
+
124
+ Questo modulo implementa un sistema RAG per la consultazione di documenti.
125
+
126
+ **Classi principali:**
127
+ - `DocumentProcessor`: Elabora e estrae testo dai documenti
128
+ - `VectorStore`: Gestisce il vector store per il sistema RAG
129
+ - `RAGSystem`: Implementa il sistema RAG completo
130
+
131
+ **Metodi principali:**
132
+ - `extract_text(file_path)`: Estrae il testo da un documento
133
+ - `process_document(file_path)`: Elabora un documento e lo divide in chunk
134
+ - `add_document(document_info)`: Aggiunge un documento al vector store
135
+ - `search(query, k)`: Cerca documenti simili a una query
136
+ - `query(query_text)`: Esegue una query sul sistema RAG
137
+
138
+ ## Interfaccia Utente (app.py)
139
+
140
+ L'interfaccia utente è implementata utilizzando Gradio, una libreria Python per la creazione di interfacce web per modelli di machine learning.
141
+
142
+ **Funzioni principali:**
143
+ - `preprocess_image(image)`: Pre-elabora un'immagine
144
+ - `compare_signatures(image1, image2)`: Confronta due firme
145
+ - `analyze_font_and_ink(image)`: Analizza font e inchiostro
146
+ - `measure_document(image)`: Misura un documento
147
+ - `enhance_image(image, enhancement_type)`: Migliora un'immagine
148
+ - `detect_anomalies(image)`: Rileva anomalie in una firma
149
+ - `verify_signatures(image1, image2)`: Verifica due firme
150
+ - `upload_document(file)`: Carica un documento nel sistema RAG
151
+ - `query_rag(query_text)`: Esegue una query sul sistema RAG
152
+
153
+ ## Dipendenze Principali
154
+
155
+ - **OpenCV**: Elaborazione delle immagini
156
+ - **NumPy**: Operazioni numeriche
157
+ - **Pandas**: Manipolazione dei dati
158
+ - **Matplotlib**: Visualizzazione
159
+ - **Scikit-learn**: Algoritmi di machine learning
160
+ - **PyTorch**: Deep learning
161
+ - **Gradio**: Interfaccia utente
162
+ - **LangChain**: Framework per il sistema RAG
163
+ - **Sentence-Transformers**: Modelli di embedding
164
+ - **ChromaDB**: Database vettoriale
165
+ - **PyMuPDF, python-docx, python-pptx**: Estrazione di testo da documenti
166
+ - **pytesseract**: OCR per l'estrazione di testo dalle immagini
167
+
168
+ ## Deployment
169
+
170
+ L'applicazione è progettata per essere deployata su Hugging Face Spaces, una piattaforma per l'hosting di applicazioni di machine learning.
171
+
172
+ **File di configurazione:**
173
+ - `requirements.txt`: Elenca tutte le dipendenze Python
174
+ - `hf-space.yaml`: Configura l'ambiente Hugging Face Spaces
175
+ - `README.md`: Contiene metadati per Hugging Face Spaces
176
+
177
+ ## Estensione dell'Applicazione
178
+
179
+ ### Aggiungere Nuove Funzionalità
180
+
181
+ Per aggiungere nuove funzionalità all'applicazione:
182
+
183
+ 1. Creare un nuovo modulo in `src/` o estendere un modulo esistente
184
+ 2. Implementare la logica della nuova funzionalità
185
+ 3. Aggiungere una nuova funzione in `app.py` che utilizza la nuova funzionalità
186
+ 4. Aggiungere un nuovo tab o elemento UI in `create_interface()` in `app.py`
187
+
188
+ ### Addestrare Nuovi Modelli
189
+
190
+ Per addestrare nuovi modelli di machine learning:
191
+
192
+ 1. Raccogliere un dataset di firme (autentiche e false per il verificatore, solo autentiche per il rilevatore di anomalie)
193
+ 2. Utilizzare le classi `AnomalyDetector` o `SignatureVerifier` per addestrare i modelli
194
+ 3. Salvare i modelli addestrati nella directory `models/`
195
+ 4. Aggiornare l'applicazione per utilizzare i nuovi modelli
196
+
197
+ ## Considerazioni sulla Sicurezza
198
+
199
+ - L'applicazione non memorizza le immagini caricate a lungo termine
200
+ - I documenti caricati nel sistema RAG sono memorizzati localmente
201
+ - Non vengono utilizzate API esterne per l'elaborazione dei dati
202
+ - Il sistema RAG funziona in modalità di sola ricerca per evitare la necessità di token API
203
+
204
+ ## Limitazioni Tecniche
205
+
206
+ - L'OCR potrebbe non funzionare correttamente con testi in lingue non latine
207
+ - I modelli di machine learning richiedono un addestramento specifico per casi d'uso particolari
208
+ - L'analisi del font e dell'inchiostro ha una precisione limitata
209
+ - Il sistema RAG funziona in modalità di sola ricerca, senza generazione di risposte AI
210
+
211
+ ## Risoluzione dei Problemi
212
+
213
+ - **Errori di memoria**: Ridurre la dimensione delle immagini o utilizzare batch più piccoli
214
+ - **Errori di OCR**: Migliorare la qualità delle immagini o utilizzare pre-elaborazione
215
+ - **Prestazioni lente**: Ottimizzare i parametri dei modelli o utilizzare hardware più potente
216
+
217
+ ## Riferimenti
218
+
219
+ - [OpenCV Documentation](https://docs.opencv.org/)
220
+ - [Scikit-learn Documentation](https://scikit-learn.org/stable/documentation.html)
221
+ - [PyTorch Documentation](https://pytorch.org/docs/stable/index.html)
222
+ - [Gradio Documentation](https://gradio.app/docs/)
223
+ - [LangChain Documentation](https://python.langchain.com/docs/get_started/introduction)
docs/user_guide.md ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Guida Utente - Applicazione di Grafologia Forense
2
+
3
+ ## Introduzione
4
+
5
+ Benvenuti nell'applicazione di Grafologia Forense, uno strumento completo per l'analisi e la verifica di firme e documenti. Questa applicazione combina tecniche di elaborazione delle immagini, machine learning e sistemi di recupero delle informazioni per fornire un'analisi dettagliata di firme e documenti.
6
+
7
+ ## Funzionalità Principali
8
+
9
+ L'applicazione è organizzata in diverse sezioni, ciascuna dedicata a specifiche funzionalità:
10
+
11
+ ### 1. Pre-elaborazione
12
+
13
+ Questa sezione permette di caricare e pre-elaborare le immagini di firme e documenti. Il processo di pre-elaborazione include:
14
+ - Conversione in scala di grigi
15
+ - Normalizzazione dell'immagine
16
+ - Riduzione del rumore
17
+ - Binarizzazione
18
+
19
+ **Come utilizzare:**
20
+ 1. Caricare un'immagine utilizzando il pulsante di upload
21
+ 2. Cliccare su "Pre-elabora"
22
+ 3. Visualizzare i risultati della pre-elaborazione
23
+
24
+ ### 2. Comparazione Firme
25
+
26
+ Questa sezione permette di confrontare due firme per determinare il loro grado di similarità. L'analisi include:
27
+ - Estrazione di caratteristiche dalle firme
28
+ - Calcolo di metriche di similarità
29
+ - Generazione di un report dettagliato
30
+
31
+ **Come utilizzare:**
32
+ 1. Caricare due immagini di firme
33
+ 2. Cliccare su "Confronta"
34
+ 3. Analizzare il report di similarità generato
35
+
36
+ ### 3. Analisi Font e Inchiostro
37
+
38
+ Questa sezione analizza il tipo di font e l'inchiostro utilizzato in un documento. L'analisi include:
39
+ - Rilevamento delle regioni di testo
40
+ - Estrazione del testo
41
+ - Analisi del font (serif/sans-serif, monospaced, grassetto, corsivo)
42
+ - Analisi dell'inchiostro (tipo, colore, stampato/manoscritto)
43
+
44
+ **Come utilizzare:**
45
+ 1. Caricare un'immagine contenente testo
46
+ 2. Cliccare su "Analizza"
47
+ 3. Esaminare il report dettagliato sul font e l'inchiostro
48
+
49
+ ### 4. Misurazione e Profilazione
50
+
51
+ Questa sezione fornisce strumenti per misurare vari aspetti di un documento, come:
52
+ - Spazio tra le linee
53
+ - Spazio tra le parole
54
+ - Margini
55
+ - Inclinazione dei caratteri
56
+ - Profilo di pressione
57
+
58
+ **Come utilizzare:**
59
+ 1. Caricare un'immagine di un documento
60
+ 2. Cliccare su "Misura"
61
+ 3. Analizzare le misurazioni e i grafici generati
62
+
63
+ ### 5. Miglioramento Immagini
64
+
65
+ Questa sezione offre vari filtri e tecniche per migliorare la qualità delle immagini:
66
+ - Miglioramento del contrasto
67
+ - Sharpening
68
+ - Rilevamento dei bordi
69
+ - Evidenziazione dei punti di pressione
70
+ - Effetto rilievo
71
+ - Mappa di calore
72
+
73
+ **Come utilizzare:**
74
+ 1. Caricare un'immagine
75
+ 2. Selezionare il tipo di miglioramento desiderato
76
+ 3. Cliccare su "Migliora"
77
+ 4. Visualizzare l'immagine migliorata
78
+
79
+ ### 6. Machine Learning
80
+
81
+ Questa sezione include due strumenti basati su machine learning:
82
+
83
+ #### 6.1 Rilevamento Anomalie
84
+ Utilizza algoritmi di Isolation Forest per rilevare anomalie nelle firme.
85
+
86
+ **Come utilizzare:**
87
+ 1. Caricare un'immagine di firma
88
+ 2. Cliccare su "Rileva Anomalie"
89
+ 3. Analizzare il report che indica se la firma è anomala
90
+
91
+ #### 6.2 Verifica Firme
92
+ Utilizza una rete neurale siamese per verificare se due firme appartengono alla stessa persona.
93
+
94
+ **Come utilizzare:**
95
+ 1. Caricare due immagini di firme
96
+ 2. Cliccare su "Verifica"
97
+ 3. Analizzare il report che indica la probabilità che le firme siano della stessa persona
98
+
99
+ ### 7. Sistema RAG
100
+
101
+ Questa sezione permette di caricare, consultare e gestire documenti utilizzando un sistema RAG (Retrieval Augmented Generation).
102
+
103
+ #### 7.1 Caricamento Documenti
104
+ **Come utilizzare:**
105
+ 1. Caricare un documento (PDF, DOCX, PPTX, TXT)
106
+ 2. Cliccare su "Carica"
107
+ 3. Verificare che il documento sia stato indicizzato correttamente
108
+
109
+ #### 7.2 Query
110
+ **Come utilizzare:**
111
+ 1. Inserire una domanda o query nel campo di testo
112
+ 2. Cliccare su "Esegui Query"
113
+ 3. Leggere la risposta generata in base ai documenti caricati
114
+
115
+ #### 7.3 Gestione Documenti
116
+ **Come utilizzare:**
117
+ 1. Cliccare su "Lista Documenti" per vedere tutti i documenti caricati
118
+ 2. Per eliminare un documento, inserire l'ID del documento e cliccare su "Elimina Documento"
119
+
120
+ ## Consigli per Ottenere Risultati Ottimali
121
+
122
+ 1. **Qualità delle immagini**: Utilizzare immagini ad alta risoluzione per ottenere risultati migliori.
123
+ 2. **Illuminazione**: Assicurarsi che le immagini siano ben illuminate e non abbiano ombre eccessive.
124
+ 3. **Contrasto**: Le immagini con buon contrasto tra testo/firma e sfondo producono risultati migliori.
125
+ 4. **Formati supportati**: L'applicazione supporta i formati immagine più comuni (JPG, PNG) e vari formati di documento (PDF, DOCX, PPTX, TXT).
126
+
127
+ ## Limitazioni
128
+
129
+ 1. Il sistema RAG funziona in modalità di sola ricerca, senza generazione di risposte AI.
130
+ 2. I modelli di machine learning richiedono un addestramento specifico per casi d'uso particolari.
131
+ 3. L'analisi del font e dell'inchiostro potrebbe non essere accurata per scritture molto stilizzate o inusuali.
132
+
133
+ ## Risoluzione dei Problemi
134
+
135
+ Se riscontri problemi con l'applicazione, prova le seguenti soluzioni:
136
+
137
+ 1. **Immagini non caricate correttamente**: Verifica che il formato dell'immagine sia supportato e che la dimensione non sia eccessiva.
138
+ 2. **Errori nell'analisi**: Prova a migliorare la qualità dell'immagine o a utilizzare la sezione di pre-elaborazione prima dell'analisi.
139
+ 3. **Prestazioni lente**: Le operazioni di machine learning possono richiedere tempo, specialmente su immagini di grandi dimensioni.
140
+
141
+ Per ulteriori informazioni o assistenza, consulta la documentazione tecnica o contatta il supporto.
hf-space.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ sdk: gradio
2
+ sdk_version: 5.22.0
3
+ app_file: app.py
requirements.txt ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==23.2.1
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.11.14
4
+ aiosignal==1.3.2
5
+ annotated-types==0.7.0
6
+ anyio==4.9.0
7
+ asgiref==3.8.1
8
+ async-timeout==4.0.3
9
+ attrs==25.3.0
10
+ backoff==2.2.1
11
+ bcrypt==4.3.0
12
+ build==1.2.2.post1
13
+ cachetools==5.5.2
14
+ certifi==2025.1.31
15
+ cffi==1.17.1
16
+ charset-normalizer==3.4.1
17
+ chroma-hnswlib==0.7.6
18
+ chromadb==0.6.3
19
+ click==8.1.8
20
+ coloredlogs==15.0.1
21
+ contourpy==1.3.1
22
+ cryptography==44.0.2
23
+ cycler==0.12.1
24
+ dataclasses-json==0.6.7
25
+ Deprecated==1.2.18
26
+ distro==1.9.0
27
+ durationpy==0.9
28
+ exceptiongroup==1.2.2
29
+ fastapi==0.115.11
30
+ ffmpy==0.5.0
31
+ filelock==3.13.1
32
+ flatbuffers==25.2.10
33
+ fonttools==4.56.0
34
+ frozenlist==1.5.0
35
+ fsspec==2024.6.1
36
+ google-auth==2.38.0
37
+ googleapis-common-protos==1.69.2
38
+ gradio==5.22.0
39
+ gradio_client==1.8.0
40
+ greenlet==3.1.1
41
+ groovy==0.1.2
42
+ grpcio==1.71.0
43
+ h11==0.14.0
44
+ httpcore==1.0.7
45
+ httptools==0.6.4
46
+ httpx==0.28.1
47
+ httpx-sse==0.4.0
48
+ huggingface-hub==0.29.3
49
+ humanfriendly==10.0
50
+ idna==3.10
51
+ importlib_metadata==8.6.1
52
+ importlib_resources==6.5.2
53
+ Jinja2==3.1.4
54
+ joblib==1.4.2
55
+ jsonpatch==1.33
56
+ jsonpointer==3.0.0
57
+ kiwisolver==1.4.8
58
+ kubernetes==32.0.1
59
+ langchain==0.3.21
60
+ langchain-community==0.3.20
61
+ langchain-core==0.3.47
62
+ langchain-text-splitters==0.3.7
63
+ langsmith==0.3.18
64
+ lxml==5.3.1
65
+ markdown-it-py==3.0.0
66
+ MarkupSafe==2.1.5
67
+ marshmallow==3.26.1
68
+ matplotlib==3.10.1
69
+ mdurl==0.1.2
70
+ mmh3==5.1.0
71
+ monotonic==1.6
72
+ mpmath==1.3.0
73
+ multidict==6.2.0
74
+ mypy-extensions==1.0.0
75
+ networkx==3.3
76
+ numpy==2.2.4
77
+ oauthlib==3.2.2
78
+ onnxruntime==1.21.0
79
+ opencv-python==4.11.0.86
80
+ opentelemetry-api==1.31.1
81
+ opentelemetry-exporter-otlp-proto-common==1.31.1
82
+ opentelemetry-exporter-otlp-proto-grpc==1.31.1
83
+ opentelemetry-instrumentation==0.52b1
84
+ opentelemetry-instrumentation-asgi==0.52b1
85
+ opentelemetry-instrumentation-fastapi==0.52b1
86
+ opentelemetry-proto==1.31.1
87
+ opentelemetry-sdk==1.31.1
88
+ opentelemetry-semantic-conventions==0.52b1
89
+ opentelemetry-util-http==0.52b1
90
+ orjson==3.10.15
91
+ overrides==7.7.0
92
+ packaging==24.2
93
+ pandas==2.2.3
94
+ pdfminer.six==20231228
95
+ pdfplumber==0.11.5
96
+ pillow==11.1.0
97
+ posthog==3.21.0
98
+ propcache==0.3.0
99
+ protobuf==5.29.4
100
+ pyasn1==0.6.1
101
+ pyasn1_modules==0.4.1
102
+ pycparser==2.22
103
+ pydantic==2.10.6
104
+ pydantic-settings==2.8.1
105
+ pydantic_core==2.27.2
106
+ pydub==0.25.1
107
+ Pygments==2.19.1
108
+ PyMuPDF==1.25.4
109
+ pyparsing==3.2.1
110
+ pypdfium2==4.30.1
111
+ PyPika==0.48.9
112
+ pyproject_hooks==1.2.0
113
+ pytesseract==0.3.13
114
+ python-dateutil==2.9.0.post0
115
+ python-docx==1.1.2
116
+ python-dotenv==1.0.1
117
+ python-multipart==0.0.20
118
+ python-pptx==1.0.2
119
+ pytz==2025.1
120
+ PyYAML==6.0.2
121
+ regex==2024.11.6
122
+ requests==2.32.3
123
+ requests-oauthlib==2.0.0
124
+ requests-toolbelt==1.0.0
125
+ rich==13.9.4
126
+ rsa==4.9
127
+ ruff==0.11.2
128
+ safehttpx==0.1.6
129
+ safetensors==0.5.3
130
+ scikit-learn==1.6.1
131
+ scipy==1.15.2
132
+ semantic-version==2.10.0
133
+ sentence-transformers==3.4.1
134
+ shellingham==1.5.4
135
+ six==1.17.0
136
+ sniffio==1.3.1
137
+ SQLAlchemy==2.0.39
138
+ starlette==0.46.1
139
+ sympy==1.13.1
140
+ tenacity==9.0.0
141
+ threadpoolctl==3.6.0
142
+ tokenizers==0.21.1
143
+ tomli==2.2.1
144
+ tomlkit==0.13.2
145
+ torch==2.6.0+cpu
146
+ torchaudio==2.6.0+cpu
147
+ torchvision==0.21.0+cpu
148
+ tqdm==4.67.1
149
+ transformers==4.50.0
150
+ typer==0.15.2
151
+ typing-inspect==0.9.0
152
+ typing_extensions==4.12.2
153
+ tzdata==2025.1
154
+ urllib3==2.3.0
155
+ uvicorn==0.34.0
156
+ uvloop==0.21.0
157
+ watchfiles==1.0.4
158
+ websocket-client==1.8.0
159
+ websockets==15.0.1
160
+ wrapt==1.17.2
161
+ XlsxWriter==3.2.2
162
+ yarl==1.18.3
163
+ zipp==3.21.0
164
+ zstandard==0.23.0
src/font_analysis.py ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import pytesseract
4
+ from .preprocessing import ImagePreprocessor
5
+
6
+ class FontAnalyzer:
7
+ """
8
+ Classe per l'analisi dei font e il riconoscimento del tipo di inchiostro.
9
+ Implementa funzionalità per identificare i font utilizzati nei documenti
10
+ e analizzare le caratteristiche dell'inchiostro.
11
+ """
12
+
13
+ def __init__(self):
14
+ """Inizializza l'analizzatore di font."""
15
+ self.preprocessor = ImagePreprocessor()
16
+
17
+ def detect_text_regions(self, image):
18
+ """
19
+ Rileva le regioni di testo in un'immagine.
20
+
21
+ Args:
22
+ image (numpy.ndarray): Immagine di input
23
+
24
+ Returns:
25
+ list: Lista di rettangoli (x, y, w, h) che contengono testo
26
+ """
27
+ # Converti in scala di grigi se necessario
28
+ if len(image.shape) > 2:
29
+ gray = self.preprocessor.convert_to_grayscale(image)
30
+ else:
31
+ gray = image
32
+
33
+ # Applica soglia per binarizzare l'immagine
34
+ binary = self.preprocessor.threshold_image(gray, method='adaptive')
35
+
36
+ # Applica operazioni morfologiche per connettere i componenti del testo
37
+ kernel = np.ones((5, 1), np.uint8) # Kernel rettangolare orizzontale
38
+ dilated = cv2.dilate(binary, kernel, iterations=2)
39
+
40
+ # Trova i contorni
41
+ contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
42
+
43
+ # Filtra i contorni per dimensione
44
+ text_regions = []
45
+ for contour in contours:
46
+ x, y, w, h = cv2.boundingRect(contour)
47
+
48
+ # Filtra i contorni troppo piccoli
49
+ if w > 20 and h > 8 and w > h: # Probabilmente testo
50
+ text_regions.append((x, y, w, h))
51
+
52
+ return text_regions
53
+
54
+ def extract_text(self, image, text_regions=None):
55
+ """
56
+ Estrae il testo da un'immagine utilizzando OCR.
57
+
58
+ Args:
59
+ image (numpy.ndarray): Immagine di input
60
+ text_regions (list, optional): Lista di regioni di testo (x, y, w, h)
61
+
62
+ Returns:
63
+ dict: Dizionario con il testo estratto e le informazioni sulle regioni
64
+ """
65
+ # Se non sono fornite regioni di testo, rileva automaticamente
66
+ if text_regions is None:
67
+ text_regions = self.detect_text_regions(image)
68
+
69
+ # Converti in scala di grigi se necessario
70
+ if len(image.shape) > 2:
71
+ gray = self.preprocessor.convert_to_grayscale(image)
72
+ else:
73
+ gray = image
74
+
75
+ # Prepara il risultato
76
+ result = {
77
+ 'full_text': '',
78
+ 'regions': []
79
+ }
80
+
81
+ # Estrai il testo da ciascuna regione
82
+ for i, (x, y, w, h) in enumerate(text_regions):
83
+ # Estrai la regione
84
+ roi = gray[y:y+h, x:x+w]
85
+
86
+ # Applica miglioramenti all'immagine per OCR
87
+ roi = cv2.resize(roi, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
88
+ roi = cv2.GaussianBlur(roi, (5, 5), 0)
89
+ roi = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
90
+
91
+ # Esegui OCR
92
+ text = pytesseract.image_to_string(roi, config='--psm 6')
93
+
94
+ # Aggiungi al risultato
95
+ if text.strip():
96
+ result['full_text'] += text + '\n'
97
+ result['regions'].append({
98
+ 'id': i,
99
+ 'bbox': (x, y, w, h),
100
+ 'text': text.strip()
101
+ })
102
+
103
+ return result
104
+
105
+ def analyze_font(self, image, text_regions=None):
106
+ """
107
+ Analizza i font presenti in un'immagine.
108
+
109
+ Args:
110
+ image (numpy.ndarray): Immagine di input
111
+ text_regions (list, optional): Lista di regioni di testo (x, y, w, h)
112
+
113
+ Returns:
114
+ dict: Dizionario con le informazioni sui font
115
+ """
116
+ # Se non sono fornite regioni di testo, rileva automaticamente
117
+ if text_regions is None:
118
+ text_regions = self.detect_text_regions(image)
119
+
120
+ # Converti in scala di grigi se necessario
121
+ if len(image.shape) > 2:
122
+ gray = self.preprocessor.convert_to_grayscale(image)
123
+ else:
124
+ gray = image
125
+
126
+ # Prepara il risultato
127
+ result = {
128
+ 'regions': []
129
+ }
130
+
131
+ # Analizza ciascuna regione
132
+ for i, (x, y, w, h) in enumerate(text_regions):
133
+ # Estrai la regione
134
+ roi = gray[y:y+h, x:x+w]
135
+
136
+ # Applica miglioramenti all'immagine per OCR
137
+ roi = cv2.resize(roi, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
138
+ roi = cv2.GaussianBlur(roi, (5, 5), 0)
139
+ roi = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
140
+
141
+ # Esegui OCR con output dettagliato
142
+ ocr_data = pytesseract.image_to_data(roi, output_type=pytesseract.Output.DICT)
143
+
144
+ # Analizza le caratteristiche del font
145
+ font_info = self._analyze_font_characteristics(roi, ocr_data)
146
+
147
+ # Aggiungi al risultato
148
+ result['regions'].append({
149
+ 'id': i,
150
+ 'bbox': (x, y, w, h),
151
+ 'font_info': font_info
152
+ })
153
+
154
+ return result
155
+
156
+ def _analyze_font_characteristics(self, image, ocr_data):
157
+ """
158
+ Analizza le caratteristiche del font in una regione di testo.
159
+
160
+ Args:
161
+ image (numpy.ndarray): Immagine della regione di testo
162
+ ocr_data (dict): Dati OCR dalla regione
163
+
164
+ Returns:
165
+ dict: Caratteristiche del font
166
+ """
167
+ # Inizializza le caratteristiche
168
+ font_info = {
169
+ 'is_serif': False,
170
+ 'is_monospaced': False,
171
+ 'is_bold': False,
172
+ 'is_italic': False,
173
+ 'font_size': 0,
174
+ 'confidence': 0,
175
+ 'possible_fonts': []
176
+ }
177
+
178
+ # Estrai le caratteristiche dai dati OCR
179
+ if 'conf' in ocr_data and len(ocr_data['conf']) > 0:
180
+ # Calcola la confidenza media
181
+ valid_conf = [float(conf) for conf in ocr_data['conf'] if conf != '-1']
182
+ if valid_conf:
183
+ font_info['confidence'] = sum(valid_conf) / len(valid_conf)
184
+
185
+ # Analizza la spaziatura per determinare se è monospaced
186
+ if 'text' in ocr_data and 'left' in ocr_data and len(ocr_data['text']) > 1:
187
+ # Filtra solo le parole valide
188
+ valid_indices = [i for i, text in enumerate(ocr_data['text']) if text.strip()]
189
+
190
+ if len(valid_indices) > 1:
191
+ # Calcola le distanze tra le parole
192
+ lefts = [ocr_data['left'][i] for i in valid_indices]
193
+ widths = [ocr_data['width'][i] for i in valid_indices]
194
+
195
+ # Calcola la deviazione standard delle larghezze dei caratteri
196
+ char_widths = []
197
+ for i in valid_indices:
198
+ if ocr_data['text'][i] and len(ocr_data['text'][i]) > 0:
199
+ char_width = ocr_data['width'][i] / len(ocr_data['text'][i])
200
+ char_widths.append(char_width)
201
+
202
+ if char_widths:
203
+ std_dev = np.std(char_widths)
204
+ mean_width = np.mean(char_widths)
205
+
206
+ # Se la deviazione standard è bassa rispetto alla media, è probabilmente monospaced
207
+ if std_dev / mean_width < 0.1:
208
+ font_info['is_monospaced'] = True
209
+
210
+ # Analizza l'immagine per determinare se è serif o sans-serif
211
+ # Questo è un approccio semplificato basato sul conteggio dei pixel
212
+ binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
213
+
214
+ # Calcola il numero di pixel bianchi (testo) e neri (sfondo)
215
+ white_pixels = cv2.countNonZero(binary)
216
+ total_pixels = binary.shape[0] * binary.shape[1]
217
+ black_pixels = total_pixels - white_pixels
218
+
219
+ # Calcola la densità del testo
220
+ text_density = white_pixels / total_pixels if total_pixels > 0 else 0
221
+
222
+ # Applica operazioni morfologiche per rilevare caratteristiche serif
223
+ kernel = np.ones((2, 2), np.uint8)
224
+ eroded = cv2.erode(binary, kernel, iterations=1)
225
+
226
+ # Calcola la differenza tra l'immagine originale e quella erosa
227
+ diff = cv2.subtract(binary, eroded)
228
+
229
+ # Conta i pixel nella differenza
230
+ diff_pixels = cv2.countNonZero(diff)
231
+
232
+ # Calcola il rapporto tra i pixel di differenza e i pixel bianchi originali
233
+ serif_ratio = diff_pixels / white_pixels if white_pixels > 0 else 0
234
+
235
+ # Se il rapporto è alto, è probabilmente serif
236
+ if serif_ratio > 0.2:
237
+ font_info['is_serif'] = True
238
+
239
+ # Stima la dimensione del font
240
+ if 'height' in ocr_data and len(ocr_data['height']) > 0:
241
+ valid_heights = [h for h in ocr_data['height'] if h > 0]
242
+ if valid_heights:
243
+ font_info['font_size'] = sum(valid_heights) / len(valid_heights) / 2 # Approssimazione
244
+
245
+ # Determina se è grassetto
246
+ if text_density > 0.4: # Soglia arbitraria
247
+ font_info['is_bold'] = True
248
+
249
+ # Determina se è corsivo
250
+ # Questo richiederebbe un'analisi più complessa dell'inclinazione dei caratteri
251
+ # Per ora, utilizziamo un'euristica basata sui dati OCR
252
+ if 'text' in ocr_data and 'left' in ocr_data and 'width' in ocr_data:
253
+ # Calcola l'inclinazione media dei caratteri
254
+ # Questo è un approccio semplificato
255
+ font_info['is_italic'] = False # Implementazione semplificata
256
+
257
+ # Suggerisci possibili font
258
+ if font_info['is_serif'] and font_info['is_monospaced']:
259
+ font_info['possible_fonts'] = ['Courier', 'Courier New', 'Consolas']
260
+ elif font_info['is_serif'] and not font_info['is_monospaced']:
261
+ if font_info['is_bold']:
262
+ font_info['possible_fonts'] = ['Times New Roman Bold', 'Georgia Bold', 'Garamond Bold']
263
+ else:
264
+ font_info['possible_fonts'] = ['Times New Roman', 'Georgia', 'Garamond']
265
+ elif not font_info['is_serif'] and font_info['is_monospaced']:
266
+ font_info['possible_fonts'] = ['Monaco', 'Menlo', 'Lucida Console']
267
+ else: # sans-serif, non-monospaced
268
+ if font_info['is_bold']:
269
+ font_info['possible_fonts'] = ['Arial Bold', 'Helvetica Bold', 'Calibri Bold']
270
+ else:
271
+ font_info['possible_fonts'] = ['Arial', 'Helvetica', 'Calibri']
272
+
273
+ return font_info
274
+
275
+ def analyze_ink(self, image):
276
+ """
277
+ Analizza il tipo di inchiostro utilizzato in un'immagine.
278
+
279
+ Args:
280
+ image (numpy.ndarray): Immagine di input
281
+
282
+ Returns:
283
+ dict: Informazioni sul tipo di inchiostro
284
+ """
285
+ # Verifica che l'immagine sia a colori
286
+ if len(image.shape) < 3:
287
+ # Converti in BGR se è in scala di grigi
288
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
289
+
290
+ # Converti in HSV per un'analisi migliore del colore
291
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
292
+
293
+ # Estrai i canali HSV
294
+ h, s, v = cv2.split(hsv)
295
+
296
+ # Crea una maschera per isolare l'inchiostro (pixel scuri)
297
+ _, ink_mask = cv2.threshold(v, 150, 255, cv2.THRESH_BINARY_INV)
298
+
299
+ # Applica la maschera ai canali HSV
300
+ h_ink = cv2.bitwise_and(h, h, mask=ink_mask)
301
+ s_ink = cv2.bitwise_and(s, s, mask=ink_mask)
302
+
303
+ # Calcola le statistiche dei canali HSV per l'inchiostro
304
+ h_values = h_ink[ink_mask > 0]
305
+ s_values = s_ink[ink_mask > 0]
306
+ v_values = 255 - v[ink_mask > 0] # Inverti V per ottenere l'intensità dell'inchiostro
307
+
308
+ # Se non ci sono pixel di inchiostro, restituisci un risultato predefinito
309
+ if len(h_values) == 0:
310
+ return {
311
+ 'ink_type': 'unknown',
312
+ 'ink_color': 'unknown',
313
+ 'is_printed': False,
314
+ 'confidence': 0,
315
+ 'details': {
316
+ 'hue_mean': 0,
317
+ 'saturation_mean': 0,
318
+ 'value_mean': 0,
319
+ 'hue_std': 0,
320
+ 'saturation_std': 0,
321
+ 'value_std': 0,
322
+ 'ink_coverage': 0
323
+ }
324
+ }
325
+
326
+ # Calcola le statistiche
327
+ hue_mean = np.mean(h_values)
328
+ saturation_mean = np.mean(s_values)
329
+ value_mean = np.mean(v_values)
330
+ hue_std = np.std(h_values)
331
+ saturation_std = np.std(s_values)
332
+ value_std = np.std(v_values)
333
+
334
+ # Calcola la copertura dell'inchiostro
335
+ ink_coverage = np.count_nonzero(ink_mask) / (ink_mask.shape[0] * ink_mask.shape[1])
336
+
337
+ # Determina il colore dell'inchiostro
338
+ ink_color = self._determine_ink_color(hue_mean, saturation_mean, value_mean)
339
+
340
+ # Determina se è stampato o scritto a mano
341
+ is_printed = self._is_printed_ink(value_std, saturation_std, ink_coverage)
342
+
343
+ # Determina il tipo di inchiostro
344
+ ink_type, confidence = self._determine_ink_type(
345
+ hue_mean, saturation_mean, value_mean,
346
+ hue_std, saturation_std, value_std,
347
+ ink_coverage, is_printed
348
+ )
349
+
350
+ return {
351
+ 'ink_type': ink_type,
352
+ 'ink_color': ink_color,
353
+ 'is_printed': is_printed,
354
+ 'confidence': confidence,
355
+ 'details': {
356
+ 'hue_mean': float(hue_mean),
357
+ 'saturation_mean': float(saturation_mean),
358
+ 'value_mean': float(value_mean),
359
+ 'hue_std': float(hue_std),
360
+ 'saturation_std': float(saturation_std),
361
+ 'value_std': float(value_std),
362
+ 'ink_coverage': float(ink_coverage)
363
+ }
364
+ }
365
+
366
+ def _determine_ink_color(self, hue_mean, saturation_mean, value_mean):
367
+ """
368
+ Determina il colore dell'inchiostro in base ai valori HSV.
369
+
370
+ Args:
371
+ hue_mean (float): Media del canale H
372
+ saturation_mean (float): Media del canale S
373
+ value_mean (float): Media del canale V
374
+
375
+ Returns:
376
+ str: Nome del colore dell'inchiostro
377
+ """
378
+ # Se la saturazione è bassa, è probabilmente nero o grigio
379
+ if saturation_mean < 50:
380
+ if value_mean > 200:
381
+ return 'black'
382
+ else:
383
+ return 'gray'
384
+
385
+ # Altrimenti, determina il colore in base alla tonalità
386
+ if 0 <= hue_mean < 30 or 330 <= hue_mean <= 360:
387
+ return 'red'
388
+ elif 30 <= hue_mean < 90:
389
+ return 'yellow'
390
+ elif 90 <= hue_mean < 150:
391
+ return 'green'
392
+ elif 150 <= hue_mean < 210:
393
+ return 'cyan'
394
+ elif 210 <= hue_mean < 270:
395
+ return 'blue'
396
+ elif 270 <= hue_mean < 330:
397
+ return 'magenta'
398
+ else:
399
+ return 'unknown'
400
+
401
+ def _is_printed_ink(self, value_std, saturation_std, ink_coverage):
402
+ """
403
+ Determina se l'inchiostro è stampato o scritto a mano.
404
+
405
+ Args:
406
+ value_std (float): Deviazione standard del canale V
407
+ saturation_std (float): Deviazione standard del canale S
408
+ ink_coverage (float): Percentuale di copertura dell'inchiostro
409
+
410
+ Returns:
411
+ bool: True se l'inchiostro è probabilmente stampato, False altrimenti
412
+ """
413
+ # L'inchiostro stampato tende ad avere una deviazione standard più bassa
414
+ # e una copertura più uniforme
415
+ if value_std < 30 and saturation_std < 20:
416
+ return True
417
+
418
+ # Se la copertura è molto alta, è probabilmente stampato
419
+ if ink_coverage > 0.4:
420
+ return True
421
+
422
+ return False
423
+
424
+ def _determine_ink_type(self, hue_mean, saturation_mean, value_mean,
425
+ hue_std, saturation_std, value_std,
426
+ ink_coverage, is_printed):
427
+ """
428
+ Determina il tipo di inchiostro in base alle statistiche HSV.
429
+
430
+ Args:
431
+ hue_mean (float): Media del canale H
432
+ saturation_mean (float): Media del canale S
433
+ value_mean (float): Media del canale V
434
+ hue_std (float): Deviazione standard del canale H
435
+ saturation_std (float): Deviazione standard del canale S
436
+ value_std (float): Deviazione standard del canale V
437
+ ink_coverage (float): Percentuale di copertura dell'inchiostro
438
+ is_printed (bool): Se l'inchiostro è stampato o scritto a mano
439
+
440
+ Returns:
441
+ tuple: (tipo_inchiostro, confidenza)
442
+ """
443
+ if is_printed:
444
+ # Inchiostro stampato
445
+ if saturation_mean < 30 and value_mean > 200:
446
+ return 'laser_printer', 0.8
447
+ elif saturation_mean < 50:
448
+ return 'inkjet_printer', 0.7
449
+ else:
450
+ return 'color_printer', 0.6
451
+ else:
452
+ # Inchiostro scritto a mano
453
+ if saturation_mean < 30 and value_mean > 200:
454
+ # Penna a sfera (biro)
455
+ return 'ballpoint_pen', 0.7
456
+ elif saturation_mean > 100 and value_std > 40:
457
+ # Pennarello
458
+ return 'marker', 0.8
459
+ elif value_mean < 150 and value_std < 30:
460
+ # Penna stilografica
461
+ return 'fountain_pen', 0.6
462
+ elif saturation_mean < 50 and value_mean < 180:
463
+ # Matita
464
+ return 'pencil', 0.7
465
+ else:
466
+ return 'unknown_pen', 0.4
src/image_enhancer.py ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from .preprocessing import ImagePreprocessor
5
+
6
+ class ImageEnhancer:
7
+ """
8
+ Classe per l'elaborazione avanzata delle immagini di firme e documenti.
9
+ Implementa funzionalità per migliorare la qualità delle immagini,
10
+ evidenziare dettagli e applicare filtri speciali per l'analisi forense.
11
+ """
12
+
13
+ def __init__(self):
14
+ """Inizializza l'enhancer di immagini."""
15
+ self.preprocessor = ImagePreprocessor()
16
+
17
+ def enhance_contrast(self, image, method='clahe'):
18
+ """
19
+ Migliora il contrasto di un'immagine.
20
+
21
+ Args:
22
+ image (numpy.ndarray): Immagine di input
23
+ method (str): Metodo di miglioramento ('clahe', 'histogram_eq', 'adaptive')
24
+
25
+ Returns:
26
+ numpy.ndarray: Immagine con contrasto migliorato
27
+ """
28
+ # Converti in scala di grigi se necessario
29
+ if len(image.shape) > 2:
30
+ gray = self.preprocessor.convert_to_grayscale(image)
31
+ else:
32
+ gray = image.copy()
33
+
34
+ if method == 'clahe':
35
+ # Contrast Limited Adaptive Histogram Equalization
36
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
37
+ enhanced = clahe.apply(gray)
38
+ elif method == 'histogram_eq':
39
+ # Equalizzazione dell'istogramma globale
40
+ enhanced = cv2.equalizeHist(gray)
41
+ elif method == 'adaptive':
42
+ # Miglioramento adattivo del contrasto
43
+ enhanced = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
44
+ cv2.THRESH_BINARY, 11, 2)
45
+ else:
46
+ raise ValueError(f"Metodo di miglioramento del contrasto non supportato: {method}")
47
+
48
+ return enhanced
49
+
50
+ def sharpen_image(self, image, kernel_size=3, strength=1.0):
51
+ """
52
+ Applica un filtro di sharpening all'immagine.
53
+
54
+ Args:
55
+ image (numpy.ndarray): Immagine di input
56
+ kernel_size (int): Dimensione del kernel
57
+ strength (float): Intensità dell'effetto di sharpening
58
+
59
+ Returns:
60
+ numpy.ndarray: Immagine affilata
61
+ """
62
+ # Converti in scala di grigi se necessario
63
+ if len(image.shape) > 2:
64
+ gray = self.preprocessor.convert_to_grayscale(image)
65
+ else:
66
+ gray = image.copy()
67
+
68
+ # Applica un filtro gaussiano per ridurre il rumore
69
+ blurred = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
70
+
71
+ # Calcola la maschera di sharpening (immagine originale - immagine sfocata)
72
+ mask = cv2.subtract(gray, blurred)
73
+
74
+ # Applica la maschera all'immagine originale
75
+ sharpened = cv2.addWeighted(gray, 1.0, mask, strength, 0)
76
+
77
+ return sharpened
78
+
79
+ def apply_edge_detection(self, image, method='canny'):
80
+ """
81
+ Applica un rilevatore di bordi all'immagine.
82
+
83
+ Args:
84
+ image (numpy.ndarray): Immagine di input
85
+ method (str): Metodo di rilevamento bordi ('canny', 'sobel', 'laplacian')
86
+
87
+ Returns:
88
+ numpy.ndarray: Immagine con bordi rilevati
89
+ """
90
+ # Converti in scala di grigi se necessario
91
+ if len(image.shape) > 2:
92
+ gray = self.preprocessor.convert_to_grayscale(image)
93
+ else:
94
+ gray = image.copy()
95
+
96
+ # Applica un filtro gaussiano per ridurre il rumore
97
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
98
+
99
+ if method == 'canny':
100
+ # Rilevatore di bordi Canny
101
+ edges = cv2.Canny(blurred, 50, 150)
102
+ elif method == 'sobel':
103
+ # Rilevatore di bordi Sobel
104
+ sobelx = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
105
+ sobely = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
106
+
107
+ # Calcola il gradiente
108
+ magnitude = cv2.magnitude(sobelx, sobely)
109
+
110
+ # Normalizza e converti in uint8
111
+ edges = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
112
+ elif method == 'laplacian':
113
+ # Rilevatore di bordi Laplaciano
114
+ laplacian = cv2.Laplacian(blurred, cv2.CV_64F)
115
+
116
+ # Normalizza e converti in uint8
117
+ edges = cv2.normalize(laplacian, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
118
+ else:
119
+ raise ValueError(f"Metodo di rilevamento bordi non supportato: {method}")
120
+
121
+ return edges
122
+
123
+ def highlight_pressure_points(self, image, threshold=50):
124
+ """
125
+ Evidenzia i punti di pressione in una firma.
126
+
127
+ Args:
128
+ image (numpy.ndarray): Immagine di input
129
+ threshold (int): Soglia per considerare un punto come punto di pressione
130
+
131
+ Returns:
132
+ numpy.ndarray: Immagine con punti di pressione evidenziati
133
+ """
134
+ # Converti in scala di grigi se necessario
135
+ if len(image.shape) > 2:
136
+ gray = self.preprocessor.convert_to_grayscale(image)
137
+ else:
138
+ gray = image.copy()
139
+
140
+ # Inverti l'immagine (testo bianco su sfondo nero)
141
+ gray_inv = cv2.bitwise_not(gray)
142
+
143
+ # Applica una soglia per isolare il testo
144
+ _, binary = cv2.threshold(gray_inv, threshold, 255, cv2.THRESH_BINARY)
145
+
146
+ # Crea un'immagine a colori per la visualizzazione
147
+ result = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
148
+
149
+ # Applica una mappa di colori per evidenziare i punti di pressione
150
+ # Più scuro è il pixel, maggiore è la pressione
151
+ for i in range(gray.shape[0]):
152
+ for j in range(gray.shape[1]):
153
+ if binary[i, j] > 0:
154
+ # Calcola l'intensità normalizzata (0-1)
155
+ intensity = gray_inv[i, j] / 255.0
156
+
157
+ # Applica una mappa di colori (blu -> verde -> rosso)
158
+ if intensity < 0.33:
159
+ # Blu (bassa pressione)
160
+ result[i, j] = [255, 0, 0]
161
+ elif intensity < 0.66:
162
+ # Verde (media pressione)
163
+ result[i, j] = [0, 255, 0]
164
+ else:
165
+ # Rosso (alta pressione)
166
+ result[i, j] = [0, 0, 255]
167
+
168
+ return result
169
+
170
+ def extract_profile(self, image, direction='horizontal'):
171
+ """
172
+ Estrae il profilo di un'immagine in una direzione specifica.
173
+
174
+ Args:
175
+ image (numpy.ndarray): Immagine di input
176
+ direction (str): Direzione del profilo ('horizontal', 'vertical')
177
+
178
+ Returns:
179
+ numpy.ndarray: Profilo estratto
180
+ """
181
+ # Converti in scala di grigi se necessario
182
+ if len(image.shape) > 2:
183
+ gray = self.preprocessor.convert_to_grayscale(image)
184
+ else:
185
+ gray = image.copy()
186
+
187
+ # Inverti l'immagine (testo bianco su sfondo nero)
188
+ gray_inv = cv2.bitwise_not(gray)
189
+
190
+ if direction == 'horizontal':
191
+ # Somma i pixel per ogni riga
192
+ profile = np.sum(gray_inv, axis=1)
193
+ elif direction == 'vertical':
194
+ # Somma i pixel per ogni colonna
195
+ profile = np.sum(gray_inv, axis=0)
196
+ else:
197
+ raise ValueError(f"Direzione del profilo non supportata: {direction}")
198
+
199
+ # Normalizza il profilo
200
+ if np.max(profile) > 0:
201
+ profile = profile / np.max(profile)
202
+
203
+ return profile
204
+
205
+ def visualize_profile(self, image, save_path=None):
206
+ """
207
+ Visualizza i profili orizzontale e verticale di un'immagine.
208
+
209
+ Args:
210
+ image (numpy.ndarray): Immagine di input
211
+ save_path (str, optional): Percorso dove salvare l'immagine
212
+
213
+ Returns:
214
+ matplotlib.figure.Figure: Figura con la visualizzazione
215
+ """
216
+ # Estrai i profili
217
+ h_profile = self.extract_profile(image, direction='horizontal')
218
+ v_profile = self.extract_profile(image, direction='vertical')
219
+
220
+ # Crea una figura con più sottografici
221
+ fig, axs = plt.subplots(1, 3, figsize=(15, 5))
222
+
223
+ # Immagine originale
224
+ if len(image.shape) > 2:
225
+ axs[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
226
+ else:
227
+ axs[0].imshow(image, cmap='gray')
228
+ axs[0].set_title('Immagine Originale')
229
+ axs[0].axis('off')
230
+
231
+ # Profilo orizzontale
232
+ axs[1].plot(h_profile, range(len(h_profile)), 'b-')
233
+ axs[1].invert_yaxis() # Inverti l'asse y per corrispondere all'immagine
234
+ axs[1].set_title('Profilo Orizzontale')
235
+ axs[1].set_xlabel('Intensità Normalizzata')
236
+ axs[1].set_ylabel('Riga')
237
+
238
+ # Profilo verticale
239
+ axs[2].plot(v_profile, 'r-')
240
+ axs[2].set_title('Profilo Verticale')
241
+ axs[2].set_xlabel('Colonna')
242
+ axs[2].set_ylabel('Intensità Normalizzata')
243
+
244
+ plt.tight_layout()
245
+
246
+ # Salva l'immagine se richiesto
247
+ if save_path:
248
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
249
+
250
+ return fig
251
+
252
+ def apply_color_filter(self, image, color_range):
253
+ """
254
+ Applica un filtro di colore all'immagine.
255
+
256
+ Args:
257
+ image (numpy.ndarray): Immagine di input (BGR)
258
+ color_range (dict): Intervallo di colori in formato HSV
259
+ {'lower': [h_min, s_min, v_min], 'upper': [h_max, s_max, v_max]}
260
+
261
+ Returns:
262
+ numpy.ndarray: Immagine filtrata
263
+ """
264
+ # Verifica che l'immagine sia a colori
265
+ if len(image.shape) < 3:
266
+ # Converti in BGR se è in scala di grigi
267
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
268
+
269
+ # Converti in HSV
270
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
271
+
272
+ # Crea una maschera per il colore specificato
273
+ lower = np.array(color_range['lower'])
274
+ upper = np.array(color_range['upper'])
275
+ mask = cv2.inRange(hsv, lower, upper)
276
+
277
+ # Applica la maschera all'immagine originale
278
+ filtered = cv2.bitwise_and(image, image, mask=mask)
279
+
280
+ return filtered
281
+
282
+ def extract_stamp(self, image):
283
+ """
284
+ Estrae i timbri da un'immagine.
285
+
286
+ Args:
287
+ image (numpy.ndarray): Immagine di input (BGR)
288
+
289
+ Returns:
290
+ tuple: (immagine_originale_senza_timbri, timbri_estratti)
291
+ """
292
+ # Verifica che l'immagine sia a colori
293
+ if len(image.shape) < 3:
294
+ # Converti in BGR se è in scala di grigi
295
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
296
+
297
+ # Definisci intervalli di colore per i timbri comuni
298
+ color_ranges = [
299
+ # Blu (timbri comuni)
300
+ {'lower': [100, 50, 50], 'upper': [140, 255, 255]},
301
+ # Rosso (timbri comuni)
302
+ {'lower': [0, 50, 50], 'upper': [10, 255, 255]},
303
+ # Rosso (parte alta dello spettro HSV)
304
+ {'lower': [170, 50, 50], 'upper': [180, 255, 255]},
305
+ # Viola (alcuni timbri ufficiali)
306
+ {'lower': [140, 50, 50], 'upper': [170, 255, 255]}
307
+ ]
308
+
309
+ # Converti in HSV
310
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
311
+
312
+ # Crea una maschera combinata per tutti i colori
313
+ combined_mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)
314
+
315
+ for color_range in color_ranges:
316
+ lower = np.array(color_range['lower'])
317
+ upper = np.array(color_range['upper'])
318
+ mask = cv2.inRange(hsv, lower, upper)
319
+ combined_mask = cv2.bitwise_or(combined_mask, mask)
320
+
321
+ # Applica operazioni morfologiche per migliorare la maschera
322
+ kernel = np.ones((5, 5), np.uint8)
323
+ combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel)
324
+ combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel)
325
+
326
+ # Estrai i timbri
327
+ stamps = cv2.bitwise_and(image, image, mask=combined_mask)
328
+
329
+ # Crea un'immagine senza timbri
330
+ inv_mask = cv2.bitwise_not(combined_mask)
331
+ image_without_stamps = cv2.bitwise_and(image, image, mask=inv_mask)
332
+
333
+ return image_without_stamps, stamps
334
+
335
+ def convert_to_grayscale_enhanced(self, image, method='weighted'):
336
+ """
337
+ Converte un'immagine a colori in scala di grigi con metodi avanzati.
338
+
339
+ Args:
340
+ image (numpy.ndarray): Immagine di input (BGR)
341
+ method (str): Metodo di conversione ('weighted', 'luminosity', 'desaturation', 'decomposition')
342
+
343
+ Returns:
344
+ numpy.ndarray: Immagine in scala di grigi
345
+ """
346
+ # Verifica che l'immagine sia a colori
347
+ if len(image.shape) < 3:
348
+ return image.copy()
349
+
350
+ if method == 'weighted':
351
+ # Metodo standard (ponderato)
352
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
353
+ elif method == 'luminosity':
354
+ # Metodo della luminosità (pesi personalizzati)
355
+ b, g, r = cv2.split(image)
356
+ gray = np.uint8(0.07 * b + 0.72 * g + 0.21 * r)
357
+ elif method == 'desaturation':
358
+ # Metodo della desaturazione (media di min e max)
359
+ b, g, r = cv2.split(image)
360
+ min_val = np.minimum(np.minimum(r, g), b)
361
+ max_val = np.maximum(np.maximum(r, g), b)
362
+ gray = np.uint8((min_val + max_val) / 2)
363
+ elif method == 'decomposition':
364
+ # Metodo della decomposizione (massimo dei canali)
365
+ b, g, r = cv2.split(image)
366
+ gray = np.maximum(np.maximum(r, g), b)
367
+ else:
368
+ raise ValueError(f"Metodo di conversione in scala di grigi non supportato: {method}")
369
+
370
+ return gray
371
+
372
+ def apply_emboss_effect(self, image, direction='top-left'):
373
+ """
374
+ Applica un effetto di rilievo all'immagine.
375
+
376
+ Args:
377
+ image (numpy.ndarray): Immagine di input
378
+ direction (str): Direzione della luce ('top-left', 'top-right', 'bottom-left', 'bottom-right')
379
+
380
+ Returns:
381
+ numpy.ndarray: Immagine con effetto di rilievo
382
+ """
383
+ # Converti in scala di grigi se necessario
384
+ if len(image.shape) > 2:
385
+ gray = self.preprocessor.convert_to_grayscale(image)
386
+ else:
387
+ gray = image.copy()
388
+
389
+ # Definisci il kernel in base alla direzione
390
+ if direction == 'top-left':
391
+ kernel = np.array([[-1, -1, 0],
392
+ [-1, 0, 1],
393
+ [0, 1, 1]])
394
+ elif direction == 'top-right':
395
+ kernel = np.array([[0, -1, -1],
396
+ [1, 0, -1],
397
+ [1, 1, 0]])
398
+ elif direction == 'bottom-left':
399
+ kernel = np.array([[0, 1, 1],
400
+ [-1, 0, 1],
401
+ [-1, -1, 0]])
402
+ elif direction == 'bottom-right':
403
+ kernel = np.array([[1, 1, 0],
404
+ [1, 0, -1],
405
+ [0, -1, -1]])
406
+ else:
407
+ raise ValueError(f"Direzione non supportata: {direction}")
408
+
409
+ # Applica il filtro
410
+ embossed = cv2.filter2D(gray, -1, kernel)
411
+
412
+ # Aggiungi 128 per spostare i valori nel range medio
413
+ embossed = cv2.add(embossed, 128)
414
+
415
+ return embossed
416
+
417
+ def create_signature_heatmap(self, image, kernel_size=15):
418
+ """
419
+ Crea una mappa di calore della firma per evidenziare le aree di maggiore intensità.
420
+
421
+ Args:
422
+ image (numpy.ndarray): Immagine di input
423
+ kernel_size (int): Dimensione del kernel per il filtro gaussiano
424
+
425
+ Returns:
426
+ numpy.ndarray: Mappa di calore della firma
427
+ """
428
+ # Converti in scala di grigi se necessario
429
+ if len(image.shape) > 2:
430
+ gray = self.preprocessor.convert_to_grayscale(image)
431
+ else:
432
+ gray = image.copy()
433
+
434
+ # Inverti l'immagine (testo bianco su sfondo nero)
435
+ gray_inv = cv2.bitwise_not(gray)
436
+
437
+ # Applica un filtro gaussiano per creare l'effetto di calore
438
+ heatmap = cv2.GaussianBlur(gray_inv, (kernel_size, kernel_size), 0)
439
+
440
+ # Normalizza la mappa di calore
441
+ heatmap = cv2.normalize(heatmap, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
442
+
443
+ # Applica una mappa di colori
444
+ heatmap_color = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
445
+
446
+ # Crea una maschera per isolare la firma
447
+ _, mask = cv2.threshold(gray_inv, 10, 255, cv2.THRESH_BINARY)
448
+
449
+ # Dilata la maschera per includere le aree circostanti
450
+ kernel = np.ones((5, 5), np.uint8)
451
+ mask_dilated = cv2.dilate(mask, kernel, iterations=2)
452
+
453
+ # Applica la maschera alla mappa di calore
454
+ result = cv2.bitwise_and(heatmap_color, heatmap_color, mask=mask_dilated)
455
+
456
+ # Crea un'immagine di sfondo bianco
457
+ background = np.ones_like(image) * 255
458
+ if len(background.shape) < 3:
459
+ background = cv2.cvtColor(background, cv2.COLOR_GRAY2BGR)
460
+
461
+ # Combina lo sfondo con la mappa di calore
462
+ mask_dilated_3ch = cv2.cvtColor(mask_dilated, cv2.COLOR_GRAY2BGR) / 255.0
463
+ result = background * (1 - mask_dilated_3ch) + result * mask_dilated_3ch
464
+
465
+ return result.astype(np.uint8)
466
+
467
+ def enhance_signature(self, image):
468
+ """
469
+ Applica una serie di miglioramenti a un'immagine di firma.
470
+
471
+ Args:
472
+ image (numpy.ndarray): Immagine di input
473
+
474
+ Returns:
475
+ dict: Dizionario con diverse versioni migliorate della firma
476
+ """
477
+ # Carica l'immagine se è un percorso file
478
+ if isinstance(image, str):
479
+ image = self.preprocessor.load_image(image)
480
+
481
+ # Converti in scala di grigi
482
+ gray = self.preprocessor.convert_to_grayscale(image)
483
+
484
+ # Migliora il contrasto
485
+ contrast_enhanced = self.enhance_contrast(gray, method='clahe')
486
+
487
+ # Applica sharpening
488
+ sharpened = self.sharpen_image(gray, kernel_size=3, strength=1.5)
489
+
490
+ # Rileva i bordi
491
+ edges = self.apply_edge_detection(gray, method='canny')
492
+
493
+ # Evidenzia i punti di pressione
494
+ pressure_points = self.highlight_pressure_points(gray)
495
+
496
+ # Applica effetto di rilievo
497
+ embossed = self.apply_emboss_effect(gray)
498
+
499
+ # Crea una mappa di calore
500
+ heatmap = self.create_signature_heatmap(gray)
501
+
502
+ return {
503
+ 'original': image,
504
+ 'grayscale': gray,
505
+ 'contrast_enhanced': contrast_enhanced,
506
+ 'sharpened': sharpened,
507
+ 'edges': edges,
508
+ 'pressure_points': pressure_points,
509
+ 'embossed': embossed,
510
+ 'heatmap': heatmap
511
+ }
src/measurement.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from .preprocessing import ImagePreprocessor
5
+
6
+ class MeasurementTool:
7
+ """
8
+ Classe per la misurazione e profilazione di documenti e firme.
9
+ Implementa funzionalità per misurare interlinea, spazi, margini,
10
+ e generare profili di analisi.
11
+ """
12
+
13
+ def __init__(self):
14
+ """Inizializza lo strumento di misurazione."""
15
+ self.preprocessor = ImagePreprocessor()
16
+
17
+ def detect_lines(self, image, method='projection'):
18
+ """
19
+ Rileva le linee di testo in un'immagine.
20
+
21
+ Args:
22
+ image (numpy.ndarray): Immagine di input
23
+ method (str): Metodo di rilevamento ('projection', 'hough')
24
+
25
+ Returns:
26
+ list: Lista di coordinate y delle linee di testo
27
+ """
28
+ # Converti in scala di grigi se necessario
29
+ if len(image.shape) > 2:
30
+ gray = self.preprocessor.convert_to_grayscale(image)
31
+ else:
32
+ gray = image
33
+
34
+ # Binarizza l'immagine
35
+ binary = self.preprocessor.threshold_image(gray, method='adaptive')
36
+
37
+ if method == 'projection':
38
+ # Metodo della proiezione orizzontale
39
+ # Somma i pixel bianchi per ogni riga
40
+ projection = np.sum(binary, axis=1)
41
+
42
+ # Normalizza la proiezione
43
+ projection = projection / np.max(projection)
44
+
45
+ # Trova i picchi nella proiezione (linee di testo)
46
+ lines = []
47
+ threshold = 0.3 # Soglia per considerare un picco
48
+ in_line = False
49
+ start_line = 0
50
+
51
+ for i in range(len(projection)):
52
+ if projection[i] > threshold and not in_line:
53
+ in_line = True
54
+ start_line = i
55
+ elif projection[i] <= threshold and in_line:
56
+ in_line = False
57
+ mid_line = (start_line + i) // 2
58
+ lines.append(mid_line)
59
+
60
+ # Se l'ultima linea non è stata chiusa
61
+ if in_line:
62
+ mid_line = (start_line + len(projection) - 1) // 2
63
+ lines.append(mid_line)
64
+
65
+ elif method == 'hough':
66
+ # Metodo delle trasformate di Hough
67
+ edges = cv2.Canny(binary, 50, 150, apertureSize=3)
68
+
69
+ # Rileva le linee
70
+ lines_hough = cv2.HoughLines(edges, 1, np.pi/180, threshold=100)
71
+
72
+ # Filtra le linee orizzontali
73
+ lines = []
74
+ if lines_hough is not None:
75
+ for line in lines_hough:
76
+ rho, theta = line[0]
77
+ # Considera solo le linee orizzontali (theta vicino a 0 o pi)
78
+ if (theta < 0.1 or abs(theta - np.pi) < 0.1):
79
+ a = np.cos(theta)
80
+ b = np.sin(theta)
81
+ x0 = a * rho
82
+ y0 = b * rho
83
+ # y = (rho - x * cos(theta)) / sin(theta)
84
+ # Per linee orizzontali, y è costante
85
+ y = int(y0)
86
+ lines.append(y)
87
+
88
+ # Ordina le linee per posizione y
89
+ lines.sort()
90
+ else:
91
+ raise ValueError(f"Metodo di rilevamento linee non supportato: {method}")
92
+
93
+ return lines
94
+
95
+ def measure_line_spacing(self, image):
96
+ """
97
+ Misura lo spazio tra le linee di testo.
98
+
99
+ Args:
100
+ image (numpy.ndarray): Immagine di input
101
+
102
+ Returns:
103
+ dict: Informazioni sullo spazio tra le linee
104
+ """
105
+ # Rileva le linee
106
+ lines = self.detect_lines(image)
107
+
108
+ if len(lines) < 2:
109
+ return {
110
+ 'line_count': len(lines),
111
+ 'average_spacing': 0,
112
+ 'spacing_std': 0,
113
+ 'line_positions': lines,
114
+ 'spacing_values': []
115
+ }
116
+
117
+ # Calcola lo spazio tra le linee consecutive
118
+ spacing = [lines[i+1] - lines[i] for i in range(len(lines)-1)]
119
+
120
+ return {
121
+ 'line_count': len(lines),
122
+ 'average_spacing': np.mean(spacing),
123
+ 'spacing_std': np.std(spacing),
124
+ 'line_positions': lines,
125
+ 'spacing_values': spacing
126
+ }
127
+
128
+ def detect_word_boundaries(self, image, line_positions=None):
129
+ """
130
+ Rileva i confini delle parole in un'immagine.
131
+
132
+ Args:
133
+ image (numpy.ndarray): Immagine di input
134
+ line_positions (list, optional): Posizioni y delle linee di testo
135
+
136
+ Returns:
137
+ list: Lista di tuple (linea, x_inizio, x_fine) per ogni parola
138
+ """
139
+ # Converti in scala di grigi se necessario
140
+ if len(image.shape) > 2:
141
+ gray = self.preprocessor.convert_to_grayscale(image)
142
+ else:
143
+ gray = image
144
+
145
+ # Binarizza l'immagine
146
+ binary = self.preprocessor.threshold_image(gray, method='adaptive')
147
+
148
+ # Se non sono fornite le posizioni delle linee, rilevale
149
+ if line_positions is None:
150
+ line_positions = self.detect_lines(binary)
151
+
152
+ # Se non ci sono linee, restituisci una lista vuota
153
+ if not line_positions:
154
+ return []
155
+
156
+ # Calcola l'altezza media delle linee
157
+ line_height = 30 # Valore predefinito
158
+ if len(line_positions) > 1:
159
+ line_height = int(np.mean([line_positions[i+1] - line_positions[i]
160
+ for i in range(len(line_positions)-1)]))
161
+
162
+ # Rileva le parole per ogni linea
163
+ words = []
164
+ for i, y in enumerate(line_positions):
165
+ # Estrai una regione intorno alla linea
166
+ y_start = max(0, y - line_height // 2)
167
+ y_end = min(binary.shape[0], y + line_height // 2)
168
+ line_region = binary[y_start:y_end, :]
169
+
170
+ # Proiezione verticale (somma i pixel bianchi per ogni colonna)
171
+ projection = np.sum(line_region, axis=0)
172
+
173
+ # Normalizza la proiezione
174
+ if np.max(projection) > 0:
175
+ projection = projection / np.max(projection)
176
+
177
+ # Trova i confini delle parole
178
+ threshold = 0.1 # Soglia per considerare uno spazio
179
+ in_word = False
180
+ start_word = 0
181
+
182
+ for j in range(len(projection)):
183
+ if projection[j] > threshold and not in_word:
184
+ in_word = True
185
+ start_word = j
186
+ elif projection[j] <= threshold and in_word:
187
+ in_word = False
188
+ words.append((i, start_word, j))
189
+
190
+ # Se l'ultima parola non è stata chiusa
191
+ if in_word:
192
+ words.append((i, start_word, len(projection) - 1))
193
+
194
+ return words
195
+
196
+ def measure_word_spacing(self, image):
197
+ """
198
+ Misura lo spazio tra le parole.
199
+
200
+ Args:
201
+ image (numpy.ndarray): Immagine di input
202
+
203
+ Returns:
204
+ dict: Informazioni sullo spazio tra le parole
205
+ """
206
+ # Rileva le linee
207
+ lines = self.detect_lines(image)
208
+
209
+ # Rileva i confini delle parole
210
+ words = self.detect_word_boundaries(image, lines)
211
+
212
+ # Calcola lo spazio tra le parole consecutive sulla stessa linea
213
+ spacing = []
214
+ for i in range(len(words)-1):
215
+ line1, _, end1 = words[i]
216
+ line2, start2, _ = words[i+1]
217
+
218
+ # Considera solo le parole sulla stessa linea
219
+ if line1 == line2:
220
+ space = start2 - end1
221
+ if space > 0: # Ignora sovrapposizioni
222
+ spacing.append(space)
223
+
224
+ if not spacing:
225
+ return {
226
+ 'word_count': len(words),
227
+ 'average_spacing': 0,
228
+ 'spacing_std': 0,
229
+ 'spacing_values': []
230
+ }
231
+
232
+ return {
233
+ 'word_count': len(words),
234
+ 'average_spacing': np.mean(spacing),
235
+ 'spacing_std': np.std(spacing),
236
+ 'spacing_values': spacing
237
+ }
238
+
239
+ def detect_margins(self, image):
240
+ """
241
+ Rileva i margini del documento.
242
+
243
+ Args:
244
+ image (numpy.ndarray): Immagine di input
245
+
246
+ Returns:
247
+ dict: Informazioni sui margini (sinistra, destra, superiore, inferiore)
248
+ """
249
+ # Converti in scala di grigi se necessario
250
+ if len(image.shape) > 2:
251
+ gray = self.preprocessor.convert_to_grayscale(image)
252
+ else:
253
+ gray = image
254
+
255
+ # Binarizza l'immagine
256
+ binary = self.preprocessor.threshold_image(gray, method='adaptive')
257
+
258
+ # Inverti l'immagine (testo bianco su sfondo nero)
259
+ binary_inv = cv2.bitwise_not(binary)
260
+
261
+ # Proiezione orizzontale (somma i pixel bianchi per ogni riga)
262
+ h_projection = np.sum(binary_inv, axis=1)
263
+
264
+ # Proiezione verticale (somma i pixel bianchi per ogni colonna)
265
+ v_projection = np.sum(binary_inv, axis=0)
266
+
267
+ # Normalizza le proiezioni
268
+ if np.max(h_projection) > 0:
269
+ h_projection = h_projection / np.max(h_projection)
270
+ if np.max(v_projection) > 0:
271
+ v_projection = v_projection / np.max(v_projection)
272
+
273
+ # Trova i margini
274
+ threshold = 0.05 # Soglia per considerare un margine
275
+
276
+ # Margine superiore
277
+ top_margin = 0
278
+ while top_margin < len(h_projection) and h_projection[top_margin] <= threshold:
279
+ top_margin += 1
280
+
281
+ # Margine inferiore
282
+ bottom_margin = len(h_projection) - 1
283
+ while bottom_margin >= 0 and h_projection[bottom_margin] <= threshold:
284
+ bottom_margin -= 1
285
+ bottom_margin = len(h_projection) - 1 - bottom_margin
286
+
287
+ # Margine sinistro
288
+ left_margin = 0
289
+ while left_margin < len(v_projection) and v_projection[left_margin] <= threshold:
290
+ left_margin += 1
291
+
292
+ # Margine destro
293
+ right_margin = len(v_projection) - 1
294
+ while right_margin >= 0 and v_projection[right_margin] <= threshold:
295
+ right_margin -= 1
296
+ right_margin = len(v_projection) - 1 - right_margin
297
+
298
+ return {
299
+ 'top': top_margin,
300
+ 'bottom': bottom_margin,
301
+ 'left': left_margin,
302
+ 'right': right_margin
303
+ }
304
+
305
+ def measure_character_slant(self, image):
306
+ """
307
+ Misura l'inclinazione dei caratteri.
308
+
309
+ Args:
310
+ image (numpy.ndarray): Immagine di input
311
+
312
+ Returns:
313
+ dict: Informazioni sull'inclinazione dei caratteri
314
+ """
315
+ # Converti in scala di grigi se necessario
316
+ if len(image.shape) > 2:
317
+ gray = self.preprocessor.convert_to_grayscale(image)
318
+ else:
319
+ gray = image
320
+
321
+ # Binarizza l'immagine
322
+ binary = self.preprocessor.threshold_image(gray, method='adaptive')
323
+
324
+ # Applica la trasformata di Hough probabilistica
325
+ edges = cv2.Canny(binary, 50, 150, apertureSize=3)
326
+ lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=50, minLineLength=15, maxLineGap=10)
327
+
328
+ if lines is None:
329
+ return {
330
+ 'average_slant': 0,
331
+ 'slant_std': 0,
332
+ 'slant_values': []
333
+ }
334
+
335
+ # Calcola l'angolo di inclinazione per ogni linea
336
+ angles = []
337
+ for line in lines:
338
+ x1, y1, x2, y2 = line[0]
339
+
340
+ # Ignora le linee orizzontali
341
+ if abs(x2 - x1) > 5:
342
+ # Calcola l'angolo in gradi
343
+ angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
344
+
345
+ # Considera solo gli angoli tra -45 e 45 gradi (caratteri inclinati)
346
+ if -45 <= angle <= 45:
347
+ angles.append(angle)
348
+
349
+ if not angles:
350
+ return {
351
+ 'average_slant': 0,
352
+ 'slant_std': 0,
353
+ 'slant_values': []
354
+ }
355
+
356
+ return {
357
+ 'average_slant': np.mean(angles),
358
+ 'slant_std': np.std(angles),
359
+ 'slant_values': angles
360
+ }
361
+
362
+ def analyze_pressure_profile(self, image):
363
+ """
364
+ Analizza il profilo di pressione in un'immagine.
365
+
366
+ Args:
367
+ image (numpy.ndarray): Immagine di input
368
+
369
+ Returns:
370
+ dict: Informazioni sul profilo di pressione
371
+ """
372
+ # Converti in scala di grigi se necessario
373
+ if len(image.shape) > 2:
374
+ gray = self.preprocessor.convert_to_grayscale(image)
375
+ else:
376
+ gray = image
377
+
378
+ # Inverti l'immagine (testo bianco su sfondo nero)
379
+ gray_inv = cv2.bitwise_not(gray)
380
+
381
+ # Applica una soglia per isolare il testo
382
+ _, binary = cv2.threshold(gray_inv, 50, 255, cv2.THRESH_BINARY)
383
+
384
+ # Calcola l'intensità media dei pixel di testo
385
+ text_pixels = gray_inv[binary > 0]
386
+
387
+ if len(text_pixels) == 0:
388
+ return {
389
+ 'average_pressure': 0,
390
+ 'pressure_std': 0,
391
+ 'pressure_histogram': None
392
+ }
393
+
394
+ # Calcola l'istogramma dell'intensità
395
+ hist, bins = np.histogram(text_pixels, bins=50, range=(0, 255))
396
+
397
+ # Normalizza l'istogramma
398
+ hist = hist / np.sum(hist)
399
+
400
+ # Calcola la pressione media (intensità media)
401
+ average_pressure = np.mean(text_pixels)
402
+
403
+ # Calcola la deviazione standard della pressione
404
+ pressure_std = np.std(text_pixels)
405
+
406
+ return {
407
+ 'average_pressure': float(average_pressure),
408
+ 'pressure_std': float(pressure_std),
409
+ 'pressure_histogram': {
410
+ 'hist': hist.tolist(),
411
+ 'bins': bins.tolist()
412
+ }
413
+ }
414
+
415
+ def generate_measurement_report(self, image):
416
+ """
417
+ Genera un report completo di misurazione per un'immagine.
418
+
419
+ Args:
420
+ image (numpy.ndarray): Immagine di input
421
+
422
+ Returns:
423
+ dict: Report completo di misurazione
424
+ """
425
+ # Carica l'immagine se è un percorso file
426
+ if isinstance(image, str):
427
+ image = self.preprocessor.load_image(image)
428
+
429
+ # Misura lo spazio tra le linee
430
+ line_spacing = self.measure_line_spacing(image)
431
+
432
+ # Misura lo spazio tra le parole
433
+ word_spacing = self.measure_word_spacing(image)
434
+
435
+ # Rileva i margini
436
+ margins = self.detect_margins(image)
437
+
438
+ # Misura l'inclinazione dei caratteri
439
+ slant = self.measure_character_slant(image)
440
+
441
+ # Analizza il profilo di pressione
442
+ pressure = self.analyze_pressure_profile(image)
443
+
444
+ return {
445
+ 'line_spacing': line_spacing,
446
+ 'word_spacing': word_spacing,
447
+ 'margins': margins,
448
+ 'character_slant': slant,
449
+ 'pressure_profile': pressure
450
+ }
451
+
452
+ def visualize_measurements(self, image, measurements, save_path=None):
453
+ """
454
+ Visualizza le misurazioni su un'immagine.
455
+
456
+ Args:
457
+ image (numpy.ndarray): Immagine di input
458
+ measurements (dict): Risultato di generate_measurement_report
459
+ save_path (str, optional): Percorso dove salvare l'immagine
460
+
461
+ Returns:
462
+ matplotlib.figure.Figure: Figura con la visualizzazione
463
+ """
464
+ # Crea una copia dell'immagine per la visualizzazione
465
+ if len(image.shape) == 2:
466
+ # Converti in BGR se è in scala di grigi
467
+ vis_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
468
+ else:
469
+ vis_image = image.copy()
470
+
471
+ # Converti in RGB per matplotlib
472
+ vis_image_rgb = cv2.cvtColor(vis_image, cv2.COLOR_BGR2RGB)
473
+
474
+ # Crea una figura con più sottografici
475
+ fig, axs = plt.subplots(2, 2, figsize=(15, 12))
476
+
477
+ # Immagine con linee di testo
478
+ axs[0, 0].imshow(vis_image_rgb)
479
+ axs[0, 0].set_title('Linee di Testo e Margini')
480
+
481
+ # Disegna le linee di testo
482
+ for y in measurements['line_spacing']['line_positions']:
483
+ axs[0, 0].axhline(y=y, color='r', linestyle='-', alpha=0.5)
484
+
485
+ # Disegna i margini
486
+ margins = measurements['margins']
487
+ h, w = image.shape[:2]
488
+
489
+ # Margine superiore
490
+ axs[0, 0].axhline(y=margins['top'], color='g', linestyle='--')
491
+ # Margine inferiore
492
+ axs[0, 0].axhline(y=h - margins['bottom'], color='g', linestyle='--')
493
+ # Margine sinistro
494
+ axs[0, 0].axvline(x=margins['left'], color='g', linestyle='--')
495
+ # Margine destro
496
+ axs[0, 0].axvline(x=w - margins['right'], color='g', linestyle='--')
497
+
498
+ axs[0, 0].axis('off')
499
+
500
+ # Grafico dell'inclinazione dei caratteri
501
+ if measurements['character_slant']['slant_values']:
502
+ axs[0, 1].hist(measurements['character_slant']['slant_values'], bins=20,
503
+ range=(-45, 45), color='blue', alpha=0.7)
504
+ axs[0, 1].axvline(x=measurements['character_slant']['average_slant'],
505
+ color='r', linestyle='-', linewidth=2)
506
+ axs[0, 1].set_title(f"Inclinazione dei Caratteri: {measurements['character_slant']['average_slant']:.1f}°")
507
+ axs[0, 1].set_xlabel('Angolo (gradi)')
508
+ axs[0, 1].set_ylabel('Frequenza')
509
+ else:
510
+ axs[0, 1].text(0.5, 0.5, 'Dati di inclinazione non disponibili',
511
+ horizontalalignment='center', verticalalignment='center')
512
+ axs[0, 1].set_title('Inclinazione dei Caratteri')
513
+
514
+ # Grafico del profilo di pressione
515
+ if measurements['pressure_profile']['pressure_histogram'] is not None:
516
+ hist = measurements['pressure_profile']['pressure_histogram']['hist']
517
+ bins = measurements['pressure_profile']['pressure_histogram']['bins']
518
+ bin_centers = 0.5 * (bins[:-1] + bins[1:])
519
+
520
+ axs[1, 0].bar(bin_centers, hist, width=bins[1] - bins[0], color='green', alpha=0.7)
521
+ axs[1, 0].axvline(x=measurements['pressure_profile']['average_pressure'],
522
+ color='r', linestyle='-', linewidth=2)
523
+ axs[1, 0].set_title(f"Profilo di Pressione: {measurements['pressure_profile']['average_pressure']:.1f}")
524
+ axs[1, 0].set_xlabel('Intensità')
525
+ axs[1, 0].set_ylabel('Frequenza Normalizzata')
526
+ else:
527
+ axs[1, 0].text(0.5, 0.5, 'Dati di pressione non disponibili',
528
+ horizontalalignment='center', verticalalignment='center')
529
+ axs[1, 0].set_title('Profilo di Pressione')
530
+
531
+ # Tabella con le misurazioni
532
+ axs[1, 1].axis('tight')
533
+ axs[1, 1].axis('off')
534
+
535
+ table_data = [
536
+ ['Metrica', 'Valore'],
537
+ ['Numero di Linee', f"{measurements['line_spacing']['line_count']}"],
538
+ ['Spazio Medio tra Linee', f"{measurements['line_spacing']['average_spacing']:.1f} px"],
539
+ ['Numero di Parole', f"{measurements['word_spacing']['word_count']}"],
540
+ ['Spazio Medio tra Parole', f"{measurements['word_spacing']['average_spacing']:.1f} px"],
541
+ ['Margine Superiore', f"{margins['top']} px"],
542
+ ['Margine Inferiore', f"{margins['bottom']} px"],
543
+ ['Margine Sinistro', f"{margins['left']} px"],
544
+ ['Margine Destro', f"{margins['right']} px"],
545
+ ['Inclinazione Media', f"{measurements['character_slant']['average_slant']:.1f}°"],
546
+ ['Pressione Media', f"{measurements['pressure_profile']['average_pressure']:.1f}"]
547
+ ]
548
+
549
+ table = axs[1, 1].table(cellText=table_data, loc='center', cellLoc='center')
550
+ table.auto_set_font_size(False)
551
+ table.set_fontsize(10)
552
+ table.scale(1, 1.5)
553
+ axs[1, 1].set_title('Riepilogo Misurazioni')
554
+
555
+ plt.tight_layout()
556
+
557
+ # Salva l'immagine se richiesto
558
+ if save_path:
559
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
560
+
561
+ return fig
562
+
563
+ def create_digital_ruler(self, image, dpi=96, save_path=None):
564
+ """
565
+ Crea un righello digitale sovrapposto all'immagine.
566
+
567
+ Args:
568
+ image (numpy.ndarray): Immagine di input
569
+ dpi (int): Punti per pollice (per la conversione in unità fisiche)
570
+ save_path (str, optional): Percorso dove salvare l'immagine
571
+
572
+ Returns:
573
+ numpy.ndarray: Immagine con righello sovrapposto
574
+ """
575
+ # Crea una copia dell'immagine per la visualizzazione
576
+ if len(image.shape) == 2:
577
+ # Converti in BGR se è in scala di grigi
578
+ vis_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
579
+ else:
580
+ vis_image = image.copy()
581
+
582
+ # Dimensioni dell'immagine
583
+ h, w = image.shape[:2]
584
+
585
+ # Calcola la scala (pixel per millimetro)
586
+ pixels_per_mm = dpi / 25.4 # 25.4 mm = 1 pollice
587
+
588
+ # Disegna il righello orizzontale
589
+ y_ruler = 30 # Posizione y del righello orizzontale
590
+
591
+ # Disegna la linea principale
592
+ cv2.line(vis_image, (0, y_ruler), (w, y_ruler), (0, 0, 255), 2)
593
+
594
+ # Disegna le tacche principali (ogni 10 mm)
595
+ for x in range(0, w, int(10 * pixels_per_mm)):
596
+ cv2.line(vis_image, (x, y_ruler - 10), (x, y_ruler + 10), (0, 0, 255), 2)
597
+ # Aggiungi l'etichetta (in mm)
598
+ label = f"{int(x / pixels_per_mm)}"
599
+ cv2.putText(vis_image, label, (x - 10, y_ruler - 15),
600
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
601
+
602
+ # Disegna le tacche secondarie (ogni 1 mm)
603
+ for x in range(0, w, int(1 * pixels_per_mm)):
604
+ cv2.line(vis_image, (x, y_ruler - 5), (x, y_ruler + 5), (0, 0, 255), 1)
605
+
606
+ # Disegna il righello verticale
607
+ x_ruler = 30 # Posizione x del righello verticale
608
+
609
+ # Disegna la linea principale
610
+ cv2.line(vis_image, (x_ruler, 0), (x_ruler, h), (0, 0, 255), 2)
611
+
612
+ # Disegna le tacche principali (ogni 10 mm)
613
+ for y in range(0, h, int(10 * pixels_per_mm)):
614
+ cv2.line(vis_image, (x_ruler - 10, y), (x_ruler + 10, y), (0, 0, 255), 2)
615
+ # Aggiungi l'etichetta (in mm)
616
+ label = f"{int(y / pixels_per_mm)}"
617
+ cv2.putText(vis_image, label, (x_ruler - 30, y + 5),
618
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
619
+
620
+ # Disegna le tacche secondarie (ogni 1 mm)
621
+ for y in range(0, h, int(1 * pixels_per_mm)):
622
+ cv2.line(vis_image, (x_ruler - 5, y), (x_ruler + 5, y), (0, 0, 255), 1)
623
+
624
+ # Aggiungi informazioni sulla scala
625
+ scale_info = f"Scala: 1 pixel = {1/pixels_per_mm:.3f} mm (DPI: {dpi})"
626
+ cv2.putText(vis_image, scale_info, (w - 300, h - 20),
627
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1)
628
+
629
+ # Salva l'immagine se richiesto
630
+ if save_path:
631
+ cv2.imwrite(save_path, vis_image)
632
+
633
+ return vis_image
src/ml_models.py ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ from sklearn.ensemble import IsolationForest
5
+ from sklearn.preprocessing import StandardScaler
6
+ from sklearn.model_selection import train_test_split
7
+ from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.optim as optim
11
+ from torch.utils.data import Dataset, DataLoader
12
+ import cv2
13
+ import os
14
+ import pickle
15
+ import joblib
16
+
17
+ from .preprocessing import ImagePreprocessor
18
+ from .signature_analysis import SignatureAnalyzer
19
+
20
+
21
+ class SignatureFeatureExtractor:
22
+ """
23
+ Classe per estrarre caratteristiche dalle firme da utilizzare nei modelli di machine learning.
24
+ """
25
+
26
+ def __init__(self):
27
+ """Inizializza l'estrattore di caratteristiche."""
28
+ self.preprocessor = ImagePreprocessor()
29
+ self.analyzer = SignatureAnalyzer()
30
+
31
+ def extract_features(self, image_path):
32
+ """
33
+ Estrae un vettore di caratteristiche da un'immagine di firma.
34
+
35
+ Args:
36
+ image_path (str): Percorso dell'immagine della firma
37
+
38
+ Returns:
39
+ dict: Dizionario di caratteristiche
40
+ """
41
+ # Pre-elabora la firma
42
+ processed = self.preprocessor.preprocess_signature(image_path)
43
+
44
+ # Estrai metriche grafometriche
45
+ metrics = self.analyzer.extract_signature_metrics(processed['binary'])
46
+
47
+ # Estrai caratteristiche ORB
48
+ keypoints, descriptors = self.analyzer.extract_features_orb(processed['binary'])
49
+
50
+ # Se non ci sono descrittori, restituisci un vettore di zeri
51
+ if descriptors is None:
52
+ orb_features = np.zeros(32)
53
+ else:
54
+ # Calcola la media dei descrittori per ottenere un vettore di caratteristiche fisso
55
+ orb_features = np.mean(descriptors, axis=0) if descriptors.shape[0] > 0 else np.zeros(32)
56
+
57
+ # Calcola caratteristiche aggiuntive dall'immagine binaria
58
+ binary = processed['binary']
59
+
60
+ # Calcola il numero di componenti connessi (tratti separati)
61
+ num_labels, labels = cv2.connectedComponents(binary)
62
+
63
+ # Calcola il rapporto tra pixel bianchi e neri
64
+ white_pixels = cv2.countNonZero(binary)
65
+ total_pixels = binary.shape[0] * binary.shape[1]
66
+ black_pixels = total_pixels - white_pixels
67
+ white_black_ratio = white_pixels / black_pixels if black_pixels > 0 else 0
68
+
69
+ # Calcola la densità dei pixel (percentuale di pixel bianchi)
70
+ density = white_pixels / total_pixels
71
+
72
+ # Calcola il centro di massa
73
+ y_indices, x_indices = np.where(binary > 0)
74
+ if len(x_indices) > 0 and len(y_indices) > 0:
75
+ center_x = np.mean(x_indices)
76
+ center_y = np.mean(y_indices)
77
+ else:
78
+ center_x = 0
79
+ center_y = 0
80
+
81
+ # Normalizza il centro di massa rispetto alle dimensioni dell'immagine
82
+ norm_center_x = center_x / binary.shape[1] if binary.shape[1] > 0 else 0
83
+ norm_center_y = center_y / binary.shape[0] if binary.shape[0] > 0 else 0
84
+
85
+ # Calcola momenti di Hu (invarianti alla rotazione, scala e traslazione)
86
+ moments = cv2.moments(binary)
87
+ hu_moments = cv2.HuMoments(moments).flatten()
88
+
89
+ # Logaritmo dei momenti di Hu per gestire meglio i valori molto piccoli
90
+ hu_moments = -np.sign(hu_moments) * np.log10(np.abs(hu_moments) + 1e-10)
91
+
92
+ # Combina tutte le caratteristiche in un dizionario
93
+ features = {
94
+ # Metriche grafometriche
95
+ 'area': metrics['area'],
96
+ 'perimeter': metrics['perimeter'],
97
+ 'width': metrics['width'],
98
+ 'height': metrics['height'],
99
+ 'aspect_ratio': metrics['aspect_ratio'],
100
+ 'density': metrics['density'],
101
+ 'slant_angle': metrics['slant_angle'],
102
+
103
+ # Caratteristiche aggiuntive
104
+ 'num_components': num_labels - 1, # -1 perché lo sfondo è contato come componente
105
+ 'white_black_ratio': white_black_ratio,
106
+ 'pixel_density': density,
107
+ 'center_x_norm': norm_center_x,
108
+ 'center_y_norm': norm_center_y,
109
+
110
+ # Momenti di Hu
111
+ 'hu1': hu_moments[0],
112
+ 'hu2': hu_moments[1],
113
+ 'hu3': hu_moments[2],
114
+ 'hu4': hu_moments[3],
115
+ 'hu5': hu_moments[4],
116
+ 'hu6': hu_moments[5],
117
+ 'hu7': hu_moments[6],
118
+ }
119
+
120
+ # Aggiungi le caratteristiche ORB
121
+ for i, val in enumerate(orb_features):
122
+ features[f'orb_{i}'] = float(val)
123
+
124
+ return features
125
+
126
+ def extract_features_batch(self, image_paths):
127
+ """
128
+ Estrae caratteristiche da un batch di immagini di firme.
129
+
130
+ Args:
131
+ image_paths (list): Lista di percorsi delle immagini
132
+
133
+ Returns:
134
+ pandas.DataFrame: DataFrame con le caratteristiche estratte
135
+ """
136
+ features_list = []
137
+
138
+ for path in image_paths:
139
+ try:
140
+ features = self.extract_features(path)
141
+ features['image_path'] = path
142
+ features_list.append(features)
143
+ except Exception as e:
144
+ print(f"Errore nell'estrazione delle caratteristiche da {path}: {e}")
145
+
146
+ return pd.DataFrame(features_list)
147
+
148
+
149
+ class AnomalyDetector:
150
+ """
151
+ Classe per il rilevamento di anomalie nelle firme utilizzando Isolation Forest.
152
+ """
153
+
154
+ def __init__(self, contamination=0.1, random_state=42):
155
+ """
156
+ Inizializza il rilevatore di anomalie.
157
+
158
+ Args:
159
+ contamination (float): Percentuale attesa di outlier nei dati
160
+ random_state (int): Seed per la riproducibilità
161
+ """
162
+ self.model = IsolationForest(contamination=contamination, random_state=random_state)
163
+ self.scaler = StandardScaler()
164
+ self.feature_extractor = SignatureFeatureExtractor()
165
+ self.is_fitted = False
166
+
167
+ def fit(self, signatures_df=None, signatures_paths=None):
168
+ """
169
+ Addestra il modello di rilevamento anomalie.
170
+
171
+ Args:
172
+ signatures_df (pandas.DataFrame, optional): DataFrame con le caratteristiche estratte
173
+ signatures_paths (list, optional): Lista di percorsi delle immagini di firme autentiche
174
+
175
+ Returns:
176
+ self: Istanza addestrata
177
+ """
178
+ if signatures_df is None and signatures_paths is None:
179
+ raise ValueError("È necessario fornire o un DataFrame di caratteristiche o una lista di percorsi di immagini")
180
+
181
+ if signatures_df is None:
182
+ # Estrai caratteristiche dalle immagini
183
+ signatures_df = self.feature_extractor.extract_features_batch(signatures_paths)
184
+
185
+ # Rimuovi colonne non numeriche
186
+ features_df = signatures_df.select_dtypes(include=['number'])
187
+
188
+ # Normalizza le caratteristiche
189
+ X = self.scaler.fit_transform(features_df)
190
+
191
+ # Addestra il modello
192
+ self.model.fit(X)
193
+ self.is_fitted = True
194
+
195
+ # Salva le colonne utilizzate
196
+ self.feature_columns = features_df.columns.tolist()
197
+
198
+ return self
199
+
200
+ def predict(self, signature_path=None, features=None):
201
+ """
202
+ Predice se una firma è anomala.
203
+
204
+ Args:
205
+ signature_path (str, optional): Percorso dell'immagine della firma
206
+ features (dict, optional): Caratteristiche già estratte
207
+
208
+ Returns:
209
+ dict: Risultato della predizione
210
+ """
211
+ if not self.is_fitted:
212
+ raise ValueError("Il modello deve essere addestrato prima di fare predizioni")
213
+
214
+ if signature_path is None and features is None:
215
+ raise ValueError("È necessario fornire o un percorso di immagine o le caratteristiche estratte")
216
+
217
+ if features is None:
218
+ # Estrai caratteristiche dall'immagine
219
+ features = self.feature_extractor.extract_features(signature_path)
220
+
221
+ # Crea un DataFrame con le caratteristiche
222
+ features_df = pd.DataFrame([features])
223
+
224
+ # Seleziona solo le colonne utilizzate durante l'addestramento
225
+ features_df = features_df[self.feature_columns]
226
+
227
+ # Normalizza le caratteristiche
228
+ X = self.scaler.transform(features_df)
229
+
230
+ # Predici l'anomalia
231
+ # -1 per outlier (anomalia), 1 per inlier (normale)
232
+ prediction = self.model.predict(X)[0]
233
+
234
+ # Calcola il punteggio di anomalia
235
+ # Più negativo è il punteggio, più anomala è la firma
236
+ score = self.model.decision_function(X)[0]
237
+
238
+ # Converti il punteggio in un valore percentuale
239
+ # 0% = molto anomalo, 100% = normale
240
+ normalized_score = (score + 0.5) / 1.0 # Adatta in base ai tuoi dati
241
+ normalized_score = max(0, min(1, normalized_score)) * 100
242
+
243
+ return {
244
+ 'is_anomaly': prediction == -1,
245
+ 'anomaly_score': score,
246
+ 'confidence': normalized_score,
247
+ 'prediction': 'anomaly' if prediction == -1 else 'normal'
248
+ }
249
+
250
+ def save_model(self, model_path, scaler_path=None):
251
+ """
252
+ Salva il modello addestrato.
253
+
254
+ Args:
255
+ model_path (str): Percorso dove salvare il modello
256
+ scaler_path (str, optional): Percorso dove salvare lo scaler
257
+ """
258
+ if not self.is_fitted:
259
+ raise ValueError("Il modello deve essere addestrato prima di essere salvato")
260
+
261
+ # Salva il modello
262
+ joblib.dump(self.model, model_path)
263
+
264
+ # Salva lo scaler se specificato
265
+ if scaler_path:
266
+ joblib.dump(self.scaler, scaler_path)
267
+
268
+ # Salva anche le colonne delle caratteristiche
269
+ metadata = {
270
+ 'feature_columns': self.feature_columns
271
+ }
272
+
273
+ # Salva i metadati
274
+ metadata_path = os.path.splitext(model_path)[0] + '_metadata.pkl'
275
+ with open(metadata_path, 'wb') as f:
276
+ pickle.dump(metadata, f)
277
+
278
+ def load_model(self, model_path, scaler_path=None):
279
+ """
280
+ Carica un modello addestrato.
281
+
282
+ Args:
283
+ model_path (str): Percorso del modello salvato
284
+ scaler_path (str, optional): Percorso dello scaler salvato
285
+ """
286
+ # Carica il modello
287
+ self.model = joblib.load(model_path)
288
+
289
+ # Carica lo scaler se specificato
290
+ if scaler_path:
291
+ self.scaler = joblib.load(scaler_path)
292
+
293
+ # Carica i metadati
294
+ metadata_path = os.path.splitext(model_path)[0] + '_metadata.pkl'
295
+ if os.path.exists(metadata_path):
296
+ with open(metadata_path, 'rb') as f:
297
+ metadata = pickle.load(f)
298
+ self.feature_columns = metadata['feature_columns']
299
+
300
+ self.is_fitted = True
301
+
302
+
303
+ class SignatureDataset(Dataset):
304
+ """
305
+ Dataset PyTorch per le immagini di firme.
306
+ """
307
+
308
+ def __init__(self, image_paths, labels=None, transform=None, target_size=(128, 128)):
309
+ """
310
+ Inizializza il dataset.
311
+
312
+ Args:
313
+ image_paths (list): Lista di percorsi delle immagini
314
+ labels (list, optional): Lista di etichette (1 per autentico, 0 per falso)
315
+ transform (callable, optional): Trasformazioni da applicare alle immagini
316
+ target_size (tuple): Dimensione target per le immagini
317
+ """
318
+ self.image_paths = image_paths
319
+ self.labels = labels
320
+ self.transform = transform
321
+ self.target_size = target_size
322
+ self.preprocessor = ImagePreprocessor()
323
+
324
+ def __len__(self):
325
+ return len(self.image_paths)
326
+
327
+ def __getitem__(self, idx):
328
+ # Carica l'immagine
329
+ image = self.preprocessor.load_image(self.image_paths[idx])
330
+
331
+ # Pre-elabora l'immagine
332
+ image = self.preprocessor.convert_to_grayscale(image)
333
+ image = self.preprocessor.normalize_image(image)
334
+
335
+ # Ridimensiona l'immagine
336
+ image = cv2.resize(image, self.target_size)
337
+
338
+ # Normalizza i valori dei pixel nell'intervallo [0, 1]
339
+ image = image.astype(np.float32) / 255.0
340
+
341
+ # Aggiungi una dimensione per il canale (1 canale per immagini in scala di grigi)
342
+ image = np.expand_dims(image, axis=0)
343
+
344
+ # Converti in tensore PyTorch
345
+ image = torch.from_numpy(image)
346
+
347
+ # Applica trasformazioni se specificate
348
+ if self.transform:
349
+ image = self.transform(image)
350
+
351
+ # Restituisci l'immagine e l'etichetta se disponibile
352
+ if self.labels is not None:
353
+ label = self.labels[idx]
354
+ return image, torch.tensor(label, dtype=torch.float32)
355
+ else:
356
+ return image
357
+
358
+
359
+ class SiameseNetwork(nn.Module):
360
+ """
361
+ Rete siamese per la verifica delle firme.
362
+ """
363
+
364
+ def __init__(self):
365
+ """Inizializza la rete siamese."""
366
+ super(SiameseNetwork, self).__init__()
367
+
368
+ # CNN per l'estrazione delle caratteristiche
369
+ self.cnn = nn.Sequential(
370
+ # Prima convoluzione
371
+ nn.Conv2d(1, 64, kernel_size=10, stride=1),
372
+ nn.ReLU(inplace=True),
373
+ nn.MaxPool2d(2),
374
+
375
+ # Seconda convoluzione
376
+ nn.Conv2d(64, 128, kernel_size=7, stride=1),
377
+ nn.ReLU(inplace=True),
378
+ nn.MaxPool2d(2),
379
+
380
+ # Terza convoluzione
381
+ nn.Conv2d(128, 128, kernel_size=4, stride=1),
382
+ nn.ReLU(inplace=True),
383
+ nn.MaxPool2d(2),
384
+
385
+ # Quarta convoluzione
386
+ nn.Conv2d(128, 256, kernel_size=4, stride=1),
387
+ nn.ReLU(inplace=True)
388
+ )
389
+
390
+ # Fully connected per la classificazione
391
+ self.fc = nn.Sequential(
392
+ nn.Linear(256 * 9 * 9, 4096),
393
+ nn.Sigmoid()
394
+ )
395
+
396
+ # Layer di output
397
+ self.output = nn.Sequential(
398
+ nn.Linear(4096, 1),
399
+ nn.Sigmoid()
400
+ )
401
+
402
+ def forward_one(self, x):
403
+ """
404
+ Forward pass per una singola immagine.
405
+
406
+ Args:
407
+ x (torch.Tensor): Immagine di input
408
+
409
+ Returns:
410
+ torch.Tensor: Embedding dell'immagine
411
+ """
412
+ x = self.cnn(x)
413
+ x = x.view(x.size(0), -1)
414
+ x = self.fc(x)
415
+ return x
416
+
417
+ def forward(self, input1, input2):
418
+ """
419
+ Forward pass per una coppia di immagini.
420
+
421
+ Args:
422
+ input1 (torch.Tensor): Prima immagine
423
+ input2 (torch.Tensor): Seconda immagine
424
+
425
+ Returns:
426
+ torch.Tensor: Probabilità che le firme siano della stessa persona
427
+ """
428
+ # Ottieni gli embedding per entrambe le immagini
429
+ output1 = self.forward_one(input1)
430
+ output2 = self.forward_one(input2)
431
+
432
+ # Calcola la distanza euclidea
433
+ distance = torch.abs(output1 - output2)
434
+
435
+ # Calcola la probabilità
436
+ prob = self.output(distance)
437
+
438
+ return prob
439
+
440
+
441
+ class SignatureVerifier:
442
+ """
443
+ Classe per la verifica delle firme utilizzando una rete siamese.
444
+ """
445
+
446
+ def __init__(self, model_path=None):
447
+ """
448
+ Inizializza il verificatore di firme.
449
+
450
+ Args:
451
+ model_path (str, optional): Percorso del modello pre-addestrato
452
+ """
453
+ self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
454
+ self.model = SiameseNetwork().to(self.device)
455
+ self.preprocessor = ImagePreprocessor()
456
+
457
+ if model_path and os.path.exists(model_path):
458
+ self.load_model(model_path)
459
+
460
+ def train(self, genuine_paths, forged_paths, epochs=20, batch_size=32, learning_rate=0.0001):
461
+ """
462
+ Addestra la rete siamese.
463
+
464
+ Args:
465
+ genuine_paths (list): Lista di percorsi delle firme autentiche
466
+ forged_paths (list): Lista di percorsi delle firme false
467
+ epochs (int): Numero di epoche di addestramento
468
+ batch_size (int): Dimensione del batch
469
+ learning_rate (float): Tasso di apprendimento
470
+
471
+ Returns:
472
+ dict: Metriche di addestramento
473
+ """
474
+ # Crea coppie di immagini e etichette
475
+ pairs = []
476
+ labels = []
477
+
478
+ # Coppie genuine (stessa persona)
479
+ for i in range(len(genuine_paths)):
480
+ for j in range(i + 1, len(genuine_paths)):
481
+ pairs.append((genuine_paths[i], genuine_paths[j]))
482
+ labels.append(1) # 1 = stessa persona
483
+
484
+ # Coppie false (persone diverse)
485
+ for genuine_path in genuine_paths:
486
+ for forged_path in forged_paths:
487
+ pairs.append((genuine_path, forged_path))
488
+ labels.append(0) # 0 = persone diverse
489
+
490
+ # Dividi in training e validation
491
+ train_pairs, val_pairs, train_labels, val_labels = train_test_split(
492
+ pairs, labels, test_size=0.2, random_state=42, stratify=labels
493
+ )
494
+
495
+ # Crea i dataset
496
+ train_dataset = PairDataset(train_pairs, train_labels)
497
+ val_dataset = PairDataset(val_pairs, val_labels)
498
+
499
+ # Crea i dataloader
500
+ train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
501
+ val_loader = DataLoader(val_dataset, batch_size=batch_size)
502
+
503
+ # Definisci l'ottimizzatore e la funzione di perdita
504
+ optimizer = optim.Adam(self.model.parameters(), lr=learning_rate)
505
+ criterion = nn.BCELoss()
506
+
507
+ # Addestra il modello
508
+ train_losses = []
509
+ val_losses = []
510
+ val_accuracies = []
511
+
512
+ for epoch in range(epochs):
513
+ # Training
514
+ self.model.train()
515
+ train_loss = 0
516
+
517
+ for batch_idx, (img1, img2, target) in enumerate(train_loader):
518
+ img1, img2, target = img1.to(self.device), img2.to(self.device), target.to(self.device)
519
+
520
+ # Forward pass
521
+ output = self.model(img1, img2)
522
+ loss = criterion(output, target.view(-1, 1))
523
+
524
+ # Backward pass
525
+ optimizer.zero_grad()
526
+ loss.backward()
527
+ optimizer.step()
528
+
529
+ train_loss += loss.item()
530
+
531
+ train_loss /= len(train_loader)
532
+ train_losses.append(train_loss)
533
+
534
+ # Validation
535
+ self.model.eval()
536
+ val_loss = 0
537
+ correct = 0
538
+
539
+ with torch.no_grad():
540
+ for img1, img2, target in val_loader:
541
+ img1, img2, target = img1.to(self.device), img2.to(self.device), target.to(self.device)
542
+
543
+ # Forward pass
544
+ output = self.model(img1, img2)
545
+ val_loss += criterion(output, target.view(-1, 1)).item()
546
+
547
+ # Calcola l'accuratezza
548
+ pred = (output > 0.5).float()
549
+ correct += pred.eq(target.view(-1, 1)).sum().item()
550
+
551
+ val_loss /= len(val_loader)
552
+ val_losses.append(val_loss)
553
+
554
+ val_accuracy = 100. * correct / len(val_dataset)
555
+ val_accuracies.append(val_accuracy)
556
+
557
+ print(f'Epoch: {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.2f}%')
558
+
559
+ return {
560
+ 'train_losses': train_losses,
561
+ 'val_losses': val_losses,
562
+ 'val_accuracies': val_accuracies
563
+ }
564
+
565
+ def verify(self, image_path1, image_path2):
566
+ """
567
+ Verifica se due firme sono della stessa persona.
568
+
569
+ Args:
570
+ image_path1 (str): Percorso della prima immagine
571
+ image_path2 (str): Percorso della seconda immagine
572
+
573
+ Returns:
574
+ dict: Risultato della verifica
575
+ """
576
+ self.model.eval()
577
+
578
+ # Carica e pre-elabora le immagini
579
+ img1 = self._preprocess_image(image_path1)
580
+ img2 = self._preprocess_image(image_path2)
581
+
582
+ # Converti in tensori PyTorch
583
+ img1 = torch.from_numpy(img1).unsqueeze(0).to(self.device)
584
+ img2 = torch.from_numpy(img2).unsqueeze(0).to(self.device)
585
+
586
+ # Forward pass
587
+ with torch.no_grad():
588
+ output = self.model(img1, img2)
589
+
590
+ # Calcola la probabilità
591
+ probability = output.item()
592
+
593
+ return {
594
+ 'is_same_person': probability > 0.5,
595
+ 'probability': probability,
596
+ 'confidence': probability * 100 if probability > 0.5 else (1 - probability) * 100
597
+ }
598
+
599
+ def _preprocess_image(self, image_path, target_size=(128, 128)):
600
+ """
601
+ Pre-elabora un'immagine per la rete siamese.
602
+
603
+ Args:
604
+ image_path (str): Percorso dell'immagine
605
+ target_size (tuple): Dimensione target
606
+
607
+ Returns:
608
+ numpy.ndarray: Immagine pre-elaborata
609
+ """
610
+ # Carica l'immagine
611
+ image = self.preprocessor.load_image(image_path)
612
+
613
+ # Pre-elabora l'immagine
614
+ image = self.preprocessor.convert_to_grayscale(image)
615
+ image = self.preprocessor.normalize_image(image)
616
+
617
+ # Ridimensiona l'immagine
618
+ image = cv2.resize(image, target_size)
619
+
620
+ # Normalizza i valori dei pixel nell'intervallo [0, 1]
621
+ image = image.astype(np.float32) / 255.0
622
+
623
+ # Aggiungi una dimensione per il canale (1 canale per immagini in scala di grigi)
624
+ image = np.expand_dims(image, axis=0)
625
+
626
+ return image
627
+
628
+ def save_model(self, model_path):
629
+ """
630
+ Salva il modello addestrato.
631
+
632
+ Args:
633
+ model_path (str): Percorso dove salvare il modello
634
+ """
635
+ torch.save(self.model.state_dict(), model_path)
636
+
637
+ def load_model(self, model_path):
638
+ """
639
+ Carica un modello pre-addestrato.
640
+
641
+ Args:
642
+ model_path (str): Percorso del modello salvato
643
+ """
644
+ self.model.load_state_dict(torch.load(model_path, map_location=self.device))
645
+ self.model.eval()
646
+
647
+
648
+ class PairDataset(Dataset):
649
+ """
650
+ Dataset PyTorch per coppie di immagini di firme.
651
+ """
652
+
653
+ def __init__(self, pairs, labels, target_size=(128, 128)):
654
+ """
655
+ Inizializza il dataset.
656
+
657
+ Args:
658
+ pairs (list): Lista di coppie di percorsi di immagini
659
+ labels (list): Lista di etichette (1 per stessa persona, 0 per persone diverse)
660
+ target_size (tuple): Dimensione target per le immagini
661
+ """
662
+ self.pairs = pairs
663
+ self.labels = labels
664
+ self.target_size = target_size
665
+ self.preprocessor = ImagePreprocessor()
666
+
667
+ def __len__(self):
668
+ return len(self.pairs)
669
+
670
+ def __getitem__(self, idx):
671
+ # Carica la prima immagine
672
+ img1_path, img2_path = self.pairs[idx]
673
+
674
+ # Pre-elabora le immagini
675
+ img1 = self._preprocess_image(img1_path)
676
+ img2 = self._preprocess_image(img2_path)
677
+
678
+ # Converti in tensori PyTorch
679
+ img1 = torch.from_numpy(img1)
680
+ img2 = torch.from_numpy(img2)
681
+
682
+ # Restituisci le immagini e l'etichetta
683
+ return img1, img2, self.labels[idx]
684
+
685
+ def _preprocess_image(self, image_path):
686
+ """
687
+ Pre-elabora un'immagine.
688
+
689
+ Args:
690
+ image_path (str): Percorso dell'immagine
691
+
692
+ Returns:
693
+ numpy.ndarray: Immagine pre-elaborata
694
+ """
695
+ # Carica l'immagine
696
+ image = self.preprocessor.load_image(image_path)
697
+
698
+ # Pre-elabora l'immagine
699
+ image = self.preprocessor.convert_to_grayscale(image)
700
+ image = self.preprocessor.normalize_image(image)
701
+
702
+ # Ridimensiona l'immagine
703
+ image = cv2.resize(image, self.target_size)
704
+
705
+ # Normalizza i valori dei pixel nell'intervallo [0, 1]
706
+ image = image.astype(np.float32) / 255.0
707
+
708
+ # Aggiungi una dimensione per il canale (1 canale per immagini in scala di grigi)
709
+ image = np.expand_dims(image, axis=0)
710
+
711
+ return image
src/preprocessing.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import os
4
+ from PIL import Image
5
+ import fitz # PyMuPDF
6
+
7
+ class ImagePreprocessor:
8
+ """
9
+ Classe per l'acquisizione e pre-elaborazione delle immagini di firme e documenti.
10
+ Implementa funzionalità di base come la conversione in scala di grigi,
11
+ normalizzazione, scontorno dei timbri, ecc.
12
+ """
13
+
14
+ def __init__(self):
15
+ """Inizializza il preprocessore di immagini."""
16
+ pass
17
+
18
+ def load_image(self, image_path):
19
+ """
20
+ Carica un'immagine da un percorso file.
21
+
22
+ Args:
23
+ image_path (str): Percorso dell'immagine da caricare
24
+
25
+ Returns:
26
+ numpy.ndarray: Immagine caricata in formato BGR
27
+ """
28
+ if not os.path.exists(image_path):
29
+ raise FileNotFoundError(f"Il file {image_path} non esiste")
30
+
31
+ # Controlla l'estensione del file
32
+ _, ext = os.path.splitext(image_path)
33
+ ext = ext.lower()
34
+
35
+ if ext == '.pdf':
36
+ return self.extract_image_from_pdf(image_path)
37
+ else:
38
+ # Carica l'immagine usando OpenCV
39
+ image = cv2.imread(image_path)
40
+ if image is None:
41
+ raise ValueError(f"Impossibile caricare l'immagine {image_path}")
42
+ return image
43
+
44
+ def extract_image_from_pdf(self, pdf_path, page_num=0):
45
+ """
46
+ Estrae un'immagine da un file PDF.
47
+
48
+ Args:
49
+ pdf_path (str): Percorso del file PDF
50
+ page_num (int): Numero di pagina da cui estrarre l'immagine (default: 0)
51
+
52
+ Returns:
53
+ numpy.ndarray: Immagine estratta in formato BGR
54
+ """
55
+ # Apri il documento PDF
56
+ doc = fitz.open(pdf_path)
57
+
58
+ # Controlla se il numero di pagina è valido
59
+ if page_num >= len(doc):
60
+ raise ValueError(f"Il PDF ha {len(doc)} pagine, ma è stata richiesta la pagina {page_num}")
61
+
62
+ # Ottieni la pagina
63
+ page = doc.load_page(page_num)
64
+
65
+ # Renderizza la pagina come immagine
66
+ pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # Fattore di scala 2 per migliore qualità
67
+
68
+ # Converti in formato immagine
69
+ img_data = pix.samples
70
+
71
+ # Crea un array numpy dall'immagine
72
+ img_array = np.frombuffer(img_data, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)
73
+
74
+ # Se l'immagine è in formato RGB, converti in BGR per OpenCV
75
+ if pix.n == 3:
76
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
77
+
78
+ return img_array
79
+
80
+ def convert_to_grayscale(self, image):
81
+ """
82
+ Converte un'immagine in scala di grigi.
83
+
84
+ Args:
85
+ image (numpy.ndarray): Immagine di input in formato BGR
86
+
87
+ Returns:
88
+ numpy.ndarray: Immagine in scala di grigi
89
+ """
90
+ if len(image.shape) == 2:
91
+ # L'immagine è già in scala di grigi
92
+ return image
93
+
94
+ return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
95
+
96
+ def normalize_image(self, image):
97
+ """
98
+ Normalizza un'immagine per migliorare contrasto e luminosità.
99
+
100
+ Args:
101
+ image (numpy.ndarray): Immagine di input (scala di grigi o BGR)
102
+
103
+ Returns:
104
+ numpy.ndarray: Immagine normalizzata
105
+ """
106
+ # Converti in scala di grigi se necessario
107
+ if len(image.shape) > 2:
108
+ gray = self.convert_to_grayscale(image)
109
+ else:
110
+ gray = image
111
+
112
+ # Applica equalizzazione dell'istogramma
113
+ return cv2.equalizeHist(gray)
114
+
115
+ def detect_and_extract_stamps(self, image, lower_color=None, upper_color=None):
116
+ """
117
+ Rileva e estrae i timbri da un'immagine utilizzando il filtraggio del colore.
118
+
119
+ Args:
120
+ image (numpy.ndarray): Immagine di input in formato BGR
121
+ lower_color (numpy.ndarray, optional): Limite inferiore del colore in formato HSV
122
+ upper_color (numpy.ndarray, optional): Limite superiore del colore in formato HSV
123
+
124
+ Returns:
125
+ tuple: (immagine_originale_senza_timbri, maschera_timbri, timbri_estratti)
126
+ """
127
+ # Valori predefiniti per rilevare timbri blu (comuni nei documenti)
128
+ if lower_color is None:
129
+ lower_color = np.array([100, 50, 50]) # Blu in HSV
130
+ if upper_color is None:
131
+ upper_color = np.array([140, 255, 255])
132
+
133
+ # Converti l'immagine in HSV
134
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
135
+
136
+ # Crea una maschera per il colore specificato
137
+ mask = cv2.inRange(hsv, lower_color, upper_color)
138
+
139
+ # Applica operazioni morfologiche per migliorare la maschera
140
+ kernel = np.ones((5, 5), np.uint8)
141
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
142
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
143
+
144
+ # Estrai i timbri
145
+ stamps = cv2.bitwise_and(image, image, mask=mask)
146
+
147
+ # Crea un'immagine senza timbri
148
+ inv_mask = cv2.bitwise_not(mask)
149
+ image_without_stamps = cv2.bitwise_and(image, image, mask=inv_mask)
150
+
151
+ return image_without_stamps, mask, stamps
152
+
153
+ def threshold_image(self, image, method='adaptive'):
154
+ """
155
+ Applica una soglia all'immagine per binarizzarla.
156
+
157
+ Args:
158
+ image (numpy.ndarray): Immagine in scala di grigi
159
+ method (str): Metodo di soglia ('simple', 'adaptive', 'otsu')
160
+
161
+ Returns:
162
+ numpy.ndarray: Immagine binaria
163
+ """
164
+ if len(image.shape) > 2:
165
+ gray = self.convert_to_grayscale(image)
166
+ else:
167
+ gray = image
168
+
169
+ if method == 'simple':
170
+ _, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
171
+ elif method == 'adaptive':
172
+ binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
173
+ cv2.THRESH_BINARY_INV, 11, 2)
174
+ elif method == 'otsu':
175
+ _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
176
+ else:
177
+ raise ValueError(f"Metodo di soglia non supportato: {method}")
178
+
179
+ return binary
180
+
181
+ def resize_image(self, image, width=None, height=None, keep_aspect_ratio=True):
182
+ """
183
+ Ridimensiona un'immagine a una larghezza o altezza specificata.
184
+
185
+ Args:
186
+ image (numpy.ndarray): Immagine di input
187
+ width (int, optional): Larghezza desiderata
188
+ height (int, optional): Altezza desiderata
189
+ keep_aspect_ratio (bool): Mantiene il rapporto d'aspetto originale
190
+
191
+ Returns:
192
+ numpy.ndarray: Immagine ridimensionata
193
+ """
194
+ if width is None and height is None:
195
+ return image
196
+
197
+ h, w = image.shape[:2]
198
+
199
+ if keep_aspect_ratio:
200
+ if width is None:
201
+ aspect_ratio = height / float(h)
202
+ dim = (int(w * aspect_ratio), height)
203
+ else:
204
+ aspect_ratio = width / float(w)
205
+ dim = (width, int(h * aspect_ratio))
206
+ else:
207
+ dim = (width if width is not None else w, height if height is not None else h)
208
+
209
+ return cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
210
+
211
+ def denoise_image(self, image, method='gaussian'):
212
+ """
213
+ Applica un filtro di riduzione del rumore all'immagine.
214
+
215
+ Args:
216
+ image (numpy.ndarray): Immagine di input
217
+ method (str): Metodo di denoising ('gaussian', 'median', 'bilateral')
218
+
219
+ Returns:
220
+ numpy.ndarray: Immagine filtrata
221
+ """
222
+ if method == 'gaussian':
223
+ return cv2.GaussianBlur(image, (5, 5), 0)
224
+ elif method == 'median':
225
+ return cv2.medianBlur(image, 5)
226
+ elif method == 'bilateral':
227
+ if len(image.shape) > 2:
228
+ return cv2.bilateralFilter(image, 9, 75, 75)
229
+ else:
230
+ # Per immagini in scala di grigi, convertiamo temporaneamente in BGR
231
+ temp = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
232
+ temp = cv2.bilateralFilter(temp, 9, 75, 75)
233
+ return cv2.cvtColor(temp, cv2.COLOR_BGR2GRAY)
234
+ else:
235
+ raise ValueError(f"Metodo di denoising non supportato: {method}")
236
+
237
+ def preprocess_signature(self, image_path, resize_width=800):
238
+ """
239
+ Pipeline completa di pre-elaborazione per le firme.
240
+
241
+ Args:
242
+ image_path (str): Percorso dell'immagine della firma
243
+ resize_width (int): Larghezza a cui ridimensionare l'immagine
244
+
245
+ Returns:
246
+ dict: Dizionario contenente le diverse fasi di pre-elaborazione
247
+ """
248
+ # Carica l'immagine
249
+ original = self.load_image(image_path)
250
+
251
+ # Ridimensiona l'immagine
252
+ resized = self.resize_image(original, width=resize_width)
253
+
254
+ # Converti in scala di grigi
255
+ gray = self.convert_to_grayscale(resized)
256
+
257
+ # Normalizza l'immagine
258
+ normalized = self.normalize_image(gray)
259
+
260
+ # Applica denoising
261
+ denoised = self.denoise_image(normalized, method='bilateral')
262
+
263
+ # Applica soglia
264
+ binary = self.threshold_image(denoised, method='adaptive')
265
+
266
+ # Restituisci tutte le fasi di pre-elaborazione
267
+ return {
268
+ 'original': original,
269
+ 'resized': resized,
270
+ 'grayscale': gray,
271
+ 'normalized': normalized,
272
+ 'denoised': denoised,
273
+ 'binary': binary
274
+ }
src/rag_system.py ADDED
@@ -0,0 +1,799 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import fitz # PyMuPDF
4
+ import docx
5
+ import pptx
6
+ import numpy as np
7
+ import pandas as pd
8
+ import chromadb
9
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
10
+ from langchain.embeddings import HuggingFaceEmbeddings
11
+ from langchain.vectorstores import Chroma
12
+ from langchain.schema import Document
13
+ from langchain.prompts import PromptTemplate
14
+ from langchain.chains import LLMChain
15
+ from langchain.llms import HuggingFaceHub
16
+ from sentence_transformers import SentenceTransformer
17
+ import torch
18
+ import re
19
+ import hashlib
20
+ import json
21
+ import datetime
22
+
23
+
24
+ class DocumentProcessor:
25
+ """
26
+ Classe per l'elaborazione e l'estrazione di testo da vari formati di documenti.
27
+ """
28
+
29
+ def __init__(self, upload_dir):
30
+ """
31
+ Inizializza il processore di documenti.
32
+
33
+ Args:
34
+ upload_dir (str): Directory dove salvare i documenti caricati
35
+ """
36
+ self.upload_dir = upload_dir
37
+ os.makedirs(upload_dir, exist_ok=True)
38
+
39
+ def save_uploaded_file(self, file_obj, filename=None):
40
+ """
41
+ Salva un file caricato nella directory di upload.
42
+
43
+ Args:
44
+ file_obj: Oggetto file caricato
45
+ filename (str, optional): Nome del file
46
+
47
+ Returns:
48
+ str: Percorso del file salvato
49
+ """
50
+ if filename is None:
51
+ filename = file_obj.name
52
+
53
+ # Genera un nome file sicuro
54
+ safe_filename = self._sanitize_filename(filename)
55
+
56
+ # Aggiungi timestamp per evitare sovrascritture
57
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
58
+ filename_with_timestamp = f"{timestamp}_{safe_filename}"
59
+
60
+ # Percorso completo del file
61
+ file_path = os.path.join(self.upload_dir, filename_with_timestamp)
62
+
63
+ # Salva il file
64
+ with open(file_path, 'wb') as f:
65
+ f.write(file_obj.read())
66
+
67
+ return file_path
68
+
69
+ def _sanitize_filename(self, filename):
70
+ """
71
+ Sanitizza un nome file rimuovendo caratteri non sicuri.
72
+
73
+ Args:
74
+ filename (str): Nome file originale
75
+
76
+ Returns:
77
+ str: Nome file sanitizzato
78
+ """
79
+ # Rimuovi caratteri non sicuri
80
+ safe_filename = re.sub(r'[^\w\.-]', '_', filename)
81
+ return safe_filename
82
+
83
+ def extract_text(self, file_path):
84
+ """
85
+ Estrae il testo da un file in base al suo formato.
86
+
87
+ Args:
88
+ file_path (str): Percorso del file
89
+
90
+ Returns:
91
+ str: Testo estratto
92
+ """
93
+ # Determina il formato del file dall'estensione
94
+ _, ext = os.path.splitext(file_path)
95
+ ext = ext.lower()
96
+
97
+ if ext == '.pdf':
98
+ return self.extract_text_from_pdf(file_path)
99
+ elif ext == '.docx':
100
+ return self.extract_text_from_docx(file_path)
101
+ elif ext == '.pptx':
102
+ return self.extract_text_from_pptx(file_path)
103
+ elif ext == '.txt':
104
+ return self.extract_text_from_txt(file_path)
105
+ else:
106
+ raise ValueError(f"Formato file non supportato: {ext}")
107
+
108
+ def extract_text_from_pdf(self, pdf_path):
109
+ """
110
+ Estrae il testo da un file PDF.
111
+
112
+ Args:
113
+ pdf_path (str): Percorso del file PDF
114
+
115
+ Returns:
116
+ str: Testo estratto
117
+ """
118
+ text = ""
119
+ try:
120
+ # Apri il documento PDF
121
+ doc = fitz.open(pdf_path)
122
+
123
+ # Estrai il testo da ogni pagina
124
+ for page_num in range(len(doc)):
125
+ page = doc.load_page(page_num)
126
+ text += page.get_text()
127
+
128
+ # Chiudi il documento
129
+ doc.close()
130
+ except Exception as e:
131
+ print(f"Errore nell'estrazione del testo dal PDF {pdf_path}: {e}")
132
+
133
+ return text
134
+
135
+ def extract_text_from_docx(self, docx_path):
136
+ """
137
+ Estrae il testo da un file DOCX.
138
+
139
+ Args:
140
+ docx_path (str): Percorso del file DOCX
141
+
142
+ Returns:
143
+ str: Testo estratto
144
+ """
145
+ text = ""
146
+ try:
147
+ # Apri il documento DOCX
148
+ doc = docx.Document(docx_path)
149
+
150
+ # Estrai il testo da ogni paragrafo
151
+ for para in doc.paragraphs:
152
+ text += para.text + "\n"
153
+
154
+ # Estrai il testo dalle tabelle
155
+ for table in doc.tables:
156
+ for row in table.rows:
157
+ for cell in row.cells:
158
+ text += cell.text + " "
159
+ text += "\n"
160
+ except Exception as e:
161
+ print(f"Errore nell'estrazione del testo dal DOCX {docx_path}: {e}")
162
+
163
+ return text
164
+
165
+ def extract_text_from_pptx(self, pptx_path):
166
+ """
167
+ Estrae il testo da un file PPTX.
168
+
169
+ Args:
170
+ pptx_path (str): Percorso del file PPTX
171
+
172
+ Returns:
173
+ str: Testo estratto
174
+ """
175
+ text = ""
176
+ try:
177
+ # Apri la presentazione PPTX
178
+ prs = pptx.Presentation(pptx_path)
179
+
180
+ # Estrai il testo da ogni diapositiva
181
+ for slide in prs.slides:
182
+ for shape in slide.shapes:
183
+ if hasattr(shape, "text"):
184
+ text += shape.text + "\n"
185
+ except Exception as e:
186
+ print(f"Errore nell'estrazione del testo dal PPTX {pptx_path}: {e}")
187
+
188
+ return text
189
+
190
+ def extract_text_from_txt(self, txt_path):
191
+ """
192
+ Estrae il testo da un file TXT.
193
+
194
+ Args:
195
+ txt_path (str): Percorso del file TXT
196
+
197
+ Returns:
198
+ str: Testo estratto
199
+ """
200
+ try:
201
+ # Apri il file TXT
202
+ with open(txt_path, 'r', encoding='utf-8') as f:
203
+ text = f.read()
204
+ except UnicodeDecodeError:
205
+ # Prova con una codifica diversa
206
+ try:
207
+ with open(txt_path, 'r', encoding='latin-1') as f:
208
+ text = f.read()
209
+ except Exception as e:
210
+ print(f"Errore nell'estrazione del testo dal TXT {txt_path}: {e}")
211
+ text = ""
212
+ except Exception as e:
213
+ print(f"Errore nell'estrazione del testo dal TXT {txt_path}: {e}")
214
+ text = ""
215
+
216
+ return text
217
+
218
+ def chunk_text(self, text, chunk_size=500, chunk_overlap=50):
219
+ """
220
+ Divide il testo in chunk più piccoli.
221
+
222
+ Args:
223
+ text (str): Testo da dividere
224
+ chunk_size (int): Dimensione di ogni chunk in token
225
+ chunk_overlap (int): Sovrapposizione tra chunk consecutivi
226
+
227
+ Returns:
228
+ list: Lista di chunk di testo
229
+ """
230
+ # Utilizza il text splitter di LangChain
231
+ text_splitter = RecursiveCharacterTextSplitter(
232
+ chunk_size=chunk_size,
233
+ chunk_overlap=chunk_overlap,
234
+ length_function=len
235
+ )
236
+
237
+ # Dividi il testo in chunk
238
+ chunks = text_splitter.split_text(text)
239
+
240
+ return chunks
241
+
242
+ def process_document(self, file_path, chunk_size=500, chunk_overlap=50):
243
+ """
244
+ Elabora un documento: estrae il testo e lo divide in chunk.
245
+
246
+ Args:
247
+ file_path (str): Percorso del file
248
+ chunk_size (int): Dimensione di ogni chunk in token
249
+ chunk_overlap (int): Sovrapposizione tra chunk consecutivi
250
+
251
+ Returns:
252
+ dict: Informazioni sul documento elaborato
253
+ """
254
+ # Estrai il testo dal documento
255
+ text = self.extract_text(file_path)
256
+
257
+ # Dividi il testo in chunk
258
+ chunks = self.chunk_text(text, chunk_size, chunk_overlap)
259
+
260
+ # Calcola l'hash del file per l'identificazione
261
+ file_hash = self._calculate_file_hash(file_path)
262
+
263
+ # Ottieni il nome del file
264
+ filename = os.path.basename(file_path)
265
+
266
+ # Crea metadati per il documento
267
+ metadata = {
268
+ 'filename': filename,
269
+ 'file_path': file_path,
270
+ 'file_hash': file_hash,
271
+ 'chunk_count': len(chunks),
272
+ 'total_text_length': len(text),
273
+ 'processing_date': datetime.datetime.now().isoformat()
274
+ }
275
+
276
+ return {
277
+ 'text': text,
278
+ 'chunks': chunks,
279
+ 'metadata': metadata
280
+ }
281
+
282
+ def _calculate_file_hash(self, file_path):
283
+ """
284
+ Calcola l'hash SHA-256 di un file.
285
+
286
+ Args:
287
+ file_path (str): Percorso del file
288
+
289
+ Returns:
290
+ str: Hash SHA-256 del file
291
+ """
292
+ sha256_hash = hashlib.sha256()
293
+
294
+ with open(file_path, "rb") as f:
295
+ # Leggi il file a blocchi per gestire file di grandi dimensioni
296
+ for byte_block in iter(lambda: f.read(4096), b""):
297
+ sha256_hash.update(byte_block)
298
+
299
+ return sha256_hash.hexdigest()
300
+
301
+
302
+ class VectorStore:
303
+ """
304
+ Classe per la gestione del vector store per il sistema RAG.
305
+ """
306
+
307
+ def __init__(self, persist_directory, embedding_model_name="all-MiniLM-L6-v2"):
308
+ """
309
+ Inizializza il vector store.
310
+
311
+ Args:
312
+ persist_directory (str): Directory dove salvare il vector store
313
+ embedding_model_name (str): Nome del modello di embedding
314
+ """
315
+ self.persist_directory = persist_directory
316
+ self.embedding_model_name = embedding_model_name
317
+
318
+ # Crea la directory se non esiste
319
+ os.makedirs(persist_directory, exist_ok=True)
320
+
321
+ # Inizializza il modello di embedding
322
+ self.embedding_model = self._initialize_embedding_model(embedding_model_name)
323
+
324
+ # Inizializza il vector store
325
+ self.vector_store = self._initialize_vector_store()
326
+
327
+ def _initialize_embedding_model(self, model_name):
328
+ """
329
+ Inizializza il modello di embedding.
330
+
331
+ Args:
332
+ model_name (str): Nome del modello
333
+
334
+ Returns:
335
+ object: Modello di embedding
336
+ """
337
+ try:
338
+ # Utilizza HuggingFaceEmbeddings di LangChain
339
+ embedding_model = HuggingFaceEmbeddings(model_name=model_name)
340
+ return embedding_model
341
+ except Exception as e:
342
+ print(f"Errore nell'inizializzazione del modello di embedding: {e}")
343
+ # Fallback: carica direttamente il modello con sentence-transformers
344
+ return SentenceTransformer(model_name)
345
+
346
+ def _initialize_vector_store(self):
347
+ """
348
+ Inizializza il vector store.
349
+
350
+ Returns:
351
+ object: Vector store
352
+ """
353
+ try:
354
+ # Controlla se esiste già un vector store
355
+ if os.path.exists(os.path.join(self.persist_directory, 'chroma.sqlite3')):
356
+ # Carica il vector store esistente
357
+ vector_store = Chroma(
358
+ persist_directory=self.persist_directory,
359
+ embedding_function=self.embedding_model
360
+ )
361
+ else:
362
+ # Crea un nuovo vector store
363
+ vector_store = Chroma(
364
+ persist_directory=self.persist_directory,
365
+ embedding_function=self.embedding_model
366
+ )
367
+
368
+ return vector_store
369
+ except Exception as e:
370
+ print(f"Errore nell'inizializzazione del vector store: {e}")
371
+ # Fallback: utilizza direttamente ChromaDB
372
+ client = chromadb.PersistentClient(path=self.persist_directory)
373
+ collection_name = "forensic_graphology_docs"
374
+
375
+ # Controlla se la collezione esiste già
376
+ try:
377
+ collection = client.get_collection(name=collection_name)
378
+ except:
379
+ # Crea una nuova collezione
380
+ collection = client.create_collection(name=collection_name)
381
+
382
+ return collection
383
+
384
+ def add_document(self, document_info):
385
+ """
386
+ Aggiunge un documento al vector store.
387
+
388
+ Args:
389
+ document_info (dict): Informazioni sul documento
390
+
391
+ Returns:
392
+ dict: Risultato dell'operazione
393
+ """
394
+ chunks = document_info['chunks']
395
+ metadata = document_info['metadata']
396
+
397
+ # Crea documenti LangChain
398
+ documents = []
399
+ for i, chunk in enumerate(chunks):
400
+ # Crea metadati per il chunk
401
+ chunk_metadata = metadata.copy()
402
+ chunk_metadata['chunk_id'] = i
403
+ chunk_metadata['chunk_index'] = i
404
+ chunk_metadata['chunk_total'] = len(chunks)
405
+
406
+ # Crea un documento LangChain
407
+ doc = Document(page_content=chunk, metadata=chunk_metadata)
408
+ documents.append(doc)
409
+
410
+ try:
411
+ # Aggiungi i documenti al vector store
412
+ self.vector_store.add_documents(documents)
413
+
414
+ return {
415
+ 'success': True,
416
+ 'document_id': metadata['file_hash'],
417
+ 'chunks_added': len(chunks)
418
+ }
419
+ except Exception as e:
420
+ print(f"Errore nell'aggiunta del documento al vector store: {e}")
421
+ return {
422
+ 'success': False,
423
+ 'error': str(e)
424
+ }
425
+
426
+ def search(self, query, k=4):
427
+ """
428
+ Cerca documenti simili alla query.
429
+
430
+ Args:
431
+ query (str): Query di ricerca
432
+ k (int): Numero di risultati da restituire
433
+
434
+ Returns:
435
+ list: Lista di documenti simili
436
+ """
437
+ try:
438
+ # Cerca documenti simili
439
+ results = self.vector_store.similarity_search(query, k=k)
440
+ return results
441
+ except Exception as e:
442
+ print(f"Errore nella ricerca: {e}")
443
+ return []
444
+
445
+ def delete_document(self, document_id):
446
+ """
447
+ Elimina un documento dal vector store.
448
+
449
+ Args:
450
+ document_id (str): ID del documento
451
+
452
+ Returns:
453
+ dict: Risultato dell'operazione
454
+ """
455
+ try:
456
+ # Elimina il documento
457
+ self.vector_store.delete(filter={"file_hash": document_id})
458
+
459
+ return {
460
+ 'success': True,
461
+ 'document_id': document_id
462
+ }
463
+ except Exception as e:
464
+ print(f"Errore nell'eliminazione del documento: {e}")
465
+ return {
466
+ 'success': False,
467
+ 'error': str(e)
468
+ }
469
+
470
+ def get_all_documents(self):
471
+ """
472
+ Ottiene tutti i documenti nel vector store.
473
+
474
+ Returns:
475
+ list: Lista di documenti
476
+ """
477
+ try:
478
+ # Ottieni tutti i documenti
479
+ results = self.vector_store.get()
480
+
481
+ # Estrai i metadati unici
482
+ unique_docs = {}
483
+ for i, metadata in enumerate(results['metadatas']):
484
+ if 'file_hash' in metadata:
485
+ file_hash = metadata['file_hash']
486
+ if file_hash not in unique_docs:
487
+ unique_docs[file_hash] = {
488
+ 'document_id': file_hash,
489
+ 'filename': metadata.get('filename', 'Unknown'),
490
+ 'file_path': metadata.get('file_path', ''),
491
+ 'chunk_total': metadata.get('chunk_total', 0),
492
+ 'processing_date': metadata.get('processing_date', '')
493
+ }
494
+
495
+ return list(unique_docs.values())
496
+ except Exception as e:
497
+ print(f"Errore nel recupero dei documenti: {e}")
498
+ return []
499
+
500
+
501
+ class RAGSystem:
502
+ """
503
+ Classe per il sistema RAG (Retrieval Augmented Generation).
504
+ """
505
+
506
+ def __init__(self, upload_dir, vector_store_dir, use_local_model=False, model_name=None):
507
+ """
508
+ Inizializza il sistema RAG.
509
+
510
+ Args:
511
+ upload_dir (str): Directory per i documenti caricati
512
+ vector_store_dir (str): Directory per il vector store
513
+ use_local_model (bool): Se utilizzare un modello locale
514
+ model_name (str): Nome del modello da utilizzare
515
+ """
516
+ self.document_processor = DocumentProcessor(upload_dir)
517
+ self.vector_store = VectorStore(vector_store_dir)
518
+ self.use_local_model = use_local_model
519
+ self.model_name = model_name
520
+
521
+ # Inizializza il modello come None (modalità senza LLM)
522
+ self.model = None
523
+
524
+ # Prova a inizializzare il modello solo se specificato
525
+ if model_name:
526
+ try:
527
+ self._initialize_model(use_local_model, model_name)
528
+ except Exception as e:
529
+ print(f"Errore nell'inizializzazione del modello: {e}")
530
+ print("Il sistema RAG funzionerà in modalità di sola ricerca (senza generazione).")
531
+
532
+ def _initialize_model(self, use_local_model, model_name):
533
+ """
534
+ Inizializza il modello di linguaggio.
535
+
536
+ Args:
537
+ use_local_model (bool): Se utilizzare un modello locale
538
+ model_name (str): Nome del modello
539
+
540
+ Returns:
541
+ object: Modello di linguaggio
542
+ """
543
+ # In questa versione semplificata, non inizializziamo alcun modello
544
+ # per evitare problemi di dipendenze e token API
545
+ print("Modalità di sola ricerca attivata (senza generazione).")
546
+ return None
547
+
548
+ def process_and_store_document(self, file_obj, filename=None):
549
+ """
550
+ Elabora e memorizza un documento.
551
+
552
+ Args:
553
+ file_obj: Oggetto file caricato
554
+ filename (str, optional): Nome del file
555
+
556
+ Returns:
557
+ dict: Risultato dell'operazione
558
+ """
559
+ try:
560
+ # Salva il file caricato
561
+ file_path = self.document_processor.save_uploaded_file(file_obj, filename)
562
+
563
+ # Elabora il documento
564
+ document_info = self.document_processor.process_document(file_path)
565
+
566
+ # Aggiungi il documento al vector store
567
+ result = self.vector_store.add_document(document_info)
568
+
569
+ # Aggiungi informazioni aggiuntive al risultato
570
+ result['filename'] = os.path.basename(file_path)
571
+ result['file_path'] = file_path
572
+ result['chunk_count'] = len(document_info['chunks'])
573
+
574
+ return result
575
+ except Exception as e:
576
+ print(f"Errore nell'elaborazione e memorizzazione del documento: {e}")
577
+ return {
578
+ 'success': False,
579
+ 'error': str(e)
580
+ }
581
+
582
+ def query(self, query_text, k=4, scrub_sensitive=True):
583
+ """
584
+ Esegue una query sul sistema RAG.
585
+
586
+ Args:
587
+ query_text (str): Testo della query
588
+ k (int): Numero di documenti da recuperare
589
+ scrub_sensitive (bool): Se rimuovere informazioni sensibili
590
+
591
+ Returns:
592
+ dict: Risultato della query
593
+ """
594
+ try:
595
+ # Cerca documenti simili
596
+ retrieved_docs = self.vector_store.search(query_text, k=k)
597
+
598
+ # Estrai il contesto dai documenti
599
+ context = self._build_context(retrieved_docs, scrub_sensitive)
600
+
601
+ # Prepara i riferimenti
602
+ references = self._prepare_references(retrieved_docs)
603
+
604
+ # Se non c'è un modello, restituisci solo i documenti recuperati
605
+ if self.model is None:
606
+ response = "Modalità di sola ricerca attiva. Ecco i documenti più rilevanti per la tua query:\n\n"
607
+ for i, doc in enumerate(retrieved_docs):
608
+ response += f"[Documento {i+1}] {doc.metadata.get('filename', 'Unknown')}\n"
609
+ response += f"Estratto: {doc.page_content[:200]}...\n\n"
610
+ else:
611
+ # Crea il prompt
612
+ prompt = self._create_prompt(query_text, context)
613
+
614
+ # Genera la risposta
615
+ response = self._generate_response(prompt)
616
+
617
+ return {
618
+ 'success': True,
619
+ 'query': query_text,
620
+ 'response': response,
621
+ 'references': references
622
+ }
623
+ except Exception as e:
624
+ print(f"Errore nell'esecuzione della query: {e}")
625
+ return {
626
+ 'success': False,
627
+ 'error': str(e),
628
+ 'query': query_text
629
+ }
630
+
631
+ def _build_context(self, documents, scrub_sensitive=True):
632
+ """
633
+ Costruisce il contesto dai documenti recuperati.
634
+
635
+ Args:
636
+ documents (list): Lista di documenti
637
+ scrub_sensitive (bool): Se rimuovere informazioni sensibili
638
+
639
+ Returns:
640
+ str: Contesto
641
+ """
642
+ context_parts = []
643
+
644
+ for i, doc in enumerate(documents):
645
+ # Estrai il contenuto e i metadati
646
+ content = doc.page_content
647
+ metadata = doc.metadata
648
+
649
+ # Rimuovi informazioni sensibili se richiesto
650
+ if scrub_sensitive:
651
+ content = self._scrub_sensitive_info(content)
652
+
653
+ # Aggiungi il contenuto al contesto
654
+ context_parts.append(f"[Documento {i+1}] {content}")
655
+
656
+ # Unisci le parti del contesto
657
+ context = "\n\n".join(context_parts)
658
+
659
+ return context
660
+
661
+ def _scrub_sensitive_info(self, text):
662
+ """
663
+ Rimuove informazioni sensibili dal testo.
664
+
665
+ Args:
666
+ text (str): Testo da elaborare
667
+
668
+ Returns:
669
+ str: Testo elaborato
670
+ """
671
+ # Rimuovi numeri di telefono
672
+ text = re.sub(r'\b\d{10}\b', '[TELEFONO]', text)
673
+ text = re.sub(r'\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b', '[TELEFONO]', text)
674
+
675
+ # Rimuovi indirizzi email
676
+ text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text)
677
+
678
+ # Rimuovi codici fiscali italiani
679
+ text = re.sub(r'\b[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]\b', '[CODICE_FISCALE]', text)
680
+
681
+ # Rimuovi numeri di carte di credito
682
+ text = re.sub(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', '[CARTA_DI_CREDITO]', text)
683
+
684
+ # Rimuovi IBAN
685
+ text = re.sub(r'\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}[A-Z0-9]{0,16}\b', '[IBAN]', text)
686
+
687
+ return text
688
+
689
+ def _create_prompt(self, query, context):
690
+ """
691
+ Crea il prompt per il modello.
692
+
693
+ Args:
694
+ query (str): Query dell'utente
695
+ context (str): Contesto dai documenti
696
+
697
+ Returns:
698
+ str: Prompt
699
+ """
700
+ prompt_template = """
701
+ Sei un consulente di Grafologia Forense. Ti fornisco del contesto da documenti caricati.
702
+ Rispondi in modo coerente e professionale, senza rivelare mai dati privati.
703
+
704
+ CONTENUTO RILEVANTE:
705
+ {context}
706
+
707
+ DOMANDA: {query}
708
+
709
+ RISPOSTA:
710
+ """
711
+
712
+ # Crea il prompt
713
+ prompt = PromptTemplate(
714
+ template=prompt_template,
715
+ input_variables=["context", "query"]
716
+ )
717
+
718
+ # Formatta il prompt
719
+ formatted_prompt = prompt.format(context=context, query=query)
720
+
721
+ return formatted_prompt
722
+
723
+ def _generate_response(self, prompt):
724
+ """
725
+ Genera una risposta dal modello.
726
+
727
+ Args:
728
+ prompt (str): Prompt per il modello
729
+
730
+ Returns:
731
+ str: Risposta generata
732
+ """
733
+ if self.model is None:
734
+ return "Mi dispiace, il modello di linguaggio non è disponibile al momento."
735
+
736
+ try:
737
+ # Crea una chain
738
+ chain = LLMChain(llm=self.model, prompt=PromptTemplate.from_template(prompt))
739
+
740
+ # Genera la risposta
741
+ response = chain.run("")
742
+
743
+ return response
744
+ except Exception as e:
745
+ print(f"Errore nella generazione della risposta: {e}")
746
+
747
+ # Fallback: risposta semplice
748
+ return "Mi dispiace, non sono riuscito a generare una risposta. Si è verificato un errore."
749
+
750
+ def _prepare_references(self, documents):
751
+ """
752
+ Prepara i riferimenti ai documenti.
753
+
754
+ Args:
755
+ documents (list): Lista di documenti
756
+
757
+ Returns:
758
+ list: Lista di riferimenti
759
+ """
760
+ references = []
761
+
762
+ for i, doc in enumerate(documents):
763
+ # Estrai i metadati
764
+ metadata = doc.metadata
765
+
766
+ # Crea un riferimento
767
+ reference = {
768
+ 'id': i + 1,
769
+ 'filename': metadata.get('filename', 'Unknown'),
770
+ 'chunk_id': metadata.get('chunk_id', 0),
771
+ 'chunk_index': metadata.get('chunk_index', 0),
772
+ 'chunk_total': metadata.get('chunk_total', 0),
773
+ 'snippet': doc.page_content[:100] + "..." if len(doc.page_content) > 100 else doc.page_content
774
+ }
775
+
776
+ references.append(reference)
777
+
778
+ return references
779
+
780
+ def get_document_list(self):
781
+ """
782
+ Ottiene la lista dei documenti memorizzati.
783
+
784
+ Returns:
785
+ list: Lista di documenti
786
+ """
787
+ return self.vector_store.get_all_documents()
788
+
789
+ def delete_document(self, document_id):
790
+ """
791
+ Elimina un documento.
792
+
793
+ Args:
794
+ document_id (str): ID del documento
795
+
796
+ Returns:
797
+ dict: Risultato dell'operazione
798
+ """
799
+ return self.vector_store.delete_document(document_id)
src/signature_analysis.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+ import matplotlib.pyplot as plt
5
+ from .preprocessing import ImagePreprocessor
6
+
7
+ class SignatureAnalyzer:
8
+ """
9
+ Classe per l'analisi e la comparazione di firme.
10
+ Implementa funzionalità per estrarre caratteristiche dalle firme,
11
+ confrontarle e calcolare metriche di similarità.
12
+ """
13
+
14
+ def __init__(self):
15
+ """Inizializza l'analizzatore di firme."""
16
+ self.preprocessor = ImagePreprocessor()
17
+
18
+ def extract_contours(self, binary_image):
19
+ """
20
+ Estrae i contorni da un'immagine binaria.
21
+
22
+ Args:
23
+ binary_image (numpy.ndarray): Immagine binaria
24
+
25
+ Returns:
26
+ list: Lista di contorni
27
+ """
28
+ contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
29
+ return contours
30
+
31
+ def extract_features_orb(self, image, n_features=1000):
32
+ """
33
+ Estrae caratteristiche ORB (Oriented FAST and Rotated BRIEF) da un'immagine.
34
+
35
+ Args:
36
+ image (numpy.ndarray): Immagine di input
37
+ n_features (int): Numero di caratteristiche da estrarre
38
+
39
+ Returns:
40
+ tuple: (keypoints, descriptors)
41
+ """
42
+ # Converti in scala di grigi se necessario
43
+ if len(image.shape) > 2:
44
+ gray = self.preprocessor.convert_to_grayscale(image)
45
+ else:
46
+ gray = image
47
+
48
+ # Inizializza il rilevatore ORB
49
+ orb = cv2.ORB_create(nfeatures=n_features)
50
+
51
+ # Rileva keypoints e calcola i descrittori
52
+ keypoints, descriptors = orb.detectAndCompute(gray, None)
53
+
54
+ return keypoints, descriptors
55
+
56
+ def match_features(self, desc1, desc2, method='bf'):
57
+ """
58
+ Confronta i descrittori di due immagini.
59
+
60
+ Args:
61
+ desc1 (numpy.ndarray): Descrittori della prima immagine
62
+ desc2 (numpy.ndarray): Descrittori della seconda immagine
63
+ method (str): Metodo di matching ('bf' per Brute Force, 'flann' per FLANN)
64
+
65
+ Returns:
66
+ list: Lista di corrispondenze
67
+ """
68
+ if desc1 is None or desc2 is None:
69
+ return []
70
+
71
+ if method == 'bf':
72
+ # Brute Force Matcher con norma di Hamming (per descrittori binari come ORB)
73
+ matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
74
+ matches = matcher.match(desc1, desc2)
75
+
76
+ # Ordina le corrispondenze in base alla distanza
77
+ matches = sorted(matches, key=lambda x: x.distance)
78
+
79
+ elif method == 'flann':
80
+ # FLANN Matcher (più veloce per dataset di grandi dimensioni)
81
+ # Converti i descrittori in float32 se necessario
82
+ if desc1.dtype != np.float32:
83
+ desc1 = np.float32(desc1)
84
+ if desc2.dtype != np.float32:
85
+ desc2 = np.float32(desc2)
86
+
87
+ FLANN_INDEX_LSH = 6
88
+ index_params = dict(algorithm=FLANN_INDEX_LSH,
89
+ table_number=6,
90
+ key_size=12,
91
+ multi_probe_level=1)
92
+ search_params = dict(checks=50)
93
+
94
+ flann = cv2.FlannBasedMatcher(index_params, search_params)
95
+ matches = flann.knnMatch(desc1, desc2, k=2)
96
+
97
+ # Applica il test del rapporto di Lowe
98
+ good_matches = []
99
+ for pair in matches:
100
+ if len(pair) == 2:
101
+ m, n = pair
102
+ if m.distance < 0.7 * n.distance:
103
+ good_matches.append(m)
104
+ matches = good_matches
105
+ else:
106
+ raise ValueError(f"Metodo di matching non supportato: {method}")
107
+
108
+ return matches
109
+
110
+ def calculate_similarity_score(self, matches, kp1, kp2):
111
+ """
112
+ Calcola un punteggio di similarità basato sulle corrispondenze.
113
+
114
+ Args:
115
+ matches (list): Lista di corrispondenze
116
+ kp1 (list): Keypoints della prima immagine
117
+ kp2 (list): Keypoints della seconda immagine
118
+
119
+ Returns:
120
+ float: Punteggio di similarità (0-100)
121
+ """
122
+ if len(matches) == 0 or len(kp1) == 0 or len(kp2) == 0:
123
+ return 0.0
124
+
125
+ # Calcola il punteggio come rapporto tra il numero di corrispondenze e il minimo numero di keypoints
126
+ score = 100.0 * len(matches) / min(len(kp1), len(kp2))
127
+
128
+ return min(score, 100.0) # Limita il punteggio a 100
129
+
130
+ def extract_signature_metrics(self, binary_image):
131
+ """
132
+ Estrae metriche grafometriche da una firma.
133
+
134
+ Args:
135
+ binary_image (numpy.ndarray): Immagine binaria della firma
136
+
137
+ Returns:
138
+ dict: Dizionario di metriche
139
+ """
140
+ # Estrai i contorni
141
+ contours = self.extract_contours(binary_image)
142
+
143
+ if not contours:
144
+ return {
145
+ 'area': 0,
146
+ 'perimeter': 0,
147
+ 'width': 0,
148
+ 'height': 0,
149
+ 'aspect_ratio': 0,
150
+ 'density': 0,
151
+ 'slant_angle': 0
152
+ }
153
+
154
+ # Trova il contorno più grande (la firma)
155
+ signature_contour = max(contours, key=cv2.contourArea)
156
+
157
+ # Calcola l'area
158
+ area = cv2.contourArea(signature_contour)
159
+
160
+ # Calcola il perimetro
161
+ perimeter = cv2.arcLength(signature_contour, True)
162
+
163
+ # Calcola il rettangolo delimitatore
164
+ x, y, w, h = cv2.boundingRect(signature_contour)
165
+
166
+ # Calcola il rapporto d'aspetto
167
+ aspect_ratio = float(w) / h if h > 0 else 0
168
+
169
+ # Calcola la densità (area / area del rettangolo delimitatore)
170
+ density = area / (w * h) if w * h > 0 else 0
171
+
172
+ # Calcola l'angolo di inclinazione
173
+ # Utilizziamo l'ellisse che meglio approssima il contorno
174
+ if len(signature_contour) >= 5: # Servono almeno 5 punti per adattare un'ellisse
175
+ ellipse = cv2.fitEllipse(signature_contour)
176
+ # L'angolo è in gradi, 0-180
177
+ slant_angle = ellipse[2]
178
+ # Normalizziamo l'angolo a -90 - +90 gradi
179
+ if slant_angle > 90:
180
+ slant_angle = slant_angle - 180
181
+ else:
182
+ slant_angle = 0
183
+
184
+ return {
185
+ 'area': area,
186
+ 'perimeter': perimeter,
187
+ 'width': w,
188
+ 'height': h,
189
+ 'aspect_ratio': aspect_ratio,
190
+ 'density': density,
191
+ 'slant_angle': slant_angle
192
+ }
193
+
194
+ def compare_signatures(self, image_path1, image_path2):
195
+ """
196
+ Confronta due firme e calcola metriche di similarità.
197
+
198
+ Args:
199
+ image_path1 (str): Percorso della prima immagine
200
+ image_path2 (str): Percorso della seconda immagine
201
+
202
+ Returns:
203
+ dict: Risultati del confronto
204
+ """
205
+ # Pre-elabora le firme
206
+ sig1_processed = self.preprocessor.preprocess_signature(image_path1)
207
+ sig2_processed = self.preprocessor.preprocess_signature(image_path2)
208
+
209
+ # Estrai caratteristiche ORB
210
+ kp1, desc1 = self.extract_features_orb(sig1_processed['binary'])
211
+ kp2, desc2 = self.extract_features_orb(sig2_processed['binary'])
212
+
213
+ # Trova le corrispondenze
214
+ matches = self.match_features(desc1, desc2, method='bf')
215
+
216
+ # Calcola il punteggio di similarità
217
+ similarity_score = self.calculate_similarity_score(matches, kp1, kp2)
218
+
219
+ # Estrai metriche grafometriche
220
+ metrics1 = self.extract_signature_metrics(sig1_processed['binary'])
221
+ metrics2 = self.extract_signature_metrics(sig2_processed['binary'])
222
+
223
+ # Calcola le differenze tra le metriche
224
+ metric_diffs = {
225
+ 'area_diff': abs(metrics1['area'] - metrics2['area']) / max(metrics1['area'], metrics2['area'], 1) * 100,
226
+ 'perimeter_diff': abs(metrics1['perimeter'] - metrics2['perimeter']) / max(metrics1['perimeter'], metrics2['perimeter'], 1) * 100,
227
+ 'aspect_ratio_diff': abs(metrics1['aspect_ratio'] - metrics2['aspect_ratio']) / max(metrics1['aspect_ratio'], metrics2['aspect_ratio'], 1) * 100,
228
+ 'density_diff': abs(metrics1['density'] - metrics2['density']) / max(metrics1['density'], metrics2['density'], 1) * 100,
229
+ 'slant_angle_diff': abs(metrics1['slant_angle'] - metrics2['slant_angle'])
230
+ }
231
+
232
+ # Calcola un punteggio di similarità basato sulle metriche
233
+ # Minore è la differenza, maggiore è la similarità
234
+ metric_similarity = 100 - (
235
+ 0.2 * metric_diffs['area_diff'] +
236
+ 0.2 * metric_diffs['perimeter_diff'] +
237
+ 0.2 * metric_diffs['aspect_ratio_diff'] +
238
+ 0.2 * metric_diffs['density_diff'] +
239
+ 0.2 * min(metric_diffs['slant_angle_diff'] / 90 * 100, 100) # Normalizza la differenza di angolo
240
+ )
241
+
242
+ # Combina i punteggi (50% feature matching, 50% metriche)
243
+ combined_score = 0.5 * similarity_score + 0.5 * metric_similarity
244
+
245
+ return {
246
+ 'feature_similarity': similarity_score,
247
+ 'metric_similarity': metric_similarity,
248
+ 'combined_score': combined_score,
249
+ 'metrics1': metrics1,
250
+ 'metrics2': metrics2,
251
+ 'metric_differences': metric_diffs,
252
+ 'keypoints1': len(kp1),
253
+ 'keypoints2': len(kp2),
254
+ 'matches': len(matches),
255
+ 'processed_images': {
256
+ 'signature1': sig1_processed,
257
+ 'signature2': sig2_processed
258
+ }
259
+ }
260
+
261
+ def visualize_comparison(self, comparison_result, save_path=None):
262
+ """
263
+ Visualizza il confronto tra due firme.
264
+
265
+ Args:
266
+ comparison_result (dict): Risultato del confronto
267
+ save_path (str, optional): Percorso dove salvare l'immagine
268
+
269
+ Returns:
270
+ matplotlib.figure.Figure: Figura con la visualizzazione
271
+ """
272
+ # Crea una figura con più sottografici
273
+ fig, axs = plt.subplots(2, 3, figsize=(15, 10))
274
+
275
+ # Immagini originali
276
+ axs[0, 0].imshow(cv2.cvtColor(comparison_result['processed_images']['signature1']['original'], cv2.COLOR_BGR2RGB))
277
+ axs[0, 0].set_title('Firma 1 (Originale)')
278
+ axs[0, 0].axis('off')
279
+
280
+ axs[0, 1].imshow(cv2.cvtColor(comparison_result['processed_images']['signature2']['original'], cv2.COLOR_BGR2RGB))
281
+ axs[0, 1].set_title('Firma 2 (Originale)')
282
+ axs[0, 1].axis('off')
283
+
284
+ # Immagini binarie
285
+ axs[0, 2].imshow(comparison_result['processed_images']['signature1']['binary'], cmap='gray')
286
+ axs[0, 2].set_title('Firma 1 (Binaria)')
287
+ axs[0, 2].axis('off')
288
+
289
+ axs[1, 0].imshow(comparison_result['processed_images']['signature2']['binary'], cmap='gray')
290
+ axs[1, 0].set_title('Firma 2 (Binaria)')
291
+ axs[1, 0].axis('off')
292
+
293
+ # Grafico a barre per i punteggi di similarità
294
+ scores = ['Feature Similarity', 'Metric Similarity', 'Combined Score']
295
+ values = [comparison_result['feature_similarity'],
296
+ comparison_result['metric_similarity'],
297
+ comparison_result['combined_score']]
298
+
299
+ axs[1, 1].bar(scores, values, color=['blue', 'green', 'red'])
300
+ axs[1, 1].set_ylim(0, 100)
301
+ axs[1, 1].set_ylabel('Punteggio (%)')
302
+ axs[1, 1].set_title('Punteggi di Similarità')
303
+
304
+ # Tabella con le metriche
305
+ metrics_table = [
306
+ ['Metrica', 'Firma 1', 'Firma 2', 'Diff (%)'],
307
+ ['Area', f"{comparison_result['metrics1']['area']:.1f}", f"{comparison_result['metrics2']['area']:.1f}",
308
+ f"{comparison_result['metric_differences']['area_diff']:.1f}"],
309
+ ['Perimetro', f"{comparison_result['metrics1']['perimeter']:.1f}", f"{comparison_result['metrics2']['perimeter']:.1f}",
310
+ f"{comparison_result['metric_differences']['perimeter_diff']:.1f}"],
311
+ ['Rapporto Aspetto', f"{comparison_result['metrics1']['aspect_ratio']:.2f}", f"{comparison_result['metrics2']['aspect_ratio']:.2f}",
312
+ f"{comparison_result['metric_differences']['aspect_ratio_diff']:.1f}"],
313
+ ['Densità', f"{comparison_result['metrics1']['density']:.2f}", f"{comparison_result['metrics2']['density']:.2f}",
314
+ f"{comparison_result['metric_differences']['density_diff']:.1f}"],
315
+ ['Inclinazione (°)', f"{comparison_result['metrics1']['slant_angle']:.1f}", f"{comparison_result['metrics2']['slant_angle']:.1f}",
316
+ f"{comparison_result['metric_differences']['slant_angle_diff']:.1f}"]
317
+ ]
318
+
319
+ axs[1, 2].axis('tight')
320
+ axs[1, 2].axis('off')
321
+ table = axs[1, 2].table(cellText=metrics_table, loc='center', cellLoc='center')
322
+ table.auto_set_font_size(False)
323
+ table.set_fontsize(9)
324
+ table.scale(1, 1.5)
325
+
326
+ # Aggiungi un titolo generale
327
+ plt.suptitle(f"Analisi Comparativa delle Firme - Score: {comparison_result['combined_score']:.1f}%",
328
+ fontsize=16)
329
+
330
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
331
+
332
+ # Salva l'immagine se richiesto
333
+ if save_path:
334
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
335
+
336
+ return fig
337
+
338
+ def generate_comparison_report(self, comparison_result):
339
+ """
340
+ Genera un report testuale del confronto tra firme.
341
+
342
+ Args:
343
+ comparison_result (dict): Risultato del confronto
344
+
345
+ Returns:
346
+ str: Report testuale
347
+ """
348
+ report = []
349
+ report.append("REPORT DI ANALISI COMPARATIVA DELLE FIRME")
350
+ report.append("=" * 50)
351
+ report.append("")
352
+
353
+ # Punteggi di similarità
354
+ report.append("PUNTEGGI DI SIMILARITÀ:")
355
+ report.append(f"- Similarità delle caratteristiche: {comparison_result['feature_similarity']:.2f}%")
356
+ report.append(f"- Similarità delle metriche: {comparison_result['metric_similarity']:.2f}%")
357
+ report.append(f"- Punteggio combinato: {comparison_result['combined_score']:.2f}%")
358
+ report.append("")
359
+
360
+ # Interpretazione del punteggio
361
+ if comparison_result['combined_score'] >= 85:
362
+ interpretation = "ALTA probabilità che le firme provengano dalla stessa persona."
363
+ elif comparison_result['combined_score'] >= 70:
364
+ interpretation = "MEDIA-ALTA probabilità che le firme provengano dalla stessa persona."
365
+ elif comparison_result['combined_score'] >= 50:
366
+ interpretation = "MEDIA probabilità che le firme provengano dalla stessa persona."
367
+ elif comparison_result['combined_score'] >= 30:
368
+ interpretation = "BASSA probabilità che le firme provengano dalla stessa persona."
369
+ else:
370
+ interpretation = "MOLTO BASSA probabilità che le firme provengano dalla stessa persona."
371
+
372
+ report.append(f"INTERPRETAZIONE: {interpretation}")
373
+ report.append("")
374
+
375
+ # Dettagli tecnici
376
+ report.append("DETTAGLI TECNICI:")
377
+ report.append(f"- Punti chiave rilevati nella Firma 1: {comparison_result['keypoints1']}")
378
+ report.append(f"- Punti chiave rilevati nella Firma 2: {comparison_result['keypoints2']}")
379
+ report.append(f"- Corrispondenze trovate: {comparison_result['matches']}")
380
+ report.append("")
381
+
382
+ # Metriche grafometriche
383
+ report.append("METRICHE GRAFOMETRICHE:")
384
+ report.append(f"{'Metrica':<20} {'Firma 1':<15} {'Firma 2':<15} {'Differenza (%)':<15}")
385
+ report.append("-" * 65)
386
+
387
+ metrics = [
388
+ ('Area', comparison_result['metrics1']['area'], comparison_result['metrics2']['area'],
389
+ comparison_result['metric_differences']['area_diff']),
390
+ ('Perimetro', comparison_result['metrics1']['perimeter'], comparison_result['metrics2']['perimeter'],
391
+ comparison_result['metric_differences']['perimeter_diff']),
392
+ ('Larghezza', comparison_result['metrics1']['width'], comparison_result['metrics2']['width'],
393
+ abs(comparison_result['metrics1']['width'] - comparison_result['metrics2']['width']) /
394
+ max(comparison_result['metrics1']['width'], comparison_result['metrics2']['width'], 1) * 100),
395
+ ('Altezza', comparison_result['metrics1']['height'], comparison_result['metrics2']['height'],
396
+ abs(comparison_result['metrics1']['height'] - comparison_result['metrics2']['height']) /
397
+ max(comparison_result['metrics1']['height'], comparison_result['metrics2']['height'], 1) * 100),
398
+ ('Rapporto Aspetto', comparison_result['metrics1']['aspect_ratio'], comparison_result['metrics2']['aspect_ratio'],
399
+ comparison_result['metric_differences']['aspect_ratio_diff']),
400
+ ('Densità', comparison_result['metrics1']['density'], comparison_result['metrics2']['density'],
401
+ comparison_result['metric_differences']['density_diff']),
402
+ ('Inclinazione (°)', comparison_result['metrics1']['slant_angle'], comparison_result['metrics2']['slant_angle'],
403
+ comparison_result['metric_differences']['slant_angle_diff'])
404
+ ]
405
+
406
+ for name, val1, val2, diff in metrics:
407
+ report.append(f"{name:<20} {val1:<15.2f} {val2:<15.2f} {diff:<15.2f}")
408
+
409
+ report.append("")
410
+ report.append("NOTA: Questo report è generato automaticamente e deve essere interpretato da un esperto di grafologia forense.")
411
+
412
+ return "\n".join(report)