本篇文章为车道线检测模型系列文章的第四篇,第一篇介绍了模型所使用的单片机和开发板,第二篇介绍了实时操作系统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下半部分是摄像头实时拍摄到的画面,上半部分是经过处理的图像,红色为检测出来的车道线,因为是用电脑播放视频模拟的车道线场景,摄像头拍摄效果不太好,但算法上是可以实现的。
以上就是整个模型的实现过程了,作为一个初学者,肯定有不完善的地方,欢迎大家交流指正!