Bad conversion to jpg from 16UC1 image or jpg compression artifacts?

I use 16UC1 images in the context of depth cameras that produce a monochrome depth image, where typically an intensity of 1 represents a distance of 1 mm.

This small script generates a correct PNG image but incorrect JPG with the same source.

import cv2

import numpy as np

# Create a 100x100 image with uint16 type.
width = 100
height = 100
image = np.empty((height, width), dtype=np.uint16)

# Define some intensities.
intensities = [65535, 32767, 127, 0]

# Fill the regions with the corresponding values.
n_regions = 2 * (len(intensities) - 1) + 1
for i, intensity in enumerate(intensities):
    h_margin = round(i * width / n_regions)
    w_margin = round(i * height / n_regions)
    image[h_margin:height-h_margin, w_margin:width-w_margin] = intensity

cv2.imwrite('output_image.png', image)
cv2.imwrite('output_image.jpg', image)

The correct image are 4 squares from white (large square, intensity 65535) to black (smallest square, intensity 0).

When I export the PNG to JPEG from Gimp, the result is correct.

Is it a bad conversion to JPEG from 16UC1 image or JPEG compression artifacts?

are 16-bit per channel JPEG files even a thing? that would be news to me.

can you show us the artefacts you speak of?

JPEG

PNG

I have no clew about the JPEG format but imwrite should either transform the data or refuse to convert them if the conversion does not make sense.

If you apply a margin, rather apply a symmetric formula:

I[m:height-2*m, m:width-2*m]

no, that was correct.

I see nothing wrong here.

JPEG is a lossy format. did you know that?

For me, JPEG is able to represent this since I can convert from the PNG and get “normal” results.

So, according to you @crackwitz, the wrong result is due to JPEG’s lossy format?

as far as I know, there’s something called Losless JPEG and that’s the only jpeg that supports 16BIT

If i try it with C, I get a [ WARN:0@0.025] global loadsave.cpp:848 cv::imwrite_ Unsupported depth image for selected encoder is fallbacked to CV_8U. warning when saving it to PNG and no warning when saving it to JPEG. But in the end, both are 8Bit

but check the documentation


* With JPEG 2000 encoder, 8-bit unsigned (CV_8U) and 16-bit unsigned (CV_16U) images can be saved.

* With JPEG XL encoder, 8-bit unsigned (CV_8U), 16-bit unsigned (CV_16U) and 32-bit float(CV_32F) images can be saved.

//Edit: I did a mistake with the file format, this happens when using CV_16FC1 instead of CV_16U. Now the correction: With CV_16U, PNG get saved as 16Bit and I get this warning for writing JPG instead of PNG.

//Edit2: I think GIMP is simply norming the values before saving it to a 8Bit JPEG. I don’t have python but what happens if you divide your image by 256 before saving it to JPEG, does it look correct to you?

If I save the JPG image with cv2.imwrite('output_image.jpg', (image / 256).astype(np.uint8)), then the image is correct (even without .astype(np.uint8)).

I did some further tests with Gimp and MATLAB. Matlab can save 16-bit jpegs, its help says

'BitDepth'     A scalar value indicating desired bitdepth;
                   for grayscale images this can be 8, 12, or 16;
                   for truecolor images this can be 8 or 12.  Only
                   lossless mode is supported for 16-bit images.

But only Matlab can read these 16-Bit JPEGs. Gimp, IrfanView, Windows Paint, Paint.NET and OpenCV failed to read this file.

If you load a 16Bit-Mono-PNG to Gimp and save it as JPEG, your saved jpeg file will be 8Bit.

So if you need a precision of 16Bit, then better stay with PNG, it can keep the information and compared with JPEG you don’t have annoying artefacts. But don’t forget to set the IMREAD_ANYDEPTH parameter for imread, when trying to read them, or they will be 8-Bit again