diff --git a/nd2reader/main.py b/nd2reader/main.py index 9ce3f75..5727d7b 100644 --- a/nd2reader/main.py +++ b/nd2reader/main.py @@ -14,6 +14,7 @@ class Nd2(object): major_version, minor_version = get_version(self._fh) self._parser = get_parser(self._fh, major_version, minor_version) self._metadata = self._parser.metadata + self._roi_metadata = self._parser.roi_metadata def __repr__(self): return "\n".join(["" % self._filename, diff --git a/nd2reader/model/roi.py b/nd2reader/model/roi.py new file mode 100644 index 0000000..23c0f04 --- /dev/null +++ b/nd2reader/model/roi.py @@ -0,0 +1,63 @@ +import six +import numpy as np + + +class Roi(object): + """ + A ND2 ROI representation. + Coordinates are the center coordinates of the ROI in (x, y, z) order. + Sizes are the sizes of the ROI in (x, y, z) order. + Shapes are represented by numbers, defined by constants in this class. + All these properties can be set for multiple timepoints. + """ + SHAPE_RECTANGLE = 3 + SHAPE_CIRCLE = 9 + + def __init__(self, raw_roi_dict): + self.timepoints = [] + self.positions = [] + self.sizes = [] + self.shapes = [] + + self._extract_vect_anims(raw_roi_dict) + + def _extract_vect_anims(self, raw_roi_dict): + """ + Extract the vector animation parameters from the ROI. + This includes the position and size at the given timepoints. + :param raw_roi_dict: + :return: + """ + number_of_timepoints = raw_roi_dict[six.b('m_vectAnimParams_Size')] + + for i in range(number_of_timepoints): + shape = raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')] + self._parse_vect_anim(raw_roi_dict[six.b('m_vectAnimParams_%d') % i], shape) + + # convert to NumPy arrays + self.timepoints = np.array(self.timepoints, dtype=np.float) + self.positions = np.array(self.positions, dtype=np.float) + self.sizes = np.array(self.sizes, dtype=np.float) + self.shapes = np.array(self.shapes, dtype=np.uint) + + def _parse_vect_anim(self, animation_dict, shape): + """ + Parses a ROI vector animation object and adds it to the global list of timepoints and positions. + :param animation_dict: + :return: + """ + self.timepoints.append(animation_dict[six.b('m_dTimeMs')]) + self.positions.append((animation_dict[six.b('m_dCenterX')], + animation_dict[six.b('m_dCenterY')], + animation_dict[six.b('m_dCenterZ')])) + size_dict = animation_dict[six.b('m_sBoxShape')] + self.sizes.append((size_dict[six.b('m_dSizeX')], + size_dict[six.b('m_dSizeY')], + size_dict[six.b('m_dSizeZ')])) + self.shapes.append(shape) + + def is_circle(self, timepoint_id=0): + return self.shapes[timepoint_id] == self.SHAPE_CIRCLE + + def is_rectangle(self, timepoint_id=0): + return self.shapes[timepoint_id] == self.SHAPE_RECTANGLE diff --git a/nd2reader/parser/base.py b/nd2reader/parser/base.py index 186edfa..52186ff 100644 --- a/nd2reader/parser/base.py +++ b/nd2reader/parser/base.py @@ -6,6 +6,7 @@ class BaseParser(object): self._fh = fh self.camera_metadata = None self.metadata = None + self.roi_metadata = None @abstractproperty def driver(self): diff --git a/nd2reader/parser/v3.py b/nd2reader/parser/v3.py index 88c8a45..e7990a7 100644 --- a/nd2reader/parser/v3.py +++ b/nd2reader/parser/v3.py @@ -3,6 +3,7 @@ from datetime import datetime from nd2reader.model.metadata import Metadata from nd2reader.model.label import LabelMap +from nd2reader.model.roi import Roi from nd2reader.parser.base import BaseParser from nd2reader.driver.v3 import V3Driver from nd2reader.common.v3 import read_chunk, read_array, read_metadata @@ -18,6 +19,7 @@ def ignore_missing(func): return func(*args, **kwargs) except: return None + return wrapper @@ -142,6 +144,7 @@ class V3Parser(BaseParser): self._label_map = self._build_label_map() self.raw_metadata = V3RawMetadata(self._fh, self._label_map) self._parse_metadata() + self._parse_roi_metadata() @property def driver(self): @@ -165,7 +168,8 @@ class V3Parser(BaseParser): total_images_per_channel = self._parse_total_images_per_channel(self.raw_metadata) channels = self._parse_channels(self.raw_metadata) pixel_microns = self.raw_metadata.image_calibration.get(six.b('SLxCalibration'), {}).get(six.b('dCalibration')) - self.metadata = Metadata(height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel, pixel_microns) + self.metadata = Metadata(height, width, channels, date, fields_of_view, frames, z_levels, + total_images_per_channel, pixel_microns) def _parse_date(self, raw_metadata): """ @@ -205,7 +209,8 @@ class V3Parser(BaseParser): channels = [] metadata = raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')] try: - validity = raw_metadata.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')] + validity = raw_metadata.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][ + six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')] except (KeyError, TypeError): # If none of the channels have been deleted, there is no validity list, so we just make one validity = [True for _ in metadata] @@ -303,6 +308,26 @@ class V3Parser(BaseParser): """ return raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')] + def _parse_roi_metadata(self): + """ + Parse the raw ROI metadata. + :return: + """ + if not six.b('RoiMetadata_v1') in self.raw_metadata.roi_metadata: + self.roi_metadata = None + return + + raw_roi_data = self.raw_metadata.roi_metadata[six.b('RoiMetadata_v1')] + + number_of_rois = raw_roi_data[six.b('m_vectGlobal_Size')] + + roi_objects = [] + for i in range(number_of_rois): + current_roi = raw_roi_data[six.b('m_vectGlobal_%d' % i)] + roi_objects.append(Roi(current_roi)) + + self.roi_metadata = roi_objects + def _build_label_map(self): """ Every label ends with an exclamation point, however, we can't directly search for those to find all the labels