Slicer 4.2
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
DataProbe.py
Go to the documentation of this file.
00001 import os
00002 import unittest
00003 import qt, vtk
00004 from __main__ import slicer
00005 
00006 #
00007 # DataProbe
00008 #
00009 
00010 class DataProbe:
00011   def __init__(self, parent):
00012     import string
00013     parent.title = "DataProbe"
00014     parent.categories = ["Quantification"]
00015     parent.contributors = ["Steve Pieper (Isomics)"]
00016     parent.helpText = string.Template("""
00017 The DataProbe module is used to get information about the current RAS position being indicated by the mouse position.  See <a href=\"$a/Documentation/$b.$c/Modules/DataProbe\">$a/Documentation/$b.$c/Modules/DataProbe</a> for more information.
00018     """).substitute({ 'a':parent.slicerWikiUrl, 'b':slicer.app.majorVersion, 'c':slicer.app.minorVersion })
00019     parent.acknowledgementText = """
00020 This work is supported by NA-MIC, NAC, NCIGT, and the Slicer Community. See <a>http://www.slicer.org</a> for details.  Module implemented by Steve Pieper.
00021     """
00022     # TODO: need a DataProbe icon
00023     #parent.icon = qt.QIcon(':Icons/XLarge/SlicerDownloadMRHead.png')
00024     self.parent = parent
00025     self.infoWidget = 0
00026 
00027     if slicer.mrmlScene.GetTagByClassName( "vtkMRMLScriptedModuleNode" ) != 'ScriptedModule':
00028       slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
00029 
00030     # Trigger the menu to be added when application has started up
00031     if not slicer.app.commandOptions().noMainWindow :
00032       qt.QTimer.singleShot(0, self.addView);
00033 
00034     # Add this test to the SelfTest module's list for discovery when the module
00035     # is created.  Since this module may be discovered before SelfTests itself,
00036     # create the list if it doesn't already exist.
00037     try:
00038       slicer.selfTests
00039     except AttributeError:
00040       slicer.selfTests = {}
00041     slicer.selfTests['DataProbe'] = self.runTest
00042 
00043   def runTest(self):
00044     tester = DataProbeTest()
00045     tester.runTest()
00046 
00047   def __del__(self):
00048     if self.infoWidget:
00049       self.infoWidget.removeObservers()
00050 
00051   def addView(self):
00052     """
00053     Create the persistent widget shown in the bottom left of the user interface
00054     Do this in a singleShot callback so the rest of the interface is already
00055     built.
00056     """
00057     # TODO - the parent name will likely change
00058     try:
00059       parent = slicer.util.findChildren(text='Data Probe')[0]
00060     except IndexError:
00061       print("No Data Probe frame - cannot create DataProbe")
00062       return
00063     self.infoWidget = DataProbeInfoWidget(parent,type='small')
00064     parent.layout().insertWidget(0,self.infoWidget.frame)
00065 
00066 class DataProbeInfoWidget(object):
00067 
00068   def __init__(self, parent=None,type='small'):
00069     self.type = type
00070     self.nameSize = 24
00071 
00072     # Default observer priority is 0.0, and the widgets have a 0.5 priority
00073     # so we set this to 1 in order to get events that would
00074     # otherwise be swallowed.  Since we do not abort the event, this is harmless.
00075     self.priority = 2
00076 
00077     # keep list of pairs: [observee,tag] so they can be removed easily
00078     self.styleObserverTags = []
00079     # keep a map of interactor styles to sliceWidgets so we can easily get sliceLogic
00080     self.sliceWidgetsPerStyle = {}
00081     self.refreshObservers()
00082 
00083     layoutManager = slicer.app.layoutManager()
00084     layoutManager.connect('layoutChanged(int)', self.refreshObservers)
00085 
00086     self.frame = qt.QFrame(parent)
00087     self.frame.setLayout(qt.QVBoxLayout())
00088     if type == 'small':
00089       self.createSmall()
00090 
00091     #Helper class to calculate and display tensor scalars
00092     self.calculateTensorScalars = CalculateTensorScalars()
00093 
00094 
00095   def __del__(self):
00096     self.removeObservers()
00097 
00098   def fitName(self,name):
00099     if len(name) > self.nameSize:
00100       preSize = self.nameSize / 2
00101       postSize = preSize - 3
00102       name = name[:preSize] + "..." + name[-postSize:]
00103     return name
00104 
00105 
00106   def removeObservers(self):
00107     # remove observers and reset
00108     for observee,tag in self.styleObserverTags:
00109       observee.RemoveObserver(tag)
00110     self.styleObserverTags = []
00111     self.sliceWidgetsPerStyle = {}
00112 
00113   def refreshObservers(self):
00114     """ When the layout changes, drop the observers from
00115     all the old widgets and create new observers for the
00116     newly created widgets"""
00117     self.removeObservers()
00118     # get new slice nodes
00119     layoutManager = slicer.app.layoutManager()
00120     sliceNodeCount = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode')
00121     for nodeIndex in xrange(sliceNodeCount):
00122       # find the widget for each node in scene
00123       sliceNode = slicer.mrmlScene.GetNthNodeByClass(nodeIndex, 'vtkMRMLSliceNode')
00124       sliceWidget = layoutManager.sliceWidget(sliceNode.GetLayoutName())
00125       if sliceWidget:
00126         # add obserservers and keep track of tags
00127         style = sliceWidget.sliceView().interactor()
00128         self.sliceWidgetsPerStyle[style] = sliceWidget
00129         events = ("MouseMoveEvent", "EnterEvent", "LeaveEvent")
00130         for event in events:
00131           tag = style.AddObserver(event, self.processEvent, self.priority)
00132           self.styleObserverTags.append([style,tag])
00133       # TODO: also observe the slice nodes
00134 
00135   def getPixelString(self,volumeNode,ijk):
00136     """Given a volume node, create a human readable
00137     string describing the contents"""
00138     # TODO: the volume nodes should have a way to generate
00139     # these strings in a generic way
00140     if not volumeNode:
00141       return "No volume"
00142     imageData = volumeNode.GetImageData()
00143     if not imageData:
00144       return "No Image"
00145     dims = imageData.GetDimensions()
00146     for ele in xrange(3):
00147       if ijk[ele] < 0 or ijk[ele] >= dims[ele]:
00148         return "Out of Frame"
00149     pixel = ""
00150     if volumeNode.GetLabelMap():
00151       labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0))
00152       labelValue = "Unknown"
00153       colorNode = volumeNode.GetDisplayNode().GetColorNode()
00154       if colorNode:
00155         labelValue = colorNode.GetColorName(labelIndex)
00156       return "%s (%d)" % (labelValue, labelIndex)
00157 
00158     if volumeNode.IsA("vtkMRMLDiffusionTensorVolumeNode"):
00159         point_idx = imageData.FindPoint(ijk[0], ijk[1], ijk[2])
00160         if point_idx == -1:
00161             return "Out of bounds"
00162 
00163         if not imageData.GetPointData():
00164             return "No Point Data"
00165 
00166         tensors = imageData.GetPointData().GetTensors()
00167         if not tensors:
00168             return "No Tensor Data"
00169 
00170         tensor = imageData.GetPointData().GetTensors().GetTuple9(point_idx)
00171         scalarVolumeDisplayNode = volumeNode.GetScalarVolumeDisplayNode()
00172 
00173         if scalarVolumeDisplayNode:
00174             operation = scalarVolumeDisplayNode.GetScalarInvariant()
00175         else:
00176             operation = None
00177 
00178         value = self.calculateTensorScalars(tensor, operation=operation)
00179         if value is not None:
00180             valueString = ("%f" % value).rstrip('0').rstrip('.')
00181             return "%s %s"%(scalarVolumeDisplayNode.GetScalarInvariantAsString(), valueString)
00182         else:
00183             return scalarVolumeDisplayNode.GetScalarInvariantAsString()
00184 
00185     # default - non label scalar volume
00186     numberOfComponents = imageData.GetNumberOfScalarComponents()
00187     if numberOfComponents > 3:
00188       return "%d components" % numberOfComponents
00189     for c in xrange(numberOfComponents):
00190       component = imageData.GetScalarComponentAsDouble(ijk[0],ijk[1],ijk[2],c)
00191       if component.is_integer():
00192         component = int(component)
00193       # format string according to suggestion here:
00194       # http://stackoverflow.com/questions/2440692/formatting-floats-in-python-without-superfluous-zeros
00195       componentString = ("%f" % component).rstrip('0').rstrip('.')
00196       pixel += ("%s, " % componentString)
00197     return pixel[:-2]
00198 
00199 
00200   def processEvent(self,observee,event):
00201     # TODO: use a timer to delay calculation and compress events
00202     if event == 'LeaveEvent':
00203       # reset all the readouts
00204       self.viewerColor.setText( "" )
00205       self.viewerName.setText( "" )
00206       self.viewerRAS.setText( "" )
00207       self.viewerOrient.setText( "" )
00208       self.viewerSpacing.setText( "" )
00209       layers = ('L', 'F', 'B')
00210       for layer in layers:
00211         self.layerNames[layer].setText( "" )
00212         self.layerIJKs[layer].setText( "" )
00213         self.layerValues[layer].setText( "" )
00214       return
00215     if self.sliceWidgetsPerStyle.has_key(observee):
00216       sliceWidget = self.sliceWidgetsPerStyle[observee]
00217       sliceLogic = sliceWidget.sliceLogic()
00218       sliceNode = sliceWidget.mrmlSliceNode()
00219       interactor = observee
00220       xy = interactor.GetEventPosition()
00221       xyz = sliceWidget.sliceView().convertDeviceToXYZ(xy);
00222       # populate the widgets
00223       self.viewerColor.setText( " " )
00224       rgbColor = sliceNode.GetLayoutColor();
00225       color = qt.QColor.fromRgbF(rgbColor[0], rgbColor[1], rgbColor[2])
00226       if hasattr(color, 'name'):
00227         self.viewerColor.setStyleSheet('QLabel {background-color : %s}' % color.name())
00228       self.viewerName.setText( "  " + sliceNode.GetLayoutName() + "  " )
00229       # TODO: get z value from lightbox
00230       ras = sliceWidget.sliceView().convertXYZToRAS(xyz)
00231       self.viewerRAS.setText( "RAS: (%.1f, %.1f, %.1f)" % ras )
00232       self.viewerOrient.setText( "  " + sliceWidget.sliceOrientation )
00233       self.viewerSpacing.setText( "%.1f" % sliceLogic.GetLowestVolumeSliceSpacing()[2] )
00234       if sliceNode.GetSliceSpacingMode() == 1:
00235         self.viewerSpacing.setText( "(" + self.viewerSpacing.text + ")" )
00236       self.viewerSpacing.setText( " Sp: " + self.viewerSpacing.text )
00237       layerLogicCalls = (('L', sliceLogic.GetLabelLayer),
00238                          ('F', sliceLogic.GetForegroundLayer),
00239                          ('B', sliceLogic.GetBackgroundLayer))
00240       for layer,logicCall in layerLogicCalls:
00241         layerLogic = logicCall()
00242         volumeNode = layerLogic.GetVolumeNode()
00243         nameLabel = "None"
00244         ijkLabel = ""
00245         valueLabel = ""
00246         if volumeNode:
00247           nameLabel = self.fitName(volumeNode.GetName())
00248           xyToIJK = layerLogic.GetXYToIJKTransform().GetMatrix()
00249           ijkFloat = xyToIJK.MultiplyPoint(xyz+(1,))[:3]
00250           ijk = []
00251           for element in ijkFloat:
00252             try:
00253               index = int(round(element))
00254             except ValueError:
00255               index = 0
00256             ijk.append(index)
00257             ijkLabel += "%d, " % index
00258           ijkLabel = ijkLabel[:-2]
00259           valueLabel = self.getPixelString(volumeNode,ijk)
00260         self.layerNames[layer].setText( '<b>' + nameLabel )
00261         self.layerIJKs[layer].setText( '(' + ijkLabel + ')' )
00262         self.layerValues[layer].setText( '<b>' + valueLabel )
00263 
00264   def createSmall(self):
00265     """Make the internals of the widget to display in the
00266     Data Probe frame (lower left of slicer main window by default)"""
00267     # top row - things about the viewer itself
00268     self.viewerFrame = qt.QFrame(self.frame)
00269     self.viewerFrame.setLayout(qt.QHBoxLayout())
00270     self.frame.layout().addWidget(self.viewerFrame)
00271     self.viewerColor = qt.QLabel(self.viewerFrame)
00272     self.viewerFrame.layout().addWidget(self.viewerColor)
00273     self.viewerName = qt.QLabel(self.viewerFrame)
00274     self.viewerFrame.layout().addWidget(self.viewerName)
00275     self.viewerRAS = qt.QLabel()
00276     self.viewerFrame.layout().addWidget(self.viewerRAS)
00277     self.viewerOrient = qt.QLabel()
00278     self.viewerFrame.layout().addWidget(self.viewerOrient)
00279     self.viewerSpacing = qt.QLabel()
00280     self.viewerFrame.layout().addWidget(self.viewerSpacing)
00281     self.viewerFrame.layout().addStretch(1)
00282 
00283     # the grid - things about the layers
00284     # this method makes labels
00285     self.layerGrid = qt.QFrame(self.frame)
00286     self.layerGrid.setLayout(qt.QGridLayout())
00287     self.frame.layout().addWidget(self.layerGrid)
00288     layers = ('L', 'F', 'B')
00289     self.layerNames = {}
00290     self.layerIJKs = {}
00291     self.layerValues = {}
00292     row = 0
00293     for layer in layers:
00294       col = 0
00295       self.layerGrid.layout().addWidget(qt.QLabel(layer), row, col)
00296       col += 1
00297       self.layerNames[layer] = qt.QLabel()
00298       self.layerGrid.layout().addWidget(self.layerNames[layer], row, col)
00299       col += 1
00300       self.layerIJKs[layer] = qt.QLabel()
00301       self.layerGrid.layout().addWidget(self.layerIJKs[layer], row, col)
00302       col += 1
00303       self.layerValues[layer] = qt.QLabel()
00304       self.layerGrid.layout().addWidget(self.layerValues[layer], row, col)
00305       self.layerGrid.layout().setColumnStretch(col,100)
00306       col += 1
00307       row += 1
00308 
00309     # goto module button
00310     self.goToModule = qt.QPushButton('->', self.frame)
00311     self.goToModule.setToolTip('Go to the DataProbe module for more information and options')
00312     self.frame.layout().addWidget(self.goToModule)
00313     self.goToModule.connect("clicked()", self.onGoToModule)
00314     # hide this for now - there's not much to see in the module itself
00315     self.goToModule.hide()
00316 
00317   def onGoToModule(self):
00318     m = slicer.util.mainWindow()
00319     m.moduleSelector().selectModule('DataProbe')
00320 
00321 #
00322 # DataProbe widget
00323 #
00324 
00325 class DataProbeWidget:
00326   """This builds the module contents - nothing here"""
00327   # TODO: this could have a more in-depth set of information
00328   # about the volumes and layers in the slice views
00329   # and possibly other view types as well
00330   # TODO: Since this is empty for now, it should be hidden
00331   # from the Modules menu.
00332 
00333   def __init__(self, parent=None):
00334     self.observerTags = []
00335 
00336     if not parent:
00337       self.parent = slicer.qMRMLWidget()
00338       self.parent.setLayout(qt.QVBoxLayout())
00339       self.parent.setMRMLScene(slicer.mrmlScene)
00340       self.layout = self.parent.layout()
00341       self.setup()
00342       self.parent.show()
00343     else:
00344       self.parent = parent
00345       self.layout = parent.layout()
00346 
00347   def enter(self):
00348     pass
00349 
00350   def exit(self):
00351     pass
00352 
00353   def updateGUIFromMRML(self, caller, event):
00354     pass
00355 
00356   def setup(self):
00357 
00358     # reload button
00359     # (use this during development, but remove it when delivering
00360     #  your module to users)
00361     self.reloadButton = qt.QPushButton("Reload")
00362     self.reloadButton.toolTip = "Reload this module."
00363     self.reloadButton.name = "DataProbe Reload"
00364     self.layout.addWidget(self.reloadButton)
00365     self.reloadButton.connect('clicked()', self.onReload)
00366 
00367     # reload and test button
00368     # (use this during development, but remove it when delivering
00369     #  your module to users)
00370     self.reloadAndTestButton = qt.QPushButton("Reload and Test")
00371     self.reloadAndTestButton.toolTip = "Reload this module and then run the self tests."
00372     self.layout.addWidget(self.reloadAndTestButton)
00373     self.reloadAndTestButton.connect('clicked()', self.onReloadAndTest)
00374 
00375     self.parent.layout().addStretch(1)
00376 
00377   def onReload(self,moduleName="DataProbe"):
00378     """Generic reload method for any scripted module.
00379     ModuleWizard will subsitute correct default moduleName.
00380     """
00381     import imp, sys, os, slicer
00382 
00383     widgetName = moduleName + "Widget"
00384 
00385     # reload the source code
00386     # - set source file path
00387     # - load the module to the global space
00388     filePath = eval('slicer.modules.%s.path' % moduleName.lower())
00389     p = os.path.dirname(filePath)
00390     if not sys.path.__contains__(p):
00391       sys.path.insert(0,p)
00392     fp = open(filePath, "r")
00393     globals()[moduleName] = imp.load_module(
00394         moduleName, fp, filePath, ('.py', 'r', imp.PY_SOURCE))
00395     fp.close()
00396 
00397     # rebuild the widget
00398     # - find and hide the existing widget
00399     # - create a new widget in the existing parent
00400     parent = slicer.util.findChildren(name='%s Reload' % moduleName)[0].parent()
00401     for child in parent.children():
00402       try:
00403         child.hide()
00404       except AttributeError:
00405         pass
00406     # Remove spacer items
00407     item = parent.layout().itemAt(0)
00408     while item:
00409       parent.layout().removeItem(item)
00410       item = parent.layout().itemAt(0)
00411     # create new widget inside existing parent
00412     globals()[widgetName.lower()] = eval(
00413         'globals()["%s"].%s(parent)' % (moduleName, widgetName))
00414     globals()[widgetName.lower()].setup()
00415 
00416   def onReloadAndTest(self,moduleName="DataProbe"):
00417     self.onReload()
00418     evalString = 'globals()["%s"].%sTest()' % (moduleName, moduleName)
00419     tester = eval(evalString)
00420     tester.runTest()
00421 
00422 
00423 class CalculateTensorScalars:
00424     def __init__(self):
00425         self.dti_math = slicer.vtkDiffusionTensorMathematics()
00426 
00427         self.single_pixel_image = vtk.vtkImageData()
00428         self.single_pixel_image.SetExtent(0, 0, 0, 0, 0, 0)
00429         self.single_pixel_image.AllocateScalars()
00430 
00431         self.tensor_data = vtk.vtkFloatArray()
00432         self.tensor_data.SetNumberOfComponents(9)
00433         self.tensor_data.SetNumberOfTuples(self.single_pixel_image.GetNumberOfPoints())
00434         self.single_pixel_image.GetPointData().SetTensors(self.tensor_data)
00435 
00436         self.dti_math.SetInput(self.single_pixel_image)
00437 
00438     def __call__(self, tensor, operation=None):
00439         if len(tensor) != 9:
00440             raise ValueError("Invalid tensor a 9-array is required")
00441 
00442         self.tensor_data.SetTupleValue(0, tensor)
00443         self.tensor_data.Modified()
00444         self.single_pixel_image.Modified()
00445 
00446         if operation is not None:
00447             self.dti_math.SetOperation(operation)
00448         else:
00449             self.dti_math.SetOperationToFractionalAnisotropy()
00450 
00451         self.dti_math.Update()
00452         output = self.dti_math.GetOutput()
00453 
00454         if output and output.GetNumberOfScalarComponents() > 0:
00455             value = output.GetScalarComponentAsDouble(0, 0, 0, 0)
00456             return value
00457         else:
00458             return None
00459 
00460 
00461 #
00462 # DataProbeLogic
00463 #
00464 
00465 class DataProbeLogic:
00466   """This class should implement all the actual 
00467   computation done by your module.  The interface 
00468   should be such that other python code can import
00469   this class and make use of the functionality without
00470   requiring an instance of the Widget
00471   """
00472   def __init__(self):
00473     pass
00474 
00475   def hasImageData(self,volumeNode):
00476     """This is a dummy logic method that 
00477     returns true if the passed in volume
00478     node has valid image data
00479     """
00480     if not volumeNode:
00481       print('no volume node')
00482       return False
00483     if volumeNode.GetImageData() == None:
00484       print('no image data')
00485       return False
00486     return True
00487 
00488 
00489 class DataProbeTest(unittest.TestCase):
00490   """
00491   This is the test case for your scripted module.
00492   """
00493 
00494   def delayDisplay(self,message,msec=1000):
00495     """This utility method displays a small dialog and waits.
00496     This does two things: 1) it lets the event loop catch up
00497     to the state of the test so that rendering and widget updates
00498     have all taken place before the test continues and 2) it
00499     shows the user/developer/tester the state of the test
00500     so that we'll know when it breaks.
00501     """
00502     print(message)
00503     self.info = qt.QDialog()
00504     self.infoLayout = qt.QVBoxLayout()
00505     self.info.setLayout(self.infoLayout)
00506     self.label = qt.QLabel(message,self.info)
00507     self.infoLayout.addWidget(self.label)
00508     qt.QTimer.singleShot(msec, self.info.close)
00509     self.info.exec_()
00510 
00511   def setUp(self):
00512     """ Do whatever is needed to reset the state - typically a scene clear will be enough.
00513     """
00514     pass
00515 
00516   def runTest(self):
00517     """Run as few or as many tests as needed here.
00518     """
00519     self.setUp()
00520     self.test_DataProbe1()
00521 
00522   def test_DataProbe1(self):
00523     """ Ideally you should have several levels of tests.  At the lowest level
00524     tests sould exercise the functionality of the logic with different inputs
00525     (both valid and invalid).  At higher levels your tests should emulate the
00526     way the user would interact with your code and confirm that it still works
00527     the way you intended.
00528     One of the most important features of the tests is that it should alert other
00529     developers when their changes will have an impact on the behavior of your
00530     module.  For example, if a developer removes a feature that you depend on,
00531     your test should break so they know that the feature is needed.
00532     """
00533 
00534     self.delayDisplay("Starting the test")
00535     #
00536     # first, get some data
00537     #
00538     if not slicer.util.getNode('FA'):
00539       import urllib
00540       downloads = (
00541           ('http://slicer.kitware.com/midas3/download?items=5767', 'FA.nrrd', slicer.util.loadVolume),
00542           )
00543 
00544       for url,name,loader in downloads:
00545         filePath = slicer.app.temporaryPath + '/' + name
00546         if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
00547           print('Requesting download %s from %s...\n' % (name, url))
00548           urllib.urlretrieve(url, filePath)
00549         if loader:
00550           print('Loading %s...\n' % (name,))
00551           loader(filePath)
00552     self.delayDisplay('Finished with download and loading\n')
00553 
00554     self.widget = DataProbeInfoWidget()
00555     self.widget.frame.show()
00556 
00557     self.delayDisplay('Test passed!')
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines