Retinaface+CurricularFace与SpringBoot集成:Web端人脸识别服务开发
最近在做一个内部员工管理系统,需要集成人脸识别功能来做门禁和签到。市面上现成的方案要么太贵,要么不够灵活,于是决定自己动手,基于开源的Retinaface和CurricularFace模型,用SpringBoot搭建一个Web服务。
这个方案最大的好处是可控性强,从人脸检测、特征提取到比对识别,整个流程都能自己掌握。而且SpringBoot的生态成熟,前后端对接、API设计、性能优化都有现成的轮子可用。今天就来分享一下具体的实现思路和踩过的坑。
1. 为什么选择这个技术栈?
在开始动手之前,我们先看看为什么选Retinaface+CurricularFace+SpringBoot这个组合。
1.1 Retinaface:精准的人脸检测器
Retinaface是一个单阶段的人脸检测模型,它的特点是检测精度高,尤其是在小脸和密集人脸的场景下表现很好。我们之前测试过几个开源的人脸检测模型,Retinaface在准确率和速度上找到了不错的平衡点。
它不仅能框出人脸位置,还能给出5个关键点(双眼、鼻尖、嘴角),这对后续的人脸对齐很重要。对齐后的人脸图片,特征提取的效果会更好。
1.2 CurricularFace:更聪明的特征提取
CurricularFace是ArcFace的一个改进版本,它在训练时引入了一个“课程学习”的机制。简单来说,就是先学简单的样本,再学难的样本,这样模型学到的特征更有区分度。
在实际测试中,CurricularFace提取的512维特征向量,在相似度计算时,同一个人不同照片的相似度更高,不同人之间的差异更明显。这意味着识别准确率会更好。
1.3 SpringBoot:快速搭建Web服务
SpringBoot就不用多说了,Java生态里最流行的Web框架。我们选它主要是考虑这几个点:
- 开发速度快:各种starter依赖,配置简单,能快速搭起一个可用的服务
- 生态丰富:需要什么功能,基本上都能找到现成的库或解决方案
- 易于维护:团队里Java开发人员多,后续维护成本低
- 性能不错:经过适当优化,完全能满足中小规模的人脸识别需求
2. 整体架构设计
我们的目标是一个完整的Web端人脸识别服务,用户可以通过浏览器上传图片,服务端处理后返回识别结果。整体架构分为三层:
前端页面 → SpringBoot服务 → 人脸识别模型 → 数据库2.1 服务端核心模块
在SpringBoot项目中,我们设计了几个核心模块:
模型加载模块:负责在服务启动时加载Retinaface和CurricularFace模型。这里要注意的是,模型文件比较大(几百MB),加载需要一定时间,我们用了懒加载和缓存机制,避免每次请求都重新加载。
图片处理模块:接收前端上传的图片,进行预处理,包括格式转换、尺寸调整、颜色空间转换等。OpenCV在这里派上了大用场。
人脸识别管道:这是最核心的部分,一个完整的处理流程包括:
- 用Retinaface检测图片中的人脸
- 根据关键点进行人脸对齐
- 用CurricularFace提取对齐后人脸的特征向量
- 与数据库中已有的人脸特征进行相似度计算
- 返回识别结果(是谁,或者不认识)
数据管理模块:负责管理已知人脸的特征向量。我们用了Redis做缓存,MySQL做持久化存储。新用户注册时,会提取特征向量存入数据库;识别时,先从Redis查,没有再去MySQL查。
2.2 API设计
对外提供两个主要的RESTful API:
注册接口(POST /api/face/register)
@PostMapping("/register") public ApiResponse<String> registerFace( @RequestParam("userId") String userId, @RequestParam("image") MultipartFile image) { // 1. 校验图片格式和大小 // 2. 检测人脸并提取特征 // 3. 保存特征到数据库 // 4. 返回成功或失败 }识别接口(POST /api/face/recognize)
@PostMapping("/recognize") public ApiResponse<RecognitionResult> recognizeFace( @RequestParam("image") MultipartFile image, @RequestParam(value = "threshold", defaultValue = "0.6") float threshold) { // 1. 检测图片中所有人脸 // 2. 对每张脸提取特征 // 3. 与数据库比对,找到最相似的人 // 4. 返回识别结果列表 }查询接口(GET /api/face/users) 和删除接口(DELETE /api/face/{userId}) 也是必要的,方便管理已注册的用户。
3. 关键代码实现
3.1 模型加载与初始化
模型加载是比较耗时的操作,我们放在服务启动时完成:
@Component public class FaceModelLoader { private RetinaFaceDetector detector; private CurricularFaceRecognizer recognizer; @PostConstruct public void init() { // 异步加载,不阻塞服务启动 CompletableFuture.runAsync(() -> { try { // 加载Retinaface模型 detector = new RetinaFaceDetector(); detector.loadModel("models/retinaface.onnx"); // 加载CurricularFace模型 recognizer = new CurricularFaceRecognizer(); recognizer.loadModel("models/curricularface.onnx"); log.info("人脸识别模型加载完成"); } catch (Exception e) { log.error("模型加载失败", e); } }); } public RetinaFaceDetector getDetector() { return detector; } public CurricularFaceRecognizer getRecognizer() { return recognizer; } }3.2 人脸检测与对齐
这是识别流程的第一步,也是影响准确率的关键:
@Service public class FaceDetectionService { @Autowired private FaceModelLoader modelLoader; public List<FaceBox> detectFaces(Mat image) { RetinaFaceDetector detector = modelLoader.getDetector(); // 调整图片尺寸,太大影响速度,太小影响精度 Mat resizedImage = resizeImage(image, 640, 480); // 执行人脸检测 List<FaceBox> faceBoxes = detector.detect(resizedImage); // 过滤掉置信度太低的结果 return faceBoxes.stream() .filter(box -> box.getConfidence() > 0.9f) .collect(Collectors.toList()); } public Mat alignFace(Mat image, FaceBox faceBox) { // 获取5个关键点 Point[] landmarks = faceBox.getLandmarks(); // 目标对齐位置(标准脸) Point[] dstPoints = new Point[] { new Point(38.2946, 51.6963), // 左眼 new Point(73.5318, 51.5014), // 右眼 new Point(56.0252, 71.7366), // 鼻子 new Point(41.5493, 92.3655), // 左嘴角 new Point(70.7299, 92.2041) // 右嘴角 }; // 计算仿射变换矩阵 Mat transform = opencv_calib3d.estimateAffinePartial2D( new MatOfPoint2f(landmarks), new MatOfPoint2f(dstPoints) ); // 执行对齐变换 Mat alignedFace = new Mat(); opencv_imgproc.warpAffine( image, alignedFace, transform, new Size(112, 112) // CurricularFace要求的输入尺寸 ); return alignedFace; } }3.3 特征提取与比对
对齐后的人脸图片,就可以提取特征向量了:
@Service public class FaceRecognitionService { @Autowired private FaceModelLoader modelLoader; @Autowired private FaceFeatureRepository featureRepository; public float[] extractFeature(Mat alignedFace) { CurricularFaceRecognizer recognizer = modelLoader.getRecognizer(); // 图片预处理:归一化、转BGR等 Mat processed = preprocessImage(alignedFace); // 提取512维特征向量 return recognizer.extractFeature(processed); } public RecognitionResult recognize(float[] queryFeature, float threshold) { // 从数据库获取所有已注册的特征 List<FaceFeature> allFeatures = featureRepository.findAll(); String bestMatchUserId = null; float bestSimilarity = 0; for (FaceFeature feature : allFeatures) { // 计算余弦相似度 float similarity = cosineSimilarity(queryFeature, feature.getFeatureVector()); if (similarity > bestSimilarity) { bestSimilarity = similarity; bestMatchUserId = feature.getUserId(); } } // 判断是否超过阈值 if (bestSimilarity >= threshold) { return new RecognitionResult(bestMatchUserId, bestSimilarity, true); } else { return new RecognitionResult(null, bestSimilarity, false); } } private float cosineSimilarity(float[] a, float[] b) { float dotProduct = 0; float normA = 0; float normB = 0; for (int i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return (float) (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))); } }3.4 完整的识别流程
把上面的模块组合起来,就是一个完整的识别服务:
@RestController @RequestMapping("/api/face") public class FaceRecognitionController { @Autowired private FaceDetectionService detectionService; @Autowired private FaceRecognitionService recognitionService; @PostMapping("/recognize") public ApiResponse<List<RecognitionResult>> recognize( @RequestParam("image") MultipartFile file, @RequestParam(value = "threshold", defaultValue = "0.6") float threshold) { try { // 1. 转换图片格式 Mat image = convertMultipartFileToMat(file); // 2. 检测人脸 List<FaceBox> faces = detectionService.detectFaces(image); List<RecognitionResult> results = new ArrayList<>(); // 3. 对每张脸进行识别 for (FaceBox face : faces) { // 对齐人脸 Mat alignedFace = detectionService.alignFace(image, face); // 提取特征 float[] feature = recognitionService.extractFeature(alignedFace); // 比对识别 RecognitionResult result = recognitionService.recognize(feature, threshold); // 添加位置信息 result.setFaceBox(face); results.add(result); } return ApiResponse.success(results); } catch (Exception e) { log.error("人脸识别失败", e); return ApiResponse.error("识别失败: " + e.getMessage()); } } }4. 性能优化实践
在实际使用中,性能是个大问题。特别是并发请求多的时候,模型推理速度直接影响了用户体验。我们做了几个优化:
4.1 图片预处理优化
原始图片可能很大,直接送给模型检测很慢。我们根据经验设置了一个最大尺寸:
private Mat resizeImage(Mat image, int maxWidth, int maxHeight) { int width = image.cols(); int height = image.rows(); // 如果图片太大,等比例缩小 if (width > maxWidth || height > maxHeight) { float scale = Math.min( (float) maxWidth / width, (float) maxHeight / height ); int newWidth = (int) (width * scale); int newHeight = (int) (height * scale); Mat resized = new Mat(); opencv_imgproc.resize(image, resized, new Size(newWidth, newHeight)); return resized; } return image; }4.2 批量处理支持
当需要识别多张图片时,批量处理能显著提升效率。我们修改了识别接口,支持一次上传多张图片:
@PostMapping("/batch-recognize") public ApiResponse<Map<String, List<RecognitionResult>>> batchRecognize( @RequestParam("images") MultipartFile[] files) { // 使用并行流处理多张图片 Map<String, List<RecognitionResult>> results = Arrays.stream(files) .parallel() .collect(Collectors.toMap( MultipartFile::getOriginalFilename, file -> recognizeSingleImage(file) )); return ApiResponse.success(results); }4.3 特征缓存机制
用户注册后,特征向量会被缓存到Redis中。识别时先从Redis查,查不到再去数据库,并重新缓存:
@Service public class FaceFeatureService { @Autowired private RedisTemplate<String, byte[]> redisTemplate; private static final String FEATURE_KEY_PREFIX = "face:feature:"; public float[] getFeatureByUserId(String userId) { // 先从Redis获取 byte[] cached = redisTemplate.opsForValue().get(FEATURE_KEY_PREFIX + userId); if (cached != null) { return bytesToFloatArray(cached); } // Redis没有,查数据库 FaceFeature feature = featureRepository.findByUserId(userId); if (feature == null) { return null; } float[] featureVector = feature.getFeatureVector(); // 存入Redis,设置24小时过期 redisTemplate.opsForValue().set( FEATURE_KEY_PREFIX + userId, floatArrayToBytes(featureVector), 24, TimeUnit.HOURS ); return featureVector; } }4.4 异步处理与响应
人脸识别比较耗时,我们用了Spring的异步支持,让请求快速返回,处理结果通过WebSocket推送给前端:
@Async @PostMapping("/async-recognize") public CompletableFuture<ApiResponse<String>> asyncRecognize( @RequestParam("image") MultipartFile file) { String taskId = UUID.randomUUID().toString(); // 立即返回任务ID ApiResponse<String> immediateResponse = ApiResponse.success(taskId); // 后台处理识别任务 executorService.submit(() -> { try { List<RecognitionResult> results = processRecognition(file); // 通过WebSocket推送结果 webSocketService.sendResult(taskId, results); } catch (Exception e) { webSocketService.sendError(taskId, e.getMessage()); } }); return CompletableFuture.completedFuture(immediateResponse); }5. 前端交互实现
前端用Vue.js实现,主要功能包括图片上传、实时预览、结果显示等。
5.1 图片上传组件
<template> <div class="face-upload"> <input type="file" accept="image/*" @change="handleFileChange" ref="fileInput" /> <div v-if="previewUrl" class="preview"> <img :src="previewUrl" alt="预览" /> <button @click="recognize">开始识别</button> </div> <div v-if="loading" class="loading">识别中...</div> <div v-if="results" class="results"> <div v-for="(result, index) in results" :key="index"> <p>人脸 {{ index + 1 }}: <span v-if="result.recognized"> {{ result.userId }} (相似度: {{ (result.similarity * 100).toFixed(1) }}%) </span> <span v-else>未识别</span> </p> </div> </div> </div> </template> <script> export default { data() { return { previewUrl: null, loading: false, results: null }; }, methods: { handleFileChange(event) { const file = event.target.files[0]; if (!file) return; // 生成预览图 this.previewUrl = URL.createObjectURL(file); this.file = file; }, async recognize() { this.loading = true; this.results = null; const formData = new FormData(); formData.append('image', this.file); formData.append('threshold', 0.6); try { const response = await fetch('/api/face/recognize', { method: 'POST', body: formData }); const data = await response.json(); this.results = data.data; } catch (error) { console.error('识别失败', error); } finally { this.loading = false; } } } }; </script>5.2 实时视频识别
对于门禁等场景,还需要支持摄像头实时识别:
// 开启摄像头 async function startCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); const video = document.getElementById('camera'); video.srcObject = stream; // 每2秒截取一帧进行识别 setInterval(() => { captureAndRecognize(video); }, 2000); } // 截取并识别 async function captureAndRecognize(video) { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0); // 转换为Blob canvas.toBlob(async (blob) => { const formData = new FormData(); formData.append('image', blob, 'frame.jpg'); const response = await fetch('/api/face/recognize', { method: 'POST', body: formData }); const results = await response.json(); updateRecognitionResults(results.data); }, 'image/jpeg'); }6. 部署与监控
6.1 Docker化部署
为了便于部署,我们把整个服务打包成Docker镜像:
FROM openjdk:11-jre-slim # 安装OpenCV依赖 RUN apt-get update && apt-get install -y \ libopencv-dev \ python3-opencv \ && rm -rf /var/lib/apt/lists/* # 复制应用 COPY target/face-recognition-service.jar /app.jar COPY models /models # 创建非root用户 RUN useradd -m -u 1000 appuser USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]6.2 健康检查与监控
Spring Boot Actuator提供了健康检查端点,我们可以自定义人脸识别模型的健康状态:
@Component public class FaceModelHealthIndicator implements HealthIndicator { @Autowired private FaceModelLoader modelLoader; @Override public Health health() { if (modelLoader.isLoaded()) { return Health.up() .withDetail("retinaface", "loaded") .withDetail("curricularface", "loaded") .build(); } else { return Health.down() .withDetail("error", "models not loaded") .build(); } } }在application.yml中配置Actuator端点:
management: endpoints: web: exposure: include: health,metrics,info endpoint: health: show-details: always6.3 日志与告警
我们用了Logback记录详细的识别日志,包括每次请求的图片、识别结果、耗时等:
<!-- logback-spring.xml --> <appender name="FACE_RECOGNITION" class="ch.qos.logback.core.FileAppender"> <file>logs/face-recognition.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} | %msg%n</pattern> </encoder> </appender> <logger name="com.example.face.recognition" level="INFO"> <appender-ref ref="FACE_RECOGNITION" /> </logger>对于异常情况,比如识别失败率突然升高,我们配置了告警规则,通过邮件或钉钉通知开发人员。
7. 总结
整个项目做下来,最大的感受是开源模型的成熟度已经很高了,Retinaface和CurricularFace的组合在准确率上完全能满足大多数业务场景。SpringBoot的生态也确实强大,从Web服务到数据库,从缓存到监控,每个环节都有成熟的解决方案。
性能方面,经过优化后,单张图片的识别时间能控制在200-300毫秒,完全能满足Web端的实时性要求。当然,如果并发量特别大,可能需要考虑模型推理的进一步优化,比如用TensorRT加速,或者部署多个实例做负载均衡。
安全性也是需要考虑的,我们后来增加了图片防篡改校验、请求频率限制、识别结果审计日志等功能,确保服务不会被滥用。
如果你也在考虑自建人脸识别服务,这个方案值得一试。从零开始搭建大概需要2-3周时间,但后续的维护和定制化会非常灵活。特别是对于有特殊需求的业务场景,自己掌控全流程的优势就很明显了。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。