边缘检测与轮廓提取:Canny算子、霍夫变换、轮廓分析完整指南

引言

你有没有想过,自动驾驶汽车是怎么“看见”车道线的?手机APP是怎么一键测量杯子直径的? 答案藏在今天要聊的两个基础且硬核的计算机视觉技术里——边缘检测轮廓提取

简单来说:

  • 边缘:是图像中亮度/颜色“跳变”最剧烈的像素点集合,对应物体边界、阴影、纹理变化等;
  • 轮廓:是把这些跳变的点连起来形成的闭合曲线,能直接框出我们关心的“形状”。

这俩技术是传统CV的“敲门砖”,也是很多高级任务的前置步骤。

📂 所属阶段:第一阶段 — 图像处理基石(传统 CV 篇)
🔗 前置/后续章节:图像增强与滤波 · 特征匹配实战


1. 边缘检测基础概念

1.1 什么是边缘?

边缘是图像的“高频信号”,通常出现在:

  • 物体与背景的交界处
  • 不同材质/纹理的过渡区
  • 光照阴影的边缘
  • 颜色的突变位置

1.2 数学原理:梯度

边缘检测本质上是寻找图像梯度的局部最大值

  • 梯度幅值:衡量亮度变化的强度(幅值越大,越可能是边缘)
  • 梯度方向:指示亮度变化的方向(边缘垂直于梯度方向)
import numpy as np
import cv2

def compute_gradient_mag(image):
    """计算Sobel梯度幅值(简化边缘检测演示)"""
    gray = cv2.imread(image, 0) if isinstance(image, str) else image
    # Sobel算子(一阶导数,可检测x/y方向)
    grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
    grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
    # 计算梯度幅值(归一化到0-255方便显示)
    mag = np.sqrt(grad_x**2 + grad_y**2)
    return cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)

2. Canny边缘检测详解

Canny算子是John F. Canny在1986年提出的最优边缘检测算法,至今仍是工业界的“黄金标准”。

2.1 算法四步走

  1. 高斯滤波:平滑图像,去除噪声(噪声会被误判为边缘)
  2. 计算梯度:用Sobel/Scharr算子求梯度幅值和方向
  3. 非极大值抑制(NMS):细化边缘(只保留梯度幅值的“峰值点”)
  4. 滞后阈值:用双阈值区分“真边缘”和“假边缘”

2.2 代码实现与参数调整

def auto_canny(image, sigma=0.33):
    """
    自动计算Canny阈值的最佳实践(基于图像中值)
    sigma越小,阈值越窄,检测到的边缘越多(可能含噪声)
    sigma越大,阈值越宽,边缘越干净(可能丢失细节)
    """
    gray = cv2.imread(image, 0) if isinstance(image, str) else image
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)  # 前置高斯滤波
    
    v = np.median(blurred)
    lower = int(max(0, (1.0 - sigma) * v))
    upper = int(min(255, (1.0 + sigma) * v))
    return cv2.Canny(blurred, lower, upper)

# 快速调用
edges = auto_canny("test.jpg")
cv2.imshow("Auto Canny", edges)
cv2.waitKey(0)
- **高斯核大小**:通常用(3,3)/(5,5)/(7,7),越大平滑效果越强(只保留大边缘) - **双阈值比例**:手动设置时,建议`upper:lower`在`2:1 ~ 3:1`之间

3. 霍夫变换详解

霍夫变换是特征提取技术,专门用来检测图像中的规则几何形状(直线、圆、椭圆等)。

3.1 核心原理(直线检测)

将图像空间的“点→线”问题,转换为参数空间的“线→点”投票问题

  • 图像中1个共线点→参数空间中1条线
  • 参数空间中多条线的交点→图像中直线的参数

为避免垂直线的斜率问题,工业界常用极坐标形式
ρ = x·cosθ + y·sinθ(ρ是原点到直线的距离,θ是直线法线与x轴的夹角)

3.2 实战:概率霍夫直线检测

概率霍夫变换(HoughLinesP)比标准霍夫变换更高效、更实用,直接返回线段的端点坐标。

def detect_lines(image_path):
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edges = auto_canny(gray)  # 复用前面的自动Canny
    
    # 概率霍夫变换参数
    lines = cv2.HoughLinesP(
        edges,
        rho=1,          # ρ的精度(像素)
        theta=np.pi/180,# θ的精度(弧度,1度)
        threshold=50,   # 累加器阈值(线段上的最小点数)
        minLineLength=50,# 最小线段长度
        maxLineGap=10   # 同一直线上两点的最大间隙(可连接断续线段)
    )
    
    # 绘制检测到的直线
    result = img.copy()
    if lines is not None:
        for x1,y1,x2,y2 in lines[:,0]:
            cv2.line(result, (x1,y1), (x2,y2), (0,255,0), 2)
    return result

3.3 霍夫圆检测

圆的参数是(x0,y0,r)(圆心坐标+半径),所以是三维投票,计算量更大,通常需要先高斯模糊再检测。

def detect_circles(image_path):
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (9,9), 2)  # 强模糊去噪
    
    circles = cv2.HoughCircles(
        blurred,
        cv2.HOUGH_GRADIENT,
        dp=1,           # 累加器分辨率(dp=1时与原图一致)
        minDist=20,     # 圆心之间的最小距离(避免重复检测)
        param1=50,      # Canny的高阈值
        param2=30,      # 圆心检测阈值(越小圆越多)
        minRadius=5,    # 最小半径
        maxRadius=100   # 最大半径
    )
    
    # 绘制圆和圆心
    result = img.copy()
    if circles is not None:
        circles = np.round(circles[0,:]).astype("int")
        for x,y,r in circles:
            cv2.circle(result, (x,y), r, (0,255,0), 2)
            cv2.circle(result, (x,y), 2, (0,0,255), 3)
    return result

4. 轮廓提取与分析

轮廓是边缘检测的“升级版”,它能直接获取物体的完整边界几何特征

4.1 轮廓提取基础

轮廓提取需要二值图像(黑白图,黑背景白前景),OpenCV的findContours函数会返回轮廓列表和层次结构。

def extract_contours(image_path):
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化(也可用自适应二值化处理光照不均)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)  # 反转:黑前景变白
    
    # 查找轮廓
    # RETR_EXTERNAL:只检测最外层轮廓
    # CHAIN_APPROX_SIMPLE:压缩水平/垂直/对角线的冗余点
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 绘制轮廓(-1表示绘制所有轮廓)
    result = img.copy()
    cv2.drawContours(result, contours, -1, (0,255,0), 2)
    return result

4.2 轮廓几何特征分析

轮廓能提取很多有用的特征,用于形状识别尺寸测量

def analyze_contour(contour):
    """分析单个轮廓的核心特征"""
    # 1. 基础特征
    area = cv2.contourArea(contour)          # 面积
    perimeter = cv2.arcLength(contour, True) # 周长(True表示闭合)
    
    # 2. 边界矩形
    x,y,w,h = cv2.boundingRect(contour)      # 轴对齐边界矩形
    aspect_ratio = w/h                        # 长宽比
    
    # 3. 圆度(判断是否接近圆)
    circularity = 4 * np.pi * area / (perimeter**2) if perimeter > 0 else 0
    
    # 4. 凸包(判断是否是凸多边形)
    hull = cv2.convexHull(contour)
    solidity = area / cv2.contourArea(hull) if cv2.contourArea(hull) > 0 else 0
    
    return {
        "area": area,
        "perimeter": perimeter,
        "aspect_ratio": aspect_ratio,
        "circularity": circularity,
        "solidity": solidity,
        "bounding_box": (x,y,w,h)
    }

5. 实战项目:简单形状检测器

结合轮廓分析和轮廓近似(approxPolyDP),我们可以快速识别三角形、矩形、正方形、圆形等常见形状。

class SimpleShapeDetector:
    def detect(self, contour):
        # 1. 轮廓近似(压缩轮廓的拐点)
        perimeter = cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, 0.04*perimeter, True)
        vertices = len(approx)
        
        # 2. 核心特征
        features = analyze_contour(contour)
        
        # 3. 形状判断
        if vertices == 3:
            return "Triangle"
        elif vertices == 4:
            return "Square" if 0.95 < features["aspect_ratio"] < 1.05 else "Rectangle"
        elif vertices == 5:
            return "Pentagon"
        elif features["circularity"] > 0.8:
            return "Circle"
        else:
            return "Polygon"
    
    def detect_in_image(self, image_path):
        img = cv2.imread(image_path)
        binary = cv2.threshold(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 127, 255, cv2.THRESH_BINARY_INV)[1]
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        result = img.copy()
        for c in contours:
            if cv2.contourArea(c) < 200:  # 过滤小噪声
                continue
            # 识别形状并获取中心点
            shape = self.detect(c)
            M = cv2.moments(c)
            cX = int(M["m10"]/M["m00"]) if M["m00"] !=0 else 0
            cY = int(M["m01"]/M["m00"]) if M["m00"] !=0 else 0
            
            # 绘制结果
            cv2.drawContours(result, [c], -1, (0,255,0), 2)
            cv2.putText(result, shape, (cX-20, cY), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
        return result

6. 总结

边缘检测与轮廓提取是传统CV的核心技能,以下是关键要点:

技术核心功能适用场景
Canny算子最优边缘检测通用边缘提取、轮廓前置处理
霍夫变换检测规则几何形状车道线检测、硬币检测、表盘刻度
轮廓提取/分析获取物体边界+几何特征形状识别、尺寸测量、物体计数
建议多调整参数观察效果,理解每种方法的**局限性**(比如霍夫变换对噪声敏感,轮廓提取依赖清晰的二值图像),实际项目中通常需要组合使用预处理(滤波、二值化)、边缘检测、轮廓分析。

🔗 扩展阅读