From e2b353b15176fd9983817db276b0bb59b6622aeb Mon Sep 17 00:00:00 2001 From: Constantin Pape Date: Thu, 16 Jul 2026 20:11:43 +0200 Subject: [PATCH 1/2] Bump version and extend disclaimer --- README.md | 6 ++++++ src/bioimage_cpp/__init__.py | 16 ++++++++++++---- src/bioimage_cpp/_version.py | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 294388a..71dc6ad 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,9 @@ conda install -c conda-forge bioimage-cpp ``` Please refer to [the documentation](https://computational-cell-analytics.github.io/bioimage-cpp/) for details. + +**Disclaimer:** The functionality of this library was implemented mainly by coding agents (Claude Code and OpenAI Codex). +We have made our best efforts to test the implementations thoroughly and are already using it heavily in our day-to-day research and have integrated with other software tools. +Nevertheless, it may contain bugs or unintended behavior (as most software does). +If you find such a problem, please [open an issue on github](https://github.com/computational-cell-analytics/bioimage-cpp/issues). +We are committed to improving and mainting this software. diff --git a/src/bioimage_cpp/__init__.py b/src/bioimage_cpp/__init__.py index 23c8657..c84c0cc 100644 --- a/src/bioimage_cpp/__init__.py +++ b/src/bioimage_cpp/__init__.py @@ -13,8 +13,14 @@ The goal is to provide the functionality within a single library and via pip as well as conda. -**Warning:** This library was written mainly by coding agents (claude code and openai codex). -It is not very thoroughly tested and may contain bugs. +**Disclaimer:** The functionality of this library was implemented mainly by coding agents (Claude Code, OpenAI Codex). +We make our best efforts to test the implementations thoroughly +and are already using it heavily in our day-to-day research +and have integrated it with other software tools. +Nevertheless, it may contain bugs or unintended behavior (as most software does). +If you find such a problem, +please [open an issue on github](https://github.com/computational-cell-analytics/bioimage-cpp/issues). +We are committed to improving and mainting this software. ## Installation @@ -28,7 +34,8 @@ conda install -c conda-forge bioimage-cpp ``` -Additional dependencies for tests / data downloads can be installed via `pip install bioimage-cpp[test]` / `pip install bioimage-cpp[data]` respectively. +Additional dependencies for tests / data downloads +can be installed via `pip install bioimage-cpp[test]` / `pip install bioimage-cpp[data]` respectively. You can also install this library from source. The build requires C++20 (GCC >= 10 or Clang >= 13). @@ -39,7 +46,8 @@ pip install -e . ``` -On systems with an older compiler (e.g. many HPC clusters ship GCC 8), install a modern one from conda-forge alongside the build dependencies, and point `CC`/`CXX` at it: +On systems with an older compiler (e.g. many HPC clusters ship GCC 8), +install a modern one from conda-forge alongside the build dependencies, and point `CC`/`CXX` at it: ```bash conda install gcc_linux-64 gxx_linux-64 scikit-build-core nanobind -c conda-forge -y export CC=x86_64-conda-linux-gnu-gcc diff --git a/src/bioimage_cpp/_version.py b/src/bioimage_cpp/_version.py index 49e0fc1..777f190 100644 --- a/src/bioimage_cpp/_version.py +++ b/src/bioimage_cpp/_version.py @@ -1 +1 @@ -__version__ = "0.7.0" +__version__ = "0.8.0" From d67427c51329716f95bcbb1a9d9b6791b37b2ccd Mon Sep 17 00:00:00 2001 From: Constantin Pape Date: Thu, 23 Jul 2026 15:59:43 +0200 Subject: [PATCH 2/2] Add GASP example --- examples/segmentation/gasp.py | 181 ++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 examples/segmentation/gasp.py diff --git a/examples/segmentation/gasp.py b/examples/segmentation/gasp.py new file mode 100644 index 0000000..924b884 --- /dev/null +++ b/examples/segmentation/gasp.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import argparse + +import napari +import numpy as np +from skimage.feature import peak_local_max +from skimage.measure import label as label_components +from skimage.segmentation import find_boundaries, watershed + +import bioimage_cpp as bic +from bioimage_cpp._data import load_isbi_affinities, load_isbi_raw + + +def load_problem(ndim: int, z_slice: int): + affinities, offsets = load_isbi_affinities() + offsets = [tuple(int(v) for v in offset) for offset in offsets] + if ndim == 2: + channels_2d = [index for index, offset in enumerate(offsets) if offset[0] == 0] + affinities = affinities[channels_2d, z_slice] + offsets = [offsets[index][1:] for index in channels_2d] + elif ndim != 3: + raise ValueError(f"ndim must be 2 or 3, got {ndim}") + + direct_channels = [ + index for index, offset in enumerate(offsets) if sum(abs(v) for v in offset) == 1 + ] + direct_affinities = np.ascontiguousarray(affinities[direct_channels], dtype=np.float32) + direct_offsets = [offsets[index] for index in direct_channels] + return direct_affinities, direct_offsets + + +def load_raw(ndim: int, z_slice: int) -> np.ndarray: + raw = load_isbi_raw() + return np.ascontiguousarray(raw[z_slice] if ndim == 2 else raw) + + +def make_heightmap(affinities: np.ndarray) -> np.ndarray: + return np.ascontiguousarray(np.mean(affinities, axis=0), dtype=np.float32) + + +def make_watershed_oversegmentation( + heightmap: np.ndarray, + *, + min_distance: int, + grid_spacing: int, + max_markers: int, +) -> np.ndarray: + coordinates = peak_local_max( + -heightmap, + min_distance=min_distance, + exclude_border=False, + num_peaks=max_markers, + ) + marker_mask = np.zeros(heightmap.shape, dtype=bool) + if len(coordinates) > 0: + marker_mask[tuple(coordinates.T)] = True + markers = label_components(marker_mask).astype(np.int32, copy=False) + + if int(markers.max()) < 2: + markers = np.zeros(heightmap.shape, dtype=np.int32) + slices = tuple(slice(None, None, grid_spacing) for _ in heightmap.shape) + marker_coordinates = np.argwhere(np.ones(heightmap.shape, dtype=bool)[slices]) + marker_coordinates *= grid_spacing + for marker_id, coord in enumerate(marker_coordinates, start=1): + markers[tuple(coord)] = marker_id + + return watershed(heightmap, markers=markers).astype(np.uint32, copy=False) + + +def gasp_from_affinities( + affinities: np.ndarray, + offsets: list[tuple[int, ...]], + *, + threshold: float, + linkage: str, + num_clusters_stop: int, + number_of_threads: int, + watershed_min_distance: int, + watershed_grid_spacing: int, + max_markers: int, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + heightmap = make_heightmap(affinities) + oversegmentation = make_watershed_oversegmentation( + heightmap, + min_distance=watershed_min_distance, + grid_spacing=watershed_grid_spacing, + max_markers=max_markers, + ) + + rag = bic.graph.region_adjacency_graph( + oversegmentation, number_of_threads=number_of_threads + ) + features = bic.graph.features.affinity_features( + rag, + oversegmentation, + affinities, + offsets, + number_of_threads=number_of_threads, + ) + edge_weights = threshold - features[:, 0] + edge_sizes = features[:, 1] + + node_labels = bic.graph.agglomeration.GaspClusterPolicy( + num_clusters_stop=num_clusters_stop, + linkage=linkage, + ).optimize(rag, edge_weights, edge_sizes=edge_sizes) + segmentation = bic.graph.project_node_labels_to_pixels( + rag, + oversegmentation, + node_labels, + number_of_threads=number_of_threads, + ) + return heightmap, oversegmentation, segmentation + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Run watershed oversegmentation + GASP graph agglomeration on the " + "ISBI affinity example." + ) + ) + parser.add_argument("--ndim", type=int, choices=(2, 3), default=2) + parser.add_argument("--z-slice", type=int, default=0) + parser.add_argument( + "--threshold", + type=float, + default=0.1, + help=( + "Affinity threshold for signed edge weights. Lower affinities " + "produce attractive edges (default: %(default)s)." + ), + ) + parser.add_argument( + "--linkage", + choices=("sum", "mean", "max", "min", "abs_max", "mutex_watershed"), + default="mean", + help="GASP linkage rule (default: %(default)s).", + ) + parser.add_argument( + "--num-clusters-stop", + type=int, + default=1, + help=( + "Stop at this many RAG clusters. GASP may stop earlier when no " + "attractive edge remains (default: %(default)s)." + ), + ) + parser.add_argument("--threads", type=int, default=0) + parser.add_argument("--watershed-min-distance", type=int, default=5) + parser.add_argument("--watershed-grid-spacing", type=int, default=12) + parser.add_argument("--max-markers", type=int, default=2048) + args = parser.parse_args() + + affinities, offsets = load_problem(args.ndim, args.z_slice) + raw = load_raw(args.ndim, args.z_slice) + heightmap, oversegmentation, segmentation = gasp_from_affinities( + affinities, + offsets, + threshold=args.threshold, + linkage=args.linkage, + num_clusters_stop=args.num_clusters_stop, + number_of_threads=args.threads, + watershed_min_distance=args.watershed_min_distance, + watershed_grid_spacing=args.watershed_grid_spacing, + max_markers=args.max_markers, + ) + + viewer = napari.Viewer() + viewer.add_image(raw, name="raw") + viewer.add_image(affinities, name="direct affinities") + viewer.add_image(heightmap, name="watershed heightmap") + viewer.add_labels(oversegmentation, name="watershed oversegmentation") + viewer.add_labels(segmentation, name="GASP segmentation") + viewer.add_labels(find_boundaries(segmentation), name="GASP boundaries") + napari.run() + + +if __name__ == "__main__": + main()