1. ncnn 部署 yolov5 的完整链路从 ONNX 导出到端侧推理对齐ncnn 是腾讯开源的一个为移动端和嵌入式设备高度优化的高性能神经网络推理框架它没有第三方依赖、跨平台、在 ARM 设备上跑得飞快。yolov5 则是目前工程落地最广的目标检测模型之一结构清晰、精度和速度平衡得好。把这两者结合起来就是「ncnn 部署 yolov5」这条链路要解决的事让一个训练好的 yolov5 模型最终能在手机、树莓派、RK3588 这类端侧设备上稳定跑出正确的检测框。这条链路适合谁适合已经跑通过 yolov5 训练、手里有.pt权重但卡在「怎么搬到端侧」这一步的工程师也适合做智能硬件、边缘盒子、IPC 摄像头方案需要把检测模型塞进资源受限设备的同学。整条链路的核心难点不在推理本身而在前后处理对齐输入预处理的归一化、letterbox 的填充方式、输出层的解码逻辑任何一处和训练时不一致框就会偏、置信度就会乱。我试过在 x86 上转换成功、拿到 ARM 板上却检测全错的情况最后排查下来就是 param 文件里输出层没改对。所以这篇不打算只给你几条命令而是把每一步「为什么这么做」「怎么验证做对了」讲清楚让你在端侧能复现出和 PC 上一致的推理结果。下面按导出、转换、改 param、编译、验证、排障的顺序拆开讲每一步都给可复制的命令和配置。2. 前置准备编译 ncnn 与 onnx2ncnn 工具链在动模型之前得先把工具链搭好。ncnn 的模型转换依赖onnx2ncnn这个工具而它只有在编译 ncnn 时检测到 protobuf 才会被编译出来。很多人第一次编译完发现build/tools/onnx/目录是空的就是因为缺了 protobuf。OpenCV 也建议一起装上后面跑 example 验证要用到图像读写。先装系统依赖。以 Ubuntu 为例sudo apt update sudo apt install -y build-essential cmake git libopencv-devprotobuf 建议用源码编译一个固定版本避免和系统自带的版本冲突。ncnn 对 protobuf 版本比较宽容3.x 系列都能用wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.3/protobuf-cpp-3.20.3.tar.gz tar -xzf protobuf-cpp-3.20.3.tar.gz cd protobuf-3.20.3 ./configure --prefix/usr/local make -j$(nproc) sudo make install sudo ldconfig装完验证一下protoc --version能打印出版本号就说明 protobuf 就位了。接着编译 ncnngit clone https://github.com/Tencent/ncnn.git cd ncnn git submodule update --init mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DNCNN_VULKANOFF -DNCNN_BUILD_EXAMPLESON .. make -j$(nproc) make install这里NCNN_VULKANOFF是因为端侧大多用 CPU 推理如果你目标设备有 GPU 再打开。编译完成后重点确认这个文件存在ls build/tools/onnx/onnx2ncnn如果这个可执行文件在说明工具链齐了。如果不在八成是 cmake 阶段没找到 protobuf回头检查protoc是否在 PATH 里、ldconfig是否执行过。这一步是整个链路的地基地基不稳后面全是坑。3. 可复制配置从 .pt 导出 ONNX 再转 ncnn param/bin工具链好了开始处理模型。yolov5 官方仓库自带导出脚本先把.pt转成.onnx。注意导出时的--img要和训练/推理时保持一致端侧一般用 640git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt onnx onnxsim python export.py --weights yolov5s.pt --img 640 --batch 1 --include onnx导出后会得到yolov5s.onnx。接着用 onnxsim 做一次图简化去掉冗余算子这一步能显著减少转换时的报错python -m onnxsim yolov5s.onnx yolov5s-sim.onnx然后调用onnx2ncnn转换cd ncnn/build/tools/onnx ./onnx2ncnn yolov5s-sim.onnx yolov5s.param yolov5s.bin正常情况下会生成yolov5s.param和yolov5s.bin两个文件。但 yolov5 里有个 Focus 模块ncnn 原生不支持转换时通常会报类似Unsupported slice或直接跳过该层导致 param 文件里层结构错乱。这就是为什么必须手动改 param。改之前先理解 param 文件的结构。第一行是版本信息第二行是层数和 blob 数之后每行描述一层层类型 层名 输入数 输出数 输入blob 输出blob。Focus 模块在原始网络里做的是切片拼接ncnn 里可以用一个自定义的Yolov5Focus层替代或者干脆把切片逻辑拆成多个CropConcat。最省事的做法是删掉不支持的层插入一层 Focus并保证输入输出 blob 名对得上。改完 Focus 之后第二处必须改的是输出层。yolov5 的三个检测头输出在 param 里对应的层要把输出 shape 改成-1否则后处理解码时会冒出一堆乱七八糟的 bounding box。同时要记住这三个输出层的 blob 名字比如output、out0之类后面写推理代码时要用它去取结果。不同版本的 yolov5 导出后名字不一样一定要打开 param 文件确认别照抄别人的。改完 param 后建议用 ncnn 自带的ncnnoptimize再过一遍把能合并的层合并掉./ncnnoptimize yolov5s.param yolov5s.bin yolov5s-opt.param yolov5s-opt.bin 0到这里模型侧的产物就是yolov5s-opt.param和yolov5s-opt.bin可以拷到端侧设备了。4. 验证请求与逐层输出比对确认端侧推理结果正确模型转完不能直接信得验证。最稳的验证方式是「逐层比对」在 PC 上用 ONNX Runtime 跑一遍拿到中间层输出再用 ncnn 跑一遍对比同一层的数值。如果某一层开始偏差变大问题就出在那附近。先写一个最小的 ncnn 推理程序加载模型并打印输出#include opencv2/opencv.hpp #include net.h int main() { ncnn::Net net; net.opt.use_vulkan_compute false; net.load_param(yolov5s-opt.param); net.load_model(yolov5s-opt.bin); cv::Mat img cv::imread(test.jpg); ncnn::Mat in ncnn::Mat::from_pixels_resize( img.data, ncnn::Mat::PIXEL_BGR2RGB, img.cols, img.rows, 640, 640); const float mean_vals[3] {0.f, 0.f, 0.f}; const float norm_vals[3] {1/255.f, 1/255.f, 1/255.f}; in.substract_mean_normalize(mean_vals, norm_vals); ncnn::Extractor ex net.create_extractor(); ex.input(images, in); ncnn::Mat out; ex.extract(output, out); printf(out shape: w%d h%d c%d\n, out.w, out.h, out.c); return 0; }编译时链接 ncnn 和 OpenCVg yolov5_demo.cpp -o yolov5_demo \ -I/path/to/ncnn/build/install/include \ -L/path/to/ncnn/build/install/lib \ -lncnn -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_imgcodecs跑起来后先看输出 shape 对不对。yolov5s 在 640 输入下三个检测头的输出通常是(1, 255, 80, 80)、(1, 255, 40, 40)、(1, 255, 20, 20)这种形式ncnn 里会拆成 w/h/c 三个维度。如果 shape 完全不对说明 param 里输出层没改好。数值比对的话在 Python 侧用 onnxruntime 跑同一张图把对应层输出 dump 成 npy再在 C 侧把 ncnn 输出存成文本用脚本算最大绝对误差。正常情况下误差应该在 1e-3 量级以内。如果某层误差突然到 1e0基本就是那层的算子实现有差异重点查 Focus 和最后的 Concat。后处理对齐是另一个大坑。yolov5 的输出需要做 sigmoid、解码 xywh、再乘 stride最后 NMS。这套逻辑必须和训练时完全一致。建议直接把 yolov5 官方utils/general.py里的non_max_suppression逻辑翻译成 C别自己重写。letterbox 的缩放比例和 padding 也要记下来画框时再映射回原图坐标否则框的位置会整体偏移。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth端侧部署过程中报错往往不在模型本身而在环境。下面几个是我实际踩过的对照着看。local proxy failed一般出现在你通过某个本地服务去拉模型或依赖时本地端口没起来或者被占用。先确认服务进程在不在再看端口ss -tlnp | grep 你的端口如果是容器环境注意端口有没有映射出来。reading choices这类报错通常和配置文件解析有关比如 param 文件里某层输入输出数量对不上ncnn 读的时候解析失败。用文本编辑器打开 param逐行数一下每行的字段数重点看改过的那几层。层类型、层名、输入数、输出数、blob 名一个都不能少。401是鉴权失败多出现在你调用云端 API 做辅助验证、或者用在线服务拉权重时。检查你的 Key 是否过期、请求头有没有带上。如果你在用 TaoToken 这类聚合服务做模型对话或验证Key 要在控制台重新生成一次确认复制完整没有多余空格。OAuth相关报错一般出现在用 Claude Code 这类工具做辅助开发时授权回调没走通。这种情况优先检查回调地址和本地时间是否同步时间偏差过大会直接导致 token 校验失败。还有一个高频坑模型在 PC 上跑得好好的到 ARM 板上结果全错。这通常是输入预处理不一致。PC 上你可能用了from_pixels_resize自动做了 resize但端侧如果自己手写 resize 而没做 letterbox长宽比就变了。统一用 letterbox把缩放比例和 padding 记录下来后处理时再还原。排查顺序建议是先确认 param/bin 加载成功再确认输入 shape 和数值范围然后确认输出 shape最后确认后处理解码。一层层往下卡比盲目改代码高效得多。6. 端侧长期运行与 Coding Plan 接入建议模型跑通只是第一步真正上端侧还要考虑长期运行的稳定性。几个实用建议推理线程和采集线程分离用队列缓冲帧避免采集阻塞推理ncnn 的Extractor每次推理新建不要复用复用会有状态残留模型文件建议 mmap 加载减少启动时的内存拷贝。如果你后续要做的是持续迭代的编码类任务比如批量改推理代码、写后处理、对接不同硬件平台可以考虑用 Coding Plan 这类长期方案来辅助开发把重复的样板代码交给工具生成自己专注在前后处理对齐这种真正容易出错的地方。模型对话入口可以用来快速验证某个算子的数值行为接入文档里有完整的参数说明。端侧部署这条链路说到底就是「对齐」两个字输入对齐、输出对齐、后处理对齐。把这三处对齐了ncnn 上跑 yolov5 就能稳定复现 PC 上的结果。