物体の検出と追跡

カメラ画像での物体の「検出」/「追跡」は、次のようなものです。

検出: detection
画像中で対象物が映っている領域をみつける
追跡: tracking
フレーム中での対象物の移動を把握する

今回は、対象物の色を手がかりにした追跡を行ってみます。

対象物が映っている領域の指定は、マウスでドラッグすることで行います。その後、指定した領域の特徴をもとに、追跡を行います。

検出と追跡の両方を行えば、例えばカメラに写った顔をみつけてそれがフレーム中でどのように移動していくのかを把握することができます。

色相による追跡の流れ

対象物の色相を特徴として、それを追跡します。

あらかじめ対象物が映っている領域を指定するので、このときにその領域のHueのヒストグラムが得られます。指定された領域とこのヒストグラムをもとに、追跡を行っていきます。追跡の流れは、次のとおりです。

  1. RGB画像をHSV画像に変換
  2. S画像、V画像からマスクを作成(色相の比較に利用できない領域を切り離すためのオプションの課程)
  3. H画像と対象物のヒストグラム(対象物のHueのヒストグラム)からBack projectionにより特徴量の画像を作成
  4. 特徴量の画像を閾値で切って2値画像に変換(オプションの課程)
  5. Camshift法により特徴量の画像のうち、明るい領域(=特徴のよく現れた領域)を囲う領域を探索

重要なのは、HSV画像への変換、Back projection、CamShift法 だと言えます。

今回のソフトは、OpenCVのCamShift法で説明されているサンプルソフトをベースにしています。 色相による追跡では、CamShift法を利用している例が多いために、CamShift法は色相で追跡するアルゴリズムと思われているかもしれません。

しかし、CamShift法は特徴量の画像(特徴が良く現れてる部分を白で表現した画像)で特徴の強い領域を探索するアルゴリズムであって、終盤で使う方法の1つにすぎません。

追跡を試してみる

追跡するノードを試してみます。カメラノードと追跡するためのノードを起動します。

$roslaunch ptcam_vision usb_cam.launch
$roslaunch ptcam_vision camshift.launch
		

カメラ画像に対象物を映し出し、対象物をマウスドラッグで囲って選択すると、その領域における色相のヒストグラムが作成され、それを特徴とした追跡が行われます。

対象物を移動していくと、追跡された領域:緑色の矩形も移動してきます。対象物を回転させると、それに合わせて矩形も回転しますし、カメラから対象物を離すと矩形も小さくなります。追跡領域中央の十字マークは、矩形の中心を表します。

左上のBackprojectウインドウは、特徴量の画像で、Back projectionの結果を2値化した画像です。右上のウインドウは、マウスで選択された領域の色相のヒストグラムです。

わざと色とりどりの背景がある場面で実験しています。特徴量の画像を見ていると、背景部分も白くノイズのようにチカチカ出てきます。対象物の色がはっきりしているので、閾値をもっと大きく設定すれば、背景と対象物をしっかり判別できます。

ソースコード

前記しましたが、ソフトはOpenCVのCamShift法のサンプルをベースにしています。また、以前紹介したROS2OpenCV2クラスを利用しています。

追跡は、process_image()で行われています。

    def process_image(self, cv_image):
        try:
            # First blur the image
            frame = cv2.blur(cv_image, (5, 5))
            
            # Convert from RGB to HSV space
            hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
            
            # Create a mask using the current saturation and value parameters
            mask = cv2.inRange(hsv, np.array((0., self.smin, self.vmin)), np.array((180., 255., self.vmax)))
            
            # If the user is making a selection with the mouse, 
            # calculate a new histogram to track
            if self.selection is not None:
                x0, y0, w, h = self.selection
                x1 = x0 + w
                y1 = y0 + h
                self.track_window = (x0, y0, x1, y1)
                hsv_roi = hsv[y0:y1, x0:x1]
                mask_roi = mask[y0:y1, x0:x1]
                self.hist = cv2.calcHist( [hsv_roi], [0], mask_roi, [16], [0, 180] )
                cv2.normalize(self.hist, self.hist, 0, 255, cv2.NORM_MINMAX);
                self.hist = self.hist.reshape(-1)
                self.show_hist()
    
            if self.detect_box is not None:
                self.selection = None
            
            # If we have a histogram, track it with CamShift
            if self.hist is not None:
                # Compute the backprojection from the histogram
                backproject = cv2.calcBackProject([hsv], [0], self.hist, [0, 180], 1)
                
                # Mask the backprojection with the mask created earlier
                backproject &= mask
    
                # Threshold the backprojection
                ret, backproject = cv2.threshold(backproject, self.threshold, 255, cv.CV_THRESH_TOZERO)
    
                x, y, w, h = self.track_window
                if self.track_window is None or w <= 0 or h <=0:
                    self.track_window = 0, 0, self.frame_width - 1, self.frame_height - 1
                
                # Set the criteria for the CamShift algorithm
                term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )
                
                # Run the CamShift algorithm
                self.track_box, self.track_window = cv2.CamShift(backproject, self.track_window, term_crit)
                
                
                if len(self.track_box) == 3:
                    center = self.track_box[0]
                    llen = 30;
                    pt1 = (int(center[0] - llen), int(center[1]))
                    pt2 = (int(center[0] + llen), int(center[1]))
                    pt3 = (int(center[0]), int(center[1] - llen))
                    pt4 = (int(center[0]), int(center[1] + llen))
                    cv2.line(self.marker_image, pt1, pt2, (255, 255, 0), 5)
                    cv2.line(self.marker_image, pt3, pt4, (255, 255, 0), 5)
                
                # Display the resulting backprojection
                cv2.imshow("Backproject", backproject)
        except:
            pass

Back projection

色相を特徴として対象物の追跡を行いますが、元画像のどの部分にその特徴がよく現れているかを示す、特徴量の画像を作成します。

例えば、オレンジのコップを対象物としたとき、元画像でそのオレンジに近い色が出ている領域は、特徴量の画像では白くなります。 逆に、元画像でオレンジから離れた色の領域は、特徴量の画像では黒くなります。

この特徴量の画像を作成するために、Back projectionを利用します。

Back projectionでは、ヒストグラムの比を特徴量とします。対象物の(色相の)ヒストグラムをあらかじめ作成しておき、入力された元画像(これも今回は色相の画像)のヒストグラムを作成します。この2つのヒストグラムの比が大きいところを、特徴量の画像で輝度を高く(白く)します。

Back projectionでは、ヒストグラムの比をとっているのがミソです。

全体的に赤っぽい中にオレンジ色のコップを映せば、その色相の違いは小さいものの、特徴量は大きくなります。また、対象物が単色ではなく、広く色が分布しているような場合では、対象物の色のうち画像全体の色にない色が特徴として大きく評価され、特徴量が大きくなります。

今回のソフトでは、Backprojectウインドウに特徴量の画像を2値化したものを表示しています。

オレンジのコップは、単色ではっきりしているので色相による特徴がはっきり出ます。特徴量の画像を見ると、周囲とコップを的確に判別できています。 しかし、もっと色が曖昧なものを対象物にすると、色相のヒストグラムが横に広がり、特徴がつかみにくくなることがわかります。

MeanShift法

CamShift法は、MeanShift法を拡張したものなので、まずMeanShift法を簡単に説明します。

MeanShift法は、Gray scale画像と領域(窓)を与え、その窓を平行移動して画像に重ねたとき、窓内の輝度が高くなる(極大になる)ところを探索します。

具体的には、窓に入っている画像の重心を求め、窓をその重心に移動して再び重心を求め・・・ということを繰り返していき、最初に与えられた窓の位置の周囲において最も明るくなる窓の位置を見つけます。

上の図において、水色の丸が輝点、赤い枠が初期値として与えられる領域(窓)を表します。初期の窓(赤い枠)の重心を求めるとピンク色の丸印が求まり、そこに窓を移動(ピンク色の一点鎖線)します。これを繰り返していくと、最終的に緑点線の窓が見つかるという仕組みです。

MeanShift法では、画像全体を探索するのではなく、初期値として与えられた位置の周囲に窓を移動していって明るくなるところを探す、ということです。 そのため、周囲で輝度が極大となる窓の位置がみつかります。

CamShift法

CamShift法は、MeanShift法を行ったあと、さらに窓の大きさと傾きを変えていき、輝度が極大となる窓の大きさと傾きを得ます。

そのため、対象物がカメラに近づいて大きく映った場合には、領域(窓)の大きさが大きくなり、逆に遠くなったときには窓は小さくなります。

MeanShift法では、初期の窓を移動していくだけでしたが、CamShift法では窓の大きさと傾きも加わるので対象物が映っているおおよその領域を掴むことができます。

マスクの役割

スライドバーでマスクのパラメータを変更できます。このマスクでは、彩度(Saturation)の最小値と輝度(Value)の範囲により画像をマスクしています。

画像の中でも彩度が低い(色がうすい)部分や特に暗い/明るい部分は色相がはっきり出ない(実物の色相から離れてしまう)ために、このようなマスクをかけ、探索の範囲から外しています。

追跡だけ行うことの問題点

上の動画を見ると、対象物が色鮮やかで今回の方式に向いていることもあり、追跡はうまくいっていることがわかります。

動画では、わざと対象物をフレーム外に出して追跡を逃れたり、再び出現させたりしています。 このとき、どのように追跡されているのかを見ると、追跡だけを行うことの問題点がよくわかります。

追跡できているうちはよいのですが、対象物が追跡を逃れたときには、画面全体が対象物として認識されたりします。画面に対象物があるか否かの検出を行っていないので、いつまでも対象物があると思って追跡しようとしているのです。