Sign in Join
Wiki copytemporary

PHASH: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## Calculating Perceptual hashes (PHASH) and comparing image similarity
22
33Here is a program to calculate the perceptual hash for images different than a CRC or md5 hash a perceptual hash will be similar for similar images. A slight change in the image (sharpen, blur, resize) will give perceptual hash that is similar where a CRC or MD5 hash will be completely different. I looked for a PHASH program for pascal and could not find any. I did find three methods for getting a perceptual hash. The first was using a DCT transform but the code was written in C and was not easy to link the program in. The second way is to resize it and make a hash based on whether each pixel was more or less then its next neighbor. The third way was to use image moments. ImageMagick(IM) uses this method but I found that after a certain threshold the images where radically different. Also calculating the differences required a lot of computing power as you had to square the difference of 42 numbers for each image comparison. I tried all three methods, only the first had good promise. The last one was not even part of the IM Lazarus interface.
44
55This program uses ImageMagick and I want to express my appreciation for the work of the translators of this tool to work with Lazarus. However I had a lot of problems getting it to work properly. First because it has not kept up with the code there were some routines that did not work because they have been deprecated (and no longer available) in ImageMagick. Second because some of the newer routines were missing entirely. And third because I was using the Q8 version of ImageMagick and it appeared that the translation was for the Q16 version. PPixelPacket for instance assumes the colors are 16 bit and not 8 bit so I had to change how I handled the structure). I use 8 bit ImageMagick because 99.99999% of the pictures I wanted to process are 8 bit or less and the Q8 program is much faster then the Q16 version.
66
77To get this to work under windows 7 you need to compile under 32bit mode (and call 32 bit ImageMagick) as the calling conventions for 64 bit are quite different and Lazarus does not support the 64 bit calling conventions. (this may change but keep it in mind).
88
99This program takes one main argument - the directory to parse for images (currently only JPG and GIF but you can add more). The program maintains a file 'yalls' that contains information for each image it finds in that directory - deleting files that no longer exists, adding new files and if the md5 does not match recalculating data for that file. (for speed you can have it skip the md5 check and just rewrite existing lines for files by placing SKIP before the directory argument. Since I had a specific set of directories to parse if you do not use any argument it will parse those directories. The output file will consist of one line for each image it finds consisting of fields separated by blanks. The program expects the filenames are without spaces and would need modification otherwise. There was a memory leak with the program and after about 200,000 images it crashed. This may be gone but if the program dies just move the yall file to yalls and restart with JPEGINFO SKIP to restart from where it ended.
1010
1111The output file contains:
1212
13-` MD5 - 32 hex bytes of the MD5 sum of the entire file.`\
14-` Signature - 64 hex bytes that comes from IM identify that is I think the md5 of the image only`\
15-` PHASH - 16 hex bytes of the DCT hash of the image`\
16-` Date of the file in YYYYMMDDHHMMSS format (from ImageMagick identify)`\
17-` Size in bytes of the file`\
18-` Bitdepth of the image`\
19-` Width of the image`\
20-` Height of the image`\
21-` Filename of the image`\
22-` Complete fileid of the image`\
23-` Example:`\
24-` md5       fb54e12c44d2b14d4d3266251109799e`\
25-` signature 2318170444932902d0541895584df4c2f8f38ad8b1d4dad899870760817422f2`\
26-` phash     CCFB4EAEF3F30C0D`\
27-` date      20151001010847`\
28-` size      901`\
29-` bitdepth  8`\
30-` width     32`\
31-` width     32`\
32-` filename  01.gif`\
33-` fid       F:\CCC\01.gif`
13+```text
14+ MD5 - 32 hex bytes of the MD5 sum of the entire file.
15+ Signature - 64 hex bytes that comes from IM identify that is I think the md5 of the image only
16+ PHASH - 16 hex bytes of the DCT hash of the image
17+ Date of the file in YYYYMMDDHHMMSS format (from ImageMagick identify)
18+ Size in bytes of the file
19+ Bitdepth of the image
20+ Width of the image
21+ Height of the image
22+ Filename of the image
23+ Complete fileid of the image
24+ Example:
25+ md5 fb54e12c44d2b14d4d3266251109799e
26+ signature 2318170444932902d0541895584df4c2f8f38ad8b1d4dad899870760817422f2
27+ phash CCFB4EAEF3F30C0D
28+ date 20151001010847
29+ size 901
30+ bitdepth 8
31+ width 32
32+ width 32
33+ filename 01.gif
34+ fid F:\CCC\01.gif
35+```
3436
3537I will add another program later if there is interest that calculates phash similarity. This program was written on a windows7 platform. Basically to get phash similarity you count the number of bits that are different. If that number is 0 the pictures are likely to be the same, if it is over 10 the pictures are likely to be different, otherwise the pictures may be similar to some degree. Note it takes about 24 hours to process 700000 images with this program on my computer. I estimate it may take another 24 hours to compare these hashes up to 4 bit differences. At least twice that for 8 bit differences.
3638
3739``` pascal
3840program JPEGInfo;
3941
4042{$APPTYPE CONSOLE} {$R-}
4143
4244uses strutils,sysutils,classes, md5,magick_wand, ImageMagick,DateUtils;
4345
4446{ Bytes = array[0..0] of Byte;}
4547{ LongWords = array[0..255] of Byte;}
4648
4749type
4850 clr = record
4951 red:byte;
5052 green:byte;
5153 blue:byte;
5254 opacity:byte;
5355 end;
5456var
5557 allfile:text;
5658 infile:text;
5759 donedir,donefname, inrec:string;
5860 skipped,deleted,added,modified,count:longword;
5961 wand: PMagickWand;
6062 updatefileexists, skipmd5check,notstarted,readnextrec :boolean;
6163 dctmatrix : array[0..7,0..31] of real;
6264 status: MagickBooleanType;
6365 starttime:tdatetime;
6466 filename:pchar;
6567 udir,ufname:string;
6668 Size : Integer;
6769 timed:longint;
6870 DirName: string;
6971
7072procedure ThrowWandException(wand: PMagickWand);
7173var
7274 description: PChar;
7375 severity: ExceptionType;
7476begin
7577 description := MagickGetException(wand, @severity);
7678 WriteLn(Format('An error occurred. Description: %s', [description]));
7779 description := MagickRelinquishMemory(description);
7880 Abort;
7981end;
8082
8183function pad(msg:string;len:byte):string;
8284var par : byte;
8385begin
8486 result := msg;
8587 if length(msg) > len then result := copy(msg,1,len);
8688 while length(result) < len do result := result + ' ';
8789 for par := 1 to len do if ord(result[par]) < 32 then result[par] := ' ';
8890end;
8991
9092{read the next old file record into inrec and fileid into donefname,donedir}
9193procedure getnextok;
9294var i: integer;
9395
9496procedure skipitem;
9597begin
9698 while (i < length(inrec)) and (inrec[i] <> ' ') do inc(i);
9799end;
98100
99101procedure skipspaces;
100102begin
101103 while inrec[i] = ' ' do inc(i)
102104end;
103105begin
104106 if eof(infile) then donedir := chr(255)
105107 else begin
106108 readln(infile,inrec);
107109 i := 1;
108110 skipitem; {md5} skipspaces; {fb54e12c44d2b14d4d3266251109799e}
109111 skipitem; {md6} skipspaces; {2318170444932902d0541895584df4c2f8f38ad8b1d4dad899870760817422f2}
110112 skipitem; {phash} skipspaces; {CCFB4EAEF3F30C0D}
111113 skipitem; {date} skipspaces; {20151001010847}
112114 skipitem; {size} skipspaces; { 901}
113115 skipitem; {depth} skipspaces; { 8}
114116 skipitem; {width} skipspaces; { 32}
115117 skipitem; {height} skipspaces; { 32}
116118 skipitem; {name} skipspaces; {01.gif }
117119 donefname:= copy(inrec,i,999); {F:\CCC\01.gif}
118120 i := rpos('\',donefname);
119121 if i = 0 then writeln('error reading line from yalls:',inrec);
120122 donedir := StringReplace(uppercase(copy(donefname,1,i)),'\',' ',[rfReplaceAll]);;
121123 donefname := uppercase(copy(donefname,i+1,9999));
122124 end;
123125 readnextrec := false;
124126end;
125127function GetDif(time:longint):string;
126128var part: longint; t:longint;
127129begin
128130 result := '';
129131 t := time;
130132 if time > 86400 then begin
131133 part := t div 86400;
132134 t := t mod 86400;
133135 result := result + format('%.2d',[part])+' '; end;
134136 if time > 3600 then begin
135137 part := t div 3600;
136138 t := t mod 3600;
137139 result := result + format('%.2d',[part])+':'; end;
138140 if time > 60 then begin
139141 part := t div 60;
140142 t := t mod 60;
141143 result := result + format('%.2d',[part])+':'; end;
142144 if time > 0 then
143145 result := result + format('%.2d',[t]);
144146end;
145147
146148function gethash:string;
147149var
148150 intermediate : array[0..7,0..31] of real;
149151 i,j,r,c,k:byte;
150152 img:array[0..31,0..31] of byte;
151153 dctresult:array[0..7,0..7] of real;
152154 pimg: Pimage;
153155 pack:record case integer of
154156 1:( pkt:PPixelPacket);
155157 2:( co:^clr);
156158 end;
157159 hash,one:qword; mean:DOUBLE;
158160begin
159161 result := 'xxxxxxxxxxxxxxxx'; { in case an error occurs}
160162 try
161163 {Resize the image to 32x32. If it is my test case file
162164 'f:\ccc\01.gif' then it is already at the correct size and I
163165 found that if imagemagic resizes it the file gets changed ---
164166 I did not want that, it might be faster to grayscale the image
165167 first then resize but not if you use MagickQuantizeImage too slow}
166168 if uppercase(filename) <> 'F:\CCC\01.GIF' then
167169 MagickResizeImage(wand, 32, 32, LanczosFilter, 1.0);
168170 {now we convert the image to grayscale - this procedure is similar to
169171 MagickQuantizeImage(wand, 256, GRAYColorspace, iszero, iszero, iszero);
170172 but is a LOT faster. BTW I don't know why I had to use variables
171173 for this function. The conversion numbers were selected to be
172174 close to what MagickQuantizeImage used. Note I had an issue
173175 here. Under Q8 IM GetAuthenticPixels it returns an array of
174176 byte values for colors but the lazarus interface mapped it to word sizes.
175177 You could get an entire array of values with getauthenticpixels but
176178 the speed won't be much different so I didn't bother.
177179 We dump the results into the img array}
178180 pimg := GetImageFromMagickWand(wand);
179181 for j := 0 to 31 do begin
180182 for i := 0 to 31 do begin
181183 pack.pkt := GetAuthenticPixels(pimg, i, j, 1, 1, nil);
182184 img[i,j] :=round(pack.co^.red*0.072+
183185 pack.co^.green*0.715+
184186 pack.co^.blue*0.213);
185187 end;
186188 end;
187189 {results are in the img array. Now do matrix multiplcation
188190 DCTMATRIX * img * transposed(DCTMATRIX)
189191 since we only need the top 8x8 array we only do the
190192 calculations for that part}
191193 {intermediate = DCTMATRIX*IMG}
192194 fillchar(intermediate,sizeof(intermediate),0);
193195 for r := 0 to 7 do
194196 for c := 0 to 31 do
195197 for k := 0 to 31 do
196198 intermediate[r,c] := intermediate[r,c] +
197199 dctmatrix[r,k]*img[k,c];
198200 {dctresult = intermediate*transposed(DCTMATRIX)}
199201 fillchar(dctresult,sizeof(dctresult),0);
200202 for r := 0 to 7 do
201203 for c := 0 to 7 do
202204 for k := 0 to 31 do
203205 dctresult[r,c] := dctresult[r,c] +
204206 intermediate[r,k]*dctmatrix[c,k]{inverted dctmatrix};
205207 {now we need to calculate the arithmetic mean. Note the first value
206208 (0,0) is much too large and affects the mean value too much.
207209 To fix it is set to 0 but perhaps some other solution can be done
208210 such as do a log on the value}
209211 mean := 0;
210212 dctresult[0,0] := 0;
211213 for r := 0 to 7 do
212214 for c := 0 to 7 do
213215 mean := mean + dctresult[r,c];
214216 mean := mean / 64;
215217 {the hash is calculated by setting the bit of
216218 each value greater than the mean}
217219 one := 1;
218220 hash := 0;
219221 for r := 0 to 7 do
220222 for c := 0 to 7 do begin
221223 if dctresult[r,c] > mean then inc(hash,one);
222224 one := one shl 1;
223225 end;
224226 one := 1;
225227 {return the phash}
226228 result := inttohex(hash,16);
227229 finally
228230 end;
229231end;
230232
231233procedure processfile(dir:string;fname:string);
232234var
233235 F: file;
234236 idents:pchar;
235237 i:integer;
236238 md5,md6, date, fsize,depth,imageheight,imagewidth,fno:string;
237239 P: Pointer;
238240begin
239241 inc(count);
240242 if (count mod 1000) = 1 then begin
241243 timed := SecondsBetween(Now,starttime);
242244 writeln('at count ',count-1,' E:'+ GetDif(timed)+' D:',deleted,' A:',added,' M:',modified,' S:',skipped,' ',dir,fname);
243245 end;
244246 filename := pchar(dir+fname);
245247 udir := StringReplace(uppercase(dir),'\',' ',[rfReplaceAll]);;
246248 ufname := uppercase(fname);
247249 if updatefileexists then begin
248250 if readnextrec then getnextok;
249251 while (udir > donedir) or ((udir = donedir) and (ufname > donefname)) do begin getnextok; inc(deleted); end;
250252 if skipmd5check then
251253 if (udir+ufname) = (donedir+donefname) then begin
252254 inc(skipped);
253255 writeln(allfile,inrec);
254256 readnextrec := true;
255257 exit;
256258 end;
257259 end;
258260 TRY
259261 Assign(F,FileName);
260262 FileMode:=fmOpenRead + fmShareDenyNone;
261263 Reset(F, 1);
262264 Size := FileSize(F);
263265 GetMem(P, Size);
264266 BlockRead(F, P^, Size);
265267 md5 := md5print(md5buffer(p^,Size));
266268 finally
267269 FreeMem(P);
268270 CloseFile(F);
269271 end;
270272 if updatefileexists then begin
271273 if (udir+ufname) = (donedir+donefname) then begin
272274 if copy(inrec,1,32) = md5 then begin
273275 inc(skipped);
274276 writeln(allfile,inrec);
275277 readnextrec := true;
276278 exit;
277279 end;
278280 inc(modified);
279281 end
280282 else inc(added);
281283 end;
282284 if notstarted then begin
283285 notstarted := false;
284286 writeln('Started at line:',count,' with file:',filename);
285287 end;
286288 try
287289 wand := NewMagickWand;
288290 status := MagickReadImage(wand, filename);
289291 if (status = MagickFalse) then ThrowWandException(wand);
290292 idents := magickidentifyimage(wand);
291293 md6 := copy(idents,pos('signature:',idents)+11,64);
292294 i := pos('date:modify:',idents)+13;
293295 if i = 0 then i := pos('date:create:',idents)+13;
294296 date := copy(idents,i ,4)+copy(idents,i+ 5,2)+copy(idents,i+ 8,2)+
295297 copy(idents,i+11,2)+copy(idents,i+14,2)+copy(idents,i+17,2);
296298 fsize := copy(idents,pos('Filesize:',idents)+10,64);
297299 i := POS(Chr(10),FSIZE);
298300 if i > 0 then fsize := copy(fsize,1,i-1);
299301 i := pos('Depth:', idents)+7;
300302 depth := copy(idents, i,1);
301303 i := pos('Geometry:',idents)+10;
302304 imageheight := copy(idents, i,99);
303305 i := pos('x',imageheight);
304306 imagewidth := copy(imageheight,i+1,99);
305307 imageheight := copy(imageheight,1,i-1);
306308 i := pos('+',imagewidth);
307309 imagewidth := copy(imagewidth,1,i-1);
308310 fno := fname;
309311 if length(fno) < 22 then fno := pad(fno,25);
310312 writeln(allfile,md5,' ',md6,' ',gethash,' ',date:14,' ',
311313 size:12, depth:2,imageheight:7,imagewidth:7,' ',
312314 fno,' ',filename);
313315 finally
314316 wand := DestroyMagickWand(wand);
315317 end;
316318end;
317319
318320// Recursive procedure to build a list of files
319321procedure FindFiles(StartDir: string);
320322var
321323 SR: TSearchRec;
322324 IsFound: Boolean;
323325begin
324326 if StartDir[length(StartDir)] <> '\' then StartDir := StartDir + '\';
325327 { Build a list of the files in directory StartDir
326328 (but not the directories!) }
327329 IsFound := FindFirst(StartDir+'*.*', faAnyFile-faDirectory, SR) = 0;
328330 while IsFound do begin
329331 if (uppercase(extractFileExt(sr.name)) = '.JPG')
330332 or (uppercase(extractFileExt(sr.name)) = '.GIF')
331333 then processfile(StartDir,SR.Name);
332334 IsFound := FindNext(SR) = 0;
333335 end;
334336 FindClose(SR);
335337 // Build a list of subdirectories
336338 IsFound := FindFirst(StartDir+'*.*', faDirectory, SR) = 0;
337339 while IsFound do begin
338340 if ((SR.Attr and faDirectory) <> 0) and (SR.Name[1] <> '.') then
339341 FindFiles(StartDir + SR.Name);
340342 IsFound := FindNext(SR) = 0;
341343 end;
342344 FindClose(SR);
343345end;
344346
345347{$R *.res}
346348procedure makedct;
347349var c1:real; ii,p,q: byte;
348350begin
349351 c1 := 1/sqrt(32);
350352 for ii := 0 to 31 do dctmatrix[0,ii] := c1;
351353 c1 := c1*2;
352354 for p := 1 to 7 do
353355 for q := 0 to 31 do
354356 dctmatrix[p,q] := c1 * cos(pi/64*p*(2*q+1));
355357end;
356358
357359begin
358360 starttime := now; {keep track of time}
359361 {have we started processing files as opposed to copy old recs}
360362 notstarted := true;
361363 skipmd5check := false; {on copy skip md5 check}
362364 DirName := ParamStr(1); {did he pass a filename}
363365 if length(dirname) > 3 then
364366 if copy(upcase(dirname),1,4) = 'SKIP' then begin
365367 dirname := paramstr(2);
366368 skipmd5check := true;
367369 end;
368370 count := 0;
369371 deleted := 0; modified := 0; added := 0; skipped := 0;
370372 {allow access to file while writing it}
371373 FileMode:=fmOpenwrite + fmShareDenyNone;
372374 assign(allfile,'yall');
373375 rewrite(allfile);
374376 readnextrec := true;
375377 updatefileexists := fileexists('yalls');
376378 if updatefileexists then begin
377379 {allow access to file while reading it}
378380 FileMode:=fmOpenwrite + fmShareDenyNone;
379381 assign(infile,'yalls');
380382 reset(infile);
381383 getnextok;
382384 end;
383385
384386 MagickWandGenesis;
385387 makedct; {make DCT matrix (or at least what is needed)}
386388 {This is a set of directories that I desired to maintain}
387389 if (dirname = 'F:\') or (dirname = 'F:\') or
388390 (length(dirname) < 3) then begin
389391 FindFiles('F:\CCC\');
390392 FindFiles('F:\CD\');
391393 FindFiles('F:\CHECK\');
392394 FindFiles('F:\QDISK\');
393395 end
394396 else FindFiles(DirName); {or use a user supplied directory name}
395397 writeln('found:',count,' files ',' Deleted:',deleted,' Added:',added,' Changed:',modified,' SKIPPED:',skipped);
396398 close(allfile);
397399 if updatefileexists then close(infile);
398400 MagickWandTerminus;
399401 { I get paranoid and like to keep a few versions of the files around}
400402 if fileexists('yalls.005') then deletefile('yalls.005');
401403 if fileexists('yalls.004') then renamefile('yalls.004','yalls.005');
402404 if fileexists('yalls.003') then renamefile('yalls.003','yalls.004');
403405 if fileexists('yalls.002') then renamefile('yalls.002','yalls.003');
404406 if fileexists('yalls.001') then renamefile('yalls.001','yalls.002');
405407 if fileexists('yalls') then renamefile('yalls','yalls.001');
406408 renamefile('yall','yalls');
407409end.
408410```
409411
410412---
411413*From the Free Pascal wiki, page "PHASH", as it stood on 2022-12-11 after a change by Alextp, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
412414Category: Code
413415Category: FPC
414416Category: Graphics
415417Category: Software security

Back to history