diff --git a/CHANGES.md b/CHANGES.md index 89cce1f..6582056 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/Documentation/developer_guide/AnnotationFileFormat.md b/Documentation/developer_guide/AnnotationFileFormat.md index 4dde961..0ae25f9 100644 --- a/Documentation/developer_guide/AnnotationFileFormat.md +++ b/Documentation/developer_guide/AnnotationFileFormat.md @@ -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. diff --git a/Modules/Scripted/Home/Home.py b/Modules/Scripted/Home/Home.py index ad6fc7b..086c15b 100644 --- a/Modules/Scripted/Home/Home.py +++ b/Modules/Scripted/Home/Home.py @@ -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: @@ -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(), @@ -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 @@ -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: @@ -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'])) @@ -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): @@ -222,7 +239,7 @@ 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 @@ -230,7 +247,7 @@ def __init__(self, markup=None): 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) @@ -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' @@ -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'] @@ -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 @@ -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 @@ -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() @@ -690,12 +707,12 @@ 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.""" @@ -703,7 +720,7 @@ def onCloneAnnotationButtonClicked(self): # 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) @@ -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() @@ -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: @@ -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()) @@ -1678,8 +1695,7 @@ def onCurrentItemChanged(self, vtkId): self.setAttributeWidgetsEnabled() def onOntologyChanged(self, ontology): - annotation = slicer.mrmlScene.GetFirstNodeByName( - os.path.splitext(os.path.basename(HomeLogic.annotationFilePath(self.logic.atlasType())))[0]) + annotation = self.logic.getAnnotation() if ontology == "Structure": colorNodeID = slicer.mrmlScene.GetFirstNodeByName("allen").GetID() elif ontology == "Layer": @@ -1741,6 +1757,7 @@ class HomeLogic(object): def __init__(self): self.AllenStructurePaths = {} self.AllenLayerStructurePaths = {} + self.AllenStructureNames = {} self.SlicerToAllenMapping = {} self.AllenToSlicerMapping = {} @@ -1877,28 +1894,30 @@ def loadData(self, atlas_type): with open(self.ontologyFilePath(atlas_type=atlas_type)) as content: msg = json.load(content)["msg"] - allenStructureNames = {} + self.AllenStructureNames = {} for structure in msg: - allenStructureNames[structure["id"]] = structure["acronym"] + self.AllenStructureNames[structure["id"]] = structure["acronym"] self.AllenStructurePaths = {} for structure in msg: self.AllenStructurePaths[structure["id"]] = " > ".join( - [allenStructureNames[int(structure_id)] for structure_id in structure["structure_id_path"][1:-1].split("/")]) + [self.AllenStructureNames[int(structure_id)] for structure_id in + structure["structure_id_path"][1:-1].split("/")]) # Load "layer" ontology (only available for the CCF atlas type) if atlas_type == HomeLogic.CCF_ATLAS: with open(self.layerOntologyFilePath()) as content: msg = json.load(content)["msg"] - allenStructureNames = {997: "root"} + self.AllenStructureNames[997] = "root" for structure in msg: - allenStructureNames[structure["id"]] = structure["acronym"] + self.AllenStructureNames[structure["id"]] = structure["acronym"] self.AllenLayerStructurePaths = {} for structure in msg: self.AllenLayerStructurePaths[structure["id"]] = " > ".join( - [allenStructureNames[int(structure_id)] for structure_id in structure["structure_id_path"][1:-1].split("/")]) + [self.AllenStructureNames[int(structure_id)] for structure_id in + structure["structure_id_path"][1:-1].split("/")]) # Load annotation try: @@ -1918,63 +1937,103 @@ def loadData(self, atlas_type): def getCrosshairPixelString(self, crosshairNode): """Given a crosshair node, create a human readable string describing the contents associated with crosshair cursor position.""" - ras = [0.0,0.0,0.0] - xyz = [0.0,0.0,0.0] + + ras = [0.0, 0.0, 0.0] + xyz = [0.0, 0.0, 0.0] insideView = crosshairNode.GetCursorPositionRAS(ras) sliceNode = crosshairNode.GetCursorPositionXYZ(xyz) + appLogic = slicer.app.applicationLogic() sliceLogic = appLogic.GetSliceLogic(sliceNode) - if not insideView or not sliceNode or not sliceLogic: - return - def _roundInt(value): - try: - return int(round(value)) - except ValueError: - return 0 + if not all((insideView, sliceNode, sliceLogic)): + return None layerLogic = sliceLogic.GetLabelLayer() volumeNode = layerLogic.GetVolumeNode() - ijk = [0, 0, 0] - if volumeNode: - xyToIJK = layerLogic.GetXYToIJKTransform() - ijkFloat = xyToIJK.TransformDoublePoint(xyz) - ijk = [_roundInt(value) for value in ijkFloat] - return self.getPixelString(volumeNode, ijk, ras) + if not volumeNode: + return None - def getPixelString(self,volumeNode,ijk,ras): + return self.getPixelString(ras, volumeNode) + + def getPixelString(self, ras, volumeNode): """Given a volume node, create a human readable string describing the contents""" - if not volumeNode: + if not volumeNode or not volumeNode.IsA('vtkMRMLLabelMapVolumeNode'): + return "" + + RASToIJK = self.getWorldRASToIJKTransform(volumeNode) + ijk = RASToIJK.TransformPoint(ras) + + allenLabelIndex = self.getAllenLabelIndex(ijk, volumeNode) + if allenLabelIndex is None: + return "" + + rasStr = ' '.join(f'{coeff:3.1f}' for coeff in ras) + + displayNode = volumeNode.GetDisplayNode() + if not displayNode: + return "" + + colorNode = displayNode.GetColorNode() + if not colorNode: return "" + + try: + labelValue = "Unknown" + if colorNode.GetName() == "allen": + labelValue = self.AllenStructurePaths[allenLabelIndex] + elif colorNode.GetName() == "allen_layer": + labelValue = self.AllenLayerStructurePaths[allenLabelIndex] + return "%s | %s (%d)" % (rasStr, labelValue, allenLabelIndex) + except KeyError: + # print(allenLabelIndex) + return rasStr + + def getWorldRASToIJKTransform(self, volumeNode): + if not volumeNode or not volumeNode.IsA('vtkMRMLLabelMapVolumeNode'): + return None + + # World RAS -> Local RAS -> Local IJK + + transform = vtk.vtkGeneralTransform() + transform.Identity() + + # World RAS -> Local RAS + parent = volumeNode.GetParentTransformNode() + if parent: + parent.GetTransformFromWorld(transform) + + # Local RAS -> Local IJK + matrix = vtk.vtkMatrix4x4() + volumeNode.GetRASToIJKMatrix(matrix) + transform.Concatenate(matrix) + + return transform + + def getAllenLabelIndex(self, ijk, volumeNode): + """Given a volume node, fetch the allen structure id at a point""" + if not volumeNode or not volumeNode.IsA('vtkMRMLLabelMapVolumeNode'): + print('bad volumeNode') + return None + imageData = volumeNode.GetImageData() if not imageData: - return "" + print('bad imageData') + return None + + ijk = [int(round(coeff)) for coeff in ijk] + dims = imageData.GetDimensions() - for ele in range(3): - if ijk[ele] < 0 or ijk[ele] >= dims[ele]: - return "" # Out of frame - rasStr = "{ras_x:3.1f} {ras_y:3.1f} {ras_z:3.1f}".format(ras_x=abs(ras[0]), ras_y=abs(ras[1]), ras_z=abs(ras[2])) - if volumeNode.IsA("vtkMRMLLabelMapVolumeNode"): - labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0)) - labelValue = "Unknown" - displayNode = volumeNode.GetDisplayNode() - if displayNode: - colorNode = displayNode.GetColorNode() - if colorNode and labelIndex > 0: - allenLabelIndex = self.SlicerToAllenMapping[labelIndex] - try: - if colorNode.GetName() == "allen": - labelValue = self.AllenStructurePaths[allenLabelIndex] - elif colorNode.GetName() == "allen_layer": - labelValue = self.AllenLayerStructurePaths[allenLabelIndex] - return "%s | %s (%d)" % (rasStr, labelValue, allenLabelIndex) - except KeyError: - #print(allenLabelIndex) - return rasStr + if not all(0 <= ele < bound for ele, bound in zip(ijk, dims)): + if hasattr(self, 'DEBUG'): + print('bad bounds', ijk, dims) + return None # Out of frame - return "" + labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0)) + allenLabelIndex = self.SlicerToAllenMapping[labelIndex] + return allenLabelIndex @staticmethod def limsBaseURL(): @@ -1986,6 +2045,17 @@ def atlasType(self): def setAtlasType(self, atlasType): self._atlasType = atlasType + def getAnnotation(self): + annotation = slicer.mrmlScene.GetFirstNodeByName( + os.path.splitext( + os.path.basename( + HomeLogic.annotationFilePath(self.atlasType()) + ) + )[0] + ) + + return annotation + class HomeTest(ScriptedLoadableModuleTest): """ diff --git a/Scripts/convert/converters.py b/Scripts/convert/converters.py index e8d62ac..631fbae 100644 --- a/Scripts/convert/converters.py +++ b/Scripts/convert/converters.py @@ -13,6 +13,7 @@ # most-recent versions first version_order = [ + 'v0.2.1+2022.03.04', 'v0.2.0+2021.08.12', 'v0.1.1+2021.06.11', 'v0.1.0+2020.09.18', diff --git a/Scripts/convert/model.py b/Scripts/convert/model.py index 9a31165..62b928f 100644 --- a/Scripts/convert/model.py +++ b/Scripts/convert/model.py @@ -3,7 +3,7 @@ import inspect from dataclasses import dataclass from pathlib import Path -from typing import List, Tuple +from typing import List, Tuple, Optional __all__ = ['Annotation', 'Document', 'Converter', 'versioned'] @@ -13,6 +13,19 @@ float, float, float, float, float, float, float, float] +@dataclass +class Structure: + id: int + acronym: str + +@dataclass +class Point: + position: Vector3f + structure: Optional[Structure] = None + + def __iter__(self): + # enables unpacking like `x, y, z = point` + return iter(self.position) @dataclass class Annotation: @@ -40,7 +53,7 @@ class Annotation: ) """A transformation matrix storing the orientation of the slicing plane.""" - points: List[Vector3f] = dataclasses.field(default_factory=list) + points: List[Point] = dataclasses.field(default_factory=list) """Control point positions for the annotation markup.""" diff --git a/Scripts/convert/versions/v0.0.0+2019.01.26.py b/Scripts/convert/versions/v0.0.0+2019.01.26.py index 2aec474..fa583dc 100644 --- a/Scripts/convert/versions/v0.0.0+2019.01.26.py +++ b/Scripts/convert/versions/v0.0.0+2019.01.26.py @@ -27,7 +27,7 @@ def normalize(cls, data: dict): ann.coordinate_system = 'LPS' ann.points = [ - (-p['x'], -p['y'], p['z']) # RAS → LPS conversion + model.Point((-p['x'], -p['y'], p['z'])) # RAS → LPS conversion for p in dmark['Points'] ] diff --git a/Scripts/convert/versions/v0.0.0+2020.04.16.py b/Scripts/convert/versions/v0.0.0+2020.04.16.py index dc8c803..fd7641f 100644 --- a/Scripts/convert/versions/v0.0.0+2020.04.16.py +++ b/Scripts/convert/versions/v0.0.0+2020.04.16.py @@ -39,7 +39,7 @@ def normalize(cls, data: dict): ann.coordinate_system = 'LPS' ann.points = [ - (-p['x'], -p['y'], p['z']) # RAS → LPS conversion + model.Point((-p['x'], -p['y'], p['z'])) # RAS → LPS conversion for p in dmark['Points'] ] diff --git a/Scripts/convert/versions/v0.0.0+2020.08.26.py b/Scripts/convert/versions/v0.0.0+2020.08.26.py index bf9db19..8171a25 100644 --- a/Scripts/convert/versions/v0.0.0+2020.08.26.py +++ b/Scripts/convert/versions/v0.0.0+2020.08.26.py @@ -27,7 +27,7 @@ def normalize(cls, data: dict): ann.coordinate_units = dmark['coordinateUnits'] for point in dmark['controlPoints']: - ann.points.append(tuple(point['position'])) + ann.points.append(model.Point(tuple(point['position']))) doc.annotations.append(ann) @@ -50,7 +50,7 @@ def specialize(cls, doc: model.Document): 'label': f'MarkupsClosedCurve-{i}', 'description': '', 'associatedNodeID': 'vtkMRMLScalarVolumeNode1', - 'position': pt, + 'position': pt.position, 'orientation': [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, +0.0, +0.0, +1.0], diff --git a/Scripts/convert/versions/v0.1.0+2020.09.18.py b/Scripts/convert/versions/v0.1.0+2020.09.18.py index a6381b1..451780e 100644 --- a/Scripts/convert/versions/v0.1.0+2020.09.18.py +++ b/Scripts/convert/versions/v0.1.0+2020.09.18.py @@ -29,7 +29,7 @@ def normalize(cls, data: dict): ann.coordinate_units = dmark['coordinateUnits'] for point in dmark['controlPoints']: - ann.points.append(tuple(point['position'])) + ann.points.append(model.Point(tuple(point['position']))) doc.annotations.append(ann) @@ -49,7 +49,7 @@ def specialize(cls, doc: model.Document): 'controlPoints': [ { 'id': str(i), - 'position': pt, + 'position': pt.position, 'orientation': [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, +0.0, +0.0, +1.0] diff --git a/Scripts/convert/versions/v0.1.1+2021.06.11.py b/Scripts/convert/versions/v0.1.1+2021.06.11.py index e5051d2..0579f19 100644 --- a/Scripts/convert/versions/v0.1.1+2021.06.11.py +++ b/Scripts/convert/versions/v0.1.1+2021.06.11.py @@ -29,7 +29,7 @@ def normalize(cls, data: dict): ann.coordinate_units = dmark['coordinateUnits'] for point in dmark['controlPoints']: - ann.points.append(tuple(point['position'])) + ann.points.append(model.Point(tuple(point['position']))) doc.annotations.append(ann) @@ -48,7 +48,7 @@ def specialize(cls, doc: model.Document): 'controlPoints': [ { 'id': str(i), - 'position': pt, + 'position': pt.position, 'orientation': [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, +0.0, +0.0, +1.0] diff --git a/Scripts/convert/versions/v0.2.0+2021.08.12.py b/Scripts/convert/versions/v0.2.0+2021.08.12.py index e5051d2..0579f19 100644 --- a/Scripts/convert/versions/v0.2.0+2021.08.12.py +++ b/Scripts/convert/versions/v0.2.0+2021.08.12.py @@ -29,7 +29,7 @@ def normalize(cls, data: dict): ann.coordinate_units = dmark['coordinateUnits'] for point in dmark['controlPoints']: - ann.points.append(tuple(point['position'])) + ann.points.append(model.Point(tuple(point['position']))) doc.annotations.append(ann) @@ -48,7 +48,7 @@ def specialize(cls, doc: model.Document): 'controlPoints': [ { 'id': str(i), - 'position': pt, + 'position': pt.position, 'orientation': [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, +0.0, +0.0, +1.0] diff --git a/Scripts/convert/versions/v0.2.1+2022.03.04.py b/Scripts/convert/versions/v0.2.1+2022.03.04.py new file mode 100644 index 0000000..4bba24f --- /dev/null +++ b/Scripts/convert/versions/v0.2.1+2022.03.04.py @@ -0,0 +1,85 @@ +import model + + +class Converter(model.Converter): + @classmethod + def normalize(cls, data: dict): + doc = model.Document() + doc.current_id = data['currentId'] + doc.reference_view = data['referenceView'] + doc.ontology = data['ontology'] + doc.stepSize = data['stepSize'] + doc.camera_position = tuple(data['cameraPosition']) + doc.camera_view_up = tuple(data['cameraViewUp']) + + for dann in data['markups']: + dmark = dann['markup'] + + ann = model.Annotation() + ann.name = dann['name'] + ann.orientation = dann['orientation'] + ann.markup_type = dmark['type'] + + if ann.markup_type == 'ClosedCurve': + ann.representation_type = dann['representationType'] + ann.thickness = dann['thickness'] + + ann.coordinate_system = dmark['coordinateSystem'] + if 'coordinateUnits' in dmark: + ann.coordinate_units = dmark['coordinateUnits'] + + for point in dmark['controlPoints']: + position = tuple(point['position']) + structure = point.get('structure', None) + if structure: + structure = model.Structure( + structure['id'], + structure['acronym'], + ) + + ann.points.append(model.Point(position, structure)) + + doc.annotations.append(ann) + + return doc + + @classmethod + @model.versioned + def specialize(cls, doc: model.Document): + data = dict() + data['markups'] = [ + { + 'markup': { + 'type': ann.markup_type, + 'coordinateSystem': ann.coordinate_system, + 'coordinateUnits': ann.coordinate_units, + 'controlPoints': [ + { + 'id': str(i), + 'position': pt.position, + 'orientation': [-1.0, -0.0, -0.0, + -0.0, -1.0, -0.0, + +0.0, +0.0, +1.0], + 'structure': { + 'id': pt.structure.id, + 'acronym': pt.structure.acronym + } if pt.structure else None + } + for i, pt in enumerate(ann.points, start=1) + ], + }, + 'name': ann.name, + 'orientation': ann.orientation, + 'representationType': ann.representation_type, + 'thickness': ann.thickness + } + for ann in doc.annotations + ] + data['currentId'] = doc.current_id + data['referenceView'] = doc.reference_view + data['ontology'] = doc.ontology + data['stepSize'] = doc.stepSize + data['cameraPosition'] = doc.camera_position + data['cameraViewUp'] = doc.camera_view_up + + return data