Astro logo

17. May 2019

Visual regression testing

Visual regression testing

Visual regression testing is a good way to detect visual deviations between software releases. Screenshotting the visual output of an application along each release can be automated with a CI pipeline. The actual test logic can be done in multiple ways.

I find Python programming language very convenient for detecting differences between images. PIL Python Image Library has functions for asserting stats but this gives you just a numeric score that can be translated to a yes/no answer based on a certain threshold. However, a test result can be much more. For example an image representing the visual deviation between the two screenshots.

Here is a little script to compare images visually.

#!/usr/bin/env python3.6
from PIL import Image, ImageChops, ImageDraw
im1 = Image.open("Capture1.png")
im2 = Image.open("Capture2.png")
diff = ImageChops.difference(im2, im1)
left,upper,right,lower = diff.getbbox()
draw = ImageDraw.Draw(im1)
draw.rectangle((left,upper,right,lower), outline = "red")
im1.save('difference.png')

This works even better when there is only a small alignment deviation in a certain area of the screen.

#!/usr/bin/env python3.6

# Script for visual regression testing
from PIL import Image, ImageChops, ImageDraw
im2 = Image.open("Capture2.png")
im3 = Image.open("Capture3.png")

# Detect difference
diff = ImageChops.difference(im2, im3)

# Draw bounding box
left,upper,right,lower = diff.getbbox()
draw = ImageDraw.Draw(im2)
draw.rectangle((left,upper,right,lower), outline = "red")

# Overlap and blend the images 
blended = Image.blend(im2, im3, alpha=0.5)
blended.save("blended.png")

And here is a third way. This shows the areas with differences better when there are multiple differences.

#!/usr/bin/env python3.6

# Script for visual regression testing
from PIL import Image, ImageDraw

class ScreenAnalysis:

    def __init__(self):
        self.analyze()

    def analyze(self):
        capture2 = Image.open("Capture2.png")
        capture4 = Image.open("Capture4.png")
        columns = 60
        rows = 80
        screen_width, screen_height = capture2.size
        block_width = ((screen_width - 1) // columns) + 1 # this is just a division ceiling
        block_height = ((screen_height - 1) // rows) + 1
        for y in range(0, screen_height, block_height+1):
            for x in range(0, screen_width, block_width+1):
                region2 = self.process_region(capture2, x, y, block_width, block_height)
                region4 = self.process_region(capture4, x, y, block_width, block_height)
                if region2 is not None and region4 is not None and region4 != region2:
                    draw = ImageDraw.Draw(capture2)
                    draw.rectangle((x, y, x+block_width, y+block_height), outline = "red", width = 1)
        capture2.save("difference.png")

    def process_region(self, image, x, y, width, height):
        region_total = 0
        # This can be used as the sensitivity factor, the larger it is the less sensitive the comparison
        factor = 10000
        for coordinateY in range(y, y+height):
            for coordinateX in range(x, x+width):
                try:
                    pixel = image.getpixel((coordinateX, coordinateY))
                    region_total += sum(pixel)/4
                except:
                    return
        return region_total/factor

ScreenAnalysis()