Package Gnumed :: Package business :: Module gmDICOM
[frames] | no frames]

Source Code for Module Gnumed.business.gmDICOM

   1  # -*- coding: utf-8 -*- 
   2  #============================================================ 
   3  __doc__ = """GNUmed DICOM handling middleware""" 
   4   
   5  __license__ = "GPL v2 or later" 
   6  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>" 
   7   
   8   
   9  # stdlib 
  10  import io 
  11  import os 
  12  import sys 
  13  import re as regex 
  14  import logging 
  15  import http.client              # exception names used by httplib2 
  16  import socket 
  17  import httplib2 
  18  import json 
  19  import zipfile 
  20  import shutil 
  21  import time 
  22  import datetime as pydt 
  23  from urllib.parse import urlencode 
  24  import distutils.version as version 
  25   
  26   
  27  # GNUmed modules 
  28  if __name__ == '__main__': 
  29          sys.path.insert(0, '../../') 
  30  from Gnumed.pycommon import gmTools 
  31  from Gnumed.pycommon import gmShellAPI 
  32  from Gnumed.pycommon import gmMimeLib 
  33  from Gnumed.pycommon import gmDateTime 
  34  #from Gnumed.pycommon import gmHooks 
  35  #from Gnumed.pycommon import gmDispatcher 
  36   
  37  _log = logging.getLogger('gm.dicom') 
  38   
  39  _map_gender_gm2dcm = { 
  40          'm': 'M', 
  41          'f': 'F', 
  42          'tm': 'M', 
  43          'tf': 'F', 
  44          'h': 'O' 
  45  } 
  46   
  47  #============================================================ 
48 -class cOrthancServer:
49 # REST API access to Orthanc DICOM servers 50 51 # def __init__(self): 52 # self.__server_identification = None 53 # self.__user = None 54 # self.__password = None 55 # self.__conn = None 56 # self.__server_url = None 57 58 #--------------------------------------------------------
59 - def connect(self, host, port, user, password, expected_minimal_version=None, expected_name=None, expected_aet=None):
60 try: 61 int(port) 62 except Exception: 63 _log.error('invalid port [%s]', port) 64 return False 65 if (host is None) or (host.strip() == ''): 66 host = 'localhost' 67 try: 68 self.__server_url = str('http://%s:%s' % (host, port)) 69 except Exception: 70 _log.exception('cannot create server url from: host [%s] and port [%s]', host, port) 71 return False 72 self.__user = user 73 self.__password = password 74 _log.info('connecting as [%s] to Orthanc server at [%s]', self.__user, self.__server_url) 75 cache_dir = os.path.join(gmTools.gmPaths().user_tmp_dir, '.orthanc2gm-cache') 76 gmTools.mkdir(cache_dir, 0o700) 77 _log.debug('using cache directory: %s', cache_dir) 78 self.__conn = httplib2.Http(cache = cache_dir) 79 self.__conn.add_credentials(self.__user, self.__password) 80 _log.debug('connected to server: %s', self.server_identification) 81 self.connect_error = '' 82 if self.server_identification is False: 83 self.connect_error += 'retrieving server identification failed' 84 return False 85 if expected_minimal_version is not None: 86 if version.LooseVersion(self.server_identification['Version']) < version.LooseVersion(expected_min_version): 87 _log.error('server too old, needed [%s]', expected_min_version) 88 self.connect_error += 'server too old, needed version [%s]' % expected_min_version 89 return False 90 if expected_name is not None: 91 if self.server_identification['Name'] != expected_name: 92 _log.error('wrong server name, expected [%s]', expected_name) 93 self.connect_error += 'wrong server name, expected [%s]' % expected_name 94 return False 95 if expected_aet is not None: 96 if self.server_identification['DicomAet'] != expected_name: 97 _log.error('wrong server AET, expected [%s]', expected_aet) 98 self.connect_error += 'wrong server AET, expected [%s]' % expected_aet 99 return False 100 return True
101 102 #--------------------------------------------------------
103 - def _get_server_identification(self):
104 try: 105 return self.__server_identification 106 except AttributeError: 107 pass 108 system_data = self.__run_GET(url = '%s/system' % self.__server_url) 109 if system_data is False: 110 _log.error('unable to get server identification') 111 return False 112 _log.debug('server: %s', system_data) 113 self.__server_identification = system_data 114 115 self.__initial_orthanc_encoding = self.__run_GET(url = '%s/tools/default-encoding' % self.__server_url) 116 _log.debug('initial Orthanc encoding: %s', self.__initial_orthanc_encoding) 117 118 # check time skew 119 tolerance = 60 # seconds 120 client_now_as_utc = pydt.datetime.utcnow() 121 start = time.time() 122 orthanc_now_str = self.__run_GET(url = '%s/tools/now' % self.__server_url) # 20180208T165832 123 end = time.time() 124 query_duration = end - start 125 orthanc_now_unknown_tz = pydt.datetime.strptime(orthanc_now_str, '%Y%m%dT%H%M%S') 126 _log.debug('GNUmed "now" (UTC): %s', client_now_as_utc) 127 _log.debug('Orthanc "now" (UTC): %s', orthanc_now_unknown_tz) 128 _log.debug('wire roundtrip (seconds): %s', query_duration) 129 _log.debug('maximum skew tolerance (seconds): %s', tolerance) 130 if query_duration > tolerance: 131 _log.info('useless to check GNUmed/Orthanc time skew, wire roundtrip (%s) > tolerance (%s)', query_duration, tolerance) 132 else: 133 if orthanc_now_unknown_tz > client_now_as_utc: 134 real_skew = orthanc_now_unknown_tz - client_now_as_utc 135 else: 136 real_skew = client_now_as_utc - orthanc_now_unknown_tz 137 _log.info('GNUmed/Orthanc time skew: %s', real_skew) 138 if real_skew > pydt.timedelta(seconds = tolerance): 139 _log.error('GNUmed/Orthanc time skew > tolerance (may be due to timezone differences on Orthanc < v1.3.2)') 140 141 return self.__server_identification
142 143 server_identification = property(_get_server_identification, lambda x:x) 144 145 #--------------------------------------------------------
146 - def _get_as_external_id_issuer(self):
147 # fixed type :: user level instance name :: DICOM AET 148 return 'Orthanc::%(Name)s::%(DicomAet)s' % self.__server_identification
149 150 as_external_id_issuer = property(_get_as_external_id_issuer, lambda x:x) 151 152 #--------------------------------------------------------
153 - def _get_url_browse_patients(self):
154 if self.__user is None: 155 return self.__server_url 156 return self.__server_url.replace('http://', 'http://%s@' % self.__user)
157 158 url_browse_patients = property(_get_url_browse_patients, lambda x:x) 159 160 #--------------------------------------------------------
161 - def get_url_browse_patient(self, patient_id):
162 # http://localhost:8042/#patient?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 163 return '%s/#patient?uuid=%s' % (self.url_browse_patients, patient_id)
164 165 #--------------------------------------------------------
166 - def get_url_browse_study(self, study_id):
167 # http://localhost:8042/#study?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 168 return '%s/#study?uuid=%s' % (self.url_browse_patients, study_id)
169 170 #-------------------------------------------------------- 171 # download API 172 #--------------------------------------------------------
173 - def get_matching_patients(self, person):
174 _log.info('searching for Orthanc patients matching %s', person) 175 176 # look for patient by external ID first 177 pacs_ids = person.get_external_ids(id_type = 'PACS', issuer = self.as_external_id_issuer) 178 if len(pacs_ids) > 1: 179 _log.error('GNUmed patient has more than one ID for this PACS: %s', pacs_ids) 180 _log.error('the PACS ID is expected to be unique per PACS') 181 return [] 182 183 pacs_ids2use = [] 184 185 if len(pacs_ids) == 1: 186 pacs_ids2use.append(pacs_ids[0]['value']) 187 pacs_ids2use.extend(person.suggest_external_ids(target = 'PACS')) 188 189 for pacs_id in pacs_ids2use: 190 _log.debug('using PACS ID [%s]', pacs_id) 191 pats = self.get_patients_by_external_id(external_id = pacs_id) 192 if len(pats) > 1: 193 _log.warning('more than one Orthanc patient matches PACS ID: %s', pacs_id) 194 if len(pats) > 0: 195 return pats 196 197 _log.debug('no matching patient found in PACS') 198 # return find type ? especially useful for non-matches on ID 199 200 # search by name 201 202 # # then look for name parts 203 # name = person.get_active_name() 204 return []
205 206 #--------------------------------------------------------
207 - def get_patients_by_external_id(self, external_id=None):
208 matching_patients = [] 209 _log.info('searching for patients with external ID >>>%s<<<', external_id) 210 211 # elegant server-side approach: 212 search_data = { 213 'Level': 'Patient', 214 'CaseSensitive': False, 215 'Expand': True, 216 'Query': {'PatientID': external_id.strip('*')} 217 } 218 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data) 219 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data) 220 221 # paranoia 222 for match in matches: 223 self.protect_patient(orthanc_id = match['ID']) 224 return matches
225 226 # # recursive brute force approach: 227 # for pat_id in self.__run_GET(url = '%s/patients' % self.__server_url): 228 # orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id)) 229 # orthanc_external_id = orthanc_pat['MainDicomTags']['PatientID'] 230 # if orthanc_external_id != external_id: 231 # continue 232 # _log.debug(u'match: %s (name=[%s], orthanc_id=[%s])', orthanc_external_id, orthanc_pat['MainDicomTags']['PatientName'], orthanc_pat['ID']) 233 # matching_patients.append(orthanc_pat) 234 # if len(matching_patients) == 0: 235 # _log.debug(u'no matches') 236 # return matching_patients 237 238 #--------------------------------------------------------
239 - def get_patients_by_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
240 _log.info('name parts %s, gender [%s], dob [%s], fuzzy: %s', name_parts, gender, dob, fuzzy) 241 if len(name_parts) > 1: 242 return self.get_patients_by_name_parts(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy) 243 if not fuzzy: 244 search_term = name_parts[0].strip('*') 245 else: 246 search_term = name_parts[0] 247 if not search_term.endswith('*'): 248 search_term += '*' 249 search_data = { 250 'Level': 'Patient', 251 'CaseSensitive': False, 252 'Expand': True, 253 'Query': {'PatientName': search_term} 254 } 255 if gender is not None: 256 gender = _map_gender_gm2dcm[gender.lower()] 257 if gender is not None: 258 search_data['Query']['PatientSex'] = gender 259 if dob is not None: 260 search_data['Query']['PatientBirthDate'] = dob.strftime('%Y%m%d') 261 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data) 262 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data) 263 return matches
264 265 #--------------------------------------------------------
266 - def get_patients_by_name_parts(self, name_parts=None, gender=None, dob=None, fuzzy=False):
267 # fuzzy: allow partial/substring matches (but not across name part boundaries ',' or '^') 268 matching_patients = [] 269 clean_parts = [] 270 for part in name_parts: 271 if part.strip() == '': 272 continue 273 clean_parts.append(part.lower().strip()) 274 _log.info('client-side patient search, scrubbed search terms: %s', clean_parts) 275 pat_ids = self.__run_GET(url = '%s/patients' % self.__server_url) 276 if pat_ids is False: 277 _log.error('cannot retrieve patients') 278 return [] 279 for pat_id in pat_ids: 280 orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id)) 281 if orthanc_pat is False: 282 _log.error('cannot retrieve patient') 283 continue 284 orthanc_name = orthanc_pat['MainDicomTags']['PatientName'].lower().strip() 285 if not fuzzy: 286 orthanc_name = orthanc_name.replace(' ', ',').replace('^', ',').split(',') 287 parts_in_orthanc_name = 0 288 for part in clean_parts: 289 if part in orthanc_name: 290 parts_in_orthanc_name += 1 291 if parts_in_orthanc_name == len(clean_parts): 292 _log.debug('name match: "%s" contains all of %s', orthanc_name, clean_parts) 293 if gender is not None: 294 gender = _map_gender_gm2dcm[gender.lower()] 295 if gender is not None: 296 if orthanc_pat['MainDicomTags']['PatientSex'].lower() != gender: 297 _log.debug('gender mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientSex'], gender) 298 continue 299 if dob is not None: 300 if orthanc_pat['MainDicomTags']['PatientBirthDate'] != dob.strftime('%Y%m%d'): 301 _log.debug('dob mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientBirthDate'], dob) 302 continue 303 matching_patients.append(orthanc_pat) 304 else: 305 _log.debug('name mismatch: "%s" does not contain all of %s', orthanc_name, clean_parts) 306 return matching_patients
307 308 #--------------------------------------------------------
309 - def get_studies_list_by_patient_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
310 return self.get_studies_list_by_orthanc_patient_list ( 311 orthanc_patients = self.get_patients_by_name(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy) 312 )
313 314 #--------------------------------------------------------
315 - def get_studies_list_by_external_id(self, external_id=None):
316 return self.get_studies_list_by_orthanc_patient_list ( 317 orthanc_patients = self.get_patients_by_external_id(external_id = external_id) 318 )
319 320 #--------------------------------------------------------
321 - def get_study_as_zip(self, study_id=None, filename=None):
322 if filename is None: 323 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 324 _log.info('exporting study [%s] into [%s]', study_id, filename) 325 f = io.open(filename, 'wb') 326 f.write(self.__run_GET(url = '%s/studies/%s/archive' % (self.__server_url, str(study_id)), allow_cached = True)) 327 f.close() 328 return filename
329 330 #--------------------------------------------------------
331 - def get_study_as_zip_with_dicomdir(self, study_id=None, filename=None):
332 if filename is None: 333 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 334 _log.info('exporting study [%s] into [%s]', study_id, filename) 335 f = io.open(filename, 'wb') 336 f.write(self.__run_GET(url = '%s/studies/%s/media' % (self.__server_url, str(study_id)), allow_cached = True)) 337 f.close() 338 return filename
339 340 #--------------------------------------------------------
341 - def get_studies_as_zip(self, study_ids=None, patient_id=None, filename=None):
342 if filename is None: 343 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 344 if study_ids is None: 345 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 346 f = io.open(filename, 'wb') 347 f.write(self.__run_GET(url = '%s/patients/%s/archive' % (self.__server_url, str(patient_id)), allow_cached = True)) 348 f.close() 349 return filename
350 351 #--------------------------------------------------------
352 - def _manual_get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
353 354 if filename is None: 355 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 356 357 # all studies 358 if study_ids is None: 359 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 360 f = io.open(filename, 'wb') 361 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id)) 362 _log.debug(url) 363 f.write(self.__run_GET(url = url, allow_cached = True)) 364 f.close() 365 if create_zip: 366 return filename 367 if target_dir is None: 368 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 369 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 370 return False 371 return target_dir 372 373 # a selection of studies 374 dicomdir_cmd = 'gm-create_dicomdir' # args: 1) name of DICOMDIR to create 2) base directory where to start recursing for DICOM files 375 found, external_cmd = gmShellAPI.detect_external_binary(dicomdir_cmd) 376 if not found: 377 _log.error('[%s] not found', dicomdir_cmd) 378 return False 379 380 if create_zip: 381 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 382 _log.info('exporting studies [%s] into [%s] (sandbox [%s])', study_ids, filename, sandbox_dir) 383 else: 384 sandbox_dir = target_dir 385 _log.info('exporting studies [%s] into [%s]', study_ids, sandbox_dir) 386 _log.debug('sandbox dir: %s', sandbox_dir) 387 idx = 0 388 for study_id in study_ids: 389 study_zip_name = gmTools.get_unique_filename(prefix = 'dcm-', suffix = '.zip') 390 # getting with DICOMDIR returns DICOMDIR compatible subdirs and filenames 391 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name) 392 # non-beautiful per-study dir name required by subsequent DICOMDIR generation 393 idx += 1 394 study_unzip_dir = os.path.join(sandbox_dir, 'STUDY%s' % idx) 395 _log.debug('study [%s] -> %s -> %s', study_id, study_zip_name, study_unzip_dir) 396 # need to extract into per-study subdir because get-with-dicomdir 397 # returns identical-across-studies subdirs / filenames 398 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True): 399 return False 400 401 # create DICOMDIR across all studies, 402 # we simply ignore the already existing per-study DICOMDIR files 403 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR') 404 gmTools.remove_file(target_dicomdir_name, log_error = False) # better safe than sorry 405 _log.debug('generating [%s]', target_dicomdir_name) 406 cmd = '%(cmd)s %(DICOMDIR)s %(startdir)s' % { 407 'cmd': external_cmd, 408 'DICOMDIR': target_dicomdir_name, 409 'startdir': sandbox_dir 410 } 411 success = gmShellAPI.run_command_in_shell ( 412 command = cmd, 413 blocking = True 414 ) 415 if not success: 416 _log.error('problem running [gm-create_dicomdir]') 417 return False 418 # paranoia 419 try: 420 io.open(target_dicomdir_name) 421 except Exception: 422 _log.error('[%s] not generated, aborting', target_dicomdir_name) 423 return False 424 425 # return path to extracted studies 426 if not create_zip: 427 return sandbox_dir 428 429 # else return ZIP of all studies 430 studies_zip = shutil.make_archive ( 431 gmTools.fname_stem_with_path(filename), 432 'zip', 433 root_dir = gmTools.parent_dir(sandbox_dir), 434 base_dir = gmTools.dirname_stem(sandbox_dir), 435 logger = _log 436 ) 437 _log.debug('archived all studies with one DICOMDIR into: %s', studies_zip) 438 # studies can be _large_ so attempt to get rid of intermediate files 439 gmTools.rmdir(sandbox_dir) 440 return studies_zip
441 442 #--------------------------------------------------------
443 - def get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
444 445 if filename is None: 446 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 447 448 # all studies 449 if study_ids is None: 450 if patient_id is None: 451 raise ValueError('<patient_id> must be defined if <study_ids> is None') 452 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 453 f = io.open(filename, 'wb') 454 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id)) 455 _log.debug(url) 456 f.write(self.__run_GET(url = url, allow_cached = True)) 457 f.close() 458 if create_zip: 459 return filename 460 if target_dir is None: 461 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 462 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 463 return False 464 return target_dir 465 466 # selection of studies 467 _log.info('exporting %s studies into [%s]', len(study_ids), filename) 468 _log.debug('studies: %s', study_ids) 469 f = io.open(filename, 'wb') 470 # You have to make a POST request against URI "/tools/create-media", with a 471 # JSON body that contains the array of the resources of interest (as Orthanc 472 # identifiers). Here is a sample command-line: 473 # curl -X POST http://localhost:8042/tools/create-media -d '["8c4663df-c3e66066-9e20a8fc-dd14d1e5-251d3d84","2cd4848d-02f0005f-812ffef6-a210bbcf-3f01a00a","6eeded74-75005003-c3ae9738-d4a06a4f-6beedeb8","8a622020-c058291c-7693b63f-bc67aa2e-0a02e69c"]' -v > /tmp/a.zip 474 # (this will not create duplicates but will also not check for single-patient-ness) 475 url = '%s/tools/create-media-extended' % self.__server_url 476 _log.debug(url) 477 try: 478 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f) 479 if not downloaded: 480 _log.error('this Orthanc version probably does not support "create-media-extended"') 481 except TypeError: 482 f.close() 483 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version') 484 return False 485 # retry with old URL 486 if not downloaded: 487 url = '%s/tools/create-media' % self.__server_url 488 _log.debug('retrying: %s', url) 489 try: 490 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f) 491 if not downloaded: 492 return False 493 except TypeError: 494 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version') 495 return False 496 finally: 497 f.close() 498 if create_zip: 499 return filename 500 if target_dir is None: 501 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 502 _log.debug('exporting studies into [%s]', target_dir) 503 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 504 return False 505 return target_dir
506 507 #--------------------------------------------------------
508 - def get_instance_dicom_tags(self, instance_id, simplified=True):
509 _log.debug('retrieving DICOM tags for instance [%s]', instance_id) 510 if simplified: 511 download_url = '%s/instances/%s/simplified-tags' % (self.__server_url, instance_id) 512 else: 513 download_url = '%s/instances/%s/tags' % (self.__server_url, instance_id) 514 return self.__run_GET(url = download_url, allow_cached = True)
515 516 #--------------------------------------------------------
517 - def get_instance_preview(self, instance_id, filename=None):
518 if filename is None: 519 filename = gmTools.get_unique_filename(suffix = '.png') 520 521 _log.debug('exporting preview for instance [%s] into [%s]', instance_id, filename) 522 download_url = '%s/instances/%s/preview' % (self.__server_url, instance_id) 523 f = io.open(filename, 'wb') 524 f.write(self.__run_GET(url = download_url, allow_cached = True)) 525 f.close() 526 return filename
527 528 #--------------------------------------------------------
529 - def get_instance(self, instance_id, filename=None):
530 if filename is None: 531 filename = gmTools.get_unique_filename(suffix = '.dcm') 532 533 _log.debug('exporting instance [%s] into [%s]', instance_id, filename) 534 download_url = '%s/instances/%s/attachments/dicom/data' % (self.__server_url, instance_id) 535 f = io.open(filename, 'wb') 536 f.write(self.__run_GET(url = download_url, allow_cached = True)) 537 f.close() 538 return filename
539 540 #-------------------------------------------------------- 541 # server-side API 542 #--------------------------------------------------------
543 - def protect_patient(self, orthanc_id):
544 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 545 if self.__run_GET(url) == 1: 546 _log.debug('patient already protected: %s', orthanc_id) 547 return True 548 _log.warning('patient [%s] not protected against recycling, enabling protection now', orthanc_id) 549 self.__run_PUT(url = url, data = '1') 550 if self.__run_GET(url) == 1: 551 return True 552 _log.error('cannot protect patient [%s] against recycling', orthanc_id) 553 return False
554 555 #--------------------------------------------------------
556 - def unprotect_patient(self, orthanc_id):
557 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 558 if self.__run_GET(url) == 0: 559 return True 560 _log.info('patient [%s] protected against recycling, disabling protection now', orthanc_id) 561 self.__run_PUT(url = url, data = '0') 562 if self.__run_GET(url) == 0: 563 return True 564 _log.error('cannot unprotect patient [%s] against recycling', orthanc_id) 565 return False
566 567 #--------------------------------------------------------
568 - def patient_is_protected(self, orthanc_id):
569 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 570 return (self.__run_GET(url) == 1)
571 572 #--------------------------------------------------------
573 - def verify_patient_data(self, orthanc_id):
574 _log.info('verifying DICOM data of patient [%s]', orthanc_id) 575 bad_data = [] 576 instances_url = '%s/patients/%s/instances' % (self.__server_url, orthanc_id) 577 instances = self.__run_GET(instances_url) 578 for instance in instances: 579 instance_id = instance['ID'] 580 attachments_url = '%s/instances/%s/attachments' % (self.__server_url, instance_id) 581 attachments = self.__run_GET(attachments_url, allow_cached = True) 582 for attachment in attachments: 583 verify_url = '%s/%s/verify-md5' % (attachments_url, attachment) 584 # False, success = "{}" 585 #2018-02-08 19:11:27 ERROR gm.dicom [-1211701504 MainThread] (gmDICOM.py::__run_POST() #986): cannot POST: http://localhost:8042/instances/5a8206f4-24619e76-6650d9cd-792cdf25-039e96e6/attachments/dicom-as-json/verify-md5 586 #2018-02-08 19:11:27 ERROR gm.dicom [-1211701504 MainThread] (gmDICOM.py::__run_POST() #987): response: {'status': '400', 'content-length': '0'} 587 if self.__run_POST(verify_url) is not False: 588 continue 589 _log.error('bad MD5 of DICOM file at url [%s]: patient=%s, attachment_type=%s', verify_url, orthanc_id, attachment) 590 bad_data.append({'patient': orthanc_id, 'instance': instance_id, 'type': attachment, 'orthanc': '%s [%s]' % (self.server_identification, self.__server_url)}) 591 592 return bad_data
593 594 #--------------------------------------------------------
595 - def modify_patient_id(self, old_patient_id, new_patient_id):
596 597 if old_patient_id == new_patient_id: 598 return True 599 600 modify_data = { 601 'Replace': { 602 'PatientID': new_patient_id 603 #,u'0010,0021': praxis.name / "GNUmed vX.X.X" 604 #,u'0010,1002': series of (old) patient IDs 605 } 606 , 'Force': True 607 # "Keep" doesn't seem to do what it suggests ATM 608 #, u'Keep': True 609 } 610 o_pats = self.get_patients_by_external_id(external_id = old_patient_id) 611 all_modified = True 612 for o_pat in o_pats: 613 _log.info('modifying Orthanc patient [%s]: DICOM ID [%s] -> [%s]', o_pat['ID'], old_patient_id, new_patient_id) 614 if self.patient_is_protected(o_pat['ID']): 615 _log.debug('patient protected: %s, unprotecting for modification', o_pat['ID']) 616 if not self.unprotect_patient(o_pat['ID']): 617 _log.error('cannot unlock patient [%s], skipping', o_pat['ID']) 618 all_modified = False 619 continue 620 was_protected = True 621 else: 622 was_protected = False 623 pat_url = '%s/patients/%s' % (self.__server_url, o_pat['ID']) 624 modify_url = '%s/modify' % pat_url 625 result = self.__run_POST(modify_url, data = modify_data) 626 _log.debug('modified: %s', result) 627 if result is False: 628 _log.error('cannot modify patient [%s]', o_pat['ID']) 629 all_modified = False 630 continue 631 newly_created_patient_id = result['ID'] 632 _log.debug('newly created Orthanc patient ID: %s', newly_created_patient_id) 633 _log.debug('deleting archived patient: %s', self.__run_DELETE(pat_url)) 634 if was_protected: 635 if not self.protect_patient(newly_created_patient_id): 636 _log.error('cannot re-lock (new) patient [%s]', newly_created_patient_id) 637 638 return all_modified
639 640 #-------------------------------------------------------- 641 # upload API 642 #--------------------------------------------------------
643 - def upload_dicom_file(self, filename, check_mime_type=False):
644 if gmTools.fname_stem(filename) == 'DICOMDIR': 645 _log.debug('ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename) 646 return True 647 648 if check_mime_type: 649 if gmMimeLib.guess_mimetype(filename) != 'application/dicom': 650 _log.error('not considered a DICOM file: %s', filename) 651 return False 652 try: 653 f = io.open(filename, 'rb') 654 except Exception: 655 _log.exception('cannot open [%s]', filename) 656 return False 657 658 dcm_data = f.read() 659 f.close() 660 _log.debug('uploading [%s]', filename) 661 upload_url = '%s/instances' % self.__server_url 662 uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom') 663 if uploaded is False: 664 _log.error('cannot upload [%s]', filename) 665 return False 666 667 _log.debug(uploaded) 668 if uploaded['Status'] == 'AlreadyStored': 669 # paranoia, as is our custom 670 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path']) # u'Path': u'/instances/1440110e-9cd02a98-0b1c0452-087d35db-3fd5eb05' 671 available_fields = self.__run_GET(available_fields_url, allow_cached = True) 672 if 'md5' not in available_fields: 673 _log.debug('md5 of instance not available in Orthanc, cannot compare against file md5, trusting Orthanc') 674 return True 675 md5_url = '%s/md5' % available_fields_url 676 md5_db = self.__run_GET(md5_url) 677 md5_file = gmTools.file2md5(filename) 678 if md5_file != md5_db: 679 _log.error('local md5: %s', md5_file) 680 _log.error('in-db md5: %s', md5_db) 681 _log.error('MD5 mismatch !') 682 return False 683 684 _log.error('MD5 match between file and database') 685 686 return True
687 688 #--------------------------------------------------------
689 - def upload_dicom_files(self, files=None, check_mime_type=False):
690 uploaded = [] 691 not_uploaded = [] 692 for filename in files: 693 success = self.upload_dicom_file(filename, check_mime_type = check_mime_type) 694 if success: 695 uploaded.append(filename) 696 continue 697 not_uploaded.append(filename) 698 699 if len(not_uploaded) > 0: 700 _log.error('not all files uploaded') 701 return (uploaded, not_uploaded)
702 703 #--------------------------------------------------------
704 - def upload_from_directory(self, directory=None, recursive=False, check_mime_type=False, ignore_other_files=True):
705 706 #-------------------- 707 def _on_error(exc): 708 _log.error('DICOM (?) file not accessible: %s', exc.filename) 709 _log.error(exc)
710 #-------------------- 711 712 _log.debug('uploading DICOM files from [%s]', directory) 713 if not recursive: 714 files2try = os.listdir(directory) 715 _log.debug('found %s files', len(files2try)) 716 if ignore_other_files: 717 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ] 718 _log.debug('DICOM files therein: %s', len(files2try)) 719 return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type) 720 721 _log.debug('recursing for DICOM files') 722 uploaded = [] 723 not_uploaded = [] 724 for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error): 725 _log.debug('recursing into [%s]', curr_root) 726 files2try = [ os.path.join(curr_root, f) for f in curr_root_files ] 727 _log.debug('found %s files', len(files2try)) 728 if ignore_other_files: 729 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ] 730 _log.debug('DICOM files therein: %s', len(files2try)) 731 up, not_up = self.upload_dicom_files ( 732 files = files2try, 733 check_mime_type = check_mime_type 734 ) 735 uploaded.extend(up) 736 not_uploaded.extend(not_up) 737 738 return (uploaded, not_uploaded)
739 740 #--------------------------------------------------------
741 - def upload_by_DICOMDIR(self, DICOMDIR=None):
742 pass
743 744 #-------------------------------------------------------- 745 # helper functions 746 #--------------------------------------------------------
747 - def get_studies_list_by_orthanc_patient_list(self, orthanc_patients=None):
748 749 study_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentPatient', 'Series'] 750 series_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentStudy', 'Instances'] 751 752 studies_by_patient = [] 753 series_keys = {} 754 series_keys_m = {} 755 756 # loop over patients 757 for pat in orthanc_patients: 758 pat_dict = { 759 'orthanc_id': pat['ID'], 760 'name': None, 761 'external_id': None, 762 'date_of_birth': None, 763 'gender': None, 764 'studies': [] 765 } 766 try: 767 pat_dict['name'] = pat['MainDicomTags']['PatientName'].strip() 768 except KeyError: 769 pass 770 try: 771 pat_dict['external_id'] = pat['MainDicomTags']['PatientID'].strip() 772 except KeyError: 773 pass 774 try: 775 pat_dict['date_of_birth'] = pat['MainDicomTags']['PatientBirthDate'].strip() 776 except KeyError: 777 pass 778 try: 779 pat_dict['gender'] = pat['MainDicomTags']['PatientSex'].strip() 780 except KeyError: 781 pass 782 for key in pat_dict: 783 if pat_dict[key] in ['unknown', '(null)', '']: 784 pat_dict[key] = None 785 pat_dict[key] = cleanup_dicom_string(pat_dict[key]) 786 studies_by_patient.append(pat_dict) 787 788 # loop over studies of patient 789 orth_studies = self.__run_GET(url = '%s/patients/%s/studies' % (self.__server_url, pat['ID'])) 790 if orth_studies is False: 791 _log.error('cannot retrieve studies') 792 return [] 793 for orth_study in orth_studies: 794 study_dict = { 795 'orthanc_id': orth_study['ID'], 796 'date': None, 797 'time': None, 798 'description': None, 799 'referring_doc': None, 800 'requesting_doc': None, 801 'performing_doc': None, 802 'operator_name': None, 803 'radiographer_code': None, 804 'radiology_org': None, 805 'radiology_dept': None, 806 'radiology_org_addr': None, 807 'station_name': None, 808 'series': [] 809 } 810 try: 811 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip() 812 except KeyError: 813 pass 814 try: 815 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip() 816 except KeyError: 817 pass 818 try: 819 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip() 820 except KeyError: 821 pass 822 try: 823 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip() 824 except KeyError: 825 pass 826 try: 827 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip() 828 except KeyError: 829 pass 830 try: 831 study_dict['radiology_org_addr'] = orth_study['MainDicomTags']['InstitutionAddress'].strip() 832 except KeyError: 833 pass 834 try: 835 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip() 836 if study_dict['radiology_org_addr'] is not None: 837 if study_dict['radiology_org'] in study_dict['radiology_org_addr']: 838 study_dict['radiology_org'] = None 839 except KeyError: 840 pass 841 try: 842 study_dict['radiology_dept'] = orth_study['MainDicomTags']['InstitutionalDepartmentName'].strip() 843 if study_dict['radiology_org'] is not None: 844 if study_dict['radiology_dept'] in study_dict['radiology_org']: 845 study_dict['radiology_dept'] = None 846 if study_dict['radiology_org_addr'] is not None: 847 if study_dict['radiology_dept'] in study_dict['radiology_org_addr']: 848 study_dict['radiology_dept'] = None 849 except KeyError: 850 pass 851 try: 852 study_dict['station_name'] = orth_study['MainDicomTags']['StationName'].strip() 853 if study_dict['radiology_org'] is not None: 854 if study_dict['station_name'] in study_dict['radiology_org']: 855 study_dict['station_name'] = None 856 if study_dict['radiology_org_addr'] is not None: 857 if study_dict['station_name'] in study_dict['radiology_org_addr']: 858 study_dict['station_name'] = None 859 if study_dict['radiology_dept'] is not None: 860 if study_dict['station_name'] in study_dict['radiology_dept']: 861 study_dict['station_name'] = None 862 except KeyError: 863 pass 864 for key in study_dict: 865 if study_dict[key] in ['unknown', '(null)', '']: 866 study_dict[key] = None 867 study_dict[key] = cleanup_dicom_string(study_dict[key]) 868 study_dict['all_tags'] = {} 869 try: 870 orth_study['PatientMainDicomTags'] 871 except KeyError: 872 orth_study['PatientMainDicomTags'] = pat['MainDicomTags'] 873 for key in orth_study.keys(): 874 if key == 'MainDicomTags': 875 for mkey in orth_study['MainDicomTags'].keys(): 876 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip() 877 continue 878 if key == 'PatientMainDicomTags': 879 for pkey in orth_study['PatientMainDicomTags'].keys(): 880 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip() 881 continue 882 study_dict['all_tags'][key] = orth_study[key] 883 _log.debug('study: %s', study_dict['all_tags'].keys()) 884 for key in study_keys2hide: 885 try: del study_dict['all_tags'][key] 886 except KeyError: pass 887 pat_dict['studies'].append(study_dict) 888 889 # loop over series in study 890 for orth_series_id in orth_study['Series']: 891 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id)) 892 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id)) 893 if ordered_slices is False: 894 slices = orth_series['Instances'] 895 else: 896 slices = [ s[0] for s in ordered_slices['SlicesShort'] ] 897 if orth_series is False: 898 _log.error('cannot retrieve series') 899 return [] 900 series_dict = { 901 'orthanc_id': orth_series['ID'], 902 'instances': slices, 903 'modality': None, 904 'date': None, 905 'time': None, 906 'description': None, 907 'body_part': None, 908 'protocol': None, 909 'performed_procedure_step_description': None, 910 'acquisition_device_processing_description': None, 911 'operator_name': None, 912 'radiographer_code': None, 913 'performing_doc': None 914 } 915 try: 916 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip() 917 except KeyError: 918 pass 919 try: 920 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip() 921 except KeyError: 922 pass 923 try: 924 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip() 925 except KeyError: 926 pass 927 try: 928 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip() 929 except KeyError: 930 pass 931 try: 932 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip() 933 except KeyError: 934 pass 935 try: 936 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip() 937 except KeyError: 938 pass 939 try: 940 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip() 941 except KeyError: 942 pass 943 try: 944 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip() 945 except KeyError: 946 pass 947 try: 948 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip() 949 except KeyError: 950 pass 951 try: 952 series_dict['radiographer_code'] = orth_series['MainDicomTags']['RadiographersCode'].strip() 953 except KeyError: 954 pass 955 try: 956 series_dict['performing_doc'] = orth_series['MainDicomTags']['PerformingPhysicianName'].strip() 957 except KeyError: 958 pass 959 for key in series_dict: 960 if series_dict[key] in ['unknown', '(null)', '']: 961 series_dict[key] = None 962 if series_dict['description'] == series_dict['protocol']: 963 _log.debug('<series description> matches <series protocol>, ignoring protocol') 964 series_dict['protocol'] = None 965 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]: 966 series_dict['performed_procedure_step_description'] = None 967 if series_dict['performed_procedure_step_description'] is not None: 968 # weed out "numeric" only 969 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE): 970 series_dict['performed_procedure_step_description'] = None 971 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]: 972 series_dict['acquisition_device_processing_description'] = None 973 if series_dict['acquisition_device_processing_description'] is not None: 974 # weed out "numeric" only 975 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE): 976 series_dict['acquisition_device_processing_description'] = None 977 if series_dict['date'] == study_dict['date']: 978 _log.debug('<series date> matches <study date>, ignoring date') 979 series_dict['date'] = None 980 if series_dict['time'] == study_dict['time']: 981 _log.debug('<series time> matches <study time>, ignoring time') 982 series_dict['time'] = None 983 for key in series_dict: 984 series_dict[key] = cleanup_dicom_string(series_dict[key]) 985 series_dict['all_tags'] = {} 986 for key in orth_series.keys(): 987 if key == 'MainDicomTags': 988 for mkey in orth_series['MainDicomTags'].keys(): 989 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip() 990 continue 991 series_dict['all_tags'][key] = orth_series[key] 992 _log.debug('series: %s', series_dict['all_tags'].keys()) 993 for key in series_keys2hide: 994 try: del series_dict['all_tags'][key] 995 except KeyError: pass 996 study_dict['operator_name'] = series_dict['operator_name'] # will collapse all operators into that of the last series 997 study_dict['radiographer_code'] = series_dict['radiographer_code'] # will collapse all into that of the last series 998 study_dict['performing_doc'] = series_dict['performing_doc'] # will collapse all into that of the last series 999 study_dict['series'].append(series_dict) 1000 1001 return studies_by_patient
1002 1003 #-------------------------------------------------------- 1004 # generic REST helpers 1005 #--------------------------------------------------------
1006 - def __run_GET(self, url=None, data=None, allow_cached=False):
1007 if data is None: 1008 data = {} 1009 headers = {} 1010 if not allow_cached: 1011 headers['cache-control'] = 'no-cache' 1012 params = '' 1013 if len(data.keys()) > 0: 1014 params = '?' + urlencode(data) 1015 url_with_params = url + params 1016 1017 try: 1018 response, content = self.__conn.request(url_with_params, 'GET', headers = headers) 1019 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError): 1020 _log.exception('exception in GET') 1021 _log.debug(' url: %s', url_with_params) 1022 _log.debug(' headers: %s', headers) 1023 return False 1024 1025 if response.status not in [ 200 ]: 1026 _log.error('GET returned non-OK status: %s', response.status) 1027 _log.debug(' url: %s', url_with_params) 1028 _log.debug(' headers: %s', headers) 1029 _log.error(' response: %s', response) 1030 _log.debug(' content: %s', content) 1031 return False 1032 1033 # _log.error(' response: %s', response) 1034 # _log.error(' content type: %s', type(content)) 1035 1036 if response['content-type'].startswith('text/plain'): 1037 # utf8 ? 1038 # urldecode ? 1039 # latin1 = Orthanc default = tools/default-encoding ? 1040 # ascii ? 1041 return content.decode('utf8') 1042 1043 if response['content-type'].startswith('application/json'): 1044 try: 1045 return json.loads(content) 1046 except Exception: 1047 return content 1048 1049 return content
1050 1051 #--------------------------------------------------------
1052 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
1053 1054 body = data 1055 headers = {'content-type' : content_type} 1056 if isinstance(data, str): 1057 if content_type is None: 1058 headers['content-type'] = 'text/plain' 1059 elif isinstance(data, bytes): 1060 if content_type is None: 1061 headers['content-type'] = 'application/octet-stream' 1062 else: 1063 body = json.dumps(data) 1064 headers['content-type'] = 'application/json' 1065 1066 try: 1067 try: 1068 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1069 except BrokenPipeError: 1070 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1071 except (socket.error, http.client.ResponseNotReady, OverflowError): 1072 _log.exception('exception in POST') 1073 _log.debug(' url: %s', url) 1074 _log.debug(' headers: %s', headers) 1075 _log.debug(' body: %s', body[:16]) 1076 return False 1077 1078 if response.status == 404: 1079 _log.debug('no data, response: %s', response) 1080 if output_file is None: 1081 return [] 1082 return False 1083 if response.status not in [ 200, 302 ]: 1084 _log.error('POST returned non-OK status: %s', response.status) 1085 _log.debug(' url: %s', url) 1086 _log.debug(' headers: %s', headers) 1087 _log.debug(' body: %s', body[:16]) 1088 _log.error(' response: %s', response) 1089 _log.debug(' content: %s', content) 1090 return False 1091 1092 try: 1093 content = json.loads(content) 1094 except Exception: 1095 pass 1096 if output_file is None: 1097 return content 1098 output_file.write(content) 1099 return True
1100 1101 #--------------------------------------------------------
1102 - def __run_PUT(self, url=None, data=None, content_type=None):
1103 1104 body = data 1105 headers = {'content-type' : content_type} 1106 if isinstance(data, str): 1107 if content_type is None: 1108 headers['content-type'] = 'text/plain' 1109 elif isinstance(data, bytes): 1110 if content_type is None: 1111 headers['content-type'] = 'application/octet-stream' 1112 else: 1113 body = json.dumps(data) 1114 headers['content-type'] = 'application/json' 1115 1116 try: 1117 try: 1118 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1119 except BrokenPipeError: 1120 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1121 except (socket.error, http.client.ResponseNotReady, OverflowError): 1122 _log.exception('exception in PUT') 1123 _log.debug(' url: %s', url) 1124 _log.debug(' headers: %s', headers) 1125 _log.debug(' body: %s', body[:16]) 1126 return False 1127 1128 if response.status == 404: 1129 _log.debug('no data, response: %s', response) 1130 return [] 1131 if response.status not in [ 200, 302 ]: 1132 _log.error('PUT returned non-OK status: %s', response.status) 1133 _log.debug(' url: %s', url) 1134 _log.debug(' headers: %s', headers) 1135 _log.debug(' body: %s', body[:16]) 1136 _log.error(' response: %s', response) 1137 _log.debug(' content: %s', content) 1138 return False 1139 1140 if response['content-type'].startswith('text/plain'): 1141 # utf8 ? 1142 # urldecode ? 1143 # latin1 = Orthanc default = tools/default-encoding ? 1144 # ascii ? 1145 return content.decode('utf8') 1146 1147 if response['content-type'].startswith('application/json'): 1148 try: 1149 return json.loads(content) 1150 except Exception: 1151 return content 1152 1153 return content
1154 1155 #--------------------------------------------------------
1156 - def __run_DELETE(self, url=None):
1157 try: 1158 response, content = self.__conn.request(url, 'DELETE') 1159 except (http.client.ResponseNotReady, socket.error, OverflowError): 1160 _log.exception('exception in DELETE') 1161 _log.debug(' url: %s', url) 1162 return False 1163 1164 if response.status not in [ 200 ]: 1165 _log.error('DELETE returned non-OK status: %s', response.status) 1166 _log.debug(' url: %s', url) 1167 _log.error(' response: %s', response) 1168 _log.debug(' content: %s', content) 1169 return False 1170 1171 if response['content-type'].startswith('text/plain'): 1172 # utf8 ? 1173 # urldecode ? 1174 # latin1 = Orthanc default = tools/default-encoding ? 1175 # ascii ? 1176 return content.decode('utf8') 1177 1178 if response['content-type'].startswith('application/json'): 1179 try: 1180 return json.loads(content) 1181 except Exception: 1182 return content 1183 1184 return content
1185 1186 #------------------------------------------------------------
1187 -def cleanup_dicom_string(dicom_str):
1188 if not isinstance(dicom_str, str): 1189 return dicom_str 1190 dicom_str = regex.sub('\^+', ' ', dicom_str.strip('^')) 1191 #dicom_str = dicom_str.replace('\r\n', ' [CR] ') 1192 return dicom_str
1193 1194 #---------------------------------------------------------------------------
1195 -def dicomize_file(filename, title=None, person=None, dcm_name=None, verbose=False, dcm_template_file=None, dcm_transfer_series=True):
1196 assert (filename is not None), '<filename> must not be None' 1197 assert (not ((person is None) and (dcm_template_file is None))), '<person> or <dcm_template_file> must not be None' 1198 1199 # already DCM ? 1200 if gmMimeLib.guess_mimetype(filename) == 'application/dicom': 1201 _log.error('already a DICOM file: %s', filename) 1202 if dcm_name is None: 1203 return filename 1204 return shutil.copy2(filename, dcm_name) 1205 1206 dcm_fname = dicomize_pdf ( 1207 pdf_name = filename, 1208 title = title, 1209 person = person, 1210 dcm_name = dcm_name, 1211 verbose = verbose, 1212 dcm_template_file = dcm_template_file, 1213 dcm_transfer_series = dcm_transfer_series 1214 ) 1215 if dcm_fname is not None: 1216 return dcm_fname 1217 1218 _log.debug('does not seem to be a PDF: %s', filename) 1219 converted_fname = gmMimeLib.convert_file(filename = filename, target_mime = 'image/jpeg') 1220 if converted_fname is None: 1221 _log.error('cannot convert to JPG: %s', filename) 1222 return None 1223 1224 dcm_name = dicomize_jpg ( 1225 jpg_name = converted_fname, 1226 title = title, 1227 person = person, 1228 dcm_name = dcm_name, 1229 verbose = verbose, 1230 dcm_template_file = dcm_template_file, 1231 dcm_transfer_series = dcm_transfer_series 1232 ) 1233 return dcm_name
1234 1235 #---------------------------------------------------------------------------
1236 -def dicomize_pdf(pdf_name=None, title=None, person=None, dcm_name=None, verbose=False, dcm_template_file=None, dcm_transfer_series=True):
1237 assert (pdf_name is not None), '<pdf_name> must not be None' 1238 assert (not ((person is None) and (dcm_template_file is None))), '<person> or <dcm_template_file> must not be None' 1239 1240 if dcm_name is None: 1241 dcm_name = gmTools.get_unique_filename(suffix = '.dcm') 1242 _log.debug('%s -> %s', pdf_name, dcm_name) 1243 if title is None: 1244 title = pdf_name 1245 now = gmDateTime.pydt_now_here() 1246 cmd_line = [ 1247 'pdf2dcm', 1248 '--title', title, 1249 '--key', '0008,0020=%s' % now.strftime('%Y%m%d'), # StudyDate 1250 '--key', '0008,0021=%s' % now.strftime('%Y%m%d'), # SeriesDate 1251 '--key', '0008,0023=%s' % now.strftime('%Y%m%d'), # ContentDate 1252 '--key', '0008,0030=%s' % now.strftime('%H%M%s.0'), # StudyTime 1253 '--key', '0008,0031=%s' % now.strftime('%H%M%s.0'), # SeriesTime 1254 '--key', '0008,0033=%s' % now.strftime('%H%M%s.0') # ContentTime 1255 ] 1256 if dcm_template_file is None: 1257 name = person.active_name 1258 cmd_line.append('--patient-id') 1259 cmd_line.append(person.suggest_external_id(target = 'PACS')) 1260 cmd_line.append('--patient-name') 1261 cmd_line.append(('%s^%s' % (name['lastnames'], name['firstnames'])).replace(' ', '^')) 1262 if person['dob'] is not None: 1263 cmd_line.append('--patient-birthdate') 1264 cmd_line.append(person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False)) 1265 if person['gender'] is not None: 1266 cmd_line.append('--patient-sex') 1267 cmd_line.append(_map_gender_gm2dcm[person['gender']]) 1268 else: 1269 _log.debug('DCM template file: %s', dcm_template_file) 1270 if dcm_transfer_series: 1271 cmd_line.append('--series-from') 1272 else: 1273 cmd_line.append('--study-from') 1274 cmd_line.append(dcm_template_file) 1275 if verbose: 1276 cmd_line.append('--log-level') 1277 cmd_line.append('trace') 1278 cmd_line.append(pdf_name) 1279 cmd_line.append(dcm_name) 1280 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose) 1281 if success: 1282 return dcm_name 1283 1284 return None
1285 1286 #---------------------------------------------------------------------------
1287 -def dicomize_jpg(jpg_name=None, title=None, person=None, dcm_name=None, verbose=False, dcm_template_file=None, dcm_transfer_series=True):
1288 assert (jpg_name is not None), '<jpg_name> must not be None' 1289 assert (not ((person is None) and (dcm_template_file is None))), 'both <person> and <dcm_template_file> are None, but one is needed' 1290 1291 if dcm_name is None: 1292 dcm_name = gmTools.get_unique_filename(suffix = '.dcm') 1293 _log.debug('%s -> %s', jpg_name, dcm_name) 1294 now = gmDateTime.pydt_now_here() 1295 cmd_line = [ 1296 'img2dcm', 1297 '--keep-appn', # carry over EXIF data 1298 '--insist-on-jfif', # process valid JFIF only 1299 '--key', '0008,0021=%s' % now.strftime('%Y%m%d'), # SeriesDate 1300 '--key', '0008,0031=%s' % now.strftime('%H%M%s.0'), # SeriesTime 1301 '--key', '0008,0023=%s' % now.strftime('%Y%m%d'), # ContentDate 1302 '--key', '0008,0033=%s' % now.strftime('%H%M%s.0') # ContentTime 1303 ] 1304 if title is not None: 1305 # SeriesDescription 1306 if title is not None: 1307 cmd_line.append('--key') 1308 cmd_line.append('0008,103E=%s' % title) 1309 if dcm_template_file is None: 1310 # StudyDescription 1311 if title is not None: 1312 cmd_line.append('--key') 1313 cmd_line.append('0008,1030=%s' % title) 1314 # StudyDate 1315 cmd_line.append('--key') 1316 cmd_line.append('0008,0020=%s' % now.strftime('%Y%m%d')) 1317 # StudyTime 1318 cmd_line.append('--key') 1319 cmd_line.append('0008,0030=%s' % now.strftime('%H%M%s.0')) 1320 # PatientName 1321 name = person.active_name 1322 cmd_line.append('--key') 1323 cmd_line.append('0010,0010=%s' % ('%s^%s' % ( 1324 name['lastnames'], 1325 name['firstnames']) 1326 ).replace(' ', '^')) 1327 # PatientID 1328 cmd_line.append('--key') 1329 cmd_line.append('0010,0020=%s' % person.suggest_external_id(target = 'PACS')) 1330 # DOB 1331 cmd_line.append('--key') 1332 cmd_line.append('0010,0030=%s' % person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False)) 1333 # gender 1334 if person['gender'] is not None: 1335 cmd_line.append('--key') 1336 cmd_line.append('0010,0040=%s' % _map_gender_gm2dcm[person['gender']]) 1337 else: 1338 _log.debug('DCM template file: %s', dcm_template_file) 1339 if dcm_transfer_series: 1340 cmd_line.append('--series-from') 1341 else: 1342 cmd_line.append('--study-from') 1343 cmd_line.append(dcm_template_file) 1344 if verbose: 1345 cmd_line.append('--log-level') 1346 cmd_line.append('trace') 1347 cmd_line.append(jpg_name) 1348 cmd_line.append(dcm_name) 1349 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose) 1350 if success: 1351 return dcm_name 1352 1353 return None
1354 1355 #============================================================ 1356 # main 1357 #------------------------------------------------------------ 1358 if __name__ == "__main__": 1359 1360 if len(sys.argv) == 1: 1361 sys.exit() 1362 1363 if sys.argv[1] != 'test': 1364 sys.exit() 1365 1366 # if __name__ == '__main__': 1367 # sys.path.insert(0, '../../') 1368 from Gnumed.pycommon import gmLog2 1369 1370 #--------------------------------------------------------
1371 - def orthanc_console(host, port):
1372 orthanc = cOrthancServer() 1373 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1374 print('error connecting to server:', orthanc.connect_error) 1375 return False 1376 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % ( 1377 orthanc.server_identification['Name'], 1378 orthanc.server_identification['DicomAet'], 1379 orthanc.server_identification['Version'], 1380 orthanc.server_identification['DatabaseVersion'], 1381 orthanc.server_identification['ApiVersion'] 1382 )) 1383 print('') 1384 print('Please enter patient name parts, separated by SPACE.') 1385 1386 while True: 1387 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1388 if entered_name in ['exit', 'quit', 'bye', None]: 1389 print("user cancelled patient search") 1390 break 1391 1392 pats = orthanc.get_patients_by_external_id(external_id = entered_name) 1393 if len(pats) > 0: 1394 print('Patients found:') 1395 for pat in pats: 1396 print(' -> ', pat) 1397 continue 1398 1399 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1400 print('Patients found:') 1401 for pat in pats: 1402 print(' -> ', pat) 1403 print(' verifying ...') 1404 bad_data = orthanc.verify_patient_data(pat['ID']) 1405 print(' bad data:') 1406 for bad in bad_data: 1407 print(' -> ', bad) 1408 continue 1409 1410 continue 1411 1412 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True) 1413 print('Patients found from studies list:') 1414 for pat in pats: 1415 print(' -> ', pat['name']) 1416 for study in pat['studies']: 1417 print(' ', gmTools.format_dict_like(study, relevant_keys = ['orthanc_id', 'date', 'time'], template = 'study [%%(orthanc_id)s] at %%(date)s %%(time)s contains %s series' % len(study['series']))) 1418 # for series in study['series']: 1419 # print ( 1420 # u' ', 1421 # gmTools.format_dict_like ( 1422 # series, 1423 # relevant_keys = ['orthanc_id', 'date', 'time', 'modality', 'instances', 'body_part', 'protocol', 'description', 'station'], 1424 # template = u'series [%(orthanc_id)s] at %(date)s %(time)s: "%(description)s" %(modality)s@%(station)s (%(protocol)s) of body part "%(body_part)s" holds images:\n%(instances)s' 1425 # ) 1426 # ) 1427 # print(orthanc.get_studies_with_dicomdir(study_ids = [study['orthanc_id']], filename = 'study_%s.zip' % study['orthanc_id'], create_zip = True)) 1428 #print(orthanc.get_study_as_zip(study_id = study['orthanc_id'], filename = 'study_%s.zip' % study['orthanc_id'])) 1429 #print(orthanc.get_studies_as_zip_with_dicomdir(study_ids = [ s['orthanc_id'] for s in pat['studies'] ], filename = 'studies_of_%s.zip' % pat['orthanc_id'])) 1430 print('--------')
1431 1432 #--------------------------------------------------------
1433 - def run_console():
1434 try: 1435 host = sys.argv[2] 1436 except IndexError: 1437 host = None 1438 try: 1439 port = sys.argv[3] 1440 except IndexError: 1441 port = '8042' 1442 1443 orthanc_console(host, port)
1444 1445 #--------------------------------------------------------
1446 - def test_modify_patient_id():
1447 try: 1448 host = sys.argv[2] 1449 port = sys.argv[3] 1450 except IndexError: 1451 host = None 1452 port = '8042' 1453 orthanc = cOrthancServer() 1454 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1455 print('error connecting to server:', orthanc.connect_error) 1456 return False 1457 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1458 orthanc.server_identification['Name'], 1459 orthanc.server_identification['DicomAet'], 1460 orthanc.server_identification['Version'], 1461 orthanc.server_identification['DatabaseVersion'] 1462 )) 1463 print('') 1464 print('Please enter patient name parts, separated by SPACE.') 1465 1466 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1467 if entered_name in ['exit', 'quit', 'bye', None]: 1468 print("user cancelled patient search") 1469 return 1470 1471 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1472 if len(pats) == 0: 1473 print('no patient found') 1474 return 1475 1476 pat = pats[0] 1477 print('test patient:') 1478 print(pat) 1479 old_id = pat['MainDicomTags']['PatientID'] 1480 new_id = old_id + '-1' 1481 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1482 1483 #--------------------------------------------------------
1484 - def test_upload_files():
1485 # try: 1486 # host = sys.argv[2] 1487 # port = sys.argv[3] 1488 # except IndexError: 1489 host = None 1490 port = '8042' 1491 1492 orthanc = cOrthancServer() 1493 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1494 print('error connecting to server:', orthanc.connect_error) 1495 return False 1496 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % ( 1497 orthanc.server_identification['Name'], 1498 orthanc.server_identification['DicomAet'], 1499 orthanc.server_identification['Version'], 1500 orthanc.server_identification['DatabaseVersion'], 1501 orthanc.server_identification['ApiVersion'] 1502 )) 1503 print('') 1504 1505 #orthanc.upload_dicom_file(sys.argv[2]) 1506 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1507 1508 #--------------------------------------------------------
1509 - def test_get_instance_preview():
1510 host = None 1511 port = '8042' 1512 1513 orthanc = cOrthancServer() 1514 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1515 print('error connecting to server:', orthanc.connect_error) 1516 return False 1517 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1518 orthanc.server_identification['Name'], 1519 orthanc.server_identification['DicomAet'], 1520 orthanc.server_identification['Version'], 1521 orthanc.server_identification['DatabaseVersion'] 1522 )) 1523 print('') 1524 1525 print(orthanc.get_instance_preview('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1')) 1526 print(orthanc.get_instance('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1'))
1527 1528 #--------------------------------------------------------
1529 - def test_get_instance_tags():
1530 host = None 1531 port = '8042' 1532 1533 orthanc = cOrthancServer() 1534 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1535 print('error connecting to server:', orthanc.connect_error) 1536 return False 1537 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1538 orthanc.server_identification['Name'], 1539 orthanc.server_identification['DicomAet'], 1540 orthanc.server_identification['Version'], 1541 orthanc.server_identification['DatabaseVersion'] 1542 )) 1543 print('') 1544 1545 instance_id = 'f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1' 1546 for key, value in orthanc.get_instance_dicom_tags(instance_id, simplified = False).items(): 1547 print(key, ':', value) 1548 print()
1549 #print(orthanc.get_instance_dicom_tags(instance_id, simplified = True)) 1550 1551 #--------------------------------------------------------
1552 - def test_pdf2dcm():
1553 #print(pdf2dcm(filename = filename, patient_id = 'ID::abcABC', dob = '19900101')) 1554 from Gnumed.business import gmPerson 1555 pers = gmPerson.cPerson(12) 1556 try: 1557 print(dicomize_pdf(pdf_name = sys.argv[2], person = pers, dcm_name = None, verbose = True, dcm_template_file = sys.argv[3]))#, title = 'test')) 1558 except IndexError: 1559 print(dicomize_pdf(pdf_name = sys.argv[2], person = pers, dcm_name = None, verbose = True))#, title = 'test'))
1560 1561 #--------------------------------------------------------
1562 - def test_img2dcm():
1563 #print(pdf2dcm(filename = filename, patient_id = 'ID::abcABC', dob = '19900101')) 1564 from Gnumed.business import gmPerson 1565 pers = gmPerson.cPerson(12) 1566 try: 1567 print(dicomize_jpg(jpg_name = sys.argv[2], person = pers, dcm_name = sys.argv[2]+'.dcm', verbose = True, dcm_template_file = sys.argv[3]))#, title = 'test')) 1568 except IndexError: 1569 print(dicomize_jpg(jpg_name = sys.argv[2], person = pers, dcm_name = sys.argv[2]+'.dcm', verbose = True))#, title = 'test'))
1570 1571 #--------------------------------------------------------
1572 - def test_file2dcm():
1573 from Gnumed.business import gmPersonSearch 1574 person = gmPersonSearch.ask_for_patient() 1575 if person is None: 1576 return 1577 print(person) 1578 try: 1579 print(dicomize_file(filename = sys.argv[2], person = person, dcm_name = sys.argv[2]+'.dcm', verbose = True, dcm_template_file = sys.argv[3], title = sys.argv[4])) 1580 except IndexError: 1581 pass 1582 try: 1583 print(dicomize_file(filename = sys.argv[2], person = person, dcm_name = sys.argv[2]+'.dcm', verbose = True, title = sys.argv[3])) 1584 except IndexError: 1585 print(dicomize_file(filename = sys.argv[2], person = person, dcm_name = sys.argv[2]+'.dcm', verbose = True))
1586 1587 #-------------------------------------------------------- 1588 #run_console() 1589 #test_modify_patient_id() 1590 #test_upload_files() 1591 #test_get_instance_preview() 1592 #test_get_instance_tags() 1593 #test_pdf2dcm() 1594 #test_img2dcm() 1595 test_file2dcm() 1596