OpenCV (提取直线、轮廓和区域)
Eva.Q Lv9

提取图元 ~ ~

轮廓

Canny算子
1
2
3
// Apply Canny algorithm
cv::Mat contours;
cv::Canny(image,contours,125,350); // 灰度图像,输出轮廓,低阈值,高阈值

基于 Sobel 算子

直线

霍夫变换

HoughLines()

检测
1
2
3
4
5
6
7
Mat contours;
Canny(image, contours, 125, 350);
// 用霍夫变换检测直线
vector<Vec2f> lines;
HoughLines(test, lines,
1, PI/180, // 步长
60); // 最小投票数

半径步长为 1,表示函数将搜索所有可能的半径

角度步长为 PI/180,表示函数将搜索所有可能的角度

注:算法检测的是直线,而不是线段。故,即使点的坐标超出了图像范围,这个函数也能正常运行,因此没必要检查交叉点是否在图像内部。

画直线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<Vec2f>::const_iterator it = lines.begin();
while(it != lines.end()){
float rho = (*it)[0]; // 距离
float theta = (*it)[1]; // 角度

if(theta < PI/4. || theta > 3.*PI/4.){ // 垂直线(大致)
// 直线与第一行交叉点
Point pt1(rho/cos(theta), 0);
// 直线与最后一行交叉点
Point pt12((rho - result.rows*sin(theta))/cos(theta), result.rows);
// 画白色的线
line(image, pt1, pt2, Scalar(255), 1);
}else{ // 水平线(大致)
// 直线与第一列交叉点
Point pt1(0, rho/sin(theta));
// 直线与最后一列交叉点
Point pt12(result.cols, (rho - result.cols*cos(theta))/sin(theta));
// 画白色的线
line(image, pt1, pt2, Scalar(255), 1);
}
++it;
}
不足之处

有些像素只是碰巧排成了直线,霍夫变换可能产生错误

也可能因为多条参数相近的直线穿过了同一个像素对齐区域,而导致重复检测

概率霍夫变换

HoughLinesP()

用概率霍夫变换创建 LineFinder

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
#if !defined LINEF
#define LINEF

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#define PI 3.1415926

class LineFinder {

private:

// original image
cv::Mat img;

// vector containing the end points
// of the detected lines
std::vector<cv::Vec4i> lines;

// accumulator resolution parameters
double deltaRho;
double deltaTheta;

// minimum number of votes that a line
// must receive before being considered
int minVote;

// min length for a line
double minLength;

// max allowed gap along the line
double maxGap;

public:

// Default accumulator resolution is 1 pixel by 1 degree
// no gap, no mimimum length
LineFinder() : deltaRho(1), deltaTheta(PI/180), minVote(10), minLength(0.), maxGap(0.) {}

// Set the resolution of the accumulator
void setAccResolution(double dRho, double dTheta) {

deltaRho= dRho;
deltaTheta= dTheta;
}

// Set the minimum number of votes
void setMinVote(int minv) {

minVote= minv;
}

// Set line length and gap
void setLineLengthAndGap(double length, double gap) {

minLength= length;
maxGap= gap;
}

// Apply probabilistic Hough Transform
std::vector<cv::Vec4i> findLines(cv::Mat& binary) {

lines.clear();
cv::HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote, minLength, maxGap);

return lines;
}

// Draw the detected lines on an image
void drawDetectedLines(cv::Mat &image, cv::Scalar color=cv::Scalar(255,255,255)) {

// Draw the lines
std::vector<cv::Vec4i>::const_iterator it2= lines.begin();

while (it2!=lines.end()) {

cv::Point pt1((*it2)[0],(*it2)[1]);
cv::Point pt2((*it2)[2],(*it2)[3]);

cv::line( image, pt1, pt2, color);

++it2;
}
}

// Eliminates lines that do not have an orientation equals to
// the ones specified in the input matrix of orientations
// At least the given percentage of pixels on the line must
// be within plus or minus delta of the corresponding orientation
std::vector<cv::Vec4i> removeLinesOfInconsistentOrientations(
const cv::Mat &orientations, double percentage, double delta) {

std::vector<cv::Vec4i>::iterator it= lines.begin();

// check all lines
while (it!=lines.end()) {

// end points
int x1= (*it)[0];
int y1= (*it)[1];
int x2= (*it)[2];
int y2= (*it)[3];

// line orientation + 90o to get the parallel line
double ori1= atan2(static_cast<double>(y1-y2),static_cast<double>(x1-x2))+PI/2;
if (ori1>PI) ori1= ori1-2*PI;

double ori2= atan2(static_cast<double>(y2-y1),static_cast<double>(x2-x1))+PI/2;
if (ori2>PI) ori2= ori2-2*PI;

// for all points on the line
cv::LineIterator lit(orientations,cv::Point(x1,y1),cv::Point(x2,y2));
int i,count=0;
for(i = 0, count=0; i < lit.count; i++, ++lit) {

float ori= *(reinterpret_cast<float *>(*lit));

// is line orientation similar to gradient orientation ?
if (std::min(fabs(ori-ori1),fabs(ori-ori2))<delta)
count++;

}

double consistency= count/static_cast<double>(i);

// set to zero lines of inconsistent orientation
if (consistency < percentage) {

(*it)[0]=(*it)[1]=(*it)[2]=(*it)[3]=0;

}

++it;
}

return lines;
}
};
#endif

点集的直线拟合

首先要识别出图像中靠近直线的点。使用 HoughLinesP 检测到的直线。

HoughlinesP 检测到的直线存放在 vector<Vec4i> lines 中,为了提取出靠近这条直线的点集,在黑色图像上画一条白色直线,并且穿过用于检测直线的 Canny 轮廓图

1
2
3
4
5
6
7
int n = 0; // 选用直线0
// 黑色图像
Mat oneline(contours.size(), CV_8U, Scalar(0));
// 白色直线
line(oneline, Point(lines[n][0], lines[n][1]), Point(lines[n][2], lines[n][3]), Scalar(255), 3);
// 轮廓与白色直线进行“与”运算
bitwise_and(contours, oneline, oneline);

结果是一个包含了与指定直线相关的点的图像(为了提升显示效果,可以进行反转)

然后把这些集合内点的坐标插入到 vector<Point> points

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<Point> points;
// 迭代遍历像素,得到所有点的位置
for(int y = 0; y < oneline.rows; ++y){
// 行y
uchar* rowPtr = oneline.ptr<uchar>(y);

for(int x = 0; x < oneline.cols; ++x){
// 列x

// 如果在轮廓上
if(rowPtr[x]){
points.push_back(Point(x, y));
}
}
}

得到点集后,利用这些点集你和出直线。

1
2
3
4
5
Vec4f line;
fitLine(points, line,
DIST_L2, // 距离类型
0, // L2类型不用这个参数
0.01, 0.01); // 精度

上述代码把直线方程式作为参数,形式式一个单位方向向量( Vec4f 的前两个数值)和直线上一个点的坐标( Vec4f 的后两个数值)。

直线方程式通常用于某些属性的计算(例如需要精确参数的校准)。下面演示一下其用法

1
2
3
4
5
6
int x0 = line[2]; // 直线上的一个点
int y0 = line[3];
int x1 = x0 + 100 * line[0]; // 加上长度为100的向量
int y1 = y0 + 100 * line[1];
// 绘制
line(image, Point(x0, y0), Point(x1, y1), 0.2);

区域

提取连续区域

第一步:生成二值图像(可以用直方图反向投影,也可以用运动分析)

第二步:执行一次简单的阈值操作,然后应用形态学滤波器。

提取连续区域 就是 提取在二值图像中由一批连通的像素构成的形状。

1
2
3
4
5
vector<vector<Point> > contours;
findContours(image, // 二值图像
contours, // 存储轮廓的向量
RETR_EXTERNAL, // 检索外部轮廓
CHAIN_ASPPOX_NONE); // 每个轮廓的全部像素

画出区域

1
2
3
4
5
Mat result(image.size(), CV_8U, Scalar(255));
drawContours(result, contours,
-1, // 画全部轮廓 (可以指定要画的轮廓的序号)
0, // 颜色:黑
2); // 宽度

若事先知道感兴趣物体的大小,就可以将部分区域删除。

1
2
3
4
5
6
7
8
9
10
int cmin = 50;
int cmax = 1000;
vector<vector<Point> > ::iterator itc = contours.begin();

while(itc != contours.end()){
if(itc -> size() < cmin || itc -> size() > cmax)
itc = contours.erase(itc);
else
++itc;
}
计算区域的形状描述子

测试边界框、

1
2
3
4
// testing the bounding box 
cv::Rect r0= cv::boundingRect(contours[0]);
// draw the rectangle
cv::rectangle(result,r0, 0, 2);

测试覆盖圆

1
2
3
4
5
6
// testing the enclosing circle 
float radius;
cv::Point2f center;
cv::minEnclosingCircle(contours[1],center,radius);
// draw the cricle
cv::circle(result,center,static_cast<int>(radius), 0, 2);

测试多边形逼近

1
2
3
4
5
// testing the approximate polygon
std::vector<cv::Point> poly;
cv::approxPolyDP(contours[2],poly,5,true);
// draw the polygon
cv::polylines(result, poly, true, 0, 2);
1
std::cout << "Polygon size: " << poly.size() << std::endl; // 几边形

测试凸包

1
2
3
4
5
// testing the convex hull
std::vector<cv::Point> hull;
cv::convexHull(contours[3],hull);
// draw the polygon
cv::polylines(result, hull, true, 0, 2);

测试轮廓矩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// testing the moments

// iterate over all contours
itc= contours.begin();
while (itc!=contours.end()) {

// compute all moments
cv::Moments mom= cv::moments(*itc++);

// draw mass center
cv::circle(result,
// position of mass center converted to integer
cv::Point(mom.m10/mom.m00,mom.m01/mom.m00),
2,cv::Scalar(0),2); // draw black dot
}
1
2
cv::namedWindow("Some Shape descriptors");
cv::imshow("Some Shape descriptors",result);
  • Post title:OpenCV (提取直线、轮廓和区域)
  • Post author:Eva.Q
  • Create time:2021-08-09 09:56:02
  • Post link:https://qyy/2021/08/09/OPENCV/OPENCV1-10/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.