歡迎來到 OpenCV 圖像處理的第二部分!在第一部分,我們學習了如何加載、顯示、保存圖像以及訪問像素等基礎知識。現在,我們將深入探索如何利用 OpenCV 提供的強大工具來修改和分析圖像。
圖像處理是計算機視覺領域的基石。通過對圖像進行各種操作,我們可以改善圖像質量、提取重要特征、準備圖像用于更復雜的任務(如目標識別或圖像分割)。
本部分將涵蓋以下關鍵主題:
- 圖像增強與濾波
- 亮度與對比度調整
- 圖像平滑(均值濾波、高斯濾波)
- 圖像銳化(拉普拉斯算子)
- 圖像形態學操作
- 腐蝕與膨脹
- 開運算與閉運算
- 形態學梯度
- 邊緣檢測
- Sobel 算子
- Canny 邊緣檢測
- 實戰:檢測圖像中的邊緣
讓我們開始吧!
1. 圖像增強與濾波
圖像增強旨在改善圖像的視覺效果或為后續處理提供更好的輸入。濾波則是通過對圖像像素及其鄰域像素進行計算,來達到平滑、銳化或提取特征的目的。
1.1 亮度與對比度調整
調整圖像的亮度和對比度是最常見的圖像增強操作之一。
- 亮度 (Brightness): 控制圖像的整體明暗程度。增加亮度就像給圖像"加光"。
- 對比度 (Contrast): 控制圖像中明暗區域之間的差異程度。增加對比度會使亮區更亮,暗區更暗,圖像看起來更"鮮明"。
數學上,簡單的亮度和對比度調整可以通過線性變換實現:
OpenCV 提供了 cv2.convertScaleAbs()
函數來實現這個線性變換,并且會自動處理像素值超出 [0, 255] 范圍的情況(截斷到 0 或 255)。
Python
import cv2
import numpy as np# --- 練習 1.1: 調整圖像亮度與對比度 ---# 1. 加載圖像
# 請替換成你自己的圖片路徑
image_path = 'your_image.jpg'
try:image = cv2.imread(image_path)if image is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")# 使用一個虛擬圖片代替,以便代碼可以運行(實際操作中請替換)image = np.zeros((300, 500, 3), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)print(f"原始圖像尺寸: {image.shape}")# 2. 定義調整參數
alpha = 1.5 # 對比度控制 (增益),大于1增強對比度
beta = 30 # 亮度控制 (偏置),正數增加亮度# 3. 應用線性變換進行亮度與對比度調整
# 注意: cv2.convertScaleAbs 會自動將結果轉換為 uint8 并取絕對值 (雖然對于亮度/對比度調整,結果通常是非負的)
adjusted_image = cv2.convertScaleAbs(image, alpha=alpha, beta=beta)# 4. 顯示原始圖像和調整后的圖像
cv2.imshow('Original Image', image)
cv2.imshow('Adjusted Image (alpha=1.5, beta=30)', adjusted_image)# 5. 嘗試不同的參數組合
alpha_low_contrast = 0.7
beta_dark = -50
adjusted_low_contrast_dark = cv2.convertScaleAbs(image, alpha=alpha_low_contrast, beta=beta_dark)
cv2.imshow('Adjusted Image (alpha=0.7, beta=-50)', adjusted_low_contrast_dark)# 等待按鍵,然后關閉窗口
cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 1.1 完成 ---")
練習提示:
- 嘗試不同的
alpha
和beta
值,觀察圖像的變化。 alpha
可以是小數(例如 0.5 會降低對比度)。beta
可以是負數(例如 -50 會降低亮度)。
1.2 圖像平滑 (濾波)
圖像平滑(也稱為模糊)是一種常用的濾波技術,主要用于減少圖像中的噪聲或在進行邊緣檢測等操作前平滑圖像。平滑的原理是使用一個核(Kernel)或掩膜(Mask)與圖像進行卷積。核是一個小矩陣,它在圖像上滑動,在每個位置,將核的元素與對應的圖像像素值相乘,然后將結果求和,得到中心像素的新值。
均值濾波 (Mean Filter)
Python
import cv2
import numpy as np# --- 練習 1.2.1: 均值濾波 ---# 1. 加載圖像
image_path = 'your_image.jpg'
try:image = cv2.imread(image_path)if image is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")image = np.zeros((300, 500, 3), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)# 2. 定義核的大小 (必須是正奇數,如 (3, 3), (5, 5) 等)
ksize_mean = (5, 5)# 3. 應用均值濾波
# cv2.blur() 函數用于進行均值濾波
# 參數: src (輸入圖像), ksize (核大小)
mean_blurred_image = cv2.blur(image, ksize_mean)# 4. 顯示原始圖像和均值濾波后的圖像
cv2.imshow('Original Image', image)
cv2.imshow(f'Mean Blurred Image (ksize={ksize_mean})', mean_blurred_image)# 5. 嘗試不同的核大小
ksize_mean_large = (15, 15)
mean_blurred_image_large = cv2.blur(image, ksize_mean_large)
cv2.imshow(f'Mean Blurred Image (ksize={ksize_mean_large})', mean_blurred_image_large)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 1.2.1 完成 ---")
練習提示:
- 嘗試使用不同大小的核。核越大,平滑效果越明顯,但圖像細節丟失越多。
- 均值濾波對去除“椒鹽噪聲”等隨機噪聲有一定效果。
高斯濾波 (Gaussian Filter)
高斯濾波是一種更常用的平滑濾波器。它使用一個符合高斯分布(正態分布)的核。與均值濾波不同,高斯核中的元素不是相等的,而是距離核中心越近的元素權重越大,距離越遠權重越小。這使得高斯濾波在平滑圖像的同時,能夠更好地保留邊緣信息。
高斯核的形狀由標準差 σ 控制。σ 越大,核越“胖”,平滑范圍越大。
Python
import cv2
import numpy as np# --- 練習 1.2.2: 高斯濾波 ---# 1. 加載圖像
image_path = 'your_image.jpg'
try:image = cv2.imread(image_path)if image is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")image = np.zeros((300, 500, 3), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)# 2. 定義核的大小 (必須是正奇數) 和 高斯標準差 (sigmaX, sigmaY)
# sigmaX 和 sigmaY 分別表示高斯核在X和Y方向上的標準差
# 如果 sigmaY 為 0,則 sigmaY = sigmaX
# 如果 sigmaX 和 sigmaY 都為 0,則它們由核大小計算得出
ksize_gaussian = (5, 5)
sigma_x = 0 # 通常設置為 0,讓 OpenCV 根據核大小自動計算# 3. 應用高斯濾波
# cv2.GaussianBlur() 函數用于進行高斯濾波
# 參數: src (輸入圖像), ksize (核大小), sigmaX, sigmaY (標準差), borderType (邊界處理方式,通常不需要指定)
gaussian_blurred_image = cv2.GaussianBlur(image, ksize_gaussian, sigma_x)# 4. 顯示原始圖像和高斯濾波后的圖像
cv2.imshow('Original Image', image)
cv2.imshow(f'Gaussian Blurred Image (ksize={ksize_gaussian}, sigmaX={sigma_x})', gaussian_blurred_image)# 5. 嘗試不同的核大小和 sigmaX 值
ksize_gaussian_large = (15, 15)
sigma_x_large = 5 # 明確指定較大的標準差
gaussian_blurred_image_large = cv2.GaussianBlur(image, ksize_gaussian_large, sigma_x_large)
cv2.imshow(f'Gaussian Blurred Image (ksize={ksize_gaussian_large}, sigmaX={sigma_x_large})', gaussian_blurred_image_large)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 1.2.2 完成 ---")
練習提示:
- 比較均值濾波和高斯濾波的效果,注意它們在邊緣處的差異。
- 嘗試不同的
ksize
和sigmaX
值。sigmaX
對平滑效果影響很大。當sigmaX
較大時,即使核較小,平滑效果也很明顯。
1.3 圖像銳化
與平滑相反,圖像銳化旨在增強圖像的邊緣和細節,使圖像看起來更清晰。銳化通常通過突出圖像中的變化區域(如邊緣)來實現。
一種常見的銳化方法是使用拉普拉斯算子 (Laplacian Operator)。拉普拉斯算子是二階微分算子,它檢測圖像中灰度變化率最大的區域,這些區域通常對應于邊緣。對圖像應用拉普拉斯算子后得到的是邊緣信息。
要實現圖像銳化,可以將原始圖像與拉普拉斯算子檢測到的邊緣信息進行結合。一個簡單的銳化核是:
[ 0, 1, 0 ]
[ 1, -4, 1 ]
[ 0, 1, 0 ]
或
[-1, -1, -1]
[-1, 8, -1]
[-1, -1, -1]
使用這些核進行卷積可以直接得到銳化效果。
OpenCV 提供了 cv2.Laplacian()
函數來計算拉普拉斯算子,或者你可以使用 cv2.filter2D()
函數和自定義的銳化核進行卷積。對于初學者,使用 filter2D
配合銳化核可能更直觀。
Python
import cv2
import numpy as np# --- 練習 1.3: 圖像銳化 ---# 1. 加載圖像 (灰度圖通常更適合濾波操作)
# 如果你的圖片是彩色圖,可以先轉換為灰度圖
image_path = 'your_image.jpg'
try:image_color = cv2.imread(image_path)if image_color is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY) # 轉換為灰度圖
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")# 使用一個虛擬灰度圖片代替image = np.zeros((300, 500), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)print(f"灰度圖像尺寸: {image.shape}")# 2. 定義銳化核 (一個常用的拉普拉斯銳化核)
sharpening_kernel = np.array([[0, -1, 0],[-1, 5, -1],[0, -1, 0]
], dtype=np.float32) # 使用 float32 類型# 3. 應用自定義核進行濾波 (銳化)
# cv2.filter2D() 函數使用自定義核對圖像進行卷積
# 參數: src (輸入圖像), ddepth (輸出圖像深度,-1 表示與輸入圖像相同), kernel (核)
sharpened_image = cv2.filter2D(image, -1, sharpening_kernel)# 4. 顯示原始灰度圖像和銳化后的圖像
cv2.imshow('Original Grayscale Image', image)
cv2.imshow('Sharpened Image (using filter2D)', sharpened_image)# 5. 使用 cv2.Laplacian 函數(僅獲取邊緣信息,不是直接銳化)
# 需要指定輸出深度,通常使用 cv2.CV_64F 以避免溢出
laplacian = cv2.Laplacian(image, cv2.CV_64F)
# 將結果轉換回 uint8
# 注意: 拉普拉斯結果可能有負值,需要取絕對值并縮放到 0-255 范圍
laplacian_abs = cv2.convertScaleAbs(laplacian)
cv2.imshow('Laplacian Edge Information', laplacian_abs)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 1.3 完成 ---")
練習提示:
cv2.filter2D()
非常靈活,你可以嘗試定義其他核(例如,一個簡單的邊緣檢測核np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]])
)來觀察效果。cv2.Laplacian()
直接計算的是圖像的二階導數,它更常用于邊緣檢測,而不是直接產生銳化后的圖像(雖然可以通過與原圖疊加實現銳化)。理解filter2D
和Laplacian
的區別。- 注意
filter2D
的ddepth
參數,-1
表示輸出與輸入深度相同,如果核導致結果超出 0-255,可能會發生截斷。對于一些濾波操作,可能需要將輸入轉換為更高的數據類型(如float32
)進行計算,然后再轉換回uint8
。但在上面的銳化例子中,[-1, 5, -1]
核通常不會導致嚴重溢出,所以直接用-1
輸出深度是可行的。
2. 圖像形態學操作
形態學操作是基于圖像中特定形狀(稱為結構元素或核)的幾何操作。它們主要應用于二值圖像(只有黑白兩種像素值,0 或 255),但也適用于灰度圖像。形態學操作在處理圖像中的對象形狀、大小、連接等方面非常有用,例如去除噪聲、分割獨立元素、連接相鄰元素、查找圖像中的孔洞等。
形態學操作的關鍵在于結構元素。它是一個小矩陣,定義了在操作過程中如何探測或影響鄰域像素。OpenCV 提供了 cv2.getStructuringElement()
函數來創建不同形狀和大小的結構元素。
Python
import cv2
import numpy as np# --- 練習 2.0: 創建結構元素 ---# 1. 定義結構元素的形狀和大小
# 形狀: cv2.MORPH_RECT (矩形), cv2.MORPH_ELLIPSE (橢圓形), cv2.MORPH_CROSS (交叉形)
shape = cv2.MORPH_RECT
ksize = (5, 5) # 大小 (寬度, 高度)# 2. 創建結構元素
kernel_rect = cv2.getStructuringElement(shape, ksize)
print(f"矩形結構元素 ({ksize}):\n{kernel_rect}")shape_ellipse = cv2.MORPH_ELLIPSE
ksize_ellipse = (7, 7)
kernel_ellipse = cv2.getStructuringElement(shape_ellipse, ksize_ellipse)
print(f"\n橢圓形結構元素 ({ksize_ellipse}):\n{kernel_ellipse}")shape_cross = cv2.MORPH_CROSS
ksize_cross = (5, 5)
kernel_cross = cv2.getStructuringElement(shape_cross, ksize_cross)
print(f"\n交叉形結構元素 ({ksize_cross}):\n{kernel_cross}")# 注意:形態學操作通常用于二值圖像,所以我們創建一個簡單的二值圖像作為例子
# 創建一個包含白色方塊的黑色圖像
binary_image = np.zeros((100, 100), dtype=np.uint8)
binary_image[20:80, 20:80] = 255# 添加一些小的噪聲點 (白色點)
binary_image[10, 10] = 255
binary_image[90, 90] = 255
binary_image[10, 90] = 255
binary_image[90, 10] = 255# 添加一些小的孔洞 (黑色點)
binary_image[45:55, 45:55] = 0cv2.imshow('Original Binary Image', binary_image)
cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 2.0 完成 ---")
練習提示:
- 理解不同形狀和大小的結構元素。它們定義了形態學操作的“感受野”。
- 注意形態學操作通常在二值圖像上效果最明顯。你需要先將圖像轉換為二值圖像(例如使用
cv2.threshold()
)。
2.1 腐蝕 (Erosion)
腐蝕操作會“縮小”二值圖像中的前景物體(白色區域)。它的工作原理是:對于輸出圖像的每個像素,如果其對應的輸入圖像像素的鄰域中,完全被結構元素覆蓋的區域都是前景像素,則輸出像素為前景(白色);否則為背景(黑色)。
簡單來說,如果結構元素在某個位置包含任何背景像素,并且其中心對應輸入圖像的一個前景像素,那么這個前景像素在輸出中就會變成背景。這會導致前景物體的邊緣被“腐蝕”掉。腐蝕可以用來去除圖像中的小噪聲點。
Python
import cv2
import numpy as np# --- 練習 2.1: 腐蝕操作 ---# 1. 加載或創建二值圖像 (使用上面創建的帶噪聲和孔洞的二值圖像)
binary_image = np.zeros((150, 150), dtype=np.uint8)
binary_image[30:120, 30:120] = 255 # 大方塊
binary_image[10, 10] = 255 # 噪聲點
binary_image[10, 130] = 255
binary_image[130, 10] = 255
binary_image[130, 130] = 255
binary_image[60:90, 60:90] = 0 # 孔洞# 2. 創建結構元素
kernel_erode = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 3. 應用腐蝕操作
# cv2.erode() 函數用于進行腐蝕
# 參數: src (輸入圖像), kernel (結構元素), iterations (迭代次數,可選,默認為1)
eroded_image = cv2.erode(binary_image, kernel_erode, iterations=1)# 4. 顯示原始二值圖像和腐蝕后的圖像
cv2.imshow('Original Binary Image', binary_image)
cv2.imshow('Eroded Image (iterations=1)', eroded_image)# 5. 嘗試多次迭代腐蝕
eroded_image_3_iter = cv2.erode(binary_image, kernel_erode, iterations=3)
cv2.imshow('Eroded Image (iterations=3)', eroded_image_3_iter)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 2.1 完成 ---")
練習提示:
- 觀察小的噪聲點是如何被腐蝕掉的。
- 觀察大的白色方塊的邊緣是如何向內收縮的。
- 嘗試增加
iterations
參數的值,觀察腐蝕效果如何增強。
2.2 膨脹 (Dilation)
膨脹操作會“放大”二值圖像中的前景物體(白色區域)。它的工作原理是:對于輸出圖像的每個像素,如果其對應的輸入圖像像素的鄰域中,至少有一個像素被結構元素覆蓋的區域是前景像素,則輸出像素為前景(白色);否則為背景(黑色)。
簡單來說,如果結構元素的中心對應輸入圖像的一個背景像素,但該結構元素覆蓋的區域內包含任何前景像素,那么這個背景像素在輸出中就會變成前景。這會導致前景物體的邊緣向外擴展。膨脹可以用來填補物體內部的小孔洞或連接相鄰的物體。
Python
import cv2
import numpy as np# --- 練習 2.2: 膨脹操作 ---# 1. 加載或創建二值圖像 (使用上面創建的帶噪聲和孔洞的二值圖像)
binary_image = np.zeros((150, 150), dtype=np.uint8)
binary_image[30:120, 30:120] = 255 # 大方塊
binary_image[10, 10] = 255 # 噪聲點
binary_image[10, 130] = 255
binary_image[130, 10] = 255
binary_image[130, 130] = 255
binary_image[60:90, 60:90] = 0 # 孔洞# 2. 創建結構元素
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 3. 應用膨脹操作
# cv2.dilate() 函數用于進行膨脹
# 參數: src (輸入圖像), kernel (結構元素), iterations (迭代次數,可選,默認為1)
dilated_image = cv2.dilate(binary_image, kernel_dilate, iterations=1)# 4. 顯示原始二值圖像和膨脹后的圖像
cv2.imshow('Original Binary Image', binary_image)
cv2.imshow('Dilated Image (iterations=1)', dilated_image)# 5. 嘗試多次迭代膨脹
dilated_image_3_iter = cv2.dilate(binary_image, kernel_dilate, iterations=3)
cv2.imshow('Dilated Image (iterations=3)', dilated_image_3_iter)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 2.2 完成 ---")
練習提示:
- 觀察小的孔洞是如何被膨脹操作填補的。
- 觀察大的白色方塊的邊緣是如何向外擴展的。
- 嘗試增加
iterations
參數的值,觀察膨脹效果如何增強。
2.3 開運算 (Opening)
開運算是先腐蝕后膨脹的操作。它常用于去除圖像中的小噪聲點(孤立的前景像素),同時基本保持較大物體的大小和形狀不變。先腐蝕可以去除小的噪聲點,再膨脹則可以恢復被腐蝕掉的較大物體。
Opening(Image)=Dilate(Erode(Image))
2.4 閉運算 (Closing)
閉運算是先膨脹后腐蝕的操作。它常用于填充物體內部的小孔洞或連接物體之間的狹窄間隙,同時基本保持物體的大小和形狀不變。先膨脹可以填補孔洞和連接間隙,再腐蝕則可以恢復因膨脹而稍有增大的物體大小。
Closing(Image)=Erode(Dilate(Image))
OpenCV 提供了 cv2.morphologyEx()
函數,它可以執行包括開運算、閉運算在內的多種形態學操作。
Python
import cv2
import numpy as np# --- 練習 2.3 & 2.4: 開運算與閉運算 ---# 1. 加載或創建二值圖像 (使用上面創建的帶噪聲和孔洞的二值圖像)
binary_image = np.zeros((150, 150), dtype=np.uint8)
binary_image[30:120, 30:120] = 255 # 大方塊
binary_image[10, 10] = 255 # 噪聲點
binary_image[10, 130] = 255
binary_image[130, 10] = 255
binary_image[130, 130] = 255
binary_image[60:90, 60:90] = 0 # 孔洞# 2. 創建結構元素
kernel_morph = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 3. 應用開運算
# cv2.morphologyEx() 函數用于執行多種形態學操作
# 參數: src (輸入圖像), op (操作類型), kernel (結構元素), iterations (迭代次數,可選)
# cv2.MORPH_OPEN 表示開運算
opening_image = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel_morph)# 4. 應用閉運算
# cv2.MORPH_CLOSE 表示閉運算
closing_image = cv2.morphologyEx(binary_image, cv2.MORPH_CLOSE, kernel_morph)# 5. 顯示原始、開運算和閉運算后的圖像
cv2.imshow('Original Binary Image', binary_image)
cv2.imshow('Opening Operation', opening_image)
cv2.imshow('Closing Operation', closing_image)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 2.3 & 2.4 完成 ---")
練習提示:
- 觀察開運算如何有效地去除了小的噪聲點,同時保留了較大的白色方塊。
- 觀察閉運算如何填補了方塊內部的孔洞。
- 嘗試使用不同大小和形狀的結構元素,以及不同的迭代次數,看看它們如何影響開閉運算的效果。
2.5 形態學梯度 (Morphological Gradient)
形態學梯度是膨脹圖像與腐蝕圖像之間的差值。它會突顯出物體的邊緣輪廓。
MorphologicalGradient=Dilate(Image)?Erode(Image)
這個操作的結果通常可以看作是物體邊界的“厚度”。
Python
import cv2
import numpy as np# --- 練習 2.5: 形態學梯度 ---# 1. 加載或創建二值圖像 (使用上面創建的二值圖像)
binary_image = np.zeros((150, 150), dtype=np.uint8)
binary_image[30:120, 30:120] = 255 # 大方塊# 2. 創建結構元素
kernel_grad = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 3. 應用形態學梯度
# cv2.morphologyEx() 函數, op=cv2.MORPH_GRADIENT 表示形態學梯度
gradient_image = cv2.morphologyEx(binary_image, cv2.MORPH_GRADIENT, kernel_grad)# 4. 顯示原始二值圖像和形態學梯度圖像
cv2.imshow('Original Binary Image', binary_image)
cv2.imshow('Morphological Gradient', gradient_image)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 2.5 完成 ---")
練習提示:
- 形態學梯度結果是物體的邊界區域,其寬度取決于結構元素的大小。
- 將其與后面的邊緣檢測結果進行比較,理解它們的不同用途。形態學梯度側重于根據物體形狀來提取邊界,而邊緣檢測側重于像素灰度變化。
3. 邊緣檢測
邊緣是圖像中像素強度發生顯著變化的區域。邊緣攜帶著圖像中物體形狀、邊界等重要信息,因此邊緣檢測是許多高級計算機視覺任務(如目標識別、圖像分割)的基礎步驟。
邊緣檢測通常涉及計算圖像的梯度。梯度表示圖像在某個方向上的變化率。在邊緣位置,梯度的大小通常較大。
3.1 Sobel 算子
OpenCV 提供了 cv2.Sobel()
函數來計算 Sobel 梯度。
Python
import cv2
import numpy as np# --- 練習 3.1: Sobel 算子邊緣檢測 ---# 1. 加載圖像并轉換為灰度圖 (邊緣檢測通常在灰度圖上進行)
image_path = 'your_image.jpg'
try:image_color = cv2.imread(image_path)if image_color is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")image = np.zeros((300, 500), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)# 2. 計算水平方向的 Sobel 梯度
# 參數: src (輸入圖像), ddepth (輸出圖像深度), dx (x方向導數階數), dy (y方向導數階數), ksize (Sobel核大小)
# 為了避免計算過程中出現負值截斷,通常將輸出深度設為 cv2.CV_64F
sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)# 3. 計算垂直方向的 Sobel 梯度
sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)# 4. 計算梯度幅值 (這里使用近似值: |Gx| + |Gy|)
# 將結果轉換為 uint8,cv2.convertScaleAbs 會取絕對值并截斷到 0-255
sobelx_abs = cv2.convertScaleAbs(sobelx)
sobely_abs = cv2.convertScaleAbs(sobely)
sobel_combined = cv2.addWeighted(sobelx_abs, 0.5, sobely_abs, 0.5, 0) # 簡單相加作為合并# 也可以使用更精確的梯度幅值計算: sqrt(Gx^2 + Gy^2)
# combined_magnitude = np.sqrt(sobelx**2 + sobely**2)
# combined_magnitude = cv2.normalize(combined_magnitude, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U) # 歸一化到 0-255# 5. 顯示結果
cv2.imshow('Original Grayscale Image', image)
cv2.imshow('Sobel X', sobelx_abs)
cv2.imshow('Sobel Y', sobely_abs)
cv2.imshow('Sobel Combined (|Gx| + |Gy|)', sobel_combined)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 3.1 完成 ---")
練習提示:
- 觀察 Sobel X 結果主要突出垂直方向的邊緣,Sobel Y 結果主要突出水平方向的邊緣。
- 合并后的結果顯示了各個方向的邊緣。
- 理解為什么需要使用
cv2.CV_64F
作為輸出深度,以及cv2.convertScaleAbs()
的作用。 - 嘗試不同的
ksize
值。
3.2 Canny 邊緣檢測
Canny 邊緣檢測是一種多階段的邊緣檢測算法,被廣泛認為是目前效果最好的邊緣檢測算法之一。它比 Sobel 算子更復雜,但能產生更細、更準確的邊緣。Canny 算法主要包括以下步驟:
- 噪聲抑制: 使用高斯濾波器平滑圖像,去除噪聲。
- 計算梯度: 計算圖像在水平和垂直方向的梯度(通常使用 Sobel 算子)。
- 非極大值抑制 (Non-maximum Suppression): 細化邊緣。在梯度方向上,只保留梯度局部最大值,抑制非最大值,使得邊緣變細。
- 雙閾值處理 (Double Thresholding): 定義兩個閾值:高閾值 (
high_threshold
) 和低閾值 (low_threshold
)。- 梯度值高于
high_threshold
的點被確定為強邊緣。 - 梯度值低于
low_threshold
的點被排除。 - 梯度值介于
low_threshold
和high_threshold
之間的點被稱為弱邊緣,它們可能成為邊緣,也可能不是。
- 梯度值高于
- 邊緣跟蹤 (Edge Tracking by Hysteresis): 連接邊緣。通過分析弱邊緣的鄰域,將與強邊緣相連的弱邊緣也視為邊緣。
OpenCV 提供了 cv2.Canny()
函數來執行 Canny 邊緣檢測。
Python
import cv2
import numpy as np# --- 練習 3.2: Canny 邊緣檢測 ---# 1. 加載圖像并轉換為灰度圖
image_path = 'your_image.jpg'
try:image_color = cv2.imread(image_path)if image_color is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")image = np.zeros((300, 500), dtype=np.uint8)cv2.putText(image, "Image not found", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.putText(image, "Please replace 'your_image.jpg'", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)# 2. 定義雙閾值
low_threshold = 50
high_threshold = 150 # 通常 high_threshold 是 low_threshold 的 2 到 3 倍# 3. 應用 Canny 邊緣檢測
# cv2.Canny() 函數執行 Canny 邊緣檢測
# 參數: src (8位輸入圖像), threshold1 (低閾值), threshold2 (高閾值),
# apertureSize (Sobel算子大小,默認為3), L2gradient (是否使用更精確的L2范數計算梯度幅值,默認為False)
canny_edges = cv2.Canny(image, low_threshold, high_threshold)# 4. 顯示原始灰度圖像和 Canny 邊緣圖像
cv2.imshow('Original Grayscale Image', image)
cv2.imshow(f'Canny Edges (low={low_threshold}, high={high_threshold})', canny_edges)# 5. 嘗試不同的閾值組合
canny_edges_loose = cv2.Canny(image, 30, 100) # 較低閾值,可能檢測到更多弱邊緣
canny_edges_strict = cv2.Canny(image, 100, 200) # 較高閾值,只檢測到強邊緣
cv2.imshow(f'Canny Edges (low=30, high=100)', canny_edges_loose)
cv2.imshow(f'Canny Edges (low=100, high=200)', canny_edges_strict)cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 練習 3.2 完成 ---")
練習提示:
- Canny 邊緣通常比 Sobel 邊緣更細。
- 雙閾值的選擇對結果影響很大。較低的閾值組合會檢測到更多細節和噪聲,較高的閾值組合則只保留強邊緣。
- 理解雙閾值處理和邊緣跟蹤的工作原理。
4. 實戰:檢測圖像中的邊緣
現在,我們將結合學到的知識,在一個實際圖像上進行邊緣檢測的實戰練習。我們將加載一張圖片,將其轉換為灰度圖,然后應用 Canny 邊緣檢測來找到物體的輪廓。
Python
import cv2
import numpy as np# --- 實戰練習: 使用 Canny 邊緣檢測 ---# 1. 加載你的圖像
# 請替換 'your_real_image.jpg' 為你的圖片路徑
image_path = 'your_real_image.jpg'
try:image_color = cv2.imread(image_path)if image_color is None:raise FileNotFoundError(f"圖片文件未找到: {image_path}")print(f"成功加載彩色圖像: {image_path}")
except FileNotFoundError as e:print(e)print("請確保你的圖片文件存在并位于正確路徑。")print("將使用一個內置的 OpenCV 示例圖像進行演示。")# 如果圖片加載失敗,使用 OpenCV 的內置圖像# 需要確保安裝了 opencv-contrib-python 或手動下載圖片# 例如,可以使用 './data/lena.jpg' 如果你有opencv_extra庫的data文件夾# 或者簡單創建一個模擬圖像image_color = np.zeros((400, 600, 3), dtype=np.uint8)cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)# 2. 將圖像轉換為灰度圖
gray_image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)# 3. 應用 Canny 邊緣檢測
# 可以嘗試不同的閾值來獲得最佳效果
low_threshold = 100
high_threshold = 200
edges = cv2.Canny(gray_image, low_threshold, high_threshold)# 4. 顯示原始圖像和邊緣檢測結果
cv2.imshow('Original Image', image_color)
cv2.imshow('Grayscale Image', gray_image)
cv2.imshow(f'Canny Edges (low={low_threshold}, high={high_threshold})', edges)# 5. 等待按鍵,然后關閉窗口
cv2.waitKey(0)
cv2.destroyAllWindows()print("\n--- 實戰練習 完成 ---")
實戰提示:
- 更換不同的圖片文件進行測試。
- 仔細調整 Canny 函數的
low_threshold
和high_threshold
參數,觀察邊緣檢測結果的變化。這是獲得滿意邊緣檢測結果的關鍵步驟。 - 對于一些有噪聲的圖片,你可能需要在 Canny 之前先進行高斯平滑,雖然
cv2.Canny()
函數內部已經包含了高斯平滑的步驟,但有時外部的預處理平滑可以改善結果。
總結
在這一部分,我們深入學習了 OpenCV 中的核心圖像處理技術,包括:
- 圖像增強和濾波: 通過調整亮度對比度改善視覺效果,使用均值濾波和高斯濾波進行平滑,使用拉普拉斯算子或自定義核進行銳化。
- 形態學操作: 利用結構元素進行腐蝕、膨脹、開運算、閉運算和形態學梯度等操作,常用于二值圖像處理和形狀分析。
-
邊緣檢測: 學習了 Sobel 算子和更強大的 Canny 算法,用于檢測圖像中像素強度變化顯著的邊緣區域。