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 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 (YOLOv8) | 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,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 (YOLOv8)")
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 (YOLOv8)")
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
- - get_yolo() lazy loader for the YOLOv8 signature detector
7
  - preprocess_signature() sigver-compatible preprocessing
8
  - sig_verify() verify signature authenticity (SigNet)
9
- - sig_detect() detect signature locations in a document (YOLO)
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
- YOLO_REPO = "tech4humans/yolov8s-signature-detector"
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
- _yolo_model = None
99
- _yolo_lock = threading.Lock()
 
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 get_yolo():
121
- """Return the YOLO signature detector, downloading on first call (thread-safe)."""
122
- global _yolo_model
123
- if _yolo_model is None:
124
- with _yolo_lock:
125
- if _yolo_model is None:
126
- from huggingface_hub import hf_hub_download
127
- from ultralytics import YOLO
128
- print("Loading YOLOv8 signature detector...")
129
- hf_token = os.environ.get("HF_TOKEN")
130
- model_path = hf_hub_download(
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 YOLO.
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
- yolo = get_yolo()
305
  except Exception as e:
306
  msg = (
307
  "⚠️ **Modello non disponibile.**\n\n"
308
- "Il modello `tech4humans/yolov8s-signature-detector` è ad accesso limitato su Hugging Face.\n\n"
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
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
320
- pil_img.save(tmp.name)
321
- tmp_path = tmp.name
322
-
323
- results = yolo.predict(tmp_path, conf=conf_threshold, verbose=False)
324
- os.unlink(tmp_path)
 
325
 
326
- result = results[0]
327
  annotated = image.copy()
328
  count = 0
329
-
330
- if result.boxes is not None:
331
- for box in result.boxes:
332
- x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
333
- conf = float(box.conf[0].cpu())
334
- cv2.rectangle(annotated, (x1, y1), (x2, y2), (255, 0, 0), 2)
335
- cv2.putText(annotated, f"Sig #{count+1} {conf:.0%}",
336
- (x1, max(y1 - 8, 0)),
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:** `tech4humans/yolov8s-signature-detector`\n"
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 YOLO detection and return (annotated, first_crop, summary).
354
 
355
- Gracefully degrades when YOLO is not available (missing HF_TOKEN).
356
  """
357
  annotated = image.copy()
358
  try:
359
- yolo = get_yolo()
360
  except Exception:
361
- return annotated, None, "⚠️ Rilevamento firma non disponibile (HF_TOKEN mancante)."
362
 
363
  pil_img = Image.fromarray(image).convert("RGB")
364
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
365
- pil_img.save(tmp.name)
366
- tmp_path = tmp.name
367
-
368
- results = yolo.predict(tmp_path, conf=conf_threshold, verbose=False)
369
- os.unlink(tmp_path)
 
370
 
371
- result = results[0]
372
  first_crop: np.ndarray | None = None
373
  count = 0
374
 
375
- if result.boxes is not None:
376
- for box in result.boxes:
377
- x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
378
- conf = float(box.conf[0].cpu())
379
- cv2.rectangle(annotated, (x1, y1), (x2, y2), (255, 0, 0), 2)
380
- cv2.putText(annotated, f"Sig #{count+1} {conf:.0%}",
381
- (x1, max(y1 - 8, 0)),
382
- cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
383
- if count == 0:
384
- x1c = max(0, x1); y1c = max(0, y1)
385
- x2c = min(image.shape[1], x2); y2c = min(image.shape[0], y2)
386
- if x2c > x1c and y2c > y1c:
387
- first_crop = image[y1c:y2c, x1c:x2c]
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
- "source": "## GraphoLab Core — Quick Start\n\n> The production implementation of signature detection is available in [`core/signature.py`](../core/signature.py).\n> It wraps **YOLOv8** (`tech4humans/yolov8s-signature-detector`) with lazy thread-safe model loading,\n> bounding-box annotation, and automatic cropping of detected signatures.\n>\n> Run the cell below to import it directly. The remaining cells implement the same detection\n> pipeline from scratch for educational purposes.",
31
- "metadata": {}
 
 
 
 
 
 
 
 
 
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
- "execution_count": null,
39
- "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "version": "3.11.0"
 
 
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