@@ -263,3 +263,93 @@ def test_blob_detector_2d_with_reference_image():
263263 # Check that radius is reasonable
264264 assert np .all (radius_values < 100 ), f"Radius values too large: { radius_values } "
265265 assert np .all (radius_values > 0 ), f"Radius values should be positive: { radius_values } "
266+
267+
268+ def test_blob_detector_round_channel_assignment ():
269+ """Test that BlobDetector with is_volume=False assigns spots to correct (round, channel) pairs.
270+
271+ This is a regression test for a bug where spots were assigned to incorrect round/channel
272+ combinations due to a mismatch between the sorted (r, ch) tuples and the iteration order
273+ from _iter_axes({Axes.ROUND, Axes.CH}).
274+ """
275+ # Create a test ImageStack with 2 rounds, 3 channels, 1 z-plane
276+ # Each (round, channel) pair gets a spot at a unique location
277+ data = np .zeros ((2 , 3 , 1 , 100 , 100 ), dtype = np .float32 )
278+
279+ # Add spots at different y-coordinates for each (round, channel)
280+ # Round 0, Channel 0: y=10
281+ data [0 , 0 , 0 , 8 :13 , 8 :13 ] = 1.0
282+ # Round 0, Channel 1: y=20
283+ data [0 , 1 , 0 , 18 :23 , 18 :23 ] = 1.0
284+ # Round 0, Channel 2: y=30
285+ data [0 , 2 , 0 , 28 :33 , 28 :33 ] = 1.0
286+ # Round 1, Channel 0: y=40
287+ data [1 , 0 , 0 , 38 :43 , 38 :43 ] = 1.0
288+ # Round 1, Channel 1: y=50
289+ data [1 , 1 , 0 , 48 :53 , 48 :53 ] = 1.0
290+ # Round 1, Channel 2: y=60
291+ data [1 , 2 , 0 , 58 :63 , 58 :63 ] = 1.0
292+
293+ image_stack = ImageStack .from_numpy (data )
294+
295+ # Test with is_volume=False
296+ detector_2d = BlobDetector (
297+ min_sigma = 1 ,
298+ max_sigma = 3 ,
299+ num_sigma = 5 ,
300+ threshold = 0.01 ,
301+ is_volume = False ,
302+ measurement_type = 'mean'
303+ )
304+
305+ spots_2d = detector_2d .run (image_stack = image_stack )
306+
307+ # Test with is_volume=True for comparison
308+ detector_3d = BlobDetector (
309+ min_sigma = 1 ,
310+ max_sigma = 3 ,
311+ num_sigma = 5 ,
312+ threshold = 0.01 ,
313+ is_volume = True ,
314+ measurement_type = 'mean'
315+ )
316+
317+ spots_3d = detector_3d .run (image_stack = image_stack )
318+
319+ # Define expected y-coordinates for each (round, channel) pair
320+ expected_y = {
321+ (0 , 0 ): 10 ,
322+ (0 , 1 ): 20 ,
323+ (0 , 2 ): 30 ,
324+ (1 , 0 ): 40 ,
325+ (1 , 1 ): 50 ,
326+ (1 , 2 ): 60 ,
327+ }
328+
329+ # Check that both is_volume=False and is_volume=True produce the same results
330+ for r in range (2 ):
331+ for ch in range (3 ):
332+ # Get spots for this (round, channel)
333+ spots_2d_data = spots_2d [{Axes .ROUND : r , Axes .CH : ch }].spot_attrs .data
334+ spots_3d_data = spots_3d [{Axes .ROUND : r , Axes .CH : ch }].spot_attrs .data
335+
336+ # Both should have found the spot
337+ assert len (spots_2d_data ) > 0 , \
338+ f"No spots found for Round={ r } , CH={ ch } with is_volume=False"
339+ assert len (spots_3d_data ) > 0 , \
340+ f"No spots found for Round={ r } , CH={ ch } with is_volume=True"
341+
342+ # Check that the y-coordinate matches the expected value
343+ y_2d = spots_2d_data ['y' ].values [0 ]
344+ y_3d = spots_3d_data ['y' ].values [0 ]
345+ expected = expected_y [(r , ch )]
346+
347+ assert np .abs (y_2d - expected ) < 5 , \
348+ f"is_volume=False: Round={ r } , CH={ ch } has y={ y_2d :.0f} , expected ~{ expected } "
349+ assert np .abs (y_3d - expected ) < 5 , \
350+ f"is_volume=True: Round={ r } , CH={ ch } has y={ y_3d :.0f} , expected ~{ expected } "
351+
352+ # Both should produce the same y-coordinate
353+ assert np .abs (y_2d - y_3d ) < 2 , \
354+ f"Round={ r } , CH={ ch } : is_volume=False has y={ y_2d :.0f} " \
355+ f"but is_volume=True has y={ y_3d :.0f} "
0 commit comments