上手做了个“基于YOLO与SpringBoot的护目镜佩戴检测系统”之后,我才发现这类项目最大的工作量根本不在模型本身。YOLOv8到YOLOv12这一路用下来,检测精度确实一个版本比一个版本稳,但真正让我折腾到凌晨的,反而是数据标注质量、后端接口怎么设计、前端怎么把检测结果变成车间里能用的东西,以及DeepSeek分析结果到底该以什么姿态出现在Web页面上。这篇文章把我从零搭到能演示、能部署、能做二次开发的全过程写下来,包括选型理由、训练参数、前后端联调细节、踩坑记录和排查思路。如果你正准备用YOLO系列做工业安全检测类的Web系统,这篇文章应该能帮你少走不少弯路。
1. 项目背景与需求拆解
1.1 为什么要在生产环境中检测护目镜佩戴
很多工厂车间、实验室、建筑工地,安全规范里白纸黑字写着“必须佩戴护目镜”,但实际执行全靠安全员巡检。人盯人永远有死角,尤其是流水线工位多、人员流动快的场景,靠人工盯根本盯不过来。护目镜佩戴检测系统解决的就是这个问题:用摄像头实时捕捉画面,通过目标检测模型判断画面里的人有没有正确佩戴护目镜,没戴就立即报警,并把记录存下来。
这个需求听起来简单,但落地时有个非常关键的分寸:现场安全监控不是搞人脸识别,不需要知道“你是谁”,只需要知道“这个区域里的人有没有按规定佩戴护目镜”。所以系统的核心逻辑可以抽象成以下三个问题:画面里有没有人、这个人有没有戴护目镜、如果没戴怎么通知和记录。
1.2 需求分析与方案选型
我一开始给自己列的需求清单只有五条:
- 能对摄像头画面(或本地视频、图片)进行人员检测和护目镜佩戴状态判断
- 检测结果要在Web页面上实时展示,标注出佩戴、未佩戴、无人三种状态
- 未佩戴时生成告警记录,支持历史查询
- 能接入DeepSeek做一句自然语言形式的智能分析,比如“今天哪个班次违规最多”“上午10点左右有没有连续违规”
- 系统要前后端分离,方便后续加功能或者换前端框架
围绕这五条需求,技术栈基本是明牌:检测模型选YOLO系列,后端用Spring Boot,前端走Vue,智能分析接DeepSeek的API。这个组合的好处是每一层都足够成熟,出了问题好排查,社区资料也多,不像某些冷门框架出个错得去翻源码。
这里有一个容易被忽略的选型点:为什么用Spring Boot而不是Python后端。检测模型本身是Python生态的,但如果整个系统只用Python写,训练、推理、Web服务全混在一起,后期做权限管理、告警记录、多路摄像头接入会特别别扭。Spring Boot在业务系统上确实是更顺手的工具,而且Java生态做定时任务、消息队列、文件存储都有很成熟的方案。
2. 总体架构设计与技术栈选择
2.1 前后端分离架构
整个系统我分成了三个独立的部分:
| 模块 | 技术选型 | 职责 |
|---|---|---|
| 前端 | Vue 3 + Element Plus + ECharts | 实时画面展示、告警列表、统计分析、DeepSeek对话面板 |
| 后端 | Spring Boot 3.x + MyBatis-Plus | 提供检测结果接口、告警记录管理、调用Python推理服务、对接DeepSeek API |
| 推理服务 | Python + FastAPI + YOLO模型 | 加载模型、执行目标检测、返回检测框和类别信息 |
前后端分离的好处非常明显:前端和后端可以并行开发,接口定义好之后互不阻塞;推理服务的Python部分可以独立重启,不影响Spring Boot业务接口;将来如果要把检测能力接入钉钉、企业微信之类的告警通道,只要在Spring Boot层加逻辑就够了。
这里我特别想强调“推理服务单独拆出来”这个决策。一开始我想过用PyTorch的TorchServe或者直接把YOLO模型导成ONNX后在Java里跑,但实际测试下来都不理想。TorchServe配置偏重,Java端直接用ONNX虽然可行,但前处理、后处理代码要自己写一大段,而且一旦模型版本升级,Java端的预处理逻辑可能要跟着改。把Python推理封装成FastAPI服务,接口只要定义成“传图片路径,返回JSON检测结果”,两边都清爽。
2.2 YOLO多版本对比与选型
YOLOv8、YOLOv10、YOLOv11、YOLOv12这四代模型我都实际跑过,说下真实感受:
- YOLOv8:最稳定,生态最好,文档和教程铺天盖地,出问题基本一搜就有答案。如果不追求极致性能,选它最保险。
- YOLOv10:主打无NMS(非极大值抑制)推理,速度上有优势,但训练时的回调机制和部分参数跟习惯的YOLOv8有差异,新手容易踩坑。
- YOLOv11:在特征提取和检测头上有优化,精度比v8稳,但发布后社区资料比v8少一些,遇到自定义数据集的问题得自己多试。
- YOLOv12:注意力机制加持,小目标检测能力有提升,但成熟度不如前几个版本,我测试时发现同一个数据集下,v12的训练收敛速度确实快,可部署时对GPU的显存要求也更高。
我最终在项目里选了YOLOv11作为主模型,原因很务实:护目镜本身在画面里算中尺寸目标,不需要特别强的小目标能力;v11的精度和推理速度平衡得很好;而且它的接口跟v8、v10基本一致,换模型时迁移成本低。实际上代码里保留了配置项,可以随时切回v8。
YOLO数据集的构建我多提一句:很多人直接拿公开数据集,但护目镜检测这个场景,公开数据集很少,基本都是自己标注。
3. 数据集构建与标注
3.1 数据采集与清洗
护目镜检测的数据集看起来只需要两个类别——戴了和没戴——但实际上手才发现,数据采集要考虑的场景比你想象的多:
- 正常佩戴、护目镜推到额头上、护目镜挂脖子上、手里拿着护目镜、戴着普通眼镜又戴护目镜
- 不同角度:正面、侧面、低头、抬头
- 不同光线:车间强光、暗光、背光
- 不同肤色和面部特征差异
- 部分遮挡:戴口罩、戴安全帽、头发遮挡
我采集数据的渠道主要有三个:一是从车间摄像头截取真实生产画面(脱敏后),二是自己组织人戴不同款式护目镜在不同光线和角度下拍摄,三是从开源图片库里筛选工业场景相关图片。总共攒了大概6000多张有效图片。
清洗数据这一步很多人不重视,但非常关键。我做的事是:去掉重复度极高的连拍帧、去掉画面里人物极小(低于32x32像素)的样本、去掉标注錯誤的样本。因为检测护目镜本质上依赖人脸和眼部区域,如果人脸在画面里占的比例太小,模型根本学不到有效特征。
3.2 标注规范与数据增强
标注规范我踩过一次大的坑。一开始我只标了两个类别:with_goggles和without_goggles。但训练之后发现,模型经常把“手拿护目镜”的人误判成“已佩戴”——因为手部附近有护目镜的轮廓,模型就产生了错误关联。后来我改成三个类别:
- with_goggles:护目镜佩戴在眼部正确位置
- without_goggles:人脸上没有护目镜
- holding_goggles:手里拿着护目镜(作为独立类别,强制模型区分“拿着”和“戴着”)
这个改动让误报率下降了非常多,也是我给所有做安全穿戴检测的朋友第一个建议:不要只标正反两个类别,要把“中间状态”也标注出来,模型学到的是特征边界,不是概念定义。
数据增强方面,因为GPU资源有限,我没有无脑堆增强策略。用了Mosaic、随机旋转(±15度)、HSV微调、随机水平翻转。这里有一个实测规律:随机水平翻转对护目镜检测是有效的,因为人体对称,但要注意标注框会跟着翻转,YOLO训练框架会自动处理,不需要手动干预。
4. 模型训练与优化
4.1 训练配置与过程
我的训练环境是单张RTX 3090,24GB显存。用YOLOv11m(中号模型)作为起点,图片输入尺寸设为640x640,batch size选了16。
训练参数的几个关键选择:
- epochs = 200,但实际用到约150轮时早停了。早停机制在v11里叫patience,设置为30,意思是连续30轮验证集精度不提升就自行终止。
- optimizer选了AdamW,初始lr = 0.001,weight_decay = 0.0005。官方预训练权重是从COCO来的,因为护目镜这类工业小目标在COCO里没有相关知识迁移,所以冻结前10层骨干网络,只微调后面的检测头,这样能显著加快收敛。
- 类别不均衡问题:without_goggles的数量是with_goggles的1.5倍左右,没有做重采样,因为相差不算悬殊,靠loss本身能收敛。但我把cls_loss的权重调到0.7,box_loss设为0.5,让分类任务更容易学到区分。
训练过程里我记录过一个比较典型的现象:前50轮loss下降非常快,但从第70轮开始,验证集的mAP50只在0.93~0.95之间反复横跳,不再明显提升。这说明模型容量基本到极限了。这时候我做了一件很重要的事——检查误检样本,而不是继续加大训练量。发现大量误检集中在“侧脸且光线暗”的画面上,于是用在线视频抽帧补了一批暗光样本重训,mAP50提升到了0.97。
4.2 性能评估与模型导出
评估指标我重点关注三个:
- mAP50:IoU阈值0.5时的平均精度,我的最终模型达到0.97
- mAP50-95:更严格的评估,达到0.82
- 推理延迟:GPU上单张图片平均推理时间约18ms
导出模型时我做了两件事:一是导出为ONNX格式,方便在没有完整PyTorch环境的生产机上用ONNX Runtime做推理;二是保留了原始PyTorch权重文件作为备份。实际上在FastAPI推理服务里我同时支持了.pt和.onnx两种加载方式,默认走.pt,因为接口简单。
有一点值得注意:导出ONNX时,如果用了自定义的类别名映射,一定要统一版本。我在一次部署时把类别顺序调过,结果前端显示的标记全错了,排查了一个小时才发现是导出时映射文件和训练时的class列表不一致。这类问题极其隐蔽,排查建议是:推理服务启动时打印一次类别列表,跟训练日志里的列表做对比,两边一不一致一眼就能看出来。
5. SpringBoot后端与DeepSeek智能分析集成
5.1 后端接口设计
Spring Boot后端在系统里承接三块职责:鉴权和权限管理、检测结果与告警记录的管理、调用推理服务和DeepSeek的聚合层。
核心接口我设计了这样几个:
| 接口 | 方法 | 说明 |
|---|---|---|
| /api/detect | POST | 上传图片,返回检测结果JSON |
| /api/detect/stream | POST | 视频流检测,按帧返回结果(WebSocket实现) |
| /api/alarm/list | GET | 分页查询告警记录 |
| /api/alarm/stats | GET | 统计今日/本周违规次数、按时间聚合 |
| /api/analysis/chat | POST | 接收用户自然语言问题,调用DeepSeek返回分析结果 |
检测结果的数据结构也值得说:我定义了一个DetectionResult对象,字段包括detectionId、imageUrl、detections(列表)、totalPersons、withCount、withoutCount、holdingCount、timestamp。前端拿到这个JSON后,只需要解析detections数组里的每个框的坐标、置信度和类别名,就能直接在canvas上画框。这种设计的好处是前后端完全解耦,不受具体模型格式影响。
http请求推理服务的代码要注意超时设置。推理单张图片一般200ms以内,但如果是视频抽帧,可能一次传10张,就要设置连接超时和读取超时,IO超时我设置为10秒。
5.2 DeepSeek智能分析模块实现
DeepSeek在这个项目里不是核心检测引擎,而是“智能分析大脑”。它做的事情是基于检测记录做自然语言问答,比如安全员输入“今天下午3点到4点,二号线工位的违规情况怎么样”,系统先通过Spring Boot从数据库查出这个时间段的告警记录,再把这些记录作为上下文传给DeepSeek,让DeepSeek生成一段可读的分析文字。
这个设计有个核心原则:DeepSeek不需要直接访问数据库,它只负责“组织语言和总结归纳”。Spring Boot先做数据检索和过滤,把结构化数据拼成Prompt,再调DeepSeek API。这样做的好处是:
- 数据安全可控,数据库连接和查询逻辑不暴露给外部API
- 减少不必要的API调用成本,DeepSeek只需要处理已经筛选好的结果
- 如果DeepSeek接口临时不可用,系统降级返回原始统计数据,不影响主流程
Prompt模板我大概是这么写的:
你是一名安全生产数据分析师。以下是一次护目镜佩戴检测的统计记录: 时间段:{startTime} ~ {endTime} 总检测人数:{total} 佩戴人数:{withCount} 未佩戴人数:{withoutCount} 未佩戴记录:{records} 请用一段简洁的语言总结该时间段的安全状况,指出是否存在问题, 并给出不超过三条改进建议。实际用下来,DeepSeek返回的分析质量取决于Prompt的结构,尤其是“未佩戴记录”里每条记录的时间、工位信息是否完整。如果数据太少,它只会给出泛泛的套话;数据越结构化,它的分析就越有针对性。
6. Web端界面设计与交互实现
6.1 前端页面结构
前端用Vue 3 + Element Plus搭了三块核心页面:
- 实时监控页:左侧是实时视频流或摄像头画面,右侧是检测结果列表,底部是告警滚动条
- 数据分析页:用ECharts展示按小时/天统计的违规趋势、每个工位的违规次数TOP10、佩戴率变化曲线
- 智能问答页:一个类似聊天界面的面板,用户输入问题,后端调DeepSeek返回分析
实时监控页的实现我是用WebSocket推流。后端从摄像头取帧后缩放到合适分辨率,然后通过WebSocket把JPEG图片的Base64字符串推给前端。前端用Image对象接收并绘制到Canvas上,再把YOLO检测框叠加绘制。这里要注意格式:如果直接把原图推过去,带宽扛不住,我做了两层优化,一是把帧率限制在10~15fps,二是把图片质量压缩到80%。
Canvas叠加框的时候有个细节:不能直接用CSS的img元素上画框,因为没法定位到具体坐标。正确做法是用Canvas同时画底图和画框,底图是视频帧,框是检测结果。代码逻辑大概是:收到一帧数据后,先绘制图片,再遍历detections数组,用strokeRect绘制矩形,用fillText绘制类别标签。
6.2 实时检测流程与数据可视化
前端的工作流程是一条清晰的链路:
- WebSocket连接建立后,后端按固定间隔推送视频帧
- 前端每收到一帧,把ImageData绘制到Canvas
- 前端把当前帧发送给推理服务(或者是后端推理后在WebSocket消息里带上检测结果,我选择了前者)
- 推理服务返回JSON,前端更新标注框、统计面板和告警列表
这里有一个常见的轮询与推流选择问题。一开始我试过前端定时轮询后端接口拿检测结果,但发现视频画面和检测结果有几百毫秒的错位,体验很差。后来改成后端通过WebSocket统一推“画面+检测结果”的组合消息,前端一次性渲染,效果好了很多。
数据可视化方面,我实现了三个图表:违规时段分布(折线图)、工位违规TOP10(横向柱状图)、近7天佩戴率变化(面积图)。这些图表的底层数据都来自告警记录表的聚合查询,用MyBatis-Plus的GroupBy Query就能搞定,不需要额外引入复杂框架。
还有一个值得说的点:告警记录入库时,我除了存常规信息,还存了一个thumbUrl字段,就是违规瞬间截图的缩略图路径。这样做的好处是后期查看告警记录时可以直接在列表里看到当时的画面,不用再回放视频找证据。安全问题是这个字段别直接暴露文件路径给前端,应该走一个后端的文件访问接口,做权限控制。
7. 常见问题与排查技巧
7.1 模型检测效果不理想的故障排查
做这种检测系统,最糟心的就是模型精度还可以,但一放到现场就各种误检漏检。我梳理了三个最常遇到的问题:
误报比较多:未佩戴的误报原因大概率是数据集里缺少“手拿护目镜”或“护目镜挂脖子”的样本。解决办法是回到数据集,而不是调模型参数。
漏检比较多:如果画面里人很多、间距紧,漏检往往是因为NMS阈值设置太高或者输入分辨率太低。我测试发现,把推理时的imgsz从640提高到960后,小目标漏检率下降明显,但推理时间从18ms涨到55ms。取舍要看现场摄像头是固定视角还是移动视角,固定视角可以适当提高分辨率。
特定角度一直检测不出来:这种情况几乎都是因为训练数据里该角度占比太少。我的建议是统计标注数据里每个角度的分布情况,如果侧面样本不足,就专门拍侧面补数据,单纯靠增强旋转角度解决不了所有问题。
7.2 前后端联调与部署问题
跨域问题:Spring Boot和Vue开发环境下端口不同,CORS配置必须正确设置。我用的方式是在Spring Boot里加一个配置类,允许前端所在域名的跨域请求。生产环境则通过Nginx反向代理把前后端映射到同一个域名下,彻底规避跨域问题。
WebSocket连接频繁断开:排查下来,是Nginx默认的proxy_read_timeout只有60秒,WebSocket超过60秒没有心跳就断了。解决办法是在Nginx配置里把proxy_read_timeout调大到120秒,并在前端加一个心跳检测,每30秒发一次ping消息。
推理服务内存溢出:Python推理服务如果在跑一段时间后内存持续增长,大概率是Dataset类的缓存机制或者视频帧没有及时释放。我排查后发现问题出在FastAPI里没有显式关闭已读取的图像文件句柄,加上内存中保存了太多历史帧的引用。解决办法是每次处理完帧后手动调用gc.collect(),并限制处理队列的长度。
训练显存不足:batch size可以调小,但更推荐使用梯度累积(gradient_accumulation)来等效达到大的batch size效果。V11里设置accumulate参数为2,就相当于用原来一半的显存跑了两倍的batch size。
写在最后
这个护目镜检测系统从数据采集到最终部署,前后用了一个多月,真正写代码的时间其实只有两周,剩下全在跟数据和模型效果较劲。我个人最深刻的一个体会是:YOLO系列的模型能力已经远远超出了很多场景的实际需求,瓶颈从来不在“能不能检测出来”,而在“你给模型喂了什么数据”和“系统设计得够不够顺手”。数据标注时那一点点“多标一个中间状态”的决定,带来的误报率下降比调任何模型参数都明显。Spring Boot和Python推理服务的分离,也让我在后期迭代时几乎没有为架构操过心。如果你也打算做类似的安全穿戴检测系统,这里有一句我踩坑之后攒下来的话:先把数据标注规范定到极致,再谈模型调优和Web展示,顺序反了,后面全是返工。最后提醒一个部署细节:生产环境千万把推理服务和工作目录分开,模型文件和日志输出路径别放在系统盘临时目录里,不然后续维护起来你会很想摔键盘。希望这篇记录能帮你少走几段弯路,有更好的方案也欢迎交流。