imageBrowser.py 33 KB

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