|
| 1 | +package org.vcell.cli.testsupport; |
| 2 | + |
| 3 | +import java.awt.image.BufferedImage; |
| 4 | + |
| 5 | +public class SSIMComparisonTool { |
| 6 | + private static final double STANDARD_RED_WEIGHT = 0.299; |
| 7 | + private static final double STANDARD_GREEN_WEIGHT = 0.587; |
| 8 | + private static final double STANDARD_BLUE_WEIGHT = 0.114; |
| 9 | + private static final double DYNAMIC_RANGE = 0xFF; // 2^8 - 1 |
| 10 | + |
| 11 | + private static final double STANDARD_LUMINANCE_CONSTANT = 0.01; |
| 12 | + private static final double STANDARD_CONTRAST_CONSTANT = 0.03; |
| 13 | + //private double STANDARD_STRUCTURE_CONSTANT = CONTRAST_CONSTANT / Math.sqrt(2); |
| 14 | + |
| 15 | + private static final double ALPHA_WEIGHT = 1; |
| 16 | + private static final double BETA_WEIGHT = 1; |
| 17 | + private static final double GAMMA_WEIGHT = 1; |
| 18 | + |
| 19 | + private final double LUMINANCE_STABILIZER; |
| 20 | + private final double CONTRAST_STABILIZER; |
| 21 | + private final double STRUCTURE_STABILIZER; |
| 22 | + |
| 23 | + public SSIMComparisonTool(){ |
| 24 | + this(STANDARD_LUMINANCE_CONSTANT, STANDARD_CONTRAST_CONSTANT, STANDARD_CONTRAST_CONSTANT / 2); |
| 25 | + } |
| 26 | + |
| 27 | + /** |
| 28 | + * Builds the stabilizer variables from luminance and contrast constants |
| 29 | + * @param luminanceConstant |
| 30 | + * @param contrastConstant |
| 31 | + */ |
| 32 | + public SSIMComparisonTool(double luminanceConstant, double contrastConstant){ |
| 33 | + this( |
| 34 | + DYNAMIC_RANGE * DYNAMIC_RANGE * luminanceConstant * luminanceConstant, |
| 35 | + DYNAMIC_RANGE * DYNAMIC_RANGE * contrastConstant * contrastConstant, |
| 36 | + (DYNAMIC_RANGE * DYNAMIC_RANGE * contrastConstant * contrastConstant) / 2 |
| 37 | + ); |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Assign pre-computed stabilizers for SSIM calculations |
| 42 | + * @param luminanceStabilizer |
| 43 | + * @param contrastStabilizer |
| 44 | + * @param structureStabilizer |
| 45 | + */ |
| 46 | + public SSIMComparisonTool(double luminanceStabilizer, double contrastStabilizer, double structureStabilizer){ |
| 47 | + this.LUMINANCE_STABILIZER = luminanceStabilizer; |
| 48 | + this.CONTRAST_STABILIZER = contrastStabilizer; |
| 49 | + this.STRUCTURE_STABILIZER = structureStabilizer; |
| 50 | + } |
| 51 | + |
| 52 | + public double performSSIMComparison(BufferedImage original, BufferedImage contender){ |
| 53 | + double[][] originalGrayscaleData = SSIMComparisonTool.getGrayScaleData(original); |
| 54 | + double[][] contenderGrayscaleData = SSIMComparisonTool.getGrayScaleData(contender); |
| 55 | + double originalPSM = SSIMComparisonTool.pixelSampleMean(originalGrayscaleData); |
| 56 | + double contenderPSM = SSIMComparisonTool.pixelSampleMean(contenderGrayscaleData); |
| 57 | + double originalVariance = SSIMComparisonTool.sampleVariance(originalGrayscaleData, originalPSM); |
| 58 | + double contenderVariance = SSIMComparisonTool.sampleVariance(contenderGrayscaleData, contenderPSM); |
| 59 | + double coVariance = SSIMComparisonTool.sampleCovariance(originalGrayscaleData, originalPSM, contenderGrayscaleData, contenderPSM); |
| 60 | + double luminance = this.luminanceCalculation(originalPSM, contenderPSM); |
| 61 | + double contrast = this.contrastCalculation(originalVariance, contenderVariance); |
| 62 | + double structure = this.structureCalculation(originalVariance, contenderVariance, coVariance); |
| 63 | + double weightedLuminance = Math.pow(luminance, SSIMComparisonTool.ALPHA_WEIGHT); |
| 64 | + double weightedContrast = Math.pow(contrast, SSIMComparisonTool.BETA_WEIGHT); |
| 65 | + double weightedStructure = Math.pow(structure, SSIMComparisonTool.GAMMA_WEIGHT); |
| 66 | + return weightedLuminance * weightedContrast * weightedStructure; |
| 67 | + } |
| 68 | + |
| 69 | + private double luminanceCalculation(double originalPSM, double contenderPSM){ |
| 70 | + double opsmSquared = originalPSM * originalPSM; |
| 71 | + double cpsmSquared = contenderPSM * contenderPSM; |
| 72 | + double numerator = 2 * originalPSM * contenderPSM + this.LUMINANCE_STABILIZER; |
| 73 | + double denominator = opsmSquared + cpsmSquared + this.LUMINANCE_STABILIZER; |
| 74 | + return numerator / denominator; |
| 75 | + } |
| 76 | + |
| 77 | + private double contrastCalculation(double originalVariance, double contenderVariance){ |
| 78 | + double ovSquared = originalVariance * originalVariance; |
| 79 | + double cvSquared = contenderVariance * contenderVariance; |
| 80 | + double numerator = 2 * originalVariance * contenderVariance + this.CONTRAST_STABILIZER; |
| 81 | + double denominator = ovSquared + cvSquared + this.CONTRAST_STABILIZER; |
| 82 | + return numerator / denominator; |
| 83 | + } |
| 84 | + |
| 85 | + private double structureCalculation(double originalVariance, double contenderVariance, double coVariance){ |
| 86 | + double numerator = coVariance + this.STRUCTURE_STABILIZER; |
| 87 | + double denominator = Math.sqrt(originalVariance * contenderVariance) + this.STRUCTURE_STABILIZER; |
| 88 | + return numerator / denominator; |
| 89 | + } |
| 90 | + |
| 91 | + private static double pixelSampleMean(double[][] grayscaleData){ |
| 92 | + int numRows = grayscaleData.length; |
| 93 | + int numCols = grayscaleData[0].length; |
| 94 | + double sum = 0; |
| 95 | + for (var row : grayscaleData) for (double v : row) sum += v; |
| 96 | + return sum / (numRows * numCols); |
| 97 | + } |
| 98 | + |
| 99 | + private static double sampleVariance(double[][] grayscaleData, double mean) { |
| 100 | + int numRows = grayscaleData.length; |
| 101 | + int numCols = grayscaleData[0].length; |
| 102 | + double sum = 0; |
| 103 | + for (var row : grayscaleData) for (double v : row) sum += (v - mean) * (v - mean); |
| 104 | + return sum / (numRows * numCols); |
| 105 | + } |
| 106 | + |
| 107 | + private static double sampleCovariance(double[][] originalGrayscaleData, double originalMean, |
| 108 | + double[][] contenderGrayscaleData, double contenderMean) { |
| 109 | + double sum = 0; |
| 110 | + int originalNumRows = originalGrayscaleData.length, originalNumCols = originalGrayscaleData[0].length; |
| 111 | + for (int i = 0; i < originalNumRows; i++) for (int j = 0; j < originalNumCols; j++) |
| 112 | + sum += (originalGrayscaleData[i][j] - originalMean) * (contenderGrayscaleData[i][j] - contenderMean); |
| 113 | + return sum / (originalNumRows * originalNumCols); |
| 114 | + } |
| 115 | + |
| 116 | + // The idea for SSIM is based on human perception, so grayscale works well |
| 117 | + private static double[][] getGrayScaleData(BufferedImage image){ |
| 118 | + int w = image.getWidth(); |
| 119 | + int h = image.getHeight(); |
| 120 | + double[][] convertedData = new double[h][w]; |
| 121 | + |
| 122 | + for (int y = 0; y < h; y++) { |
| 123 | + for (int x = 0; x < w; x++) { |
| 124 | + int rgb = image.getRGB(x, y); |
| 125 | + double blue = STANDARD_BLUE_WEIGHT * (0xff & rgb); |
| 126 | + rgb = (rgb >> 8); |
| 127 | + double green = STANDARD_GREEN_WEIGHT * (0xff & rgb); |
| 128 | + rgb = (rgb >> 8); |
| 129 | + double red = STANDARD_RED_WEIGHT * (0xff & rgb); |
| 130 | + |
| 131 | + convertedData[y][x] = red + green + blue; |
| 132 | + } |
| 133 | + } |
| 134 | + return convertedData; |
| 135 | + } |
| 136 | +} |
0 commit comments