计算图像的核心图像数据哈希值(不包括元数据)。

27

我正在编写一段脚本来计算图像的MD5校验和,但需要排除EXIF标签。

为了准确地做到这一点,我需要知道文件中EXIF标签的位置(开头、中间、结尾),以便将其排除在外。

如何确定标签在文件中的位置?

我扫描的图像格式包括TIFF、JPG、PNG、BMP、DNG、CR2,还有一些视频格式如MOV、AVI和MPG。


1
你想要实现什么目标? - Sven Marnach
2
尝试高效地创建图像的哈希值,当编辑EXIF数据时不会改变。(ImageMagick具有视觉总和功能,但这非常慢。) - ensnare
请查看规格说明书:http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf - Maksym Polshcha
2
请注意,您可能不仅要排除EXIF,还要仅包括图像的“核心”部分。如果格式支持私有数据块,则许多软件包(如照片组织器)将其元数据添加到文件中。 - Krumelur
可能是[唯一的图像哈希值,如果EXIF信息更新不会改变]的重复问题(https://dev59.com/El7Va4cB1Zd3GeqPLqNt)。 - mlt
4个回答

22

使用Python Imaging Library提取图片数据要容易得多(在iPython中的示例):

In [1]: import Image

In [2]: import hashlib

In [3]: im = Image.open('foo.jpg')

In [4]: hashlib.md5(im.tobytes()).hexdigest()
Out[4]: '171e2774b2549bbe0e18ed6dcafd04d5'

这适用于PIL可以处理的任何类型的图像。 tobytes 方法返回一个包含像素数据的字符串。

顺便说一句,MD5哈希现在被认为相当脆弱。 最好使用SHA512:

In [6]: hashlib.sha512(im.tobytes()).hexdigest()
Out[6]: '6361f4a2722f221b277f81af508c9c1d0385d293a12958e2c56a57edf03da16f4e5b715582feef3db31200db67146a4b52ec3a8c445decfc2759975a98969c34'

在我的电脑上,对一张2500x1600像素的JPEG图像计算MD5校验和大约需要0.07秒。使用SHA512算法,需要0.10秒。以下是完整示例:

#!/usr/bin/env python3

from PIL import Image
import hashlib
import sys

im = Image.open(sys.argv[1])
print(hashlib.sha512(im.tobytes()).hexdigest(), end="")

对于电影,您可以使用例如ffmpeg从中提取帧,然后像上面展示的那样进行处理。


3
请注意,MD5在哈希冲突方面是不安全的。但它仍然是一种优秀且非常快速的算法,适用于快速检查文件是否更改(之后可以进行逐字节检查)。 - parasietje
1
在我的计算机上,像上面展示的创建2560x1600 JPEG文件的MD5校验和大约需要0.07秒。使用SHA512大约需要0.10秒。并没有太大的差异。SHA256校验和需要最长的时间,大约需要0.14秒。从人类的角度来看,它们都很快。我仍然坚持我的建议使用SHA512。 - Roland Smith
你已经说服了我。感谢你进行研究。 - parasietje
@gbin:但是,你的论点是视频编码不是确定性的,这难道不会必然地否定你比较二进制流的建议吗? - Roland Smith
4
在对100k张图像进行哈希处理时,哈希时间的30%到100%的差异会累计起来。MD5不适用于密码哈希,但在处理图像哈希时需要考虑碰撞风险极小。你不是对随机数据块进行哈希处理,而是图像文件。另一个具有完全相同哈希值且也是相同类型的有效图像文件且没有明显视觉瑕疵的概率有多大? - Mzzl
显示剩余3条评论

8

一种简单的方法是对核心图像数据进行哈希。对于PNG格式,您可以通过仅计算“关键块”(即以大写字母开头的块)来完成此操作。JPEG具有类似但更简单的文件结构。

ImageMagick中的视觉哈希在哈希时将图像解压缩。在您的情况下,您可以立即对压缩的图像数据进行哈希,因此(如果正确实现)它应该与哈希原始文件一样快。

这是一个小型Python脚本,说明了这个想法。它可能适用于您,也可能不适用,但至少应该说明我的意思 :)

import struct
import os
import hashlib

def png(fh):
    hash = hashlib.md5()
    assert fh.read(8)[1:4] == "PNG"
    while True:
        try:
            length, = struct.unpack(">i",fh.read(4))
        except struct.error:
            break
        if fh.read(4) == "IDAT":
            hash.update(fh.read(length))
            fh.read(4) # CRC
        else:
            fh.seek(length+4,os.SEEK_CUR)
    print "Hash: %r" % hash.digest()

def jpeg(fh):
    hash = hashlib.md5()
    assert fh.read(2) == "\xff\xd8"
    while True:
        marker,length = struct.unpack(">2H", fh.read(4))
        assert marker & 0xff00 == 0xff00
        if marker == 0xFFDA: # Start of stream
            hash.update(fh.read())
            break
        else:
            fh.seek(length-2, os.SEEK_CUR)
    print "Hash: %r" % hash.digest()


if __name__ == '__main__':
    png(file("sample.png"))
    jpeg(file("sample.jpg"))

谢谢 - 我如何计算关键块?任何示例代码都将不胜感激。 - ensnare
回到答案。我如何扩展它以支持TIFF、CR2、DNG、MOV和AVI文件?或者更一般地说,有什么建议可以找到文件中的模式,以查看关键块从哪里开始? - ensnare
如果你正在使用Python,那么你应该和我差不多:随时愿意用CPU时间来换取开发速度。如果是这样的话,为单个格式实现哈希函数,然后将其他格式转换为该格式似乎是合理的——在“事后”处理性能问题(如果你真的发现有任何问题的话)。 - Paulo Scardine
@ensnare 你提到的文件格式(除了我对CR2和DNG一无所知),在我的代码示例中基本上是以与PNG相同的方式构建的,因此你应该能够使用相同的方法。 - Krumelur

4

您可以使用stream,它是ImageMagick套件的一部分:

$ stream -map rgb -storage-type short image.tif - | sha256sum
d39463df1060efd4b5a755b09231dcbc3060e9b10c5ba5760c7dbcd441ddcd64  -

或者

$ sha256sum <(stream -map rgb -storage-type short image.tif -)
d39463df1060efd4b5a755b09231dcbc3060e9b10c5ba5760c7dbcd441ddcd64  /dev/fd/63

这个例子是针对一个RGB的TIFF文件,每个样本有16位(即每个像素48位)的。因此我使用map将其转换为rgb,并使用short存储类型(如果RGB值是8位,则可以在此处使用char)。
这种方法报告与冗长的Imagemagick identify命令报告的相同的signature哈希值:
$ identify -verbose image.tif | grep signature
signature: d39463df1060efd4b5a755b09231dcbc3060e9b10c5ba5760c7dbcd441ddcd64

(适用于ImageMagick v6.x; 版本7中由identify报告的哈希与使用stream获取的哈希不同,但后者可以由任何能够提取原始位图数据的工具(例如dcraw对于某些图像类型)复制。)

你知道从图像中提取正确的参数到-map-storage-type选项的方法吗?只给出typeOfHash和文件就能得到哈希值。此外,正如你所注意到的,即使他们说在v7上也会更改identify签名,但如何复制它仍不清楚。 - Pablo Bianchi
我能想到的唯一方法是解释元数据。不幸的是,它在文件类型之间并不一致。我刚试了一个TIFF(exiftool -PhotometricInterpretation -BitsPerSample),而对于JPEG,则需要使用-ColorMode。关于ImageMagick哈希更改,这仅适用于identify报告的值,而不适用于问题所示的stream输出。我能够使用一些线性DNG文件上的dcraw生成与stream相同的哈希,因此我很高兴stream输出可以使用另一个工具进行再现。 - starfry
这个可能的重复问题中:在PHP文档中:Imagemagick Signature_为图像像素流生成SHA-256消息摘要。_看起来是“答案”,但仍需要一个替代清晰的方法来获得相同的签名。 - Pablo Bianchi
@PabloBianchi 如果PHP使用的库调用与“identify”命令相同的代码,则它将受到v7算法更改的影响。然而,“stream”的输出确实是图像像素流,在v7之前,“identify”也使用了它,但在v7中不再如此。我使用另一个工具(“dcraw”)交叉检查了“stream”的输出,因此我非常有信心它是原始流数据。不幸的是,自v7以来,ImageMagick计算的签名不再是图像像素流的SHA-256消息摘要 - starfry
1
identify -verbose works as promised (a signature is the same for the same image with different metadata), but it is four times as slow compared to stream - sanmai
"identify -format '%#' "仅报告哈希值,在我的测试中比" identify -verbose "快4到9倍。干杯 :) - mcs

1
我会使用元数据剥离器来预处理您的哈希值:
从 ImageMagick 包中,您可以找到...
mogrify -strip blah.jpg

如果你这样做

identify -list format 

它显然可以与所有引用的格式一起使用。


1
这个解决方案适用于所有视频和图片格式,这是奖励的要求,对吧? - gbin

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