Skip to content

Commit 03583ed

Browse files
author
anna-grim
committed
bug: load model, upsample, rename noskip3d
1 parent 0105428 commit 03583ed

2 files changed

Lines changed: 150 additions & 28 deletions

File tree

src/aind_exaspim_image_compression/inference.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ def load_model(path, device="cuda"):
274274
transform : IntensityTransform
275275
The intensity transform the model was trained with.
276276
"""
277+
# Read ckpt and create model config
277278
ckpt = torch.load(path, map_location=device)
278279
if isinstance(ckpt, dict) and "model" in ckpt:
279280
state_dict = ckpt["model"]
@@ -284,7 +285,14 @@ def load_model(path, device="cuda"):
284285
transform_cfg = {"kind": "asinh"}
285286
model_cfg = {}
286287

287-
model = UNet(**model_cfg)
288+
# Determine correct model class
289+
model_type = model_cfg.pop("model", "UNet")
290+
if model_type == "N2V2UNet":
291+
model = N2V2UNet(**model_cfg)
292+
else:
293+
model = UNet(**model_cfg)
294+
295+
# Load ckpt into model
288296
model.load_state_dict(state_dict)
289297
model.to(device)
290298
model.eval()

src/aind_exaspim_image_compression/machine_learning/unet3d.py

Lines changed: 141 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,24 @@ def forward(self, x1, x2):
321321
(B, out_channels, D, H2, W2).
322322
"""
323323
x1 = self.up(x1)
324-
diffY = x2.size()[2] - x1.size()[2]
325-
diffX = x2.size()[3] - x1.size()[3]
324+
325+
diffD = x2.size()[2] - x1.size()[2]
326+
diffH = x2.size()[3] - x1.size()[3]
327+
diffW = x2.size()[4] - x1.size()[4]
326328

327329
x1 = F.pad(
328330
x1,
329-
[diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2],
331+
[
332+
diffW // 2,
333+
diffW - diffW // 2,
334+
diffH // 2,
335+
diffH - diffH // 2,
336+
diffD // 2,
337+
diffD - diffD // 2,
338+
],
330339
)
331340
x = torch.cat([x2, x1], dim=1)
341+
332342
return self.conv(x)
333343

334344

@@ -389,8 +399,9 @@ class N2V2UNet(UNet):
389399
1. Replaces max pooling with anti-aliased MaxBlurPool3D.
390400
2. Removes the highest-resolution skip connection.
391401
392-
Residual learning is disabled by default to avoid reintroducing an
393-
identity path from the input to the output.
402+
Residual learning can be enabled to predict a residual correction
403+
that is added to the input image. This is commonly used for denoising
404+
tasks.
394405
"""
395406

396407
def __init__(
@@ -399,7 +410,6 @@ def __init__(
399410
trilinear=True,
400411
residual=True,
401412
):
402-
# Call parent class
403413
super().__init__(
404414
width_multiplier=width_multiplier,
405415
trilinear=trilinear,
@@ -408,11 +418,67 @@ def __init__(
408418

409419
factor = 2 if trilinear else 1
410420

411-
# Encoder
412-
self.down1 = DownBlur(self.channels[0], self.channels[1])
413-
self.down2 = DownBlur(self.channels[1], self.channels[2])
414-
self.down3 = DownBlur(self.channels[2], self.channels[3])
415-
self.down4 = DownBlur(self.channels[3], self.channels[4] // factor)
421+
self.down1 = DownBlur(
422+
self.channels[0],
423+
self.channels[1],
424+
)
425+
self.down2 = DownBlur(
426+
self.channels[1],
427+
self.channels[2],
428+
)
429+
self.down3 = DownBlur(
430+
self.channels[2],
431+
self.channels[3],
432+
)
433+
self.down4 = DownBlur(
434+
self.channels[3],
435+
self.channels[4] // factor,
436+
)
437+
438+
# remove highest-resolution skip
439+
self.up4 = UpNoSkip3D(
440+
self.channels[1] // factor,
441+
self.channels[0],
442+
trilinear,
443+
)
444+
445+
def forward(self, x):
446+
input_size = x.shape[2:]
447+
448+
x1 = self.inc(x)
449+
x2 = self.down1(x1)
450+
x3 = self.down2(x2)
451+
x4 = self.down3(x3)
452+
x5 = self.down4(x4)
453+
454+
d = self.up1(x5, x4)
455+
d = self.up2(d, x3)
456+
d = self.up3(d, x2)
457+
d = self.up4(d)
458+
459+
logits = self.outc(d)
460+
461+
# Restore original spatial size
462+
diffD = input_size[0] - logits.size(2)
463+
diffH = input_size[1] - logits.size(3)
464+
diffW = input_size[2] - logits.size(4)
465+
466+
logits = F.pad(
467+
logits,
468+
[
469+
diffW // 2,
470+
diffW - diffW // 2,
471+
diffH // 2,
472+
diffH - diffH // 2,
473+
diffD // 2,
474+
diffD - diffD // 2,
475+
],
476+
)
477+
478+
if self.residual:
479+
return x + logits
480+
481+
return logits
416482

417483
@property
418484
def config(self):
@@ -442,10 +508,9 @@ class MaxBlurPool3D(nn.Module):
442508
"""
443509

444510
def __init__(self, channels):
445-
# Call parent class
446-
super().__init__()
511+
super().__init__()
447512

448-
# Create kernel
513+
# Create separable binomial blur kernel
449514
kernel = torch.tensor([1.0, 2.0, 1.0])
450515
kernel = (
451516
kernel[:, None, None]
@@ -454,13 +519,16 @@ def __init__(self, channels):
454519
)
455520
kernel /= kernel.sum()
456521

522+
# Expand kernel for depthwise convolution
523+
# Shape: (channels, 1, 3, 3, 3)
524+
kernel = kernel[None, None].repeat(channels, 1, 1, 1, 1)
525+
457526
self.register_buffer(
458527
"kernel",
459-
kernel[None, None],
460-
persistent=False,
528+
kernel,
529+
persistent=True,
461530
)
462531

463-
# Instance attributes
464532
self.channels = channels
465533
self.pool = nn.MaxPool3d(2, stride=1)
466534

@@ -471,18 +539,64 @@ def forward(self, x):
471539
[1, 1, 1, 1, 1, 1],
472540
mode="replicate",
473541
)
474-
475-
kernel = self.kernel.expand(
476-
self.channels,
477-
-1,
478-
-1,
479-
-1,
480-
-1,
481-
)
482-
483542
return F.conv3d(
484543
x,
485-
kernel,
544+
self.kernel,
486545
stride=2,
487546
groups=self.channels,
488547
)
548+
549+
550+
class UpNoSkip3D(nn.Module):
551+
"""
552+
Upsampling block without a skip connection.
553+
"""
554+
555+
def __init__(self, in_channels, out_channels, trilinear=True):
556+
super().__init__()
557+
558+
if trilinear:
559+
self.up = nn.Upsample(
560+
scale_factor=2,
561+
mode="trilinear",
562+
align_corners=True,
563+
)
564+
self.conv = DoubleConv(
565+
in_channels,
566+
out_channels,
567+
mid_channels=in_channels // 2,
568+
)
569+
else:
570+
self.up = nn.ConvTranspose3d(
571+
in_channels,
572+
in_channels // 2,
573+
kernel_size=2,
574+
stride=2,
575+
)
576+
self.conv = DoubleConv(
577+
in_channels // 2,
578+
out_channels,
579+
)
580+
581+
def forward(self, x):
582+
x = self.up(x)
583+
return self.conv(x)
584+
585+
586+
def test_unets():
587+
for cls in [UNet, N2V2UNet]:
588+
model = cls()
589+
model.eval()
590+
591+
for size in [32, 33, 64, 65, 128]:
592+
x = torch.randn(1, 1, size, size, size)
593+
594+
with torch.no_grad():
595+
y = model(x)
596+
597+
assert y.shape == x.shape, (
598+
f"{cls.__name__}: "
599+
f"{x.shape} -> {y.shape}"
600+
)
601+
602+
print(f"{cls.__name__}: passed")

0 commit comments

Comments
 (0)