|
|
@@ -0,0 +1,256 @@
|
|
|
+package cn.iocoder.yudao.module.system.util.login;
|
|
|
+
|
|
|
+import cn.hutool.core.lang.Assert;
|
|
|
+import cn.hutool.core.util.ObjectUtil;
|
|
|
+import cn.iocoder.yudao.framework.common.util.io.FileDriveLetterUtils;
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.usercharacteristic.vo.FaceCutVO;
|
|
|
+import com.arcsoft.face.*;
|
|
|
+import com.arcsoft.face.enums.DetectMode;
|
|
|
+import com.arcsoft.face.enums.DetectOrient;
|
|
|
+import com.arcsoft.face.enums.ErrorInfo;
|
|
|
+import com.arcsoft.face.toolkit.ImageInfo;
|
|
|
+import com.google.common.collect.Lists;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.ExecutionException;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+
|
|
|
+import static com.arcsoft.face.toolkit.ImageFactory.getRGBData;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 面部裁剪工具
|
|
|
+ *
|
|
|
+ * @author cgj
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+public class ArcSoftMothodUtil {
|
|
|
+
|
|
|
+
|
|
|
+ static FaceEngine faceEngine;
|
|
|
+
|
|
|
+ static {
|
|
|
+ //从官网获取
|
|
|
+ String appId;
|
|
|
+ String sdkKey;
|
|
|
+ String libarcsoftFaceDllPath;
|
|
|
+ String driveLetter = FileDriveLetterUtils.getDriveLetter();
|
|
|
+ if (StringUtils.isBlank(driveLetter)) {
|
|
|
+ // 在linux
|
|
|
+ appId = "FTN3G4pk8n2RKwjD955sRapRjbYQFefwhHd4sBZMYEz6";
|
|
|
+ sdkKey = "BjJomNU2bQc2SYhT7NNqwvFd1t6RRNv5DTZNYd8pMH19";
|
|
|
+ libarcsoftFaceDllPath = "/usr/lib/ArcSoft_ArcFace_Java_Linux_x64_V3.0/libs/LINUX64";
|
|
|
+ } else {
|
|
|
+ // 在win
|
|
|
+ appId = "FTN3G4pk8n2RKwjD955sRapRjbYQFefwhHd4sBZMYEz6";
|
|
|
+ sdkKey = "BjJomNU2bQc2SYhT7NNqwvFd9zfc72Q7nneh75r3NT3x";
|
|
|
+ libarcsoftFaceDllPath = "D:\\work\\app\\appinstall\\ArcSoft_ArcFace_Java_Windows_x64_V3.0\\libs\\WIN64";
|
|
|
+ }
|
|
|
+ faceEngine = new FaceEngine(libarcsoftFaceDllPath);
|
|
|
+ //激活引擎
|
|
|
+ int errorCode = faceEngine.activeOnline(appId, sdkKey);
|
|
|
+
|
|
|
+ if (errorCode != ErrorInfo.MOK.getValue() && errorCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
|
|
|
+ System.out.println("引擎激活失败");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ ActiveFileInfo activeFileInfo = new ActiveFileInfo();
|
|
|
+ errorCode = faceEngine.getActiveFileInfo(activeFileInfo);
|
|
|
+ if (errorCode != ErrorInfo.MOK.getValue() && errorCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
|
|
|
+ System.out.println("获取激活文件信息失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ //引擎配置
|
|
|
+ EngineConfiguration engineConfiguration = new EngineConfiguration();
|
|
|
+ engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
|
|
|
+ engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);
|
|
|
+ engineConfiguration.setDetectFaceMaxNum(10);
|
|
|
+ engineConfiguration.setDetectFaceScaleVal(16);
|
|
|
+ //功能配置
|
|
|
+ FunctionConfiguration functionConfiguration = new FunctionConfiguration();
|
|
|
+ functionConfiguration.setSupportAge(true);
|
|
|
+ functionConfiguration.setSupportFace3dAngle(true);
|
|
|
+ functionConfiguration.setSupportFaceDetect(true);
|
|
|
+ functionConfiguration.setSupportFaceRecognition(true);
|
|
|
+ functionConfiguration.setSupportGender(true);
|
|
|
+ functionConfiguration.setSupportLiveness(true);
|
|
|
+ functionConfiguration.setSupportIRLiveness(true);
|
|
|
+ engineConfiguration.setFunctionConfiguration(functionConfiguration);
|
|
|
+
|
|
|
+ //初始化引擎
|
|
|
+ errorCode = faceEngine.init(engineConfiguration);
|
|
|
+
|
|
|
+ if (errorCode != ErrorInfo.MOK.getValue()) {
|
|
|
+ System.out.println("初始化引擎失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 图片人脸检测
|
|
|
+ */
|
|
|
+ public static FaceCutVO saveArcData( String imageUrl, String imagePath) {
|
|
|
+ int errorCode;
|
|
|
+ // 3--------------------------开始读取照片的特征值-------------------------------
|
|
|
+ ImageInfo imageInfo = getRGBData(new File(imagePath));
|
|
|
+ List<FaceInfo> faceInfoList = new ArrayList<>();
|
|
|
+ errorCode = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList);
|
|
|
+ Assert.isFalse(errorCode != ErrorInfo.MOK.getValue(), "获取人脸失败!");
|
|
|
+
|
|
|
+ //特征提取
|
|
|
+ FaceFeature faceFeature = new FaceFeature();
|
|
|
+ errorCode = faceEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList.get(0), faceFeature);
|
|
|
+ Assert.isFalse(errorCode != ErrorInfo.MOK.getValue(), "获取人脸特征值失败!");
|
|
|
+ byte[] featureData = faceFeature.getFeatureData();
|
|
|
+ // 4------------------------返回解析的数据---------------------
|
|
|
+ FaceCutVO faceCutVO = new FaceCutVO();
|
|
|
+ faceCutVO.setType("2");
|
|
|
+ faceCutVO.setContent(Arrays.toString(featureData));
|
|
|
+ faceCutVO.setImageUrl(imageUrl);
|
|
|
+ faceCutVO.setImagePath(imagePath);
|
|
|
+ return faceCutVO;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 相似度门槛
|
|
|
+ private static final double THRESHOLD = 0.8; // THRESHOLD越高,错误率越低,阈值[0,1]
|
|
|
+ private static final ExecutorService THREAD_POOL_EXECUTOR = Executors.newFixedThreadPool(4); // 线程池
|
|
|
+
|
|
|
+ public static FaceMatchVO completableFutureComparison(final MultipartFile file, final Set<String> matcher, String basePath) throws IOException {
|
|
|
+ int errorCode;
|
|
|
+ // 时间戳
|
|
|
+ long currentTimestamp = System.currentTimeMillis();
|
|
|
+ // 确保目录存在,不存在则创建一个
|
|
|
+ Path uploadPath = Paths.get(basePath);
|
|
|
+ if (!Files.exists(uploadPath)) {
|
|
|
+ Files.createDirectories(uploadPath);
|
|
|
+ }
|
|
|
+ // 获取文件名并构建目标路径
|
|
|
+ String fileName = basePath + currentTimestamp + "_" + file.getOriginalFilename();
|
|
|
+ Path targetLocation = uploadPath.resolve(fileName);
|
|
|
+ // 将文件写入目标路径
|
|
|
+ file.transferTo(targetLocation.toFile());
|
|
|
+ try {
|
|
|
+ //人脸检测
|
|
|
+ ImageInfo imageInfo = getRGBData(new File(fileName));
|
|
|
+ List<FaceInfo> faceInfoList = new ArrayList<>();
|
|
|
+ errorCode = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList);
|
|
|
+ Assert.isFalse(errorCode != ErrorInfo.MOK.getValue(), "未识别到人脸!");
|
|
|
+
|
|
|
+ //特征提取
|
|
|
+ FaceFeature faceFeature = new FaceFeature();
|
|
|
+ errorCode = faceEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList.get(0), faceFeature);
|
|
|
+ Assert.isFalse(errorCode != ErrorInfo.MOK.getValue(), "获取人脸特征值失败!");
|
|
|
+ byte[] featureData = faceFeature.getFeatureData();
|
|
|
+
|
|
|
+ // 转成list
|
|
|
+ List<String> matcherFeatureDataList = new ArrayList<>(matcher);
|
|
|
+
|
|
|
+ // 切分四等份
|
|
|
+ int denominator = 1;
|
|
|
+ if (matcherFeatureDataList.size() >= 4) {
|
|
|
+ denominator = 4;
|
|
|
+ }
|
|
|
+ List<List<String>> averageMatcherFeatureDataList = Lists.partition(matcherFeatureDataList, matcherFeatureDataList.size() / denominator);
|
|
|
+
|
|
|
+ // 构建四个线程进行处理,防止人员过多对比速度太慢
|
|
|
+ CompletableFuture<FaceMatchVO>[] completableFutureArray = averageMatcherFeatureDataList.stream().map(partitionFeatureData -> CompletableFuture.supplyAsync(() -> comparison(featureData, new ArrayList<>(partitionFeatureData)), THREAD_POOL_EXECUTOR)).toArray(CompletableFuture[]::new);
|
|
|
+ // 等待所有任务执行完
|
|
|
+ CompletableFuture.allOf(completableFutureArray).join();
|
|
|
+ List<FaceMatchVO> verificationList = new ArrayList<>();
|
|
|
+ for (CompletableFuture<FaceMatchVO> completableFuture : completableFutureArray) {
|
|
|
+ try {
|
|
|
+ FaceMatchVO verification = completableFuture.get();
|
|
|
+ if (ObjectUtil.isNotEmpty(verification)) {
|
|
|
+ verificationList.add(verification);
|
|
|
+ }
|
|
|
+ } catch (InterruptedException | ExecutionException e) {
|
|
|
+ log.error(e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 找出最匹配的人脸(匹配度最高)
|
|
|
+ if (ObjectUtil.isNotEmpty(verificationList)) {
|
|
|
+ FaceMatchVO max = verificationList.stream().max(Comparator.comparing(FaceMatchVO::getScore)).get();
|
|
|
+ log.info("相似值最佳的的人脸:{},分数{}", max.getContent(), max.getScore());
|
|
|
+ return max;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸比对异常:{}", e.getMessage());
|
|
|
+ } finally {
|
|
|
+ //特征值提取完毕后清除数据
|
|
|
+ removeFile(fileName);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static FaceMatchVO comparison(final byte[] featureData, final List<String> matcherFeatureDataList) {
|
|
|
+ try {
|
|
|
+ if (!matcherFeatureDataList.isEmpty()) {
|
|
|
+ // 最匹配的人脸
|
|
|
+ String match = null;
|
|
|
+ // 峰值
|
|
|
+ double high = 0;
|
|
|
+ FaceFeature targetFaceFeature = new FaceFeature();
|
|
|
+ targetFaceFeature.setFeatureData(featureData);
|
|
|
+ FaceFeature sourceFaceFeature = new FaceFeature();
|
|
|
+ FaceSimilar faceSimilar = new FaceSimilar();
|
|
|
+ for (String matcherData : matcherFeatureDataList) {
|
|
|
+ // 输入的人脸和人脸库中的人脸进行对比,找到当前线程中的分数最高的人脸
|
|
|
+ byte[] matchData = strToBytes(matcherData);
|
|
|
+ //特征比对
|
|
|
+ sourceFaceFeature.setFeatureData(matchData);
|
|
|
+ int errorCode = faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar);
|
|
|
+ Assert.isFalse(errorCode != ErrorInfo.MOK.getValue(), "人脸对比异常" + errorCode + "!");
|
|
|
+ // 相似度
|
|
|
+ float score = faceSimilar.getScore();
|
|
|
+ if (score > high) {
|
|
|
+ high = score;
|
|
|
+ match = matcherData;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return high >= THRESHOLD ? new FaceMatchVO().setContent(match).setScore(high) : null;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸比对异常:{}", e.getMessage());
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static byte[] strToBytes(String string) {
|
|
|
+ // Remove the brackets and spaces from the string
|
|
|
+ string = string.substring(1, string.length() - 1).replaceAll("\\s", "");
|
|
|
+ // Split the string by commas to get each number as a string
|
|
|
+ String[] numbers = string.split(",");
|
|
|
+ // Create a new byte array with the same length as the original
|
|
|
+ byte[] newBytes = new byte[numbers.length];
|
|
|
+ // Convert each string number back to a byte and store it in the new array
|
|
|
+ for (int i = 0; i < numbers.length; i++) {
|
|
|
+ newBytes[i] = Byte.parseByte(numbers[i]);
|
|
|
+ }
|
|
|
+ // Print the new byte array to verify it matches the original
|
|
|
+ // System.out.println("New byte array: " + Arrays.toString(newBytes));
|
|
|
+ return newBytes;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void removeFile(String path) {
|
|
|
+ try {
|
|
|
+ File file = new File(path);
|
|
|
+ if (!file.delete()) {
|
|
|
+ log.error("DAT文件删除失败: " + path);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("存在异常文件:{}", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|