1
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
10 import io
11 import os
12 import sys
13 import re as regex
14 import logging
15 import http.client
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
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
35
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
49
50
51
52
53
54
55
56
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
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
119 tolerance = 60
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)
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
147
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
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
164
165
169
170
171
172
174 _log.info('searching for Orthanc patients matching %s', person)
175
176
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
199
200
201
202
203
204 return []
205
206
208 matching_patients = []
209 _log.info('searching for patients with external ID >>>%s<<<', external_id)
210
211
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
222 for match in matches:
223 self.protect_patient(orthanc_id = match['ID'])
224 return matches
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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
267
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
313
314
319
320
329
330
339
340
350
351
353
354 if filename is None:
355 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
356
357
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
374 dicomdir_cmd = 'gm-create_dicomdir'
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
391 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name)
392
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
397
398 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True):
399 return False
400
401
402
403 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR')
404 gmTools.remove_file(target_dicomdir_name, log_error = False)
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
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
426 if not create_zip:
427 return sandbox_dir
428
429
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
439 gmTools.rmdir(sandbox_dir)
440 return studies_zip
441
442
444
445 if filename is None:
446 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
447
448
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
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
471
472
473
474
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
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
515
516
527
528
539
540
541
542
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
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
569 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
570 return (self.__run_GET(url) == 1)
571
572
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
585
586
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
596
597 if old_patient_id == new_patient_id:
598 return True
599
600 modify_data = {
601 'Replace': {
602 'PatientID': new_patient_id
603
604
605 }
606 , 'Force': True
607
608
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
642
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 dcm_data = f.read()
658 f.close()
659 _log.debug('uploading [%s]', filename)
660 upload_url = '%s/instances' % self.__server_url
661 uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom')
662 if uploaded is False:
663 _log.error('cannot upload [%s]', filename)
664 return False
665 _log.debug(uploaded)
666 if uploaded['Status'] == 'AlreadyStored':
667
668 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path'])
669 available_fields = self.__run_GET(available_fields_url, allow_cached = True)
670 if 'md5' not in available_fields:
671 _log.debug('md5 of instance not available in Orthanc, cannot compare against file md5, trusting Orthanc')
672 return True
673 md5_url = '%s/md5' % available_fields_url
674 md5_db = self.__run_GET(md5_url)
675 md5_file = gmTools.file2md5(filename)
676 if md5_file != md5_db:
677 _log.error('local md5: %s', md5_file)
678 _log.error('in-db md5: %s', md5_db)
679 _log.error('MD5 mismatch !')
680 return False
681 _log.error('MD5 match between file and database')
682 return True
683
684
686 uploaded = []
687 not_uploaded = []
688 for filename in files:
689 success = self.upload_dicom_file(filename, check_mime_type = check_mime_type)
690 if success:
691 uploaded.append(filename)
692 continue
693 not_uploaded.append(filename)
694
695 if len(not_uploaded) > 0:
696 _log.error('not all files uploaded')
697 return (uploaded, not_uploaded)
698
699
700 - def upload_from_directory(self, directory=None, recursive=False, check_mime_type=False, ignore_other_files=True):
701
702
703 def _on_error(exc):
704 _log.error('DICOM (?) file not accessible: %s', exc.filename)
705 _log.error(exc)
706
707
708 _log.debug('uploading DICOM files from [%s]', directory)
709 if not recursive:
710 files2try = os.listdir(directory)
711 _log.debug('found %s files', len(files2try))
712 if ignore_other_files:
713 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
714 _log.debug('DICOM files therein: %s', len(files2try))
715 return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type)
716
717 _log.debug('recursing for DICOM files')
718 uploaded = []
719 not_uploaded = []
720 for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error):
721 _log.debug('recursing into [%s]', curr_root)
722 files2try = [ os.path.join(curr_root, f) for f in curr_root_files ]
723 _log.debug('found %s files', len(files2try))
724 if ignore_other_files:
725 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
726 _log.debug('DICOM files therein: %s', len(files2try))
727 up, not_up = self.upload_dicom_files (
728 files = files2try,
729 check_mime_type = check_mime_type
730 )
731 uploaded.extend(up)
732 not_uploaded.extend(not_up)
733
734 return (uploaded, not_uploaded)
735
736
739
740
741
742
744
745 study_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentPatient', 'Series']
746 series_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentStudy', 'Instances']
747
748 studies_by_patient = []
749 series_keys = {}
750 series_keys_m = {}
751
752
753 for pat in orthanc_patients:
754 pat_dict = {
755 'orthanc_id': pat['ID'],
756 'name': None,
757 'external_id': None,
758 'date_of_birth': None,
759 'gender': None,
760 'studies': []
761 }
762 try:
763 pat_dict['name'] = pat['MainDicomTags']['PatientName'].strip()
764 except KeyError:
765 pass
766 try:
767 pat_dict['external_id'] = pat['MainDicomTags']['PatientID'].strip()
768 except KeyError:
769 pass
770 try:
771 pat_dict['date_of_birth'] = pat['MainDicomTags']['PatientBirthDate'].strip()
772 except KeyError:
773 pass
774 try:
775 pat_dict['gender'] = pat['MainDicomTags']['PatientSex'].strip()
776 except KeyError:
777 pass
778 for key in pat_dict:
779 if pat_dict[key] in ['unknown', '(null)', '']:
780 pat_dict[key] = None
781 pat_dict[key] = cleanup_dicom_string(pat_dict[key])
782 studies_by_patient.append(pat_dict)
783
784
785 orth_studies = self.__run_GET(url = '%s/patients/%s/studies' % (self.__server_url, pat['ID']))
786 if orth_studies is False:
787 _log.error('cannot retrieve studies')
788 return []
789 for orth_study in orth_studies:
790 study_dict = {
791 'orthanc_id': orth_study['ID'],
792 'date': None,
793 'time': None,
794 'description': None,
795 'referring_doc': None,
796 'requesting_doc': None,
797 'performing_doc': None,
798 'operator_name': None,
799 'radiographer_code': None,
800 'radiology_org': None,
801 'radiology_dept': None,
802 'radiology_org_addr': None,
803 'station_name': None,
804 'series': []
805 }
806 try:
807 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip()
808 except KeyError:
809 pass
810 try:
811 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip()
812 except KeyError:
813 pass
814 try:
815 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip()
816 except KeyError:
817 pass
818 try:
819 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip()
820 except KeyError:
821 pass
822 try:
823 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip()
824 except KeyError:
825 pass
826 try:
827 study_dict['radiology_org_addr'] = orth_study['MainDicomTags']['InstitutionAddress'].strip()
828 except KeyError:
829 pass
830 try:
831 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip()
832 if study_dict['radiology_org_addr'] is not None:
833 if study_dict['radiology_org'] in study_dict['radiology_org_addr']:
834 study_dict['radiology_org'] = None
835 except KeyError:
836 pass
837 try:
838 study_dict['radiology_dept'] = orth_study['MainDicomTags']['InstitutionalDepartmentName'].strip()
839 if study_dict['radiology_org'] is not None:
840 if study_dict['radiology_dept'] in study_dict['radiology_org']:
841 study_dict['radiology_dept'] = None
842 if study_dict['radiology_org_addr'] is not None:
843 if study_dict['radiology_dept'] in study_dict['radiology_org_addr']:
844 study_dict['radiology_dept'] = None
845 except KeyError:
846 pass
847 try:
848 study_dict['station_name'] = orth_study['MainDicomTags']['StationName'].strip()
849 if study_dict['radiology_org'] is not None:
850 if study_dict['station_name'] in study_dict['radiology_org']:
851 study_dict['station_name'] = None
852 if study_dict['radiology_org_addr'] is not None:
853 if study_dict['station_name'] in study_dict['radiology_org_addr']:
854 study_dict['station_name'] = None
855 if study_dict['radiology_dept'] is not None:
856 if study_dict['station_name'] in study_dict['radiology_dept']:
857 study_dict['station_name'] = None
858 except KeyError:
859 pass
860 for key in study_dict:
861 if study_dict[key] in ['unknown', '(null)', '']:
862 study_dict[key] = None
863 study_dict[key] = cleanup_dicom_string(study_dict[key])
864 study_dict['all_tags'] = {}
865 try:
866 orth_study['PatientMainDicomTags']
867 except KeyError:
868 orth_study['PatientMainDicomTags'] = pat['MainDicomTags']
869 for key in orth_study.keys():
870 if key == 'MainDicomTags':
871 for mkey in orth_study['MainDicomTags'].keys():
872 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip()
873 continue
874 if key == 'PatientMainDicomTags':
875 for pkey in orth_study['PatientMainDicomTags'].keys():
876 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip()
877 continue
878 study_dict['all_tags'][key] = orth_study[key]
879 _log.debug('study: %s', study_dict['all_tags'].keys())
880 for key in study_keys2hide:
881 try: del study_dict['all_tags'][key]
882 except KeyError: pass
883 pat_dict['studies'].append(study_dict)
884
885
886 for orth_series_id in orth_study['Series']:
887 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id))
888
889 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id))
890 slices = [ s[0] for s in ordered_slices['SlicesShort'] ]
891 if orth_series is False:
892 _log.error('cannot retrieve series')
893 return []
894 series_dict = {
895 'orthanc_id': orth_series['ID'],
896 'instances': slices,
897 'modality': None,
898 'date': None,
899 'time': None,
900 'description': None,
901 'body_part': None,
902 'protocol': None,
903 'performed_procedure_step_description': None,
904 'acquisition_device_processing_description': None,
905 'operator_name': None,
906 'radiographer_code': None,
907 'performing_doc': None
908 }
909 try:
910 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip()
911 except KeyError:
912 pass
913 try:
914 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip()
915 except KeyError:
916 pass
917 try:
918 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip()
919 except KeyError:
920 pass
921 try:
922 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip()
923 except KeyError:
924 pass
925 try:
926 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip()
927 except KeyError:
928 pass
929 try:
930 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip()
931 except KeyError:
932 pass
933 try:
934 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip()
935 except KeyError:
936 pass
937 try:
938 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip()
939 except KeyError:
940 pass
941 try:
942 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip()
943 except KeyError:
944 pass
945 try:
946 series_dict['radiographer_code'] = orth_series['MainDicomTags']['RadiographersCode'].strip()
947 except KeyError:
948 pass
949 try:
950 series_dict['performing_doc'] = orth_series['MainDicomTags']['PerformingPhysicianName'].strip()
951 except KeyError:
952 pass
953 for key in series_dict:
954 if series_dict[key] in ['unknown', '(null)', '']:
955 series_dict[key] = None
956 if series_dict['description'] == series_dict['protocol']:
957 _log.debug('<series description> matches <series protocol>, ignoring protocol')
958 series_dict['protocol'] = None
959 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]:
960 series_dict['performed_procedure_step_description'] = None
961 if series_dict['performed_procedure_step_description'] is not None:
962
963 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE):
964 series_dict['performed_procedure_step_description'] = None
965 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]:
966 series_dict['acquisition_device_processing_description'] = None
967 if series_dict['acquisition_device_processing_description'] is not None:
968
969 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE):
970 series_dict['acquisition_device_processing_description'] = None
971 if series_dict['date'] == study_dict['date']:
972 _log.debug('<series date> matches <study date>, ignoring date')
973 series_dict['date'] = None
974 if series_dict['time'] == study_dict['time']:
975 _log.debug('<series time> matches <study time>, ignoring time')
976 series_dict['time'] = None
977 for key in series_dict:
978 series_dict[key] = cleanup_dicom_string(series_dict[key])
979 series_dict['all_tags'] = {}
980 for key in orth_series.keys():
981 if key == 'MainDicomTags':
982 for mkey in orth_series['MainDicomTags'].keys():
983 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip()
984 continue
985 series_dict['all_tags'][key] = orth_series[key]
986 _log.debug('series: %s', series_dict['all_tags'].keys())
987 for key in series_keys2hide:
988 try: del series_dict['all_tags'][key]
989 except KeyError: pass
990 study_dict['operator_name'] = series_dict['operator_name']
991 study_dict['radiographer_code'] = series_dict['radiographer_code']
992 study_dict['performing_doc'] = series_dict['performing_doc']
993 study_dict['series'].append(series_dict)
994
995 return studies_by_patient
996
997
998
999
1000 - def __run_GET(self, url=None, data=None, allow_cached=False):
1001 if data is None:
1002 data = {}
1003 headers = {}
1004 if not allow_cached:
1005 headers['cache-control'] = 'no-cache'
1006 params = ''
1007 if len(data.keys()) > 0:
1008 params = '?' + urlencode(data)
1009 url_with_params = url + params
1010
1011 try:
1012 response, content = self.__conn.request(url_with_params, 'GET', headers = headers)
1013 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError):
1014 _log.exception('exception in GET')
1015 _log.debug(' url: %s', url_with_params)
1016 _log.debug(' headers: %s', headers)
1017 return False
1018
1019 if response.status not in [ 200 ]:
1020 _log.error('GET returned non-OK status: %s', response.status)
1021 _log.debug(' url: %s', url_with_params)
1022 _log.debug(' headers: %s', headers)
1023 _log.error(' response: %s', response)
1024 _log.debug(' content: %s', content)
1025 return False
1026
1027
1028
1029
1030 if response['content-type'].startswith('text/plain'):
1031
1032
1033
1034
1035 return content.decode('utf8')
1036
1037 if response['content-type'].startswith('application/json'):
1038 try:
1039 return json.loads(content)
1040 except Exception:
1041 return content
1042
1043 return content
1044
1045
1046 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
1047
1048 body = data
1049 headers = {'content-type' : content_type}
1050 if isinstance(data, str):
1051 if content_type is None:
1052 headers['content-type'] = 'text/plain'
1053 elif isinstance(data, bytes):
1054 if content_type is None:
1055 headers['content-type'] = 'application/octet-stream'
1056 else:
1057 body = json.dumps(data)
1058 headers['content-type'] = 'application/json'
1059
1060 try:
1061 try:
1062 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1063 except BrokenPipeError:
1064 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1065 except (socket.error, http.client.ResponseNotReady, OverflowError):
1066 _log.exception('exception in POST')
1067 _log.debug(' url: %s', url)
1068 _log.debug(' headers: %s', headers)
1069 _log.debug(' body: %s', body[:16])
1070 return False
1071
1072 if response.status == 404:
1073 _log.debug('no data, response: %s', response)
1074 if output_file is None:
1075 return []
1076 return False
1077 if response.status not in [ 200, 302 ]:
1078 _log.error('POST returned non-OK status: %s', response.status)
1079 _log.debug(' url: %s', url)
1080 _log.debug(' headers: %s', headers)
1081 _log.debug(' body: %s', body[:16])
1082 _log.error(' response: %s', response)
1083 _log.debug(' content: %s', content)
1084 return False
1085
1086 try:
1087 content = json.loads(content)
1088 except Exception:
1089 pass
1090 if output_file is None:
1091 return content
1092 output_file.write(content)
1093 return True
1094
1095
1096 - def __run_PUT(self, url=None, data=None, content_type=None):
1097
1098 body = data
1099 headers = {'content-type' : content_type}
1100 if isinstance(data, str):
1101 if content_type is None:
1102 headers['content-type'] = 'text/plain'
1103 elif isinstance(data, bytes):
1104 if content_type is None:
1105 headers['content-type'] = 'application/octet-stream'
1106 else:
1107 body = json.dumps(data)
1108 headers['content-type'] = 'application/json'
1109
1110 try:
1111 try:
1112 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1113 except BrokenPipeError:
1114 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1115 except (socket.error, http.client.ResponseNotReady, OverflowError):
1116 _log.exception('exception in PUT')
1117 _log.debug(' url: %s', url)
1118 _log.debug(' headers: %s', headers)
1119 _log.debug(' body: %s', body[:16])
1120 return False
1121
1122 if response.status == 404:
1123 _log.debug('no data, response: %s', response)
1124 return []
1125 if response.status not in [ 200, 302 ]:
1126 _log.error('PUT returned non-OK status: %s', response.status)
1127 _log.debug(' url: %s', url)
1128 _log.debug(' headers: %s', headers)
1129 _log.debug(' body: %s', body[:16])
1130 _log.error(' response: %s', response)
1131 _log.debug(' content: %s', content)
1132 return False
1133
1134 if response['content-type'].startswith('text/plain'):
1135
1136
1137
1138
1139 return content.decode('utf8')
1140
1141 if response['content-type'].startswith('application/json'):
1142 try:
1143 return json.loads(content)
1144 except Exception:
1145 return content
1146
1147 return content
1148
1149
1151 try:
1152 response, content = self.__conn.request(url, 'DELETE')
1153 except (http.client.ResponseNotReady, socket.error, OverflowError):
1154 _log.exception('exception in DELETE')
1155 _log.debug(' url: %s', url)
1156 return False
1157
1158 if response.status not in [ 200 ]:
1159 _log.error('DELETE returned non-OK status: %s', response.status)
1160 _log.debug(' url: %s', url)
1161 _log.error(' response: %s', response)
1162 _log.debug(' content: %s', content)
1163 return False
1164
1165 if response['content-type'].startswith('text/plain'):
1166
1167
1168
1169
1170 return content.decode('utf8')
1171
1172 if response['content-type'].startswith('application/json'):
1173 try:
1174 return json.loads(content)
1175 except Exception:
1176 return content
1177
1178 return content
1179
1180
1182 if not isinstance(dicom_str, str):
1183 return dicom_str
1184 dicom_str = regex.sub('\^+', ' ', dicom_str.strip('^'))
1185
1186 return dicom_str
1187
1188
1189 -def dicomize_pdf(pdf_name=None, title=None, person=None, dcm_name=None, verbose=False, dcm_template_file=None):
1190 assert (pdf_name is not None), '<pdfname> must not be None'
1191 assert (not ((person is None) and (dcm_template_file is None))), '<person> or <dcm_template_file> must not be None'
1192
1193 _log.debug('pdf: %s', pdf_name)
1194 if title is None:
1195 title = pdf_name
1196 if dcm_name is None:
1197 dcm_name = gmTools.get_unique_filename(suffix = '.dcm')
1198 now = gmDateTime.pydt_now_here()
1199 cmd_line = [
1200 'pdf2dcm',
1201 '--title', title,
1202 '--key', '0008,0020=%s' % now.strftime('%Y%M%d'),
1203 '--key', '0008,0023=%s' % now.strftime('%H%m%s.0'),
1204 '--key', '0008,0030=%s' % now.strftime('%H%m%s.0'),
1205 '--key', '0008,0033=%s' % now.strftime('%H%m%s.0')
1206 ]
1207 if dcm_template_file is None:
1208 name = person.active_name
1209 cmd_line.append('--patient-id')
1210 cmd_line.append(person.suggest_external_id(target = 'PACS'))
1211 cmd_line.append('--patient-name')
1212 cmd_line.append(('%s^%s' % (name['lastnames'], name['firstnames'])).replace(' ', '^'))
1213 if person['dob'] is not None:
1214 cmd_line.append('--patient-birthdate')
1215 cmd_line.append(person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False))
1216 if person['gender'] is not None:
1217 cmd_line.append('--patient-sex')
1218 cmd_line.append(_map_gender_gm2dcm[person['gender']])
1219 else:
1220 _log.debug('DCM template file: %s', dcm_template_file)
1221 cmd_line.append('--series-from')
1222 cmd_line.append(dcm_template_file)
1223
1224 if verbose:
1225 cmd_line.append('--log-level')
1226 cmd_line.append('trace')
1227 cmd_line.append(pdf_name)
1228 cmd_line.append(dcm_name)
1229 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose)
1230 if success:
1231 return dcm_name
1232 return None
1233
1234
1235
1236
1237 if __name__ == "__main__":
1238
1239 if len(sys.argv) == 1:
1240 sys.exit()
1241
1242 if sys.argv[1] != 'test':
1243 sys.exit()
1244
1245
1246
1247 from Gnumed.pycommon import gmLog2
1248
1249
1251 orthanc = cOrthancServer()
1252 if not orthanc.connect(host, port, user = None, password = None):
1253 print('error connecting to server:', orthanc.connect_error)
1254 return False
1255 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % (
1256 orthanc.server_identification['Name'],
1257 orthanc.server_identification['DicomAet'],
1258 orthanc.server_identification['Version'],
1259 orthanc.server_identification['DatabaseVersion'],
1260 orthanc.server_identification['ApiVersion']
1261 ))
1262 print('')
1263 print('Please enter patient name parts, separated by SPACE.')
1264
1265 while True:
1266 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1267 if entered_name in ['exit', 'quit', 'bye', None]:
1268 print("user cancelled patient search")
1269 break
1270
1271 pats = orthanc.get_patients_by_external_id(external_id = entered_name)
1272 if len(pats) > 0:
1273 print('Patients found:')
1274 for pat in pats:
1275 print(' -> ', pat)
1276 continue
1277
1278 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1279 print('Patients found:')
1280 for pat in pats:
1281 print(' -> ', pat)
1282 print(' verifying ...')
1283 bad_data = orthanc.verify_patient_data(pat['ID'])
1284 print(' bad data:')
1285 for bad in bad_data:
1286 print(' -> ', bad)
1287 continue
1288
1289 continue
1290
1291 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True)
1292 print('Patients found from studies list:')
1293 for pat in pats:
1294 print(' -> ', pat['name'])
1295 for study in pat['studies']:
1296 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'])))
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309 print('--------')
1310
1311
1313 try:
1314 host = sys.argv[2]
1315 except IndexError:
1316 host = None
1317 try:
1318 port = sys.argv[3]
1319 except IndexError:
1320 port = '8042'
1321
1322 orthanc_console(host, port)
1323
1324
1326 try:
1327 host = sys.argv[2]
1328 port = sys.argv[3]
1329 except IndexError:
1330 host = None
1331 port = '8042'
1332 orthanc = cOrthancServer()
1333 if not orthanc.connect(host, port, user = None, password = None):
1334 print('error connecting to server:', orthanc.connect_error)
1335 return False
1336 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % (
1337 orthanc.server_identification['Name'],
1338 orthanc.server_identification['DicomAet'],
1339 orthanc.server_identification['Version'],
1340 orthanc.server_identification['DatabaseVersion']
1341 ))
1342 print('')
1343 print('Please enter patient name parts, separated by SPACE.')
1344
1345 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1346 if entered_name in ['exit', 'quit', 'bye', None]:
1347 print("user cancelled patient search")
1348 return
1349
1350 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1351 if len(pats) == 0:
1352 print('no patient found')
1353 return
1354
1355 pat = pats[0]
1356 print('test patient:')
1357 print(pat)
1358 old_id = pat['MainDicomTags']['PatientID']
1359 new_id = old_id + '-1'
1360 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1361
1362
1364
1365
1366
1367
1368 host = None
1369 port = '8042'
1370
1371 orthanc = cOrthancServer()
1372 if not orthanc.connect(host, port, user = None, password = None):
1373 print('error connecting to server:', orthanc.connect_error)
1374 return False
1375 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % (
1376 orthanc.server_identification['Name'],
1377 orthanc.server_identification['DicomAet'],
1378 orthanc.server_identification['Version'],
1379 orthanc.server_identification['DatabaseVersion'],
1380 orthanc.server_identification['ApiVersion']
1381 ))
1382 print('')
1383
1384
1385 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1386
1387
1406
1407
1428
1429
1430
1439
1440
1441
1442
1443
1444
1445
1446 test_pdf2dcm()
1447