FFmpeg转16位整数/32位浮点RGB为何存在色彩偏差?
FFmpeg YUV转RGB时16位/浮点输出未达100%白色的原因分析
以下Python代码用于验证FFmpeg将8位YUV输入转换为不同位深RGB的能力:脚本先生成无损编码的YUV测试帧,再提取8位整数、16位整数、32位浮点RGB帧,以及原始YUV帧。测试发现,对应100%白色的像素,8位RGB输出正确,但16位和浮点RGB输出未达到理论上的100%值。
import cv2 import subprocess import numpy as np ffmpeg = "ffmpeg -hide_banner -loglevel error -y" flags = "-sws_flags accurate_rnd+bitexact+full_chroma_int+neighbor -sws_dither none" for input_color_range in ("tv", "pc"): # Generate YUV444 video and encode it losslessly subprocess.check_call(rf"{ffmpeg} -y {flags} -color_range {input_color_range} -f lavfi -i yuvtestsrc {flags} -pix_fmt yuv444p -color_range {input_color_range} -x264-params qp=0 -frames 1 -c:v libx264 video_temp.mp4") # Extract YUV444 frame, as well as 8-bit int, 16-bit int and 32-bit float frame subprocess.check_call(rf"{ffmpeg} -y {flags} -i video_temp.mp4 {flags} -f rawvideo video_temp_444_frame1.yuv") subprocess.check_call(rf"{ffmpeg} -y {flags} -i video_temp.mp4 {flags} -color_range pc -pix_fmt rgb24 video_temp_444_frame1_8u.png") subprocess.check_call(rf"{ffmpeg} -y {flags} -i video_temp.mp4 {flags} -color_range pc -pix_fmt rgb48be video_temp_444_frame1_16u.png") subprocess.check_call(rf"{ffmpeg} -y {flags} -i video_temp.mp4 {flags} -color_range pc -pix_fmt gbrpf32be -f rawvideo video_temp_444_frame1_32f.gbrpf32be") # Read these frames into (height, width, 3) Numpy arrays containing YUV or RGB data data_8u = cv2.imread("video_temp_444_frame1_8u.png", cv2.IMREAD_UNCHANGED)[..., ::-1] data_16u = cv2.imread("video_temp_444_frame1_16u.png", cv2.IMREAD_UNCHANGED)[..., ::-1] data_yuv = np.rollaxis(np.frombuffer(open("video_temp_444_frame1.yuv", "rb").read()).view(np.uint8).reshape((3, 240, 320)), 0, 3).copy() data_32f = np.rollaxis(np.frombuffer(open("video_temp_444_frame1_32f.gbrpf32be", "rb").read()).view(np.dtype(">f4")).reshape((3, 240, 320)), 0, 3).copy() data_32f[..., (0, 1, 2)] = data_32f[..., (2, 0, 1)] # This pixel in yuvtestsrc corresponds to limited-range YUV=(235, 128, # 128)=(100%, 0.0, 0.0), which should correspond to 100% white, i.e. # RGB=(100%, 100%, 100%). if input_color_range == "tv": i, j = 10, 294 # This pixel in yuvtestsrc corresponds to full-range YUV=(255, 128, # 128)=(100%, 0.0, 0.0), which should correspond to 100% white, i.e. # RGB=(100%, 100%, 100%). elif input_color_range == "pc": i, j = 10, 319 else: raise Exception(input_color_range) # Print pixel values print("") print(f"Values for {input_color_range}-range input video at ({i}, {j}):") print("- 8-bit YUV input = %s" % data_yuv[i, j, :]) print("- 8-bit RGB output = %s (= %s)" % (data_8u[i, j, :], data_8u[i, j, :] / 255)) print("- 16-bit RGB output = %s (= %s)" % (data_16u[i, j, :], data_16u[i, j, :] / 65535)) print("- Float RGB output = %s" % data_32f[i, j, :])
测试输出结果如下:
Values for tv-range input video at (10, 294): - 8-bit YUV input = [235 128 128] - 8-bit RGB output = [255 255 255] (= [1. 1. 1.]) - 16-bit RGB output = [65283 65283 65283] (= [0.99615473 0.99615473 0.99615473]) - Float RGB output = [0.9961547 0.9961547 0.9961547] Values for pc-range input video at (10, 319): - 8-bit YUV input = [255 128 128] - 8-bit RGB output = [255 255 255] (= [1. 1. 1.]) - 16-bit RGB output = [65280 65280 65280] (= [0.99610895 0.99610895 0.99610895]) - Float RGB output = [0.99610895 0.99610895 0.99610895]
问题原因分析
核心差异来自FFmpeg在不同位深格式下的色彩转换逻辑设计:
1. 8位RGB的饱和对齐处理
对于8位RGB输出,FFmpeg优先保证视觉效果,做了边界饱和映射:
- 有限范围(TV)YUV的Y峰值235,或全范围(PC)YUV的Y峰值255,会被直接映射到RGB的最大值255(即100%白色)。
- 这种处理是为了适配8位格式的有限精度,避免视觉上的亮度偏差,让用户看到预期的纯白效果。
2. 高位深/浮点的精确数值映射
而16位整数和32位浮点格式的转换,FFmpeg遵循严格的色彩转换标准公式,没有做额外的饱和拉满处理:
- 转换过程中,FFmpeg保留了计算的精确数值,没有强制把输入Y的峰值对齐到输出RGB的理论最大值。
- 以TV范围输入为例,Y=235按公式计算理论值为1,但实际输出的0.9961547是由于FFmpeg内部定点转浮点的精度限制,或是转换矩阵的精确系数计算导致的微小偏差——这是为了保证高位深格式下数值的准确性,而非视觉上的绝对对齐。
- 全范围输入的Y=255转换同理,严格按线性映射计算后,结果接近但不等于理论上的1.0或65535。
这种设计差异是FFmpeg的有意选择:高位深格式更注重数值精度,用于专业后期处理场景;而8位格式更注重视觉体验,用于普通播放场景。
内容的提问来源于stack exchange,提问作者F.X.
相关产品推荐
相关产品推荐

