Filtre de détection des contours sur une vidéo

filtrage fait après enregistrement webcam dans le navigateur

a marqué ce sujet comme résolu.

bonjour ! J’essaie d’enregistrer une vidéo depuis ma webcam et de lui appliquer un filtre de détection des contours avec openCV.js. L’enregistrement fonctionne bien, il est fait avec WebRTC, il me donne une vidéo stockée dans un blob, et je peux l’afficher sans problème. Voici le code de ma fonction de filtrage :

async function detectContours(inputBlob) {
    console.trace("a");

    const response = await fetch(URL.createObjectURL(inputBlob));
    const blob = await response.blob();

    const inputVideo = document.createElement("video");
    inputVideo.src = URL.createObjectURL(blob);

    await new Promise((resolve) => {
    inputVideo.addEventListener("loadedmetadata", () => {
        resolve();
    });
    inputVideo.load();
    });

    console.log("type de inputVideo : %o", typeof inputVideo);
    console.log("vidéo d'entrée : %o * %o", inputVideo.height, inputVideo.width);

    console.trace("b");
    await new Promise((resolve) => {
    inputVideo.addEventListener("canplaythrough", () => {
        resolve();
    });
    inputVideo.load();
    });
    await inputVideo.play();

    console.trace("c");
    const outputCanvas = document.createElement("canvas");
    outputCanvas.width = inputVideo.videoWidth;
    outputCanvas.height = inputVideo.videoHeight;
    const outputCtx = outputCanvas.getContext("2d");

    console.trace("d");
    outputCtx.drawImage(
        inputVideo,
        0,
        0,
        outputCanvas.width,
        outputCanvas.height
    );

    console.trace("e");
    const cvInput = cv.imread(outputCanvas);
    const cvOutput = new cv.Mat();

    console.trace("f");
    cv.Canny(cvInput, cvOutput, 100, 200);

    console.trace("g");
    console.log(
      "vidéo d'entrée : %o * %o",
        inputVideo.videoHeight,
        inputVideo.videoWidth
    );
    console.log("canvas de sortie : %o * %o", cvOutput.rows, cvOutput.cols);
    console.log("%o bytes", cvOutput.data.length);
    const outputImageData = new ImageData(
        new Uint8ClampedArray(cvOutput.data),
        cvOutput.cols,
        cvOutput.rows / 4
    );

    console.trace("h");
    const outputCanvas2 = document.createElement("canvas");
    outputCanvas2.width = outputImageData.width;
    outputCanvas2.height = outputImageData.height;
    const outputCtx2 = outputCanvas2.getContext("2d");
    outputCtx2.putImageData(outputImageData, 0, 0);

    console.trace("i");
    const outputBlob = await new Promise((resolve) =>
        outputCanvas2.toBlob(resolve, "image/png")
    );

    console.trace("j");
    URLvideoEdge = URL.createObjectURL(outputBlob);
    blobVideoEdge = outputBlob;

    console.trace("k");
}

Voici le code de ma fonction appelée quand j’appuie sur mon bouton pour arrêter d’enregistrer :

async function stopRecording() {
    console.trace("1");
    //Recording the video
    for (let track of stream.getTracks()) {
        track.stop(); //when this event is triggered, function called display the video without any issue 
    }
    console.trace("2");

    // verifying blobVideoCouleur actually is an object
    while (typeof blobVideoCouleur !== "object") {
      await new Promise(resolve => setTimeout(resolve, 100)); // Pause 100ms
    }

    console.trace("3");


    //Applying edge detection filter
    await detectContours(blobVideoCouleur);

    console.trace("4");


    //Displaying filtered video 
    playBack.src = URLvideoEdge;
    playBack.autoplay = true;

    console.trace("5");

}

playBack est une variable globale définie comme l’élément html <vidéo> où je joue la vidéo enregistrée. Ca fonctionne très bien pour la vidéo "normale". Voici le code qui la définie, cette fonction est appelée quand j’appuie sur mon bouton d’arrêt d’enregistrement :

    recorder.onstop = () => { // Event triggered when I stop the recorder 
        const blob = new Blob(chunks, { //
        type: 'video/webm' // Et il le fait en webm
        })
        chunks = [];
        URLvideoCouleur = URL.createObjectURL(blob);
        playBack = document.getElementById("playBack");
        playBack.src = URLvideoCouleur;
        playBack.controls = true;
        blobVideoCouleur = blob;
    }

J’ai eu pas mal de soucis avec cette fonction, c’est pourquoi il y a plein de console.trace un peu partout, pour suivre l’exécution étape par étape. Ca m’a mené à corriger un dernier bug. La "solution" que j’ai trouvée était une division par 4 dans cette section de la fonction detectContours :

    const outputImageData = new ImageData(
        new Uint8ClampedArray(cvOutput.data),
        cvOutput.cols,
        cvOutput.rows / 4
    );

J’ai fait ça car apparemment ImageData a besoin que la taille totale des données soit largeur_frame*hauteur_frame*4, à cause des 4 couleurs (RGBA), mais moi j’ai une seule couche, en noir et blanc, après la détection des contours, donc je divise par 4 pour qu’il arrête de râler… A partir de là, j’ai plus aucune erreur dans la console, ou interruption ou autre, pour me guider, mais malheureusement le comportement n’est pas celui attendu. playBack ne joue pas ma vidéo filtrée à la place de la vidéo "normale". Il me donne juste un carré noir avec les contrôles du player, mais inutiles car la vidéo a une durée de 0s. Screenshot_7.pngce qui s'affiche au lieu de ma vidéo filtrée

J’ai plus la console pour avance je disais, et j’ai plus d’idée farfelue comme la division par 4. Avez-vous une suggestion de ce qui peut clocher ? Aussi, pour moi cette division par 4 est un signe que ma fonction detectContours est plus que douteuse, et en la regardant ça m’a l’air d’une grosse usine à gaz. Il n’y a pas une méthode plus simple pour appliquer un filtre de détection des contours en js ? Merci par avance !

Hello,

Je suis pas expert mais diviser par 4 me paraît dangereux quand l’API ImageData attend les dimensions de l’image à traiter et non un nombre de couleurs. De plus ta division est faite sur la hauteur : tu n’afficherais donc qu’une ligne sur 4 ?

Tu exposes peut-être uniquement du noir et blanc, mais l’affichage se fait bien en RGB, de même que le traitement : blanc étant du RGB (255, 255, 255), noir aussi (0, 0, 0). Le A de RGBA étant la couche alpha (transparence).

Quelle était l’erreur avant l’ajout de cette division ?

Hii !

On est bien d’accord, une division par 4 à l’endroit où il attend la dimension de l’image, c’est bizarre. C’est pour ça que je dis que ma fonction est douteuse, car comme tu dis sois j’affiche une ligne sur 4, soit j’affiche seulement le premier quart, soit… pas moyen de le savoir. de plus j’avais jamais entendu parler d’un format d’image à une seule couche uniquement pour les trucs en noirs et blanc, mais c’est ce qui m’a corrigé l’erreur. :(

Donc c’est au niveau de ces lignes que j’avais l’erreur précisément (57 - 63) précisément :

    console.log("canvas de sortie : %o * %o", cvOutput.rows, cvOutput.cols);
    console.log("%o bytes", cvOutput.data.length);
    const outputImageData = new ImageData(
        new Uint8ClampedArray(cvOutput.data),
        cvOutput.cols,
        cvOutput.rows
    );

J’ai retiré le /4, l’erreur est "Uncaught (in promise) DOMException: Failed to construct 'ImageData’: The input data length is not equal to (4 width height)." C’est pour ça qu’il y a les 2 console.log pour comparer les dimensions de cvOutput (hauteur et largeur) et le nombre de bytes : Il se trouve que j’ai cvOutput.data.length = vOutput.rows*cvOutput.cols sans le *4, d’où ma division sur cvOutput.rows / 4, comme ça la multiplication et la division se neutralisent.

Je viens d’essayé, avec type = cv.CV_64FC4 (j’ai supposé que le 4 final faisait référence aux 4 canaux de couleur), mais je reste sur la même erreur. Je suis en train de me demander si le problème ne vient pas de cv.Canny ? Est-ce que cette fonction ne renvoie pas justement une matrice remplie de 1 et de 0 (blanc et noir) au lieu d’une image ? Dans ce cas ça serait à moi de faire un traitement derrière…

Double post // Un edit est pas pertinent cette fois.

Je me plongé dans la doc de openCV.js (chose que je voulais éviter car je la trouvait peu pratique) ce qui m’a mené à écrire une fonction très courte qui fonctionne sur une image :

function detectContour(){
    //Conversion d'une image en détection des contours
    //Obtention de la matrice à partir de <img>
    let matriceCouleur= cv.imread("imageSource"); // id d'une <img> dans html
    let matriceGris = new cv.Mat(); 
    let matriceEdge = new cv.Mat(); 
    cv.cvtColor(matriceCouleur, matriceGris, cv.COLOR_RGBA2GRAY); 
    cv.imshow("canvasGris", matriceGris);
    cv.Canny(matriceGris, matriceEdge, 50, 100, 3, false);
    cv.imshow("canvasEdge", matriceEdge);
}

Je n’ai pas gardé la page dans la doc qui m’a permis d’arriver ici. :( Mais ça répond à mes 2 problèmes : C’est beaucoup plus lisible, et ça fonctionne.

Maintenant, il ne me reste plus qu’à adapter à la vidéo. La doc me donne encore ce qu’il faut :

let video = document.getElementById('videoInput');
let src = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let dst = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let cap = new cv.VideoCapture(video);

const FPS = 60;
function processVideo() {
    try {
        if (!streaming) {
            // clean and stop.
            src.delete();
            dst.delete();
            return;
        }
        let begin = Date.now();
        // start processing.
        cap.read(src);
        cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
        cv.Canny(dst, dst, 50, 100, 3, false);
        cv.imshow('canvasOutput', dst);
        // schedule the next one.
        let delay = 1000/FPS - (Date.now() - begin);
        setTimeout(processVideo, delay);
    } catch (err) {
        utils.printError(err);
    }
};

// schedule the first one.
setTimeout(processVideo, 0);

Ce qui fonctionne très bien dans le bac à sable de ladite page. Seulement, ça fonctionne avec le live de la caméra, et je veux travailler avec une vidéo qui est déjà enregistrée par la caméra et finie, donc j’ai essayé d’adapter un peu la chose :

/* Variables globales */
//...

window.addEventListener('DOMContentLoaded', demarrerApp); //On attend que tout soit chargé

function demarrerApp(){ // Fonction qui fait fonctionner la page, après avoir chargé tout html
    initialise_variables(); // Initialisation des variables globales
    cameraShow(); // Lancement de la webcam

}

function initialise_variables(){
    //...
}

function cameraShow()
{
    //...
}

function startRecording() // Bouton "démarrer l'enregistrement"
{
    //...
}

async function stopRecording() { // Bouton "arrêter l'enregistrement"
    //...
    matSrc = new cv.Mat(traitement.videoHeight, traitement.videoWidth, cv.CV_8UC4);
    matDst = new cv.Mat(traitement.videoHeight, traitement.videoWidth, cv.CV_8UC1);
    captur = cv.VideoCapture(traitement); //On capture la vidéo à traiter avec cette fonction, on passe en paramètre la vidéo voulue
    console.log(typeof captur); // réponse : undefined
    detectContourVideo();
}

function detectContourVideo(){
    captur.read(matSrc); // Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'read')
    //...

}

Je ne met pas l’intégralité du code : je sais que le reste fonctionne, donc pas la peine d’encombrer le topic. J’ai ce problème très embêtant avec la première ligne de detectContourVideo, l’erreur est en commentaire. Effectivement, capature est de type undefined, ce qui ne doit apparemment être le cas. Si j’ai bien compris la doc, c’est censé être type object n’est-ce pas ? Qu’est-ce qui peut faire défaut ?

Mettre un await dans la ligne captur = await cv.VideoCapture(traitement); ne produit aucun changement de comportement. Mais j’ai pas l’impression que VideoCapture retourne une promesse, donc prévisible.

Faire une boucle avec une promesse justement pour attendre que typeof captur == object mène à une boucle infinie. Donc j’en déduit que le type de captur ne changera pas avec le temps.

Essayer avec une vidéo qui ne vient pas de la caméra mais qui est directement chargée dans la page en même temps que le reste ne change pas le problème.

Quant à l’initialisation de captur avec la variable traitement, c’est dans tout ce que je n’ai pas mis : traitement désigne une <video> html, qui est correctement identifiée par son ID, et qui a bien la vidéo enregistrée chargée, puisqu’elle se joue sans problème.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte