Pynote

Python、機械学習、画像処理について

OpenCV - 輪郭を近似する、面積を求める、Bounding Box を求める方法について

概要

FindContours() で抽出した輪郭に対して行う以下の処理を紹介する。
輪郭の抽出については下記の記事を参照されたい。

pynote.hatenablog.com

  • 輪郭の周囲の長さを計算する。[arcLength()]
  • 輪郭の面積を計算する。[contourArea()]
  • 輪郭を近似する。[approxPolyDP()]
  • 輪郭に外接する長方形を取得する。[boundingRect()]
  • 輪郭に外接する回転した長方形を取得する。[minAreaRect()]
  • 輪郭に外接する円を取得する。[minEnclosingCircle()]
  • 輪郭に外接する三角形を取得する。[minEnclosingTriangle()]
  • 輪郭に外接する凸包を取得する。[convexHull()/isContourConvex()]

OpenCV 3 の注意

OpenCV 3 までは返り値が以下のように3つだったので、OpenCV 3 を使ってる場合は注意。サンプルコードは OpenCV 4 を前提として記載している。

image, contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

輪郭を抽出する。

画像を読み込む。

使用する画像

輪郭を抽出する。

import cv2

# 画像を読み込む。
img = cv2.imread("sample.png")

# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

輪郭を描画する。

matplotlib で抽出した輪郭を描画する。

import matplotlib.pyplot as plt
from matplotlib.patches import Polygon


def draw_contours(ax, img, contours):
    ax.imshow(img)  # 画像を表示する。
    ax.set_axis_off()

    for i, cnt in enumerate(contours):
        # 形状を変更する。(NumPoints, 1, 2) -> (NumPoints, 2)
        cnt = cnt.squeeze(axis=1)
        # 輪郭の点同士を結ぶ線を描画する。
        ax.add_patch(Polygon(cnt, color="b", fill=None, lw=2))
        # 輪郭の点を描画する。
        ax.plot(cnt[:, 0], cnt[:, 1], "ro", mew=0, ms=4)
        # 輪郭の番号を描画する。
        ax.text(cnt[0][0], cnt[0][1], i, color="orange", size="20")


fig, ax = plt.subplots(figsize=(8, 8))
draw_contours(ax, img, contours)

plt.show()


輪郭の周囲の長さを計算する。[arcLength()]

arcLength() で輪郭の周囲の長さを計算できる。

retval = cv2.arcLength(curve, closed)
  • 引数
    • curve: (NumPoints, 1, 2) の numpy 配列。輪郭。
    • closed: 輪郭が閉じているかどうか
  • 返り値
    • retval: 輪郭の長さ
for i, cnt in enumerate(contours):
    # 輪郭の周囲の長さを計算する。
    arclen = cv2.arcLength(cnt, True)
    print('arc length of contour {}: {:.2f}'.format(i, arclen))
arc length of contour 0: 280.00
arc length of contour 1: 279.97
arc length of contour 2: 387.50
arc length of contour 3: 245.24
arc length of contour 4: 209.68

輪郭の面積を計算する。[contourArea()]

contourArea() で輪郭の面積を計算できる。

retval = cv2.contourArea(contour[, oriented])
  • 引数
    • contour: (NumPoints, 1, 2) の numpy 配列。輪郭。
    • oriented: True の場合、輪郭の点の順序が反時計回りの場合は正、時計回りの場合は負の値を返す。False の場合はいずれも正の値を返す。
  • 返り値
    • points: 輪郭の面積
for i, cnt in enumerate(contours):
    # 輪郭の面積を計算する。
    area = cv2.contourArea(cnt)
    print('contour: {}, area: {}'.format(i, area))
area of contour 0: 4900.00
area of contour 1: 3362.00
area of contour 2: 10743.00
area of contour 3: 3784.00
area of contour 4: 1923.00

oriented 引数について

oriented=True の場合は輪郭の点の順序が反時計回りの場合は正、時計回りの場合は負の値を返す。

# 点の順序が反時計回りの輪郭
ccw_contour = np.array([[5, 5], [10, 5], [10, 10], [5, 10], [5, 5]]).reshape(-1, 1, 2)
# 点の順序が時計回りの輪郭
cw_contour = np.flip(ccw_contour, axis=0).reshape(-1, 1, 2)

# oriented=False の場合
ccw_contour_area = cv2.contourArea(ccw_contour, oriented=False)
cw_contour_area = cv2.contourArea(cw_contour, oriented=False)
print('oriented=False => ccw order contour: {:.2f}, cw order contour: {:.2f}'.format(
    ccw_contour_area, cw_contour_area))
# oriented=False => counter-clockwise: 25.0, clock-wise: 25.0

# oriented=True の場合
ccw_contour_area = cv2.contourArea(ccw_contour, oriented=True)
cw_contour_area = cv2.contourArea(cw_contour, oriented=True)
print('oriented=True => ccw order contour: {:.2f}, cw order contour: {:.2f}'.format(
    ccw_contour_area, cw_contour_area))
# oriented=True => counter-clockwise: 25.0, clock-wise: -25.0


輪郭を近似する。[approxPolyDP()]

approxPolyDP() で曲線など多数の点で構成される輪郭をより少ない点で近似できる。
処理は、Ramer-Douglas-Peucker アルゴリズム に従う。

approxCurve = cv2.approxPolyDP(curve, epsilon, closed[, approxCurve])
  • 引数
    • curve: (NumPoints, 1, 2) の numpy 配列。輪郭
    • epsilon: アルゴリズムで使用する許容距離
    • closed: 輪郭が閉じているかどうか
    • approxCurve: 引数で結果を受け取る場合、指定する。
  • 返り値
    • approxCurve: 近似した輪郭

引数の epsilon は、通常、輪郭の周囲の長さ * ratio を使用する。
ratio を大きい値にするほど点の数は削減できるが、近似の精度が大雑把になる。
逆に小さい値にするほど近似の精度がよくなるが、点の数があまり削減できない。

approx_contours = []
for i, cnt in enumerate(contours):
    # 輪郭の周囲の長さを計算する。
    arclen = cv2.arcLength(cnt, True)
    # 輪郭を近似する。
    approx_cnt = cv2.approxPolyDP(cnt, epsilon=0.005 * arclen, closed=True)
    approx_contours.append(approx_cnt)
    # 元の輪郭及び近似した輪郭の点の数を表示する。
    print('contour {}: {} -> {}'.format(i, len(cnt), len(approx_cnt)))

fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, approx_contours)
plt.show()
contour 0: 4 -> 4
contour 1: 165 -> 3
contour 2: 171 -> 16
contour 3: 6 -> 4
contour 4: 10 -> 7

円や三角など沢山の点で構成されていた輪郭がより少ない点で構成される輪郭で近似されていることがわかる。

ratio による近似精度の変化

ratio を変えることで次の sin 曲線の近似がどう変化するか確認する。

# sin 曲線を作成する。
x = np.linspace(0, np.pi * 2, 20, dtype=np.float32)
y = np.sin(x)
contour = np.vstack([x, y]).T.reshape(-1, 1, 2)

# sin 曲線を描画する。
fig, ax = plt.subplots(figsize=(6, 6))
axes.plot(contour[:, 0, 0], contour[:, 0, 1], 'o-', label='sin')

# sin 曲線の弧長を計算する。
arclen = cv2.arcLength(contour, closed=False)  

for ratio in [0.1, 0.01, 0.001]:
    # 輪郭を近似する。
    approx = cv2.approxPolyDP(contour, epsilon=ratio * arclen, closed=False)
    print('eps: {:.2f}, points; {}'.format(ratio * arclen, len(approx)))
    # 近似した輪郭を描画する。
    ax.plot(approx[:, 0, 0], approx[:, 0, 1], 'o-', label='eps={}'.format(ratio))
ax.legend()
plt.show()
eps: 0.76, points; 4
eps: 0.08, points; 8
eps: 0.01, points; 18


輪郭に外接する長方形を取得する。[boundingRect()]

boundingRect() で輪郭に外接する長方形を取得できる。

retval = cv2.boundingRect(points)
  • 引数
    • points: (NumPoints, 1, 2) の numpy 配列。輪郭。
  • 返り値
    • retval: (左上の x 座標, 左上の y 座標, 幅, 高さ) であるタプル。輪郭に外接する長方形。
fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, contours)

for cnt in contours:
    # 輪郭に外接する長方形を取得する。
    x, y, width, height = cv2.boundingRect(cnt)
    # 長方形を描画する。
    ax.add_patch(Rectangle(
        xy=(x, y), width=width, height=height, color='g', fill=None, lw=2))

plt.show()


輪郭に外接する回転した長方形を取得する。[minAreaRect()]

minAreaRect() で輪郭に外接する回転した長方形を取得できる。

retval = cv2.minAreaRect(points)
  • 引数
    • points: (NumPoints, 1, 2) の numpy 配列。輪郭。
  • 返り値
    • retval: ((中心の x 座標, 中心の y 座標), (長方形の幅, 長方形の高さ), 回転角度) であるタプル。回転した長方形の情報。

返り値は、回転した長方形の回転中心、大きさ、回転角度である。
boxPoints() により、これを4点の座標に変換することができる。

points = cv2.boxPoints(box[, points])
  • 引数
    • box: 回転した長方形の情報。
    • points: 引数で結果を受け取る場合、指定する。
  • 返り値
    • points: (4, 2) の numpy 配列。回転した長方形の4点の座標。
fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, contours)

for i, cnt in enumerate(contours):
    # 輪郭に外接する回転した長方形を取得する。
    rect = cv2.minAreaRect(cnt)
    (cx, cy), (width, height), angle = rect
    print('bounding box of contour {} => '
          'center: ({:.2f}, {:.2f}), size: ({:.2f}, {:.2f}), angle: {:.2f}'.format(
        i, cx, cy, width, height, angle))
    # 回転した長方形の4点の座標を取得する。
    rect_points = cv2.boxPoints(rect)
    # 回転した長方形を描画する。
    ax.add_patch(Polygon(rect_points, color='g', fill=None, lw=2))

plt.show()
bounding box of contour 0 => center: (60.00, 286.00), size: (70.00, 70.00), angle: -90.00
bounding box of contour 1 => center: (327.00, 288.00), size: (82.00, 82.00), angle: -90.00
bounding box of contour 2 => center: (202.50, 224.00), size: (117.00, 116.00), angle: -0.00
bounding box of contour 3 => center: (299.50, 139.00), size: (61.52, 61.52), angle: -45.00
bounding box of contour 4 => center: (220.50, 74.50), size: (55.15, 55.15), angle: -45.00


輪郭に外接する円を取得する。[minEnclosingCircle()]

minEnclosingCircle() で輪郭に外接する円を取得できる。

center, radius = cv2.minEnclosingCircle(points)
  • 引数
    • points: (NumPoints, 1, 2) の numpy 配列。輪郭。
  • 返り値
    • center: 円の中心
    • radius: 円の半径
fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, contours)

for cnt in contours:
    # 輪郭に外接する円を取得する。
    center, radius = cv2.minEnclosingCircle(cnt)
    # 描画する。
    ax.add_patch(Circle(xy=center, radius=radius, color='g', fill=None, lw=2))

plt.show()


輪郭に外接する三角形を取得する。[minEnclosingTriangle()]

minEnclosingTriangle() で輪郭に外接する三角形を取得できる。

retval, triangle = cv2.minEnclosingTriangle(points[, triangle])
  • 引数
    • points: (NumPoints, 1, 2) の numpy 配列。輪郭。
    • triangle: 引数で結果を受け取る場合、指定する。
  • 返り値
    • retval: 三角形の面積
    • triangle: (3, 1, 2) の numpy 配列。三角形の輪郭。
fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, contours)

for cnt in contours:
    # 輪郭に外接する三角形を取得する。
    retval, triangle = cv2.minEnclosingTriangle(cnt)
    # 描画する。
    ax.add_patch(Polygon(triangle.reshape(-1, 2), color='g', fill=None, lw=2))

plt.show()


輪郭に外接する凸包を取得する。[convexHull()/isContourConvex()]

convexHull() で輪郭に外接する凸包を取得できる。

hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]])
  • 引数
    • points: (NumPoints, 1, 2) の numpy 配列。輪郭
    • hull: 輪郭に外接する凸包 (引数経由で受け取る場合)
    • clockwise: 輪郭が時計回りかどうか
    • returnPoints: 輪郭に外接する凸包を返すかどうか
  • 返り値
    • hull: (NumPoints, 1, 2) の numpy 配列。輪郭に外接する凸包

また isContourConvex() で輪郭が凸包かどうか判定できる。

retval = cv2.isContourConvex(contour)
  • 引数
    • contour: (NumPoints, 1, 2) の numpy 配列。輪郭。
  • 返り値
    • retval: 輪郭が凸多角形の場合は True、そうでない場合は False を返す。
fig, ax = plt.subplots(figsize=(6, 6))
draw_contours(ax, img, contours)

for i, cnt in enumerate(contours):
    print('contour {} is convex: {}'.format(i, cv2.isContourConvex(cnt)))
    
    # 輪郭に外接する凸包を取得する。
    hull = cv2.convexHull(cnt)

    # 凸包を描画する。
    ax.add_patch(Polygon(hull.reshape(-1, 2), color='g', fill=None, lw=2))

plt.show()
contour 0 is convex: True
contour 1 is convex: False
contour 2 is convex: False
contour 3 is convex: True
contour 4 is convex: False

No1,2 の図形は凸多角形であるが、結果は False となっている。
このように、輪郭検出した点は凸多角形になっていない場合がある。