Compare commits
10 Commits
v2.0-alpha
...
75a167ad87
| Author | SHA1 | Date | |
|---|---|---|---|
| 75a167ad87 | |||
| 9363029603 | |||
| bfa9f55561 | |||
| 466ada80ad | |||
| 4a66a8a202 | |||
| 67db172d62 | |||
| 0040572ac8 | |||
| 9ef72f4567 | |||
| 7d53803c7d | |||
| 801ca04299 |
95
README.md
95
README.md
@@ -1,5 +1,96 @@
|
|||||||
# video2geoframes.py
|
# 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.
|
_🇬🇧 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
113
README_fr.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# video2geoframes.py
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_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
8
dependencies/EXTRA_LICENSES.md
vendored
Normal 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/
|
||||||
@@ -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]
|
||||||
|
|
||||||
@@ -28,10 +28,10 @@ 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) : "
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
@@ -28,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) : "
|
||||||
|
|||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal 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
|
||||||
@@ -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-alpha5"
|
__version__ = "2.0-alpha9"
|
||||||
__maintainer__ = "Lucas MATHIEU (@campanu)"
|
__maintainer__ = "Lucas MATHIEU (@campanu)"
|
||||||
__email__ = "campanu@luc-geo.fr"
|
__email__ = "campanu@luc-geo.fr"
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import piexif
|
import piexif
|
||||||
from tomlkit import dumps, loads
|
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):
|
||||||
@@ -45,52 +45,106 @@ 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
|
|
||||||
if os.path.exists(ini_file_path):
|
|
||||||
configuration = {}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(ini_file_path, 'r') as file:
|
# Configuration file reading
|
||||||
for line in file:
|
try:
|
||||||
if line[0] == '#':
|
if os.path.exists(conf_file_path):
|
||||||
continue
|
with codecs.open(conf_file_path, mode='r', encoding='utf-8') as f:
|
||||||
|
conf_toml = loads(f.read())
|
||||||
|
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:
|
if len(check_result['missing']) != 0:
|
||||||
print('\nError... not readable or incomplete ini file. Default configuration will be used.')
|
missing_parameters_list = list_enumerator(check_result['missing'], ', ', ' and ')
|
||||||
|
|
||||||
|
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
|
# Localization
|
||||||
locale_file_path = '{}/locales/{}.toml'.format(base_path, locale)
|
locale_file_path = '{}/locales/{}.toml'.format(base_path, locale)
|
||||||
@@ -100,8 +154,8 @@ if os.path.exists(locale_file_path):
|
|||||||
locale_toml = loads(f.read())
|
locale_toml = loads(f.read())
|
||||||
f.close()
|
f.close()
|
||||||
else:
|
else:
|
||||||
print("Error.... file for locale \"{}\" doesn't exists or invalid.".format(locale))
|
print("\nError.... file for locale \"{}\" doesn't exists or invalid.".format(locale))
|
||||||
ValueError
|
raise InterruptedError
|
||||||
|
|
||||||
user_agree = locale_toml['user']['agree'][0].upper()
|
user_agree = locale_toml['user']['agree'][0].upper()
|
||||||
user_disagree = locale_toml['user']['disagree'][0].upper()
|
user_disagree = locale_toml['user']['disagree'][0].upper()
|
||||||
@@ -111,7 +165,7 @@ path_error = locale_toml['ui']['paths']['path_err']
|
|||||||
print(locale_toml['ui']['info']['intro'])
|
print(locale_toml['ui']['info']['intro'])
|
||||||
|
|
||||||
# User input
|
# User input
|
||||||
## TOML setting file
|
# TOML setting file
|
||||||
toml_setting = input('\n{}'.format(locale_toml['ui']['parameters']['toml_setting'].format(user_agree, user_disagree)))
|
toml_setting = input('\n{}'.format(locale_toml['ui']['parameters']['toml_setting'].format(user_agree, user_disagree)))
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
@@ -122,26 +176,24 @@ if toml_setting.upper() == 'O':
|
|||||||
toml_file_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['toml_file']))).strip()
|
toml_file_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['toml_file']))).strip()
|
||||||
|
|
||||||
if os.path.exists(toml_file_path):
|
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:
|
with codecs.open(toml_file_path, mode='r', encoding='utf-8') as f:
|
||||||
setting_toml = loads(f.read())
|
setting_toml = loads(f.read())
|
||||||
f.close()
|
f.close()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
|
||||||
|
True
|
||||||
|
|
||||||
# <--coding in progress-->
|
# <--coding in progress-->
|
||||||
video_path = ''
|
raise NotImplementedError
|
||||||
gps_track_path = ''
|
|
||||||
|
|
||||||
## Paths
|
# Paths
|
||||||
else:
|
else:
|
||||||
print('\n{}'.format(locale_toml['ui']['info']['paths_title']))
|
print('\n{}'.format(locale_toml['ui']['info']['paths_title']))
|
||||||
|
|
||||||
### Video file
|
# Video file
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
video_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['video_file']))).strip()
|
video_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['video_file']))).strip()
|
||||||
@@ -154,7 +206,14 @@ else:
|
|||||||
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
|
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
|
||||||
True
|
True
|
||||||
|
|
||||||
### GPS track file
|
# 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:
|
while True:
|
||||||
try:
|
try:
|
||||||
gps_track_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['gps_track']))).strip()
|
gps_track_path = unix_path(input('{}'.format(locale_toml['ui']['paths']['gps_track']))).strip()
|
||||||
@@ -167,35 +226,34 @@ else:
|
|||||||
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
|
print('{}\n'.format(locale_toml['ui']['paths']['path_err']))
|
||||||
True
|
True
|
||||||
|
|
||||||
### Output folder
|
# Output folder
|
||||||
output_folder = unix_path(input(locale_toml['ui']['paths']['output_folder']))
|
output_folder = unix_path(input(locale_toml['ui']['paths']['output_folder']))
|
||||||
|
|
||||||
## Parameters
|
# Parameters
|
||||||
print('\n{}'.format(locale_toml['ui']['info']['parameters_title']))
|
print('\n{}'.format(locale_toml['ui']['info']['parameters_title']))
|
||||||
|
|
||||||
### Timelapse video
|
# Timelapse video
|
||||||
timelapse = input(locale_toml['ui']['parameters']['timelapse'].format(user_agree, user_disagree))
|
timelapse = input(locale_toml['ui']['parameters']['timelapse'].format(user_agree, user_disagree)).upper()
|
||||||
|
|
||||||
if timelapse.upper() == user_agree:
|
if timelapse == user_agree:
|
||||||
### Timelapse framerate parameter
|
# Timelapse framerate parameter
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
timelapse_fps = int(input(locale_toml['ui']['parameters']['timelapse_fps'].format(min_timelapse_fps,
|
timelapse_fps = int(input(locale_toml['ui']['parameters']['timelapse_fps'].format(min_timelapse_fps,
|
||||||
max_timelapse_fps)))
|
max_timelapse_fps)))
|
||||||
if max_timelapse_fps >= timelapse_fps >= min_timelapse_fps:
|
if max_timelapse_fps >= timelapse_fps >= min_timelapse_fps:
|
||||||
frame_sampling = 1 / timelapse_fps
|
frame_sampling = float(1 / timelapse_fps)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps,
|
print('\n{}'.format(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps,
|
||||||
max_timelapse_fps))
|
max_timelapse_fps)))
|
||||||
True
|
True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps, max_timelapse_fps))
|
print('\n{}'.format(locale_toml['ui']['parameters']['timelapse_fps_err'].format(min_timelapse_fps, max_timelapse_fps)))
|
||||||
True
|
True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
### Frame sampling parameter
|
# Frame sampling parameter
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
frame_sampling = float(input(locale_toml['ui']['parameters']['frame_samp'].format(min_frame_samp,
|
frame_sampling = float(input(locale_toml['ui']['parameters']['frame_samp'].format(min_frame_samp,
|
||||||
@@ -204,15 +262,15 @@ else:
|
|||||||
if max_frame_samp >= frame_sampling >= min_frame_samp:
|
if max_frame_samp >= frame_sampling >= min_frame_samp:
|
||||||
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_samp_err'].format(min_frame_samp, max_frame_samp)))
|
||||||
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_samp_err'].format(min_frame_samp, max_frame_samp)))
|
||||||
True
|
True
|
||||||
|
|
||||||
## Frame height parameter
|
# Frame height parameter
|
||||||
min_frame_height = 480
|
if video_height <= max_frame_height:
|
||||||
max_frame_height = 6000
|
max_frame_height = int(round(video_height, 0))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -221,44 +279,46 @@ else:
|
|||||||
|
|
||||||
if max_frame_height >= frame_height >= min_frame_height:
|
if max_frame_height >= frame_height >= min_frame_height:
|
||||||
break
|
break
|
||||||
|
elif frame_height == 0:
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
print(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height))
|
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_height_err'].format(min_frame_height, max_frame_height))
|
print('\n{}'.format(locale_toml['ui']['parameters']['frame_height_err'].format(min_frame_height, max_frame_height)))
|
||||||
True
|
True
|
||||||
|
|
||||||
### Video start datetime parameter
|
# Video start datetime parameter
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
video_start_datetime = input(locale_toml['ui']['parameters']['video_start_datetime'])
|
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')
|
video_start_datetime_obj = datetime.strptime(video_start_datetime, '%Y-%m-%dT%H:%M:%S.%f')
|
||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(locale_toml['ui']['parameters']['video_start_datetime_err'])
|
print('\n{}'.format(locale_toml['ui']['parameters']['video_start_datetime_err']))
|
||||||
True
|
True
|
||||||
|
|
||||||
### Video recording timezone
|
# Video recording timezone
|
||||||
video_rec_timezone = input(locale_toml['ui']['parameters']['rec_timezone'])
|
video_rec_timezone = input(locale_toml['ui']['parameters']['rec_timezone'])
|
||||||
|
|
||||||
### Time offset parameter
|
# Time offset parameter
|
||||||
min_time_offset = -10.0
|
|
||||||
max_time_offset = 10.0
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
time_offset = float(input(locale_toml['ui']['parameters']['time_offset'].format(min_time_offset, max_time_offset)))
|
time_offset = float(input(locale_toml['ui']['parameters']['time_offset'].format(min_time_offset,
|
||||||
|
max_time_offset)))
|
||||||
|
|
||||||
if max_time_offset >= frame_sampling >= min_time_offset:
|
if max_time_offset >= frame_sampling >= min_time_offset:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset))
|
print('\n{}'.format(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset)))
|
||||||
True
|
True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset))
|
print('\n{}'.format(locale_toml['ui']['parameters']['time_offset_err'].format(min_time_offset, max_time_offset)))
|
||||||
True
|
True
|
||||||
|
|
||||||
### User-defined metadata
|
# User-defined metadata
|
||||||
|
print('\n{}'.format(locale_toml['ui']['info']['tags_title']))
|
||||||
|
|
||||||
make = input(locale_toml['ui']['metadatas']['make'])
|
make = input(locale_toml['ui']['metadatas']['make'])
|
||||||
model = input(locale_toml['ui']['metadatas']['model'])
|
model = input(locale_toml['ui']['metadatas']['model'])
|
||||||
author = input(locale_toml['ui']['metadatas']['author'])
|
author = input(locale_toml['ui']['metadatas']['author'])
|
||||||
@@ -266,25 +326,19 @@ else:
|
|||||||
# Video metadatas formatting
|
# Video metadatas formatting
|
||||||
print('\n{}'.format(locale_toml['processing']['reading_metadatas']))
|
print('\n{}'.format(locale_toml['processing']['reading_metadatas']))
|
||||||
|
|
||||||
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_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)
|
||||||
|
|
||||||
# Metadata recap
|
# 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)))
|
||||||
|
|
||||||
# Output folder creation
|
# Output folder creation
|
||||||
@@ -292,7 +346,7 @@ output_folder = '{}/{}'.format(output_folder, video_file_name)
|
|||||||
existing_path(output_folder)
|
existing_path(output_folder)
|
||||||
|
|
||||||
# Processes
|
# Processes
|
||||||
## Frame sampling + tagging (OpenCV + piexif)
|
# Frame sampling + tagging (OpenCV + piexif)
|
||||||
print('\n{}'.format(locale_toml['processing']['sampling']))
|
print('\n{}'.format(locale_toml['processing']['sampling']))
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
@@ -310,34 +364,26 @@ for i in tqdm(range(cv2_tqdm_range), unit=cv2_tqdm_unit):
|
|||||||
video.set(cv2.CAP_PROP_POS_MSEC, t)
|
video.set(cv2.CAP_PROP_POS_MSEC, t)
|
||||||
ret, frame = video.read()
|
ret, frame = video.read()
|
||||||
|
|
||||||
|
# Image resizing
|
||||||
|
if frame_height != 0:
|
||||||
|
resize_factor = video_height / frame_height
|
||||||
|
image_height = frame_height
|
||||||
|
image_width = int(round(video_height * resize_factor, 0))
|
||||||
|
|
||||||
|
frame = cv2.resize(frame, (image_width, image_height), interpolation=cv2.INTER_LANCZOS4)
|
||||||
|
|
||||||
frame_name = '{:05d}'.format(i)
|
frame_name = '{:05d}'.format(i)
|
||||||
image_name = "{}_f{}.jpg".format(video_file_name.split('.')[0], frame_name)
|
image_name = "{}_f{}.jpg".format(video_file_name.split('.')[0], frame_name)
|
||||||
image_path = "{}/{}".format(output_folder, image_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, 0x411111])
|
||||||
|
|
||||||
## Time tags formatting
|
# Time tags formatting
|
||||||
time_shift = i * frame_sampling
|
time_shift = i * frame_sampling
|
||||||
current_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_shift)
|
current_datetime_obj = video_start_datetime_obj + timedelta(seconds=time_shift)
|
||||||
current_datetime = current_datetime_obj.strftime('%Y:%m:%d %H:%M:%S')
|
current_datetime = current_datetime_obj.strftime('%Y:%m:%d %H:%M:%S')
|
||||||
current_subsec_time = int(int(current_datetime_obj.strftime('%f')) / 1000)
|
current_subsec_time = int(int(current_datetime_obj.strftime('%f')) / 1000)
|
||||||
|
|
||||||
# exif code
|
|
||||||
# with open(image_path, 'rb') as image_file:
|
|
||||||
# image = Image(image_file)
|
|
||||||
# image.make = make
|
|
||||||
# image.model = model
|
|
||||||
# image.author = author
|
|
||||||
# image.copyright = "{}, {}".format(author, video_start_datetime_obj.strftime('%Y'))
|
|
||||||
# image.datetime_original = current_datetime
|
|
||||||
# #image.offset_time_original = video_rec_timezone
|
|
||||||
#
|
|
||||||
# if current_subsec_time > 0 :
|
|
||||||
# image.subsec_time_original = str(current_subsec_time)
|
|
||||||
#
|
|
||||||
# with open(image_path, 'wb') as tagged_image_file:
|
|
||||||
# tagged_image_file.write(image.get_file())
|
|
||||||
|
|
||||||
# piexif code
|
# piexif code
|
||||||
image_exif = piexif.load(image_path)
|
image_exif = piexif.load(image_path)
|
||||||
|
|
||||||
@@ -374,3 +420,8 @@ geotagging = os.system(geotagging_cmd)
|
|||||||
|
|
||||||
# End
|
# End
|
||||||
input('\n{}'.format(locale_toml['ui']['info']['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.")
|
||||||
|
|
||||||
|
|||||||
19
video2geoframes_conf.toml
Normal file
19
video2geoframes_conf.toml
Normal 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"
|
||||||
Reference in New Issue
Block a user