Selaa lähdekoodia

Adding imageBrowser and licensing under Mozilla license

Andrej 1 vuosi sitten
vanhempi
commit
ca707226aa
2 muutettua tiedostoa jossa 1048 lisäystä ja 0 poistoa
  1. 201 0
      LICENSE
  2. 847 0
      slicerModules/imageBrowser.py

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2019 Andrej Studen
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 847 - 0
slicerModules/imageBrowser.py

@@ -0,0 +1,847 @@
+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 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 = "image 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 ...
+      
+        self.logic=imageBrowserLogic(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.spectField=qt.QLabel("spectResampled")
+        infoLayout.addRow("Data field (PET):",self.spectField)
+
+        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=imageBrowserLogic(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)
+
+        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)
+
+        self.forceReload=qt.QCheckBox("Force reload")
+        self.forceReload.setChecked(0)
+        patientsFormLayout.addRow("Force reload",self.forceReload)
+        
+
+
+    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("llezaic")
+        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("limfomiPET_iBrowser_selected.json")
+        #self.setupList.addItem("iraemm_iBrowserProspective.json")
+        #self.setupList.addItem("iraemm_iBrowserRetrospective.json")
+        self.setupList.addItem("cardiacSpect_iBrowser.json")
+        self.setupList.currentIndexChanged.connect(self.onSetupListChanged)
+        setupFormLayout.addRow("Setup:",self.setupList)
+
+
+
+    def addSegmentEditor(self):
+        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.spectCode.setText(row[self.spectField.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.forceReload.isChecked())
+        self.logic.loadSegmentation(self.currentRow)
+        self.setSegmentEditor()
+        #self.logic.loadReview(self.currentRow)
+        #self.logic.loadAE(self.currentRow)
+
+        #self.onReviewSegmentChanged()
+
+    def setSegmentEditor(self):
+        #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()
+        self.patientSave.setStyleSheet('background-color:gray')
+
+    def onPatientSaveButtonClicked(self):
+        status=self.logic.saveSegmentation()
+        if status:
+            self.patientSave.setStyleSheet('background-color:green')
+        else:
+            self.patientSave.setStyleSheet('background-color:red')
+
+
+    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)
+
+
+#
+# imageBrowserLogic
+#
+
+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)
+        print('imageBrowserLogic 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"]
+        self.segmentList=["One","Two","Three","Four","Five","Six","Seven","Eight"]
+        print('imageBrowserLogic 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=="llezaic":
+            fileName="llezaic.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()
+        try:
+            filterValue=self.isetup['filterEntries']
+        except KeyError:
+            #this is default
+            ids=[row[self.isetup['participantField']] for row in ds['rows']]
+            status['ids']=list(set(ids))
+            return status
+        
+        #look for entries where segmentation was already done
+        dsSet=self.getDataset('SegmentationsMaster')
+        segMap={'{}:{}'.format(r['ParticipantId'],r['visitCode']):r['comments'] 
+                for r in dsSet['rows']}
+        ids=[]
+        for r in ds['rows']:
+            code='{}:{}'.format(r['ParticipantId'],r['visitCode'])
+            try:
+                comment=segMap[code]
+            except KeyError:
+                ids.append(r['ParticipantId'])
+                continue
+            if comment==filterValue:
+                ids.append(r['ParticipantId'])
+        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']
+        try:
+            forceReload=iData['forceReload']
+        except KeyError:
+            forceReload=False
+        dset=self.isetup['datasets'][iData['dataset']]
+        
+        localPath=os.path.join(self.tempDir,path[-1])
+
+        if not os.path.isfile(localPath) or forceReload:
+            #download from server
+            remotePath=self.fb.formatPathURL(dset['project'],'/'.join(path))
+            if not self.fb.entryExists(remotePath):
+                print("Failed to get {}".format(remotePath))
+                return
+            #overwrites existing file from remote
+            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:
+            pass
+            #os.remove(localPath)
+
+
+    def loadImages(self,row,keepCached, forceReload=False):
+      
+      
+        #fields={'ctResampled':True,'petResampled':False}
+        fields={"CT":self.parent.ctField.text,\
+              "SPECT":self.parent.spectField.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',
+                    'forceReload':forceReload}
+            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['SPECT'].GetScalarVolumeDisplayNode().\
+            SetAndObserveColorNodeID(\
+            slicer.util.getNode('Inferno').GetID())
+        slicer.util.setSliceViewerLayers(background=self.volumeNode['CT'],\
+            foreground=self.volumeNode['SPECT'],foregroundOpacity=0.5,fit=True)
+
+    def loadSegmentation(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'])>1:
+            print('Multiple segmentations found!')
+            return
+        if len(ds['rows'])==1:
+            #update self.segmentationEntry
+            self.segmentationEntry=ds['rows'][0]
+            self.segmentationEntry['origin']='database'
+            if loadFile:
+                self.loadSegmentationFromEntry()
+            return
+        #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):
+        #compile path
+        entry=self.segmentationEntry
+        path=self.getSegmentationPath()
+        path.append(entry['latestFile'])
+        iData={'idx':'Segmentation','path':path,
+                    'keepCached':1,'dataset':'SegmentationsMaster'}
+        self.loadImage(iData)
+        #look for missing segments
+        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):
+        #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)
+        status=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)
+        return status
+        #self.segmentationEntry['origin']='database'
+        
+
+
+    def saveNode(self,node,dataset,path):
+        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)
+        return self.fb.entryExists(remotePath)
+        #add entry to segmentation list
+
+    def createSegmentation(self,entry):
+
+        #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 imageBrowserTest(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
+        #
+