オプティカルフローを簡易ジェスチャ認識に利用する
少し前に Intel 製デプスカメラ RealSense D415 と 3DiVi 社製 Nuitrack SDK の組み合わせでジェスチャ認識を試していました。下の動画には三脚に据えた D415 ごしに所定のジェスチャでスマート照明の操作をテストした様子を収めています。
[フレーム]
このように期待どおりの反応は得られるのですが、次のことが気になっていました。
- ここでのジェスチャ認識は人体の骨格検出を前提としている
- そのため立位であれ座位であれおおむね全身をカメラに呈示する必要がある
このあたりの事情は Kinect も同様のようです。もちろん骨格の検出は腕や手を適切に識別するために必要ですが、一方でもっとラフなものもあればと考えました。日常感覚ではコタツや布団にもぐり込んだまま姿勢を正すことなくスッと指示を出すといったこともできれば便利そうです。そんなわけで試しに簡単なしくみを作ってみることにしました。
考え方として、スコープ内の被写体に所定の水準以上の移動をざっくり検知した場合にその向きを判定することを想定しました。実現方法を考えながら社内でそういう話をしたところ、「OpenCV でオプティカルフローを利用してはどうか?」というレスポンスがありました。手元ではトラッキング API (リンク: @nonbiri15 様によるドキュメントの和訳) を試していたところでオプティカルフローのことは知らずにおり、@icoxfog417 様の次の記事中の比較がちょうどわかりやすくとても参考になりました。
- OpenCVでとらえる画像の躍動、Optical Flow - qiita.com/icoxfog417
画像間の動きの解析については、様々な目的とそれを実現する手法があります。ここでは、まずOptical Flowがその中でどのような位置づけになるのか説明しておきます。
- トラッキング
- 目的: 画像の中の特定の物体(人やオブジェクト)を追跡したい
- 手法: リアルタイムに行うものとそうでないものの、大別して2種類
:
- フロー推定
- 目的: 画像の中で何がどう動いたのかを検知したい(観測対象が決まっているトラッキングとは異なる)
- 手法: 画像の中の特徴的な点に絞って解析するsparse型と、画素全体の動きを解析するdense型に大別できる
- -> sparse型: Lucas-Kanade法など
- -> dense型: Horn-Schunck法、Gunnar Farneback法など
- トラッキング
以下は GitHub 上の OpenCV 公式のサンプルプログラムです。これは Dense(密)型で Gunnar Farneback 法が用いられています。
- opencv/samples/python/opt_flow.py - github.com/opencv
一般的なウェブカメラを PC へ接続してこのプログラムを実行した様子を以下の動画に収めています。スコープ内の被写体の動きが格子点の座標群を起点に捕捉されている様子が視覚的に表現されます。
[フレーム] (ScreenShot)
このサンプルプログラムに上下左右四方向への移動を大きく判定する処理を加えてみました。次の動画の要領で動作します。
[フレーム]
加筆したプログラムのソースコードです。まだまだ改良の余地はあるものの今回オプティカルフローの利便性に触れたことは大きな収穫でした。いろいろな使い方ができそうです。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 高密度オプティカルフローを利用した簡易ジェスチャ認識の試み
#
# Web カメラ視野内の被写体全般について
# Left, Right, Up, Down 四方向への移動を検知する内容
#
# OpenCV 公式の下記サンプルに処理を追加したもの
# https://github.com/opencv/opencv/blob/master/samples/python/opt_flow.py
#
# 2020-04
#
import sys
import time
import numpy as np
import cv2
# 有効移動量の閾値
THRESHOLD_DETECT = 300
# 切り捨ての閾値
THRESHOLD_IGNORE = 2
# 所定アクション区間内の x, y 方向への総移動量
MOVE = [0, 0]
# 移動発生カウンタ
COUNT_MOVE = 0
# 直近のジェスチャ検出システム時間 msec
TIME_DETECT = 0
def millis():
return int(round(time.time() * 1000))
def put_header(img, str):
cv2.putText(img, str, (26, 50), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 3)
cv2.putText(img, str, (24, 48), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0,255), 3)
# 指定イメージに高密度オプティカルフローのベクトル情報を重ねて線描(原作)
# あわせてスコープ内の左右上下 四方向への移動状況を判定
def draw_flow(img, flow, step=16):
global MOVE, COUNT_MOVE, TIME_DETECT
# 当該イメージの Hight, Width
h, w = img.shape[:2]
# イメージの縦横サイズと step 間隔指定に基づき縦横格子点座標の配列を生成
y, x = np.mgrid[step/2:h:step, step/2:w:step].reshape(2,-1).astype(int)
# 格子点座標配列要素群に対応する移動量配列を得る
fx, fy = flow[y,x].T
# 各格子点を始点として描画する線分情報(ベクトル)の配列を生成
lines = np.vstack([x, y, x+fx, y+fy]).T.reshape(-1, 2, 2)
lines = np.int32(lines + 0.5)
vis = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# 移動ベクトル線を描画
cv2.polylines(vis, lines, 0, (0, 255, 0))
#for (x1, y1), (_x2, _y2) in lines:
# cv2.circle(vis, (x1, y1), 2, (0, 0, 255), -1)
# 線分情報配列の全要素について
# 始点から終点までの x, y 各方向への移動量の累計を得る
vx = vy = 0
for i in range(len(lines)):
val = lines[i][1][0]-lines[i][0][0]
if abs(val) >= THRESHOLD_IGNORE:
vx += val
val = lines[i][1][1]-lines[i][0][1]
if abs(val) >= THRESHOLD_IGNORE:
vy += val
# 総移動量に加算
MOVE[0] += vx
MOVE[1] += vy
# 移動量の累計が所定の閾値以上なら移動中と判断
if abs(vx) >= THRESHOLD_DETECT or abs(vy) >= THRESHOLD_DETECT:
# 移動発生カウンタを加算
COUNT_MOVE += 1
# 所定の閾値未満なら移動終了状態と仮定
else:
mx = my = 0
if COUNT_MOVE > 0 and \
millis() - TIME_DETECT > 1000: # ノイズ除け
# x, y 各方向への移動量の平均を求める
mx = int(MOVE[0]/COUNT_MOVE)
my = int(MOVE[1]/COUNT_MOVE)
# x, y 方向いずれかの移動量平均が所定の閾値以上なら
# 左右上下のどの方向への移動かを判定して表示
if abs(mx) >= THRESHOLD_DETECT or abs(my) >= THRESHOLD_DETECT:
TIME_DETECT = millis()
if abs(mx) >= abs(my):
if mx >= 0:
put_header(vis, 'LEFT')
else:
put_header(vis, 'RIGHT')
else:
if my >= 0:
put_header(vis, 'DOWN')
else:
put_header(vis, 'UP')
# 総移動量と移動発生カウンタをクリア
MOVE = [0, 0]
COUNT_MOVE = 0
return vis
def main():
cam = cv2.VideoCapture(0)
# 最初のフレームを読み込む
_ret, prev = cam.read()
# グレイスケール化して保持
prevgray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
while True:
# フレーム読み込み
_ret, img = cam.read()
# グレイスケール化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 前回分と今回分のイメージから高密度オプティカルフローを求める
flow = cv2.calcOpticalFlowFarneback(prevgray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)
# 今回分を保持しておく
prevgray = gray
# flow を可視化
# https://docs.opencv.org/3.1.0/d7/d8b/tutorial_py_lucas_kanade.html
"""
hsv = np.zeros_like(img)
hsv[...,1] = 255
mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
hsv[...,0] = ang*180/np.pi/2
hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX)
rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
cv2.imshow('hsv', rgb)
"""
# 画像にフローのベクトル情報を重ねて表示
cv2.imshow('flow', draw_flow(gray, flow))
ch = cv2.waitKey(1)
if ch & 0xFF == ord('q') or ch == 27: # ESC
break
if __name__ == '__main__':
main()
cv2.destroyAllWindows()
(tanabe)