1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Orthanc - A Lightweight, RESTful DICOM Store 5# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics 6# Department, University Hospital of Liege, Belgium 7# Copyright (C) 2017-2021 Osimis S.A., Belgium 8# 9# This program is free software: you can redistribute it and/or 10# modify it under the terms of the GNU General Public License as 11# published by the Free Software Foundation, either version 3 of the 12# License, or (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, but 15# WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17# General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21 22 23import argparse 24import time 25import os 26import os.path 27import sys 28import RestToolbox 29 30parser = argparse.ArgumentParser( 31 description = 'Automated classification of DICOM files from Orthanc.', 32 formatter_class = argparse.ArgumentDefaultsHelpFormatter) 33 34parser.add_argument('--host', default = '127.0.0.1', 35 help = 'The host address that runs Orthanc') 36parser.add_argument('--port', type = int, default = '8042', 37 help = 'The port number to which Orthanc is listening for the REST API') 38parser.add_argument('--target', default = 'OrthancFiles', 39 help = 'The target directory where to store the DICOM files') 40parser.add_argument('--all', action = 'store_true', 41 help = 'Replay the entire history on startup (disabled by default)') 42parser.set_defaults(all = False) 43parser.add_argument('--remove', action = 'store_true', 44 help = 'Remove DICOM files from Orthanc once classified (disabled by default)') 45parser.set_defaults(remove = False) 46 47 48def FixPath(p): 49 return p.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip() 50 51def GetTag(resource, tag): 52 if ('MainDicomTags' in resource and 53 tag in resource['MainDicomTags']): 54 return resource['MainDicomTags'][tag] 55 else: 56 return 'No' + tag 57 58def ClassifyInstance(instanceId): 59 # Extract the patient, study, series and instance information 60 instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId)) 61 series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries'])) 62 study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy'])) 63 patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient'])) 64 65 # Construct a target path 66 a = '%s - %s' % (GetTag(patient, 'PatientID'), 67 GetTag(patient, 'PatientName')) 68 b = GetTag(study, 'StudyDescription') 69 c = '%s - %s' % (GetTag(series, 'Modality'), 70 GetTag(series, 'SeriesDescription')) 71 d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID') 72 73 p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c)) 74 f = os.path.join(p, FixPath(d)) 75 76 # Copy the DICOM file to the target path 77 print('Writing new DICOM file: %s' % f) 78 79 try: 80 os.makedirs(p) 81 except: 82 # Already existing directory, ignore the error 83 pass 84 85 dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId)) 86 with open(f, 'wb') as g: 87 g.write(dcm) 88 89 90# Parse the arguments 91args = parser.parse_args() 92URL = 'http://%s:%d' % (args.host, args.port) 93print('Connecting to Orthanc on address: %s' % URL) 94 95# Compute the starting point for the changes loop 96if args.all: 97 current = 0 98else: 99 current = RestToolbox.DoGet(URL + '/changes?last')['Last'] 100 101# Polling loop using the 'changes' API of Orthanc, waiting for the 102# incoming of new DICOM files 103while True: 104 r = RestToolbox.DoGet(URL + '/changes', { 105 'since' : current, 106 'limit' : 4 # Retrieve at most 4 changes at once 107 }) 108 109 for change in r['Changes']: 110 # We are only interested in the arrival of new instances 111 if change['ChangeType'] == 'NewInstance': 112 try: 113 ClassifyInstance(change['ID']) 114 115 # If requested, remove the instance once it has been 116 # properly handled by "ClassifyInstance()". Thanks to 117 # the "try/except" block, the instance is not removed 118 # if the "ClassifyInstance()" function fails. 119 if args.remove: 120 RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID'])) 121 122 except: 123 print('Unable to write instance %s to the disk' % change['ID']) 124 125 current = r['Last'] 126 127 if r['Done']: 128 print('Everything has been processed: Waiting...') 129 time.sleep(1) 130