OpenCV 霍夫直线变换实战指南:HoughLines 与 HoughLinesP 的检测原理、参数详解与代码示例
2026/9/7 19:07:20 网站建设 项目流程

OpenCV 霍夫直线变换实战指南:HoughLines 与 HoughLinesP 的检测原理、参数详解与代码示例

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

霍夫直线变换(Hough Line Transform)是图像处理中最经典的直线检测算法之一,广泛用于车道线识别、文档扫描矫正、棋盘格与网格线提取等场景。本篇技术指南基于 OpenCV 官方教程 hough_lines.markdown,结合当前仓库中的真实示例源码与 API 声明,系统讲解标准霍夫变换(HoughLines)与概率霍夫变换(HoughLinesP)的数学原理、完整参数含义,并提供可直接编译运行的 C++ / Python 实战代码。读完本文,你将掌握:如何用 Canny 预处理图像,如何调用两类霍夫函数并解析其输出((ρ, θ)与线段端点),以及如何依据阈值、minLineLengthmaxLineGap等参数调出稳定、可控的直线检测结果。

学习目标

本教程的核心目标非常聚焦:学会使用 OpenCV 的HoughLines()HoughLinesP()两个函数,在二值边缘图像中检测图像内的直线,并正确地把检测结果绘制回原图。

在动手调用 API 之前,理解算法背后的"投票(voting)"思想至关重要——它决定了每个参数的取值逻辑。

理论基础:霍夫直线变换是如何工作的

本节原理说明参考了 Bradski 与 Kaehler 所著的Learning OpenCV一书。

为什么要先做边缘检测?霍夫直线变换用于检测直线,而"待投票的点"正是图像中的边缘像素。因此官方推荐在使用霍夫变换之前,先通过边缘检测(如 Canny)对图像做预处理,把彩色/灰度图收缩为一张只有边缘轮廓的二值图,再送入霍夫函数。

直线在图像空间中的两种表示

众所周知,图像空间中的一条直线可以用两个变量来描述:

  • 笛卡尔坐标系:参数为斜率与截距 $(m, b)$,对应直线方程 $y = mx + b$;
  • 极坐标系:参数为 $(r, \theta)$,其中 $r$ 是直线到坐标原点(图像左上角 $(0,0)$)的垂直距离,$\theta$ 是垂线与横轴的夹角。

霍夫变换采用极坐标系表示直线,其方程可写为:

$$y = \left( -\frac{\cos\theta}{\sin\theta} \right) x + \left( \frac{r}{\sin\theta} \right)$$

整理各项后即得到霍夫直线变换的核心公式:

$$r = x \cos\theta + y \sin\theta$$

之所以不用 $y = mx + b$,是因为当直线竖直时斜率 $m$ 趋近无穷,参数空间无法覆盖所有情况;而 $(r, \theta)$ 对任意方向的直线都是有界的、稳定的。

从"图像空间的一个点"到"参数空间的一条正弦曲线"

对于图像空间中任意一个固定点 $(x_0, y_0)$,所有经过该点的直线族可以表示为:

$$r_\theta = x_0 \cos\theta + y_0 \sin\theta$$

也就是说,每一对 $(r_\theta, \theta)$ 都对应一条经过 $(x_0, y_0)$ 的直线。如果把 $\theta$ 看作自变量、$r$ 看作因变量,在 $(\theta, r)$ 平面上把经过该点的所有直线画出来,就会得到一条正弦曲线(sinusoid)。例如对点 $x_0 = 8$、$y_0 = 6$,其轨迹即是一条正弦波。

关键洞察:曲线相交 = 共线投票

对图像中的每一个边缘点都执行上述操作,我们会得到一族正弦曲线。此时出现霍夫变换最核心的几何事实:

若两条不同点的曲线在 $(\theta, r)$ 平面上相交,说明这两个点位于同一条直线上。

继续上面的例子,再补充两个点 $x_1 = 4$、$y_1 = 9$ 和 $x_2 = 12$、$y_2 = 3$,画出它们的曲线后可以发现,三条曲线相交于唯一的一点 $(0.925, 9.6)$。这一坐标正是 $(x_0,y_0)$、$(x_1,y_1)$、$(x_2,y_2)$ 三点所在的直线所对应的参数 $(\theta, r)$。

把上面这些几何观察翻译成算法语言,就是:

  • 一条直线之所以能被"检测"到,是因为它在参数空间中对应曲线交点的数量足够多
  • 相交的曲线越多,说明落在同一条直线上的边缘点越多,该直线越"真实";
  • 因此我们可以设定一个阈值(threshold):只有当参数单元获得的票数(相交曲线数)超过该阈值时,才判定 $(\theta, r_\theta)$ 对应一条真实直线。

这就是霍夫直线变换的全部本质:追踪图像中每个点对应的曲线之间的交点(即向一个 $(\theta, r)$ 累加器投票),统计票数,超过阈值即宣布检测到一条直线。

需要说明的是,实现中只考虑 $r > 0$ 且 $0 < \theta < 2\pi$ 范围内的参数,以保证参数空间紧凑且无歧义。

OpenCV 实现的三种霍夫直线变换

OpenCV 的imgproc模块共提供三种霍夫直线变换实现,它们被封装在两个函数中:

a. 标准霍夫变换(Standard Hough Transform)

  • 实现方式与前文推导的"逐点画曲线、统计交点"完全一致;
  • 输出是直线参数对 $(\theta, r_\theta)$ 的向量;
  • 对应函数:HoughLines()

b. 概率霍夫直线变换(Probabilistic Hough Line Transform)

  • 标准霍夫变换更高效的实现,它不再对全部边缘点做全量投票,而是随机抽取边缘点子集参与投票(参考文献Matas00,即 J. Matas 等人的Progressive Probabilistic Hough Transform);
  • 输出不再是无限长的直线参数,而是已检测线段两个端点的坐标$(x_{start}, y_{start}, x_{end}, y_{end})$,便于直接绘制线段并计算长度;
  • 对应函数:HoughLinesP()

c. 加权霍夫变换(Weighted Hough Transform)

  • 标准霍夫变换对二值边缘图投票,每个边缘点贡献值恒为 1;加权霍夫变换则直接利用**边缘强度(edge intensity)**而非 0/1 值参与投票,从而对弱边缘更鲁棒;
  • 它同样是HoughLines(),只是需要把参数use_edgeval置为true,并传入一张按边缘强度编码的图像;
  • 交互式演示见仓库示例 HoughLines_Demo.cpp。

函数签名与完整参数(源码级)

在头文件 modules/imgproc/include/opencv2/imgproc.hpp 中,HoughLines的完整声明为:

CV_EXPORTS_W void HoughLines( InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = 0, double stn = 0, double min_theta = 0, double max_theta = CV_PI, bool use_edgeval = false );

各参数含义如下:

参数含义
image8 位单通道二值源图。注意:函数可能会修改该图像
lines输出直线向量。每条线由 2 或 3 元素向量表示:$(\rho, \theta)$ 或 $(\rho, \theta, votes)$。其中 $\rho$ 是直线到坐标原点(图像左上角)的距离,$\theta$ 是直线的旋转角(弧度,$\theta=0$ 为竖直线、$\theta=\pi/2$ 为水平线),votes是累加器票数
rho累加器在距离维上的分辨率(像素)
theta累加器在角度维上的分辨率(弧度)
threshold累加器阈值,只有票数大于该值的直线才会被返回
srn多尺度霍夫变换中rho的除数:粗累加器的距离分辨率是rho,精化分辨率为rho/srn。若srnstn同时为 0,则退化为经典霍夫变换;否则两者都应为正
stn多尺度霍夫变换中角度分辨率的除数
min_theta需要检测的最小角度,取值须介于 0 与max_theta之间
max_theta角度上界,须介于min_thetaCV_PI之间(实际累加器最大角度可能略小于它)
use_edgeval置为true时启用加权霍夫变换

HoughLinesP(见 imgproc.hpp)的声明为:

CV_EXPORTS_W void HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0 );
参数含义
image8 位单通道二值源图(同样可能被函数修改)
lines输出向量,每条线段由 4 元素向量 $(x_1, y_1, x_2, y_2)$ 表示,即线段的两个端点坐标
rho累加器距离分辨率(像素)
theta累加器角度分辨率(弧度)
threshold累加器阈值,只有票数大于它的直线才会被返回
minLineLength最小线段长度,短于该值的线段将被丢弃
maxLineGap同一直线上两点之间允许连接的最大间距,用于把断裂的短线段拼接起来

顺带一提,imgproc还提供了针对点集输入的HoughLinesPointSet,用于在若干离散点而非整幅二值图中检测直线,其输出格式为 $(votes, rho, theta)$。当你的输入本身是特征点集而非边缘图时,可以考虑它。

完整示例代码解读

官方讲解的核心示例位于仓库的 samples/cpp/tutorial_code/ImgTrans/houghlines.cpp,默认输入图片为 samples/data/sudoku.png(一张数独棋盘网格图,非常适合演示横竖网格线的检测)。同目录下还有带 Trackbar 的增强版 HoughLines_Demo.cpp。

下面给出完整 C++ 代码(与仓库源码一致),随后逐段讲解:

/** * @file houghlines.cpp * @brief This program demonstrates line finding with the Hough transform */ #include "opencv2/imgcodecs.hpp" #include "opencv2/highgui.hpp" #include "opencv2/imgproc.hpp" using namespace cv; using namespace std; int main(int argc, char** argv) { // Declare the output variables Mat dst, cdst, cdstP; // Loads an image(默认 sudoku.png,可用命令行参数覆盖) const char* default_file = "sudoku.png"; const char* filename = argc >= 2 ? argv[1] : default_file; 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; } // Edge detection:先用 Canny 提取边缘 Canny(src, dst, 50, 200, 3); // Copy edges to the images that will display the results in BGR cvtColor(dst, cdst, COLOR_GRAY2BGR); cdstP = cdst.clone(); // 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 // 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); } // 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 // 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); } // Show results imshow("Source", src); imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst); imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP); // Wait and Exit waitKey(); return 0; }

第一步:以灰度模式加载图像

const char* default_file = "sudoku.png"; const char* filename = argc >= 2 ? argv[1] : default_file; Mat src = imread( samples::findFile( filename ), IMREAD_GRAYSCALE );

代码先以IMREAD_GRAYSCALE(灰度)模式读图,支持用命令行参数argv[1]覆盖默认的sudoku.pngsamples::findFile会在已配置的 OpenCV 样本数据目录中定位图片,这正是该示例不写绝对路径也能找到 samples/data/sudoku.png 的原因。读图后用src.empty()做健壮性检查,失败则打印用法并返回。

第二步:用 Canny 检测图像边缘

Canny(src, dst, 50, 200, 3);

前文原理部分强调过:霍夫直线变换作用于二值边缘图。这里用 Canny 边缘检测器把灰度图src变为只含边缘的二值图dst,阈值取低阈值 50、高阈值 200,Sobel 核大小 3。之后再将边缘图转回 BGR 彩色空间(cvtColor(dst, cdst, COLOR_GRAY2BGR))并克隆一份,分别用于叠加两种算法的结果,便于目视对比。

第三步:应用标准霍夫直线变换并绘制

vector<Vec2f> lines; HoughLines(dst, lines, 1, CV_PI/180, 150, 0, 0 );

各实参的取法(与官方文档逐条对应):

  • dst:边缘检测器的输出。按文档要求它应是一张灰度图——虽然实际上内容已是二值的;
  • lines:用于存放检测结果的向量,元素类型Vec2f,即每一条直线存 $(\rho, \theta)$ 两个参数;
  • rho:$r$ 方向的分辨率,单位像素,此处取1 像素
  • theta:$\theta$ 方向的分辨率,单位弧度,此处取1 度,即CV_PI/180
  • threshold:判定"检测到直线"所需的最小投票数(最小交点数量),此处取 150;
  • srnstn:多尺度霍夫参数,默认取 0,此时调用的是经典(非多尺度)霍夫变换。

绘制直线时,由于标准霍夫只给出参数 $(\rho, \theta)$ 而没有端点,需要自己换算成图像上的两个点:

float rho = lines[i][0], theta = lines[i][1]; 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);

其中 $(x_0, y_0) = (\rho\cos\theta, \rho\sin\theta)$ 是直线上离原点最近的那个点;沿垂直于它的方向(方向向量 $(-\sin\theta, \cos\theta)$)向两侧各延伸 1000 像素即得到pt1pt2,从而把"无限长"直线完整画出来。代码用line()以红色Scalar(0,0,255)、线宽 3、抗锯齿LINE_AA绘制。

第四步:应用概率霍夫直线变换并绘制

vector<Vec4i> linesP; HoughLinesP(dst, linesP, 1, CV_PI/180, 50, 50, 10 );

与标准霍夫不同,HoughLinesP额外暴露了两个对线段质量影响极大的参数:

  • minLineLength:构成一条线所需的最少点数。低于该点数的短线会被直接忽略,此处取 50;
  • maxLineGap:判定为同一条直线时,两点之间允许出现的最大间距。此值越大,被拼接成一条长线的"断口容忍度"越高,此处取 10。

由于输出Vec4i l就是线段两端点 $(x_1,y_1,x_2,y_2)$,绘制无需换算,直接连线即可:

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;

程序会弹出三个窗口:原图、标准霍夫结果、概率霍夫结果,随后在waitKey()处阻塞,等待用户按键后退出。这也是几乎所有 OpenCV GUI 示例的标准收尾方式。

Python 版本要点

对于 Python 用户,仓库提供了逐段与 C++ 对应的脚本 samples/python/tutorial_code/ImgTrans/HoughLine/hough_lines.py。核心调用完全一一对应:

import math import cv2 as cv import numpy as np src = cv.imread(cv.samples.findFile('sudoku.png'), cv.IMREAD_GRAYSCALE) dst = cv.Canny(src, 50, 200, None, 3) cdst = cv.cvtColor(dst, cv.COLOR_GRAY2BGR) cdstP = np.copy(cdst) # 标准霍夫:输出 (rho, theta) 列表 lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 0, 0) if lines is not None: for rho, theta in lines.reshape(-1, 2): a, b = math.cos(theta), math.sin(theta) x0, y0 = a * rho, 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 x1, y1, x2, y2 in linesP.reshape(-1, 4): cv.line(cdstP, (x1, y1), (x2, y2), (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()

运行命令(可省略图片参数,默认使用sudoku.png):

python3 samples/python/tutorial_code/ImgTrans/HoughLine/hough_lines.py samples/data/sudoku.png

Java 版本见 samples/java/tutorial_code/ImgTrans/HoughLine/HoughLines.java,调用形式为Imgproc.HoughLines(...)/Imgproc.HoughLinesP(...),读取Mat中的(rho, theta)后同样按"最近点 ± 垂直方向延伸"的方式绘制;此处不再赘述。

进阶演示:带 Trackbar 的 HoughLines_Demo.cpp

官方教程的示例结果来自增强版 HoughLines_Demo.cpp。它的算法逻辑与上面相同,但额外做了三件事:

  1. 为三种变换各开一个窗口与 TrackbarStandard_HoughProbabilistic_Hough共用阈值滑条(范围 50~150),可实时观察阈值对检出线条数的影响;
  2. 演示加权霍夫变换:先用Sobel(src_gray, sobel_edge, CV_16S, 1, 0)求水平方向梯度得到边缘强度图,经convertScaleAbsthresholdTHRESH_TOZERO)处理后,以HoughLines(..., use_edgeval=true)基于边缘强度投票(见函数体内的第三个回调Weighted_Hough);
  3. 滑条回调机制保证了每次拖动阈值都即时重跑检测并刷新画面,非常适合用来培养对阈值参数的直觉。

编译运行方式与普通 OpenCV 程序一致,用法为./HoughLines_Demo <image_name>,默认测试图为building.jpg

运行与编译方法

  • Python:安装带opencv-python的环境后,直接执行上文给出的python3命令即可;
  • C++:仓库在 samples/CMakeLists.txt 提供了整套示例的 CMake 构建方案。可按 OpenCV 常规方式配置源码构建目录,编译目标houghlinesHoughLines_Demo;随后在已设置好OPENCV_SAMPLES_DATA_PATH的情况下运行,samples::findFile会自动定位sudoku.png等样本数据;
  • 传入自己的图片时,仅需把第一个命令行参数替换为图片路径即可,例如./houghlines /path/to/your/image.png

结果展示与阈值调参

以仓库自带的 sudoku.png(数独棋盘)为输入,标准霍夫直线变换的检测结果(红线上叠加于原图)如下:

概率霍夫直线变换的结果如下:

对比两图可以发现:标准霍夫输出的是贯穿整幅图像的"无限长"直线(数独网格的横竖线被还原为长直线);而概率霍夫直接输出线段,对图像边缘残缺、断续的容错性更好,适合定位如纸张边缘、车道线这类真实存在的有限线段。

调参规律(在带 Trackbar 的 Demo 中反复拖动即可验证):改变threshold会显著改变检出的线条数——道理很直观:阈值越高,意味着"声明一条直线所需的共线点数越多",只有更长、更连续的边缘才能达标,因而检出的直线更少;反之阈值越低,检出的直线越多,同时噪声造成的假阳性也会增多。实际项目中应结合minLineLength(过滤短线碎片)与maxLineGap(容忍断点拼接)协同调节。

小结

  • 霍夫直线变换把"图像空间中的直线检测"转化为"参数空间中的峰值投票":每个边缘点在 $(\theta, r)$ 平面画一条正弦曲线,曲线交点即共线证据,票数超过threshold即判定为直线;
  • 标准霍夫(HoughLines输出 $(ρ, θ)$ 对,直线无端点概念,适合检测"贯穿性结构"(网格、地平线),配合srn/stn还可切换为多尺度霍夫,配合use_edgeval=true可切换为加权霍夫;
  • 概率霍夫(HoughLinesP输出线段端点,速度快、可直接得到线段几何,额外受minLineLengthmaxLineGap约束,是多数工程场景的首选;
  • 两条 API 都要求输入8 位单通道二值边缘图,且可能原地修改输入图像,记得传入 Canny 的输出版本并保存原图用于叠加绘制。

想继续深入本系列,可接着阅读仓库中的 霍夫圆变换教程(HoughCircles检测圆)与前置的 Canny 边缘检测教程。所有示例源码、输入图片与 API 文档均可在本文引用的仓库路径下找到,动手运行一遍即可直观掌握每种变换的行为差异。

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询