单片机/MCU论坛
直播中

顾吾恋之

5年用户 7经验值
擅长:可编程逻辑,嵌入式技术,EDA/IC设计,接口/总线/驱动,控制/MCU
私信 关注
[文章]

智能车竞赛浅谈——图像篇

前言

本文主要记录一点有关智能车摄像头相关的内容,供入门智能车的同学一个参考,主要参考opencv、图像处理的部分知识来分析描述。

认识图像

基本含义

首先,咱来了解一下图像的基本含义,图像是人类视觉的基础,是自然事物的客观反映,“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形成的印象或认识,照片、绘画、手写汉字、心电图等都是图像(来自百度百科对“图像”一词的解释)。

图像类型

广义上,图像就是所有具有视觉效果的画面,图像根据图像记录方式的不同可分为两大类:模拟图像数字图像。模拟图像可以通过某种物理量(如光、电等)的强弱变化来记录图像亮度信息,例如模拟电视图像;而数字图像则是用计算机存储的数据来记录图像上各点的亮度信息。[^1]

数字图像

在智能车系统中,通过摄像头对赛道信息进行采集处理,将赛道转换成由像素组成的二维排列的数字图像。(一般采用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。
在这里插入图片描述
在这里插入图片描述
通过这个例子,不难发现,把一张高清图片按照横竖切割,重新奇偶排列后还可以得到与原图类似的图片,只是细节部分有所丢失,但是整体框架还是可以看出,智能车中的图像提取和图像压缩利用的就是此原理,只选取原数组中的奇数或者偶数行列进行重新组合得到一帧压缩的图像,图像压缩代码如下代码片

// An highlighted block
/*************************************************************************
 *  函数名称:void Get_Use_Image (void)
 *  功能说明:把摄像头采集到原始图像,缩放到赛道识别所需大小
 *  参数说明:无
 *  函数返回:无
 *  修改时间:2020年10月28日
 *  备    注:  IMAGEW为原始图像的宽度,神眼为188,OV7725为320
 *       IMAGEH为原始图像的高度,神眼为120,OV7725为240
 *************************************************************************/
void Get_Use_Image(void)
{
    short i = 0, j = 0, row = 0, line = 0;

    for (i = 0; i < IMAGEH; i += 2)          //神眼高 120 / 2  = 60,
    // for (i = 0; i < IMAGEH; i += 3)       //OV7725高 240 / 3  = 80,
    {
        for (j = 0; j <= IMAGEW; j += 2)     //神眼宽188 / 2  = 94,
        // for (j = 0; j <= IMAGEW; j += 3)  //OV7725宽320 / 3  = 106,
        {
            Image_Use[row][line] = Image_Data[i][j];
            line++;
        }
        line = 0;
        row++;
    }
}

处理前和经过图像压缩处理后得到的图像如下:
在这里插入图片描述

在这里插入图片描述

二值化

经过图像压缩后,我们为了进一步简化数据,会将灰度图像处理成黑白图像,这个转换过程使用的就是二值化;二值化见名知意就是将灰度图像中的0-255这些数据转换成0-1两个值,将原先的灰度图像转化为黑白两色图,那么怎样进行二值化呢,常见的方法有以下几种:

固定阈值法

这种方法比较好理解,即设置一个阈值Threshold(1-254),当像素数值大于这个阈值Threshold就置1,小于等于这个值就置0,这样就可以把原来的图像转化成黑白图像。

// An highlighted block
 /* 二值化 */
    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站工训大魔王的【智能车制作加餐:摄像头数字图像处理算法-哔哩哔哩】 具体代码如下

// An highlighted block
/*************************************************************************
 *  函数名称:short GetOSTU (unsigned char tmImage[LCDH][LCDW])
 *  功能说明:大津法求阈值大小
 *  参数说明:tmImage : 图像数据
 *  函数返回:无
 *  修改时间:2011年10月28日
 *  备    注:  GetOSTU(Image_Use);//大津法阈值
Ostu方法又名最大类间差方法,通过统计整个图像的直方图特性来实现全局阈值T的自动选取,其算法步骤为:
1) 先计算图像的直方图,即将图像所有的像素点按照0~255共256个bin,统计落在每个bin的像素点数量
2) 归一化直方图,也即将每个bin中像素点数量除以总的像素点
3) i表示分类的阈值,也即一个灰度级,从0开始迭代 1
4) 通过归一化的直方图,统计0~i 灰度级的像素(假设像素值在此范围的像素叫做前景像素) 所占整幅图像
        的比例w0,        并统计前景像素的平均灰度u0;统计i~255灰度级的像素(假设像素值在此范围的像素叫做背
        景像素)  * 所占整幅图像的比例w1,并统计背景像素的平均灰度u1;
5) 计算前景像素和背景像素的方差 g = w0*w1*(u0-u1) (u0-u1)
6) i++;转到4),直到i为256时结束迭代
7) 将最大g相应的i值作为图像的全局阈值
缺陷:OSTU算法在处理光照不均匀的图像的时候,效果会明显不好,因为利用的是全局像素信息。
*************************************************************************/
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)                    //遍历最大的类间方差g //找出最大类间方差以及对应的阈值
        {
            SigmaB = Sigma;
            Threshold = j;
        }
    }
    return Threshold;                        //返回最佳阈值;
}

实际效果如下:
在这里插入图片描述

图像降噪(腐蚀)

经过二值化处理后,整个数组内可能会出现一下孤立的小白点,如下图所示,这会对我们后面的巡线造成干扰,所以需要进行降噪处理,思路就是,判断一个像素点周围的数值是否与其一致,如果 周围像素点的数据都与此点不一致则此点将被修改为与周围相同的数值。
在这里插入图片描述
降噪代码如下:

// An highlighted block
/*---------------------------------------------------------------
 【函    数】Bin_Image_Filter
 【功    能】过滤噪点
 【参    数】无
 【返 回 值】无
 【注意事项】
 ----------------------------------------------------------------*/
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按上述处理后的效果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
程序大体框架如下

// An highlighted block
int main(void)
{
	All_Init();
	while(1)
	{
		// IWDG_Feed();  				//如果WK_UP按下,则喂狗
        All_Module_ProDeal();           //所有的动作
        Usart1_Command_Handle();		//串口控制
		Display();						//显示函数
		Key_Action();					//按键操作函数 
		//保证每次while循环的时间固定,消除程序运行时的时序混乱 		
        while(1)
        {
            if(delay_10ms_Arrive)		//定时10ms到达标志位
            {
                delay_10ms_Arrive = 0;	//定时10ms到达标志位
                break;
            }
        }
	}	
}

/*******************************************************************************
* 函数名		:All_Module_ProDeal
* 描述			:各动作模块过程处理
* 参数			:无
* 返回			:无
* 编写者		:
* 编写日期	:2022-01-12
*******************************************************************************/
void All_Module_ProDeal(void)
{
	static u16 time_out=0;
	if(mt9v03x_finish_flag)
    {
		Get_Use_Image();     // 取出赛道及显示所需图像数据
		Get_Bin_Image(3);    // 转换为01格式数据,0、1原图;2、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);//显示到OLED上
		mt9v03x_finish_flag = 0;
	}
	if(BasicArea.XIn[STOP] == 0) //停车标志位没到
	{
		if(run_bit == 0)   //小车动作
		{
		  switch(step)       //根据状态执行动作
		  {
			case Go_Stragist:		// 直行
			{
				//直行的操作
				//例如修改PID参数
				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边沿检测算子、八领域、四邻域这些方法都是很厉害也是很好用的,但是笔者能力有限,这方面就不再做分析了,而且不一定非要按照笔者上述流程去操作处理图像,之前逐飞科技就出过一个用灰度识别处理赛道的方法,效果也是很好,这里大家可以参考,链接奉上
欢迎大佬来沟通交流。
如果文章对你的比赛有所帮助,而且赛后对此文还有映像,笔者在评论区等待你们的分享。

智能车系列文章汇总

智能车浅谈——硬件篇
智能车浅谈——方向控制篇
智能车浅谈——电机控制篇
智能车浅谈——图像篇
智能车浅谈——控制规律篇
智能车浅谈——过程通道篇
智能车浅谈——抗干扰技术硬件篇
智能车浅谈——抗干扰技术软件篇
智能车浅谈——手把手让车跑起来(电磁篇)
智能车浅谈 电磁组——环岛处理
第十七届智能车越野硬件篇——无刷电机驱动
无刷驱动设计——浅谈MOS驱动电路
芯源&立创EDA训练营——无刷电机驱动

回帖(4)

王栋春

2022-9-24 22:11:10
楼主的技术非常到位,学习了
举报

王栋春

2022-9-24 22:11:14
楼主的技术非常到位,学习了
举报

chunhuahua

2022-9-28 11:58:42
楼主可以开个专栏了
举报

lihnj

2022-10-1 10:12:17
淘宝入驻企业店铺,长期有单,诚聘兼职:STM32,STC51,QT,LabVIEW,JAVA,C#,C++等有意+V:WX1452679
举报

更多回帖

发帖
×
20
完善资料,
赚取积分