Skip to content

Commit 900d7b3

Browse files
Method to detect specimens in H&E images (#1044)
* mvp for function; without testgs * added option to retain holes * refactor + 1 test * added missing import * renamed test so that a plot would be generated * added img from runner; cross-os-data-cache * improved docstring * added data download script to correct location * updated hatch commands * modified coverage combine * removed superflous combine step * first download data, then run tests * attempt to simplify * aligned testing * updated toml * aligned __init__ files * no uv cache for data download * removed download step that'd never get hit * simplify * parallel * speed up tests --------- Co-authored-by: Phil Schaf <[email protected]>
1 parent a3061c2 commit 900d7b3

File tree

12 files changed

+846
-45
lines changed

12 files changed

+846
-45
lines changed

.github/workflows/test.yml

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
schedule:
5-
- cron: 00 00 * * 1 # every Monday at 00:00
5+
- cron: "00 00 * * 1" # every Monday at 00:00
66
push:
77
branches:
88
- main
@@ -14,11 +14,42 @@ env:
1414
FORCE_COLOR: "1"
1515
MPLBACKEND: agg
1616
# It's impossible to ignore SyntaxWarnings for a single module,
17-
# so because leidenalg 0.10.0 has them, we pre-compile things: https://github.com/vtraag/leidenalg/issues/173
17+
# so because leidenalg 0.10.0 has them, we pre-compile things:
18+
# https://github.com/vtraag/leidenalg/issues/173
1819
UV_COMPILE_BYTECODE: "1"
20+
COVERAGE_FILE: ${{ github.workspace }}/.coverage
1921

2022
jobs:
23+
ensure-data-is-cached:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0
29+
filter: blob:none
30+
31+
- uses: astral-sh/setup-uv@v6
32+
with:
33+
enable-cache: false
34+
python-version: "3.13"
35+
cache-dependency-glob: pyproject.toml
36+
37+
- name: Restore data cache
38+
id: data-cache
39+
uses: actions/cache@v4
40+
with:
41+
path: |
42+
~/.cache/squidpy/*.h5ad
43+
~/.cache/squidpy/*.zarr
44+
key: data-${{ hashFiles('**/download_data.py') }}
45+
enableCrossOsArchive: true
46+
47+
- name: Download datasets
48+
if: steps.data-cache.outputs.cache-hit != 'true'
49+
run: uvx hatch run data:download
50+
2151
test:
52+
needs: [ensure-data-is-cached]
2253
runs-on: ${{ matrix.os }}
2354
strategy:
2455
fail-fast: false
@@ -34,16 +65,15 @@ jobs:
3465
os: ubuntu-latest
3566
python: "3.13"
3667
test-type: "coverage"
68+
pytest-addopts: "-v --color=yes -n auto"
3769
- name: hatch-test.py3.13-pre
3870
os: macos-latest
3971
python: "3.13"
40-
env: # environment variable for use in codecov's env_vars tagging
72+
env:
4173
ENV_NAME: ${{ matrix.name }}
4274
steps:
4375
- uses: actions/checkout@v4
44-
with:
45-
fetch-depth: 0
46-
filter: blob:none
76+
with: { fetch-depth: 0, filter: blob:none }
4777

4878
- uses: astral-sh/setup-uv@v6
4979
with:
@@ -60,12 +90,9 @@ jobs:
6090
with:
6191
path: |
6292
~/.cache/squidpy/*.h5ad
93+
~/.cache/squidpy/*.zarr
6394
key: data-${{ hashFiles('**/download_data.py') }}
64-
65-
- name: Download datasets
66-
if: steps.data-cache.outputs.cache-hit != 'true'
67-
run: |
68-
uvx hatch run ${{ matrix.name }}:download
95+
enableCrossOsArchive: true
6996

7097
- name: System dependencies (Linux)
7198
if: matrix.os == 'ubuntu-latest'
@@ -81,18 +108,22 @@ jobs:
81108
if: matrix.os == 'macos-latest'
82109
run: brew install automake
83110

84-
- name: Install dependencies
111+
- name: Create env
85112
run: uvx hatch -v env create ${{ matrix.name }}
86113

87114
- name: Run tests
88115
if: matrix.test-type == null
89-
run: uvx hatch run ${{ matrix.name }}:run
116+
run: uvx hatch run ${{ matrix.name }}:run -n logical
117+
90118
- name: Run tests (coverage)
91119
if: matrix.test-type == 'coverage'
120+
env:
121+
PYTEST_ADDOPTS: ${{ matrix.pytest-addopts }}
92122
run: |
93-
uvx hatch run ${{ matrix.name }}:run-cov
94-
uvx hatch run ${{ matrix.name }}:coverage combine
95-
uvx hatch run ${{ matrix.name }}:coverage xml
123+
uvx hatch run ${{ matrix.name }}:cov-erase
124+
uvx hatch run ${{ matrix.name }}:run-cov -n logical
125+
uvx hatch run ${{ matrix.name }}:cov-combine
126+
uvx hatch run ${{ matrix.name }}:cov-report
96127
97128
- name: Archive figures generated during testing
98129
if: always()
@@ -111,8 +142,7 @@ jobs:
111142

112143
check:
113144
if: always()
114-
needs:
115-
- test
145+
needs: [test]
116146
runs-on: ubuntu-latest
117147
steps:
118148
- uses: re-actors/alls-green@release/v1

hatch.toml

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,11 @@
22
installer = "uv"
33
features = ["dev"]
44

5-
[envs.coverage]
6-
extra-dependencies = [
7-
"coverage[toml]",
8-
"diff_cover",
9-
]
10-
11-
[envs.coverage.scripts]
12-
clean = "coverage erase"
13-
report = "coverage report --omit='tox/*'"
14-
xml = "coverage xml --omit='tox/*' -o coverage.xml"
15-
diff = "diff-cover --compare-branch origin/main coverage.xml"
16-
175
[envs.docs]
186
features = ["docs"]
197
extra-dependencies = [
208
"setuptools",
219
]
22-
2310
[envs.docs.scripts]
2411
build = "make -C docs html {args}"
2512
clean = "make -C docs clean"
@@ -32,14 +19,20 @@ download = "python ./.scripts/ci/download_data.py {args}"
3219

3320
[envs.hatch-test]
3421
features = ["test"]
35-
extra-dependencies = [
36-
"pytest",
37-
"pytest-xdist",
38-
"pytest-cov",
39-
"pytest-mock",
40-
"pytest-timeout",
22+
extra-dependencies = ["diff-cover"]
23+
[envs.hatch-test.scripts]
24+
# defaults (only `cov-report` is overridden)
25+
run = "pytest{env:HATCH_TEST_ARGS:} -p no:cov {args}"
26+
run-cov = "coverage run -m pytest{env:HATCH_TEST_ARGS:} -p no:cov {args}"
27+
cov-combine = ["coverage combine"]
28+
cov-report = [
29+
"coverage report",
30+
"coverage xml -o coverage.xml",
31+
"diff-cover --compare-branch origin/main coverage.xml",
4132
]
42-
33+
# extra commands
34+
cov-erase = "coverage erase"
35+
download = "python ./.scripts/ci/download_data.py {args}"
4336

4437

4538
[[envs.hatch-test.matrix]]
@@ -72,4 +65,4 @@ extras = ["docs"]
7265
[envs.notebooks.scripts]
7366

7467
setup-squidpy-kernel = "python -m ipykernel install --user --name=squidpy"
75-
run-notebooks = "python ./.scripts/ci/run_notebooks.py docs/notebooks"
68+
run-notebooks = "python ./.scripts/ci/run_notebooks.py docs/notebooks"

pyproject.toml

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,20 @@ authors = [
3737
{name = "Giovanni Palla"},
3838
{name = "Michal Klein"},
3939
{name = "Hannah Spitzer"},
40+
{name = "Tim Treis"},
41+
{name = "Laurens Lehner"},
42+
{name = "Selman Ozleyen"},
4043
]
4144
maintainers = [
42-
{name = "Giovanni Palla", email = "[email protected]"},
43-
{name = "Michal Klein", email = "[email protected]"},
44-
{name = "Tim Treis", email = "[email protected]"}
45+
{name = "Tim Treis", email = "[email protected]"},
46+
{name = "Selman Ozleyen", email = "[email protected]"}
4547
]
4648

4749
dependencies = [
4850
"aiohttp>=3.8.1",
4951
"anndata>=0.9",
52+
"spatialdata>=0.2.5",
53+
"spatialdata-plot",
5054
"cycler>=0.11.0",
5155
"dask-image>=0.5.0",
5256
"dask[array]>=2021.02.0,<=2024.11.2",
@@ -61,7 +65,7 @@ dependencies = [
6165
"pandas>=2.1.0",
6266
"Pillow>=8.0.0",
6367
"scanpy>=1.9.3",
64-
"scikit-image>=0.20",
68+
"scikit-image>=0.25",
6569
# due to https://github.com/scikit-image/scikit-image/issues/6850 breaks rescale ufunc
6670
"scikit-learn>=0.24.0",
6771
"statsmodels>=0.12.0",
@@ -70,14 +74,20 @@ dependencies = [
7074
"tqdm>=4.50.2",
7175
"validators>=0.18.2",
7276
"xarray>=2024.10.0",
77+
"imagecodecs>=2025.8.2,<2026",
7378
"zarr>=2.6.1",
74-
"spatialdata>=0.2.5",
7579
]
7680

7781
[project.optional-dependencies]
7882
dev = [
7983
"pre-commit>=3.0.0",
8084
"hatch>=1.9.0",
85+
"jupyterlab",
86+
"notebook",
87+
"ipykernel",
88+
"ipywidgets",
89+
"jupytext",
90+
"ruff",
8191
]
8292
test = [
8393
"scanpy[leiden]",
@@ -262,6 +272,7 @@ omit = [
262272
"*/__init__.py",
263273
"*/_version.py",
264274
"squidpy/pl/_interactive/*",
275+
"tox/*",
265276
]
266277

267278
[tool.coverage.paths]
@@ -282,3 +293,37 @@ show_missing = true
282293
precision = 2
283294
skip_empty = true
284295
sort = "Miss"
296+
297+
[tool.pixi.workspace]
298+
channels = ["conda-forge"]
299+
platforms = ["osx-arm64", "linux-64"]
300+
301+
[tool.pixi.dependencies]
302+
python = ">=3.11"
303+
304+
[tool.pixi.pypi-dependencies]
305+
squidpy = { path = ".", editable = true }
306+
307+
[tool.pixi.feature.py311.dependencies]
308+
python = "3.11.*"
309+
310+
[tool.pixi.feature.py313.dependencies]
311+
python = "3.13.*"
312+
313+
[tool.pixi.environments]
314+
dev-py311 = { features = ["dev", "test", "py311"], solve-group = "py311" }
315+
docs-py311 = { features = ["docs", "py311"], solve-group = "py311" }
316+
317+
default = { features = ["py313"], solve-group = "py313" }
318+
dev-py313 = { features = ["dev", "test", "py313"], solve-group = "py313" }
319+
docs-py313 = { features = ["docs", "py313"], solve-group = "py313" }
320+
test-py313 = { features = ["test", "py313"], solve-group = "py313" }
321+
322+
[tool.pixi.tasks]
323+
lab = "jupyter lab"
324+
kernel-install = "python -m ipykernel install --user --name pixi-dev --display-name \"squidpy (dev)\""
325+
test = "pytest -v --color=yes --tb=short --durations=10"
326+
lint = "ruff check ."
327+
format = "ruff format ."
328+
pre-commit-install = "pre-commit install"
329+
pre-commit = "pre-commit run"

src/squidpy/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from importlib import metadata
44
from importlib.metadata import PackageMetadata
55

6-
from squidpy import datasets, gr, im, pl, read, tl
6+
from squidpy import datasets, experimental, gr, im, pl, read, tl
77

88
try:
99
md: PackageMetadata = metadata.metadata(__name__)
@@ -14,3 +14,5 @@
1414
md = None # type: ignore[assignment]
1515

1616
del metadata, md
17+
18+
__all__ = ["datasets", "experimental", "gr", "im", "pl", "read", "tl"]

src/squidpy/_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import joblib as jl
1717
import numba
1818
import numpy as np
19+
import spatialdata as sd
20+
from spatialdata.models import Image2DModel, Labels2DModel
1921

2022
__all__ = ["singledispatchmethod", "Signal", "SigQueue", "NDArray", "NDArrayA"]
2123

@@ -347,3 +349,27 @@ def new_func2(*args: Any, **kwargs: Any) -> Any:
347349

348350
else:
349351
raise TypeError(repr(type(reason)))
352+
353+
354+
def _get_scale_factors(
355+
element: Image2DModel | Labels2DModel,
356+
) -> list[float]:
357+
"""
358+
Get the scale factors of an image or labels.
359+
"""
360+
if not hasattr(element, "keys"):
361+
return [] # element isn't a datatree -> single scale
362+
363+
shapes = [_yx_from_shape(element[scale].image.shape) for scale in element.keys()]
364+
365+
factors: list[float] = [(y0 / y1 + x0 / x1) / 2 for (y0, x0), (y1, x1) in zip(shapes, shapes[1:], strict=False)]
366+
return [int(f) for f in factors]
367+
368+
369+
def _yx_from_shape(shape: tuple[int, ...]) -> tuple[int, int]:
370+
if len(shape) == 2: # (y, x)
371+
return shape[0], shape[1]
372+
if len(shape) == 3: # (c, y, x)
373+
return shape[1], shape[2]
374+
375+
raise ValueError(f"Unsupported shape {shape}. Expected (y, x) or (c, y, x).")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Experimental module for Squidpy.
2+
3+
This module contains experimental features that are still under development.
4+
These features may change or be removed in future releases.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from . import im
10+
from .im._detect_tissue import detect_tissue
11+
12+
__all__ = ["detect_tissue", "im"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from ._detect_tissue import (
4+
BackgroundDetectionParams,
5+
FelzenszwalbParams,
6+
detect_tissue,
7+
)
8+
9+
__all__ = ["detect_tissue", "BackgroundDetectionParams", "FelzenszwalbParams"]

0 commit comments

Comments
 (0)