libheif
libheif copied to clipboard
Lossless encoding/decoding of 10bit grayscale image stored as 16 bit pngs
Hello,
Apologies if this is not the right place to ask questions but I couldn't find a forum or a Discussions tab.
I am trying to encode 10 bit grayscale images losslessly in heic. The images are stored as 16 bit png files (but only use the lest significant 10 bits).
If I simply use
heif-enc -b 10 -L -v -o output-10bit-lsb.heic input-10bit-lsb.png
the resulting heic image looks completely dark (using Preview.app on macos 12.0.1) so I guess heif-enc
does not only use the 10 least significant bits. If I scale / bitshift or left replicate the original intensities in the input png, the generated heic file looks ok on preview.app but I can't get back the original values.
If I simply use
heif-convert output-10bit-msb.heic recons-10bit-msb.png
the resulting png is an 8 bit rgb file. Using imagemagick seems to help there as I can generate 16bit images with
magick convert output-10bit-msb.heic -depth 10 recons-10bit-msb.png
However, the result is not lossless. Is there a way to do such a lossless round-trip with libheif command-line tools?
To ease replicating my issue, below if a simple python script that generates the 10bit image on the fly:
import subprocess
import numpy as np
import tempfile
import imageio
# Create simple image with gradient from
# 0 to (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)
# Tile it to be at least 64 pix as x265 encoder may only work
# with image of size 64 and up
im0 = np.tile(im0, (2, 2))
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)
# bitshift it or rescale intensities
im0ref = im0
im0 = (im0<<6) # Bitshift the values to use most significant bits
#im0 = (im0<<6) + (im0>>4) Left bit replication as a cost-effective approximation of scaling (See http://www.libpng.org/pub/png/spec/1.1/PNG-Encoders.html)
#im0 = np.uint16(np.round(im0 * np.float64((1<<16)-1)/np.float64((1<<10)-1))) # Scale the values use all 16 bits
print('im0',np.min(im0),np.max(im0),im0.shape,im0.dtype)
# Save it
tmp0 = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
print(f'Using tmp file: {tmp0.name}')
imageio.imwrite(tmp0.name,im0)
# Encode with heif-enc
tmp1 = tempfile.NamedTemporaryFile(suffix='.heic', delete=False)
mycmd = f'heif-enc -b 10 -L -v -o {tmp1.name} {tmp0.name}'
print(mycmd)
p = subprocess.run(mycmd.split(), capture_output=True)
print( 'stdout:', p.stdout.decode() )
print( 'stderr:', p.stderr.decode() )
if p.returncode:
exit(p.returncode)
# Decode with heif-convert or imagemagick
tmp2 = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
mycmd = f'magick convert {tmp1.name} -depth 10 {tmp2.name}'
#mycmd = f'heif-convert {tmp1.name} {tmp2.name}'
print(mycmd)
p = subprocess.run(mycmd.split(), capture_output=True)
print( 'stdout:', p.stdout.decode() )
print( 'stderr:', p.stderr.decode() )
if p.returncode:
exit(p.returncode)
# Read back
im1 = imageio.imread(tmp2.name)
print('im1',np.min(im1),np.max(im1),im1.shape,im1.dtype)
# Bitshift or scale back
im1pre = im1
im1 = (im1>>6)
#im1 = np.uint16(np.round(im1 * np.float64((1<<10)-1)/np.float64((1<<16)-1)))
print('err: ',np.linalg.norm((np.float32(im1)-np.float32(im0ref)).ravel()))
Note that my question is very similar to that I have about doing this with ffmpeg: https://stackoverflow.com/q/69739665/17261462 In the case of ffmpeg, achieving a lossless roundrip with HEVC is at least possible by using temporary rawvideo file: https://stackoverflow.com/a/69874453/17261462
For ease of use, I am also attaching some sample pngs generated from teh python commands above.
LSB version:
MSB version:
LBR version: