【一文读懂】车道线检测——传统方法图像处理C++


前言

基于传统数字图像处理方法的车道线检测项目,除图像输入输出外,不调用任何库
源代码已经上传至github:TommyGong08
如果对你有帮助的话,记得follow和star~


一、图像处理流程

图像按照以下流程处理:

  1. 输入图片
  2. 转灰度图
  3. 直方图均衡化
  4. 高斯滤波
  5. 二值化
  6. 边缘平滑
  7. 去除小区域
  8. 闭运算
  9. 霍夫变换

二、具体步骤

1.图片输入

使用 cv::imread()函数出入图片,图片类型为 cv::Mat。同时还能获得图片的长宽信息。

cv::Mat image = cv::imread(src);
int width, height = 0;
height = image.rows;
width = image.cols;

执行 imread()语句后,能够使用 imshow()函数显示图片,验证是否正确获取图片。

2.转为灰度图像

调用 myRGB2GRAY()函数,传入参数为原始图片 image,图像宽度 width 和高度 height。
myRGB2GRAY()函数具体原理为将 image 图像的 RGB 三个通道拆分,为每个像素点的 RGB 值进行相应的运算,得到灰度值。运算公式为 gray = 0.1140 * blue+ 0.5870 * green + 0.2989 * red。函数具体代码如下:

void myRGB2GRAY(cv::Mat& src, cv::Mat& gray, int width, int height)
{
	vector<cv::Mat> channels;
	cv::split(src, channels);
	cv::Mat red, green, blue;
	blue = channels.at(0);
	green = channels.at(1);
	red = channels.at(2);
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
				gray.at<uchar>(i, j) = blue.at<uchar>(i, j) * 0.1140 +
				green.at<uchar>(i, j) * 0.5870 + red.at<uchar>(i, j) * 0.2989;
		}
	}
	//cv::imshow("gray image", gray_img);
	red.release();
	green.release();
	blue.release();
}

在这里插入图片描述

3. 直方图均衡化

转为灰度图像后,对灰度图进行直方图均衡化,能够使得灰度分布更均匀,避免原始图像颜色分布不均匀对我们的图像处理造成的干扰。
直方图均衡化的实现分为四个步骤:统计每个灰度下的像素个数、统计灰度频率、统计累计密度、重新计算灰度之后的值。
具体代码如下所示:

void myHist(cv::Mat& src, cv::Mat& dst)
{
	int height, width = 0;
	height = src.rows;
	width = src.cols;
	int gray[256] = { 0 }; //记录每个灰度级别下的像素个数
	double gray_prob[256] = { 0 }; //记录灰度分布密度
	double gray_distribution[256] = { 0 }; //记录累计密度
	int gray_new[256] = { 0 }; //均衡化后的灰度值
	int value;
	int sum = width * height;
	//统计每个灰度下的像素个数
	for (int i = 0; i < height; i++)
	{
	uchar* p = src.ptr<uchar>(i);
	for (int j = 0; j < width; j++)
	{
	value = p[j];
	gray[value]++;
	}
}
//统计灰度频率
for (int i = 0; i < 256; i++)
{
	gray_prob[i] = ((double)gray[i] / sum);
}
//计算累计密度
gray_distribution[0] = gray_prob[0];
for (int i = 1; i < 256; i++)
{
	gray_distribution[i] = gray_prob[i] + gray_distribution[i - 1];
}
//重新计算均衡化后的灰度值,四舍五入
for (int i = 0; i < 256; i++)
{
	gray_new[i] = round(gray_distribution[i] * 255);
}
for (int i = 0; i < width; i++)
{
	uchar* p = dst.ptr<uchar>(i);
	for (int j = 0; j < height; j++)
	{
		p[j] = gray_new[p[j]];
	}
}
src.copyTo(dst);
src.release();
}

在这里插入图片描述

4. 高斯模糊

对直方图均衡化之后的灰度图调用 myGaussian 函数进行高斯模糊,设置卷积核 kernel 的大小 ksize = 3,sigma = 1.5。具体代码如下所示:

void myGaussian(cv::Mat& src, cv::Mat& dst, int ksize, int sigma)
{
	if (!src.data) return;
	double** arr;
	cv::Mat temp(src.size(), src.type());
	for (int i = 0; i < src.rows; ++i)
	for (int j = 0; j < src.cols; ++j) {
	//边缘不进行处理
	if ((i - 1) > 0 && (i + 1) < src.rows && (j - 1) > 0 && (j + 1) <src.cols) {
	arr = getGuassionArray(ksize, sigma);
	temp.at<uchar>(i, j) = 0;
	for (int x = 0; x < 3; ++x) {
		for (int y = 0; y < 3; ++y) {
		temp.at<uchar>(i, j) += arr[x][y] * src.at<uchar>(i + 1 -x, j + 1 - y);}
		}
	}
}
temp.copyTo(dst);
temp.release();
}

在这里插入图片描述

5. 二值化

高斯模糊之后,调用 myGaus2Binary()函数对图像进行二值化处理。二值化处理分为以下几步:①提取 ROI,将纵坐标 350 及以下的部分设置为黑色,因为该部分车道线较少,这样设置能过够减少计算量。②将灰度值大于 150 小于 255部分设置为白色。具体代码如下所示:

void myGaus2Binary(cv::Mat& src, cv::Mat& dst)
{
	if (!src.data) return;
	cv::Mat temp(src.size(), src.type());
	for (int i = 0; i < src.rows; i++)
	{
		for (int j = 0; j < src.cols; j++)
		{
			if (i >= 0 && i <= 350)
			{
				temp.at<uchar>(i, j) = 0;
			}else{
			if(src.at<uchar>(i, j) > 150 && src.at<uchar>(i, j) < 255)
			{
				temp.at<uchar>(i, j) = 255;
			}
			else temp.at<uchar>(i, j) = 0;
		}
	}
}
temp.copyTo(dst); 
temp.release();
}

在这里插入图片描述

6. 边缘平滑

得到二值图之后,调用 MedianFlitering()函数进行边缘平滑,边缘平滑的原理为中值滤波。对每个像素点,取它以及周围的八邻域的中值取代它,从而实现边缘平滑。

//中值滤波函数
void MedianFlitering(const Mat& src, Mat& dst)
{
	if (!src.data)return;
	Mat _dst(src.size(), src.type());
	for (int i = 0; i < src.rows; ++i)
	for (int j = 0; j < src.cols; ++j) {
	if ((i - 1) > 0 && (i + 1) < src.rows && (j - 1) > 0 && (j + 1) <
	src.cols) {
	_dst.at<uchar>(i, j) = Median(src.at<uchar>(i, j), src.at<uchar>(i + 1, j + 1),
	src.at<uchar>(i + 1, j), src.at<uchar>(i, j + 1), src.at<uchar>(i + 1, j - 1),
	src.at<uchar>(i - 1, j + 1), src.at<uchar>(i - 1, j), src.at<uchar>(i, j - 1),
	src.at<uchar>(i - 1, j - 1));
	}
	else
	_dst.at<uchar>(i, j) = src.at<uchar>(i, j);
}
_dst.copyTo(dst);
_dst.release();
}

MedianFlitering()函数种调用 Median()函数求 9 个数的中值。

//求九个数的中值
uchar Median(uchar n1, uchar n2, uchar n3, uchar n4, uchar n5,
uchar n6, uchar n7, uchar n8, uchar n9) {
uchar arr[9];
arr[0] = n1;
arr[1] = n2;
arr[2] = n3;
arr[3] = n4;
arr[4] = n5;
arr[5] = n6;
arr[6] = n7;
arr[7] = n8;
arr[8] = n9;
for (int gap = 9 / 2; gap > 0; gap /= 2)//希尔排序
for (int i = gap; i < 9; ++i)
for (int j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap)
swap(arr[j], arr[j + gap]);
return arr[4];//返回中值
}

在这里插入图片描述

7. 去除小区域

调用 RemoveSmallRegion()函数去除一些小区域,对于某些情况,例如车道上存在车辆,可能会对二值化之后的图像造车高干扰,形成面积较小的白块,使用这个函数的目的是让出去小区域的干扰,让提取的线更“纯净”,因此去除小区域是十分有必要的。
具体代码如下所示:参数 CheckMode: 0 代表去除黑区域,1 代表去除白区域;NeihborMode:0 代表 4 邻域,1 代表 8 邻域
下面的代码太多就不贴了。
具体内容在https://github.com/TommyGong08/Traditional_Lane_Detection

在这里插入图片描述

8.闭运算

闭运算是对图像先进行膨胀再进行腐蚀,在本次车道线检测的闭运算中,膨
胀时卷积核大小为 22,腐蚀过程卷积核大小为 11。这样闭运算之后就能达到边缘提取的效果。

在这里插入图片描述

9. 霍夫变换提取直线和筛选直线

利用霍夫变换提取图像中的直线, 具体代码如下所示。霍夫变换的原理在此
不多赘述,重点想讲述一下直线筛选的过程。
由于霍夫变换之后得到的直线数量很多,为了得到符合条件的直线,我们有
必要对直线进行一定的筛选。在 draw_lane()函数种对直线进行比较精确地筛选,首先根据车道线的淘汰画面中较为平行的直线,接着设定 rho 和 angle 的阈值,淘汰重复的直线,使得每条车道仅有一条直线保留。

具体霍夫变换检测直线代码可以参考我的另一篇博客
在这里插入图片描述

10.画出直线并采样

在y方向上每个10个像素点采样,用黄色圆点可视化
在这里插入图片描述

三、总结

优点:基于传统方法的车道线检测方法简单,速度较快。不需要构建神经网络,硬件成本低。
缺点:①易受光线、环境、道路车辆等因素的影响,使得结果有偏差。
②需要调整的参数较多,例如本项目中常常需要调整霍夫变换的
threshold,灰度图二值化的阈值等,稳定性差。
③霍夫变换常用于拟合直线,对于弯道的检测不理想。
【注】传统车道线检测的方法还有很多,本篇博客给小伙伴们提供一个思路,希望对大家有帮助,欢迎点赞收藏~


版权声明:本文为weixin_43794327原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。