Restore the Diameter parameter in the Cellpose-SAM worker - #170
Conversation
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
|
@codex review Restores the Points worth your attention:
An earlier review round is already folded in ( |
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
|
Both Codex findings were real and are fixed in P1 ( The gap that let it through matters more than the bug. P2 (non-finite values): also correct, and the asymmetry you described is exactly right — 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. |
Summary
a3e4524(shipped in #152) removed the Diameter field from thecellposesaminference 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.1sdist and thedeeptilesource:CellposeModel(diam_mean=...)— constructorCellposeModel.eval(diameter=...)— eval-timeif diameter is not None and diameter > 0: rescale = 30. / diameterOur value reaches it:
deeptile'scellpose_segmentationsplatseval_parametersstraight intomodel.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
0sentinel.The
30inrescale = 30. / diameteris a hardcoded literal insideCellposeModel.eval(), not a per-modeldiam_mean(v4 ignores that entirely). SoDiameter = 30givesrescale = 1.0— precisely whatdiameter=Nonegives — and every downstream branch keys offrescale != 1.0rather 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:eval_parametersnull/""{}30(default){}"60"(numeric string){'diameter': 60.0}0or negative{}10(old default){'diameter': 10.0}"abc",[30],TruesendError+ raisemin/maxare 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.Code review round
A high-effort review found five issues; four were real and are fixed in
ffb6119:float('')crash (medium).compute()guarded onlyNone, but''is a documented "unset" shape for saved interface values in this repo —annotation_tools.get_selected_channelstreatsNone/''/{}alike. A config whose Diameter had been cleared crashed with a bare traceback before anysendError. Parsing moved intoparse_diameter(), which resolves unset shapes to the identity and raisesValueErroron anything non-numeric socompute()cansendError+raise. Booleans are rejected explicitly, sinceboolis anintsubclass andfloat(True)would quietly mean a 30x upscale.Warning missed the case it existed for (medium). The warning was gated on
tile_size * rescale > 2048. A pre-July-2026 config carriesDiameter: 10(3x upscale); atTile Size512 that is 1536 px — under the threshold, so the segmentation changed with no warning at all. It now fires on anyrescale != 1.0.Tile-size-relative threshold (low).
Tile Sizecan be0, and0 * rescalenever trips any threshold. Fixed by the same change; the GPU-memory caveat is now appended to the warning rather than gating it.Incomplete test assertion (low).
test_default_diameter_is_within_the_offered_rangeasserted onlyMIN <= DEFAULT, so raisingDEFAULTto 500 would have kept it green. Now bounds both ends.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=Truenote stale, on the grounds thatresampleis deprecated in v4 and the resize-back is now unconditional. Verified against the pinnedcellpose==4.2.1.1:resampleis a live parameter defaulting toTruewith no deprecation warning, and the resize back is gated onif 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 —
NameErroron every compute request. My previous commit rewrote the Diameter warning using a text-range replacement whose end anchor was thebuild_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_dirwas left undefined at its only use, so every valid inference request would have raisedNameErrorbefore the model was built, and custom Girder models would no longer download. Restored verbatim; the diff againstfeb696bis now limited to the intended changes.This had no test coverage, which is the more important finding.
entrypoint.pycannot be imported in the lightweight venv (cellpose, deeptile, annotation_client), so nothing here catches aNameErroruntil the worker runs on a GPU host against real data. Addedtests/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:+infpasses cellpose'sdiameter > 0guard and yieldsrescale = 30/inf = 0.0— a degenerate zero-scale resize.NaNfails that guard, so cellpose would not rescale at all, while this worker's own30/nan != 1.0check would report that it had.parse_diameter()now rejects non-finite values viamath.isfinite(), anddiameter_rescale()treats them as1.0as defence in depth so the two can never disagree.Final suites:
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 inCELLPOSESAM.md; those configs should have their Diameter re-checked or set to30.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 thatdiameter_rescale()matches cellpose's30/diameterat the default, the minimum, and the old default of 10.Docker build / GPU inference was not run in this environment.
Docs
CELLPOSESAM.mdgets the parameter row back plus a "Diameter and Rescaling" section: why 30 is the default and identity, thediam_meanvs.eval(diameter=)distinction that caused the original removal, coordinate safety, warning behavior and the GPU-memory caveat, theparse_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 alone —train_seggenuinely defaults torescale=False. Training and inference differ here.🤖 Generated with Claude Code
https://claude.ai/code/session_01UT5Ckq9RHyhbytJEMg3Wzp