cardiacSPECT.py 16 KB

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