@@ -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