【Python・OpenCV】ヒストグラム逆投影法によるオブジェクト検出(cv2.calcBackProject)

※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】ヒストグラム逆投影法によるオブジェクト検出(cv2.calcBackProject)

2023-08-09

はじめに

画像処理においてヒストグラムを利用した解析方法が様々に開発されてきました。
この記事では、ヒストグラム逆投影法について解説します。
ヒストグラム逆投影法は、機械学習やディープラーニングを用いずに、オブジェクト検出やトラッキングを行う際に有用な手法です。

ヒストグラム逆投影法の概要

ヒストグラム逆投影法は、オブジェクトの特徴をヒストグラムとして表現し、そのヒストグラムを利用して画像上でオブジェクトの位置を推定する手法です。この手法を用いることで、オブジェクトの光度や色情報を元に、背景からオブジェクトを分離することが可能となるため、オブジェクト検出や動画の各フレームに適用することで、オブジェクトのトラッキングに用ることができます。

基本手順

ヒストグラム逆投影法の基本的な手順は以下の通りです:

  1. ヒストグラムの作成:
    オブジェクトの特徴を抽出し、その特徴を元にヒストグラムを作成します。例えば、Hue(色相)値を抽出してヒストグラムを作成することがよくあります。このため、色空間をRGBからHSVに変換する必要があります。
  2. 検出対象画像の作成:
    検出するオブジェクトが含まれる範囲を選択した画像を用意します。後ほど逆投影を行う際に必要となります。
  3. 逆投影の計算:
    検出対象画像に対して、先ほど作成したヒストグラムを逆投影します。OpenCVのcv2.calcBackProject関数を使用します。これにより、検出対象画像内の各ピクセルがオブジェクトにどれだけ寄与しているかが反映されます。
  4. 逆投影の結果のポスト処理:
    cv2.calcBackProject関数の出力に対して、オブジェクト検出の精度を向上させるため畳み込みを行います。畳み込みは、フィルタを用いて画像上の値を平滑化する操作です。フィルタによって、近接するピクセル値の重み付け平均が計算され、画像のノイズが軽減されます。ヒストグラム逆投影の結果に畳み込みを適用することで、オブジェクトの特徴がより滑らかになり、より明確な検出やトラッキング結果が得られることがあります。

cv2.calcBackProject関数

cv2.calcBackProject関数はヒストグラム逆投影法において、指定された画像の各ピクセルが、与えられたヒストグラムにどれだけ寄与しているかを計算します。

cv2.calcBackProject(入力画像, channels, hist, ranges, scale)

引数

名称説明
入力画像(必須)画像データ。対応するデータ型はuint8、uint16およびfloat32です。
channels(必須)画像のヒストグラムを計算するためのチャンネルのインデックス。(リスト形式で指定)
hist(必須)ヒストグラム。通常、cv2.calcHist関数を使用して事前に計算されたヒストグラムを指定します。
ranges(必須)ヒストグラムの範囲。通常は[0, 256]のように指定します。
scale(必須)ヒストグラムをスケーリングするための値。通常は1を指定します。

これらの引数はcv2.calcHist関数と共通なものが多くあります。

戻り値

画像と同じサイズ、uint8型の単一チャンネル画像(グレースケール画像)がヒストグラム逆投影の結果として得られます。

サンプルコード

ヒストグラム逆投影法のサンプルコードを下に示します。

本サンプルコードでは野球のグラウンドの写真で、芝の部分を検出します。

import cv2
import numpy as np
from matplotlib import pyplot as plt

# 入力画像を読み込み
image = cv2.imread('image.jpg')

# 色空間をHSVに変換する
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

# BGRのチャンネル並びをRGBの並びに変更
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 

# 検出対象の画像を切り抜き、左上座標(190,170)、右下座標(290,270)
roi1 = (190, 170, 290, 270)
roi_hsv_image1 = hsv_image[roi1[1]:roi1[3], roi1[0]:roi1[2]]
roi2 = (470, 165, 570, 265)
roi_hsv_image2 = hsv_image[roi2[1]:roi2[3], roi2[0]:roi2[2]]
roi3 = (510, 5, 610, 55)
roi_hsv_image3 = hsv_image[roi3[1]:roi3[3], roi3[0]:roi3[2]]
roi_hsv_image = np.vstack((roi_hsv_image1, roi_hsv_image2, roi_hsv_image3))

# 切り抜いた部分を四角形で明示
anortated_image = image.copy()
cv2.rectangle(anortated_image, (roi1[0], roi1[1]), (roi1[2], roi1[3]), (255, 0, 255), 2, cv2.LINE_AA)
cv2.rectangle(anortated_image, (roi2[0], roi2[1]), (roi2[2], roi2[3]), (255, 0, 255), 2, cv2.LINE_AA)
cv2.rectangle(anortated_image, (roi3[0], roi3[1]), (roi3[2], roi3[3]), (255, 0, 255), 2, cv2.LINE_AA)
cv2.imshow("Notation", anortated_image)

# 検出対象の画像のヒストグラムを作成
hist_hsv = cv2.calcHist([hsv_image],[0, 1], None, [180, 256], [0, 180, 0, 256] )
hist_roi_hsv = cv2.calcHist([roi_hsv_image],[0, 1], None, [180, 256], [0, 180, 0, 256] )

# ヒストグラム逆投影の計算
cv2.normalize(hist_roi_hsv, hist_roi_hsv, 0, 255, cv2.NORM_MINMAX) # 対象物体のヒストグラムはcalcBackProject関数に渡す前に必ず正規化をします
back_proj = cv2.calcBackProject([hsv_image], [0, 1], hist_roi_hsv, [0, 180, 0, 256], 1)
cv2.imshow('calcBackProject: codevace.com', back_proj)

# 畳み込みによる精度向上(直径3ピクセルの円形のカーネルを作成)
karnel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
cv2.filter2D(back_proj, -1 ,karnel, back_proj) # フィルタ処理を2回行うことで、フィルタの効果がより強く得られ、精度の向上が期待できます。
cv2.filter2D(back_proj, -1 ,karnel, back_proj)
cv2.imshow('Convolute: codevace.com', back_proj)

# 閾値処理で検出した領域を切り分ける
ret,thresh = cv2.threshold(back_proj, 5 , 255, 0)
cv2.imshow('Threshold: codevace.com', thresh)

# 検出した結果を入力画像に重ねる
thresh = cv2.merge((thresh, thresh, thresh)) # シングル チャンネルの閾値画像を3つ使い、3チャンネルの画像を作成
res = cv2.bitwise_and(rgb_image, thresh)

# ヒストグラムを表示
plt.rcParams["figure.figsize"] = [12, 7.5]                        # 表示領域のアスペクト比を設定
plt.figure('Histograms: codevace.com')                            # ウィンドウタイトルを設定
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.10, top=0.95) # 余白を設定
plt.subplot(221)                                                  # 2行2列の1番目(左上)の領域にプロットを設定
plt.imshow(rgb_image)                                             # 画像をRGBで表示
plt.axis('off')                                                   # 軸目盛、軸ラベルを消す
plt.subplot(222)                                                  # 2行2列の2番目(右上)の領域にプロットを設定
plt.imshow(hist_hsv,interpolation = 'nearest')                    # ヒストグラムのグラフを表示
plt.xlabel('Saturation')                                          # x軸ラベル(彩度)
plt.ylabel('Hue')                                                 # y軸ラベル(色相)

plt.subplot(223)                                                  # 2行2列の3番目(左中段)の領域にプロットを設定
plt.imshow(cv2.cvtColor(roi_hsv_image, cv2.COLOR_HSV2RGB))        # 画像をRGBで表示
plt.axis("off")                                                   # 軸目盛、軸ラベルを消す
plt.subplot(224)                                                  # 2行2列の4番目(右中段)の領域にプロットを設定
plt.imshow(hist_roi_hsv,interpolation = 'nearest')                # ヒストグラムのグラフを表示
plt.xlabel('Saturation')                                          # x軸ラベル(彩度)
plt.ylabel('Hue')                                                 # y軸ラベル(色相)    

# 逆投影の結果を表示
plt.rcParams["figure.figsize"] = [12, 3.5]                        # 表示領域のアスペクト比を設定
plt.figure('Back Project Results: codevace.com')                  # ウィンドウタイトルを設定
plt.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98) # 余白を設定
plt.subplot(131)                                                  # 1行3列の1番目(左)の領域にプロットを設定
plt.imshow(rgb_image)                                             # 画像をRGBで表示
plt.axis("off")                                                   # 軸目盛、軸ラベルを消す
plt.subplot(132)                                                  # 1行3列の2番目(真ん中)の領域にプロットを設定
plt.imshow(thresh)                                                # 画像をRGBで表示
plt.axis("off")                                                   # 軸目盛、軸ラベルを消す
plt.subplot(133)                                                  # 1行3列の3番目(右)の領域にプロットを設定
plt.imshow(res)                                                   # 画像をRGBで表示
plt.axis("off")                                                   # 軸目盛、軸ラベルを消す

plt.show()                                                        # ウィンドウを表示

処理の手順は次の通りです。

入力画像として、この画像を使用しました。

6行目:入力画像の読み込み。

9行目:色相チャンネルに対してヒストグラムを計算をするため、入力画像の色空間をHSVに変換します。

12行目:後でmatplotlibで画像を表示するため、入力画像のチャンネルの並びをBGRからRGBに変換します。

16〜21行目:3箇所の芝のエリアを切り取ります。21行目で一つの画像に繋ぎ合わせています。3つとも幅を100ピクセルに合わせて、縦方向に結合して一つの画像としています。

24〜28行目:3箇所の芝のエリアは下の画像の四角で囲った領域です。1箇所でも認識できますが、より多くの芝の領域を使う方が精度が高くなります。

31行目:入力画像のヒストグラムを計算します。

32行目:21行目で作成した、芝の画像のヒストグラムを計算します。

35〜37行目:ヒストグラム逆投影の計算を行います。OpenCVのcv2.calcBackProject関数の第3引数のhistに入力するヒストグラムデータは正規化したものである必要があります。35行目で正規化を行なっています。cv2.calcBackProject関数の出力結果を下に示します。

芝の領域が若干明るくなっていることが確認できます。

40〜43行目:cv2.calcBackProject関数の出力に対して、芝の領域をより鮮明化するために、畳み込み処理を行います。cv2.getStructuringElement関数で3ピクセルのサイズのカーネルを作成します。41、42行目で、このカーネルを使った畳み込みを行なっています。同じ処理を2回くり返すことで、フィルタの効果がより強く得られ、精度の向上が期待できます。43行目の出力結果を下に示します。

46〜46行目:閾値処理で検出した芝の領域を切り分けます。閾値処理では輝度が閾値より大きい場合は255を割り当て、そうではない場合は0を割り当てます。46行目では閾値を'5'に設定しています。白の部分が検出した芝の領域です。上の画像と比較するとノイズが除去されていることが確認できます。

50〜51行目:閾値処理の結果と入力画像を重ね合わせます。50行目でシングル チャンネルの画像を3つ使い、3チャンネルの画像を作成しています。51行目で重ね合わせ処理をして、閾値処理の白い部分に入力画像が表示されます。

54〜71行目:入力画像、16〜21行目で切り取った芝の画像、それぞれの2次元ヒストグラムを表示します。2次元ヒストグラムの縦軸は色相、横軸は彩度を表し、明るくなるほど画素のカウント数が大きいことを示しています。色相が40前後、彩度がおよそ100〜200の範囲が芝を表していることがわかります。

74〜85行目:新しいウィンドウを準備し、ヒストグラム逆投影法の結果を表示しています。左:入力画像、真ん中:閾値処理後の芝を検出した領域の画像、右:左と真ん中の画像を重ね合わせた画像。芝の領域のみを良く検出できていることが確認できます。

おわりに

ヒストグラム逆投影法について解説しました。
検出する芝の領域画像を3箇所にすることや畳み込み処理を2回繰り返すといった工夫をすることで、検出精度の向上を図りました。
サンプルコードは若干長めとなっていますが、様々な画像処理の手法が含まれておりご参考にして頂けたらと思います。

ご質問や取り上げて欲しい内容などがありましたら、コメントを頂けると嬉しいです。
最後までご覧頂きありがとうございました。

■(広告)OpenCVの参考書としてどうぞ!■

参考リンク

-Python, プログラミング, 画像処理
-