pageimprover commited on
Commit
9e5e184
·
verified ·
1 Parent(s): 2b79915

ibteda.pt: the deployed geometry model, with its card

Browse files
Files changed (1) hide show
  1. README.md +167 -111
README.md CHANGED
@@ -45,148 +45,204 @@ audit regenerates this card and fails on any difference. -->
45
 
46
  # PageImprover Geometry Model
47
 
48
- A ResNet-34 regression model that predicts the two page crops of a photographed two-page spread. It is the first of
49
- three stages: the model proposes the crops, a per-book calibration of five spreads corrects them to the book's own
50
- margins, and a review queue sends the least trustworthy page sides to a person. It was trained and measured on one
51
- archive's books and is built to be pointed at anyone's; the calibration is what carries it to a book it has never
52
- seen. On its own it does not produce a finished page, and the figures below only mean anything as a trio.
53
 
54
- <img src="https://ibteda.org/journey/assets/full/bahar-correspondence-spread.jpg" width="480"
55
- alt="A photographed two-page spread from the Ibteda archive on a dark table, with the two page crops drawn on it">
56
-
57
- *One photograph, two page crops: a spread as the model sees it.*
58
-
59
- Every performance number here is a row in [`docs/claims.md`](https://github.com/ibteda-org/pageimprover/blob/main/docs/claims.md), which names the cohort, the
60
- calibration state and the evidence behind it. A build gate refuses any figure that drifts from that ledger.
61
-
62
- ## What it takes as input
63
-
64
- | | works | does not work |
65
- |---|---|---|
66
- | Capture | one photograph per spread, both facing pages in the frame | one page per photograph: the model predicts two crops per image and has nothing to predict on a single leaf |
67
- | Material | printed books | handwritten manuscripts, a documented blind spot: 0.0727<!--claim:manuscript_blindspot--> calibrated pass@80 |
68
- | Framing | the paper filling most of the frame, roughly centred, as in the training captures | a small paper block in a wide surround, which the release measures and reports but does not correct |
69
- | Image size and format | any size; the package letterboxes the image to the model's 704<!--claim:model_input--> px tile itself; JPEG, PNG, TIFF and WebP | |
70
 
71
- Closed covers are recognised by the framing check and cropped from the detected paper block as single pages; the
72
- model is not asked about them.
73
-
74
- ## Getting the model
75
 
76
- Install `pageimprover` and run `pageimprover fetch model`. It downloads `ibteda.pt`, checks the bytes against the
77
- pinned digest and refuses any others. Nothing downloads on its own, and the model is loaded only through the
78
- package: it is a TorchScript archive carrying its own preprocessing contract, not a state dict.
79
 
80
- The pinned artifact is `ibteda.pt`, SHA-256 `359ae48b6a846e01d70a21c2c49426125f3b8950ae7b4736ba8cf954af60babe`. The code, its tests and the claim ledger every figure here is rendered from are at <https://github.com/ibteda-org/pageimprover>.
 
81
 
82
- ## How the three stages work
83
 
84
- **1. The model.** Given one spread, it predicts the left and right page rectangles as a single fixed-length
85
- vector. Geometric regression, not detection: no anchors, no proposals, no class labels. The 704 px input was chosen
86
- to fit a T4's memory, not for accuracy, and has never been tuned against the acceptance bar; a larger input is an
87
- untested lever, not a rejected one.
 
 
 
88
 
89
- A prediction is scored by its **worst corner**. A page side passes when the worst of its four corners lands within
90
- 80<!--claim:pass_bar--> px of the operator's crop, measured in the fixed 2048×2897 finishing frame. This is
91
- `metrics.PASS_PX`, a geometry rule and nothing more: not an OCR score, not an aesthetic judgement, and not
92
- "accuracy" in any sense a reader should generalise.
93
 
94
- **2. Per-book calibration.** The dominant residual is a per-book size constant: the margins a book was finished at
95
- leave no trace in a single image, so no model recovers them at any capacity. Ten operator crops do. Five spreads
96
- (k=5<!--claim:k_default-->), both sides, 10<!--claim:cal_crops--> crops per book, pooled into one offset
97
- (`calibrate/pooled_median.py`). The offset is applied only when its magnitude reaches 40<!--claim:cal_gate-->
98
- canonical px (`refusals.CALIBRATION_GATE_PX`); below that it is indistinguishable from an operator's own
99
- self-consistency and can only add noise.
100
 
101
- Two design choices are recorded as costs, not victories. Pooled, not per-side: splitting the same ten crops by page
102
- side harmed 8<!--claim:cal_harm_perside--> held-out books; pooling them harmed 2<!--claim:cal_harm_pooled-->, same
103
- crops, estimator the only difference. Five spreads, not seven: seven beat five by +0.0104<!--claim:k7_delta-->
104
- pass@80, decisively (p<0.0001), and was declined because at corpus scale that point costs thousands of extra
105
- operator pages.
106
 
107
- **3. Review routing.** A flip-consistency check runs the model on the spread and on its horizontally flipped copy
108
- and flags a side when the two disagree by more than 87<!--claim:route_threshold--> canonical px
109
- (`review.absolute`). Those sides go to a person; the rest ship.
110
 
111
- ## Measured performance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- Two held-out cohorts, both inside the training archive. "Sealed" is a 54-book sensitivity test held back from every
114
- design decision; "confirmation" is a 192-book cohort unexposed during development. All figures are pass@80.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  | stage | sealed cohort | confirmation cohort |
117
  |---|---|---|
118
  | model alone | 0.6770<!--claim:geo_raw_sealed--> | — |
119
  | + per-book calibration | 0.8278<!--claim:route_sealed_baseline--> | 0.8334<!--claim:route_conf_baseline--> |
120
- | + review of the routed sides | 0.9151<!--claim:route_sealed_routed--> | 0.9248<!--claim:route_conf_routed-->, with 28.5%<!--claim:route_conf_share--> of page sides sent to a person |
121
 
122
- The calibration gain was measured on its own, larger confirmation cohort: held-out pass@80 rose from
123
- 0.7107<!--claim:cal_baseline--> uncalibrated to 0.8271<!--claim:cal_pooled--> with the single pooled offset, a gain
124
- bought with five spreads of operator work per book.
125
-
126
- The review share is an in-archive operating point, not a portable promise; a different collection's distribution
127
- moves it. Reaching a higher 0.930 target under a coverage *sweep*, which is explicitly not the deployed rule, needs
128
- 34.5%<!--claim:route_sweep_930_sealed--> review on sealed and 30.5%<!--claim:route_sweep_930_conf--> on
129
- confirmation. A sweep point and the deployed point answer different questions, and the two were once mixed to
130
- produce a claim that had to be withdrawn.
131
 
132
  ## Using it on another collection
133
 
134
  The model works on printed-book spreads from other scanning setups, on three conditions.
135
 
136
- - **Calibrate every book.** Five spreads, ten crops, as above. This is not optional away from the training archive:
137
- the calibration is what adapts the model to a binding and a finishing convention it has never seen. On a public
138
- seventeenth-century atlas photographed by another collection, the model located the text block on every spread
139
- and the remaining error was one consistent margin offset, exactly what the ten crops remove. That run is a worked
140
- example, not a measurement: no accuracy figure is claimed for other collections, and any figure you need must be
141
- measured against a local operator's crops.
142
- - **Frame the paper as the training captures did.** The paper should fill most of the frame, roughly centred. The
143
- release measures the framing of every frame and reports it, but applies no correction, so a capture with a wide
144
- surround needs cropping upstream. Whether letterboxing or centring is better for a frame with no surround is
145
- unmeasured.
146
- - **Do not carry the review numbers across.** The disagreement score's level does not transfer, its ordering was
147
- found not to either, and the deployed threshold and review share describe one archive. Read the routed queue as
148
- a list of sides to look at, not as a forecast of operator workload.
149
-
150
- ## Known failure modes
151
-
152
- **Handwritten manuscripts.** The deployed model reads printed material well at correct framing and reads
153
- handwritten pages badly: 0.0727<!--claim:manuscript_blindspot--> calibrated pass@80 on the manuscript sides
154
- evaluated, with the routing signal actively anti-ranked rather than merely uninformative. A model built to fix this
155
- was measured and refused: it learned its manuscripts almost perfectly and still failed both foreign reads, so this
156
- model ships with the blind spot named rather than papered over. Anything quoting this model as adequate for
157
- manuscript work should quote that number instead.
158
-
159
- **The disagreement score away from the archive.** On foreign captures the flip-consistency gap is large, signed
160
- and material-dependent: it has overstated a small error by more than an order of magnitude on one book and tracked
161
- a large median on another, and on a fourth book its ranking put the two most nearly perfect sides last.
162
- `review.score.disagreement_px` is a routing signal, not a page error, and away from the archive it must not be read
163
- as one in either direction.
164
 
165
- **Framing is never corrected.** The paper block must sit at roughly the training framing before the model has a
166
- fair chance. The release reports framing; it does not fix it.
167
 
168
  ## Training data and attribution
169
 
170
- The model was trained on completed page crops from the **Ibteda corpus**: books finished by hand by the archive's
171
- operators, whose crops are the target the model regresses toward. That labour is the ground truth here; the model
172
- reproduces a finishing convention, it does not invent one. Any use of this model should credit the Ibteda corpus
173
- and its operators as the source of the training crops.
 
 
 
 
 
174
 
175
- That convention is narrow and measured as such: 98.7%<!--claim:canvas_share--> of finished pages used a single
176
- 2048×2897 Photoshop template, and the export round-trip reconstructs that historical output to a median residual
177
- of 3.0<!--claim:canvas_residual--> px. The high share is a fact about one archive's finishing habit, not a universal
178
- page ratio, and the residual describes the convention's reproducibility, not the model's quality.
179
 
180
- ## Intended use, and what would be misuse
 
 
 
 
 
 
181
 
182
- Intended: locating and cropping page geometry for photographed two-page spreads of printed books, from the archive
183
- it was trained on or from any other scanning setup, as the first stage of the three-stage loop whose other two
184
- stages are a person's calibration crops and a person's review of the routed sides.
 
 
185
 
186
- Misuse: quoting any single number here as "accuracy"; treating the figures as a promise about another collection
187
- without re-measuring against a local operator's crops; deploying on handwritten manuscript pages; citing a review
188
- share as portable; or reading the disagreement score as a per-page error. The model does not finish a page on its
189
- own, and this card exists so that no figure in it is read as a claim that it does.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  ## Licence and authorization
192
 
 
45
 
46
  # PageImprover Geometry Model
47
 
48
+ PageImprover is a ResNet-34 regression model that takes one photograph of an open book and returns the crop
49
+ rectangles of its two pages, built to run inside a calibrate-and-review workflow on printed books.
 
 
 
50
 
51
+ - **Model:** `ibteda.pt`, a TorchScript archive that carries its own preprocessing contract
52
+ - **Input:** one photograph with both facing pages in frame, any size or format
53
+ - **Output:** a left and a right page rectangle, from a direct pass and a flipped pass
54
+ - **Trained on:** printed books from the Ibteda corpus
55
+ - **Metric:** pass@80, scored per page side
56
+ - **Needed to be useful:** a per-book calibration and a review step; the model is stage one of three
 
 
 
 
 
 
 
 
 
 
57
 
58
+ <img src="https://ibteda.org/journey/assets/full/bahar-correspondence-spread.jpg" width="480"
59
+ alt="A photographed spread from the Ibteda archive on a dark table, with the operators' finished page crops projected back onto it">
 
 
60
 
61
+ *One photograph, two page crops. The rectangles are the operators' finished pages projected back onto the spread:
62
+ the target the model is trained to reproduce.*
 
63
 
64
+ Every figure in this card is a row in [`docs/claims.md`](https://github.com/ibteda-org/pageimprover/blob/main/docs/claims.md), which names its cohort, calibration state
65
+ and evidence. A build gate rejects any figure that drifts from that ledger.
66
 
67
+ ## Quick start
68
 
69
+ ```sh
70
+ git clone https://github.com/ibteda-org/pageimprover && cd pageimprover
71
+ uv sync --extra inference # the app and the model runtime; no GPU needed
72
+ uv run pageimprover fetch model # downloads ibteda.pt and checks its digest; nothing else ever downloads
73
+ uv run pageimprover sample # a public book to try it on, or: add NAME FOLDER for your own photographs
74
+ uv run pageimprover studio # process, calibrate, review and export in the browser
75
+ ```
76
 
77
+ From Python, with the `inference` extra installed:
 
 
 
78
 
79
+ ```python
80
+ import cv2
81
+ from pageimprover import model
 
 
 
82
 
83
+ geometry = model.load() # the verified ibteda.pt, refused if the digest differs
84
+ prediction = geometry.predict(cv2.imread("spread.jpg")) # any size; letterboxed to the model's tile internally
85
+ left, right = prediction.side("left"), prediction.side("right") # each has .direct and .mirrored rectangles
86
+ ```
 
87
 
88
+ ## Input and output
 
 
89
 
90
+ | | works | does not work |
91
+ |---|---|---|
92
+ | Capture | one photograph per spread, both facing pages in frame | one page per photograph: the model predicts two crops and has nothing to predict on a single leaf |
93
+ | Material | printed books | handwritten manuscripts: 0.0727<!--claim:manuscript_blindspot--> calibrated pass@80, a documented blind spot |
94
+ | Framing | the paper filling most of the frame, roughly centred, as in the training captures | a small paper block in a wide surround; the release measures framing but does not correct it, so crop upstream |
95
+ | Image | any size; the package letterboxes it to the model's 704<!--claim:model_input--> px tile | |
96
+ | Format | JPEG, PNG, TIFF, WebP | |
97
+
98
+ The contract, as the artifact itself declares it:
99
+
100
+ ```text
101
+ photograph → 1500×1000 working tile, letterboxed to 704×704
102
+ → 24 numbers: for each of {left, right} × {direct pass, flipped pass},
103
+ centre x, centre y, log width, log height, sin θ, cos θ
104
+ → two page rectangles in the photograph's own pixels
105
+ ```
106
+
107
+ Closed covers are recognised by the framing check and cropped from the detected paper block as single pages. The
108
+ model is not asked about them.
109
 
110
+ ## The three stages
111
+
112
+ ```text
113
+ photographed spread
114
+ │
115
+ ▼
116
+ 1. geometry model proposes the two page crops
117
+ │
118
+ ▼
119
+ 2. per-book calibration five spreads, ten operator crops, one pooled offset
120
+ │
121
+ ▼
122
+ 3. review routing the least trustworthy page sides go to a person
123
+ │
124
+ ▼
125
+ finished page crops
126
+ ```
127
+
128
+ **1. The model** predicts the left and right page rectangles as one fixed-length vector. Geometric regression, not
129
+ detection: no anchors, no proposals, no class labels. Its 704 px input was chosen to fit a T4's memory, not for
130
+ accuracy, and has never been tuned against the acceptance bar.
131
+
132
+ **2. Calibration** corrects the one thing no photograph shows: how tightly this book's pages were finished. The
133
+ tool picks five spreads spanning the book, skipping the front matter, and shows them one at a time. On each you
134
+ adjust the left and right page crops. That is 10<!--claim:cal_crops--> crops from k=5<!--claim:k_default-->
135
+ spreads, pooled into a single offset for the whole book (`calibrate/pooled_median.py`). The offset is applied only
136
+ when it reaches 40<!--claim:cal_gate--> canonical px (`refusals.CALIBRATION_GATE_PX`); a smaller one is within an
137
+ operator's own self-consistency and is not applied.
138
+
139
+ **3. Review** runs the model on the spread and on a horizontally flipped copy. A page side whose two answers
140
+ disagree by more than 87<!--claim:route_threshold--> canonical px (`review.absolute`) goes to a person; the rest
141
+ ship. The disagreement is a routing signal, not a page error.
142
+
143
+ ## Performance
144
+
145
+ pass@80: a page side passes when the worst of its four corners lands within 80<!--claim:pass_bar--> px of the
146
+ operator's crop in the fixed 2048×2897 finishing frame (`metrics.PASS_PX`). A geometry rule only: not OCR accuracy,
147
+ not aesthetics, not "accuracy" in any general sense.
148
+
149
+ Two held-out cohorts inside the training archive, measured over **page sides**. Sealed: 54 books, 1,626 sides,
150
+ held back from every design decision. Confirmation: 192 books, 5,636 sides, unexposed during development.
151
 
152
  | stage | sealed cohort | confirmation cohort |
153
  |---|---|---|
154
  | model alone | 0.6770<!--claim:geo_raw_sealed--> | — |
155
  | + per-book calibration | 0.8278<!--claim:route_sealed_baseline--> | 0.8334<!--claim:route_conf_baseline--> |
156
+ | + review of the routed sides | 0.9151<!--claim:route_sealed_routed--> | 0.9248<!--claim:route_conf_routed-->, with 28.5%<!--claim:route_conf_share--> of sides reviewed |
157
 
158
+ The review share is an operating point inside one archive, not a portable promise.
 
 
 
 
 
 
 
 
159
 
160
  ## Using it on another collection
161
 
162
  The model works on printed-book spreads from other scanning setups, on three conditions.
163
 
164
+ - **Calibrate every book.** Away from the training archive this is not optional: the calibration is what adapts
165
+ the model to a binding and a finishing convention it has never seen. On a public seventeenth-century atlas
166
+ photographed by another collection, the model found the text block on every spread and the remaining error was
167
+ one consistent margin offset, which is what the ten crops remove. A worked example, not a measurement: no
168
+ accuracy figure is claimed for other collections.
169
+ - **Frame the paper as the training captures did.** Most of the frame, roughly centred. A wide surround needs
170
+ cropping before inference.
171
+ - **Do not carry the review numbers across.** The disagreement score's level and ordering do not transfer, and
172
+ the threshold and review share describe one archive. Read the routed queue as sides to look at, not as a
173
+ workload forecast.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ **Do not use it on handwritten manuscripts.** See the evaluation notes.
 
176
 
177
  ## Training data and attribution
178
 
179
+ Trained on completed page crops from the **Ibteda corpus**: books finished by hand by the archive's operators,
180
+ whose crops are the target. The model reproduces a finishing convention; it does not invent one. Any use should
181
+ credit the Ibteda corpus and its operators as the source of the training crops.
182
+
183
+ The convention is narrow: 98.7%<!--claim:canvas_share--> of finished pages used one 2048×2897 Photoshop template,
184
+ and the export round-trip reconstructs that historical output to a median residual of
185
+ 3.0<!--claim:canvas_residual--> px. Both are facts about one archive's finishing habit, not about the model.
186
+
187
+ ## Evaluation notes
188
 
189
+ Methodological detail behind the figures above. Skip it to use the model; read it before quoting a number.
 
 
 
190
 
191
+ **Calibration design.** The calibration gain was measured on its own, larger cohort: held-out pass@80 rose from
192
+ 0.7107<!--claim:cal_baseline--> uncalibrated to 0.8271<!--claim:cal_pooled--> with the single pooled offset. Two
193
+ choices are recorded as costs, not victories. Pooled, not per-side: splitting the same ten crops by page side
194
+ harmed 8<!--claim:cal_harm_perside--> held-out books; pooling harmed 2<!--claim:cal_harm_pooled-->, same crops,
195
+ estimator the only difference. Five spreads, not seven: seven beat five by +0.0104<!--claim:k7_delta--> pass@80,
196
+ decisively (p<0.0001), and was declined because at corpus scale that point costs thousands of extra operator
197
+ pages.
198
 
199
+ **The review sweep, and a withdrawn claim.** Reaching a higher 0.930 target under a coverage *sweep*, which is not
200
+ the deployed rule, needs 34.5%<!--claim:route_sweep_930_sealed--> review on sealed and
201
+ 30.5%<!--claim:route_sweep_930_conf--> on confirmation. A sweep point and the deployed point answer different
202
+ questions; the two were once mixed to produce a claim that had to be withdrawn, which is why this card keeps them
203
+ apart.
204
 
205
+ **Manuscripts.** The deployed model reads handwritten pages badly: 0.0727<!--claim:manuscript_blindspot-->
206
+ calibrated pass@80 on the manuscript sides evaluated, with the routing signal actively anti-ranked rather than
207
+ merely uninformative. A model built to fix this was measured and refused: it learned its manuscripts almost
208
+ perfectly and still failed both foreign reads. This model ships with the blind spot named rather than papered over.
209
+
210
+ **The disagreement score away from the archive.** On foreign captures the flip-consistency gap is large, signed
211
+ and material-dependent: it overstated a small error by more than an order of magnitude on one book, tracked a large
212
+ median on another, and on a fourth book ranked the two most nearly perfect sides last.
213
+ `review.score.disagreement_px` must not be read as a page error in either direction.
214
+
215
+ **Untested levers.** A larger input than 704 px, and letterboxing versus centring for a frame with no surround.
216
+ Both are unmeasured, not rejected.
217
+
218
+ ## Reproducibility and versions
219
+
220
+ - File: `ibteda.pt`
221
+ - SHA-256: `359ae48b6a846e01d70a21c2c49426125f3b8950ae7b4736ba8cf954af60babe`
222
+ - Pinned by: pageimprover 0.1.0
223
+ - Code, tests and the claim ledger: <https://github.com/ibteda-org/pageimprover>
224
+ - Model identity, from the artifact's embedded contract: ResNet-34 trunk, checkpoint `best_pass80.pt` at epoch 59,
225
+ 704 px letterboxed input built from a 1500×1000 working tile, 24 output numbers as declared above. The package
226
+ exposes this contract and refuses an artifact whose contract disagrees with the code.
227
+ - Hub revision: pinned in `pageimprover.config.ARTIFACT` inside the package, never in this card, because the
228
+ revision exists only after the commit that publishes it. `pageimprover fetch model` requests that revision alone.
229
+ - Evidence: [`docs/claims.md`](https://github.com/ibteda-org/pageimprover/blob/main/docs/claims.md) is derived from the research project's evidence ledger, committed
230
+ 2026-09-04 as `1a259aaeeb8d05524695dba7a2a969e395a0e482`, and cites it by commit. The release audit reproduces each
231
+ value against that commit's retained result files.
232
+ - Public benchmark: `pageimprover fetch benchmark coronelli_1693` from a checkout downloads a twelve-spread
233
+ seventeenth-century atlas with a digest per frame and reproduces the recorded framing measurements.
234
+
235
+ ## Citation
236
+
237
+ ```bibtex
238
+ @software{pageimprover_2026,
239
+ title = {PageImprover: page geometry for photographed book spreads},
240
+ author = {{Ibteda Digital Library}},
241
+ year = {2026},
242
+ url = {https://github.com/ibteda-org/pageimprover},
243
+ note = {Model artifact ibteda.pt (CC BY 4.0); code Apache-2.0}
244
+ }
245
+ ```
246
 
247
  ## Licence and authorization
248