@@ -161,9 +161,13 @@ def _make_config(
161161 gpu : bool
162162 Run on the GPU.
163163 channels : list of int or None
164- Cellpose-3 ``[cyto, nucleus]`` channels; defaults to ``[0, 0]``.
164+ *Cellpose 3 only.* ``[cyto, nucleus]``, 1-based into the channel axis
165+ (0 = grayscale). ``None`` resolves per tile: ``[1, 2]`` when the tile
166+ carries two channels, else ``[0, 0]``. Cellpose 4 dropped this
167+ argument, so it is ignored there.
165168 channel_axis : int or None
166- Cellpose-4 channel axis.
169+ Axis of the tile holding channels, forwarded to ``eval`` for both
170+ Cellpose 3 and 4. ``None`` means single-channel tiles.
167171 diameter : float or None
168172 Expected cell diameter in pixels.
169173 do_3D : bool
@@ -179,7 +183,9 @@ def _make_config(
179183 return {
180184 "model" : model ,
181185 "gpu" : gpu ,
182- "channels" : channels if channels is not None else [0 , 0 ],
186+ # Left as None ("auto") rather than [0, 0]: _run only knows how many
187+ # channels a tile actually carries once it has one in hand.
188+ "channels" : channels ,
183189 "channel_axis" : channel_axis ,
184190 "diameter" : diameter ,
185191 "do_3D" : do_3D ,
@@ -284,37 +290,50 @@ def _run(block: np.ndarray, cellpose_dict: dict[str, Any]) -> np.ndarray:
284290 Integer (``int32``) label array of the same spatial shape.
285291 """
286292 do_3D = cellpose_dict ["do_3D" ]
293+ channel_axis = cellpose_dict .get ("channel_axis" )
294+ n_channels = block .shape [channel_axis ] if channel_axis is not None else 1
295+
296+ kwargs : dict [str , Any ] = dict (
297+ channel_axis = channel_axis ,
298+ diameter = cellpose_dict ["diameter" ],
299+ do_3D = do_3D ,
300+ ** cellpose_dict .get ("cellpose_kwargs" , {}),
301+ )
302+ if not _CELLPOSE_V4 :
303+ # Cellpose 4 (cpsam) dropped `channels` and reads whatever channels
304+ # the array carries; Cellpose 3 needs the cyto/nucleus pairing named.
305+ channels = cellpose_dict .get ("channels" )
306+ if channels is None :
307+ channels = [1 , 2 ] if n_channels >= 2 else [0 , 0 ]
308+ kwargs ["channels" ] = channels
287309
288- if _CELLPOSE_V4 :
289- kwargs : dict [str , Any ] = dict (
290- channel_axis = cellpose_dict .get ("channel_axis" ),
291- diameter = cellpose_dict ["diameter" ],
292- do_3D = do_3D ,
293- ** cellpose_dict .get ("cellpose_kwargs" , {}),
294- )
295- else :
296- kwargs = dict (
297- channels = cellpose_dict ["channels" ],
298- diameter = cellpose_dict ["diameter" ],
299- do_3D = do_3D ,
300- ** cellpose_dict .get ("cellpose_kwargs" , {}),
301- )
310+ # Where z sits once the channel axis is accounted for.
311+ z_axis = 1 if channel_axis == 0 else 0
302312
303313 if do_3D :
304- kwargs ["z_axis" ] = 0
314+ kwargs ["z_axis" ] = z_axis
305315 masks = _eval_with_oom_fallback (block , kwargs , cellpose_dict )
306316 return masks .astype ("int32" )
307317 else :
308318 # Squeeze singleton z so Cellpose gets a clean 2-D image
309- squeeze = block .ndim == 3 and block .shape [0 ] == 1
310- if block .ndim == 3 and not squeeze :
319+ spatial = list (block .shape )
320+ if channel_axis is not None :
321+ spatial .pop (channel_axis )
322+ squeeze = len (spatial ) == 3 and spatial [0 ] == 1
323+ if len (spatial ) == 3 and not squeeze :
311324 raise ValueError (
312- f"do_3D is False but this tile has { block . shape [0 ]} z-planes. "
325+ f"do_3D is False but this tile has { spatial [0 ]} z-planes. "
313326 "Cellpose would receive the stack with no z_axis and treat "
314327 "the leading axis as channels. Set do_3D: true, or tile with "
315328 "z=1 to segment plane by plane."
316329 )
317- img = block [0 ] if squeeze else block
330+ if squeeze :
331+ img = block [(slice (None ),) * z_axis + (0 ,)]
332+ # Dropping z shifts any channel axis that sat behind it.
333+ if channel_axis is not None and channel_axis > z_axis :
334+ kwargs ["channel_axis" ] = channel_axis - 1
335+ else :
336+ img = block
318337 masks = _eval_with_oom_fallback (img , kwargs , cellpose_dict )
319338 masks = masks .astype ("int32" )
320339 return masks [np .newaxis ] if squeeze else masks
0 commit comments