はじめに
画像の重心などによるcv2.moments
関数を使ったモーメントとは異なり、Huモーメントは画像の特徴量を表す手法で、画像のスケーリングや回転に対して不変な性質を持っています。
この記事では、Huモーメントを使ったマッチング処理について解説します。
この記事ではcv2.moments
関数について解説しています。
Huモーメントとは
Huモーメントは7個の不変量で構成されており、以下の性質を持っています。
- 画像のスケーリング(拡大縮小)に対して不変
- 画像の回転に対して不変
- 画像の並行移動(平行移動)に対して不変
これらの性質のおかげで、画像が幾何学的な変形を受けても、Huモーメントの値はほぼ変化しません。
画像認識などの分野で非常に有用な特徴量となっています。
Huモーメントを用いて、文字認識、指紋認識などのパターンマッチング特徴量として利用することがすることができます。
一方で、Huモーメントは物体の細かい違いを捉えにくいという欠点もあります。
そのため、より高次のモーメントや、他の手法と組み合わせて用いられることが多い様です。
Huモーメントは2次元画像のモーメントから以下の式で計算されます。
hu1 = η20 + η02
hu2 = (η20 - η02)^2 + 4η11^2
hu3 = (η30 - 3η12)^2 + (3η21 - η03)^2
hu4 = (η30 + η12)^2 + (η21 + η03)^2
hu5 = (η30 - 3η12)(η30 + η12)[(η30 + η12)^2 - 3(η21 + η03)^2] + (3η21 - η03)(η21 + η03)[3(η30 + η12)^2 - (η21 + η03)^2]
hu6 = (η20 - η02)[(η30 + η12)^2 - (η21 + η03)^2] + 4η11(η30 + η12)(η21 + η03)
hu7 = (3η21 - η03)(η30 + η12)[(η30 + η12)^2 - 3(η21 + η03)^2] - (η30 - 3η12)(η21 + η03)[3(η30 + η12)^2 - (η21 + η03)^2]
ここで、ηは中心化済みの規格化モーメントを表します。
(引用元:Wikipedia - Image moment)
メリットとデメリットは、次の様になります。
メリット
- 画像の幾何学的な変形(スケーリング、回転、平行移動)に対して不変な性質を持つ
- 雑音や輪郭の一部が欠けていても、ある程度の認識が可能
- 計算コストが比較的小さい
デメリット
- 物体の細かい違いを捉えにくい
- 複雑な画像に対する認識精度が低下する可能性がある
- 最適なモーメント次数の選択が難しい
cv2.HuMoments関数
cv2.HuMoments
関数の引数と戻り値について説明します。
引数
名称 | 説明 |
---|---|
モーメント(必須) | 画像のモーメント。cv2.moment関数で計算されます。 |
hu(オプション) | 結果がこの変数に格納されます。指定しない場合は、戻り値として取得できます。 |
戻り値
戻り値はNumPy配列のHuモーメントになります。
この配列のshapeは(1, 7)
となっており、7つのHuモーメントの値が格納されています。
配列の中身を具体的に見ると以下のようになっています。
hu = [ [hu0, hu1, hu2, hu3, hu4, hu5, hu6] ]
ここで、hu0
~ hu6
がHuモーメントの7つの不変量になります。
Huモーメントによる画像マッチング
画像マッチングとは
画像マッチングとは、入力された画像から特定のパターンやオブジェクトを検出する処理のことです。
Huモーメントを利用した画像マッチングでは、以下の手順が一般的です。
- 前処理:
入力画像とテンプレート画像に対して適切な前処理(グレースケール変換、ノイズ除去、輪郭抽出など)を行います。 - Huモーメントの計算 :
前処理されたそれぞれの画像から、Huモーメントを計算します。 - 特徴量ベクトルの作成:
計算されたHuモーメントから、特徴量ベクトルを作成します。 - 距離の計算 :
入力画像とテンプレート画像の特徴量ベクトル間の距離(ユークリッド距離やマハラノビス距離など)を計算します。
距離が小さいほど、両者の類似度が高いことを示します。 - 閾値処理:
計算された距離と、あらかじめ設定した閾値を比較します。距離が閾値以下であれば、入力画像にテンプレートと一致するパターンが含まれていると判定されます。
このように、Huモーメントを利用することで画像の幾何学的変形に頑健な特徴量を抽出でき、効率的なマッチングが可能になります。ただし、対象物体の複雑さによってはマッチング精度が低下する可能性があるため、実用的なシステムでは他の手法と組み合わせることがよくあります。
cv2.matchShapes関数
cv2.matchShapes関数は2つのHuモーメントの比較により、類似度を表す距離を算出します。
引数
名称 | 説明 |
contour1(必須) | 比較する最初のHuモーメントのベクトル |
contour2(必須) | 比較する最初のHuモーメントのベクトル |
method(必須) | 使用する比較方法の指定。ShapeMatchModesを参照 |
parameter(必須) | メソッドによるパラメータの指定 (現在未使用なので0を指定) |
contour1
とcontour2
には、Huモーメントから計算された7次元の特徴ベクトルを渡します。method
には、使用する距離尺度を指定します。一般的にはHuモーメントに基づくcv2.CONTOURS_MATCH_I3
を使用しますが、状況に応じて他のメソッドを選ぶこともできます。parameter
は現在未使用なので、"0"の指定で問題ありません。
戻り値
float型の値となります。これはcontour1
とcontour2
の類似度を表す距離値です。
距離値が小さいほど2つのベクトルは類似していることを意味します。
具体的には、次のような値が返されます。
- 0.0 : 完全に一致する (同一の形状)
- 0より大きい値 : 距離に応じた類似度 (小さいほど類似している)
一般に、この戻り値と任意の閾値を比較し、マッチングの成功・失敗を判定します。
画像マッチングの実践
実際に画像マッチングを行ってみましょう。
マッチングで検出対象の画像は、下になります。
この画像に対して、次の画像でマッチング処理を行います。
左から回転、縮小、回転と縮小、異なる画像(ネコ)、異なる画像(イヌ)の5種類です。(クリックで拡大)
これらはFontAwsomeに収録されているものを利用させて頂きました。
次にサンプルコードを示します。
import cv2 import matplotlib.pyplot as plt # 入力画像とテンプレート画像を読み込む image = cv2.imread("hippo.jpg") image1 = cv2.imread("hippo_rot.jpg") image2 = cv2.imread("hippo_small.jpg") image3 = cv2.imread("hippo_rot-small.jpg") image4 = cv2.imread("cat.jpg") image5 = cv2.imread("dog.jpg") # グレースケール変換 image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) image2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) image3 = cv2.cvtColor(image3, cv2.COLOR_BGR2GRAY) image4 = cv2.cvtColor(image4, cv2.COLOR_BGR2GRAY) image5 = cv2.cvtColor(image5, cv2.COLOR_BGR2GRAY) # Huモーメントの計算と特徴ベクトルの作成 image_moments = cv2.HuMoments(cv2.moments(image)).flatten() image1_moments = cv2.HuMoments(cv2.moments(image1)).flatten() image2_moments = cv2.HuMoments(cv2.moments(image2)).flatten() image3_moments = cv2.HuMoments(cv2.moments(image3)).flatten() image4_moments = cv2.HuMoments(cv2.moments(image4)).flatten() image5_moments = cv2.HuMoments(cv2.moments(image5)).flatten() # Huモーメントの距離を計算 dist1 = cv2.matchShapes(image_moments, image1_moments, cv2.CONTOURS_MATCH_I3, 0) dist2 = cv2.matchShapes(image_moments, image2_moments, cv2.CONTOURS_MATCH_I3, 0) dist3 = cv2.matchShapes(image_moments, image3_moments, cv2.CONTOURS_MATCH_I3, 0) dist4 = cv2.matchShapes(image_moments, image4_moments, cv2.CONTOURS_MATCH_I3, 0) dist5 = cv2.matchShapes(image_moments, image5_moments, cv2.CONTOURS_MATCH_I3, 0) # 画像とエッジ画像を表示 plt.rcParams["figure.figsize"] = [7,10] # ウィンドウサイズを設定 title = "cv2.HuMoments: codevace.com" plt.figure(title) # ウィンドウタイトルを設定 plt.subplots_adjust(left=0.05, right=0.95, bottom=0.03, top=0.95) # 余白を設定 plt.subplot(321) # 6行1列の1番目の領域にプロットを設定 plt.imshow(image, cmap='gray') # 入力画像をグレースケールで表示 plt.title('Image') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(322) # 6行1列の2番目の領域にプロットを設定 plt.imshow(image1, cmap='gray') # Image1の画像とマッチングの結果表示 plt.title('Image1 (' + str(dist1) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(323) # 6行1列の2番目の領域にプロットを設定 plt.imshow(image2, cmap='gray') # Image2の画像とマッチングの結果表示 plt.title('Image2 (' + str(dist2) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(324) # 6行1列の3番目の領域にプロットを設定 plt.imshow(image3, cmap='gray') # Image3の画像とマッチングの結果表示 plt.title('Image3 (' + str(dist3) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(325) # 6行1列の4番目の領域にプロットを設定 plt.imshow(image4, cmap='gray') # Image4の画像とマッチングの結果表示 plt.title('Image4 (' + str(dist4) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(326) # 6行1列の5番目の領域にプロットを設定 plt.imshow(image5, cmap='gray') # Image5の画像とマッチングの結果表示 plt.title('Image5 (' + str(dist5) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.show()
このサンプルコードを実行した結果を示します。
各画像のタイトルの後の数値がHuモーメントの距離値です。
カバの画像は回転や縮小をしてもHuモーメントの距離値は十分に小さな値となっています。
写真の画像ではないため、Huモーメントの距離値がかなり小さくなっていると考えられます。
前に述べた手順と比較すると、処理対象の画像にノイズなどがないのでこのサンプルコードでは前処理を行っていませんが、それ以外は同様の処理をしています。
また、"Huモーメントの計算"と"特徴ベクトルの作成"は1行で行なっています。cv2.HuMoments
関数の戻り値をflatten()
関数で1次元ベクトルに変換しています。
# Huモーメントの計算と特徴ベクトルの作成 image_moments = cv2.HuMoments(cv2.moments(image)).flatten() image1_moments = cv2.HuMoments(cv2.moments(image1)).flatten() image2_moments = cv2.HuMoments(cv2.moments(image2)).flatten() image3_moments = cv2.HuMoments(cv2.moments(image3)).flatten() image4_moments = cv2.HuMoments(cv2.moments(image4)).flatten() image5_moments = cv2.HuMoments(cv2.moments(image5)).flatten()
次に、前処理を行うコードを考えてみます。
cv2.medianBlur
関数でノイズ削減処理を行い、cv2.Canny
関数でエッジを抽出する前処理を追加してみましょう。
実際のアプリケーションでは、この様な処理を行うことが多いと想定されます。
#!python3 # -*- coding: utf-8 -*- ''' Huモーメントによるマッチング処理(cv2.HuMoments) ''' import cv2 import matplotlib.pyplot as plt # 入力画像とテンプレート画像を読み込む image = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/hippo.jpg") image1 = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/hippo_rot.jpg") image2 = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/hippo_small.jpg") image3 = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/hippo_rot-small.jpg") image4 = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/cat.jpg") image5 = cv2.imread("【Python・OpenCV】Huモーメントによるマッチング処理(cv2.HuMoments)/images/dog.jpg") # グレースケール変換 image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) image2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) image3 = cv2.cvtColor(image3, cv2.COLOR_BGR2GRAY) image4 = cv2.cvtColor(image4, cv2.COLOR_BGR2GRAY) image5 = cv2.cvtColor(image5, cv2.COLOR_BGR2GRAY) # ノイズ除去(メディアンフィルター) image = cv2.medianBlur(image, 11) image1 = cv2.medianBlur(image1, 11) image2 = cv2.medianBlur(image2, 11) image3 = cv2.medianBlur(image3, 11) image4 = cv2.medianBlur(image4, 11) image5 = cv2.medianBlur(image5, 11) # Cannyエッジ検出を適用 th1 = 30 th2 = 200 image = cv2.Canny(image, threshold1=th1, threshold2=th2) image1 = cv2.Canny(image1, threshold1=th1, threshold2=th2) image2 = cv2.Canny(image2, threshold1=th1, threshold2=th2) image3 = cv2.Canny(image3, threshold1=th1, threshold2=th2) image4 = cv2.Canny(image4, threshold1=th1, threshold2=th2) image5 = cv2.Canny(image5, threshold1=th1, threshold2=th2) # Huモーメントの計算と特徴ベクトルの作成 image_moments = cv2.HuMoments(cv2.moments(image)).flatten() image1_moments = cv2.HuMoments(cv2.moments(image1)).flatten() image2_moments = cv2.HuMoments(cv2.moments(image2)).flatten() image3_moments = cv2.HuMoments(cv2.moments(image3)).flatten() image4_moments = cv2.HuMoments(cv2.moments(image4)).flatten() image5_moments = cv2.HuMoments(cv2.moments(image5)).flatten() # Huモーメントの距離を計算 dist1 = cv2.matchShapes(image_moments, image1_moments, cv2.CONTOURS_MATCH_I3, 0) dist2 = cv2.matchShapes(image_moments, image2_moments, cv2.CONTOURS_MATCH_I3, 0) dist3 = cv2.matchShapes(image_moments, image3_moments, cv2.CONTOURS_MATCH_I3, 0) dist4 = cv2.matchShapes(image_moments, image4_moments, cv2.CONTOURS_MATCH_I3, 0) dist5 = cv2.matchShapes(image_moments, image5_moments, cv2.CONTOURS_MATCH_I3, 0) # 画像とエッジ画像を表示 plt.rcParams["figure.figsize"] = [7,9] # ウィンドウサイズを設定 title = "cv2.HuMoments with Canny,medianBlur: codevace.com" plt.figure(title) # ウィンドウタイトルを設定 plt.subplots_adjust(left=0.05, right=0.95, bottom=0.03, top=0.95) # 余白を設定 plt.subplot(321) # 6行1列の1番目の領域にプロットを設定 plt.imshow(image, cmap='gray') # 入力画像をグレースケールで表示 plt.title('Image') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(322) # 6行1列の2番目の領域にプロットを設定 plt.imshow(image1, cmap='gray') # Image1の画像とマッチングの結果表示 plt.title('Image1 (' + str(dist1) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(323) # 6行1列の2番目の領域にプロットを設定 plt.imshow(image2, cmap='gray') # Image2の画像とマッチングの結果表示 plt.title('Image2 (' + str(dist2) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(324) # 6行1列の3番目の領域にプロットを設定 plt.imshow(image3, cmap='gray') # Image3の画像とマッチングの結果表示 plt.title('Image3 (' + str(dist3) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(325) # 6行1列の4番目の領域にプロットを設定 plt.imshow(image4, cmap='gray') # Image4の画像とマッチングの結果表示 plt.title('Image4 (' + str(dist4) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(326) # 6行1列の5番目の領域にプロットを設定 plt.imshow(image5, cmap='gray') # Image5の画像とマッチングの結果表示 plt.title('Image5 (' + str(dist5) + ')') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.show()
このコードを実行した結果が次になります。
前のサンプルコードと同様に、カバの画像は回転や縮小をしてもHuモーメントの距離値は十分に小さな値となっていますが、全体的にHuモーメントの距離値が大きくなっています。cv2.medianBlur
関数で、cv2.Canny
関数の引数の値によりHuモーメントの距離値が変わります。
下記の結果はcv2.medianBlur
関数の引数をksize=11、cv2.Canny
関数の引数をthreshold1=30, threshold2=200として行っています。
実際のアプリケーションは、この様なレンジの値になっていると想定されます。
どちらの結果も画像マッチングで良い結果を得ることができました。
後者のサンプルコードの様に、前処理を行うことが通常です。画像の状態から、どの様な処理を行うことが適切か判断する必要があります。
画像マッチングで良い結果を得るための精度向上のポイントは下のようになります。
また、画像マッチング結果の距離値の閾値を設定は課題のひとつであると思います。
しかし、一般的な答えはありません。閾値の設定は、使用するデータセットや用途、要求される精度などによって異なります。
そこで、適切な閾値を見つけるために、次のようなアプローチが有効かもしれません。
- データセットから経験的に閾値を決定:
マッチングの成功例と失敗例の距離分布を確認し、両者を適切に分離できる閾値を探します。さらに、実際の用途で許容できる精度を考慮して調整します。 - クロスバリデーションによる最適化:
利用可能なデータセットを学習用とテスト用に分割し、様々な閾値に対するマッチング精度を評価します。精度が最大となる閾値を採用します。 - 事前知識の活用:
対象物体の大きさや形状の範囲などの事前知識を活用して、おおよその閾値の範囲を絞り込みます。その上で微調整を行います。 - ユーザ入力による調整:
ユーザに初期の閾値を設定してもらい、マッチング結果に対するフィードバックを元に、徐々に閾値を最適化していきます。 - 適応的な閾値調整:
システムの動作中に、マッチングの成功率や誤検出率などを監視し、自動的に閾値を調整していく手法も考えられます。
距離の閾値は用途によって大きく異なる可能性があり、単純に0.1などの一般的な値を使うのではなく、上記のようなアプローチで問題に合わせて調整する必要があります。
初期値の設定は重要ですが、運用中にデータを収集しながら適切に調整することが肝心です。
おわりに
Huモーメントを利用した画像マッチングは、画像の幾何学的な変形に頑健な手法として知られています。
しかし、適切な前処理やパラメータ設定が重要であり、対象物体の複雑さによってはマッチング精度が低下する可能性があります。
実用的な画像認識システムを構築する際は、Huモーメントに加えて他の手法も組み合わせることで、より高い認識精度が期待できます。
ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。
参考リンク
OpenCV: HuMoments
Calculates seven Hu invariants.
OpenCV: matchShapes
Compares two shapes.