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