相似度算法
parent
68d9c39caf
commit
b6055e3cbb
8
pom.xml
8
pom.xml
|
@ -46,6 +46,7 @@
|
|||
<lowagie.iTextAsian.version>1.0</lowagie.iTextAsian.version>
|
||||
<itextpdf.version>5.5.13</itextpdf.version>
|
||||
<aspose.words.version>15.8.0</aspose.words.version>
|
||||
<opencv.version>4.5.1-2</opencv.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
|
@ -295,6 +296,13 @@
|
|||
<version>8.0.31</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenCV -->
|
||||
<dependency>
|
||||
<groupId>org.openpnp</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
<version>${opencv.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
|
|
@ -154,5 +154,11 @@
|
|||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenCV -->
|
||||
<dependency>
|
||||
<groupId>org.openpnp</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
package com.yanzhu.common.core.utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.opencv.core.*;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.opencv.objdetect.CascadeClassifier;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* 图片相似度计算工具类(基于OpenCV实现)
|
||||
* 专门针对人脸识别场景优化的相似度计算
|
||||
*
|
||||
* @author yanzhu
|
||||
* @date 2025-09-03
|
||||
*/
|
||||
public class ImageSimilarityUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(ImageSimilarityUtils.class);
|
||||
|
||||
// 人脸识别场景下的考勤阈值(针对同一个人的不同照片)
|
||||
private static final double ATTENDANCE_THRESHOLD = 0.70;
|
||||
|
||||
// OpenCV相关
|
||||
private static CascadeClassifier faceDetector;
|
||||
|
||||
static {
|
||||
try {
|
||||
// 加载OpenCV库
|
||||
nu.pattern.OpenCV.loadLocally();
|
||||
|
||||
// 初始化人脸检测器
|
||||
// 注意:在实际部署时,需要确保haarcascade_frontalface_alt.xml文件在classpath中
|
||||
String cascadePath = "haarcascade_frontalface_alt.xml";
|
||||
faceDetector = new CascadeClassifier(cascadePath);
|
||||
} catch (Exception e) {
|
||||
log.error("OpenCV初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两张人脸图片的相似度
|
||||
*
|
||||
* @param imagePath1 人脸图片1路径(可以是本地路径或URL)
|
||||
* @param imagePath2 人脸图片2路径(可以是本地路径或URL)
|
||||
* @return 相似度(0-1之间,1表示完全相同)
|
||||
*/
|
||||
public static double calculateFaceSimilarity(String imagePath1, String imagePath2) {
|
||||
try {
|
||||
// 检查参数
|
||||
if (imagePath1 == null || imagePath2 == null) {
|
||||
log.warn("图片路径不能为空");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 读取图片
|
||||
Mat image1 = readImageAsMat(imagePath1);
|
||||
Mat image2 = readImageAsMat(imagePath2);
|
||||
|
||||
if (image1.empty() || image2.empty()) {
|
||||
log.warn("无法读取图片文件: {} 或 {}", imagePath1, imagePath2);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 检测并裁剪人脸区域
|
||||
Mat face1 = detectAndCropFace(image1);
|
||||
Mat face2 = detectAndCropFace(image2);
|
||||
|
||||
// 如果无法检测到人脸,则使用整张图片
|
||||
if (face1.empty()) face1 = image1;
|
||||
if (face2.empty()) face2 = image2;
|
||||
|
||||
// 为人脸识别场景预处理
|
||||
Mat processedImage1 = preprocessForFaceRecognition(face1);
|
||||
Mat processedImage2 = preprocessForFaceRecognition(face2);
|
||||
|
||||
// 使用OpenCV计算直方图相似度
|
||||
double similarity = calculateHistogramSimilarity(processedImage1, processedImage2);
|
||||
|
||||
log.debug("计算人脸图片相似度完成: {} 和 {}, 相似度: {}",
|
||||
imagePath1, imagePath2, similarity);
|
||||
|
||||
// 释放资源
|
||||
image1.release();
|
||||
image2.release();
|
||||
face1.release();
|
||||
face2.release();
|
||||
processedImage1.release();
|
||||
processedImage2.release();
|
||||
|
||||
return similarity;
|
||||
} catch (Exception e) {
|
||||
log.error("计算人脸图片相似度时发生错误", e);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查人脸图片是否匹配(基于考勤场景的阈值)
|
||||
*
|
||||
* @param imagePath1 人脸图片1路径
|
||||
* @param imagePath2 人脸图片2路径
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public static boolean isFaceMatchForAttendance(String imagePath1, String imagePath2) {
|
||||
double similarity = calculateFaceSimilarity(imagePath1, imagePath2);
|
||||
return similarity >= ATTENDANCE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取图片为OpenCV的Mat对象
|
||||
*
|
||||
* @param imagePath 图片路径
|
||||
* @return Mat对象
|
||||
*/
|
||||
private static Mat readImageAsMat(String imagePath) {
|
||||
try {
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
// 处理网络图片
|
||||
URL url = new URL(imagePath);
|
||||
Path tempFile = Files.createTempFile("image_", ".jpg");
|
||||
Files.copy(url.openStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
Mat mat = Imgcodecs.imread(tempFile.toAbsolutePath().toString());
|
||||
Files.delete(tempFile); // 清理临时文件
|
||||
return mat;
|
||||
} else {
|
||||
// 处理本地图片
|
||||
return Imgcodecs.imread(imagePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取图片失败: {}", imagePath, e);
|
||||
return new Mat();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并裁剪人脸区域
|
||||
*
|
||||
* @param image 原始图片
|
||||
* @return 裁剪后的人脸区域图片,如果未检测到则返回空Mat
|
||||
*/
|
||||
private static Mat detectAndCropFace(Mat image) {
|
||||
try {
|
||||
// 转换为灰度图
|
||||
Mat grayImage = new Mat();
|
||||
Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
|
||||
|
||||
// 检测人脸
|
||||
MatOfRect faceDetections = new MatOfRect();
|
||||
faceDetector.detectMultiScale(grayImage, faceDetections);
|
||||
|
||||
Rect[] faces = faceDetections.toArray();
|
||||
|
||||
// 如果检测到人脸,裁剪第一个人脸区域
|
||||
if (faces.length > 0) {
|
||||
Rect face = faces[0];
|
||||
Mat croppedFace = new Mat(image, face);
|
||||
grayImage.release();
|
||||
faceDetections.release();
|
||||
return croppedFace;
|
||||
}
|
||||
|
||||
grayImage.release();
|
||||
faceDetections.release();
|
||||
return new Mat();
|
||||
} catch (Exception e) {
|
||||
log.warn("人脸检测失败,使用整张图片", e);
|
||||
return new Mat();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为人脸识别场景预处理图片
|
||||
*
|
||||
* @param originalImage 原始图片
|
||||
* @return 预处理后的图片
|
||||
*/
|
||||
private static Mat preprocessForFaceRecognition(Mat originalImage) {
|
||||
// 1. 调整大小到标准尺寸(256x256)
|
||||
Mat resized = new Mat();
|
||||
Imgproc.resize(originalImage, resized, new Size(256, 256));
|
||||
|
||||
// 2. 转换为灰度图(如果还不是的话)
|
||||
Mat gray = new Mat();
|
||||
if (resized.channels() == 3) {
|
||||
Imgproc.cvtColor(resized, gray, Imgproc.COLOR_BGR2GRAY);
|
||||
} else {
|
||||
resized.copyTo(gray);
|
||||
}
|
||||
|
||||
// 3. 直方图均衡化增强对比度
|
||||
Mat enhanced = new Mat();
|
||||
Imgproc.equalizeHist(gray, enhanced);
|
||||
|
||||
// 释放中间资源
|
||||
resized.release();
|
||||
gray.release();
|
||||
|
||||
return enhanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算直方图相似度(使用相关性比较)
|
||||
*
|
||||
* @param image1 图片1
|
||||
* @param image2 图片2
|
||||
* @return 相似度(0-1之间)
|
||||
*/
|
||||
private static double calculateHistogramSimilarity(Mat image1, Mat image2) {
|
||||
// 计算直方图
|
||||
Mat hist1 = new Mat();
|
||||
Mat hist2 = new Mat();
|
||||
|
||||
// 设置直方图参数
|
||||
MatOfInt histSize = new MatOfInt(256);
|
||||
MatOfFloat ranges = new MatOfFloat(0f, 256f);
|
||||
MatOfInt channels = new MatOfInt(0);
|
||||
|
||||
// 计算直方图
|
||||
Imgproc.calcHist(java.util.Arrays.asList(image1), channels, new Mat(), hist1, histSize, ranges);
|
||||
Imgproc.calcHist(java.util.Arrays.asList(image2), channels, new Mat(), hist2, histSize, ranges);
|
||||
|
||||
// 归一化直方图
|
||||
Core.normalize(hist1, hist1);
|
||||
Core.normalize(hist2, hist2);
|
||||
|
||||
// 计算直方图相关性
|
||||
double similarity = Imgproc.compareHist(hist1, hist2, Imgproc.HISTCMP_CORREL);
|
||||
|
||||
// 释放资源
|
||||
hist1.release();
|
||||
hist2.release();
|
||||
histSize.release();
|
||||
ranges.release();
|
||||
channels.release();
|
||||
|
||||
// 确保相似度在0-1范围内
|
||||
return Math.max(0.0, Math.min(1.0, similarity));
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String userPicture = "https://xiangguan.sxyanzhu.com/statics/2025/09/03/836e5c21f83604894486069394dcd22e_20250903170015A004.jpg";
|
||||
String attImg = "https://xiangguan.sxyanzhu.com/statics/2025/09/03/c4e2c8fb9d9f66e03902f9e3fea17f99_20250903165717A003.jpg";
|
||||
double similarity = ImageSimilarityUtils.calculateFaceSimilarity(userPicture, attImg);
|
||||
System.out.println("人脸相似度: " + similarity);
|
||||
System.out.println("考勤匹配: " + ImageSimilarityUtils.isFaceMatchForAttendance(userPicture, attImg));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -163,6 +163,26 @@ public class ProMobileAttendanceData extends BaseEntity
|
|||
return state;
|
||||
}
|
||||
|
||||
private ProMobileAttendanceConfig cfgInfo;
|
||||
|
||||
private String userPicture;
|
||||
|
||||
public String getUserPicture() {
|
||||
return userPicture;
|
||||
}
|
||||
|
||||
public void setUserPicture(String userPicture) {
|
||||
this.userPicture = userPicture;
|
||||
}
|
||||
|
||||
public ProMobileAttendanceConfig getCfgInfo() {
|
||||
return cfgInfo;
|
||||
}
|
||||
|
||||
public void setCfgInfo(ProMobileAttendanceConfig cfgInfo) {
|
||||
this.cfgInfo = cfgInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
|
|
|
@ -190,6 +190,13 @@
|
|||
<artifactId>hasor-dataway</artifactId>
|
||||
<version>4.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenCV -->
|
||||
<dependency>
|
||||
<groupId>org.openpnp</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
<version>4.5.1-2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.yanzhu.common.core.web.page.TableDataInfo;
|
|||
import com.yanzhu.common.log.annotation.Log;
|
||||
import com.yanzhu.common.log.enums.BusinessType;
|
||||
import com.yanzhu.common.security.annotation.RequiresPermissions;
|
||||
import com.yanzhu.manage.domain.ProMobileAttendanceData;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
@ -119,10 +120,23 @@ public class ProMobileAttendanceConfigController extends BaseController
|
|||
*/
|
||||
@RequiresPermissions("manage:mobileAttendConfig:add")
|
||||
@PostMapping("/attendance")
|
||||
public AjaxResult attendance(@RequestBody ProMobileAttendanceConfig cfg){
|
||||
public AjaxResult attendance(@RequestBody ProMobileAttendanceData attData){
|
||||
//根据用户上传的照片与用户信息的照片计算相似度
|
||||
//增加考勤数据
|
||||
//增加考勤历史记录
|
||||
return AjaxResult.success("OK");
|
||||
String userPicture=attData.getUserPicture();
|
||||
String attImg=attData.getAttImg();
|
||||
|
||||
// 使用专门为人脸识别考勤优化的相似度计算
|
||||
boolean isMatch = com.yanzhu.common.core.utils.ImageSimilarityUtils.isFaceMatchForAttendance(userPicture, attImg);
|
||||
|
||||
if (isMatch) {
|
||||
// 相似度达标,增加考勤数据
|
||||
// TODO: 增加考勤数据逻辑
|
||||
// TODO: 增加考勤历史记录逻辑
|
||||
|
||||
return AjaxResult.success("考勤成功,人脸匹配");
|
||||
} else {
|
||||
// 相似度不达标,拒绝考勤
|
||||
return AjaxResult.error("考勤失败,人脸不匹配");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -269,8 +269,7 @@ Page({
|
|||
|
||||
// 继续执行打卡操作
|
||||
this.uploadFaceImage(() => {
|
||||
console.log(this.data.faceImageUrl);
|
||||
debugger;
|
||||
this.doSaveAttendance();
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
|
@ -344,18 +343,6 @@ Page({
|
|||
});
|
||||
},
|
||||
|
||||
getDistance(longitude, latitude) {
|
||||
// 获取当前位置计算距离
|
||||
// 这里直接计算给定坐标与考勤地点的距离
|
||||
const cfgData = this.data.cfgData;
|
||||
if (!cfgData) return 0;
|
||||
return calculateDistance(
|
||||
longitude,
|
||||
latitude,
|
||||
cfgData.longitude,
|
||||
cfgData.latitude
|
||||
);
|
||||
},
|
||||
onProjectSelect(e) {
|
||||
let projectId = e.detail.id;
|
||||
let projectName = e.detail.text;
|
||||
|
@ -374,4 +361,19 @@ Page({
|
|||
"cfgData.attDate": fmt(new Date()).format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
},
|
||||
doSaveAttendance() {
|
||||
let cfgData = this.data.cfgData;
|
||||
let postData = {
|
||||
userPicture: app.globalData.subDeptUserData.userPicture,
|
||||
userId: app.globalData.subDeptUserData.userId,
|
||||
projectId: app.globalData.useProjectId,
|
||||
cfgId: cfgData.id,
|
||||
inOut: this.data.arrSel,
|
||||
longitude: cfgData.attLongitude,
|
||||
latitude: cfgData.attLatitude,
|
||||
attDate: cfgData.attDate,
|
||||
attImg: this.data.faceImageUrl,
|
||||
cfgInfo: cfgData,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue