asyirafitri commited on
Commit
65cc549
·
verified ·
1 Parent(s): 674e5f9

Add unified_pipeline: merged signature-verification synthetic document generator (part 15)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +71 -0
  2. unified_pipeline/README.md +844 -0
  3. unified_pipeline/data/raw/signet/train/060_forg/01_0111060.PNG +3 -0
  4. unified_pipeline/data/raw/signet/train/060_forg/01_0121060.PNG +3 -0
  5. unified_pipeline/data/raw/signet/train/060_forg/01_0126060.PNG +3 -0
  6. unified_pipeline/data/raw/signet/train/060_forg/02_0111060.PNG +3 -0
  7. unified_pipeline/data/raw/signet/train/060_forg/02_0121060.PNG +3 -0
  8. unified_pipeline/data/raw/signet/train/060_forg/02_0126060.PNG +3 -0
  9. unified_pipeline/data/raw/signet/train/060_forg/03_0111060.PNG +3 -0
  10. unified_pipeline/data/raw/signet/train/060_forg/03_0121060.PNG +3 -0
  11. unified_pipeline/data/raw/signet/train/060_forg/03_0126060.PNG +3 -0
  12. unified_pipeline/data/raw/signet/train/060_forg/04_0111060.PNG +3 -0
  13. unified_pipeline/data/raw/signet/train/060_forg/04_0121060.PNG +3 -0
  14. unified_pipeline/data/raw/signet/train/060_forg/04_0126060.PNG +3 -0
  15. unified_pipeline/data/raw/signet/train/061/01_061.png +3 -0
  16. unified_pipeline/data/raw/signet/train/061/02_061.png +3 -0
  17. unified_pipeline/data/raw/signet/train/061/03_061.png +3 -0
  18. unified_pipeline/data/raw/signet/train/061/04_061.png +3 -0
  19. unified_pipeline/data/raw/signet/train/061/05_061.png +3 -0
  20. unified_pipeline/data/raw/signet/train/061/06_061.png +3 -0
  21. unified_pipeline/data/raw/signet/train/061/07_061.png +3 -0
  22. unified_pipeline/data/raw/signet/train/061/08_061.png +3 -0
  23. unified_pipeline/data/raw/signet/train/061/09_061.png +3 -0
  24. unified_pipeline/data/raw/signet/train/061/10_061.png +3 -0
  25. unified_pipeline/data/raw/signet/train/061/11_061.png +3 -0
  26. unified_pipeline/data/raw/signet/train/061/12_061.png +3 -0
  27. unified_pipeline/data/raw/signet/train/061_forg/01_0102061.PNG +0 -0
  28. unified_pipeline/data/raw/signet/train/061_forg/01_0112061.PNG +0 -0
  29. unified_pipeline/data/raw/signet/train/061_forg/01_0206061.PNG +0 -0
  30. unified_pipeline/data/raw/signet/train/061_forg/02_0102061.PNG +0 -0
  31. unified_pipeline/data/raw/signet/train/061_forg/02_0112061.PNG +0 -0
  32. unified_pipeline/data/raw/signet/train/061_forg/02_0206061.PNG +0 -0
  33. unified_pipeline/data/raw/signet/train/061_forg/03_0102061.PNG +0 -0
  34. unified_pipeline/data/raw/signet/train/061_forg/03_0112061.PNG +0 -0
  35. unified_pipeline/data/raw/signet/train/061_forg/03_0206061.PNG +0 -0
  36. unified_pipeline/data/raw/signet/train/061_forg/04_0102061.PNG +0 -0
  37. unified_pipeline/data/raw/signet/train/061_forg/04_0112061.PNG +0 -0
  38. unified_pipeline/data/raw/signet/train/061_forg/04_0206061.PNG +0 -0
  39. unified_pipeline/data/raw/signet/train/062/01_062.png +3 -0
  40. unified_pipeline/data/raw/signet/train/062/02_062.png +3 -0
  41. unified_pipeline/data/raw/signet/train/062/03_062.png +3 -0
  42. unified_pipeline/data/raw/signet/train/062/04_062.png +3 -0
  43. unified_pipeline/data/raw/signet/train/062/05_062.png +3 -0
  44. unified_pipeline/data/raw/signet/train/062/06_062.png +3 -0
  45. unified_pipeline/data/raw/signet/train/062/07_062.png +3 -0
  46. unified_pipeline/data/raw/signet/train/062/08_062.png +3 -0
  47. unified_pipeline/data/raw/signet/train/062/09_062.png +3 -0
  48. unified_pipeline/data/raw/signet/train/062/10_062.png +3 -0
  49. unified_pipeline/data/raw/signet/train/062/11_062.png +3 -0
  50. unified_pipeline/data/raw/signet/train/062/12_062.png +3 -0
.gitattributes CHANGED
@@ -4974,3 +4974,74 @@ unified_pipeline/data/raw/signet/train/059_forg/03_0104059.PNG filter=lfs diff=l
4974
  unified_pipeline/data/raw/signet/train/059_forg/03_0125059.PNG filter=lfs diff=lfs merge=lfs -text
4975
  unified_pipeline/data/raw/signet/train/059_forg/04_0104059.PNG filter=lfs diff=lfs merge=lfs -text
4976
  unified_pipeline/data/raw/signet/train/059_forg/04_0125059.PNG filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4974
  unified_pipeline/data/raw/signet/train/059_forg/03_0125059.PNG filter=lfs diff=lfs merge=lfs -text
4975
  unified_pipeline/data/raw/signet/train/059_forg/04_0104059.PNG filter=lfs diff=lfs merge=lfs -text
4976
  unified_pipeline/data/raw/signet/train/059_forg/04_0125059.PNG filter=lfs diff=lfs merge=lfs -text
4977
+ unified_pipeline/data/raw/signet/train/060_forg/01_0111060.PNG filter=lfs diff=lfs merge=lfs -text
4978
+ unified_pipeline/data/raw/signet/train/060_forg/01_0121060.PNG filter=lfs diff=lfs merge=lfs -text
4979
+ unified_pipeline/data/raw/signet/train/060_forg/01_0126060.PNG filter=lfs diff=lfs merge=lfs -text
4980
+ unified_pipeline/data/raw/signet/train/060_forg/02_0111060.PNG filter=lfs diff=lfs merge=lfs -text
4981
+ unified_pipeline/data/raw/signet/train/060_forg/02_0121060.PNG filter=lfs diff=lfs merge=lfs -text
4982
+ unified_pipeline/data/raw/signet/train/060_forg/02_0126060.PNG filter=lfs diff=lfs merge=lfs -text
4983
+ unified_pipeline/data/raw/signet/train/060_forg/03_0111060.PNG filter=lfs diff=lfs merge=lfs -text
4984
+ unified_pipeline/data/raw/signet/train/060_forg/03_0121060.PNG filter=lfs diff=lfs merge=lfs -text
4985
+ unified_pipeline/data/raw/signet/train/060_forg/03_0126060.PNG filter=lfs diff=lfs merge=lfs -text
4986
+ unified_pipeline/data/raw/signet/train/060_forg/04_0111060.PNG filter=lfs diff=lfs merge=lfs -text
4987
+ unified_pipeline/data/raw/signet/train/060_forg/04_0121060.PNG filter=lfs diff=lfs merge=lfs -text
4988
+ unified_pipeline/data/raw/signet/train/060_forg/04_0126060.PNG filter=lfs diff=lfs merge=lfs -text
4989
+ unified_pipeline/data/raw/signet/train/062_forg/01_0116062.PNG filter=lfs diff=lfs merge=lfs -text
4990
+ unified_pipeline/data/raw/signet/train/062_forg/01_0201062.PNG filter=lfs diff=lfs merge=lfs -text
4991
+ unified_pipeline/data/raw/signet/train/062_forg/02_0116062.PNG filter=lfs diff=lfs merge=lfs -text
4992
+ unified_pipeline/data/raw/signet/train/062_forg/02_0201062.PNG filter=lfs diff=lfs merge=lfs -text
4993
+ unified_pipeline/data/raw/signet/train/062_forg/03_0109062.PNG filter=lfs diff=lfs merge=lfs -text
4994
+ unified_pipeline/data/raw/signet/train/062_forg/03_0116062.PNG filter=lfs diff=lfs merge=lfs -text
4995
+ unified_pipeline/data/raw/signet/train/062_forg/03_0201062.PNG filter=lfs diff=lfs merge=lfs -text
4996
+ unified_pipeline/data/raw/signet/train/062_forg/04_0109062.PNG filter=lfs diff=lfs merge=lfs -text
4997
+ unified_pipeline/data/raw/signet/train/062_forg/04_0116062.PNG filter=lfs diff=lfs merge=lfs -text
4998
+ unified_pipeline/data/raw/signet/train/062_forg/04_0201062.PNG filter=lfs diff=lfs merge=lfs -text
4999
+ unified_pipeline/data/raw/signet/train/063_forg/01_0104063.PNG filter=lfs diff=lfs merge=lfs -text
5000
+ unified_pipeline/data/raw/signet/train/063_forg/01_0119063.PNG filter=lfs diff=lfs merge=lfs -text
5001
+ unified_pipeline/data/raw/signet/train/063_forg/02_0104063.PNG filter=lfs diff=lfs merge=lfs -text
5002
+ unified_pipeline/data/raw/signet/train/063_forg/02_0119063.PNG filter=lfs diff=lfs merge=lfs -text
5003
+ unified_pipeline/data/raw/signet/train/063_forg/03_0104063.PNG filter=lfs diff=lfs merge=lfs -text
5004
+ unified_pipeline/data/raw/signet/train/063_forg/04_0104063.PNG filter=lfs diff=lfs merge=lfs -text
5005
+ unified_pipeline/data/raw/signet/train/064_forg/01_0105064.PNG filter=lfs diff=lfs merge=lfs -text
5006
+ unified_pipeline/data/raw/signet/train/064_forg/01_0203064.PNG filter=lfs diff=lfs merge=lfs -text
5007
+ unified_pipeline/data/raw/signet/train/064_forg/02_0105064.PNG filter=lfs diff=lfs merge=lfs -text
5008
+ unified_pipeline/data/raw/signet/train/064_forg/02_0203064.PNG filter=lfs diff=lfs merge=lfs -text
5009
+ unified_pipeline/data/raw/signet/train/064_forg/03_0105064.PNG filter=lfs diff=lfs merge=lfs -text
5010
+ unified_pipeline/data/raw/signet/train/064_forg/03_0203064.PNG filter=lfs diff=lfs merge=lfs -text
5011
+ unified_pipeline/data/raw/signet/train/064_forg/04_0105064.PNG filter=lfs diff=lfs merge=lfs -text
5012
+ unified_pipeline/data/raw/signet/train/064_forg/04_0203064.PNG filter=lfs diff=lfs merge=lfs -text
5013
+ unified_pipeline/data/raw/signet/train/065_forg/01_0118065.PNG filter=lfs diff=lfs merge=lfs -text
5014
+ unified_pipeline/data/raw/signet/train/065_forg/01_0206065.PNG filter=lfs diff=lfs merge=lfs -text
5015
+ unified_pipeline/data/raw/signet/train/065_forg/02_0118065.PNG filter=lfs diff=lfs merge=lfs -text
5016
+ unified_pipeline/data/raw/signet/train/065_forg/03_0118065.PNG filter=lfs diff=lfs merge=lfs -text
5017
+ unified_pipeline/data/raw/signet/train/065_forg/03_0206065.PNG filter=lfs diff=lfs merge=lfs -text
5018
+ unified_pipeline/data/raw/signet/train/065_forg/04_0118065.PNG filter=lfs diff=lfs merge=lfs -text
5019
+ unified_pipeline/data/raw/signet/train/065_forg/04_0206065.PNG filter=lfs diff=lfs merge=lfs -text
5020
+ unified_pipeline/data/raw/signet/train/066_forg/01_0212066.PNG filter=lfs diff=lfs merge=lfs -text
5021
+ unified_pipeline/data/raw/signet/train/066_forg/02_0212066.PNG filter=lfs diff=lfs merge=lfs -text
5022
+ unified_pipeline/data/raw/signet/train/066_forg/03_0101066.PNG filter=lfs diff=lfs merge=lfs -text
5023
+ unified_pipeline/data/raw/signet/train/066_forg/03_0212066.PNG filter=lfs diff=lfs merge=lfs -text
5024
+ unified_pipeline/data/raw/signet/train/066_forg/04_0212066.PNG filter=lfs diff=lfs merge=lfs -text
5025
+ unified_pipeline/data/raw/signet/train/067_forg/01_0205067.PNG filter=lfs diff=lfs merge=lfs -text
5026
+ unified_pipeline/data/raw/signet/train/067_forg/01_0212067.PNG filter=lfs diff=lfs merge=lfs -text
5027
+ unified_pipeline/data/raw/signet/train/067_forg/02_0205067.PNG filter=lfs diff=lfs merge=lfs -text
5028
+ unified_pipeline/data/raw/signet/train/067_forg/02_0212067.PNG filter=lfs diff=lfs merge=lfs -text
5029
+ unified_pipeline/data/raw/signet/train/067_forg/03_0205067.PNG filter=lfs diff=lfs merge=lfs -text
5030
+ unified_pipeline/data/raw/signet/train/067_forg/03_0212067.PNG filter=lfs diff=lfs merge=lfs -text
5031
+ unified_pipeline/data/raw/signet/train/067_forg/04_0205067.PNG filter=lfs diff=lfs merge=lfs -text
5032
+ unified_pipeline/data/raw/signet/train/067_forg/04_0212067.PNG filter=lfs diff=lfs merge=lfs -text
5033
+ unified_pipeline/data/raw/signet/train/068_forg/03_0113068.PNG filter=lfs diff=lfs merge=lfs -text
5034
+ unified_pipeline/data/raw/signet/train/068_forg/04_0113068.PNG filter=lfs diff=lfs merge=lfs -text
5035
+ unified_pipeline/data/raw/signet/train/069_forg/01_0106069.PNG filter=lfs diff=lfs merge=lfs -text
5036
+ unified_pipeline/data/raw/signet/train/069_forg/01_0108069.PNG filter=lfs diff=lfs merge=lfs -text
5037
+ unified_pipeline/data/raw/signet/train/069_forg/01_0111069.PNG filter=lfs diff=lfs merge=lfs -text
5038
+ unified_pipeline/data/raw/signet/train/069_forg/02_0106069.PNG filter=lfs diff=lfs merge=lfs -text
5039
+ unified_pipeline/data/raw/signet/train/069_forg/02_0108069.PNG filter=lfs diff=lfs merge=lfs -text
5040
+ unified_pipeline/data/raw/signet/train/069_forg/02_0111069.PNG filter=lfs diff=lfs merge=lfs -text
5041
+ unified_pipeline/data/raw/signet/train/069_forg/03_0106069.PNG filter=lfs diff=lfs merge=lfs -text
5042
+ unified_pipeline/data/raw/signet/train/069_forg/03_0108069.PNG filter=lfs diff=lfs merge=lfs -text
5043
+ unified_pipeline/data/raw/signet/train/069_forg/03_0111069.PNG filter=lfs diff=lfs merge=lfs -text
5044
+ unified_pipeline/data/raw/signet/train/069_forg/04_0106069.PNG filter=lfs diff=lfs merge=lfs -text
5045
+ unified_pipeline/data/raw/signet/train/069_forg/04_0108069.PNG filter=lfs diff=lfs merge=lfs -text
5046
+ unified_pipeline/data/raw/signet/train/069_forg/04_0111069.PNG filter=lfs diff=lfs merge=lfs -text
5047
+ unified_pipeline/docs/unified_pipeline_Metrics_Thresholds_Formulas.pptx filter=lfs diff=lfs merge=lfs -text
unified_pipeline/README.md ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # unified_pipeline
2
+
3
+ A single, self-contained synthetic signed-bank-document generator, built
4
+ against a spec deck (`Metrics_Thresholds_Formulas` /
5
+ `Synthetic_Test_Data_Combined.pptx`). It combines two template families --
6
+ 9 real scanned bank-document photographs with hand-measured signature-box
7
+ annotations, and 15 procedurally (matplotlib-)drawn documents with
8
+ cosmetic-field realism (SWIFT/NRIC/UEN/amounts) -- behind one config-driven
9
+ (Pydantic + YAML) generation engine, a 12-case-type reject-code taxonomy,
10
+ a shared signature dataset pool (CEDAR, SigNet, BHSig260 Hindi/Bengali), a
11
+ richer Albumentations-backed augmentation set, and a 119-test pytest suite.
12
+
13
+ **Start here if you're reviewing this:** `data/showcase_dataset/` -- 24
14
+ pre-generated cases covering all 12 case types, each rendered once on a
15
+ scanned template and once on a procedural template, with
16
+ `SHOWCASE_INDEX.csv` as a one-page overview, a `before_after/` folder
17
+ holding every case's metrics preview grouped together (see "Before/after
18
+ degradation preview" below), and `reference_signers_index.json` mapping
19
+ every name/role that appears in the dataset back to their signer identity
20
+ and reference signature image(s) (see "Reference-signer index" below). No
21
+ need to run anything to see what the pipeline produces.
22
+ `docs/unified_pipeline_Metrics_Thresholds_Formulas.pptx` walks through the
23
+ same metrics/thresholds/formulas with worked examples and images.
24
+
25
+ ## This repo is fully self-contained
26
+
27
+ **Nothing here points outside this folder.** Unzip it anywhere and
28
+ `python main.py` / `pytest` just work -- no sibling checkouts, no external
29
+ dataset downloads. See "Datasets" below for exactly what's bundled and what
30
+ was left out (and why).
31
+
32
+ ## Design notes
33
+
34
+ A few structural choices worth calling out up front:
35
+
36
+ - **Per-case output folders** (`case_NNN.../`) hold the rendered document,
37
+ the exact reference signature used (copied in, not just referenced by
38
+ path), and the ground truth together -- a flat `sample_NNNNNN.png/.json`
39
+ layout only makes sense at ML-training scale where nobody opens
40
+ individual cases; for a reviewed dataset, self-contained per-case folders
41
+ are the better fit.
42
+ - **Ground-truth schema** is deep and fully populated (geometry/placement/
43
+ rendering/augmentation audit trail), with top-level convenience fields
44
+ (`case_id`, `expected_verdict`, `expected_reject_code`) layered on top --
45
+ so a reader doesn't need to know the nested `verification.verdict` shape
46
+ to answer "what should a verifier conclude here."
47
+ - **Case generation** runs off one data table (`CASE_TABLE`) plus one
48
+ generic engine, rather than 12 separate hardcoded functions duplicating
49
+ the same logic.
50
+ - **Config system** is Pydantic + YAML (validated, typed) -- catches bad
51
+ values before a long batch run fails partway through.
52
+ - **Templates** are two genuinely complementary families (9 real scanned
53
+ photos, 15 programmatically-drawn documents) kept separate but unified
54
+ under one id space. `template_provider: both` is the **default** (see
55
+ "Template providers" below), so a plain `python main.py` shows both.
56
+ - **Difficulty formula** is identical across both template families --
57
+ same weights/tiers/normalization; only the *reference constants* differ
58
+ (see "Difficulty calibration profiles"), and that's a config choice, not
59
+ a code fork.
60
+ - **Difficulty CLI** takes a specific numeric target (`--target-difficulty
61
+ 0.55`), not a coarse tier, since the score itself is already continuous.
62
+ See "Difficulty-score targeting" below.
63
+
64
+ ## Datasets ("use all the dataset")
65
+
66
+ `SignatureDataset` (`src/signatures/signature_dataset.py`) indexes every
67
+ source into one pool under `data/raw/`, each record tagged with
68
+ `source_type` (genuine/forged) and `script` (latin/hindi/bengali):
69
+
70
+ | Source | Path | Signer group prefix | Bundled | Notes |
71
+ |---|---|---|---|---|
72
+ | CEDAR | `data/raw/cedar/{genuine,forged}` | `signer_NNNN` | Full (1320 + 1320) | Used for the scanned templates. |
73
+ | SigNet | `data/raw/signet/{train,test}` | `signet_NNNN` | Full (2149 files) | Primary Latin-script source. |
74
+ | BHSig260-Hindi | `data/raw/bhsig260_hindi` | `bhsig260_hindi_NNNN` | Capped 8 genuine + 8 forged / writer, 160 writers (2560 files; full archive is ~8600 files / 2.3GB) | |
75
+ | BHSig260-Bengali | `data/raw/bhsig260_bengali` | `bhsig260_bengali_NNNN` | Capped 8+8 / writer, 100 writers (1600 files; full archive is ~5400 files / 1.5GB) | |
76
+ | GPDS | -- | `gpds_NNNN` | **Not bundled** | Never wired into the live generation path (present in the original dataset archive, never validated). `SignatureDataset` still supports it (`gpds_dir` config) if you have the archive and want to point at it. |
77
+
78
+ **Dropped: an 11-file named-signer demo overlay** (`dataset/hindi_bengali/`
79
+ + `metadata.csv`, mapping a handful of files to hand-picked names like
80
+ "Priya Sharma"). It added no signature diversity beyond what BHSig260
81
+ already provides at ~260x the scale (260 writers vs. 7) -- its only effect
82
+ was a few ground-truth JSONs showing a real name instead of an
83
+ auto-generated one. `SignatureDataset`'s `metadata.csv` support itself is
84
+ still fully functional generic code (see `test_metadata_csv_overrides_signer_identity_and_script`
85
+ in `tests/test_discovery.py`), just not exercised by bundled data anymore.
86
+
87
+ Total bundled dataset: **~1.7GB**. Any source can be omitted by pointing
88
+ its config path at a location that doesn't exist -- `SignatureDataset`
89
+ skips missing sources gracefully rather than failing (this is how GPDS
90
+ support stays in the code without being bundled).
91
+
92
+ ## Output structure
93
+
94
+ Every generated case gets its own folder:
95
+
96
+ ```
97
+ output/case_000001/
98
+ case_000001_document.png # the rendered document
99
+ case_000001_reference_signature.png # copy of the exact signature image used
100
+ case_000001_companion_signature.png # (case types that officially test a 2nd signer)
101
+ case_000001_<box_id>_signature.png # (any OTHER box on a multi-box template -- see below)
102
+ case_000001_visualization.png # (--save-visualization: geometry/metrics overlay)
103
+ case_000001_ground_truth.json # full metadata
104
+ output/before_after/
105
+ case_000001_before_after.png # (--save-before-after: clean vs. final side by side)
106
+ case_000002_before_after.png # grouped in one folder, not scattered per-case --
107
+ ... # easier to flip through in a row
108
+ output/dataset_summary.csv # one row per case: template/type/verdict/difficulty
109
+ output/generation_summary.json # batch totals
110
+ output/resolved_config.yaml # the fully-resolved config that produced this run
111
+ ```
112
+
113
+ Case-type batches (`quality.case_type` set) get `_<type>` appended to the
114
+ folder name, e.g. `case_000001_3b/`.
115
+
116
+ **Every signature box on a multi-box template gets signed, always.** If a
117
+ template has more than one signature box but the sample's case type doesn't
118
+ officially test the extra one (e.g. `1a`/`2a`/generic samples on a 2-box
119
+ template), that box still gets filled with its own correct, genuine
120
+ signature rather than being left blank with no explanation -- tracked in
121
+ `metadata["additional_signatures"]`. This never overrides a case type's own
122
+ deliberate blank/wrong target (a `2a` case's target box still correctly
123
+ stays blank with `NO_SIGNATURE`; only its *unrelated* second box gets the
124
+ extra fill). See "Multi-signature-box logic, audited" below.
125
+
126
+ ## Template providers ("works with both template approaches")
127
+
128
+ Set via `paths.template_provider` in config or `--set paths.template_provider=...`:
129
+
130
+ - `both` (**default**) -- both pools merged into one id space (`bank_doc_*` + `proc_doc_*`), so a plain `python main.py` shows both template families.
131
+ - `scanned` -- the 9 real scanned bank-document images only.
132
+ - `procedural` -- the 15 matplotlib-drawn documents only (`proc_doc_01`..`proc_doc_15`).
133
+
134
+ `paths.excluded_template_ids` (default `[]`) can pull any scanned template
135
+ out of the pool without touching the source image file -- exclusion is
136
+ id-stable (doesn't renumber/shift any other template's id).
137
+
138
+ Every ground-truth JSON records `template_kind` (`"scanned"` / `"procedural"`)
139
+ at the top level regardless of mode.
140
+
141
+ Both families live under one `templates/` folder:
142
+
143
+ ```
144
+ templates/
145
+ scanned/
146
+ guarantee/ bank_doc_01.png, bank_doc_02.png, bank_doc_03.png
147
+ loan_forms/ bank_doc_04.png, bank_doc_05.png, bank_doc_06.png
148
+ mandate/ bank_doc_07.png, bank_doc_08.png, bank_doc_09.png
149
+ procedural/ proc_doc_01.png .. proc_doc_15.png
150
+ ```
151
+
152
+ `templates/scanned/` holds the 9 real scanned photographs (`paths.image_dir`);
153
+ `templates/procedural/` holds the 15 procedurally-drawn documents, rendered
154
+ once and cached as PNGs the same way a scanned template is a fixed image
155
+ (`paths.cache_dir`) -- delete it and it's regenerated automatically on the
156
+ next run, from the drawing code in `src/templates/procedural_template_provider.py`.
157
+
158
+ ## Difficulty calibration profiles
159
+
160
+ Real signature ink occupies a different proportion of the page on the two
161
+ template families, so procedural samples use different smallness/faintness/
162
+ low_texture reference constants (`smallness_reference_area_ratio=0.003`,
163
+ `faintness_dark_ref=165`, `faintness_light_ref=220`,
164
+ `low_texture_reference_stddev=46` -- see
165
+ `src/config/schema.py::DifficultyWeights`) than scanned samples, which keep
166
+ the spec deck's literal generic values (`0.04` / `90` / `210` / `60`) --
167
+ still an accurate fit for scanned templates' own measured population.
168
+
169
+ `faintness_dark_ref` and `low_texture_reference_stddev` were refit
170
+ 2026-08-11 against a fresh 60-sample natural draw of real procedural output.
171
+ The prior `faintness_dark_ref` (184) sat *above* the real median
172
+ `mean_dark_intensity` (180.6), which floored `faintness` to exactly `0.000`
173
+ for every sample measured -- a 20%-weighted component contributing nothing,
174
+ discriminating nothing, for the whole procedural family. Re-anchored to the
175
+ real 10th percentile (~164, rounded to 165): median sample faintness is now
176
+ ~0.29, only the darkest ~13% floor to 0, spread runs to ~0.79 for the
177
+ lightest observed ink. `low_texture_reference_stddev` had never been split
178
+ from the deck-literal default at all, despite the same underlying asymmetry
179
+ existing there too (procedural's vector-rendered ink is naturally less
180
+ textured than scanned's real-photograph ink) -- anchored to the real 90th
181
+ percentile `gray_stddev` (~46.5, rounded to 46), the same treatment already
182
+ given to `smallness_reference_area_ratio`. Verified: re-measured 60 fresh
183
+ procedural samples post-fix -- faintness floors to 0 for only 8/60 (was
184
+ 60/60), low_texture floors to 0 for only 7/60; full 119-test suite still
185
+ passes.
186
+
187
+ This is applied automatically, **per sample**, by `GeneratorContext`
188
+ (`src/generator.py::_difficulty_analyzer_for()`), keyed on each sample's own
189
+ `template_kind()` -- not a config profile you have to remember to select.
190
+ Previously this only applied if the whole run used
191
+ `--config configs/procedural_templates.yaml`, which meant `template_provider:
192
+ both` (or the plain CLI default) silently scored every procedural sample
193
+ against the deck-literal, too-aggressive constants regardless. Fixed: each
194
+ sample now gets the constants that actually fit its template family, even
195
+ within a mixed `both` batch.
196
+
197
+ If you explicitly set any of the 3 reference constants yourself -- a CLI
198
+ `--set difficulty.smallness_reference_area_ratio=...` override, or a whole
199
+ profile like `configs/procedural_templates.yaml` that sets them itself --
200
+ that choice is respected as-is and applied to every sample regardless of
201
+ template kind; the automatic per-kind default only fills in fields you
202
+ didn't already decide for yourself
203
+ (`DifficultyWeights.model_fields_set` is how this is detected).
204
+
205
+ ## Placement calibration
206
+
207
+ Signatures are also pasted at a different scale per template family, same
208
+ per-sample-keyed pattern as difficulty calibration above
209
+ (`GeneratorContext._placement_engine_for()`):
210
+
211
+ - **`min_signature_height_px`**: `24px -> 28px` (shared default -- see
212
+ `src/config/schema.py::PlacementConfig`, moved off a `placement_engine.py`
213
+ module constant so it's config-overridable like every other placement
214
+ knob). Measured 2026-08-11 across 20 real placements per candidate into a
215
+ genuine 18px "sign here" line: 24px -> 15.1% fully-opaque ink, 28px ->
216
+ 16.8%, with a modest overflow cost (median 4% at 28px vs. 0% at 24px);
217
+ 32px was tested and rejected (20%+ median overflow -- escalates much
218
+ faster than the legibility gain).
219
+ - **`placement.scale_factor`**: `1.15` (procedural only; scanned keeps the
220
+ shipped `{min: 0.78, max: 0.95}` range unchanged). Procedural's boxes
221
+ (200x80px) have real headroom scanned's cramped real "sign here" lines
222
+ don't -- 1.1 already gave a meaningful legibility gain (opaque-ink
223
+ fraction 0.238 -> 0.280) with zero overflow; 1.3 starts trading further
224
+ gains for real overflow (6-7%). Scanned was tested too (0.9 through 1.2)
225
+ and showed **no change at all** in that range -- `min_signature_height_px`
226
+ is the actual binding constraint there, not `scale_factor`, so bumping
227
+ scanned's scale_factor would have been a no-op.
228
+
229
+ **A real bug surfaced while implementing this**: the difficulty-calibration
230
+ pattern detects genuine user overrides via `model_fields_set`, which works
231
+ because `configs/default.yaml` never explicitly sets the difficulty
232
+ reference constants. It does explicitly set `placement.scale_factor`
233
+ though (to its own baseline range) -- so applying the same pattern
234
+ unmodified would have silently defeated this recalibration on every single
235
+ run, with or without a real override. Fixed by comparing the resolved
236
+ value against the known shipped baseline directly instead of relying on
237
+ field-presence tracking.
238
+
239
+ Pasting bigger also organically fixed a gap flagged in the earlier
240
+ recalibration work: `smallness_reference_area_ratio=0.003` previously sat
241
+ above even the best 10% of real procedural samples (floor ~0.4, never
242
+ reaching 0). Re-measured post-placement-fix: real median
243
+ `signature_area_ratio` moved 0.00095 -> 0.00180, and `smallness` now
244
+ resolves with genuine spread on its own (8/50 samples reach 0, median
245
+ 0.398) -- no further change to the reference constant was needed.
246
+
247
+ ## Difficulty-score targeting
248
+
249
+ ```bash
250
+ python main.py --count 5 --target-difficulty 0.55
251
+ python main.py --count 5 --target-difficulty 0.55 --difficulty-tolerance 0.03 --difficulty-max-attempts 8
252
+ ```
253
+
254
+ A retry-loop (`case_generation.py::generate_case_at_difficulty()`) built on
255
+ this pipeline's own CLI conventions (argparse, `--set key=value`):
256
+ since the difficulty score is already a continuous `[0, 1]` value (not a
257
+ bucket), you ask for a specific number (`0.55`), not a tier. Retries (`--difficulty-max-attempts`, default 5) with fresh but
258
+ still fully deterministic randomness (`seed_manager.python_rng` derived
259
+ from `f"{sample_index}_difficulty_retry{attempt}"`) until the score lands
260
+ within `--difficulty-tolerance` (default 0.05) of the target; if no
261
+ attempt matches, the closest one is kept and `ground_truth.json`'s
262
+ `difficulty_targeting.matched` is `false` so this is never silently
263
+ misreported. See `GeneratorContext.run_sample_targeting_difficulty()`.
264
+
265
+ ## Before/after degradation preview
266
+
267
+ ```bash
268
+ python main.py --count 5 --save-before-after
269
+ ```
270
+
271
+ Builds a clean-vs-final side-by-side comparison combined with a metrics
272
+ overlay into one image. Writes
273
+ `before_after/<case_id>_before_after.png` per sample -- grouped in one
274
+ shared folder rather than scattered one per case folder, so every sample's
275
+ preview can be flipped through in a row without opening each case folder
276
+ individually:
277
+
278
+ - **BEFORE (left)** -- the clean render (signature placed, before any
279
+ document-level augmentation), annotated with the "what was actually
280
+ measured" overlay: the target box (red), the actual
281
+ signature bbox (yellow), its detected ink polygon (green), and any
282
+ cosmetic field boxes (cyan = actual, orange = target cell), plus a
283
+ legend, a **difficulty breakdown** (`value x weight = contribution` per
284
+ component, e.g. `misplacement: 0.464 x 0.35 = 0.162`), and an
285
+ **augmentation-applied** panel listing every effect this sample rolled --
286
+ ON with its strength, or off.
287
+ - **AFTER (right)** -- the final look, labeled with a one-line summary of
288
+ which effects actually fired (`"AFTER - 2 effect(s): contrast,
289
+ scanner_noise"`).
290
+
291
+ Every case in `data/showcase_dataset/` has one of these pre-generated.
292
+ Distinct from the existing `--save-visualization` (a single matplotlib-based
293
+ multi-panel figure -- crop, ink mask, bar chart -- on the FINAL image only,
294
+ no before/after comparison); the two can be used together. See
295
+ `src/visualization/before_after_preview.py`.
296
+
297
+ ## Cosmetic (non-scored) fields
298
+
299
+ `document.enable_cosmetic_fields: true` (**on by default**) fills in
300
+ name/company-name and, on procedural templates, the fuller SWIFT/NRIC/UEN/
301
+ amount set too -- never read by `DifficultyAnalyzer` or any scored metric.
302
+
303
+ - **Procedural templates**: full field set at exact coordinates (known
304
+ from the drawing spec) -- see `src/cosmetics/cosmetic_fields.py`.
305
+ - **Scanned templates**: a best-effort name/company-name field on 7 of the
306
+ 9 templates, found via the same CV line-detection technique already used
307
+ for signature-box auto-detection (search a hand-picked region for the
308
+ blank line/table-cell, draw text against it) -- see
309
+ `src/cosmetics/scanned_cosmetic_fields.py`. `bank_doc_01`/`bank_doc_02`
310
+ are stock photos with fabricated content already baked into the image
311
+ (bracketed `[PLACEHOLDER ...]` text, or an already-fully-filled specimen
312
+ letter) and are left as-is -- there's no genuine blank field to fill
313
+ without inpainting over existing print.
314
+
315
+ ## Template exclusion (mechanism exists, nothing excluded by default)
316
+
317
+ `paths.excluded_template_ids` (config list, default `[]`) can pull a
318
+ scanned template out of the pool without touching the source image file --
319
+ exclusion is id-stable (`TemplateLoader(excluded_ids=...)`), so pulling one
320
+ id never renumbers/shifts any other template's id. Every discovered
321
+ template is included by default.
322
+
323
+ ## Scanned cosmetic fields: field semantics, not just position
324
+
325
+ `bank_doc_09`'s "Company Name" table cell (under `ENTITY DETAILS`, next to
326
+ `Registration Number`) was pasting the *signer's own personal name* into
327
+ it (e.g. "Nancy Ivanov" under a column literally headed "Company Name") --
328
+ the position was correct, but the wrong *kind* of text. Every other
329
+ scanned field really is a personal-name field ("Print Name:", "Name of
330
+ Account Holder:", "Mr./Ms. ___") and correctly gets the signer's name;
331
+ `company_name` is the one exception and now generates an actual company
332
+ name (`generate_company_name()`, seeded, same generator procedural
333
+ templates already used) instead. See `src/cosmetics/scanned_cosmetic_fields.py`'s
334
+ `_GENERATED_FIELD_TEXT` dispatch table.
335
+
336
+ ### Cosmetic-field coordinates are pre-measured and cached, like signature boxes
337
+
338
+ `signature_boxes` in `annotations/<template_id>.json` have always been
339
+ hand-measured once and just read at generation time -- no per-sample image
340
+ analysis. Cosmetic-field coordinates (where the "Name"/"Company Name" text
341
+ gets drawn on a scanned template) now work exactly the same way, via a new
342
+ `cosmetic_fields` array on the same annotation file:
343
+
344
+ ```json
345
+ "cosmetic_fields": [
346
+ {"field": "company_name", "mode": "below_line", "line_x": 74, "line_y": 164, "line_w": 303}
347
+ ]
348
+ ```
349
+
350
+ **How each coordinate is measured** (`scripts/detect_cosmetic_field_coordinates.py`):
351
+
352
+ 1. A generous *search region* is hand-picked per field, once, by eye (a
353
+ `(x0, y0, x1, y1)` rectangle expressed as fractions of the page's
354
+ width/height -- see `FIELD_REGION_RECIPES` in that script). This step
355
+ only needs to be roughly right, not precise.
356
+ 2. Within that region, the *same* classical-CV horizontal-line detector
357
+ already used to auto-detect signature boxes when no manual annotation
358
+ exists (`src/templates/box_detector.py`: binarize, morphological
359
+ opening with a wide horizontal kernel to isolate long thin dark runs,
360
+ merge nearby fragments) finds the actual blank underline or table
361
+ row-divider pixel-exactly. Deterministic given the same image, so it
362
+ only ever needs to run once per template.
363
+ 3. The result (`field`, `above_line`/`below_line` mode, and the detected
364
+ line's exact `x`/`y`/`width`) is written into the template's
365
+ `annotations/<id>.json`, alongside `signature_boxes`.
366
+
367
+ Run it manually (`python scripts/detect_cosmetic_field_coordinates.py
368
+ [--force]`) after adding a new scanned template or if a template image
369
+ changes -- it is **not** run as part of normal generation. At generation
370
+ time, `fill_scanned_cosmetic_fields()` (`src/cosmetics/scanned_cosmetic_fields.py`)
371
+ just reads `template.annotation.pages[N].cosmetic_fields` directly, the
372
+ same way signature-box placement already reads `signature_boxes` -- one
373
+ measurement, reused on every sample, instead of re-running CV detection
374
+ per call.
375
+
376
+ ### One "Print Name:" field per signatory, not one per template
377
+
378
+ `bank_doc_03`, `04`, `05`, `06`, and `08` each have **two** signature
379
+ boxes, and (confirmed by visually cropping every candidate line in each
380
+ template) each box's signatory has their *own* "Print Name:"-style line --
381
+ not one field shared by both parties. The original single-field-per-template
382
+ recipe only ever measured one of the two, so the second signatory's name
383
+ was always left blank regardless of what filled the first. Each field now
384
+ carries an optional `box_id`, tying it to the `signature_boxes[].id` whose
385
+ signer's name belongs on that line:
386
+
387
+ ```json
388
+ "cosmetic_fields": [
389
+ {"field": "name_sig1", "mode": "above_line", "line_x": 606, "line_y": 464, "line_w": 136, "box_id": "sig1"},
390
+ {"field": "name_sig2", "mode": "above_line", "line_x": 828, "line_y": 464, "line_w": 136, "box_id": "sig2"}
391
+ ]
392
+ ```
393
+
394
+ Fields with no `box_id` (e.g. `bank_doc_09`'s `company_name`) are
395
+ entity-level, not tied to one signer, and are unaffected.
396
+
397
+ Because a companion/additional signer's identity usually isn't resolved
398
+ until *after* the primary box's cosmetic fields are already filled (see
399
+ `_run_case_sample`/`_run_generic_sample` in `src/generator.py`),
400
+ `_maybe_fill_cosmetic_fields()` only ever fills fields with no `box_id` or
401
+ tied to the box it's given; a second pass, `_fill_box_cosmetic_field()`,
402
+ fills the companion/extra box's own field once that signer's display name
403
+ is known -- so both parties' names land on their own lines, matching
404
+ whichever signature actually got pasted into their box.
405
+
406
+ While narrowing `bank_doc_03`'s search region to find its second field, a
407
+ related pre-existing bug surfaced: its *first* field had been landing on
408
+ the "By:" line instead of "Print Name:" (both lines sit in the same
409
+ generous search region, and `_find_line_in_region()`'s "pick the widest
410
+ line" heuristic happened to prefer "By:"). Fixed by narrowing that
411
+ template's region to the exact "Print Name:" line's y-band.
412
+
413
+ ## Legible ink on short signature lines
414
+
415
+ A real "sign here" line on a scanned bank form is often only ~15-25px tall
416
+ at this pipeline's working resolution (e.g. `bank_doc_08`'s "Signature of
417
+ Bank Manager:" line is 22px tall) -- placing a signature stamp from a
418
+ typical few-hundred-pixel-tall dataset source crop into a box that short
419
+ forces a large downscale, sometimes 10-20x. `cv2.resize`'s `INTER_AREA`
420
+ area-averages a multi-pixel-wide source stroke down into a sub-pixel-wide
421
+ destination stroke, which is the geometrically correct way to downscale --
422
+ but it also dilutes that stroke's alpha well below fully opaque across most
423
+ of its footprint. A signature that was solidly inked in its source crop
424
+ could come out mostly under 30-40% opacity: a faint gray smudge rather than
425
+ a smaller-but-still-dark signature (real signatures squeezed onto a short
426
+ line stay dark, just compact).
427
+
428
+ `PlacementEngine.place()` (`src/placement/placement_engine.py`) now applies
429
+ a gamma correction to the placed alpha channel after any real downscale --
430
+ every partially-transparent pixel gets boosted non-linearly (alpha^gamma,
431
+ gamma<1), which is what actually restores legibility (a plain linear
432
+ rescale matching the *source's own* percentile was tried first and wasn't
433
+ enough: even the source crop's own mid-tone pixels are already somewhat
434
+ soft/anti-aliased from the original scan). Gamma self-scales with
435
+ `final_scale` (the actual resize factor) -- a sample that barely shrank
436
+ gets ~no correction (gamma near 1, alpha^1 = alpha); one squeezed 10-20x
437
+ onto a short line gets the strongest correction (gamma floor 0.18).
438
+ Applied to alpha only, never RGB, so ink *color* is unaffected -- this
439
+ makes thin/faint strokes fully opaque sooner, not darker in hue. Regression
440
+ test: `test_aggressive_downscale_onto_a_short_box_keeps_ink_dark` in
441
+ `tests/test_placement_engine.py`.
442
+
443
+ ## Post-render ink detection: not fooled by the template's own printed content
444
+
445
+ `GeometryAnalyzer` (`src/analysis/geometry_analyzer.py`) redetects each
446
+ sample's signature from the rendered pixels alone (deliberately -- a real
447
+ verifier only ever sees the final scanned document, not ground-truth
448
+ placement coordinates). Four contamination sources were found and fixed
449
+ across three review passes -- the first two via a user-reported case
450
+ (`bank_doc_08`'s Bank Manager signature, a genuine PASS, whose metrics
451
+ preview showed a bounding box/polygon spanning almost the entire printed
452
+ line instead of just the ink), the third via a broader anomaly scan across
453
+ the whole showcase dataset after a follow-up "the metrics still look kinda
454
+ off" report, the fourth via the interactive UI's degradation lever
455
+ surfacing a sample that visually looked heavily degraded yet scored "easy":
456
+
457
+ 1. **The printed "sign here" rule line itself.** A pasted signature very
458
+ often sits directly on top of, and touches, that line -- 8-connectivity
459
+ then merges the line and the ink into ONE component, whose bounding box
460
+ spans the line's full width. `_strip_printed_rule_line_pixels()` runs a
461
+ *narrow, absolute-width* (20px, not proportional to the crop -- a
462
+ signature crossing the middle of the line splits it into two shorter
463
+ sub-segments, and a proportional kernel could exceed either half's
464
+ length) horizontal morphological opening to isolate straight
465
+ single-row runs, then only subtracts ones whose overall connected-component
466
+ height stays <=3px (so a genuinely tall block of ink -- thick
467
+ handwriting, or a filled test fixture -- survives; only truly thin,
468
+ line-like structures get erased).
469
+ 2. **Printed field labels near the box** (e.g. "Bank Manager:"). A tight
470
+ detection pass expands slightly past the annotated box edge, and on a
471
+ short box that expansion can reach into adjacent label text. Fixed by
472
+ optionally diffing against the clean, unsigned template image
473
+ (`analyze(..., baseline_image_rgb=...)`) -- pixels already dark in the
474
+ baseline (permanent printed content) are never counted as new ink. The
475
+ baseline is known in advance the same way a real production pipeline
476
+ knows its own form layouts, not this run's specific signature
477
+ placement, so this isn't using ground truth the way a real verifier
478
+ couldn't. Originally skipped automatically when the sample underwent a
479
+ page-level geometric transform (`perspective`/`page_tilt`/`crumple`)
480
+ that would misalign a pixel-for-pixel diff -- see contamination source
481
+ 4 below for why that gap itself needed fixing.
482
+
483
+ Net effect on the reported case: `box_coverage_pct` dropped from a
484
+ contamination-inflated 56% to an accurate ~10%, and the detected polygon
485
+ now tightly traces just the actual ink.
486
+
487
+ 3. **A printed caption below a deliberately BLANK target box** (2a/5a case
488
+ types -- `_BLANK_CASE_TYPES`). The widened retry pass (`analyze()`'s
489
+ second, more generous search) exists to chase a signature that was
490
+ deliberately misplaced far from its own box -- but a 2a/5a target box
491
+ has no signature at all, so there's nothing to chase, and on a sample
492
+ where `baseline_image_rgb` had to be omitted (a geometric transform was
493
+ applied, so a pixel diff would misalign), the widened search had no
494
+ baseline to exclude already-printed content with. Caught a printed
495
+ role-caption ("1. SIGNATURE: AUTHORIZED REPRESENTATIVE *") sitting just
496
+ below the blank box and reported it as a wildly-misplaced signature
497
+ (`distance_from_box_center_px` ~90-130px, `margin_utilization` >2).
498
+ Fixed with `analyze(..., expect_blank=True)`, passed whenever the
499
+ caller already knows from ground truth that this target has no
500
+ signature (`target_rgba is None` in `_run_case_sample`) -- skips the
501
+ widened retry entirely, so a genuinely blank box just reports
502
+ `detected: false` and zeroed metrics, exactly as it should.
503
+
504
+ 4. **Whole blocks of printed text, whenever `perspective`/`page_tilt`/
505
+ `crumple` actually fired.** Found via the interactive UI's degradation
506
+ lever: a heavily rotated/noisy "Explore from clean" preview scored
507
+ 16% ("easy") despite looking obviously harder to read than the clean
508
+ original (48%, "medium"). Root cause: contamination source 2's
509
+ baseline-diff exclusion was skipped entirely whenever a geometric
510
+ transform fired (a rotated page no longer lines up pixel-for-pixel with
511
+ the un-rotated baseline template), so the ONLY defense against printed
512
+ content near the box was gone -- and `_strip_printed_rule_line_pixels()`
513
+ only catches thin rule *lines*, not printed paragraph *text*. On one
514
+ measured sample, a `page_tilt` of well under 1 degree was enough for a
515
+ nearby paragraph heading to rotate into the search box's expanded
516
+ region and get counted wholesale as "ink": detected pixels jumped from
517
+ 545 (the real signature) to 13,993, `mean_dark_intensity` dropped from
518
+ 175 to 107 (consistent with picking up dense black print), and the
519
+ artificially large, well-centered "blob" read as LESS misplaced and
520
+ LESS small than the real, small, genuine signature -- the opposite of
521
+ what a harder-to-read page should score. Fixed at the source instead of
522
+ working around it: `AugmentationEngine.apply_to_document()` now accepts
523
+ an optional `baseline_image`, and whenever perspective/page_tilt/crumple
524
+ actually apply, the identical transform (same warp matrix / same
525
+ `map_x`,`map_y` remap field) is also applied to the baseline companion
526
+ image, so it stays pixel-aligned with the final document and baseline
527
+ exclusion keeps working instead of being skipped. Verified in isolation:
528
+ `page_tilt` alone at maximum strength now detects 552 ink pixels (vs.
529
+ 545 with no tilt at all) -- effectively fixed.
530
+
531
+ **A separate, smaller contamination source was found (not fixed) while
532
+ verifying #4 above:** forcing `scanner_noise`/`salt_pepper`/`dust` on
533
+ simultaneously with everything else -- as the lever's "Explore from
534
+ clean (all effects)" mode deliberately does -- inflates detected ink
535
+ pixels through a *different* mechanism: the geometry pipeline scores a
536
+ signature using the convex hull/bounding box of *every* surviving "ink"
537
+ pixel, so even a few scattered noise specks near opposite corners of the
538
+ search box balloon that box to span nearly the whole region, regardless
539
+ of how little total dark area they cover (measured: ink pixel *count*
540
+ only roughly doubled, but IoU-with-box jumped from 0.11 to 0.82). A
541
+ median-blur denoise pre-pass was prototyped as a fix and rejected: a
542
+ kernel strong enough to meaningfully suppress noise at extreme,
543
+ all-effects-maxed strength (5x5) also cut a genuinely clean sample's
544
+ real ink-pixel count by ~60% (403 -> 157) -- real pen strokes are only a
545
+ few pixels wide, the same order of magnitude as the noise being
546
+ filtered, so the fix would trade real detection accuracy for a mostly
547
+ synthetic problem. A shape/cluster-aware alternative (discard ink
548
+ components far from the "main" cluster) was also rejected: it directly
549
+ conflicts with this same section's existing, deliberate design (the
550
+ hull is meant to *bridge* a genuine flourish far from the main strokes,
551
+ not discard it) and would need real testing against genuine faint/thin
552
+ signatures to avoid new false negatives -- a bigger investment than
553
+ warranted, since at realistic (non-maxed) strengths the effect is mild
554
+ (measured: only ~8% ink-pixel inflation at strength 0.3, vs. 25x for the
555
+ page_tilt bug above). Left as a documented limitation of the "Explore
556
+ from clean (all effects)" lever mode specifically -- which stacks all 14
557
+ effects simultaneously, something real generation never does (each
558
+ effect fires independently/probabilistically) -- rather than changing
559
+ detection logic; the lever now shows an explicit warning above 25% in
560
+ that mode instead of a silently-misleading score.
561
+
562
+ ## Placement floor: 28px, not the deck's 16px -- resolved by measurement, not forced
563
+
564
+ A full line-by-line audit against `Metrics_Thresholds_Formulas.pptx`'s
565
+ "Threshold reference sheet" slide found every constant in this pipeline
566
+ matches exactly except one: `min_signature_height_px`
567
+ (`src/config/schema.py::PlacementConfig`, formerly a
568
+ `placement_engine.py` module constant) is 24px-then-28px in code vs. the
569
+ deck's 16px. Rather than silently override it to match the deck, or leave
570
+ it unexplained, this was resolved empirically: a real signature was placed
571
+ into a genuine short 18px "sign here" line (the deck's own "real lines are
572
+ 12-20px tall" range) at each floor value and the result measured directly.
573
+
574
+ | Floor | Placed height | Fully-opaque ink (alpha > 200/255) |
575
+ |---|---|---|
576
+ | 16px (deck) | 16px | 12.3% |
577
+ | 24px (first pass) | 22px | 15.1% |
578
+ | 28px (current, see "Placement calibration" above) | -- | 16.8% |
579
+
580
+ Each step up measurably preserves more solid, legible ink -- `cv2.INTER_AREA`
581
+ dilutes a multi-pixel source stroke's alpha the smaller the downscale
582
+ target gets, and 16px sits deepest in that dilution zone (the same artifact
583
+ already described in this file's placement-scaling comments). 32px was
584
+ also tested (see "Placement calibration" above) and rejected -- overflow
585
+ past 28px escalates much faster than the legibility gain. The constant's
586
+ own definition cites this measurement directly, the same way
587
+ `DifficultyWeights`' smallness/faintness constants document their own
588
+ deliberate deviation from the deck's generic values.
589
+
590
+ This was one finding from a full line-by-line audit (every formula/
591
+ threshold/case-type in the deck checked against this codebase) -- 52 of 53
592
+ checks matched exactly; this was the one documented deviation, resolved by
593
+ measurement rather than left silently unexplained.
594
+
595
+ ## The ink polygon on the preview tints the real ink, not the hull
596
+
597
+ Several rendering styles were tried this session, in order, each fixing
598
+ the previous one's complaint:
599
+
600
+ 1. **Hull, outline only** (the original design). A convex hull "bridges"
601
+ gaps between separate ink blobs (letters, a flourish far from the main
602
+ strokes) with straight lines -- the bridging edges visibly don't hug
603
+ the ink wherever they cross a gap. Reported: "the polygon doesn't even
604
+ cover the signature."
605
+ 2. **Hull, translucent fill.** Fixed #1 (the ink is always inside the
606
+ tinted region), but a filled hull shades in every gap it bridges too.
607
+ Reported, correctly: "shouldn't [it] only cover surround the signature
608
+ not whitespaces."
609
+ 3. **Hull outline + `cv2.approxPolyDP` simplification** (a vertex-count
610
+ reduction on the hull polygon). Still visibly failed to "hug" a
611
+ wide/gappy signature on direct re-inspection, since it's still
612
+ fundamentally the same convex hull -- a known tradeoff of hull-based
613
+ rendering that just isn't obvious on a small, compact signature.
614
+ 4. **`display_contours`, translucent fill** (current, final). A
615
+ purely-visual field on `GeometryResult`: one real boundary contour per
616
+ disjoint ink blob, via `cv2.findContours` on the exact mask used for
617
+ scoring (mildly dilated first, on a copy, just to merge one letter's
618
+ own stroke fragments into a single blob -- nowhere near enough to
619
+ bridge a real gap between separate letters). Filling each blob's own
620
+ contour tints only actual ink, never the whitespace between strokes,
621
+ and -- unlike an *outline* of these same contours (tried and rejected
622
+ earlier: for a dense cursive/Devanagari signature it visually resembled
623
+ a second overlaid signature) -- a fill has no separate line art to be
624
+ confused with handwriting.
625
+
626
+ `signature_polygon_px`/`convex_hull_px` in `ground_truth.json` remain the
627
+ convex hull (with the `approxPolyDP` simplification from step 3 kept, as a
628
+ minor genuine improvement to the scoring polygon's vertex count) -- every
629
+ coverage metric (`box_coverage_pct`/IoU/`containment_pct`/`margin_utilization`)
630
+ still comes from the hull, unchanged. `display_contours` is additive and
631
+ explicitly documented as "never used in scoring." Both
632
+ `before_after_preview.py` and `visualizer.py` (`--save-visualization`)
633
+ fill `display_contours` when present, falling back to filling the hull for
634
+ any geometry dict that predates this field. This was a deliberate choice,
635
+ made after comparing several rendering styles directly -- prioritizing an
636
+ accurate, non-misleading preview over hull-only visual simplicity.
637
+
638
+ ## Metrics preview shows every signed box, not just the scored one
639
+
640
+ A multi-box template's companion/additional-signature boxes are genuinely
641
+ rendered with real ink but aren't part of a sample's *scored* target
642
+ (geometry/difficulty stay keyed to the one target box the rest of the
643
+ pipeline scores) -- previously the before/after metrics preview only ever
644
+ overlaid that one box, leaving a second signature elsewhere on the page
645
+ unexplained. `compute_extra_signature_overlays()` (`src/generator.py`)
646
+ re-runs `GeometryAnalyzer` (view-only, never for scoring) on every other
647
+ signed box for the preview, and `save_before_after_preview()`'s
648
+ `extra_boxes` param draws them in a distinct purple/pink palette, labeled
649
+ by role (e.g. "Account Holder (other box)") instead of full re-scoring.
650
+
651
+ ## Reference-signer index
652
+
653
+ `scripts/generate_showcase_dataset.py` also writes
654
+ `data/showcase_dataset/reference_signers_index.json` -- every distinct
655
+ name/role that appears anywhere in the dataset (as a primary signer,
656
+ companion, or additional/other-box signer), with which signer identity
657
+ backs it and where its ink actually comes from:
658
+
659
+ ```json
660
+ {
661
+ "signer_group": "signet_0009",
662
+ "display_name": "John Thompson",
663
+ "script": "latin",
664
+ "roles": ["Bank Manager"],
665
+ "appears_in": [{"case_id": "case_001_1a_scanned", "role": "Bank Manager", "as": "primary", "template": "bank_doc_08"}],
666
+ "reference_images": ["data/raw/signet/train/009/009_24.PNG"]
667
+ }
668
+ ```
669
+
670
+ `signer_group` is the pipeline's internal, deterministic identity key
671
+ (`src/verification/registry.py::SignerIdentityRegistry`) -- `display_name`
672
+ is the human-readable name derived from it, stable across every case that
673
+ identity appears in. `appears_in` distinguishes *how* that identity was
674
+ used in each case (`"primary"` = the case's own target signer, `"companion"`
675
+ = the officially-tested second signer for `1c`/`4a`/`4b`/`5a`/`5b`,
676
+ `"additional"` = an unrelated-but-still-genuine extra box fill -- see
677
+ "Every signature box on a multi-box template gets signed" above).
678
+ `reference_images` collects every distinct source image path used for that
679
+ identity across the whole dataset (not just one case), each already copied
680
+ into its case folder too (`*_reference_signature.png` /
681
+ `*_companion_signature.png` / `*_<box_id>_signature.png`) so nothing here
682
+ depends on `data/raw/` actually being present to inspect a specific case.
683
+
684
+ ## Multi-signature-box logic, audited (and a real gap fixed)
685
+
686
+ Every case type that needs two signature boxes (`1c`, `4a`, `4b`, `5a`,
687
+ `5b` -- see `CASE_TABLE`'s `fill_companion` flag) was verified end-to-end
688
+ across both scanned and procedural 2-box templates: template selection
689
+ correctly restricts to templates with >=2 boxes, the companion box is
690
+ always filled with its own correct authorized signer, and the target box
691
+ gets exactly what its case type specifies (`1c`: also correct; `4a`: the
692
+ *companion's* signer, in the wrong box, same form; `4b`: a signer
693
+ authorized on a *different* template entirely; `5a`: left blank; `5b`: an
694
+ unrelated wrong signer) -- confirmed both from the ground-truth JSON
695
+ (`expected_signer_group` vs `actual_signer_group` vs the companion's
696
+ group) and from rendered images.
697
+
698
+ **Gap found and fixed:** case types that *don't* officially test a second
699
+ signer (`1a`, `1b`, `1d`, `2a`, `2b`, `3a`, `3b` -- `fill_companion=False`)
700
+ and plain generic (no `quality.case_type` at all) samples were leaving any
701
+ *extra* box on a multi-box template completely blank, with no verdict or
702
+ reject_code explaining why -- it just looked like a broken render. Fixed
703
+ in both `_run_generic_sample` and `_run_case_sample`
704
+ (`src/generator.py`): any signature box beyond the one the sample is
705
+ actually scored/tested against now gets filled with its own correct
706
+ authorized signer too, recorded under the new `additional_signatures`
707
+ metadata field (list of `{box_id, role, signature_id, signer_group,
708
+ source_type, script, ref_path}`) and copied into the case folder as
709
+ `..._<box_id>_signature.png`. This only ever *adds* a properly-signed box
710
+ -- it never overrides a case type's own deliberate blank/wrong-signer
711
+ target (verified: a `2a` case's target box is still correctly blank with
712
+ `NO_SIGNATURE`, while its unrelated second box is now properly signed
713
+ instead of mysteriously empty).
714
+
715
+ One real annotation bug found and fixed in the process: `bank_doc_09`'s
716
+ bundled signature-box annotation (`annotations/bank_doc_09.json`,
717
+ inherited as-is from an earlier revision) placed both boxes 27px too
718
+ high -- inside the paragraph text above the "First/Second Authorised
719
+ Signatory" line instead of on it. Found by rendering a sample and looking
720
+ at where the ink actually landed (not visible from the annotation JSON
721
+ alone), confirmed against the template's real line position via the same
722
+ CV line-detector `scanned_cosmetic_fields.py` uses, and corrected.
723
+
724
+ ## A bug worth knowing about (fixed): reference signature images
725
+
726
+ An earlier version of the per-case output copied the reference signature
727
+ file with `shutil.copy2` under a `*_reference_signature.png` name. For
728
+ CEDAR/SigNet sources (already `.png`) that's fine; for **BHSig260 sources
729
+ (`.tif` files)** it silently produced a file containing real TIFF bytes
730
+ under a `.png` name -- any standards-compliant image viewer correctly
731
+ refuses to open it. Fixed by decoding with Pillow and re-saving as a real
732
+ PNG (`src/generator.py::_export_reference_signature`) -- every reference/
733
+ companion signature image is now guaranteed to actually be what its
734
+ extension claims. Regression test: `tests/test_scanned_cosmetic_fields.py::test_export_reference_signature_produces_a_real_png_from_a_tiff_source`.
735
+
736
+ ## Usage
737
+
738
+ ```bash
739
+ pip install -r requirements.txt
740
+
741
+ # The flagship demo -- all 12 case types x both template families, 24 cases
742
+ python scripts/generate_showcase_dataset.py
743
+ # (already generated once into data/showcase_dataset/ -- rerun to regenerate)
744
+
745
+ # Default: both template pools, deck-literal difficulty constants
746
+ python main.py --count 20 --seed 1
747
+
748
+ # Procedural only, recalibrated difficulty constants
749
+ python main.py --config configs/procedural_templates.yaml --count 20 --seed 1
750
+
751
+ # Scanned only
752
+ python main.py --set paths.template_provider=scanned --count 20 --seed 1
753
+
754
+ # A specific case type (see src/verification/case_types.py for all 12)
755
+ python main.py --config configs/procedural_templates.yaml --set quality.case_type=3b --count 10
756
+
757
+ # Request a specific difficulty score, with a before/after preview
758
+ python main.py --count 5 --target-difficulty 0.55 --save-before-after
759
+ ```
760
+
761
+ ## Interactive UI
762
+
763
+ ```bash
764
+ streamlit run streamlit_app.py
765
+ ```
766
+
767
+ A local browser UI (`streamlit_app.py`) for one-off, customizable document
768
+ creation -- pick template provider/id (picking a specific template shows a
769
+ live preview of its blank form, with its document type and signature-box
770
+ roles, right below the dropdown, before you even generate anything), case
771
+ type (with its own description/verdict shown inline), a natural or
772
+ targeted difficulty (as a plain percentage, not a raw `0.0-1.0` score), a
773
+ **difficulty calibration** toggle (see below), signer name/role overrides,
774
+ cosmetic fields on/off, and a seed, then generate and immediately see the
775
+ rendered document, verdict, and a plain difficulty percentage (the
776
+ easy/medium/hard tier is kept in the ground-truth data for case-type
777
+ validation, but isn't the headline UI number anymore -- a caption under
778
+ the percentage shows it for anyone who wants it), and (organized into tabs
779
+ to keep the page scannable):
780
+ - **Difficulty & degradation** -- what actually built the difficulty score
781
+ (a labeled progress bar per weighted component --
782
+ misplacement/whitespace/faintness/smallness/low texture, each with a
783
+ one-line plain-English explanation) and which degradation effects
784
+ actually, randomly fired for this specific sample (not just which were
785
+ possible). Difficulty calibration is now automatic per sample (see
786
+ "Difficulty calibration profiles" above) -- procedural samples get
787
+ `smallness_reference_area_ratio=0.003` / `faintness_dark_ref/light_ref=
788
+ 165/220` / `low_texture_reference_stddev=46` and scanned samples keep the
789
+ deck-literal `0.04` / `90` / `210` / `60`, without needing to opt in. The
790
+ sidebar's **"Force deck-literal reference for ALL templates"** checkbox
791
+ (off by default) is only there to deliberately turn this OFF -- forcing
792
+ every sample, including procedural ones, back onto the deck-literal
793
+ constants that peg smallness/faintness near their ceiling for almost any
794
+ real signature (real signature area is only ~0.2-1.5% of the page vs. the
795
+ deck's 4% "normal"), e.g. to compare
796
+ against pre-recalibration scores.
797
+ - **Interactive lever** -- drag a slider to watch the same clean render
798
+ pass through progressively stronger document degradation live, without
799
+ re-running the whole pipeline. Three modes: *match this document*
800
+ (default) scales only the effects that actually fired, together, from
801
+ 0% (clean) to 100% (this document's real per-effect strengths -- so it
802
+ closely matches the Before/After tab, modulo the lever using its own
803
+ seed key for random-variant choices like stain shape/shadow corner);
804
+ *adjust individually* gives one slider per document-level effect,
805
+ defaulted to what actually fired (0% if it didn't), so you can push any
806
+ single effect harder/weaker/on independently of the rest; *explore from
807
+ clean* ignores what actually fired entirely and forces all 14 effects on
808
+ together at one shared strength, starting from clean -- a generic
809
+ worst-case stress-test (this is what the original version of the lever
810
+ always did, kept as its own mode). Both the clean and degraded preview
811
+ now show a live `difficulty_score`, freshly re-detected from the
812
+ pictured image with the same `GeometryAnalyzer`/`DifficultyAnalyzer` the
813
+ real pipeline uses -- it updates as you drag, but is computed
814
+ independently of (and can legitimately differ from) the "Difficulty"
815
+ badge at the top of the page, which reflects the actual generated
816
+ document. The lever only covers the 14 whole-page effects
817
+ (`apply_to_document()`) -- the 6 signature-only effects (rotation,
818
+ stroke thickness, ink bleed/fading, blur) run earlier, on the signature
819
+ snippet alone before it's pasted onto the page, so they're already baked
820
+ into "Clean" as plain pixels and can't be toggled here; a warning banner
821
+ surfaces exactly which of them fired (if any) for the current sample, so
822
+ the gap is visible rather than silent.
823
+ - **Before / After (real)** -- the saved before/after preview PNG, with a
824
+ note that BEFORE's colored boxes are a measurement diagnostic
825
+ (target/detected/cosmetic-field boxes) and AFTER deliberately has no
826
+ overlay, since it's meant to show what a real reviewer/scanner would see.
827
+ - **Ground truth & export** -- the full ground-truth JSON, the equivalent
828
+ `python main.py --set ...` CLI command (every control is a thin wrapper
829
+ over a config override the CLI already accepts, so anything built in the
830
+ UI is exactly reproducible from the command line), and "Save this case",
831
+ which writes the document/ground-truth/reference-signature into
832
+ `data/ui_generated/<name>/`, in the same per-case-folder shape as
833
+ `output/`/`data/showcase_dataset/`.
834
+
835
+ ## Tests
836
+
837
+ ```bash
838
+ python -m pytest -q # 119 passed, ~2-3 min
839
+ ```
840
+
841
+ Covers formula correctness, placement, augmentation, validation, all 12
842
+ case types, end-to-end reproducibility, `ProceduralTemplateProvider`,
843
+ `CompositeTemplateProvider`, the additive signature sources, and the two
844
+ difficulty-calibration profiles.
unified_pipeline/data/raw/signet/train/060_forg/01_0111060.PNG ADDED

Git LFS Details

  • SHA256: 31496c6d68074637ca36a0c6bb346d6678551afe2803f339bef367091574d9a7
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
unified_pipeline/data/raw/signet/train/060_forg/01_0121060.PNG ADDED

Git LFS Details

  • SHA256: acb9e77c2a4aeaf4600cf5907ce5413344f13f00459048c45e9c9b42e7c71819
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
unified_pipeline/data/raw/signet/train/060_forg/01_0126060.PNG ADDED

Git LFS Details

  • SHA256: 96cb14811ad8ed05325c923286559d627054d2f615b809d562c1b676207ce7fe
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
unified_pipeline/data/raw/signet/train/060_forg/02_0111060.PNG ADDED

Git LFS Details

  • SHA256: 55f2e683a222502f51fec263ea8fb00c98b91389a03f0406877b47b3dc0fb3b3
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
unified_pipeline/data/raw/signet/train/060_forg/02_0121060.PNG ADDED

Git LFS Details

  • SHA256: 5858e02eaeda90aca615c8dafd425737f2f319e393cd506278b757271b98b40c
  • Pointer size: 131 Bytes
  • Size of remote file: 173 kB
unified_pipeline/data/raw/signet/train/060_forg/02_0126060.PNG ADDED

Git LFS Details

  • SHA256: f8cffe194d22603bd6d68ea0d9945f5b72890e2ab67675956ee9729ebf078ce6
  • Pointer size: 131 Bytes
  • Size of remote file: 265 kB
unified_pipeline/data/raw/signet/train/060_forg/03_0111060.PNG ADDED

Git LFS Details

  • SHA256: 3da30dd830c67ba350d57cbd8cc6d50749eee11a92d4476d285d959e8740e8ca
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
unified_pipeline/data/raw/signet/train/060_forg/03_0121060.PNG ADDED

Git LFS Details

  • SHA256: d3e667c430d9eb878e2ced5e7b6ff47c4f9381368866511cdd61386e60b96025
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
unified_pipeline/data/raw/signet/train/060_forg/03_0126060.PNG ADDED

Git LFS Details

  • SHA256: db6d7545a5e2f3ae6e9c311daa04bfabf735946f576aa657a410e6712cff2f2b
  • Pointer size: 131 Bytes
  • Size of remote file: 261 kB
unified_pipeline/data/raw/signet/train/060_forg/04_0111060.PNG ADDED

Git LFS Details

  • SHA256: dbbefd8f0241a8f134103438159de09e0a1a55af6f6514447cb87f19724b8d1a
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
unified_pipeline/data/raw/signet/train/060_forg/04_0121060.PNG ADDED

Git LFS Details

  • SHA256: 00295e55830320743ad5decd91912eb28002d44ea3fe53085ac1369091a05c63
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
unified_pipeline/data/raw/signet/train/060_forg/04_0126060.PNG ADDED

Git LFS Details

  • SHA256: da9aedadd72a45fbd1b1b39d3c0ffdb5ca1865f26fbd7e07d3f50ec9ea78c609
  • Pointer size: 131 Bytes
  • Size of remote file: 276 kB
unified_pipeline/data/raw/signet/train/061/01_061.png ADDED

Git LFS Details

  • SHA256: 24ee1233423aee1b1fd0b9b188aaf9d7d35b5d98707c435502d1b0916cc70e0e
  • Pointer size: 130 Bytes
  • Size of remote file: 52.7 kB
unified_pipeline/data/raw/signet/train/061/02_061.png ADDED

Git LFS Details

  • SHA256: 3bf7f2a6433b6ffc93c089b98941ec593593d6475e5f1fda714934bc1bfd7ec6
  • Pointer size: 130 Bytes
  • Size of remote file: 42.3 kB
unified_pipeline/data/raw/signet/train/061/03_061.png ADDED

Git LFS Details

  • SHA256: 98c63f6e32e34d1abd37d25225710c8981a2ccc163b511a889538b6a6804e251
  • Pointer size: 130 Bytes
  • Size of remote file: 39.9 kB
unified_pipeline/data/raw/signet/train/061/04_061.png ADDED

Git LFS Details

  • SHA256: d11c2fec2f7f08c96e4c0b7e27baaca873bf664b29a9fd4cf812bef7cf9fb005
  • Pointer size: 130 Bytes
  • Size of remote file: 44.1 kB
unified_pipeline/data/raw/signet/train/061/05_061.png ADDED

Git LFS Details

  • SHA256: bc96513a511a90aac1078032ff7870c58e31c4ea0a14f490a97d971b327a51d1
  • Pointer size: 130 Bytes
  • Size of remote file: 39.3 kB
unified_pipeline/data/raw/signet/train/061/06_061.png ADDED

Git LFS Details

  • SHA256: 919b0121c9b55cbb1fb24f2d35d5b2f6935b19d16b718d6c8e1a9fc0b9f46beb
  • Pointer size: 130 Bytes
  • Size of remote file: 41.6 kB
unified_pipeline/data/raw/signet/train/061/07_061.png ADDED

Git LFS Details

  • SHA256: b9ec3552b8a9de49728f7177beaf6505095ce327407cd6b4fdf08edbffe7295c
  • Pointer size: 130 Bytes
  • Size of remote file: 41.7 kB
unified_pipeline/data/raw/signet/train/061/08_061.png ADDED

Git LFS Details

  • SHA256: e4811dd029b7f5829fc89437eea6675968abda9d4d87e3e004362d5e4ff08ec8
  • Pointer size: 130 Bytes
  • Size of remote file: 42.4 kB
unified_pipeline/data/raw/signet/train/061/09_061.png ADDED

Git LFS Details

  • SHA256: 7bc5b17d930fd24c32c63e704db711582678be8d4ea916313720ea23463c3489
  • Pointer size: 130 Bytes
  • Size of remote file: 38.7 kB
unified_pipeline/data/raw/signet/train/061/10_061.png ADDED

Git LFS Details

  • SHA256: b722e8c703ce21729273cd65f9d9a77980108f9e68fc03d6ea151ce4cee46813
  • Pointer size: 130 Bytes
  • Size of remote file: 50.7 kB
unified_pipeline/data/raw/signet/train/061/11_061.png ADDED

Git LFS Details

  • SHA256: 64837fc5a63a915211e1487485e3b1903bf06cb64f80b31fc6ca0ca9c23bb295
  • Pointer size: 130 Bytes
  • Size of remote file: 57.1 kB
unified_pipeline/data/raw/signet/train/061/12_061.png ADDED

Git LFS Details

  • SHA256: 469a35b7d78e91d335d83285b80e1be153a173122175e397ccd77fce3173ff03
  • Pointer size: 130 Bytes
  • Size of remote file: 58.7 kB
unified_pipeline/data/raw/signet/train/061_forg/01_0102061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/01_0112061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/01_0206061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/02_0102061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/02_0112061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/02_0206061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/03_0102061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/03_0112061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/03_0206061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/04_0102061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/04_0112061.PNG ADDED
unified_pipeline/data/raw/signet/train/061_forg/04_0206061.PNG ADDED
unified_pipeline/data/raw/signet/train/062/01_062.png ADDED

Git LFS Details

  • SHA256: 97958e22de9d091e0b6263b948feac01b67ec3cf2c4ee0be68270151844e05cf
  • Pointer size: 131 Bytes
  • Size of remote file: 125 kB
unified_pipeline/data/raw/signet/train/062/02_062.png ADDED

Git LFS Details

  • SHA256: d8a50bdc4694dffdf4a09a2e5cabfda7d90761ea86a4858309d22f5c67b0bb95
  • Pointer size: 130 Bytes
  • Size of remote file: 84.6 kB
unified_pipeline/data/raw/signet/train/062/03_062.png ADDED

Git LFS Details

  • SHA256: ea4d90d03a1424880f7f2b9dbdb06fdbe0b8fdc6b6dc54e95a24a3f05bba983b
  • Pointer size: 130 Bytes
  • Size of remote file: 74.7 kB
unified_pipeline/data/raw/signet/train/062/04_062.png ADDED

Git LFS Details

  • SHA256: 7ea4adf0785b2208c85eb4cb5eb0eb2c6787b8b20dab2525ffccab352a0bf6a9
  • Pointer size: 130 Bytes
  • Size of remote file: 92.7 kB
unified_pipeline/data/raw/signet/train/062/05_062.png ADDED

Git LFS Details

  • SHA256: 5a8d66677e02eee1eae7684de2c94fe90995ed35d43c0b3303d72d78c7362888
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB
unified_pipeline/data/raw/signet/train/062/06_062.png ADDED

Git LFS Details

  • SHA256: 3ad82c164dab8580c29076421598dd092088d80372917e0989acfa317ec58cbd
  • Pointer size: 130 Bytes
  • Size of remote file: 65.9 kB
unified_pipeline/data/raw/signet/train/062/07_062.png ADDED

Git LFS Details

  • SHA256: c7527248c5455a852be985e0e9e359fd2b792cc0860b5591d96d4a660047cdc0
  • Pointer size: 131 Bytes
  • Size of remote file: 149 kB
unified_pipeline/data/raw/signet/train/062/08_062.png ADDED

Git LFS Details

  • SHA256: d9978a128b81c80d1edded76f2bb8f3a7181c9f618efd3db0a12dae50d26f430
  • Pointer size: 130 Bytes
  • Size of remote file: 100 kB
unified_pipeline/data/raw/signet/train/062/09_062.png ADDED

Git LFS Details

  • SHA256: 0fb5aaa7821705ed7e3584e50c7b3d2171b59b5a725dacb0314c63857bcdda81
  • Pointer size: 130 Bytes
  • Size of remote file: 79.1 kB
unified_pipeline/data/raw/signet/train/062/10_062.png ADDED

Git LFS Details

  • SHA256: 10beb290f0adcaa7c7ee286ad16b5763b5e5e1cfb2e5cc4df1bf688a9f747091
  • Pointer size: 131 Bytes
  • Size of remote file: 131 kB
unified_pipeline/data/raw/signet/train/062/11_062.png ADDED

Git LFS Details

  • SHA256: b6e748df181d9b76fa67a884ff5457dd2e66a3bcf5edd8773dc86ca10a6433a2
  • Pointer size: 131 Bytes
  • Size of remote file: 181 kB
unified_pipeline/data/raw/signet/train/062/12_062.png ADDED

Git LFS Details

  • SHA256: 64069b6c5bd978e5b93a7694283c9264b4976245f73a8d6bdc343857cc99d085
  • Pointer size: 131 Bytes
  • Size of remote file: 136 kB