Source code for sc_tools.bm.mask_io
"""
Mask format adapters for segmentation benchmarking.
Loads segmentation masks from various tools into standardized labeled
numpy arrays (0=background, >0=cell_id).
"""
from __future__ import annotations
import logging
from pathlib import Path
import numpy as np
__all__ = [
"load_mask",
"load_cellpose_mask",
"load_stardist_mask",
"load_cellprofiler_mask",
"load_deepcell_mask",
"load_tiff_mask",
]
logger = logging.getLogger(__name__)
[docs]
def load_mask(path: str | Path, format: str = "auto") -> np.ndarray:
"""Load a segmentation mask, auto-detecting format from extension.
Parameters
----------
path
Path to the mask file.
format
One of ``"auto"``, ``"cellpose"``, ``"stardist"``, ``"cellprofiler"``,
``"deepcell"``, ``"tiff"``. ``"auto"`` detects from file extension.
Returns
-------
Labeled integer array (0=background, >0=cell_id).
"""
path = Path(path)
if format == "auto":
format = _detect_format(path)
loaders = {
"cellpose": load_cellpose_mask,
"stardist": load_stardist_mask,
"cellprofiler": load_cellprofiler_mask,
"deepcell": load_deepcell_mask,
"tiff": load_tiff_mask,
}
if format not in loaders:
raise ValueError(f"Unknown mask format: {format!r}. Choose from {list(loaders)}")
return loaders[format](path)
def _detect_format(path: Path) -> str:
"""Detect mask format from file extension and naming conventions."""
name = path.name.lower()
suffix = path.suffix.lower()
if name.endswith("_seg.npy") or suffix == ".npy":
return "cellpose"
if suffix in (".tif", ".tiff"):
return "tiff"
if suffix == ".npz":
return "stardist"
raise ValueError(
f"Cannot auto-detect mask format for {path.name!r}. Specify format explicitly."
)
[docs]
def load_cellpose_mask(path: str | Path) -> np.ndarray:
"""Load a Cellpose segmentation mask from ``*_seg.npy``.
Cellpose saves a dict with key ``"masks"`` containing the labeled array.
Also handles plain labeled arrays saved directly.
"""
path = Path(path)
data = np.load(path, allow_pickle=True)
if isinstance(data, np.ndarray) and data.ndim == 0:
# dict saved via np.save
item = data.item()
if isinstance(item, dict) and "masks" in item:
mask = np.asarray(item["masks"])
else:
raise ValueError(f"Cellpose file does not contain 'masks' key: {path}")
elif isinstance(data, np.ndarray) and data.ndim >= 2:
mask = data
else:
raise ValueError(f"Unexpected Cellpose file structure: {path}")
return _validate_mask(mask, path)
[docs]
def load_stardist_mask(path: str | Path) -> np.ndarray:
"""Load a StarDist label image from ``.tif``/``.tiff`` or ``.npz``.
For ``.npz`` files, expects a ``"labels"`` key.
"""
path = Path(path)
suffix = path.suffix.lower()
if suffix == ".npz":
data = np.load(path)
if "labels" in data:
mask = data["labels"]
else:
# Try first array
keys = list(data.keys())
if keys:
mask = data[keys[0]]
else:
raise ValueError(f"Empty npz file: {path}")
elif suffix in (".tif", ".tiff"):
mask = _load_tiff(path)
else:
raise ValueError(f"Unsupported StarDist file extension: {suffix}")
return _validate_mask(mask, path)
[docs]
def load_cellprofiler_mask(path: str | Path) -> np.ndarray:
"""Load a CellProfiler label image from TIFF."""
return load_tiff_mask(path)
[docs]
def load_deepcell_mask(path: str | Path) -> np.ndarray:
"""Load a DeepCell/Mesmer output mask from TIFF or NPZ.
DeepCell output is typically ``(1, H, W, 1)``; squeeze extra dims.
"""
path = Path(path)
suffix = path.suffix.lower()
if suffix == ".npz":
data = np.load(path)
keys = list(data.keys())
mask = data[keys[0]] if keys else np.array([])
elif suffix in (".tif", ".tiff"):
mask = _load_tiff(path)
else:
mask = np.load(path)
mask = np.squeeze(mask)
return _validate_mask(mask, path)
[docs]
def load_tiff_mask(path: str | Path) -> np.ndarray:
"""Load a labeled TIFF mask (generic loader)."""
path = Path(path)
mask = _load_tiff(path)
return _validate_mask(mask, path)
def _load_tiff(path: Path) -> np.ndarray:
"""Load a TIFF file using tifffile."""
try:
import tifffile
except ImportError as e:
raise ImportError(
"tifffile is required for TIFF mask loading. "
"Install with: pip install sc-tools[benchmark]"
) from e
return tifffile.imread(str(path))
def _validate_mask(mask: np.ndarray, path: Path) -> np.ndarray:
"""Validate and standardize a mask array."""
if mask.ndim != 2:
if mask.ndim > 2:
mask = np.squeeze(mask)
if mask.ndim != 2:
raise ValueError(f"Mask must be 2D, got {mask.ndim}D array from {path}")
# Ensure integer labels
if not np.issubdtype(mask.dtype, np.integer):
mask = mask.astype(np.int32)
n_cells = len(np.unique(mask)) - (1 if 0 in mask else 0)
logger.debug("Loaded mask from %s: shape=%s, n_cells=%d", path, mask.shape, n_cells)
return mask