C#和Python中JPEG压缩的差异

6
我正在将一些图像处理功能从.NET移植到Python,但输出图像必须以与.NET相同的方式进行压缩。然而,当我在text-compare等工具上比较.jpg输出文件并选择Ignore nothing时,这些文件的压缩方式存在显著差异。
例如: 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

大理石.bmp 样例图像

文本比较的差异

Difference on text-compare

Marbles difference details

问题

  • 假设这两种JPEG压缩的实现可以产生完全相同的输出文件,这是合理的吗?
  • 如果是这样,PILSystem.Drawing.Image是否执行了任何额外的步骤,如反锯齿,使结果不同?
  • 还是PIL .save()有其他参数可以使其更像C#中的JPEG编码器?

谢谢

更新

根据 Jeremy's recommendation,我使用 JPEGsnoop比较了文件之间的更多细节,并发现亮度和色度表是不同的。我修改了代码:

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
)

现在表格相同,但文件之间的差异保持不变。JPEGsnoop显示的唯一差异是在“压缩统计”和“霍夫曼编码直方图统计”中。

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%)
      ...

我现在正在寻找一种通过 PIL 同步这些值的方法。


3
如果你对低级别的jpeg信息感兴趣,可能像https://www.impulseadventure.com/photo/jpeg-snoop.html这样的东西会很有用? - Jeremy Lakeman
3
好问题,+1。就我所知,你不应该期望它们完全相同——例如,我怀疑在编码过程中存在太多的四舍五入误差,但我不是专家。出于好奇,如果你使用100%的质量,会有多大的差异? - 500 - Internal Server Error
@500-服务器内部错误 当我将两种实现的质量都提高到100%时,差异百分比从42.99%降至31.81%。不幸的是,由于我需要它与我们以前在.NET中压缩的75%质量相匹配,因此我只能更改Python来尝试使图像匹配。在Python中任意更改质量而不在.NET中更改会迅速增加差异百分比。 - J. Mac
@JeremyLakeman 感谢您的推荐,我已经更新了我的发现。 - J. Mac
可以尝试设置 subsampling=1 进行测试吗? - user16930239
显示剩余2条评论
2个回答

2
这两种JPEG压缩的实现能产生相同的输出文件吗?
实际上不太可能。
JPEG压缩的目的是高压缩率和损失。即使质量设置为100,由于算法需要无限精度才能完全复制源图像,损失也是不可避免的。
如果两个算法使用相同的参数进行编码,包括精度、边界选择和填充/偏移规范以提供FFT的2次幂大小,则可以生成相同的文件。
JPEG算法的实现可以使用预处理来优化算法的参数。
考虑到两个实现之间的参数优化不同,它们的输出很可能不相同。
是否有其他参数可以使PIL.save()更像C#中的JPEG编码器?
我不能直接回答这个问题,但是你可以使用包:Python for.NET从Python访问C# JPEG编码器。这个解决方案将提供一致的相同结果。
为什么除了教育价值外,还需要二进制兼容性呢?
在我所感知的所有实际场景中,唯一的需求是保存图像的额外哈希:将新哈希保存在单独的字段中。
选择一种技术并使用它,直到它不再适合你的需求。当它不再适合你的需求时(最好在之前),找到填补差距的接口并重写代码以利用新技术。

Pythonnet能够在Windows上运行脚本并生成完全相同的图像。不幸的是,在Linux上运行代码时,我遇到了一个新问题,输出与原来的问题一样有所不同(实际上是相同百分比)。然而,这段代码理想的运行环境是Linux。 - J. Mac
我认为在语言/操作系统之间没有真正需要这种压缩平等性。提出这个问题的动机仅仅是因为这个图像处理部分向实例分割模型提供图像,而Python实现比.NET快得多。但由于某种原因(即它是如何训练的),当图像在Python中被压缩时,该模型产生的结果始终较差。在高度规范化的行业中,我们正在寻求加速和更清晰的架构,而不改变模型的性能。 - J. Mac

0

我不相信JPEG是确定性的,所以我预计不同的实现会产生不同的二进制文件。我没有任何参考来支持这个说法。事实上,我不认为.NET在API的整个生命周期内都是完全一致的,因为在Windows 98上的.NET 1.1与在Windows 11上的.NET 4.8产生相同的输出可能性不大,除非经过测试并证明否则。您应该确认应用程序生命周期开始时生成的最旧图像今天仍然可以转换为相同的图像。

[编辑:我看到Strom提到了Python.NET。我仍然会在这里包含我的代码,但建议不要自己编写。]

相反,我会通过让Python代码调用.NET函数来解决这个问题。未经测试:

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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接