Skip to content

Commit

Permalink
ENH: Add structure ID to JSON format.
Browse files Browse the repository at this point in the history
Adds `structure` to each control point in JSON output. This value is ignored during file loading, only used as metadata in consuming applications. See #208.

Note this change requires each `Annotation` instance hold a reference to the `HomeLogic` instance. Looking toward #196, it may make sense to introduce a "AtlasLogic" or similar which contains the volume node, color node, and related metadata _for only one atlas_. This metadata could then be serialized within `Annotation.toDict`.
  • Loading branch information
allemangD committed Mar 7, 2022
1 parent fbdeae7 commit 6ca9f71
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Features:

- Add structure ID and acronym to JSON format. [#208](https://github.com/BICCN/cell-locator/issues/208)

Fixes:

- Don't show exit confirmation when file (or LIMS specimen) is unchanged. [#204](https://github.com/BICCN/cell-locator/pull/204)
Expand Down
4 changes: 4 additions & 0 deletions Documentation/developer_guide/AnnotationFileFormat.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ We mean it.

## Versions

## Next Release

Add the `structure` object to control point. `null` for values with missing structure (outside of atlas, converted from old file, etc.)

## 0.2.0 (2021-08-11)

Unchanged from 0.1.1; this version synchronizes the file format and cell locator release.
Expand Down
59 changes: 38 additions & 21 deletions Modules/Scripted/Home/Home.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ class Annotation(VTKObservationMixin):
DisplayName = 'Annotation'
MarkupType = ''

def __init__(self, markup=None):
def __init__(self, homeLogic, markup=None):
"""Setup the annotation markup and model, and add each to the scene."""

VTKObservationMixin.__init__(self)

self.homeLogic = homeLogic # For structure information.
self.logic = slicer.vtkSlicerSplinesLogic()

if markup is not None:
Expand Down Expand Up @@ -144,6 +145,22 @@ def toDict(self) -> dict:
for control in markup['controlPoints']
]

volumeNode = self.homeLogic.getAnnotation()
transform = self.homeLogic.getWorldRASToIJKTransform(volumeNode)
for i, point in enumerate(markup['controlPoints']):
ras = [0, 0, 0]
self.markup.GetNthControlPointPositionWorld(i, ras)
ijk = transform.TransformPoint(ras)
index = self.homeLogic.getAllenLabelIndex(ijk, volumeNode)

try:
point['structure'] = {
'id': index,
'acronym': self.homeLogic.AllenStructureNames[index],
}
except KeyError:
point['structure'] = None

return {
'markup': markup,
'name': self.markup.GetName(),
Expand All @@ -152,7 +169,7 @@ def toDict(self) -> dict:
}

@classmethod
def fromMarkup(cls, markup):
def fromMarkup(cls, homeLogic, markup):
"""Initialize an Annotation given an existing markup. Selects from subclasses based on that class's MarkupType.
For example, FiducialsAnnotation is a subclass of Annotation, so it is queried. FiducialsAnnotation.MarkupType is
Expand All @@ -167,13 +184,13 @@ def fromMarkup(cls, markup):

for icls in cls.__subclasses__():
if markup.IsA(icls.MarkupType):
return icls(markup=markup)
return icls(homeLogic, markup=markup)

logging.error('Unsupported markup type %r', markup.GetClassName())
return None

@classmethod
def fromDict(cls, data):
def fromDict(cls, homeLogic, data):
"""Convert a dict representation to an annotation, suitable for json deserialization."""

with tempfile('json/annotation.json') as filename:
Expand All @@ -193,7 +210,7 @@ def fromDict(cls, data):
# we want to use fromMarkup so the correct behavior (annotation type) is used for the given markup type.
# since each annotation type may have extra parameters, we should use setMetadata

annotation = cls.fromMarkup(markup)
annotation = cls.fromMarkup(homeLogic, markup)
annotation.setMetadata(data)
annotation.orientation.DeepCopy(listToMat(data['orientation']))

Expand All @@ -211,8 +228,8 @@ class FiducialAnnotation(Annotation):
DisplayName = 'Point'
MarkupType = 'vtkMRMLMarkupsFiducialNode'

def __init__(self, markup=None):
super().__init__(markup=markup)
def __init__(self, homeLogic, markup=None):
super().__init__(homeLogic, markup=markup)


class ClosedCurveAnnotation(Annotation):
Expand All @@ -222,15 +239,15 @@ class ClosedCurveAnnotation(Annotation):
DefaultRepresentationType = 'spline'
DefaultThickness = 50

def __init__(self, markup=None):
def __init__(self, homeLogic, markup=None):
self.representationType = self.DefaultRepresentationType
self.thickness = self.DefaultThickness

self.model = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode())
self.model.CreateDefaultDisplayNodes()

# need to have representationType, thickness, model, etc when the markup is created.
super().__init__(markup=markup)
super().__init__(homeLogic, markup=markup)

generator = self.markup.GetCurveGenerator()
generator.SetNumberOfPointsPerInterpolatingSegment(20)
Expand Down Expand Up @@ -285,7 +302,7 @@ def setMetadata(self, data):
class AnnotationManager:
"""Manage serialization and bookkeeping for a collection of annotations."""

FORMAT_VERSION = '0.2.0+2021.08.12'
FORMAT_VERSION = '0.2.1+2022.03.04'

DefaultReferenceView = 'Coronal'
DefaultOntology = 'Structure'
Expand Down Expand Up @@ -413,12 +430,12 @@ def toDict(self):
}

@classmethod
def fromDict(cls, data):
def fromDict(cls, homeLogic, data):
"""Convert a dict representation to annotation collection, suitable for json deserialization."""

manager = cls()

manager.annotations = [Annotation.fromDict(item) for item in data['markups']]
manager.annotations = [Annotation.fromDict(homeLogic, item) for item in data['markups']]
manager.currentIdx = data['currentId']

manager.referenceView = data['referenceView']
Expand Down Expand Up @@ -453,7 +470,7 @@ def toFile(self, fileName=None, indent=2):
json.dump(data, f, indent=indent)

@classmethod
def fromFile(cls, fileName):
def fromFile(cls, homeLogic, fileName):
"""Load an annotation collection from a json file.
Instance variable fileName is set, so that subsequent calls to toFile() will
Expand All @@ -463,7 +480,7 @@ def fromFile(cls, fileName):
with open(fileName) as f:
data = json.load(f)

manager = cls.fromDict(data)
manager = cls.fromDict(homeLogic, data)
manager.fileName = fileName

return manager
Expand Down Expand Up @@ -603,7 +620,7 @@ def postStartupInitialization():

annotationFilePath = slicer.app.commandOptions().annotationFilePath
if annotationFilePath:
annotations = AnnotationManager.fromFile(annotationFilePath)
annotations = AnnotationManager.fromFile(self.logic, annotationFilePath)
self.setAnnotations(annotations)
self.annotationStored()

Expand Down Expand Up @@ -690,20 +707,20 @@ def saveIfRequired(self):
def onAddCurveAnnotationButtonClicked(self):
"""Add a curve annotation to the tree view."""

self.Annotations.add(ClosedCurveAnnotation())
self.Annotations.add(ClosedCurveAnnotation(self.logic))

def onAddPointAnnotationButtonClicked(self):
"""Add a point annotation to the tree view."""

self.Annotations.add(FiducialAnnotation())
self.Annotations.add(FiducialAnnotation(self.logic))

def onCloneAnnotationButtonClicked(self):
"""Clone the current annotation and add it to the tree view."""

# copy all attributes of the annotation, reusing the existing serialization logic.
# convert to dict and back.
data = self.Annotations.current.toDict()
annotation = Annotation.fromDict(data)
annotation = Annotation.fromDict(self.logic, data)

self.Annotations.add(annotation=annotation)

Expand Down Expand Up @@ -769,7 +786,7 @@ def onLoadAnnotationButtonClicked(self):

self.setInteractionState('explore')
self.Annotations.clear()
annotations = AnnotationManager.fromFile(fileName)
annotations = AnnotationManager.fromFile(self.logic, fileName)
self.setAnnotations(annotations)
self.annotationStored()

Expand Down Expand Up @@ -798,7 +815,7 @@ def loadLIMSSpecimen(self, specimenID):
return

if res.status_code == 200:
annotations = AnnotationManager.fromDict(res.json()['data'])
annotations = AnnotationManager.fromDict(self.logic, res.json()['data'])
self.setAnnotations(annotations)
else:
try:
Expand Down Expand Up @@ -1422,7 +1439,7 @@ def onSceneEndCloseEvent(self, caller=None, event=None):

# Setup Annotations
annotations = AnnotationManager()
annotations.add(ClosedCurveAnnotation())
annotations.add(ClosedCurveAnnotation(self.logic))
self.setAnnotations(annotations)
self.setDefaultSettings()
self.Annotations.current.orientation.DeepCopy(sliceNode.GetSliceToRAS())
Expand Down

0 comments on commit 6ca9f71

Please sign in to comment.