iraemmBrowser.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import os
  2. import unittest
  3. from __main__ import vtk, qt, ctk, slicer
  4. from slicer.ScriptedLoadableModule import *
  5. import slicerNetwork
  6. import loadDicom
  7. import json
  8. import datetime
  9. #
  10. # labkeySlicerPythonExtension
  11. #
  12. class iraemmBrowser(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. self.parent.title = "irAEMM Browser" # TODO make this more human readable by adding spaces
  19. self.parent.categories = ["LabKey"]
  20. self.parent.dependencies = []
  21. self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
  22. self.parent.helpText = """
  23. Interface to irAEMM files in LabKey
  24. """
  25. self.parent.acknowledgementText = """
  26. Developed within the medical physics research programme of the Slovenian research agency.
  27. """ # replace with organization, grant and thanks.
  28. #
  29. # labkeySlicerPythonExtensionWidget
  30. #
  31. class iraemmBrowserWidget(ScriptedLoadableModuleWidget):
  32. """Uses ScriptedLoadableModuleWidget base class, available at:
  33. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  34. """
  35. def setup(self):
  36. print("Setting up iraemmBrowserWidget")
  37. ScriptedLoadableModuleWidget.setup(self)
  38. # Instantiate and connect widgets ...
  39. self.network=slicerNetwork.labkeyURIHandler()
  40. fconfig=os.path.join(os.path.expanduser('~'),'.labkey','network.json')
  41. self.network.parseConfig(fconfig)
  42. self.network.initRemote()
  43. self.project="iPNUMMretro/Study"
  44. self.dataset="Imaging1"
  45. self.reviewDataset="ImageReview"
  46. self.logic=iraemmBrowserLogic(self)
  47. ds=self.network.filterDataset(self.project,self.dataset,[])
  48. ids=[row['PatientId'] for row in ds['rows']]
  49. ids=list(set(ids))
  50. #
  51. # Setup Area
  52. #
  53. setupCollapsibleButton = ctk.ctkCollapsibleButton()
  54. setupCollapsibleButton.text = "Setup"
  55. self.layout.addWidget(setupCollapsibleButton)
  56. setupFormLayout = qt.QFormLayout(setupCollapsibleButton)
  57. self.ctField=qt.QLabel("ctResampled")
  58. setupFormLayout.addRow("Data field (CT):",self.ctField)
  59. self.petField=qt.QLabel("petResampled")
  60. setupFormLayout.addRow("Data field (PET):",self.petField)
  61. self.segmentationField=qt.QLabel("Segmentation")
  62. setupFormLayout.addRow("Data field (Segmentation):",self.segmentationField)
  63. #
  64. # Patienrs Area
  65. #
  66. patientsCollapsibleButton = ctk.ctkCollapsibleButton()
  67. patientsCollapsibleButton.text = "Patients"
  68. self.layout.addWidget(patientsCollapsibleButton)
  69. patientsFormLayout = qt.QFormLayout(patientsCollapsibleButton)
  70. self.patientList=qt.QComboBox()
  71. for id in ids:
  72. self.patientList.addItem(id)
  73. self.patientList.currentIndexChanged.connect(self.onPatientListChanged)
  74. patientsFormLayout.addRow("Patient:",self.patientList)
  75. self.visitList=qt.QComboBox()
  76. self.visitList.currentIndexChanged.connect(self.onVisitListChanged)
  77. patientsFormLayout.addRow("Visit:",self.visitList)
  78. self.ctCode=qt.QLabel("ctCode")
  79. patientsFormLayout.addRow("CT:",self.ctCode)
  80. self.petCode=qt.QLabel("petCode")
  81. patientsFormLayout.addRow("PET:",self.petCode)
  82. self.segmentationCode=qt.QLabel("segmentationCode")
  83. patientsFormLayout.addRow("Segmentation",self.segmentationCode)
  84. self.patientLoad=qt.QPushButton("Load")
  85. self.patientLoad.clicked.connect(self.onPatientLoadButtonClicked)
  86. patientsFormLayout.addRow("Load patient",self.patientLoad)
  87. self.patientClear=qt.QPushButton("Clear")
  88. self.patientClear.clicked.connect(self.onPatientClearButtonClicked)
  89. patientsFormLayout.addRow("Clear patient",self.patientClear)
  90. self.keepCached=qt.QCheckBox("keep Cached")
  91. self.keepCached.setChecked(1)
  92. patientsFormLayout.addRow("Keep cached",self.keepCached)
  93. #set to a defined state
  94. self.onPatientListChanged(0)
  95. #
  96. # Review Area
  97. #
  98. reviewCollapsibleButton = ctk.ctkCollapsibleButton()
  99. reviewCollapsibleButton.text = "Review"
  100. self.layout.addWidget(reviewCollapsibleButton)
  101. self.reviewBoxLayout = qt.QVBoxLayout(reviewCollapsibleButton)
  102. self.reviewFormLayout = qt.QFormLayout()
  103. self.reviewSegment=qt.QComboBox()
  104. self.reviewSegment.currentIndexChanged.connect(self.onReviewSegmentChanged)
  105. self.reviewFormLayout.addRow("Selected region:",self.reviewSegment)
  106. self.reviewResult=qt.QComboBox()
  107. self.reviewFormLayout.addRow("What do you think about the segmentation:",\
  108. self.reviewResult)
  109. reviewOptions=['Select','Excellent','Minor deficiencies','Major deficiencies','Unusable']
  110. for opt in reviewOptions:
  111. self.reviewResult.addItem(opt)
  112. self.updateReview=qt.QPushButton("Save")
  113. self.reviewFormLayout.addRow("Save segmentation decision for current segment",\
  114. self.updateReview)
  115. self.updateReview.clicked.connect(self.onUpdateReviewButtonClicked)
  116. self.reviewBoxLayout.addLayout(self.reviewFormLayout)
  117. submitFrame=qt.QFrame()
  118. self.submitFormLayout=qt.QFormLayout()
  119. self.reviewComment=qt.QTextEdit("this is a test")
  120. self.submitFormLayout.addRow("Comments (optional)",\
  121. self.reviewComment)
  122. self.submitReviewButton=qt.QPushButton("Submit")
  123. self.submitFormLayout.addRow("Submit to database",\
  124. self.submitReviewButton)
  125. self.submitReviewButton.clicked.connect(self.onSubmitReviewButtonClicked)
  126. submitFrame.setLayout(self.submitFormLayout)
  127. submitFrame.setFrameShape(qt.QFrame.StyledPanel)
  128. submitFrame.setFrameShadow(qt.QFrame.Sunken)
  129. submitFrame.setStyleSheet("background-color:rgba(220,215,180,45)")
  130. self.reviewBoxLayout.addWidget(submitFrame)
  131. def onPatientListChanged(self,i):
  132. idFilter={'variable':'PatientId','value':self.patientList.currentText,'oper':'eq'}
  133. ds=self.network.filterDataset(self.project,self.dataset, [idFilter])
  134. seq=[int(row['SequenceNum']) for row in ds['rows']]
  135. self.visitList.clear()
  136. for s in seq:
  137. self.visitList.addItem("Visit "+str(s))
  138. self.onVisitListChanged(0)
  139. def onVisitListChanged(self,i):
  140. try:
  141. s=self.visitList.currentText.split(' ')[1]
  142. except IndexError:
  143. return
  144. print("Visit: Selected item: {}->{}".format(i,s))
  145. idFilter={'variable':'PatientId',\
  146. 'value':self.patientList.currentText,'oper':'eq'}
  147. sFilter={'variable':'SequenceNum','value':s,'oper':'eq'}
  148. ds=self.network.filterDataset(self.project,self.dataset,[idFilter,sFilter])
  149. if not len(ds['rows'])==1:
  150. print("Found incorrect number {} of matches for [{}]/[{}]".\
  151. format(len(ds['rows']),\
  152. self.patientList.currentText,s))
  153. row=ds['rows'][0]
  154. #copy row properties for data access
  155. self.currentRow=row
  156. self.petCode.setText(row[self.petField.text])
  157. self.ctCode.setText(row[self.ctField.text])
  158. self.segmentationCode.setText(row[self.segmentationField.text])
  159. def onPatientLoadButtonClicked(self):
  160. print("Load")
  161. #delegate loading to logic
  162. #try:
  163. self.logic.loadImage(self.currentRow,self.keepCached.isChecked())
  164. segmentList=self.logic.compileSegmentation()
  165. importantSegmentList=['liver','bowel','thyroid','lung','spleen']
  166. #also bladder,vertebraL1, stomach, heart
  167. for seg in segmentList:
  168. if not seg in importantSegmentList:
  169. continue
  170. #filter to most important ones
  171. self.reviewSegment.addItem(seg)
  172. self.logic.loadReview(self.currentRow)
  173. self.onReviewSegmentChanged()
  174. #except AttributeError:
  175. # print("Missing current row")
  176. # return
  177. def onReviewSegmentChanged(self):
  178. self.logic.hideSegments()
  179. self.logic.showSegment(self.reviewSegment.currentText)
  180. #set reviewFlag to stored value
  181. self.reviewResult.setCurrentIndex(self.logic.getReviewResult(self.reviewSegment.currentText))
  182. def onSubmitReviewButtonClicked(self):
  183. print("Submit")
  184. print("Selected review:{}/{}".format(self.reviewResult.currentIndex,
  185. self.reviewResult.currentText))
  186. print("Comment:{}".format(self.reviewComment))
  187. self.logic.submitReview(self.currentRow,\
  188. self.reviewComment.plainText)
  189. def onUpdateReviewButtonClicked(self):
  190. print("Save")
  191. self.logic.updateReview(self.reviewSegment.currentText,\
  192. self.reviewResult.currentIndex)
  193. idx=self.findCompletedSegment(self.reviewSegment.currentText)
  194. if idx<0:
  195. qReview=qt.QLabel(self.reviewResult.currentText)
  196. self.submitFormLayout.insertRow(0,self.reviewSegment.currentText,qReview)
  197. try:
  198. self.segmentsCompleted.append(self.reviewSegment.currentText)
  199. except AttributeError:
  200. self.segmentsCompleted=[]
  201. self.segmentsCompleted.append(self.reviewSegment.currentText)
  202. else:
  203. qReview=self.submitFormLayout.itemAt(idx,1).widget()
  204. qReview.setText(self.reviewResult.currentText)
  205. colors=['pink','green','yellow','orange','red']
  206. qReview.setStyleSheet("background-color: "+colors[self.reviewResult.currentIndex])
  207. def findCompletedSegment(self,segment):
  208. for i in range(self.submitFormLayout.rowCount()):
  209. if self.submitFormLayout.itemAt(i,0).widget().text==segment:
  210. return i
  211. return -1
  212. def removeCompletedSegments(self):
  213. try:
  214. segments=self.segmentsCompleted
  215. except AttributeError:
  216. return
  217. for seg in segments:
  218. idx=self.findCompletedSegment(seg)
  219. if idx>-1:
  220. self.submitFormLayout.removeRow(idx)
  221. self.segmentsCompleted=[]
  222. def onPatientClearButtonClicked(self):
  223. self.logic.clearVolumesAndSegmentations()
  224. self.reviewSegment.clear()
  225. self.removeCompletedSegments()
  226. def cleanup(self):
  227. pass
  228. #
  229. # irAEMMBrowserLogic
  230. #
  231. class iraemmBrowserLogic(ScriptedLoadableModuleLogic):
  232. """This class should implement all the actual
  233. computation done by your module. The interface
  234. should be such that other python code can import
  235. this class and make use of the functionality without
  236. requiring an instance of the Widget.
  237. Uses ScriptedLoadableModuleLogic base class, available at:
  238. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  239. """
  240. def __init__(self,parent=None):
  241. ScriptedLoadableModuleLogic.__init__(self, parent)
  242. if not parent==None:
  243. #assume parent has the network set up
  244. self.parent=parent
  245. self.net=parent.network
  246. self.project=parent.project
  247. self.segLabel={'1':'liver','2':'spleen','3':'lung','4':'thyroid',\
  248. '5':'kidney','6':'pancreas','7':'gallbladder','8':'bladder',\
  249. '9':'aorta','10':'trachea','11':'sternum','12':'vertebraL1',\
  250. '13':'adrenal','14':'psoasMajor','15':'rectus',\
  251. '16':'bowel','17':'stomach','18':'heart'}
  252. def setLabkeyInterface(self,net):
  253. #additional way of setting the labkey network interface
  254. #if no parent was provided in logic initialization (stand-alone mode)
  255. self.net=net
  256. def setLabkeyProject(self,project):
  257. self.project=project
  258. def loadImage(self,row,keepCached):
  259. #fields={'ctResampled':True,'petResampled':False}
  260. fields={"CT":self.parent.ctField.text,\
  261. "PET":self.parent.petField.text,\
  262. "Segmentation":self.parent.segmentationField.text}
  263. relativePaths={x:self.project+'/@files/preprocessedImages/'\
  264. +row['patientCode']+'/'+row['visitCode']+'/'+row[y]\
  265. for (x,y) in fields.items()}
  266. self.volumeNode={}
  267. for f in relativePaths:
  268. p=relativePaths[f]
  269. labkeyPath=self.net.GetLabkeyPathFromRelativePath(p)
  270. rp=self.net.head(labkeyPath)
  271. if not slicerNetwork.labkeyURIHandler.HTTPStatus(rp):
  272. print("Failed to get {}".format(labkeyPath))
  273. continue
  274. #pushes it to background
  275. properties={}
  276. #make sure segmentation gets loaded as a labelmap
  277. if f=="Segmentation":
  278. properties["labelmap"]=1
  279. self.volumeNode[f]=self.net.loadNode(p,'VolumeFile',\
  280. properties=properties,returnNode=True,keepCached=keepCached)
  281. #mimic abdominalCT standardized window setting
  282. self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
  283. SetWindowLevel(1400, -500)
  284. #set colormap for PET to PET-Heat (this is a verbatim setting from
  285. #the Volumes->Display->Lookup Table colormap identifier)
  286. self.volumeNode['PET'].GetScalarVolumeDisplayNode().\
  287. SetAndObserveColorNodeID(\
  288. slicer.util.getNode('PET-Heat').GetID())
  289. slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
  290. foreground=self.volumeNode['PET'],foregroundOpacity=0.5,fit=True)
  291. #segmentations
  292. def compileSegmentation(self):
  293. try:
  294. labelmapVolumeNode = self.volumeNode['Segmentation']
  295. except KeyError:
  296. print("No segmentaion volumeNode available")
  297. return
  298. self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
  299. slicer.modules.segmentations.logic().\
  300. ImportLabelmapToSegmentationNode(labelmapVolumeNode, self.segmentationNode)
  301. segmentList=[]
  302. seg=self.segmentationNode.GetSegmentation()
  303. for i in range(seg.GetNumberOfSegments()):
  304. segment=seg.GetNthSegment(i)
  305. segment.SetName(self.segLabel[segment.GetName()])
  306. segmentList.append(segment.GetName())
  307. #seg.CreateClosedSurfaceRepresentation()
  308. slicer.mrmlScene.RemoveNode(labelmapVolumeNode)
  309. self.volumeNode.pop('Segmentation',None)
  310. #return list of segment names
  311. return segmentList
  312. def hideSegments(self):
  313. try:
  314. displayNode=self.segmentationNode.GetDisplayNode()
  315. except AttributeError:
  316. return
  317. seg=self.segmentationNode.GetSegmentation()
  318. for i in range(seg.GetNumberOfSegments()):
  319. #segment=self.segmentationNode.GetSegmentation().GetNthSegment(i)
  320. segmentID=seg.GetNthSegmentID(i)
  321. displayNode.SetSegmentVisibility(segmentID, False)
  322. #print("Done")
  323. def showSegment(self,name):
  324. try:
  325. displayNode=self.segmentationNode.GetDisplayNode()
  326. except AttributeError:
  327. return
  328. seg=self.segmentationNode.GetSegmentation()
  329. for i in range(seg.GetNumberOfSegments()):
  330. segment=seg.GetNthSegment(i)
  331. if not segment.GetName()==name:
  332. continue
  333. segmentID=seg.GetNthSegmentID(i)
  334. displayNode.SetSegmentVisibility(segmentID, True)
  335. break
  336. #print("Done")
  337. #clear
  338. def clearVolumesAndSegmentations(self):
  339. nodes=slicer.util.getNodesByClass("vtkMRMLVolumeNode")
  340. nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
  341. res=[slicer.mrmlScene.RemoveNode(f) for f in nodes]
  342. self.segmentationNode=None
  343. self.reviewResult={}
  344. #reviews by segment
  345. def updateReview(self,segment,value):
  346. try:
  347. self.reviewResult[segment]=value
  348. except AttributeError:
  349. self.reviewResult={}
  350. self.updateReview(segment,value)
  351. def getReviewResult(self,segment):
  352. try:
  353. return self.reviewResult[segment]
  354. except AttributeError:
  355. #review result not initialized
  356. return 0
  357. except KeyError:
  358. #segment not done yet
  359. return 0
  360. #load review from labkey
  361. def loadReview(self,currentRow):
  362. #see if we have already done a review
  363. filters=[]
  364. fields=['PatientId','SequenceNum']
  365. for f in fields:
  366. filters.append({'variable':f,'value':str(currentRow[f]),'oper':'eq'})
  367. ds=self.net.filterDataset(self.parent.project,\
  368. self.parent.reviewDataset,filters)
  369. if len(ds['rows'])==0:
  370. return
  371. row=ds['rows'][0]
  372. for label in self.segLabel:
  373. name=self.segLabel[label]+'Review'
  374. try:
  375. self.updateReview(self.segLabel[label],row[name])
  376. except KeyError:
  377. continue
  378. #submit review to labkey
  379. def submitReview(self,currentRow,comment):
  380. row={}
  381. fields=['PatientId','SequenceNum']
  382. #see if we have to update or insert
  383. filters=[]
  384. for f in fields:
  385. filters.append({'variable':f,'value':str(currentRow[f]),'oper':'eq'})
  386. ds=self.net.filterDataset(self.parent.project,\
  387. self.parent.reviewDataset,filters)
  388. mode='insert'
  389. if len(ds['rows'])>0:
  390. row=ds['rows'][0]
  391. mode='update'
  392. else:
  393. for f in fields:
  394. row[f]=currentRow[f]
  395. seg=self.segmentationNode.GetSegmentation()
  396. for i in range(seg.GetNumberOfSegments()):
  397. segment=seg.GetNthSegment(i)
  398. fieldName=segment.GetName()+'Review'
  399. value=self.getReviewResult(segment.GetName())
  400. row[fieldName]=value
  401. row['reviewComment']=comment
  402. row['Date']=datetime.datetime.now().ctime()
  403. ds=self.net.modifyDataset(mode,self.parent.project,\
  404. self.parent.reviewDataset,[row])
  405. print("review submitted")
  406. class irAEMMBrowserTest(ScriptedLoadableModuleTest):
  407. """
  408. This is the test case for your scripted module.
  409. Uses ScriptedLoadableModuleTest base class, available at:
  410. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  411. """
  412. def setUp(self):
  413. """ Do whatever is needed to reset the state - typically a scene clear will be enough.
  414. """
  415. slicer.mrmlScene.Clear(0)
  416. def runTest(self):
  417. """Run as few or as many tests as needed here.
  418. """
  419. self.setUp()
  420. self.test_irAEMMBrowser()
  421. def test_irAEMMBrowser(self):
  422. """ Ideally you should have several levels of tests. At the lowest level
  423. tests sould exercise the functionality of the logic with different inputs
  424. (both valid and invalid). At higher levels your tests should emulate the
  425. way the user would interact with your code and confirm that it still works
  426. the way you intended.
  427. One of the most important features of the tests is that it should alert other
  428. developers when their changes will have an impact on the behavior of your
  429. module. For example, if a developer removes a feature that you depend on,
  430. your test should break so they know that the feature is needed.
  431. """
  432. self.delayDisplay("Starting the test")
  433. #
  434. # first, get some data
  435. #