graphicsdeviceinterface/directgdi/test/scripts/refimage.py
author Faisal Memon <faisal.memon@nokia.com>
Fri, 25 Jun 2010 17:49:58 +0100
branchEGL_MERGE
changeset 105 158b2308cc08
parent 0 5d03bc08d59c
permissions -rw-r--r--
Fix def files so that the implementation agnostic interface definition has no non-standards defined entry points, and change the eglrefimpl specific implementation to place its private entry points high up in the ordinal order space in the implementation region, not the standards based entrypoints region.

# 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()