diff --git a/ledsa/core/ConfigData.py b/ledsa/core/ConfigData.py index 5bda6d3..27f863f 100644 --- a/ledsa/core/ConfigData.py +++ b/ledsa/core/ConfigData.py @@ -415,7 +415,7 @@ def get_start_time(self) -> None: Updates the 'DEFAULT' key with the 'start_time' computed. """ - exif_entry = get_exif_entry(os.path.join(self['DEFAULT']['img_directory'] + self['DEFAULT']['img_name_string'].format( + exif_entry = get_exif_entry(os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'].format( self['DEFAULT']['first_img_experiment_id'])), 'DateTimeOriginal') date, time_meta = exif_entry.split(' ') time_img = _get_datetime_from_str(date, time_meta) diff --git a/ledsa/core/image_handling.py b/ledsa/core/image_handling.py index 86285ac..6342da2 100644 --- a/ledsa/core/image_handling.py +++ b/ledsa/core/image_handling.py @@ -3,6 +3,26 @@ from ledsa.core.file_handling import read_table +def format_img_name(img_name_string: str, img_id) -> str: + """ + Build an image file name from the config template and an image ID. + + Plain ``{}`` placeholders keep the ID as given, so zero-padded IDs such as + '0001' (e.g. DSC_0001.NEF) are preserved. Numeric format specifications + such as ``{:04d}`` require an integer, for which the ID is cast. + + :param img_name_string: Name template from the config, e.g. 'DSC_{}.CR3'. + :type img_name_string: str + :param img_id: The image ID, as string or integer. + :return: The formatted image file name. + :rtype: str + """ + try: + return img_name_string.format(img_id) + except ValueError: + return img_name_string.format(int(img_id)) + + def get_img_name(img_id: str) -> str: """ Retrieves the image path corresponding to a given image ID. diff --git a/ledsa/core/image_reading.py b/ledsa/core/image_reading.py index f7cc8f9..35a0712 100644 --- a/ledsa/core/image_reading.py +++ b/ledsa/core/image_reading.py @@ -22,8 +22,10 @@ def read_channel_data_from_img(filename: str, channel: int) -> np.ndarray: extension = os.path.splitext(filename)[-1] if extension in ['.JPG', '.JPEG', '.jpg', '.jpeg', '.PNG', '.png']: channel_array = _read_channel_data_from_img_file(filename, channel) - elif extension in ['.CR2', '.CR3']: + elif extension in ['.CR2', '.CR3', '.NEF', '.ARW', '.DNG']: channel_array = _read_channel_data_from_raw_file(filename, channel) + else: + raise ValueError(f"Unsupported image format '{extension}' of file {filename}.") return channel_array def read_img_array_from_img(filename: str, channel: int) -> np.ndarray: @@ -41,8 +43,10 @@ def read_img_array_from_img(filename: str, channel: int) -> np.ndarray: extension = os.path.splitext(filename)[-1] if extension in ['.JPG', '.JPEG', '.jpg', '.jpeg', '.PNG', '.png']: img_array = _read_grayscale_img_array_from_img_file(filename) - elif extension in ['.CR2', '.CR3']: + elif extension in ['.CR2', '.CR3', '.NEF', '.ARW', '.DNG']: img_array, _ = _read_img_array_from_raw_file(filename, channel) + else: + raise ValueError(f"Unsupported image format '{extension}' of file {filename}.") return img_array diff --git a/ledsa/data_extraction/DataExtractor.py b/ledsa/data_extraction/DataExtractor.py index 47978b5..7fc5c63 100644 --- a/ledsa/data_extraction/DataExtractor.py +++ b/ledsa/data_extraction/DataExtractor.py @@ -93,7 +93,7 @@ def find_search_areas(self) -> None: Identify all LEDs in the reference image and define the areas where LEDs will be searched in the experiment images. """ config = self.config['find_search_areas'] - in_file_path = os.path.join(config['img_directory'], config['img_name_string'].format(int(config['ref_img_id']))) + in_file_path = os.path.join(config['img_directory'], ledsa.core.image_handling.format_img_name(config['img_name_string'], config['ref_img_id'])) channel = config['channel'] search_area_radius = int(config['search_area_radius']) max_num_leds = int(config['max_num_leds']) @@ -126,7 +126,7 @@ def plot_search_areas(self, reorder_leds=False) -> None: if self.search_areas is None: self.load_search_areas() - in_file_path = os.path.join(config['img_directory'], config['img_name_string'].format(int(config['ref_img_id']))) + in_file_path = os.path.join(config['img_directory'], ledsa.core.image_handling.format_img_name(config['img_name_string'], config['ref_img_id'])) # TODO this currently only works for RAW files but should work for JPG files as well data = ledsa.core.image_reading.read_img_array_from_img(in_file_path, channel=0) search_area_radius = int(config['search_area_radius']) diff --git a/ledsa/data_extraction/init_functions.py b/ledsa/data_extraction/init_functions.py index 484b055..da22341 100644 --- a/ledsa/data_extraction/init_functions.py +++ b/ledsa/data_extraction/init_functions.py @@ -3,6 +3,7 @@ from typing import List from ledsa.core.ConfigData import ConfigData +from ledsa.core.image_handling import format_img_name from ledsa.core.image_reading import get_exif_entry @@ -114,7 +115,7 @@ def _calc_experiment_and_real_time(build_type: str, config: ConfigData, tag: str :rtype: tuple """ exif_entry = get_exif_entry(os.path.join(config['DEFAULT']['img_directory'], - config['DEFAULT']['img_name_string'].format(int(img_number))), tag) + format_img_name(config['DEFAULT']['img_name_string'], img_number)), tag) date, time_meta = exif_entry.split(' ') date_time_img = _get_datetime_from_str(date, time_meta) @@ -201,7 +202,7 @@ def _build_img_data_string(build_type: str, config: ConfigData) -> str: for img_id in img_id_list: tag = 'DateTimeOriginal' experiment_time, time = _calc_experiment_and_real_time(build_type, config, tag, img_id) - img_data += (str(img_idx) + ',' + config[build_type]['img_name_string'].format(int(img_id)) + + img_data += (str(img_idx) + ',' + format_img_name(config[build_type]['img_name_string'], img_id) + ',' + time.strftime('%H:%M:%S') + ',' + str(experiment_time) + '\n') img_idx += 1 return img_data diff --git a/ledsa/tests/UnitTests/__init__.py b/ledsa/tests/UnitTests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ledsa/tests/UnitTests/test_raw_format_compat.py b/ledsa/tests/UnitTests/test_raw_format_compat.py new file mode 100644 index 0000000..02e9b62 --- /dev/null +++ b/ledsa/tests/UnitTests/test_raw_format_compat.py @@ -0,0 +1,41 @@ +import numpy as np +import pytest + +import ledsa.core.image_reading as image_reading +from ledsa.core.image_handling import format_img_name + + +class TestRawExtensionRouting: + @pytest.fixture + def raw_reader_stub(self, monkeypatch): + calls = [] + + def stub(filename, channel): + calls.append(filename) + return np.zeros((4, 4)) + + monkeypatch.setattr(image_reading, '_read_channel_data_from_raw_file', stub) + return calls + + @pytest.mark.parametrize('extension', ['.CR2', '.CR3', '.NEF', '.ARW', '.DNG']) + def test_raw_formats_are_routed_to_raw_reader(self, raw_reader_stub, extension): + image_reading.read_channel_data_from_img(f'img_0001{extension}', channel=0) + assert raw_reader_stub == [f'img_0001{extension}'] + + def test_unsupported_format_raises(self): + with pytest.raises(ValueError, match='Unsupported image format'): + image_reading.read_channel_data_from_img('img_0001.xyz', channel=0) + + +class TestFormatImgName: + def test_plain_placeholder_preserves_leading_zeros(self): + assert format_img_name('DSC_{}.NEF', '0001') == 'DSC_0001.NEF' + + def test_plain_placeholder_with_int_id(self): + assert format_img_name('test_img_{}.jpg', 7) == 'test_img_7.jpg' + + def test_numeric_format_spec_with_string_id(self): + assert format_img_name('IMG_{:04d}.CR2', '7') == 'IMG_0007.CR2' + + def test_numeric_format_spec_with_int_id(self): + assert format_img_name('IMG_{:04d}.CR2', 7) == 'IMG_0007.CR2'