目录
[2.1 原理简述](#2.1 原理简述)
[2.2 先说结论:用 HoughLinesP](#2.2 先说结论:用 HoughLinesP)
[2.3 代码上手](#2.3 代码上手)
[2.4 参数怎么调](#2.4 参数怎么调)
[2.5 车道线检测实例](#2.5 车道线检测实例)
[2.6 HoughLines 的用法](#2.6 HoughLines 的用法)
[3.1 原理](#3.1 原理)
[3.2 代码上手](#3.2 代码上手)
[3.3 参数调优](#3.3 参数调优)
[3.4 硬币计数](#3.4 硬币计数)
一、为什么要学霍夫变换
做图像处理的同学,迟早会遇到一个问题:怎么从一张图里把直线或者圆"抠"出来?
比如做车道线检测,你需要从道路图像中找到车道标线;做仪表读数识别,得先找到表盘的圆形轮廓;做文档扫描,得检测文档的边缘直线。这些场景背后都指向同一个技术------霍夫变换(Hough Transform)。
简单说,霍夫变换干的事情就是:在图像里找特定形状。直线、圆是它最擅长的两种。
OpenCV 里对应的函数有三个:
cv2.HoughLines:找直线,返回 (ρ, θ) 参数 ;
cv2.HoughLinesP: 找直线,返回线段端点(实际更常用)
cv2.HoughCircles:找圆
下面逐个讲。
二、霍夫直线检测
2.1 原理简述
一条直线用 y = mx + b 表示有个麻烦:垂直线斜率无穷大,没法算。霍夫变换换了个思路,用极坐标来表示直线:
ρ = x·cos(θ) + y·sin(θ)
ρ 是原点到直线的垂直距离,θ 是法线方向与 x 轴的夹角。这样一来,图像里的一条直线就变成了参数空间里的一个点 (ρ, θ)。
反过来想:图像里一个点 (x₀, y₀),通过它可以做无数条直线,对应参数空间里一条正弦曲线。如果图像里多个点共线,那它们在参数空间里对应的正弦曲线就会交于同一点。
霍夫变换做的事情就是:遍历图像中所有边缘点,在参数空间里"投票",最后票数最高的位置就是检测到的直线。
2.2 先说结论:用 HoughLinesP
实际开发中,几乎总是优先选 `cv2.HoughLinesP`(概率霍夫变换),而不是 `cv2.HoughLines`(标准霍夫变换)。原因很简单:
HoughLinesP 直接返回线段端点坐标 (x1, y1, x2, y2),拿来就能画
HoughLines 返回 (ρ, θ),你还得自己算端点坐标
HoughLinesP 速度更快,因为用了随机采样
2.3 代码上手
python
import cv2
import numpy as np
image = cv2.imread('road.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#霍夫变换的前提:二值边缘图
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
#检测直线
lines = cv2.HoughLinesP(
edges,
rho=1, #ρ 精度,1个像素
theta=np.pi / 180, #θ 精度,1度
threshold=100, #累加器阈值,票数超过这个才算直线
minLineLength=50, #最短线段长度
maxLineGap=10 #允许的最大间断
)
#画出来
result = image.copy()
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(result, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow('result', result)
cv2.waitKey(0)
2.4 参数怎么调
这是用霍夫变换最头疼的地方。参数不对,要么什么都检测不到,要么一堆噪声直线。
检测不到直线------大概率是 threshold 设太高了:
```python
#从 100 降到 50 试试
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=50,
minLineLength=50, maxLineGap=10)
```
太多噪声线------threshold 太低,或者 minLineLength 太短:
```python
#提高门槛,加长最小线段
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=200,
minLineLength=100, maxLineGap=10)
```
直线断成好几段------maxLineGap 太小:
```python
#增大允许的间断距离
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100,
minLineLength=50, maxLineGap=30)
```
调参没有万能公式,得根据具体场景反复试。我的经验是:先把 threshold 和 minLineLength 设大一点,保证检测出来的都是真直线,然后慢慢降低门槛,直到把需要的线都找出来。
2.5 车道线检测实例
一个经典的应用场景。核心思路:先做边缘检测,然后限制检测区域(只关注道路部分),最后跑霍夫变换。
python
import cv2
import numpy as np
def detect_lanes(image):
h, w = image.shape[:2]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blur, 50, 150)
#只保留道路区域的三角形
mask = np.zeros_like(edges)
roi = np.array([[
(int(w*0.1), h),
(int(w*0.45), int(h*0.6)),
(int(w*0.55), int(h*0.6)),
(int(w*0.9), h)
]], dtype=np.int32)
cv2.fillPoly(mask, roi, 255)
masked_edges = cv2.bitwise_and(edges, mask)
lines = cv2.HoughLinesP(masked_edges, 1, np.pi/180,
threshold=50, minLineLength=100, maxLineGap=150)
result = image.copy()
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(result, (x1, y1), (x2, y2), (0, 255, 0), 3)
return result
ROI 这一步很关键。不做区域限制的话,路边的树木、建筑边缘也会被检测成直线,干扰很大。
2.6 HoughLines 的用法
虽然用得少,但有时候你需要直线的数学表示(比如算角度),这时候标准霍夫变换反而方便:
python
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=200)
result = image.copy()
if lines is not None:
for line in lines:
rho, theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
#把直线延长到图像边界
x1 = int(x0 + 2000 * (-b))
y1 = int(y0 + 2000 * a)
x2 = int(x0 - 2000 * (-b))
y2 = int(y0 - 2000 * a)
cv2.line(result, (x1, y1), (x2, y2), (0, 0, 255), 2)
三、霍夫圆检测
3.1 原理
圆有三个参数:圆心 (xc, yc) 和半径 r。直接做三维投票计算量太大,OpenCV 的实现用了个技巧------基于梯度的两步法:
-
先利用边缘梯度信息找出可能的圆心(二维投票)
-
对每个候选圆心,搜索最优半径(一维搜索)
这样就把三维问题降成了二维+一维,效率提升不少。
3.2 代码上手
python
import cv2
import numpy as np
image = cv2.imread('coins.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#圆检测对噪声很敏感,先做中值滤波
gray_blur = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(
gray_blur,
method=cv2.HOUGH_GRADIENT, #目前只支持这一种方法
dp=1, #累加器分辨率,1表示与图像同分辨率
minDist=20, #圆心之间最小距离
param1=50, #Canny 高阈值(低阈值自动取一半)
param2=30, #圆心检测的累加器阈值
minRadius=0, #最小圆半径
maxRadius=0 #最大圆半径,0表示不限
)
result = image.copy()
if circles is not None:
circles = np.uint16(np.around(circles))
for c in circles[0, :]:
cx, cy, r = c[0], c[1], c[2]
cv2.circle(result, (cx, cy), r, (0, 255, 0), 2) #画圆
cv2.circle(result, (cx, cy), 2, (0, 0, 255), 3) #画圆心
cv2.imshow('circles', result)
cv2.waitKey(0)
3.3 参数调优
圆检测的参数比直线检测更多,也更难调。几个关键经验:
param2 是核心参数。它相当于累加器的阈值------param2 越小,检测到的圆越多(包括假的);越大,漏检越多。一般从 30-50 开始试。
minDist 决定能不能区分相邻的圆。设太小了,同一个圆可能被检测多次;设太大了,相邻的圆会被合并。建议至少设为最小圆半径。
param1 控制边缘检测的灵敏度。它其实是 Canny 的高阈值,低阈值自动取 param1 的一半。通常 50-150 之间够用。
minRadius 和 maxRadius 尽量设上。如果你知道要检测的圆大概多大,一定要限制范围。不设的话搜索空间太大,又慢又容易出假圆。
一个常见的坑:输入图像不做滤波就直接检测。圆检测对噪声比直线检测更敏感,中值滤波这一步不能省。
3.4 硬币计数
一个实用的例子:
python
import cv2
import numpy as np
def count_coins(image_path):
image = cv2.imread(image_path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(
gray, cv2.HOUGH_GRADIENT, dp=1, minDist=30,
param1=150, param2=30, minRadius=15, maxRadius=50
)
result = image.copy()
count = 0
if circles is not None:
circles = np.uint16(np.around(circles))
count = len(circles[0])
for i, c in enumerate(circles[0, :]):
cx, cy, r = c[0], c[1], c[2]
cv2.circle(result, (cx, cy), r, (0, 255, 0), 2)
cv2.circle(result, (cx, cy), 2, (0, 0, 255), 3)
cv2.putText(result, str(i+1), (cx-5, cy-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
cv2.putText(result, f"Total: {count}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
print(f"检测到 {count} 个硬币")
return result
四、预处理很重要
不管你检测的是直线还是圆,输入图像的质量直接决定结果好坏。以下是我总结的一套预处理流程:
python
def preprocess(image, detect_type='line'):
"""根据检测类型做预处理"""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image.copy()
#对比度增强
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
#去噪------直线用高斯,圆用中值
if detect_type == 'circle':
gray = cv2.medianBlur(gray, 5)
else:
gray = cv2.GaussianBlur(gray, (5, 5), 0)
#边缘检测
edges = cv2.Canny(gray, 50, 150)
return gray, edges
另外一个技巧:用 ROI 限制检测区域。如果你知道目标大概出现在图像的哪个位置,就先裁出来再检测,既快又准。
五、文档倾斜校正
霍夫变换的一个有趣应用:检测文档的倾斜角度,然后校正。
思路很直接------文档里的文字行是水平的,如果检测到文字行有角度,那文档就是歪的。
python
import cv2
import numpy as np
def detect_skew(image):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.bitwise_not(gray) #文字变白
edges = cv2.Canny(gray, 50, 150)
lines = cv2.HoughLinesP(edges, 1, np.pi/180,
threshold=100, minLineLength=100, maxLineGap=20)
if lines is None:
return 0.0
#收集所有接近水平的直线角度
angles = []
for line in lines:
x1, y1, x2, y2 = line[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if -45 < angle < 45:
angles.append(angle)
if not angles:
return 0.0
#取中位数,比平均值更鲁棒
return float(np.median(angles))
def deskew(image_path):
image = cv2.imread(image_path)
angle = detect_skew(image)
print(f"倾斜角度: {angle:.2f}°")
if abs(angle) < 0.5:
return image #角度太小,不校正
h, w = image.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, -angle, 1.0)
rotated = cv2.warpAffine(image, M, (w, h),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
return rotated
六、常见问题
Q:检测不到任何直线/圆?
先检查边缘图(cv2.imshow('edges', edges)),看看 Canny 输出的边缘是否清晰。如果边缘本身就糊了,霍夫变换不可能有好结果。然后降低 threshold/param2 试试。
Q:检测出一堆乱七八糟的线?
输入图像噪声太多。加中值滤波或高斯滤波。另外提高 threshold 和 minLineLength。
Q:同一个圆被检测了好几次?
minDist 设太小了。把它调大,至少大于最小圆的半径。
Q:霍夫变换太慢?
几个办法:1)缩小图像分辨率再检测,结果缩放回去;2)用 ROI 限制区域;3)圆检测设 dp=2;4)限制 minRadius/maxRadius 缩小搜索空间。
Q:能检测椭圆吗?
OpenCV 没有内置的霍夫椭圆检测。替代方案是用 cv2.findContours找轮廓,然后用 cv2.fitEllipse拟合椭圆。
七、小结
霍夫变换本质上就是在参数空间里做投票。直线检测用极坐标 (ρ, θ),圆检测用 (xc, yc, r)。实际开发中记住几点:
直线检测用 `HoughLinesP`,别用 `HoughLines`
圆检测前一定要做中值滤波
参数调优没有捷径,得多试。先保守(高阈值),再放宽
尽量限制 ROI 和半径范围,减少搜索空间
预处理(去噪、增强对比度)比调参更重要