Image Segmentation
Transformers
Safetensors
inkdetection_resnet3d
feature-extraction
vesuvius-challenge
ink-detection
herculaneum
resnet3d
u-net
3d-segmentation
volumetric-imaging
custom_code
Instructions to use scrollprize/PHerc.1667-iteration-4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use scrollprize/PHerc.1667-iteration-4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="scrollprize/PHerc.1667-iteration-4", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("scrollprize/PHerc.1667-iteration-4", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Model card / docstring: state the input scaling that actually works (clip [0,200] / 255)
#1
by khj1222 - opened
- README.md +197 -193
- modeling_inkdetection.py +239 -239
README.md
CHANGED
|
@@ -1,193 +1,197 @@
|
|
| 1 |
-
---
|
| 2 |
-
library_name: transformers
|
| 3 |
-
license: mit
|
| 4 |
-
tags:
|
| 5 |
-
- vesuvius-challenge
|
| 6 |
-
- ink-detection
|
| 7 |
-
- herculaneum
|
| 8 |
-
- resnet3d
|
| 9 |
-
- u-net
|
| 10 |
-
- 3d-segmentation
|
| 11 |
-
- volumetric-imaging
|
| 12 |
-
pipeline_tag: image-segmentation
|
| 13 |
-
---
|
| 14 |
-
|
| 15 |
-
# PHerc.1667-iteration-4
|
| 16 |
-
|
| 17 |
-
> **Trained on segment l_2 with l_2_inklabels4.png (24,773 tiles).**
|
| 18 |
-
|
| 19 |
-
Ablation 4/5 — 24,773 training tiles.
|
| 20 |
-
|
| 21 |
-
This is one of **six sibling models** released together — five label
|
| 22 |
-
ablations on segment `l_2` (`ink1`–`ink5`, increasing label coverage)
|
| 23 |
-
and one cross-segment baseline (`ink0`). The full family is listed
|
| 24 |
-
at the bottom of this card.
|
| 25 |
-
|
| 26 |
-
## Preview
|
| 27 |
-
|
| 28 |
-
`l_2` (training segment) prediction with the training label overlaid in
|
| 29 |
-
magenta, and `l_5` (held-out segment) prediction. All panels are
|
| 30 |
-
downsampled 16× and rotated 180° to match the publication-figure
|
| 31 |
-
convention. The full-resolution `last.ckpt` outputs are at 43008 × ~30000
|
| 32 |
-
voxels.
|
| 33 |
-
|
| 34 |
-
| training label | l_2 prediction | l_5 prediction |
|
| 35 |
-
|----------------|----------------|----------------|
|
| 36 |
-
|  |  |  |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
## Architecture in one paragraph
|
| 40 |
-
|
| 41 |
-
A 3-D volumetric input `(B, 1, 62, 256, 256)` is encoded by a
|
| 42 |
-
**ResNet3D-50** backbone (Hara, Kataoka & Satoh, 2018; initialised from
|
| 43 |
-
the Kinetics-700 release `r3d50_KM_200ep.pth` with conv1 weights
|
| 44 |
-
summed across RGB → 1 grayscale channel). Each of the four backbone
|
| 45 |
-
stages is collapsed along the z (depth) axis with `torch.max`,
|
| 46 |
-
producing a 2-D feature pyramid `{(256,64,64), (512,32,32),
|
| 47 |
-
(1024,16,16), (2048,8,8)}`. A small **2-D U-Net decoder** upsamples
|
| 48 |
-
coarse-to-fine with concatenated skip connections; a 1×1 conv head
|
| 49 |
-
produces a single sigmoid logit channel at quarter resolution
|
| 50 |
-
`(B, 1, 64, 64)`. Training uses `0.5·Dice + 0.5·SoftBCE` against the
|
| 51 |
-
label down-interpolated to 64×64.
|
| 52 |
-
|
| 53 |
-
## Quick start
|
| 54 |
-
|
| 55 |
-
```python
|
| 56 |
-
import torch
|
| 57 |
-
from transformers import AutoModel
|
| 58 |
-
|
| 59 |
-
model = AutoModel.from_pretrained(
|
| 60 |
-
"YoussefMoNader/PHerc.1667-iteration-4",
|
| 61 |
-
trust_remote_code=True,
|
| 62 |
-
).eval().cuda()
|
| 63 |
-
|
| 64 |
-
# Input: float32, shape (B, 1, D=62, H=256, W=256).
|
| 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 |
-
H, W,
|
| 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 |
-
| [`PHerc.1667-iteration-
|
| 172 |
-
| [`PHerc.1667-iteration-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: mit
|
| 4 |
+
tags:
|
| 5 |
+
- vesuvius-challenge
|
| 6 |
+
- ink-detection
|
| 7 |
+
- herculaneum
|
| 8 |
+
- resnet3d
|
| 9 |
+
- u-net
|
| 10 |
+
- 3d-segmentation
|
| 11 |
+
- volumetric-imaging
|
| 12 |
+
pipeline_tag: image-segmentation
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# PHerc.1667-iteration-4
|
| 16 |
+
|
| 17 |
+
> **Trained on segment l_2 with l_2_inklabels4.png (24,773 tiles).**
|
| 18 |
+
|
| 19 |
+
Ablation 4/5 — 24,773 training tiles.
|
| 20 |
+
|
| 21 |
+
This is one of **six sibling models** released together — five label
|
| 22 |
+
ablations on segment `l_2` (`ink1`–`ink5`, increasing label coverage)
|
| 23 |
+
and one cross-segment baseline (`ink0`). The full family is listed
|
| 24 |
+
at the bottom of this card.
|
| 25 |
+
|
| 26 |
+
## Preview
|
| 27 |
+
|
| 28 |
+
`l_2` (training segment) prediction with the training label overlaid in
|
| 29 |
+
magenta, and `l_5` (held-out segment) prediction. All panels are
|
| 30 |
+
downsampled 16× and rotated 180° to match the publication-figure
|
| 31 |
+
convention. The full-resolution `last.ckpt` outputs are at 43008 × ~30000
|
| 32 |
+
voxels.
|
| 33 |
+
|
| 34 |
+
| training label | l_2 prediction | l_5 prediction |
|
| 35 |
+
|----------------|----------------|----------------|
|
| 36 |
+
|  |  |  |
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
## Architecture in one paragraph
|
| 40 |
+
|
| 41 |
+
A 3-D volumetric input `(B, 1, 62, 256, 256)` is encoded by a
|
| 42 |
+
**ResNet3D-50** backbone (Hara, Kataoka & Satoh, 2018; initialised from
|
| 43 |
+
the Kinetics-700 release `r3d50_KM_200ep.pth` with conv1 weights
|
| 44 |
+
summed across RGB → 1 grayscale channel). Each of the four backbone
|
| 45 |
+
stages is collapsed along the z (depth) axis with `torch.max`,
|
| 46 |
+
producing a 2-D feature pyramid `{(256,64,64), (512,32,32),
|
| 47 |
+
(1024,16,16), (2048,8,8)}`. A small **2-D U-Net decoder** upsamples
|
| 48 |
+
coarse-to-fine with concatenated skip connections; a 1×1 conv head
|
| 49 |
+
produces a single sigmoid logit channel at quarter resolution
|
| 50 |
+
`(B, 1, 64, 64)`. Training uses `0.5·Dice + 0.5·SoftBCE` against the
|
| 51 |
+
label down-interpolated to 64×64.
|
| 52 |
+
|
| 53 |
+
## Quick start
|
| 54 |
+
|
| 55 |
+
```python
|
| 56 |
+
import torch
|
| 57 |
+
from transformers import AutoModel
|
| 58 |
+
|
| 59 |
+
model = AutoModel.from_pretrained(
|
| 60 |
+
"YoussefMoNader/PHerc.1667-iteration-4",
|
| 61 |
+
trust_remote_code=True,
|
| 62 |
+
).eval().cuda()
|
| 63 |
+
|
| 64 |
+
# Input: float32, shape (B, 1, D=62, H=256, W=256).
|
| 65 |
+
# Explicitly reproduce clipping followed by Albumentations'
|
| 66 |
+
# Normalize(mean=0, std=1, max_pixel_value=255): clip(raw, 0, 200) / 255.
|
| 67 |
+
# This is intensity scaling, not per-tile z-score normalization.
|
| 68 |
+
# This synthetic tensor demonstrates the call shape only, not ink quality.
|
| 69 |
+
raw = torch.randint(0, 256, (1, 1, 62, 256, 256), device="cuda")
|
| 70 |
+
x = raw.float().clamp(0, 200) / 255.0
|
| 71 |
+
|
| 72 |
+
with torch.no_grad():
|
| 73 |
+
out = model(x)
|
| 74 |
+
|
| 75 |
+
print(out.logits.shape) # torch.Size([1, 1, 64, 64])
|
| 76 |
+
prob = torch.sigmoid(out.logits) # ink probability per pixel
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Full-segment inference (tiling)
|
| 80 |
+
|
| 81 |
+
The model only sees 256×256 windows. For a full scroll segment you
|
| 82 |
+
need to slide the window across the (padded) layer stack and average
|
| 83 |
+
overlapping predictions:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
import numpy as np, cv2, torch
|
| 87 |
+
import torch.nn.functional as F
|
| 88 |
+
from transformers import AutoModel
|
| 89 |
+
|
| 90 |
+
model = AutoModel.from_pretrained(
|
| 91 |
+
"YoussefMoNader/PHerc.1667-iteration-4", trust_remote_code=True,
|
| 92 |
+
).eval().cuda()
|
| 93 |
+
|
| 94 |
+
WINDOW, STRIDE = 256, 128 # 128 = 2x oversample; 64 for 8x oversample
|
| 95 |
+
D = 62 # number of z-layers
|
| 96 |
+
|
| 97 |
+
# image: (H, W, D) uint8 stack of the 62 layers, padded to multiples of 256.
|
| 98 |
+
# fmask: (H, W) uint8 fragment mask (0 = outside, 255 = inside).
|
| 99 |
+
H, W, _ = image.shape
|
| 100 |
+
mask_pred = np.zeros((H, W), dtype=np.float32)
|
| 101 |
+
mask_count = np.zeros((H, W), dtype=np.float32)
|
| 102 |
+
|
| 103 |
+
with torch.no_grad():
|
| 104 |
+
for y in range(0, H - WINDOW + 1, STRIDE):
|
| 105 |
+
for x in range(0, W - WINDOW + 1, STRIDE):
|
| 106 |
+
if np.any(fmask[y:y+WINDOW, x:x+WINDOW] == 0):
|
| 107 |
+
continue
|
| 108 |
+
tile = image[y:y+WINDOW, x:x+WINDOW] # (256,256,62)
|
| 109 |
+
# Apply the same input scaling as the quick start; keep image unchanged.
|
| 110 |
+
tile = np.clip(tile.astype(np.float32), 0, 200) / 255.0
|
| 111 |
+
t = torch.from_numpy(tile).permute(2, 0, 1) # (62,256,256)
|
| 112 |
+
t = t.unsqueeze(0).unsqueeze(0).float().cuda() # (1,1,62,256,256)
|
| 113 |
+
logits = model(t).logits # (1,1,64,64)
|
| 114 |
+
prob = torch.sigmoid(logits)
|
| 115 |
+
prob = F.interpolate(prob, scale_factor=4,
|
| 116 |
+
mode="bilinear").squeeze().cpu().numpy()
|
| 117 |
+
mask_pred[y:y+WINDOW, x:x+WINDOW] += prob
|
| 118 |
+
mask_count[y:y+WINDOW, x:x+WINDOW] += 1.0
|
| 119 |
+
|
| 120 |
+
pred = np.divide(mask_pred, mask_count,
|
| 121 |
+
out=np.zeros_like(mask_pred),
|
| 122 |
+
where=mask_count != 0)
|
| 123 |
+
cv2.imwrite("prediction.png", np.clip(pred * 255, 0, 255).astype(np.uint8))
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
## Training summary
|
| 127 |
+
|
| 128 |
+
| | |
|
| 129 |
+
|---|---|
|
| 130 |
+
| **Backbone** | ResNet3D-50 (3-D conv, BN, ReLU residual blocks) |
|
| 131 |
+
| **Encoder init** | `r3d50_KM_200ep.pth` (Kinetics-700), conv1 summed across RGB |
|
| 132 |
+
| **Decoder** | 2-D U-Net (3 up-blocks: bilinear 2× + concat skip + 3×3 conv + BN + ReLU) |
|
| 133 |
+
| **Output** | 1 channel, sigmoid logit, quarter-resolution (64×64) |
|
| 134 |
+
| **Loss** | 0.5 × Dice + 0.5 × SoftBCE (smooth = 0.25) |
|
| 135 |
+
| **Optimizer** | AdamW, OneCycle lr 2e-5 → 3e-4, pct_start = 0.15 |
|
| 136 |
+
| **Batch** | 2 (effective 8 via accumulate 4), 16-mixed, grad-clip 1.0 |
|
| 137 |
+
| **Max steps** | 12,396 (= 3 epochs over the densest ablation label) |
|
| 138 |
+
| **Training segment(s)** | `l_2` |
|
| 139 |
+
| **Training label** | `l_2_inklabels4.png` |
|
| 140 |
+
| **Training tiles** (256×256 sub-tiles at stride 64) | **24,773** |
|
| 141 |
+
| **Final train loss (`_epoch`)** | **0.5256** |
|
| 142 |
+
| **Final train loss (`_step`, single-batch noise)** | 0.5005 |
|
| 143 |
+
| **Wandb** | [vesuvius-challenge/paper/l2_ink4_l5infer](https://wandb.ai/vesuvius-challenge/paper/runs/h73rxb10) |
|
| 144 |
+
| **Random seed** | 130697 |
|
| 145 |
+
| **Determinism** | `cudnn.deterministic = True`, `cudnn.benchmark = False` |
|
| 146 |
+
| **Hardware** | 1 × NVIDIA H100 80 GB; ≈ 2 h end-to-end (load + train + inference) |
|
| 147 |
+
|
| 148 |
+
## Files
|
| 149 |
+
|
| 150 |
+
| file | size | description |
|
| 151 |
+
|------|------|-------------|
|
| 152 |
+
| `config.json` | 1 KB | architecture + provenance metadata; loaded by `AutoConfig` |
|
| 153 |
+
| `configuration_inkdetection.py` | 2 KB | `InkDetectionConfig(PretrainedConfig)` |
|
| 154 |
+
| `modeling_inkdetection.py` | 9 KB | self-contained `InkDetectionModel(PreTrainedModel)` |
|
| 155 |
+
| `model.safetensors` | 319 MB | converted weights (338 tensors) |
|
| 156 |
+
| `last.ckpt` | 963 MB | original PyTorch-Lightning checkpoint (incl. optimizer + LR-scheduler state) — load with `torch.load(...)["state_dict"]` |
|
| 157 |
+
| `preview_l_2.png` | ~700 KB | low-res preview of the l_2 prediction (1/16 scale, 180° rotated) |
|
| 158 |
+
| `preview_l_5.png` | ~2 MB | low-res preview of the l_5 (held-out) prediction |
|
| 159 |
+
| `preview_label.png` | ~50 KB | the training label, same scale + rotation |
|
| 160 |
+
|
| 161 |
+
The HuggingFace weights are **bit-perfect identical** to the original
|
| 162 |
+
PyTorch-Lightning checkpoint (verified `max abs diff = 0.0e+00` on
|
| 163 |
+
identical inputs). Use `model.safetensors` for `AutoModel.from_pretrained`;
|
| 164 |
+
use `last.ckpt` only if you want to resume training from the saved
|
| 165 |
+
optimizer / scheduler state.
|
| 166 |
+
|
| 167 |
+
## The model family
|
| 168 |
+
|
| 169 |
+
| model | training segment(s) | label | tiles | effective epochs |
|
| 170 |
+
|-------|---------------------|-------|-------|-------------------|
|
| 171 |
+
| [`PHerc.1667-iteration-0`](https://huggingface.co/scrollprize/PHerc.1667-iteration-0) | 500p2a + 658 + 20250910185200 + 20250919125754* | (cross-segment baseline) | 20,075 | ~5 |
|
| 172 |
+
| [`PHerc.1667-iteration-1`](https://huggingface.co/scrollprize/PHerc.1667-iteration-1) | `l_2` | `l_2_inklabels.png` | 3,396 | ~30 |
|
| 173 |
+
| [`PHerc.1667-iteration-2`](https://huggingface.co/scrollprize/PHerc.1667-iteration-2) | `l_2` | `l_2_inklabels2.png` | 8,970 | ~12 |
|
| 174 |
+
| [`PHerc.1667-iteration-3`](https://huggingface.co/scrollprize/PHerc.1667-iteration-3) | `l_2` | `l_2_inklabels3.png` | 15,286 | ~7 |
|
| 175 |
+
| [`PHerc.1667-iteration-4`](https://huggingface.co/scrollprize/PHerc.1667-iteration-4) | `l_2` | `l_2_inklabels4.png` | 24,773 | ~5 |
|
| 176 |
+
| [`PHerc.1667-iteration-5`](https://huggingface.co/scrollprize/PHerc.1667-iteration-5) | `l_2` | `l_2_inklabels5.png` | 33,061 | 3 |
|
| 177 |
+
|
| 178 |
+
All six share the architecture, hyperparameters, and a fixed step
|
| 179 |
+
budget of 12,396 optimizer steps; the only thing that varies between
|
| 180 |
+
rows is the supervising label (or, for ink0, the training segments).
|
| 181 |
+
|
| 182 |
+
## Citation
|
| 183 |
+
|
| 184 |
+
If you use this model in published work, please cite the Vesuvius
|
| 185 |
+
Challenge and the underlying ResNet3D paper:
|
| 186 |
+
|
| 187 |
+
```bibtex
|
| 188 |
+
@inproceedings{hara2018can,
|
| 189 |
+
title = {Can spatiotemporal 3D CNNs retrace the history of 2D CNNs and ImageNet?},
|
| 190 |
+
author = {Hara, Kensho and Kataoka, Hirokatsu and Satoh, Yutaka},
|
| 191 |
+
booktitle = {CVPR}, year = {2018},
|
| 192 |
+
}
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
## Licence
|
| 196 |
+
|
| 197 |
+
MIT.
|
modeling_inkdetection.py
CHANGED
|
@@ -1,239 +1,239 @@
|
|
| 1 |
-
"""HuggingFace-style wrapper around the ResNet3D-50 + 2-D U-Net ink-detection model.
|
| 2 |
-
|
| 3 |
-
This file is **self-contained** — vendored ResNet3D-50 (Hara et al., 2018)
|
| 4 |
-
inline with the decoder so a downloader only needs `transformers` and
|
| 5 |
-
`torch`. Loadable via:
|
| 6 |
-
|
| 7 |
-
from transformers import AutoModel
|
| 8 |
-
model = AutoModel.from_pretrained(
|
| 9 |
-
"<user>/<repo>", trust_remote_code=True
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
Input: float32 tensor of shape `(B, 1, D, H, W)` or `(B, D, H, W)`,
|
| 13 |
-
where D = 62, H = W = 256 (
|
| 14 |
-
Output: `ModelOutput(logits=<sigmoid logits, shape (B, 1, H/4, W/4)>)`.
|
| 15 |
-
"""
|
| 16 |
-
from dataclasses import dataclass
|
| 17 |
-
from functools import partial
|
| 18 |
-
from typing import List, Optional, Tuple
|
| 19 |
-
|
| 20 |
-
import torch
|
| 21 |
-
import torch.nn as nn
|
| 22 |
-
import torch.nn.functional as F
|
| 23 |
-
|
| 24 |
-
from transformers import PreTrainedModel
|
| 25 |
-
from transformers.modeling_outputs import ModelOutput
|
| 26 |
-
|
| 27 |
-
from .configuration_inkdetection import InkDetectionConfig
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
# =============================================================================
|
| 31 |
-
# Vendored ResNet3D-50 (Hara, Kataoka & Satoh, 2018)
|
| 32 |
-
# =============================================================================
|
| 33 |
-
def _conv3x3x3(in_planes, out_planes, stride=1):
|
| 34 |
-
return nn.Conv3d(in_planes, out_planes, kernel_size=3,
|
| 35 |
-
stride=stride, padding=1, bias=False)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _conv1x1x1(in_planes, out_planes, stride=1):
|
| 39 |
-
return nn.Conv3d(in_planes, out_planes, kernel_size=1,
|
| 40 |
-
stride=stride, bias=False)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
class _Bottleneck(nn.Module):
|
| 44 |
-
expansion = 4
|
| 45 |
-
|
| 46 |
-
def __init__(self, in_planes, planes, stride=1, downsample=None):
|
| 47 |
-
super().__init__()
|
| 48 |
-
self.conv1 = _conv1x1x1(in_planes, planes)
|
| 49 |
-
self.bn1 = nn.BatchNorm3d(planes)
|
| 50 |
-
self.conv2 = _conv3x3x3(planes, planes, stride)
|
| 51 |
-
self.bn2 = nn.BatchNorm3d(planes)
|
| 52 |
-
self.conv3 = _conv1x1x1(planes, planes * self.expansion)
|
| 53 |
-
self.bn3 = nn.BatchNorm3d(planes * self.expansion)
|
| 54 |
-
self.relu = nn.ReLU(inplace=True)
|
| 55 |
-
self.downsample = downsample
|
| 56 |
-
self.stride = stride
|
| 57 |
-
|
| 58 |
-
def forward(self, x):
|
| 59 |
-
residual = x
|
| 60 |
-
out = self.relu(self.bn1(self.conv1(x)))
|
| 61 |
-
out = self.relu(self.bn2(self.conv2(out)))
|
| 62 |
-
out = self.bn3(self.conv3(out))
|
| 63 |
-
if self.downsample is not None:
|
| 64 |
-
residual = self.downsample(x)
|
| 65 |
-
out = self.relu(out + residual)
|
| 66 |
-
return out
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
class _ResNet3D(nn.Module):
|
| 70 |
-
"""ResNet3D-50 backbone returning the 4 intermediate feature maps."""
|
| 71 |
-
|
| 72 |
-
def __init__(self, n_input_channels=1, block_inplanes=(64, 128, 256, 512),
|
| 73 |
-
layers=(3, 4, 6, 3), conv1_t_size=7, conv1_t_stride=1):
|
| 74 |
-
super().__init__()
|
| 75 |
-
self.in_planes = block_inplanes[0]
|
| 76 |
-
self.conv1 = nn.Conv3d(
|
| 77 |
-
n_input_channels, self.in_planes,
|
| 78 |
-
kernel_size=(conv1_t_size, 7, 7),
|
| 79 |
-
stride=(conv1_t_stride, 2, 2),
|
| 80 |
-
padding=(conv1_t_size // 2, 3, 3),
|
| 81 |
-
bias=False,
|
| 82 |
-
)
|
| 83 |
-
self.bn1 = nn.BatchNorm3d(self.in_planes)
|
| 84 |
-
self.relu = nn.ReLU(inplace=True)
|
| 85 |
-
self.maxpool = nn.MaxPool3d(
|
| 86 |
-
kernel_size=(1, 3, 3), stride=(1, 2, 2), padding=(0, 1, 1)
|
| 87 |
-
)
|
| 88 |
-
self.layer1 = self._make_layer(block_inplanes[0], layers[0], stride=1)
|
| 89 |
-
self.layer2 = self._make_layer(block_inplanes[1], layers[1], stride=2)
|
| 90 |
-
self.layer3 = self._make_layer(block_inplanes[2], layers[2], stride=2)
|
| 91 |
-
self.layer4 = self._make_layer(block_inplanes[3], layers[3], stride=2)
|
| 92 |
-
|
| 93 |
-
def _make_layer(self, planes, blocks, stride=1):
|
| 94 |
-
downsample = None
|
| 95 |
-
if stride != 1 or self.in_planes != planes * _Bottleneck.expansion:
|
| 96 |
-
downsample = nn.Sequential(
|
| 97 |
-
_conv1x1x1(self.in_planes,
|
| 98 |
-
planes * _Bottleneck.expansion, stride),
|
| 99 |
-
nn.BatchNorm3d(planes * _Bottleneck.expansion),
|
| 100 |
-
)
|
| 101 |
-
layers = [_Bottleneck(self.in_planes, planes, stride, downsample)]
|
| 102 |
-
self.in_planes = planes * _Bottleneck.expansion
|
| 103 |
-
for _ in range(1, blocks):
|
| 104 |
-
layers.append(_Bottleneck(self.in_planes, planes))
|
| 105 |
-
return nn.Sequential(*layers)
|
| 106 |
-
|
| 107 |
-
def forward(self, x) -> List[torch.Tensor]:
|
| 108 |
-
x = self.relu(self.bn1(self.conv1(x)))
|
| 109 |
-
x = self.maxpool(x)
|
| 110 |
-
x1 = self.layer1(x)
|
| 111 |
-
x2 = self.layer2(x1)
|
| 112 |
-
x3 = self.layer3(x2)
|
| 113 |
-
x4 = self.layer4(x3)
|
| 114 |
-
return [x1, x2, x3, x4]
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
# =============================================================================
|
| 118 |
-
# 2-D U-Net decoder
|
| 119 |
-
# =============================================================================
|
| 120 |
-
class _Decoder(nn.Module):
|
| 121 |
-
def __init__(self, encoder_dims, upscale):
|
| 122 |
-
super().__init__()
|
| 123 |
-
self.convs = nn.ModuleList([
|
| 124 |
-
nn.Sequential(
|
| 125 |
-
nn.Conv2d(encoder_dims[i] + encoder_dims[i - 1],
|
| 126 |
-
encoder_dims[i - 1], 3, 1, 1, bias=False),
|
| 127 |
-
nn.BatchNorm2d(encoder_dims[i - 1]),
|
| 128 |
-
nn.ReLU(inplace=True),
|
| 129 |
-
)
|
| 130 |
-
for i in range(1, len(encoder_dims))
|
| 131 |
-
])
|
| 132 |
-
self.logit = nn.Conv2d(encoder_dims[0], 1, 1, 1, 0)
|
| 133 |
-
self.up = nn.Upsample(scale_factor=upscale, mode='bilinear')
|
| 134 |
-
|
| 135 |
-
def forward(self, feature_maps):
|
| 136 |
-
for i in range(len(feature_maps) - 1, 0, -1):
|
| 137 |
-
f_up = F.interpolate(feature_maps[i], scale_factor=2,
|
| 138 |
-
mode='bilinear')
|
| 139 |
-
f = torch.cat([feature_maps[i - 1], f_up], dim=1)
|
| 140 |
-
feature_maps[i - 1] = self.convs[i - 1](f)
|
| 141 |
-
return self.up(self.logit(feature_maps[0]))
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# =============================================================================
|
| 145 |
-
# HuggingFace ModelOutput + PreTrainedModel
|
| 146 |
-
# =============================================================================
|
| 147 |
-
@dataclass
|
| 148 |
-
class InkDetectionOutput(ModelOutput):
|
| 149 |
-
"""Output of `InkDetectionModel.forward`.
|
| 150 |
-
|
| 151 |
-
- `logits`: pre-sigmoid prediction at quarter resolution. Shape
|
| 152 |
-
`(B, 1, H / 4, W / 4)`. Apply `torch.sigmoid` for probabilities.
|
| 153 |
-
"""
|
| 154 |
-
logits: torch.FloatTensor = None
|
| 155 |
-
loss: Optional[torch.FloatTensor] = None
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
class InkDetectionModel(PreTrainedModel):
|
| 159 |
-
"""Vesuvius Challenge ink-detection model.
|
| 160 |
-
|
| 161 |
-
Pipeline:
|
| 162 |
-
1. 3-D volume `(B, 1, D, H, W)` enters a ResNet3D-50 backbone.
|
| 163 |
-
2. Each of the 4 stages is collapsed along the z (depth) axis
|
| 164 |
-
with `torch.max` -> 2-D feature pyramid.
|
| 165 |
-
3. A small 2-D U-Net decoder upsamples coarse-to-fine with
|
| 166 |
-
concatenated skip connections.
|
| 167 |
-
4. A 1x1 conv head produces 1 logit channel.
|
| 168 |
-
"""
|
| 169 |
-
config_class = InkDetectionConfig
|
| 170 |
-
base_model_prefix = "inkdetection"
|
| 171 |
-
|
| 172 |
-
def __init__(self, config: InkDetectionConfig):
|
| 173 |
-
super().__init__(config)
|
| 174 |
-
layers_map = {50: (3, 4, 6, 3),
|
| 175 |
-
101: (3, 4, 23, 3),
|
| 176 |
-
152: (3, 8, 36, 3)}
|
| 177 |
-
if config.backbone_depth not in layers_map:
|
| 178 |
-
raise ValueError(
|
| 179 |
-
f"Unsupported backbone_depth={config.backbone_depth}; "
|
| 180 |
-
"expected one of 50, 101, 152."
|
| 181 |
-
)
|
| 182 |
-
self.backbone = _ResNet3D(
|
| 183 |
-
n_input_channels=config.in_channels,
|
| 184 |
-
block_inplanes=(64, 128, 256, 512),
|
| 185 |
-
layers=layers_map[config.backbone_depth],
|
| 186 |
-
)
|
| 187 |
-
self.decoder = _Decoder(
|
| 188 |
-
encoder_dims=list(config.backbone_channels),
|
| 189 |
-
upscale=config.decoder_upscale,
|
| 190 |
-
)
|
| 191 |
-
# No init from scratch — weights are loaded from the published
|
| 192 |
-
# checkpoint via `from_pretrained`.
|
| 193 |
-
|
| 194 |
-
def forward(
|
| 195 |
-
self,
|
| 196 |
-
pixel_values: torch.FloatTensor,
|
| 197 |
-
labels: Optional[torch.FloatTensor] = None,
|
| 198 |
-
return_dict: Optional[bool] = True,
|
| 199 |
-
**kwargs,
|
| 200 |
-
) -> InkDetectionOutput:
|
| 201 |
-
# Accept (B, D, H, W) or (B, 1, D, H, W)
|
| 202 |
-
if pixel_values.ndim == 4:
|
| 203 |
-
pixel_values = pixel_values.unsqueeze(1)
|
| 204 |
-
if pixel_values.ndim != 5:
|
| 205 |
-
raise ValueError(
|
| 206 |
-
f"pixel_values must be 4-D (B, D, H, W) or 5-D (B, 1, D, H, W); "
|
| 207 |
-
f"got shape {tuple(pixel_values.shape)}"
|
| 208 |
-
)
|
| 209 |
-
feats = self.backbone(pixel_values)
|
| 210 |
-
pooled = [torch.max(f, dim=2)[0] for f in feats]
|
| 211 |
-
logits = self.decoder(pooled)
|
| 212 |
-
|
| 213 |
-
loss = None
|
| 214 |
-
if labels is not None:
|
| 215 |
-
# Dice + SoftBCE in [0, 1] target space, label assumed to
|
| 216 |
-
# already be down-interpolated to logits.shape[-2:].
|
| 217 |
-
sig = torch.sigmoid(logits)
|
| 218 |
-
inter = (sig * labels).sum(dim=(-2, -1))
|
| 219 |
-
denom = sig.sum(dim=(-2, -1)) + labels.sum(dim=(-2, -1))
|
| 220 |
-
dice = (1.0 - (2.0 * inter + 1.0) / (denom + 1.0)).mean()
|
| 221 |
-
bce = F.binary_cross_entropy_with_logits(logits, labels)
|
| 222 |
-
loss = 0.5 * dice + 0.5 * bce
|
| 223 |
-
|
| 224 |
-
if not return_dict:
|
| 225 |
-
return (loss, logits) if loss is not None else (logits,)
|
| 226 |
-
return InkDetectionOutput(logits=logits, loss=loss)
|
| 227 |
-
|
| 228 |
-
@torch.no_grad()
|
| 229 |
-
def predict_probability(self, pixel_values: torch.FloatTensor) -> torch.FloatTensor:
|
| 230 |
-
"""Convenience: sigmoid probabilities at quarter resolution."""
|
| 231 |
-
return torch.sigmoid(self(pixel_values).logits)
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
# Register the config so HF's mapping infrastructure recognises model_type.
|
| 235 |
-
# Note: AutoModel.from_pretrained(..., trust_remote_code=True) reads `auto_map`
|
| 236 |
-
# from config.json — no explicit register call is required at import time, but
|
| 237 |
-
# it does not hurt to keep this association in place.
|
| 238 |
-
InkDetectionConfig.register_for_auto_class("AutoConfig")
|
| 239 |
-
InkDetectionModel.register_for_auto_class("AutoModel")
|
|
|
|
| 1 |
+
"""HuggingFace-style wrapper around the ResNet3D-50 + 2-D U-Net ink-detection model.
|
| 2 |
+
|
| 3 |
+
This file is **self-contained** — vendored ResNet3D-50 (Hara et al., 2018)
|
| 4 |
+
inline with the decoder so a downloader only needs `transformers` and
|
| 5 |
+
`torch`. Loadable via:
|
| 6 |
+
|
| 7 |
+
from transformers import AutoModel
|
| 8 |
+
model = AutoModel.from_pretrained(
|
| 9 |
+
"<user>/<repo>", trust_remote_code=True
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
Input: float32 tensor of shape `(B, 1, D, H, W)` or `(B, D, H, W)`,
|
| 13 |
+
where D = 62, H = W = 256 (raw uint8 clipped to [0, 200] and divided by 255; not z-score normalised).
|
| 14 |
+
Output: `ModelOutput(logits=<sigmoid logits, shape (B, 1, H/4, W/4)>)`.
|
| 15 |
+
"""
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from functools import partial
|
| 18 |
+
from typing import List, Optional, Tuple
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn as nn
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
|
| 24 |
+
from transformers import PreTrainedModel
|
| 25 |
+
from transformers.modeling_outputs import ModelOutput
|
| 26 |
+
|
| 27 |
+
from .configuration_inkdetection import InkDetectionConfig
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# =============================================================================
|
| 31 |
+
# Vendored ResNet3D-50 (Hara, Kataoka & Satoh, 2018)
|
| 32 |
+
# =============================================================================
|
| 33 |
+
def _conv3x3x3(in_planes, out_planes, stride=1):
|
| 34 |
+
return nn.Conv3d(in_planes, out_planes, kernel_size=3,
|
| 35 |
+
stride=stride, padding=1, bias=False)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _conv1x1x1(in_planes, out_planes, stride=1):
|
| 39 |
+
return nn.Conv3d(in_planes, out_planes, kernel_size=1,
|
| 40 |
+
stride=stride, bias=False)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class _Bottleneck(nn.Module):
|
| 44 |
+
expansion = 4
|
| 45 |
+
|
| 46 |
+
def __init__(self, in_planes, planes, stride=1, downsample=None):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.conv1 = _conv1x1x1(in_planes, planes)
|
| 49 |
+
self.bn1 = nn.BatchNorm3d(planes)
|
| 50 |
+
self.conv2 = _conv3x3x3(planes, planes, stride)
|
| 51 |
+
self.bn2 = nn.BatchNorm3d(planes)
|
| 52 |
+
self.conv3 = _conv1x1x1(planes, planes * self.expansion)
|
| 53 |
+
self.bn3 = nn.BatchNorm3d(planes * self.expansion)
|
| 54 |
+
self.relu = nn.ReLU(inplace=True)
|
| 55 |
+
self.downsample = downsample
|
| 56 |
+
self.stride = stride
|
| 57 |
+
|
| 58 |
+
def forward(self, x):
|
| 59 |
+
residual = x
|
| 60 |
+
out = self.relu(self.bn1(self.conv1(x)))
|
| 61 |
+
out = self.relu(self.bn2(self.conv2(out)))
|
| 62 |
+
out = self.bn3(self.conv3(out))
|
| 63 |
+
if self.downsample is not None:
|
| 64 |
+
residual = self.downsample(x)
|
| 65 |
+
out = self.relu(out + residual)
|
| 66 |
+
return out
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class _ResNet3D(nn.Module):
|
| 70 |
+
"""ResNet3D-50 backbone returning the 4 intermediate feature maps."""
|
| 71 |
+
|
| 72 |
+
def __init__(self, n_input_channels=1, block_inplanes=(64, 128, 256, 512),
|
| 73 |
+
layers=(3, 4, 6, 3), conv1_t_size=7, conv1_t_stride=1):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.in_planes = block_inplanes[0]
|
| 76 |
+
self.conv1 = nn.Conv3d(
|
| 77 |
+
n_input_channels, self.in_planes,
|
| 78 |
+
kernel_size=(conv1_t_size, 7, 7),
|
| 79 |
+
stride=(conv1_t_stride, 2, 2),
|
| 80 |
+
padding=(conv1_t_size // 2, 3, 3),
|
| 81 |
+
bias=False,
|
| 82 |
+
)
|
| 83 |
+
self.bn1 = nn.BatchNorm3d(self.in_planes)
|
| 84 |
+
self.relu = nn.ReLU(inplace=True)
|
| 85 |
+
self.maxpool = nn.MaxPool3d(
|
| 86 |
+
kernel_size=(1, 3, 3), stride=(1, 2, 2), padding=(0, 1, 1)
|
| 87 |
+
)
|
| 88 |
+
self.layer1 = self._make_layer(block_inplanes[0], layers[0], stride=1)
|
| 89 |
+
self.layer2 = self._make_layer(block_inplanes[1], layers[1], stride=2)
|
| 90 |
+
self.layer3 = self._make_layer(block_inplanes[2], layers[2], stride=2)
|
| 91 |
+
self.layer4 = self._make_layer(block_inplanes[3], layers[3], stride=2)
|
| 92 |
+
|
| 93 |
+
def _make_layer(self, planes, blocks, stride=1):
|
| 94 |
+
downsample = None
|
| 95 |
+
if stride != 1 or self.in_planes != planes * _Bottleneck.expansion:
|
| 96 |
+
downsample = nn.Sequential(
|
| 97 |
+
_conv1x1x1(self.in_planes,
|
| 98 |
+
planes * _Bottleneck.expansion, stride),
|
| 99 |
+
nn.BatchNorm3d(planes * _Bottleneck.expansion),
|
| 100 |
+
)
|
| 101 |
+
layers = [_Bottleneck(self.in_planes, planes, stride, downsample)]
|
| 102 |
+
self.in_planes = planes * _Bottleneck.expansion
|
| 103 |
+
for _ in range(1, blocks):
|
| 104 |
+
layers.append(_Bottleneck(self.in_planes, planes))
|
| 105 |
+
return nn.Sequential(*layers)
|
| 106 |
+
|
| 107 |
+
def forward(self, x) -> List[torch.Tensor]:
|
| 108 |
+
x = self.relu(self.bn1(self.conv1(x)))
|
| 109 |
+
x = self.maxpool(x)
|
| 110 |
+
x1 = self.layer1(x)
|
| 111 |
+
x2 = self.layer2(x1)
|
| 112 |
+
x3 = self.layer3(x2)
|
| 113 |
+
x4 = self.layer4(x3)
|
| 114 |
+
return [x1, x2, x3, x4]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# =============================================================================
|
| 118 |
+
# 2-D U-Net decoder
|
| 119 |
+
# =============================================================================
|
| 120 |
+
class _Decoder(nn.Module):
|
| 121 |
+
def __init__(self, encoder_dims, upscale):
|
| 122 |
+
super().__init__()
|
| 123 |
+
self.convs = nn.ModuleList([
|
| 124 |
+
nn.Sequential(
|
| 125 |
+
nn.Conv2d(encoder_dims[i] + encoder_dims[i - 1],
|
| 126 |
+
encoder_dims[i - 1], 3, 1, 1, bias=False),
|
| 127 |
+
nn.BatchNorm2d(encoder_dims[i - 1]),
|
| 128 |
+
nn.ReLU(inplace=True),
|
| 129 |
+
)
|
| 130 |
+
for i in range(1, len(encoder_dims))
|
| 131 |
+
])
|
| 132 |
+
self.logit = nn.Conv2d(encoder_dims[0], 1, 1, 1, 0)
|
| 133 |
+
self.up = nn.Upsample(scale_factor=upscale, mode='bilinear')
|
| 134 |
+
|
| 135 |
+
def forward(self, feature_maps):
|
| 136 |
+
for i in range(len(feature_maps) - 1, 0, -1):
|
| 137 |
+
f_up = F.interpolate(feature_maps[i], scale_factor=2,
|
| 138 |
+
mode='bilinear')
|
| 139 |
+
f = torch.cat([feature_maps[i - 1], f_up], dim=1)
|
| 140 |
+
feature_maps[i - 1] = self.convs[i - 1](f)
|
| 141 |
+
return self.up(self.logit(feature_maps[0]))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# =============================================================================
|
| 145 |
+
# HuggingFace ModelOutput + PreTrainedModel
|
| 146 |
+
# =============================================================================
|
| 147 |
+
@dataclass
|
| 148 |
+
class InkDetectionOutput(ModelOutput):
|
| 149 |
+
"""Output of `InkDetectionModel.forward`.
|
| 150 |
+
|
| 151 |
+
- `logits`: pre-sigmoid prediction at quarter resolution. Shape
|
| 152 |
+
`(B, 1, H / 4, W / 4)`. Apply `torch.sigmoid` for probabilities.
|
| 153 |
+
"""
|
| 154 |
+
logits: torch.FloatTensor = None
|
| 155 |
+
loss: Optional[torch.FloatTensor] = None
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class InkDetectionModel(PreTrainedModel):
|
| 159 |
+
"""Vesuvius Challenge ink-detection model.
|
| 160 |
+
|
| 161 |
+
Pipeline:
|
| 162 |
+
1. 3-D volume `(B, 1, D, H, W)` enters a ResNet3D-50 backbone.
|
| 163 |
+
2. Each of the 4 stages is collapsed along the z (depth) axis
|
| 164 |
+
with `torch.max` -> 2-D feature pyramid.
|
| 165 |
+
3. A small 2-D U-Net decoder upsamples coarse-to-fine with
|
| 166 |
+
concatenated skip connections.
|
| 167 |
+
4. A 1x1 conv head produces 1 logit channel.
|
| 168 |
+
"""
|
| 169 |
+
config_class = InkDetectionConfig
|
| 170 |
+
base_model_prefix = "inkdetection"
|
| 171 |
+
|
| 172 |
+
def __init__(self, config: InkDetectionConfig):
|
| 173 |
+
super().__init__(config)
|
| 174 |
+
layers_map = {50: (3, 4, 6, 3),
|
| 175 |
+
101: (3, 4, 23, 3),
|
| 176 |
+
152: (3, 8, 36, 3)}
|
| 177 |
+
if config.backbone_depth not in layers_map:
|
| 178 |
+
raise ValueError(
|
| 179 |
+
f"Unsupported backbone_depth={config.backbone_depth}; "
|
| 180 |
+
"expected one of 50, 101, 152."
|
| 181 |
+
)
|
| 182 |
+
self.backbone = _ResNet3D(
|
| 183 |
+
n_input_channels=config.in_channels,
|
| 184 |
+
block_inplanes=(64, 128, 256, 512),
|
| 185 |
+
layers=layers_map[config.backbone_depth],
|
| 186 |
+
)
|
| 187 |
+
self.decoder = _Decoder(
|
| 188 |
+
encoder_dims=list(config.backbone_channels),
|
| 189 |
+
upscale=config.decoder_upscale,
|
| 190 |
+
)
|
| 191 |
+
# No init from scratch — weights are loaded from the published
|
| 192 |
+
# checkpoint via `from_pretrained`.
|
| 193 |
+
|
| 194 |
+
def forward(
|
| 195 |
+
self,
|
| 196 |
+
pixel_values: torch.FloatTensor,
|
| 197 |
+
labels: Optional[torch.FloatTensor] = None,
|
| 198 |
+
return_dict: Optional[bool] = True,
|
| 199 |
+
**kwargs,
|
| 200 |
+
) -> InkDetectionOutput:
|
| 201 |
+
# Accept (B, D, H, W) or (B, 1, D, H, W)
|
| 202 |
+
if pixel_values.ndim == 4:
|
| 203 |
+
pixel_values = pixel_values.unsqueeze(1)
|
| 204 |
+
if pixel_values.ndim != 5:
|
| 205 |
+
raise ValueError(
|
| 206 |
+
f"pixel_values must be 4-D (B, D, H, W) or 5-D (B, 1, D, H, W); "
|
| 207 |
+
f"got shape {tuple(pixel_values.shape)}"
|
| 208 |
+
)
|
| 209 |
+
feats = self.backbone(pixel_values)
|
| 210 |
+
pooled = [torch.max(f, dim=2)[0] for f in feats]
|
| 211 |
+
logits = self.decoder(pooled)
|
| 212 |
+
|
| 213 |
+
loss = None
|
| 214 |
+
if labels is not None:
|
| 215 |
+
# Dice + SoftBCE in [0, 1] target space, label assumed to
|
| 216 |
+
# already be down-interpolated to logits.shape[-2:].
|
| 217 |
+
sig = torch.sigmoid(logits)
|
| 218 |
+
inter = (sig * labels).sum(dim=(-2, -1))
|
| 219 |
+
denom = sig.sum(dim=(-2, -1)) + labels.sum(dim=(-2, -1))
|
| 220 |
+
dice = (1.0 - (2.0 * inter + 1.0) / (denom + 1.0)).mean()
|
| 221 |
+
bce = F.binary_cross_entropy_with_logits(logits, labels)
|
| 222 |
+
loss = 0.5 * dice + 0.5 * bce
|
| 223 |
+
|
| 224 |
+
if not return_dict:
|
| 225 |
+
return (loss, logits) if loss is not None else (logits,)
|
| 226 |
+
return InkDetectionOutput(logits=logits, loss=loss)
|
| 227 |
+
|
| 228 |
+
@torch.no_grad()
|
| 229 |
+
def predict_probability(self, pixel_values: torch.FloatTensor) -> torch.FloatTensor:
|
| 230 |
+
"""Convenience: sigmoid probabilities at quarter resolution."""
|
| 231 |
+
return torch.sigmoid(self(pixel_values).logits)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
# Register the config so HF's mapping infrastructure recognises model_type.
|
| 235 |
+
# Note: AutoModel.from_pretrained(..., trust_remote_code=True) reads `auto_map`
|
| 236 |
+
# from config.json — no explicit register call is required at import time, but
|
| 237 |
+
# it does not hurt to keep this association in place.
|
| 238 |
+
InkDetectionConfig.register_for_auto_class("AutoConfig")
|
| 239 |
+
InkDetectionModel.register_for_auto_class("AutoModel")
|