Comment relier, à l'image près, une vidéo et un fichier de télémétrie qui n'ont aucune horloge commune.
Le problème
Une caméra et un téléphone comptent le temps chacun de leur côté. Le fichier vidéo commence à zéro ; la télémétrie porte l'heure du téléphone. Rien, dans l'un ou l'autre, ne dit quelle image correspond au premier échantillon. Les métadonnées de création du fichier vidéo ne suffisent pas : elles sont arrondies à la seconde, décalées par le temps de démarrage de l'enregistrement, et parfois écrites à la fin du plan.
Une erreur d'une demi-seconde suffit à faire apparaître un pic de freinage alors que la voiture est déjà à l'arrêt.
La solution : le clap
C'est le principe du clap de cinéma, transposé. La caméra filme l'écran du téléphone, qui émet un flash blanc plein écran à des instants dont l'horodatage est connu à la milliseconde et inscrit dans les données.
Retrouver l'image du flash dans la vidéo suffit alors à caler les deux sources — sans horloge commune, sans réseau, sans matériel.
Pourquoi un flash plein écran plutôt qu'un compteur affiché ? Un aplat blanc occupant tout le cadre survit au flou de bougé, à la mise au point ratée, au mauvais angle et à la faible lumière. Des chiffres à lire ne survivent à aucun des quatre. Et surtout : un flash se détecte automatiquement, par simple mesure de luminance moyenne, sans OCR.
Marche à suivre
- Dans l'application web, ouvrir ÉCRAN DE SYNCHRO VIDÉO.
- Placer l'écran du téléphone dans le champ de la caméra.
- Lancer l'enregistrement vidéo, puis START dans l'application.
- Un flash part 2 secondes après le START, puis toutes les 30 secondes. On peut aussi en déclencher un à la main.
- Le téléphone peut ensuite sortir du champ : les flashs suivants ne sont utiles que s'ils restent filmés.
Ce que contiennent les fichiers
Chaque flash est inscrit dans les deux formats exportés.
SRT — le champ [sync: 1]
Le bloc de sous-titre couvrant l'instant du flash porte le champ
[sync: 1]. Sa deuxième ligne donne l'horodatage absolu.
1
00:00:02,000 --> 00:00:02,050
<font size="28">SrtCnt : 41, DiffTime : 50ms
2026-08-21 20:03:44.153
[latitude: 46.429520] [longitude: 6.920797] [speed: 12.4] [g_force: 1.01] [sync: 1] </font>
MDF4 — le canal « Repère de synchro »
Fichier ASAM MDF 4.10. Le canal Repère de synchro vaut
1 sur la seule ligne du flash, et NaN partout ailleurs.
C'est un événement, jamais un niveau : il n'est pas prolongé par le
maintien de valeur appliqué aux autres grandeurs.
L'algorithme de resynchronisation
- Détecter le flash dans la vidéo. Calculer la luminance moyenne de
chaque image. Le flash est un pic isolé très au-dessus de la médiane
locale. Retenir la première image du pic — c'est le front montant
qui fait référence, pas le milieu du palier. Noter son instant
t_video, en secondes depuis le début du fichier. - Lire l'horodatage correspondant dans le SRT : le bloc portant
[sync: 1], ligne 2. Soitt_data. - Calculer le décalage :
offset = t_data − t_video. - Appliquer : tout échantillon d'horodatage
Tse place à l'instant vidéoT − offset.
Mesurer et corriger la dérive
Deux flashs ou plus permettent de faire mieux qu'un simple décalage. Les horloges de la caméra et du téléphone n'avancent pas exactement à la même vitesse : sur un plan de vingt minutes, l'écart atteint couramment quelques dizaines de millisecondes.
offset(T) = a · T + b
avec a, b obtenus par régression linéaire
sur l'ensemble des couples (t_data, t_video)
Avec un flash toutes les 30 secondes, la régression est très surdéterminée : la dérive résiduelle devient négligeable devant la durée d'une image.
Précision réellement atteignable
| Source d'incertitude | Ordre de grandeur |
|---|---|
| Quantification par la cadence vidéo (30 i/s) | ± 33 ms |
| Latence d'affichage de la dalle (60 Hz) | ± 16 ms |
| Pas d'échantillonnage de la télémétrie | ± 25 ms (pas de 50 ms) |
| Total réaliste | ± 50 ms environ |
Ce que cette méthode ne corrige pas. Elle cale les données sur la vidéo, elle ne rend pas les données plus fines. Le GPS d'un téléphone donne un point par seconde : une trajectoire restera interpolée entre deux points, quelle que soit la qualité du calage. De même, un obturateur rolling déforme un flash sur quelques lignes de l'image — sans effet sur la détection, mais qui interdit de prétendre à mieux que l'image.
Détection automatique — code de référence
# Détection du clap Youlook Car dans une vidéo.
# Dépendances : OpenCV et NumPy, rien d'autre.
import cv2, numpy as np
def instants_des_flashs(chemin, tuiles=8, seuil=6.0, sous_ech=4):
"""Instants, en secondes, des fronts montants du flash.
L'image est découpée en tuiles×tuiles cases. C'est le point clé : filmé
d'un support, l'écran du téléphone n'occupe que quelques pour cent d'un
cadre 4K. Sur la moyenne de l'image entière, un flash plein écran ne
déplace alors la luminance que d'une fraction de point et se noie dans le
bruit de scène. Case par case, il sature.
La case retenue est celle dont la luminance s'écarte le plus fortement
vers le haut : c'est celle qui contient l'écran, trouvée toute seule.
"""
cap = cv2.VideoCapture(chemin)
fps = cap.get(cv2.CAP_PROP_FPS)
series = []
while True:
ok, img = cap.read()
if not ok:
break
g = img[::sous_ech, ::sous_ech].mean(axis=2)
h, w = g.shape
bloc = g[:h // tuiles * tuiles, :w // tuiles * tuiles]
bloc = bloc.reshape(tuiles, h // tuiles, tuiles, w // tuiles)
series.append(bloc.mean(axis=(1, 3)).ravel())
series = np.asarray(series) # (images, tuiles²)
# Médiane glissante sur 2 s : insensible aux changements de scène lents.
k = max(3, int(fps * 2) | 1)
meilleur, resultat = -1.0, []
for c in range(series.shape[1]):
lum = series[:, c]
fond = np.array([np.median(lum[max(0, i - k // 2): i + k // 2 + 1])
for i in range(len(lum))])
ecart = max(float(np.median(np.abs(lum - fond))), 1e-6)
force = (lum - fond) / ecart
if force.max() > meilleur:
haut = force > seuil * 3
# FRONT MONTANT seulement : première image de chaque palier.
fronts = np.flatnonzero(haut & ~np.r_[False, haut[:-1]])
meilleur = force.max()
resultat = [float(i) / fps for i in fronts]
return resultat
Vérifiez qu'il y a bien un flash. Sur une capture réelle où l'écran n'était pas dans le champ, la méthode par moyenne globale renvoyait onze « pics » — tous du bruit de scène, à un demi-point de luminance sur 255. La méthode par cases, elle, atteint couramment un rapport signal/bruit de plusieurs centaines lorsqu'un vrai flash est présent. Si la force du pic retenu reste faible, concluez à l'absence de flash plutôt qu'à sa position.
Grammaire complète du SRT
Le format suit la grammaire de télémétrie DJI, pour rester lisible par les outils existants. Les clés supplémentaires sont ignorées sans dommage par un lecteur qui ne les connaît pas.
| Clé | Unité | Origine |
|---|---|---|
latitude, longitude | degrés décimaux | GPS de l'appareil |
rel_alt, abs_alt | m | GPS — rel_alt relative au départ |
speed | m/s | véhicule (OBD) sinon GPS |
rpm | tr/min | OBD PID 0x0C |
throttle | % | OBD PID 0x11 |
coolant | °C | OBD PID 0x05 |
engine_load | % | OBD PID 0x04 |
fuel_level | % | OBD PID 0x2F |
accel_long, accel_lat | m/s² | accéléromètre |
g_force | g | module de l'accélération avec pesanteur |
yaw_rate | °/s | gyroscope |
compass | ° | boussole |
sync | — | repère de synchro : présent uniquement sur le bloc du flash |
battery | % | batterie du téléphone — une capture qui s'arrête sans prévenir s'explique souvent par là |
braking | g | déduit de la décélération longitudinale — aucune norme OBD ne publie le freinage |
boost | bar | calculé : (PID 0x0B − PID 0x33) / 100 |
elapsed | s | chronomètre — temps depuis le départ |
lap | — | chronomètre — numéro du tour en cours, à partir de 1 |
lap_time | s | chronomètre — durée du dernier tour bouclé |
lap_mark | — | 1 départ · 2 tour · 3 arrivée — présent uniquement sur le bloc du geste |
Deux conventions à connaître. La vitesse est en m/s, jamais en km/h — c'est l'unité de DJI. Et une grandeur absente d'un bloc est simplement absente : elle n'est jamais remplacée par un zéro, qui se lirait comme une mesure réelle.
Le chronomètre — clé de lecture
Trois gestes sont enregistrés pendant la capture : départ, tour, arrivée. Ils produisent quatre canaux, présents à la fois dans le SRT et dans le MDF4.
| MDF4 — nom du canal | SRT — clé | Unité | Nature |
|---|---|---|---|
Chrono · temps écoulé | elapsed | s | niveau, 10 Hz |
Chrono · tour | lap | — | niveau, entier ≥ 1 |
Chrono · dernier tour | lap_time | s | niveau, vide avant le premier tour bouclé |
Chrono · top | lap_mark | — | événement — une seule ligne |
Le canal « top » est un événement, pas un niveau. Il ne porte de valeur que sur l'échantillon exact du geste ; partout ailleurs il est vide. Un lecteur ne doit jamais le prolonger, sous peine de transformer une arrivée unique en une arrivée qui dure trois secondes. Les trois autres canaux, eux, sont des niveaux : ils se lisent à tout instant.
Ses trois valeurs : 1 = départ · 2 = passage sur la ligne · 3 = arrivée.
Reconstituer les tours
Tout est déductible du seul canal lap_mark, sans état :
tops = [(t, v) for t, v in canal("lap_mark") if v is not None]
depart = next(t for t, v in tops if v == 1)
lignes = [t for t, v in tops if v in (2, 3)]
tours = []
precedent = depart
for t in lignes:
tours.append({"numero": len(tours) + 1, "duree": t - precedent})
precedent = t
Le numéro de tour se lit aussi directement dans lap, et la durée
du dernier tour bouclé dans lap_time — les deux façons doivent
donner le même résultat. La seconde évite d'avoir à parcourir le fichier, la
première reste vraie même si l'échantillonnage des niveaux a manqué des points.
Trois pièges.
— Le chronomètre est facultatif : une capture sans chrono ne contient
aucun de ces quatre canaux, et un lecteur doit le supporter.
— lap_time est vide pendant le premier tour : aucun
tour n'a encore été bouclé. Ce n'est pas zéro.
— Un redépart est possible en cours de capture : on peut donc
rencontrer plusieurs lap_mark: 1, et elapsed repart
alors à zéro. Le dernier départ fait foi.
Numérotation des canaux
Les fichiers portent des noms de canaux, pas des numéros ; cette table n'existe que pour un outil qui voudrait s'y référer directement.
| Octet | Canal | Octet | Canal |
|---|---|---|---|
0xE0–0xEC | accéléromètre, gyroscope, boussole | 0xF0–0xF5 | position GPS |
0xED | batterie du téléphone | 0xF6–0xF9 | chronomètre |
0xEE | repère de synchro | 0xFA | freinage |
0xEF | suralimentation | 0x00–0xA6 | PID de la norme SAE J1979 |
Le freinage n'est pas sur 0xED. C'eût été
le voisin naturel des autres capteurs, mais cet octet porte déjà la batterie du
téléphone dans tous les fichiers produits jusqu'ici. Réutiliser un numéro occupé
rendrait illisible tout ce qui a été enregistré avant.
Base de temps
elapsed compte depuis le départ du chronomètre ; l'axe de temps
du fichier, lui, compte depuis le début de la capture. Les deux ne
coïncident pas, et c'est voulu : on lance l'enregistrement avant de s'élancer.
Le décalage se lit sur la ligne du lap_mark: 1. Pour placer le
chronomètre sur la vidéo, on applique d'abord le recalage du clap, puis on lit
elapsed tel quel.
English — specification for overlay software
Video and telemetry come from two independent clocks. To align them, the camera films the phone screen, which emits a full-screen white flash at instants whose absolute timestamps are written into the data.
Where the marker lives
- SRT — the subtitle block covering the flash carries the field
[sync: 1]. Line 2 of that block is the absolute timestamp, formattedYYYY-MM-DD hh:mm:ss.mmm(local time of the recording device). - Stopwatch — SRT keys
elapsed(s),lap(integer, from 1),lap_time(s, duration of the last completed lap, absent during lap 1) andlap_mark(1 start · 2 lap · 3 finish). MDF4 channelsChrono · temps écoulé,Chrono · tour,Chrono · dernier tour,Chrono · top.lap_markis an event, not a level: it carries a value on exactly one sample and must never be forward-filled. A restart mid-capture emits a secondlap_mark: 1and resetselapsed; the last start wins. All four channels are absent when the stopwatch was not used. - MDF4 (ASAM 4.10) — channel
Repère de synchroequals1on the flash row only,NaNelsewhere. It is an event, never held across rows.
Alignment procedure
- Compute per-frame mean luminance; find isolated peaks. Take the
first frame of each peak (rising edge) →
t_video. - Read the timestamp of the matching
[sync: 1]block →t_data. offset = t_data − t_video. A sample at timeTbelongs at video timeT − offset.- With two or more flashes, fit
offset(T) = a·T + bby linear regression to also remove clock drift between camera and phone.
Achievable accuracy
About ±50 ms: ±33 ms from a 30 fps frame grid, ±16 ms from display latency, ±25 ms from the 50 ms telemetry step. Flashes repeat every 30 s, so drift over a long clip is measurable rather than assumed.
Notes
- Speed is in m/s, following DJI convention — not km/h.
- A missing quantity is omitted from the block. It is never written as
0, which would read as a real measurement. - Sample values are stored as 32-bit floats. For latitude this is about 0.4 m of quantisation — well below GNSS error, but worth knowing before differentiating a position series.