libheif icon indicating copy to clipboard operation
libheif copied to clipboard

Lossless encoding/decoding of 10bit grayscale image stored as 16 bit pngs

Open tvercaut opened this issue 3 years ago • 0 comments

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: gradient10bit-lsb

MSB version: gradient10bit-msb

LBR version: gradient10bit-leftbitreplication

tvercaut avatar Nov 08 '21 10:11 tvercaut