Les Flots liés
Cet article poursuit le propos de la vidéo de présentation éponyme, je vous invite à la visionner si ce n'est pas déjà fait.
Il y a deux ans, je publiais un article traitant du datamoshing, une technique de corruption vidéo aux effets visuels intriguant. En me renseignant sur le sujet, j'ai d'abord implémenté un outil naÏf se contentant de supprimer les images de référence d'un flux H.264. Les résultats étaient corrects mais pas à la hauteur des effets professionnels. Depuis, j'ai découvert l'outil FFglitch, dont la documentation révèle les principaux écueils de ma méthode. Mais alors, ignorant cela, j'ai cherché une méthode alternative, centrée sur un des aspects majeurs (selon moi) du datamoshing : les mouvements apparents de la vidéo suivant l'image de référence perdue s'appliquent à la dernière image de la vidéo précédente. Ce mouvement apparent des pixels est un concept bien formalisé dans le domaine de la vision par ordinateur : le flux optique. Ainsi, j'étais arrivé au transfert de flux optique. Le premier script était très basique, plus une preuve de concept qu'un outil pratique. Un aperçu est disponible sur YouTube — la vidéo est désormais non-répertoriée, je la considère comme bâclée. Récemment, je me suis repenché sur ce projet avec l'objectif de publier quelque-chose d'opérationnel : le module Transflow — contraction d'optical flow transfer — accompagné d'exemples concrets, à vocation plus artistique.
À titre de références, voici quelques travaux m'ayant inspiré dans ma démarche (voir également la section à propos du mordançage) :
- Flowfields par Emil Dziewanowski
- gush par Adam Ferris
- Motion Extraction par Posy
- ofxFlowtools par Matthias Oostrik
En comparaison à mon dernier projet d'envergure — le détecteur et visualisateur de rythme Beatviewer — le développement ne fut pas trop complexe.
J'ai rapidement opté pour les algorithmes d'OpenCV pour l'extraction du flux. Par curiosité, j'ai tout de même parcouru les articles majeurs du domaine, que je référence ici, à toutes fins utiles :
- Lucas, B. D., & Kanade, T. (1981). An iterative image registration technique with an application to stereo vision. In IJCAI’81 : 7th international joint conference on Artificial intelligence (Vol. 2, pp. 674-679).
- Horn, B. K., & Schunck, B. G. (1981). Determining optical flow. Artificial intelligence, 17(1-3), 185-203.
- Farnebäck, G. (2002). Polynomial expansion for orientation and motion estimation (Doctoral dissertation, Linköping University Electronic Press).
- Farnebäck, G. (2003). Two-frame motion estimation based on polynomial expansion. In Image Analysis : 13th Scandinavian Conference, SCIA 2003 Halmstad, Sweden, June 29–July 2, 2003 Proceedings 13 (pp. 363-370). Springer Berlin Heidelberg.
Méthodes de cumul des flux
Mon principal défi fut de proposer une méthode pour appliquer le flux optique à une vidéo. Dans la version basique, il ne s'appliquait qu'à une image, modifiée à chaque étape. Pour atteindre le même résultat avec une vidéo, il faut pouvoir appliquer l'ensemble des transformations en une passe. Il faut donc cumuler les flux. Il y a plusieurs façons de faire cela, la plus directe étant d'appliquer les transformations à une cartographie UV, le mécanisme est identique à l'application naïve sur une image mais devient peut désormais s'appliquer à tout support.
Comme expliqué dans la vidéo, cette technique ne me satisfaisait pas totalement car elle laissait des vides théoriques : qu'afficher lorsqu'un pixel quitte sa position d'origine ? qu'afficher lorsque plusieurs pixels se rencontrent ? Pour y répondre, j'ai d'abord pensé à laisser les pixels se comporter comme des particules de couleur. Quand elles se déplacent, elles laissent du vide derrière elles. Quand elles se superposent, elles s'empilent, et la couleur résultante dépend de la composition de la pile — e.g. une composition additive ou soustractive. Cette représentation me semble plus naturelle mais difficile à calculer efficacement. Pour une minute vidéo en haute résolution, il faut compter plusieurs heures de rendu.
Heureusement, j'ai trouvé une façon plus élégante de résoudre ce problème, en inversant la direction de calcul du flux optique : au lieu de calculer le déplacement des pixels du passé vers le futur, on calcule du futur vers le passé, le déplacement des pixels futurs peut ainsi être interprété comme leur origine. Cette origine est surjective, et peut être cumulée en sommant les flux. Voici une version allégée de la logique du code en Python :
import cv2, numpy
width, height = 1920, 1080
capture = cv2.VideoCapture("flow.mp4")
prev_gray = None
accumulator = numpy.zeros((height, width, 2), dtype=float)
farneback_args = {"flow": None, "pyr_scale": 0.5,
"levels": 3, "winsize": 15, "iterations": 3,
"poly_n": 5, "poly_sigma": 1.2, "flags": 0}
success = True
while success:
success, frame = capture.read()
next_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if prev_gray is not None:
flow = cv2.calcOpticalFlowFarneback(
prev=next_gray,
next=prev_gray,
**farneback_args)
accumulator += flow
prev_gray = numpy.copy(next_gray)
capture.release()
Les transformations, depuis le début de la vidéo, sont accumulées dans un seul tableau, accumulator
. On peut alors les appliquer d'une passe sur n'importe quelle image :
# L'image à transformer
bitmap = numpy.array(PIL.Image.open("bitmap.jpg"))[:,:,:3]
shape = (height, width)
# contient les numéros de colonne de chaque cellule
basex = numpy.broadcast_to(
numpy.arange(width),
shape)
# contient les numéros de ligne de chaque cellule
basey = numpy.broadcast_to(
numpy.arange(height)[:,numpy.newaxis],
shape)
# Le flux est arrondi à l'entier le plus proche,
# pour un déplacement en nombre de pixels
accumulator_int = numpy.round(accumulator).astype(int)
# L'image transformée
transformed = bitmap[
basey + accumulator_int[:,:,1],
basex + accumulator_int[:,:,0]]
Les rendus sont très différents entre ces trois techniques. L'empilement est granuleux, la somme est continue, la cartographie est un entre-deux.
Mordançage
Une des mes principaux objectifs était de reproduire un effet utilisé en photographie, le mordançage. Un solvant décolle les noirs d'une photo développée, qui flottent dans le bac de développement, permettant de leur donner un mouvement avant de sortir la photo du bac et figer à nouveau les noirs.
J'ai pensé un moment à simuler le comportement d'un fluide pour déplacer les pixels de l'image, mais le transfert de flux me permet d'utiliser la captation d'un véritable flux. Cela laisse moins de contrôle, mais en un sens lie résultat à une réalité physique, ce qui en accroît l'intérêt — pour moi tout du moi. Ainsi, j'ai filmé quelques rivières ou cascade.
Pour simuler la différence de mobilité des différentes parties de l'image — e.g. pour laisser bouger les noirs et laisser statiques les blancs — j'ai intégré la possibilité d'appliquer un masque au flux ou à la réinitialisation des pixels. Pour plus de détails, je vous réfère à l'exemple donné dans le guide d'utilisation du module.
À noter que j'ai également tenté de reproduire cet effet physiquement, dans ce que j'appelle un réservoir d'ondulations : une caisse peinte en blanche contenant de l'eau, que l'on penche pour créer des ondulations, et où est placé un obstacle imprimé en 3D avec la forme de la partie de l'image à garder statique. Quelques mesures sont nécessaires pour s'assurer que la taille de l'obstacle filmé corresponde à l'image initiale sur laquelle le flux vidéo sera appliqué. Malheureusement, je n'ai pas obtenu de résultats probants, le dispositif reste à améliorer.
Le temps réel
Rapidement, l'idée d'utiliser cet outil en temps réel est survenue, notamment dans la perspective d'installations artistiques. Malheureusement, le script Python dont je disposais n'était clairement pas optimisé — le traitement d'une vidéo en 1080p se fait au rythme de 4 images par seconde, cela fait remonter des souvenirs. J'ai donc exploré plusieurs voies pour améliorer cela : implémentation de diverses méthodes d'estimation du mouvement ou de calcul du flux optique — Nvidia permet d'utiliser la carte graphique pour cela mais mon matériel ne le supporte pas, ce qui m'empêche également de l'utiliser dans TouchDesigner, l'utilisation de numba, de CUDA ou pycuda. Ces chemins n'ont mené qu'à des lourdeurs ou des lenteurs.
Sur la route, j'ai tout de même atteint une presque-solution. Tout d'abord, j'ai réimplémenté le cœur du programme en C++, pour éliminer les lenteurs inhérentes à Python. Le flux optique peut être calculé avec l'algorithme de Gunar Farnebäck, mais aussi extrait des vecteurs de mouvements calculés lors de l'encodage de la vidéo. En éliminant ainsi la partie la plus longue du calcul, j'ai pu atteindre 30 images par seconde pour du 720p. Cela peut également fonctionner pour des webcams, moyennant un peu de bidouillage et de latence.
Mais, désireux d'obtenir quelque chose de vraiment fluide et facile à utiliser et partager, je me suis tenté, pour la première fois, au développement avec l'API JavaScript WebGL, afin de pouvoir facilement exploiter la carte graphique. La prise en main n'est pas trop complexe même si elle nécessite des notions spécifiques au développement sur GPU. La principale difficulté fut d'implémenter les algorithmes de calcul du flux optique dans des shaders OpenGL. Faute de bibliothèque existante, j'ai bricolé des implémentations de la méthode de Lucas-Kanade et de Horn-Schunck. Bilan mitigé : les résultats sont fluides et l'effet fonctionne, cependant, ils semblent moins précis qu'avec la méthode de Farnebäck, que je n'ai pas eu le courage d'implémenter. Sur GitHub, @Volcomix/optical-flow-web propose une implémentation de l'expansion polynomiale pour WebGL, il faudrait prendre le temps de poursuivre ce travail. Si lacunaire soit-elle, mon implémentation est disponible en ligne, et m'aura permis la réalisation de quelques beaux plans.
Expérimentation in-situ
Le temps réel fonctionnel, une des applications envisagées était la création d'une fenêtre portable dont le mouvement de face impactait la vue arrière. Concrètement, il s'agit d'un écran disposé sur un pied, avec deux webcams, l'une face avant dont le flux est extrait, l'autre face arrière sur laquelle le flux est appliqué. Idéalement, il aurait fallu une structure stable, discrète et légère, fonctionnant sur batterie, si possible avec un ordinateur intégré, sinon avec un ordinateur portable installé au pied.
Après quelques tentatives de bricolage en impression 3D ou en bois, il apparaît que quelques investissements seraient nécessaires pour construire la structure répondant aux critères demandés. Aussi, pour la vidéo de présentation, j'ai simplement utilisé un ordinateur portable, avec sa webcam intégrée et une seconde webcam externe.
Durant ce développement, j'ai également pensé à utiliser un téléphone portable possédant — la plupart du temps — deux caméras sur deux faces, et — la plupart du temps — la puissance nécessaire pour calculer le transfert de flux. Malheureusement, si l'utilisation des deux caméras en même temps est théoriquement possible dans l'API Camera2 d'Android, seuls quelques modèles disposent du matériel nécessaire : il n'y a souvent qu'un processeur d'image pour les deux caméras.
Pistes futures
Si la curiosité technique reste le point de départ de ce projet, j'ai voulu m'en détacher en partie, pour me donner l'occasion de créer quelque chose de plus universel, plus esthétique, plus chaud. Cela m'est un peu contre nature, mais en comparaison au projet de Beatviewer, la différence est notable. Je suis notamment heureux d'avoir pu inclure l'aide et les travaux d'amis. Et puis, je commence à accepter l'idée que l'expérimentation technique constitue déjà une démarche légitime.
Si cette première publication suscite un quelconque intérêt, peut-être me lancerais-je dans la construction d'une installation plus exploitable. D'autres idées m'intéresseraient, comme la constitution d'une galerie de mouvements, la réalisation d'un court-métrage basé sur cette technique — où un personnage n'apparaîtrait que par sa distorsion de l'espace — ou l'exploitation d'illusions optiques liées aux mouvements apparents — comme l'enseigne du barbier. Enfin, pour le moment, je vais changer un peu de domaine et m'aérer l'esprit.
Mise à jour 05/12/24. En discutant des possibilités du programme, on m'a suggéré la possibilité de fusionner plusieurs sources de flux. C'est maintenant possible, et détaillé dans une nouvelle entrée du guide d'utilisation.