|
1 # Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
2 # All rights reserved. |
|
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". |
|
7 # |
|
8 # Initial Contributors: |
|
9 # Nokia Corporation - initial contribution. |
|
10 # |
|
11 # Contributors: |
|
12 # |
|
13 # Description: |
|
14 # |
|
15 |
|
16 |
|
17 """ |
|
18 Reference Image |
|
19 |
|
20 Class representing test images and results of comparing it against reference images. |
|
21 |
|
22 """ |
|
23 |
|
24 import os |
|
25 import os.path |
|
26 from string import * |
|
27 from PIL import Image, ImageChops, ImageOps, ImageStat, ImageFilter |
|
28 from sets import Set |
|
29 import shutil |
|
30 |
|
31 # Relative path for reference images |
|
32 KRefPath = "\\ref\\" |
|
33 |
|
34 # Relative path for test images |
|
35 KTestPath = "\\test\\" |
|
36 |
|
37 # Compare test with reference images by pixel and pyramid difference; generate diff images |
|
38 class RefImage: |
|
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 |
|
43 |
|
44 # These are the types of differences that can be tested. |
|
45 PIXEL_DIFF = 1 |
|
46 DIFF_SCORE = 2 |
|
47 PYRAMID_DIFF = 3 |
|
48 |
|
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): |
|
54 self.source = aSource |
|
55 self.refFile = aRefFile |
|
56 self.testFile = aTestFile |
|
57 self.baseDir = aBaseDir |
|
58 self.targetImage = os.path.basename(aRefFile) |
|
59 self.maxDiff = -1 |
|
60 self.diffScore = -1 |
|
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]) |
|
68 |
|
69 # Read in reference images |
|
70 def _getImage(self): |
|
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 |
|
75 |
|
76 # Read in test images |
|
77 def _getTestee(self): |
|
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 |
|
82 |
|
83 # Get absolute value of the difference between test and reference images |
|
84 def _getDiff(self): |
|
85 self.cachedDiff = ImageChops.difference(self._getImage(), self._getTestee()) |
|
86 return self.cachedDiff |
|
87 |
|
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]) |
|
98 source = aImage |
|
99 res = [] |
|
100 while len(res) < 3: |
|
101 srcSize = source.size |
|
102 # Mirror borders. |
|
103 temp = Image.new("RGBA", (srcSize[0]+2, srcSize[1]+2)) |
|
104 temp.paste(source, (1, 1)) |
|
105 |
|
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)) |
|
114 |
|
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) |
|
117 source = filtered |
|
118 res.append(filtered) |
|
119 return res |
|
120 |
|
121 # Compute difference values between test and reference images |
|
122 # |
|
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. |
|
128 # - For each 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. |
|
132 # |
|
133 # See: http://www.pythonware.com/library/pil/handbook/index.htm |
|
134 # |
|
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") |
|
140 |
|
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))) |
|
143 |
|
144 # generate low-pass filtered pyramid images. |
|
145 refLevels = self._genPyramidLevels(ref) |
|
146 refL1 = refLevels[0] |
|
147 refL2 = refLevels[1] |
|
148 refL3 = refLevels[2] |
|
149 testLevels = self._genPyramidLevels(testee) |
|
150 testL1 = testLevels[0] |
|
151 testL2 = testLevels[1] |
|
152 testL3 = testLevels[2] |
|
153 maskLevels = self._genPyramidLevels(maskImage) |
|
154 |
|
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) |
|
159 |
|
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)) |
|
175 |
|
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. |
|
179 |
|
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). |
|
182 |
|
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)) |
|
187 ] |
|
188 print "self.pyramidDiffs = ", self.pyramidDiffs |
|
189 |
|
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)) |
|
193 |
|
194 # Compute diff score |
|
195 # @param file A log file to store error messages |
|
196 def compDiffScore(self, file): |
|
197 self.diffScore = 0 |
|
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)) |
|
210 |
|
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) |
|
216 |
|
217 refRefHist = ref.histogram() |
|
218 testRefHist = testee.histogram() |
|
219 |
|
220 # Calculate difference score. |
|
221 |
|
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): |
|
226 self.diffScore += 1 |
|
227 |
|
228 # Check for lightness in reference image. |
|
229 if (ImageChops.lighter(refMin, testee).histogram() != testRefHist): |
|
230 self.diffScore += 1 |
|
231 |
|
232 # Check for darkness in test image. |
|
233 if (ImageChops.darker(testMax, ref).histogram() != refRefHist): |
|
234 self.diffScore += 1 |
|
235 |
|
236 # Check for lightness in test image. |
|
237 if (ImageChops.lighter(testMin, ref).histogram() != refRefHist): |
|
238 self.diffScore += 1 |
|
239 |
|
240 print "self.diffScore: ", self.diffScore |
|
241 |
|
242 # Generate test results |
|
243 # @param file A log file to store error messages |
|
244 def pyramidValue (self): |
|
245 return self.pyramidDiffs[2] |
|
246 |
|
247 def passed(self, file, aThresholdValue): |
|
248 if aThresholdValue == -1: |
|
249 aThresholdValue = self.PYRAMID_PASS_LIMIT |
|
250 |
|
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>" |
|
257 print warningMsg; |
|
258 if file: file.write(warningMsg); |
|
259 return True |
|
260 else: |
|
261 assert False |
|
262 return False |
|
263 |
|
264 |
|
265 # Make diff images |
|
266 # @param aDestDir |
|
267 def makeDiffImages(self, aDestDir): |
|
268 diffBands = list(self._getDiff().split()) |
|
269 assert (len(diffBands) == 3 or len(diffBands) == 1) |
|
270 diffs = {} |
|
271 baseDiffName = "Diff_" + self.source + "_" + self.targetImage |
|
272 # Invert the diffs. |
|
273 for i in range(len(diffBands)): |
|
274 #for i in range(4): |
|
275 diffBands[i] = ImageChops.invert(diffBands[i]) |
|
276 |
|
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 |
|
286 |
|
287 if len(diffBands) == 3: |
|
288 rgbDiff = ImageChops.darker(diffBands[0], ImageChops.darker(diffBands[1], diffBands[2])) |
|
289 else: |
|
290 rgbDiff = diffBands[0] |
|
291 |
|
292 rgbDiffName = "RGB" + baseDiffName |
|
293 rgbDiff.save(aDestDir + rgbDiffName, "BMP") |
|
294 diffs["RGB"] = rgbDiffName |
|
295 |
|
296 self.diffImages = diffs |
|
297 return diffs |
|
298 |
|
299 |
|
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 |
|
304 |
|
305 # Get test results |
|
306 # @param file A log file to store error messages |
|
307 def getResult(self, file, aThresholdValue): |
|
308 return self.passed(file, aThresholdValue); |
|
309 |
|
310 # Get current puramid result value. |
|
311 def getPyramidResultValue(self): |
|
312 if self.pyramidDiffs: |
|
313 return self.pyramidValue() |
|
314 return 255 |
|
315 |
|
316 # Get diff images |
|
317 def getDiffImages(self): |
|
318 assert self.diffImages != None |
|
319 return self.diffImages |
|
320 |
|
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) |
|
325 |
|
326 # Enabld a diff test |
|
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) |
|
330 |
|
331 # Set diffs |
|
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) |
|
335 |
|
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: |
|
340 self.compMaxDiff() |
|
341 if self.DIFF_SCORE in self.diffsInUse: |
|
342 self.compDiffScore(file) |
|
343 if self.PYRAMID_DIFF in self.diffsInUse: |
|
344 self.compPyramidDiff() |
|
345 |
|
346 |
|
347 |