diff --git a/doc/conf.py b/doc/conf.py index 0c07ed5..8151928 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -40,6 +40,7 @@ "exifread", "rawpy", "opencv-python", + "hachoir" ] diff --git a/ledsa/core/ConfigData.py b/ledsa/core/ConfigData.py index 261806a..e4e4318 100644 --- a/ledsa/core/ConfigData.py +++ b/ledsa/core/ConfigData.py @@ -1,3 +1,4 @@ +import os import configparser as cp from datetime import datetime, timedelta @@ -271,10 +272,15 @@ def in_time_diff_to_img_time(self) -> 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(self['DEFAULT']['img_directory'] + self['DEFAULT']['time_img_id']) - tag = 'EXIF DateTimeOriginal' - exif_entry = get_exif_entry(self['DEFAULT']['img_directory'] + self['DEFAULT']['img_name_string'].format( - self['DEFAULT']['time_img_id']), tag) + print(os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'].format( + self['DEFAULT']['time_img_id']))) + if '.CR3' in os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'].format( + self['DEFAULT']['time_img_id'])): + tag = 'Creation date' + else: + tag = 'EXIF 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) date, time_meta = exif_entry.split(' ') self['DEFAULT']['date'] = date img_time = _get_datetime_from_str(date, time_meta) @@ -351,8 +357,13 @@ def get_start_time(self) -> None: Updates the 'DEFAULT' key with the 'start_time' computed. """ - exif_entry = get_exif_entry(self['DEFAULT']['img_directory'] + self['DEFAULT']['img_name_string'].format( - self['DEFAULT']['first_img_experiment_id']), 'EXIF DateTimeOriginal') + if '.CR3' in os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'].format( + self['DEFAULT']['first_img_experiment_id'])): + tag = 'Creation date' + else: + tag = 'EXIF DateTimeOriginal' + exif_entry = get_exif_entry(os.path.join(self['DEFAULT']['img_directory'], self['DEFAULT']['img_name_string'].format( + self['DEFAULT']['first_img_experiment_id'])), tag) 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')) @@ -366,6 +377,7 @@ def _get_datetime_from_str(date: str, time: str) -> datetime: The function can handle two formats: 1. '%Y:%m:%d %H:%M:%S' - standard format with colons in the date. 2. '%d.%m.%Y %H:%M:%S' - format with periods in the date. + 3. '%d-%m-%Y %H:%M:%S' - format with hyphens in the date. :param date: The date string. :type date: str @@ -376,6 +388,8 @@ def _get_datetime_from_str(date: str, time: str) -> datetime: """ if date.find(":") != -1: date_time = datetime.strptime(date + ' ' + time, '%Y:%m:%d %H:%M:%S') + elif date.find("-") != -1: + date_time = datetime.strptime(date + ' ' + time, '%Y-%m-%d %H:%M:%S') else: date_time = datetime.strptime(date + ' ' + time, '%d.%m.%Y %H:%M:%S') return date_time diff --git a/ledsa/core/image_reading.py b/ledsa/core/image_reading.py index 0878385..2bbbbee 100644 --- a/ledsa/core/image_reading.py +++ b/ledsa/core/image_reading.py @@ -1,5 +1,7 @@ import os - +from hachoir.parser import createParser +from hachoir.metadata import extractMetadata +from pathlib import Path import exifread import numpy as np import rawpy @@ -22,7 +24,7 @@ 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']: + elif extension in ['.CR2','.CR3','.NEF','.ARW','.DNG']: channel_array = _read_channel_data_from_raw_file(filename, channel) return channel_array @@ -91,6 +93,25 @@ def get_exif_entry(filename: str, tag: str) -> str: :rtype: str :raises KeyError: If the EXIF tag is not found in the image metadata. """ + + #Read Metadata CR3 (! ISO, Blende und Verschlusszeit kann durch diese Methode nicht ausgegeben werden) + if '.CR3' in filename: + parser = createParser(str(filename)) + if not parser: + raise ValueError(f"Konnte Datei nicht parsen: {filename}") + + metadata = extractMetadata(parser) + if not metadata: + raise ValueError("Keine Metadaten gefunden.") + + try: + for line in metadata.exportPlaintext(): + if tag in line: + return line.split(":", 1)[1].strip() + except: + print("Keine Aufnahmezeit gefunden.") + exit(1) + with open(filename, 'rb') as f: exif = exifread.process_file(f, details=False, stop_tag=tag) try: diff --git a/ledsa/data_extraction/init_functions.py b/ledsa/data_extraction/init_functions.py index 901a2f2..cd53be5 100644 --- a/ledsa/data_extraction/init_functions.py +++ b/ledsa/data_extraction/init_functions.py @@ -92,7 +92,7 @@ def generate_image_infos_csv(config: ConfigData, build_experiment_infos=False, b _save_analysis_infos(img_data) -def _calc_experiment_and_real_time(build_type: str, config: ConfigData, tag: str, img_number: int) -> None: +def _calc_experiment_and_real_time(build_type: str, config: ConfigData, tag: str, img_number: str) -> None: """ Calculate experiment and real-time based on image metadata and config settings. @@ -107,8 +107,8 @@ def _calc_experiment_and_real_time(build_type: str, config: ConfigData, tag: str :return: Tuple containing experiment time and real time. :rtype: tuple """ - exif_entry = get_exif_entry(config['DEFAULT']['img_directory'] + - config['DEFAULT']['img_name_string'].format(int(img_number)), tag) + exif_entry = get_exif_entry(os.path.join(config['DEFAULT']['img_directory'], + config['DEFAULT']['img_name_string'].format(img_number)), tag) date, time_meta = exif_entry.split(' ') date_time_img = _get_datetime_from_str(date, time_meta) @@ -125,7 +125,7 @@ def _get_datetime_from_str(date: str, time: str) -> datetime: """ Convert date and time strings to a datetime object. - :param date: Date string in the format '%Y:%m:%d' or '%d.%m.%Y'. + :param date: Date string in the format '%Y:%m:%d' or '%d.%m.%Y' or '%Y-%m-%d'. :type date: str :param time: Time string in the format '%H:%M:%S'. :type time: str @@ -134,6 +134,8 @@ def _get_datetime_from_str(date: str, time: str) -> datetime: """ if date.find(":") != -1: date_time = datetime.strptime(date + ' ' + time, '%Y:%m:%d %H:%M:%S') + elif date.find("-") != -1: + date_time = datetime.strptime(date + ' ' + time, '%Y-%m-%d %H:%M:%S') else: date_time = datetime.strptime(date + ' ' + time, '%d.%m.%Y %H:%M:%S') return date_time @@ -193,9 +195,12 @@ def _build_img_data_string(build_type: str, config: ConfigData) -> str: img_increment = config.getint(build_type, 'num_skip_imgs') + 1 if build_type == 'analyse_photo' else 1 img_id_list = _find_img_number_list(first_img_id, last_img_id, img_increment) for img_id in img_id_list: - tag = 'EXIF DateTimeOriginal' + if '.CR3' in config['DEFAULT']['img_name_string']: + tag = 'Creation date' + else: + tag = 'EXIF 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) + ',' + config[build_type]['img_name_string'].format(img_id) + ',' + time.strftime('%H:%M:%S') + ',' + str(experiment_time) + '\n') img_idx += 1 return img_data diff --git a/ledsa/data_extraction/step_3_functions.py b/ledsa/data_extraction/step_3_functions.py index acbfac1..607613f 100644 --- a/ledsa/data_extraction/step_3_functions.py +++ b/ledsa/data_extraction/step_3_functions.py @@ -58,7 +58,7 @@ def generate_analysis_data(img_filename: str, channel: int, search_areas: np.nda return img_analysis_data -def create_fit_result_file(img_data: List[LEDAnalysisData], img_id: int, channel: int) -> None: # TODO: rename because misleading +def create_fit_result_file(img_data: List[LEDAnalysisData], img_id: str, channel: int) -> None: # TODO: rename because misleading """ Create a result file for a single image, containing the pixel values and, if applicable, the fit results of all LEDs. diff --git a/ledsa/tools/exposure_checker.ipynb b/ledsa/tools/exposure_checker.ipynb index 21e14c5..427ed3c 100644 --- a/ledsa/tools/exposure_checker.ipynb +++ b/ledsa/tools/exposure_checker.ipynb @@ -1,43 +1,50 @@ { "cells": [ { - "metadata": {}, "cell_type": "markdown", + "id": "d5b8be1e27d27315", + "metadata": {}, "source": [ "# Image Exposure Checker\n", "\n", - "This notebook allows you to check the exposure of a series of pictures. It calculates the pixel saturation value as a percentage.\n" - ], - "id": "d5b8be1e27d27315" + "This notebook allows you to check the exposure of a series of pictures. It calculates the pixel saturation value as a percentage.\n", + "\n", + "Does not work for Images in CR3 format.\n" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "# Dependencies\n", - "id": "9df8a44a552be70" + "id": "9df8a44a552be70", + "metadata": {}, + "source": [ + "# Dependencies\n" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "41c98f47ba1f266f", + "metadata": {}, + "outputs": [], "source": [ "import os\n", "from ledsa.core.image_reading import read_channel_data_from_img, get_exif_entry" - ], - "id": "41c98f47ba1f266f" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "# Configuration\n", - "id": "8bab111a65dd1b89" + "id": "8bab111a65dd1b89", + "metadata": {}, + "source": [ + "# Configuration\n" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "aa799b526e7eb076", + "metadata": {}, + "outputs": [], "source": [ "# Configure the image path and range\n", "image_dir = \"/path/to/your/images\" # Update this to your image directory\n", @@ -45,20 +52,22 @@ "image_range = range(1, 10) # Range of image numbers to process\n", "channel = 0 # Color channel to analyze (0=Red, 1=Green, 2=Blue)\n", "saturation = 255" - ], - "id": "aa799b526e7eb076" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "# Process Images\n", - "id": "9b3295c6d521431e" + "id": "9b3295c6d521431e", + "metadata": {}, + "source": [ + "# Process Images\n" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "344c5f7ce039571b", + "metadata": {}, + "outputs": [], "source": [ "# Process each image in the range\n", "for img_id in image_range:\n", @@ -89,11 +98,14 @@ "\n", " \n", "\n" - ], - "id": "344c5f7ce039571b" + ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/ledsa/tools/photo_renamer.py b/ledsa/tools/photo_renamer.py index 343b4ed..e470e83 100644 --- a/ledsa/tools/photo_renamer.py +++ b/ledsa/tools/photo_renamer.py @@ -6,6 +6,7 @@ import pandas as pd import exifread +#does not work for Images in CR3 format def set_working_dir(): """ diff --git a/ledsa/tools/photo_renamer_cr3.py b/ledsa/tools/photo_renamer_cr3.py new file mode 100644 index 0000000..d789900 --- /dev/null +++ b/ledsa/tools/photo_renamer_cr3.py @@ -0,0 +1,137 @@ +import glob +import os +from datetime import datetime +from os import path +import csv +import pandas as pd +from ledsa.core.image_reading import get_exif_entry + + +def set_working_dir(): + """ + Prompts user for directory path and changes to that directory if it exists. + Exits program if directory is invalid. + """ + working_dir = input("Set Working directory:") + if path.exists(working_dir): + os.chdir(working_dir) + print("Working directory is set to \"{0}\"".format(os.getcwd())) + else: + print("\"{0}\" does not exist!".format(os.getcwd())) + exit() + + +def get_files(): + """ + Prompts user for image and raw file types to process. + Extracts capture date from EXIF data of images. + Finds corresponding raw files if they exist. + + Returns: + pd.DataFrame: DataFrame containing image filenames as index and columns for + capture date and associated raw filenames, sorted by capture date. + """ + # Get file type inputs from user + image_types = input("What image types do you want to take into account? (Seperate by \",\"):") + image_types = [x.strip() for x in image_types.split(',')] + image_dict = {} + image_files = [] + + # Find all matching image files + for file_type in image_types: + image_files.extend(glob.glob('*.{}'.format(file_type))) + + # Process each image file + for image in image_files: + # Extract EXIF datetime data + with open(image, 'rb') as image_file: + if '.CR3' in image: + tag_datetime = 'Creation date' + else: + tag_datetime = 'EXIF DateTimeOriginal' + capture_date = get_exif_entry(image,tag_datetime) + # Fall back to second precision + if '-' in capture_date: + datetime_object = datetime.strptime(capture_date, '%Y-%m-%d %H:%M:%S') + else: + datetime_object = datetime.strptime(capture_date, '%Y:%m:%d %H:%M:%S') + image_file.close() + image_dict[image] = [datetime_object] + + # Create and sort DataFrame + image_df = pd.DataFrame.from_dict(image_dict, columns=["capture_date"], orient='index') + image_df = image_df.sort_values(by=['capture_date'], ascending=True) + return image_df + + +def rename_images_by_date(image_df): + """ + Rename image and raw files based on capture date and user-provided name. + + Creates a log file of all renames. After previewing changes, user can choose + to proceed with renaming or cancel. User can also choose to keep or delete + the log file afterwards. + + Args: + image_df (pd.DataFrame): DataFrame containing image metadata and filenames + to be processed. + """ + # Get base name from user and setup logging + name = input("Please enter name for image:") + print("Files are renamed as follows:") + count = 1 + now = datetime.now() + now_str = now.strftime("%Y_%m_%d_%H_%M_%S") + filename = f"{now_str}_{name}_rename_log.csv" + + # Preview rename changes and write to log + with open(filename, 'w') as log_file: + writer = csv.writer(log_file) + writer.writerow(("old_image_name", "new_image_name")) + for index, row in image_df.iterrows(): + image_year = row['capture_date'].strftime('%y%m%d') + + # Generate new filenames + new_image_name = "{0}_{1}.{2}".format(name, f'{count:04d}',index.split('.')[-1]) + print("{0} --> {1}".format(index, new_image_name)) + writer.writerow((index, new_image_name)) + count += 1 + + # Process user choice to proceed or cancel + while True: + continue_rename = input("Do you want to continue? yes[y] or no[n]") + if continue_rename == "y": + # Perform the actual renaming + count = 1 + for index, row in image_df.iterrows(): + image_year = row['capture_date'].strftime('%y%m%d') + new_image_name = "{0}_{1}.{2}".format(name, f'{count:04d}',index.split('.')[-1]) + + os.rename(index, new_image_name) + count += 1 + print("Files successfully renamed!".format(count)) + + # Handle log file retention + keep_log = input("Do you want to keep the changelog? yes[y] or no[n]") + if keep_log == 'y': + print(f"{filename} saved!") + exit() + elif keep_log == 'n': + os.remove(filename) + print("Changelog deleted!") + exit() + elif continue_rename == "n": + print("No files were renamed!") + exit() + else: + continue + + +def main(): + set_working_dir() + image_df = get_files() + rename_images_by_date(image_df) + + +if __name__ == '__main__': + main()