|
Download README.md from ibteda-org/pageimprover: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/ibteda-org/pageimprover/resolve/main/README.md
- Command line
-
hf download hf://ibteda-org/pageimprover/README.md
-
curl -L -o README.md https://huggingface.co/ibteda-org/pageimprover/resolve/main/README.md
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. | |