Skip to content

Commit c4eeea6

Browse files
authored
Add new add_nx_graphs method to skeleton.py (#1130)
* add new method to skeleton.py add_nx_graphs * refac add_nx_graph and add unit test * add snapshot nml and test to check old nml generation against new annotation.save after add_nx_graphs * fix typechecks * Merge branch 'master' into 475-add-from_nx_graphs-method-to-skeleton * update Changelog.md
1 parent 4c66c5b commit c4eeea6

File tree

4 files changed

+175
-1
lines changed

4 files changed

+175
-1
lines changed

webknossos/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
1616

1717
### Added
1818
- Added an implementation of padded_with_margins for NDBoundingBox class. [#1120](https://github.com/scalableminds/webknossos-libs/pull/1120)
19+
- Added a new method add_nx_graphs to skeleton.py which supports to add nx.Graphs to the Skeleton object. [#1130](https://github.com/scalableminds/webknossos-libs/pull/1130)
1920

2021
### Changed
2122
- Removed additional logging messages during image conversion. [#1124](https://github.com/scalableminds/webknossos-libs/pull/1124)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<things>
3+
<parameters>
4+
<experiment name="MyDataset" />
5+
<scale x="1" y="1" z="1" />
6+
<zoomLevel zoom="0.4" />
7+
</parameters>
8+
<thing color.a="1.0" color.b="0.2060804781374328" color.g="0.7841773537104477" color.r="0.8354155362332778" groupId="1" id="1" name="tree1">
9+
<nodes>
10+
<node id="1" radius="1.0" x="0" y="1" z="2" />
11+
<node id="2" radius="1.0" x="3" y="1" z="2" />
12+
</nodes>
13+
<edges>
14+
<edge source="1" target="2" />
15+
</edges>
16+
</thing>
17+
<thing color.a="1.0" color.b="0.4725954567221092" color.g="0.8764923182291839" color.r="0.27071931551375283" groupId="2" id="2" name="tree2">
18+
<nodes>
19+
<node id="3" radius="1.0" x="0" y="1" z="2" />
20+
<node id="4" radius="1.0" x="3" y="1" z="2" />
21+
<node id="5" radius="1.0" x="3" y="3" z="3" />
22+
</nodes>
23+
<edges>
24+
<edge source="3" target="4" />
25+
</edges>
26+
</thing>
27+
<branchpoints />
28+
<comments>
29+
<comment content="node 1 nx" node="1" />
30+
<comment content="node 2 nx" node="2" />
31+
<comment content="node 1 nx" node="3" />
32+
<comment content="node 2 nx" node="4" />
33+
<comment content="node 3 nx" node="5" />
34+
</comments>
35+
<groups>
36+
<group id="1" name="first_group" />
37+
<group id="2" name="second_group" />
38+
</groups>
39+
</things>

webknossos/tests/test_skeleton.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44
from typing import List, Optional
55

6+
import networkx as nx
67
import pytest
78

89
import webknossos as wk
@@ -51,6 +52,15 @@ def create_dummy_skeleton() -> wk.Skeleton:
5152
return nml
5253

5354

55+
def create_dummy_nx_graph() -> nx.Graph:
56+
nx_graph = nx.Graph()
57+
nx_graph.add_node(1, position=(0, 1, 2), comment="node 1 nx")
58+
nx_graph.add_node(2, position=(3, 1, 2), comment="node 2 nx")
59+
nx_graph.add_edge(1, 2)
60+
61+
return nx_graph
62+
63+
5464
def test_doc_example() -> None:
5565
from webknossos import Annotation
5666

@@ -108,6 +118,90 @@ def test_skeleton_creation() -> None:
108118
assert grand_children[0].group == groups[0]
109119

110120

121+
def test_add_nx_graph() -> None:
122+
skeleton = create_dummy_skeleton()
123+
node_count = skeleton.get_total_node_count()
124+
tree_count = len(list(skeleton.flattened_trees()))
125+
group_count = len(list(skeleton.flattened_groups()))
126+
max_node_id = skeleton.get_max_node_id()
127+
128+
nx_graph = create_dummy_nx_graph()
129+
skeleton.add_nx_graphs(
130+
{"first_group": [nx_graph, nx_graph], "second_group": [nx_graph]}
131+
)
132+
133+
# check number of groups, nodes and trees
134+
assert len(list(skeleton.flattened_groups())) == group_count + 2
135+
assert skeleton.get_total_node_count() == node_count + 6
136+
assert len(list(skeleton.flattened_trees())) == tree_count + 3
137+
138+
# check group names
139+
for group in skeleton.flattened_groups():
140+
assert group.name in [
141+
"first_group",
142+
"second_group",
143+
"Example Group",
144+
"Nested Group",
145+
]
146+
147+
# check node attributes
148+
max_node_id = skeleton.get_max_node_id()
149+
assert skeleton.get_node_by_id(max_node_id).comment == "node 2 nx"
150+
assert skeleton.get_node_by_id(max_node_id).position == (3, 1, 2)
151+
assert skeleton.get_node_by_id(max_node_id - 1).comment == "node 1 nx"
152+
assert skeleton.get_node_by_id(max_node_id - 1).position == (0, 1, 2)
153+
154+
# check if edge was added
155+
for edge in skeleton.get_tree_by_id(max_node_id - 2).edges:
156+
assert (edge[0].id, edge[1].id) == (max_node_id - 1, max_node_id)
157+
158+
159+
def test_nml_generation(tmp_path: Path) -> None:
160+
OLD_NML_PATH = TESTDATA_DIR / "nmls" / "generate_nml_snapshot.nml"
161+
162+
tree1 = create_dummy_nx_graph()
163+
tree2 = create_dummy_nx_graph()
164+
tree2.add_node(3, position=(3, 3, 3), comment="node 3 nx")
165+
166+
tree_dict = {"first_group": [tree1], "second_group": [tree2]}
167+
168+
# old_nml was generated with the old wknml library as follows:
169+
# params_wknml = {"name": "MyDataset", "scale": (1, 1, 1), "zoomLevel": 0.4}
170+
# old_nml = generate_nml(tree_dict=tree_dict, parameters=params_wknml)
171+
# with open(tmp_path / "annotation_old.nml", "wb") as f:
172+
# write_nml(f, old_nml)
173+
174+
tree_dict = {"first_group": [tree1], "second_group": [tree2]}
175+
176+
annotation = wk.Annotation(
177+
name="MyAnnotation",
178+
dataset_name="MyDataset",
179+
voxel_size=(1, 1, 1),
180+
zoom_level=0.4,
181+
)
182+
183+
annotation.skeleton.add_nx_graphs(tree_dict)
184+
185+
annotation.save(tmp_path / "annotation_new.nml")
186+
187+
old_skeleton = wk.Skeleton.load(OLD_NML_PATH)
188+
new_skeleton = wk.Skeleton.load(tmp_path / "annotation_new.nml")
189+
190+
for old_group, new_group in zip(
191+
old_skeleton.flattened_groups(), new_skeleton.flattened_groups()
192+
):
193+
assert old_group.name == new_group.name
194+
for old_child, new_child in zip(old_group.children, new_group.children):
195+
if isinstance(old_child, wk.Tree) and isinstance(new_child, wk.Tree):
196+
for old_node, new_node in zip(old_child.nodes, new_child.nodes):
197+
assert old_node.comment == new_node.comment
198+
assert old_node.position == new_node.position
199+
assert old_node.radius == new_node.radius
200+
for old_edge, new_edge in zip(old_child.edges, new_child.edges):
201+
assert old_edge[0].position == new_edge[0].position
202+
assert old_edge[1].position == new_edge[1].position
203+
204+
111205
def diff_lines(lines_a: List[str], lines_b: List[str]) -> List[str]:
112206
diff = list(
113207
difflib.unified_diff(

webknossos/webknossos/skeleton/skeleton.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import itertools
22
from os import PathLike
33
from pathlib import Path
4-
from typing import Iterator, Optional, Tuple, Union
4+
from typing import Dict, Iterator, List, Optional, Tuple, Union
55

66
import attr
7+
import networkx as nx
78

89
from ..utils import warn_deprecated
910
from .group import Group
@@ -104,6 +105,45 @@ def save(self, out_path: Union[str, PathLike]) -> None:
104105
annotation = Annotation(name=out_path.stem, skeleton=self, time=None)
105106
annotation.save(out_path)
106107

108+
def add_nx_graphs(
109+
self, tree_dict: Union[List[nx.Graph], Dict[str, List[nx.Graph]]]
110+
) -> None:
111+
"""
112+
A utility to add nx graphs [NetworkX graph object](https://networkx.org/) to a wk skeleton object. Accepts both a simple list of multiple skeletons/trees or a dictionary grouping skeleton inputs.
113+
114+
Arguments:
115+
tree_dict (Union[List[nx.Graph], Dict[str, List[nx.Graph]]]): A list of wK tree-like structures as NetworkX graphs or a dictionary of group names and same lists of NetworkX tree objects.
116+
"""
117+
118+
if not isinstance(tree_dict, dict):
119+
tree_dict = {"main_group": tree_dict}
120+
121+
for group_name, trees in tree_dict.items():
122+
group = self.add_group(group_name)
123+
for tree in trees:
124+
tree_name = tree.graph.get("name", f"tree_{len(list(group.trees))}")
125+
wk_tree = group.add_tree(tree_name)
126+
wk_tree.color = tree.graph.get("color", None)
127+
id_node_dict = {}
128+
for id_with_node in tree.nodes(data=True):
129+
old_id, node = id_with_node
130+
node = wk_tree.add_node(
131+
position=node.get("position"),
132+
comment=node.get("comment", None),
133+
radius=node.get("radius", 1.0),
134+
rotation=node.get("rotation", None),
135+
inVp=node.get("inVp", None),
136+
inMag=node.get("inMag", None),
137+
bitDepth=node.get("bitDepth", None),
138+
interpolation=node.get("interpolation", None),
139+
time=node.get("time", None),
140+
is_branchpoint=node.get("is_branchpoint", False),
141+
branchpoint_time=node.get("branchpoint_time", None),
142+
)
143+
id_node_dict[old_id] = node
144+
for edge in tree.edges():
145+
wk_tree.add_edge(id_node_dict[edge[0]], id_node_dict[edge[1]])
146+
107147
@staticmethod
108148
def from_path(file_path: Union[PathLike, str]) -> "Skeleton":
109149
"""Deprecated. Use Skeleton.load instead."""

0 commit comments

Comments
 (0)