我使用 C++ 和 FFmpeg 将 .mp4 容器中的 H264 视频转码为 .mp4 容器中的 H265 视频。这与清晰的图像和通过 FFprobe 检查确认的编码转换完美配合。
然后,我在 H264 解码结束和 H265 编码开始之前调用一个额外函数。此时我有一个已分配的 AVFrame*,我将其作为参数传递给该函数。
该函数将 AVFrame 转换为 openCV cv::Mat 并向后转换。从技术上讲,这是简单的部分,但我在这个过程中遇到了压缩伪影问题,我不明白为什么会发生这种情况。
函数代码(包括后面问题的解决方法)如下:
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);
}
所示的代码(包括解决方法)工作完美,但不被理解。
使用 FFprobe,我确定输入像素格式为 YUV420p,这实际上是在帧格式中找到的 AV_PIX_FMT_YUV420p。如果我将其转换为 BGR24 并返回 YUV420p,而不使用步骤 1 中的解决方法,那么我会出现轻微的压缩伪影,但在使用 VLC 观看时清晰可见。所以在某些地方存在着损失,这就是我试图理解的。
但是,当我使用步骤 1 中的解决方法时,我会获得完全相同的输出,就好像未调用此额外函数一样(即清晰的 H265,没有压缩伪影)。为了确保转换发生,我修改了红色值(在现在显示 if(false) 的代码部分内),并且在使用 VLC 播放 H265 输出文件时我确实可以看到更改。
从该测试中可以清楚地看出,在输入转换后,AVFrame 中存在的数据从 YUV420P 到 cv::Map BGR24,将其转换回原始 YUV420P 输入数据所需的所有信息和数据均可用。然而,如果没有解决方法,情况就不会发生,压缩伪影证明了这一点。
我使用了以 H264 编码并可在“Blender”网站上获取的影片剪辑“Charge”的前 17 秒。
是否有人有一些解释或可以帮助我理解为什么没有解决方法的代码不能很好地将输入数据向前转换然后向后转换回原始输入数据。
与我在解决方法或(更新)视觉检查部分(cv::imshow)中看到的内容相比,如果代码的第 4 部分被注释:
这些是我在输入中使用的 FFmpeg StreamingParams:
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"
// Encoder output
x265 [info]: HEVC encoder version 3.5+98-753305aff
x265 [info]: build info [Windows][GCC 12.2.0][64 bit] 8bit+10bit+12bit
x265 [info]: using cpu capabilities: MMX2 SSE2Fast LZCNT SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
x265 [info]: Main profile, Level-3.1 (Main tier)
x265 [info]: Thread pool 0 using 64 threads on numa nodes 0
x265 [info]: Slices : 1
x265 [info]: frame threads / pool features : 1 / wpp(12 rows)
x265 [info]: Coding QT: max CU size, min CU size : 64 / 8
x265 [info]: Residual QT: max TU size, max depth : 32 / 1 inter / 1 intra
x265 [info]: ME / range / subpel / merge : hex / 57 / 2 / 2
x265 [info]: Lookahead / bframes / badapt : 15 / 4 / 0
x265 [info]: b-pyramid / weightp / weightb : 1 / 1 / 0
x265 [info]: References / ref-limit cu / depth : 3 / on / on
x265 [info]: AQ: mode / str / qg-size / cu-tree : 2 / 1.0 / 32 / 1
x265 [info]: Rate Control / qCompress : ABR-2000 kbps / 0.60
x265 [info]: VBV/HRD buffer / max-rate / init : 4000 / 2000 / 0.750
x265 [info]: tools: rd=2 psy-rd=2.00 rskip mode=1 signhide tmvp fast-intra
x265 [info]: tools: strong-intra-smoothing lslices=4 deblock sao
从原始视频数据到 H264 YUV420P 的转换由于每像素 12 位二次采样(4:2:0)而产生很小的损失,并且帧线持有 3 个平面(linesize、linesize/2、linesize/2)。
当我将其转换为 RGB24 或 BGR24 以与 openCV 的 cv::Mat 一起使用时,原始内容没有恢复,但处理为 YUV420 的原始内容被恢复。因此,RGB/BGR 开始时已经有很小的损失,但一般来说,肉眼几乎看不到单个步骤。
然后当我将RGB24转换回YUV420时,无论有没有处理,都会再次进行一轮子采样,但是这次子采样是从之前已经处理过的RGB/BGR开始的,与原始的不匹配。输出大小保持不变(每像素 12 位),因为 YUV420 转 RGB 确实恢复了原始大小(只是不是原始质量)。 当将新的 YUV420 以 H265 格式写入文件时,它会通过编解码器处理,这与 H264 不同,这可能解释了为什么除了压缩伪影之外图像变得更暗,正如 Christoph Rackwitz 在评论中注意到和提到的那样。
总的来说,图片中显示的退化就是结果。
如果无法访问原始视频数据,解决方案是创建一个新的 FFmpeg AVFrame 来保存 YUV444P。这将使文件的大小加倍,因为它使用每像素 24 位来实际保存每像素 12 位的信息。
然后,YUV444P 经过可选处理(例如应用叠加)后,可以提供给 H265 编解码器。任何其他子采样 4:2:2、4:1:1、4:1:0、3:1:1 甚至 4:2:0 本身都会导致除原始 4:2 之外的额外损失:由于再次二次采样而不是从原始开始,导致 0 转换。
如果拥有原始视频数据,那么当然可以在第一次转换/二次采样之前预先完成所有处理,然后直接转换为所需的像素格式和格式(例如 H265 YUV420 或其他任何格式)。
所以OP的问题不是FFmpeg sws_scale()之一,而是我在开始编码之前没有思考的问题之一:)