Skip to content

Commit a031b05

Browse files
authored
Merge pull request #32 from AllenNeuralDynamics/feat-n2v2unet-subclass
feat: n2v2unet subclass
2 parents f352c0c + a8b1625 commit a031b05

2 files changed

Lines changed: 223 additions & 4 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: 214 additions & 3 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

@@ -377,3 +387,204 @@ def forward(self, x):
377387
(B, 1, D, H, W).
378388
"""
379389
return self.conv(x)
390+
391+
392+
class N2V2UNet(UNet):
393+
"""
394+
Noise2Void2 variant of the 3D U-Net.
395+
396+
This implementation makes two architectural modifications relative to
397+
the standard U-Net:
398+
399+
1. Replaces max pooling with anti-aliased MaxBlurPool3D.
400+
2. Removes the highest-resolution skip connection.
401+
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.
405+
"""
406+
407+
def __init__(
408+
self,
409+
width_multiplier=1,
410+
trilinear=True,
411+
residual=True,
412+
):
413+
super().__init__(
414+
width_multiplier=width_multiplier,
415+
trilinear=trilinear,
416+
residual=residual,
417+
)
418+
419+
factor = 2 if trilinear else 1
420+
421+
self.down1 = DownBlur(self.channels[0], self.channels[1])
422+
self.down2 = DownBlur(self.channels[1], self.channels[2])
423+
self.down3 = DownBlur(self.channels[2], self.channels[3])
424+
self.down4 = DownBlur(self.channels[3], self.channels[4] // factor)
425+
426+
# remove highest-resolution skip
427+
self.up4 = UpNoSkip3D(
428+
self.channels[1] // factor,
429+
self.channels[0],
430+
trilinear,
431+
)
432+
433+
def forward(self, x):
434+
input_size = x.shape[2:]
435+
436+
x1 = self.inc(x)
437+
x2 = self.down1(x1)
438+
x3 = self.down2(x2)
439+
x4 = self.down3(x3)
440+
x5 = self.down4(x4)
441+
442+
d = self.up1(x5, x4)
443+
d = self.up2(d, x3)
444+
d = self.up3(d, x2)
445+
d = self.up4(d)
446+
447+
logits = self.outc(d)
448+
449+
# Restore original spatial size
450+
diffD = input_size[0] - logits.size(2)
451+
diffH = input_size[1] - logits.size(3)
452+
diffW = input_size[2] - logits.size(4)
453+
454+
logits = F.pad(
455+
logits,
456+
[
457+
diffW // 2,
458+
diffW - diffW // 2,
459+
diffH // 2,
460+
diffH - diffH // 2,
461+
diffD // 2,
462+
diffD - diffD // 2,
463+
],
464+
)
465+
466+
if self.residual:
467+
return x + logits
468+
469+
return logits
470+
471+
@property
472+
def config(self):
473+
config = super().config
474+
config["model"] = "N2V2UNet"
475+
return config
476+
477+
478+
class DownBlur(nn.Module):
479+
"""Downsampling block using MaxBlurPool3D."""
480+
481+
def __init__(self, in_channels, out_channels):
482+
super().__init__()
483+
484+
self.maxpool_conv = nn.Sequential(
485+
MaxBlurPool3D(in_channels),
486+
DoubleConv(in_channels, out_channels),
487+
)
488+
489+
def forward(self, x):
490+
return self.maxpool_conv(x)
491+
492+
493+
class MaxBlurPool3D(nn.Module):
494+
"""
495+
Anti-aliased 3D max pooling (BlurPool).
496+
"""
497+
498+
def __init__(self, channels):
499+
super().__init__()
500+
501+
# Create separable binomial blur kernel
502+
kernel = torch.tensor([1.0, 2.0, 1.0])
503+
kernel = (
504+
kernel[:, None, None]
505+
* kernel[None, :, None]
506+
* kernel[None, None, :]
507+
)
508+
kernel /= kernel.sum()
509+
510+
# Expand kernel for depthwise convolution
511+
# Shape: (channels, 1, 3, 3, 3)
512+
kernel = kernel[None, None].repeat(channels, 1, 1, 1, 1)
513+
514+
self.register_buffer(
515+
"kernel",
516+
kernel,
517+
persistent=True,
518+
)
519+
520+
self.channels = channels
521+
self.pool = nn.MaxPool3d(2, stride=1)
522+
523+
def forward(self, x):
524+
x = self.pool(x)
525+
x = F.pad(
526+
x,
527+
[1, 1, 1, 1, 1, 1],
528+
mode="replicate",
529+
)
530+
return F.conv3d(
531+
x,
532+
self.kernel,
533+
stride=2,
534+
groups=self.channels,
535+
)
536+
537+
538+
class UpNoSkip3D(nn.Module):
539+
"""
540+
Upsampling block without a skip connection.
541+
"""
542+
543+
def __init__(self, in_channels, out_channels, trilinear=True):
544+
super().__init__()
545+
546+
if trilinear:
547+
self.up = nn.Upsample(
548+
scale_factor=2,
549+
mode="trilinear",
550+
align_corners=True,
551+
)
552+
self.conv = DoubleConv(
553+
in_channels,
554+
out_channels,
555+
mid_channels=in_channels // 2,
556+
)
557+
else:
558+
self.up = nn.ConvTranspose3d(
559+
in_channels,
560+
in_channels // 2,
561+
kernel_size=2,
562+
stride=2,
563+
)
564+
self.conv = DoubleConv(
565+
in_channels // 2,
566+
out_channels,
567+
)
568+
569+
def forward(self, x):
570+
x = self.up(x)
571+
return self.conv(x)
572+
573+
574+
def test_unets():
575+
for cls in [UNet, N2V2UNet]:
576+
model = cls()
577+
model.eval()
578+
579+
for size in [32, 33, 64, 65, 128]:
580+
x = torch.randn(1, 1, size, size, size)
581+
582+
with torch.no_grad():
583+
y = model(x)
584+
585+
assert y.shape == x.shape, (
586+
f"{cls.__name__}: "
587+
f"{x.shape} -> {y.shape}"
588+
)
589+
590+
print(f"{cls.__name__}: passed")

0 commit comments

Comments
 (0)