|
@@ -0,0 +1,802 @@
|
|
|
+import os
|
|
|
+import unittest
|
|
|
+from __main__ import vtk, qt, ctk, slicer
|
|
|
+from slicer.ScriptedLoadableModule import *
|
|
|
+import json
|
|
|
+import datetime
|
|
|
+import sys
|
|
|
+
|
|
|
+#
|
|
|
+# labkeySlicerPythonExtension
|
|
|
+#
|
|
|
+
|
|
|
+class imageBrowser(ScriptedLoadableModule):
|
|
|
+ """Uses ScriptedLoadableModule base class, available at:
|
|
|
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, parent):
|
|
|
+ ScriptedLoadableModule.__init__(self, parent)
|
|
|
+ self.parent.title = "irAEMM Browser" # TODO make this more human readable by adding spaces
|
|
|
+ self.parent.categories = ["LabKey"]
|
|
|
+ self.parent.dependencies = []
|
|
|
+ self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
|
|
|
+ self.parent.helpText = """
|
|
|
+ Interface to irAEMM files in LabKey
|
|
|
+ """
|
|
|
+ self.parent.acknowledgementText = """
|
|
|
+ Developed within the medical physics research programme of the Slovenian research agency.
|
|
|
+ """ # replace with organization, grant and thanks.
|
|
|
+
|
|
|
+#
|
|
|
+# labkeySlicerPythonExtensionWidget
|
|
|
+#
|
|
|
+
|
|
|
+class imageBrowserWidget(ScriptedLoadableModuleWidget):
|
|
|
+ """Uses ScriptedLoadableModuleWidget base class, available at:
|
|
|
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
|
|
|
+ """
|
|
|
+
|
|
|
+ def setup(self):
|
|
|
+ print("Setting up imageBrowserWidget")
|
|
|
+ ScriptedLoadableModuleWidget.setup(self)
|
|
|
+ # Instantiate and connect widgets ...
|
|
|
+
|
|
|
+ fhome=os.path.expanduser('~')
|
|
|
+ fsetup=os.path.join(fhome,'.labkey','setup.json')
|
|
|
+ try:
|
|
|
+ with open(fsetup) as f:
|
|
|
+ self.setup=json.load(f)
|
|
|
+ except FileNotFoundError:
|
|
|
+ self.setup={}
|
|
|
+
|
|
|
+ try:
|
|
|
+ pt=self.setup['paths']
|
|
|
+ except KeyError:
|
|
|
+ self.setup['paths']={}
|
|
|
+
|
|
|
+ lName='labkeyInterface'
|
|
|
+ try:
|
|
|
+ sys.path.append(self.setup['paths'][lName])
|
|
|
+
|
|
|
+ except KeyError:
|
|
|
+ self.setup['paths'][lName]=loadLibrary(lName)
|
|
|
+
|
|
|
+ with open(fsetup,'w') as f:
|
|
|
+ json.dump(self.setup,f,indent='\t')
|
|
|
+
|
|
|
+ import labkeyInterface
|
|
|
+ import labkeyDatabaseBrowser
|
|
|
+ import labkeyFileBrowser
|
|
|
+
|
|
|
+
|
|
|
+ self.network=labkeyInterface.labkeyInterface()
|
|
|
+
|
|
|
+
|
|
|
+ setupCollapsibleButton = ctk.ctkCollapsibleButton()
|
|
|
+ setupCollapsibleButton.text = "Setup"
|
|
|
+ self.layout.addWidget(setupCollapsibleButton)
|
|
|
+
|
|
|
+
|
|
|
+ #Form layout (maybe one can think of more intuitive layouts)
|
|
|
+ setupFormLayout = qt.QFormLayout(setupCollapsibleButton)
|
|
|
+
|
|
|
+ self.serverList=qt.QComboBox()
|
|
|
+ self.serverList.addItem("ONKO-NIX")
|
|
|
+ self.serverList.currentIndexChanged.connect(self.onServerListChanged)
|
|
|
+ setupFormLayout.addRow("Database:",self.serverList)
|
|
|
+
|
|
|
+
|
|
|
+ self.setupList=qt.QComboBox()
|
|
|
+ self.setupList.addItem("limfomiPET_iBrowser.json")
|
|
|
+ self.setupList.currentIndexChanged.connect(self.onSetupListChanged)
|
|
|
+ setupFormLayout.addRow("Setup:",self.setupList)
|
|
|
+
|
|
|
+
|
|
|
+ def onSetupListChanged(self,i):
|
|
|
+ fileName=os.path.join(os.path.expanduser('~'),'.labkey',fName)
|
|
|
+ with open(fileName,'r') as f:
|
|
|
+ self.isetup=json.load(f)
|
|
|
+
|
|
|
+
|
|
|
+ self.project=self.isetup['project']"iPNUMMretro/Study"
|
|
|
+ self.schema='study'
|
|
|
+ self.dataset=self.isetup['query']
|
|
|
+
|
|
|
+ view=self.isetup['view']
|
|
|
+ try:
|
|
|
+ ds=self.db.selectRows(self.project,self.schema,self.dataset,
|
|
|
+ [],self.isetup['view'])
|
|
|
+ except KeyError:
|
|
|
+ ds=self.db.selectRows(self.project,self.schema,self.dataset,[])
|
|
|
+
|
|
|
+ ids=[row['PatientId'] for row in ds['rows']]
|
|
|
+ ids=list(set(ids))
|
|
|
+
|
|
|
+
|
|
|
+ #
|
|
|
+ # Setup Area
|
|
|
+ #
|
|
|
+
|
|
|
+ #this comes up with the database
|
|
|
+
|
|
|
+ self.participantField=qt.QLabel("PatientId")
|
|
|
+ setupFormLayout.addRow("Participant field:",self.participantField)
|
|
|
+
|
|
|
+ self.ctField=qt.QLabel("ctResampled")
|
|
|
+ setupFormLayout.addRow("Data field (CT):",self.ctField)
|
|
|
+
|
|
|
+ self.petField=qt.QLabel("petResampled")
|
|
|
+ setupFormLayout.addRow("Data field (PET):",self.petField)
|
|
|
+
|
|
|
+ self.segmentationField=qt.QLabel("Segmentation")
|
|
|
+ setupFormLayout.addRow("Data field (Segmentation):",self.segmentationField)
|
|
|
+
|
|
|
+
|
|
|
+ self.idField=qt.QLabel(self.network.getUserId()['displayName'])
|
|
|
+ setupFormLayout.addRow("ID",self.idField)
|
|
|
+ self.logic=imageBrowserLogic(self)
|
|
|
+
|
|
|
+
|
|
|
+ #
|
|
|
+ # Patients Area
|
|
|
+ #
|
|
|
+ patientsCollapsibleButton = ctk.ctkCollapsibleButton()
|
|
|
+ patientsCollapsibleButton.text = "Patients"
|
|
|
+
|
|
|
+ #don't add it yet
|
|
|
+ #self.layout.addWidget(patientsCollapsibleButton)
|
|
|
+
|
|
|
+ patientsFormLayout = qt.QFormLayout(patientsCollapsibleButton)
|
|
|
+
|
|
|
+
|
|
|
+ self.patientList=qt.QComboBox()
|
|
|
+ for id in ids:
|
|
|
+ self.patientList.addItem(id)
|
|
|
+ self.patientList.currentIndexChanged.connect(self.onPatientListChanged)
|
|
|
+ patientsFormLayout.addRow("Patient:",self.patientList)
|
|
|
+
|
|
|
+ self.visitList=qt.QComboBox()
|
|
|
+ self.visitList.currentIndexChanged.connect(self.onVisitListChanged)
|
|
|
+ patientsFormLayout.addRow("Visit:",self.visitList)
|
|
|
+
|
|
|
+
|
|
|
+ self.ctCode=qt.QLabel("ctCode")
|
|
|
+ patientsFormLayout.addRow("CT:",self.ctCode)
|
|
|
+
|
|
|
+ self.petCode=qt.QLabel("petCode")
|
|
|
+ patientsFormLayout.addRow("PET:",self.petCode)
|
|
|
+
|
|
|
+ self.segmentationCode=qt.QLabel("segmentationCode")
|
|
|
+ patientsFormLayout.addRow("Segmentation",self.segmentationCode)
|
|
|
+
|
|
|
+ self.patientLoad=qt.QPushButton("Load")
|
|
|
+ self.patientLoad.clicked.connect(self.onPatientLoadButtonClicked)
|
|
|
+ patientsFormLayout.addRow("Load patient",self.patientLoad)
|
|
|
+
|
|
|
+ self.patientClear=qt.QPushButton("Clear")
|
|
|
+ self.patientClear.clicked.connect(self.onPatientClearButtonClicked)
|
|
|
+ patientsFormLayout.addRow("Clear patient",self.patientClear)
|
|
|
+
|
|
|
+ self.keepCached=qt.QCheckBox("keep Cached")
|
|
|
+ self.keepCached.setChecked(1)
|
|
|
+ patientsFormLayout.addRow("Keep cached",self.keepCached)
|
|
|
+
|
|
|
+ #set to a defined state
|
|
|
+ self.onPatientListChanged(0)
|
|
|
+
|
|
|
+
|
|
|
+ #
|
|
|
+ # Review Area
|
|
|
+ #
|
|
|
+ reviewCollapsibleButton = ctk.ctkCollapsibleButton()
|
|
|
+ reviewCollapsibleButton.text = "Review"
|
|
|
+ self.layout.addWidget(reviewCollapsibleButton)
|
|
|
+
|
|
|
+ self.reviewBoxLayout = qt.QVBoxLayout(reviewCollapsibleButton)
|
|
|
+
|
|
|
+ self.reviewFormLayout = qt.QFormLayout()
|
|
|
+
|
|
|
+
|
|
|
+ self.reviewSegment=qt.QComboBox()
|
|
|
+ self.reviewSegment.currentIndexChanged.connect(self.onReviewSegmentChanged)
|
|
|
+ self.reviewFormLayout.addRow("Selected region:",self.reviewSegment)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ self.reviewResult=qt.QComboBox()
|
|
|
+ self.reviewFormLayout.addRow("What do you think about the segmentation:",\
|
|
|
+ self.reviewResult)
|
|
|
+ reviewOptions=['Select','Excellent','Minor deficiencies',\
|
|
|
+ 'Major deficiencies','Unusable']
|
|
|
+ for opt in reviewOptions:
|
|
|
+ self.reviewResult.addItem(opt)
|
|
|
+
|
|
|
+ self.aeResult=qt.QComboBox()
|
|
|
+ self.reviewFormLayout.addRow("Is organ suffering from adverse effect?",\
|
|
|
+ self.aeResult)
|
|
|
+ aeOptions=['Select','Yes','No']
|
|
|
+ for opt in aeOptions:
|
|
|
+ self.aeResult.addItem(opt)
|
|
|
+ #self.aeResult.setCurrentIndex(0)
|
|
|
+
|
|
|
+ self.updateReview=qt.QPushButton("Save")
|
|
|
+ self.reviewFormLayout.\
|
|
|
+ addRow("Save segmentation and AE decision for current segment",\
|
|
|
+ self.updateReview)
|
|
|
+ self.updateReview.clicked.connect(self.onUpdateReviewButtonClicked)
|
|
|
+
|
|
|
+ self.reviewBoxLayout.addLayout(self.reviewFormLayout)
|
|
|
+
|
|
|
+ submitFrame=qt.QGroupBox("Submit data")
|
|
|
+
|
|
|
+ self.submitFormLayout=qt.QFormLayout()
|
|
|
+
|
|
|
+ self.reviewComment=qt.QTextEdit("this is a test")
|
|
|
+ self.submitFormLayout.addRow("Comments (optional)",\
|
|
|
+ self.reviewComment)
|
|
|
+
|
|
|
+ self.submitReviewButton=qt.QPushButton("Submit")
|
|
|
+ self.submitFormLayout.addRow("Submit to database",\
|
|
|
+ self.submitReviewButton)
|
|
|
+ self.submitReviewButton.clicked.connect(self.onSubmitReviewButtonClicked)
|
|
|
+
|
|
|
+ submitFrame.setLayout(self.submitFormLayout)
|
|
|
+ submitFrame.setFlat(1)
|
|
|
+ #submitFrame.setFrameShape(qt.QFrame.StyledPanel)
|
|
|
+ #submitFrame.setFrameShadow(qt.QFrame.Sunken)
|
|
|
+ submitFrame.setStyleSheet("background-color:rgba(220,215,180,45)")
|
|
|
+ self.reviewBoxLayout.addWidget(submitFrame)
|
|
|
+
|
|
|
+ def onServerListChanged(self,i):
|
|
|
+ serverName=self.serverList.currentText
|
|
|
+ fileName="NONE"
|
|
|
+ if serverName=="ONKO-NIX":
|
|
|
+ fileName="onko-nix.json"
|
|
|
+ if fileName=="NONE":
|
|
|
+ print("No path was associated with server {}".format(serverName))
|
|
|
+ self.serverList.setStyleSheet('background-color: violet')
|
|
|
+ return
|
|
|
+ fconfig=os.path.join(os.path.expanduser('~'),'.labkey',fileName)
|
|
|
+ self.network.init(fconfig)
|
|
|
+ remoteId=self.network.getUserId()
|
|
|
+ if remoteId==None:
|
|
|
+ self.serverList.setStyleSheet('background-color: red')
|
|
|
+ return
|
|
|
+
|
|
|
+ self.serverList.setStyleSheet('background-color: green')
|
|
|
+ self.db=labkeyDatabaseBrowser.labkeyDB(self.network)
|
|
|
+ self.fb=labkeyFileBrowser.labkeyFileBrowser(self.network)
|
|
|
+
|
|
|
+def onPatientListChanged(self,i):
|
|
|
+ idFilter={'variable':'PatientId','value':self.patientList.currentText,'oper':'eq'}
|
|
|
+ ds=self.db.selectRows(self.project,self.schema,self.dataset, [idFilter],"segmentationReview")
|
|
|
+ seq=[int(row['SequenceNum']) for row in ds['rows']]
|
|
|
+ self.visitList.clear()
|
|
|
+
|
|
|
+ for s in seq:
|
|
|
+ self.visitList.addItem("Visit "+str(s))
|
|
|
+ self.onVisitListChanged(0)
|
|
|
+
|
|
|
+ def onVisitListChanged(self,i):
|
|
|
+ try:
|
|
|
+ s=self.visitList.currentText.split(' ')[1]
|
|
|
+ except IndexError:
|
|
|
+ return
|
|
|
+ print("Visit: Selected item: {}->{}".format(i,s))
|
|
|
+ idFilter={'variable':'PatientId',\
|
|
|
+ 'value':self.patientList.currentText,'oper':'eq'}
|
|
|
+ sFilter={'variable':'SequenceNum','value':s,'oper':'eq'}
|
|
|
+ ds=self.db.selectRows(self.project,self.schema,self.dataset,[idFilter,sFilter],"segmentationReview")
|
|
|
+ if not len(ds['rows'])==1:
|
|
|
+ print("Found incorrect number {} of matches for [{}]/[{}]".\
|
|
|
+ format(len(ds['rows']),\
|
|
|
+ self.patientList.currentText,s))
|
|
|
+ row=ds['rows'][0]
|
|
|
+
|
|
|
+ #copy row properties for data access
|
|
|
+ self.currentRow=row
|
|
|
+ self.petCode.setText(row[self.petField.text])
|
|
|
+ self.ctCode.setText(row[self.ctField.text])
|
|
|
+ self.segmentationCode.setText(row[self.segmentationField.text])
|
|
|
+
|
|
|
+ def onPatientLoadButtonClicked(self):
|
|
|
+ print("Load")
|
|
|
+ #delegate loading to logic
|
|
|
+ #try:
|
|
|
+ self.logic.loadImage(self.currentRow,self.keepCached.isChecked())
|
|
|
+ segmentList=self.logic.compileSegmentation()
|
|
|
+ #also bladder,vertebraL1, stomach, heart
|
|
|
+ for seg in segmentList:
|
|
|
+ if not seg in self.segmentList:
|
|
|
+ continue
|
|
|
+ #filter to most important ones
|
|
|
+ self.reviewSegment.addItem(seg)
|
|
|
+ self.logic.loadReview(self.currentRow)
|
|
|
+ self.logic.loadAE(self.currentRow)
|
|
|
+ for segment in self.segmentList:
|
|
|
+ rIdx=self.logic.getReviewResult(segment)
|
|
|
+ aIdx=self.logic.getAEResult(segment)
|
|
|
+ print("Segment {}: {}/{}".format(segment,rIdx,aIdx))
|
|
|
+ try:
|
|
|
+ if (rIdx+aIdx)>0:
|
|
|
+ self.updateResult(segment,rIdx,aIdx)
|
|
|
+ except TypeError:
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ self.reviewComment.setPlainText(self.logic.reviewComment)
|
|
|
+ except AttributeError:
|
|
|
+ pass
|
|
|
+
|
|
|
+ self.onReviewSegmentChanged()
|
|
|
+ #except AttributeError:
|
|
|
+ # print("Missing current row")
|
|
|
+ # return
|
|
|
+
|
|
|
+ def onReviewSegmentChanged(self):
|
|
|
+ segment=self.reviewSegment.currentText
|
|
|
+ self.logic.hideSegments()
|
|
|
+ self.logic.showSegment(segment)
|
|
|
+ #set reviewFlag to stored value
|
|
|
+ self.reviewResult.setCurrentIndex(self.logic.getReviewResult(segment))
|
|
|
+ self.aeResult.setCurrentIndex(self.logic.getAEResult(segment))
|
|
|
+
|
|
|
+ def onSubmitReviewButtonClicked(self):
|
|
|
+ print("Submit")
|
|
|
+ print("Selected review:{}/{}".format(self.reviewResult.currentIndex,
|
|
|
+ self.reviewResult.currentText))
|
|
|
+ print("Comment:{}".format(self.reviewComment))
|
|
|
+ self.logic.submitReview(self.currentRow,\
|
|
|
+ self.reviewComment.plainText)
|
|
|
+ self.logic.submitAE(self.currentRow)
|
|
|
+
|
|
|
+ def onUpdateReviewButtonClicked(self):
|
|
|
+ print("Save")
|
|
|
+
|
|
|
+ segment=self.reviewSegment.currentText
|
|
|
+ self.logic.updateReview(segment,\
|
|
|
+ self.reviewResult.currentIndex)
|
|
|
+
|
|
|
+ self.logic.updateAE(segment,\
|
|
|
+ self.aeResult.currentIndex)
|
|
|
+ self.updateResult(segment,self.reviewResult.currentIndex,\
|
|
|
+ self.aeResult.currentIndex)
|
|
|
+
|
|
|
+
|
|
|
+ def updateResult(self,segment,reviewResult,aeResult):
|
|
|
+ reviewText=self.reviewResult.itemText(reviewResult)
|
|
|
+ aeText=self.aeResult.itemText(aeResult)
|
|
|
+
|
|
|
+ idx=self.findCompletedSegment(segment)
|
|
|
+
|
|
|
+ if idx<0:
|
|
|
+ qReview=qt.QLabel(reviewText)
|
|
|
+ self.submitFormLayout.insertRow(0,segment,qReview)
|
|
|
+ qAE=qt.QLabel(aeText)
|
|
|
+ self.submitFormLayout.insertRow(1,segment+'AE',qAE)
|
|
|
+ try:
|
|
|
+ self.segmentsCompleted.append(segment)
|
|
|
+ self.segmentsCompleted.append(segment+'AE')
|
|
|
+ except AttributeError:
|
|
|
+ self.segmentsCompleted=[]
|
|
|
+ self.segmentsCompleted.append(segment)
|
|
|
+ self.segmentsCompleted.append(segment+'AE')
|
|
|
+ else:
|
|
|
+ qReview=self.submitFormLayout.itemAt(idx,1).widget()
|
|
|
+ qReview.setText(reviewText)
|
|
|
+ qAE=self.submitFormLayout.itemAt(idx+1,1).widget()
|
|
|
+ qAE.setText(aeText)
|
|
|
+
|
|
|
+ reviewColors=['pink','green','yellow','orange','red']
|
|
|
+ qReview.setStyleSheet("background-color: "+reviewColors[reviewResult])
|
|
|
+ aeColors=['pink','red','green']
|
|
|
+ qAE.setStyleSheet("background-color: "+aeColors[aeResult])
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ def findCompletedSegment(self,segment):
|
|
|
+
|
|
|
+ for i in range(self.submitFormLayout.rowCount()):
|
|
|
+ if self.submitFormLayout.itemAt(i,0).widget().text==segment:
|
|
|
+ return i
|
|
|
+ return -1
|
|
|
+
|
|
|
+ def removeCompletedSegments(self):
|
|
|
+
|
|
|
+ try:
|
|
|
+ segments=self.segmentsCompleted
|
|
|
+ except AttributeError:
|
|
|
+ return
|
|
|
+
|
|
|
+ for seg in segments:
|
|
|
+ idx=self.findCompletedSegment(seg)
|
|
|
+ if idx>-1:
|
|
|
+ self.submitFormLayout.removeRow(idx)
|
|
|
+
|
|
|
+ self.segmentsCompleted=[]
|
|
|
+
|
|
|
+
|
|
|
+ def onPatientClearButtonClicked(self):
|
|
|
+ self.logic.clearVolumesAndSegmentations()
|
|
|
+ self.reviewSegment.clear()
|
|
|
+ self.removeCompletedSegments()
|
|
|
+ self.reviewComment.clear()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ def cleanup(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+#
|
|
|
+# dbBrowserLogic
|
|
|
+#
|
|
|
+
|
|
|
+class imageBrowserLogic(ScriptedLoadableModuleLogic):
|
|
|
+ """This class should implement all the actual
|
|
|
+ computation done by your module. The interface
|
|
|
+ should be such that other python code can import
|
|
|
+ this class and make use of the functionality without
|
|
|
+ requiring an instance of the Widget.
|
|
|
+ Uses ScriptedLoadableModuleLogic base class, available at:
|
|
|
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
|
|
|
+ """
|
|
|
+ def __init__(self,parent=None):
|
|
|
+ ScriptedLoadableModuleLogic.__init__(self, parent)
|
|
|
+ if not parent==None:
|
|
|
+ #assume parent has the network set up
|
|
|
+ self.parent=parent
|
|
|
+ self.net=parent.network
|
|
|
+ self.db=parent.db
|
|
|
+ self.fb=parent.fb
|
|
|
+ self.project=parent.project
|
|
|
+ self.schema=parent.schema
|
|
|
+ self.participantField=parent.participantField.text
|
|
|
+ self.segmentList=parent.segmentList
|
|
|
+
|
|
|
+ self.segLabel={'1':'liver','2':'spleen','3':'lung','4':'thyroid',\
|
|
|
+ '5':'kidney','6':'pancreas','7':'gallbladder','8':'bladder',\
|
|
|
+ '9':'aorta','10':'trachea','11':'sternum','12':'vertebraL1',\
|
|
|
+ '13':'adrenal','14':'psoasMajor','15':'rectus',\
|
|
|
+ '16':'bowel','17':'stomach','18':'heart'}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ def setLabkeyInterface(self,net):
|
|
|
+ #additional way of setting the labkey network interface
|
|
|
+ #if no parent was provided in logic initialization (stand-alone mode)
|
|
|
+ self.net=net
|
|
|
+
|
|
|
+ def setLabkeyProject(self,project):
|
|
|
+ self.project=project
|
|
|
+
|
|
|
+ def loadImage(self,row,keepCached):
|
|
|
+
|
|
|
+ tempDir=os.path.join(os.path.expanduser('~'),'temp')
|
|
|
+ if not os.path.isdir(tempDir):
|
|
|
+ os.mkdir(tempDir)
|
|
|
+
|
|
|
+ #fields={'ctResampled':True,'petResampled':False}
|
|
|
+ fields={"CT":self.parent.ctField.text,\
|
|
|
+ "PET":self.parent.petField.text,\
|
|
|
+ "Segmentation":self.parent.segmentationField.text}
|
|
|
+
|
|
|
+ relativePaths={x:['preprocessedImages',row['patientCode'],row['visitCode'],row[y]]\
|
|
|
+ for (x,y) in fields.items()}
|
|
|
+
|
|
|
+ self.volumeNode={}
|
|
|
+ for f in relativePaths:
|
|
|
+
|
|
|
+ p=relativePaths[f]
|
|
|
+
|
|
|
+ localPath=os.path.join(tempDir,p[-1])
|
|
|
+
|
|
|
+ if not os.path.isfile(localPath):
|
|
|
+ #download from server
|
|
|
+ remotePath=self.fb.formatPathURL(self.project,'/'.join(p))
|
|
|
+ if not self.fb.entryExists(remotePath):
|
|
|
+ print("Failed to get {}".format(remotePath))
|
|
|
+ continue
|
|
|
+
|
|
|
+ self.fb.readFileToFile(remotePath,localPath)
|
|
|
+
|
|
|
+ properties={}
|
|
|
+ #make sure segmentation gets loaded as a labelmap
|
|
|
+ if f=="Segmentation":
|
|
|
+ properties["labelmap"]=1
|
|
|
+
|
|
|
+
|
|
|
+ self.volumeNode[f]=slicer.util.loadNodeFromFile(localPath,filetype='VolumeFile',properties=properties)
|
|
|
+
|
|
|
+ if not keepCached:
|
|
|
+ os.remove(localPath)
|
|
|
+
|
|
|
+ #mimic abdominalCT standardized window setting
|
|
|
+ self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
|
|
|
+ SetWindowLevel(1400, -500)
|
|
|
+ #set colormap for PET to PET-Heat (this is a verbatim setting from
|
|
|
+ #the Volumes->Display->Lookup Table colormap identifier)
|
|
|
+ self.volumeNode['PET'].GetScalarVolumeDisplayNode().\
|
|
|
+ SetAndObserveColorNodeID(\
|
|
|
+ slicer.util.getNode('PET-Heat').GetID())
|
|
|
+ slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
|
|
|
+ foreground=self.volumeNode['PET'],foregroundOpacity=0.1,fit=True)
|
|
|
+
|
|
|
+
|
|
|
+ #segmentations
|
|
|
+
|
|
|
+ def compileSegmentation(self):
|
|
|
+ try:
|
|
|
+ labelmapVolumeNode = self.volumeNode['Segmentation']
|
|
|
+ except KeyError:
|
|
|
+ print("No segmentaion volumeNode available")
|
|
|
+ return
|
|
|
+
|
|
|
+ self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
|
|
|
+ slicer.modules.segmentations.logic().\
|
|
|
+ ImportLabelmapToSegmentationNode(labelmapVolumeNode, self.segmentationNode)
|
|
|
+
|
|
|
+
|
|
|
+ segmentList=[]
|
|
|
+
|
|
|
+ seg=self.segmentationNode.GetSegmentation()
|
|
|
+
|
|
|
+ for i in range(seg.GetNumberOfSegments()):
|
|
|
+ segment=seg.GetNthSegment(i)
|
|
|
+ segment.SetName(self.segLabel[segment.GetName()])
|
|
|
+ segmentList.append(segment.GetName())
|
|
|
+
|
|
|
+ #seg.CreateClosedSurfaceRepresentation()
|
|
|
+ slicer.mrmlScene.RemoveNode(labelmapVolumeNode)
|
|
|
+ self.volumeNode.pop('Segmentation',None)
|
|
|
+
|
|
|
+ #return list of segment names
|
|
|
+ return segmentList
|
|
|
+
|
|
|
+ def hideSegments(self):
|
|
|
+
|
|
|
+ try:
|
|
|
+ displayNode=self.segmentationNode.GetDisplayNode()
|
|
|
+ except AttributeError:
|
|
|
+ return
|
|
|
+
|
|
|
+ seg=self.segmentationNode.GetSegmentation()
|
|
|
+ for i in range(seg.GetNumberOfSegments()):
|
|
|
+ #segment=self.segmentationNode.GetSegmentation().GetNthSegment(i)
|
|
|
+ segmentID=seg.GetNthSegmentID(i)
|
|
|
+ displayNode.SetSegmentVisibility(segmentID, False)
|
|
|
+ #print("Done")
|
|
|
+
|
|
|
+ def showSegment(self,name):
|
|
|
+ try:
|
|
|
+ displayNode=self.segmentationNode.GetDisplayNode()
|
|
|
+ except AttributeError:
|
|
|
+ return
|
|
|
+
|
|
|
+ seg=self.segmentationNode.GetSegmentation()
|
|
|
+ for i in range(seg.GetNumberOfSegments()):
|
|
|
+ segment=seg.GetNthSegment(i)
|
|
|
+ if not segment.GetName()==name:
|
|
|
+ continue
|
|
|
+ segmentID=seg.GetNthSegmentID(i)
|
|
|
+ displayNode.SetSegmentVisibility(segmentID, True)
|
|
|
+ break
|
|
|
+ #print("Done")
|
|
|
+
|
|
|
+
|
|
|
+ #clear
|
|
|
+
|
|
|
+ def clearVolumesAndSegmentations(self):
|
|
|
+ nodes=slicer.util.getNodesByClass("vtkMRMLVolumeNode")
|
|
|
+ nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
|
|
|
+ res=[slicer.mrmlScene.RemoveNode(f) for f in nodes]
|
|
|
+ self.segmentationNode=None
|
|
|
+ self.reviewResult={}
|
|
|
+ self.aeList={}
|
|
|
+
|
|
|
+ #reviews by segment
|
|
|
+
|
|
|
+ def updateReview(self,segment,value):
|
|
|
+ try:
|
|
|
+ self.reviewResult[segment]=value
|
|
|
+ except AttributeError:
|
|
|
+ self.reviewResult={}
|
|
|
+ self.updateReview(segment,value)
|
|
|
+
|
|
|
+ def getReviewResult(self,segment):
|
|
|
+ try:
|
|
|
+ return self.reviewResult[segment]
|
|
|
+ except AttributeError:
|
|
|
+ #review result not initialized
|
|
|
+ return 0
|
|
|
+ except KeyError:
|
|
|
+ #segment not done yet
|
|
|
+ return 0
|
|
|
+
|
|
|
+ #load review from labkey
|
|
|
+
|
|
|
+ def getUniqueRows(self, project, dataset, fields, inputRow):
|
|
|
+ filters=[]
|
|
|
+
|
|
|
+ for f in fields:
|
|
|
+ filters.append({'variable':f,'value':str(inputRow[f]),'oper':'eq'})
|
|
|
+
|
|
|
+
|
|
|
+ ds=self.db.selectRows(project,self.schema,dataset,filters)
|
|
|
+ return ds['rows']
|
|
|
+
|
|
|
+ def loadReview(self,currentRow):
|
|
|
+
|
|
|
+ #see if we have already done a review
|
|
|
+ currentRow['ModifiedBy']=self.net.getUserId()['id']
|
|
|
+ rows=self.getUniqueRows(self.parent.project,self.parent.reviewDataset,\
|
|
|
+ [self.participantField,'visitCode','Segmentation','ModifiedBy'],currentRow)
|
|
|
+
|
|
|
+ if len(rows)==0:
|
|
|
+ return
|
|
|
+
|
|
|
+ row=rows[0]
|
|
|
+ for label in self.segmentList:
|
|
|
+ #name=self.segLabel[label]+'Review'
|
|
|
+ name=label+'Review'
|
|
|
+ try:
|
|
|
+ self.updateReview(label,row[name])
|
|
|
+ except KeyError:
|
|
|
+ continue
|
|
|
+ self.reviewComment=row['reviewComment']
|
|
|
+
|
|
|
+ #submit review to labkey
|
|
|
+ def submitReview(self,currentRow,comment):
|
|
|
+ currentRow['ModifiedBy']=self.net.getUserId()['id']
|
|
|
+ fields=[self.participantField,'visitCode','Segmentation','ModifiedBy']
|
|
|
+ rows=self.getUniqueRows(self.parent.project,self.parent.reviewDataset,\
|
|
|
+ fields,currentRow)
|
|
|
+
|
|
|
+
|
|
|
+ mode='update'
|
|
|
+
|
|
|
+ if len(rows)==0:
|
|
|
+ mode='insert'
|
|
|
+ row={}
|
|
|
+ for f in fields:
|
|
|
+ row[f]=currentRow[f]
|
|
|
+
|
|
|
+ frows=self.getUniqueRows(self.parent.project,self.parent.reviewDataset,\
|
|
|
+ [self.participantField,'visitCode'],currentRow)
|
|
|
+
|
|
|
+ row['SequenceNum']=currentRow['SequenceNum']+0.01*len(frows)
|
|
|
+
|
|
|
+ else:
|
|
|
+ row=rows[0]
|
|
|
+
|
|
|
+ seg=self.segmentationNode.GetSegmentation()
|
|
|
+
|
|
|
+ for i in range(seg.GetNumberOfSegments()):
|
|
|
+ segment=seg.GetNthSegment(i)
|
|
|
+ fieldName=segment.GetName()+'Review'
|
|
|
+ value=self.getReviewResult(segment.GetName())
|
|
|
+ row[fieldName]=value
|
|
|
+
|
|
|
+ row['reviewComment']=comment
|
|
|
+ row['Date']=datetime.datetime.now().ctime()
|
|
|
+ self.db.modifyRows(mode,self.parent.project,self.parent.schema,\
|
|
|
+ self.parent.reviewDataset,[row])
|
|
|
+
|
|
|
+ print("review submitted")
|
|
|
+
|
|
|
+
|
|
|
+#AE management
|
|
|
+ def updateAE(self,segment,value):
|
|
|
+ try:
|
|
|
+ self.aeList[segment]=value
|
|
|
+ except AttributeError:
|
|
|
+ self.aeList={}
|
|
|
+ self.updateAE(segment,value)
|
|
|
+
|
|
|
+ def getAEResult(self,segment):
|
|
|
+ try:
|
|
|
+ return self.aeList[segment]
|
|
|
+ except AttributeError:
|
|
|
+ #review result not initialized (unknown)
|
|
|
+ return 0
|
|
|
+ except KeyError:
|
|
|
+ #segment not done yet (unknown)
|
|
|
+ return 0
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ def loadAE(self,currentRow):
|
|
|
+ currentRow['ModifiedBy']=self.net.getUserId()['id']
|
|
|
+ fields=[self.participantField,'petResampled','ModifiedBy']
|
|
|
+ rows=self.getUniqueRows(self.parent.project,self.parent.aeDataset,\
|
|
|
+ fields,currentRow)
|
|
|
+
|
|
|
+ if len(rows)==0:
|
|
|
+ return
|
|
|
+
|
|
|
+ print("Found {} rows".format(len(rows)))
|
|
|
+ row=rows[0]
|
|
|
+
|
|
|
+ for seg in self.segmentList:
|
|
|
+ name=seg+'AE'
|
|
|
+ try:
|
|
|
+ self.updateAE(seg,row[name])
|
|
|
+ except AttributeError:
|
|
|
+ continue
|
|
|
+ except KeyError:
|
|
|
+ continue
|
|
|
+
|
|
|
+ def submitAE(self,currentRow):
|
|
|
+ currentRow['ModifiedBy']=self.net.getUserId()['id']
|
|
|
+ fields=[self.participantField,'petResampled','ModifiedBy']
|
|
|
+ rows=self.getUniqueRows(self.parent.project,self.parent.aeDataset,\
|
|
|
+ fields,currentRow)
|
|
|
+
|
|
|
+
|
|
|
+ if len(rows)==0:
|
|
|
+ mode='insert'
|
|
|
+ row={}
|
|
|
+ for f in fields:
|
|
|
+ row[f]=currentRow[f]
|
|
|
+
|
|
|
+ frows=self.getUniqueRows(self.parent.project,self.parent.aeDataset,\
|
|
|
+ [self.participantField,'visitCode'],currentRow)
|
|
|
+
|
|
|
+ row['SequenceNum']=currentRow['SequenceNum']+0.01*len(frows)
|
|
|
+
|
|
|
+ else:
|
|
|
+ mode='update'
|
|
|
+ row=rows[0]
|
|
|
+
|
|
|
+ for seg in self.segmentList:
|
|
|
+ row[seg+'AE']=self.getAEResult(seg)
|
|
|
+
|
|
|
+ row['Date']=datetime.datetime.now().ctime()
|
|
|
+ resp=self.db.modifyRows(mode,self.parent.project,self.parent.schema,\
|
|
|
+ self.parent.aeDataset,[row])
|
|
|
+ print("Response {}".format(resp))
|
|
|
+ print("AE submitted")
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class irAEMMBrowserTest(ScriptedLoadableModuleTest):
|
|
|
+
|
|
|
+ """
|
|
|
+ This is the test case for your scripted module.
|
|
|
+ Uses ScriptedLoadableModuleTest base class, available at:
|
|
|
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
|
|
|
+ """
|
|
|
+
|
|
|
+ def setUp(self):
|
|
|
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
|
|
|
+ """
|
|
|
+ slicer.mrmlScene.Clear(0)
|
|
|
+
|
|
|
+ def runTest(self):
|
|
|
+ """Run as few or as many tests as needed here.
|
|
|
+ """
|
|
|
+ self.setUp()
|
|
|
+ self.test_irAEMMBrowser()
|
|
|
+
|
|
|
+ def test_irAEMMBrowser(self):
|
|
|
+ """ Ideally you should have several levels of tests. At the lowest level
|
|
|
+ tests sould exercise the functionality of the logic with different inputs
|
|
|
+ (both valid and invalid). At higher levels your tests should emulate the
|
|
|
+ way the user would interact with your code and confirm that it still works
|
|
|
+ the way you intended.
|
|
|
+ One of the most important features of the tests is that it should alert other
|
|
|
+ developers when their changes will have an impact on the behavior of your
|
|
|
+ module. For example, if a developer removes a feature that you depend on,
|
|
|
+ your test should break so they know that the feature is needed.
|
|
|
+ """
|
|
|
+
|
|
|
+ self.delayDisplay("Starting the test")
|
|
|
+ #
|
|
|
+ # first, get some data
|
|
|
+ #
|
|
|
+
|