Browse Source

Adding segmentation review browser

Andrej Studen 2 years ago
parent
commit
14650d8350
1 changed files with 880 additions and 0 deletions
  1. 880 0
      slicerModules/segmentationBrowser.py

+ 880 - 0
slicerModules/segmentationBrowser.py

@@ -0,0 +1,880 @@
+import os
+import unittest
+from __main__ import vtk, qt, ctk, slicer
+from slicer.ScriptedLoadableModule import *
+import json
+import datetime
+import sys
+import nixModule
+import pathlib
+import chardet
+import re
+#
+# labkeySlicerPythonExtension
+#
+
+class segmentationBrowser(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 = "segmentation 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 stored segmentation 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 segmentationBrowserWidget(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 segmentationBrowserWidget")
+        ScriptedLoadableModuleWidget.setup(self)
+        # Instantiate and connect widgets ...
+      
+        self.logic=segmentationBrowserLogic(self)
+        self.addInfoSection()
+        self.addSetupSection()
+        self.addPatientsSelector()
+        #self.addSegmentEditor()
+        self.addWindowManipulator()
+
+    def addInfoSection(self):  
+        #a python overview of json settings
+        infoCollapsibleButton = ctk.ctkCollapsibleButton()
+        infoCollapsibleButton.text = "Info"
+        self.layout.addWidget(infoCollapsibleButton)
+    
+        infoLayout = qt.QFormLayout(infoCollapsibleButton)
+
+
+        self.participantField=qt.QLabel("PatientId")
+        infoLayout.addRow("Participant field:",self.participantField)
+    
+        self.ctField=qt.QLabel("ctResampled")
+        infoLayout.addRow("Data field (CT):",self.ctField)
+
+        self.petField=qt.QLabel("petResampled")
+        infoLayout.addRow("Data field (PET):",self.petField)
+
+        self.userField=qt.QLabel("Loading")
+        infoLayout.addRow("User",self.userField)
+
+        self.idField=qt.QLabel("Loading")
+        infoLayout.addRow("ID",self.idField)
+
+        #Add logic at some point
+        #self.logic=segmentationBrowserLogic(self)
+
+
+
+    def addPatientsSelector(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()
+        self.patientList.currentIndexChanged.connect(self.onPatientListChanged)
+        self.patientList.setEditable(True)
+        self.patientList.setInsertPolicy(qt.QComboBox.NoInsert)
+        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.patientLoad=qt.QPushButton("Load")
+        self.patientLoad.clicked.connect(self.onPatientLoadButtonClicked)
+        patientsFormLayout.addRow("Load patient",self.patientLoad)
+
+        #no Save button
+        #self.patientSave=qt.QPushButton("Save")
+        #self.patientSave.clicked.connect(self.onPatientSaveButtonClicked)
+        #patientsFormLayout.addRow("Save segmentation",self.patientSave)
+
+        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)
+    
+
+
+    def addSetupSection(self):
+        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('<Select>')
+        self.serverList.addItem("astuden")
+        self.serverList.addItem("adoma")
+        self.serverList.addItem("kzevnik")
+        self.serverList.currentIndexChanged.connect(self.onServerListChanged)
+        setupFormLayout.addRow("Select user:",self.serverList)
+
+
+        self.setupList=qt.QComboBox()
+        self.setupList.addItem('<Select>')
+        self.setupList.addItem("limfomiPET_iBrowser.json")
+        self.setupList.addItem("iraemm_iBrowserProspective.json")
+        self.setupList.addItem("iraemm_iBrowserRetrospective.json")
+        self.setupList.currentIndexChanged.connect(self.onSetupListChanged)
+        setupFormLayout.addRow("Setup:",self.setupList)
+
+
+    def addReviewSection(self):
+
+        #
+        # Review Area (not used, relic)
+        #
+        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()
+        sLabel="What do you think about the segmentation:"
+        self.reviewFormLayout.addRow(sLabel,self.reviewResult)
+        reviewOptions=['Select','Excellent','Minor deficiencies',\
+            'Major deficiencies','Unusable']
+        for opt in reviewOptions:
+            self.reviewResult.addItem(opt)
+    
+        self.aeResult=qt.QComboBox()
+        aeLabel="Is organ suffering from adverse effect?"
+        self.reviewFormLayout.addRow(aeLabel,self.aeResult)
+        aeOptions=['Select','Yes','No']
+        for opt in aeOptions:
+            self.aeResult.addItem(opt)
+        #self.aeResult.setCurrentIndex(0)
+
+        self.updateReview=qt.QPushButton("Save")
+        saLabel="Save segmentation and AE decision for current segment"
+        self.reviewFormLayout.addRow(saLabel,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 addSegmentEditor(self):
+        #not used (for review)
+        editorCollapsibleButton = ctk.ctkCollapsibleButton()
+        editorCollapsibleButton.text = "Segment Editor"
+        self.layout.addWidget(editorCollapsibleButton)
+        hLayout=qt.QVBoxLayout(editorCollapsibleButton)
+
+        self.segmentEditorWidget=slicer.qMRMLSegmentEditorWidget()
+        hLayout.addWidget(self.segmentEditorWidget)
+
+        self.segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
+        segEditorNode=slicer.vtkMRMLSegmentEditorNode()
+        slicer.mrmlScene.AddNode(segEditorNode)
+        self.segmentEditorWidget.setMRMLSegmentEditorNode(segEditorNode)
+       
+    def addWindowManipulator(self):
+        windowManipulatorCollapsibleButton=ctk.ctkCollapsibleButton()
+        windowManipulatorCollapsibleButton.text="CT Window Manipulator"
+        self.layout.addWidget(windowManipulatorCollapsibleButton)
+
+        hLayout=qt.QHBoxLayout(windowManipulatorCollapsibleButton)
+
+        ctWins={'CT:bone':self.onCtBoneButtonClicked,
+            'CT:air':self.onCtAirButtonClicked,
+            'CT:abdomen':self.onCtAbdomenButtonClicked,
+            'CT:brain':self.onCtBrainButtonClicked,
+            'CT:lung':self.onCtLungButtonClicked}
+        for ctWin in ctWins:
+            ctButton=qt.QPushButton(ctWin)
+            ctButton.clicked.connect(ctWins[ctWin])
+            hLayout.addWidget(ctButton)
+
+    def onSetupListChanged(self,i):
+        status=self.logic.setConfig(self.setupList.currentText)
+        try:
+            if status['error']=='FILE NOT FOUND':
+                print('File {} not found.'.format(self.setupList.currentText))
+                return
+        except KeyError:
+            pass
+
+        #sort ids
+        ids=status['ids']
+        ids.sort()
+
+        self.updatePatientList(ids)
+        self.onPatientListChanged(0)
+ 
+    def onServerListChanged(self,i):
+        status=self.logic.setServer(self.serverList.currentText)
+        try:
+            if status['error']=='KEY ERROR':
+                self.serverList.setStyleSheet('background-color: violet')
+            if status['error']=='ID ERROR':
+                self.serverList.setStyleSheet('background-color: red')
+            return
+        except KeyError:
+            pass 
+
+        self.idField.setText(status['id'])
+        self.userField.setText(status['displayName'])
+        self.serverList.setStyleSheet('background-color: green')
+
+    def onPatientListChanged(self,i):
+        self.visitList.clear()   
+        self.petCode.setText("")
+        self.ctCode.setText("")
+        
+        #add potential filters from setup to dbFilter
+        ds=self.logic.getDataset(dbFilter={'participant':self.patientList.currentText})
+       
+        visitVar=self.logic.getVarName(var='visitField')
+        dt=datetime.datetime
+        
+        #label is a combination of sequence number and date of imaging
+        try:
+            seq={row['SequenceNum']:
+                {'label':row[visitVar],
+                'date': dt.strptime(row['studyDate'],'%Y/%m/%d %H:%M:%S')}
+                for row in ds['rows']}
+        except TypeError:
+            #if studyDate is empty, this will return no possible visits
+            return
+
+        #apply lookup to visitVar if available
+        try:
+            seq={x:
+                    {'label':ds['lookups'][visitVar][seq[x]['label']],
+                    'date':seq[x]['date']}
+                for x in seq}
+        except KeyError:
+            pass
+
+        #format label
+        seq={x:'{} ({})'.format(seq[x]['label'],dt.strftime(seq[x]['date'],'%d-%b-%Y')) 
+            for x in seq}
+
+            
+        for s in seq:
+            #onVisitListChanged is called for every addItem
+            self.visitList.addItem(seq[s],s)
+
+        #self.onVisitListChanged(0)
+
+    def onVisitListChanged(self,i):
+    
+        #ignore calls on empty list
+        if self.visitList.count==0:
+            return
+
+        #get sequence num
+        s=self.visitList.itemData(i)
+
+        print("Visit: SequenceNum:{}, label{}".format(s,self.visitList.currentText))
+        dbFilter={'participant':self.patientList.currentText,
+            'seqNum':s}
+        ds=self.logic.getDataset(dbFilter=dbFilter)
+        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 updatePatientList(self,ids):
+        self.patientList.clear()
+
+        for id in ids:
+            self.patientList.addItem(id)
+
+    def onPatientLoadButtonClicked(self):
+        print("Load")
+        #delegate loading to logic
+        self.logic.loadImages(self.currentRow,self.keepCached.isChecked())
+        self.logic.loadSegmentations(self.currentRow)
+        #self.setSegmentEditor()
+        #self.logic.loadReview(self.currentRow)
+        #self.logic.loadAE(self.currentRow)
+
+        #self.onReviewSegmentChanged()
+
+    def setSegmentEditor(self):
+        #not used
+        #use current row to set segment in segment editor
+        self.segmentEditorWidget.setSegmentationNode(
+            self.logic.volumeNode['Segmentation'])
+        self.segmentEditorWidget.setMasterVolumeNode(
+            self.logic.volumeNode['PET'])
+  
+    def onReviewSegmentChanged(self):
+        pass
+
+    def onPatientClearButtonClicked(self):
+        self.logic.clearVolumesAndSegmentations()
+
+    def onPatientSaveButtonClicked(self):
+        #not used
+        self.logic.saveSegmentation()
+
+    def onCtBoneButtonClicked(self):
+        self.logic.setWindow('CT:bone')
+
+    def onCtAirButtonClicked(self):
+        self.logic.setWindow('CT:air')
+
+    def onCtAbdomenButtonClicked(self):
+        self.logic.setWindow('CT:abdomen')
+
+    def onCtBrainButtonClicked(self):
+        self.logic.setWindow('CT:brain')
+
+    def onCtLungButtonClicked(self):
+        self.logic.setWindow('CT:lung')
+
+    def cleanup(self):
+        pass
+
+
+def loadLibrary(name):
+    #utility function to load nix library from git
+    fwrapper=nixModule.getWrapper('nixWrapper.py')
+    p=pathlib.Path(fwrapper)
+    sys.path.append(str(p.parent))
+    import nixWrapper
+    return nixWrapper.loadLibrary(name)
+
+
+#
+# segmentationBrowserLogic
+#
+
+class segmentationBrowserLogic(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)
+        print('segmentationBrowserLogic loading')
+        if not parent==None:
+            #use layout and data from parent widget
+            self.parent=parent
+        
+        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'
+        loadLibrary(lName)
+      
+        import labkeyInterface
+        import labkeyDatabaseBrowser
+        import labkeyFileBrowser
+
+
+        self.network=labkeyInterface.labkeyInterface() 
+        self.dbBrowser=labkeyDatabaseBrowser
+        self.fBrowser=labkeyFileBrowser
+        self.tempDir=os.path.join(os.path.expanduser('~'),'temp')
+        if not os.path.isdir(self.tempDir):
+            os.mkdir(self.tempDir)
+        self.lookups={}
+        self.segmentList=["Liver","Blood","Marrow","Disease","Deauville"]
+        print('segmentationBrowserLogic setup complete')
+    
+
+
+    def setServer(self,serverName):
+        #additional way of setting the labkey network interface 
+        #if no parent was provided in logic initialization (stand-alone mode)
+        status={}
+        fileName="NONE"
+        if serverName=="astuden":
+            fileName="astuden.json"
+        if serverName=="adoma":
+            fileName="adoma.json"
+        if serverName=="kzevnik":
+            fileName="kzevnik.json"
+        if fileName=="NONE":
+            print("No path was associated with server {}".format(serverName))
+            status['error']='KEY ERROR'
+            return status
+        fconfig=os.path.join(os.path.expanduser('~'),'.labkey',fileName)
+        self.network.init(fconfig)
+        self.remoteId=self.network.getUserId()
+
+        if self.remoteId==None:
+            status['error']='ID ERROR'
+            return status
+     
+
+        status['displayName']=self.remoteId['displayName']
+        status['id']=self.remoteId['id']
+
+        #reset db and fb (they are thin classes anyhow)
+        self.db=self.dbBrowser.labkeyDB(self.network)
+        self.fb=self.fBrowser.labkeyFileBrowser(self.network)
+        return status
+
+    def setConfig(self,configName):
+
+        status={}
+        fileName=os.path.join(os.path.expanduser('~'),'.labkey',configName)
+        if not os.path.isfile(fileName):
+            status['error']='FILE NOT FOUND'
+            return status
+
+        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']
+   
+        #include filters...
+
+        ds=self.getDataset()
+
+        ids=[row[self.isetup['participantField']] for row in ds['rows']]
+        status['ids']=list(set(ids))
+        return status
+
+    def getVarName(self,name="Imaging",var="visitField"):
+        dset=self.isetup['datasets'][name]
+        defaults={"visitField":"imagingVisitId"}
+        try:
+            return dset[var]
+        except KeyError:
+            return defaults[var]
+
+        
+    def getDataset(self,name="Imaging",dbFilter={}):
+        dset=self.isetup['datasets'][name]
+        project=dset['project']
+        schema=dset['schema']
+        query=dset['query']
+
+        #add default filters
+        qFilter=[]
+        try:
+            for qf in dset['filter']:
+                v=dset['filter'][qf]
+                qFilter.append({'variable':qf,'value':v,'oper':'eq'})
+        except KeyError:
+            pass
+        for f in dbFilter:
+            if f=='participant':
+                qFilter.append({'variable':self.isetup['participantField'],
+                    'value':dbFilter[f],'oper':'eq'})
+                continue
+            if f=='seqNum':
+                qFilter.append({'variable':'SequenceNum',
+                    'value':'{}'.format(dbFilter[f]),
+                    'oper':'eq'})
+                continue
+            qFilter.append({'variable':f,'value':dbFilter[f],'oper':'eq'})
+
+        try:
+            ds=self.db.selectRows(project,schema,query, \
+                qFilter,dset['view'])
+        except KeyError:
+            ds=self.db.selectRows(project,schema,query,qFilter)
+
+        #get lookups as well 
+        lookups={}
+        for f in ds['metaData']['fields']:
+            try:
+                lookup=f['lookup']
+            except KeyError:
+                continue
+            var=f['name']
+            lookupCode='{}:{}'.format(lookup['schema'],lookup['queryName'])
+            try:
+                lookups[var]=self.lookups[lookupCode]
+            except KeyError:
+                self.lookups[lookupCode]=self.loadLookup(project,lookup)
+                lookups[var]=self.lookups[lookupCode]
+            
+        return {'rows':ds['rows'],'lookups':lookups}
+
+    def loadLookup(self,project,lookup):
+
+        qData={}
+        key=lookup['keyColumn']
+        value=lookup['displayColumn']
+
+        fSet=self.db.selectRows(project,lookup['schema'],lookup['queryName'],[])
+        for q in fSet['rows']:
+            qData[q[key]]=q[value]
+        return qData
+
+    def loadImage(self,iData):
+
+        #unpack iData
+        idx=iData['idx']
+        path=iData['path']
+        keepCached=iData['keepCached']
+        dset=self.isetup['datasets'][iData['dataset']]
+        
+        localPath=os.path.join(self.tempDir,path[-1])
+
+        if not os.path.isfile(localPath):
+            #download from server
+            remotePath=self.fb.formatPathURL(dset['project'],'/'.join(path))
+            if not self.fb.entryExists(remotePath):
+                print("Failed to get {}".format(remotePath))
+                return
+
+            self.fb.readFileToFile(remotePath,localPath) 
+          
+        properties={}
+        filetype='VolumeFile'
+
+        #make sure segmentation gets loaded as a labelmap
+        if idx=="Segmentation":
+            filetype='SegmentationFile'
+            #properties["labelmap"]=1
+              
+          
+        self.volumeNode[idx]=slicer.util.loadNodeFromFile(localPath,
+                        filetype=filetype,properties=properties)
+          
+        if not keepCached:
+            os.remove(localPath)
+
+
+    def loadImages(self,row,keepCached):
+      
+      
+        #fields={'ctResampled':True,'petResampled':False}
+        fields={"CT":self.parent.ctField.text,\
+              "PET":self.parent.petField.text}
+
+        path=[self.isetup['imageDir'],row['patientCode'],row['visitCode']]
+        relativePaths={x:path+[row[y]] for (x,y) in fields.items()}
+
+        self.volumeNode={}
+        for f in relativePaths:
+            iData={'idx':f,'path':relativePaths[f],
+                    'keepCached':keepCached,'dataset':'Imaging'}
+            self.loadImage(iData)
+
+        #mimic abdominalCT standardized window setting
+        self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
+              SetAutoWindowLevel(False)
+        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('Inferno').GetID())
+        slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
+            foreground=self.volumeNode['PET'],foregroundOpacity=0.5,fit=True)
+
+    def loadSegmentations(self,row, loadFile=1):
+        dbFilter={
+            #'User':'{}'.format(self.remoteId['id']),
+            'participant':row[self.isetup['participantField']],
+            'visitCode':row['visitCode']}
+        ds=self.getDataset(name='SegmentationsMaster',
+                dbFilter=dbFilter)
+        if len(ds['rows'])<0:
+            print('No segmentations for {}'.format(dbFilter))
+            return
+        for r in ds['rows']:
+            #update self.segmentationEntry
+            #self.segmentationEntry=ds['rows'][0]
+            #self.segmentationEntry['origin']='database'
+            #if loadFile:
+            self.loadSegmentationFromEntry(r)
+            return
+        print('load Segmentation: done')
+        #create new segmentation 
+        #self.createSegmentation(row)
+
+    def getSegmentationPath(self):
+        path=[self.isetup['imageDir'],
+                self.segmentationEntry['patientCode'],
+                self.segmentationEntry['visitCode']]
+        path.append('Segmentations')
+        return path
+
+    def loadSegmentationFromEntry(self,r):
+        #compile path
+        entry=r
+        path=self.getSegmentationPath()
+        path.append(entry['latestFile'])
+        iData={'idx':'Segmentation','path':path,
+                    'keepCached':0,'dataset':'SegmentationsMaster'}
+        self.loadImage(iData)
+        #look for missing segments
+        return
+        #skip this
+        segNode=self.volumeNode['Segmentation']
+        seg=segNode.GetSegmentation()
+        segNames=[seg.GetNthSegmentID(i) for i in range(seg.GetNumberOfSegments())]
+        print('Segments')
+        try:
+            segmentList=self.isetup['segmentList']
+        except KeyError:
+            segmentList=self.segmentList
+        for s in segmentList:
+            if s not in segNames:
+                seg.AddEmptySegment(s,s)
+                print(s)
+        print('Done')
+
+
+
+
+    def saveSegmentation(self):
+        #not used
+        #get the latest key by adding an entry to SegmentationList
+        copyFields=[self.isetup['participantField'],'patientCode','visitCode','User']
+        outRow={x:self.segmentationEntry[x] for x in copyFields}
+        sList=self.isetup['datasets']['SegmentationsList']
+        resp=self.db.modifyRows('insert',sList['project'],
+            sList['schema'],sList['query'],[outRow])
+        encoding=chardet.detect(resp)['encoding']
+        respJSON=json.loads(resp.decode(encoding))
+        outRow=respJSON['rows'][0]
+        #print(outRow)
+
+        #construct file name with known key
+        uName=re.sub(' ','_',self.remoteId['displayName'])
+        fName='Segmentation_{}-{}_{}_{}.nrrd'.format(
+                self.segmentationEntry['patientCode'],
+                self.segmentationEntry['visitCode'],
+                uName,outRow['Key'])
+        path=self.getSegmentationPath()
+        path.append(fName)
+        self.saveNode(self.volumeNode['Segmentation'],'SegmentationsMaster',path)
+        
+        #update SegmentationList with know file name
+        outRow['Segmentation']=fName
+        self.db.modifyRows('update',sList['project'],
+            sList['schema'],sList['query'],[outRow])
+
+        #update SegmentationsMaster
+        self.segmentationEntry['latestFile']=fName
+        self.segmentationEntry['version']=outRow['Key']
+        des=self.isetup['datasets']['SegmentationsMaster']
+        op='insert'
+        if self.segmentationEntry['origin']=='database':
+            op='update'
+        print('Saving: mode={}'.format(op))
+        resp=self.db.modifyRows(op,des['project'],
+                des['schema'],des['query'],[self.segmentationEntry])
+        print(resp)
+        #since we loaded a version, origin should be set to database
+        self.loadSegmentation(self.segmentationEntry,0)
+        #self.segmentationEntry['origin']='database'
+        
+
+
+    def saveNode(self,node,dataset,path):
+        #not used
+        fName=path[-1]
+        localPath=os.path.join(self.tempDir,fName)
+        slicer.util.saveNode(node,localPath)
+
+        dset=self.isetup['datasets'][dataset]
+        #exclude file name when building directory structure
+        remotePath=self.fb.buildPathURL(dset['project'],path[:-1])
+        remotePath+='/'+fName
+        self.fb.writeFileToFile(localPath,remotePath)
+
+        #add entry to segmentation list
+
+    def createSegmentation(self,entry):
+        #not used
+
+        #create segmentation entry for database update
+        #note that origin is not set to database
+        copyFields=[self.isetup['participantField'],'patientCode','visitCode','SequenceNum']
+        #copyFields=['ParticipantId','patientCode','visitCode','SequenceNum']
+        self.segmentationEntry={x:entry[x] for x in copyFields}
+        self.segmentationEntry['User']=self.remoteId['id']
+        self.segmentationEntry['origin']='created'
+        self.segmentationEntry['version']=-1111
+    
+        #create a segmentation node
+        uName=re.sub(' ','_',self.remoteId['displayName'])
+        segNode=slicer.vtkMRMLSegmentationNode()
+        self.volumeNode['Segmentation']=segNode
+        segNode.SetName('Segmentation_{}_{}_{}'.
+                format(entry['patientCode'],entry['visitCode'],uName))
+        slicer.mrmlScene.AddNode(segNode)
+        segNode.CreateDefaultDisplayNodes()
+        segNode.SetReferenceImageGeometryParameterFromVolumeNode(self.volumeNode['PET'])
+        try:
+            segmentList=self.isetup['segmentList']
+        except KeyError:
+            segmentList=self.segmentList
+        for s in segmentList:
+            segNode.GetSegmentation().AddEmptySegment(s,s)
+
+
+
+    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={}
+
+    def setWindow(self,windowName):
+
+        #default
+        w=1400
+        l=-500
+        
+        if windowName=='CT:bone':
+            w=1000
+            l=400
+
+        if windowName=='CT:air':
+            w=1000
+            l=-426
+        
+        if windowName=='CT:abdomen':
+            w=350
+            l=40
+
+        if windowName=='CT:brain':
+            w=100
+            l=50
+
+        if windowName=='CT:lung':
+            w=1400
+            l=-500
+
+
+        self.volumeNode['CT'].GetScalarVolumeDisplayNode().\
+              SetWindowLevel(w,l)
+        
+        print('setWindow: {} {}/{}'.format(windowName,w,l))
+
+class segmentationBrowserTest(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
+        #
+