Sekarang banyak dari kita dikarantina karena COVID-19 , panggilan video menjadi jauh lebih umum daripada sebelumnya. Secara khusus, layanan ZOOM tiba-tiba menjadi sangat populer. Mungkin fitur Zoom yang paling menarik adalah dukungan untuk Latar Belakang Virtual . Ini memungkinkan pengguna untuk mengganti latar belakang di belakangnya dengan gambar atau video secara interaktif.Saya telah menggunakan Zoom di tempat kerja untuk waktu yang lama, pada pertemuan sumber terbuka di Kubernetes, biasanya melakukan ini dari laptop perusahaan. Sekarang, ketika saya bekerja dari rumah, saya cenderung menggunakan komputer desktop pribadi yang lebih kuat dan nyaman untuk menyelesaikan beberapa tugas open source saya.Sayangnya, Zoom hanya mendukung metode penghapusan latar belakang yang dikenal sebagai " kunci chroma " atau " layar hijau ". Untuk menggunakan metode ini, latar belakang harus diwakili oleh beberapa warna solid, idealnya hijau, dan menyala secara seragam.Karena saya tidak memiliki layar hijau, saya memutuskan untuk hanya menerapkan sistem penghapusan latar belakang saya sendiri. Dan ini, tentu saja, jauh lebih baik daripada menertibkan apartemen, atau penggunaan laptop kantor secara konstan.Ternyata, menggunakan komponen open source yang sudah jadi dan menulis hanya beberapa baris kode Anda sendiri, Anda bisa mendapatkan hasil yang sangat baik.Membaca data kamera
Mari kita mulai dari awal dan menjawab pertanyaan berikut: "Bagaimana cara mendapatkan video dari webcam yang akan kami proses?"Karena saya menggunakan Linux di komputer rumah saya (ketika saya tidak bermain game), saya memutuskan untuk menggunakan binding Open CV Python yang sudah saya kenal. Selain V4L2- binding untuk membaca data dari webcam, mereka termasuk fungsi pemrosesan video dasar yang berguna.Membaca bingkai dari webcam di python-opencv sangat sederhana:import cv2
cap = cv2.VideoCapture('/dev/video0')
success, frame = cap.read()
Untuk meningkatkan hasil saat bekerja dengan kamera saya, saya menerapkan pengaturan berikut sebelum mengambil video dari itu:
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)
Ada perasaan bahwa sebagian besar program konferensi video membatasi video hingga 720p @ 30 FPS atau lebih rendah. Tapi kami, bagaimanapun, mungkin tidak membaca setiap frame. Pengaturan semacam itu menetapkan batas atas.Masukkan mekanisme penangkapan bingkai dalam satu lingkaran. Sekarang kita memiliki akses ke aliran video dari kamera!while True:
success, frame = cap.read()
Anda dapat menyimpan bingkai untuk tujuan pengujian sebagai berikut:cv2.imwrite("test.jpg", frame)
Setelah itu, kami dapat memastikan bahwa kamera berfungsi. Bagus!Saya harap Anda tidak menentang janggut sayaDeteksi latar belakang
Sekarang kami memiliki akses ke aliran video, kami akan berpikir tentang cara mendeteksi latar belakang dengan memungkinkan untuk menggantinya dengan menemukannya. Tapi ini sudah tugas yang agak sulit.Meskipun ada perasaan bahwa pencipta Zoom tidak pernah berbicara tentang bagaimana program menghapus latar belakang, cara sistem berperilaku membuat saya berpikir tentang apa yang bisa dilakukan tanpa jaringan saraf. Sulit untuk dijelaskan, tetapi hasilnya terlihat persis seperti itu. Selain itu, saya menemukan sebuah artikel tentang bagaimana Microsoft Tim mengimplementasikan background blur menggunakan jaringan saraf convolutional .Pada prinsipnya, membuat jaringan saraf Anda sendiri tidak begitu sulit. Ada banyak artikel dan makalah ilmiah tentang segmentasi gambar.. Ada banyak perpustakaan dan alat sumber terbuka. Tetapi kami membutuhkan dataset yang sangat khusus untuk mendapatkan hasil yang baik.Secara khusus, kita membutuhkan banyak gambar menyerupai yang diperoleh dari webcam, dengan gambar sempurna dari seseorang di latar depan. Setiap piksel dari gambar seperti itu harus ditandai sebagai berbeda dari latar belakang.Membangun dataset seperti itu dalam persiapan untuk pelatihan jaringan saraf mungkin tidak membutuhkan banyak usaha. Hal ini disebabkan oleh fakta bahwa tim peneliti dari Google telah melakukan semua yang paling sulit dan memasukkan ke dalam open source jaringan saraf pra-terlatih untuk segmentasi orang. Jaringan ini disebut BodyPix . Ini bekerja dengan sangat baik!BodyPix sekarang hanya tersedia dalam bentuk yang cocok untuk TensorFlow.js. Akibatnya, paling mudah untuk menerapkan menggunakan perpustakaan body-pix-node .Untuk mempercepat output jaringan (perkiraan) di browser, lebih disukai untuk menggunakan backend WebGL , tetapi di lingkungan Node.js Anda dapat menggunakan backend Tensorflow GPU (perhatikan bahwa ini akan memerlukan kartu video dari NVIDIA , yang saya miliki).Untuk menyederhanakan pengaturan proyek, kami akan menggunakan lingkungan kemas kecil yang menyediakan TensorFlow GPU dan Node.js. Menggunakan semuanya dengan nvidia-docker- Jauh lebih mudah daripada mengumpulkan sendiri dependensi yang diperlukan di komputer Anda. Untuk melakukan ini, Anda hanya perlu Docker dan driver grafis terbaru di komputer Anda.Berikut adalah isi file bodypix/package.json
:{
"name": "bodypix",
"version": "0.0.1",
"dependencies": {
"@tensorflow-models/body-pix": "^2.0.5",
"@tensorflow/tfjs-node-gpu": "^1.7.1"
}
}
Ini file tersebut 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
Sekarang mari kita bicara tentang mendapatkan hasil. Tapi saya segera memperingatkan Anda: Saya bukan ahli Node.js! Ini hanya hasil dari eksperimen malam saya, jadi bersikap lunak kepada saya :-).Skrip sederhana berikut sedang sibuk memproses gambar topeng biner yang dikirim ke server menggunakan permintaan HTTP POST. Topeng adalah array piksel dua dimensi. Piksel yang diwakili oleh nol adalah latar belakangnya.Ini adalah kode file 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);
})();
Untuk mengonversi bingkai menjadi topeng, kami, dalam skrip Python, dapat menggunakan paket numpy dan permintaan :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
Hasilnya kira-kira sebagai berikut.TopengSementara saya melakukan semua ini, saya menemukantweet berikutnya .Ini jelas merupakan latar belakang terbaik untuk panggilan video.Sekarang kita memiliki topeng untuk memisahkan latar depan dari latar belakang, mengganti latar belakang dengan sesuatu yang lain akan sangat sederhana.Saya mengambil gambar latar belakang dari cabang tweet dan memotongnya sehingga saya mendapatkan gambar 16x9.Gambar latar belakangSetelah itu saya melakukan hal berikut:
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
Itulah yang saya dapatkan setelah itu.Hasil dari mengganti latar belakangTopeng ini jelas tidak cukup akurat, alasan untuk ini adalah trade-off kinerja yang kami buat saat menyiapkan BodyPix. Secara umum, sementara semuanya terlihat lebih atau kurang toleran.Tetapi, ketika saya melihat latar belakang ini, satu ide muncul pada saya.Eksperimen yang menarik
Sekarang kami telah menemukan cara untuk menutupi, kami akan bertanya bagaimana cara meningkatkan hasilnya.Langkah pertama yang jelas adalah untuk melunakkan tepi topeng. Sebagai contoh, ini dapat dilakukan seperti ini: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
Ini akan sedikit memperbaiki situasi, tetapi tidak ada banyak kemajuan. Dan penggantian yang sederhana cukup membosankan. Tapi, karena kita sudah menyelesaikan semua ini sendiri, ini berarti kita bisa melakukan apa saja dengan gambar, dan tidak hanya menghilangkan latar belakang.Karena kami menggunakan latar belakang virtual dari Star Wars, saya memutuskan untuk membuat efek hologram untuk membuat gambar lebih menarik. Ini, sebagai tambahan, memungkinkan Anda untuk menghaluskan buram topeng.Pertama, perbarui kode pasca pemrosesan: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
Tepinya sekarang buram. Ini bagus, tetapi kita masih perlu membuat efek hologram.Hologram Hollywood biasanya memiliki sifat-sifat berikut:- Warna pucat atau gambar monokrom - seolah-olah ditarik oleh laser yang terang.
- Efek yang mengingatkan pada garis pindai atau sesuatu seperti kisi - seolah-olah gambar ditampilkan dalam beberapa sinar.
- "Efek hantu" - seolah-olah proyeksi dilakukan dalam lapisan atau seolah-olah jarak yang benar di mana ia harus ditampilkan tidak dipertahankan selama pembuatan proyeksi.
Semua efek ini dapat diimplementasikan langkah demi langkah.Pertama, untuk mewarnai gambar dengan warna biru, kita dapat menggunakan metode ini applyColorMap
:
holo = cv2.applyColorMap(frame, cv2.COLORMAP_WINTER)
Berikutnya - tambahkan garis sapuan dengan efek mengingatkan meninggalkan halftone:
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)
Selanjutnya, kami menerapkan "efek hantu" dengan menambahkan salinan tertimbang dari efek saat ini ke gambar:
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)
Dan akhirnya, kami ingin mempertahankan beberapa warna asli, jadi kami menggabungkan efek holografik dengan bingkai asli, melakukan sesuatu yang mirip dengan menambahkan "efek hantu":holo_done = cv2.addWeighted(img, 0.5, holo2, 0.6, 0)
Seperti inilah bingkai dengan efek hologram:Bingkai dengan efek hologramBingkai ini sendiri terlihat cukup bagus.Sekarang mari kita coba menggabungkannya dengan latar belakang.Gambar overlay pada latar belakang.Selesai! (Saya berjanji - video jenis ini akan terlihat lebih menarik).Output video
Dan sekarang saya harus mengatakan bahwa kami telah melewatkan sesuatu di sini. Faktanya adalah bahwa kita masih tidak dapat menggunakan semua ini untuk melakukan panggilan video.Untuk memperbaiki ini, kami akan menggunakan pyfakewebcam dan v4l2loopback untuk membuat webcam dummy.Selain itu, kami berencana untuk memasang kamera ini ke Docker.Pertama, buat file fakecam/requirements.txt
deskripsi ketergantungan:numpy==1.18.2
opencv-python==4.2.0.32
requests==2.23.0
pyfakewebcam==0.1.0
Sekarang buat file fakecam/Dockerfile
untuk aplikasi yang mengimplementasikan kemampuan kamera tiruan: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
Sekarang, dari baris perintah, instal v4l2loopback:sudo apt install v4l2loopback-dkms
Siapkan kamera tiruan:sudo modprobe -r v4l2loopback
sudo modprobe v4l2loopback devices=1 video_nr=20 card_label="v4l2loopback" exclusive_caps=1
Untuk memastikan fungsionalitas beberapa aplikasi (Chrome, Zoom), kami memerlukan pengaturan exclusive_caps
. Tanda card_label
ini ditetapkan hanya untuk memastikan kenyamanan memilih kamera dalam aplikasi. Indikasi nomor video_nr=20
mengarah ke pembuatan perangkat /dev/video20
jika nomor yang sesuai tidak sibuk, dan tidak mungkin sibuk.Sekarang kita akan membuat perubahan pada skrip untuk membuat kamera tiruan:
fake = pyfakewebcam.FakeWebcam('/dev/video20', width, height)
Perlu dicatat bahwa pyfakewebcam mengharapkan gambar dengan saluran RGB (Merah, Hijau, Biru - merah, hijau, biru), dan Open CV berfungsi dengan urutan saluran BGR (Biru, Hijau, Merah).Anda dapat memperbaiki ini sebelum mengeluarkan bingkai, dan kemudian mengirim bingkai seperti ini:frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
fake.schedule_frame(frame)
Ini adalah kode skrip lengkap 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)
Sekarang kumpulkan gambar:docker build -t bodypix ./bodypix
docker build -t fakecam ./fakecam
Jalankan mereka:
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
Tetap hanya untuk mempertimbangkan bahwa ini harus dimulai sebelum kamera dibuka ketika bekerja dengan aplikasi apa pun. Dan di Zoom atau di tempat lain Anda harus memilih kamera v4l2loopback
/ /dev/video20
.Ringkasan
Berikut ini adalah klip yang menunjukkan hasil pekerjaan saya. Hasil perubahan latar belakangLihat! Saya menelepon dari Millennium Falcon menggunakan tumpukan teknologi sumber terbuka untuk bekerja dengan kamera!Apa yang saya lakukan, saya sangat suka. Dan saya pasti akan memanfaatkan semua ini di konferensi video berikutnya.Pembaca yang budiman! Apakah Anda berencana untuk mengubah apa yang terlihat selama panggilan video di belakang Anda untuk sesuatu yang lain?