瀏覽代碼

Merge branch 'master' of https://git0.fmf.uni-lj.si/studen/orthancInterface

Andrej Studen @ VBOX 1 年之前
父節點
當前提交
16a6bfd734

+ 373 - 0
LICENSE

@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 31 - 3
orthancDatabaseBrowser.py

@@ -12,12 +12,18 @@ class orthancDB:
     def __init__(self,net):
         self.net=net
 
-    def getPatients(self):
+    def getList(self,category):
         url=self.net.getCoreURL()
-        url+='/patients'
+        url+='/'+category
         response=self.net.get(url)
         return extractJSON(response.data)
+    
+    def getPatients(self):
+        return self.getList('patients')
 
+    def getStudies(self):
+        return self.getList('studies')
+    
     def getPatientData(self, orthancId):
         return self.getData('patients',orthancId)
 
@@ -40,6 +46,16 @@ class orthancDB:
         return extractJSON(response.data)
 
 
+    def getDicomField(self,orthancInstanceId,dicomField):
+        url=self.net.getCoreURL()
+        url+='/instances/'+orthancInstanceId+'/content/'+dicomField
+        response=self.net.get(url)
+        try:
+            encoding=chardet.detect(response.data)["encoding"]
+            return response.data.decode(encoding)
+        except TypeError:
+            return ''
+
     def selectRows(self, qfilter):
         obj={}
         obj["Level"]="Instance"
@@ -58,8 +74,20 @@ class orthancDB:
         url+='/instances'
         fobj=open (f, 'rb')
         response=self.net.post(url,'octet-stream',fobj.read())
-        encoding=chardet.detect(response.data)["encoding"]
+        try:
+            encoding=chardet.detect(response.data)["encoding"]
+        except AttributeError:
+            print(response)
+            return json.loads('{"status":"BAD"}')
         return json.loads(response.data.decode(encoding))
 
+    def remove(self,level,orthancId):
+        url=self.net.getCoreURL()
+        url+='/'+level+'/'+orthancId
+        response=self.net.delete(url)
+        print(response.data)
+
+
+
 
 

+ 14 - 2
orthancFileBrowser.py

@@ -9,14 +9,24 @@ class orthancFileBrowser:
 
     def getZip(self,retrieveLevel, dataId, path):
         url=self.net.getCoreURL()
-        url+="/"+retrieveLevel+"/"+dataId+"/archive";
+        url+="/"+retrieveLevel+"/"+dataId+"/archive"
         print("Using: {}".format(url))
-        response=self.net.get(url,binary=True);
+        response=self.net.get(url,binary=True)
         with open(path,'wb') as out_file:
             shutil.copyfileobj(response,out_file)
         response.release_conn()
         #response.data is a byte array -> is the same as file
 
+    def getInstance(self, instanceId, localFile):
+        url=self.net.getCoreURL()
+        url+='/instances/'+instanceId+'/file'
+        print('Using {}'.format(url))
+        response=self.net.get(url,binary=True)
+        with open(localFile,'wb') as out_file:
+            shutil.copyfileobj(response,out_file)
+        response.release_conn()
+
+
     def upload(self,path):
         url=self.net.getCoreURL()
         url+="/instances"
@@ -24,3 +34,5 @@ class orthancFileBrowser:
             response=self.net.post(url,"octet-stream", f.read())
         print(response.data)
 
+    
+

+ 15 - 3
orthancInterface.py

@@ -17,7 +17,6 @@ class orthancInterface:
         
         self.http=urllib3.PoolManager()
 
-        
         if 'SSL' in self.connectionConfig['orthanc']:
             sslConfig=self.connectionConfig['orthanc']['SSL']
             self.http = urllib3.PoolManager(\
@@ -43,7 +42,6 @@ class orthancInterface:
         debug=False
         if debug:
             print("GET: {0}").format(url)
-            print("as {0}").format(user)
         headers=urllib3.util.make_headers(basic_auth=self.getBasicAuth())
         try:
             if not binary:
@@ -89,7 +87,21 @@ class orthancInterface:
         #f contains json as a return value
         except urllib3.exceptions.HTTPError as e:
             print(e)
-      
+     
+
+    def delete(self,url):
+
+        debug=False
+        if debug:
+            print("DELETE: {0}").format(url)
+        
+        headers=urllib3.util.make_headers(basic_auth=self.getBasicAuth())
+        try:
+            return self.http.request('DELETE',url,headers=headers)
+        except urllib3.exceptions.HTTPError as e:
+            print(e)
+            return None
+ 
 
             
 

+ 157 - 0
pythonScripts/scanOrthanc.py

@@ -0,0 +1,157 @@
+#labkey/orthanc interface
+
+#scans the orthanc internal database and fills labkey table
+
+#"Orthanc" section expected in the configuration file, with
+#"queryName": query to be filled for each image 
+#"demographicsQuery": query to be filled for every patient
+#"schemaName": the schema both queries are part of
+#"project": name of the project under labkey that collects Orthanc data,
+#           typically named Orthanc or similar
+#"participantField": sorting participant field label in labkey study
+
+
+import os
+import json
+import re
+import sys
+
+def main(parameterFile):
+
+    fhome=os.path.expanduser('~')
+    fsetup=os.path.join(fhome,'.labkey','setup.json')
+
+    with open(fsetup,'r') as f:
+        setup=json.load(f)
+
+    sys.path.insert(0,setup['paths']['nixWrapper'])
+    import nixWrapper
+
+    nixWrapper.loadLibrary("labkeyInterface")
+    import labkeyInterface
+    import labkeyDatabaseBrowser
+
+    nixWrapper.loadLibrary('orthancInterface')
+    import orthancInterface
+    import orthancDatabaseBrowser
+
+    fconfig=os.path.join(fhome,'.labkey','network.json')
+
+    net=labkeyInterface.labkeyInterface()
+    net.init(fconfig)
+    db=labkeyDatabaseBrowser.labkeyDB(net)
+
+
+    onet=orthancInterface.orthancInterface()
+    onet.init(fconfig)
+    odb=orthancDatabaseBrowser.orthancDB(onet)
+
+
+    with open(parameterFile) as f:
+        pars=json.load(f)
+
+    i=0
+    opars=pars['Orthanc']
+    project=opars['project']
+    
+    participantField=opars['participantField']
+
+    patients=odb.getPatients()
+    n=len(patients)
+
+    for p in patients:
+        pdata=odb.getPatientData(p)
+        dicom=pdata['MainDicomTags']
+        patientId=dicom['PatientID']
+    
+        print("[{}/{}] Patient: {} ID: {}".format(i,n,p,patientId))
+
+        qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+        ds=db.selectRows(project,opars['schemaName'],\
+                opars['demographicsQuery'],[qfilter])
+        if len(ds['rows'])==0:
+            row={}
+            row[participantField]=patientId
+            row['birthDate']=dicom['PatientBirthDate']
+            row['PatientName']=dicom['PatientName']
+            row['OrthancId']=p
+            db.modifyRows('insert',project,opars['schemaName'],\
+                    opars['demographicsQuery'],[row])
+
+        for s in pdata['Studies']:
+            sdata=odb.getStudyData(s)
+            sdicom=sdata['MainDicomTags']
+            sid=sdicom['StudyInstanceUID']
+            #print('Data: {}'.format(sdata))
+            #this is try/except protetcted in previous version...
+            sdate="19700101"
+            try:
+                sdate=sdicom['StudyDate']
+            except KeyError:
+                pass
+            #continue
+        
+            print('\tStudy[{}]: {}/{}'.format(sdate,s,sid))
+        
+            for se in sdata['Series']:
+
+                sedata=odb.getSeriesData(se)
+                sedicom=sedata['MainDicomTags']
+                    
+                seid=sedicom['SeriesInstanceUID']
+                #print('Data: {}'.format(sedata))
+                seDesc="NONE"
+                try:
+                    seDesc=sedicom['SeriesDescription']
+                except KeyError:
+                    pass
+
+                #replace letters that might trip the database
+                spanishOAcute=''.join([chr(3619),chr(3603)])
+                spanishAAcute=''.join([chr(3619),chr(3585)])
+                seDesc=re.sub(spanishOAcute,'o',seDesc)
+                seDesc=re.sub(spanishAAcute,'a',seDesc)
+
+
+                print('\t\tSeries[{}]: {}/{}'.format(seDesc,se,seid))
+
+                qfilter={'variable':'orthancSeries','value':se,'oper':'eq'}
+                ds=db.selectRows(project,opars['schemaName'],\
+                        opars['queryName'],[qfilter])
+                
+                mode='insert'
+
+                if len(ds['rows'])>0:
+                    mode='update'
+                    #use existing row data
+                    row=ds['rows'][0]
+
+                else:
+
+                    #count existing entries for patient
+                    qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+                    ds=db.selectRows(project,opars['schemaName'],\
+                        opars['queryName'],[qfilter])
+                    seqNum=len(ds['rows'])
+                    
+                    #create new row
+                    row={}
+                    row[participantField]=patientId
+                    row['sequenceNum']=seqNum
+                    row['orthancSeries']=se
+                
+                
+                row['dicomStudy']=sid
+                row['orthancStudy']=s
+                row['dicomSeries']=seid
+                row['studyDate']=sdate
+                row['seriesDescription']=seDesc
+                db.modifyRows(mode,project,opars['schemaName'],\
+                        opars['queryName'],[row])
+
+
+        i+=1
+    print("Done")
+
+if __name__=="__main__":
+    main(sys.argv[1])

+ 162 - 0
pythonScripts/scanOrthancLocal.py

@@ -0,0 +1,162 @@
+#labkey/orthanc interface
+
+#scans the orthanc internal database and fills labkey table
+
+#"Orthanc" section expected in the configuration file, with
+#"queryName": query to be filled for each image 
+#"demographicsQuery": query to be filled for every patient
+#"schemaName": the schema both queries are part of
+#"project": name of the project under labkey that collects Orthanc data,
+#           typically named Orthanc or similar
+#"participantField": sorting participant field label in labkey study
+
+
+import os
+import json
+import re
+import sys
+
+def main(parameterFile):
+
+   fhome=os.path.expanduser('~')
+   fsetup=os.path.join(fhome,'.labkey','setup.json')
+
+   with open(fsetup,'r') as f:
+     setup=json.load(f)
+
+   sys.path.insert(0,setup['paths']['nixWrapper'])
+   sys.path.insert(0,setup['paths']['labkeyInterface'])
+   sys.path.insert(0,setup['paths']['orthancInterface'])
+   import nixWrapper
+
+#nixWrapper.loadLibrary("labkeyInterface")
+   import labkeyInterface
+   import labkeyDatabaseBrowser
+
+#nixWrapper.loadLibrary('orthancInterface')
+   import orthancInterface
+   import orthancDatabaseBrowser
+
+   fconfig=os.path.join(fhome,'.labkey','network.json')
+
+   net=labkeyInterface.labkeyInterface()
+   net.init(fconfig)
+   db=labkeyDatabaseBrowser.labkeyDB(net)
+
+
+   onet=orthancInterface.orthancInterface()
+   onet.init(fconfig)
+   odb=orthancDatabaseBrowser.orthancDB(onet)
+
+
+   with open(parameterFile) as f:
+     pars=json.load(f)
+
+   i=0
+   opars=pars['Orthanc']
+   project=opars['project']
+   demoQuery=opars['demographicsQuery']
+   schema=opars['schemaName']
+   query=opars['queryName']
+
+   participantField=opars['participantField']
+
+   patients=odb.getPatients()
+   n=len(patients)
+   print('Npatients: {}'.format(n))
+
+   for p in patients:
+      pdata=odb.getPatientData(p)
+      dicom=pdata['MainDicomTags']
+      patientId=dicom['PatientID']
+
+      print("[{}/{}] Patient: {} ID: {}".format(i,n,p,patientId))
+
+      qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+      print('selectRows({},{},{})'.format(project,schema,demoQuery))
+      ds=db.selectRows(project,schema,demoQuery,[qfilter])
+      if len(ds['rows'])==0:
+         row={}
+         row[participantField]=patientId
+         try:
+            row['birthDate']=dicom['PatientBirthDate']
+         except KeyError:
+            pass
+         row['PatientName']=dicom['PatientName']
+         row['OrthancId']=p
+         db.modifyRows('insert',project,schema,demoQuery,[row])
+
+      for s in pdata['Studies']:
+         sdata=odb.getStudyData(s)
+         sdicom=sdata['MainDicomTags']
+         sid=sdicom['StudyInstanceUID']
+         #print('Data: {}'.format(sdata))
+         #this is try/except protetcted in previous version...
+         sdate="19700101"
+         try:
+             sdate=sdicom['StudyDate']
+         except KeyError:
+             pass
+         #continue
+
+         print('\tStudy[{}]: {}/{}'.format(sdate,s,sid))
+
+         for se in sdata['Series']:
+
+             sedata=odb.getSeriesData(se)
+             sedicom=sedata['MainDicomTags']
+                 
+             seid=sedicom['SeriesInstanceUID']
+             #print('Data: {}'.format(sedata))
+             seDesc="NONE"
+             try:
+                 seDesc=sedicom['SeriesDescription']
+             except KeyError:
+                 pass
+
+             #replace letters that might trip the database
+             spanishOAcute=''.join([chr(3619),chr(3603)])
+             spanishAAcute=''.join([chr(3619),chr(3585)])
+             seDesc=re.sub(spanishOAcute,'o',seDesc)
+             seDesc=re.sub(spanishAAcute,'a',seDesc)
+
+
+             print('\t\tSeries[{}]: {}/{}'.format(seDesc,se,seid))
+
+             qfilter={'variable':'orthancSeries','value':se,'oper':'eq'}
+             ds=db.selectRows(project,schema,query,[qfilter])
+             
+             mode='insert'
+
+             if len(ds['rows'])>0:
+                 mode='update'
+                 #use existing row data
+                 row=ds['rows'][0]
+
+             else:
+
+                 #count existing entries for patient
+                 qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+                 ds=db.selectRows(project,schema,query,[qfilter])
+                 seqNum=len(ds['rows'])
+                 
+                 #create new row
+                 row={}
+                 row[participantField]=patientId
+                 row['sequenceNum']=seqNum
+                 row['orthancSeries']=se
+             
+             
+             row['dicomStudy']=sid
+             row['orthancStudy']=s
+             row['dicomSeries']=seid
+             row['studyDate']=sdate
+             row['seriesDescription']=seDesc
+             db.modifyRows(mode,project,schema,query,[row])
+
+
+      i+=1
+   print("Done")
+
+if __name__=="__main__":
+    main(sys.argv[1])

+ 193 - 0
pythonScripts/scanOrthancQuick.py

@@ -0,0 +1,193 @@
+#labkey/orthanc interface
+
+#scans the orthanc internal database and fills labkey table
+
+#"Orthanc" section expected in the configuration file, with
+#"queryName": query to be filled for each image 
+#"demographicsQuery": query to be filled for every patient
+#"schemaName": the schema both queries are part of
+#"project": name of the project under labkey that collects Orthanc data,
+#           typically named Orthanc or similar
+#"participantField": sorting participant field label in labkey study
+
+
+import os
+import json
+import re
+import sys
+
+def main(parameterFile):
+
+    fhome=os.path.expanduser('~')
+    fsetup=os.path.join(fhome,'.labkey','setup.json')
+
+    with open(fsetup,'r') as f:
+        setup=json.load(f)
+
+    with open(parameterFile) as f:
+        pars=json.load(f)
+
+    sys.path.insert(0,setup['paths']['nixWrapper'])
+    import nixWrapper
+
+    nixWrapper.loadLibrary("labkeyInterface")
+    import labkeyInterface
+    import labkeyDatabaseBrowser
+
+    nixWrapper.loadLibrary('orthancInterface')
+    import orthancInterface
+    import orthancDatabaseBrowser
+
+    try:
+        networkSetup=pars['networkSetup']
+    except KeyError:
+        networkSetup='network.json'
+
+    fconfig=os.path.join(fhome,'.labkey',networkSetup)
+    
+    if not os.path.isfile(fconfig):
+        print('Failed to find network configuration {}'.format(fconfig))
+        return
+
+
+    net=labkeyInterface.labkeyInterface()
+    net.init(fconfig)
+    db=labkeyDatabaseBrowser.labkeyDB(net)
+
+
+    onet=orthancInterface.orthancInterface()
+    onet.init(fconfig)
+    odb=orthancDatabaseBrowser.orthancDB(onet)
+
+
+
+    opars=pars['Orthanc']
+    project=opars['project']
+    
+    participantField=opars['participantField']
+
+    patients=odb.getPatients()
+
+
+    #equivalent for labkey side?
+    dsDemo=db.selectRows(project,opars['schemaName'],\
+            opars['demographicsQuery'],[])
+    dsPatients=[row['OrthancId'] for row in dsDemo['rows']]
+    pMissing=[p for p in patients if p not in dsPatients]
+    print('Missing : {}'.format(len(pMissing)))
+
+    #we need details for all patients (some might have just a study uploaded
+    pdata={p:odb.getPatientData(p) for p in patients}
+    
+    #update patient data for missing patients
+    prows=[]
+    for p in pMissing:
+        dicom=pdata[p]['MainDicomTags']
+        row={}
+        row[participantField]=dicom['PatientID']
+        row['birthDate']=dicom['PatientBirthDate']
+        row['PatientName']=dicom['PatientName']
+        row['OrthancId']=p
+        prows.append(row)
+
+    db.modifyRows('insert',project,opars['schemaName'],\
+                    opars['demographicsQuery'],prows)
+
+    n=len(patients)
+    i=0
+    #download the full images dataset (~1000 rows)
+    ds=db.selectRows(project,opars['schemaName'],opars['queryName'],[])
+    for p in patients:
+        dicom=pdata[p]['MainDicomTags']
+        patientId=dicom['PatientID']
+
+        #get all studies for pateint
+        #qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+        dsStudies=[row['orthancStudy'] for row in ds['rows'] if row[participantField]==patientId]
+        #number of entries for patient (if new must be inserted)
+        seqNum=len(dsStudies)
+        
+        #unique
+        dsStudies=list(set(dsStudies))
+        studies=pdata[p]['Studies']
+        pHex=p.split('-')
+
+        print("[{:>3d}/{:>3d}] ID: {:>10s} [{}] Studies: {:>2}/{:>2} Entries in DB: {:>4d}".format(i,n,patientId,pHex[-1],len(dsStudies),len(studies),seqNum))
+        missingStudies=[s for s in studies if s not in dsStudies]
+        #print('Missing studies: {}'.format(len(missingStudies)))
+
+        srows=[]
+        for s in missingStudies:
+
+            sdata=odb.getStudyData(s)
+            sdicom=sdata['MainDicomTags']
+            sid=sdicom['StudyInstanceUID']
+            #print('Data: {}'.format(sdata))
+            #this is try/except protetcted in previous version...
+            sdate="19700101"
+            try:
+                sdate=sdicom['StudyDate']
+            except KeyError:
+                pass
+            #continue
+        
+            print('\tStudy[{}]: {}/{}'.format(sdate,s,sid))
+        
+            for se in sdata['Series']:
+
+                sedata=odb.getSeriesData(se)
+                sedicom=sedata['MainDicomTags']
+                    
+                seid=sedicom['SeriesInstanceUID']
+                #print('Data: {}'.format(sedata))
+                seDesc="NONE"
+                try:
+                    seDesc=sedicom['SeriesDescription']
+                except KeyError:
+                    pass
+
+                #replace letters that might trip the database
+                spanishOAcute=''.join([chr(3619),chr(3603)])
+                spanishAAcute=''.join([chr(3619),chr(3585)])
+                seDesc=re.sub(spanishOAcute,'o',seDesc)
+                seDesc=re.sub(spanishAAcute,'a',seDesc)
+
+
+                print('\t\tSeries[{}]: {}/{}'.format(seDesc,se,seid))
+
+                #qfilter={'variable':'orthancSeries','value':se,'oper':'eq'}
+                #ds=db.selectRows(project,opars['schemaName'],\
+                #        opars['queryName'],[qfilter])
+                
+                #count existing entries for patient
+                #qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+                #ds=db.selectRows(project,opars['schemaName'],\
+                #opars['queryName'],[qfilter])
+                #    seqNum=len(ds['rows'])
+                    
+                #create new row
+                row={}
+                row[participantField]=patientId
+                row['sequenceNum']=seqNum
+                row['orthancSeries']=se
+                
+                
+                row['dicomStudy']=sid
+                row['orthancStudy']=s
+                row['dicomSeries']=seid
+                row['studyDate']=sdate
+                row['seriesDescription']=seDesc
+                srows.append(row)
+                seqNum+=1
+
+        #upload updates
+        if len(srows)>0:
+            db.modifyRows('insert',project,opars['schemaName'],\
+                opars['queryName'],srows)
+
+
+        i+=1
+    print("Done")
+
+if __name__=="__main__":
+    main(sys.argv[1])

+ 202 - 0
pythonScripts/scanOrthancQuickLocal.py

@@ -0,0 +1,202 @@
+#labkey/orthanc interface
+
+#scans the orthanc internal database and fills labkey table
+
+#"Orthanc" section expected in the configuration file, with
+#"queryName": query to be filled for each image 
+#"demographicsQuery": query to be filled for every patient
+#"schemaName": the schema both queries are part of
+#"project": name of the project under labkey that collects Orthanc data,
+#           typically named Orthanc or similar
+#"participantField": sorting participant field label in labkey study
+
+
+import os
+import json
+import re
+import sys
+import datetime
+
+def main(parameterFile):
+
+   fhome=os.path.expanduser('~')
+   fsetup=os.path.join(fhome,'.labkey','setup.json')
+
+   with open(fsetup,'r') as f:
+     setup=json.load(f)
+
+   sys.path.insert(0,setup['paths']['nixWrapper'])
+   sys.path.insert(0,setup['paths']['orthancInterface'])
+   sys.path.insert(0,setup['paths']['labkeyInterface'])
+   import nixWrapper
+
+#nixWrapper.loadLibrary("labkeyInterface")
+   import labkeyInterface
+   import labkeyDatabaseBrowser
+
+#nixWrapper.loadLibrary('orthancInterface')
+   import orthancInterface
+   import orthancDatabaseBrowser
+
+   fconfig=os.path.join(fhome,'.labkey','network.json')
+
+   net=labkeyInterface.labkeyInterface()
+   net.init(fconfig)
+   db=labkeyDatabaseBrowser.labkeyDB(net)
+
+
+   onet=orthancInterface.orthancInterface()
+   onet.init(fconfig)
+   odb=orthancDatabaseBrowser.orthancDB(onet)
+
+
+   with open(parameterFile) as f:
+      pars=json.load(f)
+
+   opars=pars['Orthanc']
+   project=opars['project']
+
+   participantField=opars['participantField']
+
+   patients=odb.getPatients()
+
+
+#equivalent for labkey side?
+   dsDemo=db.selectRows(project,opars['schemaName'],\
+         opars['demographicsQuery'],[])
+   dsPatients=[row['OrthancId'] for row in dsDemo['rows']]
+   pMissing=[p for p in patients if p not in dsPatients]
+   print('Missing : {}'.format(len(pMissing)))
+
+#we need details for all patients (some might have just a study uploaded
+   pdata={p:odb.getPatientData(p) for p in patients}
+
+#update patient data for missing patients
+   prows=[]
+   for p in pMissing:
+      dicom=pdata[p]['MainDicomTags']
+      row={}
+      pid=dicom['PatientID']
+      if len(pid)>32:
+         pid=pid[:32]
+      row[participantField]=pid
+      
+      try:
+         row['birthDate']=datetime.datetime.strptime(dicom['PatientBirthDate'],'%Y-%m-%d %H:%M')
+      except (KeyError,ValueError):
+         pass
+      
+      row['PatientName']=dicom['PatientName']
+      row['OrthancId']=p
+      prows.append(row)
+
+   if len(prows)>0:
+
+      resp=db.modifyRows('insert',project,opars['schemaName'],\
+         opars['demographicsQuery'],prows)
+      print(resp)
+
+   n=len(patients)
+   i=0
+#download the full images dataset (~1000 rows)
+   ds=db.selectRows(project,opars['schemaName'],opars['queryName'],[])
+   for p in patients:
+      dicom=pdata[p]['MainDicomTags']
+      patientId=dicom['PatientID']
+      if len(patientId)>32:
+         patientId=patientId[:32]
+
+#get all studies for pateint
+#qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+      dsStudies=[row['orthancStudy'] for row in ds['rows'] if row[participantField]==patientId]
+#number of entries for patient (if new must be inserted)
+      seqNum=len(dsStudies)
+
+#unique
+      dsStudies=list(set(dsStudies))
+      studies=pdata[p]['Studies']
+      pHex=p.split('-')
+
+      print("[{:>3d}/{:>3d}] ID: {:>10s} [{}] Studies: {:>2}/{:>2} Entries in DB: {:>4d}".format(i,n,patientId,pHex[-1],len(dsStudies),len(studies),seqNum))
+      missingStudies=[s for s in studies if s not in dsStudies]
+#print('Missing studies: {}'.format(len(missingStudies)))
+
+      srows=[]
+      for s in missingStudies:
+
+         sdata=odb.getStudyData(s)
+         sdicom=sdata['MainDicomTags']
+         sid=sdicom['StudyInstanceUID']
+         #print('Data: {}'.format(sdata))
+         #this is try/except protetcted in previous version...
+         sdate="19700101"
+         try:
+             sdate=datetime.datetime.strptime(sdicom['StudyDate'],'%Y-%m-%d %H:%M')
+         except (KeyError,ValueError):
+             pass
+         #continue
+
+         print('\tStudy[{}]: {}/{}'.format(sdate,s,sid))
+
+         for se in sdata['Series']:
+
+            sedata=odb.getSeriesData(se)
+            sedicom=sedata['MainDicomTags']
+              
+            seid=sedicom['SeriesInstanceUID']
+#print('Data: {}'.format(sedata))
+            seDesc="NONE"
+            try:
+              seDesc=sedicom['SeriesDescription']
+            except KeyError:
+              pass
+
+#replace letters that might trip the database
+            spanishOAcute=''.join([chr(3619),chr(3603)])
+            spanishAAcute=''.join([chr(3619),chr(3585)])
+            seDesc=re.sub(spanishOAcute,'o',seDesc)
+            seDesc=re.sub(spanishAAcute,'a',seDesc)
+            #this is a weird O that appears sometimes
+            seDesc=seDesc.replace(chr(212),'O')
+
+
+            print('\t\tSeries[{}]: {}/{}'.format(seDesc,se,seid))
+
+            
+
+#qfilter={'variable':'orthancSeries','value':se,'oper':'eq'}
+#ds=db.selectRows(project,opars['schemaName'],\
+#        opars['queryName'],[qfilter])
+
+#count existing entries for patient
+#qfilter={'variable':participantField,'value':patientId,'oper':'eq'}
+#ds=db.selectRows(project,opars['schemaName'],\
+#opars['queryName'],[qfilter])
+#    seqNum=len(ds['rows'])
+              
+#create new row
+            row={}
+            row[participantField]=patientId
+            row['sequenceNum']=seqNum
+            row['orthancSeries']=se
+
+
+            row['dicomStudy']=sid
+            row['orthancStudy']=s
+            row['dicomSeries']=seid
+            row['studyDate']=sdate
+            row['seriesDescription']=seDesc
+            srows.append(row)
+            seqNum+=1
+
+#upload updates
+      if len(srows)>0:
+         db.modifyRows('insert',project,opars['schemaName'],\
+             opars['queryName'],srows)
+
+
+      i+=1
+   print("Done")
+
+if __name__=="__main__":
+   main(sys.argv[1])

+ 20 - 0
scripts/countUnzipped.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+if [ $# -lt 1 ] ; then
+	echo Usage $0 DIR;
+	exit 0;
+fi;
+
+aDirs=($(echo $1/*/)); 
+aZips=($(echo $1/*.zip));
+
+nDir=${#aDirs[@]}
+
+echo $nDir/${#aZips[@]};
+
+for ((i=0;i<nDir;i++)) ; do
+	echo ${aDirs[$i]};
+done;
+
+
+

+ 21 - 0
scripts/doZip.sh

@@ -0,0 +1,21 @@
+#!/bin/bash
+
+N=1;
+echo Narg $#;
+if [ $# -gt 0 ] ; 
+then
+	N=$1;
+	echo Setting N to $N;
+fi;
+
+arr=($(echo */)); 
+echo Processing $N/${#arr[@]};
+for ((i=0;i<$N;i++)) ; do 
+	q=$(echo ${arr[i]} |sed 's/\///'); 
+	zip -r $q.zip $q/*; 
+	rm -rf $q/*; 
+	rmdir $q; 
+	chown tomcat8:tomcat8 $q.zip; 
+done;
+
+

+ 40 - 0
scripts/moveDicom.sh

@@ -0,0 +1,40 @@
+#copy study from one orthanc instance to another
+#works for orthanc 1.12.0, not for 1.6.1
+#reuqires source network json plus target network json
+
+function parse_cfg () {
+   USER=$(jq -r ".orthanc.user" $1);
+   PASSWORD=$(jq -r ".orthanc.password" $1);
+   SERVER=$(jq -r ".orthanc.server" $1);
+}
+
+
+if [ $# -lt 1 ] ; then
+	echo $#;
+	echo Usage $0 sourceNetworkConfig targetNetworkConfig studyId
+	exit 0;
+fi;
+
+FILE=$HOME/temp/Study.zip
+
+if [ -f $FILE ] ; then
+   rm $FILE;
+fi;
+
+#parse source config
+parse_cfg $1;
+
+CURL="curl -u $USER:$PASSWORD"
+
+$CURL $SERVER/studies/$3/archive > $FILE;
+echo Downloaded study $3 to $FILE;
+
+#target config
+parse_cfg $2;
+
+CURL="curl -u $USER:$PASSWORD"
+
+R=$($CURL -X POST $SERVER/instances --data-binary "@$FILE" | jq -r ".[] | .Status");
+echo $R;
+rm $FILE;
+

+ 51 - 0
scripts/uploadFiles.sh

@@ -0,0 +1,51 @@
+#copy content of directory to orthanc
+#works for orthanc 1.12.0, not for 1.6.1
+
+
+if [ $# -lt 1 ] ; then
+	echo $#;
+	echo Usage $0 DIR networkConfig;
+	exit 0;
+fi;
+
+CFG=$2;
+USER=$(jq -r ".orthanc.user" $CFG);
+PASSWORD=$(jq -r ".orthanc.password" $CFG);
+SERVER=$(jq -r ".orthanc.server" $CFG);
+
+CURL="curl -u $USER:$PASSWORD"
+
+arr=($(echo $1/*.zip));
+
+Nall=${#arr[@]};
+N=$Nall
+
+echo Loading $N/$Nall;
+i=0
+for ((j=0;j<Nall;j++)) ; do
+	#ls ${arr[j]};
+	R=$($CURL -X POST $SERVER/instances --data-binary "@${arr[j]}" |jq -r ".[] | .Status");
+
+		
+	for x in $R ; do
+		#echo $x;
+		if [ $x == "AlreadyStored" ] ; then
+			#echo Done
+			break;
+		else
+			#echo New
+			i=$((i+1));
+			break;
+		fi;
+	done;
+
+	if [ $i -eq $N ] ; then
+		#echo "[$i/$N] Done"
+		break;
+	else
+		#echo "[$i/$N] Not done"
+		:
+	fi;
+	echo "Done [$j/$Nall, new $i]"
+
+done;