Skip to content

Restore the Diameter parameter in the Cellpose-SAM worker - #170

Merged
arjunrajlab merged 4 commits into
masterfrom
fix/cellposesam-restore-diameter
Aug 28, 2026
Merged

Restore the Diameter parameter in the Cellpose-SAM worker#170
arjunrajlab merged 4 commits into
masterfrom
fix/cellposesam-restore-diameter

Conversation

@arjunrajlab

@arjunrajlab arjunrajlab commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

a3e4524 (shipped in #152) removed the Diameter field from the cellposesam inference worker. The stated reason was that "Cellpose-SAM trains and evaluates at native resolution" — but that conflates two different cellpose 4.x parameters. Inference does still accept a diameter, so this restores the field.

The evidence

Verified against the pinned cellpose==4.2.1.1 sdist and the deeptile source:

Parameter Status in v4.2.1.1
CellposeModel(diam_mean=...)constructor Genuinely deprecated: "not used in v4.0.1+. Ignoring this argument..."
CellposeModel.eval(diameter=...)eval-time Still fully honoured: if diameter is not None and diameter > 0: rescale = 30. / diameter

Our value reaches it: deeptile's cellpose_segmentation splats eval_parameters straight into model.eval(tile, **eval_parameters).

Coordinates are safe: with resample=True (cellpose's default, still live and undeprecated in 4.2.1.1) the flows are resized back to the original tile size, so polygons come out in native tile space regardless of any rescale.

Why the default is 30

The default is 30, cellpose's identity value — not a guess, and not an out-of-band 0 sentinel.

The 30 in rescale = 30. / diameter is a hardcoded literal inside CellposeModel.eval(), not a per-model diam_mean (v4 ignores that entirely). So Diameter = 30 gives rescale = 1.0 — precisely what diameter=None gives — and every downstream branch keys off rescale != 1.0 rather than off the diameter. The two are bit-for-bit equivalent, so the default run is byte-identical to today's behavior.

That matters beyond tidiness: because "off" is now a legitimate in-range value, the field can carry a meaningful minimum of 10. With 0-as-off it could not.

Behavior

Stored values are normalized by parse_diameter(), then the omission is keyed off the resulting rescale, so anything cellpose would treat as identity is dropped rather than passed through to no effect:

Stored value Used eval_parameters Rescale Warns?
absent / null / "" 30 {} 1.00
30 (default) 30 {} 1.00
"60" (numeric string) 60 {'diameter': 60.0} 0.50 yes
0 or negative as given {} 1.00
10 (old default) 10 {'diameter': 10.0} 3.00 yes
"abc", [30], True sendError + raise
  • Applies uniformly to base and custom models. Pre-removal it was applied to custom models only, and silently ignored for the base checkpoints.
  • Out-of-range values are honored, not clamped. min/max are UI hints; a saved config can carry anything. Silently substituting a different diameter would change the segmentation, so the worker passes it through and warns.
  • Warns on any active rescale, not just a large one — see below.

Code review round

A high-effort review found five issues; four were real and are fixed in ffb6119:

  1. float('') crash (medium). compute() guarded only None, but '' is a documented "unset" shape for saved interface values in this repo — annotation_tools.get_selected_channels treats None/''/{} alike. A config whose Diameter had been cleared crashed with a bare traceback before any sendError. Parsing moved into parse_diameter(), which resolves unset shapes to the identity and raises ValueError on anything non-numeric so compute() can sendError + raise. Booleans are rejected explicitly, since bool is an int subclass and float(True) would quietly mean a 30x upscale.

  2. Warning missed the case it existed for (medium). The warning was gated on tile_size * rescale > 2048. A pre-July-2026 config carries Diameter: 10 (3x upscale); at Tile Size 512 that is 1536 px — under the threshold, so the segmentation changed with no warning at all. It now fires on any rescale != 1.0.

  3. Tile-size-relative threshold (low). Tile Size can be 0, and 0 * rescale never trips any threshold. Fixed by the same change; the GPU-memory caveat is now appended to the warning rather than gating it.

  4. Incomplete test assertion (low). test_default_diameter_is_within_the_offered_range asserted only MIN <= DEFAULT, so raising DEFAULT to 500 would have kept it green. Now bounds both ends.

  5. Doc citations (low). Hardcoded upstream line numbers rot across cellpose versions; replaced with symbol names.

One finding I did not apply. The reviewer also called the resample=True note stale, on the grounds that resample is deprecated in v4 and the resize-back is now unconditional. Verified against the pinned cellpose==4.2.1.1: resample is a live parameter defaulting to True with no deprecation warning, and the resize back is gated on if resample:. The note is accurate and stands, now qualified with the version it was checked against.

Codex review round (e8132ba)

Codex found two issues, both real and both fixed. The first was serious and mine.

P1 — NameError on every compute request. My previous commit rewrote the Diameter warning using a text-range replacement whose end anchor was the build_cellpose_parameters( call. That range also swallowed the block in between: the preview-client construction, models_dir = MODELS_DIR, and the custom-model download. models_dir was left undefined at its only use, so every valid inference request would have raised NameError before the model was built, and custom Girder models would no longer download. Restored verbatim; the diff against feb696b is now limited to the intended changes.

This had no test coverage, which is the more important finding. entrypoint.py cannot be imported in the lightweight venv (cellpose, deeptile, annotation_client), so nothing here catches a NameError until the worker runs on a GPU host against real data. Added tests/test_entrypoint_names.py: it walks the AST and asserts every name loaded in a function is bound somewhere — local, argument, comprehension or loop target, import, module-level name, or builtin. Verified it fails with exactly {'compute': ['models_dir']} against the buggy revision and passes against the fix, so it is a real regression test rather than a tautology.

P2 — non-finite diameters. float() accepts "inf", "nan", and overflowing literals like "1e309". Neither is safe to forward, and they fail in opposite directions:

  • +inf passes cellpose's diameter > 0 guard and yields rescale = 30/inf = 0.0 — a degenerate zero-scale resize.
  • NaN fails that guard, so cellpose would not rescale at all, while this worker's own 30/nan != 1.0 check would report that it had.

parse_diameter() now rejects non-finite values via math.isfinite(), and diameter_rescale() treats them as 1.0 as defence in depth so the two can never disagree.

Final suites:

workers/annotations/cellposesam/tests    41 passed
workers/annotations/cellposesam_train    17 passed
annotation_utilities                     89 passed
worker_client                            21 passed

Compatibility note for old configs

Tool configs saved before July 2026 still hold the old Diameter: 10. Under the pre-removal code that was ignored for base checkpoints; with this change it rescales by 3x on every model — and it now sits exactly at the interface minimum, so the UI looks normal while the image is upscaled. This is why the warning is no longer gated on tile size. Called out in CELLPOSESAM.md; those configs should have their Diameter re-checked or set to 30.

Tests

Test-first across three red→green rounds. 25 new cases in tests/test_models_config.py: the identity values (absent / null / '' / 0 / negative / 30), that 29 and 31 still pass through so only the exact identity is dropped, numeric strings, non-numeric and boolean rejection, no-clamping, custom-model parity, that the checkpoint choice is untouched by the diameter, and that diameter_rescale() matches cellpose's 30/diameter at the default, the minimum, and the old default of 10.

workers/annotations/cellposesam/tests    36 passed
workers/annotations/cellposesam_train    17 passed
annotation_utilities                     89 passed
worker_client                            21 passed

Docker build / GPU inference was not run in this environment.

Docs

CELLPOSESAM.md gets the parameter row back plus a "Diameter and Rescaling" section: why 30 is the default and identity, the diam_mean vs. eval(diameter=) distinction that caused the original removal, coordinate safety, warning behavior and the GPU-memory caveat, the parse_diameter() table for out-of-range and malformed values, and the old-config warning.

cellposesam_train's "no diameter needed" statements are correct and left alonetrain_seg genuinely defaults to rescale=False. Training and inference differ here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp

arjunrajlab and others added 3 commits August 27, 2026 21:26
Commit a3e4524 removed the Diameter field from the Cellpose-SAM worker on
the premise that "Cellpose-SAM trains and evaluates at native resolution",
conflating two different cellpose 4.x parameters:

  - CellposeModel(diam_mean=...) -- genuinely deprecated in v4.0.1+
    (models.py:113-119 warns and ignores it).
  - CellposeModel.eval(diameter=...) -- still fully honoured. In the pinned
    cellpose==4.2.1.1, models.py:268-269 reads
        if diameter is not None and diameter > 0: rescale = 30. / diameter
    and deeptile forwards our eval_parameters straight into that call
    (deeptile/extensions/segmentation.py:36).

So the removal took away a working escape hatch for datasets whose objects
sit far outside the size range the checkpoint handles well. Cellpose's own
CLI keeps --diameter for exactly this reason (cli.py:101).

Restored as an optional field, off by default:

  - Default is 0 (native resolution), not the old default of 10. That keeps
    every current run byte-identical, and matches cellpose's own CLI default
    of None. The old default of 10 meant a 3x upscale, and was applied to
    custom models only while being ignored for the base checkpoints.
  - The value now applies uniformly to base and custom models.
  - compute() reads it with `.get('Diameter') or 0`, so configs saved while
    the field was absent (which have no such key) keep running natively.
  - A small Diameter enlarges each tile before inference, which is the easy
    way to exhaust GPU memory here. When the effective tile size would exceed
    2048 px the worker sends a warning rather than blocking, since the run may
    still fit.

With resample=True (cellpose's default) flows are resized back to the
original tile size (models.py:363-366), so annotation coordinates are
unaffected by the rescale.

Tests added for the off values (absent/None/0/negative), the positive
pass-through, custom-model parity, and that the checkpoint choice is
untouched by the diameter. CELLPOSESAM.md documents the parameter, the
GPU-memory caveat, and the pre-July-2026 config note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp
Follow-up to the restore: use 30 as the default and a minimum of 10, rather
than 0-as-off.

cellpose computes `rescale = 30. / diameter` with 30 as a hardcoded literal
(models.py:269) -- it is not a per-model `diam_mean`, which v4 ignores
entirely. So `diameter=30` yields rescale == 1.0, precisely what
`diameter=None` yields at models.py:267, and every downstream branch keys off
`rescale != 1.0` (models.py:344,354,363,365) or multiplies by it (line 315,
giving niter=200 either way). The two are bit-for-bit equivalent.

That makes 30 a real, in-range, self-describing "no rescaling" value, so the
field no longer needs an out-of-band 0 sentinel -- which in turn frees the
interface to carry a meaningful minimum. Changes:

  - default 0 -> 30 (DEFAULT_DIAMETER), min 0 -> 10 (MIN_DIAMETER), both
    exported from models_config so the interface and tests share one source
    of truth.
  - build_cellpose_parameters now keys the omission off the resulting rescale
    rather than the raw value, so anything cellpose would treat as identity
    (30, None, 0, negative) is dropped instead of passed through to no effect.
    At the default the worker issues exactly the call it made while the field
    was absent.
  - compute() maps an absent or null 'Diameter' to the identity rather than to
    0. Out-of-range values are honoured, not clamped: substituting a different
    diameter would silently change the segmentation.

MIN_DIAMETER caps interface-entered upscaling at 3x but does not replace the
GPU-memory warning, since a saved config can still carry a smaller value; the
warning text now points back to 30 rather than 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp
Addresses code review on the Diameter restore.

1. compute() called float() on the raw config value, guarding only None. '' is
   a documented "unset" shape for saved interface values in this repo (see
   annotation_tools.get_selected_channels, which treats None/''/{} alike), and
   float('') raises ValueError -- so a config whose Diameter had been cleared
   crashed with a bare traceback before any sendError. Parsing now lives in
   models_config.parse_diameter():
     - None / '' / whitespace  -> DEFAULT_DIAMETER (the identity)
     - numeric strings         -> parsed
     - out-of-range numbers    -> honoured, never clamped
     - non-numeric             -> ValueError, which compute() turns into
                                  sendError + raise
   Booleans are rejected explicitly: bool is an int subclass, so float(True)
   would quietly become 1.0 -- a 30x upscale.

2. The rescale warning was gated on `tile_size * rescale > 2048`, which stayed
   silent for exactly the configs it was meant to catch. A config saved before
   a3e4524 carries the old default of 10 (a 3x upscale that the pre-removal
   code applied to custom models only); with Tile Size 512 that is 1536 px --
   under the threshold, so the segmentation changed with no warning at all.
   Tile Size can also be 0, and 0 * rescale never trips any threshold.
   The warning now fires on any rescale != 1.0, names the factor and the
   effective tile size, and appends the GPU-memory caveat only when the tile
   would exceed 2048 px.

3. test_default_diameter_is_within_the_offered_range asserted only
   MIN <= DEFAULT, so raising DEFAULT to 500 would have kept it green. Now
   bounds both ends.

4. Docs: replaced hardcoded upstream line-number citations with symbol names
   (they rot across cellpose versions), corrected the claim that values below
   10 get "the GPU-memory warning" (they get the rescale warning), and
   documented the parse table.

Reviewer also flagged the `resample=True` note as stale on the grounds that
resample is deprecated and the resize-back is unconditional. Verified against
the pinned cellpose==4.2.1.1: resample is a live parameter defaulting to True
with no deprecation warning, and the resize back is gated on `if resample:`.
The note is accurate and stands, now qualified with the version it was checked
against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp
@arjunrajlab

Copy link
Copy Markdown
Collaborator Author

@codex review

Restores the Diameter parameter to the Cellpose-SAM inference worker, which a3e4524 removed on the mistaken premise that Cellpose-SAM ignores it. Verified against the pinned cellpose==4.2.1.1: only the constructor argument diam_mean is deprecated in v4; the eval-time diameter is still honoured and deeptile forwards our eval_parameters straight into model.eval().

Points worth your attention:

  1. The identity claim. The default is 30 because rescale = 30. / diameter uses a hardcoded 30 inside CellposeModel.eval(), making diameter=30 equivalent to diameter=None. build_cellpose_parameters() omits diameter from the eval call whenever the resulting rescale is 1.0. If that equivalence does not hold in some path I missed, the "default run is byte-identical" claim breaks.

  2. parse_diameter() in models_config.py — normalizes stored config values. Unset shapes (None/''/whitespace) resolve to the identity; out-of-range numbers are honoured rather than clamped; non-numeric values raise ValueError which compute() turns into sendError + raise. Booleans are rejected because bool is an int subclass. Interested in any input shape this still mishandles.

  3. Backward compatibility. Configs saved before July 2026 hold Diameter: 10 — a 3x upscale that the pre-removal code applied to custom models only and ignored for base checkpoints. The worker now warns on any rescale != 1.0 rather than gating on tile size, specifically because gating stayed silent for those configs at small tile sizes. Whether honouring the stored value (vs. migrating it) is the right call is a judgement I would like checked.

An earlier review round is already folded in (ffb6119); its findings and the one I rejected are summarised in the PR description.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffb6119143

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

info=detail)

cellpose_parameters = build_cellpose_parameters(
model, models_dir, diameter=diameter)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the model-directory setup before parameter construction

Every valid Cellpose-SAM compute request now reaches this call with models_dir undefined because the preceding initialization and custom-model download block was deleted, so inference raises NameError before cellpose_segmentation is constructed. Restore that setup; it is also required to download uncached custom Girder models.

Useful? React with 👍 / 👎.

raise ValueError(
f"Diameter must be a number in pixels, got the boolean {value!r}.")
try:
return float(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-finite diameter values

When a saved configuration or direct API request supplies values such as "Infinity", "1e309", or "NaN", float() accepts them instead of taking the documented malformed-value error path. Positive infinity produces a zero rescale that is forwarded to Cellpose and leads to invalid zero-scale image resizing, while NaN makes this worker report a rescale even though Cellpose's diameter > 0 guard treats it as inactive; reject non-finite results with math.isfinite().

Useful? React with 👍 / 👎.

…diameters

Addresses Codex review of ffb6119.

P1 -- NameError on every compute request. The previous commit rewrote the
Diameter warning block with a text-range replacement whose end anchor was the
`build_cellpose_parameters(` call, so it also swallowed the block in between:
the preview client construction, `models_dir = MODELS_DIR`, and the custom-model
download. `models_dir` was left undefined at its only use, so every valid
inference request would have raised NameError before the model was built, and
custom Girder models would no longer download. Block restored verbatim; the
diff against feb696b is now limited to the intended changes.

This class of bug had no test coverage: entrypoint.py cannot be imported in the
lightweight venv (cellpose, deeptile, annotation_client), so nothing catches a
NameError until the worker runs on a GPU host. Added
tests/test_entrypoint_names.py, which walks the AST and asserts every name
loaded in a function is bound somewhere -- as a local, argument, comprehension
or loop target, import, module-level name, or builtin. Verified it fails with
exactly {'compute': ['models_dir']} against the buggy revision and passes
against the fix.

P2 -- non-finite diameters. float() accepts 'inf', 'nan' and overflowing
literals like '1e309'. Neither is safe to forward: cellpose guards with
`diameter > 0`, so +inf passes and yields rescale = 30/inf = 0.0, a degenerate
zero-scale resize; NaN fails the guard, so cellpose would not rescale at all
even though this worker's own 30/nan != 1.0 check would report that it had.
parse_diameter now rejects non-finite values via math.isfinite(), and
diameter_rescale treats them as 1.0 as defence in depth so the two can never
disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp
@arjunrajlab

Copy link
Copy Markdown
Collaborator Author

Both Codex findings were real and are fixed in e8132ba. Thanks — the P1 was a serious one.

P1 (models_dir undefined): correct, and worse than "inference raises NameError" — the deleted range also took the preview-client construction and the custom-model download with it, so custom Girder models would not have downloaded either. My previous commit rewrote the warning block with a text-range replacement whose end anchor was the build_cellpose_parameters( call, and the range swallowed everything in between. Restored verbatim; the diff against feb696b is now limited to the intended changes.

The gap that let it through matters more than the bug. entrypoint.py cannot be imported in the lightweight test venv (cellpose, deeptile, annotation_client), so no test here would catch a NameError until the worker ran on a GPU host. Added tests/test_entrypoint_names.py, which walks the AST and asserts every name loaded in a function is bound somewhere — local, argument, comprehension/loop target, import, module-level name, or builtin. I confirmed it fails with exactly {'compute': ['models_dir']} against the buggy revision and passes against the fix, so it is a genuine regression test.

P2 (non-finite values): also correct, and the asymmetry you described is exactly right — +inf passes cellpose's diameter > 0 guard and produces a zero-scale resize, while NaN fails it, so cellpose would not rescale at all even though this worker's own 30/nan != 1.0 check would claim it had. parse_diameter() now rejects non-finite values with math.isfinite(), and diameter_rescale() treats them as 1.0 as defence in depth so the two can never disagree. Covered for inf, -inf, nan, and the overflowing '1e309' literal.

All suites green: 41 cellposesam, 17 cellposesam_train, 89 annotation_utilities, 21 worker_client. Docker build and GPU inference still have not been run in this environment.

@arjunrajlab
arjunrajlab merged commit 65909d4 into master Aug 28, 2026
1 check passed
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.

1 participant