输入源为两个视频,分别是“原视频”和“待比较视频”(折损视频),首先将两个视频进行拆帧处理;将视频处理为一系列的图片帧文件,然后进行帧对齐处理(将帧的分辨率处理成一致的分辨率,将丢帧补齐,将卡帧删掉并记录帧号),输出两列对齐后的视频帧序列,再合成对齐帧后的视频序列,再进行VMAF,PSNR等全参考得分的分数计算。
了解了上述的测试方案,我们来看下每一个部分都是怎么解决的。
2 / 如何识别每一帧 识别帧的两个思路:物理识别与代码识别。 物理识别是指,放大每一帧之间的区别,或者专门打上帧的标签来识别不同的图片。 代码识别目前 笔 者 没有 找 到 比较有效的方法来区别两帧, 因为在截取为 jpg 时 这个信息已经丢失了。 所以这里还是倾向于“物理识别”方案 。 这里根据两位测试前辈(在这特别感谢 eriel,austin)曾经尝试过的经验,有两种方法: 方法一: 给每一帧标记物理序号至某一个固定位置 方法二: 给每一帧上方标记一个黑白条形码至某一个固定位置,再来读黑白条形码 然后,再通过代码/工具识别这个更容易识别的物理特征。总之,上述两种方法都是为了放大帧和帧之间的区别。因为篇幅关系,现在我介绍一下方法一的方案具体怎么做,也给出一些代码/命令,感兴趣的小伙伴可以动手试一试: 方案 1: 先画后切 - step 1:使用 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()
经过上面两步,你将得到:
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 年十一双节欢庆,预祝各位读者朋友节日快乐,月圆人团圆~!?