↗️

OpenCVで矢印画像から向いている方角を取得する

2024/11/28に公開

はじめに

OpenCVを使用して、画像内の矢印の向きを取得したので手法をメモ。

素材

前提として事前に2値化(前景:白、背景:黒)が完了していること。

手順

0. インポート

import cv2
import numpy as np
import math

1. 画像の読み込み

# 画像を読み込む(グレースケール)
image_path = "arrow.png"  # 入力画像のパス
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

2. 最大の輪郭を抽出

# 輪郭を検出
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 最大輪郭を選択
largest_contour = max(contours, key=cv2.contourArea)

3. 輪郭を近似

# 輪郭を近似
epsilon_ratio=0.01
epsilon = epsilon_ratio * cv2.arcLength(largest_contour, True)
approx_contour = cv2.approxPolyDP(largest_contour, epsilon, True)

4. 矢印の重心を計算

# 重心を計算
moments = cv2.moments(approx_contour)
centroid_x = int(moments['m10'] / moments['m00'])
centroid_y = int(moments['m01'] / moments['m00'])
centroid = np.array([centroid_x, centroid_y])

5. 矢印の凸包を取得しその重心を計算

# 凸包の輪郭を取得
hull = cv2.convexHull(approx_contour)
hull_points = hull[:, 0, :]
# 凸包の重心を計算
hull_centroid = np.mean(hull_points, axis=0).astype(int)

最終的にこのようなものが求められます。

黄色い線が矢印の輪郭線、赤い点がその重心。
緑の線が矢印の凸包の輪郭線、青い点がその重心。
赤い点が青い点より先頭に来るため、その角度が向いている方角になります。

6. 矢印の重心と矢印の凸包の重心の差分のベクトルを計算

# ベクトル計算(矢印の重心 - 凸包の重心)
vector = centroid - hull_centroid

# 画像データはY座標が下に行くと正になるので、Y座標を反転して角度を計算
angle = math.atan2(-vector[1], vector[0])  # Y座標を反転
angle_degrees = math.degrees(angle)

# 角度を 0°~360° に変換
if angle_degrees < 0:
    angle_degrees += 360

7. 出力

print(f{angle_degrees=})
# angle_degrees=33.690067525979785°

33°くらい

最終的なコード

import cv2
import numpy as np
import math

image_path = "arrow.png"  # 入力画像のパス
# 画像を読み込む(グレースケール)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

# 輪郭を検出
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 最大輪郭を選択
largest_contour = max(contours, key=cv2.contourArea)

# 輪郭の近似
epsilon_ratio=0.01
epsilon = epsilon_ratio * cv2.arcLength(largest_contour, True)  # 輪郭の周囲長に基づく近似精度
approx_contour = cv2.approxPolyDP(largest_contour, epsilon, True)

# 普通の重心を計算
moments = cv2.moments(approx_contour)
centroid_x = int(moments['m10'] / moments['m00'])
centroid_y = int(moments['m01'] / moments['m00'])
centroid = np.array([centroid_x, centroid_y])

# 凸包の計算と重心を求める
hull = cv2.convexHull(approx_contour)
hull_points = hull[:, 0, :]
hull_centroid = np.mean(hull_points, axis=0).astype(int)

# ベクトル計算(普通の重心 - 凸包の重心)
vector = centroid - hull_centroid

# Y座標を反転して角度を計算
angle = math.atan2(-vector[1], vector[0])  # Y座標を反転
angle_degrees = math.degrees(angle)

# 角度を 0°~360° に正規化
if angle_degrees < 0:
    angle_degrees += 360

# 出力
print(f"f{angle_degrees=}°")

注意点

・前提として通常の重心と凸包の重心のどちらが先頭に来るかは既知であること
・画像は大きい方が方角の精度が上がる (輪郭点の位置が整数に丸められてしまうため、小さいと重心計算の精度が悪くなる)

・以下のような図形は今回の方法は使えない(凸包の重心が後に来ないため)

ただ凸包ではなく最小矩形(cv2.minAreaRect)の重心との差分でいけそう

おわりに

主成分分析などで方角のベクトルを特定する方法もありますが、矢印が横長の場合には法線の方向を主成分としてしまうパターンがあったり、向いている方向を特定できないデメリットがあります。
本記事の手法は図形のアスペクト比に左右されずに方角を特定したい場合に役立ちます。

Discussion