YoussefMoNader commited on
Commit
9db19e4
·
verified ·
1 Parent(s): 9531054

Initial commit: weights + custom modeling

Browse files
.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
+ | ![label](./preview_label.png) | ![l_2 pred](./preview_l_2.png) | ![l_5 pred](./preview_l_5.png) |
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

  • SHA256: 94ae6e1d049302f258f19102fc20b5a6988748c1274933d46c6f905b4be37802
  • Pointer size: 131 Bytes
  • Size of remote file: 980 kB
preview_l_5.png ADDED

Git LFS Details

  • SHA256: 83e7fe7e328ef83eb59762c9c101df582db66d9bc5626faae0fce6ba4170b2ae
  • Pointer size: 132 Bytes
  • Size of remote file: 3.4 MB
preview_label.png ADDED