原因
我们公司开发的其中一个应用是公众号应用,现在有个需求,就是定时更新用户的头像。当用户更新了微信头像后,需要将最新的头像定时同步到我们的数据库中。
之前的方案是每天晚上去调用微信的接口查一遍所有关注公众号并注册了的用户的信息,然后取到头像链接和数据库里的头像链接比较。这样的效率太低,当用户达到几万几十万的时候,需要调用几万几十万次微信接口,耗资源且耗时特别长。
微信在用户更换头像后会将旧头像链接替换成这个图片:
这样我们就可以将数据库里的所有头像取出来和这个错误图片进行对比,若相似度很高,就认为这个用户的头像已过期,调用微信的接口取出最新的头像链接,存入数据库。
图像对比方案
我是参考了阮一峰老师的《相似图片搜索的原理》这篇文章。主要步骤如下:
- 图片缩小到50*50。
- 将彩色图片转换成灰度图片。
- 用大津阈值法找到阈值,大津阈值法在刚才那篇文章里也有详细说明。
- 用找到阈值将灰度图片转换成只有两个色的黑白图片。
- 获取数据,就是长度为2500,值为1或0的数组
- 比较图像数据,将两个图像数据,也就是两个长度为2500的数组进行异或操作,其中结果中的1越少,就是越相似的图片。
实现
读取图片
BufferedImage errImage = ImageUtil.readUrl(errUrl);
缩放和灰度化
private static Integer width = 50;
private static Integer height = 50;
public static BufferedImage drawGray(BufferedImage originImage) {
Image tmpImage = originImage.getScaledInstance(50, 50, Image.SCALE_DEFAULT);
BufferedImage scaleImage = new BufferedImage(50, 50, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D graphics = scaleImage.createGraphics();
graphics.drawImage(tmpImage, 0, 0, null);
graphics.dispose();
return scaleImage;
}
大津法获取阈值
public static int getThreshHold(byte[] srcData) {
int[] histData = new int[256];
int threshold;
int ptr = 0;
while (ptr < histData.length) {
histData[ptr++] = 0;
}
ptr = 0;
while (ptr < srcData.length) {
int h = 0xFF & srcData[ptr];
histData[h]++;
ptr++;
}
int total = srcData.length;
float sum = 0;
for (int t = 0; t < 256; t++) {
sum += t * histData[t];
}
float sumB = 0;
int wB = 0;
int wF = 0;
float varMax = 0;
threshold = 0;
for (int t = 0; t < 256; t++) {
wB += histData[t]; // Weight Background
if (wB == 0) {
continue;
}
wF = total - wB; // Weight Foreground
if (wF == 0) {
break;
}
sumB += (float) (t * histData[t]);
float mB = sumB / wB; // Mean Background
float mF = (sum - sumB) / wF; // Mean Foreground
// Calculate Between Class Variance
float varBetween = (float) wB * (float) wF * (mB - mF) * (mB - mF);
// Check if new maximum found
if (varBetween > varMax) {
varMax = varBetween;
threshold = t;
}
}
return threshold;
}
获取黑白图的数据
public static int[] getWbData(BufferedImage image, int threshold) {
int[] data = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
int gray = (r + g + b) / 3;
int wb = gray > threshold ? 1 : 0;
data[x * y + x] = wb;
}
}
return data;
比较
public static Double compare(int[] srcImageData, int[] destImageData) {
int total = 0;
int length = width * height;
for (int i = 0; i < length; i++) {
int x = srcImageData[i];
int y = destImageData[i];
total += x ^ y;
}
return 1 - Double.valueOf(total) / length;
}
最后比较后获取一个相似度的百分比,我把他设定成大于95%的话,就认为图片很相似,然后调用接口获取最新的头像,更新数据库
Double percent = ImageUtil.compare(errImageData, imageData);
// 更新患者头像
if (percent > 0.95) {
String userWxStr = wxService.userDetail(user.getOpenid());
User userWx = JSONObject.parseObject(userWxStr, User.class);
user.setHeadimgurl(userWx.getHeadimgurl());
userMapper.updateByPrimaryKeySelective(user);
}
结语
在使用图片对比的方案后,大大减少了网络接口请求,提高了效率。以前可能要用几个小时的任务现在几分钟就完成了。