Python+OpenCVで画像の丸っぽさと伸びている方向の角度を求める

OpenCVのmomentsを使って丸っぽさと、伸びている方向の角度を求めるコードを書いてみた。

関数moments_round_angleXYは以下の論文の定義通りである。これは、二次のモーメントの対称性を表す指標である。(ちなみに一次のモーメントからは重心などが求まる)。丸っぽさは真円度ではなく、任意の直交する軸での対称性を表している。「丸っぽさ」が厳密には正しくない理由は下の方で述べている。

doi.org

import cv2
import matplotlib.pyplot as plt
import numpy as np
import math

height = 100
width = 120

# 楕円の長軸と短軸の設定
minors = [20, 20]
majors = [20, 20]
angles = [0, 0]
thickness = [5, -1]

# 0度から180度までの楕円を描く
for angle in range(0,181,10):
    minors.append(20)
    majors.append(50)
    angles.append(angle)
    thickness.append(-1)

# 画像のモーメントから丸っぽさと、伸びている方の角度を求める
def moments_round_angle(mom):
    X=mom["mu20"] + mom["mu02"]
    Y=((mom["mu20"] - mom["mu02"])**2 + 4.0 * mom["mu11"] **2)**0.5
    roundness = (1.0 - Y / X)**0.5
    if mom["mu20"] - mom["mu02"] == 0:
        angle = 0
    else:
        if mom["mu20"] - mom["mu02"]>0:
            angle = (math.atan(2.0*mom["mu11"]/(mom["mu20"] - mom["mu02"])) / 2.0)/math.pi*180
        else:
            angle = (math.atan(2.0*mom["mu11"]/(mom["mu20"] - mom["mu02"])) / 2.0)/math.pi*180+90
        if angle<0:
            angle += 180
    return roundness, angle

for mi, ma, an, th in zip(minors, majors, angles, thickness):
    img1 = np.zeros((height, width, 1))
    # OpenCVの角度の定義に変換するため -90
    img1 = cv2.ellipse(img1, ((width/2, height/2), (mi, ma), an-90), 255, thickness=th)
    plt.imshow(img1)
    plt.xlim(0, width)
    plt.ylim(0, height)
    plt.show()
    print("楕円の短軸長:",mi)
    print("楕円の長軸長:",ma)
    print("短軸長/長軸長:",mi/ma)
    print("楕円の角度:",an,"[deg]")
    print("楕円の厚み:",th)
    # 画像のモーメントを計算
    m1=cv2.moments(img1)
    print("丸っぽさ: {:.3f}".format(moments_round_angle(m1)[0]))
    print("角度: {:.1f} [deg]".format(moments_round_angle(m1)[1]))

f:id:onsanai:20211126140535p:plain:w300

描いた楕円の角度が40度で、計算された角度が40.1度なのでほぼあっていると言えるだろう。

次に、実用的な話にするため、アルファベットの小文字を書いて計算してみる。

for i in range(97, 123):
    img1 = np.zeros((height, width, 1))
    # 画像に文字を書く
    cv2.putText(img1, chr(i), (width//2, height//2), cv2.FONT_HERSHEY_PLAIN, 4, 255, 5, cv2.LINE_AA)
    # 画像の軸はYが逆転しているのでフリップする
    img1 = cv2.flip(img1, 0)
    plt.imshow(img1)
    plt.xlim(0, width)
    plt.ylim(0, height)
    plt.show()
    print("文字:",chr(i))
    # 画像のモーメントを計算
    m1=cv2.moments(img1)
    print("丸っぽさ: {:.3f}".format(moments_round_angle(m1)[0]))
    print("角度: {:.1f} [deg]".format(moments_round_angle(m1)[1]))

f:id:onsanai:20211126140452p:plain:w300

yの字は少し右上に伸びているので、角度として70度というのは正しそうである。

全ての小文字の丸っぽさと角度は以下の表の通り。oの丸っぽさが1なのはそのとおりだが、xも丸っぽさが1となっている。これは、今回計算した丸っぽさは任意の直行する2軸における対称性を表す指標だからである。oxはどちらも任意の直行するX軸とY軸に対して対称なので1となる。oxを区別するにはさらに高次のモーメントを使う必要がある。

最も丸っぽさが少ないのがl0.235、次にijと続く。これも直感と一致している。少し飛んでtである。実際にプログラムを実行して画像を見ながら、丸っぽさと角度を比べてみると良いだろう。

文字 丸っぽさ 角度
a 0.885 78.9
b 0.851 117.8
c 0.931 90.0
d 0.851 62.2
e 0.971 87.3
f 0.547 78.0
g 0.815 86.1
h 0.825 137.1
i 0.263 90.0
j 0.283 83.8
k 0.803 110.3
l 0.235 90.0
m 0.688 176.0
n 0.862 166.5
o 1.000 90.0
p 0.851 62.2
q 0.851 117.8
r 0.608 61.7
s 0.871 97.2
t 0.510 102.6
u 0.862 166.5
v 0.990 180.0
w 0.846 0.0
x 1.000 0.0
y 0.706 72.3
z 0.814 82.8