【CanMV K210】AI 视觉 按键采样自训练识别与现场分类
在 CanMV K210 入门和综合项目中,自训练学习识别实验是一个很适合拆解的视觉实验。它不是单纯验证摄像头能不能显示画面,也不是直接运行一个固定类别的识别模型,而是把摄像头采集、BOOT 按键输入、KPU 特征提取、样本记录、相似度比较和 LCD 结果显示串成一个完整流程。
本实验使用 CanMV K210 运行 4.12_Self_Learning.py。程序先加载 /sd/mb-0.25.kmodel 模型,再通过 BOOT 按键依次采集多个类别的样本特征。完成采样后,程序进入分类状态,对镜头前的物体提取特征,并与之前记录的样本特征进行相似度比较。若相似度超过阈值,就在 LCD 上显示类别编号和相似度分数。
| 学习目标 | 说明 |
|---|---|
| 理解自训练识别流程 | 明确本实验不是重新训练模型权重,而是在现场记录特征并进行相似度分类 |
| 掌握 BOOT 按键采样 | 使用 BOOT 按键控制状态切换和样本采集 |
| 理解状态机设计 | 通过 STATE、EVENT 和 transitions 管理初始化、采样和分类阶段 |
| 掌握 KPU 特征提取 | 使用 kpu.run_with_output(img, get_feature=True) 提取图像特征 |
| 理解现场分类逻辑 | 使用 kpu.feature_compare() 比较当前图像特征和样本特征 |
| 建立视觉项目调试思路 | 从摄像头、LCD、模型文件、按键状态和阈值几个方向排查问题 |
实验时需要固定摄像头位置,准备 3 个待分类物体。程序运行后,按屏幕提示使用 BOOT 按键依次采集样本,完成采样后进入分类阶段。后续把待检测物体放到镜头前,LCD 会显示分类编号和相似度分数。这个实验适合用来理解“现场采样—特征记录—实时分类”的 AI 硬件项目流程。
理论基础
自训练学习识别实验的核心不是在开发板上重新训练一个神经网络模型,而是利用已经训练好的特征提取模型,把摄像头画面转换成特征向量。采样阶段,程序把每个物体的多张图像特征保存到 features 列表中;分类阶段,程序对当前画面再次提取特征,并和已保存的特征逐一比较,找出相似度最高的类别。
这个过程可以理解成“先记住样本特征,再比较新图像特征”。模型文件 /sd/mb-0.25.kmodel 负责把图像转换成可比较的特征数据,kpu.feature_compare() 负责计算两个特征之间的相似度。程序中设置了 THRESHOLD = 98.5,只有最高相似度超过这个阈值时,才会在 LCD 上显示分类结果。
本实验还引入了状态机。状态机的作用是让复杂流程变得清晰:上电后进入初始化状态,按 BOOT 键进入第 1 类采样,再依次进入第 2 类和第 3 类采样,最后进入分类状态。每一次按键并不是简单执行一个固定动作,而是根据当前状态决定下一步该采样、提示换物体,还是进入分类阶段。
下面的流程图展示的是本实验的核心运行链路。摄像头负责采集画面,BOOT 按键负责触发采样和状态切换,KPU 负责提取图像特征,状态机负责管理训练和分类阶段,LCD 负责显示提示信息和识别结果。
这个流程可以理解成“图像输入—按键控制—特征记录—相似度分类—屏幕反馈”。相比普通图像显示实验,本实验多了样本采集和状态机控制;相比固定模型识别实验,本实验不依赖固定类别标签,而是现场采集待分类物体的特征。它适合做 AI 视觉教学演示,也适合作为轻量物体分类项目的原型。
硬件设施
本实验围绕 CanMV K210、摄像头、LCD、BOOT 按键、SD 卡模型文件和 KPU 推理能力展开。代码没有使用外接 GPIO 输出模块、蜂鸣器、电机或其他传感器,因此硬件重点应放在摄像头画面、LCD 显示、BOOT 按键、模型文件路径和开发板运行环境上。
这里适合放一张实验整体效果图,用来展示 CanMV K210、摄像头、LCD 和待分类物体之间的关系。拍摄时建议让屏幕提示、摄像头方向和实验物体同时出现在画面中,便于读者理解现场采样和分类的实际操作方式。
| 硬件 / 软件 | 作用 | 说明 |
|---|---|---|
| CanMV K210 开发板 | 实验运行平台 | 负责运行 MicroPython 程序,调用摄像头、LCD、BOOT 按键和 KPU |
| 摄像头模块 | 图像采集设备 | 通过 sensor.snapshot() 获取当前画面 |
| LCD 显示屏 | 图像与提示显示 | 显示摄像头画面、采样提示、FPS 和分类结果 |
| BOOT 按键 | 采样与状态切换输入 | 每次按键触发状态机事件,用于采集样本或进入下一阶段 |
| SD 卡 | 模型文件存储 | 存放 /sd/mb-0.25.kmodel 模型文件 |
| KPU 神经网络加速器 | 特征提取与相似度计算 | 加载 kmodel,并通过特征输出实现现场分类 |
gc |
内存管理模块 | 主循环中调用 gc.collect() 回收内存 |
sensor |
摄像头控制模块 | 设置图像格式、窗口大小并采集图像 |
lcd |
LCD 显示模块 | 显示图像帧和文字提示 |
maix.GPIO |
GPIO 控制模块 | 创建 BOOT 按键输入对象 |
fpioa_manager.fm |
引脚映射模块 | 将 BOOT 按键映射为 GPIOHS0 |
board_info |
板载资源信息 | 提供 board_info.BOOT_KEY 对应的板载按键引脚 |
KPU |
AI 推理模块 | 加载模型、提取特征并进行特征比较 |
BOOT 按键、摄像头、LCD 和模型文件是本实验最关键的检查对象。代码中通过 fm.register(board_info.BOOT_KEY, fm.fpioa.GPIOHS0) 将 BOOT 按键注册为 GPIOHS0,再通过 boot_gpio = GPIO(GPIO.GPIOHS0, GPIO.IN) 创建输入对象。摄像头通过 sensor.reset() 初始化,LCD 通过 lcd.init() 初始化,模型通过 kpu.load_kmodel("/sd/mb-0.25.kmodel") 从 SD 卡加载。
| 连接 / 资源 | 代码对象 | 对应硬件与说明 |
|---|---|---|
| BOOT 按键 | board_info.BOOT_KEY |
板载 BOOT 按键,用于采样和状态切换 |
| BOOT 输入功能 | GPIO.GPIOHS0 |
BOOT 按键被映射为 GPIOHS0 输入 |
| 摄像头接口 | sensor.reset()、sensor.snapshot() |
初始化摄像头并持续采集图像帧 |
| 图像窗口 | sensor.set_windowing((224, 224)) |
将摄像头画面裁剪为适合模型输入的窗口 |
| LCD 接口 | lcd.init()、lcd.display(img) |
显示摄像头画面、文字提示和分类结果 |
| 模型文件路径 | /sd/mb-0.25.kmodel |
模型文件必须放在 SD 卡指定路径下 |
| 特征记录列表 | features |
存储不同类别的图像特征 |
| 分类阈值 | THRESHOLD = 98.5 |
只有相似度超过阈值才显示分类结果 |
这里适合放一张实验运行截图,用来展示 LCD 上的提示信息。采样阶段可以拍摄 Train object 1、Boot key to take #P1 这类提示,分类阶段可以拍摄 class:1, score:xx.x 这类结果,便于读者把程序状态和屏幕现象对应起来。
实际操作时,建议固定摄像头和物体摆放位置。自训练识别依赖现场采集特征,光照、距离、角度和背景变化都会影响相似度。每次采样时应让物体完整出现在画面中心,并尽量保持背景稳定。分类阶段应把待识别物体放到与采样阶段相近的位置,这样更容易得到稳定结果。
软件代码
本实验的软件部分以 4.12_Self_Learning.py 为核心。程序导入 gc、lcd、sensor、time、GPIO、KPU、board_info 和 fm,随后完成 BOOT 按键映射、摄像头初始化、LCD 初始化、模型加载、状态机创建和主循环调度。主循环中持续读取 BOOT 按键状态、采集摄像头画面、执行特征提取或分类比较,并把提示信息绘制到 LCD 图像上。
| 软件环境 | 作用 | 检查重点 |
|---|---|---|
| CanMV IDE | 编辑、运行和调试程序 | 能识别开发板串口,并能运行基础摄像头示例 |
| CanMV 固件 | 提供 sensor、lcd、KPU、GPIO 等模块 |
固件需要支持 KPU 模型加载和特征输出 |
| SD 卡 | 存放 kmodel 文件 | /sd/mb-0.25.kmodel 路径必须正确 |
| 摄像头驱动 | 采集图像帧 | sensor.reset() 和 sensor.snapshot() 能正常运行 |
| LCD 驱动 | 显示提示信息和识别结果 | lcd.init() 和 lcd.display(img) 能正常运行 |
| 串口终端 | 查看状态机和调试输出 | 能看到 ready load model、状态切换和事件信息 |
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----湖南创乐博智能科技有限公司----
# 文件名:4.12_Self_Learning.py
# 版本:V2.0
# author: zhulin
# 说明:CanMv K210 自训练学习识别实验
## 操作说明
# 本案例用于演示自学习分类功能
# 1. 固定镜头位置运行程序
# 2. 准备要分类的3个物体
# 3. 镜头对准要被分类的某个物体,按下Key键拍照,每个物体需要拍5张,依次完成3个物体的记录
# 4. 再按一次Key键开始训练分类,并从1起始按顺序给物体标记序号
# 5. 记录完后将待检测物品放置到镜头前即可检测分类显示序号及相似度分数
#####################################################
import gc
import lcd
import sensor
import time
from maix import GPIO
from maix import KPU
from board import board_info
from fpioa_manager import fm
BOUNCE_PROTECTION = 100
def set_key_state(*_):
global state_machine
state_machine.emit_event(EVENT.BOOT_KEY)
time.sleep_ms(BOUNCE_PROTECTION)
class STATE(object):
IDLE = 0
INIT = 1
TRAIN_CLASS_1 = 2
TRAIN_CLASS_2 = 3
TRAIN_CLASS_3 = 4
CLASSIFY = 5
ST_MAX = 6
class EVENT(object):
POWER_ON = 0
BOOT_KEY = 1
BOOT_KEY_LONG_PRESS = 2
NEXT_CLASS = 3
EVT_MAX = 4
class StateMachine(object):
def __init__(self, state_handlers, event_handlers, transitions):
self.previous_state = STATE.IDLE
self.current_state = STATE.IDLE
self.state_handlers = state_handlers
self.event_handlers = event_handlers
self.transitions = transitions
self.records = dict()
def get_next_state(self, cur_state, cur_event):
'''
根据当着状态和event, 从transitions里查找出下一个状态
:param cur_state:
:param cur_event:
:return:
next_state: 下一状态
None: 找不到对应状态
'''
for cur, next, event in self.transitions:
if cur == cur_state and event == cur_event:
return next
return None
def execute_state_action(self, state):
'''
执行当前状态action函数
:param state:
:return:
'''
try:
self.state_handlers[state](self)
except Exception as e:
print("Exception")
print(e)
def emit_event(self, event):
next_state = self.get_next_state(self.current_state, event)
if next_state == None:
return
print("event valid: {}, cur: {}, next: {}".format(event, self.current_state, next_state))
self.previous_state = self.current_state
self.current_state = next_state
self.execute_state_action(self.current_state)
def engine(self):
pass
def state_init(self):
global msg_notification
print("current state: init")
msg_notification = "Prepare 3 objects pls\r\nWill take 5 pics each\r\n\r\nPress boot key to\r\nstart"
def state_idle(self):
global msg_notification
print("current state: idle")
msg_notification = None
def state_train_class_1(self):
global kpu, msg_notification, features, train_pic_cnt
global state_machine
print("current state: class 1")
if train_pic_cnt == 0: # 0 is used for prompt only
features.append([])
train_pic_cnt += 1
msg_notification = "Train object 1\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt < max_train_pic:
img = sensor.snapshot()
feature = kpu.run_with_output(img, get_feature=True)
features[0].append(feature)
train_pic_cnt += 1
msg_notification = "Train object 1\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt == max_train_pic: # prompt for next
msg_notification = "Change to another\r\nobject please"
train_pic_cnt += 1
else:
train_pic_cnt = 0
state_machine.emit_event(EVENT.NEXT_CLASS)
def state_train_class_2(self):
global kpu, msg_notification, features, train_pic_cnt
global state_machine
print("current state: class 2")
if train_pic_cnt == 0:
features.append([])
train_pic_cnt += 1
msg_notification = "Train object 2\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt < max_train_pic:
img = sensor.snapshot()
feature = kpu.run_with_output(img, get_feature=True)
features[1].append(feature)
train_pic_cnt += 1
msg_notification = "Train object 2\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt == max_train_pic:
msg_notification = "Change to another\r\nobject please"
train_pic_cnt += 1
else:
train_pic_cnt = 0
state_machine.emit_event(EVENT.NEXT_CLASS)
def state_train_class_3(self):
global kpu, msg_notification, features, train_pic_cnt
global state_machine
print("current state: class 2")
if train_pic_cnt == 0:
features.append([])
train_pic_cnt += 1
msg_notification = "Train object 3\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt < max_train_pic:
img = sensor.snapshot()
feature = kpu.run_with_output(img, get_feature=True)
features[2].append(feature)
train_pic_cnt += 1
msg_notification = "Train object 3\r\n\r\nBoot key to take #P{}".format(train_pic_cnt)
elif train_pic_cnt == max_train_pic:
msg_notification = "Training is completed!\r\n\r\nPress boot to continue"
train_pic_cnt += 1
else:
train_pic_cnt = 0
state_machine.emit_event(EVENT.NEXT_CLASS)
def state_classify(self):
global msg_notification
print("current state: classify")
msg_notification = "Classification"
def event_power_on(self, value=None):
print("emit event power_on")
def event_press_boot_key(self, value=None):
global state_machine
print("emit event boot_key")
def event_long_press_boot_key(self, value=None):
global state_machine
print("emit event boot_key_long_press")
# state action table
state_handlers = {
STATE.IDLE: state_idle,
STATE.INIT: state_init,
STATE.TRAIN_CLASS_1: state_train_class_1,
STATE.TRAIN_CLASS_2: state_train_class_2,
STATE.TRAIN_CLASS_3: state_train_class_3,
STATE.CLASSIFY: state_classify
}
# event action table, can be enabled while needed
event_handlers = {
EVENT.POWER_ON: event_power_on,
EVENT.BOOT_KEY: event_press_boot_key,
EVENT.BOOT_KEY_LONG_PRESS: event_long_press_boot_key
}
# Transition table
transitions = [
[STATE.IDLE, STATE.INIT, EVENT.POWER_ON],
[STATE.INIT, STATE.TRAIN_CLASS_1, EVENT.BOOT_KEY],
[STATE.TRAIN_CLASS_1, STATE.TRAIN_CLASS_1, EVENT.BOOT_KEY],
[STATE.TRAIN_CLASS_1, STATE.TRAIN_CLASS_2, EVENT.NEXT_CLASS],
[STATE.TRAIN_CLASS_2, STATE.TRAIN_CLASS_2, EVENT.BOOT_KEY],
[STATE.TRAIN_CLASS_2, STATE.TRAIN_CLASS_3, EVENT.NEXT_CLASS],
[STATE.TRAIN_CLASS_3, STATE.TRAIN_CLASS_3, EVENT.BOOT_KEY],
[STATE.TRAIN_CLASS_3, STATE.CLASSIFY, EVENT.NEXT_CLASS]
]
####################################################################################################################
class Button(object):
DEBOUNCE_THRESHOLD = 30
LONG_PRESS_THRESHOLD = 2000
# Internal key states
IDLE = 0
DEBOUNCE = 1
SHORT_PRESS = 2
LONG_PRESS = 3
def __init__(self):
self._state = Button.IDLE
self._key_ticks = 0
self._pre_key_state = 1
self.SHORT_PRESS_BUFF = None
def reset(self):
self._state = Button.IDLE
self._key_ticks = 0
self._pre_key_state = 1
self.SHORT_PRESS_BUFF = None
def key_up(self, delta):
global state_machine
# print("up:{}".format(delta))
# key up时,有缓存的key信息就发出去,没有的话直接复位状态
if self.SHORT_PRESS_BUFF:
state_machine.emit_event(self.SHORT_PRESS_BUFF)
self.reset()
def key_down(self, delta):
global state_machine
# print("dn:{},t:{}".format(delta, self._key_ticks))
if self._state == Button.IDLE:
self._key_ticks += delta
if self._key_ticks > Button.DEBOUNCE_THRESHOLD:
# main loop period过大时,会直接跳过去抖阶段
self._state = Button.SHORT_PRESS
self.SHORT_PRESS_BUFF = EVENT.BOOT_KEY # key_up 时发送
else:
self._state = Button.DEBOUNCE
elif self._state == Button.DEBOUNCE:
self._key_ticks += delta
if self._key_ticks > Button.DEBOUNCE_THRESHOLD:
self._state = Button.SHORT_PRESS
self.SHORT_PRESS_BUFF = EVENT.BOOT_KEY # key_up 时发送
elif self._state == Button.SHORT_PRESS:
self._key_ticks += delta
if self._key_ticks > Button.LONG_PRESS_THRESHOLD:
self._state = Button.LONG_PRESS
self.SHORT_PRESS_BUFF = None # 检测到长按,将之前可能存在的短按buffer清除,以防发两个key event出去
state_machine.emit_event(EVENT.BOOT_KEY_LONG_PRESS)
elif self._state == Button.LONG_PRESS:
self._key_ticks += delta
# 最迟 LONG_PRESS 发出信号,再以后就忽略,不需要处理。key_up时再退出状态机。
pass
else:
pass
####################################################################################################################
lcd_show_fps = True
msg_notification = None
features = []
THRESHOLD = 98.5
train_pic_cnt = 0
max_train_pic = 5
fm.register(board_info.BOOT_KEY, fm.fpioa.GPIOHS0)
boot_gpio = GPIO(GPIO.GPIOHS0, GPIO.IN)
lcd.init()
sensor.reset() # Reset and initialize the sensor. It will
# run automatically, call sensor.run(0) to stop
sensor.set_pixformat(sensor.RGB565) # Set pixel format to RGB565 (or GRAYSCALE)
sensor.set_framesize(sensor.QVGA) # Set frame size to QVGA (320x240)
sensor.set_windowing((224, 224))
# sensor.set_vflip(1)
sensor.skip_frames(time=500) # Wait for settings take effect.
clock = time.clock() # Create a clock object to track the FPS.
kpu = KPU()
print("ready load model")
kpu.load_kmodel("/sd/mb-0.25.kmodel")
state_machine = StateMachine(state_handlers, event_handlers, transitions)
state_machine.emit_event(EVENT.POWER_ON)
i = 0
fps = 0
btn_ticks_prev = time.ticks_ms()
boot_btn = Button()
while True:
i += 1
gc.collect()
clock.tick() # Update the FPS clock.
# query key status during main loop
btn_ticks_cur = time.ticks_ms()
delta = time.ticks_diff(btn_ticks_cur, btn_ticks_prev)
btn_ticks_prev = btn_ticks_cur
if boot_gpio.value() == 0:
boot_btn.key_down(delta)
else:
boot_btn.key_up(delta)
img = sensor.snapshot()
if state_machine.current_state == STATE.CLASSIFY:
scores = []
feature = kpu.run_with_output(img, get_feature=True)
high = 0
index = 0
for j in range(len(features)):
for f in features[j]:
score = kpu.feature_compare(f, feature)
if score > high:
high = score
index = j
if high > THRESHOLD:
a = img.draw_string(0, 200, "class:{},score:{:2.1f}".format(index + 1, high), color=(0, 255, 0), scale=2)
if lcd_show_fps:
img.draw_string(0, 0, "{:.2f}fps".format(fps), color=(0, 255, 0), scale=1.0)
if msg_notification:
img.draw_string(0, 60, msg_notification, color=(255, 0, 0), scale=2)
lcd.display(img)
fps = clock.fps()
这段程序可以分成状态机、按键检测、摄像头初始化、模型加载、样本采集、分类比较和 LCD 显示几个部分。状态机部分通过 STATE 定义当前阶段,通过 EVENT 定义触发事件,通过 transitions 定义“当前状态 + 事件 = 下一个状态”的变化关系。这样一来,BOOT 按键在不同阶段会产生不同效果,程序流程不会混在一个复杂的 if...else 中。
按键部分由 Button 类完成。主循环持续读取 boot_gpio.value(),按下时调用 key_down(delta),松开时调用 key_up(delta)。DEBOUNCE_THRESHOLD = 30 用于按键防抖,LONG_PRESS_THRESHOLD = 2000 用于识别长按。当前流程主要使用短按事件 EVENT.BOOT_KEY 推动采样和状态切换,长按事件虽然定义了,但没有在转移表中参与主要流程。
摄像头部分使用 sensor.set_pixformat(sensor.RGB565) 设置彩色图像格式,使用 sensor.set_framesize(sensor.QVGA) 设置输入分辨率,再通过 sensor.set_windowing((224, 224)) 将图像窗口设置为 224×224。这个窗口尺寸与模型输入更匹配,也能减少后续 KPU 处理压力。主循环中每次通过 sensor.snapshot() 获取当前画面。
KPU 部分是本实验的核心。程序通过 kpu.load_kmodel("/sd/mb-0.25.kmodel") 加载模型。采样阶段使用 kpu.run_with_output(img, get_feature=True) 提取样本特征,并保存到 features[0]、features[1]、features[2] 中。分类阶段再次提取当前图像特征,然后用 kpu.feature_compare(f, feature) 与所有样本特征比较,最高分数超过 THRESHOLD 时,在画面底部绘制 class 和 score。
| 函数 / 类名 | 功能 | 对应现象 |
|---|---|---|
STATE |
定义状态编号 | 用于区分初始化、采样第 1 类、采样第 2 类、采样第 3 类和分类阶段 |
EVENT |
定义事件编号 | 用于表示上电、BOOT 短按、BOOT 长按和切换到下一类 |
StateMachine |
管理状态切换 | 串口打印当前事件、当前状态和下一状态 |
get_next_state() |
根据当前状态和事件查找下一状态 | 判断按键后应该停留、采样还是进入下一阶段 |
execute_state_action() |
执行状态对应函数 | 进入某个状态后更新提示文字或采集样本 |
emit_event() |
发出状态机事件 | BOOT 按键或程序上电时推动状态变化 |
state_init() |
初始化提示状态 | LCD 提示准备 3 个物体并按 BOOT 开始 |
state_train_class_1() |
采集第 1 类样本 | LCD 提示训练物体 1,并保存第 1 类特征 |
state_train_class_2() |
采集第 2 类样本 | LCD 提示训练物体 2,并保存第 2 类特征 |
state_train_class_3() |
采集第 3 类样本 | LCD 提示训练物体 3,完成后进入分类准备 |
state_classify() |
进入分类状态 | LCD 显示 Classification,程序开始实时比较特征 |
Button |
按键防抖和短按/长按判断 | BOOT 按键松开后触发短按事件 |
sensor.snapshot() |
获取图像帧 | LCD 显示摄像头实时画面 |
kpu.run_with_output() |
提取图像特征 | 采样阶段记录特征,分类阶段提取当前特征 |
kpu.feature_compare() |
比较两个特征相似度 | 得出当前物体和样本物体的相似度分数 |
img.draw_string() |
在图像上绘制文字 | 显示 FPS、提示信息、分类编号和相似度 |
lcd.display(img) |
显示图像帧 | LCD 实时显示摄像头画面和文字结果 |
需要注意的是,当前代码中 state_train_class_3() 内部的串口打印写成了 print("current state: class 2"),但后续逻辑实际是在训练第 3 类,因为它使用的是 features[2] 和 Train object 3 提示。这个问题只影响串口文字,不影响第三类采样逻辑。如果希望串口输出更一致,可以把这行文字改成 current state: class 3。
扩展应用
自训练学习识别实验常见问题集中在模型文件路径、摄像头画面、BOOT 按键、样本采集质量、阈值设置和分类阶段显示几个方面。排查时应优先确认摄像头能显示、模型能加载、BOOT 按键能触发状态变化,再检查识别结果是否稳定。
| 问题现象 | 可能原因 | 处理思路 |
|---|---|---|
| 程序启动后模型加载失败 | /sd/mb-0.25.kmodel 文件不存在,SD 卡未识别,文件名不一致 |
将模型文件放到 SD 卡根目录,确认路径与 load_kmodel() 参数完全一致 |
| LCD 没有画面 | 摄像头初始化失败、LCD 初始化失败、程序没有进入主循环 | 先运行摄像头显示实验,确认 sensor.snapshot() 和 lcd.display() 正常 |
| 按 BOOT 没有反应 | BOOT 按键映射不正确,按键电平没有变化,状态机没有收到事件 | 打印 boot_gpio.value(),确认按下时是否变为低电平 |
| 状态一直停在初始化 | BOOT 短按事件没有触发 | 检查 Button 防抖逻辑,确认松开按键后能调用 emit_event(EVENT.BOOT_KEY) |
| 采样后识别不稳定 | 物体距离、角度、光照或背景变化较大 | 固定摄像头位置,保持采样和识别时的距离、背景、光照尽量一致 |
| 分类结果不显示 | 最高相似度没有超过 THRESHOLD = 98.5 |
观察分数变化,必要时适当降低阈值,或重新采集更稳定样本 |
| 分类结果经常误判 | 三类物体外观太相似,采样角度过少,背景干扰明显 | 选择差异更明显的物体,采集时让目标占据画面中心,并减少杂乱背景 |
| FPS 较低 | KPU 特征提取、LCD 绘制和串口输出都会消耗时间 | 减少无关打印,保持 224×224 窗口,避免额外图像处理 |
| 串口提示第三类却显示 class 2 | state_train_class_3() 中打印文字写错 |
将该函数中的 print("current state: class 2") 改为 class 3 |
| 训练提示和实际采样次数不易对应 | train_pic_cnt 同时承担提示和计数作用 |
按 LCD 提示逐步操作;如需严格控制采样张数,可单独优化计数逻辑 |
自训练学习识别实验的价值不只在于复现实验现象,更在于建立现场 AI 分类项目的基本框架。摄像头负责输入,KPU 模型负责提取特征,BOOT 按键负责控制采样流程,状态机负责管理阶段变化,LCD 负责显示提示和识别结果。后续只要替换样本对象或加入新的输出模块,就可以扩展成更多 AI 交互项目。
| 应用场景 | 实现思路 | 可扩展能力 |
|---|---|---|
| 现场物体分类 | 现场采集几个物体的图像特征,再实时比较当前物体 | 可用于教学分类演示、简单物品识别和交互展示 |
| AI 教学演示 | 展示样本采集、特征提取、相似度比较和阈值判断 | 适合讲解模型不是只输出标签,也可以输出特征 |
| 快速样本训练 | 使用 BOOT 按键快速记录多类样本 | 可扩展按键菜单或 LCD 操作流程,让采样过程更清晰 |
| 互动识别游戏 | 准备几种物体,让程序判断当前放入的是哪一类 | 可加入蜂鸣器、LED 或数码管反馈识别结果 |
| 创客项目原型 | 把自学习分类作为项目输入条件 | 可联动舵机、电机、继电器或语音提示模块 |
| 视觉调试工具 | 显示 FPS、提示信息和分类分数 | 可用于观察光照、背景、阈值对识别效果的影响 |
| 离线识别展示 | 不依赖电脑端训练,直接在开发板上完成采样和分类 | 适合课堂演示和便携式 AI 项目展示 |
从工程角度看,建议继续保持“采集、判断、执行、显示”分层。采集层负责摄像头和按键输入,判断层负责状态机和相似度比较,执行层负责分类结果触发,显示层负责 LCD 提示。这样后续扩展蜂鸣器报警、LED 状态灯、舵机分拣、网络上传或数据记录时,不需要推翻原有代码结构。
总结
本实验通过 CanMV K210 完成了一个现场自训练学习识别流程,核心能力包括摄像头采集、LCD 显示、BOOT 按键输入、状态机管理、KPU 模型加载、图像特征提取、样本特征记录、相似度比较和分类结果显示。它展示的不是固定标签识别,而是通过现场采样建立类别特征,再对新图像进行相似度判断。
这类实验非常适合作为 K210 AI 视觉课程中的综合案例。它把摄像头显示、按键输入、模型推理、LCD 提示和工程调试串联在一起,也为后续物体分类、交互游戏、视觉分拣、智能提示和离线 AI 演示提供了基础。实验现象异常时,应优先检查摄像头画面、BOOT 按键状态、模型文件路径、SD 卡识别和 LCD 提示,再结合串口输出逐步定位问题。
更多推荐


所有评论(0)