OpenCV 霍夫变换:直线与圆检测

目录

一、为什么要学霍夫变换

二、霍夫直线检测

[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 的实现用了个技巧------基于梯度的两步法:

  1. 先利用边缘梯度信息找出可能的圆心(二维投票)

  2. 对每个候选圆心,搜索最优半径(一维搜索)

这样就把三维问题降成了二维+一维,效率提升不少。

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)。实际开发中记住几点:

  1. 直线检测用 `HoughLinesP`,别用 `HoughLines`

  2. 圆检测前一定要做中值滤波

  3. 参数调优没有捷径,得多试。先保守(高阈值),再放宽

  4. 尽量限制 ROI 和半径范围,减少搜索空间

  5. 预处理(去噪、增强对比度)比调参更重要