Réaliser une photo-finish
Un fil X/Twitter de @Astro_Aure nous explique le principe de la photo-finish sportive. J'ai voulu recréer l'expérience avec une caméra et quelques lignes de code.
Je ne vais pas le paraphraser donc vous conseille de lire/aimer/partager ce fil qui m'a motivé à écrire cet article :
C'est parti pour un thread sur le thème des @Olympics #Paris2024 ! On va apprendre ensemble comment sont prises les fameuses photo-finish 📸 et comment elles permettent de mesurer le temps des athlètes 😉 Parlons technique 🔧 1/15 pic.twitter.com/wHrErXRIxa
— Aurélien Genin (@Astro_Aure) August 10, 2024
J'ai utilisé comme caméra un iPhone SE 2020 qui permet un enregistrement à haute cadence d'images (240fps) avec le mode "ralenti" et sur un trépieds pour avoir un cadre fixe, le tout orienté vers cette ligne d'arrivée simulée par une bande de barnier blanc. Nos athlètes sont symbolisés par 3 rouleaux de barnier 🇫🇷, ça fera bien la farce 🤡.
Traitement de la vidéo captée
Après avoir récupéré le rush, on le trim, sans perte de qualité et en retirant l'audio, pour ne garder que la partie utile.
1ffmpeg -i IMG_6168.MOV -ss 00:00:28 -t 00:00:02 -c copy -an finish.MOV
Quelques caractéristiques de la vidéo
1% mediainfo finish.MOV
2General
3Complete name : finish.MOV
4Format : MPEG-4
5Format profile : QuickTime
6Codec ID : qt 0000.02 (qt )
7File size : 9.99 MiB
8Duration : 2 s 14 ms
9Overall bit rate : 41.6 Mb/s
10Frame rate : 240.191 FPS
11Writing application : Lavf61.1.100
12
13Video
14ID : 1
15Format : HEVC
16Format/Info : High Efficiency Video Coding
17Format profile : Main@L5.1@High
18Codec ID : hvc1
19Codec ID/Info : High Efficiency Video Coding
20Duration : 2 s 14 ms
21Source duration : 1 s 45 ms
22Bit rate : 80.1 Mb/s
23Width : 1 920 pixels
24Height : 1 080 pixels
25Display aspect ratio : 16:9
26Frame rate mode : Variable
27Frame rate : 240.191 FPS
28Minimum frame rate : 240.000 FPS
29Maximum frame rate : 266.667 FPS
30Color space : YUV
31Chroma subsampling : 4:2:0
32Bit depth : 8 bits
33Bits/(Pixel*Frame) : 0.161
34Stream size : 9.98 MiB (100%)
35Source stream size : 9.98 MiB (100%)
36Title : Core Media Video
37Color range : Limited
38Color primaries : BT.709
39Transfer characteristics : BT.709
40Matrix coefficients : BT.709
41mdhd_Duration : 1037
42Codec configuration box : hvcC
Pour plus de compatibilité (et reduire le débit), on va passer la vidéo de 240fps à 30fps, sans réduire la qualité. Pour celà l'astuce est de l'extraire de son conteneur et de regénérer le PTS.
1ffmpeg -i finish.MOV -map 0:v -c:v copy -bsf:v hevc_mp4toannexb finish.h265
2ffmpeg -fflags +genpts -r 30 -i finish.h265 -c:v copy finish-30.MOV
3rm finish.h265
Et on arrive à la vidéo du dessus.
Conversion en images successives
D'après le processus de génération, on doit traiter des images successives afin d'en extraire une colonne pour chaque.
1ffmpeg -i finish-30.MOV finish-%04d.png
Ce qui nous génère les 249 fichiers suivants :
1finish-0001.png
2finish-0002.png
3finish-0003.png
4...
5finish-0247.png
6finish-0248.png
7finish-0249.png
Chaque image est Full HD (1920x1080), on choisit d'extraire la colonne 955 qui correspond à peu près à la ligne blanche verticale d'arrivée.
Chaque colonne ainsi extraite va représenter une colonne dans l'image finale. La colonne extraite de finish-0001.png
sera la colonne la plus à droite de l'image finale, la colonne extraite de finish-0249.png
sera la colonne la plus à gauche de l'image finale.
La captation étant à 240 fps, il y a 4.1667ms entre 2 photos. Chaque colonne de l'image finale représente donc 4.1667ms.
L'assemblage de la photo
Ce mini script de démonstration est écrit en PHP avec sa bibliothèque graphique GD, le principe est bien sûr adaptable avec votre langage préféré.
1#!/usr/bin/env php
2<?php
3
4$photos = glob(__DIR__ . '/*.png');
5
6// la largeur correspond au nombre de photos utiles captées
7$width = count($photos);
8$height = imagesy(imagecreatefrompng($photos[0]));
9// la colonne à garder pour chaque photo
10$col = 955;
11
12// initialisation d'un tableau à 2 dimensions de 249x1080
13$pixels = array_fill(0, $width, array_fill(0, $height, 0));
14
15// sauvegarde dans le tableau des couleurs des pixels des colonnes
16foreach ($photos as $x => $photo) {
17 $current_photo = imagecreatefrompng($photo);
18 for ($y = 0; $y < $height; $y++) {
19 $rgb = imagecolorat($current_photo, $col, $y);
20 $pixels[$width - $x][$y] = $rgb;
21 }
22}
23
24// génération de la photo finale
25$photo_finish = imagecreatetruecolor($width, $height);
26for ($x = 0; $x < $width; $x++) {
27 for ($y = 0; $y < $height; $y++) {
28 $rgb = $pixels[$x][$y];
29 $r = ($rgb >> 16) & 0xFF;
30 $g = ($rgb >> 8) & 0xFF;
31 $b = ($rgb >> 0) & 0xFF;
32 $pixel = imagecolorallocate($photo_finish, $r, $g, $b);
33 imagesetpixel($photo_finish, $x, $y, $pixel);
34 }
35}
36
37imagepng($photo_finish, 'photo-finish.png');
38imagedestroy($photo_finish);
Le résultat