16 Commits

Author SHA1 Message Date
e137d4bdf3 Merge branch 'v2' of ssh://git.luc-geo.fr:8201/lumathieu/video2geoframes.py into v2 2024-06-26 01:44:44 +02:00
708c400c61 Fix release badge 2024-06-26 01:43:22 +02:00
75a167ad87 Delete french version link 2024-06-26 01:38:07 +02:00
9363029603 Spelling fix 2024-06-26 01:36:38 +02:00
bfa9f55561 Add EXTRA_LICENSES 2024-06-26 01:34:35 +02:00
466ada80ad Add global project description to README + french version 2024-06-26 01:17:35 +02:00
4a66a8a202 [v2.0-alpha9] Fixing relative paths support 2024-06-24 03:31:59 +02:00
67db172d62 [v2.0-alpha8] Multiple fixes + complete re-writing of script initial configuration + exceptions support enhancement + code cleanup 2024-06-24 03:18:27 +02:00
0040572ac8 Fixing mistakes in import. 2024-06-24 03:03:36 +02:00
9ef72f4567 [v2.0-alpha7] TUI enhancement 2024-06-24 03:00:43 +02:00
7d53803c7d [v2.0-alpha6] Adaptations on locales text 2024-06-24 02:50:23 +02:00
801ca04299 [v2.0-alpha6] Implementing resizing frame function + modifying max. default video height (16K) + code reordering 2024-06-24 02:47:03 +02:00
c64d303fe3 [v2.0-alpha5] Re-formatting displaying date in TUI + re-formatting unit in tqdm (cv2 process) + TUI enhancement + correction in locales 2024-06-24 02:32:27 +02:00
cff5d49833 [v2.0-alpha4] Adding script version in exif.software tag 2024-06-24 02:22:16 +02:00
ab42d4f8e4 [v2.0-alpha3] Replacement of exif library by piexif + removal of tag xmp.author + addition of tag exif.artist and exif.software + TUI enhancement 2024-06-24 02:03:54 +02:00
7ead19a651 [v2.0-alpha2] Fix exif tags id + fix images path for geotagging + removal of offset_time_original tag 2024-06-24 01:51:34 +02:00
8 changed files with 576 additions and 253 deletions

View File

@@ -1,5 +1,96 @@
# video2geoframes.py # video2geoframes.py
Python script to generate a collection of geotagged images from a video and a GPS track. ![Gitea Release](https://img.shields.io/gitea/v/release/lumathieu/video2geoframes.py?gitea_url=https%3A%2F%2Fgit.luc-geo.fr&include_prereleases&sort=semver&display_name=release&style=flat&link=https%3A%2F%2Fgit.luc-geo.fr%2Flumathieu%2Fvideo2geoframes.py%2Freleases)
Designed for contribution to street-level imagery projects like Mapillary or Panoramax. _🇬🇧 version_
Python program to generate a collection of geotagged images from a video and a GPS track.
Designed for ease contribution to street-level imagery projects like Mapillary or Panoramax.
## Quick start
Nothing simpler : collect your video, your GPS track, execute Python script and follow the guide !
In detail, the program is built around a TUI or _Textual User Interface_, permitting to launch video process easily with
step-by-step parameters input.
Input is guided by textual help indicating attempted values.
Before script starting, you need to have :
* a video file with exact timestamp (start) in local time or UTC
* a clean GPS tack file covering video duration
* a working directory.
## Documentation
_Coming soon._
## Features
_Coming soon._
### Comparison v1 / v2
| Features | v1-beta | v2-alpha9 |
|-----------------------------|------------|------------|
| Timelapse video support | ✔️ | ✔️ |
| EXIF tags writing | ✔️ | ✔️ |
| Extended tags support | ✔️ | ❌ |
| Milliseconds support | ✔️ | ✔️ |
| Progress displaying | 🟡 raw | ✔️ |
| Multilingual TUI 🇺🇳 | 🟡 limited | ✔️ |
| Configuration customization | ❌ | 🟡 partial |
| JPEG qualtiy customization | ❌ | 🔄 planned |
| TOML setting | ❌ | 🔄 planned |
## Languages
TUI is multilingual thanks to "locales" base in the form of TOML files (`locales/*.toml`) easily extensible.
| Languages | Locale | Support | Maintainer |
|--------------|---------|------------|--------------|
| 🇺🇸 English | `en_us` | ✔️ 100 % | @lumathieu |
| 🇫🇷 French | `fr_fr` | ✔️ 100 % | @lumathieu |
| 🇮🇹 Italian | `it_it` | 🔄 planned | @lumathieu ? |
## Versions
See [_Releases_](https://git.luc-geo.fr/lumathieu/video2geoframes.py/releases).
## Setup
To set up program, be enough to clone Git repository, set up software dependencies and build Python environnement.
Recommended to use a virtual environnement (venv).
### Python
Entire project is developed and tested on **Python 3.11** (Windows x86-64).
### Dépendances
Core script uses following Python libraries (see also `requirements.txt`) :
- `numpy`
- `opencv-python`
- `piexif`
- `tomlkit`
- `tqdm`.
## Compatibility
Code is designed to be platform-independent.
Official supported platforms are Windows and Linux (partially tested under Debian / Ubuntu).
## Contribution
_Coming soon._
If you are interested to project contribution, you can send a mail to campanu@luc-geo.fr.
## License
This repository, except dependencies, is licensed under **GNU AGPL v3**.
Dependencies are included in repository for development and keep their original license
(see `dependencies/EXTRA_LICENSES.md`).

113
README_fr.md Normal file
View File

@@ -0,0 +1,113 @@
# video2geoframes.py
![Gitea Release](https://img.shields.io/gitea/v/release/lumathieu/video2geoframes.py?gitea_url=https%3A%2F%2Fgit.luc-geo.fr&include_prereleases&sort=semver&display_name=release&style=flat&link=https%3A%2F%2Fgit.luc-geo.fr%2Flumathieu%2Fvideo2geoframes.py%2Freleases)
_version 🇫🇷_
Programme Python permettant de générer un ensemble d'images géotaguées depuis une vidéo et une trace GPS.
Conçu pour faciliter la contribution à des projets de photo-cartographie de rue tels que Mapillary ou Panoramax.
## Démarrage rapide
Rien de plus simple : rassemblez votre vidéo, votre trace GPS, lancez le script Python et suivez le guide !
En détail, le programme est entièrement construit autour d'une TUI ou _Textual User Interface_, qui permet de lancer
facilement le traitement de la vidéo par la saisie pas-à-pas des paramètres.
La saisie est guidée par une aide textuelle indiquant les valeurs attendues.
Avant de lancer le script, vous avez besoin d'avoir :
* un fichier vidéo avec son horodatage exact (début) en temps local ou UTC
* un fichier de trace GPS propre couvrant la durée de la vidéo
* un dossier de travail.
## Documentation
_A venir._
## Fonctionnalités
Le programme permet d'exécuter en un seul traitement les tâches suivantes :
* le séquençage de la vidéo selon un intervalle de temps
* l'horodatage incrémental de la séquence d'image
* l'export des images au format JPEG
* le géotaguage des images exportées à partir de la trace GPS.
Il inclut également :
* le support des vidéos timelapse
* le redimensionnement des images à une résolution inférieure à la vidéo d'origine tout en conservant le ratio
* l'ajout de métadonnées avec les tags EXIF `artist`, `make`, `model` et `copyright` (cf. [documentation ExifTool](https://exiftool.org/TagNames/EXIF.html))
* l'horodatage à la précision de la milliseconde
* le support du temps local décalé par rapport à UTC.
* l'ajout d'un décalage temporel pour mieux corréler la vidéo et la trace GPS.
Lors de l'export, un sous-dossier nommé selon la vidéo est créé automatiquement dans le répertoire de sortie.
### Comparaison v1 / v2
| Fonctionnalité | v1-beta | v2-alpha9 |
|--------------------------------------|------------|--------------|
| Support des vidéos timelapse | ✔️ | ✔️ |
| Écriture des tags EXIF | ✔️ | ✔️ |
| Support des tags étendus | ✔️ | ❌ |
| Support des millisecondes | ✔️ | ✔️ |
| Affichage de la progression | 🟡 brut | ✔️ |
| TUI multilingue 🇺🇳 | 🟡 limitée | ✔️ |
| Personnalisation de la configuration | ❌ | 🟡 partielle |
| Personnalisation qualité JPEG | ❌ | 🔄 planifié |
| Paramétrage via TOML | ❌ | 🔄 planifié |
## Langues
La TUI est multilingue grâce une base de "locales" sous forme de fichiers TOML (`locales/*.toml`) facilement extensible.
| Langue | Locale | Support | Mainteneur |
|---------------|---------|-------------|--------------|
| 🇺🇸 Anglais | `en_us` | ✔️ 100 % | @lumathieu |
| 🇫🇷 Français | `fr_fr` | ✔️ 100 % | @lumathieu |
| 🇮🇹 Italien | `it_it` | 🔄 planifié | @lumathieu ? |
## Versions
Voir [_Releases_](https://git.luc-geo.fr/lumathieu/video2geoframes.py/releases).
## Installation
Pour installer le programme, il suffit de cloner le dépôt Git, d'installer les dépendances logicielles et de construire
l'environnement Python. Il est recommandé d'utiliser un environnement virtuel (venv).
### Python
L'ensemble du projet est développé et testé avec **Python 3.11** (Windows x86-64).
### Dépendances
Le script principal utilise les librairies Python suivantes (voir aussi `requirements.txt`) :
- `numpy`
- `opencv-python`
- `piexif`
- `tomlkit`
- `tqdm`.
Il utilise également le programme [`ExifTool`](https://exiftool.org/) pour le géotaguage des images.
Appelée par une commande système, cette dépendance est prévue pour être supprimée dans les versions futures.
## Compatibilité
Le code est conçu pour être indépendant de la plateforme.
Les plateformes officiellement supportées sont Windows et Linux (partiellement testé sous Debian / Ubuntu).
## Contribution
_A venir._
Si vous intéressé pour contribuer au projet, vous pouvez envoyer un mail à campanu@luc-geo.fr.
## Licence
Ce dépôt, à l'exception des dépendances, est sous licence **GNU AGPL v3**.
Les dépendances sont incluses au dépôt pour le développement et restent sous leur licence d'origine
(voir `dependencies/EXTRA_LICENSES.md`).

8
dependencies/EXTRA_LICENSES.md vendored Normal file
View File

@@ -0,0 +1,8 @@
# Extra licenses
Detail of dependencies original license.
## ExifTool (by Phil Harvey)
> This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
> http://dev.perl.org/licenses/

View File

@@ -1,7 +1,7 @@
# Localization file for video2geoframes.py script # Localization file for video2geoframes.py
# English (US / World) # English (US / World)
# #
# Last edition : 2024-06-22 # Last edition : 2024-06-23
[ui] [ui]
@@ -12,6 +12,7 @@ This script is designed to create geotagged frames from video and GPX track."""
end = "End of program, press Enter to quit." end = "End of program, press Enter to quit."
paths_title = "## Paths" paths_title = "## Paths"
parameters_title = "## Process parameters" parameters_title = "## Process parameters"
tags_title = "## Additional tags"
metadata = """{} ({} {}B)\n metadata = """{} ({} {}B)\n
- Duration : {} s\n - Duration : {} s\n
- Start time : {}.{}\n - Start time : {}.{}\n
@@ -21,16 +22,16 @@ metadata = """{} ({} {}B)\n
cv2_tqdm = 'frame(s)' cv2_tqdm = 'frame(s)'
[ui.parameters] [ui.parameters]
toml_setting² = "Setting with TOML file ({}/{}) ? " toml_setting = "Setting with TOML file ({}/{}) ? "
timelapse = "Timelapse video ({}/{}) ? " timelapse = "Timelapse video ({}/{}) ? "
timelapse_fps = "Timelapse framerate (frame/s) [{}-{}] : " timelapse_fps = "Timelapse framerate (frame/s) [{}-{}] : "
timelapse_fps_err = "Error... please enter a decimal between {} et {}." timelapse_fps_err = "Error... please enter a decimal between {} et {}."
frame_samp = "Enter the frame sampling in seconds [{}-{}] : " frame_samp = "Enter frame sampling in seconds [{}-{}] : "
frame_samp_err = "Error... please enter a decimal between {} and {}." frame_samp_err = "Error... please enter a decimal between {} and {}."
frame_height = "Enter frame height in pixels (ratio unchanged) [{}-{}] : " frame_height = "Enter output frame height in pixels (ratio unchanged) [{}-{}] : "
frame_height_err = "Error... please enter an integer between {} and {}." frame_height_err = "Error... please enter an integer between {} and {}."
video_start_datetime = "Enter video start datetime following ISO format (exemple : 2023-09-18T22:00:02.000) : " video_start_datetime = "Enter video start datetime following ISO format (exemple : 2023-09-18T22:00:02.000) : "
@@ -38,7 +39,7 @@ video_start_datetime_err = "Error... entered datetime is not valid."
rec_timezone = "Enter time offset related to UTC (example for CEST : +02:00) : " rec_timezone = "Enter time offset related to UTC (example for CEST : +02:00) : "
time_offset = "Enter time offset between video and GPX in seconds [{}-{}] : " time_offset = "Enter time offset with GPS track in seconds [{}-{}] : "
time_offset_err = "Error... please enter a decimal between {} and {}." time_offset_err = "Error... please enter a decimal between {} and {}."
[ui.paths] [ui.paths]

View File

@@ -1,7 +1,7 @@
# Localization file for video2geoframes.py script # Localization file for video2geoframes.py
# French (France) # French (France)
# #
# Last edition : 2024-06-22 # Last edition : 2024-06-23
[ui] [ui]
@@ -11,6 +11,7 @@ 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.""" 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." end = "Fin du programme, appuyez sur Entrée pour fermer."
paths_title = "## Chemins" paths_title = "## Chemins"
tags_title = "## Tags additionnels"
parameters_title = "## Paramètres du traitement" parameters_title = "## Paramètres du traitement"
metadatas = """{} ({} {}B) metadatas = """{} ({} {}B)
- Durée : {} s - Durée : {} s
@@ -27,10 +28,10 @@ timelapse = "Vidéo timelapse ({}/{}) ? "
timelapse_fps = "Débit d'image du timelapse (image/s) [{}-{}] : " timelapse_fps = "Débit d'image du timelapse (image/s) [{}-{}] : "
timelapse_fps_err = "Erreur... entrez un entier entre {} et {}." timelapse_fps_err = "Erreur... entrez un entier entre {} et {}."
frame_samp = "Entrez l'espacement temporel des images en secondes [{}-{}] : " frame_samp = "Entrez l'espacement temporel en secondes entre les images [{}-{}] : "
frame_samp_err = "'Erreur... veuillez entrer un nombre décimal entre {} et {}." frame_samp_err = "'Erreur... veuillez entrer un nombre décimal entre {} et {}."
frame_height = "Entrez la hauteur des images en pixels (ratio inchangé) [{}-{}] : " frame_height = "Entrez la hauteur en pixels des images en sortie (ratio inchangé) [{}-{}] : "
frame_height_err = "Erreur... veuillez entrer un nombre entier entre {} et {}." frame_height_err = "Erreur... veuillez entrer un nombre entier entre {} et {}."
video_start_datetime = "Entrez l'horodatage du début de la vidéo au format ISO (exemple : 2023-09-18T22:00:02.000) : " video_start_datetime = "Entrez l'horodatage du début de la vidéo au format ISO (exemple : 2023-09-18T22:00:02.000) : "
@@ -38,7 +39,7 @@ video_start_datetime_err = "Erreur... l'horodatage entré est invalide."
rec_timezone = "Entrez le décalage horaire par rapport à UTC (exemple pour CEST : +02:00) : " rec_timezone = "Entrez le décalage horaire par rapport à UTC (exemple pour CEST : +02:00) : "
time_offset = "Entrez le décalage temporel entre la vidéo et le GPX en secondes [{}-{}] : " time_offset = "Entrez le décalage temporel avec la trace GPS en secondes [{}-{}] : "
time_offset_err = "Erreur... veuillez entrer un nombre décimal entre {} et {}." time_offset_err = "Erreur... veuillez entrer un nombre décimal entre {} et {}."
[ui.paths] [ui.paths]

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
colorama==0.4.6
numpy==2.0.0
opencv-python==4.10.0.84
piexif==1.1.3
tomlkit==0.12.5
tqdm==4.66.4

View File

@@ -2,13 +2,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""video2geoframes.py """video2geoframes.py
Python script to generate a collection of geotagged images from a video and a GPS track. Python program 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. Designed for contribution to street-level imagery projects like Mapillary or Panoramax.
""" """
__author__ = "Lucas MATHIEU (@campanu)" __author__ = "Lucas MATHIEU (@campanu)"
__license__ = "AGPL-3.0-or-later" __license__ = "AGPL-3.0-or-later"
__version__ = "2.0-alpha1" __version__ = "2.0-alpha9"
__maintainer__ = "Lucas MATHIEU (@campanu)" __maintainer__ = "Lucas MATHIEU (@campanu)"
__email__ = "campanu@luc-geo.fr" __email__ = "campanu@luc-geo.fr"
@@ -18,9 +18,10 @@ import platform
from datetime import datetime, timedelta from datetime import datetime, timedelta
import cv2 import cv2
from tomlkit import dumps, loads import piexif
from tomlkit import loads
from tqdm import tqdm from tqdm import tqdm
from exif import Image, GpsAltitudeRef
# Functions # Functions
def unix_path(path): def unix_path(path):
@@ -44,300 +45,383 @@ def byte_multiple(size):
size = size / 1024 size = size / 1024
multiple = multiples[i] multiple = multiples[i]
return size, multiple return size, multiple
def existing_items(expected_items: list, items: list):
presents_items = []
duplicated_items = []
missing_items = []
for eit in expected_items:
i = 0
for it in items:
if it == eit:
i += 1
if i == 0:
missing_items.append(eit)
elif i == 1:
presents_items.append(eit)
else:
duplicated_items.append(eit)
return {'presents': presents_items, 'duplicated': duplicated_items, 'missing': missing_items}
def list_enumerator(item_list: list, intermediate_separator: str, last_separator: str):
i = 1
for it in item_list:
if i == 1:
enumerated_list = it
elif 1 < i < len(item_list):
enumerated_list = '{}{}{}'.format(enumerated_list, intermediate_separator, it)
else:
enumerated_list = '{}{}{}'.format(enumerated_list, last_separator, it)
i += 1
return enumerated_list
# Start # Start
print('# video2geoframes.py') print("# video2geoframes.py (v{})\n".format(__version__))
# Configuration settings # Configuration settings
base_path = unix_path(os.path.dirname(__file__)) base_path = unix_path(os.path.dirname(__file__))
ini_file_path = '{}/video2geoframes.ini'.format(base_path) conf_file_path = '{}/video2geoframes_conf.toml'.format(base_path)
ini_file_err = False conf_file_err = False
## Default values # Default values
mandatory_parameters = ['locale', 'exiftool_path']
locale = 'en_us' locale = 'en_us'
min_frame_samp = 0.5 min_frame_samp = 0.5
max_frame_samp = float(60) max_frame_samp = 60.0
min_timelapse_fps = 1 min_timelapse_fps = 1
max_timelapse_fps = 15 max_timelapse_fps = 15
min_frame_height = 480
max_frame_height = 9000
min_time_offset = -10.0
max_time_offset = 10.0
## Platform-dependent commands # Platform-dependent default paths
if platform.system() == 'Windows': if platform.system() == 'Windows':
ffmpeg_path = '{}/dependencies/ffmpeg-essentials/bin/ffmpeg.exe'.format(base_path)
exiftool_path = '{}/dependencies/exiftool.exe'.format(base_path) exiftool_path = '{}/dependencies/exiftool.exe'.format(base_path)
else: else:
ffmpeg_path = 'ffmpeg'
exiftool_path = 'exiftool' exiftool_path = 'exiftool'
## ini file reading try:
if os.path.exists(ini_file_path): # Configuration file reading
configuration = {}
try: try:
with open(ini_file_path, 'r') as file: if os.path.exists(conf_file_path):
for line in file: with codecs.open(conf_file_path, mode='r', encoding='utf-8') as f:
if line[0] == '#': conf_toml = loads(f.read())
continue f.close()
else: else:
(key, value) = line.split() raise FileNotFoundError
configuration[key] = value.replace('"', '')
locale = configuration.get('ui_language') # Configuration check
max_frame_samp = float(configuration.get('max_frame_sample')) reading_parameters = conf_toml['system'].keys()
ffmpeg_path = configuration.get('ffmpeg_path').replace('./', '{}/'.format(base_path)) check_result = existing_items(mandatory_parameters, reading_parameters)
exiftool_path = configuration.get('exiftool_path').replace('./', '{}/'.format(base_path))
except:
print('\nError... not readable or incomplete ini file. Default configuration will be used.')
# Localization if len(check_result['missing']) != 0:
locale_file_path = '{}/locales/{}.toml'.format(base_path, locale) missing_parameters_list = list_enumerator(check_result['missing'], ', ', ' and ')
if os.path.exists(locale_file_path): if len(missing_parameters_list) > 1:
verb = 'is'
else:
verb = 'are'
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))
except (FileNotFoundError, ValueError):
print("\nError... configuration file doesn't exists or invalid.")
default_conf = str(input("Use default configuration instead (Y/N) ? ").upper())
if default_conf != 'Y':
raise InterruptedError
# Localization
locale_file_path = '{}/locales/{}.toml'.format(base_path, locale)
if os.path.exists(locale_file_path):
with codecs.open(locale_file_path, mode='r', encoding='utf-8') as f: with codecs.open(locale_file_path, mode='r', encoding='utf-8') as f:
locale_toml = loads(f.read()) locale_toml = loads(f.read())
f.close() f.close()
else:
print("Error.... file for locale \"{}\" doesn't exists or invalid.".format(locale))
ValueError
user_agree = locale_toml['user']['agree'][0].upper()
user_disagree = locale_toml['user']['disagree'][0].upper()
path_error = locale_toml['ui']['paths']['path_err']
# Introduction text
print(locale_toml['ui']['info']['intro'])
# 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':
while True:
try:
i += 1
toml_file_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['toml_file']))).strip()
if os.path.exists(toml_file_path):
break
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
except:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
with codecs.open(toml_file_path, mode='r', encoding='utf-8') as f:
setting_toml = loads(f.read())
f.close()
# <--coding in progress-->
video_path = ''
gps_track_path = ''
## Paths
else:
print('\n{}'.format(locale_toml['ui']['info']['paths_title']))
### Video file
while True:
try:
video_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['video_file']))).strip()
if os.path.exists(video_path):
break
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
except:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
### GPS track file
while True:
try:
gps_track_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['gps_track']))).strip()
if os.path.exists(gps_track_path):
break
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
except:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
### Output folder
output_folder = unix_path(input(locale_toml['ui']['paths']['output_folder']))
## Parameters
print('\n{}'.format(locale_toml['ui']['info']['parameters_title']))
### Timelapse video
timelapse = input(locale_toml['ui']['parameters']['timelapse'].format(user_agree, user_disagree))
if timelapse.upper() == user_agree:
### 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 = 1 / timelapse_fps
break
else:
print(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps,
max_timelapse_fps))
True
except ValueError:
print(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps, max_timelapse_fps))
True
else: else:
### Frame sampling parameter print("\nError.... file for locale \"{}\" doesn't exists or invalid.".format(locale))
raise InterruptedError
user_agree = locale_toml['user']['agree'][0].upper()
user_disagree = locale_toml['user']['disagree'][0].upper()
path_error = locale_toml['ui']['paths']['path_err']
# Introduction text
print(locale_toml['ui']['info']['intro'])
# 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':
while True:
try:
i += 1
toml_file_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):
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
# <--coding in progress-->
raise NotImplementedError
# Paths
else:
print('\n{}'.format(locale_toml['ui']['info']['paths_title']))
# Video file
while True:
try:
video_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['video_file']))).strip()
if os.path.exists(video_path):
break
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
except:
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))
# GPS track file
while True:
try:
gps_track_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['gps_track']))).strip()
if os.path.exists(gps_track_path):
break
else:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
except:
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
True
# Output folder
output_folder = unix_path(input(locale_toml['ui']['paths']['output_folder']))
# Parameters
print('\n{}'.format(locale_toml['ui']['info']['parameters_title']))
# Timelapse video
timelapse = input(locale_toml['ui']['parameters']['timelapse'].format(user_agree, user_disagree)).upper()
if timelapse == user_agree:
# 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,
max_timelapse_fps)))
True
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:
try:
frame_sampling = float(input(locale_toml['ui']['parameters']['frame_samp'].format(min_frame_samp,
max_frame_samp)))
if max_frame_samp >= frame_sampling >= min_frame_samp:
break
else:
print('\n{}'.format(locale_toml['ui']['parameters']['frame_samp_err'].format(min_frame_samp, max_frame_samp)))
True
except ValueError:
print('\n{}'.format(locale_toml['ui']['parameters']['frame_samp_err'].format(min_frame_samp, max_frame_samp)))
True
# Frame height parameter
if video_height <= max_frame_height:
max_frame_height = int(round(video_height, 0))
while True: while True:
try: try:
frame_sampling = float(input(locale_toml['ui']['parameters']['frame_samp'].format(min_frame_samp, frame_height = int(input(locale_toml['ui']['parameters']['frame_height'].format(min_frame_height,
max_frame_samp))) max_frame_height)))
if max_frame_samp >= frame_sampling >= min_frame_samp: if max_frame_height >= frame_height >= min_frame_height:
break
elif frame_height == 0:
break break
else: else:
print(locale_toml['ui']['parameters']['frame_samp_err'].format(min_frame_samp, max_frame_samp)) print('\n{}'.format(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height)))
True True
except ValueError: except ValueError:
print(locale_toml['ui']['parameters']['frame_samp_err'].format(min_frame_samp, max_frame_samp)) print('\n{}'.format(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height)))
True True
## Frame height parameter # Video start datetime parameter
min_frame_height = 480 while True:
max_frame_height = 6000 try:
video_start_datetime = input(locale_toml['ui']['parameters']['video_start_datetime'])
while True: video_start_datetime_obj = datetime.strptime(video_start_datetime, '%Y-%m-%dT%H:%M:%S.%f')
try:
frame_height = int(input(locale_toml['ui']['parameters']['frame_height'].format(min_frame_height,
max_frame_height)))
if max_frame_height >= frame_height >= min_frame_height:
break break
else: except ValueError:
print(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height)) print('\n{}'.format(locale_toml['ui']['parameters']['video_start_datetime_err']))
True True
except ValueError:
print(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height))
True
### Video start datetime parameter # Video recording timezone
while True: video_rec_timezone = input(locale_toml['ui']['parameters']['rec_timezone'])
try:
video_start_datetime = input(locale_toml['ui']['parameters']['video_start_datetime'])
video_start_datetime_obj = datetime.strptime(video_start_datetime, '%Y-%m-%dT%H:%M:%S.%f')
break
except ValueError:
print(locale_toml['ui']['parameters']['video_start_datetime_err'])
True
### Video recording timezone # Time offset parameter
video_rec_timezone = input(locale_toml['ui']['parameters']['rec_timezone']) while True:
try:
time_offset = float(input(locale_toml['ui']['parameters']['time_offset'].format(min_time_offset,
max_time_offset)))
### Time offset parameter if max_time_offset >= frame_sampling >= min_time_offset:
min_time_offset = -10.0 break
max_time_offset = 10.0 else:
print('\n{}'.format(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset)))
while True: True
try: except ValueError:
time_offset = float(input(locale_toml['ui']['parameters']['time_offset'].format(min_time_offset, max_time_offset))) print('\n{}'.format(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset)))
if max_time_offset >= frame_sampling >= min_time_offset:
break
else:
print(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset))
True True
except ValueError:
print(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset))
True
### User-defined metadata # User-defined metadata
make = input(locale_toml['ui']['metadatas']['make']) print('\n{}'.format(locale_toml['ui']['info']['tags_title']))
model = input(locale_toml['ui']['metadatas']['model'])
author = input(locale_toml['ui']['metadatas']['author'])
# Getting video metadatas make = input(locale_toml['ui']['metadatas']['make'])
print('\n{}'.format(locale_toml['processing']['reading_metadatas'])) model = input(locale_toml['ui']['metadatas']['model'])
author = input(locale_toml['ui']['metadatas']['author'])
video = cv2.VideoCapture(video_path) # Video metadatas formatting
video_fps = video.get(cv2.CAP_PROP_FPS) print('\n{}'.format(locale_toml['processing']['reading_metadatas']))
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_file_name = os.path.basename(video_path) video_file_name = os.path.basename(video_path)
video_file_size = byte_multiple(os.stat(video_path).st_size) video_file_size = byte_multiple(os.stat(video_path).st_size)
video_duration = video_total_frames / video_fps video_duration = video_total_frames / video_fps
video_start_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_offset) 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_datetime = video_start_datetime_obj.strftime('%Y-%m-%d %H:%M:%S')
video_start_subsectime = video_start_datetime_obj.strftime('%f') video_start_subsectime = int(int(video_start_datetime_obj.strftime('%f')) / 1000)
# Displaying metadata # Metadatas recap
print('\n{}'.format(locale_toml['ui']['info']['metadatas'].format(video_file_name, print('\n{}'.format(locale_toml['ui']['info']['metadatas'].format(video_file_name,
round(video_file_size[0], 3), video_file_size[1], round(video_file_size[0], 3), video_file_size[1],
video_duration, video_start_datetime, video_duration, video_start_datetime,
int(int(video_start_subsectime) / 1000), '{:03d}'.format(video_start_subsectime),
video_rec_timezone))) video_rec_timezone)))
# Creating output folder # Output folder creation
output_folder = '{}/{}'.format(output_folder, video_file_name) output_folder = '{}/{}'.format(output_folder, video_file_name)
existing_path(output_folder) existing_path(output_folder)
# Processes # Processes
## Frame sampling + tagging (OpenCV + exif) # Frame sampling + tagging (OpenCV + piexif)
i = 0 print('\n{}'.format(locale_toml['processing']['sampling']))
if timelapse == user_agree: i = 0
frame_interval = (1000 * frame_sampling) / video_fps
else:
frame_interval = 1000 * frame_sampling
cv2_tqdm_unit = " {}".format(locale_toml['ui']['units']['cv2_tqdm']) if timelapse == user_agree:
frame_interval = frame_sampling / video_fps
else:
frame_interval = frame_sampling
for i in tqdm(range(video_total_frames - 1), unit=cv2_tqdm_unit): cv2_tqdm_unit = locale_toml['ui']['units']['cv2_tqdm']
t = frame_interval * i cv2_tqdm_range = int(video_duration / frame_interval)
video.set(cv2.CAP_PROP_POS_MSEC, t)
ret, frame = video.read()
frame_name = '{:05d}'.format(i) for i in tqdm(range(cv2_tqdm_range), unit=cv2_tqdm_unit):
image_name = "{}_f{}.jpg".format(video_file_name.split('.')[0], frame_name) t = frame_interval * i * 1000
video.set(cv2.CAP_PROP_POS_MSEC, t)
ret, frame = video.read()
cv2.imwrite(image_name, frame, [cv2.IMWRITE_JPEG_QUALITY, 88, cv2.IMWRITE_JPEG_PROGRESSIVE, 1, # Image resizing
cv2.IMWRITE_JPEG_SAMPLING_FACTOR, 0x411111]) if frame_height != 0:
resize_factor = video_height / frame_height
image_height = frame_height
image_width = int(round(video_height * resize_factor, 0))
## Time tags preparation frame = cv2.resize(frame, (image_width, image_height), interpolation=cv2.INTER_LANCZOS4)
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_subsec_time = int(int(current_datetime_obj.strftime('%f')) / 1000)
with open(image_name, 'rb') as image_file: frame_name = '{:05d}'.format(i)
image = Image(image_file) image_name = "{}_f{}.jpg".format(video_file_name.split('.')[0], frame_name)
image.make = make image_path = "{}/{}".format(output_folder, image_name)
image.model = model
image.author = author
image.copyright = "{}, {}".format(author, video_start_datetime_obj.strftime('%Y'))
image.datetime_original = current_datetime
image.subsec_time_original = current_subsec_time
image.offset_time_original = video_rec_timezone
with open('{}'.format(image_name), 'wb') as tagged_image_file: cv2.imwrite(image_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 88, cv2.IMWRITE_JPEG_PROGRESSIVE, 1, cv2.IMWRITE_JPEG_SAMPLING_FACTOR, 0x411111])
tagged_image_file.write(image.get_file())
i += 1 # Time tags formatting
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_subsec_time = int(int(current_datetime_obj.strftime('%f')) / 1000)
# Geo-tagging (ExifTool) # piexif code
print('\n{}'.format(locale_toml['processing']['geotagging'])) image_exif = piexif.load(image_path)
geotagging_cmd = '{} -P -geotag "{}" "-geotime<SubSecDateTimeOriginal" -overwrite_original "{}/{}_f*.jpg"'\
.format(exiftool_path, gps_track_path, output_folder, video_file_name) image_tags = {
geotagging = os.system(geotagging_cmd) piexif.ImageIFD.Make: make,
piexif.ImageIFD.Model: model,
piexif.ImageIFD.Artist: author,
piexif.ImageIFD.Copyright: "{}, {}".format(author, video_start_datetime_obj.strftime('%Y')),
piexif.ImageIFD.Software: 'video2geoframes.py (v{})'.format(__version__)
}
exif_tags = {
piexif.ExifIFD.DateTimeOriginal: current_datetime,
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
image_exif_bytes = piexif.dump(image_exif)
piexif.insert(image_exif_bytes, image_path)
i += 1
# Geo-tagging (ExifTool)
print('\n{}'.format(locale_toml['processing']['geotagging']))
geotagging_cmd = '{} -P -geotag "{}" "-geotime<SubSecDateTimeOriginal" -overwrite_original "{}/{}_f*.jpg"'\
.format(exiftool_path, gps_track_path, output_folder, video_file_name.split('.')[0])
geotagging = os.system(geotagging_cmd)
# End
input('\n{}'.format(locale_toml['ui']['info']['end']))
except NotImplementedError:
print("\nSorry, this function is not implemented, work in progress ;)")
except InterruptedError:
input("\nEnd of program, press Enter to quit.")
# End
input('\n{}'.format(locale_toml['ui']['info']['end']))

19
video2geoframes_conf.toml Normal file
View File

@@ -0,0 +1,19 @@
# Default configuration file for video2geoframes.py
# See documentation for more information
#
# Last edition : 2024-06-23
# Mandatory section
[system]
# See documentation for supported locales
locale = "en_us"
exiftool_path = "./dependencies/exiftool.exe"
[default]
# Optional section to avoid manual input
# Tags can be overwrited if presents in TOML setting file
[default.tags]
author = "Campanu"
make = "Camera maker"
camera = "Camera model"