Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ba17bc0
mvp
timtreis Sep 13, 2025
06be11c
mvp
timtreis Sep 13, 2025
9ef97fd
for notebook
timtreis Sep 14, 2025
affeff3
added small plotting func
timtreis Sep 14, 2025
db9f34a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 14, 2025
6d36a8e
Merge branch 'main' into bugfix/issue1034-function-to-qc-he-images
timtreis Oct 27, 2025
70c70d0
Merge branch 'main' into bugfix/issue1034-function-to-qc-he-images
timtreis Oct 27, 2025
f06e89a
refactor
timtreis Oct 29, 2025
71bc297
added images from runner
timtreis Oct 29, 2025
80ea59e
made logging robust to different loggers
timtreis Oct 29, 2025
e609d06
restored logging
timtreis Oct 29, 2025
78d6549
restored logging
timtreis Oct 29, 2025
df21e40
removed dead code
timtreis Oct 29, 2025
5c72645
edge cases that caused scverse CI to fail
timtreis Oct 29, 2025
5eebcf2
Merge branch 'main' into bugfix/issue1034-function-to-qc-he-images
timtreis Oct 29, 2025
d6f2ed4
bump
timtreis Oct 29, 2025
6a9fb54
make_tile_grid function
timtreis Oct 29, 2025
5da7432
mvp
timtreis Oct 30, 2025
88047d7
tiling functions
timtreis Nov 24, 2025
8f23aac
Merge branch 'main' into feature/hne_tiling_from_qc_base
timtreis Nov 24, 2025
aebb1c9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 24, 2025
cdcfeed
mypy fixes
timtreis Nov 24, 2025
9d4a1d5
bugfix
timtreis Nov 24, 2025
ce2da2b
updated tests for correct plot
timtreis Nov 24, 2025
89b18b9
images from runner
timtreis Nov 24, 2025
5005157
bugfix + more tests
timtreis Nov 24, 2025
09fdcb0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 24, 2025
ffd5760
images from runner
timtreis Nov 24, 2025
1009068
updated test
timtreis Nov 24, 2025
d8552ad
cleaned up
timtreis Nov 24, 2025
992b3b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 24, 2025
47715be
updated docstrings
timtreis Nov 24, 2025
fb68599
Merge branch 'main' into feature/hne_tiling_from_qc_base
selmanozleyen Nov 29, 2025
ad6144e
adressed reviewer feedback
timtreis Dec 1, 2025
0a197e9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 1, 2025
4158e43
removed dead code
timtreis Dec 1, 2025
e1be3a0
removed dead code
timtreis Dec 1, 2025
623ab3e
renamed enum
timtreis Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/squidpy/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
from multiprocessing import Manager, cpu_count
from queue import Queue
from threading import Thread
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal

import joblib as jl
import numba
import numpy as np
import xarray as xr
from spatialdata.models import Image2DModel, Labels2DModel

__all__ = ["singledispatchmethod", "Signal", "SigQueue", "NDArray", "NDArrayA"]
Expand Down Expand Up @@ -372,3 +373,17 @@ def _yx_from_shape(shape: tuple[int, ...]) -> tuple[int, int]:
return shape[1], shape[2]

raise ValueError(f"Unsupported shape {shape}. Expected (y, x) or (c, y, x).")


def _ensure_dim_order(img_da: xr.DataArray, order: Literal["cyx", "yxc"] = "yxc") -> xr.DataArray:
"""
Ensure dims are in the requested order and that a 'c' dim exists.
Only supports images with dims subset of {'y','x','c'}.
"""
dims = list(img_da.dims)
if "y" not in dims or "x" not in dims:
raise ValueError(f'Expected dims to include "y" and "x". Found dims={dims}')
if "c" not in dims:
img_da = img_da.expand_dims({"c": [0]})
# After possible expand, just transpose to target
return img_da.transpose(*tuple(order))
5 changes: 2 additions & 3 deletions src/squidpy/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from __future__ import annotations

from . import im
from .im._detect_tissue import detect_tissue
from . import im, pl

__all__ = ["detect_tissue", "im"]
__all__ = ["im", "pl"]
9 changes: 8 additions & 1 deletion src/squidpy/experimental/im/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@
FelzenszwalbParams,
detect_tissue,
)
from ._make_tiles import make_tiles, make_tiles_from_spots

__all__ = ["detect_tissue", "BackgroundDetectionParams", "FelzenszwalbParams"]
__all__ = [
"BackgroundDetectionParams",
"FelzenszwalbParams",
"detect_tissue",
"make_tiles",
"make_tiles_from_spots",
]
46 changes: 21 additions & 25 deletions src/squidpy/experimental/im/_detect_tissue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@
from dataclasses import dataclass
from typing import Literal

import dask.array as da
import numpy as np
import spatialdata as sd
import xarray as xr
from dask.base import is_dask_collection
from dask_image.ndinterp import affine_transform as da_affine
from skimage import measure
from skimage.filters import gaussian, threshold_otsu
from skimage.morphology import binary_closing, disk, remove_small_holes
from skimage.segmentation import felzenszwalb
from skimage.util import img_as_float
from spatialdata._logging import logger as logg
from spatialdata._logging import logger
from spatialdata.models import Labels2DModel
from spatialdata.transformations import get_transformation

from squidpy._utils import _get_scale_factors, _yx_from_shape
from squidpy._utils import _ensure_dim_order, _get_scale_factors, _yx_from_shape

from ._utils import _flatten_channels, _get_image_data
from ._utils import _flatten_channels, _get_element_data


class DETECT_TISSUE_METHOD(enum.Enum):
class DetectTissueMethod(enum.Enum):
OTSU = enum.auto()
FELZENSZWALB = enum.auto()

Expand Down Expand Up @@ -70,7 +72,7 @@ def detect_tissue(
image_key: str,
*,
scale: str = "auto",
method: DETECT_TISSUE_METHOD | str = DETECT_TISSUE_METHOD.OTSU,
method: DetectTissueMethod | str = DetectTissueMethod.OTSU,
channel_format: Literal["infer", "rgb", "rgba", "multichannel"] = "infer",
background_detection_params: BackgroundDetectionParams | None = None,
corners_are_background: bool = True,
Expand Down Expand Up @@ -98,8 +100,8 @@ def detect_tissue(
method
Tissue detection method. Valid options are:

- `DETECT_TISSUE_METHOD.OTSU` or `"otsu"` - Otsu thresholding with background detection.
- `DETECT_TISSUE_METHOD.FELZENSZWALB` or `"felzenszwalb"` - Felzenszwalb superpixel segmentation.
- `DetectTissueMethod.OTSU` or `"otsu"` - Otsu thresholding with background detection.
- `DetectTissueMethod.FELZENSZWALB` or `"felzenszwalb"` - Felzenszwalb superpixel segmentation.

channel_format
Expected format of image channels. Valid options are:
Expand Down Expand Up @@ -155,7 +157,7 @@ def detect_tissue(
# Normalize method
if isinstance(method, str):
try:
method = DETECT_TISSUE_METHOD[method.upper()]
method = DetectTissueMethod[method.upper()]
except KeyError as e:
raise ValueError('method must be "otsu" or "felzenszwalb"') from e

Expand All @@ -170,7 +172,9 @@ def detect_tissue(
manual_scale = scale.lower() != "auto"

# Load smallest available or explicit scale
img_src = _get_image_data(sdata, image_key, scale=scale if manual_scale else "auto")
img_node = sdata.images[image_key]
img_da = _get_element_data(img_node, scale if manual_scale else "auto", "image", image_key)
img_src = _ensure_dim_order(img_da, "yxc")
src_h, src_w = _yx_from_shape(img_src.shape)
n_src_px = src_h * src_w

Expand All @@ -180,13 +184,13 @@ def detect_tissue(
# Decide working resolution
need_downscale = (not manual_scale) and (n_src_px > auto_max_pixels)
if need_downscale:
logg.info("Downscaling for faster computation.")
logger.info("Downscaling for faster computation.")
img_grey = _downscale_with_dask(img_grey=img_grey_da, target_pixels=auto_max_pixels)
else:
img_grey = img_grey_da.values # may compute

# First-pass foreground
if method == DETECT_TISSUE_METHOD.OTSU:
if method == DetectTissueMethod.OTSU:
img_fg_mask_bool = _segment_otsu(img_grey=img_grey, params=bgp)
else:
p = felzenszwalb_params or FelzenszwalbParams()
Expand Down Expand Up @@ -225,13 +229,9 @@ def detect_tissue(
return None

# If dask-backed, return a NumPy array to honor the signature
try:
import dask.array as da # noqa: F401
if is_dask_collection(img_fg_labels_up):
return np.asarray(img_fg_labels_up.compute())

if hasattr(img_fg_labels_up, "compute"):
return np.asarray(img_fg_labels_up.compute())
except (ImportError, AttributeError, TypeError):
pass
return np.asarray(img_fg_labels_up)


Expand All @@ -241,8 +241,6 @@ def _affine_upscale_nearest(labels: np.ndarray, scale_matrix: np.ndarray, target
Nearest-neighbor affine upscaling using dask-image. Returns dask array if available, else NumPy.
"""
try:
import dask.array as da

lbl_da = da.from_array(labels, chunks="auto")
result = da_affine(
lbl_da,
Expand All @@ -256,6 +254,7 @@ def _affine_upscale_nearest(labels: np.ndarray, scale_matrix: np.ndarray, target
)

return np.asarray(result)

except (ImportError, AttributeError, TypeError):
sy = target_shape[0] / labels.shape[0]
sx = target_shape[1] / labels.shape[1]
Expand Down Expand Up @@ -311,7 +310,7 @@ def _downscale_with_dask(img_grey: xr.DataArray, target_pixels: int) -> np.ndarr

fy = max(1, int(np.ceil(h / target_h)))
fx = max(1, int(np.ceil(w / target_w)))
logg.info(f"Downscaling from {h}×{w} with coarsen={fy}×{fx} to ≤{target_pixels} px.")
logger.info(f"Downscaling from {h}×{w} with coarsen={fy}×{fx} to ≤{target_pixels} px.")

da_small = _ensure_dask(img_grey).coarsen(y=fy, x=fx, boundary="trim").mean()
return np.asarray(_dask_compute(da_small))
Expand All @@ -322,9 +321,7 @@ def _ensure_dask(da: xr.DataArray) -> xr.DataArray:
Ensure DataArray is dask-backed. If not, chunk to reasonable tiles.
"""
try:
import dask.array as dask_array

if hasattr(da, "data") and isinstance(da.data, dask_array.Array):
if hasattr(da, "data") and isinstance(da.data, da.Array):
return da
return da.chunk({"y": 2048, "x": 2048})
except (ImportError, AttributeError):
Expand All @@ -336,10 +333,9 @@ def _dask_compute(img_da: xr.DataArray) -> np.ndarray:
Compute an xarray DataArray (possibly dask-backed) to a NumPy array with a ProgressBar if available.
"""
try:
import dask.array as dask_array
from dask.diagnostics import ProgressBar

if hasattr(img_da, "data") and isinstance(img_da.data, dask_array.Array):
if hasattr(img_da, "data") and isinstance(img_da.data, da.Array):
with ProgressBar():
computed = img_da.data.compute()
return np.asarray(computed)
Expand Down
Loading