前言
博主已经大三,想着暑假参加电赛,于是将21电赛的控制题尝试做了一下,由于成本有限,想着能不能仅使用一块k210完成这个项目,但是看网上查找资料都没人这样做,于是博主就按照自己的想法实现了这个,为了让更多和博主一样的小白能够简单制作这个项目,博主决定写一下这个项目,顺便记录一下自己的学习过程,顺便希望暑假电赛一切顺利。
因为代码还没有注释整理好,所以博主暂时只分享出数字识别模型(当初博主想找网上现成的模型,由于营销号都限制收费获取,博主一怒之下暴训500张图,实现了0.97的识别率)
博主的车还是很慢的
小车的结构照片和实现功能视频博主还没拍,之后添加链接补上~
实现成本
- K210——实现循迹+数字识别
- stm32f103c8t6——实现小车主控
- 小车底盘+两个直流减速编码电机+万向轮
- M3型号铜柱,螺丝若干
- 电池,电机驱动,oled,稳压模块
赛题要求
赛题分析
题目基础部分要求使用一辆小车实现近端,中端,和远端的送药过程
并且给出了红线用于循迹,那么比赛的需求就是重点考察以下几个功能
- 1 红线循迹功能
- 2 数字识别功能
首先对于红线循迹,由于寻常传感器大多是寻黑线,大多对于红线识别效果不好,市面大多采用多路灰度传感器循迹,但是由于成本较高,有些朋友会望而却步,经济实力足够的朋友采用这种方法循迹效果自然更好。同时,也具备另一种解决方法摄像头循迹,本人采用的就是这个方法;另一个功能数字识别也有两种常用解决方法:1、使用opemmv的模板匹配;2、使用K210训练识别模型,本人采用的也是第二个选项。(博主也是刚入门的菜鸟,如果还有博主没想到的实现方法,请大佬指点)
那么这里很明显,要想实现低成本制作,我们可以把循迹和数字识别结合在一起解决,这样成本当然最低,同时opemmv价格上是大于K210的,所以博主决定使用K210实现这两个功能
ps:由于赛题为直线,有的大佬不采用循迹,而是使用编码器和电机搭配位置环控制,做到行走固定路线,实在是很牛逼的想法啊。
这种灰度传感器就不需要使用了,本人亲测排雷,还浪费了我一个礼拜,贪小便宜吃大亏。
k210任务
我的做法是将k210的需求分解成4个任务
- 开机识别数字
- 循迹加路口识别
- 循迹加终端识别
- 匹配数字并给出转向结果
至于什么时候执行对应的任务,则由stm32通过串口给出指令。这里有个细节就是K210并没有串口中断,但是你又不可能定时接收,这样做即浪费及时间和资源,实时性也没有保障,所以我选择使用stm32和k210连接一条外部中断线,这样stm32发送数据前将那条线拉低或者拉高实现K210在中断里面执行。实现伪串口中断,当时博主也是灵光一闪想到的,还是挺机智的。
解决了通信问题,接下来就是K210的重点功能实现。
- 红线循迹+路口识别+终点识别采用寻色块法
将三个色块的中心坐标经过给不同的权重,计算平均加权后的横坐标和目标直线的横坐标的差值作为误差,给到小车的转向环pid。当然方法还是很多的,网上常见的还有使用库函数实现线性回归拟合成一条直线。
ps:博主在实现这个模块的过程中遇到了一个问题,那就是颜色阈值的确定,博主实际条件惨烈,使用的是胶布实现的赛道布置,并且不知道是不是由于反光,使用阈值编辑器自己得出看似效果不错的阈值实际效果很差,最后博主也没有找到解决方法,而是询问chatgpt使用它大概提供的阈值范围,在阈值编辑器效果不好,但是实际效果绝佳。也是一件玄学的事,希望有大佬能指点有什么找阈值的好方法。
言归正传,既然循迹使用的是寻色块,那么为了简单化,博主将识别终点和识别路口都使用色块法,识别终点的逻辑是当视野中没有了红线视为终点,识别路口则是因为普通循迹状态下的色块识别的宽度和路口的宽度差距大,作为判断条件。当识别到宽度大于某个值的色块,视为识别路口
以上方法略微取巧,当然识别路口也可以使用识别黑色个数,但是那样又需要在弄一次黑色阈值,相当浪费时间,同时识别路口也有别的方法,不过博主这样是为了方便和循迹模块连接。下面是博主的代码部分,没写注释,代码细碎,看不懂没关系,博主之后将写好注释的代码整理出来。
ROIS={
'down': (0, 164, 220, 20),
'middle': (0, 144, 220, 20),
'up': (0, 124, 220, 20)
}
state_1=(0,120,224,10)
#判断是什么状态的循迹 2是识别十字路口 3是识别终点
def track():
global route_num
global code_flag
m=0#记录识别的数量
n=0#记录路程
img = sensor.snapshot()#.binary([(61, 19, 18, 91, 62, -21)])
#img.draw_rectangle(10,20,100,60,color = (255, 0, 0))(100, 0, 18, 91, 62, -21)
#img.draw_rectangle(170, 50 , 30, 50,color = (255, 0, 0))
# img =img.binary([(0, 25)])
if(code_flag==2): #路口循迹还是终点循
#for area in state_1:#路口识别
a=img.find_blobs([(22, 100, 36, 100, -8, 67)],roi=state_1,merge=True,pixels_area=100)
tmp=find_max(a)
if(tmp):
#m=m+1
if(tmp[2]>60):
uart_A.write(send_data_packet(0,1,0))#检测到了路口
print('ok1')
code_flag=0
return None
'''
if(code_flag==3): #终点循迹
a=img.find_blobs([(22, 100, 36, 100, -8, 67)],roi=[0, 164, 220, 20],merge=True,pixels_area=60)#识别红线
if (a==None):
# for b in a:
# m=m+1 #统计检测到的黑色色块的数量
# if(m>6): #统计到五块以上
uart_A.write(send_data_packet(0,1,0))#检测到了终点
print('ok2')
print(m)
code_flag=0
return None
'''
m=0
for area in ROIS:
a=img.find_blobs([(22, 100, 36, 100, -8, 67)],roi=ROIS[area],merge=True,pixels_area=25)
tmp=find_max(a)
if(tmp):
img.draw_rectangle(tmp[0:4])
img.draw_cross(tmp[5], tmp[6])
n=n+tmp[5]
m=m+1
print(tmp[5])
if((m==0)and (code_flag==3)):
uart_A.write(send_data_packet(0,1,0))#检测到了终点
print('ok2')
code_flag=0
return None
#print(m)
code_flag=0
uart_A.write(send_data_packet(((n/m-115+200)*0.16-12),0,0))
print(n/m)
print(send_data_packet(((n/m-115+200)*0.15-10),0,0))
lcd.display(img)
以上是识别路口+循迹+识别终点模块代码。
另外接下贴出K210串口识别指令和数据帧格式
#数据帧格式 | x-循迹偏移量 |y-检测成功 成功1,不成功0 | z-作为判断识别到的数字 或者 作为左右转向的0左,1右,2直行
def send_data_packet(x, y,z):
data='ba'+str('%03d'%x)+str(y)+str(z)+'\r\n'
return data
#串口中断 判断下一次是否发送数据
def fun(uart_flag):
global code_flag
text=uart_A.read(2) #读取数据
#print(1)
if text: #如果读取到了数据
text=text.decode('utf-8')
if(text[0]=='b'):
if(text[1]=='1'): #数字识别请求
code_flag=1
elif(text[1]=='2'): #循迹+路口识别请求
code_flag=2
elif(text[1]=='3'): #循迹+终点识别请求
code_flag=3
elif(text[1]=='4'): #数字匹配请求
code_flag=4
elif(text[1]=='0'): #停止运行,等待指令
code_flag=0
print(text)
uart_flag.irq(fun, GPIO.IRQ_BOTH)#使能外部中断,执行串口处理
至此实现了四个任务中的两个;
还剩下两个功能,由于都涉及数字识别,所以博主把他们放一起。
首先对于k210如果你想要长时间跑模型还想使用找色块等部分库函数,你就需要使用内存略微小点的固件,我使用的是这个固件,具体怎么烧录固件和接下来的识别模型教程不懂得大家可以去看别的博主的文章或者b站视频。
下载网站,和模型都放在文章结尾处,有需要的自取。
使用这个模型就可以实现保留部分找色块这种的基本图像功能,也能较长时间运行模型。识别部分反而更加简单,训练出一个好模型,基本就实现了功能,具体思路就是匹配数字,同时发送识别到的数字给单片机,单片机判断是否为近端,如果为近端,就不需要功能4了,如果为非近端,就让单片机发送对应的指令就i行了。任务4的实现就是简单将识别的数字和我们开机识别的数字比对,匹配成功则判断坐标是坐标还是右边,将结果发送给单片机处理。
def recog(task):
global labels
global anchors
global num_goal
global code_flag
img = sensor.snapshot()
objects = kpu.run_yolo2(task, img)
if objects:
if(code_flag==1):
max_value=objects[0]
for obj in objects:
pos = obj.rect()
img.draw_rectangle(pos)
img.draw_string(pos[0], pos[1], "%s : %.2f" %(labels[obj.classid()], obj.value()), scale=2, color=(255, 0, 0))
if(max_value.value()<obj.value()):
max_value=obj
num_goal=labels[max_value.classid()]
uart_A.write(send_data_packet(0,0,int(num_goal)))
print(int(int(num_goal)))
code_flag=0
if(code_flag==4):
for obj in objects:
pos = obj.rect()
img.draw_rectangle(pos)
img.draw_string(pos[0], pos[1], "%s : %.2f" %(labels[obj.classid()], obj.value()), scale=2, color=(255, 0, 0))
if(num_goal==labels[obj.classid()]):
if(pos[0]>80):
uart_A.write(send_data_packet(0,1,1))#检测到了右行
elif(pos[0]<80):
uart_A.write(send_data_packet(0,1,0))#检测到了左行
else:
uart_A.write(send_data_packet(0,0,2))#未检测到,直行
code_flag=0
lcd.display(img)
代码稀碎,感兴趣的我之后把整理好的代码放到文件末。
stm32控制任务
这里要处理的比较简单,具体需要的功能如下
实现与K210的通信
这里有两段功能
- stm32发送指令给k210,每次发送指令前都将K120设置的外部中断对应的端口电平反转,使得K210及时响应串口数据。
- 解析K210发送的数据帧,根据当前的stm32处于第几个状态去进行对应数据帧的解析
下面是部分代码,老样子后续详细代码整理好会发到github仓库和百度网盘。
#include"fun.h"
#include"OLED.h"
#include"UART.h"
extern char num_goal;//需要检测的目标数字
extern uint8_t recog_flag;//检测是否成功
extern uint8_t turn_err;//转向误差
extern uint8_t K210_state;//设置K210的状态
extern uint8_t turn;//转向
extern uint8_t turn_stack[4];//转向栈
extern uint8_t top;//栈顶指针
extern uint8_t send_flag;
extern uint8_t start_flag;//开始标志
extern uint8_t recog_count;
/*
*功能:将信息发送给K210
*参数:K210_state 选择要设置的K210的状态
*返回值:无
*/
void usart_pack(uint8_t K210_state)
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,!GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13));
Send_Byte(USART1,'b');//发送帧头数据
switch(K210_state)
{
case 0:Send_Byte(USART1,'0');break;
case 1:Send_Byte(USART1,'1');break;
case 2:Send_Byte(USART1,'2');break;
case 3:Send_Byte(USART1,'3');break;
case 4:Send_Byte(USART1,'4');break;
}
}
/*
*功能:解析接受的数据
*参数:需要解析的数据
*返回值:无
*/
void data_deal(char *buffer)
{
if(buffer[0]=='b'&&buffer[1]=='a')//接受的数据无误
{
send_flag=1;
if(K210_state==1)
num_goal=buffer[6];
else if(K210_state==2)
{
if(buffer[5]=='1') //检测成功
recog_flag=1,turn_err=-1;
else recog_flag=0,turn_err=(buffer[2]-0x30)*100+(buffer[3]-0x30)*10+(buffer[4]-0x30);
}
else if(K210_state==3)
{
if(buffer[5]=='1') //检测成功
recog_flag=1,turn_err=-1;
else recog_flag=0,turn_err=(buffer[2]-0x30)*100+(buffer[3]-0x30)*10+(buffer[4]-0x30);
}
else if(K210_state==4)
{
if(buffer[5]=='1')//识别成功数字
{
recog_flag=1;
turn=buffer[6]-0x30;
}
else
{
if(recog_count==9)
{
turn=0;
}
recog_count++;
}
}
else if(K210_state==0)
{
start_flag=1;
}
}
}
控制电机实现小车运动
小车的运动其实无非两点
- 控制小车基本运动,前进,后退,左转,右转建议大家写的时候把代码好好封装一下,做到这四个运动方式可以根据自己传入的参数去执行大概需要的运动幅度大小,另外可以作为一些固定运动作为小车偏移的一个补偿
- 控制小车循迹部分,我采用的是K210将pid转向环的速度结果直接传入stm32解析数据获得,但是当然也可以自己将K210的误差作为数据,在stm32中进行pid计算
对于这部分,我都是在pid速度环的基础上实现的,另外,这里我的转向环其实也是可以作为速度环的参数,其实也算是串级pid了吧。我也不太清楚,但是总的来说使用了速度环和转向环两种
ps:对于速度环的调参,也没什么经验,网上大佬都有,这里只是建议大家可以使用串口做到上位机显示波形的模式,这里博主使用的是vofa,很多大佬是使用匿名上位机,当然也是可以的,博主的速度环和转向环其实效果还是属于比较差的,因为调参太麻烦了,很是消磨耐心,没有队友帮忙调实在是太累了。至于这部分代码就不贴了,以来还是可读性不强,另一方面这部分代码网上应该有很多了,没必要贴出来丢人。
给大家瞅瞅我速度环波形
低速还是挺好的,抖动不大,所以博主也是让车的速度是低速走,是在没耐心调参了。相信各位的效果比我强。
其他
其他也没什么好说的,毕竟小白一枚,做出这个项目纯属侥幸。希望各位的项目能比我的更稳,更快!
资料获取
[数字识别模型及openmv固件]
链接: https://pan.baidu.com/s/1APhRB1pNWLSt-NwvMtKmZQ 提取码: faiz
代码
链接:https://pan.baidu.com/s/1f7gOiSKLh-ZcFvK_8mshAA
提取码:faiz
剩余资料等博主整理完上传github仓库和百度网盘~