1#!/usr/bin/python 2# Copyright (C) 1991-2004 Artifex Software, Inc. All rights reserved. 3# All Rights Reserved. 4# 5# This software is provided AS-IS with no warranty, either express or 6# implied. 7# 8# This software is distributed under license and may not be copied, modified 9# or distributed except as expressly authorized under the terms of that 10# license. Refer to licensing information at http://www.artifex.com/ 11# or contact Artifex Software, Inc., 7 Mt. Lassen Drive - Suite A-134, 12# San Rafael, CA 94903, U.S.A., +1(415)492-9861, for further information. 13 14# $Id: cmpi.py 8409 2007-11-27 20:43:09Z giles $ 15# python compare image -- interactive graphical image differencing 16# 17""" 18 cmpi (compare image or compare interactive) 19 20 usage: cmpi [basline_filename compare_filename] 21 22""" 23HelpMsg = ''' 24 key mnemonic function 25 --- ------------ ----------------------------- 26 b "baseline" show baseline image 27 c "compare" show compare image 28 d "differences" show differences (black is different) 29 h "highlight" toggle difference highlights (box outline) 30 m "mask" toggle mask of baseline/compare with diff 31 n "next" go to next difference 32 o "open" open new image files 33 p "previous" go to previous difference 34 t "this" go to current difference 35 q "quit" 36 z "zoom" zoom to a value 37 + = zoom in 38 - zoom out 39 ? help message 40''' 41 42from Tkinter import * 43import Image, ImageTk, ImageDraw 44import os 45import sys # for exit 46 47# Set globals 48baseline_filename = "" 49compare_filename = "" 50 51class ScrolledCanvas(Frame): 52 def __init__(self, parent=None, color='#E4E4E4'): # default bg = lightgray 53 Frame.__init__(self, parent) 54 self.pack(expand=YES, fill=BOTH) 55 56 self.statustext = StringVar() 57 self.statustext.set(" ") 58 self.statusbar = Label(self, anchor="w", height=1, textvariable=self.statustext) 59 self.statusbar.pack(side=BOTTOM, fill=X) 60 61 self.canv = Canvas(self, bg=color, relief=SUNKEN) 62 self.canv.config() 63 64 self.extraX = self.extraY = -1 65 66 self.sbarX = Scrollbar(self, orient=HORIZONTAL) 67 self.sbarX.config(command=self.canv.xview) 68 self.canv.config(xscrollcommand=self.sbarX.set) 69 self.sbarX.pack(side=BOTTOM, fill=X) 70 71 self.sbarY = Scrollbar(self, orient=VERTICAL) 72 self.sbarY.config(command=self.canv.yview) 73 self.canv.config(yscrollcommand=self.sbarY.set) 74 self.sbarY.pack(side=RIGHT, fill=Y) 75 76 self.canv.config(scrollregion=self.canv.bbox(ALL)) 77 self.canvas_image = None 78 79 self.line1 = self.canv.create_line(0, 0, 0, 0, arrow=LAST, fill="red") 80 self.line2 = self.canv.create_line(0, 0, 0, 0, arrow=LAST, fill="darkgreen") 81 82 self.canv.pack(side=LEFT, expand=YES, fill=BOTH) 83 # self.canv.bind("<Button-1>", self.LeftMouse) 84 85 # not used currently - can't decide what we need the mouse for 86 def LeftMouse(self, event): 87 print "Left Mouse click at: ", event.x, event.y 88 self.canv.coords(self.line1, 0, event.y, event.x, event.y) 89 self.canv.coords(self.line2, event.x, 0, event.x, event.y) 90 91def mask_func(val): 92 return val*0.9 93 94# Display an image at the current zoom factor 95def DoDisplay(SC): 96 if SC.canvas_image != None: SC.canv.delete(SC.canvas_image) 97 if SC.mask != 0: 98 blank_image = Image.new("RGB", SC.image.size, "white") 99 mask_image = SC.image_diff.point(mask_func) 100 this_image = Image.composite(blank_image, SC.image, mask_image) 101 else: 102 this_image = SC.image 103 SC.image_zoomed = this_image.resize((this_image.size[0]*SC.zoom_factor, 104 this_image.size[1]*SC.zoom_factor)) 105 SC.photo = ImageTk.PhotoImage(SC.image_zoomed) 106 SC.canvas_image = SC.canv.create_image(0, 0, image=SC.photo, anchor=NW) 107 SC.canv.tag_lower(SC.canvas_image, 1) # bottomost element 108 # set scrollregion 109 SC.canv.config(scrollregion=SC.canv.bbox(ALL)) 110 111def b_proc(SC, key): 112 # set title to 'baseline image: ' 113 s = "image: %s" % baseline_filename 114 SC.statustext.set(SC.statustext.get()[:66] + s ) 115 # display baseline 116 SC.image = SC.image_baseline 117 DoDisplay(SC) 118 119def c_proc(SC, key): 120 # set title to 'compare image: ' 121 s = "image: %s" % compare_filename 122 SC.statustext.set(SC.statustext.get()[:66] + s ) 123 # display baseline 124 SC.image = SC.image_compare 125 DoDisplay(SC) 126 127def d_proc(SC, key): 128 # set title to 'compare image: ' 129 s = "image: differences" 130 SC.statustext.set(SC.statustext.get()[:66] + s ) 131 SC.image = SC.image_diff 132 SC.mask = 0 133 DoDisplay(SC) 134 135def DelHighlights(): 136 if SC.highlight > 0: 137 for l in SC.highlight_list: 138 SC.canv.delete(l) 139 SC.highlight_list = [ ] 140 141def GoToXY(X, Y): # (0,0) is upper left 142 143 # window may have been resized - 't' == "this" will re-center 144 g = SC.master.geometry() 145 gw = int(g.split('x')[0]) 146 gh = g.split('x')[1] 147 gh = int(gh.split('+')[0]) 148 149 # compute display size 150 dw = gw - SC.extraX 151 dh = gh - SC.extraY 152 153 shx = dw / (float(SC.Width) * SC.zoom_factor) # scrollbar 'handle' width 154 xf = (X / float(SC.Width)) - (shx/2) 155 shy = dh / (float(SC.Height) * SC.zoom_factor) # scrollbar 'handle' width 156 yf = (Y / float(SC.Height)) - (shy/2) 157 SC.canv.xview("moveto", xf) 158 SC.canv.yview("moveto", yf) 159 160def HighlightArea(B, Z, Color): 161 SC.highlight_list.append(SC.canv.create_line(Z*B[0], Z*B[1], Z*(1+B[2]), Z*B[1], fill=Color)) 162 SC.highlight_list.append(SC.canv.create_line(Z*B[0], Z*(1+B[3]), Z*(1+B[2]), Z*(1+B[3]), fill=Color)) 163 SC.highlight_list.append(SC.canv.create_line(Z*B[0], Z*B[1], Z*B[0], Z*(1+B[3]), fill=Color)) 164 SC.highlight_list.append(SC.canv.create_line(Z*(1+B[2]), Z*B[1], Z*(1+B[2]), Z*(1+B[3]), fill=Color)) 165 166 167def DoHighlights(): 168 global SC 169 170 if SC.highlight > 0: 171 # draw the highlight boxes at the current zoom_factor 172 z = SC.zoom_factor 173 for i in range(len(SC.areas)): 174 if i == SC.current_area: color = "red" 175 else: color = "green" 176 HighlightArea(SC.area_boxes[i], z, color) 177 178def h_proc(SC, key): 179 DelHighlights() # If they are on, turn them 'OFF' 180 SC.highlight = 1 - SC.highlight 181 DoHighlights() # Draw highlights if now 'ON' 182 183def m_proc(SC, key): 184 if SC.image == SC.image_diff: 185 print 'Cannot mask. First select baseline or compare image.' 186 SC.mask = 1 - SC.mask 187 DoDisplay(SC) 188 189def npt_proc(SC, key): # next, previous, this 190 191 if key == 'n': SC.current_area += 1 192 if SC.current_area >= 0: # initially at -1, ignore 't' and 'p' 193 DelHighlights() # If they are on, turn them 'OFF' 194 if key == 'p': SC.current_area -= 1 195 if SC.current_area >= len(SC.areas): SC.current_area -= 1 # at last 196 if SC.current_area < 0: SC.current_area = 0 # at first 197 # set statusbar to say which we are at, format "at diff: n of m" 198 s = "at diff: %d of %d, Box: (%d,%d) - (%d,%d)" % (SC.current_area + 1, len(SC.areas), \ 199 SC.area_boxes[SC.current_area][0], SC.area_boxes[SC.current_area][1], \ 200 SC.area_boxes[SC.current_area][2], SC.area_boxes[SC.current_area][3]) 201 blanks = " " 202 SC.statustext.set(s + blanks[0:66-len(s)] + SC.statustext.get()[66:]) 203 b = SC.area_boxes[SC.current_area] 204 X = float(b[0] + b[2]) / 2.0 205 Y = float(b[1] + b[3]) / 2.0 206 GoToXY(X, Y) 207 DoHighlights() # Draw highlights if now 'ON' 208 209def open_dlg_remove(): 210 global open_dlg 211 global baseline_filename, compare_filename 212 213 open_dlg["takefocus"] = 0 214 open_dlg.grab_release() 215 open_dlg.unbind("<Key>") 216 open_dlg.destroy() 217 open_dlg == None 218 SC["takefocus"] = 1 219 SC.master.deiconify() 220 SC.bind_all("<Key>", KeyPress) 221 open_files() 222 223def open_files(): 224 # collect differences 225 # image types: P4:1-bit binary, P5:8-bit gray, P6:8-bit RGB (24-bit) 226 print "Gathering diffs, please wait ..." ##DEBUG 227 228 DelHighlights() 229 SC.zoom_factor = 1 230 SC.current_area = -1 231 SC.mask = 0 232 SC.negate = 0 233 SC.highlight = 0 234 SC.highlight_list = [ ] 235 236 B = open(baseline_filename, "rb") 237 C = open(compare_filename, "rb") 238 BType = B.readline() 239 CType = C.readline() 240 while True: 241 BDim = B.readline() 242 CDim = C.readline() 243 if BDim[0] != '#': break 244 245 SC.Width = int(BDim.split()[0]) 246 SC.Height = int(BDim.split()[1]) 247 248 pixel_size = 1 # default to 1 byte per pixel. 249 if BType[0:2] == "P6": 250 pixel_size = 3 251 BMax = B.readline() 252 CMax = C.readline() 253 254 Bstr = B.read() 255 Cstr = C.read() 256 257 # areas are lists of (start, end, line#) triples. Note that there may be 258 # more than 1 triple with the same line# created when two areas merge on 259 # a line after the first (V shape) 260 SC.areas = [ ] 261 merge = [ ] 262 # diffs_prev_line stores triples: (start, end, area#) 263 # while collecting diffs_curr_line, area# is set to -1 (unknown) 264 diffs_prev_line = [ ] 265 for line in range(SC.Height): 266 line_base = line*SC.Width*pixel_size 267 diffs_curr_line = [ ] 268 start = end = -2 269 for i in range(line_base, line_base+(SC.Width*pixel_size), pixel_size): 270 j = i+pixel_size 271 if Bstr[i:j] != Cstr[i:j]: 272 # next differing pixel 273 if i == end+pixel_size: 274 # set new end point 275 end = i 276 else: 277 if end >= 0: 278 # store previous run 279 diffs_curr_line.append( ((start-line_base)/pixel_size, (end-line_base)/pixel_size, -1) ) 280 start = end = i 281 # end-of-line, store final diff 282 if end >= 0: 283 diffs_curr_line.append( ((start-line_base)/pixel_size, (end-line_base)/pixel_size, -1) ) 284 if len(diffs_curr_line) > 0: 285 # Now update the areas 286 if len(diffs_prev_line) == 0: 287 # add all as new areas 288 for i in range(len(diffs_curr_line)): 289 diff = diffs_curr_line[i] 290 SC.areas.append( [ (diff[0], diff[1], line) ] ) 291 diffs_curr_line[i] = (diff[0], diff[1], len(SC.areas)-1) # update area# 292 else: 293 # process curr_line, checking for areas adjacent to those in prev_line 294 index_in_prev_line = 0 295 prev_diff = diffs_prev_line[0] # [index_in_prev_line] 296 for i in range(len(diffs_curr_line)): 297 diff = diffs_curr_line[i] 298 while diff[0] > prev_diff[1]+1: 299 # advance to next diff in prev_line (if any) 300 index_in_prev_line += 1 301 prev_diff = advance_diff_in_prev_line(diffs_prev_line, index_in_prev_line) 302 303 # curr.E >= prev.S - 1 & curr.S <= prev.E + 1 304 if (prev_diff[2] >= 0) and \ 305 (diff[1] >= prev_diff[0]-1) and \ 306 (diff[0] <= prev_diff[1]+1): 307 # append this diff to area of prev diff's area 308 SC.areas[prev_diff[2]].append( (diff[0], diff[1], line) ) 309 diffs_curr_line[i] = (diff[0], diff[1], prev_diff[2]) # update area# from prev line 310 else: 311 SC.areas.append( [ (diff[0], diff[1], line) ] ) 312 diffs_curr_line[i] = (diff[0], diff[1], len(SC.areas)-1) 313 314 # Merge areas. 315 for curr in diffs_curr_line: 316 for prev in diffs_prev_line: 317 if curr[2] != prev[2]: 318 # areas are different 319 if (curr[0] <= prev[1]+1) & (curr[1] >= prev[0]): 320 # merge areas curr[2] and prev[2] 321 merge.append( [curr[2], prev[2]] ) 322 323 # After updating the areas, save current line as previous 324 diffs_prev_line = diffs_curr_line 325 326 # finished all lines 327 B.close() 328 C.close() 329 # Process the merge list 330 for m in range(len(merge)): 331 for n in range(m+1, len(merge)): 332 if merge[m][0] == merge[n][0] or merge[m][1] == merge[n][0]: 333 merge[m].append(merge[n][1]) 334 merge[n][1] = -1 335 if merge[m][0] == merge[n][1] or merge[m][1] == merge[n][1]: 336 merge[m].append(merge[n][0]) 337 merge[n][0] = -1 338 dest = merge[m][0] 339 if dest >= 0: 340 for src in merge[m][1:]: 341 if src >= 0: 342 SC.areas[dest].extend(SC.areas[src]) 343 SC.areas[src] = [ ] 344 345 # remove any empty (placeholder) elements in areas 346 for i in range(SC.areas.count( [ ] )): SC.areas.remove( [ ] ) 347 348 # Capture the images for the Canvas 349 SC.image_baseline = Image.open(baseline_filename) 350 SC.image_compare = Image.open(compare_filename) 351 352 # Finally, construct the difference image (mask) 353 # collect the box for each area as we process 354 SC.image_diff = Image.new("L", SC.image_baseline.size, 255) 355 SC.area_boxes = [ ] 356 for a in SC.areas: 357 box = [ 999999, 999999, -1, -1 ] 358 for run in a: 359 for x in range(run[0], run[1]+1): 360 SC.image_diff.putpixel((x,run[2]), 0) 361 if run[2] < box[1]: box[1] = run[2] 362 if run[2] > box[3]: box[3] = run[2] 363 if run[0] < box[0]: box[0] = run[0] 364 if run[1] > box[2]: box[2] = run[1] 365 SC.area_boxes.append(box) 366 367 SC.master.title("cmpi %s %s" % (baseline_filename, compare_filename) ) 368 369 print "Difference area count: ",len(SC.areas) ##DEBUG 370 371 # set the canvas to display the entire image if it fits, otherwise use the max 372 if SC.Width < 0.95*int(SC.master.maxsize()[0]): w = 1.06*int(SC.Width) 373 else: w = 0.95*int(SC.master.maxsize()[0]) 374 if SC.Height < 0.90*int(SC.master.maxsize()[1]): h = 1.11*int(SC.Height) 375 else: h = 0.90*int(SC.master.maxsize()[1]) 376 g = "%dx%d+50+50" % (w, h) 377 SC.master.geometry(g) 378 379 # update the statusbar 380 s = "%d differences." % len(SC.areas) 381 blanks = " " 382 SC.statustext.set(s + blanks[0:66-len(s)] + SC.statustext.get()[66:]) 383 384 # Start with the baseline image 385 b_proc(SC, 'b') 386 387def FileList_KeyPress(event): 388 global open_dlg, open_names, FileListPrompt 389 global baseline_filename, compare_filename 390 391 if event.char == 'q' or event.keysym == "Escape": 392 open_names = [ ] 393 if baseline_filename == "": 394 FileListPrompt["text"] = "\nSelect baseline image file" 395 else: 396 open_dlg_remove() 397 elif (event.char == "o" or event.keysym == "Return") and \ 398 len(open_names) == 2: 399 baseline_filename = open_names[0] 400 compare_filename = open_names[1] 401 open_names = [ ] 402 open_dlg_remove() 403 404def GrabFileName(): 405 global FileList, FileListPrompt, open_names 406 407 cur = FileList.curselection() 408 if len(cur) > 0: 409 name = FileList.get(cur) 410 FileList.after(200, FileList.selection_clear, (cur)) 411 if len(open_names) == 0: 412 open_names.append(name) 413 FileListPrompt["text"] = "\nSelect compare image file" 414 elif len(open_names) == 1: 415 open_names.append(name) 416 FileListPrompt["text"] = "\nPress <Enter> if OK or <Esc> to Cancel" 417 418def FileList_Clicked(event): 419 GrabFileName() 420 421def open_dlg_pop(): 422 global open_dlg 423 global FileList, FileListPrompt, open_names 424 425 open_names = [ ] 426 open_dlg = Toplevel() 427 FileListPrompt = Label(open_dlg, height=3, width=80, anchor=CENTER, text="\nSelect baseline image file") 428 FileListPrompt.grid(row=0, sticky=N) 429 yScroll = Scrollbar ( open_dlg, orient=VERTICAL ) 430 yScroll.grid ( row=1, column=1, sticky=N+S ) 431 xScroll = Scrollbar ( open_dlg, orient=HORIZONTAL ) 432 xScroll.grid ( row=2, column=0, sticky=E+W ) 433 FileList = Listbox ( open_dlg, height=20, width=80, xscrollcommand=xScroll.set, yscrollcommand=yScroll.set ) 434 FileList.grid ( row=1, column=0, sticky=N+S+E+W ) 435 xScroll["command"] = FileList.xview 436 yScroll["command"] = FileList.yview 437 SC.master.iconify() 438 SC.unbind_all("<Key>") 439 open_dlg.bind("<Key>", FileList_KeyPress) 440 FileList.bind("<Button-1>",FileList_Clicked) 441 open_dlg.grab_set() 442 open_dlg.focus_set() 443 SC["takefocus"] = 0 444 445 files = os.listdir(".") 446 files.sort() 447 for f in files: 448 FileList.insert(END, f) 449 450def o_proc(SC, key): # open file using listbox selections 451 open_dlg_pop() 452 453def quit(SC, key): 454 sys.exit(0) 455 456def x_proc(SC, key): 457 g = SC.master.geometry() 458 gw = int(g.split('x')[0]) 459 gh = g.split('x')[1] 460 gh = int(gh.split('+')[0]) 461 print "gw x gh = ",gw, " x ", gh ##DEBUG 462 print "extra X,Y: ",SC.extraX,",",SC.extraY 463 print "zoom_factor: ", SC.zoom_factor 464 465 xy = input("Enter factors x,y: ") 466 xf = xy[0] 467 yf = xy[1] 468 SC.canv.xview("moveto", xf) 469 SC.canv.yview("moveto", yf) 470 471def zoom(SC, key): 472 # We are changing zoom factor, so delete highlights and 473 # redraw them at new zoom 474 DelHighlights() 475 if key == '-': 476 SC.zoom_factor -= 1 477 if SC.zoom_factor < 1: SC.zoom_factor = 1 478 else: 479 SC.zoom_factor += 1 480 DoHighlights() # if highlights are 'ON', draw them at new zoom_factor 481 DoDisplay(SC) 482 483def help_proc(SC, key): 484 print HelpMsg 485 486KeyProcs = { 'b' : b_proc, 'c' : c_proc, 'd' : d_proc, 487 'h' : h_proc, 'm' : m_proc, 'n' : npt_proc, 'o' : o_proc, 488 'p' : npt_proc, 't' : npt_proc, 'q' : quit, 'x' : x_proc, 489 '=' : zoom, '+' : zoom, '-' : zoom, 490 '?' : help_proc 491 } 492 493def KeyPress(event): 494 if SC.extraX < 0: 495 # compute space outside the Canvas (scroll bars, and bottom statusbar) 496 SC.extraX = int(SC.sbarY["width"]) 497 SC.extraY = 2 * int(SC.sbarX["width"]) 498 499 if event.keysym != "Return" and event.keysym != "Escape": 500 if KeyProcs.has_key(event.char): 501 KeyProcs[event.char](SC, event.char) 502 else: 503 print "unknown key function: keysym='%s'" % event.keysym 504 print HelpMsg 505 506def advance_diff_in_prev_line(diffs_prev_line, index_in_prev_line): 507 if index_in_prev_line < len(diffs_prev_line): 508 prev_diff = diffs_prev_line[index_in_prev_line] 509 else: 510 prev_diff = (99999, 99999, -1) # dummy value starts past max line length 511 return prev_diff 512 513 514# Initialize the Scrolled Canvas 515SC = ScrolledCanvas() 516SC.highlight = 0 517 518# get file names, open the images 519if len(sys.argv) < 3: 520 open_dlg_pop() 521else: 522 baseline_filename = sys.argv[1] 523 compare_filename = sys.argv[2] 524 open_files() 525 526SC.bind_all("<Key>", KeyPress) 527 528if __name__ == '__main__': SC.mainloop() 529