Last active
June 22, 2019 20:17
-
-
Save lassoan/9bf334743871e400f7e3b3745b312b14 to your computer and use it in GitHub Desktop.
3D Slicer scripted module for measuring angle between rulers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Installation: | |
# - Save this file as AngleMeasurement.py to a directory on your computer | |
# - Add the directory to the additional module paths in the Slicer application settings: | |
# - Choose in the menu: Edit / Application settings | |
# - Click Modules, click >> next to Additional module paths | |
# - Click Add, and choose the .py file's location | |
# - After you restart Slicer, "Angle Measurment" module should show up in Quantification category | |
# | |
import os | |
import unittest | |
import vtk, qt, ctk, slicer | |
from slicer.ScriptedLoadableModule import * | |
from slicer.util import VTKObservationMixin | |
import logging | |
# | |
# AngleMeasurement | |
# | |
class AngleMeasurement(ScriptedLoadableModule): | |
"""Uses ScriptedLoadableModule base class, available at: | |
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py | |
""" | |
def __init__(self, parent): | |
ScriptedLoadableModule.__init__(self, parent) | |
self.parent.title = "Angle Measurement" | |
self.parent.categories = ["Quantification"] | |
self.parent.dependencies = [] | |
self.parent.contributors = ["Andras Lasso (PerkLab)"] | |
self.parent.helpText = """ | |
Measure angles between rulers | |
""" | |
self.parent.acknowledgementText = """ | |
""" # replace with organization, grant and thanks. | |
# | |
# AngleMeasurementWidget | |
# | |
class AngleMeasurementWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): | |
"""Uses ScriptedLoadableModuleWidget base class, available at: | |
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py | |
""" | |
def __init__(self, parent): | |
ScriptedLoadableModuleWidget.__init__(self, parent) | |
VTKObservationMixin.__init__(self) | |
# Members | |
self.numberOfRulersInScene = 0 | |
self.ruler1 = None | |
self.ruler2 = None | |
self.angleDeg = None | |
self.rulerNodeClass = 'vtkMRMLAnnotationRulerNode' | |
def setup(self): | |
ScriptedLoadableModuleWidget.setup(self) | |
# Instantiate and connect widgets ... | |
# | |
# Parameters Area | |
# | |
parametersCollapsibleButton = ctk.ctkCollapsibleButton() | |
parametersCollapsibleButton.text = "Parameters" | |
self.layout.addWidget(parametersCollapsibleButton) | |
# Layout within the dummy collapsible button | |
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) | |
# | |
# input volume selector | |
# | |
self.resultPreview = qt.QLabel() | |
parametersFormLayout.addRow("Angle: ", self.resultPreview) | |
# | |
# Add Button | |
# | |
self.applyButton = qt.QPushButton("Add to table") | |
self.applyButton.setAutoFillBackground(True) | |
self.applyButton.setStyleSheet("background-color: rgb(150, 255, 150); color: rgb(0, 0, 0); height: 40px") | |
self.applyButton.enabled = False | |
parametersFormLayout.addRow(self.applyButton) | |
if not hasattr(slicer, 'angleMeasurementData'): | |
# Store table in an internal scene so that results table is preserved | |
# even when we close the scene | |
slicer.angleMeasurementData = {} | |
self.internalScene = slicer.vtkMRMLScene() | |
self.resultsTableNode = slicer.vtkMRMLTableNode() | |
self.resultsTableNode.SetName('Angle measurements') | |
self.resultsTableNode.SetUseColumnNameAsColumnHeader(True) | |
self.resultsTableNode.AddColumn().SetName('Image') | |
self.resultsTableNode.AddColumn().SetName('Angle') | |
self.resultsTableNode.AddColumn().SetName('Comment') | |
self.internalScene.AddNode(self.resultsTableNode) | |
slicer.angleMeasurementData["internalScene"] = self.internalScene | |
slicer.angleMeasurementData["resultsTableNode"] = self.resultsTableNode | |
else: | |
self.internalScene = slicer.angleMeasurementData["internalScene"] | |
self.resultsTableNode = slicer.angleMeasurementData["resultsTableNode"] | |
self.resultsTableView = slicer.qMRMLTableView() | |
self.resultsTableView.setMRMLScene(self.internalScene) | |
self.resultsTableView.setMRMLTableNode(self.resultsTableNode) | |
policy = qt.QSizePolicy() | |
policy.setVerticalStretch(1) | |
policy.setHorizontalPolicy(qt.QSizePolicy.Expanding) | |
policy.setVerticalPolicy(qt.QSizePolicy.Expanding) | |
self.resultsTableView.setSizePolicy(policy) | |
parametersFormLayout.addRow(self.resultsTableView) | |
# | |
# Clear button | |
# | |
self.clearRulersButton = qt.QPushButton("Clear rulers") | |
self.clearRulersButton.setAutoFillBackground(True) | |
self.clearRulersButton.setStyleSheet("background-color: rgb(255, 100, 100); color: rgb(0, 0, 0)") | |
parametersFormLayout.addRow(self.clearRulersButton) | |
self.clearLastMeasurementButton = qt.QPushButton("Remove last measurement") | |
self.clearLastMeasurementButton.setAutoFillBackground(True) | |
self.clearLastMeasurementButton.setStyleSheet("background-color: rgb(255, 100, 100); color: rgb(0, 0, 0)") | |
parametersFormLayout.addRow(self.clearLastMeasurementButton) | |
# connections | |
self.applyButton.connect('clicked(bool)', self.onAddToTableButton) | |
self.clearRulersButton.connect('clicked(bool)', self.onClearRulers) | |
self.clearLastMeasurementButton.connect('clicked(bool)', self.onClearLastMeasurement) | |
self.addObserver(slicer.mrmlScene, slicer.vtkMRMLScene.NodeAddedEvent, self.onSceneUpdated) | |
self.addObserver(slicer.mrmlScene, slicer.vtkMRMLScene.NodeRemovedEvent, self.onSceneUpdated) | |
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneUpdated) | |
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneUpdated) | |
self.onSceneUpdated() | |
self.onRulerChanged() | |
selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") | |
# call the set reference to make sure the event is invoked | |
selectionNode.SetReferenceActivePlaceNodeClassName(self.rulerNodeClass) | |
selectionNode.SetActivePlaceNodeID(None) | |
interactionNode = slicer.app.applicationLogic().GetInteractionNode() | |
interactionNode.SetPlaceModePersistence(True) | |
def cleanup(self): | |
self.removeObservers() | |
def onAddToTableButton(self): | |
rowIndex = self.resultsTableNode.AddEmptyRow() | |
volumeNode = slicer.util.getNode(self.ruler1.GetAttribute('AssociatedNodeID')) | |
self.resultsTableNode.SetCellText(rowIndex, 0, volumeNode.GetName() if volumeNode else "") | |
self.resultsTableNode.SetCellText(rowIndex, 1, "{:.1f}".format(self.angleDeg)) | |
self.onClearRulers() | |
def onSceneUpdated(self, caller = None, event = None): | |
if not self.parent.isEntered: | |
return | |
oldNumberOfRulersInScene = self.numberOfRulersInScene | |
oldRuler1 = self.ruler1 | |
oldRuler2 = self.ruler2 | |
newRuler1 = None | |
newRuler2 = None | |
self.numberOfRulersInScene = slicer.mrmlScene.GetNumberOfNodesByClass(self.rulerNodeClass) | |
if self.numberOfRulersInScene == 2: | |
newRuler1 = slicer.mrmlScene.GetNthNodeByClass(0, self.rulerNodeClass) | |
newRuler2 = slicer.mrmlScene.GetNthNodeByClass(1, self.rulerNodeClass) | |
if newRuler1 == oldRuler1 and newRuler2 == oldRuler2 and oldNumberOfRulersInScene == self.numberOfRulersInScene: | |
# no change | |
return | |
if self.numberOfRulersInScene >= 2: | |
interactionNode = slicer.app.applicationLogic().GetInteractionNode() | |
interactionNode.SetCurrentInteractionMode(interactionNode.ViewTransform) | |
self.ruler1 = newRuler1 | |
self.ruler2 = newRuler2 | |
self.removeObservers(self.onRulerChanged) | |
if self.ruler1 and self.ruler2: | |
self.addObserver(self.ruler1, vtk.vtkCommand.ModifiedEvent, self.onRulerChanged) | |
self.addObserver(self.ruler2, vtk.vtkCommand.ModifiedEvent, self.onRulerChanged) | |
self.onRulerChanged() | |
def onRulerChanged(self, caller = None, event = None): | |
if self.numberOfRulersInScene != 2: | |
if self.numberOfRulersInScene < 2: | |
self.resultPreview.text = "Not enough rulers" | |
else: | |
self.resultPreview.text = "There are more than two rulers" | |
self.angleDeg = None | |
self.applyButton.enabled = False | |
return | |
import numpy as np | |
import math | |
directionVectors = [] | |
for ruler in [self.ruler1, self.ruler2]: | |
p1=np.array([0,0,0]) | |
p2=np.array([0,0,0]) | |
ruler.GetControlPointWorldCoordinates(0,p1) | |
ruler.GetControlPointWorldCoordinates(1,p2) | |
directionVectors.append(p2-p1) | |
# Compute angle (0 <= angle <= 90) | |
cosang = np.dot(directionVectors[0], directionVectors[1]) | |
sinang = np.linalg.norm(np.cross(directionVectors[0], directionVectors[1])) | |
angleDeg = math.fabs(np.arctan2(sinang, cosang)*180.0/math.pi) | |
self.angleDeg = angleDeg | |
self.resultPreview.text = "{:.1f}".format(self.angleDeg) | |
self.applyButton.enabled = True | |
def onClearRulers(self): | |
rulers = slicer.util.getNodesByClass(self.rulerNodeClass) | |
for ruler in rulers: | |
slicer.mrmlScene.RemoveNode(ruler) | |
def onClearLastMeasurement(self): | |
numOfRows = self.resultsTableNode.GetNumberOfRows() | |
if numOfRows>0: | |
self.resultsTableNode.RemoveRow(numOfRows-1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello Prof,
In which directory should I put this angle measurement file so that I can incorporate this in Slicer?