com.github.pitzcarraldo.dissimilar.Dissimilar.java Source code

Java tutorial

Introduction

Here is the source code for com.github.pitzcarraldo.dissimilar.Dissimilar.java

Source

/*
 * Copyright 2013 The British Library/SCAPE Project Consortium
 * Author: William Palmer (William.Palmer@bl.uk)
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.github.pitzcarraldo.dissimilar;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import javax.imageio.ImageIO;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.math3.stat.correlation.Covariance;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.Variance;

/*
 * NOTE: this currently uses a snapshot of commons-imaging to load tiff files
 * When using net.java.dev.jai-imageio:jai-imageio-core-standalone:1.2-pre-dr-b04-2013-04-23 
 * the tiff files aren't loaded properly - psnr calcs are different from imagemagick/commons-imaging 
 * loaded tiffs. 
 */

/**
 * Program/library to calculate SSIM and PSNR values between two images  
 * @author wpalmer
 */
public class Dissimilar {

    /**
     * Default SSIM window size (8)
     */
    public final static int SSIMWINDOWSIZE = 8;

    /**
     * Version string (loaded from Maven properties in jar)
     */
    private static String version = "NOVERSION";

    /**
     * Calculate the SSIM value on a window of pixels
     * @param pLumaOne luma values for first image
     * @param pLumaTwo luma values for second image
     * @param pWidth width of the two images
     * @param pHeight height of the two images
     * @param pWindowSize window size
     * @param pStartX start x coordinate for the window
     * @param pStartY start y coordinate for the window
     * @return SSIM for the window
     */
    private static double calcSSIMOnWindow(final double[] pLumaOne, final double[] pLumaTwo, final int pWidth,
            final int pHeight, final int pWindowSize, final int pStartX, final int pStartY) {

        final double k1 = 0.01;
        final double k2 = 0.03;
        final double L = Math.pow(2, 8) - 1;//255 (at least for the moment all pixel values are 8-bit)
        final double c1 = Math.pow(k1 * L, 2);
        final double c2 = Math.pow(k2 * L, 2);

        final int windowWidth = ((pWindowSize + pStartX) > pWidth) ? (pWidth - pStartX) : (pWindowSize);
        final int windowHeight = ((pWindowSize + pStartY) > pHeight) ? (pHeight - pStartY) : (pWindowSize);

        if (windowWidth <= 0 || windowHeight <= 0) {
            System.out.println(pWidth + " " + pStartX + " " + windowWidth + " " + windowHeight);
            System.out.println(pHeight + " " + pStartY + " " + windowWidth + " " + windowHeight);
        }

        //store a temporary copy of the pixels
        double[] pixelsOne = new double[windowWidth * windowHeight];
        double[] pixelsTwo = new double[windowWidth * windowHeight];

        for (int h = 0; h < windowHeight; h++) {
            for (int w = 0; w < windowWidth; w++) {
                pixelsOne[(h * windowWidth) + w] = pLumaOne[(pStartY + h) * pWidth + pStartX + w];
                pixelsTwo[(h * windowWidth) + w] = pLumaTwo[(pStartY + h) * pWidth + pStartX + w];
            }
        }

        final double ux = new Mean().evaluate(pixelsOne, 0, pixelsOne.length);
        final double uy = new Mean().evaluate(pixelsTwo, 0, pixelsTwo.length);
        final double o2x = new Variance().evaluate(pixelsOne);
        final double o2y = new Variance().evaluate(pixelsTwo);
        final double oxy = new Covariance().covariance(pixelsOne, pixelsTwo);

        final double num = (2 * ux * uy + c1) * (2 * oxy + c2);
        final double den = (ux * ux + uy * uy + c1) * (o2x + o2y + c2);

        final double ssim = num / den;

        return ssim;
    }

    /**
     * Calculate the SSIM; see http://en.wikipedia.org/wiki/Structural_similarity
     * @param pOne array of integer pixel values for first image
     * @param pTwo array of integer pixel values for second image
     * @param pWidth width of the two images
     * @param pHeight height of the two images
     * @param pGreyscale if the images are greyscale
     * @param pHeatMapFilename filename for the ssim heatmap image to be saved to (png)
     * @param pMin list to hold return value for ssim-minimum
     * @param pVariance list to hold return value for ssim-variance
     * @return SSIM
     */
    public static double calcSSIM(final int[] pOne, final int[] pTwo, final int pWidth, final int pHeight,
            final boolean pGreyscale, final String pHeatMapFilename, List<Double> pMin, List<Double> pVariance) {

        if (!checkPair(pOne, pTwo))
            return -1;

        double[] lumaOne = null;
        double[] lumaTwo = null;

        //if the image is greyscale then don't extract the luma
        if (pGreyscale) {
            System.out.println("=> Greyscale");
            lumaOne = new double[pOne.length];
            lumaTwo = new double[pTwo.length];
            for (int i = 0; i < lumaOne.length; i++) {
                //all rgb values are the same
                lumaOne[i] = (pOne[i] >> 0) & 0xFF;
                lumaTwo[i] = (pTwo[i] >> 0) & 0xFF;
            }
        } else {
            lumaOne = calcLuma(pOne);
            lumaTwo = calcLuma(pTwo);
        }

        final int windowSize = SSIMWINDOWSIZE;

        final int windowsH = (int) Math.ceil((double) pHeight / windowSize);
        final int windowsW = (int) Math.ceil((double) pWidth / windowSize);

        double[] mssim = new double[windowsH * windowsW];

        double mean = 0;
        double min = 1;

        for (int height = 0; height < windowsH; height++) {
            for (int width = 0; width < windowsW; width++) {
                final int window = (height * windowsW) + width;
                mssim[window] = calcSSIMOnWindow(lumaOne, lumaTwo, pWidth, pHeight, windowSize, width * windowSize,
                        height * windowSize);
                mean += mssim[window];
                if (mssim[window] < min) {
                    min = mssim[window];
                }
            }
        }

        final double variance = new Variance().evaluate(mssim);

        mean /= (windowsH * windowsW);

        //if(variance>0.001) System.out.println("warning: high variance");

        if (null != pHeatMapFilename) {
            dumpSSIMHeatMap(mssim, windowsH, windowsW, pHeatMapFilename);
        }

        if (null != pMin) {
            pMin.add(0, new Double(min));
        }
        if (null != pVariance) {
            pVariance.add(0, new Double(variance));
        }

        return mean;
    }

    /**
     * Write an image showing the heatmap of ssim values, per window
     * @param pValues sequence of SSIM values
     * @param pHeight number of SSIM windows across
     * @param pWidth number of SSIM windows down
     * @param pFilename filename to save the image to (png)
     */
    private static BufferedImage dumpSSIMHeatMap(final double[] pValues, final int pHeight, final int pWidth,
            final String pFilename) {
        BufferedImage heatMap = new BufferedImage(pWidth, pHeight, BufferedImage.TYPE_USHORT_GRAY);

        final int maxPixelValue = (int) Math.pow(2, heatMap.getColorModel().getPixelSize()) - 1;

        int pixel = 0;
        for (int height = 0; height < pHeight; height++) {
            for (int width = 0; width < pWidth; width++) {
                pixel = ((int) (maxPixelValue * pValues[(height * pWidth) + width]));
                if (pixel < 0) {
                    pixel = 0;
                }
                heatMap.setRGB(width, height, pixel);
            }
        }

        //only write to file if filename is non-null
        if (pFilename != null) {
            try {
                ImageIO.write(heatMap, "png", new File(pFilename));
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }

        return heatMap;
    }

    /**
     * Calculate the luma values for an image; see http://en.wikipedia.org/wiki/Luma_%28video%29
     * @param pOne pixels to calculate luma for
     * @return array of luma values for input pixels
     */
    private static double[] calcLuma(final int[] pOne) {

        double[] ret = new double[pOne.length];

        int blue = 0;
        int green = 0;
        int red = 0;
        for (int i = 0; i < ret.length; i++) {
            blue = (pOne[i] >> 0) & 0xFF;
            green = (pOne[i] >> 8) & 0xFF;
            red = (pOne[i] >> 16) & 0xFF;
            ret[i] = (0.2126 * red + 0.7152 * green + 0.0722 * blue);
        }

        return ret;
    }

    /**
     * Calculate the mean-squared-error between two images
     * @param pOne first image to compare
     * @param pTwo second image to compare
     * @param pGreyscale whether the images are greyscale or not
     * @return mean squared error
     */
    private static double calcMSE(final int[] pOne, final int[] pTwo, final boolean pGreyscale) {

        //getRGB only returns 8 bits per component, so what about 16-bit images? 

        if (pOne.length != pTwo.length)
            return -1;

        int channels = 3;
        if (pGreyscale)
            channels = 1;

        //FIXME: change to BigDecimal?
        double sumSquaredErrors = 0;

        int oneBlue = 0;
        int oneGreen = 0;
        int oneRed = 0;
        int twoBlue = 0;
        int twoGreen = 0;
        int twoRed = 0;

        for (int i = 0; i < pOne.length; i++) {
            //see javadoc for Color class
            oneBlue = (pOne[i] >> 0) & 0xFF;
            twoBlue = (pTwo[i] >> 0) & 0xFF;
            //just use one channel - rgb should all be the same here (briefly tested)
            sumSquaredErrors += Math.pow((oneBlue - twoBlue), 2);
            if (!pGreyscale) {
                oneGreen = (pOne[i] >> 8) & 0xFF;
                oneRed = (pOne[i] >> 16) & 0xFF;
                twoGreen = (pTwo[i] >> 8) & 0xFF;
                twoRed = (pTwo[i] >> 16) & 0xFF;
                sumSquaredErrors += /*Math.pow((oneBlue-twoBlue),2)+*/Math.pow((oneGreen - twoGreen), 2)
                        + Math.pow((oneRed - twoRed), 2);
            } else {

            }
        }

        //System.out.println("SSE: "+sumSquaredErrors);

        final double meanSquaredError = sumSquaredErrors / (pOne.length * channels);

        return meanSquaredError;
    }

    /**
     * Check to see if the two images can be compared (in an image format migration sense i.e. they are the same size)
     * The current tests here do not take in to account dimensions etc.
     * @param pOne first image to check
     * @param pTwo second image to check
     * @return whether the images should be compared or not
     */
    private static boolean checkPair(final int[] pOne, final int[] pTwo) {

        if (pOne.length != pTwo.length)
            return false;

        //more checks?

        return true;
    }

    /**
     * Print an error if one of the input files cannot be opened
     * @param pOne first image file
     * @param pReadOne if the file can be read or not
     * @param pTriedTwo whether an attempt to open the second file has been made
     * @param pTwo second image file
     * @param pReadTwo if the file can be read or not
     */
    private static void printError(final File pOne, final boolean pReadOne, final boolean pTriedTwo,
            final File pTwo, final boolean pReadTwo) {
        System.out.println("<dissimilar version=\"" + version + "\">");
        System.out.println("     <error/>");
        System.out.println("     <file error=\"" + !pReadOne + "\">" + pOne.getAbsolutePath() + "</file>");
        if (pTriedTwo) {
            System.out.println("     <file error=\"" + !pReadTwo + "\">" + pTwo.getAbsolutePath() + "</file>");
        }
        System.out.println("</dissimilar>");
    }

    /**
     * Calculate PSNR; see http://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
     * @param pOne first image to compare
     * @param pTwo second image to compare
     * @param pGreyscale whether the images are greyscale or not
     * @return calculated psnr
     */
    public static double calcPSNR(final int[] pOne, final int[] pTwo, final boolean pGreyscale) {

        if (!checkPair(pOne, pTwo))
            return -1;

        final double mse = calcMSE(pOne, pTwo, pGreyscale);

        //what to do if the bit depth of the images are different?

        //we are just using an 8-bit per channel representation at the moment
        final double maxPixelValue = Math.pow(2, 8) - 1;

        final double psnr = 10 * Math.log10((maxPixelValue * maxPixelValue) / mse);

        return psnr;
    }

    /**
     * Calculate the PSNR between two files
     * @param pOne first image to compare
     * @param pTwo second image to compare
     * @return calculated psnr
     */
    public static double calcPSNR(final File pOne, final File pTwo) {
        BufferedImage imageOne = null;
        try {
            imageOne = Imaging.getBufferedImage(pOne);
        } catch (IOException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        } catch (NullPointerException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        } catch (ImageReadException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        }

        //getRGB only returns 8 bits per component, so what about 16-bit images? 
        final int[] oneA = imageOne.getRGB(0, 0, imageOne.getWidth(), imageOne.getHeight(), null, 0,
                imageOne.getWidth());
        final boolean greyscale = (imageOne.getType() == BufferedImage.TYPE_BYTE_GRAY
                || imageOne.getType() == BufferedImage.TYPE_USHORT_GRAY);
        imageOne = null;

        BufferedImage imageTwo = null;
        try {
            imageTwo = Imaging.getBufferedImage(pTwo);
        } catch (IOException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        } catch (NullPointerException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        } catch (ImageReadException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        }

        //getRGB only returns 8 bits per component, so what about 16-bit images? 
        final int[] twoA = imageTwo.getRGB(0, 0, imageTwo.getWidth(), imageTwo.getHeight(), null, 0,
                imageTwo.getWidth());
        imageTwo = null;

        final double psnr = calcPSNR(oneA, twoA, greyscale);

        return psnr;
    }

    /**
     * Calculate the SSIM between two files
     * @param pOne first image to compare
     * @param pTwo second image to compare
     * @return calculated ssim
     */
    public static double calcSSIM(final File pOne, final File pTwo) {
        return calcSSIM(pOne, pTwo, null, null, null);
    }

    /**
     * Calculate the SSIM between two files
     * @param pOne first image to compare
     * @param pTwo second image to compare
     * @param pHeatMapFilename ssim heat map image filename (can be null)
     * @param pMin list for return value - ssim minimum (can be null)
     * @param pVariance list for return value - ssim variance (can be null)
     * @return calculated ssim
     */
    public static double calcSSIM(final File pOne, final File pTwo, final String pHeatMapFilename,
            List<Double> pMin, List<Double> pVariance) {

        BufferedImage imageOne = null;
        try {
            imageOne = Imaging.getBufferedImage(pOne);
        } catch (IOException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        } catch (NullPointerException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        } catch (ImageReadException e) {
            printError(pOne, false, false, pTwo, false);
            return -1;
        }

        //getRGB only returns 8 bits per component, so what about 16-bit images? 
        final int[] oneA = imageOne.getRGB(0, 0, imageOne.getWidth(), imageOne.getHeight(), null, 0,
                imageOne.getWidth());
        final int width = imageOne.getWidth();
        final int height = imageOne.getHeight();
        final boolean greyscale = (imageOne.getType() == BufferedImage.TYPE_BYTE_GRAY
                || imageOne.getType() == BufferedImage.TYPE_USHORT_GRAY);
        imageOne = null;

        BufferedImage imageTwo = null;
        try {
            imageTwo = Imaging.getBufferedImage(pTwo);
        } catch (IOException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        } catch (NullPointerException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        } catch (ImageReadException e) {
            printError(pOne, true, true, pTwo, false);
            return -1;
        }

        //getRGB only returns 8 bits per component, so what about 16-bit images? 
        final int[] twoA = imageTwo.getRGB(0, 0, imageTwo.getWidth(), imageTwo.getHeight(), null, 0,
                imageTwo.getWidth());
        imageTwo = null;

        final double ssim = calcSSIM(oneA, twoA, width, height, greyscale, pHeatMapFilename, pMin, pVariance);

        return ssim;
    }

    /**
     * Compare two files, according to parameters passed via command line
     * @param pOne first file to compare
     * @param pTwo second file to compare
     * @param pHeatMapImage file to save ssim heat map image to
     * @param pCalcSSIM whether or not to calculate ssim
     * @param pCalcPSNR whether or not to calculate psnr
     */
    private static void compare(final File pOne, final File pTwo, final String pHeatMapImage,
            final boolean pCalcSSIM, final boolean pCalcPSNR) {

        //just load the images once and use the internal methods for calculating ssim/psnr
        long time = System.currentTimeMillis();
        BufferedImage imageOne = null;
        try {
            imageOne = Imaging.getBufferedImage(pOne);
        } catch (IOException e) {
            printError(pOne, false, false, pTwo, false);
            return;
        } catch (NullPointerException e) {
            printError(pOne, false, false, pTwo, false);
            return;
        } catch (ImageReadException e) {
            printError(pOne, false, false, pTwo, false);
            return;
        }
        final long oneLoadTime = System.currentTimeMillis() - time;
        //getRGB only returns 8 bits per component, so what about 16-bit images?
        final int[] oneA = imageOne.getRGB(0, 0, imageOne.getWidth(), imageOne.getHeight(), null, 0,
                imageOne.getWidth());
        final int width = imageOne.getWidth();
        final int height = imageOne.getHeight();
        final boolean greyscale = (imageOne.getType() == BufferedImage.TYPE_BYTE_GRAY
                || imageOne.getType() == BufferedImage.TYPE_USHORT_GRAY);
        imageOne = null;
        time = System.currentTimeMillis();
        BufferedImage imageTwo = null;
        try {
            imageTwo = Imaging.getBufferedImage(pTwo);
        } catch (IOException e) {
            printError(pOne, true, true, pTwo, false);
            return;
        } catch (NullPointerException e) {
            printError(pOne, true, true, pTwo, false);
            return;
        } catch (ImageReadException e) {
            printError(pOne, true, true, pTwo, false);
            return;
        }
        final long twoLoadTime = System.currentTimeMillis() - time;

        //getRGB only returns 8 bits per component, so what about 16-bit images?
        final int[] twoA = imageTwo.getRGB(0, 0, imageTwo.getWidth(), imageTwo.getHeight(), null, 0,
                imageTwo.getWidth());
        imageTwo = null;

        //calculate psnr if wanted
        time = System.currentTimeMillis();
        double psnr = 0;
        long psnrCalc = 0;
        if (pCalcPSNR) {
            psnr = calcPSNR(oneA, twoA, greyscale);
            psnrCalc = System.currentTimeMillis() - time;
        }

        //calculate ssim if wanted
        time = System.currentTimeMillis();
        List<Double> ssimMin = new LinkedList<Double>();
        List<Double> ssimVariance = new LinkedList<Double>();
        double ssim = 0;
        long ssimCalc = 0;
        if (pCalcSSIM) {
            ssim = calcSSIM(oneA, twoA, width, height, greyscale, pHeatMapImage, ssimMin, ssimVariance);
            ssimCalc = System.currentTimeMillis() - time;
        }

        System.out.println("<dissimilar version=\"" + version + "\">");
        System.out.println("     <file loadTimeMS=\"" + oneLoadTime + "\">" + pOne + "</file>");
        System.out.println("     <file loadTimeMS=\"" + twoLoadTime + "\">" + pTwo + "</file>");
        if (pCalcSSIM) {
            System.out.println("     <ssim calcTimeMS=\"" + ssimCalc + "\">");
            if (ssim > 0) {
                System.out.println("          <mean>" + new DecimalFormat("0.0000000").format(ssim) + "</mean>");
                System.out.println(
                        "          <min>" + new DecimalFormat("0.0000000").format(ssimMin.get(0)) + "</min>");
                System.out.println("          <variance>"
                        + new DecimalFormat("0.0000000").format(ssimVariance.get(0)) + "</variance>");
            } else {
                System.out.println("failed");
            }
            System.out.println("     </ssim>");
        }
        if (pCalcPSNR) {
            System.out.println("     <psnr calcTimeMS=\"" + psnrCalc + "\">"
                    + new DecimalFormat("0.0000").format(psnr) + "</psnr>");
        }
        System.out.println("</dissimilar>");

    }

    /**
     * Main method
     * @param args command line arguments
     */
    public static void main(String[] args) {

        Properties mavenProps = new Properties();
        try {
            mavenProps.load(Dissimilar.class.getClassLoader()
                    .getResourceAsStream("META-INF/maven/uk.bl.dpt.dissimilar/dissimilar/pom.properties"));
            version = mavenProps.getProperty("version");
        } catch (Exception e) {
        }

        boolean calcPSNR = true;
        boolean calcSSIM = true;
        String heatMapImage = null;

        CommandLineParser parser = new PosixParser();
        Options options = new Options();
        options.addOption("m", "heatmap", true, "file to save the ssim heatmap to (png)");
        options.addOption("p", "psnr", false, "calculate just psnr");
        options.addOption("s", "ssim", false, "calculate just ssim");
        options.addOption("h", "help", false, "help text");

        CommandLine com = null;
        try {
            com = parser.parse(options, args);
        } catch (ParseException e) {
            HelpFormatter help = new HelpFormatter();
            help.printHelp("Dissimilar v" + version, options);
            return;
        }

        if (com.hasOption("help")) {
            HelpFormatter help = new HelpFormatter();
            help.printHelp("Dissimilar v" + version, options);
            return;
        }

        if (com.hasOption("psnr") & com.hasOption("ssim")) {
            //do nothing - both on by default
        } else {
            if (com.hasOption("psnr")) {
                calcPSNR = true;
                calcSSIM = false;
            }
            if (com.hasOption("ssim")) {
                calcPSNR = false;
                calcSSIM = true;
            }
        }

        if (com.hasOption("heatmap")) {
            heatMapImage = com.getOptionValue("heatmap");
        }

        File one = new File(com.getArgs()[0]);
        File two = new File(com.getArgs()[1]);

        if (one.exists() && two.exists()) {
            compare(one, two, heatMapImage, calcSSIM, calcPSNR);
        }

    }

}