0


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

一、前言

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

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

   ** 本文处理的图像大小为 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;
        }
    }
}

二、图像压缩

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

    压缩图像可以大幅缩减后续计算量,同时保留必要的细节,但是压缩不可过小,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坐标来自于图像的行,即每行只可以有一个点。

    二维边线信息量丰富,但不适宜中线的提取,因此一般会使用算法从二维边线提取出一维边线。同时一维边线还可以用于元素判断和处理的特殊场景,后续文案讲解。
/**
* 函数功能:      由二维边线数组提取一维边线
* 特殊说明:      无
* 形  参:        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、十字效果展示—正常灯光(有补线处理)

    ![](https://i-blog.csdnimg.cn/direct/4af6846e2dc24c8fb4a90062bfb32b74.jpeg)

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

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

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

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


手机俯拍

手机位于摄像头视角

算法运行效果

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

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


手机俯拍

手机位于摄像头视角

算法运行效果

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


手机俯拍

手机位于摄像头视角

算法运行效果

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

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


手机俯拍

算法运行效果

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

9、一张抽象的图片

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

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

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

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

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

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


本文转载自: https://blog.csdn.net/lh66969696/article/details/142988183
版权归原作者 三唐队队长 所有, 如有侵权,请联系我们删除。

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

还没有评论