AVFrame YUV420p与OpenCV Mat BGR24互转的压缩伪影问题
我用C++结合FFmpeg将MP4容器中的H264视频转码为MP4容器的H265视频,原本流程能生成清晰画面,转码结果经FFprobe验证无误。但在H264解码完成后、H265编码开始前,加入AVFrame与OpenCV cv::Mat互转的函数后,出现了压缩伪影问题,具体现象:
- 输入像素格式为YUV420p(FFprobe确认),移除Step1的workaround(将
frame->format设为AV_PIX_FMT_RGB24)时,互转后输出视频有明显压缩伪影; - 保留该workaround后,输出视频画质与未调用该函数时一致,且修改像素的操作可正常生效;
- 可视化检查显示转Mat后的画面无问题,但转回AVFrame后出现伪影。
相关代码
void modifyVideoFrame(AVFrame * frame) { // STEP 1: WORKAROUND, overwriting AV_PIX_FMT_YUV420P BEFORE both sws_scale() functions below, solves "compression artifacts" problem; frame->format = AV_PIX_FMT_RGB24; // STEP 2: Convert the FFmpeg AVFrame to an openCV cv::Mat (matrix) object. cv::Mat image(frame->height, frame->width, CV_8UC3); int clz = image.step1(); SwsContext* context = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat::AV_PIX_FMT_BGR24, SWS_FAST_BILINEAR, NULL, NULL, NULL); sws_scale(context, frame->data, frame->linesize, 0, frame->height, &image.data, &clz); sws_freeContext(context); // STEP 3 : Change the pixels. if (false) { // TODO when "compression artifacts" problem with baseline YUV420p to BGR24 and back BGR24 to YUV420P is solved or explained and understood. } // UPDATE: Added VISUAL CHECK cv::imshow("Visual Check of Conversion AVFrame to cv:Map", image); cv::waitKey(20); // STEP 4: Convert the openCV Mat object back to the FFmpeg AVframe. clz = image.step1(); context = sws_getContext(frame->width, frame->height, AVPixelFormat::AV_PIX_FMT_BGR24, frame->width, frame->height, (AVPixelFormat)frame->format, SWS_FAST_BILINEAR, NULL, NULL, NULL); sws_scale(context, &image.data, &clz, 0, frame->height, frame->data, frame->linesize); sws_freeContext(context); }
使用的FFmpeg参数
copy_audio => 1 copy_video => 0 vid_codec => "libx265" vid_video_codec_priv_key => "x265-params" vid_codec_priv_value => "keyint=60:min-keyint=60:scenecut=0" // 编码器输出信息 x265 [info]: HEVC编码器版本 3.5+98-753305aff x265 [info]: 编译信息 [Windows][GCC 12.2.0][64位] 8bit+10bit+12bit x265 [info]: 使用CPU特性: MMX2 SSE2Fast LZCNT SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2 x265 [info]: Main profile, Level-3.1 (Main tier) x265 [info]: 线程池0使用64线程,NUMA节点0 x265 [info]: 切片数 : 1 x265 [info]: 帧线程数 / 池特性 : 1 / wpp(12行) x265 [info]: 编码QT: 最大CU尺寸, 最小CU尺寸 : 64 / 8 x265 [info]: 残差QT: 最大TU尺寸, 最大深度 : 32 / 1 帧间 / 1 帧内 x265 [info]: 运动估计 / 搜索范围 / 亚像素精度 / 合并模式 : hex / 57 / 2 / 2 x265 [info]: 前瞻帧 / B帧数量 / B帧自适应 : 15 / 4 / 0 x265 [info]: B帧金字塔 / 加权P帧 / 加权B帧 : 1 / 1 / 0 x265 [info]: 参考帧数量 / 参考帧限制 CU / 深度 : 3 / 开启 / 开启 x265 [info]: AQ: 模式 / 强度 / 量化组尺寸 / CU树 : 2 / 1.0 / 32 / 1 x265 [info]: 码率控制 / qCompress : ABR-2000 kbps / 0.60 x265 [info]: VBV/HRD缓冲区 / 最大码率 / 初始填充 : 4000 / 2000 / 0.750 x265 [info]: 工具: rd=2 psy-rd=2.00 rskip mode=1 signhide tmvp fast-intra x265 [info]: 工具: strong-intra-smoothing lslices=4 deblock sao
1. 两次颜色空间转换的不可逆精度损失
当不使用workaround时,你执行了YUV420p ↔ BGR24两次跨格式转换:
- YUV420p是平面格式,亮度(Y)通道全分辨率,色度(U/V)通道是1/2分辨率亚采样,且默认采用BT.601/BT.709色域;
- BGR24是打包格式,三个通道全分辨率存储,OpenCV默认采用sRGB色域。
转换过程中,色度通道的插值运算、色域空间的映射都会带来不可逆的精度损失。人眼对RGB画面的细微损失不敏感,但转换回YUV420p后,这些差异会被x265的压缩算法放大,最终形成可见伪影。
而你的workaround本质是跳过了真正的颜色空间转换:将frame->format强制设为AV_PIX_FMT_RGB24后,sws_getContext创建的是RGB24与BGR24之间的通道翻转上下文(仅调整R、G、B通道顺序),没有涉及YUV与RGB的色域转换,因此数据几乎无损失,编码后画质与原始一致。
2. 像素格式布局不匹配导致的转换错误
YUV420p是多平面格式,AVFrame的data数组包含三个指针(分别指向Y、U、V平面),linesize数组对应三个平面的行字节数;而BGR24是单平面格式,仅用data[0]和linesize[0]。
当你直接用原始YUV420p的AVFrame接收转回的YUV数据时,若未提前确保AVFrame的内存布局(如linesize的字节对齐、各平面的内存大小)与转换要求完全匹配,sws_scale可能出现数据写入偏移、越界或覆盖的情况,导致转回的YUV数据错乱,最终产生伪影。
而workaround下,转换仅涉及单平面的通道翻转,linesize和内存布局的兼容性问题被掩盖,因此不会出现数据错乱。
3. 低精度转换算法的误差累积
你使用的SWS_FAST_BILINEAR是快速线性插值算法,虽然速度快,但转换精度较低。在YUV与RGB的双向转换中,这种低精度算法会累积误差,进一步加剧画质损失,最终在编码后显现为伪影。
内容的提问来源于stack exchange,提问作者Walter

