diff --git a/CHANGELOG.md b/CHANGELOG.md index c89b7b7..7d23032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [2.0.0] - 2015-12-20 +### ADDED +- `select()` method to rapidly iterate over a subset of images matching certain criteria +- We parse metadata relating to the physical camera used to produce the images +- Raw metadata can be accessed conveniently, to allow contributors to find more interesting things to add +- An XML parsing library was added since the raw metadata contains some XML blocks +- The version number is now available in the nd2reader module +- Created a DOI to allow citation of the code + +### REMOVED +- The `ImageGroup` container object +- The `data` attribute on Images. Images now inherit from ndarray, making this redundant +- The `image_sets` iterator + ## [1.1.4] - 2015-10-27 ### FIXED - Implemented missing get_image_by_attributes method @@ -9,16 +23,16 @@ ## [1.1.2] - 2015-10-09 ### ADDED - `Image` objects now have a `frame_number` attribute. -- `Nd2` can be used as a context manager. +- `Nd2` can be used as a context manager - More unit tests and functional tests ### CHANGED -- `Image` objects now directly subclass Numpy arrays. +- `Image` objects now directly subclass Numpy arrays - Refactored code to permit parsing of different versions of ND2s, which will allow us to add support for NIS Elements 3.x. ### DEPRECATED -- The `data` attribute is no longer needed since `Image` is now a Numpy array. -- The `image_sets` iterator will be removed in the near future. You should implement this yourself. +- The `data` attribute is no longer needed since `Image` is now a Numpy array +- The `image_sets` iterator will be removed in the near future. You should implement this yourself ## [1.1.1] - 2015-09-02 ### FIXED diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index d845fad..e3a045a 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -1 +1,3 @@ -from nd2reader.interface import Nd2 +from nd2reader.main import Nd2 + +__version__ = '2.0.0' diff --git a/nd2reader/interface.py b/nd2reader/main.py similarity index 95% rename from nd2reader/interface.py rename to nd2reader/main.py index d47cd63..d6bd756 100644 --- a/nd2reader/interface.py +++ b/nd2reader/main.py @@ -72,9 +72,9 @@ class Nd2(object): :type z_levels: int or tuple or list """ - fields_of_view = self._to_list(fields_of_view, self.fields_of_view) - channels = self._to_list(channels, self.channels) - z_levels = self._to_list(z_levels, self.z_levels) + fields_of_view = self._to_tuple(fields_of_view, self.fields_of_view) + channels = self._to_tuple(channels, self.channels) + z_levels = self._to_tuple(z_levels, self.z_levels) for frame in self.frames: field_of_view, channel, z_level = self._parser.driver.calculate_image_properties(frame) @@ -152,6 +152,12 @@ class Nd2(object): @property def camera_settings(self): + """ + Basic information about the physical cameras used. + + :return: dict of {channel_name: model.metadata.CameraSettings} + + """ return self._parser.camera_metadata @property @@ -187,6 +193,14 @@ class Nd2(object): self.height, self.width) + def close(self): + """ + Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or + use Nd2 as a context manager. + + """ + self._fh.close() + def _slice(self, start, stop, step): """ Allows for iteration over a selection of the entire dataset. @@ -204,7 +218,7 @@ class Nd2(object): for i in range(start, stop)[::step]: yield self[i] - def _to_list(self, value, default): + def _to_tuple(self, value, default): """ Idempotently converts a value to a tuple. This allows users to pass in scalar values and iterables to select(), which is more ergonomic than having to remember to pass in single-member lists @@ -216,11 +230,3 @@ class Nd2(object): """ value = default if value is None else value return (value,) if isinstance(value, int) or isinstance(value, six.string_types) else tuple(value) - - def close(self): - """ - Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or - use Nd2 as a context manager. - - """ - self._fh.close() diff --git a/nd2reader/model/metadata.py b/nd2reader/model/metadata.py index 3ee65fa..080efad 100644 --- a/nd2reader/model/metadata.py +++ b/nd2reader/model/metadata.py @@ -99,6 +99,7 @@ class Metadata(object): class CameraSettings(object): + """ Contains some basic information about a physical camera and its settings. """ def __init__(self, name, id, exposure, x_binning, y_binning, channel_name): self.name = name.decode("utf8") self.id = id.decode("utf8") diff --git a/nd2reader/parser/base.py b/nd2reader/parser/base.py index 07fef59..186edfa 100644 --- a/nd2reader/parser/base.py +++ b/nd2reader/parser/base.py @@ -2,18 +2,15 @@ from abc import abstractproperty class BaseParser(object): - @abstractproperty - def metadata(self): - """ - Instantiates a Metadata object. - - """ - raise NotImplementedError + def __init__(self, fh): + self._fh = fh + self.camera_metadata = None + self.metadata = None @abstractproperty def driver(self): """ - Instantiates a driver object. + Must return an object that can look up and read images. """ raise NotImplementedError diff --git a/nd2reader/parser/v3.py b/nd2reader/parser/v3.py index 32bcca6..37ba676 100644 --- a/nd2reader/parser/v3.py +++ b/nd2reader/parser/v3.py @@ -135,36 +135,32 @@ class V3Parser(BaseParser): :type fh: file """ - self._fh = fh - self._metadata = None - self._raw_metadata = None - self._label_map = None - self._camera_metadata = {} + if six.PY3: + super().__init__(fh) + else: + super(V3Parser, self).__init__(fh) + self._label_map = self._build_label_map() + self.raw_metadata = V3RawMetadata(self._fh, self._label_map) + self._parse_camera_metadata() self._parse_metadata() - @property - def camera_metadata(self): - return self._camera_metadata - @property def driver(self): - return V3Driver(self.metadata, self._label_map, self._fh) - - @property - def metadata(self): """ - :rtype: Metadata + Provides an object that knows how to look up and read images based on an index. """ - return self._metadata + return V3Driver(self.metadata, self._label_map, self._fh) - @property - def raw_metadata(self): - if not self._raw_metadata: - self._label_map = self._build_label_map() - self._raw_metadata = V3RawMetadata(self._fh, self._label_map) - return self._raw_metadata + def _parse_camera_metadata(self): + """ + Gets parsed data about the physical cameras used to produce images and throws them in a dictionary. + """ + self.camera_metadata = {} + for camera_setting in self._parse_camera_settings(): + self.camera_metadata[camera_setting.channel_name] = camera_setting + def _parse_metadata(self): """ Reads all metadata and instantiates the Metadata object. @@ -177,14 +173,17 @@ class V3Parser(BaseParser): frames = self._parse_frames(self.raw_metadata) z_levels = self._parse_z_levels(self.raw_metadata) total_images_per_channel = self._parse_total_images_per_channel(self.raw_metadata) - channels = [] - for camera_setting in self._parse_camera_settings(): - channels.append(camera_setting.channel_name) - self._camera_metadata[camera_setting.channel_name] = camera_setting - self._metadata = Metadata(height, width, sorted(list(channels)), date, fields_of_view, frames, z_levels, total_images_per_channel) + channels = sorted([key for key in self.camera_metadata.keys()]) + self.metadata = Metadata(height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel) def _parse_camera_settings(self): - for camera in self._raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')][six.b('sSampleSetting')].values(): + """ + Looks up information in the raw metadata about the camera(s) and puts it into a CameraSettings object. + Duplicate cameras can be returned if the same one was used for multiple channels. + + :return: + """ + for camera in self.raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')][six.b('sSampleSetting')].values(): name = camera[six.b('pCameraSetting')][six.b('CameraUserName')] id = camera[six.b('pCameraSetting')][six.b('CameraUniqueName')] exposure = camera[six.b('dExposureTime')] diff --git a/test.py b/test.py index 49b4387..02a87e0 100644 --- a/test.py +++ b/test.py @@ -2,4 +2,4 @@ import unittest loader = unittest.TestLoader() tests = loader.discover('tests', pattern='*.py', top_level_dir='.') testRunner = unittest.TextTestRunner() -testRunner.run(tests) \ No newline at end of file +testRunner.run(tests)