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
34
35
36 _log = logging.getLogger('gm.dicom')
37
38 _map_gender_gm2dcm = {
39 'm': 'M',
40 'f': 'F',
41 'tm': 'M',
42 'tf': 'F',
43 'h': 'O'
44 }
45
46
48
49
50
51
52
53
54
55
56
57
58 - def connect(self, host, port, user, password, expected_minimal_version=None, expected_name=None, expected_aet=None):
59 try:
60 int(port)
61 except Exception:
62 _log.error('invalid port [%s]', port)
63 return False
64 if (host is None) or (host.strip() == ''):
65 host = 'localhost'
66 try:
67 self.__server_url = str('http://%s:%s' % (host, port))
68 except Exception:
69 _log.exception('cannot create server url from: host [%s] and port [%s]', host, port)
70 return False
71 self.__user = user
72 self.__password = password
73 _log.info('connecting as [%s] to Orthanc server at [%s]', self.__user, self.__server_url)
74 cache_dir = os.path.join(gmTools.gmPaths().user_tmp_dir, '.orthanc2gm-cache')
75 gmTools.mkdir(cache_dir, 0o700)
76 _log.debug('using cache directory: %s', cache_dir)
77 self.__conn = httplib2.Http(cache = cache_dir)
78 self.__conn.add_credentials(self.__user, self.__password)
79 _log.debug('connected to server: %s', self.server_identification)
80 self.connect_error = ''
81 if self.server_identification is False:
82 self.connect_error += 'retrieving server identification failed'
83 return False
84 if expected_minimal_version is not None:
85 if version.LooseVersion(self.server_identification['Version']) < version.LooseVersion(expected_min_version):
86 _log.error('server too old, needed [%s]', expected_min_version)
87 self.connect_error += 'server too old, needed version [%s]' % expected_min_version
88 return False
89 if expected_name is not None:
90 if self.server_identification['Name'] != expected_name:
91 _log.error('wrong server name, expected [%s]', expected_name)
92 self.connect_error += 'wrong server name, expected [%s]' % expected_name
93 return False
94 if expected_aet is not None:
95 if self.server_identification['DicomAet'] != expected_name:
96 _log.error('wrong server AET, expected [%s]', expected_aet)
97 self.connect_error += 'wrong server AET, expected [%s]' % expected_aet
98 return False
99 return True
100
101
103 try:
104 return self.__server_identification
105 except AttributeError:
106 pass
107 system_data = self.__run_GET(url = '%s/system' % self.__server_url)
108 if system_data is False:
109 _log.error('unable to get server identification')
110 return False
111 _log.debug('server: %s', system_data)
112 self.__server_identification = system_data
113
114 self.__initial_orthanc_encoding = self.__run_GET(url = '%s/tools/default-encoding' % self.__server_url)
115 _log.debug('initial Orthanc encoding: %s', self.__initial_orthanc_encoding)
116
117
118 tolerance = 60
119 client_now_as_utc = pydt.datetime.utcnow()
120 start = time.time()
121 orthanc_now_str = self.__run_GET(url = '%s/tools/now' % self.__server_url)
122 end = time.time()
123 query_duration = end - start
124 orthanc_now_unknown_tz = pydt.datetime.strptime(orthanc_now_str, '%Y%m%dT%H%M%S')
125 _log.debug('GNUmed "now" (UTC): %s', client_now_as_utc)
126 _log.debug('Orthanc "now" (UTC): %s', orthanc_now_unknown_tz)
127 _log.debug('wire roundtrip (seconds): %s', query_duration)
128 _log.debug('maximum skew tolerance (seconds): %s', tolerance)
129 if query_duration > tolerance:
130 _log.info('useless to check GNUmed/Orthanc time skew, wire roundtrip (%s) > tolerance (%s)', query_duration, tolerance)
131 else:
132 if orthanc_now_unknown_tz > client_now_as_utc:
133 real_skew = orthanc_now_unknown_tz - client_now_as_utc
134 else:
135 real_skew = client_now_as_utc - orthanc_now_unknown_tz
136 _log.info('GNUmed/Orthanc time skew: %s', real_skew)
137 if real_skew > pydt.timedelta(seconds = tolerance):
138 _log.error('GNUmed/Orthanc time skew > tolerance (may be due to timezone differences on Orthanc < v1.3.2)')
139
140 return self.__server_identification
141
142 server_identification = property(_get_server_identification, lambda x:x)
143
144
146
147 return 'Orthanc::%(Name)s::%(DicomAet)s' % self.__server_identification
148
149 as_external_id_issuer = property(_get_as_external_id_issuer, lambda x:x)
150
151
153 if self.__user is None:
154 return self.__server_url
155 return self.__server_url.replace('http://', 'http://%s@' % self.__user)
156
157 url_browse_patients = property(_get_url_browse_patients, lambda x:x)
158
159
163
164
168
169
170
171
173 _log.info('searching for Orthanc patients matching %s', person)
174
175
176 pacs_ids = person.get_external_ids(id_type = 'PACS', issuer = self.as_external_id_issuer)
177 if len(pacs_ids) > 1:
178 _log.error('GNUmed patient has more than one ID for this PACS: %s', pacs_ids)
179 _log.error('the PACS ID is expected to be unique per PACS')
180 return []
181
182 pacs_ids2use = []
183
184 if len(pacs_ids) == 1:
185 pacs_ids2use.append(pacs_ids[0]['value'])
186 pacs_ids2use.extend(person.suggest_external_ids(target = 'PACS'))
187
188 for pacs_id in pacs_ids2use:
189 _log.debug('using PACS ID [%s]', pacs_id)
190 pats = self.get_patients_by_external_id(external_id = pacs_id)
191 if len(pats) > 1:
192 _log.warning('more than one Orthanc patient matches PACS ID: %s', pacs_id)
193 if len(pats) > 0:
194 return pats
195
196 _log.debug('no matching patient found in PACS')
197
198
199
200
201
202
203 return []
204
205
207 matching_patients = []
208 _log.info('searching for patients with external ID >>>%s<<<', external_id)
209
210
211 search_data = {
212 'Level': 'Patient',
213 'CaseSensitive': False,
214 'Expand': True,
215 'Query': {'PatientID': external_id.strip('*')}
216 }
217 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data)
218 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data)
219
220
221 for match in matches:
222 self.protect_patient(orthanc_id = match['ID'])
223 return matches
224
225
226
227
228
229
230
231
232
233
234
235
236
237
239 _log.info('name parts %s, gender [%s], dob [%s], fuzzy: %s', name_parts, gender, dob, fuzzy)
240 if len(name_parts) > 1:
241 return self.get_patients_by_name_parts(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy)
242 if not fuzzy:
243 search_term = name_parts[0].strip('*')
244 else:
245 search_term = name_parts[0]
246 if not search_term.endswith('*'):
247 search_term += '*'
248 search_data = {
249 'Level': 'Patient',
250 'CaseSensitive': False,
251 'Expand': True,
252 'Query': {'PatientName': search_term}
253 }
254 if gender is not None:
255 gender = _map_gender_gm2dcm[gender.lower()]
256 if gender is not None:
257 search_data['Query']['PatientSex'] = gender
258 if dob is not None:
259 search_data['Query']['PatientBirthDate'] = dob.strftime('%Y%m%d')
260 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data)
261 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data)
262 return matches
263
264
266
267 matching_patients = []
268 clean_parts = []
269 for part in name_parts:
270 if part.strip() == '':
271 continue
272 clean_parts.append(part.lower().strip())
273 _log.info('client-side patient search, scrubbed search terms: %s', clean_parts)
274 pat_ids = self.__run_GET(url = '%s/patients' % self.__server_url)
275 if pat_ids is False:
276 _log.error('cannot retrieve patients')
277 return []
278 for pat_id in pat_ids:
279 orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id))
280 if orthanc_pat is False:
281 _log.error('cannot retrieve patient')
282 continue
283 orthanc_name = orthanc_pat['MainDicomTags']['PatientName'].lower().strip()
284 if not fuzzy:
285 orthanc_name = orthanc_name.replace(' ', ',').replace('^', ',').split(',')
286 parts_in_orthanc_name = 0
287 for part in clean_parts:
288 if part in orthanc_name:
289 parts_in_orthanc_name += 1
290 if parts_in_orthanc_name == len(clean_parts):
291 _log.debug('name match: "%s" contains all of %s', orthanc_name, clean_parts)
292 if gender is not None:
293 gender = _map_gender_gm2dcm[gender.lower()]
294 if gender is not None:
295 if orthanc_pat['MainDicomTags']['PatientSex'].lower() != gender:
296 _log.debug('gender mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientSex'], gender)
297 continue
298 if dob is not None:
299 if orthanc_pat['MainDicomTags']['PatientBirthDate'] != dob.strftime('%Y%m%d'):
300 _log.debug('dob mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientBirthDate'], dob)
301 continue
302 matching_patients.append(orthanc_pat)
303 else:
304 _log.debug('name mismatch: "%s" does not contain all of %s', orthanc_name, clean_parts)
305 return matching_patients
306
307
312
313
318
319
328
329
338
339
349
350
352
353 if filename is None:
354 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
355
356
357 if study_ids is None:
358 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename)
359 f = io.open(filename, 'wb')
360 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id))
361 _log.debug(url)
362 f.write(self.__run_GET(url = url, allow_cached = True))
363 f.close()
364 if create_zip:
365 return filename
366 if target_dir is None:
367 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
368 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
369 return False
370 return target_dir
371
372
373 dicomdir_cmd = 'gm-create_dicomdir'
374 found, external_cmd = gmShellAPI.detect_external_binary(dicomdir_cmd)
375 if not found:
376 _log.error('[%s] not found', dicomdir_cmd)
377 return False
378
379 if create_zip:
380 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
381 _log.info('exporting studies [%s] into [%s] (sandbox [%s])', study_ids, filename, sandbox_dir)
382 else:
383 sandbox_dir = target_dir
384 _log.info('exporting studies [%s] into [%s]', study_ids, sandbox_dir)
385 _log.debug('sandbox dir: %s', sandbox_dir)
386 idx = 0
387 for study_id in study_ids:
388 study_zip_name = gmTools.get_unique_filename(prefix = 'dcm-', suffix = '.zip')
389
390 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name)
391
392 idx += 1
393 study_unzip_dir = os.path.join(sandbox_dir, 'STUDY%s' % idx)
394 _log.debug('study [%s] -> %s -> %s', study_id, study_zip_name, study_unzip_dir)
395
396
397 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True):
398 return False
399
400
401
402 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR')
403 gmTools.remove_file(target_dicomdir_name, log_error = False)
404 _log.debug('generating [%s]', target_dicomdir_name)
405 cmd = '%(cmd)s %(DICOMDIR)s %(startdir)s' % {
406 'cmd': external_cmd,
407 'DICOMDIR': target_dicomdir_name,
408 'startdir': sandbox_dir
409 }
410 success = gmShellAPI.run_command_in_shell (
411 command = cmd,
412 blocking = True
413 )
414 if not success:
415 _log.error('problem running [gm-create_dicomdir]')
416 return False
417
418 try:
419 io.open(target_dicomdir_name)
420 except Exception:
421 _log.error('[%s] not generated, aborting', target_dicomdir_name)
422 return False
423
424
425 if not create_zip:
426 return sandbox_dir
427
428
429 studies_zip = shutil.make_archive (
430 gmTools.fname_stem_with_path(filename),
431 'zip',
432 root_dir = gmTools.parent_dir(sandbox_dir),
433 base_dir = gmTools.dirname_stem(sandbox_dir),
434 logger = _log
435 )
436 _log.debug('archived all studies with one DICOMDIR into: %s', studies_zip)
437
438 gmTools.rmdir(sandbox_dir)
439 return studies_zip
440
441
443
444 if filename is None:
445 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
446
447
448 if study_ids is None:
449 if patient_id is None:
450 raise ValueError('<patient_id> must be defined if <study_ids> is None')
451 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename)
452 f = io.open(filename, 'wb')
453 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id))
454 _log.debug(url)
455 f.write(self.__run_GET(url = url, allow_cached = True))
456 f.close()
457 if create_zip:
458 return filename
459 if target_dir is None:
460 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
461 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
462 return False
463 return target_dir
464
465
466 _log.info('exporting %s studies into [%s]', len(study_ids), filename)
467 _log.debug('studies: %s', study_ids)
468 f = io.open(filename, 'wb')
469
470
471
472
473
474 url = '%s/tools/create-media-extended' % self.__server_url
475 _log.debug(url)
476 try:
477 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f)
478 if not downloaded:
479 _log.error('this Orthanc version probably does not support "create-media-extended"')
480 except TypeError:
481 f.close()
482 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version')
483 return False
484
485 if not downloaded:
486 url = '%s/tools/create-media' % self.__server_url
487 _log.debug('retrying: %s', url)
488 try:
489 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f)
490 if not downloaded:
491 return False
492 except TypeError:
493 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version')
494 return False
495 finally:
496 f.close()
497 if create_zip:
498 return filename
499 if target_dir is None:
500 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
501 _log.debug('exporting studies into [%s]', target_dir)
502 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
503 return False
504 return target_dir
505
506
514
515
526
527
538
539
540
541
543 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
544 if self.__run_GET(url) == 1:
545 _log.debug('patient already protected: %s', orthanc_id)
546 return True
547 _log.warning('patient [%s] not protected against recycling, enabling protection now', orthanc_id)
548 self.__run_PUT(url = url, data = '1')
549 if self.__run_GET(url) == 1:
550 return True
551 _log.error('cannot protect patient [%s] against recycling', orthanc_id)
552 return False
553
554
556 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
557 if self.__run_GET(url) == 0:
558 return True
559 _log.info('patient [%s] protected against recycling, disabling protection now', orthanc_id)
560 self.__run_PUT(url = url, data = '0')
561 if self.__run_GET(url) == 0:
562 return True
563 _log.error('cannot unprotect patient [%s] against recycling', orthanc_id)
564 return False
565
566
568 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
569 return (self.__run_GET(url) == 1)
570
571
573 _log.info('verifying DICOM data of patient [%s]', orthanc_id)
574 bad_data = []
575 instances_url = '%s/patients/%s/instances' % (self.__server_url, orthanc_id)
576 instances = self.__run_GET(instances_url)
577 for instance in instances:
578 instance_id = instance['ID']
579 attachments_url = '%s/instances/%s/attachments' % (self.__server_url, instance_id)
580 attachments = self.__run_GET(attachments_url, allow_cached = True)
581 for attachment in attachments:
582 verify_url = '%s/%s/verify-md5' % (attachments_url, attachment)
583
584
585
586 if self.__run_POST(verify_url) is not False:
587 continue
588 _log.error('bad MD5 of DICOM file at url [%s]: patient=%s, attachment_type=%s', verify_url, orthanc_id, attachment)
589 bad_data.append({'patient': orthanc_id, 'instance': instance_id, 'type': attachment, 'orthanc': '%s [%s]' % (self.server_identification, self.__server_url)})
590
591 return bad_data
592
593
595
596 if old_patient_id == new_patient_id:
597 return True
598
599 modify_data = {
600 'Replace': {
601 'PatientID': new_patient_id
602
603
604 }
605 , 'Force': True
606
607
608 }
609 o_pats = self.get_patients_by_external_id(external_id = old_patient_id)
610 all_modified = True
611 for o_pat in o_pats:
612 _log.info('modifying Orthanc patient [%s]: DICOM ID [%s] -> [%s]', o_pat['ID'], old_patient_id, new_patient_id)
613 if self.patient_is_protected(o_pat['ID']):
614 _log.debug('patient protected: %s, unprotecting for modification', o_pat['ID'])
615 if not self.unprotect_patient(o_pat['ID']):
616 _log.error('cannot unlock patient [%s], skipping', o_pat['ID'])
617 all_modified = False
618 continue
619 was_protected = True
620 else:
621 was_protected = False
622 pat_url = '%s/patients/%s' % (self.__server_url, o_pat['ID'])
623 modify_url = '%s/modify' % pat_url
624 result = self.__run_POST(modify_url, data = modify_data)
625 _log.debug('modified: %s', result)
626 if result is False:
627 _log.error('cannot modify patient [%s]', o_pat['ID'])
628 all_modified = False
629 continue
630 newly_created_patient_id = result['ID']
631 _log.debug('newly created Orthanc patient ID: %s', newly_created_patient_id)
632 _log.debug('deleting archived patient: %s', self.__run_DELETE(pat_url))
633 if was_protected:
634 if not self.protect_patient(newly_created_patient_id):
635 _log.error('cannot re-lock (new) patient [%s]', newly_created_patient_id)
636
637 return all_modified
638
639
640
641
643 if gmTools.fname_stem(filename) == 'DICOMDIR':
644 _log.debug('ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename)
645 return True
646
647 if check_mime_type:
648 if gmMimeLib.guess_mimetype(filename) != 'application/dicom':
649 _log.error('not considered a DICOM file: %s', filename)
650 return False
651 try:
652 f = io.open(filename, 'rb')
653 except Exception:
654 _log.exception('cannot open [%s]', filename)
655 return False
656 dcm_data = f.read()
657 f.close()
658 _log.debug('uploading [%s]', filename)
659 upload_url = '%s/instances' % self.__server_url
660 uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom')
661 if uploaded is False:
662 _log.error('cannot upload [%s]', filename)
663 return False
664 _log.debug(uploaded)
665 if uploaded['Status'] == 'AlreadyStored':
666
667 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path'])
668 available_fields = self.__run_GET(available_fields_url, allow_cached = True)
669 if 'md5' not in available_fields:
670 _log.debug('md5 of instance not available in Orthanc, cannot compare against file md5, trusting Orthanc')
671 return True
672 md5_url = '%s/md5' % available_fields_url
673 md5_db = self.__run_GET(md5_url)
674 md5_file = gmTools.file2md5(filename)
675 if md5_file != md5_db:
676 _log.error('local md5: %s', md5_file)
677 _log.error('in-db md5: %s', md5_db)
678 _log.error('MD5 mismatch !')
679 return False
680 _log.error('MD5 match between file and database')
681 return True
682
683
685 uploaded = []
686 not_uploaded = []
687 for filename in files:
688 success = self.upload_dicom_file(filename, check_mime_type = check_mime_type)
689 if success:
690 uploaded.append(filename)
691 continue
692 not_uploaded.append(filename)
693
694 if len(not_uploaded) > 0:
695 _log.error('not all files uploaded')
696 return (uploaded, not_uploaded)
697
698
699 - def upload_from_directory(self, directory=None, recursive=False, check_mime_type=False, ignore_other_files=True):
700
701
702 def _on_error(exc):
703 _log.error('DICOM (?) file not accessible: %s', exc.filename)
704 _log.error(exc)
705
706
707 _log.debug('uploading DICOM files from [%s]', directory)
708 if not recursive:
709 files2try = os.listdir(directory)
710 _log.debug('found %s files', len(files2try))
711 if ignore_other_files:
712 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
713 _log.debug('DICOM files therein: %s', len(files2try))
714 return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type)
715
716 _log.debug('recursing for DICOM files')
717 uploaded = []
718 not_uploaded = []
719 for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error):
720 _log.debug('recursing into [%s]', curr_root)
721 files2try = [ os.path.join(curr_root, f) for f in curr_root_files ]
722 _log.debug('found %s files', len(files2try))
723 if ignore_other_files:
724 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
725 _log.debug('DICOM files therein: %s', len(files2try))
726 up, not_up = self.upload_dicom_files (
727 files = files2try,
728 check_mime_type = check_mime_type
729 )
730 uploaded.extend(up)
731 not_uploaded.extend(not_up)
732
733 return (uploaded, not_uploaded)
734
735
738
739
740
741
743
744 study_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentPatient', 'Series']
745 series_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentStudy', 'Instances']
746
747 studies_by_patient = []
748 series_keys = {}
749 series_keys_m = {}
750
751
752 for pat in orthanc_patients:
753 pat_dict = {
754 'orthanc_id': pat['ID'],
755 'name': None,
756 'external_id': None,
757 'date_of_birth': None,
758 'gender': None,
759 'studies': []
760 }
761 try:
762 pat_dict['name'] = pat['MainDicomTags']['PatientName'].strip()
763 except KeyError:
764 pass
765 try:
766 pat_dict['external_id'] = pat['MainDicomTags']['PatientID'].strip()
767 except KeyError:
768 pass
769 try:
770 pat_dict['date_of_birth'] = pat['MainDicomTags']['PatientBirthDate'].strip()
771 except KeyError:
772 pass
773 try:
774 pat_dict['gender'] = pat['MainDicomTags']['PatientSex'].strip()
775 except KeyError:
776 pass
777 for key in pat_dict:
778 if pat_dict[key] in ['unknown', '(null)', '']:
779 pat_dict[key] = None
780 pat_dict[key] = cleanup_dicom_string(pat_dict[key])
781 studies_by_patient.append(pat_dict)
782
783
784 orth_studies = self.__run_GET(url = '%s/patients/%s/studies' % (self.__server_url, pat['ID']))
785 if orth_studies is False:
786 _log.error('cannot retrieve studies')
787 return []
788 for orth_study in orth_studies:
789 study_dict = {
790 'orthanc_id': orth_study['ID'],
791 'date': None,
792 'time': None,
793 'description': None,
794 'referring_doc': None,
795 'requesting_doc': None,
796 'performing_doc': None,
797 'operator_name': None,
798 'radiographer_code': None,
799 'radiology_org': None,
800 'radiology_dept': None,
801 'radiology_org_addr': None,
802 'station_name': None,
803 'series': []
804 }
805 try:
806 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip()
807 except KeyError:
808 pass
809 try:
810 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip()
811 except KeyError:
812 pass
813 try:
814 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip()
815 except KeyError:
816 pass
817 try:
818 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip()
819 except KeyError:
820 pass
821 try:
822 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip()
823 except KeyError:
824 pass
825 try:
826 study_dict['radiology_org_addr'] = orth_study['MainDicomTags']['InstitutionAddress'].strip()
827 except KeyError:
828 pass
829 try:
830 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip()
831 if study_dict['radiology_org_addr'] is not None:
832 if study_dict['radiology_org'] in study_dict['radiology_org_addr']:
833 study_dict['radiology_org'] = None
834 except KeyError:
835 pass
836 try:
837 study_dict['radiology_dept'] = orth_study['MainDicomTags']['InstitutionalDepartmentName'].strip()
838 if study_dict['radiology_org'] is not None:
839 if study_dict['radiology_dept'] in study_dict['radiology_org']:
840 study_dict['radiology_dept'] = None
841 if study_dict['radiology_org_addr'] is not None:
842 if study_dict['radiology_dept'] in study_dict['radiology_org_addr']:
843 study_dict['radiology_dept'] = None
844 except KeyError:
845 pass
846 try:
847 study_dict['station_name'] = orth_study['MainDicomTags']['StationName'].strip()
848 if study_dict['radiology_org'] is not None:
849 if study_dict['station_name'] in study_dict['radiology_org']:
850 study_dict['station_name'] = None
851 if study_dict['radiology_org_addr'] is not None:
852 if study_dict['station_name'] in study_dict['radiology_org_addr']:
853 study_dict['station_name'] = None
854 if study_dict['radiology_dept'] is not None:
855 if study_dict['station_name'] in study_dict['radiology_dept']:
856 study_dict['station_name'] = None
857 except KeyError:
858 pass
859 for key in study_dict:
860 if study_dict[key] in ['unknown', '(null)', '']:
861 study_dict[key] = None
862 study_dict[key] = cleanup_dicom_string(study_dict[key])
863 study_dict['all_tags'] = {}
864 try:
865 orth_study['PatientMainDicomTags']
866 except KeyError:
867 orth_study['PatientMainDicomTags'] = pat['MainDicomTags']
868 for key in orth_study.keys():
869 if key == 'MainDicomTags':
870 for mkey in orth_study['MainDicomTags'].keys():
871 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip()
872 continue
873 if key == 'PatientMainDicomTags':
874 for pkey in orth_study['PatientMainDicomTags'].keys():
875 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip()
876 continue
877 study_dict['all_tags'][key] = orth_study[key]
878 _log.debug('study: %s', study_dict['all_tags'].keys())
879 for key in study_keys2hide:
880 try: del study_dict['all_tags'][key]
881 except KeyError: pass
882 pat_dict['studies'].append(study_dict)
883
884
885 for orth_series_id in orth_study['Series']:
886 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id))
887
888 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id))
889 slices = [ s[0] for s in ordered_slices['SlicesShort'] ]
890 if orth_series is False:
891 _log.error('cannot retrieve series')
892 return []
893 series_dict = {
894 'orthanc_id': orth_series['ID'],
895 'instances': slices,
896 'modality': None,
897 'date': None,
898 'time': None,
899 'description': None,
900 'body_part': None,
901 'protocol': None,
902 'performed_procedure_step_description': None,
903 'acquisition_device_processing_description': None,
904 'operator_name': None,
905 'radiographer_code': None,
906 'performing_doc': None
907 }
908 try:
909 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip()
910 except KeyError:
911 pass
912 try:
913 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip()
914 except KeyError:
915 pass
916 try:
917 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip()
918 except KeyError:
919 pass
920 try:
921 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip()
922 except KeyError:
923 pass
924 try:
925 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip()
926 except KeyError:
927 pass
928 try:
929 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip()
930 except KeyError:
931 pass
932 try:
933 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip()
934 except KeyError:
935 pass
936 try:
937 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip()
938 except KeyError:
939 pass
940 try:
941 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip()
942 except KeyError:
943 pass
944 try:
945 series_dict['radiographer_code'] = orth_series['MainDicomTags']['RadiographersCode'].strip()
946 except KeyError:
947 pass
948 try:
949 series_dict['performing_doc'] = orth_series['MainDicomTags']['PerformingPhysicianName'].strip()
950 except KeyError:
951 pass
952 for key in series_dict:
953 if series_dict[key] in ['unknown', '(null)', '']:
954 series_dict[key] = None
955 if series_dict['description'] == series_dict['protocol']:
956 _log.debug('<series description> matches <series protocol>, ignoring protocol')
957 series_dict['protocol'] = None
958 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]:
959 series_dict['performed_procedure_step_description'] = None
960 if series_dict['performed_procedure_step_description'] is not None:
961
962 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE):
963 series_dict['performed_procedure_step_description'] = None
964 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]:
965 series_dict['acquisition_device_processing_description'] = None
966 if series_dict['acquisition_device_processing_description'] is not None:
967
968 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE):
969 series_dict['acquisition_device_processing_description'] = None
970 if series_dict['date'] == study_dict['date']:
971 _log.debug('<series date> matches <study date>, ignoring date')
972 series_dict['date'] = None
973 if series_dict['time'] == study_dict['time']:
974 _log.debug('<series time> matches <study time>, ignoring time')
975 series_dict['time'] = None
976 for key in series_dict:
977 series_dict[key] = cleanup_dicom_string(series_dict[key])
978 series_dict['all_tags'] = {}
979 for key in orth_series.keys():
980 if key == 'MainDicomTags':
981 for mkey in orth_series['MainDicomTags'].keys():
982 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip()
983 continue
984 series_dict['all_tags'][key] = orth_series[key]
985 _log.debug('series: %s', series_dict['all_tags'].keys())
986 for key in series_keys2hide:
987 try: del series_dict['all_tags'][key]
988 except KeyError: pass
989 study_dict['operator_name'] = series_dict['operator_name']
990 study_dict['radiographer_code'] = series_dict['radiographer_code']
991 study_dict['performing_doc'] = series_dict['performing_doc']
992 study_dict['series'].append(series_dict)
993
994 return studies_by_patient
995
996
997
998
999 - def __run_GET(self, url=None, data=None, allow_cached=False):
1000 if data is None:
1001 data = {}
1002 headers = {}
1003 if not allow_cached:
1004 headers['cache-control'] = 'no-cache'
1005 params = ''
1006 if len(data.keys()) > 0:
1007 params = '?' + urlencode(data)
1008 url_with_params = url + params
1009
1010 try:
1011 response, content = self.__conn.request(url_with_params, 'GET', headers = headers)
1012 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError):
1013 _log.exception('exception in GET')
1014 _log.debug(' url: %s', url_with_params)
1015 _log.debug(' headers: %s', headers)
1016 return False
1017
1018 if response.status not in [ 200 ]:
1019 _log.error('GET returned non-OK status: %s', response.status)
1020 _log.debug(' url: %s', url_with_params)
1021 _log.debug(' headers: %s', headers)
1022 _log.error(' response: %s', response)
1023 _log.debug(' content: %s', content)
1024 return False
1025
1026
1027
1028
1029 if response['content-type'].startswith('text/plain'):
1030
1031
1032
1033
1034 return content.decode('utf8')
1035
1036 if response['content-type'].startswith('application/json'):
1037 try:
1038 return json.loads(content)
1039 except Exception:
1040 return content
1041
1042 return content
1043
1044
1045 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
1046
1047 body = data
1048 headers = {'content-type' : content_type}
1049 if isinstance(data, str):
1050 if content_type is None:
1051 headers['content-type'] = 'text/plain'
1052 elif isinstance(data, bytes):
1053 if content_type is None:
1054 headers['content-type'] = 'application/octet-stream'
1055 else:
1056 body = json.dumps(data)
1057 headers['content-type'] = 'application/json'
1058
1059 try:
1060 try:
1061 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1062 except BrokenPipeError:
1063 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1064 except (socket.error, http.client.ResponseNotReady, OverflowError):
1065 _log.exception('exception in POST')
1066 _log.debug(' url: %s', url)
1067 _log.debug(' headers: %s', headers)
1068 _log.debug(' body: %s', body[:16])
1069 return False
1070
1071 if response.status == 404:
1072 _log.debug('no data, response: %s', response)
1073 if output_file is None:
1074 return []
1075 return False
1076 if response.status not in [ 200, 302 ]:
1077 _log.error('POST returned non-OK status: %s', response.status)
1078 _log.debug(' url: %s', url)
1079 _log.debug(' headers: %s', headers)
1080 _log.debug(' body: %s', body[:16])
1081 _log.error(' response: %s', response)
1082 _log.debug(' content: %s', content)
1083 return False
1084
1085 try:
1086 content = json.loads(content)
1087 except Exception:
1088 pass
1089 if output_file is None:
1090 return content
1091 output_file.write(content)
1092 return True
1093
1094
1095 - def __run_PUT(self, url=None, data=None, content_type=None):
1096
1097 body = data
1098 headers = {'content-type' : content_type}
1099 if isinstance(data, str):
1100 if content_type is None:
1101 headers['content-type'] = 'text/plain'
1102 elif isinstance(data, bytes):
1103 if content_type is None:
1104 headers['content-type'] = 'application/octet-stream'
1105 else:
1106 body = json.dumps(data)
1107 headers['content-type'] = 'application/json'
1108
1109 try:
1110 try:
1111 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1112 except BrokenPipeError:
1113 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1114 except (socket.error, http.client.ResponseNotReady, OverflowError):
1115 _log.exception('exception in PUT')
1116 _log.debug(' url: %s', url)
1117 _log.debug(' headers: %s', headers)
1118 _log.debug(' body: %s', body[:16])
1119 return False
1120
1121 if response.status == 404:
1122 _log.debug('no data, response: %s', response)
1123 return []
1124 if response.status not in [ 200, 302 ]:
1125 _log.error('PUT returned non-OK status: %s', response.status)
1126 _log.debug(' url: %s', url)
1127 _log.debug(' headers: %s', headers)
1128 _log.debug(' body: %s', body[:16])
1129 _log.error(' response: %s', response)
1130 _log.debug(' content: %s', content)
1131 return False
1132
1133 if response['content-type'].startswith('text/plain'):
1134
1135
1136
1137
1138 return content.decode('utf8')
1139
1140 if response['content-type'].startswith('application/json'):
1141 try:
1142 return json.loads(content)
1143 except Exception:
1144 return content
1145
1146 return content
1147
1148
1150 try:
1151 response, content = self.__conn.request(url, 'DELETE')
1152 except (http.client.ResponseNotReady, socket.error, OverflowError):
1153 _log.exception('exception in DELETE')
1154 _log.debug(' url: %s', url)
1155 return False
1156
1157 if response.status not in [ 200 ]:
1158 _log.error('DELETE returned non-OK status: %s', response.status)
1159 _log.debug(' url: %s', url)
1160 _log.error(' response: %s', response)
1161 _log.debug(' content: %s', content)
1162 return False
1163
1164 if response['content-type'].startswith('text/plain'):
1165
1166
1167
1168
1169 return content.decode('utf8')
1170
1171 if response['content-type'].startswith('application/json'):
1172 try:
1173 return json.loads(content)
1174 except Exception:
1175 return content
1176
1177 return content
1178
1179
1181 if not isinstance(dicom_str, str):
1182 return dicom_str
1183 dicom_str = regex.sub('\^+', ' ', dicom_str.strip('^'))
1184
1185 return dicom_str
1186
1187
1188 -def dicomize_pdf(pdf_name=None, title=None, person=None, dcm_name=None, verbose=False):
1189 assert (pdf_name is not None), '<pdfname> must not be None'
1190 assert (person is not None), '<person> must not be None'
1191
1192 if title is None:
1193 title = pdf_name
1194 if dcm_name is None:
1195 dcm_name = gmTools.get_unique_filename(suffix = '.dcm')
1196 name = person.active_name
1197 cmd_line = [
1198 'pdf2dcm',
1199 '--patient-id', person.suggest_external_id(target = 'PACS'),
1200 '--patient-name', ('%s^%s' % (name['lastnames'], name['firstnames'])).replace(' ', '^'),
1201 '--title', title,
1202 '--log-level', 'trace'
1203 ]
1204 if person['dob'] is not None:
1205 cmd_line.append('--patient-birthdate')
1206 cmd_line.append(person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False))
1207 if person['gender'] is not None:
1208 cmd_line.append('--patient-sex')
1209 cmd_line.append(_map_gender_gm2dcm[person['gender']])
1210 cmd_line.append(pdf_name)
1211 cmd_line.append(dcm_name)
1212 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose)
1213 if success:
1214 return dcm_name
1215 return None
1216
1217
1218
1219
1220 if __name__ == "__main__":
1221
1222 if len(sys.argv) == 1:
1223 sys.exit()
1224
1225 if sys.argv[1] != 'test':
1226 sys.exit()
1227
1228
1229
1230 from Gnumed.pycommon import gmLog2
1231
1232
1234 orthanc = cOrthancServer()
1235 if not orthanc.connect(host, port, user = None, password = None):
1236 print('error connecting to server:', orthanc.connect_error)
1237 return False
1238 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % (
1239 orthanc.server_identification['Name'],
1240 orthanc.server_identification['DicomAet'],
1241 orthanc.server_identification['Version'],
1242 orthanc.server_identification['DatabaseVersion'],
1243 orthanc.server_identification['ApiVersion']
1244 ))
1245 print('')
1246 print('Please enter patient name parts, separated by SPACE.')
1247
1248 while True:
1249 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1250 if entered_name in ['exit', 'quit', 'bye', None]:
1251 print("user cancelled patient search")
1252 break
1253
1254 pats = orthanc.get_patients_by_external_id(external_id = entered_name)
1255 if len(pats) > 0:
1256 print('Patients found:')
1257 for pat in pats:
1258 print(' -> ', pat)
1259 continue
1260
1261 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1262 print('Patients found:')
1263 for pat in pats:
1264 print(' -> ', pat)
1265 print(' verifying ...')
1266 bad_data = orthanc.verify_patient_data(pat['ID'])
1267 print(' bad data:')
1268 for bad in bad_data:
1269 print(' -> ', bad)
1270 continue
1271
1272 continue
1273
1274 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True)
1275 print('Patients found from studies list:')
1276 for pat in pats:
1277 print(' -> ', pat['name'])
1278 for study in pat['studies']:
1279 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'])))
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292 print('--------')
1293
1294
1296 try:
1297 host = sys.argv[2]
1298 except IndexError:
1299 host = None
1300 try:
1301 port = sys.argv[3]
1302 except IndexError:
1303 port = '8042'
1304
1305 orthanc_console(host, port)
1306
1307
1309 try:
1310 host = sys.argv[2]
1311 port = sys.argv[3]
1312 except IndexError:
1313 host = None
1314 port = '8042'
1315 orthanc = cOrthancServer()
1316 if not orthanc.connect(host, port, user = None, password = None):
1317 print('error connecting to server:', orthanc.connect_error)
1318 return False
1319 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % (
1320 orthanc.server_identification['Name'],
1321 orthanc.server_identification['DicomAet'],
1322 orthanc.server_identification['Version'],
1323 orthanc.server_identification['DatabaseVersion']
1324 ))
1325 print('')
1326 print('Please enter patient name parts, separated by SPACE.')
1327
1328 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1329 if entered_name in ['exit', 'quit', 'bye', None]:
1330 print("user cancelled patient search")
1331 return
1332
1333 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1334 if len(pats) == 0:
1335 print('no patient found')
1336 return
1337
1338 pat = pats[0]
1339 print('test patient:')
1340 print(pat)
1341 old_id = pat['MainDicomTags']['PatientID']
1342 new_id = old_id + '-1'
1343 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1344
1345
1347
1348
1349
1350
1351 host = None
1352 port = '8042'
1353
1354 orthanc = cOrthancServer()
1355 if not orthanc.connect(host, port, user = None, password = None):
1356 print('error connecting to server:', orthanc.connect_error)
1357 return False
1358 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % (
1359 orthanc.server_identification['Name'],
1360 orthanc.server_identification['DicomAet'],
1361 orthanc.server_identification['Version'],
1362 orthanc.server_identification['DatabaseVersion'],
1363 orthanc.server_identification['ApiVersion']
1364 ))
1365 print('')
1366
1367
1368 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1369
1370
1389
1390
1411
1412
1413
1419
1420
1421
1422
1423
1424
1425
1426 test_pdf2dcm()
1427