HOWTO · OpenCV
OpenCV 形狀檢測
本文介紹如何使用 OpenCV 的 findContours() 和 approxPolyDP() 函式檢測影象中存在的形狀。
本頁內容
本教學將示範如何使用 OpenCV 準備影像、找出外部輪廓、將各輪廓近似為多邊形,並以頂點數作為簡單的形狀分類啟發式。
準備用於形狀偵測的二值影像
findContours() 會以二值遮罩為輸入。在以下流程中,前景形狀為白色(255),背景為黑色(0)。使用 cvtColor() 將 BGR 影像轉為灰階,再以 threshold() 搭配 Otsu 方法,從影像中自動選出閾值。如果前景比淺色背景暗,請反轉產生的遮罩,或使用反向閾值,讓物件呈現白色。
載入輸入檔案時,可使用 cv2.imread("shape-detection.webp"),並在處理前確認回傳的影像不是 None。接著將 BGR 影像轉為灰階,再用 cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 建立二值遮罩。遮罩應以黑色背景呈現白色前景物件;若影像的明暗極性相反,請改用反向閾值。
使用 findContours() 和 approxPolyDP() 偵測並分類形狀
呼叫 findContours() 並指定 RETR_EXTERNAL,即可取得每個白色物件的外部輪廓;接著使用 approxPolyDP() 近似輪廓,再計算所得頂點數。三個頂點可作為三角形的線索,四個表示四邊形,五個表示五邊形。這些都只是啟發式判斷:四個頂點無法證明形狀是正方形。頂點數大於 12 時,可將輪廓標記為近似圓形;這個門檻會受到近似容差、影像解析度及輪廓品質影響。
簡短範例以幾個代表性的頂點數,展示各種分類標籤。下方的完整影像範例會從腳本所在目錄載入提供的 shape-detection.webp,即使你從其他工作目錄啟動腳本也能如此載入,並將標註後的 PNG 儲存至目前工作目錄。若要執行影像處理流程,請安裝 OpenCV(opencv-python)和 NumPy。提供的執行證據僅涵蓋標籤範例,並未證明 OpenCV 影像處理流程已執行。簡短範例中的分類標籤仍以英文顯示,因程式碼與輸出區塊須與英文原文逐位元組一致;請依照本文說明理解這些標籤。
def label_for_vertex_count(count):
if count == 3:
return "triangle"
if count == 4:
return "quadrilateral"
if count == 5:
return "pentagon"
if count > 12:
return "circle-like"
return "polygon"
approximated_vertex_counts = [3, 4, 5, 16]
for count in approximated_vertex_counts:
print(f"{label_for_vertex_count(count)}: {count} vertices")
triangle: 3 vertices
quadrilateral: 4 vertices
pentagon: 5 vertices
circle-like: 16 vertices
簡短的程式碼片段單獨展示了分類規則。下面的完整腳本將同樣的規則應用於所提供的影像:檢查影像載入、將 BGR 像素轉換為灰階圖,並建立 Otsu 二值遮罩。遮罩假設明亮的前景物件位於較暗的背景上;當前前景較暗、背景較亮時,請使用 cv2.THRESH_BINARY_INV。所提供的影像包含多個面板,因此外部輪廓除了形狀本身,也會包含面板邊界。標籤仍然只是啟發式判斷:4 個頂點表示四邊形,而不一定是正方形;輪廓順序和頂點數量取決於影像品質和近似容差。
from pathlib import Path
def label_for_vertex_count(count):
if count == 3:
return "triangle"
if count == 4:
return "quadrilateral"
if count == 5:
return "pentagon"
if count > 12:
return "circle-like"
return "polygon"
def main():
try:
import cv2
except ImportError:
print("OpenCV is required; install it with python -m pip install opencv-python.")
raise SystemExit(1)
image_path = Path(__file__).resolve().with_name("shape-detection.webp")
image = cv2.imread(str(image_path))
if image is None:
print(f"Could not load image: {image_path}")
raise SystemExit(1)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(
gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
result = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
contours = result[0] if len(result) == 2 else result[1]
draw = True
annotated = image.copy()
for contour in contours:
perimeter = cv2.arcLength(contour, True)
approximation = cv2.approxPolyDP(
contour, 0.01 * perimeter, True
)
label = label_for_vertex_count(len(approximation))
print(f"{label}: {len(approximation)} vertices")
if draw:
cv2.drawContours(
annotated, [approximation], -1, (0, 0, 255), 2
)
if draw:
output_path = Path.cwd() / "shape-detection-annotated.png"
if cv2.imwrite(str(output_path), annotated):
print(f"Saved annotated image: {output_path}")
else:
print(f"Could not save annotated image: {output_path}")
raise SystemExit(1)
if __name__ == "__main__":
main()
這些影像處理程式碼供讀者參考和改編;提供的執行時期輸出是來自上面的獨立標籤範例,並不代表這些程式碼已執行。
OpenCV 的顏色元組採用 BGR 順序,因此 (0, 0, 255) 會繪製紅色。厚度設為 2 會繪製輪廓線;負數厚度則會填滿輪廓。
使用 drawContours() 繪製偵測到的形狀
在完整腳本中,draw = True 會以紅色繪製每條近似輪廓,並將 shape-detection-annotated.png 儲存到目前工作目錄;cv2.imwrite() 會報告儲存是否成功。設定 draw = False 可同時略過繪製和儲存。腳本不會開啟 GUI,因此可以在無頭環境中執行。下圖仍僅作視覺參考。
了解 OpenCV 輪廓函式的引數
findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 依序接收二值遮罩、輪廓檢索模式及輪廓近似方法。RETR_EXTERNAL 只會回傳外部邊界;如果任務不需要處理內部孔洞,這種模式就很實用。CHAIN_APPROX_SIMPLE 會壓縮輪廓點中連續的直線段,同時保留邊界形狀。不同 OpenCV 版本的 findContours() 回傳值數量可能不同;若程式需支援兩種格式,可用 result[0] if len(result) == 2 else result[1] 選取輪廓。
approxPolyDP(contour, epsilon, closed) 接收輪廓、近似距離上限,以及表示曲線是否閉合的布林值。epsilon 常見的設定方式是 0.01 * cv2.arcLength(contour, True);一般而言,調高這個比例會減少頂點,調低則會讓近似結果更貼合輪廓。此函式使用 Douglas–Peucker 演算法,因此頂點數同時取決於這個容差與影像本身。
drawContours(image, contours, contour_index, color, thickness) 會直接修改影像;contours 提供邊界,contour_index 指定要繪製的單一輪廓(或用 -1 繪製全部輪廓),而 color 是 BGR 元組。thickness 為負數時會填滿所選輪廓。