可靠地识别 JPG?

Reliably identifying a JPG?

为了识别和比较从相机拍摄的 JPG 图像,我想计算 JPG 中图像扫描部分的 MD5 哈希值。我的想法是获取 SOS 和 EOI 标记之间的字节,并基于这些字节永远不会改变的假设对这些字节执行哈希,除非实际图像被处理和更改。

显然这个问题已经出现好几次了1,2, 3。已经提出了相当复杂的解决方案,我觉得看着我相当简单但显然有效的方法很烦人。 (或者是不是太简单了?)

我知道一个 JPG 文件中可以有多对 SOS ($FFDA) 和 EOI ($FFD9),在我现在的文件中有 3 个:缩略图、实际图像和附加的 1920x1080 图像(索尼).我目前的方法是解析流并定位下一个 SOS,然后寻找 EOI,计算大小,如果大小超过文件大小的 50%,则假设为实际图像。

这种方法适用于我现有的文件。我用 exiftool -all= image.jpg 从 JPG 文件中删除了所有元数据,发现 MD5 哈希值是相同的。然而,该算法对我来说似乎相当粗糙。 所以这是我的问题:

简单地检查 SOS 和 EOI 之间的 space 是否有失败的风险?我已阅读,但仍不确定。

从实际图像的 SOS 中解析每个字节需要花费大量时间。我从 here 中了解到,找到压缩数据的结尾没有捷径可走。但我可能会从第二个 SOS 标记向前跳跃 80% 左右。我说的是来自相机的图像 - 我能在多大程度上依赖于先有缩略图然后有实际图像这一事实?

我应该在 SOS 之后开始 6 字节吗 (here?)

有更好的方法吗?

Is there any risk that simply examining the space between SOS and EOI can fail?

是的,为了您的目的,如果您只是对扫描数据进行校验和。它们之间可能有多个 SOS 标记和其他标记。

经过一些研究和运行这里的一系列测试后,我提出了我对问题的解决方案。

首先,我想澄清一下,我们不是谈论取证调查。可能有一些方法可以使标记出现在它们不应该出现的地方并且不出现在根据规范必须出现的地方。

我们也不是谈论图像同一性或相似性。如果您无损地旋转 JPG,您仍然拥有完全相同的图像信息,但不再是相同的图像。我们也不是在谈论以任何其他方式调整大小、优化或更改的图像。

我们讨论的 是识别简单的重复项或已重命名或元数据已被修改或删除的 JPG,但是图片本身从未以任何方式被处理或篡改过。

Is a hash of the bytes between the SOS and the EOI markers a reliable way to uniquely identify an image?

是的,是的。在合理范围内,图像扫描数据的 MD5 校验和相同的两个文件不可能包含不相同的图像,反之亦然。
我检查了用 12 个不同制造商的相机拍摄的示例照片和 edited/stripped 元数据。实际上,这并不是真正必要的,因为从规范和代码中您 知道 所有元数据都驻留在单独的块中(这就是为什么您可以在 JPG 中隐藏所有类型的内容)并且元数据操作永远不会触及扫描数据,但是,是的,到处都是相同的 MD5 校验和。

Is there any way to quickly locate the (right) SOS marker?

当然。 JPG 规范是一团糟和惩罚。在尝试了很多代码后,我发现 NativeJPG by Nils Haeck 是最直接的。 这改编自 sdJpegImage:

function FindSOSPos(S: TStream): Cardinal;
var
  B, MarkerTag, BytesRead: byte;
  Size,W: word;
const
  mkNone = 0; mkSOF0 = $c0; mkSOF1 = $c1; mkSOF2 = $c2; mkSOF3 = $c3; mkSOF5 = $c5; 
  mkSOF6 = $c6; mkSOF7 = $c7; mkSOF9 = $c9; mkSOF10 = $ca; mkSOF11 = $cb; mkSOF13 = $cd; 
  mkSOF14 = $ce; mkSOF15 = $cf; mkDHT = $c4; mkDAC = $cc; mkSOI = $d8; mkEOI = $d9; mkSOS = $da; 
  mkDQT = $db; mkDNL = $dc; mkDRI = $dd; mkDHP = $de; mkEXP = $df; mkAPP0 = $e0; mkAPP15 = $ef; mkCOM = $fe; 
begin
  Repeat
    Result := 0;
    // Read markers from the stream, until a non $FF is encountered
    If S.Read(B, 1) = 0 then
      exit;
    // Do we have a marker?
    if B = $FF then
    begin
      BytesRead := S.Read(MarkerTag, 1);
      while (BytesRead > 0) and (MarkerTag = $FF) do
      begin
        MarkerTag := mkNone;
        BytesRead := S.Read(MarkerTag, 1);
      end;
      Size := 0;
      if MarkerTag in [mkAPP0..mkAPP15, mkDHT, mkDQT, mkDRI,
        mkSOF0, mkSOF1, mkSOF2, mkSOF3, mkSOF5, mkSOF6, mkSOF7, mkSOF9, mkSOF10, mkSOF11, mkSOF13, mkSOF14, mkSOF15,
        mkCOM, mkDNL] then
      begin
        // Read length of marker
        If S.Read(W, 2) = 2 then
          Size := Swap(W) - 2
        else exit;
      end else
        If MarkerTag = mkSOS
          then break;
      S.Position := S.Position + Size;
    end else
    begin
      // B <> $FF is an error, we try to be flexible
      repeat
        BytesRead := S.Read(B, 1);
      until (BytesRead = 0) or (B = $FF);
      if BytesRead = 0 then
        exit;
      S.Seek(-1, soFromCurrent);
    end;
  Until (MarkerTag = mkSOS) or (MarkerTag = mkNone);
  Result := S.Position;
end; 

Omit the first 6 Bytes after the SOS marker?

我决定对 SOS 和 EOI 之间的所有内容进行哈希处理,不包括标记本身。

Is there a fast way to locate the trailing EOI marker?

没有。但这无关紧要,因为要执行散列,您无论如何都必须读取每个字节。

How reliable is this approach?

正如我所说,我相信 在合理范围内 这种方法不会产生误报的可能性实际上是 100%。至于找到正确的图像:NativeJPG 已经存在了 10 多年,你发现很少有抱怨,如果有的话,他们处理图像解码,而不是丢失它。

在我的应用程序中,我提供了在 UserComment 字段中存储原始文件名、EXIF DateTimeDigitized、相机品牌、GPS 坐标和扫描数据(完整和前 16 kB)的 MD5 哈希值的选项。我非常有信心这将允许稍后在大多数情况下识别文件(如果 UserComment 保持完整)。