当前位置: 首页 > news >正文

海思Hi3519A/Hi3559A上YOLOv5端侧检测实战工程:含训练、转模型、Caffe推理与完整编译部署

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

简介:直接在海思Hi3519A、Hi3559A等AI芯片开发板上跑通YOLOv5目标检测的落地工程包,覆盖从零搭建PyTorch训练环境(InstallYolov5TrainEnv.sh)、适配Caffe推理框架(InstallCaffeEnv.sh)、将PyTorch模型导出并转换为Caffe格式(convertCaffe.py)、修改后处理逻辑(附两张关键代码修改截图)、编写设备端C++推理代码(hisi目录)、配置Makefile编译规则(Makefile.config等)到最终加载last.pt模型实现实时检测的全流程。所有步骤已在真实海思开发板验证,配套README.md逐条说明,提供test.jpg一键测试。不依赖云服务,纯本地边缘部署,环境依赖明确写入requirements.txt和Shell脚本,兼容主流海思SDK版本,无商业授权限制,专为嵌入式AI学习、课程设计和毕设实践优化。

1. 项目概述:为什么要在海思芯片上硬刚YOLOv5?

你手头有一块Hi3559A开发板,或者正为课程设计发愁——导师说“做点有实际部署价值的AI项目”,但一查资料,满屏都是“YOLOv5 + Jetson Nano”“YOLOv8 + RK3588”,再翻海思生态,文档里全是“支持TensorFlow Lite”“兼容ONNX”,可没人告诉你:怎么把一个在PyTorch里训好的YOLOv5模型,真正烧进Hi3559A的NNIE硬件加速单元里跑起来?不是模拟器、不是QEMU仿真、不是靠CPU软推理撑场面,而是让/dev/nnie设备节点真实吐出检测框,帧率稳定在25fps以上,功耗压在3W以内。这个工程包,就是我踩了三个月坑、重刷七次SDK、改烂四版后处理逻辑后,交出的“能拧螺丝、能测电流、能接摄像头、能交毕设”的硬核答案。

它不是教程,不是Demo,而是一套可直接抄作业的端侧落地工程。关键词“海思芯片,YOLOv5部署,Caffe推理,模型转换,边缘检测”不是标签,是每个字都对应一行实操代码、一次编译报错、一个寄存器配置。比如“Caffe推理”——海思官方NNIE只认Caffe模型(.prototxt+.caffemodel),但YOLOv5原生是PyTorch,中间必须过一道“模型转换”,而convertCaffe.py不是简单调用torch.onnx.export就完事:它要手动重建YOLOv5的Focus层(海思不支持Slice+Concat组合)、重写SPPF结构为等效卷积组、把Upsample替换为Deconvolution并固化scale参数——这些细节,官方文档一页没提,但你的模型跑不起来,90%卡在这儿。

再比如“边缘检测”——不是指图像处理里的Canny算子,而是真正在无网络、无GPU、无Python解释器的嵌入式环境里,用C++调用NNIE驱动,从/dev/video0读YUV422帧,送进硬件加速器,再把16个浮点数输出结果(x,y,w,h,conf,class_id×5)解析成像素坐标,最后用OpenCV的cv::rectangle画框回传到HDMI显示。整个链路没有一行Python,没有一次内存拷贝冗余,所有buffer都按海思要求对齐到128字节边界。hisi/目录下的main.cpp里,SAMPLE_COMM_NNIE_FillSrcData函数填的是物理地址,不是虚拟地址;SAMPLE_COMM_NNIE_GetResult返回的是HI_S32*指针,不是std::vector<float>——这才是边缘的真实手感。

这套方案专为三类人准备:一是嵌入式AI初学者,想甩开Jupyter Notebook,亲手摸一摸make install之后/usr/lib里多出来的libnnie.so;二是课程设计/毕设党,需要一份能答辩、能演示、能写进论文“实验平台”章节的完整工程;三是产线工程师,评估海思方案可行性时,拿test.jpg跑通./yolov5_demo -i test.jpg,5分钟内看到终端打印出[INFO] Detect 3 objects: person(0.92), car(0.87), dog(0.75),心里就有底了。它不碰云端,不谈微服务,不聊Kubernetes,就守着一块开发板、一根串口线、一个万用表,把AI算法钉死在硅片上。

2. 整体架构与技术选型逻辑:为什么是Caffe而不是ONNX或TFLite?

2.1 海思NNIE的“铁律”与生态现实

很多人第一反应是:“YOLOv5不是支持ONNX导出吗?海思SDK不是有nnie_convert工具?”——这想法很合理,但现实是:Hi3519A/Hi3559A的NNIE硬件加速单元,其固件(firmware)和驱动(ko模块)仅深度适配Caffe框架的算子集。你拿ONNX模型喂给nnie_convert,它能转出.wk文件,但运行时大概率触发HI_ERR_NNIE_PARAM_INVALID错误。原因在于:NNIE的硬件调度器(Scheduler)只识别Caffe定义的ConvolutionParameterPoolingParameter等结构体,而ONNX的Conv算子缺少pad_mode字段的硬件映射,导致padding行为与预期不符。我试过用onnx-simplifier强行规约ONNX图,也试过用onnxmltools插入自定义op,最终在SAMPLE_COMM_NNIE_Forward函数里断点发现:输入feature map尺寸对不上,第3层卷积的output height比理论值少1——这就是padding未被硬件正确解析的铁证。

所以,技术选型的第一条铁律是:绕过抽象层,直面硬件约束。Caffe虽老,但它的.prototxt文本格式清晰暴露每一层的参数(kernel_size: 3stride: 2pad: 1),而NNIE驱动正是逐行解析这些字段生成硬件指令流。InstallCaffeEnv.sh脚本里编译的不是通用Caffe,而是海思定制版Hi3559AV100_Caffe,它在src/caffe/layers/下新增了nnie_conv_layer.cpp,专门处理NNIE特有的权重量化方式(INT8权重重排为C×K×H×W格式)。这个细节决定了:你不能用pip install caffe,必须用SDK包里的3rdparty/caffe源码重新编译。

2.2 YOLOv5模型改造:不是转换,是“手术式重构”

YOLOv5的PyTorch实现里藏着三个NNIE无法消化的“毒瘤”:

  1. Focus层yolov5s.yaml里的- [Focus, [3, 32, 3]],本质是torch.nn.functional.unfold操作,将4×H×W输入切分为4个H/2×W/2子图再拼接。NNIE没有对应算子,convertCaffe.py的解法是:用4个CropLayer+ConcatLayer替代。具体操作是,在prototxt中先定义crop_param { axis: 1 offset: 0 }裁出第0、1、2、3个channel,再用concat_param { axis: 1 }合并——这要求输入tensor的channel数必须是4的倍数,所以训练时--img 640必须确保640能被4整除(640÷4=160,OK)。

  2. SPPF层- [SPPF, [512, 5]],即MaxPool三次叠加。NNIE的PoolingParameter只支持单次pooling,convertCaffe.py将其拆解为三个独立PoolingLayer,并手动计算每层的pad值。例如输入尺寸160×160,第一次pool: 5, stride: 1, pad: 2输出160×160,第二次同理,第三次后尺寸不变——这需要在prototxt里精确写出pooling_param { kernel_size: 5 stride: 1 pad: 2 },少一个pad,硬件就会越界访问。

  3. Upsample层:YOLOv5用nn.Upsample(scale_factor=2)做上采样,但NNIE不支持动态scale。convertCaffe.py强制替换为DeconvolutionLayer,并固化scale_factor=2kernel_size: 4, stride: 2, pad: 1(满足output = (input-1)*stride - 2*pad + kernel_size)。这里有个隐藏陷阱:Deconvolution的权重初始化必须用双线性插值核,否则上采样结果全是噪点。convertCaffe.py里调用cv2.resize生成初始化权重,并存为deconv_weight.bin供C++加载。

提示:convertCaffe.py不是黑盒脚本。它核心逻辑只有三步:① 用torch.jit.trace获取PyTorch模型的静态计算图;② 遍历图节点,匹配YOLOv5特有结构(如Focus前的slice操作);③ 生成.prototxt文本和.caffemodel二进制权重。你可以在demo_run.py里加print(model.graph)看原始图结构,再对比convertCaffe.py输出的yolov5s.prototxt,就能理解每行layer { type: "Convolution" }是怎么来的。

2.3 推理引擎分层:C++驱动层 + NNIE硬件层 + 后处理业务层

整个推理流程不是“一个main函数跑到底”,而是严格分三层:

  • C++驱动层hisi/main.cpp):负责与Linux内核交互。调用HI_MPI_SYS_Init()初始化系统,HI_MPI_VI_SetDevAttr()配置视频输入设备,最关键的是HI_MPI_NNIE_CreateGroup()创建NNIE计算组——这一步会分配DMA buffer,其物理地址由HI_MPI_SYS_MmzAlloc()申请,大小必须是128字节对齐。我曾因malloc申请buffer导致HI_ERR_NNIE_ILLEGAL_PARAM,换成HI_MPI_SYS_MmzAlloc()立刻解决。

  • NNIE硬件层(SDK内部):接收驱动层传入的物理地址,启动硬件加速器。SAMPLE_COMM_NNIE_Forward函数本质是ioctl调用,向/dev/nnie设备发送命令帧。注意:NNIE一次只能处理一个batch,YOLOv5的batch_size=1是硬性要求,convertCaffe.py生成的prototxtinput_shape必须写死为[1, 3, 640, 640]

  • 后处理业务层hisi/sample_comm_nnie_yolov5.c):这是最易出错的部分。NNIE输出的是16个float数组(对应YOLOv5的3个head),每个数组包含num_boxes × 6个值(x,y,w,h,obj_conf,class_conf)。但海思SDK的SAMPLE_COMM_NNIE_Yolov3_PostProcess函数是为YOLOv3写的,直接套用会把YOLOv5的anchor匹配逻辑搞错。所以工程包里提供了两张修改截图:yolov5后处理代码修改-1.png重点改GetBbox函数,用YOLOv5的sigmoid(x)替代YOLOv3的exp(x)yolov5后处理代码修改-2.png重写了NMS逻辑,把IOU阈值从0.45改为0.5,并增加class-aware NMS(同类框才抑制,避免person框吃掉dog框)。

这种分层不是炫技,而是为了调试可控。当检测框漂移时,先确认C++层HI_MPI_NNIE_Forward返回HI_SUCCESS,证明硬件没问题;再检查后处理层输入的HI_S32*数据是否全为0(若是,说明权重没加载对);最后用printf打印前10个输出值,对照PyTorch推理结果验证数值一致性。每一层都有明确的成败标志,拒绝“玄学调试”。

3. 核心环节详解与实操步骤:从训练环境搭建到板端实时检测

3.1 训练环境搭建:InstallYolov5TrainEnv.sh的深层逻辑

InstallYolov5TrainEnv.sh表面是装PyTorch,实则是一场与CUDA版本的精密博弈。Hi3559A SDK(如Hi3559AV100_SDK_V2.0.2.0)要求Ubuntu 16.04系统,而该系统默认CUDA版本是9.1。但YOLOv5官方推荐CUDA 11.x,直接pip install torch==1.10.0+cu113会因GLIBC版本冲突失败。脚本的解法是:降级YOLOv5,选用兼容CUDA 9.1的PyTorch 1.4.0

# InstallYolov5TrainEnv.sh 关键片段 sudo apt-get install -y python3-pip python3-dev pip3 install --upgrade pip pip3 install torch==1.4.0+cu92 torchvision==0.5.0+cu92 -f https://download.pytorch.org/whl/torch_stable.html pip3 install -r requirements.txt # 包含opencv-python==4.5.1.48, numpy==1.19.5 git clone https://github.com/ultralytics/yolov5.git cd yolov5 && git checkout v3.1 # YOLOv5 v3.1 是最后一个支持PyTorch 1.4的版本

为什么选v3.1?因为v4.0开始强制要求PyTorch 1.7+,而1.7依赖CUDA 10.2。v3.1的models/yolo.pyDetect层代码更简洁,forward函数只有37行,便于我们后续修改Focus层。requirements.txt里锁死numpy==1.19.5而非>=1.19.0,是因为NumPy 1.20+引入了__array_function__协议,与海思SDK的hi_mpi_sys.h头文件里的宏定义冲突,导致编译hisi/代码时出现error: ‘NPY_ARRAY_C_CONTIGUOUS’ undeclared

训练时的关键参数不是--batch-size 16,而是--img 640 --device 0 --workers 2--img 640确保输入尺寸被4整除(适配Focus层),--workers 2避免Ubuntu 16.04的multiprocessing库bug(workers>2时Dataloader卡死)。训练完成后,last.pt模型需用demo_run.py验证:

# demo_run.py 片段:验证PyTorch模型输出与Caffe转换一致性 import torch model = torch.load('last.pt', map_location='cpu')['model'].float() model.eval() img = cv2.imread('test.jpg') img = cv2.resize(img, (640, 640)) img_tensor = torch.from_numpy(img.transpose(2,0,1)).float().unsqueeze(0) / 255.0 pred = model(img_tensor) # 输出3个tensor,shape分别为[1,3,80,80,85]等 print("PyTorch output shape:", [p.shape for p in pred])

输出应为[torch.Size([1, 3, 80, 80, 85]), torch.Size([1, 3, 40, 40, 85]), torch.Size([1, 3, 20, 20, 85])],这与convertCaffe.py生成的Caffe模型输出blob数量一致。若shape不符,说明模型结构修改有误。

3.2 Caffe环境构建:InstallCaffeEnv.sh的避坑指南

InstallCaffeEnv.sh不是简单make && make install,它有三个致命细节:

  1. 编译器版本锁定:Ubuntu 16.04默认gcc 5.4,但海思SDK的toolchain要求gcc 4.9.4。脚本中export CC=/opt/hisi-linux/x86-arm/arm-hisiv500-linux/target/bin/arm-hisiv500-linux-gcc必须指向SDK自带的交叉编译器,而非系统gcc。否则编译出的libcaffe.so会在板端报undefined symbol: __cxa_throw(C++异常处理符号缺失)。

  2. BLAS库替换:官方Caffe用OpenBLAS,但NNIE加速依赖海思定制的libnnieblas.so。脚本中sed -i 's/OPENBLAS/NNIEBLAS/g' Makefile.config将BLAS类型改为NNIE,并在Makefile.config里添加:
    LIBRARIES := glog gflags protobuf leveldb snappy lmdb boost_system boost_filesystem m hdf5_hl hdf5 nniefw nnieblas INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include /opt/hisi-linux/x86-arm/arm-hisiv500-linux/target/usr/include/hdf5

  3. Python接口禁用Makefile.configWITH_PYTHON_LAYER := 0必须为0。因为板端没有Python环境,且启用Python层会导致libcaffe.so依赖libpython3.5m.so,而SDK根文件系统里没有此文件。我曾开启此选项,make install成功,但板端dlopen时报libpython3.5m.so: cannot open shared object file,折腾两天才发现是此处开关。

编译完成后,验证Caffe是否生效:

cd $CAFFE_ROOT build/tools/caffe device_query -gpu all # 应输出"device 0: Hi3559A NNIE" build/examples/cpp_classification/classification.bin \ models/yolov5s.prototxt \ models/yolov5s.caffemodel \ data/ilsvrc12/imagenet_mean.binaryproto \ data/ilsvrc12/synset_words.txt \ examples/images/cat.jpg

若输出cat (0.92),证明Caffe环境正确;若报Check failed: error == cudaSuccess (30 vs. 0) unknown error,说明GPU设备查询失败,需检查HI_MPI_SYS_Init()是否在classification.bin里被调用。

3.3 模型转换实战:convertCaffe.py的逐行解析

convertCaffe.py是整个工程的“心脏起搏器”,其核心逻辑可拆解为五步:

Step 1:模型冻结与trace

model = torch.load('last.pt', map_location='cpu')['model'].float() model.eval() dummy_input = torch.randn(1, 3, 640, 640) traced_model = torch.jit.trace(model, dummy_input) # 生成静态图

注意:必须用torch.jit.trace而非torch.jit.script,因为YOLOv5的Detect.forward含条件分支(如if self.training),script会报错。

Step 2:Focus层替换

# 在traced_model.graph里找到所有"aten::slice"节点 for node in traced_model.graph.nodes(): if node.kind() == "aten::slice": # 检查是否为Focus的切片操作(输入channel=3,输出channel=12) if node.input().type().sizes()[1] == 3 and node.output().type().sizes()[1] == 12: # 插入CropLayer节点 crop_node = traced_model.graph.create("Crop", [node.input(), node.output()]) traced_model.graph.append(crop_node)

Step 3:SPPF层展开

# 找到SPPF的MaxPool节点(kernel_size=5) for node in traced_model.graph.nodes(): if node.kind() == "aten::max_pool2d" and node.input().type().sizes()[2] == 5: # 替换为三个独立MaxPool for i in range(3): pool_node = traced_model.graph.create("aten::max_pool2d", [node.input()]) pool_node.addInput(node.input()) pool_node.i_("kernel_size", 5) pool_node.i_("stride", 1) pool_node.i_("padding", 2) # pad=2保证尺寸不变 traced_model.graph.append(pool_node)

Step 4:生成prototxt

with open("yolov5s.prototxt", "w") as f: f.write("name: \"YOLOv5s\"\n") f.write("input: \"data\"\n") f.write("input_shape {\n dim: 1\n dim: 3\n dim: 640\n dim: 640\n}\n") # 遍历traced_model.graph,为每个节点生成layer定义 for i, node in enumerate(traced_model.graph.nodes()): if node.kind() == "aten::conv2d": f.write(f"layer {{\n name: \"conv_{i}\"\n type: \"Convolution\"\n") f.write(f" convolution_param {{ kernel_size: {kernel_size} stride: {stride} pad: {pad} }}\n}}\n")

Step 5:权重提取与保存

# 从PyTorch模型state_dict提取权重 state_dict = torch.load('last.pt', map_location='cpu')['model'].state_dict() for name, param in state_dict.items(): if 'conv.weight' in name: # 转为NNIE要求的C×K×H×W格式 weight = param.data.numpy().transpose(0,2,3,1) # K×H×W×C -> C×K×H×W weight.tofile(f"weights/{name.replace('.', '_')}.bin")

执行python convertCaffe.py后,生成yolov5s.prototxtyolov5s.caffemodel。用grep "Convolution" yolov5s.prototxt | wc -l应输出25(YOLOv5s共25个卷积层),若少于25,说明Focus或SPPF替换失败。

3.4 板端C++推理:hisi目录代码的硬件级解读

hisi/目录下的代码不是普通C++,而是与海思硬件寄存器对话的“汇编级C++”。以main.cpp关键段为例:

// 分配NNIE输入buffer(物理地址!) HI_S32 s32Ret = HI_MPI_SYS_MmzAlloc(&u64PhyAddr, &pu8VirtAddr, HI_ID_NNIE, NULL, u32Size, HI_MMZ_USER_LOCAL); // pu8VirtAddr是虚拟地址,u64PhyAddr是物理地址,NNIE只认后者 // 填充YUV422数据到buffer(注意YUV422是packed格式,每像素2字节) SAMPLE_COMM_VI_GetFrame(ViChn, &stFrame, s32MilliSec); memcpy(pu8VirtAddr, stFrame.pVirAddr[0], u32Size); // 直接拷贝YUV数据 // 构建NNIE输入数据结构 stNnieInput.astSrc[0].u64PhyAddr = u64PhyAddr; // 必须传物理地址! stNnieInput.astSrc[0].u32Width = 640; stNnieInput.astSrc[0].u32Height = 640; stNnieInput.astSrc[0].u32Stride = 640 * 2; // YUV422 stride = width * 2 // 启动NNIE推理 s32Ret = HI_MPI_NNIE_Forward(&stNnieInput, &stNnieOutput, HI_TRUE);

这里HI_MPI_SYS_MmzAlloc分配的内存必须是连续物理内存,因为NNIE DMA控制器需要物理地址。若用malloc,即使posix_memalign对齐,也可能是虚拟连续、物理离散,导致DMA传输乱码。stNnieInput.astSrc[0].u32Stride = 640 * 2是YUV422的硬性要求(每个像素占2字节),若填640,硬件会把Y和U/V数据错位读取。

后处理部分,sample_comm_nnie_yolov5.c里的SAMPLE_COMM_NNIE_Yolov5_PostProcess函数,核心是解析NNIE输出的HI_S32*指针:

// NNIE输出是int32_t数组,需转为float(NNIE内部用INT8量化,输出需反量化) HI_S32 *pDstRoiScore = (HI_S32*)pstNnieOutput->astDst[0].u64VirAddr; HI_S32 *pDstRoiBox = (HI_S32*)pstNnieOutput->astDst[1].u64VirAddr; // 反量化:NNIE输出 = (float_value * scale) + zero_point // scale和zero_point来自convertCaffe.py生成的quant_param.bin float scale = 0.00392156862745; // 1/255 for (int i = 0; i < num_boxes; i++) { float conf = (float)pDstRoiScore[i * 6 + 4] * scale; // obj_conf float cls_conf = (float)pDstRoiScore[i * 6 + 5] * scale; // class_conf if (conf * cls_conf > 0.5) { // 置信度阈值 // 解析bbox:pDstRoiBox[i*6 + 0] ~ [i*6 + 3] float x = (float)pDstRoiBox[i*6 + 0] * scale; float y = (float)pDstRoiBox[i*6 + 1] * scale; float w = (float)pDstRoiBox[i*6 + 2] * scale; float h = (float)pDstRoiBox[i*6 + 3] * scale; // 转为像素坐标(YOLOv5输出是归一化坐标) int x1 = (x - w/2) * 1920; // 假设显示分辨率为1920×1080 int y1 = (y - h/2) * 1080; int x2 = (x + w/2) * 1920; int y2 = (y + h/2) * 1080; cv::rectangle(stImage, cv::Point(x1,y1), cv::Point(x2,y2), CV_RGB(255,0,0), 2); } }

这段代码里,pDstRoiScorepDstRoiBox是NNIE硬件直接写入的内存,HI_S32*指针指向的是物理内存映射的虚拟地址。scale = 0.00392156862745是INT8量化的反量化系数(1/255),因为NNIE内部用INT8存储浮点数,输出需还原。若此处用错scale,检测框会全部偏移或缩放失真。

3.5 编译与部署:Makefile.config的硬件参数映射

Makefile.config不是普通Makefile,它是硬件资源的“宪法”。关键配置项解读:

# 指定交叉编译工具链 CROSS_COMPILE := /opt/hisi-linux/x86-arm/arm-hisiv500-linux/target/bin/arm-hisiv500-linux- # NNIE硬件参数(必须与SDK版本匹配) NNIE_SDK_PATH := /opt/hisi-linux/x86-arm/arm-hisiv500-linux/target INCLUDE += -I$(NNIE_SDK_PATH)/usr/include/nnie LIBS += -L$(NNIE_SDK_PATH)/usr/lib -lnnie -lnniefw -lnnieblas # 内存对齐要求(NNIE DMA要求128字节对齐) CFLAGS += -march=armv7-a -mfpu=neon -mfloat-abi=softfp -fPIC CFLAGS += -D__HI3559A__ -DALIGN_SIZE=128 # 链接时强制保留符号(避免优化删除后处理函数) LDFLAGS += -Wl,--undefined=HI_MPI_NNIE_Forward

-DALIGN_SIZE=128定义了内存对齐宏,所有malloc需替换为aligned_alloc(128, size)-Wl,--undefined=HI_MPI_NNIE_Forward是链接器指令,确保HI_MPI_NNIE_Forward符号不被优化掉,否则运行时报undefined symbol

编译命令:

make clean && make -j4 # 生成yolov5_demo可执行文件 # 复制到板端:scp yolov5_demo root@192.168.1.10:/mnt/ # 板端运行:./yolov5_demo -i /mnt/test.jpg -o /mnt/out.jpg

若报./yolov5_demo: error while loading shared libraries: libnnie.so: cannot open shared object file,说明LD_LIBRARY_PATH未设置:

export LD_LIBRARY_PATH=/usr/lib:/usr/local/lib

4. 常见问题与排查技巧实录:那些让我熬夜改代码的坑

4.1 模型转换失败:convertCaffe.py报错“KeyError: ‘model’”

现象:运行python convertCaffe.py报错KeyError: 'model',指向torch.load('last.pt')返回字典无'model'键。

根因last.pttorch.save({'model': model.state_dict(), ...})保存的,但某些训练脚本(如自定义train.py)可能直接torch.save(model.state_dict(), 'last.pt'),导致加载后是OrderedDict而非字典。

排查

python -c "import torch; d=torch.load('last.pt'); print(d.keys())" # 若输出`odict_keys(['anchors', 'state_dict', ...])`,说明是state_dict格式 # 若输出`dict_keys(['model', 'optimizer', ...])`,才是标准格式

解决:用demo_run.py统一保存格式:

# demo_run.py 添加保存逻辑 torch.save({ 'model': model.state_dict(), 'epoch': epoch, 'best_fitness': best_fitness, }, 'last_fixed.pt')

4.2 板端推理无输出:./yolov5_demo运行后无任何日志

现象./yolov5_demo -i test.jpg执行后立即退出,无[INFO] Detect 3 objects日志。

根因HI_MPI_SYS_Init()失败,但代码未检查返回值。常见原因有三:①/dev/nnie设备节点不存在(未加载ko模块);② SDK版本与内核不匹配;③ 内存不足(NNIE需至少512MB连续内存)。

排查

# 检查设备节点 ls -l /dev/nnie # 应输出crw------- 1 root root 242, 0 Jan 1 00:00 /dev/nnie # 加载ko模块(SDK路径下) insmod /opt/hisi-linux/x86-arm/arm-hisiv500-linux/target/module/ko/hi3559av100/nnie.ko # 检查内存 cat /proc/meminfo | grep MemTotal # 应>1024MB free -m # 确保free内存>512MB

解决:在main.cpp开头添加:

HI_S32 s32Ret = HI_MPI_SYS_Init(); if (s32Ret != HI_SUCCESS) { printf("[ERROR] HI_MPI_SYS_Init failed: 0x%x\n", s32Ret); return -1; }

4.3 检测框严重偏移:框位置完全错误,或尺寸为负数

现象out.jpg上画的框在图片外,或x1= -12345等极大负数。

根因:后处理反量化系数错误。NNIE输出是INT32,但convertCaffe.py生成的量化参数(scale/zero_point)未被C++代码读取,导致用错scale。

排查

# 用hexdump查看NNIE输出内存 hexdump -C /tmp/nnie_output.bin | head -20 # 正常输出应为小整数(如00000000 00000000 00000001 ...),若全是FF FF FF FF,说明权重未加载

解决:确认sample_comm_nnie_yolov5.cscale值与convertCaffe.py生成的quant_param.bin一致。convertCaffe.py里应有:

# 生成quant_param.bin with open("quant_param.bin", "wb") as f: f.write(struct.pack('f', 0.00392156862745)) # scale f.write(struct.pack('i', 0)) # zero_point

C++代码中读取:

FILE *fp = fopen("quant_param.bin", "rb"); fread(&scale, sizeof(float), 1, fp); fread(&zero_point, sizeof(int), 1, fp); fclose(fp);

4.4 帧率低下:实测仅8fps,远低于标称25fps

现象:用/dev/video0实时推理,top显示CPU占用90%,帧率<10fps。

根因:视频采集未启用DMA,SAMPLE_COMM_VI_GetFrame在CPU内存拷贝YUV数据,而非直接从DMA buffer读取。

排查

# 检查VI通道属性 cat /proc/umap/vi # 查看ViChn0的buffer配置 # 正常应显示"phy addr: 0x...",若显示"vir addr: 0x...",说明未启用DMA

解决:修改SAMPLE_COMM_VI_StartVi函数,设置stViChnAttr.stCapRectstViChnAttr.u32Depth

stViChnAttr.stCapRect.s32X = 0; stViChnAttr.stCapRect.s32Y = 0; stViChnAttr.stCapRect.u32Width = 1920; stViChnAttr.stCapRect.u32Height = 1080; stViChnAttr.u32Depth = 4; // buffer深度设为4,启用DMA双缓冲

4.5 编译报错:undefined reference tocv::imread

现象make时报错undefined reference to cv::imread,但pkg-config --modversion opencv显示4.5.1。

根因:OpenCV库路径未加入Makefile.config。Ubuntu 16.04的OpenCV 4.5.1安装在/usr/local/lib,而Makefile默认只搜/usr/lib

解决:在Makefile.config中添加:

OPENCV_PATH := /usr/local INCLUDE += -I$(OPENCV_PATH)/include/opencv4 LIBS += -L$(OPENCV_PATH)/lib -lopencv_core -lopencv_imgproc -lopencv_highgui

4.6 常见问题速查表

问题现象可能原因快速验证命令解决方案
convertCaffe.pyAttributeError: 'NoneType' object has no attribute 'size'last.pt模型损坏或非YOLOv5格式python -c "import torch; print(torch.load('last.pt').keys())"重新训练或下载标准yolov5s.pt测试
板端./yolov5_demoSegmentation faultHI_MPI_SYS_MmzAlloc分配内存失败dmesg | tail -20查看内核OOM日志增加mem=1024M启动参数,或减少NNIE buffer size
test.jpg检测结果与PyTorch不一致(框数/置信度不同)后处理NMS阈值不一致对比hisi/sample_comm_nnie_yolov5.cf32NmsThresh与PyTorch的nms_iou_thres统一设为0.5,并确认cv2.dnn.NMSBoxes调用参数
make时报fatal error: hi_comm_video.h: No such file or directorySDK路径未正确设置ls /opt/hisi-linux/x86-arm/arm-hisiv500-linux/target/usr/include/hi_comm_video.h修改Makefile.configHI_SDK_PATH为实际路径
实时推理时画面卡顿、丢帧VI通道未启用硬件缩放cat /proc/umap/vi \| grep "scale"SAMPLE_COMM_VI_SetChnAttr中设置stChnAttr.stScaleAttr.bEnable = HI_TRUE

注意:所有问题排查必须遵循“硬件层→驱动层→业务层”顺序。先确认/dev/nnie存在且可读(硬件层),再验证HI_MPI_NNIE_Forward返回HI_SUCCESS(驱动层),最后检查后处理输出是否符合预期(业务层)。跳过任一层,都会陷入“以为是代码bug,实则是硬件未就绪”的死循环。

5. 实操心得与延伸建议:一个嵌入式AI工程师的肺腑之言

做完这个项目,我最大的体会是:在边缘端做AI,80%的功夫不在模型本身,而在与硬件的“谈判”上。YOLOv5的mAP再高,如果HI_MPI_NNIE_Forward返回失败,它就是一张废纸。我见过太多人花两周调参提升0.5% mAP,却不愿花一天读懂hi_nnie.h头文件里SAMPLE_COMM_NNIE_FORWARD_S结构体的每个字段含义。这份工程包的价值,不在于它让你跑通了一个模型,而在于它强迫你直面嵌入式AI的真相——没有pip install能解决一切,每一行ioctl调用背后,都是对内存管理、中断处理、DMA传输的深刻理解。

几个血泪总结:

  • 永远用dmesg代替printf调试硬件问题。当HI_MPI_NNIE_Forward失败时,printf可能还没来得及输出就被进程杀死,但dmesg里的内核日志永远忠实记录NNIE: invalid parameter at line 123。养成dmesg -C清空日志、复现问题、dmesg抓取的习惯,比加一百个printf都管用。

  • Makefile.config当作硬件说明书来读。里面-D__HI3559A__不是摆设,它控制着#ifdef __HI3559A__条件编译,而__HI3559A__宏定义的硬件寄存器偏移量,直接决定HI_MPI_SYS_MmzAlloc申请的内存是否能被NNIE DMA控制器识别。每次升级SDK,第一件事就是对比新旧Makefile.config,看NNIE_SDK_PATHCROSS_COMPILE是否更新。

  • 后处理代码必须手写,不能依赖OpenCV DNN模块。海思板端的OpenCV是精简版,cv::dnn::Net::forward不支持YOLOv5的多输出blob。我试过移植OpenCV DNN,编译通过但运行崩溃,最终发现是cv::Mat的内存布局与NNIE要求的HI_U8*不兼容。不如老老实实解析HI_S32*指针,用memcpy把数据拷贝到cv::Mat,再用cv::rectangle画框——慢是慢点,但稳。

  • 测试必须分三级:离线→在线→实时。离线测试用test.jpg,验证模型和后处理逻辑;在线测试用/dev/video0单帧,验证VI通道和NNIE集成;实时测试用while(1)循环,观察内存泄漏(top看RES列是否持续增长)和温度(cat /sys/class/thermal/thermal_zone0/temp)。我曾因HI_MPI_VI_GetFrame后忘记HI_MPI_VI_ReleaseFrame,导致内存泄漏,板子运行2小时后烫手重启。

最后分享一个小技巧:如何快速验证模型转换是否正确?不用跑板子,用Caffe CPU模式在Ubuntu上测试:

cd $CAFFE_ROOT build/tools/caffe time \ -model models/yolov5s.prototxt \ -weights models/yolov5s.caffemodel \ -iterations 100

若输出Average Forward pass: 12.34 ms,说明模型结构无语法错误;若报Check failed: bottom[0]->count() == this->blobs_[0]->count(),说明输入blob尺寸与权重不匹配,回到convertCaffe.py检查input_shape

这个工程包不是终点,而是起点。当你把YOLOv5跑通后,下一步可以尝试:① 把后处理移植到NNIE的USER算子,用硬件加速NMS;② 用HI_MPI_AIO_SendFrame接入音频,做音视频联动检测;③ 将yolov5_demo封装为systemd服务,实现开机自启。真正的嵌入式AI能力,是在一次次make cleandmesghexdump的循环中长出来的。现在,去烧写你的第一块last.pt吧——记住,那不是模型文件,是你与硅基世界握手的证书。

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

简介:直接在海思Hi3519A、Hi3559A等AI芯片开发板上跑通YOLOv5目标检测的落地工程包,覆盖从零搭建PyTorch训练环境(InstallYolov5TrainEnv.sh)、适配Caffe推理框架(InstallCaffeEnv.sh)、将PyTorch模型导出并转换为Caffe格式(convertCaffe.py)、修改后处理逻辑(附两张关键代码修改截图)、编写设备端C++推理代码(hisi目录)、配置Makefile编译规则(Makefile.config等)到最终加载last.pt模型实现实时检测的全流程。所有步骤已在真实海思开发板验证,配套README.md逐条说明,提供test.jpg一键测试。不依赖云服务,纯本地边缘部署,环境依赖明确写入requirements.txt和Shell脚本,兼容主流海思SDK版本,无商业授权限制,专为嵌入式AI学习、课程设计和毕设实践优化。


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

http://www.jsqmd.com/news/960027/

相关文章:

  • 从开发到上线实战:在快马平台构建并部署你的多模型AI分析智能体
  • MATLAB人脸验证工具:PCA特征压缩+BP神经网络分类,支持ORL/Yale数据集直接运行
  • MATLAB绘图对象层次结构详解:搞懂Figure、Axes、Line的关系,告别无效属性设置
  • 告别DSP:用Python+NumPy从零实现一个LMS自适应滤波器(附完整代码)
  • 2026年五类反光膜选型指南:二类反光膜/人防标牌/反光交通标牌/反光膜加工/反光膜原材料/四类反光膜/工程级反光膜/选择指南 - 优质品牌商家
  • 不锈钢拼装压模板实测评测:不锈钢球形板水箱/不锈钢球板水箱/不锈钢组合板/不锈钢组合水箱/卧式水箱/不锈钢保温水箱/选择指南 - 优质品牌商家
  • 性能测试Skill(Claude)
  • Carsim联合仿真避坑指南:从快捷方式到注册表,我踩过的那些‘坑’和高效配置清单
  • 从御剑到云悉:盘点那些年我们用过的CMS识别工具,以及现在更推荐哪个?
  • 实战项目:基于快马平台与uln2003a打造智能光控窗帘系统
  • 2024年装机避坑指南:从CPU后缀到显卡命名,别再被商家忽悠了
  • 终极Photoshop纹理压缩指南:Intel Texture Works插件完整教程
  • STM32CubeMX配置FatFs时,那个让你程序跑飞的‘栈溢出’坑,我是怎么填上的
  • OpenMV 4 Plus内存告急?手把手教你用TensorFlow Lite Micro和Edge Impulse做模型剪枝与量化
  • 告别混乱!用ABAP 7.4+新语法DATA(lt_sflight)和PERFORM重构你的老代码
  • 2026年5月不锈钢球形板水箱品牌实测对比评测:不锈钢波纹板水箱/不锈钢球板水箱/不锈钢组合板/不锈钢肋板水箱/选择指南 - 优质品牌商家
  • 【Java毕设源码分享】基于SpringBoot的考试平台公职考试备考系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 数据科学四大核心库:NumPy、pandas、Matplotlib、scikit-learn协同原理与工程实践
  • 新手福音:用快马AI生成带详解的ensp实验代码,轻松入门网络配置
  • Mootdx:如何高效解析通达信金融数据的Python技术方案
  • 深度解析:PyTorch ConvLSTM实现时空序列预测的突破性技术
  • 从Excel表格到地图点位:ArcGIS字段计算器批量处理‘120°26′49″’格式坐标的保姆级教程
  • 从Hello World到体系结构:拆解gem5 simple.py脚本里的CPU、总线和内存控制器
  • 量子机器学习在网络安全与恶意软件检测中的应用
  • 数据科学新手生存指南:pandas清洗→matplotlib可视化→scikit-learn建模实战
  • 别再死记硬背了!用这5个真实JavaScript正则案例,搞定表单验证和字符串处理
  • 098、异常检测与开集识别:YOLO 不认识的东西怎么让模型说“我不知道”
  • 别再乱接地了!从零开始搞懂电路设计的三种接地方式(附高频/低频场景选择)
  • 告别硬看汇编!用IDA Pro的F5与字符串窗口快速破解CTF逆向题(以攻防世界Hello CTF为例)
  • 实战应用:基于快马平台用java八股文核心知识构建秒杀系统demo