前言
前面已经记录了智能车硬件系统、方向控制以及电机控制,今天接着记录一些有关智能车摄像头相关的内容,本文主要参考opencv、图像处理以及高等数学的部分知识。
认识图像
基本含义
首先,咱来了解一下图像的基本含义,图像是人类视觉的基础,是自然事物的客观反映,“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形成的印象或认识,照片、绘画、手写汉字、心电图等都是图像(来自百度百科对“图像”一词的解释)。
图像类型
广义上,图像就是所有具有视觉效果的画面,图像根据图像记录方式的不同可分为两大类:模拟图像和数字图像。模拟图像可以通过某种物理量(如光、电等)的强弱变化来记录图像亮度信息,例如模拟电视图像;而数字图像则是用计算机存储的数据来记录图像上各点的亮度信息。
数字图像
在智能车系统中,通过摄像头对赛道信息进行采集处理,将赛道转换成由像素组成的二维排列的数字图像。(一般采用120×188的分辨率) 例如智能车灰度摄像头采集的一帧图像如下图所示(120×188): 而在单片机内部图像的存储是一个二维数组Image_Data[120][188],数组中的每个数据对应图像中该点位置的亮度信息。(这里由于数据量太大,截图不清晰,仅截取了整幅图像的部分) 为了更形象的凸显数字图像与二维数组的对应关系,这里放一张经过二值化处理后的图片: 这张图片可以清晰的看出原始图像的轮廓。 这里估计有人会有疑问,为什么智能车摄像头采集显示的是黑白图像与肉眼看见的彩色赛道不一致;为什么原始数组里面的数据也都是0-255的数值;为什么清晰显示轮廓的数组中数值只有0和1。要弄清楚这些问题,首先需要了解一下彩色图像、灰度图像和黑白图像。
彩色图像
彩色图像可以理解为一个像素点的亮度信息是由RGB三原色的不同配比表示;其中R、G、B三种颜色都被分为了0-255共256个色阶,每个像素点的信息通过三原色的色阶搭配进行表示,举例如下图该像素点的红色色阶是205,绿色色阶是89,蓝色色阶是68,所以这一个像素点的数据集合就是(205,89,68)也就是说彩色数字图像在计算机内部就是由这样一个个三原色组合而成的,做个简单的排列组合,如果要把所有的颜色用0、1、2这样的数字表示,则需要256×256×256个数,也就是一个24位的数,一个像素点就是一个24位的数据,可想一副图像的数据量会有多么庞大,显然一般单片机是没法完成这么大的数据处理的,这也就是为什么智能车中很少有人使用彩色摄像头的原因。
灰度图像
灰度图像,也就是我们智能车使用的灰度摄像头所采集那种图像,不是彩色画面,但也不是非黑即白,而是将黑色分成了0-255共256个色阶,整幅图像的每一个像素都是用0-255中间的一个数值来表示的就类似于上面提到的二维数组Image_Data[120][188]中的数据。对比彩色图像,可以发现一个像素点只需要一个8位数据表示即可,数据量相对彩色图像是不是大大减少了。
黑白图像
黑白图像就是整个图像中只有黑和白两种颜色,如下图所示:
而且图像的数据表示也只有0和1两个数,类似于上面提到的二值化后形式。 下图是彩色图像、灰度图像以及黑白图像的对比图。 这三者都是图像亮度的表示形式,彩色图像可以通过计算公式转换成灰度图像,灰度图像也可以通过二值化处理转换成黑白图像,有关灰度到黑白图像的转换下一节图像处理介绍。
彩色图像
灰度图像
黑白图像
-转换公式
- 二值化
彩色图像
灰度图像
黑白图像
小结
结合前面讲到的场中断行中断就好理解了,智能车摄像头是采集的是灰度图像,一帧图像有188×120个像素组成,每个像素是由0-255共256个色号来表达图像的亮度信息。关于图像认识就介绍到这里,智能车中使用到的一般都是灰度图像和黑白图像,有关灰度数据如何通过单片机传递给单片机,在之前的硬件篇也已经介绍智能车浅谈——硬件篇; 有关图像的详细介绍参考数字图像处理学习笔记(一)——数字图像处理概述、也可以参考百度百科关于图像的介绍。
图像处理
图像处理是信号处理在图像领域的应用,是信号处理的一个子类,与计算机科学、人工智能等领域有着密切的关系。智能车中使用到的图像均是数字图像,所以使用的理论知识也都是信号处理。数字图像处理过程中许多传统的一维信号处理方法和概念仍然适用,如降噪和量化。不同之处在于图像属于二维信号,与一维信号相比,它有其特殊的一面,处理方法和角度也不同。以下内容主要依托智能车的图像处理,有关数字图像处理的整体知识框架可以参考这篇博文——传送门。
图像压缩
上一节已经弄清楚了摄像头采集的图像是什么样子的,接下来就处理采集到的图像,前面提到过一帧图像是188×120个像素点,每个像素点是一个8位数据即0-255的数字,通过我们单片机的DMA搬运,可以得到一帧可供操作的图像Image_Data[120][188]。得到这样一个188×120的二维数组后,我们不难发现处理起来还是有些麻烦,数据量太大了,为了在保持图像特征的基础上减少数据量,我们需要进行图像压缩。 首先看一段视频 假设视频中小狗的初始图像为188×120。 经过第一次切割和组合,图片的宽度变成了原来的一半。
此时两张图片变成120×94。 经过第二次的切割和组合,图片分割成了四张,这四张的尺寸变成了60×94。 通过这个例子,不难发现,把一张高清图片按照横竖切割,重新奇偶排列后还可以得到与原图类似的图片,只是细节部分有所丢失,但是整体框架还是可以看出,智能车中的图像提取和图像压缩利用的就是此原理,只选取原数组中的奇数或者偶数行列进行重新组合得到一帧压缩的图像,图像压缩代码如下代码片 。
void Get_Use_Image(void)
{
short i = 0, j = 0, row = 0, line = 0;
for (i = 0; i < LCDH; i ++ i += 3)
{
for (j = 0; j <= LCDW; j ++)
{
Image_Use[row][line] = Image_Data[i][j];
line++;
}
line = 0;
row++;
}
}
#endif
处理前和经过图像压缩处理后得到的图像如下:
二值化
经过图像压缩后,我们为了进一步简化数据,会将灰度图像处理成黑白图像,这个转换过程使用的就是二值化;二值化见名知意就是将灰度图像中的0-255这些数据转换成0-1两个值,将原先的灰度图像转化为黑白两色图,那么怎样进行二值化呢,常见的方法有以下几种:
固定阈值法
这种方法比较好理解,即设置一个阈值Threshold(1-254),当像素数值大于这个阈值Threshold就置1,小于等于这个值就置0,这样就可以把原来的图像转化成黑白图像。
for (i = 0; i < LCDH; i++)
{
for (j = 0; j < LCDW; j++)
{
if (Image_Use[i][j] > Threshold)
Bin_Image[i][j] = 0;
else
Bin_Image[i][j] = 1;
}
}
以下同一图像不同阈值的二值化情况。 此法中阈值数值越大,显示的内容越多,较浅的图像也能显示出来,由于是固定阈值对赛道环境要求很高,小车在运行过程中的图像是在不断变化的,显然靠设置唯一阈值的方法有较大的风险。
大津法
大津法是通过获取一帧图像的灰度分布直方图,并根据直方图的波峰和波谷计算出灰度的中间值,根据中间值进行二值化处理,实现一个动态的阈值调整。想要学习这个算法的参考B站工训大魔王的【智能车制作加餐:摄像头数字图像处理算法-哔哩哔哩】 具体代码如下 。
short GetOSTU (unsigned char tmImage[LCDH][LCDW])
{
signed short i, j;
unsigned long Amount = 0;
unsigned long PixelBack = 0;
unsigned long PixelshortegralBack = 0;
unsigned long Pixelshortegral = 0;
signed long PixelshortegralFore = 0;
signed long PixelFore = 0;
float OmegaBack, OmegaFore, MicroBack, MicroFore, SigmaB, Sigma;
signed short MinValue, MaxValue;
signed short Threshold = 0;
unsigned char HistoGram[256];
for (j = 0; j < 256; j++)
HistoGram[j] = 0;
for (j = 0; j < LCDH; j++)
{
for (i = 0; i < LCDW; i++)
{
HistoGram[tmImage[j][i]]++;
}
}
for (MinValue = 0; MinValue < 256 && HistoGram[MinValue] == 0; MinValue++);
for (MaxValue = 255; MaxValue > MinValue && HistoGram[MinValue] == 0; MaxValue--);
if (MaxValue == MinValue)
return MaxValue;
if (MinValue + 1 == MaxValue)
return MinValue;
for (j = MinValue; j <= MaxValue; j++)
Amount += HistoGram[j];
Pixelshortegral = 0;
for (j = MinValue; j <= MaxValue; j++)
{
Pixelshortegral += HistoGram[j] * j;
}
SigmaB = -1;
for (j = MinValue; j < MaxValue; j++)
{
PixelBack = PixelBack + HistoGram[j];
PixelFore = Amount - PixelBack;
OmegaBack = (float) PixelBack / Amount;
OmegaFore = (float) PixelFore / Amount;
PixelshortegralBack += HistoGram[j] * j;
PixelshortegralFore = Pixelshortegral - PixelshortegralBack;
MicroBack = (float) PixelshortegralBack / PixelBack;
MicroFore = (float) PixelshortegralFore / PixelFore;
Sigma = OmegaBack * OmegaFore * (MicroBack - MicroFore) * (MicroBack - MicroFore);
if (Sigma > SigmaB)
{
SigmaB = Sigma;
Threshold = j;
}
}
return Threshold;
}
实际效果如下:
图像降噪(腐蚀)
经过二值化处理后,整个数组内可能会出现一下孤立的小白点,如下图所示,这会对我们后面的巡线造成干扰,所以需要进行降噪处理,思路就是,判断一个像素点周围的数值是否与其一致,如果 周围像素点的数据都与此点不一致则此点将被修改为与周围相同的数值。 降噪代码如下: 。
void Bin_Image_Filter (void)
{
sint16 nr;
sint16 nc;
for (nr = 1; nr < LCDH - 1; nr++)
{
for (nc = 1; nc < LCDW - 1; nc = nc + 1)
{
if ((Bin_Image[nr][nc] == 0)
&& (Bin_Image[nr - 1][nc] + Bin_Image[nr + 1][nc] + Bin_Image[nr][nc + 1] + Bin_Image[nr][nc - 1] > 2))
{
Bin_Image[nr][nc] = 1;
}
else if ((Bin_Image[nr][nc] == 1)
&& (Bin_Image[nr - 1][nc] + Bin_Image[nr + 1][nc] + Bin_Image[nr][nc + 1] + Bin_Image[nr][nc - 1] < 2))
{
Bin_Image[nr][nc] = 0;
}
}
}
}
处理前与处理后对比图如下所示:
寻边线
经过上述一系列的简化处理后,我们就可以正式开始赛道的识别处理了,首先需要根据图像建立一个坐标系,根据坐标系内的图像需要提取出赛道信息,由图像可以很直观地想到一种寻找边线的办法,就是寻找每行的黑白跳变点,也就是0-1跳变的位置,根据这个跳变点我们可以找到整个赛道的左右边线(理想情况下),然后根据两个边线的横坐标就可以得到中线位置, 中线=(左边线横坐标+右边线横坐标)/2 再根据中线位置与理论中值进行比较就可以得到偏差,进而控制舵机打角,利用拟合、补线、求斜率、曲率(此处可以用高数求曲率的思路)等方式来获取偏差进行计算实现控制,还有特殊元素的识别判断处理这都是需要自己去编写代码的,网上有很多类似的文章介绍,笔者推荐一篇来自博主温水很好喝的第十六届全国大学生智能汽车比赛—摄像头算法控制总结。 以下是赛道经过大津法二值化,以及巡线补线后的效果。(下面的效果采用的是乾勤科技的开源代码,平台是VS2019) 最后放一些笔者自己用RT1064按上述处理后的效果: 程序大体框架如下 :
int main(void)
{
All_Init();
while(1)
{
All_Module_ProDeal();
Usart1_Command_Handle();
Display();
Key_Action();
while(1)
{
if(delay_10ms_Arrive)
{
delay_10ms_Arrive = 0;
break;
}
}
}
}
void All_Module_ProDeal(void)
{
static u16 time_out=0;
if(mt9v03x_finish_flag)
{
Get_Use_Image();
Get_Bin_Image(3);
Bin_Image_Filter();
for(int i=0;i<60;i++)
{
bord_L[i]=0;
bord_R[i]=0;
}
FindBorder_L(59,20,45,0,WHITE_MY);
FindBorder_R(59,20,45,80,WHITE_MY);
CalculateCentralLine(60,20,WHITE_MY);
OLED_Road(LCDH,LCDW,(unsigned char *) Bin_Image);
mt9v03x_finish_flag = 0;
}
if(BasicArea.XIn[STOP] == 0)
{
if(run_bit == 0)
{
switch(step)
{
case Go_Stragist:
{
BasicArea.Kp=0;
BasicArea.Ki=1;
if(TIMER_1ms(time_out,3500))
{
user_printf(USART1,"Cylinder=1,front,NG\r\n");
step=WAIT;
}
if(BasicArea.XIn[SENSOR_DOWN]==0)
{
user_printf(USART1,"Cylinder=1,front,OK\r\n");
step=WAIT;
}
break;
}
case Round_About:
{
BasicArea.YOut[CY_UP]=1;
BasicArea.YOut[CY_DOWN]=0;
if(TIMER_1ms(time_out,3500))
{
user_printf(USART1,"Cylinder=1,back,NG\r\n");
step=WAIT;
}
if(BasicArea.XIn[SENSOR_UP]==0)
{
user_printf(USART1,"Cylinder=1,back,OK\r\n");
step=WAIT;
}
break;
}
case Cross_Shaped:
{
BasicArea.YOut[CY_L_UP]=1;
if(TIMER_1ms(time_out,3500))
{
user_printf(USART1,"Cylinder=2,front,NG\r\n");
step=WAIT;
}
if(BasicArea.XIn[L_UP_SENSOR_FRONT]==0)
{
user_printf(USART1,"Cylinder=2,front,OK\r\n");
step=WAIT;
}
break;
}
case Access_Road:
{
BasicArea.YOut[CY_L_UP]=0;
if(TIMER_1ms(time_out,3500))
{
user_printf(USART1,"Cylinder=2,back,NG\r\n");
step=WAIT;
}
if(BasicArea.XIn[L_UP_SENSOR_BACK]==0)
{
user_printf(USART1,"Cylinder=2,back,OK\r\n");
step=WAIT;
}
break;
}
case :
{
if(TIMER_1ms(time_out,3500))
{
step=WAIT;
}
if(BasicArea.XIn[L_DOWN_SENSOR_FRONT]==0)
{
step=WAIT;
}
break;
}
case WAIT:
{
if(直道条件满足 && step == WAIT)
{
step=Go_Stragist;
}
else if(环岛条件满足 && step == WAIT)
{
step=Round_About;
}
else if(...)
{
...
}
else
{
}
time_out=_TIMER_1MS;
break;
}
default:
break;
}
}
}
else
{
if(run_bit==0)
{
run_bit=1;
}
}
}
总结
智能车比赛已经举办了十六届,网上各式各样的资料很多,大家自己平时多留意,在此祝愿各位参赛者都能取得好成绩。有关图像处理的一些高级算法,例如卷积、soble边沿检测算子、八领域、四邻域这些方法都是很厉害也是很好用的,但是笔者能力有限,这方面就不再做分析了,而且不一定非要按照笔者上述流程去操作处理图像,之前逐飞科技就出过一个用灰度识别处理赛道的方法,效果也是很好,这里大家可以参考,链接奉上 欢迎大佬来沟通交流。
|