diff --git a/ledsa/core/ConfigData.py b/ledsa/core/ConfigData.py index 5bda6d3..f23e21a 100644 --- a/ledsa/core/ConfigData.py +++ b/ledsa/core/ConfigData.py @@ -298,6 +298,7 @@ def in_time_diff_to_img_time(self) -> None: If the 'time_ref_img_time' is not set, prompts the user to provide the time shown on the clock in the time reference image. """ import warnings + from ledsa.core.image_handling import format_img_name # local import, image_handling imports ConfigData indirectly has_id = self['DEFAULT']['time_ref_img_id'] != 'None' has_file = self['DEFAULT']['time_ref_img_file'] != 'None' @@ -311,7 +312,7 @@ def in_time_diff_to_img_time(self) -> None: if has_id: generated_path = os.path.abspath(os.path.join( self['DEFAULT']['img_directory'], - self['DEFAULT']['img_name_string'].format(self['DEFAULT']['time_ref_img_id']) + format_img_name(self['DEFAULT']['img_name_string'], self['DEFAULT']['time_ref_img_id']) )) if has_file: @@ -415,8 +416,9 @@ 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( - self['DEFAULT']['first_img_experiment_id'])), 'DateTimeOriginal') + from ledsa.core.image_handling import format_img_name # local import, image_handling imports ConfigData indirectly + exif_entry = get_exif_entry(os.path.join(self['DEFAULT']['img_directory'], format_img_name( + self['DEFAULT']['img_name_string'], self['DEFAULT']['first_img_experiment_id'])), 'DateTimeOriginal') date, time_meta = exif_entry.split(' ') time_img = _get_datetime_from_str(date, time_meta) start_time = time_img - timedelta(seconds=self['DEFAULT'].getint('exif_time_infront_real_time')) 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/AcceptanceTests/LedsaATestLibrary.py b/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py index d616b12..61615f3 100644 --- a/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py +++ b/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py @@ -144,7 +144,7 @@ def create_and_fill_config_for_stacked_cam(self, cam_z, first=1, last=4): time_ref_img_id=None, time_ref_img_time=None, time_diff_to_image_time=0, - img_name_string='test_img_{}.jpg', + img_name_string='test_img_{:d}.jpg', num_img_overflow=None, first_img_experiment_id=first, last_img_experiment_id=last, @@ -286,7 +286,7 @@ def create_and_fill_config(self, first=1, last=4): conf = ConfigData(load_config_file=False, img_directory='test_data/', search_area_radius=10, pixel_value_percentile=99.875, channel=0, max_num_leds=1000, num_arrays=1, num_cores=1, date=None, start_time=None, time_ref_img_id=None, time_ref_img_time=None, time_diff_to_image_time=0, - img_name_string='test_img_{}.jpg', num_img_overflow=None, first_img_experiment_id=first, + img_name_string='test_img_{:d}.jpg', num_img_overflow=None, first_img_experiment_id=first, last_img_experiment_id=last, ref_img_id=1, ignore_led_indices=None, led_array_edge_indices=None, led_array_edge_coordinates=None, first_img_analysis_id=first, last_img_analysis_id=last, num_skip_imgs=0, num_skip_leds=0, @@ -314,7 +314,7 @@ def execute_ledsa_s1(self, use_config): out = self.execute_ledsa('-s1') else: self.execute_ledsa('--config') - inp = b'test_data/\ntest_img_{}.jpg\n1\n12:00:00\n1\n1000\n1\n1\n1' + inp = b'test_data/\ntest_img_{:d}.jpg\n1\n12:00:00\n1\n1000\n1\n1\n1' out = self.execute_ledsa('-s1', inp) check_error_msg(out) return out[0].decode('utf-8')[-9:-6] 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'