基本原理

Canny 邊緣檢測是一種經典且常用的圖像邊緣提取算法,由 John Canny 在 1986 年提出。它在 OpenCV 中得到了高效實現,是結構化光、SLAM、醫學影像分析與機器視覺領域中最常用的邊緣檢測方法之一。其目標是以最優方式識別圖像中的邊緣,既能準確檢測真實邊緣,又能保持較強的抗噪能力與定位精度。

Canny 算法之所以經典,是因為它通過數學推導給出了邊緣檢測的三個最優準則:

  1. 檢測率高(Good Detection):儘可能找到真實邊緣;
  2. 定位準確(Good Localization):檢測到的邊緣點應準確落在真實邊緣位置;
  3. 一次響應(Minimal Response):相同邊緣只產生一條響應,避免多條邊緣線。

Canny 算法實現包含 5 個關鍵步驟:

第一步:噪聲抑制(Gaussian Blur)

邊緣通常對應灰度變化劇烈的區域,但噪聲也會產生劇烈變化。如果不去噪,邊緣會出現大量虛假響應。因此 Canny 算法的第一步是使用高斯濾波進行平滑:

GaussianBlur(src, blur, Size(3,3), 1.0);

高斯濾波使用如下數學形式的卷積核:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_OpenCV

σ 越大,平滑越強,但也會模糊真實邊緣。

第二步:計算梯度(Sobel)

Canny 使用 Sobel 算子計算圖像在水平方向與垂直方向的梯度:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_#計算機視覺_02

OpenCV 內部相當於:

Sobel(blur, grad_x, CV_16S, 1, 0);
Sobel(blur, grad_y, CV_16S, 0, 1);

然後得到:

梯度幅值:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_#計算機視覺_03

梯度方向:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_邊緣檢測_04

梯度方向説明邊緣的方向,梯度越大表示邊緣越強。

第三步:非極大值抑制(NMS)

未經處理的梯度圖會出現寬厚的邊緣帶,這不符合 “一次響應” 原則。NMS 的目標是 細化邊緣

  1. 根據梯度方向確定像素的比較方向(四種情況:0°、45°、90°、135°)
  2. 當前像素的梯度如果不是局部最大值,則置為 0

這種方法能有效將粗邊縮減為優美細線。

第四步:雙閾值檢測(Double Threshold)

Canny 的創新點之一就是“雙閾值”,使用高低兩個閾值 T1(低)、T2(高):

  • 強邊緣:G > T2
  • 弱邊緣:T1 < G ≤ T2
  • 非邊緣:G ≤ T1

為何要這樣做?

  • 邊緣是連通的,一個強邊緣附近往往會有梯度較弱的像素;
  • 如果只用單閾值,弱邊可能被直接丟棄;
  • 雙閾值允許保留“可能屬於邊緣”的弱像素。

第五步:邊緣連接(Hysteresis)

弱邊緣並不是全部保留,而是需要與強邊緣連接才認為是真實邊。

過程如下:

  1. 對所有強邊緣保留;
  2. 對弱邊緣,若與強邊相鄰(8 連接),則保留;
  3. 其他弱邊緣捨棄。

最終得到乾淨、連續、不重複的邊緣線。

OpenCV 中的 Canny 函數

OpenCV 在 Python 中提供的 Canny 邊緣檢測函數原型如下:

cv2.Canny(image, threshold1, threshold2, edges=None, apertureSize=3, L2gradient=False)

1. image(輸入圖像)

  • 類型:uint8 單通道(灰度圖)
  • 説明:如果傳入的是彩色圖,OpenCV 不會報錯,但會自動將像素分量混合,使結果不可控。

建議永遠手動轉換為灰度圖:

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

2. threshold1(低閾值)

  • 用於雙閾值檢測中的“弱邊緣”判斷。
  • 越低 → 保留越多弱邊緣(噪聲也更多)
  • 越高 → 結果越乾淨,但容易漏掉細節邊緣

一般經驗:

low = 0.66 * 中位數

3. threshold2(高閾值)

  • 用於判定“強邊緣”
  • 邊緣必須 > threshold2 才能直接保留為強邊

常用經驗規則:

threshold2 ≈ 2 × threshold1

建議:

  • 圖像乾淨 → 高閾值可放寬
  • 圖像噪聲大 → 高閾值應提高

4. edges(輸出圖像,可忽略)

一般不需要傳入,由 OpenCV 創建:

edges = cv2.Canny(image, 50, 150)

5. apertureSize(Sobel 算子卷積核尺寸)

  • 默認值:3
  • 可選:3, 5, 7

它決定了梯度計算的卷積核大小(Sobel 算子大小),影響梯度平滑程度。

apertureSize

特點

3

默認,精度好

5

邊緣更平滑,適合噪聲較大圖像

7

更強的平滑,適合強噪聲環境

6. L2gradient(梯度幅值計算方式)

  • 默認:False
  • 控制梯度幅值計算方法:

False(默認)

使用簡單加和:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_#opencv_05

速度較快,在多數應用中足夠。

True

使用更準確的歐幾里得距離:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_#opencv_06

更精確但計算更慢。

如果你對邊緣精度要求高,應啓用 L2gradient:

edges = cv2.Canny(gray, 50, 150, L2gradient=True)

示例

import cv2
import numpy as np

# 1. 讀取圖像
img = cv2.imread("test.jpg")

# 檢查是否讀取成功
if img is None:
    raise FileNotFoundError("無法找到圖像,請檢查路徑 test.jpg 是否存在!")

# 2. 轉灰度圖
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 3. 高斯濾波(Canny 內部也會處理,但建議手動提高效果)
blur = cv2.GaussianBlur(gray, (3, 3), 1)

# 4. 設置 Canny 雙閾值
low_threshold = 50
high_threshold = 150

# 5. 執行 Canny 邊緣檢測
edges = cv2.Canny(
    blur,
    low_threshold,
    high_threshold,
    apertureSize=3,
    L2gradient=True  # 使用更精確的梯度計算
)

# 6. 顯示結果
cv2.imshow("Original", img)
cv2.imshow("Gray", gray)
cv2.imshow("Canny Edges", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()

執行效果:

OpenCV——邊緣檢測入門、Canny邊緣檢測_51CTO博客_#人工智能_07