第18届全国大学生智能汽车竞赛四轮车开源讲解【2】

编程入门 行业动态 更新时间:2024-10-26 18:23:47

第18届全国大学生智能汽车竞赛四轮车<a href=https://www.elefans.com/category/jswz/34/1770131.html style=开源讲解【2】"/>

第18届全国大学生智能汽车竞赛四轮车开源讲解【2】

开源汇总写在下面

第18届全国大学生智能汽车竞赛四轮车开源讲解_Joshua.X的博客-CSDN博客

一、图像的基本参数

volatile uint8 mt9v03x_finish_flag = 0;   // 一场图像采集完成标志位
uint8 mt9v03x_image[MT9V03X_H][MT9V03X_W];//采集到的图像数据

基本参数有两个,一个是采集标志位,一个是图像数组。

1.标志位

标志位很好理解,当摄像头采集完一帧图像,标志位会被置一,可以在主循环中不断判断,当标志位是1时,你就可以读取该帧图像,处理完图像再把标志位清零,让他开始下一帧数据的采集。

根据习惯不同,也可以先清零标志位,再处理图像;或者先处理图像,再清零标志位。

理论上是有点区别,个人感觉没什么太大差异。

if(mt9v03x_finish_flag)//先处理图像,再清除标志位
{Threshold=My_Adapt_Threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//大津算阈值Image_Binarization(Threshold);//图像二值化mt9v03x_finish_flag=0;//标志位清除
}
if(mt9v03x_finish_flag)//先清除志位,再处理图像
{mt9v03x_finish_flag=0;//标志位清除Threshold=My_Adapt_Threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//大津计算阈值Image_Binarization(Threshold);//图像二值化
}

本人习惯使用第一种处理方法,先将图像处理后,再清零标志位,让他写入下一帧数据,防止处理过程中,原始图像数组被写入下一帧数据。实际上我队友用的第二种,对实际使用也没有影响。

2.灰度数组

uint8 mt9v03x_image[MT9V03X_H][MT9V03X_W];

这就是一个二维数组,长和宽就是在第一章我们定义的图像大小,MT9V03X_H代表图像的行数(在循环中一般是i),MT9V03X_W代表列数(在循环中一般是j),每个二维数组的值代表他的颜色。

我们看到的所有图像都是由一个个像素组成,常见的彩图的每个像素是RGB三个通道的数组值决定的,我们智能车常用的摄像头用的是灰度图像,只有一个通道。像素点数据用0~255的数字来表示颜色。

数字与颜色对应关系见图

其中,0是黑,255是白。其中越接近0越靠近黑,越靠近255越接近白。为了在后面便于使用,我们使用了两个宏定义。这两个宏定义在后面就会使用到。

(灰度值也对应了摄像头数组的数据类型uint8,就是8位数的数据范围0~255)

#define IMG_BLACK     0X00      //0x00是黑
#define IMG_WHITE     0Xff      //0xff为白

需要注意的是,坐标系问题。

本文使用的坐标系是和数组一样的坐标系,也就是数组的(0,0)点位图像的的左上角,与数组的访问下标规则一样。并非数学上经常使用的直角空间坐标系,数学上常用的坐标系左下角是原点。

本文及以后所有图像都基于此坐标系

另一个需要注意的问题是数据的范围问题,就例如上图,我开的是18*9的一个数组,但是访问是时候,最边界的一个数据的坐标是17和8。因为数组访问的是偏移量,最开始的一位是0,所以大家在遍历图像时,请注意数据范围,不要越界操作。

下面两种方法都可以,不要弄混就好。不然数据越界轻则卡死,重则数据异常查不出bug。

(卡死,你能立刻反应到有bug;数据异常,很难想到是数组越界,更多的以为算法算错了)

//访问范围<MT9V03X_H
for(i=0;i<MT9V03X_H;i++)
{for(j=0;j<MT9V03X_W;j++){//处理摄像头数据}
}//访问范围<=MT9V03X_H-1
for(i=0;i<=MT9V03X_H-1;i++)
{for(j=0;j<=MT9V03X_W-1;j++){//处理摄像头数据}
}

二、图像的基本处理

1.二值化

一幅灰度图的每个像素值从0~255,总共256个数值。二值化就是将这256个数进行“两极分化”,要么是0要么是255。

由于比赛规则规定的很清楚,赛道是蓝底白皮。这两者之间颜色差异很大,我们只需要分得清蓝色的是赛道背景,白色的是我们的赛道即可,没必要分析出其他的信息。

所以我们可以简单粗暴的将摄像头数据进行二值化处理,将0~255的像素数值,找到一个合适的阈值,直接变成0或者255这样只有两个值,也就是“非黑即白”。黑色的就是蓝布0,白色的就是赛道255。这就是二值化。

比赛场地铺设规范中有提到蓝色背景布

 第十八届全国大学生智能车竞赛赛场赛道铺设规范(实际稿件)_卓晴的博客-CSDN博客

当然,二值化必定会损失一些信息,但是只要前期图像获取的比较好,配上合理的二值化,损失的信息如果都是无关信息,那么对我们就没有影响,就可以忽略不计。

彩色图像 灰度图像 二值化图像

 二值化效果如下,只要做到白的是赛道,黑色的是背景蓝布,这就可以了,图像中间的光斑,后文会提到解决办法的。

二值化前后

二值化代码也很简单,找到一个阈值,大于该阈值的,给255,小于该阈值的,给0。

需要注意的是,我对图像二值化处理后,我没有把他放在原数组,而是新开了一个数组,后续所有图像识别操作都是对这个新数组进行识别。这样可以尽可能的避免刚二值化处理的一幅图像,摄像头采集的新的数据写入,覆盖掉我们二值化的数据。

uint8 image_two_value[MT9V03X_H][MT9V03X_W];//二值化后的原数组

二值化代码如下:

/*-------------------------------------------------------------------------------------------------------------------@brief     图像二值化处理函数@param     二值化阈值@return    NULLSample     Image_Binarization(Threshold);//图像二值化@note      二值化后直接访问image_two_value[i][j]这个数组即可
-------------------------------------------------------------------------------------------------------------------*/
void Image_Binarization(int threshold)//图像二值化
{uint16 i,j;for(i=0;i<MT9V03X_H;i++){for(j=0;j<MT9V03X_W;j++)//灰度图的数据只做判断,不进行更改,二值化图像放在了新数组中{if(mt9v03x_image[i][j]>=threshold)image_two_value[i][j]=IMG_WHITE;//白elseimage_two_value[i][j]=IMG_BLACK;//黑}}
}

1.1 大津法

大津法应该是二值化中比较出名的算法,个人理解如下。

由于图像的灰度范围已知,为0~255。那么我就去计算每个像素值的点的然个数,后就可以得到一张灰度直方图,横坐标是0~255,纵坐标是每个像素值点的个数。由于赛道的特殊性,会在在深色(蓝色)区域附近,在白色的区域附近会有两个尖峰,那我们就在这两个尖峰中间,找到一个最低值,作为阈值。对图像进行分割,大于该阈值的,直接给255小于该阈值的给0。 

以上仅作为个人理解,真实性没有任何保证。

灰度直方图

但是大津法有个弊端,由于需要遍历全图进行像素点的数值计算,那么我遍历一张180*70的图,就起码需要180*70=12600次访问,再加上一些计算处理,其实是比较费时间的,一般的单片机需要几ms来对图像进行大津法+二值化处理,略费时间。所以不建议将图像开的太大。

大津法参考代码参考如下

/*-------------------------------------------------------------------------------------------------------------------@brief     普通大津求阈值@param     image       图像数组width       列 ,宽度height      行,长度@return    threshold   返回int类型的的阈值Sample     threshold=my_adapt_threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//普通大津@note      据说没有山威大津快,我感觉两个区别不大
-------------------------------------------------------------------------------------------------------------------*/
int My_Adapt_Threshold(uint8*image,uint16 width, uint16 height)   //大津算法,注意计算阈值的一定要是原图像
{#define GrayScale 256int pixelCount[GrayScale];float pixelPro[GrayScale];int i, j;int pixelSum = width * height/4;int  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; i++) //计算每个像素值的点在整幅图像中的比例{pixelPro[i] = (float)pixelCount[i] / pixelSum;}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; j++)//遍历灰度级[0,255]{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);//平方if (deltaTmp > deltaMax){deltaMax = deltaTmp;//最大类间方差法threshold = j;}if (deltaTmp < deltaMax){break;}}if(threshold>255)threshold=255;if(threshold<0)threshold=0;return threshold;
}

 这里还有我找到的山威的快速大津,使用效果没什么差别,据说计算速度会快一些。

/*-------------------------------------------------------------------------------------------------------------------@brief     快速大津求阈值,来自山威@param     image       图像数组col         列 ,宽度row         行,长度@return    nullSample     threshold=my_adapt_threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//山威快速大津@note      据说比传统大津法快一点,实测使用效果差不多
-------------------------------------------------------------------------------------------------------------------*/
int my_adapt_threshold(uint8 *image, uint16 col, uint16 row)   //注意计算阈值的一定要是原图像
{#define GrayScale 256uint16 width = col;uint16 height = row;int pixelCount[GrayScale];float pixelPro[GrayScale];int i, j;int pixelSum = width * height/4;int 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; i++){pixelPro[i] = (float)pixelCount[i] / 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; 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);if (deltaTmp > deltaMax){deltaMax = deltaTmp;threshold = j;}if (deltaTmp < deltaMax){break;}}return threshold;
}

大家在使用时也不必关心计算方法,只需要关心传入的图像,传出的阈值就好,如果认为处理过于复杂,有能力的朋友可以自行优化上述代码。

1.2 索贝尔算子Sobel operator

索贝尔算子是计算机视觉领域的一种重要处理方法。主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。索贝尔算子是把图像中每个像素的上下左右四领域的灰度值加权差,在边缘处达到极值从而检测边缘。

索贝尔算子主要用作边缘检测。在技术上,它是一离散性差分算子,用来运算图像亮度函数的梯度之近似值。在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。

索贝尔算子不但产生较好的检测效果,而且对噪声具有平滑抑制作用,但是得到的边缘较粗,且可能出现伪边缘。

这个算法是专业的图像边缘检测算法,核心原理是对数学公式的推导,大概就是什么计算相邻像素之间的梯度,也就是数值下降,上升的速度比较快吧,本人不懂。

Sobel边缘检测

 参考代码如下(某邱的开源代码)

/*-------------------------------------------------------------------------------------------------------------------@brief     sobel二值化@param     imagein       原图数组imangeout     二值化后的数组@return    nullSample     lq_sobelAutoThreshold (mt9v03x_image[MT9V03X_H][MT9V03X_W],image_two_value[MT9V03X_H][MT9V03X_W])@note      会比大津慢一些,效果比大津法好不少
-------------------------------------------------------------------------------------------------------------------*/
void lq_sobelAutoThreshold (unsigned char imageIn[LCDH][LCDW], unsigned char imageOut[LCDH][LCDW])
{/**卷积核大小**/short KERNEL_SIZE = 3;short xStart = KERNEL_SIZE / 2;short xEnd = LCDW - KERNEL_SIZE / 2;short yStart = KERNEL_SIZE / 2;short yEnd = LCDH - KERNEL_SIZE / 2;short i, j, k;short temp[4];for (i = yStart; i < yEnd; i++){for (j = xStart; j < xEnd; j++){/* 计算不同方向梯度幅值  */temp[0] = -(short) imageIn[i - 1][j - 1] + (short) imageIn[i - 1][j + 1]//{{-1, 0, 1},- (short) imageIn[i][j - 1] + (short) imageIn[i][j + 1]                 // {-1, 0, 1},- (short) imageIn[i + 1][j - 1] + (short) imageIn[i + 1][j + 1];        // {-1, 0, 1}};temp[2] = -(short) imageIn[i - 1][j] + (short) imageIn[i][j - 1]        //{{0, -1, -1}- (short) imageIn[i][j + 1] + (short) imageIn[i + 1][j]                 // {1,  0, -1}- (short) imageIn[i - 1][j + 1] + (short) imageIn[i + 1][j - 1];        // {1,  1,  0}};temp[3] = -(short) imageIn[i - 1][j] + (short) imageIn[i][j + 1]        //{{-1, -1,  0}- (short) imageIn[i][j - 1] + (short) imageIn[i + 1][j]                 // {-1,  0,  1}- (short) imageIn[i - 1][j - 1] + (short) imageIn[i + 1][j + 1];        // { 0,  1,  1}};temp[0] = abs(temp[0]);temp[1] = abs(temp[1]);temp[2] = abs(temp[2]);temp[3] = abs(temp[3]);/* 找出梯度幅值最大值  */for (k = 1; k < 4; k++){if (temp[0] < temp[k]){temp[0] = temp[k];}}/* 使用像素点邻域内像素点之和的一定比例    作为阈值  */temp[3] = (short) imageIn[i - 1][j - 1] + (short) imageIn[i - 1][j] + (short) imageIn[i - 1][j + 1]+ (short) imageIn[  i  ][j - 1] + (short) imageIn[  i  ][j] + (short) imageIn[  i  ][j + 1]+ (short) imageIn[i + 1][j - 1] + (short) imageIn[i + 1][j] + (short) imageIn[i + 1][j + 1];if (temp[0] > temp[3] / 12.0f){imageOut[i][j] = 0;}else{imageOut[i][j] = 0xff;}}}
}

实际二值化速度要比大津法慢不少,但是效果要更棒,看大佬们有无时间对算法进行优化。

2.灰度

灰度巡线就是利用赛道的特点,在黑白交界处会发生非常明显的数值跳变,计算每一行的跳变,放大他,当大于或者小于某一阈值,认为找到边界。

由于本人没有使用灰度,所以不多做介绍,详细情况请参考下面的推文。

电磁及摄像头(总钻风)寻迹算法浅析--逐飞科技

直接处理灰度图好处如下:

  1. 减少运算量,二值化处理需要对全图进行遍历,浪费时间。
  2. 抗干扰能力强,对于大津法,在光线不均的情况下,阈值会爆炸,而灰度会保留更多信息。

其实最开始我也是想使用灰度图的,但是到后期控制逐渐成熟,对于车子也不敢有太大的改动,所以也就没用进行灰度处理,这里还是比较推荐各位使用灰度的。

3.图像压缩

图像压缩可以在视野广阔的前提下缩小数组,就是把一张很大的图片等比例进行缩小。这样必定会损失一部分细节,但是好处是可以显著降低图像体积,在后续处理的时候,对于全图的遍历扫线,找点的操作计算量会减小很多。

进行图像压缩的可以将原数组开的大一些,保证获得广阔的视野。然后每间隔几个点,取一个点作为有效点存起来,我印象中有些国赛的队伍甚至图像压缩到80*60。这样对后续图像处理速度会有质的提升。

由于我没有使用,代码就不放在下面了。就是间隔,按照比例选取点丢到另一个数组去即可。

这里提一个注意事项,在压缩时,尽量压缩整倍数。如果压缩的倍数不合理,会产生小数点丢弃的情况,这样会造成图像变歪。摄像头歪着图像才是正的。我们实验室的同学就发生过这样的情况。所以将原始图像调好后,选择合适的缩放比例,尽量去凑整倍数,这样才会有比较好的效果。

4.抗干扰

每年的线下赛举办方各不相同,现场情况更是无法估计,总会出现非常多的问题,光线就是摄像头最大的杀手之一。

这里提出几个抗干扰的方法。

1.遮光窗帘

未使用遮光窗帘

调车时要记得开灯,这里只是示意图,窗帘遮光性能很好。

使用遮光窗帘后

 由于智能车对于光线要求高,这里的高值得是均匀度,只要是光线均匀,哪怕暗一些都无所谓。所以直接买一套遮光窗帘,屏蔽掉室外的太阳光,只使用室内的灯,会使光线条件变好。当然也要注意,整体变亮或者变暗,需要调整摄像头的曝光,让摄像头尽量得到比较适中的数据。

当然,比赛现场有没有,那得看命。

2.偏振片

摄像头前面那个大圆盘就是偏振片

 偏振片可以清除掉特定角度的反射光线,所以我们在开头看到的灯光反射的亮斑,使用偏振片,根据实际情况调整他的角度,就可以直接滤掉。

旋转偏振片角度,滤掉某一特定角度的光

 据我实际使用经验,偏振片+遮光窗帘就处理掉90%光线问题。

3.抹布

当光线效果是在不佳时,征得裁判员同意后,老老实实用抹布吧,有些情况是技术上无解的,得采取一些物理手段了。

十七届电子科技大学充电组国赛现场

 到这里,我们就获取到了一帧可以直接处理的优秀图像了(二值化),具体如何提取赛道信息,提取那些信息,下期我们再讲。

希望能够帮助到一些人。

本人菜鸡一只,各位大佬发现问题欢迎留言指出。

qq:2296449414

更多推荐

第18届全国大学生智能汽车竞赛四轮车开源讲解【2】

本文发布于:2024-02-12 18:47:17,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1688959.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:开源   四轮   全国大学生   智能   汽车

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!