os/graphics/graphicsdeviceinterface/directgdi/test/scripts/refimage.py
author sl
Tue, 10 Jun 2014 14:32:02 +0200
changeset 1 260cb5ec6c19
permissions -rw-r--r--
Update contrib.
     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