Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
3D Slicer scripted module for measuring angle between rulers
#
# 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)
@shrimanti
Copy link

shrimanti commented Nov 2, 2017

Hello Prof,
In which directory should I put this angle measurement file so that I can incorporate this in Slicer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment