diff --git a/CrossSectionAnalysis/CrossSectionAnalysis.py b/CrossSectionAnalysis/CrossSectionAnalysis.py index 31a3646..b442b20 100644 --- a/CrossSectionAnalysis/CrossSectionAnalysis.py +++ b/CrossSectionAnalysis/CrossSectionAnalysis.py @@ -73,6 +73,8 @@ def setup(self): uiWidget.setMRMLScene(slicer.mrmlScene) self.logic = CrossSectionAnalysisLogic() + self.ui.parameterSetSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", self.moduleName) + self.initializeParameterNode() # Track the polydata regions identified in the lumen surface. self._lumenRegions = [] @@ -81,8 +83,6 @@ def setup(self): # Position the crosshair on a lumen region. self.crosshair=slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLCrosshairNode") - self.initializeParameterNode() - self.resetMoveToPointSliderWidget() # TODO: a module must not change the application-wide unit format @@ -122,6 +122,7 @@ def setup(self): self.ui.axialSliceViewSelector.connect("currentNodeChanged(vtkMRMLNode*)", lambda node: self.setNodeReferenceInParameterNode("AxialSliceNode", node)) self.ui.longitudinalSliceViewSelector.connect("currentNodeChanged(vtkMRMLNode*)", lambda node: self.setNodeReferenceInParameterNode("LongitudinalSliceNode", node)) self.ui.segmentSelector.connect("currentSegmentChanged(QString)", lambda value: self.setValueInParameterNode("InputSegment", value)) + self.ui.useCurrentPointAsOriginButton.connect("clicked()", lambda: self.setValueInParameterNode("OriginPointIndex", int(self.ui.moveToPointSliderWidget.value))) self.ui.radioRAS.connect("clicked()", lambda: self.setValueInParameterNode("UseLPS", False)) self.ui.radioLPS.connect("clicked()", lambda: self.setValueInParameterNode("UseLPS", True)) self.ui.distinctColumnsCheckBox.connect("toggled(bool)", lambda value: self.setValueInParameterNode("DistinctColumns", "True" if value else "False")) @@ -137,7 +138,8 @@ def setup(self): self.ui.surfaceInformationGoToToolButton.connect("toggled(bool)", lambda value: self.setValueInParameterNode("GoToRegion", "True" if value else "False")) # connections - self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) + self.ui.parameterSetSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onParameterNodeChanged) + self.ui.applyButton.connect('clicked(bool)', self.onApply) self.ui.inputCenterlineSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.setInputCenterlineNode) self.ui.segmentationSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputSegmentationNode) @@ -171,6 +173,33 @@ def setup(self): # Refresh wall result widgets self.updateWallLabelsVisibility() + def initializeParameterNode(self): + """ + Ensure parameter node exists and observed. + """ + # Parameter node stores all user choices in parameter values, node selections, etc. + # so that when the scene is saved and reloaded, these settings are restored. + + # The initial parameter node originates from logic and added to the parameter set node. + # Other parameter nodes are created by the parameter set node and used here. + if not self._parameterNode: + self.setParameterNode(self.logic.getParameterNode()) + wasBlocked = self.ui.parameterSetSelector.blockSignals(True) + self.ui.parameterSetSelector.setCurrentNode(self._parameterNode) + self.ui.parameterSetSelector.blockSignals(wasBlocked) + + def onParameterNodeChanged(self, parameterNode): + # Distinguish between a new and used parameter node. + pointIndex = 0 + if parameterNode.HasParameter("Initialized"): + pointIndex = parameterNode.GetParameter("BrowsePointIndex") + self.setParameterNode(parameterNode) + self.updatePlotChartView(parameterNode) + self.resetOutput() + if parameterNode.HasParameter("Initialized"): + self.onApply(True) + self.ui.moveToPointSliderWidget.setValue(float(pointIndex)) + def cleanup(self): """ Called when the application closes and the module widget is destroyed. @@ -185,14 +214,15 @@ def exit(self): """ Called each time the user opens a different module. """ - # Do not react to parameter node changes (GUI will be updated when the user enters into the module) - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) self.ui.surfaceInformationPaintToolButton.setChecked(False) def onSceneStartClose(self, caller, event): """ Called just before the scene is closed. """ + self.logic.initMemberVariables() + self._lumenRegions.clear() + self.ui.browseCollapsibleButton.collapsed = True # Parameter node will be reset, do not use it anymore self.setParameterNode(None) @@ -201,20 +231,6 @@ def onSceneEndClose(self, caller, event): Called just after the scene is closed. """ # Clean up logic variables first. Avoids some Python console errors. - self.logic.initMemberVariables() - # If this module is shown while the scene is closed then recreate a new parameter node immediately - if self.parent.isEntered: - self.initializeParameterNode() - self._lumenRegions.clear() - - def initializeParameterNode(self): - """ - Ensure parameter node exists and observed. - """ - # Parameter node stores all user choices in parameter values, node selections, etc. - # so that when the scene is saved and reloaded, these settings are restored. - - self.setParameterNode(self.logic.getParameterNode()) def setParameterNode(self, inputParameterNode): """ @@ -222,8 +238,9 @@ def setParameterNode(self, inputParameterNode): Observation is needed because when the parameter node is changed then the GUI must be updated immediately. """ - if inputParameterNode: - self.logic.setDefaultParameters(inputParameterNode) + if inputParameterNode == self._parameterNode: + # No change + return # Unobserve previously selected parameter node and add an observer to the newly selected. # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module @@ -233,14 +250,37 @@ def setParameterNode(self, inputParameterNode): self._parameterNode = inputParameterNode if self._parameterNode is not None: self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - - # Initial GUI update - self.updateGUIFromParameterNode() + # Do not set defaults each time. + if not self._parameterNode.HasParameter("Initialized"): + self.logic.setDefaultParameters(self._parameterNode) + else: + # Initial GUI update + self.updateGUIFromParameterNode() def resetOutput(self): + # Only UI widgets, output tables and plot chart/series are left untouched. self.ui.moveToPointSliderWidget.setValue(0) self.resetMoveToPointSliderWidget() self.clearMetrics() + self.ui.browseCollapsibleButton.collapsed = True + + def updatePlotChartNode(self, parameterNode): + plotChartNode = parameterNode.GetNodeReference("PlotChartNode") + if not plotChartNode: + plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode") + plotChartNode.SetName(parameterNode.GetName()) # Name in the parameter set combobox. + parameterNode.SetNodeReferenceID("PlotChartNode", plotChartNode.GetID()) + self.logic.plotChartNode = plotChartNode + + def updatePlotChartView(self, parameterNode = None): + if not slicer.app.layoutManager().plotWidget(0): + return + if self.logic.plotChartNode: + slicer.app.layoutManager().plotWidget(0).mrmlPlotViewNode().SetPlotChartNodeID(self.logic.plotChartNode.GetID()) + if not slicer.app.layoutManager().plotWidget(0).visible: + return + if self.logic.outputPlotSeriesNode: + self.logic.showPlot() def updateGUIFromParameterNode(self, caller=None, event=None): """ @@ -253,7 +293,6 @@ def updateGUIFromParameterNode(self, caller=None, event=None): # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) self._updatingGUIFromParameterNode = True - # Update the logic self.logic.setInputCenterlineNode(self._parameterNode.GetNodeReference("InputCenterline")) @@ -273,6 +312,7 @@ def updateGUIFromParameterNode(self, caller=None, event=None): self.logic.setShowCrossSection(self._parameterNode.GetParameter("ShowCrossSection") == "True") self.logic.axialSliceHorizontalFlip = (self._parameterNode.GetParameter("AxialSliceHorizontalFlip") == "True") if self._parameterNode.GetParameter("AxialSliceHorizontalFlip") else False self.logic.axialSliceVerticalFlip = (self._parameterNode.GetParameter("AxialSliceVerticalFlip") == "True") if self._parameterNode.GetParameter("AxialSliceVerticalFlip") else False + self.logic.relativeOriginPointIndex = int(self._parameterNode.GetParameter("OriginPointIndex")) if self._parameterNode.GetParameter("OriginPointIndex") else 0 # Update node selectors and sliders @@ -340,15 +380,16 @@ def setValueInParameterNode(self, parameterName, value): self._parameterNode.SetParameter(parameterName, str(value)) def createOutputNodes(self): - #if self.logic.isCenterlineRadiusAvailable(): - if not self.ui.outputTableSelector.currentNode(): - outputTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - self.ui.outputTableSelector.setCurrentNode(outputTableNode) - if not self.ui.outputPlotSeriesSelector.currentNode(): - outputPlotSeriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode") - self.ui.outputPlotSeriesSelector.setCurrentNode(outputPlotSeriesNode) - - def onApplyButton(self): + if not self.ui.outputTableSelector.currentNode(): + outputTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + self.ui.outputTableSelector.setCurrentNode(outputTableNode) + if not self.ui.outputPlotSeriesSelector.currentNode(): + outputPlotSeriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode") + self.ui.outputPlotSeriesSelector.setCurrentNode(outputPlotSeriesNode) + # Update/create the plot chart node in logic. There is no widget tracking the chart node. + self.updatePlotChartNode(self._parameterNode) + + def onApply(self, replay = False): if not self.logic.isInputCenterlineValid(): msg = _("Input is invalid.") slicer.util.showStatusMessage(msg, 3000) @@ -358,12 +399,15 @@ def onApplyButton(self): try: slicer.app.setOverrideCursor(qt.Qt.WaitCursor) - self.previousLayoutId = slicer.app.layoutManager().layout - self.clearMetrics() - self.createOutputNodes() - self.logic.run() + if not replay: + self.previousLayoutId = slicer.app.layoutManager().layout + self.clearMetrics() + self.createOutputNodes() + self.logic.run() + self.onGoToOriginPoint() - self.ui.browseCollapsibleButton.collapsed = False + tableHasData = (self.logic.outputTableNode and self.logic.outputTableNode.GetNumberOfRows()) + self.ui.browseCollapsibleButton.collapsed = (not tableHasData) isCenterlineRadiusAvailable = self.logic.isCenterlineRadiusAvailable() self.ui.toggleTableLayoutButton.visible = self.logic.outputTableNode is not None @@ -376,13 +420,15 @@ def onApplyButton(self): self.ui.moveToMaximumAreaPushButton.enabled = self.logic.lumenSurfaceNode is not None self.ui.showCrossSectionButton.enabled = self.logic.lumenSurfaceNode is not None - numberOfPoints = self.logic.getNumberOfPoints() - ## Prevent going to the endpoint (direction computation is only implemented for models with forward difference) - #numberOfPoints -= 1 - - self.ui.moveToPointSliderWidget.maximum = numberOfPoints - 1 - self.updateMeasurements() - self.updateWallLabelsVisibility() + if tableHasData: + numberOfPoints = self.logic.getNumberOfPoints() + ## Prevent going to the endpoint (direction computation is only implemented for models with forward difference) + #numberOfPoints -= 1 + self.ui.moveToPointSliderWidget.maximum = numberOfPoints - 1 + self.updateMeasurements() + self.updateWallLabelsVisibility() + if self.logic.plotChartNode and self.logic.outputPlotSeriesNode: + self.updatePlotChartView() finally: slicer.app.restoreOverrideCursor() @@ -574,6 +620,7 @@ def updateMeasurements(self): def setCurrentPointIndex(self, value): if not self.logic.isInputCenterlineValid(): return + self.setValueInParameterNode("BrowsePointIndex", value) pointIndex = int(value) # Update slice view position @@ -594,7 +641,7 @@ def setCurrentPointIndex(self, value): self.updateUIWithMetrics(value) def setPlotSeriesType(self, type): - self.setValueInParameterNode("OutputPlotSeriesType", str(self.ui.outputPlotSeriesTypeComboBox.currentData)) + self.setValueInParameterNode("OutputPlotSeriesType", self.ui.outputPlotSeriesTypeComboBox.currentData) self.logic.setPlotSeriesType(self.ui.outputPlotSeriesTypeComboBox.currentData) def setHorizontalFlip(self): @@ -618,7 +665,7 @@ def setInputCenterlineNode(self, centerlineNode): self.ui.inputCenterlineSelector.setCurrentNode(None) self.logic.setInputCenterlineNode(None) return - # N.B. updateGUIFromParameterNode() has already done this. + # Notes: updateGUIFromParameterNode() has already done this. self.logic.setInputCenterlineNode(centerlineNode) self.updatePlotOptions() self.updateWallLabelsVisibility() @@ -750,7 +797,7 @@ def checkAndSetSegmentEditor(self, setNodes = False): Show the number of points and the number of cells of the selected region. Optionally locate the region in all slice views. The 'Paint' tool of the 'Segment editor' may then be used to fix a hole in a lumen. - N.B: some regions may be as small as 3 points with the same coordinates, invisible. + Notes: some regions may be as small as 3 points with the same coordinates, invisible. """ def onRegionSelected(self, id): if (not self._lumenRegions): @@ -832,14 +879,7 @@ def initMemberVariables(self): self.lumenSurfaceNode = None self.currentSegmentID = "" self.crossSectionPolyDataCache = {} - # Stack of cross-sections - self.appendedPolyData = vtk.vtkAppendPolyData() - self.allCrossSectionsModelNode = None self.crossSectionColor = [0.2, 0.2, 1.0] - ## Do not re-append - self.crossSectionsPointIndices = vtk.vtkIntArray() - ## To reset things if the appended model is removed by the user - self.sceneNodeRemovedObservation = None self.showCrossSection = False self.crossSectionModelNode = None self.maximumInscribedSphereModelNode = None @@ -864,27 +904,20 @@ def showStatusMessage(self, messages): slicer.util.showStatusMessage(msg, 3000) slicer.app.processEvents() - @property - def relativeOriginPointIndex(self): - originPointIndexStr = self.getParameterNode().GetParameter("originPointIndex") - originPointIndex = int(float(originPointIndexStr)) if originPointIndexStr else 0 - return originPointIndex - - @relativeOriginPointIndex.setter - def relativeOriginPointIndex(self, originPointIndex): - self.getParameterNode().SetParameter("originPointIndex", str(originPointIndex)) - def setDefaultParameters(self, parameterNode): """ Initialize parameter node with default settings. """ + parameterNode.SetParameter("OriginPointIndex", "0") parameterNode.SetParameter("UseLPS", "False") parameterNode.SetParameter("DistinctColumns", "False") parameterNode.SetParameter("CentreInSliceView", "True") parameterNode.SetParameter("OrthogonalReformat", "True") parameterNode.SetParameter("ShowMISModel", "False") parameterNode.SetParameter("ShowCrossSectionModel", "False") - parameterNode.SetParameter("OutputPlotSeriesType", "0") + parameterNode.SetParameter("OutputPlotSeriesType", MIS_DIAMETER) + parameterNode.SetParameter("BrowsePointIndex", "0") + parameterNode.SetParameter("Initialized", "1") def resetCrossSections(self): self.crossSectionPolyDataCache = {} @@ -937,7 +970,7 @@ def setShowCrossSection(self, checked): def setPlotSeriesType(self, type): self.outputPlotSeriesType = type if self.outputPlotSeriesNode and self.outputTableNode and self.inputCenterlineNode: - self.updatePlot(self.outputPlotSeriesNode, self.outputTableNode, self.inputCenterlineNode.GetName()) + self.updatePlot(self.outputPlotSeriesNode, self.outputTableNode) if self.isPlotVisible(): self.showPlot() @@ -1051,7 +1084,7 @@ def run(self): self.emptyOutputTableNode() self.updateOutputTable(self.inputCenterlineNode, self.outputTableNode) if self.outputPlotSeriesNode: - self.updatePlot(self.outputPlotSeriesNode, self.outputTableNode, self.inputCenterlineNode.GetName()) + self.updatePlot(self.outputPlotSeriesNode, self.outputTableNode) logging.info(_("Processing completed")) def emptyOutputTableNode(self): @@ -1171,7 +1204,6 @@ def updateOutputTable(self, inputCenterline, outputTable): """ Fill in cross-section areas in C++ threads. - N.B. polydata caching is not concerned here. """ import vtkSlicerCrossSectionAnalysisModuleLogicPython as vtkSlicerCrossSectionAnalysisModuleLogic crossSectionCompute = vtkSlicerCrossSectionAnalysisModuleLogic.vtkCrossSectionCompute() @@ -1197,7 +1229,7 @@ def updateOutputTable(self, inputCenterline, outputTable): """ We may also use the TubeRadius scalar array of the spline. This may prevent weird measurements at both ends of the tube. Not tested. - We select to slice the wall so as to use the same method of slicing the lumen. + We elect to slice the wall so as to use the same method of slicing the lumen. Good ? Bad ? """ if inputCenterline.IsTypeOf("vtkMRMLMarkupsShapeNode"): @@ -1263,11 +1295,9 @@ def updateOutputTable(self, inputCenterline, outputTable): logging.info(message) slicer.util.showStatusMessage(message, 5000) - def updatePlot(self, outputPlotSeries, outputTable, name=None): + def updatePlot(self, outputPlotSeries, outputTable): # Create plot - if name: - outputPlotSeries.SetName(name) outputPlotSeries.SetAndObserveTableNodeID(outputTable.GetID()) outputPlotSeries.SetXColumnName(DISTANCE_ARRAY_NAME) if self.outputPlotSeriesType == MIS_DIAMETER: @@ -1315,11 +1345,6 @@ def isTableVisible(self): return False def showPlot(self): - # Create chart - if not self.plotChartNode: - plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode") - self.plotChartNode = plotChartNode - lengthUnit = self.getUnitNodeUnitDisplayString(0.0, "length") areaUnit = self.getUnitNodeUnitDisplayString(0.0, "area") self.plotChartNode.SetXAxisTitle("{nameOfDistanceArray} ( {unitOfLength})".format(nameOfDistanceArray=DISTANCE_ARRAY_NAME, unitOfLength=lengthUnit)) @@ -1339,9 +1364,9 @@ def showPlot(self): self.plotChartNode.SetYAxisTitle(_("Stenosis (%)")) else: pass - # Make sure the plot is in the chart - if not self.plotChartNode.HasPlotSeriesNodeID(self.outputPlotSeriesNode.GetID()): - self.plotChartNode.AddAndObservePlotSeriesNodeID(self.outputPlotSeriesNode.GetID()) + # Reset the chart. + self.plotChartNode.RemoveAllPlotSeriesNodeIDs() + self.plotChartNode.AddAndObservePlotSeriesNodeID(self.outputPlotSeriesNode.GetID()) # Show plot in layout slicer.modules.plots.logic().ShowChartInLayout(self.plotChartNode) slicer.app.layoutManager().plotWidget(0).plotView().fitToContent() @@ -1659,54 +1684,6 @@ def getPositionMaximumInscribedSphereRadius(self, pointIndex): radius = radiusA * (controlPointFloatIndex - controlPointIndexA) + radiusB * (controlPointIndexB - controlPointFloatIndex) return position, radius - def updateAllCrossSectionsModel(self): - # TODO: make this method create a merged model of all cross-sections in the cache - pass - - # # precompute all lumen surfaces - # if self.logic.lumenSurfaceNode: - # for pointIndex in range(numberOfPoints): - # self.logic.getCrossSectionArea(pointIndex) - - # if self.allCrossSectionsModelNode is not None: - # self.allCrossSectionsModelNode.GetDisplayNode().SetVisibility(self.showCrossSection) - # # Don't append again if already done at a centerline point - # for i in range(self.crossSectionsPointIndices.GetNumberOfValues()): - # if self.crossSectionsPointIndices.GetValue(i) == pointIndex: - # return - - # # Work on a copy of the input polydata for the stack model - # islandPolyDataCopy = vtk.vtkPolyData() - # islandPolyDataCopy.DeepCopy(islandPolyData) - # # Set same scalar value to each point of polydata. - # intArray = vtk.vtkIntArray() - # intArray.SetName("PointIndex") - # for i in range(islandPolyDataCopy.GetNumberOfPoints()): - # intArray.InsertNextValue(int(pointIndex)) - # islandPolyDataCopy.GetPointData().SetScalars(intArray) - # islandPolyDataCopy.Modified() - - # self.appendedPolyData.AddInputData(islandPolyDataCopy) - # self.appendedPolyData.Update() - - # # Remember where it's already done - # self.crossSectionsPointIndices.InsertNextValue(int(pointIndex)) - - # # Remove stack model and observation - # if self.allCrossSectionsModelNode is not None: - # # Don't react if we remove it from scene on our own - # slicer.mrmlScene.RemoveObserver(self.sceneNodeRemovedObservation) - # slicer.mrmlScene.RemoveNode(self.allCrossSectionsModelNode) - # # Create a new stack model - # self.crossSectionsModelNode = slicer.modules.models.logic().AddModel(self.appendedPolyData.GetOutputPort()) - # separator = " for " if surfaceName else "" - # self.crossSectionsModelNode.SetName("Cross-section stack" + separator + surfaceName) - # self.crossSectionsModelNode.GetDisplayNode().SetVisibility(self.showCrossSection) - # # Remember stack model by id - # self.crossSectionsModelNodeId = self.crossSectionsModelNode.GetID() - # # Add an observation if stack model is deleted by the user - # self.sceneNodeRemovedObservation = slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeRemovedEvent, self.onSceneNodeRemoved) - def updateMaximumInscribedSphereModel(self, value): if self.inputCenterlineNode.IsTypeOf("vtkMRMLMarkupsShapeNode"): # 'Centerline' is an invisible spline of a Tube, not a lumen centerline. @@ -1776,7 +1753,6 @@ def calculateRelativeDistance(self, pointIndex): distanceFromStart = distanceArray.GetValue(int(pointIndex)) return distanceFromStart - relativeOriginDistance - # # CrossSectionAnalysisTest # @@ -1817,3 +1793,4 @@ def test_CrossSectionAnalysis1(self): WALL_CROSS_SECTION_AREA = "WALL_CROSS_SECTION_AREA" DIAMETER_STENOSIS = "DIAMETER_STENOSIS" SURFACE_AREA_STENOSIS = "SURFACE_AREA_STENOSIS" + diff --git a/CrossSectionAnalysis/Resources/UI/CrossSectionAnalysis.ui b/CrossSectionAnalysis/Resources/UI/CrossSectionAnalysis.ui index 2d2fcdb..f6015a6 100644 --- a/CrossSectionAnalysis/Resources/UI/CrossSectionAnalysis.ui +++ b/CrossSectionAnalysis/Resources/UI/CrossSectionAnalysis.ui @@ -20,6 +20,49 @@ + + + + + + Parameter set: + + + + + + + false + + + Pick a node to store the parameter set. + +This is intended to represent a unique combination of input centerline and surface nodes. Create a distinct parameter set for each combination. + + + + vtkMRMLScriptedModuleNode + + + + true + + + + + + CrossSectionAnalysis + + + true + + + + + + + + @@ -1426,5 +1469,21 @@ Caution: values at bifurcations may not have clinical meaning. + + CrossSectionAnalysis + mrmlSceneChanged(vtkMRMLScene*) + parameterSetSelector + setMRMLScene(vtkMRMLScene*) + + + 265 + 466 + + + 310 + 22 + + + diff --git a/Docs/CrossSectionAnalysis.md b/Docs/CrossSectionAnalysis.md index 80b7476..dc33210 100644 --- a/Docs/CrossSectionAnalysis.md +++ b/Docs/CrossSectionAnalysis.md @@ -52,6 +52,7 @@ A graphical plot of MIS diameter, CE diameter or cross-section area against dist ## Notes +- The parameter set node is intended for distinct combinations of centerlines and surfaces to isolate a study. - Point coordinates can be displayed as an array in a single column, or split in three distinct columns. One can choose between RAS or LPS coordinates. - A markups curve may also lie outside a surface. - Providing a surface (segmentation or model) is optional. For example, a markups curve may be drawn on a vascular structure's boundary in slice views, to see the corresponding cross-sections only. diff --git a/Docs/CrossSectionAnalysisScreenshot_1.png b/Docs/CrossSectionAnalysisScreenshot_1.png index bda304e..0aee450 100644 Binary files a/Docs/CrossSectionAnalysisScreenshot_1.png and b/Docs/CrossSectionAnalysisScreenshot_1.png differ