Ibteda Digital Library began with a flat table, a Nikon D5300 mounted above it, inexpensive Philips LED bulbs, and glass salvaged from a photocopier. We had no book scanner, grant, or digitisation lab. One exposure gave us both facing pages, plus everything around them: the black tabletop, gutter shadow, paper edges, stains, handwritten notes, and uneven margins.
For almost a decade, capture was the quick part. After the shutter, one of us still had to open every photograph in Photoshop and turn the spread into two finished page images.
The Manual Bottleneck
The archive grew out of an earlier project that did not make it into print. Around 2015, while researching a modern illustrated and textual edition of Ghalib's Diwan for the 150th anniversary of his death in 2019, we discovered how difficult it was to find historic editions. Important volumes were out of print, difficult to access in university collections, or sitting on private shelves where readers and researchers could not reach them. That frustration became Ibteda Digital Library: if the literature was inaccessible, we would have to find the physical books and build the archive ourselves.
Ibteda Digital Library is a private community archive in Pakistan. We were a small team working without the equipment budget of a national institution or the staff of a commercial scanning bureau. The capture rig below was typical of how we solved problems: use the camera we had, build the table ourselves, and make the setup repeatable enough to keep going.
Pressing a book against glass reduced the paper curve near the spine and gave us much flatter exposures than the budget suggested. The photograph was still messy. It included the binding, table, gutter shadow, library stamps, damaged edges, and more margin than we usually wanted in the published page. Photoshop was where we decided what belonged.
For years, our production line had only three stages:
- CapturePhotograph an open spread through the glass.
- Post-processAn operator finishes both pages in Photoshop.
- PublishStore two ordered, consistently sized page images.
This worked, and the finished books were consistent. The problem was that every spread consumed a fresh set of small decisions: where the page ended, how much to rotate it, which margin to keep, what to do with a damaged corner, and whether a dark mark was part of the object or scanner noise. None of those clicks felt expensive on one page. Across hundreds of pages they became hours, and across thousands of books they became the limit of the archive.
The catalog now contains over 5,000 books. The analyzed inventory holds 822,161 dual-page photographs, or approximately 1.64 million page sides. We finished 575,729 of those sides in Photoshop across 1,765 books. The operating history covers 2016 into 2026 and reconstructs about 10,200 active processing hours; it does not include every hour that every contributor ever spent.
What the Cameras Recorded
We can trace most of the capture years from Nikon maker notes instead of memory. The D3300 appears at the start of the archive in 2016. The D5300 became the main body in February 2018; its final retained photograph from March 2023 reports a shutter count of 575,747. Nikon lists the D5300 shutter as tested to more than 100,000 cycles, so the recorded count reached about 5.8 times that reference point. We used the D3300 again after that, added a D3500 from August 2023 through April 2024, and returned to the D3300 as the main body in May 2024. The last retained photographs from those bodies report 83,373 actuations on the D3500 and 326,128 on the D3300.
Exact camera timestamps survive for 807,295 of the 822,161 photographs. Grouping those exposures into sessions with a 30-minute inactivity gap produces 2,467 capture sessions and about 3,000 active capture hours, averaging 268 photographs per active hour. The remaining 14,866 files retain date-level evidence but are excluded from the hourly calculation.
Capture time and Photoshop time are separate clocks in our records. The 10,200 processing hours come from reconstructed work sessions built from finished-page modification timestamps using the same 30-minute inactivity rule. That works out to about 56 finished page sides per active processing hour. These sessions cover active post-processing; camera time and unrecorded contributions sit outside that estimate.
The collection was far from tidy. Nineteenth-century lithographs sit beside mid-century prose, dictionaries, magazines, illustrated volumes, pages full of marginal notes, cleaner machine-set type, and English left-to-right books. A rule tuned only for neat Urdu prose would fail on a large part of the shelf.
By the final years, capture had outpaced post-processing. New photographs accumulated faster than we could finish them, and the repetitive Photoshop work was becoming difficult to sustain.
I first tried conventional computer vision: contour detection, projection profiles, paper-colour thresholds, and edge heuristics. Each approach worked on a narrow subset of books and failed across the collection. A rule tuned for clean machine-printed pages broke on lithographed borders. Thresholds failed on stained paper. Gutter shadows and curved bindings produced convincing false edges. OpenCV remained useful, but fixed rules could not reliably choose our preferred crop.
When we suspended daily operations in April 2026, 526,392 dual-page photographs were still unfinished. They had already been captured and preserved, but completing them through the old workflow would require years of additional operator work.
The breakthrough came when I stopped treating the Photoshop output only as publication files. Those 575,729 finished page images recorded ten years of crop decisions: rotation, margins, physical side, and the book-specific framing repeated across hundreds of pages. They still needed correspondence recovery before they could become labels. I began matching each finished page back to its raw spread and reconstructing the geometry the operator had chosen.
Finished Pages Became Labels
The raw photographs contained the physical paper edge, but our finished pages often sat slightly inside it. The inset changed from book to book. We might remove a gutter shadow while preserving marginal writing only a few millimetres away. A generic “find the bright paper rectangle” detector could locate the sheet and still produce the wrong archival page.
I also found two conventions that were easy to miss if you only inspected the raw photographs. Most of our Photoshop work used a 2048 × 2897 page template, and we stretched the selected crop to fill that template instead of preserving the photographed aspect ratio. Once I reproduced that transform exactly, the rendered result matched the operator page with a median residual of 3.0 pixels. That isolated later geometry errors to model prediction or correspondence.
These pairs show what the old workflow had actually taught us.
Dense dictionary · 1877
Useful ink reaches the edge
On this dictionary, finding the paper is the easy part. The finished page tells us how much border and marginal writing we chose to preserve.
Poetry · 1925
A stable frame is still book-specific
The printed frame is obvious, yet the whitespace around it comes from a book-wide finishing choice that we repeated page after page.
Periodical · 1945
The page is more than its text block
Advertising, photographs, halftone texture, and display type all reach the edge. Cropping around body text would destroy the layout.
English · 1948
Geometry is independent of reading direction
The physical page still sits on the left or right side of the camera frame. Reading direction only changes how we order the two finished pages afterward.
Modern Urdu · 1950
Cleaner printing still needs a crop
Later typography and cleaner paper remove some visual noise, but we still need a stable page boundary, scale, and canvas for the whole book.
The supervised-dataset idea was now clear. The dataset itself still had to be built. The raw spreads lived in one collection and the finished pages in another, with no trustworthy table linking a finished page to a source spread, a physical side, and four source-image corners. Building that table became the first engineering problem.
Recovering Crop Geometry
File numbering got me close. Two finished pages usually came from one spread, so I could estimate a source frame from the page number and a book-specific offset. It broke around exactly the pages that make archives interesting: covers, blanks, inserts, missing leaves, advertisements, and front matter with its own numbering. I used the arithmetic to open a small search window, then required the pixels to prove the match.
For each finished page, I ran SIFT against nearby candidate spreads. I filtered descriptors with Lowe's ratio test in both directions, kept only mutual pairs, and fitted a homography with USAC_MAGSAC. Once the homography was stable, I projected the four corners of the finished page back into the raw spread. That projection became the crop quadrilateral.
The matcher was only half the job. Dense Urdu print repeats strokes, words, borders, and column patterns, so a neighboring page can produce a transformation that looks convincing at first glance. I spent more time making the pipeline reject these near-misses than improving the raw match count. For this capture setup, an accepted correspondence had to satisfy:
The accepted matches were usually nowhere near these cutoffs. Correct pages often had hundreds of inliers and beat neighboring candidates by 50–250×. I chose conservative gates because a false label is worse than losing one training page. Anyone adapting this method should keep the same kinds of checks and remeasure the actual thresholds on their own camera, paper, and typography.
The acceptance logic below is abridged from the implementation. The src and dst points have already survived Lowe's 0.75 ratio test in both directions and a mutual-match check.
homography, inlier_mask = cv2.findHomography(
src.reshape(-1, 1, 2),
dst.reshape(-1, 1, 2),
cv2.USAC_MAGSAC,
3.0,
)
if homography is None:
accepted = False
else:
quad = cv2.perspectiveTransform(page_corners, homography).reshape(-1, 2)
inliers = int(inlier_mask.sum())
area_share = polygon_area(quad) / (spread_width * spread_height)
accepted = (
np.isfinite(quad).all()
and inliers >= 100
and inliers / len(src) >= 0.40
and inliers / max(runner_up_inliers, 1) >= 5.0
and is_convex(quad)
and 0.15 <= area_share <= 0.45
)
Across 36,450 attempted page correspondences, 35,648 were accepted: 97.8%. Interior pages accepted at 98.6%; pages near book edges accepted at 77.9%, where covers, one-sided leaves, and sequence changes are more common.
I made an expensive mistake early in dataset construction: one bad file could disqualify an entire book. An empty image, duplicated filename, or unreadable spread might throw away hundreds of otherwise usable examples. I changed the rule to fail locally. The builder records the defective file and keeps the rest of the volume unless a substantial part of the book is missing.
I also considered excluding periodicals because they looked like noise beside ordinary prose books. That would have been a convenient way to make the dataset cleaner and the production problem worse. Periodicals form a meaningful part of the unfinished shelf, and the few issues we had already completed were our only labels for advertisements, photographs, mixed columns, and display type.
How Pages Were Chosen
I found it useful to separate five different selection decisions. When they are collapsed into one vague idea of “sampling pages,” leakage becomes very easy to miss.
Candidate search chooses the few raw spreads that might contain a finished page. Sequence arithmetic and a tracked offset keep this cheap.
Label acceptance decides whether the image evidence is strong enough to turn one candidate into supervision.
Training selection chooses which accepted books contribute examples to a particular model run.
Calibration sampling chooses the five spreads an operator corrects in a new book.
Evaluation reserves untouched books and pages that decide whether an experiment worked.
I sampled through the depth of each book instead of taking only the first leaves. Front matter contains covers, title pages, stamps, and one-sided frames. Middle and late pages add binding drift, changes in page stack, and damage that only appears deeper in the book.
The selected corpus kept dense dictionaries, damaged borders, magazines, illustrated volumes, cleaner modern typography, and both reading directions. I treated the book and its capture conditions as the unit of diversity. Adding another hundred adjacent pages from one volume was less useful than adding a book with a layout the model had not seen.
The same mistake appeared later when I mined retouch labels only from front matter. That run found 1,732 capped page sides while 35,646 labelled sides were eligible across the books. Marks and edge damage were scattered through the sequence, so I changed the miner to sample across the whole book.
Inside One Training Record
I made one complete spread the unit of geometry training. The model sees both pages together and predicts one rotated rectangle for each physical side.
The concrete record below comes from a validation spread in Kulliyat-e-Ghalib (1924). Both sides are present. The working image is 1500 × 1000, which I scale to 704 pixels wide and center vertically inside a 704 × 704 frame.
For this row, the scale is 0.469333, horizontal padding is zero, and continuous vertical padding is 117.333 pixels. I use RGB (114, 114, 114) for the raster padding. Black looked natural as a letterbox colour, but it was a bad training signal because the table and deep gutter shadows were already close to black.
Each side starts as four ordered corners—top-left, top-right, bottom-right, bottom-left—and is stored as:
t = (cx, cy, log w, log h, sin θ, cos θ)
I normalize center and size to the spread, store width and height in log space, and encode rotation as sine and cosine so the target has no angle-wrap discontinuity. A separate availability bit says whether each physical side exists. When a spread contains one page, the missing side is masked out of the loss instead of being represented by a fake zero rectangle.
| Field | Physical left | Physical right |
|---|---|---|
| Available | 1 | 1 |
| Target vector | (0.340810, 0.487675, −1.074716, −0.623448, 0.037104, 0.999311) | (0.647466, 0.494762, −1.081413, −0.616596, 0.022046, 0.999757) |
| Top-left | (126.84, 150.29) | (340.67, 155.72) |
| Top-right | (367.02, 159.21) | (579.34, 160.99) |
| Bottom-right | (353.02, 536.36) | (570.97, 540.90) |
| Bottom-left | (112.84, 527.44) | (332.29, 535.64) |
Book identity is stored only so the dataset builder can keep every page from a volume on one side of the split; the model never receives it.
The easiest augmentation bug is to move the image and forget that the labels live in the same coordinate system. In the retained example below, I scale the frame to 92%, shift it right by 21.12 pixels, and shift it upward by 14.08 pixels. The same affine matrix moves both quadrilaterals.
Split by Book
I split the dataset by book from the beginning. Pages in one volume share paper, binding, camera position, page ratio, typography, defects, and our preferred crop inset. A random page split would put near-identical neighbors in training and evaluation, then report how well the model handled another page from a book it already knew.
Production needed a harder answer: what happens when the next book has never appeared in training?
Rejected
Random pages Neighboring pages leak book identity across the boundary.
I pinned the held-out books while the correspondence corpus was still growing and protected a separate set of confirmation books from future training. Without that second guard, a new dataset rebuild could silently absorb a measurement book into training while the evaluation script continued to produce a number.
Book-level splitting lowered the scores, as expected. It also made them relevant to the books we still had to process.
The Geometry Model
I kept the first deployable geometry model simple:
- 704 × 704 spreadLetterboxed dual-page image
- ResNet-34 trunkImageNet-pretrained shared visual features
- Left headSix values for the physical-left page
- Right headSix values for the physical-right page
The ResNet-34 trunk ends in global average pooling. With a 704-pixel input, its 22 × 22 feature map collapses to a 512-value vector. I attach two small heads to that vector: one for the physical-left page and one for the physical-right page. Keeping the sides separate made missing-page masking straightforward and avoided mixing geometry with reading order.
For the loss, I decode each six-value prediction back into four corners and apply Smooth L1 in canonical page pixels. This saved me from inventing arbitrary weights for center, width, height, and rotation. If an angle error and a size error move a corner by the same amount, the loss treats them as equally important.
This is the core of the masked loss. decode_corners normalizes the predicted sine and cosine, reconstructs the rotated rectangle, and returns four corner coordinates in canonical pixels.
def corner_loss(pred, target, available):
total = pred.new_zeros(())
count = available.sum().clamp_min(1.0)
for side in range(2):
values = slice(side * 6, (side + 1) * 6)
per_corner = torch.nn.functional.smooth_l1_loss(
decode_corners(pred[:, values]),
decode_corners(target[:, values]),
beta=10.0,
reduction="none",
).sum(dim=2)
per_page = per_corner.mean(dim=1)
total += (per_page * available[:, side]).sum()
return total / count
I didn't use horizontal flips as ordinary training augmentation because a flip swaps the physical sides and changes corner identities. I kept the flipped view for inference, where disagreement between the original and flipped predictions became one of the review signals.
The bridge run on an NVIDIA L4 GPU used 8,824 training spreads from 378 books and 745 validation spreads. The early log was ugly: epoch 1 managed pass@80 0.029 with a median worst-corner error of 199.1 pixels, and the high-learning-rate part of the one-cycle schedule repeatedly improved and then collapsed. The run only became stable late. Epoch 39 reached 0.661, epoch 42 reached 0.731, and epoch 47 reached 0.770. I selected epoch 59, which reached 0.791 with a 50.5-pixel median worst-corner error.
I ran that bridge before comparing ResNet-34 with ResNet-50 on the new GPU. Reproducing the smaller model first kept the architecture fixed while the numerical environment changed. The 0.791 figure belongs to validation during that run; the sealed-book result comes later.
Measuring Crop Error
My first instinct was to average coordinate error. That hid exactly the failures an archivist notices. Three corners can be excellent while the fourth cuts through a line of text.
I project the four predicted corners into the operator page's canonical coordinate system, measure the Euclidean distance at each corner, and keep the largest distance as the page error:
Epage = max(dTL, dTR, dBR, dBL)
pass@80 counts a page as correct when that worst corner is at most 80 canonical pixels away.
| Corner | Error |
|---|---|
| Top-left | 33.51 px |
| Top-right | 87.90 px |
| Bottom-right | 106.52 px |
| Bottom-left | 23.30 px |
Fail · worst corner 106.52 px
| Corner | Error |
|---|---|
| Top-left | 63.62 px |
| Top-right | 26.68 px |
| Bottom-right | 3.11 px |
| Bottom-left | 79.50 px |
Pass · worst corner 79.50 px
I chose the 80-pixel threshold after inspecting rendered pages, not from the metric curve alone. Around 20 pixels, I usually could not distinguish the crop from the operator page. Near 80, a descender or one line might start to trim. At roughly 125 pixels, margins looked visibly uneven and a sliver of the facing page could enter even when the main text survived. Larger misses could remove marginal writing or expose the binding.
On the sealed sensitivity cohort, the 704-pixel ResNet-34 reached raw pass@80 0.6770 across 54 books and 1,626 routed sides after calibration pages were excluded. That was useful enough to build on, but about one side in three still missed the geometry bar.
When I grouped the failures by book, the worst volumes were often centered and rotated correctly. Their rectangles were simply too large or too small in the same way throughout the book. Subtracting each book's median bias reduced page-to-page scatter on several catastrophic volumes to 33–68 pixels, which was already below the acceptance threshold.
The network found the physical page but missed our book-specific finishing preference.
Ten Crops Beat a Bigger Model
The camera image contains the physical paper edge, but the desired finishing inset is invisible. One book may look right with a 40-pixel inset and another with 120 pixels. Training on old books cannot reveal that preference for a volume the model has never seen.
I stopped asking the network to guess and added a small calibration step.
The Studio selects five spreads through the book. An operator adjusts both physical pages, giving me ten residual vectors between the base prediction and the desired crop. I take the element-wise median of those ten residuals and store one correction for the volume. If that correction would move a corner by at least 40 canonical pixels, I apply it to every remaining spread.
The estimate assumes one physical volume. Pooling different bindings under one Studio book key would turn one common offset into a mixture.
For the 1865 Bahar-e-Ajam example, the pooled correction moved corners by 115 canonical pixels, above the 40-pixel activation gate. I kept a sixth spread out of calibration so the before-and-after comparison would be honest.
On that held-out spread, worst-corner error fell from 121.21 to 25.17 pixels on one side and from 117.03 to 34.08 pixels on the other.
The retained public evidence includes the five selected spreads and the before, after, and reference pages. The ten individual correction crops and their row-by-row residuals were not retained as publishable artifacts, so I have left that table unreconstructed.
The aggregate confirmation covered 249 books, 5,022 held-out page sides per repeat, and 20 repeats. Uncalibrated pass@80 was 0.7107. The ten-crop pooled correction raised it to 0.8271.
I expected separate left and right corrections to work better because the two physical sides do have measurable bias. The experiment went the other way. Splitting the same ten observations produced two noisy five-sample medians. On held-out books, the per-side estimator materially harmed eight volumes; the pooled estimator harmed two.
I kept one pooled book correction because ten observations were more valuable than a theoretically cleaner split with only five samples per side.
One Canvas per Book
Our Photoshop workflow had already settled one presentation question: pages inside a book should keep the same output size even though their source crops change from spread to spread.
Among 1,765 historically completed books, 1,762 have exactly one page size; the other three contain one stray page each. Canvas size is selected per book. A small poetry volume and a large dictionary can have different dimensions, but the pages inside either book should not jump in size while someone reads.
I map each final quadrilateral onto the book's selected canvas with a projective transform and the same anisotropic scale-to-fill convention we used in Photoshop. Preserving aspect ratio would be a reasonable design for another archive, but it would not reproduce our historical pages. The important lesson is to measure the old workflow before replacing it with a cleaner-looking assumption.
RTL and LTR
I kept reading direction out of the geometry model. It predicts the physical-left and physical-right pages visible in the camera frame.
After rendering both pages, I infer reading direction from the recovered page-number progression and order the outputs accordingly. Urdu books run right-to-left; the 1948 English example runs left-to-right. The same correspondence, prediction, calibration, warping, and canvas implementation handles both directions.
The English example proves that the workflow can order an LTR book. It does not establish equal aggregate accuracy across scripts; that would need its own held-out cohort.
Building Retouch Labels
Once geometry was stable, I moved to the uglier question: which marks could we remove without touching the printed page?
The Photoshop pages supplied label-building evidence. An unfinished book has no finished reference at runtime, so the deployed model works from the raw page alone. Historical output decides what to teach and then leaves the inference path completely.
My first attempt was the obvious one: register the raw and finished pages, subtract them, and threshold the difference. The result was unusable. Tiny alignment errors lit up both sides of every Urdu stroke, and ordinary Photoshop contrast changes looked like removals. On one measured page, the naive mask covered 7.28% of the image; 49.2% of that area was ink we had kept.
I replaced the binary difference with three label-building states:
The network itself is still binary and emits one removal logit per pixel. IGNORE simply removes uncertain pixels from the loss.
The shipped example below is a 512 × 512 patch from Naqoosh: Adab-e-Alia Number (1960). I kept the raw patch, a 16-bit ordinal map for candidate regions, and a state table. The Photoshop page is absent by design, making any runtime-reference leak easy to detect.
| Ordinal | Tier | Area | Strength | Arm A | Arm B |
|---|---|---|---|---|---|
| 0 | Background | — | — | KEEP | KEEP |
| 3 | Spot | 4,154 px | 2.9002 | REMOVE | IGNORE |
| 4 | Spot | 2,463 px | 3.5968 | REMOVE | IGNORE |
| 6 | Mark | 30,765 px | 5.7838 | REMOVE | IGNORE |
| 8 | Spot | 3,451 px | 6.2100 | REMOVE | REMOVE |
I compared two label cuts while holding the images, book split, architecture, schedule, threshold, and evaluation path fixed. Inclusive Arm A reached mark-tier IoU 0.5581 but produced 161 measured false-positive diacritic pixels per patch. Strict Arm B moved weaker regions to IGNORE, reached 0.6024 IoU, and produced 0.0.
Detect, Then Reconstruct
The geometry network can summarize a spread with twelve numbers. Retouching needs to remember exactly where a mark appears, so global pooling was the wrong tool. I used a U-Net-style segmenter with a ResNet-34 encoder and skip connections that carry location back into the decoder.
- Raw page tiles512 px tiles, 256 px stride
- ResNet-34 encoderImageNet-pretrained spatial features
- Skip decoderRestores resolution using encoder features
- Removal mapOne logit per pixel
- Component gateArea and confidence abstention
- Classical fillOpenCV and NumPy reconstruct paper
The encoder comes from the same ResNet-34 family as the geometry model, with fresh weights above the ImageNet initialization. Geometry and retouching need different representations; a shared backbone name does not make their learned features interchangeable.
I trained with masked binary cross-entropy plus soft Dice. Cross-entropy supplies local supervision, while Dice keeps sparse removal regions from disappearing inside the much larger KEEP background. IGNORE pixels contribute to neither term.
At inference, I tile each page with 50% overlap and average probabilities in the overlap. This keeps tile seams from deciding the mask. I drop connected components below the 1,500-pixel mining floor because the dataset contains no supervised removals smaller than that. Low-confidence components are rejected whole instead of being eroded into fragments; half a stamp or half a dark band usually looks worse than leaving it alone.
The network only proposes a support mask. I reconstruct the paper with ordinary OpenCV and NumPy:
- expand the mask with a small dilation so mark-coloured edge pixels cannot seed the fill;
- downsample the page and inpaint at one-eighth scale to follow low-frequency paper shading;
- upsample that field into the accepted support;
- estimate high-frequency paper grain from a ring around the region and add deterministic texture;
- feather the boundary over three pixels;
- copy the raw page back outside the declared support exactly.
The reconstruction code uses classical image processing. It follows the low-frequency paper field at one-eighth scale, restores grain measured around the mask, and finally reassigns the original pixels outside the permitted support.
def reconstruct_paper(image, mask, support):
height, width = mask.shape
small = cv2.resize(
image, (width // 8, height // 8), interpolation=cv2.INTER_AREA
)
small_mask = cv2.resize(
mask, (width // 8, height // 8), interpolation=cv2.INTER_NEAREST
)
small_mask = cv2.dilate(small_mask, np.ones((3, 3), np.uint8))
base = cv2.resize(
cv2.inpaint(small, small_mask, 5, cv2.INPAINT_TELEA),
(width, height),
interpolation=cv2.INTER_LINEAR,
)
paper_ring = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21, 21))
ring = (cv2.dilate(mask, paper_ring) > 0) & (mask == 0)
grain = image.astype(np.float32) - cv2.GaussianBlur(image, (0, 0), 3)
noise = np.random.default_rng(0).normal(
0.0, float(grain[ring].std()), size=(height, width)
)
base[mask > 0] = np.clip(
base[mask > 0].astype(np.float32) + noise[mask > 0, None],
0,
255,
)
distance = cv2.distanceTransform(support, cv2.DIST_L2, 3)
alpha = np.clip(distance / 3.0, 0.0, 1.0)[..., None]
output = (
image.astype(np.float32) * (1.0 - alpha)
+ base.astype(np.float32) * alpha
)
output = np.clip(output, 0, 255).astype(np.uint8)
output[support == 0] = image[support == 0]
return output
This separation made the failure boundary much easier to reason about. The model decides where there is enough evidence to act; the fill estimates a local paper field from surrounding pixels. I canceled the planned generative inpainter after the classical method reached the measured noise floor against operator pages. There was little demonstrated headroom and far more opportunity to invent texture or soften text.
On the held-out Bahar-e-Ajam pair, the model abstained on one side. On the other, it changed 2,604 pixels—less than 0.05% of the page—and exactly zero pixels outside the accepted support.
The 1950 example earlier in the article contains a visible stamp, but no verified automated removal sequence survives for that page. I left it as historical evidence. The before-and-after above is a smaller background-healing case. If a heavy stamp crosses printed text, the current pipeline abstains unless both the mask and the reconstruction are supported.
Before publishing retouched pages, we ran randomized A/B comparisons on one development book. Of 39 answered pages, the operator preferred the retouched side in 30, preferred the raw side in one, and marked eight as indistinguishable. Another 38 sides were absent because the model had abstained. That works out to 76.9% preference among answered comparisons and 2.6% against retouching. This 76.9% preference score comes from a single development book; we haven't measured it across the full archive yet.
Routing Uncertainty
I didn't want a single confidence score that sent pages to a queue without explaining why. The router stores named reasons and the measurement behind each one:
Flip disagreement is a proxy derived from two predictions. I chose its 87-pixel operating threshold from held-out risk-coverage curves inside this archive instead of eyeballing a few examples.
On the in-archive confirmation cohort, calibrated baseline pass@80 was 0.8334. Reviewing 28.5% of page sides raised it to 0.9248. Reaching 0.930 required 30.5% review.
On in-archive sealed sensitivity data, baseline was 0.8278. The same 28.5% review point reached 0.9151. Approximately 34.5% review was required for 0.930.
I had to correct one published interpretation here. An earlier reading paired 28.5% review with 0.930 by combining a quality target from one curve with coverage from another model. The actual confirmation point is 0.9248, and the sealed result is 0.9151. I kept the correction in the record because a router is only useful if its operating point survives on books that were protected from threshold selection.
Where the score stopped transferring
With the in-archive routing curves fixed, I went looking for open-spread captures from elsewhere. That was harder than I expected: most public digitisation had already split books into single pages. The survivors included three historical atlases, each giving me consecutive pages from one physical volume and a different way for the score to fail.
On the 1676 English atlas, all 24 sides received a router score and 22 had operator-drawn truth. Every labelled side cleared the 80-pixel bar, at a median error of 31.2 pixels, yet the router flagged 21 of all 24 sides. On the 1874 atlas, median error was 15.0 pixels and two of 17 labelled sides missed the bar, but neither appeared among the five highest disagreement scores. The 1693 atlas failed differently: its error was systematic and one-sided, with a median of 57.6 pixels and eight of 23 sides over the bar. Disagreement and actual error had a Spearman rank correlation of only ρ = +0.018 across those 23 sides.
The crop model travelled. The alarm attached to it did not.
My best guess starts with lighting, but not with a simple studio-good, DIY-bad story. Lighting is not just exposure; it is a spatial field. An illumination gradient has a direction. So does a gutter shadow. Mirror the frame and both reverse relative to the gutter, backdrop, and the model's left and right outputs.
The Ibteda training corpus came from one repeatable environment: an overhead camera, inexpensive LED bulbs, a glass platen, a dark table, and recurring gutter shadow. During training I deliberately excluded horizontal flips because they swap the page sides and corner identities. Flip-TTA later asks the model to behave as though that mirror symmetry holds anyway. If the model has borrowed any side-specific capture cues, the direct prediction can remain accurate while the mirrored prediction drifts. The router sees the distance between them and calls it uncertainty; the operator sees a usable crop.
That explanation fits the observations, but I did not isolate lighting from framing, binding, resolution, or post-processing. The 1676 atlas also blocks the easier story: if clean institutional captures necessarily produced low disagreement, the router would not have flagged almost the whole volume.
Paper fill fixed geometry, not confidence
Framing produced an even clearer warning against treating disagreement as physical error. On one small-paper conservation cohort, normalisation fixed the actual geometry: 101 crop edges that had fallen outside the paper became zero.
The crops improved. The disagreement score became worse on all 60 frames.
Across seven external cohorts, padding toward Ibteda's median paper-area fraction moved disagreement in both directions. Paper fill can identify inputs unlike the training archive and help prevent gross off-paper crops, but it does not define a portable confidence window. The 1874 atlas makes the same point about aspect ratio: accurate crops survived an 11% aspect deficit, but without a paired letterboxed run I cannot conclude that the distortion was harmless.
These remain small, deliberately selected cohorts judged by one operator, not an accuracy estimate for other archives. They establish a narrower boundary: neither the absolute level nor the ordering of flip disagreement is portable. The 87-pixel threshold and the review shares reported here are operating points for Ibteda only. On another archive I would collect local truth before letting disagreement set the review queue.
Calibration still had diagnostic value. Ten crops isolated a width bias in the 1693 book, a rotation bias in the 1676 book, and no common offset in the 1874 book. The 40-pixel activation gate declined all three. Applying the smaller correction improved the width-biased book in hindsight, but the already accurate rotated book showed why lowering the gate globally would be unsafe. I left it at 40 pixels.
One Book, End to End
To avoid presenting a best-of montage, I kept the Bahar-e-Ajam example inside one book from capture through retouching.
Five fixed spreads supplied the ten calibration crops. I held out a sixth spread, matched its two finished pages back to the raw photograph for evaluation, ran the deployed geometry model on the full spread, and applied the correction estimated from the other five. The retouch model then saw the calibrated pages. It abstained on one side and changed a small supported region on the other.
In code, the book moves through seven explicit handoffs:
- correspondence produces source geometry;
- the base model produces generic book-independent geometry;
- calibration supplies the book-specific inset;
- rendering fixes page dimensions and order;
- segmentation supplies removal support;
- classical reconstruction changes only that support;
- publication records the exact bytes and every decision that produced them.
The crop correction is easy to see, while retouching stays confined to a small supported region.
Page Improver Studio
I built Page Improver Studio after realizing that model checkpoints were the easy part. We still had to calibrate each book, review routed pages, keep retouch decisions attached to the right generation, render every page, verify the output, and prepare the archive package. Keeping all of that state in my head or in terminal history was no longer workable.
The Studio is a local FastAPI application. Its Python backend runs PyTorch inference, OpenCV rendering, database transitions, generation-manifest checks, and archive packaging. The browser uses plain HTML, CSS, and JavaScript over local HTTP, draws the crop editor on a canvas, and keeps page navigation and operator hotkeys responsive without a frontend build system.
The Pages view below shows Qasas-ul-Quran — 02: 143 source spreads, 283 output pages, 45 operator-drawn page frames, no remaining flags, and 283 written pages. The book is calibrated, compiled, checkpointed, and its Internet Archive package has been built. I captured this image through a read-only browser session, so the article did not change the book.
- GeometryPredict both physical page frames from every dual-page capture.
- CalibrateApply one pooled book correction from ten operator crops.
- ReviewOpen routed pages with their reasons and current crop state.
- RetouchAccept bounded removal evidence or preserve the page unchanged.
- CompileRender one complete, verified generation on a fixed book canvas.
- UploadBuild the archive package only from the completed generation.
At the 28.5% in-archive confirmation operating point, 71.5% of page sides skip geometry review. We still supply ten calibration crops, decide the routed pages, and inspect retouch abstentions. Those decisions now happen in one place and survive a restart.
What Failed
Most of the useful engineering lessons came from runs I did not deploy.
The Generalisation Wall
I expected more training books to improve unseen-book geometry. Instead, increasing the training set from 378 to 572 books reduced training fit from 0.9116 to 0.9010 while raw held-out pass@80 stayed almost flat at 0.6708 versus 0.6746. My first diagnosis was that ResNet-34 lacked capacity.
So I trained ResNet-50. It fit the training data much better—0.9415 instead of 0.9051—but raw held-out quality again stayed flat at 0.6784 versus 0.6770, with paired p = 0.91. After calibration, the larger model was worse: 0.8138 instead of 0.8271.
Those two runs killed the simple capacity story. More books moved training fit down; more parameters moved it up. Neither moved unseen-book quality away from roughly 0.68 raw and 0.82–0.83 calibrated.
The larger 193-book unexposed cohort made the deployment choice clearer. ResNet-34 achieved 0.842 automated quality with 1,169 failures. ResNet-50 reached 0.817 with 1,352 failures, and the review share required for 0.930 rose from 28.5% to 35.5%.
I kept ResNet-34 because it performed better on unseen books and kept the review queue smaller.
The worst volumes explained the ceiling. Their crops were centered correctly but stayed consistently too wide, narrow, tall, or short. That near-constant bias came from our chosen inset, which was invisible in the new book's raw photographs. A bigger model could memorize more old preferences, while ten corrections from the new book supplied the missing information directly.
A spatial head
I suspected global average pooling was throwing away location information, so I preserved a 4 × 4 feature grid and expanded the head input from 512 to 8,192 values. The implementation got worse everywhere: training fit fell from 0.9051 to 0.8509, raw held-out pass@80 fell from 0.6770 to 0.6451, and calibrated quality fell from 0.8271 to 0.7969. I rejected that head, while leaving open the possibility that a better spatial design could work.
Higher resolution
I also raised the input from 704 to 1024 pixels, expecting page-edge detail to help. Calibrated pass@80 moved from 0.8271 to 0.8260. The extra pixels cost more and did not address the book-level bias, so I stayed at 704.
Local refinement
The local-refinement model was supposed to clean up the long tail around the first crop. It improved ordinary pages more than the tail and missed the preregistered endpoint. Another checkpoint and inference path could not justify itself without solving that assigned failure, so I removed the stage.
Image-only calibration
I tried to infer calibration from visible paper edges and page-block anchors. Neither recovered the internal finishing boundary reliably enough. This is why the ten operator crops remain: they measure information the photograph does not contain.
Raw/finished subtraction
The raw/finished subtraction experiment selected 7.28% of a measured page, and 49.2% of the mask was retained ink. I abandoned direct differencing and moved to connected candidate regions with REMOVE, KEEP, and IGNORE states.
Frontmatter-only mining
Mining only front matter covered 1,732 sides while 35,646 labelled sides were eligible. Marks appeared deep in books too, so I sampled through the sequence.
Zone-balanced retouching
Zone-balanced sampling improved its target zone by 4.56 percentage points, but the chosen model produced 12.27 false-positive diacritic pixels per patch. I rejected it. A coverage gain that erases language features is a regression.
Edge-band relabelling
The edge-band relabelling run kept the zero-diacritic ceiling but missed its coverage endpoint. The labels contained useful signal, but not enough evidence to let the model modify published pages, so I did not deploy it.
The measurement failures
Some of my harder corrections concerned the analysis:
- One random calibration draw reported a significant model difference. Across 20 draws, the effect was +0.19 percentage points with median p = 0.50.
- Two models evaluated on the same pages required paired analysis; independent-proportion tests discarded the information in the discordant pages.
- Book-level hypotheses were tested over books; thousands of correlated pages would have inflated the evidence.
- A calibration subgroup with p = 0.023 did not survive correction across 24 examined buckets.
- The unexposed routing cohort needed an explicit exclusion rule so a later dataset build could not consume it.
- The retouch verdict used per-page randomized A/B positions; an unblinded sheet would have measured layout habit along with page preference.
- Incorrect operating points and estimator assumptions remain visible in the errata.
I made wrong inferences during the project. Preregistration, protected cohorts, paired analysis, and retained result files made those mistakes visible enough to correct.
Making It Survive
Our worst production crash was not a GPU OOM or a bad prediction. I had written a renderer that kept every decoded 4000 × 6000 RGB spread in a Python dictionary until the book finished. Each spread occupied about 72 MB, so a 378-spread volume could retain roughly 27 GB before counting the rest of the process.
From the Studio, it looked like compile had crashed halfway through. I sampled memory every ten seconds and watched resident usage climb by one decoded spread after another until Linux exhausted swap and killed the process.
The fix was a two-spread cache shared by compile and page-write paths. Its decoded working set stays around 144 MB regardless of book length. Decode failures are cached too, preventing repeated reads for every page cut from an unreadable spread.
The crash also forced me to separate “the worker returned” from “the book is publishable.” Each generation records page dimensions, byte lengths, content hashes, geometry intent, retouch intent, and per-page retouch evidence. I write into staging, validate and flush the complete generation, then replace the published book atomically. An interrupted run leaves the previous generation in place.
One scaling problem remains. Startup verification gets slower as the published shelf grows because the Studio rehashes and decodes existing pages. Turning verification off would make startup faster by giving up evidence. The next design step is incremental verification: remember what was proved, recheck changed books, and rotate older coverage without trusting a stale status flag.
Appendix: Reproduction Contract
This section records the implementation choices needed to rebuild the models and decision gates described above. It transfers the method, not the trained artifact: another archive still needs its own raw captures, operator-finished pages, verified correspondences, protected book splits, and locally measured thresholds. The settings below are the ones used for the reported Ibteda experiments.
Geometry decoder
The decoder converts the model's normalized rectangle back into the 2048 × 2897 Photoshop page coordinate system. Width and height are scaled independently because the historical workflow stretched each selected crop to that fixed canvas instead of preserving the photographed aspect ratio.
The model emits one six-value vector per physical side. I normalize the sine/cosine pair directly instead of converting it through atan2, exponentiate width and height, construct the four local corners, rotate them, and add the centre. The two scale factors below convert normalized letterbox coordinates into canonical page pixels.
SX, SY = 2048.0 / 0.349, 2897.0 / 0.573
def decode_corners(vector):
cx, cy = vector[:, 0], vector[:, 1]
width, height = vector[:, 2].exp(), vector[:, 3].exp()
norm = vector[:, 4:6].square().sum(1).sqrt().clamp_min(1e-6)
sin_theta, cos_theta = vector[:, 4] / norm, vector[:, 5] / norm
half_width, half_height = width * SX / 2, height * SY / 2
local = torch.stack((
torch.stack((-half_width, -half_height), 1),
torch.stack(( half_width, -half_height), 1),
torch.stack(( half_width, half_height), 1),
torch.stack((-half_width, half_height), 1),
), 1)
rotation = torch.stack((
torch.stack((cos_theta, -sin_theta), 1),
torch.stack((sin_theta, cos_theta), 1),
), 1)
centre = torch.stack((cx * SX, cy * SY), 1).unsqueeze(1)
return torch.einsum("bij,bcj->bci", rotation, local) + centre
| Geometry component | Deployed contract |
|---|---|
| Input | A 1500 × 1000 RGB spread resized to 704 × 469.33, centred in a 704 × 704 frame with RGB (114, 114, 114) padding. ImageNet mean and standard deviation are applied after scaling pixels to 0–1. |
| Target | Two physical-side vectors: (cx, cy, log w, log h, sin θ, cos θ), plus one availability mask per side. Missing physical pages contribute no loss. |
| Network | ImageNet-pretrained ResNet-34 trained end to end, followed by global average pooling. Each side has its own Linear(512, 128) → ReLU → Linear(128, 6) head. There is no frozen-trunk warm-up. |
| Loss | Decode both predictions and targets into four canonical-pixel corners. Apply Smooth L1 with beta=10, sum x/y error per corner, average the four corners, mask missing sides, and average present sides. The deployed run used no soft worst-corner term. |
| Geometric augmentation | Uniform scale from 0.85 to 1.15 and independent x/y translations from −5% to +5% of the 704-pixel frame. The identical affine transform updates the image, centres, and log sizes. |
| Photometric augmentation | Brightness multiplier 0.85–1.15; contrast multiplier 0.85–1.15 around the image mean; Gaussian noise σ 0.01 with probability 0.3. Values are clipped to 0–1. Horizontal flips are excluded from training. |
| Optimization | 8,824 spreads from 378 books; 745 validation spreads from 34 books. AdamW, maximum learning rate 3 × 10−4, weight decay 10−4, batch 24, 60 epochs, OneCycle schedule, gradient norm capped at 5, seed 0. This produced 22,020 optimizer steps. |
| Checkpoint | Selected by validation pass@80, never by the sealed test set. Epoch 59 reached 0.7905 validation pass@80 with 50.46-pixel median worst-corner error. |
At inference I also run a horizontally flipped spread. Recovering that prediction requires swapping the physical-side heads, mapping cx to 1 − cx, and negating sin θ. The maximum corner distance between the direct and recovered predictions becomes the geometry-disagreement signal.
Retouch label construction
The finished Photoshop page supplies supervision only while building the dataset. The deployed segmenter receives the registered raw page alone.
| Stage | Operational definition |
|---|---|
| Registration | Warp the operator-derived quadrilateral into the finished page’s own dimensions. Refine it with a Euclidean ECC transform for at most 200 iterations, epsilon 10−6, then fit a linear raw-to-finished luminance map. Exclude the page when mean absolute luminance disagreement outside the mined mask and outer 2% border exceeds 6 levels. |
| Candidate pixels | Compute tone-matched finished − raw luminance, then a 41 × 41 local mean. A pixel enters the candidate mask when that mean exceeds 2.5 levels and raw luminance is at least 8 levels below the page’s 90th-percentile paper estimate. |
| Candidate regions | Close the mask with a 31 × 31 elliptical kernel, label 8-connected components, and discard components below 1,500 canonical pixels. Strength is the mean local-lightening value over the closed component. |
| Completeness | Build a 24-pixel ring around the component. Inside the original candidate core, keep pixels that became at least 15 luminance levels lighter. Residual is median finished-paper luminance in the ring minus the fifth percentile inside that footprint. Residual < 6 is complete, 6–16 is partial, and ≥16 is trace. |
| Tiers | Components below 10,000 pixels are Spot; larger components are Mark. The tiers are evaluated separately because small specks otherwise dominate the aggregate score. |
| Page exclusions | Exclude artwork pages and pages whose outside-mask alignment error exceeds 6. These pages contribute neither positive labels nor negatives because their raw/finished comparison is unreliable. |
| IGNORE rules | Move a region to IGNORE when removal is partial or trace; surviving finished-page ink exceeds 0.5%; more than 10% of its raw pixels are near-black backdrop below luminance 40; a central-body region has edge fraction above 0.3 or elongation above 4; or the region exceeds 10% of the page. |
| Arm B REMOVE | Apply all preceding gates, then require strength ≥6.0. Regions that qualify under the inclusive rule but miss this strength floor become IGNORE, never KEEP. Background and separately mined hard-negative patches are KEEP. |
This produced 16,027 Arm B removal regions from the mined corpus: 10,124 Spot and 5,903 Mark. Another 30,986 ambiguous regions remained IGNORE, and 11,005 regions belonged to excluded pages. Those counts are useful integrity checks for this dataset; another collection should expect different values.
Retouch model and abstention
| Retouch component | Deployed contract |
|---|---|
| Architecture | U-Net-style network with an ImageNet-pretrained ResNet-34 encoder. Decoder stages concatenate encoder skips and emit 256, 128, 64, 32, and 16 channels; each stage uses nearest-neighbour upsampling followed by two 3 × 3 Conv–BatchNorm–ReLU blocks. A 1 × 1 head emits one removal logit per pixel. |
| Loss | Binary cross-entropy with logits plus soft Dice. Pixels labelled IGNORE are removed from both terms. Dice uses one unit of numerator and denominator smoothing. |
| Training | 6,999 training and 2,000 validation patches, each 512 × 512. AdamW, learning rate 3 × 10−4, weight decay 10−4, batch 16, 20 epochs, OneCycle schedule, gradient norm capped at 1, seed 0, TF32 disabled, and bfloat16 autocast on CUDA. |
| Augmentation | Independent horizontal and vertical flips, each with probability 0.5. I did not rotate patches because resampling changes small type, and I did not apply colour jitter because colour carries label evidence. |
| Checkpoint | The deployed Arm B checkpoint is epoch 16: Mark IoU 0.6024 at the fixed 0.5 pixel threshold, with 0.0 measured false-positive diacritic pixels per text-dense validation patch. |
| Page inference | Tile the canonical page at 512 pixels with stride 256, include flush-to-edge tiles, and average overlapping probability maps. Threshold once at probability >0.5. |
| Component abstention | Label 8-connected predicted components. Drop the whole component when area is below 1,500 pixels or its mean predicted probability is below 0.6. Partial erosion is avoided because half-removing a stamp is worse than preserving it. |
Review routing
The review queue stores named reasons rather than one opaque risk score. These are the deployed pixel rules:
| Reason | Rule |
|---|---|
| Geometry disagreement | Route when the largest recovered direct-versus-flip corner distance exceeds 87 canonical pixels. |
| Backdrop in margin | Inspect the outer 140 pixels in CIE Lab and estimate page paper from the 75th percentile of a central 200-pixel band. Candidate pixels sit at least 10 b* units bluer and 8 L* units darker than that paper. Route an 8-connected component when it touches the outer edge, covers at least 400 pixels, and reaches at least 12 pixels inward. |
| Partial retouch band | In each outer 140-pixel strip, a row belongs to a dark band when at least 20 pixels lie 20 luminance levels below the page’s central 75th-percentile paper estimate. Require the band to span at least 5% of page rows and average at least 45 levels below paper. If the retouch mask entered that band but covered less than 90% of its rows, route the page. |
These thresholds are part of the reported operating point, not universal document constants. An archive can reproduce the calculations directly, but it should choose its own review thresholds on protected books and report the resulting risk-coverage curve.
Appendix: Practical Implementation Checklist
For another archive, I would start with the order of operations below. Each learned component should solve a measured problem in the collection that will actually use it.
-
Preserve both sides of the old workflow
Keep raw captures and operator-finished pages together with stable book and page identities. Do not overwrite either when a new pipeline is introduced.
Go when: a finished page can be traced to a book and a plausible raw-capture neighborhood.
Stop when: filenames or numbering are too inconsistent to narrow candidates without manual help.
Non-neural fallback: build a small reviewed correspondence table for representative books.
-
Prove correspondence before training
Register finished pages back to raw captures with local features and robust geometry. Keep runner-up scores and rejection reasons.
Go when: reviewed winners are visibly correct and wrong neighbors fail by a wide margin.
Stop when: the gate depends on one fragile threshold or accepts plausible wrong spreads.
Non-neural fallback: use the verified correspondences directly as a semi-automatic crop tool.
-
Measure label acceptance
Count attempted, accepted, and rejected pages by book position and material type. Treat file defects locally.
Go when: acceptance is high enough to cover multiple books and failures have understandable causes.
Stop when: one class of book or page silently disappears from the dataset.
Non-neural fallback: restrict automation to the well-supported regime and route the rest for operator crops.
-
Split whole books
Pin validation, sealed test, and any later confirmation cohort before model selection. Keep negatives and derived data under the same boundary.
Go when: no book identity appears in more than one split and rebuilds preserve the protected sets.
Stop when: the dataset builder cannot explain where newly added books land.
Non-neural fallback: evaluate a median-geometry or paper-boundary baseline on the same unseen books.
-
Train the simplest geometry model
Use a representation that reconstructs corners, masked missing sides, and an objective measured in final-page pixels.
Go when: the model beats non-neural baselines on unseen books and rendered pages support the metric.
Stop when: only training or random-page validation improves.
Non-neural fallback: template geometry plus operator correction may already remove much of the repetitive work.
-
Add calibration only for consistent bias
Decompose errors by book. If residuals are nearly constant within a volume, test a small robust correction on held-out pages.
Go when: a few corrections improve unseen pages and materially harm few books.
Stop when: residuals are page-specific or the estimator moves already-correct books.
Non-neural fallback: apply one operator-approved book template without a learned base model.
-
Mine retouch labels only after geometry
Register raw and finished pages closely enough that text strokes do not become differences. Separate REMOVE, KEEP, and IGNORE.
Go when: candidate regions can be reviewed and hard negatives cover the smallest marks that must survive.
Stop when: label construction needs a finished-page quantity that would also be required at inference.
Non-neural fallback: use mined regions as review suggestions without changing published pixels.
-
Let retouching abstain
Train dense support prediction, then gate components by the area and confidence represented in the labels. Keep reconstruction separate.
Go when: typography safety holds, changed support is bounded, and blinded operator review prefers the result.
Stop when: better coverage raises false removal or an attractive metric hides damage inside the mask.
Non-neural fallback: preserve the mask for operator use and keep publication unretouched.
-
Route uncertainty before increasing automation
Measure named failure signals on protected books and choose the review share at a fixed quality target.
Go when: the risk-coverage curve replicates outside the threshold-selection cohort.
Stop when: a router reduces review only by lowering held-out quality.
Non-neural fallback: review deterministic high-risk classes such as covers, near-edge pages, and admitted backdrop.
-
Publish bounded, atomic generations
Bound decoded image lifetimes, stage complete books, hash output, and retain the intent that produced every page.
Go when: interruption tests preserve the previous book and incomplete work remains diagnosable.
Stop when: “processed” or “worker finished” is the only evidence of completeness.
Non-neural fallback: use the same manifests and atomic replacement for operator-finished output.
This order saved me from training on bad correspondences, mining retouch labels from unstable crops, and treating a good metric as proof that every output page existed.
Limits
The current system still needs ten calibration crops for a new book. At the high-quality operating points reported here, roughly one-third of page sides may still go to review.
Those review shares are Ibteda measurements, not a portable staffing estimate. On printed captures from outside Ibteda, geometry sometimes cleared the bar without adaptation while disagreement over-routed clean pages or failed to put the bad pages at the top. A new archive needs locally drawn truth before choosing a routing threshold.
Retouching has much narrower evidence than geometry. The strongest preference result comes from one development book, and abstention remains common. I cannot safely reconstruct printed text hidden by a heavy stamp just because the segmenter detects the stamp.
Single covers, folded pages, inserts, missing leaves, extreme page ratios, and unfamiliar camera setups remain difficult to certify. Some need explicit frame types; others are better left manual.
This generalisation wall reflects the architecture and data we tested; future models may find better representations. More books, ResNet-50, 1024-pixel input, the tested spatial head, and the local-refinement stage all failed to improve unseen-book quality enough to remove calibration and review.
The software also has unfinished work. Startup verification grows with the shelf. A skipped or ambiguous post-processing probe needs persistent state. Published generations need stronger integrity checks than file count. These issues sit outside the neural networks, but they decide whether I can trust the books those networks produce.
What Remains
We knew we could not preserve every book produced across three centuries of Urdu literary culture. What we did save, however, forms a substantial record of how the language was written, printed, studied, and read.
The catalog includes nineteenth-century dictionaries, early editions of poetry and prose, more than 350 lithographed volumes, nearly 500 poetry collections, and over 1,000 periodical and special issues. Its dated holdings begin in 1821; among them are Persian and multilingual dictionaries from 1828, 1865, 1876, and 1877. More than 1,300 completed books are already accessible through the Internet Archive.
One milestone was accidental. We began this project frustrated with private collectors and the books gathering dust on their shelves. Ten years later, we had bought over 5,000 volumes ourselves—and built new bookshelves to hold them. We ended up exactly where we started. Only this time, the bits are safe.
In April 2026, after ten years of continuous work, we faced an undeniable reality. Motivation had been worn down by financial, personal, and emotional strain. Running a private archive out of pocket takes a heavy toll, and we had to accept that continuing the daily manual operation was no longer sustainable for any of us.
We made the difficult decision to close daily operations. The hardest part was closing a chapter I had shared with two old friends. Our photographer spent a decade at the tabletop, braving paper dust, allergies, and book insects while enduring the repetitive, mentally exhausting work of turning leaves, pressing volumes against the glass, and clicking the remote shutter for 822,161 dual-page exposures: more than 1.64 million photographed page sides. Our operator then spent roughly 10,200 reconstructed active hours turning those photographs into finished pages in Photoshop.
We couldn't save three centuries of Urdu literature. But we preserved the part three friends—working with limited resources, occasional criticism, and a remarkably dependable supply of demotivation—were able to place beyond the immediate reach of paper decay, inaccessible shelves, and institutional neglect.
The work left behind a foundation. The raw photographs remain on a ZFS storage pool, protected by BLAKE3 manifests and integrity audits. The 35,648 accepted geometry labels remain. Page Improver Studio remains. We turned a decade of human effort into a process that can be measured, stopped, inspected, and resumed whenever GPU time and operator attention are available.
The daily scanning is over, but the work we did together will outlive us all.
زر محبت من خریدم این کتاب گر کہ دعوا کند خانہ خراب در حقیقت مالک ہر شے خداست این امانت چند روز نزد ماست
With love's own gold I bought this book—
should anyone claim it otherwise, may his house fall to ruin.
Yet in truth, God alone is the Owner of all things:
this is a trust with us, for a few days only.