Skip to content

Commit 0e74c3c

Browse files
Merge branch 'main' into feat/remove-ligrec-parallelize
2 parents 288010d + 9aa7c09 commit 0e74c3c

19 files changed

Lines changed: 1430 additions & 192 deletions

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ default_stages:
77
minimum_pre_commit_version: 2.16.0
88
repos:
99
- repo: https://github.com/biomejs/pre-commit
10-
rev: v2.4.9
10+
rev: v2.4.11
1111
hooks:
1212
- id: biome-format
1313
exclude: ^\.cruft\.json$ # inconsistent indentation with cruft - file never to be modified manually.
1414
- repo: https://github.com/tox-dev/pyproject-fmt
15-
rev: v2.20.0
15+
rev: v2.21.1
1616
hooks:
1717
- id: pyproject-fmt
1818
- repo: https://github.com/astral-sh/ruff-pre-commit
19-
rev: v0.15.8
19+
rev: v0.15.10
2020
hooks:
2121
- id: ruff-check
2222
types_or: [python, pyi, jupyter]

pyproject.toml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,27 @@ build.targets.wheel.packages = [ "src/squidpy" ]
133133
metadata.allow-direct-references = true
134134
version.source = "vcs"
135135

136+
[tool.pixi]
137+
workspace.channels = [ "conda-forge" ]
138+
workspace.platforms = [ "linux-64", "osx-arm64" ]
139+
dependencies.python = ">=3.11"
140+
pypi-dependencies.squidpy = { path = ".", editable = true }
141+
tasks.format = "ruff format ."
142+
tasks.kernel-install = 'python -m ipykernel install --user --name pixi-dev --display-name "squidpy (dev)"'
143+
tasks.lab = "jupyter lab"
144+
tasks.lint = "ruff check ."
145+
tasks.pre-commit = "pre-commit run"
146+
tasks.pre-commit-install = "pre-commit install"
147+
tasks.test = "pytest -v --color=yes --tb=short --durations=10"
148+
feature.py311.dependencies.python = "3.11.*"
149+
feature.py313.dependencies.python = "3.13.*"
150+
environments.default = { features = [ "py313" ], solve-group = "py313" }
151+
environments.dev-py311 = { features = [ "dev", "test", "py311" ], solve-group = "py311" }
152+
environments.dev-py313 = { features = [ "dev", "test", "py313" ], solve-group = "py313" }
153+
environments.docs-py311 = { features = [ "docs", "py311" ], solve-group = "py311" }
154+
environments.docs-py313 = { features = [ "docs", "py313" ], solve-group = "py313" }
155+
environments.test-py313 = { features = [ "test", "py313" ], solve-group = "py313" }
156+
136157
[tool.ruff]
137158
line-length = 120
138159
exclude = [
@@ -278,24 +299,3 @@ skip = [
278299
"docs/references.md",
279300
"docs/notebooks/example.ipynb",
280301
]
281-
282-
[tool.pixi]
283-
dependencies.python = ">=3.11"
284-
environments.dev-py311 = { features = [ "dev", "test", "py311" ], solve-group = "py311" }
285-
environments.docs-py311 = { features = [ "docs", "py311" ], solve-group = "py311" }
286-
environments.default = { features = [ "py313" ], solve-group = "py313" }
287-
environments.dev-py313 = { features = [ "dev", "test", "py313" ], solve-group = "py313" }
288-
environments.docs-py313 = { features = [ "docs", "py313" ], solve-group = "py313" }
289-
environments.test-py313 = { features = [ "test", "py313" ], solve-group = "py313" }
290-
feature.py311.dependencies.python = "3.11.*"
291-
feature.py313.dependencies.python = "3.13.*"
292-
pypi-dependencies.squidpy = { path = ".", editable = true }
293-
tasks.lab = "jupyter lab"
294-
tasks.kernel-install = 'python -m ipykernel install --user --name pixi-dev --display-name "squidpy (dev)"'
295-
tasks.test = "pytest -v --color=yes --tb=short --durations=10"
296-
tasks.lint = "ruff check ."
297-
tasks.format = "ruff format ."
298-
tasks.pre-commit-install = "pre-commit install"
299-
tasks.pre-commit = "pre-commit run"
300-
workspace.channels = [ "conda-forge" ]
301-
workspace.platforms = [ "osx-arm64", "linux-64" ]

src/squidpy/experimental/im/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
detect_tissue,
88
)
99
from ._make_tiles import make_tiles, make_tiles_from_spots
10+
from ._qc_image import qc_image
11+
from ._qc_metrics import QCMetric
1012

1113
__all__ = [
1214
"BackgroundDetectionParams",
1315
"FelzenszwalbParams",
16+
"QCMetric",
1417
"WekaParams",
1518
"detect_tissue",
1619
"make_tiles",
1720
"make_tiles_from_spots",
21+
"qc_image",
1822
]

src/squidpy/experimental/im/_detect_tissue.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

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

27-
from ._utils import _flatten_channels, _get_element_data
27+
from ._utils import flatten_channels, get_element_data
2828

2929

3030
class DetectTissueMethod(enum.Enum):
@@ -357,7 +357,7 @@ def detect_tissue(
357357

358358
# Load smallest available or explicit scale
359359
img_node = sdata.images[image_key]
360-
img_da = _get_element_data(img_node, scale if manual_scale else "auto", "image", image_key)
360+
img_da = get_element_data(img_node, scale if manual_scale else "auto", "image", image_key)
361361
img_src = _ensure_dim_order(img_da, "yxc")
362362
src_h = int(img_src.sizes["y"])
363363
src_w = int(img_src.sizes["x"])
@@ -375,7 +375,7 @@ def detect_tissue(
375375
# Channel flattening (greyscale) for threshold-based methods
376376
img_grey = None
377377
if method != DetectTissueMethod.WEKA:
378-
img_grey_da: xr.DataArray = _flatten_channels(img=img_src, channel_format=channel_format)
378+
img_grey_da: xr.DataArray = flatten_channels(img=img_src, channel_format=channel_format)
379379
if need_downscale:
380380
logger.info("Downscaling for faster computation.")
381381
img_grey = _downscale_with_dask(img_grey=img_grey_da, target_pixels=auto_max_pixels)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
5+
# --- Intensity metrics (grayscale input) ---
6+
7+
8+
def brightness_mean(block: np.ndarray) -> np.ndarray:
9+
"""Mean pixel intensity of a grayscale tile."""
10+
return np.array([[float(block.mean())]], dtype=np.float32)
11+
12+
13+
def brightness_std(block: np.ndarray) -> np.ndarray:
14+
"""Standard deviation of pixel intensity of a grayscale tile."""
15+
return np.array([[float(block.std())]], dtype=np.float32)
16+
17+
18+
def entropy(block: np.ndarray) -> np.ndarray:
19+
"""Shannon entropy of pixel intensity histogram."""
20+
arr = block.ravel()
21+
lo, hi = float(arr.min()), float(arr.max())
22+
if hi - lo < 1e-10:
23+
return np.array([[0.0]], dtype=np.float32)
24+
# Quantize to 256 bins directly without storing intermediate normalized array
25+
bins = np.clip(((arr - lo) * (255.0 / (hi - lo))).astype(np.int32), 0, 255)
26+
counts = np.bincount(bins, minlength=256)
27+
probs = counts[counts > 0].astype(np.float64)
28+
probs /= probs.sum()
29+
ent = -float(np.dot(probs, np.log2(probs)))
30+
return np.array([[ent]], dtype=np.float32)
31+
32+
33+
# --- Staining metrics (RGB input, H&E only) ---
34+
35+
36+
def rgb_to_hed(block_rgb: np.ndarray) -> np.ndarray:
37+
"""Convert RGB tile to HED colour space using Beer-Lambert deconvolution.
38+
39+
Parameters
40+
----------
41+
block_rgb
42+
(ty, tx, 3) float32 array in [0, 1].
43+
44+
Returns
45+
-------
46+
(ty, tx, 3) float64 array with channels H, E, D.
47+
"""
48+
from skimage.color import rgb2hed
49+
50+
rgb_clipped = np.clip(block_rgb, 0.0, 1.0)
51+
return rgb2hed(rgb_clipped)
52+
53+
54+
def hed_metrics(block: np.ndarray) -> np.ndarray:
55+
"""Return all HED-derived metrics for one RGB tile."""
56+
hed = rgb_to_hed(block)
57+
h = hed[..., 0]
58+
e = hed[..., 1]
59+
60+
return np.array(
61+
[
62+
[
63+
[
64+
float(h.mean()),
65+
float(h.std()),
66+
float(e.mean()),
67+
float(e.std()),
68+
float(np.abs(h).mean() / (np.abs(e).mean() + 1e-10)),
69+
]
70+
]
71+
],
72+
dtype=np.float32,
73+
)
74+
75+
76+
def hematoxylin_mean(block: np.ndarray) -> np.ndarray:
77+
"""Mean hematoxylin channel intensity."""
78+
hed = rgb_to_hed(block)
79+
return np.array([[float(hed[..., 0].mean())]], dtype=np.float32)
80+
81+
82+
def hematoxylin_std(block: np.ndarray) -> np.ndarray:
83+
"""Std of hematoxylin channel intensity."""
84+
hed = rgb_to_hed(block)
85+
return np.array([[float(hed[..., 0].std())]], dtype=np.float32)
86+
87+
88+
def eosin_mean(block: np.ndarray) -> np.ndarray:
89+
"""Mean eosin channel intensity."""
90+
hed = rgb_to_hed(block)
91+
return np.array([[float(hed[..., 1].mean())]], dtype=np.float32)
92+
93+
94+
def eosin_std(block: np.ndarray) -> np.ndarray:
95+
"""Std of eosin channel intensity."""
96+
hed = rgb_to_hed(block)
97+
return np.array([[float(hed[..., 1].std())]], dtype=np.float32)
98+
99+
100+
def he_ratio(block: np.ndarray) -> np.ndarray:
101+
"""Ratio of hematoxylin to eosin mean intensity."""
102+
hed = rgb_to_hed(block)
103+
h_mean = float(np.abs(hed[..., 0]).mean())
104+
e_mean = float(np.abs(hed[..., 1]).mean())
105+
ratio = h_mean / (e_mean + 1e-10)
106+
return np.array([[ratio]], dtype=np.float32)
107+
108+
109+
# --- Artifact metrics (RGB input, H&E only) ---
110+
111+
112+
def fold_fraction(block: np.ndarray) -> np.ndarray:
113+
"""Fraction of pixels identified as tissue folds.
114+
115+
Uses HSV thresholds tuned for H&E staining: saturation > 0.4 and
116+
value < 0.3 captures the dark, saturated appearance of folded tissue.
117+
"""
118+
from skimage.color import rgb2hsv
119+
120+
rgb_clipped = np.clip(block, 0.0, 1.0)
121+
hsv = rgb2hsv(rgb_clipped)
122+
sat = hsv[..., 1]
123+
val = hsv[..., 2]
124+
fold_mask = (sat > 0.4) & (val < 0.3)
125+
frac = float(fold_mask.sum()) / max(fold_mask.size, 1)
126+
return np.array([[frac]], dtype=np.float32)
127+
128+
129+
# --- Tissue coverage (mask input) ---
130+
131+
132+
def tissue_fraction(block: np.ndarray) -> np.ndarray:
133+
"""Fraction of pixels that are tissue (nonzero) in a binary mask tile."""
134+
return np.array([[float(block.mean())]], dtype=np.float32)

0 commit comments

Comments
 (0)