系列文章目录

之前也把自己做的全向所有电路都开源了,内容也包含完整的原理图PDF,想了解的可以看看往期博客。

硬件开源第一章

硬件开源第二章

前言

       算法开源系列估计会比较多,自己做车大概是电磁和摄像头两种方案都用过。但随着这几年的规则改变,智能车也不像前几年那样按照传感器分类了。随着大佬们不断的刷新智能车的车速极限,新手们想或许可以通过多传感器融合的方法,去尽快的实现车的完赛。要知道,完赛才是首先要考虑的事,就算是在国赛赛场,翻车的也不在少数。毕竟完赛才意味着自己有成绩,其实,这几年想要拿奖,最关键的是要保持车的稳定性,追求极限速度可能大部分人都做不到,尤其对于室外PVC赛道的组别。3M神车也仅仅是赛区前三左右,大部分的车进国赛的车都在2M多左右。如何在保持自己车的稳定性是首要任务,提速也是需要在一个速度下跑的90%不出意外才可以完美的加速。  

    所以如果想要完赛就满足或者说是在短时间内完赛,可以在遇到识别问题上加一种传感器试试,当然,这是之后给大家开源的东西了,先把这一章的内容解决吧,这一章先介绍下两种方法的预处理方案。不是高深学问,只是作为一种经验分享,希望大佬轻喷。


一、智能车比赛常用的赛道检测方法

        对于传统的智能车,除了创意组之外,几乎都离不开磁和光。当然十五届的声音除外,这里只介绍一直使用在智能车赛场的检测方法。磁的话就是磁场信号,比赛中用的是通过信号发生器产生的变化磁场。参赛队员需要通过电感采集到信号,再通过运算放大电路将信号放大,最后将信号通过单片机的ADC采集管脚输入到单片机了。光的话可能会让人产生误解,其实就是摄像头。为什么说我在这里说是光呢,其实一开始,摄像头组叫光电组,当时参赛的队伍常用的事CCD摄像头,CCD是光学信号转换为模拟电流信号,然后经过一系列处理得到图像。这里不展开了,感兴趣的可以自己去了解。还有一方面说光的原因就是摄像头对光真的是太敏感了,比赛时候碰到上帝之光,真的无可奈何。不过现在大部分队伍都用了CMOS摄像头,比较有代表性的就是逐飞的总钻风和龙邱的神眼。今年的全向组我用的是逐飞的总钻风,一开始比较担心麦轮的颠簸,绞尽脑汁也没找到最优的解决办法。后面发现自己多虑了,总钻风动态特性还挺好,跑的过程图像根本没我想的那么抖(我真是是跟着车跑,盯着TFT上的屏幕一直看着,真的累)。

遇到这样的上帝之光就跪了吧

二、电磁信号预处理

1.ADC采集

     我记得在逐飞提供的参考库好像是将信号采集多次,求取平均数。这样做的原因是磁场的信号是变化的,我们一般希望采集到的是峰峰值,就是用最大值的信号。但是用于采集速度很快,我们如果直接采集的是放大器输出的信号,ADC的值是从小到大变化的,而且可能值跳动的很大。比如跳动范围在20以内,这是很影响识别精度的。所以,一般会采集一个周期采集多次求取平均值的方法来稳定信号。

     刚做车的时候还被学长坑过,之前学长教的方法是采集60次然后冒泡排序,选出最大值。听起来蛮合理的,实际在只使用电磁的时候也没毛病,信号采集的也不错。但实际上呢,如果结合图像处理的话,就有问题了。冒泡排序是非常非常非常慢的一种排序方法,而我们用的电感也不是仅仅一个,多个电感的采集再加上排序,时间之前用k60测得是3ms到2ms。要知道,正常一幅图像的处理时间也就要压缩在10ms(我用的是100FPS)。这种费时费运算的方法太坑爹了,比大津法还浪费时间。

     所以,我们用了OPA2350放大器,在放大器输出端加上检波的电路,把交流信号变成直流。信号波动很小,噪声可以忽略。每次只需要采集就可以,不放心可以采集5次,求个平均。这对于单片机来说根本就不是事。关于检波电路在前面一章已经开源了,喜欢电磁的同学可以看看。

之后就是对偏差数据的处理了,这里我用的是差比和,目的是为了在把偏差放在自己想要的范围之类,在不同环境下也可以得到有效偏差。虽然今年一直没用电磁,但加上运放板车子还挺好看的。

void ADC_init()
{
 
   adc_init(ADC_IN8_B0);
   adc_init(ADC_IN9_B1);
 //  adc_init(ADC_IN4_A4);
  // adc_init(ADC_IN6_A6);
 
}
void ADC_GET()
{
 
    left_ad = adc_convert(ADC_IN8_B0 ,ADC_8BIT);
    right_ad = adc_convert(ADC_IN9_B1 ,ADC_8BIT);
 
}
 
void ADC_Error()
{
int16 ad_difference=0;
int16 ad_sum=0;
ad_difference=left_ad-right_ad;
ad_sum=left_ad+right_ad;
ad_error=(ad_difference*30)/ad_sum;
 
}

附上今年全向组的的ADC采集与偏差代码。

二.图像的预处理

1.图像压缩

由于本届单片机属实拉胯,再逐飞的库上随便加一个图像数组就GG了。无奈只能放弃全部移植之前学长的代码,改用十五届自己的灰度图算法。这里分享一个用ch32也可以处理大图的方法,无论是逐飞还是龙邱的推荐方案中,都是推荐使用小一点的图,大概188*20吧,具体也记不住了。目的就是为了二值化以后新建的数组不撑爆RAM,但我这里最大的时候用的是188*80,一个完整的周期,包括全元素,最慢11ms,正常7ms左右,采集用的是100FPS,后面跑的时候为了处理速度还是采集原图缩到了160*60

具体方案是图像压缩,将采集回来的160*60的图像进行压缩,再处理压缩后的图。我这里是压缩了3/4,将原图160*60的压缩成80*303/4指的是像素点减少了3/4,原图是16*60=9600,压缩后是80*30=2400。像素点就减少了许多,但不影响信息的提取。其实本来我们眼中看到的一条线,在采集的图像里可能一行有好多的像素值,减掉几个根本不是事。

      压缩前和压缩后的图像对比,可以看到,真的没少信息,就是看起来有点费劲。这个方法是当时我大一时候一个学长用的,可以说是我做车的引路人兼偶像吧(电赛和智能车都拿过国一的神人),几乎我用的算法都有他的影子。下面附上压缩代码,直接可以移植的函数觉得有用的话可以处理。


/*----------------------------------------------------------------------------------------------------------------
*  @brief   图像减半函数  数据量减少3/4
*  @param   *p 图像数组地址
*       *p1 转换图像地址
*       row图像行
*                  col图像列
*  @return  void
*  @since   v1.0
*  Sample usage:      halve_image(image_buff[0],image[0],ROW,COL);    //输出image[0]
*  乐哥YYDS
*  乐哥YYDS
*  乐哥YYDS
//-----------------------------------------------------------------------------------------------------------------*/
void halve_image(unsigned char *p_in,unsigned char  *p_out,unsigned char row,unsigned char col) //图像减半
{
 
       uint8 i, j;
       for (i = 0; i<row/2; i++)
       {
          for (j = 0;j<col/2; j++)
          {
             *(p_out+i*col/2+j)=*(p_in+i*2*col+j*2);
          }
       }
 
}

 2.大津法

      大津法大家应该不陌生了,几乎都会用大津法计算动态阈值,不过这里希望不要直接拿来主义用逐飞提供的原始大津法。逐飞只是给大家一个参考,作为思想启发,直接用过于费时间。现在基本都是用网上开源的简化大津法,至于我为什么处理灰度图还需要大津法计算阈值,会在下一章图像边界处理中写上。

至于大津法的原理,我也不细说了,这样又得增加篇幅了,直接附上代码

/***************************************************************
简化大津法  1.01ms  计算量大时 1.5ms    计算量小时0.7ms
隔点扫描获取阈值
* 函数名称:uint8 otsuThreshold(uint8 Image[ROW][COL], uint16 col, uint16 row)
* 功能说明:获取图像的灰度信息 取最佳阈值
* 参数说明:
* 函数返回:void uint8 threshold
* 修改时间:2018年3月7日
* 备 注:
***************************************************************/
unsigned char adapt_otsuThreshold(uint8 *image, uint8 col, uint8 row,unsigned char *threshold)   //注意计算阈值的一定要是原图像
{
       #define GrayScale 256
       uint8 width = col;
       uint8 height = row;
       int16 pixelCount[GrayScale];  //每个像素点的个数
       float pixelPro[GrayScale];  //每个像素点占总像素点的比例
       int16  pixelSum = width * height/4;
       int16 i, j;
       //uint8 threshold = 0;
       uint8* data = image;  //指向像素数据的指针
       for (i = 0; i < GrayScale; i++)
       {
          pixelCount[i] = 0;
          pixelPro[i] = 0;
       }
       uint32 gray_sum=0;
       //统计灰度级中每个像素在整幅图像中的个数
       for (i = 0; i < height; i+=2)
       {
          for (j = 0; j < width; j+=2)
          {
             pixelCount[(int)data[i * width + j]]++;  //将当前的点的像素值作为计数数组的下标
             gray_sum+=(int)data[i * width + j];       //灰度值总和
          }
       }
 
       //计算每个像素值的点在整幅图像中的比例
 
       for (i = 0; i < GrayScale/2; i++)
       {
         pixelPro[i] = (float)pixelCount[i] / pixelSum;
        //  pixelPro[i+2] = (float)pixelCount[i+2] / pixelSum;
        //  pixelPro[i+3] = (float)pixelCount[i+3] / pixelSum;
       }
 
       //遍历灰度级[0,255]
       float w0, w1, u0tmp, u1tmp, u0, u1, u, deltaTmp, deltaMax = 0;
 
 
 
       w0 = w1 = u0tmp = u1tmp = u0 = u1 = u = deltaTmp = 0;
       for (j = 0; j < GrayScale/2; j++)
       {
 
          w0 += pixelPro[j];  //背景部分每个灰度值的像素点所占比例之和   即背景部分的比例
          u0tmp += j * pixelPro[j];  //背景部分 每个灰度值的点的比例 *灰度值
 
          w1=1-w0;
          u1tmp=gray_sum/pixelSum-u0tmp;
 
          u0 = u0tmp / w0;              //背景平均灰度
          u1 = u1tmp / w1;              //前景平均灰度
          u = u0tmp + u1tmp;            //全局平均灰度
         // deltaTmp = w0 * pow((u0 - u), 2) + w1 * pow((u1 - u), 2);
         deltaTmp = w0 * (u0 - u)*(u0 - u) + w1 * (u1 - u)*(u1 - u);
          if (deltaTmp > deltaMax)
          {
             deltaMax = deltaTmp;
             *threshold = j;
          }
          if (deltaTmp < deltaMax)
          {
             *threshold+=0;
             break;
          }
       }
       return *threshold;
}

     配合图像压缩处理,可以更快的算出阈值,也不需要对原始采集的图像进行运算了,如果单片机性能足够,也可以直接压缩后,大津法计算阈值,再二值化。这样的速度比直接大津法二值化要快,信息也不会丢失。

总结

       在使用CH32的时候,往往会因为图像代码太大了,这时候除了自己算法的精简和优化,也可以开优化器。现在几乎所有的编译器都可以开,各个官网或者百度都可以找到相关编译器开优化的教程。不过这里我在使用优化器时候遇到过一些问题,比如在IAR全局开优化器时候,总钻风无法初始化成功,CH32开优化器时候逐飞的模拟IIC和模拟SPI无法使用。也没有深度了解这个问题,不过还是推荐,优化器能不全开就不全开,有些编译器支持单独文件优化,可以对自己的算法和控制开,初始化各个模块的时候可以选择不开,毕竟也没啥大的运算,但有时候对初始化开优化还容易出bug。而对于CH32逐飞库开优化无法使用模拟IIC我是用了硬件SPI来解决的,icm20602陀螺仪。 

   这一章主要介绍一下信号的预处理,看起来没啥用,也确实不会影响太多,但好的预处理完全可以简化之后很多问题,也可以提高程序的整体运行效率。

  最后,还是那句话,开源不是开源整个工程,在我看来,算法思想的开源比简单的代码放出来要有用的多,毕竟每个车的参数,性能都不一样,有了方法才能更好的找到适合自己车的运行方案。