graphicsdeviceinterface/directgdi/test/scripts/refimage.py
changeset 0 5d03bc08d59c
equal deleted inserted replaced
-1:000000000000 0:5d03bc08d59c
       
     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