摄像头搜线算法就是获取赛道正确左右边界的一种方法。有传统的从中间往两边搜,生长算法等(4邻域,8邻域等)。
我使用的搜线算法都是在传统的搜线算法上改进而来的,总体来说就是,针对遇到的问题进行修改。目前是一个比较稳定的状态。但在调车的时候也遇到了一些问题,是传统搜线算法解决不了的。所以我还是提倡大家使用生长算法(上交大就开源过一份程序)。
这里我先用车处于赛道正中间的图像举例说明搜线算法的全过程:
1.确认每一行左右搜线的起始点(先确认最下面一行的搜线左右起始点,倒数第二行的起始点会因为最后一行是否搜到线而影响)
2.从起始点分别先两边搜,记录下是否搜到左右边线,并记录下左右变线的位置。
3.更新左右搜线起始点的位置,进行下一行的搜线,重复步骤2. 直至满足搜线终止条件。
先上图像:
第一行搜线起始点为:MT9V03X_W/2 (MT9V03X_W为每一行像素点的个数,x最大值为MT9V03X_W -1)
l_search_start = r_search_start = MT9V03X_W/2;
分别向左和向右搜索赛道边界
搜到最后一行(MT9V03X_H - 1)的边界以后,(MT9V03X_H -1即为行的最大值),开始确定MT9V03X_H - 2行的搜线左右起始点。
针对上图这样的图像,我们当然可以继续使用MT9V03X_W/2 作为左右搜线起始点。但我们要意识到,并不是每一次摄像头都可以采集到如此理想的图像。
如果有赛道左右边界都出现在图像的的中间偏右,或者偏左的话就搜索不到一侧的边界了。
这里拿两侧边界出现都出现在图像中间偏右为例:
(判断左边界的条件是出现黑黑白三个连续像素点,判断右边界的条件是出现白黑黑三个连续的像素点。)
这幅图像中,左边界在上面的一段未搜索到,使用了错误的边界作为赛道左边界。
要解决这个问题也很简单,让我们的赛道左右搜线起始点能够跟随赛道的边界变化就可以了。
l_search_start[MT9V03X_H -2] = r_search_start[MT9V03X_H -2] = (l_line_x[MT9V03X_H -1] + r_line_x[MT9V03X_H -1])/2;
//l_line_x[MT9V03X_H - 1] 为最后一行的左边界
//r_line_x[MT9V03X_H - 1] 为最后一行的右边界
//l_search_start[MT9V03X_H - 2] 为倒数第二行的左搜线起始点
//r_search_start[MT9V03X_H - 2] 为倒数第二行的右搜线起始点
由于搜线起始点的位置会随赛道的变化趋势而变化,这样就可以搜索到正确的左边界了(黄线为搜线起始点)
弯道搜线示意大概是这样的:
有了以上的知识,摄像头小车就可以在基础的直道和弯道进行搜线了。
使用上面的方案确实可以准确的搜到左右边线,但在搜索每一行的左右边线的时候都要做很多的无用功。
比如,我们都知道赛道的左右边界不可能出现在黑色阴影区域,但我们每一次搜线都要查找一次这片区域,从而浪费了很大的算力。
有没有方法避免这种不必要的浪费呢! 答案是肯定的,思路跟图像压缩求阈值是一样的。
图像压缩利用灰度值不突变的思想。搜索边界也可以利用左右边界不突变的思想。
我们每一行搜线的左边界在上一行确定的左边界的基础上往右移动一定的像素点,从这里开始往左搜索左边界,这样就可以达到节省算力的目的。而且这样我们就不用考虑左右边界都出现在图像的中间偏一侧,而找不到边界的问题。
l_search_start[y -1] = l_line_x[y] + 5;
r_search_start[y -1] = r_line_x[y] - 5;
//此为伪代码,仅起到说明作用
//偏移像素点的数量为5
搜线的演示大概如下图:
由此可见,使用这个思路,可以有效的节省算力。
以上,都没有涉及到特殊元素,涉及到特殊元素的时候,我们针对元素进行一些处理。
在上图这种情况下,如果我们采样以前的方法,搜索右边界的话,会出现上面有一段区域是找不到正确的右边界的。
解决的方法也很简单,就是当找不到右边界的时候,把搜线右起始点回中。
if((l_line_x[y+1] != 0 && r_line_x[y+1] == MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //左不丢线,右丢线
{
l_search_start[y-1] = l_line_x[y] + 5;
r_search_start[y-1] = MT9V03X_W/2; //回中
}
//此为伪代码,仅作说明使用
//MT9V03X_W 为每一行像素点的值,在数组里面的范围就是0-MT9V03X_W -1,MT9V03X_W -1为横向最大值
效果如图:
左边界出现丢失又出现也一样处理,要是左右边线都出现丢失又出现,就左右搜线起始点都回中。
效果如图:
这里还要提到的一点是,在搜索在下面一行的时候,搜线起始点是要自己赋值的,为了保险起见,我在搜索前四行的时候,都取左右起始点为MT9V03X_W/2。
确认搜线起始点的代码如下:
for(uint8 y = MT9V03X_H - 1; y > search_line_end; y--) //确定每行的搜线起始横坐标
{
if( (y > MT9V03X_H - 5) || ( (l_line_x[y+1] == 0) && (r_line_x[y+1] == MT9V03X_W -1) && (y < MT9V03X_H - 4) )) //前四行,或者左右都丢线的行
{
l_search_start = r_search_start = MT9V03X_W/2;
}
else if((l_line_x[y+1] != 0) && (r_line_x[y+1] != MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //左右都不丢线
{
l_search_start = l_line_x[y+1] + 5;
r_search_start = r_line_x[y+1] - 5;
}
else if((l_line_x[y+1] != 0 && r_line_x[y+1] == MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //左不丢线,右丢线
{
l_search_start = l_line_x[y+1] + 5;
r_search_start = MT9V03X_W/2;
}
else if((l_line_x[y+1] == 0 && r_line_x[y+1] != MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //右不丢线,左丢线
{
l_search_start = MT9V03X_W/2;
r_search_start = r_line_x[y+1] - 5;
}
如果看不惯黑色风格,这里贴一张白色主题的图片。
当然,有时候也要考虑到一下特殊情况要提前终止搜线。比如,左边线出现在了右边线的右边,(当然我们这种搜线方案是不会出现的)我们就没有必要再搜索下去了,因为后面搜索到的边界已经没有意义了。因为我没有使用代码来提前终止搜线,所以我也没有什么很好的思路。
既然已经确认了搜线起始点,后面就直接遍历这个二维数组,找到边界就可以了。
有了前面的基础,那我们直接上代码:
for(uint8 y = MT9V03X_H - 1; y > search_line_end; y--)
{
//左边搜线
for(l_width = l_search_start; l_width > 1; l_width--)
{
if(image_01[y][l_width -2] == 0 && image_01[y][l_width - 1] == 0 && image_01[y][l_width] != 0 && l_width > 2)
{//黑黑白
l_line_x[y] = l_width - 1;
l_line_x_yuanshi[y] = l_width - 1;
l_search_flag[y] = 1;
break;
}
else if(l_width == 2)
{
l_line_x[y] = 0;
l_line_x_yuanshi[y] = 0;
l_search_flag[y] = 0;
l_lose_value++;
break;
}
}
//右边搜线
for(r_width = r_search_start; r_width < (MT9V03X_W - 2); r_width++)
{
if(image_01[y][r_width] != 0 && image_01[y][r_width +1] == 0 && image_01[y][r_width +2] == 0 && r_width < MT9V03X_W - 3)
{//白黑黑
r_line_x[y] = r_width + 1;
r_line_x_yuanshi[y] = r_width + 1;
r_search_flag[y] = 1;
break;
}
else if(r_width == MT9V03X_W - 3)
{
r_line_x[y] = MT9V03X_W - 1;
r_line_x_yuanshi[y] = MT9V03X_W - 1;
r_search_flag[y] = 0;
r_lose_value++;
break;
}
}
}
}
完整搜线代码:
//---------------------------------------------------------------------------------------
// @param void
// @return void
// @date 2022.10.3
// @author xiaodong
// Sample usage: Search_Line();
//---------------------------------------------------------------------------------------
int16 l_line_x[MT9V03X_H], r_line_x[MT9V03X_H], m_line_x[MT9V03X_H]; //储存原始图像的左右边界的列数
uint8 l_lose_value = 0, r_lose_value = 0; //左右丢线数
uint8 l_search_flag[MT9V03X_H], r_search_flag[MT9V03X_H]; //是否搜到线的标志
uint8 l_width, r_width; //循环变量名
uint8 l_search_start, r_search_start; //搜线起始x坐标
uint8 l_line_x_yuanshi[MT9V03X_H], r_line_x_yuanshi[MT9V03X_H];
void Search_Line()
{
uint8 l_flag=0, r_flag=0;
r_lose_value = l_lose_value = 0; //搜线之前将丢线数清零
for(uint8 y = MT9V03X_H - 1; y > search_line_end; y--) //确定每行的搜线起始横坐标
{
if( (y > MT9V03X_H - 5) || ( (l_line_x[y+1] == 0) && (r_line_x[y+1] == MT9V03X_W -1) && (y < MT9V03X_H - 4) )) //前四行,或者左右都丢线的行
{
l_search_start = r_search_start = MT9V03X_W/2;
}
else if((l_line_x[y+1] != 0) && (r_line_x[y+1] != MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //左右都不丢线
{
l_search_start = l_line_x[y+1] + 5;
r_search_start = r_line_x[y+1] - 5;
}
else if((l_line_x[y+1] != 0 && r_line_x[y+1] == MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //左不丢线,右丢线
{
l_search_start = l_line_x[y+1] + 5;
r_search_start = MT9V03X_W/2;
}
else if((l_line_x[y+1] == 0 && r_line_x[y+1] != MT9V03X_W - 1) && (y < MT9V03X_H - 4)) //右不丢线,左丢线
{
l_search_start = MT9V03X_W/2;
r_search_start = r_line_x[y+1] - 5;
}
for(l_width = l_search_start; l_width > 1; l_width--) //左边搜线
{
if(image_01[y][l_width -2] == 0 && image_01[y][l_width - 1] == 0 && image_01[y][l_width] != 0 && l_width > 2)
{//黑黑白
l_line_x[y] = l_width - 1;
l_line_x_yuanshi[y] = l_width - 1;
l_search_flag[y] = 1;
break;
}
else if(l_width == 2)
{
if(l_flag==0)
{
l_flag = 1;
l_losemax = y +1;
}
l_line_x[y] = 0;
l_line_x_yuanshi[y] = 0;
l_search_flag[y] = 0;
l_lose_value++;
break;
}
}
for(r_width = r_search_start; r_width < (MT9V03X_W - 2); r_width++) //右边搜线
{
if(image_01[y][r_width] != 0 && image_01[y][r_width +1] == 0 && image_01[y][r_width +2] == 0 && r_width < MT9V03X_W - 3)
{//白黑黑
r_line_x[y] = r_width + 1;
r_line_x_yuanshi[y] = r_width + 1;
r_search_flag[y] = 1;
break;
}
else if(r_width == MT9V03X_W - 3)
{
if(r_flag==0)
{
r_flag = 1;
r_losemax = y + 1;
}
r_line_x[y] = MT9V03X_W - 1;
r_line_x_yuanshi[y] = MT9V03X_W - 1;
r_search_flag[y] = 0;
r_lose_value++;
break;
}
}
}
}
到这里我们已经搜索到了正确的左右边界,但我们并不知道,我们所得到边界是否正确。如果你只是想到左右边界是否找到了,你可以直接给一系列的像素点直接赋一个灰度值。这样就可以知道是否搜到线了。但这种方法很难区分左右边界。
附上我第一次搜到线的图片:
所以我推荐使用液晶画点函数来显示彩色的边界。但这个函数很吃算力,有些单片机可能跑不动,而且也存在溢出的风险。你的屏幕如果能显示的像素点比你图像的像素少,那么也要进行处理才能够正确显示。(特别是STC单片机,能用的IO口较少,用不了比较好的屏幕,要么你就牺牲图像的像素值,要么你就自己研究显示的代码)
这里附上显示赛道边界的代码:
//---------------------------------------------------------------------------------------
// @param void
// @return void
// @date 2022.10.3
// @author xiaodong
// Sample usage: Blacking();
//---------------------------------------------------------------------------------------
void Blacking()
{
//画左右边界
for(uint8 y=(MT9V03X_H-1); y>search_line_end; y--)
{
ips200_drawpoint(l_line_x[y]+2, y, RGB565_GREEN);
ips200_drawpoint(l_line_x[y]+3, y, RGB565_GREEN);
ips200_drawpoint(r_line_x[y]-2, y, RGB565_PURPLE);
ips200_drawpoint(r_line_x[y]-3, y, RGB565_PURPLE);
ips200_drawpoint( (l_line_x_yuanshi[y] + r_line_x_yuanshi[y])/2, y, RGB565_BLACK);
}
//画屏幕中线
for(uint8 i =0; i<MT9V03X_W; i++)
{
ips200_drawpoint(i, MT9V03X_H/2, RGB565_RED);
}
for(uint8 i=0; i<MT9V03X_H; i++)
{
ips200_drawpoint(MT9V03X_W/2, i, RGB565_RED);
}
}
至此,搜线就搞一段落了。在加上一些基本的小知识,小车就可以在普通的赛道上面跑起来了。