1. 环境准备与模型获取大家好我是老张在AI和嵌入式这块摸爬滚打了十来年今天想和大家聊聊一个非常实际的话题怎么把当下最火的YOLOv8模型塞进海思的摄像头里跑起来。我知道很多朋友一听到“模型部署”、“嵌入式”这些词就有点发怵觉得门槛高、流程复杂。别担心我这次会把我自己踩过的坑、趟过的路掰开揉碎了讲给你听保证你跟着做就能跑通。咱们的目标很明确就是从YOLOv8官网下载一个现成的模型一路“护送”它最终在海思Hi3516或者Hi3519这类芯片的摄像头里完成实时的目标检测。首先咱们得把“战场”打扫干净准备好所有工具。这个过程有点像做菜前的备料料备齐了后面炒菜才顺手。你需要准备一台Linux开发机Ubuntu 18.04或20.04都行这是咱们进行模型转换的主战场。为什么不用Windows因为后面用到的一些工具链比如海思的Aistudio对Linux的支持更友好坑也少一些。接下来是软件“三件套”Python环境我强烈建议使用Anaconda来管理一个独立的Python环境避免和系统自带的Python打架。创建一个新环境比如叫nniePython版本用3.8比较稳妥。conda create -n nnie python3.8 conda activate nniePyTorch和UltralyticsYOLOv8官方模型是用PyTorch写的所以我们需要安装PyTorch。去PyTorch官网根据你的CUDA版本选择安装命令。如果没有GPU就安装CPU版本。然后安装Ultralytics库这是YOLOv8的官方娘家。pip install torch torchvision torchaudio # 请根据官网命令调整 pip install ultralytics安装完后你可以试试yolo命令看看是否成功。模型文件这就是咱们的“主菜”了。打开终端用一行命令就能把官方的预训练模型请下来。咱们先从最小的yolov8n.ptnano版本开始它速度快适合在资源受限的嵌入式端做验证。yolo export modelyolov8n.pt formatonnx # 这其实会同时下载模型并导出ONNX但我们先只关心下载运行后模型文件通常会下载到当前用户目录下的某个位置。我更习惯手动操作去Ultralytics的GitHub仓库下载源码解压后在根目录下新建一个weights文件夹然后把从官网下载的.pt权重文件放进去。这样文件路径清晰后续脚本好调用。最后别忘了海思的“法宝”——Aistudio开发工具。这个需要从海思的官方渠道获取通常是和芯片SDK一起提供的。它的主要作用就是把我们转换好的中间模型Caffe编译成海思NNIE硬件加速器能直接吃的“粮食”。确保你手上的Aistudio版本和你的目标摄像头芯片比如Hi3516DV300、Hi3519AV101匹配这点至关重要版本不对会直接导致编译失败。2. 模型转换第一步PyTorch到ONNX拿到官方的.pt文件后咱们不能直接用它。海思的NNIE不认识PyTorch模型它认识的是Caffe格式。所以我们需要一座“桥梁”先把PyTorch转成ONNX再转成Caffe。ONNX就像一个通用的翻译官几乎所有的AI框架都能通过它来交流。直接用Ultralytics自带的export功能可以很方便地导出ONNX。但是这里有个大坑我踩过必须提醒你直接导出的ONNX模型输入输出张量的名字是随机的这会给后续海思工具链的配置带来巨大麻烦。海思的NNIE配置文件中需要明确指定输入和输出节点的名称如果每次导出名字都变配置就得跟着改非常不自动化。所以我们需要一个“定制化”的导出脚本。这个脚本要做两件事一是导出ONNX二是把输入输出节点的名字改成我们固定的、好记的名字。我在项目根目录下创建一个export_for_nnie.py文件内容如下# encoding:utf-8 import onnx from ultralytics import YOLO import argparse # 设置参数方便灵活指定不同模型 parser argparse.ArgumentParser() parser.add_argument(--weights, typestr, default./weights/yolov8n.pt, help初始权重路径) opt parser.parse_args() print(f正在处理模型: {opt.weights}) # 1. 加载官方模型并导出ONNX model YOLO(opt.weights) # 关键参数opset12算子集版本simplifyTrue简化模型dynamicFalse固定输入尺寸 success model.export(formatonnx, opset12, simplifyTrue, dynamicFalse, imgsz640) assert success, ONNX导出失败 # 2. 重命名输入输出节点 onnx_model_path opt.weights.replace(.pt, .onnx) model onnx.load(onnx_model_path) # 将唯一的输入节点名称改为固定的“data” for input_node in model.graph.input: old_input_name input_node.name for node in model.graph.node: for i, name in enumerate(node.input): if name old_input_name: node.input[i] data input_node.name data # 将多个输出节点名称改为固定的“out0”, “out1”, “out2”... output_idx 0 for output_node in model.graph.output: old_output_name output_node.name for node in model.graph.node: for i, name in enumerate(node.output): if name old_output_name: node.output[i] fout{output_idx} output_node.name fout{output_idx} output_idx 1 # 保存修改后的模型我习惯加个_nnie后缀以示区别 new_model_path onnx_model_path.replace(.onnx, _nnie.onnx) onnx.save(model, new_model_path) print(fNNIE专用ONNX模型已保存至: {new_model_path})运行这个脚本python export_for_nnie.py --weights ./weights/yolov8n.pt如果一切顺利你会在weights文件夹里看到一个yolov8n_nnie.onnx文件。用Netron一个可视化神经网络结构的工具打开它你会发现输入节点名字稳稳地叫data输出节点依次是out0out1out2。这一步的标准化为后续所有流程扫清了障碍切记。3. 模型转换第二步ONNX到Caffe现在我们有了一个“规整”的ONNX模型。下一步是把它转换成Caffe模型。为什么是Caffe因为海思NNIE的工具链最成熟、支持最完善的框架就是Caffe。虽然现在也逐步支持一些其他格式但Caffe依然是最稳妥的选择。转换需要用到onnx2caffe这个工具。网上有很多开源版本但经过我实测很多对YOLOv8新算子的支持不够好容易在转换诸如Upsample上采样、Split分割等节点时出错。我推荐使用一个经过社区验证的、支持自定义层的版本。你可以从GitHub上搜索onnx2caffe找一些高星项目。假设你已经把onnx2caffe的代码克隆到本地它的使用方式通常是python convertCaffe.py --onnx_path ./weights/yolov8n_nnie.onnx --caffe_prototxt_path ./weights/yolov8n.prototxt --caffe_model_path ./weights/yolov8n.caffemodel这个过程可能会遇到一些算子不支持的报错。别慌这是常态。常见的解决办法有修改ONNX模型有时候ONNX模型里包含了一些非常规的算子组合。我们可以用ONNX Runtime或者ONNX Simplifier再对模型做一次优化和简化有时能自动解决。自定义Caffe层对于onnx2caffe工具不支持的算子我们需要在Caffe框架中实现它的自定义层。这需要一定的C和Caffe源码知识。例如YOLOv8中的Upsample操作你可能需要在Caffe的layer_factory中注册一个自己的UpsampleLayer并实现它的前向传播逻辑。这个过程比较硬核但好在网上针对YOLO系列模型的转换通常能找到现成的自定义层实现参考。使用替代方案如果某个算子实在搞不定可以思考一下模型结构。有没有可能用一组Caffe支持的算子比如Deconvolution卷积来等效替换这个不支持的算子这需要对模型结构有较深的理解。转换成功后你会得到两个关键文件yolov8n.prototxt和yolov8n.caffemodel。前者是模型的结构定义像建筑的图纸后者是训练好的权重参数像建筑用的砖瓦。用海思Aistudio工具打开.prototxt文件如果能正常显示网络结构图没有红色的错误节点那恭喜你最难啃的骨头之一已经拿下了。4. 海思Aistudio配置与NNIE模型生成拿到了Caffe模型就来到了海思的主场。打开Aistudio工具新建一个NNIE转换工程。这里有几个关键配置点我一个个说。首先是SOC版本选择这一步绝对不能错。你的工程要和你最终部署的摄像头芯片型号严格对应。比如你是Hi3516DV300就选Hi3516DV300的配置。选错了会导致编译出的模型在板子上根本无法加载。其次是导入模型文件在工程中将我们上一步生成的yolov8n.prototxt和yolov8n.caffemodel导入。Aistudio会自动解析网络结构。然后是配置.cfg文件这是NNIE转换的核心配置文件。你需要用文本编辑器如VSCode打开它重点关注以下几项image_list指向一个文本文件里面写着一系列校准图片的路径。这些图片用于在转换时做量化校准如果你的模型是浮点型需要转为定点型以在NNIE上高效运行。图片最好来自你实际应用的场景比如都是道路、室内的图片这样量化误差更小。data_format输入图片的格式通常是RGB或BGR要和你的模型训练时以及摄像头输出的格式对齐。input_data输入节点的名字这里填我们之前固定好的data。output_node输出节点的名字这里填out0,out1,out2根据你的实际输出数量填写用逗号分隔。max_input_num通常是1。norm_type数据归一化方式。YOLO模型通常不需要做减均值除方差的归一化norm_type0但有时训练时做了预处理这里就需要相应配置。一个简化版的.cfg文件内容看起来是这样的[prototxt_file] ./yolov8n.prototxt [caffemodel_file] ./yolov8n.caffemodel [image_list] ./calib.txt [batch_num] 1 [data_format] RGB [input_data] data [output_node] out0,out1,out2 [norm_type] 0 [mean_value] 0 [scale_value] 1配置好后在Aistudio界面点击“运行”或“转换”按钮。工具会开始解析模型、进行量化如果选择了定点、编译生成NNIE专用的模型文件。这个过程如果出错日志信息会非常详细通常是配置文件有误、模型有不支持的层、或者校准图片有问题。需要耐心根据日志排查。转换成功的关键标志是你在输出目录下得到了一个.wk文件比如yolov8n.wk。这个.wk文件就是最终可以加载到海思芯片内存中由NNIE硬件加速器执行的模型文件。拿到它模型转换的万里长征就算走完了90%。5. 嵌入式端加载与推理实战模型转换好了接下来就是真刀真枪地在摄像头里跑了。我们需要在海思芯片的嵌入式Linux系统上编写C/C代码来加载.wk文件并执行推理。海思提供了完整的NNIE API通常叫hi_nnie接口这些API封装在libnnie.so这样的动态库里。我们的程序需要链接这个库。核心流程可以分为四步第一步初始化与加载模型#include hi_nnie.h // ... 其他头文件 SAMPLE_SVP_NNIE_MODEL_S stModel; // 模型结构体 HI_CHAR* pszModelFile ./yolov8n.wk; // wk模型路径 // 1. 初始化NNIE模块 HI_MPI_SVP_NNIE_Init(); // 2. 从文件加载模型 HI_S32 s32Ret HI_MPI_SVP_NNIE_LoadModel(stModel, pszModelFile); if (s32Ret ! HI_SUCCESS) { printf(Load model failed! Error code: %#x\n, s32Ret); return -1; }这一步会把模型从Flash加载到DDR内存中并解析出网络结构信息。第二步准备输入输出内存NNIE硬件需要操作物理上连续的内存块。海思提供了HI_MPI_SYS_MmzAlloc等函数来分配这种内存。SAMPLE_SVP_NNIE_PARAM_S stParam; // 推理参数结构体 SVP_SRC_BLOB_S stSrcBlob; // 输入数据Blob SVP_DST_BLOB_S stDstBlob[3]; // 输出数据Blob数组对应3个输出层 // 根据模型信息为输入输出分配内存 // 输入通常是一张图片尺寸为 1x3x640x640 (Batch x Channel x Height x Width) s32Ret SAMPLE_COMM_SVP_MallocMem(stSrcBlob.u64PhyAddr, stSrcBlob.u64VirAddr, stModel.astSeg[0].astSrcNode[0].u32Dim); // 输出为每一个输出层分配内存 for (i 0; i stModel.u32NetSegNum; i) { for (j 0; j stModel.astSeg[i].u16DstNum; j) { s32Ret SAMPLE_COMM_SVP_MallocMem(stDstBlob[j].u64PhyAddr, stDstBlob[j].u64VirAddr, stModel.astSeg[i].astDstNode[j].u32Dim); } }分配好后我们需要把从摄像头采集到的、经过预处理缩放、格式转换的图像数据拷贝到stSrcBlob.u64VirAddr这个虚拟地址指向的内存中。第三步执行前向推理这是最核心的一步调用NNIE硬件加速计算。// 填充推理参数结构体 stParam.astSrcData stSrcBlob; stParam.astDstData stDstBlob; stParam.u32TmpBufSize stModel.u32TmpBufSize; // 临时缓冲区大小从模型加载时获得 // 执行推理 s32Ret HI_MPI_SVP_NNIE_Forward(stModel, stParam, HI_TRUE); if (s32Ret ! HI_SUCCESS) { printf(NNIE Forward failed! Error code: %#x\n, s32Ret); }HI_TRUE参数表示阻塞式调用函数会等待NNIE计算完成才返回。在实际产品中为了提升吞吐量可能会使用非阻塞模式配合回调函数。第四步解析输出与后处理推理完成后结果就存放在stDstBlob数组对应的内存里。对于YOLOv8它的输出和YOLOv5等版本不同不再是三个尺度的特征图直接输出框和分数而是输出更“干净”的张量需要我们自己编写后处理代码来解码。以yolov8n为例它通常有三个输出out0 out1 out2对应三个不同尺度的特征图。每个输出的形状可能是[1, 64, 80, 80][1, 64, 40, 40][1, 64, 20, 20]。这里的64包含了框的坐标4维、置信度1维和80个类别的概率对于COCO数据集是80维总共85维。后处理的过程包括从输出张量中解析出每个网格点预测的85维向量。应用Sigmoid函数得到置信度和类别概率。根据预设的锚框Anchor或者直接解码YOLOv8是无锚框的直接预测中心点偏移和宽高计算出边界框在原始图像上的坐标。应用非极大值抑制NMS过滤掉重叠的、低置信度的框。这部分C代码逻辑相对复杂需要仔细对照YOLOv8官方Python推理代码的逻辑进行移植确保解码规则一致。这也是嵌入式AI部署中最体现功力的地方之一。最后别忘了释放资源// 释放输入输出内存 SAMPLE_COMM_SVP_FreeMem(stSrcBlob.u64PhyAddr, stSrcBlob.u64VirAddr); for (i 0; i 3; i) { SAMPLE_COMM_SVP_FreeMem(stDstBlob[i].u64PhyAddr, stDstBlob[i].u64VirAddr); } // 卸载模型 HI_MPI_SVP_NNIE_UnloadModel(stModel); // 去初始化 HI_MPI_SVP_NNIE_Deinit();把以上代码整合到一个完整的样例程序中交叉编译使用海思提供的arm-himix200-linux-gcc等工具链将可执行文件和.wk模型文件一起放到摄像头文件系统中运行起来。当你从串口日志中看到推理耗时例如“forward time: 45ms”并且能正确打印出检测到的物体类别和坐标时那种成就感是无与伦比的。这意味着一个最先进的检测模型已经在一个小小的、功耗仅几瓦的摄像头芯片里“安家落户”开始真正发挥作用了。整个过程虽然繁琐但每一步都有迹可循遇到问题多查海思官方文档、多利用社区资源总能找到解决方案。