使用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
相关产品推荐
相关产品推荐

