相似度算法

dev_xd
lj7788@126.com 2025-09-03 17:30:23 +08:00
parent 68d9c39caf
commit b6055e3cbb
8 changed files with 24679 additions and 19 deletions

View File

@ -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>

View File

@ -154,5 +154,11 @@
<artifactId>okhttp</artifactId>
</dependency>
<!-- OpenCV -->
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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 1URL
* @param imagePath2 2URL
* @return 0-11
*/
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;
}
/**
* OpenCVMat
*
* @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));
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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("考勤失败,人脸不匹配");
}
}
}
}

View File

@ -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,
};
},
});