├── .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 | --------------------------------------------------------------------------------