0

I am working with 12-bit integer image data (i.e. data aquired from a camera that uses a 12-bit ADC) which I ultimately store in a Zarr array. Currently I store the images as 16-bit integers, which means I am wasting 30% extra memory. I would like to avoid this extra memory use by storing the images using a "12-bit packed format", i.e. by storing two pixel values in 3 bytes. See the description of MONO12_PACKED for a complete description of this format.

The most convenient way to do this would be if zarr/numcodecs had a compressor for 16-bit integer data which converted it to this 12-bit packed format. Then, assuming my image data consists of nt time points and each image is of size ny x nx, I would run something like

import zarr

img = load() # load image of size nt x ny x nx

z = zarr("test.zarr", "w")
z.array("images", img, chunks=(1, ny, nx), dtype="uint16", compressor=mono12_packed)

However, currently numcodecs does not appear to implement such a compressor. A quick look at their packbits compressor suggests it would be easy to implement a 12-bit packing compressor. However, the downside of writing this myself is I would then need to distribute that code together with my zarr file for someone else to be able to read it.

After some experimenting, creating a mono12 packed codec is doable (see below).

I am working with datasets which are ~90GB when stored as uint16. When saving these to hard disk I am IO bound, and using compression speeds up the saving.

I find that the below implementation of mono12 compresses the data by the expected factor of 4:3 and runs ~20% faster. I find that zlib compresses ~2:1 and runs ~25% faster and bz2 compresses ~3:1 and runs ~2x slower.

However, none of this directly addresses my question, which is: is there an existing implementation of the mono12 compressor which is compatible with zarr/numcodecs? Or if not, is this kind of compressor general enough to be included in numcodecs (since it only works on a very specific subclass of 16-bit integers)?

import numpy as np
import zarr
from numcodecs.abc import Codec
from numcodecs.compat import ensure_ndarray, ndarray_copy
from numcodecs.registry import register_codec

def pack12(data):
    """
    Convert data from uint16 integers to 12-bit packed format

    :param data: even sized array of uint16
    :return bytes:
    """

    # most significant 8 bits of first integer
    first_bit = (data[::2] >> 4).astype(np.uint8)
    # least significant 4 bits of first integer and most significant 4 bits of second
    second_bit = ((np.bitwise_and(15, data[::2]) << 4) + (data[1::2] >> 8)).astype(np.uint8)
    # least significant 8 bits of second integer
    third_bit = np.bitwise_and(255, data[1::2]).astype(np.uint8)

    return np.stack((first_bit, second_bit, third_bit), axis=1).ravel()

def unpack12(data: np.ndarray) -> np.ndarray:
    """
    Convert data from 12-bit packed integers to uint16 integers

    :param data: an array of uint8 integers
    :return img: an array of uint16 integers
    """

    # convert from 12L packed (two 12-bit pixels packed into three bytes)
    a_uint8 = data[::3].astype(np.uint16)
    b_uint8 = data[1::3].astype(np.uint16)
    c_uint8 = data[2::3].astype(np.uint16)

    # middle byte contains least-significant bits of first integer
    # and most-significant bits of second integer
    first_int = (a_uint8 << 4) + (b_uint8 >> 4)
    second_int = (np.bitwise_and(15, b_uint8) << 8) + c_uint8

    img = np.stack((first_int, second_int), axis=1).ravel()

    return img

class mono12(Codec):
    codec_id = 'mono12'

    def init__(self):
        pass

    def encode(self, buf):
        # normalise input
        arr = ensure_ndarray(buf)

        # flatten to simplify implementation
        arr = arr.reshape(-1, order='A')

        # determine size of packed data
        n = arr.size
        n_bytes_packed = (n // 2) * 3
        n_ints_leftover = n % 2
        if n_ints_leftover > 0:
            n_bytes_packed += 3

        # setup output
        enc = np.empty(n_bytes_packed + 1, dtype='u1')

        # store how many bits were padded
        if n_ints_leftover > 0:
            n_ints_padded = 1
            # apply encoding
            enc[1:] = pack12(np.concatenate((arr, np.array([0], dtype=np.uint16))))
        else:
            n_ints_padded = 0
            # apply encoding
            enc[1:] = pack12(arr)
        enc[0] = n_ints_padded

        return enc

    def decode(self, buf, out=None):
        # normalise input
        enc = ensure_ndarray(buf).view('u1')

        # flatten to simplify implementation
        enc = enc.reshape(-1, order='A')

        # find out how many integers were padded
        n_ints_padded = int(enc[0])

        # apply decoding
        dec = unpack12(enc[1:])

        # remove padded bits
        if n_ints_padded:
            dec = dec[:-n_ints_padded]

        # handle destination
        return ndarray_copy(dec, out)

# need to register codec if want to load data later
register_codec(mono12)

img = load() # load image of size nt x ny x nx

z = zarr("test.zarr", "w")
z.array("images", img, chunks=(1, ny, nx), dtype="uint16", compressor=mono12)

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
ptbrown
  • 101
  • 2
  • 10
  • 2
    Just to give you something to think about, there's a tradeoff here. If you read and write these images more often then you do analysis, then what you're doing is a net loss, because you'll pay the price of encoding and decoding. – Tim Roberts Jul 27 '23 at 22:54
  • 1
    HOWEVER, `numcodecs` supports zllib and bz2, which would provide better than the 3:2 compression you'd be getting. As long as you always use your programs, there's no need to have the files be readable, right? – Tim Roberts Jul 27 '23 at 23:03
  • @TimRoberts those could be reasonable options as long as they are lossless. I will take a look at them. The advantage of 12-bit packing is it is very fast to compute and well known by the imaging community. – ptbrown Jul 27 '23 at 23:17
  • @TimRoberts I'm not sure if I'm interpreting your last comment correctly. It would be best if the image files are readable by others without access to my code (e.g. when sharing data with collaborators or posting data publicly for a paper) – ptbrown Jul 27 '23 at 23:20
  • Well, what about PNG? It supports 16-bit grayscale and lossless zip compression, and is a worldwide standard. – Tim Roberts Jul 27 '23 at 23:23
  • @TimRoberts for the community I'm working in, I would prefer to stick to zarr (or tiff, which is the defacto community standard). I usually work with 4D or 5D datasets and zarr has good support for nD data. Furthermore most of the image viewing and other software tools I work with (imagej, napari, dask) have better support for zarr and tif. Using zlib or bz2 would be much more attractive than switching file formats – ptbrown Jul 27 '23 at 23:33
  • TIFF also supports zip and LZW. – Tim Roberts Jul 27 '23 at 23:37
  • @TimRoberts I appreciate your input, but if we are going to discuss why I am using zarr and not another format then there are more details I would need to include in the post. Vanilla tiff does not support files larger than 4GB, the metadata is not as flexible as zarr, it does not provide great support for nD data. That said there are some extensions of tiff files which solve some f these issues (e.g. ome tiff, BigTiff, NDTiff). Suffice it to say that my goal is to improve the compression with the existing format – ptbrown Jul 27 '23 at 23:50
  • 1
    @mkrieger1 I would say it is some additional context. But the main question has not been addressed: "is there an existing implementation of mono12 codec compatible with zarr/numcodecs?" I am happy to take advice on how/whether to include this "update" here. – ptbrown Jul 28 '23 at 20:38

0 Answers0