def test_dependencies(fixed_fid_sum): data = MRSData(fixed_fid_sum, 5e-4, 123) model = { "phase0": 0.0, "phase1": 0.0, "pcr": { "amplitude": 1.0, "fwhm": { "value": 45.0, "min": 42.0, "max": 55.0, }, "phase": "0", "frequency": 0 }, "pcr2": { "amplitude": 1.0, "fwhm": { "value": 45.0, "min": 42.0, "max": 55.0, }, "phase": "0", "frequency": "pcr_frequency+200" } } fitting_result = singlet.fit(data, model) numpy.testing.assert_allclose(fitting_result["model"]["pcr"]["fwhm"], 50.0, rtol=1e-2) numpy.testing.assert_allclose(fitting_result["model"]["pcr"]["amplitude"], 1.0, rtol=2e-2) numpy.testing.assert_allclose(fitting_result["model"]["pcr"]["frequency"], 0.0, atol=1e-1) numpy.testing.assert_allclose(fitting_result["fit"], fixed_fid_sum, atol=0.001)
def load_sdat(sdat_filename, spar_filename=None, spar_encoding=None): # if the spar filename is not supplied, assume it is in the same folder as # the sdat and only differs in the extension if spar_filename is None: path, ext = os.path.splitext(sdat_filename) # match the capitalisation of the sdat extension if ext == ".SDAT": spar_filename = path + ".SPAR" elif ext == ".sdat": spar_filename = path + ".spar" with open(spar_filename, 'r', encoding=spar_encoding) as fin: parameter_dict = {} for line in fin: # ignore empty lines and comments starting with ! if line != "\n" and not line.startswith("!"): key, value = map(str.strip, line.split(":", 1)) if key in spar_types["floats"]: parameter_dict[key] = float(value) elif key in spar_types["integers"]: parameter_dict[key] = int(value) elif key in spar_types["strings"]: parameter_dict[key] = value else: pass #print("{} : {}".format(key, value)) dt = 1 / parameter_dict["sample_frequency"] with open(sdat_filename, 'rb') as fin: raw_bytes = fin.read() floats = _vax_to_ieee_single_float(raw_bytes) data_iter = iter(floats) complex_iter = (complex(r, -i) for r, i in zip(data_iter, data_iter)) raw_data = numpy.fromiter(complex_iter, "complex64") raw_data = numpy.reshape( raw_data, (parameter_dict["rows"], parameter_dict["samples"])).squeeze() return MRSData(raw_data, dt, parameter_dict["synthesizer_frequency"] * 1e-6, te=parameter_dict["echo_time"])
def test_missing_param(fixed_fid): data = MRSData(fixed_fid, 5e-4, 123) # No width value passed in, to test whether KeyError is raised model = { "phase0": 0, "phase1": 0, "pcr": { "amplitude": 1, "fwhm": { # "value": 45, "min": 42, "max": 55, }, "phase": "0", "frequency": 0 } } with pytest.raises(KeyError): fitting_result = singlet.fit(data, model)
def test_missing_global_phase(fixed_fid): data = MRSData(fixed_fid, 5e-4, 123) # None value supplied for phase0 and phase1, to test whether TypeError is raised model = { "phase0": None, "phase1": None, "pcr": { "amplitude": 1.0, "fwhm": { "value": 45.0, "min": 42.0, "max": 55.0, }, "phase": "0", "frequency": 0.0 } } with pytest.raises(TypeError): fitting_result = singlet.fit(data, model)
def test_bad_param(fixed_fid): data = MRSData(fixed_fid, 5e-4, 123) # invalid key added to width dict, to test whether KeyError is raised model = { "phase0": 0.0, "phase1": 0.0, "pcr": { "amplitude": 1.0, "fwhm": { "value": 45.0, "min": 42.0, "max": 55.0, "avg": 47 # this is the bad key }, "phase": "0", "frequency": 0.0 } } with pytest.raises(KeyError): fitting_result = singlet.fit(data, model)
def prepare_pfile_advanced(pfile): """ Transform a P-file containing advanced MRS data into an MRSData object. Despite the name, this is the simplest P-file preparation option as it does nothing to the data apart from unpacking it into an ndarray and adding the necessary Suspect metadata. Parameters ---------- pfile P-file loaded with GE's Orchestra for Python library. Returns ------- MRSData The raw FID data loaded into a Suspect MRSData object. """ header = pfile.Header() metadata = pfile.MetaData() num_echoes = int(header["rdb_hdr_rec"]["rdb_hdr_nechoes"]) # when we load the data from the pfile, we do it echo by echo # the order of the other axes is initially what is returned from pfile # we will reshape and reorder it afterwards raw_data = numpy.zeros((num_echoes, metadata["acquiredXRes"], metadata["acquiredYRes"], metadata["channels"]), dtype=numpy.complex) for i in range(num_echoes): raw_data[i] = pfile.KSpace(0, i) # reorganise the data to put channels and ADC after the averages/phase encodes # then squeeze to remove any size 1 axes (e.g. echoes or channels) raw_data = raw_data.transpose((0, 2, 3, 1)).squeeze() return MRSData(raw_data, **extract_header_parameters(header))
def load_dicom(filename): """ Load a file in the DICOM Magnetic Resonance Spectroscopy format (SOP 1.2.840.10008.5.1.4.1.1.4.2) Parameters ---------- filename : str The name of the file to load Returns ------- MRSData The loaded data from the file """ dataset = pydicom.dicomio.read_file(filename) sw = dataset[0x0018, 0x9052].value dt = 1.0 / sw f0 = dataset[0x0018, 0x9098].value ppm0 = dataset[0x0018, 0x9053].value rows = dataset[0x0028, 0x0010].value cols = dataset[0x0028, 0x0011].value frames = dataset[0x0028, 0x0008].value num_second_spectral = dataset[0x0028, 0x9001].value num_points = dataset[0x0028, 0x9002].value data_shape = [frames, rows, cols, num_second_spectral, num_points] # turn the data into a numpy array data_iter = iter(dataset[0x5600, 0x0020]) data = complex_array_from_iter(data_iter, shape=data_shape, chirality=-1) return MRSData(data, dt, f0, ppm0=ppm0)
def load_siemens_dicom(filename): """Imports a file in the Siemens .IMA format. Parameters ---------- filename : str The name of the file to import """ # the .IMA format is a DICOM standard, unfortunately most of the information is contained inside a private and very # complicated header with its own data storage format, we have to get that information out along with the data # start by reading in the DICOM file completely dataset = pydicom.dicomio.read_file(filename) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 header_index = 0 while (0x0029, xx) in dataset: if dataset[0x0029, xx].value == "SIEMENS CSA HEADER": header_index = xx xx += 1 # check that we have found the header if header_index == 0: raise KeyError("Could not find header index") # now we know which tag contains the CSA image header info: (0029, xx10) csa_header_bytes = dataset[0x0029, 0x0100 * header_index + 0x0010].value csa_header = read_csa_header(csa_header_bytes) # for key, value in csa_header.items(): # print("%s : %s" % (str(key), str(value))) # we can also get the series header info: (0029, xx20), but this seems to be mostly pretty boring # now we can work out the shape of the data (slices, rows, columns, fid_points) data_shape = ( csa_header["SpectroscopyAcquisitionOut-of-planePhaseSteps"], csa_header["Rows"], csa_header["Columns"], csa_header["DataPointColumns"], ) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 data_index = 0 while (0x7fe1, xx) in dataset: if dataset[0x7fe1, xx].value == "SIEMENS CSA NON-IMAGE": data_index = xx xx += 1 # check that we have found the data if data_index == 0: raise KeyError("Could not find data index") # extract the actual data bytes csa_data_bytes = dataset[0x7fe1, 0x0100 * data_index + 0x0010].value # the data is stored as a list of 4 byte floats in (real, imaginary) pairs data_floats = struct.unpack("<%df" % (len(csa_data_bytes) / 4), csa_data_bytes) complex_data = complex_array_from_iter(iter(data_floats), length=len(data_floats) // 2, shape=data_shape) in_plane_rot = csa_header["VoiInPlaneRotation"] x_vector = numpy.array([-1, 0, 0]) normal_vector = numpy.array(csa_header["VoiOrientation"]) orthogonal_x = x_vector - numpy.dot(x_vector, normal_vector) * normal_vector orthonormal_x = orthogonal_x / numpy.linalg.norm(orthogonal_x) rot_matrix = rotation_matrix(in_plane_rot, normal_vector) row_vector = numpy.dot(rot_matrix, orthonormal_x) column_vector = numpy.cross(row_vector, normal_vector) voxel_size = (*csa_header["PixelSpacing"], csa_header["SliceThickness"]) transform = transformation_matrix(row_vector, column_vector, csa_header["VoiPosition"], voxel_size) voi_size = [ csa_header["VoiReadoutFoV"], csa_header["VoiPhaseFoV"], csa_header["VoiThickness"] ] metadata = {"voi_size": voi_size} return MRSData(complex_data, csa_header["RealDwellTime"] * 1e-9, csa_header["ImagingFrequency"], te=csa_header["EchoTime"], tr=csa_header["RepetitionTime"], transform=transform, metadata=metadata)
def load_sdat(sdat_filename, spar_filename=None, spar_encoding=None): # if the spar filename is not supplied, assume it is in the same folder as # the sdat and only differs in the extension if spar_filename is None: path, ext = os.path.splitext(sdat_filename) # match the capitalisation of the sdat extension if ext == ".SDAT": spar_filename = path + ".SPAR" elif ext == ".sdat": spar_filename = path + ".spar" with open(spar_filename, 'r', encoding=spar_encoding) as fin: parameter_dict = {} for line in fin: # ignore empty lines and comments starting with ! if line != "\n" and not line.startswith("!"): key, value = map(str.strip, line.split(":", 1)) if key in spar_types["floats"]: parameter_dict[key] = float(value) elif key in spar_types["integers"]: parameter_dict[key] = int(value) elif key in spar_types["strings"]: parameter_dict[key] = value else: pass #print("{} : {}".format(key, value)) dt = 1 / parameter_dict["sample_frequency"] with open(sdat_filename, 'rb') as fin: raw_bytes = fin.read() floats = _vax_to_ieee_single_float(raw_bytes) data_iter = iter(floats) complex_iter = (complex(r, -i) for r, i in zip(data_iter, data_iter)) raw_data = numpy.fromiter(complex_iter, "complex64") if parameter_dict["rows"] > 1: raw_data = numpy.reshape( raw_data, (parameter_dict["rows"] // parameter_dict["nr_of_phase_encoding_profiles_ky"], parameter_dict["nr_of_phase_encoding_profiles_ky"], parameter_dict["nr_of_slices_for_multislice"], parameter_dict["samples"])).squeeze() else: raw_data = numpy.reshape( raw_data, (parameter_dict["rows"], parameter_dict["samples"])).squeeze() # calculate transformation matrix voxel_size = numpy.array([ parameter_dict["lr_size"], parameter_dict["ap_size"], parameter_dict["cc_size"] ]) position_vector = numpy.array([ parameter_dict["lr_off_center"], parameter_dict["ap_off_center"], parameter_dict["cc_off_center"] ]) A = numpy.eye(3) for a, ang in enumerate( ["lr_angulation", "ap_angulation", "cc_angulation"]): axis = numpy.zeros(3) axis[a] = 1 A = A @ rotation_matrix(parameter_dict[ang] / 180 * numpy.pi, axis) e1 = A[:, 0] e1 = e1 / numpy.linalg.norm(e1) e2 = A[:, 1] e2 = e2 / numpy.linalg.norm(e2) transform = transformation_matrix(e1, e2, position_vector, voxel_size) return MRSData(raw_data, dt, parameter_dict["synthesizer_frequency"] * 1e-6, te=parameter_dict["echo_time"], tr=parameter_dict["repetition_time"], transform=transform)
def load_rda(filename): header_dict = {} with open(filename, 'rb') as fin: header_line = fin.readline().strip() if header_line != b">>> Begin of header <<<": raise Exception("Error reading file {} as a .rda".format(filename)) header_line = fin.readline().strip().decode('windows-1252') while header_line != ">>> End of header <<<": key, value = map(str.strip, header_line.split(":", 1)) if key in rda_types["strings"]: header_dict[key] = value elif key in rda_types["integers"]: header_dict[key] = int(value) elif key in rda_types["floats"]: header_dict[key] = float(value) elif "[" in key and "]" in key: # could be a dict or a list key, index = re.split(r"\]|\[", key)[0:2] if key in rda_types["dictionaries"]: if key not in header_dict: header_dict[key] = {} header_dict[key][index] = value else: # not a dictionary, must be a list if key in rda_types["float_arrays"]: value = float(value) elif key in rda_types["integer_arrays"]: value = int(value) index = int(index) # make sure there is a list in the header_dict, with enough entries if not key in header_dict: header_dict[key] = [] while len(header_dict[key]) <= index: header_dict[key].append(0) header_dict[key][index] = value header_line = fin.readline().strip().decode('windows-1252') # now we can read the data data = fin.read() # the shape of the data in slice, column, row, time format data_shape = header_dict["CSIMatrixSize"][::-1] data_shape.append(header_dict["VectorSize"]) data_shape = numpy.array(data_shape) data_size = numpy.prod( data_shape) * 16 # each data point is a complex double, 16 bytes if data_size != len(data): raise ValueError( "Error reading file {}: expected {} bytes of data, got {}".format( filename, data_size, len(data))) # unpack the data into complex numbers data_as_floats = struct.unpack("<{}d".format(numpy.prod(data_shape) * 2), data) float_iter = iter(data_as_floats) complex_iter = (complex(r, i) for r, i in zip(float_iter, float_iter)) complex_data = numpy.fromiter(complex_iter, "complex64", int(numpy.prod(data_shape))) complex_data = numpy.reshape(complex_data, data_shape).squeeze() # some .rda files have a misnamed field, correct this here if "VOIReadoutFOV" not in header_dict: if "VOIReadoutVOV" in header_dict: header_dict["VOIReadoutFOV"] = header_dict.pop("VOIReadoutVOV") # combine positional elements in the header voi_size = (header_dict["VOIReadoutFOV"], header_dict["VOIPhaseFOV"], header_dict["VOIThickness"]) voi_center = (header_dict["VOIPositionSag"], header_dict["VOIPositionCor"], header_dict["VOIPositionTra"]) voxel_size = (header_dict["PixelSpacingCol"], header_dict["PixelSpacingRow"], header_dict["PixelSpacing3D"]) x_vector = numpy.array(header_dict["RowVector"]) y_vector = numpy.array(header_dict["ColumnVector"]) to_scanner = transformation_matrix(x_vector, y_vector, numpy.array(voi_center), voxel_size) # put useful components from the header in the metadata metadata = { "voi_size": voi_size, "position": voi_center, "voxel_size": voxel_size, "protocol": header_dict["ProtocolName"], "to_scanner": to_scanner, "from_scanner": numpy.linalg.inv(to_scanner) } return MRSData(complex_data, header_dict["DwellTime"] * 1e-6, header_dict["MRFrequency"], te=header_dict["TE"], tr=header_dict["TR"], transform=to_scanner, metadata=metadata)
def prepare_pfile_svs(pfile): """ Transform a P-file containing SVS MRS data into an MRSData object. Single voxel spectroscopy on GE is the most complicated case to process. Parameters ---------- pfile P-file loaded with GE's Orchestra for Python library. Returns ------- The raw FID data loaded into a Suspect MRSData object. """ header = pfile.Header() metadata = pfile.MetaData() num_echoes = int(header["rdb_hdr_rec"]["rdb_hdr_nechoes"]) num_frames = int(header["rdb_hdr_rec"]["rdb_hdr_nframes"]) num_averages = int(header["rdb_hdr_rec"]["rdb_hdr_navs"]) # read the number of (water-suppressed) spectra acquired data_frames = int(header["rdb_hdr_rec"]["rdb_hdr_user4"]) # in some cases GE automatically combines blocks of num_averages on the # scanner depending on the value of the no_add parameter # I think that this is stored in the lsb of the below header parameter no_add = header["rdb_hdr_image"]["user24"] % 2 == 1 if no_add is not True: data_frames //= num_averages # the header parameter with the number of reference frames is not reliable: # sometimes it ignores the value of no_add so we just assume that all non # data frames are ref frames ref_frames = num_frames - data_frames # when we load the data from the pfile, we do it echo by echo # the order of the other axes is initially what is returned from pfile # we will reshape and reorder it afterwards raw_data = numpy.zeros((num_echoes, metadata["acquiredXRes"], metadata["acquiredYRes"], metadata["channels"]), dtype=numpy.complex) for i in range(num_echoes): raw_data[i] = pfile.KSpace(0, i) # if no_add was set, every other average will have opposite sign and has to # be flipped if no_add: # TODO we assume that the flipping happens within a set of num_averages # TODO and not outside, should check this is the case flip_array = numpy.array(-1)**numpy.arange(num_averages) flip_array = numpy.tile(flip_array, int(num_frames / num_averages)) flip_array = flip_array[numpy.newaxis, numpy.newaxis, :, numpy.newaxis] raw_data *= flip_array # reorganise the data to be echoes, averages, channels and ADC raw_data = raw_data.transpose((0, 2, 3, 1)) # we have the acquired data, now put together the desired metadata header_params = extract_header_parameters(header) mrs_data = MRSData(raw_data, **header_params) #print(ref_frames) #print(metadata) #print(mrs_data.shape) wref = mrs_data[:, :ref_frames].reshape( -1, metadata["channels"], metadata["acquiredXRes"]).squeeze() data = mrs_data[:, ref_frames:].squeeze() return data, wref
def load_svs_bruker(fid_filename, acqp_filename=None, method_filename=None): """ Load SVS data in the Bruker format Parameters ---------- fid_filename: str The location of the file containing the fid data to load acqp_filename: str, optional The location of the acquisition parameters file. If not provided a file named acqp in the same directory as fid_filename will be used or an error raised if no such file exists. method_filename: str, optional The location of the method file. If not provided a file named method in the same directory as fid_filename will be used, or an error raised if no such file exists. Returns ------- data: MRSData Data read from the file """ if acqp_filename is None: path, fid_file = os.path.split(fid_filename) acqp_filename = os.path.join(path, "acqp") if not os.path.isfile(acqp_filename): raise FileNotFoundError( "No acqp file found at {0}".format(acqp_filename)) if method_filename is None: path, fid_file = os.path.split(fid_filename) method_filename = os.path.join(path, "method") if not os.path.isfile(method_filename): raise FileNotFoundError("No method file found") # read the data out of the method file with open(method_filename) as fin: method_string = fin.read() dwell_time_string = re.search(r"\$PVM_DigDw=\d+\.?\d*", method_string).group() # dwell time is in ms, convert to s dt = float(dwell_time_string.split("=")[1]) * 1e-3 digitiser_delay_string = re.search(r"\$PVM_DigShift=\d+", method_string).group() digitiser_delay = int(digitiser_delay_string.split("=")[1]) # read the data out of the acqp file with open(acqp_filename) as fin: acqp_string = fin.read() f0_string = re.search(r"\$BF1=\d+\.\d*", acqp_string).group() f0 = float(f0_string.split("=")[1]) # read the fid data with open(fid_filename, 'rb') as fin: fid_bytes = fin.read() # bruker data is stored as real/imaginary int 32 pairs num_ints = len(fid_bytes) // 4 fid_ints = struct.unpack("{0}i".format(num_ints), fid_bytes) data = complex_array_from_iter(iter(fid_ints), num_ints // 2, chirality=-1) return MRSData(data[digitiser_delay:], dt, f0)
def load_siemens_dicom(filename): """Imports a file in the Siemens .IMA format. Parameters ---------- filename : str The name of the file to import """ # the .IMA format is a DICOM standard, unfortunately most of the information is contained inside a private and very # complicated header with its own data storage format, we have to get that information out along with the data # start by reading in the DICOM file completely dataset = pydicom.dicomio.read_file(filename) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 header_index = 0 while (0x0029, xx) in dataset: if dataset[0x0029, xx].value == "SIEMENS CSA HEADER": header_index = xx xx += 1 # check that we have found the header if header_index == 0: raise KeyError("Could not find header index") # now we know which tag contains the CSA image header info: (0029, xx10) csa_header_bytes = dataset[0x0029, 0x0100 * header_index + 0x0010].value csa_header = read_csa_header(csa_header_bytes) # for key, value in csa_header.items(): # print("%s : %s" % (str(key), str(value))) # we can also get the series header info: (0029, xx20), but this seems to be mostly pretty boring # now we can work out the shape of the data (slices, rows, columns, fid_points) data_shape = ( csa_header["SpectroscopyAcquisitionOut-of-planePhaseSteps"], csa_header["Rows"], csa_header["Columns"], csa_header["DataPointColumns"], ) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 data_index = 0 while (0x7fe1, xx) in dataset: if dataset[0x7fe1, xx].value == "SIEMENS CSA NON-IMAGE": data_index = xx xx += 1 # check that we have found the data if data_index == 0: raise KeyError("Could not find data index") # extract the actual data bytes csa_data_bytes = dataset[0x7fe1, 0x0100 * data_index + 0x0010].value # the data is stored as a list of 4 byte floats in (real, imaginary) pairs data_floats = struct.unpack("<%df" % (len(csa_data_bytes) / 4), csa_data_bytes) # a bug report (#143) has been submitted that for at least one .IMA dataset # created with an old Siemens VB17 WIP, the data_shape worked out above # does not match the actual size of the data because the # Out-of-planePhaseSteps value is not the number of slices. Assuming this # is a rare situation that is unlikely to happen often, the simple solution # is simply to check the size matches here, and if not then use the size # of data available as the shape available_points = len(data_floats) // 2 if numpy.prod(data_shape) != available_points: data_shape = (available_points, ) warnings.warn("The calculated data shape for this file {} does not " "match the size of data contained in the file {}. " "Therefore the returned data shape from this function " "will simply be ({},), any reshaping must be done by " "the user. If you need help with this or believe this " "has occured in error, please raise an issue at" "https://github.com/openmrslab/suspect/issues.") complex_data = complex_array_from_iter(iter(data_floats), length=len(data_floats) // 2, shape=data_shape) in_plane_rot = csa_header["VoiInPlaneRotation"] x_vector = numpy.array([-1, 0, 0]) normal_vector = numpy.array(csa_header["VoiOrientation"]) orthogonal_x = x_vector - numpy.dot(x_vector, normal_vector) * normal_vector orthonormal_x = orthogonal_x / numpy.linalg.norm(orthogonal_x) rot_matrix = rotation_matrix(in_plane_rot, normal_vector) row_vector = numpy.dot(rot_matrix, orthonormal_x) column_vector = numpy.cross(row_vector, normal_vector) voxel_size = (*csa_header["PixelSpacing"], csa_header["SliceThickness"]) transform = transformation_matrix(row_vector, column_vector, csa_header["VoiPosition"], voxel_size) voi_size = [ csa_header["VoiReadoutFoV"], csa_header["VoiPhaseFoV"], csa_header["VoiThickness"] ] metadata = {"voi_size": voi_size} return MRSData(complex_data, csa_header["RealDwellTime"] * 1e-9, csa_header["ImagingFrequency"], te=csa_header["EchoTime"], tr=csa_header["RepetitionTime"], transform=transform, metadata=metadata)
def load_siemens_dicom(filename): """ Imports a file in the Siemens .IMA format. :param filename: The filename of the file to import """ # the .IMA format is a DICOM standard, unfortunately most of the information is contained inside a private and very # complicated header with its own data storage format, we have to get that information out along with the data # start by reading in the DICOM file completely dataset = pydicom.dicomio.read_file(filename) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 header_index = 0 while (0x0029, xx) in dataset: if dataset[0x0029, xx].value == "SIEMENS CSA HEADER": header_index = xx xx += 1 # check that we have found the header if header_index == 0: raise KeyError("Could not find header index") # now we know which tag contains the CSA image header info: (0029, xx10) csa_header_bytes = dataset[0x0029, 0x0100 * header_index + 0x0010].value csa_header = read_csa_header(csa_header_bytes) #for key, value in csa_header.items(): # print("%s : %s" % (str(key), str(value))) # we can also get the series header info: (0029, xx20), but this seems to be mostly pretty boring # now we can work out the shape of the data (slices, rows, columns, fid_points) data_shape = ( csa_header["SpectroscopyAcquisitionOut-of-planePhaseSteps"], csa_header["Rows"], csa_header["Columns"], csa_header["DataPointColumns"], ) # now look through the tags (0029, 00xx) to work out which xx refers to the csa header # xx seems to start at 10 for Siemens xx = 0x0010 data_index = 0 while (0x7fe1, xx) in dataset: if dataset[0x7fe1, xx].value == "SIEMENS CSA NON-IMAGE": data_index = xx xx += 1 # check that we have found the data if data_index == 0: raise KeyError("Could not find data index") # extract the actual data bytes csa_data_bytes = dataset[0x7fe1, 0x0100 * data_index + 0x0010].value # the data is stored as a list of 4 byte floats in (real, imaginary) pairs data_floats = struct.unpack("<%df" % (len(csa_data_bytes) / 4), csa_data_bytes) # form an iterator which will provide the numbers one at a time, then an iterator which calls that iterator twice # each cycle to give a complex pair data_iter = iter(data_floats) complex_iter = (complex(r, i) for r, i in zip(data_iter, data_iter)) # give this iterator to numpy to build the data array complex_data = numpy.fromiter(complex_iter, "complex128", int(len(csa_data_bytes) / 8)) # reshape the array to structure of rows, columns and slices complex_data = numpy.reshape(complex_data, data_shape).squeeze() return MRSData(complex_data, csa_header["RealDwellTime"] * 1e-9, csa_header["ImagingFrequency"], te=csa_header["EchoTime"])