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-1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use scrollprize/PHerc.1667-iteration-1 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="scrollprize/PHerc.1667-iteration-1", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("scrollprize/PHerc.1667-iteration-1", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Initial commit: weights + custom modeling
Browse files- .gitattributes +2 -0
- README.md +193 -0
- config.json +29 -0
- configuration_inkdetection.py +48 -0
- last.ckpt +3 -0
- model.safetensors +3 -0
- modeling_inkdetection.py +239 -0
- preview_l_2.png +3 -0
- preview_l_5.png +3 -0
- preview_label.png +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
preview_l_2.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
preview_l_5.png filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,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 |
+
# vesuvius-inkdetection-ink1
|
| 16 |
+
|
| 17 |
+
> **Trained on segment l_2 with l_2_inklabels.png (3,396 tiles).**
|
| 18 |
+
|
| 19 |
+
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.
|
| 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/vesuvius-inkdetection-ink1",
|
| 61 |
+
trust_remote_code=True,
|
| 62 |
+
).eval().cuda()
|
| 63 |
+
|
| 64 |
+
# Input: float32, shape (B, 1, D=62, H=256, W=256).
|
| 65 |
+
# Intensity should already be in roughly [0, 1] (the training pipeline
|
| 66 |
+
# clipped raw uint8 layers to [0, 200] then applied Normalize(mean=0, std=1)
|
| 67 |
+
# which keeps the magnitude small).
|
| 68 |
+
x = torch.randn(1, 1, 62, 256, 256, device="cuda")
|
| 69 |
+
|
| 70 |
+
with torch.no_grad():
|
| 71 |
+
out = model(x)
|
| 72 |
+
|
| 73 |
+
print(out.logits.shape) # torch.Size([1, 1, 64, 64])
|
| 74 |
+
prob = torch.sigmoid(out.logits) # ink probability per pixel
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Full-segment inference (tiling)
|
| 78 |
+
|
| 79 |
+
The model only sees 256×256 windows. For a full scroll segment you
|
| 80 |
+
need to slide the window across the (padded) layer stack and average
|
| 81 |
+
overlapping predictions:
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
import numpy as np, cv2, torch
|
| 85 |
+
import torch.nn.functional as F
|
| 86 |
+
from transformers import AutoModel
|
| 87 |
+
|
| 88 |
+
model = AutoModel.from_pretrained(
|
| 89 |
+
"YoussefMoNader/vesuvius-inkdetection-ink1", trust_remote_code=True,
|
| 90 |
+
).eval().cuda()
|
| 91 |
+
|
| 92 |
+
WINDOW, STRIDE = 256, 128 # 128 = 2x oversample; 64 for 8x oversample
|
| 93 |
+
D = 62 # number of z-layers
|
| 94 |
+
|
| 95 |
+
# image: (H, W, D) uint8 stack of the 62 layers, padded to multiples of 256.
|
| 96 |
+
# fmask: (H, W) uint8 fragment mask (0 = outside, 255 = inside).
|
| 97 |
+
H, W, _ = image.shape
|
| 98 |
+
mask_pred = np.zeros((H, W), dtype=np.float32)
|
| 99 |
+
mask_count = np.zeros((H, W), dtype=np.float32)
|
| 100 |
+
|
| 101 |
+
with torch.no_grad():
|
| 102 |
+
for y in range(0, H - WINDOW + 1, STRIDE):
|
| 103 |
+
for x in range(0, W - WINDOW + 1, STRIDE):
|
| 104 |
+
if np.any(fmask[y:y+WINDOW, x:x+WINDOW] == 0):
|
| 105 |
+
continue
|
| 106 |
+
tile = image[y:y+WINDOW, x:x+WINDOW] # (256,256,62)
|
| 107 |
+
t = torch.from_numpy(tile).permute(2, 0, 1) # (62,256,256)
|
| 108 |
+
t = t.unsqueeze(0).unsqueeze(0).float().cuda() # (1,1,62,256,256)
|
| 109 |
+
logits = model(t).logits # (1,1,64,64)
|
| 110 |
+
prob = torch.sigmoid(logits)
|
| 111 |
+
prob = F.interpolate(prob, scale_factor=4,
|
| 112 |
+
mode="bilinear").squeeze().cpu().numpy()
|
| 113 |
+
mask_pred[y:y+WINDOW, x:x+WINDOW] += prob
|
| 114 |
+
mask_count[y:y+WINDOW, x:x+WINDOW] += 1.0
|
| 115 |
+
|
| 116 |
+
pred = np.divide(mask_pred, mask_count,
|
| 117 |
+
out=np.zeros_like(mask_pred),
|
| 118 |
+
where=mask_count != 0)
|
| 119 |
+
cv2.imwrite("prediction.png", np.clip(pred * 255, 0, 255).astype(np.uint8))
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## Training summary
|
| 123 |
+
|
| 124 |
+
| | |
|
| 125 |
+
|---|---|
|
| 126 |
+
| **Backbone** | ResNet3D-50 (3-D conv, BN, ReLU residual blocks) |
|
| 127 |
+
| **Encoder init** | `r3d50_KM_200ep.pth` (Kinetics-700), conv1 summed across RGB |
|
| 128 |
+
| **Decoder** | 2-D U-Net (3 up-blocks: bilinear 2× + concat skip + 3×3 conv + BN + ReLU) |
|
| 129 |
+
| **Output** | 1 channel, sigmoid logit, quarter-resolution (64×64) |
|
| 130 |
+
| **Loss** | 0.5 × Dice + 0.5 × SoftBCE (smooth = 0.25) |
|
| 131 |
+
| **Optimizer** | AdamW, OneCycle lr 2e-5 → 3e-4, pct_start = 0.15 |
|
| 132 |
+
| **Batch** | 2 (effective 8 via accumulate 4), 16-mixed, grad-clip 1.0 |
|
| 133 |
+
| **Max steps** | 12,396 (= 3 epochs over the densest ablation label) |
|
| 134 |
+
| **Training segment(s)** | `l_2` |
|
| 135 |
+
| **Training label** | `l_2_inklabels.png` |
|
| 136 |
+
| **Training tiles** (256×256 sub-tiles at stride 64) | **3,396** |
|
| 137 |
+
| **Final train loss (`_epoch`)** | **0.4219** |
|
| 138 |
+
| **Final train loss (`_step`, single-batch noise)** | 0.4381 |
|
| 139 |
+
| **Wandb** | [vesuvius-challenge/Nature/l2_ink1_l5infer](https://wandb.ai/Nature/l2_ink1_l5infer) |
|
| 140 |
+
| **Random seed** | 130697 |
|
| 141 |
+
| **Determinism** | `cudnn.deterministic = True`, `cudnn.benchmark = False` |
|
| 142 |
+
| **Hardware** | 1 × NVIDIA H100 80 GB; ≈ 2 h end-to-end (load + train + inference) |
|
| 143 |
+
|
| 144 |
+
## Files
|
| 145 |
+
|
| 146 |
+
| file | size | description |
|
| 147 |
+
|------|------|-------------|
|
| 148 |
+
| `config.json` | 1 KB | architecture + provenance metadata; loaded by `AutoConfig` |
|
| 149 |
+
| `configuration_inkdetection.py` | 2 KB | `InkDetectionConfig(PretrainedConfig)` |
|
| 150 |
+
| `modeling_inkdetection.py` | 9 KB | self-contained `InkDetectionModel(PreTrainedModel)` |
|
| 151 |
+
| `model.safetensors` | 319 MB | converted weights (338 tensors) |
|
| 152 |
+
| `last.ckpt` | 963 MB | original PyTorch-Lightning checkpoint (incl. optimizer + LR-scheduler state) — load with `torch.load(...)["state_dict"]` |
|
| 153 |
+
| `preview_l_2.png` | ~700 KB | low-res preview of the l_2 prediction (1/16 scale, 180° rotated) |
|
| 154 |
+
| `preview_l_5.png` | ~2 MB | low-res preview of the l_5 (held-out) prediction |
|
| 155 |
+
| `preview_label.png` | ~50 KB | the training label, same scale + rotation |
|
| 156 |
+
|
| 157 |
+
The HuggingFace weights are **bit-perfect identical** to the original
|
| 158 |
+
PyTorch-Lightning checkpoint (verified `max abs diff = 0.0e+00` on
|
| 159 |
+
identical inputs). Use `model.safetensors` for `AutoModel.from_pretrained`;
|
| 160 |
+
use `last.ckpt` only if you want to resume training from the saved
|
| 161 |
+
optimizer / scheduler state.
|
| 162 |
+
|
| 163 |
+
## The model family
|
| 164 |
+
|
| 165 |
+
| model | training segment(s) | label | tiles | effective epochs |
|
| 166 |
+
|-------|---------------------|-------|-------|-------------------|
|
| 167 |
+
| [`vesuvius-inkdetection-ink0`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink0) | 500p2a + 658 + 20250910185200 + 20250919125754* | (cross-segment baseline) | 20,075 | ~5 |
|
| 168 |
+
| [`vesuvius-inkdetection-ink1`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink1) | `l_2` | `l_2_inklabels.png` | 3,396 | ~30 |
|
| 169 |
+
| [`vesuvius-inkdetection-ink2`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink2) | `l_2` | `l_2_inklabels2.png` | 8,970 | ~12 |
|
| 170 |
+
| [`vesuvius-inkdetection-ink3`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink3) | `l_2` | `l_2_inklabels3.png` | 15,286 | ~7 |
|
| 171 |
+
| [`vesuvius-inkdetection-ink4`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink4) | `l_2` | `l_2_inklabels4.png` | 24,773 | ~5 |
|
| 172 |
+
| [`vesuvius-inkdetection-ink5`](https://huggingface.co/YoussefMoNader/vesuvius-inkdetection-ink5) | `l_2` | `l_2_inklabels5.png` | 33,061 | 3 |
|
| 173 |
+
|
| 174 |
+
All six share the architecture, hyperparameters, and a fixed step
|
| 175 |
+
budget of 12,396 optimizer steps; the only thing that varies between
|
| 176 |
+
rows is the supervising label (or, for ink0, the training segments).
|
| 177 |
+
|
| 178 |
+
## Citation
|
| 179 |
+
|
| 180 |
+
If you use this model in published work, please cite the Vesuvius
|
| 181 |
+
Challenge and the underlying ResNet3D paper:
|
| 182 |
+
|
| 183 |
+
```bibtex
|
| 184 |
+
@inproceedings{hara2018can,
|
| 185 |
+
title = {Can spatiotemporal 3D CNNs retrace the history of 2D CNNs and ImageNet?},
|
| 186 |
+
author = {Hara, Kensho and Kataoka, Hirokatsu and Satoh, Yutaka},
|
| 187 |
+
booktitle = {CVPR}, year = {2018},
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
## Licence
|
| 192 |
+
|
| 193 |
+
MIT.
|
config.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "inkdetection_resnet3d",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"InkDetectionModel"
|
| 5 |
+
],
|
| 6 |
+
"auto_map": {
|
| 7 |
+
"AutoConfig": "configuration_inkdetection.InkDetectionConfig",
|
| 8 |
+
"AutoModel": "modeling_inkdetection.InkDetectionModel"
|
| 9 |
+
},
|
| 10 |
+
"in_channels": 1,
|
| 11 |
+
"input_depth": 62,
|
| 12 |
+
"input_size": 256,
|
| 13 |
+
"backbone_depth": 50,
|
| 14 |
+
"backbone_channels": [
|
| 15 |
+
256,
|
| 16 |
+
512,
|
| 17 |
+
1024,
|
| 18 |
+
2048
|
| 19 |
+
],
|
| 20 |
+
"num_classes": 1,
|
| 21 |
+
"decoder_upscale": 1,
|
| 22 |
+
"train_segment": "l_2",
|
| 23 |
+
"train_inklabels": "l_2_inklabels.png",
|
| 24 |
+
"train_steps": 12396,
|
| 25 |
+
"train_tiles": 3396,
|
| 26 |
+
"train_loss_final": 0.4219,
|
| 27 |
+
"torch_dtype": "float32",
|
| 28 |
+
"transformers_version": "4.57.6"
|
| 29 |
+
}
|
configuration_inkdetection.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration class for the Vesuvius ink-detection model.
|
| 2 |
+
|
| 3 |
+
See modeling_inkdetection.py for the model itself. Designed to be
|
| 4 |
+
loaded with `AutoConfig.from_pretrained(..., trust_remote_code=True)`.
|
| 5 |
+
"""
|
| 6 |
+
from transformers import PretrainedConfig
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class InkDetectionConfig(PretrainedConfig):
|
| 10 |
+
"""Configuration for an InkDetectionModel.
|
| 11 |
+
|
| 12 |
+
Architecture: ResNet3D-50 backbone (Hara et al., 2018) initialised
|
| 13 |
+
from Kinetics-700; per-stage max-pool over the z (depth) axis to
|
| 14 |
+
collapse 5-D features to 4-D; small 2-D U-Net decoder; 1x1 conv
|
| 15 |
+
output head producing one sigmoid logit channel.
|
| 16 |
+
"""
|
| 17 |
+
model_type = "inkdetection_resnet3d"
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
in_channels: int = 1,
|
| 22 |
+
input_depth: int = 62,
|
| 23 |
+
input_size: int = 256,
|
| 24 |
+
backbone_depth: int = 50,
|
| 25 |
+
backbone_channels=(256, 512, 1024, 2048),
|
| 26 |
+
num_classes: int = 1,
|
| 27 |
+
decoder_upscale: int = 1,
|
| 28 |
+
# Optional metadata (informational only)
|
| 29 |
+
train_segment: str = "l_2",
|
| 30 |
+
train_inklabels: str = "l_2_inklabels.png",
|
| 31 |
+
train_steps: int = 12396,
|
| 32 |
+
train_tiles: int = 0,
|
| 33 |
+
train_loss_final: float = 0.0,
|
| 34 |
+
**kwargs,
|
| 35 |
+
):
|
| 36 |
+
super().__init__(**kwargs)
|
| 37 |
+
self.in_channels = in_channels
|
| 38 |
+
self.input_depth = input_depth
|
| 39 |
+
self.input_size = input_size
|
| 40 |
+
self.backbone_depth = backbone_depth
|
| 41 |
+
self.backbone_channels = list(backbone_channels)
|
| 42 |
+
self.num_classes = num_classes
|
| 43 |
+
self.decoder_upscale = decoder_upscale
|
| 44 |
+
self.train_segment = train_segment
|
| 45 |
+
self.train_inklabels = train_inklabels
|
| 46 |
+
self.train_steps = train_steps
|
| 47 |
+
self.train_tiles = train_tiles
|
| 48 |
+
self.train_loss_final = train_loss_final
|
last.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:04274e174463a7feecac912abab2ef62844cecbf93e5de25d3f1e2b971938d0b
|
| 3 |
+
size 1008820953
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:75e4009673b2c419c23cb388474c87357c226058598f1e2d7cba4e71d22e0fcb
|
| 3 |
+
size 333531996
|
modeling_inkdetection.py
ADDED
|
@@ -0,0 +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 (intensity already 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")
|
preview_l_2.png
ADDED
|
Git LFS Details
|
preview_l_5.png
ADDED
|
Git LFS Details
|
preview_label.png
ADDED
|