|
|
@@ -0,0 +1,591 @@
|
|
|
+package com.usky.eg.controller.web;
|
|
|
+
|
|
|
+import ai.djl.modality.cv.Image;
|
|
|
+import cn.smartjavaai.common.cv.SmartImageFactory;
|
|
|
+import cn.smartjavaai.common.entity.DetectionInfo;
|
|
|
+import cn.smartjavaai.common.entity.DetectionResponse;
|
|
|
+import cn.smartjavaai.common.entity.R;
|
|
|
+import cn.smartjavaai.common.entity.face.FaceSearchResult;
|
|
|
+import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
|
|
+import cn.smartjavaai.face.entity.FaceSearchParams;
|
|
|
+import cn.smartjavaai.face.vector.entity.FaceVector;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.usky.common.core.bean.ApiResult;
|
|
|
+import com.usky.eg.domain.SysPerson;
|
|
|
+import com.usky.eg.mapper.SysPersonMapper;
|
|
|
+import com.usky.eg.service.SmartFaceDetectionService;
|
|
|
+import com.usky.eg.service.SmartFaceRecognitionService;
|
|
|
+import io.swagger.annotations.Api;
|
|
|
+import io.swagger.annotations.ApiOperation;
|
|
|
+import io.swagger.annotations.ApiParam;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * SmartJavaAI人脸识别控制器
|
|
|
+ *
|
|
|
+ * @author SmartJavaAI Integration
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Api(tags = "SmartJavaAI人脸识别接口")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/smartface")
|
|
|
+@ConditionalOnProperty(prefix = "smartface", name = "enabled", havingValue = "true")
|
|
|
+public class SmartFaceController {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SmartFaceDetectionService faceDetectionService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SmartFaceRecognitionService faceRecognitionService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SysPersonMapper sysPersonMapper;
|
|
|
+
|
|
|
+ // ==================== 人脸检测接口 ====================
|
|
|
+
|
|
|
+ @ApiOperation("人脸检测 - 上传图片")
|
|
|
+ @PostMapping("/detect")
|
|
|
+ public ApiResult<Map<String, Object>> detectFaces(
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] imageBytes = file.getBytes();
|
|
|
+ R<DetectionResponse> result = faceDetectionService.detectFaces(imageBytes);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ DetectionResponse response = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("faceCount", response.getDetectionInfoList().size());
|
|
|
+ data.put("faces", response.getDetectionInfoList());
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸检测失败", e);
|
|
|
+ return ApiResult.error("人脸检测失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("人脸检测 - 通过图片路径")
|
|
|
+ @PostMapping("/detect/path")
|
|
|
+ public ApiResult<Map<String, Object>> detectFacesByPath(
|
|
|
+ @ApiParam("图片路径") @RequestParam("imagePath") String imagePath) {
|
|
|
+ try {
|
|
|
+ R<DetectionResponse> result = faceDetectionService.detectFaces(imagePath);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ DetectionResponse response = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("faceCount", response.getDetectionInfoList().size());
|
|
|
+ data.put("faces", response.getDetectionInfoList());
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸检测失败", e);
|
|
|
+ return ApiResult.error("人脸检测失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸特征提取接口 ====================
|
|
|
+
|
|
|
+ @ApiOperation("提取人脸特征 - 上传图片")
|
|
|
+ @PostMapping("/extract/feature")
|
|
|
+ public ApiResult<Map<String, Object>> extractFeature(
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] imageBytes = file.getBytes();
|
|
|
+ R<float[]> result = faceRecognitionService.extractTopFaceFeature(imageBytes);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("featureDimension", result.getData().length);
|
|
|
+ data.put("features", result.getData());
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("提取人脸特征失败", e);
|
|
|
+ return ApiResult.error("提取人脸特征失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("提取所有人脸特征 - 上传图片")
|
|
|
+ @PostMapping("/extract/features")
|
|
|
+ public ApiResult<Map<String, Object>> extractFeatures(
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] imageBytes = file.getBytes();
|
|
|
+ Image image = SmartImageFactory.getInstance().fromInputStream(
|
|
|
+ new java.io.ByteArrayInputStream(imageBytes));
|
|
|
+
|
|
|
+ R<DetectionResponse> result = faceRecognitionService.extractFeatures(image);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ DetectionResponse response = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("faceCount", response.getDetectionInfoList().size());
|
|
|
+
|
|
|
+ List<Map<String, Object>> faceList = new ArrayList<>();
|
|
|
+ for (DetectionInfo detectionInfo : response.getDetectionInfoList()) {
|
|
|
+ Map<String, Object> faceData = new HashMap<>();
|
|
|
+ faceData.put("bbox", detectionInfo.getDetectionRectangle());
|
|
|
+ faceData.put("confidence", detectionInfo.getScore());
|
|
|
+ // 注意:DetectionInfo 可能没有 feature 字段,需要单独提取
|
|
|
+ // faceData.put("features", detectionInfo.getFeature());
|
|
|
+ // faceData.put("featureDimension", detectionInfo.getFeature() != null ? detectionInfo.getFeature().length : 0);
|
|
|
+ faceList.add(faceData);
|
|
|
+ }
|
|
|
+ data.put("faces", faceList);
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("提取人脸特征失败", e);
|
|
|
+ return ApiResult.error("提取人脸特征失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸比对接口(1:1) ====================
|
|
|
+
|
|
|
+ @ApiOperation("人脸比对(1:1)- 上传两张图片")
|
|
|
+ @PostMapping("/compare")
|
|
|
+ public ApiResult<Map<String, Object>> compareFaces(
|
|
|
+ @ApiParam("第一张图片") @RequestParam("file1") MultipartFile file1,
|
|
|
+ @ApiParam("第二张图片") @RequestParam("file2") MultipartFile file2,
|
|
|
+ @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
|
|
|
+ try {
|
|
|
+ if (file1.isEmpty() || file2.isEmpty()) {
|
|
|
+ return ApiResult.error("图片文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ Image image1 = SmartImageFactory.getInstance().fromInputStream(file1.getInputStream());
|
|
|
+ Image image2 = SmartImageFactory.getInstance().fromInputStream(file2.getInputStream());
|
|
|
+
|
|
|
+ R<Float> result = faceRecognitionService.compareFeatures(image1, image2);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ float similarity = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("similarity", similarity);
|
|
|
+ data.put("threshold", threshold);
|
|
|
+ data.put("isMatch", similarity >= threshold);
|
|
|
+ data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸比对失败", e);
|
|
|
+ return ApiResult.error("人脸比对失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("人脸比对(1:1)- 通过图片路径")
|
|
|
+ @PostMapping("/compare/path")
|
|
|
+ public ApiResult<Map<String, Object>> compareFacesByPath(
|
|
|
+ @ApiParam("第一张图片路径") @RequestParam("imagePath1") String imagePath1,
|
|
|
+ @ApiParam("第二张图片路径") @RequestParam("imagePath2") String imagePath2,
|
|
|
+ @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
|
|
|
+ try {
|
|
|
+ R<Float> result = faceRecognitionService.compareFeatures(imagePath1, imagePath2);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ float similarity = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("similarity", similarity);
|
|
|
+ data.put("threshold", threshold);
|
|
|
+ data.put("isMatch", similarity >= threshold);
|
|
|
+ data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸比对失败", e);
|
|
|
+ return ApiResult.error("人脸比对失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸比对(1:1)- base64图片 ====================
|
|
|
+
|
|
|
+ @ApiOperation("人脸比对(1:1)- base64图片")
|
|
|
+ @PostMapping("/compare/base64")
|
|
|
+ public ApiResult<Map<String, Object>> compareFacesByBase64(
|
|
|
+ @ApiParam("第一张图片base64(支持data:image/...;base64,前缀或纯base64)")
|
|
|
+ @RequestParam("imageBase641") String imageBase641,
|
|
|
+ @ApiParam("第二张图片base64(支持data:image/...;base64,前缀或纯base64)")
|
|
|
+ @RequestParam("imageBase642") String imageBase642,
|
|
|
+ @ApiParam("相似度阈值")
|
|
|
+ @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
|
|
|
+ try {
|
|
|
+ if (imageBase641 == null || imageBase641.isEmpty() || imageBase642 == null || imageBase642.isEmpty()) {
|
|
|
+ return ApiResult.error("图片base64不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ String base64Data1 = imageBase641.contains(",")
|
|
|
+ ? imageBase641.substring(imageBase641.indexOf(",") + 1)
|
|
|
+ : imageBase641;
|
|
|
+ String base64Data2 = imageBase642.contains(",")
|
|
|
+ ? imageBase642.substring(imageBase642.indexOf(",") + 1)
|
|
|
+ : imageBase642;
|
|
|
+
|
|
|
+ byte[] imageBytes1 = java.util.Base64.getDecoder().decode(base64Data1);
|
|
|
+ byte[] imageBytes2 = java.util.Base64.getDecoder().decode(base64Data2);
|
|
|
+
|
|
|
+ Image image1 = SmartImageFactory.getInstance().fromInputStream(
|
|
|
+ new java.io.ByteArrayInputStream(imageBytes1));
|
|
|
+ Image image2 = SmartImageFactory.getInstance().fromInputStream(
|
|
|
+ new java.io.ByteArrayInputStream(imageBytes2));
|
|
|
+
|
|
|
+ R<Float> result = faceRecognitionService.compareFeatures(image1, image2);
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ float similarity = result.getData();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("similarity", similarity);
|
|
|
+ data.put("threshold", threshold);
|
|
|
+ data.put("isMatch", similarity >= threshold);
|
|
|
+ data.put("confidenceLevel", getConfidenceLevel(similarity, threshold));
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸比对失败", e);
|
|
|
+ return ApiResult.error("人脸比对失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸注册接口 ====================
|
|
|
+
|
|
|
+ @ApiOperation("注册人脸 - 上传图片")
|
|
|
+ @PostMapping("/register")
|
|
|
+ public ApiResult<Map<String, Object>> registerFace(
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
|
|
|
+ @ApiParam("用户ID(可选)") @RequestParam(value = "userId", required = false) String userId,
|
|
|
+ @ApiParam("元数据(JSON格式)") @RequestParam(value = "metadata", required = false) String metadata) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
|
|
|
+
|
|
|
+ FaceRegisterInfo registerInfo = new FaceRegisterInfo();
|
|
|
+ if (userId != null && !userId.isEmpty()) {
|
|
|
+ registerInfo.setId(userId);
|
|
|
+ }
|
|
|
+ if (metadata != null && !metadata.isEmpty()) {
|
|
|
+ registerInfo.setMetadata(metadata);
|
|
|
+ }
|
|
|
+
|
|
|
+ R<String> result = faceRecognitionService.registerFace(registerInfo, image);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("faceId", result.getData());
|
|
|
+ data.put("message", "人脸注册成功");
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("注册人脸失败", e);
|
|
|
+ return ApiResult.error("注册人脸失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸搜索接口(1:N) ====================
|
|
|
+
|
|
|
+ @ApiOperation("搜索人脸(1:N)- 上传图片")
|
|
|
+ @PostMapping("/search")
|
|
|
+ public ApiResult<Map<String, Object>> searchFace(
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
|
|
|
+ @ApiParam("返回结果数量") @RequestParam(value = "topK", defaultValue = "5") int topK,
|
|
|
+ @ApiParam("相似度阈值") @RequestParam(value = "threshold", defaultValue = "0.62") float threshold) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待人脸库加载完成
|
|
|
+ if (!faceRecognitionService.isLoadFaceCompleted()) {
|
|
|
+ return ApiResult.error("人脸库正在加载中,请稍后再试");
|
|
|
+ }
|
|
|
+
|
|
|
+ Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
|
|
|
+
|
|
|
+ FaceSearchParams searchParams = new FaceSearchParams();
|
|
|
+ searchParams.setTopK(topK);
|
|
|
+ searchParams.setThreshold(threshold);
|
|
|
+
|
|
|
+ List<FaceSearchResult> searchResults = faceRecognitionService.searchFace(image, searchParams);
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("resultCount", searchResults.size());
|
|
|
+ data.put("results", searchResults);
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("搜索人脸失败", e);
|
|
|
+ return ApiResult.error("搜索人脸失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸管理接口 ====================
|
|
|
+
|
|
|
+ @ApiOperation("删除人脸")
|
|
|
+ @DeleteMapping("/delete/{faceId}")
|
|
|
+ public ApiResult<String> deleteFace(
|
|
|
+ @ApiParam("人脸ID") @PathVariable("faceId") String faceId) {
|
|
|
+ try {
|
|
|
+ R<Boolean> result = faceRecognitionService.deleteFace(faceId);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ return ApiResult.success("人脸删除成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("删除人脸失败: {}", faceId, e);
|
|
|
+ return ApiResult.error("删除人脸失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("获取人脸信息")
|
|
|
+ @GetMapping("/info/{faceId}")
|
|
|
+ public ApiResult<FaceVector> getFaceInfo(
|
|
|
+ @ApiParam("人脸ID") @PathVariable("faceId") String faceId) {
|
|
|
+ try {
|
|
|
+ R<FaceVector> result = faceRecognitionService.getFaceById(faceId);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ return ApiResult.success(result.getData());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取人脸信息失败: {}", faceId, e);
|
|
|
+ return ApiResult.error("获取人脸信息失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("获取人脸列表")
|
|
|
+ @GetMapping("/list")
|
|
|
+ public ApiResult<Map<String, Object>> listFaces(
|
|
|
+ @ApiParam("页码") @RequestParam(value = "page", defaultValue = "1") int page,
|
|
|
+ @ApiParam("每页大小") @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
|
|
|
+ try {
|
|
|
+ R<List<FaceVector>> result = faceRecognitionService.listFaces(page, pageSize);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("page", page);
|
|
|
+ data.put("pageSize", pageSize);
|
|
|
+ data.put("list", result.getData());
|
|
|
+ data.put("total", result.getData().size());
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取人脸列表失败", e);
|
|
|
+ return ApiResult.error("获取人脸列表失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("更新人脸")
|
|
|
+ @PutMapping("/update")
|
|
|
+ public ApiResult<String> updateFace(
|
|
|
+ @ApiParam("人脸ID") @RequestParam("faceId") String faceId,
|
|
|
+ @ApiParam("图片文件") @RequestParam("file") MultipartFile file,
|
|
|
+ @ApiParam("元数据(JSON格式)") @RequestParam(value = "metadata", required = false) String metadata) {
|
|
|
+ try {
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return ApiResult.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ Image image = SmartImageFactory.getInstance().fromInputStream(file.getInputStream());
|
|
|
+
|
|
|
+ FaceRegisterInfo registerInfo = new FaceRegisterInfo();
|
|
|
+ registerInfo.setId(faceId);
|
|
|
+ if (metadata != null && !metadata.isEmpty()) {
|
|
|
+ registerInfo.setMetadata(metadata);
|
|
|
+ }
|
|
|
+
|
|
|
+ R<Boolean> result = faceRecognitionService.updateFace(registerInfo, image);
|
|
|
+
|
|
|
+ if (!result.isSuccess()) {
|
|
|
+ return ApiResult.error(result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ return ApiResult.success("人脸更新成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("更新人脸失败: {}", faceId, e);
|
|
|
+ return ApiResult.error("更新人脸失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("检查人脸库加载状态")
|
|
|
+ @GetMapping("/status")
|
|
|
+ public ApiResult<Map<String, Object>> checkStatus() {
|
|
|
+ try {
|
|
|
+ boolean isLoaded = faceRecognitionService.isLoadFaceCompleted();
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("isLoaded", isLoaded);
|
|
|
+ data.put("status", isLoaded ? "READY" : "LOADING");
|
|
|
+ data.put("message", isLoaded ? "人脸库已加载完成" : "人脸库正在加载中");
|
|
|
+
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("检查人脸库状态失败", e);
|
|
|
+ return ApiResult.error("检查人脸库状态失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 人脸比对(1:N)- 与sys_person表比对 ====================
|
|
|
+
|
|
|
+ @ApiOperation("人脸比对(1:N)- 通过base64图片与sys_person表人脸库比对")
|
|
|
+ @PostMapping("/compare/person")
|
|
|
+ public ApiResult<Map<String, Object>> compareFaceWithPersonLib(
|
|
|
+ @ApiParam("入参 JSON: {\"faceBase\":\"...\",\"threshold\":0.7},默认相似度阈值 0.7(约 70%)") @RequestBody Map<String, Object> body) {
|
|
|
+ try {
|
|
|
+ String faceBase = body != null ? (String) body.get("faceBase") : null;
|
|
|
+ Object thresholdObj = body != null ? body.get("threshold") : null;
|
|
|
+ float threshold = thresholdObj instanceof Number ? ((Number) thresholdObj).floatValue() : 0.70f;
|
|
|
+
|
|
|
+ if (faceBase == null || faceBase.isEmpty()) {
|
|
|
+ return comparePersonFailureResponse("faceBase不能为空", null);
|
|
|
+ }
|
|
|
+
|
|
|
+ int sep = faceBase.indexOf(',');
|
|
|
+ String targetPayload = sep >= 0 ? faceBase.substring(sep + 1) : faceBase;
|
|
|
+ byte[] imageBytes = Base64.getDecoder().decode(targetPayload);
|
|
|
+ Image targetImage = SmartImageFactory.getInstance().fromInputStream(new ByteArrayInputStream(imageBytes));
|
|
|
+
|
|
|
+ // 提取目标人脸特征(兼作是否有人脸;不用独立 detection,避免模型路径未配时初始化失败)
|
|
|
+ R<float[]> featureResult = faceRecognitionService.extractTopFaceFeature(targetImage);
|
|
|
+ if (!featureResult.isSuccess() || featureResult.getData() == null) {
|
|
|
+ return comparePersonFailureResponse("未检测到人脸,请上传包含清晰人脸的图片",
|
|
|
+ Collections.singletonMap("hasFace", false));
|
|
|
+ }
|
|
|
+ float[] targetFeatures = featureResult.getData();
|
|
|
+
|
|
|
+ List<SysPerson> personList = sysPersonMapper.selectAllValidFace();
|
|
|
+ if (personList == null || personList.isEmpty()) {
|
|
|
+ return comparePersonFailureResponse("人脸库为空,请先录入人员人脸信息", null);
|
|
|
+ }
|
|
|
+
|
|
|
+ float maxSimilarity = 0f;
|
|
|
+ SysPerson matchedPerson = null;
|
|
|
+ for (SysPerson person : personList) {
|
|
|
+ try {
|
|
|
+ String personFaceBase = person.getFaceBase();
|
|
|
+ if (personFaceBase == null || personFaceBase.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ sep = personFaceBase.indexOf(',');
|
|
|
+ String personPayload = sep >= 0 ? personFaceBase.substring(sep + 1) : personFaceBase;
|
|
|
+ byte[] personImageBytes = Base64.getDecoder().decode(personPayload);
|
|
|
+ R<float[]> personFeatureResult = faceRecognitionService.extractTopFaceFeature(personImageBytes);
|
|
|
+ if (!personFeatureResult.isSuccess() || personFeatureResult.getData() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ float similarity = faceRecognitionService.calculateSimilarity(targetFeatures, personFeatureResult.getData());
|
|
|
+ if (similarity > maxSimilarity) {
|
|
|
+ maxSimilarity = similarity;
|
|
|
+ matchedPerson = person;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("比对人员ID={}的人脸时发生异常,已跳过: {}", person.getId(), e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>(8);
|
|
|
+ boolean matched = maxSimilarity >= threshold && matchedPerson != null;
|
|
|
+ data.put("success", matched);
|
|
|
+ data.put("hasFace", true);
|
|
|
+ data.put("faceCount", 1);
|
|
|
+ data.put("threshold", threshold);
|
|
|
+ data.put("maxSimilarity", maxSimilarity);
|
|
|
+ data.put("isMatch", matched);
|
|
|
+ if (matched) {
|
|
|
+ Map<String, Object> personInfo = new HashMap<>(8);
|
|
|
+ personInfo.put("id", matchedPerson.getId());
|
|
|
+ personInfo.put("fullName", matchedPerson.getFullName());
|
|
|
+ personInfo.put("faceName", matchedPerson.getFaceName());
|
|
|
+ personInfo.put("cardNum", matchedPerson.getCardNum());
|
|
|
+ data.put("matchedPerson", personInfo);
|
|
|
+ } else {
|
|
|
+ data.put("matchedPerson", null);
|
|
|
+ data.put("message", "未找到匹配的人员");
|
|
|
+ }
|
|
|
+ return ApiResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("人脸1:N比对失败", e);
|
|
|
+ return comparePersonFailureResponse("人脸比对失败: " + e.getMessage(), null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 人脸与 sys_person 比对:业务失败时仍返回 HTTP 成功包装,data 内 success=false + message 供调用方判断。
|
|
|
+ */
|
|
|
+ private ApiResult<Map<String, Object>> comparePersonFailureResponse(String message, Map<String, Object> extra) {
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("success", false);
|
|
|
+ data.put("message", message);
|
|
|
+ if (extra != null && !extra.isEmpty()) {
|
|
|
+ data.putAll(extra);
|
|
|
+ }
|
|
|
+ return ApiResult.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取置信度等级
|
|
|
+ */
|
|
|
+ private String getConfidenceLevel(float similarity, float threshold) {
|
|
|
+ if (similarity >= threshold + 0.15f) {
|
|
|
+ return "HIGH";
|
|
|
+ } else if (similarity >= threshold) {
|
|
|
+ return "MEDIUM";
|
|
|
+ } else if (similarity >= threshold - 0.10f) {
|
|
|
+ return "LOW";
|
|
|
+ } else {
|
|
|
+ return "VERY_LOW";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|