Implémentation complète de l'algorithme ViBe pour l'extraction de premier plan vidéo avec MATLAB

Principes de l'algorithme ViBe

L'algorithme ViBe, pour Visual Background Extractor, est une technique performante pour la modélisation de fond et la détection d'objets en mouvement dans les flux vidéo. Introduit par Barnich et Van Droogenbroeck en 2009, il se distingue par son efficacité et sa simplicité.

Avantages clés

  • Initialisation en une seule image : Le modèle de fond peut être initialisé à partir de la première trame.
  • Mise à jour stochastique : Adaptation continue aux changements du fond.
  • Propagation spatiale : Réduction des artefacts de type "fantôme".
  • Efficacité computationnelle : Compatible avec le traitement en temps réel.
  • Faible empreinte mémoire : Chaque pixel nécessite le stockage d'un nombre limité d'échantillons.

Mécanisme de fonctionnement

  1. Modèle de fond : Chaque pixel est associé à un ensemble de N échantillons de valeurs passées.
  2. Détection du premier plan : Un pixel est classé comme premier plan si le nombre d'échantillons de fond proches de sa valeur courante est inférieur à un seuil minimum.
  3. Stratégie de mise à jour : Les échantillons de fond sont mis à jour aléatoirement, incluant une propagation vers les pixels voisins.

Implémentation sous MATLAB

Programme principal : extraction_premier_plan.m

%% Script principal pour l'extraction de premier plan avec ViBe
clear; clc; close all;

%% Configuration des paramètres
parametres = struct();
parametres.nbEchantillons = 20;          % Nombre d'échantillons par pixel
parametres.seuilCorrespondance = 20;     % Seuil de différence en niveaux de gris
parametres.correspondancesMin = 2;       % Nombre minimum de correspondances pour le fond
parametres.facteurMiseAJour = 16;        % Facteur φ, probabilité de mise à jour = 1/φ
parametres.rayonVoisinage = 20;          % Rayon pour l'initialisation
parametres.operationsMorpho = true;      % Appliquer des opérations morphologiques

%% Chemins des fichiers vidéo
cheminEntree = 'video_source.avi';
cheminSortie = 'video_resultat.avi';

% Générer une vidéo exemple si nécessaire
if ~exist(cheminEntree, 'file')
    fprintf('Fichier vidéo introuvable, création d\'un exemple...\n');
    creerVideoExemple();
end

%% Initialisation des lecteurs et écrivains vidéo
lecteurVideo = VideoReader(cheminEntree);
nombreTotalImages = lecteurVideo.NumFrames;
hauteur = lecteurVideo.Height;
largeur = lecteurVideo.Width;

ecrivainVideo = VideoWriter(cheminSortie, 'Motion JPEG AVI');
ecrivainVideo.FrameRate = lecteurVideo.FrameRate;
open(ecrivainVideo);

%% Initialisation du modèle de fond ViBe
fprintf('Initialisation du modèle de fond ViBe...\n');
premiereImage = readFrame(lecteurVideo);
if size(premiereImage, 3) == 3
    premiereImageNiveauxGris = rgb2gray(premiereImage);
else
    premiereImageNiveauxGris = premiereImage;
end

modeleFond = initialiserModeleViBe(premiereImageNiveauxGris, parametres);

%% Boucle de traitement principale
fprintf('Début du traitement vidéo, total d\'images : %d\n', nombreTotalImages);
compteurImages = 0;
tempsTraitement = zeros(nombreTotalImages-1, 1);

figure('Position', [100, 100, 1200, 400]);
subplot(1,3,1); handle1 = imshow(premiereImage); title('Image originale');
subplot(1,3,2); handle2 = imshow(zeros(hauteur, largeur)); title('Masque de premier plan');
subplot(1,3,3); handle3 = imshow(premiereImage); title('Résultat de détection');

while hasFrame(lecteurVideo)
    compteurImages = compteurImages + 1;
    
    imageCourante = readFrame(lecteurVideo);
    if size(imageCourante, 3) == 3
        imageCouranteNG = rgb2gray(imageCourante);
    else
        imageCouranteNG = imageCourante;
    end
    
    tic;
    [masquePremierPlan, modeleFond] = detecterPremierPlanViBe(...
        double(imageCouranteNG), modeleFond, parametres);
    
    if parametres.operationsMorpho
        masquePremierPlan = traitementMorphologique(masquePremierPlan);
    end
    
    tempsTraitement(compteurImages) = toc;
    
    imageResultat = afficherResultats(imageCourante, masquePremierPlan);
    
    set(handle1, 'CData', imageCourante);
    set(handle2, 'CData', masquePremierPlan);
    set(handle3, 'CData', imageResultat);
    drawnow;
    
    writeVideo(ecrivainVideo, imageResultat);
    
    if mod(compteurImages, 50) == 0
        fprintf('Images traitées %d/%d, temps moyen : %.3f s/image\n', ...
            compteurImages, nombreTotalImages-1, mean(tempsTraitement(1:compteurImages)));
    end
end

close(ecrivainVideo);
fprintf('Traitement terminé !\n');
fprintf('Total d\'images traitées : %d\n', compteurImages);
fprintf('Temps de traitement moyen : %.4f s/image\n', mean(tempsTraitement));
fprintf('Vidéo résultat enregistrée dans : %s\n', cheminSortie);

figure;
plot(tempsTraitement, 'b-', 'LineWidth', 1.5);
xlabel('Numéro d\'image');
ylabel('Temps de traitement (s)');
title('Statistiques de temps de traitement ViBe');
grid on;

Fonction d'initialisatoin : initialiserModeleViBe.m

function modeleFond = initialiserModeleViBe(premiereImage, parametres)
% Initialisation du modèle de fond ViBe
% Entrées :
%   premiereImage - Image initiale (niveaux de gris)
%   parametres - Structure des paramètres
% Sorties :
%   modeleFond - Structure du modèle de fond

[hauteurImg, largeurImg] = size(premiereImage);
premiereImage = double(premiereImage);

modeleFond = struct();
modeleFond.echantillons = zeros(hauteurImg, largeurImg, parametres.nbEchantillons, 'uint8');
modeleFond.hauteur = hauteurImg;
modeleFond.largeur = largeurImg;
modeleFond.nbEchantillons = parametres.nbEchantillons;

fprintf('Remplissage de l\'échantillon de fond...\n');
for ligne = 1:hauteurImg
    for colonne = 1:largeurImg
        debutLigne = max(1, ligne - 1);
        finLigne = min(hauteurImg, ligne + 1);
        debutColonne = max(1, colonne - 1);
        finColonne = min(largeurImg, colonne + 1);
        
        voisinage = premiereImage(debutLigne:finLigne, debutColonne:finColonne);
        voisinage = voisinage(:);
        
        for echantillon = 1:parametres.nbEchantillons
            if ~isempty(voisinage)
                indexAleatoire = randi(length(voisinage));
                modeleFond.echantillons(ligne, colonne, echantillon) = voisinage(indexAleatoire);
            else
                modeleFond.echantillons(ligne, colonne, echantillon) = premiereImage(ligne, colonne);
            end
        end
    end
end

fprintf('Modèle initialisé, dimensions de l\'échantillon : %d×%d×%d\n', ...
    hauteurImg, largeurImg, parametres.nbEchantillons);
end

Fonction de détection : detecterPremierPlanViBe.m

function [masque, modeleMisAJour] = detecterPremierPlanViBe(...
    imageActuelle, modeleFond, parametres)
% Détection du premier plan et mise à jour du modèle
% Entrées :
%   imageActuelle - Image courante (double, niveaux de gris)
%   modeleFond - Modèle de fond actuel
%   parametres - Structure des paramètres
% Sorties :
%   masque - Masque binaire du premier plan
%   modeleMisAJour - Modèle de fond mis à jour

[hauteurImg, largeurImg] = size(imageActuelle);
masque = false(hauteurImg, largeurImg);

echantillons = modeleFond.echantillons;
nbEch = modeleFond.nbEchantillons;

for ligne = 1:hauteurImg
    for colonne = 1:largeurImg
        pixelCourant = imageActuelle(ligne, colonne);
        
        nbCorrespondances = 0;
        for k = 1:nbEch
            if abs(double(echantillons(ligne, colonne, k)) - pixelCourant) < parametres.seuilCorrespondance
                nbCorrespondances = nbCorrespondances + 1;
                if nbCorrespondances >= parametres.correspondancesMin
                    break;
                end
            end
        end
        
        if nbCorrespondances < parametres.correspondancesMin
            masque(ligne, colonne) = true;
        else
            masque(ligne, colonne) = false;
            
            if rand() < 1/parametres.facteurMiseAJour
                echantillonAlea = randi(nbEch);
                echantillons(ligne, colonne, echantillonAlea) = uint8(pixelCourant);
                
                if rand() < 1/parametres.facteurMiseAJour
                    deltaLigne = randi(3) - 2;
                    deltaCol = randi(3) - 2;
                    
                    ligneVoisine = ligne + deltaLigne;
                    colonneVoisine = colonne + deltaCol;
                    
                    if ligneVoisine >= 1 && ligneVoisine <= hauteurImg && colonneVoisine >= 1 && colonneVoisine <= largeurImg
                        echantillonAlea2 = randi(nbEch);
                        echantillons(ligneVoisine, colonneVoisine, echantillonAlea2) = uint8(pixelCourant);
                    end
                end
            end
        end
    end
end

modeleMisAJour = modeleFond;
modeleMisAJour.echantillons = echantillons;
end

Traitement morphologique : traitementMorphologique.m

function masqueTraite = traitementMorphologique(masqueBinaire)
% Post-traitement morphologique pour le nettoyage du masque
% Entrée :
%   masqueBinaire - Masque binaire brut
% Sortie :
%   masqueTraite - Masque nettoyé

masqueTraite = bwareaopen(masqueBinaire, 50);

elementStructurant1 = strel('disk', 3);
masqueTraite = imclose(masqueTraite, elementStructurant1);

elementStructurant2 = strel('disk', 2);
masqueTraite = imopen(masqueTraite, elementStructurant2);

masqueTraite = imfill(masqueTraite, 'holes');

masqueTraite = medfilt2(masqueTraite, [3, 3]);
end

Visualisation des résultats : afficherResultats.m

function imageFinale = afficherResultats(imageOriginale, masquePremierPlan)
% Création d'une image de visualisation
% Entrées :
%   imageOriginale - Image source (RGB ou niveaux de gris)
%   masquePremierPlan - Masque binaire
% Sortie :
%   imageFinale - Image annotée

if size(imageOriginale, 3) == 1
    imageRGB = cat(3, imageOriginale, imageOriginale, imageOriginale);
else
    imageRGB = imageOriginale;
end

canalRouge = imageRGB(:,:,1);
canalVert = imageRGB(:,:,2);
canalBleu = imageRGB(:,:,3);

canalRouge(masquePremierPlan) = 255;
canalVert(masquePremierPlan) = 0;
canalBleu(masquePremierPlan) = 0;

imageFinale = cat(3, canalRouge, canalVert, canalBleu);

statistiques = regionprops(masquePremierPlan, 'BoundingBox', 'Area');
regionValide = statistiques([statistiques.Area] > 100);

for k = 1:length(regionValide)
    boite = regionValide(k).BoundingBox;
    imageFinale = insertShape(imageFinale, 'Rectangle', boite, ...
        'Color', 'green', 'LineWidth', 2);
end
end

Création d'une vidéo exemple : creerVideoExemple.m

function creerVideoExemple()
% Génération d'une vidéo synthétique pour les tests

cheminVideo = 'video_exemple.avi';
ecrivain = VideoWriter(cheminVideo, 'Motion JPEG AVI');
ecrivain.FrameRate = 10;
open(ecrivain);

for idx = 1:100
    image = uint8(128 * ones(240, 320, 3));
    
    positionRectangle = [50 + idx, 100, 40, 40];
    image = insertShape(image, 'FilledRectangle', positionRectangle, ...
        'Color', 'white', 'Opacity', 1);
    
    rectangleStatique = [150, 150, 30, 30];
    image = insertShape(image, 'FilledRectangle', rectangleStatique, ...
        'Color', 'blue', 'Opacity', 1);
    
    bruit = uint8(randn(240, 320, 3) * 10);
    image = min(255, max(0, double(image) + double(bruit)));
    
    writeVideo(ecrivain, image);
end

close(ecrivain);
fprintf('Vidéo exemple créée : %s\n', cheminVideo);
end

Optimisation des paramètres

Description des paramètres clés

% Plages de valeurs recommandées
parametres.nbEchantillons = 20;          % [10-30]
parametres.seuilCorrespondance = 20;     % [10-40], plus sensible si bas
parametres.correspondancesMin = 2;       % [1-5], plus sensible si bas
parametres.facteurMiseAJour = 16;        % [8-32], mise à jour plus lente si haut

Suggestions pour différents scénarios

% Scénario fond statique avec objets en mouvement marqués
param_statique = struct('nbEchantillons', 15, 'seuilCorrespondance', 15, ...
    'correspondancesMin', 2, 'facteurMiseAJour', 20);

% Scénario fond dynamique (feuilles, eau)
param_dynamique = struct('nbEchantillons', 25, 'seuilCorrespondance', 25, ...
    'correspondancesMin', 3, 'facteurMiseAJour', 12);

% Scénario faible luminosité ou fort bruit
param_faibleLumiere = struct('nbEchantillons', 20, 'seuilCorrespondance', 30, ...
    'correspondancesMin', 2, 'facteurMiseAJour', 16);

Fonctionnalités avancées

Support des images couleur

function modeleFondCouleur = initialiserViBeCouleur(imageCouleur, parametres)
% Initialisation ViBe pour les images couleur
[hauteurImg, largeurImg, ~] = size(imageCouleur);
modeleFondCouleur = struct();
modeleFondCouleur.echantillons = zeros(hauteurImg, largeurImg, 3, parametres.nbEchantillons, 'uint8');

for canal = 1:3
    donneesCanal = imageCouleur(:,:,canal);
    for ligne = 1:hauteurImg
        for colonne = 1:largeurImg
            debutLigne = max(1, ligne-1); finLigne = min(hauteurImg, ligne+1);
            debutColonne = max(1, colonne-1); finColonne = min(largeurImg, colonne+1);
            voisinage = donneesCanal(debutLigne:finLigne, debutColonne:finColonne);
            voisinage = voisinage(:);
            
            for k = 1:parametres.nbEchantillons
                if ~isempty(voisinage)
                    indexAlea = randi(length(voisinage));
                    modeleFondCouleur.echantillons(ligne, colonne, canal, k) = voisinage(indexAlea);
                else
                    modeleFondCouleur.echantillons(ligne, colonne, canal, k) = donneesCanal(ligne, colonne);
                end
            end
        end
    end
end
end

Traitement en temps réel avec webcam

function traitementTempsReelViBe()
% Détection de premier plan en flux continu

camera = webcam();
parametres.nbEchantillons = 20;
parametres.seuilCorrespondance = 25;
parametres.correspondancesMin = 2;
parametres.facteurMiseAJour = 16;

premiereCapture = snapshot(camera);
if size(premiereCapture, 3) == 3
    premiereCaptureNG = rgb2gray(premiereCapture);
else
    premiereCaptureNG = premiereCapture;
end

modeleFond = initialiserModeleViBe(premiereCaptureNG, parametres);

figure('Name', 'ViBe temps réel');
subplot(1,2,1); h1 = imshow(premiereCapture); title('Flux entrant');
subplot(1,2,2); h2 = imshow(zeros(size(premiereCaptureNG))); title('Détection');

compteur = 0;
while true
    compteur = compteur + 1;
    imageBrute = snapshot(camera);
    imageNG = rgb2gray(imageBrute);
    
    [masque, modeleFond] = detecterPremierPlanViBe(double(imageNG), modeleFond, parametres);
    masque = traitementMorphologique(masque);
    
    set(h1, 'CData', imageBrute);
    set(h2, 'CData', masque);
    drawnow;
    
    if strcmp(get(gcf, 'CurrentKey'), 'escape')
        break;
    end
end

clear camera;
end

ViBe multi-échelle pour une meilleure précision

function [masqueFinal, modeleMisAJour] = viBeMultiEchelle(...
    imageCourante, modeleFond, parametres)
% Détection à plusieurs échelles

niveauxPyramide = 2;
imagesRedimensionnees = cell(niveauxPyramide, 1);
masquesNiveaux = cell(niveauxPyramide, 1);

imagesRedimensionnees{1} = imageCourante;

for niveau = 2:niveauxPyramide
    imagesRedimensionnees{niveau} = imresize(imageCourante, 1/(2^(niveau-1)));
end

for niveau = 1:niveauxPyramide
    [masquesNiveaux{niveau}, modeleFond] = detecterPremierPlanViBe(...
        imagesRedimensionnees{niveau}, modeleFond, parametres);
end

masqueFinal = masquesNiveaux{1};
for niveau = 2:niveauxPyramide
    masqueAgrandi = imresize(masquesNiveaux{niveau}, size(masqueFinal));
    masqueFinal = masqueFinal | masqueAgrandi;
end

modeleMisAJour = modeleFond;
end

Résolution des problèmes courants

Phénomène de "fantôme"

Problème : Un objet en mouvement quitte la scène, mais son ancienne position est encore détectée comme premier plan.

Solutions :

  • Augmenter la valeur de facteurMiseAJour pour ralentir la mise à jour du fond.
  • Implémenter une mise à jour conservatrice : ne mettre à jour que si le pixel est classé fond sur plusieurs trames consécutives.
  • Ajouter une vérification de cohérence du mouvement.

Sensibilité au bruit

Problème : Le bruit de l'image cause des fausses détections.

Solutions :

  • Augmenter seuilCorrespondance.
  • Augmenter correspondancesMin.
  • Ajouter un pré-filtrage (filtre gaussien, filtre médian).
  • Renforcer les opérations morphologiques post-traitement.

Lenteur de calcul

Problème : Traitement lent pour les vidéos haute résolution.

Solutions :

  • Utiliser des opérations vectorielles à la place des boucles.
  • Implémenter une version accélérée par GPU.
  • Réduire la résolution de l'image.
  • Accélérer les parties critiques avec des fonctions C/MEX.

Exemples d'application

Détection de véhicules

param_vehicules = struct('nbEchantillons', 25, 'seuilCorrespondance', 30, ...
    'correspondancesMin', 3, 'facteurMiseAJour', 20, 'operationsMorpho', true);

function masqueVehicule = postTraitementVehicules(masqueBrut)
% Filtrage spécifique pour la détection de véhicules
statistiques = regionprops(masqueBrut, 'Area', 'BoundingBox');
aireMinVehicule = 500;
aireMaxVehicule = 5000;

masqueVehicule = false(size(masqueBrut));
for i = 1:length(statistiques)
    if statistiques(i).Area >= aireMinVehicule && statistiques(i).Area <= aireMaxVehicule
        boite = round(statistiques(i).BoundingBox);
        masqueVehicule(boite(2):boite(2)+boite(4)-1, ...
                     boite(1):boite(1)+boite(3)-1) = true;
    end
end
end

Détection de piétons

param_pietons = struct('nbEchantillons', 20, 'seuilCorrespondance', 20, ...
    'correspondancesMin', 2, 'facteurMiseAJour', 16, 'operationsMorpho', true);

function masquePieton = postTraitementPietons(masqueBrut)
% Traitement adapté aux piétons
elementStructurant = strel('disk', 2);
masquePieton = imopen(masqueBrut, elementStructurant);
masquePieton = imclose(masquePieton, elementStructurant);

statistiques = regionprops(masquePieton, 'Area', 'Eccentricity');
aireMinPieton = 100;
aireMaxPieton = 1000;
excentriciteMax = 0.9;

regionsValides = false(length(statistiques), 1);
for i = 1:length(statistiques)
    if statistiques(i).Area >= aireMinPieton && ...
       statistiques(i).Area <= aireMaxPieton && ...
       statistiques(i).Eccentricity <= excentriciteMax
        regionsValides(i) = true;
    end
end

masquePieton = ismember(bwlabel(masquePieton), find(regionsValides));
end

Utilisation et remarques

Pour utiliser cette implémentation :

  1. Sauvegardez chaque fonction dans un fichier .m distinct.
  2. Exécutez extraction\_premier\_plan.m pour lancer le traitement vidéo.
  3. Ajustez les paramètres selon votre scénario spécifique.
  4. Utilisez traitementTempsReelViBe() pour tester avec une webcam en direct.

Considérations importantes :

  • Lors de la première exécution, une vidéo exemple sera générée si aucun fichier source n'est fourni.
  • Le traitement de longues vidéos peut nécessiter du temps ; testez d'abord sur un court extrait.
  • L'ajustement des paramètres est crucial pour optimiser les performances selon l'environnement.
  • La consommation mémoire dépend de la taille de l'image et du nombre d'échantillons.

Étiquettes: MATLAB ViBe segmentation vidéo détection premier plan traitement image

Publié le 3 juillet à 09h51