目次
一、媒介
二、摄像头采集与屏幕初始化
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 都会吧,那就放心往下看。
在利用算法求取边线时,我们记录了每个中心点的阈值,将些阈值举行特殊处理,可以得到判断斑马线的阈值,下张图像差比和找出发点的阈值和补黑框的灰度值。实现真正意义上的图像迭代。(变量名应该已经很明了了)
至于差比和阈值、斑马线阈值、黑框灰度值的计算公式是怎么得到的,只能说是恣意丢的,但是实际测试结果很好,可根据自己需求去调解。黑框灰度值与赛道蓝布配景的融合度越高越好。
至此,整个代码和算法处理就真正活了起来,算法可以自己优化参数,以增强其顺应性。这也是我比力推荐的代码方式,怎样让代码活起来、动起来,是很烧脑,同时也很风趣的一件事。
至于得到的边线阈值均值,可以直接拿这个参数来二值化图像,是的,你没有看错,就是二值化图像。我之前做过测试,结果照旧非常好的,但是没什么须要。由于已经得到了边线。
同时边线阈值均值打在屏幕上,可以在比赛时,不看图像直接调曝光值,由于阈值均值就反映了图像的亮暗程度。均值阈值在60 ~ 150 之间为宜,范围也是比力宽泛,上场打开摄像头,一看值比力公道,直接上去跑就完事。个人实测结果黑白常好的。至于摄像头参数设置,可以参考我这篇文案:智能车摄像头开源—2 摄像头参数设置经验分享。
十、综合梳理
这里举行归纳总结,实际利用时代码按以下流程:
- 所有设备初始化
- 读取图像并压缩复制
- 图像补黑框
- 差比和找爬线起始点
- 自顺应八向迷宫爬取二维边线
- 二维边线提取一维边线
- 阈值处理与迭代参数计算
至此,摄像头图像基础处理与边线提取(底层)部分就已讲解完毕。建议写一个函数,直接将上述流程丢入函数内,就可一键调用提取出边线信息。
十一、结果展示
展示均为一维边线
1、十字结果展示—正常灯光(有补线处理)
2、圆环结果展示—正常灯光(有单边巡线处理)
3、弯道结果展示—正常灯光
4、直线结果展示—正常灯光
5、实行室开灯,遇强光结果展示
手机俯拍
手机位于摄像头视角
算法运行结果
可以看出近端的强光对算法来说险些没有难度
6、实行室关灯,环岛遇强光结果展示
手机俯拍
手机位于摄像头视角
算法运行结果
7、实行室关灯,由暗地区过渡到高亮地区结果展示
手机俯拍
手机位于摄像头视角 算法运行结果
显然有很大难度,左侧出现了部分边线紊乱,但算法依然抗住了。
8、实行室关灯,由高亮地区过渡到暗地区结果展示
手机俯拍
算法运行结果
可以看出远端出现了部分紊乱,但仍能保证车正常通过。
9、一张抽象的图片
当时拿起车模偶然保存到的一张图片,可以看到在很恶劣的情况下仍能保证正常运行。所以说算法的上限照旧很高的。
上述处理的都是80 * 60的图像,但显然188 * 120的图像会对光线敏感不均的图像能表现出更好的顺应性,假如选取我的算法,照旧建议处理188 * 120图像,一张图下来也不会凌驾1ms。
智能车摄像头开源—1.1 核默算法:自顺应八向迷宫(上)
智能车摄像头开源—1.2 核默算法:自顺应八向迷宫(下)
智能车摄像头开源—2 摄像头基础参数设置经验分享
智能车摄像头开源—3 图像基础处理、迭代优化与结果展示
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |