3333from monai .transforms .inverse import InvertibleTransform
3434from monai .transforms .transform import RandomizableTransform , Transform
3535from monai .transforms .utils import Fourier , equalize_hist , is_positive , rescale_array , soft_clip
36- from monai .transforms .utils_pytorch_numpy_unification import clip , percentile , where
36+ from monai .transforms .utils_pytorch_numpy_unification import clip , nonzero , percentile , ravel , where
3737from monai .utils .enums import TraceKeys , TransformBackends
3838from monai .utils .misc import ensure_tuple , ensure_tuple_rep , ensure_tuple_size , fall_back_tuple
3939from monai .utils .module import min_version , optional_import
@@ -847,8 +847,10 @@ class NormalizeIntensity(InvertibleTransform):
847847
848848 The subtrahend and divisor actually used (whether provided or computed) are stored in the
849849 transform's meta information, so the transform is invertible via :meth:`inverse`, recovering
850- ``img * divisor + subtrahend``. Inversion is not supported when ``nonzero=True``, because the
851- zero-voxel mask would be required to reverse the operation exactly.
850+ ``img * divisor + subtrahend``. With ``nonzero=True`` only the voxels that were non-zero on the
851+ forward pass are restored: they are identified as the non-zero voxels of the normalized image,
852+ plus the (usually empty) set of voxels whose value equalled the subtrahend exactly and therefore
853+ became zero, whose flat indices are also stored in the meta information.
852854
853855 Args:
854856 subtrahend: the amount to subtract by (usually the mean).
@@ -890,13 +892,28 @@ def _std(x):
890892 return x .item () if x .numel () == 1 else x
891893
892894 def _normalize (self , img : NdarrayOrTensor , sub = None , div = None ):
895+ """
896+ Normalize ``img`` in place where possible and report what was done, for :meth:`inverse`.
897+
898+ Args:
899+ img: image (or single channel when ``channel_wise=True``) to normalize.
900+ sub: subtrahend to use; computed as the mean of the (non-zero) voxels if None.
901+ div: divisor to use; computed as the std of the (non-zero) voxels if None.
902+
903+ Returns:
904+ a tuple ``(normalized, sub, div, zeroed_idx)``: the normalized image, the subtrahend and
905+ divisor actually used (identity ``0.0``/``1.0`` when ``nonzero=True`` and there is nothing to
906+ normalize), and, when ``nonzero=True``, the flat indices of voxels that were non-zero before but
907+ are exactly zero after normalization (``None`` when ``nonzero=False``).
908+ """
893909 img , * _ = convert_data_type (img , dtype = torch .float32 )
894910
895911 if self .nonzero :
896912 slices = img != 0
897913 masked_img = img [slices ]
898914 if not slices .any ():
899- return img , None , None
915+ # nothing was normalized: store identity stats (keeps meta collate-safe) and no indices
916+ return img , 0.0 , 1.0 , nonzero (ravel (slices ))
900917 else :
901918 slices = None
902919 masked_img = img
@@ -917,12 +934,16 @@ def _normalize(self, img: NdarrayOrTensor, sub=None, div=None):
917934 _div = _div [slices ]
918935 _div [_div == 0.0 ] = 1.0
919936
937+ zeroed_idx = None
920938 if slices is not None :
921939 img [slices ] = (masked_img - _sub ) / _div
940+ # voxels that were non-zero but now equal zero (value == subtrahend) are indistinguishable
941+ # from the untouched zero voxels in the output, so record them for inverse().
942+ zeroed_idx = nonzero (ravel (slices & (img == 0 )))
922943 else :
923944 img = (img - _sub ) / _div
924945 # Return the subtrahend/divisor actually used so the transform can be inverted.
925- return img , _sub , _div
946+ return img , _sub , _div , zeroed_idx
926947
927948 def __call__ (self , img : NdarrayOrTensor ) -> NdarrayOrTensor :
928949 """
@@ -931,9 +952,11 @@ def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor:
931952 img_t : torch .Tensor = convert_to_tensor (img , track_meta = get_track_meta ()) # type: ignore[assignment]
932953 dtype = self .dtype or img .dtype
933954 img_len = len (img_t )
934- # Subtrahend/divisor used per channel (channel_wise) or once (global), kept for inverse().
955+ # Subtrahend/divisor used per channel (channel_wise) or once (global), kept for inverse(),
956+ # plus (nonzero=True only) the indices of voxels that were zeroed by the normalization.
935957 subs : list = []
936958 divs : list = []
959+ zeroed : list = []
937960 if self .channel_wise :
938961 if self .subtrahend is not None and len (self .subtrahend ) != img_len :
939962 raise ValueError (f"img has { img_len } channels, but subtrahend has { len (self .subtrahend )} components." )
@@ -944,31 +967,55 @@ def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor:
944967 img_t , * _ = convert_data_type (img_t , dtype = torch .float32 )
945968
946969 for i , d in enumerate (img_t ):
947- img_t [i ], _sub , _div = self ._normalize ( # type: ignore
970+ img_t [i ], _sub , _div , _idx = self ._normalize ( # type: ignore
948971 d ,
949972 sub = self .subtrahend [i ] if self .subtrahend is not None else None ,
950973 div = self .divisor [i ] if self .divisor is not None else None ,
951974 )
952975 subs .append (_sub )
953976 divs .append (_div )
977+ zeroed .append (_idx )
954978 else :
955- img_t , _sub , _div = self ._normalize (img_t , self .subtrahend , self .divisor ) # type: ignore[assignment]
979+ img_t , _sub , _div , _idx = self ._normalize (img_t , self .subtrahend , self .divisor ) # type: ignore
956980 subs .append (_sub )
957981 divs .append (_div )
982+ zeroed .append (_idx )
958983
959984 out = convert_to_dst_type (img_t , img_t , dtype = dtype )[0 ]
960- out = self ._push_transform_with_stats (out , subs , divs )
961- return out
985+ return self ._push_transform_with_stats (out , subs , divs , zeroed )
962986
963- def _to_storable (self , value ):
964- """Convert a subtrahend/divisor to something storable in transform meta."""
987+ @staticmethod
988+ def _to_storable (value ):
989+ """
990+ Convert a value computed on the forward pass to something storable in the transform meta information.
991+
992+ Args:
993+ value: a subtrahend, divisor or index array; a python/numpy scalar, ``np.ndarray`` or ``torch.Tensor``.
994+
995+ Returns:
996+ a detached, plain (non-Meta) CPU ``torch.Tensor`` for array inputs, otherwise the value unchanged.
997+ """
998+ if isinstance (value , MetaTensor ):
999+ value = value .as_tensor ()
9651000 if isinstance (value , torch .Tensor ):
9661001 return value .detach ().cpu ()
9671002 if isinstance (value , np .ndarray ):
9681003 return torch .as_tensor (value )
9691004 return value # python/numpy scalar
9701005
971- def _push_transform_with_stats (self , out , subs : list , divs : list ):
1006+ def _push_transform_with_stats (self , out : NdarrayOrTensor , subs : list , divs : list , zeroed : list ) -> NdarrayOrTensor :
1007+ """
1008+ Record the parameters needed by :meth:`inverse` in the transform meta information of ``out``.
1009+
1010+ Args:
1011+ out: the normalized image; only a ``MetaTensor`` (with meta tracking enabled) can carry the record.
1012+ subs: subtrahend used for each channel (``channel_wise=True``) or a single-element list.
1013+ divs: divisor used for each channel (``channel_wise=True``) or a single-element list.
1014+ zeroed: per-channel flat indices of voxels zeroed by the normalization (``nonzero=True`` only).
1015+
1016+ Returns:
1017+ ``out``, with the transform pushed onto its applied operations when it is a ``MetaTensor``.
1018+ """
9721019 if not isinstance (out , MetaTensor ) or not get_track_meta ():
9731020 return out
9741021 extra_info = {
@@ -977,33 +1024,51 @@ def _push_transform_with_stats(self, out, subs: list, divs: list):
9771024 "channel_wise" : self .channel_wise ,
9781025 "nonzero" : self .nonzero ,
9791026 }
1027+ if self .nonzero :
1028+ extra_info ["zeroed_idx" ] = [self ._to_storable (z ) for z in zeroed ]
9801029 self .push_transform (out , extra_info = extra_info )
9811030 return out
9821031
9831032 def inverse (self , data : torch .Tensor ) -> torch .Tensor :
1033+ """
1034+ Undo the normalization recorded on ``data`` by :meth:`__call__`, i.e. ``img * divisor + subtrahend``.
1035+ With ``nonzero=True`` only the voxels that were normalized on the forward pass are restored.
1036+
1037+ Args:
1038+ data: a ``MetaTensor`` produced by this transform, with the transform still on its applied operations.
1039+
1040+ Returns:
1041+ the de-normalized image, of the same type as ``data``.
1042+
1043+ Raises:
1044+ RuntimeError: if the most recent applied operation on ``data`` was not made by this transform.
1045+ """
9841046 transform = self .pop_transform (data )
9851047 info = transform [TraceKeys .EXTRA_INFO ]
986- if info ["nonzero" ]:
987- raise NotImplementedError (
988- "NormalizeIntensity.inverse is not supported when nonzero=True, because the "
989- "zero-voxel mask is needed to reverse the normalization exactly."
990- )
9911048 subs , divs = info ["sub" ], info ["div" ]
1049+ zeroed = info .get ("zeroed_idx" ) if info ["nonzero" ] else None
9921050 out : torch .Tensor = convert_to_tensor (data , track_meta = get_track_meta ()) # type: ignore[assignment]
9931051
994- def _restore (x , sub , div ):
995- sub , * _ = convert_to_dst_type (sub , x )
996- div , * _ = convert_to_dst_type (div , x )
997- return x * div + sub
1052+ def _restore (x , sub , div , zeroed_idx = None ):
1053+ if zeroed_idx is None :
1054+ sub , * _ = convert_to_dst_type (sub , x )
1055+ div , * _ = convert_to_dst_type (div , x )
1056+ return x * div + sub
1057+ # nonzero=True: the forward mask is the output's non-zero voxels plus the recorded zeroed ones
1058+ mask = x != 0
1059+ zeroed_idx , * _ = convert_to_dst_type (zeroed_idx , mask , dtype = torch .long )
1060+ mask .view (- 1 )[zeroed_idx ] = True
1061+ vals = x [mask ]
1062+ sub , * _ = convert_to_dst_type (sub , vals )
1063+ div , * _ = convert_to_dst_type (div , vals )
1064+ x [mask ] = vals * div + sub
1065+ return x
9981066
9991067 if info ["channel_wise" ]:
10001068 for i in range (len (out )):
1001- if subs [i ] is None or divs [i ] is None : # all-zero channel skipped on the forward pass
1002- continue
1003- out [i ] = _restore (out [i ], subs [i ], divs [i ])
1069+ out [i ] = _restore (out [i ], subs [i ], divs [i ], None if zeroed is None else zeroed [i ])
10041070 else :
1005- if subs [0 ] is not None and divs [0 ] is not None :
1006- out = _restore (out , subs [0 ], divs [0 ])
1071+ out = _restore (out , subs [0 ], divs [0 ], None if zeroed is None else zeroed [0 ])
10071072 return out
10081073
10091074
0 commit comments