开源汇总写在下面
第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(0xFF)。
由于比赛规则规定的很清楚,赛道是蓝底白皮。这两者之间颜色差异很大,我们只需要分得清蓝色的是赛道背景,白色的是我们的赛道即可,没必要分析出其他的信息。
所以我们可以简单粗暴的将摄像头数据进行二值化处理,将0~255的像素数值,找到一个合适的阈值,直接变成0或者255这样只有两个值,也就是“非黑即白”。黑色的就是蓝布0,白色的就是赛道255。这就是二值化。
比赛场地铺设规范中有提到蓝色背景布
第十八届全国大学生智能车竞赛赛场赛道铺设规范(实际稿件)_卓晴的博客-CSDN博客
当然,二值化必定会损失一些信息,但是只要前期图像获取的比较好,配上合理的二值化,损失的信息如果都是无关信息,那么对我们就没有影响,就可以忽略不计。
彩色图像 灰度图像 二值化图像
二值化效果如下,只要做到白的是赛道,黑色的是背景蓝布,这就可以了,图像中间的光斑,后文会提到解决办法的。
二值化前后
二值化代码也很简单,找到一个阈值,大于该阈值的,给255,小于该阈值的,给0。
需要注意的是,我对图像二值化处理后,我没有把他放在原数组,而是新开了一个数组,后续所有图像识别操作都是对这个新数组进行识别。这样可以尽可能的避免刚二值化处理的一幅图像,摄像头采集的新的数据写入,覆盖掉我们二值化的数据。
uint8 image_two_value[MT9V03X_H][MT9V03X_W];//二值化后的原数组
二值化代码如下:
/*-------------------------------------------------------------------------------------------------------------------
@brief 图像二值化处理函数
@param 二值化阈值
@return NULL
Sample 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;//白
else
image_two_value[i][j]=IMG_BLACK;//黑
}
}
}
1.1 大津法
大津法应该是二值化中比较出名的算法,个人理解如下。
由于图像的灰度范围已知,为0255。那么我就去计算每个像素值的点的然个数,后就可以得到一张灰度直方图,横坐标是0255,纵坐标是每个像素值点的个数。由于赛道的特殊性,会在在深色(蓝色)区域附近,在白色的区域附近会有两个尖峰,那我们就在这两个尖峰中间,找到一个最低值,作为阈值。对图像进行分割,大于该阈值的,直接给255小于该阈值的给0。
注:以上仅作为个人理解,真实性没有任何保证。
灰度直方图
但是大津法有个弊端,由于需要遍历全图进行像素点的数值计算,那么我遍历一张18070的图,就起码需要18070=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 256
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;
}
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 null
Sample threshold=my_adapt_threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//山威快速大津
@note 据说比传统大津法快一点,实测使用效果差不多
-------------------------------------------------------------------------------------------------------------------*/
int my_adapt_threshold(uint8 *image, uint16 col, uint16 row) //注意计算阈值的一定要是原图像
{
#define GrayScale 256
uint16 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 null
Sample 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.灰度
灰度巡线就是利用赛道的特点,在黑白交界处会发生非常明显的数值跳变,计算每一行的跳变,放大他,当大于或者小于某一阈值,认为找到边界。
由于本人没有使用灰度,所以不多做介绍,详细情况请参考下面的推文。
电磁及摄像头(总钻风)寻迹算法浅析--逐飞科技
直接处理灰度图好处如下:
- 减少运算量,二值化处理需要对全图进行遍历,浪费时间。
- 抗干扰能力强,对于大津法,在光线不均的情况下,阈值会爆炸,而灰度会保留更多信息。
其实最开始我也是想使用灰度图的,但是到后期控制逐渐成熟,对于车子也不敢有太大的改动,所以也就没用进行灰度处理,这里还是比较推荐各位使用灰度的。
3.图像压缩
图像压缩可以在视野广阔的前提下缩小数组,就是把一张很大的图片等比例进行缩小。这样必定会损失一部分细节,但是好处是可以显著降低图像体积,在后续处理的时候,对于全图的遍历扫线,找点的操作计算量会减小很多。
进行图像压缩的可以将原数组开的大一些,保证获得广阔的视野。然后每间隔几个点,取一个点作为有效点存起来,我印象中有些国赛的队伍甚至图像压缩到80*60。这样对后续图像处理速度会有质的提升。
由于我没有使用,代码就不放在下面了。原理就是间隔,按照比例选取点丢到另一个数组去即可。
这里提一个注意事项,在压缩时,尽量压缩整倍数。如果压缩的倍数不合理,会产生小数点丢弃的情况,这样会造成图像变歪。摄像头歪着图像才是正的。我们实验室的同学就发生过这样的情况。所以将原始图像调好后,选择合适的缩放比例,尽量去凑整倍数,这样才会有比较好的效果。
4.抗干扰
每年的线下赛举办方各不相同,现场情况更是无法估计,总会出现非常多的问题,光线就是摄像头最大的杀手之一。
这里提出几个抗干扰的方法。
1.遮光窗帘
未使用遮光窗帘
调车时要记得开灯,这里只是示意图,窗帘遮光性能很好。
使用遮光窗帘后
由于智能车对于光线要求高,这里的高值得是均匀度,只要是光线均匀,哪怕暗一些都无所谓。所以直接买一套遮光窗帘,屏蔽掉室外的太阳光,只使用室内的灯,会使光线条件变好。当然也要注意,整体变亮或者变暗,需要调整摄像头的曝光,让摄像头尽量得到比较适中的数据。
当然,比赛现场有没有,那得看命。
2.偏振片
摄像头前面那个大圆盘就是偏振片
偏振片可以清除掉特定角度的反射光线,所以我们在开头看到的灯光反射的亮斑,使用偏振片,根据实际情况调整他的角度,就可以直接滤掉。
旋转偏振片角度,滤掉某一特定角度的光
据我实际使用经验,偏振片+遮光窗帘就处理掉90%光线问题。
3.抹布
当光线效果是在不佳时,征得裁判员同意后,老老实实用抹布吧,有些情况是技术上无解的,得采取一些物理手段了。
十七届电子科技大学充电组国赛现场
到这里,我们就获取到了一帧可以直接处理的优秀图像了(二值化),具体如何提取赛道信息,提取那些信息,下期我们再讲。
希望能够帮助到一些人。
本人菜鸡一只,各位大佬发现问题欢迎留言指出。
版权归原作者 Joshua.X 所有, 如有侵权,请联系我们删除。