File size: 8,358 Bytes
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
7672a95
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7672a95
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7672a95
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2012ce
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7672a95
 
 
 
 
 
9db19e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
---
library_name: transformers
license: mit
tags:
- vesuvius-challenge
- ink-detection
- herculaneum
- resnet3d
- u-net
- 3d-segmentation
- volumetric-imaging
pipeline_tag: image-segmentation
---

# PHerc.1667-iteration-1

> **Trained on segment l_2 with l_2_inklabels.png (3,396 tiles).**

Ablation 1/5 — sparsest training label (3,396 tiles). Smallest annotation set; trained for ~30 effective epochs over its data to match the step budget.

This is one of **six sibling models** released together — five label
ablations on segment `l_2` (`ink1`–`ink5`, increasing label coverage)
and one cross-segment baseline (`ink0`).  The full family is listed
at the bottom of this card.

## Preview

`l_2` (training segment) prediction with the training label overlaid in
magenta, and `l_5` (held-out segment) prediction. All panels are
downsampled 16× and rotated 180° to match the publication-figure
convention. The full-resolution `last.ckpt` outputs are at 43008 × ~30000
voxels.

| training label | l_2 prediction | l_5 prediction |
|----------------|----------------|----------------|
| ![label](./preview_label.png) | ![l_2 pred](./preview_l_2.png) | ![l_5 pred](./preview_l_5.png) |


## Architecture in one paragraph

A 3-D volumetric input `(B, 1, 62, 256, 256)` is encoded by a
**ResNet3D-50** backbone (Hara, Kataoka & Satoh, 2018; initialised from
the Kinetics-700 release `r3d50_KM_200ep.pth` with conv1 weights
summed across RGB → 1 grayscale channel).  Each of the four backbone
stages is collapsed along the z (depth) axis with `torch.max`,
producing a 2-D feature pyramid `{(256,64,64), (512,32,32),
(1024,16,16), (2048,8,8)}`.  A small **2-D U-Net decoder** upsamples
coarse-to-fine with concatenated skip connections; a 1×1 conv head
produces a single sigmoid logit channel at quarter resolution
`(B, 1, 64, 64)`.  Training uses `0.5·Dice + 0.5·SoftBCE` against the
label down-interpolated to 64×64.

## Quick start

```python
import torch
from transformers import AutoModel

model = AutoModel.from_pretrained(
    "YoussefMoNader/PHerc.1667-iteration-1",
    trust_remote_code=True,
).eval().cuda()

# Input: float32, shape (B, 1, D=62, H=256, W=256).
# Intensity should already be in roughly [0, 1] (the training pipeline
# clipped raw uint8 layers to [0, 200] then applied Normalize(mean=0, std=1)
# which keeps the magnitude small).
x = torch.randn(1, 1, 62, 256, 256, device="cuda")

with torch.no_grad():
    out = model(x)

print(out.logits.shape)                        # torch.Size([1, 1, 64, 64])
prob = torch.sigmoid(out.logits)               # ink probability per pixel
```

## Full-segment inference (tiling)

The model only sees 256×256 windows.  For a full scroll segment you
need to slide the window across the (padded) layer stack and average
overlapping predictions:

```python
import numpy as np, cv2, torch
import torch.nn.functional as F
from transformers import AutoModel

model = AutoModel.from_pretrained(
    "YoussefMoNader/PHerc.1667-iteration-1", trust_remote_code=True,
).eval().cuda()

WINDOW, STRIDE = 256, 128       # 128 = 2x oversample; 64 for 8x oversample
D = 62                           # number of z-layers

# image: (H, W, D) uint8 stack of the 62 layers, padded to multiples of 256.
# fmask: (H, W) uint8 fragment mask (0 = outside, 255 = inside).
H, W, _ = image.shape
mask_pred  = np.zeros((H, W), dtype=np.float32)
mask_count = np.zeros((H, W), dtype=np.float32)

with torch.no_grad():
    for y in range(0, H - WINDOW + 1, STRIDE):
        for x in range(0, W - WINDOW + 1, STRIDE):
            if np.any(fmask[y:y+WINDOW, x:x+WINDOW] == 0):
                continue
            tile = image[y:y+WINDOW, x:x+WINDOW]            # (256,256,62)
            t = torch.from_numpy(tile).permute(2, 0, 1)     # (62,256,256)
            t = t.unsqueeze(0).unsqueeze(0).float().cuda()  # (1,1,62,256,256)
            logits = model(t).logits                        # (1,1,64,64)
            prob   = torch.sigmoid(logits)
            prob   = F.interpolate(prob, scale_factor=4,
                                   mode="bilinear").squeeze().cpu().numpy()
            mask_pred[y:y+WINDOW, x:x+WINDOW]  += prob
            mask_count[y:y+WINDOW, x:x+WINDOW] += 1.0

pred = np.divide(mask_pred, mask_count,
                 out=np.zeros_like(mask_pred),
                 where=mask_count != 0)
cv2.imwrite("prediction.png", np.clip(pred * 255, 0, 255).astype(np.uint8))
```

## Training summary

| | |
|---|---|
| **Backbone** | ResNet3D-50 (3-D conv, BN, ReLU residual blocks) |
| **Encoder init** | `r3d50_KM_200ep.pth` (Kinetics-700), conv1 summed across RGB |
| **Decoder** | 2-D U-Net (3 up-blocks: bilinear 2× + concat skip + 3×3 conv + BN + ReLU) |
| **Output** | 1 channel, sigmoid logit, quarter-resolution (64×64) |
| **Loss** | 0.5 × Dice + 0.5 × SoftBCE (smooth = 0.25) |
| **Optimizer** | AdamW, OneCycle lr 2e-5 → 3e-4, pct_start = 0.15 |
| **Batch** | 2 (effective 8 via accumulate 4), 16-mixed, grad-clip 1.0 |
| **Max steps** | 12,396 (= 3 epochs over the densest ablation label) |
| **Training segment(s)** | `l_2` |
| **Training label** | `l_2_inklabels.png` |
| **Training tiles** (256×256 sub-tiles at stride 64) | **3,396** |
| **Final train loss (`_epoch`)** | **0.4219** |
| **Final train loss (`_step`, single-batch noise)** | 0.4381 |
| **Wandb** | [vesuvius-challenge/paper/l2_ink1_l5infer](https://wandb.ai/vesuvius-challenge/paper/runs/xlj01hgh) |
| **Random seed** | 130697 |
| **Determinism** | `cudnn.deterministic = True`, `cudnn.benchmark = False` |
| **Hardware** | 1 × NVIDIA H100 80 GB; ≈ 2 h end-to-end (load + train + inference) |

## Files

| file | size | description |
|------|------|-------------|
| `config.json` | 1 KB | architecture + provenance metadata; loaded by `AutoConfig` |
| `configuration_inkdetection.py` | 2 KB | `InkDetectionConfig(PretrainedConfig)` |
| `modeling_inkdetection.py` | 9 KB | self-contained `InkDetectionModel(PreTrainedModel)` |
| `model.safetensors` | 319 MB | converted weights (338 tensors) |
| `last.ckpt` | 963 MB | original PyTorch-Lightning checkpoint (incl. optimizer + LR-scheduler state) — load with `torch.load(...)["state_dict"]` |
| `preview_l_2.png` | ~700 KB | low-res preview of the l_2 prediction (1/16 scale, 180° rotated) |
| `preview_l_5.png` | ~2 MB | low-res preview of the l_5 (held-out) prediction |
| `preview_label.png` | ~50 KB | the training label, same scale + rotation |

The HuggingFace weights are **bit-perfect identical** to the original
PyTorch-Lightning checkpoint (verified `max abs diff = 0.0e+00` on
identical inputs).  Use `model.safetensors` for `AutoModel.from_pretrained`;
use `last.ckpt` only if you want to resume training from the saved
optimizer / scheduler state.

## The model family

| model | training segment(s) | label | tiles | effective epochs |
|-------|---------------------|-------|-------|-------------------|
| [`PHerc.1667-iteration-0`](https://huggingface.co/scrollprize/PHerc.1667-iteration-0) | 500p2a + 658 + 20250910185200 + 20250919125754* | (cross-segment baseline) | 20,075 | ~5 |
| [`PHerc.1667-iteration-1`](https://huggingface.co/scrollprize/PHerc.1667-iteration-1) | `l_2` | `l_2_inklabels.png`  | 3,396 | ~30 |
| [`PHerc.1667-iteration-2`](https://huggingface.co/scrollprize/PHerc.1667-iteration-2) | `l_2` | `l_2_inklabels2.png` | 8,970 | ~12 |
| [`PHerc.1667-iteration-3`](https://huggingface.co/scrollprize/PHerc.1667-iteration-3) | `l_2` | `l_2_inklabels3.png` | 15,286 | ~7 |
| [`PHerc.1667-iteration-4`](https://huggingface.co/scrollprize/PHerc.1667-iteration-4) | `l_2` | `l_2_inklabels4.png` | 24,773 | ~5 |
| [`PHerc.1667-iteration-5`](https://huggingface.co/scrollprize/PHerc.1667-iteration-5) | `l_2` | `l_2_inklabels5.png` | 33,061 | 3 |

All six share the architecture, hyperparameters, and a fixed step
budget of 12,396 optimizer steps; the only thing that varies between
rows is the supervising label (or, for ink0, the training segments).

## Citation

If you use this model in published work, please cite the Vesuvius
Challenge and the underlying ResNet3D paper:

```bibtex
@inproceedings{hara2018can,
  title  = {Can spatiotemporal 3D CNNs retrace the history of 2D CNNs and ImageNet?},
  author = {Hara, Kensho and Kataoka, Hirokatsu and Satoh, Yutaka},
  booktitle = {CVPR}, year = {2018},
}
```

## Licence

MIT.