基本原理
Canny 邊緣檢測是一種經典且常用的圖像邊緣提取算法,由 John Canny 在 1986 年提出。它在 OpenCV 中得到了高效實現,是結構化光、SLAM、醫學影像分析與機器視覺領域中最常用的邊緣檢測方法之一。其目標是以最優方式識別圖像中的邊緣,既能準確檢測真實邊緣,又能保持較強的抗噪能力與定位精度。
Canny 算法之所以經典,是因為它通過數學推導給出了邊緣檢測的三個最優準則:
- 檢測率高(Good Detection):儘可能找到真實邊緣;
- 定位準確(Good Localization):檢測到的邊緣點應準確落在真實邊緣位置;
- 一次響應(Minimal Response):相同邊緣只產生一條響應,避免多條邊緣線。
Canny 算法實現包含 5 個關鍵步驟:
第一步:噪聲抑制(Gaussian Blur)
邊緣通常對應灰度變化劇烈的區域,但噪聲也會產生劇烈變化。如果不去噪,邊緣會出現大量虛假響應。因此 Canny 算法的第一步是使用高斯濾波進行平滑:
GaussianBlur(src, blur, Size(3,3), 1.0);
高斯濾波使用如下數學形式的卷積核:
σ 越大,平滑越強,但也會模糊真實邊緣。
第二步:計算梯度(Sobel)
Canny 使用 Sobel 算子計算圖像在水平方向與垂直方向的梯度:
OpenCV 內部相當於:
Sobel(blur, grad_x, CV_16S, 1, 0);
Sobel(blur, grad_y, CV_16S, 0, 1);
然後得到:
梯度幅值:
梯度方向:
梯度方向説明邊緣的方向,梯度越大表示邊緣越強。
第三步:非極大值抑制(NMS)
未經處理的梯度圖會出現寬厚的邊緣帶,這不符合 “一次響應” 原則。NMS 的目標是 細化邊緣:
- 根據梯度方向確定像素的比較方向(四種情況:0°、45°、90°、135°)
- 當前像素的梯度如果不是局部最大值,則置為 0
這種方法能有效將粗邊縮減為優美細線。
第四步:雙閾值檢測(Double Threshold)
Canny 的創新點之一就是“雙閾值”,使用高低兩個閾值 T1(低)、T2(高):
- 強邊緣:G > T2
- 弱邊緣:T1 < G ≤ T2
- 非邊緣:G ≤ T1
為何要這樣做?
- 邊緣是連通的,一個強邊緣附近往往會有梯度較弱的像素;
- 如果只用單閾值,弱邊可能被直接丟棄;
- 雙閾值允許保留“可能屬於邊緣”的弱像素。
第五步:邊緣連接(Hysteresis)
弱邊緣並不是全部保留,而是需要與強邊緣連接才認為是真實邊。
過程如下:
- 對所有強邊緣保留;
- 對弱邊緣,若與強邊相鄰(8 連接),則保留;
- 其他弱邊緣捨棄。
最終得到乾淨、連續、不重複的邊緣線。
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(默認)
使用簡單加和:
速度較快,在多數應用中足夠。
True
使用更準確的歐幾里得距離:
更精確但計算更慢。
如果你對邊緣精度要求高,應啓用 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()
執行效果: