From c6fa9b944a1356e336074e9ce7f6198643619c27 Mon Sep 17 00:00:00 2001 From: Kristian Date: Mon, 9 Mar 2026 15:24:54 +0100 Subject: [PATCH 1/3] allow time_ref_image to be set by id or file path --- ledsa/core/ConfigData.py | 89 ++++++++++++++++--- ledsa/data_extraction/init_functions.py | 10 ++- ledsa/demo/demo_setup.py | 2 +- .../AcceptanceTests/LedsaATestLibrary.py | 2 +- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/ledsa/core/ConfigData.py b/ledsa/core/ConfigData.py index 4763d3f..6bdf060 100644 --- a/ledsa/core/ConfigData.py +++ b/ledsa/core/ConfigData.py @@ -13,7 +13,7 @@ class ConfigData(cp.ConfigParser): """ def __init__(self, load_config_file=True, img_directory=None, search_area_radius=10, pixel_value_percentile=99.875, channel=0, - max_num_leds=None, num_arrays=None, num_cores=1, date=None, start_time=None, time_img_id=None, time_ref_img_time=None, + max_num_leds=None, num_arrays=None, num_cores=1, date=None, start_time=None, time_ref_img_id=None, time_ref_img_file=None, time_ref_img_time=None, time_diff_to_image_time=None, img_name_string=None, num_img_overflow=None, first_img_experiment_id=None, last_img_experiment_id=None, ref_img_id=None, ignore_led_indices=None, led_array_edge_indices=None, led_array_edge_coordinates=None, first_img_analysis_id=None, @@ -39,9 +39,11 @@ def __init__(self, load_config_file=True, img_directory=None, search_area_radius :type date: str or None :param start_time: Start time for the experiment. Will be calculated from first image if None. Defaults to None. #TODO: format :type start_time: str or None - :param time_img_id: ID of image with a visible clock used for synchronization. Defaults to None. - :type time_img_id: str or None - :param time_ref_img_time: Time shown on the image with time_img_id. Defaults to None. #TODO: format + :param time_ref_img_id: ID of time reference image with a visible clock used for synchronization. Defaults to None. + :type time_ref_img_id: str or None + :param time_ref_img_id: File path of time reference image with a visible clock used for synchronization. Defaults to None. + :type time_ref_img_id: str or None + :param time_ref_img_time: Time shown on the time reference image with. Defaults to None. #TODO: format :type time_ref_img_time: str or None :param time_diff_to_image_time: Time difference in seconds to the actual image time. Defaults to None. :type time_diff_to_image_time: int or None @@ -95,10 +97,13 @@ def __init__(self, load_config_file=True, img_directory=None, search_area_radius self['DEFAULT'][' date'] = str(date) self.set('DEFAULT', ' # Beginning of the experiment, will be calculated from image with first_img_experiment_id if None.') self['DEFAULT'][' start_time'] = str(start_time) - self.set('DEFAULT', ' # Image name with a clock to calculate the offset of the camera time. Can be None,') - self.set('DEFAULT', ' # if time_diff_to_img_time in seconds is given.') - self['DEFAULT'][' time_img_id'] = str(time_img_id) - self.set('DEFAULT', ' # Time shown on the image with time_img_id') + self.set('DEFAULT', ' # ID of time reference image name with a clock to calculate the offset of the camera time. Can be None,') + self.set('DEFAULT', ' # if time_diff_to_img_time in seconds is given. You might also use time_ref_img_file instead.') + self['DEFAULT'][' time_img_id'] = str(time_ref_img_id) + self.set('DEFAULT', ' # Path of time reference image with a clock to calculate the offset of the camera time. Can be None,') + self.set('DEFAULT', ' # if time_diff_to_img_time in seconds is given. ou might also use time_ref_img_id instead.') + self['DEFAULT'][' time_ref_img_file'] = str(time_ref_img_file) + self.set('DEFAULT', ' # Time shown on the time reference image') self['DEFAULT'][' time_ref_img_time'] = str(time_ref_img_time) self['DEFAULT'][' exif_time_infront_real_time'] = str(time_diff_to_image_time) @@ -146,6 +151,20 @@ def __init__(self, load_config_file=True, img_directory=None, search_area_radius self.write(configfile) print('config.ini created') + def get(self, section, option, *, raw=False, vars=None, fallback='None') -> str: + """ + Retrieve a config value, returning 'None' by default if the key is missing. + + :param section: Section name in the configuration. + :param option: Option name within the section. + :param raw: If True, interpolation is not performed. + :param vars: Additional variables for interpolation. + :param fallback: Value to return if the key is missing. Defaults to 'None'. + :return: The configuration value as a string. + :rtype: str + """ + return super().get(section, option, raw=raw, vars=vars, fallback=fallback) + def load(self) -> None: """ Loads the configuration data from 'config.ini' file. @@ -250,12 +269,20 @@ def in_max_num_leds(self) -> None: self['find_search_areas']['max_num_leds'] = input('Please give the maximum number of LEDs to be detected on the reference ' 'image: ') - def in_time_img_id(self) -> None: # + def in_time_ref_img_id(self) -> None: # """ Prompts the user for the time reference image name and updates the configuration. """ - self['DEFAULT']['time_img_id'] = input('Please give the ID of the time reference image, the image where a clock ' - 'is visible, to synchronise multiple cameras in one experiment: ') + self['DEFAULT']['time_ref_img_id'] = input('Please give the ID of the time reference image, the image where a clock ' + 'is visible, to synchronise multiple cameras in one experiment. Type "None" if you want to set the file path of the time reference image in the next step: ') + + def in_time_ref_img_file(self) -> None: # + """ + Prompts the user for the file path of the time reference image and updates the configuration. + """ + self['DEFAULT']['time_ref_img_file'] = input( + 'Please provide the file path of the time reference image, the image where a clock ' + 'is visible, to synchronise multiple cameras in one experiment: ') def in_num_arrays(self) -> None: """ @@ -268,14 +295,48 @@ def in_time_diff_to_img_time(self) -> None: Update the configuration with the time difference between the reference image's timestamp and the real time. 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 + + has_id = self['DEFAULT']['time_ref_img_id'] != 'None' + has_file = self['DEFAULT']['time_ref_img_file'] != 'None' + + if not has_id and not has_file: + raise ValueError( + "Neither 'time_img_id' nor 'time_ref_img_file' is set. " + "Please provide at least one to identify the time reference image." + ) + + 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']) + )) + + if has_file: + direct_path = os.path.abspath(self['DEFAULT']['time_ref_img_file']) + + if has_id and has_file: + if generated_path != direct_path: + warnings.warn( + f"The path generated from 'time_img_id' ({generated_path}) differs from " + f"'time_ref_img_file' ({direct_path}). Using 'time_ref_img_file'.", + UserWarning + ) + time_ref_img_file_path = direct_path + elif has_file: + time_ref_img_file_path = direct_path + else: + time_ref_img_file_path = generated_path + + print("Time reference image file path: ", time_ref_img_file_path) + if self['DEFAULT']['time_ref_img_time'] == 'None': time = input('Please give the time shown on the clock in the time reference image in hh:mm:ss: ') self['DEFAULT']['time_ref_img_time'] = str(time) time = self['DEFAULT']['time_ref_img_time'] - print(os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['time_img_id'])) + tag = 'DateTimeOriginal' - exif_entry = get_exif_entry(os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'] - .format(self['DEFAULT']['time_img_id'])), tag) + exif_entry = get_exif_entry(time_ref_img_file_path, tag) date, time_meta = exif_entry.split(' ') self['DEFAULT']['date'] = date img_time = _get_datetime_from_str(date, time_meta) diff --git a/ledsa/data_extraction/init_functions.py b/ledsa/data_extraction/init_functions.py index ade2e0c..484b055 100644 --- a/ledsa/data_extraction/init_functions.py +++ b/ledsa/data_extraction/init_functions.py @@ -39,9 +39,15 @@ def request_config_parameters(config: ConfigData) -> None: if config['DEFAULT']['img_name_string'] == 'None': config.in_img_name_string() config.save() - if config['DEFAULT']['time_img_id'] == 'None' and \ + if config.get('DEFAULT', 'time_ref_img_id') == 'None' and \ + config.get('DEFAULT', 'time_ref_img_file') == 'None' and \ config['DEFAULT']['exif_time_infront_real_time'] == 'None': - config.in_time_img_id() + config.in_time_ref_img_id() + config.save() + if config.get('DEFAULT', 'time_ref_img_id')== 'None' and \ + config.get('DEFAULT', 'time_ref_img_file') == 'None' and \ + config['DEFAULT']['exif_time_infront_real_time'] == 'None': + config.in_time_ref_img_file() config.save() if config['DEFAULT']['exif_time_infront_real_time'] == 'None': config.in_time_diff_to_img_time() diff --git a/ledsa/demo/demo_setup.py b/ledsa/demo/demo_setup.py index 7ff9bbe..25b0199 100644 --- a/ledsa/demo/demo_setup.py +++ b/ledsa/demo/demo_setup.py @@ -51,7 +51,7 @@ def _create_config_files(path): num_cores=4, date="27.11.2018", start_time="15:36:07", - time_img_id=None, + time_ref_img_id=None, time_ref_img_time=None, time_diff_to_image_time=-1, ref_img_id=1, diff --git a/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py b/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py index bcea3b6..aafb831 100644 --- a/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py +++ b/ledsa/tests/AcceptanceTests/LedsaATestLibrary.py @@ -103,7 +103,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_img_id=None, time_ref_img_time=None, time_diff_to_image_time=0, + 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, last_img_experiment_id=last, ref_img_id=1, ignore_led_indices=None, led_array_edge_indices=None, led_array_edge_coordinates=None, From 3e5ac885aa23f58961f7dbd925350ce7adb5dffc Mon Sep 17 00:00:00 2001 From: Kristian Date: Mon, 9 Mar 2026 16:24:21 +0100 Subject: [PATCH 2/3] add missing docstrings --- ledsa/core/ConfigData.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ledsa/core/ConfigData.py b/ledsa/core/ConfigData.py index 6bdf060..5bda6d3 100644 --- a/ledsa/core/ConfigData.py +++ b/ledsa/core/ConfigData.py @@ -41,6 +41,8 @@ def __init__(self, load_config_file=True, img_directory=None, search_area_radius :type start_time: str or None :param time_ref_img_id: ID of time reference image with a visible clock used for synchronization. Defaults to None. :type time_ref_img_id: str or None + :param time_ref_img_file: File path of time reference image with a visible clock used for synchronization. Defaults to None. + :type time_ref_img_file: str or None :param time_ref_img_id: File path of time reference image with a visible clock used for synchronization. Defaults to None. :type time_ref_img_id: str or None :param time_ref_img_time: Time shown on the time reference image with. Defaults to None. #TODO: format From cf7bd40ce1c2602962745884a97786f2dd0b3f77 Mon Sep 17 00:00:00 2001 From: Kristian Date: Mon, 9 Mar 2026 16:25:19 +0100 Subject: [PATCH 3/3] update version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a53c394..e499753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"] [project] name = "ledsa" -version = "0.9.7" +version = "0.9.8" description = "A scientific package to analyse smoke via the dimming of light sources." authors = [ {name = "Kristian Börger", email = "boerger@uni-wuppertal.de"},