cgohlke / imagecodecs

Image transformation, compression, and decompression codecs
https://pypi.org/project/imagecodecs
BSD 3-Clause "New" or "Revised" License
111 stars 21 forks source link

The level parameter marginally affects output size and quality in jpegxl_encode #58

Closed rfezzani closed 1 year ago

rfezzani commented 1 year ago

I performed few experiments trying to understand the effect of the different parameters of jpegxl_encode, and I found that only the distance parameter affects the output size and the image quality.

Generally, in other formats (JPEG, JPEGXR, JPEG2000, AVIF), the level parameter is used to set compression quality and size. What about making level parameter in jpegxl_encode depend on the actual distance parameter for consistency?

Here are the results of my experiments:

Evaluation

Using two test images from skimage (cat and astronaut), I measured 4 quantities: output size, Normalized root MSE (NMSE) and finally encoding and decoding time:

from tqdm import tqdm, trange
import numpy as np
from itertools import product
from imagecodecs import jpegxl_encode, jpegxl_decode
from skimage.metrics import normalized_root_mse
from skimage import data
from time import time

time_rep = 10
level = range(5)
distance = np.arange(0., 3.1, 0.5)
effort = range(3, 10)

measures = {}

fname = "results/jxl.json"

for func in tqdm([data.cat, data.astronaut]):
    results = {}
    img0 = func()

    for lev, dist, eff in tqdm(list(product(level, distance, effort)), leave=False):
        tqdm.write(
            f"\nlevel: {lev} - distance: {dist:.01f} - effort: {eff}")
        t = 0
        for _ in trange(time_rep, leave=False):
            t0 = time()
            buf = jpegxl_encode(img0, level=lev, effort=eff,
                                distance=dist, lossless=0)
            t1 = time()
            t += t1 - t0

        t /= time_rep

        t_dec = 0
        for _ in trange(time_rep, leave=False):
            t0 = time()
            jpegxl_decode(buf)
            t1 = time()
            t_dec += t1 - t0

        t_dec /= time_rep

        size = len(buf) * 1e-3

        err = normalized_root_mse(img0, jpegxl_decode(buf))

        tqdm.write(f"\tEnc Time: {t:.04f}sec -- "
                        f"Dec Time: {t_dec:.04f}sec -- " +
                        f"Size: {size:.02f}Kb -- NMSE: {err:.02e}")

        results[f"{lev}, {dist}, {eff}"] = {"enc_time": t, "dec_time": t_dec,
                                            "nmse": err, "size": size}
    measures[func.__name__] = results

Results

Measures on both test images are consistent:

Effort parameter influence

NMSE and output size are marginally affected by the effort parameter

effort

Level parameter influence

effort is set to 3 Again, NMSE and output size are marginally affected by the level parameter

level

distance parameter influence

effort is set to 3 and level to 4

NMSE and output size are controled by the distance parameter

distance

cgohlke commented 1 year ago

I haven't looked in detail, but the level parameter does map to distance in the latest version of imagecodecs. The mapping function is taken from the libjxl source code . See also discussion at #54.

https://github.com/cgohlke/imagecodecs/blob/906a0d58aca1e74b4cd5e9a41a8a9cc53e66b717/imagecodecs/_jpegxl.pyx#L193-L206

cgohlke commented 1 year ago

It looks like you are using an outdated version of imagecodecs.

rfezzani commented 1 year ago

You are right, I am running the penultimate version :sweat_smile:

>>> import imagecodecs
>>> print(imagecodecs.__version__)
2022.9.26
rfezzani commented 1 year ago

Thank you again @cgohlke! After reading the code lines you pointed me, the distance needs to be None for level to affect the compression quality. Here are the corresponding measure curves if you are interested: effort_1 level_1 It is interesting to see how decoding speeds up when level > 60