Python+OpenCVで重心を求める

今回はPythonで厳密な重心を求める手法を紹介します。まずは重心の定義から見ていきましょう。C++版はこちら

幾何学的には、ある図形の、そのまわりでの一次モーメントが 0 であるような点のこと。図形 D (およびその周辺)の各点 r が密度 f(r) を持つなら、その重心 g とは、

\int_D (\boldsymbol{g} - \boldsymbol{r})f(\boldsymbol{r})\,dV = \boldsymbol{0}

を満たす点 g である(g が D 外の点であることもあり得る)。

密度が一定の場合、単体に限って言うなら、全頂点の各座標の値の算術平均をその座標の値として持つ点はその単体の重心となる。

[Wikipediaより]

moments関数について

ポリゴンまたはラスタライズされた形状の,3次までのモーメントを求める、moments関数を使用します。

関数momentsは,ベクタ形状またはラスタライズされた形状の、3次までのモーメントを求めます。

\texttt{m} _{ji}= \sum _{x,y} \left ( \texttt{array} (x,y) \cdot x^j \cdot y^i \right ),

また,中心モーメントは次のように求められます。

\texttt{mu} _{ji}= \sum _{x,y} \left ( \texttt{array} (x,y) \cdot (x - \bar{x} )^j \cdot (y - \bar{y} )^i \right )

ここで $$(\bar{x}, \bar{y})$$は重心を表します:

\bar{x} = \frac{\texttt{m}_{10}}{\texttt{m}_{00}} , \; \bar{y} = \frac{\texttt{m}_{01}}{\texttt{m}_{00}} [OpenCV 2.2 documentation より]

では、実際に次の画像について重心を求めてみましょう。画像をもとに重心を求めるやり方と、輪郭をもとに重心を求めるやり方の2通りのコードを以下に示しています。画像をimread関数で読み込む部分は共通です。

sample

いずれの場合にも次のような、重心の位置に円が描かれた画像が表示されます。

 

画像をもとに重心を求める

moments関数はグレースケールの画像をもとにモーメントを計算するのでimreadで画像を読み込むときに、IMREAD_GRAYSCALE オプションをつけます。moments(img, False)としていますが、この第2引数が真(True)のとき、すべての非0ピクセルが1として扱われ、均一な密度を持つ板の重心を求めることができます。逆に言えば、この第2引数が偽(False)のときには、密度の不均一な図形の重心を求めることができます。このとき、密度は画素値に対応し、白(255)ほど高密度、黒(0)ほど低密度となります。このプログラムは重心座標を次のように出力します。

(143, 102)
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread("sample.jpg",cv2.IMREAD_GRAYSCALE)

mu = cv2.moments(img, False)
x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])

cv2.circle(img, (x,y), 4, 100, 2, 4)
plt.imshow(img)
plt.colorbar()
plt.show()

print(x,y)

 

 

輪郭をもとに重心を求める

moments関数が輪郭をもとにモーメントを計算するときには、内部的にはグリーンの定理を用いて求めています。なお、findContours関数もグレースケールの画像をもとに輪郭を計算するのでimreadで画像を読み込むときに、IMREAD_GRAYSCALE オプションをつけます。下のコードでは最大の輪郭長を持つ輪郭について重心を求めています。輪郭をもとにモーメントを計算する場合にはmoments関数に第2引数を指定する必要はなく、指定しても無視されます。このプログラムは重心座標を次のように出力します。途中for文を回しているのは最大の輪郭構成点数を持つ輪郭を計算するためのコードです。周囲長を求めるための関数を使って周囲長をcv2.arcLength(maxCont,True)というようにして計算し、比較するでも良いでしょう。

(143, 102)
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread("sample.jpg",cv2.IMREAD_GRAYSCALE)
_, contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

maxCont=contours[0]
for c in contours:
 if len(maxCont)<len(c):
 maxCont=c
 
mu = cv2.moments(maxCont)
x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])

cv2.circle(img, (x,y), 4, 100, 2, 4)
plt.imshow(img)
plt.colorbar()
plt.show()

print(x,y)

 

実は、全く同じ画像を入力に用いても、画像から求めた重心と、輪郭から求めた重心は少しだけ異なります。これは、画像の解像度が有限であるためです。

画像処理でお困りのことはありませんか?


様々なご相談に応じることができます。様々なデータに関して解析の実績がありますので、処理方法について少しだけ相談に乗って欲しいという方も、実装までお願いしたいという方も、お気軽にお問い合わせください!