Ahora que muchos de nosotros estamos en cuarentena debido a COVID-19 , las videollamadas se han vuelto mucho más comunes que antes. En particular, el servicio ZOOM de repente se hizo muy popular. Probablemente la característica de Zoom más interesante es la compatibilidad con el Fondo virtual . Permite a los usuarios reemplazar interactivamente el fondo detrás de ellos con cualquier imagen o video.He estado usando Zoom durante mucho tiempo en el trabajo, en reuniones de código abierto sobre Kubernetes, generalmente usando una computadora portátil corporativa. Ahora, en el modo de trabajo desde casa, me inclino a usar una computadora de escritorio personal más potente y conveniente para resolver algunas de mis tareas de código abierto.Desafortunadamente, Zoom solo admite un método de eliminación de fondo conocido como " clave de croma " o " pantalla verde ". Para utilizar este método, es necesario que el fondo esté representado por un color sólido, idealmente verde, y esté iluminado de manera uniforme.Como no tengo una pantalla verde, decidí simplemente implementar mi propio sistema de eliminación de fondo. Y esto, por supuesto, es mucho mejor que poner las cosas en orden en el apartamento, o el uso constante de una computadora portátil de trabajo.Al final resultó que, usando componentes de código abierto listos para usar y escribiendo solo unas pocas líneas de su propio código, puede obtener resultados muy decentes.Leer datos de la cámara
Comencemos desde el principio y respondamos la siguiente pregunta: "¿Cómo obtener video de una cámara web que procesaremos?"Como uso Linux en la computadora de mi casa (cuando no juego), decidí usar los enlaces Open CV Python , que ya conozco. Además de los enlaces V4L2 para leer datos de una cámara web, incluyen funciones básicas útiles de procesamiento de video.Leer un marco desde una cámara web en python-opencv es muy simple:import cv2
cap = cv2.VideoCapture('/dev/video0')
success, frame = cap.read()
Para mejorar los resultados al trabajar con mi cámara, apliqué la siguiente configuración antes de capturar video de ella:
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)
Parece que la mayoría de los programas de videoconferencia limitan el video a 720p a 30 FPS o menos. Pero, en cualquier caso, es posible que no leamos todos los cuadros. Dichas configuraciones establecen el límite superior.Ponga el mecanismo de captura de cuadros en un bucle. ¡Ahora tenemos acceso a la transmisión de video desde la cámara!while True:
success, frame = cap.read()
Puede guardar el marco para fines de prueba de la siguiente manera:cv2.imwrite("test.jpg", frame)
Después de eso, podemos asegurarnos de que la cámara esté funcionando. ¡Excelente!Espero que no estes en contra de mi barbaDetección de fondo
Ahora que tenemos acceso a la transmisión de video, pensaremos en cómo detectar el fondo haciendo posible reemplazarlo al encontrarlo. Pero esta ya es una tarea bastante difícil.Aunque existe la sensación de que los creadores de Zoom nunca hablan exactamente de cómo el programa elimina el fondo, la forma en que se comporta el sistema me hace pensar en lo que podría haber hecho sin redes neuronales. Es difícil de explicar, pero los resultados se ven exactamente así. Además, encontré un artículo sobre cómo Microsoft Teams implementa el desenfoque de fondo utilizando una red neuronal convolucional .En principio, crear su propia red neuronal no es tan difícil. Hay muchos artículos y artículos científicos sobre segmentación de imágenes.. Hay muchas bibliotecas y herramientas de código abierto. Pero necesitamos un conjunto de datos muy especializado para obtener buenos resultados.En particular, necesitamos muchas imágenes que recuerden a las obtenidas de una cámara web, con una imagen perfecta de una persona en primer plano. Cada píxel de dicha imagen debe marcarse como diferente del fondo.Construir un conjunto de datos como preparación para entrenar una red neuronal puede no requerir mucho esfuerzo. Esto se debe al hecho de que el equipo de investigadores de Google ya ha hecho todo lo posible y ha puesto en código abierto una red neuronal pre-entrenada para segmentar a las personas. Esta red se llama BodyPix . ¡Funciona muy bien!BodyPix ahora solo está disponible en una forma adecuada para TensorFlow.js. Como resultado, es más fácil de aplicar usando la biblioteca body-pix-node .Para acelerar la salida de la red (pronóstico) en el navegador, es preferible usar el backend WebGL , pero en el entorno Node.js puede usar el backend GPU Tensorflow (tenga en cuenta que esto requerirá una tarjeta de video de NVIDIA , que tengo).Para simplificar la configuración del proyecto, utilizaremos un pequeño entorno en contenedores que proporciona la GPU TensorFlow y Node.js. Utilizándolo todo con nvidia-docker- mucho más fácil que recopilar las dependencias necesarias en su computadora usted mismo. Para hacer esto, solo necesita Docker y los últimos controladores de gráficos en su computadora.Aquí está el contenido del archivo bodypix/package.json
:{
"name": "bodypix",
"version": "0.0.1",
"dependencies": {
"@tensorflow-models/body-pix": "^2.0.5",
"@tensorflow/tfjs-node-gpu": "^1.7.1"
}
}
Aquí está el archivo 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
Ahora hablemos de obtener resultados. Pero te advierto de inmediato: ¡no soy un experto en Node.js! Esto es solo el resultado de mis experimentos nocturnos, así que sé indulgente conmigo :-).El siguiente script simple está ocupado procesando una imagen de máscara binaria enviada al servidor utilizando una solicitud HTTP POST. Una máscara es una matriz bidimensional de píxeles. Los píxeles representados por ceros son el fondo.Aquí está el código del archivo 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);
})();
Para convertir un marco en una máscara, nosotros, en un script de Python, podemos usar los paquetes numpy y 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
El resultado es aproximadamente el siguiente.MáscaraMientras hacía todo esto, me encontré con el siguiente tweet.Este es definitivamente el mejor fondo para las videollamadas.Ahora que tenemos una máscara para separar el primer plano del fondo, reemplazar el fondo con algo más será muy simple.Tomé la imagen de fondo de la rama de tweet y la corté para obtener una imagen de 16x9.Imagen de fondoDespués de eso hice lo siguiente:
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
Eso es lo que obtuve después de eso.El resultado de reemplazar el fondoObviamente, esta máscara no es lo suficientemente precisa, la razón de esto son las compensaciones de rendimiento que hicimos al configurar BodyPix. En general, mientras todo parece más o menos tolerante.Pero cuando miré este fondo, se me ocurrió una idea.Experimentos interesantes
Ahora que hemos descubierto cómo enmascarar, preguntaremos cómo mejorar el resultado.El primer paso obvio es suavizar los bordes de la máscara. Por ejemplo, esto se puede hacer así: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
Esto mejorará un poco la situación, pero no hay mucho progreso. Y un reemplazo simple es bastante aburrido. Pero, dado que llegamos a todo esto nosotros mismos, esto significa que podemos hacer cualquier cosa con la imagen, y no solo eliminar el fondo.Dado que estamos usando un fondo virtual de Star Wars, decidí crear un efecto de holograma para hacer que la imagen sea más interesante. Esto, además, le permite suavizar el desenfoque de la máscara.Primero, actualice el código de procesamiento posterior: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
Los bordes ahora están borrosos. Esto es bueno, pero aún necesitamos crear un efecto de holograma.Los hologramas de Hollywood generalmente tienen las siguientes propiedades:- Una imagen en color pálido o monocromo, como dibujada por un láser brillante.
- Un efecto que recuerda a las líneas de exploración o algo parecido a una cuadrícula, como si la imagen se mostrara en varios rayos.
- “Efecto fantasma”: como si la proyección se realizara en capas o como si no se mantuviera la distancia correcta a la que debería mostrarse al crear la proyección.
Todos estos efectos se pueden implementar paso a paso.Primero, para colorear la imagen en un tono de azul, podemos usar el método applyColorMap
:
holo = cv2.applyColorMap(frame, cv2.COLORMAP_WINTER)
A continuación, agregue una línea de barrido con un efecto que recuerde dejar en tono medio:
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)
A continuación, implementamos el "efecto fantasma" agregando copias ponderadas desplazadas del efecto actual a la imagen:
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)
Y finalmente, queremos mantener algunos de los colores originales, por lo que combinamos el efecto holográfico con el marco original, haciendo algo similar a agregar el "efecto fantasma":holo_done = cv2.addWeighted(img, 0.5, holo2, 0.6, 0)
Así es como se ve un cuadro con un efecto de holograma:Un cuadro con efecto de hologramaEste cuadro en sí se ve bastante bien.Ahora intentemos combinarlo con el fondo.Superpuesto imagen en el fondo.Hecho! (Lo prometo, este tipo de video se verá más interesante).Salida de video
Y ahora debo decir que nos hemos perdido algo aquí. El hecho es que todavía no podemos usar todo esto para hacer videollamadas.Para solucionar esto, utilizaremos pyfakewebcam y v4l2loopback para crear una cámara web falsa.Además, planeamos conectar esta cámara al Docker.Primero, cree un archivo de fakecam/requirements.txt
descripción de dependencia:numpy==1.18.2
opencv-python==4.2.0.32
requests==2.23.0
pyfakewebcam==0.1.0
Ahora cree un archivo fakecam/Dockerfile
para la aplicación que implemente las capacidades de una cámara falsa: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
Ahora, desde la línea de comandos, instale v4l2loopback:sudo apt install v4l2loopback-dkms
Configurar una cámara falsa:sudo modprobe -r v4l2loopback
sudo modprobe v4l2loopback devices=1 video_nr=20 card_label="v4l2loopback" exclusive_caps=1
Para garantizar la funcionalidad de algunas aplicaciones (Chrome, Zoom), necesitamos una configuración exclusive_caps
. La marca card_label
se establece solo para garantizar la conveniencia de elegir una cámara en las aplicaciones. La indicación del número video_nr=20
lleva a la creación del dispositivo /dev/video20
si el número correspondiente no está ocupado y es poco probable que esté ocupado.Ahora haremos cambios en el script para crear una cámara falsa:
fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)
Cabe señalar que pyfakewebcam espera imágenes con canales RGB (Rojo, Verde, Azul - rojo, verde, azul), y Open CV funciona con el orden de los canales BGR (Azul, Verde, Rojo).Puede arreglar esto antes de generar el marco y luego enviar el marco de esta manera:frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)
Aquí está el código completo del script 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)
Ahora recoge las imágenes:docker build -t bodypix ./bodypix
docker build -t fakecam ./fakecam
Ejecútalos:
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
Solo queda considerar que esto debe iniciarse antes de abrir la cámara cuando se trabaja con cualquier aplicación. Y en Zoom o en otro lugar, debe seleccionar una cámara v4l2loopback
/ /dev/video20
.Resumen
Aquí hay un clip que muestra los resultados de mi trabajo. Resultado de cambio de fondoVer! ¡Llamo desde el Halcón Milenario usando la pila de tecnología de código abierto para trabajar con la cámara!Lo que hice me gustó mucho. Y definitivamente aprovecharé todo esto en la próxima videoconferencia.¡Queridos lectores! ¿Estás planeando cambiar lo que está visible durante las videollamadas detrás de ti por algo más?