aicell-lab / aicell-lab/bioengine

cellpose4-runner blocks samples that already fit a valid input size, so symmetric halo padding invents cells around the image border

Open
#162 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
9
Forks
4
Avg merge
1d 19h
Merged PRs (30d)
5

Description

Component: apps/cellpose4-runner (v0.4.0)


Summary

cellpose4-runner always routes inference through predict_sample_with_blocking, including for samples that are already a valid whole input for the model. Blocking pads the outer image boundary to supply the model's declared halo, and the model then segments that padded content, producing extra partial cells in a ring around the image edge that bioimageio.core's own reference path does not produce.

The effect scales inversely with image size. On a 256x256 sample a halo: 16 ring is about 23% of the image area, so it is significant for annotation crops and small fields of view, and negligible on a large image.

Evidence

Comparing the runner's output against each model's packaged test_output.npy:

model declared objects declared touching border runner objects runner touching border foreground IoU
famous-sheep 18 4 24 10 0.916
passionate-bug 18 4 22 8 0.935
idealistic-eagle 99 0 97 0 0.967

Every extra object on famous-sheep and passionate-bug is edge-clipped, 29 to 442 px, all above the model's min_size: 15. The interior objects are unaffected: the 18 declared objects match at a mean best-match IoU of 0.9925 with none split or merged.

This is not a packaging defect. bioimageio.core reproduces test_output.npy bitwise for both models.

Root cause

The two prediction paths differ by design in the halo ring, and the runner takes the one that does not match the reference.

  • _resource_tests.py:886 builds the reference with predict_sample_without_blocking: a single pass, no padding, halo ignored because there is nothing to stitch.
  • deployment.py:281-306 calls predict_sample_with_blocking. block.py:56 pads the outer boundary with pad_mode, defaulting to "symmetric" (sample.py:406), to supply the halo.

Both models declare:

inputs:  [{id: y, type: space, size: {min: 64, step: 8}}, {id: x, ...}]
outputs: [{id: y, type: space, halo: 16, ...}, {id: x, type: space, halo: 16, ...}]

Because the input size is parameterized as min: 64, step: 8, a 256x256 sample is already a valid whole input and does not need to be blocked at all. Blocking it mirrors 16 px of image content outward, the model segments the mirrored strip, and those detections survive into the result.

bioimageio.core is behaving as specified here and should not change. halo: 16 is the model's own declaration that its boundary output is unreliable when tiled, symmetric outer padding is the standard treatment for every tiled model, and altering it would move output for every haloed model in the collection.

Proposed fix

Skip blocking when the sample already fits a valid input size for the loaded model, and keep predict_sample_with_blocking for samples that genuinely exceed it. Small images then take the same path as the reference and reproduce it.

Do not filter edge-touching masks as a workaround. The declared output legitimately contains 4 border-touching objects on both models, so remove_edge_masks or a bbox-touches-boundary filter would delete real cells.

Open question before implementing

deployment.py:281-286 states that running unblocked "pads it by the halo and breaks the ViT positional embed". If that holds at 256x256 for these models, the proposed fix does not apply and the alternative is to suppress halo cropping at the outer boundary only, keeping it at internal seams. bioimageio.core's blocking may not currently expose that as an option, in which case this needs an upstream discussion rather than a runner-local change.

Reproduction
  1. Run famous-sheep through cellpose4-runner.infer on the sample input packaged with the model.
  2. Load the packaged test_output.npy.
  3. Count connected components in each and count how many touch the image boundary.

Expect 18 objects with 4 touching the border from the packaged output, and 24 with 10 touching from the runner.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/cellpose4-runner and read deployment.py:281-306, then compare the reference path in _resource_tests.py:886 with padding in block.py:56 and sample.py:406. Reproduce the famous-sheep result against test_output.npy and verify whether valid whole inputs can use the unblocked path without breaking the stated positional-embedding constraint. Done means the runner matches the packaged reference for these samples while larger inputs still use blocking.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.