Compare commits

...

5 changed files with 186 additions and 5 deletions
Split View
  1. +1
    -0
      COPYING
  2. +105
    -0
      nd2reader/parser.py
  3. +61
    -1
      nd2reader/raw_metadata.py
  4. +15
    -0
      nd2reader/reader.py
  5. +4
    -4
      setup.py

+ 1
- 0
COPYING View File

@ -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


+ 105
- 0
nd2reader/parser.py View File

@ -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.


+ 61
- 1
nd2reader/raw_metadata.py View File

@ -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


+ 15
- 0
nd2reader/reader.py View File

@ -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:


+ 4
- 4
setup.py View File

@ -1,5 +1,5 @@
from setuptools import setup
from nd2reader import __version__ as VERSION
#from nd2reader import __version__ as VERSION
if __name__ == '__main__':
setup(
@ -9,14 +9,14 @@ if __name__ == '__main__':
'numpy>=1.6.2',
'six>=1.4',
'xmltodict>=0.9.2',
'pims>=0.3.0'
'PIMS>=0.5.0'
],
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',


Loading…
Cancel
Save