Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2008f2
Output settings with id and major.minor.patch version
rchristie Aug 25, 2025
df79681
Read user endpoints files
rchristie Sep 3, 2025
63dc745
Add category groups to working region
rchristie Sep 4, 2025
d80546b
Store mean segment length
rchristie Sep 4, 2025
c8afb6a
Auto align either segment in connection
rchristie Sep 9, 2025
1acbbb9
Add new linking and alignment
rchristie Sep 16, 2025
14d6dfc
Improve building links
rchristie Sep 23, 2025
d734188
Add methods to lock, unlock, select locked links
rchristie Sep 24, 2025
f6f508a
Handle having no radius or rgb values
rchristie Sep 24, 2025
a00d28d
Reimplement auto-align segment
rchristie Sep 26, 2025
3b15eb0
Add method for linking selected end points
rchristie Sep 30, 2025
7147525
Add 2 stage auto align
rchristie Oct 2, 2025
5d36708
Fix link locking from selection
rchristie Oct 2, 2025
cb77539
Fix best fit line side direction
rchristie Oct 2, 2025
e912dea
Handle zero radius and single point paths
rchristie Nov 28, 2025
7803059
Allow links between different annotations in same network group
rchristie Jan 16, 2026
dcc323e
Add interior end points where annotation changes
rchristie Feb 12, 2026
cc29781
Comment out user end points tweaks
rchristie Feb 12, 2026
a19f33b
Improve association of annotation terms
rchristie Feb 13, 2026
9c34f48
Improve association of annotation terms 2
rchristie Feb 16, 2026
62da86b
Do not build links on loading existing connections
rchristie Feb 16, 2026
ee6f3f5
Support interior end points with multiple branches
rchristie Feb 17, 2026
cefd22f
Support segment ignore orientation
rchristie Feb 18, 2026
ef7c059
Docstring fixes
rchristie Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 97 additions & 13 deletions src/segmentationstitcher/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from cmlibs.utils.zinc.field import get_group_list
from cmlibs.utils.zinc.group import group_get_highest_dimension, groups_have_same_local_contents
from cmlibs.zinc.field import Field
import logging


logger = logging.getLogger(__name__)


class AnnotationCategory(Enum):
Expand All @@ -24,9 +28,18 @@ def get_group_name(self):
"""
return '.' + self.name

def get_lower_name(self):
"""
:return: Lower case category name.
"""
return self.name.lower()

def is_connectable(self):
return self in (self.INDEPENDENT_NETWORK, self.NETWORK_GROUP_1, self.NETWORK_GROUP_2)

def is_connectable_different_annotation(self):
return self.is_connectable() and not (self == self.INDEPENDENT_NETWORK)


class Annotation:
"""
Expand Down Expand Up @@ -56,8 +69,8 @@ def decode_settings(self, settings_in: dict):
assert (settings_in.get("name") == self._name) and (settings_in.get("term") == self._term)
settings_dimension = settings_in.get("dimension")
if settings_dimension != self._dimension:
print("WARNING: Segmentation Stitcher. Annotation with name", self._name, "term", self._term,
"was dimension ", settings_dimension, "in settings, is now ", self._dimension,
logger.warning("Segmentation Stitcher. Annotation with name " + self._name, " term " + str(self._term) +
" was dimension " + str(settings_dimension), "in settings, is now " + str(self._dimension) +
". Have input files changed?")
settings_in["dimension"] = self._dimension
# update current settings to gain new ones and override old ones
Expand Down Expand Up @@ -125,6 +138,31 @@ def set_term(self, term):
assert self._term is None
self._term = term

def clear_term(self):
"""
Clear term to None, call in cases of mismatched terms for the same group name.
"""
self._term = None

def is_connectable(self):
"""
:return: True if annotation's category is connectable.
"""
return self._category.is_connectable()

def is_connectable_with(self, other_annotation):
"""
Query whether ends annotated with self and other_annotation can be connected.
:param other_annotation: Another Annotation object.
:return: True if self and other_annotation are allowed to be connected by a link.
"""
if self._category.is_connectable() and (self._category == other_annotation.get_category()):
if other_annotation is self:
return True
elif self._category.is_connectable_different_annotation():
return True
return False


def region_get_annotations(region, network_group1_keywords, network_group2_keywords, term_keywords):
"""
Expand All @@ -146,6 +184,17 @@ def region_get_annotations(region, network_group1_keywords, network_group2_keywo
annotations = []
term_annotations = []
datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS)
# these terms have had slight mismatches with contents of url group, so explicitly matching:
segment_name = region.getParent().getName()
known_terms = {
"epineurium": "http://uri.interlex.org/base/ilx_0103892",
"left cervical vagus nerve": "http://uri.interlex.org/base/ilx_0794142",
"right cervical vagus nerve": "http://uri.interlex.org/base/ilx_0794141",
"left thoracic vagus nerve": "http://uri.interlex.org/base/ilx_0787543",
"right thoracic vagus nerve": "http://uri.interlex.org/base/ilx_0786664",
"left vagus x nerve trunk": "http://uri.interlex.org/base/ilx_0736691",
"right vagus x nerve trunk": "http://uri.interlex.org/base/ilx_0730515"
}
for group in groups:
# clean up name to remove case and leading/trailing whitespace
name = group.getName().strip()
Expand All @@ -159,7 +208,9 @@ def region_get_annotations(region, network_group1_keywords, network_group2_keywo
continue # empty group
if lower_name.isdigit():
continue # ignore as these can never be valid annotation names
category = AnnotationCategory.GENERAL
if '<property name=' in lower_name:
continue # don't want these groups output by mbfxml2ex
category = AnnotationCategory.EXCLUDE if ('_' in lower_name) else AnnotationCategory.GENERAL
for keyword in network_group1_keywords:
if keyword in lower_name:
category = AnnotationCategory.NETWORK_GROUP_1
Expand All @@ -169,9 +220,10 @@ def region_get_annotations(region, network_group1_keywords, network_group2_keywo
if keyword in lower_name:
category = AnnotationCategory.NETWORK_GROUP_2
break
annotation = Annotation(name, None, dimension, category)
term = known_terms.get(lower_name)
annotation = Annotation(name, term, dimension, category)
is_term = False
if category == AnnotationCategory.GENERAL:
if category in (AnnotationCategory.GENERAL, AnnotationCategory.EXCLUDE):
for keyword in term_keywords:
if keyword in lower_name:
is_term = True
Expand All @@ -180,23 +232,55 @@ def region_get_annotations(region, network_group1_keywords, network_group2_keywo
term_annotations.append(annotation)
else:
annotations.append(annotation)

# sort annotations to have networks first, then general, lastly EXCLUDE to associate terms with earlier ones first
ordered_categories = (
AnnotationCategory.NETWORK_GROUP_1,
AnnotationCategory.NETWORK_GROUP_2,
AnnotationCategory.INDEPENDENT_NETWORK,
AnnotationCategory.GENERAL,
AnnotationCategory.EXCLUDE)
assert len(ordered_categories) == len(AnnotationCategory) # in case new categories added, expand the above
sorted_annotations = []
for category in ordered_categories:
for annotation in annotations:
if annotation.get_category() == category:
sorted_annotations.append(annotation)

for term_annotation in term_annotations:
term = term_annotation.get_name()
term_group = fieldmodule.findFieldByName(term).castGroup()
dimension = term_annotation.get_dimension()
for annotation in annotations:
if annotation.get_term() is not None:
continue
term_matched = False
for annotation in sorted_annotations:
if annotation.get_dimension() != dimension:
continue
if annotation.get_category() == AnnotationCategory.EXCLUDE:
continue
name = annotation.get_name()
name_group = fieldmodule.findFieldByName(name).castGroup()
if groups_have_same_local_contents(name_group, term_group):
annotation.set_term(term)
break
else:
print("WARNING: Segmentation Stitcher. Did not find matching annotation name for term", term,
". Adding separate annotation.")
old_term = annotation.get_term()
if old_term:
if old_term != term:
logger.warning("Segment " + segment_name + ": " +
"Annotation name " + name + " already has term " + old_term +
" but matched group with term " + term + ". Keeping original term.")
else:
# logger.info("Segment " + segment_name + ": " + "Annotation name " + name +
# " discovered term " + term + ".")
annotation.set_term(term)
term_matched = True
# do not break to allow all groups with matching contents to get the term
else:
known_term = known_terms.get(name.lower())
if known_term == term:
logger.warning("Segment " + segment_name + ": " +
"Known annotation name " + name + " and term " + term + " groups differ. Using name group.")
term_matched = True
if not term_matched:
logger.warning("Segment " + segment_name + ": " +
". Did not find matching annotation name for term " + term + ". Adding separate annotation.")
term_annotation.set_term(term)
index = 0
for annotation in annotations:
Expand Down
Loading