9 Commits

6 changed files with 266 additions and 68 deletions

View File

@@ -45,7 +45,7 @@ It also includes :
### Features v1 / v2
| Features | v1-beta | v2-alpha9 |
| Features | v1-beta | v2-alpha10 |
|-----------------------------|----------------|------------|
| Timelapse video support | ✔️ | ✔️ |
| EXIF tags writing | ✔️ | ✔️ |
@@ -55,7 +55,7 @@ It also includes :
| Multilingual TUI 🇺🇳 | 🟡 2 languages | ✔️ |
| Configuration customization | ❌ | 🟡 partial |
| JPEG quality customization | ❌ | 🔄 planned |
| TOML process setting | ❌ | 🔄 planned |
| TOML process setting | ❌ | ✔️ |
## Languages

View File

@@ -47,7 +47,7 @@ Lors de l'export, un sous-dossier nommé selon la vidéo est créé automatiquem
### Fonctionnalités v1 / v2
| Fonctionnalité | v1-beta | v2-alpha9 |
| Fonctionnalité | v1-beta | v2-alpha10 |
|--------------------------------------|--------------|--------------|
| Support des vidéos timelapse | ✔️ | ✔️ |
| Écriture des tags EXIF | ✔️ | ✔️ |
@@ -57,7 +57,7 @@ Lors de l'export, un sous-dossier nommé selon la vidéo est créé automatiquem
| TUI multilingue 🇺🇳 | 🟡 2 langues | ✔️ |
| Personnalisation de la configuration | ❌ | 🟡 partielle |
| Personnalisation qualité JPEG | ❌ | 🔄 planifiée |
| Paramétrage du traitement via TOML | ❌ | 🔄 planifié |
| Paramétrage du traitement via TOML | ❌ | ✔️ |
## Langues

View File

@@ -0,0 +1,32 @@
# Example process setting file for video2geoframes.py
# See documentation for more information
#
# Last edition : 2024-07-01
# Mandatory section
[paths]
video_file = "one/video/file.mp4"
gps_track_file = "one/track_gps/file.gpx"
output_folder = "one/folder"
# Mandatory section
[video]
# For timelapse video, set video.timelapse = [true, x] (x = framerate)
# Otherwise, set video.timelapse = [false, 0]
timelapse = [false, 0]
start_datetime = 2024-06-30T23:32:00.000
rec_timezone = "+02:00"
[process_settings]
# Mandatory setting, but ignored if video.timelapse = [true, x]
frame_sampling = 1
# Optional settings, ignored if set to 0
frame_height = 0
time_offset = +0.0
# Mandatory section
[metadata]
author = "Name or alias"
camera_maker = "One"
camera_model = "Super Cam"

View File

@@ -1,26 +1,40 @@
# Localization file for video2geoframes.py
# English (US / World)
#
# Last edition : 2024-06-23
# Last edition : 2024-07-01
[ui]
[ui.info]
title = "# video2geoframes.py"
intro = """Welcome in video2geoframes.py script !
This script is designed to create geotagged frames from video and GPX track."""
This script is designed to create geotagged frames from video and GPS track."""
end = "End of program, press Enter to quit."
cancel = "Cancelling... empty input on required setting."
paths_title = "## Paths"
parameters_title = "## Process parameters"
tags_title = "## Additional tags"
metadata = """{} ({} {}B)\n
- Duration : {} s\n
- Start time : {}.{}\n
metadata = """{} ({} {}B)
- Duration : {} s
- Start time : {}.{}
- Time offset : {}"""
[ui.units]
[ui.error]
not_implemented = "Sorry, this function is not implemented, work in progress ;)"
file_not_found = "'{}' not found."
invalid_toml_key = "Error... invalid key found in TOML file, please check values."
[ui.unit]
cv2_tqdm = 'frame(s)'
[ui.toml_setting]
incomplete_err = "{} {} missing in TOML setting file."
video_file = "Video file : {}"
gps_track_file = "GPS track file : {}"
timelapse_mode = "{} fps timelapse"
classic_mode = "{} s sampling"
resizing = "Resizing from {}p to {}p"
[ui.parameters]
toml_setting = "Setting with TOML file ({}/{}) ? "
@@ -48,15 +62,16 @@ video_file = "Enter video path : "
gps_track = "Enter GPS track path : "
output_folder = "Enter output folder : "
path_err = "Error... File doesn't exist."
path_err = "Error... file doesn't exist."
[ui.metadatas]
[ui.metadata]
make = "Enter the camera brand : "
model = "Enter the camera model : "
author = "Enter author name : "
[processing]
reading_metadatas = "Reading video metadatas..."
reading_toml_setting = "Reading TOML setting file..."
reading_metadata = "Reading video metadata..."
sampling = "Extracting frames from video..."
timestamping = "Setting timestamp on frames..."
geotagging = "Geotagging frames..."

View File

@@ -1,7 +1,7 @@
# Localization file for video2geoframes.py
# French (France)
#
# Last edition : 2024-06-23
# Last edition : 2024-07-01
[ui]
@@ -10,17 +10,31 @@ title = "# video2geoframes.py"
intro = """Bienvenue dans le script video2geoframes.py !
Ce script permet, à partir d'une vidéo et d'une trace GPS, de créer un ensemble de photos géotaguées."""
end = "Fin du programme, appuyez sur Entrée pour fermer."
cancel = "Annulation... entrée vide sur un paramètre requis."
paths_title = "## Chemins"
tags_title = "## Tags additionnels"
parameters_title = "## Paramètres du traitement"
metadatas = """{} ({} {}B)
metadata = """{} ({} {}B)
- Durée : {} s
- Heure de début : {}.{}
- Décalage horaire : {}"""
[ui.units]
[ui.error]
not_implemented = "Désolé, cette fonction n'est pas encore implémentée, chantier en cours ;)"
file_not_found = "'{}' non-trouvé."
invalid_toml_key = "Erreur... clé invalide trouvée dans le fichier TOML, vérifiez les valeurs."
[ui.unit]
cv2_tqdm = 'image(s)'
[ui.toml_setting]
incomplete_err = "{} {} manquant(s) dans le fichier TOML de paramétrage."
video_file = "Fichier vidéo : {}"
gps_track_file = "Fichier de trace GPS : {}"
timelapse_mode = "Timelapse à {} ips"
classic_mode = "Échantillonnage de {} s"
resizing = "Redimensionnement de {}p vers {}p"
[ui.parameters]
toml_setting = "Paramétrage avec un fichier TOML ({}/{}) ? "
@@ -50,13 +64,14 @@ output_folder = "Entrez le dossier de sortie : "
path_err = "Erreur... le fichier n'existe pas."
[ui.metadatas]
[ui.metadata]
make = "Entrez la marque du capteur : "
model = "Entrez le modèle du capteur : "
author = "Entrez l'auteur : "
[processing]
reading_metadatas = "Lecture des métadonnées de la vidéo..."
reading_toml_setting = "Lecture du fichier TOML de paramétrage..."
reading_metadata = "Lecture des métadonnées de la vidéo..."
sampling = "Extraction des images depuis la vidéo..."
timestamping = "Définition de l'horodatage des images..."
geotagging = "Géotaguage des images..."

View File

@@ -8,7 +8,7 @@ Designed for contribution to street-level imagery projects like Mapillary or Pan
__author__ = "Lucas MATHIEU (@campanu)"
__license__ = "GPL-3.0-or-later"
__version__ = "2.0-alpha9"
__version__ = "2.0-alpha12"
__maintainer__ = "Lucas MATHIEU (@campanu)"
__email__ = "campanu@luc-geo.fr"
@@ -71,6 +71,7 @@ def existing_items(expected_items: list, items: list):
def list_enumerator(item_list: list, intermediate_separator: str, last_separator: str):
i = 1
enumerated_list = []
for it in item_list:
if i == 1:
@@ -85,6 +86,19 @@ def list_enumerator(item_list: list, intermediate_separator: str, last_separator
return enumerated_list
def video_metadata_reader(video_path: str):
video_md = {}
video = cv2.VideoCapture(video_path)
video_md['fps'] = video.get(cv2.CAP_PROP_FPS)
video_md['width'] = video.get(cv2.CAP_PROP_FRAME_WIDTH)
video_md['height'] = video.get(cv2.CAP_PROP_FRAME_HEIGHT)
video_md['frame_number'] = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
video = None
return video_md
# Start
print("# video2geoframes.py (v{})\n".format(__version__))
@@ -95,6 +109,11 @@ conf_file_err = False
# Default values
mandatory_parameters = ['locale', 'exiftool_path']
mandatory_settings1 = ['video_file', 'gps_track_file', 'output_folder']
mandatory_settings2 = ['frame_sampling']
mandatory_settings3 = ['timelapse', 'start_datetime', 'rec_timezone']
optional_settings = ['frame_height', 'time_offset']
minimal_md = ['author', 'camera_maker', 'camera_model']
locale = 'en_us'
min_frame_samp = 0.5
max_frame_samp = 60.0
@@ -135,10 +154,10 @@ try:
print("(!) {} {} missing in configuration file.".format(missing_parameters_list, verb))
raise ValueError
# Configuration assignment
locale = conf_toml['system']['locale']
exiftool_path = unix_path(conf_toml['system']['exiftool_path']).replace('./', '{}/'.format(base_path))
else:
# Configuration assignment
locale = conf_toml['system']['locale']
exiftool_path = unix_path(conf_toml['system']['exiftool_path']).replace('./', '{}/'.format(base_path))
except (FileNotFoundError, ValueError):
print("\nError... configuration file doesn't exists or invalid.")
default_conf = str(input("Use default configuration instead (Y/N) ? ").upper())
@@ -167,28 +186,145 @@ try:
# User input
# TOML setting file
toml_setting = input('\n{}'.format(locale_toml['ui']['parameters']['toml_setting'].format(user_agree, user_disagree)))
i = 0
if toml_setting.upper() == 'O':
if toml_setting.upper() == user_agree:
# TOML setting file path input
while True:
try:
i += 1
toml_file_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['toml_file']))).strip()
toml_setting_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['toml_file']))).strip()
if os.path.exists(toml_file_path):
with codecs.open(toml_file_path, mode='r', encoding='utf-8') as f:
setting_toml = loads(f.read())
f.close()
break
else:
raise FileNotFoundError
except (FileNotFoundError, ValueError):
if os.path.exists(toml_setting_path):
break
elif toml_setting_path == '':
print('{}'.format(locale_toml['ui']['info']['cancel']))
raise InterruptedError
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
# <--coding in progress-->
raise NotImplementedError
# TOML file checking
print("\n{}".format(locale_toml['processing']['reading_toml_setting']))
try:
if os.path.exists(toml_setting_path):
with codecs.open(toml_setting_path, mode='r', encoding='utf-8') as f:
setting_toml = loads(f.read())
f.close()
reading_settings1 = setting_toml['paths'].keys()
reading_settings2 = setting_toml['process_settings'].keys()
reading_settings3 = setting_toml['video'].keys()
reading_metadata = setting_toml['metadata'].keys()
check_settings1 = existing_items(mandatory_settings1, reading_settings1)
check_settings2 = existing_items(mandatory_settings2, reading_settings2)
check_settings3 = existing_items(mandatory_settings3, reading_settings3)
check_metadata = existing_items(minimal_md, reading_metadata)
missing_settings = []
missing_settings.extend(check_settings1['missing'])
missing_settings.extend(check_settings2['missing'])
missing_settings.extend(check_settings3['missing'])
missing_metadata = check_metadata['missing']
if len(missing_settings) > 0:
missing_settings_list = list_enumerator(missing_settings, ', ', ' and ')
if len(missing_settings) == 1:
verb = 'is'
else:
verb = 'are'
print("(!) {}".format(locale_toml['ui']['toml_setting']['incomplete_err'].format(missing_settings_list, verb)))
raise ValueError
else:
video_path = unix_path(setting_toml['paths']['video_file'])
if os.path.exists(video_path):
print('> {}'.format(locale_toml['ui']['toml_setting']['video_file'].format(video_path)))
video_md = video_metadata_reader(video_path)
if video_md['frame_number'] > 0:
video_fps = video_md['fps']
video_width = video_md['width']
video_height = video_md['height']
video_frame_number = video_md['frame_number']
else:
raise ValueError
else:
current_file = video_path
raise FileNotFoundError
gps_track_path = unix_path(setting_toml['paths']['gps_track_file'])
if os.path.exists(gps_track_path):
print('> {}'.format(locale_toml['ui']['toml_setting']['gps_track_file'].format(gps_track_path)))
else:
current_file = gps_track_path
raise FileNotFoundError
output_folder = unix_path(setting_toml['paths']['output_folder'])
timelapse = bool(setting_toml['video']['timelapse'][0])
timelapse_fps = int(setting_toml['video']['timelapse'][1])
if timelapse and min_timelapse_fps <= timelapse_fps <= max_timelapse_fps:
print("> {}".format(locale_toml['ui']['toml_setting']['timelapse_mode'].format(timelapse_fps)))
elif timelapse and (timelapse_fps < min_timelapse_fps or timelapse_fps > max_timelapse_fps):
raise ValueError
else:
frame_sampling = float(setting_toml['process_settings']['frame_sampling'])
if min_frame_samp <= frame_sampling <= max_frame_samp:
print("> {}".format(locale_toml['ui']['toml_setting']['classic_mode'].format(frame_sampling)))
else:
raise ValueError
video_start_datetime_obj = setting_toml['video']['start_datetime']
video_rec_timezone = str(setting_toml['video']['rec_timezone'])
if 'time_offset' in setting_toml['process_settings']:
time_offset = setting_toml['process_settings']['time_offset']
if time_offset != 0:
if min_time_offset <= time_offset <= max_time_offset:
time_offset = float(time_offset)
else:
raise ValueError
else:
time_offset = 0
if video_height <= max_frame_height:
max_frame_height = int(round(video_height, 0))
if 'frame_height' in setting_toml['process_settings']:
frame_height = int(setting_toml['process_settings']['frame_height'])
if min_frame_height <= frame_height <= max_frame_height:
print("> {}".format(locale_toml['ui']['toml_setting']['resizing'].format(video_height, frame_height)))
pass
elif frame_height == 0 or frame_height > max_frame_height:
frame_height = max_frame_height
elif frame_height < min_frame_height:
print("> {}".format(locale_toml['ui']['toml_setting']['resizing'].format(video_height, min_frame_height)))
frame_height = min_frame_height
else:
print("> {}".format(locale_toml['ui']['toml_setting']['resizing'].format(video_height, max_frame_height)))
frame_height = max_frame_height
if len(missing_metadata) > 0:
raise ValueError
else:
author = setting_toml['metadata']['author']
make = setting_toml['metadata']['camera_maker']
model = setting_toml['metadata']['camera_model']
else:
current_file = toml_setting_path
raise FileNotFoundError
except FileNotFoundError:
print('(!) {}'.format(locale_toml['ui']['error']['file_not_found'].format(current_file)))
except ValueError:
print('{}'.format(locale_toml['ui']['error']['invalid_toml_key']))
# Paths
else:
print('\n{}'.format(locale_toml['ui']['info']['paths_title']))
@@ -206,12 +342,12 @@ try:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
# Video metadatas extraction
video = cv2.VideoCapture(video_path)
video_fps = video.get(cv2.CAP_PROP_FPS)
video_width = video.get(cv2.CAP_PROP_FRAME_WIDTH)
video_height = video.get(cv2.CAP_PROP_FRAME_HEIGHT)
video_total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
# Video metadata extraction
video_md = video_metadata_reader(video_path)
video_fps = video_md['fps']
video_width = video_md['width']
video_height = video_md['height']
video_frame_number = video_md['frame_number']
# GPS track file
while True:
@@ -236,13 +372,14 @@ try:
timelapse = input(locale_toml['ui']['parameters']['timelapse'].format(user_agree, user_disagree)).upper()
if timelapse == user_agree:
timelapse = True
# Timelapse framerate parameter
while True:
try:
timelapse_fps = int(input(locale_toml['ui']['parameters']['timelapse_fps'].format(min_timelapse_fps,
max_timelapse_fps)))
if max_timelapse_fps >= timelapse_fps >= min_timelapse_fps:
frame_sampling = float(1 / timelapse_fps)
break
else:
print('\n{}'.format(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps,
@@ -251,7 +388,6 @@ try:
except ValueError:
print('\n{}'.format(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps, max_timelapse_fps)))
True
else:
# Frame sampling parameter
while True:
@@ -319,23 +455,24 @@ try:
# User-defined metadata
print('\n{}'.format(locale_toml['ui']['info']['tags_title']))
make = input(locale_toml['ui']['metadatas']['make'])
model = input(locale_toml['ui']['metadatas']['model'])
author = input(locale_toml['ui']['metadatas']['author'])
make = input(locale_toml['ui']['metadata']['make'])
model = input(locale_toml['ui']['metadata']['model'])
author = input(locale_toml['ui']['metadata']['author'])
# Video metadatas formatting
print('\n{}'.format(locale_toml['processing']['reading_metadatas']))
# Video metadata formatting
print('\n{}'.format(locale_toml['processing']['reading_metadata']))
video = cv2.VideoCapture(video_path)
video_file_name = os.path.basename(video_path)
video_file_size = byte_multiple(os.stat(video_path).st_size)
video_duration = video_total_frames / video_fps
video_duration = video_frame_number / video_fps
video_start_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_offset)
video_start_datetime = video_start_datetime_obj.strftime('%Y-%m-%d %H:%M:%S')
video_start_subsectime = int(int(video_start_datetime_obj.strftime('%f')) / 1000)
# Metadatas recap
print('\n{}'.format(locale_toml['ui']['info']['metadatas'].format(video_file_name,
# metadata recap
print('\n{}'.format(locale_toml['ui']['info']['metadata'].format(video_file_name,
round(video_file_size[0], 3), video_file_size[1],
video_duration, video_start_datetime,
'{:03d}'.format(video_start_subsectime),
@@ -351,24 +488,25 @@ try:
i = 0
if timelapse == user_agree:
frame_interval = frame_sampling / video_fps
if timelapse == True:
frame_sampling = float(1 / video_fps)
frame_interval = float(1 / timelapse_fps)
else:
frame_interval = frame_sampling
cv2_tqdm_unit = locale_toml['ui']['units']['cv2_tqdm']
cv2_tqdm_range = int(video_duration / frame_interval)
cv2_tqdm_unit = locale_toml['ui']['unit']['cv2_tqdm']
cv2_tqdm_range = int(video_duration / frame_sampling)
for i in tqdm(range(cv2_tqdm_range), unit=cv2_tqdm_unit):
t = frame_interval * i * 1000
t = frame_sampling * i * 1000
video.set(cv2.CAP_PROP_POS_MSEC, t)
ret, frame = video.read()
# Image resizing
if frame_height != 0:
resize_factor = video_height / frame_height
if frame_height != video_height:
resize_factor = frame_height / video_height
image_height = frame_height
image_width = int(round(video_height * resize_factor, 0))
image_width = int(round(video_width * resize_factor, 0))
frame = cv2.resize(frame, (image_width, image_height), interpolation=cv2.INTER_LANCZOS4)
@@ -376,10 +514,10 @@ try:
image_name = "{}_f{}.jpg".format(video_file_name.split('.')[0], frame_name)
image_path = "{}/{}".format(output_folder, image_name)
cv2.imwrite(image_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 88, cv2.IMWRITE_JPEG_PROGRESSIVE, 1, cv2.IMWRITE_JPEG_SAMPLING_FACTOR, 0x411111])
cv2.imwrite(image_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 88, cv2.IMWRITE_JPEG_PROGRESSIVE, 1, cv2.IMWRITE_JPEG_SAMPLING_FACTOR, 0x221111])
# Time tags formatting
time_shift = i * frame_sampling
time_shift = i * frame_interval
current_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_shift)
current_datetime = current_datetime_obj.strftime('%Y:%m:%d %H:%M:%S')
current_subsec_time = int(int(current_datetime_obj.strftime('%f')) / 1000)
@@ -397,12 +535,10 @@ try:
exif_tags = {
piexif.ExifIFD.DateTimeOriginal: current_datetime,
piexif.ExifIFD.SubSecTimeOriginal: str(current_subsec_time),
piexif.ExifIFD.OffsetTimeOriginal: video_rec_timezone
}
if current_subsec_time > 0:
exif_tags[piexif.ExifIFD.SubSecTime] = str(current_subsec_time)
image_exif['0th'] = image_tags
image_exif['Exif'] = exif_tags
@@ -421,7 +557,7 @@ try:
# End
input('\n{}'.format(locale_toml['ui']['info']['end']))
except NotImplementedError:
print("\nSorry, this function is not implemented, work in progress ;)")
print('\n{}'.format(locale_toml['ui']['error']['not_implemented']))
except InterruptedError:
input("\nEnd of program, press Enter to quit.")