将 16 位灰度 PNG 转换为 HEVC/x265

Convert 16bit Grayscale PNG to HEVC/x265

我想将 12 位图像信号转换为 HEVC 以进行有效压缩。因为我需要能够重建原始的 12 位信号,所以压缩需要是无损可逆的。目前我的数据是 16 位 PNG 文件。

我的第一次尝试是使用 ffmpeg:

ffmpeg -y -framerate 1 -i input.png -c:v libx265 -x265-params "lossless=1" output.mp4

不幸的是,输出是不可逆的。从mp4中提取图像时,像素值略有偏差。

ffmpeg -i output.mp4 -vframes 1 reconstructed.png

以下答案建议先将输入转换为 YUV444 以避免 ffmpeg 的意外行为:Lossless x264 compression

到目前为止,我未能成功将我的 16 位文件转换为 YUV,将其转换为 x256 并在解码时接收到正确的重建。

是否有直接的方法将 16 位图像转换为 HEVC?

我找到了一个有小舍入误差的解决方案:

编码:

  • 基于以下post:How to render png's as h.265 12 bit video?
    使用可以使用以下编解码器参数:-x265-params lossless=1 -pix_fmt yuv444p12le 用于有损 12 bpc 编码。

  • 通过反复试验,我意识到12位数据必须在每个16位元素的高12位中。 您需要将输入像素放大 16 倍才能将数据放在高位。
    (缩放 16 相当于将 uint16 元素左移 4)。
    要放大像素,您可以使用 colorlevels 视频过滤器:
    -vf colorlevels=rimax=0.0625:gimax=0.0625:bimax=0.0625

以下命令对单个帧进行编码:

 ffmpeg -i input.png -vf colorlevels=rimax=0.0625:gimax=0.0625:bimax=0.0625 -c:v libx265 -x265-params lossless=1 -pix_fmt yuv444p12le output.mkv

解码:

  • 为了解码,需要将像素除以16,以便将数据放在低12位。
    (除以 16 相当于将 uint16 元素右移 4)。
    我找不到使用 colorlevels 的解决方案,所以我使用了 curves 过滤器:
    -vf "curves=r='0/0 1.0/0.0625':g='0/0 1.0/0.0625':b='0/0 1.0/0.0625'"
  • 适合 16 位 PNG 的像素格式是 rgb48be

以下命令解码单个帧(并除以 16):

ffmpeg -i output.mkv -vf "curves=r='0/0 1.0/0.0625':g='0/0 1.0/0.0625':b='0/0 1.0/0.0625'" -pix_fmt rgb48be reconstructed.png

差异:
input.pngreconstructed.png 之间的最大绝对差是 4 级。
差异的原因可能是将 RGB 转换为 YUV 并返回时造成的舍入误差。


我使用了以下 MATLAB 代码进行测试:

I = imread('peppers.png');

% Build 10 PNG images (used as input).
for i = 1:10
    J = insertText(I, [size(I,2)/2-18, size(I,1)/2-36], num2str(i), 'FontSize', 72);
    J = imnoise(im2double(J), 'gaussian', 0, 0.01); % Add some noise
    J = uint16(round(J*4095)); % Convert to 12 bits range (range [0, 4095])
    imwrite(J, sprintf('input%02d.png', i), 'fmt', 'png', 'BitDepth', 16, 'Mode', 'lossless'); % Write to PNG file
end

 %Encode video file using x265 codec, and 12 bits YUV444 format. 
[status, cmdout] = system('ffmpeg -y -i input%02d.png -vf colorlevels=rimax=0.0625:gimax=0.0625:bimax=0.0625 -c:v libx265 -x265-params lossless=1 -pix_fmt yuv444p12le output.mkv');
if (status ~= 0), disp(cmdout);end

% Decode output.mkv into 10 PNG image files
[status, cmdout] = system('ffmpeg -y -i output.mkv -vf "curves=r=''0/0 1.0/0.0625'':g=''0/0 1.0/0.0625'':b=''0/0 1.0/0.0625''" -pix_fmt rgb48be reconstructed%02d.png');
if (status ~= 0), disp(cmdout);end

% Compare input and output:
for i = 1:10
    I = imread(sprintf('input%02d.png', i));
    J = imread(sprintf('reconstructed%02d.png', i));
    max_abs_diff = max(max(max(imabsdiff(I, J))));
    disp(['max_abs_diff = ', num2str(max_abs_diff)]);
end

更新:

使用灰度格式:
工作灰度时,不需要将像素格式转换为YUV。
从灰度转换为 YUV444 会将输入数据的大小乘以 3,因此最好避免转换。

以下命令对单个灰度帧进行编码:

 ffmpeg -i input.png -vf "curves=all='0/0 0.0625/1.0'" -c:v libx265 -x265-params lossless=1 -pix_fmt gray12le -bsf:v hevc_metadata=video_full_range_flag=1 output.mkv

以下命令解码单个灰度帧(并除以 16):

ffmpeg -i output.mkv -vf "curves=all='0/0 1.0/0.0625'" -pix_fmt gray16be reconstructed.png

最大绝对差为 2。


使用注意事项-bsf:v hevc_metadata=video_full_range_flag=1:

在H.265中,Y颜色通道的默认范围是“有限范围”。
对于 8 位,“有限范围”适用 [16, 235].
对于 12 位,“有限范围”适用 [256, 3760].
当使用“全范围”[0, 255] 表示 8 位或 [0, 4095] 表示 12 位时,您需要在流的元数据中指定它。
使用 FFmpeg 设置元数据的方法是使用 bitstream filter.

我试图对 10 位灰度数据实现同样的效果。

感谢 ffmpeg-user mailing list 上的 Paul B Mahol,我已经能够通过使用临时原始视频文件并欺骗原始视频分路器以我想要的位深度解释文件来解决剩余的舍入错误。

我假设相同的解决方案适用于 12 位数据并且可以扩展到 RGB 数据。 ffmpeg 命令行可以在我的相关(几乎重复)问题中找到: