segmentationBrowser.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  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. import chardet
  11. import re
  12. import functools
  13. import nixWrapper
  14. #
  15. # labkeySlicerPythonExtension
  16. #
  17. class segmentationBrowser(ScriptedLoadableModule):
  18. """Uses ScriptedLoadableModule base class, available at:
  19. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  20. """
  21. def __init__(self, parent):
  22. ScriptedLoadableModule.__init__(self, parent)
  23. self.parent.title = "segmentation Browser"
  24. # TODO make this more human readable by adding spaces
  25. self.parent.categories = ["LabKey"]
  26. self.parent.dependencies = []
  27. self.parent.contributors = ["Andrej Studen (UL/FMF)"]
  28. # replace with "Firstname Lastname (Organization)"
  29. self.parent.helpText = """
  30. Interface to stored segmentation files in LabKey
  31. """
  32. self.parent.acknowledgementText = """
  33. Developed within the medical physics research programme
  34. of the Slovenian research agency.
  35. """ # replace with organization, grant and thanks.
  36. #
  37. # labkeySlicerPythonExtensionWidget
  38. #
  39. class segmentationBrowserWidget(ScriptedLoadableModuleWidget):
  40. """Uses ScriptedLoadableModuleWidget base class, available at:
  41. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  42. """
  43. def setup(self):
  44. print("Setting up segmentationBrowserWidget")
  45. ScriptedLoadableModuleWidget.setup(self)
  46. # Instantiate and connect widgets ...
  47. self.logic=segmentationBrowserLogic(self)
  48. self.addInfoSection()
  49. self.addSetupSection()
  50. self.addPatientsSelector()
  51. self.addSegmentViewer()
  52. self.addWindowManipulator()
  53. def addInfoSection(self):
  54. #a python overview of json settings
  55. infoCollapsibleButton = ctk.ctkCollapsibleButton()
  56. infoCollapsibleButton.text = "Info"
  57. self.layout.addWidget(infoCollapsibleButton)
  58. infoLayout = qt.QFormLayout(infoCollapsibleButton)
  59. self.participantField=qt.QLabel("PatientId")
  60. infoLayout.addRow("Participant field:",self.participantField)
  61. self.ctField=qt.QLabel("ctResampled")
  62. infoLayout.addRow("Data field (CT):",self.ctField)
  63. self.petField=qt.QLabel("petResampled")
  64. infoLayout.addRow("Data field (PET):",self.petField)
  65. self.userField=qt.QLabel("Loading")
  66. infoLayout.addRow("User",self.userField)
  67. self.idField=qt.QLabel("Loading")
  68. infoLayout.addRow("ID",self.idField)
  69. #Add logic at some point
  70. #self.logic=segmentationBrowserLogic(self)
  71. def addPatientsSelector(self):
  72. #
  73. # Patients Area
  74. #
  75. patientsCollapsibleButton = ctk.ctkCollapsibleButton()
  76. patientsCollapsibleButton.text = "Patients"
  77. #don't add it yet
  78. self.layout.addWidget(patientsCollapsibleButton)
  79. patientsFormLayout = qt.QFormLayout(patientsCollapsibleButton)
  80. self.patientList=qt.QComboBox()
  81. self.patientList.currentIndexChanged.connect(self.onPatientListChanged)
  82. self.patientList.setEditable(True)
  83. self.patientList.setInsertPolicy(qt.QComboBox.NoInsert)
  84. patientsFormLayout.addRow("Patient:",self.patientList)
  85. self.visitList=qt.QComboBox()
  86. self.visitList.currentIndexChanged.connect(self.onVisitListChanged)
  87. patientsFormLayout.addRow("Visit:",self.visitList)
  88. self.ctCode=qt.QLabel("ctCode")
  89. patientsFormLayout.addRow("CT:",self.ctCode)
  90. self.petCode=qt.QLabel("petCode")
  91. patientsFormLayout.addRow("PET:",self.petCode)
  92. self.patientLoad=qt.QPushButton("Load")
  93. self.patientLoad.clicked.connect(self.onPatientLoadButtonClicked)
  94. patientsFormLayout.addRow("Load patient",self.patientLoad)
  95. #no Save button
  96. #self.patientSave=qt.QPushButton("Save")
  97. #self.patientSave.clicked.connect(self.onPatientSaveButtonClicked)
  98. #patientsFormLayout.addRow("Save segmentation",self.patientSave)
  99. self.patientClear=qt.QPushButton("Clear")
  100. self.patientClear.clicked.connect(self.onPatientClearButtonClicked)
  101. patientsFormLayout.addRow("Clear patient",self.patientClear)
  102. self.keepCached=qt.QCheckBox("keep Cached")
  103. self.keepCached.setChecked(1)
  104. patientsFormLayout.addRow("Keep cached",self.keepCached)
  105. def addSetupSection(self):
  106. setupCollapsibleButton = ctk.ctkCollapsibleButton()
  107. setupCollapsibleButton.text = "Setup"
  108. self.layout.addWidget(setupCollapsibleButton)
  109. #Form layout (maybe one can think of more intuitive layouts)
  110. setupFormLayout = qt.QFormLayout(setupCollapsibleButton)
  111. self.serverList=qt.QComboBox()
  112. self.serverList.addItem('<Select>')
  113. self.serverList.addItem("astuden")
  114. self.serverList.addItem("adoma")
  115. self.serverList.addItem("kzevnik")
  116. self.serverList.currentIndexChanged.connect(self.onServerListChanged)
  117. setupFormLayout.addRow("Select user:",self.serverList)
  118. self.setupList=qt.QComboBox()
  119. self.setupList.addItem('<Select>')
  120. self.setupList.addItem("limfomiPET_iBrowser.json")
  121. self.setupList.addItem("iraemm_iBrowserProspective.json")
  122. self.setupList.addItem("iraemm_iBrowserRetrospective.json")
  123. self.setupList.addItem("ORLPET_iBrowser.json")
  124. self.setupList.currentIndexChanged.connect(self.onSetupListChanged)
  125. setupFormLayout.addRow("Setup:",self.setupList)
  126. def addReviewSection(self):
  127. #
  128. # Review Area (not used, relic)
  129. #
  130. reviewCollapsibleButton = ctk.ctkCollapsibleButton()
  131. reviewCollapsibleButton.text = "Review"
  132. self.layout.addWidget(reviewCollapsibleButton)
  133. self.reviewBoxLayout = qt.QVBoxLayout(reviewCollapsibleButton)
  134. self.reviewFormLayout = qt.QFormLayout()
  135. self.reviewSegment=qt.QComboBox()
  136. self.reviewSegment.currentIndexChanged.connect(\
  137. self.onReviewSegmentChanged)
  138. self.reviewFormLayout.addRow("Selected region:",self.reviewSegment)
  139. self.reviewResult=qt.QComboBox()
  140. sLabel="What do you think about the segmentation:"
  141. self.reviewFormLayout.addRow(sLabel,self.reviewResult)
  142. reviewOptions=['Select','Excellent','Minor deficiencies',\
  143. 'Major deficiencies','Unusable']
  144. for opt in reviewOptions:
  145. self.reviewResult.addItem(opt)
  146. self.aeResult=qt.QComboBox()
  147. aeLabel="Is organ suffering from adverse effect?"
  148. self.reviewFormLayout.addRow(aeLabel,self.aeResult)
  149. aeOptions=['Select','Yes','No']
  150. for opt in aeOptions:
  151. self.aeResult.addItem(opt)
  152. #self.aeResult.setCurrentIndex(0)
  153. self.updateReview=qt.QPushButton("Save")
  154. saLabel="Save segmentation and AE decision for current segment"
  155. self.reviewFormLayout.addRow(saLabel,self.updateReview)
  156. self.updateReview.clicked.connect(self.onUpdateReviewButtonClicked)
  157. self.reviewBoxLayout.addLayout(self.reviewFormLayout)
  158. submitFrame=qt.QGroupBox("Submit data")
  159. self.submitFormLayout=qt.QFormLayout()
  160. self.reviewComment=qt.QTextEdit("this is a test")
  161. self.submitFormLayout.addRow("Comments (optional)",self.reviewComment)
  162. self.submitReviewButton=qt.QPushButton("Submit")
  163. self.submitFormLayout.addRow("Submit to database",\
  164. self.submitReviewButton)
  165. self.submitReviewButton.clicked.connect(\
  166. self.onSubmitReviewButtonClicked)
  167. submitFrame.setLayout(self.submitFormLayout)
  168. submitFrame.setFlat(1)
  169. #submitFrame.setFrameShape(qt.QFrame.StyledPanel)
  170. #submitFrame.setFrameShadow(qt.QFrame.Sunken)
  171. submitFrame.setStyleSheet("background-color:rgba(220,215,180,45)")
  172. self.reviewBoxLayout.addWidget(submitFrame)
  173. def addSegmentViewer(self):
  174. #not used (for review)
  175. editorCollapsibleButton = ctk.ctkCollapsibleButton()
  176. editorCollapsibleButton.text = "Segment Viewer"
  177. self.layout.addWidget(editorCollapsibleButton)
  178. self.vSegViewLayout=qt.QVBoxLayout(editorCollapsibleButton)
  179. segList=' '.join(self.logic.segmentList)
  180. label=qt.QLabel(" {}".format(segList))
  181. self.vSegViewLayout.addWidget(label)
  182. self.segList=[]
  183. def addWindowManipulator(self):
  184. windowManipulatorCollapsibleButton=ctk.ctkCollapsibleButton()
  185. windowManipulatorCollapsibleButton.text="CT Window Manipulator"
  186. self.layout.addWidget(windowManipulatorCollapsibleButton)
  187. hLayout=qt.QHBoxLayout(windowManipulatorCollapsibleButton)
  188. ctWins={'CT:bone':self.onCtBoneButtonClicked,
  189. 'CT:air':self.onCtAirButtonClicked,
  190. 'CT:abdomen':self.onCtAbdomenButtonClicked,
  191. 'CT:brain':self.onCtBrainButtonClicked,
  192. 'CT:lung':self.onCtLungButtonClicked}
  193. for ctWin in ctWins:
  194. ctButton=qt.QPushButton(ctWin)
  195. ctButton.clicked.connect(ctWins[ctWin])
  196. hLayout.addWidget(ctButton)
  197. def onSetupListChanged(self,i):
  198. status=self.logic.setConfig(self.setupList.currentText)
  199. try:
  200. if status['error']=='FILE NOT FOUND':
  201. print('File {} not found.'.format(self.setupList.currentText))
  202. return
  203. except KeyError:
  204. pass
  205. #sort ids
  206. ids=status['ids']
  207. ids.sort()
  208. self.updatePatientList(ids)
  209. self.onPatientListChanged(0)
  210. def onServerListChanged(self,i):
  211. status=self.logic.setServer(self.serverList.currentText)
  212. try:
  213. if status['error']=='KEY ERROR':
  214. self.serverList.setStyleSheet('background-color: violet')
  215. if status['error']=='ID ERROR':
  216. self.serverList.setStyleSheet('background-color: red')
  217. return
  218. except KeyError:
  219. pass
  220. self.idField.setText(status['id'])
  221. self.userField.setText(status['displayName'])
  222. self.serverList.setStyleSheet('background-color: green')
  223. def onPatientListChanged(self,i):
  224. self.visitList.clear()
  225. self.petCode.setText("")
  226. self.ctCode.setText("")
  227. #add potential filters from setup to dbFilter
  228. ds=self.logic.getDataset(dbFilter={'participant':self.patientList.currentText})
  229. visitVar=self.logic.getVarName(var='visitField')
  230. dt=datetime.datetime
  231. #label is a combination of sequence number and date of imaging
  232. try:
  233. seq={row['SequenceNum']:
  234. {'label':row[visitVar],
  235. 'date': dt.strptime(row['studyDate'],'%Y/%m/%d %H:%M:%S')}
  236. for row in ds['rows']}
  237. except TypeError:
  238. #if studyDate is empty, this will return no possible visits
  239. return
  240. #apply lookup to visitVar if available
  241. try:
  242. seq={x:
  243. {'label':ds['lookups'][visitVar][seq[x]['label']],
  244. 'date':seq[x]['date']}
  245. for x in seq}
  246. except KeyError:
  247. pass
  248. #format label
  249. seq={x:'{} ({})'.format(seq[x]['label'],dt.strftime(seq[x]['date'],'%d-%b-%Y'))
  250. for x in seq}
  251. for s in seq:
  252. #onVisitListChanged is called for every addItem
  253. self.visitList.addItem(seq[s],s)
  254. #self.onVisitListChanged(0)
  255. def onVisitListChanged(self,i):
  256. #ignore calls on empty list
  257. if self.visitList.count==0:
  258. return
  259. #get sequence num
  260. s=self.visitList.itemData(i)
  261. print("Visit: SequenceNum:{}, label{}".format(s,self.visitList.currentText))
  262. dbFilter={'participant':self.patientList.currentText,
  263. 'seqNum':s}
  264. ds=self.logic.getDataset(dbFilter=dbFilter)
  265. if not len(ds['rows'])==1:
  266. print("Found incorrect number {} of matches for [{}]/[{}]".\
  267. format(len(ds['rows']),\
  268. self.patientList.currentText,s))
  269. row=ds['rows'][0]
  270. #copy row properties for data access
  271. self.currentRow=row
  272. self.petCode.setText(row[self.petField.text])
  273. self.ctCode.setText(row[self.ctField.text])
  274. #self.segmentationCode.setText(row[self.segmentationField.text])
  275. def updatePatientList(self,ids):
  276. self.patientList.clear()
  277. for id in ids:
  278. self.patientList.addItem(id)
  279. def onPatientLoadButtonClicked(self):
  280. #clear first
  281. self.logic.clearVolumesAndSegmentations()
  282. print("Load")
  283. #delegate loading to logic
  284. self.logic.loadImages(self.currentRow,self.keepCached.isChecked())
  285. self.logic.loadSegmentations(self.currentRow)
  286. self.updateSegmentationViewer(self.logic.segmentationRows)
  287. #self.logic.loadReview(self.currentRow)
  288. #self.logic.loadAE(self.currentRow)
  289. #self.onReviewSegmentChanged()
  290. def generateSegmentationView(self):
  291. s=qt.QHBoxLayout()
  292. nm=qt.QLabel("name")
  293. s.addWidget(nm)
  294. for g in self.logic.segmentList:
  295. t=qt.QCheckBox()
  296. s.addWidget(t)
  297. return s
  298. def setSegmentationViewName(self,s,name):
  299. label=s.itemAt(0).widget()
  300. label.setText(name)
  301. def getWidgetsFromLayout(self,s):
  302. return [s.itemAt(i).widget() for i in range(s.count())]
  303. def hideLayout(self,s):
  304. for w in self.getWidgetsFromLayout(s):
  305. w.hide()
  306. def disconnect(self,s):
  307. for w in self.getWidgetsFromLayout(s)[1:]:
  308. try:
  309. w.stateChanged.disconnect()
  310. except TypeError:
  311. continue
  312. def showLayout(self,s):
  313. for w in self.getWidgetsFromLayout(s):
  314. w.show()
  315. def refreshCallbacks(self,s,entry):
  316. w=self.getWidgetsFromLayout(s)
  317. for i in range(len(self.logic.segmentList)):
  318. w[i+1].stateChanged.connect(functools.partial(self.onSegmentationViewChanged,
  319. latestFile=entry['latestFile'],
  320. segment=self.logic.segmentList[i]))
  321. def getUserMap(self):
  322. users=self.logic.lookups['core:Users']
  323. #print('{}'.format(users))
  324. return users
  325. def updateSegmentationViewer(self,rows):
  326. users=self.getUserMap()
  327. for s in self.segList:
  328. self.hideLayout(s)
  329. self.disconnect(s)
  330. #can't hide a layout
  331. #s.hide()
  332. i=0
  333. for r in rows:
  334. try:
  335. s=self.segList[i]
  336. except IndexError:
  337. #assume we only have to add one (since we iterate sequentially)
  338. self.segList.append(self.generateSegmentationView())
  339. s=self.segList[i]
  340. self.vSegViewLayout.addLayout(s)
  341. self.showLayout(s)
  342. self.setSegmentationViewName(s,users[r['User']])
  343. self.refreshCallbacks(s,r)
  344. i+=1
  345. def onReviewSegmentChanged(self):
  346. pass
  347. def onPatientClearButtonClicked(self):
  348. self.logic.clearVolumesAndSegmentations()
  349. def onPatientSaveButtonClicked(self):
  350. #not used
  351. self.logic.saveSegmentation()
  352. def onCtBoneButtonClicked(self):
  353. self.logic.setWindow('CT:bone')
  354. def onCtAirButtonClicked(self):
  355. self.logic.setWindow('CT:air')
  356. def onCtAbdomenButtonClicked(self):
  357. self.logic.setWindow('CT:abdomen')
  358. def onCtBrainButtonClicked(self):
  359. self.logic.setWindow('CT:brain')
  360. def onCtLungButtonClicked(self):
  361. self.logic.setWindow('CT:lung')
  362. def onSegmentationViewChanged(self,state,latestFile,segment):
  363. print('onSegmentationViewChanged {} {} {} '.format(latestFile,segment,state))
  364. self.logic.toggleSegmentVisibility(latestFile,segment,state>0)
  365. def cleanup(self):
  366. pass
  367. #
  368. # segmentationBrowserLogic
  369. #
  370. class segmentationBrowserLogic(ScriptedLoadableModuleLogic):
  371. """This class should implement all the actual
  372. computation done by your module. The interface
  373. should be such that other python code can import
  374. this class and make use of the functionality without
  375. requiring an instance of the Widget.
  376. Uses ScriptedLoadableModuleLogic base class, available at:
  377. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  378. """
  379. def __init__(self,parent=None):
  380. ScriptedLoadableModuleLogic.__init__(self, parent)
  381. print('segmentationBrowserLogic loading')
  382. if not parent==None:
  383. #use layout and data from parent widget
  384. self.parent=parent
  385. fhome=os.path.expanduser('~')
  386. fsetup=os.path.join(fhome,'.labkey','setup.json')
  387. try:
  388. with open(fsetup) as f:
  389. self.setup=json.load(f)
  390. except FileNotFoundError:
  391. self.setup={}
  392. try:
  393. pt=self.setup['paths']
  394. except KeyError:
  395. self.setup['paths']={}
  396. lName='labkeyInterface'
  397. nixWrapper.loadLibrary(lName)
  398. import labkeyInterface
  399. import labkeyDatabaseBrowser
  400. import labkeyFileBrowser
  401. self.network=labkeyInterface.labkeyInterface()
  402. self.dbBrowser=labkeyDatabaseBrowser
  403. self.fBrowser=labkeyFileBrowser
  404. self.tempDir=os.path.join(os.path.expanduser('~'),'temp')
  405. if not os.path.isdir(self.tempDir):
  406. os.mkdir(self.tempDir)
  407. self.lookups={}
  408. self.segmentList=["Liver","Blood","Marrow","Disease","Deauville"]
  409. print('segmentationBrowserLogic setup complete')
  410. def setServer(self,serverName):
  411. #additional way of setting the labkey network interface
  412. #if no parent was provided in logic initialization (stand-alone mode)
  413. status={}
  414. fileName="NONE"
  415. if serverName=="astuden":
  416. fileName="astuden.json"
  417. if serverName=="adoma":
  418. fileName="adoma.json"
  419. if serverName=="kzevnik":
  420. fileName="kzevnik.json"
  421. if fileName=="NONE":
  422. print("No path was associated with server {}".format(serverName))
  423. status['error']='KEY ERROR'
  424. return status
  425. fconfig=os.path.join(os.path.expanduser('~'),'.labkey',fileName)
  426. self.network.init(fconfig)
  427. self.remoteId=self.network.getUserId()
  428. if self.remoteId==None:
  429. status['error']='ID ERROR'
  430. return status
  431. status['displayName']=self.remoteId['displayName']
  432. status['id']=self.remoteId['id']
  433. #reset db and fb (they are thin classes anyhow)
  434. self.db=self.dbBrowser.labkeyDB(self.network)
  435. self.fb=self.fBrowser.labkeyFileBrowser(self.network)
  436. return status
  437. def setConfig(self,configName):
  438. status={}
  439. fileName=os.path.join(os.path.expanduser('~'),'.labkey',configName)
  440. if not os.path.isfile(fileName):
  441. status['error']='FILE NOT FOUND'
  442. return status
  443. with open(fileName,'r') as f:
  444. self.isetup=json.load(f)
  445. #self.project=self.isetup['project']
  446. #"iPNUMMretro/Study"
  447. #self.schema='study'
  448. #self.dataset=self.isetup['query']
  449. #include filters...
  450. ds=self.getDataset()
  451. ids=[row[self.isetup['participantField']] for row in ds['rows']]
  452. status['ids']=list(set(ids))
  453. return status
  454. def getVarName(self,name="Imaging",var="visitField"):
  455. dset=self.isetup['datasets'][name]
  456. defaults={"visitField":"imagingVisitId"}
  457. try:
  458. return dset[var]
  459. except KeyError:
  460. return defaults[var]
  461. def getDataset(self,name="Imaging",dbFilter={}):
  462. dset=self.isetup['datasets'][name]
  463. project=dset['project']
  464. schema=dset['schema']
  465. query=dset['query']
  466. #add default filters
  467. qFilter=[]
  468. try:
  469. for qf in dset['filter']:
  470. v=dset['filter'][qf]
  471. qFilter.append({'variable':qf,'value':v,'oper':'eq'})
  472. except KeyError:
  473. pass
  474. for f in dbFilter:
  475. if f=='participant':
  476. qFilter.append({'variable':self.isetup['participantField'],
  477. 'value':dbFilter[f],'oper':'eq'})
  478. continue
  479. if f=='seqNum':
  480. qFilter.append({'variable':'SequenceNum',
  481. 'value':'{}'.format(dbFilter[f]),
  482. 'oper':'eq'})
  483. continue
  484. qFilter.append({'variable':f,'value':dbFilter[f],'oper':'eq'})
  485. try:
  486. ds=self.db.selectRows(project,schema,query, \
  487. qFilter,dset['view'])
  488. except KeyError:
  489. ds=self.db.selectRows(project,schema,query,qFilter)
  490. #get lookups as well
  491. lookups={}
  492. for f in ds['metaData']['fields']:
  493. try:
  494. lookup=f['lookup']
  495. except KeyError:
  496. continue
  497. var=f['name']
  498. lookupCode='{}:{}'.format(lookup['schema'],lookup['queryName'])
  499. try:
  500. lookups[var]=self.lookups[lookupCode]
  501. except KeyError:
  502. self.lookups[lookupCode]=self.loadLookup(project,lookup)
  503. lookups[var]=self.lookups[lookupCode]
  504. return {'rows':ds['rows'],'lookups':lookups}
  505. def loadLookup(self,project,lookup):
  506. qData={}
  507. key=lookup['keyColumn']
  508. value=lookup['displayColumn']
  509. fSet=self.db.selectRows(project,lookup['schema'],lookup['queryName'],[])
  510. for q in fSet['rows']:
  511. qData[q[key]]=q[value]
  512. return qData
  513. def loadImage(self,iData):
  514. #unpack iData
  515. idx=iData['idx']
  516. path=iData['path']
  517. keepCached=iData['keepCached']
  518. dset=self.isetup['datasets'][iData['dataset']]
  519. localPath=os.path.join(self.tempDir,path[-1])
  520. if not os.path.isfile(localPath):
  521. #download from server
  522. remotePath=self.fb.formatPathURL(dset['project'],'/'.join(path))
  523. if not self.fb.entryExists(remotePath):
  524. print("Failed to get {}".format(remotePath))
  525. return
  526. self.fb.readFileToFile(remotePath,localPath)
  527. properties={}
  528. filetype='VolumeFile'
  529. #make sure segmentation gets loaded as a labelmap
  530. if idx=="Segmentation":
  531. filetype='SegmentationFile'
  532. #properties["labelmap"]=1
  533. try:
  534. volumeIdx=iData['volumeIdx']
  535. except KeyError:
  536. volumeIdx=idx
  537. self.volumeNode[volumeIdx]=slicer.util.loadNodeFromFile(localPath,
  538. filetype=filetype,properties=properties)
  539. if not keepCached:
  540. os.remove(localPath)
  541. def loadImages(self,row,keepCached):
  542. #fields={'ctResampled':True,'petResampled':False}
  543. fields={"CT":self.parent.ctField.text,\
  544. "PET":self.parent.petField.text}
  545. path=[self.isetup['imageDir'],row['patientCode'],row['visitCode']]
  546. relativePaths={x:path+[row[y]] for (x,y) in fields.items()}
  547. self.volumeNode={}
  548. for f in relativePaths:
  549. iData={'idx':f,'path':relativePaths[f],
  550. 'keepCached':keepCached,'dataset':'Imaging'}
  551. self.loadImage(iData)
  552. #mimic abdominalCT standardized window setting
  553. self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
  554. SetAutoWindowLevel(False)
  555. self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
  556. SetWindowLevel(1400, -500)
  557. #set colormap for PET to PET-Heat (this is a verbatim setting from
  558. #the Volumes->Display->Lookup Table colormap identifier)
  559. self.volumeNode['PET'].GetScalarVolumeDisplayNode().\
  560. SetAndObserveColorNodeID(\
  561. slicer.util.getNode('Inferno').GetID())
  562. slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
  563. foreground=self.volumeNode['PET'],foregroundOpacity=0.5,fit=True)
  564. def loadSegmentations(self,row, loadFile=1):
  565. dbFilter={
  566. #'User':'{}'.format(self.remoteId['id']),
  567. 'participant':row[self.isetup['participantField']],
  568. 'visitCode':row['visitCode']}
  569. ds=self.getDataset(name='SegmentationsMaster',
  570. dbFilter=dbFilter)
  571. self.segmentationRows=ds['rows']
  572. if len(self.segmentationRows)<0:
  573. print('No segmentations for {}'.format(dbFilter))
  574. return
  575. for r in self.segmentationRows:
  576. #update self.segmentationEntry
  577. #self.segmentationEntry=ds['rows'][0]
  578. #self.segmentationEntry['origin']='database'
  579. #if loadFile:
  580. self.loadSegmentationFromEntry(r)
  581. #make invisile by default
  582. for s in self.segmentList:
  583. self.toggleSegmentVisibility(r['latestFile'],s,False)
  584. print('load Segmentation: done')
  585. #create new segmentation
  586. #self.createSegmentation(row)
  587. def getSegmentationPath(self,entry):
  588. path=[self.isetup['imageDir'],
  589. entry['patientCode'],
  590. entry['visitCode']]
  591. path.append('Segmentations')
  592. return path
  593. def loadSegmentationFromEntry(self,r):
  594. #compile path
  595. entry=r
  596. path=self.getSegmentationPath(entry)
  597. path.append(entry['latestFile'])
  598. iData={'idx':'Segmentation','path':path,
  599. 'keepCached':0,'dataset':'SegmentationsMaster',
  600. 'volumeIdx':entry['latestFile']}
  601. self.loadImage(iData)
  602. #look for missing segments
  603. return
  604. #skip this
  605. segNode=self.volumeNode['Segmentation']
  606. seg=segNode.GetSegmentation()
  607. segNames=[seg.GetNthSegmentID(i) for i in range(seg.GetNumberOfSegments())]
  608. print('Segments')
  609. try:
  610. segmentList=self.isetup['segmentList']
  611. except KeyError:
  612. segmentList=self.segmentList
  613. for s in segmentList:
  614. if s not in segNames:
  615. seg.AddEmptySegment(s,s)
  616. print(s)
  617. print('Done')
  618. def toggleSegmentVisibility(self,latestFile,segmentId,visible):
  619. segNode=self.volumeNode[latestFile]
  620. if segNode==None:
  621. print('Could not find segNode')
  622. return
  623. seg=segNode.GetSegmentation()
  624. segment=seg.GetSegment(segmentId)
  625. if segment==None:
  626. print('Could not find segment {}'.format(segmentId))
  627. return
  628. segNode.GetDisplayNode().SetSegmentVisibility(segmentId, visible)
  629. def saveSegmentation(self):
  630. #not used
  631. #get the latest key by adding an entry to SegmentationList
  632. copyFields=[self.isetup['participantField'],'patientCode','visitCode','User']
  633. outRow={x:self.segmentationEntry[x] for x in copyFields}
  634. sList=self.isetup['datasets']['SegmentationsList']
  635. resp=self.db.modifyRows('insert',sList['project'],
  636. sList['schema'],sList['query'],[outRow])
  637. encoding=chardet.detect(resp)['encoding']
  638. respJSON=json.loads(resp.decode(encoding))
  639. outRow=respJSON['rows'][0]
  640. #print(outRow)
  641. #construct file name with known key
  642. uName=re.sub(' ','_',self.remoteId['displayName'])
  643. fName='Segmentation_{}-{}_{}_{}.nrrd'.format(
  644. self.segmentationEntry['patientCode'],
  645. self.segmentationEntry['visitCode'],
  646. uName,outRow['Key'])
  647. path=self.getSegmentationPath()
  648. path.append(fName)
  649. self.saveNode(self.volumeNode['Segmentation'],'SegmentationsMaster',path)
  650. #update SegmentationList with know file name
  651. outRow['Segmentation']=fName
  652. self.db.modifyRows('update',sList['project'],
  653. sList['schema'],sList['query'],[outRow])
  654. #update SegmentationsMaster
  655. self.segmentationEntry['latestFile']=fName
  656. self.segmentationEntry['version']=outRow['Key']
  657. des=self.isetup['datasets']['SegmentationsMaster']
  658. op='insert'
  659. if self.segmentationEntry['origin']=='database':
  660. op='update'
  661. print('Saving: mode={}'.format(op))
  662. resp=self.db.modifyRows(op,des['project'],
  663. des['schema'],des['query'],[self.segmentationEntry])
  664. print(resp)
  665. #since we loaded a version, origin should be set to database
  666. self.loadSegmentation(self.segmentationEntry,0)
  667. #self.segmentationEntry['origin']='database'
  668. def saveNode(self,node,dataset,path):
  669. #not used
  670. fName=path[-1]
  671. localPath=os.path.join(self.tempDir,fName)
  672. slicer.util.saveNode(node,localPath)
  673. dset=self.isetup['datasets'][dataset]
  674. #exclude file name when building directory structure
  675. remotePath=self.fb.buildPathURL(dset['project'],path[:-1])
  676. remotePath+='/'+fName
  677. self.fb.writeFileToFile(localPath,remotePath)
  678. #add entry to segmentation list
  679. def createSegmentation(self,entry):
  680. #not used
  681. #create segmentation entry for database update
  682. #note that origin is not set to database
  683. copyFields=[self.isetup['participantField'],'patientCode','visitCode','SequenceNum']
  684. #copyFields=['ParticipantId','patientCode','visitCode','SequenceNum']
  685. self.segmentationEntry={x:entry[x] for x in copyFields}
  686. self.segmentationEntry['User']=self.remoteId['id']
  687. self.segmentationEntry['origin']='created'
  688. self.segmentationEntry['version']=-1111
  689. #create a segmentation node
  690. uName=re.sub(' ','_',self.remoteId['displayName'])
  691. segNode=slicer.vtkMRMLSegmentationNode()
  692. self.volumeNode['Segmentation']=segNode
  693. segNode.SetName('Segmentation_{}_{}_{}'.
  694. format(entry['patientCode'],entry['visitCode'],uName))
  695. slicer.mrmlScene.AddNode(segNode)
  696. segNode.CreateDefaultDisplayNodes()
  697. segNode.SetReferenceImageGeometryParameterFromVolumeNode(self.volumeNode['PET'])
  698. try:
  699. segmentList=self.isetup['segmentList']
  700. except KeyError:
  701. segmentList=self.segmentList
  702. for s in segmentList:
  703. segNode.GetSegmentation().AddEmptySegment(s,s)
  704. def clearVolumesAndSegmentations(self):
  705. nodes=slicer.util.getNodesByClass("vtkMRMLVolumeNode")
  706. nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
  707. res=[slicer.mrmlScene.RemoveNode(f) for f in nodes]
  708. try:
  709. self.volumeNode.clear()
  710. except AttributeError:
  711. pass
  712. #self.segmentationNode=None
  713. #self.reviewResult={}
  714. #self.aeList={}
  715. def setWindow(self,windowName):
  716. #default
  717. w=1400
  718. l=-500
  719. if windowName=='CT:bone':
  720. w=1000
  721. l=400
  722. if windowName=='CT:air':
  723. w=1000
  724. l=-426
  725. if windowName=='CT:abdomen':
  726. w=350
  727. l=40
  728. if windowName=='CT:brain':
  729. w=100
  730. l=50
  731. if windowName=='CT:lung':
  732. w=1400
  733. l=-500
  734. self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
  735. SetWindowLevel(w,l)
  736. print('setWindow: {} {}/{}'.format(windowName,w,l))
  737. class segmentationBrowserTest(ScriptedLoadableModuleTest):
  738. """
  739. This is the test case for your scripted module.
  740. Uses ScriptedLoadableModuleTest base class, available at:
  741. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  742. """
  743. def setup(self):
  744. """ Do whatever is needed to reset the state - typically a scene clear will be enough.
  745. """
  746. slicer.mrmlScene.Clear(0)
  747. def runTest(self):
  748. """Run as few or as many tests as needed here.
  749. """
  750. self.setUp()
  751. self.test_irAEMMBrowser()
  752. def test_irAEMMBrowser(self):
  753. """ Ideally you should have several levels of tests. At the lowest level
  754. tests sould exercise the functionality of the logic with different inputs
  755. (both valid and invalid). At higher levels your tests should emulate the
  756. way the user would interact with your code and confirm that it still works
  757. the way you intended.
  758. One of the most important features of the tests is that it should alert other
  759. developers when their changes will have an impact on the behavior of your
  760. module. For example, if a developer removes a feature that you depend on,
  761. your test should break so they know that the feature is needed.
  762. """
  763. self.delayDisplay("Starting the test")
  764. #
  765. # first, get some data
  766. #