草庐IT

【编解码】记录一个ffmpeg解码生成YUV的 color range 问题,以及video_full_range_flag用法。

taotao830 2025-06-21 原文

1. 问题起因

注: 本文软件版本:ffmpeg 4.4 。安霸cv2x SDK 3.0.9

最近有客户在使用我们芯片的avc/hevc 编码器的提了一个问题,很有意思,我花了2天来找原因和做实验。现将过程和结论记录如此,希望能帮助到后来者。

1.1 问题的现象

客户使用我们的芯片去编码一段固定的YUV 序列(golden data,记为YUV-A),
得到HEVC码流(记作StreamB)后,再使用ffmpeg解码,得到YUV(记作YUV-B)。

ffmpeg -i case1.h265 -vcodec rawvideo -pix_fmt nv12 -an YUV-B.yuv

然后使用PSNR 和 VMAF,参考YUV-A去计算YUV-B,计算视频质量,以此想评估编码器性能。
意外的发现,直接使用默认参数编解码后,得分不高。
但是如果强行码流中的SPS中的VUI的 video_full_range_flag 字段从1改为0,得到StreamC,再去解码,得到新的YUV(记为YUV-C),再评估,参考YUV-A去计算YUV-C,得分大大提高。
因此,客户想跟我们确认这个参数是否可以改动,有没有什么影响。

1.2 问题的分析

首先得分高低肯定是因为每个像素的差值变化。
我简单比较了一下两个文件的像素值,发现YUV-B和YUV-A差值较大,而YUV-C 和 YUV-A的差值就比较小。
而在码流文件中,用vega比较了streamB 和 streamC,发现YUV数据完全一致,二者的参数中仅有SPS中的video_full_range_flag不同。
如图,一共60帧2个GOP,是只有2个SPS的数据不同。

但是解码出来的YUV数据差别很大,做了一个简单的数据统计,YUV-A/B/C 的统计数据如下:

输入YUV是我做的一个灰阶图,覆盖0~256范围。

解出来的YUV情况分别如下

YUV-B:16~235

YUVC:0~256

所以造成两个YUV这么大差异的原因,一定是ffmpeg解码流程的不同。
把ffmpeg解码YUV的两段log拿出来比较一下:

可以看出,ffmpeg把streamB 和 streamC识别成了不同的格式,一个是jpeg,一个mpeg。
这就是问题的关键的,这些是什么?会导致什么?
OK,下面开始正式分析ffmpeg的解码。

2. video_full_range_flag

2.1 color range : full (jpeg)/ limited(mpeg)

在正式看 video_full_range_flag 的问题之前,我们需要先了解一下mpeg 和 jpeg这么两个东西。
YUV的色彩范围分为两种:

  1. full range Y / U / V 的范围是[0, 255]
  2. limited range Y [16,235]
    UV[16,240]
    为什么会有这两种区别,主要是应用场景不同
    电视机一般只支持240个色阶,从16~255,这就是limited
    电脑显示器支持255个色阶,从0~255,这就是full。

在下面的内容中,你可以默认这些东西是等效的,因为不同软件,不同模块,使用了不同的表达方式,但是他们的含义都是一样的。

“full range” = “jpeg” = “pc” = “cg” = “high rgb”

“limited range” = “mpeg” = “tv” = “broadcast” = “low rgb”

2.2 video_full_range_flag

接下来,我们先来看一下video_full_range_flag的含义,看看这个字段能不能随意修改。
我以H264的白皮书来分析,翻到附录E。
video_full_range_flag字段解释如下:

后面还给了一些计算公式,简单来说就是,
如果video_full_range_flag=0,代表这段码流是limited range的。
如果video_full_range_flag=1,代表这段码流是full range的。
同时,默认值为0.
我不是很理解的就是为什么默认是0。
是个历史遗留问题?为了以前都是电视机播放? 可能是为了过去的兼容性问题吧。一直也不改。
感觉放到现在还是默认为1比较好,做一个全范围的。
我们芯片里的参数就是默认为1的。我认为1更全面更合理。

3. ffmpeg的解码问题

3.1 解码流程

回到ffmpeg的解码问题,从日志log可以看出来。

ffmpeg会把一种格式,解码成另一种格式。
对于 streamB -> YUV-B,是从 yuv420p(tv, bt709) 转到nv12(tv, bt709, progressive)。
对于 streamC-> YUV-C,是从 yuvj420p(pc, bt709)转到nv12(tv, bt709, progressive)。
对于输入的格式:
tv 和 pc我们前面已经解释过了,是color range,这个值就是根据 码流中的 “video_full_range_flag” 字段来确定的。
但是输出的格式:
我就不能理解了,居然默认是tv,也就是limited?
为什么呢?就是因为这个,导致了二者解码流程的不同,进而YUV数据出现巨大的差异, PSNR的得分变化很大。
下面来分析一下流程,
为什么streamB的分低,streamC的分高。

  1. StreamB
    我们的reference YUV 是 full range的。
    使用编码器编出来的H265也是full range的,没有做色彩的映射,同时标注了video_full_range_flag=1。
    ffmpeg检测到码流是full range的(因为video_full_range_flag=1),但是它默认输出limited range的 YUV,因此做了色彩映射,把[0,255]的范围映射到了[16, 235]之间,相当于每个像素值都做了偏移。
    在最后计算得分的时候,这些偏移都被当做了噪声。refer YUV是[0, 255]的,结果YUV是[16, 235]的,可想而知,MSE均方差很大,得分就低。

  2. SteamC
    我们的reference YUV 是 full range的。
    使用编码器编出来的H265也是full range的,没有做色彩的映射,但是video_full_range_flag被强制改成了0。
    ffmpeg检测到码流是limited range的(因为video_full_range_flag=0),而且它默认输出limited range的 YUV,因此没有色彩映射,保持了码流中的color range,码流中的色彩范围实际是full range,相当于每个像素值都没有添加偏移。
    在最后计算得分的时候,因为没有偏移,每个像素间的差值就小,所以MES小,得分高。

这就是根本原因。

3.2 警告: [swscaler @ 0000022f98f360c0] deprecated pixel format used, make sure you did set range correctly

log中有一句warning,之前忽略了。
看了一下代码,这句话的触发条件就是,使用yuvj420p(或者yuvj444p/yuvj422p) 这个格式。
我们的码流中的video_full_range_flag=1导致在解码过程中,设置src format是yuvj420p,触发了这个打印。
已经提醒了 检查设置range。
只不过当初没看懂罢了。

3.3 解决方案

针对full range的reference YUV, full range编码的encoder,在用ffmpeg解码的时候,需要指定输出的范围是full,而不能使用默认的limited!

看一下ffmpeg支持的mode:

ffmpeg.exe -i case.h265 -vcodec rawvideo -pix_fmt nv12 -an case.yuv -lavfi "scale=out_range=full"

注意最后的
-lavfi “scale=out_range=full”

最后再check一下ffmpeg中的log是不是变化了

因为有时候我发现有些参数并没有生效,但是ffmpeg也不会报错,这里还是要自己在check看一遍。

4. 参考链接

有关【编解码】记录一个ffmpeg解码生成YUV的 color range 问题,以及video_full_range_flag用法。的更多相关文章

  1. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  2. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

    我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

  3. ruby - 有人可以解释一下在 Ruby 中注入(inject)的真实、通俗易懂的用法吗? - 2

    我正在学习Ruby,遇到了inject。我正处于理解它的风口浪尖,但当我是那种需要真实世界的例子来学习一些东西的人时。我遇到的最常见的例子是人们使用inject来添加一个(1..10)范围的总和,我不太关心这个。这是一个任意的例子。在实际程序中我会用它做什么?我正在学习,所以我可以继续使用Rails,但我不必有一个以Web为中心的示例。我只需要一些我可以全神贯注的目标。谢谢大家。 最佳答案 inject有时可以通过它的“其他”名称reduce更好地理解。它是一个对Enumerable进行操作(迭代一次)并返回单个值的函数。它有许多有

  4. ruby - 使用法拉第上传文件 - 2

    我在尝试使用Faraday将文件上传到网络服务时遇到问题。我的代码:conn=Faraday.new('http://myapi')do|f|f.request:multipartendpayload={:file=>Faraday::UploadIO.new('...','image/jpeg')}conn.post('/',payload)尝试发布后似乎没有任何反应。当我检查响应时this是我所看到的:#:post,:body=>#,#,@opts={}>,#],@index=0>>,#>],@ios=[#,#,@opts={}>,#],@index=0>,#],@index=0>

  5. ruby - rspec: raise_error 用法来匹配错误信息 - 2

    我使用raise(ConfigurationError.new(msg))引发错误我试着用rspec测试一下:expect{Base.configuration.username}.toraise_error(ConfigurationError,message)但这行不通。我该如何测试呢?目标是匹配message。 最佳答案 您可以使用正则表达式匹配错误消息:it{expect{Foo.bar}.toraise_error(NoMethodError,/private/)}这将检查NoMethodError是否由privateme

  6. 【ChatGPT】ChatGPT 的 N 种用法 - 2

    目录ChatGPT简介技术原理应用未来发展ChatGPT的10 种用法ChatGPT简介ChatGPT是一种基于深度学习的大型语言模型,由OpenAI公司开发。技术原理GPT是GenerativePre-trainedTransformer的缩写,意为生成式预训练变压器。它的技术原理是使用了一个基于注意力机制的变压器(Trans

  7. ruby - 是否有 Rack::Session::Cookie 用法的基本示例? - 2

    我找不到任何使用Rack::Session::Cookie的简单示例,并且希望能够将信息存储在cookie中,并在以后的请求中访问它并让它过期.这些是我能找到的唯一示例:HowdoIset/getsessionvarsinaRackapp?http://rack.rubyforge.org/doc/classes/Rack/Session/Cookie.html这是我得到的:useRack::Session::Cookie,:key=>'rack.session',:domain=>'foo.com',:path=>'/',:expire_after=>2592000,:secret=

  8. ruby - Ruby 方法的双冒号(双列或::)语法的惯用用法 - 2

    我是Ruby的新手,发现以下几对令人困惑示例同样有效:File.included_modulesFile::included_modulesFile.stat('mbox')#Returnsa'#'objectFile::stat('mbox')File.new("foo.txt","w")File::new("foo.txt","w")"asdf".size#Aninstancemethod"asdf"::size2+32::send(:+,3)#AnextremeexampleFile::new,尤其是我经常遇到的东西。我的问题:如果我永远避免使用::运算符来限定除类、模块和常量之

  9. ruby - 制作命令行程序 "full screen" - 2

    我想知道如何在shell中创建“全屏”窗口的外观,如在vim、emacs等中。是否可以在Ruby中以编程方式执行此操作?这对平台的依赖程度如何?编辑:我不是在寻找如何让我的shell进入全屏模式。我正在寻找一种方法来隐藏以前输入的命令并用应用程序“填充”shell屏幕。它适用于安装程序。 最佳答案 您可能正在寻找的是ncurses或S-Lang支持提供你的全TUI经验。Ruby的gem环境提供了几个可能值得探索的gem:$gemlist--remote|grep-icursescursesx(003)ffi-ncurses(0.4.

  10. ruby-on-rails - Ruby 的 range step 方法导致执行速度很慢? - 2

    我有这段代码:date_counter=Time.mktime(2011,01,01,00,00,00,"+05:00")@weeks=Array.new(date_counter..Time.now).step(1.week)do|week|logger.debug"WEEK:"+week.inspect@weeks从技术上讲,代码有效,输出:SatJan0100:00:00-05002011SatJan0800:00:00-05002011SatJan1500:00:00-05002011etc.但是执行时间完全是垃圾!每周计算大约需要四秒钟。我在这段代码中是否遗漏了一些奇怪的低效

随机推荐