|
@ -8,9 +8,9 @@ from pims.base_frames import Frame |
|
|
import numpy as np |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
from nd2reader.common import get_version, read_chunk |
|
|
from nd2reader.common import get_version, read_chunk |
|
|
from nd2reader.exceptions import InvalidVersionError |
|
|
|
|
|
from nd2reader.label_map import LabelMap |
|
|
from nd2reader.label_map import LabelMap |
|
|
from nd2reader.raw_metadata import RawMetadata |
|
|
from nd2reader.raw_metadata import RawMetadata |
|
|
|
|
|
from nd2reader import stitched |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Parser(object): |
|
|
class Parser(object): |
|
@ -232,8 +232,7 @@ class Parser(object): |
|
|
Returns: |
|
|
Returns: |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
return (image_group_number - (field_of_view * len(self.metadata["z_levels"]) + z_level)) / ( |
|
|
|
|
|
len(self.metadata["fields_of_view"]) * len(self.metadata["z_levels"])) |
|
|
|
|
|
|
|
|
return (image_group_number - (field_of_view * len(self.metadata["z_levels"]) + z_level)) / (len(self.metadata["fields_of_view"]) * len(self.metadata["z_levels"])) |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _channel_offset(self): |
|
|
def _channel_offset(self): |
|
@ -247,38 +246,6 @@ class Parser(object): |
|
|
""" |
|
|
""" |
|
|
return {channel: n for n, channel in enumerate(self.metadata["channels"])} |
|
|
return {channel: n for n, channel in enumerate(self.metadata["channels"])} |
|
|
|
|
|
|
|
|
def _get_unwanted_bytes_ids( |
|
|
|
|
|
self, image_group_data, image_data_start, height, width |
|
|
|
|
|
): |
|
|
|
|
|
# Check if the byte array size conforms to the image axes size. If not, check |
|
|
|
|
|
# that the number of unexpected (unwanted) bytes is a multiple of the number of |
|
|
|
|
|
# rows (height), as the same unmber of unwanted bytes is expected to be |
|
|
|
|
|
# appended at the end of each row. Then, returns the indexes of the unwanted |
|
|
|
|
|
# bytes. |
|
|
|
|
|
number_of_true_channels = int(len(image_group_data[4:]) / (height * width)) |
|
|
|
|
|
n_unwanted_bytes = (len(image_group_data[image_data_start:])) % (height * width) |
|
|
|
|
|
if not n_unwanted_bytes: |
|
|
|
|
|
return np.arange(0) |
|
|
|
|
|
assert 0 == n_unwanted_bytes % height, ( |
|
|
|
|
|
"An unexpected number of extra bytes was encountered based on the expected" |
|
|
|
|
|
+ " frame size, therefore the file could not be parsed." |
|
|
|
|
|
) |
|
|
|
|
|
return np.arange( |
|
|
|
|
|
image_data_start + height * number_of_true_channels, |
|
|
|
|
|
len(image_group_data) - n_unwanted_bytes + 1, |
|
|
|
|
|
height * number_of_true_channels, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _remove_bytes_by_id(self, byte_ids, image_group_data, height): |
|
|
|
|
|
# Remove bytes by ID. |
|
|
|
|
|
bytes_per_row = len(byte_ids) // height |
|
|
|
|
|
warnings.warn( |
|
|
|
|
|
f"{len(byte_ids)} ({bytes_per_row}*{height}) unexpected zero " |
|
|
|
|
|
+ "bytes were found in the ND2 file and removed to allow further parsing." |
|
|
|
|
|
) |
|
|
|
|
|
for i in range(len(byte_ids)): |
|
|
|
|
|
del image_group_data[byte_ids[i] : (byte_ids[i] + bytes_per_row)] |
|
|
|
|
|
|
|
|
|
|
|
def _get_raw_image_data(self, image_group_number, channel_offset, height, width): |
|
|
def _get_raw_image_data(self, image_group_number, channel_offset, height, width): |
|
|
"""Reads the raw bytes and the timestamp of an image. |
|
|
"""Reads the raw bytes and the timestamp of an image. |
|
|
|
|
|
|
|
@ -300,42 +267,17 @@ class Parser(object): |
|
|
timestamp = struct.unpack("d", data[:8])[0] |
|
|
timestamp = struct.unpack("d", data[:8])[0] |
|
|
image_group_data = array.array("H", data) |
|
|
image_group_data = array.array("H", data) |
|
|
image_data_start = 4 + channel_offset |
|
|
image_data_start = 4 + channel_offset |
|
|
|
|
|
|
|
|
# Stitched ND2 files have been reported to contain unexpected (according to |
|
|
|
|
|
# image shape) zero bytes at the end of each image data row. This hinders |
|
|
|
|
|
# proper reshaping of the data. Hence, here the unwanted zero bytes are |
|
|
|
|
|
# identified and removed. |
|
|
|
|
|
unwanted_byte_ids = self._get_unwanted_bytes_ids( |
|
|
|
|
|
image_group_data, image_data_start, height, width |
|
|
|
|
|
) |
|
|
|
|
|
if 0 != len(unwanted_byte_ids): |
|
|
|
|
|
assert np.all( |
|
|
|
|
|
image_group_data[unwanted_byte_ids + np.arange(len(unwanted_byte_ids))] |
|
|
|
|
|
== 0 |
|
|
|
|
|
), ( |
|
|
|
|
|
f"{len(unwanted_byte_ids)} unexpected non-zero bytes were found" |
|
|
|
|
|
+ " in the ND2 file, the file could not be parsed." |
|
|
|
|
|
) |
|
|
|
|
|
self._remove_bytes_by_id(unwanted_byte_ids, image_group_data, height) |
|
|
|
|
|
|
|
|
image_group_data = stitched.remove_parsed_unwanted_bytes(image_group_data, image_data_start, height, width) |
|
|
|
|
|
|
|
|
# The images for the various channels are interleaved within the same array. For example, the second image |
|
|
# The images for the various channels are interleaved within the same array. For example, the second image |
|
|
# of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design |
|
|
# of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design |
|
|
# a data structure that way, please send the author of this library a message. |
|
|
# a data structure that way, please send the author of this library a message. |
|
|
number_of_true_channels = int(len(image_group_data[4:]) / (height * width)) |
|
|
number_of_true_channels = int(len(image_group_data[4:]) / (height * width)) |
|
|
try: |
|
|
try: |
|
|
image_data = np.reshape( |
|
|
|
|
|
image_group_data[image_data_start::number_of_true_channels], |
|
|
|
|
|
(height, width), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, width)) |
|
|
except ValueError: |
|
|
except ValueError: |
|
|
image_data = np.reshape( |
|
|
|
|
|
image_group_data[image_data_start::number_of_true_channels], |
|
|
|
|
|
( |
|
|
|
|
|
height, |
|
|
|
|
|
len(image_group_data[image_data_start::number_of_true_channels]) |
|
|
|
|
|
// height, |
|
|
|
|
|
), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
new_width = len(image_group_data[image_data_start::number_of_true_channels]) // height |
|
|
|
|
|
image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, new_width)) |
|
|
|
|
|
|
|
|
# Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you |
|
|
# Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you |
|
|
# don't have the same number of images each cycle. We discovered this because we only took GFP images every |
|
|
# don't have the same number of images each cycle. We discovered this because we only took GFP images every |
|
@ -349,9 +291,7 @@ class Parser(object): |
|
|
else: |
|
|
else: |
|
|
empty_frame = np.full((height, width), np.nan) |
|
|
empty_frame = np.full((height, width), np.nan) |
|
|
warnings.warn( |
|
|
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)" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
"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 |
|
|
return timestamp, image_data |
|
|
|
|
|
|
|
|
def _get_frame_metadata(self): |
|
|
def _get_frame_metadata(self): |
|
|