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, which names its cohort, calibration state and evidence. A build gate rejects any figure that drifts from that ledger.

Quick start

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:

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 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 px tile
Format JPEG, PNG, TIFF, WebP

The model's input/output contract is embedded in the artifact itself:

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.

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

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 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 crop adjustments in total, pooled into one volume-wide offset (calibrate/pooled_median.py). The offset is applied only when it reaches 40 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 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 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 —
+ per-book calibration 0.8278 0.8334
+ calibration + human review 0.9151 0.9248, with 28.5% 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% 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 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 uncalibrated to 0.8271 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 held-out books, while the pooled estimator harmed 2; the estimator was the only difference. Five spreads, not seven: seven beat five by +0.0104 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% review on sealed and 30.5% 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 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 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

@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. The authorization is correspondence and is retained with the project's private records; this repository states only that it exists and what it covers.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Evaluation results

  • raw pass@80 - sealed sensitivity test, 54 books, 1,626 routed sides (calibration pages excluded) - uncalibrated, unrouted [claim geo_raw_sealed] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.677
  • held-out pass@80, uncalibrated - calibration confirmation, 249 books, 5,022 held-out sides per repeat, 20 repeats - uncalibrated [claim cal_baseline] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.711
  • held-out pass@80, one pooled 10-crop book offset - calibration confirmation, 249 books - calibrated (pooled, 10 crops) [claim cal_pooled] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.827
  • calibrated pass@80, before review - routing confirmation, 192 books, 5,636 held-out sides - calibrated, unrouted [claim route_conf_baseline] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.833
  • pass@80 at the deployed review rule - routing confirmation, 192 books, 5,636 sides - calibrated + routed (28.5% review) [claim route_conf_routed] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.925
  • calibrated pass@80, before review - sealed sensitivity, 54 books, 1,626 sides - calibrated, unrouted [claim route_sealed_baseline] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.828
  • pass@80 the deployed review rule delivers - sealed sensitivity, 54 books, 1,626 sides - calibrated + routed (deployed rule) [claim route_sealed_routed] on Ibteda Digital Library held-out cohorts (in-archive; each metric names its cohort)
    self-reported
    0.915