树莓派5实战:用NCNN跑通YOLOv5目标检测(附完整代码)
树莓派5实战用NCNN跑通YOLOv5目标检测附完整代码最近在捣鼓树莓派5想在上面跑点“硬核”的视觉应用比如实时目标检测。市面上方案不少但要么太重动辄几百兆的框架塞不进小小的SD卡要么太慢一帧图等上好几秒黄花菜都凉了。折腾了一圈最后把目光锁定在了NCNN和YOLOv5这对组合上。NCNN的轻量和高效配上YOLOv5在精度与速度间的平衡感觉是为树莓派这类嵌入式设备量身定做的。不过从模型准备到最终在派上流畅运行中间有不少坑要踩尤其是性能调优部分直接决定了你的应用是“玩具”还是“工具”。这篇文章我就把自己从零搭建、优化到最终部署的完整过程包括那些容易出错的细节和提升性能的“骚操作”毫无保留地分享出来。无论你是想做个智能门铃、小车避障还是搞点边缘AI的创意项目这套流程应该都能给你省下不少时间。1. 环境准备与基础搭建在树莓派5上玩转深度学习第一步不是急着写代码而是把地基打牢。树莓派5虽然性能相比前代有显著提升配备了更强大的ARM Cortex-A76 CPU但其计算资源和内存尤其是4GB或8GB版本相比桌面级设备依然有限。因此一个干净、高效且针对硬件优化的系统环境至关重要。我强烈推荐使用Raspberry Pi OS (64-bit) Lite版本作为起点。这个版本没有图形桌面资源占用极低能把每一分算力都留给我们的推理任务。通过SSH连接操作即可对于服务器或嵌入式应用来说这才是“专业”的做法。当然如果你需要桌面环境进行图像预览或调试也可以选择带桌面的版本只是要接受性能上的一点妥协。系统烧录并启动后第一件事就是更新软件源并安装核心的编译工具和依赖库。下面这个命令组合是我每次搭建环境必跑的它能确保后续编译NCNN或OpenCV时不会因为缺少某个库而报错。sudo apt update sudo apt upgrade -y sudo apt install -y build-essential cmake git wget unzip sudo apt install -y libopencv-dev libjpeg-dev libpng-dev libtiff-dev sudo apt install -y libvulkan-dev vulkan-tools # 为可选的Vulkan加速做准备注意libopencv-dev安装的是系统仓库中预编译的OpenCV。对于追求极致性能的玩家可以后续从源码编译开启更多优化选项如NEON、VFPv3的OpenCV但这会耗费大量时间。对于初次部署使用预编译版本更快捷。接下来我们需要一个合适的C编译环境。树莓派5的ARM Cortex-A76架构支持ARMv8-A指令集GCC编译器版本最好在8以上。检查一下你的GCC版本gcc --version如果版本较低可以考虑更新到较新的版本。不过Raspberry Pi OS的最新版本通常已经提供了足够新的工具链。环境准备的最后一步是为我们的项目创建一个独立的工作空间。这有助于保持文件结构清晰也方便管理多个项目。mkdir -p ~/projects/raspberrypi_yolov5 cd ~/projects/raspberrypi_yolov5至此一个为深度学习推理优化的基础Linux环境就准备好了。我们避开了图形界面的资源消耗安装了所有必要的开发工具为下一步编译高性能的NCNN推理引擎扫清了障碍。2. 编译与部署NCNN推理引擎NCNN是腾讯优图实验室开源的宝藏框架它的设计哲学深深契合嵌入式场景极致轻量、无第三方依赖、CPU推理高效。在树莓派上使用它通常有两种方式直接安装预编译的包或者从源码编译。为了获得最佳的硬件适配性和开启所有可能的优化比如针对ARM架构的NEON SIMD指令加速我强烈推荐从源码编译。首先获取NCNN的源代码。建议使用git克隆最新的稳定版本或某个特定发布版以确保功能的完整性和稳定性。cd ~/projects/raspberrypi_yolov5 git clone https://github.com/Tencent/ncnn.git cd ncnnNCNN的编译配置非常灵活。树莓派5是ARM64aarch64架构我们可以使用其自带的交叉编译工具链文件也可以直接在派上进行原生编译。原生编译更简单直接也是我最常用的方式。mkdir -p build cd build cmake -DCMAKE_BUILD_TYPERelease -DNCNN_VULKANON -DNCNN_SYSTEM_GLSLANGOFF -DNCNN_BUILD_EXAMPLESON ..这里有几个关键的CMake选项-DCMAKE_BUILD_TYPERelease: 启用编译器优化这是提升性能的关键务必使用。-DNCNN_VULKANON: 开启Vulkan API支持。树莓派5的GPUVideoCore VII驱动正在逐步完善对Vulkan的支持。虽然目前可能还不是最稳定的选择但先编译进去为未来的GPU加速留个后门。-DNCNN_BUILD_EXAMPLESON: 编译官方示例程序。这些示例是极好的学习资料能帮你快速理解API用法。配置完成后就可以开始编译了。使用make -j$(nproc)命令让编译过程充分利用树莓派5的所有CPU核心这能大幅缩短等待时间。make -j$(nproc) sudo make install编译安装完成后NCNN的库文件和头文件会被安装到/usr/local/目录下。你可以通过以下命令验证是否安装成功ls /usr/local/lib/libncnn* ls /usr/local/include/ncnn/如果看到libncnn.a静态库或libncnn.so动态库以及一系列头文件恭喜你推理引擎就位了。为了后续编写代码方便我们最好让系统知道如何找到NCNN的库。如果遇到链接错误可以尝试将库路径加入环境变量export LD_LIBRARY_PATH/usr/local/lib:$LD_LIBRARY_PATH更一劳永逸的方法是创建一个.conf文件让系统在启动时自动加载echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/ncnn.conf sudo ldconfig至此一个为树莓派5量身定制、开启了所有可能加速选项的NCNN推理引擎已经部署完毕。它就像一台高性能的发动机只等我们放入合适的燃料——也就是经过转换的YOLOv5模型。3. YOLOv5模型转换与优化技巧有了NCNN引擎下一步就是准备“燃料”——将训练好的YOLOv5模型转换成NCNN能识别的格式。这个过程就像把汽油精炼成发动机能直接燃烧的形式转换的质量直接决定最终推理的效率和精度。通常我们从PyTorch生态获取YOLOv5模型。你可以使用官方预训练的模型如yolov5s.pt,yolov5m.pt也可以使用自己数据集训练得到的.pt权重文件。模型转换的通用路径是PyTorch (.pt) - ONNX (.onnx) - NCNN (.param .bin)。我建议在一台性能更强的x86机器比如你的开发电脑上完成转换工作因为模型导出和优化可能需要一些计算资源。首先确保你有一个Python环境并安装了必要的包# 在你的开发机上操作 pip install torch torchvision onnx onnx-simplifier pip install ncnn # 用于后续的pnnx工具可选更推荐 # 克隆YOLOv5官方仓库用于导出脚本 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt步骤一导出为ONNX格式使用YOLOv5官方提供的export.py脚本可以很方便地完成导出。关键参数是--include onnx和--dynamic。--dynamic允许输入尺寸动态变化这在嵌入式设备上调整输入大小时非常有用。python export.py --weights yolov5s.pt --include onnx --dynamic执行后你会得到yolov5s.onnx文件。但直接导出的ONNX模型可能包含一些冗余算子不利于在NCNN上高效推理。步骤二简化与优化ONNX模型这里推荐使用onnx-simplifier工具它能自动优化计算图结构合并冗余算子显著减小模型体积并提升推理速度。python -m onnxsim yolov5s.onnx yolov5s-sim.onnx现在我们得到了一个优化后的yolov5s-sim.onnx文件。对比一下原始文件和简化后文件的大小通常会有可观的缩减。步骤三转换为NCNN格式这是最关键的一步。我们需要使用NCNN提供的转换工具onnx2ncnn。这个工具通常在编译NCNN时生成位于ncnn-root/build/tools/onnx/目录下。将其拷贝到方便使用的地方。# 假设在开发机上也有编译好的ncnn /path/to/ncnn/build/tools/onnx/onnx2ncnn yolov5s-sim.onnx yolov5s.param yolov5s.bin转换成功后你会得到两个文件yolov5s.param: 文本文件描述模型的计算图结构。yolov5s.bin: 二进制文件包含模型的所有权重参数。模型优化进阶技巧直接转换的模型可能还不是最优。为了在树莓派5上获得最佳性能我们还可以进行以下优化FP16量化NCNN支持将模型权重从FP32转换为FP16存储。这几乎不会损失精度但能减少近一半的模型体积并利用ARM处理器的半精度计算单元提升速度。可以使用NCNN提供的ncnnoptimize工具/path/to/ncnn/build/tools/ncnnoptimize yolov5s.param yolov5s.bin yolov5s-opt.param yolov5s-opt.bin 65536参数65536代表使用FP16存储。优化后的yolov5s-opt.param和yolov5s-opt.bin就是最终部署的模型。自定义输入输出名转换后的.param文件中输入和输出层的名称可能是通用的input和output。为了代码清晰你可以用文本编辑器打开.param文件将第一行的输入层名如Input_0改为data将输出层名改为output或你喜欢的名字并在代码中保持一致。选择合适的基础模型YOLOv5有s/m/l/x等多个版本体积和精度递增。对于树莓派5yolov5s或yolov5nnano通常是速度和精度兼顾的最佳起点。下表对比了不同模型在典型输入尺寸下的参数量和相对速度模型版本参数量 (百万)相对推理速度 (树莓派5预估)适用场景YOLOv5n~1.9最快对速度要求极高精度要求稍低YOLOv5s~7.2快通用推荐平衡之选YOLOv5m~21.2中等需要更高检测精度可接受稍慢速度YOLOv5l~46.5慢精度要求高实时性要求不高将优化好的yolov5s-opt.param和yolov5s-opt.bin通过SCP或SD卡拷贝到树莓派5的项目目录中模型的准备工作就全部完成了。4. 编写与调试C推理代码模型和引擎都已就绪现在让我们用C代码将它们串联起来实现完整的检测流程。在嵌入式开发中C能提供最好的性能控制。我们的代码主要分为几个部分加载模型、预处理图像、执行推理、解析输出结果、后处理非极大值抑制NMS以及绘制检测框。首先在项目目录下创建我们的主程序文件main.cpp并开始编写代码。第一部分头文件与全局定义引入必要的头文件并定义一些常量如输入图像尺寸、置信度阈值和NMS阈值。这些参数直接影响检测效果。#include ncnn/net.h #include opencv2/opencv.hpp #include iostream #include vector #include algorithm // 定义YOLOv5的输入尺寸这里使用640x640 const int target_width 640; const int target_height 640; // 置信度阈值低于此值的检测框将被过滤 const float confidence_threshold 0.25f; // 非极大值抑制阈值用于消除重叠框 const float nms_threshold 0.45f; // 假设使用COCO数据集的80个类别 const char* class_names[] { person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic light, fire hydrant, stop sign, parking meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports ball, kite, baseball bat, baseball glove, skateboard, surfboard, tennis racket, bottle, wine glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot dog, pizza, donut, cake, chair, couch, potted plant, bed, dining table, toilet, tv, laptop, mouse, remote, keyboard, cell phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy bear, hair drier, toothbrush };第二部分定义检测结果结构体和后处理函数我们需要一个结构体来存储每个检测框的信息并实现NMS算法来过滤冗余框。struct Object { cv::Rect_float rect; // 检测框 int label; // 类别ID float prob; // 置信度 }; // 非极大值抑制实现 static void nms_sorted_bboxes(const std::vectorObject objects, std::vectorint picked, float nms_threshold) { picked.clear(); const int n objects.size(); std::vectorfloat areas(n); for (int i 0; i n; i) { areas[i] objects[i].rect.area(); } for (int i 0; i n; i) { const Object a objects[i]; bool keep true; for (int j : picked) { const Object b objects[j]; // 计算IoU float inter_area (a.rect b.rect).area(); float union_area areas[i] areas[j] - inter_area; float iou inter_area / union_area; if (iou nms_threshold) { keep false; break; } } if (keep) { picked.push_back(i); } } }第三部分主函数——加载模型与推理这是代码的核心我们按步骤加载模型、处理图像、运行网络并解析输出。int main(int argc, char** argv) { if (argc ! 3) { std::cerr Usage: argv[0] model.param model.bin std::endl; return -1; } const char* param_path argv[1]; const char* bin_path argv[2]; const char* image_path test.jpg; // 测试图片 // 1. 加载模型 ncnn::Net net; if (net.load_param(param_path) ! 0 || net.load_model(bin_path) ! 0) { std::cerr Failed to load ncnn model! std::endl; return -1; } std::cout Model loaded successfully. std::endl; // 2. 读取并预处理图像 cv::Mat img_bgr cv::imread(image_path); if (img_bgr.empty()) { std::cerr Failed to read image: image_path std::endl; return -1; } int img_w img_bgr.cols; int img_h img_bgr.rows; // 将图像缩放到网络输入尺寸并保持长宽比进行填充letterbox cv::Mat resized; float scale std::min(target_width / (float)img_w, target_height / (float)img_h); int new_w (int)(img_w * scale); int new_h (int)(img_h * scale); cv::resize(img_bgr, resized, cv::Size(new_w, new_h)); int dw target_width - new_w; int dh target_height - new_h; dw / 2; dh / 2; cv::Mat padded; cv::copyMakeBorder(resized, padded, dh, dh, dw, dw, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114)); // 3. 转换为ncnn::Mat并进行归一化 (除以255) ncnn::Mat in ncnn::Mat::from_pixels(padded.data, ncnn::Mat::PIXEL_BGR, target_width, target_height); in.substract_mean_normalize(0, 1.f/255.f); // 通常YOLOv5只需要除以255 // 4. 创建提取器并执行推理 ncnn::Extractor ex net.create_extractor(); // 设置线程数树莓派5有4个高性能核心可以尝试设置为4 ex.set_num_threads(4); ex.input(data, in); // 注意这里的data需要与.param文件中的输入层名一致 ncnn::Mat out; ex.extract(output, out); // 注意这里的output需要与.param文件中的输出层名一致 // 5. 解析输出 std::vectorObject proposals; // YOLOv5的输出格式为 [num_boxes, 85]其中85 cx, cy, w, h, conf 80个类别的概率 for (int i 0; i out.h; i) { const float* ptr out.row(i); float obj_conf ptr[4]; if (obj_conf confidence_threshold) continue; // 找到最大类别概率 int class_id -1; float cls_conf -1.0f; for (int c 0; c 80; c) { float score ptr[5 c]; if (score cls_conf) { cls_conf score; class_id c; } } float final_score obj_conf * cls_conf; if (final_score confidence_threshold) continue; // 解析框坐标 (cx, cy, w, h) 并转换到原始图像尺寸 float cx ptr[0]; float cy ptr[1]; float w ptr[2]; float h ptr[3]; float x1 (cx - w * 0.5f - dw) / scale; float y1 (cy - h * 0.5f - dh) / scale; float x2 (cx w * 0.5f - dw) / scale; float y2 (cy h * 0.5f - dh) / scale; // 确保坐标在图像范围内 x1 std::max(std::min(x1, (float)(img_w - 1)), 0.f); y1 std::max(std::min(y1, (float)(img_h - 1)), 0.f); x2 std::max(std::min(x2, (float)(img_w - 1)), 0.f); y2 std::max(std::min(y2, (float)(img_h - 1)), 0.f); Object obj; obj.rect cv::Rect_float(x1, y1, x2 - x1, y2 - y1); obj.label class_id; obj.prob final_score; proposals.push_back(obj); } // 6. 应用非极大值抑制 std::vectorint picked; nms_sorted_bboxes(proposals, picked, nms_threshold); // 7. 绘制检测框并输出结果 cv::Mat result_img img_bgr.clone(); for (int idx : picked) { const Object obj proposals[idx]; cv::rectangle(result_img, obj.rect, cv::Scalar(0, 255, 0), 2); char text[256]; sprintf(text, %s %.2f, class_names[obj.label], obj.prob); int baseLine 0; cv::Size label_size cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, baseLine); cv::putText(result_img, text, cv::Point(obj.rect.x, obj.rect.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1); std::cout Detected: class_names[obj.label] at [ obj.rect.x , obj.rect.y , obj.rect.width , obj.rect.height ] with confidence obj.prob std::endl; } cv::imwrite(result.jpg, result_img); std::cout Detection finished. Result saved to result.jpg std::endl; return 0; }第四部分编译与运行编写一个简单的CMakeLists.txt来管理编译cmake_minimum_required(VERSION 3.10) project(YOLOv5_NCNN_Demo) set(CMAKE_CXX_STANDARD 11) find_package(OpenCV REQUIRED) # 假设ncnn安装在默认路径如果没有可以手动指定 find_package(ncnn REQUIRED) add_executable(yolov5_demo main.cpp) target_link_libraries(yolov5_demo ${OpenCV_LIBS} ncnn)然后进行编译mkdir build cd build cmake .. make -j$(nproc)编译成功后将测试图片test.jpg、模型文件yolov5s-opt.param和yolov5s-opt.bin放在与可执行文件相同的目录运行./yolov5_demo yolov5s-opt.param yolov5s-opt.bin如果一切顺利你将看到控制台输出检测到的物体信息并在当前目录生成一张带有检测框的result.jpg图片。至此一个完整的YOLOv5目标检测程序就在树莓派5上跑起来了。5. 性能调优与实战踩坑指南代码能跑通只是第一步让它在树莓派5上跑得又快又稳才是实战的精华所在。性能调优是一个系统工程涉及模型、代码、系统多个层面。下面是我在多次实践中总结出的有效策略和常见问题的解决方法。1. 输入分辨率与模型剪裁这是提升速度最直接有效的方法。YOLOv5默认输入是640x640但对于一些近距离、大目标的场景比如人脸识别门禁降低到320x320甚至224x224速度能有数倍提升而精度下降在可接受范围内。你需要在模型转换前在导出ONNX时指定--imgsz 320参数。同时代码中的target_width和target_height也要相应修改。2. 多线程推理设置NCNN的Extractor支持设置线程数。树莓派5有4个高性能核心理论上设置为4可以获得最佳性能。但实际测试中由于内存带宽和调度开销有时设置为2或3可能效率更高。这是一个需要根据实际场景微调的参数。ex.set_num_threads(4); // 尝试2, 3, 43. 内存与缓存优化深度学习推理是内存密集型任务。确保树莓派5有足够的可用内存并尽量关闭不必要的后台进程。你可以使用htop命令监控内存和CPU使用情况。此外将模型文件和程序放在高速SD卡甚至USB 3.0的固态硬盘上也能减少加载时间。4. 利用ARM NEON指令集NCNN在编译时默认已开启对NEON的支持这是ARM架构的SIMD指令集能大幅加速浮点计算。确保你的CMake编译选项中没有意外关闭它。你可以通过查看NCNN的编译输出确认是否包含了-mfpuneon等优化标志。5. 性能基准测试与监控要优化先测量。编写一个简单的循环对同一张图片进行多次推理计算平均耗时和帧率(FPS)。#include chrono // ... 在推理循环前后 auto start std::chrono::high_resolution_clock::now(); // 推理代码 auto end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed end - start; std::cout Inference time: elapsed.count() * 1000 ms std::endl;常见踩坑点与解决方案坑点一模型转换后输出维度不对或结果异常。排查首先检查ONNX模型是否简化成功。然后用Netron工具一个神经网络可视化工具分别打开原始的ONNX模型和转换后的NCNN.param文件对比输入输出层的名称和维度是否一致。确保代码中ex.input()和ex.extract()使用的层名与.param文件完全一致。坑点二推理速度远低于预期。排查检查是否在Release模式下编译的程序cmake -DCMAKE_BUILD_TYPERelease。使用perf或sudo cpufreq-set -g performance命令将CPU频率 governor 设置为性能模式避免动态调频。通过ex.set_num_threads()调整线程数找到最佳点。考虑使用更小的模型如YOLOv5n或更低的输入分辨率。坑点三检测框位置错乱或大小异常。排查这几乎总是预处理或后处理的坐标转换逻辑错误。仔细核对代码中从网络输出坐标(cx, cy, w, h)到原始图像坐标(x1, y1, x2, y2)的转换过程特别是letterbox填充dw,dh和缩放比例scale的计算是否正确。建议用一张简单的、目标居中的图片进行调试并打印出每一步的中间坐标值。坑点四内存不足导致程序崩溃。排查树莓派5的4GB内存看似不少但系统、桌面环境如果有和其他应用会占用一部分。运行推理程序时如果出现std::bad_alloc或段错误可能是内存不足。尝试关闭所有不必要的图形界面和应用。使用sudo dphys-swapfile swapoff sudo dphys-swapfile swapon适当增加交换空间swap但这会影响速度。从根本上还是优化模型大小和输入分辨率。实战建议表格优化方向具体措施预期效果潜在代价模型层面使用YOLOv5n/v5s模型进行FP16量化显著提升速度减少内存占用精度轻微下降输入层面降低输入图像分辨率如640-320大幅提升推理速度对小目标检测能力下降代码层面设置合适线程数优化预处理/后处理逻辑中等程度提升效率增加代码复杂度系统层面CPU调频模式设为performance关闭无关进程稳定且小幅提升增加功耗与发热最后性能调优没有银弹它是一个权衡的艺术。你需要根据具体应用场景是要求实时性30FPS还是可以接受1FPS来调整这些旋钮。我的经验是在树莓派5上使用量化后的YOLOv5s模型输入320x320配合4线程处理单张图片的时间可以稳定在100-200毫秒以内这对于很多边缘计算项目来说已经是一个相当可用的性能了。多试几次找到最适合你那个项目的甜蜜点这个过程本身也充满了乐趣。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2419533.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!