【问题标题】:What is the fastest solution to compare JPEG image? (ignoring metadata, just the "pixels")比较JPEG图像的最快解决方案是什么? (忽略元数据,只是“像素”)
【发布时间】:2012-05-11 21:50:09
【问题描述】:

当我搜索“JPEG”和“元数据”这两个词时,我有很多操作元数据的答案...而我想要的恰恰相反... ;o)

我编写了一个完全符合我要求的函数...(如果图像相似,并且只有元数据发生变化,则该函数返回 True ;如果至少有一个像素发生变化,则返回 False ) 但是,我想提高性能...

瓶颈是bmp.Assign(jpg);

function CompareImages(fnFrom, fnTo: TFileName): Boolean;
var
  j1, j2: TJpegImage;
  b1, b2: TBitmap;
  s1, s2: TMemoryStream;
begin
  Result := False;
sw1.Start;
  j1 := TJpegImage.Create;
  j2 := TJpegImage.Create;
sw1.Stop;
sw2.Start;
  s1 := TMemoryStream.Create;
  s2 := TMemoryStream.Create;
sw2.Stop;
//sw3.Start;
  b1 := TBitmap.Create;
  b2 := TBitmap.Create;
//sw3.Stop;
  try
  sw1.Start;
    j1.LoadFromFile(fnFrom);
    j2.LoadFromFile(fnTo);
  sw1.Stop;

            // the very long part...
            sw3.Start;
              b1.Assign(j1);
              b2.Assign(j2);
            sw3.Stop;


  sw4.Start;
    b1.SaveToStream(s1);
    b2.SaveToStream(s2);
  sw4.Stop;
  sw2.Start;
    s1.Position := 0;
    s2.Position := 0;
  sw2.Stop;
  sw5.Start;
    Result := IsIdenticalStreams(s1, s2);
  sw5.Stop;
  finally
//  sw3.Start;
    b1.Free;
    b2.Free;
//  sw3.Stop;
  sw2.Start;
    s1.Free;
    s2.Free;
  sw2.Stop;
  sw1.Start;
    j1.Free;
    j2.Free;
  sw1.Stop;
  end;
end;

sw1, ..., sw5 是TStopWatch,我用来识别花费的时间。

IsIdenticalStreams 来自here

如果我直接比较TJpegImage,流不一样...

有更好的编码方式吗?

问候,

W.

更新:

测试从 cmets 中提取的一些解决方案,我使用此代码具有相同的性能:

type
  TMyJpeg = class(TJPEGImage)
    public
      function Equals(Graphic: TGraphic): Boolean; override;
  end;

...

function CompareImages(fnFrom, fnTo: TFileName): Boolean;
var
  j1, j2: TMyJpeg;
begin
  sw1.Start;
  Result := False;
  j1 := TMyJpeg.Create;
  j2 := TMyJpeg.Create;
  try
    j1.LoadFromFile(fnFrom);
    j2.LoadFromFile(fnTo);
  Result := j1.Bitmap.Equals(j2.Bitmap);
  finally
    j1.Free;
    j2.Free;
  end;
  sw1.Stop;
end;

有什么方法可以直接访问文件中的像素数据字节(跳过元数据字节)而不进行位图转换?

【问题讨论】:

  • 我会提取没有元数据的像素数据,并使用 md5-hashing 来比较内容。尝试一下可能是一个有用的想法...
  • @ChristopheD:知道如何直接提取像素数据吗?
  • 一种蛮力的方法是查看图像的位图并使用扫描线循环。如果图像不同,它可能会很快停止......
  • @Dorin,最好的方法是比较标头后面的块,而不会像 MBo 试图解释的那样做出任何妥协(我不确定这是否像在所有情况下所说的那样简单,但是 @987654323 @例如是一个好看的非官方解释)。然后可能是TJPEGImage.BitmapTGraphic.Equals,以防OP 想要直观地比较图像,将JPEG 解压缩为位图并将它们的像素与CompareMem 进行比较。那是因为他们会是例如。用不同的方式压缩,但是......
  • @Dorin, ...视觉上肯定是一样的。这个问题是 TJPEGImage.Bitmap getter 进行解压缩,除了几个步骤外,里面还有 jpeg_read_scanlines,所以你可以尝试创建自己的函数,你可以在其中同时解压缩两个图像,一旦你会发现你已经完成并且有结果。我想指出这里的位图扫描线会解压缩两个图像并使用迭代,但TBitmap.Equals 用于位图数据比较CompareMem 并执行相同操作。

标签: delphi comparison jpeg


【解决方案1】:

JPEG 文件由块组成,其类型由标记标识。 chunks的结构(除了独立的SOI、EOI、RSTn):

chunk type marker (big-endian FFxx)
chunk length (big-endian word)
data (length-2 bytes)

编辑: SOS 块受到另一个标记的限制,而不是长度。

元数据块以 APPn 标记 (FFEn) 开头,但带有 JFIF 标题的 APP0 (FFE0) 标记除外。

所以我们可以只读取和比较重要的块,而忽略 APPn 块和 COM 块(正如 TLama 所注意到的)。

示例:一些 jpeg 文件的十六进制视图:

它以 SOI(图像开始)标记 FFD8(独立,无长度)开头,

然后是长度为 16 字节的 APP0 块 (FFE0),

然后是 APP1 块 (FFE1),其中包含元数据(EXIF 数据、NIKON COOLPIX 名称等),因此我们可以忽略 9053 字节(23 5D)并检查地址 2373 处的下一个块标记,依此类推...

编辑:简单解析示例:

var
  jp: TMemoryStream;
  Marker, Len: Word;
  Position: Integer;
  PBA: PByteArray;

  procedure ReadLenAndMovePosition;
  begin
    Inc(Position, 2);
    Len := Swap(PWord(@PBA[Position])^);
    Inc(Position, Len);
  end;

begin
  jp := TMemoryStream.Create;
  jp.LoadFromFile('D:\3.jpg');
  Position := 0;
  PBA := jp.Memory;

  while (Position < jp.Size - 1) do begin
    Marker := Swap(PWord(@PBA[Position])^);
    case Marker of
      $FFD8: begin
          Memo1.Lines.Add('Start Of Image');
          Inc(Position, 2);
        end;
      $FFD9: begin
          Memo1.Lines.Add('End Of Image');
          Inc(Position, 2);
        end;
      $FFE0: begin
          ReadLenAndMovePosition;
          Memo1.Lines.Add(Format('JFIF Header Len: %d', [Len]));
        end;
      $FFE1..$FFEF, $FFFE: begin
          ReadLenAndMovePosition;
          Memo1.Lines.Add(Format('APPn or COM Len: %d Ignored', [Len]));
        end;
      $FFDA: begin
          //SOS marker, data stream, ended by another marker except for RSTn
          Memo1.Lines.Add(Format('SOS data stream started at %d', [Position]));
          Inc(Position, 2);
          while Position < jp.Size - 1 do begin
            if PBA[Position] = $FF then
              if not (PBA[Position + 1] in [0, $D0..$D7]) then begin
                Inc(Position, 2);
                Memo1.Lines.Add(Format('SOS data stream ended at %d',
                  [Position]));
                Break;
              end;
            Inc(Position);
          end;
        end;
    else begin
        ReadLenAndMovePosition;
        Memo1.Lines.Add(Format('Marker %x Len: %d Significant', [Marker, Len]));
      end;
    end;
  end;
  jp.Free;
end;

【讨论】:

  • 感谢结构解释...我尝试实现代码,稍后再回来提供反馈...
  • 说实话...我迷路了...“SOS 数据流开始”正是我需要的...所以,这应该没问题。 .. 但是,重要标记是什么?为什么它们很重要?因为我所有的图片后面都是一样的(字节相同)
  • 这是我的标签 - 我在元数据和评论块的输出中添加了“忽略”,对与真实数据相关的块添加了“重要” - SOS 流和 Huffman 以及量化表
猜你喜欢
  • 1970-01-01
  • 2018-10-04
  • 1970-01-01
  • 1970-01-01
  • 2012-10-31
  • 2015-06-15
  • 2022-01-03
  • 2023-03-07
  • 2013-10-10
相关资源
最近更新 更多