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.
@@ -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)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,5 @@
1
+ """Pairwise affinities from label volumes."""
2
+
3
+ from .compute_affinities import compute_affinities
4
+
5
+ __all__ = ["compute_affinities"]
@@ -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
+ ]