'''Command Line Interface (CLI)''' import os import sys import json import logging import argparse import tempfile import traceback import getpass from io import BytesIO import pydicom from dicomweb_client.api import DICOMwebClient, load_json_dataset from dicomweb_client.log import configure_logging from dicomweb_client.session_utils import ( create_session, create_session_from_user_pass, add_certs_to_session, ) logger = logging.getLogger(__name__) def _get_parser(): '''Builds the object for parsing command line arguments. Returns ------- argparse.ArgumentParser ''' parser = argparse.ArgumentParser( description='Client for DICOMweb RESTful services.', prog='dicomweb_client' ) parser.add_argument( '-v', '--verbosity', dest='logging_verbosity', default=0, action='count', help=( 'logging verbosity that maps to a logging level ' '(default: error, -v: warning, -vv: info, -vvv: debug, ' '-vvvv: debug + traceback); ' 'all log messages are written to standard error' ) ) parser.add_argument( '-u', '--user', dest='username', metavar='NAME', help='username for authentication with the DICOMweb service' ) parser.add_argument( '-p', '--password', dest='password', metavar='PASSWORD', help='password for authentication with the DICOMweb service' ) parser.add_argument( '--ca', dest='ca_bundle', metavar='CERT-FILE', help='path to a CA bundle file' ) parser.add_argument( '--cert', dest='cert', metavar='CERT-FILE', help='path to a client certificate file in PEM format' ) parser.add_argument( '--bearer-token', dest='bearer_token', metavar='TOKEN', help='bearer token for authentication with the DICOMweb service' ) parser.add_argument( '--url', dest='url', metavar='URL', help='uniform resource locator of the DICOMweb service' ) parser.add_argument( '--chunk-size', dest='chunk_size', type=int, metavar='NUM', help='maximum size of a network transfer chunk in bytes' ) abstract_optional_study_parser = argparse.ArgumentParser(add_help=False) abstract_optional_study_parser.add_argument( '--study', metavar='UID', dest='study_instance_uid', help='unique study identifer (StudyInstanceUID)' ) abstract_required_study_parser = argparse.ArgumentParser(add_help=False) abstract_required_study_parser.add_argument( '--study', metavar='UID', dest='study_instance_uid', required=True, help='unique study identifier (StudyInstanceUID)' ) abstract_optional_series_parser = argparse.ArgumentParser(add_help=False) abstract_optional_series_parser.add_argument( '--series', metavar='UID', dest='series_instance_uid', help='unique series identifier (SeriesInstanceUID)' ) abstract_required_series_parser = argparse.ArgumentParser(add_help=False) abstract_required_series_parser.add_argument( '--series', metavar='UID', dest='series_instance_uid', required=True, help='unique series identifier (SeriesInstanceUID)' ) abstract_optional_instance_parser = argparse.ArgumentParser(add_help=False) abstract_optional_instance_parser.add_argument( '--instance', metavar='UID', dest='sop_instance_uid', help='unique instance identifier (SOPInstanceUID)' ) abstract_required_instance_parser = argparse.ArgumentParser(add_help=False) abstract_required_instance_parser.add_argument( '--instance', metavar='UID', dest='sop_instance_uid', required=True, help='unique instance identifier (SOPInstanceUID)' ) abstract_search_parser = argparse.ArgumentParser(add_help=False) abstract_search_parser.add_argument( '--filter', metavar='KEY=VALUE', dest='search_filters', action='append', default=[], help='query filter criterion' ) abstract_search_parser.add_argument( '--field', metavar='NAME', dest='search_fields', action='append', help='field that should be included in response' ) abstract_search_parser.add_argument( '--limit', metavar='NUM', type=int, dest='search_limit', help='number of items that should be maximally retrieved' ) abstract_search_parser.add_argument( '--offset', metavar='NUM', type=int, dest='search_offset', help='number of items that should be skipped' ) abstract_search_parser.add_argument( '--fuzzy', dest='search_fuzzymatching', action='store_true', help='perform fuzzy matching' ) abstract_retrieve_parser = argparse.ArgumentParser(add_help=False) abstract_retrieve_parser.add_argument( '--media-type', metavar='MEDIATYPE', action='append', default=None, nargs='+', dest='media_types', help=( 'acceptable media type and the optionally the UID of a ' 'corresponding tranfer syntax separted by a whitespace' '(e.g., "image/jpeg" or "image/jpeg 1.2.840.10008.1.2.4.50")' ) ) abstract_fmt_parser = argparse.ArgumentParser(add_help=False) abstract_fmt_group = abstract_fmt_parser.add_mutually_exclusive_group() abstract_fmt_group.add_argument( '--prettify', action='store_true', help='pretty print JSON response message' ) abstract_fmt_group.add_argument( '--dicomize', action='store_true', help='convert JSON response message to DICOM data set' ) abstract_save_parser = argparse.ArgumentParser(add_help=False) abstract_save_parser.add_argument( '--save', action='store_true', help='whether downloaded data should be saved' ) abstract_save_parser.add_argument( '--output-dir', metavar='PATH', dest='output_dir', default=tempfile.gettempdir(), help='path to directory where downloaded data should be saved' ) abstract_load_parser = argparse.ArgumentParser(add_help=False) abstract_load_parser.add_argument( metavar='PATH', dest='files', nargs='+', help='paths to DICOM files that should be loaded' ) subparsers = parser.add_subparsers(dest='method', help='services') subparsers.required = True # QIDO search_parser = subparsers.add_parser( 'search', description=( 'QIDO-RS: Query based on ID for DICOM Objects by RESTful Serices.' ) ) search_subparsers = search_parser.add_subparsers( dest='qido_ie', metavar='INFORMATION ENTITIES', description='', help='' ) search_subparsers.required = True # QIDO - Studies search_for_studies_parser = search_subparsers.add_parser( 'studies', description='Search for DICOM studies.', parents=[abstract_search_parser, abstract_fmt_parser] ) search_for_studies_parser.set_defaults(func=_search_for_studies) # QUIDO - Series search_for_series_parser = search_subparsers.add_parser( 'series', description='Search for DICOM series.', parents=[ abstract_search_parser, abstract_fmt_parser, abstract_optional_study_parser ] ) search_for_series_parser.set_defaults(func=_search_for_series) # QIDO - Instances search_for_instances_parser = search_subparsers.add_parser( 'instances', description='Search for DICOM instances.', parents=[ abstract_fmt_parser, abstract_search_parser, abstract_optional_study_parser, abstract_optional_series_parser ] ) search_for_instances_parser.set_defaults(func=_search_for_instances) # WADO retrieve_parser = subparsers.add_parser( 'retrieve', description='WADO-RS: Web Access to DICOM Objects by RESTful Services.', ) retrieve_subparsers = retrieve_parser.add_subparsers( dest='wado_ie', metavar='INFORMATION ENTITIES', help='', description='' ) retrieve_subparsers.required = True # WADO - studies retrieve_studies_parser = retrieve_subparsers.add_parser( 'studies', help='retrieve data for instances of a given study', description=( 'Retrieve data for all DICOM instances of a given DICOM study.' ), parents=[abstract_required_study_parser] ) retrieve_studies_subparsers = retrieve_studies_parser.add_subparsers( dest='studies_resource' ) retrieve_studies_subparsers.required = True retrieve_studies_metadata_parser = retrieve_studies_subparsers.add_parser( 'metadata', description=( 'Retrieve metadata of DICOM instances of a given DICOM study.' ), parents=[abstract_fmt_parser, abstract_save_parser] ) retrieve_studies_metadata_parser.set_defaults( func=_retrieve_study_metadata ) retrieve_studies_full_parser = retrieve_studies_subparsers.add_parser( 'full', description=( 'Retrieve DICOM instances of a given DICOM study.' ), parents=[abstract_save_parser, abstract_retrieve_parser] ) retrieve_studies_full_parser.set_defaults(func=_retrieve_study) # WADO - series retrieve_series_parser = retrieve_subparsers.add_parser( 'series', help='retrieve data for instances of a given series', description=( 'Retrieve data for all DICOM instances of a given DICOM series.' ), parents=[ abstract_required_study_parser, abstract_required_series_parser, ] ) retrieve_series_subparsers = retrieve_series_parser.add_subparsers( dest='series_resource' ) retrieve_series_subparsers.required = True retrieve_series_metadata_parser = retrieve_series_subparsers.add_parser( 'metadata', description=( 'Retrieve metadata of DICOM instances of a given DICOM series.' ), parents=[abstract_fmt_parser, abstract_save_parser] ) retrieve_series_metadata_parser.set_defaults( func=_retrieve_series_metadata ) retrieve_series_full_parser = retrieve_series_subparsers.add_parser( 'full', description=( 'Retrieve DICOM instances of a given DICOM series.' ), parents=[abstract_save_parser, abstract_retrieve_parser] ) retrieve_series_full_parser.set_defaults(func=_retrieve_series) # WADO - instance retrieve_instance_parser = retrieve_subparsers.add_parser( 'instances', help='retrieve data of a given instance', description=( 'Retrieve data for an individual DICOM instance.' ), parents=[ abstract_required_study_parser, abstract_required_series_parser, abstract_required_instance_parser ] ) retrieve_instance_subparsers = retrieve_instance_parser.add_subparsers( dest='instances_resource' ) retrieve_instance_subparsers.required = True retrieve_instance_metadata_parser = retrieve_instance_subparsers.add_parser( 'metadata', description=( 'Retrieve metadata of an invidividual DICOM instance.' ), parents=[abstract_fmt_parser, abstract_save_parser] ) retrieve_instance_metadata_parser.set_defaults( func=_retrieve_instance_metadata ) retrieve_instance_full_parser = retrieve_instance_subparsers.add_parser( 'full', description=('Retrieve a DICOM instance.'), parents=[abstract_save_parser, abstract_retrieve_parser] ) retrieve_instance_full_parser.set_defaults(func=_retrieve_instance) retrieve_instance_frames_parser = retrieve_instance_subparsers.add_parser( 'frames', description=( 'Retrieve one or more frames of the pixel data element of an ' 'invidividual DICOM instance.' ), parents=[abstract_save_parser, abstract_retrieve_parser] ) retrieve_instance_frames_parser.add_argument( '--numbers', metavar='NUM', type=int, nargs='+', dest='frame_numbers', help='frame numbers' ) retrieve_instance_frames_parser.add_argument( '--show', action='store_true', help='display retrieved images' ) retrieve_instance_frames_parser.set_defaults(func=_retrieve_instance_frames) # WADO - bulkdata retrieve_bulkdata_parser = retrieve_subparsers.add_parser( 'bulkdata', help='retrieve bulk data from a known location', description=( 'Retrieve bulk data of a DICOM object from a known location.' ), parents=[abstract_retrieve_parser] ) retrieve_bulkdata_parser.add_argument( '--uri', metavar='URI', dest='bulkdata_uri', required=True, help='unique resource identifier of bulk data element' ) retrieve_bulkdata_parser.set_defaults(func=_retrieve_bulkdata) # STOW store_parser = subparsers.add_parser( 'store', description='STOW-RS: Store Over the Web by RESTful Services.', ) store_subparsers = store_parser.add_subparsers( dest='stow_ie', metavar='INFORMATION ENTITIES', help='', description='' ) store_subparsers.required = True # STOW - instances store_studies_parser = store_subparsers.add_parser( 'instances', help='store one or more DICOM instances', description='Store DICOM instances.', parents=[abstract_optional_study_parser, abstract_load_parser] ) store_studies_parser.set_defaults(func=_store_instances) return parser def _parse_search_parameters(args): params = dict() if args.search_fuzzymatching: params['fuzzymatching'] = args.search_fuzzymatching params['offset'] = args.search_offset params['limit'] = args.search_limit params['fields'] = args.search_fields params['search_filters'] = {} for f in args.search_filters: k, v = f.split('=') params['search_filters'][k] = v return params def _print_instance(data): logger.info('print instance') with BytesIO() as fp: pydicom.dcmwrite(fp, data) output = fp.getvalue() print(output) def _print_metadata(data, prettify=False, dicomize=False): logger.info('print metadata') if dicomize: if isinstance(data, list): for ds in data: dcm_ds = load_json_dataset(ds) print(dcm_ds) print('\n') else: dcm_ds = load_json_dataset(data) print(dcm_ds) elif prettify: print(json.dumps(data, indent=4, sort_keys=True)) else: print(json.dumps(data, sort_keys=True)) def _save_instance(data, directory, sop_instance_uid): filename = '{}.dcm'.format(sop_instance_uid) filepath = os.path.join(directory, filename) logger.info('save instance to file: {}'.format(filepath)) pydicom.dcmwrite(filepath, data) def _save_metadata(data, directory, sop_instance_uid, prettify=False, dicomize=False): if dicomize: filename = '{}.dcm'.format(sop_instance_uid) else: filename = '{}.json'.format(sop_instance_uid) filepath = os.path.join(directory, filename) logger.info('save metadata to file: {}'.format(filepath)) if dicomize: dataset = load_json_dataset(data) dataset.save_as(filepath) else: with open(filepath, 'w') as f: if prettify: json.dump(data, f, indent=4, sort_keys=True) else: json.dump(data, f, sort_keys=True) def _print_pixel_data(pixels): logger.info('print pixel data') print(pixels) print('\n') def _create_headers(args): headers = {} if hasattr(args, "bearer_token"): headers = { "Authorization": "Bearer {}".format(args.bearer_token) } return headers def _search_for_studies(client, args): '''Searches for *Studies* and writes metadata to standard output.''' params = _parse_search_parameters(args) studies = client.search_for_studies(**params) _print_metadata(studies, args.prettify, args.dicomize) def _search_for_series(client, args): '''Searches for Series and writes metadata to standard output.''' params = _parse_search_parameters(args) series = client.search_for_series(args.study_instance_uid, **params) _print_metadata(series, args.prettify, args.dicomize) def _search_for_instances(client, args): '''Searches for Instances and writes metadata to standard output.''' params = _parse_search_parameters(args) instances = client.search_for_instances( args.study_instance_uid, args.series_instance_uid, **params ) _print_metadata(instances, args.prettify, args.dicomize) def _retrieve_study(client, args): '''Retrieves all Instances of a given Study and either writes them to standard output or to files on disk. ''' instances = client.retrieve_study( args.study_instance_uid, media_types=args.media_types, ) for inst in instances: sop_instance_uid = inst.SOPInstanceUID if args.save: _save_instance(inst, args.output_dir, sop_instance_uid) else: _print_instance(inst) def _retrieve_series(client, args): '''Retrieves all Instances of a given Series and either writes them to standard output or to files on disk. ''' instances = client.retrieve_series( args.study_instance_uid, args.series_instance_uid, media_types=args.media_types, ) for inst in instances: sop_instance_uid = inst.SOPInstanceUID if args.save: _save_instance(inst, args.output_dir, sop_instance_uid) else: _print_instance(inst) def _retrieve_instance(client, args): '''Retrieves an Instances and either writes it to standard output or to a file on disk. ''' instance = client.retrieve_instance( args.study_instance_uid, args.series_instance_uid, args.sop_instance_uid, media_types=args.media_types, ) if args.save: _save_instance(instance, args.output_dir, args.sop_instance_uid) else: _print_instance(instance) def _retrieve_study_metadata(client, args): '''Retrieves metadata for all Instances of a given Study and either writes it to standard output or to files on disk. ''' metadata = client.retrieve_study_metadata(args.study_instance_uid) if args.save: for md in metadata: tag = client.lookup_tag('SOPInstanceUID') sop_instance_uid = md[tag]['Value'][0] _save_metadata( md, args.output_dir, sop_instance_uid, args.prettify, args.dicomize ) else: _print_metadata(metadata, args.prettify, args.dicomize) def _retrieve_series_metadata(client, args): '''Retrieves metadata for all Instances of a given Series and either writes it to standard output or to files on disk. ''' metadata = client.retrieve_series_metadata( args.study_instance_uid, args.series_instance_uid ) if args.save: for md in metadata: tag = client.lookup_tag('SOPInstanceUID') sop_instance_uid = md[tag]['Value'][0] _save_metadata( md, args.output_dir, sop_instance_uid, args.prettify, args.dicomize ) else: _print_metadata(metadata, args.prettify, args.dicomize) def _retrieve_instance_metadata(client, args): '''Retrieves metadata for an individual Instances and either writes it to standard output or to a file on disk. ''' metadata = client.retrieve_instance_metadata( args.study_instance_uid, args.series_instance_uid, args.sop_instance_uid ) if args.save: _save_metadata( metadata, args.output_dir, args.sop_instance_uid, args.prettify, args.dicomize ) else: _print_metadata(metadata, args.prettify, args.dicomize) def _retrieve_instance_frames(client, args): '''Retrieves frames for an individual instances and either writes them to standard output or files on disk or displays them in a GUI (depending on the requested content type). Frames can only be saved and shown if they are retrieved using image media types. ''' pixel_data = client.retrieve_instance_frames( args.study_instance_uid, args.series_instance_uid, args.sop_instance_uid, args.frame_numbers, media_types=args.media_types, ) for i, data in enumerate(pixel_data): if args.save: if data[:2] == b'\xFF\xD8': # SOI marker => JPEG if data[2:4] == b'\xFF\xF7': # SOF 55 marker => JPEG-LS extension = 'jls' else: extension = 'jpg' elif data[:2] == b'\xFF\x4F': # SOC marker => JPEG 2000 extension = 'jp2' else: extension = 'dat' filename = ( '{sop_instance_uid}_{frame_number}.{extension}'.format( sop_instance_uid=args.sop_instance_uid, frame_number=args.frame_numbers[i], extension=extension ) ) filepath = os.path.join(args.output_dir, filename) with open(filepath, 'bw') as fp: fp.write(data) else: _print_pixel_data(data) def _retrieve_bulkdata(client, args): '''Retrieves bulk data and either writes them to standard output or to a file on disk. ''' data = client.retrieve_bulkdata(args.bulkdata_uri, args.media_type) print(data) print('\n') def _store_instances(client, args): '''Loads Instances from files on disk and stores them.''' datasets = [pydicom.dcmread(f) for f in args.files] client.store_instances(datasets) def _main(): parser = _get_parser() args = parser.parse_args() main(args) def main(args): '''Main entry point for the ``dicomweb_client`` command line program.''' configure_logging(args.logging_verbosity) if args.username: if not args.password: message = 'Enter password for user "{0}": '.format(args.username) args.password = getpass.getpass(message) session = create_session_from_user_pass(args.username, args.password) else: session = create_session() try: session = add_certs_to_session(session, args.ca_bundle, args.cert) session.headers.update(_create_headers(args)) client = DICOMwebClient( args.url, session=session, chunk_size=args.chunk_size ) args.func(client, args) sys.exit(0) except Exception as err: logger.error(str(err)) if args.logging_verbosity > 3: tb = traceback.format_exc() logger.error(tb) sys.exit(1)