---
title: Teaching a Small Model to Crop Two Book Pages
date: 2026-11-10
---

# Teaching a Small Model to Crop Two Book Pages

One photo of an open book contains two pages. Before I can extract the text, I need to separate those pages.

I can do this by hand. I select four corners around the left page, then four around the right page.
The problem is repetition. Each new photo needs another crop check.

I wanted a small model to suggest those corners. I also wanted to test it separately from the existing PDF and text extraction flow.
That experiment became [Book Page Cropper](https://huggingface.co/Aananda-giri/book-page-cropper).

The latest version follows the manual crops closely on new photos from familiar books.
It still makes serious mistakes on a book excluded from training. Every proposal needs review.

## The manual corrections became the dataset

Each saved correction gives me a training example: the complete photo and the page corners selected by a person.
The model receives the uncropped photo. It learns to predict the corner positions.

This distinction matters. Giving it an already cropped page would remove much of the problem I want it to solve.
It needs to find both pages inside the original scene, including the table and the gap between the pages.

The latest dataset contains 290 photos and 549 page annotations from two physical books.
I used 224 photos to train the published model and kept 66 photos for testing.

The training images keep the complete scene, but use fewer pixels.
The export reduces the longest side to 640 pixels. Training fits the image inside a 384 by 384 square.
Padding fills the unused space without stretching the photo.

At inference time, the runner accepts an original photo.
It converts its predicted positions back to that photo's pixels.

## First, I had to fix the data

Before changing the model, I reviewed how the examples reached the trainer.
That review found problems that could make the results misleading.

The split selected specific photos for testing.
But a loader could load every photo from the same books for training.
I changed it to load only the exact training rows.
The corrected training and test sets share no photo identifiers.

Some photos had only one page annotation. The earlier data could treat the other page as absent.
But an unfinished annotation does not prove that a page is missing.
The trainer now ignores unknown targets instead of teaching the wrong answer.

The same physical book also appeared under two source folders.
I grouped those folders before making book-level splits.
Otherwise, a test described as an unseen book could include a book already used for training.

Finally, old single-page selections could overwrite newer two-page corrections.
The exporter now selects the latest layout and the latest correction for each page side.

These changes made the next comparison more useful.
A better score needed to come from better predictions, rather than shared test images or stale labels.

## One model predicts eight corners

The earlier crop model directly predicted sixteen numbers: an x and y position for each of eight corners.
The new model predicts eight heatmaps instead.

A heatmap is a small grid of scores. Higher scores indicate a likely corner location.
Four maps describe one page. Four describe the other page.
The runner finds each map's strongest location and refines the position using nearby scores.

The model uses a pretrained MobileNetV3 Large network to find general image features.
I added and trained the part that predicts page corners from those features.
Training combines the real full photos with generated two-page spreads whose corner positions are known.

The result is one network with about 3.2 million learned values, called parameters.
It returns two page proposals. Each contains four corner positions and a flag for basic shape checks.
A separate image operation can use those corners to produce the page images.

The output also includes a page-presence score, but this score needs caution.
The book labels contain no confirmed absent-page examples.
The score therefore cannot reliably tell me that a page is missing, or that a crop is correct.
The runner keeps `requires_review` set to `true`.

## What the output looks like

The runner returns JSON with two page proposals.
This is a real prediction, with numbers rounded for readability:

```json
{
  "schema": "pageprep-spread-crop-v1",
  "image_size": [482, 640],
  "coordinate_space": "exif-transposed-photo-pixels",
  "pages": [
    {
      "slot": 0,
      "presence": 0.9999,
      "corners": [
        [40.07, 93.77],
        [261.94, 93.47],
        [250.09, 411.63],
        [36.12, 403.45]
      ],
      "valid": true
    },
    {
      "slot": 1,
      "presence": 0.9999,
      "corners": [
        [255.79, 94.16],
        [460.96, 102.63],
        [453.92, 417.49],
        [248.31, 410.33]
      ],
      "valid": true
    }
  ],
  "requires_review": true,
  "review_reasons": ["experimental-crop-only-model"]
}
```

| Field | Meaning |
|---|---|
| `schema` | The prediction format's version. |
| `image_size` | `[width, height]` after applying the display turn saved in the photo. |
| `coordinate_space` | Positions use that full photo, before model resizing and padding. |
| `pages` | The two proposed page slots. |
| `slot` | `0` or `1`, ordered along the spread's main axis. |
| `presence` | An uncalibrated page-presence score, not confidence that the crop is correct. |
| `corners` | Four `[x, y]` positions in photo pixels. The top-left origin is `[0, 0]`. |
| `valid` | Whether the proposed shape passes basic checks. |
| `requires_review` | Always `true` in this experimental release. |
| `review_reasons` | Why a person needs to inspect the proposal. |

The saved display turn comes from the photo's EXIF metadata.
The corner positions refer to the full photo after that turn, not the internal 384 by 384 image.

Corners run clockwise, starting at the corner with the smallest `x + y` value.
Slots run left to right, or top to bottom when the spread's main axis is vertical.
This order follows the photo's geometry. It does not establish text reading order.

The shape checks require a convex outline with corners inside the photo.
The outline must also cover more than 2 percent of the photo's area.
These checks can reject crossed corners, but they cannot prove that the crop contains the correct page.
The runner retains invalid proposals so they can be inspected.

`review_reasons` always includes `experimental-crop-only-model`.
It can also flag invalid corners or report `no-page-detected` when both presence scores are below 0.5.
The book labels do not validate absent-page detection, so these scores must not trigger page deletion.

Both the PyTorch and ONNX runners use this format.
The output contains crop coordinates and review information, rather than cropped image files or extracted text.
The [model README](https://huggingface.co/Aananda-giri/book-page-cropper#output-format) documents the same fields and checks.

## Try it in a notebook

Run these cells in order in Colab or Jupyter.
The example uses the generated cover image to demonstrate the workflow.
It is not an accuracy test on a real book photo.
The input image has no crop lines. The plotted lines come from the model.

1. Download the model and its runtime files.

```python
!pip install huggingface_hub

from huggingface_hub import snapshot_download

snapshot_download(
    repo_id="Aananda-giri/book-page-cropper",
    local_dir="book-page-cropper",
)
```

2. Install the model requirements and the plotting package.

```python
!pip install -r book-page-cropper/requirements.txt
!pip install matplotlib
```

3. Download the example image.

```python
from pathlib import Path
from urllib.request import Request, urlopen

image_path = Path("book-page-cropper-cover-v2.png")
url = "https://aanandagiri.com.np/api/images/book-page-cropper-cover-v2.png"
request = Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(request, timeout=60) as response:
    image_path.write_bytes(response.read())
```

4. Run the model and save its JSON output.

```python
!python book-page-cropper/predict.py --image book-page-cropper-cover-v2.png > predictions.json
```

5. Draw the proposed corners over the image.

```python
import json
import matplotlib.pyplot as plt
from PIL import Image, ImageOps

prediction = json.loads(Path("predictions.json").read_text())
photo = ImageOps.exif_transpose(Image.open(image_path)).convert("RGB")
fig, ax = plt.subplots(figsize=(12, 7))
ax.imshow(photo)

for page in prediction["pages"]:
    if page["presence"] < 0.5:
        continue
    points = page["corners"] + [page["corners"][0]]
    xs, ys = zip(*points)
    color = ["tab:blue", "tab:green"][page["slot"]]
    label = f"Page {page['slot'] + 1}"
    if not page["valid"]:
        color = "tab:red"
        label += " — check invalid shape"
    ax.plot(xs, ys, "o-", color=color, linewidth=2, label=label)

ax.set_title("Model crop proposals — review before use")
ax.axis("off")
if ax.lines:
    ax.legend()
plt.tight_layout()
plt.show()
```

Red outlines identify proposals that failed the basic shape checks.
Every proposal still needs review, including blue and green outlines.

![Model page outlines on the clean generated example](https://aanandagiri.com.np/api/images/book-page-cropper-demo-v2.png)

The outlines above are actual model outputs. The right outline includes some extra margin.
To test your own photo, replace the example filename in the prediction command and update `image_path`.
The JSON values will differ from the earlier sample because this image has different dimensions and content.
No Hugging Face token is required.

## The familiar-book result improved

I measured how closely the model's crop overlaps the human crop.
The metric is called intersection over union, or IoU.
A score of 1 means exact overlap. A score of 0 means no overlap.

For the first comparison, I used the same 31 test photos and the same 57 updated page annotations.
The earlier coordinate model scored 0.801 average IoU. The latest heatmap model scored 0.947.

The larger test set contained 125 annotated pages from 66 photos.
The latest model averaged 0.951 IoU. Of those 125 pages, 124 scored at least 0.85.

This is a useful improvement for crop suggestions.
It does not mean that 95 percent of crops are safe to accept automatically.
Overlap measures the shape. It does not prove that every line of text remains inside the crop.

The experiment changed several things together: the labels, the amount of data, the training process, and the model.
These results do not show how much each change contributed.

## A different book was harder

New photos from familiar books are the easier test.
Different captures can even show the same printed page.
I also needed to test whether the method could handle a book excluded from training.

I trained separate versions for that check.
One learned from Book B and was tested on Book A. Another learned from Book A and was tested on Book B.

| Test | Mean overlap |
|---|---:|
| New photos from familiar books | 0.951 |
| Book A excluded from training | 0.806 |
| Book B excluded from training | 0.726 |

The first row contains 125 page annotations. The other rows contain 395 and 154 annotations, respectively.
Each row uses its own training split.
The downloadable model learned from both books; the book-exclusion rows measure separately trained versions.

Visual checks explain why review remains necessary.
Some predicted corners cross each other. Blank pages and covers cause failures.
A geometric check can reject a crossed shape, but a valid shape can still select the wrong region.

These are development results from two books, with repeated comparisons during model selection.
They do not establish accuracy across other books, phones, or tables.

## More online data did not help this version

I also tried document photos from [SmartDoc 2015](https://github.com/jchazalon/smartdoc15-ch1-dataset), available under CC BY 4.0.
I prepared 480 training frames and reserved another 120 frames by document model.

Adding the single-document photos barely changed the familiar-book score and reduced both book-transfer scores.
I kept that version out of the public release.
The reserved frames were not used to choose the book model.

Single sheets and open books have different boundaries.
That difference might explain the result, but this experiment does not prove the cause.
The measured result was enough to reject this data mix for now.

## A smaller crop stage, with clear limits

The public release includes [weights, run instructions, and the JSON output format](https://huggingface.co/Aananda-giri/book-page-cropper).
It runs separately from the book app, its database, and text extraction services.
The source book photos are not included.

There is also a 12.7 MB ONNX file for CPU inference without PyTorch.
It passed numerical comparison with PyTorch on all 66 test photos.
After the model loaded, one prediction took about 28 milliseconds on the notebook server with two CPU threads.
That timing excludes image loading and resizing. It is not a phone latency measurement.

This release suggests crop boundaries.
It does not decide reading orientation, blur, blank-page deletion, duplicate preference, or retake need.
Those tasks need separate labels and tests.

The next useful test is another physical book with a different capture setup.
I also need checks for lost text and examples of confirmed absent pages.
For now, the model can suggest the corners, and a person makes the final decision.

*The cover is an AI-generated book illustration. It contains no crop annotations.*
