Panggilan video dengan latar belakang virtual dan alat sumber terbuka

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:

#    720p @ 60 FPS
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 saya

Deteksi 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:

#  ,   TensorFlow GPU
FROM nvcr.io/nvidia/cuda:10.0-cudnn7-runtime-ubuntu18.04
#  node
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
# ,     
#   tfjs-node-gpu      GPU :(
ENV TF_FORCE_GPU_ALLOW_GROWTH=true
#  node-
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'})
    #     numpy-
    #     uint8[width * height]   0  1
    mask = np.frombuffer(r.content, dtype=np.uint8)
    mask = mask.reshape((frame.shape[0], frame.shape[1]))
    return mask

Hasilnya kira-kira sebagai berikut.


Topeng

Sementara 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 belakang

Setelah itu saya melakukan hal berikut:

#    (     16:9)
replacement_bg_raw = cv2.imread("background.jpg")

#    ,       (width & height   )
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 belakang

Topeng 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    10-30%,
#    bandGap.
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:

# shift_img : https://stackoverflow.com/a/53140617
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

#    : holo * 0.2 + shifted_holo * 0.8 + 0
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 hologram

Bingkai 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.txtdeskripsi ketergantungan:

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

Sekarang buat file fakecam/Dockerfileuntuk aplikasi yang mengimplementasikan kemampuan kamera tiruan:

FROM python:3-buster
#   pip
RUN pip install --upgrade pip
#   opencv
RUN apt-get update && \
    apt-get install -y \
      `# opencv requirements` \
      libsm6 libxext6 libxrender-dev \
      `# opencv video opening requirements` \
      libv4l-dev
#    requirements.txt
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_labelini ditetapkan hanya untuk memastikan kenyamanan memilih kamera dalam aplikasi. Indikasi nomor video_nr=20mengarah ke pembuatan perangkat /dev/video20jika nomor yang sesuai tidak sibuk, dan tidak mungkin sibuk.

Sekarang kita akan membuat perubahan pada skrip untuk membuat kamera tiruan:

# ,  ,   ,   ,  width  height
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)
    #    RGB-
    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
#   bodypix
docker run -d \
  --name=bodypix \
  --network=fakecam \
  --gpus=all --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \
  bodypix
#  ,  ,      ,  ,
#           
# ,     `sudo groupadd $USER video`
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 belakang

Lihat! 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?


All Articles