Transformer une vidéo en stop motion : un outil d’analyse visuelle

T

Dans le cadre de l’analyse multimodale en Sciences Humaines et Sociales, ce script Python propose une méthode pour transformer une vidéo (YouTube ou en local .mp4) en stop motion (animation image par image).
Que ce soit depuis un lien YouTube ou un fichier vidéo local au format .mp4, l’utilisateur peut générer une nouvelle version de la vidéo où les images sont extraites à une fréquence d’images réduite.
Ce procédé permet de ralentir visuellement le déroulement d’une scène tout en conservant sa dynamique globale, à la manière d’un arrêt sur image animé.

https://www.youtube.com/watch?v=JN7tFEbz8vg

 

Le principe : alors qu’une vidéo classique est fluide à partir de 25 images par seconde (standard européen) ou 30 fps (standard américain), ce script extrait des images à une fréquence plus basse, comprise entre 4 et 16 fps (au choix). Cette “réduction du rythme visuel” produit un effet de stop motion : les mouvements apparaissent plus saccadés, à l’oeil humain, ce qui facilite l’analyse des micro-gestes, des expressions ou des régulations corporelles.
Autrement dit, on transforme une séquence fluide en une série de moments visuellement cadencés.

L’utilisateur peut choisir librement la fréquence d’image à laquelle il souhaite extraire la vidéo (4, 6, 8, 10, 12, 14 ou 16 fps). Une fois ces images extraites, elles sont montées dans une nouvelle vidéo encodée à 25 fps.

Par exemple, si vous choisissez d’extraire les images à une fréquence plus basse (ex : 8 fps), le script sélectionne 8 images par seconde parmi les 25 d’origine. Cela veut dire qu’il en ignore 17 sur 25. Dans un second temps, la vidéo est reconstruction à 25 fps avec ces images.
La vidéo résultante est encodée à 25 fps créant un effet de saccade volontaire (effet stop motion).

En option, il est également possible de superposer à la vidéo le flux optique (opticalflow) permettant de visualiser les déplacements de pixels entre deux images successives.
Cela met en évidence les zones de mouvement dans l’image.

https://www.youtube.com/watch?v=4RHDbrf36oY

 

Ce script ne constitue qu’un exemple parmi d’autres des possibilités de visualisation offertes par une approche multimodale en Sciences Humaines et Sociales.

Dans la lignée des travaux de Gregory Bateson (croisant observation participante et chronophotographie), il vise à explorer les situations de communication paradoxale, à détecter les ambivalences gestuelles et à mettre en lumière les silences porteurs de sens.

En rendant visibles des détails souvent imperceptibles à vitesse réelle, ce script contribue à approfondir l’analyse des interactions humaines. Il offre aux chercheurs un outil pour mieux saisir la complexité des échanges à travers l’image en mouvement.

Le code source

Avant de commencer, il est nécessaire d’installer ffmpeg localement sur votre Mac ou PC.

Ensuite, avant d’exécuter le script, vous devrez installer les bibliothèques nécessaires via le terminal de votre éditeur de code.

pip install streamlit opencv-python yt-dlp

Le code source complet 😉

# pip install streamlit opencv-python yt-dlp

import streamlit as st
import cv2
import os
import tempfile
import subprocess

def telecharger_video_yt_dlp(url, dossier_temporaire):
    """
    Télécharge une vidéo YouTube avec yt-dlp.
    """
    commande = [
        "yt-dlp",
        "-f", "mp4",
        "-o", os.path.join(dossier_temporaire, "video_originale.%(ext)s"),
        url
    ]
    subprocess.run(commande, check=True)
    for fichier in os.listdir(dossier_temporaire):
        if fichier.endswith(".mp4"):
            return os.path.join(dossier_temporaire, fichier)
    return None

def appliquer_optical_flow(images):
    """
    Applique la visualisation du flux optique sur les images successives.
    """
    images_avec_flow = []
    for i in range(len(images) - 1):
        img1 = cv2.cvtColor(images[i], cv2.COLOR_BGR2GRAY)
        img2 = cv2.cvtColor(images[i + 1], cv2.COLOR_BGR2GRAY)
        flow = cv2.calcOpticalFlowFarneback(img1, img2, None,
                                            0.5, 3, 15, 3, 5, 1.2, 0)
        vis = images[i].copy()
        h, w = img1.shape
        step = 16
        for y in range(0, h, step):
            for x in range(0, w, step):
                fx, fy = flow[y, x]
                cv2.arrowedLine(vis, (x, y), (int(x + fx), int(y + fy)),
                                (0, 255, 0), 1, tipLength=0.4)
        images_avec_flow.append(vis)
    images_avec_flow.append(images[-1])  # Dernière image sans flow
    return images_avec_flow

def extraire_images_echantillonnées(chemin_video, dossier_sortie, fps_cible, avec_flow=False):
    """
    Extrait les images à intervalle régulier (effet stop motion), avec option Optical Flow.
    """
    cap = cv2.VideoCapture(chemin_video)
    fps_original = cap.get(cv2.CAP_PROP_FPS)
    ratio_saut = max(1, int(round(fps_original / fps_cible)))

    images_extraites = []
    compteur = 0
    index = 0

    while cap.isOpened():
        succès, image = cap.read()
        if not succès:
            break
        if index % ratio_saut == 0:
            images_extraites.append(image)
            compteur += 1
        index += 1
    cap.release()

    if avec_flow and len(images_extraites) > 1:
        images_extraites = appliquer_optical_flow(images_extraites)

    for i, img in enumerate(images_extraites):
        nom = os.path.join(dossier_sortie, f"image_{i:05d}.jpg")
        cv2.imwrite(nom, img)

    return int(fps_original), len(images_extraites)

def créer_vidéo_depuis_images(dossier_images, chemin_sortie, fps=12):
    """
    Construit une vidéo à partir d’images extraites.
    """
    fichiers = sorted([f for f in os.listdir(dossier_images) if f.endswith(".jpg")])
    if not fichiers:
        return None

    image_exemple = cv2.imread(os.path.join(dossier_images, fichiers[0]))
    h, w, _ = image_exemple.shape
    codec = cv2.VideoWriter_fourcc(*'mp4v')
    video = cv2.VideoWriter(chemin_sortie, codec, fps, (w, h))

    for f in fichiers:
        img = cv2.imread(os.path.join(dossier_images, f))
        img = cv2.resize(img, (w, h))
        video.write(img)

    video.release()
    return chemin_sortie

def reencoder_video_h264(chemin_entrée, chemin_sortie):
    """
    Réencode une vidéo en H.264 pour compatibilité Streamlit.
    """
    commande = [
        "ffmpeg",
        "-y",
        "-i", chemin_entrée,
        "-vcodec", "libx264",
        "-preset", "fast",
        "-crf", "23",
        chemin_sortie
    ]
    subprocess.run(commande, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# Interface Streamlit
st.title("Stop Motion avec Optical Flow (optionnel)")

mode = st.radio("Source de la vidéo :", ["YouTube (yt-dlp)", "Fichier local (.mp4)"])

if mode == "YouTube (yt-dlp)":
    url = st.text_input("Entrez l'URL de la vidéo YouTube")
else:
    fichier = st.file_uploader("Téléverser une vidéo .mp4", type=["mp4"])

fps_cible = st.selectbox("FPS cible (effet Stop Motion)", [4, 6, 8, 10, 12, 14, 16], index=2)
avec_optical_flow = st.checkbox("Ajouter le flux optique (mouvement entre images)")

if st.button("Créer la vidéo Stop Motion"):
    with tempfile.TemporaryDirectory() as tmpdir:
        try:
            # Charger la vidéo
            if mode == "YouTube (yt-dlp)":
                if not url:
                    st.error("Veuillez fournir une URL YouTube.")
                    st.stop()
                st.info("Téléchargement de la vidéo...")
                chemin_video = telecharger_video_yt_dlp(url, tmpdir)
                st.success("Téléchargement terminé.")
            else:
                if not fichier:
                    st.error("Veuillez téléverser une vidéo.")
                    st.stop()
                chemin_video = os.path.join(tmpdir, "video_originale.mp4")
                with open(chemin_video, "wb") as f:
                    f.write(fichier.read())
                st.success("Vidéo téléversée.")

            # Extraction images
            dossier_images = os.path.join(tmpdir, "images")
            os.makedirs(dossier_images, exist_ok=True)

            st.info("Extraction des images en cours...")
            fps_origine, nb = extraire_images_echantillonnées(
                chemin_video, dossier_images, fps_cible, avec_flow=avec_optical_flow)
            st.success(f"{nb} images extraites (FPS origine : {fps_origine})")

            # Création de la vidéo temporaire
            chemin_brut = os.path.join(tmpdir, "video_brute.mp4")
            créer_vidéo_depuis_images(dossier_images, chemin_brut, fps=fps_cible)

            # Réencodage final
            chemin_final = os.path.join(tmpdir, "video_finale.mp4")
            st.info("Réencodage final (H.264)...")
            reencoder_video_h264(chemin_brut, chemin_final)

            with open(chemin_final, "rb") as f:
                st.success("Vidéo générée avec succès.")
                st.video(f.read())
                st.download_button("Télécharger la vidéo", data=f, file_name="stopmotion.mp4", mime="video/mp4")

        except subprocess.CalledProcessError:
            st.error("Erreur lors de l'utilisation de yt-dlp ou ffmpeg. Vérifiez leur installation.")
        except Exception as e:
            st.error(f"Erreur : {str(e)}")

 

A propos de l'auteur

Stéphane Meurisse

Ajouter un commentaire

Stéphane Meurisse