防挡脸弹幕,即大量弹幕飘过,但不会遮挡视频画面中的人物,看起来像是从人物背后飘过去的。
机器学习已经火了好几年了,但很多人都不知道浏览器中也能运行这些能力;
本文介绍在视频弹幕方面的实践优化过程,文末列举了一些本方案可适用的场景,期望能开启一些脑洞。
mediapipe demo(https://google.github.io/mediapipe/)展示
主流防挡脸弹幕实现原理点播up 上传视频
服务器后台计算提取视频画面中的人像区域,转换成 svg 存储
客户端播放视频的同时,从服务器下载 svg 与弹幕合成,人像区域不显示弹幕
直播主播推流时,实时(主播设备)从画面提取人像区域,转换成 svg将 svg 数据合并到视频流中(sei),推流至服务器客户端播放视频同时,从视频流中(sei)解析出 svg将 svg 与弹幕合成,人像区域不显示弹幕本文实现方案客户端播放视频同时,实时从画面提取人像区域信息,将人像区域信息导出成图片与弹幕合成,人像区域不显示弹幕。
实现原理采用机器学习开源库从视频画面实时提取人像轮廓,如body segmentation(https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/readme.md)将人像轮廓转导出为图片,设置弹幕层的 mask-image(https://developer.mozilla.org/zh-cn/docs/web/css/mask-image) 对比传统(直播sei实时)方案
优点:
易于实现;只需要video标签一个参数,无需多端协同配合无网络带宽消耗缺点:
理论性能极限劣于传统方案;相当于性能资源换网络资源面临的问题众所周知,javascript 的性能较差,因此不适合用于处理 cpu 密集型任务。由官方demo变成工程实践,最大的挑战就是——性能。
本次实践最终将 cpu 占用优化到 5% 左右(2020 m1 macbook),达到生产可用状态。
实践调优过程选择机器学习模型bodypix (https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/src/body_pix/readme.md)
精确度太差,面部偏窄,有很明显的弹幕与人物面部边缘重叠现象
blazepose(https://github.com/tensorflow/tfjs-models/blob/master/pose-detection/src/blazepose_mediapipe/readme.md)
精确度优秀,且提供了肢体点位信息,但性能较差
返回数据结构示例
[{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)
精确度优秀(跟 blazepose 模型效果一致),cpu 占用相对 blazepose 模型降低 15% 左右,性能取胜,但返回数据中不提供肢体点位信息
返回数据结构示例
{maskvaluetolabel: (maskvalue: number) => { return 'person' },mask: {tocanvasimagesource(): ...toimagedata(): ...totensor(): ...getunderlyingtype(): ...}}
初版实现参考 mediapipe selfiesegmentation 模型 官方实现(https://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/readme.md#bodysegmentationdrawmask),未做优化的情况下 cpu 占用 70% 左右
const canvas = document.createelement('canvas')canvas.width = videoel.videowidthcanvas.height = videoel.videoheightasync 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图片,需要的是轮廓,图片质量设为最低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% 左右
解决性能瓶颈
分析火焰图可发现,性能瓶颈在 tobinarymask 和 todataurl
重写tobinarymask分析源码,结合打印segmentation的信息,发现segmentation.mask.tocanvasimagesource可获取原始imagebitmap对象,即是模型提取出来的信息。尝试自己编写代码,将imagebitmap转换为遮罩(mask),以代替使用开源库所提供的默认实现。
实现原理async function detect (): promise<void> {const segmentation = await segmenter.segmentpeople(videoel) context.clearrect(0, 0, canvas.width, canvas.height)// 1. 将`imagebitmap`绘制到 canvas 上context.drawimage(// 经验证 即使出现多人,也只有一个 segmentationawait 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图片,需要的是轮廓,图片质量设为最低handler(canvas.todataurl('image/png', 0)) window.settimeout(detect, 66)}
第 2、3 步相当于给人像区域外的内容填充黑色(反向填充imagebitmap),是为了配合css(mask-image), 不然只有当弹幕飘到人像区域才可见(与目标效果正好相反)。
globalcompositeoperation mdn(https://developer.mozilla.org/zh-cn/docs/web/api/canvasrenderingcontext2d/globalcompositeoperation)
此时,cpu 占用 33% 左右
多线程优化我原先认为todataurl是由浏览器内部实现的,无法再进行优化,现在只有优化todataurl这个耗时操作了。
虽没有替换实现,但可使用 offscreencanvas (https://developer.mozilla.org/zh-cn/docs/web/api/offscreencanvas)+ worker,将耗时任务转移到 worker 中去, 避免占用主线程,就不会影响用户体验了。
并且imagebitmap实现了transferable接口,可被转移所有权,跨 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 也可以转移到 worker 中// 用 offscreencanvas 实现, 此处略过 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% 左右
降低分辨率继续分析,上图重新计算样式(紫色部分)耗时约 3ms
demo 足够简单很容易推测到是这行代码导致的,发现 imgstr 大概 100kb 左右(视频分辨率 1280x720)。
danmakucontainer.style.webkitmaskimage = `url(${imgstr})
通过canvas缩小图片尺寸(360p甚至更低),再进行推理。
优化后,导出的 imgstr 大概 12kb,重新计算样式耗时约 0.5ms。
此时,cpu 占用 5% 左右
启动条件优化虽然提取 mask 整个过程的 cpu 占用已优化到可喜程度。
当在画面没人的时候,或没有弹幕时候,可以停止计算,实现 0 cpu 占用。
无弹幕判断比较简单(比如 10s 内收超过两条弹幕则启动计算),也不在该 sdk 实现范围,略过
判定画面是否有人第一步中为了高性能,选择的模型只有imagebitmap,并没有提供肢体点位信息,所以只能使用getimagedata返回的像素点值来判断画面是否有人。
画面无人时,cpu 占用接近 0%
发布构建优化依赖包的提交较大,构建出的 bundle 体积:684.75 kib / gzip: 125.83 kib
所以,可以进行异步加载sdk,提升页面加载性能。
分别打包一个 loader,一个主体由业务方 import loader,首次启用时异步加载主体这个两步前端工程已经非常成熟了,略过细节。
运行效果
总结过程选择高性能模型后,初始状态 cpu 70%降低 mask 刷新频率(15fps),cpu 50%重写开源库实现(tobinarymask),cpu 33%多线程优化,cpu 15%降低分辨率,cpu 5%判断画面是否有人,无人时 cpu 接近 0%cpu 数值指主线程占用注意事项
兼容性:chrome 79及以上,不支持 firefox、safari。因为使用了offscreencanvas不应创建多个或多次创建segmenter实例(bodysegmentation.createsegmenter),如需复用请保存实例引用,因为:创建实例时低性能设备会有明显的卡顿现象会内存泄露;如果无法避免,这是mediapipe 内存泄露 解决方法(https://github.com/google/mediapipe/issues/2819#issuecomment-1160335349)经验优化完成之后,提取并应用 mask 关键计算量在 gpu (30%左右),而不是 cpu性能优化需要业务场景分析,防挡弹幕场景可以使用低分辨率、低刷新率的 mask-image,能大幅减少计算量该方案其他应用场景:替换/模糊人物背景人像马赛克人像抠图卡通头套,虚拟饰品,如猫耳朵、兔耳朵、带花、戴眼镜什么的(换一个模型,略改)关注web 神经网络 api (https://mp.weixin.qq.com/s/v7-xwyjqoffdiavwivzvdg)进展,以后实现相关功能也许会更简单本期作者
刘俊
哔哩哔哩资深开发工程师
以上就是web 端实时防挡脸弹幕(基于机器学习)的详细内容。