def parse_private_csa_header(dcm_data, public_attr, private_attr, default=None): """ Parses CSA header in cases where value is not defined publicly Parameters ---------- dcm_data : pydicom Dataset object DICOM metadata public_attr : string non-private DICOM attribute private_attr : string private DICOM attribute default (optional) default value if private_attr not found Returns ------- val (default: empty string) private attribute value or default """ # TODO: provide mapping to private_attr from public_attr from nibabel.nicom import csareader import dcmstack.extract as dsextract try: # TODO: test with attr besides ProtocolName for protocolStr in ['MrPhoenixProtocol', 'MrProtocol']: if protocolStr in csareader.get_csa_header(dcm_data, 'series')['tags']: break csastr = csareader.get_csa_header( dcm_data, 'series')['tags'][protocolStr]['items'][0] csastr = csastr.replace("### ASCCONV BEGIN", "### ASCCONV BEGIN ### ") parsedhdr = dsextract.parse_phoenix_prot(protocolStr, csastr) val = parsedhdr[private_attr] # if val is a string, it will remove spaces. val = val.replace(' ', '') except AttributeError: # it's a number; just return it: return val except Exception as e: lgr.debug("Failed to parse CSA header: %s", str(e)) val = default or "" return val
def get_slice_direction(dcm_file): ret_dirs = [("i","i-"),("j","j-"),("k-","k")] dcm_dat = dicom.read_file(dcm_file) csa = csareader.get_csa_header(dcm_dat) norm = csareader.get_slice_normal(csa) max_index = np.argmax(np.abs(norm)) return ret_dirs[max_index][norm[max_index] > 0]
def get_ascii_header(ds): """ Expects a pydicom dataset. Uses nibabel's "nicom" module to extract CSA data """ csa = csar.get_csa_header(ds, 'series') return csa['tags']['MrPhoenixProtocol']['items'][0]
def identify_integrated_references(img, inst_num): '''Heuristics for identifying integrated reference scans in known sequences. Sequences handled: CMRR svs_slaserVOI_dkd :param img: nibable dicom image :return: Ref scan index 0 = not refderence scan, higher integer splits into groups. :return: name suffix ''' fullcsa = csar.get_csa_header(img.dcm_data, csa_type='series') xprot = parse_buffer(fullcsa['tags']['MrPhoenixProtocol']['items'][0]) # Handle CMRR DKD sequence # https://www.cmrr.umn.edu/spectro/ # SEMI-LASER (MRM 2011, NMB 2019) Release 2016-12 if xprot[('tSequenceFileName',)].strip('"').lower() == '%customerseq%\\svs_slaservoi_dkd'\ and xprot[('sSpecPara', 'lAutoRefScanMode')] == 8.0: num_ref = int(xprot[('sSpecPara', 'lAutoRefScanNo')]) num_dyn = int(xprot[('lAverages', )]) total_dyn = num_dyn + (num_ref * 4) if inst_num <= num_ref: # First ecc calibration references return 1, '_ecc' elif inst_num > num_ref\ and inst_num <= (num_ref * 2): # First quantitation calibration references return 2, '_quant' elif inst_num <= (total_dyn - num_ref)\ and inst_num > (total_dyn - (2 * num_ref)): # Second ecc calibration references return 1, '_ecc' elif inst_num <= total_dyn\ and inst_num > (total_dyn - num_ref): # Second quantitation calibration references return 2, '_quant' else: return 0, '' else: return 0, ''
action="store_true", default=False) inArgs = parser.parse_args() if inArgs.debug: print "inArgs.display = " + str(inArgs.display) print "inArgs.debug = " + str(inArgs.debug) print "inArgs.verbose = " + str(inArgs.verbose) ds = dicom.read_file(inArgs.in_dcm) print ds print dir(ds) print ds.PatientsName csa = csar.get_csa_header(ds, 'b_matrix') print csa # ascii_header = csa['tags']['MrPhoenixProtocol']['items'][0] """ Patient's Name PN: 'CENC_WL_WFU_111315' Patient ID LO: 'CENC_WL_WFU_111315' Issuer of Patient ID LO: '' Patient's Birth Date DA: '19720101' Patient's Sex CS: 'F' Patient's Age AS: '043Y' Patient's Size DS: '1.5748031516667' Patient's Weight DS: '58.967015601' Body Part Examined CS: 'HEAD' Scanning Sequence CS: ['GR', 'IR'] Sequence Variant CS: ['SK', 'SP', 'MP'] Scan Options CS: 'IR'
def get_csa_header(dcm_file): import pydicom from nibabel.nicom import csareader as csar dcm_data = pydicom.read_file(dcm_file) return csar.get_csa_header(dcm_data, 'series')
def parse_private_data(dicom_dataset): private_data = { 'is_mosaic': False, 'data': None } try: manufacturer = dicom_dataset[(0x0008, 0x0070)].value if "siemens" in manufacturer.lower(): try: siemens_header = get_csa_header(dicom_dataset) except: siemens_header = None if siemens_header: private_data['is_mosaic'] = is_mosaic(siemens_header) # Remove an unused key that is composed of random bytes and seems to be used for padding # (can't be converted to json easily) siemens_header.pop('unused0', None) # # For the remember of the entries, check that the values in the 'items' key of the 'tags' # # field in the header are of the proper type according to the specified VR for that tag. # # Additionally, ignore tags that are of VR SQ, PN, or any of the binary types. if siemens_header.get('tags', None): for tag, val in siemens_header['tags'].items(): vr = val['vr'] if vr in ('OB', 'OD', 'OF', 'OL', 'OW', 'UN', 'SQ', 'PN'): continue sanitized_items = [] for item in val['items']: sanitized_items.append(_vr_encoding(vr)(item)) siemens_header['tags'][tag]['items'] = sanitized_items # Make sure the remaining csa headers are json serializable try: _ = json.dumps(siemens_header) private_data['data'] = siemens_header except TypeError: pass elif ("ge" in manufacturer.lower()) or ("general electric" in manufacturer.lower()): ge_dat = dicom_dataset[(0x0025, 0x101B)].value ge_priv_data = decode_ge_private_data(ge_dat) if ge_priv_data: private_data['data'] = ge_priv_data except KeyError: pass except (CSAError, CSAReadError): pass return private_data
def __init__(self, path): """Initialize a Siemens object from a DICOM file or directory. If a directory is given, read the first DICOM file in the directory. Args: path (str): The path to a Siemens SVS DICOM file or directory of DICOM files. """ self.path = os.path.realpath(path) dcmfile = None if os.path.isdir(self.path): # find the first dicom file to read meta data from files = os.listdir(self.path) # exclude any non dicoms files = [ f for f in files if (os.path.isfile(os.path.join(self.path, f)) and f.endswith(( '.DCM', '.dcm', '.ima', '.IMA')) and not f.startswith('.')) ] if len(files) == 0: raise FileNotFoundError('No DICOM files found') self.files = sorted(files) dcmfile = os.path.join(self.path, files[0]) # a single file, change the path if len(files) == 1: path = os.path.join(self.path, files[0]) self.path = path if not (dcmfile): raise FileNotFoundError('No DICOM files found') else: dcmfile = path dcm = dcmwrapper.wrapper_from_file(dcmfile) self.csa = csareader.get_csa_header(dcm.dcm_data) scalar_fields = [ 'MagneticFieldStrength', 'ImagingFrequency', 'MixingTime', 'EchoTime', 'RepetitionTime', 'ImaCoilString', 'SequenceName', 'VoiReadoutFoV', 'VoiPhaseFoV', 'VoiThickness', 'VoiInPlaneRotation', 'DataPointColumns', 'RealDwellTime', 'PixelBandwidth', 'ImagedNucleus' ] for k in scalar_fields: self.meta[k] = csareader.get_scalar(self.csa, k) self.meta['ImagePositionPatient'] = \ csareader.get_vector(self.csa, 'ImagePositionPatient', 3) self.meta['VoiPosition'] = csareader.get_vector( self.csa, 'VoiPosition', 3) self.meta['VoiOrientation'] = csareader.get_vector( self.csa, 'VoiOrientation', 3) self.meta['ImageOrientationPatient'] = \ csareader.get_vector(self.csa, 'ImageOrientationPatient', 6)
def read_data(self, conj=True): """Read the associated fids. If the instance was initialized with a directory, the directory is assumed to contain a single series in order. Dimensions are channel x rep x mega x isis x t """ print('Reading data...') if os.path.isfile(self.path): dcm = dcmwrapper.wrapper_from_file(self.path) data = np.array(_read_fid(dcm), ndmin=5) self.dcm_data = dcm.dcm_data elif os.path.isdir(self.path): # read a directory of DICOMS, each containing one fid # the directory must contain more than one file, as this is checked # in the class constructor channels = [] # find the instance number of the last dicom to calculate data size # this assumes different interleaved acquisitions have the same # (0020, 0012) Acquisition Number # true for eja sequences lastdcmfile = os.path.join(self.path, self.files[-1]) lastdcm = dcmwrapper.wrapper_from_file(lastdcmfile) self.dcm_data = lastdcm.dcm_data lastinstance = int(lastdcm.get((0x0020, 0x0013)).value) # figure out which channels are on csa_series = csareader.get_csa_header(lastdcm.dcm_data, 'series') csa_image = csareader.get_csa_header(lastdcm.dcm_data, 'image') siemens_hdr = csa_series['tags']['MrPhoenixProtocol']['items'][0] m = re.findall( r"""sCoilSelectMeas.aRxCoilSelectData\[0\].asList\[(?P<coilnum>\d+)\].sCoilElementID.tElement\t = \t""(?P<coilname>[HENC]+\d+)""" "", siemens_hdr) channels = dict(m) channels = dict(zip(channels.values(), channels.keys())) n_channels = len(channels) self.channels = channels # is the data combined over channels? # mri_probedicom reports ucUncombImages, but where is this in the CSA? # the first two instances of uncombined eja sequences are single channels # TODO: figure out what they are # Assume the first match the last two, which are missing the same channel n_reps = lastinstance - 2 is_combined = False # TODO: handle channel combined data if len(self.files) != (n_reps * (n_channels + 1)): # not enough files for uncombined data if len(self.files) == lastinstance: warnings.warn('Assuming channels are combined') n_reps = lastinstance n_channels = 1 is_combined = True else: raise Exception( 'Expected n_reps[%d] * (n_channels[%d] + 1 files' % (n_reps, n_channels)) data = np.zeros( (n_channels, n_reps, 1, 1, int(csareader.get_scalar(csa_image, 'DataPointColumns'))), dtype=complex) for fi in range(len(self.files)): dcmfile = os.path.join(self.path, self.files[fi]) dcm = dcmwrapper.wrapper_from_file(dcmfile) csa = csareader.get_csa_header(dcm.dcm_data) fid = np.array(_read_fid(dcm), ndmin=5) channel = csareader.get_scalar(csa, 'ImaCoilString') inst = int(dcm.get((0x0020, 0x0013)).value) if not is_combined: if inst <= 2: ri = n_reps - inst else: ri = inst - 2 - 1 else: ri = inst - 1 # there are combined coils (HC1-7) in the dicoms? # make sure this channel is one that is turned on if not is_combined: if channel in channels.keys(): ci = int(channels[channel]) data[ci, ri, 0, 0, :] = fid else: data[0, ri, 0, 0, :] = fid print('Read %d acquisitions from %d channels' % (n_reps, n_channels)) print(channels.keys()) # take the complex conjugate, which Tarquin seems to expect if conj: data = np.conj(data) # permute data for SPECIAL and MEGA if self.get_parameter('SequenceName') in [ 'eja_svs_mpress', 'eja_svs_mslaser' ]: data_on = data[:, 0:int(n_reps / 2.), 0, ::] data_off = data[:, int(n_reps / 2.):n_reps, 0, ::] data = np.stack((data_off, data_on), 2) #data = np.reshape(data, (n_channels, int(n_reps/2.), 2, 1, self.get_parameter('DataPointColumns'))) self.data = data return (data)
def get_csa_header(dcm_file): import pydicom from nibabel.nicom import csareader as csar dcm_data = pydicom.read_file(dcm_file) return csar.get_csa_header(dcm_data, 'series')
def dicom_readout_msec(ds, mode='CVN'): ''' dicom_readout_msec(ds, mode='CVN'): output the <epireadouttime> in milliseconds. This function is based on Keith's matlab utility function dicom_readout_msec.m. The key variable is 'BandwidthPerPixelPhaseEncode' This variable is in csaheader. We use nibabel.nicom.csareader.get_csa_header() function to read this field. Input: <ds>: can be 1. ds structure created by pydicom.dcmread() 2. a file path or wildcard for a dicom file, e.g, 'MR008-0032.dcm'. When wildcard case, make sure only one matched file. <mode>: FSL and SPM expect different calculations of <epireadouttime> can be 'FSL': 'SPM','CVN' (default): Output: <epireadouttime> in millisecs, is an important variable in KK's func preprocessing pipeline to use fieldmap to correct <bpppe>: BandwidthPerPixelPhaseEncode in millisecs Note 1. partial fourier does not effect distortion 2. iPAT is "included" in echo spacing when computed directly from the dicom header bpppe, so don't separately account for that 3. Chris said the regular expression approach might fail in some dataset. consider to use other one To do: * more rigourous testing?? ''' from pydicom import dcmread from pydicom.dataset import FileDataset from nibabel.nicom.csareader import get_csa_header from RZutilpy.rzio import matchfiles import re if isinstance(ds, FileDataset): pass elif isinstance(ds, str): try: ds = matchfiles(ds) ds = dcmread(ds) except: print('dicom read failed, double check this file') csa = get_csa_header(ds) bpppe = csa['tags']['BandwidthPerPixelPhaseEncode']['items'][0] # figure out inplane matrix, not that we assume # use regular expression might be wrong for some data structure.. be careful p = re.compile(r'^(\d{1,4})p\*(\d{1,4})s$') matchgroup = p.match(ds[int('0051', 16), int('100b', 16)].value) npe = int(matchgroup.group(1)) # step in phase encoding direction es = 1 / (bpppe * npe) if mode.upper() in ['SPM', 'CVN']: epireadouttime = 1000 * npe * es elif mode.upper() in ['FSL']: epireadouttime = 1000 * (npe - 1) * es return epireadouttime, bpppe