Update contrib.
1 # Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
3 # This component and the accompanying materials are made available
4 # under the terms of "Eclipse Public License v1.0"
5 # which accompanies this distribution, and is available
6 # at the URL "http://www.eclipse.org/legal/epl-v10.html".
8 # Initial Contributors:
9 # Nokia Corporation - initial contribution.
20 Class representing test images and results of comparing it against reference images.
27 from PIL import Image, ImageChops, ImageOps, ImageStat, ImageFilter
31 # Relative path for reference images
34 # Relative path for test images
35 KTestPath = "\\test\\"
37 # Compare test with reference images by pixel and pyramid difference; generate diff images
39 # Change the value to tune the passing limit for pyramid diff
40 PYRAMID_PASS_LIMIT = 10
41 # Change the value to tune the passing limit for pixel diff
42 PIXEL_DIFF_PASS_LIMIT = 2
44 # These are the types of differences that can be tested.
49 # @param aRefFile The reference images
50 # @param aTestFile The test images
51 # @param aBaseDir The base directory of reference and test images
52 # @param aSource The distinctive part of the expected diff image name
53 def __init__(self, aRefFile, aTestFile, aBaseDir, aSource):
55 self.refFile = aRefFile
56 self.testFile = aTestFile
57 self.baseDir = aBaseDir
58 self.targetImage = os.path.basename(aRefFile)
61 self.pyramidDiffs = None
62 self.refImageCache = None
63 self.testFileCache = None
64 self.cachedTestFile = None
65 self.cachedDiff = None
66 self.diffImages = None
67 self.diffsInUse = Set([self.PIXEL_DIFF,self.PYRAMID_DIFF])
69 # Read in reference images
71 if not self.refImageCache:
72 self.refImageCache = Image.open(self.baseDir + KRefPath + self.refFile)
73 print "ref image: ", self.refFile
74 return self.refImageCache
78 if not self.testFileCache:
79 self.testFileCache = Image.open(self.baseDir + KTestPath + self.testFile)
80 print "test image: ", self.testFile
81 return self.testFileCache
83 # Get absolute value of the difference between test and reference images
85 self.cachedDiff = ImageChops.difference(self._getImage(), self._getTestee())
86 return self.cachedDiff
88 # Get pyramid levels of an image.
89 # Returns a set of successively low-pass filtered images, resized by 1/2, 1/4, 1/8 respectivly.
90 # @param aImage The image as the source to get pyramid levels
91 # @return A set of 3 images scaled at 1/2, 1/4, and 1/8
92 def _genPyramidLevels(self, aImage):
93 # Create a 3X3 convolution kernel.
94 # Gaussian image smoothing kernel, approximated by 3x3 convolution filter.
95 # A convolution is a weighted average of all the pixels in some neighborhood of a given pixel.
96 # The convolution kernel values are the weights for the average.
97 kernel = ImageFilter.Kernel((3, 3), [.75, .9, .75, .9, 1, .9, .75, .9, .75])
101 srcSize = source.size
103 temp = Image.new("RGBA", (srcSize[0]+2, srcSize[1]+2))
104 temp.paste(source, (1, 1))
106 # .crop returns a rectangular region from the current image. Passed: left, upper, right, and lower pixel coordinate.
107 # .paste to upper-left corner.
108 # left, top, right, bottom
109 # Add a one pixel border around the image, so the center of the 3x3 convolution filter starts on the corner pixel of the image.
110 temp.paste(source.crop((1, 0, 1, srcSize[1]-1)), (0, 1))
111 temp.paste(source.crop((0, 1, srcSize[1]-1, 1)), (1, 0))
112 temp.paste(source.crop((srcSize[0]-2, 0, srcSize[0]-2, srcSize[1]-1)), (srcSize[0]+1, 1))
113 temp.paste(source.crop((0, srcSize[1]-2, srcSize[1]-1, srcSize[1]-2)), (1, srcSize[1]+1))
115 # Resize the filtered image to 0.5 size, via. 2x2 linear interpolation.
116 filtered = temp.filter(kernel).crop((1, 1, srcSize[0], srcSize[1])).resize((srcSize[0]/2, srcSize[1]/2), Image.BILINEAR)
121 # Compute difference values between test and reference images
123 # - Generate mask image (3x3 max/min differences)
124 # - Generate pyramid reference images (1/2, 1/4, 1/8 low-pass filtered and scaled).
125 # - Generate pyramid test images (1/2, 1/4, 1/8 low-pass filtered and scaled).
126 # - Generate pyramid mask images (1/2, 1/4, 1/8 low-pass filtered and scaled).
127 # - Weight the mask according to level.
129 # - Get absolute difference image between reference and test.
130 # - Multiply absolute difference with inverted mask at that level
131 # - Take maximum pixel value at each level as the pyramid difference.
133 # See: http://www.pythonware.com/library/pil/handbook/index.htm
135 def compPyramidDiff(self):
136 ref = self._getImage()
137 testee = self._getTestee()
138 #if testee.size != ref.size:
139 # file.write("WARNING: The reference image has different dimension from the testee image")
141 # maskImage is the difference between min and max pixels within a 3x3 pixel environment in the reference image.
142 maskImage = ImageChops.difference(ref.filter(ImageFilter.MinFilter(3)), ref.filter(ImageFilter.MaxFilter(3)))
144 # generate low-pass filtered pyramid images.
145 refLevels = self._genPyramidLevels(ref)
149 testLevels = self._genPyramidLevels(testee)
150 testL1 = testLevels[0]
151 testL2 = testLevels[1]
152 testL3 = testLevels[2]
153 maskLevels = self._genPyramidLevels(maskImage)
155 # Apply weighting factor to masks at levels 1, 2, and 3.
156 maskL1 = Image.eval(maskLevels[0], lambda x: 5*x)
157 maskL2 = Image.eval(maskLevels[1], lambda x: 3*x)
158 maskL3 = Image.eval(maskLevels[2], lambda x: 2*x)
160 # Generate a pixel difference image between reference and test.
161 # Multiply the difference image with the inverse of the mask.
162 # Mask inverse (out = MAX - image):
163 # So, areas of regional (3x3) similarity thend to MAX and differences tend to 0x00.
164 # Multiply (out = image1 * image2 / MAX:
165 # Superimposes two images on top of each other. If you multiply an image with a solid black image,
166 # the result is black. If you multiply with a solid white image, the image is unaffected.
167 # This has the effect of accentuating any test/reference differences where there is a small
168 # regional difference in the reference image.
169 diffL1 = ImageChops.difference(refL1, testL1)
170 diffL1 = ImageChops.multiply(diffL1, ImageChops.invert(maskL1))
171 diffL2 = ImageChops.difference(refL2, testL2)
172 diffL2 = ImageChops.multiply(diffL2, ImageChops.invert(maskL2))
173 diffL3 = ImageChops.difference(refL3, testL3)
174 diffL3 = ImageChops.multiply(diffL3, ImageChops.invert(maskL3))
176 # So now the difference images are a grey-scale image that are brighter where differences
177 # between the reference and test images were detected in regions where there was little
178 # variability in the reference image.
180 # Get maxima for all bands at each pyramid level, and take the maximum value as the pyramid value.
181 # stat.extrema (Get min/max values for each band in the image).
183 self.pyramidDiffs = [
184 max(map(lambda (x): x[1], ImageStat.Stat(diffL1).extrema)),
185 max(map(lambda (x): x[1], ImageStat.Stat(diffL2).extrema)),
186 max(map(lambda (x): x[1], ImageStat.Stat(diffL3).extrema))
188 print "self.pyramidDiffs = ", self.pyramidDiffs
190 # Compute max diff of pixel difference
191 def compMaxDiff(self):
192 self.maxDiff = max(map(lambda (x): x[1], ImageStat.Stat(self._getDiff()).extrema))
195 # @param file A log file to store error messages
196 def compDiffScore(self, file):
198 ref = self._getImage()
199 testee = self._getTestee()
200 if testee.size != ref.size:
201 file.write("WARNING: Reference image from source has different dimension than the testee image")
202 #raise ValueError("Reference image from source has different dimension than the testee image")
203 # If a difference exists...
204 if self.maxDiff != 0:
205 # Filter images for min and max pixel (dark and light) values within 5x5 environment.
206 refMin = ref.filter(ImageFilter.MinFilter(5))
207 refMax = ref.filter(ImageFilter.MaxFilter(5))
208 testMin = testee.filter(ImageFilter.MinFilter(5))
209 testMax = testee.filter(ImageFilter.MaxFilter(5))
211 # make the min and max filter images a bit darker and lighter, respectively.
212 refMin = Image.eval(refMin, lambda x: x - 4)
213 refMax = Image.eval(refMax, lambda x: x + 4)
214 testMin = Image.eval(testMin, lambda x: x - 4)
215 testMax = Image.eval(testMax, lambda x: x + 4)
217 refRefHist = ref.histogram()
218 testRefHist = testee.histogram()
220 # Calculate difference score.
222 # Check for darkness in reference image.
223 # Generate an image of the darkest pixels when comparing the 5x5 max filtered and lightened reference image against the test image.
224 # If the pixel colour histogram of the generated image is different from the test image histogram, increase the difference score.
225 if (ImageChops.darker(refMax, testee).histogram() != testRefHist):
228 # Check for lightness in reference image.
229 if (ImageChops.lighter(refMin, testee).histogram() != testRefHist):
232 # Check for darkness in test image.
233 if (ImageChops.darker(testMax, ref).histogram() != refRefHist):
236 # Check for lightness in test image.
237 if (ImageChops.lighter(testMin, ref).histogram() != refRefHist):
240 print "self.diffScore: ", self.diffScore
242 # Generate test results
243 # @param file A log file to store error messages
244 def pyramidValue (self):
245 return self.pyramidDiffs[2]
247 def passed(self, file, aThresholdValue):
248 if aThresholdValue == -1:
249 aThresholdValue = self.PYRAMID_PASS_LIMIT
251 if self.pyramidDiffs:
252 return self.pyramidValue() <= aThresholdValue
253 elif self.maxDiff >= 0:
254 return self.maxDiff <= self.PIXEL_DIFF_PASS_LIMIT
255 elif self.maxDiff < 0:
256 warningMsg = "WARNING: Differences were not computed for the test image " + self.testFile + " against its reference image<br>"
258 if file: file.write(warningMsg);
267 def makeDiffImages(self, aDestDir):
268 diffBands = list(self._getDiff().split())
269 assert (len(diffBands) == 3 or len(diffBands) == 1)
271 baseDiffName = "Diff_" + self.source + "_" + self.targetImage
273 for i in range(len(diffBands)):
275 diffBands[i] = ImageChops.invert(diffBands[i])
277 temp = ["R", "G", "B"]
278 for i in range(len(diffBands)):
279 name = temp[i] + baseDiffName
280 # Highlight the differing pixels
281 if not self.PYRAMID_DIFF in self.diffsInUse and not self.DIFF_SCORE in self.diffsInUse:
282 diffBands[i] = Image.eval(diffBands[i], lambda x: (x / (255 - self.PIXEL_DIFF_PASS_LIMIT)) * 255)
283 # Following line commented as we don't need to save bitmaps for the separate R,G or B channels.
284 #diffBands[i].save(aDestDir + name, "BMP")
285 diffs[temp[i]] = name
287 if len(diffBands) == 3:
288 rgbDiff = ImageChops.darker(diffBands[0], ImageChops.darker(diffBands[1], diffBands[2]))
290 rgbDiff = diffBands[0]
292 rgbDiffName = "RGB" + baseDiffName
293 rgbDiff.save(aDestDir + rgbDiffName, "BMP")
294 diffs["RGB"] = rgbDiffName
296 self.diffImages = diffs
300 # Print test results to command line
301 # @param file A log file to store error messages
302 def printResult(self, file, aThresholdValue):
303 print "test result: ", self.passed(file, aThresholdValue), "maxDiff: ", self.maxDiff
306 # @param file A log file to store error messages
307 def getResult(self, file, aThresholdValue):
308 return self.passed(file, aThresholdValue);
310 # Get current puramid result value.
311 def getPyramidResultValue(self):
312 if self.pyramidDiffs:
313 return self.pyramidValue()
317 def getDiffImages(self):
318 assert self.diffImages != None
319 return self.diffImages
321 # Disable a diff test
322 # @param diff Holds either self.PIXEL_DIFF,self.PYRAMID_DIFF or both when the tester wants to disable either or both of the diff tests
323 def disableDiff(self, diff):
324 self.diffsInUse.discard(diff)
327 # @param diff Holds either self.PIXEL_DIFF,self.PYRAMID_DIFF or both when the tester wants to enable either or both of the diff tests
328 def enableDiff(self, diff):
329 self.diffsInUse.add(diff)
332 # @param diffs Either self.PIXEL_DIFF,self.PYRAMID_DIFF or both when the tester wants to set either or both of the diff tests
333 def setDiffs(self, diffs):
334 self.diffsInUse = (diffs)
336 # Compute difference according to the values in self.diffsInUse
337 # @param file A log file to store error messages
338 def computeDifferences(self, file):
339 if self.PIXEL_DIFF in self.diffsInUse:
341 if self.DIFF_SCORE in self.diffsInUse:
342 self.compDiffScore(file)
343 if self.PYRAMID_DIFF in self.diffsInUse:
344 self.compPyramidDiff()