syntx 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syntx/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ from .syn import registration, SyNTo
2
+ from .syn_jax import SyNTo as SyNToJax
3
+ from .transform import SyNToTransform
4
+ from .features import FeatureSpaceLoss, VGG19Extractor, DINOv2Extractor, ResNet10Extractor, SwinUNETRExtractor
5
+
6
+ # Expose both syn and registration to satisfy user requirements
7
+ syn = registration
8
+
9
+ __version__ = "0.1.7"
10
+
11
+
12
+ __all__ = [
13
+ "syn",
14
+ "registration",
15
+ "SyNTo",
16
+ "SyNToJax",
17
+ "SyNToTransform",
18
+ "FeatureSpaceLoss",
19
+ "VGG19Extractor",
20
+ "DINOv2Extractor",
21
+ "ResNet10Extractor",
22
+ "SwinUNETRExtractor",
23
+ ]
syntx/features.py ADDED
@@ -0,0 +1,473 @@
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import numpy as np
6
+
7
+ from .resnet import resnet10_2d, resnet10_3d
8
+
9
+ class FeatureExtractor(nn.Module):
10
+ """Abstract base for all feature extractors."""
11
+ @property
12
+ def is_3d(self) -> bool:
13
+ raise NotImplementedError
14
+
15
+ @property
16
+ def in_channels(self) -> int:
17
+ raise NotImplementedError
18
+
19
+ def normalize(self, x: torch.Tensor) -> torch.Tensor:
20
+ raise NotImplementedError
21
+
22
+ def extract(self, x: torch.Tensor) -> list:
23
+ raise NotImplementedError
24
+
25
+
26
+ class VGG19Extractor(FeatureExtractor):
27
+ """VGG19 feature extractor with memory truncation."""
28
+ is_3d = False
29
+ in_channels = 3
30
+
31
+ def __init__(self, feature_layers=[8]):
32
+ super().__init__()
33
+ import torchvision.models as models
34
+ vgg = models.vgg19(weights=models.VGG19_Weights.DEFAULT).features
35
+
36
+ # Discard layers beyond the max needed layer to save memory
37
+ max_layer = max(feature_layers)
38
+ self.layers = nn.ModuleList([vgg[i] for i in range(max_layer + 1)])
39
+ self.feature_layers = feature_layers
40
+
41
+ for m in self.layers.modules():
42
+ if isinstance(m, nn.ReLU):
43
+ m.inplace = False
44
+ for p in self.parameters():
45
+ p.requires_grad = False
46
+ self.eval()
47
+
48
+ self.register_buffer("mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
49
+ self.register_buffer("std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
50
+
51
+ def normalize(self, x):
52
+ return (x - self.mean.to(x)) / self.std.to(x)
53
+
54
+ def extract(self, x):
55
+ features = []
56
+ for i, layer in enumerate(self.layers):
57
+ x = layer(x)
58
+ if i in self.feature_layers:
59
+ features.append(x)
60
+ return features
61
+
62
+
63
+ class DINOv2Extractor(FeatureExtractor):
64
+ """DINOv2 feature extractor supporting both ViT-S and ViT-B with sub-network pruning."""
65
+ is_3d = False
66
+ in_channels = 3
67
+
68
+ def __init__(self, version='vits14', feature_layers=[11]):
69
+ super().__init__()
70
+ model_name = f'dinov2_{version}'
71
+ # Load from torch hub
72
+ self.model = torch.hub.load('facebookresearch/dinov2', model_name)
73
+ self.patch_size = 14
74
+ self.feature_layers = feature_layers
75
+
76
+ # Extract only the needed transformer blocks to save memory
77
+ max_layer = max(feature_layers)
78
+ self.model.blocks = nn.ModuleList([self.model.blocks[i] for i in range(max_layer + 1)])
79
+
80
+ for p in self.model.parameters():
81
+ p.requires_grad = False
82
+ self.model.eval()
83
+ self.eval()
84
+
85
+ self.register_buffer("mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
86
+ self.register_buffer("std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
87
+
88
+ def normalize(self, x):
89
+ return (x - self.mean.to(x)) / self.std.to(x)
90
+
91
+ def extract(self, x):
92
+ B, C, H, W = x.shape
93
+ # Pad to patch_size-divisible dimensions
94
+ ph = (self.patch_size - H % self.patch_size) % self.patch_size
95
+ pw = (self.patch_size - W % self.patch_size) % self.patch_size
96
+ if ph > 0 or pw > 0:
97
+ x = F.pad(x, (0, pw, 0, ph))
98
+
99
+ # We step through the model blocks to collect intermediate features
100
+ # Matching DINOv2's forward pass but stopping at the max block
101
+ x_tokens = self.model.prepare_tokens_with_masks(x)
102
+
103
+ features = []
104
+ for i, blk in enumerate(self.model.blocks):
105
+ x_tokens = blk(x_tokens)
106
+ if i in self.feature_layers:
107
+ # Patch tokens shape: (B, H_p * W_p, C_feat)
108
+ patch_tokens = x_tokens[:, 1:] # skip class token
109
+ hp = (H + ph) // self.patch_size
110
+ wp = (W + pw) // self.patch_size
111
+ feat_grid = patch_tokens.reshape(B, hp, wp, -1).permute(0, 3, 1, 2)
112
+ features.append(feat_grid)
113
+ return features
114
+
115
+
116
+ class ResNet10Extractor(FeatureExtractor):
117
+ """Unified ResNet-10 extractor supporting 2D or 3D."""
118
+ def __init__(self, dim=3, feature_layers=[4]):
119
+ super().__init__()
120
+ self._is_3d = (dim == 3)
121
+ self.feature_layers = feature_layers
122
+
123
+ if self._is_3d:
124
+ self.model = resnet10_3d()
125
+ self._in_channels = 1
126
+ # Try to load MedicalNet weights if available
127
+ weights_path = os.path.expanduser("~/.syntx_cache/resnet_10_23iseg.pth")
128
+ if os.path.exists(weights_path):
129
+ state = torch.load(weights_path, map_location='cpu')
130
+ self.model.load_state_dict(state.get('state_dict', state), strict=False)
131
+ else:
132
+ self.model = resnet10_2d()
133
+ self._in_channels = 1
134
+
135
+ for p in self.model.parameters():
136
+ p.requires_grad = False
137
+ self.model.eval()
138
+ self.eval()
139
+
140
+ @property
141
+ def is_3d(self) -> bool:
142
+ return self._is_3d
143
+
144
+ @property
145
+ def in_channels(self) -> int:
146
+ return self._in_channels
147
+
148
+ def normalize(self, x):
149
+ # Grayscale volumes are already in [0, 1]
150
+ return x
151
+
152
+ def extract(self, x):
153
+ # Extract features at layers
154
+ out = self.model.relu(self.model.bn1(self.model.conv1(x)))
155
+ out = self.model.maxpool(out)
156
+
157
+ features = []
158
+ # Layer 1
159
+ out = self.model.layer1(out)
160
+ if 1 in self.feature_layers:
161
+ features.append(out)
162
+ # Layer 2
163
+ out = self.model.layer2(out)
164
+ if 2 in self.feature_layers:
165
+ features.append(out)
166
+ # Layer 3
167
+ out = self.model.layer3(out)
168
+ if 3 in self.feature_layers:
169
+ features.append(out)
170
+ # Layer 4
171
+ out = self.model.layer4(out)
172
+ if 4 in self.feature_layers:
173
+ features.append(out)
174
+
175
+ return features
176
+
177
+
178
+ class SwinUNETRExtractor(FeatureExtractor):
179
+ """SwinUNETR 3D self-supervised encoder feature extractor with lazy loading and dynamic size resizing."""
180
+ is_3d = True
181
+ in_channels = 1
182
+
183
+ def __init__(self, feature_layers=[4], weights_path=None, img_size=(96, 96, 96)):
184
+ super().__init__()
185
+ # Lazy import to avoid hard dependency on MONAI
186
+ try:
187
+ from monai.networks.nets import SwinUNETR
188
+ except ImportError:
189
+ raise ImportError(
190
+ "MONAI is required to use SwinUNETRExtractor. "
191
+ "Please install it using 'pip install monai'."
192
+ )
193
+
194
+ if not feature_layers:
195
+ raise ValueError("feature_layers cannot be empty.")
196
+ for layer in feature_layers:
197
+ if layer not in [1, 2, 3, 4]:
198
+ raise ValueError("Invalid layer index. SwinUNETR layers must be in [1, 2, 3, 4].")
199
+
200
+ self.feature_layers = feature_layers
201
+ if isinstance(img_size, int):
202
+ self.img_size = (img_size, img_size, img_size)
203
+ else:
204
+ self.img_size = tuple(img_size)
205
+
206
+ # Default SwinUNETR configuration for pre-trained weights
207
+ self.model = SwinUNETR(
208
+ in_channels=self.in_channels,
209
+ out_channels=14, # default out channels in SSL pretrained zoo
210
+ feature_size=48,
211
+ spatial_dims=3
212
+ )
213
+
214
+ if weights_path != "random":
215
+ if weights_path is None:
216
+ weights_path = os.path.expanduser("~/.syntx_cache/model_swinvit.pt")
217
+
218
+ if not os.path.exists(weights_path):
219
+ url = "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/model_swinvit.pt"
220
+ try:
221
+ os.makedirs(os.path.dirname(weights_path), exist_ok=True)
222
+ temp_path = weights_path + ".tmp"
223
+ import urllib.request
224
+ urllib.request.urlretrieve(url, temp_path)
225
+ os.rename(temp_path, weights_path)
226
+ except Exception as e:
227
+ import warnings
228
+ warnings.warn(
229
+ f"Failed to download Swin ViT weights from MONAI zoo: {e}. "
230
+ f"If you are in an offline or restricted network environment, "
231
+ f"please manually download the weights from {url} and place them at '{weights_path}'."
232
+ )
233
+
234
+ if os.path.exists(weights_path):
235
+ # Load checkpoint, strip nested keys, and load into backbone
236
+ state = torch.load(weights_path, map_location='cpu')
237
+ state_dict = state.get('state_dict', state)
238
+
239
+ swinvit_state_dict = {}
240
+ for k, v in state_dict.items():
241
+ if k.startswith("module."):
242
+ k = k[7:]
243
+ if k.startswith("swinViT."):
244
+ k = k[8:]
245
+ swinvit_state_dict[k] = v
246
+
247
+ self.model.swinViT.load_state_dict(swinvit_state_dict, strict=False)
248
+
249
+ # Freeze model parameters for extraction
250
+ for p in self.model.parameters():
251
+ p.requires_grad = False
252
+ self.model.eval()
253
+
254
+ def normalize(self, x: torch.Tensor) -> torch.Tensor:
255
+ # Grayscale volumes are already scaled.
256
+ return x
257
+
258
+ def extract(self, x: torch.Tensor) -> list:
259
+ if x.shape[0] == 0:
260
+ raise ValueError("Batch size cannot be 0")
261
+ if len(x.shape) != 5:
262
+ raise ValueError("Input must be a 5D tensor (B, C, D, H, W)")
263
+
264
+ import math
265
+ spatial_shape = x.shape[2:]
266
+
267
+ # Compute padding size (multiple of 32, at least 32)
268
+ pad_size = [int(math.ceil(s / 32.0) * 32) for s in spatial_shape]
269
+
270
+ pad_d = pad_size[0] - spatial_shape[0]
271
+ pad_h = pad_size[1] - spatial_shape[1]
272
+ pad_w = pad_size[2] - spatial_shape[2]
273
+
274
+ # Pad input volume to this size (constant zero padding at the end of dimensions)
275
+ x_input = F.pad(x, (0, pad_w, 0, pad_h, 0, pad_d), mode='constant', value=0.0)
276
+
277
+ hidden_states = self.model.swinViT(x_input)
278
+ features = []
279
+ for layer in self.feature_layers:
280
+ if len(hidden_states) == 5:
281
+ feat = hidden_states[layer]
282
+ else:
283
+ feat = hidden_states[layer - 1]
284
+
285
+ # Crop the padded feature map back to expected_shape
286
+ downsample_factor = 2 ** (layer + 1)
287
+ expected_shape = [max(1, s // downsample_factor) for s in spatial_shape]
288
+ feat = feat[:, :, :expected_shape[0], :expected_shape[1], :expected_shape[2]]
289
+
290
+ features.append(feat)
291
+
292
+ return features
293
+
294
+
295
+ class FeatureSpaceLoss(nn.Module):
296
+ """Dimension-agnostic loss using modular feature extractors."""
297
+ def __init__(self, extractor: FeatureExtractor, mode='lncc_3d', num_slices=4, lncc_window=9):
298
+ super().__init__()
299
+ self.extractor = extractor
300
+ self.mode = mode
301
+ self.num_slices = num_slices
302
+ self.lncc_window = lncc_window
303
+
304
+ def forward(self, input_nd, target_nd):
305
+ dim = len(input_nd.shape) - 2
306
+
307
+ if self.extractor.is_3d:
308
+ if dim == 2:
309
+ raise ValueError("Cannot run 3D feature extractor on 2D input.")
310
+ return self._forward_3d(input_nd, target_nd)
311
+ else:
312
+ if dim == 2:
313
+ return self._forward_2d_direct(input_nd, target_nd)
314
+ else:
315
+ if self.mode == 'lncc_3d':
316
+ return self._forward_2d_reconstruct_3d(input_nd, target_nd)
317
+ else:
318
+ return self._forward_2d_triplanar(input_nd, target_nd)
319
+
320
+ def _forward_3d(self, input_nd, target_nd):
321
+ # Native 3D pass
322
+ feats_in = self.extractor.extract(self.extractor.normalize(input_nd))
323
+ feats_tg = self.extractor.extract(self.extractor.normalize(target_nd))
324
+
325
+ loss = 0.0
326
+ from .syn import local_ncc_loss_nd
327
+ for f_in, f_tg in zip(feats_in, feats_tg):
328
+ # Compute 3D LNCC
329
+ loss += local_ncc_loss_nd(f_in, f_tg, window_size=self.lncc_window)
330
+ return loss
331
+
332
+ def _forward_2d_direct(self, input_nd, target_nd):
333
+ # Direct 2D pass for 2D registration using 2D networks
334
+ # Ensure 3-channel input if extractor expects RGB
335
+ if self.extractor.in_channels == 3 and input_nd.shape[1] == 1:
336
+ input_nd = input_nd.repeat(1, 3, 1, 1)
337
+ target_nd = target_nd.repeat(1, 3, 1, 1)
338
+
339
+ feats_in = self.extractor.extract(self.extractor.normalize(input_nd))
340
+ feats_tg = self.extractor.extract(self.extractor.normalize(target_nd))
341
+
342
+ loss = 0.0
343
+ from .syn import local_ncc_loss_nd
344
+ for f_in, f_tg in zip(feats_in, feats_tg):
345
+ loss += local_ncc_loss_nd(f_in, f_tg, window_size=self.lncc_window)
346
+ return loss
347
+
348
+ def _forward_2d_triplanar(self, input_nd, target_nd):
349
+ # Extract orthogonal 2D slices and run 2D LNCC on 2D feature maps
350
+ D, H, W = input_nd.shape[2:]
351
+ device = input_nd.device
352
+
353
+ z_indices = torch.linspace(D // 4, 3 * D // 4, self.num_slices, dtype=torch.long, device=device)
354
+ y_indices = torch.linspace(H // 4, 3 * H // 4, self.num_slices, dtype=torch.long, device=device)
355
+ x_indices = torch.linspace(W // 4, 3 * W // 4, self.num_slices, dtype=torch.long, device=device)
356
+
357
+ target_size = max(D, H, W)
358
+ slices_in = []
359
+ slices_tg = []
360
+
361
+ # Axial
362
+ for z in z_indices:
363
+ # Handle RGB channel stacking if needed
364
+ if self.extractor.in_channels == 3:
365
+ slice_in = input_nd[:, 0, z-1:z+2]
366
+ slice_tg = target_nd[:, 0, z-1:z+2]
367
+ else:
368
+ slice_in = input_nd[:, 0:1, z:z+1]
369
+ slice_tg = target_nd[:, 0:1, z:z+1]
370
+
371
+ if H != target_size or W != target_size:
372
+ slice_in = F.interpolate(slice_in, size=(target_size, target_size), mode='bilinear', align_corners=True)
373
+ slice_tg = F.interpolate(slice_tg, size=(target_size, target_size), mode='bilinear', align_corners=True)
374
+ slices_in.append(slice_in)
375
+ slices_tg.append(slice_tg)
376
+
377
+ # Coronal
378
+ for y in y_indices:
379
+ if self.extractor.in_channels == 3:
380
+ slice_in = input_nd[:, 0, :, y-1:y+2, :].movedim(2, 1)
381
+ slice_tg = target_nd[:, 0, :, y-1:y+2, :].movedim(2, 1)
382
+ else:
383
+ slice_in = input_nd[:, 0:1, :, y:y+1, :].movedim(2, 1)
384
+ slice_tg = target_nd[:, 0:1, :, y:y+1, :].movedim(2, 1)
385
+
386
+ if D != target_size or W != target_size:
387
+ slice_in = F.interpolate(slice_in, size=(target_size, target_size), mode='bilinear', align_corners=True)
388
+ slice_tg = F.interpolate(slice_tg, size=(target_size, target_size), mode='bilinear', align_corners=True)
389
+ slices_in.append(slice_in)
390
+ slices_tg.append(slice_tg)
391
+
392
+ # Sagittal
393
+ for xi in x_indices:
394
+ if self.extractor.in_channels == 3:
395
+ slice_in = input_nd[:, 0, :, :, xi-1:xi+2].movedim(3, 1)
396
+ slice_tg = target_nd[:, 0, :, :, xi-1:xi+2].movedim(3, 1)
397
+ else:
398
+ slice_in = input_nd[:, 0:1, :, :, xi:xi+1].movedim(3, 1)
399
+ slice_tg = target_nd[:, 0:1, :, :, xi:xi+1].movedim(3, 1)
400
+
401
+ if D != target_size or H != target_size:
402
+ slice_in = F.interpolate(slice_in, size=(target_size, target_size), mode='bilinear', align_corners=True)
403
+ slice_tg = F.interpolate(slice_tg, size=(target_size, target_size), mode='bilinear', align_corners=True)
404
+ slices_in.append(slice_in)
405
+ slices_tg.append(slice_tg)
406
+
407
+ input_batch = torch.cat(slices_in, dim=0)
408
+ target_batch = torch.cat(slices_tg, dim=0)
409
+
410
+ feats_in = self.extractor.extract(self.extractor.normalize(input_batch))
411
+ feats_tg = self.extractor.extract(self.extractor.normalize(target_batch))
412
+
413
+ loss = 0.0
414
+ from .syn import local_ncc_loss_nd
415
+ for f_in, f_tg in zip(feats_in, feats_tg):
416
+ loss += local_ncc_loss_nd(f_in, f_tg, window_size=self.lncc_window)
417
+ return loss
418
+
419
+ def _forward_2d_reconstruct_3d(self, input_nd, target_nd):
420
+ # 3D Feature LNCC on reconstructed feature volume
421
+ D, H, W = input_nd.shape[2:]
422
+ B = input_nd.shape[0]
423
+
424
+ def reconstruct_3d_features(x):
425
+ # 1. Axial
426
+ slices_ax = []
427
+ for z in range(1, D - 1):
428
+ if self.extractor.in_channels == 3:
429
+ slices_ax.append(x[:, 0, z-1:z+2])
430
+ else:
431
+ slices_ax.append(x[:, 0:1, z:z+1])
432
+ batch_ax = self.extractor.normalize(torch.cat(slices_ax, dim=0))
433
+
434
+ # 2. Coronal
435
+ slices_co = []
436
+ for y in range(1, H - 1):
437
+ if self.extractor.in_channels == 3:
438
+ slices_co.append(x[:, 0, :, y-1:y+2, :].movedim(2, 1))
439
+ else:
440
+ slices_co.append(x[:, 0:1, :, y:y+1, :].movedim(2, 1))
441
+ batch_co = self.extractor.normalize(torch.cat(slices_co, dim=0))
442
+
443
+ # 3. Sagittal
444
+ slices_sa = []
445
+ for xi in range(1, W - 1):
446
+ if self.extractor.in_channels == 3:
447
+ slices_sa.append(x[:, 0, :, :, xi-1:xi+2].movedim(3, 1))
448
+ else:
449
+ slices_sa.append(x[:, 0:1, :, :, xi:xi+1].movedim(3, 1))
450
+ batch_sa = self.extractor.normalize(torch.cat(slices_sa, dim=0))
451
+
452
+ # Run through extractor
453
+ # We assume extract returns a single feature map for simplicity or we use the last one
454
+ feat_ax = self.extractor.extract(batch_ax)[-1]
455
+ feat_co = self.extractor.extract(batch_co)[-1]
456
+ feat_sa = self.extractor.extract(batch_sa)[-1]
457
+
458
+ # Reconstruct volumes
459
+ vol_ax = feat_ax.view(D-2, B, -1, feat_ax.shape[2], feat_ax.shape[3]).permute(1, 2, 0, 3, 4)
460
+ vol_co = feat_co.view(H-2, B, -1, feat_co.shape[2], feat_co.shape[3]).permute(1, 2, 3, 0, 4)
461
+ vol_sa = feat_sa.view(W-2, B, -1, feat_sa.shape[2], feat_sa.shape[3]).permute(1, 2, 3, 4, 0)
462
+
463
+ return vol_ax, vol_co, vol_sa
464
+
465
+ vol_in_ax, vol_in_co, vol_in_sa = reconstruct_3d_features(input_nd)
466
+ vol_tg_ax, vol_tg_co, vol_tg_sa = reconstruct_3d_features(target_nd)
467
+
468
+ from .syn import local_ncc_loss_nd
469
+ loss_ax = local_ncc_loss_nd(vol_in_ax, vol_tg_ax, window_size=5)
470
+ loss_co = local_ncc_loss_nd(vol_in_co, vol_tg_co, window_size=5)
471
+ loss_sa = local_ncc_loss_nd(vol_in_sa, vol_tg_sa, window_size=5)
472
+
473
+ return loss_ax + loss_co + loss_sa
syntx/resnet.py ADDED
@@ -0,0 +1,100 @@
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ class BasicBlock2D(nn.Module):
5
+ expansion = 1
6
+
7
+ def __init__(self, in_planes, planes, stride=1):
8
+ super().__init__()
9
+ self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
10
+ self.bn1 = nn.BatchNorm2d(planes)
11
+ self.relu = nn.ReLU(inplace=False)
12
+ self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
13
+ self.bn2 = nn.BatchNorm2d(planes)
14
+
15
+ self.shortcut = nn.Sequential()
16
+ if stride != 1 or in_planes != self.expansion * planes:
17
+ self.shortcut = nn.Sequential(
18
+ nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
19
+ nn.BatchNorm2d(self.expansion * planes)
20
+ )
21
+
22
+ def forward(self, x):
23
+ out = self.relu(self.bn1(self.conv1(x)))
24
+ out = self.bn2(self.conv2(out))
25
+ out += self.shortcut(x)
26
+ out = self.relu(out)
27
+ return out
28
+
29
+
30
+ class BasicBlock3D(nn.Module):
31
+ expansion = 1
32
+
33
+ def __init__(self, in_planes, planes, stride=1):
34
+ super().__init__()
35
+ self.conv1 = nn.Conv3d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
36
+ self.bn1 = nn.BatchNorm3d(planes)
37
+ self.relu = nn.ReLU(inplace=False)
38
+ self.conv2 = nn.Conv3d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
39
+ self.bn2 = nn.BatchNorm3d(planes)
40
+
41
+ self.shortcut = nn.Sequential()
42
+ if stride != 1 or in_planes != self.expansion * planes:
43
+ self.shortcut = nn.Sequential(
44
+ nn.Conv3d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
45
+ nn.BatchNorm3d(self.expansion * planes)
46
+ )
47
+
48
+ def forward(self, x):
49
+ out = self.relu(self.bn1(self.conv1(x)))
50
+ out = self.bn2(self.conv2(out))
51
+ out += self.shortcut(x)
52
+ out = self.relu(out)
53
+ return out
54
+
55
+
56
+ class ResNet10(nn.Module):
57
+ def __init__(self, block, num_blocks, dim=3, num_classes=1):
58
+ super().__init__()
59
+ self.in_planes = 64
60
+ self.dim = dim
61
+
62
+ if dim == 2:
63
+ self.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
64
+ self.bn1 = nn.BatchNorm2d(64)
65
+ self.relu = nn.ReLU(inplace=False)
66
+ self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
67
+ else:
68
+ self.conv1 = nn.Conv3d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
69
+ self.bn1 = nn.BatchNorm3d(64)
70
+ self.relu = nn.ReLU(inplace=False)
71
+ self.maxpool = nn.MaxPool3d(kernel_size=3, stride=2, padding=1)
72
+
73
+ self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
74
+ self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
75
+ self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
76
+ self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
77
+
78
+ def _make_layer(self, block, planes, num_blocks, stride):
79
+ strides = [stride] + [1]*(num_blocks-1)
80
+ layers = []
81
+ for s in strides:
82
+ layers.append(block(self.in_planes, planes, s))
83
+ self.in_planes = planes * block.expansion
84
+ return nn.Sequential(*layers)
85
+
86
+ def forward(self, x):
87
+ out = self.relu(self.bn1(self.conv1(x)))
88
+ out = self.maxpool(out)
89
+ out1 = self.layer1(out)
90
+ out2 = self.layer2(out1)
91
+ out3 = self.layer3(out2)
92
+ out4 = self.layer4(out3)
93
+ return out4
94
+
95
+
96
+ def resnet10_2d():
97
+ return ResNet10(BasicBlock2D, [1, 1, 1, 1], dim=2)
98
+
99
+ def resnet10_3d():
100
+ return ResNet10(BasicBlock3D, [1, 1, 1, 1], dim=3)