AI智能弹幕(也称蒙版弹幕):弹幕浮在视频的上方却永远不会挡住人物。起源于哔哩哔哩的web端黑科技,而后分别实现在IOS和Android的app端,如今被用于短视频、直播等媒体行业,用户体验提升显著。
本文除了会使用 Flutter
新方案进行跨端实现,同时也会讲解如何将一段任意视频流使用 opencv-python
处理成蒙版数据源,达成从0到1的前后端AI体系。先来看看双端最终运行效果吧:
APP运行截图:
实现流程目录
- Python后端:
- 依次提取视频流的 关键帧 保存为图片
- 将所有关键帧传给 神经网络模型 让算法将图片中非人物抹去,并保存图片帧
- 将只含有人物的图片帧进行 像素色值转换 ,得到 灰度图 ,最后再转为 黑白反色图
- 通过识别黑白反色图的 轮廓坐标 ,生成一份 时间:路径 配置文件提供给前端
- Flutter前端:
- 实现一个弹幕调度动画组
- 根据 配置文件 将弹幕外层容器 裁剪 为一个刚好透出人物的漏洞形状,也称蒙版
- 引入播放器,视频流播放时,为 关键帧 同步渲染其对应的蒙版形状
- 拓展:
- Web前端实现
- 视频点播与直播
- 总结与优化
1. Python后端
1.1 提取关键帧
# config.py --- 配置文件 import os import cv2 VIDEO_NAME = 'source.mp4' # 处理的视频文件名 FACE_KEY = '*****' # AI识别key FACE_SECRET = '*****' # AI密钥 dirPath = os.path.dirname(os.path.abspath(__file__)) cap = cv2.VideoCapture(os.path.join(dirPath, VIDEO_NAME)) FPS = round(cap.get(cv2.CAP_PROP_FPS), 0) # 进行识别的关键帧,FPS每上升30,关键帧间隔+1(保证flutter在重绘蒙版时的性能的一致性) FRAME_CD = max(1, round(FPS / 30)) if cv2.CAP_PROP_FRAME_COUNT / FRAME_CD >= 900: raise Warning('经计算你的视频关键帧已经超过了900,建议减少视频时长或FPS帧率!') 复制代码
在这份配置文件中,会先读取视频的帧率, 30FPS
的视频会吧每一帧都当做关键帧进行处理, 60FPS
则会隔一帧处理一次,这样是为了保证Flutter在绘制蒙版的性能统一。
另外需要注意的是由于演示DEMO为完全离线环境,视频和最终蒙版文件都会被打包到APP,视频文件不宜过大。
# frame.py --- 视频帧提取 import os import shutil import cv2 import config dirPath = os.path.dirname(os.path.abspath(__file__)) images_path = dirPath + '/images' cap = cv2.VideoCapture(os.path.join(dirPath, config.VIDEO_NAME)) count = 1 if os.path.exists(images_path): shutil.rmtree(images_path) os.makedirs(images_path) # 循环读取视频的每一帧 while True: ret, frame = cap.read() if ret: if(count % config.FRAME_CD == 0): print('the number of frames:' + str(count)) # 保存截取帧到本地 cv2.imwrite(images_path + '/frame' + str(count) + '.jpg', frame) count += 1 cv2.waitKey(0) else: print('frames were created successfully') break cap.release() 复制代码
这里使用 opencv
提取视频的关键帧图片并保存在当前目录 images
文件夹下。
1.2 通过AI模型提取人物
提取图像中人物的工作需要交给 卷积神经网络 来完成,不同程度的训练对图像分类的准确率影响很大,而这也直接决定了最终的效果。大公司有算法团队来专门训练模型,我们的DEMO使用FACE++提供的开放测试接口,准确率与其付费商用的无异,就是会被限流,失败率高达80%,不过后面我们可以在代码编写中解决这个问题。
# discern.py --- 调用算法接口返回人体模型灰度图 import os import shutil import base64 import re import json import threading import requests import config dirPath = os.path.dirname(os.path.abspath(__file__)) clip_path = dirPath + '/clip' if not os.path.exists(clip_path): os.makedirs(clip_path) # 图像识别类 class multiple_req: reqTimes = 0 filename = None data = { 'api_key': config.FACE_KEY, 'api_secret': config.FACE_SECRET, 'return_grayscale': 1 } def __init__(self, filename): self.filename = filename def once_again(self): # 成功率大约10%,记录一