You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ffmpeg HEVC对16bit PNG存储的10bit图像无损压缩问题咨询

10bit灰度图像HEVC无损编码回读像素值偏差问题

我尝试将10bit图像以视频格式做无损编码,优先选用HEVC编码。这批图像原本以16bit PNG格式存储(实际仅用到10bit位宽),目前我通过ffmpeg生成对应视频文件再回读验证,但一直无法实现完全无损的重建。
我目前的最优方案参考自Stack Overflow的相关回答,但正如该回答提到的,编码后回读得到的像素亮度值存在偏差,我推测是10bit与16bit格式转换过程中的舍入误差导致。我尝试过位偏移、左位复制、浮点缩放等多种转换方式,仍然没能实现真正的无损重建。
以下是可复现问题的代码,恳请各位帮忙指出问题所在,非常感谢:

import subprocess
import numpy as np
import matplotlib.pyplot as plt
import tempfile
import imageio

# 生成测试图像
bitdepth = 10
hbd = int(bitdepth/2)
im0 = np.zeros((1<<hbd,1<<hbd),dtype=np.uint16)
im0[:] = np.arange(0,1<<bitdepth).reshape(im0.shape)
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)
# 平铺图像确保尺寸至少为64像素,适配编码器要求
im0 = np.tile(im0, (2, 2))
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)
im0ref = im0
# 位偏移或者强度缩放逻辑
#im0 = (im0<<6)
#im0 = (im0<<6) + (im0>>4)
im0 = np.uint16(np.round(im0 * np.float64((1<<16)-1)/np.float64((1<<10)-1)))
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)

# 保存为PNG
tmp0 = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
print(f'临时文件路径: {tmp0.name}')
imageio.imwrite(tmp0.name,im0)

# 用ffmpeg编码
tmp1 = tempfile.NamedTemporaryFile(suffix='.mkv', delete=False)
# 注意:添加以下参数似乎对结果没有影响
#  + ' -bsf:v hevc_metadata=video_full_range_flag=1' 
mycmd = f'ffmpeg -y -i {tmp0.name}' \
  + ' -c:v libx265 -x265-params lossless=1' \
  + ' -pix_fmt gray10be' \
  + f' {tmp1.name}'
print(mycmd)
p = subprocess.run(mycmd.split(), capture_output=True)
print( '标准输出:', p.stdout.decode() )
print( '错误输出:', p.stderr.decode() )

tmp2 = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
mycmd = f'ffmpeg -y -i {tmp1.name}' \
  + ' -pix_fmt gray16be' \
  + f' {tmp2.name}'
print(mycmd)
p = subprocess.run(mycmd.split(), capture_output=True)
print( '标准输出:', p.stdout.decode() )
print( '错误输出:', p.stderr.decode() )

# 回读解码后的图像
im1 = imageio.imread(tmp2.name)
print('im1',np.min(im1),np.max(im1),im1.shape,im1.dtype)

# 位偏移或缩放回原始位宽
im1pre = im1
#im1 = (im1>>6)
im1 = np.uint16(np.round(im1 * np.float64((1<<10)-1)/np.float64((1<<16)-1)))

# 验证结果
plt.figure()
plt.imshow(im0ref)
plt.colorbar()

plt.figure()
plt.imshow(im1)
plt.colorbar()

plt.figure()
plt.imshow(np.int32(im1)-np.int32(im0ref))
plt.colorbar()

print('误差: ',np.linalg.norm((np.float32(im1)-np.float32(im0ref)).ravel()))

plt.show()

编辑:我已将该问题同步发布到FFmpeg用户邮件列表。
为方便大家测试,我还提供了简易脚本,可以生成10bit数据在16bit空间下的不同存储变体:

import numpy as np
import imageio

# 生成渐变测试图像,取值范围为0到(2^bitdepth - 1)
bitdepth = 10
unusedbitdepth = 16-bitdepth
hbd = int(bitdepth/2)
im0 = np.zeros((1<<hbd,1<<hbd),dtype=np.uint16)
im0[:] = np.arange(0,1<<bitdepth).reshape(im0.shape)

# 平铺图像确保尺寸至少为64像素,适配ffmpeg编码器要求
im0 = np.tile(im0, (2, 2))
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)

# 存储原始低位对齐的10bit图像
imageio.imwrite('gradient10bit-lsb.png',im0)

# 位偏移到高位对齐
im1 = (im0<<unusedbitdepth)
print('im1',np.min(im1),np.max(im1),im1.shape,im1.dtype)
imageio.imwrite('gradient10bit-msb.png',im1)

# 缩放至满16bit范围
im2 = np.uint16(np.round(im0 * np.float64((1<<16)-1)/np.float64((1<<bitdepth)-1)))
print('im2',np.min(im2),np.max(im2),im2.shape,im2.dtype)
imageio.imwrite('gradient10bit-scaledto16bits.png',im2)

# 左位复制,是缩放的低成本近似方案,参考PNG编码规范
im3 = (im0<<unusedbitdepth) + (im0>>(bitdepth-unusedbitdepth))
print('im3',np.min(im3),np.max(im3),im3.shape,im3.dtype)
imageio.imwrite('gradient10bit-leftbitreplication.png',im3)

同时附上纯ffmpeg/ImageMagick的操作命令:

  • 编码命令:
ffmpeg -y -i gradient10bit-scaledto16bits.png -c:v libx265 -x265-params lossless=1 -pix_fmt gray10be gradient10bit-scaledto16bits.mkv
  • 解码回PNG命令:
ffmpeg -y -i gradient10bit-scaledto16bits.mkv -pix_fmt gray16be recons-gradient10bit-scaledto16bits.png
  • 对比命令:
magick compare -verbose -metric mae gradient10bit-scaledto16bits.png recons-gradient10bit-scaledto16bits.png diff-scaledto16bits.png

内容的提问来源于stack exchange,提问作者Tom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 22:36:02