Maintenant que beaucoup d'entre nous sont mis en quarantaine en raison de COVID-19 , les appels vidéo sont devenus beaucoup plus fréquents qu'auparavant. En particulier, le service ZOOM est soudainement devenu très populaire. La fonction Zoom la plus intéressante est probablement la prise en charge de l' arrière-plan virtuel . Il permet aux utilisateurs de remplacer de manière interactive l'arrière-plan derrière eux par n'importe quelle image ou vidéo.J'utilise Zoom au travail depuis longtemps, lors de réunions open source sur Kubernetes, généralement à partir d'un ordinateur portable d'entreprise. Maintenant, lorsque je travaille à domicile, je suis enclin à utiliser un ordinateur de bureau personnel plus puissant et plus pratique pour résoudre certaines de mes tâches open source.Malheureusement, Zoom prend uniquement en charge une méthode de suppression d'arrière-plan connue sous le nom de « incrustation chroma » ou « écran vert ». Pour utiliser cette méthode, il est nécessaire que l'arrière-plan soit représenté par une couleur unie, idéalement verte, et soit uniformément éclairé.Comme je n'ai pas d'écran vert, j'ai décidé de simplement implémenter mon propre système de suppression d'arrière-plan. Et cela, bien sûr, est bien mieux que de mettre de l'ordre dans l'appartement ou d'utiliser constamment un ordinateur portable de travail.Il s'est avéré qu'en utilisant des composants open source prêts à l'emploi et en écrivant quelques lignes de votre propre code, vous pouvez obtenir des résultats très décents.Lecture des données de la caméra
Commençons par le début et répondons à la question suivante: "Comment obtenir la vidéo d'une webcam que nous allons traiter?"Comme j'utilise Linux sur mon ordinateur personnel (quand je ne joue pas à des jeux), j'ai décidé d'utiliser les liaisons Open CV Python , que je connais déjà. En plus des liaisons V4L2 pour lire les données d'une webcam, elles incluent des fonctions de traitement vidéo de base utiles.La lecture d'un cadre depuis une webcam en python-opencv est très simple:import cv2
cap = cv2.VideoCapture('/dev/video0')
success, frame = cap.read()
Pour améliorer les résultats lorsque je travaille avec ma caméra, j'ai appliqué les paramètres suivants avant d'en capturer la vidéo:
height, width = 720, 1280
cap.set(cv2.CAP_PROP_FRAME_WIDTH ,width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT,height)
cap.set(cv2.CAP_PROP_FPS, 60)
On a le sentiment que la plupart des programmes de visioconférence limitent la vidéo à 720p @ 30 FPS ou moins. Mais nous, dans tous les cas, ne lisons pas toutes les images. Ces paramètres définissent la limite supérieure.Mettez le mécanisme de capture d'image dans une boucle. Maintenant, nous avons accès au flux vidéo de la caméra!while True:
success, frame = cap.read()
Vous pouvez enregistrer le cadre à des fins de test comme suit:cv2.imwrite("test.jpg", frame)
Après cela, nous pouvons nous assurer que la caméra fonctionne. Génial!J'espère que tu n'es pas contre ma barbeDétection d'arrière-plan
Maintenant que nous avons accès au flux vidéo, nous allons réfléchir à la façon de détecter l'arrière-plan en permettant de le remplacer en le trouvant. Mais c'est déjà une tâche assez difficile.Bien qu'il y ait un sentiment que les créateurs de Zoom ne parlent jamais exactement de la façon dont le programme supprime l'arrière-plan, la façon dont le système se comporte me fait penser à ce qui aurait pu se passer des réseaux de neurones. C'est difficile à expliquer, mais les résultats ressemblent exactement à ça. En outre, j'ai trouvé un article sur la façon dont Microsoft Teams implémente le flou d'arrière-plan à l' aide d'un réseau neuronal convolutionnel .En principe, la création de votre propre réseau de neurones n'est pas si difficile. Il existe de nombreux articles et articles scientifiques sur la segmentation d'images.. Il existe de nombreuses bibliothèques et outils open source. Mais nous avons besoin d'un ensemble de données très spécialisé pour obtenir de bons résultats.En particulier, nous avons besoin de beaucoup d'images ressemblant à celles obtenues à partir d'une webcam, avec une image parfaite d'une personne au premier plan. Chaque pixel d'une telle image doit être marqué comme différent de l'arrière-plan.La construction d'un tel ensemble de données en préparation de la formation d'un réseau neuronal peut ne pas nécessiter beaucoup d'efforts. Cela est dû au fait que l'équipe de chercheurs de Google a déjà fait tout son possible et a mis en open source un réseau neuronal pré-formé pour segmenter les gens. Ce réseau s'appelle BodyPix . Il fonctionne très bien!BodyPix n'est désormais disponible que sous une forme adaptée à TensorFlow.js. Par conséquent, il est plus facile d'appliquer à l'aide de la bibliothèque body-pix-node .Pour accélérer la sortie réseau (prévisions) dans le navigateur, il est préférable d'utiliser le backend WebGL , mais dans l'environnement Node.js , vous pouvez utiliser le backend Tensorflow GPU (notez que cela nécessitera une carte vidéo de NVIDIA , que j'ai).Afin de simplifier la configuration du projet, nous utiliserons un petit environnement conteneurisé qui fournit le GPU TensorFlow et Node.js. Tout utiliser avec nvidia-docker- beaucoup plus facile que de collecter vous-même les dépendances nécessaires sur votre ordinateur. Pour ce faire, vous n'avez besoin que de Docker et des derniers pilotes graphiques sur votre ordinateur.Voici le contenu du fichier bodypix/package.json
:{
"name": "bodypix",
"version": "0.0.1",
"dependencies": {
"@tensorflow-models/body-pix": "^2.0.5",
"@tensorflow/tfjs-node-gpu": "^1.7.1"
}
}
Voici le dossier bodypix/Dockerfile
:
FROM nvcr.io/nvidia/cuda:10.0-cudnn7-runtime-ubuntu18.04
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
ENV TF_FORCE_GPU_ALLOW_GROWTH=true
WORKDIR /src
COPY package.json /src/
RUN npm install
COPY app.js /src/
ENTRYPOINT node /src/app.js
Parlons maintenant de l'obtention de résultats. Mais je vous préviens tout de suite: je ne suis pas un expert Node.js! Ce n'est que le résultat de mes expériences du soir, alors soyez indulgents avec moi :-).Le script simple suivant est occupé à traiter une image de masque binaire envoyée au serveur à l'aide d'une requête HTTP POST. Un masque est un tableau bidimensionnel de pixels. Les pixels représentés par des zéros sont l'arrière-plan.Voici le code du fichier app.js
:const tf = 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 server = http.createServer();
server.on('request', async (req, res) => {
var chunks = [];
req.on('data', (chunk) => {
chunks.push(chunk);
});
req.on('end', async () => {
const image = tf.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();
tf.dispose(image);
});
});
server.listen(9000);
})();
Pour convertir un cadre en masque, nous pouvons, dans un script Python, utiliser le paquet numpy et request :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'})
mask = np.frombuffer(r.content, dtype=np.uint8)
mask = mask.reshape((frame.shape[0], frame.shape[1]))
return mask
Le résultat est approximativement le suivant.MasquePendant que je faisais tout cela, je suis tombé sur letweet suivant .C'est certainement le meilleur fond pour les appels vidéo.Maintenant que nous avons un masque pour séparer le premier plan de l'arrière-plan, remplacer l'arrière-plan par quelque chose d'autre sera très simple.J'ai pris l'image d'arrière-plan de la branche de tweet et je l'ai coupée pour obtenir une image 16x9.Image d'arrière-planAprès cela, j'ai fait ce qui suit:
replacement_bg_raw = cv2.imread("background.jpg")
width, height = 720, 1280
replacement_bg = cv2.resize(replacement_bg_raw, (width, height))
inv_mask = 1-mask
for c in range(frame.shape[2]):
frame[:,:,c] = frame[:,:,c]*mask + replacement_bg[:,:,c]*inv_mask
C'est ce que j'ai obtenu après ça.Le résultat du remplacement de l'arrière-plan.Un tel masque n'est évidemment pas assez précis, la raison en est les compromis de performance que nous avons faits lors de la configuration de BodyPix. En général, alors que tout semble plus ou moins tolérant.Mais, quand j'ai regardé ce contexte, une idée m'est venue.Expériences intéressantes
Maintenant que nous avons compris comment masquer, nous allons demander comment améliorer le résultat.La première étape évidente consiste à adoucir les bords du masque. Par exemple, cela peut être fait comme ceci:def post_process_mask(mask):
mask = cv2.dilate(mask, np.ones((10,10), np.uint8) , iterations=1)
mask = cv2.erode(mask, np.ones((10,10), np.uint8) , iterations=1)
return mask
Cela améliorera un peu la situation, mais il n'y a pas beaucoup de progrès. Et un simple remplacement est assez ennuyeux. Mais, puisque nous sommes arrivés à tout cela nous-mêmes, cela signifie que nous pouvons tout faire avec l'image, et pas seulement supprimer l'arrière-plan.Étant donné que nous utilisons un arrière-plan virtuel de Star Wars, j'ai décidé de créer un effet hologramme afin de rendre l'image plus intéressante. De plus, cela vous permet de lisser le flou du masque.Tout d'abord, mettez à jour le code de post-traitement:def post_process_mask(mask):
mask = cv2.dilate(mask, np.ones((10,10), np.uint8) , iterations=1)
mask = cv2.blur(mask.astype(float), (30,30))
return mask
Les bords sont maintenant flous. C'est bien, mais nous devons encore créer un effet hologramme.Les hologrammes hollywoodiens ont généralement les propriétés suivantes:- Une image pâle ou monochrome - comme si elle était dessinée par un laser brillant.
- Un effet qui rappelle les lignes de balayage ou quelque chose comme une grille - comme si l'image était affichée en plusieurs rayons.
- «Effet fantôme» - comme si la projection était effectuée en couches ou comme si la distance correcte à laquelle elle devait être affichée ne serait pas maintenue lors de la création de la projection.
Tous ces effets peuvent être implémentés étape par étape.Tout d'abord, pour colorer l'image dans une nuance de bleu, nous pouvons utiliser la méthode applyColorMap
:
holo = cv2.applyColorMap(frame, cv2.COLORMAP_WINTER)
Ensuite - ajoutez une ligne de balayage avec un effet rappelant de laisser en demi-teinte:
bandLength, bandGap = 2, 3
for y in range(holo.shape[0]):
if y % (bandLength+bandGap) < bandLength:
holo[y,:,:] = holo[y,:,:] * np.random.uniform(0.1, 0.3)
Ensuite, nous implémentons «l'effet fantôme» en ajoutant des copies pondérées décalées de l'effet actuel à l'image:
def shift_img(img, dx, dy):
img = np.roll(img, dy, axis=0)
img = np.roll(img, dx, axis=1)
if dy>0:
img[:dy, :] = 0
elif dy<0:
img[dy:, :] = 0
if dx>0:
img[:, :dx] = 0
elif dx<0:
img[:, dx:] = 0
return img
holo2 = cv2.addWeighted(holo, 0.2, shift_img(holo1.copy(), 5, 5), 0.8, 0)
holo2 = cv2.addWeighted(holo2, 0.4, shift_img(holo1.copy(), -5, -5), 0.6, 0)
Et enfin, nous voulons conserver certaines des couleurs d'origine, nous combinons donc l'effet holographique avec le cadre d'origine, en faisant quelque chose de similaire à l'ajout de «l'effet fantôme»:holo_done = cv2.addWeighted(img, 0.5, holo2, 0.6, 0)
Voici à quoi ressemble un cadre avec un effet hologramme:Un cadre avec un effet hologrammeCe cadre lui-même semble assez bon.Essayons maintenant de le combiner avec l'arrière-plan.Image superposée sur le fond.Terminé! (Je promets - ce genre de vidéo sera plus intéressant).Sortie vidéo
Et maintenant, je dois dire que nous avons manqué quelque chose ici. Le fait est que nous ne pouvons toujours pas utiliser tout cela pour passer des appels vidéo.Afin de résoudre ce problème, nous utiliserons pyfakewebcam et v4l2loopback pour créer une webcam factice.De plus, nous prévoyons de connecter cette caméra au Docker.Créez d'abord un fichier de fakecam/requirements.txt
description des dépendances:numpy==1.18.2
opencv-python==4.2.0.32
requests==2.23.0
pyfakewebcam==0.1.0
Créez maintenant un fichier fakecam/Dockerfile
pour l'application qui implémente les capacités d'une caméra factice:FROM python:3-buster
RUN pip install --upgrade pip
RUN apt-get update && \
apt-get install -y \
`
libsm6 libxext6 libxrender-dev \
`
libv4l-dev
WORKDIR /src
COPY requirements.txt /src/
RUN pip install --no-cache-dir -r /src/requirements.txt
COPY background.jpg /data/
COPY fake.py /src/
ENTRYPOINT python -u fake.py
Maintenant, à partir de la ligne de commande, installez v4l2loopback:sudo apt install v4l2loopback-dkms
Configurer une caméra factice:sudo modprobe -r v4l2loopback
sudo modprobe v4l2loopback devices=1 video_nr=20 card_label="v4l2loopback" exclusive_caps=1
Pour garantir la fonctionnalité de certaines applications (Chrome, Zoom), nous avons besoin d'un paramètre exclusive_caps
. La marque card_label
est définie uniquement pour garantir la commodité du choix d'un appareil photo dans les applications. L'indication du numéro video_nr=20
conduit à la création de l'appareil /dev/video20
si le numéro correspondant n'est pas occupé, et il est peu probable qu'il soit occupé.Nous allons maintenant modifier le script pour créer une caméra factice:
fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)
Il convient de noter que pyfakewebcam attend des images avec des canaux RVB (rouge, vert, bleu - rouge, vert, bleu), et Open CV fonctionne avec l'ordre des canaux BGR (bleu, vert, rouge).Vous pouvez résoudre ce problème avant de sortir le cadre, puis envoyer le cadre comme ceci:frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)
Voici le code de script complet fakecam/fake.py
:import os
import cv2
import numpy as np
import requests
import pyfakewebcam
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'})
mask = np.frombuffer(r.content, dtype=np.uint8)
mask = mask.reshape((frame.shape[0], frame.shape[1]))
return mask
def post_process_mask(mask):
mask = cv2.dilate(mask, np.ones((10,10), np.uint8) , iterations=1)
mask = cv2.blur(mask.astype(float), (30,30))
return mask
def shift_image(img, dx, dy):
img = np.roll(img, dy, axis=0)
img = np.roll(img, dx, axis=1)
if dy>0:
img[:dy, :] = 0
elif dy<0:
img[dy:, :] = 0
if dx>0:
img[:, :dx] = 0
elif dx<0:
img[:, dx:] = 0
return img
def hologram_effect(img):
holo = cv2.applyColorMap(img, cv2.COLORMAP_WINTER)
bandLength, bandGap = 2, 3
for y in range(holo.shape[0]):
if y % (bandLength+bandGap) < bandLength:
holo[y,:,:] = holo[y,:,:] * np.random.uniform(0.1, 0.3)
holo_blur = cv2.addWeighted(holo, 0.2, shift_image(holo.copy(), 5, 5), 0.8, 0)
holo_blur = cv2.addWeighted(holo_blur, 0.4, shift_image(holo.copy(), -5, -5), 0.6, 0)
out = cv2.addWeighted(img, 0.5, holo_blur, 0.6, 0)
return out
def get_frame(cap, background_scaled):
_, frame = cap.read()
mask = None
while mask is None:
try:
mask = get_mask(frame)
except requests.RequestException:
print("mask request failed, retrying")
mask = post_process_mask(mask)
frame = hologram_effect(frame)
inv_mask = 1-mask
for c in range(frame.shape[2]):
frame[:,:,c] = frame[:,:,c]*mask + background_scaled[:,:,c]*inv_mask
return frame
cap = cv2.VideoCapture('/dev/video0')
height, width = 720, 1280
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
cap.set(cv2.CAP_PROP_FPS, 60)
fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)
background = cv2.imread("/data/background.jpg")
background_scaled = cv2.resize(background, (width, height))
while True:
frame = get_frame(cap, background_scaled)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)
Maintenant, rassemblez les images:docker build -t bodypix ./bodypix
docker build -t fakecam ./fakecam
Exécutez-les:
docker network create --driver bridge fakecam
docker run -d \
--name=bodypix \
--network=fakecam \
--gpus=all --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \
bodypix
docker run -d \
--name=fakecam \
--network=fakecam \
-p 8080:8080 \
-u "$$(id -u):$$(getent group video | cut -d: -f3)" \
$$(find /dev -name 'video*' -printf "--device %p ") \
fakecam
Il ne reste plus qu'à considérer que cela doit être démarré avant l'ouverture de la caméra lorsque vous travaillez avec des applications. Et dans Zoom ou ailleurs, vous devez sélectionner une caméra v4l2loopback
/ /dev/video20
.Sommaire
Voici un clip qui montre les résultats de mon travail. Résultat du changement de fondVoir! J'appelle depuis le Millennium Falcon en utilisant la pile de technologies open source pour travailler avec la caméra!Ce que j'ai fait, j'ai vraiment aimé. Et je vais certainement profiter de tout cela lors de la prochaine vidéoconférence.Chers lecteurs! Envisagez-vous de changer ce qui est visible pendant les appels vidéo derrière vous pour autre chose?