2023年电赛E题完整设计暨电赛全记录
目录
一.2023年E题完整设计
选择方案
任务一:实现按键按下复位(基础部分)
任务二:实现激光点绕边框一周(基础部分)
任务三:实现激光点绕A4纸边缘一周(基础部分)
任务四:实现绿色激光追踪红色激光(发挥部分)
任务分配
代码分析
1.stm32上关键源码分析
I.基本部分
II.模块代码
(1)Timer——定时器延时函数模块
(2)servo_motor——云台舵机控制模块
①控制舵机的旋转
②控制激光点到达某一像素点
③与上位机jetson nano通讯接收点坐标
④得到一点坐标对应的舵机pwm波OC值
⑤控制激光点沿四边形巡线
III.主函数与中断函数部分
(1)红色激光云台
(2)绿色激光云台
2.jetson nano上关键源码分析
I.相机参数的调整
II.关键识别算法
(1)铅笔线识别及顶点的计算
(2)A4纸顶点识别及巡线顶点的计算
(3)区分红绿激光
二.学习资料分享
学习笔记
其他资料
三.备赛阶段记录
四.电赛总结及经验教训
本次比赛作品的不足、改进之处
本次比赛的经验教训
“愿大家都少走弯路,在迷茫时看到希望!”
一.2023年E题完整设计
选择方案
任务一:实现按键按下复位(基础部分)
方法①:识别四顶点位置->连接对角线得到中心点->PID调节使激光点与中心点重合
方法②:识别四顶点位置->对角顶点坐标求平均值得中心点位置->PID调节使重合
方法③:固定所有器件位置,保证各点PWM值不变,得到中心点PWM固定值,开环设定
任务二:实现激光点绕边框一周(基础部分)
步骤I:激光点由中心点到达边线左上角
步骤II:顺时针绕一圈
方法①:两点定线,先确定两点坐标,连线确定等分点,使用PID算法在等分点间移动
方法②:不使用PID,利用与目标点坐标差计算移动方向,每次移动距离为舵机最小精度值
方法③:求PWM和坐标(x,y)的函数关系(近似线性),直接设定PWM值到达指定点
任务三:实现激光点绕A4纸边缘一周(基础部分)
(与任务二区别:矩形放置角度可以倾斜;要区分两矩形宽度以识别A4纸)
任务四:实现绿色激光追踪红色激光(发挥部分)
方法①:区分红绿色激光并得到坐标->PID直接跟踪
任务分配
将上述任务分解成多个要完成的技术,以便分工:
1.硬件平台搭建
2.stm32控制算法:
①PID控制激光点移动到目标点算法(核心)
②舵机以最小分度值移动算法(细微调节)
③给定两点以及等分数计算所有等分点算法(线上移动减少偏差)
④在PID寻点时获取基本点(矩形顶点及中心)PWM值算法
⑤stm32和jetson nano的通信规则设计与数据互传
3.OpenCV识别算法
①识别铅笔线边框:灰度图转换->阈值分割成二值图->霍夫直线变换得到直线上两点(非端点)->从得到的多条直线中筛选去重->编写“已知两直线上两点求直线交点”算法->求得四端点
②识别A4纸边框:阈值分割后利用Harris角点检测出A4框的8个顶点->编写“从8个顶点中识别两两相邻顶点”算法->求得框中心线4顶点
③区分红绿激光点算法:转换到Hsv色彩空间->分别设置阈值,在Hsv空间中二值化图像提取红绿色区域以得到激光点坐标
4.主函数(程序流程)设计
5.电赛报告书写
代码分析
1.stm32上关键源码分析
I.基本部分
(1)引脚使用说明
//*************************引脚使用说明*************************
/*
oled.h GPIOA PIN0/1
bluetooth.h GPIOA PIN2/3
joystick.h GPIOA PIN4/5 ADC1_CH4/5 GPIOB PIN11/12/13 EXTI12/13
Pwm.h GPIOA PIN8/11 TIM1_CH1/4 50hz
usart.h GPIOA PIN9/10 TX/RX Black/White
beep.h GPIOB PIN14
led.h GPIOB PIN15
Timer.h TIM2/3
*/
(2)头文件声明
//************************头文件声明************************
#include "public.h" //公用引用函数封装
//#include "bluetooth.h" //蓝牙模块
#include "oled.h" //OLED显示屏模块
#include "Pwm.h" //PWM波生成模块
#include "servo_motor.h" //云台控制函数模块
#include "joystick.h" //摇杆控制模块
#include "string.h"
#include "Delay.h"
#include "Timer.h" //定时器模块
#include "usart.h" //uart通信模块
#include "beep.h" //蜂鸣器模块
#include "led.h" //led灯模块
#include "dma.h" //dma数据转存模块
(3)全局变量和宏定义声明
//************************全局变量和宏定义声明************************
//#define OpenLoop_OL //开环实现功能执行
#define CloseLoop_CL //闭环实现功能执行extern float Voltage[2]; //ad测量电压值[0.3.3] //ad.c
extern char USART_RX_INFO[USART_REC_LEN]; //uart接收数据 //usart.c
extern int x,y; //激光当前坐标 //servo_motor.c
extern int Vertex[4][2]; //四顶点位置 //servo_motor.c
extern int Vertex_Peak_Pos[4][2];
extern int Vertex_A4[4][2];
extern Pwm Center_Pwm;
extern Pwm Peak_Pwm[4];
extern Pwm A4_Pwm[4];int Programme_Progress=0; //比赛程序进度
int order=0; //蓝牙接收到的命令
int Main_Wait_Stop_Sign =1; //主程序等待标志位
extern int JoyStick_Control_Stop_Sign; //摇杆控制程序结束标志位
int Get_Depend_Point_Pos_Stop_Sign=1;
int Get_A4_Point_Pos_Stop_Sign=1;
extern int Follow_Track_Stop_Sign; //矩形寻迹结束标志位
extern int Follow_Point_Stop_Sign; //绿激光跟随红激光结束标志位
II.模块代码
(1)Timer——定时器延时函数模块
#include "Timer.h"//TIM2/3void Timer_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);TIM_InternalClockConfig(TIM2);TIM_InternalClockConfig(TIM3);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInitStructure.TIM_Period = 60000 - 1; //分辨率1us,最大60msTIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);}void Timer_delay_us(int xus)
{TIM_Cmd(TIM2,ENABLE); //启动定时器while(TIM2->CNT < xus);TIM2->CNT = 0;TIM_Cmd(TIM2,DISABLE); //关闭定时器
}void Timer_delay_ms(int xms)
{int i=0;for(i=0;iCNT < xus);TIM3->CNT = 0;TIM_Cmd(TIM3,DISABLE); //关闭定时器
}void EXIT_LINE_Timer_delay_ms(int xms)
{int i=0;for(i=0;i
说明:
在Timer_Init()中开启了两个定时器TIM1/2,由Timer_delay_us()和EXIT_LINE_Timer_delay_us()分别使用,分别在中断函数内外使用,避免重复调用冲突
(2)servo_motor——云台舵机控制模块
①控制舵机的旋转
int Oc_Lp[4]={750,750,750,750};
int Oc_Vp[4]={763,763,763,763};
/*********************************************************
函数功能:云台水平方向旋转
*********************************************************/
void Spinnig_Level(int diff)
{if(diff<0){Oc_Lp[0]=Oc_L=(Oc_L+diff)<660?660:(Oc_L+diff);}else if(diff>0){Oc_Lp[0]=Oc_L=(Oc_L+diff)>840?840:(Oc_L+diff); }TIM_SetCompare1(TIM1,Oc_L); int i;for(i=3;i>0;i--)Oc_Lp[i]=Oc_Lp[i-1];
}
说明:
这里的Oc_Lp存储的是控制舵机的pwm波参数中的OC寄存器中的值,作为舵机运动最基本的函数,舵机的控制通过改变pwm波参数中的OC寄存器中的值实现。这里定义数组实现记忆功能,可存储前三次的OC值。并通过三元运算符设定上下限,将最终的OC值通过TIM_SetCompare1()设定。
②控制激光点到达某一像素点
/*********************************************************
函数功能:云台控制激光点到达某一点
函数参数:目标点的坐标
*********************************************************/
int x=360,y=360; //跟随点当前坐标
int Reach_Pos_CL_Stop_Sign=1;
//云台水平方向旋转PID值
float Level_Kp=0.06;
float Level_Ki=0.02;
float Level_Kd=0.01;
//云台竖直方向旋转PID值
float Vert_Kp=0.06;
float Vert_Ki=0.02;
float Vert_Kd=0.01;
void Reach_Pos_CL(int Target_X,int Target_Y,int Reach_Pos_CL_MODE)
{int Sign(int num);void Get_Point_Pos(void);int near(int Target_X,int Target_Y);int diff_x,diff_y;while(Reach_Pos_CL_Stop_Sign){Timer_delay_ms(30);Get_Point_Pos();if(near(Target_X,Target_Y)<=6){Beep_Times(10,1,NORMAL_MODE);break;}if(Reach_Pos_CL_MODE==PID_MODE && near(Target_X,Target_Y)>60) //用pid计算舵机单位数{diff_x=Pid_Control(Level_Kp,Level_Ki,Level_Kd,Target_X,x,PID_REALIZE);diff_y=Pid_Control(Vert_Kp,Vert_Ki,Vert_Kd,Target_Y,y,PID_REALIZE);}else if(Reach_Pos_CL_MODE==MINMIZE_MODE) //以舵机最小分辨率为单位{diff_x=-Sign(x-Target_X);diff_y=-Sign(y-Target_Y);}else if(Reach_Pos_CL_MODE==PID_MODE && near(Target_X,Target_Y)<=60) //用pid计算舵机单位数{diff_x=-Sign(x-Target_X);diff_y=-Sign(y-Target_Y);Timer_delay_ms(30);}Spinnig_Level(X_DIR*diff_x);Spinnig_Vert(Y_DIR*diff_y);Timer_delay_ms(20);}
}int Sign(int num)
{if(num>5)return 1;else if(num<-5)return -1;else return 0;
}int my_abs(int a,int b)
{return a-b>0?a-b:b-a;
}int near(int Target_X,int Target_Y)
{return my_abs(Target_X,x)+my_abs(Target_Y,y);
}
说明:
输入参数:目标点像素坐标;追踪模式(PID【PID与最小精度混合】模式和最小精度值模式)
追踪过程:
——得到当前激光点坐标:Get_Point_Pos()
——如果接近目标点则蜂鸣器鸣叫并退出【near(Target_X,Target_Y)<=6,说明当前坐标与目标横纵坐标差之和{“距离”}小于6个像素】
——如果使用PID模式:
——“距离”大于60时采用PID算法快速靠近,计算出OC变化值diff_x、diff_y
——“距离”小于60时使用最小精度模式缓慢靠近,利用“符号函数sign()”计算diff
——调用Spinnig_Level()、Spinnig_Level()进行水平和垂直舵机的旋转
③与上位机jetson nano通讯接收点坐标
a.激光点坐标的实时接收
/*********************************************************
函数功能:stm32获取当前激光坐标
*********************************************************/
void Get_Point_Pos(void)
{if(USART_RX_INFO[0]=='x') //检查数据定位是否正确(上位机发送信息为:x123y456){x=(USART_RX_INFO[1]-'0')*100+(USART_RX_INFO[2]-'0')*10+USART_RX_INFO[3]-'0';}if(USART_RX_INFO[4]=='y') //检查数据定位是否正确(上位机发送信息为:x123y456){y=(USART_RX_INFO[5]-'0')*100+(USART_RX_INFO[6]-'0')*10+USART_RX_INFO[7]-'0';}
}
说明:
规定上位机每次发送数据格式为:以#开头,以$结尾;stm32usart模块对接收数据进行解析
上位机坐标数据格式为:x123y456;123、456代表三位坐标值,字符'x'、'y'起定位作用
stm32对接收到的字符坐标进行解析如上
b.特殊坐标接收
//********************************************************高级控制函数(CloseLoop--CL)********************************************************
int Vertex_Peak_Pos[4][2];
int Center_Pos[2];
Pwm Center_Pwm;
Pwm Peak_Pwm[4];
Pwm A4_Pwm[4];
//获取重要点坐标
void Get_Point_5(void)
{int i,j;while(1){for(i=0;i<8;i++){if(USART_RX_INFO[4*i]=='a'+i)continue;else break;}if(i==8){for(i=0;i<4;i++){for(j=0;j<2;j++)Vertex_Peak_Pos[i][j]=(USART_RX_INFO[4*(2*i+j)+1]-'0')*100+(USART_RX_INFO[4*(2*i+j)+2]-'0')*10+(USART_RX_INFO[4*(2*i+j)+3]-'0');}break;}}while(!(USART_RX_INFO[0]=='i'&&USART_RX_INFO[4]=='j'));Center_Pos[0]=(USART_RX_INFO[1]-'0')*100+(USART_RX_INFO[2]-'0')*10+USART_RX_INFO[3]-'0';Center_Pos[1]=(USART_RX_INFO[5]-'0')*100+(USART_RX_INFO[6]-'0')*10+USART_RX_INFO[7]-'0';Beep_Times(50,5,NORMAL_MODE);}
说明:
这里接收的是铅笔线框四个顶点的坐标和中心点坐标,但是一次发送的数据长度不能太长,这里拆分成两部分接收(数据格式为:axxxbxxxcxxx...hxxx共8组值四个坐标),关键在于两部分的衔接
while(!(USART_RX_INFO[0]=='i'&&USART_RX_INFO[4]=='j'));确保收到四个顶点坐标后持续等待中心点坐标的发送
④得到一点坐标对应的舵机pwm波OC值
int sum_num(int *num,int n)
{int i,sum;for(i=sum=0;ilevel=sum_num(Oc_Lp,n)/n;target_pwm->vert=sum_num(Oc_Vp,n)/n;
}
说明:
通过②控制函数控制激光点到达指定点后记录目标点pwm值并返回;Pwm结构体定义如下
typedef struct Pwm{int level;int vert;
}Pwm;
可以通过改变参数n的值选择是否滤波,4>n>1时进行滤波,取前几次OC值的平均值,不建议滤波
⑤控制激光点沿四边形巡线
//巡线
void Follow_Track(int Vertex[4][2],int divide_num)
{int i,j;float sub_l,sub_v;Pwm Vertex_Pwm[4];for(i=0;i<4;i++)Get_Pwm(Vertex[i][0],Vertex[i][1],&Vertex_Pwm[i],1);for(i=0;i<4;i++){sub_l=(Vertex_Pwm[(i+1)%4].level-Vertex_Pwm[i].level); //下一个顶点与当前顶点pwm之差sub_v=(Vertex_Pwm[(i+1)%4].vert-Vertex_Pwm[i].vert); //下一个顶点与当前顶点纵坐标之差for(j=0;j
说明:
输入参数:四边形顺时针顺序顶点坐标、每段等分数divide_num
巡线过程:
——得到四个顶点坐标对应的水平、数值舵机OC值
——在for循环内依次经过四个顶点,视作四个大任务
——内部使用for循环分解小任务,根据等分段数divide_num计算等分点横纵pwm值并移动至
——任务结束鸣叫示意
III.主函数与中断函数部分
(1)红色激光云台
//*************************主函数部分*************************
//重新重启初值还原设置
void Programme_Reset(void)
{Beep_Times(1000,1,NORMAL_MODE);Led_Times(1000,1,NORMAL_MODE);Programme_Progress=0;Main_Wait_Stop_Sign=1;JoyStick_Control_Stop_Sign=1;Follow_Track_Stop_Sign=1;Get_A4_Point_Pos_Stop_Sign=1;Get_Depend_Point_Pos_Stop_Sign=1;
}int main(void)
{ //********************初始化程序********************Timer_Init(); //定时器初始化
// BlueToothInit(9600,USART_Parity_No,USART_StopBits_1,USART_WordLength_8b); //蓝牙初始化OLED_Init(); //oled初始化Beep_Init(); //蜂鸣器初始化Led_Init(); //led灯初始化TIM1_PWM_Init(9999,143); //一周期20ms,分辨率20ms/10000)TIM_SetCompare1(TIM1,750); //对齐角度为90度(1.5ms)TIM_SetCompare4(TIM1,763); //对齐角度为90度(1.5ms)uart_init(115200); //uart1初始化JoyStick_Init(); //JoyStick摇杆初始化//*************************比赛程序部分*************************while(1){int i;//重新重启初值还原设置Programme_Reset();
// Reach_Pos_CL(50,50,PID_MODE);Axes_Init();// Follow_Track(Vertex_Peak_Pos,1);while(Main_Wait_Stop_Sign);//摇杆控制JoyStick_Control();//#ifdef OpenLoop_OL
// Follow_Track_OL();
//#endif
//#ifdef CloseLoop_CL
// //等待上位机发送初始坐标
// Get_Depend_Point_Pos();
// //环绕正方形顺时针旋转一周
// while(Get_Depend_Point_Pos_Stop_Sign);//Follow_Track_CL(Vertex_Peak_Pos,2,PID_MODE);//#endifPwm_Track(Peak_Pwm,1);while(Follow_Track_Stop_Sign);Get_A4_Point_Pos();Timer_delay_ms(2000);
// Follow_Track_CL(Vertex_A4,4,MINMIZE_MODE);
// Follow_Track(Vertex_A4,4);for(i=0;i<4;i++)Get_Pwm(Vertex_A4[i][0],Vertex_A4[i][1],&A4_Pwm[i],1);Pwm_Track(A4_Pwm,6);while(Get_A4_Point_Pos_Stop_Sign);}
}
//*********************************************中断函数部分*********************************************
//按键中断函数
void EXTI15_10_IRQHandler()
{if (EXTI_GetITStatus(EXTI_Line11) == SET){EXIT_LINE_Timer_delay_ms(10); if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0) //软件防抖{Beep_Times(50,2,EXIT_LINE_MODE);Reach_Pos_OL(Oc_L,Oc_V); //保持激光当前指向位置while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0); //等待按键松开//再次按下才退出EXIT_LINE_Timer_delay_ms(10);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==1); EXIT_LINE_Timer_delay_ms(10); if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0); //软件防抖while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11==1)); //等待按键松开Beep_Times(50,2,EXIT_LINE_MODE);EXTI_ClearITPendingBit(EXTI_Line11);}}else if (EXTI_GetITStatus(EXTI_Line12) == SET){EXIT_LINE_Timer_delay_ms(10); if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_12)==0) //软件防抖{Programme_Progress++;Beep_Times(500,1,EXIT_LINE_MODE);if(Programme_Progress==1){Main_Wait_Stop_Sign=0;}else if(Programme_Progress==2){JoyStick_Control_Stop_Sign=0;}else if(Programme_Progress==3){
// Get_Depend_Point_Pos_Stop_Sign=0;Follow_Track_Stop_Sign=0;}else if(Programme_Progress==4){Get_A4_Point_Pos_Stop_Sign=0;
// Follow_Track_Stop_Sign=0;}else if(Programme_Progress==5){
// Get_A4_Point_Pos_Stop_Sign=0;}else if(Programme_Progress==6){;}else if(Programme_Progress==7){;}else{Programme_Reset();}while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_12)==0); //等待按键松开EXTI_ClearITPendingBit(EXTI_Line12);}}else if (EXTI_GetITStatus(EXTI_Line13) == SET){EXIT_LINE_Timer_delay_ms(10); if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0) //软件防抖{Beep_Times(50,3,EXIT_LINE_MODE);Reach_Pos_OL(Center_Pwm.level,Center_Pwm.vert);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0); //等待按键松开//再次按下才退出EXIT_LINE_Timer_delay_ms(10);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==1); EXIT_LINE_Timer_delay_ms(10); if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0); //软件防抖while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13==1)); //等待按键松开Beep_Times(50,3,EXIT_LINE_MODE);EXTI_ClearITPendingBit(EXTI_Line13);}}
}
说明:
主函数与中断函数相辅相成,程序整体使用外部中断推进以及实现一些特殊功能(立即复位);
由于主函数内小功能函数都借助while()循环实现,设置循环标志位Stop_Sign和程序阶段标志位Programme_Progress来推进主函数;
按下GPIOB,GPIO_Pin_12的按键触发中断,Programme_Progress++以及相应的Stop_Sign=0,以控制目前运行小功能停止并进入下一阶段;
在程序开始和结束处执行Programme_Reset()函数,重置各标志位实现程序重新运行;
注意:
在中断函数内部涉及到的延时函数统统使用EXIT_LINE_Timer_delay_ms()函数,区别外部使用的Timer_delay_ms()函数,防止调用冲突程序卡死
(2)绿色激光云台
//绿车
int r_x=0,r_y=0;
void Get_RaG_Point_Pos(void)
{if(USART_RX_INFO[0]=='g'&& USART_RX_INFO[8]=='r'&& USART_RX_INFO[4]=='y'&& USART_RX_INFO[12]=='y') //检查数据定位是否正确(上位机发送信息为:x123y456){x=(USART_RX_INFO[1]-'0')*100+(USART_RX_INFO[2]-'0')*10+USART_RX_INFO[3]-'0';y=(USART_RX_INFO[5]-'0')*100+(USART_RX_INFO[6]-'0')*10+USART_RX_INFO[7]-'0';r_x=(USART_RX_INFO[9]-'0')*100+(USART_RX_INFO[10]-'0')*10+USART_RX_INFO[11]-'0';r_y=(USART_RX_INFO[13]-'0')*100+(USART_RX_INFO[14]-'0')*10+USART_RX_INFO[15]-'0';}
}void G_Follow_R(int Reach_Pos_CL_MODE)
{int Sign(int num);void Get_RaG_Point_Pos(void);int near(int r_x,int Target_Y);int diff_x,diff_y,dis;while(Reach_Pos_CL_Stop_Sign){if(x==0&&y==0)Reach_Pos_OL(750,750);Get_RaG_Point_Pos();dis=near(r_x,r_y);if(dis<=20){Beep_Times(300,1,NORMAL_MODE);Led_Times(300,1,NORMAL_MODE);continue;}if(Reach_Pos_CL_MODE==PID_MODE && dis>60) //用pid计算舵机单位数{Get_RaG_Point_Pos();diff_x=Pid_Control(Level_Kp,Level_Ki,Level_Kd,r_x,x,PID_REALIZE);diff_y=Pid_Control(Vert_Kp,Vert_Ki,Vert_Kd,r_y,y,PID_REALIZE);}else if(Reach_Pos_CL_MODE==MINMIZE_MODE) //以舵机最小分辨率为单位{Get_RaG_Point_Pos();diff_x=-0.5*Sign(x-r_x);diff_y=-0.5*Sign(y-r_y);}else if(Reach_Pos_CL_MODE==PID_MODE && dis<=60) //用pid计算舵机单位数{Get_RaG_Point_Pos();diff_x=-0.4*Sign(x-r_x);diff_y=-0.4*Sign(y-r_y);}Spinnig_Level(X_DIR*diff_x);Spinnig_Vert(Y_DIR*diff_y);Timer_delay_ms(20);}
}
说明:
上位机数据格式为:g123y123r123y123,实时传输红绿激光点两个坐标;
执行点到点的跟踪即可,在主函数中不断重复即可,即while(1)G_Follow_R(PID_MODE);
2.jetson nano上关键源码分析
文件说明:
mian_10、main_11、mian_12是测试函数,分别测试铅笔线识别效果、A4纸识别效果、红绿激光分别识别效果。设置了滑动条供调参使用,确定好参数
q_1、q_2、q_3即为三个问题对应的程序,分别实现发送铅笔线顶点和中心坐标后实时传输红色激光点坐标、发送A4纸顶点坐标后实时传输红色激光点坐标、实时传输红色和绿色激光点坐标
I.相机参数的调整
string gstreamer_pipeline(int capture_width, int capture_height, int display_width, int display_height, int framerate, int flip_method)
{return "nvarguscamerasrc exposurecompensation=1 ! video/x-raw(memory:NVMM), width=(int)" + to_string(capture_width) + ", height=(int)" +to_string(capture_height) + ", format=(string)NV12, framerate=(fraction)" + to_string(framerate) +"/1 ! nvvidconv flip-method=" + to_string(flip_method) + " ! video/x-raw, width=(int)" + to_string(display_width) + ", height=(int)" +to_string(display_height) + ", format=(string)BGRx ! videoconvert ! video/x-raw, format=(string)BGR ! appsink";
}
这里设置好管道参数,主要调整曝光和饱和度,方便之后线条的检测以及红绿激光的区分
可以参考:NVIDIA Jetson Nano 2GB 系列文章(9):调节 CSI 图像质量
II.关键识别算法
(1)铅笔线识别及顶点的计算
变量解析:
int Find = 0, l_x = 0, l_y = 0, r_x = 0, r_y = 0;
int l[2][2],r[2][2],u[2][2],d[2][2];
int ul[2],ur[2],dl[2],dr[2],ce[2];
Find有效个数标志位,表示找到了几组有效的边上两点;
l_x、l_y、r_x、r_y寻找标志位,为1则分别表示上下左右边未找到有效值的两点值
l[2][2]、r[2][2]、u[2][2]、d[2][2]分别存储上下左右边上两点坐标
ul[2]、ur[2]、dl[2]、dr[2]、ce[2]分别存储最终的顶点和中心点坐标
过程:
——转换成灰度图->阈值划分成二制图->霍夫直线检测得到直线并输出直线上两点坐标
——设计算法过滤筛选重复直线并存储两点坐标
for (size_t i = 0; i < linesPPHT.size(); i++) {x1 = linesPPHT[i][0], y1 = linesPPHT[i][1], x2 = linesPPHT[i][2], y2 = linesPPHT[i][3];line(image, Point(x1, y1), Point(x2, y2), Scalar(0), 1, 8);if (x1 < 150 && x2 < 150 && myabs(x2 - x1) < 3 && !l_x){Find++;l_x = (x2 + x1) / 2;l[0][0]=x1;l[0][1]=y1;l[1][0]=x2;l[1][1]=y2;}else if (y1 < 150 && y2 < 150 && myabs(y1 - y2) < 3 && !l_y){Find++;l_y = (y1 + y2) / 2;u[0][0]=x1;u[0][1]=y1;u[1][0]=x2;u[1][1]=y2;}else if (x1 > 570 && x2 > 570 && myabs(x2 - x1) < 3 && !r_x){Find++;r_x = (x2 + x1) / 2;r[0][0]=x1;r[0][1]=y1;r[1][0]=x2;r[1][1]=y2;}else if (y1 > 570 && y2 > 570 && myabs(y1 - y2) < 3 && !r_y){Find++;r_y = (y1 + y2) / 2;d[0][0]=x1;d[0][1]=y1;d[1][0]=x2;d[1][1]=y2;}}
linesPPHT是霍夫直线检测函数的输出,linesPPHT.size()表示检测到直线的条数;这里根据直线上两点坐标值大小判断属于四条边的那一条;属于其中一条且之前未存储(标志位为1)(见if语句中的判断)则存储并将找点标志位Find+1;Find==4时即寻找结束
——由于霍夫直线检测算法得到的并非顶点而是直线上两点,设计求两直线交点函数
void crossline(int x1,int y1,int x2,int y2,int x3,int y3,int x4,int y4,int cross[2])
{cross[0]=(y3*x4*x2-y4*x3*x2-y3*x4*x1+y4*x3*x1-y1*x2*x4+y2*x1*x4+y1*x2*x3-y2*x1*x3)/(x4*y2-x4*y1-x3*y2+x3*y1-x2*y4+x2*y3+x1*y4-x1*y3);cross[1]=(-y3*x4*y2+y4*x3*y2+y3*x4*y1-y4*x3*y1+y1*x2*y4-y1*x2*y3-y2*x1*y4+y2*x1*y3)/(y4*x2-y4*x1-y3*x2+x1*y3-y2*x4+y2*x3+y1*x4-y1*x3);
}
输入的(x1,y1)~(x4,y4)是两条直线上四点坐标,输出交点坐标并赋值给cross;
crossline(l[0][0],l[0][1],l[1][0],l[1][1],u[0][0],u[0][1],u[1][0],u[1][1],ul);
crossline(r[0][0],r[0][1],r[1][0],r[1][1],u[0][0],u[0][1],u[1][0],u[1][1],ur);
crossline(l[0][0],l[0][1],l[1][0],l[1][1],d[0][0],d[0][1],d[1][0],d[1][1],dl);
crossline(r[0][0],r[0][1],r[1][0],r[1][1],d[0][0],d[0][1],d[1][0],d[1][1],dr);crossline(ul[0],ul[1],dr[0],dr[1],ur[0],ur[1],dl[0],dl[1],ce);
输入之前得到的坐标计算四个顶点值和中心坐标
——向下位机stm32输出坐标
sprintf(m,"#a%03db%03dc%03dd%03de%03df%03dg%03dh%03d$\n",ul[0],ul[1],ur[0],ur[1],dr[0],dr[1],dl[0],dl[1]);
uart.sendUart(m);
usleep(50000);
sprintf(m,"#i%03dj%03d$\n",ce[0],ce[1]);
uart.sendUart(m);

(2)A4纸顶点识别及巡线顶点的计算
过程:
——灰度图->二值化->角点检测得到角点坐标CornerImg
——设计算法过滤筛选得到八个顶点P[8][2](绝缘胶布内外边形成两个矩形)
#define MAX_DIS 20
int Is_Exit(int i, int j)
{int k = 0;for (k = 0; k < Find; k++) {if (myabs(P[k][0]-i)+ myabs(P[k][1]-j)
int P[8][2] = { 0 };
int Find = 0;for (int j = 0; j < CornerImg.rows; j++) {for (int i = 0; i < CornerImg.cols; i++) {if (CornerImg.at(j, i) > 150.0f) {if (!Is_Exit(i, j)){ P[Find][0] = i;P[Find][1] = j;Find++;}}}
}
Is_Exit()函数遍历已经视作有效的点,如果与当前坐标(i,j)接近则不存储;找到八个有效点退出
——设计根据八个顶点P[8][2]求得巡线四边形的顶点Vertex[4][2](同一个角的内外顶点的中点)
int Vertex[4][2] = { 0 };
int sign[8] = { 0 };
int i,j,k,dis,min = 1000;
int temp1, temp2;
for (k=0,i = 0; i < 8; i++)
{if (sign[i])continue;min = 2000;for (j = 0; j < 8; j++){if (i == j||sign[j])continue;dis = myabs(P[i][0] - P[j][0]) + myabs(P[i][1] - P[j][1]);if (dis< min){min = dis;temp1 = i;temp2 = j;}}sign[temp1] = 1;sign[temp2] = 1;Vertex[k][0] = (P[temp1][0] + P[temp2][0])/2;Vertex[k][1] = (P[temp1][1] + P[temp2][1])/2;k++;
}
这里使用for循环遍历P[8][2]中顶点,将距离最近的两点视为A4纸一个角内外两边的两个顶点,求其中点存储在Vertex[4][2]中
——设计算法使巡线的四个端点按照顺时针传输给下位机,否则巡线顺序错误
int temp;
//先整体按y值大小排序
for(i=0;i<4;i++)
{for(min=Vertex[i][1],j=k=i;j<4;j++){if(Vertex[j][1]<=min)k=j;}temp=Vertex[k][0];Vertex[k][0]=Vertex[i][0];Vertex[i][0]=temp;temp=Vertex[k][1];Vertex[k][1]=Vertex[i][1];Vertex[i][1]=temp;
}
//y值中等的两点按x值排序
if(Vertex[1][0]
if(Vertex[0][0]&& Vertex[0][1]&&Vertex[1][0]&&Vertex[1][1]&&Vertex[3][0]&& Vertex[3][1]&&Vertex[2][0]&& Vertex[2][1])
{sprintf(m,"#k%03dl%03dm%03dn%03do%03dp%03dq%03dr%03d$\n", Vertex[0][0], Vertex[0][1],Vertex[1][0], Vertex[1][1],Vertex[3][0], Vertex[3][1],Vertex[2][0], Vertex[2][1]);u.sendUart(m);
}
观察任意矩形顶点坐标规律,要顺时针发送,可将y值最小的作为第一个发送,y值最大的第三个发送,介于中间的两点按x值大小判断,x小的最后发送,大的第二个发送
即先整体按y值大小排序,y值中等的两点按x值排序->排序后按0~1~3~2的顺序发送坐标

(3)区分红绿激光
过程如下:
Point color_recognite(Mat image, Scalar Low, Scalar High)
{vector> g_vContours;vector g_vHierarchy;vector hsvSplit;double maxarea = 0;int maxAreaIdx = 0;Mat g_grayImage, hsv, g_cannyMat_output;cvtColor(image, hsv, COLOR_BGR2HSV);split(hsv, hsvSplit);equalizeHist(hsvSplit[2], hsvSplit[2]);merge(hsvSplit, hsv);inRange(hsv, Low, High, g_grayImage);//二值化识别颜色//开操作 (去除一些噪点)Mat element = getStructuringElement(MORPH_RECT, Size(2, 2));morphologyEx(g_grayImage, g_grayImage, MORPH_OPEN, element);//闭操作 (连接一些连通域)morphologyEx(g_grayImage, g_grayImage, MORPH_CLOSE, element);// Canny(g_grayImage, g_cannyMat_output, 80, 80 * 2, 3);// 寻找轮廓findContours(g_grayImage, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));//假设contours是用findContours函数所得出的边缘点集RotatedRect box;Point centre;if (g_vContours.size() != 0){for (int index = 0; index < g_vContours.size(); index++){double tmparea = fabs(contourArea(g_vContours[index]));if (tmparea > maxarea){maxarea = tmparea;maxAreaIdx = index;//记录最大轮廓的索引号}}box = minAreaRect(g_vContours[maxAreaIdx]);rectangle(image, box.boundingRect(), Scalar(0, 0, 255), 2);centre = box.center;}return centre;
}
关键是调用inRange()函数HSV色彩空间二值化的阈值上下限设置
可以参考:OpenCV学习笔记-inRange()阈值操作函数怎么用_cv.inrange函数
并设置滑动条调整参数获得经验值
最终评判标准:
激光在绝缘胶布上是能否识别(黑色胶布吸光;通过提高曝光,调参,增大激光功率等可以解决)
红绿激光靠近时能否区分(红绿在HSC空间互斥,更亮的会掩盖另一个;调inRange()参数解决)
二.学习资料分享
学习笔记
OpenCV学习笔记——《基于OpenCV的数字图像处理》_switch_swq的博客-CSDN博客
图像识别小车(电源部分)——电赛学习笔记(1)_switch_swq的博客-CSDN博客
图像识别小车(电机部分)——电赛学习笔记(2)_switch_swq的博客-CSDN博客
图像识别小车(jetson nano部分)——电赛学习笔记(3)_switch_swq的博客-CSDN博客
图像识别小车(PCB设计)——电赛学习笔记(4)_switch_swq的博客-CSDN博客
PID控制算法理解_switch_swq的博客-CSDN博客
其他资料
1.唐老师讲电赛的个人空间-唐老师讲电赛个人主页-哔哩哔哩视频
2.电赛资料:电赛资料_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:1234
3.我的“电赛”、“VS Studio”、“cmake”、“opencv”、“makefile”、“linux、操作系统”、“stm32”收藏夹
3.以及上面笔记中所包含信息
三.备赛阶段记录
7.2.2023
- 问题
- Nano板子供电5v4A,需求电流较大
- 实验室现有开关电源模块似乎调不了5v,需自己设计电源模块
- 作为底层主要负责,如何为项目打下坚实基础
- 硬件使用有明确目标,如何学习
- 解决
- 先不考虑电源模块,用适配器及直流电源供电
- 先把电机控制写好,提供友善接口
- 先用简单硬件过度,后期转高级的。如电机先用直流后用伺服。先拼起来,再细化雕琢
- 收获
- 用vscode远程开发jetson,下载remote-ssh插件,ssh jetson@IP地址;快捷键ctrl+o调出要打开页面
7.3.2023
- 问题
- Stm32很多知识忘记,如定时器和A/D、D/A;是否需要复习,因复习耗时且不一定需要stm32
- 目前目标尚不明确
- 解决
- 先将stm32相关知识看完,stm32作为保底
- 先搭一个蓝牙遥控小车!
- 收获
- 修好了学长的小车,看到了PID实现双轮平衡小车的现象
- 学会了MG995型号舵机控制(控制脉冲占空比实现角度控制)
- 搭建了测试平台(OLED屏、蓝牙、i2c通讯)
- 搭建了简单的两轮遥控小车,采用直流电机控制,未加入PWM波调速,实现简单的前进、倒退、转弯。
7.4.2023
- 问题
- 昨天做的小车电源直接冒烟,因为电源采用两节3.7V锂电池供电,buck升压到12V以匹配LM298N,电流过大
- 解决
- 暂时给l298n提供5V电压,驱动能力下降,但系统可以运行。以后电源模块之后重新设计或使用小功率电机
- 收获
- 主要将昨天搭好的小车完善,并加入了测压模块(利用STM32的ADC外设)
- 复习了stm32相关知识(外部中断,定制器TIM设置,定时器比较OC产生PWM波)
- 打开了jetson nano的摄像头,它睁开了眼
7.5.2023
- 问题
- 编码器旋转无响应,电机也不动了
- 烧了一个stm32板子,当时接的自制稳压模块,之前都是好的,不知道什么原因
- 解决
- 重新测试电机是否能正常工作
- 在小车到达前有时间学习MPU6050,相关姿态轨迹传输算法
- 收获
- 在jetson nano上跑了例程及自己上传的几张图片
- 学会了linux的vim的使用
- 复习了stm32相关知识(定时器IC输入捕获模式)
- 学会了超声波测距CS100A模块和红外传感模块以及电机编码器部分
7.6.2023
- 问题
- 如何解决电机编码器输出波形峰值小,stm32无法接收
- Pwm波和电源需供电,不然波形失真
- 解决
- 昨天电机不转是因为接线不紧,编码器不行是因为输出电压太小,只有0.5v左右
- 考虑IO口输出模式,不行加电压比较器,ref=0.33V
- 要重新系统性设计电源了,所有信号共地!
- 收获
- 拼好了大车,发现了诸多问题,舵机控制程序完成
- 浅浅学了PCB绘制流程
7.7.2023
- 问题
- 控制函数太过简陋,后续仍需不断升级
- 图像识别进度为零
- 解决
- 《基于opencv的数字图像处理技术》
- 收获
- 用洞洞板搭建好了电源系统(12V-5V-3.7V),系统完全移植到大车
- 新车编码器输出足够大,无需放大器,编码器计数正常
7.8.2023
- 收获
- 学会在Windows配置OpenCV环境,掌握OpenCV图像视频基本操作以及一些基础知识
- PCB绘制进展
7.9.2023
- 收获
- 学会在linux中运行调用OpenCV的c++文件(cmake的使用)
- 学习OpenCV基本数据结构和类的使用
- 进一步了解VS studio上编译选项配置以及debug和release的区别
7.10.2023
- 收获
- 学会OpenCV灰度变换、直方图、边缘检测、霍夫检测直线和圆
7.11.2023
- 问题
- 源码在linux上无法运行(OpenCV调用摄像头出问题,采用CMake方法编译)
- 解决
- 今天下午加晚上未解决
- 收获
- 学会阈值分割(图像二值化方法)
7.12.2023
- 问题
- C++无法编译成功,Mat类未定义引用(QT上编译)
- 解决
- 使用python编写运行成功
- 收获
- 看完特征提取和目标检测(HOG特征+SVM基本流程;LBP特征+级联分类器)
7.13.2023
- 问题
- 依旧无法运行以C++运行OpenCV代码
- 可以运行的OpenCV代码不能直接以videocapture capture(0)的方法获取视频流
- 解决
- 重新系统性安装OpenCV库并重走CMake流程
- 将视频流通过管道gstreamer传输
- 收获
- 学会CMake以及基本编译链接流程
- 重新安装配置OpenCV4.8.0,成功在jetson nano上运行OpenCV代码
7.14.2023
- 问题
- 但运行自己编写的直线检测程序过于卡顿,一秒一帧
- 蓝牙模块无法正常工作
- 解决
- 霍夫直线检测运算量大,不使用该算法
- 调整视频大小及帧率
- 经检测应是蓝牙模块问题,重新购买
- 收获
- 使用画好的pcb搭建小车,将全部器件搭载在小车上
7.15.2023
- 问题
- 电机控制出错,一边电机不受控制
- 目前控制算法学的太少,但图像识别进展不够
- 解决
- GPIO口选到了下载口JTDI/O,换GPIO口控制
- 先用超声波模块、MPU6050、红外传感等模块写避障、路径记录、寻迹等功能
- 收获
- 学会jetson nano上的GPIO使用(基本和树莓派一样)
- 解决了电机的基本控制问题并将电机的四控制线改成了两根
- jetson使用电池供电(器件全供地);实现stm32与jetson nano的usart通信(照搬蓝牙)
7.16.2023
- 问题
- 超声波测距模块中断代码写的不好,拔下模块进入while循环等待,系统卡住
- 拉肚子
- 解决
- 使用static变量,进入中断模式改为EXTI_Trigger_Rising_Falling...
- 休息一天(今日中午至明天中午)
7.17.2023
- 问题
- 欲添加mpu6050模块,但其与oled、蓝牙、超声波模块冲突(非引脚分配问题)
- 解决
- 更改方案,debug试试。仍不行
7.18.2023
- 问题
- 昨天问题仍然存在
- 多个中断之间不协调,影响超声波测距精度。以及测角度过于耗时
- 解决
- 使用江科大自动化的例程代码,简洁明了,解决冲突
- 更改各个中断优先级,控制mpu6050的使用
- 收获
7.19.2023
- 收获
- 学会PID算法
- 重新绘制PCB,解决若干问题
7.20.2023
- 问题
- SysTick定时器冲突问题(外部和中断同时调用delay_us函数会卡死)
- 解决
- 避免了0.1s定时器中断(数据刷新)的SysTick定时函数
- 收获
- 使用编码器利用PID编写行驶给定长度函数及测速
7.21.2023
- 问题
- 后退时编码器反向计数,上限不明确,速度测算出现问题
- 解决
- 通过TIM_EncoderInterfaceConfig设置编码器反转依旧向上计数
- 收获
- 编写小车倒车定长距离
7.22.2023
- 收获
- 编写小车以恒定速度行驶和拐弯90度算法
7.23.2023
- 收获
- 焊好新到的板子
7.24.2023
- 问题
- Jetson配置难,yolo难跑通
- 要求设计完整程序,在jetson开机时自动执行
- 解决
- 学习OpenCV备用
- 学习python或c++可执行文件Linux开机自动执行方法
7.25.2023
- 问题
- 采用硬盘直接克隆方式克隆SD卡依旧无法启动jetson nano系统
- 解决
- 烧录官方镜像文件,成功还原系统。并发现python和C++环境已经配好,之前不会用。解决yolov5摄像头实时检测问题,方案参考亚博论坛。C++也是,g++编译时加上一个参数就行
7.26.2023
- 收获
- 看今年电赛器件清单,简单编写完云台代码,购买K210等器材
7.27.2023
- 收获
- 简单编写完红外寻迹功能
- 解决nano开机启动python文件
7.28.2023
- 问题
- 分析电赛清单,云台摄像头加激光笔应该涉及到动态物体追踪
- 解决
- 学习视频目标跟踪
7.29.2023
- 问题
- Stm32定时器资源有限无法满足云台的加入
- 解决
- 使用pca9685驱动
- 收获
- 采用stm32管脚重定义解决pca9685驱动的使用问题
7.30.2023
- 问题
- Jetson nano的C++库不包含串口uart相关内容
- 解决
- 使用其设备/dev/tthTSH1,研究网上代码
- 收获
- 编写以及pca9685控制云台函数
7.31.2023
- 问题
- 霍夫圆检测一定也不稳定
- 解决
- 调整参数或使用深度学习识别物体的方法
- 收获
- 终于解决串口通信问题,实现C++语言的nano和电脑以及stm32通信
- 完善霍夫圆检测代码,加入uart传输圆心坐标
- 学会nano开机自启动程序方法
- 编写stm32的PID点跟踪函数,实现点跟踪
8.1.2023
- 问题
- 走定长不精确,大约是设定5cm行驶6cm这个比例
- 收获
- 编写摇杆控制云台程序
- 发现之前使用的pid算法全犯了低级错误,本应用float定义PID值结果用了int,修改后大范围应用,各个控制加入PID平稳精确了很多
四.电赛总结及经验教训
本次比赛作品的不足、改进之处
1.stm32和jetson nano通讯不稳定(可能原因:杜邦线传输能力差、波特率可能设置高了【但低了影响系统处理速度】)(实际原因:while写成了if,导致时机很难对上,通信规则设计失误!)
2.stm32主函数设计不行,没花时间改进,想要重复运行某个程序只能重启,人机交互也不友好
3.比赛报告没有在头脑风暴之后就开始写,导致后期书写太急,不够规范
4.linux操作不熟,开机自启动程序出现问题,且jetson nano上的程序设计缺乏系统性结构性(每问都写了一个程序,而不是整合成一个大的测试程序)
5.所有任务完成太晚,没有留下时间仔细调试调参找问题。而且全流程过一遍后立马就要封箱了,急急忙忙乱改代码导致出现了意想不到的错误!再给一天就刚好了啊!!!
本次比赛的经验教训
1.器件准备很重要:比赛发布器件清单后要备齐,最好每个器件都多买几个。以满足比赛器件需求并防止比赛时器件损坏!(本次比赛oled屏、舵机都反复坏过)
2.器件精度很重要:比赛前统计自己所有器件清单,并实测是否可以使用?精度如何?硬件精度不足会直接导致结果无法满足!(本次比赛刚开始使用的舵机为20kg大扭矩低精度,调了一晚PID参数舵机仍然运动不准,最后才发现是精度问题)
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
