系列文章目录
之前也把自己做的全向所有电路都开源了,内容也包含完整的原理图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*30。3/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陀螺仪。
这一章主要介绍一下信号的预处理,看起来没啥用,也确实不会影响太多,但好的预处理完全可以简化之后很多问题,也可以提高程序的整体运行效率。
最后,还是那句话,开源不是开源整个工程,在我看来,算法思想的开源比简单的代码放出来要有用的多,毕竟每个车的参数,性能都不一样,有了方法才能更好的找到适合自己车的运行方案。
版权声明:本文为CSDN博主「六五绅士」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43488141/article/details/119977374
暂无评论