智能车摄像头开源—3 图像基础处理、迭代优化与效果展示

目录

一、前言

二、摄像头采集与屏幕初始化

2.1 初始化摄像头和TFT180屏幕

2.2 初始化摄像头和IPS200屏幕

2.3 单独初始化摄像头

二、图像压缩

三、总初始化函数

四、图像补黑框

五、差比和

六、爬线算法找起点

七、求取边线

八、二维边线提取一维边线

九、阈值处理与图像迭代

十、综合梳理

十一、效果展示

1、十字效果展示—正常灯光(有补线处理)

2、圆环效果展示—正常灯光(有单边巡线处理)

3、弯道效果展示—正常灯光

4、直线效果展示—正常灯光

5、实验室开灯,遇强光效果展示

6、实验室关灯,环岛遇强光效果展示

7、实验室关灯,由暗区域过渡到高亮区域效果展示

8、实验室关灯,由高亮区域过渡到暗区域效果展示

9、一张抽象的图片


一、前言

        本文主要讲解一些基本的处理,如图像压缩、起点找寻、阈值处理等。同时展示自适应八向迷宫的运行效果。

        同时声明本文内容皆由作者实践得出,并不保证绝对正确,仅供入门者学习和参考

        本文处理的图像大小为 80 * 60

二、摄像头采集与屏幕初始化

2.1 初始化摄像头和TFT180屏幕

        直接调用逐飞库:

/**
* 函数功能:      初始化 TFT180 和 总钻风摄像头
* 特殊说明:      与 Cammer_Init_IPS200 函数只可调用其中一个,当都不调用时,默认开启摄像头初始化
* 形  参:        无
* 示例:          Cammer_Init_TFT180();
* 返回值:        无
*/
void Cammer_Init_TFT180(void)                  //初始化摄像头和显示屏     *
{
    TFT180_Show_Init();
    tft180_show_string(0,0,"mt9v034 init.");
    while(1)
    {
        if(mt9v03x_init())      //摄像头初始化
        {
            tft180_show_string(0,16,"mt9v034 reinit.");
        }
        else
        {
            break;
        }
    }
    tft180_show_string(0,16,"init success.");
    tft180_clear();

}

       

2.2 初始化摄像头和IPS200屏幕

        直接调用逐飞库函数:

/**
* 函数功能:      初始化 IPS200 和 总钻风摄像头
* 特殊说明:      与 Cammer_Init_TFT180 函数只可调用其中一个,当都不调用时,默认开启摄像头初始化
* 形  参:        无
* 示例:          Cammer_Init_IPS200();
* 返回值:        无
*/
void Cammer_Init_IPS200(void)                  //初始化摄像头和显示屏         *
{
    IPS200_Show_Init();
    ips200_show_string(0,0,"mt9v034 init.");
    while(1)
    {
        if(mt9v03x_init())      //摄像头初始化
        {
            ips200_show_string(0,16,"mt9v034 reinit.");
        }
        else
        {
            break;
        }
    }
    ips200_show_string(0,16,"init success.");
    ips200_clear();
}

2.3 单独初始化摄像头

        直接调用逐飞库函数:

/**
* 函数功能:      初始化总钻风摄像头
* 特殊说明:      当 Cammer_Init_TFT180 和 Cammer_Init_IPS200 都不调用时,默认开启摄像头初始化
* 形  参:        无
* 示例:          Cammer_Init();
* 返回值:        无
*/
void Cammer_Init(void)                  //初始化摄像头和显示屏            *
{
    while(1)
    {
        if(mt9v03x_init())      //摄像头初始化
        {
        }
        else
        {
            break;
        }
    }
}

2.4 屏幕初始化函数

         IPS200与TFT180同理:

void IPS200_Show_Init(void)
{
    ips200_set_color(RGB565_BLACK, RGB565_WHITE);    //设置颜色为彩色
    ips200_set_font(IPS200_6X8_FONT);    //设置字体大小为 6 * 8像素
    ips200_set_dir(IPS200_PORTAIT);    //设置显示方向,图像和字体可以在屏幕上横着或竖着显示
    ips200_init(IPS200_TYPE_SPI);    //选用SPI通信
}

二、图像压缩

        得到摄像头的一帧图像后,复制图像是必须的,这样可以在处理图像时仍可接收图像,即接收图像和处理图像同时进行。

        压缩图像可以大幅缩减后续计算量,同时保留必要的细节,但是压缩不可过小,80 * 60已经很小了,建议不要再小于这个尺寸,当然求取边线的算法处理够快的话,可以直接处理188 * 120的图像,这样可以更好地适应局部反光和留存细节。图像再大没有意义。此处算法无难度,不过多赘述:

/**
* 函数功能:      复制并压缩图像,将 188 * 120 图像压缩为 80 * 60 大小
* 特殊说明:      总钻风使用手册中说明:图像分辨率为  752 * 480, 376 * 240, 188 * 120 这三种分辨率视野是一样的,三者呈整数倍关系
*                其他分辨率是通过裁减得到的(这个裁减包含比188 * 120小的任何分辨率,如 94 * 60),如376 * 240 的视野反而比752 * 400 的视野广
*                此处将总钻风传回图像 188 * 120 压缩为 80 * 60, 所以将 j 乘系数 2.35(188 / 80)
*                经实际测试,当设置图像大小为 94 * 60 时,传回的图像视野是 188 * 120 的四分之一,虽然也和 752 * 480 呈整数倍关系,但和上述情况不同
*
*                注意复制是必须的,这样在处理复制图像时,原图像变量就可以正常接收摄像头数据
* 形  参:        无
* 示例:          Copy_Zip_Image();
* 返回值:        无
*/

void Copy_Zip_Image(void)               //*****
{
    uint8 i,j;
    if(mt9v03x_finish_flag == 1 && Inverse_Flag == 0)       //mt9v03x_finish_flag:逐飞库定义摄像头采集标志位,采集完一帧图像会挂1
                                                            //Inverse_Flag:自定义逆透视标志位,挂1时会得到一张逆透视图像,根据自己需求而定
    {
        for(i = 0; i < Image_Y; i++)
        {
            for(j = 0; j < Image_X; j++)
            {
                Find_Line_Image[i][j] = mt9v03x_image[i * 2][(uint8)(j * 2.35)];    //将188 * 120图像压缩为 80 * 60大小,X轴比为2.35,Y轴比为2
            }
        }
        if(Image_Count_Flag == 1)                 //Image_Count_Flag:自定义开启图像计数标志位,挂1时开启图像计数,即每采集一张图像计数+1;挂0时图像计数清零
        {
            Image_Count ++;
        }
        else if(Image_Count_Flag == 0)
        {
            Image_Count = 0;
        }
//        Image_Num ++;
        mt9v03x_finish_flag = 0;    //注意清掉图像采集完成标志位
    }
    else if(mt9v03x_finish_flag == 1 && Inverse_Flag == 1)
    {
        for(i = 0; i < Image_Y; i++)
        {
            for(j = 0; j < Image_X; j++)
            {
                Find_Line_Image[i][j] = mt9v03x_image[i * 2][(uint8)(j * 2.35f)];
            }
        }
        Get_Inverse_Perspective_Image(Find_Line_Image, I_Perspective_Image);    //逆透视处理函数,在另处详细说明
        if(Image_Count_Flag == 1)
        {
            Image_Count ++;
        }
        else if(Image_Count_Flag == 0)
        {
            Image_Count = 0;
        }
//        Image_Num ++;
        mt9v03x_finish_flag = 0;
    }
}

注:图像大小是工程早期就要确立的,比如确定80 * 60的图像,那么整个比赛期间就不要再更改了,更改整个代码都要大动筋骨。同时,摄像头位对X,Y的坐标值应该很敏感,后期若该变图像大小,会导致写代码时会因固有的印象经常写出bug。

三、总初始化函数

        十分建议写一个总初始化函数,并把所有初始化丢进去,然后用标志位来开关这些初始化。这里给个例子:

uint8 Other_Show_Flag = 0;
uint8 Show_Flag = 0;

void All_Init(uint8 pit_flag, uint8 show_flag, uint8 other_show_flag, uint8 WiFi_send_flag, uint8 LED_screen_flag, uint8 TOF_flag)
{
    Other_Show_Flag = other_show_flag;      //自定义其余信息显示标志位,主要用于测试代码时,要在代码运行内部显示一些参数用于找出异常和错误
                                            //同时又要经常开启或关闭,不想一直注释或删掉,就可以if判断这个标志位是否为1,进而打开或关闭这些显示
    if(pit_flag == 1)       //中断初始化
    {
        pit_ms_init(CCU60_CH0, 10);
    }

    if(show_flag == 1)      //摄像头和图像初始化
    {
//        Cammer_Init();

        Cammer_Init_TFT180();
    }
    else if(show_flag == 2)
    {
//        Cammer_Init();

        Cammer_Init_IPS200();
    }
    else
    {
        Cammer_Init();
    }

    if(WiFi_send_flag == 1)     //WiFi图传初始化
    {
        WiFi_Send_Init();
    }

    if(LED_screen_flag == 1)    //灯光秀初始化
    {
        LED_Screen_Init();
    }

    if(TOF_flag == 1)       //TOF测距模块初始化
    {
        TOF_Init();
    }

//    wireless_uart_init();
}

        而后在主函数中如下调用:

    All_Init( 0,            //是否开启中断标志位            //0:关闭       1:开启
              2,            //是否开启屏幕显示标志位        //0:关闭        1:TFT180显示      2:IPS200显示    (默认开启摄像头初始化)
              0,            //是否开启其余显示标志位        //0:关闭        1:开启
              0,            //WiFi图传初始化标志位         //0:关闭        1:开启
              0,            //LED点阵屏初始化标志位        //0:关闭        1:开启
              0);           //TOF模块初始化标志位          //0: 关闭        1:开启

        这样通过0,1赋值即可开关控制所有初始化,对于后期调试代码,或是比赛时临时更改都非常方便明了。

四、图像补黑框

        对于爬线算法,不论是迷宫还是八邻域,当遇到十字或弯道时,会有一侧或两侧丢线(即图像内没有赛道边线),这个时候爬线算法该怎么处理处理呢?答案是在爬线前补黑框,对于八邻域或常规的迷宫算法,只需在图像的    上、左、右    边缘补一格宽度的黑框即可。

        那么黑框怎么补?将指定的图像行或列原灰度值更改为指定的灰度值(对于二值化图像来说,指定灰度值为0;对于灰度图像来说,为了更好地融入背景环境,灰度值就得通过算法求取了,算法在下方会讲解)。

        但对于上交自适应迷宫,或本人优化后的自适应八向迷宫来说,黑框就不能在图像最边缘补了,而是要间隔一行取补黑框。因为计算阈值是 5 * 5 大小,对于每一次的中心点,上下左右最少得有两格像素宽度。(未使用此算法的直接看代码就可以)

        对于算法可看我之前的文章:智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)

        实际效果如图:(这里用高斯模糊处理了赛道边线,但对黑框无影响,或者应该称之为灰框)

(黑框灰度值经过算法处理,与背景融合度较高,但想来不难分辨,相必看懂这张图像就可以理解如何在最边缘补黑框了)

        如此以后黑框就可牢牢锁住边线。那对于自适应迷宫算法为什么不直接补灰度值为0的黑框呢?

        在小车运行时,会遇到环岛、十字、弯道等导致赛道边线丢失的情况,此时,爬线算法由原先沿着赛道边线爬取转为沿着黑框爬取,那么不可避免的会经过黑框与赛道边线的交接区域。在交接区域,若黑框的灰度值为0,当 5 * 5 区间计算阈值时,由于黑框的原因,会导致阈值被大幅拉低,直接将赛道边线判定为白,意味着交接失败,导致后续爬线紊乱。

        至于为什么补一格宽,而不补两格宽,是为了更好地保留原图像信息,进而使算法更精确。

        上代码,初始第一张图像黑框使用固定的灰度值,后续由算法处理得到:

uint8 Black_Box_Value_FFF = 50;
uint8 Black_Box_Value_FF = 50;
uint8 Black_Box_Value_F = 50;
//画黑框(必须为一个像素宽度,边界务必空出一格)
/**
* 函数功能:      图像补黑框
* 特殊说明:      注意黑框与边界间隔一格像素宽度
* 形  参:        uint8 black_box_value            黑框的灰度值
*                uint8(*image)[Image_X]            要补黑框的图像
*
* 示例:          Draw_Black_Box(Black_Box_Value, Find_Line_Image);;
* 返回值:        无
*/
void Draw_Black_Box(uint8 black_box_value, uint8(*image)[Image_X])          
{
    uint8 i,j;

    Black_Box_Value_FFF = Black_Box_Value_FF;
    Black_Box_Value_FF = Black_Box_Value_F;
    Black_Box_Value_F = black_box_value;
    black_box_value = 0.5 * Black_Box_Value_F + 0.3 * Black_Box_Value_FF + 0.2 * Black_Box_Value_FFF;        //滤波
    Black_Box_Value = black_box_value;
    for(i = 1; i < 60; i++)
    {
        image[i][Image_X - 2] = black_box_value;
        image[i][1] = black_box_value;
    }
    for(j = 1; j < Image_X - 2; j++)
    {
        image[1][j] = black_box_value;
    }
}

五、差比和

        差比和原理是使用两个像素点灰度值(分别设值为 a 和 b),使用式子 (a - b) / (a + b),将比值左移七位(乘128倍放大,移位运算速度比直接乘更快,故不乘100),最后得到的值可以反应两个像素点灰度值的差异。当两者灰度值相差越大,结果便越大。最后将结果与阈值相比较,大于阈值时即可判定出现了灰度值快速变化,这也是爬线算法找起点的关键所在。

/**
* 函数功能:      差比和
* 特殊说明:      用于爬线算法找起点
* 形  参:        int16 a                  数值较大的灰度值
*                int16 b                   数值较小的灰度值
*                uint8 compare_value       差比和阈值
*
* 示例:          Compare_Num(image[start_row][i + 5], image[start_row][i], Compare_Value);
* 返回值:        大于阈值返回1,否则返回0.
*/
int16 Compare_Num(int16 a, int16 b, uint8 compare_value)               //****
{
    if((((a - b) << 7) / (a + b)) > compare_value)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

六、爬线算法找起点

        爬线算法又称生长算法,那么“生长”,就必定有种子,这个种子就是起点。车赛常用的生长算法为八邻域和迷宫巡线。

        算法原理为从选定的一行图像中间开始(如我的图像宽度为80,那么就从 X = 40 开始),先向左,将每个点与其坐标 X + 5 的点进行差比和比较,当大于阈值时,就可判定为找到了左侧赛道边线。对于右侧,向右将每个点与其坐标 X - 5 的点进行差比和处理,当大于阈值时,就可判定为找到了右侧起点。

/**
* 函数功能:      爬线算法找起点
* 特殊说明:      无
* 形  参:        uint8 start_row              找起点的图像行Y坐标
*                uint8(*image)[Image_X]        要处理的图像
*                uint8 *l_start_point          存储左侧起始点的数组(全局变量)
*                uint8 *r_start_point          存储右侧起始点的数组(全局变量)
*                uint8 l_border_x              向左找起点的截止点,最远找到这里就停止
*                uint8 r_border_x              向右找起点的截止点,最远找到这里就停止
*
* 示例:          Get_Start_Point(Image_Y - 3, Find_Line_Image, Adaptive_L_Start_Point, Adaptive_R_Start_Point, 1, 78)
* 返回值:        两边都找到返回1,否则返回0.
*/
uint8 Get_Start_Point(uint8 start_row, uint8(*image)[Image_X], uint8 *l_start_point, uint8 *r_start_point, uint8 l_border_x, uint8 r_border_x)          //*****
{
    uint8 i = 0, j = 0;
    uint8 L_Is_Found = 0, R_Is_Found = 0;   //找到起点时挂出对应标志位
    uint8 Start_X  = 0;                     //起始X坐标,第一张图像取图像的行中点,后续图像用上一次图像左右两侧起始点的中间值
    uint8 Start_Row_0 = 0;                  //起始Y坐标

    Start_Row_0 = start_row;
    Start_X = Image_X / 2;
    //从中间往左边,先找起点
    for(j = 0; j < 10; j ++)        //指定的行没找到起点时,向上走一行继续找,最多找十行
    {
        l_start_point[1] = start_row;//y
        r_start_point[1] = start_row;//y

        if(Start_Flag == 0 || Element_State == Zebra)       //第一张图像和遇到斑马线时,起始X坐标选用图像的行中点
        {
            Start_X = Image_X / 2;
        }
        else
        {
            Start_X = (l_start_point[0] + r_start_point[0]) / 2;    //否则起始X坐标用上一次图像左右两侧起始点的中间值
        }

        {
            for (i = Start_X; i > l_border_x - 1; i--)      //向左找起始点
            {
                if (Compare_Num(image[start_row][i + 5], image[start_row][i], Compare_Value))//差比和为真
                {
                    {
                        l_start_point[0] = i;   //找到后记录X坐标
                        L_Is_Found = 1;         //挂出找见标志位
                        break;
                    }
                }
            }

            for (i = Start_X; i < r_border_x + 1; i++)      //向右找起始点
            {
                if (Compare_Num(image[start_row][i - 5], image[start_row][i], Compare_Value))//差比和为真
                {
                    {
                        r_start_point[0] = i;
                        R_Is_Found = 1;
                        break;
                    }
                }
            }
            if(L_Is_Found && R_Is_Found)
            {
                Start_Flag = 1;    //是否为第一张图像标志位
                return 1;
            }
            else
            {
                start_row = start_row - 1;      //当此行有一侧没找到,就向上移动一行重新找
            }
        }
    }
}

七、求取边线

        前面的文章中讲过,就不过多赘述,这里给到链接:

        智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)

八、二维边线提取一维边线

        爬线算法得到的是二维数组边线,即存储了每个点的横坐标和纵坐标,每行可以有多个点。但一维边线是只存储X坐标,Y坐标来自于图像的行,即每行只可以有一个点。即一维数组的下标为Y坐标,而对应下标的值为X坐标。

        二维边线信息量丰富,但不适宜中线的提取,因此一般会使用算法从二维边线提取出一维边线。同时一维边线还可以用于元素判断和处理的特殊场景,后续文案讲解。

/**
* 函数功能:      由二维边线数组提取一维边线
* 特殊说明:      无
* 形  参:        uint16 l_total       //左侧二维边线点的个数
*                 uint16 r_total      //右侧二维边线点的个数
*                 uint8 start         //起始行(图像底部)
*                 uint8 end           //截止行(图像顶部)
*                 uint8 *l_border     //存储左侧一维边线的数组
*                 uint8 *r_border     //存储右侧一维边线的数组
*                 uint8(*l_line)[2]   //存储左侧二维边线的数组
*                 uint8(*r_line)[2]   //存储右侧二维边线的数组
*
* 示例:          Get_Border(L_Statics, R_Statics, Image_Y - 3, 2, L_Border, R_Border, L_Line, R_Line);
* 返回值:        无
*/
void Get_Border(uint16 l_total, uint16 r_total, uint8 start, uint8 end, uint8 *l_border, uint8 *r_border, uint8(*l_line)[2], uint8(*r_line)[2])
{
    uint8 i = 0;
    uint16 j = 0;
    uint8 h = 0;
    for (i = 0; i < Image_Y; i++)
    {
        l_border[i] = X_Border_Min;
        r_border[i] = X_Border_Max;     //右边线初始化放到最右边,左边线放到最左边,这样闭合区域外的中线就会在中间,不会干扰得到的数据
    }
    h = start;
    //右边
    for (j = 0; j < r_total; j++)
    {
        if (r_line[j][1] == h)
        {
            r_border[h] = r_line[j][0];
        }
        else
        {
            continue;//每行只取一个点,没到下一行就不记录
        }
        h--;
        if (h == end)
        {
            break;//到最后一行退出
        }
    }
    h = start;
    for (j = 0; j < l_total; j++)
    {
        if (l_line[j][1] == h)
        {
            l_border[h] = l_line[j][0];
        }
        else
        {
            continue;//每行只取一个点,没到下一行就不记录
        }
        h--;
        if (h == end)
        {
            break;//到最后一行退出
        }
    }
}

九、阈值处理与图像迭代

        此处处理只针对于自适应(八向)迷宫,未使用此算法的可以略过。

        相必 1 + 1 = 2 都会吧,那就放心往下看。

        在使用算法求取边线时,我们记录了每个中心点的阈值,将些阈值进行特殊处理,可以得到判断斑马线的阈值,下张图像差比和找起点的阈值和补黑框的灰度值。实现真正意义上的图像迭代。(变量名应该已经很明了了)

uint8 Adaptive_L_Thres_Max = 0;     //左侧阈值最大值
uint8 Adaptive_R_Thres_Max = 0;     //右侧阈值最大值
uint8 Adaptive_L_Thres_Min = 0;     //左侧阈值最小值
uint8 Adaptive_R_Thres_Min = 0;     //右侧阈值最小值
uint8 Adaptive_Thres_Average = 0;   //阈值均值
uint8 Last_Adaptive_Thres_Average = 0;  //用于阈值均值滤波
/**
* 函数功能:      提取阈值中的最大最小值,并求出阈值均值
* 特殊说明:      计算时间小于5us
* 形  参:        无
*
* 示例:          Thres_Record_Process();
* 返回值:        无
*/
void Thres_Record_Process(void)
{
    uint8 i = 0;
    uint8 Left_Temp_Value_1 = 0;
    uint32 Left_Temp_Value_2 = 0;
    uint8 Right_Temp_Value_1 = 0;
    uint32 Right_Temp_Value_2 = 0;
    uint8 L_Average_Thres = 0;
    uint8 R_Average_Thres = 0;

    Adaptive_L_Thres_Max = 0;
    Adaptive_R_Thres_Max = 0;
    Adaptive_L_Thres_Min = 0;
    Adaptive_R_Thres_Min = 0;

    Adaptive_L_Thres_Max = L_Thres_Record[0];
    Adaptive_L_Thres_Min = L_Thres_Record[0];
    Adaptive_R_Thres_Max = R_Thres_Record[0];
    Adaptive_R_Thres_Min = R_Thres_Record[0];

    for(i = 0; i < Adaptive_L_Statics; i += 2)      //间隔取值即可,减少计算量
    {
        if(L_Line[i][0] != 2 && L_Line[i][1] != 2)      //舍去位于黑框上的边线点阈值,“2”即左边线位于黑框上时的X坐标
        {
            if(L_Thres_Record[i] < Adaptive_L_Thres_Min)
            {
                Adaptive_L_Thres_Min = L_Thres_Record[i];
            }
            if(L_Thres_Record[i] > Adaptive_L_Thres_Max)
            {
                Adaptive_L_Thres_Max = L_Thres_Record[i];
            }
            Left_Temp_Value_1 ++;
            Left_Temp_Value_2 += L_Thres_Record[i];
        }
    }
    for(i = 0; i < Adaptive_R_Statics; i += 2)
    {
        if(Adaptive_R_Line[i][0] != 77 && Adaptive_R_Line[i][1] != 2)       //与左侧同理
        {
            if(R_Thres_Record[i] < Adaptive_R_Thres_Min)
            {
                Adaptive_R_Thres_Min = R_Thres_Record[i];
            }
            if(R_Thres_Record[i] > Adaptive_R_Thres_Max)
            {
                Adaptive_R_Thres_Max = R_Thres_Record[i];
            }
            Right_Temp_Value_1 ++;
            Right_Temp_Value_2 += R_Thres_Record[i];
        }
    }

    if(Left_Temp_Value_1 == 0)      //当左侧边线全部位于边界上时,直接将阈值均值取0
    {
        L_Average_Thres = 0;
    }
    else        //求出左侧阈值均值
    {
        L_Average_Thres = (uint8)(Left_Temp_Value_2 / Left_Temp_Value_1);
    }

    if(Right_Temp_Value_1 == 0)     //与左侧同理
    {
        R_Average_Thres = 0;
    }
    else
    {
        R_Average_Thres = (uint8)(Right_Temp_Value_2 / Right_Temp_Value_1);
    }

    if(Image_Num <= 1)      //前两张图像直接求均值
    {
        Last_Adaptive_Thres_Average = (uint8)((L_Average_Thres + R_Average_Thres) / 2);
    }
    else
    {
        if(My_ABS_uint8(L_Average_Thres - R_Average_Thres) >= 40)       //当两侧边线的阈值均值相差过大时,进一步处理
        {
            if(My_ABS_uint8(L_Average_Thres - Last_Adaptive_Thres_Average) <= My_ABS_uint8(R_Average_Thres - Last_Adaptive_Thres_Average))      //选取两侧阈值均值最接近上次图像阈值均值的值作为此次的值
            {
                Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + L_Average_Thres) / 2);
                Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
            }
            else
            {
                Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + R_Average_Thres) / 2);
                Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
            }
        }
        else    //当两侧阈值均值相差不大时,直接求三者均值
        {
            Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + L_Average_Thres + R_Average_Thres) / 3);
            Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
        }
    }

    //获得判断斑马线的阈值
    Zbra_Thres = Adaptive_Thres_Average - 10;

    //获得起始点差比和的阈值
    if(Adaptive_Thres_Average >= 100 && Adaptive_Thres_Average <= 140)      //阈值均值落在100 - 140之间,说明图像亮度很合适,此时将差比和阈值设为20即可
    {
        Compare_Value = 20;
    }
    else if(Adaptive_Thres_Average < 100)
    {
        Compare_Value = 20 - (uint8)(((float)(100 - Adaptive_Thres_Average) / 60.0f) * 10.0f);      //当小于100时,适当拉低差比和阈值,使其在图像较暗的情况下更容易找出赛道边线
    }
    else if(Adaptive_Thres_Average > 140)
    {
        Compare_Value = 20 - (uint8)(((float)(Adaptive_Thres_Average - 140) / 60.0f) * 10.0f);      //当大于140时,适当拉低差比和阈值,使其在图像较亮的情况下更容易找出赛道边线
    }

    //求黑框灰度值
    Black_Box_Value = (uint8)(0.45f * (float)sqrt(Adaptive_L_Thres_Min * Adaptive_L_Thres_Min + Adaptive_R_Thres_Min * Adaptive_R_Thres_Min)) + (uint8)(0.1f * (float)Black_Box_Value_1);      
    //0.45f即0.9 * 0.5,0.9为权重,0.5可自己调节。Black_Box_Value_1为两侧爬线起点的灰度值均值(起点值灰度值为图像中赛道黑线的灰度值,不可为黑框灰度值,可以自己写算法处理下)
    //或者Black_Box_Value_1直接丢20 ~ 50之间的值即可
}

        至于差比和阈值、斑马线阈值、黑框灰度值的计算公式是怎么得到的,只能说是随便丢的,但是实际测试效果很好,可根据自己需求去调整。黑框灰度值与赛道蓝布背景的融合度越高越好。

        至此,整个代码和算法处理就真正活了起来,算法可以自己优化参数,以增强其适应性。这也是我比较推荐的代码方式,如何让代码活起来、动起来,是很烧脑,同时也很有趣的一件事。

        至于得到的边线阈值均值,可以直接拿这个参数来二值化图像,是的,你没有看错,就是二值化图像。我之前做过测试,效果还是非常好的,但是没什么必要。因为已经得到了边线。

        同时边线阈值均值打在屏幕上,可以在比赛时,不看图像直接调曝光值,因为阈值均值就反映了图像的亮暗程度。均值阈值在60 ~ 150 之间为宜,范围也是比较宽泛,上场打开摄像头,一看值比较合理,直接上去跑就完事。个人实测效果是非常好的。至于摄像头参数设置,可以参考我这篇文案:智能车摄像头开源—2 摄像头参数设置经验分享

十、综合梳理

这里进行归纳总结,实际使用时代码按以下流程:

  1. 所有设备初始化
  2. 读取图像并压缩复制
  3. 图像补黑框
  4. 差比和找爬线起始点
  5. 自适应八向迷宫爬取二维边线
  6. 二维边线提取一维边线
  7. 阈值处理与迭代参数计算

        至此,摄像头图像基础处理与边线提取(底层)部分就已讲解完毕。建议写一个函数,直接将上述流程丢入函数内,就可一键调用提取出边线信息。

十一、效果展示

展示均为一维边线

1、十字效果展示—正常灯光(有补线处理)

        

2、圆环效果展示—正常灯光(有单边巡线处理)

3、弯道效果展示—正常灯光

4、直线效果展示—正常灯光

5、实验室开灯,遇强光效果展示

手机俯拍

手机位于摄像头视角

算法运行效果

         可以看出近端的强光对算法来说几乎没有难度

6、实验室关灯,环岛遇强光效果展示

手机俯拍

手机位于摄像头视角

算法运行效果

7、实验室关灯,由暗区域过渡到高亮区域效果展示

手机俯拍

手机位于摄像头视角
算法运行效果

        显然有很大难度,左侧出现了部分边线紊乱,但算法依然抗住了。

8、实验室关灯,由高亮区域过渡到暗区域效果展示

手机俯拍

算法运行效果

        可以看出远端出现了部分紊乱,但仍能保证车正常通过。

9、一张抽象的图片

        当时拿起车模偶然保存到的一张图片,可以看到在很恶劣的情况下仍能保证正常运行。所以说算法的上限还是很高的。

        上述处理的都是80 * 60的图像,但显然188 * 120的图像会对光线敏感不均的图像能表现出更好的适应性,如果选取我的算法,还是建议处理188 * 120图像,一张图下来也不会超过1ms。

智能车摄像头开源—1.1 核心算法:自适应八向迷宫(上)

智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)

智能车摄像头开源—2 摄像头基础参数设置经验分享

智能车摄像头开源—3 图像基础处理、迭代优化与效果展示

评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三唐队队长

祝各位宝们车车飞起

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值