| Home | Trees | Indices | Help |
|
|---|
|
|
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 #============================================================
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 # 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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
515
516 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 # 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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
743
744 #--------------------------------------------------------
745 # helper functions
746 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #------------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
1549 #print(orthanc.get_instance_dicom_tags(instance_id, simplified = True))
1550
1551 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sun Nov 10 02:55:34 2019 | http://epydoc.sourceforge.net |