diff --git a/ismrmrd/acquisition.py b/ismrmrd/acquisition.py index 585601a..8dd5c1b 100644 --- a/ismrmrd/acquisition.py +++ b/ismrmrd/acquisition.py @@ -6,6 +6,7 @@ from .constants import * from .flags import FlagsMixin from .equality import EqualityMixin +from . import decorators class EncodingCounters(EqualityMixin, ctypes.Structure): @@ -58,7 +59,6 @@ class AcquisitionHeader(FlagsMixin, EqualityMixin, ctypes.Structure): ("idx", EncodingCounters), ("user_int", ctypes.c_int32 * USER_INTS), ("user_float", ctypes.c_float * USER_FLOATS)] - def __str__(self): retstr = '' for field_name, field_type in self._fields_: @@ -70,8 +70,9 @@ def __str__(self): return retstr +@decorators.expose_header_fields(AcquisitionHeader) class Acquisition(FlagsMixin): - __readonly = ('number_of_samples', 'active_channels', 'trajectory_dimensions') + _readonly = ('number_of_samples', 'active_channels', 'trajectory_dimensions') @staticmethod def deserialize_from(read_exactly): @@ -97,7 +98,7 @@ def deserialize_from(read_exactly): return acquisition def serialize_into(self, write): - write(self.__head) + write(self._head) write(self.__traj.tobytes()) write(self.__data.tobytes()) @@ -146,7 +147,7 @@ def __init__(self, head=None, data=None, trajectory=None): def generate_header(): if head is None: if data is None: - return AcquisitionHeader() + return AcquisitionHeader() else: nchannels, nsamples = data.shape trajectory_dimensions = trajectory.shape[1] if trajectory is not None else 0 @@ -170,55 +171,26 @@ def generate_trajectory_array(header): return trajectory if trajectory is not None else np.zeros( shape=(header.number_of_samples, header.trajectory_dimensions), dtype=np.float32) - self.__head = generate_header() - - self.__data = generate_data_array(self.__head) - self.__traj = generate_trajectory_array(self.__head) - - for (field, _) in self.__head._fields_: - try: - g = '__get_' + field - s = '__set_' + field - setattr(Acquisition, g, self.__getter(field)) - setattr(Acquisition, s, self.__setter(field)) - p = property(getattr(Acquisition, g), getattr(Acquisition, s)) - setattr(Acquisition, field, p) - except TypeError: - # e.g. if key is an `int`, skip it - pass - - def __getter(self, name): - if name in self.__readonly: - def fn(self): - return copy.copy(self.__head.__getattribute__(name)) - else: - def fn(self): - return self.__head.__getattribute__(name) - return fn - - def __setter(self, name): - if name in self.__readonly: - def fn(self, val): - raise AttributeError(name + " is read-only. Use resize instead.") - else: - def fn(self, val): - self.__head.__setattr__(name, val) - - return fn + self._head = generate_header() + + self.__data = generate_data_array(self._head) + self.__traj = generate_trajectory_array(self._head) + + def resize(self, number_of_samples=0, active_channels=1, trajectory_dimensions=0): self.__data = np.resize(self.__data, (active_channels, number_of_samples)) self.__traj = np.resize(self.__traj, (number_of_samples, trajectory_dimensions)) - self.__head.number_of_samples = number_of_samples - self.__head.active_channels = active_channels - self.__head.trajectory_dimensions = trajectory_dimensions + self._head.number_of_samples = number_of_samples + self._head.active_channels = active_channels + self._head.trajectory_dimensions = trajectory_dimensions def getHead(self): - return copy.deepcopy(self.__head) + return copy.deepcopy(self._head) def setHead(self, hdr): - self.__head = self.__head.__class__.from_buffer_copy(hdr) - self.resize(self.__head.number_of_samples, self.__head.active_channels, self.__head.trajectory_dimensions) + self._head = self._head.__class__.from_buffer_copy(hdr) + self.resize(self._head.number_of_samples, self._head.active_channels, self._head.trajectory_dimensions) @property def data(self): @@ -230,7 +202,7 @@ def traj(self): def __str__(self): retstr = '' - retstr += 'Header:\n %s\n' % (self.__head) + retstr += 'Header:\n %s\n' % (self._head) retstr += 'Trajectory:\n %s\n' % (self.traj) retstr += 'Data:\n %s\n' % (self.data) return retstr @@ -240,7 +212,8 @@ def __eq__(self, other): return False return all([ - self.__head == other.__head, + self._head == other._head, np.array_equal(self.__data, other.__data), np.array_equal(self.__traj, other.__traj) ]) + diff --git a/ismrmrd/decorators.py b/ismrmrd/decorators.py new file mode 100644 index 0000000..a667163 --- /dev/null +++ b/ismrmrd/decorators.py @@ -0,0 +1,42 @@ +import copy + + +class expose_header_fields: + def __init__(self,header_cls) -> None: + self.header_cls = header_cls + + def __call__(self,cls): + def create_getter_and_setter(field): + if field in cls._readonly: + def getter(self): + return copy.copy(self._head.__getattribute__(field)) + def setter(self,val): + raise AttributeError(field+ " is read-only. Use resize instead.") + else: + def getter(self): + return self._head.__getattribute__(field) + def setter(self, val): + self._head.__setattr__(field, val) + + return getter,setter + + ignore_list = cls._ignore if hasattr(cls,"_ignore") else [] + + for (field, _) in self.header_cls._fields_: + if field in ignore_list: + continue + try: + g = '__get_' + field + s = '__set_' + field + + getter,setter = create_getter_and_setter(field) + setattr(cls, g, getter) + setattr(cls, s, setter) + p = property(getattr(cls, g), getattr(cls, s)) + setattr(cls, field, p) + except TypeError: + # e.g. if key is an `int`, skip it + pass + + return cls + diff --git a/ismrmrd/file.py b/ismrmrd/file.py index b08ed7d..ce4b276 100644 --- a/ismrmrd/file.py +++ b/ismrmrd/file.py @@ -18,7 +18,7 @@ def __len__(self): def __iter__(self): for raw in self.data: - yield self.from_numpy(raw) + yield self.from_numpy(raw) def __getitem__(self, key): if isinstance(key, slice): @@ -65,17 +65,14 @@ def __init__(self, data): @classmethod def from_numpy(cls, raw): - acquisition = Acquisition(raw['head']) - - acquisition.data[:] = raw['data'].view(np.complex64).reshape( - (acquisition.active_channels, - acquisition.number_of_samples) - )[:] - - acquisition.traj[:] = raw['traj'].reshape( - (acquisition.number_of_samples, - acquisition.trajectory_dimensions) - )[:] + acquisition = Acquisition(raw['head'],raw['data'].view(np.complex64).reshape( + (raw['head']['active_channels'], + raw['head']['number_of_samples']) + ), + raw['traj'].reshape( + (raw['head']['number_of_samples'], + raw['head']['trajectory_dimensions'] + ))) return acquisition @@ -378,7 +375,7 @@ def has_acquisitions(self): class File(Folder): def __init__(self, filename, mode='a'): - self.__file = h5py.File(filename, mode) + self.__file = h5py.File(filename, mode,driver='stdio') super().__init__(self.__file) def __enter__(self): @@ -386,3 +383,6 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): self.__file.close() + + def close(self): + self.__file.close() diff --git a/ismrmrd/image.py b/ismrmrd/image.py index e3b4bd4..cad7560 100644 --- a/ismrmrd/image.py +++ b/ismrmrd/image.py @@ -12,6 +12,7 @@ from .flags import FlagsMixin from .equality import EqualityMixin from .constants import * +from . import decorators dtype_mapping = { DATATYPE_USHORT: np.dtype('uint16'), @@ -125,9 +126,10 @@ def __repr__(self): # Image class +@decorators.expose_header_fields(ImageHeader) class Image(FlagsMixin): - __readonly = ('data_type', 'matrix_size', 'channels') - __ignore = ('matrix_size', 'attribute_string_len') + _readonly = ('data_type', 'matrix_size', 'channels') + _ignore = ('matrix_size', 'attribute_string_len') @staticmethod def deserialize_from(read_exactly): @@ -154,9 +156,9 @@ def calculate_number_of_entries(nchannels, xs, ys, zs): def serialize_into(self, write): attribute_bytes = self.attribute_string.encode('utf-8') - self.__head.attribute_string_len = len(attribute_bytes) + self._head.attribute_string_len = len(attribute_bytes) - write(self.__head) + write(self._head) write(ctypes.c_uint64(len(attribute_bytes))) write(attribute_bytes) @@ -229,15 +231,15 @@ def create_consistent_header(header, data): if head is None: if data is None: data = np.empty((1, 1, 1, 0), dtype=np.complex64) - self.__head = create_consistent_header(ImageHeader(), data) + self._head = create_consistent_header(ImageHeader(), data) else: - self.__head = ImageHeader.from_buffer_copy(head) + self._head = ImageHeader.from_buffer_copy(head) if data is None: - data = np.empty(shape=(self.__head.channels, self.__head.matrix_size[2], - self.__head.matrix_size[1], self.__head.matrix_size[0]), - dtype=get_dtype_from_data_type(self.__head.data_type)) + data = np.empty(shape=(self._head.channels, self._head.matrix_size[2], + self._head.matrix_size[1], self._head.matrix_size[0]), + dtype=get_dtype_from_data_type(self._head.data_type)) else: - self.__head = create_consistent_header(self.__head, data) + self._head = create_consistent_header(self._head, data) self.__data = data if attribute_string is not None: @@ -249,48 +251,14 @@ def create_consistent_header(header, data): else: self.__meta = Meta() - for (field, type) in self.__head._fields_: - if field in self.__ignore: - continue - else: - try: - g = '__get_' + field - s = '__set_' + field - setattr(Image, g, self.__getter(field)) - setattr(Image, s, self.__setter(field)) - p = property(getattr(Image, g), getattr(Image, s)) - setattr(Image, field, p) - except TypeError: - # e.g. if key is an `int`, skip it - pass - - def __getter(self, name): - if name in self.__readonly: - def fn(self): - return copy.copy(self.__head.__getattribute__(name)) - else: - def fn(self): - return self.__head.__getattribute__(name) - return fn - - def __setter(self, name): - if name in self.__readonly: - def fn(self, val): - raise AttributeError(name + " is read-only.") - else: - def fn(self, val): - self.__head.__setattr__(name, val) - - return fn - def getHead(self): - return copy.deepcopy(self.__head) + return copy.deepcopy(self._head) def setHead(self, hdr): - self.__head = self.__head.__class__.from_buffer_copy(hdr) - self.setDataType(self.__head.data_type) - self.resize(self.__head.channels, self.__head.matrix_size[2], self.__head.matrix_size[1], - self.__head.matrix_size[0]) + self._head = self._head.__class__.from_buffer_copy(hdr) + self.setDataType(self._head.data_type) + self.resize(self._head.channels, self._head.matrix_size[2], self._head.matrix_size[1], + self._head.matrix_size[0]) def setDataType(self, val): self.__data = self.__data.astype(get_dtype_from_data_type(val)) @@ -340,18 +308,18 @@ def attribute_string_len(self): return len(self.attribute_string) def __str__(self): - return "Header:\n {}\nAttribute string:\n {}\nData:\n {}\n".format(self.__head, self.attribute_string, + return "Header:\n {}\nAttribute string:\n {}\nData:\n {}\n".format(self._head, self.attribute_string, self.__data) def __repr__(self): - return f"Image(head={self.__head.__repr__()},meta={self.__meta.__repr__()},data={self.__data.__repr__()})" + return f"Image(head={self._head.__repr__()},meta={self.__meta.__repr__()},data={self.__data.__repr__()})" def __eq__(self, other): if not isinstance(other, Image): return False return all([ - self.__head == other.__head, + self._head == other._head, np.array_equal(self.__data, other.__data), np.array_equal(self.attribute_string, other.attribute_string) ]) diff --git a/ismrmrd/waveform.py b/ismrmrd/waveform.py index 5769940..f128ffa 100644 --- a/ismrmrd/waveform.py +++ b/ismrmrd/waveform.py @@ -5,7 +5,7 @@ from .flags import FlagsMixin from .equality import EqualityMixin - +from . import decorators class WaveformHeader(FlagsMixin, EqualityMixin, ctypes.Structure): _pack_ = 8 @@ -31,9 +31,9 @@ def __str__(self): retstr += '%s: %s\n' % (field_name, var) return retstr - +@decorators.expose_header_fields(WaveformHeader) class Waveform(FlagsMixin): - __readonly = ('number_of_samples', 'channels') + _readonly = ('number_of_samples', 'channels') @staticmethod def deserialize_from(read_exactly): @@ -50,7 +50,7 @@ def deserialize_from(read_exactly): return waveform def serialize_into(self, write): - write(self.__head) + write(self._head) write(self.__data.tobytes()) @staticmethod @@ -89,70 +89,39 @@ def from_array(data, **kwargs): def __init__(self, head=None, data=None): if head is None: - self.__head = WaveformHeader() + self._head = WaveformHeader() self.__data = np.empty(shape=(1, 0), dtype=np.uint32) else: - self.__head = WaveformHeader.from_buffer_copy(head) - self.__data = np.empty(shape=(self.__head.channels, self.__head.number_of_samples), dtype=np.uint32) + self._head = WaveformHeader.from_buffer_copy(head) + self.__data = np.empty(shape=(self._head.channels, self._head.number_of_samples), dtype=np.uint32) if data is not None: - self.data[:] = data.reshape((self.__head.channels,self.__head.number_of_samples), order="C") - - for (field, type) in self.__head._fields_: - try: - g = '__get_' + field - s = '__set_' + field - setattr(Waveform, g, self.__getter(field)) - setattr(Waveform, s, self.__setter(field)) - p = property(getattr(Waveform, g), getattr(Waveform, s)) - setattr(Waveform, field, p) - except TypeError: - # e.g. if key is an `int`, skip it - pass - - def __getter(self, name): - if name in self.__readonly: - def fn(self): - return copy.copy(self.__head.__getattribute__(name)) - else: - def fn(self): - return self.__head.__getattribute__(name) - return fn - - def __setter(self, name): - if name in self.__readonly: - def fn(self, val): - raise AttributeError(name+" is read-only. Use resize instead.") - else: - def fn(self, val): - self.__head.__setattr__(name, val) - - return fn + self.data[:] = data.reshape((self._head.channels,self._head.number_of_samples), order="C") def resize(self, number_of_samples=0, channels=1): self.__data = np.resize(self.__data, (channels, number_of_samples)) - self.__head.number_of_samples = number_of_samples - self.__head.channels = channels + self._head.number_of_samples = number_of_samples + self._head.channels = channels def getHead(self): - return copy.deepcopy(self.__head) + return copy.deepcopy(self._head) def setHead(self, hdr): - self.__head = self.__head.__class__.from_buffer_copy(hdr) - self.resize(self.__head.number_of_samples, self.__head.active_channels) + self._head = self._head.__class__.from_buffer_copy(hdr) + self.resize(self._head.number_of_samples, self._head.active_channels) @property def data(self): return self.__data.view() def __str__(self): - return "Header:\n {}\nData:\n {}\n".format(self.__head, self.__data) + return "Header:\n {}\nData:\n {}\n".format(self._head, self.__data) def __eq__(self, other): if not isinstance(other, Waveform): return False return all([ - self.__head == other.__head, + self._head == other._head, np.array_equal(self.__data, other.__data) ]) diff --git a/setup.py b/setup.py index c5ee13d..47324e4 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def to_uri(filename): setup( name='ismrmrd', - version='1.13.0', + version='1.13.1', author='ISMRMRD Developers', description='Python implementation of the ISMRMRD', license='Public Domain', diff --git a/tests/test_common.py b/tests/test_common.py index 0bd96a5..2112572 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -96,8 +96,7 @@ def create_random_trajectory(shape=(256, 2)): def create_random_waveform_data(shape=(32, 256)): - data = numpy.random.randint(0, 1 << 32, size=shape) - return data.astype(np.uint32) + return numpy.random.randint(0, 1 << 32, size=shape,dtype=np.uint32) def create_random_acquisition(seed=42): diff --git a/tests/test_waveform.py b/tests/test_waveform.py index de94284..9e25234 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -17,8 +17,7 @@ def test_initialization_from_array(): nchannels = 32 nsamples = 256 - data = numpy.random.randint(0, 1 << 32, size=(nchannels, nsamples)) - data = data.astype(np.uint32) + data = numpy.random.randint(0, 1 << 32, size=(nchannels, nsamples),dtype=np.uint32) waveform = ismrmrd.Waveform.from_array(data)