YOULOOK CAR — SYNCHRONISATION VIDÉO

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

  1. Dans l'application web, ouvrir ÉCRAN DE SYNCHRO VIDÉO.
  2. Placer l'écran du téléphone dans le champ de la caméra.
  3. Lancer l'enregistrement vidéo, puis START dans l'application.
  4. Un flash part 2 secondes après le START, puis toutes les 30 secondes. On peut aussi en déclencher un à la main.
  5. 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

  1. 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.
  2. Lire l'horodatage correspondant dans le SRT : le bloc portant [sync: 1], ligne 2. Soit t_data.
  3. Calculer le décalage : offset = t_data − t_video.
  4. Appliquer : tout échantillon d'horodatage T se place à l'instant vidéo T − 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'incertitudeOrdre 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, longitudedegrés décimauxGPS de l'appareil
rel_alt, abs_altmGPS — rel_alt relative au départ
speedm/svéhicule (OBD) sinon GPS
rpmtr/minOBD PID 0x0C
throttle%OBD PID 0x11
coolant°COBD PID 0x05
engine_load%OBD PID 0x04
fuel_level%OBD PID 0x2F
accel_long, accel_latm/s²accéléromètre
g_forcegmodule de l'accélération avec pesanteur
yaw_rate°/sgyroscope
compass°boussole
syncrepè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à
brakinggdéduit de la décélération longitudinale — aucune norme OBD ne publie le freinage
boostbarcalculé : (PID 0x0B − PID 0x33) / 100
elapsedschronomètre — temps depuis le départ
lapchronomètre — numéro du tour en cours, à partir de 1
lap_timeschronomètre — durée du dernier tour bouclé
lap_mark1 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 canalSRT — cléUnitéNature
Chrono · temps écouléelapsedsniveau, 10 Hz
Chrono · tourlapniveau, entier ≥ 1
Chrono · dernier tourlap_timesniveau, vide avant le premier tour bouclé
Chrono · toplap_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.

OctetCanalOctetCanal
0xE0–0xECaccéléromètre, gyroscope, boussole 0xF0–0xF5position GPS
0xEDbatterie du téléphone 0xF6–0xF9chronomètre
0xEErepère de synchro 0xFAfreinage
0xEFsuralimentation 0x00–0xA6PID 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

Alignment procedure

  1. Compute per-frame mean luminance; find isolated peaks. Take the first frame of each peak (rising edge) → t_video.
  2. Read the timestamp of the matching [sync: 1] block → t_data.
  3. offset = t_data − t_video. A sample at time T belongs at video time T − offset.
  4. With two or more flashes, fit offset(T) = a·T + b by 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