from pims import FramesSequenceND, Frame
|
|
from nd2reader.parser import Parser
|
|
import numpy as np
|
|
|
|
|
|
class ND2Reader(FramesSequenceND):
|
|
"""PIMS wrapper for the ND2 parser.
|
|
This is the main class: use this to process your .nd2 files.
|
|
"""
|
|
|
|
def __init__(self, filename):
|
|
self.filename = filename
|
|
|
|
# first use the parser to parse the file
|
|
self._fh = open(filename, "rb")
|
|
self._parser = Parser(self._fh)
|
|
|
|
# Setup metadata
|
|
self.metadata = self._parser.metadata
|
|
|
|
# Set data type
|
|
self._dtype = self._parser.get_dtype_from_metadata()
|
|
|
|
# Setup the axes
|
|
self._setup_axes()
|
|
|
|
@classmethod
|
|
def class_exts(cls):
|
|
"""Let PIMS open function use this reader for opening .nd2 files
|
|
|
|
"""
|
|
return {'nd2'} | super(ND2Reader, cls).class_exts()
|
|
|
|
def close(self):
|
|
"""Correctly close the file handle
|
|
|
|
"""
|
|
if self._fh is not None:
|
|
self._fh.close()
|
|
|
|
def get_frame(self, i):
|
|
"""Return one frame
|
|
|
|
Args:
|
|
i: The frame number
|
|
|
|
Returns:
|
|
numpy.ndarray: The requested frame
|
|
|
|
"""
|
|
fetch_all_channels = 'c' in self.bundle_axes
|
|
|
|
if fetch_all_channels:
|
|
return self._get_frame_all_channels(i)
|
|
else:
|
|
return self.get_frame_2D(self.default_coords['c'], i, self.default_coords['z'])
|
|
|
|
def _get_frame_all_channels(self, i):
|
|
"""Get all color channels for this frame
|
|
|
|
Args:
|
|
i: The frame number
|
|
|
|
Returns:
|
|
numpy.ndarray: The requested frame, with all color channels.
|
|
|
|
"""
|
|
frames = None
|
|
for c in range(len(self.metadata["channels"])):
|
|
frame = self.get_frame_2D(c, i, self.default_coords['z'])
|
|
if frames is None:
|
|
frames = Frame([frame])
|
|
else:
|
|
frames = np.concatenate((frames, [frame]), axis=0)
|
|
return frames
|
|
|
|
def get_frame_2D(self, c, t, z):
|
|
"""Gets a given frame using the parser
|
|
|
|
Args:
|
|
c: The color channel number
|
|
t: The frame number
|
|
z: The z stack number
|
|
|
|
Returns:
|
|
numpy.ndarray: The requested frame
|
|
|
|
"""
|
|
c_name = self.metadata["channels"][c]
|
|
return self._parser.get_image_by_attributes(t, 0, c_name, z, self.metadata["height"], self.metadata["width"])
|
|
|
|
@property
|
|
def parser(self):
|
|
"""
|
|
Returns the parser object.
|
|
Returns:
|
|
Parser: the parser object
|
|
"""
|
|
return self._parser
|
|
|
|
@property
|
|
def pixel_type(self):
|
|
"""Return the pixel data type
|
|
|
|
Returns:
|
|
dtype: the pixel data type
|
|
|
|
"""
|
|
return self._dtype
|
|
|
|
def _get_metadata_property(self, key, default=None):
|
|
if self.metadata is None:
|
|
return default
|
|
|
|
if key not in self.metadata:
|
|
return default
|
|
|
|
if self.metadata[key] is None:
|
|
return default
|
|
|
|
return self.metadata[key]
|
|
|
|
def _setup_axes(self):
|
|
"""Setup the xyctz axes, iterate over t axis by default
|
|
|
|
"""
|
|
self._init_axis('x', self._get_metadata_property("width", default=0))
|
|
self._init_axis('y', self._get_metadata_property("height", default=0))
|
|
self._init_axis('c', len(self._get_metadata_property("channels", default=[])))
|
|
self._init_axis('t', len(self._get_metadata_property("frames", default=[])))
|
|
self._init_axis('z', len(self._get_metadata_property("z_levels", default=[])))
|
|
|
|
# provide the default
|
|
self.iter_axes = 't'
|
|
|
|
def get_timesteps(self):
|
|
"""Get the timesteps of the experiment
|
|
|
|
Returns:
|
|
np.ndarray: an array of times in milliseconds.
|
|
|
|
"""
|
|
timesteps = np.array([])
|
|
current_time = 0.0
|
|
for loop in self.metadata['experiment']['loops']:
|
|
if loop['stimulation']:
|
|
continue
|
|
|
|
timesteps = np.concatenate(
|
|
(timesteps, np.arange(current_time, current_time + loop['duration'], loop['sampling_interval'])))
|
|
current_time += loop['duration']
|
|
|
|
# if experiment did not finish, number of timesteps is wrong. Take correct amount of leading timesteps.
|
|
return timesteps[:self.metadata['num_frames']]
|