在瀏覽器實作虛擬會議室
18 May 2021這一年因為武漢肺炎的關係,使用網路會議室開會的頻率變得越來越高,而現在幾個主流的服務都有提供虛擬會議室的功能,簡單來說就是無需綠幕的去背,藉此避免在家工作時背景亂糟糟的。
最近公司要我嘗試實作這功能,以下簡單紀錄一下做法與結果。
使用到的技術
可以理解到,此需求最核心的問題是如何從每一禎的視訊畫面中找出人在哪裡。
一開始去找了一些影像處理的涵式庫,例如 OpenCV 之類的,後來發現我找的方向錯了,真正需要的東西是 TensorFlow.js
TensorFlow 是個開源的機器學習工具,TensorFlow.js 就是他的 JavaScript 版,可以跑在 Node.js 或者直接跑在瀏覽器上。最棒的是它有提供幾個已經預先訓練好的模型可以使用,其中就包含辨識人體的機器學習模型 BodyPix
於是大概的思維如下:
- 使用瀏覽器的 API 擷取視訊畫面
- 把畫面丟給 TensorFlow 分離出人與背景的像素
- 在 html canvas 把背景與切割好的人物像素合成
- 把 canvas 當成視訊串流進行輸出
- 把串流接上 WebRTC
1. 取得視訊畫面
引入涵式庫
<head>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.1"></script>
</head>
準備需要的元素
<body>
<video id="video" src="" hidden></video>
<canvas id="canvas"></canvas>
</body>
然後撰寫 js
const videoElement = document.getElementById("video");
const canvas = document.getElementById("canvas");
let width = 640;
let height = 480;
(async () => {
let stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
videoElement.onplaying = () => {
let settings = stream.getVideoTracks()[0].getSettings();
width = settings.width;
height = settings.height;
canvas.width = width;
canvas.height = height;
init();
};
videoElement.srcObject = stream;
videoElement.play();
})();
大致上就是取得視訊畫面,然後丟到 html video 上面播放,撥放後執行 init()
開始後續工作。
記得要存好拿到影片的長寬資訊,繪圖的時候會需要。
2. 執行 BodyPix
async function init() {
const modelOption = {
architecture: "MobileNetV1",
outputStride: 16,
multiplier: 0.75,
quantBytes: 2,
};
let net = await bodyPix.load(modelOption);
let image = await loadImage("my-background.jpg");
setInterval(() => draw(net, image), 100); //ms
}
- 初始化模型,這邊設定的方式請參考官方說明文件,選擇的結果會影響判斷的準確度與需要的硬體性能。
- 從檔案讀取需要使用的背景照片。找個漂亮的風景圖或會議室照片之類的,也有公司有提供免費的圖給大家玩,如果你的老闆不介意看到你在鬼滅之刃或神隱少女的場景開會的話。
- 定時重複執行繪圖
3. 合成畫面
const foregroundColor = { r: 0, g: 0, b: 0, a: 255 };
const backgroundColor = { r: 0, g: 0, b: 0, a: 0 };
const segmentOption = {
flipHorizontal: true,
internalResolution: "medium",
segmentationThreshold: 0.5,
};
async function draw(net, backgroundImage) {
let segmentation = await net.segmentPerson(videoElement, segmentOption);
let maskData = bodyPix.toMask(segmentation, foregroundColor, backgroundColor);
let ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = "destination-over";
ctx.putImageData(maskData, 0, 0);
ctx.globalCompositeOperation = "source-in";
ctx.drawImage(videoElement, 0, 0, width, height);
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(backgroundImage, 0, 0, width, height);
}
- 設定前景、背景色、與辨識參數(參數一樣參考官方文件設定)
- 執行主程式
bodyPix.toMask()
取得遮罩 - 將遮罩與視訊畫面、背景畫面疊圖
理論上至此已經可以在瀏覽器上測試,並於螢幕上看到運算結果。
4.取得串流
最後執行 canvas.captureStream()
即可將 canvas 轉換成 MediaStream ,然後就去串接自己的 WebRTC 服務吧。
成果如何?
不好意思露臉就沒放測試的圖了。
辨識的結果我覺得還算準確,只要你不是穿跟背景顏色一樣的衣服,基本上可以精確抓出頭、手、身體。
但問題是滿吃效能的,測試過程中我的瀏覽器當掉過好幾次,沒當機也是記憶體滿載,不確定是瀏覽器性能的原罪,還是我程式撰寫還需要改善。
總之目前的成果我覺得不到可以正式上線的程度,勉強堪用而已。
有點好奇那使用 Node.js 來跑會不會比較好?之後有空再來嘗試吧。
- 封面圖片來源:elitedaily