Spaces:
Running
Running
Fabio Antonini Claude Sonnet 4.6 commited on
Commit ·
4b50683
1
Parent(s): b59af0d
feat: replace ultralytics/YOLOv8 with Conditional DETR (Apache 2.0)
Browse files- core/signature.py: get_yolo() → get_detector() via transformers AutoImageProcessor +
AutoModelForObjectDetection (tech4humans/conditional-detr-50-signature-detector)
- requirements.txt: remove ultralytics>=8.0.0, albumentations>=1.3.0; add timm>=0.9.0
- core/pipeline.py, app/grapholab_demo.py: update UI strings YOLOv8 → Conditional DETR
- notebooks: new 04_signature_detection_detr.ipynb; archive old yolo notebook
- notebooks/01, 03: update references and links to Lab 04
Removes AGPL-3.0 dependency blocking commercial release.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app/grapholab_demo.py +2 -2
- core/pipeline.py +1 -1
- core/signature.py +60 -73
- notebooks/01_intro_forensic_graphology.ipynb +2 -50
- notebooks/03_signature_verification_siamese.ipynb +2 -14
- notebooks/04_signature_detection_detr.ipynb +384 -0
- notebooks/{04_signature_detection_yolo.ipynb → archive/04_signature_detection_yolo.ipynb} +55 -8
- requirements.txt +1 -6
app/grapholab_demo.py
CHANGED
|
@@ -414,7 +414,7 @@ with gr.Blocks() as pipeline_tab:
|
|
| 414 |
"Il sistema eseguirà in sequenza tutti e sei gli strumenti AI e produrrà un **referto forense integrato**.\n\n"
|
| 415 |
"| Step | Strumento | Input |\n"
|
| 416 |
"|------|-----------|-------|\n"
|
| 417 |
-
"| 1 | Rilevamento Firma (
|
| 418 |
"| 2 | Trascrizione HTR (EasyOCR) | Documento |\n"
|
| 419 |
"| 3 | Riconoscimento Entità — NER | Testo da Step 2 |\n"
|
| 420 |
"| 4 | Identificazione Scrittore | Documento |\n"
|
|
@@ -430,7 +430,7 @@ with gr.Blocks() as pipeline_tab:
|
|
| 430 |
pipe_btn = gr.Button("▶ Avvia Analisi Forense", variant="primary", size="lg")
|
| 431 |
|
| 432 |
with gr.Column(visible=False) as pipe_results:
|
| 433 |
-
gr.Markdown("### Step 1 — Rilevamento Firma (
|
| 434 |
with gr.Row():
|
| 435 |
out_s1_img = gr.Image(label="Documento annotato", type="numpy")
|
| 436 |
out_s1_txt = gr.Textbox(label="Riepilogo", lines=3)
|
|
|
|
| 414 |
"Il sistema eseguirà in sequenza tutti e sei gli strumenti AI e produrrà un **referto forense integrato**.\n\n"
|
| 415 |
"| Step | Strumento | Input |\n"
|
| 416 |
"|------|-----------|-------|\n"
|
| 417 |
+
"| 1 | Rilevamento Firma (Conditional DETR) | Documento |\n"
|
| 418 |
"| 2 | Trascrizione HTR (EasyOCR) | Documento |\n"
|
| 419 |
"| 3 | Riconoscimento Entità — NER | Testo da Step 2 |\n"
|
| 420 |
"| 4 | Identificazione Scrittore | Documento |\n"
|
|
|
|
| 430 |
pipe_btn = gr.Button("▶ Avvia Analisi Forense", variant="primary", size="lg")
|
| 431 |
|
| 432 |
with gr.Column(visible=False) as pipe_results:
|
| 433 |
+
gr.Markdown("### Step 1 — Rilevamento Firma (Conditional DETR)")
|
| 434 |
with gr.Row():
|
| 435 |
out_s1_img = gr.Image(label="Documento annotato", type="numpy")
|
| 436 |
out_s1_txt = gr.Textbox(label="Riepilogo", lines=3)
|
core/pipeline.py
CHANGED
|
@@ -304,7 +304,7 @@ def generate_forensic_pdf(results: PipelineResults) -> str:
|
|
| 304 |
pdf.image(buf, x=x, w=disp_w, h=disp_h)
|
| 305 |
pdf.ln(4)
|
| 306 |
|
| 307 |
-
_section_title("Step 1 — Rilevamento Firma (
|
| 308 |
_body_text(results.sig_detect_summary)
|
| 309 |
_embed_image(results.sig_detect_image)
|
| 310 |
|
|
|
|
| 304 |
pdf.image(buf, x=x, w=disp_w, h=disp_h)
|
| 305 |
pdf.ln(4)
|
| 306 |
|
| 307 |
+
_section_title("Step 1 — Rilevamento Firma (Conditional DETR)")
|
| 308 |
_body_text(results.sig_detect_summary)
|
| 309 |
_embed_image(results.sig_detect_image)
|
| 310 |
|
core/signature.py
CHANGED
|
@@ -3,10 +3,10 @@ GraphoLab core — Signature Verification and Detection.
|
|
| 3 |
|
| 4 |
Provides:
|
| 5 |
- get_signet() lazy loader for the SigNet model
|
| 6 |
-
-
|
| 7 |
- preprocess_signature() sigver-compatible preprocessing
|
| 8 |
- sig_verify() verify signature authenticity (SigNet)
|
| 9 |
-
- sig_detect() detect signature locations in a document (
|
| 10 |
- detect_and_crop() detect + return annotated image and first crop
|
| 11 |
"""
|
| 12 |
|
|
@@ -14,7 +14,6 @@ from __future__ import annotations
|
|
| 14 |
|
| 15 |
import io
|
| 16 |
import os
|
| 17 |
-
import tempfile
|
| 18 |
import threading
|
| 19 |
from collections import OrderedDict
|
| 20 |
from pathlib import Path
|
|
@@ -38,8 +37,7 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
|
| 38 |
SIGNET_CANVAS = (952, 1360)
|
| 39 |
SIG_THRESHOLD = 0.35
|
| 40 |
|
| 41 |
-
|
| 42 |
-
YOLO_FILENAME = "yolov8s.pt"
|
| 43 |
|
| 44 |
# ──────────────────────────────────────────────────────────────────────────────
|
| 45 |
# SigNet architecture
|
|
@@ -95,8 +93,9 @@ _signet = None
|
|
| 95 |
_signet_pretrained = False
|
| 96 |
_signet_lock = threading.Lock()
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
def get_signet(weights_path: Path):
|
|
@@ -117,21 +116,17 @@ def get_signet(weights_path: Path):
|
|
| 117 |
return _signet
|
| 118 |
|
| 119 |
|
| 120 |
-
def
|
| 121 |
-
"""Return the
|
| 122 |
-
global
|
| 123 |
-
if
|
| 124 |
-
with
|
| 125 |
-
if
|
| 126 |
-
from
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
repo_id=YOLO_REPO, filename=YOLO_FILENAME, token=hf_token
|
| 132 |
-
)
|
| 133 |
-
_yolo_model = YOLO(model_path)
|
| 134 |
-
return _yolo_model
|
| 135 |
|
| 136 |
|
| 137 |
# ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -288,7 +283,7 @@ def sig_detect(
|
|
| 288 |
image: np.ndarray,
|
| 289 |
conf_threshold: float,
|
| 290 |
) -> tuple[np.ndarray, str]:
|
| 291 |
-
"""Detect signature locations in a document image using
|
| 292 |
|
| 293 |
Args:
|
| 294 |
image: RGB numpy array of the document.
|
|
@@ -301,46 +296,39 @@ def sig_detect(
|
|
| 301 |
if image is None:
|
| 302 |
return image, "Carica un'immagine del documento."
|
| 303 |
try:
|
| 304 |
-
|
| 305 |
except Exception as e:
|
| 306 |
msg = (
|
| 307 |
"⚠️ **Modello non disponibile.**\n\n"
|
| 308 |
-
"
|
| 309 |
-
"**Per abilitare questa sezione:**\n"
|
| 310 |
-
"1. Crea un account su huggingface.co\n"
|
| 311 |
-
"2. Richiedi l'accesso su huggingface.co/tech4humans/yolov8s-signature-detector\n"
|
| 312 |
-
"3. Crea un token su huggingface.co/settings/tokens\n"
|
| 313 |
-
"4. Imposta la variabile d'ambiente `HF_TOKEN=<il_tuo_token>` prima di avviare l'app\n\n"
|
| 314 |
f"Errore: {e}"
|
| 315 |
)
|
| 316 |
return image, msg
|
| 317 |
|
| 318 |
pil_img = Image.fromarray(image).convert("RGB")
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
results =
|
| 324 |
-
|
|
|
|
| 325 |
|
| 326 |
-
result = results[0]
|
| 327 |
annotated = image.copy()
|
| 328 |
count = 0
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 338 |
-
count += 1
|
| 339 |
|
| 340 |
summary = (
|
| 341 |
f"Rilevat{'a' if count == 1 else 'e'} {count} firma{'' if count == 1 else 'e'} "
|
| 342 |
f"(confidenza ≥ {conf_threshold:.0%})\n\n"
|
| 343 |
-
f"**Modello:** `
|
| 344 |
f"**Uso forense:** Estrazione automatica di firme da documenti legali."
|
| 345 |
)
|
| 346 |
return annotated, summary
|
|
@@ -350,42 +338,41 @@ def detect_and_crop(
|
|
| 350 |
image: np.ndarray,
|
| 351 |
conf_threshold: float = 0.3,
|
| 352 |
) -> tuple[np.ndarray, np.ndarray | None, str]:
|
| 353 |
-
"""Run
|
| 354 |
|
| 355 |
-
Gracefully degrades when
|
| 356 |
"""
|
| 357 |
annotated = image.copy()
|
| 358 |
try:
|
| 359 |
-
|
| 360 |
except Exception:
|
| 361 |
-
return annotated, None, "⚠️ Rilevamento firma non disponibile
|
| 362 |
|
| 363 |
pil_img = Image.fromarray(image).convert("RGB")
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
results =
|
| 369 |
-
|
|
|
|
| 370 |
|
| 371 |
-
result = results[0]
|
| 372 |
first_crop: np.ndarray | None = None
|
| 373 |
count = 0
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
count += 1
|
| 389 |
|
| 390 |
summary = (
|
| 391 |
f"Rilevat{'a' if count == 1 else 'e'} {count} firma{'' if count == 1 else 'e'}."
|
|
|
|
| 3 |
|
| 4 |
Provides:
|
| 5 |
- get_signet() lazy loader for the SigNet model
|
| 6 |
+
- get_detector() lazy loader for the Conditional DETR signature detector
|
| 7 |
- preprocess_signature() sigver-compatible preprocessing
|
| 8 |
- sig_verify() verify signature authenticity (SigNet)
|
| 9 |
+
- sig_detect() detect signature locations in a document (Conditional DETR)
|
| 10 |
- detect_and_crop() detect + return annotated image and first crop
|
| 11 |
"""
|
| 12 |
|
|
|
|
| 14 |
|
| 15 |
import io
|
| 16 |
import os
|
|
|
|
| 17 |
import threading
|
| 18 |
from collections import OrderedDict
|
| 19 |
from pathlib import Path
|
|
|
|
| 37 |
SIGNET_CANVAS = (952, 1360)
|
| 38 |
SIG_THRESHOLD = 0.35
|
| 39 |
|
| 40 |
+
DETR_REPO = "tech4humans/conditional-detr-50-signature-detector"
|
|
|
|
| 41 |
|
| 42 |
# ──────────────────────────────────────────────────────────────────────────────
|
| 43 |
# SigNet architecture
|
|
|
|
| 93 |
_signet_pretrained = False
|
| 94 |
_signet_lock = threading.Lock()
|
| 95 |
|
| 96 |
+
_detector_processor = None
|
| 97 |
+
_detector_model = None
|
| 98 |
+
_detector_lock = threading.Lock()
|
| 99 |
|
| 100 |
|
| 101 |
def get_signet(weights_path: Path):
|
|
|
|
| 116 |
return _signet
|
| 117 |
|
| 118 |
|
| 119 |
+
def get_detector():
|
| 120 |
+
"""Return the Conditional DETR signature detector, downloading on first call (thread-safe)."""
|
| 121 |
+
global _detector_processor, _detector_model
|
| 122 |
+
if _detector_model is None:
|
| 123 |
+
with _detector_lock:
|
| 124 |
+
if _detector_model is None:
|
| 125 |
+
from transformers import AutoImageProcessor, AutoModelForObjectDetection
|
| 126 |
+
print("Loading Conditional DETR signature detector...")
|
| 127 |
+
_detector_processor = AutoImageProcessor.from_pretrained(DETR_REPO)
|
| 128 |
+
_detector_model = AutoModelForObjectDetection.from_pretrained(DETR_REPO).to(DEVICE).eval()
|
| 129 |
+
return _detector_processor, _detector_model
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
| 283 |
image: np.ndarray,
|
| 284 |
conf_threshold: float,
|
| 285 |
) -> tuple[np.ndarray, str]:
|
| 286 |
+
"""Detect signature locations in a document image using Conditional DETR.
|
| 287 |
|
| 288 |
Args:
|
| 289 |
image: RGB numpy array of the document.
|
|
|
|
| 296 |
if image is None:
|
| 297 |
return image, "Carica un'immagine del documento."
|
| 298 |
try:
|
| 299 |
+
processor, model = get_detector()
|
| 300 |
except Exception as e:
|
| 301 |
msg = (
|
| 302 |
"⚠️ **Modello non disponibile.**\n\n"
|
| 303 |
+
f"Impossibile caricare `{DETR_REPO}`.\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
f"Errore: {e}"
|
| 305 |
)
|
| 306 |
return image, msg
|
| 307 |
|
| 308 |
pil_img = Image.fromarray(image).convert("RGB")
|
| 309 |
+
inputs = processor(images=pil_img, return_tensors="pt").to(DEVICE)
|
| 310 |
+
with torch.no_grad():
|
| 311 |
+
outputs = model(**inputs)
|
| 312 |
+
target_sizes = torch.tensor([pil_img.size[::-1]])
|
| 313 |
+
results = processor.post_process_object_detection(
|
| 314 |
+
outputs, threshold=conf_threshold, target_sizes=target_sizes
|
| 315 |
+
)[0]
|
| 316 |
|
|
|
|
| 317 |
annotated = image.copy()
|
| 318 |
count = 0
|
| 319 |
+
for score, box in zip(results["scores"], results["boxes"]):
|
| 320 |
+
x1, y1, x2, y2 = box.cpu().numpy().astype(int)
|
| 321 |
+
conf = float(score.cpu())
|
| 322 |
+
cv2.rectangle(annotated, (x1, y1), (x2, y2), (255, 0, 0), 2)
|
| 323 |
+
cv2.putText(annotated, f"Sig #{count+1} {conf:.0%}",
|
| 324 |
+
(x1, max(y1 - 8, 0)),
|
| 325 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 326 |
+
count += 1
|
|
|
|
|
|
|
| 327 |
|
| 328 |
summary = (
|
| 329 |
f"Rilevat{'a' if count == 1 else 'e'} {count} firma{'' if count == 1 else 'e'} "
|
| 330 |
f"(confidenza ≥ {conf_threshold:.0%})\n\n"
|
| 331 |
+
f"**Modello:** `{DETR_REPO}`\n"
|
| 332 |
f"**Uso forense:** Estrazione automatica di firme da documenti legali."
|
| 333 |
)
|
| 334 |
return annotated, summary
|
|
|
|
| 338 |
image: np.ndarray,
|
| 339 |
conf_threshold: float = 0.3,
|
| 340 |
) -> tuple[np.ndarray, np.ndarray | None, str]:
|
| 341 |
+
"""Run Conditional DETR detection and return (annotated, first_crop, summary).
|
| 342 |
|
| 343 |
+
Gracefully degrades when the model is not available.
|
| 344 |
"""
|
| 345 |
annotated = image.copy()
|
| 346 |
try:
|
| 347 |
+
processor, model = get_detector()
|
| 348 |
except Exception:
|
| 349 |
+
return annotated, None, "⚠️ Rilevamento firma non disponibile."
|
| 350 |
|
| 351 |
pil_img = Image.fromarray(image).convert("RGB")
|
| 352 |
+
inputs = processor(images=pil_img, return_tensors="pt").to(DEVICE)
|
| 353 |
+
with torch.no_grad():
|
| 354 |
+
outputs = model(**inputs)
|
| 355 |
+
target_sizes = torch.tensor([pil_img.size[::-1]])
|
| 356 |
+
results = processor.post_process_object_detection(
|
| 357 |
+
outputs, threshold=conf_threshold, target_sizes=target_sizes
|
| 358 |
+
)[0]
|
| 359 |
|
|
|
|
| 360 |
first_crop: np.ndarray | None = None
|
| 361 |
count = 0
|
| 362 |
|
| 363 |
+
for score, box in zip(results["scores"], results["boxes"]):
|
| 364 |
+
x1, y1, x2, y2 = box.cpu().numpy().astype(int)
|
| 365 |
+
conf = float(score.cpu())
|
| 366 |
+
cv2.rectangle(annotated, (x1, y1), (x2, y2), (255, 0, 0), 2)
|
| 367 |
+
cv2.putText(annotated, f"Sig #{count+1} {conf:.0%}",
|
| 368 |
+
(x1, max(y1 - 8, 0)),
|
| 369 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 370 |
+
if count == 0:
|
| 371 |
+
x1c = max(0, x1); y1c = max(0, y1)
|
| 372 |
+
x2c = min(image.shape[1], x2); y2c = min(image.shape[0], y2)
|
| 373 |
+
if x2c > x1c and y2c > y1c:
|
| 374 |
+
first_crop = image[y1c:y2c, x1c:x2c]
|
| 375 |
+
count += 1
|
|
|
|
| 376 |
|
| 377 |
summary = (
|
| 378 |
f"Rilevat{'a' if count == 1 else 'e'} {count} firma{'' if count == 1 else 'e'}."
|
notebooks/01_intro_forensic_graphology.ipynb
CHANGED
|
@@ -83,55 +83,7 @@
|
|
| 83 |
{
|
| 84 |
"cell_type": "markdown",
|
| 85 |
"metadata": {},
|
| 86 |
-
"source":
|
| 87 |
-
"## Overview of the GraphoLab Labs\n",
|
| 88 |
-
"\n",
|
| 89 |
-
"| Lab | Title | AI Technique | Key Libraries |\n",
|
| 90 |
-
"|-----|-------|-------------|---------------|\n",
|
| 91 |
-
"| 02 | Handwritten Text Recognition | TrOCR (Transformer) | `transformers`, `torch` |\n",
|
| 92 |
-
"| 03 | Signature Verification | Siamese Neural Network | `torch`, `torchvision` |\n",
|
| 93 |
-
"| 04 | Signature Detection | YOLOv8 Object Detection | `ultralytics` |\n",
|
| 94 |
-
"| 05 | Writer Identification | CNN Features + Classifier | `torch`, `scikit-learn` |\n",
|
| 95 |
-
"| 06 | Graphological Analysis | OpenCV + Image Processing | `opencv-python`, `matplotlib` |\n",
|
| 96 |
-
"\n",
|
| 97 |
-
"---\n",
|
| 98 |
-
"\n",
|
| 99 |
-
"## Key Concepts\n",
|
| 100 |
-
"\n",
|
| 101 |
-
"### Handwritten Text Recognition (HTR)\n",
|
| 102 |
-
"HTR converts handwriting images into machine-readable text. Modern approaches use **sequence-to-sequence Transformers** (like TrOCR) that encode the image with a Vision Transformer and decode it token-by-token with a language model.\n",
|
| 103 |
-
"\n",
|
| 104 |
-
"### Signature Verification\n",
|
| 105 |
-
"A **Siamese network** takes two images and learns whether they are from the same class (genuine vs. forged). It does this by mapping both images into an embedding space and measuring their distance — close = same author, far = different/forged.\n",
|
| 106 |
-
"\n",
|
| 107 |
-
"### Writer Identification\n",
|
| 108 |
-
"Like fingerprints, handwriting has unique style characteristics. A classifier trained on enough samples can distinguish between writers even on short text fragments.\n",
|
| 109 |
-
"\n",
|
| 110 |
-
"### Graphological Feature Extraction\n",
|
| 111 |
-
"Classical computer vision (OpenCV) can objectively measure graphological traits:\n",
|
| 112 |
-
"- **Slant**: angle of letter strokes relative to vertical\n",
|
| 113 |
-
"- **Pressure**: mean pixel intensity in stroke regions\n",
|
| 114 |
-
"- **Spacing**: distribution of whitespace between words and letters\n",
|
| 115 |
-
"- **Size**: height/width statistics of letter forms\n",
|
| 116 |
-
"\n",
|
| 117 |
-
"---\n",
|
| 118 |
-
"\n",
|
| 119 |
-
"## Legal and Ethical Notes\n",
|
| 120 |
-
"\n",
|
| 121 |
-
"- AI-generated analysis must be reviewed and validated by a qualified forensic document examiner before use in court.\n",
|
| 122 |
-
"- Model outputs are probabilistic and carry uncertainty — always report confidence scores alongside verdicts.\n",
|
| 123 |
-
"- Training data biases can affect performance on under-represented handwriting styles, ages, or languages.\n",
|
| 124 |
-
"- These tools are for **augmenting** expert analysis, not replacing it.\n",
|
| 125 |
-
"\n",
|
| 126 |
-
"---\n",
|
| 127 |
-
"\n",
|
| 128 |
-
"## References\n",
|
| 129 |
-
"\n",
|
| 130 |
-
"- Li, M. et al. (2021). *TrOCR: Transformer-based Optical Character Recognition with Pre-trained Models.* arXiv:2109.10282\n",
|
| 131 |
-
"- Dey, S. et al. (2017). *SigNet: Convolutional Siamese Network for Writer Independent Offline Signature Verification.* arXiv:1707.02131\n",
|
| 132 |
-
"- Hafemann, L. G. et al. (2017). *Learning Features for Offline Handwritten Signature Verification using Deep Convolutional Neural Networks.* Pattern Recognition.\n",
|
| 133 |
-
"- Marti, U.-V. & Bunke, H. (2002). *The IAM-database: an English sentence database for offline handwriting recognition.* IJDAR.\n"
|
| 134 |
-
]
|
| 135 |
}
|
| 136 |
],
|
| 137 |
"metadata": {
|
|
@@ -147,4 +99,4 @@
|
|
| 147 |
},
|
| 148 |
"nbformat": 4,
|
| 149 |
"nbformat_minor": 5
|
| 150 |
-
}
|
|
|
|
| 83 |
{
|
| 84 |
"cell_type": "markdown",
|
| 85 |
"metadata": {},
|
| 86 |
+
"source": "## Overview of the GraphoLab Labs\n\n| Lab | Title | AI Technique | Key Libraries |\n|-----|-------|-------------|---------------|\n| 02 | Handwritten Text Recognition | TrOCR (Transformer) | `transformers`, `torch` |\n| 03 | Signature Verification | Siamese Neural Network | `torch`, `torchvision` |\n| 04 | Signature Detection | Conditional DETR (Transformer) | `transformers` |\n| 05 | Writer Identification | CNN Features + Classifier | `torch`, `scikit-learn` |\n| 06 | Graphological Analysis | OpenCV + Image Processing | `opencv-python`, `matplotlib` |\n\n---\n\n## Key Concepts\n\n### Handwritten Text Recognition (HTR)\nHTR converts handwriting images into machine-readable text. Modern approaches use **sequence-to-sequence Transformers** (like TrOCR) that encode the image with a Vision Transformer and decode it token-by-token with a language model.\n\n### Signature Verification\nA **Siamese network** takes two images and learns whether they are from the same class (genuine vs. forged). It does this by mapping both images into an embedding space and measuring their distance — close = same author, far = different/forged.\n\n### Writer Identification\nLike fingerprints, handwriting has unique style characteristics. A classifier trained on enough samples can distinguish between writers even on short text fragments.\n\n### Graphological Feature Extraction\nClassical computer vision (OpenCV) can objectively measure graphological traits:\n- **Slant**: angle of letter strokes relative to vertical\n- **Pressure**: mean pixel intensity in stroke regions\n- **Spacing**: distribution of whitespace between words and letters\n- **Size**: height/width statistics of letter forms\n\n---\n\n## Legal and Ethical Notes\n\n- AI-generated analysis must be reviewed and validated by a qualified forensic document examiner before use in court.\n- Model outputs are probabilistic and carry uncertainty — always report confidence scores alongside verdicts.\n- Training data biases can affect performance on under-represented handwriting styles, ages, or languages.\n- These tools are for **augmenting** expert analysis, not replacing it.\n\n---\n\n## References\n\n- Li, M. et al. (2021). *TrOCR: Transformer-based Optical Character Recognition with Pre-trained Models.* arXiv:2109.10282\n- Dey, S. et al. (2017). *SigNet: Convolutional Siamese Network for Writer Independent Offline Signature Verification.* arXiv:1707.02131\n- Hafemann, L. G. et al. (2017). *Learning Features for Offline Handwritten Signature Verification using Deep Convolutional Neural Networks.* Pattern Recognition.\n- Marti, U.-V. & Bunke, H. (2002). *The IAM-database: an English sentence database for offline handwriting recognition.* IJDAR.\n- Meng, D. et al. (2021). *Conditional DETR for Fast Training Convergence.* ICCV 2021."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
],
|
| 89 |
"metadata": {
|
|
|
|
| 99 |
},
|
| 100 |
"nbformat": 4,
|
| 101 |
"nbformat_minor": 5
|
| 102 |
+
}
|
notebooks/03_signature_verification_siamese.ipynb
CHANGED
|
@@ -494,19 +494,7 @@
|
|
| 494 |
"cell_type": "markdown",
|
| 495 |
"id": "fe74b3d8",
|
| 496 |
"metadata": {},
|
| 497 |
-
"source": [
|
| 498 |
-
"## Forensic Notes\n",
|
| 499 |
-
"\n",
|
| 500 |
-
"- **Threshold calibration:** The default threshold of 0.70 is a starting point. Calibrate it on a representative dataset of genuine and forged pairs for your specific use case.\n",
|
| 501 |
-
"- **Multiple reference samples:** Always compare against several genuine reference signatures (minimum 5–10), not just one.\n",
|
| 502 |
-
"- **Model limitations:** ResNet-18 ImageNet features are a general-purpose baseline. For production forensic use, domain-specific SigNet weights (trained on CEDAR or SigComp'11) provide significantly better performance — see [luizgh/sigver](https://github.com/luizgh/sigver).\n",
|
| 503 |
-
"- **Scan quality:** Use high-resolution scans (≥300 DPI) with consistent lighting and background.\n",
|
| 504 |
-
"- **AI is a screening tool:** A high similarity score supports — but does not prove — authenticity. Skilled forgeries may score high; unusual genuine signatures may score low.\n",
|
| 505 |
-
"\n",
|
| 506 |
-
"---\n",
|
| 507 |
-
"\n",
|
| 508 |
-
"**Next lab →** [04 — Signature Detection in Documents (YOLOv8)](04_signature_detection_yolo.ipynb)\n"
|
| 509 |
-
]
|
| 510 |
}
|
| 511 |
],
|
| 512 |
"metadata": {
|
|
@@ -530,4 +518,4 @@
|
|
| 530 |
},
|
| 531 |
"nbformat": 4,
|
| 532 |
"nbformat_minor": 5
|
| 533 |
-
}
|
|
|
|
| 494 |
"cell_type": "markdown",
|
| 495 |
"id": "fe74b3d8",
|
| 496 |
"metadata": {},
|
| 497 |
+
"source": "## Forensic Notes\n\n- **Threshold calibration:** The default threshold of 0.70 is a starting point. Calibrate it on a representative dataset of genuine and forged pairs for your specific use case.\n- **Multiple reference samples:** Always compare against several genuine reference signatures (minimum 5–10), not just one.\n- **Model limitations:** ResNet-18 ImageNet features are a general-purpose baseline. For production forensic use, domain-specific SigNet weights (trained on CEDAR or SigComp'11) provide significantly better performance — see [luizgh/sigver](https://github.com/luizgh/sigver).\n- **Scan quality:** Use high-resolution scans (≥300 DPI) with consistent lighting and background.\n- **AI is a screening tool:** A high similarity score supports — but does not prove — authenticity. Skilled forgeries may score high; unusual genuine signatures may score low.\n\n---\n\n**Next lab →** [04 — Signature Detection in Documents (Conditional DETR)](04_signature_detection_detr.ipynb)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
],
|
| 500 |
"metadata": {
|
|
|
|
| 518 |
},
|
| 519 |
"nbformat": 4,
|
| 520 |
"nbformat_minor": 5
|
| 521 |
+
}
|
notebooks/04_signature_detection_detr.ipynb
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.11.0"
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"cells": [
|
| 16 |
+
{
|
| 17 |
+
"cell_type": "markdown",
|
| 18 |
+
"id": "a1b2c3d4",
|
| 19 |
+
"metadata": {},
|
| 20 |
+
"source": [
|
| 21 |
+
"# Lab 04 — Signature Detection in Documents (Conditional DETR)\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"> **GraphoLab** | Forensic Graphology Laboratory\n",
|
| 24 |
+
"\n",
|
| 25 |
+
"**Model:** `tech4humans/conditional-detr-50-signature-detector` (Hugging Face / Apache 2.0) \n",
|
| 26 |
+
"**Task:** Locate and extract signatures from scanned documents \n",
|
| 27 |
+
"**Forensic use case:** Document pipeline — detect → extract → verify (feeds into Lab 03)\n",
|
| 28 |
+
"\n",
|
| 29 |
+
"---\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"## How Conditional DETR Object Detection Works\n",
|
| 32 |
+
"\n",
|
| 33 |
+
"DETR (DEtection TRansformer) reformulates object detection as a set-prediction problem using a Transformer encoder-decoder:\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"1. A **CNN backbone** (ResNet-50) extracts spatial feature maps from the image.\n",
|
| 36 |
+
"2. The **Transformer encoder** refines the feature maps with self-attention.\n",
|
| 37 |
+
"3. A fixed set of learned **object queries** are decoded by the Transformer decoder — each query \"attends\" to the relevant image region.\n",
|
| 38 |
+
"4. A feed-forward network predicts a bounding box and class label for each query.\n",
|
| 39 |
+
"\n",
|
| 40 |
+
"**Conditional DETR** (Meng et al., 2021) speeds up convergence vs. original DETR by conditioning cross-attention on predicted reference points, reducing the number of training epochs needed by ~10×.\n",
|
| 41 |
+
"\n",
|
| 42 |
+
"The model was fine-tuned on annotated signature images to detect signatures specifically in document scans, achieving **mAP@50 = 93.65%**.\n"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"cell_type": "markdown",
|
| 47 |
+
"id": "b2c3d4e5",
|
| 48 |
+
"metadata": {},
|
| 49 |
+
"source": [
|
| 50 |
+
"## GraphoLab Core — Quick Start\n",
|
| 51 |
+
"\n",
|
| 52 |
+
"> The production implementation of signature detection is available in [`core/signature.py`](../core/signature.py).\n",
|
| 53 |
+
"> It wraps **Conditional DETR** (`tech4humans/conditional-detr-50-signature-detector`) with lazy thread-safe model loading,\n",
|
| 54 |
+
"> bounding-box annotation, and automatic cropping of detected signatures.\n",
|
| 55 |
+
">\n",
|
| 56 |
+
"> Run the cell below to import it directly. The remaining cells implement the same detection\n",
|
| 57 |
+
"> pipeline from scratch for educational purposes."
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cell_type": "code",
|
| 62 |
+
"execution_count": null,
|
| 63 |
+
"id": "c3d4e5f6",
|
| 64 |
+
"metadata": {},
|
| 65 |
+
"outputs": [],
|
| 66 |
+
"source": [
|
| 67 |
+
"# GraphoLab Core — production usage\n",
|
| 68 |
+
"# Run this cell to use the shared core module instead of the notebook implementation below.\n",
|
| 69 |
+
"import sys, pathlib\n",
|
| 70 |
+
"sys.path.insert(0, str(pathlib.Path(\"..\").resolve()))\n",
|
| 71 |
+
"\n",
|
| 72 |
+
"from core.signature import detect_and_crop, sig_detect, get_detector\n",
|
| 73 |
+
"from PIL import Image\n",
|
| 74 |
+
"import numpy as np\n",
|
| 75 |
+
"\n",
|
| 76 |
+
"# Example: detect and crop signature from a document\n",
|
| 77 |
+
"# doc = np.array(Image.open(\"../data/samples/document_with_signature_01.png\").convert(\"RGB\"))\n",
|
| 78 |
+
"# annotated, crop, summary = detect_and_crop(doc)\n",
|
| 79 |
+
"# print(summary)\n",
|
| 80 |
+
"print(\"core.signature imported — detect_and_crop(), sig_detect() ready.\")"
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"cell_type": "markdown",
|
| 85 |
+
"id": "d4e5f6a7",
|
| 86 |
+
"metadata": {},
|
| 87 |
+
"source": [
|
| 88 |
+
"## Setup\n"
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"cell_type": "code",
|
| 93 |
+
"execution_count": null,
|
| 94 |
+
"id": "e5f6a7b8",
|
| 95 |
+
"metadata": {},
|
| 96 |
+
"outputs": [],
|
| 97 |
+
"source": [
|
| 98 |
+
"# !pip install transformers torch Pillow matplotlib opencv-python"
|
| 99 |
+
]
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"cell_type": "code",
|
| 103 |
+
"execution_count": null,
|
| 104 |
+
"id": "f6a7b8c9",
|
| 105 |
+
"metadata": {},
|
| 106 |
+
"outputs": [],
|
| 107 |
+
"source": [
|
| 108 |
+
"import warnings\n",
|
| 109 |
+
"warnings.filterwarnings('ignore')\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"from pathlib import Path\n",
|
| 112 |
+
"\n",
|
| 113 |
+
"import cv2\n",
|
| 114 |
+
"import numpy as np\n",
|
| 115 |
+
"import torch\n",
|
| 116 |
+
"from PIL import Image\n",
|
| 117 |
+
"import matplotlib.pyplot as plt\n",
|
| 118 |
+
"import matplotlib.patches as patches\n",
|
| 119 |
+
"\n",
|
| 120 |
+
"from transformers import AutoImageProcessor, AutoModelForObjectDetection\n",
|
| 121 |
+
"\n",
|
| 122 |
+
"DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
|
| 123 |
+
"print(f\"Libraries loaded. Device: {DEVICE}\")"
|
| 124 |
+
]
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"cell_type": "markdown",
|
| 128 |
+
"id": "a7b8c9d0",
|
| 129 |
+
"metadata": {},
|
| 130 |
+
"source": [
|
| 131 |
+
"## Load the Model\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"The model is downloaded from Hugging Face the first time and cached locally (~170 MB). \n",
|
| 134 |
+
"No token or access request required — Apache 2.0 licence, publicly available.\n"
|
| 135 |
+
]
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"cell_type": "code",
|
| 139 |
+
"execution_count": null,
|
| 140 |
+
"id": "b8c9d0e1",
|
| 141 |
+
"metadata": {},
|
| 142 |
+
"outputs": [],
|
| 143 |
+
"source": [
|
| 144 |
+
"DETR_REPO = \"tech4humans/conditional-detr-50-signature-detector\"\n",
|
| 145 |
+
"\n",
|
| 146 |
+
"print(\"Loading Conditional DETR signature detector from Hugging Face...\")\n",
|
| 147 |
+
"processor = AutoImageProcessor.from_pretrained(DETR_REPO)\n",
|
| 148 |
+
"model = AutoModelForObjectDetection.from_pretrained(DETR_REPO).to(DEVICE).eval()\n",
|
| 149 |
+
"print(\"Model ready.\")"
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"cell_type": "markdown",
|
| 154 |
+
"id": "c9d0e1f2",
|
| 155 |
+
"metadata": {},
|
| 156 |
+
"source": [
|
| 157 |
+
"## Helper Functions\n"
|
| 158 |
+
]
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"cell_type": "code",
|
| 162 |
+
"execution_count": null,
|
| 163 |
+
"id": "d0e1f2a3",
|
| 164 |
+
"metadata": {},
|
| 165 |
+
"outputs": [],
|
| 166 |
+
"source": [
|
| 167 |
+
"def detect_signatures(image: Image.Image | Path | str, conf_threshold: float = 0.3) -> dict:\n",
|
| 168 |
+
" \"\"\"Run signature detection on a PIL image or image path.\"\"\"\n",
|
| 169 |
+
" if not isinstance(image, Image.Image):\n",
|
| 170 |
+
" image = Image.open(image).convert(\"RGB\")\n",
|
| 171 |
+
" else:\n",
|
| 172 |
+
" image = image.convert(\"RGB\")\n",
|
| 173 |
+
"\n",
|
| 174 |
+
" inputs = processor(images=image, return_tensors=\"pt\").to(DEVICE)\n",
|
| 175 |
+
" with torch.no_grad():\n",
|
| 176 |
+
" outputs = model(**inputs)\n",
|
| 177 |
+
"\n",
|
| 178 |
+
" target_sizes = torch.tensor([image.size[::-1]])\n",
|
| 179 |
+
" results = processor.post_process_object_detection(\n",
|
| 180 |
+
" outputs, threshold=conf_threshold, target_sizes=target_sizes\n",
|
| 181 |
+
" )[0]\n",
|
| 182 |
+
"\n",
|
| 183 |
+
" detections = []\n",
|
| 184 |
+
" for score, box in zip(results[\"scores\"], results[\"boxes\"]):\n",
|
| 185 |
+
" x1, y1, x2, y2 = box.cpu().numpy().astype(int)\n",
|
| 186 |
+
" detections.append({\n",
|
| 187 |
+
" \"bbox\": (x1, y1, x2, y2),\n",
|
| 188 |
+
" \"confidence\": float(score.cpu()),\n",
|
| 189 |
+
" })\n",
|
| 190 |
+
"\n",
|
| 191 |
+
" return {\n",
|
| 192 |
+
" \"image\": image,\n",
|
| 193 |
+
" \"detections\": detections,\n",
|
| 194 |
+
" \"count\": len(detections),\n",
|
| 195 |
+
" }\n",
|
| 196 |
+
"\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"def show_detections(result: dict, title: str = \"Signature Detection\") -> None:\n",
|
| 199 |
+
" \"\"\"Visualise detected signatures with bounding boxes.\"\"\"\n",
|
| 200 |
+
" image = result[\"image\"]\n",
|
| 201 |
+
" detections = result[\"detections\"]\n",
|
| 202 |
+
"\n",
|
| 203 |
+
" fig, ax = plt.subplots(figsize=(12, 8))\n",
|
| 204 |
+
" ax.imshow(image)\n",
|
| 205 |
+
"\n",
|
| 206 |
+
" for i, det in enumerate(detections):\n",
|
| 207 |
+
" x1, y1, x2, y2 = det[\"bbox\"]\n",
|
| 208 |
+
" conf = det[\"confidence\"]\n",
|
| 209 |
+
" rect = patches.Rectangle(\n",
|
| 210 |
+
" (x1, y1), x2 - x1, y2 - y1,\n",
|
| 211 |
+
" linewidth=2, edgecolor='red', facecolor='none'\n",
|
| 212 |
+
" )\n",
|
| 213 |
+
" ax.add_patch(rect)\n",
|
| 214 |
+
" ax.text(x1, y1 - 6, f\"Signature #{i+1} {conf:.0%}\",\n",
|
| 215 |
+
" color='red', fontsize=9, fontweight='bold',\n",
|
| 216 |
+
" bbox=dict(facecolor='white', alpha=0.6, pad=1))\n",
|
| 217 |
+
"\n",
|
| 218 |
+
" ax.set_title(f\"{title} — {len(detections)} signature(s) found\", fontsize=13)\n",
|
| 219 |
+
" ax.axis('off')\n",
|
| 220 |
+
" plt.tight_layout()\n",
|
| 221 |
+
" plt.show()\n",
|
| 222 |
+
"\n",
|
| 223 |
+
"\n",
|
| 224 |
+
"def crop_signatures(result: dict, output_dir: Path) -> list:\n",
|
| 225 |
+
" \"\"\"Crop and save each detected signature as a separate image file.\"\"\"\n",
|
| 226 |
+
" output_dir = Path(output_dir)\n",
|
| 227 |
+
" output_dir.mkdir(parents=True, exist_ok=True)\n",
|
| 228 |
+
" image = result[\"image\"]\n",
|
| 229 |
+
" saved = []\n",
|
| 230 |
+
" for i, det in enumerate(result[\"detections\"]):\n",
|
| 231 |
+
" x1, y1, x2, y2 = det[\"bbox\"]\n",
|
| 232 |
+
" crop = image.crop((x1, y1, x2, y2))\n",
|
| 233 |
+
" out_path = output_dir / f\"detected_signature_{i+1:02d}.png\"\n",
|
| 234 |
+
" crop.save(out_path)\n",
|
| 235 |
+
" saved.append(out_path)\n",
|
| 236 |
+
" print(f\" Saved: {out_path}\")\n",
|
| 237 |
+
" return saved"
|
| 238 |
+
]
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"cell_type": "markdown",
|
| 242 |
+
"id": "e1f2a3b4",
|
| 243 |
+
"metadata": {},
|
| 244 |
+
"source": [
|
| 245 |
+
"## Demo 1 — Detect Signatures in a Sample Document\n"
|
| 246 |
+
]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"cell_type": "code",
|
| 250 |
+
"execution_count": null,
|
| 251 |
+
"id": "f2a3b4c5",
|
| 252 |
+
"metadata": {},
|
| 253 |
+
"outputs": [],
|
| 254 |
+
"source": [
|
| 255 |
+
"def make_synthetic_document() -> Path:\n",
|
| 256 |
+
" \"\"\"Create a simple synthetic document image with a fake signature for demo.\"\"\"\n",
|
| 257 |
+
" from PIL import ImageDraw\n",
|
| 258 |
+
" img = Image.new('RGB', (800, 1000), color='white')\n",
|
| 259 |
+
" draw = ImageDraw.Draw(img)\n",
|
| 260 |
+
"\n",
|
| 261 |
+
" # Simulate document text lines\n",
|
| 262 |
+
" for y in range(60, 700, 35):\n",
|
| 263 |
+
" line_width = np.random.randint(300, 700)\n",
|
| 264 |
+
" draw.rectangle([(60, y), (60 + line_width, y + 10)], fill='#cccccc')\n",
|
| 265 |
+
"\n",
|
| 266 |
+
" # Signature area label\n",
|
| 267 |
+
" draw.text((60, 750), \"Signature:\", fill='black')\n",
|
| 268 |
+
" # Simulate a cursive signature stroke\n",
|
| 269 |
+
" points = [(200, 800), (220, 780), (260, 820), (300, 790),\n",
|
| 270 |
+
" (340, 810), (380, 780), (400, 805)]\n",
|
| 271 |
+
" draw.line(points, fill='black', width=3)\n",
|
| 272 |
+
" draw.line([(200, 830), (420, 830)], fill='black', width=1)\n",
|
| 273 |
+
"\n",
|
| 274 |
+
" out = Path(\"../data/samples/document_with_signature_demo.png\")\n",
|
| 275 |
+
" out.parent.mkdir(parents=True, exist_ok=True)\n",
|
| 276 |
+
" img.save(out)\n",
|
| 277 |
+
" return out\n",
|
| 278 |
+
"\n",
|
| 279 |
+
"\n",
|
| 280 |
+
"samples_dir = Path(\"../data/samples\")\n",
|
| 281 |
+
"real_doc = samples_dir / \"document_with_signature_01.png\"\n",
|
| 282 |
+
"\n",
|
| 283 |
+
"if real_doc.exists():\n",
|
| 284 |
+
" doc_path = real_doc\n",
|
| 285 |
+
" print(f\"Using real document: {doc_path}\")\n",
|
| 286 |
+
"else:\n",
|
| 287 |
+
" doc_path = make_synthetic_document()\n",
|
| 288 |
+
" print(f\"Using synthetic document: {doc_path}\")\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"result = detect_signatures(doc_path, conf_threshold=0.25)\n",
|
| 291 |
+
"print(f\"\\nDetected {result['count']} signature(s)\")\n",
|
| 292 |
+
"show_detections(result)"
|
| 293 |
+
]
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"cell_type": "markdown",
|
| 297 |
+
"id": "a3b4c5d6",
|
| 298 |
+
"metadata": {},
|
| 299 |
+
"source": [
|
| 300 |
+
"## Demo 2 — Crop Detected Signatures for Further Analysis\n",
|
| 301 |
+
"\n",
|
| 302 |
+
"The cropped signatures can be fed directly into Lab 03 (Signature Verification):\n"
|
| 303 |
+
]
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"cell_type": "code",
|
| 307 |
+
"execution_count": null,
|
| 308 |
+
"id": "b4c5d6e7",
|
| 309 |
+
"metadata": {},
|
| 310 |
+
"outputs": [],
|
| 311 |
+
"source": [
|
| 312 |
+
"crops_dir = Path(\"../data/samples/detected_crops\")\n",
|
| 313 |
+
"\n",
|
| 314 |
+
"if result[\"count\"] > 0:\n",
|
| 315 |
+
" saved_paths = crop_signatures(result, crops_dir)\n",
|
| 316 |
+
"\n",
|
| 317 |
+
" # Show crops\n",
|
| 318 |
+
" fig, axes = plt.subplots(1, len(saved_paths), figsize=(4 * len(saved_paths), 3))\n",
|
| 319 |
+
" if len(saved_paths) == 1:\n",
|
| 320 |
+
" axes = [axes]\n",
|
| 321 |
+
" for ax, path in zip(axes, saved_paths):\n",
|
| 322 |
+
" ax.imshow(Image.open(path))\n",
|
| 323 |
+
" ax.set_title(path.name, fontsize=9)\n",
|
| 324 |
+
" ax.axis('off')\n",
|
| 325 |
+
" plt.suptitle(\"Extracted Signature Crops\", fontsize=12, fontweight='bold')\n",
|
| 326 |
+
" plt.tight_layout()\n",
|
| 327 |
+
" plt.show()\n",
|
| 328 |
+
" print(f\"\\n{len(saved_paths)} signature(s) saved to {crops_dir}\")\n",
|
| 329 |
+
" print(\"These can be used as input for Lab 03 (Signature Verification).\")\n",
|
| 330 |
+
"else:\n",
|
| 331 |
+
" print(\"No signatures detected — nothing to crop.\")\n",
|
| 332 |
+
" print(\"Try lowering conf_threshold or using a document with visible signatures.\")"
|
| 333 |
+
]
|
| 334 |
+
},
|
| 335 |
+
{
|
| 336 |
+
"cell_type": "markdown",
|
| 337 |
+
"id": "c5d6e7f8",
|
| 338 |
+
"metadata": {},
|
| 339 |
+
"source": [
|
| 340 |
+
"## Demo 3 — Load Your Own Document\n"
|
| 341 |
+
]
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"cell_type": "code",
|
| 345 |
+
"execution_count": null,
|
| 346 |
+
"id": "d6e7f8a9",
|
| 347 |
+
"metadata": {},
|
| 348 |
+
"outputs": [],
|
| 349 |
+
"source": [
|
| 350 |
+
"# ─── Change this path to your own scanned document ────────────────────────────\n",
|
| 351 |
+
"USER_DOC_PATH = \"../data/samples/document_with_signature_01.png\"\n",
|
| 352 |
+
"CONF_THRESHOLD = 0.30\n",
|
| 353 |
+
"# ──────────────────────────────────────────────────────────────────────────────\n",
|
| 354 |
+
"\n",
|
| 355 |
+
"path = Path(USER_DOC_PATH)\n",
|
| 356 |
+
"if path.exists():\n",
|
| 357 |
+
" res = detect_signatures(path, conf_threshold=CONF_THRESHOLD)\n",
|
| 358 |
+
" print(f\"Detected {res['count']} signature(s)\")\n",
|
| 359 |
+
" show_detections(res, title=path.name)\n",
|
| 360 |
+
"else:\n",
|
| 361 |
+
" print(f\"File not found: {path}\")\n",
|
| 362 |
+
" print(\"Place a scanned document image at the path above and re-run.\")"
|
| 363 |
+
]
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"cell_type": "markdown",
|
| 367 |
+
"id": "e7f8a9b0",
|
| 368 |
+
"metadata": {},
|
| 369 |
+
"source": [
|
| 370 |
+
"## Forensic Notes\n",
|
| 371 |
+
"\n",
|
| 372 |
+
"- Adjust **`conf_threshold`** based on your document quality. Low-resolution scans may need a lower threshold.\n",
|
| 373 |
+
"- For multi-page PDFs, convert each page to an image first (e.g., with `pdf2image` / `poppler`).\n",
|
| 374 |
+
"- Detected regions should be **reviewed by a human** before proceeding to automated verification — the detector may occasionally flag stamps, logos, or printed signatures.\n",
|
| 375 |
+
"- Detection output (coordinates, confidence scores) should be logged and preserved as part of the forensic record.\n",
|
| 376 |
+
"- Conditional DETR does **not** require Non-Maximum Suppression post-processing — each object query produces at most one prediction, eliminating duplicate detections by design.\n",
|
| 377 |
+
"\n",
|
| 378 |
+
"---\n",
|
| 379 |
+
"\n",
|
| 380 |
+
"**Next lab →** [05 — Writer Identification](05_writer_identification.ipynb)\n"
|
| 381 |
+
]
|
| 382 |
+
}
|
| 383 |
+
]
|
| 384 |
+
}
|
notebooks/{04_signature_detection_yolo.ipynb → archive/04_signature_detection_yolo.ipynb}
RENAMED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
|
|
|
| 5 |
"metadata": {},
|
| 6 |
"source": [
|
| 7 |
"# Lab 04 — Signature Detection in Documents (YOLOv8)\n",
|
|
@@ -27,19 +28,44 @@
|
|
| 27 |
{
|
| 28 |
"cell_type": "markdown",
|
| 29 |
"id": "48jzghnekwn",
|
| 30 |
-
"
|
| 31 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
},
|
| 33 |
{
|
| 34 |
"cell_type": "code",
|
|
|
|
| 35 |
"id": "yjec2b865cc",
|
| 36 |
-
"source": "# GraphoLab Core — production usage\n# Run this cell to use the shared core module instead of the notebook implementation below.\nimport sys, pathlib\nsys.path.insert(0, str(pathlib.Path(\"..\").resolve()))\n\nfrom core.signature import detect_and_crop, sig_detect, get_yolo\nfrom PIL import Image\nimport numpy as np\n\n# Example: detect and crop signature from a document\n# doc = np.array(Image.open(\"../data/samples/document_with_signature_01.png\").convert(\"RGB\"))\n# annotated, crop, summary = detect_and_crop(doc)\n# print(summary)\nprint(\"core.signature imported — detect_and_crop(), sig_detect() ready.\")",
|
| 37 |
"metadata": {},
|
| 38 |
-
"
|
| 39 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"cell_type": "markdown",
|
|
|
|
| 43 |
"metadata": {},
|
| 44 |
"source": [
|
| 45 |
"## Setup\n"
|
|
@@ -48,6 +74,7 @@
|
|
| 48 |
{
|
| 49 |
"cell_type": "code",
|
| 50 |
"execution_count": null,
|
|
|
|
| 51 |
"metadata": {},
|
| 52 |
"outputs": [],
|
| 53 |
"source": [
|
|
@@ -57,6 +84,7 @@
|
|
| 57 |
{
|
| 58 |
"cell_type": "code",
|
| 59 |
"execution_count": null,
|
|
|
|
| 60 |
"metadata": {},
|
| 61 |
"outputs": [],
|
| 62 |
"source": [
|
|
@@ -79,6 +107,7 @@
|
|
| 79 |
},
|
| 80 |
{
|
| 81 |
"cell_type": "markdown",
|
|
|
|
| 82 |
"metadata": {},
|
| 83 |
"source": [
|
| 84 |
"## Load the Model\n",
|
|
@@ -89,6 +118,7 @@
|
|
| 89 |
{
|
| 90 |
"cell_type": "code",
|
| 91 |
"execution_count": null,
|
|
|
|
| 92 |
"metadata": {},
|
| 93 |
"outputs": [],
|
| 94 |
"source": [
|
|
@@ -105,6 +135,7 @@
|
|
| 105 |
},
|
| 106 |
{
|
| 107 |
"cell_type": "markdown",
|
|
|
|
| 108 |
"metadata": {},
|
| 109 |
"source": [
|
| 110 |
"## Helper Functions\n"
|
|
@@ -113,6 +144,7 @@
|
|
| 113 |
{
|
| 114 |
"cell_type": "code",
|
| 115 |
"execution_count": null,
|
|
|
|
| 116 |
"metadata": {},
|
| 117 |
"outputs": [],
|
| 118 |
"source": [
|
|
@@ -181,6 +213,7 @@
|
|
| 181 |
},
|
| 182 |
{
|
| 183 |
"cell_type": "markdown",
|
|
|
|
| 184 |
"metadata": {},
|
| 185 |
"source": [
|
| 186 |
"## Demo 1 — Detect Signatures in a Sample Document\n"
|
|
@@ -189,6 +222,7 @@
|
|
| 189 |
{
|
| 190 |
"cell_type": "code",
|
| 191 |
"execution_count": null,
|
|
|
|
| 192 |
"metadata": {},
|
| 193 |
"outputs": [],
|
| 194 |
"source": [
|
|
@@ -234,6 +268,7 @@
|
|
| 234 |
},
|
| 235 |
{
|
| 236 |
"cell_type": "markdown",
|
|
|
|
| 237 |
"metadata": {},
|
| 238 |
"source": [
|
| 239 |
"## Demo 2 — Crop Detected Signatures for Further Analysis\n",
|
|
@@ -244,6 +279,7 @@
|
|
| 244 |
{
|
| 245 |
"cell_type": "code",
|
| 246 |
"execution_count": null,
|
|
|
|
| 247 |
"metadata": {},
|
| 248 |
"outputs": [],
|
| 249 |
"source": [
|
|
@@ -272,6 +308,7 @@
|
|
| 272 |
},
|
| 273 |
{
|
| 274 |
"cell_type": "markdown",
|
|
|
|
| 275 |
"metadata": {},
|
| 276 |
"source": [
|
| 277 |
"## Demo 3 — Load Your Own Document\n"
|
|
@@ -280,6 +317,7 @@
|
|
| 280 |
{
|
| 281 |
"cell_type": "code",
|
| 282 |
"execution_count": null,
|
|
|
|
| 283 |
"metadata": {},
|
| 284 |
"outputs": [],
|
| 285 |
"source": [
|
|
@@ -300,6 +338,7 @@
|
|
| 300 |
},
|
| 301 |
{
|
| 302 |
"cell_type": "markdown",
|
|
|
|
| 303 |
"metadata": {},
|
| 304 |
"source": [
|
| 305 |
"## Forensic Notes\n",
|
|
@@ -317,15 +356,23 @@
|
|
| 317 |
],
|
| 318 |
"metadata": {
|
| 319 |
"kernelspec": {
|
| 320 |
-
"display_name": "Python 3",
|
| 321 |
"language": "python",
|
| 322 |
"name": "python3"
|
| 323 |
},
|
| 324 |
"language_info": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
"name": "python",
|
| 326 |
-
"
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
},
|
| 329 |
"nbformat": 4,
|
| 330 |
"nbformat_minor": 5
|
| 331 |
-
}
|
|
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
| 5 |
+
"id": "9e1e6c7c",
|
| 6 |
"metadata": {},
|
| 7 |
"source": [
|
| 8 |
"# Lab 04 — Signature Detection in Documents (YOLOv8)\n",
|
|
|
|
| 28 |
{
|
| 29 |
"cell_type": "markdown",
|
| 30 |
"id": "48jzghnekwn",
|
| 31 |
+
"metadata": {},
|
| 32 |
+
"source": [
|
| 33 |
+
"## GraphoLab Core — Quick Start\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"> The production implementation of signature detection is available in [`core/signature.py`](../core/signature.py).\n",
|
| 36 |
+
"> It wraps **YOLOv8** (`tech4humans/yolov8s-signature-detector`) with lazy thread-safe model loading,\n",
|
| 37 |
+
"> bounding-box annotation, and automatic cropping of detected signatures.\n",
|
| 38 |
+
">\n",
|
| 39 |
+
"> Run the cell below to import it directly. The remaining cells implement the same detection\n",
|
| 40 |
+
"> pipeline from scratch for educational purposes."
|
| 41 |
+
]
|
| 42 |
},
|
| 43 |
{
|
| 44 |
"cell_type": "code",
|
| 45 |
+
"execution_count": null,
|
| 46 |
"id": "yjec2b865cc",
|
|
|
|
| 47 |
"metadata": {},
|
| 48 |
+
"outputs": [],
|
| 49 |
+
"source": [
|
| 50 |
+
"# GraphoLab Core — production usage\n",
|
| 51 |
+
"# Run this cell to use the shared core module instead of the notebook implementation below.\n",
|
| 52 |
+
"import sys, pathlib\n",
|
| 53 |
+
"sys.path.insert(0, str(pathlib.Path(\"..\").resolve()))\n",
|
| 54 |
+
"\n",
|
| 55 |
+
"from core.signature import detect_and_crop, sig_detect, get_yolo\n",
|
| 56 |
+
"from PIL import Image\n",
|
| 57 |
+
"import numpy as np\n",
|
| 58 |
+
"\n",
|
| 59 |
+
"# Example: detect and crop signature from a document\n",
|
| 60 |
+
"# doc = np.array(Image.open(\"../data/samples/document_with_signature_01.png\").convert(\"RGB\"))\n",
|
| 61 |
+
"# annotated, crop, summary = detect_and_crop(doc)\n",
|
| 62 |
+
"# print(summary)\n",
|
| 63 |
+
"print(\"core.signature imported — detect_and_crop(), sig_detect() ready.\")"
|
| 64 |
+
]
|
| 65 |
},
|
| 66 |
{
|
| 67 |
"cell_type": "markdown",
|
| 68 |
+
"id": "d04f2c7a",
|
| 69 |
"metadata": {},
|
| 70 |
"source": [
|
| 71 |
"## Setup\n"
|
|
|
|
| 74 |
{
|
| 75 |
"cell_type": "code",
|
| 76 |
"execution_count": null,
|
| 77 |
+
"id": "919826d2",
|
| 78 |
"metadata": {},
|
| 79 |
"outputs": [],
|
| 80 |
"source": [
|
|
|
|
| 84 |
{
|
| 85 |
"cell_type": "code",
|
| 86 |
"execution_count": null,
|
| 87 |
+
"id": "eca98486",
|
| 88 |
"metadata": {},
|
| 89 |
"outputs": [],
|
| 90 |
"source": [
|
|
|
|
| 107 |
},
|
| 108 |
{
|
| 109 |
"cell_type": "markdown",
|
| 110 |
+
"id": "3f9379fa",
|
| 111 |
"metadata": {},
|
| 112 |
"source": [
|
| 113 |
"## Load the Model\n",
|
|
|
|
| 118 |
{
|
| 119 |
"cell_type": "code",
|
| 120 |
"execution_count": null,
|
| 121 |
+
"id": "fa0c4ee9",
|
| 122 |
"metadata": {},
|
| 123 |
"outputs": [],
|
| 124 |
"source": [
|
|
|
|
| 135 |
},
|
| 136 |
{
|
| 137 |
"cell_type": "markdown",
|
| 138 |
+
"id": "83306ace",
|
| 139 |
"metadata": {},
|
| 140 |
"source": [
|
| 141 |
"## Helper Functions\n"
|
|
|
|
| 144 |
{
|
| 145 |
"cell_type": "code",
|
| 146 |
"execution_count": null,
|
| 147 |
+
"id": "80769d03",
|
| 148 |
"metadata": {},
|
| 149 |
"outputs": [],
|
| 150 |
"source": [
|
|
|
|
| 213 |
},
|
| 214 |
{
|
| 215 |
"cell_type": "markdown",
|
| 216 |
+
"id": "6eb37bdd",
|
| 217 |
"metadata": {},
|
| 218 |
"source": [
|
| 219 |
"## Demo 1 — Detect Signatures in a Sample Document\n"
|
|
|
|
| 222 |
{
|
| 223 |
"cell_type": "code",
|
| 224 |
"execution_count": null,
|
| 225 |
+
"id": "e4a7bc76",
|
| 226 |
"metadata": {},
|
| 227 |
"outputs": [],
|
| 228 |
"source": [
|
|
|
|
| 268 |
},
|
| 269 |
{
|
| 270 |
"cell_type": "markdown",
|
| 271 |
+
"id": "d41087e5",
|
| 272 |
"metadata": {},
|
| 273 |
"source": [
|
| 274 |
"## Demo 2 — Crop Detected Signatures for Further Analysis\n",
|
|
|
|
| 279 |
{
|
| 280 |
"cell_type": "code",
|
| 281 |
"execution_count": null,
|
| 282 |
+
"id": "b5170d57",
|
| 283 |
"metadata": {},
|
| 284 |
"outputs": [],
|
| 285 |
"source": [
|
|
|
|
| 308 |
},
|
| 309 |
{
|
| 310 |
"cell_type": "markdown",
|
| 311 |
+
"id": "e16a15d9",
|
| 312 |
"metadata": {},
|
| 313 |
"source": [
|
| 314 |
"## Demo 3 — Load Your Own Document\n"
|
|
|
|
| 317 |
{
|
| 318 |
"cell_type": "code",
|
| 319 |
"execution_count": null,
|
| 320 |
+
"id": "24edb2bf",
|
| 321 |
"metadata": {},
|
| 322 |
"outputs": [],
|
| 323 |
"source": [
|
|
|
|
| 338 |
},
|
| 339 |
{
|
| 340 |
"cell_type": "markdown",
|
| 341 |
+
"id": "c2183d23",
|
| 342 |
"metadata": {},
|
| 343 |
"source": [
|
| 344 |
"## Forensic Notes\n",
|
|
|
|
| 356 |
],
|
| 357 |
"metadata": {
|
| 358 |
"kernelspec": {
|
| 359 |
+
"display_name": "Python 3 (ipykernel)",
|
| 360 |
"language": "python",
|
| 361 |
"name": "python3"
|
| 362 |
},
|
| 363 |
"language_info": {
|
| 364 |
+
"codemirror_mode": {
|
| 365 |
+
"name": "ipython",
|
| 366 |
+
"version": 3
|
| 367 |
+
},
|
| 368 |
+
"file_extension": ".py",
|
| 369 |
+
"mimetype": "text/x-python",
|
| 370 |
"name": "python",
|
| 371 |
+
"nbconvert_exporter": "python",
|
| 372 |
+
"pygments_lexer": "ipython3",
|
| 373 |
+
"version": "3.11.9"
|
| 374 |
}
|
| 375 |
},
|
| 376 |
"nbformat": 4,
|
| 377 |
"nbformat_minor": 5
|
| 378 |
+
}
|
requirements.txt
CHANGED
|
@@ -8,14 +8,12 @@ torchvision>=0.16.0
|
|
| 8 |
# Hugging Face ecosystem
|
| 9 |
transformers>=4.35.0
|
| 10 |
huggingface_hub>=0.25.1
|
|
|
|
| 11 |
|
| 12 |
# Computer Vision
|
| 13 |
Pillow>=10.0.0
|
| 14 |
opencv-python>=4.8.0
|
| 15 |
|
| 16 |
-
# Object Detection (YOLOv8)
|
| 17 |
-
ultralytics>=8.0.0
|
| 18 |
-
|
| 19 |
# audioop backport for Python 3.13 (pydub dependency of gradio 4.x)
|
| 20 |
audioop-lts; python_version >= "3.13"
|
| 21 |
fpdf2>=2.7 # PDF report generation
|
|
@@ -30,9 +28,6 @@ scipy>=1.11.0
|
|
| 30 |
jupyterlab>=4.0.0
|
| 31 |
ipywidgets>=8.0.0
|
| 32 |
|
| 33 |
-
# Image augmentation (used in signature processing)
|
| 34 |
-
albumentations>=1.3.0
|
| 35 |
-
|
| 36 |
# Signature preprocessing (sigver-compatible)
|
| 37 |
scikit-image>=0.21.0
|
| 38 |
|
|
|
|
| 8 |
# Hugging Face ecosystem
|
| 9 |
transformers>=4.35.0
|
| 10 |
huggingface_hub>=0.25.1
|
| 11 |
+
timm>=0.9.0
|
| 12 |
|
| 13 |
# Computer Vision
|
| 14 |
Pillow>=10.0.0
|
| 15 |
opencv-python>=4.8.0
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
# audioop backport for Python 3.13 (pydub dependency of gradio 4.x)
|
| 18 |
audioop-lts; python_version >= "3.13"
|
| 19 |
fpdf2>=2.7 # PDF report generation
|
|
|
|
| 28 |
jupyterlab>=4.0.0
|
| 29 |
ipywidgets>=8.0.0
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
# Signature preprocessing (sigver-compatible)
|
| 32 |
scikit-image>=0.21.0
|
| 33 |
|