What is the fastest solution to compare JPEG image? (ignoring metadata, just the "pixels")
comparison, delphi, jpeg
Solution
JPEG file consists of chunks, which types are identified by markers. The structure of chunks (except for stand-alone SOI, EOI, RSTn):
chunk type marker (big-endian FFxx)
chunk length (big-endian word)
data (length-2 bytes)
Edit: SOS chunk is limited by another marker, not by length.
Metadata chunks start with APPn marker (FFEn), except for APP0 (FFE0) marker with JFIF title.
So we can read and compare only significant chunks and ignore APPn chunks and COM chunk (as TLama noticed).
Example: hex view of some jpeg file:
It starts with SOI (Start Of Image) marker FFD8 (stand-alone, without length),
then APP0 chunk (FFE0) with length = 16 bytes,
then APP1 chunk (FFE1), which contains metadata (EXIF data, NIKON COOLPIX name etc), so we can ignore 9053 bytes (23 5D) and check next chunk marker at address 2373, and so on...
Edit: Simple parsing example:
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;
Problem
When I search the words "JPEG" and "metadata", I have many answers to manipulate the metadata... and this is the opposite I want... ;o) I have written a function which exactly works like I want... (if images are similar, and only the metadata change or not, the function returns `True` ; if at least one pixel changes, it returns `False`) but, I'd like to improve the performance... The bottleneck is the `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 are TStopWatch, I used to identify the time spent. IsIdenticalStreams comes from here. If I directly compare the `TJpegImage`, the streams are different... Any better way to code that? Regards, W. Update: Testing some solutions extract from the comments, I have the same performance with this code: ``` 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; ``` Any way to directly access the pixel data bytes from the file (skipping the metadata bytes) without bitmap conversion?