Web 端實時防擋臉彈幕(基于機器學習)
防擋臉彈幕,即大量彈幕飄過,但不會遮擋視頻畫面中的人物,看起來像是從人物背后飄過去的。
機器學習已經(jīng)火了好幾年了,但很多人都不知道瀏覽器中也能運行這些能力;
本文介紹在視頻彈幕方面的實踐優(yōu)化過程,文末列舉了一些本方案可適用的場景,期望能開啟一些腦洞。
mediapipe Demo(https://google.github.io/mediapipe/)展示
主流防擋臉彈幕實現(xiàn)原理
點播
up 上傳視頻
服務器后臺計算提取視頻畫面中的人像區(qū)域,轉(zhuǎn)換成 svg 存儲
客戶端播放視頻的同時,從服務器下載 svg 與彈幕合成,人像區(qū)域不顯示彈幕
直播
- 主播推流時,實時(主播設備)從畫面提取人像區(qū)域,轉(zhuǎn)換成 svg
- 將 svg 數(shù)據(jù)合并到視頻流中(SEI),推流至服務器
- 客戶端播放視頻同時,從視頻流中(SEI)解析出 svg
- 將 svg 與彈幕合成,人像區(qū)域不顯示彈幕
本文實現(xiàn)方案
客戶端播放視頻同時,實時從畫面提取人像區(qū)域信息,將人像區(qū)域信息導出成圖片與彈幕合成,人像區(qū)域不顯示彈幕。
實現(xiàn)原理
- 采用機器學習開源庫從視頻畫面實時提取人像輪廓,如Body Segmentation(https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/README.md)
- 將人像輪廓轉(zhuǎn)導出為圖片,設置彈幕層的 mask-image(https://developer.mozilla.org/zh-CN/docs/Web/CSS/mask-image)
對比傳統(tǒng)(直播SEI實時)方案
優(yōu)點:
- 易于實現(xiàn);只需要Video標簽一個參數(shù),無需多端協(xié)同配合
- 無網(wǎng)絡帶寬消耗
缺點:
- 理論性能極限劣于傳統(tǒng)方案;相當于性能資源換網(wǎng)絡資源
面臨的問題
眾所周知“JS 性能太辣雞”,不適合執(zhí)行 CPU 密集型任務。由官方demo變成工程實踐,最大的挑戰(zhàn)就是——性能。
本次實踐最終將 CPU 占用優(yōu)化到 5% 左右(2020 M1 Macbook),達到生產(chǎn)可用狀態(tài)。
實踐調(diào)優(yōu)過程
選擇機器學習模型
BodyPix (https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/src/body_pix/README.md)
精確度太差,面部偏窄,有很明顯的彈幕與人物面部邊緣重疊現(xiàn)象
BlazePose(https://github.com/tensorflow/tfjs-models/blob/master/pose-detection/src/blazepose_mediapipe/README.md)
精確度優(yōu)秀,且提供了肢體點位信息,但性能較差
返回數(shù)據(jù)結(jié)構(gòu)示例
[
{
score: 0.8,
keypoints: [
{x: 230, y: 220, score: 0.9, score: 0.99, name: "nose"},
{x: 212, y: 190, score: 0.8, score: 0.91, name: "left_eye"},
...
],
keypoints3D: [
{x: 0.65, y: 0.11, z: 0.05, score: 0.99, name: "nose"},
...
],
segmentation: {
maskValueToLabel: (maskValue: number) => { return 'person' },
mask: {
toCanvasImageSource(): ...
toImageData(): ...
toTensor(): ...
getUnderlyingType(): ...
}
}
}
]
MediaPipe SelfieSegmentation (https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/src/selfie_segmentation_mediapipe/README.md)
精確度優(yōu)秀(跟 BlazePose 模型效果一致),CPU 占用相對 BlazePose 模型降低 15% 左右,性能取勝,但返回數(shù)據(jù)中不提供肢體點位信息
返回數(shù)據(jù)結(jié)構(gòu)示例
{
maskValueToLabel: (maskValue: number) => { return 'person' },
mask: {
toCanvasImageSource(): ...
toImageData(): ...
toTensor(): ...
getUnderlyingType(): ...
}
}
初版實現(xiàn)
參考 MediaPipe SelfieSegmentation 模型 官方實現(xiàn)(https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/README.md#bodysegmentationdrawmask),未做優(yōu)化的情況下 CPU 占用 70% 左右
const canvas = document.createElement('canvas')
canvas.width = videoEl.videoWidth
canvas.height = videoEl.videoHeight
async function detect (): Promise<void> {
const segmentation = await segmenter.segmentPeople(videoEl)
const foregroundColor = { r: 0, g: 0, b: 0, a: 0 }
const backgroundColor = { r: 0, g: 0, b: 0, a: 255 }
const mask = await toBinaryMask(segmentation, foregroundColor, backgroundColor)
await drawMask(canvas, canvas, mask, 1, 9)
// 導出Mask圖片,需要的是輪廓,圖片質(zhì)量設為最低
handler(canvas.toDataURL('image/png', 0))
window.setTimeout(detect, 33)
}
detect().catch(console.error)
降低提取頻率,平衡 性能-體驗
一般視頻 30FPS,嘗試彈幕遮罩(后稱 Mask)刷新頻率降為 15FPS,體驗上還能接受
window.setTimeout(detect, 66) // 33 => 66
此時,CPU 占用 50% 左右
解決性能瓶頸
分析火焰圖可發(fā)現(xiàn),性能瓶頸在 toBinaryMask 和 toDataURL
重寫toBinaryMask
分析源碼,結(jié)合打印segmentation的信息,發(fā)現(xiàn)segmentation.mask.toCanvasImageSource可獲取原始ImageBitmap對象,即是模型提取出來的信息。嘗試自行實現(xiàn)將ImageBitmap轉(zhuǎn)換成 Mask 的能力,替換開源庫提供的默認實現(xiàn)。
實現(xiàn)原理
async function detect (): Promise<void> {
const segmentation = await segmenter.segmentPeople(videoEl)
context.clearRect(0, 0, canvas.width, canvas.height)
// 1. 將`ImageBitmap`繪制到 Canvas 上
context.drawImage(
// 經(jīng)驗證 即使出現(xiàn)多人,也只有一個 segmentation
await segmentation[0].mask.toCanvasImageSource(),
0, 0,
canvas.width, canvas.height
)
// 2. 設置混合模式
context.globalCompositeOperation = 'source-out'
// 3. 反向填充黑色
context.fillRect(0, 0, canvas.width, canvas.height)
// 導出Mask圖片,需要的是輪廓,圖片質(zhì)量設為最低
handler(canvas.toDataURL('image/png', 0))
window.setTimeout(detect, 66)
}
第 2、3 步相當于給人像區(qū)域外的內(nèi)容填充黑色(反向填充ImageBitmap),是為了配合css(mask-image), 不然只有當彈幕飄到人像區(qū)域才可見(與目標效果正好相反)。
globalCompositeOperation MDN(https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation)
此時,CPU 占用 33% 左右
多線程優(yōu)化
只剩下toDataURL這個耗時操作了,本以為toDataURL是瀏覽器內(nèi)部實現(xiàn),無法再進行優(yōu)化了。
雖沒有替換實現(xiàn),但可使用 OffscreenCanvas (https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas)+ Worker,將耗時任務轉(zhuǎn)移到 Worker 中去, 避免占用主線程,就不會影響用戶體驗了。
并且ImageBitmap實現(xiàn)了Transferable接口,可被轉(zhuǎn)移所有權(quán),跨 Worker 傳遞也沒有性能損耗(https://hughfenghen.github.io/fe-basic-course/js-concurrent.html#%E4%B8%A4%E4%B8%AA%E6%96%B9%E6%B3%95%E5%AF%B9%E6%AF%94)。
// 前文 detect 的反向填充 ImageBitmap 也可以轉(zhuǎn)移到 Worker 中
// 用 OffscreenCanvas 實現(xiàn), 此處略過
const reader = new FileReaderSync()
// OffscreenCanvas 不支持 toDataURL,使用 convertToBlob 代替
offsecreenCvsEl.convertToBlob({
type: 'image/png',
quality: 0
}).then((blob) => {
const dataURL = reader.readAsDataURL(blob)
self.postMessage({
msgType: 'mask',
val: dataURL
})
}).catch(console.error)
可以看到兩個耗時的操作消失了
此時,CPU 占用 15% 左右
降低分辨率
繼續(xù)分析,上圖重新計算樣式(紫色部分)耗時約 3ms
Demo 足夠簡單很容易推測到是這行代碼導致的,發(fā)現(xiàn) imgStr 大概 100kb 左右(視頻分辨率 1280x720)。
danmakuContainer.style.webkitMaskImage = `url(${imgStr})
通過canvas縮小圖片尺寸(360P甚至更低),再進行推理。
優(yōu)化后,導出的 imgStr 大概 12kb,重新計算樣式耗時約 0.5ms。
此時,CPU 占用 5% 左右
啟動條件優(yōu)化
雖然提取 Mask 整個過程的 CPU 占用已優(yōu)化到可喜程度。
當在畫面沒人的時候,或沒有彈幕時候,可以停止計算,實現(xiàn) 0 CPU 占用。
無彈幕判斷比較簡單(比如 10s 內(nèi)收超過兩條彈幕則啟動計算),也不在該 SDK 實現(xiàn)范圍,略過
判定畫面是否有人
第一步中為了高性能,選擇的模型只有ImageBitmap,并沒有提供肢體點位信息,所以只能使用getImageData返回的像素點值來判斷畫面是否有人。
畫面無人時,CPU 占用接近 0%
發(fā)布構(gòu)建優(yōu)化
依賴包的提交較大,構(gòu)建出的 bundle 體積:684.75 KiB / gzip: 125.83 KiB
所以,可以進行異步加載SDK,提升頁面加載性能。
- 分別打包一個 loader,一個主體
- 由業(yè)務方 import loader,首次啟用時異步加載主體
這個兩步前端工程已經(jīng)非常成熟了,略過細節(jié)。
運行效果
總結(jié)
過程
- 選擇高性能模型后,初始狀態(tài) CPU 70%
- 降低 Mask 刷新頻率(15FPS),CPU 50%
- 重寫開源庫實現(xiàn)(toBinaryMask),CPU 33%
- 多線程優(yōu)化,CPU 15%
- 降低分辨率,CPU 5%
- 判斷畫面是否有人,無人時 CPU 接近 0%
CPU 數(shù)值指主線程占用
注意事項
- 兼容性:Chrome 79及以上,不支持 Firefox、Safari。因為使用了OffscreenCanvas
- 不應創(chuàng)建多個或多次創(chuàng)建segmenter實例(bodySegmentation.createSegmenter),如需復用請保存實例引用,因為:
- 創(chuàng)建實例時低性能設備會有明顯的卡頓現(xiàn)象
- 會內(nèi)存泄露;如果無法避免,這是mediapipe 內(nèi)存泄露 解決方法(https://github.com/google/mediapipe/issues/2819#issuecomment-1160335349)
經(jīng)驗
- 優(yōu)化完成之后,提取并應用 Mask 關(guān)鍵計算量在 GPU (30%左右),而不是 CPU
- 性能優(yōu)化需要業(yè)務場景分析,防擋彈幕場景可以使用低分辨率、低刷新率的 mask-image,能大幅減少計算量
- 該方案其他應用場景:
- 替換/模糊人物背景
- 人像馬賽克
- 人像摳圖
- 卡通頭套,虛擬飾品,如貓耳朵、兔耳朵、帶花、戴眼鏡什么的(換一個模型,略改)
- 關(guān)注Web 神經(jīng)網(wǎng)絡 API (https://mp.weixin.qq.com/s/v7-xwYJqOfFDIAvwIVZVdg)進展,以后實現(xiàn)相關(guān)功能也許會更簡單
本期作者
劉俊
嗶哩嗶哩資深開發(fā)工程師