☰
树莓派4B部署YOLOv5-Lite:NCNN量化与实时目标检测实战
2026/10/21 7:35:07 网站建设 项目流程

简介:面向树莓派4B的YOLOv5-Lite目标检测部署资源包,专为在低算力边缘设备上运行轻量级YOLO模型的开发者与学习者设计。包内共7个文件,包含4个Python脚本、2个不同规模的PyTorch预训练权重文件以及1个适配ARMv7l架构的onnxruntime安装包。Python脚本覆盖了LabelMe标注转YOLO格式、标签映射字典、单图像检测与视频流实时推理等完整流程,配合权重文件可在树莓派上直接运行,实现端侧本地目标检测,无需云端参与。两个权重版本在精度与速度之间提供了平衡选项,可根据实际场景选用。整个资源包仅7.91MB,部署门槛低,非常适合用于边缘计算、物联网视觉项目或深度学习模型轻量化实验,也便于在课程设计或竞赛中快速上手。目前已有9548人学习下载,对想要掌握YOLOv5-Lite落地方法、优化模型适配低性能设备的读者,这份资源提供了可直接运行的代码与模型,能有效缩短环境搭建和调试周期,具有很好的实践参考价值。

1. 基于树莓派4B的YOLOv5-Lite目标检测:轻量化模型落地的完整参考

当你在树莓派4B这种算力有限(CPU 4核Cortex-A72,内存4GB/8GB)的板子上跑标准YOLOv5s,OpenCV推理一张640×640的图能吃掉2-3秒,视频流基本是幻灯片效果。但换上经过通道剪枝和结构重参数化压缩的YOLOv5-Lite,配合NCNN这种为ARM平台做过汇编级优化的推理后端,单帧延迟能压到100ms以内,视频流跑到12FPS以上是可行的。这份资源把环境配置、模型转换、量化、部署的全过程做成了一套nginx可复现的工程,面向的是想在边缘设备上做实时检测的开发者,以及那些毕业论文或实训项目需要「在嵌入式设备上跑深度学习」这个亮点的学生。它不解决算法创新问题,解决的是把YOLOv5-Lite从服务器搬到嵌入式设备这一路的所有坑。

2. 硬件选型与整体架构:为什么是4B而不是树莓派Zero或Jetson Nano

2.1 树莓派4B的算力边界与选型逻辑

树莓派4B的CPU是BCM2711四核Cortex-A72,主频1.5GHz(可通过config.txt稳定超频到1.8GHz),内存带宽在LPDDR4-3200下理论值约3.9GB/s。对比Jetson Nano的128个CUDA核心,树莓派没有GPU加速能力,所有深度学习推理都压在CPU上。因此选型时就要明确:树莓派4B适合跑轻量级模型,不适合跑大模型。

YOLOv5-Lite相比标准YOLOv5s做了三件事:把Focus层换成普通卷积以降低计算量、通过通道剪枝将特征层通道数压缩、用结构重参数化在推理时把多分支合并成单路卷积。这使得模型的FLOPs比YOLOv5s降低约4倍(从16.5 GFLOPs降到4.2 GFLOPs左右),参数量从7.2M降到约1.2M,为在树莓派上做实时推理提供了前提。

模型选型的核心考量在于:精度与速度的平衡。yolov5_lite_v2.1_s(最小版本在COCO上mAP约40%),在这个资源里作为默认模型,它能在树莓派4B CPU上达到单帧推理80-120ms(NCNN 2线程),如果是更小的v2.1_nano版本能到60-80ms。如果你对精度要求高,就换yolov5_lite_v2.1_m;如果对速度要求极致,就用nano版本。这份资源默认v2.1_s,属于中间档位。

2.2 整体架构:从摄像头采集到结果显示的完整链路

这套部署架构可以拆成4个层次:图像采集层、数据预处理层、推理引擎层、结果可视化层。图像采集层用的是OpenCV的VideoCapture,树莓派Camera Module接口的CSI摄像头需要先通过raspistill或libcamera初始化,USB摄像头则直接走V4L2;数据预处理层负责把摄像头帧做letterbox变换(保持宽高比放入640×640画布)和RGB通道重排;推理引擎层是NCNN封装好的模型加载、前向推理、后处理解码;结果可视化层负责把检测框、类别标签、FPS数值画到帧上输出到HDMI屏幕或者通过网络推流。

资源里给了一份文件清单,其中核心部分包括:预训练权重文件(pt格式和onnx格式各一份)、NCNN转换脚本、量化用的校准图片集、以及完整的C++推理Demo源码。C++推理Demo是部署的主程序,Python版本的脚本只用于模型转换和调试,因为Python在树莓派上做逐帧推理存在GIL锁和解释型开销,实测帧率损失约30%。

整体代码结构是这样的:

raspberrypi-yolov5-lite/ ├── convert_pt_to_onnx.py # PyTorch模型转ONNX ├── convert_onnx_to_ncnn.py # ONNX转NCNN格式 ├── quantize_ncnn.py # NCNN模型量化脚本 ├── deploy/ │ ├── CMakeLists.txt # C++工程构建配置 │ ├── main.cpp # 推理主程序入口 │ ├── yolo_layer.cpp # 后处理解码实现 │ ├── yolo_layer.h │ ├── letterbox.cpp # 图像预处理 │ └── camera_capture.cpp # 摄像头采集封装 ├── calib_images/ # 量化校准图片集(约200张) ├── models/ # 转换后的ncnn模型存放目录 └── README.md # 部署说明文档

这份结构的价值在于把转换、量化、部署三个阶段完全解耦,你可以在PC上完成前两步(转换和量化),只把最终生成的ncnn格式模型拷贝到树莓派上。这样树莓派上不需要安装PyTorch,节省2GB以上的磁盘空间和内存占用,也避免了因为ARM平台不兼容某些x86的Python包而踩坑。

2.3 推理引擎的取舍:为什么推荐NCNN而不是OpenCV DNN或TFLite

在树莓派4B上有多种推理后端可选:

  • OpenCV DNN:支持ONNX格式,部署最简单,但CPU上的底层优化依赖OpenCV自身实现,实测fps只有NCNN的50%-60%
  • TFLite:需要先把模型从ONNX转成TFLite格式,算子支持度有限,YOLOv5的Focus层在某些版本下转换会直接报错
  • NCNN:腾讯开源的前向推理框架,针对ARM架构做了Neon汇编级优化,支持INT8量化,在树莓派上效果最好

资源选NCNN有两个关键原因:一是NCNN的模型转换工具链成熟,onnx2ncnn工具能处理YOLOv5系列的大部分算子,包括SiLU激活函数;二是NCNN的线程调度在ARM Cortex A72上的cache亲和性优化比OpenCV做得好。实测结果:在4核环境下,NCNN开2线程比开4线程FPS更高,因为树莓派4B的内存带宽有限,4线程反而因为缓存争抢导致性能下降。这份踩坑细节在使用文档里有专门说明,属于跑通项目时需要特别关注的调参点。

3. 核心模块拆解:一个视频帧从进入到出现检测框的完整链路

3.1 图像预处理:letterbox不是简单的resize

常见的错误做法是直接调用OpenCV的cv2.resize或C++的cv::resize把图像变成640×640,这样会拉伸图像导致目标形变,检测精度会明显下降。NCNN推理时输入模型的是方形图,但原图不一定是方形(摄像头通常是16:9或4:3),正确做法是letterbox策略:保持原始宽高比,缩放后填充边缘到目标尺寸。

C++部署代码里对letterbox的封装如下:

void letterbox(const cv::Mat& src, cv::Mat& dst, int target_size) { int h = src.rows, w = src.cols; float ratio = std::min(static_cast<float>(target_size) / w, static_cast<float>(target_size) / h); int new_w = static_cast<int>(w * ratio); int new_h = static_cast<int>(h * ratio); // 缩放到目标宽高(保持宽高比) cv::Mat resized; cv::resize(src, resized, cv::Size(new_w, new_h), 0, 0, cv::INTER_LINEAR); // 计算pad偏移 int pad_w = (target_size - new_w) / 2; int pad_h = (target_size - new_h) / 2; // 填充灰色边(128,128,128) cv::copyMakeBorder(resized, dst, pad_h, pad_h, pad_w, pad_w, cv::BORDER_CONSTANT, cv::Scalar(128, 128, 128)); }

这段代码的核心逻辑是先计算缩放比例,然后通过copyMakeBorder在边缘填充。注意ratio的计算用的是min而不是max,这样能保证图像完全放入640×640画布且不超出边界。pad_w和pad_h用于计算填充偏移量,后续后处理解码时,需要把这些偏移量反向映射回原图坐标,否则检测框位置会错位。

关于填充值的选择,用128(灰色)而不是0(黑色),因为模型训练时数据处理是从COCO数据集样本学来的,COCO图片letterbox后边缘区域是自然图像的灰度分布,灰色比黑色的过杀效应更小。图像在进入NCNN推理前,还需要从BGR顺序转为RGB(NCNN的默认输入顺序是RGB),并且除以255归一化到[0,1]区间。这些操作在C++里要手动逐通道遍历像素完成,不能用OpenCV的cvtColor走捷径,因为cvtColor不可控的框方式会引入额外耗时。

3.2 NCNN推理引擎的初始化与线程配置

NCNN推理引擎的初始化在main.cpp里有固定的模板,这里直接看关键代码:

#include "net.h" // 全局网络对象,初始化一次复用多次 ncnn::Net yolo_net; int init_yolo_model(const std::string& param_path, const std::string& bin_path) { // 清空网络实例,避免重复加载 yolo_net.clear(); // load_param加载模型结构文件(.param,文本格式) // load_model加载模型权重文件(.bin,二进制格式) int ret = yolo_net.load_param(param_path.c_str()); if (ret != 0) { fprintf(stderr, "load param failed: %d\n", ret); return -1; } ret = yolo_net.load_model(bin_path.c_str()); if (ret != 0) { fprintf(stderr, "load model failed: %d\n", ret); return -1; } // 关键配置:设置推理线程数 // 树莓派4B上推荐2线程,4线程反而更慢 yolo_net.opt.num_threads = 2; // 开启light模式,合并算子,减少内存开销 yolo_net.opt.lightmode = true; // 输入是float32格式 yolo_net.opt.use_fp16_storage = false; // 量化模型需要开启use_int8_inference // yolo_net.opt.use_int8_inference = true; // 如果加载的是int8量化模型 return 0; }

这里解释一下几个参数的含义。num_threads默认不做设置时,NCNN会拿满所有核心,但在树莓派4B上用4线程实测会发现CPU占用100%但FPS反而低于2线程,原因前面提到过:内存带宽瓶颈。lightmode为true时,NCNN会做算子融合优化,比如把卷积+BN+ReLU合并成单个算子,这在推理时能减少内存读写次数,是NCNN运行效率的关键开关。use_fp16_storage在Cortex-A72上建议设false,因为fp16的half类型在A72上并没有硬件加速单元,反而要做额外的转换开销。

这些配置项不是一个黑匣子,改完后可以通过打印每层耗时来确认影响:

// 在推理时打印每层耗时,用于性能分析 yolo_net.opt.use_winograd_convolution = true; // 推理完成后统计耗时 double start = ncnn::get_current_time(); ncnn::Extractor ex = yolo_net.create_extractor(); // ... 设置输入执行推理 double end = ncnn::get_current_time(); fprintf(stderr, "infer time: %.2fms\n", end - start);

如果发现某层占用时间异常,可以在使用文档里找到故障排查的说明,常见原因是模型没有经过优化转换,或者网络结构在转换时丢失了关键算子信息。

3.3 YOLOv5-Lite后处理:解码候选框到最终输出

模型的前向推理输出是原始张量,拿到的是三个尺度的特征图(大中小各一组),直接看这些数字没有意义,需要解码后处理。YOLOv5-Lite的输出结构是每个尺度输出一个张量,形状为[batch, anchor_num, grid_h, grid_w, 5 + num_classes],其中5代表x、y、w、h和objectness置信度。

C++里yolo_layer.cpp实现了解码的核心逻辑,简化版如下:

void decode_yolo_output(const ncnn::Mat& output, int grid_h, int grid_w, int num_classes, float conf_thresh, std::vector<Object>& objects) { // 遍历网格中的每个单元格 for (int h = 0; h < grid_h; h++) { for (int w = 0; w < grid_w; w++) { // 每个位置有3个anchor for (int a = 0; a < 3; a++) { // 从输出张量中提取该anchor的5+num_classes个通道值 const float* ptr = output.channel_range(a * (5 + num_classes), 5 + num_classes); float obj_conf = ptr[4]; if (obj_conf < conf_thresh) continue; // 置信度滤波 // 找到类别中得分最高的 int class_id = 0; float max_score = 0.0f; for (int c = 0; c < num_classes; c++) { float score = ptr[5 + c] * obj_conf; if (score > max_score) { max_score = score; class_id = c; } } if (max_score < conf_thresh) continue; // 解码框坐标(相对网格单位),需要乘以stride映射回原图 float cx = (ptr[0] + w) * stride; // 中心x坐标 float cy = (ptr[1] + h) * stride; // 中心y坐标 float bw = ptr[2] * stride; // 宽度 float bh = ptr[3] * stride; // 高度 // 把坐标转换为yolo输出的框格式(left, top, right, bottom) Object obj; obj.rect.x = cx - bw / 2.0f; obj.rect.y = cy - bh / 2.0f; obj.rect.width = bw; obj.rect.height = bh; obj.label = class_id; obj.prob = max_score; objects.push_back(obj); } } } }

这段逻辑里最关键的是坐标映射关系:YOLO的输出是相对网格偏移量(sigmoid归一化后加上网格坐标),乘以stride(该特征图相对原图的缩放步长)才能得到640×640尺度下的像素坐标。三个尺度的stride分别为8、16、32,对应特征图尺寸80×80、40×40、20×20。解码完成后还需要把检测框从640×640的letterbox坐标映射回原始帧坐标,映射公式在文档里有明确说明。

最终结果需要做NMS(非极大值抑制)去除重复框,这里的NMS用的是NCNN自带的nms_sorted_bboxes函数,在C++里调用即可,不需要自己实现。需要注意NMS的IoU阈值默认设0.45,在树莓派这种算力有线的场景下,调高到0.55能略微减少计算量,但代价是相邻目标可能被合并,需要根据你的实际摄像头场景测试。

4. 模型转换与量化:从PyTorch到NCNN的完整链路

4.1 PT转ONNX:torch.onnx.export的操作细节

模型转换的第一步是把PyTorch训练好的pt权重导出为ONNX格式。这个资源里提供了Python脚本convert_pt_to_onnx.py,但它的使用前提是你有一个能跑PyTorch的环境。在没有GPU的情况下,CPU也能完成ONNX导出,只是这一步通常耗时几分钟,不涉及推理。

导出的核心代码一般在项目脚本基础上修改而来:

import torch import sys # 加载自定义的yolov5 lite模型结构 from models.yolo_lite import YOLOv5Lite def export_onnx(): # 实例化模型并加载权重 model = YOLOv5Lite(num_classes=80) # COCO数据集80类 # 加载pt权重,注意需要去掉前面前缀,strip_optimizer处理 ckpt = torch.load('yolov5_lite_v2.1_s.pt', map_location='cpu') model.load_state_dict(ckpt['model'].float().state_dict()) model.eval() # 固定输入尺寸为640x640 dummy_input = torch.randn(1, 3, 640, 640) # 导出ONNX,opset_version要选11,高版本可能引入不兼容算子 torch.onnx.export( model, # 模型 dummy_input, # 示例输入 "yolov5_lite_v2.1_s.onnx", opset_version=11, # ONNX算子版本 input_names=['images'], # 输入节点名 output_names=['output'], # 输出节点名 dynamic_axes=None, # 固定尺寸,不动态 do_constant_folding=True # 常量折叠优化 ) print("ONNX导出完成: yolov5_lite_v2.1_s.onnx")

在这段代码里需要注意的参数是opset_version=11和dynamic_axes=None。opset_version用11而不是更高的13或17,是因为NCNN的onnx2ncnn工具对opset 11的算子支持最完备,高版本ONNX里的一些新算子(比如GatherElements的高维版)在转换时容易报不支持。dynamic_axes设置为None表示输入是固定尺寸640×640,如果设置成动态尺寸,虽然灵活但生成的ONNX会包含Resize和Shape等动态算子,NCNN转换时又要引入额外的支持代码,没有必要。

导出后的ONNX文件可以用Netron可视化检查,正常应该看到Focus层被拆成Slice、Concat等基础算子,SiLU激活函数替换成Sigmoid+Mul的组合。如果你看到某些奇怪的算子(比如TFOpLambda),说明导出过程有问题,需要回退到opset 11重新导出。

4.2 ONNX转NCNN:命令行操作与常见报错处理

ONNX转NCNN使用NCNN自带的onnx2ncnn命令行工具,在编译好的NCNN工具目录里。具体命令如下:

# 进入NCNN工具目录 cd ncnn/build/tools/onnx/ # onnx2ncnn命令转换 ./onnx2ncnn yolov5_lite_v2.1_s.onnx yolov5_lite_v2.1_s.param yolov5_lite_v2.1_s.bin

转换过程会在终端输出每一层的信息,包括算子类型、输入输出张量形状。正常结束的标志是打印"onnx2ncnn finish"没有红色报错。如果你遇到如下这类报错:

Unsupported operator: Focus

这是因为Focus层在onnx导出时没有被解析成基础算子。解决办法是回退到源码的models文件夹检查导出逻辑,或者改用opset 11统一处理。如果遇到Unsupported operator: HardSwish之类的错误,需要回到PyTorch侧,把模型中的HardSwish激活改为SiLU(两者数学上等价),然后重新导出。

另一个常见报错是Resize的坐标变换模式不支持,报错信息类似Unsupported coordinate_transformation_mode: asymmetric。解决方案是在导出时设置opset 11,并且注意PyTorch的F.interpolate默认align_corners参数保持False,这样导出后NCNN能正确处理。

转换成功后,还会生成一个.param和.bin文件对,其中param是可读的文本,bin是二进制权重。如果你需要部署到不同平台,param和bin是配套的,不能单独替换其中一个。文本格式的param文件可以直接编辑,如果某些层被NCNN识别成Unknown,你可以手动修改param里的optype替换成支持的类型,但这对新手不推荐,除非你很熟悉模型结构。

4.3 INT8量化部署:校准集和量化脚本说明

树莓派4B上如果直接用FP32模型推理,耗时约180ms/帧;如果转成INT8量化模型,推理时间能压缩到80-110ms/帧,提升明显。但INT8量化的前提是校准流程正确,否则精度会掉得厉害。

NCNN的量化工具是ncnn2table和ncnn2int8。资源里的quantize_ncnn.py脚本把校准流程做了封装,核心操作如下:

# 第一步:生成校准参数表 ./ncnn2table yolov5_lite_v2.1_s.param yolov5_lite_v2.1_s.bin \ calib_images/ yolov5_lite_v2.1_s.table \ mean=[104.0,117.0,123.0] norm=[0.01712,0.01712,0.01712] \ shape=640x640x3 pixel=0 thread=2 method=kl # 第二步:根据校准表生成INT8量化模型 ./ncnn2int8 yolov5_lite_v2.1_s.param yolov5_lite_v2.1_s.bin \ yolov5_lite_v2.1_s.table yolov5_lite_v2.1_s_int8.param yolov5_lite_v2.1_s_int8.bin

第二段命令里mean和norm参数是关键。NCNN的输入数据归一化方式是(输入像素值 - mean) * norm,而YOLOv5官方训练时的归一化方式是像素值/255,两者不等价。如果你把mean=[104,117,123]和norm=[0.01712, 0.01712, 0.01712]代入会发现:(0 - 104) * 0.01712 ≈ -1.78,(255 - 104) * 0.01712 ≈ 2.58,这对应的是YOLOv5输入归一化到[-1.78, 2.58]区间的做法,实际效果等同于除以255。

校准图片集放在calib_images目录,约200张从训练集或现场采集的图片即可,数量太少会导致量化精度损失加大。校准过程会逐张跑前向推理并统计每层激活值的分布,耗时取决于你的PC性能,可能从几分钟到半小时不等。校准表的生成值是经验性的,如果你换了一批校准图,table文件也会变化,需要重新生成。

量化后的模型在树莓派上加载时,需要把NCNN的配置改为:

yolo_net.opt.use_int8_inference = true; yolo_net.opt.use_fp16_storage = false;

同时需要注意,量化模型不支持修改输入分辨率动态变化,必须严格使用640×640。

5. 部署必知必会:摄像头接入、CMake构建与避坑排查

5.1 摄像头接入与视频循环推理主循环

资源里camera_capture.cpp封装了CSI和USB两种摄像头接入方式。CSI摄像头的接入在树莓派上要先通过raspistill或libcamera确认摄像头能被系统识别。树莓派官网推荐用libcamera,但OpenCV的VideoCapture不能直接监听libcamera的输出节点,需要通过V4L2驱动桥接。具体做法是在/boot/config.txt中设置camera_auto_detect=1,然后使用libcamera-vid -t 0 --width 640 --height 480 --framerate 30来初始化摄像头。

USB摄像头的接入就简单很多,直接识别为/dev/video0节点,OpenCV的VideoCapture(0)就能读取。但需要注意的是USB摄像头在树莓派4B上的性能和帧率与摄像头的UVC协议相关,如果输出是MJPG格式(大多数廉价USB摄像头默认),必须手动设置抓取格式:

cv::VideoCapture cap; cap.open(0); // 关键:设置抓取帧的宽高和fps,不设置的话可能默认走YUV格式且只有15fps cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); cap.set(cv::CAP_PROP_FPS, 30); // 强制使用MJPG格式,提升USB传输效率 cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));

推理主循环在main.cpp中的结构是这样的:

int run_detection() { cv::VideoCapture cap; if (!cap.open(0)) { fprintf(stderr, "cannot open camera device"); return -1; } cv::Mat frame, resized; while (true) { // 读取摄像头帧 cap >> frame; if (frame.empty()) break; // 预处理:letterbox + BGR转RGB + 归一化 letterbox(frame, resized, 640); cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB); // 构造NCNN输入Mat ncnn::Mat in = ncnn::Mat::from_pixels(resized.data, ncnn::Mat::PIXEL_RGB, 640, 640); // 归一化:像素值 * norm + mean const float mean[3] = {104.0f, 117.0f, 123.0f}; const float norm[3] = {0.01712f, 0.01712f, 0.01712f}; in.substract_mean_normalize(mean, norm); // 推理 ncnn::Extractor ex = yolo_net.create_extractor(); ex.input("images", in); ncnn::Mat out; ex.extract("output", out); // 后处理解码 std::vector<Object> objects; decode_yolo_output(out, objects); nms_boxes(objects, 0.45); // 把检测框映射回原图坐标画框 draw_boxes(frame, objects); // 显示 cv::imshow("yolov5-lite", frame); if (cv::waitKey(1) == 'q') break; } return 0; }

这段代码是可以直接编译运行的,但也暴露了一个性能问题:cvtColor和letterbox两次操作都在CPU上做,额外耗时会叠加在推理时间上。实测下来,这两步大约占单帧总耗时里的15-20ms。如果追求极致性能,可以省去cvtColor,直接把BGR的Mat数据送进NCNN然后在网络第一层里做通道顺序变换,但这样需要修改模型。作为常规用法,用cvtColor已经足够,毕竟树莓派上省不出更多性能了。

5.2 CMake构建与交叉编译配置

资源里的CMakeLists.txt已经写好了NCNN的链接路径依赖,核心配置如下:

cmake_minimum_required(VERSION 3.10) project(yolov5_lite_deploy) set(CMAKE_CXX_STANDARD 11) # 引入OpenCV find_package(OpenCV REQUIRED) # 设置NCNN的路径变量 set(NCNN_DIR "/home/pi/ncnn" CACHE PATH "NCNN root directory") include_directories(${NCNN_DIR}/src) include_directories(${NCNN_DIR}/build/src) link_directories(${NCNN_DIR}/build/src) add_executable(yolov5_lite_deploy main.cpp yolo_layer.cpp letterbox.cpp camera_capture.cpp ) target_link_libraries(yolov5_lite_deploy ncnn ${OpenCV_LIBS} pthread )

这段配置里需要特别说明的是NCNN_DIR的路径变量。如果你在树莓派上直接编译NCNN,那么NCNN_DIR就是你clone的源码目录;如果你在PC上交叉编译,需要把编译好的NCNN库同步到树莓派上再改路径。最省事的方式是直接在树莓派上编译NCNN,耗时大约20-40分钟(取决于Pi 4B的散热情况),但能保证库与本机完全兼容,后续排查问题会少很多。

如果你是交叉编译,要额外注意CMake工具链配置,在树莓派上放置一个toolchain.cmake文件,指向树莓派gcc交叉编译器,然后通过CMAKE_TOOLCHAIN_FILE参数指定。这份资源里没有包含交叉编译的具体步骤,但我们通常做法是:在PC上mkdir build-arm,然后cmake -DCMAKE_TOOLCHAIN_FILE=../rpi-toolchain.cmake,生成Makefile后make,再把编译好的可执行文件scp到树莓派上运行。

5.3 避坑指南:树莓派部署YOLOv5-Lite的4个血泪经验

以下是在这个部署项目里最容易翻车的4个环节,我逐一说明现象、成因和解法。

第一个坑:NCNN编译时提示c++编译器不支持C++11特性。树莓派系统自带的gcc版本太老(老系统的gcc 8以下版本),会导致NCNN源码中stack、tuple等C++11特性编译失败。解决方法是把系统更新到较新的版本(基于Debian Bullseye的树莓派OS版本gcc版本10以上可用),或者手动安装高版本gcc。不建议手动编译高版本gcc,耗时太长,升级系统最省事。

第二个坑:摄像头显示“Cannot open camera device”但摄像头明明插着。原因多半是CSI摄像头的设备文件被libcamera在开机后自动占用,或者在设备树里没有正确配置。解决方法是检查/boot/config.txt里是否有camera_auto_detect=1,然后执行sudo libcamera-hello测试摄像头正常哈。USB摄像头的话先ls /dev/video*确认设备节点存在。还有种少见原因是权限问题,把用户加入video组就能搞定。

第三个坑:量化模型精度大幅下降,mAP掉点超过5个点。常见原因是校准图片集尺寸不对,例如校准图尺寸小于640×640,导致统计的激活值分布偏移。另外mean和norm参数配置错误也会让输入分布和训练时不一致。解决方法是严格按资源里的流程重新生成校准表,校准图片集用从实景中拍摄的图片,不要用网络随便下载的图片,因为数据分布差异会直接影响量化精度。

第四个坑:开启USE_INT8_INFERENCE后CPU占用率100%但FPS反而降低。原因是树莓派4B的A72核心跑INT8 SIMD指令时,一个核心需要两个周期才能处理一次load + store,数据搬运瓶颈非常明显。解决方式是检查CPU降频是否严重,在config.txt的[all]段中增加force_turbo=1和arm_freq=1800(注意散热),如果还不行就换回FP32模型,毕竟FPS还是比量化后差距大的时候,FP32也就慢了30ms,不是不可接受。

6. 二次开发改造思路:从跑通Demo到落地到你的场景

6.1 改1:替换为自己的数据集和类别

文件包里默认模型是在COCO 80类上训练的,如果你的项目只需要检测两三类目标,比如安全帽检测、口罩检测或车辆检测,直接用这个模型权重会有冗余(实际上COCO里没有安全帽类,模型根本检测不到),正确做法是:

  • 使用数据准备脚本准备自己的标注数据,标注格式按YOLO格式(txt每行:class_id cx cy w h,数值归一化)
  • 在YOLOv5-Lite源码基础上修改数据配置,重训练一个自己的权重
  • 训练结束后导出ONNX并走转换量化流程

这套流程的验证方法是对比新权重和原权重在相同测试集上的mAP,同时对比树莓派上的FPS。通常重训练后的模型对目标类别少,模型可以进一步缩放(比如把模型宽度乘0.75剪枝),推理速度会更快。需要注意的坑是:重训练后的模型在转换NCNN时如果分类数改了,输出层的通道数也会变,对应的后处理解码代码里的num_classes常量要同步修改,否则会读到错误的内存区间。

6.2 改2:把检测结果通过网络推送到服务器

树莓派设备往往没有屏幕,一个常见的落地做法是通过HTTP或WebSocket把检测结果和视频帧推送到PC端或手机浏览器。最简单的改造是在主循环中增加一个发送逻辑,将检测到的目标类别、置信度、坐标信息序列化成JSON,通过ZMQ或MQTT发送。如果你对实时性要求高,用ZMQ的PUB/SUB模式,代码量小,延迟低。

这里给一个MQTT发送的模板片段:

// 假设使用paho-mqtt-c库 mqtt_client_t client = mqtt_client_new("tcp://192.168.1.100:1883"); mqtt_client_connect(client); while (true) { // 推理并得到objects... // 序列化为JSON std::string json_str = build_json_objects(objects); // 发送到主题,比如 topic: yolo/detections mqtt_client_publish(client, "yolo/detections", json_str.c_str(), json_str.length(), 0); }

这种改造的验证方式很简单:在PC端订阅这个MQTT主题,用Python的paho-mqtt跑一个订阅脚本,打印收到的检测数据。如果收到的可变payload和本地log一致,说明网络链路通了。实际部署时还需要注意WiFi在树莓派上的稳定性,建议使用5GHz频段,2.4GHz实测延迟波动较大。

6.3 改3:性能优化再进阶的三个方向

如果FPS还不够理想,从代码工程层面有几个优化手段可以做。

方向一是把图像预处理从OpenCV的cvtColor替换成NCNN的内置像素转换API,NCNN的ncnn::Mat::from_pixels_resize直接完成缩放和色彩空间转换,少一次中间拷贝,实测省掉5-8ms。

方向二是修改NCNN的推理线程策略。前面说过2线程是最优解,但如果你把NCNN设置为1线程,配合开启use_winograd_convolution,卷积层的Winograd变换能减少乘法次数,在1050ms这个量级下反而更有效果。实测单线程+Winograd模式下,CPU占用率约60%,耗时可压到70-90ms,同时功耗也下来了。

方向三是把模型输入从640×640降到480×480或320×320。输入尺寸每降低一级,计算量按平方规律下降。320×320输入时推理约30-45ms/帧,FPS接近20-25。代价是精度下降,对小目标尤其不友好。如果你的摄像头画面里目标是大尺寸物体(比如车辆、行人半身),可以考虑这个方向。

验证这些优化的方式是做一个定时任务脚本,循环跑1000帧,记录平均耗时、帧率和CPU温度,然后用matplotlib画出曲线对比。我在这个项目上跑通后发现,结合2线程NCNN + 480输入尺寸 + Winograd优化,树莓派4B可以达到接近20FPS的表现,同时CPU温度稳定在65度以内(配合散热片),这已经能满足大多数安防监控类项目的实时性要求了。

6.4 回头看这次拆包的几个收获

跑完这套资源后我最大的感受是:树莓派上做深度学习部署的难点不在模型算法,而在工程化链路——模型转换的算子兼容性、量化校准的分布匹配、CPU线程调度和内存带宽的权衡、摄像头数据链路的初始化,每一步都有可能翻车。从那以后我再做边缘设备部署,都强制走一遍「FP32模型跑通基线→INT8量化→单线程对比双线程→固定输入尺寸」这个流程,每一步用tensorboard记录耗时曲线,不靠错觉调参。希望这份拆包笔记能帮你把YOLOv5-Lite在树莓派4B上跑起来,跑起来之后怎么改就跟着你的实际场景走了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询