在嵌入式板卡上跑目标检测,很多人第一反应是树莓派加摄像头,然后发现帧率感人,或者以为得上Jetson。实际上有一类自带NPU的RISC-V开发板,比如嘉楠的K230,搭配YOLOv8n这种轻量级检测模型,跑实时视频流完全是可行的,而且整体成本很低。这篇文章就把我从训练YOLOv8到部署到K230板子上做实时目标检测的全过程记录一下,包括环境搭建、模型导出、量化转换、板端推理这些关键环节,还有我在实际调试中踩过的坑和排查思路。
这个实战流程适合刚接触嵌入式AI的开发者、正在做相关毕业设计的同学,以及想在低成本硬件上做目标检测原型验证的工程师。不需要太深的底子,但你需要会一点Python、知道YOLO系列的基本用法,这样读起来会更顺。我会尽量把每一步的原理和操作都讲清楚,方便你照着复现。
1. 项目概述与核心需求解析
1.1 K230是什么,为什么选它
K230是嘉楠科技推出的一款AIoT芯片,核心亮点是内置了KPU神经网络加速单元,同时集成了两个RISC-V CPU核心。我最初看上这块板子,主要是三个原因:
- 板子自带KPU,可以直接跑量化后的CNN模型,不占CPU算力,推理效率比纯CPU高得多。
- 官方SDK和工具链支持比较完善,尤其对YOLO系列模型做了适配,社区资料也多。
- 价格便宜,开发板套件几百块就能入手,相比Jetson或者高性能GPU服务器来说门槛低很多,适合用来做原型验证和教育项目。
从实测来看,K230跑YOLOv8n,640x640输入,KPU推理大概在二十多毫秒到三十毫秒级别的水平,配合摄像头实时采集,帧率能到二三十帧。这个性能在入门级AI板卡里已经算能打的了。
那为什么模型选YOLOv8n,而不是YOLOv5s或者YOLOX?主要原因是YOLOv8本身是当前生态最活跃的目标检测框架之一,训练、导出、量化都有很成熟的路径。而且YOLOv8n是nano版本,参数量和计算量都很小,正好匹配K230这种算力有限的边缘设备。
另外补充一点:现在很多人提到K230会想到“激光打蚊子”这种有点娱乐向的项目,其实它的本质也是“模型检测目标 + 控制云台/激光模块响应”,检测部分完全可以复用下面这套部署流程。
1.2 完整技术链路与方案选型
整个项目的技术链路是这样的:
准备数据集 -> 训练YOLOv8n模型 -> 导出ONNX -> nncase转换为kmodel -> 板端加载kmodel -> 摄像头采集 -> KPU推理 -> 后处理NMS -> 输出检测结果方案选型方面,我做了这几个权衡:
- 模型尺寸选YOLOv8n,默认输入640x640。有人可能觉得320x320会更快,但从实际测试来看,640x640在K230上KPU表现不差,精度损失也小,所以我优先保留640分辨率。
- 部署SDK选CanMV版,也就是MicroPython环境,方便快速验证流程。后面如果要做产品化,再切换到RT-Smart的C++环境。
- 转换工具链用官方的nncase,它负责把ONNX模型编译成KPU能直接跑的kmodel格式,同时支持INT8量化校准。
这条链路几乎是K230做视觉检测的“标准答案”,后面我会把每一步的细节拆开讲。
2. 环境搭建与工具链准备
2.1 开发板系统选择:CanMV还是RT-Smart
K230支持两套主流SDK:CanMV和RT-Smart。
CanMV是MicroPython生态,上手极其顺滑。你把它当成一个Python解释器跑在板子上,可以直接通过CanMV IDE连接开发板,写Python脚本读摄像头、跑模型、画框。对新手和快速原型验证来说,这是最省事的方案。我整个项目调试阶段用的就是CanMV。
RT-Smart是RT-Thread的微内核系统,支持C/C++开发,适合做性能要求更高、需要深度集成的产品化项目。C++环境下能更精细地控制内存、线程和KPU调度,但调试门槛也更高。
我的建议是:如果你只是想验证YOLOv8能不能在K230上跑起来,或者做毕设演示,直接用CanMV;如果后期要上设备、接外部传感器做完整系统,再迁到RT-Smart。两套方案在模型转换环节完全一样,换的只是板端推理代码。
2.2 nncase工具链安装与版本匹配
nncase是K230模型转换的核心工具,作用是把ONNX模型编译成kmodel。这一步需要在电脑上完成,推荐用Ubuntu系统。
我用的工具链方式有两种:一种是下载官方编译好的工具链包,解压后直接在命令行调用;另一种是用Python API操作,更灵活。我习惯用Python API,因为可以在脚本里同时完成加载模型、配置量化校准数据、编译导出这一整套流程,方便调试。
这里有一个很重要的教训:工具链版本必须和SDK版本匹配。我一开始没注意,随便在Gitee上下了一个新版nncase,结果转换时报了一堆底层错误,后来才发现是版本不匹配。建议你下载K230 SDK时直接把配套的nncase工具链一起拿下来,或者直接参考官方文档给出的版本对应关系。
安装好之后,你可以先跑一下官方给出的示例,把resnet或者YOLOv5的示例模型走通,再处理自己训练的YOLOv8模型。
2.3 固件烧录与串口连接
拿到K230开发板之后,先烧录固件。方法很简单,把官方镜像用烧录工具写入TF卡,然后开发板从TF卡启动。不同版本的开发板烧录方式略有差异,但基本都是这个思路。
连接方式我个人更推荐串口:
- USB转串口模块连接开发板的调试串口,波特率一般设为115200。
- 上电后可以在串口终端看到系统启动日志,CanMV固件会进入Python交互环境。
- 如果要用CanMV IDE,需要在IDE里连接开发板的USB端口,这样能直接在线跑脚本并显示摄像头画面。
这里提一个易踩的坑:很多人在CanMV IDE里连接开发板后,喜欢一边实时显示画面一边调试。显示画面本身会占用不少CPU资源,导致模型推理帧率明显下降。后面做性能测试时,务必关掉IDE画面,或者用串口只输出文本日志来判断速度。
3. YOLOv8模型训练与导出
3.1 数据标注与数据集配置
要部署YOLOv8,首先得有一个训练好的模型。如果直接用官方的COCO预训练权重,那检测类别是80类,很多场景下够用。但如果要做特定目标检测,比如检测硬币、检测药剂残留、检测零件缺陷这类定制任务,就需要自己标数据训练。
标注工具方面,我用过LabelImg和X-AnyLabeling。LabelImg是老牌工具,操作简单,导出YOLO格式很直接。X-AnyLabeling支持更多辅助标注功能,比如自动分割、模型辅助标注,能省不少人工。我个人做项目习惯用X-AnyLabeling,因为它的标注体验更现代,而且支持导出YOLO格式。
数据集目录结构参考如下:
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml里需要指定类别数和类别名。例如:
path: /path/to/dataset train: images/train val: images/val names: 0: person 1: car有几个细节值得注意:
- 数据量不要贪多,先保证质量。每个类别最少几百张,覆盖不同角度、不同光照条件。
- 标注框不要留太大边距,紧贴目标即可,否则训练时会给模型引入噪声。
- 训练集和验证集一定不能有重复图片,否则验证指标虚高,部署后实际效果拉胯。
3.2 训练实操与参数调整
训练直接用Ultralytics的YOLOv8框架。安装方式很简单:
pip install ultralytics训练命令也不复杂,核心是指定数据集配置和模型尺寸:
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0几个关键参数我给一下实际建议:
- 模型选yolov8n.pt,因为最终要部署到K230,参数量大的模型即使能转换,板端推理也Hold不住。
- imgsz设为640,和部署时保持不变,这样能保证训练和部署的分辨率一致,减少精度损失。
- batch大小取决于显卡显存,16或者32都可以,只要能稳定训练。
- epochs我一般先用100轮,观察mAP曲线不再上升再继续加。训练完看验证集mAP50和mAP50-95,如果明显偏低,优先检查数据质量而不是盲目加训练轮数。
训练过程中可以画损失曲线来看收敛情况。Ultralytics训练完会自动在runs目录下生成results.png,包含box_loss、cls_loss、dfl_loss以及mAP的曲线。如果你需要自己画更精细的曲线,可以从训练日志中提取loss值,然后用matplotlib画,网上有很多现成脚本可以参考。
3.3 模型导出为ONNX:关键细节
板端工具链不认识PyTorch权重,所以需要先把模型导出成ONNX。这一步看似简单,但有几个细节直接决定了后面nncase转换是否顺利。
我的导出命令是:
yolo export model=best.pt format=onnx imgsz=640 opset=12 dynamic=False simplify=True这里注意三点:
- opset版本选12,nncase对太新的opset支持不够稳定,如果转换报算子不支持,先考虑降低opset版本。
- 不要开dynamic,K230要求固定shape,动态尺寸会增加转换难度,板端也不好处理。
- simplify作用是用onnxsim进行图优化,很多冗余节点会被去掉,有效减少转换阶段出问题的概率。
导出完成后,我用onnxruntime加载ONNX模型做一个推理验证,输入随机张量或者真实图像,确认输出shape正确、数值合理。这一步能帮你提前发现导出问题,避免后面在nncase阶段才暴露。
确认ONNX没问题之后,可以顺便把输出层的名字记录下来,后面板端解析输出时会用到。YOLOv8n的ONNX输出通常是一到三个head输出,每个输出的shape与类别数相关,比如80类时输出为[1, 84, 8400]这种结构,84实际上就是4个坐标值加80个类别置信度。
4. 模型转换:从ONNX到kmodel
4.1 nncase转换流程
nncase转换是整个部署流程中最容易出问题的一环。我的做法是写一个Python脚本,调用nncase的Python API完成从ONNX到kmodel的全过程。
核心逻辑大致如下:
import nncase # 1. 创建编译配置,指定目标芯片为k230 compile_options = nncase.CompileOptions() compile_options.target = "k230" compile_options.input_type = "float32" compile_options.input_shape = [1, 3, 640, 640] # 与导出ONNX时保持一致 # 2. 导入ONNX模型 model = nncase.ImportModel(compile_options, "yolov8n.onnx") # 3. 设置量化校准数据集 calib_dataset = nncase.CalibDataset("calib_data.npy", ...) # 4. 编译 nncase.Compile(model, calib_dataset, "yolov8n.kmodel")不同工具链版本导入模型和编译的API写法会有差异,以你下载版本对应的官方文档为准。这里想强调的是几个通用关键点:
- input_shape一定要和你导出ONNX时保持一致,如果你导出的是640x640,这里就得写成1x3x640x640。
- 数据类型如果是量化部署,通常会用float32输入,然后工具链内部做INT8量化。也有直接做uint8输入的方案,但预处理逻辑会变复杂,新手建议先用float32输入。
- 编译完成后会生成.kmodel文件,这个文件就是最终拷贝到开发板上运行的东西。
如果你不想写Python脚本,nncase工具链也提供了命令行转换工具,但Python API的报错信息更友好,调试起来更方便。
4.2 校准数据集与INT8量化
K230的KPU主要是为INT8量化设计的,直接跑float32模型虽然也能转换,但推理效率偏低。所以正常情况下我们都会做INT8量化。
量化需要准备一个校准数据集,它的作用是统计模型各层激活值的分布,从而确定合适的量化参数。校准数据集不需要很大,一般几十到几百张代表性图片即可。
我实际操作时,是从验证集里随机抽200张图片,预处理成模型输入需要的格式,然后保存成npy文件,作为校准数据输入给nncase。
校准数据有几个注意事项:
- 校准图片应该尽可能接近你实际部署场景,比如你要检测道路车辆,校准集就应该包含白天、夜晚、不同角度的车辆图片,而不是随便找一堆风景图。
- 校准集不要和训练集完全重合。虽然理论上也能用训练集做校准,但量化参数的泛化性会变差,部署后遇到新场景容易精度崩坏。
- 量化后mAP掉点是非常正常的,一般掉1到3个点都算健康范围。如果掉点严重,可以适当增加校准图片数量,或者检查预处理是否一致。
4.3 算子兼容与网络调整
YOLOv8的检测头结构相对复杂,包含Split、Slice、Concat、Upsample等算子。大部分算子nncase在较新版本都支持,但老版本可能遇到个别算子不支持或者报错的情况。
我遇到比较典型的问题是某些自定义结构或较新的op导致转换失败。解决方法通常是:
- 升级工具链到SDK匹配的最新版本。
- 如果单个算子不支持,考虑在模型导出前替换或剪枝相关结构。比较常见的处理是把SiLU激活函数替换成ReLU,因为ReLU在KPU上是肯定支持的,而SiLU在某些版本上可能编译不了。替换后精度可能会有一点损失,但换来的是转换顺利和推理稳定。
- 还有一种是ONNX里因为动态shape导致的一些Reshape/Transpose算子报错,解决办法就是把模型导出固定shape,同时尽量精简前处理里不必要的Reshape。
我在这个阶段浪费了大概两天时间,最后发现是onnx模型里混进了一个不支持的Gather算子,用onnxsim做图优化之后节点被绕过了,转换就正常了。
5. 板端部署与实时检测实现
5.1 部署框架选择与代码骨架
模型转换成kmodel之后,剩下的工作就是到K230板子上写推理代码。
如果你用CanMV,代码逻辑相对简单。我建议直接参考官方提供的ultralytics_yolov8示例,加载kmodel、摄像头初始化、预处理、推理、后处理这些模块都有现成代码,你要做的主要是修改类别数和后处理逻辑。
整体流程是这样的:
初始化摄像头 -> 设置分辨率 -> 初始化KPU -> 加载kmodel -> 循环采集 -> 预处理 -> KPU推理 -> 解析输出 -> NMS -> 绘制/输出结果如果你用RT-Smart的C++环境,逻辑也类似,只是API换成C++版本。无论哪套,核心都是把kmodel加载进KPU,然后把图像数据喂给KPU做推理。
5.2 摄像头采集与预处理实现
K230开发板默认配的是GC2093摄像头,最高能出1920x1080分辨率的图像。实际做目标检测时,我不会直接拿1080P的图像喂给模型,而是先做缩放或者裁剪到模型输入尺寸。
预处理看起来简单,但极其关键,因为预处理错了模型推理结果基本就废了。我在实践中总结出的几个必须注意的点:
- 颜色通道顺序。KPU输入一般要求RGB,但摄像头原始输出可能是BGR或者YUV,做转换时一定要确认。
- 归一化。模型训练时如果用的是0-1归一化,板端预处理也要做同样的操作;如果转换时已经把归一化融合进了模型,板端就不用再归一化。这个必须搞清楚nncase的目标部署方式是哪种,否则推理结果会出现系统性偏差。
- 尺寸变换方式。YOLOv8训练时通常用了letterbox,也就是等比缩放加填充,板端预处理也要用同样的方式。如果直接拉伸到640x640,目标的宽高比会失真,检测框精度会明显下降。
- 数据排布。KPU输入通常是NHWC或者NCHW,代码里要按kmodel要求的排布去填数据,搞反了同样会出问题。
为了提速,可以在KPU推理前把resize操作放到KPU里去执行,也就是用KPU的缩放单元做resize,可以省下CPU的缩放耗时。这个优化建议放在功能跑通之后再做。
5.3 KPU推理与后处理NMS
模型推理阶段,把预处理好的图像数据写入KPU输入tensor,然后调用推理接口,得到输出tensor。K230的KPU推理是同步阻塞式调用,性能瓶颈主要看模型复杂度和输入分辨率。
YOLOv8输出解析的核心逻辑是:把模型输出的特征图解码成候选框坐标、置信度和类别概率,然后通过置信度阈值过滤,再用NMS去除重叠框。
NMS的逻辑很简单,核心代码可以自己写,大致是:
def nms(boxes, scores, iou_threshold): indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True) keep = [] while indices: i = indices.pop(0) keep.append(i) for j in indices[:]: if compute_iou(boxes[i], boxes[j]) > iou_threshold: indices.remove(j) return keep在板端,NMS一般用Python或者C++实现。Python实现更简单,但速度会慢一些;如果帧率要求高,建议把后处理代码做一些优化,比如用矩阵运算代替纯循环,或者想办法减少候选框数量。
YOLOv8n输出的是8400个候选框,也就是640x640下三个尺度特征图的像素点总和。每个像素点对应一个候选框,所以后处理计算量不小。我实测下来,纯Python做后处理,每帧大约要多花10到20毫秒,这会成为帧率瓶颈。所以后面要优化性能时,后处理是个重点区域。
5.4 实时性能调优与进阶玩法
功能跑通之后,我看到帧率大约在十几帧,距离“实时流畅”还有差距。于是做了几项优化,性能提升非常明显:
- 把预处理里的resize从CPU移到KPU,CPU占用下降,整体帧率提升。
- 降低摄像头分辨率到640x640,直接用640x640喂给模型,省去一次缩放。
- 对后处理做降级优化,比如先按类别置信度排序取前N个框再计算NMS,减少IOU计算次数。
- 关闭CanMV IDE的画面预览,只在检测到目标时做标记,这样能大幅提升处理速度。
做完这几项优化,实际帧率能到20到30帧,完全可以用于实时检测演示。
进阶玩法方面,K230常见的扩展方向是加云台控制。检测到目标位置后,把目标中心坐标转成云台偏转角度,通过串口通信发给云台模块,就能做目标跟踪。之前说的“K230激光打蚊子”项目,本质就是这种架构:检测到蚊子目标坐标,然后控制激光云台瞄准。
如果你要做类似的产品或者赛题项目,还可以把检测结果叠加显示在HDMI输出上,或者通过串口把目标坐标发送给上位机。
6. 常见问题与排查技巧实录
6.1 推理结果全乱或坐标全0
这是新手最容易遇到的问题,我排查步骤一般是按这个顺序来:
- 先确认ONNX模型在电脑上输出正常。把同一张测试图分别通过ONNX模型和kmodel推理,对比输出tensor的数值分布。如果ONNX正常但kmodel异常,问题大概率出在转换阶段或者输入预处理。
- 再检查输入预处理。颜色通道顺序对不对,RGB还是BGR;归一化做了没有;尺寸是letterbox还是直接拉伸;数据排布是NHWC还是NCHW。这些错一个,模型输出就会完全不对。
- 最后检查后处理解析。YOLOv8输出格式是坐标加类别置信度,如果解析顺序搞错,坐标全部乱掉也不奇怪。尤其注意YOLOv8的输出头可能是解耦的,也就是坐标输出和分类输出分在不同tensor里,解析时要分清。
如果以上都没问题,还有一个很容易忽略的点:kmodel输入要求的数值范围可能不是0-1,而是0-255,或者反过来。这个要看你转换时input_type怎么配置的。不匹配的话,模型输出的置信度会普遍偏低,框还是会乱飘。
6.2 帧率上不去的瓶颈定位
帧率不高先别急着怪KPU,先用计时器定位瓶颈:
t1 = time.ticks_ms() # 预处理 t2 = time.ticks_ms() # KPU推理 t3 = time.ticks_ms() # 后处理 t4 = time.ticks_ms()把四段时间分别打印出来,看哪个耗时最长。
我遇到的情况是,KPU推理其实只花了30毫秒不到,但预处理加后处理加起来花了超过50毫秒,帧率就被拖垮了。优化后处理后,整体耗时就降下来了。
另外,如果发现KPU推理本身超过50毫秒,可以考虑降低输入分辨率到320x320,这是一个很直接的加速手段。320x320比640x640的KPU推理速度快不少,代价是检测小目标能力下降。具体用多大分辨率,取决于你的目标尺寸和部署场景。
6.3 串口调试与日志技巧
板端调试离不开串口。CanMV下用print打日志,然后通过串口终端查看,这是最基础的调试手段。
我的习惯是:
- 在每个阶段都打印关键信息,比如摄像头是否初始化成功、kmodel是否加载成功、输入tensor shape是否正常。
- 在循环推理时只打印关键帧的数据,比如每30帧打印一次耗时信息,避免大量日志拖慢运行。
- 用断言或者try-except捕获KMODEL相关错误,有些错误在CanMV IDE里表现不明显,但串口终端会打出一大堆堆栈,这些信息对排查问题非常有用。
如果你在调试后接云台或者电机,串口通信更是刚需。K230的UART接口可以直接输出目标坐标或控制指令,我通常用简单的文本协议,比如“x:320,y:240,conf:0.85”,上位机解析起来非常方便。
另外提醒一点:烧录固件后第一次上电,建议先看串口日志确认系统正常启动,再连接CanMV IDE。别一上来就急着跑脚本,先确认基础环境没问题,后面排查会轻松很多。
7. 实操后的经验沉淀
整个项目从零开始到跑通,最深的感受是:K230这套方案的难点不在KPU推理本身,而在模型转换和数据处理的一致性。你训练时怎么预处理,导出ONNX时怎么配置,转换kmodel时怎么量化,板端推理时怎么填数据,这四个环节必须完全对齐,差一个细节结果都不对。所以每一环节都要保持记录,把输入尺寸、颜色通道、归一化方式、输出解析方式这些关键信息记下来,调试时能省很多时间。
还有一个建议是,刚开始不要追求复杂的检测场景,先用官方预训练权重走通整个流程,确认板端部署没问题后,再训练自己的数据集替换模型。这样能把变量拆开,遇到问题知道该往哪个方向排查。
如果你做完实时检测后想继续扩展,思路很清晰:往上游加数据集扩充和模型精度优化,往下游加云台控制和通信协议,整个系统就是一个完整的AIoT视觉应用了。从一个简单的检测demo,到能实际落地的成品,中间缺的其实就是把这些环节吃透。希望这篇实战记录能帮你少走一些弯路。