imageBrowser.py 32 KB

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