前言:之前写的直接找中线的循迹方式过于简单粗暴,在面对环岛,三岔路,等复杂路段时就无法处理了。所以我们要使用提取赛道边界的算法来使得我们面对复杂路况也能够处理。
一、八邻域原理
八邻域其实指的是一个像素点它周围的八个点,我们可以根据一个像素点周围的八个点出现的情况来判断这个点到底是不是边界点。这里的八个点出现的情况我们可以用很多if来进行每种情况的匹配,也可以通过for函数的一行行遍历查找可能的边界点。这两种方法我们下面会通过两段代码分别介绍。
二、八邻域代码讲解
(1)首先,八邻域需要一个爬取赛道边界的起点,这个起点位于视野的最下方,左右边界赛道各一个。
这个找赛道边界的八邻域算法是在原始灰度图像被二值化之后的图像数组里面运行的。所以二值化阈值一定要设置好,边界提取效果不好。
#define WHITE_IMG 255
#define BLACK_IMG 0
#define ROW 120
#define COL 188
struct DIV
{
int Row[240];
int Col[240];//边线行列坐标的存储数组
};
struct DIV left,right;//左右边线
int right_flag = 0; //右起点存在标志
int left_flag = 0;
int right_start = 0; //右边线起点行
int left_start = 0;
//其中的IMG_DATA是被二值化处理过后的一个二维图像数组(根据自己的代码修改)
void LeftStartFind()//寻找左起点并存起来
{
int row,col;
for(row=ROW-5;row>40 && left.Row[0]==254;row--)
{
for(col = 5;col<COL - 55 && left.Col[0] == 254;col++)//在屏幕底部的那一块里面寻找起点
{
if(IMG_DATA[row][col]==BLACK_IMG && IMG_DATA[row][col+1]==BLACK_IMG)
{
if(IMG_DATA[row][col+2]==WHITE_IMG && IMG_DATA[row][col+3]==WHITE_IMG)
{
left.Row[0]=row;
left.Col[0]=col+1;
left_flag=1;
}
}
}
}
left_start=left.Row[0];
}
void RightStartFind()//同理,右起点
{
int row,col;
for(row=ROW-5;row > 40 && right.Row[0] == 254;row--)
{
for(col=COL - 5;col > 55 && right.Col[0] == 254;col--)
{
if(IMG_DATA[row][col]==BLACK_IMG && IMG_DATA[row][col-1]==BLACK_IMG)
{
if(IMG_DATA[row][col-2]==WHITE_IMG && IMG_DATA[row][col-3]==WHITE_IMG)
{
right.Row[0]=row;
right.Col[0]=col-1;
right_flag=1;
}
}
}
}
right_start=right.Row[0];
}
其中判别是否是赛道边界的起点的条件是,拿左起点举例,如果有一个点的左边是黑色,自己本身是黑色,它的右边两个点都是白色。那么我们可以初步判断它是一个边界点。然后再根据这个边界点进行边界的爬取。
(2)八邻域边界爬取
正如我上面提到,边界爬取有多种方式,例如if条件匹配(八邻域法)、逐行遍历判别、生长种子法,迷宫法等。这边我介绍前两种较为容易理解的。
1、if条件匹配(八邻域)
拿左边界举例,一个点如果是边界,那么它的周围无非就是七种情况,画的很抽象。
大概就是这么一个意思,然后我们看代码。
这个代码与上面介绍的找起点有点冲突,这个代码自带起点了其实,上面的找起点是适配逐行遍历判别法的
//-------------------------------------------------------------------------------------------------------------------
// @brief 给图像画黑框为八邻域做准备,目的是为了让车通过十字路口的时候不会丢失边界
// @return void
// @since v1.0
// Sample usage: image_draw_rectan(Image_use);
//-------------------------------------------------------------------------------------------------------------------
void image_draw_rectan(uint8(*image)[IMAGE_W])
{
uint8 i = 0;
for (i = 0; i < IMAGE_H; i++)
{
image[i][0] = 0;
image[i][1] = 0;
image[i][IMAGE_W - 1] = 0;
image[i][IMAGE_W - 2] = 0;
}
for (i = 0; i < IMAGE_W; i++)
{
image[0][i] = 0;
image[1][i] = 0;
}
}
/*---------------------------------------------------------------
【函 数】search_neighborhood
【功 能】八邻域找边界
【参 数】无
【返 回 值】无
【注意事项】
----------------------------------------------------------------*/
//struct LEFT_EDGE
//{
// int16 row; //行坐标
// int16 col; //列坐标
// uint8 flag; //存在边界的标志
//};
//struct RIGHT_EDGE
//{
// int16 row; //行坐标
// int16 col; //列坐标
// uint8 flag; //存在边界的标志
//};
struct LEFT_EDGE L_edge[140]; //左边界结构体
struct RIGHT_EDGE R_edge[140]; //右边界结构体
uint8 L_edge_count=0, R_edge_count = 0; //左右边点的个数
uint8 dire_left,dire_right; //记录上一个点的相对位置
uint8 L_search_amount = 140, R_search_amount = 140; //左右边界搜点时最多允许的点
int L_start_y=0 ;
int L_start_x=0 ;
int left_findflag=0 ;
int R_start_y=0 ;
int R_start_x=0 ;
int right_findflag=0 ;
int left_lose;
int right_lose;
void search_neighborhood(void)
{
L_edge_count = 0;//左边点个数清0
R_edge_count = 0;//右边点个数清0
for (int l=120;l>118;l--){
for (int j=MT9V03X_W/2;j>0;j--){
if(image_use[l][j]==0){ L_start_y=l , L_start_x=j , left_findflag=1 ;
// else {imagic_W_left[l]=0;}
break;}
}
for (int j=MT9V03X_W/2;j<MT9V03X_W;j++){
if(image_use[l][j]==0){ R_start_y=l , R_start_x=j , right_findflag=1 ;
// else {imagic_W_left[l]=160;}
break;}
}
}
if(left_findflag)//如果左边界点存在并找到,则开始爬线
{
//变量声明
L_edge[0].row = L_start_y;
L_edge[0].col = L_start_x;
L_edge[0].flag = 1;
int16 curr_row = L_start_y;//初始化行坐标
int16 curr_col = L_start_x;//初始化列坐标
dire_left = 0; //初始化上个边界点的来向
//开始搜线,最多取150个点,不会往下搜,共7个方位
for(int i = 1;i < L_search_amount; i++) //最多搜索150个点
{
越界退出 行越界和列越界(向上向下向左向右)
if(curr_row+1 < 1 || curr_row>IMAGE_H-1)
break;
//搜线过程
if(dire_left != 2&&image_use[curr_row-1][curr_col-1]==BLACK&&image_use[curr_row-1][curr_col]==WHITE) //左上黑,2,右边白
{
curr_row = curr_row -1;
curr_col = curr_col -1;
L_edge_count = L_edge_count +1;
dire_left = 7;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else if(dire_left!=3&&image_use[curr_row-1][curr_col+1]==BLACK&&image_use[curr_row][curr_col+1]==WHITE) //右上黑,3,下边白
{
curr_row = curr_row -1;
curr_col = curr_col + 1;
L_edge_count = L_edge_count + 1;
dire_left = 6;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else if(image_use[curr_row-1][curr_col]==BLACK&&image_use[curr_row-1][curr_col+1]==WHITE) //正上黑,1,右白
{
curr_row = curr_row - 1;
L_edge_count = L_edge_count + 1;
dire_left = 0;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else if(dire_left!=5&&image_use[curr_row][curr_col-1]==BLACK&&image_use[curr_row-1][curr_col-1]==WHITE) //正左黑,5,上白
{
curr_col = curr_col - 1;
L_edge_count = L_edge_count +1;
dire_left = 4;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
left_lose=L_edge[i].col; //拐点出现点,开始检测标志
}
else if(dire_left!=4&&image_use[curr_row][curr_col+1]==BLACK&&image_use[curr_row+1][curr_col+1]==WHITE) //正右黑,4,下白
{
curr_col = curr_col + 1;
L_edge_count = L_edge_count +1;
dire_left = 5;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else if(dire_left!=6&&image_use[curr_row+1][curr_col-1]==BLACK&&image_use[curr_row][curr_col-1]==WHITE) //左下黑,6,上白
{
curr_row = curr_row + 1;
curr_col = curr_col -1;
L_edge_count = L_edge_count +1;
dire_left = 3;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else if(dire_left!=7&&image_use[curr_row+1][curr_col+1]==BLACK&&image_use[curr_row+1][curr_col]==WHITE) //右下黑,7,左白
{
curr_row = curr_row + 1;
curr_col = curr_col + 1;
L_edge_count = L_edge_count +1;
dire_left = 2;
L_edge[i].row = curr_row;
L_edge[i].col = curr_col;
L_edge[i].flag = 1;
}
else
break;
}
}
if(right_findflag)//如果右边界存在并搜到
{
R_edge[0].row = R_start_y;
R_edge[0].col = R_start_x;
R_edge[0].flag = 1;
int16 curr_row = R_start_y;
int16 curr_col = R_start_x;
dire_right = 0;
for(int i = 1;i<R_search_amount;i++)
{
越界退出 行越界和列越界(向上向下向左向右)
if(curr_row < 1 || curr_row>IMAGE_H-1||curr_row+1<1)
break;
//爬线过程
if(curr_col<IMAGE_W&&dire_right!=3&&image_use[curr_row-1][curr_col+1]==BLACK&&image_use[curr_row-1][curr_col]==WHITE) //右上黑,3,左白
{
curr_row = curr_row - 1;
curr_col = curr_col + 1;
R_edge_count = R_edge_count + 1;
dire_right = 6;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else if(dire_right!=2&&image_use[curr_row-1][curr_col-1]==BLACK&&image_use[curr_row][curr_col-1]==WHITE) //左上黑,2,下白
{
curr_row = curr_row-1;
curr_col = curr_col-1;
R_edge_count = R_edge_count + 1;
dire_right = 7;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else if(image_use[curr_row-1][curr_col]==BLACK&&image_use[curr_row-1][curr_col-1]==WHITE) //正上黑,1,左白
{
curr_row = curr_row - 1;
R_edge_count = R_edge_count + 1;
dire_right = 0;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else if(dire_right!=4&&image_use[curr_row][curr_col+1]==BLACK&&image_use[curr_row-1][curr_col+1]==WHITE) //正右黑,4,上白
{
curr_col = curr_col + 1;
R_edge_count = R_edge_count + 1;
dire_right = 5;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
// right_lose = R_edge[i].col ; //拐点出现点,开始检测标志
}
else if(dire_right!=5&&image_use[curr_row][curr_col-1]==BLACK&&image_use[curr_row+1][curr_col-1]==WHITE) //正左黑,5,下白
{
curr_col = curr_col-1;
R_edge_count = R_edge_count + 1;
dire_right = 4;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else if(dire_right!=6&&image_use[curr_row+1][curr_col-1]==BLACK&&image_use[curr_row+1][curr_col]==WHITE) //左下黑,6,右白
{
curr_row = curr_row + 1;
curr_col = curr_col - 1;
R_edge_count = R_edge_count + 1;
dire_right = 3;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else if(dire_right!=7&&image_use[curr_row+1][curr_col+1]==BLACK&&image_use[curr_row][curr_col+1]==WHITE) //右下黑,7,上白
{
curr_row = curr_row + 1;
curr_col = curr_col + 1;
R_edge_count = R_edge_count + 1;
dire_right = 2;
R_edge[i].row = curr_row;
R_edge[i].col = curr_col;
R_edge[i].flag = 1;
}
else
break;
}
}
}
2、逐行遍历判别
这个方法的好处就是非常的简单易懂。原理就先不介绍,直接上代码就能看懂
这个是接在上面的找起点函数后面调用的,然后这边的定义什么的和上面的那个一样
void left_jump()//八邻域爬赛道边界
{
int pin;// i, j;
int row, col,find;
int colmin, colmax;
if(left.Row[0] == 254)
{
return;
}
left_flag=1;
pin=1;
for(row=left.Row[0] - pin;pin<240;pin++)
{
find = 0;
row = left.Row[0] - pin;
if(row<=row_lim)break;
colmin = left.Col[pin - 1] - 10;
colmax = left.Col[pin - 1] + 10;
for(col = colmin;col <= colmax;col++)
{
if(col<5) col=5;
if(col < COL - 5)
{
if(IMG_DATA[row][col] == BLACK_IMG && IMG_DATA[row][col + 1] == BLACK_IMG)
{
if(IMG_DATA[row][col + 2] == WHITE_IMG && IMG_DATA[row][col + 3] == WHITE_IMG)
{
left.Row[pin] = row;
left.Col[pin] = col + 1;
IMG_DATA[row][col + 1] = RED_IMG;
find = 1;
L_lenth++;
left_end_row=row;
left_end_col=col;
break;
}
}
}
}
if(find == 0)
{
break;
}
}
if(L_lenth<=5)
{
left_flag=0;
left.Row[0]=254;
left.Col[0]=254;
}
}
void right_jump()
{
int pin;//, i, j;
int row, col,find;
// int row_temp, col_temp;
// int whitecounter = 0;
int colmin, colmax;
// int rowcheack[12] = {-1, 1, -1, 0, 2, 0, 0, -1, -1, 1, 0, 1};
// int colcheack[12] = {-1, 0, 1, 1, -2, 1, 1, 0, 0, -1, 2, -1};
if(right.Row[0] == 254)
{
return;
}
right_flag=1;
for(pin=1,row=right.Row[0] - pin;pin<240;pin++)
{
find = 0;
row = right.Row[0] - pin;
if(row<=row_lim)break;
colmin = right.Col[pin - 1] - 10;
colmax = right.Col[pin - 1] + 10;
for(col = colmax;col >= colmin;col--)
{
if(col > COL - 5) col=COL - 5;
if(col > 5)
{
if(IMG_DATA[row][col] == BLACK_IMG && IMG_DATA[row][col - 1] == BLACK_IMG)
{
if(IMG_DATA[row][col - 2] == WHITE_IMG && IMG_DATA[row][col - 3] == WHITE_IMG)
{
right.Row[pin] = row;
right.Col[pin] = col - 1;
IMG_DATA[row][col - 1] = BLUE_IMG;
find = 1;
R_lenth++;
right_end_row=row;
right_end_col=col;
break;
}
}
}
}
if(find == 0)
{
break;
}
}
if(R_lenth<=5)
{
right_flag=0;
right.Row[0]=254;
right.Col[0]=254;
}
}
当你看完这个代码其实就明白了,其实它找边界的方法就是每一行遍历,然后判别是不是边界的方法也很简单,就是看当前这一行,比如左边界就是看当前点是黑色并且左边点是黑色,然后右边的两个点是白色。那么当前黑点就判别为左边界。
结语:提取赛道边界的方法有很多,我这边只介绍了最经典的八邻域法,和我自己写的遍历判别法,当然方法之间各有好坏,各位根据实际情况自己选择。