Skip to content

Fix BlobDetector round/channel assignment when is_volume=False#2160

Merged
shachafl merged 3 commits intocopilot/fix-blobdetector-resultsfrom
copilot/fix-blobdetector-rounds-channels
Nov 24, 2025
Merged

Fix BlobDetector round/channel assignment when is_volume=False#2160
shachafl merged 3 commits intocopilot/fix-blobdetector-resultsfrom
copilot/fix-blobdetector-rounds-channels

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 24, 2025

When BlobDetector runs with is_volume=False and no reference image, spots are assigned to incorrect (round, channel) pairs. With 2 rounds and 3 channels, a spot at R0C1 gets assigned to R1C0 instead.

Root cause: The code sorted merged (round, ch) keys lexicographically then paired them by index with selectors from _iter_axes({ROUND, CH}). Since _iter_axes() iterates in channel-major order (c=0,r=0), (c=0,r=1), (c=1,r=0)... while sorted() produces round-major order (r=0,c=0), (r=0,c=1), (r=1,c=0)..., the pairing mismatches.

Changes:

  • Iterate directly over _iter_axes({ROUND, CH}) and look up each (r, ch) in the merged tables instead of sorting keys
  • Add regression test with 2 rounds × 3 channels verifying spots land in correct locations for both is_volume=True/False
# Before: sorts keys and pairs by index
r_chs = sorted([*merged_z_tables])
selectors = list(image_stack._iter_axes({Axes.ROUND, Axes.CH}))
for i, (r, ch) in enumerate(r_chs):
    new.append((results, selectors[i]))  # Mismatch!

# After: iterate in _iter_axes order
for selector in image_stack._iter_axes({Axes.ROUND, Axes.CH}):
    r, ch = selector[Axes.ROUND], selector[Axes.CH]
    if (r, ch) in merged_z_tables:
        new.append((results, selector))  # Correct pairing
Original prompt

This section details on the original issue you should resolve

<issue_title>Blobdetector gives incorrect rounds and channels for spots when run with no reference image and is_volume=False</issue_title>
<issue_description>#### Description
Spots results from starfish/core/spots/FindSpots/blob.py are saved with incorrect round and channels when using no reference image and "is_volume=False" (either a 2d data or 3d data that is processed as a set of 2d tiles).

Working on #2154 I also discovered a different issue with different results between "is_volume=True" and "is_volume=False" when no reference image is used. It is due to the way spots are merged from the same round/channel but different z slices:

# If not a volume, merge spots from the same round/channel but different z slices
if not self.is_volume:
merged_z_tables = defaultdict(pd.DataFrame) # type: ignore
for i in range(len(spot_attributes_list)):
spot_attributes_list[i][0].spot_attrs.data['z'] = \
spot_attributes_list[i][1]['z']
r = spot_attributes_list[i][1][Axes.ROUND]
ch = spot_attributes_list[i][1][Axes.CH]
merged_z_tables[(r, ch)] = pd.concat(
[merged_z_tables[(r, ch)], spot_attributes_list[i][0].spot_attrs.data])
new = []
r_chs = sorted([*merged_z_tables])
selectors = list(image_stack._iter_axes({Axes.ROUND, Axes.CH}))
for i, (r, ch) in enumerate(r_chs):
merged_z_tables[(r, ch)]['spot_id'] = range(len(merged_z_tables[(r, ch)]))
spot_attrs = SpotAttributes(merged_z_tables[(r, ch)].reset_index(drop=True))
new.append((PerImageSliceSpotResults(spot_attrs=spot_attrs, extras=None),
selectors[i]))
spot_attributes_list = new
results = SpotFindingResults(imagestack_coords=image_stack.xarray.coords,
log=image_stack.log,
spot_attributes_list=spot_attributes_list)

Bottom line, the {round,ch}I pairs are saved incorrectly ({0,1} should be {1,0}, and so on).

Steps/Code to Reproduce

Run the script examples/how_to/assess_spotfindingresults.py, without a reference image and once with "is_volume=True" and once with "is_volume=False".

# run blob detector on dots (reference image with every spot)
bd = FindSpots.BlobDetector(
    min_sigma=1,
    max_sigma=3,
    num_sigma=10,
    threshold=0.01,
    #is_volume=False,
    is_volume=True, ## DEBUG1 - True instead of False
    measurement_type='mean',
)
#spots = bd.run(image_stack=imgs, reference_image=dots)
spots = bd.run(image_stack=imgs)  ## DEBUG2 - No reference image

# Start Debugging
for r in range(imgs.num_rounds):
    for ch in range(imgs.num_chs):
        data=spots[{Axes.CH:ch, Axes.ROUND:r}].spot_attrs.data
        print(f"\nRound:{r}, Ch:{ch}")
        print(data.sort_values("intensity", ascending=False).head(5))
# End Debugging

Expected Results

The same results for "is_volume=True" and "is_volume=False" when the same data is presented as 2d (y,x) or 3d with a single z-plane (1,y,x), especially as we now squeeze the single z-plane and send blob_log() the same 2d data.

Actual Results

One example result for the case of "is_volume=True":

Round:0, Ch:1
     intensity  z    y    x  radius  spot_id
217   0.225376  0  938  428     2.0      217
13    0.223259  0  890  344     2.0       13
115   0.222446  0  158  979     2.0      115
36    0.221881  0  309  586     2.0       36
70    0.221012  0  446  212     3.0       70

One example result for the case of "is_volume=False":

Round:0, Ch:1
    intensity  z    y     x  radius  spot_id
0    0.196602  0  472  1174     2.0        0
1    0.188165  0  452  1093     2.0        1
15   0.186682  0  944   338     2.0       15
10   0.185512  0  549   782     2.0       10
34   0.184856  0    7   980     2.0       34

</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Fix incorrect rounds and channels in blobdetector results Fix BlobDetector round/channel assignment when is_volume=False Nov 24, 2025
Copilot AI requested a review from shachafl November 24, 2025 07:58
@shachafl shachafl marked this pull request as ready for review November 24, 2025 09:12
@shachafl shachafl merged commit 2e542d0 into copilot/fix-blobdetector-results Nov 24, 2025
@shachafl shachafl deleted the copilot/fix-blobdetector-rounds-channels branch November 24, 2025 10:26
shachafl added a commit that referenced this pull request Nov 24, 2025
…across dimensionalities (#2154)

* Fix BlobDetector 2D indexing bug causing incorrect y-values and radius

* Squeeze singleton z-dimension before blob detection for consistency

When data_image has shape (1, y, x), squeeze it to (y, x) before calling
blob_log to ensure consistent results. This prevents blob_log from treating
singleton z-dimensions as 3D, which produces slightly different detection
results compared to true 2D images.

After detection, restore the original shape for consistent intensity indexing.

* Refactor singleton z-dimension handling for clarity

Use separate variable data_image_for_detection to make it clearer that
the squeezed data is only used for blob detection, while the original
data_image is used for intensity extraction. This should make debugging
easier and the code more maintainable.

* Fix anisotropic sigma detection logic using data dimensionality

Changed from using fitted_blobs_array.shape[1] to check data_image_for_detection.ndim
to determine if 2D or 3D blob detection was performed. This correctly handles:
- 3D with scalar sigma: (n, 4) = [z, y, x, sigma]
- 2D with anisotropic sigma: (n, 4) = [y, x, sigma_y, sigma_x]
- 3D with anisotropic sigma: (n, 6) = [z, y, x, sigma_z, sigma_y, sigma_x]

For anisotropic sigma, radius is computed as average of sigma values.

* Fix BlobDetector round/channel assignment when is_volume=False (#2160)

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: shachafl <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blobdetector gives incorrect rounds and channels for spots when run with no reference image and is_volume=False

2 participants