milkzheng commited on
Commit
b2be135
·
verified ·
1 Parent(s): f66b285

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +52 -0
  2. config.json +40 -0
  3. dpt.py +507 -0
  4. hf_model.py +193 -0
  5. model.safetensors +3 -0
README.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ tags:
4
+ - image-segmentation
5
+ - pathology
6
+ - dpt
7
+ pipeline_tag: image-segmentation
8
+ ---
9
+
10
+ # MetSeg — Macro-structure Segmentation (DPT)
11
+
12
+ Pathology macro-structure segmentation. DPT backbone with a custom HF wrapper
13
+ bundled via `trust_remote_code`.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from PIL import Image
19
+ import torch
20
+ from transformers import AutoModel
21
+
22
+ model = AutoModel.from_pretrained("RendeiroLab/MetPredict-lung-structure-segmentation", trust_remote_code=True).eval()
23
+
24
+ # Preprocess: PIL / ndarray / tensor — single image or list/batch.
25
+ # Applies ImageNet normalization (mean/std stored in config.json).
26
+ img = Image.open("tile.png")
27
+ pixel_values = model.preprocess(img) # → (1, 3, H, W) on model device
28
+
29
+ with torch.no_grad():
30
+ out = model(pixel_values)
31
+ logits = out.logits # (1, n_classes, H, W)
32
+ pred = logits.argmax(dim=1) # (1, H, W)
33
+ ```
34
+
35
+ `preprocess` accepts:
36
+ - `PIL.Image` (RGB) — single or list
37
+ - `numpy.ndarray` shape `(H, W, 3)` or `(B, H, W, 3)`, uint8 or float
38
+ - `torch.Tensor` shape `(3, H, W)` or `(B, 3, H, W)`, uint8 or float
39
+
40
+ No resize is applied. H and W must be divisible by the backbone patch size
41
+ (typically 14 or 16). Tile or pad upstream as needed.
42
+
43
+ If you already have a normalized tensor, you can call the model directly with
44
+ `pixel_values=...`.
45
+
46
+ ## Alternative: portable `torch.export`
47
+
48
+ ```python
49
+ import torch
50
+ m = torch.export.load("model.pt2").module()
51
+ y = m(torch.randn(1, 3, 224, 224))
52
+ ```
config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation": null,
3
+ "architectures": [
4
+ "DPTForSegmentation"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "hf_model.DPTConfig",
8
+ "AutoModel": "hf_model.DPTForSegmentation"
9
+ },
10
+ "backbone": "hf-hub:bioptimus/H-optimus-0",
11
+ "class_names": [
12
+ "background",
13
+ "blood vessel",
14
+ "airway"
15
+ ],
16
+ "decoder_fusion_channels": 224,
17
+ "decoder_intermediate_channels": [
18
+ 224,
19
+ 448,
20
+ 896,
21
+ 896
22
+ ],
23
+ "decoder_readout": "cat",
24
+ "dtype": "float32",
25
+ "encoder_depth": 4,
26
+ "image_mean": [
27
+ 0.485,
28
+ 0.456,
29
+ 0.406
30
+ ],
31
+ "image_std": [
32
+ 0.229,
33
+ 0.224,
34
+ 0.225
35
+ ],
36
+ "in_channels": 3,
37
+ "model_type": "metpredict_dpt",
38
+ "n_classes": 3,
39
+ "transformers_version": "5.3.0"
40
+ }
dpt.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # %%
2
+ import warnings
3
+ from typing import Optional, Sequence, Union, Callable, Literal, Any
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+ from segmentation_models_pytorch.base import (
9
+ ClassificationHead,
10
+ SegmentationModel,
11
+ )
12
+ from segmentation_models_pytorch.encoders.timm_vit import TimmViTEncoder
13
+ from segmentation_models_pytorch.base.utils import is_torch_compiling
14
+ from segmentation_models_pytorch.base.hub_mixin import supports_config_loading
15
+ from segmentation_models_pytorch.base.modules import Activation
16
+
17
+
18
+ class ReadoutConcatBlock(nn.Module):
19
+ """
20
+ Concatenates the cls tokens with the features to make use of the global information aggregated in the prefix (cls) tokens.
21
+ Projects the combined feature map to the original embedding dimension using a MLP.
22
+
23
+ According to:
24
+ https://github.com/isl-org/DPT/blob/cd3fe90bb4c48577535cc4d51b602acca688a2ee/dpt/vit.py#L79-L90
25
+ """
26
+
27
+ def __init__(self, embed_dim: int, has_prefix_tokens: bool):
28
+ super().__init__()
29
+ in_features = embed_dim * 2 if has_prefix_tokens else embed_dim
30
+ out_features = embed_dim
31
+ self.project = nn.Sequential(
32
+ nn.Linear(in_features, out_features),
33
+ nn.GELU(),
34
+ )
35
+
36
+ def forward(
37
+ self, features: torch.Tensor, prefix_tokens: Optional[torch.Tensor] = None
38
+ ) -> torch.Tensor:
39
+ batch_size, embed_dim, height, width = features.shape
40
+
41
+ # Rearrange to (batch_size, height * width, embed_dim)
42
+ features = features.view(batch_size, embed_dim, -1)
43
+ features = features.transpose(1, 2).contiguous()
44
+
45
+ if prefix_tokens is not None:
46
+ # (batch_size, num_prefix_tokens, embed_dim) -> (batch_size, 1, embed_dim)
47
+ prefix_tokens = prefix_tokens[:, :1].expand_as(features)
48
+ features = torch.cat([features, prefix_tokens], dim=2)
49
+
50
+ # Project to embedding dimension
51
+ features = self.project(features)
52
+
53
+ # Rearrange back to (batch_size, embed_dim, height, width)
54
+ features = features.transpose(1, 2)
55
+ features = features.view(batch_size, -1, height, width)
56
+
57
+ return features
58
+
59
+
60
+ class ReadoutAddBlock(nn.Module):
61
+ """
62
+ Adds the prefix tokens to the features to make use of the global information aggregated in the prefix (cls) tokens.
63
+
64
+ According to:
65
+ https://github.com/isl-org/DPT/blob/cd3fe90bb4c48577535cc4d51b602acca688a2ee/dpt/vit.py#L71-L76
66
+ """
67
+
68
+ def forward(
69
+ self, features: torch.Tensor, prefix_tokens: Optional[torch.Tensor] = None
70
+ ) -> torch.Tensor:
71
+ if prefix_tokens is not None:
72
+ batch_size, embed_dim, height, width = features.shape
73
+ prefix_tokens = prefix_tokens.mean(dim=1)
74
+ prefix_tokens = prefix_tokens.view(batch_size, embed_dim, 1, 1)
75
+ features = features + prefix_tokens
76
+ return features
77
+
78
+
79
+ class ReadoutIgnoreBlock(nn.Module):
80
+ """
81
+ Ignores the prefix tokens and returns the features as is.
82
+ """
83
+
84
+ def forward(self, features: torch.Tensor, *args, **kwargs) -> torch.Tensor:
85
+ return features
86
+
87
+
88
+ class ReassembleBlock(nn.Module):
89
+ """
90
+ Processes the features such that they have progressively increasing embedding size and progressively decreasing
91
+ spatial dimension
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ in_channels: int,
97
+ mid_channels: int,
98
+ out_channels: int,
99
+ upsample_factor: int,
100
+ ):
101
+ super().__init__()
102
+
103
+ self.project_to_out_channel = nn.Conv2d(
104
+ in_channels=in_channels,
105
+ out_channels=mid_channels,
106
+ kernel_size=1,
107
+ )
108
+
109
+ if upsample_factor > 1.0:
110
+ self.upsample = nn.ConvTranspose2d(
111
+ in_channels=mid_channels,
112
+ out_channels=mid_channels,
113
+ kernel_size=int(upsample_factor),
114
+ stride=int(upsample_factor),
115
+ )
116
+ elif upsample_factor == 1.0:
117
+ self.upsample = nn.Identity()
118
+ else:
119
+ self.upsample = nn.Conv2d(
120
+ in_channels=mid_channels,
121
+ out_channels=mid_channels,
122
+ kernel_size=3,
123
+ stride=int(1 / upsample_factor),
124
+ padding=1,
125
+ )
126
+
127
+ self.project_to_feature_dim = nn.Conv2d(
128
+ in_channels=mid_channels,
129
+ out_channels=out_channels,
130
+ kernel_size=3,
131
+ padding=1,
132
+ bias=False,
133
+ )
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ x = self.project_to_out_channel(x)
137
+ x = self.upsample(x)
138
+ x = self.project_to_feature_dim(x)
139
+ return x
140
+
141
+
142
+ class ResidualConvBlock(nn.Module):
143
+ def __init__(self, feature_dim: int):
144
+ super().__init__()
145
+
146
+ self.conv_1 = nn.Conv2d(
147
+ in_channels=feature_dim,
148
+ out_channels=feature_dim,
149
+ kernel_size=3,
150
+ padding=1,
151
+ bias=False,
152
+ )
153
+ self.batch_norm_1 = nn.BatchNorm2d(num_features=feature_dim)
154
+ self.conv_2 = nn.Conv2d(
155
+ in_channels=feature_dim,
156
+ out_channels=feature_dim,
157
+ kernel_size=3,
158
+ padding=1,
159
+ bias=False,
160
+ )
161
+ self.batch_norm_2 = nn.BatchNorm2d(num_features=feature_dim)
162
+ self.activation = nn.ReLU()
163
+
164
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
165
+ residual = x
166
+
167
+ # Block 1
168
+ x = self.activation(x)
169
+ x = self.conv_1(x)
170
+ x = self.batch_norm_1(x)
171
+
172
+ # Block 2
173
+ x = self.activation(x)
174
+ x = self.conv_2(x)
175
+ x = self.batch_norm_2(x)
176
+
177
+ # Add residual
178
+ x = x + residual
179
+
180
+ return x
181
+
182
+
183
+ class FusionBlock(nn.Module):
184
+ """
185
+ Fuses the processed encoder features in a residual manner and upsamples them
186
+ """
187
+
188
+ def __init__(self, feature_dim: int):
189
+ super().__init__()
190
+ self.residual_conv_block1 = ResidualConvBlock(feature_dim)
191
+ self.residual_conv_block2 = ResidualConvBlock(feature_dim)
192
+ self.project = nn.Conv2d(feature_dim, feature_dim, kernel_size=1)
193
+ self.activation = nn.ReLU()
194
+
195
+ def forward(
196
+ self,
197
+ feature: torch.Tensor,
198
+ previous_feature: Optional[torch.Tensor] = None,
199
+ ) -> torch.Tensor:
200
+ feature = self.residual_conv_block1(feature)
201
+ if previous_feature is not None:
202
+ feature = feature + previous_feature
203
+ feature = self.residual_conv_block2(feature)
204
+ feature = nn.functional.interpolate(
205
+ feature, scale_factor=2, align_corners=True, mode="bilinear"
206
+ )
207
+ feature = self.project(feature)
208
+ return feature
209
+
210
+
211
+ class DPTDecoder(nn.Module):
212
+ """
213
+ Decoder part for DPT
214
+
215
+ Processes the encoder features and class tokens (if encoder has class_tokens) to have spatial downsampling ratios of
216
+ [1/4, 1/8, 1/16, 1/32, ...] relative to the input image spatial dimension.
217
+
218
+ The decoder then fuses these features in a residual manner and progressively upsamples them by a factor of 2 so that the
219
+ output has a downsampling ratio of 1/2 relative to the input image spatial dimension
220
+
221
+ """
222
+
223
+ def __init__(
224
+ self,
225
+ encoder_out_channels: Sequence[int] = (756, 756, 756, 756),
226
+ encoder_output_strides: Sequence[int] = (16, 16, 16, 16),
227
+ encoder_has_prefix_tokens: bool = True,
228
+ readout: Literal["cat", "add", "ignore"] = "cat",
229
+ intermediate_channels: Sequence[int] = (256, 512, 1024, 1024),
230
+ fusion_channels: int = 256,
231
+ ):
232
+ super().__init__()
233
+
234
+ if not (
235
+ len(encoder_out_channels)
236
+ == len(encoder_output_strides)
237
+ == len(intermediate_channels)
238
+ ):
239
+ raise ValueError(
240
+ "encoder_out_channels, encoder_output_strides and intermediate_channels must have the same length"
241
+ )
242
+
243
+ num_blocks = len(encoder_out_channels)
244
+
245
+ # If encoder has prefix tokens (e.g. cls_token), then we can concat/add/ignore them
246
+ # according to the readout mode
247
+ if readout == "cat":
248
+ blocks = [
249
+ ReadoutConcatBlock(in_channels, encoder_has_prefix_tokens)
250
+ for in_channels in encoder_out_channels
251
+ ]
252
+ elif readout == "add":
253
+ blocks = [ReadoutAddBlock() for _ in encoder_out_channels]
254
+ elif readout == "ignore":
255
+ blocks = [ReadoutIgnoreBlock() for _ in encoder_out_channels]
256
+ else:
257
+ raise ValueError(
258
+ f"Invalid readout mode: {readout}, should be one of: 'cat', 'add', 'ignore'"
259
+ )
260
+ self.projection_blocks = nn.ModuleList(blocks)
261
+
262
+ # Upsample factors to resize features to progressively smaller scales
263
+ # For ViT models where all layers have the same stride, we create multi-scale features
264
+ # by progressively downsampling from the encoder output
265
+ scale_factors = []
266
+ min_stride = min(encoder_output_strides)
267
+ for i, stride in enumerate(encoder_output_strides):
268
+ # Progressive downsampling: i=0 keeps original, i=1 halves, i=2 quarters, etc.
269
+ target_stride = min_stride * (2 ** i)
270
+ scale_factor = stride / target_stride
271
+ scale_factors.append(scale_factor)
272
+ self.reassemble_blocks = nn.ModuleList()
273
+ for i in range(num_blocks):
274
+ block = ReassembleBlock(
275
+ in_channels=encoder_out_channels[i],
276
+ mid_channels=intermediate_channels[i],
277
+ out_channels=fusion_channels,
278
+ upsample_factor=scale_factors[i],
279
+ )
280
+ self.reassemble_blocks.append(block)
281
+
282
+ # Fusion blocks to fuse the processed features in a sequential manner
283
+ fusion_blocks = [FusionBlock(fusion_channels) for _ in range(num_blocks)]
284
+ self.fusion_blocks = nn.ModuleList(fusion_blocks)
285
+
286
+ def forward(
287
+ self, features: list[torch.Tensor], prefix_tokens: list[Optional[torch.Tensor]]
288
+ ) -> torch.Tensor:
289
+ # Process the encoder features to scale of [1/4, 1/8, 1/16, 1/32, ...]
290
+ processed_features = []
291
+ for i, (feature, prefix_tokens_i) in enumerate(zip(features, prefix_tokens)):
292
+ projected_feature = self.projection_blocks[i](feature, prefix_tokens_i)
293
+ processed_feature = self.reassemble_blocks[i](projected_feature)
294
+ processed_features.append(processed_feature)
295
+
296
+ # Fusion and progressive upsampling starting from the last processed feature
297
+ processed_features = processed_features[::-1]
298
+ fused_feature = None
299
+ for fusion_block, feature in zip(self.fusion_blocks, processed_features):
300
+ fused_feature = fusion_block(feature, fused_feature)
301
+
302
+ return fused_feature
303
+
304
+
305
+ class DPTSegmentationHead(nn.Module):
306
+ def __init__(
307
+ self,
308
+ in_channels: int,
309
+ out_channels: int,
310
+ activation: Optional[Union[str, Callable]] = None,
311
+ kernel_size: int = 3,
312
+ upsampling: float = 2.0,
313
+ ):
314
+ super().__init__()
315
+
316
+ self.head = nn.Sequential(
317
+ nn.Conv2d(
318
+ in_channels, in_channels, kernel_size=kernel_size, padding=1, bias=False
319
+ ),
320
+ nn.BatchNorm2d(in_channels),
321
+ nn.ReLU(inplace=True),
322
+ nn.Dropout(p=0.1, inplace=False),
323
+ nn.Conv2d(in_channels, out_channels, kernel_size=1),
324
+ )
325
+ self.activation = Activation(activation)
326
+ self.upsampling_factor = upsampling
327
+
328
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
329
+ head_output = self.head(x)
330
+ resized_output = nn.functional.interpolate(
331
+ head_output,
332
+ scale_factor=self.upsampling_factor,
333
+ mode="bilinear",
334
+ align_corners=True,
335
+ )
336
+ activation_output = self.activation(resized_output)
337
+ return activation_output
338
+
339
+
340
+ class DPT(SegmentationModel):
341
+ """
342
+ DPT is a dense prediction architecture that leverages vision transformers in place of convolutional networks as
343
+ a backbone for dense prediction tasks
344
+
345
+ It assembles tokens from various stages of the vision transformer into image-like representations at various resolutions
346
+ and progressively combines them into full-resolution predictions using a convolutional decoder.
347
+
348
+ The transformer backbone processes representations at a constant and relatively high resolution and has a global receptive
349
+ field at every stage. These properties allow the dense vision transformer to provide finer-grained and more globally coherent
350
+ predictions when compared to fully-convolutional networks
351
+
352
+ Note:
353
+ Since this model uses a Vision Transformer backbone, it typically requires a fixed input image size.
354
+ To handle variable input sizes, you can set `dynamic_img_size=True` in the model initialization
355
+ (if supported by the specific `timm` encoder). You can check if an encoder requires fixed size
356
+ using `model.encoder.is_fixed_input_size`, and get the required input dimensions from
357
+ `model.encoder.input_size`, however it's no guarantee that information is available.
358
+
359
+ Args:
360
+ encoder_name: Name of the classification model that will be used as an encoder (a.k.a backbone)
361
+ to extract features of different spatial resolution.
362
+ encoder_depth: A number of stages used in encoder in range [1,4]. Each stage generate features
363
+ smaller by a factor equal to the ViT model patch_size in spatial dimensions.
364
+ Default is 4.
365
+ encoder_weights: One of **None** (random initialization), or not **None** (pretrained weights would be loaded
366
+ with respect to the encoder_name, e.g. for ``"tu-vit_base_patch16_224.augreg_in21k"`` - ``"augreg_in21k"``
367
+ weights would be loaded).
368
+ encoder_output_indices: The indices of the encoder output features to use. If **None** will be sampled uniformly
369
+ across the number of blocks in encoder, e.g. if number of blocks is 4 and encoder has 20 blocks, then
370
+ encoder_output_indices will be (4, 9, 14, 19). If specified the number of indices should be equal to
371
+ encoder_depth. Default is **None**.
372
+ decoder_readout: The strategy to utilize the prefix tokens (e.g. cls_token) from the encoder.
373
+ Can be one of **"cat"**, **"add"**, or **"ignore"**. Default is **"cat"**.
374
+ decoder_intermediate_channels: The number of channels for the intermediate decoder layers. Reduce if you
375
+ want to reduce the number of parameters in the decoder. Default is (256, 512, 1024, 1024).
376
+ decoder_fusion_channels: The latent dimension to which the encoder features will be projected to before fusion.
377
+ Default is 256.
378
+ in_channels: Number of input channels for the model, default is 3 (RGB images)
379
+ classes: Number of classes for output mask (or you can think as a number of channels of output mask)
380
+ activation: An activation function to apply after the final convolution layer.
381
+ Available options are **"sigmoid"**, **"softmax"**, **"logsoftmax"**, **"tanh"**, **"identity"**,
382
+ **callable** and **None**. Default is **None**.
383
+ aux_params: Dictionary with parameters of the auxiliary output (classification head). Auxiliary output is build
384
+ on top of encoder if **aux_params** is not **None** (default). Supported params:
385
+
386
+ - **classes** (*int*): A number of classes;
387
+ - **pooling** (*str*): One of "max", "avg". Default is "avg";
388
+ - **dropout** (*float*): Dropout factor in [0, 1);
389
+ - **activation** (*str*): An activation function to apply "sigmoid"/"softmax" (could be **None** to return logits).
390
+ kwargs: Arguments passed to the encoder class ``__init__()`` function. Applies only to ``timm`` models. Keys with
391
+ ``None`` values are pruned before passing. Specify ``dynamic_img_size=True`` to allow the model to handle images of different sizes.
392
+
393
+ Returns:
394
+ ``torch.nn.Module``: DPT
395
+
396
+ """
397
+
398
+ # fails for encoders with prefix tokens
399
+ _is_torch_scriptable = False
400
+ _is_torch_compilable = True
401
+ requires_divisible_input_shape = True
402
+
403
+ @supports_config_loading
404
+ def __init__(
405
+ self,
406
+ encoder_name: str = "hf-hub:paige-ai/Virchow2",
407
+ encoder_depth: int = 4,
408
+ encoder_output_indices: Optional[list[int]] = None,
409
+ decoder_readout: Literal["ignore", "add", "cat"] = "cat",
410
+ decoder_intermediate_channels: Sequence[int] = (224, 448, 896, 896),
411
+ decoder_fusion_channels: int = 224,
412
+ in_channels: int = 3,
413
+ classes: int = 1,
414
+ activation: Optional[Union[str, Callable]] = None,
415
+ aux_params: Optional[dict] = None,
416
+ **kwargs: dict[str, Any],
417
+ ):
418
+ super().__init__()
419
+
420
+ if decoder_readout not in ["ignore", "add", "cat"]:
421
+ raise ValueError(
422
+ f"Invalid decoder readout mode. Must be one of: 'ignore', 'add', 'cat'. Got: {decoder_readout}"
423
+ )
424
+
425
+ from timm.layers import SwiGLUPacked
426
+
427
+ self.encoder = TimmViTEncoder(
428
+ name=encoder_name,
429
+ mlp_layer=SwiGLUPacked,
430
+ act_layer=torch.nn.SiLU,
431
+ in_channels=in_channels,
432
+ depth=encoder_depth,
433
+ output_indices=encoder_output_indices,
434
+ **kwargs,
435
+ )
436
+
437
+ if not self.encoder.has_prefix_tokens and decoder_readout != "ignore":
438
+ warnings.warn(
439
+ f"Encoder does not have prefix tokens (e.g. cls_token), but `decoder_readout` is set to '{decoder_readout}'. "
440
+ f"It's recommended to set `decoder_readout='ignore'` when using a encoder without prefix tokens.",
441
+ UserWarning,
442
+ )
443
+
444
+ self.decoder = DPTDecoder(
445
+ encoder_out_channels=self.encoder.out_channels,
446
+ encoder_output_strides=self.encoder.output_strides,
447
+ encoder_has_prefix_tokens=self.encoder.has_prefix_tokens,
448
+ readout=decoder_readout,
449
+ intermediate_channels=decoder_intermediate_channels,
450
+ fusion_channels=decoder_fusion_channels,
451
+ )
452
+
453
+ # Calculate required upsampling for segmentation head
454
+ # Decoder output spatial size: encoder_stride / (2^num_fusion_blocks)
455
+ # For ViT with stride 14 and 4 fusion blocks: 16 / (2^2) = 4, times 2 for each fusion = 32
456
+ # Actually: encoder_spatial = 224 / encoder_stride
457
+ # decoder_output_spatial = encoder_spatial / (2^(num_fusion_blocks-1))
458
+ # But we want output to match input, so:
459
+ # segmentation_upsampling = encoder_stride / 2 (to match input size)
460
+ # However, simpler: just use encoder stride as the upsampling factor
461
+ encoder_stride = max(self.encoder.output_strides) if self.encoder.output_strides else 14
462
+ seg_head_upsampling = float(encoder_stride / 2) # Upsample by encoder_stride/2 to reach input resolution
463
+
464
+ self.segmentation_head = DPTSegmentationHead(
465
+ in_channels=decoder_fusion_channels,
466
+ out_channels=classes,
467
+ activation=activation,
468
+ kernel_size=3,
469
+ upsampling=seg_head_upsampling,
470
+ )
471
+
472
+ if aux_params is not None:
473
+ self.classification_head = ClassificationHead(
474
+ in_channels=self.encoder.out_channels[-1], **aux_params
475
+ )
476
+ else:
477
+ self.classification_head = None
478
+
479
+ self.name = f"dpt-{encoder_name.split('/')[-1]}"
480
+ self.initialize()
481
+
482
+ def forward(self, x):
483
+ """Sequentially pass `x` trough model`s encoder, decoder and heads"""
484
+
485
+ if not (
486
+ torch.jit.is_scripting() or torch.jit.is_tracing() or is_torch_compiling()
487
+ ):
488
+ self.check_input_shape(x)
489
+
490
+ features, prefix_tokens = self.encoder(x)
491
+ decoder_output = self.decoder(features, prefix_tokens)
492
+ masks = self.segmentation_head(decoder_output)
493
+ # Ensure contiguous memory layout for DDP compatibility
494
+ masks = masks.contiguous()
495
+
496
+ if self.classification_head is not None:
497
+ labels = self.classification_head(features[-1])
498
+ return masks, labels
499
+
500
+ return masks
501
+
502
+
503
+ if __name__ == "__main__":
504
+ model = DPT()
505
+ img = torch.randn(1, 3, 224, 224)
506
+ out = model(img)
507
+ print(out.shape) # torch.Size([1, 1, 224, 224])
hf_model.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace `PreTrainedModel` + `PretrainedConfig` wrapper around `DPT`.
2
+
3
+ Lets consumers do `AutoModel.from_pretrained(repo_id, trust_remote_code=True)`
4
+ without importing the local `DPT` class. The `auto_map` field on the config
5
+ tells HF to bundle `hf_model.py` + `dpt.py` with the uploaded weights so the
6
+ classes are reconstructable in a clean env.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Literal, Optional, Sequence, Union, cast
11
+
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn.functional as F
15
+ from transformers import (
16
+ AutoConfig,
17
+ AutoModel,
18
+ PretrainedConfig,
19
+ PreTrainedModel,
20
+ )
21
+ from transformers.modeling_outputs import SemanticSegmenterOutput
22
+
23
+ from .dpt import DPT
24
+
25
+ # ImageNet stats — matches the `A.Normalize()` default used at training time
26
+ # (correct for Virchow2 / H-optimus-0 backbones).
27
+ _IMAGENET_MEAN = (0.485, 0.456, 0.406)
28
+ _IMAGENET_STD = (0.229, 0.224, 0.225)
29
+
30
+
31
+ class DPTConfig(PretrainedConfig):
32
+ model_type = "metpredict_dpt"
33
+
34
+ def __init__(
35
+ self,
36
+ n_classes: int = 4,
37
+ class_names: Optional[list[str]] = None,
38
+ backbone: str = "hf-hub:bioptimus/H-optimus-0",
39
+ encoder_depth: int = 4,
40
+ decoder_intermediate_channels: tuple[int, ...] = (224, 448, 896, 896),
41
+ decoder_fusion_channels: int = 224,
42
+ decoder_readout: str = "cat",
43
+ activation: Optional[str] = None,
44
+ in_channels: int = 3,
45
+ image_mean: Sequence[float] = _IMAGENET_MEAN,
46
+ image_std: Sequence[float] = _IMAGENET_STD,
47
+ **kwargs,
48
+ ):
49
+ super().__init__(**kwargs)
50
+ self.n_classes = n_classes
51
+ self.class_names = list(class_names) if class_names else []
52
+ self.backbone = backbone
53
+ self.encoder_depth = encoder_depth
54
+ self.decoder_intermediate_channels = list(decoder_intermediate_channels)
55
+ self.decoder_fusion_channels = decoder_fusion_channels
56
+ self.decoder_readout = decoder_readout
57
+ self.activation = activation
58
+ self.in_channels = in_channels
59
+ self.image_mean = list(image_mean)
60
+ self.image_std = list(image_std)
61
+ # `auto_map` makes the repo loadable as AutoModel without local imports.
62
+ self.auto_map = {
63
+ "AutoConfig": "hf_model.DPTConfig",
64
+ "AutoModel": "hf_model.DPTForSegmentation",
65
+ }
66
+
67
+
68
+ class DPTForSegmentation(PreTrainedModel):
69
+ config_class = DPTConfig
70
+ base_model_prefix = "dpt"
71
+ main_input_name = "pixel_values"
72
+
73
+ def __init__(self, config: DPTConfig):
74
+ super().__init__(config)
75
+ # `decoder_readout` is Literal-typed in DPT — cast since pydantic-loaded
76
+ # value is a plain str.
77
+ readout = cast(Literal["ignore", "add", "cat"], config.decoder_readout)
78
+ dpt_kwargs: dict[str, Any] = dict(
79
+ encoder_name=config.backbone,
80
+ encoder_depth=config.encoder_depth,
81
+ decoder_readout=readout,
82
+ decoder_intermediate_channels=tuple(config.decoder_intermediate_channels),
83
+ decoder_fusion_channels=config.decoder_fusion_channels,
84
+ in_channels=config.in_channels,
85
+ classes=config.n_classes,
86
+ activation=config.activation,
87
+ )
88
+ self.dpt = DPT(**dpt_kwargs)
89
+ # Normalize stats kept as buffers so they move with `.to(device)` and
90
+ # show up in `state_dict` for inspection but are excluded from grads.
91
+ self.register_buffer(
92
+ "image_mean",
93
+ torch.tensor(config.image_mean, dtype=torch.float32).view(1, -1, 1, 1),
94
+ persistent=False,
95
+ )
96
+ self.register_buffer(
97
+ "image_std",
98
+ torch.tensor(config.image_std, dtype=torch.float32).view(1, -1, 1, 1),
99
+ persistent=False,
100
+ )
101
+ # Skip post_init weight init — DPT.initialize() already ran inside DPT.__init__.
102
+
103
+ @torch.no_grad()
104
+ def preprocess(
105
+ self,
106
+ images: Union[
107
+ "PIL.Image.Image", # noqa: F821
108
+ np.ndarray,
109
+ torch.Tensor,
110
+ list,
111
+ ],
112
+ ) -> torch.Tensor:
113
+ """Convert raw inputs into a model-ready ``pixel_values`` tensor.
114
+
115
+ Accepts a single image or a batch, in any of:
116
+ - PIL.Image (RGB)
117
+ - numpy.ndarray, shape (H, W, 3) or (B, H, W, 3), dtype uint8 or float
118
+ - torch.Tensor, shape (3, H, W) or (B, 3, H, W), dtype uint8 or float
119
+
120
+ Returns a float tensor of shape ``(B, 3, H, W)`` normalized with the
121
+ ImageNet stats used during training, on the model's current device.
122
+
123
+ Notes:
124
+ - uint8 inputs are scaled to [0, 1] before normalization.
125
+ - No resize is applied: the DPT is fully convolutional but the H and W
126
+ must be divisible by the backbone's patch size (typically 14 or 16).
127
+ Tile/pad upstream as needed.
128
+ """
129
+ if isinstance(images, list):
130
+ tensors = [self._to_chw_float(x) for x in images]
131
+ batch = torch.stack(tensors, dim=0)
132
+ else:
133
+ t = self._to_chw_float(images)
134
+ batch = t if t.ndim == 4 else t.unsqueeze(0)
135
+ batch = batch.to(self.image_mean.device, dtype=torch.float32)
136
+ return (batch - self.image_mean) / self.image_std
137
+
138
+ @staticmethod
139
+ def _to_chw_float(x: Any) -> torch.Tensor:
140
+ """Convert a single image-like input to a CHW float tensor in [0, 1]."""
141
+ # Lazy import: PIL is optional at inference time.
142
+ try:
143
+ from PIL import Image as _PILImage
144
+ except ImportError: # pragma: no cover
145
+ _PILImage = None # type: ignore[assignment]
146
+ if _PILImage is not None and isinstance(x, _PILImage.Image):
147
+ arr = np.asarray(x.convert("RGB")) # HWC uint8
148
+ t = torch.from_numpy(arr).permute(2, 0, 1).contiguous()
149
+ elif isinstance(x, np.ndarray):
150
+ t = torch.from_numpy(x)
151
+ if t.ndim == 3 and t.shape[-1] in (1, 3):
152
+ t = t.permute(2, 0, 1).contiguous()
153
+ elif t.ndim == 4 and t.shape[-1] in (1, 3):
154
+ t = t.permute(0, 3, 1, 2).contiguous()
155
+ elif isinstance(x, torch.Tensor):
156
+ t = x
157
+ else:
158
+ raise TypeError(f"Unsupported image type: {type(x).__name__}")
159
+ if t.dtype == torch.uint8:
160
+ t = t.float() / 255.0
161
+ else:
162
+ t = t.float()
163
+ return t
164
+
165
+ def forward(
166
+ self,
167
+ pixel_values: torch.Tensor,
168
+ labels: Optional[torch.Tensor] = None,
169
+ return_dict: bool = True,
170
+ ):
171
+ logits = self.dpt(pixel_values)
172
+ loss: Optional[torch.Tensor] = None
173
+ if labels is not None:
174
+ loss = F.cross_entropy(logits, labels.long())
175
+ if not return_dict:
176
+ return (loss, logits) if loss is not None else (logits,)
177
+ # SemanticSegmenterOutput expects FloatTensor — cast suppresses Pylance.
178
+ return SemanticSegmenterOutput(
179
+ loss=cast(Any, loss),
180
+ logits=cast(Any, logits),
181
+ )
182
+
183
+
184
+ def _register() -> None:
185
+ try:
186
+ AutoConfig.register("metpredict_dpt", DPTConfig)
187
+ AutoModel.register(DPTConfig, DPTForSegmentation)
188
+ except ValueError:
189
+ # Already registered (re-import).
190
+ pass
191
+
192
+
193
+ _register()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c7134fc04f842b056ebe5d4abe9113acaea76449642521549d2e6d4d8a3c49c
3
+ size 4746335316