|
|
@ -1,12 +1,9 @@ |
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
|
|
|
import array |
|
|
|
from datetime import datetime |
|
|
|
import logging |
|
|
|
from nd2reader.model import Image, ImageSet |
|
|
|
from nd2reader.parser import Nd2Parser |
|
|
|
import re |
|
|
|
import struct |
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__) |
|
|
|
log.addHandler(logging.StreamHandler()) |
|
|
@ -20,30 +17,30 @@ class Nd2(Nd2Parser): |
|
|
|
|
|
|
|
def __repr__(self): |
|
|
|
return "\n".join(["ND2: %s" % self._filename, |
|
|
|
"Created: %s" % self.absolute_start.strftime("%Y-%m-%d %H:%M:%S"), |
|
|
|
"Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"), |
|
|
|
"Image size: %sx%s (HxW)" % (self.height, self.width), |
|
|
|
"Image cycles: %s" % self.time_index_count, |
|
|
|
"Channels: %s" % ", ".join(["'%s'" % channel for channel in self.channels]), |
|
|
|
"Fields of View: %s" % self.field_of_view_count, |
|
|
|
"Z-Levels: %s" % self.z_level_count |
|
|
|
"Image cycles: %s" % self._time_index_count, |
|
|
|
"Channels: %s" % ", ".join(["'%s'" % channel for channel in self._channels]), |
|
|
|
"Fields of View: %s" % self._field_of_view_count, |
|
|
|
"Z-Levels: %s" % self._z_level_count |
|
|
|
]) |
|
|
|
|
|
|
|
def __iter__(self): |
|
|
|
for i in range(self._image_count): |
|
|
|
for fov in range(self.field_of_view_count): |
|
|
|
for z_level in range(self.z_level_count): |
|
|
|
for channel_name in self.channels: |
|
|
|
for fov in range(self._field_of_view_count): |
|
|
|
for z_level in range(self._z_level_count): |
|
|
|
for channel_name in self._channels: |
|
|
|
image = self.get_image(i, fov, channel_name, z_level) |
|
|
|
if image is not None: |
|
|
|
yield image |
|
|
|
|
|
|
|
@property |
|
|
|
def image_sets(self): |
|
|
|
for time_index in xrange(self.time_index_count): |
|
|
|
for time_index in xrange(self._time_index_count): |
|
|
|
image_set = ImageSet() |
|
|
|
for fov in range(self.field_of_view_count): |
|
|
|
for channel_name in self.channels: |
|
|
|
for z_level in xrange(self.z_level_count): |
|
|
|
for fov in range(self._field_of_view_count): |
|
|
|
for channel_name in self._channels: |
|
|
|
for z_level in xrange(self._z_level_count): |
|
|
|
image = self.get_image(time_index, fov, channel_name, z_level) |
|
|
|
if image is not None: |
|
|
|
image_set.add(image) |
|
|
@ -59,22 +56,6 @@ class Nd2(Nd2Parser): |
|
|
|
else: |
|
|
|
return image |
|
|
|
|
|
|
|
@property |
|
|
|
def channels(self): |
|
|
|
metadata = self.metadata['ImageMetadataSeq']['SLxPictureMetadata']['sPicturePlanes'] |
|
|
|
try: |
|
|
|
validity = self.metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx'][''][0]['ppNextLevelEx'][''][0]['pItemValid'] |
|
|
|
except KeyError: |
|
|
|
# If none of the channels have been deleted, there is no validity list, so we just make one |
|
|
|
validity = [True for _ in metadata] |
|
|
|
# Channel information is contained in dictionaries with the keys a0, a1...an where the number |
|
|
|
# indicates the order in which the channel is stored. So by sorting the dicts alphabetically |
|
|
|
# we get the correct order. |
|
|
|
for (label, chan), valid in zip(sorted(metadata['sPlaneNew'].items()), validity): |
|
|
|
if not valid: |
|
|
|
continue |
|
|
|
yield chan['sDescription'] |
|
|
|
|
|
|
|
@property |
|
|
|
def height(self): |
|
|
|
""" |
|
|
@ -90,102 +71,3 @@ class Nd2(Nd2Parser): |
|
|
|
|
|
|
|
""" |
|
|
|
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiWidth'] |
|
|
|
|
|
|
|
@property |
|
|
|
def absolute_start(self): |
|
|
|
for line in self.metadata['ImageTextInfo']['SLxImageTextInfo'].values(): |
|
|
|
absolute_start_12 = None |
|
|
|
absolute_start_24 = None |
|
|
|
# ND2s seem to randomly switch between 12- and 24-hour representations. |
|
|
|
try: |
|
|
|
absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S") |
|
|
|
except ValueError: |
|
|
|
pass |
|
|
|
try: |
|
|
|
absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p") |
|
|
|
except ValueError: |
|
|
|
pass |
|
|
|
if not absolute_start_12 and not absolute_start_24: |
|
|
|
continue |
|
|
|
return absolute_start_12 if absolute_start_12 else absolute_start_24 |
|
|
|
raise ValueError("This ND2 has no recorded start time. This is probably a bug.") |
|
|
|
|
|
|
|
@property |
|
|
|
def channel_count(self): |
|
|
|
pattern = r""".*?λ\((\d+)\).*?""" |
|
|
|
try: |
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
except AttributeError: |
|
|
|
return 1 |
|
|
|
else: |
|
|
|
return count |
|
|
|
|
|
|
|
@property |
|
|
|
def field_of_view_count(self): |
|
|
|
""" |
|
|
|
The metadata contains information about fields of view, but it contains it even if some fields |
|
|
|
of view were cropped. We can't find anything that states which fields of view are actually |
|
|
|
in the image data, so we have to calculate it. There probably is something somewhere, since |
|
|
|
NIS Elements can figure it out, but we haven't found it yet. |
|
|
|
|
|
|
|
""" |
|
|
|
pattern = r""".*?XY\((\d+)\).*?""" |
|
|
|
try: |
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
except AttributeError: |
|
|
|
return 1 |
|
|
|
else: |
|
|
|
return count |
|
|
|
|
|
|
|
@property |
|
|
|
def time_index_count(self): |
|
|
|
""" |
|
|
|
The number of cycles. |
|
|
|
|
|
|
|
:rtype: int |
|
|
|
|
|
|
|
""" |
|
|
|
pattern = r""".*?T'\((\d+)\).*?""" |
|
|
|
try: |
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
except AttributeError: |
|
|
|
return 1 |
|
|
|
else: |
|
|
|
return count |
|
|
|
|
|
|
|
@property |
|
|
|
def z_level_count(self): |
|
|
|
pattern = r""".*?Z\((\d+)\).*?""" |
|
|
|
try: |
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
except AttributeError: |
|
|
|
return 1 |
|
|
|
else: |
|
|
|
return count |
|
|
|
|
|
|
|
@property |
|
|
|
def _channel_offset(self): |
|
|
|
""" |
|
|
|
Image data is interleaved for each image set. That is, if there are four images in a set, the first image |
|
|
|
will consist of pixels 1, 5, 9, etc, the second will be pixels 2, 6, 10, and so forth. Why this would be the |
|
|
|
case is beyond me, but that's how it works. |
|
|
|
|
|
|
|
""" |
|
|
|
channel_offset = {} |
|
|
|
for n, channel in enumerate(self.channels): |
|
|
|
channel_offset[channel] = n |
|
|
|
return channel_offset |
|
|
|
|
|
|
|
def _get_raw_image_data(self, image_set_number, channel_offset): |
|
|
|
chunk = self._label_map["ImageDataSeq|%d!" % image_set_number] |
|
|
|
data = self._read_chunk(chunk) |
|
|
|
timestamp = struct.unpack("d", data[:8])[0] |
|
|
|
# The images for the various channels are interleaved within each other. |
|
|
|
image_data = array.array("H", data) |
|
|
|
image_data_start = 4 + channel_offset |
|
|
|
if any(image_data): |
|
|
|
return timestamp, image_data[image_data_start::self.channel_count] |
|
|
|
return None |
|
|
|
|
|
|
|
def _calculate_image_set_number(self, time_index, fov, z_level): |
|
|
|
return time_index * self.field_of_view_count * self.z_level_count + (fov * self.z_level_count + z_level) |