百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Web 端实时防挡脸弹幕(基于机器学习)

myzbx 2025-04-08 16:46 14 浏览

刘俊 哔哩哔哩技术 2023-06-09 12:00 发表于上海


本期作者



刘俊

哔哩哔哩资深开发工程师


前言


防挡脸弹幕,即大量弹幕飘过,但不会遮挡视频画面中的人物,看起来像是从人物背后飘过去的。



机器学习已经火了好几年了,但很多人都不知道浏览器中也能运行这些能力;

本文介绍在视频弹幕方面的实践优化过程,文末列举了一些本方案可适用的场景,期望能开启一些脑洞。

mediapipe Demo(
https://google.github.io/mediapipe/
)展示


,时长00:05


主流防挡脸弹幕实现原理


点播


  1. up 上传视频
  2. 服务器后台计算提取视频画面中的人像区域,转换成 svg 存储
  3. 客户端播放视频的同时,从服务器下载 svg 与弹幕合成,人像区域不显示弹幕


直播


  1. 主播推流时,实时(主播设备)从画面提取人像区域,转换成 svg
  2. 将 svg 数据合并到视频流中(SEI),推流至服务器
  3. 客户端播放视频同时,从视频流中(SEI)解析出 svg
  4. 将 svg 与弹幕合成,人像区域不显示弹幕


本文实现方案


客户端播放视频同时,实时从画面提取人像区域信息,将人像区域信息导出成图片与弹幕合成,人像区域不显示弹幕。


实现原理


  1. 采用机器学习开源库从视频画面实时提取人像轮廓,如Body Segmentationhttps://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/README.md
  2. 将人像轮廓转导出为图片,设置弹幕层的 mask-imagehttps://developer.mozilla.org/zh-CN/docs/Web/CSS/mask-image


对比传统(直播SEI实时)方案


优点:

  • 易于实现;只需要Video标签一个参数,无需多端协同配合
  • 无网络带宽消耗

缺点:

  • 理论性能极限劣于传统方案;相当于性能资源换网络资源


面临的问题


众所周知“JS 性能太辣鸡”,不适合执行 CPU 密集型任务。
由官方demo变成工程实践,最大的挑战就是——性能。

本次实践最终将 CPU 占用优化到 5% 左右(2020 M1 Macbook),达到生产可用状态。


实践调优过程


选择机器学习模型


BodyPixhttps://github.com/tensorflow/tfjs-models/blob/master/body-segmentation/src/body_pix/README.md

精确度太差,面部偏窄,有很明显的弹幕与人物面部边缘重叠现象



BlazePosehttps://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 SelfieSegmentationhttps://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.videoWidth
canvas.height = videoEl.videoHeight
async function detect (): Promise {
  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 {
  const segmentation = await segmenter.segmentPeople(videoEl)
 
  context.clearRect(0, 0, canvas.width, canvas.height)
  // 1. 将`ImageBitmap`绘制到 Canvas 上
  context.drawImage(
    // 经验证 即使出现多人,也只有一个 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图片,需要的是轮廓,图片质量设为最低
  handler(canvas.toDataURL('image/png', 0))
 
  window.setTimeout(detect, 66)
}


第 2、3 步相当于给人像区域外的内容填充黑色(反向填充ImageBitmap),是为了配合css(mask-image), 不然只有当弹幕飘到人像区域才可见(与目标效果正好相反)。

globalCompositeOperation MDNhttps://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,提升页面加载性能。

  1. 分别打包一个 loader,一个主体
  2. 由业务方 import loader,首次启用时异步加载主体

这个两步前端工程已经非常成熟了,略过细节。


运行效果


,时长00:13


总结


过程


  • 选择高性能模型后,初始状态 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)进展,以后实现相关功能也许会更简单


以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!

相关推荐

怎么恢复7z文件 7z文件删除了怎么恢复

7z是一种压缩格式的文件,它运用LZMA压缩算法,该压缩算法的输出稍后被算数编码进行处理以便后续进一步压缩,压缩比十分高。我们可以将文件压缩成这种格式,便于传输,保存,占空间少。了解更多7z文件知识...

郎酒让消费者喝得明明白白 算术题里有答案

日前,『郎酒酱香产品企业内控准则』颁布,郎酒首次公开酱香产品生产全过程,公布酱香产品产能、储能及投放计划。随后,郎酒官微向消费者发出「品控算术题」有奖问答。郎酒亮出家底,消费者踊跃留言。8天后,谜底揭...

学龄前,比识字、算术更重要的是这三件事

“为了给孩子选择一家合适的幼儿园,我曾穿梭于纽约各家幼儿园的开放日,这些幼儿员既包括主流的公立幼儿园,还包括那些遥不可及的私人幼儿园。我的目的就是想了解他们的教育理念是什么,到底厉害在哪里,看看对于我...

参加CSP-J信奥赛需要掌握数学知识

在C++语法的学习中需要储备的数学知识如下①数据类型:需要知道整数、正整数、负整数、小数、判断对错②算术运算符:加法、减法、乘法、除法、取模运算③关系表达式:大于、大于等于、小于、小...

1g米饭能做多少深蹲?今天我们来算一算

减重我们都知道3分在练,7分在吃,吃这件事情上,真的是每一口都算数。今天我们来算一笔账,1粒米饭可以做多少事情?本着认真负责的态度,今天在食物秤上称了1g米饭,是16粒。根据能量换算:100g米饭是4...

web 自动化测试,一定得掌握的 8 个核心知识点

使用cypress进行端对端测试,和其他的一些框架有一个显著不同的地方,它使用JavaScript作为编程语言。传统主流的selenium框架是支持多语言的,大多数QA会的pytho...

大话C语言:赋值运算符(c语言中赋值运算符是什么)

赋值运算符是最基本的运算符之一,用于将右侧的值或表达式的计算结果赋给左侧的变量。它是一个二元运算符,意味着它需要两个操作数:一个是目标变量(左侧),另一个是要赋给该变量的值或表达式(右侧)。赋值运算符...

Vue进阶(幺幺伍):js 将字符串转换为boolean

Boolean();参数为0、null和无参数返回false,有参数返回true。Boolean("");//输出为:falseBoolean(null);//输出为...

mongodb查询的语法(大于,小于,大于或等于,小于或等于等等)

1).大于,小于,大于或等于,小于或等于$gt:大于$lt:小于$gte:大于或等于$lte:小于或等于例子:db.collection.find({"field":{$gt:valu...

Python学不会来打我(21)python表达式知识点汇总

在Python中,表达式是由变量、运算符、函数调用等组合而成的语句,用于产生值或执行特定操作。以下是对Python中常见表达式的详细讲解:1.1算术表达式涉及数学运算的表达式。例如:a=5b...

C|数据存储地址与字节偏移、数据索引

话说C是面向内存的编程语言。数据要能存得进去,取得出来,且要考虑效率。不管是顺序存储还是链式存储,其寻址方式总是很重要。顺序存储是连续存储。同质结构的数组通过其索引表示位置偏移,异质结构的结构体通过其...

下班后累懵?4 个 JS 手写题帮你搞定前端面试高频考点

打工人下班后最痛苦的事,莫过于拖着疲惫的身子还要啃前端面试题吧?看着那些密密麻麻的JS代码,脑子都快转不动了!别担心,今天咱就用轻松的方式,带你吃透4道高频手写题,让你在面试时自信满满,再也不...

嵌入式数据库sqlite3【进阶篇】-子句和函数的使用,小白一文入门

sqlite在《嵌入式数据库sqlite3命令操作基础篇-增删改查,小白一文入门》一文中讲解了如何实现sqlite3的基本操作增删改查,本文介绍一些其他复杂一点的操作。比如where、orderby...

前缀表达式与后缀表达式(前缀表达式后缀表达式中缀表达式计算)

昨天晚上和儿子一起学习了前缀表达式和后缀表达式。这应该是字符串算式如何被计算机识别并计算的2种方法。本来是想先给他讲一个逆波兰式(后缀表达式),以后再讲前缀表达式。没想到他还挺聪明,很快就把2个都掌握...

Python快速入门教程1:基本语法、数据类型、运算符、数字字符串

Python3的基础教程,涵盖了基本语法、数据类型、类型转换、解释器、注释、运算符、数字和字符串等内容,并附有使用实例场景。Python3的基础教程,涵盖了基本语法、数据类型、类型转换、解释器、注释、...