labelimage-tools 0.1.3__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.
- labelimage_tools/__init__.py +117 -0
- labelimage_tools/_bbox.py +148 -0
- labelimage_tools/_graph_io.py +298 -0
- labelimage_tools/_optional.py +26 -0
- labelimage_tools/adjacency.py +363 -0
- labelimage_tools/coloring.py +408 -0
- labelimage_tools/contours.py +86 -0
- labelimage_tools/io.py +264 -0
- labelimage_tools/junctions.py +417 -0
- labelimage_tools/plotting.py +433 -0
- labelimage_tools/preprocessing.py +520 -0
- labelimage_tools/typing.py +17 -0
- labelimage_tools/validation.py +78 -0
- labelimage_tools-0.1.3.dist-info/METADATA +286 -0
- labelimage_tools-0.1.3.dist-info/RECORD +18 -0
- labelimage_tools-0.1.3.dist-info/WHEEL +5 -0
- labelimage_tools-0.1.3.dist-info/licenses/LICENSE +28 -0
- labelimage_tools-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Reusable utilities for 2-D labeled tissue segmentation images."""
|
|
2
|
+
|
|
3
|
+
from .adjacency import (
|
|
4
|
+
adjacency_from_labels,
|
|
5
|
+
adjacency_pairs_from_labels,
|
|
6
|
+
adjacency_with_contact_from_labels,
|
|
7
|
+
adjacency_with_unique_from_labels,
|
|
8
|
+
border_labels,
|
|
9
|
+
centroids_from_labels,
|
|
10
|
+
get_centroids,
|
|
11
|
+
graph_from_labels,
|
|
12
|
+
label_is_border,
|
|
13
|
+
label_pixel_counts,
|
|
14
|
+
)
|
|
15
|
+
from .coloring import (
|
|
16
|
+
apply_color_lut_int,
|
|
17
|
+
color_planar_with_variety,
|
|
18
|
+
dsatur_color,
|
|
19
|
+
rebalance_K_colors,
|
|
20
|
+
refine_to_K_colors,
|
|
21
|
+
show_map_with_colors,
|
|
22
|
+
)
|
|
23
|
+
from .contours import ordered_contour_from_mask, ordered_contours_from_labels
|
|
24
|
+
from .io import (
|
|
25
|
+
load_img,
|
|
26
|
+
load_label_graph,
|
|
27
|
+
load_label_image,
|
|
28
|
+
save_img,
|
|
29
|
+
save_label_graph,
|
|
30
|
+
save_label_graph_from_labels,
|
|
31
|
+
save_label_image,
|
|
32
|
+
)
|
|
33
|
+
from .junctions import (
|
|
34
|
+
Junction,
|
|
35
|
+
cluster_junctions_with_labels,
|
|
36
|
+
junction_pixels_with_labels,
|
|
37
|
+
junctions_from_labels,
|
|
38
|
+
merge_close_junctions,
|
|
39
|
+
)
|
|
40
|
+
from .plotting import (
|
|
41
|
+
draw_graph,
|
|
42
|
+
label_map,
|
|
43
|
+
plot_adjacency_graph,
|
|
44
|
+
plot_contours,
|
|
45
|
+
plot_junctions,
|
|
46
|
+
plot_label_boundaries,
|
|
47
|
+
plot_label_image,
|
|
48
|
+
)
|
|
49
|
+
from .preprocessing import (
|
|
50
|
+
crop_to_foreground_bbox,
|
|
51
|
+
dialate_labels,
|
|
52
|
+
dilate_labels,
|
|
53
|
+
erode_labels,
|
|
54
|
+
fill_internal_gaps_edt,
|
|
55
|
+
find_non_self_connected_labels,
|
|
56
|
+
load_image_pipeline,
|
|
57
|
+
remove_non_self_connected_bits,
|
|
58
|
+
shuffle_labels,
|
|
59
|
+
skeletonize_dilate,
|
|
60
|
+
skeletonize_erode,
|
|
61
|
+
skeletonize_labels,
|
|
62
|
+
)
|
|
63
|
+
from .validation import unique_labels, validate_label_image
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"Junction",
|
|
67
|
+
"adjacency_from_labels",
|
|
68
|
+
"adjacency_pairs_from_labels",
|
|
69
|
+
"adjacency_with_contact_from_labels",
|
|
70
|
+
"adjacency_with_unique_from_labels",
|
|
71
|
+
"apply_color_lut_int",
|
|
72
|
+
"border_labels",
|
|
73
|
+
"centroids_from_labels",
|
|
74
|
+
"cluster_junctions_with_labels",
|
|
75
|
+
"color_planar_with_variety",
|
|
76
|
+
"crop_to_foreground_bbox",
|
|
77
|
+
"dialate_labels",
|
|
78
|
+
"dilate_labels",
|
|
79
|
+
"draw_graph",
|
|
80
|
+
"dsatur_color",
|
|
81
|
+
"erode_labels",
|
|
82
|
+
"fill_internal_gaps_edt",
|
|
83
|
+
"find_non_self_connected_labels",
|
|
84
|
+
"get_centroids",
|
|
85
|
+
"graph_from_labels",
|
|
86
|
+
"junction_pixels_with_labels",
|
|
87
|
+
"junctions_from_labels",
|
|
88
|
+
"label_is_border",
|
|
89
|
+
"label_pixel_counts",
|
|
90
|
+
"label_map",
|
|
91
|
+
"load_image_pipeline",
|
|
92
|
+
"load_img",
|
|
93
|
+
"load_label_graph",
|
|
94
|
+
"load_label_image",
|
|
95
|
+
"merge_close_junctions",
|
|
96
|
+
"ordered_contour_from_mask",
|
|
97
|
+
"ordered_contours_from_labels",
|
|
98
|
+
"plot_adjacency_graph",
|
|
99
|
+
"plot_contours",
|
|
100
|
+
"plot_junctions",
|
|
101
|
+
"plot_label_boundaries",
|
|
102
|
+
"plot_label_image",
|
|
103
|
+
"rebalance_K_colors",
|
|
104
|
+
"refine_to_K_colors",
|
|
105
|
+
"remove_non_self_connected_bits",
|
|
106
|
+
"save_label_graph",
|
|
107
|
+
"save_label_graph_from_labels",
|
|
108
|
+
"save_img",
|
|
109
|
+
"save_label_image",
|
|
110
|
+
"show_map_with_colors",
|
|
111
|
+
"shuffle_labels",
|
|
112
|
+
"skeletonize_dilate",
|
|
113
|
+
"skeletonize_erode",
|
|
114
|
+
"skeletonize_labels",
|
|
115
|
+
"unique_labels",
|
|
116
|
+
"validate_label_image",
|
|
117
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import ndimage as ndi
|
|
5
|
+
|
|
6
|
+
from .validation import unique_labels
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _clip_padded_slice(
|
|
10
|
+
slc: tuple[slice, slice],
|
|
11
|
+
shape: tuple[int, ...],
|
|
12
|
+
padding: int,
|
|
13
|
+
) -> tuple[slice, slice]:
|
|
14
|
+
"""Pad a 2-D slice tuple, clipping the result to image bounds."""
|
|
15
|
+
return (
|
|
16
|
+
slice(max(0, slc[0].start - padding), min(shape[0], slc[0].stop + padding)),
|
|
17
|
+
slice(max(0, slc[1].start - padding), min(shape[1], slc[1].stop + padding)),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _slice_from_coords(coords: np.ndarray) -> tuple[slice, slice]:
|
|
22
|
+
"""Build the tight 2-D bounding-box slice around coordinate rows."""
|
|
23
|
+
mins = coords.min(axis=0)
|
|
24
|
+
maxs = coords.max(axis=0) + 1
|
|
25
|
+
return (slice(int(mins[0]), int(maxs[0])), slice(int(mins[1]), int(maxs[1])))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compact_problematic_labels(
|
|
29
|
+
labels: np.ndarray,
|
|
30
|
+
problematic: list[int],
|
|
31
|
+
) -> tuple[np.ndarray, list[int]]:
|
|
32
|
+
"""
|
|
33
|
+
Remap sparse/problematic labels to dense positive ids in one image pass.
|
|
34
|
+
|
|
35
|
+
``ndi.find_objects`` is fast for dense positive labels, but cannot directly
|
|
36
|
+
handle negative labels or very large sparse labels without creating a huge
|
|
37
|
+
list. This helper creates a compact temporary image whose values are
|
|
38
|
+
``1..M`` only where the original image contains one of the problematic
|
|
39
|
+
labels. The output order is sorted and mapped back by the caller.
|
|
40
|
+
"""
|
|
41
|
+
sorted_labels = sorted(problematic)
|
|
42
|
+
lookup = np.asarray(sorted_labels, dtype=labels.dtype)
|
|
43
|
+
compact = np.zeros(labels.shape, dtype=np.int32)
|
|
44
|
+
|
|
45
|
+
indices = np.searchsorted(lookup, labels)
|
|
46
|
+
in_range = indices < lookup.size
|
|
47
|
+
matches = np.zeros(labels.shape, dtype=bool)
|
|
48
|
+
matches[in_range] = lookup[indices[in_range]] == labels[in_range]
|
|
49
|
+
compact[matches] = indices[matches] + 1
|
|
50
|
+
return compact, sorted_labels
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def label_slices(
|
|
54
|
+
labels,
|
|
55
|
+
*,
|
|
56
|
+
background=0,
|
|
57
|
+
include_background: bool = False,
|
|
58
|
+
padding: int = 0,
|
|
59
|
+
max_direct_label: int = 100_000,
|
|
60
|
+
max_manual_labels: int = 100,
|
|
61
|
+
) -> dict[int, tuple[slice, slice]]:
|
|
62
|
+
"""
|
|
63
|
+
Return global bounding-box slices for labels in a 2-D label image.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
labels : np.ndarray
|
|
68
|
+
2-D integer label image.
|
|
69
|
+
background : int, optional
|
|
70
|
+
Background label value. Default is ``0``.
|
|
71
|
+
include_background : bool, optional
|
|
72
|
+
If ``False`` (default), the background label is excluded. If ``True``,
|
|
73
|
+
the background is included when present in the image.
|
|
74
|
+
padding : int, optional
|
|
75
|
+
Number of pixels to add around each bounding box. Padding is clipped to
|
|
76
|
+
the image boundary.
|
|
77
|
+
max_direct_label : int, optional
|
|
78
|
+
Largest positive label handled by the direct ``ndi.find_objects`` fast
|
|
79
|
+
path. Larger labels are handled by the sparse-label fallback.
|
|
80
|
+
max_manual_labels : int, optional
|
|
81
|
+
Maximum number of sparse/problematic labels to handle with bounded
|
|
82
|
+
per-label scans. When there are more, labels are compacted in one pass
|
|
83
|
+
and processed with ``ndi.find_objects``.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
dict[int, tuple[slice, slice]]
|
|
88
|
+
Mapping from original label value to global ``(row_slice, col_slice)``.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
Ordinary positive labels use ``scipy.ndimage.find_objects`` directly. This
|
|
93
|
+
preserves the windowed design of the original tools, where expensive
|
|
94
|
+
per-label work happens only inside local crops. Negative labels, zero-valued
|
|
95
|
+
foreground labels, and very large sparse labels are still supported without
|
|
96
|
+
requiring a label-indexed list of length ``max_label + 1``.
|
|
97
|
+
"""
|
|
98
|
+
labels = np.asarray(labels)
|
|
99
|
+
padding = int(padding)
|
|
100
|
+
max_direct_label = int(max_direct_label)
|
|
101
|
+
max_manual_labels = int(max_manual_labels)
|
|
102
|
+
if labels.ndim != 2:
|
|
103
|
+
raise ValueError("labels must be a 2-D array")
|
|
104
|
+
if padding < 0:
|
|
105
|
+
raise ValueError("padding must be non-negative")
|
|
106
|
+
if max_direct_label < 1:
|
|
107
|
+
raise ValueError("max_direct_label must be positive")
|
|
108
|
+
if max_manual_labels < 0:
|
|
109
|
+
raise ValueError("max_manual_labels must be non-negative")
|
|
110
|
+
|
|
111
|
+
values = [
|
|
112
|
+
int(label)
|
|
113
|
+
for label in unique_labels(
|
|
114
|
+
labels,
|
|
115
|
+
background=background,
|
|
116
|
+
include_background=include_background,
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
if not values:
|
|
120
|
+
return {}
|
|
121
|
+
|
|
122
|
+
shape = labels.shape
|
|
123
|
+
slices: dict[int, tuple[slice, slice]] = {}
|
|
124
|
+
|
|
125
|
+
direct = [label for label in values if 0 < label <= max_direct_label]
|
|
126
|
+
problematic = [label for label in values if label <= 0 or label > max_direct_label]
|
|
127
|
+
|
|
128
|
+
if direct:
|
|
129
|
+
objects = ndi.find_objects(labels, max_label=max_direct_label)
|
|
130
|
+
for label in direct:
|
|
131
|
+
slc = objects[label - 1]
|
|
132
|
+
if slc is not None:
|
|
133
|
+
slices[label] = _clip_padded_slice(slc, shape, padding)
|
|
134
|
+
|
|
135
|
+
if len(problematic) <= max_manual_labels:
|
|
136
|
+
for label in problematic:
|
|
137
|
+
coords = np.argwhere(labels == label)
|
|
138
|
+
if coords.size:
|
|
139
|
+
slices[label] = _clip_padded_slice(_slice_from_coords(coords), shape, padding)
|
|
140
|
+
elif problematic:
|
|
141
|
+
compact, original_labels = _compact_problematic_labels(labels, problematic)
|
|
142
|
+
objects = ndi.find_objects(compact, max_label=len(original_labels))
|
|
143
|
+
for compact_id, label in enumerate(original_labels, start=1):
|
|
144
|
+
slc = objects[compact_id - 1]
|
|
145
|
+
if slc is not None:
|
|
146
|
+
slices[label] = _clip_padded_slice(slc, shape, padding)
|
|
147
|
+
|
|
148
|
+
return {label: slices[label] for label in values if label in slices}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ._optional import optional_import
|
|
8
|
+
from .typing import Cont, Neig
|
|
9
|
+
|
|
10
|
+
LabelGraphData = namedtuple(
|
|
11
|
+
"LabelGraphData",
|
|
12
|
+
["neighbors", "contacts", "centroids", "pixel_counts", "metadata"],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _json_default(value):
|
|
17
|
+
if isinstance(value, np.integer):
|
|
18
|
+
return int(value)
|
|
19
|
+
if isinstance(value, np.floating):
|
|
20
|
+
return float(value)
|
|
21
|
+
if isinstance(value, np.ndarray):
|
|
22
|
+
return value.tolist()
|
|
23
|
+
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _infer_graph_format(path, format: str) -> str:
|
|
27
|
+
fmt = format.lower()
|
|
28
|
+
if fmt != "auto":
|
|
29
|
+
if fmt not in {"npz", "json", "graphml", "gexf"}:
|
|
30
|
+
raise ValueError("format must be one of 'auto', 'npz', 'json', 'graphml', or 'gexf'")
|
|
31
|
+
return fmt
|
|
32
|
+
suffix = Path(path).suffix.lower()
|
|
33
|
+
if suffix == ".npz":
|
|
34
|
+
return "npz"
|
|
35
|
+
if suffix == ".json":
|
|
36
|
+
return "json"
|
|
37
|
+
if suffix == ".graphml":
|
|
38
|
+
return "graphml"
|
|
39
|
+
if suffix == ".gexf":
|
|
40
|
+
return "gexf"
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"could not infer graph format from suffix; use .npz, .json, .graphml, or .gexf"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _graph_arrays(
|
|
47
|
+
neighbors: Neig,
|
|
48
|
+
*,
|
|
49
|
+
contacts: Cont | None = None,
|
|
50
|
+
centroids: dict[int, np.ndarray] | None = None,
|
|
51
|
+
pixel_counts: dict[int, int] | None = None,
|
|
52
|
+
):
|
|
53
|
+
node_set = {int(label) for label in neighbors}
|
|
54
|
+
for nbrs in neighbors.values():
|
|
55
|
+
node_set.update(int(nbr) for nbr in np.asarray(nbrs).ravel())
|
|
56
|
+
if centroids is not None:
|
|
57
|
+
node_set.update(int(label) for label in centroids)
|
|
58
|
+
if pixel_counts is not None:
|
|
59
|
+
node_set.update(int(label) for label in pixel_counts)
|
|
60
|
+
nodes = np.asarray(sorted(node_set), dtype=np.int64)
|
|
61
|
+
|
|
62
|
+
edge_contacts: dict[tuple[int, int], float] = {}
|
|
63
|
+
edge_set: set[tuple[int, int]] = set()
|
|
64
|
+
for label, nbrs in neighbors.items():
|
|
65
|
+
label = int(label)
|
|
66
|
+
weights = contacts.get(label) if contacts is not None else None
|
|
67
|
+
if weights is None:
|
|
68
|
+
weights = [None] * len(nbrs)
|
|
69
|
+
for nbr, weight in zip(np.asarray(nbrs).ravel(), weights, strict=True):
|
|
70
|
+
edge = tuple(sorted((label, int(nbr))))
|
|
71
|
+
if edge[0] == edge[1]:
|
|
72
|
+
continue
|
|
73
|
+
edge_set.add(edge) # type: ignore (edge is 2-tuple of int, should be fine)
|
|
74
|
+
if contacts is not None and weight is not None:
|
|
75
|
+
edge_contacts.setdefault(edge, float(weight)) # type: ignore (edge is 2-tuple of int, should be fine)
|
|
76
|
+
|
|
77
|
+
edges = np.asarray(sorted(edge_set), dtype=np.int64).reshape(-1, 2)
|
|
78
|
+
contact_values = (
|
|
79
|
+
np.asarray([edge_contacts[tuple(edge)] for edge in edges], dtype=float)
|
|
80
|
+
if contacts is not None
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
83
|
+
centroid_values = (
|
|
84
|
+
np.asarray(
|
|
85
|
+
[np.asarray(centroids.get(int(node), [np.nan, np.nan]), dtype=float) for node in nodes],
|
|
86
|
+
dtype=float,
|
|
87
|
+
)
|
|
88
|
+
if centroids is not None
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
pixel_count_values = (
|
|
92
|
+
np.asarray([int(pixel_counts.get(int(node), 0)) for node in nodes], dtype=np.int64)
|
|
93
|
+
if pixel_counts is not None
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
return nodes, edges, contact_values, centroid_values, pixel_count_values
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _graph_from_arrays(
|
|
100
|
+
nodes,
|
|
101
|
+
edges,
|
|
102
|
+
*,
|
|
103
|
+
contacts=None,
|
|
104
|
+
centroids=None,
|
|
105
|
+
pixel_counts=None,
|
|
106
|
+
):
|
|
107
|
+
neighbors_lists: dict[int, list[int]] = {int(node): [] for node in np.asarray(nodes).ravel()}
|
|
108
|
+
contact_lists: dict[int, list[float]] | None = (
|
|
109
|
+
{int(node): [] for node in np.asarray(nodes).ravel()} if contacts is not None else None
|
|
110
|
+
)
|
|
111
|
+
for idx, edge in enumerate(np.asarray(edges, dtype=np.int64).reshape(-1, 2)):
|
|
112
|
+
a, b = int(edge[0]), int(edge[1])
|
|
113
|
+
neighbors_lists.setdefault(a, []).append(b)
|
|
114
|
+
neighbors_lists.setdefault(b, []).append(a)
|
|
115
|
+
if contact_lists is not None:
|
|
116
|
+
weight = float(np.asarray(contacts, dtype=float)[idx])
|
|
117
|
+
contact_lists.setdefault(a, []).append(weight)
|
|
118
|
+
contact_lists.setdefault(b, []).append(weight)
|
|
119
|
+
|
|
120
|
+
neighbors = {
|
|
121
|
+
label: np.asarray(values, dtype=np.int64)
|
|
122
|
+
for label, values in neighbors_lists.items()
|
|
123
|
+
}
|
|
124
|
+
contact_map = (
|
|
125
|
+
{label: np.asarray(values, dtype=float) for label, values in contact_lists.items()}
|
|
126
|
+
if contact_lists is not None
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
centroid_map = (
|
|
130
|
+
{
|
|
131
|
+
int(node): np.asarray(value, dtype=float)
|
|
132
|
+
for node, value in zip(nodes, centroids, strict=True)
|
|
133
|
+
}
|
|
134
|
+
if centroids is not None
|
|
135
|
+
else None
|
|
136
|
+
)
|
|
137
|
+
pixel_count_map = (
|
|
138
|
+
{int(node): int(value) for node, value in zip(nodes, pixel_counts, strict=True)}
|
|
139
|
+
if pixel_counts is not None
|
|
140
|
+
else None
|
|
141
|
+
)
|
|
142
|
+
return neighbors, contact_map, centroid_map, pixel_count_map
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _json_dict_from_graph_data(neighbors, contacts, centroids, pixel_counts, metadata):
|
|
146
|
+
nodes, edges, contact_values, _, _ = _graph_arrays(
|
|
147
|
+
neighbors,
|
|
148
|
+
contacts=contacts,
|
|
149
|
+
centroids=centroids,
|
|
150
|
+
pixel_counts=pixel_counts,
|
|
151
|
+
)
|
|
152
|
+
node_items = []
|
|
153
|
+
for node in nodes:
|
|
154
|
+
item = {"id": int(node)}
|
|
155
|
+
if centroids is not None and int(node) in centroids:
|
|
156
|
+
item["centroid"] = np.asarray(centroids[int(node)], dtype=float).tolist()
|
|
157
|
+
if pixel_counts is not None and int(node) in pixel_counts:
|
|
158
|
+
item["pixel_count"] = int(pixel_counts[int(node)])
|
|
159
|
+
node_items.append(item)
|
|
160
|
+
edge_items = []
|
|
161
|
+
for idx, edge in enumerate(edges):
|
|
162
|
+
item = {"source": int(edge[0]), "target": int(edge[1])}
|
|
163
|
+
if contact_values is not None:
|
|
164
|
+
contact = float(contact_values[idx])
|
|
165
|
+
item["contact"] = contact # type: ignore (contact is types ad float for flexibility, should be fine)
|
|
166
|
+
item["weight"] = contact # type: ignore (contact is types ad float for flexibility, should be fine)
|
|
167
|
+
edge_items.append(item)
|
|
168
|
+
return {"nodes": node_items, "edges": edge_items, "metadata": dict(metadata)}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _graph_data_from_json_dict(data):
|
|
172
|
+
nodes = np.asarray([int(node["id"]) for node in data.get("nodes", [])], dtype=np.int64)
|
|
173
|
+
edges = np.asarray(
|
|
174
|
+
[[int(edge["source"]), int(edge["target"])] for edge in data.get("edges", [])],
|
|
175
|
+
dtype=np.int64,
|
|
176
|
+
).reshape(-1, 2)
|
|
177
|
+
has_contacts = any("contact" in edge for edge in data.get("edges", []))
|
|
178
|
+
contacts = (
|
|
179
|
+
np.asarray([float(edge.get("contact", edge.get("weight", 1.0))) for edge in data["edges"]])
|
|
180
|
+
if has_contacts
|
|
181
|
+
else None
|
|
182
|
+
)
|
|
183
|
+
has_centroids = any("centroid" in node for node in data.get("nodes", []))
|
|
184
|
+
centroids = (
|
|
185
|
+
np.asarray([node.get("centroid", [np.nan, np.nan]) for node in data["nodes"]], dtype=float)
|
|
186
|
+
if has_centroids
|
|
187
|
+
else None
|
|
188
|
+
)
|
|
189
|
+
has_pixel_counts = any("pixel_count" in node for node in data.get("nodes", []))
|
|
190
|
+
pixel_counts = (
|
|
191
|
+
np.asarray([int(node.get("pixel_count", 0)) for node in data["nodes"]], dtype=np.int64)
|
|
192
|
+
if has_pixel_counts
|
|
193
|
+
else None
|
|
194
|
+
)
|
|
195
|
+
neighbors, contact_map, centroid_map, pixel_count_map = _graph_from_arrays(
|
|
196
|
+
nodes,
|
|
197
|
+
edges,
|
|
198
|
+
contacts=contacts,
|
|
199
|
+
centroids=centroids,
|
|
200
|
+
pixel_counts=pixel_counts,
|
|
201
|
+
)
|
|
202
|
+
return LabelGraphData(
|
|
203
|
+
neighbors,
|
|
204
|
+
contact_map,
|
|
205
|
+
centroid_map,
|
|
206
|
+
pixel_count_map,
|
|
207
|
+
dict(data.get("metadata", {})),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _graph_to_networkx(neighbors, contacts=None, centroids=None, pixel_counts=None, metadata=None):
|
|
212
|
+
nx = optional_import(
|
|
213
|
+
"networkx",
|
|
214
|
+
extra="graph-standard",
|
|
215
|
+
feature="GraphML/GEXF graph I/O",
|
|
216
|
+
package_name="networkx",
|
|
217
|
+
)
|
|
218
|
+
graph = nx.Graph()
|
|
219
|
+
nodes, edges, contact_values, _, _ = _graph_arrays(
|
|
220
|
+
neighbors,
|
|
221
|
+
contacts=contacts,
|
|
222
|
+
centroids=centroids,
|
|
223
|
+
pixel_counts=pixel_counts,
|
|
224
|
+
)
|
|
225
|
+
for node in nodes:
|
|
226
|
+
attrs = {}
|
|
227
|
+
if centroids is not None and int(node) in centroids:
|
|
228
|
+
cy, cx = np.asarray(centroids[int(node)], dtype=float)
|
|
229
|
+
attrs.update({"centroid_y": float(cy), "centroid_x": float(cx)})
|
|
230
|
+
if pixel_counts is not None and int(node) in pixel_counts:
|
|
231
|
+
attrs["pixel_count"] = int(pixel_counts[int(node)])
|
|
232
|
+
graph.add_node(str(int(node)), **attrs)
|
|
233
|
+
for idx, edge in enumerate(edges):
|
|
234
|
+
attrs = {}
|
|
235
|
+
if contact_values is not None:
|
|
236
|
+
contact = float(contact_values[idx])
|
|
237
|
+
attrs.update({"contact": contact, "weight": contact})
|
|
238
|
+
graph.add_edge(str(int(edge[0])), str(int(edge[1])), **attrs)
|
|
239
|
+
if metadata:
|
|
240
|
+
graph.graph["metadata"] = json.dumps(metadata, default=_json_default)
|
|
241
|
+
return graph
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _graph_from_networkx(graph):
|
|
245
|
+
nodes = np.asarray([int(node) for node in graph.nodes], dtype=np.int64)
|
|
246
|
+
edges = np.asarray([[int(a), int(b)] for a, b in graph.edges], dtype=np.int64).reshape(-1, 2)
|
|
247
|
+
has_contacts = any(
|
|
248
|
+
"contact" in data or "weight" in data
|
|
249
|
+
for _, _, data in graph.edges(data=True)
|
|
250
|
+
)
|
|
251
|
+
contacts = (
|
|
252
|
+
np.asarray(
|
|
253
|
+
[
|
|
254
|
+
float(data.get("contact", data.get("weight", 1.0)))
|
|
255
|
+
for _, _, data in graph.edges(data=True)
|
|
256
|
+
],
|
|
257
|
+
dtype=float,
|
|
258
|
+
)
|
|
259
|
+
if has_contacts
|
|
260
|
+
else None
|
|
261
|
+
)
|
|
262
|
+
has_centroids = any(
|
|
263
|
+
"centroid_y" in data and "centroid_x" in data
|
|
264
|
+
for _, data in graph.nodes(data=True)
|
|
265
|
+
)
|
|
266
|
+
centroids = (
|
|
267
|
+
np.asarray(
|
|
268
|
+
[
|
|
269
|
+
[float(data.get("centroid_y", np.nan)), float(data.get("centroid_x", np.nan))]
|
|
270
|
+
for _, data in graph.nodes(data=True)
|
|
271
|
+
],
|
|
272
|
+
dtype=float,
|
|
273
|
+
)
|
|
274
|
+
if has_centroids
|
|
275
|
+
else None
|
|
276
|
+
)
|
|
277
|
+
has_pixel_counts = any("pixel_count" in data for _, data in graph.nodes(data=True))
|
|
278
|
+
pixel_counts = (
|
|
279
|
+
np.asarray(
|
|
280
|
+
[int(data.get("pixel_count", 0)) for _, data in graph.nodes(data=True)],
|
|
281
|
+
dtype=np.int64,
|
|
282
|
+
)
|
|
283
|
+
if has_pixel_counts
|
|
284
|
+
else None
|
|
285
|
+
)
|
|
286
|
+
metadata_raw = graph.graph.get("metadata", "{}")
|
|
287
|
+
try:
|
|
288
|
+
metadata = json.loads(metadata_raw)
|
|
289
|
+
except TypeError:
|
|
290
|
+
metadata = {}
|
|
291
|
+
neighbors, contact_map, centroid_map, pixel_count_map = _graph_from_arrays(
|
|
292
|
+
nodes,
|
|
293
|
+
edges,
|
|
294
|
+
contacts=contacts,
|
|
295
|
+
centroids=centroids,
|
|
296
|
+
pixel_counts=pixel_counts,
|
|
297
|
+
)
|
|
298
|
+
return LabelGraphData(neighbors, contact_map, centroid_map, pixel_count_map, metadata)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def optional_import(
|
|
8
|
+
module_name: str,
|
|
9
|
+
*,
|
|
10
|
+
extra: str,
|
|
11
|
+
feature: str,
|
|
12
|
+
package_name: str | None = None,
|
|
13
|
+
) -> Any:
|
|
14
|
+
"""Import an optional dependency or raise a clear installation hint."""
|
|
15
|
+
display_name = package_name or module_name
|
|
16
|
+
try:
|
|
17
|
+
return import_module(module_name)
|
|
18
|
+
except ImportError as exc:
|
|
19
|
+
install_hint = f"`pip install labelimage-tools[{extra}]`"
|
|
20
|
+
if extra != "all":
|
|
21
|
+
install_hint += " or `pip install labelimage-tools[all]`"
|
|
22
|
+
raise ImportError(
|
|
23
|
+
f"{feature} requires the optional dependency `{display_name}`. "
|
|
24
|
+
f"Install it with {install_hint} "
|
|
25
|
+
f"or install `{display_name}` directly."
|
|
26
|
+
) from exc
|