Skip to content

Commit 848fc5e

Browse files
Release v0.16.5: CI: docker build fix with cellpose without deepcell and arm64/amd64 build
1 parent 422b823 commit 848fc5e

File tree

6 files changed

+114
-33
lines changed

6 files changed

+114
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Changed
1212

13-
- fixed new docker build distribution, where cellpose does not include deepcell + proper arm64/amd64 build
13+
- fixed new docker build distribution, where cellpose does not include deepcell, added lazy loading + proper arm64/amd64 build
14+
1415

1516
## [0.16.4] - 2026-03-26
1617

steinbock/_cli/_cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from ..utils._cli import utils_cmd_group
1010
from .apps import apps_cmd_group
1111
from .utils import OrderedClickGroup
12-
from .visualization import view_cmd
1312

1413

1514
@click.group(name="steinbock", cls=OrderedClickGroup)
@@ -18,6 +17,14 @@ def steinbock_cmd_group():
1817
pass
1918

2019

20+
@click.command(name="view")
21+
@click.pass_context
22+
def view_cmd(ctx, *args, **kwargs):
23+
from .visualization import view_cmd as _view_cmd
24+
25+
return ctx.invoke(_view_cmd, *args, **kwargs)
26+
27+
2128
steinbock_cmd_group.add_command(preprocess_cmd_group)
2229
steinbock_cmd_group.add_command(classify_cmd_group)
2330
steinbock_cmd_group.add_command(segment_cmd_group)

steinbock/_cli/visualization.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from .. import io
77
from .._steinbock import SteinbockException
88
from .._steinbock import logger as steinbock_logger
9-
from ..visualization import view
109
from .utils import catch_exception
1110

1211

@@ -48,13 +47,13 @@
4847
@click_log.simple_verbosity_option(logger=steinbock_logger)
4948
@catch_exception(handle=SteinbockException)
5049
def view_cmd(img_dir, mask_dirs, panel_file, pixel_size_um, img_file_name):
50+
from ..visualization import view
51+
5152
img = io.read_image(Path(img_dir) / img_file_name, native_dtype=True)
5253
masks = None
5354
if len(mask_dirs) > 0:
5455
masks = {
55-
f"Mask ({Path(mask_dir).name})": io.read_mask(
56-
Path(mask_dir) / img_file_name, native_dtype=True
57-
)
56+
f"Mask ({Path(mask_dir).name})": io.read_mask(Path(mask_dir) / img_file_name, native_dtype=True)
5857
for mask_dir in mask_dirs
5958
}
6059
channel_names = None

steinbock/segmentation/_cli/cellpose.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,32 @@
88
from ..._cli.utils import catch_exception, logger
99
from ..._steinbock import SteinbockException
1010
from ..._steinbock import logger as steinbock_logger
11-
from .. import cellpose
1211

13-
cellpose_cli_available = cellpose.cellpose_available
1412

13+
def _get_cellpose_module():
14+
try:
15+
from .. import cellpose as cellpose_module
16+
except ImportError as e:
17+
raise click.ClickException("The 'cellpose' command requires the optional Cellpose dependencies.") from e
1518

16-
@click.command(
17-
name="cellpose", help="Run an object segmentation batch using CellposeSAM"
18-
)
19+
if not getattr(cellpose_module, "cellpose_available", False):
20+
raise click.ClickException(
21+
"The 'cellpose' command is not available because Cellpose dependencies "
22+
"are not installed in this environment."
23+
)
24+
25+
return cellpose_module
26+
27+
28+
try:
29+
from .. import cellpose as _cellpose_probe
30+
31+
cellpose_cli_available = bool(getattr(_cellpose_probe, "cellpose_available", False))
32+
except ImportError:
33+
cellpose_cli_available = False
34+
35+
36+
@click.command(name="cellpose", help="Run an object segmentation batch using CellposeSAM")
1937
@click.option(
2038
"--img",
2139
"img_dir",
@@ -193,14 +211,22 @@ def cellpose_cmd(
193211
tile_overlap,
194212
mask_dir,
195213
):
214+
cellpose = _get_cellpose_module()
215+
196216
channel_groups = None
197217
if Path(panel_file).is_file():
198218
panel = io.read_panel(panel_file)
199219
if "cellpose" in panel and panel["cellpose"].notna().any():
200220
channel_groups = panel["cellpose"].values
201-
aggr_func = getattr(np, aggr_func_name)
221+
222+
try:
223+
aggr_func = getattr(np, aggr_func_name)
224+
except AttributeError as e:
225+
raise click.ClickException(f"Invalid numpy aggregation function: {aggr_func_name}") from e
226+
202227
img_files = io.list_image_files(img_dir)
203228
Path(mask_dir).mkdir(exist_ok=True)
229+
204230
for img_file, mask, _, _ in cellpose.try_segment_objects(
205231
img_files,
206232
channelwise_minmax=channelwise_minmax,

steinbock/segmentation/_cli/deepcell.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,52 @@
88
from ..._cli.utils import catch_exception, logger
99
from ..._steinbock import SteinbockException
1010
from ..._steinbock import logger as steinbock_logger
11-
from .. import deepcell
1211

13-
if deepcell.deepcell_available:
14-
import yaml
1512

16-
deepcell_cli_available = deepcell.deepcell_available
13+
def _get_deepcell_module():
14+
try:
15+
from .. import deepcell as deepcell_module
16+
except ImportError as e:
17+
raise click.ClickException("The 'deepcell' command requires the optional DeepCell dependencies.") from e
1718

18-
_applications = {
19-
"mesmer": deepcell.Application.MESMER,
20-
}
19+
if not getattr(deepcell_module, "deepcell_available", False):
20+
raise click.ClickException(
21+
"The 'deepcell' command is not available because DeepCell dependencies "
22+
"are not installed in this environment."
23+
)
24+
25+
return deepcell_module
26+
27+
28+
def _get_yaml_module():
29+
try:
30+
import yaml
31+
except ImportError as e:
32+
raise click.ClickException("The 'deepcell' command requires PyYAML.") from e
33+
34+
return yaml
35+
36+
37+
def _get_applications():
38+
deepcell_module = _get_deepcell_module()
39+
return {
40+
"mesmer": deepcell_module.Application.MESMER,
41+
}
42+
43+
44+
try:
45+
from .. import deepcell as _deepcell_probe
46+
47+
deepcell_cli_available = bool(getattr(_deepcell_probe, "deepcell_available", False))
48+
except ImportError:
49+
deepcell_cli_available = False
2150

2251

2352
@click.command(name="deepcell", help="Run an object segmentation batch using DeepCell")
2453
@click.option(
2554
"--app",
2655
"application_name",
27-
type=click.Choice(list(_applications.keys()), case_sensitive=True),
56+
type=click.Choice(["mesmer"], case_sensitive=True),
2857
show_choices=True,
2958
default="mesmer",
3059
show_default=True,
@@ -138,36 +167,54 @@ def deepcell_cmd(
138167
postprocess_file,
139168
mask_dir,
140169
):
170+
deepcell = _get_deepcell_module()
171+
applications = _get_applications()
172+
141173
channel_groups = None
142174
if Path(panel_file).is_file():
143175
panel = io.read_panel(panel_file)
144176
if "deepcell" in panel and panel["deepcell"].notna().any():
145177
channel_groups = panel["deepcell"].values
146-
aggr_func = getattr(np, aggr_func_name)
178+
179+
try:
180+
aggr_func = getattr(np, aggr_func_name)
181+
except AttributeError as e:
182+
raise click.ClickException(f"Invalid numpy aggregation function: {aggr_func_name}") from e
183+
147184
img_files = io.list_image_files(img_dir)
185+
148186
model = None
149187
if model_path_or_name is not None:
150-
from tensorflow.keras.models import load_model # type: ignore
151-
152-
if Path(model_path_or_name).exists():
153-
model = load_model(model_path_or_name, compile=False)
154-
elif Path(keras_model_dir).joinpath(model_path_or_name).exists():
155-
model = load_model(
156-
Path(keras_model_dir).joinpath(model_path_or_name),
157-
compile=False,
158-
)
188+
try:
189+
from tensorflow.keras.models import load_model # type: ignore
190+
except ImportError as e:
191+
raise click.ClickException("TensorFlow/Keras is required to load a DeepCell model.") from e
192+
193+
model_path = Path(model_path_or_name)
194+
keras_model_path = Path(keras_model_dir).joinpath(model_path_or_name)
195+
196+
if model_path.exists():
197+
model = load_model(model_path, compile=False)
198+
elif keras_model_path.exists():
199+
model = load_model(keras_model_path, compile=False)
200+
159201
preprocess_kwargs = None
160202
if preprocess_file is not None:
203+
yaml = _get_yaml_module()
161204
with Path(preprocess_file).open() as f:
162205
preprocess_kwargs = yaml.load(f, yaml.Loader)
206+
163207
postprocess_kwargs = None
164208
if postprocess_file is not None:
209+
yaml = _get_yaml_module()
165210
with Path(postprocess_file).open() as f:
166211
postprocess_kwargs = yaml.load(f, yaml.Loader)
212+
167213
Path(mask_dir).mkdir(exist_ok=True)
214+
168215
for img_file, mask in deepcell.try_segment_objects(
169216
img_files,
170-
_applications[application_name],
217+
applications[application_name],
171218
model=model,
172219
channelwise_minmax=channelwise_minmax,
173220
channelwise_zscore=channelwise_zscore,

steinbock/visualization.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Dict, Optional, Sequence
22

3-
import napari
43
import numpy as np
54

65

@@ -11,7 +10,9 @@ def view(
1110
pixel_size_um: float = 1.0,
1211
run: bool = True,
1312
**viewer_kwargs,
14-
) -> Optional[napari.Viewer]:
13+
) -> Optional["napari.Viewer"]:
14+
import napari
15+
1516
viewer = napari.Viewer(**viewer_kwargs)
1617
viewer.axes.visible = True
1718
viewer.dims.axis_labels = ("y", "x")

0 commit comments

Comments
 (0)