现在,由于COVID-19,我们中的许多人被隔离了,视频通话比以前更加频繁。特别是,ZOOM服务突然变得非常流行。最有趣的缩放功能可能是对虚拟背景的支持。它允许用户以任何图像或视频交互地替换其背后的背景。 在Kubernetes上的开源会议上,我在工作中使用Zoom已有很长时间了,通常是使用企业笔记本电脑来实现的。现在,当我在家工作时,我倾向于使用功能更强大,更方便的个人台式计算机来解决我的一些开源任务。不幸的是,缩放仅支持称为“ 色度键 ”或“ 绿屏 ” 的背景去除方法。要使用此方法,必须用某种纯色(理想情况下为绿色)表示背景并均匀照明。由于我没有绿屏,因此我决定简单地实施自己的后台删除系统。当然,这比在公寓中整理物品或不断使用工作用的笔记本电脑要好得多。事实证明,使用现成的开源组件并编写几行自己的代码,您可以获得非常不错的结果。读取相机数据
让我们从头开始,回答以下问题:“如何从将要处理的网络摄像头中获取视频?”由于我在家用计算机上使用Linux(当我不玩游戏时),因此我决定使用我已经知道的Open CV Python绑定 。除了用于从网络摄像头读取数据的V4L2绑定之外,它们还包括有用的基本视频处理功能。
从python-opencv中的摄像头读取帧非常简单:import cv2
cap = cv2.VideoCapture('/dev/video0')
success, frame = cap.read()
为了改善使用相机的效果,我在从相机拍摄视频之前应用了以下设置:
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)
感觉大多数视频会议程序会将视频限制为720p @ 30 FPS或更低。但是,无论如何,我们可能无法读取每一帧。这样的设置设置上限。将帧捕获机制置于一个循环中。现在,我们可以从摄像机访问视频流了!while True:
success, frame = cap.read()
您可以保存框架以进行测试,如下所示:cv2.imwrite("test.jpg", frame)
此后,我们可以确保相机正在工作。大!我希望你不反对我的胡须背景检测
现在,我们可以访问视频流了,我们将考虑如何通过找到背景来替换背景来检测背景。但这已经是一个相当困难的任务。尽管有一种感觉,Zoom的创建者从未谈论过该程序如何确切地消除背景,但系统的行为方式使我思考了如果没有神经网络,该怎么办。很难解释,但结果看起来完全像那样。此外,我发现了一篇有关Microsoft Teams如何使用卷积神经网络实现背景模糊的文章。
原则上,创建自己的神经网络并不那么困难。关于图像分割有很多文章和科学论文。。有很多开源库和工具。但是我们需要一个非常专业的数据集才能获得良好的结果。特别是,我们需要大量类似于从网络摄像头获得的图像,并在前景中完美呈现人的照片。此类图片的每个像素应标记为与背景不同。建立此类数据集以准备训练神经网络可能不需要太多的工作。这是由于以下事实:来自Google的研究人员团队已经尽了最大的努力,并将预先训练的神经网络用于细分人员,并将其投入开源。该网络称为BodyPix。效果很好!现在仅以适用于TensorFlow.js的形式提供BodyPix。结果,最简单的方法是使用body-pix-node库。为了加快浏览器中的网络输出(预测),最好使用WebGL后端,但是在Node.js环境中,您可以使用Tensorflow GPU后端(请注意,为此您将需要NVIDIA提供的视频卡)。为了简化项目设置,我们将使用一个小的容器化环境,该环境提供TensorFlow GPU和Node.js。与nvidia-docker一起使用-比自己收集计算机上的必要依赖关系容易得多。为此,您只需要计算机上的Docker和最新的图形驱动程序。这是文件的内容bodypix/package.json
:{
"name": "bodypix",
"version": "0.0.1",
"dependencies": {
"@tensorflow-models/body-pix": "^2.0.5",
"@tensorflow/tfjs-node-gpu": "^1.7.1"
}
}
这是文件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
现在让我们谈谈获得结果。但我立即警告您:我不是Node.js专家!这只是我晚上实验的结果,所以请对我宽容:-)。以下简单脚本正在忙于处理使用HTTP POST请求发送到服务器的二进制掩码图像。遮罩是像素的二维阵列。零表示的像素是背景。这是文件代码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);
})();
要将框架转换为蒙版,我们可以在Python脚本中使用numpy并请求包: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
结果大致如下。遮罩在执行所有操作时,我遇到了下一条推文。绝对是视频通话的最佳背景,现在我们有了一个将前景与背景分开的遮罩,用其他东西替换背景将非常简单。我从tweet分支中获取了背景图像,并将其剪切后得到了16x9的图片。背景图片后,我做了以下内容:
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
那就是我得到的。替换背景的结果此蒙版显然不够准确,其原因是我们在设置BodyPix时进行的性能折衷。总的来说,尽管一切看起来都可以容忍。但是,当我看这个背景时,一个主意浮现在我的脑海。有趣的实验
现在,我们已经找到了遮罩的方法,我们将询问如何改善效果。显而易见的第一步是软化蒙版的边缘。例如,可以这样完成: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
这样可以改善情况,但进展不大。而且简单的替换很无聊。但是,由于我们自己完成了所有这些工作,因此这意味着我们可以对图片进行任何操作,而不仅仅是删除背景。鉴于我们使用的是《星球大战》的虚拟背景,我决定创建全息图效果,以使图片更有趣。此外,这还可以使蒙版的模糊变得平滑。首先,更新后处理代码: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
现在,边缘变得模糊了。很好,但是我们仍然需要创建全息图效果。好莱坞全息图通常具有以下属性:- 浅色或单色图片-好像是由明亮的激光绘制的。
- 这种效果让人想起扫描线或诸如网格之类的东西-就像图像显示在多条光线中一样。
- “幻影效果”-好像投影是分层执行的,或者好像在创建投影期间未保持应显示的正确距离。
所有这些效果都可以逐步实现。首先,要将图像着色为蓝色阴影,我们可以使用方法applyColorMap
:
holo = cv2.applyColorMap(frame, 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)
接下来,我们通过向图像添加当前效果的偏移加权副本来实现“鬼影效果”:
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)
最后,我们要保留一些原始颜色,因此我们将全息效果与原始帧结合在一起,进行类似于添加“幻影效果”的操作:holo_done = cv2.addWeighted(img, 0.5, holo2, 0.6, 0)
这是具有全息效果的框架的外观:具有全息效果的镜架此镜架本身看起来非常不错。现在,让我们尝试将其与背景结合起来。背景图像叠加。完成!(我保证-这种视频看起来会更有趣)。视频输出
现在我必须说,我们在这里错过了一些东西。事实是,我们仍然不能使用所有这些来进行视频通话。为了解决这个问题,我们将使用pyfakewebcam和v4l2loopback创建一个虚拟的网络摄像头。此外,我们计划将此摄像头连接到Docker。首先,创建一个fakecam/requirements.txt
依赖项描述文件:numpy==1.18.2
opencv-python==4.2.0.32
requests==2.23.0
pyfakewebcam==0.1.0
现在fakecam/Dockerfile
,为实现虚拟相机功能的应用程序创建文件: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
现在,从命令行安装v4l2loopback:sudo apt install v4l2loopback-dkms
设置虚拟摄像机:sudo modprobe -r v4l2loopback
sudo modprobe v4l2loopback devices=1 video_nr=20 card_label="v4l2loopback" exclusive_caps=1
为了确保某些应用程序(Chrome,Zoom)的功能,我们需要进行设置exclusive_caps
。card_label
设置此标记仅是为了确保在应用程序中选择相机的便利。如果相应的号码不忙,并且不太可能忙,则指示号码video_nr=20
会导致设备的创建/dev/video20
。现在,我们将对脚本进行更改以创建虚拟摄像机:
fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)
应该注意的是,pyfakewebcam期望具有RGB通道(红色,绿色,蓝色-红色,绿色,蓝色)的图像,而Open CV则按BGR通道(蓝色,绿色,红色)的顺序工作。您可以在输出帧之前解决此问题,然后像这样发送帧:frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)
这是完整的脚本代码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)
现在收集图像:docker build -t bodypix ./bodypix
docker build -t fakecam ./fakecam
运行它们:
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
只需考虑在使用任何应用程序时必须在打开相机之前就开始此操作。在Zoom或其他位置,您需要选择一个摄像机v4l2loopback
/ /dev/video20
。摘要
这是演示我的工作成果的剪辑。背景变化结果见!我是从千禧猎鹰号召集的,使用的是开放源代码技术堆栈,可以用于相机!我所做的,我真的很喜欢。在下一次视频会议上,我一定会利用所有这些优势。亲爱的读者们!您是否打算将视频通话后的可见内容更改为其他内容?