直接进入正题。做工地安全监控的同行应该都体会过:现场几十路摄像头,网络忽好忽坏,云端检测再下发告警,延迟高、视频流量大,一年下来流量费都心疼。我这次做的这个“边缘AI视频分析引擎”,核心就一句话——把安全帽检测模型部署到 RK3588 这块开发板上,让它直接在摄像头旁边完成推理,本地出结果、本地触发告警。整套架构用 YOLOv8 做检测模型,跑的是安全帽佩戴识别这个高频场景,从数据集准备、模型训练,到 RKNN 转换、NPU 推理、多路视频接入,全部走了一遍,踩了不少坑,记录下整个拆解过程,给同样在搞边缘 AI 项目的人一个参考。
无论你是刚入行的算法工程师,还是在工地上做集成落地的人,这篇文章都会有点用。
1. 项目定位:把安全帽检测从云端搬到工地现场
1.1 云端推理的延迟、带宽与隐私痛点
最开始我也想过用云端方案:摄像头 RTSP 推流到服务器,后端跑 YOLOv8,检测完之后把告警结果推给平台。单看技术链路,这套方案很成熟,做 demo 一天就能跑通。但往实际工地上一放,问题就全出来了。
第一是延迟。视频先要经过编码、打包、推流、云端解码、推理,一轮下来至少一两秒,甚至更久。安全帽检测这个场景,工人要从“没戴帽子”状态走进危险区域,到告警触发,最好控制在几百毫秒内,云端模式做不到。
第二是带宽。一路 1080p 视频,H.264 编码后大概需要 2 到 4 Mbps 上行带宽,一个工地几十路摄像头,上行带宽根本扛不住。而且很多工地现场网络很差,稍微偏远一点,4G 信号不稳定,视频流经常卡住。
第三是成本。视频存储和流量费用是长期开销,云服务器 GPU 实例一个月几千块,这还不算网络专线费用。边缘端一台 RK3588 设备,一次性投入几百到一千多块,功耗只有十几瓦,电费几乎可以忽略。
我在项目启动前专门整理过一组对比数据,结论很直接:
| 对比项 | 云端推理方案 | RK3588 边缘方案 |
|---|---|---|
| 单路识别延迟 | 1~3 秒 | 100 毫秒左右 |
| 上行带宽需求 | 高,受限于现场网络 | 仅需传输告警结果 |
| 稳定性 | 断网即失效 | 本地运行,断网不影响 |
| 长期成本 | GPU 实例 + 流量费 | 一次性硬件成本 + 低功耗 |
| 隐私安全 | 视频需传输到外部 | 视频不出现场 |
这个表其实就回答了一个问题:为什么要把 AI 放到边缘端。边缘智能不是新鲜概念,但直到 RK3588 这种“高性价比 + 高算力”的芯片出现,安全帽检测这类实时分析任务才算真正适合落地。
1.2 为什么选 RK3588 而不是 Jetson 或 ESP32
选型阶段我对比过几个方案:NVIDIA Jetson Orin Nano、RK3588、还有 ESP32 这类 MCU 方案。Jetson 生态确实成熟,但价格高,而且芯片供货周期不稳定,最关键的是 Jetson 的功耗对于工地这种环境来说偏高,还需要主动散热。ESP32 大家最近聊得很多,但说实话,ESP32 更适合做简单的人体感应、语音唤醒这类轻量 AI,6 TOPS 的 NPU 和 ESP32 完全不是一个量级,拿它跑 YOLOv8 实时检测不现实。
RK3588 是瑞芯微的旗舰 SoC,几个关键参数我列一下:
- CPU:4 核 Cortex-A76(2.4GHz)+ 4 核 Cortex-A55(1.8GHz)
- GPU:Mali-G610 MP4
- NPU:3 核,6 TOPS INT8 算力
- 编解码:支持 8K 解码、多路 1080p 视频硬解码
- 内存:支持 LPDDR4/4x/5
这块芯片最大的优势是“够用且便宜”。安全帽检测属于单目标检测任务,模型不用很大,YOLOv8n 或 YOLOv8s 就能跑得不错。在 INT8 量化之后,RK3588 的 NPU 跑 YOLOv8s,一帧 640×640 的推理时间能做到 40 毫秒左右,完全满足实时需求。再加上它硬件解码能力强,多路 RTSP 视频流可以靠 RKMPP 硬解处理,不会把 CPU 打满。
在芯片选型上我的建议是:不要盲目追 Jetson,也不要因为 ESP32 便宜就硬上。先评估算力需求、路数需求、供电散热条件,RK3588 在这个量级的边缘视频分析项目里是性价比最均衡的选择。
2. 整体架构与核心模块拆解
2.1 视频流入到告警出的完整链路
先给大家画一下这个引擎的逻辑链路,我按照模块拆开发。整个系统分成四个层:采集层、推理层、业务层、输出层。
采集层负责取流和解码。工地的 IPC 摄像头输出 RTSP 流,通过 FFmpeg 拉流,利用 RK3588 的硬件解码器得到 YUV 帧,再转成 RGB。推理层是关键:对帧做 letterbox 缩放,送到 RKNN 加载的 YOLOv8 INT8 模型里,NPU 推理输出原始结果,然后在 CPU 上完成置信度过滤和 NMS 后处理,得到目标框、类别、置信度。业务层根据检测结果判断是否“未戴安全帽”,如果连续多帧都命中,就触发告警。输出层负责推送告警,同时把检测结果叠加到视频帧上,推流给监控大屏。
这个架构看起来简单,但每一层都有各种坑。拉流断线重连、解码帧率控制、NPU 与 CPU 的负载均衡、后处理时间优化,任何一个环节出问题都会导致整个引擎不稳定。后面我会一个个讲。
2.2 RK3588 算力评估与 YOLOv8 模型选型
算力评估这件事,我做了一个简单的换算。RK3588 NPU 标称 6 TOPS,这里的单位是 INT8 稠密算力。YOLOv8s 在 640×640 输入下大概需要 20 到 30 GFLOPs 的计算量。理论上限是 6000 GFLOPs 除以 25 GFLOPs,也就是 240 FPS 左右。但这是理想值,真实场景有内存带宽、算子利用率、CPU 后处理开销等因素,实测单路推理 25 到 45 FPS 是合理的。
模型选型我做了两个版本对比。第一版直接用 YOLOv8n,优点是推理极快,INT8 之后能做到 20 毫秒以内,但小目标的检测能力弱一些。安全帽在监控画面里通常只占几十像素,YOLOv8n 容易出现漏检。第二版换成 YOLOv8s,mAP 明显提升,推理时间在 35 到 45 毫秒,仍然在实时范围内。最终我选了 YOLOv8s,因为安全帽检测这种场景,漏检比延迟更致命。
关于“安全帽改进 yolo”这条线,我也尝试过。网上有人给 YOLOv8 加注意力机制模块,比如在 C2f 里嵌入 SE、CBAM,或者在 Neck 部分换 BiFPN,对小目标有帮助,但换来的代价是模型复杂度上升,NPU 推理时间增加。我的建议是:先用标准 YOLOv8s 把完整流程跑通,确认效果后再考虑改进结构,不要一开始就上魔改模型,否则 RKNN 转换时算子不支持的问题会把你折磨疯。
2.3 工具链选型:RKNN-Toolkit2 与运行时
RK3588 的模型部署有一套独立工具链,核心是 RKNN-Toolkit2。这个工具在 PC 上运行,做模型转换、量化、导出 RKNN 格式;板子上通过 RKNN Runtime 或者轻量版 RKNNLite 加载 RKNN 模型执行推理。
工具链的大致流程是:PyTorch 训练 YOLOv8 → 导出 ONNX → RKNN-Toolkit2 做 ONNX 转 RKNN(可含量化) → 部署到板子 → RKNNLite 推理。这套流程本身不复杂,但细节很多。比如 ONNX 导出的算子版本、是否做简化、量化数据集如何准备、转换时 target_platform 参数怎么填,都会影响最终效果。
我强烈建议在 PC 上建一个独立的 conda 环境来装 RKNN-Toolkit2,避免和训练环境互相污染。转换的时候,板子和 PC 最好同网段,有些调试功能需要联调。
3. 数据集准备与模型训练实践
3.1 安全帽数据集怎么凑、怎么标、怎么划分
安全帽检测本质上是目标检测任务,数据是决定效果上限的第一要素。公开数据集方面,SHWD(Safety Helmet Wearing Dataset)是绕不开的选择,里面包含戴帽子的头部和未戴帽子的头部两类标注,图像数量大几千张,覆盖工地常见场景。不过 SHWD 的标注是用更早的格式做的,需要转成 YOLO 格式才能喂给 Ultralytics。还有一个常用的是“Hard Hat Workers”数据集,里面是工人作业的实拍图,质量也不错。
只用公开数据集是不够的。工地现场的光照、摄像头角度、扬尘环境、安全帽颜色,和数据集里的差异很大,模型很容易出现“训练集里能检测、现场效果稀烂”的问题。我在项目里做了一个补充采集:用现场摄像头录了 3 小时视频,抽帧筛出大概 1500 张有效图像,用 LabelImg 手动标注。标注时遵循一个原则:安全帽区域画目标的头戴区域,不是画整个人。这样模型学习到的特征更聚焦,部署后也不会因为半身遮挡导致检测失败。
数据集划分遵循 8:1:1 的比例,我习惯用固定随机种子划分,保证每次训练验证集一致,方便对比实验。训练前还要做一次数据检查,重点看有没有漏标、错标、重复标注。这部分很耗时,但值得做,脏数据对模型精度的影响超过你的想象。
3.2 训练配置与卡点排查
训练硬件我用的是 GTX 1660 Ti,6GB 显存,属于性价比最低但最容易买到的一类卡。跑 YOLOv8s,imgsz=640,batch size 设 8 或 16 都能训。如果显存不够,优先减 batch size,而不是减输入分辨率,因为检测目标本身就小,输入分辨率一降,小目标特征就没了。
关键训练参数我列一下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 640 | 保持与部署端一致 |
| batch | 8~16 | 显存决定,好卡可以更大 |
| epochs | 100~150 | 结合早停策略 |
| optimizer | SGD 或 AdamW | 数据量中等用 SGD 稳 |
| 预训练权重 | yolov8s.pt | 迁移学习,收敛快 |
| 数据增强 | mosaic + hsv | 增强泛化,mosaic 对小目标有效 |
训练启动后,我习惯用 ultralytics 自带的训练输出观察趋势。它不是只给你一个最终 mAP,而是每轮输出 loss、精度、召回率、mAP50、mAP50-95 这些指标,同时生成 results.png 曲线图。如果发现 val 的 mAP 在长时间不动,大概率是学习率设置问题,可以调低 lr0。如果训练剧烈震荡,优先降低 lr 或调大 warmup_epochs。
这里有一个我踩过的坑:直接用默认的 mosaic 增强训练安全帽数据集,发现后期收敛变慢,因为 mosaic 拼接后的图像中目标尺寸变得更小,对本来就小的安全帽不利。我的处理是在最后 20 个 epoch 关闭 mosaic,让模型在接近真实分布的数据上微调。Ultralytics 支持在训练中期关闭部分增强,这个特性帮了大忙。把损失函数画出来对比,关闭 mosaic 后 loss 曲线明显更平滑,最终 mAP50 从 0.82 提升到 0.89 左右。
3.3 从损失曲线判断模型状态
损失曲线是判断训练是否正常的核心依据。YOLOv8 的损失分为三部分:box_loss(目标框损失)、cls_loss(分类损失)、dfl_loss(分布焦点损失)。train 阶段和 val 阶段各有一套曲线。
正常的训练曲线应该是:box_loss 从高值稳步下降,后期趋于平缓;cls_loss 下降速度快于 box_loss;val 曲线大体跟随 train 下降,如果两者差距拉开,说明过拟合风险出现。如果 val 曲线在某个 epoch 突然掉头向上,而 train 还在降,那就是过拟合信号,可以提前用早停,或者检查数据有无泄露。
我还自己做了一个额外的检查:在训练完成后,把验证集上置信度最低的那批预测结果可视化出来,看模型到底漏检了哪些图像。很多时候你会发现漏检的都集中在安全帽颜色与背景接近、远处小目标、人物侧身走路这三类场景。针对这些样本做数据增强和补充标注,比盲目换模型结构有效得多。
4. RK3588 端完整部署流程
4.1 导出 ONNX 的正确姿势
YOLOv8 训练完成后,导出 ONNX 是部署的第一步。Ultralytics 提供了导出接口,直接调用就行:
from ultralytics import YOLO model = YOLO("best.pt") model.export(format="onnx", imgsz=640, opset=12, simplify=True)这段代码看起来简单,但有几个细节直接影响后面的 RKNN 转换。第一是 opset 版本,RKNN-Toolkit2 对高版本算子的支持不一定完备,实测 opset=12 最稳,不必刻意追求高版本。第二是 simplify 参数,它调用 onnxsim 对模型结构做精简,能去掉很多冗余算子,转 RKNN 时成功率更高。第三是输出尺寸,YOLOv8 导出的 ONNX 有三组输出,对应三个尺度,每组是 1×(4+类别数)×(预测框数量),在 640 输入下总计 8400 个候选框。后面后处理部分要按这个格式写解析函数。
导出成功后用 Netron 把 ONNX 的结构可视化一遍,重点确认输出节点的名字,RKNN 转换时会用到。如果你导出的 ONNX 输出名混乱,可能在 RKNN-Toolkit2 里解析不到,这是个容易踩的小坑。
4.2 转 RKNN 与 INT8 量化
转换这一步是部署链路里最容易出问题的环节。先在 PC 上装好 rknn-toolkit2,然后写转换脚本:
from rknn.api import RKNN rknn = RKNN() # 配置:target_platform 对应 RK3588 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform="rk3588", ) # 加载 ONNX rknn.load_onnx(model="yolov8s_helmet.onnx") # 构建模型,do_quantization=True 表示 INT8 量化 rknn.build(do_quantization=True, dataset="dataset.txt") # 导出 RKNN 文件 rknn.export_rknn("helmet_det.rknn")这里说明两点。第一,mean_values 和 std_values 设置的是输入预处理参数,推理时输入的图像也要按相同方式归一化,很多人在板端推理时忘记这一层导致效果全崩。第二,INT8 量化需要一个校准数据集,dataset.txt 里逐行写图片路径,至少准备一两百张有代表性的图像,覆盖不同光照、角度、目标大小。这个校准集影响非常大,如果校准集太偏,量化后模型精度会掉得很离谱。
量化之后,建议在 PC 上用模拟器先跑一遍推理,确认输出和 PyTorch 版本一致。模拟器速度慢,但能让你在部署前发现 80% 的问题。模拟器通过的时候,再上板实测。
4.3 端侧推理代码与后处理
板端推理我用的是 RKNNLite,它是 RKNN Runtime 的轻量版,占用资源更小,适合在正式产品里使用。推理核心代码:
from rknnlite.api import RKNNLite rknn = RKNNLite() ret = rknn.load_rknn("helmet_det.rknn") ret = rknn.init_runtime() # 输入帧预处理(与训练时一致:letterbox 到 640x640) img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = letterbox(img, (640, 640)) # 推理,输入维度是 [1, 3, 640, 640] outputs = rknn.inference(inputs=[img]) # outputs 为列表,包含三个尺度的检测输出 # 后处理:解析坐标 + 置信度过滤 + NMS后处理代码是 CPU 端完成的。YOLOv8 的输出不是像 YOLOv5 那样有 objectness,而是直接在预测维度里输出类别得分,所以要先把每个候选取置信度阈值过滤,再做 NMS。这里我建议优先复用 Ultralytics 的推理与后处理逻辑,把 PyTorch 版本替换成 NumPy 版本,逻辑完全一致,避免自己从零写导致坐标系混乱。坐标输出的格式是归一化的中心点加宽高,最终要反算回原图坐标,别忘了 letterbox 前后的缩放与偏移还原。
一个更省事的方案是部署时直接把输入图像一次性做归一化,再把 output 里的坐标换算回原图。实测下来后处理一帧的时间大约 5 到 8 毫秒,对整体延迟影响不大。
5. 性能优化与多路视频接入
5.1 单路性能实测与瓶颈定位
先上一组我在板子上实测的数据。YOLOv8s,INT8 量化,640×640 输入,单次 NPU 推理时间约 38 到 42 毫秒。加上预处理的 resize 和 letterbox、后处理 NMS、画框渲染,单路整体端到端延迟能控制在 80 到 100 毫秒以内。
在这个基础上做性能优化,最先要判断瓶颈在哪。用top看 CPU 占用,如果 CPU 占用很高而 NPU 利用率不高,说明预处理或后处理拖了后腿。预处理部分可以用 RKNN 支持的量表输入,尽量把颜色转换和 resize 放在 NPU 侧或 GPU 侧完成。后处理部分可以用矩阵批量计算代替 Python 循环,避免耗时集中在 NMS。
我实际做的优化是:输入分辨率从 640×640 降到 416×416。对安全帽这种中等大小的目标,416 分辨率在近距离监控下效果下降不明显,但 NPU 推理时间能压到 25 毫秒左右。如果你的现场摄像头距离远、目标小,不建议降分辨率,宁可上 YOLOv8m。这里要结合自己的摄像头布局来定,没有万能参数。
5.2 多路视频流处理
多路接入是这个项目从 demo 走向可用的关键一步。单路推理 30 多毫秒,理论上 NPU 可以处理多路,但视频解码和预处理会成为新的瓶颈。我的做法是每路视频一个采集线程,用队列缓存帧,一组工作线程负责取帧、推理和结果回写,避免线程之间互相阻塞。
我实测过 RK3588 接 4 路 1080p RTSP 视频流,每路独立线程解码,配合 RKNNLite 分别加载模型实例,CPU 总占用率大约 60% 到 70%,NPU 是主要的占用者。如果接入路数更多,比如 8 路,建议把检测帧率降到每路 10 FPS 到 15 FPS,搭一个“抽帧检测”模式,而不是每帧都检测,依然能覆盖安全帽检测的实际需求。
多路并发的稳定性坑点是:初始化多个 RKNNLite 实例时要注意 core_mask 参数的分配。RK3588 的 NPU 有三个核心,可以通过 core_mask 指定单核、双核或全核运行。多路场景下,不要让每个实例都抢全核,否则会出现推理延迟抖动。合理方式是每个实例绑定独立的 NPU 核,或者按路数动态分配。
5.3 误报抑制与业务联动
检测模型的输出不能直接当告警用。工地现场的树叶晃动、人影重叠、摄像机抖动都可能造成误报。我做了一个简单的状态机:同一路视频连续 3 帧检测到“未戴安全帽”,并且置信度都超过 0.5,才触发告警;出现一帧误检就清零重新累计。这个机制能过滤掉 90% 以上的偶发误报。
告警触发后的业务联动有几种方式。最简单的是在板子上用 OpenCV 把检测框画到视频帧上,然后推 RTSP 流给监控平台。更实用的是通过 HTTP POST 把告警快照和坐标发给后端平台,或者用 MQTT 推给消息总线,让现场声光报警设备联动。我在项目里用 MQTT 做告警推送,协议轻、断线重连机制成熟,后端只需要订阅一个主题就能收到所有告警事件。
6. 问题排查与避坑实录
6.1 RKNN 转换失败的典型原因
我在 RKNN 转换阶段遇到最多的问题,是 ONNX 模型里含有 RKNN-Toolkit2 不支持的算子。YOLOv8 本身结构通用,正常情况下能直接转换,但如果做过改进,加了自定义模块,很容易出现算子兼容问题。
排查思路分三步:第一,看报错信息里是哪个算子不支持,用 Netron 定位到对应层;第二,尝试升级 rknn-toolkit2 到最新版本,新版支持的算子更多;第三,实在不支持,就在导出 ONNX 时把那个分支拆掉,或者在 PyTorch 侧改写结构,替换成等价算子。实测下来,sqrt、pow、部分广播操作容易出问题,可以用乘法和加法替代。
还有一类问题是 ONNX 导出的模型带有动态维度,RKNN 转换要求固定输入形状。导出时务必指定 imgsz=640,不要用动态尺寸导出,否则 RKNN 的 build 阶段会报错。
6.2 量化后掉点怎么办
INT8 量化是必选项,因为 FP16 在 RK3588 NPU 上没有推理速度优势,而且 INT8 能从算力上真正发挥硬件性能。但量化带来的精度损失,不同模型差异很大。安全帽检测中有大量的小目标,量化后容易出现“边界框偏移一点、置信度下降明显”的情况。
应对量化掉点,我的经验是优先做这几件事:
第一,扩大量化校准集。校准集要覆盖暗光、过曝、远距离、安全帽颜色与背景相近的图像,不要只选看起来很清晰的图。校准集规模从 100 张提到 300 张后,我的模型 mAP 回升了大约 3 个百分点。第二,检查转换时batch设置和batch_size参数,RKNN-Toolkit2 的量化过程一次处理的图片越多,统计越稳定。第三,考虑混合量化。RKNN 支持指定某些层保持 FP16,优先把最后几层检测头保持高精度,因为检测头对数值变化最敏感。把检测头单独设置为 FP16 之后,小目标漏检率下降明显,推理时间只增加几毫秒,可以接受。
6.3 检测效果不行的工程解法
部署完模型后现场测试效果拉胯,先不要急着改模型结构,按这个顺序排查:第一,确认输入预处理与训练时一致,letterbox 的填充颜色、归一化方式,差一点都可能导致效果大幅劣化。第二,确认 NMS 阈值和置信度阈值是否合理,安全帽检测场景置信度阈值不宜设太高,0.3 到 0.4 更合适。第三,检查摄像头安装位置,安全帽目标在画面里低于 30 像素时,再强的模型也难稳定检出,这种情况下要从机位调整入手。
我遇到过最隐蔽的问题,是 RKNN 推理输入图像是 NCHW,而我在板端用 OpenCV 读图后直接以 HWC 传入,结果图像内容完全错位。排查了很久才发现是数据布局问题。建议在板端初始化 RKNN 后,先用一张已知标签的测试图跑推理验证一次,确认输出坐标与预期一致再接入视频流,这个“冒烟测试”能省下大量调试时间。
另外,关于“改进 YOLOv8 以提升安全帽检测效果”,我在最后阶段尝试过给检测头加一个 P2 小目标层,对小目标召回率确实有帮助,但代价是模型计算量明显增加,在 RK3588 上推理时间从 40 毫秒涨到 70 毫秒,同时 RKNN 转换时部分算子不兼容,需要额外适配。个人体会是:先靠数据和部署细节把效果做到 90 分,最后那 5 分再用结构改进去换,性价比更高。
7. 项目复盘与个人体会
整套引擎从零到跑通,我最想说的不是模型精度或代码实现,而是工程化思维的转变。模型在 PC 上跑得好,只是万里长征第一步;真正让你熬夜的永远是 RKNN 转换、量化精度、多线程并发这些工程问题。把这些问题一个个解决掉,才能算真正交付了一个能用的边缘 AI 视频分析引擎。
如果只从这次项目中提炼一条经验,那就是部署导向的模型设计从一开始就要做:训练时用与部署端一致的分辨率,避免后期迁移;模型结构尽量保持算子主流,减少自定义模块;数据集里刻意加入现场采集的困难样本,比单纯堆训练轮数有用得多。按这个思路走,RK3588 加 YOLOv8 的组合,足够撑起工地上 90% 的安全帽检测需求。