graphicsdeviceinterface/directgdi/test/scripts/refimage.py
changeset 0 5d03bc08d59c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graphicsdeviceinterface/directgdi/test/scripts/refimage.py	Tue Feb 02 01:47:50 2010 +0200
@@ -0,0 +1,347 @@
+# Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
+# All rights reserved.
+# This component and the accompanying materials are made available
+# under the terms of "Eclipse Public License v1.0"
+# which accompanies this distribution, and is available
+# at the URL "http://www.eclipse.org/legal/epl-v10.html".
+#
+# Initial Contributors:
+# Nokia Corporation - initial contribution.
+#
+# Contributors:
+#
+# Description:
+#
+
+
+"""
+Reference Image
+
+Class representing test images and results of comparing it against reference images.
+
+"""
+
+import os
+import os.path
+from string import *
+from PIL import Image, ImageChops, ImageOps, ImageStat, ImageFilter
+from sets import Set
+import shutil
+
+# Relative path for reference images
+KRefPath = "\\ref\\"
+
+# Relative path for test images
+KTestPath = "\\test\\"
+
+# Compare test with reference images by pixel and pyramid difference; generate diff images
+class RefImage:
+    # Change the value to tune the passing limit for pyramid diff
+    PYRAMID_PASS_LIMIT = 10
+    # Change the value to tune the passing limit for pixel diff
+    PIXEL_DIFF_PASS_LIMIT = 2
+
+    # These are the types of differences that can be tested.
+    PIXEL_DIFF = 1
+    DIFF_SCORE = 2
+    PYRAMID_DIFF = 3
+
+    # @param aRefFile The reference images
+    # @param aTestFile The test images
+    # @param aBaseDir The base directory of reference and test images
+    # @param aSource The distinctive part of the expected diff image name
+    def __init__(self, aRefFile, aTestFile, aBaseDir, aSource):
+        self.source     = aSource
+        self.refFile    = aRefFile
+        self.testFile   = aTestFile
+        self.baseDir    = aBaseDir
+        self.targetImage  = os.path.basename(aRefFile)
+        self.maxDiff      = -1
+        self.diffScore    = -1
+        self.pyramidDiffs = None
+        self.refImageCache  = None
+        self.testFileCache  = None
+        self.cachedTestFile = None
+        self.cachedDiff   = None
+        self.diffImages   = None
+        self.diffsInUse   = Set([self.PIXEL_DIFF,self.PYRAMID_DIFF])
+
+    # Read in reference images
+    def _getImage(self):
+        if not self.refImageCache:
+            self.refImageCache = Image.open(self.baseDir + KRefPath + self.refFile)
+            print "ref image: ", self.refFile
+        return self.refImageCache
+
+    # Read in test images
+    def _getTestee(self):
+        if not self.testFileCache:
+            self.testFileCache = Image.open(self.baseDir + KTestPath + self.testFile)
+            print "test image: ", self.testFile
+        return self.testFileCache  
+
+    # Get absolute value of the difference between test and reference images
+    def _getDiff(self):
+         self.cachedDiff = ImageChops.difference(self._getImage(), self._getTestee())
+         return self.cachedDiff
+
+    # Get pyramid levels of an image.
+    # Returns a set of successively low-pass filtered images, resized by 1/2, 1/4, 1/8 respectivly.
+    # @param aImage The image as the source to get pyramid levels
+    # @return A set of 3 images scaled at 1/2, 1/4, and 1/8
+    def _genPyramidLevels(self, aImage):
+        # Create a 3X3 convolution kernel.
+        # Gaussian image smoothing kernel, approximated by 3x3 convolution filter.
+        # A convolution is a weighted average of all the pixels in some neighborhood of a given pixel.
+        # The convolution kernel values are the weights for the average.
+        kernel = ImageFilter.Kernel((3, 3), [.75, .9, .75, .9, 1, .9, .75, .9, .75])
+        source = aImage
+        res = []
+        while len(res) < 3:
+            srcSize = source.size
+            # Mirror borders.
+            temp = Image.new("RGBA", (srcSize[0]+2, srcSize[1]+2))
+            temp.paste(source, (1, 1))
+            
+            # .crop returns a rectangular region from the current image. Passed: left, upper, right, and lower pixel coordinate.
+            # .paste to upper-left corner.
+            # left, top, right, bottom
+            # Add a one pixel border around the image, so the center of the 3x3 convolution filter starts on the corner pixel of the image. 
+            temp.paste(source.crop((1, 0, 1, srcSize[1]-1)), (0, 1))
+            temp.paste(source.crop((0, 1, srcSize[1]-1, 1)), (1, 0))
+            temp.paste(source.crop((srcSize[0]-2, 0, srcSize[0]-2, srcSize[1]-1)), (srcSize[0]+1, 1))
+            temp.paste(source.crop((0, srcSize[1]-2, srcSize[1]-1, srcSize[1]-2)), (1, srcSize[1]+1))
+            
+            # Resize the filtered image to 0.5 size, via. 2x2 linear interpolation.
+            filtered = temp.filter(kernel).crop((1, 1, srcSize[0], srcSize[1])).resize((srcSize[0]/2, srcSize[1]/2), Image.BILINEAR)
+            source = filtered
+            res.append(filtered)
+        return res
+
+    # Compute difference values between test and reference images
+    #
+    # - Generate mask image (3x3 max/min differences)
+    # - Generate pyramid reference images (1/2, 1/4, 1/8 low-pass filtered and scaled).
+    # - Generate pyramid test      images (1/2, 1/4, 1/8 low-pass filtered and scaled).
+    # - Generate pyramid mask      images (1/2, 1/4, 1/8 low-pass filtered and scaled).
+    # - Weight the mask according to level.
+    # - For each level:
+    #   - Get absolute difference image between reference and test.
+    #   - Multiply absolute difference with inverted mask at that level
+    # - Take maximum pixel value at each level as the pyramid difference.
+    #
+    # See: http://www.pythonware.com/library/pil/handbook/index.htm
+    #
+    def compPyramidDiff(self):
+        ref = self._getImage()
+        testee = self._getTestee()
+        #if testee.size != ref.size:
+        #	file.write("WARNING: The reference image has different dimension from the testee image")
+        
+        # maskImage is the difference between min and max pixels within a 3x3 pixel environment in the reference image.
+        maskImage = ImageChops.difference(ref.filter(ImageFilter.MinFilter(3)), ref.filter(ImageFilter.MaxFilter(3)))
+  
+        # generate low-pass filtered pyramid images.
+        refLevels = self._genPyramidLevels(ref)
+        refL1 = refLevels[0]
+        refL2 = refLevels[1]
+        refL3 = refLevels[2]
+        testLevels = self._genPyramidLevels(testee)
+        testL1 = testLevels[0]
+        testL2 = testLevels[1]
+        testL3 = testLevels[2]
+        maskLevels = self._genPyramidLevels(maskImage)
+
+        # Apply weighting factor to masks at levels 1, 2, and 3.
+        maskL1 = Image.eval(maskLevels[0], lambda x: 5*x)
+        maskL2 = Image.eval(maskLevels[1], lambda x: 3*x)
+        maskL3 = Image.eval(maskLevels[2], lambda x: 2*x)
+
+        # Generate a pixel difference image between reference and test.
+        # Multiply the difference image with the inverse of the mask.
+        #   Mask inverse (out = MAX - image):
+        #     So, areas of regional (3x3) similarity thend to MAX and differences tend to 0x00.
+        #   Multiply (out = image1 * image2 / MAX:
+        #     Superimposes two images on top of each other. If you multiply an image with a solid black image,
+        #     the result is black. If you multiply with a solid white image, the image is unaffected.
+        #   This has the effect of accentuating any test/reference differences where there is a small
+        #   regional difference in the reference image.
+        diffL1 = ImageChops.difference(refL1, testL1)
+        diffL1 = ImageChops.multiply(diffL1, ImageChops.invert(maskL1))
+        diffL2 = ImageChops.difference(refL2, testL2)
+        diffL2 = ImageChops.multiply(diffL2, ImageChops.invert(maskL2))
+        diffL3 = ImageChops.difference(refL3, testL3)
+        diffL3 = ImageChops.multiply(diffL3, ImageChops.invert(maskL3))
+        
+        # So now the difference images are a grey-scale image that are brighter where differences
+        # between the reference and test images were detected in regions where there was little 
+        # variability in the reference image.
+
+        # Get maxima for all bands at each pyramid level, and take the maximum value as the pyramid value.
+        # stat.extrema (Get min/max values for each band in the image).
+
+        self.pyramidDiffs = [
+            max(map(lambda (x): x[1], ImageStat.Stat(diffL1).extrema)),
+            max(map(lambda (x): x[1], ImageStat.Stat(diffL2).extrema)),
+            max(map(lambda (x): x[1], ImageStat.Stat(diffL3).extrema))
+        ]
+        print "self.pyramidDiffs = ", self.pyramidDiffs
+
+    # Compute max diff of pixel difference  
+    def compMaxDiff(self):
+        self.maxDiff = max(map(lambda (x): x[1], ImageStat.Stat(self._getDiff()).extrema))
+
+    # Compute diff score
+    # @param file A log file to store error messages
+    def compDiffScore(self, file):
+        self.diffScore = 0
+        ref = self._getImage()
+        testee = self._getTestee()
+        if testee.size != ref.size:
+            file.write("WARNING: Reference image from source has different dimension than the testee image")
+            #raise ValueError("Reference image from source has different dimension than the testee image")
+        # If a difference exists...
+        if self.maxDiff != 0:      
+            # Filter images for min and max pixel (dark and light) values within 5x5 environment.                  
+            refMin = ref.filter(ImageFilter.MinFilter(5))
+            refMax = ref.filter(ImageFilter.MaxFilter(5))
+            testMin = testee.filter(ImageFilter.MinFilter(5))
+            testMax = testee.filter(ImageFilter.MaxFilter(5))
+            
+            # make the min and max filter images a bit darker and lighter, respectively.
+            refMin = Image.eval(refMin, lambda x: x - 4)
+            refMax = Image.eval(refMax, lambda x: x + 4)
+            testMin = Image.eval(testMin, lambda x: x - 4)
+            testMax = Image.eval(testMax, lambda x: x + 4)
+
+            refRefHist = ref.histogram()
+            testRefHist = testee.histogram()
+
+            # Calculate difference score.
+            
+            # Check for darkness in reference image.
+            # Generate an image of the darkest pixels when comparing the 5x5 max filtered and lightened reference image against the test image.
+            # If the pixel colour histogram of the generated image is different from the test image histogram, increase the difference score.
+            if (ImageChops.darker(refMax, testee).histogram() != testRefHist):
+                self.diffScore += 1
+            
+            # Check for lightness in reference image.
+            if (ImageChops.lighter(refMin, testee).histogram() != testRefHist):
+                self.diffScore += 1
+            
+            # Check for darkness in test image.
+            if (ImageChops.darker(testMax, ref).histogram() != refRefHist):
+                self.diffScore += 1
+            
+            #  Check for lightness in test image.
+            if (ImageChops.lighter(testMin, ref).histogram() != refRefHist):
+                self.diffScore += 1
+
+        print "self.diffScore: ", self.diffScore
+
+    # Generate test results
+    # @param file A log file to store error messages
+    def pyramidValue (self):
+      return self.pyramidDiffs[2]
+
+    def passed(self, file, aThresholdValue):
+        if aThresholdValue == -1:
+            aThresholdValue = self.PYRAMID_PASS_LIMIT
+         
+        if self.pyramidDiffs:
+            return self.pyramidValue() <= aThresholdValue
+        elif self.maxDiff >= 0:
+            return self.maxDiff <= self.PIXEL_DIFF_PASS_LIMIT
+        elif self.maxDiff < 0:
+            warningMsg = "WARNING: Differences were not computed for the test image " + self.testFile + " against its reference image<br>"
+            print warningMsg;
+            if file: file.write(warningMsg);
+            return True
+        else:
+            assert False
+            return False
+
+
+    # Make diff images
+    # @param aDestDir
+    def makeDiffImages(self, aDestDir):
+        diffBands = list(self._getDiff().split())
+        assert (len(diffBands) == 3 or len(diffBands) == 1)
+        diffs = {}
+        baseDiffName = "Diff_" + self.source +  "_" + self.targetImage
+        # Invert the diffs.
+        for i in range(len(diffBands)):
+        #for i in range(4):
+	        diffBands[i] = ImageChops.invert(diffBands[i])
+
+        temp = ["R", "G", "B"]
+        for i in range(len(diffBands)):
+	        name = temp[i] + baseDiffName
+        # Highlight the differing pixels
+        if not self.PYRAMID_DIFF in self.diffsInUse and not self.DIFF_SCORE in self.diffsInUse:
+           	diffBands[i] = Image.eval(diffBands[i], lambda x: (x / (255 - self.PIXEL_DIFF_PASS_LIMIT)) * 255)
+		# Following line commented as we don't need to save bitmaps for the separate R,G or B channels.
+        #diffBands[i].save(aDestDir + name, "BMP")
+        diffs[temp[i]] = name
+            
+        if len(diffBands) == 3:
+        	rgbDiff = ImageChops.darker(diffBands[0], ImageChops.darker(diffBands[1], diffBands[2]))
+        else:
+        	rgbDiff = diffBands[0]
+            
+        rgbDiffName = "RGB" + baseDiffName 
+        rgbDiff.save(aDestDir + rgbDiffName, "BMP")
+        diffs["RGB"] = rgbDiffName
+                    
+    	self.diffImages = diffs
+    	return diffs
+
+
+    # Print test results to command line    
+    # @param file A log file to store error messages
+    def printResult(self, file, aThresholdValue):
+        print "test result: ", self.passed(file, aThresholdValue), "maxDiff: ", self.maxDiff
+
+    # Get test results
+    # @param file A log file to store error messages
+    def getResult(self, file, aThresholdValue):
+        return self.passed(file, aThresholdValue);
+
+    # Get current puramid result value.
+    def getPyramidResultValue(self):
+        if self.pyramidDiffs:
+            return self.pyramidValue()
+        return 255
+
+    # Get diff images
+    def getDiffImages(self):
+        assert self.diffImages != None
+        return self.diffImages
+
+    # Disable a diff test
+    # @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 
+    def disableDiff(self, diff):
+        self.diffsInUse.discard(diff)
+
+    # Enabld a diff test
+    # @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
+    def enableDiff(self, diff):
+        self.diffsInUse.add(diff)
+
+    # Set diffs
+    # @param diffs Either self.PIXEL_DIFF,self.PYRAMID_DIFF or both when the tester wants to set either or both of the diff tests
+    def setDiffs(self, diffs):
+        self.diffsInUse = (diffs)
+
+    # Compute difference according to the values in self.diffsInUse
+    # @param file A log file to store error messages
+    def computeDifferences(self, file):
+        if self.PIXEL_DIFF in self.diffsInUse:
+            self.compMaxDiff()
+        if self.DIFF_SCORE in self.diffsInUse:
+            self.compDiffScore(file)
+        if self.PYRAMID_DIFF in self.diffsInUse:
+            self.compPyramidDiff()
+
+
+