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.
Files changed (40) hide show
  1. inkmap-0.1.4/.claude/skills/inkmap-dev-validation/SKILL.md +111 -0
  2. {inkmap-0.1.2 → inkmap-0.1.4}/PKG-INFO +70 -11
  3. {inkmap-0.1.2 → inkmap-0.1.4}/README.md +69 -10
  4. inkmap-0.1.4/docs/methods-and-results.md +505 -0
  5. inkmap-0.1.4/docs/methods-and-results.pdf +0 -0
  6. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/__init__.py +11 -2
  7. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/config.py +15 -0
  8. inkmap-0.1.2/inkmap/data.py → inkmap-0.1.4/inkmap/corpus.py +35 -17
  9. inkmap-0.1.4/inkmap/data/commons_marks.json +3458 -0
  10. inkmap-0.1.4/inkmap/data/skills/inkmap-usage/SKILL.md +222 -0
  11. inkmap-0.1.4/inkmap/locate/__init__.py +310 -0
  12. inkmap-0.1.4/inkmap/locate/barcode.py +258 -0
  13. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/synth.py +2 -2
  14. {inkmap-0.1.2 → inkmap-0.1.4}/pyproject.toml +1 -1
  15. inkmap-0.1.4/scripts/build_report.py +92 -0
  16. {inkmap-0.1.2 → inkmap-0.1.4}/scripts/corpus_eval.py +1 -1
  17. inkmap-0.1.4/scripts/evaluate.py +319 -0
  18. inkmap-0.1.4/scripts/evaluate_dibco.py +165 -0
  19. inkmap-0.1.4/scripts/evaluate_text.py +217 -0
  20. {inkmap-0.1.2 → inkmap-0.1.4}/tests/test_inkmap.py +327 -1
  21. inkmap-0.1.2/inkmap/assets/commons_qr.json +0 -1514
  22. inkmap-0.1.2/inkmap/locate/__init__.py +0 -117
  23. inkmap-0.1.2/inkmap/locate/barcode.py +0 -159
  24. {inkmap-0.1.2 → inkmap-0.1.4}/.github/workflows/ci.yml +0 -0
  25. {inkmap-0.1.2 → inkmap-0.1.4}/.gitignore +0 -0
  26. {inkmap-0.1.2 → inkmap-0.1.4}/LICENSE +0 -0
  27. {inkmap-0.1.2 → inkmap-0.1.4}/docs/terminology.md +0 -0
  28. {inkmap-0.1.2 → inkmap-0.1.4}/docs/validation.md +0 -0
  29. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/coverage.py +0 -0
  30. {inkmap-0.1.2/inkmap/assets → inkmap-0.1.4/inkmap/data}/default_qr.txt +0 -0
  31. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/geometry.py +0 -0
  32. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/locate/qr.py +0 -0
  33. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/__init__.py +0 -0
  34. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/base.py +0 -0
  35. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/freeform.py +0 -0
  36. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/models/grid.py +0 -0
  37. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/photometry.py +0 -0
  38. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/types.py +0 -0
  39. {inkmap-0.1.2 → inkmap-0.1.4}/inkmap/viz.py +0 -0
  40. {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.2
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 and mask pattern are not recoverable from the text, so
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. Exact up to quantisation, and independent of lighting.
175
- 5. **Erode to a weight** — by 2–3× the estimated blur σ.
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
- 189 real photographs of marks in the wild (CC0 / public domain / CC BY / CC BY-SA),
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 data
197
- paths = data.fetch()
198
- print(data.attribution_text())
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 and mask pattern are not recoverable from the text, so
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. Exact up to quantisation, and independent of lighting.
142
- 5. **Erode to a weight** — by 2–3× the estimated blur σ.
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
- 189 real photographs of marks in the wild (CC0 / public domain / CC BY / CC BY-SA),
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 data
164
- paths = data.fetch()
165
- print(data.attribution_text())
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