imageBrowser.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import os
  2. import unittest
  3. from __main__ import vtk, qt, ctk, slicer
  4. from slicer.ScriptedLoadableModule import *
  5. import json
  6. import datetime
  7. import sys
  8. import nixModule
  9. import pathlib
  10. #
  11. # labkeySlicerPythonExtension
  12. #
  13. class imageBrowser(ScriptedLoadableModule):
  14. """Uses ScriptedLoadableModule base class, available at:
  15. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  16. """
  17. def __init__(self, parent):
  18. ScriptedLoadableModule.__init__(self, parent)
  19. self.parent.title = "image Browser"
  20. # TODO make this more human readable by adding spaces
  21. self.parent.categories = ["LabKey"]
  22. self.parent.dependencies = []
  23. self.parent.contributors = ["Andrej Studen (UL/FMF)"]
  24. # replace with "Firstname Lastname (Organization)"
  25. self.parent.helpText = """
  26. Interface to irAEMM files in LabKey
  27. """
  28. self.parent.acknowledgementText = """
  29. Developed within the medical physics research programme
  30. of the Slovenian research agency.
  31. """ # replace with organization, grant and thanks.
  32. #
  33. # labkeySlicerPythonExtensionWidget
  34. #
  35. class imageBrowserWidget(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. print("Setting up imageBrowserWidget")
  41. ScriptedLoadableModuleWidget.setup(self)
  42. # Instantiate and connect widgets ...
  43. self.logic=imageBrowserLogic(self)
  44. self.addInfoSection()
  45. self.addSetupSection()
  46. self.addPatientsSelector()
  47. def addInfoSection(self):
  48. #a python overview of json settings
  49. infoCollapsibleButton = ctk.ctkCollapsibleButton()
  50. infoCollapsibleButton.text = "Info"
  51. self.layout.addWidget(infoCollapsibleButton)
  52. infoLayout = qt.QFormLayout(infoCollapsibleButton)
  53. self.participantField=qt.QLabel("PatientId")
  54. infoLayout.addRow("Participant field:",self.participantField)
  55. self.ctField=qt.QLabel("ctResampled")
  56. infoLayout.addRow("Data field (CT):",self.ctField)
  57. self.petField=qt.QLabel("petResampled")
  58. infoLayout.addRow("Data field (PET):",self.petField)
  59. self.idField=qt.QLabel("Loading")
  60. infoLayout.addRow("ID",self.idField)
  61. #Add logic at some point
  62. #self.logic=imageBrowserLogic(self)
  63. def updatePatientList(self,ids):
  64. self.patientList.clear()
  65. for id in ids:
  66. self.patientList.addItem(id)
  67. def addPatientsSelector(self):
  68. #
  69. # Patients Area
  70. #
  71. patientsCollapsibleButton = ctk.ctkCollapsibleButton()
  72. patientsCollapsibleButton.text = "Patients"
  73. #don't add it yet
  74. self.layout.addWidget(patientsCollapsibleButton)
  75. patientsFormLayout = qt.QFormLayout(patientsCollapsibleButton)
  76. self.patientList=qt.QComboBox()
  77. self.patientList.currentIndexChanged.connect(self.onPatientListChanged)
  78. patientsFormLayout.addRow("Patient:",self.patientList)
  79. self.visitList=qt.QComboBox()
  80. self.visitList.currentIndexChanged.connect(self.onVisitListChanged)
  81. patientsFormLayout.addRow("Visit:",self.visitList)
  82. self.ctCode=qt.QLabel("ctCode")
  83. patientsFormLayout.addRow("CT:",self.ctCode)
  84. self.petCode=qt.QLabel("petCode")
  85. patientsFormLayout.addRow("PET:",self.petCode)
  86. self.patientLoad=qt.QPushButton("Load")
  87. self.patientLoad.clicked.connect(self.onPatientLoadButtonClicked)
  88. patientsFormLayout.addRow("Load patient",self.patientLoad)
  89. self.patientClear=qt.QPushButton("Clear")
  90. self.patientClear.clicked.connect(self.onPatientClearButtonClicked)
  91. patientsFormLayout.addRow("Clear patient",self.patientClear)
  92. self.keepCached=qt.QCheckBox("keep Cached")
  93. self.keepCached.setChecked(1)
  94. patientsFormLayout.addRow("Keep cached",self.keepCached)
  95. def addSetupSection(self):
  96. setupCollapsibleButton = ctk.ctkCollapsibleButton()
  97. setupCollapsibleButton.text = "Setup"
  98. self.layout.addWidget(setupCollapsibleButton)
  99. #Form layout (maybe one can think of more intuitive layouts)
  100. setupFormLayout = qt.QFormLayout(setupCollapsibleButton)
  101. self.serverList=qt.QComboBox()
  102. self.serverList.addItem('<Select>')
  103. self.serverList.addItem("astuden")
  104. self.serverList.currentIndexChanged.connect(self.onServerListChanged)
  105. setupFormLayout.addRow("Database:",self.serverList)
  106. self.setupList=qt.QComboBox()
  107. self.setupList.addItem('<Select>')
  108. self.setupList.addItem("limfomiPET_iBrowser.json")
  109. self.setupList.currentIndexChanged.connect(self.onSetupListChanged)
  110. setupFormLayout.addRow("Setup:",self.setupList)
  111. def addReviewSection(self):
  112. #
  113. # Review Area
  114. #
  115. reviewCollapsibleButton = ctk.ctkCollapsibleButton()
  116. reviewCollapsibleButton.text = "Review"
  117. self.layout.addWidget(reviewCollapsibleButton)
  118. self.reviewBoxLayout = qt.QVBoxLayout(reviewCollapsibleButton)
  119. self.reviewFormLayout = qt.QFormLayout()
  120. self.reviewSegment=qt.QComboBox()
  121. self.reviewSegment.currentIndexChanged.connect(\
  122. self.onReviewSegmentChanged)
  123. self.reviewFormLayout.addRow("Selected region:",self.reviewSegment)
  124. self.reviewResult=qt.QComboBox()
  125. sLabel="What do you think about the segmentation:"
  126. self.reviewFormLayout.addRow(sLabel,self.reviewResult)
  127. reviewOptions=['Select','Excellent','Minor deficiencies',\
  128. 'Major deficiencies','Unusable']
  129. for opt in reviewOptions:
  130. self.reviewResult.addItem(opt)
  131. self.aeResult=qt.QComboBox()
  132. aeLabel="Is organ suffering from adverse effect?"
  133. self.reviewFormLayout.addRow(aeLabel,self.aeResult)
  134. aeOptions=['Select','Yes','No']
  135. for opt in aeOptions:
  136. self.aeResult.addItem(opt)
  137. #self.aeResult.setCurrentIndex(0)
  138. self.updateReview=qt.QPushButton("Save")
  139. saLabel="Save segmentation and AE decision for current segment"
  140. self.reviewFormLayout.addRow(saLabel,self.updateReview)
  141. self.updateReview.clicked.connect(self.onUpdateReviewButtonClicked)
  142. self.reviewBoxLayout.addLayout(self.reviewFormLayout)
  143. submitFrame=qt.QGroupBox("Submit data")
  144. self.submitFormLayout=qt.QFormLayout()
  145. self.reviewComment=qt.QTextEdit("this is a test")
  146. self.submitFormLayout.addRow("Comments (optional)",self.reviewComment)
  147. self.submitReviewButton=qt.QPushButton("Submit")
  148. self.submitFormLayout.addRow("Submit to database",\
  149. self.submitReviewButton)
  150. self.submitReviewButton.clicked.connect(\
  151. self.onSubmitReviewButtonClicked)
  152. submitFrame.setLayout(self.submitFormLayout)
  153. submitFrame.setFlat(1)
  154. #submitFrame.setFrameShape(qt.QFrame.StyledPanel)
  155. #submitFrame.setFrameShadow(qt.QFrame.Sunken)
  156. submitFrame.setStyleSheet("background-color:rgba(220,215,180,45)")
  157. self.reviewBoxLayout.addWidget(submitFrame)
  158. def onSetupListChanged(self,i):
  159. status=self.logic.setConfig(self.setupList.currentText)
  160. try:
  161. if status['error']=='FILE NOT FOUND':
  162. print('File {} not found.'.format(self.setupList.currentText))
  163. return
  164. except KeyError:
  165. pass
  166. self.updatePatientList(status['ids'])
  167. self.onPatientListChanged(0)
  168. def onServerListChanged(self,i):
  169. status=self.logic.setServer(self.serverList.currentText)
  170. try:
  171. if status['error']=='KEY ERROR':
  172. self.serverList.setStyleSheet('background-color: violet')
  173. if status['error']=='ID ERROR':
  174. self.serverList.setStyleSheet('background-color: red')
  175. return
  176. except KeyError:
  177. pass
  178. self.idField.setText(status['id'])
  179. self.serverList.setStyleSheet('background-color: green')
  180. def onPatientListChanged(self,i):
  181. idFilter={'variable':'PatientId',
  182. 'value':self.patientList.currentText,
  183. 'oper':'eq'}
  184. ds=self.logic.getDataset([idFilter])
  185. seq=[int(row['SequenceNum']) for row in ds['rows']]
  186. self.visitList.clear()
  187. for s in seq:
  188. self.visitList.addItem("Visit "+str(s))
  189. self.onVisitListChanged(0)
  190. def onVisitListChanged(self,i):
  191. try:
  192. s=self.visitList.currentText.split(' ')[1]
  193. except IndexError:
  194. return
  195. print("Visit: Selected item: {}->{}".format(i,s))
  196. idFilter={'variable':'PatientId',\
  197. 'value':self.patientList.currentText,'oper':'eq'}
  198. sFilter={'variable':'SequenceNum','value':s,'oper':'eq'}
  199. ds=self.logic.getDataset([idFilter,sFilter])
  200. if not len(ds['rows'])==1:
  201. print("Found incorrect number {} of matches for [{}]/[{}]".\
  202. format(len(ds['rows']),\
  203. self.patientList.currentText,s))
  204. row=ds['rows'][0]
  205. #copy row properties for data access
  206. self.currentRow=row
  207. self.petCode.setText(row[self.petField.text])
  208. self.ctCode.setText(row[self.ctField.text])
  209. #self.segmentationCode.setText(row[self.segmentationField.text])
  210. def onPatientLoadButtonClicked(self):
  211. print("Load")
  212. #delegate loading to logic
  213. self.logic.loadImages(self.currentRow,self.keepCached.isChecked())
  214. #self.logic.loadReview(self.currentRow)
  215. #self.logic.loadAE(self.currentRow)
  216. #self.onReviewSegmentChanged()
  217. def onReviewSegmentChanged(self):
  218. pass
  219. def onPatientClearButtonClicked(self):
  220. self.logic.clearVolumesAndSegmentations()
  221. #self.reviewSegment.clear()
  222. #self.removeCompletedSegments()
  223. #self.reviewComment.clear()
  224. def cleanup(self):
  225. pass
  226. def loadLibrary(name):
  227. #utility function to load nix library from git
  228. fwrapper=nixModule.getWrapper('nixWrapper.py')
  229. p=pathlib.Path(fwrapper)
  230. sys.path.append(str(p.parent))
  231. import nixWrapper
  232. return nixWrapper.loadLibrary(name)
  233. #
  234. # imageBrowserLogic
  235. #
  236. class imageBrowserLogic(ScriptedLoadableModuleLogic):
  237. """This class should implement all the actual
  238. computation done by your module. The interface
  239. should be such that other python code can import
  240. this class and make use of the functionality without
  241. requiring an instance of the Widget.
  242. Uses ScriptedLoadableModuleLogic base class, available at:
  243. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  244. """
  245. def __init__(self,parent=None):
  246. ScriptedLoadableModuleLogic.__init__(self, parent)
  247. print('imageBrowserLogic loading')
  248. if not parent==None:
  249. #use layout and data from parent widget
  250. self.parent=parent
  251. fhome=os.path.expanduser('~')
  252. fsetup=os.path.join(fhome,'.labkey','setup.json')
  253. try:
  254. with open(fsetup) as f:
  255. self.setup=json.load(f)
  256. except FileNotFoundError:
  257. self.setup={}
  258. try:
  259. pt=self.setup['paths']
  260. except KeyError:
  261. self.setup['paths']={}
  262. lName='labkeyInterface'
  263. loadLibrary(lName)
  264. import labkeyInterface
  265. import labkeyDatabaseBrowser
  266. import labkeyFileBrowser
  267. self.network=labkeyInterface.labkeyInterface()
  268. self.dbBrowser=labkeyDatabaseBrowser
  269. self.fBrowser=labkeyFileBrowser
  270. print('imageBrowserLogic setup complete')
  271. def setServer(self,serverName):
  272. #additional way of setting the labkey network interface
  273. #if no parent was provided in logic initialization (stand-alone mode)
  274. status={}
  275. fileName="NONE"
  276. if serverName=="astuden":
  277. fileName="astuden.json"
  278. if fileName=="NONE":
  279. print("No path was associated with server {}".format(serverName))
  280. status['error']='KEY ERROR'
  281. return status
  282. fconfig=os.path.join(os.path.expanduser('~'),'.labkey',fileName)
  283. self.network.init(fconfig)
  284. remoteId=self.network.getUserId()
  285. if remoteId==None:
  286. status['error']='ID ERROR'
  287. return status
  288. status['id']=remoteId['displayName']
  289. #reset db and fb (they are thin classes anyhow)
  290. self.db=self.dbBrowser.labkeyDB(self.network)
  291. self.fb=self.fBrowser.labkeyFileBrowser(self.network)
  292. return status
  293. def setConfig(self,configName):
  294. status={}
  295. fileName=os.path.join(os.path.expanduser('~'),'.labkey',configName)
  296. if not os.path.isfile(fileName):
  297. status['error']='FILE NOT FOUND'
  298. return status
  299. with open(fileName,'r') as f:
  300. self.isetup=json.load(f)
  301. self.project=self.isetup['project']
  302. #"iPNUMMretro/Study"
  303. self.schema='study'
  304. self.dataset=self.isetup['query']
  305. ds=self.getDataset([])
  306. ids=[row[self.isetup['participantField']] for row in ds['rows']]
  307. status['ids']=list(set(ids))
  308. return status
  309. def getDataset(self,dbFilter):
  310. project=self.project
  311. schema=self.schema
  312. query=self.dataset
  313. try:
  314. return self.db.selectRows(project,schema,query, \
  315. dbFilter,self.isetup['view'])
  316. except KeyError:
  317. return self.db.selectRows(project,schema,query,dbFilter)
  318. def loadImage(self,idx,path,tempDir,keepCached):
  319. localPath=os.path.join(tempDir,path[-1])
  320. if not os.path.isfile(localPath):
  321. #download from server
  322. remotePath=self.fb.formatPathURL(self.project,'/'.join(path))
  323. if not self.fb.entryExists(remotePath):
  324. print("Failed to get {}".format(remotePath))
  325. return
  326. self.fb.readFileToFile(remotePath,localPath)
  327. properties={}
  328. #make sure segmentation gets loaded as a labelmap
  329. if idx=="Segmentation":
  330. properties["labelmap"]=1
  331. self.volumeNode[idx]=slicer.util.loadNodeFromFile(localPath,
  332. filetype='VolumeFile',properties=properties)
  333. if not keepCached:
  334. os.remove(localPath)
  335. def loadImages(self,row,keepCached):
  336. tempDir=os.path.join(os.path.expanduser('~'),'temp')
  337. if not os.path.isdir(tempDir):
  338. os.mkdir(tempDir)
  339. #fields={'ctResampled':True,'petResampled':False}
  340. fields={"CT":self.parent.ctField.text,\
  341. "PET":self.parent.petField.text}
  342. path=['preprocessedImages',row['patientCode'],row['visitCode']]
  343. relativePaths={x:path+[row[y]] for (x,y) in fields.items()}
  344. self.volumeNode={}
  345. for f in relativePaths:
  346. p=relativePaths[f]
  347. self.loadImage(f,p,tempDir,keepCached)
  348. #mimic abdominalCT standardized window setting
  349. self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
  350. SetWindowLevel(1400, -500)
  351. #set colormap for PET to PET-Heat (this is a verbatim setting from
  352. #the Volumes->Display->Lookup Table colormap identifier)
  353. self.volumeNode['PET'].GetScalarVolumeDisplayNode().\
  354. SetAndObserveColorNodeID(\
  355. slicer.util.getNode('PET-Heat').GetID())
  356. slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
  357. foreground=self.volumeNode['PET'],foregroundOpacity=0.1,fit=True)
  358. def clearVolumesAndSegmentations(self):
  359. nodes=slicer.util.getNodesByClass("vtkMRMLVolumeNode")
  360. #nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
  361. res=[slicer.mrmlScene.RemoveNode(f) for f in nodes]
  362. #self.segmentationNode=None
  363. #self.reviewResult={}
  364. #self.aeList={}
  365. class imageBrowserTest(ScriptedLoadableModuleTest):
  366. """
  367. This is the test case for your scripted module.
  368. Uses ScriptedLoadableModuleTest base class, available at:
  369. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  370. """
  371. def setup(self):
  372. """ Do whatever is needed to reset the state - typically a scene clear will be enough.
  373. """
  374. slicer.mrmlScene.Clear(0)
  375. def runTest(self):
  376. """Run as few or as many tests as needed here.
  377. """
  378. self.setUp()
  379. self.test_irAEMMBrowser()
  380. def test_irAEMMBrowser(self):
  381. """ Ideally you should have several levels of tests. At the lowest level
  382. tests sould exercise the functionality of the logic with different inputs
  383. (both valid and invalid). At higher levels your tests should emulate the
  384. way the user would interact with your code and confirm that it still works
  385. the way you intended.
  386. One of the most important features of the tests is that it should alert other
  387. developers when their changes will have an impact on the behavior of your
  388. module. For example, if a developer removes a feature that you depend on,
  389. your test should break so they know that the feature is needed.
  390. """
  391. self.delayDisplay("Starting the test")
  392. #
  393. # first, get some data
  394. #