2020年04月27日

オプティカルフローを簡易ジェスチャ認識に利用する

はてなブックマークに登録

少し前に Intel 製デプスカメラ RealSense D4153DiVi 社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 法が用いられています。

一般的なウェブカメラを 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)
klab_gijutsu2 at 14:56│Comments(0)その他 

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント