inkmap 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inkmap-0.1.4/.claude/skills/inkmap-dev-validation/SKILL.md +111 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/PKG-INFO +70 -11
- {inkmap-0.1.2 → inkmap-0.1.4}/README.md +69 -10
- inkmap-0.1.4/docs/methods-and-results.md +505 -0
- inkmap-0.1.4/docs/methods-and-results.pdf +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/__init__.py +11 -2
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/config.py +15 -0
- inkmap-0.1.2/inkmap/data.py → inkmap-0.1.4/inkmap/corpus.py +35 -17
- inkmap-0.1.4/inkmap/data/commons_marks.json +3458 -0
- inkmap-0.1.4/inkmap/data/skills/inkmap-usage/SKILL.md +222 -0
- inkmap-0.1.4/inkmap/locate/__init__.py +310 -0
- inkmap-0.1.4/inkmap/locate/barcode.py +258 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/synth.py +2 -2
- {inkmap-0.1.2 → inkmap-0.1.4}/pyproject.toml +1 -1
- inkmap-0.1.4/scripts/build_report.py +92 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/scripts/corpus_eval.py +1 -1
- inkmap-0.1.4/scripts/evaluate.py +319 -0
- inkmap-0.1.4/scripts/evaluate_dibco.py +165 -0
- inkmap-0.1.4/scripts/evaluate_text.py +217 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/tests/test_inkmap.py +327 -1
- inkmap-0.1.2/inkmap/assets/commons_qr.json +0 -1514
- inkmap-0.1.2/inkmap/locate/__init__.py +0 -117
- inkmap-0.1.2/inkmap/locate/barcode.py +0 -159
- {inkmap-0.1.2 → inkmap-0.1.4}/.github/workflows/ci.yml +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/.gitignore +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/LICENSE +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/docs/terminology.md +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/docs/validation.md +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/coverage.py +0 -0
- {inkmap-0.1.2/inkmap/assets → inkmap-0.1.4/inkmap/data}/default_qr.txt +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/geometry.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/locate/qr.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/__init__.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/base.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/freeform.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/grid.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/photometry.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/types.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/viz.py +0 -0
- {inkmap-0.1.2 → inkmap-0.1.4}/scripts/validate.py +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inkmap-dev-validation
|
|
3
|
+
description: Read before changing anything in inkmap that could move a number — geometry, coverage rasterisation, refinement, blur estimation, measurement weighting, or a locator — and before regenerating docs/methods-and-results.md or docs/validation.md. Covers the three validation tiers (synthetic ground truth, real-photograph round-trip, corpus detection), how to run each, which numbers live in which document, and the half-pixel convention failures that any of these changes can silently reintroduce. Triggers on "regenerate the benchmarks", "update the validation numbers", "the report is stale", "did that change accuracy", or any edit to coverage.py / geometry.py / models/ / locate/.
|
|
4
|
+
metadata:
|
|
5
|
+
audience: developers
|
|
6
|
+
project: inkmap
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Validating inkmap
|
|
10
|
+
|
|
11
|
+
Three tiers, answering three different questions. Changing anything geometric means
|
|
12
|
+
re-running at least tiers 1 and 2, because the failure mode here is *silent*: a wrong
|
|
13
|
+
mask still looks like a mask.
|
|
14
|
+
|
|
15
|
+
| Tier | Question | Ground truth | Script |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| 1. Synthetic | Is the coverage value *correct*? | exact, by construction | `scripts/validate.py` |
|
|
18
|
+
| 2. Round-trip | Is the geometry right on *real* photos? | an independent decoder | `scripts/evaluate.py` |
|
|
19
|
+
| 3. Corpus | Does detection survive the real world? | none (rates only) | `scripts/corpus_eval.py` |
|
|
20
|
+
|
|
21
|
+
## Tier 1 — synthetic, exact
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python scripts/validate.py > /tmp/validation.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No network, ~2 minutes. Produces the geometric-accuracy, coverage-MAE,
|
|
28
|
+
photometric-bias and blur-estimation tables in `docs/validation.md`.
|
|
29
|
+
|
|
30
|
+
**Why it can be exact:** `synth.render` builds the image *from* the coverage map —
|
|
31
|
+
supersample the model through a known transform, then form the image as
|
|
32
|
+
`255 * (1 - coverage)`. Ground truth and stimulus are the same object, so degradations
|
|
33
|
+
applied afterwards leave the truth correct. `test_scene_ground_truth_is_self_consistent`
|
|
34
|
+
guards that property; if it fails, every number in tier 1 is meaningless.
|
|
35
|
+
|
|
36
|
+
## Tier 2 — round-trip on real photographs
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python scripts/evaluate.py <corpus_root> eval.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`<corpus_root>` holds `qr/`, `barcode/`, `text/` subfolders of real images.
|
|
43
|
+
|
|
44
|
+
**The trick:** no per-pixel ground truth exists for real photographs, so instead sample
|
|
45
|
+
the model through inkmap's *own* fitted transform, re-render it clean, and hand it to a
|
|
46
|
+
decoder. The decoder was not involved in fitting the geometry and has error correction,
|
|
47
|
+
so it does not accept near-misses.
|
|
48
|
+
|
|
49
|
+
**Calibrated sensitivity** (measured, and worth re-checking if you touch sampling):
|
|
50
|
+
round-trip passes at ≤0.25 units of transform error and fails at ≥0.5. So it certifies
|
|
51
|
+
"within a quarter of a unit", not "perfect". Do not oversell it.
|
|
52
|
+
|
|
53
|
+
Also computed: independent-model agreement (our sampled QR matrix vs OpenCV's separate
|
|
54
|
+
rectification) and class separation (background minus foreground grey level, which
|
|
55
|
+
collapses if the mask is misaligned).
|
|
56
|
+
|
|
57
|
+
## Tier 3 — corpus detection rates
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python scripts/corpus_eval.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Fetches and caches ~189 CC-licensed Commons photographs on first run (several minutes;
|
|
64
|
+
Commons rate-limits and the fetcher backs off). Produces the per-locator detection table.
|
|
65
|
+
|
|
66
|
+
## What lives where
|
|
67
|
+
|
|
68
|
+
- `docs/validation.md` — the numbers, all tiers. Regenerate wholesale; do not hand-edit
|
|
69
|
+
a table, or it will drift from the script that claims to produce it.
|
|
70
|
+
- `docs/methods-and-results.md` — the narrative: approach, algorithms, results, limits.
|
|
71
|
+
The published report; also the source of the PDF.
|
|
72
|
+
- `README.md` — only the headline photometric-bias table and the detection rates. If you
|
|
73
|
+
change those numbers, update the README too; there is no automation tying them.
|
|
74
|
+
|
|
75
|
+
Rebuild the PDF after editing the report:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python scripts/build_report.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## The failure this all exists to catch
|
|
82
|
+
|
|
83
|
+
Three half-pixel conventions have to agree, and each is invisible on inspection:
|
|
84
|
+
|
|
85
|
+
1. **Pixel centre vs corner** — OpenCV's integer coordinate `x` is the *centre* of a
|
|
86
|
+
pixel spanning `[x-0.5, x+0.5)`. Rasterising `[x, x+1)` offsets the whole map half a
|
|
87
|
+
pixel.
|
|
88
|
+
2. **The ECC template raster** — a model rendered at `s` px/unit puts template pixel `t`
|
|
89
|
+
over `[t/s, (t+1)/s)`, so its centre is `(t+0.5)/s`.
|
|
90
|
+
3. **Locator corner conventions differ** — OpenCV's QR corners are the last *contained*
|
|
91
|
+
pixel index; zxing-cpp returns the continuous outer boundary.
|
|
92
|
+
|
|
93
|
+
Getting these wrong cost 50× in accuracy during development. **The diagnostic:** if
|
|
94
|
+
refinement error grows with the mark's rotation angle following `R(d) − d`, the residual
|
|
95
|
+
`d` is a coordinate-convention offset, not noise. `test_rotation_does_not_scramble_the_model`
|
|
96
|
+
is the regression test; it is parameterised over eight angles precisely because the bug
|
|
97
|
+
is invisible at 0°.
|
|
98
|
+
|
|
99
|
+
A fourth, related trap: the ECC template needs a rendered clear margin. Blurring a
|
|
100
|
+
template that stops at the mark's edge makes the kernel reflect interior content across
|
|
101
|
+
the border, inventing an edge profile the photograph does not have — ECC then shifts the
|
|
102
|
+
whole fit to match the artefact (~0.45 px).
|
|
103
|
+
|
|
104
|
+
## Before you claim an improvement
|
|
105
|
+
|
|
106
|
+
- Run the full test suite; `tests/test_inkmap.py` is parameterised across model kinds
|
|
107
|
+
(matrix, bars, raster, polygon, glyph) on purpose — a change that helps QR and breaks
|
|
108
|
+
glyphs is not an improvement.
|
|
109
|
+
- Re-run tier 1 and compare tables, not vibes.
|
|
110
|
+
- Check that `measurability` still refuses the cases it should. Making more images
|
|
111
|
+
"measurable" is a regression unless the bias table improved with it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: inkmap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Per-pixel foreground coverage for photographed marks -- QR, barcodes, text, any shape you can model
|
|
5
5
|
Project-URL: Homepage, https://github.com/thorwhalen/inkmap
|
|
6
6
|
Author: Thor Whalen
|
|
@@ -159,24 +159,73 @@ and medical-imaging vocabulary: [docs/terminology.md](docs/terminology.md).
|
|
|
159
159
|
|
|
160
160
|
## How it works
|
|
161
161
|
|
|
162
|
+
```
|
|
163
|
+
photograph ──► locate ──► recover model ──► refine transform ──► rasterise ──► erode
|
|
164
|
+
```
|
|
165
|
+
|
|
162
166
|
1. **Locate** — a cascade of `zxing-cpp`, OpenCV's Aruco-based QR detector and OpenCV's
|
|
163
167
|
classic detector, plus linear barcodes. On 189 real photographs: zxing 74%, aruco
|
|
164
168
|
68%, OpenCV 43%, **union 80%**. For marks nothing can auto-detect, `inkmap.place()`
|
|
165
169
|
takes the corners from you.
|
|
166
170
|
2. **Recover the model** — from a bit-exact rectification where a decoder offers one,
|
|
167
171
|
otherwise sampled from the image. *Never* by re-encoding a decoded payload:
|
|
168
|
-
error-correction level
|
|
169
|
-
re-encoding can silently produce a different pattern.
|
|
172
|
+
error-correction level, mask pattern and code-set choices are not recoverable from
|
|
173
|
+
the text, so re-encoding can silently produce a different pattern.
|
|
170
174
|
3. **Refine** — align the rendered model to the photograph with ECC. Four locator
|
|
171
175
|
corners are four observations; the model has thousands. Takes corner error from
|
|
172
|
-
~1 px to **0.02–0.03 px
|
|
176
|
+
~1 px to **0.02–0.03 px** (4–53× depending on conditions).
|
|
173
177
|
4. **Rasterise** — supersample each pixel, back-project through the transform, ask the
|
|
174
|
-
model
|
|
175
|
-
|
|
178
|
+
model `occupancy(u, v)`. Exact up to the supersampling rate, and independent of
|
|
179
|
+
lighting. Coverage MAE against exact ground truth is **0.0000–0.0005**.
|
|
180
|
+
5. **Erode to a weight** — by ~2× the estimated blur σ, because geometric purity is not
|
|
181
|
+
enough (see the table above).
|
|
176
182
|
|
|
177
183
|
Decoding is optional throughout. A mark whose material barely deposited will not
|
|
178
184
|
decode, and that is exactly the case that must still work.
|
|
179
185
|
|
|
186
|
+
Sub-pixel geometry turns out to hinge on **three half-pixel conventions agreeing** —
|
|
187
|
+
pixel-centre vs pixel-corner rasterisation, the template raster, and the fact that
|
|
188
|
+
different locators disagree about what a "corner" is. Getting them wrong cost 50× in
|
|
189
|
+
accuracy during development, and none of it is visible on inspection: the mask still
|
|
190
|
+
looks like a mask. The diagnostic, if you ever chase this: *if the error grows with the
|
|
191
|
+
mark's rotation angle following `R(d) − d`, the residual `d` is a convention offset, not
|
|
192
|
+
noise.*
|
|
193
|
+
|
|
194
|
+
## How well it works
|
|
195
|
+
|
|
196
|
+
Validated three ways, because no single way is sufficient — synthetic scenes (exact
|
|
197
|
+
ground truth by construction), real photographs (round-trip through an independent
|
|
198
|
+
decoder), and DIBCO (hand-labelled per-pixel ink truth).
|
|
199
|
+
|
|
200
|
+
| | detection | round-trip geometry | round-trip when *measurable* |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| **QR** (189 real photos) | 80% | 77% | **96%** |
|
|
203
|
+
| **Barcode** (56 real photos) | 95% | 48% | **89%** |
|
|
204
|
+
| **Text** (86 real words) | — (no locator; you supply the quad) | — | see below |
|
|
205
|
+
|
|
206
|
+
The conditional matters: **when inkmap judges an image measurable, an independent decoder
|
|
207
|
+
confirms the geometry 96% of the time.** Failures concentrate where the library already
|
|
208
|
+
says not to trust it.
|
|
209
|
+
|
|
210
|
+
On **real ink with hand-labelled ground truth** (DIBCO), eroding a *perfect* mask by one
|
|
211
|
+
pixel shifts the measured foreground mean by **17% of the foreground/background
|
|
212
|
+
contrast** — so the contamination argument is not an artefact of synthetic data.
|
|
213
|
+
|
|
214
|
+
**Text is the honest weak spot.** Placement works (IoU 0.76 with an image-derived shape)
|
|
215
|
+
but a glyph model with the wrong font does not (IoU 0.17). Measured directly: with the
|
|
216
|
+
correct font IoU is 0.997–1.000; with a wrong one, 0.46–0.80. For text, pass the actual
|
|
217
|
+
font or take the shape from the image.
|
|
218
|
+
|
|
219
|
+
📄 **[Methods and results](docs/methods-and-results.md)** ([PDF](docs/methods-and-results.pdf))
|
|
220
|
+
— the full write-up: approach, algorithms, every number, the datasets, the limits, and a
|
|
221
|
+
barcode defect the evaluation caught and forced a fix for (round-trip 5% → 48%).
|
|
222
|
+
|
|
223
|
+
## Large photographs: locate coarse, refine fine
|
|
224
|
+
|
|
225
|
+
Locating is a large share of the cost on big frames and scales with pixel count, so `find_all(photo, locate_scale=0.5)` (or `locate(bgr, gray, scale=0.5)`) runs the detectors on an `INTER_AREA`-downscaled copy and maps the placements back to full resolution; refinement then pins them to sub-pixel accuracy as usual, so coverage maps match the full-resolution ones. On a 4000 x 3000 photograph of a page with 8 QR codes, locating took 1,129 ms at full resolution, 288 ms at 0.5 and 152 ms at 0.35, with all 8 codes found and decoded (on a slower 2-core server, a synthetic page of the same size: 11.6 s, 2.5 s and 1.1 s).
|
|
226
|
+
|
|
227
|
+
Use it when your codes are large in the frame: a code's modules should still span about 4 px after downscaling (`config.MIN_COARSE_PIXELS_PER_FEATURE`), i.e. 8 px at full resolution for 0.5 and about 11.5 px for 0.35. Below that the module count and pattern read off the small frame go wrong even though the code still decodes (at 2 px a 25 x 25 QR reads as 21 x 21), and refinement cannot repair a wrong model. So, with `full_res_retry=True` (the default), any coarse placement under that floor, or found but not decoded, is re-located on a full-resolution crop around it (`metadata["locate_retry"]` says why). The crops are small, but if every code needs one the speedup is gone, so pick the scale from your smallest code. With `full_res_retry=False`, placements under the floor are kept and marked `metadata["locate_undersampled"] = True`; do not trust their model. Custom locators run on the downscaled frame and must return coordinates in the frame they are given. A code the coarse pass misses entirely is not recovered, so lower the downscaling instead. The default, `None`, locates at full resolution exactly as before.
|
|
228
|
+
|
|
180
229
|
## Extending it
|
|
181
230
|
|
|
182
231
|
```python
|
|
@@ -187,15 +236,25 @@ inkmap.find(photo, locators=["mine", "qr_zxing"])
|
|
|
187
236
|
Tunable defaults live in `inkmap.config` — supersampling, ECC parameters, erosion
|
|
188
237
|
margin, saturation levels — as documented constants rather than inline magic numbers.
|
|
189
238
|
|
|
239
|
+
## Skills for AI agents
|
|
240
|
+
|
|
241
|
+
If you drive inkmap from an agent, the shipped skill
|
|
242
|
+
[`inkmap-usage`](inkmap/data/skills/inkmap-usage/SKILL.md) is the short version of
|
|
243
|
+
everything that is easy to get wrong: choosing a model, the corner-order trap, which of
|
|
244
|
+
the three 0–1 channels to use for what, and gating on measurability. Install it with
|
|
245
|
+
[`skill`](https://github.com/thorwhalen/skill), or just read it.
|
|
246
|
+
|
|
190
247
|
## Test data
|
|
191
248
|
|
|
192
|
-
|
|
193
|
-
fetched on demand and cached:
|
|
249
|
+
384 real photographs of marks in the wild — 189 QR, 56 barcodes, 139 printed text —
|
|
250
|
+
(CC0 / public domain / CC BY / CC BY-SA), fetched on demand and cached:
|
|
194
251
|
|
|
195
252
|
```python
|
|
196
|
-
from inkmap import
|
|
197
|
-
|
|
198
|
-
|
|
253
|
+
from inkmap import corpus
|
|
254
|
+
|
|
255
|
+
corpus.kinds() # {'qr': 189, 'barcode': 56, 'text': 139}
|
|
256
|
+
paths = corpus.fetch(kind="barcode")
|
|
257
|
+
print(corpus.attribution_text()) # attribution is required for CC BY / CC BY-SA
|
|
199
258
|
```
|
|
200
259
|
|
|
201
260
|
Synthetic scenes with **exact** ground-truth coverage — the only way to validate a 0–1
|
|
@@ -126,24 +126,73 @@ and medical-imaging vocabulary: [docs/terminology.md](docs/terminology.md).
|
|
|
126
126
|
|
|
127
127
|
## How it works
|
|
128
128
|
|
|
129
|
+
```
|
|
130
|
+
photograph ──► locate ──► recover model ──► refine transform ──► rasterise ──► erode
|
|
131
|
+
```
|
|
132
|
+
|
|
129
133
|
1. **Locate** — a cascade of `zxing-cpp`, OpenCV's Aruco-based QR detector and OpenCV's
|
|
130
134
|
classic detector, plus linear barcodes. On 189 real photographs: zxing 74%, aruco
|
|
131
135
|
68%, OpenCV 43%, **union 80%**. For marks nothing can auto-detect, `inkmap.place()`
|
|
132
136
|
takes the corners from you.
|
|
133
137
|
2. **Recover the model** — from a bit-exact rectification where a decoder offers one,
|
|
134
138
|
otherwise sampled from the image. *Never* by re-encoding a decoded payload:
|
|
135
|
-
error-correction level
|
|
136
|
-
re-encoding can silently produce a different pattern.
|
|
139
|
+
error-correction level, mask pattern and code-set choices are not recoverable from
|
|
140
|
+
the text, so re-encoding can silently produce a different pattern.
|
|
137
141
|
3. **Refine** — align the rendered model to the photograph with ECC. Four locator
|
|
138
142
|
corners are four observations; the model has thousands. Takes corner error from
|
|
139
|
-
~1 px to **0.02–0.03 px
|
|
143
|
+
~1 px to **0.02–0.03 px** (4–53× depending on conditions).
|
|
140
144
|
4. **Rasterise** — supersample each pixel, back-project through the transform, ask the
|
|
141
|
-
model
|
|
142
|
-
|
|
145
|
+
model `occupancy(u, v)`. Exact up to the supersampling rate, and independent of
|
|
146
|
+
lighting. Coverage MAE against exact ground truth is **0.0000–0.0005**.
|
|
147
|
+
5. **Erode to a weight** — by ~2× the estimated blur σ, because geometric purity is not
|
|
148
|
+
enough (see the table above).
|
|
143
149
|
|
|
144
150
|
Decoding is optional throughout. A mark whose material barely deposited will not
|
|
145
151
|
decode, and that is exactly the case that must still work.
|
|
146
152
|
|
|
153
|
+
Sub-pixel geometry turns out to hinge on **three half-pixel conventions agreeing** —
|
|
154
|
+
pixel-centre vs pixel-corner rasterisation, the template raster, and the fact that
|
|
155
|
+
different locators disagree about what a "corner" is. Getting them wrong cost 50× in
|
|
156
|
+
accuracy during development, and none of it is visible on inspection: the mask still
|
|
157
|
+
looks like a mask. The diagnostic, if you ever chase this: *if the error grows with the
|
|
158
|
+
mark's rotation angle following `R(d) − d`, the residual `d` is a convention offset, not
|
|
159
|
+
noise.*
|
|
160
|
+
|
|
161
|
+
## How well it works
|
|
162
|
+
|
|
163
|
+
Validated three ways, because no single way is sufficient — synthetic scenes (exact
|
|
164
|
+
ground truth by construction), real photographs (round-trip through an independent
|
|
165
|
+
decoder), and DIBCO (hand-labelled per-pixel ink truth).
|
|
166
|
+
|
|
167
|
+
| | detection | round-trip geometry | round-trip when *measurable* |
|
|
168
|
+
|---|---|---|---|
|
|
169
|
+
| **QR** (189 real photos) | 80% | 77% | **96%** |
|
|
170
|
+
| **Barcode** (56 real photos) | 95% | 48% | **89%** |
|
|
171
|
+
| **Text** (86 real words) | — (no locator; you supply the quad) | — | see below |
|
|
172
|
+
|
|
173
|
+
The conditional matters: **when inkmap judges an image measurable, an independent decoder
|
|
174
|
+
confirms the geometry 96% of the time.** Failures concentrate where the library already
|
|
175
|
+
says not to trust it.
|
|
176
|
+
|
|
177
|
+
On **real ink with hand-labelled ground truth** (DIBCO), eroding a *perfect* mask by one
|
|
178
|
+
pixel shifts the measured foreground mean by **17% of the foreground/background
|
|
179
|
+
contrast** — so the contamination argument is not an artefact of synthetic data.
|
|
180
|
+
|
|
181
|
+
**Text is the honest weak spot.** Placement works (IoU 0.76 with an image-derived shape)
|
|
182
|
+
but a glyph model with the wrong font does not (IoU 0.17). Measured directly: with the
|
|
183
|
+
correct font IoU is 0.997–1.000; with a wrong one, 0.46–0.80. For text, pass the actual
|
|
184
|
+
font or take the shape from the image.
|
|
185
|
+
|
|
186
|
+
📄 **[Methods and results](docs/methods-and-results.md)** ([PDF](docs/methods-and-results.pdf))
|
|
187
|
+
— the full write-up: approach, algorithms, every number, the datasets, the limits, and a
|
|
188
|
+
barcode defect the evaluation caught and forced a fix for (round-trip 5% → 48%).
|
|
189
|
+
|
|
190
|
+
## Large photographs: locate coarse, refine fine
|
|
191
|
+
|
|
192
|
+
Locating is a large share of the cost on big frames and scales with pixel count, so `find_all(photo, locate_scale=0.5)` (or `locate(bgr, gray, scale=0.5)`) runs the detectors on an `INTER_AREA`-downscaled copy and maps the placements back to full resolution; refinement then pins them to sub-pixel accuracy as usual, so coverage maps match the full-resolution ones. On a 4000 x 3000 photograph of a page with 8 QR codes, locating took 1,129 ms at full resolution, 288 ms at 0.5 and 152 ms at 0.35, with all 8 codes found and decoded (on a slower 2-core server, a synthetic page of the same size: 11.6 s, 2.5 s and 1.1 s).
|
|
193
|
+
|
|
194
|
+
Use it when your codes are large in the frame: a code's modules should still span about 4 px after downscaling (`config.MIN_COARSE_PIXELS_PER_FEATURE`), i.e. 8 px at full resolution for 0.5 and about 11.5 px for 0.35. Below that the module count and pattern read off the small frame go wrong even though the code still decodes (at 2 px a 25 x 25 QR reads as 21 x 21), and refinement cannot repair a wrong model. So, with `full_res_retry=True` (the default), any coarse placement under that floor, or found but not decoded, is re-located on a full-resolution crop around it (`metadata["locate_retry"]` says why). The crops are small, but if every code needs one the speedup is gone, so pick the scale from your smallest code. With `full_res_retry=False`, placements under the floor are kept and marked `metadata["locate_undersampled"] = True`; do not trust their model. Custom locators run on the downscaled frame and must return coordinates in the frame they are given. A code the coarse pass misses entirely is not recovered, so lower the downscaling instead. The default, `None`, locates at full resolution exactly as before.
|
|
195
|
+
|
|
147
196
|
## Extending it
|
|
148
197
|
|
|
149
198
|
```python
|
|
@@ -154,15 +203,25 @@ inkmap.find(photo, locators=["mine", "qr_zxing"])
|
|
|
154
203
|
Tunable defaults live in `inkmap.config` — supersampling, ECC parameters, erosion
|
|
155
204
|
margin, saturation levels — as documented constants rather than inline magic numbers.
|
|
156
205
|
|
|
206
|
+
## Skills for AI agents
|
|
207
|
+
|
|
208
|
+
If you drive inkmap from an agent, the shipped skill
|
|
209
|
+
[`inkmap-usage`](inkmap/data/skills/inkmap-usage/SKILL.md) is the short version of
|
|
210
|
+
everything that is easy to get wrong: choosing a model, the corner-order trap, which of
|
|
211
|
+
the three 0–1 channels to use for what, and gating on measurability. Install it with
|
|
212
|
+
[`skill`](https://github.com/thorwhalen/skill), or just read it.
|
|
213
|
+
|
|
157
214
|
## Test data
|
|
158
215
|
|
|
159
|
-
|
|
160
|
-
fetched on demand and cached:
|
|
216
|
+
384 real photographs of marks in the wild — 189 QR, 56 barcodes, 139 printed text —
|
|
217
|
+
(CC0 / public domain / CC BY / CC BY-SA), fetched on demand and cached:
|
|
161
218
|
|
|
162
219
|
```python
|
|
163
|
-
from inkmap import
|
|
164
|
-
|
|
165
|
-
|
|
220
|
+
from inkmap import corpus
|
|
221
|
+
|
|
222
|
+
corpus.kinds() # {'qr': 189, 'barcode': 56, 'text': 139}
|
|
223
|
+
paths = corpus.fetch(kind="barcode")
|
|
224
|
+
print(corpus.attribution_text()) # attribution is required for CC BY / CC BY-SA
|
|
166
225
|
```
|
|
167
226
|
|
|
168
227
|
Synthetic scenes with **exact** ground-truth coverage — the only way to validate a 0–1
|