Go的`image.Bytes`和Python的PIL库的`tobytes`方法生成的字节数据是不同的。

huangapple go评论79阅读模式
英文:

Go image bytes and Python PIL tobytes produce different byte data

问题

我接手了一些之前由别人用Python编写的代码,现在我用Go重新编写了这部分代码。

其中一部分代码是打开图像,读取其数据,并生成MD5哈希以防止重复。

Python版本的代码使用了PIL库:

from PIL import Image
f = Image.open('2d16395b-da48-11eb-8cbe-36a331f79a1e.png')
hashlib.md5(f.tobytes()).hexdigest()
'c699a448b38df386d036ed418d7714f3'

而Go版本的代码只是将字节读入MD5哈希中:

f, _ := os.Open(p)
_, err := f.Seek(0, io.SeekStart)
if err != nil {
    fmt.Println(err)
}

h := md5.New()
if _, err := io.Copy(h, f); err != nil {
    log.Fatal(err)
}

hb := h.Sum(nil)
hash := hex.EncodeToString(hb)

然而,它们生成了不同的MD5哈希值。

看起来PIL库以不同的方式读取字节,可能是去掉了头部/元数据之类的东西。

有没有人知道我如何在Go中复制PIL的字节读取方式以获得相同的MD5哈希值?

英文:

I have taken over some code that was previously in python written by someone else, and no is in go, written by myself.

Part of this code was to open an image, read its data, and make an MD5 hash to prevent duplicates.

The python version of this code used PIL:

from PIL import Image
f = Image.open('2d16395b-da48-11eb-8cbe-36a331f79a1e.png')
hashlib.md5(f.tobytes()).hexdigest()
'c699a448b38df386d036ed418d7714f3'

And the go version just read the bytes into an md5 hash

    f, _ := os.Open(p)
	_, err := f.Seek(0, io.SeekStart)
	if err != nil {
		fmt.Println(err)
	}

	h := md5.New()
	if _, err := io.Copy(h, f); err != nil {
		log.Fatal(err)
	}

	hb := h.Sum(nil)
	hash := hex.EncodeToString(hb)

However these produce different MD5.

It seems like the PIL library is readying the bytes differently, maybe stripping of header/metadata or something?

Does anyone know of a way i can replicate the byte-read of PIL in go to get the same MD5 hash?

答案1

得分: 1

我将从我使用的(虚拟)测试图像开始:

Go的`image.Bytes`和Python的PIL库的`tobytes`方法生成的字节数据是不同的。

这是一个32X16RGBA)的*.png*图像,只有两种颜色:

  • 红色(237, 28, 36)
  • 蓝色(0, 0, 255)

回到问题:[ReadTheDocs.Pillow]: Image.load()(由open隐式调用)处理(解码)图像,生成其原始位图数据,这与(编码的)文件内容完全不同。

这是两者之间的区别。

code00.py

#!/usr/bin/env python

import sys
from PIL import Image
from hashlib import md5


def read_file_data(file_name):
    with open(file_name, "rb") as fin:
        return fin.read()


def read_img_data(file_name):
    with Image.open(file_name) as img:
        return img.tobytes()


def process_bytes(buf, first=20, last=20):
    print("Len: {:d}\nFirst bytes:\n  ".format(len(buf)), end=" ")
    for i in range(first):
        print("0x{:02X}".format(buf[i]), end=" ")
    print("\nLast bytes:\n  ", end=" ")
    for i in range(-last, 0, 1):
        print("0x{:02X}".format(buf[i]), end=" ")
    print("\nMD5: {:}".format(md5(buf).hexdigest()))


def main(*argv):
    img_name = "rb.png"
    funcs = [
        read_file_data,
        read_img_data,
    ]
    for func in funcs:
        print("\nFunction {:s}".format(func.__name__))
        b = func(img_name)
        process_bytes(b)


if __name__ == "__main__":
    print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                   64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

输出

> lang-bat > [cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q068231412]> sopr.bat > ### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ### > > [prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.08.07_test0\Scripts\python.exe" code00.py > Python 3.8.7 (tags/v3.8.7:6503f05, Dec 21 2020, 17:59:51) [MSC v.1928 64 bit (AMD64)] 064bit on win32 > > > Function read_file_data: > Len: 169 > First bytes: > 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52 0x00 0x00 0x00 0x10 > Last bytes: > 0xE2 0x8A 0x24 0x69 0x53 0x4C 0xB3 0x03 0x00 0x00 0x00 0x00 0x49 0x45 0x4E 0x44 0xAE 0x42 0x60 0x82 > MD5: 8368b5c29a12b298cea2ad4b32955830 > > Function read_img_data: > Len: 2048 > First bytes: > 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF > Last bytes: > 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF > MD5: ebdf44b7ad36d79b221a70ea2b0fa0c7 > > Done. >

经过几个小时的研究(试错、Google搜索、阅读doc和示例 - 我不能不在这里提到[SO]: Get a pixel array from from golang image.Image (@ArunaHerath's answer)),我能够将上述脚本翻译成Go

code00.go

package main

import (
    "crypto/md5"
    "fmt"
    "image"
    "image/draw"
    "io/ioutil"
    "os"

    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"
)

type ImageFunc func(string) []byte
type ImageFuncs []ImageFunc

func ReadFileData(fileName string) []byte {
    buf, _ := ioutil.ReadFile(fileName)
    return buf
}

func ReadImgData(fileName string) []byte {
    reader, _ := os.Open(fileName)
    defer reader.Close()
    img, _, _ := image.Decode(reader)
    rect := img.Bounds()
    rgba := image.NewRGBA(rect)
    draw.Draw(rgba, rect, img, rect.Min, draw.Src)
    //fmt.Printf("%v\n", rgba.Pix)
    return rgba.Pix
}

func ProcessBytes(buf []byte, first int, last int) {
    lb := len(buf)
    fmt.Printf("Len: %d\nFirst bytes:\n  ", lb)
    for i := 0; i < first; i++ {
        fmt.Printf("0x%02X ", buf[i])
    }
    fmt.Printf("\nLast bytes:\n  ")
    for i := lb - last; i < lb; i++ {
        fmt.Printf("0x%02X ", buf[i])
    }
    fmt.Printf("\nMD5: %x", md5.Sum(buf))
}

func main() {
    imgName := "rb.png"
    first := 20
    last := 20
    funcs := ImageFuncs{
        ReadFileData,
        ReadImgData,
    }
    for idx := range funcs {
        function := funcs[idx]
        fmt.Printf("\n\nFunction %#v:\n", function)
        b := function(imgName)
        ProcessBytes(b, first, last)
    }
    fmt.Printf("\n\nDone.\n")
}

输出

> lang-bat &gt; [prompt]&gt; "f:\Install\pc064\Google\GoLang\1.16.5\bin\go.exe" run code00.go &gt; &gt; &gt; Function (main.ImageFunc)(0x9f1060): &gt; Len: 169 &gt; First bytes: &gt; 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52 0x00 0x00 0x00 0x10 &gt; Last bytes: &gt; 0xE2 0x8A 0x24 0x69 0x53 0x4C 0xB3 0x03 0x00 0x00 0x00 0x00 0x49 0x45 0x4E 0x44 0xAE 0x42 0x60 0x82 &gt; MD5: 8368b5c29a12b298cea2ad4b32955830 &gt; &gt; Function (main.ImageFunc)(0x9f10e0): &gt; Len: 2048 &gt; First bytes: &gt; 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF &gt; Last bytes: &gt; 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF &gt; MD5: ebdf44b7ad36d79b221a70ea2b0fa0c7 &gt; &gt; Done. &gt;

注意

  • 重要:我只使用(虚拟)RGBA图像测试了代码,对于其他颜色格式,可能需要额外的工作。
  • 如上所示,我没有进行任何错误处理,以保持代码简洁(但无论如何,这超出了本问题的范围)。
英文:

I'm going to start with the (dummy) test image that I used:

Go的`image.Bytes`和Python的PIL库的`tobytes`方法生成的字节数据是不同的。

which is a 32X16 (RGBA) .png with only 2 colors:

  • Red (237, 28, 36)
  • Blue (0, 0, 255)

Back to the question: [ReadTheDocs.Pillow]: Image.load() (called implicitly by open) processes (decodes) the image, yielding its raw bitmap data, which is totally different than the (encoded) file contents.

Here's the difference between the 2.

code00.py:

#!/usr/bin/env python

import sys
from PIL import Image
from hashlib import md5


def read_file_data(file_name):
    with open(file_name, &quot;rb&quot;) as fin:
        return fin.read()


def read_img_data(file_name):
    with Image.open(file_name) as img:
        return img.tobytes()


def process_bytes(buf, first=20, last=20):
    print(&quot;Len: {:d}\nFirst bytes:\n  &quot;.format(len(buf)), end=&quot; &quot;)
    for i in range(first):
        print(&quot;0x{:02X}&quot;.format(buf[i]), end=&quot; &quot;)
    print(&quot;\nLast bytes:\n  &quot;, end=&quot; &quot;)
    for i in range(-last, 0, 1):
        print(&quot;0x{:02X}&quot;.format(buf[i]), end=&quot; &quot;)
    print(&quot;\nMD5: {:}&quot;.format(md5(buf).hexdigest()))


def main(*argv):
    img_name = &quot;rb.png&quot;
    funcs = [
        read_file_data,
        read_img_data,
    ]
    for func in funcs:
        print(&quot;\nFunction {:s}&quot;.format(func.__name__))
        b = func(img_name)
        process_bytes(b)


if __name__ == &quot;__main__&quot;:
    print(&quot;Python {:s} {:03d}bit on {:s}\n&quot;.format(&quot; &quot;.join(elem.strip() for elem in sys.version.split(&quot;\n&quot;)),
                                                   64 if sys.maxsize &gt; 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print(&quot;\nDone.&quot;)
    sys.exit(rc)

Output:

> lang-bat
&gt; [cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q068231412]&gt; sopr.bat
&gt; ### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ###
&gt;
&gt; [prompt]&gt; &quot;e:\Work\Dev\VEnvs\py_pc064_03.08.07_test0\Scripts\python.exe&quot; code00.py
&gt; Python 3.8.7 (tags/v3.8.7:6503f05, Dec 21 2020, 17:59:51) [MSC v.1928 64 bit (AMD64)] 064bit on win32
&gt;
&gt;
&gt; Function read_file_data:
&gt; Len: 169
&gt; First bytes:
&gt; 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52 0x00 0x00 0x00 0x10
&gt; Last bytes:
&gt; 0xE2 0x8A 0x24 0x69 0x53 0x4C 0xB3 0x03 0x00 0x00 0x00 0x00 0x49 0x45 0x4E 0x44 0xAE 0x42 0x60 0x82
&gt; MD5: 8368b5c29a12b298cea2ad4b32955830
&gt;
&gt; Function read_img_data:
&gt; Len: 2048
&gt; First bytes:
&gt; 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF
&gt; Last bytes:
&gt; 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF
&gt; MD5: ebdf44b7ad36d79b221a70ea2b0fa0c7
&gt;
&gt; Done.
&gt;

After some hours of research (trial and error, Googleing, reading docs and examples - I can't afford not mention here [SO]: Get a pixel array from from golang image.Image (@ArunaHerath's answer)), I was able to translate the above script to Go.

code00.go:

package main

import (
    &quot;crypto/md5&quot;
    &quot;fmt&quot;
    &quot;image&quot;
    &quot;image/draw&quot;
    &quot;io/ioutil&quot;
    &quot;os&quot;

    _ &quot;image/gif&quot;
    _ &quot;image/jpeg&quot;
    _ &quot;image/png&quot;
)

type ImageFunc func(string) []byte
type ImageFuncs []ImageFunc

func ReadFileData(fileName string) []byte {
    buf, _ := ioutil.ReadFile(fileName)
    return buf
}

func ReadImgData(fileName string) []byte {
    reader, _ := os.Open(fileName)
    defer reader.Close()
    img, _, _ := image.Decode(reader)
    rect := img.Bounds()
    rgba := image.NewRGBA(rect)
    draw.Draw(rgba, rect, img, rect.Min, draw.Src)
    //fmt.Printf(&quot;%v\n&quot;, rgba.Pix)
    return rgba.Pix
}

func ProcessBytes(buf []byte, first int, last int) {
    lb := len(buf)
    fmt.Printf(&quot;Len: %d\nFirst bytes:\n  &quot;, lb)
    for i := 0; i &lt; first; i++ {
        fmt.Printf(&quot;0x%02X &quot;, buf[i])
    }
    fmt.Printf(&quot;\nLast bytes:\n  &quot;)
    for i := lb - last; i &lt; lb; i++ {
        fmt.Printf(&quot;0x%02X &quot;, buf[i])
    }
    fmt.Printf(&quot;\nMD5: %x&quot;, md5.Sum(buf))
}

func main() {
    imgName := &quot;rb.png&quot;
    first := 20
    last := 20
    funcs := ImageFuncs{
        ReadFileData,
        ReadImgData,
    }
    for idx := range funcs {
        function := funcs[idx]
        fmt.Printf(&quot;\n\nFunction %#v:\n&quot;, function)
        b := function(imgName)
        ProcessBytes(b, first, last)
    }
    fmt.Printf(&quot;\n\nDone.\n&quot;)
}

Output:

> lang-bat
&gt; [prompt]&gt; &quot;f:\Install\pc064\Google\GoLang\1.16.5\bin\go.exe&quot; run code00.go
&gt;
&gt;
&gt; Function (main.ImageFunc)(0x9f1060):
&gt; Len: 169
&gt; First bytes:
&gt; 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52 0x00 0x00 0x00 0x10
&gt; Last bytes:
&gt; 0xE2 0x8A 0x24 0x69 0x53 0x4C 0xB3 0x03 0x00 0x00 0x00 0x00 0x49 0x45 0x4E 0x44 0xAE 0x42 0x60 0x82
&gt; MD5: 8368b5c29a12b298cea2ad4b32955830
&gt;
&gt; Function (main.ImageFunc)(0x9f10e0):
&gt; Len: 2048
&gt; First bytes:
&gt; 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF 0xED 0x1C 0x24 0xFF
&gt; Last bytes:
&gt; 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF
&gt; MD5: ebdf44b7ad36d79b221a70ea2b0fa0c7
&gt;
&gt; Done.
&gt;

Notes:

  • Important: I've only tested the code with the (dummy) RGBA image, for other color formats it might need additional work
  • As seen, I didn't do any error handling, in order to keep code short (but anyway, that's beyond this question's scope)

答案2

得分: 1

这里存在一个基本的误解。

JPEG/PNG/TIFF 文件是在磁盘上进行编码的。它包含以下一些或全部内容:

  • 图像的宽度和高度
  • 图像的颜色空间和压缩表
  • 创建日期
  • GPS 坐标
  • 作者
  • 版权信息
  • 压缩后的像素

文件的前几个字节将是 JPEG/PNG 的魔数(或签名),后面是其他 PNG 块或 JPEG 段,包含上述列出的信息。

PIL Image 经过转换为字节后,只包含未压缩的像素。前几个字节将是左上角的像素,接下来的几个字节将是第一行中的第二个像素。

它们在本质上是不同的,永远不会具有相同的 MD5 校验和。

英文:

There's a fundamental misunderstanding here.

A JPEG/PNG/TIFF file is encoded on disk. It contains some, or all, of the following:

  • image width and height
  • image colourspace and compression tables
  • date of creation
  • GPS coordinates
  • author
  • copyright
  • compressed pixels

The first few bytes will be a JPEG/PNG magic number (or signature) followed by other PNG chunks, or JPEG sections, containing the info itemised above.

A PIL Image, after conversion to bytes, contains only uncompressed pixels. The first few bytes will be the top-left pixel, the next few bytes will be the second pixel across in the first row.

They are fundamentally different and will never have the same MD5 checksum.

huangapple
  • 本文由 发表于 2021年7月3日 05:30:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/68231412.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定