topologysegmentation 0.1.0__py3-none-any.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.
Files changed (40) hide show
  1. topologysegmentation/VERSION +1 -0
  2. topologysegmentation/__init__.py +27 -0
  3. topologysegmentation/_native/__init__.py +112 -0
  4. topologysegmentation/_native/cubical_betti.c +329 -0
  5. topologysegmentation/_native/cubical_matching.c +427 -0
  6. topologysegmentation/_native/fallback.py +159 -0
  7. topologysegmentation/_native/matching.py +270 -0
  8. topologysegmentation/losses/__init__.py +74 -0
  9. topologysegmentation/losses/common.py +36 -0
  10. topologysegmentation/losses/critical.py +44 -0
  11. topologysegmentation/losses/hu.py +83 -0
  12. topologysegmentation/losses/pooling.py +100 -0
  13. topologysegmentation/losses/registry.py +61 -0
  14. topologysegmentation/losses/segmentation.py +81 -0
  15. topologysegmentation/losses/topology.py +231 -0
  16. topologysegmentation/losses/vendor/__init__.py +1 -0
  17. topologysegmentation/losses/vendor/bm/BettiMatching.py +3040 -0
  18. topologysegmentation/losses/vendor/bm/__init__.py +0 -0
  19. topologysegmentation/losses/vendor/bm/loss_functions.py +158 -0
  20. topologysegmentation/losses/vendor/bm3d/__init__.py +1 -0
  21. topologysegmentation/losses/vendor/bm3d/loss.py +154 -0
  22. topologysegmentation/losses/vendor/cldice/__init__.py +0 -0
  23. topologysegmentation/losses/vendor/cldice/cldice.py +73 -0
  24. topologysegmentation/losses/vendor/cldice/soft_skeleton.py +56 -0
  25. topologysegmentation/losses/vendor/sat/PDMatching.py +116 -0
  26. topologysegmentation/losses/vendor/sat/SATLoss.py +104 -0
  27. topologysegmentation/losses/vendor/sat/__init__.py +0 -0
  28. topologysegmentation/losses/vendor/sat/cubical_complex.py +106 -0
  29. topologysegmentation/losses/vendor/tloss/LICENSE +182 -0
  30. topologysegmentation/losses/vendor/tloss/__init__.py +0 -0
  31. topologysegmentation/losses/vendor/tloss/tloss.py +126 -0
  32. topologysegmentation/metrics.py +133 -0
  33. topologysegmentation/persistence.py +140 -0
  34. topologysegmentation-0.1.0.dist-info/METADATA +139 -0
  35. topologysegmentation-0.1.0.dist-info/RECORD +40 -0
  36. topologysegmentation-0.1.0.dist-info/WHEEL +5 -0
  37. topologysegmentation-0.1.0.dist-info/licenses/LICENSE +21 -0
  38. topologysegmentation-0.1.0.dist-info/licenses/NOTICE +39 -0
  39. topologysegmentation-0.1.0.dist-info/licenses/src/topologysegmentation/losses/vendor/tloss/LICENSE +182 -0
  40. topologysegmentation-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,27 @@
1
+ """Losses and metrics for topology-aware segmentation.
2
+
3
+ The VessMAP training pipeline lives in ``experiments/`` and is not installed
4
+ with this package. The release number is the single line in ``VERSION``.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ __version__ = Path(__file__).with_name("VERSION").read_text(encoding="utf-8").strip()
10
+
11
+ from topologysegmentation.losses.registry import available_losses, create_loss, loss_shapes
12
+ from topologysegmentation.losses.segmentation import BCELoss, CombinedLoss, DiceLoss, FocalLoss
13
+ from topologysegmentation.metrics import betti_numbers, centerline_dice, dice_iou, topology_errors
14
+
15
+ __all__ = [
16
+ "BCELoss",
17
+ "CombinedLoss",
18
+ "DiceLoss",
19
+ "FocalLoss",
20
+ "available_losses",
21
+ "betti_numbers",
22
+ "centerline_dice",
23
+ "create_loss",
24
+ "dice_iou",
25
+ "loss_shapes",
26
+ "topology_errors",
27
+ ]
@@ -0,0 +1,112 @@
1
+ """Optional C implementation of binary cubical Betti numbers.
2
+
3
+ The source ships inside the wheel. The first call compiles it with the system
4
+ C compiler and caches the library under ``~/.cache/topologysegmentation``.
5
+ ``TOPOLOGYSEGMENTATION_NATIVE=0`` keeps the NumPy fallback. Install itself
6
+ does not need a compiler.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import hashlib
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import threading
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+ _LOCK = threading.Lock()
23
+ _LIBRARY = None
24
+ _UNAVAILABLE = False
25
+
26
+
27
+ def binary_betti(mask: np.ndarray) -> tuple[int, ...] | None:
28
+ """Return Betti numbers, or ``None`` when the compiled routine is unavailable."""
29
+ library = _library()
30
+ if library is None:
31
+ return None
32
+ out = np.zeros(mask.ndim, dtype=np.int32)
33
+ shape = np.ascontiguousarray(mask.shape, dtype=np.int64)
34
+ code = library.cubical_betti(
35
+ mask.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
36
+ int(mask.ndim),
37
+ shape.ctypes.data_as(ctypes.POINTER(ctypes.c_int64)),
38
+ out.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)),
39
+ )
40
+ if code != 0:
41
+ return None
42
+ return tuple(int(value) for value in out)
43
+
44
+
45
+ def _library():
46
+ global _LIBRARY, _UNAVAILABLE
47
+ if _UNAVAILABLE:
48
+ return None
49
+ if _LIBRARY is not None:
50
+ return _LIBRARY
51
+ with _LOCK:
52
+ if _LIBRARY is not None or _UNAVAILABLE:
53
+ return _LIBRARY
54
+ if os.environ.get("TOPOLOGYSEGMENTATION_NATIVE") == "0":
55
+ _UNAVAILABLE = True
56
+ return None
57
+ try:
58
+ _LIBRARY = _compile()
59
+ except (OSError, subprocess.SubprocessError):
60
+ _LIBRARY = None
61
+ _UNAVAILABLE = True
62
+ return _LIBRARY
63
+
64
+
65
+ def _compile():
66
+ source = Path(__file__).with_name("cubical_betti.c")
67
+ digest = hashlib.sha256(source.read_bytes()).hexdigest()[:16]
68
+ suffix = ".dylib" if sys.platform == "darwin" else ".so"
69
+ cache = _cache_dir()
70
+ cache.mkdir(parents=True, exist_ok=True)
71
+ library_path = cache / f"cubical_betti_{digest}{suffix}"
72
+ if not library_path.exists():
73
+ compiler = _compiler()
74
+ link = "-dynamiclib" if sys.platform == "darwin" else "-shared"
75
+ command = [compiler, "-O3", "-std=c11", "-fPIC", link, "-o", str(library_path), str(source)]
76
+ subprocess.run(command, check=True, capture_output=True)
77
+ library = ctypes.CDLL(str(library_path))
78
+ library.cubical_betti.argtypes = [
79
+ ctypes.POINTER(ctypes.c_uint8),
80
+ ctypes.c_int,
81
+ ctypes.POINTER(ctypes.c_int64),
82
+ ctypes.POINTER(ctypes.c_int32),
83
+ ]
84
+ library.cubical_betti.restype = ctypes.c_int
85
+ return library
86
+
87
+
88
+ def _compiler() -> str:
89
+ for name in ("clang", "cc", "gcc"):
90
+ if _which(name):
91
+ return name
92
+ raise OSError("No C compiler for the Betti routine")
93
+
94
+
95
+ def _which(name: str) -> bool:
96
+ path = os.environ.get("PATH", "")
97
+ for directory in path.split(os.pathsep):
98
+ if (Path(directory) / name).is_file():
99
+ return True
100
+ return False
101
+
102
+
103
+ def _cache_dir() -> Path:
104
+ override = os.environ.get("TOPOLOGYSEGMENTATION_CACHE")
105
+ if override:
106
+ return Path(override)
107
+ home_cache = Path.home() / ".cache" / "topologysegmentation"
108
+ try:
109
+ home_cache.mkdir(parents=True, exist_ok=True)
110
+ return home_cache
111
+ except OSError:
112
+ return Path(tempfile.gettempdir()) / "topologysegmentation"
@@ -0,0 +1,329 @@
1
+ /* Binary cubical Betti numbers for a 2D or 3D mask.
2
+
3
+ Foreground cells that share a corner are one component (8-connected in 2D,
4
+ 26-connected in 3D). Voids are finite 6-connected pieces of the complement.
5
+ The Euler characteristic uses the same closed cubes as Gudhi, so the counts
6
+ match a superlevel diagram of a 0/1 image read at a threshold in (0, 1].
7
+ */
8
+
9
+ #include <stdint.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+
13
+ static int64_t find_root(int32_t *parent, int64_t x) {
14
+ int64_t root = x;
15
+ while (parent[root] != root) {
16
+ root = parent[root];
17
+ }
18
+ while (parent[x] != (int32_t)root) {
19
+ int32_t next = parent[x];
20
+ parent[x] = (int32_t)root;
21
+ x = next;
22
+ }
23
+ return root;
24
+ }
25
+
26
+ static void unite(int32_t *parent, int64_t a, int64_t b) {
27
+ a = find_root(parent, a);
28
+ b = find_root(parent, b);
29
+ if (a != b) {
30
+ parent[b] = (int32_t)a;
31
+ }
32
+ }
33
+
34
+ static int link_if_set(int32_t *parent, int64_t i, int64_t j) {
35
+ if (parent[j] >= 0) {
36
+ unite(parent, i, j);
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ static int betti2(const uint8_t *mask, int64_t height, int64_t width, int32_t *out) {
42
+ int64_t count = height * width;
43
+ int32_t *parent = (int32_t *)malloc((size_t)count * sizeof(int32_t));
44
+ if (parent == NULL) {
45
+ return -1;
46
+ }
47
+ for (int64_t i = 0; i < count; i++) {
48
+ parent[i] = -1;
49
+ }
50
+ for (int64_t row = 0; row < height; row++) {
51
+ for (int64_t col = 0; col < width; col++) {
52
+ int64_t index = row * width + col;
53
+ if (!mask[index]) {
54
+ continue;
55
+ }
56
+ parent[index] = (int32_t)index;
57
+ if (row > 0) {
58
+ if (col > 0) {
59
+ link_if_set(parent, index, index - width - 1);
60
+ }
61
+ link_if_set(parent, index, index - width);
62
+ if (col + 1 < width) {
63
+ link_if_set(parent, index, index - width + 1);
64
+ }
65
+ }
66
+ if (col > 0) {
67
+ link_if_set(parent, index, index - 1);
68
+ }
69
+ }
70
+ }
71
+ int64_t components = 0;
72
+ int64_t faces = 0;
73
+ for (int64_t i = 0; i < count; i++) {
74
+ if (parent[i] >= 0) {
75
+ faces += 1;
76
+ if (find_root(parent, i) == i) {
77
+ components += 1;
78
+ }
79
+ }
80
+ }
81
+ free(parent);
82
+
83
+ int64_t edges = 0;
84
+ for (int64_t row = 0; row <= height; row++) {
85
+ for (int64_t col = 0; col < width; col++) {
86
+ int above = row > 0 && mask[(row - 1) * width + col];
87
+ int below = row < height && mask[row * width + col];
88
+ edges += above || below;
89
+ }
90
+ }
91
+ for (int64_t row = 0; row < height; row++) {
92
+ for (int64_t col = 0; col <= width; col++) {
93
+ int left = col > 0 && mask[row * width + (col - 1)];
94
+ int right = col < width && mask[row * width + col];
95
+ edges += left || right;
96
+ }
97
+ }
98
+ int64_t vertices = 0;
99
+ for (int64_t row = 0; row <= height; row++) {
100
+ for (int64_t col = 0; col <= width; col++) {
101
+ int on = 0;
102
+ if (row > 0 && col > 0 && mask[(row - 1) * width + (col - 1)]) on = 1;
103
+ if (row > 0 && col < width && mask[(row - 1) * width + col]) on = 1;
104
+ if (row < height && col > 0 && mask[row * width + (col - 1)]) on = 1;
105
+ if (row < height && col < width && mask[row * width + col]) on = 1;
106
+ vertices += on;
107
+ }
108
+ }
109
+ out[0] = (int32_t)components;
110
+ out[1] = (int32_t)(components - (vertices - edges + faces));
111
+ return 0;
112
+ }
113
+
114
+ static int64_t offset3(int64_t z, int64_t y, int64_t x, int64_t height, int64_t width) {
115
+ return (z * height + y) * width + x;
116
+ }
117
+
118
+ static int betti3(const uint8_t *mask, int64_t depth, int64_t height, int64_t width, int32_t *out) {
119
+ int64_t count = depth * height * width;
120
+ int32_t *parent = (int32_t *)malloc((size_t)count * sizeof(int32_t));
121
+ int32_t *background = (int32_t *)malloc((size_t)count * sizeof(int32_t));
122
+ uint8_t *touches = (uint8_t *)malloc((size_t)count);
123
+ if (parent == NULL || background == NULL || touches == NULL) {
124
+ free(parent);
125
+ free(background);
126
+ free(touches);
127
+ return -1;
128
+ }
129
+ for (int64_t i = 0; i < count; i++) {
130
+ parent[i] = -1;
131
+ background[i] = -1;
132
+ touches[i] = 0;
133
+ }
134
+ static const int neighbor[13][3] = {
135
+ {-1, -1, -1}, {-1, -1, 0}, {-1, -1, 1}, {-1, 0, -1}, {-1, 0, 0},
136
+ {-1, 0, 1}, {-1, 1, -1}, {-1, 1, 0}, {-1, 1, 1}, {0, -1, -1},
137
+ {0, -1, 0}, {0, -1, 1}, {0, 0, -1},
138
+ };
139
+ for (int64_t z = 0; z < depth; z++) {
140
+ for (int64_t y = 0; y < height; y++) {
141
+ for (int64_t x = 0; x < width; x++) {
142
+ int64_t index = offset3(z, y, x, height, width);
143
+ if (!mask[index]) {
144
+ continue;
145
+ }
146
+ parent[index] = (int32_t)index;
147
+ for (int k = 0; k < 13; k++) {
148
+ int64_t zz = z + neighbor[k][0];
149
+ int64_t yy = y + neighbor[k][1];
150
+ int64_t xx = x + neighbor[k][2];
151
+ if (zz < 0 || yy < 0 || xx < 0 || yy >= height || xx >= width) {
152
+ continue;
153
+ }
154
+ int64_t other = offset3(zz, yy, xx, height, width);
155
+ link_if_set(parent, index, other);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ int64_t components = 0;
161
+ int64_t cubes = 0;
162
+ for (int64_t i = 0; i < count; i++) {
163
+ if (parent[i] >= 0) {
164
+ cubes += 1;
165
+ if (find_root(parent, i) == i) {
166
+ components += 1;
167
+ }
168
+ }
169
+ }
170
+ free(parent);
171
+
172
+ for (int64_t z = 0; z < depth; z++) {
173
+ for (int64_t y = 0; y < height; y++) {
174
+ for (int64_t x = 0; x < width; x++) {
175
+ int64_t index = offset3(z, y, x, height, width);
176
+ if (mask[index]) {
177
+ continue;
178
+ }
179
+ background[index] = (int32_t)index;
180
+ if (z > 0) link_if_set(background, index, offset3(z - 1, y, x, height, width));
181
+ if (y > 0) link_if_set(background, index, offset3(z, y - 1, x, height, width));
182
+ if (x > 0) link_if_set(background, index, offset3(z, y, x - 1, height, width));
183
+ }
184
+ }
185
+ }
186
+ for (int64_t z = 0; z < depth; z++) {
187
+ for (int64_t y = 0; y < height; y++) {
188
+ for (int64_t x = 0; x < width; x++) {
189
+ if (z != 0 && y != 0 && x != 0 && z + 1 != depth && y + 1 != height && x + 1 != width) {
190
+ continue;
191
+ }
192
+ int64_t index = offset3(z, y, x, height, width);
193
+ if (background[index] >= 0) {
194
+ touches[find_root(background, index)] = 1;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ int64_t voids = 0;
200
+ for (int64_t i = 0; i < count; i++) {
201
+ if (background[i] >= 0 && find_root(background, i) == i && !touches[i]) {
202
+ voids += 1;
203
+ }
204
+ }
205
+ free(background);
206
+ free(touches);
207
+
208
+ int64_t faces = 0;
209
+ int64_t edges = 0;
210
+ int64_t vertices = 0;
211
+ for (int64_t z = 0; z <= depth; z++) {
212
+ for (int64_t y = 0; y < height; y++) {
213
+ for (int64_t x = 0; x < width; x++) {
214
+ int behind = z > 0 && mask[offset3(z - 1, y, x, height, width)];
215
+ int ahead = z < depth && mask[offset3(z, y, x, height, width)];
216
+ faces += behind || ahead;
217
+ }
218
+ }
219
+ }
220
+ for (int64_t z = 0; z < depth; z++) {
221
+ for (int64_t y = 0; y <= height; y++) {
222
+ for (int64_t x = 0; x < width; x++) {
223
+ int behind = y > 0 && mask[offset3(z, y - 1, x, height, width)];
224
+ int ahead = y < height && mask[offset3(z, y, x, height, width)];
225
+ faces += behind || ahead;
226
+ }
227
+ }
228
+ }
229
+ for (int64_t z = 0; z < depth; z++) {
230
+ for (int64_t y = 0; y < height; y++) {
231
+ for (int64_t x = 0; x <= width; x++) {
232
+ int behind = x > 0 && mask[offset3(z, y, x - 1, height, width)];
233
+ int ahead = x < width && mask[offset3(z, y, x, height, width)];
234
+ faces += behind || ahead;
235
+ }
236
+ }
237
+ }
238
+ for (int64_t z = 0; z <= depth; z++) {
239
+ for (int64_t y = 0; y <= height; y++) {
240
+ for (int64_t x = 0; x < width; x++) {
241
+ int on = 0;
242
+ for (int dz = 0; dz < 2; dz++) {
243
+ for (int dy = 0; dy < 2; dy++) {
244
+ int64_t zz = z - dz;
245
+ int64_t yy = y - dy;
246
+ if (zz < 0 || yy < 0 || zz >= depth || yy >= height) continue;
247
+ if (mask[offset3(zz, yy, x, height, width)]) on = 1;
248
+ }
249
+ }
250
+ edges += on;
251
+ }
252
+ }
253
+ }
254
+ for (int64_t z = 0; z <= depth; z++) {
255
+ for (int64_t y = 0; y < height; y++) {
256
+ for (int64_t x = 0; x <= width; x++) {
257
+ int on = 0;
258
+ for (int dz = 0; dz < 2; dz++) {
259
+ for (int dx = 0; dx < 2; dx++) {
260
+ int64_t zz = z - dz;
261
+ int64_t xx = x - dx;
262
+ if (zz < 0 || xx < 0 || zz >= depth || xx >= width) continue;
263
+ if (mask[offset3(zz, y, xx, height, width)]) on = 1;
264
+ }
265
+ }
266
+ edges += on;
267
+ }
268
+ }
269
+ }
270
+ for (int64_t z = 0; z < depth; z++) {
271
+ for (int64_t y = 0; y <= height; y++) {
272
+ for (int64_t x = 0; x <= width; x++) {
273
+ int on = 0;
274
+ for (int dy = 0; dy < 2; dy++) {
275
+ for (int dx = 0; dx < 2; dx++) {
276
+ int64_t yy = y - dy;
277
+ int64_t xx = x - dx;
278
+ if (yy < 0 || xx < 0 || yy >= height || xx >= width) continue;
279
+ if (mask[offset3(z, yy, xx, height, width)]) on = 1;
280
+ }
281
+ }
282
+ edges += on;
283
+ }
284
+ }
285
+ }
286
+ for (int64_t z = 0; z <= depth; z++) {
287
+ for (int64_t y = 0; y <= height; y++) {
288
+ for (int64_t x = 0; x <= width; x++) {
289
+ int on = 0;
290
+ for (int dz = 0; dz < 2; dz++) {
291
+ for (int dy = 0; dy < 2; dy++) {
292
+ for (int dx = 0; dx < 2; dx++) {
293
+ int64_t zz = z - dz;
294
+ int64_t yy = y - dy;
295
+ int64_t xx = x - dx;
296
+ if (zz < 0 || yy < 0 || xx < 0 || zz >= depth || yy >= height || xx >= width) {
297
+ continue;
298
+ }
299
+ if (mask[offset3(zz, yy, xx, height, width)]) on = 1;
300
+ }
301
+ }
302
+ }
303
+ vertices += on;
304
+ }
305
+ }
306
+ }
307
+ int64_t chi = vertices - edges + faces - cubes;
308
+ out[0] = (int32_t)components;
309
+ out[1] = (int32_t)(components + voids - chi);
310
+ out[2] = (int32_t)voids;
311
+ return 0;
312
+ }
313
+
314
+ int cubical_betti(const uint8_t *mask, int ndim, const int64_t *shape, int32_t *out) {
315
+ if (mask == NULL || shape == NULL || out == NULL || (ndim != 2 && ndim != 3)) {
316
+ return -2;
317
+ }
318
+ int64_t count = 1;
319
+ for (int axis = 0; axis < ndim; axis++) {
320
+ if (shape[axis] <= 0 || count > (INT64_C(0x7fffffff) / shape[axis])) {
321
+ return -1;
322
+ }
323
+ count *= shape[axis];
324
+ }
325
+ if (ndim == 2) {
326
+ return betti2(mask, shape[0], shape[1], out);
327
+ }
328
+ return betti3(mask, shape[0], shape[1], shape[2], out);
329
+ }