上一篇文章最后阐述到哪儿了?嗯,阐述到了把灰度图像处理成黑白图像,以便提取赛道特征。
好,接着上一篇继续往下说。
爬梯
黑白图像有了,那下一步是什么呢?嗯,对,提取赛道的边界线。提取赛道的边界线是为了干嘛呢?得到赛道的中线,然后计算其偏差,看所得到的偏差与直线相较,它这会儿是要前行还是要转向。
那么,要开始了。既然要提取边线,那边线代表的数据存哪儿就是一个问题了。所以,定义数组,存储数据。
//左边界
int8 ui8_LPoint[ROW];
//右边界
int8 ui8_RPoint[ROW];
左右各定义一串一维数组来存储数据。
除此之外,定义一个结构体。结构体有啥用啊?咱也不清楚,感觉用起来跟一般的变量区别不大。但是当定义的变量越来越多之后,自己写代码写着写着,或多或少也容易写懵。在我的理解里,把需要使用到的变量一块儿一块儿分类,然后封装到结构体中,需要用的时候就取出来。
typedef struct {
//图像X坐标
uint8 ui8_ImageX;
//图像Y坐标
uint8 ui8_ImageY;
//图像单边最远点
uint8 ui8_MaxY;
//图像最远点
uint8 ui8_AllMaxY;
} LadderMovePoint;
typedef struct {
//状态判断计数
uint16 ui16_counter[5];
//状态标志
int8 i8_StatusFlag[9];
//状态处理变量
int8 i8_StatusHandle[9];
//图像处理范围
uint8 ui8_DisposeScopeUp;
uint8 ui8_DisposeScopeDown;
uint8 ui8_DisposeScopeLeft;
uint8 ui8_DisposeScopeRight;
//直道行位置
float f_BaseY[10];
//标准数组
uint8* ui8_LineWidth;
//标准权重
float f_BaseLineWeight[10];
//行权重
float f_LineWeight[10];
//控制量数组
int16* i16p_dataImage;
//图像数组
uint8 ui8_ImageArray[ROW][COL];
//左边界
int8 ui8_LPoint[ROW];
//右边界
int8 ui8_RPoint[ROW];
//扫描行距离
uint8 ui8_ScanLineY[10];
//扫描行左边界(补线)
uint8 ui8_ScanLineL[10];
//扫描行右边界(补线)
uint8 ui8_ScanLineR[10];
//扫描行左边界(最边界)
uint8 ui8_ScanLineToL[10];
//扫描行右边界(最边界)
uint8 ui8_ScanLineToR[10];
//扫描赛道宽度
uint8 ui8_ScanLineWidth[10];
//中值求取起点
uint8 ui8_ScanDirection;
//初始中值
int16 i16_Mid[10];
//最终中值
int16 i16_FinallyMid[10];
//最小可视距离
uint8 ui8_MinH;
int8 i8_MinHX;
//反向可视距离
uint8 VisitableScope;
//爬梯最远点
uint8 MaxPoint;
} Dispose_Image;
爬梯它自然是一层一层来的,首先肯定是从最底层开始,底层一般来说是黑白黑的一条线。当然,如果你使用的传感器过于高级,90度的那种当我没说,摆在直道上底下这条线都是全白的,也不是不可能。90度的摄像头我也用过,不得不说确实高级,写了好几行防止左右边线丢失的代码,然后使它一丢线就左摇右晃,让它尽快地摸到那条边线。但建议嘛,不是没啥变法不要用那种直戳戳90度的摄像头,咱是觉得不好使。130和150都挺不错的。
遍历黑白图像二维数组最底下的那条线,无论从右到左还是从左到右,找出黑白分割的那个像素点。然后确定左右边界线的第一个点,随后再以这个点向上遍历一层一层爬梯。不过不用爬太高,因为远了边线估摸着就不正常了。可能会黑白黑白黑这样来。
代码可参考:
uint8 LeftPointLadder (uint8* ui8p_LF) {
if (!DI.ui8_ImageArray[L_Move.ui8_ImageY][L_Move.ui8_ImageX]) //黑点进入白区
{
while (L_Move.ui8_ImageX < DI.ui8_DisposeScopeRight
&& !DI.ui8_ImageArray[L_Move.ui8_ImageY][L_Move.ui8_ImageX + 1])
{
L_Move.ui8_ImageX ++;
}
if (L_Move.ui8_ImageX < DI.ui8_DisposeScopeRight)
{
L_Move.ui8_ImageX ++;
return TRUE;
} else
{
return FALSE;
}
}
else if (DI.ui8_ImageArray[L_Move.ui8_ImageY - 1][L_Move.ui8_ImageX]) //白区内向上
{
if (L_Move.ui8_ImageY == DI.ui8_DisposeScopeDown
&& DI.ui8_LPoint[L_Move.ui8_ImageY] - DI.ui8_DisposeScopeLeft > MID_POINT >> 2
&& DI.ui8_LPoint[L_Move.ui8_ImageY] - L_Move.ui8_ImageX > MID_POINT >> 2)
{
L_Move.ui8_ImageX = (DI.ui8_LPoint[L_Move.ui8_ImageY] + L_Move.ui8_ImageX) / 2;//左下角出现噪点,根据上次左点比较跳变
}
else
{
if (!ui8p_LF[L_Move.ui8_ImageY])
{
DI.ui8_LPoint[L_Move.ui8_ImageY] = L_Move.ui8_ImageX;//找到并记录!!!
L_Move.ui8_AllMaxY = L_Move.ui8_ImageY;
ui8p_LF[L_Move.ui8_ImageY] = 1;
}
L_Move.ui8_ImageY --;
}
return TRUE;
}
else if (DI.ui8_ImageArray[L_Move.ui8_ImageY][L_Move.ui8_ImageX + 1]) //向上是黑则向右
{
L_Move.ui8_ImageX ++;
return TRUE;
}
else if (L_Move.ui8_ImageY < DI.ui8_DisposeScopeDown //向上找不到白点,且向右找不到白点,则返回找
&& DI.ui8_ImageArray[L_Move.ui8_ImageY + 1][L_Move.ui8_ImageX])
{
while (L_Move.ui8_ImageY < DI.ui8_DisposeScopeDown
&& DI.ui8_ImageArray[L_Move.ui8_ImageY + 1][L_Move.ui8_ImageX]
&& !DI.ui8_ImageArray[L_Move.ui8_ImageY + 1][L_Move.ui8_ImageX + 1])
{
L_Move.ui8_ImageY ++;
}
if (L_Move.ui8_ImageY < DI.ui8_DisposeScopeDown)
{
L_Move.ui8_ImageY ++;
L_Move.ui8_ImageX ++;
return TRUE;
} else
{
return FALSE;
}
}
else {
return FALSE;
}
}
中线拟合
得到了左右边界线就可以试着中线拟合了。也不能说是最蠢的办法,毕竟我这个水平就只喜欢用这个办法,那就是左右两边的坐标(嗯,对,刚刚边线提取出来的不是像素点,而是每一行边界点所在的坐标)相加除以2.但是这方法的缺点也很明显,等到弯道的时候,拟合出来的中线往往不如人意,特别是在像180度的这种大弯,有着明显丢线的情况下,这种中线拟合的办法就是依托答辩,多半弯道无法完全转过去就冲出了赛道。我的建议是,写个最小二乘法拟合曲线的函数,这里的最小二乘法肯定不是拟合线性kx+b这种曲线的,虽然拟合这种直线也可以,但是觉着效果不会那么好,x^2的应该就差不多。将左右边线拟合一下,再好好拟合中线,处理好了中线,就能确保正常情况下车不会偏航,剩下的就只需要处理赛道元素和组别需要完成的相关任务了。