|
@ -21,13 +21,18 @@ class Nd2Parser(object): |
|
|
def __init__(self, filename): |
|
|
def __init__(self, filename): |
|
|
self._filename = filename |
|
|
self._filename = filename |
|
|
self._fh = None |
|
|
self._fh = None |
|
|
|
|
|
self._channels = None |
|
|
|
|
|
self._channel_count = None |
|
|
self._chunk_map_start_location = None |
|
|
self._chunk_map_start_location = None |
|
|
self._cursor_position = 0 |
|
|
self._cursor_position = 0 |
|
|
self._dimension_text = None |
|
|
self._dimension_text = None |
|
|
|
|
|
self._fields_of_view = None |
|
|
self._label_map = {} |
|
|
self._label_map = {} |
|
|
self.metadata = {} |
|
|
self.metadata = {} |
|
|
self._read_map() |
|
|
self._read_map() |
|
|
|
|
|
self._time_indexes = None |
|
|
self._parse_metadata() |
|
|
self._parse_metadata() |
|
|
|
|
|
self._z_levels = None |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _file_handle(self): |
|
|
def _file_handle(self): |
|
@ -93,7 +98,7 @@ class Nd2Parser(object): |
|
|
return self._dimension_text |
|
|
return self._dimension_text |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _channels(self): |
|
|
|
|
|
|
|
|
def channels(self): |
|
|
""" |
|
|
""" |
|
|
These are labels created by the NIS Elements user. Typically they may a short description of the filter cube |
|
|
These are labels created by the NIS Elements user. Typically they may a short description of the filter cube |
|
|
used (e.g. "bright field", "GFP", etc.) |
|
|
used (e.g. "bright field", "GFP", etc.) |
|
@ -101,19 +106,21 @@ class Nd2Parser(object): |
|
|
:rtype: str |
|
|
:rtype: str |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
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'] |
|
|
|
|
|
|
|
|
if not self._channels: |
|
|
|
|
|
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 |
|
|
|
|
|
self._channels.append(chan['sDescription']) |
|
|
|
|
|
return self._channels |
|
|
|
|
|
|
|
|
def _calculate_image_group_number(self, time_index, fov, z_level): |
|
|
def _calculate_image_group_number(self, time_index, fov, z_level): |
|
|
""" |
|
|
""" |
|
@ -126,7 +133,7 @@ class Nd2Parser(object): |
|
|
:rtype: int |
|
|
:rtype: int |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
return time_index * self._field_of_view_count * self._z_level_count + (fov * self._z_level_count + z_level) |
|
|
|
|
|
|
|
|
return time_index * len(self.fields_of_view) * len(self.z_levels) + (fov * len(self.z_levels) + z_level) |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _channel_offset(self): |
|
|
def _channel_offset(self): |
|
@ -167,24 +174,28 @@ class Nd2Parser(object): |
|
|
return absolute_start_12 if absolute_start_12 else absolute_start_24 |
|
|
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.") |
|
|
raise ValueError("This ND2 has no recorded start time. This is probably a bug.") |
|
|
|
|
|
|
|
|
|
|
|
def _parse_dimension_text(self, pattern): |
|
|
|
|
|
try: |
|
|
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
|
|
except AttributeError: |
|
|
|
|
|
return [0] |
|
|
|
|
|
else: |
|
|
|
|
|
return range(count) |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _channel_count(self): |
|
|
|
|
|
|
|
|
def channel_count(self): |
|
|
""" |
|
|
""" |
|
|
The number of different channels used, including bright field. |
|
|
The number of different channels used, including bright field. |
|
|
|
|
|
|
|
|
:rtype: int |
|
|
:rtype: int |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
pattern = r""".*?λ\((\d+)\).*?""" |
|
|
|
|
|
try: |
|
|
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
|
|
except AttributeError: |
|
|
|
|
|
return 1 |
|
|
|
|
|
else: |
|
|
|
|
|
return count |
|
|
|
|
|
|
|
|
if self._channel_count is None: |
|
|
|
|
|
self._channel_count = self._parse_dimension_text(r""".*?λ\((\d+)\).*?""") |
|
|
|
|
|
return self._channel_count |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _field_of_view_count(self): |
|
|
|
|
|
|
|
|
def fields_of_view(self): |
|
|
""" |
|
|
""" |
|
|
The metadata contains information about fields of view, but it contains it even if some fields |
|
|
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 |
|
|
of view were cropped. We can't find anything that states which fields of view are actually |
|
@ -194,45 +205,33 @@ class Nd2Parser(object): |
|
|
:rtype: int |
|
|
:rtype: int |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
pattern = r""".*?XY\((\d+)\).*?""" |
|
|
|
|
|
try: |
|
|
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
|
|
except AttributeError: |
|
|
|
|
|
return 1 |
|
|
|
|
|
else: |
|
|
|
|
|
return count |
|
|
|
|
|
|
|
|
if self._fields_of_view is None: |
|
|
|
|
|
self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""") |
|
|
|
|
|
return self._fields_of_view |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _time_index_count(self): |
|
|
|
|
|
|
|
|
def time_indexes(self): |
|
|
""" |
|
|
""" |
|
|
The number of cycles. |
|
|
The number of cycles. |
|
|
|
|
|
|
|
|
:rtype: int |
|
|
:rtype: int |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
pattern = r""".*?T'\((\d+)\).*?""" |
|
|
|
|
|
try: |
|
|
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
|
|
except AttributeError: |
|
|
|
|
|
return 1 |
|
|
|
|
|
else: |
|
|
|
|
|
return count |
|
|
|
|
|
|
|
|
if self._time_indexes is None: |
|
|
|
|
|
self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""") |
|
|
|
|
|
return self._time_indexes |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _z_level_count(self): |
|
|
|
|
|
|
|
|
def z_levels(self): |
|
|
""" |
|
|
""" |
|
|
The number of different levels in the Z-plane. |
|
|
|
|
|
|
|
|
The different levels in the Z-plane. Just a sequence from 0 to n. |
|
|
|
|
|
|
|
|
:rtype: int |
|
|
:rtype: int |
|
|
|
|
|
|
|
|
""" |
|
|
""" |
|
|
pattern = r""".*?Z\((\d+)\).*?""" |
|
|
|
|
|
try: |
|
|
|
|
|
count = int(re.match(pattern, self._dimensions).group(1)) |
|
|
|
|
|
except AttributeError: |
|
|
|
|
|
return 1 |
|
|
|
|
|
else: |
|
|
|
|
|
return count |
|
|
|
|
|
|
|
|
if self._z_levels is None: |
|
|
|
|
|
self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""") |
|
|
|
|
|
return self._z_levels |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def _image_count(self): |
|
|
def _image_count(self): |
|
|