单片机车道线检测模型(4)——图像处理算法

本篇文章为车道线检测模型系列文章的第四篇,第一篇介绍了模型所使用的单片机和开发板,第二篇介绍了实时操作系统RTOS,第三篇介绍了所用到的摄像头和LCD触摸屏外设,想了解的朋友点击:
(一)https://blog.csdn.net/weixin_42967006/article/details/106687245
(二)https://blog.csdn.net/weixin_42967006/article/details/106688601
(三)https://blog.csdn.net/weixin_42967006/article/details/106690883
本篇将为大家介绍本项目中最重要,也是难度最大的部分,图像预处理及车道线检测。共分为图像滤波、边缘检测、灰度图像二值化和霍夫变换检测直线四个步骤

一、图像滤波

为了消除图像中的尖锐噪点,在图像处理的初始阶段要进行滤波。常见的滤波方式有均值滤波、中值滤波、高斯滤波等。这里先介绍一下比较简单的均值滤波和中值滤波。

1.均值滤波

如下图所示,选取一个n * n的滤波核(n最好为单数,下图中是3 * 3),从图像一个角开始有序依次滑过整个图像,每次将滤波核中心点的值置为滤波核内所有值的平均值,即完成了均值滤波。
在这里插入图片描述
均值滤波以一定程度的图像模糊为代价,来消除图像中的噪点,滤波核越大,模糊越严重,消除噪点效果也越好。不同滤波核的效果参考下图:
在这里插入图片描述

2.中值滤波

中值滤波大体思想和均值滤波相同,只不过把取平均值换成了取中值,相比均值滤波来说不会让图片变得过于模糊,而且滤波效果也不错,但因为每滑动一次都要进行排序,所以比较耗时间。均值滤波和中值滤波的效果对比图如下(本小节的几幅图均来自北京理工大学在中国大学MOOC上发布的《无人驾驶车辆》课程):
在这里插入图片描述

3.C代码实现

下面代码中取均值和取中值的函数都给了出来,可根据具体情况进行选择。我这次用的均值滤波,相对来说运算量小一点。代码中需要注意的一点是Filter函数参数中的FilterLength指 n * n 滤波核中的n,而GetMedianNum函数和GetAverageNum函数中的iFilterLen指 n * n。

unsigned char GetMedianNum(u8 * bArray, int iFilterLen)
{
	int i,j;// 循环变量
	unsigned char bTemp;
	// 用冒泡法对数组进行排序
	for (i = 0; i < iFilterLen - 1; i ++)
	{
    for(j = 0;j < iFilterLen - i - 1; j ++)
			if (bArray[j] > bArray[j + 1])
			{
				// 互换
				bTemp = bArray[j];
				bArray[j] = bArray[j + 1];
				bArray[j + 1] = bTemp;
			}
	}
	// 计算中值
	if ((iFilterLen & 1) > 0)
	{
		// 数组有奇数个元素,返回中间一个元素
		bTemp = bArray[(iFilterLen + 1) / 2];
	}
	else
	{
		// 数组有偶数个元素,返回中间两个元素平均值
		bTemp = (bArray[iFilterLen / 2] + bArray[iFilterLen / 2 + 1]) / 2;
	}

	return bTemp;
}

unsigned char GetAverageNum(u8 * bArray, int iFilterLen)
{
	u32 sum;
    int i;
	for (i = 0; i < iFilterLen; i ++)
	{
    	sum += bArray[i];
	}
 	return sum/iFilterLen;
}

void Filter(u8 * pic,u16 W,u16 H, u16 FilterLength)
{
	u8 i,j;// 循环变量
	u8 m,n;
	u8* bTemp;//滤波器内存
	 bTemp = rt_malloc(FilterLength * FilterLength);
	 if(!bTemp)
	 {
	   rt_kprintf("Get filter memory FAILED!\n");
	   return;
	 }
	 for(i=0;i<H-FilterLength;++i)
	 {
	   for(j=0;j<W-FilterLength;++j)
	   {
	     for(m=0;m<FilterLength;++m)
	     {
	       for(n=0;n<FilterLength;++n)
	       {
	         *(bTemp+m*FilterLength+n) = *(pic+(m+i)*W+n+j);
	       }
	     }
	//      *(pic+i*W+j) = GetMedianNum(bTemp, FilterLength*FilterLength);//中值滤波
	     *(pic+i*W+j) = GetAverageNum(bTemp, FilterLength*FilterLength);//均值滤波
	   }
	 }
	 rt_free(bTemp);
	 bTemp = NULL;
}

二、边缘检测

边缘检测就是指将图像中亮度梯度变化较大的边缘提取出来,其余信息抹去。图像的边缘中包含了很多有用的信息,准确的边缘提取至关重要。边缘检测常用的有以下两个算子:
(1)Prewitt算子
在这里插入图片描述
(2)Sobel算子
在这里插入图片描述
本次采用的是Sobel算子,Sobel算子提取出来的边缘粗大明亮,通常情况下提取边缘后还会加一步非极大值抑制,能去除边缘周围的发光区域,但这次单片机性能确实有限,权衡之后没有做非极大值抑制。
以下是边缘检测的代码:

Sobel算子边缘检测
void EdgeDetect(unsigned char* pBmpBuf,unsigned char* pBmpBuf2,int lineByte,int width,int height)
{
  int ul, uc, ur, dl, dc, dr;
	int lu, lc, ld, ru, rc, rd;
	double hir, vec;
	for (int i = 1; i < height - 1; ++i){
		for (int j = 1; j < width - 1; ++j){
			// 垂直梯度算子:检测水平边缘
			vec = 0;
			ul = *(pBmpBuf + (i + 1)*lineByte + (j - 1))*(-1);
			uc = *(pBmpBuf + (i + 1)*lineByte + j)*(-2);
			ur = *(pBmpBuf + (i + 1)*lineByte + j)*(-1);
			dl = *(pBmpBuf + (i - 1)*lineByte + (j - 1)) * 1;
			dc = *(pBmpBuf + (i - 1)*lineByte + j) * 2;
			dr = *(pBmpBuf + (i - 1)*lineByte + j) * 1;
			vec = ul + uc + ur + dl + dc + dr;
			// 水平梯度算子:检测垂直边缘
			hir = 0;
			lu = *(pBmpBuf + (i + 1)*lineByte + (j - 1))*(-1);
			lc = *(pBmpBuf + (i - 0)*lineByte + (j - 1))*(-2);
			ld = *(pBmpBuf + (i - 1)*lineByte + (j - 1))*(-1);
			ru = *(pBmpBuf + (i + 1)*lineByte + (j + 1)) * 1;
			rc = *(pBmpBuf + (i - 0)*lineByte + (j + 1)) * 2;
			rd = *(pBmpBuf + (i - 1)*lineByte + (j + 1)) * 1;
			hir = lu + lc + ld + ru + rc + rd;
			*(pBmpBuf2+i*lineByte+j) = round(sqrt(hir*hir + vec*vec));
		}
	}
}

三、灰度图像二值化

经过边缘检测后,图像中的边缘信息高亮显示,在此基础上再进行二值化,可进一步将图像中不重要的信息去除,只留下亮度高的边缘。二值化的软件实现较为简单,设置一个阈值,然后将亮度值与阈值进行比较,大于阈值的置为最亮,小于阈值的置为最暗,就可以得到二值化的图像。本次取的阈值为120,在能够检测出车道线的情况下,阈值越大越能提高运行效率。代码如下:

void Image_YUV2Bitmap(u8*InputImageBuf,u8* OutputImageBuf,int width,int height,u8 threshold)
{
  int i;
  for(i=0;i<width*height;++i)
  {
    if(*(InputImageBuf+i) >= threshold)
      *(OutputImageBuf+i) = 0xFF;
    else
      *(OutputImageBuf+i) = 0;
  }
}

四、霍夫变换检测直线

在进行完以上的图像预处理后,就可以开始检测直线了。

检测直线用到的是霍夫变换,简单来讲,在直角坐标系中,表示一条直线可以用:y = kx + b,对于一条直线有且只有一条过原点的垂线,这条垂线可以用(θ, r)来表示(如下图),因此,直角坐标系中的任意一条直线都与一组固定(θ, r)相对应。其实这组(θ, r)就是极坐标中的一个点,也就是说,直角坐标系中的一条直线在极坐标系中可以用一个点来表示。直角坐标系转换成极坐标系算式如下:
r=(x* cos(π * θ /180)+y * sin(π * θ /180))(0<θ<180);

在这里插入图片描述
在这个理论的基础上,我们先来分析两个点确定一条直线的情况:将过直角坐标系中一个点的所有直线的(θ, r)求出来进行存储,然后再求过另一个点的所有直线的(θ, r),因为两个点必然能确定一条直线,所以一定会存在一组(θ, r)与之前存储的值重合,我们找出这组(θ, r),就可以求得与之相对应的直线,也就是这两个点所确定的直线。但理论上过一点可以画出无数条直线,实际计算过程中没办法实现,我们可以设置一个分辨率,每隔几度取一条直线。

将两个点确定一条直线的情况扩展到求三个点是否在同一条直线上:同样求出过三个点的所有直线的(θ, r)值,如果存在一组(θ, r)出现了三次,那么三个点就在同一条直线上,否则不在同一直线上。

再进一步扩展到求图像中的最长直线:将过直角坐系上所有点的所有直线都转化成极坐标系中的点,我们存储所有(θ, r)的出现次数,最后找出出现次数最多的(θ, r)值,所对应的直线就是图像中最长的直线。

经过上面的分析我们可以看出来直线检测的计算量是相当大的,这也是整个流程中最耗时间和内存的一个环节。具体实现代码如下:
程序中的变量与上文描述中变量的对应关系:p=r,k=θ
实际计算过程中,为了提高检测准确率和速度,我们可以融合一些先验知识来限定直线检测的范围,比如车道线通常出现在图像下半部分,那上半部分就不参与计算;车道线通常是左右两条,给每一条的角度分别划定一个检测范围,可以不用遍历 0°到 180°,节省时间。这些在代码中都有体现,就不详细展开说了。

#define pi 3.14159265
#define width   240                 //图像宽度
#define height  160                 //图像高度

#define ImgWBgn 0                   //参与计算的图像宽度起始
#define ImgWEd  width               //参与计算的图像宽度结束
#define ImgHBgn 0                   //参与计算的图像高度起始,原图像的40%处开始计算
#define ImgHEd  (height/10*6)       //参与计算的图像高度结束

#define k_begin_1 20                //融合先验知识,定义两条车道线所在的角度范围
#define k_end_1   80
#define k_begin_2 100
#define k_end_2   160
#define k_resolution 5              //极坐标下检测线段的角度分辨率

#define mk_1  (k_end_1-k_begin_1)     //极坐标下角度最大值
#define mk_2  (k_end_2-k_begin_2)     //极坐标下角度最大值
#define mp    288                   //极坐标下长度最大值,即参与计算的图像对角线长度,
                                    //=sqrt((ImgHEd-ImgHBgn)*(ImgHEd-ImgHBgn)+(ImgWEd-ImgWBgn)*(ImgWEd-ImgWBgn))
#define TwoDArrayGet(Array,x,y,xLen)	(*((u8*)(Array+x*xLen+y)))
int HoughLineDetect(u8* Imagebuf)
{
  int i,j;
  int p,k;
	int threshold;
  u8* PointBuf1;
  u8* PointBuf2;
//#define PointBuf1_get(k,p)  (*(PointBuf1+k*mk_1+p))

	int kmax_1,pmax_1,kmax_2,pmax_2;

  PointBuf1=(u8*)rt_malloc(mk_1 * mp);
  if(!PointBuf1)
    return 1;
  PointBuf2=(u8*)rt_malloc(mk_2 * mp);
  if(!PointBuf2)
  {
    rt_free(PointBuf1);
    return 2;
  }
  rt_memset(PointBuf1,(u8)0,mk_1*mp);
  rt_memset(PointBuf2,(u8)0,mk_2*mp);

  for(i=ImgHBgn;i<ImgHEd;++i)
  {
    for(j=ImgWBgn;j<ImgWEd;++j)
    {
      if(Imagebuf[i*width+j]==0xFF)
      {
        for(k=k_begin_1;k<k_end_1;k+=k_resolution)
        {
          p=(int)(i*cos(pi*k/180)+j*sin(pi*k/180));//p hough变换中距离参数
          p=(int)(p/2+mp/2); //对p值优化防止为负
          TwoDArrayGet(PointBuf1,k,p,mk_1)+=1; //对变换域中对应重复出现的点累加
//          rt_kprintf("P=%X,V=%02X\n",(u32)(PointBuf1+k*mk_1+p),TwoDArrayGet(PointBuf1,k,p,mk_1));
        }
        for(k=k_begin_2;k<k_end_2;k+=k_resolution)
        {
          p=(int)(i*cos(pi*k/180)+j*sin(pi*k/180));//p hough变换中距离参数
          p=(int)(p/2+mp/2); //对p值优化防止为负
          TwoDArrayGet(PointBuf2,k,p,mk_2)+=1; //对变换域中对应重复出现的点累加
        }
      }
    }
  }

  //在范围1内寻找最长直线
  kmax_1=0; //最长直线的角度
  pmax_1=0; //最长直线的距离
  threshold = 0;
  for(i=k_begin_1;i<k_end_1;i+=k_resolution)
  {
    for(j=1;j<mp;j++) //mp为原图对角线距离
    {
//      rt_kprintf("P=%X,V=%02X\n",(u32)(PointBuf1+i*mk_1+j),TwoDArrayGet(PointBuf1,i,j,mk_1));
      if(TwoDArrayGet(PointBuf1,i,j,mk_1)>threshold) //找出最长直线 threshold为中间变量用于比较
      {
        threshold=TwoDArrayGet(PointBuf1,i,j,mk_1);
        kmax_1=i; //记录最长直线的角度
        pmax_1=j; //记录最长直线的距离
      }
    }
  }
  //在范围2内寻找最长直线
  kmax_2=0; //最长直线的角度
  pmax_2=0; //最长直线的距离
  threshold = 0;
  for(i=k_begin_2;i<k_end_2;i+=k_resolution)
  {
    for(j=1;j<mp;j++) //mp为原图对角线距离
    {
      if(TwoDArrayGet(PointBuf2,i,j,mk_2)>threshold) //找出最长直线 threshold为中间变量用于比较
      {
        threshold=TwoDArrayGet(PointBuf2,i,j,mk_2);
        kmax_2=i; //记录最长直线的角度
        pmax_2=j; //记录最长直线的距离
      }

    }
  }

  for(i=ImgHBgn;i<ImgHEd;++i)
  {
    for(j=ImgWBgn;j<ImgWEd;++j)
    {
      if(Imagebuf[i*width+j]==0xFF)
      {
        p=(int)(i*cos(pi*kmax_1/180)+j*sin(pi*kmax_1/180));//pi=3.1415926
        p=(int)(p/2+mp/2); //mp为原图对角线距离
        if(p==pmax_1)
          TwoDArrayGet(Imagebuf,i,j,width)=0x01; //将原图中的直线像素点更改颜色值
        p=(int)(i*cos(pi*kmax_2/180)+j*sin(pi*kmax_2/180));//pi=3.1415926
        p=(int)(p/2+mp/2); //mp为原图对角线距离
        if(p==pmax_2)
          TwoDArrayGet(Imagebuf,i,j,width)=0x01; //将原图中的直线像素点更改颜色值
      }
    }
  }

  rt_free(PointBuf1);
  rt_free(PointBuf2);
  return 0;
}

经过直线检测后,图像中的像素点变成了最亮(0xFF)、最暗(0x00)和直线(0x01),显示的时候再稍加处理即可:

#define ColorCvt(YUV)  (YUV==0 ? BLACK:(YUV==0x01?RED:WHITE))

最后,放一张最终结果图,LCD下半部分是摄像头实时拍摄到的画面,上半部分是经过处理的图像,红色为检测出来的车道线,因为是用电脑播放视频模拟的车道线场景,摄像头拍摄效果不太好,但算法上是可以实现的。
在这里插入图片描述
在这里插入图片描述
以上就是整个模型的实现过程了,作为一个初学者,肯定有不完善的地方,欢迎大家交流指正!

  • 13
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老孟的孟不是很老的孟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值