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
- Run
famous-sheep through cellpose4-runner.infer on the sample input packaged with the model.
- Load the packaged
test_output.npy.
- 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.
Component:
apps/cellpose4-runner(v0.4.0)Summary
cellpose4-runneralways routes inference throughpredict_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 declaredhalo, and the model then segments that padded content, producing extra partial cells in a ring around the image edge thatbioimageio.core's own reference path does not produce.The effect scales inversely with image size. On a 256x256 sample a
halo: 16ring 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: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.corereproducestest_output.npybitwise 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:886builds the reference withpredict_sample_without_blocking: a single pass, no padding, halo ignored because there is nothing to stitch.deployment.py:281-306callspredict_sample_with_blocking.block.py:56pads the outer boundary withpad_mode, defaulting to"symmetric"(sample.py:406), to supply the halo.Both models declare:
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.coreis behaving as specified here and should not change.halo: 16is 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_blockingfor 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_masksor a bbox-touches-boundary filter would delete real cells.Open question before implementing
deployment.py:281-286states 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
famous-sheepthroughcellpose4-runner.inferon the sample input packaged with the model.test_output.npy.Expect 18 objects with 4 touching the border from the packaged output, and 24 with 10 touching from the runner.