bioimage-cpp 0.1.1__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bioimage_cpp/__init__.py +78 -0
- bioimage_cpp/_core.cp312-win_amd64.pyd +0 -0
- bioimage_cpp/_data.py +318 -0
- bioimage_cpp/_version.py +1 -0
- bioimage_cpp/affinities/__init__.py +5 -0
- bioimage_cpp/affinities/compute_affinities.py +127 -0
- bioimage_cpp/filters/__init__.py +20 -0
- bioimage_cpp/filters/_filters.py +236 -0
- bioimage_cpp/graph/__init__.py +2115 -0
- bioimage_cpp/graph/_external.py +234 -0
- bioimage_cpp/segmentation/__init__.py +11 -0
- bioimage_cpp/segmentation/mutex_watershed.py +394 -0
- bioimage_cpp/segmentation/watershed.py +261 -0
- bioimage_cpp/transformation/__init__.py +13 -0
- bioimage_cpp/transformation/_transformation.py +342 -0
- bioimage_cpp/utils.py +368 -0
- bioimage_cpp-0.1.1.dist-info/METADATA +36 -0
- bioimage_cpp-0.1.1.dist-info/RECORD +20 -0
- bioimage_cpp-0.1.1.dist-info/WHEEL +5 -0
- bioimage_cpp-0.1.1.dist-info/licenses/LICENSE +21 -0
bioimage_cpp/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""`bioimage_cpp` implements image processing and segmentation functionality in C++.
|
|
2
|
+
It generates light-weight python bindings with nanobind with minimal dependencies to enable distribution via pip.
|
|
3
|
+
|
|
4
|
+
The main goal of this library is to provide functionality that is missing from scipy or scikit-image,
|
|
5
|
+
or to provide more performant versions of functionality from these libraries.
|
|
6
|
+
|
|
7
|
+
The functionality implemented here bundles and improves algorithms etc. from:
|
|
8
|
+
- [affogato](https://github.com/constantinpape/affogato)
|
|
9
|
+
- [fastfilters](https://github.com/sciai-lab/fastfilters)
|
|
10
|
+
- [nifty](https://github.com/DerThorsten/nifty)
|
|
11
|
+
- [vigra](https://github.com/ukoethe/vigra)
|
|
12
|
+
|
|
13
|
+
The goal is to provide the functionality within a single library and via pip as well as conda.
|
|
14
|
+
|
|
15
|
+
**Warning:** This library was written mainly by coding agents (claude code and openai codex).
|
|
16
|
+
It is not very thoroughly tested and may contain bugs.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
TODO: document once on pip / conda
|
|
21
|
+
|
|
22
|
+
## Functionality
|
|
23
|
+
|
|
24
|
+
This library provides the following functionality:
|
|
25
|
+
- `affinities`: functionality for deriving affinities from segmentations.
|
|
26
|
+
- `filters`: efficient implementation of convolutional image filters.
|
|
27
|
+
- `graph`: graph creation and graph (partitioning) algorithms.
|
|
28
|
+
- `segmentation`: image segmentation functionality.
|
|
29
|
+
- `transformation`: affine transformations.
|
|
30
|
+
- `utils`: misc utility functionality.
|
|
31
|
+
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
Below is a simple example for creating and partitioning a graph with this library.
|
|
35
|
+
For more realistic use-cases check out [the migration guide](#migration-guide).
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import numpy as np
|
|
39
|
+
import bioimage_cpp as bic
|
|
40
|
+
|
|
41
|
+
# Create a graph with 50 nodes.
|
|
42
|
+
graph = bic.graph.undirected_graph(number_of_nodes=50)
|
|
43
|
+
|
|
44
|
+
# Insert a bunch of edges forming a chain.
|
|
45
|
+
graph.insert_edges(
|
|
46
|
+
np.concatenate([np.arange(0, 49)[:, None], np.arange(1, 50)[:, None]], axis=1)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Create edge weights in [-1, 1].
|
|
50
|
+
weights = 2 * np.random.rand(graph.number_of_edges) - 1
|
|
51
|
+
|
|
52
|
+
# Partition the graph via multicut (greedy solver).
|
|
53
|
+
objective = bic.graph.MulticutObjective(graph, weights)
|
|
54
|
+
solver = bic.graph.GreedyAdditiveMulticut()
|
|
55
|
+
partition = solver.optimize(objective)
|
|
56
|
+
print("Partitioned into", len(np.unique(partition)), "elements")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
.. include:: ../../MIGRATION_GUIDE.md
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from ._version import __version__
|
|
63
|
+
from . import affinities
|
|
64
|
+
from . import filters
|
|
65
|
+
from . import graph
|
|
66
|
+
from . import segmentation
|
|
67
|
+
from . import transformation
|
|
68
|
+
from . import utils
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"__version__",
|
|
72
|
+
"affinities",
|
|
73
|
+
"filters",
|
|
74
|
+
"graph",
|
|
75
|
+
"segmentation",
|
|
76
|
+
"transformation",
|
|
77
|
+
"utils",
|
|
78
|
+
]
|
|
Binary file
|
bioimage_cpp/_data.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Pooch-based registry for downloadable problem instances.
|
|
2
|
+
|
|
3
|
+
Caches files under ``~/.cache/bioimage-cpp/`` by default, overridable with
|
|
4
|
+
``BIOIMAGE_CPP_CACHE``. Pooch is imported lazily so the runtime does not gain
|
|
5
|
+
a hard dependency on it; ``fetch`` raises a clear error if pooch is missing.
|
|
6
|
+
|
|
7
|
+
Registered files
|
|
8
|
+
----------------
|
|
9
|
+
|
|
10
|
+
Multicut problems (text files with rows ``u v cost``; originate from
|
|
11
|
+
``elf.segmentation.utils.load_multicut_problem``):
|
|
12
|
+
|
|
13
|
+
- ``multicut_problem_A_small.txt`` ... ``multicut_problem_C_medium.txt``
|
|
14
|
+
(3 samples × 2 sizes = 6 problems).
|
|
15
|
+
|
|
16
|
+
Lifted multicut problems (``.npz`` files written by
|
|
17
|
+
``examples/segmentation/serialize_lifted_problem.py``):
|
|
18
|
+
|
|
19
|
+
- ``lifted_multicut_problem_2d.npz`` — 2D ISBI slice (small, ~756 nodes).
|
|
20
|
+
- ``lifted_multicut_problem_3d.npz`` — full 3D ISBI volume (medium, ~18k nodes).
|
|
21
|
+
- ``lifted_multicut_problem_grid.npz`` — lifted multicut problem from grid graph (large, ~260k nodes).
|
|
22
|
+
|
|
23
|
+
Affinities:
|
|
24
|
+
- ``affinities`` — HDF5 file with sample affinities from the ISBI volume.
|
|
25
|
+
Contains affinities under key ``affinities``.
|
|
26
|
+
|
|
27
|
+
Semantic labels:
|
|
28
|
+
- ``semantic_labels`` — HDF5 file with paired instance and semantic ground
|
|
29
|
+
truth on a single 3D volume. Contains ``labels/instances``,
|
|
30
|
+
``labels/semantic`` and ``raw``. The on-disk arrays are padded with ``-1``
|
|
31
|
+
outside the labelled region; loaders crop to the labelled content slab.
|
|
32
|
+
Used by the semantic-mutex-watershed comparison scripts under
|
|
33
|
+
``development/``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import os
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Optional
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
|
|
44
|
+
DEFAULT_CACHE_DIR = Path.home() / ".cache" / "bioimage-cpp"
|
|
45
|
+
CACHE_ENV_VAR = "BIOIMAGE_CPP_CACHE"
|
|
46
|
+
ISBI_AFFINITY_FILENAME = "affinities"
|
|
47
|
+
SEMANTIC_LABELS_FILENAME = "semantic_labels"
|
|
48
|
+
ISBI_AFFINITY_OFFSETS = (
|
|
49
|
+
(-1, 0, 0),
|
|
50
|
+
(0, -1, 0),
|
|
51
|
+
(0, 0, -1),
|
|
52
|
+
(-1, -1, -1),
|
|
53
|
+
(-1, 1, 1),
|
|
54
|
+
(-1, -1, 1),
|
|
55
|
+
(-1, 1, -1),
|
|
56
|
+
(0, -9, 0),
|
|
57
|
+
(0, 0, -9),
|
|
58
|
+
(0, -9, -9),
|
|
59
|
+
(0, 9, -9),
|
|
60
|
+
(0, -9, -4),
|
|
61
|
+
(0, -4, -9),
|
|
62
|
+
(0, 4, -9),
|
|
63
|
+
(0, 9, -4),
|
|
64
|
+
(0, -27, 0),
|
|
65
|
+
(0, 0, -27),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Each entry is filename -> (url, sha256). To refresh a hash, delete the
|
|
70
|
+
# corresponding file under :func:`cache_dir`, re-download it, and run
|
|
71
|
+
# ``sha256sum`` on the cached file.
|
|
72
|
+
_REGISTRY: dict[str, tuple[str, Optional[str]]] = {
|
|
73
|
+
"multicut_problem_A_small.txt": (
|
|
74
|
+
"https://oc.embl.de/index.php/s/yVKwyQ8VoPXYkft/download",
|
|
75
|
+
"eeb1083557a20f7ce1ece28f5c613cc8ce5bf6231cd74aadbeb8a5012c6f8ef0",
|
|
76
|
+
),
|
|
77
|
+
"multicut_problem_A_medium.txt": (
|
|
78
|
+
"https://oc.embl.de/index.php/s/ztnwjmv0bmd3mnS/download",
|
|
79
|
+
"a8cdd23fcd911ad62b1b859b242bac28d16e7cdc3920137116b05672c4a6ec8a",
|
|
80
|
+
),
|
|
81
|
+
"multicut_problem_B_small.txt": (
|
|
82
|
+
"https://oc.embl.de/index.php/s/QKYA2EoMXqxQuO4/download",
|
|
83
|
+
"abd2c040234f20b107cc237b2c87120058d78e2c5e3ba2b95bc12b3b4d433aa5",
|
|
84
|
+
),
|
|
85
|
+
"multicut_problem_B_medium.txt": (
|
|
86
|
+
"https://oc.embl.de/index.php/s/yuk7VwCvgZC017q/download",
|
|
87
|
+
"6a8406c774553753e49103531945c32170587cc0d20d0459c866b47de5b014ec",
|
|
88
|
+
),
|
|
89
|
+
"multicut_problem_C_small.txt": (
|
|
90
|
+
"https://oc.embl.de/index.php/s/eDZprDwT2cXFAe0/download",
|
|
91
|
+
"6db8336c0ba3f75e3f9432628ac13b156fb9e43f75307cdda11469927ed1a108",
|
|
92
|
+
),
|
|
93
|
+
"multicut_problem_C_medium.txt": (
|
|
94
|
+
"https://oc.embl.de/index.php/s/hGyqlkenHfsq5P4/download",
|
|
95
|
+
"130d1be14d69f8bfb5d20d1375452291db7ba620e2f03bf9ffbe52d1f577f0dc",
|
|
96
|
+
),
|
|
97
|
+
"lifted_multicut_problem_2d.npz": (
|
|
98
|
+
"https://owncloud.gwdg.de/index.php/s/QikYgJzbVxD5q8q/download",
|
|
99
|
+
"27f10d9b7b2405cf64fab49c9065291455f2f1364224bb94a255c4cc72798240",
|
|
100
|
+
),
|
|
101
|
+
"lifted_multicut_problem_3d.npz": (
|
|
102
|
+
"https://owncloud.gwdg.de/index.php/s/ZVzDy8Xb0Dr2Ell/download",
|
|
103
|
+
"269ce644e2b9f8259f7f2ff827d5808ac5c9bfe6ca0444e298290f23867dce8a",
|
|
104
|
+
),
|
|
105
|
+
"lifted_multicut_problem_grid.npz": (
|
|
106
|
+
"https://owncloud.gwdg.de/index.php/s/YWNZSYsBd1VwSX1/download",
|
|
107
|
+
"20583b2000838ed0942f8f1c343b84287d8bf218d19d77a8b5627924661c5aa3",
|
|
108
|
+
),
|
|
109
|
+
"affinities": (
|
|
110
|
+
"https://owncloud.gwdg.de/index.php/s/aAyF2ekzsW7DFJo/download",
|
|
111
|
+
"6472ad0fcf3c57a4ae345fda68c3cbb6072ee3e8db67b423502746b46d8cd5e5",
|
|
112
|
+
),
|
|
113
|
+
"semantic_labels": (
|
|
114
|
+
"https://owncloud.gwdg.de/index.php/s/Ah7IGuYH7uuomQV/download",
|
|
115
|
+
"6232fe2fd58fdbd3def978798143fdcc65a2af118b4d9ee177b5c942173ece26",
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cache_dir() -> Path:
|
|
121
|
+
"""Return the cache directory used for downloaded problem instances."""
|
|
122
|
+
override = os.environ.get(CACHE_ENV_VAR)
|
|
123
|
+
return Path(override).expanduser() if override else DEFAULT_CACHE_DIR
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def registered_files() -> list[str]:
|
|
127
|
+
"""List every filename available via :func:`fetch`."""
|
|
128
|
+
return sorted(_REGISTRY.keys())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def fetch(filename: str, *, timeout: Optional[float] = None) -> Path:
|
|
132
|
+
"""Return the local path to a registered file, downloading on first call.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
filename:
|
|
137
|
+
Filename as listed by :func:`registered_files`.
|
|
138
|
+
timeout:
|
|
139
|
+
Optional HTTP timeout in seconds, forwarded to pooch's downloader.
|
|
140
|
+
|
|
141
|
+
Raises
|
|
142
|
+
------
|
|
143
|
+
FileNotFoundError
|
|
144
|
+
If ``filename`` is not registered.
|
|
145
|
+
ModuleNotFoundError
|
|
146
|
+
If ``pooch`` is not installed. Install with ``pip install pooch``.
|
|
147
|
+
RuntimeError
|
|
148
|
+
If the download fails. The underlying ``HTTPError`` is chained.
|
|
149
|
+
"""
|
|
150
|
+
if filename not in _REGISTRY:
|
|
151
|
+
registered = registered_files()
|
|
152
|
+
raise FileNotFoundError(
|
|
153
|
+
f"{filename!r} is not in the bioimage-cpp data registry. "
|
|
154
|
+
f"Available: {registered}"
|
|
155
|
+
)
|
|
156
|
+
try:
|
|
157
|
+
import pooch
|
|
158
|
+
except ModuleNotFoundError as error:
|
|
159
|
+
raise ModuleNotFoundError(
|
|
160
|
+
"pooch is required to download bioimage-cpp problem instances. "
|
|
161
|
+
"Install it with `pip install pooch`."
|
|
162
|
+
) from error
|
|
163
|
+
|
|
164
|
+
url, sha256 = _REGISTRY[filename]
|
|
165
|
+
fetcher = pooch.create(
|
|
166
|
+
path=cache_dir(),
|
|
167
|
+
base_url="",
|
|
168
|
+
registry={filename: sha256},
|
|
169
|
+
urls={filename: url},
|
|
170
|
+
)
|
|
171
|
+
downloader = None
|
|
172
|
+
if timeout is not None:
|
|
173
|
+
downloader = pooch.HTTPDownloader(timeout=float(timeout))
|
|
174
|
+
try:
|
|
175
|
+
local_path = fetcher.fetch(filename, downloader=downloader)
|
|
176
|
+
except Exception as error:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"could not download {filename} from {url}: {error}"
|
|
179
|
+
) from error
|
|
180
|
+
return Path(local_path)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def affinity_path(*, timeout: Optional[float] = None) -> Path:
|
|
184
|
+
"""Return the cached path to the registered ISBI affinity HDF5 file."""
|
|
185
|
+
return fetch(ISBI_AFFINITY_FILENAME, timeout=timeout)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_isbi_affinities(
|
|
189
|
+
*,
|
|
190
|
+
timeout: Optional[float] = None,
|
|
191
|
+
) -> tuple[np.ndarray, list[tuple[int, int, int]]]:
|
|
192
|
+
"""Load the registered ISBI affinity volume and its offsets.
|
|
193
|
+
|
|
194
|
+
The offsets are the fixed channel offsets used by
|
|
195
|
+
``elf.segmentation.utils.load_mutex_watershed_problem`` for this data.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
import h5py
|
|
199
|
+
except ModuleNotFoundError as error:
|
|
200
|
+
raise ModuleNotFoundError(
|
|
201
|
+
"h5py is required to load the registered ISBI affinity file. "
|
|
202
|
+
"Install it with `pip install h5py`."
|
|
203
|
+
) from error
|
|
204
|
+
|
|
205
|
+
with h5py.File(affinity_path(timeout=timeout), "r") as f:
|
|
206
|
+
affinities = f["affinities"][:]
|
|
207
|
+
return np.ascontiguousarray(affinities), list(ISBI_AFFINITY_OFFSETS)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def load_isbi_raw(
|
|
211
|
+
*,
|
|
212
|
+
timeout: Optional[float] = None,
|
|
213
|
+
) -> np.ndarray:
|
|
214
|
+
"""Load the registered ISBI raw volume.
|
|
215
|
+
|
|
216
|
+
The raw data is stored in the same HDF5 file as the affinities under key
|
|
217
|
+
``raw``.
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
import h5py
|
|
221
|
+
except ModuleNotFoundError as error:
|
|
222
|
+
raise ModuleNotFoundError(
|
|
223
|
+
"h5py is required to load the registered ISBI raw file. "
|
|
224
|
+
"Install it with `pip install h5py`."
|
|
225
|
+
) from error
|
|
226
|
+
|
|
227
|
+
with h5py.File(affinity_path(timeout=timeout), "r") as f:
|
|
228
|
+
raw = f["raw"][:]
|
|
229
|
+
return np.ascontiguousarray(raw)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def load_isbi_gt_segmentation(
|
|
233
|
+
*,
|
|
234
|
+
timeout: Optional[float] = None,
|
|
235
|
+
) -> np.ndarray:
|
|
236
|
+
"""Load the registered ISBI ground-truth segmentation volume.
|
|
237
|
+
|
|
238
|
+
The labels are stored in the same HDF5 file as the affinities under key
|
|
239
|
+
``labels/gt_segmentation``. Returned as a C-contiguous ``uint64`` array
|
|
240
|
+
with shape ``(30, 512, 512)`` (~7.9 M voxels).
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
import h5py
|
|
244
|
+
except ModuleNotFoundError as error:
|
|
245
|
+
raise ModuleNotFoundError(
|
|
246
|
+
"h5py is required to load the registered ISBI segmentation file. "
|
|
247
|
+
"Install it with `pip install h5py`."
|
|
248
|
+
) from error
|
|
249
|
+
|
|
250
|
+
with h5py.File(affinity_path(timeout=timeout), "r") as f:
|
|
251
|
+
labels = f["labels/gt_segmentation"][:]
|
|
252
|
+
return np.ascontiguousarray(labels)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def semantic_labels_path(*, timeout: Optional[float] = None) -> Path:
|
|
256
|
+
"""Return the cached path to the registered semantic-labels HDF5 file."""
|
|
257
|
+
return fetch(SEMANTIC_LABELS_FILENAME, timeout=timeout)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Bounding box of the labelled content in the on-disk volume. Outside this
|
|
261
|
+
# slab both label volumes are uniformly ``-1``; cropping here keeps callers
|
|
262
|
+
# from having to special-case the padding.
|
|
263
|
+
SEMANTIC_LABELS_CROP = (slice(16, 32), slice(64, 512), slice(64, 512))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def load_semantic_labels(
|
|
267
|
+
*,
|
|
268
|
+
timeout: Optional[float] = None,
|
|
269
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
270
|
+
"""Load the registered (instance, semantic) ground-truth volumes.
|
|
271
|
+
|
|
272
|
+
Both volumes share the same spatial shape and live in a single HDF5 file
|
|
273
|
+
under keys ``labels/instances`` and ``labels/semantic``. The on-disk
|
|
274
|
+
arrays are padded with ``-1`` outside the labelled slab; this loader
|
|
275
|
+
returns the cropped labelled region (``(16, 448, 448)``).
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
instance_labels:
|
|
280
|
+
Integer instance-segmentation volume.
|
|
281
|
+
semantic_labels:
|
|
282
|
+
Integer semantic-class volume (one class id per voxel).
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
import h5py
|
|
286
|
+
except ModuleNotFoundError as error:
|
|
287
|
+
raise ModuleNotFoundError(
|
|
288
|
+
"h5py is required to load the registered semantic-labels file. "
|
|
289
|
+
"Install it with `pip install h5py`."
|
|
290
|
+
) from error
|
|
291
|
+
|
|
292
|
+
with h5py.File(semantic_labels_path(timeout=timeout), "r") as f:
|
|
293
|
+
instance = f["labels/instances"][SEMANTIC_LABELS_CROP]
|
|
294
|
+
semantic = f["labels/semantic"][SEMANTIC_LABELS_CROP]
|
|
295
|
+
return np.ascontiguousarray(instance), np.ascontiguousarray(semantic)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def load_semantic_raw(
|
|
299
|
+
*,
|
|
300
|
+
timeout: Optional[float] = None,
|
|
301
|
+
) -> np.ndarray:
|
|
302
|
+
"""Load the raw volume paired with the registered semantic labels.
|
|
303
|
+
|
|
304
|
+
Stored alongside ``labels/instances`` / ``labels/semantic`` in the same
|
|
305
|
+
HDF5 file under key ``raw``. Cropped consistently with
|
|
306
|
+
:func:`load_semantic_labels`.
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
import h5py
|
|
310
|
+
except ModuleNotFoundError as error:
|
|
311
|
+
raise ModuleNotFoundError(
|
|
312
|
+
"h5py is required to load the registered semantic-raw file. "
|
|
313
|
+
"Install it with `pip install h5py`."
|
|
314
|
+
) from error
|
|
315
|
+
|
|
316
|
+
with h5py.File(semantic_labels_path(timeout=timeout), "r") as f:
|
|
317
|
+
raw = f["raw"][SEMANTIC_LABELS_CROP]
|
|
318
|
+
return np.ascontiguousarray(raw)
|
bioimage_cpp/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Pairwise boolean affinities from a label volume."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import overload
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .. import _core
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_COMPUTE_AFFINITIES_2D_BY_DTYPE = {
|
|
14
|
+
np.dtype("uint32"): _core._compute_affinities_2d_uint32,
|
|
15
|
+
np.dtype("uint64"): _core._compute_affinities_2d_uint64,
|
|
16
|
+
np.dtype("int32"): _core._compute_affinities_2d_int32,
|
|
17
|
+
np.dtype("int64"): _core._compute_affinities_2d_int64,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_COMPUTE_AFFINITIES_3D_BY_DTYPE = {
|
|
21
|
+
np.dtype("uint32"): _core._compute_affinities_3d_uint32,
|
|
22
|
+
np.dtype("uint64"): _core._compute_affinities_3d_uint64,
|
|
23
|
+
np.dtype("int32"): _core._compute_affinities_3d_int32,
|
|
24
|
+
np.dtype("int64"): _core._compute_affinities_3d_int64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@overload
|
|
29
|
+
def compute_affinities(
|
|
30
|
+
labels: np.ndarray,
|
|
31
|
+
offsets: Sequence[Sequence[int]] | np.ndarray,
|
|
32
|
+
*,
|
|
33
|
+
ignore_label: int | None = None,
|
|
34
|
+
return_mask: bool = True,
|
|
35
|
+
number_of_threads: int = 1,
|
|
36
|
+
) -> tuple[np.ndarray, np.ndarray]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compute_affinities(
|
|
40
|
+
labels: np.ndarray,
|
|
41
|
+
offsets: Sequence[Sequence[int]] | np.ndarray,
|
|
42
|
+
*,
|
|
43
|
+
ignore_label: int | None = None,
|
|
44
|
+
return_mask: bool = True,
|
|
45
|
+
number_of_threads: int = 1,
|
|
46
|
+
):
|
|
47
|
+
"""Compute boolean pairwise affinities from a label volume.
|
|
48
|
+
|
|
49
|
+
For each spatial coordinate ``c`` and offset index ``oi``,
|
|
50
|
+
``affinities[oi, c]`` is ``1.0`` if ``labels[c] == labels[c + offsets[oi]]``
|
|
51
|
+
(the two voxels are in the same cluster) and ``0.0`` otherwise.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
labels:
|
|
56
|
+
2D or 3D integer label volume. Supported dtypes are ``uint32``,
|
|
57
|
+
``uint64``, ``int32``, ``int64``. Non-contiguous arrays are copied
|
|
58
|
+
to a C-contiguous buffer first.
|
|
59
|
+
offsets:
|
|
60
|
+
Shape ``(n_offsets, ndim)``. Each offset is a per-axis displacement,
|
|
61
|
+
in NumPy axis order, applied at each voxel to find the neighbor.
|
|
62
|
+
ignore_label:
|
|
63
|
+
If given, any pair where either endpoint has this label produces
|
|
64
|
+
``affinity = 0`` and ``mask = 0`` (treated as out-of-volume).
|
|
65
|
+
return_mask:
|
|
66
|
+
When ``True`` (default), also return a ``uint8`` validity mask of
|
|
67
|
+
the same shape as the affinities: ``1`` for in-bounds non-ignored
|
|
68
|
+
pairs, ``0`` otherwise. Set to ``False`` to skip the allocation
|
|
69
|
+
when only the affinities are needed.
|
|
70
|
+
number_of_threads:
|
|
71
|
+
Number of threads to parallelize over the offset channels.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
affinities : np.ndarray
|
|
76
|
+
``float32`` array of shape ``(n_offsets, *labels.shape)``.
|
|
77
|
+
mask : np.ndarray, only if ``return_mask`` is ``True``
|
|
78
|
+
``uint8`` array of shape ``(n_offsets, *labels.shape)``.
|
|
79
|
+
"""
|
|
80
|
+
array = np.ascontiguousarray(labels)
|
|
81
|
+
if array.ndim not in (2, 3):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"labels must be 2D or 3D, got ndim=" + str(array.ndim)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
table = (
|
|
87
|
+
_COMPUTE_AFFINITIES_2D_BY_DTYPE if array.ndim == 2
|
|
88
|
+
else _COMPUTE_AFFINITIES_3D_BY_DTYPE
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
run = table[array.dtype]
|
|
92
|
+
except KeyError as error:
|
|
93
|
+
supported = ", ".join(str(dtype) for dtype in table)
|
|
94
|
+
raise TypeError(
|
|
95
|
+
f"labels must have one of dtypes ({supported}), got dtype={array.dtype}"
|
|
96
|
+
) from error
|
|
97
|
+
|
|
98
|
+
normalized_offsets = [
|
|
99
|
+
[int(value) for value in offset] for offset in np.asarray(offsets).tolist()
|
|
100
|
+
]
|
|
101
|
+
if len(normalized_offsets) == 0:
|
|
102
|
+
raise ValueError("offsets must not be empty")
|
|
103
|
+
if any(len(offset) != array.ndim for offset in normalized_offsets):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"each offset must have length matching the spatial ndim, got "
|
|
106
|
+
f"spatial ndim={array.ndim}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
n_threads = int(number_of_threads)
|
|
110
|
+
if n_threads < 1:
|
|
111
|
+
raise ValueError("number_of_threads must be >= 1")
|
|
112
|
+
|
|
113
|
+
if ignore_label is None:
|
|
114
|
+
typed_ignore: int | None = None
|
|
115
|
+
else:
|
|
116
|
+
typed_ignore = int(ignore_label)
|
|
117
|
+
|
|
118
|
+
affs, mask = run(
|
|
119
|
+
array,
|
|
120
|
+
normalized_offsets,
|
|
121
|
+
typed_ignore,
|
|
122
|
+
bool(return_mask),
|
|
123
|
+
n_threads,
|
|
124
|
+
)
|
|
125
|
+
if return_mask:
|
|
126
|
+
return affs, mask
|
|
127
|
+
return affs
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Image filters: separable Gaussian-family derivatives, gradient magnitude,
|
|
2
|
+
Laplacian of Gaussian, Hessian and structure-tensor eigenvalues."""
|
|
3
|
+
|
|
4
|
+
from ._filters import (
|
|
5
|
+
gaussian_derivative,
|
|
6
|
+
gaussian_gradient_magnitude,
|
|
7
|
+
gaussian_smoothing,
|
|
8
|
+
hessian_of_gaussian_eigenvalues,
|
|
9
|
+
laplacian_of_gaussian,
|
|
10
|
+
structure_tensor_eigenvalues,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"gaussian_smoothing",
|
|
15
|
+
"gaussian_derivative",
|
|
16
|
+
"gaussian_gradient_magnitude",
|
|
17
|
+
"laplacian_of_gaussian",
|
|
18
|
+
"hessian_of_gaussian_eigenvalues",
|
|
19
|
+
"structure_tensor_eigenvalues",
|
|
20
|
+
]
|