ffmpeg如何在结尾添加帧_一种“视频帧对齐”的测试方案实践

点击 蓝字 ?关注【 测试先锋 】,不再迷路! 一起成为互联网测试精英,前瞻测试技术~ 663e3c6ad3335b1a91b1b6ec12f05cfc.png 导语  全参考清晰度测算的时候,输入两个视频帧序列,但是视频帧序列没有对齐,怎么知道丢了哪帧?又怎么知道补回哪一帧?今天介绍一种直播视频帧的对齐方案,如果您有更好的方法,欢迎在公众号下方留言联系作者探讨交流(文章留言近期开放),再次感谢您的关注和阅读。 ae0305a9539c2c4f60ddd0ff32adbeea.png 1 /  问题背景      全参考测算视频质量,广泛存在在视频业务场景。 开始算法计算时,是需要仔细的对齐这一系列的帧再能做计算的,因为全参考的计算原理是根据两张相同场景的图片数据,做比较测算。仔细想想:“帧对齐”其实这是一个挺麻烦的事儿,为什么这么说?因为: 第一、帧非常多(手机端直播业务一般在 18-25fps,pc 直播业务一般 60fps),想让人工挑出到底哪帧丢了,这是非常不现实的。 cce274d2fed89cf52a18878acd8704f3.png 第二、由于人眼的一个重要特性——视觉惰性,表现在人眼会存在一个视觉暂留,肉眼基本看不出来连续两帧的区别。人眼可以保留 0.1-0.4秒的影像。所以每一个视频都是一个非常快的“走马灯”,人们是很难通过图像特征来区别每一帧。

1e3f272bd3849e8d339244bb83216e5d.png

所以,“帧对齐”这个问题,拆解为两个问题:如何识别每一个帧+找到未对齐的帧 这两个问题。根据上述分析,我们可以拿到具体的折损测试方案:

           de601b93ce8a5c0061f669439cdde46b.png

输入源为两个视频,分别是“原视频”和“待比较视频”(折损视频),首先将两个视频进行拆帧处理;将视频处理为一系列的图片帧文件,然后进行帧对齐处理(将帧的分辨率处理成一致的分辨率,将丢帧补齐,将卡帧删掉并记录帧号),输出两列对齐后的视频帧序列,再合成对齐帧后的视频序列,再进行VMAF,PSNR等全参考得分的分数计算。

了解了上述的测试方案,我们来看下每一个部分都是怎么解决的。

2 / 如何识别每一帧     识别帧的两个思路:物理识别与代码识别。 物理识别是指,放大每一帧之间的区别,或者专门打上帧的标签来识别不同的图片。 代码识别目前 笔 者 没有 找 到 比较有效的方法来区别两帧, 因为在截取为 jpg 时 这个信息已经丢失了。 所以这里还是倾向于“物理识别”方案 。 这里根据两位测试前辈(在这特别感谢 eriel,austin)曾经尝试过的经验,有两种方法: 方法一: 给每一帧标记物理序号至某一个固定位置

ff810608dafabcb88e4210e2ae7205ed.png

方法二: 给每一帧上方标记一个黑白条形码至某一个固定位置,再来读黑白条形码

f317add9f95ed62dc9c61eafdc0ea300.png

然后,再通过代码/工具识别这个更容易识别的物理特征。总之,上述两种方法都是为了放大帧和帧之间的区别。因为篇幅关系,现在我介绍一下方法一的方案具体怎么做,也给出一些代码/命令,感兴趣的小伙伴可以动手试一试: 方案 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
     这样,你就得到了一个画好了帧号的视频:

af8c48f9f5600389f92f0aa00f2cd9d4.gif

然后再执行切帧操作,这里关键参数是
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) :

38f96a407d927087ad0836cfd6e3f8f6.png4a59702dec1616c91bfacbc64d7c7228.png529609fed38aad754da339de972fc92f.png

因为你在预测的时候,不知道具体帧号,所以这个预测位置你不好判断是多长。如果你一刀切使用最长的(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 位数甚至更多,从而做到了对齐。) 经过上面两步,你将得到:  - 一个标注了帧号的且对齐了位数的图片集

ed27050a67dac116c76ef527e7eda580.png5b807f96adeb0aa0a4e7d5b460d70e8c.pngf971b12d5dd757c65b1ce2dfec196483.png

看到这样的图片标注集合,别提多舒服了~ 可是,下一个问题来了,如果使用方案 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 年十一双节欢庆,预祝各位读者朋友节日快乐,月圆人团圆~!?

注:以上图片部分来自互联网,如有侵权请后台联系公众号,会第一时间删除,谢谢!ae0305a9539c2c4f60ddd0ff32adbeea.png974d0e3d1cfb335c454490e96b94c94a.pngedef4d0d973fdd705ac4d32bf97de86c.gif

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用FFmpeg解码B视频,可以结合使用avcodec_send_packet()和avcodec_receive_frame()函数进行解码,然后使用av_write_frame()函数将解码后的入输出文件。具体步骤如下: 1. 初始化FFmpeg ``` av_register_all(); avcodec_register_all(); ``` 2. 打开输入文件 ``` AVFormatContext *inputFormatCtx = NULL; avformat_open_input(&inputFormatCtx, inputFilePath, NULL, NULL); avformat_find_stream_info(inputFormatCtx, NULL); ``` 3. 找到视频流 ``` AVCodec *inputCodec = NULL; int videoStreamIndex = av_find_best_stream(inputFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &inputCodec, 0); AVCodecContext *inputCodecCtx = inputFormatCtx->streams[videoStreamIndex]->codec; ``` 4. 打开解码器 ``` avcodec_open2(inputCodecCtx, inputCodec, NULL); ``` 5. 初始化输出文件 ``` AVFormatContext *outputFormatCtx = NULL; avformat_alloc_output_context2(&outputFormatCtx, NULL, NULL, outputFilePath); avio_open(&outputFormatCtx->pb, outputFilePath, AVIO_FLAG_WRITE); ``` 6. 入输出文件头 ``` avformat_write_header(outputFormatCtx, NULL); ``` 7. 读取数据并解码 ``` AVPacket packet; AVFrame *frame = av_frame_alloc(); while (av_read_frame(inputFormatCtx, &packet) == 0) { if (packet.stream_index == videoStreamIndex) { avcodec_send_packet(inputCodecCtx, &packet); while (avcodec_receive_frame(inputCodecCtx, frame) == 0) { // do something with the decoded frame, e.g. write to output file av_write_frame(outputFormatCtx, frame); } } av_packet_unref(&packet); } ``` 8. 入输出文件尾 ``` av_write_trailer(outputFormatCtx); ``` 9. 释放资源 ``` avformat_close_input(&inputFormatCtx); avcodec_free_context(&inputCodecCtx); avformat_free_context(inputFormatCtx); avformat_free_context(outputFormatCtx); av_frame_free(&frame); ``` 这样,就可以使用FFmpeg解码B视频了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值