MeanShiftでタチコマを追いかける

最初に、MeanShiftの理解の為に写真を撮ります。
その後で、タチコマを追いかけます。

MeanShiftの概要

ミーンシフト法は(mean shift)法は、画像領域の分割や対象画像の追跡に用いられる手法である。

ディジタル画像処理 p.205

近傍の点群の平均(mean)位置に移動(shift)を繰り返し、極大点を求める方法からミーンシフト法と呼ばれる。

ディジタル画像処理 p.206

MeanShiftのサンプルプログラムの説明

OpenCV 3.2のMeanShiftのサンプルプログラムを少し変更したソースコードを掲載します。
コメントを付けて、写真を保存するように変更しています。

open_cv_meanshift.py

# -*- coding: utf-8 -*-

# [USAGE]
# python3 open_cv_meanshift.py

# ------------------------------------------------------------------------------

import os
import time

## library for array
import numpy as np
## OpenCV 3.2
import cv2 as open_cv

import matplotlib.pyplot as plt

# ------------------------------------------------------------------------------

SLEEP_TIME = 0.0
MEANSHIFT_RECOVERY_TIME = 0.5

WIDTH = 640
HEIGHT = 480
IMAGE_CENTER_X = WIDTH/2
IMAGE_CENTER_Y = HEIGHT/2

ROI_WIDTH = 240
ROI_HEIGHT = 240
ROI_X_START = int((WIDTH - ROI_WIDTH) / 2)
ROI_Y_START = int((HEIGHT - ROI_HEIGHT) / 2)

# ------------------------------------------------------------------------------

## create instance

video_capture = open_cv.VideoCapture(0)

if video_capture.isOpened():
    print('video capture: 0')
else:
    video_capture = open_cv.VideoCapture(1)
    if video_capture.isOpened():
        print('video capture: 1')
    else:
        print('can not get video capture')
        quit()

# ------------------------------------------------------------------------------

## video -> photo
## get a frame

while True:
    ret, frame = video_capture.read()
    if ret:
        print("GET VIDEO CAPTURE")
        break
    else:
        print("NOT GET VIDEO CAPTURE")

open_cv.imwrite("001_frame.png", frame)

# ------------------------------------------------------------------------------

## Width

video_capture.set(open_cv.CAP_PROP_FRAME_WIDTH, WIDTH)

# ------------------------------------------------------------------------------

## Height

video_capture.set(open_cv.CAP_PROP_FRAME_HEIGHT, HEIGHT)

# ------------------------------------------------------------------------------

## setup ROI for tracking

## setup initial location of window
## C525   (depend on specification) -> 1280 x 720
## OpenCV (depend on setting)       ->  640 x 480
## ROI    (depend on setting)       ->  240 x 240

#                    640
#         <----------------------->
#       (0,0)      x = 200
#         --------------------------  ^
#         |                        |  |
#         |           w = 240      |  |
# y = 120 |        -------         |  |
#         |        |     | h = 240 |  | 480
#         |        -------         |  |
#         |                        |  |
#         |                        |  |
#         --------------------------  v

x, y, w, h = ROI_X_START, ROI_Y_START, ROI_WIDTH, ROI_HEIGHT
track_window = (x, y, w, h)
print('x, y, w, h: ', x, y, w, h)

## Region of Interest / Region of Image
## from y to y+h
## from x to x+w
roi = frame[y:y+h, x:x+w]
open_cv.imwrite("002_roi.png", roi)

# ------------------------------------------------------------------------------

## ignore dark pixel

## BGR (Blue Green Red) -> HSV (Hue Saturation Value)
## Value == Brightness
hsv_roi = open_cv.cvtColor(roi, open_cv.COLOR_BGR2HSV)

## open_cv.inRange( hsv_roi, np.array( Hue Min, Saturation Min, Value Min ), np.array( Hue Max, Saturation Max, Value Max ) )
img_mask = open_cv.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
open_cv.imwrite("003_img_mask.png", img_mask)

# ------------------------------------------------------------------------------

## histgram

roi_hist = open_cv.calcHist([hsv_roi], [0], img_mask, [180], [0, 180])
print('roi_hist')
print(roi_hist)
plt.plot(roi_hist)
plt.savefig("004_roi_hist.png")

open_cv.normalize(roi_hist, roi_hist, 0, 255, open_cv.NORM_MINMAX)
print('normalized_roi_hist')
print(roi_hist)
plt.plot(roi_hist)
plt.savefig("005_normalized_roi_hist.png")

# ------------------------------------------------------------------------------

## make criteria for mean shift

## type                  : open_cv.TERM_CRITERIA_EPS or open_cv.TERM_CRITERIA_COUNT
## max_iter              : 10
## epsilon / accuracy    : 1.0
termination_criteria = (open_cv.TERM_CRITERIA_EPS | open_cv.TERM_CRITERIA_COUNT, 10, 1)

# ------------------------------------------------------------------------------

while True:
    ## input q -> quit
    k = open_cv.waitKey(1)
    if k == ord('q'):
        S.write('q'.encode('utf-8'))
        time.sleep(2)
        break

    ## get a frame
    ret, frame = video_capture.read()

    if ret:
        ## BGR (Blue Green Red) -> HSV (Hue Saturation Value) ## Value == Brightness
        hsv = open_cv.cvtColor(frame, open_cv.COLOR_BGR2HSV)

        ## open_cv.calcBackProject( images, channels, mask, hist_size, ranges )
        dst = open_cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
        open_cv.imwrite("006_back_project.png", dst)

        ## get the position
        ret, track_window = open_cv.meanShift(dst, track_window, termination_criteria)
        x, y, w, h = track_window
        print('x, y, w, h: ', x, y, w, h)

        if ret:
            print('meanshift success')
        else:
            print('meanshift fail')

        ## write rectangle
        ## color                 : Scalar(255,255,255) == white
        ## thickness             : thickness of lines : 3
        img_dst = open_cv.rectangle(frame, (x, y), (x+w, y+h), (255, 255, 255), 3)

        ## get center of meanshift
        roi_center_x = int(x + (ROI_WIDTH/2))
        roi_center_y = int(y + (ROI_HEIGHT/2))
        print('roi_center_x, roi_center_y: ', roi_center_x, roi_center_y)
        print('IMAGE_CENTER_X, IMAGE_CENTER_Y: ', IMAGE_CENTER_X, IMAGE_CENTER_Y)

        ## write small circle to roi center
        ## image
        ## center
        ## radius : diameter / 2
        ## color
        ## thickness : negative value -> filled
        open_cv.circle(img_dst, (roi_center_x, roi_center_y), 10, (0, 180, 0), -1)

        open_cv.imshow('SHOW MEANSHIFT IMAGE', img_dst)

        time.sleep(SLEEP_TIME)

        quit()

        # ------------------------------------------------------------------------------

    else:
        break

video_capture.release()
open_cv.destroyAllWindows()

それぞれについて説明します。(重要なところだけ)

video_capture = open_cv.VideoCapture(0)

if video_capture.isOpened():
    print('video capture: 0')
else:
    video_capture = open_cv.VideoCapture(1)
    if video_capture.isOpened():
        print('video capture: 1')
    else:
        print('can not get video capture')
        quit()

WebカメラをOpenCVから使えるようにします。大体の場合に0か1の値で良いのですが、失敗するときがあります。pythonスクリプトの実行を繰り返すと成功します。

while True:
    ret, frame = video_capture.read()
    if ret:
        print("GET VIDEO CAPTURE")
        break
    else:
        print("NOT GET VIDEO CAPTURE")

open_cv.imwrite("001_frame.png", frame)

Webカメラから画像を1枚取得します。retは、return valueの略で画像の取得に成功するとtrueとなり、画像の取得に失敗するとfalseになります。画像の取得に成功しているときframeには画像データが入ります。

MeanShift_001

roi = frame[y:y+h, x:x+w]
open_cv.imwrite("002_roi.png", roi)

左上を(0, 0)として(120, 200)から、高さ240、幅240の領域をROI(Region of Interest / Region of Image)として使います。

MeanShift_002

hsv_roi = open_cv.cvtColor(roi, open_cv.COLOR_BGR2HSV)

## open_cv.inRange( hsv_roi, np.array( Hue Min, Saturation Min, Value Min ), np.array( Hue Max, Saturation Max, Value Max ) )
img_mask = open_cv.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
open_cv.imwrite("003_img_mask.png", img_mask)

ROIの部分だけをcvtColor関数を利用してBGRをHSVに変換します。inRange関数を利用して暗すぎる部分(彩度59以下、明度31以下)を無視するためのフィルターを作成します。

MeanShift_003

HSVについて

BGRは、Blue、Green、Redを表します。OpenCVではRGBでなくBGRが使われています。
HSVは、Hue(色相)、Saturation(彩度)、Value(明度)を表します。

MeanShift_hue
色相は、赤を0度、緑を120度、青を240度として、360度で色を表します。OpenCVでは1/2の180度で同じことを表しています。ですので、緑は60度、青は120度の値になります。
彩度は、0のときは黒や白になり、値が大きいほど白や黒ではなく赤、青、黄、緑、紫だけの色になります。OpenCVでは上限は255です。
明度は、OpenCVでは0が黒、255が白になり、1から254は値が大きいほど白に近いグレーになります。

OpenCV以外では、考え方は同じですが、値の範囲などが異なる場合があります。
BGRやRGBより、HSVの方が適した処理(全体的に明るくしたい、など)があります。BGRやRGBから簡単な計算でHSVを得ることができます。

roi_hist = open_cv.calcHist([hsv_roi], [0], img_mask, [180], [0, 180])
print('roi_hist')
print(roi_hist)
plt.plot(roi_hist)
plt.savefig("004_roi_hist.png")

calcHist関数を利用して、ヒストグラムを作成します。
calcHist関数の引数について説明します。

[hsv_roi] 入力画像。
[0] 入力画像がHSVなので、[0]はHue。[1]ならSaturation。[2]ならValue。
img_mask inRange関数で作成したフィルター。
[180] Hueの全ての値についてヒストグラムを作るため180を設定。入れ物の数を設定する。
[0,180] Hueの全ての値についてヒストグラムを作るため0-180を設定。範囲を設定する。

色相だけに基づいたヒストグラムを作成しています。色相だけのヒストグラムを使ったMeanShiftになります。
ROIの部分(=2枚目の小さな写真)について色相だけを考える場合、大部分は濃い青なので、120°付近だけが飛び出たグラフになっています。横軸は色相(範囲は0-180)で、縦軸はpixel数です。

MeanShift_004

open_cv.normalize(roi_hist, roi_hist, 0, 255, open_cv.NORM_MINMAX)
print('normalized_roi_hist')
print(roi_hist)
plt.plot(roi_hist)
plt.savefig("005_normalized_roi_hist.png")

calcBackProject関数の入力とするために0-255の範囲に正規化します。見えにくいですが、緑色の線が正規化したヒストグラムです。

MeanShift_005

    ret, frame = video_capture.read()

    if ret:
        ## BGR (Blue Green Red) -> HSV (Hue Saturation Value) ## Value == Brightness
        hsv = open_cv.cvtColor(frame, open_cv.COLOR_BGR2HSV)

        ## open_cv.calcBackProject( images, channels, mask, hist_size, ranges )
        dst = open_cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
        open_cv.imwrite("006_back_project.png", dst)

        ## get the position
        ret, track_window = open_cv.meanShift(dst, track_window, termination_criteria)

もう一度、USBカメラから画像を取得して、HSVに変換します。この画像と、正規化したROIの色相のヒストグラムをcalcBackProject関数に渡します。
ROIの色相のヒストグラムに基づいたグレースケール画像を取得します。
roi_histは更新されずに、最初の計算のまま使われ続けます。

MeanShift_006

色がある部分については、濃い青の部分のみ白くなっていて、赤、緑、黄、薄い青の部分は黒くなっています。
白い部分は白く、黒い部分は黒く、そのままです。
ROIの色相のヒストグラムに基づいて、正確に処理している様子がわかります。
この関数の仕様を調べると、single-channel arrayを出力するとなっているので、これはグレースケール画像の出力とわかります。

これから先、ここで得たROIの中の画素の分布密度に基づいてtrack_windowを移動させることを繰り返して、タチコマを追いかけます。

MeanShiftでタチコマを追いかける

四角の中の中心の点は不要だったかもしれません。
他の処理と連携させると色々とできそうです。

参考

Meanshift and Camshift
calcBackProject()





«       »