diff --git a/COPYING b/COPYING index 7cd7b82..b242b98 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,5 @@ Copyright 2014-2016 Jim Rybarski, 2017 Ruben Verweij +Copyright 2020-2021 Lorenzo Zolfanelli, ESPCI Paris - PSL nd2reader is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/nd2reader/parser.py b/nd2reader/parser.py index 8534e79..bebbcaa 100644 --- a/nd2reader/parser.py +++ b/nd2reader/parser.py @@ -78,6 +78,38 @@ class Parser(object): else: return Frame(image, frame_no=frame_number, metadata=self._get_frame_metadata()) + def get_slice_by_attributes(self, xywh, frame_number, field_of_view, channel, z_level, height, width): + """Gets a rectangular slice of an image based on its attributes alone + + Args: + xywh: tuples containing (x, y, w, h) values of the + rectangular region to load + frame_number: the frame number + field_of_view: the field of view + channel_name: the color channel name + z_level: the z level + height: the height of the image + width: the width of the image + + Returns: + Frame: the requested image + + """ + frame_number = 0 if frame_number is None else frame_number + field_of_view = 0 if field_of_view is None else field_of_view + channel = 0 if channel is None else channel + z_level = 0 if z_level is None else z_level + + image_group_number = self._calculate_image_group_number(frame_number, field_of_view, z_level) + try: + timestamp, raw_image_data = self._get_raw_slice_data( + xywh, image_group_number, channel, height, width + ) + except (TypeError): + return Frame([], frame_no=frame_number, metadata=self._get_frame_metadata()) + else: + return Frame(raw_image_data, frame_no=frame_number, metadata=self._get_frame_metadata()) + def get_image_by_attributes(self, frame_number, field_of_view, channel, z_level, height, width): """Gets an image based on its attributes alone @@ -246,6 +278,79 @@ class Parser(object): """ return {channel: n for n, channel in enumerate(self.metadata["channels"])} + def _get_raw_slice_data(self, xywh, image_group_number, channel, height, width): + """Reads the raw bytes and the timestamp of a rectangular slice + of an image. + + Args: + xywh: tuples containing (x, y, w, h) values of the + rectangular region to load + image_group_number: the image group number (see _calculate_image_group_number) + channel: the position (int) of the channel to load + height: the height of the image + width: the width of the image + + Returns: + + """ + size_c = len(self.metadata["channels"]) + + x0, y0, w, h = xywh + chunk_location = self._label_map.get_image_data_location(image_group_number) + fh = self._fh + + if chunk_location is None or fh is None: + return None + fh.seek(chunk_location) + # The chunk metadata is always 16 bytes long + chunk_metadata = fh.read(16) + header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) + if header != 0xabeceda: + raise ValueError("The ND2 file seems to be corrupted.") + # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the + # start of the actual data field, which is at some arbitrary place after the metadata. + fh.seek(chunk_location + 16 + relative_offset) + + # Read timestamp (8 bytes) + timestamp = struct.unpack("d", fh.read(8))[0] + + # Stitched Images: evaluate number of bytes to strip + # (with stitched images sometimes after each row we have a regular number of extra bytes) + n_unwanted_bytes = (data_length-8) % (height*width) + assert 0 == n_unwanted_bytes % height + rowskip = n_unwanted_bytes // height + + # Read ROI: row-by-row + image_start_pos = chunk_location + 16 + relative_offset + 8 + + line_bytemask = np.zeros(size_c, dtype=np.bool) + line_bytemask[channel] = True + line_bytemask = np.tile(line_bytemask.repeat(2),w) + + def get_line(y): + fh.seek(image_start_pos + size_c*2*((width)*y+x0) + y*rowskip) + return np.frombuffer(fh.read(size_c*2*w), np.byte)[line_bytemask] + + data = [get_line(y) for y in range(y0, y0+h)] + data = bytes().join(data) + + image_group_data = array.array("H", data) + true_channels_no = int(len(image_group_data) / (h * w)) + + image_data = np.reshape(image_group_data, (h, w, true_channels_no)) + + missing_channels = ~np.any(image_data, axis=(0, 1)) + image_data[..., missing_channels] = np.full( + (h, w, missing_channels.sum()), np.nan) + + if np.any(missing_channels): + warnings.warn( + "ND2 file contains gap frames which are represented by " + + "np.nan-filled arrays; to convert to zeros use e.g. " + + "np.nan_to_num(array)") + return timestamp, image_data[...,0] + + def _get_raw_image_data(self, image_group_number, channel_offset, height, width): """Reads the raw bytes and the timestamp of an image. diff --git a/nd2reader/raw_metadata.py b/nd2reader/raw_metadata.py index 1152219..3c25d57 100644 --- a/nd2reader/raw_metadata.py +++ b/nd2reader/raw_metadata.py @@ -45,9 +45,13 @@ class RawMetadata(object): "frames": self._parse_frames(), "z_levels": self._parse_z_levels(), "z_coordinates": parse_if_not_none(self.z_data, self._parse_z_coordinates), + "x_coordinates": parse_if_not_none(self.x_data, self._parse_x_coordinates), + "y_coordinates": parse_if_not_none(self.y_data, self._parse_y_coordinates), "total_images_per_channel": frames_per_channel, "channels": self._parse_channels(), - "pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration) + "pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration), + "camera_stage_angle": parse_if_not_none(self.image_metadata_sequence, self._parse_camera_angle), + "camera_stage_matrix": parse_if_not_none(self.image_metadata_sequence, self._parse_camera_matrix) } self._set_default_if_not_empty('fields_of_view') @@ -195,6 +199,62 @@ class RawMetadata(object): """ return self.z_data.tolist() + def _parse_x_coordinates(self): + """The coordinate in micron for all x frames. + + Returns: + list: the x coordinates in micron + """ + return self.x_data.tolist() + + def _parse_y_coordinates(self): + """The coordinate in micron for all y frames. + + Returns: + list: the y coordinates in micron + """ + return self.y_data.tolist() + + def _parse_camera_angle(self): + if self.image_metadata_sequence is None: + return [] + + try: + metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')] + except KeyError: + return [] + + try: + return metadata[b'dAngle'] + except KeyError: + return None + + def _parse_camera_matrix(self): + if self.image_metadata_sequence is None: + return [] + + try: + metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')][b'sPicturePlanes'] + except KeyError: + return [] + + validity = self._get_channel_validity_list(metadata) + + channels = [] + for valid, (label, chan) in zip(validity, sorted(metadata[b'sSampleSetting'].items())): + if not valid: + continue + if chan[b'matCameraToStage'] is not None: + mat_data = chan[b'matCameraToStage'][b'Data'] + mat_rows = chan[b'matCameraToStage'][b'Rows'] + mat_columns = chan[b'matCameraToStage'][b'Columns'] + mat = np.frombuffer(mat_data, dtype=np.float64).reshape([mat_rows, mat_columns]) + channels.append(mat) + else: + channels.append(None) + return channels + + def _parse_dimension_text(self): """While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable. Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text diff --git a/nd2reader/reader.py b/nd2reader/reader.py index 4e14f42..295bded 100644 --- a/nd2reader/reader.py +++ b/nd2reader/reader.py @@ -69,6 +69,21 @@ class ND2Reader(FramesSequenceND): except KeyError: return 0 + def get_roi(self, roi, c=0, t=0, z=0, x=0, y=0, v=0): + height = self.metadata['height'] + width = self.metadata['width'] + ylim = roi[0].indices(height) + xlim = roi[1].indices(width) + + y = ylim[0] + x = xlim[0] + w = xlim[1]-xlim[0] + h = ylim[1]-ylim[0] + + return self._parser.get_slice_by_attributes( + (x, y, w, h), t, v, c, z, height, width + ) + def get_frame_2D(self, c=0, t=0, z=0, x=0, y=0, v=0): """Gets a given frame using the parser Args: diff --git a/setup.py b/setup.py index c5dde1c..5b6ebdd 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ from setuptools import setup - -VERSION = '3.3.0' +#from nd2reader import __version__ as VERSION if __name__ == '__main__': setup( @@ -10,15 +9,15 @@ if __name__ == '__main__': 'numpy>=1.14', 'six>=1.4', 'xmltodict>=0.9.2', - 'pims>=0.3.0' + 'PIMS>=0.5.0' ], python_requires=">=3.6", - version=VERSION, + version="3.2.3-zolfa-dev0", description='A tool for reading ND2 files produced by NIS Elements', author='Ruben Verweij', author_email='ruben@lighthacking.nl', url='https://github.com/rbnvrw/nd2reader', - download_url='https://github.com/rbnvrw/nd2reader/tarball/%s' % VERSION, + download_url='https://github.com/rbnvrw/nd2reader/tarball/%s' % "3.2.3-zolfa-dev0", keywords=['nd2', 'nikon', 'microscopy', 'NIS Elements'], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research',