@@ -137,6 +137,246 @@ def test_invert(self):
137137
138138 set_determinism (seed = None )
139139
140+ def test_invertd_with_postprocessing_transforms (self ):
141+ """Test that Invertd ignores postprocessing transforms using automatic group tracking.
142+
143+ This is a regression test for the issue where Invertd would fail when
144+ postprocessing contains invertible transforms before Invertd is called.
145+ The fix uses automatic group tracking where Compose assigns its ID to child transforms.
146+ """
147+ from monai .data import MetaTensor , create_test_image_2d
148+ from monai .transforms .utility .dictionary import Lambdad
149+
150+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
151+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
152+ key = "image"
153+
154+ # Preprocessing pipeline
155+ preprocessing = Compose ([
156+ EnsureChannelFirstd (key ),
157+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
158+ ])
159+
160+ # Postprocessing with Lambdad before Invertd
161+ # Previously this would raise RuntimeError about transform ID mismatch
162+ postprocessing = Compose ([
163+ Lambdad (key , func = lambda x : x ), # Should be ignored during inversion
164+ Invertd (key , transform = preprocessing , orig_keys = key )
165+ ])
166+
167+ # Apply transforms
168+ item = {key : img }
169+ pre = preprocessing (item )
170+
171+ # This should NOT raise an error (was failing before the fix)
172+ try :
173+ post = postprocessing (pre )
174+ # If we get here, the bug is fixed
175+ self .assertIsNotNone (post )
176+ self .assertIn (key , post )
177+ print (f"SUCCESS! Automatic group tracking fixed the bug." )
178+ print (f" Preprocessing group ID: { id (preprocessing )} " )
179+ print (f" Postprocessing group ID: { id (postprocessing )} " )
180+ except RuntimeError as e :
181+ if "getting the most recently applied invertible transform" in str (e ):
182+ self .fail (f"Invertd still has the postprocessing transform bug: { e } " )
183+
184+ def test_invertd_multiple_pipelines (self ):
185+ """Test that Invertd correctly handles multiple independent preprocessing pipelines."""
186+ from monai .data import MetaTensor , create_test_image_2d
187+ from monai .transforms .utility .dictionary import Lambdad
188+
189+ img1 , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
190+ img1 = MetaTensor (img1 , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
191+ img2 , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
192+ img2 = MetaTensor (img2 , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
193+
194+ # Two different preprocessing pipelines
195+ preprocessing1 = Compose ([
196+ EnsureChannelFirstd ("image1" ),
197+ Spacingd ("image1" , pixdim = [2.0 , 2.0 ]),
198+ ])
199+
200+ preprocessing2 = Compose ([
201+ EnsureChannelFirstd ("image2" ),
202+ Spacingd ("image2" , pixdim = [1.5 , 1.5 ]),
203+ ])
204+
205+ # Postprocessing that inverts both
206+ postprocessing = Compose ([
207+ Lambdad (["image1" , "image2" ], func = lambda x : x ),
208+ Invertd ("image1" , transform = preprocessing1 , orig_keys = "image1" ),
209+ Invertd ("image2" , transform = preprocessing2 , orig_keys = "image2" ),
210+ ])
211+
212+ # Apply transforms
213+ item = {"image1" : img1 , "image2" : img2 }
214+ pre1 = preprocessing1 (item )
215+ pre2 = preprocessing2 (pre1 )
216+
217+ # Should not raise error - each Invertd should only invert its own pipeline
218+ post = postprocessing (pre2 )
219+ self .assertIn ("image1" , post )
220+ self .assertIn ("image2" , post )
221+
222+ def test_invertd_multiple_postprocessing_transforms (self ):
223+ """Test Invertd with multiple invertible transforms in postprocessing before Invertd."""
224+ from monai .data import MetaTensor , create_test_image_2d
225+ from monai .transforms .utility .dictionary import Lambdad
226+
227+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
228+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
229+ key = "image"
230+
231+ preprocessing = Compose ([
232+ EnsureChannelFirstd (key ),
233+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
234+ ])
235+
236+ # Multiple transforms in postprocessing before Invertd
237+ postprocessing = Compose ([
238+ Lambdad (key , func = lambda x : x * 2 ),
239+ Lambdad (key , func = lambda x : x + 1 ),
240+ Lambdad (key , func = lambda x : x - 1 ),
241+ Invertd (key , transform = preprocessing , orig_keys = key )
242+ ])
243+
244+ item = {key : img }
245+ pre = preprocessing (item )
246+ post = postprocessing (pre )
247+
248+ self .assertIsNotNone (post )
249+ self .assertIn (key , post )
250+
251+ def test_invertd_group_isolation (self ):
252+ """Test that groups correctly isolate transforms from different Compose instances."""
253+ from monai .data import MetaTensor , create_test_image_2d
254+
255+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
256+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
257+ key = "image"
258+
259+ # First preprocessing
260+ preprocessing1 = Compose ([
261+ EnsureChannelFirstd (key ),
262+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
263+ ])
264+
265+ # Second preprocessing (different pipeline)
266+ preprocessing2 = Compose ([
267+ Spacingd (key , pixdim = [1.5 , 1.5 ]),
268+ ])
269+
270+ item = {key : img }
271+ pre1 = preprocessing1 (item )
272+
273+ # Verify group IDs are in applied_operations
274+ self .assertTrue (len (pre1 [key ].applied_operations ) > 0 )
275+ group1 = pre1 [key ].applied_operations [0 ].get ("group" )
276+ self .assertIsNotNone (group1 )
277+ self .assertEqual (group1 , str (id (preprocessing1 )))
278+
279+ # Apply second preprocessing
280+ pre2 = preprocessing2 (pre1 )
281+
282+ # Should have operations from both pipelines with different groups
283+ groups = [op .get ("group" ) for op in pre2 [key ].applied_operations ]
284+ self .assertIn (str (id (preprocessing1 )), groups )
285+ self .assertIn (str (id (preprocessing2 )), groups )
286+
287+ # Inverting preprocessing1 should only invert its transforms
288+ inverter = Invertd (key , transform = preprocessing1 , orig_keys = key )
289+ inverted = inverter (pre2 )
290+ self .assertIsNotNone (inverted )
291+
292+ def test_compose_inverse_with_groups (self ):
293+ """Test that Compose.inverse() works correctly with automatic group tracking."""
294+ from monai .data import MetaTensor , create_test_image_2d
295+
296+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
297+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
298+ key = "image"
299+
300+ # Create a preprocessing pipeline
301+ preprocessing = Compose ([
302+ EnsureChannelFirstd (key ),
303+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
304+ ])
305+
306+ # Apply preprocessing
307+ item = {key : img }
308+ pre = preprocessing (item )
309+
310+ # Call inverse() directly on the Compose object
311+ inverted = preprocessing .inverse (pre )
312+
313+ # Should successfully invert
314+ self .assertIsNotNone (inverted )
315+ self .assertIn (key , inverted )
316+ # Shape should be restored after inversion
317+ self .assertEqual (inverted [key ].shape [1 :], img .shape )
318+
319+ def test_compose_inverse_with_postprocessing_groups (self ):
320+ """Test Compose.inverse() when data has been through multiple pipelines with different groups."""
321+ from monai .data import MetaTensor , create_test_image_2d
322+ from monai .transforms .utility .dictionary import Lambdad
323+
324+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
325+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
326+ key = "image"
327+
328+ # Preprocessing pipeline
329+ preprocessing = Compose ([
330+ EnsureChannelFirstd (key ),
331+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
332+ ])
333+
334+ # Postprocessing pipeline (different group)
335+ postprocessing = Compose ([
336+ Lambdad (key , func = lambda x : x * 2 ),
337+ ])
338+
339+ # Apply both pipelines
340+ item = {key : img }
341+ pre = preprocessing (item )
342+ post = postprocessing (pre )
343+
344+ # Now call inverse() directly on preprocessing
345+ # This tests that inverse() can handle data that has transforms from multiple groups
346+ # This WILL fail because applied_operations contains postprocessing transforms
347+ # and inverse() doesn't do group filtering (only Invertd does)
348+ with self .assertRaises (RuntimeError ):
349+ inverted = preprocessing .inverse (post )
350+
351+ def test_mixed_invertd_and_compose_inverse (self ):
352+ """Test mixing Invertd (with group filtering) and Compose.inverse() (without filtering)."""
353+ from monai .data import MetaTensor , create_test_image_2d
354+
355+ img , _ = create_test_image_2d (60 , 60 , 2 , 10 , num_seg_classes = 2 )
356+ img = MetaTensor (img , meta = {"original_channel_dim" : float ("nan" ), "pixdim" : [1.0 , 1.0 , 1.0 ]})
357+ key = "image"
358+
359+ # First pipeline
360+ pipeline1 = Compose ([
361+ EnsureChannelFirstd (key ),
362+ Spacingd (key , pixdim = [2.0 , 2.0 ]),
363+ ])
364+
365+ # Apply first pipeline
366+ item = {key : img }
367+ result1 = pipeline1 (item )
368+
369+ # Use Compose.inverse() directly - should work fine
370+ inverted1 = pipeline1 .inverse (result1 )
371+ self .assertIsNotNone (inverted1 )
372+ self .assertEqual (inverted1 [key ].shape [1 :], img .shape )
373+
374+ # Now apply pipeline again and use Invertd
375+ result2 = pipeline1 (item )
376+ inverter = Invertd (key , transform = pipeline1 , orig_keys = key )
377+ inverted2 = inverter (result2 )
378+ self .assertIsNotNone (inverted2 )
379+
140380
141381if __name__ == "__main__" :
142382 unittest .main ()
0 commit comments