视频渲染“扭曲”问题处理
视频营销业务中,是一个新兴领域。视频能给用户带来更加全面,有冲击力的体验。讯飞AI营销也在致力于发展视频技术。这次我们来分享一次之前遇到一个困扰我们多日的技术问题。
视频被扭曲了
原来应该是这样
但实际上是这样
背景介绍
抛去技术实现细节,程序大体步骤如下:
- 视频帧经由ffmpeg解码为RGB格式
- RGB格式数据经过OpenGL进行GPU渲染
- GPU输出渲染后的RGB数据
- RGB数据经过ffmpeg编码重新生成视频帧
其中:
- RGB格式是指,每个像素由红绿蓝三个色值组成,每个色值使用8bit表示。如此一个像素需要3B表示,在内存中的表示方式为RGBRGBRGB……
- 于此对应RGBA,在RGB基础上添加Alpha(透明度),如此每个像素需要4B来表示RGBARGBARGBA……
排查思路
中医讲究“望闻问切”,调试程序不外如是。首先我们对故障现象进行观察:
- 常规尺寸
1280*720
800*600
无扭曲现象 - 其他尺寸 如
400*300
,404*300
等出现扭曲 - 不使用GPU渲染效果,对应尺寸无扭曲现象
- 部分GPU渲染效果,所有尺寸无扭曲现象
对现象进行简单的总结:
- 当对某些尺寸,使用特定的OpenGL渲染效果时,会出现图像扭曲
- 这个某些尺寸应该和分辨率有关,具体关系不明
定位分析
病征放在眼前了,虽然还有不明确的地方,但也不能再走一套“螺旋CT”,“核磁共振”再做考虑了。就此先做一系列分析:
只有当使用OpenGL渲染时,才会出现扭曲
- 基本可以定位问题出现在OpenGL渲染阶段
对某些尺寸,使用特定OpenGL效果才会出现扭曲
- 可能是OpenGL的效果代码和尺寸有兼容性问题
制定手段
心里有谱了,先配服药试试?
如何确定OpenGL渲染阶段出现的问题?
OpenGL内部不好调试,在输入阶段和输出阶段分别将视频帧导出
- 如果输入正确,输出异常,基本可以确定是中间环节的问题
- 如果输入就错误,需要重新考虑方案
- 打开OpenGL渲染窗口,直接观察渲染过程
如何确定是否OpenGL的效果代码和尺寸有兼容性问题
- 对比正常效果代码和异常效果代码
- 对异常效果代码审查,调试。定位到有问题的部分
执行处理
有没有效果,先打“一针”再说
观察OpenGL的输入输出帧。将RGB数据格式化后保存层jpg
int save_frame_as_jpeg(AVCodecContext *pCodecCtx, AVFrame *pFrame, int FrameNo) { AVCodec *jpegCodec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); if (!jpegCodec) { return -1; } AVCodecContext *jpegContext = avcodec_alloc_context3(jpegCodec); if (!jpegContext) { return -1; } jpegContext->pix_fmt = pCodecCtx->pix_fmt; jpegContext->height = pFrame->height; jpegContext->width = pFrame->width; if (avcodec_open2(jpegContext, jpegCodec, NULL) < 0) { return -1; } FILE *JPEGFile; char JPEGFName[256]; AVPacket packet = {.data = NULL, .size = 0}; av_init_packet(&packet); int gotFrame; if (avcodec_encode_video2(jpegContext, &packet, pFrame, &gotFrame) < 0) { return -1; } sprintf(JPEGFName, "dvr-%06d.jpg", FrameNo); JPEGFile = fopen(JPEGFName, "wb"); fwrite(packet.data, 1, packet.size, JPEGFile); fclose(JPEGFile); av_free_packet(&packet); avcodec_close(jpegContext); return 0; }
- 结果:符合预期,输入帧正常,输出帧扭曲(和视频效果一致)
打开OpenGL渲染窗口观察
glfwWindowHint(GLFW_VISIBLE, 1); glfwSetWindowPos(window,0,0); ... glfwSwapBuffers(window); glfwPollEvents();
- 结果:视频帧渲染扭曲,动态效果正常(与视频效果不同),说明从OpenGL获取数据还有二次扭曲?
对比OpenGL效果代码
- 结果异常代码都使用了坐标变换 似乎是坐标整个错乱了
编写简单的效果观察坐标情况,
vec4(x,y,(x+y)/2,1);
OpenGL输入正常,同上
输出帧如图:
进一步分析
“一针”下去,我们获取了更多的现象,再来进一步分析:
从上面现象来看,输入正常,但是在OpenGL贴图和输出阶段都出现了扭曲,具体体现了为坐标系错乱。
经过Google搜索(关键字 opengl crooked
),一般来说这种问题都是因为GPU 的alignment
(数据对齐)默认为4B导致的。数据对齐的意义是能够提升数据读写效率,具体就不在此讨论了。
具体来说就是GPU每次读取4Byte数据,视频帧逐行读入GPU,如果行末数据不足4B,应该将填充补齐不足的部分。
以一个2*2的全红的像素举例:
正常数据为 FF 00 00 FF 00 00 FF 00 00 FF 00 00
按照上述规则数据应该为FF 00 00 FF 00 00 FF FF FF 00 00 FF 00 00 FF FF
我们按照行来分割数据进行表示的话
FF 00 00 FF 00 00 | FF FF
FF 00 00 FF 00 00 | FF FF
上述数据|
左侧为像素数据,右侧为填充数据
解决方式,就是将alignment
设置为1
尝试&困惑
但是故障现象和上述分析结果存在冲突的地方:
在最开始,我们提到,400*300
尺寸出现扭曲。
RGB格式每个像素点3B,宽400px
应该是不会出现对齐问题的。
不管是否有效,试一试,买不了吃亏,买不了上当。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
结果:不出所料,没有任何效果
三板斧用完,并没有出现任何变化,是哪个环节有问题呢?
没有方向的时候,只好进行一些尝试了
按照数据对齐为4B,实际上RGBA格式比RGB亲和度更好,我们将输入格式调整为RGBA
400*300
尺寸居然正常了!可这是为什么呢?
在此基础上,进行新一轮测试:
402*300
,404*300
依然出现扭曲
重新分析
通过回顾之前的验证方案,发现一个疑点:
为了观察数据,前面将视频帧转化为jpg进行观察,实际上已经进行过额外的处理,能否有办法观察更原始的数据呢?
首先导出原始数据:
static int frame_write(unsigned char *image, int imageWidth, int imageHeight, char *filename)
{
char fname_bmp[128];
sprintf(fname_bmp, "%s.rgb", filename);
FILE *fp;
if (!(fp = fopen(fname_bmp, "wb")))
return -1;
fwrite(image, sizeof(unsigned char), (size_t)(long)imageWidth * imageHeight * 4, fp);
fclose(fp);
return 0;
}
我们将像素数据上传到工具站 http://rawpixels.net
填好参数
width:404
height:300
Predefined format: RGB32
Pixel Format:RGBA
居然出现了扭曲!而且和视频贴图中的扭曲一致!
看来找到病根了?
通过查询ffmpeg文档,发现解码的视频帧存在padding,其alignment为16或32Byte(实测当前使用的服务器为32B)
<-------------- linesize ------------------------>
+-------------------------------+----------------+ ^
| | | |
| | | |
| picture | padding | | height
| | | |
| | | |
+-------------------------------+----------------+ v
<----------- width ------------->
那么可以得出,其具体现象为:
- 当视频宽度不为8的倍数时,由于数据对齐的存在,视频帧右侧会被添加一个黑边,使其满足宽度为8的倍数
据此上述的参数进行修改
width:408
结果: 符合预期,图片显示正常,且右侧出现4px的黑边
解决方案
- 调整数据格式为RGBA(之前已做)
- 在数据输入OpenGL前,清除padding
- 在数据输出OpenGL后,还原padding
结果:所有扭曲的现象消失,问题解决
总结
本次问题有两部分
主要是ffmpeg 解码数据 alignment为32B 可能带有一定的padding
linesize = 32 * ceil(width*pixSize/32) padding = linesize - width*pixSize
- 当为RGB格式时 pixSize 为 3Byte,所以当width为32的倍数时,padding为0,所以
400*300
出现扭曲 - 当为RGBA格式时 pixSize 为 4Byte 所以当width需要为8的倍数,padding为0,所以
400*300
效果正常 - 而OpenGL不需要这个padding的存在,padding使得图像像素错位,且输出后ffmpeg又将输出的数据当作有padding处理,导致二次错位
- 所以需要在输入OpenGL前取出padding,并在输出OpenGL后将其还原
- 当为RGB格式时 pixSize 为 3Byte,所以当width为32的倍数时,padding为0,所以
其次,OpenGL alignment默认为4B,若解决问题一,且数据格式不转化为RGBA
- 则当
width*3 %4 != 0
,也就是视频宽度不为4的倍数的话,同样会出现扭曲现象 - 即当使用RGB格式时 视频宽度必须为4的倍数
- 所以,需要将数据格式改为RGBA(当然也可以在上一步,按照alignment为4B处理)
问题回顾
- 则当
最初判断,问题出现在OpenGL渲染阶段,可以算对,也可以算不对。
- 实际情况是,输入OpenGL的时候,数据格式不符合OpenGL的要求,导致OpenGL渲染出错
- 不对的点在于,在查看了输入帧对应的JPG图像之后,将问题锁定到了OpenGL内部。相反,应该直接查看原始数据。
- 工具很重要,在导出RGB数据后,如何查看造成了一定困扰。直到后来找到了工具站http://rawpixels.net
- 这个问题本质还是对数据格式有点想当然了,没有快速意识到alignment的存在