├── .gitignore
├── pom.xml
└── src
└── main
└── java
└── com
└── virjar
└── image
└── magic
├── ImageAvgMerger.java
├── ImageCategory.java
├── ImageHilltopV2.java
├── Main.java
├── SimilarImageSearcher.java
└── libs
├── ImageHistogram.java
├── ImagePHash.java
└── ImageUtils.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea/
3 | target/
4 | .DS_Store
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.virjar.image
8 | image-magic
9 | 1.0
10 |
11 |
12 |
13 | org.projectlombok
14 | lombok
15 | 1.18.16
16 | provided
17 |
18 |
19 |
20 |
21 |
22 |
23 | org.apache.maven.plugins
24 | maven-compiler-plugin
25 | 3.3
26 |
27 | 1.8
28 | 1.8
29 | 1.8
30 | UTF-8
31 |
32 |
33 |
34 |
35 |
36 |
37 | release-int
38 | Release Repository
39 | http://nexus.virjar.com/repository/maven-releases/
40 |
41 |
42 | snapshot-int
43 | Snapshot Repository
44 | http://nexus.virjar.com/repository/maven-snapshots/
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/ImageAvgMerger.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic;
2 |
3 | import com.virjar.image.magic.libs.ImageUtils;
4 |
5 | import java.awt.image.BufferedImage;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.TreeMap;
9 |
10 | /**
11 | * 相似图合并,求相似图的最真图
12 | */
13 | public class ImageAvgMerger {
14 |
15 | private static class RGBP {
16 | long r = 0;
17 | long g = 0;
18 | long b = 0;
19 | long p = 0;
20 |
21 | int totalRecord = 0;
22 |
23 | void setRGBP(int rgbp) {
24 | totalRecord++;
25 | r += (rgbp >>> 24 & 0xff);
26 | g += (rgbp >>> 16 & 0xff);
27 | b += (rgbp >>> 8 & 0xff);
28 | p += (rgbp & 0xff);
29 | }
30 |
31 | int avgRGB() {
32 | return shift(r, 24, totalRecord)
33 | | shift(g, 16, totalRecord)
34 | | shift(b, 8, totalRecord)
35 | | shift(p, 0, totalRecord);
36 | }
37 |
38 |
39 | private int shift(long val, int shiftSize, int totalRecord) {
40 | return ((int) (val / totalRecord)) << shiftSize;
41 | }
42 | }
43 |
44 |
45 | public static BufferedImage avg(List input) {
46 | if (input.size() == 0) {
47 | throw new IllegalStateException("input image can not be empty!!");
48 | }
49 | long widthTotal = 0, heightTotal = 0;
50 | for (BufferedImage bufferedImage : input) {
51 | widthTotal += bufferedImage.getWidth();
52 | heightTotal += bufferedImage.getHeight();
53 | }
54 | int width = (int) (widthTotal / input.size());
55 | int height = (int) (heightTotal / input.size());
56 |
57 | RGBP[][] points = new RGBP[width][height];
58 | for (int i = 0; i < width; i++) {
59 | for (int j = 0; j < height; j++) {
60 | points[i][j] = new RGBP();
61 | }
62 | }
63 |
64 | // 大规模计算的时候发现,BuffedImage内部有比较大的计算量,所以这里直接变成数组
65 | ArrayList thumbedImages = new ArrayList<>();
66 |
67 | for (BufferedImage bufferedImage : input) {
68 | if (bufferedImage.getWidth() != width
69 | || bufferedImage.getHeight() != height
70 | ) {
71 | bufferedImage = ImageUtils.thumb(bufferedImage, width, height);
72 | }
73 | int[][] imgData = new int[width][height];
74 | thumbedImages.add(imgData);
75 | for (int i = 0; i < width; i++) {
76 | for (int j = 0; j < height; j++) {
77 | int rgb = bufferedImage.getRGB(i, j);
78 | imgData[i][j] = rgb;
79 | points[i][j].setRGBP(rgb);
80 | }
81 | }
82 | }
83 |
84 | int[][] firstAvgImg = new int[width][height];
85 |
86 | for (int i = 0; i < width; i++) {
87 | for (int j = 0; j < height; j++) {
88 | firstAvgImg[i][j] = points[i][j].avgRGB();
89 | }
90 | }
91 |
92 | BufferedImage out = new BufferedImage(width, height, input.get(0).getType());
93 | for (int i = 0; i < width; i++) {
94 | for (int j = 0; j < height; j++) {
95 | TreeMap topPoint = new TreeMap<>();
96 | int index = 0;
97 | for (int[][] bufferedImage : thumbedImages) {
98 | int rgbDiff = ImageUtils.rgbDiff(bufferedImage[i][j],
99 | firstAvgImg[i][j]);
100 | topPoint.put((((long) rgbDiff) << 32) + index, bufferedImage[i][j]);
101 | index++;
102 | }
103 | // 只保留 3/4的图像,去除尾部,认为尾部为差异内容
104 | int avgPointSize = (int) (thumbedImages.size() * 0.85);
105 | RGBP rgbp = new RGBP();
106 | int avgPointIndex = 0;
107 | for (long key : topPoint.keySet()) {
108 | rgbp.setRGBP(topPoint.get(key));
109 | avgPointIndex++;
110 | if (avgPointIndex >= avgPointSize) {
111 | break;
112 | }
113 | }
114 | out.setRGB(i, j, rgbp.avgRGB());
115 | }
116 | }
117 | return out;
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/ImageCategory.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic;
2 |
3 | import com.virjar.image.magic.libs.ImageHistogram;
4 | import com.virjar.image.magic.libs.ImagePHash;
5 | import lombok.Getter;
6 |
7 | import javax.imageio.ImageIO;
8 | import java.awt.image.BufferedImage;
9 | import java.io.File;
10 | import java.io.FileInputStream;
11 | import java.io.FileOutputStream;
12 | import java.io.IOException;
13 | import java.security.MessageDigest;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | /**
18 | * 图片分类,计算为多个相似图
19 | */
20 | public class ImageCategory {
21 |
22 | /**
23 | * 对图片进行分类
24 | *
25 | * @param sourceDir 原图片内容
26 | * @param outDir 目标图片内容
27 | * @throws IOException
28 | */
29 | public static void doCategory(File sourceDir, File outDir) throws IOException {
30 | if (!outDir.exists()) {
31 | if (!outDir.mkdirs()) {
32 | throw new IOException("can not create dir: " + outDir.getAbsolutePath());
33 | }
34 | }
35 | List imageGroups = doCategory(sourceDir);
36 | for (ImageGroup imageGroup : imageGroups) {
37 | File groupDir = new File(outDir, imageGroup.baseFileHash);
38 | if (!groupDir.exists()) {
39 | groupDir.mkdirs();
40 | }
41 | for (File file : imageGroup.files) {
42 | File target = new File(groupDir, file.getName());
43 | if (!file.renameTo(target)) {
44 | // 不在一个盘符下,通过copy流的方式
45 | FileOutputStream fileOutputStream = new FileOutputStream(target);
46 | FileInputStream fileInputStream = new FileInputStream(file);
47 | byte[] buf = new byte[1024];
48 | int readCount;
49 | while ((readCount = fileInputStream.read(buf)) > 0) {
50 | fileOutputStream.write(buf, 0, readCount);
51 | }
52 | fileInputStream.close();
53 | fileOutputStream.close();
54 | file.delete();
55 | }
56 | }
57 |
58 | }
59 | }
60 |
61 |
62 | /**
63 | * 对图片进行分类
64 | *
65 | * @param sourceDir 原图片内容
66 | * @return 分类后的模型对象
67 | */
68 | public static List doCategory(File sourceDir) {
69 | List imageGroups = new ArrayList<>();
70 | File[] imageFiles = sourceDir.listFiles(File::isFile);
71 | if (imageFiles == null) {
72 | System.out.println("can not scan file from: " + sourceDir.getAbsolutePath());
73 | return imageGroups;
74 | }
75 |
76 | for (File file : imageFiles) {
77 | BufferedImage bufferedImage;
78 | try {
79 | bufferedImage = ImageIO.read(file);
80 | } catch (Exception e) {
81 | //ignore
82 | continue;
83 | }
84 |
85 | boolean found = false;
86 | for (ImageGroup imageGroup : imageGroups) {
87 | if (imageGroup.isSimilar(bufferedImage)) {
88 | imageGroup.addFile(file);
89 | found = true;
90 | break;
91 | }
92 | }
93 |
94 | if (!found) {
95 | imageGroups.add(new ImageGroup(file, bufferedImage));
96 | }
97 | }
98 | return imageGroups;
99 | }
100 |
101 | private static final ImagePHash sImagePHash = new ImagePHash();
102 |
103 | @Getter
104 | private static class ImageGroup {
105 | private final File file;
106 | private String baseFileHash;
107 | private final String imagePHash;
108 | private final BufferedImage bufferedImage;
109 | private final ImageHistogram imageHistogram;
110 |
111 | private final List files = new ArrayList<>();
112 |
113 |
114 | public ImageGroup(File file, BufferedImage bufferedImage) {
115 | this.file = file;
116 | this.bufferedImage = bufferedImage;
117 | this.imagePHash = sImagePHash.getHash(bufferedImage);
118 | imageHistogram = new ImageHistogram(bufferedImage);
119 | baseFileHash = fileHash(file);
120 | addFile(file);
121 | }
122 |
123 | private void addFile(File file) {
124 | files.add(file);
125 | }
126 |
127 |
128 | private boolean isSimilar(BufferedImage bufferedImage) {
129 | int distance = ImagePHash.distance(sImagePHash.getHash(bufferedImage), null);
130 | if (distance <= 6) {
131 | return true;
132 | }
133 | if (distance > 20) {
134 | return false;
135 | }
136 | double match = imageHistogram.match(bufferedImage);
137 | return match > 0.8;
138 | }
139 | }
140 |
141 | private static String fileHash(File file) {
142 | try {
143 | byte[] buffer = new byte[1024];
144 | FileInputStream fileInputStream = new FileInputStream(file);
145 | MessageDigest md5 = MessageDigest.getInstance("MD5");
146 | int numRead;
147 | while ((numRead = fileInputStream.read(buffer)) > 0) {
148 | md5.update(buffer, 0, numRead);
149 | }
150 | fileInputStream.close();
151 | byte[] digest = md5.digest();
152 | StringBuilder sb = new StringBuilder(digest.length * 2);
153 | for (byte b1 : digest) {
154 | sb.append(hexChar[((b1 & 0xF0) >>> 4)]);
155 | sb.append(hexChar[(b1 & 0xF)]);
156 | }
157 | return sb.toString();
158 | } catch (Exception e) {
159 | throw new RuntimeException(e);
160 | }
161 | }
162 |
163 | private static char[] hexChar = {'0', '1', '2', '3',
164 | '4', '5', '6', '7',
165 | '8', '9', 'a', 'b',
166 | 'c', 'd', 'e', 'f'};
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/ImageHilltopV2.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic;
2 |
3 |
4 | import com.virjar.image.magic.libs.ImagePHash;
5 | import com.virjar.image.magic.libs.ImageUtils;
6 | import lombok.Getter;
7 |
8 | import javax.imageio.ImageIO;
9 | import java.awt.image.BufferedImage;
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | /**
16 | * 山顶坐标计算
17 | */
18 | @SuppressWarnings("ALL")
19 | public class ImageHilltopV2 {
20 |
21 | /**
22 | * 计算图像上的前N个物体
23 | *
24 | * @param hilltopParamAndResult 入参封装,包括原图、输入图、物体大小、物体个数
25 | * @return 前N个坐标点
26 | */
27 | public static List topN(HilltopParamAndResult hilltopParamAndResult) {
28 | hilltopParamAndResult.width = hilltopParamAndResult.challengeImage.getWidth();
29 | hilltopParamAndResult.height = hilltopParamAndResult.challengeImage.getHeight();
30 |
31 | if (hilltopParamAndResult.backgroundImage.getWidth() != hilltopParamAndResult.width || hilltopParamAndResult.backgroundImage.getHeight() != hilltopParamAndResult.height) {
32 | hilltopParamAndResult.backgroundImage = ImageUtils.thumb(hilltopParamAndResult.backgroundImage, hilltopParamAndResult.width, hilltopParamAndResult.height);
33 | }
34 |
35 | long totalDiff = 0;
36 | int[][] diff = new int[hilltopParamAndResult.width][hilltopParamAndResult.height];
37 |
38 | long[][] calculateDiff = new long[hilltopParamAndResult.width][hilltopParamAndResult.height];
39 |
40 | for (int i = 0; i < hilltopParamAndResult.width; i++) {
41 | for (int j = 0; j < hilltopParamAndResult.height; j++) {
42 | int rgbDiff = ImageUtils.rgbDiff(hilltopParamAndResult.backgroundImage.getRGB(i, j), hilltopParamAndResult.challengeImage.getRGB(i, j));
43 | diff[i][j] = rgbDiff;
44 | calculateDiff[i][j] = rgbDiff;
45 | totalDiff += rgbDiff;
46 | }
47 | }
48 | hilltopParamAndResult.diff = diff;
49 | hilltopParamAndResult.avgDiff = (int) (totalDiff / (hilltopParamAndResult.width * hilltopParamAndResult.height));
50 |
51 | AggregateMountain aggregateMountain = new AggregateMountain(calculateDiff, hilltopParamAndResult.width,
52 | hilltopParamAndResult.height, hilltopParamAndResult);
53 | aggregateMountain.genAggregateMountainMapping();
54 | aggregateMountain.invalidRectangle(0, 0, hilltopParamAndResult.width - 1, hilltopParamAndResult.height - 1);
55 | aggregateMountain.saveImage();
56 |
57 | List ret = new ArrayList<>();
58 |
59 | for (int i = 0; i < hilltopParamAndResult.topN; i++) {
60 | // 最高的一个点,这个点是基于边长5个像素点判定的
61 | AggregateMountain.XY topXY = aggregateMountain.fetchTopPoint();
62 | // topPoint没有考虑斑点大小,都是按照5个像素斑点计算的
63 | // 所以这个topInt需要根据实际的斑点大小进行二次调整
64 | topXY = adjustCenterPoint(topXY, aggregateMountain, hilltopParamAndResult);
65 |
66 | Point point = new Point();
67 | point.x = topXY.x;
68 | point.y = topXY.y;
69 | point.weight = topXY.weight;
70 | point.hilltopParamAndResult = hilltopParamAndResult;
71 | ret.add(point);
72 |
73 | if (i < hilltopParamAndResult.topN - 1) {
74 | // 抹除当前点的数据,这样从新扫描将会得到下个断点
75 | tripAggregateMountain(aggregateMountain, topXY, hilltopParamAndResult);
76 | }
77 | }
78 |
79 | return ret;
80 | }
81 |
82 | private static AggregateMountain.XY adjustCenterPoint(AggregateMountain.XY topXY, AggregateMountain aggregateMountain,
83 | HilltopParamAndResult hilltopParamAndResult) {
84 |
85 | Rectangle candidatePoints = rectangleRange(topXY.x, topXY.y, hilltopParamAndResult.chSize * 2, hilltopParamAndResult.width, hilltopParamAndResult.height);
86 |
87 | // thumbTimes 缩放倍数,使用开方的方式估算,这样可以减少两个平方的时间复杂度
88 | int thumbTimes = (int) Math.sqrt(hilltopParamAndResult.chSize);
89 | // 创建缩略图,进行快速定位
90 | int shortCurtWith = (hilltopParamAndResult.chSize) / thumbTimes;
91 | shortCurtWith *= 2;
92 |
93 | long[][] shortCurt = new long[shortCurtWith][shortCurtWith];
94 |
95 | for (int i = 0; i < shortCurtWith; i++) {
96 | for (int j = 0; j < shortCurtWith; j++) {
97 |
98 | int startX = i * thumbTimes + candidatePoints.leftTopX;
99 | int startY = j * thumbTimes + candidatePoints.leftTopY;
100 | int endX = Math.min(startX + thumbTimes - 1, hilltopParamAndResult.width - 1);
101 | int endY = Math.min(startY + thumbTimes - 1, hilltopParamAndResult.height - 1);
102 |
103 | long totalDiff = 0;
104 | for (int x = startX; x <= endX; x++) {
105 | for (int y = startY; y <= endY; y++) {
106 | totalDiff += hilltopParamAndResult.diff[x][y];
107 | }
108 | }
109 | shortCurt[i][j] = totalDiff;
110 | }
111 | }
112 |
113 | saveImage(shortCurt, shortCurtWith, shortCurtWith);
114 |
115 | int shortCurtMontainWith = shortCurtWith / 2;
116 |
117 | AggregateMountain.XY shortCurtxy = new AggregateMountain.XY();
118 | long[][] shortCurtMountain = new long[shortCurtMontainWith][shortCurtMontainWith];
119 | for (int i = 0; i < shortCurtMontainWith; i++) {
120 | for (int j = 0; j < shortCurtMontainWith; j++) {
121 |
122 | int shortCurtCenterX = i + shortCurtMontainWith / 2;
123 | int shortCurtCenterY = j + shortCurtMontainWith / 2;
124 |
125 | Rectangle aggredateRange = rectangleRange(shortCurtCenterX, shortCurtCenterY,
126 | shortCurtMontainWith,
127 | shortCurtWith, shortCurtWith);
128 | double aggretateDiff = 0;
129 |
130 | for (int aggredateRangeI = aggredateRange.leftTopX; aggredateRangeI <= aggredateRange.rightBottomX; aggredateRangeI++) {
131 | for (int aggredateRangeJ = aggredateRange.leftTopY; aggredateRangeJ <= aggredateRange.rightBottomY; aggredateRangeJ++) {
132 |
133 | long base = shortCurt[aggredateRangeI][aggredateRangeJ];
134 | double distance = Math.sqrt((aggredateRangeI - shortCurtCenterX) * (aggredateRangeI - shortCurtCenterX) + (aggredateRangeJ - shortCurtCenterY) * (aggredateRangeJ - shortCurtCenterY));
135 |
136 | double distanceRatio = distance / (sqrt2 * (shortCurtMontainWith / 2));
137 | if (distanceRatio > 1) {
138 | continue;
139 | }
140 | double ratio = (Math.cos(Math.PI * distanceRatio) + 1) / 2;
141 | aggretateDiff += base * base * base * ratio;
142 | }
143 | }
144 | shortCurtMountain[i][j] = (long) aggretateDiff;
145 | // System.out.println("shortCurtMountain:(" + i + "," + j + ") = " + shortCurtMountain[i][j]);
146 | shortCurtxy.update(shortCurtCenterX, shortCurtCenterY, (long) aggretateDiff);
147 | }
148 | }
149 |
150 | saveImage(shortCurtMountain, shortCurtMontainWith, shortCurtMontainWith);
151 |
152 | // 在缩略图里面寻找最高点,之后再回放到原图进行
153 |
154 | int realCandidateStartX = shortCurtxy.x * thumbTimes + candidatePoints.leftTopX;
155 | int realCandidateEndX = shortCurtxy.x * thumbTimes + thumbTimes + candidatePoints.leftTopX;
156 | int realCandidateStartY = shortCurtxy.y * thumbTimes + candidatePoints.leftTopY;
157 | int realCandidateEndY = shortCurtxy.y * thumbTimes + thumbTimes + candidatePoints.leftTopY;
158 |
159 |
160 | AggregateMountain.XY xy = new AggregateMountain.XY();
161 | for (int candidateI = realCandidateStartX; candidateI <= realCandidateEndX; candidateI++) {
162 | for (int candidateJ = realCandidateStartY; candidateJ <= realCandidateEndY; candidateJ++) {
163 | Rectangle aggredateRange = rectangleRange(candidateI, candidateJ, hilltopParamAndResult.chSize, hilltopParamAndResult.width, hilltopParamAndResult.height);
164 |
165 | double aggretateDiff = 0;
166 | for (int i = aggredateRange.leftTopX; i <= aggredateRange.rightBottomX; i++) {
167 | for (int j = aggredateRange.leftTopY; j <= aggredateRange.rightBottomY; j++) {
168 | double distance = Math.sqrt((i - candidateI) * (i - candidateI) + (j - candidateJ) * (j - candidateJ));
169 |
170 | double distanceRatio = distance / (sqrt2 * (hilltopParamAndResult.chSize / 2));
171 | if (distanceRatio > 1) {
172 | continue;
173 | }
174 | double ratio = (Math.cos(Math.PI * distanceRatio) + 1) / 2;
175 | aggretateDiff += aggregateMountain.diffData[i][j] * ratio;
176 | }
177 | }
178 | xy.update(candidateI, candidateJ, (long) aggretateDiff);
179 | }
180 | }
181 | return xy;
182 | }
183 |
184 | private static void tripAggregateMountain(AggregateMountain aggregateMountain, AggregateMountain.XY topXY,
185 | HilltopParamAndResult hilltopParamAndResult) {
186 | int stripStartX = Math.max(topXY.x - hilltopParamAndResult.chSize / 2, 0);
187 | int stripEndX = Math.min(topXY.x + hilltopParamAndResult.chSize / 2, hilltopParamAndResult.width - 1);
188 | int stripStartY = Math.max(topXY.y - hilltopParamAndResult.chSize / 2, 0);
189 | int stripEndY = Math.min(topXY.y + hilltopParamAndResult.chSize / 2, hilltopParamAndResult.height - 1);
190 |
191 | long maxDiff = 0;
192 | for (int i = stripStartX; i <= stripEndX; i++) {
193 | for (int j = stripStartY; j <= stripEndY; j++) {
194 | if (aggregateMountain.diffData[i][j] > maxDiff) {
195 | maxDiff = aggregateMountain.diffData[i][j];
196 | }
197 | }
198 | }
199 |
200 | for (int i = stripStartX; i <= stripEndX; i++) {
201 | for (int j = stripStartY; j <= stripEndY; j++) {
202 | double distance = Math.sqrt((i - topXY.x) * (i - topXY.x) + (j - topXY.y) * (j - topXY.y));
203 |
204 | double distanceRatio = distance / hilltopParamAndResult.chSize;
205 | if (distanceRatio > 1) {
206 | continue;
207 | }
208 | // y = 1- x*x / 2.25 权值衰减函数,为2次函数,要求命中坐标: (0,1) (1.5,0)
209 | // 当距离为0的时候,衰减权重为1,当距离为1.5的时候,衰减权重为0
210 | // 当距离为1的时候, 衰减权重为:1- 1/2.25 = 0.55
211 | aggregateMountain.diffData[i][j] -= maxDiff * (1 - distanceRatio * distanceRatio / 2.25);
212 | if (aggregateMountain.diffData[i][j] < 0) {
213 | aggregateMountain.diffData[i][j] = 0;
214 | }
215 | }
216 | }
217 |
218 | saveImage(aggregateMountain.diffData, aggregateMountain.width, aggregateMountain.height);
219 | aggregateMountain.invalidRectangle(stripStartX, stripStartY, stripEndX, stripEndY);
220 | }
221 |
222 | private static class AggregateMountain {
223 | private long[][] diffData;
224 | private int width;
225 | private int height;
226 | private HilltopParamAndResult hilltopParamAndResult;
227 |
228 | private AggregateMountain nextAggregateMountain = null;
229 | private AggregateMountain preAggregateMountain = null;
230 | private boolean isLast = false;
231 |
232 | private static class XY {
233 | private int x;
234 | private int y;
235 |
236 | private long weight = 0;
237 |
238 | public void update(int x, int y, long weight) {
239 | if (weight > this.weight) {
240 | this.x = x;
241 | this.y = y;
242 | this.weight = weight;
243 | }
244 | }
245 |
246 | }
247 |
248 |
249 | public XY fetchTopPoint() {
250 | if (isLast) {
251 | XY xy = new XY();
252 | for (int i = 0; i < width; i++) {
253 | for (int j = 0; j < height; j++) {
254 | xy.update(i, j, diffData[i][j]);
255 | }
256 | }
257 | return xy;
258 | }
259 |
260 | XY nextXy = nextAggregateMountain.fetchTopPoint();
261 | int startX = nextXy.x * 5;
262 | int endX = Math.min(nextXy.x * 5 + 4, width - 1);
263 | int startY = nextXy.y * 5;
264 | int endY = Math.min(nextXy.y * 5 + 4, height - 1);
265 |
266 | XY xy = new XY();
267 | for (int i = startX; i <= endX; i++) {
268 | for (int j = startY; j <= endY; j++) {
269 | xy.update(i, j, diffData[i][j]);
270 | }
271 | }
272 | return xy;
273 | }
274 |
275 | public AggregateMountain(long[][] diffData, int width, int height, HilltopParamAndResult hilltopParamAndResult) {
276 | this.diffData = diffData;
277 | this.width = width;
278 | this.height = height;
279 | this.hilltopParamAndResult = hilltopParamAndResult;
280 | }
281 |
282 | private void saveImage() {
283 | ImageHilltopV2.saveImage(diffData, width, height);
284 | }
285 |
286 |
287 | private void invalidRectangle(int leftTopX, int leftTopY, int rightBottomX, int rightBottomY) {
288 | if (isLast) {
289 | saveImage();
290 | return;
291 | }
292 | int nextDiffDataInvalidStartX = leftTopX / 5;
293 | int nextDiffDataInvalidStartY = leftTopY / 5;
294 |
295 | int nextDiffDataInvalidEndX = (rightBottomX + 4) / 5;
296 | int nextDiffDataInvalidEndY = (rightBottomY + 4) / 5;
297 |
298 | if (leftTopX % 5 != 0) {
299 | nextDiffDataInvalidStartX = Math.max(nextDiffDataInvalidStartX - 1, 0);
300 | }
301 |
302 | if (leftTopY % 5 != 0) {
303 | nextDiffDataInvalidStartY = Math.max(nextDiffDataInvalidStartY - 1, 0);
304 | }
305 |
306 | if (rightBottomX % 5 != 0) {
307 | nextDiffDataInvalidEndX = Math.min(nextDiffDataInvalidEndX + 1, nextAggregateMountain.width - 1);
308 | }
309 |
310 | if (rightBottomY % 5 != 0) {
311 | nextDiffDataInvalidEndY = Math.min(nextDiffDataInvalidEndY + 1, nextAggregateMountain.height - 1);
312 | }
313 | // fill in next diff data
314 | for (int i = nextDiffDataInvalidStartX; i <= nextDiffDataInvalidEndX; i++) {
315 | for (int j = nextDiffDataInvalidStartY; j <= nextDiffDataInvalidEndY; j++) {
316 | int scanStartX = i * 5;
317 | int scanStartY = j * 5;
318 |
319 | int scanEndX = Math.min(scanStartX + 4, width - 1);
320 | int scanEndY = Math.min(scanStartY + 4, height - 1);
321 | int centerX = (scanStartX + scanEndX) / 2;
322 | int centerY = (scanStartY + scanEndY) / 2;
323 |
324 |
325 | // long base = diffData[centerX][centerY];
326 |
327 | long aggretateDiff = 0;
328 | for (int nextI = scanStartX; nextI <= scanEndX; nextI++) {
329 | for (int nextJ = scanStartY; nextJ <= scanEndY; nextJ++) {
330 | aggretateDiff += diffData[nextI][nextJ];
331 | }
332 | }
333 | nextAggregateMountain.diffData[i][j] = aggretateDiff;
334 | }
335 | }
336 |
337 | nextAggregateMountain.invalidRectangle(nextDiffDataInvalidStartX, nextDiffDataInvalidStartY, nextDiffDataInvalidEndX, nextDiffDataInvalidEndY);
338 | saveImage();
339 | }
340 |
341 | private void genAggregateMountainMapping() {
342 | if (width < 5 || height < 5) {
343 | isLast = true;
344 | return;
345 | }
346 |
347 | int nextDiffDataWith = (width + 4) / 5;
348 | int nextDiffDataHeight = (height + 4) / 5;
349 | long nextDiffData[][] = new long[nextDiffDataWith][nextDiffDataHeight];
350 |
351 |
352 | nextAggregateMountain = new AggregateMountain(nextDiffData, nextDiffDataWith, nextDiffDataHeight, hilltopParamAndResult);
353 | nextAggregateMountain.preAggregateMountain = this;
354 | nextAggregateMountain.genAggregateMountainMapping();
355 |
356 | }
357 | }
358 |
359 | private static final double sqrt2 = Math.sqrt(2);
360 | private static final double sqrt2MULTI2_5 = sqrt2 * 2.5;
361 |
362 |
363 | @Getter
364 | public static class Point {
365 | private int x;
366 | private int y;
367 |
368 | /**
369 | * 权重,越高代表识别越精准
370 | */
371 | private long weight;
372 |
373 |
374 | private HilltopParamAndResult hilltopParamAndResult;
375 |
376 | /**
377 | * 获取裁剪图
378 | *
379 | * @param backgroundColor 可以指定背景颜色 如 白色:0xFFFFFFFF 黑色:0x00000000
380 | * @return 在挑战图中的裁剪小图
381 | */
382 | public BufferedImage generatedSlice(int backgroundColor) {
383 | return ImageHilltopV2.generatedSlice(this, backgroundColor);
384 | }
385 |
386 | }
387 |
388 |
389 | private static BufferedImage generatedSlice(Point point, int backgroundColor) {
390 | int x = point.getX();
391 | int y = point.getY();
392 | HilltopParamAndResult hilltopParamAndResult = point.getHilltopParamAndResult();
393 | int chSize = hilltopParamAndResult.getChSize();
394 |
395 | Rectangle rectangle = rectangleRange(x, y, hilltopParamAndResult.getChSize(),
396 | hilltopParamAndResult.width, hilltopParamAndResult.height
397 | );
398 |
399 |
400 | int[][] diff = point.getHilltopParamAndResult().getDiff();
401 | // 当前图片上的最大diff
402 | long totalDiff = 0;
403 | int maxDiff = 0;
404 | for (int i = 0; i < chSize; i++) {
405 | for (int j = 0; j < chSize; j++) {
406 | int nowDiff = diff[rectangle.leftTopX + i][rectangle.leftTopY + j];
407 | totalDiff += nowDiff;
408 | if (nowDiff > maxDiff) {
409 | maxDiff = nowDiff;
410 | }
411 | }
412 | }
413 |
414 | int avgDiff = (int) (totalDiff / (chSize * chSize));
415 |
416 | BufferedImage bufferedImage = new BufferedImage(chSize, chSize, point.getHilltopParamAndResult().getChallengeImage().getType());
417 | for (int i = 0; i < chSize; i++) {
418 | for (int j = 0; j < chSize; j++) {
419 | int rgb = point.getHilltopParamAndResult().getChallengeImage().getRGB(rectangle.leftTopX + i, rectangle.leftTopY + j);
420 | int pointDiff = diff[rectangle.leftTopX + i][rectangle.leftTopY + j];
421 | // 由于字母的背景是白色,所以这里,我们直接把背景颜色转化为白色
422 | if (pointDiff < (avgDiff * 0.1)) {
423 | bufferedImage.setRGB(i, j, backgroundColor);
424 | } else if (pointDiff >= avgDiff) {
425 | bufferedImage.setRGB(i, j, rgb);
426 | } else {
427 | // 差异比较大的时候,我们按照比例进行颜色叠加
428 | double whiteRatio = ((double) pointDiff) / avgDiff;
429 | // y= (x-1) * (x-1)
430 | whiteRatio = (whiteRatio - 1) * (whiteRatio - 1);
431 | int mergedRGB = ImageUtils.maskMerge(backgroundColor, rgb, whiteRatio);
432 | bufferedImage.setRGB(i, j, mergedRGB);
433 | }
434 | }
435 | }
436 | return bufferedImage;
437 | }
438 |
439 |
440 | @Getter
441 | public static class HilltopParamAndResult {
442 |
443 | public HilltopParamAndResult(BufferedImage backgroundImage, BufferedImage challengeImage, int chSize, int topN) {
444 | this.backgroundImage = backgroundImage;
445 | this.challengeImage = challengeImage;
446 | this.chSize = chSize;
447 | this.topN = topN;
448 | }
449 |
450 | /**
451 | * 背景原图
452 | */
453 | private BufferedImage backgroundImage;
454 |
455 | /**
456 | * 输入的挑战图
457 | */
458 | private BufferedImage challengeImage;
459 |
460 |
461 | /**
462 | * 斑点大小
463 | */
464 | private int chSize;
465 |
466 |
467 | /**
468 | * 待计算的斑点数量
469 | */
470 | private int topN;
471 |
472 |
473 | ///// 以下为输出的信息
474 | /**
475 | * 合并图像的宽
476 | */
477 | private int width;
478 | /**
479 | * 合并图像的高
480 | */
481 | private int height;
482 |
483 |
484 | /**
485 | * 图像diff数据
486 | */
487 | private int[][] diff;
488 |
489 | /**
490 | * 整张图的平均diff
491 | */
492 | private int avgDiff;
493 |
494 | }
495 |
496 | private static class Rectangle {
497 | private int leftTopX;
498 | private int leftTopY;
499 | private int rightBottomX;
500 | private int rightBottomY;
501 | }
502 |
503 | private static Rectangle rectangleRange(int centerX, int centerY, int sliceSize, int totalWidth, int totalHeight) {
504 | int leftTopX = centerX - sliceSize / 2;
505 | int leftTopY = centerY - sliceSize / 2;
506 | int rightBottomX = centerX + sliceSize / 2;
507 | int rightBottomY = centerY + sliceSize / 2;
508 |
509 | if (leftTopX < 0) {
510 | leftTopX = 0;
511 | }
512 | if (leftTopY < 0) {
513 | leftTopY = 0;
514 | }
515 | if (rightBottomX >= totalWidth) {
516 | rightBottomX = totalWidth - 1;
517 | }
518 | if (rightBottomY >= totalHeight) {
519 | rightBottomY = totalHeight - 1;
520 | }
521 | Rectangle rectangle = new Rectangle();
522 | rectangle.leftTopX = leftTopX;
523 | rectangle.leftTopY = leftTopY;
524 | rectangle.rightBottomX = rightBottomX;
525 | rectangle.rightBottomY = rightBottomY;
526 |
527 | return rectangle;
528 | }
529 |
530 | private static boolean saveImageFlag = false;
531 | private static int saveImageIndex = 1;
532 |
533 | private static void saveImage(long[][] diffData, int width, int height) {
534 | if (!saveImageFlag) {
535 | return;
536 | }
537 | BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
538 | long maxDiff = 0;
539 | for (int i = 0; i < width; i++) {
540 | for (int j = 0; j < height; j++) {
541 | if (maxDiff < diffData[i][j]) {
542 | maxDiff = diffData[i][j];
543 | }
544 | }
545 | }
546 | for (int i = 0; i < width; i++) {
547 | for (int j = 0; j < height; j++) {
548 | int rgb = (int) (diffData[i][j] * 255 / maxDiff);
549 | int rgbGray = rgb << 24 | rgb << 16 | rgb << 8 | rgb;
550 | bufferedImage.setRGB(i, j, rgbGray);
551 | }
552 | }
553 | ImagePHash imagePHash = new ImagePHash();
554 | String hash = imagePHash.getHash(bufferedImage);
555 | File file = new File("assets/test/" + (saveImageIndex++) + "_" + hash + ".jpg");
556 | try {
557 | ImageIO.write(bufferedImage, "jpg", file);
558 | } catch (IOException e) {
559 | e.printStackTrace();
560 | }
561 | }
562 | }
563 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/Main.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic;
2 |
3 | public class Main {
4 | public static void main(String[] args) {
5 |
6 |
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/SimilarImageSearcher.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic;
2 |
3 | import com.virjar.image.magic.libs.ImageHistogram;
4 | import com.virjar.image.magic.libs.ImagePHash;
5 | import lombok.Getter;
6 |
7 | import javax.imageio.ImageIO;
8 | import java.awt.image.BufferedImage;
9 | import java.io.IOException;
10 | import java.net.URL;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | public class SimilarImageSearcher {
15 | private static final ImagePHash imagePHash = new ImagePHash();
16 |
17 | private final ArrayList imageSamples = new ArrayList<>();
18 |
19 | public void addSample(URL url) throws IOException {
20 | addSample(url, null);
21 | }
22 |
23 | public void addSample(URL url, String tag) throws IOException {
24 | imageSamples.add(new ImageSample(ImageIO.read(url), tag));
25 | }
26 |
27 | public ImageSample findSimilarImage(BufferedImage bufferedImage, String imageHash) {
28 | if (imageHash == null || imageHash.trim().isEmpty()) {
29 | imageHash = imagePHash.getHash(bufferedImage);
30 | }
31 | List similarTasks = new ArrayList<>();
32 | int nowDistance = Integer.MAX_VALUE;
33 | ImageSample imageSample = null;
34 |
35 | for (ImageSample testSample : imageSamples) {
36 | int distance = ImagePHash.distance(imageHash, testSample.imgHash);
37 | if (distance < nowDistance) {
38 | imageSample = testSample;
39 | nowDistance = distance;
40 | }
41 | if (distance < 20) {
42 | similarTasks.add(imageSample);
43 | }
44 | }
45 |
46 | if (nowDistance <= 6 || similarTasks.size() <= 1) {
47 | return imageSample;
48 | }
49 | // 当距离大于6的时候,证明可能存在误差。这个时候再走一次直方图
50 |
51 | double nowScore = 0;
52 | for (ImageSample testSample : similarTasks) {
53 | double score = testSample.imageHistogram.match(bufferedImage);
54 | if (score > nowScore) {
55 | nowScore = score;
56 | imageSample = testSample;
57 | }
58 | }
59 |
60 | return imageSample;
61 | }
62 |
63 |
64 | public static class ImageSample {
65 |
66 | @Getter
67 | private final String imgHash;
68 | @Getter
69 | private final BufferedImage image;
70 | private final ImageHistogram imageHistogram;
71 | @Getter
72 | private final String tag;
73 |
74 | ImageSample(BufferedImage image, String tag) {
75 | this.image = image;
76 | this.imgHash = imagePHash.getHash(image);
77 | this.imageHistogram = new ImageHistogram(image);
78 | if (tag == null) {
79 | this.tag = imgHash;
80 | } else {
81 | this.tag = tag;
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/libs/ImageHistogram.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic.libs;
2 |
3 | import javax.imageio.ImageIO;
4 | import java.awt.image.BufferedImage;
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.net.URL;
8 |
9 | /**
10 | * @desc 相似图片识别(直方图)
11 | */
12 | public class ImageHistogram {
13 |
14 | private final int redBins;
15 | private final int greenBins;
16 | private final int blueBins;
17 |
18 | private final float[] baseData;
19 |
20 | public ImageHistogram(BufferedImage base) {
21 | redBins = greenBins = blueBins = 4;
22 | this.baseData = filter(base);
23 | }
24 |
25 | private float[] filter(BufferedImage src) {
26 | int width = src.getWidth();
27 | int height = src.getHeight();
28 |
29 | int[] inPixels = new int[width * height];
30 | float[] histogramData = new float[redBins * greenBins * blueBins];
31 | getRGB(src, 0, 0, width, height, inPixels);
32 | int index = 0;
33 | int redIdx = 0, greenIdx = 0, blueIdx = 0;
34 | int singleIndex = 0;
35 | float total = 0;
36 | for (int row = 0; row < height; row++) {
37 | int tr = 0, tg = 0, tb = 0;
38 | for (int col = 0; col < width; col++) {
39 | index = row * width + col;
40 | tr = (inPixels[index] >> 16) & 0xff;
41 | tg = (inPixels[index] >> 8) & 0xff;
42 | tb = inPixels[index] & 0xff;
43 | redIdx = (int) getBinIndex(redBins, tr);
44 | greenIdx = (int) getBinIndex(greenBins, tg);
45 | blueIdx = (int) getBinIndex(blueBins, tb);
46 | singleIndex = redIdx + greenIdx * redBins + blueIdx * redBins * greenBins;
47 | histogramData[singleIndex] += 1;
48 | total += 1;
49 | }
50 | }
51 |
52 | // start to normalize the histogram data
53 | for (int i = 0; i < histogramData.length; i++) {
54 | histogramData[i] = histogramData[i] / total;
55 | }
56 |
57 | return histogramData;
58 | }
59 |
60 | private float getBinIndex(int binCount, int color) {
61 | float binIndex = (((float) color) / ((float) 255)) * ((float) binCount);
62 | if (binIndex >= binCount)
63 | binIndex = binCount - 1;
64 | return binIndex;
65 | }
66 |
67 | private int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
68 | int type = image.getType();
69 | if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB)
70 | return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
71 | return image.getRGB(x, y, width, height, pixels, 0, width);
72 | }
73 |
74 | /**
75 | * Bhattacharyya Coefficient
76 | * http://www.cse.yorku.ca/~kosta/CompVis_Notes/bhattacharyya.pdf
77 | *
78 | * @return 返回值大于等于0.8可以简单判断这两张图片内容一致
79 | * @throws IOException
80 | */
81 | public double match(File srcFile, File canFile) throws IOException {
82 | float[] sourceData = this.filter(ImageIO.read(srcFile));
83 | float[] candidateData = this.filter(ImageIO.read(canFile));
84 | return calcSimilarity(sourceData, candidateData);
85 | }
86 |
87 | /**
88 | * @return 返回值大于等于0.8可以简单判断这两张图片内容一致
89 | * @throws IOException
90 | */
91 | public double match(URL srcUrl, URL canUrl) throws IOException {
92 | float[] sourceData = this.filter(ImageIO.read(srcUrl));
93 | float[] candidateData = this.filter(ImageIO.read(canUrl));
94 | return calcSimilarity(sourceData, candidateData);
95 | }
96 |
97 | /**
98 | * Bhattacharyya Coefficient
99 | * http://www.cse.yorku.ca/~kosta/CompVis_Notes/bhattacharyya.pdf
100 | *
101 | * @return 返回值大于等于0.8可以简单判断这两张图片内容一致
102 | * @throws IOException
103 | */
104 | public double match(BufferedImage target) {
105 | float[] candidateData = this.filter(target);
106 | return calcSimilarity(baseData, candidateData);
107 | }
108 |
109 | private double calcSimilarity(float[] sourceData, float[] candidateData) {
110 | double[] mixedData = new double[sourceData.length];
111 | for (int i = 0; i < sourceData.length; i++) {
112 | mixedData[i] = Math.sqrt(sourceData[i] * candidateData[i]);
113 | }
114 |
115 | // The values of Bhattacharyya Coefficient ranges from 0 to 1,
116 | double similarity = 0;
117 | for (int i = 0; i < mixedData.length; i++) {
118 | similarity += mixedData[i];
119 | }
120 |
121 | // The degree of similarity
122 | return similarity;
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/libs/ImagePHash.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic.libs;
2 |
3 | import javax.imageio.ImageIO;
4 | import java.awt.*;
5 | import java.awt.color.ColorSpace;
6 | import java.awt.image.BufferedImage;
7 | import java.awt.image.ColorConvertOp;
8 | import java.io.File;
9 | import java.io.FileInputStream;
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.net.URL;
13 |
14 | /*
15 | * pHash-like image hash.
16 | * Author: Elliot Shepherd (elliot@jarofworms.com
17 | * Based On: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
18 | */
19 | public class ImagePHash {
20 |
21 | private int size = 32;
22 | private int smallerSize = 8;
23 | private double[] c;
24 |
25 | public ImagePHash() {
26 | initCoefficients();
27 | }
28 |
29 | public ImagePHash(int size, int smallerSize) {
30 | this.size = size;
31 | this.smallerSize = smallerSize;
32 | initCoefficients();
33 | }
34 |
35 | private void initCoefficients() {
36 | c = new double[size];
37 | for (int i = 1; i < size; i++) {
38 | c[i] = 1;
39 | }
40 | c[0] = 1 / Math.sqrt(2.0);
41 | }
42 |
43 |
44 | public static int distance(String s1, String s2) {
45 | int counter = 0;
46 | for (int k = 0; k < s1.length(); k++) {
47 | if (s1.charAt(k) != s2.charAt(k)) {
48 | counter++;
49 | }
50 | }
51 | return counter;
52 | }
53 |
54 | public String getHash(InputStream is) throws IOException {
55 | BufferedImage img = ImageIO.read(is);
56 | return getHash(img);
57 | }
58 |
59 | // Returns a 'binary string' (like. 001010111011100010) which is easy to do
60 | // a hamming distance on.
61 | public String getHash(BufferedImage img) {
62 |
63 | /*
64 | * 1. Reduce size. Like Average Hash, pHash starts with a small image.
65 | * However, the image is larger than 8x8; 32x32 is a good size. This is
66 | * really done to simplify the DCT computation and not because it is
67 | * needed to reduce the high frequencies.
68 | */
69 | img = resize(img, size, size);
70 |
71 | /*
72 | * 2. Reduce color. The image is reduced to a grayscale just to further
73 | * simplify the number of computations.
74 | */
75 | img = grayscale(img);
76 |
77 | double[][] vals = new double[size][size];
78 |
79 | for (int x = 0; x < img.getWidth(); x++) {
80 | for (int y = 0; y < img.getHeight(); y++) {
81 | vals[x][y] = getBlue(img, x, y);
82 | }
83 | }
84 |
85 | /*
86 | * 3. Compute the DCT. The DCT separates the image into a collection of
87 | * frequencies and scalars. While JPEG uses an 8x8 DCT, this algorithm
88 | * uses a 32x32 DCT.
89 | */
90 | double[][] dctVals = applyDCT(vals);
91 |
92 | /*
93 | * 4. Reduce the DCT. This is the magic step. While the DCT is 32x32,
94 | * just keep the top-left 8x8. Those represent the lowest frequencies in
95 | * the picture.
96 | */
97 | /*
98 | * 5. Compute the average value. Like the Average Hash, compute the mean
99 | * DCT value (using only the 8x8 DCT low-frequency values and excluding
100 | * the first term since the DC coefficient can be significantly
101 | * different from the other values and will throw off the average).
102 | */
103 | double total = 0;
104 |
105 | for (int x = 0; x < smallerSize; x++) {
106 | for (int y = 0; y < smallerSize; y++) {
107 | total += dctVals[x][y];
108 | }
109 | }
110 | total -= dctVals[0][0];
111 |
112 | double avg = total / (double) ((smallerSize * smallerSize) - 1);
113 |
114 | /*
115 | * 6. Further reduce the DCT. This is the magic step. Set the 64 hash
116 | * bits to 0 or 1 depending on whether each of the 64 DCT values is
117 | * above or below the average value. The result doesn't tell us the
118 | * actual low frequencies; it just tells us the very-rough relative
119 | * scale of the frequencies to the mean. The result will not vary as
120 | * long as the overall structure of the image remains the same; this can
121 | * survive gamma and color histogram adjustments without a problem.
122 | */
123 | String hash = "";
124 |
125 | for (int x = 0; x < smallerSize; x++) {
126 | for (int y = 0; y < smallerSize; y++) {
127 | if (x != 0 && y != 0) {
128 | hash += (dctVals[x][y] > avg ? "1" : "0");
129 | }
130 | }
131 | }
132 |
133 | return hash;
134 | }
135 |
136 | private BufferedImage resize(BufferedImage image, int width, int height) {
137 | BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
138 | Graphics2D g = resizedImage.createGraphics();
139 | g.drawImage(image, 0, 0, width, height, null);
140 | g.dispose();
141 | return resizedImage;
142 | }
143 |
144 | private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
145 |
146 | private BufferedImage grayscale(BufferedImage img) {
147 | colorConvert.filter(img, img);
148 | return img;
149 | }
150 |
151 | private static int getBlue(BufferedImage img, int x, int y) {
152 | return (img.getRGB(x, y)) & 0xff;
153 | }
154 |
155 | // DCT function stolen from
156 | // http://stackoverflow.com/questions/4240490/problems-with-dct-and-idct-algorithm-in-java
157 | private double[][] applyDCT(double[][] f) {
158 | int N = size;
159 |
160 | double[][] F = new double[N][N];
161 | for (int u = 0; u < N; u++) {
162 | for (int v = 0; v < N; v++) {
163 | double sum = 0.0;
164 | for (int i = 0; i < N; i++) {
165 | for (int j = 0; j < N; j++) {
166 | sum += Math.cos(((2 * i + 1) / (2.0 * N)) * u * Math.PI)
167 | * Math.cos(((2 * j + 1) / (2.0 * N)) * v * Math.PI) * (f[i][j]);
168 | }
169 | }
170 | sum *= ((c[u] * c[v]) / 4.0);
171 | F[u][v] = sum;
172 | }
173 | }
174 | return F;
175 | }
176 |
177 | /**
178 | * @param srcUrl
179 | * @param canUrl
180 | * @return 值越小相识度越高,10之内可以简单判断这两张图片内容一致
181 | * @throws Exception
182 | * @throws
183 | */
184 | public int distance(URL srcUrl, URL canUrl) throws Exception {
185 | String imgStr = this.getHash(srcUrl.openStream());
186 | String canStr = this.getHash(canUrl.openStream());
187 | return this.distance(imgStr, canStr);
188 | }
189 |
190 | /**
191 | * @param srcFile
192 | * @param canFile
193 | * @return 值越小相识度越高,10之内可以简单判断这两张图片内容一致
194 | * @throws Exception
195 | */
196 | public int distance(File srcFile, File canFile) throws Exception {
197 | String imageSrcFile = this.getHash(new FileInputStream(srcFile));
198 | String imageCanFile = this.getHash(new FileInputStream(canFile));
199 | return this.distance(imageSrcFile, imageCanFile);
200 | }
201 |
202 | }
--------------------------------------------------------------------------------
/src/main/java/com/virjar/image/magic/libs/ImageUtils.java:
--------------------------------------------------------------------------------
1 | package com.virjar.image.magic.libs;
2 |
3 | import java.awt.*;
4 | import java.awt.geom.AffineTransform;
5 | import java.awt.image.BufferedImage;
6 | import java.awt.image.ColorModel;
7 | import java.awt.image.WritableRaster;
8 |
9 | public class ImageUtils {
10 |
11 | /**
12 | * 计算两张图片的差异
13 | *
14 | * @param rgbLeft 像素1
15 | * @param rgbRight 像素2
16 | * @return 差异
17 | */
18 | public static int rgbDiff(int rgbLeft, int rgbRight) {
19 | int redLeft = rgbLeft >> 16 & 255;
20 | int greenLeft = rgbLeft >> 8 & 255;
21 | int blueLeft = rgbLeft & 255;
22 |
23 | int redLRight = rgbRight >> 16 & 255;
24 | int greenRight = rgbRight >> 8 & 255;
25 | int blueRight = rgbRight & 255;
26 | return Math.abs(redLeft - redLRight)
27 | + Math.abs(greenLeft - greenRight)
28 | + Math.abs(blueLeft - blueRight);
29 | }
30 |
31 | /**
32 | * 透明度叠加,两个图像根据指定比例叠加
33 | *
34 | * @param rgbLeft 像素1
35 | * @param rgbRight 像素2
36 | * @param leftRatio 像素1在合并结果占有的比例
37 | * @return 输出的像素
38 | */
39 | public static int maskMerge(int rgbLeft, int rgbRight, double leftRatio) {
40 | if (leftRatio < 0 || leftRatio > 1) {
41 | throw new IllegalStateException("error leftRatio: " + leftRatio);
42 | }
43 | int r = (int) (((rgbLeft >>> 24) & 0xff) * leftRatio
44 | + ((rgbRight >>> 24) & 0xff) * (1 - leftRatio));
45 |
46 | int g = (int) (((rgbLeft >>> 16) & 0xff) * leftRatio
47 | + ((rgbRight >>> 16) & 0xff) * (1 - leftRatio));
48 |
49 | int b = (int) (((rgbLeft >>> 8) & 0xff) * leftRatio
50 | + ((rgbRight >>> 8) & 0xff) * (1 - leftRatio));
51 |
52 | int p = (int) (((rgbLeft) & 0xff) * leftRatio
53 | + ((rgbRight) & 0xff) * (1 - leftRatio));
54 |
55 | return (r << 24) | (g << 16) | (b << 8) | p;
56 | }
57 |
58 | /**
59 | * 将目标图像缩放到指定大小
60 | *
61 | * @param source 输入图像
62 | * @param width 目标图像宽度
63 | * @param height 目标图像高度
64 | * @return 缩放后的图像
65 | */
66 | public static BufferedImage thumb(BufferedImage source, int width, int height) {
67 | if (width == source.getWidth()
68 | && height == source.getHeight()) {
69 | return source;
70 | }
71 | int type = source.getType();
72 | BufferedImage target;
73 | double sx = (double) width / (double) source.getWidth();
74 | double sy = (double) height / (double) source.getHeight();
75 |
76 | if (type == 0) {
77 | ColorModel g = source.getColorModel();
78 | WritableRaster raster = g.createCompatibleWritableRaster(width, height);
79 | boolean alphaPremultiplied = g.isAlphaPremultiplied();
80 | target = new BufferedImage(g, raster, alphaPremultiplied, null);
81 | } else {
82 | target = new BufferedImage(width, height, type);
83 | }
84 |
85 | Graphics2D g1 = target.createGraphics();
86 | g1.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
87 | g1.drawRenderedImage(source, AffineTransform.getScaleInstance(sx, sy));
88 | g1.dispose();
89 | return target;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------