##はじめに
###オプティカルフローとは
OpenCVの機能の一つにOptical Flow(オプティカルフロー)というものがあります。これを使うことによって物体の動きをベクトルで表すことができます。オブジェクトをトラッキングしたり、ものの動きの残像の表現などをすることができるようになります。
###UnityでOpenCV
OpenCVの大本はC++で書かれているので、何かしらの方法でこれをUnityのC#スクリプトからアクセスできるようにする必要があります。幸いOpenCVのC#ラッパーはすでにいくつかあるので、その中から選んで利用します。
Unityに関していえば、Asset StoreでOpenCVと検索すると二種類使えそうなものがヒットします。
と
です。
前者のOpenCV for Unityの方が安いので、そちらを買いがちですが、こちらはJavaのOpenCVラッパーをポートしているもので、残念ながら現在最新のC++版のOpenCVで使えるすべての機能が使えるというわけではありません。
後者のEmgu CVはC#ラッパーとしては古参で、最新のOpenCV3に対応したものとなっていて、オリジナルのOpenCVのほぼすべての機能が使えるようになっています。また、後者の方はWindows版であればSourceforgeにオープンソース版(商用利用不可)があるので、そちらを自分でUnityに組み込むことも可能かと思います。今回の記事では後者のライブラリを使ったオプティカルフローの方法を記しておきたいと思います。
##準備
###UnityでEmguCVのセットアップ
まずはUnity用のEmguCVのプラグインを手に入れUnityプロジェクトにインポートします。
ここではUnity 5を使うことを前提に話をすすめます。
###流れ
おおまかにオプティカルフローの結果を得る方法の流れを簡単に記すと、
- 動いている絵(ウェブキャムの動画等)を用意する
- 毎フレーム時、その時のフレームの画像と、その直前のフレームの画像を動画から抽出する
- 2つの画像をOpenCVのオプティカルフロー計算にかける
- 各特徴点、あるいは画像のピクセルの位置での流れの情報がベクトルデータとして得られる
- 出てきたベクトルデータをベクトルフィールドとして表現する
となります。動画はEmguCVのサンプルでウェブキャムの動画で画像処理として白黒反転させているものがあったので、それをそのまま使ってしまおうと思います。
##コーディング
まずはネームスペースでEmguを使えるように指定。
using System.IO;
using System.Linq;
using Emgu.CV.CvEnum;
using UnityEngine;
using System;
using System.Drawing;
using System.Collections;
using System.Text;
using Emgu.CV;
using Emgu.CV.Structure;
using Emgu.CV.Util;
using System.Runtime.InteropServices;
次にウェブカメラのセットアップをします。
private WebCamTexture webcamTexture;
private WebCamDevice[] devices;
private int wSize;
private int hSize;
private FlipType flip = FlipType.Vertical;
void Start () {
SetupWebCamera ();
}
void SetupWebCamera(){
WebCamDevice[] devices = WebCamTexture.devices;
int cameraCount = devices.Length;
if (cameraCount == 0)
{
Image<Bgr, Byte> img = new Image<Bgr, byte>(640, 240);
CvInvoke.PutText(img, String.Format("{0} camera found", devices.Length), new System.Drawing.Point(10, 60), Emgu.CV.CvEnum.FontFace.HersheyDuplex,1.0, new MCvScalar(0, 255, 0));
Texture2D texture = TextureConvert.ImageToTexture2D(img, flip);
this.GetComponent<GUITexture>().texture = texture;
this.GetComponent<GUITexture>().pixelInset = new Rect(-img.Width/2, -img.Height/2, img.Width, img.Height);
vectorFields = new Vector2[img.Width * img.Height];
wSize = img.Width;
hSize = img.Height;
}
else
{
webcamTexture = new WebCamTexture(devices[0].name);
webcamTexture.Play();
CvInvoke.CheckLibraryLoaded();
vectorFields = new Vector2[webcamTexture.width * webcamTexture.height];
wSize = webcamTexture.width;
hSize = webcamTexture.height;
}
}
次にウェブカメラの描写し、前フレームとカメラ画像と現フレームのカメラ画像を利用してオプティカルフローの計算をします。
public GameObject plane;
private Texture2D resultTexture;
private Color32[] data; //holds pixel data for webcam
private byte[] bytes;
private Image<Gray,Byte> prevImage;
private Image<Gray,Byte> currentImage;
private Vector2[] vectorFields;
void Update () {
DrawCamera ();
}
void DrawCamera(){
if (webcamTexture != null && webcamTexture.didUpdateThisFrame)
{
if (data == null || (data.Length != webcamTexture.width * webcamTexture.height))
{
data = new Color32[webcamTexture.width * webcamTexture.height];
}
webcamTexture.GetPixels32(data);
if (bytes == null || bytes.Length != data.Length*3)
{
bytes = new byte[data.Length*3];
}
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
GCHandle resultHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
using (Image<Bgra, byte> image = new Image<Bgra, byte>(webcamTexture.width, webcamTexture.height, webcamTexture.width * 4, handle.AddrOfPinnedObject()))
using (Mat bgr = new Mat(webcamTexture.height, webcamTexture.width, DepthType.Cv8U, 3, resultHandle.AddrOfPinnedObject(), webcamTexture.width * 3))
{
CvInvoke.CvtColor(image.Convert<Gray,Byte>(), bgr, ColorConversion.Gray2Bgr);
CvInvoke.BitwiseNot(bgr, bgr);
if (flip != FlipType.None)
CvInvoke.Flip(bgr, bgr, flip);
currentImage = image.Convert<Gray,Byte>();
currentImage = currentImage.Resize(0.3f,Inter.Linear);
currentImage = currentImage.Flip(FlipType.Horizontal);
vectorFields = new Vector2[currentImage.Width * currentImage.Height];
wSize = currentImage.Width;
hSize = currentImage.Height;
CalculateOpticalFlow ();
}
handle.Free();
resultHandle.Free();
if (resultTexture == null || resultTexture.width != webcamTexture.width ||
resultTexture.height != webcamTexture.height)
{
resultTexture = new Texture2D(webcamTexture.width, webcamTexture.height, TextureFormat.RGB24, false);
}
resultTexture.LoadRawTextureData(bytes);
resultTexture.Apply();
plane.GetComponent<MeshRenderer>().material.mainTexture = resultTexture;
}
}
void CalculateOpticalFlow(){
if (currentImage != null) {
GCHandle vectorHandle = GCHandle.Alloc (vectorFields, GCHandleType.Pinned);
using (Mat flowMat = new Mat(currentImage.Height, currentImage.Width, DepthType.Cv32F,2, vectorHandle.AddrOfPinnedObject(),currentImage.Width*8)) {
if (prevImage != null) {
CvInvoke.CalcOpticalFlowFarneback (prevImage, currentImage, flowMat, 0.5, 3, 15, 3, 5, 1.2, 0);
}
prevImage = currentImage;
}
vectorHandle.Free ();
}
}
最後に計算結果として出たベクトルデータを可視化します。
void OnDrawGizmos(){
Vector3 minPos = plane.GetComponent<BoxCollider>().bounds.min;
Vector3 maxPos = plane.GetComponent<BoxCollider>().bounds.max;
if (vectorFields != null && vectorFields.Length == wSize*hSize) {
int resolution = 5;
for (int i=0; i<hSize; i+=resolution) {
for (int n=0; n<wSize; n+=resolution) {
Vector2 v = vectorFields [wSize * i + n];
Gizmos.color = Color.red;
Vector3 start = new Vector3(minPos.x+n*1f/wSize*(maxPos.x-minPos.x),0,minPos.z+i*1f/hSize*(maxPos.z-minPos.z));
Gizmos.DrawLine (start,start + new Vector3 (v.x/10f, 0, v.y/10f));
}
}
}
}