OpenCV提取铁轨轨道特征(一)

挺简单的。

关联项目:
http://www.h13studio.com/基于Linux嵌入式开发板的轨道检测机器人-大创项目-持续更新中
http://www.h13studio.com/OpenCV在Arm-Linux上的安装
http://www.h13studio.com/OpenCV提取最大联通面积-铁轨轨道特征-二

寒假的时候花了两天略微学了一下OpenCV 觉得这玩意挺好玩的。
然后因为我们的大创项目就是需要对轻轨和铁轨的轨道特征进行提取,曲率计算,障碍物检测。
所以前两天又花了两个小时时间去了解了一下侵蚀算法 Canny算法 findContours算法等等。
然后就开始着手设计了一个提取铁轨轨道特征的算法。感觉还挺简单的。

在这里 我强烈推荐这个UP的OpenCV视频,学习完OpenCV基础操作之后再去学这个 感觉特别简单。
https://space.bilibili.com/517381507/video?keyword=opencv

算法思路

无论要设计什么样的算法,其算法思路终究还是围绕着被检测物的特征来设计的
这回我要提取的是铁轨的轨道特征,那么首先打开百度 搜索一下铁轨的图片。
https://image.baidu.com/search/index?tn=baiduimage&ct=201326592&lm=-1&cl=2&ie=gb18030&word=%CC%FA%B9%EC&fr=ala&ala=1&alatpl=adress&pos=0&hs=2&xthttps=111111

显然,铁轨的最大特征就是,他有两条又长又粗的轨道 →_→
并且这个轨道颜色和周围环境颜色差异还很大,那么我们就很容易过滤掉其他很多颜色变化较小的色块,突出颜色变化率较大的铁轨特征。

这里我首先选择了这张图片做demo。因为他不禁图片拍摄角度正合我意,并且旁边还有经常出现的树木干扰。

首先提取图片颜色变化率,之前我曾经自己写了一个很简单的图片颜色变化率算法,不过既然OpenCV官方都提供这个api了 也没必要重复造轮子了。

Sobel算法

函数原理: 其实就是对图像的每个通道的值进行求导和卷积。

函数原型:

处理效果:

这样一处理 就能增强不少铁轨的特征,减弱其他无关干扰。图片里面最主要信息的就是两根铁轨了。
不过还不能直接用这张图进行特征提取 还需要筛掉一部分亮度很低的区域,减少干扰,减小计算量。

Thershold二值化算法

函数原理: 一个比较简单的比较运算,对于高于/低于阀值的像素按照给予的参数进行对应操作。

函数原型:

处理效果:

因为咱轨道主要是白色,不是标准的RGB颜色,所以也不能简单的只提取一个单通道进行计算。
而这个时候 图片是彩色还是黑白就不是很重要了。不如转换成黑白图片进行处理降低计算量。
毕竟这个特征提取是要在Arm板子上跑的。

cvtColor颜色空间转换

函数原理: 一个图片通道转换函数,在这里用于将彩色图片转换为灰度图片。

函数原型:

处理效果:

这个时候 图片中的两根铁轨得到了很好的保留 而其他剩余的信息已经对后续处理产生不了很大影响了。
现在 我们只有一个目的: 找出图中最长的两根线 也就是咱的铁轨。
方案1: 画两根横向的细线 和铁轨相交 得到封闭图形 再直接利用OpenCV官方自带的 findContours 查找出图片中面积最大的轮廓即可。
方案2: 自己设计一个算法 提取图中最长的线。这个我下一篇会讲到我是如何设计的。
这里先用方案1,毕竟只会对于一些只会用Python人来说 如果不用官方api来处理大量运算的话 Python的性能是不够用的,放到Arm板子上只能通过降分辨率的方法来实现自定义的大量运算。

划线,得到大面积轮廓

这里直接用line函数即可,具体划线的纵坐标取决于你摄像头摆放角度,不是很难。

处理效果:

findContours查找轮廓

函数原理: 我也不知道.. 反正就是查找轮廓常用函数。

函数原型:

处理效果:

效果出乎意料 居然把这个轮廓相交之后多余的线也保留了。

优化方案: 判断轮廓面积大于一个临界值之后就停止判断即可,这样概率上能减少一半的无用计算部分(但是我没有验证)。

但是保留铁轨是我们需要的,保留我们画上去的两根线就不合适了,不过这个很简单,直接把findContourscvtColor产生的灰度图用逻辑与运算处理一下就OK。

and逻辑与运算


关于两条横线造成的干扰点问题,其实很好解决,只需要移动这两条横线的位置 之后用生成的两幅图进行and操作即可完美消除。代价就是计算量加倍。
接下来就只剩下视角修正,曲线拟合以及曲率计算了,这些都是数学问题,很简单。

最终效果

原图&最终处理图

原图最终图叠加效果:

当然了 这个程序还有一个可以改进的部分: 目前二值图的阀值需要手动调参,这个到时候加个自适应算法就OK了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
分辨率: 1000 * 667
单线程,I5-9300H
性能测试:

Debug模式:
sobel0: 86ms
threshold0: 5ms
cvtColor0: 10ms
line0: 1ms
contours0: 39ms
dilate0: 5ms
end0: 2ms
all: 148ms

Release模式:
sobel0: 13ms
threshold0: 6ms
cvtColor0: 1ms
line0: 3ms
contours0: 7ms
dilate0: 2ms
end0: 4ms
all: 36ms

其实findContours并没有花费太长时间,完全可以执行两次findContours 然后 and 出一张完美图片。
Just Like This:

最终代码

当然,我并没有做消除干扰点的算法,因为没有必要。强迫症同学请自行添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <vector>

#include <time.h>

int main()
{
using namespace cv;

//读入图像
Mat input;
input = imread("C:/Users/h13/Desktop/pathway.jpg");

//开始计算时间
clock_t tstart,tsobel0,tthreshold0,tcvtColor0,tline0,tcontours0,tdilate0,tend0;//用于分别计算各个环节的时间消耗。
tstart = clock();

//显示原图
//imshow("input", input); //性能测试时请注释本行代码。

//求导图像
Mat sobel0;
Sobel(input,sobel0,CV_8UC1,1,0); //Opencv的Sobel函数
tsobel0 = clock();
//imshow("sobel0", sobel0); //性能测试时请注释本行代码。

//滤掉低亮度部分
Mat thershold0;
threshold(sobel0, thershold0, 200, 255, THRESH_TOZERO);
tthreshold0 = clock();
//imshow("thershold0", thershold0);//性能测试时请注释本行代码。

//转灰度图
Mat gray0;
cvtColor(thershold0, gray0, CV_BGR2GRAY);
tcvtColor0 = clock();
//imshow("gray0", gray0); //性能测试时请注释本行代码。

//膨胀(防止断线),根据情况决定是否开启本功能。
//Mat dilate0, structure_element3;
//structure_element3 = Mat::ones(3,3, CV_8UC1);
/*
structure_element3.at<uchar>(0, 1) = 1;
structure_element3.at<uchar>(1, 1) = 1;
structure_element3.at<uchar>(2, 1) = 1;
*/
//dilate(gray0, dilate0, structure_element3);
//tdilate = clock();
//imshow("dilate0", dilate0); //性能测试时请注释本行代码。
//gray0 = dilate0;

//画横线 保留最大面积的轮廓区域
Mat line0;
line0 = gray0.clone();
//上面的线
line(line0, Point(0, gray0.rows * 0.5), Point(gray0.cols, gray0.rows * 0.5), Scalar(255), 1, CV_AA);

//下面的线
line(line0, Point(0, gray0.rows * 0.85), Point(gray0.cols, gray0.rows * 0.85), Scalar(255), 1, CV_AA);
tline0 = clock();
//imshow("line0", line0); //性能测试时请注释本行代码。


//保留轮廓
std::vector<std::vector<Point>> contours;
std::vector<Vec4i> hierarchy;
Mat findContours0 = Mat::zeros(gray0.size(), CV_8U);
findContours(line0, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE,Point());

Mat Contours = Mat::zeros(gray0.size(), CV_8UC1);
double maxarea=0, temparea = 0;
int maxid = 0;
for (int i = 0; i < contours.size(); i++)
{
//contours[i]代表的是第i个轮廓,contours[i].size()代表的是第i个轮廓上所有的像素点数

//找出面积最大的轮廓
temparea = fabs(contourArea(contours[i]));
if (temparea > maxarea)
{
maxarea = temparea;
maxid = i;
}
}

for (int j = 0; j < contours[maxid].size(); j++)
{
//绘制出contours向量内所有的像素点
Point P = Point(contours[maxid][j].x, contours[maxid][j].y);
Contours.at<uchar>(P) = 255;
}
//绘制轮廓
drawContours(findContours0, contours, maxid, Scalar(255), 1, 8, hierarchy);
tcontours0 = clock();
//imshow("contours", findContours0);

//取逻辑与,得到非横向特征
Mat and0;
bitwise_and(gray0, findContours0, and0, noArray());
tcontours0 = clock();
//imshow("and0", and0);

//膨胀(加粗线条)
Mat dilate0, structure_element3;
structure_element3 = Mat::ones(3, 3, CV_8UC1);

structure_element3.at<uchar>(0, 1) = 1;
structure_element3.at<uchar>(1, 1) = 1;
structure_element3.at<uchar>(2, 1) = 1;

dilate(and0, dilate0, structure_element3);
tdilate0 = clock();

//将生成的二值图作为红色通道叠加到原图中
std::vector<Mat> Channels;
split(input, Channels);
Channels[2] += dilate0;

Mat end0;
merge(Channels, end0);
tend0 = clock();
//imshow("end0", end0);

//计算时间消耗
system("pause");

tsobel0, tthreshold0, tcvtColor0, tline0, tcontours0, tdilate0, tend0;

std::cout << "tsobel0: " << (tsobel0 - tstart) << "ms" << std::endl;
std::cout << "tthreshold0: " << (tthreshold0 - tsobel0) << "ms" << std::endl;
std::cout << "tcvtColor0: " << (tcvtColor0 - tthreshold0) << "ms" << std::endl;
std::cout << "tline0: " << (tline0 - tcvtColor0) << "ms" << std::endl;
std::cout << "tcontours0: " << (tcontours0 - tline0) << "ms" << std::endl;
std::cout << "tdilate0: " << (tdilate0 - tcontours0) << "ms" << std::endl;
std::cout << "tend0: " << (tend0 - tdilate0) << "ms" << std::endl;
std::cout << "tall: " << (tend0 - tstart) << "ms" << std::endl;
system("pause");
imshow("input", input);
imshow("and0", and0);
imshow("end", end0);

cv::waitKey(0);
}

PS: 其实我自己实现的Sobel算法,有黑边,速度只比官方快了一点点(一半左右,也就是快了5ms),所以我还是用官方的了。

Video