365 lines
14 KiB
Python
365 lines
14 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""video2geoframes.py
|
|
Python script to generate a collection of geotagged images from a video and a GPS track.
|
|
Designed for contribution to street-level imagery projects like Mapillary or Panoramax.
|
|
"""
|
|
|
|
__author__ = "Lucas MATHIEU (@campanu)"
|
|
__license__ = "AGPL-3.0-or-later"
|
|
__version__ = "1.0-beta"
|
|
__maintainer__ = "Lucas MATHIEU (@campanu)"
|
|
__email__ = "campanu@luc-geo.fr"
|
|
|
|
import os
|
|
import glob
|
|
import platform
|
|
from datetime import datetime, timedelta
|
|
import exiftool as et
|
|
#import tqdm
|
|
|
|
# Functions
|
|
def unix_path(path):
|
|
if '\\' in path:
|
|
path = path.replace('\\', '/')
|
|
|
|
return path
|
|
|
|
|
|
def existing_path(folder_path):
|
|
if not os.path.exists(folder_path):
|
|
os.makedirs(folder_path)
|
|
|
|
|
|
def byte_multiple(size):
|
|
multiples = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']
|
|
i = -1
|
|
|
|
while size >= 1024 and i < 9:
|
|
i += 1
|
|
size = size / 1024
|
|
|
|
multiple = multiples[i]
|
|
|
|
return size, multiple
|
|
|
|
|
|
# Start
|
|
print('# video2geoframes.py')
|
|
|
|
# Configuration settings
|
|
base_path = unix_path(os.path.dirname(__file__))
|
|
ini_file_path = '{}/video2geoframes.ini'.format(base_path)
|
|
ini_file_err = False
|
|
|
|
## Default values
|
|
ui_language = 'en'
|
|
max_frame_samp = float(60)
|
|
|
|
if platform.system() == 'Windows':
|
|
ffmpeg_path = '{}/dependencies/ffmpeg-essentials/bin/ffmpeg.exe'.format(base_path)
|
|
exiftool_path = '{}/dependencies/exiftool.exe'.format(base_path)
|
|
else:
|
|
ffmpeg_path = 'ffmpeg'
|
|
exiftool_path = 'exiftool'
|
|
|
|
## ini file reading
|
|
if os.path.exists(ini_file_path):
|
|
configuration = {}
|
|
|
|
try:
|
|
with open(ini_file_path, 'r') as file:
|
|
for line in file:
|
|
if line[0] == '#':
|
|
continue
|
|
else:
|
|
(key, value) = line.split()
|
|
configuration[key] = value.replace('"', '')
|
|
|
|
ui_language = configuration.get('ui_language')
|
|
max_frame_samp = float(configuration.get('max_frame_sample'))
|
|
ffmpeg_path = configuration.get('ffmpeg_path').replace('./', '{}/'.format(base_path))
|
|
exiftool_path = configuration.get('exiftool_path').replace('./', '{}/'.format(base_path))
|
|
except:
|
|
print('\nError... not readable or incomplete ini file. Default configuration will be used.')
|
|
|
|
# UI language
|
|
ui_text_dict = {
|
|
'fr':
|
|
{
|
|
'ui_intro': 'Bienvenue dans le script video2geoframes.py !\n'
|
|
'Ce script permet de créer à partir d\'une vidéo et d\'une trace GPX un ensemble de photos'
|
|
' géotaguées.',
|
|
'ui_end': 'Fin du programme, appuyez sur Entrée pour fermer.',
|
|
'ui_paths_title': '## Chemins',
|
|
'ui_parameters_title': '## Paramètres du traitement',
|
|
'ui_timelapse': 'Vidéo timelapse (O/N) ? ',
|
|
'ui_timelapse_fps': 'Débit d\'image du timelapse (image/s) : ',
|
|
'ui_timelapse_fps_err': 'Débit d\'image du timelapse (image/s) : ',
|
|
'ui_frame_samp': 'Entrez l\'espacement temporel des images en secondes [{}-{}] : ',
|
|
'ui_frame_samp_err': 'Erreur... veuillez entrer un nombre décimal.',
|
|
'ui_frame_height': 'Entrez la hauteur des images en pixels (ratio inchangé) [{}-{}] : ',
|
|
'ui_frame_height_err': 'Erreur... veuillez entrer un nombre entier entre {} et {}.',
|
|
'ui_video_start_datetime': 'Entrez l\'horodatage du début de la vidéo au format ISO'
|
|
' (exemple : 2023-09-18T22:00:02.000) : ',
|
|
'ui_video_start_datetime_err': 'Erreur... la l\'horodatage entré est invalide.',
|
|
'ui_rec_timezone': 'Entrez le décalage horaire par rapport à UTC (exemple pour CEST : +02:00) : ',
|
|
'ui_time_offset': 'Entrez le décalage temporel entre la vidéo et le GPX en secondes [{}-{}] : ',
|
|
'ui_time_offset_err': 'Erreur... veuillez entrer un nombre décimal entre {} et {}.',
|
|
'ui_video_path': 'Entrez le chemin de la vidéo : ',
|
|
'ui_gpx_path': 'Entrez le chemin de la trace GPX : ',
|
|
'ui_output': 'Entrez le dossier de sortie : ',
|
|
'ui_path_err': 'Erreur... le fichier n\'existe pas.',
|
|
'ui_make': 'Entrez la marque du capteur : ',
|
|
'ui_model': 'Entrez le modèle du capteur : ',
|
|
'ui_author': 'Entrez l\'auteur : ',
|
|
'ui_metadata': '{} ({} {}B)\n'
|
|
'- Durée : {} s\n'
|
|
'- Heure de début : {}.{}\n'
|
|
'- Décalage horaire : {}',
|
|
'process_reading_metadata': 'Lecture des métadonnées de la vidéo...',
|
|
'process_sampling': 'Extraction des images depuis la vidéo...',
|
|
'process_timestamping': 'Définition de l\'horodatage des images...',
|
|
'process_geotagging': 'Géotaguage des images...',
|
|
'agree': 'O'
|
|
},
|
|
|
|
'en':
|
|
{
|
|
'ui_intro': 'Welcome in video2geoframes.py script !\n'
|
|
'This script is designed to create geotagged frames from video and GPX track.',
|
|
'ui_end': 'End of program, press Enter to quit.',
|
|
'ui_paths_title': '## Paths',
|
|
'ui_parameters_title': '## Process parameters',
|
|
'ui_timelapse': 'Timelapse video (Y/N) ? ',
|
|
'ui_timelapse_fps': 'Timelapse framerate (frame/s) : ',
|
|
'ui_timelapse_fps_err': 'Error... please enter a decimal.',
|
|
'ui_frame_samp': 'Enter the frame sampling in seconds [{}-{}] : ',
|
|
'ui_frame_samp_err': 'Error... please enter a decimal between {} and {}.',
|
|
'ui_frame_height': 'Enter frame height in pixels (ratio unchanged) [{}-{}] : ',
|
|
'ui_frame_height_err': 'Error... please enter an integer between {} and {}.',
|
|
'ui_video_start_datetime': 'Enter video start datetime following ISO format'
|
|
' (exemple : 2023-09-18T22:00:02.000) : ',
|
|
'ui_video_start_datetime_err': 'Error... entered datetime is not valid.',
|
|
'ui_rec_timezone': 'Enter time offset related to UTC (example for CEST : +02:00) : ',
|
|
'ui_time_offset': 'Enter time offset between video and GPX in seconds [{}-{}] : ',
|
|
'ui_time_offset_err': 'Error... please enter a decimal between {} and {}.',
|
|
'ui_video_path': 'Enter video path : ',
|
|
'ui_gpx_path': 'Enter GPX track path : ',
|
|
'ui_output': 'Enter output folder : ',
|
|
'ui_path_err': 'Error... File doesn\'t exist.',
|
|
'ui_make': 'Enter the camera brand : ',
|
|
'ui_model': 'Enter the camera model : ',
|
|
'ui_author': 'Enter author name : ',
|
|
'ui_metadata': '{} ({} {}B)\n'
|
|
'- Duration : {} s\n'
|
|
'- Start time : {}.{}\n'
|
|
'- Time offset : {}',
|
|
'process_reading_metadata': 'Reading video metadata...',
|
|
'process_sampling': 'Extracting frames from video...',
|
|
'process_timestamping': 'Setting timestamp on frames...',
|
|
'process_geotagging': 'Geotagging frames...',
|
|
'agree': 'Y'
|
|
}
|
|
}
|
|
|
|
ui_text_locale = ui_text_dict.get(ui_language)
|
|
|
|
# Introduction text
|
|
print(ui_text_locale.get('ui_intro'))
|
|
|
|
# User variables
|
|
## Paths
|
|
print('\n{}'.format(ui_text_locale.get('ui_paths_title')))
|
|
|
|
### Video file
|
|
while True:
|
|
try:
|
|
video_path = unix_path(input('{}'.format(ui_text_locale.get('ui_video_path')))).strip()
|
|
if os.path.exists(video_path):
|
|
break
|
|
else:
|
|
print('{}\n'.format(ui_text_locale.get('ui_path_err')))
|
|
True
|
|
except:
|
|
print('{}\n'.format(ui_text_locale.get('ui_path_err')))
|
|
|
|
### GPX track file
|
|
while True:
|
|
try:
|
|
gpx_path = unix_path(input('{}'.format(ui_text_locale.get('ui_gpx_path')))).strip()
|
|
if os.path.exists(gpx_path):
|
|
break
|
|
else:
|
|
print('{}\n'.format(ui_text_locale.get('ui_path_err')))
|
|
True
|
|
except:
|
|
print('{}\n'.format(ui_text_locale.get('ui_path_err')))
|
|
|
|
### Output folder
|
|
output_path = unix_path(input(ui_text_locale.get('ui_output')))
|
|
|
|
## Parameters
|
|
print('\n{}'.format(ui_text_locale.get('ui_parameters_title')))
|
|
|
|
### Timelapse video
|
|
timelapse = input(ui_text_locale.get('ui_timelapse'))
|
|
|
|
if timelapse.upper() == ui_text_locale.get('agree'):
|
|
### Timelapse framerate parameter
|
|
while True:
|
|
try:
|
|
timelapse_fps = int(input(ui_text_locale.get('ui_timelapse_fps')))
|
|
frame_sampling = 1 / timelapse_fps
|
|
break
|
|
except ValueError:
|
|
print(ui_text_locale.get('ui_timelapse_fps_err'))
|
|
True
|
|
|
|
else:
|
|
### Frame sampling parameter
|
|
min_frame_samp = 0.5
|
|
|
|
while True:
|
|
try:
|
|
frame_sampling = float(input(ui_text_locale.get('ui_frame_samp').format(min_frame_samp, max_frame_samp)))
|
|
|
|
if max_frame_samp >= frame_sampling >= min_frame_samp:
|
|
break
|
|
else:
|
|
print(ui_text_locale.get('ui_frame_samp_err').format(min_frame_samp, max_frame_samp))
|
|
True
|
|
except ValueError:
|
|
print(ui_text_locale.get('ui_frame_samp_err').format(min_frame_samp, max_frame_samp))
|
|
|
|
## Frame height parameter
|
|
min_frame_height = 480
|
|
max_frame_height = 6000
|
|
|
|
while True:
|
|
try:
|
|
frame_height = int(input(ui_text_locale.get('ui_frame_height').format(min_frame_height, max_frame_height)))
|
|
|
|
if max_frame_height >= frame_height >= min_frame_height:
|
|
break
|
|
else:
|
|
print(ui_text_locale.get('ui_frame_height_err').format(min_frame_height, max_frame_height))
|
|
True
|
|
except ValueError:
|
|
print(ui_text_locale.get('ui_frame_height_err').format(min_frame_height, max_frame_height))
|
|
|
|
### Video start datetime parameter
|
|
while True:
|
|
try:
|
|
video_start_datetime = input(ui_text_locale.get('ui_video_start_datetime'))
|
|
video_start_datetime_obj = datetime.strptime(video_start_datetime, '%Y-%m-%dT%H:%M:%S.%f')
|
|
break
|
|
except ValueError:
|
|
print(ui_text_locale.get('ui_video_start_datetime_err'))
|
|
True
|
|
|
|
### Video recording timezone
|
|
video_rec_timezone = input(ui_text_locale.get('ui_rec_timezone'))
|
|
|
|
### Time offset parameter
|
|
min_time_offset = -10.0
|
|
max_time_offset = 10.0
|
|
|
|
while True:
|
|
try:
|
|
time_offset = float(input(ui_text_locale.get('ui_time_offset').format(min_time_offset, max_time_offset)))
|
|
|
|
if max_time_offset >= frame_sampling >= min_time_offset:
|
|
break
|
|
else:
|
|
print(ui_text_locale.get('ui_time_offset_err').format(min_time_offset, max_time_offset))
|
|
True
|
|
except ValueError:
|
|
print(ui_text_locale.get('ui_time_offset_err').format(min_time_offset, max_time_offset))
|
|
|
|
### User-defined metadata
|
|
make = input(ui_text_locale.get('ui_make'))
|
|
model = input(ui_text_locale.get('ui_model'))
|
|
author = input(ui_text_locale.get('ui_author'))
|
|
|
|
# Getting EXIF metadata
|
|
print('\n{}'.format(ui_text_locale.get('process_reading_metadata')))
|
|
|
|
if exiftool_path != 'exiftool':
|
|
et.ExifTool(executable=exiftool_path)
|
|
|
|
with et.ExifToolHelper() as eth:
|
|
video_metadata = eth.get_metadata('{}'.format(video_path))
|
|
|
|
# for d in video_metadata:
|
|
# for k, v in d.items():
|
|
# print(f"Dict: {k} = {v}")
|
|
|
|
## Building metadata variables
|
|
video_metadata = video_metadata[0]
|
|
video_file_name = os.path.splitext(video_metadata.get('File:FileName'))[0]
|
|
video_size = byte_multiple(video_metadata.get('File:FileSize'))
|
|
#video_rec_timezone = video_metadata.get('File:FileModifyDate')[-6:]
|
|
video_duration = video_metadata.get('QuickTime:Duration')
|
|
|
|
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 = video_start_datetime_obj.strftime('%f')
|
|
|
|
# Displaying metadata
|
|
print('\n{}'.format(ui_text_locale.get('ui_metadata').format(video_metadata.get('File:FileName'),
|
|
round(video_size[0], 3), video_size[1], video_duration,
|
|
video_start_datetime,
|
|
int(int(video_start_subsectime) / 1000),
|
|
video_rec_timezone)))
|
|
|
|
# Creating output folder
|
|
output_path = '{}/{}'.format(output_path, video_metadata.get('File:FileName'))
|
|
existing_path(output_path)
|
|
|
|
# Processes
|
|
## ffmpeg process
|
|
print('\n{}'.format(ui_text_locale.get('process_sampling')))
|
|
|
|
if timelapse.upper() == ui_text_locale.get('agree'):
|
|
sampling_cmd = (('{} -loglevel error -hide_banner -stats -i {} -vf "scale=-2:{}" -qscale:v 2 '
|
|
'{}/{}_f%04d.jpg').format(ffmpeg_path, video_path, frame_height, output_path, video_file_name))
|
|
else:
|
|
sampling_cmd = (('{} -loglevel error -hide_banner -stats -i {} -vf "scale=-2:{},fps=fps=1/{}:start_time=0:round=zero"'
|
|
' -qscale:v 2 {}/{}_f%04d.jpg').format(ffmpeg_path, video_path, frame_height, frame_sampling,
|
|
output_path, video_file_name))
|
|
|
|
sampling = os.system(sampling_cmd)
|
|
|
|
## ExifTool processes
|
|
print('\n{}'.format(ui_text_locale.get('process_timestamping')))
|
|
user_metadata = '"-Make={}" "-Model={}" "-Author={}" "-Copyright={}, {}"'\
|
|
.format(make, model, author, author, video_start_datetime_obj.strftime('%Y'))
|
|
metadata_cmd = '{} -overwrite_original {} "{}/{}_f*.jpg"'\
|
|
.format(exiftool_path, user_metadata, output_path, video_file_name)
|
|
metadata = os.system(metadata_cmd)
|
|
|
|
frame_list = glob.glob('{}/{}_f*.jpg'.format(output_path, video_file_name))
|
|
i = 0
|
|
|
|
for f in frame_list:
|
|
time_shift = i * frame_sampling
|
|
current_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_shift)
|
|
current_datetime = current_datetime_obj.strftime('%Y:%m:%d %H:%M:%S')
|
|
current_subsectime = int(int(current_datetime_obj.strftime('%f')) / 1000)
|
|
|
|
timestamp_cmd = ('{} -P -m -overwrite_original "-DateTimeOriginal={}" "-OffsetTimeOriginal={}"'
|
|
' "-SubSecTimeOriginal={}" "{}"')\
|
|
.format(exiftool_path, current_datetime, video_rec_timezone, current_subsectime, f)
|
|
timestamp = os.system(timestamp_cmd)
|
|
i += 1
|
|
|
|
print('\n{}'.format(ui_text_locale.get('process_geotagging')))
|
|
geotagging_cmd = '{} -P -geotag "{}" "-geotime<SubSecDateTimeOriginal" -overwrite_original "{}/{}_f*.jpg"'\
|
|
.format(exiftool_path, gpx_path, output_path, video_file_name)
|
|
geotagging = os.system(geotagging_cmd)
|
|
|
|
|
|
input('\n{}'.format(ui_text_locale.get('ui_end')))
|