cardiacSPECT.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import os
  2. import sys
  3. import unittest
  4. import vtk, qt, ctk, slicer
  5. from slicer.ScriptedLoadableModule import *
  6. import logging
  7. import parseDicom as pd
  8. import vtkInterface as vi
  9. import fileIO
  10. import slicer
  11. import numpy as np
  12. #
  13. # cardiacSPECT
  14. #
  15. class cardiacSPECT(ScriptedLoadableModule):
  16. """Uses ScriptedLoadableModule base class, available at:
  17. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  18. """
  19. def __init__(self, parent):
  20. ScriptedLoadableModule.__init__(self, parent)
  21. parent.title = "Cardiac SPECT"
  22. parent.categories = ["Examples"]
  23. parent.dependencies = []
  24. parent.contributors = ["Andrej Studen (FMF/JSI)"] # replace with "Firstname Lastname (Org)"
  25. parent.helpText = """
  26. Load dynamic cardiac SPECT data to Slicer
  27. """
  28. parent.acknowledgementText = """
  29. This module was developed within the frame of the ARRS sponsored medical
  30. physics research programe to investigate quantitative measurements of cardiac
  31. function using sestamibi-like tracers
  32. """ # replace with organization, grant and thanks.
  33. self.parent.helpText += self.getDefaultModuleDocumentationLink()
  34. self.parent = parent
  35. #
  36. # cardiacSPECTWidget
  37. #
  38. class cardiacSPECTWidget(ScriptedLoadableModuleWidget):
  39. """Uses ScriptedLoadableModuleWidget base class, available at:
  40. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  41. """
  42. def setup(self):
  43. ScriptedLoadableModuleWidget.setup(self)
  44. self.selectRemote=fileIO.remoteFileSelector()
  45. self.network=slicer.modules.labkeySlicerPythonExtensionWidget.network
  46. self.selectRemote.setMaster(self)
  47. # Instantiate and connect widgets ...
  48. dataButton = ctk.ctkCollapsibleButton()
  49. dataButton.text = "Data"
  50. self.layout.addWidget(dataButton)
  51. # Layout within the sample collapsible button
  52. dataFormLayout = qt.QFormLayout(dataButton)
  53. #pathGuess="file://"+os.environ['HOME']+"/SPECT"
  54. pathGuess="labkey://" + "dinamic_spect/%40files/Dinamika%20test2/SPECT_Dinamika_Rekonstruirano"
  55. self.dataPath=qt.QLineEdit(pathGuess)
  56. dataFormLayout.addRow("Data location",self.dataPath)
  57. self.remotePath=qt.QLineEdit();
  58. dataFormLayout.addRow('Remote Path', self.remotePath)
  59. self.remotePath.textChanged.connect(self.onRemotePathTextChanged)
  60. browseButton = qt.QPushButton("Browse local")
  61. browseButton.toolTip="Set file location"
  62. dataFormLayout.addRow("Select local",browseButton)
  63. browseButton.connect('clicked(bool)',self.onBrowseButtonClicked)
  64. browseRemoteButton = qt.QPushButton("Browse remote")
  65. browseRemoteButton.toolTip="Set remote location"
  66. dataFormLayout.addRow("Select remote",browseRemoteButton)
  67. browseRemoteButton.connect('clicked(bool)',self.onRemoteBrowseButtonClicked)
  68. dataLoadButton = qt.QPushButton("Load")
  69. dataLoadButton.toolTip="Load data from DICOM"
  70. dataFormLayout.addRow("Data",dataLoadButton)
  71. dataLoadButton.connect('clicked(bool)',self.onDataLoadButtonClicked)
  72. self.dataLoadButton = dataLoadButton
  73. # Add vertical spacer
  74. self.layout.addStretch(1)
  75. #addFrameButton=qt.QPushButton("Add Frame")
  76. #addFrameButton.toolTip="Add frame to VTK"
  77. #dataFormLayout.addWidget(addFrameButton)
  78. #addFrameButton.connect('clicked(bool)',self.onAddFrameButtonClicked)
  79. #addCTButton=qt.QPushButton("Add CT")
  80. #addCTButton.toolTip="Add CT to VTK"
  81. #dataFormLayout.addWidget(addCTButton)
  82. #addCTButton.connect('clicked(bool)',self.onAddCTButtonClicked)
  83. #
  84. # Parameters Area
  85. #
  86. parametersCollapsibleButton = ctk.ctkCollapsibleButton()
  87. parametersCollapsibleButton.text = "Parameters"
  88. self.layout.addWidget(parametersCollapsibleButton)
  89. # Layout within the dummy collapsible button
  90. parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
  91. #
  92. # check box to trigger taking screen shots for later use in tutorials
  93. #
  94. hbox1=qt.QHBoxLayout()
  95. frameLabel = qt.QLabel()
  96. frameLabel.setText("Select frame")
  97. hbox1.addWidget(frameLabel)
  98. self.time_frame_select=qt.QSlider(qt.Qt.Horizontal)
  99. self.time_frame_select.valueChanged.connect(self.onTimeFrameSelect)
  100. #self.time_frame_select.connect('valueChanged()', self.onTimeFrameSelect)
  101. self.time_frame_select.setMinimum(0)
  102. self.time_frame_select.setMaximum(0)
  103. self.time_frame_select.setValue(0)
  104. self.time_frame_select.setTickPosition(qt.QSlider.TicksBelow)
  105. self.time_frame_select.setTickInterval(5)
  106. self.time_frame_select.toolTip = "Select the time frame"
  107. hbox1.addWidget(self.time_frame_select)
  108. parametersFormLayout.addRow(hbox1)
  109. hbox2 = qt.QHBoxLayout()
  110. meanROILabel = qt.QLabel()
  111. meanROILabel.setText("MeanROI")
  112. hbox2.addWidget(meanROILabel)
  113. self.meanROIVolume = qt.QLineEdit()
  114. self.meanROIVolume.setText("testVolume15")
  115. hbox2.addWidget(self.meanROIVolume)
  116. self.meanROISegment = qt.QLineEdit()
  117. self.meanROISegment.setText("Segment_1")
  118. hbox2.addWidget(self.meanROISegment)
  119. computeMeanROI = qt.QPushButton("Compute mean ROI")
  120. computeMeanROI.connect('clicked(bool)',self.onComputeMeanROIClicked)
  121. hbox2.addWidget(computeMeanROI)
  122. self.meanROIResult = qt.QLineEdit()
  123. self.meanROIResult.setText("0")
  124. hbox2.addWidget(self.meanROIResult)
  125. parametersFormLayout.addRow(hbox2)
  126. #row 3
  127. hbox3 = qt.QHBoxLayout()
  128. drawTimePlot=qt.QPushButton("Draw ROI time plot")
  129. drawTimePlot.connect('clicked(bool)',self.onDrawTimePlotClicked)
  130. hbox3.addWidget(drawTimePlot)
  131. parametersFormLayout.addRow(hbox3)
  132. #dataFormLayout.addWidget(hbox)
  133. #row 4
  134. hbox4 = qt.QHBoxLayout()
  135. countSegments=qt.QPushButton("Count segmentation segments")
  136. countSegments.connect('clicked(bool)',self.onCountSegmentsClicked)
  137. hbox4.addWidget(countSegments)
  138. self.countSegmentsDisplay=qt.QLineEdit()
  139. self.countSegmentsDisplay.setText("0")
  140. hbox4.addWidget(self.countSegmentsDisplay)
  141. parametersFormLayout.addRow(hbox4)
  142. #
  143. # Apply Button
  144. #
  145. self.applyButton = qt.QPushButton("Apply")
  146. self.applyButton.toolTip = "Run the algorithm."
  147. self.applyButton.enabled = False
  148. parametersFormLayout.addRow(self.applyButton)
  149. # connections
  150. self.applyButton.connect('clicked(bool)', self.onApplyButton)
  151. #self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
  152. #self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
  153. # Add vertical spacer
  154. self.layout.addStretch(1)
  155. self.logic=cardiacSPECTLogic()
  156. self.resetPosition=1
  157. def cleanup(self):
  158. pass
  159. def onApplyButton(self):
  160. pass
  161. #logic = cardiacSPECTLogic()
  162. #imageThreshold = self.imageThresholdSliderWidget.value
  163. def onBrowseButtonClicked(self):
  164. startDir=self.dataPath.text
  165. inputDir=qt.QFileDialog.getExistingDirectory(None,
  166. 'Select DICOM directory',startDir)
  167. self.dataPath.setText("file://"+inputDir)
  168. def onRemoteBrowseButtonClicked(self):
  169. self.selectRemote.show()
  170. def onDataLoadButtonClicked(self):
  171. self.logic.loadData(self)
  172. def onRemotePathTextChanged(self,str):
  173. self.dataPath.setText('labkey://'+str)
  174. def onTimeFrameSelect(self):
  175. it=self.time_frame_select.value
  176. selectionNode = slicer.app.applicationLogic().GetSelectionNode()
  177. print("Propagating CT volume")
  178. node=slicer.mrmlScene.GetFirstNodeByName("testCT")
  179. selectionNode.SetReferenceActiveVolumeID(node.GetID())
  180. if self.resetPosition==1:
  181. self.resetPosition=0
  182. slicer.app.applicationLogic().PropagateVolumeSelection(1)
  183. else:
  184. slicer.app.applicationLogic().PropagateVolumeSelection(0)
  185. print("Propagating SPECT volume")
  186. nodeName='testVolume'+str(it)
  187. node=slicer.mrmlScene.GetFirstNodeByName(nodeName)
  188. selectionNode.SetSecondaryVolumeID(node.GetID())
  189. slicer.app.applicationLogic().PropagateForegroundVolumeSelection(0)
  190. node.GetDisplayNode().SetAndObserveColorNodeID('vtkMRMLColorTableNodeRed')
  191. lm = slicer.app.layoutManager()
  192. sID=['Red','Yellow','Green']
  193. for s in sID:
  194. sliceLogic = lm.sliceWidget(s).sliceLogic()
  195. compositeNode = sliceLogic.GetSliceCompositeNode()
  196. compositeNode.SetForegroundOpacity(0.5)
  197. #make sure the viewer is matched to the volume
  198. print("Done")
  199. #to access sliceLogic (slice control) use
  200. #lcol=slicer.app.layoutManager().mrmlSliceLogics() (vtkCollection)
  201. #vtkMRMLSliceLogic are named by colors (Red,Green,Blue)
  202. def onComputeMeanROIClicked(self):
  203. s=self.logic.meanROI(self.meanROIVolume.text,self.meanROISegment.text)
  204. self.meanROIResult.setText(str(s))
  205. def onDrawTimePlotClicked(self):
  206. n=self.time_frame_select.maximum
  207. ft=self.logic.frame_time
  208. #find number of segments
  209. ns = self.logic.countSegments()
  210. #add the chart node
  211. cn = slicer.mrmlScene.AddNode(slicer.vtkMRMLChartNode())
  212. for j in range(0,ns):
  213. segment="Segment_"+str(j+1)
  214. #add node for data
  215. dn = slicer.mrmlScene.AddNode(slicer.vtkMRMLDoubleArrayNode())
  216. a = dn.GetArray()
  217. a.SetNumberOfTuples(n)
  218. dt=0;
  219. t0=0;
  220. for i in range(0,n):
  221. vol="testVolume"+str(i)
  222. fx=ft[i]
  223. fy=self.logic.meanROI(vol,j)
  224. dt=2*ft[i]-t0
  225. t0+=dt
  226. a.SetComponent(i, 0, fx)
  227. a.SetComponent(i, 1, fy/dt)
  228. a.SetComponent(i, 2, 0)
  229. print("[{0} at {1:.2f}:{2:.2f}]".format(vol,fx,fy))
  230. cn.AddArray(segment, dn.GetID())
  231. cn.SetProperty('default', 'title', 'ROI time plot')
  232. cn.SetProperty('default', 'xAxisLabel', 'time [ms]')
  233. cn.SetProperty('default', 'yAxisLabel', 'Activity (arb)')
  234. #update the chart node
  235. cvns = slicer.mrmlScene.GetNodesByClass('vtkMRMLChartViewNode')
  236. cvns.InitTraversal()
  237. cvn = cvns.GetNextItemAsObject()
  238. cvn.SetChartNodeID(cn.GetID())
  239. def onCountSegmentsClicked(self):
  240. self.countSegmentsDisplay.setText(self.logic.countSegments())
  241. #def onAddFrameButtonClicked(self):
  242. # it=int(self.time_frame_select.text)
  243. # self.logic.addFrame(it)
  244. # def onAddCTButtonClicked(self):
  245. # self.logic.addCT()
  246. #
  247. #
  248. # cardiacSPECTLogic
  249. #
  250. class cardiacSPECTLogic(ScriptedLoadableModuleLogic):
  251. """This class should implement all the actual
  252. computation done by your module. The interface
  253. should be such that other python code can import
  254. this class and make use of the functionality without
  255. requiring an instance of the Widget.
  256. Uses ScriptedLoadableModuleLogic base class, available at:
  257. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  258. """
  259. def loadData(self,widget):
  260. inputDir=str(widget.dataPath.text)
  261. self.frame_data, self.frame_time, self.frame_origin, \
  262. self.frame_pixel_size, self.frame_orientation=pd.read_dynamic_SPECT(inputDir)
  263. self.ct_data,self.ct_origin,self.ct_pixel_size, \
  264. self.ct_orientation=pd.read_CT(inputDir)
  265. self.ct_orientation=vi.completeOrientation(self.ct_orientation)
  266. self.frame_orientation=vi.completeOrientation(self.frame_orientation)
  267. self.addCT()
  268. self.addFrames()
  269. widget.time_frame_select.setMaximum(self.frame_data.shape[3]-1)
  270. #additional message via qt
  271. qt.QMessageBox.information(
  272. slicer.util.mainWindow(),
  273. 'Slicer Python','Data loaded')
  274. def addNode(self,nodeName,v, lpsOrigin, pixel_size, lpsOrientation, dataType):
  275. # if dataType=0 it is CT data, which gets propagated to background an is
  276. #used to fit the view field dimensions
  277. # if dataType=1, it is SPECT data, which gets propagated to foreground
  278. #and is not fit; keeping window set from CT
  279. #nodeName='testVolume'+str(it)
  280. newNode=slicer.vtkMRMLScalarVolumeNode()
  281. newNode.SetName(nodeName)
  282. #pixel_size=[0,0,0]
  283. #pixel_size=v.GetSpacing()
  284. #print(pixel_size)
  285. #origin=[0,0,0]
  286. #origin=v.GetOrigin()
  287. v.SetOrigin([0,0,0])
  288. v.SetSpacing([1,1,1])
  289. ijkToRAS = vtk.vtkMatrix4x4()
  290. #think how to do this with image orientation
  291. rasOrientation=[-lpsOrientation[i] if (i%3 < 2) else lpsOrientation[i]
  292. for i in range(0,len(lpsOrientation))]
  293. rasOrigin=[-lpsOrigin[i] if (i%3<2) else lpsOrigin[i] for i in range(0,len(lpsOrigin))]
  294. for i in range(0,3):
  295. for j in range(0,3):
  296. ijkToRAS.SetElement(i,j,pixel_size[i]*rasOrientation[3*j+i])
  297. ijkToRAS.SetElement(i,3,rasOrigin[i])
  298. newNode.SetIJKToRASMatrix(ijkToRAS)
  299. newNode.SetAndObserveImageData(v)
  300. slicer.mrmlScene.AddNode(newNode)
  301. def addFrames(self):
  302. #convert data from numpy.array to vtkImageData
  303. #use time point it
  304. print "NFrames: {}".format(self.frame_data.shape[3])
  305. for it in range(0,self.frame_data.shape[3]):
  306. frame_data=self.frame_data[:,:,:,it];
  307. nodeName='testVolume'+str(it)
  308. self.addNode(nodeName,
  309. vi.numpyToVTK(frame_data,frame_data.shape),
  310. self.frame_origin,
  311. self.frame_pixel_size,
  312. self.frame_orientation,1)
  313. def addCT(self):
  314. nodeName='testCT'
  315. self.addNode(nodeName,
  316. #vi.numpyToVTK3D(self.ct_data,
  317. # self.ct_origin,self.ct_pixel_size),
  318. vi.numpyToVTK(self.ct_data,self.ct_data.shape),
  319. self.ct_origin,self.ct_pixel_size,
  320. self.ct_orientation,0)
  321. def meanROI(self, volName1, i):
  322. s=0
  323. #get the segmentation mask
  324. fNode=slicer.mrmlScene.GetNodesByClass("vtkMRMLSegmentationNode").GetItemAsObject(0)
  325. print "Found segmentation node: {}".format(fNode.GetName())
  326. segNode=slicer.vtkMRMLSegmentationNode.SafeDownCast(fNode)
  327. #no python bindings for vtkSegmentation
  328. #if segNode.GetSegmentation().GetNumberOfSegments()==0 :
  329. # print("No segments available")
  330. # return 0
  331. #edit here to change for more segments
  332. segment=segNode.GetSegmentation().GetNthSegmentID(i)
  333. mask = segNode.GetBinaryLabelmapRepresentation(segment)
  334. if mask==None:
  335. print("Segment {} not found".format(segment))
  336. return s
  337. print "Got mask for segment {}".format(segment)
  338. #get mask at (x,y,z)
  339. #mask.GetPointData().GetScalars().GetTuple1(mask.FindPoint([x,y,z]))
  340. #get the image data
  341. dataNode=slicer.mrmlScene.GetFirstNodeByName(volName1)
  342. dataImage=dataNode.GetImageData()
  343. # use IJK2RAS to get global coordinates
  344. ijkToRas = vtk.vtkMatrix4x4()
  345. dataNode.GetIJKToRASMatrix(ijkToRas)
  346. #iterate over volume pixelData
  347. n=dataImage.GetNumberOfPoints()
  348. extent=mask.GetExtent()
  349. fM=vtk.vtkMatrix4x4()
  350. mask.GetWorldToImageMatrix(fM)
  351. for i in range(0,n):
  352. #get global coordinates of point i
  353. [ix,iy,iz]=dataImage.GetPoint(i)
  354. position_ijk=[ix, iy, iz, 1]
  355. #ras are global coordinates (in mm)
  356. position_ras=ijkToRas.MultiplyPoint(position_ijk)
  357. fpos=[int(np.round(x)) for x in fM.MultiplyPoint(position_ras)]
  358. outOfRange=False
  359. for k in range(0,3):
  360. if fpos[k]<extent[2*k] or fpos[k]>extent[2*k+1]:
  361. outOfRange=True
  362. break;
  363. if outOfRange:
  364. continue
  365. #find point in mask with the same global coordinates
  366. maskValue=mask.GetPointData().GetScalars().GetTuple1(mask.ComputePointId(fpos[0:3]))
  367. if maskValue == 0:
  368. continue
  369. #use maskValue to project ROI data
  370. s+=maskValue*dataImage.GetPointData().GetScalars().GetTuple1(i)
  371. return s/n
  372. def countSegments(self):
  373. fNode=slicer.mrmlScene.GetNodesByClass("vtkMRMLSegmentationNode").GetItemAsObject(0)
  374. segNode=slicer.vtkMRMLSegmentationNode.SafeDownCast(fNode)
  375. i=1
  376. while 1:
  377. segName="Segment_"+str(i)
  378. mask = segNode.GetBinaryLabelmapRepresentation(segName)
  379. if mask==None:
  380. break
  381. i+=1
  382. return i-1
  383. class cardiacSPECTTest(ScriptedLoadableModuleTest):
  384. """
  385. This is the test case for your scripted module.
  386. Uses ScriptedLoadableModuleTest base class, available at:
  387. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  388. """
  389. def setUp(self):
  390. """ Do whatever is needed to reset the state - typically a scene clear will be enough.
  391. """
  392. slicer.mrmlScene.Clear(0)
  393. def runTest(self):
  394. """Run as few or as many tests as needed here.
  395. """
  396. self.setUp()
  397. self.test_cardiacSPECT1()
  398. def test_cardiacSPECT1(self):
  399. """ Ideally you should have several levels of tests. At the lowest level
  400. tests should exercise the functionality of the logic with different inputs
  401. (both valid and invalid). At higher levels your tests should emulate the
  402. way the user would interact with your code and confirm that it still works
  403. the way you intended.
  404. One of the most important features of the tests is that it should alert other
  405. developers when their changes will have an impact on the behavior of your
  406. module. For example, if a developer removes a feature that you depend on,
  407. your test should break so they know that the feature is needed.
  408. """
  409. self.delayDisplay("Starting the test")
  410. #
  411. # first, get some data
  412. #
  413. self.delayDisplay('Test passed!')