回顾
本在大二下的电设延延期了,有幸与车队队友半途加入,笔者之前没做过摄像头与视觉的代码(之前做的AI电磁),虽然最后有各种各样遗憾,但也临时猛学收获许多新知识。测评前顺利做完了基础和发挥(1),可惜封箱后换了场地识别出了问题。
做的途中出现过以下问题:
- 数字移动过程中识别帧率过低
- 识别过于依赖光线亮暗
- 琐碎的失误:赛前未全面准备(摄像头AI、软件队友应合作提前搭好代码框架)、组队分工不当(视觉与控制没分工写、两车没一起调[框架没提前搭])、测评时拆箱后未检查装置(封箱后可能压到摄像头改变角度)
思路及代码
主控英飞凌TC377,摄像头Openmv循迹,Openart识别
- 循迹代码
基本思路:调红色阈值,用直线回归拟合二值化后的赛道中线line,其偏移距离rho与偏移角度theta作为err;用二值化后图像中的亮度的平均值light来判别是否为分岔元素。(之前没写过openmv,判别分岔的方案不是很好,需要摄像头角度固定且阈值在各光线下较好,比完赛后想到新的思路是设置感兴趣区在左右两侧,当居中两侧有较多亮点时可判别为分岔元素)
import sensor, image, time, math
from image import SEARCH_EX, SEARCH_DS
from pyb import UART
uart = UART(3, 115200)
'变量预定义'
global cnt_loss
global track
cnt_loss=0
track=0
THRESHOLD = (14, 65, -128, 127, 22, 127)
def err_revise():
rho_err = int(abs(line.rho())-img.width()/2)
if line.theta()>90:
theta_err = line.theta()-180
else:
theta_err = line.theta()
img.draw_line(line.line(), color = (0,0,255))
theta_err=-theta_err
err = theta_err + rho_err
return err
def track_judge():
global track
global cnt_loss
light = img.statistics().l_mean()
print(light)
'track:2.岔路口,1.普通红线,0.红线丢失'
if (light>=23):
track=2
elif light<23 and light>3:
track=1
else:
track=0
'main部分'
sensor.reset()
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA)
sensor.skip_frames(time = 2000)
clock = time.clock()
while(True):
clock.tick()
img = sensor.snapshot().binary([THRESHOLD])
line = img.get_regression([(100,100)], robust = True)
track_judge()
if (line):
err=err_revise()
if track==2:
uart.write( str(err)+'T' )
print('T cro')
elif track==1:
uart.write( str(err)+'F' )
print('F cro')
else:
uart.write( str(err)+'L' )
print('loss')
print(str(err)+'\n')
time.sleep_ms(30)
- 数字识别
基本思路:参考的是逐飞官方的开源例程,在图像中找黑色边框,将框中图像输入到模型识别。第一次识别的数字通过串口发送给MCU,之后再识别到数字则根据黑框中心点位置位于左右侧来发送左右转信息给MCU。
模型训练:基本按着逐飞官方的教程来,当时Openart到已经是第二天下午,时间很紧,好在之前做过电磁的AI,官方训练的环境用anconda配置过,流程也走过。但一下午结果训练出来效果识别准确率很差,后来用大佬的训练集自己再图像增强(就增加一些其他亮度的图片)后,准确率提升了不少,但当时感觉还是特别受环境亮度影响(后来仔细想其实主要是帧率太低,应该让车在路口停下来识别,静态识别的准确率最后测评时其实有90%+)。时间来到第二天晚,识别很不稳定,而且还没和控制结合,赶工搭了控制框架能实现1和2的入病房后,2点多队友建议回去睡觉,现在想想不知是对是错了。(车赛时的模型是自己tensorflow训练后移植,说实话电赛当时也想过自己训练个部署,但没做过openart上移植模型,自己移植模型怕时间太赶,就照着官方做,其实用openmv的tf库即可,tf.load可以加载tensorflowlite模型,只需训练模型后保存为tflite模型即可)
'''数字识别Openart代码'''
import pyb
import sensor, image, time, math
import os, nncu
from machine import UART
uart = UART(1, baudrate=115200)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA) # we run out of memory if the resolution is much bigger...
sensor.set_brightness(300) # 设置图像亮度 越大越亮
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False) # must turn this off to prevent image washout...
sensor.set_auto_whitebal(False,(0,0x80,0)) # must turn this off to prevent image washout...
clock = time.clock()
net_path = "_1s_model_17_1.0000_xxxx.nncu" # 定义模型的路径
labels = [line.rstrip() for line in open("/sd/labels_number.txt")] # 加载标签
net = nncu.load(net_path, load_to_fb=True) # 加载模型
first_num=0
is_send_num=0
while(True):
img = sensor.snapshot()
print("********** Start Detections **********")
for r in img.find_rects(threshold = 30000): # 在图像中搜索矩形
img.draw_rectangle(r.rect(), color = (255, 0, 0)) # 绘制矩形外框,便于在IDE上查看识别到的矩形位置
img1 = img.copy(r.rect()) # 拷贝矩形框内的图像
print(r.rect()[3])
if r.rect()[1]>10 and r.rect()[1]<140 and r.rect()[3]>65 and r.rect()[3]<90:
for obj in nncu.classify(net , img1, min_scale=1.0, scale_mul=0.5, x_overlap=0.0, y_overlap=0.0):
sorted_list = sorted(zip(labels, obj.output()), key = lambda x: x[1], reverse = True)
# 打印准确率最高的结果
for i in range(1):
if first_num==0:
first_num=sorted_list[i][0]
###发送数字,为避免与Openmv发送err混淆,Openart发送数字对应的字母#######
if str(first_num)=='1' and is_send_num==0:
uart.write('a')
print('1')
is_send_num=1
elif str(first_num)=='2' and is_send_num==0:
uart.write('b')
print('2')
is_send_num=1
elif str(first_num)=='3' and is_send_num==0:
uart.write('c')
print('3')
is_send_num=1
elif str(first_num)=='4' and is_send_num==0:
uart.write('d')
print('4')
is_send_num=1
elif str(first_num)=='5' and is_send_num==0:
uart.write('e')
print('5')
is_send_num=1
elif str(first_num)=='6' and is_send_num==0:
uart.write('f')
print('6')
is_send_num=1
elif str(first_num)=='7' and is_send_num==0:
uart.write('g')
print('7')
is_send_num=1
elif str(first_num)=='8' and is_send_num==0:
uart.write('h')
print('8')
sensor.set_brightness(400)
is_send_num=1
##################
print('sco'+str(first_num))
if sorted_list[i][0]==first_num:
'识别到需求数字,判断位于左/右侧'
if (r.rect()[0]<120):
uart.write('l')
print(str(first_num)+' on l')
else:
uart.write('r')
print(str(first_num)+' on r')
img.draw_string(r.rect()[0] + 20, r.rect()[1]-20, sorted_list[0][0],color = (0,0,255), scale = 2,mono_space=False)
- 控制代码
基本思路:
1.药品状态标志(变量名flag.drug):
标志 | 状态 |
---|---|
0 | 初始未放药 |
1 | 放上药品后 |
2 | 病房处停下(丢红线)等待取药 |
3 | 取下药品掉头的过程 |
4 | 掉头后循线到达最后一个路口前 |
5 | 转完最后一个路口的返回药房 |
- 对应标志的控制
标志 | 控制 |
---|---|
0 | 停车 |
1 | 发车并PID循线,遇到路口时获取Openart的信息来控制左右转 |
2 | 停车 |
3 | 原地旋转180度 |
4 | PID循线,遇路口时按来时的方向反向转弯 |
5 | 加速并按PID循线直到丢线停车 |
-
发挥1部分
车2理论上不需要识别,只需要记录车1的转弯来对应串口发送指令控制车2路线即可。
核心代码:
1.通信部分以及控制指令:
#include "headfile.h"
struct OPENMV mv,art;
char * p=mv.str;
int i=0;
/*
函数:openmv_get_str()
变量说明:
UART1-与openmv通信
UART0-与openart通信
UART2-与车2通信
@mv.data:每次串口中断接收到的字符
@mv.str:将一次完整的数据储存到字符串,T,F,L为一次发送结尾标志,并分别对应T-路口,F-不是路口,L-丢线
@mv.err:将字符串偏差转化为浮点型的偏差
@mv.track:对应T,L,F,2-是路口,0-丢线,1-普通赛道
*/
void openmv_get_str()
{
if (uart_query(UART_1,&mv.data))
{
if (mv.data!='T' && mv.data!='F' && mv.data!='L' )
{
mv.str[i]=mv.data;
i++;
}
else if (mv.data=='T')
{
mv.str[i]='\0';
i=0;
mv.err = atof(mv.str);
memset(mv.str,0,sizeof(mv.str));
mv.track=2;
}
else if (mv.data=='F')
{
mv.str[i]='\0';
i=0;
mv.err = atof(mv.str);
memset(mv.str,0,sizeof(mv.str));
mv.track=1;
}
else if (mv.data=='L')
{
mv.str[i]='\0';
i=0;
mv.err = atof(mv.str);
memset(mv.str,0,sizeof(mv.str));
if (!art.con_left && !art.con_right) mv.track=0;
}
}
}
/*
控制指令函数——方向控制
函数:direc_control()
变量说明:
@art.con_left/right
状态1:(0秒~cnt.turn_sec秒)期间按固定占空比转弯
状态2:(cnt.turn_sec秒~cnt.turn_sec*2秒)期间按PID自然回线且不判断丢线(避免判断为到达病房)
@cnt.left/right:左转计时变量,1次中断5ms。
@cnt.turn_time:转弯固定差速持续的时间以及转弯后一段不判断丢线的时间。
@cnt.turn_num:转弯的次数(即遇到的路口数)
@flag.drug:药品的状态
@flag.str:是否发车
@flag.is_judge:发挥1的相关变量,即告诉车2已识别中端病房路口
@flag.send_direc:发挥1的相关变量,即告诉车2要转弯的方向
@flag.turn_memory[]:记下来时的方向,回去遇到路口时则从数组末端开始来控制方向
*/
void direc_control()
{
if (art.con_left)
{
cnt.left++;
if (cnt.left>cnt.turn_time*1000/5)
{
art.con_left=2;
if (cnt.left>cnt.turn_time*2000/5)
{
art.con_left=0;
cnt.left=0;
if (flag.drug<4&&flag.str)
{
if (!flag.is_judge)
{
flag.is_judge=1;
flag.send_direc='Z';
}
flag.turn_memory[cnt.turn_num]=1;
cnt.turn_num++;
}
else if(flag.drug>3)
{
cnt.turn_num--;
}
}
}
}
else if (art.con_right)
{
cnt.right++;
if (cnt.right>cnt.turn_second*1000/5)
{
art.con_right=2;
if (cnt.right>cnt.turn_second*2000/5)
{
/**该if中代码某次控制中只执行一次(最后一次5ms中断进入)**/
art.con_right=0;
cnt.right=0;
if (flag.drug<4&&flag.str)
{
/*发挥1部分*/
if (!flag.is_judge)
{
flag.is_judge=1;
flag.send_direc='X';
}
/**********/
flag.turn_memory[cnt.turn_num]=2;
cnt.turn_num++;
}
else if(flag.drug>3)
{
cnt.turn_num--;
}
}
}
}
}
/*
控制指令函数——赛道判别
函数:track_control()
变量说明:
@art.con_left/right:上面有介绍
@art.num_judge:识别到的数字
@flag.turn_memory[]:记下来时的方向,回去遇到路口时则从数组末端开始来控制方向
思路:分为三大种情况,1.如果识别的是1和2,则特殊处理。
*/
void track_control()
{
if (mv.track==2 &&flag.drug!=5)
{
/*识别到1或者2*/
if (art.num_judge==1 || art.num_judge==2)
{
if (art.num_judge==1 && flag.drug!=4 && flag.drug!=5)
art.con_left=1;
else if (art.num_judge==1 && flag.drug==4)
{
art.con_right=1;
flag.drug=5;
}
else if (art.num_judge==2 && flag.drug!=4 && flag.drug!=5)
art.con_right=1;
else if (art.num_judge==2 && flag.drug==4)
{
art.con_left=1;
flag.drug=5;
}
}
/****************************/
/**识别到其他数字*****/
else
{
/**如果是第一个路口(无两路口则直接下一个判断)**/
else if(flag.drug==4 && cnt.turn_num == 2)
{
if(flag.turn_memory[1]==1)
art.con_right=1;
else if (flag.turn_memory[1]==2)
art.con_left=1;
}
/**如果是最后一个路口**/
else if(flag.drug==4 && cnt.turn_num == 1 )
{
if(flag.turn_memory[0]==1)
art.con_right=1;
else if (flag.turn_memory[0]==2)
art.con_left=1;
flag.drug=5;
}
}
}
}
/*
函数:openart_get_str()
变量说明:
@art.data:接收的单个字符(由于三个串口底层都用较简单的阻塞式读取,所以除了openmv读取偏差外,其他两个串口只读取简单的单字符且仅对应某串口会发送此类字符,尽可能避免干扰openmv的通信)
@art.num_judge:识别的数字
*/
void openart_get_str()
{
if (uart_query(UART_0,&art.data))
{
if (art.data=='l')
{
art.con_left=1;
}
else if (art.data=='r')
{
art.con_right=1;
}
else if (art.data=='a')
{
art.num_judge=1;
}
else if (art.data=='b')
{
art.num_judge=2;
}
else if (art.data=='c')
{
art.num_judge=3;
}
else if (art.data=='d')
{
art.num_judge=4;
}
else if (art.data=='e')
{
art.num_judge=5;
}
else if (art.data=='f')
{
art.num_judge=6;
}
else if (art.data=='g')
{
art.num_judge=7;
}
else if (art.data=='h')
{
art.num_judge=8;
}
}
}
2.电机控制
/*
函数:motor_control()
变量说明:
@enc.speed_contorl_out:速度环输出
@enc.dif:方向环输出差速
@enc.left_pwm/right:左右速度
@cnt.round_second:原地旋转持续时间
*/
void motor_control()
{
if (flag.drug==5)
{
enc.left_pwm=enc.speed_control_out+2000-enc.dif;//steer.angle即电机差速
enc.right_pwm=enc.speed_control_out+2000+enc.dif;
}
else
{
enc.left_pwm=enc.speed_control_out+0-enc.dif;//steer.angle即电机差速
enc.right_pwm=enc.speed_control_out+0+enc.dif;
}
if (!flag.man_swi)
{
if (flag.drug==3)
{
cnt.turn_round++;
if (cnt.turn_round>cnt.round_second*1000/5)
{
cnt.turn_round=0;
flag.drug=4;
}
motor(3000);
motor2(3000);
}
else if(art.con_left==1)
{
motor(1000);
motor3(4000);
}
else if (art.con_right==1)
{
motor(4000);
motor3(1000);
}
else if (mv.track==0 && (flag.drug==5 || flag.drug==2) )
{
motor(0);
motor3(0);
motor2(0);
motor1(0);
}
else
{
if (enc.left_pwm>0)
{
motor(enc.left_pwm);
motor1(0);
}
else
{
motor(0);
motor1(-enc.left_pwm);
}
if (enc.right_pwm>0)
{
motor2(0);
motor3(enc.right_pwm*1.2);
}
else
{
motor2(-enc.right_pwm*1.2);
motor3(0);
}
}
}
}
3.中断
IFX_INTERRUPT(cc61_pit_ch0_isr, 0, CCU6_1_CH0_ISR_PRIORITY)
{
enableInterrupts();//开启中断嵌套
PIT_CLEAR_FLAG(CCU6_1, PIT_CH0);
//自动循迹
if (!flag.man_swi)
{
/***************PID*************/
/*向车2发送信号*/
Send_to_car2();
/*外设处理*/
reeds_get();//检测是否装载着药品
if_put_drug();//是否放上药物
/*亮红灯——病房丢线*/
if (flag.drug==1 && mv.track==0)
{
flag.drug=2;
flag.led=1;
}
if_get_drug();//是否取下药物
/*亮绿灯——药房丢线*/
if (flag.drug==5 && mv.track==0)
flag.led=2;
led_control();//提示灯控制
/*摄像头处理*/
openmv_get_str();
if (!art.left_control && !art.right_control)
{
if (flag.drug<3) openart_get_str();
track_control()
}
direc_control();
PID_steer_trace();
/**准备、发车**/
if (flag.str)
{
if (flag.mot_con) motor_control();
// if (flag.ste_con) steer_control();四轮车不好原地转弯,搭车还浪费好久时间
}
/**计数**/
Count();
}
/************人工测试***********/
else
{
openmv_get_str();
if (flag.str)
{
if (flag.mot_con) motor_control();
// if (flag.ste_con) steer_control();
}
/**计数**/
Count();
}
}
4.车2代码
核心思路:笔者是让车2在车1到达中端病房后前往与车1同侧的近端病房,记录车1在路口时转的方向,车2只需按这个方向在遇到路口时先转1次即到达车1的同侧近端病房,然后等待车1返回到达药房发送启动指令,车2原地旋转,再在遇到路口时转2次(共俩路口)即可到达车1抵达的病房。
/**获取车1指令**/
void car2_get_str()
{
if (uart_query(UART_2,&getcar1.data))
{
/*得知车1的方向是左转*/
if (getcar1.data=='Z' )
{
flag.turn_direc=1;
flag.str=1;
}
/*得知车1的方向是右转*/
else if (getcar1.data=='X')
{
flag.turn_direc=2;
flag.str=1;
}
/*接收到原地旋转指令*/
else if (getcar1.data=='S')
{
flag.get_turn=1;
}
}
}
/**方向控制**/
void track_control()
{
if (mv.track==2 &&flag.drug!=5)
{
if (flag.turn_direc==1)
{
art.con_left=1;
}
else if (flag.turn_direc==2)
{
art.con_right=1;
}
}
}
void direc_control()
{
//同车1
}
void motor_control()
{
//同车1
}
/***中断部分***/
IFX_INTERRUPT(cc61_pit_ch0_isr, 0, CCU6_1_CH0_ISR_PRIORITY)
{
enableInterrupts();//开启中断嵌套
PIT_CLEAR_FLAG(CCU6_1, PIT_CH0);
//自动循迹--AI/PID
if (!flag.man_swi)
{
/***************PID*************/
/*接收主车通信*/
car2_get_str();
/*外设处理*/
led_control();
flag.drug=0;
/*摄像头处理*/
openmv_get_str();
if (cnt.left==0 && cnt.right==0)
{
track_control();
}
direc_control();
PID_steer_trace();
/**准备、发车**/
if (flag.str)
{
if (flag.mot_con) motor_control();
}
/**计数**/
Count();
}
}
可优化的点
- 数字移动过程中识别帧率过低导致识别不准:
很有效的解决方法,只需在控制中加入判断到十字停车2s进入识别状态,识别后退出识别状态并控制转弯。
可惜:但当时时间太赶,身体素质不好,熬夜人弄麻了,觉得这样整体时间会超时,其实只要控制电机多提速就好了,而且测评时专家没强调时间。这真的是我觉得最遗憾的,当时人麻了,没好好思考,也是技术太菜,一言难尽。 - 识别过于依赖光线亮暗
事实上我对这点还是有所疑问,因为上面那个问题才是真正主要因素,静态时对数字的识别其实准确率有90%以上:(那天识别手持数字几乎没出错过)。当然换了场地后的影响也挺大(早晨8点的亮度也跟下午不太一样),确实该写个滤光算法使摄像头适应各种场景内的均匀光,太阳的直射光问了调摄像头的同学其实也不太好解决,只能拉窗帘了。
可惜:在这点耽误了太多时间,控制与通信大概就写了4小时左右,识别一直效果不好,却没想到是思路大方向错了,与其不断改进训练集提高亮暗识别度以及降低模型复杂度提高帧率,不如控制上做出大的改变,在十字处停下识别才是最核心的,后来看有openmv用模板匹配的同学这样识别率也不低。
尾声
- 当时时间紧没怎么拍照留念,之后拆箱了再优化优化,放上模型文件以及训练过程、完整控制工程、做点效果图、视频等放博客上回念了。这可能是大学最后一次参加比赛了,就结果来看还是蛮遗憾的。
个人是只负责视觉和控制的软件部分,硬件上有机会问问队友再放了。 之后也打算整理好每次做项目的代码放到网盘、博客,多多复盘,多多整理,不断成长吧,未来可期!
交大大佬的识别(相见恨晚):
https://zhuanlan.zhihu.com/p/391216590
openmv中文手册
https://docs.singtown.com/micropython/zh/latest/openmvcam/index.html
openart开源教程:
http://mp.weixin.qq.com/s?__biz=MzAxMjQxNjEyMw==&mid=2247487040&idx=1&sn=4d47976b398b3c92811a29356ada34b9&chksm=9bb36b54acc4e242edf935986c97ee1f6363dffc86678faf64c65903476f8c27504e1a392120&mpshare=1&scene=23&srcid=1203gAB1ALoBvqjYxJd7qp5c&sharer_sharetime=1638461358289&sharer_shareid=b8ab17ea4161b8f0d5c69ddb66ea7563#rd
规则