6

I am moving some image processing functionality from .NET to Python under the constraint that the output images must be compressed in the exact same way as they were in .NET. However, when I compare the .jpg output files on a tool like text-compare and choose Ignore nothing, there are significant differences in how the files were compressed.

For example:

Python

bmp = PIL.Image.open('marbles.bmp')

bmp.save(
    'output_python.jpg',
    format='jpeg',
    dpi=(300,300),
    subsampling=2,
    quality=75
)

.NET

ImageCodecInfo jgpEncoder = ImageCodecInfo.GetImageDecoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
EncoderParameters myEncoderParameters = new EncoderParameters(1);
myEncoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 75L);

Bitmap bmp = new Bitmap(directory + "marbles.bmp");

bmp.Save(directory + "output_net.jpg", jgpEncoder, myEncoderParameters);

exiftool output_python.jpg -a -G1 -w txt

[ExifTool]      ExifTool Version Number         : 12.31
[System]        File Name                       : output_python.jpg
[System]        Directory                       : .
[System]        File Size                       : 148 KiB
[System]        File Modification Date/Time     : 2021:09:28 09:19:20-06:00
[System]        File Access Date/Time           : 2021:09:28 09:19:21-06:00
[System]        File Creation Date/Time         : 2021:09:27 21:33:35-06:00
[System]        File Permissions                : -rw-rw-rw-
[File]          File Type                       : JPEG
[File]          File Type Extension             : jpg
[File]          MIME Type                       : image/jpeg
[File]          Image Width                     : 1419
[File]          Image Height                    : 1001
[File]          Encoding Process                : Baseline DCT, Huffman coding
[File]          Bits Per Sample                 : 8
[File]          Color Components                : 3
[File]          Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
[JFIF]          JFIF Version                    : 1.01
[JFIF]          Resolution Unit                 : inches
[JFIF]          X Resolution                    : 300
[JFIF]          Y Resolution                    : 300
[Composite]     Image Size                      : 1419x1001
[Composite]     Megapixels                      : 1.4

exiftool output_net.jpg -a -G1 -w txt

[ExifTool]      ExifTool Version Number         : 12.31
[System]        File Name                       : output_net.jpg
[System]        Directory                       : .
[System]        File Size                       : 147 KiB
[System]        File Modification Date/Time     : 2021:09:28 09:18:05-06:00
[System]        File Access Date/Time           : 2021:09:28 09:18:52-06:00
[System]        File Creation Date/Time         : 2021:09:27 21:32:19-06:00
[System]        File Permissions                : -rw-rw-rw-
[File]          File Type                       : JPEG
[File]          File Type Extension             : jpg
[File]          MIME Type                       : image/jpeg
[File]          Image Width                     : 1419
[File]          Image Height                    : 1001
[File]          Encoding Process                : Baseline DCT, Huffman coding
[File]          Bits Per Sample                 : 8
[File]          Color Components                : 3
[File]          Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
[JFIF]          JFIF Version                    : 1.01
[JFIF]          Resolution Unit                 : inches
[JFIF]          X Resolution                    : 300
[JFIF]          Y Resolution                    : 300
[Composite]     Image Size                      : 1419x1001
[Composite]     Megapixels                      : 1.4

marbles.bmp sample image

Difference on text-compare

Difference on text-compare

Marbles difference details

Questions

  • Is it reasonable to assume that these two implementations of JPEG compression could yield identical output files?
  • If so, are either PIL or System.Drawing.Image doing any extra steps like anti-aliasing that are making the results different?
  • Or are there additional parameters to PIL .save() to make it behave more like the JPEG encoder in C#?

Thanks

Update

Based on Jeremy's recommendation, I used JPEGsnoop to compare more details between the files and found that the Luminance and Chrominance tables were different. I modified the code:

bmp = PIL.Image.open('marbles.bmp')

output_net = PIL.Image.open('output_net.jpg')

bmp.save(
    'output_python.jpg',
    format='jpeg',
    dpi=(300,300),
    subsampling=2,
    qtables=output_net.quantization,
    #quality=75
)

Now the tables are the same, but the difference between the files is unchanged. The only differences JPEGsnoop shows now are in the Compression stats and Huffman code histogram stats.

output_net.jpeg

*** Decoding SCAN Data ***
  OFFSET: 0x0000026F
  Scan Decode Mode: Full IDCT (AC + DC)

  Scan Data encountered marker   0xFFD9 @ 0x00024BE7.0

  Compression stats:
    Compression Ratio: 28.43:1
    Bits per pixel:     0.84:1

  Huffman code histogram stats:
    Huffman Table: (Dest ID: 0, Class: DC)
      # codes of length 01 bits:        0 (  0%)
      # codes of length 02 bits:     1664 (  7%)
      # codes of length 03 bits:    18238 ( 81%)
      # codes of length 04 bits:     1807 (  8%)
      # codes of length 05 bits:      715 (  3%)
      # codes of length 06 bits:        4 (  0%)
      # codes of length 07 bits:        0 (  0%)
      ...

output_python.jpg

*** Decoding SCAN Data ***
  OFFSET: 0x0000026F
  Scan Decode Mode: Full IDCT (AC + DC)

  Scan Data encountered marker   0xFFD9 @ 0x00025158.0

  Compression stats:
    Compression Ratio: 28.17:1
    Bits per pixel:     0.85:1

  Huffman code histogram stats:
    Huffman Table: (Dest ID: 0, Class: DC)
      # codes of length 01 bits:        0 (  0%)
      # codes of length 02 bits:     1659 (  7%)
      # codes of length 03 bits:    18247 ( 81%)
      # codes of length 04 bits:     1807 (  8%)
      # codes of length 05 bits:      711 (  3%)
      # codes of length 06 bits:        4 (  0%)
      # codes of length 07 bits:        0 (  0%)
      ...

I am now looking for a way to sync these values through PIL.

7
  • 3
    If you're interested in low level jpeg info, perhaps something like impulseadventure.com/photo/jpeg-snoop.html will be useful? Commented Sep 29, 2021 at 4:29
  • 3
    Good question - +1 - FWIW, I don't think you can expect them to be identical - for example I suspect there are too many possible rounding errors in the encoding process, but I am no expert. Out of curiosity, how much difference do you get if you use 100 percent quality? Commented Sep 29, 2021 at 12:06
  • @500-InternalServerError The % difference drops from 42.99% to 31.81% when I increase the quality to 100% on both implementations. Unfortunately, since I need it to match how we previously compressed in .NET at 75% quality, I can only change the python to try to make the images match. Changing the quality any amount in python without changing it in .NET quickly increases the % difference. Commented Sep 29, 2021 at 18:35
  • @JeremyLakeman Thanks for the recommendation, I've added an update with what I found. Commented Sep 29, 2021 at 18:36
  • Could try to set subsampling=1 and test? Commented Sep 30, 2021 at 19:54

2 Answers 2

2
+25

Is it reasonable to assume that these two implementations of JPEG compression could yield identical output files?

The answer is not really.

The point of the JPEG compression is high compression with loss. Even with the quality setting of 100, loss is inevitable, given the algorithm requires infinite precision to exactly replicate the source image.

It is possible to produce identical files, if both algorithms are coded identically using the same parameters: precision, boundary selection and padding/offset specifications to provide the power of 2 size for the FFT.

Implementations of the JPEG algorithm may use pre-passes to optimize the parameters of the algorithm.

Given that the optimizations of the parameters differs between the two implementations, it is unlikely that the outputs would be identical.


Are there additional parameters to PIL .save() to make it behave more like the JPEG encoder in C#?

I cannot answer this question directly, but, you can use the package: Python for.NET to access the C# JPEG encoder from Python. This solution would provide consistent identical results.


Why would anyone need binary compatibility, other than the educational value?

In all of my perceived practical scenarios addressing the question, the only need is to save an additional hash of the image: save the new hash in a separate field.

Pick a technology and use it until it no longer fits your needs/requirements. When it doesn't (preferably well before), find shims to fill the gap and rewrite the code to utilize the new technology.

Sign up to request clarification or add additional context in comments.

2 Comments

Pythonnet is able to produce the exact same image when the script is run in Windows. Unfortunately I've run into a new problem where the output is different (actually the exact same percentage as in the original question) when run on Linux which is where this code would ideally live.
I agree that there is no real need for this compression equality between languages/operating systems. The motivation for this question is simply that the image processing that this is a part of feeds images to an instance segmentation model and the python implementation is many times faster than .NET. But for whatever reason (i.e. how it was trained) the model produces consistently worse results when the images are compressed in python. And in a highly regulated industry, we're looking for speedup and a cleaner architecture without changing how the model performs.
0

I do not believe JPEG is deterministic, so I would expect different implementations to produce different binaries. I don't have any reference to support that assertion. In fact I would not expect .NET to be entirely consistent across the lifetime of the API as .NET 1.1 on Windows 98 would in my opinion be unlikely to produce the same output as .NET 4.8 on Windows 11 until tested and proven otherwise. You should confirm that your oldest images produced at the beginning of the application lifecycle still convert identically today.

[Edit: I see Strom mentioned Python.NET. I will still include my code here but recommend not to roll your own.]

Instead I would approach this by having the Python code call the .NET function. Untested:

jpegnet.cs

using [...]

class JPEGNET
{
    [DllExport("save", CallingConvention = CallingConvention.Cdecl)]
    public static int save()
    {
        ImageCodecInfo jgpEncoder = ImageCodecInfo.GetImageDecoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
        EncoderParameters myEncoderParameters = new EncoderParameters(1);
        myEncoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 75L);

        Bitmap bmp = new Bitmap(directory + "marbles.bmp");

        bmp.Save(directory + "output_net.jpg", jgpEncoder, myEncoderParameters);
    }
}

jpegnet.py

import ctypes
jpegnet = ctypes.cdll.LoadLibrary(source)
jpegnet.save()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.