自習室

こもります

Three.jsのオフスクリーンレンダリングとping-pongで、リアルタイム動体除去を行う

はじめに

f:id:AMANE:20150420184058p:plain

前回に続き、ブラウザ内だけで画像処理的なことをしよう、というチャレンジです。今回は動体除去というか、モーションブラーというか、画像の時間平均処理をしてみます。

上の写真は、割と短時間の平均画像です。動いているものが透けているのがわかると思います。画像の平均処理をもっと長時間で行うと、ほぼ動体が見えなくなります。アルゴリズムはあとから説明します。

OpenCVC++で使うと、普通にMatとかで前回のフレームを保存しておいたり差分を取ったりとお手軽に出来ますが、おなじことをJavaScriptでやろうとすると、必要な枚数画像を展開して、ピクセルを全走査するなど、かなりヘビーな感じになります。今回はブラウザ内で完結させてみるのが目的なので、WebGLのシェーダを使ってみます。

完成品

完成品はこちらで確認出来ます。ページを開いたら、カメラ利用に許可を与えて下さい。上部のバーで除去具合を調整出来ます。ゼロにするとリアルタイムの動画をそのまま出しますが、大きくしていくと、だんだん動体が薄くなっていくのがわかると思います。

動作確認は以下の環境で行っています。

動体除去のアルゴリズム

FIRフィルタによる時間平均処理

動画像を時間にわたって足し合わせて平均を取ると、動かないものは残り、動くものは薄ーくなります。これが基本。FIRフィルタ的な感じ。動画像のローパスフィルタですね。

f:id:AMANE:20150420154518p:plain

式で書くとこんな感じ。 X(n) がカメラから得られる最新フレームの画像で、 X(n-1) が一個前のカメラフレーム。 Y(n) が今回表示される絵。

 {
Y(n) = a_{n}X(n) + a_{n-1}X(n-1) + a_{n-2}X(n-2)+\ldots
}

 {
\sum_{k=0}^{N}a_{n-k} = 1.0
}

係数の合計が1になる様にします。1より小さいと全体の画像がグレイがかって薄くなり、1より大きいと、白飛びした画像になります。

この手法でちゃんと動体を消すにはタップ(データを記憶して、演算をする)を多くしなければなりません。ブラウザに大量のframebuffer objectを確保しなければならなくなるので、リーズナブルではありません。

IIRフィルタ(的)な時間平均処理

上記のような問題は、画像処理だけでは無く電気回路におけるノイズ取りなどでも似たようなことがあるようで、解決法としてIIRという手法があります。

大量のタップを用意する代わりに、過去のフィルタ結果をその後も使い回し続けることで、バッファとしてはひとつか少ない数しか用意しないけど、常に平均画像を求め続けているような計算になります。

最もシンプルにバッファ一個だけでやる場合、式で書くとこんな感じ。 X(n) がカメラから得られる最新フレームの画像で、Y(n)が、今回表示される絵。Y(n-1) が、前回表示された絵。

 {
Y(n) = a * X(n) + (1.0 - a) * Y(n-1)
}

Y(n-1) ももちろん上と同じ式で一回前のカメラフレームが係数をかけて足されて出来た画像でY(n-2)も…と続くので、過去のフレームが秘伝のたれのように残りながら足され続け、平均が求められている、という感じです。一回前の描画内容を覚えておくだけで良いので、コストが低いのがポイントです。

実装

上記IIRフィルタ的処理をウェブカムに対してやってみます。

手順としては以下の様な感じになります

  • 新しいカメラ入力と前回の表示内容をテクスチャにしたもの★をブレンドする
  • ブレンド結果をオフスクリーンレンダリングして、テクスチャ★化しておく
  • ブレンド結果を画面にレンダリングする

ここで、★がついている二つの画像が、メモリ上で同一のものである、と言うことが味噌になります。

Three.js でウェブカム画像を利用する

前記事をご参照下さい -> WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける - 自習室

オフスクリーンレンダリング

THREE.WebGLRenderTarget に対してシーンを書き込めば、その画像をテクスチャとして利用する事が出来ます。オフスクリーンレンダリングについては、下記記事様が勉強になります

今回の例では、ブレンドした結果を WebGLRenderTarget に書き込んでいます。

var rt1 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat }  );

// 中略
// ここで、最新のカメラ画像とまえのフレームの描画結果をブレンドした新しいテクスチャを作り、
// それをプレーンに貼って、シーン内に配置する

renderer.render(scene, camera, rt1); // 別途用意した、rt1というレンダーターゲット(フレームバッファを含む) に書き出す
renderer.render(scene, camera);      // 「標準フレームバッファ」つまり画面に書き出される。

ブレンド

Three.js リポジトリexamples/js/shaders に、多数のシェーダのサンプルが含まれています。今回はこの中から、 BlendShader.js を利用します。

videoMaterial = new THREE.ShaderMaterial(THREE.BlendShader);
videoMaterial.uniforms['tDiffuse1'].value = videoTexture; // カメラ画像で毎フレーム更新されるTextureオブジェクト
videoMaterial.uniforms['tDiffuse2'].value = null;         // ブレンドされるもの。初めは無し
videoMaterial.uniforms['mixRatio'].value = 0.0;           // y = x1 * (1-mixRatio) + x2 * mixRatio となります

var planeGeometry = new THREE.PlaneBufferGeometry( vidWidth, vidHeight, 1, 1 );
var plane = new THREE.Mesh( planeGeometry, videoMaterial );  // ブレンドした結果の絵をテクスチャとしてプレーンに描き込む
plane.position.z = 0;

scene.add(plane);  // シーン内に配置する

ちなみに、Three.js における ShaderMaterialの扱いかたについては、下記サイトが勉強になります。

起動時は、ブレンドする2枚目のテクスチャがnullになっていますが、mixRatioも0にしてあるので、1枚目のvideoTextureがそのまま出ることになります。2枚目のテクスチャに適切に画像を当てて、mixRatioを動かすと、ブレンドされます。

FBO ping-pong

IIR的に画像の平均を求め続けるには、前回の描画結果に今回のカメラ入力をブレンドして出力する必要があるのですが、WebGLでは、Aというテクスチャを使ってシーンを作ったあと、Aにrenderする、と言うのは出来ません。メモリに書き込むにつれて絵が変わる、なんてことが起きてしまうから、そりゃそうだ、という感じなのですが。

WebGLRenderTargetを2つ用意し、交互に利用する事で、それを回避します。

var rt1, rt2;
var rtSwitch = true;

// 中略

// 交互に利用する2つの RenderTarget を用意する。
rt1 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat }  );
rt2 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat }  );

毎フレーム、BlendShaderで利用するテクスチャと、オフスクリーンレンダリング対象のレンダーターゲットを切り替えていきます。

if(rtSwitch) renderer.render(scene, camera, rt2); // (フラグonの時)rt2に書き込む
else renderer.render(scene, camera, rt1);         // (フラグoffの時)rt1に書き込む
renderer.render(scene, camera);                   // 同じ内容を画面にも書き出す

if(rtSwitch) videoMaterial.uniforms['tDiffuse2'].value = rt2; // (フラグonの時)次のターンでは、rt2の内容をブレンドに使う
else videoMaterial.uniforms['tDiffuse2'].value = rt1;         // (フラグoffの時)次のターンでは、rt1の内容をブレンドに使う

rtSwitch = !rtSwitch;  // フラグを切り替える

ちなみに、これをミスって、シーン内に使っているテクスチャに書き込もうとすると、以下の様に怒られます

[.WebGLRenderingContext-07E445A0]GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.

さいごに

改めて、完成品はこちらです

今回の手法だと、背景に対してコントラストが高い動体は、焼きついたように長時間残ることもあります。たとえば下のような例。ラズパイのマークが、白い壁に染みついちゃってます(笑)

f:id:AMANE:20150420184533p:plain

今回の手法は、厳密に背景と動体を分離して消す、というよりは、「動体をうっすらとぼかすことでプライバシーを守りつつ、背景はほぼリアルタイムのものを撮影する」といった用途にむいていると思います。

焼き付きを抑えるには、「動体がない」瞬間を検知して、テクスチャを全部書き換えちゃう、とかすると良いかと思います。

改善案募集

もちっと綺麗に動体を消せる技術ご存じの方、是非とも教えていただきたいです!

(追記) 改善しました

焼き付きのようになってしまう問題を改善しました。追加で投稿していますので、ご参照下さい。

izmiz.hateblo.jp

GPGPU的な用法

FBOのping-pongは、こういった画像の保存という単純な用途以外にも、さまざまな可能性があります。たとえば、大量の点群の

  • 速度
  • 座標
  • 質量

などの「色」ではない情報をテクスチャ風の配列群に仕立ててシェーダに渡し、fragment shaderでガッと計算をした後に、それらを再度オフスクリーンレンダリングという形でテクスチャ的データに書き出して、次のフレームでも利用する、という用法があります。Three.jsの公式サンプルにも作例があります

シェーダでの計算結果を次のフレームで活用するために、FBO ping-pong が使われています。かなり複雑ですが、GPUの計算リソースを活用出来るので、ブラウザでも上記サンプルのようになにやらすごいことが出来ます

WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける

やりたいこと

f:id:AMANE:20150414222133p:plain

このブログで何度かにわたって、ブラウザとopenFrameworksを連携させ、oFで加工した映像をWebRTCで扱う、というシリーズをやっていますが、今回は、映像の加工もブラウザ内で完結させてみます。

CSS でもいろいろ出来る

ブラーや明るさ調整は、それぞれ -webkit-filter: blur(100px)-webkit-filter: brightness(0.1) など css としてブラウザで実装されており、非常に高速に動作します。*1

これ以外の、もう少し自分好みのことをしようとしたときも、WebGL の fragment shader を使うことで、やはりブラウザ内で完結させることが可能になります。グリッチとか、ドットとか、歪ませたりとか、そういうの。

そういえば、CSS Shader とかいうのもあったような気もしますが、遠い過去の話ですね。

完成品

こちらで動いています。PCにウェブカムをつないだ状態でこのページにアクセスし、カメラの利用に許可を与えて下さい。

環境

素のWebGLを自力で書くのはしんどいので、Three.js を使います。

動作確認は、以下の環境で行っています。

参考にさせていただいたサイト

特に、一つ目のサイトの、 Bad TV Shader for Three.js と言うサンプルのコードを見てみると、既存の動画ファイルに対してでは無くウェブカムに対してエフェクトを掛けるコードが、コメントアウトされた状態で書かれています。 今回の記事は、ほぼこの内容の焼き直しになります。

やってみる

ポイントだけ追っていきます。

ウェブカムの映像を取り込む

よくあるWebRTCのサンプルのように、 navigator.getUserMedia でストリームを取得し、それを <video> タグに流し込みます。このとき、 <video> タグそのもの(無加工の映像) は使わないので、 style="display:none" しておきます。

<!-- 表示されないのでどこにあっても良い -->
<video id="video" style="display:none"></video>
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;
window.URL = window.URL || window.webkitURL;

video = document.getElementById('video');
video.autoplay = true;  // これを外すと、スタート時の映像で停止します

var option = {
  video: { mandatory:{ minWidth: 1280 } }, // firefoxでは無効
  audio: false
}
// var option = {video: true};

navigator.getUserMedia(option,
  function(stream) { // for success case
    video.src = window.URL.createObjectURL(stream);  // videoタグにストリームを流し込む
  },
  function(err) { // for error case
    console.log(err);
  }
);

// カメラ画像のサイズを記録しておく。後で使う。
video.addEventListener('loadeddata', function() {
  // Chromeは問題無いが、Firefoxだと、'loadeddata' イベントでvideoWidthらが埋まっていないので、値が得られるまで待機
  (function getVideoResolution() {
    vidWidth = video.videoWidth;
    vidHeight = video.videoHeight;
    if(vidWidth != 0) {
      console.log("video width: " + vidWidth + " height: " + vidHeight);
      setup(); // カメラの映像が流れ始めたら、Three.js内の準備を始める
    } else {
      setTimeout(getVideoResolution, 250);
    }
  })();
});

<video> の映像を Three.js のテクスチャにし、平面なメッシュに貼り付ける

videoTexture = new THREE.Texture(video);  // なんとこれだけでテクスチャとして使える!
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBFormat;
videoMaterial = new THREE.MeshBasicMaterial({  // マテリアルにする。カメラ画像をそのまま使いたいので、ライティング等は無し。
  map: videoTexture
});

var planeGeometry = new THREE.PlaneGeometry( vidWidth, vidHeight, 1, 1 );
var plane = new THREE.Mesh( planeGeometry, videoMaterial );  // 平面に書き込む
plane.position.z = 0;
scene.add( plane );  // シーンに追加

Orthographic なカメラ

パースペクティブなカメラを使ってももちろん良いですが、オルソーなカメラを使うことで、簡単に全画面ぴったりに上記カメラ画像平面を貼り付けることが出来ます。

camera = new THREE.OrthographicCamera(vidWidth/-2, vidWidth/2, vidHeight/2, vidHeight/-2, 1, 2000);
camera.position.z = 500;  // カメラの near/far クリッピングと合わせて設定する

繰り返し描画する

まずは動画にエフェクトを掛けずに、そのまま描画してみます。

HTML の <video> タグのDOMが、描画出来る状態になっているか(つまり、新しいフレームが来ているか)を調べ、その状態になっていたら、 videoTexture を更新するようにしています。

function render() {
  if(video.readyState === video.HAVE_ENOUGH_DATA) {
    if(videoTexture) videoTexture.needsUpdate = true;  // ここがポイント
  }
  // loop
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
render();

取りあえずこれで、ただの <video> タグではなく、 Three.js のつくる <canvas> 内にmeshに貼られた画像として描画されます

EffectComposer

Three.js 内で扱えるようになってしまえばこっちのものです。Three.js で作り上げたシーンをカメラで見てレンダリングした結果に対しポストプロセスをかけていきます。

Three.js は標準でシャレーなポスプロシェーダをたくさん備えています。

公式サンプルの postprocessing/ ディレクトリもおもしろいです

EffectComposerの使い方はこちらで簡潔にまとめられています

自分のものにもかけてみる

DotShaderと RGBShift をかけてみます。

composer = new THREE.EffectComposer( renderer );
composer.addPass( new THREE.RenderPass( scene, camera ) );

var dotScreenEffect = new THREE.ShaderPass( THREE.DotScreenShader );
dotScreenEffect.uniforms[ 'scale' ].value = 2;
composer.addPass( dotScreenEffect );

var rgbShiftEffect = new THREE.ShaderPass( THREE.RGBShiftShader );
rgbShiftEffect.uniforms[ 'amount' ].value = 0.003;
composer.addPass(rgbShiftEffect);

rgbShiftEffect.renderToScreen = true;

// 中略
// 繰り返し描画するルーチンの中

// renderer.render(scene, camera);
composer.render();  // rendererではなく、エフェクトのチェーン後のcomposerが画面に描き出す

本来、ブラウザ(キャンバス)が書き込み先である renderer に対し、 EffectComposer オブジェクトに対して書き込め、と指示し、そこから EffectComposer がいくつかのエフェクトを経由した後、.renderToScreen 指定したところで画面に書き出します。

完成

動作しているものへのリンクを再掲します

最後に

今回はローカルのウェブカムの画像に対してエフェクトを掛けましたが、 対象は <video> タグに描かれている内容でさえあれば良いので、もちろん WebRTC で「受信した」 映像に対してエフェクトを掛けることも出来ます。これを使うことで、さまざまな画像エフェクトが出来るビデオチャットシステムも作ることが出来るようになります。

*1:webkitのコードを読んでみたところ、OpenCLで高速化しているようです https://trac.webkit.org/browser/trunk/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp

ofxLibWebsockets で oF とウェブブラウザの間でお話しする

はじめに

相変わらず、ブラウザとopenFrameworks 間で共同作業させるシリーズです。

以前こんなものを書きました。

これらの記事では、ブラウザとoF間にNode.jsのプログラムを挟み、socket.io と OSC を相互に変換することで、データのやりとりをしていました。もともと以下の理由でそういう構成にしていました。

  • ブラウザとnode.js間はsocket.ioでの通信が楽
  • node.jsとoF間では、OSCでの通信が楽
  • それを単につなげればよいので楽

このときの構成は下図のようになります

f:id:AMANE:20150407143947p:plain

しかしこの結果

  • 機能上はほぼ意味の無い node.js の変換コードを書かなければいけない。実装コスト増大

という問題も発生しました。

  • oFでやろうとしていることを別のプログラムでシミュレートする
  • 複数のブラウザやoFのプログラムとやりとりをする

などのケースを想定すると多段構成にする意味もありますが、一対一で通信をするだけなら、ほぼ意味はありません。従って、ブラウザとoFで直接お話しをさせたいです。

いろいろ試した結果、以下の構成が可能であることがわかりました。今回はその方法についてまとめます。

f:id:AMANE:20150407143959p:plain

非バイナリデータのみ

WebSocketでバイナリのやりとりも出来る筈ですが、本記事では非バイナリデータ…つまり、数値、文字、bool とそれらをオブジェクト化した物などをやりとりします。いずれバイナリも調査します。

この記事を追うとできるもの

oFの画面でマウスを動かすと、その座標をJSONとしてラップし、WebSocketを介してブラウザに送ります。ブラウザではJSONを復元して、オブジェクトとしてコンソールに出力します。

f:id:AMANE:20150408215316j:plain

完成品はこちらにアップしています

github.com

ofxLibWebsockets の基本

addon単体としてのドキュメントがほとんど無いため、使いこなしに少々苦労しました。サンプルを読んで探していくスタイル。

プロジェクトへの追加

oFプロジェクトへの追加の方法は、ofxLibWebsockets のgithubにまとめられています。こちらを参考に追加もしくはoFプロジェクトの作成を行ってください。

自分はWindows(VS2012) で試しているのですが、配布されている ofxLibWebsockets のオプションに間違いを見つけました。以下修正してください。

プロジェクトプロパティ > リンカー > 全般 > 追加のライブラリディレクトリ で、構成が Debug の際に、

..\..\..\addons\ofxLibwebsockets\libs\libwebsockets\lib\win32\Release` となっているものを
..\..\..\addons\ofxLibwebsockets\libs\libwebsockets\lib\win32\Debug に修正。 

oF側をサーバとしてセットアップ

ofxLibWebsockets のサンプル "example_server_echo" を参考に。最低限の構成は以下、だと思います

void ofApp::setup(){
    ofxLibwebsockets::ServerOptions options = ofxLibwebsockets::defaultServerOptions();
    options.port = 9092;
    options.bUseSSL = false; // you'll have to manually accept this self-signed cert if 'true'!
    bSetup = server.setup( options );

    // this adds your app as a listener for the server
    server.addListener(this);
}

メッセージの受け取り

こちらも、ofxLibWebsockets のサンプル "example_server_echo" を参考に。ただ受け取ったメッセージを標準出力します。

void ofApp::onMessage( ofxLibwebsockets::Event& args ){
    cout << "got message " << args.message << endl;
}

試しにブラウザから送ってみる

先に "example_server_echo" を起動してから、Chromeで適当なページを開き、developer tool のコンソール上で以下の様に順に打ち込みます

ws = new WebSocket('ws://localhost:9092'); // 接続
ws.send('hoge'); // 文字列を送る

一行目をenterした時点で、oFのコンソールには "new connection open" と表示されます。
更に二行目をenterすると、oFのコンソールに "got message hoge" を表示されます。

JSON形式でやりとりしてみる

ブラウザ側から送ってくるデータは、それが元々オブジェクトであった場合も、message = JSON.stringify(data) したのちに送り出し、ofxLibWebsockets で受け取ると、jsonとして復元されます。

逆に、ブラウザ側でJSON形式で受け取りたい場合は、oF側でJSON形式でまとめ上げたものを文字列として送り出し、それをdata = JSON.parse(message) することで、jsonとして復元されます。

図にまとめると下のようになります。

f:id:AMANE:20150407162026p:plain

ofxLibWebsockets は内部でjsoncpp というライブラリを利用しているので、json <-> 文字列 の変換が出来ます。新しいjson形式のデータ作成も可能です。

ブラウザからobjectデータを送り出し、oFで受け取るケース

// 送出側 (js)
var ws = new WebSocket('ws://localhost:9092/');
var pos = {'x':0, 'y':100};
var message = JSON.stringify(pos);
ws.send(message);
// 受け取り側 (cpp)
void ofApp::onMessage( ofxLibwebsockets::Event& args ){
    // args.messageには、 js側で message = JSON.stringify(pos) として作られた文字列"message" が入っている
    cout<<"got message "<<args.message<<endl;
    
    // 送られてきた args.message がjsonにパースできる形式である場合、
    // 自動的にパースされ args.json に Json::Value 形式で収められる
    if ( !args.json.isNull() ){
        cout << "New message: " + args.json.toStyledString() + " from " + args.conn.getClientName();
    }
}

oFでJSON形式にラップしたものをブラウザで受け取るケース

マウスを動かすと、最新の座標をWebSocketで送り出す、というもの

// 送出側 (cpp)
void ofApp::mouseMoved(int x, int y ){
    Json::FastWriter    writer;
    Json::Value     value;
    value["x"] = x;
    value["y"] = y;
    std::string message =  writer.write(value);
    server.send(message);
    cout << "mouseMoved : " << message << endl;
}
// 受け取り側 (js)
var ws = new WebSocket('ws://localhost:9092/');
ws.onmessage = function(event) {
  var data = JSON.parse(event.data); //event.dataは文字列。これをJSON形式にパース
  console.log(data);
}

JSONCPP の使い方

以下のページなどを参考にしました

スピードを上げる

ofxLibWebsocket (libwebsocket)では、WebSocketの送り出し受け取りを、専用のスレッドを立てて行っています。一つの処理をコンテキストとしてまとめ、それをキューに積んでいって、前から順に処理していきます。その辺りはこちらのページで勉強させて頂きました。

ofxLibWebsocketでは、そのスレッド処理に、何故か標準で 50ms の待機時間が挿入されているため、毎秒 20 回までしか通信の処理が出来ません。マウスの動きを伝えるには遅すぎます。ここを変更します。

// addons/ofxLibwebsockets/ofxLibWebsockets/Server.cpp 19行目
    Server::Server(){
        context = NULL;
        waitMillis = 10; //★ここ 50 -> 10 など。
        reactors.push_back(this);
        
        defaultOptions = defaultServerOptions();
    }

これで、最大100回/秒 通信できるようになります。

waitMillis を変更するための public な関数をServer.cpp か 親クラスの Reactor.cpp に追加しても良いかもしれません。

さいごに

oFで画像を作ってブラウザ内で利用する、と言ったことも出来るかもしれないので、調査してみます。

dropmark のビデオプレイリストの再生をループさせるChrome Extension を作った

動機

こんなことがしたい、と思いました

  • 余ってるPC + ディスプレイで、様々な動画を垂れ流しにしたい(会社でシェアしたい)
  • 極力簡単にプレイリストを管理したい
  • YouTube と Vimeo のリンクを貼るだけでOKみたいな感じがベスト
  • 全画面再生
  • ループ

イケてる動画をみんなであつめてそれを垂れ流しておくことで、ふとした瞬間に素敵な動画に出会えるたら良いなーというのが狙いです。

調査

公式

Vimeo だと、Likesなり Watch Laterなり Channel なりを、"Watch in Couch Mode" で再生開始すると、自動的に全画面 + ループになります。

f:id:AMANE:20150301201100p:plain

YouTubeだと、お気に入りなりプレイリストに、「繰り返す」ボタンがあります。これをONにしてから全画面再生すれば良い感じです。

f:id:AMANE:20150301201113p:plain

それぞれ良いのですが、会社で色んな人に登録してもらうとすると、両方のサイトにアップロードされている動画を扱いたくなります。

おしいやつ

Tumblrで動画を集めておくことはもちろん出来ますが、自動で再生していく機能はありません。

同じようなものがないかなーと探していたところ、dropmark というサービスの存在を知りました。

dropmark

自分も今回の目的以外で使ったことがないのでよくわかっていないのですが、いわゆるブックマークサービスです。その中の特徴的な機能として、ビデオのリンクを貼っておくと自動で再生してくれる機能、と言うのがありました。この記事で知りました

これで解決!と思ったのですが、ひじょーに惜しいことに、リストの最後まで行くと、そこで一覧画面に戻ってしまう仕様です。

そこで、Chrome Extensionを使って、ウェブサービスをハックしてみることにしました。

完成品と使い方

ぎっはぶ

ここに上げてあります

使い方

  • コードをPC内のどこかにおく
  • chromeの設定 > 拡張機能 > パッケージ化されていない拡張機能を読みこむ
  • フォルダを指定して開く

現状のコードは、すべてのウェブサイトに対して処理をしてしまうような物にになっているので、dropmarkを全画面で再生し続ける専用のPCを用意できる場合は良いのですが、もしご自身のPCで一時的に試してみたい、と言う場合は、利用しない場合は、拡張機能のリスト中で「有効」のチェックをはずしておくと良いかと思います。

f:id:AMANE:20150301205851p:plain

ついでに

YouTubeの再生を極力高画質で行いたいので、別途それ用のChrome-extension を入れます。この手の物はたくさんあるのですが、こいつはちゃんと動いていました

Vimeoの方は、ある程度大きくプレイヤーが開いたら、自動的に高画質になるので心配ありません。

設計(?)

主に、以下の三つのことをやっています。

  1. プレイリストの最後のビデオになったかの判定
  2. プレイリストの先頭のリンクを取得する
  3. 最後のビデオの次に、プレイリストの先頭のビデオを再生するようにハック

1. プレイリストの最後のビデオになったかの判定

これは、画面中の "Next Item" ボタンのハイパーリンクを見ることで判別出来ます。動画を指すURLではなく、動画一覧を指すURLだった場合は該当です。

f:id:AMANE:20150301202152p:plain

2. プレイリストの先頭のリンクを取得する

プレイリストのHTMLの中身を見て動画の一個目を指すURLを取得すれば良いのですが、実際は「最後の動画を見ている最中に」異なるページのHTMLを参照することになるので、少しトリッキーなことをします。

  1. jQuery<div> 要素を作ってその中にプレイリストのHTMLを丸っと挿入する
  2. その中から該当するHTML要素を見つける
$.get(playlisturl, function(data) {
  var playlisthtml = $('<div>').html(data);

jQueryのgetコマンドで動画一覧のページのHTMLを引っこ抜いてきて、それを <div> タグの中に展開することで、jQueryで解析可能なものにします。ここでこの <div>.append とかしなければ、その要素は表示されたりはしません。あくまでHTMLデータの中身を解析するためだけに一時的に存在するものになります。

プレイリストのhtmlを取得できたら、その中から、お目当ての「先頭の動画」のURLを見つけてきます

var firsturl = playlisthtml.find('.item-preview:first-child').attr('href');

3. 最後のビデオの次に、プレイリストの先頭のビデオを再生するようにハック

1.で、最後のビデオか判定するのに使った "Next Item" の href に、先ほど見つけてきた先頭動画のURLを代入すれば完了です。

まじで?

「えっ…?」って感じでした。次の動画への飛ばし方はちゃんと本家のjs読めばわかるかも知らん、気合い入れて解析してやろう!と意気込んで始めた直後、まぁそんなわけはあるまいと思いつつもものは試しでURL書き換えてやろーと適当こいたところ、出来てしまった、という感じです。結果オーライ、簡単で良かった。やったー

さいごに

と、説明しましたとおり、dropmarkの現状の実装を、偶然も織り交ぜつつ勝手にハックした感じですので、動作保証など一切できません。万が一ご入り用の方がいらっしゃった場合は十分ご理解の上ご利用ください。

3時間くらいで出来て良かったです。会社で運用してみます。

MOVERIOのお出かけ中利用シーン難しさランキング

前説

engadget さんにこんな記事があって

japanese.engadget.com

おっ、と思って脊髄反射で申し込んだら、当選したので、行ってきました。そのときのイベントレポート記事が上がっております。

写真のどこかに、私も写ってますね。私もEngadgetさんの記事が上がる前にレポートを書いておいたらリンクを貼ってもらえたはずでしたので、悔やまれます。

そう、そういうわけで EPSON 様から MOVERIO BT-200AV をお借りしてきました。

半年貸してやるから、7本ブログ書け

とのことです。全力で楽しんでいきましょう。開発にもトライしてみる予定です。

f:id:AMANE:20150317195651j:plain

うまく撮れました。

MOVERIOについて知る

MOVERIOは、公式には、以下の様なコンセプトの商品のようです。以下公式サイトから抜粋

  • 大画面を楽しむ
  • いつでもどこでもパーソナルシアター
  • レコーダーやスマートフォンの映像を楽しむ
  • 高画質・高音質のモバイルビューアー
  • 誰にも邪魔されず、自分だけで楽しめる
  • 好きな姿勢で、好きな場所で
  • 飲食しながらでも楽しめる

などなど。要するに、映像を見る装置です。私もBT-200 を使ってアニメを数本見てみましたが、思ったよりは普通に見られます。

一方で、こうもうたわれています。

  • Androidを採用し、カメラ、GPS機能、各種センサーを搭載。機能を活用した対応アプリで楽しみ方が広がります。
  • モベリオ用のオリジナルアプリが作れる
  • Webブラウジングをモベリオで楽しむ

Androidなので、この眼鏡スタイルを活かしたアプリ体験はいろいろ探索していけそうです。自分もいくつか作ってみたいアイデアを考え中ですので、このブログで報告できたら良いなと思います。

今回は 開発ネタは置いておいて 、メインの「映像を見る」機能についてちょっと考えてみます。どんな時に使えると嬉しいか。そして、それは実際可能なのか。

MOVERIOのお出かけ中利用シーン難しさランキング

考えてみました。難しそうだなーと思った順。

  1. 通勤電車
  2. ショッピングモール
  3. ディズニーランド
  4. 飲み屋
  5. ファストフード店
  6. 喫茶店(特にスタバ)
  7. お散歩中
  8. 公園・河川敷
  9. ジム
  10. 大学構内
  11. 野球場・サッカー場・競馬場
  12. 飛行機・新幹線・長距離バス

前提

前提として、「出先で映像を見る」という行為をそもそもしそうなシーンを想定しています。言い始めればもっといくらでも候補を挙げられそうですが、

  • コンテンツを見る、という行為をスマホ等でもそもそもしないところは除外
  • 論理的には上記に含まれますが、「1人で映像を見る」という行為が不適切な場合も除外

としています。MOVERIOのまっとうな使い方を考えてみよう、と言うことです。

解説

簡単そうな順に簡単に考察をしてきます

12. 飛行機・新幹線・長距離バス

f:id:AMANE:20150330145821j:plain

公式もおすすめの利用法。手にタブレットを持って持ち上げ続けたり、首を下に向けながら映像を見るのは結構しんどい。自由な姿勢で両手を空けられるモベリオ的には相当妥当な利用法!

車の中でタブレットで動画を見ていると弱い人だと車酔いしますが、「モベリオだったら車酔いしない!」みたいな展開があったりしないでしょうか。これは要調査です。

11. 野球場・サッカー場・競馬場

f:id:AMANE:20150330145911j:plain

ラジオを聞きながら野球や競馬を見る、というのは良くある話ですが、それのTV中継版。対象物が遠距離で焦点のズレも問題なさそう!

と思ったのですが、TV放送を見る方法ってあるんでしたっけ…?

まぁその、待ち時間に見るとか…

10. 大学構内

f:id:AMANE:20150330145919j:plain

ジャンル的には公園・河川敷に近いですが、大学内、特に理工系のキャンパスなら 「実験かな」と思われるので普通にスルーされること間違いなし!

9. ジム

f:id:AMANE:20150330145932j:plain

バイクをこぎながらの動画閲覧は鉄板ユースケースでしょう。ジムによってはスタッフさんに怒られるかもしれないので、真摯な態度で(EPSON社員になったつもりで!)モベリオの良さをアピールしていきましょう。タブレットで動画見てる人なんていくらでもいますしね。 最新のスポーツマンはこれ!くらいの堂々たる態度 を見せられるかがポイントになりそうです。

8. 公園・河川敷

f:id:AMANE:20150330145940j:plain

この辺りから、少し攻めたユースケースになります。そもそも、公園でひとりで動画を見るシーンが余り思いつきませんが、「変な人って思われないかな??」と少しドキドキしながら眼鏡を外で掛ける、わりとソフトな入門編という感じでしょう。たまに通りかかる人に、全く気がつかれないか、変わったサングラス?と思われるか、社交的なおじさんに話しかけられるか、くらいで済みそうですね!

7. お散歩中

f:id:AMANE:20150330145946j:plain

公園や河川敷でこそこそ見るのにスリルを感じなくなったら、次は出歩いてみるのも良さそうです。ただしおそらく結構危険ですので、がっつり映像を楽しみたい方にはおすすめできません。 普段からアニメを2本同時見したり、プログラミングしながらアニメを見ているような玄人の方向け の用法です。

家でビール飲みながらアニメ見てたらお腹に肉が付く一方ですので、お散歩しながら健康にアニメ鑑賞は、2015年のニューオタクスタイルとしてアピールしていけるかもしれません

6. 喫茶店(特にスタバ)

f:id:AMANE:20150330145956j:plain

Macbook

スタバにおけるドヤリングはもはや社会現象とも言えるかと思います。たとえTwitterを見ているだけでも、Mac in starbucks is very おしゃれ。ただのドヤリを超える新世代のドヤリとして、 モベリオドヤリング を提唱してきましょう。店員さんと仲良くなれるかも!

5. ファストフード店

f:id:AMANE:20150330150004j:plain

公式でも結構頭の方でおすすめしている、食事をしながらの動画鑑賞。両手がふさがっていても動画が見れる!モベリオは 録り溜めた深夜アニメの消化に追われる現代人には欠かせないデバイス と言えそうです。忙しいオタリーマンなあなた(誰)、ためしに、殺伐としている吉野家で、黙々と大盛つゆだくをかっこみながら幸腹グラフィティを見る、というのはいかがでしょうか。

4. 飲み屋

f:id:AMANE:20150330150027j:plain

今回のモニターで借りている人みんなで、塚田農場で MOVERIOオフ会 なんかどうでしょうか。良く訓練された浴衣ギャルの塚田農場店員は、きっと笑顔で「えーそれなんですかぁー?」と聞いてくれるに違いありません。 これぞ商機。

3. ディズニーランド

f:id:AMANE:20150330150038j:plain

プーさんのハニーハント、2時間待ち!みたいな時こそ、モベリオです。初めの1時間くらいはなんとかたわいもない話で持ちますが、次第に間が持たなくなってきます。こんな時こそモベリオ。なんなら2人で互いにモベリオ。サングラス代わりにもなってちょうど良い感じです。アニメを一話見終わる度に感想を語り合えば、2時間でも3時間でも待てそうなものです。

ディズニーランドはリア充の聖地かと思いきや、コアなネズミファン達の強烈なオタク感はすさまじいものがあります。ちょっと変わった眼鏡を掛けているくらいでとやかく言われる筋合いはありません。どんどんかけましょう。

2. ショッピングモール

f:id:AMANE:20150330150045j:plain

いつまでも終わらない奥さんのお買い物に付き合うのに疲れたら、最後の力を振り絞り最高の笑顔で理解ある夫を演じつつ「終わったら呼んでー」と奥様を送り出し、フードコートに腰を下ろしてモベリオを取り出します。

しかしそこは大量の親子連れであふれかえっています。

通りかかった子供「なんかかっこいいめがねだよー」
そのお母さん「しっ、見るんじゃありません」

なんて、ほほえましい風景もあるかもしれませんね。

1. 通勤電車

f:id:AMANE:20150330150051j:plain

個人的に最強のユースケースがこれ。

満員電車、周りはおっさんだらけ、顔近いっす、あなたの後頭部なんか見たくないっす。いやおねえさん、別にあなたのこと見つめてないっす偶然そちらしか向けないだけです! 手が挙げられなくてパズドラもできねぇ! なんて日常も、モベリオで一変します。

乗り込む前に再生開始。満員電車の不快な30分が快適アニメタイムに!

というシナリオなのですが、同時に変な人と思われるリスクもひじょーに高い。痴漢えん罪怖いです。あまり世の中に普及しているデバイスというわけでもないので、モベリオかけてるだけで「この人ちかんです!」と言われるリスクが無いわけではありません。この季節、花粉症もあってマスクをしてたりもしますので、より一層怪しいです。

女性ならそんな心配もありませんので、ぜひ試してみていただきたいです。

最後に

途中のディズニーやショッピングモールは冗談として、通勤電車ユースケースは(マスクが取れた頃に)勇気を出してトライしてみたいです。かなり実益があると思われる用途が同時に非常に危険度が高い、というのはなんともモベリオという商品の難しさを表している気がしますが、そこんとこぶち破っていきたいです。

最初に紹介したブロガーイベントでは、エプソンの中の人が電車の中で使っている様子をちゃんと動画にしてレポートしてたりして、がんばるなぁと思ったのですが、そういえば割と美人の女性の社員さんでした(笑)

MOVERIOは電車の中でも使える!(※ただし美人女性に限る) ってわけですね。

冗談はほどほどにして

開発ネタと並行して、「こんなところで使ってみた」レポートも、ブログとして書いていこうと思います。

フリー素材最高

今回はじめて自分のブログでフリー写真素材を使ってみました。フリー素材を使った記事感が見事に出てますね!

ブラウザ上の開発環境 "CLOUD PEBBLE" で Pebble アプリの開発をしてみた

はじめに

Kick Starter で、とんでもない額の投資を得て話題になっています。$500,000- の目標に対し、3月8日午前時点で $16,816,735- の投資を集めています。達成率で言うと3000% です。

公式:

追って発表された、Pebble Time Steel と Smartstrapsについての記事:

欲しい

Apple Watchも話題になっています。むろん、表現力やアプリの作り込み、コミュニティの大きさは圧倒的だと思われます。一方で、電池もちはだいじょうぶか?と心配されています。 Pebble Timeは e-paper のおかげで(カラーにもなったのに!) 一度の充電で7日間*1使える、という点で、Android Wear のプロダクト等と比較してもかなり特殊な立ち位置にいます。

自分はApple Watchも結局買ってしまうかもしれませんが、いつもそこにいて、ちらっと見る、という従来の時計に近い使い方が出来る(と期待できる)Pebble Time を結構楽しみにしています。

SDK

カラー画面に対応したSDKが公開されました。まだ現物はありませんが、おもしろそうなので開発してみようと思いました。Android Wear や Apple Watch ほどは流行らないかと思いますが、長時間駆動可能という特性を活かした体験を作れると良いなぁと思います。

開発者向けの情報はこちらのサイトにまとめられています。既存Pebble向けのSDK Ver2系と、カラーに対応したSDK Ver3が入手できます。それらのインストールガイド、APIのドキュメントや、チュートリアルもまとめられています。

サイトがお洒落です。

Mac の開発環境 (割愛)

チュートリアルをやる分には、Macの開発環境と CLOUD PEBBLE での開発には差が無いようです。もちろん最終的に完成したアプリをPebble本体にインストールする際にはMac上に開発環境を構築する必要がありますが、手始めにちょっと触ってみたいという今回みたいな段階では、ビルド、エミュレーション出来る環境が0ステップで手に入るのはかなりありがたいです。

また、アプリの設定系の .json ファイルを、いちいち手書きしていくのではなく、埋めるべきところをGUIで埋めていく形式で書けるのも、CLOUD PEBBLE の優れているところです。手始めには、CLOUD PEBBLEを使うのが良いでしょう。

と言うわけで実際は一応Macの開発環境も作ったのですが、今回は割愛いたします。

CLOUD PEBBLE

ブラウザ上で動くPEBBLE のIDE + シミュレーション環境、CLOUD PEBBLE ですが、少しワークフローが特殊だったので、簡単にまとめておきます。とりあえず、アカウントを作成してログインして下さい。

プロジェクトの作り方

主に三つあります

  1. テンプレートから作成する
  2. ローカルからzipでインポートする
  3. GitHubからインポートする
1. テンプレートから作成する

PROJECTSタブ > CREATE で新しいプロジェクトを作成します

f:id:AMANE:20150308125227p:plain

この状態だと、SDK2.0 準拠のプロジェクトも選択出来ます。SDK2.0準拠の場合は、C言語だけではなく、 simply.js や pebble.js といったjsの方言での開発も可能です

f:id:AMANE:20150308125417p:plain:w550

が、ここでは SDK3.0を使いたいのでそちらをえらびます。すると、Project Typeはえらべなくなり、C言語での開発のみとなります。今後アップデートされると、js系も使える様になるのかもしれません。

f:id:AMANE:20150308125510p:plain:w550

ここでえらべるテンプレートは、基本的にSDK2系の頃のサンプルのようです。基本を抑えるにはこのサンプルを色々読んでみると良さそうです。

2. ローカルからzipでインポートする

既にローカルにプロジェクトがある場合は、それをzipにまとめてアップロードできます。

先に1. の手順で作ったプロジェクトを、zip でDLすると、必要なファイルとディレクトリ構成がわかるので、それがちゃんとパックされた物をアップロードすればOKです。チュートリアルをCLOUD PEBBLEでやる分には、zipでインポートをする事は無いと思います。

3. GitHub からインポートする

SDK3系のサンプルは、GitHub上に上がっています。

このページ中のスクリーンショットをクリックすると、それぞれのプロジェクトが上げられているgithubに飛びます。これらをCLOUD PEBBLEに取り込むことが出来ます。下図のように適当な名前をつけ、対象リポジトリのアドレスを指定し、IMPORT すれば、CLOUD PEBBLE上で扱えるようになります。

f:id:AMANE:20150308142800p:plain:w550

また、先にGitHub上で Fork して自分のプロジェクトとした物を、CLOUD PEBBLE にインポートして、その際に "USE AS GIT REMOTE" にチェックを入れておくと、CLOUD PEBBLE上で編集した結果を、GitHub上に再度Push出来るようになります。

f:id:AMANE:20150308144659p:plain:w550

f:id:AMANE:20150308144913p:plain

さすがにCLOUD PEBBLE上でローカルコミットを作る機能は無いようで、毎回origin/masterに対しpushする形となります。あらかじめ GitHub 上でブランチを作っておいて、そのブランチをCLOUD PEBBLE上にpullし、編集するようにすれば良いでしょう。

GitHubとの連携

上記3で説明しましたように、プロジェクトをGitHubからインポートした場合は、そのGitHubリポジトリをそのままリモートリポジトリとして運用することが出来ますが、1や2の手順でCLOUD PEBBLE上のプロジェクトとした物については、後からGitHubに突っ込むことは出来ないようです。もし既存のプロジェクトをどうしてもGitHubで管理したい場合は、一度プロジェクトをzipとしてDLして、それをGitHubに突っ込み、再度3.の手順でCLOUD PEBBLEに取り込むと良いかと思います。

ビルド、実行

ビルドの方法は2種類有ります

  • COMPILATION > RUN BUILD して、INSTALL ON BASALT
  • コードの画面で、右上の緑色の "Save, Build, Install and Run" ボタンを押す

f:id:AMANE:20150308151807p:plain

f:id:AMANE:20150308151815p:plain

左上のPebbleのウィンドウ内にアプリが表示されれば成功です

f:id:AMANE:20150308151958p:plain:w600

操作法

Pebbleウィンドウの周囲四つのボタンを、マウスクリックで押すことが出来ます。

ログの出し方・見方

C言語の場合 APP_LOG() マクロを利用します

 APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!");

js の場合、 console.log() 関数を利用します

console.log("Temperature is " + temperature);

ログの見方は2種類有ります。

  • インストール直後のモーダルウィンドウで、 "VIEW LOGS" をクリックする
  • COMPILATION > VIEW APP LOGS をクリックする

f:id:AMANE:20150308152941p:plain:w550 f:id:AMANE:20150308152951p:plain

これで、ログを見ることが出来ます f:id:AMANE:20150308153210p:plain:w600

[PHONE] と書いてある行は、PebbleKit.js 内で console.log() で出力された内容です。 [INFO][DEBUG] は、Cのファイル中から、APP_LOG() で出力された内容です。APP_LOG_LEVELについては、こちらを参照

チュートリアルを流してみる (CLOUD PEBBLE)

チュートリアルは上から順に通していけば良いと思いますが、数カ所の要点だけメモしておきます

チュートリアルのコードは、チュートリアル本文中のリンクから、CLOUD PEBBLEのプロジェクトとしてインポートできる

f:id:AMANE:20150308162419p:plain

上の図の様なリンクがあったら、そこを押すと、その時点のチュートリアルのプロジェクトをCLOUD PEBBLEに取り込むことが出来ます。これをやりながらチュートリアルを進めると良いと思います。

FONTや画像の取り込み方のちょっとしたコツ

ぽつぽつ引っかかったので。

ひとつの .ttf ファイルに対し複数のサイズでフォントを定義する

フォントは、取り込んだひとつの.ttfファイルに対し、複数のサイズで定義しておくことが出来ます。複数定義されていると、RESOURCES のなかのフォントファイルにマウスオーバーすることで、定義されているフォントを確認出来ます。

f:id:AMANE:20150308163738p:plain

サイズを追加するには、.ttf ファイルを選択して、一番下までスクロールし "ADD ANOTHRE FONT" して、"IDENTIFIER" の最後の _24 みたいなサイズの数値を、使いたいフォントサイズに変更して SAVE します。

.c コード中での画像リソースへのアクセスの仕方

画像ファイルをアップロードすると、下の画面のように表示されますが、ここに出ている "IDENTIFIER" の文字列「そのもの」では、.c コード上で扱う事が出来ません。なぜかそんな仕様です。

f:id:AMANE:20150308164714p:plain:w600

この画面に表示されている "IDENTIFIER" 文字列の先頭に、 "RESOURCE_ID_" とつける必要があります。たとえばこの場合は

  s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND);

で、使える様になります。なぜかそんな仕様です。気をつけましょう。

AppMessage の使い方

スマホ側で動くPebbleKit.jsと、Pebble本体で動く .c 言語間でデータのやりとりをする AppMessage ですが、少し手続が煩雑でした。

スマホ側(JS)
1. {key: value} のオブジェクトを作る
2. SETTINGS で、1. で作ったオブジェクトに含まれる key を登録する
3. Pebble.sendAppMessage(object, ... で送り出す

Pebble側(C)
4. static void inbox_received_callback(DictionaryIterator *iterator 関数で
    DictionaryIterator インスタンスに格納された状態で受け取る
5. Tuple *t = dict_read_first(iterator); でオブジェクトのメンバの一つ目を読み出す
6. t->key で、データの種類を調べて、 t->value でデータを読み出す

サンプルの場合、 1と3 で以下の様にメンバをオブジェクトに突っ込んで送ろうとします

var dictionary = {
    "KEY_TEMPERATURE": temperature,
    "KEY_CONDITIONS": conditions
};

// Send to Pebble
Pebble.sendAppMessage(dictionary,

これを、ちゃんとC側で受け取れる様にするには、SETTINGSの画面で、2のステップで以下の様な登録をしておく必要があります

f:id:AMANE:20150308221257p:plain

また、6 で t->key としてデータのキーを調べますが、このときに参照されるのは、 KEY_TEMPERATURE といった文字列ではなく、2 で登録した Key ID (0とか1とか)の方ですので注意が必要です。

さいごに

一通りチュートリアルを通すことで、CLOUD PEBBLEの使い方と、コーディングの仕方がなんとなーくわかりました。これをベースに、watchface(時計のテーマ) や watchapp(Pebbleのアプリ) を作っていこうと思います。

ちょうど、ustwo が "Watch Face Design Guidelines" というのを出していたり、明後日が Apple Watch の発表(おそらく)だったり、またスマートウォッチ界隈が動き始めているので、その辺りを参考にしながら、自分なりのスマートウォッチのあり方を模索してみたいです。

*1:Pebble Time Steelに至っては10日間!

Kinect, OpenCV, openFrameworks のカラー画像を相互に変換する

はじめに

以前の記事で KinectColorFrameReader から得た画像を ofImage 形式に変換して描画したりしましたが、いろいろ調べてみると、Kinect の画像形式から直接 ofImage に変換している例は意外と少ないようでした。そりゃまぁカラー画像をそのまま取得するだけだったらわざわざKinect使わんわい、という話だと思います。

それで、OpenCV も一緒に利用するケースを考えてみました。

f:id:AMANE:20150209231053p:plain

この三者間を画像データが行き来出来れば、柔軟にプログラムが出来そうです。

oF 内で OpenCV を利用する方法について

先日の記事ではoFでKinect for Windows SDK 2.0 を使う手法について調査検討しましたが、OpenCVについても同様に調査します。

大まかには、以下の方法があると思います。

  • 公式の ofxOpenCV を使う
  • kylemcdonald/ofxCv を使う
  • OpenCV を直接叩く
公式の ofxOpenCV を使う

OpenCVのバージョンは2.3.1です。ofxCvColorimage など oF での編集や描画がしやすいようアレンジされた画像クラスが利用できるのは便利です。公式のアドオンですが、IplImageを使っている箇所があったりするあたりはイケてません。そしてちょっと古い。

kylemcdonald/ofxCv を使う

内部で ofxOpenCV のアドレスを見ているので、利用しているのは ofxOpenCV と同様に 2.3.1 です。toCv() toOf() という多目的の関数があり、以下の様な変換が可能です。これは非常に便利です。変換可能な組を一部抜粋します。

- ofVec2(3)f --- Point2(3)f
- ofImage --- cv::Mat
- ofRectangle --- cv::Rect

詳細はコードをご参考 ofxCv/Utilities.h at master · kylemcdonald/ofxCv · GitHub

OpenCVはちょっと古いのですが、一方でコード中で ofDraw** という openFrameworks 0.9 系で採用される予定の記述があったりもし、いくつかのサンプルがビルドできませんでした。これの修正も結構大変そうです。

OpenCV を直接叩く

ofxAddons では無いので導入は若干面倒ですが、最新の機能を全部使える様になります。ただし、画像のデータなど、自分で変換する必要があります。

どうするか

ofxCv が素直に使えれば良かったのですが、先述の通りの問題があったりするのが玉に瑕で、Kinect for Windows SDK 2.0 を直接叩くのと同様の理屈で OpenCV も直接使えば良いじゃん、という結論になりました。OpenCV の生コードを書くのが何だかんだ楽です。

ところで、ofxCv はその目標に以下の様な物を掲げています。

Provide clean implementations of all functions in order to provide a stepping stone to direct OpenCV use.

と言う風に素のOpenCVも書けるような仕様になっているらしいので、ちゃんと ofxCv がまとまり oF も 0.9 に上がった暁には、 ofxCv を利用するのも良いかもしれません。

環境

openFrameworksv0.8.4 を Visual Studio Community 2013 で使う方法については、先日の記事をご参照ください

また、Visual StudioOpenCV を用いて開発する方法については、下記ブログなどをご参照ください

完成品はあげてあります

解説

ウィンドウが複数開いて大量に絵が出てきますが、下図の様な変換を行っています。

f:id:AMANE:20150209231104p:plain

以下ポイントだけ説明いたします ofImage と cv::Mat の相互変換については、先述の Kyle先生の ofxCv から toCv toOf 関数の実装を参考にさせていただいております。

ofImage -> cv::Mat

cv::Matコンストラクタに、配列の中身になる実データを指定出来るものがあるので、ofImageから実データを引っこ抜いて利用します。

// ofImage ofColor; ... ofVideoGrabber からデータを引っこ抜いたモノ
ofColor.allocate(CAM_WIDTH, CAM_HEIGHT, OF_IMAGE_COLOR);
cv::Mat cvColor = cv::Mat(CAM_HEIGHT, CAM_WIDTH, CV_8UC3, ofColor.getPixels());

cv::Mat のドキュメントによると

data – Pointer to the user data. Matrix constructors that take data and step parameters do not allocate matrix data. Instead, they just initialize the matrix header that points to the specified data, which means that no data is copied. This operation is very efficient and can be used to process external data using OpenCV functions. The external data is not automatically deallocated, so you should take care of it.

ということで、cv::Mat は実際は ofImage の確保しているデータを直接参照します。メモリの無駄がない、と言うことです。

また、ofImage は標準で RGB 配列ですが、 OpenCV では BGR 配列が標準ですので、変換が必要です

// #include <opencv2/imgproc/imgproc.hpp>
cvtColor(cvColor, cvColor, CV_RGB2BGR);

cv::Mat -> ofImage

// ofImage ofCropped;
ofCropped.allocate(CROPPED_WIDTH, CROPPED_HEIGHT, OF_IMAGE_COLOR); 
ofCropped.setFromPixels(cvCropped.ptr(), cvCropped.cols, cvCropped.rows, OF_IMAGE_COLOR, false);

cv 側でROIを決めてクロップした画像の生データ、のポインタを教えて sestFromPixels 関数で ofImage を作ることが出来ます。 四つ目の引数を false とすることで、BGR配列のデータでも直接 ofImage にRGB配列で突っ込むことが出来ます。

(Kinect) ColorFrame -> cv::Mat

// unsigned char* bufForColorFromKinect;
int nBufferSize = KINECT_CAM_WIDTH * KINECT_CAM_HEIGHT * 4;
bufForColorFromKinect = new unsigned char[nBufferSize];
hr = colorFrame->CopyConvertedFrameDataToArray(nBufferSize, bufForColorFromKinect, ColorImageFormat_Bgra);
cv::Mat cvColorFromK = cv::Mat(KINECT_CAM_HEIGHT, KINECT_CAM_WIDTH, CV_8UC4, bufForColorFromKinect);

Kinect画像の生データをバッファにいったんコピーして、それを使って先ほどと同様に cv::Mat() を作成します。 CopyCOnvertedFrameDataToArray() 関数で、ターゲットとなるフォーマットを指定するのがポイントです。cv::Mat のアルファ付きカラー画像は BGRAが標準です。

(Kinect) ColorFrame -> ofImage

// ofImage ofColorFromK;
ofColorFromK.allocate(KINECT_CAM_WIDTH, KINECT_CAM_HEIGHT, OF_IMAGE_COLOR_ALPHA);
int nBufferSize = KINECT_CAM_WIDTH * KINECT_CAM_HEIGHT * 4;
unsigned char* data = ofColorFromK.getPixels();
hr = colorFrame->CopyConvertedFrameDataToArray(nBufferSize, data, ColorImageFormat_Rgba);
ofColorFromK.update();

先ほどと同様に ColorFrame からデータを変換しつつバッファにコピーしますが、今回は ofImage ofColorFromK を事前に確保しているので、書き込む先の実データのポインタを直接指定できます。また、ofImage では色のフォーマットは RGBA になります。

最後の update() を忘れずに。

cv::Mat、ofImageから(Kinect)ColorFrameに戻す

Kinectの画像形式に戻すことは無い気がするので、ここでは割愛いたします。

さいごに

グレーの画像だったり、Depthなどビット深度の異なる画像も、CVの方で CV_8UC3 等と指定している画像のデータ形式を合わせることで対応出来ると思います。

少々面倒なところもありますが、ofxCv を使う場合より現時点では汎用性が高いと思います。活用していきます。