做密集行人检测系统这件事,我前前后后折腾了大半年,才把整套链路真正跑通。从最初只用YOLOv8单模型输出检测框,到后来把YOLOv8/YOLOv10/YOLOv11/YOLOv12的检测能力同时接入系统做横向对比,再叠加SpringBoot后端、前后端分离的Web管理界面,最后把千问和DeepSeek的API接进来做语义分析与异常研判,整个项目才算从"能检测"进化到了"能分析"。这篇文章就把这套系统的完整设计思路和落地过程拆开来讲,包括数据集怎么准备、模型怎么训练、后端怎么对接、前端怎么展示、部署上线会遇到哪些问题,适合正在做目标检测毕业设计、企业级视觉系统,或者打算把大模型API和传统视觉模型做融合的开发者参考。
1. 整体设计与技术选型:为什么是YOLO全家桶加SpringBoot
1.1 多个YOLO版本并存的原因
很多人看到标题里同时出现YOLOv8、v10、v11、v12会问,选一个版本不就行了吗?实际上做密集行人检测这类任务,单一模型很难在所有维度上都表现优秀。密集场景里有三个典型痛点是绕不开的:大量小尺度目标、目标之间严重遮挡、人群分布不均匀带来的密度差异,不同YOLO版本对这三个问题的处理能力有各自的侧重点。
| 模型版本 | 主干核心结构 | 关键特性 | 适合的密集检测场景 |
|---|---|---|---|
| YOLOv8 | C2f + Anchor-Free | 工程生态最成熟,部署资料多,训练稳定 | 作为基准模型和线上主力 |
| YOLOv10 | 无NMS端到端 | 去掉非极大值抑制,后处理延迟低 | 高并发实时流式处理 |
| YOLOv11 | C3k2 + 强化分类头 | 分类精度更高,特征提取更深 | 需要精确区分行人姿态的场景 |
| YOLOv12 | 注意力机制优化 | 引入区域注意力(FlashAttention),全局上下文利用更好 | 人群高度拥挤、遮挡严重的场景 |
我实际测试下来的感受是,YOLOv8在大多数通用监控画面里已经够用,但到了地铁闸机口、商场中庭这种人头攒动的画面,YOLOv12对相互遮挡的行人区分能力明显更强,漏检率能降低四到五个百分点。YOLOv10则因为省去了NMS环节,在同等硬件条件下推理帧率会更高,适合做并发的实时流分析。所以这套系统没有把模型写死,而是做了一个模型管理模块,用户可以按具体场景挑选最合适的版本,后台也可以跑批量对比实验。
1.2 为什么后端选择SpringBoot而不是纯Python
目标检测模型生态基本都在Python侧,很多人在做这类系统时会倾向于直接用FastAPI或Flask把整个服务端写掉。但我的选择是:SpringBoot作为业务中台,Python推理服务单独拆成一个独立部署的推理微服务。原因很简单,这套系统不只是接收图片、返回检测框,它还要包含完整的用户体系、权限管理、历史记录、统计报表、告警规则,这些模块用Java的Spring生态开发效率高得多,集成MyBatis、Redis、定时任务、消息队列这些中间件几乎是无缝衔接。而Python侧更适合专注做模型加载、数据预处理和推理,把边界划清楚以后,两边都不会变得臃肿。
沟通协议上我采用的是HTTP + JSON,推理服务用FastAPI提供REST接口,SpringBoot通过RestTemplate或WebClient调用。虽然gRPC的性能更好,但HTTP的方式在调试和扩展上更灵活,而且FastAPI自带接口文档,前后端联调的时候非常方便。
1.3 千问和DeepSeek在系统里扮演什么角色
检测到了行人只是第一步,用户真正关心的往往是"这个区域的客流密度是否异常""人群聚集有没有潜在安全风险""某个时间段的人流趋势是怎样的"。这些都是语义级别的分析,传统视觉模型做不了,所以我接入了千问和DeepSeek两个大模型API。
分工上我做了明确划分:千问负责结构化报告生成,它把检测框的统计结果—比如人数、密度等级、区域分布、历史变化趋势—整理成条理清晰的文字报告;DeepSeek则承担深度研判,它会结合更复杂的上下文做推理,比如判断某条通道是否出现双向对冲客流风险、是否需要触发限流告警。两者互补,一是避免单一厂商接口不稳定导致整个分析链路瘫痪,二是不同模型的思维风格确实有差异,实测下来DeepSeek在长文本推理上更细致,千问在中文报告表达的流畅度上更好。
2. 密集行人YOLO数据集:来源、格式转换与标注策略
2.1 公开数据集的选型与取舍
模型能不能在密集场景下发威,一半以上的功夫在数据上。做密集行人检测,我优先用的是CrowdHuman和CityPersons这两个公开数据集。CrowdHuman单张图片里的平均人数超过20人,密集遮挡样本非常充足,对训练"在人群中找人"的能力帮助极大。CityPersons则是车上视角的城市场景,尺寸变化大,主打的是透视造成的尺度差异。
另外我也从KITTI数据集里挑选了一部分行人标注数据做补充。KITTI原本是自动驾驶场景的数据集,它的行人目标通常距离较远、尺度较小,这正好补强了系统在远距离小目标上的表现。这里有个要点是:KITTI原始标注是XML格式,YOLO系列需要的是每行一个目标的txt文件,格式转换是绕不开的一步。
转换逻辑核心就是把目标框坐标从左上角和右下角两点表示,换算成YOLO要求的归一化中心点坐标和宽高。KITTI的XML里存的是xmin、ymin、xmax、ymax,假设图片宽为W、高为H,转换公式是:
center_x = ((xmin + xmax) / 2) / W center_y = ((ymin + ymax) / 2) / H box_width = (xmax - xmin) / W box_height = (ymax - ymin) / H转换脚本可以用Python写,遍历所有XML文件,把person类的目标提取出来,生成对应的txt文件,同时把图片路径按训练集和验证集分别写入txt清单。做这一步时要特别注意KITTI的pcd文件路径和图像文件对齐,别把训练样本和验证样本搞混了。
2.2 密集遮挡场景下的标注策略
数据准备的另一个关键点是标注策略。我在处理密集人群时遵循这几条经验:
- 可见面积占比超过50%的目标一定要标,哪怕被严重遮挡,因为这类目标在真实监控画面中非常多。
- 可见面积小于20%且完全被前排遮挡的目标建议不标,强行标记会给训练增加大量噪声,模型反而学不到稳定特征。
- 除了person类,我额外增加了一个head类用于标注人群极度稠密区域的头部目标。头顶位置通常不遮挡,是判断人数的可靠信号,双类别联合训练后,系统在"数人头"这个任务上的准确率明显提升。
- 统一标注尺度。密集数据集中目标尺度差异极大,建议保证最远距离的行人目标高度不低于32像素,否则模型很难学到有效特征。
2.3 数据增强是密集检测的放大器
YOLO框架自带的数据增强策略已经很强大,但针对密集行人我又做了几个针对性增强。Mosaic增强把四张图拼接成一张训练,模型能学到更多的小目标特征;Copy-Paste这种增强方式对遮挡场景很有效,我裁剪出一些行人实例粘贴到人群场景中,相当于手动增加遮挡样本;还有一些随机遮挡和亮度扰动,模拟不同监控设备的光照差异。
增强比例上我的建议是不要开满。Mosaic的概率设在0.5左右,Copy-Paste在0.3左右,过度的增强会让模型在真实数据上的分布偏移更明显,训练集精度很高但验证集掉点,这就过犹不及了。
3. YOLO模型训练与调优:参数细节和实测对比
3.1 环境搭建与训练参数设置
训练环境我用的是一张16GB显存的显卡,如果是6GB到8GB显存的小显卡,适当降低batch size和输入分辨率也可以训练。依赖安装主要就是torch、torchvision和ultralytics三个核心库。这里有个容易忽略的坑:ultralytics不同版本对模型定义文件的兼容性有差异,建议直接安装最新版或锁定一个稳定版本,否则跑YOLOv12这类较新模型时容易遇到算子不兼容的问题。
训练的核心参数我列一下:
yolo detect train \ model=yolov8m.pt \ data=crowdhuman_v2.yaml \ imgsz=1280 \ epochs=120 \ batch=8 \ optimizer=SGD \ lr0=0.01 \ lrf=0.01 \ mosaic=0.5 \ close_mosaic=10 \ cache=True输入分辨率我最终用的是1280而不是默认的640。密集行人检测里大量目标只有几十像素大小,分辨率翻倍之后小目标的召回率提升非常明显,代价是显存占用和训练时间大致翻倍。带上close_mosaic参数,意思是训练最后的10个epoch关闭Mosaic增强,让模型从真实分布上做微调收敛,这个技巧对最终精度的提升很显著。
3.2 损失函数与收敛状态怎么看
YOLOv8及后续版本的损失函数主要由三部分组成:box_loss是边界框回归损失,用的是CIoU或DIoU;cls_loss是分类损失,基于BCE;dfl_loss是分布焦点损失,负责让边界框的回归更加精确。训练时看终端输出的三个loss值,建议重点关注box_loss和dfl_loss的趋势,如果这两个不下降,那模型的对象定位能力就没学好。
密集场景里我体会最深的一个问题是正负样本的平衡。行人目标数量多、尺度小,默认的anchor匹配策略会丢掉不少小目标的正样本。如果训练到中后期发现recall卡住上不去,可以修改模型配置里的anchor匹配阈值,或者直接把输入分辨率再往上提一档。
3.3 四个版本的精度实测对比
训练结束后我在一个包含3000张监控级密集场景图片的验证集上做了对比测试,输入图像统一缩放到1280,置信度阈值设定为0.25,NMS阈值0.45,测试结果大致如下:
| 模型 | mAP@0.5 | mAP@0.5:0.95 | 单帧推理耗时(ms) | 漏检率 |
|---|---|---|---|---|
| YOLOv8m | 0.867 | 0.581 | 18.6 | 13.2% |
| YOLOv10m | 0.872 | 0.594 | 14.1 | 12.5% |
| YOLOv11m | 0.881 | 0.612 | 19.3 | 11.7% |
| YOLOv12m | 0.894 | 0.638 | 24.8 | 9.6% |
从数据可以看出来,YOLOv12在稠密场景下的mAP和漏检率都是最好的,但推理耗时也确实最高。YOLOv10则是速度和精度的折中选择。所以我把模型选择做成了可配置项,标注场景用v12,实时流分析用v10,保证准确率与性能能适配不同业务。
还有一个高频建议是,训练完成后导出模型时考虑使用ONNX格式。ONNX导出后可以脱离PyTorch环境运行,配合ONNX Runtime或者TensorRT推理速度会有明显提升。尤其在生产环境里,我基本都是走PyTorch导出ONNX再加载推理的路径,单帧耗时能压缩三分之一以上。
4. SpringBoot后端与推理服务集成:工程化落地的关键环节
4.1 工程结构与职责划分
后端的核心原则是"业务与推理解耦"。我把整个后端拆成了两个进程:SpringBoot主服务和Python推理服务。假设你是从零开始搭建,SpringBoot工程结构大致这样划分:
- controller层:接收前端请求,做参数校验,统一返回结果,接口里有uploadImage、runDetect、getHistory、getReport这些端点。
- service层:编排业务逻辑,比如调用推理服务、保存记录、调用大模型API、生成分析报告。
- mapper层:通过MyBatis操作MySQL,存储用户信息、检测记录、告警日志。
- client层:封装对Python推理微服务的HTTP调用,以及对千问、DeepSeek API的调用,统一管理超时和重试。
Python推理服务则保持足够简约,只暴露两个接口:/detect接收图片流并返回检测框数组,/models查看当前已经加载的模型列表。启动时把YOLO模型预加载进显存,避免每次请求都现加载模型,否则延迟会高到完全不能用。
4.2 推理接口的数据契约设计
前后端和各个服务之间能顺畅协作,靠的是接口数据契约清晰。我设计的检测返回结构大致如下:
{ "code": 0, "data": { "task_id": "a1b2c3d4", "model_name": "yolov12m", "image_width": 1920, "image_height": 1080, "objects": [ { "class_id": 0, "class_name": "person", "confidence": 0.93, "bbox": [482, 233, 610, 690] }, { "class_id": 1, "class_name": "head", "confidence": 0.88, "bbox": [1204, 387, 1268, 488] } ], "stats": { "total_count": 45, "crowd_density": "high" } } }bbox用[x1, y1, x2, y2]的绝对坐标格式,前端拿到后直接叠加到画布上,免去二次换算。后端只负责透传和记录,不做坐标解析,把计算压力推到前端,这是我在前后端分离项目中比较坚持的一点。
4.3 千问和DeepSeek API接入的实践要点
接入千问和DeepSeek是这套系统的亮点,也是有一定门槛的地方。两个服务到目前为止都提供OpenAI兼容的接口,这意味着你可以用同一个SDK同时调用两家模型,只需要切换base_url和api_key。
调用过程中我遇到的最大问题是大模型输出的稳定性。直接让大模型输出分析结论时,它偶尔会输出一段带格式标记的文本,解析很麻烦。我的解法是强制要求JSON输出,并且在Prompt里定义清晰的输出schema:
{ "density_level": "high", "congestion_points": ["A区域", "B通道"], "risk_suggestion": "建议在A区域增加疏导人员", "summary": "当前人群密度处于高位,主要拥堵点在A区域与B通道", "confidence_reason": "检测到超过40名行人,密度达到阈值0.8" }SpringBoot侧的调用逻辑里必须设置合理的超时时间,我设的是15秒连接超时加30秒读取超时。大模型接口在高并发下偶尔会出现毛刺,重试机制至少要加一次,否则前端很容易因为一次偶发超时直接报错。
4.4 Redis缓存与异步任务
检测任务如果是针对视频流的,会产生大量的重复帧分析请求,不加缓存的话后端压力会非常大。我用了Redis做缓存,以图片内容的哈希值为key,检测结果作为value,缓存时间60秒。同一个画面短时间内反复触发检测时直接命中缓存,大幅降低推理服务的负载。
另外耗时较长的分析流程要扔到异步线程池里处理。比如前端提交一段视频做整体分析,主线程立刻返回一个任务ID,后台线程循环调用推理服务和模型API,完成后把结果写入数据库并通过WebSocket推送通知前端。这种异步模式的体验远好于前端傻等一个长请求。
5. 前后端分离的Web交互界面:从画框到可视化管理
5.1 技术栈与页面模块设计
前端我选的是Vue3加Element Plus组合,状态管理用Pinia,图表用ECharts。整个界面分成几个核心模块:实时检测页支持上传图片、粘贴图片URL,也可以接入RTSP视频流做实时画面展示;历史记录页展示检测记录列表,支持按时间、模型版本、密度等级筛选;统计分析页用ECharts展示各时段的人数趋势折线图、密度热力图和告警占比饼图;数据管理页则直接对应用户上传的数据集和模型状态。
设计界面上我坚持了一个原则:先让用户看到检测框和置信度,再看统计图表,最后才看大模型生成的文字报告,这个视觉动线符合实际操作逻辑。
5.2 WebSocket实时推送检测结果
前后端分离模式下,实时性靠WebSocket而不是HTTP轮询来实现。前端建立WebSocket连接后,上传一张图片,后端推理完成立刻推送检测结果,整个过程体验非常流畅。SpringBoot集成WebSocket的方式比较简单,实现一个WebSocketHandler,在前端连接建立时把session存进内存Map,推理完成后向对应session推送消息。
高并发场景下内存session管理需要特别注意,连接断开时一定要清理Map里对应的session,否则长时间运行后内存会越占越多,最终导致OOM。
5.3 Nginx部署和跨域处理
部署上我采用的是典型的前后端分离方案:前端构建后生成静态文件,由Nginx统一托管,SpringBoot以jar包形式跑在应用服务器上。Nginx配置里最关键的是反向代理,把/api/开头的请求转发到SpringBoot应用,同时配置WebSocket的反向代理支持。这样前后端处于同一个域名下,跨域问题基本杜绝,也顺便解决了开发环境里那种"前端跨域调不到接口"的老大难问题。
服务器部署时的几个要点我踩过坑,在这里提醒一下:项目打包前记得检查后端接口是绑定到0.0.0.0而不是127.0.0.1;Nginx客户端上传大小限制要调大,默认的1MB会让稍微大一点的监控截图直接上传失败;Java应用首次启动时模型预加载需要一段时间,要配置合理的启动超时探活机制。
6. 常见问题与排查技巧实录
6.1 显存不足与推理延迟问题
显存不足是最常见的问题,尤其在训练大分辨率模型时。排查思路很简单:先把batch size调到1,再逐步增加,观察显存占用曲线;训练时开启梯度累积,模拟更大的batch效果;推理阶段如果用TensorRT做模型加速,可以大幅降低显存占用和延迟。如果你用的是AMD显卡,想跑YOLO的话并不强制依赖CUDA生态,可以将模型导出为ONNX格式,然后用ONNX Runtime的DirectML执行提供程序在AMD显卡上运行,实测下来也能获得可用的推理速度,只是训练环节支持度较弱。
6.2 密集场景漏检严重怎么解决
前端反馈漏检问题时,优先从输入侧找原因而不是急着换模型。第一步检查输入图片的分辨率是否过低,至少保持720P以上;第二步检查置信度阈值,密集人群目标相互遮挡时置信度普遍偏低,把阈值从0.5降到0.25能找回大量被过滤掉的目标;第三步才是考虑换模型,大的检测模型配合多尺度推理通常能带来再一轮提升。
6.3 大模型接口调用超时或结果不稳定
千问或DeepSeek接口即便配置了重试机制,偶发超时仍然不可避免。我的降级策略是:主模型调用失败后自动切换备用模型,如果两个模型都失败,系统直接返回基于检测统计的模板化报表,不让分析功能彻底瘫痪。这个降级设计在真实业务中非常实用,用户感知到的只是分析文字风格变了,而不是页面报错无法使用。
另外Prompt设计一定要具体。早期我把检测统计结果直接拼接给大模型,输出的质量时好时坏。后来把所有数值都格式化、单位明确、阈值边界清楚,生成的报告稳定度有了质的提升。要让模型知道置信度0.8和0.3区别巨大,需要把人数密度阈值和每个等级的判定标准明确写进Prompt里。
6.4 训练与推理问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 训练loss不下降 | 学习率过大/数据标签错误 | 降低初始学习率,检查标注文件 |
| 验证集mAP高但漏检多 | 置信度阈值过高 | 降低阈值到0.25左右重测 |
| 推理速度很慢 | 未导出ONNX或TensorRT | 使用ONNX Runtime批量推理 |
| 前端跨域请求失败 | CORS配置或Nginx代理缺失 | 统一域名或配置正确CORS头 |
| WebSocket断连频繁 | 代理未配置升级头 | Nginx配置Upgrade和Connection头 |
| 大模型返回JSON解析失败 | 输出不稳定或超长截断 | 加大max_tokens并做JSON容错解析 |
我在实际项目中还有一个体会比较深的点:整个系统能跑通和能在真实场景稳定运行完全是两回事。初期容易把精力都放在模型的精度对比上,总觉得map再高一个点才是王道,但真正放到生产环境后,最影响体验的反而是SpringBoot服务编排是否健壮、大模型接口是否有完善的降级策略、前端在弱网环境下是否还能流畅展示。如果你打算复刻这套系统,我的建议是先把一条链路闭环,用YOLOv8加千问分析跑通图片检测,再逐步替换模型版本和接入DeepSeek,这个路径走下来会顺很多。模型版本对比的实验数据可以后面慢慢补,但一个能稳定提供服务、界面友好的全栈系统,带给你的价值会远超单点指标的提升。