ramify 2.0.0__tar.gz → 2.1.0__tar.gz
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.
- {ramify-2.0.0 → ramify-2.1.0}/PKG-INFO +1 -1
- ramify-2.1.0/assets/abstract.png +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/assets/level.png +0 -0
- ramify-2.1.0/assets/open_boundary.png +0 -0
- ramify-2.1.0/assets/quickstart.png +0 -0
- ramify-2.1.0/assets/tips_options.png +0 -0
- ramify-2.1.0/assets/widths_regions.png +0 -0
- ramify-2.1.0/assets/widths_stations.png +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/pyproject.toml +1 -1
- {ramify-2.0.0 → ramify-2.1.0}/ramify/_skeleton.py +36 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/centerline.py +10 -1
- {ramify-2.0.0 → ramify-2.1.0}/ramify/partition.py +21 -0
- {ramify-2.0.0 → ramify-2.1.0}/scripts/gen_images.py +2 -2
- ramify-2.1.0/tests/conftest.py +73 -0
- {ramify-2.0.0 → ramify-2.1.0}/tests/test_centerline.py +58 -0
- {ramify-2.0.0 → ramify-2.1.0}/tests/test_partition.py +62 -0
- {ramify-2.0.0 → ramify-2.1.0}/uv.lock +1 -1
- ramify-2.0.0/assets/abstract.png +0 -0
- ramify-2.0.0/assets/open_boundary.png +0 -0
- ramify-2.0.0/assets/quickstart.png +0 -0
- ramify-2.0.0/assets/tips_options.png +0 -0
- ramify-2.0.0/assets/widths_regions.png +0 -0
- ramify-2.0.0/assets/widths_stations.png +0 -0
- ramify-2.0.0/tests/conftest.py +0 -27
- {ramify-2.0.0 → ramify-2.1.0}/.github/workflows/publish.yml +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/.gitignore +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/LICENSE +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/README.md +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/assets/partition_methods.png +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/assets/single_shape.png +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/branch/data/__init__.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/branch/data/example.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/branch/data/root.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/branch/data/tips.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/__init__.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/_io.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/data/__init__.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/data/mask.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/data/root.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/data/tips.tif +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/regions.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/ramify/width.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/scripts/progress_example.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/tests/test_io.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/tests/test_regions.py +0 -0
- {ramify-2.0.0 → ramify-2.1.0}/tests/test_width.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ramify
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Segment and measure branching shapes: centerlines, hierarchical space partitioning, and width
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: centerline,geomorphology,medial-axis,morphometry,raster,river,segmentation,skeleton,valley,width
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,6 +5,7 @@ partition network construction.
|
|
|
5
5
|
import heapq
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
|
+
from skimage.draw import line as draw_line
|
|
8
9
|
from skimage.morphology import skeletonize
|
|
9
10
|
from skimage.graph import MCP_Geometric
|
|
10
11
|
|
|
@@ -183,3 +184,38 @@ def prune_short_leaves(nodes, threshold_fn):
|
|
|
183
184
|
if not removed_any:
|
|
184
185
|
break
|
|
185
186
|
return kept
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def disk_cut(path_nodes, half_width, mask_bool, fill=False):
|
|
190
|
+
"""Replace the start of `path_nodes` with one straight chord.
|
|
191
|
+
|
|
192
|
+
Near an end, the skeleton forks toward the shape's corners (or runs past a
|
|
193
|
+
point placed on the side of a pointed end), so a path snapped to it swerves
|
|
194
|
+
before reaching the trunk. Every path node whose inscribed disk (radius =
|
|
195
|
+
local half-width, pixel units) reaches the anchor `path_nodes[0]` lies in
|
|
196
|
+
the same end blob; cut to the farthest such node and join it to the anchor
|
|
197
|
+
directly. The chord stays inside that disk, hence inside the shape -- up to
|
|
198
|
+
the slack, so each candidate chord is also rasterized and checked against
|
|
199
|
+
the mask, falling back to nearer nodes and finally the uncut path.
|
|
200
|
+
|
|
201
|
+
`fill=False` joins anchor and node as two consecutive vertices (enough for
|
|
202
|
+
a LineString); `fill=True` puts the chord's rasterized pixels in between,
|
|
203
|
+
keeping the path 8-connected for callers that work on pixel lists.
|
|
204
|
+
"""
|
|
205
|
+
anchor = np.array(path_nodes[0], dtype=float)
|
|
206
|
+
pts = np.array(path_nodes, dtype=float)
|
|
207
|
+
d = np.hypot(*(pts - anchor).T)
|
|
208
|
+
r = half_width[pts[:, 0].astype(int), pts[:, 1].astype(int)]
|
|
209
|
+
slack = np.maximum(1.0, 0.1 * r)
|
|
210
|
+
passing = np.nonzero(d <= r + slack)[0]
|
|
211
|
+
|
|
212
|
+
for i in passing[::-1]:
|
|
213
|
+
if i <= 1:
|
|
214
|
+
break # already adjacent to the anchor: nothing to cut
|
|
215
|
+
rr, cc = draw_line(*path_nodes[0], *path_nodes[i])
|
|
216
|
+
if mask_bool[rr, cc].all():
|
|
217
|
+
if fill:
|
|
218
|
+
chord = list(zip(rr.tolist(), cc.tolist()))
|
|
219
|
+
return chord[:-1] + list(path_nodes[i:])
|
|
220
|
+
return [path_nodes[0]] + list(path_nodes[i:])
|
|
221
|
+
return list(path_nodes)
|
|
@@ -13,6 +13,7 @@ from ._skeleton import (
|
|
|
13
13
|
endpoints,
|
|
14
14
|
tree_path,
|
|
15
15
|
prune_short_leaves,
|
|
16
|
+
disk_cut,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
@@ -25,7 +26,10 @@ def centerline(mask, root=None, tip=None, pixel_size=None) -> LineString:
|
|
|
25
26
|
- `tip` without `root`: raises `ValueError`.
|
|
26
27
|
|
|
27
28
|
`root` / `tip` are `(row, col)` pixel indices. Orientation: starts at
|
|
28
|
-
`root` when given; otherwise deterministic but unspecified.
|
|
29
|
+
`root` when given; otherwise deterministic but unspecified. Near a given
|
|
30
|
+
`root` / `tip`, the line leaves the skeleton and runs straight to the
|
|
31
|
+
point from the farthest skeleton pixel whose inscribed disk reaches it,
|
|
32
|
+
so it doesn't swerve into the forks a skeleton makes at a shape's end.
|
|
29
33
|
|
|
30
34
|
This is the *longest* path through the shape, deliberately not the
|
|
31
35
|
heaviest (widest) one that `partition_priority`/`partition_nearest` would
|
|
@@ -109,6 +113,11 @@ def centerline(mask, root=None, tip=None, pixel_size=None) -> LineString:
|
|
|
109
113
|
|
|
110
114
|
path_nodes = list(reversed(tree_path(parent1, b, stop=a)))
|
|
111
115
|
|
|
116
|
+
if root is not None:
|
|
117
|
+
path_nodes = disk_cut(path_nodes, half_width, mask_bool)
|
|
118
|
+
if tip is not None:
|
|
119
|
+
path_nodes = disk_cut(path_nodes[::-1], half_width, mask_bool)[::-1]
|
|
120
|
+
|
|
112
121
|
rows = np.array([n[0] for n in path_nodes])
|
|
113
122
|
cols = np.array([n[1] for n in path_nodes])
|
|
114
123
|
xs, ys = grid.to_xy(rows, cols)
|
|
@@ -28,6 +28,7 @@ from ._skeleton import (
|
|
|
28
28
|
prune_short_leaves,
|
|
29
29
|
path_length,
|
|
30
30
|
path_weight,
|
|
31
|
+
disk_cut,
|
|
31
32
|
)
|
|
32
33
|
from .centerline import centerline
|
|
33
34
|
|
|
@@ -153,10 +154,30 @@ def _build_network(mask_bool, root, tips, min_length, path_by, open_boundary, pi
|
|
|
153
154
|
n = parent[n]
|
|
154
155
|
|
|
155
156
|
segments = break_into_segments(kept, parent, tip_nodes, root)
|
|
157
|
+
segments = _cut_end_segments(
|
|
158
|
+
segments, root, set(tip_points), half_width_pixels(mask_bool, None), mask_bool
|
|
159
|
+
)
|
|
156
160
|
edt = local_half_width(mask_bool, open_boundary, pixel_size)
|
|
157
161
|
return _annotate(segments, edt, pixel_size, path_by, root)
|
|
158
162
|
|
|
159
163
|
|
|
164
|
+
def _cut_end_segments(segments, root, anchors, half_width, mask_bool):
|
|
165
|
+
# Straighten the network where it meets the root and each given tip (see
|
|
166
|
+
# `disk_cut`). Each cut is confined to the one segment touching that
|
|
167
|
+
# point: a segment's other end is a junction, so the tree's topology and
|
|
168
|
+
# shared junction pixels are untouched. Auto-detected tips are skeleton
|
|
169
|
+
# endpoints already and have nothing to cut toward. Segments run
|
|
170
|
+
# upstream -> downstream, so the root is at seg[-1] and a tip at seg[0].
|
|
171
|
+
out = []
|
|
172
|
+
for seg in segments:
|
|
173
|
+
if seg[-1] == root:
|
|
174
|
+
seg = disk_cut(seg[::-1], half_width, mask_bool, fill=True)[::-1]
|
|
175
|
+
if seg[0] in anchors:
|
|
176
|
+
seg = disk_cut(seg, half_width, mask_bool, fill=True)
|
|
177
|
+
out.append(seg)
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
160
181
|
def _annotate(segments, edt, pixel_size, path_by, root):
|
|
161
182
|
n = len(segments)
|
|
162
183
|
start_of = {seg[0]: i for i, seg in enumerate(segments)}
|
|
@@ -265,13 +265,13 @@ save("quickstart.png", fig, tight=False)
|
|
|
265
265
|
|
|
266
266
|
with warnings.catch_warnings():
|
|
267
267
|
warnings.simplefilter("ignore")
|
|
268
|
-
_, net_ml, _ = ramify.partition_priority(mask, root, min_length=
|
|
268
|
+
_, net_ml, _ = ramify.partition_priority(mask, root, min_length=100.0)
|
|
269
269
|
_, net_all, _ = ramify.partition_priority(mask, root)
|
|
270
270
|
|
|
271
271
|
fig, axes = plt.subplots(1, 3, figsize=(16, 6))
|
|
272
272
|
for ax, net_i, title in [
|
|
273
273
|
(axes[0], net, f"tips=tips ({len(tips)} tips)"),
|
|
274
|
-
(axes[1], net_ml, f"min_length=
|
|
274
|
+
(axes[1], net_ml, f"min_length=100 ({net_ml['path_id'].nunique()} paths)"),
|
|
275
275
|
(axes[2], net_all, f"neither ({net_all['path_id'].nunique()} paths)"),
|
|
276
276
|
]:
|
|
277
277
|
ax.imshow(mask_arr, cmap="gray_r", alpha=0.25)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def straight_bar():
|
|
7
|
+
"""A 1-pixel-wide horizontal bar, root at one end, tip at the other."""
|
|
8
|
+
mask = np.zeros((5, 20), dtype=np.uint8)
|
|
9
|
+
mask[2, 1:19] = 1
|
|
10
|
+
return mask, (2, 1), (2, 18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def stubbed_bar():
|
|
15
|
+
"""A long horizontal bar with a short stub off the middle -- the stub
|
|
16
|
+
should be trimmed away when choosing endpoints without an explicit tip."""
|
|
17
|
+
mask = np.zeros((10, 20), dtype=np.uint8)
|
|
18
|
+
mask[5, 1:19] = 1
|
|
19
|
+
mask[3:5, 10] = 1
|
|
20
|
+
return mask
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def toy_dataset():
|
|
25
|
+
from ramify.data import load
|
|
26
|
+
|
|
27
|
+
return load()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def flared_bar():
|
|
32
|
+
"""A thick horizontal bar whose left end flares into a fishtail, so the
|
|
33
|
+
skeleton forks into a Y there. Root sits on the flared end's edge."""
|
|
34
|
+
from skimage.draw import polygon
|
|
35
|
+
|
|
36
|
+
mask = np.zeros((40, 70), dtype=np.uint8)
|
|
37
|
+
rr, cc = polygon([4, 12, 12, 28, 28, 36], [4, 24, 66, 66, 24, 4])
|
|
38
|
+
mask[rr, cc] = 1
|
|
39
|
+
return mask, (20, 4), (20, 65)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def end_deviation():
|
|
44
|
+
"""How far the first `length` of a line strays from the straight chord
|
|
45
|
+
over that stretch: ~0 for a clean end, several pixels for a swerve into a
|
|
46
|
+
skeleton fork arm or a sideways snap."""
|
|
47
|
+
from shapely.geometry import LineString, Point
|
|
48
|
+
|
|
49
|
+
def fn(line, length=15.0):
|
|
50
|
+
chord = LineString([line.coords[0], line.interpolate(length).coords[0]])
|
|
51
|
+
return max(
|
|
52
|
+
chord.distance(Point(p))
|
|
53
|
+
for p in line.coords
|
|
54
|
+
if line.project(Point(p)) <= length
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return fn
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def inside_mask():
|
|
62
|
+
"""Whether every pixel a (pixel_size=1, numpy-grid) line crosses is in `mask`."""
|
|
63
|
+
from skimage.draw import line as draw_line
|
|
64
|
+
|
|
65
|
+
def fn(line, mask):
|
|
66
|
+
pix = [(int(y - 0.5), int(x - 0.5)) for x, y in line.coords]
|
|
67
|
+
for (r0, c0), (r1, c1) in zip(pix[:-1], pix[1:]):
|
|
68
|
+
rr, cc = draw_line(r0, c0, r1, c1)
|
|
69
|
+
if not mask[rr, cc].all():
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
return fn
|
|
@@ -106,3 +106,61 @@ def test_output_is_plain_ndarray_free(toy_dataset):
|
|
|
106
106
|
mask_np = (np.asarray(mask.values) == 1).astype(np.uint8)
|
|
107
107
|
assert isinstance(ramify.centerline(mask, root=root), LineString)
|
|
108
108
|
assert isinstance(ramify.centerline(mask_np, root=root), LineString)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_root_end_skips_skeleton_fork(flared_bar, end_deviation, inside_mask):
|
|
112
|
+
mask, root, _ = flared_bar
|
|
113
|
+
line = ramify.centerline(mask, root=root)
|
|
114
|
+
assert line.coords[0] == (root[1] + 0.5, root[0] + 0.5)
|
|
115
|
+
assert end_deviation(line) < 1.0 # uncut path swerves ~5 px into a fork arm
|
|
116
|
+
assert inside_mask(line, mask)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_tip_end_skips_skeleton_fork(flared_bar, end_deviation, inside_mask):
|
|
120
|
+
mask, tip, root = flared_bar # flared end as the tip this time
|
|
121
|
+
line = ramify.centerline(mask, root=root, tip=tip)
|
|
122
|
+
assert line.coords[-1] == (tip[1] + 0.5, tip[0] + 0.5)
|
|
123
|
+
assert end_deviation(LineString(line.coords[::-1])) < 1.0
|
|
124
|
+
assert inside_mask(line, mask)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_root_end_has_no_sideways_snap(toy_dataset, end_deviation, inside_mask):
|
|
128
|
+
# the bundled root sits on the side of a pointed end; the skeleton runs
|
|
129
|
+
# past it toward the point, so the uncut path makes a right-angle turn
|
|
130
|
+
mask, root, _ = toy_dataset
|
|
131
|
+
mask_np = (np.asarray(mask.values) == 1).astype(np.uint8)
|
|
132
|
+
line = ramify.centerline(mask_np, root=root)
|
|
133
|
+
assert end_deviation(line) < 1.0 # ~4.8 px before the cut
|
|
134
|
+
assert inside_mask(line, mask_np)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_disk_cut_rejects_chord_leaving_mask():
|
|
138
|
+
from ramify._skeleton import disk_cut
|
|
139
|
+
|
|
140
|
+
# a path that detours around a background pixel; every node's disk is
|
|
141
|
+
# large enough to reach the anchor, but the straight chord to the far
|
|
142
|
+
# nodes crosses the hole, so the cut must fall back to a nearer node
|
|
143
|
+
mask = np.ones((5, 7), dtype=bool)
|
|
144
|
+
mask[2, 3] = False
|
|
145
|
+
half_width = np.full(mask.shape, 10.0)
|
|
146
|
+
path = [(2, 1), (2, 2), (1, 3), (2, 4), (2, 5)]
|
|
147
|
+
assert disk_cut(path, half_width, mask) == [(2, 1), (1, 3), (2, 4), (2, 5)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_disk_cut_leaves_path_when_no_disk_reaches_anchor():
|
|
151
|
+
from ramify._skeleton import disk_cut
|
|
152
|
+
|
|
153
|
+
mask = np.ones((5, 7), dtype=bool)
|
|
154
|
+
half_width = np.zeros(mask.shape)
|
|
155
|
+
path = [(2, 0), (2, 1), (2, 2), (1, 3), (2, 4)]
|
|
156
|
+
assert disk_cut(path, half_width, mask) == path
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_disk_cut_fill_keeps_path_pixel_connected():
|
|
160
|
+
from ramify._skeleton import disk_cut
|
|
161
|
+
|
|
162
|
+
mask = np.ones((7, 9), dtype=bool)
|
|
163
|
+
half_width = np.full(mask.shape, 10.0)
|
|
164
|
+
path = [(3, 0), (4, 1), (4, 2), (3, 3), (3, 4), (3, 5)]
|
|
165
|
+
assert disk_cut(path, half_width, mask) == [(3, 0), (3, 5)]
|
|
166
|
+
assert disk_cut(path, half_width, mask, fill=True) == [(3, c) for c in range(6)]
|
|
@@ -158,3 +158,65 @@ def test_length_path_by_uses_max_not_sum():
|
|
|
158
158
|
assert by_head[(0, 5)]["path_id"] == trunk_path
|
|
159
159
|
assert by_head[(0, 15)]["path_id"] == trunk_path # longest single route wins
|
|
160
160
|
assert by_head[(3, 5)]["path_id"] != trunk_path
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_net_root_end_has_no_sideways_snap(toy_dataset, end_deviation):
|
|
164
|
+
# same straightening as centerline(): the bundled root sits beside the
|
|
165
|
+
# skeleton, which the uncut network meets at a right angle
|
|
166
|
+
mask, root, tips = toy_dataset
|
|
167
|
+
mask_np = (np.asarray(mask.values) == 1).astype(np.uint8)
|
|
168
|
+
_, net, _ = ramify.partition_priority(mask_np, root, tips=tips)
|
|
169
|
+
outlet = net[net["downstream_segment_id"].isna()].geometry.iloc[0]
|
|
170
|
+
assert outlet.coords[0] == (root[1] + 0.5, root[0] + 0.5)
|
|
171
|
+
assert end_deviation(outlet) < 1.0 # ~4.8 px before the cut
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_net_tip_end_skips_skeleton_fork(flared_bar, end_deviation):
|
|
175
|
+
from shapely.geometry import LineString
|
|
176
|
+
|
|
177
|
+
mask, tip, root = flared_bar # flared end as the (given) tip
|
|
178
|
+
_, net, _ = ramify.partition_priority(mask, root, tips=[tip])
|
|
179
|
+
(line,) = net.geometry # no junctions: one segment, cut at both ends
|
|
180
|
+
assert line.coords[-1] == (tip[1] + 0.5, tip[0] + 0.5)
|
|
181
|
+
assert end_deviation(LineString(line.coords[::-1])) < 1.0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _network_is_intact(df, mask_bool):
|
|
185
|
+
by_id = dict(zip(df["segment_id"], df["pixels"]))
|
|
186
|
+
for seg_id, down, pixels in zip(df["segment_id"], df["downstream_segment_id"], df["pixels"]):
|
|
187
|
+
steps = zip(pixels[:-1], pixels[1:])
|
|
188
|
+
if not all(max(abs(a[0] - b[0]), abs(a[1] - b[1])) == 1 for a, b in steps):
|
|
189
|
+
return False
|
|
190
|
+
if not all(mask_bool[r, c] for r, c in pixels):
|
|
191
|
+
return False
|
|
192
|
+
# segments run upstream -> downstream, sharing the junction pixel
|
|
193
|
+
if not pd.isna(down) and pixels[-1] != by_id[int(down)][0]:
|
|
194
|
+
return False
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_end_cut_stops_at_junction_near_root():
|
|
199
|
+
from skimage.draw import polygon
|
|
200
|
+
from ramify.partition import _build_network
|
|
201
|
+
|
|
202
|
+
# flared end with a thin tributary joining inside the root's end disk:
|
|
203
|
+
# the root cut must stay on the short root segment, not swallow the
|
|
204
|
+
# junction
|
|
205
|
+
mask = np.zeros((50, 70), dtype=np.uint8)
|
|
206
|
+
rr, cc = polygon([14, 22, 22, 38, 38, 46], [4, 24, 66, 66, 24, 4])
|
|
207
|
+
mask[rr, cc] = 1
|
|
208
|
+
mask[1:20, 14:16] = 1
|
|
209
|
+
root, tips = (30, 4), [(30, 65), (1, 14)]
|
|
210
|
+
df = _build_network(mask > 0, root, tips, None, "area", None, 1.0)
|
|
211
|
+
assert len(df) == 3
|
|
212
|
+
assert (df["downstream_segment_id"].isna()).sum() == 1
|
|
213
|
+
assert _network_is_intact(df, mask > 0)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_end_cuts_keep_toy_network_intact(toy_dataset):
|
|
217
|
+
from ramify.partition import _build_network
|
|
218
|
+
|
|
219
|
+
mask, root, tips = toy_dataset
|
|
220
|
+
mask_bool = np.asarray(mask.values) == 1
|
|
221
|
+
df = _build_network(mask_bool, root, tips, None, "area", None, 1.0)
|
|
222
|
+
assert _network_is_intact(df, mask_bool)
|
ramify-2.0.0/assets/abstract.png
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
ramify-2.0.0/tests/conftest.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pytest
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@pytest.fixture
|
|
6
|
-
def straight_bar():
|
|
7
|
-
"""A 1-pixel-wide horizontal bar, root at one end, tip at the other."""
|
|
8
|
-
mask = np.zeros((5, 20), dtype=np.uint8)
|
|
9
|
-
mask[2, 1:19] = 1
|
|
10
|
-
return mask, (2, 1), (2, 18)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture
|
|
14
|
-
def stubbed_bar():
|
|
15
|
-
"""A long horizontal bar with a short stub off the middle -- the stub
|
|
16
|
-
should be trimmed away when choosing endpoints without an explicit tip."""
|
|
17
|
-
mask = np.zeros((10, 20), dtype=np.uint8)
|
|
18
|
-
mask[5, 1:19] = 1
|
|
19
|
-
mask[3:5, 10] = 1
|
|
20
|
-
return mask
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture
|
|
24
|
-
def toy_dataset():
|
|
25
|
-
from ramify.data import load
|
|
26
|
-
|
|
27
|
-
return load()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|