pageimprover / README.md
pageimprover's picture
Model card: formatter layout for the Python
325eeb6 verified
|
Raw History Blame Contribute Delete
15.4 kB
---
license: cc-by-4.0
library_name: pageimprover
tags:
- computer-vision
- document-ai
- page-geometry
- document-layout
- book-digitisation
- torchscript
- human-in-the-loop
model-index:
- name: pageimprover
results:
- task:
type: page-geometry-regression
name: Page geometry regression (two page crops per photographed spread)
dataset:
type: ibteda-digital-library
name: Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
metrics:
- type: pass@80
value: 0.6770
name: "raw pass@80 - sealed sensitivity test, 54 books, 1,626 routed sides (calibration pages excluded) - uncalibrated, unrouted [claim geo_raw_sealed]"
- type: pass@80
value: 0.7107
name: "held-out pass@80, uncalibrated - calibration confirmation, 249 books, 5,022 held-out sides per repeat, 20 repeats - uncalibrated [claim cal_baseline]"
- type: pass@80
value: 0.8271
name: "held-out pass@80, one pooled 10-crop book offset - calibration confirmation, 249 books - calibrated (pooled, 10 crops) [claim cal_pooled]"
- type: pass@80
value: 0.8334
name: "calibrated pass@80, before review - routing confirmation, 192 books, 5,636 held-out sides - calibrated, unrouted [claim route_conf_baseline]"
- type: pass@80
value: 0.9248
name: "pass@80 at the deployed review rule - routing confirmation, 192 books, 5,636 sides - calibrated + routed (28.5% review) [claim route_conf_routed]"
- type: pass@80
value: 0.8278
name: "calibrated pass@80, before review - sealed sensitivity, 54 books, 1,626 sides - calibrated, unrouted [claim route_sealed_baseline]"
- type: pass@80
value: 0.9151
name: "pass@80 the deployed review rule delivers - sealed sensitivity, 54 books, 1,626 sides - calibrated + routed (deployed rule) [claim route_sealed_routed]"
---
<!-- Generated by tools/hub_card.py from MODEL_CARD.md and docs/claims.md. Do not edit on the hub: the release
audit regenerates this card and fails on any difference. -->
# PageImprover Geometry Model
PageImprover is a ResNet-34 regression model that takes a single photograph of an open book and predicts the crop
rectangles of its two facing pages. It is designed as the geometry stage of a calibrate-and-review workflow for
printed books: the model proposes crops, per-book calibration adapts them to the volume, and review routes
uncertain page sides to a human.
| | |
|---|---|
| **Model** | `ibteda.pt`, a TorchScript archive that carries its own preprocessing contract |
| **Input** | one photograph with both facing pages in frame, any size or format |
| **Output** | a left and a right page rectangle, from a direct pass and a flipped pass |
| **Trained on** | printed books from the Ibteda corpus |
| **Metric** | pass@80, scored per page side |
| **Not standalone** | **useful deployment requires per-book calibration and a review step; this model is stage one of three** |
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
and evidence. A build gate rejects any figure that drifts from that ledger.
## Quick start
```sh
git clone https://github.com/ibteda-org/pageimprover && cd pageimprover
uv sync --extra inference # the app and the model runtime; no GPU needed
uv run pageimprover fetch model # downloads ibteda.pt and checks its digest; nothing else ever downloads
uv run pageimprover sample # a public book to try it on, or: add NAME FOLDER for your own photographs
uv run pageimprover studio # process, calibrate, review and export in the browser
```
From Python, with the `inference` extra installed:
```python
import cv2
from pageimprover import model
geometry = model.load() # the verified ibteda.pt, refused if the digest differs
prediction = geometry.predict(cv2.imread("spread.jpg")) # any size; letterboxed to the model's tile internally
# "left" and "right" are the photograph's left and right pages, not recto and verso: reading order is applied
# later, from the book's recorded direction. Each side has .direct and .mirrored rectangles, one per pass.
left, right = prediction.side("left"), prediction.side("right")
```
## Input and output
| | works | does not work |
|---|---|---|
| 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 |
| Material | printed books | handwritten manuscripts: 0.0727<!--claim:manuscript_blindspot--> calibrated pass@80, a documented blind spot |
| 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 |
| Image | any size; the package letterboxes it to the model's 704<!--claim:model_input--> px tile | |
| Format | JPEG, PNG, TIFF, WebP | |
The model's input/output contract is embedded in the artifact itself:
```text
photograph → 1500×1000 working tile, letterboxed to 704×704
→ 24 numbers: for each of {left, right} × {direct pass, flipped pass},
centre x, centre y, log width, log height, sin θ, cos θ
→ two page rectangles in the photograph's own pixels
```
Closed covers are handled by the upstream framing check: they are cropped from the detected paper block as single
pages, and the geometry model is not run on them.
<img src="https://ibteda.org/journey/assets/full/bahar-correspondence-spread.jpg" width="480"
alt="A photographed spread from the Ibteda archive on a dark table, with the operators' finished page crops projected back onto it">
*One photograph, two page crops. The rectangles are the operators' finished pages projected back onto the spread:
the target the model is trained to reproduce.*
## The three stages
```text
photographed spread
│
▼
1. geometry model proposes the two page crops
│
▼
2. per-book calibration five spreads, one crop per page side adjusted on each, one pooled offset
│
▼
3. review routing the most disagreement-prone page sides go to a person
│
▼
finished page crops
```
**1. The model** predicts the left and right page rectangles as one fixed-length vector. Geometric regression, not
detection: no anchors, no proposals, no class labels. Its 704 px input was chosen to fit a T4's memory, not for
accuracy, and has never been tuned against the acceptance bar.
**2. Calibration** corrects the one thing no photograph shows: how tightly this book's pages were finished. The
tool picks k=5<!--claim:k_default--> spreads spanning the book, skipping the front matter, and shows them one at a
time. On each spread you adjust one left-page and one right-page crop: 10<!--claim:cal_crops--> crop adjustments in
total, pooled into one volume-wide offset (`calibrate/pooled_median.py`). The offset is applied only when it reaches
40<!--claim:cal_gate--> canonical px (`refusals.CALIBRATION_GATE_PX`); a smaller one is below the calibration gate
and is not applied.
**3. Review** runs the model on the spread and on a horizontally flipped copy. A page side whose two answers
disagree by more than 87<!--claim:route_threshold--> canonical px (`review.absolute`) goes to a person; the rest
ship. The disagreement is a routing signal, not a page error.
## Performance
pass@80: a page side passes when the worst of its four corners lands within 80<!--claim:pass_bar--> px of the
operator's crop in the fixed 2048×2897 finishing frame (`metrics.PASS_PX`). A geometry rule only: not OCR accuracy,
not aesthetics, not "accuracy" in any general sense.
Two held-out cohorts inside the training archive, measured over **page sides**. Sealed: 54 books, 1,626 sides,
held back from every design decision. Confirmation: 192 books, 5,636 sides, unexposed during development.
| stage | sealed cohort | confirmation cohort |
|---|---|---|
| model alone | 0.6770<!--claim:geo_raw_sealed--> | — |
| + per-book calibration | 0.8278<!--claim:route_sealed_baseline--> | 0.8334<!--claim:route_conf_baseline--> |
| + calibration + human review | 0.9151<!--claim:route_sealed_routed--> | 0.9248<!--claim:route_conf_routed-->, with 28.5%<!--claim:route_conf_share--> of sides reviewed |
The after-review figures are workflow outcomes, not autonomous model accuracy: every routed side is counted as
passing on the assumption that the reviewer corrects it, while unrouted sides retain the model's calibrated crop.
The five calibration spreads are excluded from routing, since an operator has already drawn them. The review share
is an operating point inside one archive, not a portable promise.
## Using it on another collection
The model can be applied to printed-book spreads from other scanning setups, subject to three conditions.
1. **Calibrate every book.** Away from the training archive this is not optional: the calibration is what adapts
the model to a binding and a finishing convention it has never seen. On a public seventeenth-century atlas
photographed by another collection, the model found the text block on all twelve spreads of the benchmark, and
the remaining observed error was a consistent margin offset, which is what the ten crops remove. A worked
example, not a measurement: no accuracy figure is claimed for other collections.
2. **Frame the paper as the training captures did.** Most of the frame, roughly centred. A wide surround needs
cropping before inference.
3. **Do not carry the review numbers across.** The disagreement score's level and ordering do not transfer, and
the threshold and review share describe one archive. Read the routed queue as sides to look at, not as a
workload forecast.
**Do not use it on handwritten manuscripts.** See the evaluation notes.
## Training data and attribution
Trained on completed page crops from the **Ibteda corpus** (<https://ibteda.org>): books finished by hand by the
archive's operators, whose crops are the target. The model reproduces a finishing convention; it does not invent
one. Any use should credit the Ibteda corpus and its operators as the source of the training crops.
The convention is narrow: 98.7%<!--claim:canvas_share--> of finished pages used one 2048×2897 Photoshop template,
and the export round-trip reconstructs that historical output to a median residual of
3.0<!--claim:canvas_residual--> px. Both are facts about one archive's finishing habit, not about the model.
## Evaluation notes
Methodological detail behind the figures above. Skip it to use the model; read it before quoting a number.
**Calibration design.** The calibration gain was measured on its own, larger cohort: held-out pass@80 rose from
0.7107<!--claim:cal_baseline--> uncalibrated to 0.8271<!--claim:cal_pooled--> with the single pooled offset. Two
choices are recorded as costs, not victories. Pooled, not per-side: using the same ten crops, a per-side estimator
harmed 8<!--claim:cal_harm_perside--> held-out books, while the pooled estimator harmed
2<!--claim:cal_harm_pooled-->; the estimator was the only difference. Five spreads, not seven: seven beat five by
+0.0104<!--claim:k7_delta--> pass@80, decisively (p<0.0001), and was declined because at corpus scale that point
costs thousands of extra operator pages.
**The review sweep, and a withdrawn claim.** Reaching a higher 0.930 target under a coverage *sweep*, which is not
the deployed rule, needs 34.5%<!--claim:route_sweep_930_sealed--> review on sealed and
30.5%<!--claim:route_sweep_930_conf--> on confirmation. A sweep point and the deployed point answer different
questions; the two were once mixed to produce a claim that had to be withdrawn, which is why this card keeps them
apart.
**Manuscripts.** The deployed model reads handwritten pages badly: 0.0727<!--claim:manuscript_blindspot-->
calibrated pass@80 on the manuscript sides evaluated, with the routing signal actively anti-ranked rather than
merely uninformative. A model built to fix this was measured and refused: it learned its manuscripts almost
perfectly and still failed both foreign reads. This model ships with the blind spot named rather than papered over.
**The disagreement score away from the archive.** On foreign captures the flip-consistency gap is large, signed
and material-dependent: it overstated a small error by more than an order of magnitude on one book, tracked a large
median on another, and on a fourth book ranked the two most nearly perfect sides last.
`review.score.disagreement_px` must not be read as a page error in either direction.
**Untested levers.** A larger input than 704 px, and letterboxing versus centring for a frame with no surround.
Both are unmeasured, not rejected.
## Reproducibility and versions
- File: `ibteda.pt`
- SHA-256: `359ae48b6a846e01d70a21c2c49426125f3b8950ae7b4736ba8cf954af60babe`
- Pinned by: pageimprover 0.1.0
- Code, tests and the claim ledger: <https://github.com/ibteda-org/pageimprover>
- Model identity, from the artifact's embedded contract: ResNet-34 trunk, checkpoint `best_pass80.pt` at epoch 59,
704 px letterboxed input built from a 1500×1000 working tile, 24 output numbers as declared above. The package
exposes this contract and refuses an artifact whose contract disagrees with the code.
- Hub revision: pinned in `pageimprover.config.ARTIFACT` inside the package, never in this card, because the
revision exists only after the commit that publishes it. `pageimprover fetch model` requests that revision alone.
- 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
2026-09-04 as `1a259aaeeb8d05524695dba7a2a969e395a0e482`, and cites it by commit. The release audit reproduces each
value against that commit's retained result files.
- Public benchmark: `pageimprover fetch benchmark coronelli_1693` from a checkout downloads a twelve-spread
seventeenth-century atlas with a digest per frame and reproduces the recorded framing measurements.
## Citation
```bibtex
@software{pageimprover_2026,
title = {PageImprover: page geometry for photographed book spreads},
author = {{Ibteda Digital Library}},
year = {2026},
url = {https://ibteda.org},
note = {Model artifact ibteda.pt (CC BY 4.0); code Apache-2.0 at https://github.com/ibteda-org/pageimprover}
}
```
## Licence and authorization
The code is Apache-2.0. The model artifact `ibteda.pt` is published under **CC BY 4.0**, with attribution to the
Ibteda Digital Library. The required credit line is:
> Trained on the Ibteda Digital Library corpus; the page crops are the work of its operators.
A written authorization from the rights holder, dated 2026-09-04, covers the code, the weights and the licence
they are published under, the benchmark metadata, this attribution, and the measurements derived from the
training data that appear in this card and in [`docs/claims.md`](https://github.com/ibteda-org/pageimprover/blob/main/docs/claims.md). The authorization is
correspondence and is retained with the project's private records; this repository states only that it exists
and what it covers.