From 16288909b991e7097e6fbde041c315c16f1b21f3 Mon Sep 17 00:00:00 2001 From: Ruben Verweij Date: Wed, 19 Apr 2017 12:09:27 +0200 Subject: [PATCH] Issue #2: clean up axes handling, guess a better default axis --- nd2reader/exceptions.py | 7 +++++++ nd2reader/raw_metadata.py | 23 +++++++++++++++++---- nd2reader/reader.py | 43 +++++++++++++++++++++++++++++++-------- tests/test_legacy.py | 27 ------------------------ tests/test_reader.py | 33 +----------------------------- 5 files changed, 62 insertions(+), 71 deletions(-) delete mode 100644 tests/test_legacy.py diff --git a/nd2reader/exceptions.py b/nd2reader/exceptions.py index 28ed71e..55c598b 100644 --- a/nd2reader/exceptions.py +++ b/nd2reader/exceptions.py @@ -15,3 +15,10 @@ class NoImageError(Exception): """ pass + + +class EmptyFileError(Exception): + """This .nd2 file seems to be empty. + + Raised if no axes are found in the file. + """ diff --git a/nd2reader/raw_metadata.py b/nd2reader/raw_metadata.py index 888bb8a..d729135 100644 --- a/nd2reader/raw_metadata.py +++ b/nd2reader/raw_metadata.py @@ -39,13 +39,15 @@ class RawMetadata(object): "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), "fields_of_view": self._parse_fields_of_view(), - "frames": np.arange(0, frames_per_channel, 1), + "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), } + self._set_default_if_not_empty('fields_of_view') + self._set_default_if_not_empty('frames') self._metadata_parsed['num_frames'] = len(self._metadata_parsed['frames']) self._parse_roi_metadata() @@ -53,6 +55,11 @@ class RawMetadata(object): return self._metadata_parsed + def _set_default_if_not_empty(self, entry): + if len(self._metadata_parsed[entry]) == 0 and self._metadata_parsed['total_images_per_channel'] > 0: + # 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: @@ -74,6 +81,14 @@ class RawMetadata(object): def _parse_calibration(self): return self.image_calibration.get(six.b('SLxCalibration'), {}).get(six.b('dCalibration')) + def _parse_frames(self): + """The number of cycles. + + Returns: + list: list of all the frame numbers + """ + return self._parse_dimension(r""".*?T'?\((\d+)\).*?""") + def _parse_channels(self): """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.) @@ -154,12 +169,12 @@ class RawMetadata(object): def _parse_dimension(self, pattern): dimension_text = self._parse_dimension_text() if dimension_text is None: - return [0] + return [] if six.PY3: dimension_text = dimension_text.decode("utf8") match = re.match(pattern, dimension_text) if not match: - return [0] + return [] count = int(match.group(1)) return list(range(count)) @@ -170,7 +185,7 @@ class RawMetadata(object): """ if self.image_attributes is None: - return None + return 0 return self.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')] def _parse_roi_metadata(self): diff --git a/nd2reader/reader.py b/nd2reader/reader.py index 1a9e0c8..95b00cf 100644 --- a/nd2reader/reader.py +++ b/nd2reader/reader.py @@ -1,4 +1,6 @@ from pims import FramesSequenceND, Frame + +from nd2reader.exceptions import EmptyFileError from nd2reader.parser import Parser import numpy as np @@ -99,17 +101,42 @@ class ND2Reader(FramesSequenceND): """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_if_exists('x', self._get_metadata_property("width", default=0)) + self._init_axis_if_exists('y', self._get_metadata_property("height", default=0)) + self._init_axis_if_exists('c', len(self._get_metadata_property("channels", default=[]))) + self._init_axis_if_exists('t', len(self._get_metadata_property("frames", default=[]))) + self._init_axis_if_exists('z', len(self._get_metadata_property("z_levels", default=[]))) - z_levels = len(self._get_metadata_property("z_levels", default=[])) - if z_levels > 1: - self._init_axis('z', z_levels) + if len(self.sizes) == 0: + raise EmptyFileError("No axes were found for this .nd2 file.") # provide the default - self.iter_axes = 't' + self.iter_axes = self._guess_default_iter_axis() + + def _init_axis_if_exists(self, axis, size): + if size > 0: + self._init_axis(axis, size) + + def _guess_default_iter_axis(self): + """ + Guesses the default axis to iterate over based on axis sizes. + Returns: + the axis to iterate over + """ + priority = ['t', 'z', 'c'] + found_axes = [] + for axis in priority: + try: + current_size = self.sizes[axis] + except KeyError: + continue + + if current_size > 1: + return axis + + found_axes.append(axis) + + return found_axes[0] def get_timesteps(self): """Get the timesteps of the experiment diff --git a/tests/test_legacy.py b/tests/test_legacy.py deleted file mode 100644 index 6796b77..0000000 --- a/tests/test_legacy.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest -from os import path - -from nd2reader.artificial import ArtificialND2 -from nd2reader.common import check_or_make_dir -from nd2reader.legacy import Nd2 - - -class TestLegacy(unittest.TestCase): - def create_test_nd2(self): - with ArtificialND2(self.test_file) as artificial: - artificial.close() - - def setUp(self): - dir_path = path.dirname(path.realpath(__file__)) - check_or_make_dir(path.join(dir_path, 'test_data/')) - self.test_file = path.join(dir_path, 'test_data/test.nd2') - - def test_can_open_test_file(self): - self.create_test_nd2() - with Nd2(self.test_file) as reader: - self.assertEqual(reader.width, 0) - self.assertEqual(reader.height, 0) - self.assertEqual(len(reader.z_levels), 1) - self.assertEqual(len(reader.channels), 0) - self.assertEqual(len(reader.frames), 0) - diff --git a/tests/test_reader.py b/tests/test_reader.py index 0430eb9..71b4033 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -4,42 +4,11 @@ import numpy as np from nd2reader.artificial import ArtificialND2 from nd2reader.common import check_or_make_dir +from nd2reader.exceptions import EmptyFileError from nd2reader.parser import Parser from nd2reader.reader import ND2Reader class TestReader(unittest.TestCase): - def create_test_nd2(self): - with ArtificialND2(self.test_file) as artificial: - artificial.close() - - def setUp(self): - dir_path = path.dirname(path.realpath(__file__)) - check_or_make_dir(path.join(dir_path, 'test_data/')) - self.test_file = path.join(dir_path, 'test_data/test.nd2') - - def test_can_open_test_file(self): - self.create_test_nd2() - with ND2Reader(self.test_file) as reader: - self.assertEqual(reader.filename, self.test_file) - self.assertEqual(reader.pixel_type, np.float64) - self.assertEqual(reader.sizes['x'], 0) - self.assertEqual(reader.sizes['y'], 0) - self.assertFalse('z' in reader.sizes) - self.assertEqual(reader.sizes['c'], 0) - self.assertEqual(reader.sizes['t'], 0) - def test_extension(self): self.assertTrue('nd2' in ND2Reader.class_exts()) - - def test_get_metadata_property(self): - self.create_test_nd2() - with ND2Reader(self.test_file) as reader: - self.assertIsNone(reader._get_metadata_property('kljdf')) - self.assertEqual(reader._get_metadata_property('kljdf', 'default'), 'default') - - def test_get_parser(self): - self.create_test_nd2() - with ND2Reader(self.test_file) as reader: - self.assertTrue(type(reader.parser) is Parser) -