Indisciplinarité

Catégorie : germe de code

Le « frame differencing » avec Processing

Il n’est pas si facile d’écrire d’une traite le cahier de cours que je suis en train de tenir. Même si j’apprends souvent de nouvelles choses dans le domaine, rédiger c’est aussi passer du temps à revenir sur des concepts que l’on maitrise parfois déjà. Rendre l’information accessible à tous est un exercice de rédaction relativement délicat; Alors, même si cela s’impose, avant de commencer à écrire les chapitres sur les matrices, les listes, les pixels, etc, voici un petit article sur une méthode de frame differencing. Un article, plutôt que de rédiger un chapitre, ça me permet de parler de quelques chose de plus neuf pour moi, sans passer par la case départ. Et puis, dans tous les cas, ça servira plus tard quand j’y viendrai dans mon cahier de cours.

Le « frame differencing »

Le frame differencing, c’est l’analyse et la comparaison de deux images successives d’un flux vidéo. À quoi ça sert? Dans le domaine du traitement des images (ou image processing en anglais) et plus précisément dans la branche de la vision par ordinateur (ou computer vision, en anglais toujours), c’est bien utile. Par exemple, en comparant deux images à la suite, on pourrait compter le nombre de pixels qui ont changé de couleur ou de luminosité. On pourrait également soustraire l’arrière plan (un décor) d’un premier plan avec des objets en mouvement (on parlera de background substraction). Ci-dessous une capture d’écran d’un programme de soustraction du décor qui utilise un algorithme de comparaison d’images.

Ce qui nous intéresse ici dans un premier temps, c’est ce fameux algorithme de différenciation d’images (et pas tellement la soustraction de décor). Alors on va voir comment ça marche, allez hop, on ouvre Processing.

Quantifier le mouvement

On va essayer ici de réaliser un petit programme qui « quantifie » les mouvements capturés via une entrée vidéo. Le principe est le suivant: analyser les couleurs des pixels d’une image issue d’un flux vidéo d’un instant T avec les pixels d’une image « passée » issue d’un instant T – 1. La différence issue de taux colorimétrique sensiblement différents de ces deux images nous permettra de faire comprendre à l’ordinateur qu’il y a eu un mouvement; pour le programme « intelligent » que nous réaliserons, ce mouvement, c’est un ou plusieurs pixels qui ont changé de valeur entre l’instant T et l’instant T – 1.

Tout d’abord on va commencer par afficher notre entrée vidéo dans Processing. Ici, une webcam. Je ne reviens pas sur le code pour acquérir un flux vidéo dans Processing.

import processing.video.*;

Capture cam;

void setup() {
size(640, 480);
cam = new Capture(this, width, height);
cam.start();
}

void captureEvent(Capture cam) {
cam.read();
}

void draw() {
image(cam, 0, 0);
}

Capture d’écran 2015-06-27 à 18.43.29

L’ensemble des pixels de l’image présente de la capture vidéo on va les appeler « pixels actuels ». Ce sont les pixels que l’on voit, ceux du « présent ». On va dire qu’en une seconde on affiche 24 images. Eh bien nous, on va s’intéresser à une seule de ces images d’un instant T qui se renouvelle 24 fois par seconde.

Ces pixels actuels vont êtres comparés avec les « pixels du passé » qu’on va appeler « pixels précédents ». Ces pixels précédents sont ceux de l’image de l’instant T moins 1.

Si les pixels présents sont bel et bien là, il faudra créer une « image retardée » ou plutôt une image qui sauvegarde l’avant dernière image du flux. Alors on va créer notre image avec createImage(), à la taille des images du flux qui est pour l’instant vide. Notre image s’appelle « imagePasse« .

Capture d’écran 2015-06-27 à 19.15.16

Bon, maintenant, on va copier les pixels courants dans notre imagePasse avec la méthode copy(). Voir l’image ci-dessous. Et dans le code, à titre démonstratif, vous remarquerez que j’ai passé en commentaire la fonction affichant le flux vidéo. Qu’on l’affiche ou non, Processing sait lire malgré tout les informations récupérées par la webcam. La fonction image() est en quelque sorte un retour tangible des informations récupérées sur l’entrée vidéo. Ainsi, on peut afficher l’image copiée avec un léger retard sans forcément afficher le flux vidéo. Bon, on se fiche également d’afficher l’image « retardée », je l’ai affiché avec image() juste pour vérifier que la méthode copy() fonctionne.

Capture d’écran 2015-06-27 à 19.27.38

Maintenant, on va créer un « mouvementiomètre » soit une variable qui va permettre de contenir de manière brute la différence de la valeur de l’ensemble des pixels actuels avec la valeur de l’ensemble des pixels passés. Mais après chaque tour de boucle de draw(), on veut que le mouvementiomètre repasse à zéro sans sauvegarder et additionner les vieilles valeurs. Juste en dessous du code on ajoute:

float mouvement = 0;

Fabuleux, mais si on veut faire des opérations sur nos pixels, il va falloir dire à Processing « hep, je veux interagir avec les pixels, charge-les ». Pour cela on va utiliser la méthode loadPixels() sur notre flux webcam et sur notre image retardée. On ajoute:

cam.loadPixels();
imagePasse.loadPixels();

Ensuite on parcours l’ensemble de nos pixels de la webcam avec une boucle.

for(int i = 0; i < cam.pixels.length; i++){ }

Pour l'instant votre code devrait ressembler à quelque chose comme ça:

Capture d’écran 2015-06-27 à 22.29.46

Bien alors que faire à l'intérieur de cette boucle? Une permanente? Non. On va plutôt récupérer les informations liés à la colorimétrie de l'ensemble des pixels de nos images vidéos de manière brute. On va placer cette valeur brute de nos deux images dans deux variables respectives:

color present = cam.pixels[i]; // variable qui récupère les infos du premier flux
color passe = imagePasse.pixels[i]; // variable qui fait de même mais sur le second flux

Si on fait un print() de ces valeurs, les chiffres obtenus seront assez barbare, et pourra même faire planter votre processing si vous ne veillez pas à placer un exit() après l'impression du résultat. On va alors extirper de ces valeurs, le taux de rouge, le taux de bleu, et le taux de vert. Pour chacun de ces taux, on va devoir créer des variables. Pour cela, on va utiliser pour l'instant les fonctions red(), blue(), green().

Capture d’écran 2015-06-27 à 22.41.32

On touche au but. Nous avons des variables qui contiennent les informations relatives à la nature des nos deux images vidéos. Il ne reste plus qu'à calculer la différence de ces deux ensembles de valeurs. Et on va utiliser dist(), fonction plutôt prévue pour calculer, via le fameux théorème de Pythagore, la distance entre deux points disposés sur un repère de plan. Mais ça, on s'en fiche un peu. Allez hop on crée encore une petite variable pour contenir la différence.

float diff = dist(rougePresent, vertPresent, bleuPresent, rougePasse, vertPasse, bleuPasse);

Et on va enfin, en dehors de la boucle, attribuer cette dernière valeur à notre variable mouvement. Et on va faire la moyenne du "taux" de mouvement pour l'ensemble des pixels. Il ne reste plus qu'a diviser mouvement par le nombre de pixels. Allez on va dessiner un petit carré qui grandit ou rapetisse pour illustrer le "taux" de changement de la capture. Voilà la fin du code ci-dessous.

Capture d’écran 2015-06-27 à 23.48.28

Ci-dessous, une série d'image capturées. Je bouge frénétiquement mes mains. Le rectangle blanc gigote aussi beaucoup.

frame_diff_000070 frame_diff_000076 frame_diff_000074 frame_diff_000072

Note: Plus l'éclairage est bon, moins il y a de bruit d'image et le script plus fidèle. On pourra cependant atténuer le bruit avec un threshold ou bien un filtre gaussien.

Voilà le code complet, téléchargeable ici également.

import processing.video.*;

Capture cam; // notre flux vidéo, affiche des images du "présent"
PImage imagePasse; // image précédente qui va sauvegarder l'avant dernier image du flux

void setup() {
size(640, 480);
cam = new Capture(this, width, height);
cam.start();

imagePasse = createImage(width, height, RGB);
}

void captureEvent(Capture cam) {
cam.read();
}

void draw() {
//FLUX PRESENT
image(cam, 0, 0); //<---- on peut ne pas afficher le flux //FLUX PASSE imagePasse.copy(cam, 0, 0, width, height, 0, 0, width, height);//léger retard avec la méthde copy qui copie les pixels de la camera //notre "mouvementiomètre", qui vaut 0, et repasse à zéro à chaque tour de boucle de draw float mouvement = 0; //CHARGER PIXELS POUR OPERATIONS sur PIXELS cam.loadPixels(); //on a besoin de charger les pixels de nos images pour pouvoir effectuer des opérations dessus imagePasse.loadPixels(); //PARCOURIR TOUS LES PIXELS, ici on additionne la luminosité de chacun des pixels dans les variables present et passe for(int i = 0; i < cam.pixels.length; i++){ color present = cam.pixels[i]; color passe = imagePasse.pixels[i]; float rougePresent = red(present); float vertPresent = green(present); float bleuPresent = blue(present); float rougePasse = red(passe); float vertPasse = green(passe); float bleuPasse = green(passe); float diff = dist(rougePresent, vertPresent, bleuPresent, rougePasse, vertPasse, bleuPasse); mouvement += diff; } float moyenneMouvement = mouvement / cam.pixels.length; float r = moyenneMouvement * 4; println(moyenneMouvement); rect(0, 0, r, 25); /* if(frameCount % 2 == 0){ saveFrame("frame_diff_######.png"); } */ }

 

Cet article a été publié le 27 juin 2015.
« Page précédente