|
Slicer 4.2
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
|
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!')
1.7.4