文章目录
- 前言
- 一、图像处理流程
- 二、具体步骤
- 1.图片输入
- 2.转为灰度图像
- 3. 直方图均衡化
- 4. 高斯模糊
- 5. 二值化
- 6. 边缘平滑
- 7. 去除小区域
- 8.闭运算
- 9. 霍夫变换提取直线和筛选直线
- 10.画出直线并采样
- 三、总结
前言
基于传统数字图像处理方法的车道线检测项目,除图像输入输出外,不调用任何库
源代码已经上传至github:TommyGong08 如果对你有帮助的话,记得follow和star~
一、图像处理流程
图像按照以下流程处理:
- 输入图片
- 转灰度图
- 直方图均衡化
- 高斯滤波
- 二值化
- 边缘平滑
- 去除小区域
- 闭运算
- 霍夫变换
二、具体步骤
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,灰度图二值化的阈值等,稳定性差。
③霍夫变换常用于拟合直线,对于弯道的检测不理想。
【注】传统车道线检测的方法还有很多,本篇博客给小伙伴们提供一个思路,希望对大家有帮助,欢迎点赞收藏~