模型不是天生的"看图高手",它是靠大量带答案的图片训练出来的。这个过程就像教小孩认钢珠:你指着一张图说"这是钢珠",指的次数多了,小孩就记住了。给模型"指图认物"的这一步,就叫数据标注。
本期项目的数据集是约300张钢珠照片(文件名photo_xxxx命名),分成训练集(train)和验证集(val)两部分。
YOLO训练有一套固定的目录结构(YOLO格式),建议照下面这样整理:
my_dataset/
├── images/ # 图片:train/(训练)和 val/(验证)两个子目录
│ ├── train/
│ └── val/
├── labels/ # 标注:每个图对应一个同名 .txt 文件
│ ├── train/
│ └── val/
└── data.yaml # 数据集的"总清单"
其中 labels 里的每个 .txt 记录了这张图里每个钢珠的类别编号和坐标,它与 images 里的图片一一对应。
data.yaml 是训练时的"地图",它告诉程序:数据在哪、训练集/验证集在哪、有几类物体。钢珠项目只有一类,示例如下:
path: ./my_dataset # 数据集根目录
train: images/train
val: images/val
names:
0: ball # 钢珠(单类)
⚠️ 小贴士:names 里的类别顺序必须与标注时一致,而且后面部署到 K230 时,板子上的 labels 列表也要按这个顺序来写(这里就是 ["ball"]),否则就会"张冠李戴"。
数据备好了,接下来就是让模型"刷题"。这里我们用 ultralytics 框架(配套PyTorch环境)来训练,它把复杂的训练过程打包成了简单命令。
本地训练命令(教程原文):
yolo detect train data=my_dataset/data.yaml model=yolo11n.pt epochs=200 imgsz=320 batch=16 device=0
如果用脚本(train.py)方式训练,有几个Windows实用要点值得注意:
如果训练中途被打断,别慌,可以从最后一个断点接着训(断点续训),不用从头再来:
yolo train resume model=path/to/last.pt
训练跑完后,程序会在 runs/detect/my_detection/ 目录下留下"战利品":
▪ best.pt:在验证集上指标最好的那一版权重——部署就用它;
▪ last.pt:最后一个 epoch 的权重——主要用于断点续训;
▪ results.png / results.csv:训练曲线与指标数据(损失下降、指标上升);
▪ PR_curve.png:PR 曲线;
▪ val_batch*_pred.jpg:验证集的预测效果图(图上直接画出框,一眼能看出对不对,很适合当配图);
▪ train_batch0.jpg:第一轮训练输入的样本可视化。
训练日志里会蹦出一串指标:P、R、mAP50、mAP50-95…… 别被它们吓到,用"看病"打个比方就懂了:
其中 IoU 要单独解释一下:它就是预测框和真实框"重叠程度"的度量——完全重合是1,完全不沾边是0。
▪ mAP50 只要求框和真实框的 IoU 达到 0.5 就算对,门槛低、分数通常好看;
▪ mAP50-95 则把 IoU 从 0.5 一路调到 0.95(步长 0.05)逐档算一遍再取平均,既考"找到没有"、又考"框准没有",是更硬核的指标。
那训练到什么样算"练好了"?教程给出几条经验判断:mAP@0.5 大于 0.80 算好、低于 0.50 算差;box_loss(框的损失)稳定下降;训练集与验证集的损失曲线接近(没有明显过拟合);本地训练可打开 val_batch*_pred.jpg 看验证集预测效果,在线平台则看训练报告里的验证效果图——钢珠都被框对,基本就能判断模型够不够用了。
模型在电脑上跑得好不算本事,真正有意思的是让它在一张小小的开发板上实时运行。这一步,我们把它部署到 K230 上。
K230 是嘉楠科技推出的一款 RISC-V架构AI芯片,内部集成了一块专门的 KPU(AI推理单元),专为端侧视觉场景设计,功耗低、算力却够用,特别适合"摄像头 + AI"的边缘设备。
本期使用的开发板是 立创·庐山派CanMV-K230:它把K230做成了开发板,板载CSI2摄像头(sensor id=2)和一块800×480的LCD屏,刷上 CanMV 固件(K230的MicroPython移植版)后,用简洁的MicroPython代码就能调用AI推理,不用去啃复杂的底层驱动。
电脑上训练出的 best.pt 是给PyTorch用的,K230的KPU不认这个格式。所以我们要走一条转换链路:
best.pt → ONNX → kmodel(nncase + 量化)
yolo export model=runs/detect/my_detection/weights/best.pt format=onnx imgsz=320 simplify=True
⚠️ 不要加 `nms=True`!K230 板端会自己做后处理(NMS),如果导出时把 NMS 也固化进 ONNX,上板后反而会出错。
第二步,用嘉楠的 nncase 工具把ONNX转成K230能跑的 kmodel。转换关键配置如下:
▪ 版本:nncase==2.9.0 + nncase-kpu==2.9.0(注意 Python 3.8~3.10 环境;Windows 下 nncase-kpu 从 GitHub Releases 下载 whl,还需要 .NET 7.0 桌面运行时,可能还要补一个 libomp140.x86_64.dll);
▪ CompileOptions(编译选项):target="k230",preprocess=True、input_type="uint8"、input_shape=[1,3,320,320]、input_layout="NCHW"、mean [0,0,0]、std [255,255,255];
▪ PTQTensorOptions(量化选项):calibrate_method="Kld"、quant_type="uint8",校准图用 20~50 张真实场景图;
▪ 产物:best.kmodel + labels.txt(每行一个类名,这里就是 ball)。
量化的本质:把32位浮点权重压缩成8位整数,模型变小、变快,代价是精度略有损失——就像把高清照片压成小尺寸,占地方少了,细节也略打折扣。所以校准图一定要用真实场景图,量化才"压得准"。
模型转好后,把 yolo11n_det_320.kmodel 和 labels.txt 拷到板子的 /sdcard/examples/kmodel/ 目录(用CanMV IDE拖拽,或命令行 mpremote cp),就能在板子上推理了。
下面是本项目的板端程序 main.py 的核心骨架——它做的事比"画个框"更进一步:
from libs.YOLO import YOLOv8 # 官方 YOLOv8 类,兼容 v8/v11 检测模型
from libs.PipeLine import PipeLine, ScopedTiming
from machine import UART
MODEL_PATH = "/sdcard/yolo11n_det_320.kmodel"
LABELS = ["ball"] # 顺序必须与训练 data.yaml 的 names 一致
MODEL_INPUT_SIZE = [320, 320]
RGB888P_SIZE = [640, 360]
SENSOR_ID = 2
DISPLAY_MODE = "lcd"
1) 初始化摄像头管线(video 模式;真实代码为 pl.create(sensor_id=SENSOR_ID, ...))
pl = PipeLine(rgb888p_size=RGB888P_SIZE, display_mode=DISPLAY_MODE)
pl.create(sensor_id=SENSOR_ID)
2) 用 YOLOv8 类加载 yolo11n kmodel(官方 YOLOv8 类兼容 v8/v11 检测模型)
yolo = YOLOv8(task_type="detect", mode="video",
kmodel_path=MODEL_PATH, labels=LABELS,
rgb888p_size=RGB888P_SIZE, model_input_size=MODEL_INPUT_SIZE,
conf_thresh=0.5, nms_thresh=0.45, max_boxes_num=50, debug_mode=0)
yolo.config_preprocess()
uart = UART(UART.UART2, baudrate=115200) # 用于把坐标发给下游 MCU
while True:
with ScopedTiming("total", 1):
img = pl.get_frame() # 从摄像头取一帧
res = yolo.run(img) # KPU 推理,返回检测框列表
# 3) 只保留离画面中心最近的钢珠,得到其中心坐标 (cx, cy)
# (遍历 res 中各框,计算框中心到画面中心 (320,180) 的距离,取最小者)
# 4) 画框到 LCD,并显示坐标(K230 不支持 f-string,用 % 格式化)
yolo.draw_result(res, pl.osd_img)
# 在 OSD 上打印 "x=xx y=yy"(用 % 格式化,而非 f-string)
# 5) UART 发送坐标与状态:帧头 55 53 + x + y + state
# x/y 范围 0~640 / 0~360;state=1 有钢珠、state=0 无钢珠
pl.show_image() # 刷新显示到 LCD
⚠️这里藏着一个双芯协作的亮点:K230 负责"看"——摄像头 + KPU 推理,得出钢珠的坐标;坐标再通过 UART(帧头 `55 53`)发给下游的 MCU(比如 STM32),由 MCU 去"动"——控制舵机、机械臂抓取钢珠。一块芯片负责视觉智能,一块芯片负责实时控制,各司其职,这就是嵌入式 AI 项目里最常见的"双芯"分工。
部署落地的核心指标就是帧率——一秒能处理多少帧。教程给出的实测数据(测试条件:K230板载CSI2摄像头 + LCD 800×480):
(注:以上为教程实测数据)可以看到,320输入明显更快——这也是本期模型统一用320的原因。想要更快,就用320输入 + n模型 + 让 rgb888p_size 等于 model_input_size;想要更准,才考虑640输入或换s模型。
下面是教程第7节整理的真实踩坑清单,几乎每个新手都会碰到其中一两条:
从数据标注到训练调参,再到把模型塞进一张K230小板子里实时跑起来——这一整套流程走下来,你会发现所谓"AI视觉"并不遥远:它不过是"好教材(数据)+ 勤刷题(训练)+ 巧落地(部署)"三件事的叠加。
而当我们让K230通过一根UART线,把钢珠的坐标实时交给下游MCU的那一刻,就已经摸到了端侧AI与嵌入式系统协作的门槛——一块芯片负责"看",一块芯片负责"动",这正是一个完整的智能硬件项目该有的样子。
YOLO的一步到位,让我们看到了"效率"的优雅;K230的小身板,则让我们看到"算力"也可以离我们这么近。作为电子信息、通信、人工智能相关专业的学生,我们既是这些技术的学习者,也完全可以成为把它们亲手做出来的人——你写下的每一条命令、标注的每一个框,最终都会变成机器"看懂"世界的那双眼睛。
"一电就通" 也会持续分享更多电子、通信、AI类的实用干货,与大家一起精进专业本领。如果你也想亲手跑通自己的目标检测模型,不妨就从这一篇开始动手试试吧!