基于飞腾派的YOLOv10n实时安全帽检测系统

1. 项目背景

建筑工地、矿山和隧道等作业环境存在坠物、碰撞等安全风险,安全帽是最基础的个体防护装备。传统人工巡检容易受到监控画面数量、光照条件和人员注意力的限制,因此本项目希望在资源受限的国产 ARM 开发板上实现视频安全帽检测,对连续出现的未佩戴安全帽行为进行告警。

高危作业环境示意图

系统需要同时解决以下问题:

  1. 飞腾派算力和内存有限,原始模型直接部署时延较高;
  2. 夜间或隧道内照度不足,安全帽与背景的特征对比度下降;
  3. 解码、增强、推理和绘制串行执行时,各阶段会相互等待;
  4. 单帧漏检、误检或短时遮挡可能导致告警抖动;
  5. OpenCV、业务线程和 ncnn 内部线程若同时过度并行,会在四核处理器上产生资源竞争。

2. 硬件与软件平台

系统部署在飞腾派开发板上。处理器包含 2 个 FTC664 核和 2 个 FTC310 核,属于大小核异构 ARMv8 架构。FTC664 适合承担计算密集型的神经网络推理,FTC310 可承担视频解码、轻量预处理和后处理等任务。

飞腾派开发板

软件栈主要包括:

  • Ubuntu 20.04;
  • C++17、CMake 和 GCC/G++;
  • OpenCV:视频解码、图像变换、结果绘制;
  • ncnn:YOLOv10n FP32/INT8 推理;
  • ARM NEON Intrinsics:加速 LIME 中的逐像素运算;
  • Qt:后续接入可视化界面,核心检测程序本身不依赖 Qt。

CPU 编号不能仅凭“0、1 是小核,2、3 是大核”直接写死。部署时需要结合 /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freqlscpu -e 和实际压测确认核编号,再传给程序的绑核参数。

3. 系统总体设计

本系统不再把视频切成多个片段后由多个进程分别处理,而是针对连续视频流建立三级异步流水线。这样既保留了视频时序,也避免了分段、临时文件和重新合并带来的额外开销。

1
2
3
4
5
6
7
flowchart LR
A["视频文件或网络视频流"] --> B["线程1:解码、亮度判断、LIME、letterbox<br/>小核"]
B --> Q1["有界队列 Q1<br/>PreprocessedFrame"]
Q1 --> C["线程2:ncnn 推理<br/>业务线程绑定双大核"]
C --> Q2["有界队列 Q2<br/>InferenceFrame"]
Q2 --> D["线程3:坐标恢复、NMS、状态机、绘制<br/>小核"]
D --> E["实时显示、结果视频或告警"]

这里的“线程2绑定双大核”包含两层含义:推理业务线程的 CPU affinity 允许它在两个大核上运行;同时设置 net.opt.num_threads = 2,由 ncnn 在线程内部把可并行算子分配给两个工作线程。它不是把相邻两帧分别交给两个大核推理,而是让一帧模型计算在两个核上并行执行。

4. YOLOv10n安全帽检测模型

4.1 类别与数据集

训练数据至少区分 helmetno_helmet 两类。如果数据集同时标注 person,还可以通过人员框与安全帽框的空间关系判断佩戴状态;如果模型只检测 no_helmet,则直接把该类别作为违规事件输入多帧状态机。

数据划分应按视频或场景划分,而不是把同一视频的连续帧随机分散到训练集和验证集,否则相邻帧高度相似,会造成指标虚高。低照度、遮挡、侧脸、小目标和多人密集场景应在训练集与校准集中保留足够样本。

4.2 通道结构化剪枝

模型压缩采用 L1 范数作为通道重要性指标。对于卷积层第 i 个输出通道,其重要性可以写为:

Sᵢ = ‖Wᵢ‖₁ = Σⱼ |Wᵢⱼ|

其中,Wᵢ 表示第 i 个输出通道的卷积核权重,Σⱼ 表示对该通道中的全部权重求和。

权重绝对值之和较小的通道通常对输出贡献较弱。项目使用 torch-pruning 的依赖图执行真实的通道裁剪,并同步修改后续卷积、BatchNorm 和残差连接的相关维度,而不是只把一部分权重置零。只有模型文件中的张量形状和真实参数量下降,才算结构化剪枝。

实际流程为:

  1. 从 10%、15%、20%、25% 等比例分别试验,不预设 25% 一定最优;
  2. 分多步裁剪,每一步后执行一次前向检查,及时发现通道依赖错误;
  3. 通道数按 8 对齐,兼顾 ARM SIMD 和 ncnn 内核实现;
  4. 记录剪枝前后参数量、MACs、mAP50、Recall 以及板端时延;
  5. 根据安全帽漏检率和板端速度选择最终模型,而不是只看压缩率。

项目中的主要脚本为:

  • model_tools/prune_structured.py:构建依赖图并裁剪通道;
  • model_tools/finetune_pruned.py:微调剪枝模型并保存最佳权重;
  • model_tools/export_pruned.py:将微调后的模型导出为 ncnn 格式。

4.3 小学习率、AMP与AdamW微调

剪枝会破坏原模型已经形成的特征分布,因此需要使用较小学习率继续训练,让保留下来的通道重新适应任务。AdamW 将权重衰减与梯度更新解耦,有助于控制参数规模;AMP 混合精度在 GPU 训练时让适合的算子使用 FP16,同时对容易溢出的部分保留 FP32,从而降低显存并提高训练吞吐。AMP 是训练优化手段,不代表导出的板端模型自动变成 INT8。

微调阶段重点关注 no_helmet 类别的 Recall。安全场景中漏掉真正的违规人员通常比多报一次更严重,因此不能只用总体 mAP 判断剪枝是否可接受。

4.4 ncnn INT8量化与KL校准

剪枝模型先导出并验证 ncnn FP32 结果,确认输入归一化、输出节点和检测框一致后,再进行后训练量化:

1
ncnnoptimize → ncnn2table(KL校准)→ ncnn2int8

KL 校准会收集代表性图片在各层产生的激活分布,尝试不同截断阈值,并选择让原始分布与量化后分布 KL 散度较小的阈值。阈值太大时量化步长粗、分辨率不足;阈值太小时又会截掉大量离群激活。KL 方法在动态范围与量化误差之间寻找平衡。

校准集必须与部署输入一致,包括:

  • 白天、夜间和隧道低照度画面;
  • 佩戴、未佩戴、小目标、遮挡和多人场景;
  • 系统实际采用的 resize、letterbox、颜色顺序和归一化;
  • 若线上会开启 LIME,校准集中也要包含经过同一 LIME 参数处理的图片。

量化完成后应分别评估 PyTorch FP32、ncnn FP32 和 ncnn INT8。不能把导出误差与 INT8 误差混在一起分析。

5. 轻量化LIME低照度增强

5.1 基本原理

LIME 基于 Retinex 思想,把观测图像 I 表示为反射分量 R 与照明分量 T 的乘积:

I = R ⊙ T

如果估计出照明图 T,就可以通过 R = I / (T + ε) 恢复较明亮的图像。考虑板端实时性,本项目采用轻量化流程:

  1. 对每个像素取 B、G、R 三通道最大值得到初始照明图;
  2. 使用盒式滤波平滑照明图,减少局部噪声和光晕;
  3. 对照明图做 Gamma 调整;
  4. 用调整后的照明图对原图逐通道恢复并裁剪到合法范围。

LIME处理流程参考图

Gamma 调整作用于照明图而不是直接作用于检测结果。对于归一化后的 T ∈ [0, 1],使用 T′ = T^γ。当 0 < γ < 1 时,中低灰度照明值被适当抬升,使恢复分母更稳定,减轻暗区过度放大、噪声和色偏。Gamma 不是越小越好,需要结合检测精度和主观画质标定。

为了避免正常光照下无意义地增强,程序使用平均亮度阈值和迟滞开关:亮度低于开启阈值时启用 LIME,高于关闭阈值时关闭。两个阈值不同可以防止画面亮度在临界点附近波动时频繁切换。

5.2 ARM NEON向量化

当前手写 NEON 覆盖两个计算密集且容易向量化的部分:

  1. BGR 三通道最大值亮度估计;
  2. 图像恢复阶段的浮点除法、上下界裁剪和写回。

亮度估计使用 vld3q_u8 一次解交错加载 16 个 BGR 像素,再通过 vmaxq_u8 计算三通道最大值,最后使用 vst1q_u8 写回。恢复阶段使用 vld1q_f32、倒数估计与牛顿迭代、vmulq_f32vminq_f32vmaxq_f32 并行处理 4 个浮点数。图像宽度不是向量宽度整数倍时,末尾像素回退到标量循环,避免越界访问。

盒式滤波与 Gamma 幂运算目前仍由 OpenCV 的 cv::boxFiltercv::pow 完成。因此准确表述应是“对轻量化 LIME 的核心逐像素运算进行 NEON 向量化”,而不是“LIME 所有阶段均为手写 NEON”。

低照度增强效果参考图

6. C++与ncnn板端推理

6.1 视频逐帧读取

预处理线程通过 OpenCV cv::VideoCapture 打开本地视频或可解码的视频流,在循环中调用 read(frame) 获取下一帧。read 等价于完成抓取和解码,不需要摄像头采集模块。读取完成后记录帧编号、视频时间戳和解码时间,再根据运行模式进入后续流水线。

6.2 letterbox与坐标恢复

模型通常要求固定输入尺寸,而视频帧宽高比并不固定。letterbox 先按比例缩放图像,再在上下或左右补边,避免直接拉伸导致目标形状变形。预处理时保存缩放比例 scale 与补边量 pad_left/pad_top,后处理时通过:

x = (x′ − padₓ) / scale,y = (y′ − padᵧ) / scale

把模型输入坐标映射回原始视频坐标,并裁剪到图像边界。

6.3 输出解析与NMS

运行时支持 N×6=[x1,y1,x2,y2,score,class_id] 和单类别 N×5 输出。若导出的 ncnn 图已经包含 NMS,则关闭板端 NMS;若输出仍包含多个候选框,则按类别执行 NMS。即使画面中通常只有一个人,模型仍可能针对同一目标产生多个候选框,因此是否需要 NMS 应由实际导出图决定,而不是由“最终只显示一个框”决定。

7. 多线程流水线与线程通信

7.1 三个长期工作线程

程序启动后创建三个长期存在的线程:

  • 预处理线程:视频解码、亮度判断、LIME 和 letterbox;
  • 推理线程:构造 ncnn 输入、执行模型并解析输出;
  • 后处理线程:坐标恢复、NMS、状态更新、绘制与输出。

线程之间传递的是带有帧号和时间戳的数据包。cv::Mat 使用引用计数并通过移动语义传递,避免每跨越一个线程就复制整帧图像。

7.2 有界队列与背压

预处理到推理、推理到后处理之间各放置一个固定容量的线程安全队列。队列内部使用互斥锁保护容器,并通过条件变量通知“非空”或“未满”状态。

系统提供两种策略:

  • 实时模式:队列满时删除最旧帧,再放入最新帧。这样允许丢帧,但限制端到端延迟持续增长;
  • 离线模式:队列满时生产者阻塞,等消费者取走数据后再继续,保证逐帧完整处理。

这就是背压:下游处理不及时的时候,上游不能无限制生产。它既限制内存占用,也让实时模式优先保证结果的新鲜度。

7.3 CPU affinity与ncnn内部线程

Linux 下通过 pthread_setaffinity_np 将不同业务线程限制在指定 CPU 集合。程序参数示例为:

1
--pre-cores 0 --infer-cores 2,3 --post-cores 1

以上编号只是示例,必须按开发板实测结果调整。推理线程允许在两个大核上运行,同时:

1
net.opt.num_threads = 2;

让 ncnn 的算子工作线程利用两个核。OpenCV 业务侧可设置 cv::setNumThreads(1),防止 OpenCV、ncnn 和三个业务线程各自创建大量线程,导致四核系统运行队列变长、缓存反复失效和上下文切换增加。

8. 多帧未佩戴安全帽告警状态机

单帧检测框不应直接触发持续告警。本项目复用原多帧状态机框架,把“跌倒阳性”改为“检测到 no_helmet 类别”,状态改为:

1
2
3
4
5
6
7
stateDiagram-v2
[*] --> NORMAL
NORMAL --> SUSPECTED: 滑动窗口内违规帧达到阈值
SUSPECTED --> VIOLATION_CONFIRMED: 持续时间达到确认阈值
SUSPECTED --> NORMAL: 违规票数不足
VIOLATION_CONFIRMED --> COOLDOWN: 告警已输出
COOLDOWN --> NORMAL: 冷却结束且未再检测到违规

例如在最近 5 帧中至少 3 帧检测到 no_helmet 才进入疑似状态,持续超过设定时间后确认告警。冷却时间可以避免同一个人连续触发大量重复告警。对于多人场景,正式版本还应加入跟踪 ID,使状态机按人员维护,而不是只维护全局状态。

代码迁移时需要把 FallStateMachinefall_class_id 和界面中的 FALL_CONFIRMED 等命名改为 HelmetViolationStateMachineviolation_class_idVIOLATION_CONFIRMED。状态转移逻辑本身可以复用。

9. 可视化界面

Qt 界面可分为原始视频区和检测结果区,并提供视频选择、开始/暂停、低照度增强开关、检测开关和告警列表。为了避免 UI 线程被模型阻塞,后处理线程只发出图像和状态信号,界面更新由 Qt 主线程完成。

UI界面参考图1

UI界面参考图2

上图用于展示界面布局来源。最终博客应换成新程序截图,并在界面中把旧模型名称、类别标签和功能按钮更新为 YOLOv10n 安全帽检测版本。

10. 构建与运行示例

在飞腾派上构建 C++ 核心:

1
2
3
4
5
cd deploy_cpp
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-Dncnn_DIR=/path/to/ncnn/lib/cmake/ncnn
cmake --build build -j2

安全帽检测版本的运行参数可以设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./build/helmet_detect \
--video input.mp4 \
--param models/helmet_pruned_int8.param \
--bin models/helmet_pruned_int8.bin \
--input-blob in0 \
--output-blob out0 \
--schema nx6 \
--classes 2 \
--violation-class 1 \
--pre-cores 0 \
--infer-cores 2,3 \
--post-cores 1 \
--lime neon \
--mode realtime \
--display

当前代码中的可执行文件名和参数仍是跌倒检测命名,因此上面的 helmet_detect--violation-class 是安全帽版本的目标接口。若尚未完成重命名,应使用现有 fall_detect--fall-class,但在博客正式发布前最好完成语义改造。

11. 系统展示

下面两张图片用于展示参考项目的设备连接与运行形式。新项目发布时,应补拍自己的飞腾派连接方式、终端性能日志和安全帽检测界面。

设备连接参考图

系统运行参考图

安全帽检测效果图建议至少准备三组:

  1. 正常光照下佩戴与未佩戴安全帽的检测结果;
  2. 相同低照度视频在关闭和开启 LIME 时的对比;
  3. 连续视频中的 SUSPECTED → VIOLATION_CONFIRMED → COOLDOWN 状态变化。

参考项目的检测效果图如下,仅用于说明最终博客所需的排版形式,不能作为本文新模型的实验结果:

安全帽检测结果排版参考

12. 项目创新点

  1. 模型与部署协同压缩:通过依赖图完成 YOLOv10n 真实通道结构化剪枝,并在 ncnn 中采用 KL 校准完成 INT8 部署,兼顾模型体积、推理开销和违规类别召回率。
  2. 面向 ARM 的低照度优化:实现轻量化 LIME,并对亮度估计和图像恢复等逐像素热点进行 NEON 向量化,改善暗光输入的同时控制预处理时延。
  3. 面向异构四核的流水线调度:将解码预处理、ncnn 推理与后处理拆分为三个长期线程,使用有界队列形成背压,并通过 CPU affinity 与 ncnn 内部线程数控制减少核间竞争。
  4. 从单帧检测到事件告警:使用滑动窗口、持续时间确认和冷却机制抑制单帧误报;后续结合目标跟踪可进一步形成逐人员的安全帽违规事件。