Skip to content

Commit a9f0d28

Browse files
committed
finalize opaque image handling fixes #38
1 parent 95348f3 commit a9f0d28

File tree

12 files changed

+232
-45
lines changed

12 files changed

+232
-45
lines changed

Diff for: README.md

+40-20
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55
[![Travis](https://travis-ci.org/KilianB/JImageHash.svg?branch=master)](https://travis-ci.org/KilianB/JImageHash)
66
[![GitHub license](https://img.shields.io/github/license/KilianB/JImageHash.svg)](https://github.com/KilianB/JImageHash/blob/master/LICENSE)
77
[![Download](https://api.bintray.com/packages/kilianb/maven/JImageHash/images/download.svg)](https://bintray.com/kilianb/maven/JImageHash/_latestVersion)
8-
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/3c7db745b9ff4dd9b89484a6aa46ad2f)](https://www.codacy.com/app/KilianB/JImageHash?utm_source=github.com&utm_medium=referral&utm_content=KilianB/JImageHash&utm_campaign=Badge_Grade)
8+
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/3c7db745b9ff4dd9b89484a6aa46ad2f)](https://www.codacy.com/app/KilianB/JImageHash?utm_source=github.com&utm_medium=referral&utm_content=KilianB/JImageHash&utm_campaign=Badge_Grade)
99

1010
JImageHash is a performant perceptual image fingerprinting library entirely written in Java. The library returns a similarity score aiming to identify entities which are likely modifications of the original source while being robust various attack vectors ie. color, rotation and scale transformation.
1111

12-
> A perceptual hash is a fingerprint of a multimedia file derived from various features from its content. Unlike cryptographic hash functions which rely on the avalanche effect of small changes in input leading to drastic changes in the output, perceptual hashes are "close" to one another if the features are similar.
12+
> A perceptual hash is a fingerprint of a multimedia file derived from various features from its content. Unlike cryptographic hash functions which rely on the avalanche effect of small changes in input leading to drastic changes in the output, perceptual hashes are "close" to one another if the features are similar.
1313
14-
This library was inspired by _Dr. Neal Krawetz_ blog post "<a href="http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html">kind of like that</a>" and incorporates several improvements. A comprehensive overview of perceptual image hashing can be found in this <a href="https://www.phash.org/docs/pubs/thesis_zauner.pdf">paper</a> by Christoph Zauner.
14+
This library was inspired by _Dr. Neal Krawetz_ blog post "<a href="http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html">kind of like that</a>" and incorporates several improvements. A comprehensive overview of perceptual image hashing can be found in this <a href="https://www.phash.org/docs/pubs/thesis_zauner.pdf">paper</a> by Christoph Zauner.
1515

1616
## Maven
1717

18-
The project is hosted on maven central. __Please be aware that migrating from one major version to another usually invalidates created hashes in order to retain validity when persistently storing the hashes.__
18+
The project is hosted on maven central. **Please be aware that migrating from one major version to another usually invalidates created hashes in order to retain validity when persistently storing the hashes.**
1919

20-
````XML
20+
```XML
2121
<dependency>
2222
<groupId>dev.brachtendorf</groupId>
2323
<artifactId>JImageHash</artifactId>
@@ -30,40 +30,61 @@ The project is hosted on maven central. __Please be aware that migrating from on
3030
<artifactId>h2</artifactId>
3131
<version>1.4.197</version>
3232
</dependency>
33-
````
33+
```
3434

3535
## Hello World
3636

37-
````Java
37+
```Java
3838
File img0 = new File("path/to/file.png");
3939
File img1 = new File("path/to/secondFile.jpg");
40-
40+
4141
HashingAlgorithm hasher = new PerceptiveHash(32);
42-
42+
4343
Hash hash0 = hasher.hash(img0);
4444
Hash hash1 = hasher.hash(img1);
45-
45+
4646
double similarityScore = hash0.normalizedHammingDistance(hash1);
47-
47+
4848
if(similarityScore < .2) {
4949
//Considered a duplicate in this particular case
5050
}
51-
51+
5252
//Chaining multiple matcher for single image comparison
5353

5454
SingleImageMatcher matcher = new SingleImageMatcher();
5555
matcher.addHashingAlgorithm(new AverageHash(64),.3);
5656
matcher.addHashingAlgorithm(new PerceptiveHash(32),.2);
57-
57+
5858
if(matcher.checkSimilarity(img0,img1)) {
5959
//Considered a duplicate in this particular case
6060
}
61-
````
61+
```
6262

6363
## Examples
6464

65-
Examples and convenience methods can be found in the [examples repository](https://github.com/KilianB/JImageHash-Examples)
65+
Examples and convenience methods can be found in the [examples repository](https://github.com/KilianB/JImageHash-Examples)
66+
67+
## Transparent image support
68+
69+
Support for transparent images has to be enabled specifically due to backwards compatibility and force users of the libraries to understand the implication of this setting.
70+
71+
The `setOpaqueHandling(Color? replacementColor, int alphaThreshold)` will replace transparent pixels with the specified color before calculating the hash.
72+
73+
### Be aware of the following culprits:
6674

75+
- the replacement color must be consistent throughout hash calculation for the entire sample space to ensure robustness against color transformations of the images.
76+
- the replacement color should be a color that does not appear often in the input space to avoid masking out available information.
77+
- when not specified `Orange` will be used as replacement. This choice was arbitrary and ideally, a default color should be chosen which results in 0 and 1 bits being computed in 50% of the time in respect to all other pixels and hashing algorithms.
78+
- supplying a replacement value of null will attempt to either use black or white as a replacement color conflicting with the advice given above. Computing the contrast color will fail if the transparent area of an image covers a large space and comes with a steep performance penalty.
79+
80+
```java
81+
HashingAlgorithm hasher = new PerceptiveHash(32);
82+
83+
//Replace all pixels with alpha values smaller than 0-255. The alpha value cutoff is taken into account after down scaling the image, therefore choose a reasonable value.
84+
int alphaThreshold = 253;
85+
hasher.setOpaqueHandling(alphaThreshold)
86+
87+
```
6788

6889
## Multiple types image matchers are available for each situation
6990

@@ -89,15 +110,15 @@ The `exotic` package features BloomFilter, and the SingleImageMatcher used to ma
89110
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
90111
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
91112
<td><p align="center"><image src="https://via.placeholder.com/30/DC143C?text=+"/></p></td>
92-
</tr>
113+
</tr>
93114

94115
<tr> <td>Altered Copyright</td> <td><img width= 75% src="https://user-images.githubusercontent.com/9025925/36542411-0438eb36-17e1-11e8-9a59-2c69937560bf.jpg"> </td>
95116
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
96117
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
97118
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
98119
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
99120
<td><p align="center"><image src="https://via.placeholder.com/30/DC143C?text=+"/></p></td>
100-
</tr>
121+
</tr>
101122

102123
<tr> <td>Thumbnail</td> <td><img src="https://user-images.githubusercontent.com/9025925/36542415-04ca8078-17e1-11e8-9be4-9a90b08c404b.jpg"></td>
103124
<td><p align="center"><image src="https://via.placeholder.com/30/228B22?text=+"/></p></td>
@@ -117,10 +138,10 @@ The `exotic` package features BloomFilter, and the SingleImageMatcher used to ma
117138

118139
</table>
119140

120-
121141
## Hashing algorithm
122142

123143
Image matchers can be configured using different algorithm. Each comes with individual properties
144+
124145
<table>
125146
<tr><th>Algorithm</th> <th>Feature</th><th>Notes</th> </tr>
126147
<tr><td><a href="https://github.com/KilianB/JImageHash/wiki/Hashing-Algorithms#averagehash-averagekernelhash-medianhash-averagecolorhash">AverageHash</a></td> <td>Average Luminosity</td> <td>Fast and good all purpose algorithm</td> </tr>
@@ -143,8 +164,7 @@ Image clustering with fuzzy hashes allowing to represent hashes with probability
143164

144165
![1_fxpw79yoon8xo3slqsvmta](https://user-images.githubusercontent.com/9025925/51272388-439d9600-19ca-11e9-8220-fe3539ed6061.png)
145166

146-
147-
### Algorithm benchmarking
167+
### Algorithm benchmarking
148168

149169
See the wiki page on how to test different hashing algorithms with your set of images
150170

Diff for: src/main/java/com/github/kilianB/hashAlgorithms/HashingAlgorithm.java

+75-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import javax.imageio.ImageIO;
1414

1515
import dev.brachtendorf.Require;
16+
import dev.brachtendorf.graphics.ColorUtil;
1617
import dev.brachtendorf.graphics.FastPixel;
1718
import dev.brachtendorf.graphics.ImageUtil;
1819

@@ -69,10 +70,10 @@ public abstract class HashingAlgorithm implements Serializable {
6970
private int algorithmId;
7071

7172
/** Color used in replacement of opaque pixels */
72-
protected Color opaqueReplacementColor = Color.WHITE;
73+
protected Color opaqueReplacementColor = Color.orange;
7374

7475
/** Maximum alpha value a pixel must have in order to be replaced */
75-
protected int opaqueReplacementThreshold = 2;
76+
protected int opaqueReplacementThreshold = -1;
7677

7778
/**
7879
* After a hash was created or the id was calculated the object may not be
@@ -110,7 +111,10 @@ public HashingAlgorithm(int bitResolution) {
110111
* entirely black image while for the user these images are perceptually
111112
* different.
112113
*
113-
* @param replacementColor The color used to replace opaque values
114+
* @param replacementColor The color used to replace opaque values. A color
115+
* should be chosen which is unlikely to be part of the
116+
* target images. By default an orange color is
117+
* selected. If a value of null is provided
114118
* @param alphaThreshold All colors with a value lower or equal value [0-255]
115119
* will be replaced.
116120
* <ul>
@@ -127,11 +131,35 @@ public void setOpaqueHandling(Color replacementColor, int alphaThreshold) {
127131
if (immutableState) {
128132
throw new IllegalStateException(LOCKED_MODIFICATION_EXCEPTION);
129133
}
130-
131134
this.opaqueReplacementColor = replacementColor;
132135
this.opaqueReplacementThreshold = alphaThreshold;
133136
}
134137

138+
/**
139+
* Define how the algorithm shall handle images with alpha value. Hashing
140+
* algorithms usually depend on the luminosity value, which by default will be
141+
* treated as being black.
142+
* <p>
143+
* Sometimes display software may choose to display missing pixels in a
144+
* different color e.g. white. For the algorithm this would result in an
145+
* entirely black image while for the user these images are perceptually
146+
* different.
147+
*
148+
* @param alphaThreshold All colors with a value lower or equal value [0-255]
149+
* will be replaced.
150+
* <ul>
151+
* <li>0 means only invisible (entirely opaque pixels will
152+
* be replaced)</li>
153+
* <li></li>
154+
* </ul>
155+
* @throws IllegalStateException if a hash was already created and the object is
156+
* considered immutable.
157+
* @since 3.0.1
158+
*/
159+
public void setOpaqueHandling(int alphaThreshold) {
160+
setOpaqueHandling(this.opaqueReplacementColor, alphaThreshold);
161+
}
162+
135163
/**
136164
* @return color used in replacement of opaque pixels
137165
* @since 3.0.1
@@ -266,9 +294,26 @@ public Hash hash(File imageFile) throws IOException {
266294
protected abstract BigInteger hash(BufferedImage image, HashBuilder hashBuilder);
267295

268296
protected FastPixel createPixelAccessor(BufferedImage image, int width, int height) {
269-
FastPixel fp = FastPixel.create(ImageUtil.getScaledInstance(image, width, height));
270-
if (this.opaqueReplacementThreshold > 0) {
271-
fp.setReplaceOpaqueColors(this.opaqueReplacementThreshold, this.opaqueReplacementColor);
297+
298+
BufferedImage scaledInstance = ImageUtil.getScaledInstance(image, width, height);
299+
FastPixel fp = FastPixel.create(scaledInstance);
300+
301+
// If opaque handling is specified and the image has an alpha channel
302+
if (this.opaqueReplacementThreshold >= 0 && fp.hasAlpha()) {
303+
/**
304+
* If no color is specified grab the contrast color. This operation might not be
305+
* the best for hash calculation depending on how the color is interpolated. The
306+
* interpolated Color might be black for white images if the alpha is in the
307+
* majority.
308+
*/
309+
if (this.opaqueReplacementColor == null) {
310+
311+
javafx.scene.paint.Color interpolatedColor = ImageUtil.interpolateColor(image);
312+
Color replacementColor = ColorUtil.getContrastColor(ColorUtil.fxToAwtColor(interpolatedColor));
313+
fp.setReplaceOpaqueColors(this.opaqueReplacementThreshold, replacementColor);
314+
} else {
315+
fp.setReplaceOpaqueColors(this.opaqueReplacementThreshold, this.opaqueReplacementColor);
316+
}
272317
}
273318
return fp;
274319
}
@@ -291,6 +336,13 @@ public final int algorithmId() {
291336
algorithmId = 31 * precomputeAlgoId();
292337
// Make sure the algo id doesn't collide with version 2.0.0 id's
293338
algorithmId = 31 * algorithmId + 5 + preProcessing.hashCode();
339+
340+
// Change hash code only if transparency is supported
341+
if (this.opaqueReplacementThreshold >= 0) {
342+
algorithmId = 31 * algorithmId
343+
+ Objects.hash(this.opaqueReplacementThreshold, this.opaqueReplacementColor);
344+
}
345+
294346
immutableState = true;
295347
}
296348
return algorithmId;
@@ -442,6 +494,22 @@ public boolean equals(Object obj) {
442494
HashingAlgorithm other = (HashingAlgorithm) obj;
443495
if (algorithmId() != other.algorithmId())
444496
return false;
497+
498+
if (this.opaqueReplacementThreshold >= 0 && other.opaqueReplacementThreshold >= 0) {
499+
500+
if (this.opaqueReplacementThreshold != other.opaqueReplacementThreshold) {
501+
return false;
502+
}
503+
if (!this.opaqueReplacementColor.equals(other.opaqueReplacementColor)) {
504+
return false;
505+
}
506+
507+
} else if (this.opaqueReplacementThreshold < 0 && other.opaqueReplacementThreshold < 0) {
508+
509+
} else {
510+
return false;
511+
}
512+
445513
return true;
446514
}
447515
}

Diff for: src/main/java/com/github/kilianB/hashAlgorithms/experimental/HogHash.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ public HogHash(int bitResolution) {
161161

162162
@Override
163163
protected BigInteger hash(BufferedImage image, HashBuilder hash) {
164-
165-
BufferedImage bi = ImageUtil.getScaledInstance(image, width, height);
166-
FastPixel fp = FastPixel.create(bi);
164+
FastPixel fp = createPixelAccessor(image, width, height);
167165

168166
int[][] lum = fp.getLuma();
169167

Diff for: src/main/java/com/github/kilianB/hashAlgorithms/experimental/HogHashAngularEncoded.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ public HogHashAngularEncoded(int width, int height, int cellWidth, int numBins)
5959

6060
@Override
6161
protected BigInteger hash(BufferedImage image, HashBuilder hash) {
62-
63-
BufferedImage bi = ImageUtil.getScaledInstance(image, width, height);
64-
FastPixel fp = FastPixel.create(bi);
62+
FastPixel fp = createPixelAccessor(image, width, height);
6563

6664
int[][] lum = fp.getLuma();
6765

Diff for: src/main/java/com/github/kilianB/hashAlgorithms/experimental/HogHashDual.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,7 @@ public HogHashDual(int bitResolution) {
6666

6767
@Override
6868
protected BigInteger hash(BufferedImage image, HashBuilder hash) {
69-
70-
71-
BufferedImage bi = ImageUtil.getScaledInstance(image, width, height);
72-
FastPixel fp = FastPixel.create(bi);
69+
FastPixel fp = createPixelAccessor(image, width, height);
7370

7471
int[][] lum = fp.getLuma();
7572

Diff for: src/test/java/com/github/kilianB/TestResources.java

+23
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ public class TestResources {
2525
public static BufferedImage highQuality;
2626
public static BufferedImage lowQuality;
2727
public static BufferedImage thumbnail;
28+
2829
public static BufferedImage lenna;
2930
public static BufferedImage lenna90;
3031
public static BufferedImage lenna180;
3132
public static BufferedImage lenna270;
3233

3334
public static BufferedImage white;
3435

36+
public static BufferedImage transparent0;
37+
public static BufferedImage transparent1;
38+
public static BufferedImage transparent0White;
39+
public static BufferedImage transparent1White;
40+
3541
static {
3642
try {
3743
ballon = ImageIO.read(TestResources.class.getClassLoader().getResourceAsStream("ballon.jpg"));
@@ -45,6 +51,15 @@ public class TestResources {
4551
lenna270 = ImageIO.read(TestResources.class.getClassLoader().getResourceAsStream("Lenna270.png"));
4652

4753
white = ImageIO.read(TestResources.class.getClassLoader().getResourceAsStream("white.jpg"));
54+
55+
transparent0 = ImageIO.read(TestResources.class.getClassLoader().getResourceAsStream("transparent0.png"));
56+
transparent1 = ImageIO.read(TestResources.class.getClassLoader().getResourceAsStream("transparent1.png"));
57+
58+
transparent0White = ImageIO
59+
.read(TestResources.class.getClassLoader().getResourceAsStream("transparent0White.png"));
60+
transparent1White = ImageIO
61+
.read(TestResources.class.getClassLoader().getResourceAsStream("transparent1White.png"));
62+
4863
} catch (IOException e) {
4964
e.printStackTrace();
5065
}
@@ -71,6 +86,14 @@ public void allResourcesLoaded() {
7186
assertTrue(lenna180.getWidth() > 0);
7287
}, () -> {
7388
assertTrue(lenna270.getWidth() > 0);
89+
}, () -> {
90+
assertTrue(transparent0.getWidth() > 0);
91+
}, () -> {
92+
assertTrue(transparent1.getWidth() > 0);
93+
}, () -> {
94+
assertTrue(transparent0White.getWidth() > 0);
95+
}, () -> {
96+
assertTrue(transparent1White.getWidth() > 0);
7497
}, () -> {
7598
assertTrue(white.getWidth() > 0);
7699
});

0 commit comments

Comments
 (0)