简介:一份基于SpringBoot与Vue的前后端分离在线人脸识别Web系统源码,适合正在学习Java后端、Vue前端以及人脸识别接口集成的开发者。系统通过前端调用笔记本或网络摄像头,每秒截帧并以Base64传给后端,后端结合虹软离线SDK提取人脸特征进行比对,从而完成实时识别。压缩包共51个文件,约1.08MB,包含Vue前端源码(js/vue)、SpringBoot后端工程(java/xml/properties)、虹软SDK相关jar与dat文件,以及mvnw、package.json等构建脚本和README说明,目录清晰便于按模块阅读。该项目从摄像头取流、数据编码传输到后端特征比对均有完整实现,适合作为课程设计或毕业设计参考。已有429人浏览学习,是一份实操性较强的人脸识别Web应用示例。
1. 在 SpringBoot+Vue 里,一套在线人脸识别 Web 系统的关键不是模型,而是链路
笔记本摄像头和网络摄像头,这两类设备已经把标题里最需要区分的问题说清楚了:本地 USB 摄像头由浏览器调用自带的 getUserMedia 就能取流,网络摄像头大多走 RTSP/RTMP 协议,不是浏览器原生能直接读的。把这条链路分给 SpringBoot 和 Vue,前端负责采集、抽帧和结果展示,后端负责检测、特征提取和比对,可以做出一个真正能用的在线人脸识别 Web 系统。做企业应用的人不用从零训练模型,但要清楚一帧人脸从摄像头到屏幕要经过哪几个环节、每一步的延迟从哪来,否则项目会卡在“模型能识别但页面动不了”这种尴尬阶段。文中代码和参数可直接落到本地联调,适合做门禁、访客签到、考勤打卡这类场景。
2. 摄像头到浏览器再到后端:实时识别的基础数据链路
2.1 识别引擎放前端还是后端
常见做法有两种:前端用 face-api.js 或 MediaPipe 直接在浏览器里跑模型,后端只存结果;后端用 OpenCV DNN 或 ONNX Runtime 跑模型,前端只推帧。这个标题锁定了 SpringBoot+Vue,说明识别引擎天然放在后端更合适:一是模型和底库集中管理,换模型不用重新发前端包;二是多人同时在线时,特征向量检索在后端更容易做统一扩展;三是笔记本摄像头和网络摄像头可以用同一套后端接口接收,前端不用关心 RTSP 的差异。
浏览器端模型也不是不能用,但代价是每个客户端都要加载模型文件,低配电脑上 GPU 不可用时会明显掉帧。后端推理则可以把模型预热放到 SpringBoot 启动阶段,CPU 版本的 OpenCV DNN 在 640x480 输入下,单帧检测加特征提取大约 80 到 120 毫秒,足够支撑 10 帧以下的实时识别。
2.2 一帧画面的识别时间线
从按下摄像头启动按钮到页面显示识别框,数据流是固定的:摄像头采集画面,video 元素实时播放,canvas 周期性截取当前画面,转成 JPEG,通过 WebSocket 推给 SpringBoot 后端,后端解码图像并完成检测和比对,再把结果 JSON 推回前端。参照下面这张时间线表,能快速判断瓶颈落在哪个节点:
| 阶段 | 承担方 | 单帧耗时参考 |
|---|---|---|
| 摄像头采集与渲染 | 浏览器 | 约 10 毫秒 |
| canvas 截帧与 JPEG 压缩 | 浏览器 | 约 20 到 40 毫秒 |
| WebSocket 上传 | 浏览器到后端 | 约 5 到 20 毫秒 |
| 图像解码与检测 | SpringBoot + OpenCV | 约 40 到 60 毫秒 |
| 特征提取与比对 | SpringBoot + ONNX | 约 30 到 50 毫秒 |
| 结果推送与渲染 | 后端到浏览器 | 约 5 到 10 毫秒 |
单看每一项都不算慢,但帧率一旦超过每秒 10 帧,WebSocket 消息队列和线程池就会迅速积压。所以链路的设计核心是:帧率要可控,处理要异步,结果要有序。SpringBoot 端不要把识别逻辑直接写进 WebSocket 的 onMessage 里,否则一帧的处理时间会把后面所有帧全部堵住。
2.3 笔记本摄像头和网络摄像头的两种接入路线
笔记本摄像头是标准 UVC 设备,浏览器里通过navigator.mediaDevices.getUserMedia可以直接拿到 MediaStream,这个方案最稳定,因为视频流从头到尾没有离开用户本机,延迟最低。而网络摄像头分两类:一类是 USB 摄像头插在迷你主机上,浏览器依然按本地摄像头处理;另一类是真正的 IP 摄像头,比如海康、大华设备,它们对外暴露的是 RTSP 地址,例如rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101,这类设备无法被浏览器直接读取,需要后端先拉流再转发。
我一般会在 SpringBoot 项目里集成 FFmpeg 拉取 RTSP 流并转成 HLS,前端用支持 m3u8 播放的 videojs 来播放,再从 video 元素里抽帧。这里有一个特别容易踩的坑:RTSP 转 HLS 默认的切片延迟可能在 2 到 8 秒,直接拿来做识别会觉得摄像头“卡顿”。解决方式是调整 hls_time 和 hls_list_size 参数,把切片压到 0.5 秒以内,或者干脆把 RTSP 转成 MJPEG 流,让后端逐帧读取。命令可以参考:
ffmpeg -rtsp_transport tcp -i rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101 \ -c:v copy -f hls -hls_time 0.5 -hls_list_size 3 -hls_flags delete_segments \ /data/hls/camera01/index.m3u8逻辑说明:-rtsp_transport tcp强制走 TCP 通道,避免 UDP 丢包导致画面花屏;-c:v copy表示不转码直接从 H.264 原流封装进 HLS,降低 CPU 占用;hls_time 0.5是关键,它把每个视频切片切成 0.5 秒,播放器才能获得接近实时的体验。如果摄像头输出的是 H.265 编码,-c:v copy会失效,因为大多数浏览器不直接支持 H.265 的 HLS 播放,这种情况必须加上-c:v libx264 -preset ultrafast做转码。
本地联调时先跑后端再跑前端,分别启动两个终端:
mvn spring-boot:run -Dspring-boot.run.profiles=dev cd frontend && npm install && npm run dev启动正常后,浏览器打开 Vue 开发服务器地址,摄像头权限弹窗出现,确认这一步后,再往后端识别服务里加逻辑就顺理成章了。
3. SpringBoot 后端实现:WebSocket 接收帧与 OpenCV 推理服务
3.1 WebSocket 端点和消息缓冲区的设置
SpringBoot 里做 WebSocket 可以用原生@ServerEndpoint,也可以走 Spring 的WebSocketHandler。实时推帧场景推荐前者,因为它直接面向 Session,处理消息的代码更短,和线程池配合也更直观。一个能接收 JPEG 帧的人脸识别端点,要处理的第一件事并不是识别,而是把消息体缓冲调大。Tomcat 对 WebSocket 文本消息默认限制在 8192 字节左右,一张 JPEG 图即使压缩到 0.7 质量也经常超过 50KB,不设置缓冲区会直接抛异常。
@ServerEndpoint("/ws/face") @Component public class FaceWsEndpoint { private static final ExecutorService RECOGNIZE_POOL = new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(500), new ThreadPoolExecutor.CallerRunsPolicy() ); private final FaceRecognitionService faceRecognitionService; public FaceWsEndpoint(FaceRecognitionService faceRecognitionService) { this.faceRecognitionService = faceRecognitionService; } @OnOpen public void onOpen(Session session) { session.setMaxTextMessageBufferSize(4 * 1024 * 1024); session.setMaxBinaryMessageBufferSize(4 * 1024 * 1024); } @OnMessage public void onMessage(String base64Data, Session session) throws Exception { String[] parts = base64Data.split(","); byte[] imageBytes = Base64.getDecoder().decode(parts.length > 1 ? parts[1] : parts[0]); RECOGNIZE_POOL.submit(() -> { try { FaceResult result = faceRecognitionService.recognize(imageBytes); session.getBasicRemote().sendText( new ObjectMapper().writeValueAsString(result) ); } catch (Exception e) { // 单帧识别失败不要影响连接,打日志后继续 Thread.currentThread().interrupt(); } }); } }逻辑说明:RECOGNIZE_POOL是核心,它把耗时推理从 WebSocket 线程里剥离出来。CallerRunsPolicy表示队列满了之后由调用方线程自己执行,防止任务无限堆积把内存撑爆。setMaxTextMessageBufferSize设置为 4MB,是为了容纳高分辨率截图推流。base64 数据如果带data:image/jpeg;base64,前缀,要先按逗号拆开再解码。
这里有个容易被忽略的点:前端如果频繁推帧,服务端 Session 可能已经断开,但任务还在线程池里排队。提交任务前应判断session.isOpen(),推送结果前也要再检查一次,否则会漏掉大量IOException异常,连接数也会被异常堆积拖垮。
3.2 OpenCV DNN 检测人脸与关键点坐标
检测部分我用 OpenCV 的 FaceDetectorYN,它读的是 ONNX 格式的 YuNet 模型,对比传统的 Haar Cascade 在侧脸、暗光下的表现好太多,而且输出本身就带五个人脸关键点坐标,方便后面做对齐。模型文件放在resources/models/face_detection_yunet_2023mar.onnx,项目启动时一次性加载到内存,避免每帧都读一次磁盘。
@Service public class FaceDetectionService { private FaceDetectorYN detector; @PostConstruct public void init() throws IOException { InputStream modelStream = getClass().getResourceAsStream("/models/face_detection_yunet_2023mar.onnx"); File modelFile = File.createTempFile("yunet", ".onnx"); Files.copy(modelStream, modelFile.toPath(), StandardCopyOption.REPLACE_EXISTING); detector = FaceDetectorYN.create( modelFile.getAbsolutePath(), "", new Size(320, 320), 0.8f, // scoreThreshold 检测置信度 0.4f, // nmsThreshold 非极大值抑制阈值 5000 // topK 最多保留的人脸数 ); } public Rect detectLargestFace(Mat frame) { Mat faces = new Mat(); detector.setInputSize(new Size(frame.width(), frame.height())); detector.detect(frame, faces); if (faces.rows() == 0) { return null; } float[] face = new float[15]; faces.get(0, 0, face); return new Rect((int) face[0], (int) face[1], (int) face[2], (int) face[3]); } }逻辑说明:detect前必须调用setInputSize传入当前帧的实际尺寸,否则模型会一直按初始化时的 320x320 处理,坐标映射会偏。FaceDetectorYN 输出的每行前 4 个值是 x、y、w、h,后面 10 个值是两眼的四个坐标以及鼻尖、嘴角的关键点,取最大人脸时直接读第一行即可。同一个模型文件用临时文件加载而不是直接加载 InputStream,是因为 OpenCV Java 的FaceDetectorYN.create只接受文件路径,这种写法最省事。
检测到最大人脸框后,要把它裁剪出来并缩放到固定尺寸,再交给下一步特征提取。裁剪时建议在框的四周各向外扩大 20%,把额头和下颚完整包进来,否则特征值会丢失信息,直接影响比对准确率。
3.3 ArcFace 特征提取与参数约束
特征提取部分使用 ONNX Runtime 加载 InsightFace 的 ArcFace 模型,输入统一为 112x112 的 RGB 人脸图,输出是 512 维的特征向量。识别比对时,用待测向量的欧氏距离和底库向量逐一计算距离,距离小于阈值就说明是同一个人。这个思路和 SpringBoot 的业务代码没什么关系,但接入方式值得单独说明:
public FaceResult recognize(byte[] imageBytes) { Mat frame = Imgcodecs.imdecode(new MatOfByte(imageBytes), Imgcodecs.IMREAD_COLOR); Rect faceBox = faceDetectionService.detectLargestFace(frame); if (faceBox == null) { return FaceResult.noFace(); } Mat cropped = new Mat(frame, faceBox); Mat resized = new Mat(); Imgproc.resize(cropped, resized, new Size(112, 112)); float[] feature = faceRecognitionService.extractFeature(resized); double minDistance = faceRepository.findMinDistance(feature); return minDistance < threshold ? FaceResult.hit(minDistance) : FaceResult.unknown(); }逻辑说明:这里的extractFeature内部用 ONNX Runtime 的OrtSession.run执行推理,输入张量格式是[1, 3, 112, 112],也就是把 HWC 转成 CHW 并归一化到 0 到 1 区间。底库查找如果只在几百人规模,用内存 List 遍历 512 维浮点距离完全够用;上万人的底库才需要引入 Faiss 或向量数据库,初期不需要把这个复杂度带进来。
ArcFace 在实际项目里最容易踩的坑是输入预处理不一致:训练时用的是人脸对齐后的 112x112,推理时如果只把裁剪框强行缩放,特征分布会和底库对不上。所以检测出关键点之后,要按两眼角度做旋转校正,再缩放到 112x112,这一步对识别率的影响比换任何模型都大。
人脸识别链路里真正影响成败的参数并不算多,按作用范围整理如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Yunet 检测置信度 | 0.6 到 0.9 | 调高减少误检,调低找回侧脸 |
| 人脸裁剪外扩比例 | 20% | 避免额头和下巴被切掉 |
| ArcFace 输入尺寸 | 112x112 | 模型固定要求,不能随意改大 |
| 识别距离阈值 | 0.45 左右 | 需要按实际摄像头成像调整 |
| OpenCV 线程数 | 等于 CPU 核数 | 过高反而增加线程切换开销 |
以上参数都放在 SpringBoot 的application.yml里通过@ConfigurationProperties注入,后续调优不需要改代码,只需要改配置重启。
4. Vue 前端实现:摄像头调用、抽帧压缩与 WebSocket 推流
4.1 getUserMedia 打开笔记本摄像头并限制分辨率
Vue 端第一步是拿到摄像头权限。调用getUserMedia时最需要注意的一点是:识别场景不要一上来就申请 1080p 的高清流。1080p 对 canvas 抽帧、JPEG 编码和 WebSocket 传输都是无意义的负担,后端模型输入只有几百像素,再清晰也派不上用场。640x480 或 800x600 是实时识别的最佳区间。
async function startCamera () { stream.value = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640, max: 800 }, height: { ideal: 480, max: 600 }, frameRate: { ideal: 15, max: 20 }, facingMode: 'user' }, audio: false }) videoRef.value.srcObject = stream.value await videoRef.value.play() requestAnimationFrame(sendFrame) }逻辑说明:facingMode: 'user'表示强制使用前置摄像头,笔记本场景一般只有一个摄像头,但外接 USB 摄像头时这个约束能让浏览器优先选择正确设备。frameRate.max设置为 20 能避免部分摄像头在弱光下自动把帧率降到个位数,保持推帧节奏稳定。音频必须显式关闭,否则有些机器会弹出麦克风权限请求,用户一旦拒绝,摄像头权限也会受影响。
如果页面上有多个摄像头设备需要切换,要用enumerateDevices()列出所有 videoinput 设备,然后通过deviceId字段指定具体设备,这个字段必须在getUserMedia的 constraints 里传给浏览器。
4.2 canvas 按帧率抽帧并用 toBlob 压缩
video 元素拿到 MediaStream 后,不能直接把整个视频流推给后端,必须用 canvas 把当前画面截成 JPEG。抽帧频率通常控制在每秒 2 到 5 帧,也就是 200 到 500 毫秒一次。交互式实时识别不需要 30 帧,业务上也不要求每帧都出结果,频率太高只会让后端线程池排队,结果推送反而更乱。
let lastSendTime = 0 const FRAME_INTERVAL = 300 function sendFrame (timestamp) { if (timestamp - lastSendTime >= FRAME_INTERVAL && !wsBusy.value) { lastSendTime = timestamp const scale = 320 / videoRef.value.videoWidth canvasRef.value.width = 320 canvasRef.value.height = Math.round(videoRef.value.videoHeight * scale) const ctx = canvasRef.value.getContext('2d') ctx.drawImage(videoRef.value, 0, 0, canvasRef.value.width, canvasRef.value.height) canvasRef.value.toBlob(blob => { if (blob && socket.value && socket.value.readyState === WebSocket.OPEN) { socket.value.send(blob) } }, 'image/jpeg', 0.7) } requestAnimationFrame(sendFrame) }逻辑说明:wsBusy是一个布尔标记,表示上一次推送的结果还没有回来。加这个开关的作用是把帧率从“按时间发”改成“按响应发”,后端处理不过来时前端自动降帧,不会形成消息队列堆积。canvas 缩放系数按 320 宽度计算,这样推到后端的 JPEG 基本在 20KB 以下,比直接发原图画质更合适。toBlob比toDataURL更省内存,前者生成 Blob 直接走二进制 WebSocket,后者会多一次 base64 编码开销。
用requestAnimationFrame控制循环而不是setInterval,是因为浏览器切换到后台标签页时 rAF 会自动暂停,避免页面不可见时还在白白推帧消耗服务器资源。
4.3 网络摄像头的接入:HLS 播放 m3u8 后再抽帧
接入 IP 网络摄像头时,前端不能直接getUserMedia。常见做法是让 SpringBoot 后台用 FFmpeg 把 RTSP 转成 HLS m3u8 流,Vue 页面用 videojs 完成播放,随后从同一个 video 元素里抽帧。这个方案的好处是播放逻辑只用一套代码,笔记本摄像头走 MediaStream,网络摄像头走 URL 播放,抽帧逻辑完全复用。
<template> <video ref="ipCamera" class="video-js" controls autoplay muted></video> </template> <script setup> import videojs from 'video.js' import 'video.js/dist/video-js.css' onMounted(() => { const player = videojs(ipCameraRef.value, { sources: [{ src: '/api/hls/camera01/index.m3u8', type: 'application/x-mpegURL' }], autoplay: true, muted: true }) player.ready(() => { requestAnimationFrame(sendIpCameraFrame) }) }) </script>逻辑说明:muted: true必须设置,浏览器自动播放策略只允许静音视频自动播放,摄像头流没有音频,这个限制不影响识别。前端抽帧时从ipCameraRef.value取视频帧,走的就是和笔记本摄像头完全相同的 canvas toBlob 流程,后端接口也不用区分来源。
RTSP 转 HLS 有一个必须提前接受的现实:即使把切片时间压到最低,整体延迟也会在 1 秒左右,如果业务上要求看见画面后的 200 毫秒内就给出识别框,这条路子行不通。因此,对延迟敏感的场景,我建议在后端直接把 RTSP 解码成 Mat,识别后再把识别结果和画面一起推给前端,这是海康、大华设备接入门禁系统的更可靠方式,虽然实现上多写点代码,但至少不再受播放器缓冲策略制约。
5. 联调验证与实时识别的关键参数调优
5.1 不打开浏览器也能测试的 WebSocket 推帧脚本
前后端联调时,每次都打开摄像头太慢了。我习惯先写一段 Python 脚本,用本地一张图片模拟前端推流,确认后端识别接口通了再回到浏览器调页面。脚本里用 websocket-client 直接推 base64,和 Vue 前端发的数据格式保持一致。
import base64 import json import websocket frame_path = "capture.jpg" with open(frame_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode() ws = websocket.create_connection("ws://127.0.0.1:8080/ws/face", timeout=10) ws.send(json.dumps({"image": image_b64})) result = ws.recv() print(result) ws.close()逻辑说明:如果一个后端能接受 base64 图片并推回识别结果,前端表单和摄像头抽帧的问题就能被隔离排查。脚本返回的 JSON 里如果带faceCount和distance字段,说明检测和特征提取链路没问题;如果返回noFace,问题出在图片质量或检测阈值,和 Vue 无关。
也可以顺带做一个延迟测量,在脚本里记录发送和接收之间的时间差,多次取平均。通常本地单帧识别延迟在 150 到 250 毫秒属于正常范围,超过 500 毫秒就要关注线程池排队和模型推理耗时。
5.2 可复现的实时识别调优参数表
调优的顺序有讲究:先保证识别准确率,再考虑降低延迟,最后再去调并发。一步到位的参数如下:
| 参数 | 推荐值 | 调优说明 |
|---|---|---|
| 前端抽帧间隔 | 300 毫秒 | 降低到 200 毫秒会提升一点平滑度,但 CPU 占用上升明显 |
| JPEG 压缩质量 | 0.7 | 低于 0.5 时人脸纹理丢失,ArcFace 特征漂移明显 |
| 后续帧率限制 | 每秒 3 帧 | 服务端处理不过来时优先降帧保准确 |
| Yunet scoreThreshold | 0.7 | 门禁场景可调到 0.8 减少误报,安防画面可降到 0.6 |
| 线程池核心线程数 | CPU 核数 - 1 | 保留一个核给 JVM GC 和 WebSocket 收发 |
| 线程池最大线程数 | 核心线程数 x 2 | 避免创建过多线程引发 OpenCV 内部竞争 |
实际调整时,前三个参数在 Vue 前端改,后三个在 SpringBoot 的 yml 里改。前端改参数不需要重启,刷新页面就生效,这是排查识别问题时最快的验证手段。
5.3 摄像头异常与识别异常的排查顺序
笔记本摄像头场景最常见的报错是NotReadableError,这表示摄像头被其他进程占用了。排查时先关掉腾讯会议、钉钉、直播软件里的摄像头开关,再到浏览器设置里清除摄像头权限记录,重新请求一次即可。如果getUserMedia始终拿不到流,还要检查访问页面是否走了 HTTPS 或 localhost,浏览器安全策略要求非安全上下文不能使用摄像头接口。
RTSP 网络摄像头如果出现画面黑屏或反复重连,先用 ffprobe 确认地址和编码格式:
ffprobe -rtsp_transport tcp -i "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"重点看输出里的Video: h264还是hevc。如果是 H.265 编码,转 HLS 时必须加 libx264 转码参数,否则前端播放器只会黑屏。海康设备如果想降低资源占用,可以到摄像头后台把主码流分辨率降为 1280x720,编码改成 H.264,节能模式打开。
后端频繁收到连接断开异常时,检查两个地方:一是nacos或网关层的连接空闲超时,二是 Nginx 的proxy_read_timeout。WebSocket 长连接保活要同时设置 TCP 层和 Nginx 层的超时时间,不能只调后端。
6. 把识别做成业务:注册底库、阈值选择与部署前最后一步
6.1 用一次请求完成人脸注册入库
识别用的底库要先有人脸特征。注册接口的输入是一张清晰人脸照片,后端走一遍相同的检测和特征提取,最终把 512 维的 float 数组和用户名绑定存入数据库。最常见的做法是把特征向量序列化成二进制的 BLOB 字段,和 MySQL 或 PostgreSQL 放在一起;几千人的规模下,全表扫描比组件向量数据库简单得多。
@PostMapping("/api/person/register") public Long register(@RequestBody RegisterRequest request) { byte[] imageBytes = Base64.getDecoder().decode(request.getImageBase64()); Mat frame = Imgcodecs.imdecode(new MatOfByte(imageBytes), Imgcodecs.IMREAD_COLOR); Rect faceBox = faceDetectionService.detectLargestFace(frame); if (faceBox == null) { throw new BusinessException("照片中未检测到人脸"); } float[] feature = faceRecognitionService.extractFeature(cropAndAlign(frame, faceBox)); Person person = new Person(); person.setName(request.getName()); person.setFaceFeature(toBytes(feature)); return personRepository.save(person).getId(); }逻辑说明:注册和识别共用extractFeature,保证特征提取的预处理一致。cropAndAlign内部完成关键点对齐和 112x112 缩放,这部分必须和识别链路完全一致,否则注册时提取的特征在识别时距离会被拉大,结果就是同一个人也匹配不上。
6.2 用欧氏距离快速选阈值
特征比对的距离计算用 Python 或 Java 都一样,核心是先算距离,再选阈值。底库建好后,拿同一个人的多张照片计算类内距离分布,再拿不同人的照片计算类间距离分布,阈值应该取两类分布的中间点。
import numpy as np f1 = np.array([0.12, 0.45, 0.86, ...]) f2 = np.array([0.15, 0.42, 0.81, ...]) dist = np.linalg.norm(f1 - f2) print(f"distance: {dist:.4f}") matched = dist < 0.45逻辑说明:阈值的初值用 0.45 是通用做法,但实际必须按摄像头成像质量调整。同一个摄像头下,不同人的特征距离通常分布在 0.8 到 1.4,同一个人的距离分布在 0.2 到 0.5。阈值调大容易把陌生人放进来,调小则导致本人都识别不了。建议把判定接口做成可视化页面,能看到具体距离数值,再根据统计结果调整阈值配置,而不是凭感觉改代码。
6.3 模型文件外部化与绝对路径加载
部署前最后一步是把模型加载路径从 classpath 改成外部绝对路径。SpringBoot 打包成 jar 后,resources里的 ONNX 模型会被打进 jar,每次启动都要解压到临时目录,并且临时目录可能被系统清理。正确做法是增加一个配置项:
face: backend: onnx model-dir: /opt/face-service/models detect-model: face_detection_yunet_2023mar.onnx arcface-model: w600k_r50.onnx启动时优先从外部目录加载模型,文件不存在再回退到 classpath。这么做还有一个隐藏好处:以后换模型只需要替换外部的 onnx 文件再重启应用,不用重新打包整个后端服务。模型加载完成后,用一个最小的自检接口验证整条链路,确保模型文件没有被破坏。
curl -X POST http://localhost:8080/api/face/check \ -H "Content-Type: application/json" \ -d '{"imageBase64": "'"$(base64 -w0 self_test.jpg)"'"}'如果返回"matched": true,整个在线识别系统从摄像头取流到特征比对就已经完整跑通,剩下的工作就是对业务方交付页面和接口对接了。
本文还有配套的精品资源,点击获取