3D Reader V3: reader-v3 with a sheet-localised depth head
In one sentence: the stock reader-v3 ink model plus a 16-plane depth head that says on which plane (sheet) each stroke sits, fine-tuned without any 3-D human labels, at about the same cost as reader-v3 (+1.3 % time, measured).
This is an experimental fine-tune of the public reader-v3 checkpoint (34.55 M parameters, 3-D depth-fusion stem + 2-D U-Net, 17 input planes at ~9.6 um). The extra head is a 1x1 convolution (528 parameters) on the finest decoder stage that outputs 16 logits per pixel, one per plane of the central 16 input planes. Code, tests and the full write-up: https://github.com/StewartSethA/villa/tree/progress-2026-11/sheet-localized-3d-ink-head
Columns: CT middle plane | refined label | stock reader-v3 | this model | this model's depth hue (blue = top plane, red = mesh plane, green = bottom plane) | Hecate depth hue. Rows: Scroll 4 20251212185248 and Scroll 1 20230929220926 windows in the held-out right 10 % band of segments the model was fine-tuned on (so the sharper map partly reflects fine-tuning to the refined labels; the transfer evidence is the table below).
Files
| file | content |
|---|---|
reader_v3_depth_head.safetensors |
net + head (net keys keep the model. prefix; alias keys .all_modules. are omitted because they are the same tensors as .conv. / .norm.; head keys start with head.). Loading it into a freshly built network reproduces the original checkpoint's logits exactly (max abs difference 0.0). |
reader_v3_depth_head_last.pt |
the same weights as a plain tensor .pt (state_dict, head, step; loads with weights_only=True) |
datasets.json |
the data record of the run (see below) |
MD5SUMS |
checksums |
Training data (all of it)
- Initialisation: the stock reader-v3 checkpoint.
- Additional data: 13 open-data segments, 8 of Scroll 1 (PHercParis4: 20230702185753, 20230929220926, 20231007101619, 20231012184424, 20231016151002, 20231031143852, 20231106155351, 20231210121321) and 5 of Scroll 4 (PHerc1667: 20240304144031, 20240304161941, 20251208130119, 20251212185248, 20251223230000), as 17-plane 9.6 um surface renders. Labels: "refined core" = human label AND the registered upstream 2.4 um prediction >= 128; "maybe" pixels are never trained on; the right 10 % of every segment is held out. Depth supervision: Hecate 9.6 um 3-D probability volumes (https://huggingface.co/scrollprize/hecate) on those segments; the 2-D label is snapped onto the ink-bearing sheet (the teacher's depth profile restricted to +-1 plane around its peak). Hecate's own training segment list is not published, so overlap of Hecate with the test segments cannot be ruled out.
- Store mix: every batch also contains ~9 windows of the reader-v3 training store (the data the stock model was trained on), x in [0, 0.88) of each representation. Without it the net specialises to Scroll 1/4 and loses transfer (0.75-0.79 upstream-test AUC in our first attempts).
- Recipe: SGD momentum 0.99 nesterov, lr 2e-4, 200-step warm-up, per-window robust-MAD normalisation, flips + rot90 on the GPU, masked BCE (2-D) + masked BCE (16 depth planes), batch 6 refined windows + ~9 store windows at 320 px, seed 0, stopped by a held-out plateau rule (step 7500).
- No data from any other scroll was used.
Results (upstream test: 8 segments, human-label AUC, equal weights, same harness for every row, one seed each)
| model | P384 | P448 |
|---|---|---|
| stock reader-v3 (positive control; README reference 0.892) | 0.8893 | 0.892 |
| single-task control (same recipe, depth head not trained) | 0.8911 | 0.8941 |
| this model (film +-1) | 0.8968 | 0.8994 |
| film +-1, other seeds / 13-segment run | 0.8947 / 0.8968 | 0.8957 / 0.8991 |
| film +-3 | 0.8933 | 0.8953 |
| any sheet (no film restriction) | 0.8841 | 0.8871 |
The gain over stock (+0.0054 to +0.0075 at P384) is small and single-seed; replicate seeds are still training. The 3 of the 8 test segments that also appear in the reader-v3 training list (see the leakage-audit branch) are in-sample for stock and fine-tuned models alike.
Use
import torch
ck = torch.load("reader_v3_depth_head_last.pt", weights_only=True) # keys: state_dict, head, step
net = build_reader_v3() # the reader-v3 network of villa's vesuvius.ink_detection
net.load_state_dict(ck["state_dict"]) # strip the "model." prefix if your build uses bare keys
head = torch.nn.Conv2d(32, 16, 1); head.load_state_dict({k.removeprefix("head."): v for k, v in ck["head"].items()})
# hook the finest decoder stage (net.model.network.shared_decoder.stages[-1]) and apply `head` to its output:
# sigmoid(logits) per plane = probability that the ink sits on that plane (planes 0-15 of the 17-plane window).
# Normalise each window with the reader-v3 robust-MAD recipe; plane order must be the one the checkpoint expects
# (see the render-direction-and-intensity-check branch).
Reference implementation: attach_depth_head in the GitHub branch above.
Limitations
Single training seed per arm; no confidence intervals; the upstream-test gain is within what seed noise could produce until replicates finish; not run through the vesuvius package loaders (adapter point documented in the branch); depth probabilities are soft targets, not calibrated.