Giter Club home page Giter Club logo

jpeglib's Introduction

PyPI version Commit CI/CD Release CI/CD Documentation Status PyPI downloads Stars Contributors Wheel Status PyPi license Last commit

jpeglib

Python envelope for the popular C library libjpeg for handling JPEG files.

libjpeg offers full control over compression and decompression and exposes DCT coefficients and quantization tables.

Installation

Simply install the package with pip3

pip install jpeglib

or using the cloned repository

python setup.py install

⚠️ This will install jpeglib together with multiple versions of libjpeg, libjpeg-turbo and mozjpeg. For common architectures/OS we provide prebuilt wheels, but installing from source takes couple of minutes.

Usage

Import the library in Python 3

import jpeglib

DCT

Get discrete cosine transform (DCT) coefficients and quantization matrices as numpy array

im = jpeglib.read_dct('input.jpeg')
im.Y; im.Cb; im.Cr; im.qt

You get luminance DCT, chrominance DCT and quantization tables.

Write the DCT coefficients back to a file with

im.write_dct('output.jpeg')

Pixel data

Decompress the input.jpeg into spatial representation in numpy array with

im = jpeglib.read_spatial('input.jpeg')
im.spatial

You can specify parameters such as output color space, DCT method, dithering, etc.

Write spatial representation in numpy arrray back to file with

im.write_spatial('output.jpeg')

You can specify input color space, DCT method, sampling factor, output quality, smoothing factor etc.

You can find all the details in the documentation.

libjpeg version

It is possible to choose, which version of libjpeg should be used.

jpeglib.version.set('6b')

Currently jpeglib supports all versions of libjpeg from 6b to 9e, libjpeg-turbo 2.1.0 and mozjpeg 4.0.3. Their source codes is baked inside the package and thus distributed with it, avoiding external dependency.

Get currently used libjpeg version by

version = jpeglib.version.get()

You can also set a libjpeg version for a scope only.

jpeglib.version.set('6b')
im = jpeglib.read_spatial('image.jpeg') # using 6b
with jpeglib.version('9e'):
    im = jpeglib.read_spatial('image.jpeg') # using 9e
im = jpeglib.read_spatial('image.jpeg') # using 6b again

Credits

Developed by Martin Benes, University of Innsbruck, 2023.

jpeglib's People

Contributors

alxshine avatar btlorch avatar licy183 avatar martinbenes1996 avatar norah2004 avatar sparks-pion avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

jpeglib's Issues

write_dct and write_spatial leak file handles

Hi there Martin, thanks a lot for this library - I have a project in which i have to manipulate JPEG DCTs directly and thought i was going to have to do something really low-level myself 'til i found it :-)

I've noticed a slight problem however, on my machine (Mac OSX 11.6.8, python 3.68, jpeglib 0.12.5, libjpeg 9e) it looks like the write_dct and write_spatial methods are leaking file descriptors, the following method (for example):

def decay_jpeg_dct_y_absolute(filename):
    try:
        print("decaying dct luminance")
        im = jpeglib.read_dct(filename)
        original = im.Y
        min = np.min(im.Y)
        max = np.max(im.Y)
        perturbed = perturb_array(original, min=min, max=max)
        im.Y = perturbed
        im.write_dct()
        im.close()
    except OSError:
        pass

...will leave a reference to filename open (verified with lsof) - this only happens when write_dct is called, so it doesn't appear to be related to the file reading. write_spatial also exhibits the same behaviour.

I've had a look into the code with a view to opening a PR, but the actual file writing seems to be in C++ land which is far beyond my abilities i'm afraid!

Any chance of a fix, or a pointer about what i might be doing wrong?

Many thanks!

Tim

Writing custom quantization table leads to segfault with libjpeg 9e

While saving a JPEG image with a custom quantization table and libjpeg 9e, the program exits with a segfault. For other libjpeg version, the same code seems to be working fine. Please find a minimal working example below.

import jpeglib
import numpy as np

# Create a random 8x8 image
filepath = "/tmp/demo.jpg"
rng = np.random.default_rng(12345)
spatial_block = rng.integers(low=0, high=256, size=(8, 8))
assert spatial_block.min() >= 0
assert spatial_block.max() <= 255

# Create a jpeglib object
im = jpeglib.from_spatial(spatial_block[:, :, None].astype(np.uint8))

# Create a custom quantization table
qt = np.ones((8, 8))

# Using version "6b" or "mozjpeg403" works, but "9e" exits with code 139 (interrupted by signal 11: SIGSEGV)
with jpeglib.version("9e"):
    im.write_spatial(filepath, qt=qt)

Swapped sampling factors

I am confused about the samp_factor property. im.samp_factor gives a 3x2 array. Intuitively, I would expect that the three rows correspond to the three channels and the two columns represent the horizontal and vertical directions. I wonder whether the horizontal and vertical directions were swapped accidentally.

Example:

  1. Compress an image using the cjpeg command line tool. With -sample 2x1, chroma pixels are 2 pixels wide and 1 pixel tall (horizontal subsampling only, compare this great blog post).
cjpeg -sample 2x1 -quality 75 testimg.ppm > /tmp/testimg.jpg
  1. Use libjpeg to retrieve and print the sampling factors.

The following code is based on libjpeg's example.c.

#include <stdio.h>
#include <stdlib.h>
#include "jpeglib.h"
#include <setjmp.h>


struct my_error_mgr {
  struct jpeg_error_mgr pub;	/* "public" fields */

  jmp_buf setjmp_buffer;	/* for return to caller */
};

typedef struct my_error_mgr * my_error_ptr;


METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
  /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
  my_error_ptr myerr = (my_error_ptr) cinfo->err;

  /* Always display the message. */
  /* We could postpone this until after returning, if we chose. */
  (*cinfo->err->output_message) (cinfo);

  /* Return control to the setjmp point */
  longjmp(myerr->setjmp_buffer, 1);
}


GLOBAL(int) read_JPEG_file (char * filename)
{
  struct jpeg_decompress_struct cinfo;
  struct my_error_mgr jerr;
  FILE * infile;

  if ((infile = fopen(filename, "rb")) == NULL) {
    fprintf(stderr, "can't open %s\n", filename);
    return 0;
  }
  cinfo.err = jpeg_std_error(&jerr.pub);
  jerr.pub.error_exit = my_error_exit;
  if (setjmp(jerr.setjmp_buffer)) {
    jpeg_destroy_decompress(&cinfo);
    fclose(infile);
    return 0;
  }

  jpeg_create_decompress(&cinfo);

  jpeg_stdio_src(&cinfo, infile);

  (void) jpeg_read_header(&cinfo, TRUE);

  for (unsigned int ci=0; ci < 3; ci++) {
    printf("Channel %u: h_samp_factor = %d, v_samp_factor = %d\n",
      ci,
      cinfo.comp_info[ci].h_samp_factor,
      cinfo.comp_info[ci].v_samp_factor);
  }

  fclose(infile);

  return 0;
}


int main(int argc, char* argv[]) {
  read_JPEG_file("/tmp/testimg.jpg");

  return 0;
}

Compile and run

# This assumes you are the libjpeg source directory and compiled libjpeg to ./build
gcc -g -O2 -o example example.c -I`pwd`/build/include -L`pwd`/build/lib -ljpeg
LD_LIBRARY_PATH=`pwd`/build/lib ./example

This gives the expected output:

Channel 0: h_samp_factor = 2, v_samp_factor = 1
Channel 1: h_samp_factor = 1, v_samp_factor = 1
Channel 2: h_samp_factor = 1, v_samp_factor = 1
  1. Conversely, jpeglib reports the values in different order.
import jpeglib
im = jpeglib.read_spatial("/tmp/testimg.jpg")
print(im.samp_factor)

Output:

[[1 2]
 [1 1]
 [1 1]]

Since the default order to specify the sampling factors is h x v, I would have expected the jpeglib output in the same order.

It would be great if you can double-check whether the current behavior is intended. In this case, please consider updating the documentation.

Quantization table mismatch for quality factor 0

This is probably a hardly relevant edge case, but nevertheless it may be surprising that jpeglib deviates from the command line tool cjpeg.

For quality factor 0 (and below), the quantization tables produced by jpeglib and the command line tool cjpeg differ. jpeglib defaults to the quantization table corresponding to QF75, while cjpeg produces the same quantization table as for QF1.

import jpeglib
import numpy as np

# Create a random 8x8 image
filepath = "/tmp/demo.jpg"
rng = np.random.default_rng(12345)
spatial_block = rng.integers(low=0, high=256, size=(8, 8)).astype(np.uint8)

# Create a jpeglib object
im = jpeglib.from_spatial(spatial_block[:, :, None])

# Write with JPEG quality 0
im.write_spatial(filepath, qt=0)

im = jpeglib.read_dct(filepath)
print(im.qt[0])
# Quantization table produced by jpeglib
[[ 8  6  5  8 12 20 26 31]
 [ 6  6  7 10 13 29 30 28]
 [ 7  7  8 12 20 29 35 28]
 [ 7  9 11 15 26 44 40 31]
 [ 9 11 19 28 34 55 52 39]
 [12 18 28 32 41 52 57 46]
 [25 32 39 44 52 61 60 51]
 [36 46 48 49 56 50 52 50]]

# Quantization table produced by cjpeg
800  550  500  800 1200 2000 2550 3050
600  600  700  950 1300 2900 3000 2750
700  650  800 1200 2000 2850 3450 2800
700  850 1100 1450 2550 4350 4000 3100
900 1100 1850 2800 3400 5450 5150 3850
1200 1750 2750 3200 4050 5200 5650 4600
2450 3200 3900 4350 5150 6050 6000 5050
3600 4600 4750 4900 5600 5000 5150 4950

Installing from source leaves .so files in root directory

When installing the package from source with pip install -e . (in a venv), the generated .so files for the different libjpeg versions end up in the root directory instead of libjpeg/cjpeglib.

This occurs on different machines:

  • my work laptop with Fedora 37 and Python 3.11.4
  • a testing server (you know which one) running Ubuntu 20.04 and python 3.8.10

Moving the files manually fixes the issue on both machines

Verify dtype in `jpeglib.from_spatial`

Although the docstring of jpeglib.from_spatial suggests that the input dtype should be uint8, jpeglib allows constructing objects from other dtypes. This can have undesired effects. For example, writing and loading such an array results in a completely different image. Please find a minimal working example below. A fix could be to raise an error for unexpected dtypes.

# Create random 8x8 integer block
filepath = "/tmp/demo.jpg"
rng = np.random.default_rng(12345)
spatial_block = rng.integers(low=0, high=256, size=(8, 8))
assert spatial_block.min() >= 0
assert spatial_block.max() <= 255

print("Original block")
print(spatial_block)
print()

# Create a jpeglib object. Note that the input dtype is int64.
im = jpeglib.from_spatial(spatial_block[:, :, None].astype(int))
print("im.spatial")
print(im.spatial[:, :, 0])
print()

# Write and load jpeglib object
im.write_spatial(filepath, qt=100)
spatial_recovered = jpeglib.read_spatial(filepath).spatial[:, :, 0]
print("spatial recovered")
print(spatial_recovered)
print()

# Create an jpeglib object with uint8 input array, write, and reload
jpeglib.from_spatial(spatial_block[:, :, None].astype(np.uint8)).write_spatial(filepath, qt=100)
spatial_uint8_recovered = jpeglib.read_spatial(filepath).spatial[:, :, 0]
print("spatial uint8 recovered")
print(spatial_uint8_recovered)

Output

Original block
[[178  58 201  81  52 204 164 173]
 [253 100 214  85 145 153  54  47]
 [ 58 172 157 241 180  63 234 242]
 [187 170  33  24  68 113  18 226]
 [121 178  54  83  29 187 198  56]
 [183  20 100  40 190  87 121 119]
 [121  68 142 208 127  49   6  33]
 [ 20  23  31 153 206 218 167 154]]

im.spatial
[[178  58 201  81  52 204 164 173]
 [253 100 214  85 145 153  54  47]
 [ 58 172 157 241 180  63 234 242]
 [187 170  33  24  68 113  18 226]
 [121 178  54  83  29 187 198  56]
 [183  20 100  40 190  87 121 119]
 [121  68 142 208 127  49   6  33]
 [ 20  23  31 153 206 218 167 154]]

spatial recovered
[[178   0   0   0   0   0   0   0]
 [ 58   0   1   0   0   0   0   0]
 [201   0   0   1   0   0   0   0]
 [ 81   0   0   0   0   0   0   0]
 [ 52   0   0   0   0   0   0   0]
 [205   0   0   0   1   0   0   0]
 [164   0   0   0   0   0   0   0]
 [173   0   0   0   0   1   0   0]]

spatial uint8 recovered
[[178  58 201  81  52 204 164 174]
 [253 100 214  85 145 153  54  47]
 [ 58 173 157 241 180  63 234 242]
 [187 170  33  24  68 113  18 226]
 [121 178  54  83  29 187 199  56]
 [183  20 100  40 190  87 121 118]
 [121  68 142 208 127  49   6  33]
 [ 20  23  32 153 207 218 167 154]]

"Bogus virtual array access" on save after from_dct with some picture dimensions and 4:2:0 chroma subsampling

Hello !
First of all, thanks a lot for this usefull library that I use for my experiments.

As said in the title, I have issues with some picture dimensions and specific chroma subsampling formats. For exemple, a 800x600 picture with 4:2:0 subsampling produce a YCbCr DCT blocks array with shape ((75, 100), (38, 50), (38, 50)). But at saving, the samp_factor function in _infere.py produce following factor array:

array([[1.97368421, 2.        ],
       [1.        , 1.        ],
       [1.        , 1.        ]])

Which is truncated to int16 (causing the first value to be rounded down to 1 rather than 2). This cause an error "Bogus virtual array access" at generated DCTJPEG object saving. I made an attempt to fix this issue by adding a np.ceil function call in samp_factor().

factor = np.ceil(np.array([
    max_subs,
    *(max_subs / dims)
])).astype('int16')

See my fork where I commit the proposed patch https://github.com/Masstock/jpeglib.
I hope I did well, thanks !

Integer QF in jpeglib.from_dct

According to the docstring, jpeglib.from_dct assumes the quantization table to be either None or to be an ndarray. The example from the documentation suggests that integer quality factors are also possible, but the example leads to an error.

Y = np.random.randint(-127, 127,(2,2,8,8),dtype=np.int16)
Cb = np.random.randint(-127, 127,(1,1,8,8),dtype=np.int16)
Cr = np.random.randint(-127, 127,(1,1,8,8),dtype=np.int16)
im = jpeglib.from_dct(Y, Cb, Cr, qt=75) # chrominance -> YCbCr infered

When only the luminance channel is given, then the jpeglib object is created successfully, but writing to a file fails.

Y = np.random.randint(-127, 127, (2, 2, 8, 8), dtype=np.int16)
im = jpeglib.from_dct(Y, qt=75)
im.write_dct("/tmp/tmp.jpg")

A simple solution would be to disallow integer quality factors. Another solution would be to retrieve the quantization tables for the given quality factor.

Error Handler

Thanks for such a great job. But when I use this library, I find something strange.

import jpeglib
jpeglib.read_dct("xxx.bmp")

Codes above will make python exit. This is not intended I suppose.

Seems that the wrapper of libjpeg doesn't oveeride the default exit handler, which will exit(EXIT_FAILURE) by default.

//load image
cinfo->err = jpeg_std_error(jerr);
jpeg_create_decompress(cinfo);
jpeg_stdio_src(cinfo, fp);

METHODDEF(void)
error_exit (j_common_ptr cinfo)
{
/* Always display the message */
(*cinfo->err->output_message) (cinfo);
/* Let the memory manager delete any temp files before we die */
jpeg_destroy(cinfo);
exit(EXIT_FAILURE);
}

But when I try to use setjmp/longjmp to handle the error, it causes Segmentation Fault on Linux. Any ideas?

#include <setjmp.h>

static jmp_buf __error_exit_jmp_buf;

METHODDEF(void)
my_custom_error_exit (j_common_ptr cinfo) {
  /* Write the message */
  (*cinfo->err->output_message) (cinfo);

  /* Let the memory manager delete any temp files before we die */
  jpeg_destroy(cinfo);

  /* Return control to the setjmp point */
  longjmp(__error_exit_jmp_buf, 1);
}

...

  //load image
  cinfo->err = jpeg_std_error(jerr);
  cinfo->err->error_exit = my_custom_error_exit;

  if (setjmp(__error_exit_jmp_buf)) {
    return NULL;
  }

  jpeg_create_decompress(cinfo);
  jpeg_stdio_src(cinfo, fp);

Trellis quantization of DC coefficients is switched off

Thanks for adding support for MozJPEG and all your efforts with the library. When compressing an image with MozJPEG via jpeglib, the output differs from the MozJPEG command line tool cjpeg. The reason seems to be that Trellis quantization of the DC coefficients is switched off. Surprisingly, adding the flag TRELLIS_QUANT_DC still doesn't enable Trellis quantization of the DC coefficients. The images of cjpeg and jpeglib only match when cjpeg is called with the argument -notrellis-dc. You can verify this using the example script below.

import tempfile
from PIL import Image
import jpeglib
import numpy as np
from imageops.jpegops.cjpeg_encoder import CjpegEncoder
import os


def assert_identical(filepath_a, filepath_b):
    """
    Compare to JPEG images and raise an assertion error when they do not match.
    :param filepath_a: path to first JPEG file
    :param filepath_b: path to second JPEG file
    """
    
    im_a = jpeglib.read_dct(filepath_a)
    im_b = jpeglib.read_dct(filepath_b)

    # Compare quantization tables
    assert np.all(im_a.qt == im_b.qt)

    # Compare chroma subsampling factors
    assert np.all(im_a.samp_factor == im_b.samp_factor)

    # Compare image shapes
    assert np.all(im_a.Y.shape == im_b.Y.shape)

    # Compare luminance channel
    assert np.all(im_a.Y == im_b.Y)

    # Compare presence of chroma channels
    assert im_a.has_chrominance == im_b.has_chrominance

    if im_a.has_chrominance:
        # Compare Cb channel
        assert np.all(im_a.Cb == im_b.Cb)

        # Compare Cr channel
        assert np.all(im_a.Cr == im_b.Cr)


# Set up mozjpeg encoder based on the command line tool cjpeg
mozjpeg_dir = "~/local/jpeg/mozjpeg-4.0.3/build"
mozjpeg_encoder = CjpegEncoder(
    cjpeg_executable=os.path.join(mozjpeg_dir, "cjpeg"),
    lib_path=mozjpeg_dir,
)

# Input path
uncompressed_filepath = "~/data/alaska2/ALASKA_v2_TIFF_512_COLOR/00001.tif"
# Output path
compressed_filepath = "/tmp/00001.jpg"
# JPEG quality
qf = 90

# Compress with jpeglib
im = np.array(Image.open(uncompressed_filepath))
with jpeglib.version("mozjpeg403"):
    im = jpeglib.from_spatial(im)
    im.write_spatial(compressed_filepath, qt=qf)

# Compress via cjpeg
with tempfile.NamedTemporaryFile(suffix=".jpg") as f:
    img = np.array(Image.open(uncompressed_filepath))
    mozjpeg_encoder.img_cjpeg(img, output_filename=f.name, quality=qf, cjpeg_args=("-sample", "2x2", "-notrellis-dc"))

    # Compare the two files
    assert_identical(compressed_filepath, f.name)

install_test_dependencies reruns pip install

the only thing that the install_dependencies action does is upgrade pip and install the requirements.txt

The first line in install_test_dependencies repeats this process. Is this on purpose/required?

IndexError: list index out of range

Thank the author for your work, but when I import jpeglib according to the tutorial, there seems to be a problem. I check that this is not a compilation environment problem. I want to see when you are free. Can you answer this question.Thank you!

图片
code:
图片

During handling of the above exception, another exception occurred:

图片

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.