@@ -377,3 +377,112 @@ def forward(self, x):
377377 (B, 1, D, H, W).
378378 """
379379 return self .conv (x )
380+
381+
382+ class N2V2UNet (UNet ):
383+ """
384+ Noise2Void2 variant of the 3D U-Net.
385+
386+ This implementation makes two architectural modifications relative to
387+ the standard U-Net:
388+
389+ 1. Replaces max pooling with anti-aliased MaxBlurPool3D.
390+ 2. Removes the highest-resolution skip connection.
391+
392+ Residual learning is disabled by default to avoid reintroducing an
393+ identity path from the input to the output.
394+ """
395+
396+ def __init__ (
397+ self ,
398+ width_multiplier = 1 ,
399+ trilinear = True ,
400+ residual = True ,
401+ ):
402+ # Call parent class
403+ super ().__init__ (
404+ width_multiplier = width_multiplier ,
405+ trilinear = trilinear ,
406+ residual = residual ,
407+ )
408+
409+ factor = 2 if trilinear else 1
410+
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 )
416+
417+ @property
418+ def config (self ):
419+ config = super ().config
420+ config ["model" ] = "N2V2UNet"
421+ return config
422+
423+
424+ class DownBlur (nn .Module ):
425+ """Downsampling block using MaxBlurPool3D."""
426+
427+ def __init__ (self , in_channels , out_channels ):
428+ super ().__init__ ()
429+
430+ self .maxpool_conv = nn .Sequential (
431+ MaxBlurPool3D (in_channels ),
432+ DoubleConv (in_channels , out_channels ),
433+ )
434+
435+ def forward (self , x ):
436+ return self .maxpool_conv (x )
437+
438+
439+ class MaxBlurPool3D (nn .Module ):
440+ """
441+ Anti-aliased 3D max pooling (BlurPool).
442+ """
443+
444+ def __init__ (self , channels ):
445+ # Call parent class
446+ super ().__init__ ()
447+
448+ # Create kernel
449+ kernel = torch .tensor ([1.0 , 2.0 , 1.0 ])
450+ kernel = (
451+ kernel [:, None , None ]
452+ * kernel [None , :, None ]
453+ * kernel [None , None , :]
454+ )
455+ kernel /= kernel .sum ()
456+
457+ self .register_buffer (
458+ "kernel" ,
459+ kernel [None , None ],
460+ persistent = False ,
461+ )
462+
463+ # Instance attributes
464+ self .channels = channels
465+ self .pool = nn .MaxPool3d (2 , stride = 1 )
466+
467+ def forward (self , x ):
468+ x = self .pool (x )
469+ x = F .pad (
470+ x ,
471+ [1 , 1 , 1 , 1 , 1 , 1 ],
472+ mode = "replicate" ,
473+ )
474+
475+ kernel = self .kernel .expand (
476+ self .channels ,
477+ - 1 ,
478+ - 1 ,
479+ - 1 ,
480+ - 1 ,
481+ )
482+
483+ return F .conv3d (
484+ x ,
485+ kernel ,
486+ stride = 2 ,
487+ groups = self .channels ,
488+ )
0 commit comments