第十八届智能车摄像头三轮组赛后总结

0.简言

本人是第十八届智能赛摄三组的参赛选手,这是我第一次参加比赛,对于这次的竞赛经历,我有一些感想,所以在这里写一篇文章来总结一下这次的比赛。这篇文章主要分享给第一次做智能车竞赛的同学们。简单讲一下本人对于智能车竞赛的理解。

1.前期的准备

1.1核心板

一般我们使用逐飞科技和龙邱科技这两家公司的产品去做智能车比赛。个人建议要买器件就全在一家买,因为两家的产品设计不一样,买一家的比较方便。我们这次摄三组使用的是英飞凌公司的TC264,我使用的是逐飞科技的核心板还有母板,全新正版的一整套还是比较贵的,可以去咸鱼上买二手的核心板、母板、下载器。

1.2编译器

编译器只能使用AURIX Development Studio(简称ADS)去编译开发TC264。可以直接去逐飞科技的官网上下载TC264的资料,里面会有该软件的下载包。

链接:https://pan.baidu.com/s/1G9-cjWjk06_sMLLhicqfmw 
提取码:6666

核心板和编译器资料

1.3摄像头

我使用的是逐飞科技的总钻风。在逐飞科技的官网上也可以找到相关的例程。

链接:https://pan.baidu.com/s/1w10mAg_pBtZ06_XZx0cgxg 
提取码:6666

这是我存在百度网盘里的资料可自取。

2摄像头寻线与元素识别

2.1摄像头采集图像

我选择逐飞科技TC264芯片总钻风的例程,选择display demo这个例程。下载就看采集到摄像头的数据,并且会在lcd屏幕上显示。显示出来的图像是灰度图。

其中mt9v03x_finish_flag就是采集完一帧画面的标志位,我们在运行完tft180显示函数之后还需要把该标志位清零。摄像头的采集其实就是把一个图片分成很多个像素点,每个像素点用0-255的数字表示,数字越大,这个像素点越白,这就是灰度图。然后我们把这个图片按顺序存入到一个二维数组中,之后就可以对其操作。这段代码中存入的数组就是mt9v03_image这个数组,我们可以通过索引去到它的定义。

在定义中我们可以更改MT9V03X_H和MT9V03X_W这两个变量的值(其他的变量根据自己的需要去改变),就是改变我们图像的大小。我们在比赛中的图像是用不到很大的,如果设置了更大的像素,相应的帧率就会减小。对我们小车的控制不利。所以我们一般先设置为120*188。

2.2大津法二值化

我们得到灰度图后就可以开始处理图像了。我们处理有很多方法,我用的就是最简单的图像二值化法,难一点的还有灰度图法和边缘检测算法等。我这里就讲一下最简单的图像二值化法。顾名思义,二值化就是只有两个值,我们将灰度图的每一个像素点全都变成0和255。0就代表黑色,255就代表白色。做到以下图片这种变化就是二值化。

但是我们怎么样来判断哪个点应该为黑哪个点为白呢,这里我们就要用另一个算法——大津法。大津法主要讲就是我们给他一个图片然后这个算法会计算出一个阈值,当我们灰度图上的像素点的值大于这个阈值我们让这个点为255也就是白色,小于这个阈值是为黑色,这样我们就得到了二值化的图像。(以下是我找的大津法代码和二值化代码,车赛也用过)

/****************大津法找阈值***************/
uint8 otsuThreshold(uint8 *image, uint16 width, uint16 height)
{
    #define GrayScale 256
    int pixelCount[GrayScale] = {0};//每个灰度值所占像素个数
    float pixelPro[GrayScale] = {0};//每个灰度值所占总像素比例
    int i,j;
    int Sumpix = width * height;   //总像素点
    uint8 threshold = 0;
    uint8* data = image;  //指向像素数据的指针
    //统计灰度级中每个像素在整幅图像中的个数
    for (i = 0; i < height; i++)
    {
        for (j = 0; j < width; j++)
        {
            pixelCount[(int)data[i * width + j]]++;  //将像素值作为计数数组的下标
          //   pixelCount[(int)image[i][j]]++;    若不用指针用这个
        }
    }
    float u = 0;
    for (i = 0; i < GrayScale; i++)
    {
        pixelPro[i] = (float)pixelCount[i] / Sumpix;   //计算每个像素在整幅图像中的比例
        u += i * pixelPro[i];  //总平均灰度
    }

    float maxVariance=0.0;  //最大类间方差
    float w0 = 0, avgValue  = 0;  //w0 前景比例 ,avgValue 前景平均灰度
    for(int i = 0; i < 256; i++)     //每一次循环都是一次完整类间方差计算 (两个for叠加为1个)
    {
        w0 += pixelPro[i];  //假设当前灰度i为阈值, 0~i 灰度像素所占整幅图像的比例即前景比例
        avgValue  += i * pixelPro[i];

        float variance = pow((avgValue/w0 - u), 2) * w0 /(1 - w0);    //类间方差
        if(variance > maxVariance)
        {
            maxVariance = variance;
            threshold = (uint8)i;
        }
    }
    return threshold;
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介    二值化摄像头函数
// 参数说明    void
// 返回参数    void
// 使用示例
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void Get_Binary_Image(void)
{
    int sita;
    sita=otsuThreshold(mt9v03x_image[0],MT9V03X_W,MT9V03X_H);//大津法二值化结果
    for (int i=0;i<MT9V03X_H;i++)//对图像进行二值化处理
    {
        for (int j=0;j<MT9V03X_W;j++)
        {
            if (mt9v03x_image[i][j]>sita)
            {
                bin_img[i][j]=255;
            }

            else
            {
                bin_img[i][j]=0;
            }
        }
    }
}

其中mt9v03x_image[i][j]是摄像头采集到的初始灰度图,bin_img[i][j]是我们自己定义的二维数组用于存放二值化后的图像。(下图是处理后的效果)

(图像的最下端为119行 最上端为第0行 最左边为第0列 最右边为第80列)

2.3寻找两边线以及中线

关于摄像头循迹控制,一般都是根据赛道中线来进行循迹的。对于赛道中线,我们可以寻找左右边线得到,类似于(x1+x2)/ 2 我们就可以得到中线。

我们找两边线的办法有很多,这里介绍最简单的一种就是扫线。我们从图像最下端开始依次往上,从中间依次向两边寻找赛道的边缘。因为我们在2.2中已经将图像变成了二值化图像,这样我们去找边线就比较简单了。

例如找第100行的左边线:黑 黑 白 当我们发现第一个黑点后(该黑点的左边也是黑点,右边是白点),那么这个黑点就是我们找到的左边线。然后我们把该点的列坐标记录下来即可。

第100行的右边线同理:白 黑 黑 当我们发现第一个黑点后(该黑点左边是百点,右边是黑点),那么这个黑点就是我们找到的右边线。同理记录

以下代码是该方法比较简单的实现。在寻找变现的时候我们还可以同时记录左右边线丢线的次数、每一行的赛道宽度等信息。便于我们之后进行一些元素操作。(这些需要大家自己去编写每个人的需求不同)

#define START_LINE  120
#define END_LINE 0
#define My_W     188
#define My_H     120

uint8 left_line[My_H];
uint8 right_line[My_H];
uint8 bin_img[My_H][My_W];

/****************图像寻线函数***************/
void Find_line(void)
{
    uint8 hang,lie;
    for(hang = START_LINE-1;hang > END_LINE;hang--)
    {
        left_line[hang] = 0;
        right_line[hang] = 187;
        for(lie = 94;lie > 3 ;lie--)
        {
            if(bin_img[hang][lie] == 0 && bin_img[hang][lie-1] == 0 && bin_img[hang][lie+1] == 255)
            {
                left_line[hang] = lie;
            }
        }

        for(lie = 94;lie < 184 ;lie++)    
        {
            if(bin_img[hang][lie] == 0 && bin_img[hang][lie-1] == 255 && bin_img[hang][lie+1] == 0)
            {
                right_line[hang] = lie;
            }
        }
        mid_line[hang] = ((right_line[hang]+left_line[hang]) / 2);
        road_width[hang] = INT_ABS(left_line[hang] - right_line[hang]);
    }
}

2.4经典元素的识别

对于所有的元素来说我们就需要做的只有两步 1识别 2处理。识别:我们只需要根据每个元素不同的图像特点,并用c语言描述出来就行。处理:我们在对环岛经行处理的时候要用状态机的写法,简单来说就是,达成一个条件,进入一种状态,依次经行。在不同的状态让单片机控制车模经行不同的操作即可。

2.4.1环岛

环岛会出现在赛道的左右两边,所以我们需要将这两种情况分开来写,但其实我们写好了其中一个,另一个就可以直接照抄。所以我们这里只举例赛道右边有环岛的情况。

识别:我们可以使用最简单的方法去判断,比如说当我们的左边线没有生变化,而右边线出现了大量的丢线(就是右边线数组的值为187)我们就认定出现了环岛这个元素,当然这种判断是非常简单的,所以会出现错误判断的时候。要想精确的判断这个元素,我们就需要把这个元素更多的特征描绘出来,或者是用更好的方法去描述。

图像左侧:方法一:当我们遇到环岛时,左边线是不会出现变化的,我们可以计算左边线的斜率(不一定要120行所有的左边线,我们可以取一段)然后判断这个斜率的范围是在某一个区间就可以认定是环岛的特征之一。方法二:我们将车模放在赛道的中央,读取此时左边线的值并将其记录在单片机中,我们用实时的左边线数组减去记录的左边线数组,将这个误差控制在一个区间,也可以判断左边线是没有变化的。还有其他的方法,这个需要大家自己去思考。

图像右侧:方法一:我们取固定的一段比如说100——60行,我们记录实时的丢线数,当丢线数大于35时,说明可能时环岛认定是特征之一。方法二:我们找如下图的这三个特征点,找到之后记录下来,当出现这三个特征点的时候,在综合左侧的特征就可以认定是环岛。然后进入环岛的状态机。

处理:我们主要的处理操作就是对图像左右边线缺失的地方,把左右边线补起来,使得我们的车模可以跑出我们想要的路线。

补线:方法一:比如说我们现在要将上图中点1和点2连起来,把右边线给补齐,我们把图像看作是一个xy的坐标,根据y=kx+b。我们已知点1和点2的坐标可以计算出斜率,然后将已知的点1点2代入式中可以求出b,现在我们只需要把每一个y值,就是行数带入就可得到x,(也就是列坐标,也是我们的右边线)方法二:我们将车模放在赛道的中央,读取此时赛道宽度的值(就是左边线减去右边线的绝对值,road_width[hang] = INT_ABS(left_line[hang] - right_line[hang]);)并将其记录在单片机中,还是补上图点1与点2,我们就可以把左边线数组的值加上赛道宽度,就可以得到赛道右边线,然后给赛道右边线重新赋值即可。方法很多大家可以去找更好的方法。

整个进入环岛以及出环岛的过程大概如下:

当我们识别到环岛时状态为1:我们需要开始补线了,将上图的点1和点2连起来,补成一条和左侧一样的直线。我们的车模才能够顺着赛道跑。并且开始判断什么时候进入下个一状态,比如说当点1消失就进入图2。

然后状态为2:继续补右边线,并且判断什么时候进入图三。条件大家就自己看吧。

依次类推

直到我们出环岛,这个元素就结束了。

我是分成七个状态来写的,大家根据自己的需求进行编写。

以下代码只供参考

//-------------------------------------------------------------------------------------------------------------------
// 函数简介     环岛状态补线函数
// 参数说明     void
// 返回参数     void
// 使用示例
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void Roundabout(void)
{
    uint8 hang = 0;
    uint8 round_flig = 0;
    uint8 round_flig1 = 0;
    uint8 round_flig2 = 0;

    uint8 down_turn_point_X = 0;
    uint8 mid_turn_point_X = 0;

    uint8 down_turn_point_Y = 0;
    uint8 mid_turn_point_Y = 0;

//***************************************状态判断*******************************************//
    switch(round_state)
    {
        case 0:if(enter_round_flig==1){round_state = 1;}break;
        case 1:if(left_line[70] <= 5 && left_line[30] != 0){round_state = 2;};break;
        case 2:if(count_lost_line(1,50,25) == 25){round_state = 3;};break;
        case 3:if(rightfindflag[60] == 0){round_judge_flig = 1;}if(round_judge_flig == 1 && rightfindflag[60] == 1 ){round_state=4;};break;
        case 4:if(rightfindflag[25] == 0 && rightfindflag[25] == 0 && count_lost_line(3,50,25) >= 15){round_state=5;};break;
        case 5:if(leftfindflag[50] == 0 && rightfindflag[50] == 0 && leftfindflag[70] == 0 && rightfindflag[70] == 0){round_state=6;};break;
        case 6:if(rightfindflag[30] == 1  && rightfindflag[50] == 1 && rightfindflag[40] ==1){round_state=7;};break;//&& rightfindflag[60] ==1
        case 7:if(leftfindflag[50] == 1){round_state = 8;}break;
    }
//**************************************状态操作******************************************//
    if(round_state==1)
    {
        for(hang = START_LINE-1;hang>=1;hang--)
        {
            if(round_flig==0 && (left_line[hang]-left_line[hang-1])>=6 && leftfindflag[hang]==1
              )
            {
                down_turn_point_X=left_line[hang];
                down_turn_point_Y=hang;
                round_flig=1;
            }
            if( round_flig == 1 && round_flig1 == 0 && leftfindflag[hang] == 1 && leftfindflag[hang+1] == 1 && leftfindflag[hang+2] == 0)       //&& l_lost[hang]!=1
            {
                round_flig1 = 1;
            }
             if(round_flig1==1 && round_flig2==0 && (left_line[hang]-left_line[hang+1])>=0 && (left_line[hang]-left_line[hang-1])>=0 && leftfindflag[hang]==1 && leftfindflag[hang+1]==1 && leftfindflag[hang-1]==1
               )
            {
                mid_turn_point_X=left_line[hang];
                mid_turn_point_Y=hang;
                round_flig2=1;
            }
         }
            ImageAddingLine(1,mid_turn_point_X,mid_turn_point_Y,down_turn_point_X,down_turn_point_Y);
    }

    if(round_state==2)
    {
        for(hang = START_LINE-1;hang>=1;hang--)
        {
            if( round_flig1==0 && leftfindflag[hang]==1 && leftfindflag[hang+1]==1 && leftfindflag[hang+2]==1)       //&& l_lost[hang]!=1
            {
                round_flig1 = 1;
            }

             if(round_flig1==1 && round_flig2==0 && (left_line[hang]-left_line[hang+1])>=0 && (left_line[hang]-left_line[hang-1])>=0 )
            {
                mid_turn_point_X=left_line[hang];
                mid_turn_point_Y=hang;
                round_flig2=1;
            }
         }
            ImageAddingLine(1,mid_turn_point_X,mid_turn_point_Y,0,99);
    }

    if(round_state==3)
    {
        for(hang = START_LINE-1;hang>=1;hang--)
        {
            if(round_flig1==0 && (left_line[hang]-left_line[hang+1]>=40) && leftfindflag[hang-1]==1)
            {
                mid_turn_point_X=left_line[hang];
                mid_turn_point_Y=hang;
                round_flig1=1;
            }
         }
        ImageAddingLine(2,mid_turn_point_X,mid_turn_point_Y,179,58);
    }

    if(round_state==4)
    {
        Dir_control(direction_error(30,60),Target_Speed,1);
    }

    if(round_state==5)
       {
           for(hang = START_LINE-1;hang>=1;hang--)
           {
               if( round_flig==0 && rightfindflag[hang]==1 && rightfindflag[hang+1]==1 && rightfindflag[hang+2]==1)       //&& l_lost[hang]!=1
               {
                   round_flig = 1;
               }
               if(round_flig1==0 && round_flig==1 && right_line[hang]-right_line[hang+1]<=0 && right_line[hang]-right_line[hang-1]<=0 )
               {
                   mid_turn_point_X=right_line[hang];
                   mid_turn_point_Y=hang;
                   round_flig1=1;
               }
            }
           ImageAddingLine(2,70,15,mid_turn_point_X,mid_turn_point_Y);
       }

    if(round_state==6)
    {
        ImageAddingLine(2,70,10,179,60);
    }

    if(round_state==7)
    {
        for(hang = START_LINE-1;hang>=1;hang--)
        {
            if(round_flig1==0 && left_line[hang]-left_line[hang-1]>=15 && leftfindflag[hang] == 1)
            {
                mid_turn_point_X=left_line[hang];
                mid_turn_point_Y=hang;
                round_flig1=1;
            }
         }
        ImageAddingLine(1,mid_turn_point_X,mid_turn_point_Y,0,99);
    }

    if(round_state==8)
    {
        enter_round_flig=0;
        round_state=0;
        Element = 0;
        gpio_low(P33_10);//蜂鸣器初始化
    }
}

2.4.2十字路口

整体和环岛差不多,也是找特殊点,然后进行补线等操作。这里就不说了。

2.4.3斑马线

识别大概跟找边线差不多,只不过我们这里需要找到多个黑与白的跳变点,我们可以在某一段里面去找这个斑马线,然后发现大量跳变点的时候停车。跟之前的元素一样,也有更好的写法,与识别方法大家努力去实现就可以了。

每一年其他的元素都不确定,所以我就不说了,反正我的大概思路就是这样,写元素的方法掌握了什么元素都能解决。

3车模的控制

最基础的就是速度环与方向环有了这两个小车就可以跑完整个赛道。剩下的环都是锦上添花,根据自身能力进行添加。

对于这两个环,一开始我们可以将他们并起来,就是把两级的输出加在一起来控制小车的转动,这样比较好上手。调完之后可以使用串级,就是把方向环的输出作为速度环的输入,实现两级串联。

3.1速度环

这个速度环主要是控制我们车子的两个轮子保持同速度,我们想让他们跑多快就跑多快,进行速度控制。这里就用最经典的PID控制。对于PID的控制算法网上有很多讲解,我感觉我讲不好,所以大家就去知乎或者本站里自行寻找该算法的讲解。(一定要多看看,了解之后才好调参数)

//-------------------------------------------------------------------------------------------------------------------
// 函数简介    电机输出控制函数
// 参数说明    左电机输入占空比
// 参数说明    右电机输入占空比
// 返回参数    void
// 使用示例
// 备注信息
//-------------------------------------------------------------------------------------------------------------------
void Motor_out(int L_pwm,int R_pwm)
{
    L_pwm = constrain_float( L_pwm, -9999 ,9999);//限幅函数
    R_pwm = constrain_float( R_pwm, -9999 ,9999);//限幅函数

    if(L_pwm >= 0)
    {
        pwm_set_duty(LEFTGO,(int)L_pwm);
        pwm_set_duty(LEFTBACK,0);
    }
    else
    {
        pwm_set_duty(LEFTBACK,(int)-L_pwm);
        pwm_set_duty(LEFTGO,0);
    }
    if(R_pwm >= 0)
    {
        pwm_set_duty(RIGHTGO,(int)R_pwm);
        pwm_set_duty(RIGHTBACK,0);
    }
    else
    {
        pwm_set_duty(RIGHTBACK,(int)-R_pwm);
        pwm_set_duty(RIGHTGO,0);
    }
}

3.2方向环

我们的方向环主要是在一段赛道上把左边线数组相加,除以行数得到平均数,右边线数组相同处理,再把平均后的左右边线数组进行差比和算法得到相对误差,在用归一化得到方向环误差。用这个误差值作为方向环的参考数据,进行转向。我是把误差控制再了-100到+100之间,目标值为零。

int16 direction_error(uint8 min_line,uint8 max_line)
{
    uint8 hang=0;
    float left_sum = 0;
    float right_sum = 0;
    int16 DIR_ERROR = 0;
    for(hang = max_line;hang > min_line;hang--)
    {
        left_sum += left_line[hang];
        right_sum += right_line[hang];
    }
    left_sum =left_sum/(max_line-min_line);
    right_sum =right_sum/(max_line-min_line);
    DIR_ERROR = 100*(left_sum+right_sum-(2*94-1))/(right_sum-left_sum);
    return DIR_ERROR;
}

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值