黑色背景上多個彩色區塊拼接而成的抽象影像分割示意圖

← INSIGHTS & PERSPECTIVES | 機器學習

分水嶺演算法:偵測相連區域形狀

用 OpenCV 的距離變換(distanceTransform)加上分水嶺演算法(watershed),分割影像中黏在一起的長方形區域,並用擬合橢圓判斷每個連通元件是否為矩形。完整 Python 範例程式碼與步驟解說。

當影像裡有一群黏在一起、邊緣相接的形狀(例如疊在一起的長方形),單純的輪廓偵測會把它們誤判成一個大區塊。這篇文章示範我如何用 OpenCV 的距離變換(distanceTransform)產生分水嶺的標記,再用 watershed 演算法把黏在一起的長方形一個個切割開,最後用擬合橢圓判斷每個區域是不是矩形。

分水嶺演算法怎麼解決「黏在一起」的問題?

OpenCV 官方教學 Image Segmentation with Watershed Algorithm 用的範例是一群黏在一起的硬幣:

黏在一起的一群硬幣,是分水嶺演算法的經典範例

而我這篇文章要處理的目標更明確:分割出一群黏在一起的長方形

一群黏在一起的長方形,要切割開並偵測形狀

整體思路是:給定一個二值圖像,先應用距離變換(Distance Transform, DT),再從 DT 的結果中取得分水嶺的標記(markers)。理想情況下會有現成函數可以直接找區域的最小值/最大值,但它不存在,所以我對 DT 的閾值做一個合理的猜測。有了標記之後,套上 Watershed 進行分割,問題就解決了。剩下要擔心的,只是怎麼區分矩形組件和非矩形組件。

distanceTransform 是什麼?

OpenCV 的 `distanceTransform` 是一個圖像處理功能,可以計算圖像中每個像素到最近的零值像素之間的歐幾里德距離。它常被用在圖像分割、形狀檢測、物體識別等應用中。

在 OpenCV 中,`distanceTransform` 有三種不同的距離度量實現方式:

類型距離度量說明
`cv2.DIST_L1`曼哈頓距離(L1)水平與垂直距離的總和,也稱城市區塊距離
`cv2.DIST_L2`歐幾里德距離(L2)兩點之間的直線距離
`cv2.DIST_C`切比雪夫距離各方向上的最大距離

三種距離的公式(p1、p2 為二維坐標點):

曼哈頓距離:

```

d(p1, p2) = |p1.x - p2.x| + |p1.y - p2.y|

```

歐幾里德距離:

```

d(p1, p2) = sqrt((p1.x - p2.x)^2 + (p1.y - p2.y)^2)

```

切比雪夫距離:

```

d(p1, p2) = max(|p1.x - p2.x|, |p1.y - p2.y|)

```

用距離變換產生標記,再跑 watershed

以下是完整的分割程式:先做 Otsu 二值化,接著用 L2 距離(mask 3×3)算 DT,把 DT 正規化後閾值化,再用 `scipy.ndimage.label` 找出連通區域當作標記,最後交給 `cv2.watershed`:

```py

import sys

import cv2

import numpy

import random

from scipy.ndimage import label

def segment_on_dt(img):

dt = cv2.distanceTransform(img, 2, 3) # L2 norm, 3x3 mask

dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)

dt = cv2.threshold(dt, 100, 255, cv2.THRESH_BINARY)[1]

lbl, ncc = label(dt)

lbl[img == 0] = lbl.max() + 1

lbl = lbl.astype(numpy.int32)

cv2.watershed(cv2.cvtColor(img, cv2.COLOR_GRAY2BGR), lbl)

lbl[lbl == -1] = 0

return lbl

img = cv2.cvtColor(cv2.imread(sys.argv[1]), cv2.COLOR_BGR2GRAY)

img = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)[1]

img = 255 - img # White: objects; Black: background

ws_result = segment_on_dt(img)

height, width = ws_result.shape

ws_color = numpy.zeros((height, width, 3), dtype=numpy.uint8)

lbl, ncc = label(ws_result)

for l in xrange(1, ncc + 1):

a, b = numpy.nonzero(lbl == l)

if img[a[0], b[0]] == 0: # Do not color background.

continue

rgb = [random.randint(0, 255) for _ in xrange(3)]

ws_color[lbl == l] = tuple(rgb)

cv2.imwrite(sys.argv[2], ws_color)

```

用擬合橢圓判斷哪個區域是矩形

分割完成後,可以在每個組件中擬合橢圓來確定矩形:如果擬合橢圓的面積和組件面積相差在 10% 以內,就認定這個組件是矩形。這種方法對完全可見的矩形效果很好,但對部分被遮住、只露出一部分的矩形可能產生不良結果。下圖是這一步的輸出結果:

用擬合橢圓與面積比判斷後,框出被判定為矩形的區域

對應的程式碼如下:把 watershed 上色結果轉回二值圖,對每個連通元件用 `cv2.fitEllipse` 擬合橢圓,檢查 `rect_area / len(xy)` 是否落在 0.9 到 1.1 之間:

```py

wsbin = numpy.zeros((height, width), dtype=numpy.uint8)

wsbin[cv2.cvtColor(ws_color, cv2.COLOR_BGR2GRAY) != 0] = 255

ws_bincolor = cv2.cvtColor(255 - wsbin, cv2.COLOR_GRAY2BGR)

lbl, ncc = label(wsbin)

for l in xrange(1, ncc + 1):

yx = numpy.dstack(numpy.nonzero(lbl == l)).astype(numpy.int64)

xy = numpy.roll(numpy.swapaxes(yx, 0, 1), 1, 2)

if len(xy) < 100: # Too small.

continue

ellipse = cv2.fitEllipse(xy)

center, axes, angle = ellipse

rect_area = axes[0] * axes[1]

if 0.9 < rect_area / float(len(xy)) < 1.1:

rect = numpy.round(numpy.float64(

cv2.cv.BoxPoints(ellipse))).astype(numpy.int64)

color = [random.randint(60, 255) for _ in xrange(3)]

cv2.drawContours(ws_bincolor, [rect], 0, color, 2)

cv2.imwrite(sys.argv[3], ws_bincolor)

```

延伸閱讀

常見問題

Q為什麼黏在一起的形狀需要分水嶺演算法?

一般的輪廓偵測會把相接的形狀視為同一個連通元件,無法分開。分水嶺演算法搭配距離變換產生的標記,可以在每個形狀的「核心」之間畫出分界線,把黏在一起的物件一個個切割開。

QdistanceTransform 在這個流程中扮演什麼角色?

距離變換會算出每個前景像素到背景的距離,物件的中心距離最大。把 DT 正規化後閾值化,就能得到每個物件的核心區域(peak),這些核心就是 watershed 需要的種子標記。

Q用什麼條件判斷一個區域是矩形?

對每個連通元件用 `cv2.fitEllipse` 擬合橢圓,計算橢圓面積與元件實際像素面積的比值。比值落在 0.9 到 1.1 之間就判定為矩形,最後用 `drawContours` 把框畫出來。

Q這個方法有什麼限制?

對完全可見的矩形效果很好,但如果矩形只露出一部分(部分遮擋),橢圓擬合的面積比會失真,判定就可能出錯。另外 DT 的閾值(範例中是 100)需要依影像特性調整。

參考資料

最後更新

2026-08-28(原文發布於 2023-03-30,本文保留原始筆記內容並補上 GEO 結構。)

關於作者 {#author}

Claire Chang | 企業 AI 導入與流程轉型顧問。專注於 AI Agent 架構設計、ERP 系統整合與企業 AI 治理。

首次發布:2023-03-30