☰
K210视觉识别实战:电赛送药小车数字识别与STM32联调全解析
2026/10/10 20:31:47 网站建设 项目流程

简介:全国大学生电子设计竞赛智能送药小车项目的K210数字识别模型及配套代码,适合参赛学生和嵌入式AI开发者。压缩包共8个文件,包含Python脚本(boot.py、boot2.py)、txt说明文件(README.txt、labels.txt等)、jpg图片(report.jpg、startup.jpg)以及kmodel神经网络模型(m.kmodel),总大小仅1.59MB。已有1173人学习下载。其中kmodel文件为K210可运行的神经网络模型,配合脚本可实现数字识别;boot.py、boot2.py等代码可帮助理解系统启动与识别流程,jpg图片则提供接线示意图或运行效果参考。通过学习和二次开发,读者能快速上手K210的模型部署,掌握将数字识别技术融入送药小车、实现药品编号识别与自动配送的完整思路,为赛题冲刺和实际项目落地打下基础。 电赛备赛那阵子,我们团队在“智能送药小车”这道题上卡了最久的就是视觉识别。明明小车底盘、电机驱动、灰度循迹都能跑通了,可只要识别病房数字这一环掉链子,整个任务评分直接崩盘。后来换了K210方案,才真正把“看得见、认得准”这件事稳定下来。K210这块芯片在电赛圈子里已经被用到烂熟,双核RISC-V处理器自带KPU神经网络加速单元,跑轻量级数字识别模型非常顺,配上摄像头模块和MaixPy固件,从模型训练到板上推理,几天光景就能出活。这篇文章就把我从数据采集、模型训练、烧录部署,到K210与STM32串口通信、再到整车联调的完整链路掰开揉碎讲清楚,不光是贴代码,每一步为什么这么做、有哪些坑,全部写在下面,给正在备赛的队伍一条能直接复现的路。

1. 电赛送药小车任务拆解:视觉部分到底在解决什么问题

1.1 从评分规则倒推视觉需求

送药小车这类赛题,场上要跑的动作拆开看,无非是:小车从药房出发,沿着地面引导线走,走到病房门口停住,把药放下,再返回。关键评分点多半落在“能不能准确停在指定病房”上,而病房的区分标识,最常见的就是门口贴的编号数字。也就是说,视觉系统的首要任务不是炫技,而是把“房门口那个数字是多少、它在画面里什么位置”这两个信息稳定地交给主控。

很多队伍一开始把视觉想象得很复杂,又是测距又是建图,实际上赛题对视野和精度的要求相对集中:识别范围大概就是小车正前方几米内的门牌区域,数字大小从几厘米到十几厘米不等,光线条件因场地而异。把需求拆到这一步,你就知道视觉模块需要输出什么了:一是目标数字的类别编号,比如0到9;二是目标在画面中的位置坐标,这样主控才能根据偏移量来调整车身姿态,保证最后停车时车头正对病房门。如果识别模型只给结果不给位置,后面做闭环控制会非常难受。

这也是为什么我建议在视觉端直接做目标检测,而不是只做个分类器来回传“我看到数字几了”。目标检测输出的边界框中心点坐标,配合画面中心参考线,就能算出一个横向偏移量,告诉STM32该左转还是右转。这个偏移量在整车调试里比单纯一个数字值有用得多,后面我会详细展开。

1.2 为什么选择K210而不是OpenMV或树莓派

备赛选型时大家通常纠结三个方向:OpenMV、树莓派、K210。我直接说结论:电赛送药小车场景下,K210是性价比和稳定性最平衡的选择。

OpenMV优点是好上手,MicroPython语法,很多教程,但它的算力应对简单的色块识别还行,真要跑数字检测模型,帧率会掉到让人崩溃。而且OpenMV的MCU主频不高,图像分辨率一大就吃力,视觉识别和串口通信同时跑时,经常出现CPU占用过高导致的时序抖动。

树莓派算力强,模型能随便跑,但它的问题在电赛里很致命:启动时间长、体积大、供电要求高、受静电和反接影响容易莫名重启。比赛现场是不可能给你时间等树莓派慢慢开机的,很多队伍在树莓派冷启动上吃过亏。

K210刚好卡在中间:算力够用,内置KPU硬件加速器,跑一个经过量化压缩的YOLO模型,实测帧率能到20到30帧,这在电动车场景下完全够用;启动速度快,上电即用;支持MicroPython(MaixPy),开发效率不低;裸板价格便宜,烧坏了换一块不心疼。更关键的是,K210的GPIO和UART、I2C接口齐全,和STM32通信非常直接,不需要额外的协议转换芯片。

1.3 系统分工:K210看,STM32跑

在整车架构上,我的建议是让K210专注于视觉感知,STM32负责运动控制和逻辑调度。这样分工清晰,排查问题也方便。

K210端要做的事:上电初始化摄像头,加载训练好的数字识别kmodel,持续取帧推理,把识别到的数字类别、目标框中心x坐标、目标框宽度、置信度,通过串口周期性地发给STM32。STM32端要做的事:接收K210的数据,解析出数字和偏移量,结合灰度循迹传感器的状态,决定直行、转向还是停车,控制电机转速和方向。

这个架构能跑得稳,有个隐形前提:K210和STM32之间的通信协议必须定义得足够简单可靠。很多队伍栽在通信上,K210发得高兴,STM32收得一脸懵,数据错位、丢帧、校验失败都是家常便饭。协议的设计我在第4章专门讲。

2. K210数字识别模型训练与部署:数据决定了识别的上限

2.1 数据采集与标注:别偷懒,背景越杂越好

很多人以为K210上跑数字识别是“模型一发就能用”,其实最花时间的往往不是调模型,而是准备数据。我们在备赛初期直接用网络上现成的手写数字数据集训练,结果一到比赛场地上,识别率惨不忍睹。原因很简单:门牌数字不是MNIST里那种规整的印刷体,它可能是打印体、亚克力雕刻、胶带贴的数字,周围有门框、墙面纹理、光线阴影,背景杂乱程度远超普通数据集。

正确的做法是自己搭一个数据采集流程。把摄像头装在车模上,推着车在不同距离、不同角度下拍摄印好的数字门牌,模拟比赛中会出现的各种视角。建议每类数字至少拍200到300张,把顺光、逆光、侧光、远一点、近一点、稍微歪斜的情况都覆盖到。拍完以后统一缩放到训练输入尺寸,再用labelImg这类工具标注,存成YOLO格式的txt文件。如果实在时间不够,可以用翻转、亮度调整、随机裁剪做数据增强,把有限的样本扩到三到五倍。记住,背景越杂、样本越多样,模型在赛场上的泛化能力就越强。

标注的时候还有个细节容易忽略:数字框尽量紧贴数字边缘,不要留太多白边,也不要切掉笔画。白边太多会让模型学到过多背景特征,切掉笔画则会干扰数字形状的学习。我见过有队伍因为标注框太随意,训练出来的模型把门框的竖向边缘当成数字1的一部分,到赛场上狂出错。

2.2 模型选型与训练:分类器还是目标检测

在K210上做数字识别,有两种常见技术路线:一种是用MobileNet之类的轻量分类网络,先通过二值化或颜色提取找到数字区域,裁剪出来再分类;另一种是直接用YOLO目标检测,一步到位输出数字框和类别。

我强烈推荐后者。原因在于:分类器依赖前一阶段能稳定地把数字区域从复杂背景里抠出来,这个“抠”的过程在赛场光照变化下非常脆弱,画质稍微一变,裁剪区域就歪了。目标检测模型直接把“在哪里”和“是什么”一起解决,场景的鲁棒性要好得多。K210的KPU跑YOLO系列轻量模型完全可行,我们用的是YOLOv5n结构,经过int8量化后模型体积大概2MB左右,单帧推理时间在30到50毫秒,完全在可接受范围内。

训练时候,内存充足且时间允许的话,可以用Ultralytics YOLOv8在本地训练,导出ONNX,再转成K210能加载的格式。但比赛节奏紧的话,直接在MaixHub上在线训练更省心,上传标注好的数据集,选YOLOv5轻量模型,训练完成后平台直接给kmodel下载链接。无论哪条路,训练时有个共同要点:类别数就设10个(数字0到9),不要额外加类别,减少模型负担,提升精度。

2.3 模型转换与烧录:kmodel才是K210看得懂的格式

K210芯片不能直接加载你在训练平台里用的torch或tflite模型,它只认经过NNCase工具链转换出来的kmodel格式。这个转换步骤看起来不起眼,实际翻车率极高。

本地训练的话,流程是:先用Ultralytics导出tflite或ONNX权重,再用K210官方的NNCase工具做量化转换,指定输入尺寸、量化方式,最终输出.kmodel文件。转换时最容易踩的坑是输入尺寸不一致。训练时你用的是640x640或320x320,转换时如果输入尺寸写错,推理阶段图像resize的宽高比就会对不上,模型实际看到的画面是变形拉伸的,识别率直线下降。建议统一用320x320,兼顾精度和K210推理速度。

烧录方面,K210有两种承载方式:一是把kmodel烧写到Flash里,二是放在SD卡上。我建议优先放在SD卡,因为比赛中需要频繁换模型测试,从SD卡加载模型,只要换文件就行,不需要每次刷Flash。固件方面,MaixPy固件版本要和模型转换工具版本匹配,不然可能出现K210报错“load error”或者推理结果乱跳。烧录可以用kflash_gui这个工具,选对串口、波特率、固件文件,写入后复位即可。

3. K210端识别代码拆解:从图像到串口的一整条链路

3.1 初始化摄像头并加载模型

K210跑MaixPy,代码风格和MicroPython基本一致,熟悉OpenMV的队友上手会非常快。第一步是初始化外设、摄像头和KPU。

import sensor, image, lcd, time from machine import UART from fpioa_manager import fm lcd.init() sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(1) sensor.set_hmirror(1) sensor.run(1) import KPU as kpu task = kpu.load("/sd/num_detect.kmodel") kpu.init_yolo2(task, 0.3, 0.3, 5, 3)

注意set_vflip和set_hmirror这两个函数,很多人刚上手会漏掉。摄像头安装角度不同,画面可能是上下颠倒或者左右镜像的,如果不做翻转,模型输入到KPU里的图像方向就是错的。翻转方式根据你的摄像头固定方向决定,调试的时候先在屏幕上画个箭头确认方向,再接模型推理。

3.2 推理主循环与结果解析

初始化完成后就是主循环:取帧、跑模型、解析检测结果、通过串口发出去。核心代码逻辑如下:

fm.register(10, fm.fpioa.UART1_TX, force=True) fm.register(11, fm.fpioa.UART1_RX, force=True) uart = UART(UART.UART1, 115200, 8, None, 0, read_buf_len=256) def send_result(detect_num, center_x, box_w, confidence): data = bytearray([0xAA, 0x55, detect_num & 0xFF, center_x >> 8, center_x & 0xFF, box_w >> 8, box_w & 0xFF, int(confidence * 100)]) checksum = 0 for b in data: checksum += b data.append(checksum & 0xFF) uart.write(data) while True: img = sensor.snapshot() objects = kpu.run_yolo2(task, img) if objects: best = None best_score = 0 for obj in objects: if obj.value() > best_score: best_score = obj.value() best = obj if best: x = best.x() y = best.y() w = best.w() h = best.h() cls_id = best.classid() cx = x + w // 2 send_result(cls_id, cx, w, best_score) lcd.display(img)

这段代码里有几个关键决策。第一,多个检测框出现时取置信度最高的那个。场景里可能同时出现两个数字框,尤其是门牌号可能是两位数,我们只取最明显的那个作为主目标,避免STM32执行逻辑混乱。第二,发送框中心坐标而不发送左上角坐标,因为后续转向控制更关心目标相对画面中心的偏移。第三,坐标拆成高字节和低字节发送,因为串口一帧数据最多发一个字节,数值大于255就必须拆分,接收端再拼回去。

3.3 提高识别稳定性的几个代码技巧

模型推理跑通以后,接下来就是扣稳定性。有三个代码层面的优化我非常推荐。

第一个是限制ROI区域。摄像头装在小车前方,真正有用的画面往往是画面中下部分,上半部分通常是天花板或者远处环境,既浪费算力又容易带进干扰。可以在sensor.snapshot()之后、模型推理之前,用image.crop或者直接设置窗口,把有效识别区缩小到中下部,比如取0到240行的下半段。ROI缩小后,模型输入图像里的门牌面积占比变大,识别率会有明显提升。

第二个是延时锁存策略。比赛中小车是运动的,画面里数字会出现、消失、再出现。如果每一帧都把识别结果发给STM32,会出现数字来回跳变,STM32一会儿觉得看到了3,一会儿觉得看到了5,控制逻辑直接乱掉。我在实际项目里加了一个简单的状态锁存:连续5帧识别到同一个数字,才认为结果有效并发送;如果识别不到数字,连续10帧以后才发送一个无效帧,告诉STM32“视觉丢了”,避免瞬时抖动干扰。

第三个是跳帧推理。K210的KPU推理虽然快,但每帧都推理对画面流畅度和功耗都有压力。如果只是做门牌识别,不需要每一帧都跑模型,可以采用每次取帧后先显示,再每两帧跑一次推理的方式,实际效果几乎不受影响,但系统整体流畅度好了很多。

4. K210与STM32的通信协议:让识别结果驱动车轮

4.1 为什么用UART而不是I2C

K210和STM32通信,有人用I2C有人用UART,我排除了I2C,原因很实在:I2C主机从机通讯需要处理时钟同步、应答信号、总线仲裁,K210的MicroPython里用I2C从机模式调试起来非常费劲,尤其比赛现场没有太多时间让你慢慢抠时序。UART串口是全双工,一根TX一根RX加一根共地就完事,逻辑简单,出错容易排查。

接线方面,只需要把K210的UART1_TX接到STM32的某一个串口RX,K210的UART1_RX接到STM32的TX,两边GND一定要连在一起,否则信号没有参考电平,会出现随机乱码。选引脚时要查K210的fpioa功能映射表,MaixPy允许把任意GPIO绑定到UART功能,我习惯用IO10和IO11,代码里fm.register就干的这件事。STM32端选一个空闲的USART,比如USART2,配置成115200 8N1。

4.2 帧协议设计:不能裸发数据

很多新手直接把一个整数用uart.write发过去,STM32收到就解析,这样短距离测试可能会通,但在电机转起来以后必炸。原因有二:第一,电机产生的电磁干扰会污染串口信号,单字节数据错位后没法纠正;第二,如果数据有多帧结构,接收端不知道一帧从哪里开始到哪里结束,解析完全靠猜。

我设计了一个非常轻量的帧协议,实际项目里跑了很久都没出过问题:

  • 帧头:0xAA 0x55,两个字节,用于接收端识别帧起始。
  • 有效数据区:数字类别1字节、中心坐标x高字节1字节、中心坐标x低字节1字节、框宽高字节1字节、框宽低字节1字节、置信度百分比1字节。
  • 校验和:前面所有字节相加后取低8位,放在帧尾。

接收端用状态机判断:等第一个0xAA,再等第二个0x55,都匹配才开始接收数据区,收满固定长度后计算校验和,比对通过才认为这一帧有效。加上帧头和对齐校验的好处是,就算串口中间丢一个字节,下一帧也能通过重新寻帧头恢复同步,系统不会彻底卡死。

4.3 STM32端中断接收与状态机解析

STM32端的代码建议用串口空闲中断加定时器超时处理,或者更简单的方式:串口接收中断里把每个字节塞进一个环形缓冲区,主循环里跑状态机解析。下面是一个通用解析框架:

uint8_t rx_buffer[16]; uint8_t rx_index = 0; uint8_t expect_len = 8; uint8_t state = 0; void ParseByte(uint8_t b) { switch (state) { case 0: if (b == 0xAA) state = 1; break; case 1: if (b == 0x55) state = 2; else state = 0; break; case 2: rx_buffer[0] = b; rx_index = 1; state = 3; break; case 3: rx_buffer[rx_index++] = b; if (rx_index >= expect_len) { uint8_t sum = 0; for (int i = 0; i < expect_len - 1; i++) sum += rx_buffer[i]; if (sum == rx_buffer[expect_len - 1]) { // 数据有效,取 rx_buffer[0] 为数字,1~2 为中心坐标,3~4 为框宽 HandleVisionResult(rx_buffer[0], ((uint16_t)rx_buffer[1] << 8) | rx_buffer[2], ((uint16_t)rx_buffer[3] << 8) | rx_buffer[4]); } state = 0; } break; } }

解析出来以后,在HandleVisionResult里,STM32就能拿到:识别到的数字类别、目标在画面中的中心x坐标、目标框宽度。有了这三个量,运动控制逻辑就可以做很多事情了。

5. 识别与控制的联动:从“看到数字”到“停在病房”

5.1 目标坐标偏移量的价值

光识别到数字还不够,K210传给STM32的目标中心坐标x是让小车对准病房门的关键。假设画面分辨率是320x240,画面中心x坐标就是160。如果识别到目标的中心x是180,说明目标目前偏右,小车需要右转一点让目标回到画面中央;如果目标中心x是120,说明目标偏左,小车需要左转修正。

实际控制里,我建议把偏移量归一化,用(center_x - 160) / 160得到一个从-1到1的偏差值,再乘一个比例系数,作为转向舵机或者差速轮的速度补偿。这个P控制器看起来简单,在电赛场景里比复杂的PID更实用,因为目标距离近、速度慢,只要方向修正连续,车身姿态就不会有大幅震荡。

5.2 到站判定逻辑

很多队伍有个误区:识别到目标数字就立刻刹车。但小车在运动中,摄像头视野里第一次出现正确数字时,车辆离病房可能还有几十厘米远,直接刹车停在半路,距离不达标。正确的做法是设定一个“识别确认窗口”:当连续若干帧都识别到正确数字,且识别框宽度w逐渐增大到一定阈值时,才认为小车已经接近门口,触发停车减速。

这个思路的本质是用目标框宽度作为距离的代理值。假设模型在距离病房30厘米时,识别框宽度可能超过150像素,而刚看到数字时可能只有50像素。所以代码里可以这么写:识别到目标数字正确,同时box_w大于某个阈值(比如120),同时小车还在直行状态,才执行停车动作。具体的阈值需要在实际场地里标定,不同摄像头高度和视角下差异挺大,不要照搬别人的数值,一定现场测。

5.3 多病房场景的决策流程

当比赛要求小车在多个病房之间选择时,视觉和控制的联动就要带上状态机。我的建议是STM32维护一个任务状态:

  1. 状态A:从药房出发,循迹直行,等待K210上报数字。
  2. 状态B:K210上报数字与目标数字不匹配,继续直行或切换到下一分支路线。
  3. 状态C:K210上报数字与目标数字匹配,按照偏移量修正车身方向,向门口靠拢。
  4. 状态D:目标框宽度达到阈值,执行停车、放下药品,倒车或掉头返回。

这里要注意,K210上报的数字可能会有瞬时误判,不能一收到匹配数字就立刻从循迹状态切换到进病房状态,必须连续确认几帧。我在前面代码里加的连续5帧锁存就在这里起到关键作用。另外,如果目标是识别的数字超过1位,比如病房号是12,建议处理策略是只识别个位数字,或者分时识别两个数字,这取决于赛题的具体要求。最简单的做法是,把门牌号设计成单个数字,或者让K210同时输出两个框,主控用两个框的相对位置关系决定读作“12”还是“21”,但这个逻辑需要提前测试,别等到比赛现场才临时加。

6. 实测翻车记录与调试经验

6.1 光线和反光带来的误识别

比赛场地为了拍视频,灯光通常很亮,地面和门牌表面容易反光。有一次模拟测试,模型把数字7认成了1,找了半天原因,发现是门牌表面有一道斜向反光,刚好把7的横笔画遮住了。解决办法有三个层次:一是采集数据时专门加一组带反光、高光的样本,让模型自己学会忽略这种干扰;二是调整摄像头曝光参数,减少过曝区域;三是在最终部署时选择哑光材质打印门牌,而不是镜面亚克力。后两个治标,第一个治本。

另外,如果赛题允许自己贴门牌,强烈建议用大号黑体或等线体数字,笔画粗、间距大的字体识别率远高于花体。比赛不是设计比赛,识别稳定才是第一位。

6.2 识别距离太近,刹车来不及

这是我们整个调试过程中遇到的最大问题。模型在最开始能识别到的最大距离只有不到20厘米,小车速度稍微快一点,等识别确认完已经冲过病房门口了。后来通过两件事改善了:一个是把模型输入从320x320之外又增加了ROI裁剪,让远处的小数字在画面里更清晰;另一个是降低电机PWM限幅,让小车进入识别区域前就提前减速,而不是等看到数字才减速。说白了,视觉识别有它的物理极限,运动控制必须配合它把速度降下来。

6.3 串口丢帧与供电干扰

小车电机一转,串口就开始乱码,这是最常见的混合信号污染问题。排查步骤从硬件到软件:先确认K210和STM32共地,再把串口线从电机电源线和电机驱动线旁边拉开,最后在串口TX线对地加一个100nF的小电容滤高频干扰。如果还不行,降低波特率到9600或19200,误码率会明显下降。我们最终用的是115200配硬件滤波,实测稳定,但不同车模情况不同,不要盲目照搬。

6.4 模型连续推理导致的卡顿和过热

K210跑KPU推理时,芯片温度会明显上升,尤其是夏季赛题,连续跑半小时后,偶尔出现推理结果变慢甚至空跑的现象。那时候别急着怀疑模型,先摸一下芯片外壳,烫手的程度就说明需要加个散热片了。小铝片加硅胶垫贴在K210背面,温度能降不少。同时可以在代码里适当降低推理频率,避免KPU长时间满载。

最后再说一点个人体会

备赛的最后几天,我们几乎每天都在改数据、调阈值、重烧模型。现在回头看,数字识别模型本身不是什么高深的东西,真正拉开队伍差距的,是数据质量、协议设计、系统联动这些看起来不起眼的工程细节。如果你正准备做同类型的赛题,我建议第一优先级是尽早把“识别——通信——停车”这条主线跑通,把所有关键阈值预留成可配置参数,不要写死在代码里,这样临场调整速度会快很多。等主线稳定了,再去优化帧率、识别距离这些性能指标。方向对了,车就一定能把药送到。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询