前言

        前面写了图像的预处理,包括图像压缩,和大津法,这一章介绍图像边界提取。这里我的算法是基于逐飞开源的灰度图算法写的,但也在上面改进了许多,使得边界的搜取不会出现丢边。


一、灰度边界提取的原理

         在我们获取摄像头的数据后,返回回来的是0-255的灰度值。就是时候每一副图都是有许多个值在0-255范围的像素点构成。而比赛时,赛道和赛道背景存在明显的亮暗差异,这也是为什么之前都会采取将图像二值化变成黑白图——便于处理,边界明显。不过今年为了加快处理效率,我选用了直接处理灰度图。

        其实处理灰度图最简单的方法就是将灰度图当做黑白图处理,可以直接在搜线的判断条件中设定某个点左边连续两点高于阈值,右边点低于阈值,这个点就是边界点,省去了一个二值化过程。

         而我在处理灰度图时,基于逐飞灰度图开源方案加上了大津法计算出来的阈值判断。逐飞开源的方法是隔5个点计算两个点之间的灰度值差,通过差比和将偏差设计到自己想要的范围。当这个差值大于认为设定的阈值后,就认定找到了边界。其实这个方法是会出现丢边的现象,因为仅仅一个条件,在光线不均匀的地方,很容易就会在赛道外满足条件。

         针对这个问题,我们采用的办法是选取的两点灰度值必须在一个大于大津法设定的阈值一个小于大津法设定的阈值。我这里用的是压缩后的图(压缩的方法在前一章),所以对于边界的判断条件我这里是每隔两个点比较两点之间的灰度值,但这个偏差大于设定的偏差,且两点灰度值一个大于大津法阈值,一个小于大津法阈值就可以判断边界存在。

附上判断条件的代码,其中difference_sum和mark_middle_num分别为灰度值差比和函数以及阈值范围判断函数。

if(difference_sum(image[EXTRACT_ROW][EXTRACT_COL+COL_SPACE],image[EXTRACT_ROW][EXTRACT_COL])>=g_threshold_value&& mark_middle_num(image[EXTRACT_ROW][EXTRACT_COL],image[EXTRACT_ROW][EXTRACT_COL+COL_SPACE],Threshold))
         {
           str->LeftEdge[EXTRACT_ROW]=EXTRACT_COL+COL_SPACE-1;//找到起始行储存
           L_edge_last=EXTRACT_COL;
 
           /*第一次得到线*/
           if(s_L_get_flag==0)
           {
             str->L_StartLine=EXTRACT_ROW;
 
           }
           break;
         }

这里是上面调用两个函数的代码结构。

 //差比和函数
 int difference_sum(unsigned char a,unsigned char b )
 {
  if(a+b==0)
  {
    return 0;
  }
   return (a-b)*300/(a+b);
 }
 
 int mark_middle_num(uint8 num1,uint8 num2,uint8 middle)//阈值比较
 {
   if(num1<=num2 && num1<=middle && num2>=middle)
   {
     return 1;
   }
   else if(num2<num1 && num1>middle && num2<middle)
   {
     return 1;
   }
   else
   {
     return 0;
   }
 }

二、边界提取的策略

     对于如何扫描边界其实也需要方法的,很多初学者往往会全部扫描一行,或者从中间向两边扫描,这样其实很浪费时间,也浪费性能。

     最简单的方法就是边缘跟踪的扫线,其实网上说的一些八领域之类的也就是跟踪的一类。主体思想就是在上一行边界附近进行扫描,因为我们比赛的赛道总体是连续的,第一行找到了边界之后,就可以顺着这点在下一行对应的这一列周围扫描。而如果不连续了那正好可以拿来做元素的识别判断。

      像这里,在我们第一次通过全部扫描最底行找到了A点之后,就可以记住A点的列坐标,在上面C点周围几个点搜索。这样可能只需要扫描很少的几个点就能找到边界了,同时,我们还可以通过C这一点的灰度值,判断搜线方向。比如C这一点是在赛道上是黑的,我们就可以从C点向右搜线,不管左边。如果C点在赛道中间,就往左搜线,不管右边,有节省了一些时间。这是左边线策略,右边线搜线同理。

再调试时,完全可以将边界线打在TFT上,看到自己的边界搜索是否合理。

TFT描点边界线。

中线的提取和偏差的计算

       在提取完边界后,就需要计算出中线了。如果左右边界都存在时直接就相加除以二,得到边界。如果只有一个边界时,可以通过赛道宽度来计算边界。一开始我是采用动态的赛道宽度,每次遇到断行后,用断行之后的行数宽度与之前的边界都存在的直道宽度做比值。通过这个比值来选取最近的实际宽度来做补线宽度。后面为了方便直接通过最小二乘法计算得到赛道的宽度一次函数,预先存在程序里。不过这种方法受摄像头角度和高度,也正因如此,我自身很多算法的参数不能直接移植到其他车上。

关于最小二乘法计算宽度的方法是通过10个点的左边界连续点和10个右边界连续点,分别用最小二乘法计算出各自的斜率与常数值。比如左边界函数是y=ax+b,右边界时Y=cx+d,宽度函数就是y=(c-a)x+(d-b)。这要摄像头角度保持合适,这和宽度函数是很准的。

中线的部分代码

  if(str->LeftEdge[ROW_END]!=0 && str->RightEdge[ROW_END]!=0)     //左右都有边界
  {
      str->MiddleLine[ROW_END] =(str->LeftEdge[ROW_END] +str->RightEdge[ROW_END])>>1;
 
  }
  else if(str->LeftEdge[ROW_END]!=0 && str->RightEdge[ROW_END]==0)//find left
  {
      str->RightEdge[ROW_END]=COL_END;
      str->MiddleLine[ROW_END]=(str->LeftEdge[ROW_END] + str->RightEdge[ROW_END])>>1;
 
  }
  else if( str->LeftEdge[ROW_END]==0 && str->RightEdge[ROW_END]!=0)//find right
  {
      str->LeftEdge[ROW_END]=0;
      str->MiddleLine[ROW_END]=(str->LeftEdge[ROW_END] + str->RightEdge[ROW_END])>>1;
 
  }
  else if( str->LeftEdge[ROW_END]==0 &&str->RightEdge[ROW_END]==0)//两边丢线   d待处理
  {
      str->LeftEdge[ROW_END]=0;
      str->RightEdge[ROW_END]=COL_END;
      str->MiddleLine[ROW_END]=(str->LeftEdge[ROW_END] + str->RightEdge[ROW_END])>>1;
  }

       而当中线提取出来之后,就可以计算偏差了,这里我采用的是计算连续几行的偏差做平均,对于不同的赛道元素,选取的偏差行数不同。对于偏差的行数选取,需要根据不同的赛道做判断。这里也是我做的不足之一,对于控制方面,我做的实在太过于简单,只是单纯的选取偏差后进行PID的计算。往往会在弯道出弯处或者知道接弯出现来不及或者超调,尤其当速度上去后,漂移十分常见,而对于控制我能做的就是不停的分段分段再分段。只能做到麦轮稳定在1.8m的沿中线跑,如果速度更快。到2m时,不是压内弯就是压赛道边界。这也是我的败笔吧,缺少好的控制方法,在高速行驶时,车模的控制一点也不稳定。

       关于赛道弯道和直道的分段后续再说吧,这一章就到此为止吧。

float Getrouteoffset(unsigned char start,unsigned char end,float midpos)
{
  unsigned char i=0;
  //unsigned char iCount=0;
  float  Black_Sum=0;
  float weightSum = 0;
  float TemError = 0.0;
 if(end<ROW_START)
 {
   end=ROW_START+2;
 }
  if(start<end)
  {
    return TemError;
  }
  else
  {
     for(i=start; i>end;i--)
     {
 
        Black_Sum += 1.0*(midpos-Boundary.MiddleLine[i]);
 
        weightSum += 1;
     }
     TemError =Black_Sum*1.0/weightSum;
 
     if(TemError > 40)
     {
        TemError =40;
     }
     if(TemError < -40)
     {
        TemError = -40;
     }
     return TemError;
  }
}