Spaces:
Sleeping
Sleeping
Fabio Antonini commited on
Commit ·
c7ccdd9
1
Parent(s): 41d92cf
First implementation
Browse files- README_forensic_graphology.md +33 -0
- app.py +807 -0
- docs/technical_docs.md +223 -0
- docs/user_guide.md +141 -0
- hf-space.yaml +3 -0
- requirements.txt +164 -0
- src/font_analysis.py +466 -0
- src/image_enhancer.py +511 -0
- src/measurement.py +633 -0
- src/ml_models.py +711 -0
- src/preprocessing.py +274 -0
- src/rag_system.py +799 -0
- src/signature_analysis.py +412 -0
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)
|