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,因此可以在無頭環境中執行。下圖仍僅作視覺參考。

shape detection

了解 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 為負數時會填滿所選輪廓。