Skip to content

Commit 6bd7c5a

Browse files
Now using SSIM image comparison
1 parent af5c3d6 commit 6bd7c5a

2 files changed

Lines changed: 151 additions & 13 deletions

File tree

vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestResults2DLinePlot.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.junit.jupiter.api.Test;
1212
import org.junit.jupiter.api.Assertions;
1313
import org.vcell.cli.commands.execution.BiosimulationsCommand;
14+
import org.vcell.cli.testsupport.SSIMComparisonTool;
1415
import org.vcell.util.Pair;
1516
import org.vcell.util.VCellUtilityHub;
1617

@@ -31,7 +32,7 @@
3132
public class TestResults2DLinePlot {
3233
private static final double PIXEL_DIFF_HIGH = 0.2; // 20%
3334
private static final double PIXEL_DIFF_LOW = -0.2; // 20%
34-
private static final double ACCURACY_THRESHOLD = 0.999; // 99.9%
35+
private static final double ACCURACY_THRESHOLD = 0.97; // 97%
3536

3637
private static final List<XYDataItem> paraData = List.of(
3738
new XYDataItem(0.0, 0.0),
@@ -63,6 +64,17 @@ public class TestResults2DLinePlot {
6364
List.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0)
6465
);
6566

67+
@Test
68+
public void ensureSSIMWorks() throws IOException {
69+
String STANDARD_IMAGE_LOCAL_PATH = "parabolic.png";
70+
InputStream standardImageStream = TestResults2DLinePlot.class.getResourceAsStream(STANDARD_IMAGE_LOCAL_PATH);
71+
if (standardImageStream == null)
72+
throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", STANDARD_IMAGE_LOCAL_PATH));
73+
BufferedImage standardImage = ImageIO.read(standardImageStream);
74+
SSIMComparisonTool comparisonTool = new SSIMComparisonTool();
75+
Assertions.assertEquals (1.0, comparisonTool.performSSIMComparison(standardImage, standardImage));
76+
}
77+
6678
@Test
6779
public void testConstructors(){
6880
Results2DLinePlot[] testInstances = {
@@ -234,17 +246,7 @@ public void pngExecutionLevelTest() throws IOException {
234246
}
235247

236248
private static double getAccuracyPercentage(BufferedImage original, BufferedImage generated){
237-
int totalNumPixels = generated.getWidth() * generated.getHeight();
238-
int accuratePixels = 0;
239-
for (int wPix = 0; wPix < generated.getWidth(); wPix++){
240-
for (int hPix = 0; hPix < generated.getHeight(); hPix++){
241-
int originalPixel = original.getRGB(wPix, hPix);
242-
int generatedPixel = generated.getRGB(wPix, hPix);
243-
double pixelComp = (originalPixel - generatedPixel) / (1.0 * originalPixel);
244-
if (pixelComp > PIXEL_DIFF_HIGH || pixelComp < PIXEL_DIFF_LOW) continue; // too far out of range
245-
accuratePixels++;
246-
}
247-
}
248-
return accuratePixels/(1.0 * totalNumPixels);
249+
SSIMComparisonTool ssim = new SSIMComparisonTool();
250+
return ssim.performSSIMComparison(original, generated);
249251
}
250252
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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

Comments
 (0)