Skip to content

Commit b63d534

Browse files
Add script to build and serialize grid LMC problem, add more sample data
1 parent 1f8984c commit b63d534

2 files changed

Lines changed: 192 additions & 1 deletion

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""Serialize a grid-graph ISBI lifted-multicut problem to a .npz file.
2+
3+
This builds a lifted multicut problem directly on the regular pixel/voxel grid
4+
from the ISBI example affinities. It uses ``grid_graph`` plus
5+
``grid_affinity_features_with_lifted`` and writes the same fields as
6+
``serialize_lifted_problem.py``:
7+
8+
n_nodes : scalar uint64
9+
local_uvs : (n_local, 2) uint64
10+
local_costs : (n_local,) float64
11+
lifted_uvs : (n_lifted, 2) uint64
12+
lifted_costs : (n_lifted,) float64
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
from pathlib import Path
19+
20+
import numpy as np
21+
22+
import bioimage_cpp as bic
23+
24+
25+
THIS_DIR = Path(__file__).resolve().parent
26+
DEFAULT_DATA_PREFIX = THIS_DIR / "isbi-data-"
27+
DEFAULT_OUTPUT = THIS_DIR / "grid_lifted_multicut_problem.npz"
28+
29+
30+
def load_affinities(
31+
data_prefix: Path,
32+
ndim: int,
33+
spatial_shape: tuple[int, ...],
34+
z_slice: int,
35+
) -> tuple[np.ndarray, list[tuple[int, ...]]]:
36+
from elf.segmentation.utils import load_mutex_watershed_problem
37+
38+
affinities, offsets = load_mutex_watershed_problem(prefix=str(data_prefix))
39+
offsets = [tuple(int(v) for v in offset) for offset in offsets]
40+
41+
if ndim == 2:
42+
y, x = spatial_shape
43+
channels_2d = [index for index, offset in enumerate(offsets) if offset[0] == 0]
44+
affinities = affinities[channels_2d, z_slice, :y, :x]
45+
offsets = [offsets[index][1:] for index in channels_2d]
46+
elif ndim == 3:
47+
z, y, x = spatial_shape
48+
affinities = affinities[:, :z, :y, :x]
49+
else:
50+
raise ValueError(f"ndim must be 2 or 3, got {ndim}")
51+
52+
if affinities.shape[1:] != spatial_shape:
53+
raise ValueError(
54+
f"requested spatial shape {spatial_shape} exceeds available data; "
55+
f"extracted shape is {affinities.shape[1:]}"
56+
)
57+
58+
return np.ascontiguousarray(affinities, dtype=np.float32), offsets
59+
60+
61+
def parse_spatial_shape(values: list[int] | None, ndim: int) -> tuple[int, ...]:
62+
if values is None:
63+
return (256, 256) if ndim == 2 else (16, 256, 256)
64+
if len(values) != ndim:
65+
raise ValueError(
66+
f"--spatial-shape must contain {ndim} values for {ndim}D, "
67+
f"got {len(values)}"
68+
)
69+
if any(value <= 0 for value in values):
70+
raise ValueError("--spatial-shape values must be positive")
71+
return tuple(values)
72+
73+
74+
def build_grid_lifted_problem(
75+
affinities: np.ndarray,
76+
offsets: list[tuple[int, ...]],
77+
*,
78+
local_threshold: float,
79+
lifted_threshold: float,
80+
):
81+
graph = bic.graph.grid_graph(affinities.shape[1:])
82+
local_weights, valid_edges, lifted_uvs, lifted_weights, _ = (
83+
bic.graph.grid_affinity_features_with_lifted(graph, affinities, offsets)
84+
)
85+
if not np.all(valid_edges):
86+
invalid = int(valid_edges.size - np.count_nonzero(valid_edges))
87+
raise RuntimeError(
88+
"local affinity offsets did not cover all grid graph edges; "
89+
f"{invalid} edges are missing"
90+
)
91+
92+
local_costs = (local_threshold - local_weights).astype(np.float64, copy=False)
93+
lifted_costs = (lifted_threshold - lifted_weights).astype(np.float64, copy=False)
94+
return (
95+
int(graph.number_of_nodes),
96+
graph.uv_ids(),
97+
np.ascontiguousarray(local_costs),
98+
np.ascontiguousarray(lifted_uvs.astype(np.uint64, copy=False)),
99+
np.ascontiguousarray(lifted_costs),
100+
)
101+
102+
103+
def main():
104+
parser = argparse.ArgumentParser(
105+
description=(
106+
"Build an ISBI lifted multicut problem directly on a regular grid "
107+
"graph and serialize it to a .npz file."
108+
)
109+
)
110+
parser.add_argument("--ndim", type=int, choices=(2, 3), default=2)
111+
parser.add_argument(
112+
"--spatial-shape",
113+
type=int,
114+
nargs="+",
115+
default=None,
116+
metavar=("Y", "X"),
117+
help=(
118+
"Spatial crop shape. Pass Y X for 2D or Z Y X for 3D. "
119+
"Defaults to 256 256 for 2D and 16 256 256 for 3D."
120+
),
121+
)
122+
parser.add_argument(
123+
"--z-slice",
124+
type=int,
125+
default=0,
126+
help="Z slice used for 2D extraction.",
127+
)
128+
parser.add_argument("--data-prefix", type=Path, default=DEFAULT_DATA_PREFIX)
129+
parser.add_argument("--local-threshold", type=float, default=0.1)
130+
parser.add_argument("--lifted-threshold", type=float, default=0.1)
131+
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
132+
args = parser.parse_args()
133+
134+
spatial_shape = parse_spatial_shape(args.spatial_shape, args.ndim)
135+
affinities, offsets = load_affinities(
136+
args.data_prefix, args.ndim, spatial_shape, args.z_slice
137+
)
138+
n_nodes, local_uvs, local_costs, lifted_uvs, lifted_costs = (
139+
build_grid_lifted_problem(
140+
affinities,
141+
offsets,
142+
local_threshold=args.local_threshold,
143+
lifted_threshold=args.lifted_threshold,
144+
)
145+
)
146+
147+
local_uvs = np.ascontiguousarray(local_uvs.astype(np.uint64, copy=False))
148+
n_nodes_array = np.uint64(n_nodes)
149+
150+
args.output.parent.mkdir(parents=True, exist_ok=True)
151+
np.savez_compressed(
152+
args.output,
153+
n_nodes=n_nodes_array,
154+
local_uvs=local_uvs,
155+
local_costs=local_costs,
156+
lifted_uvs=lifted_uvs,
157+
lifted_costs=lifted_costs,
158+
)
159+
160+
print(f"Wrote grid lifted multicut problem to {args.output}")
161+
print(f" ndim: {args.ndim}")
162+
print(f" spatial shape: {spatial_shape}")
163+
print(f" number of nodes: {n_nodes}")
164+
print(f" number of local edges: {local_uvs.shape[0]}")
165+
print(f" number of lifted edges: {lifted_uvs.shape[0]}")
166+
if local_costs.size:
167+
print(
168+
f" local cost range: [{float(local_costs.min()):+.3f}, "
169+
f"{float(local_costs.max()):+.3f}]"
170+
)
171+
if lifted_costs.size:
172+
print(
173+
f" lifted cost range: [{float(lifted_costs.min()):+.3f}, "
174+
f"{float(lifted_costs.max()):+.3f}]"
175+
)
176+
177+
178+
if __name__ == "__main__":
179+
main()

src/bioimage_cpp/_data.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
``examples/segmentation/serialize_lifted_problem.py``):
1818
1919
- ``lifted_multicut_problem_2d.npz`` — 2D ISBI slice (small, ~756 nodes).
20-
- ``lifted_multicut_problem_3d.npz`` — full 3D ISBI volume (large, ~18k 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.h5`` — HDF5 file with sample affinities from the ISBI volume. Contains affinties under key `affinities`.
2125
"""
2226

2327
from __future__ import annotations
@@ -66,6 +70,14 @@
6670
"https://owncloud.gwdg.de/index.php/s/ZVzDy8Xb0Dr2Ell/download",
6771
"269ce644e2b9f8259f7f2ff827d5808ac5c9bfe6ca0444e298290f23867dce8a",
6872
),
73+
"lifted_multicut_problem_grid.npz": (
74+
"https://owncloud.gwdg.de/index.php/s/YWNZSYsBd1VwSX1/download",
75+
"20583b2000838ed0942f8f1c343b84287d8bf218d19d77a8b5627924661c5aa3",
76+
),
77+
"affinities": (
78+
"https://owncloud.gwdg.de/index.php/s/aAyF2ekzsW7DFJo/download",
79+
"6472ad0fcf3c57a4ae345fda68c3cbb6072ee3e8db67b423502746b46d8cd5e5",
80+
),
6981
}
7082

7183

0 commit comments

Comments
 (0)