Sunday, July 22, 2012

Visual testing of a triangle filling graphic method

Some time ago I was invited by a big company for a technical interview where I was asked by a test engineer - "how to write test for a method filling a triangle area?".. I didn't find the answer for the provided time (some inappropriate ideas like to use a neural network chased each other in my brain), but the problem was very interesting for me and an idea dawned me on the next day..
Idea: A Triangle is a very easy shape but it is very hard to distinguish the shape in an image by a computer, but a rectangle is much easier to be processed by a computer, thus we should just make a rectangle from two triangles and then we will check that the rectangle area is presented and artifacts-free, also we can check that there are not any points outside of the area..
I have written some Java code to check the idea. There is not a method in the java.awt.Graphics object to fill a triangle thus I have written such one:
public class TriangleFiller {
    /**
     * The method fills a triangle area and we check that the method fills a triangle
     * @param graphics the graphics context
     * @param xcoords an array contains the x coordinates for vertices (must have 3 positions)
     * @param ycoords an array contains the y coordinates for vertices (must have 3 positions)
     */
    public static void fillTriangle(final Graphics graphics, int[] xcoords, int[] ycoords) {
        if (xcoords.length != 3 || ycoords.length != 3) {
            throw new IllegalArgumentException("Triangle must have 3 points");
        }
        final Polygon polygon = new Polygon(xcoords, ycoords, 3);
        // we need use draw+fill because the fill operation fills the inside area
        graphics.drawPolygon(polygon);
        graphics.fillPolygon(polygon);
    }
}

Then I wrote a unit test to make the "visual check" of the method just on an image and it works well:
package com.igormaznitsa.testtriangle;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.ImageIO;
import static org.junit.Assert.*;
import org.junit.Test;

public class TriangleFillerTest {
    
    private boolean checkTriangleCornerPoints(final BufferedImage baseRGBImage, final int pointColor, final int[] xVerticies, final int[] yVerticies) {
        for (int pointIndex = 0; pointIndex < 3; pointIndex++) {
            final int x = xVerticies[pointIndex];
            final int y = yVerticies[pointIndex];

            if ((baseRGBImage.getRGB(x, y) & 0xFFFFFF) != pointColor) {
                return false;
            }
        }
        return true;
    }

    private boolean checkRectangleAreaHasBeenFilledOnly(final BufferedImage baseRGBImage, final int fillColor, final int backgroundColor, final int areaLeftX, final int areaTopY, final int areaWidth, final int areaHeight) {
        final int areaRightX = areaLeftX + areaWidth;
        final int areaBottomY = areaTopY + areaHeight;

        final int imagewidth = baseRGBImage.getWidth();
        final int imageheight = baseRGBImage.getHeight();

        for (int y = 0; y < imageheight; y++) {
            for (int x = 0; x < imagewidth; x++) {
                final int rgb = baseRGBImage.getRGB(x, y) & 0xFFFFFF;
                final boolean notInArea = x < areaLeftX || x > areaRightX || y < areaTopY || y > areaBottomY;
                if (notInArea) {
                    assertEquals("Must be background color "+backgroundColor, backgroundColor, rgb);
                } else {
                    assertEquals("Must be fill color " + fillColor, fillColor, rgb);
                }
            }
        }
        return true;
    }
    
    @Test
    public void testFillTriangle_VisualTest() throws Exception {
        // the graphic log image file
        final File gfxLogFile = new File("./testimage.png");

        // create a RGB memory rendered image which will be our base for the test
        final BufferedImage baseTestImage = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
        final Graphics2D gfx = (Graphics2D) baseTestImage.getGraphics();

        // we must disable antialasing for the graphics to avoid artefacts
        ((Graphics2D) gfx).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

        final int backColor = 0x000000;
        final int shapeColor = 0xFFFFFF;

        // fill the image by our background color
        gfx.setColor(new Color(backColor));
        gfx.clearRect(0, 0, 200, 200);

        // set the shape color
        gfx.setColor(new Color(shapeColor));

        // our triangles, we use non-equaterial triangles, to make a rectangle, not a square
        final int[] firstTriangleX = new int[]{10, 10, 50};
        final int[] firstTriangleY = new int[]{10, 150, 150};
        final int[] secondTriangleX = new int[]{10, 50, 50};
        final int[] secondTriangleY = new int[]{10, 10, 150};

        // draw the first triangle
        TriangleFiller.fillTriangle(gfx, firstTriangleX, firstTriangleY);

        // save the current graphic state as an image (like log)
        ImageIO.write(baseTestImage, "png", gfxLogFile);

        assertTrue("The image must have set the points of vertices", checkTriangleCornerPoints(baseTestImage, shapeColor, firstTriangleX, firstTriangleY));
  
        // draw the second triangle, to get a filled rectangular area on the image
        TriangleFiller.fillTriangle(gfx, secondTriangleX, secondTriangleY);

        gfx.dispose();

        // save the current graphic state as an image (like log)
        ImageIO.write(baseTestImage, "png", gfxLogFile);

        assertTrue("Only the rectangle area must be filled, without artefacts",checkRectangleAreaHasBeenFilledOnly(baseTestImage, shapeColor, backColor, 10, 10, 40, 140));
    }
}