Browse Source

find_label_border_polygonal_chains: always include vertices along border with single neighbour; test against python3.8

Fabian Peter Hammerle 3 years ago
parent
commit
86b953e450

+ 2 - 0
.github/workflows/python.yml

@@ -38,6 +38,7 @@ jobs:
         - 3.5
         - 3.6
         - 3.7
+        - 3.8
       fail-fast: false
     steps:
     - uses: actions/checkout@v1
@@ -74,6 +75,7 @@ jobs:
         - 3.5
         - 3.6
         - 3.7
+        - 3.8
       fail-fast: false
     defaults:
       run:

+ 2 - 0
CHANGELOG.md

@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Fixed
+- `find_label_border_polygonal_chains`: always include vertices along border with single neighbour
+  (previously indeterministic behaviour)
 - type hints
 
 ## [1.1.1] - 2020-10-18

File diff suppressed because it is too large
+ 0 - 0
examples/precentral_gyrus_border.ipynb


+ 73 - 23
freesurfer_surface/__init__.py

@@ -548,32 +548,82 @@ class Surface:
             if len(border_vertex_indices) == 2:
                 yield LineSegment(border_vertex_indices)
 
+    _VertexSubindex = typing.Tuple[int, int]
+
+    @classmethod
+    def _duplicate_border(
+        cls,
+        neighbour_indices: typing.DefaultDict[
+            _VertexSubindex, typing.Set[_VertexSubindex]
+        ],
+        previous_index: _VertexSubindex,
+        current_index: _VertexSubindex,
+        junction_counter: int,
+    ) -> None:
+        split_index = (current_index[0], junction_counter)
+        neighbour_indices[previous_index].add(split_index)
+        neighbour_indices[split_index].add(previous_index)
+        next_index, *extra_indices = filter(
+            lambda i: i != previous_index, neighbour_indices[current_index]
+        )
+        if extra_indices:
+            neighbour_indices[next_index].add(split_index)
+            neighbour_indices[split_index].add(next_index)
+            neighbour_indices[next_index].remove(current_index)
+            neighbour_indices[current_index].remove(next_index)
+            return
+        cls._duplicate_border(
+            neighbour_indices=neighbour_indices,
+            previous_index=split_index,
+            current_index=next_index,
+            junction_counter=junction_counter,
+        )
+
     def find_label_border_polygonal_chains(
         self, label: Label
     ) -> typing.Iterator[PolygonalChain]:
-        segments = set(self._find_label_border_segments(label))
-        available_chains = collections.deque(
-            PolygonalChain(segment.vertex_indices) for segment in segments
-        )
-        # irrespective of its poor performance,
-        # we keep this approach since it's easy to read and fast enough
-        while available_chains:
-            chain = available_chains.pop()
-            last_chains_len = None
-            while last_chains_len != len(available_chains):
-                last_chains_len = len(available_chains)
-                checked_chains = (
-                    collections.deque()
-                )  # type: typing.Deque[PolygonalChain]
-                while available_chains:
-                    potential_neighbour = available_chains.pop()
-                    try:
-                        chain.connect(potential_neighbour)
-                    except PolygonalChainsNotOverlapingError:
-                        checked_chains.append(potential_neighbour)
-                available_chains = checked_chains
-            assert all((segment in segments) for segment in chain.segments())
-            yield chain
+        neighbour_indices = collections.defaultdict(
+            set
+        )  # type: typing.DefaultDict[_VertexSubindex, typing.Set[_VertexSubindex]] # type: ignore
+        for segment in self._find_label_border_segments(label):
+            vertex_indices = [(i, 0) for i in segment.vertex_indices]
+            neighbour_indices[vertex_indices[0]].add(vertex_indices[1])
+            neighbour_indices[vertex_indices[1]].add(vertex_indices[0])
+        junction_counter = 0
+        found_leaf = True
+        while found_leaf:
+            found_leaf = False
+            for leaf_index, leaf_neighbour_indices in neighbour_indices.items():
+                if len(leaf_neighbour_indices) == 1:
+                    found_leaf = True
+                    junction_counter += 1
+                    self._duplicate_border(
+                        neighbour_indices=neighbour_indices,
+                        previous_index=leaf_index,
+                        # pylint: disable=stop-iteration-return; false positive, has 1 item
+                        current_index=next(iter(leaf_neighbour_indices)),
+                        junction_counter=junction_counter,
+                    )
+                    break
+        assert all(len(n) == 2 for n in neighbour_indices.values()), neighbour_indices
+        while neighbour_indices:
+            # pylint: disable=stop-iteration-return; has >= 1 item
+            chain = collections.deque([next(iter(neighbour_indices.keys()))])
+            chain.append(neighbour_indices[chain[0]].pop())
+            neighbour_indices[chain[1]].remove(chain[0])
+            while chain[0] != chain[-1]:
+                previous_index = chain[-1]
+                next_index = neighbour_indices[previous_index].pop()
+                neighbour_indices[next_index].remove(previous_index)
+                chain.append(next_index)
+                assert not neighbour_indices[previous_index], neighbour_indices[
+                    previous_index
+                ]
+                del neighbour_indices[previous_index]
+            assert not neighbour_indices[chain[0]], neighbour_indices[chain[0]]
+            del neighbour_indices[chain[0]]
+            chain.pop()
+            yield PolygonalChain(v[0] for v in chain)
 
     def _unused_vertices(self) -> typing.Set[int]:
         vertex_indices = set(range(len(self.vertices)))

+ 1 - 0
setup.py

@@ -38,6 +38,7 @@ setuptools.setup(
         "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
         "Topic :: Scientific/Engineering :: Information Analysis",
         "Topic :: Scientific/Engineering :: Medical Science Apps.",
         "Topic :: Utilities",

+ 13 - 2
tests/test_surface.py

@@ -485,8 +485,19 @@ def test_find_label_border_polygonal_chains():
     assert len(vertex_indices) == 418
     min_index = vertex_indices.index(min(vertex_indices))
     vertex_indices_normalized = vertex_indices[min_index:] + vertex_indices[:min_index]
-    assert vertex_indices_normalized[:4] == [32065, 32072, 32073, 32080]
-    assert vertex_indices_normalized[-4:] == [36281, 34870, 33454, 33450]
+    if vertex_indices_normalized[-1] > vertex_indices_normalized[1]:
+        vertex_indices_normalized.reverse()
+    assert vertex_indices_normalized[346:353] == [
+        63264,
+        63255,
+        62118,
+        62107,
+        62118,
+        62119,
+        61044,
+    ]
+    assert vertex_indices_normalized[:4] == [32065, 33450, 33454, 34870]
+    assert vertex_indices_normalized[-4:] == [33464, 32080, 32073, 32072]
 
 
 def test__unused_vertices():

Some files were not shown because too many files changed in this diff