一、前言
本文主要讲解一些基本的处理,如图像压缩、起点找寻、阈值处理等。同时展示自适应八向迷宫的运行效果。
同时声明本文内容皆由作者实践得出,并不保证绝对正确,仅供入门者学习和参考
** 本文处理的图像大小为 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、十字效果展示—正常灯光(有补线处理)
![](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 图像基础处理、迭代优化与效果展示
版权归原作者 三唐队队长 所有, 如有侵权,请联系我们删除。