diff --git a/nd2reader/common_raw_metadata.py b/nd2reader/common_raw_metadata.py new file mode 100644 index 0000000..21f9a78 --- /dev/null +++ b/nd2reader/common_raw_metadata.py @@ -0,0 +1,102 @@ +import six +import warnings + +from nd2reader.common import get_from_dict_if_exists + + +def parse_if_not_none(to_check, callback): + if to_check is not None: + return callback() + return None + + +def parse_dimension_text_line(line): + if six.b("Dimensions:") in line: + entries = line.split(six.b("\r\n")) + for entry in entries: + if entry.startswith(six.b("Dimensions:")): + return entry + return None + + +def parse_roi_shape(shape): + if shape == 3: + return 'rectangle' + elif shape == 9: + return 'circle' + + return None + + +def parse_roi_type(type_no): + if type_no == 4: + return 'stimulation' + elif type_no == 3: + return 'reference' + elif type_no == 2: + return 'background' + + return None + + +def get_loops_from_data(loop_data): + loops = [loop_data] + if six.b('uiPeriodCount') in loop_data and loop_data[six.b('uiPeriodCount')] > 0: + # special ND experiment + if six.b('pPeriod') not in loop_data: + return [] + + # take the first dictionary element, it contains all loop data + loops = loop_data[six.b('pPeriod')][list(loop_data[six.b('pPeriod')].keys())[0]] + return loops + + +def guess_sampling_from_loops(duration, loop): + """ In some cases, both keys are not saved. Then try to calculate it. + + Args: + duration: the total duration of the loop + loop: the raw loop data + + Returns: + float: the guessed sampling interval in milliseconds + + """ + number_of_loops = get_from_dict_if_exists('uiCount', loop) + number_of_loops = number_of_loops if number_of_loops is not None and number_of_loops > 0 else 1 + interval = duration / number_of_loops + return interval + + +def determine_sampling_interval(duration, loop): + """Determines the loop sampling interval in milliseconds + + Args: + duration: loop duration in milliseconds + loop: loop dictionary + + Returns: + float: the sampling interval in milliseconds + + """ + interval = get_from_dict_if_exists('dPeriod', loop) + avg_interval = get_from_dict_if_exists('dAvgPeriodDiff', loop) + + if interval is None or interval <= 0: + interval = avg_interval + else: + avg_interval_set = avg_interval is not None and avg_interval > 0 + + if round(avg_interval) != round(interval) and avg_interval_set: + warnings.warn("Reported average frame interval (%.1f ms) doesn't" + + "match the set interval (%.1f ms)." + + "Using the average now." % (avg_interval, + interval), + RuntimeWarning) + interval = avg_interval + + if interval is None or interval <= 0: + # In some cases, both keys are not saved. Then try to calculate it. + interval = guess_sampling_from_loops(duration, loop) + + return interval diff --git a/nd2reader/raw_metadata.py b/nd2reader/raw_metadata.py index 4487acd..6d9918e 100644 --- a/nd2reader/raw_metadata.py +++ b/nd2reader/raw_metadata.py @@ -1,9 +1,10 @@ import re -from nd2reader.common import read_chunk, read_array, read_metadata, parse_date, get_from_dict_if_exists import xmltodict import six import numpy as np -import warnings + +from nd2reader.common import read_chunk, read_array, read_metadata, parse_date, get_from_dict_if_exists +from nd2reader.common_raw_metadata import parse_dimension_text_line, parse_if_not_none, parse_roi_shape, parse_roi_type, get_loops_from_data, determine_sampling_interval class RawMetadata(object): @@ -36,15 +37,15 @@ class RawMetadata(object): frames_per_channel = self._parse_total_images_per_channel() self._metadata_parsed = { - "height": self._parse_if_not_none(self.image_attributes, self._parse_height), - "width": self._parse_if_not_none(self.image_attributes, self._parse_width), - "date": self._parse_if_not_none(self.image_text_info, self._parse_date), + "height": parse_if_not_none(self.image_attributes, self._parse_height), + "width": parse_if_not_none(self.image_attributes, self._parse_width), + "date": parse_if_not_none(self.image_text_info, self._parse_date), "fields_of_view": self._parse_fields_of_view(), "frames": self._parse_frames(), "z_levels": self._parse_z_levels(), "total_images_per_channel": frames_per_channel, "channels": self._parse_channels(), - "pixel_microns": self._parse_if_not_none(self.image_calibration, self._parse_calibration), + "pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration), } self._set_default_if_not_empty('fields_of_view') @@ -64,12 +65,6 @@ class RawMetadata(object): # if the file is not empty, we always have one of this entry self._metadata_parsed[entry] = [0] - @staticmethod - def _parse_if_not_none(to_check, callback): - if to_check is not None: - return callback() - return None - def _parse_width_or_height(self, key): try: length = self.image_attributes[six.b('SLxImageAttributes')][six.b(key)] @@ -181,21 +176,12 @@ class RawMetadata(object): return dimension_text for line in textinfo: - entry = self._parse_dimension_text_line(line) + entry = parse_dimension_text_line(line) if entry is not None: return entry return dimension_text - @staticmethod - def _parse_dimension_text_line(line): - if six.b("Dimensions:") in line: - entries = line.split(six.b("\r\n")) - for entry in entries: - if entry.startswith(six.b("Dimensions:")): - return entry - return None - def _parse_dimension(self, pattern): dimension_text = self._parse_dimension_text() if dimension_text is None: @@ -262,8 +248,8 @@ class RawMetadata(object): "timepoints": [], "positions": [], "sizes": [], - "shape": self._parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]), - "type": self._parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')]) + "shape": parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]), + "type": parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')]) } for i in range(number_of_timepoints): roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[six.b('m_vectAnimParams_%d' % i)]) @@ -275,26 +261,6 @@ class RawMetadata(object): return roi_dict - @staticmethod - def _parse_roi_shape(shape): - if shape == 3: - return 'rectangle' - elif shape == 9: - return 'circle' - - return None - - @staticmethod - def _parse_roi_type(type_no): - if type_no == 4: - return 'stimulation' - elif type_no == 3: - return 'reference' - elif type_no == 2: - return 'background' - - return None - def _parse_vect_anim(self, roi_dict, animation_dict): """ Parses a ROI vector animation object and adds it to the global list of timepoints and positions. @@ -346,18 +312,6 @@ class RawMetadata(object): if six.b('uLoopPars') in raw_data: self._metadata_parsed['experiment']['loops'] = self._parse_loop_data(raw_data[six.b('uLoopPars')]) - @staticmethod - def _get_loops_from_data(loop_data): - loops = [loop_data] - if six.b('uiPeriodCount') in loop_data and loop_data[six.b('uiPeriodCount')] > 0: - # special ND experiment - if six.b('pPeriod') not in loop_data: - return [] - - # take the first dictionary element, it contains all loop data - loops = loop_data[six.b('pPeriod')][list(loop_data[six.b('pPeriod')].keys())[0]] - return loops - def _parse_loop_data(self, loop_data): """Parse the experimental loop data @@ -368,7 +322,7 @@ class RawMetadata(object): list: list of the parsed loops """ - loops = self._get_loops_from_data(loop_data) + loops = get_loops_from_data(loop_data) # take into account the absolute time in ms time_offset = 0 @@ -378,7 +332,7 @@ class RawMetadata(object): for loop in loops: # duration of this loop duration = get_from_dict_if_exists('dDuration', loop) or 0 - interval = self._determine_sampling_interval(duration, loop) + interval = determine_sampling_interval(duration, loop) # if duration is not saved, infer it duration = self.get_duration_from_interval_and_loops(duration, interval, loop) @@ -419,49 +373,6 @@ class RawMetadata(object): return duration - @staticmethod - def _determine_sampling_interval(duration, loop): - """Determines the loop sampling interval in milliseconds - - Args: - duration: loop duration in milliseconds - loop: loop dictionary - - Returns: - float: the sampling interval in milliseconds - - """ - interval = get_from_dict_if_exists('dPeriod', loop) - if interval is None or interval <= 0: - # Use a fallback if it is still not found - interval = get_from_dict_if_exists('dAvgPeriodDiff', loop) - else: - avg_interval = get_from_dict_if_exists('dAvgPeriodDiff', loop) - if round(avg_interval) != round(interval): - warnings.warn("Reported average frame interval (%.1f ms) doesn't match the set interval (%.1f ms). Using the average now." % (avg_interval, interval), RuntimeWarning) - interval = avg_interval - - if interval is None or interval <= 0: - # In some cases, both keys are not saved. Then try to calculate it. - interval = RawMetadata._guess_sampling_from_loops(duration, loop) - return interval - - @staticmethod - def _guess_sampling_from_loops(duration, loop): - """ In some cases, both keys are not saved. Then try to calculate it. - - Args: - duration: the total duration of the loop - loop: the raw loop data - - Returns: - float: the guessed sampling interval in milliseconds - - """ - number_of_loops = get_from_dict_if_exists('uiCount', loop) - number_of_loops = number_of_loops if number_of_loops is not None and number_of_loops > 0 else 1 - interval = duration / number_of_loops - return interval @property def image_text_info(self): diff --git a/tests/test_raw_metadata.py b/tests/test_raw_metadata.py index 2ff8f8d..ec44003 100644 --- a/tests/test_raw_metadata.py +++ b/tests/test_raw_metadata.py @@ -4,6 +4,7 @@ import six from nd2reader.artificial import ArtificialND2 from nd2reader.label_map import LabelMap from nd2reader.raw_metadata import RawMetadata +from nd2reader.common_raw_metadata import parse_roi_shape, parse_roi_type class TestRawMetadata(unittest.TestCase): @@ -14,15 +15,15 @@ class TestRawMetadata(unittest.TestCase): self.metadata = RawMetadata(self.nd2.file_handle, self.label_map) def test_parse_roi_shape(self): - self.assertEqual(self.metadata._parse_roi_shape(3), 'rectangle') - self.assertEqual(self.metadata._parse_roi_shape(9), 'circle') - self.assertIsNone(self.metadata._parse_roi_shape(-1)) + self.assertEqual(parse_roi_shape(3), 'rectangle') + self.assertEqual(parse_roi_shape(9), 'circle') + self.assertIsNone(parse_roi_shape(-1)) def test_parse_roi_type(self): - self.assertEqual(self.metadata._parse_roi_type(3), 'reference') - self.assertEqual(self.metadata._parse_roi_type(2), 'background') - self.assertEqual(self.metadata._parse_roi_type(4), 'stimulation') - self.assertIsNone(self.metadata._parse_roi_type(-1)) + self.assertEqual(parse_roi_type(3), 'reference') + self.assertEqual(parse_roi_type(2), 'background') + self.assertEqual(parse_roi_type(4), 'stimulation') + self.assertIsNone(parse_roi_type(-1)) def test_dict(self): self.assertTrue(type(self.metadata.__dict__) is dict)