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