-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathJavaImageIOCompare.java
284 lines (254 loc) · 13.2 KB
/
JavaImageIOCompare.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/**
* Copyright (c) 2007, 2008 The Planets Project Partners.
*
* All rights reserved. This program and the accompanying
* materials are made available under the terms of the
* GNU Lesser General Public License v3 which
* accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
*/
package eu.planets_project.services.java_se.image;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.jws.WebService;
import javax.xml.ws.soap.MTOM;
import com.sun.xml.ws.developer.StreamingAttachment;
import eu.planets_project.ifr.core.techreg.formats.FormatRegistryFactory;
import eu.planets_project.services.PlanetsServices;
import eu.planets_project.services.compare.Compare;
import eu.planets_project.services.compare.CompareResult;
import eu.planets_project.services.compare.PropertyComparison;
import eu.planets_project.services.compare.PropertyComparison.Equivalence;
import eu.planets_project.services.datatypes.DigitalObject;
import eu.planets_project.services.datatypes.Parameter;
import eu.planets_project.services.datatypes.Property;
import eu.planets_project.services.datatypes.ServiceDescription;
import eu.planets_project.services.datatypes.ServiceReport;
import eu.planets_project.services.java_se.image.metrics.KahanSummation;
import eu.planets_project.services.utils.ServicePerformanceHelper;
import eu.planets_project.services.utils.ServiceUtils;
/**
* The purpose of this is to implement a number of image comparison metrics for quality.
* These include PSNR, MSE.
*
* http://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
*
* @author AnJackson
*/
@MTOM
@StreamingAttachment( parseEagerly=true, memoryThreshold=ServiceUtils.JAXWS_SIZE_THRESHOLD )
@WebService(name = JavaImageIOCompare.NAME,
serviceName = Compare.NAME,
targetNamespace = PlanetsServices.NS,
endpointInterface = "eu.planets_project.services.compare.Compare" )
public class JavaImageIOCompare implements Compare {
/** Service name */
public static final String NAME = "JavaImageIOCompare";
private static Logger log = Logger.getLogger(JavaImageIOCompare.class.getName());
/** The PSNR property definition */
public static final URI PSNR_URI = URI.create("planets:pc/compare/image/psnr");
public static PropertyComparison buildPsnrProperty( double psnr ) {
Property.Builder pbPsnr = new Property.Builder( PSNR_URI );
pbPsnr.name("PSNR");
pbPsnr.value(""+psnr);
pbPsnr.type("double");
pbPsnr.description("Peak Signal-To-Noise Ratio. Is 'Infinity' if the images are identical, and much lower if the images are similar. Typical values for the PSNR in lossy image and video compression are between 30 and 50 dB, where higher is better. Acceptable values for wireless transmission quality loss are considered to be about 20 dB to 25 dB.");
pbPsnr.unit("dB");
// Also record equivalence.
Equivalence eq = Equivalence.DIFFERENT;
if( Double.isInfinite(psnr) ) eq = Equivalence.EQUAL;
return new PropertyComparison(pbPsnr.build(), eq );
}
/* (non-Javadoc)
* @see eu.planets_project.services.compare.Compare#compare(eu.planets_project.services.datatypes.DigitalObject, eu.planets_project.services.datatypes.DigitalObject, java.util.List)
*/
public CompareResult compare(DigitalObject first, DigitalObject second,
List<Parameter> config) {
// Start timing...
ServicePerformanceHelper sph = new ServicePerformanceHelper();
// Initialise what will be built:
List<PropertyComparison> props = new ArrayList<PropertyComparison>();
ServiceReport sr = null;
// Load the images.
BufferedImage i1;
BufferedImage i2;
try {
i1 = ImageIO.read( first.getContent().getInputStream() );
i2 = ImageIO.read( second.getContent().getInputStream() );
} catch (IOException e) {
return new CompareResult(null, ServiceUtils.createExceptionErrorReport("IOException reading the images. ", e));
}
if( i1 == null || i2 == null ) {
log.warning("One of the images was null when loaded!");
return new CompareResult(null, ServiceUtils.createErrorReport("Error reading the images, got a NULL."));
}
// Record time take to load the inputs into memory:
sph.loaded();
// Check comparison is possible: Are the dimensions the same?
if (i1.getWidth() != i2.getWidth() || i1.getHeight() != i2.getHeight()) {
// FIXME is this really an error, or rather a 'images are different' result?
return new CompareResult(null, ServiceUtils.createErrorReport("The image dimensions must match!"));
}
// FIXME Check comparison is sensible: are there the same number of channels? This is probably a WARNING?
if( i1.getColorModel().getNumComponents() != i2.getColorModel().getNumComponents()) {
System.out.println("The number of colour components does not match. "+i1.getColorModel().getNumComponents()+" != "+i2.getColorModel().getNumComponents());
log.warning("The number of colour components does not match. "+i1.getColorModel().getNumComponents()+" != "+i2.getColorModel().getNumComponents());
sr = new ServiceReport(ServiceReport.Type.WARN, ServiceReport.Status.SUCCESS, "Number of colour components was not the same. The missing channels, e.g. the alpha channel, will be assumed to be zero.");
// FIXME I think this should be more serious, as the results can be rather misleading.
// The comparison assumes the bit-mask to be zero everywhere, but this did not lead to such a bad PSNR?!
}
// Run the comparison:
double psnr = psnr(i1, i2);
props.add( buildPsnrProperty(psnr) );
// Create a happy service report if no problems occurred.
if( sr == null) {
// Also store some service properties:
sr = new ServiceReport(ServiceReport.Type.INFO, ServiceReport.Status.SUCCESS,
"Comparison succeeded.", sph.getPerformanceProperties() );
}
// Halt performance measurement:
sph.stop();
// Return the result:
return new CompareResult( props, sr );
}
/* (non-Javadoc)
* @see eu.planets_project.services.compare.Compare#convert(eu.planets_project.services.datatypes.DigitalObject)
*/
public List<Parameter> convert(DigitalObject configFile) {
// TODO Auto-generated method stub
return null;
}
/* (non-Javadoc)
* @see eu.planets_project.services.PlanetsService#describe()
*/
public ServiceDescription describe() {
ServiceDescription.Builder mds = new ServiceDescription.Builder(NAME, Compare.class.getCanonicalName());
mds.description("A Raster DigitalObject Comparison Service based on the Java SE built-in ImageIO library.");
mds.author("Andrew Jackson <Andrew.Jackson@bl.uk>");
mds.classname(this.getClass().getCanonicalName());
mds.version("1.0.1");
mds.tool(JavaImageIOIdentify.tool);
List<URI> ifs = new ArrayList<URI>();
for( String fmt_name : JavaImageIOIdentify.unique(ImageIO.getReaderFormatNames()) ) {
ifs.add( FormatRegistryFactory.getFormatRegistry().createExtensionUri( fmt_name ) );
}
mds.inputFormats(ifs.toArray(new URI[]{}));
return mds.build();
}
/* --------------------------------------------------------------------------------------- */
/**
* Computes the peak signal to noise ratio for two images, using all four channels (R,G,B,Alpha).
*
* @param i1
* @param i2
* @return
*/
private static Double psnr( BufferedImage i1, BufferedImage i2 ) {
// Image features
int cc1 = i1.getColorModel().getNumComponents();
double bpp1 = i1.getColorModel().getPixelSize()/cc1;
int cc2 = i2.getColorModel().getNumComponents();
double bpp2 = i2.getColorModel().getPixelSize()/cc2;
int cc = 4;
if( cc1 == 3 && cc2 == 3 ) {
cc = 3;
}
// Set up the summations:
KahanSummation tr = new KahanSummation();
KahanSummation tg = new KahanSummation();
KahanSummation tb = new KahanSummation();
KahanSummation ta = new KahanSummation();
BigInteger br = BigInteger.valueOf(0);
BigInteger bg = BigInteger.valueOf(0);
BigInteger bb = BigInteger.valueOf(0);
BigInteger ba = BigInteger.valueOf(0);
// TODO How best can we cope with comparisons across colour-spaces?
// Yes, in that this is what getRBG does, but note that getRGB reduced the depth to 8-bits!
//i1.getColorModel().getColorSpace();
//i1.getData().getSample(x, y, b);
// i1.getData().getSampleModel().getSampleSize();
// FIXME So, should we compare in the common colour-space, or compare the data, or both!?
// TODO Decide on which accumulator to use. BitInteger or BigDecimal is probably best.
for (int i = 0; i < i1.getWidth(); i++) {
for (int j = 0; j < i1.getHeight(); j++) {
final Color c1 = new Color(i1.getRGB(i, j));
final Color c2 = new Color(i2.getRGB(i, j));
final int dr = c1.getRed() - c2.getRed();
final int dg = c1.getGreen() - c2.getGreen();
final int db = c1.getBlue() - c2.getBlue();
final int da = c1.getAlpha() - c2.getAlpha();
tr.add( dr*dr );
tg.add( dg*dg );
tb.add( db*db );
ta.add( da*da );
br = br.add( BigInteger.valueOf(dr*dr));
bg = bg.add( BigInteger.valueOf(dg*dg));
bb = bb.add( BigInteger.valueOf(db*db));
ba = ba.add( BigInteger.valueOf(da*da));
}
}
// Compute the mean square error:
double mse = (tr.getSum() + tg.getSum() + tb.getSum() + ta.getSum()) / (i1.getWidth() * i1.getHeight() * cc);
log.info("Mean square error: " + mse);
log.info("Mean square error 2: " + (br.doubleValue() + bg.doubleValue() + bb.doubleValue() + ba.doubleValue()) / (i1.getWidth() * i1.getHeight() * cc) );
if (mse == 0) {
log.warning("mse == 0 and so psnr will be infinity!");
}
// Also do the BigInteger calculation:
System.out.println("Got: br = "+br+", tr = "+tr.getSum());
System.out.println("Got: bg = "+bg+", tg = "+tg.getSum());
System.out.println("Got: bb = "+bb+", tb = "+tb.getSum());
System.out.println("Got: ba = "+ba+", ta = "+ta.getSum());
BigDecimal bmse = new BigDecimal("0.00");
bmse = bmse.add(new BigDecimal(br));
bmse = bmse.add(new BigDecimal(bg));
bmse = bmse.add(new BigDecimal(bb));
bmse = bmse.add(new BigDecimal(ba));
bmse = new BigDecimal( bmse.doubleValue() / (i1.getWidth() * i1.getHeight() * cc) );
System.out.println("bmse = "+bmse);
//mse = bmse.doubleValue();
// Get the bits per pixel:
// FIXME This may be approx, and need to be tightened up for the case where channels have variable depths.
if( bpp1 != bpp2 ) {
log.warning("Bits-per-pixel do not match up! bpp1 = "+bpp1+", bpp2 = "+bpp2);
}
double bpp = bpp1;
System.out.println("read bpp = "+bpp);
System.out.println("colcomp = "+cc);
// FIXME Actually, using getRGB reduces each channel to 8 bits:
bpp = 8;
// The maximum is therefore:
double max = Math.pow(2.0, bpp) - 1.0;
// Compute the peak signal to noise ratio:
double psnr = 10.0 * StrictMath.log10( max*max / mse );
log.info("Peak signal to noise ratio: " + psnr); //43.82041101171352
log.info("Peak signal to noise ratio (BigDecimal): " + 10.0 * StrictMath.log10(max*max/bmse.doubleValue()) );
return new Double( psnr );
}
public static void main( String[] args ) throws IOException {
/*
JavaImageIOCompare c = new JavaImageIOCompare();
System.out.println("SD: "+c.describe().toXmlFormatted());
*/
for( String fmt : ImageIO.getReaderFileSuffixes() ) {
System.out.println("Reads "+fmt);
}
BufferedImage i1 = ImageIO.read( new File("/home/anj/Documents/DPT/SampleContent/lossless masters tif batch3/", "Sample_A.tif") ); // .im.png
System.out.println("i1 = "+i1);
BufferedImage i2 = ImageIO.read( new File("/home/anj/Documents/DPT/SampleContent/lossless masters tif batch3/", "Sample_A.tifE4.jp2.p.tif") ); // .p.tif.im.png
System.out.println("i2 = "+i2);
// Do the comparison...
double psnr = psnr(i1,i2);
System.out.println("psnr = "+psnr);
}
}