[PROJET OpenCV et Tensorflow] – Un fond virtuel pour votre Webcam.

Durant cette période du COVID, la visioconférence est devenue très courante. Certains logiciels comme Teams proposent de remplacer le fond de votre webcam par un flou ou bien par une image, permettant une plus grande intimité lors de vos réunions de travail. Ce projet a pour but de créer un logiciel ouvert, qui propose cette fonctionnalité avec n’importe quel logiciel et vous permettre un choix d’image à placer.

Pour ce projet nous allons utilisé un language simple “python”, au vu de la grande disponibilité des modules. Nous utiliserons aussi une bibliothèque de traitement d’image “OpenCV” et la bibliothèque de google TensorFlow pour la partie machine learning du traitement.

Loin de répliquer le moteur de Microsoft Teams, très évolué permettant une image nette et limpide, Il s’avère que nous pouvons réellement obtenir des résultats assez décents avec des composants open source, open source et juste un peu de notre propre code.

Pour les besoins du code, j’utilise une machine virtuelle ubuntu sur laquelle je fais mes développement, l’accès à la webcam se fera donc à travers les accès hardware linux. Un port vers windows est très simple. Le code python reste assez portable plus ou moins quelques adaptations.

Lire le flux de la webcam

Première chose: comment allons-nous obtenir le flux vidéo de notre webcam pour le traitement?
La lecture d’un cadre depuis la webcam avec python-opencv est très simple:

import cv2
maCapture = cv2.VideoCapture('/dev/video0')
success, frame = MaCapture.read()

ci dessous un exemple des adaptations qu’on peut faire sur le code pour recevoir un flux en bonne définition à 720p et avec une vitesse de défilement (framerate) à 60 FPS :

height, width = 720, 1280
maCapture.set(cv2.CAP_PROP_FRAME_WIDTH ,width)
maCapture.set(cv2.CAP_PROP_FRAME_HEIGHT,height)
maCapture.set(cv2.CAP_PROP_FPS, 60)

Ce paramètrages va définir une limite supérieur de FPS, mais nous n’allons pas récupérer toutes les “frames” pour rendre notre code rapide et fluide.

La boucle suivante va lancer la capture infinie de la webcam.

while True:
    success, frame = maCapture.read()

Pour tester nous allons faire une capture et l’enregistrer sur un fichier image :

cv2.imwrite("testcapture.jpg", frame)

Ca fonctionne :

capture de test

Trouver le fond dans l’image

C’est la tache la plus difficile. Il s’agit de detecter sur chaque image les pixels appartenant au fond afin de les supprimer ou au moins les marquer grace à un masque pour pouvoir extraire la personne et remplacer le fond.

Pour faire ce type d’opérations, les outils comme Zoom et Microsoft Teams utilisent le machine learning, et plus précisemment des réseaux neuronaux de convolution. Ce qui permet, un résultat très correct. Cependant, si on veut faire la même chose, nous devons trouver un environnement machine learning, et l’entrainer avec une sommes d’images du même type (une grande bibliothèque d’images de personnes devant leurs webcam dans des environnement et des fond d’ecran d’intérieur de maison).

Heureusement, Google a déjà fait tout ce travail et a ouvert un réseau neuronal pré-formé pour la «segmentation des personnes» appelé BodyPix qui va nous économiser beaucoup de temps.

BodyPix est disponible en code .js (javascript) sur environnement node uniquement, et pour celà, nous allons commencer par configurer un petit environnement / projet tensorflow-gpu + node conteneurisé. Il est beaucoup plus facile de l’utiliser avec nvidia-docker que de configurer toutes les bonnes dépendances sur votre hôte, cela ne nécessite qu’un docker et un pilote GPU à jour sur l’hôte. J’ai une carte nvidia sur mon laptop ce qui facilite l’usage de ce backend.

Notre petit conteneur aura donc un fichier package.json avec les dépendances :

{
    "name": "bodypix",
    "version": "0.0.1",
    "dependencies": {
        "@tensorflow-models/body-pix": "^2.0.5",
        "@tensorflow/tfjs-node-gpu": "^1.7.1"
    }
}

Un fichier Dockerfile

# Image de base avec les prérequis Tensorflow
FROM nvcr.io/nvidia/cuda:10.0-cudnn7-runtime-ubuntu18.04
# Installation de nodejs
RUN apt update && apt install -y curl make build-essential \
    && curl -sL https://deb.nodesource.com/setup_12.x | bash - \
    && apt-get -y install nodejs \
    && mkdir /.npm \
    && chmod 777 /.npm
# S'assurer d'avoir assez de mémoire GPU sur notre conteneur
ENV TF_FORCE_GPU_ALLOW_GROWTH=true
# Installer les dépendances
WORKDIR /src
COPY package.json /src/
RUN npm install
# Configurer notre app.js comme point d'entrée du backend
COPY app.js /src/
ENTRYPOINT node /src/app.js

Pour expliquer le principe de l’application, app.js, ce script devra répondre à une image qui aura été transmise via un HTTP POST. Sur la réponse, il doit mettre un binaire (un tableau 2D de pixels binaires, où la valeur zero pixel corespond à l’arrière-plan). dans ce script nous allons appeler les méthodes de TensorFlow et bodypix (noter que ce code est très basique et inspiré de plusieurs exemples sur internet, il ne contient pas de gestion d’erreur, toute requête différente d’un post avec une image causera une erreur) :

const tfjs = require('@tensorflow/tfjs-node-gpu');
const bodyPix = require('@tensorflow-models/body-pix');
const http = require('http');
(async () => {
    const net = await bodyPix.load({
        architecture: 'MobileNetV1',
        outputStride: 16,
        multiplier: 0.75,
        quantBytes: 2,
    });
    const monServeur = http.createServer();
    monServeur.on('request', async (req, res) => {
        var chunks = [];
        req.on('data', (chunk) => {
            chunks.push(chunk);
        });
        req.on('end', async () => {
            const image = tfjs.node.decodeImage(Buffer.concat(chunks));
            segmentation = await net.segmentPerson(image, {
                flipHorizontal: false,
                internalResolution: 'medium',
                segmentationThreshold: 0.7,
            });
            res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
            res.write(Buffer.from(segmentation.data));
            res.end();
            tfjs.dispose(image);
        });
    });
    monServeur.listen(9000);
})();

De retour sur notre code python, nous allons créer une fonction qui prend comme argument notre “frame” directement capturée par maCapture sur la webcam (voir début de l’article), l’envoyer au serveur qu’on vient de créer et récupérer le masque pour le retourner :

def get_mask(frame, bodypix_url='http://localhost:9000'):
    _, data = cv2.imencode(".jpg", frame)
    r = requests.post(
        url=bodypix_url,
        data=data.tobytes(),
        headers={'Content-Type': 'application/octet-stream'})
    # transformer les bytes raw en un tableau numpy
    mask = np.frombuffer(r.content, dtype=np.uint8)
    mask = mask.reshape((frame.shape[0], frame.shape[1]))
    return mask

Le résultat ressemblera à ce masque :

masque sur base de la capture de ma webcam

Maintenant pour notre fond, nous allons utiliser cette superbe image de la place “Jamaa lafna”, l’image doit être modifiée sur un logiciel de traitement d’image pour respecter le 16:9 ratio. Et sur notre code python nous allons faire le reste des adaptations :

fond d’écran

Voici notre code :

# Importer notre fond virtuel
replacement_bg_raw = cv2.imread("fondjamaalafna.jpg")
# redimensionner pour etre de la même taille que la capture webcam
width, height = 720, 1280
replacement_bg = cv2.resize(replacement_bg_raw, (width, height))
# la magie opere ici : combiner le fond et l image webcam en utilisant le masque et son inverse pour extraire le fond et ma tete
inv_mask = 1-mask
for c in range(frame.shape[2]):
    frame[:,:,c] = frame[:,:,c]*mask + replacement_bg[:,:,c]*inv_mask

ce qui nous donne :

Combinaison du fond et de la capture webcam avec le xor du masque crée par notre serveur

Voilà à présent nous arrivons à faire cette opération image par image, le prochain chapitre va nous montrer comment exporter ce flux d’image sous forme d’une webcam virtuelle qu’on pourra par la suite utiliser dans les différents autres logiciels à la place de notre webcam classique pour disposer de cette fonctionnalité. Pour des besoins de déploiement nous allons créer un conteneur docker pour notre fakewebcam avec notre code.

Sortie de flux vidéo virtuel

Nous allons utiliser pyfakewebcam et v4l2loopback pour créer un faux appareil webcam. le tout dans un conteneur :

requirement.txt

numpy==1.18.2
opencv-python==4.2.0.32
requests==2.23.0
pyfakewebcam==0.1.0

Dockerfile

ROM python:3-buster
# avoir pip à jour
RUN pip install --upgrade pip
# install opencv dependencies
RUN apt-get update && \
    apt-get install -y \
      `# opencv requirements` \
      libsm6 libxext6 libxrender-dev \
      `# opencv video opening requirements` \
      libv4l-dev
# installer les requirements
WORKDIR /src
COPY requirements.txt /src/
RUN pip install --no-cache-dir -r /src/requirements.txt
# copier le fondvirtuel
COPY fondjamaalafna.jpg /data/
# demarrer notre script de fakewebcam
COPY fake.py /src/
ENTRYPOINT python -u fake.py

Pour créer une fausse webcam, nous aurons besoin d’accéder directement au shell linux afin d’activer le module grace à v4l2loopback et connecter cette fausse webcam au flux de sortie de notre script :

sudo apt install v4l2loopback-dkms

et configurer le module linux :

sudo modprobe -r v4l2loopback
sudo modprobe v4l2loopback devices=1 video_nr=20 card_label="v4l2loopback" exclusive_caps=1

la valeur card_label sera le nom de la camera dans certaines applications (chrome, firefox, zoom ..) et le video_nr=20 c’est le suffixe du device sur linux pour faire que /dev/video20 soit l’adresse de notre camera.

Ensuite nous allons inclure fakewebcam à notre script python avec les mêmes hauteurs et largeurs utilisées précedemment pour redimensionner l’image :

fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)

Très important à noter que pyfakewebcam s’attend à recevoir des images en RVB (rouge, vert, bleu) tandis que nos opérations OpenCV sont dans l’ordre des canaux BGR (bleu, vert, rouge).

Pour regler celà, nous allons rajouter ce bout de code :

frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)

Conclusion

Et voilà, notre code permettra donc la mise à disposition de cette webcam virtuelle qui remplace continuellement le fond de la capture webcam par une image de notre choix. Je me suis beaucoup inspiré bien sur d’articles sur internet, j’espère avoir aidé à simplifier la compréhension du code qui reste ouvert.

Des pistes de développement seraient, au lieu de remplacer le background par une image statique, le remplacer par une vidéo, ou bien par un effet de distortion (blur, flou) comme le fait ci bien Microsoft Teams.

Sources :

https://towardsdatascience.com/virtual-background-for-video-conferencing-in-python-and-opencv-a-silly-approach-5f5ad1a5abef

https://elder.dev/posts/open-source-virtual-background/

https://nodejs.org/en/docs/guides/getting-started-guide/

https://docker-curriculum.com/


Posted

in

,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *