上一个教程 : Canny边缘检测器
下一个教程 : 霍夫圆变换
原作者 | Ana Huamán |
---|---|
兼容性 | OpenCV >= 3.0 |
目标
在本教程中,您将学习如何
- 使用 OpenCV 函数 HoughLines() 和 HoughLinesP() 检测图像中的线条。
理论
注释
以下解释来自 Bradski 和 Kaehler 合著的《学习 OpenCV》一书。
霍夫线变换
- Hough Line 变换是一种用于检测直线的变换。
- 要应用该变换,首先需要进行边缘检测预处理。
它是如何工作的?
-
众所周知,图像空间中的一条直线可以用两个变量来表示。例如
a. 在直角坐标系中: 参数:(m,b)。
b. 在极坐标系中 参数: (r,θ)
对于 Hough 变换,我们将用极坐标系来表示直线。因此,直线方程可以写成
y = ( − cos θ sin θ ) x + ( r sin θ ) y=\left( -\frac{\cos \theta}{\sin \theta} \right) x+\left( \frac{r}{\sin \theta} \right) y=(−sinθcosθ)x+(sinθr)
排列项:r=xcosθ+ysinθ
- 一般来说,对于每一点(x0,y0),我们可以将经过该点的线段族定义为
r θ = x 0 ⋅ c o s θ + y 0 ⋅ s i n θ r_{\theta}=x_0⋅cos\theta +y_0⋅sin\theta rθ=x0⋅cosθ+y0⋅sinθ
这意味着每对(rθ,θ)代表经过(x0,y0)的每条直线。
- 对于给定的 (x0,y0),如果我们绘制出经过它的直线族,就会得到一个正弦曲线。例如,对于 x0=8,y0=6,我们可以得到下面的曲线图(在平面 θ - r 中):
我们只考虑 r>0 且 0<θ<2π 的点。
- 我们可以对图像中的所有点进行上述操作。如果两个不同点的曲线相交于平面 θ - r,则表示这两个点属于同一条直线。例如,按照上面的例子,再绘制两个点的曲线图:x1=4, y1=9 和 x2=12, y2=3,我们可以得到
这三幅图相交于一个点(0.925,9.6),这些坐标就是参数 ( θ,r) 或 (x0,y0)、(x1,y1) 和 (x2,y2) 所在的直线。
- 上面这些是什么意思呢?这意味着,一般来说,可以通过寻找曲线之间的交点数量来检测一条直线。一般来说,我们可以定义一个阈值,即检测一条直线所需的最小交点数。
- 这就是 Hough Line Transform 的作用。它可以跟踪图像中每个点的曲线交点。如果交叉点的数量超过某个阈值,那么它就会用交叉点的参数(θ,rθ)将其宣布为一条直线。
标准 Hough 线变换和概率 Hough 线变换
OpenCV 实现了两种 Hough 线变换:
a. 标准 Hough 变换
- 它与我们在上一节中解释的基本相同。其结果是一个耦合向量 (θ,rθ)
- 在 OpenCV 中,它是通过函数 HoughLines() 实现的。
b. 概率 Hough 线变换
- Hough Line Transform 的一种更有效的实现方式。它的输出结果是检测到的线条的极值 (x0,y0,x1,y1)
- 在 OpenCV 中,它是通过函数 HoughLinesP() 实现的。
这个程序要做什么?
- 加载图像
- 应用标准 Hough 线条变换和概率线条变换。
- 在三个窗口中显示原始图像和检测到的线条。
代码
更高级的版本(同时显示标准 Hough 和概率 Hough,并带有用于更改阈值的轨迹条)可在此处找到。
C++
我们将讲解的示例代码可从此处下载。
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
// 声明输出变量
Mat dst, cdst, cdstP;
const char* default_file = "sudoku.png";
const char* filename = argc >=2 ? argv[1] : default_file;
// 加载图像
Mat src = imread( samples::findFile( filename ), IMREAD_GRAYSCALE );
// 检查图片加载是否正常
if(src.empty()){
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default %s] \n", default_file);
return -1;
}
// 边缘检测
Canny(src, dst, 50, 200, 3);
// 将边缘复制到将在 BGR 中显示结果的图像中
cvtColor(dst, cdst, COLOR_GRAY2BGR);
cdstP = cdst.clone();
// 标准 Hough 线条变换
vector<Vec2f> lines; // 将保存检测结果
HoughLines(dst, lines, 1, CV_PI/180, 150, 0, 0 ); // 运行实际检测
// 绘制线条
for( size_t i = 0; i < lines.size(); i++ )
{
float rho = lines[i][0], theta = lines[i][1];
点 pt1、pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( cdst, pt1, pt2, Scalar(0,0,255), 3, LINE_AA);
}
// 概率直线变换
vector<Vec4i> linesP; // 将保存检测结果
HoughLinesP(dst, linesP, 1, CV_PI/180, 50, 50, 10 ); // 运行实际检测
// 绘制线条
for( size_t i = 0; i < linesP.size(); i++ )
{
Vec4i l = linesP[i];
line( cdstP, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, LINE_AA);
}
// 显示结果
imshow("Source", src);
imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst);
imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP);
// 等待并退出
waitKey();
return 0;
}
Java
我们将讲解的示例代码可从此处下载。
import org.opencv.core.*;
import org.opencv.core.Point;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
class HoughLinesRun {
public void run(String[] args) {
// 声明输出变量
Mat dst = new Mat(), cdst = new Mat(), cdstP;
String default_file = ".../../../../data/sudoku.png";
String filename = ((args.length > 0) ? args[0] : default_file);
// 加载图像
Mat src = Imgcodecs.imread(filename, Imgcodecs.IMREAD_GRAYSCALE);
// 检查图片加载是否正常
if( src.empty() ) {
System.out.println("Error opening image!");
System.out.println("Program Arguments: [image_name -- 默认 "
+ default_file +"] \n");
System.exit(-1);
}
// 边缘检测
Imgproc.Canny(src, dst, 50, 200, 3, false);
// 将边缘复制到将在 BGR 中显示结果的图像中
Imgproc.cvtColor(dst, cdst, Imgproc.COLOR_GRAY2BGR);
cdstP = cdst.clone();
// 标准霍夫线变换
Mat lines = new Mat(); // 将保存检测结果
Imgproc.HoughLines(dst, lines, 1, Math.PI/180, 150); // 运行实际检测
// 绘制线条
for (int x = 0; x < lines.rows(); x++) {
double rho = lines.get(x, 0)[0]、
theta = lines.get(x, 0)[1];
double a = Math.cos(theta),b = Math.sin(theta);
double x0 = a*rho, y0 = b*rho;
Point pt1 = new Point(Math.round(x0 + 1000*(-b)), Math.round(y0 + 1000*(a)));
Point pt2 = new Point(Math.round(x0 - 1000*(-b)), Math.round(y0 - 1000*(a)));
Imgproc.line(cdst, pt1, pt2, new Scalar(0, 0, 255), 3, Imgproc.LINE_AA, 0);
}
// 概率直线变换
Mat linesP = new Mat(); // 将保存检测结果
Imgproc.HoughLinesP(dst, linesP, 1, Math.PI/180, 50, 50, 10); // 运行实际检测结果
// 绘制线条
for (int x = 0; x < linesP.rows(); x++) {
double[] l = linesP.get(x, 0);
Imgproc.line(cdstP, new Point(l[0], l[1]), new Point(l[2], l[3]), new Scalar(0, 0, 255), 3, Imgproc.LINE_AA, 0);
}
// 显示结果
HighGui.imshow("Source", src);
HighGui.imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst);
HighGui.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP);
// 等待并退出
HighGui.waitKey();
System.exit(0);
}
}
public class HoughLines {
public static void main(String[] args) { // Load the native library.
// 加载本地库
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new HoughLinesRun().run(args);
}
}
Python
我们将讲解的示例代码可从此处下载。
"""
@file hough_lines.py
@brief This program demonstrates line finding with the Hough transform
"""
import sys
import math
import cv2 as cv
import numpy as np
def main(argv):
default_file = 'sudoku.png'
filename = argv[0] if len(argv) > 0 else default_file
# 加载图像
src = cv.imread(cv.samples.findFile(filename), cv.IMREAD_GRAYSCALE)
# 检查图像是否加载正常
if src is None:
print ('Error opening image!')
print ('Usage: hough_lines.py [image_name -- default ' + default_file + '] \n')
return -1
dst = cv.Canny(src, 50, 200, None, 3)
# 将边缘复制到将在 BGR 中显示结果的图像上
cdst = cv.cvtColor(dst, cv.COLOR_GRAY2BGR)
cdstP = np.copy(cdst)
lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 0, 0)
if lines is not None:
for i in range(0, len(lines)):
rho = lines[i][0][0]
theta = lines[i][0][1]
a = math.cos(theta)
b = math.sin(theta)
x0 = a * rho
y0 = b * rho
pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))
pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
cv.line(cdst, pt1, pt2, (0,0,255), 3, cv.LINE_AA)
linesP = cv.HoughLinesP(dst, 1, np.pi / 180, 50, None, 50, 10)
if linesP is not None:
for i in range(0, len(linesP)):
l = linesP[i][0]
cv.line(cdstP, (l[0], l[1]), (l[2], l[3]), (0,0,255), 3, cv.LINE_AA)
cv.imshow("Source", src)
cv.imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst)
cv.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP)
cv.waitKey()
return 0
if __name__ == "__main__":
main(sys.argv[1:])
说明
加载图片
C++
const char* default_file = "sudoku.png";
const char* filename = argc >=2 ? argv[1] : default_file;
// Loads an image
Mat src = imread( samples::findFile( filename ), IMREAD_GRAYSCALE );
// Check if image is loaded fine
if(src.empty()){
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default %s] \n", default_file);
return -1;
}
Java
String default_file = "../../../../data/sudoku.png";
String filename = ((args.length > 0) ? args[0] : default_file);
// Load an image
Mat src = Imgcodecs.imread(filename, Imgcodecs.IMREAD_GRAYSCALE);
// Check if image is loaded fine
if( src.empty() ) {
System.out.println("Error opening image!");
System.out.println("Program Arguments: [image_name -- default "
+ default_file +"] \n");
System.exit(-1);
}
Pyhton
default_file = 'sudoku.png'
filename = argv[0] if len(argv) > 0 else default_file
# Loads an image
src = cv.imread(cv.samples.findFile(filename), cv.IMREAD_GRAYSCALE)
# Check if image is loaded fine
if src is None:
print ('Error opening image!')
print ('Usage: hough_lines.py [image_name -- default ' + default_file + '] \n')
return -1
使用 Canny 检测器检测图像边缘:
C++
// Edge detection
Canny(src, dst, 50, 200, 3);
Java
// Edge detection
Imgproc.Canny(src, dst, 50, 200, 3, false);
Pyhton
# Edge detection
dst = cv.Canny(src, 50, 200, None, 3)
现在,我们将应用 Hough 线性变换。我们将介绍如何使用 OpenCV 的两个函数来实现这一目的。
标准 Hough 线变换:
首先,应用变换:
C++
// Standard Hough Line Transform
vector<Vec2f> lines; // will hold the results of the detection
HoughLines(dst, lines, 1, CV_PI/180, 150, 0, 0 ); // runs the actual detection
Java
// Standard Hough Line Transform
Mat lines = new Mat(); // will hold the results of the detection
Imgproc.HoughLines(dst, lines, 1, Math.PI/180, 150); // runs the actual detection
Pyhton
# Standard Hough Line Transform
lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 0, 0)
- 使用以下参数
- dst: 边缘检测器的输出。应该是灰度图像(但实际上是二值图像)
- lines: 一个向量,用于存储检测到的线条的参数 (r,θ)
- rho:参数 r 的分辨率(像素)。我们使用 1 像素。
- θ: 参数 θ 的分辨率,单位为弧度。我们使用 1 度 (CV_PI/180)
- threshold: 用于 "检测"一条直线的最小交点数
- srn 和 stn:默认参数为零。更多信息请查看 OpenCV 参考资料。
然后通过绘制线条显示结果。
C++
// Draw the lines
for( size_t i = 0; i < lines.size(); i++ )
{
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( cdst, pt1, pt2, Scalar(0,0,255), 3, LINE_AA);
}
Java
// Draw the lines
for (int x = 0; x < lines.rows(); x++) {
double rho = lines.get(x, 0)[0],
theta = lines.get(x, 0)[1];
double a = Math.cos(theta), b = Math.sin(theta);
double x0 = a*rho, y0 = b*rho;
Point pt1 = new Point(Math.round(x0 + 1000*(-b)), Math.round(y0 + 1000*(a)));
Point pt2 = new Point(Math.round(x0 - 1000*(-b)), Math.round(y0 - 1000*(a)));
Imgproc.line(cdst, pt1, pt2, new Scalar(0, 0, 255), 3, Imgproc.LINE_AA, 0);
}
Pyhton
# Draw the lines
if lines is not None:
for i in range(0, len(lines)):
rho = lines[i][0][0]
theta = lines[i][0][1]
a = math.cos(theta)
b = math.sin(theta)
x0 = a * rho
y0 = b * rho
pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))
pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
cv.line(cdst, pt1, pt2, (0,0,255), 3, cv.LINE_AA)
概率霍夫线性变换
首先应用变换:
C++
// Probabilistic Line Transform
vector<Vec4i> linesP; // will hold the results of the detection
HoughLinesP(dst, linesP, 1, CV_PI/180, 50, 50, 10 ); // runs the actual detection
Java
// Probabilistic Line Transform
Mat linesP = new Mat(); // will hold the results of the detection
Imgproc.HoughLinesP(dst, linesP, 1, Math.PI/180, 50, 50, 10); // runs the actual detection
Pyhton
# Probabilistic Line Transform
linesP = cv.HoughLinesP(dst, 1, np.pi / 180, 50, None, 50, 10)
- 使用参数
- dst: 边缘检测器的输出。应该是灰度图像(但实际上是二值图像)
- lines: 一个向量,用于存储检测到的线条的参数(xstart,ystart,xend,yend)。
- rho:参数 r 的分辨率,单位为像素。我们使用 1 像素。
- θ: 参数 θ 的分辨率,单位为弧度。我们使用 1 度 (CV_PI/180)
- threshold: 检测*"线的最小交点数
- minLineLength: 能形成一条直线的最小点数。少于此点数的线条将被忽略。
- maxLineGap:同一条直线上两个点之间的最大间距。
然后通过画线显示结果。
C++
// Draw the lines
for( size_t i = 0; i < linesP.size(); i++ )
{
Vec4i l = linesP[i];
line( cdstP, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, LINE_AA);
}
Java
// Draw the lines
for (int x = 0; x < linesP.rows(); x++) {
double[] l = linesP.get(x, 0);
Imgproc.line(cdstP, new Point(l[0], l[1]), new Point(l[2], l[3]), new Scalar(0, 0, 255), 3, Imgproc.LINE_AA, 0);
}
Pyhton
# Draw the lines
if linesP is not None:
for i in range(0, len(linesP)):
l = linesP[i][0]
cv.line(cdstP, (l[0], l[1]), (l[2], l[3]), (0,0,255), 3, cv.LINE_AA)
显示原始图像和检测到的线条:
C++
// Show results
imshow("Source", src);
imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst);
imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP);
Java
// Show results
HighGui.imshow("Source", src);
HighGui.imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst);
HighGui.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP);
Pyhton
# Show results
cv.imshow("Source", src)
cv.imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst)
cv.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP)
等待用户退出程序
C++
// Wait and Exit
waitKey();
return 0;
Java
// Wait and Exit
HighGui.waitKey();
System.exit(0);
Pyhton
# Wait and Exit
cv.waitKey()
return 0
结果
注释
下面的结果是使用我们在 "代码 "部分提到的略微高级的版本得到的。它仍然实现了与上面相同的功能,只是为阈值添加了 Trackbar。
使用输入图像,如数独图像。通过使用标准 Hough 线变换,我们可以得到以下结果:
使用概率 Hough 线变换得到的结果如下
您可能会注意到,当您改变阈值时,检测到的线条数量会有所不同。原因很明显: 如果阈值越高,检测到的线条数量就越少(因为需要更多的点才能检测到线条)。