点击蓝字?关注【测试先锋】,不再迷路!一起成为互联网测试精英,前瞻测试技术~导语全参考清晰度测算的时候,输入两个视频帧序列,但是视频帧序列没有对齐,怎么知道丢了哪帧?又怎么知道补回哪一帧?今天介绍一种直播视频帧的对齐方案,如果您有更好的方法,欢迎在公众号下方留言联系作者探讨交流(文章留言近期开放),再次感谢您的关注和阅读。1/ 问题背景全参考测算视频质量,广泛存在在视频业务场景。开始算法计算时,是需要仔细的对齐这一系列的帧再能做计算的,因为全参考的计算原理是根据两张相同场景的图片数据,做比较测算。仔细想想:“帧对齐”其实这是一个挺麻烦的事儿,为什么这么说?因为:第一、帧非常多(手机端直播业务一般在 18-25fps,pc 直播业务一般 60fps),想让人工挑出到底哪帧丢了,这是非常不现实的。第二、由于人眼的一个重要特性——视觉惰性,表现在人眼会存在一个视觉暂留,肉眼基本看不出来连续两帧的区别。人眼可以保留 0.1-0.4秒的影像。所以每一个视频都是一个非常快的“走马灯”,人们是很难通过图像特征来区别每一帧。所以,“帧对齐”这个问题,拆解为两个问题:如何识别每一个帧+找到未对齐的帧 这两个问题。根据上述分析,我们可以拿到具体的折损测试方案:
- 一个标记了帧号的视频文件- 每一帧都有帧号的图片文件集但在后面的识别图片帧号会遇到一个新的坑:你不知道需要具体需要预测的图片的坐标是多少(因为你在输入预测图片时,需要知道图片的具体 x,y,而不是整张图片输入),也就是说 个位数帧,和十位数帧,和百位数帧,具体的 x,y 都是不一样的,分别是(14,22);(28,22);(42,22) :因为你在预测的时候,不知道具体帧号,所以这个预测位置你不好判断是多长。如果你一刀切使用最长的(42,22),个位数帧的空余位置,因为你的模型/ocr 工具 没有学习过,所以会预测出奇怪的字符串。为了解决这个裁切数字准确的问题,我们有了第二个方案:更好的方案 2:先切后画- step 1:切帧命令 ffmpeg image2 或 opencv 切帧同上文方案 1- step 2:切好的帧画上帧号,具体命令和方案 1 不一样(text 参数)
输入源为两个视频,分别是“原视频”和“待比较视频”(折损视频),首先将两个视频进行拆帧处理;将视频处理为一系列的图片帧文件,然后进行帧对齐处理(将帧的分辨率处理成一致的分辨率,将丢帧补齐,将卡帧删掉并记录帧号),输出两列对齐后的视频帧序列,再合成对齐帧后的视频序列,再进行VMAF,PSNR等全参考得分的分数计算。
了解了上述的测试方案,我们来看下每一个部分都是怎么解决的。
2/ 如何识别每一帧 识别帧的两个思路:物理识别与代码识别。物理识别是指,放大每一帧之间的区别,或者专门打上帧的标签来识别不同的图片。代码识别目前笔者没有找到比较有效的方法来区别两帧,因为在截取为 jpg 时这个信息已经丢失了。所以这里还是倾向于“物理识别”方案。这里根据两位测试前辈(在这特别感谢 eriel,austin)曾经尝试过的经验,有两种方法:方法一:给每一帧标记物理序号至某一个固定位置方法二:给每一帧上方标记一个黑白条形码至某一个固定位置,再来读黑白条形码然后,再通过代码/工具识别这个更容易识别的物理特征。总之,上述两种方法都是为了放大帧和帧之间的区别。因为篇幅关系,现在我介绍一下方法一的方案具体怎么做,也给出一些代码/命令,感兴趣的小伙伴可以动手试一试:方案 1:先画后切- step1:使用ffmpeg - drawtext 命令给整个视频画帧号ffmpeg -i input.mp4 -vf drawtext=fontcolor=black:box=1:boxcolor=white:fontsize=40:fontfile=msyh.ttf:line_spacing=7:text=%{n}:x=0:y=0 -vframes 600 -y -qscale 0 out.mp4
这样,你就得到了一个画好了帧号的视频:然后再执行切帧操作,这里关键参数是text={n}
这个写法是表示标注的是帧号。更多写法可以参考 ffmpeg 说明书。 - step 2:切帧命令 ffmpeg image2 或 opencv 切帧ffmpeg image2 命令:ffmpeg -i out-1.flv -r 1 -q:v -f image2 ./result/image-%3d.jpeg
不知道为什么使用 ffmpeg 我总觉得切出来的损耗很高(可能是使用的无损参数有点问题),所以我用 opencv 实现了一把,这种方案看起来损耗至少看起来没那么大(opencv实现代码如下):
def cutFrame(srcFilePath,dstFolderPath): srcFileName = srcFilePath.split('/')[-1].split('.')[0] print(srcFileName) dstFolderPath = dstFolderPath + srcFileName + '/' times=0 #提取视频的频率,每1帧提取一个 frameFrequency=1 if not os.path.exists(dstFolderPath): #如果文件目录不存在则创建目录 os.makedirs(dstFolderPath) camera = cv2.VideoCapture(srcFilePath) count = 0 while True: times+=1 res, image = camera.read() if not res: print('error ! not res , not image') break countMax = 400 if times%frameFrequency==0 and count < countMax: count+=1 print(count) cv2.imwrite(dstFolderPath + srcFileName + '-'+ str(times-1)+'.jpg', image) print(dstFolderPath + srcFileName +'-'+ str(times)+'.jpg') print('图片提取结束') camera.release()
经过上面两步,你将得到:- 一个标记了帧号的视频文件- 每一帧都有帧号的图片文件集但在后面的识别图片帧号会遇到一个新的坑:你不知道需要具体需要预测的图片的坐标是多少(因为你在输入预测图片时,需要知道图片的具体 x,y,而不是整张图片输入),也就是说 个位数帧,和十位数帧,和百位数帧,具体的 x,y 都是不一样的,分别是(14,22);(28,22);(42,22) :因为你在预测的时候,不知道具体帧号,所以这个预测位置你不好判断是多长。如果你一刀切使用最长的(42,22),个位数帧的空余位置,因为你的模型/ocr 工具 没有学习过,所以会预测出奇怪的字符串。为了解决这个裁切数字准确的问题,我们有了第二个方案:更好的方案 2:先切后画- step 1:切帧命令 ffmpeg image2 或 opencv 切帧同上文方案 1- step 2:切好的帧画上帧号,具体命令和方案 1 不一样(text 参数)
ffmpeg -i test.jpg -vf drawtext=fontcolor=black:box=1:boxcolor=white:fontsize=40:fontfile=msyh.ttf:line_spacing=7:text='00001':x=0:y=0 -vframes 600 -y -qscale 0 output.jpg
这里的 text 参数 text = '00001'
,可以通过读step1 的名字拿到(切视频的时候的视频帧是命名是可以控制的,比如 frame-01.jpg,你可以拿到'01',然后再通过格式化%03d 的方式,对齐帧号为“00001”,再填到命令中去,这样所有的帧号就都是 4 位数甚至更多,从而做到了对齐。)经过上面两步,你将得到:- 一个标注了帧号的且对齐了位数的图片集看到这样的图片标注集合,别提多舒服了~可是,下一个问题来了,如果使用方案 2,那“带标记的视频源”如何拿到呢?这是折损的初始输入部分。这个问题简单,你可以通过 opencv 来合成无声视频片段,这个也是几乎视觉无损的:
def frameToAvi(srcFolderPath,dstFolderPath): for root, dir, files in os.walk(srcFolderPath): count = 0 for f in files: #print(f) if not f.endwith('jpg'): continue else: count += 1 fourcc = cv2.VideoWriter_fourcc(*'XVID') videoWriter = cv2.VideoWriter(dstFolderPath+'/'+srcFolderPath.split('/')[-1]+'.avi', fourcc, 18, (1088,1920)) ## 一定要对上 宽高,不然写不进去 3506463247-106.avi for i in range(0,count): img12 = cv2.imread(root + '/' + '1-' + str(i) + '.jpg') print(root + '/' + '1-' + str(i) + '.jpg is reading') cv2.imshow('img',img12) cv2.waitKey(1) videoWriter.write(img12) print(root + '/' + '1-' + str(i) + '.jpg is succeed') videoWriter.release() print('over')
上面的代码拼图片链接,使用 os.path.join() 会更好,另外值得一提的是:videoWriter = cv2.VideoWriter(dstFolderPath+'/'+srcFolderPath.split('/')[-1]+'.avi', fourcc, 18, (1088,1920)) ## 一定要对上 fps 宽高,不然写不进去 3506463247-106.avi
cv2.videoWriter 方法,要写生成视频的宽高,这里的宽高数据要从你切的图里面获得(opencv可以得到,你也可以打开右键图片简介读取),批量视频就写死,各种不同的视频就读一张图。fps 根据你的需求来调整,一切参数尽量模拟原未标号的视频。到这里,方案 2 就全部结束了。3/使用 OCR 识别模型/调用 OCR 工具,识别帧号未对齐的帧 经过part2 的一系列骚操作,你下一步需要做的就是使用 ocr 工具来识别帧号,工具如何制作和使用已经在我的前期公众号推送中说明了:《“自己动手,丰衣足食”——训练一个属于自己的OCR文字识别库(mac环境)》,我把我遇到的坑和解决方案都记录下来了;当然如果你嫌麻烦,你也可以使用在线的ocr 转换服务。4/ 丢帧的 2 种处理方法测试过程中发现不同流的丢帧处理方法不太一样,一共有 2 种类型,一种是传输过程中直接丢弃,另一种是延续上一帧,也就是说: - 传输过程中直接丢弃的方案,收集到的帧号是: 0,1,2,3,5 ... 丢了第 4 帧。- 传输过程中延续上一帧的方案,收集到的帧号是:0,1,2,3,3,5... 丢掉的第 4 帧用上一帧来弥补。值得一提的是,丢帧率和网络情况也是强相关的,如果对丢帧率的专项测试,需要将自己的网络情况仔细整理并划分类别(或者专业的网络损失实验室)来做这项专项测试。对帧率等指标感兴趣的朋友,可以参考我的这篇往期文章:《码率、分辨率、帧率那点事儿》对于全参考系算法的一些坑,我们下篇再讲,2020 年十一双节欢庆,预祝各位读者朋友节日快乐,月圆人团圆~!?