[AfterEffects Script]コンポジションマーカーの情報を取得

最近は仕事の片手間に(というか仕事で使うためにも)ofxAEという、AfterEffectsで作ったアニメーションをopenFrameworksで動かすためのライブラリを作っています。
(Contributor超募集してます!)

作ってる目的の一つにはデザイナとプログラマの分業がよりうまくいくようにするというものがあるので、
そのためにデザイナが書ける簡易スクリプトみたいなものを実装したい。
例えばあるフレームから別のフレームにジャンプしたり、ループ範囲を指定したり、ある条件でテクスチャを変更したり、など。

時間軸上に配置できる、長さを持つ、文字が入力できる、といった点からコンポジションマーカーがまさにうってつけなんだけど・・・

なんとAfterEffectsのスクリプトではコンポジションマーカーにはアクセス出来ないらしい。なんてこった。
After Effects Script Reference - MarkerValue Object

結論から言うと、できました。
もちろん素直な方法ではないですが、そんなに行儀の悪い方法でもないと思います。

  • スクリプトからレイヤーマーカーにはアクセスできる。
  • コンポジションを選択した状態で「ファイル」->「複数アイテムから新規コンポジション」を使うとマーカーの情報を保ったままレイヤーとして新規コンポジションに配置される。
    (CompItem.add(CompItem)ではマーカーの情報が落ちてしまう)
  • よって、対象のコンポジションを選択状態にして「複数アイテムから新規コンポジション」を実行し、そこに配置されたレイヤーからマーカーを取得すればよい。

というわけでできたコードがこちらです。

/*
AfterEffectsでコンポジションマーカーを取得するスクリプト
2013.10.31 by nariakiiwatani
MIT License - http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license
*/

// マーカーの情報を取得したいコンポジションを渡す
function getCompMarker(comp) {

// プロジェクトに含まれるコンポジションのリストを取得
function getCompAll(proj) {
	var ret = [];
	for(var i = 1; i <= proj.numItems; ++i) {
		if(proj.item(i) instanceof CompItem) {
			ret.push(proj.item(i));
		}
	}
	return ret;
}
// 配列の差分を取得
function getArrayDiff(a, b) {
	var ret = [];
	for(var _a in a) {
		var found = false;
		for(var _b in b) {
			if(a[_a] === b[_b]) {
				found = true;
				break;
			}
		}
		if(!found) {
			ret.push(a[_a]);
		}
	}
	var tmp = a;
	a = b;
	b = tmp;
	for(var _a in a) {
		var found = false;
		for(var _b in b) {
			if(a[_a] === b[_b]) {
				found = true;
				break;
			}
		}
		if(!found) {
			ret.push(a[_a]);
		}
	}
	return ret;
}

// プロジェクトウィンドウの選択情報を操作するので、操作前の状態を保存しておく
var selected = app.project.selection;
var selection = [];
for(var i = 0; i < selected.length; ++i) {
	selection.push(selected[i].selected);
	selected[i].selected = false;
}

// ここから処理の本体
comp.selected = true;	// 書き出し対象のコンポジションを選択状態にする
// 「複数アイテムから新規コンポジション」はプロジェクトウィンドウにフォーカスしていないと使えないので強制的にアクティブにする
app.project.showWindow(false);
app.project.showWindow(true);

var allComps = getCompAll(app.project);	// コマンド実行前のコンポジションのリスト
app.executeCommand(2796);	// 「複数アイテムから新規コンポジション」を実行
var added = getArrayDiff(getCompAll(app.project), allComps)[0];	// コマンド実行によって追加されたコンポジションを探す
var marker = added.layer(1).marker;	// これが欲しかったマーカー!

// ここから後片付け
added.remove();	// 増やしたコンポジションを削除
comp.selected = false;	// 選択状態を解除
for(var i = 0; i < selected.length; ++i) {	// スクリプト実行前の選択状態に戻す
	selected[i].selected = selection[i];
}
// マーカー情報を返す
return marker;
}

getCompMarkerに取得したいCompItemを渡してやればマーカーのPropertyObjectが返ってきます。

これで俄然ofxAEの開発が楽しくなってきました。

参考サイト。ありがとうございます!

AEP Project – CommandID
After Effects Script Reference

タマふぃるた~

昨日は日曜日だったのでこんなものを作りました。

ビデオカメラからの入力画像をリアルタイム解析して、ある程度丸いものが見つかったらパカっと開けて中でふにふに動いてくれます。

以下技術的な話です。

やってることは至って単純で、OpenCvでカメラ入力画像から輪郭抽出をして、その中で円形に近い領域に対して上半分のピクセルを上にずらして、空いたところにアニメーションを表示しているものです。

ある領域が円形に近いかどうかの判定は以下の2つの条件で行いました。

  • 領域矩形の縦横比が1に近い
  • 領域矩形との面積比がpi/4(=正方形に内接する円)に近い

誤検知を狙って作られたようなひねくれオブジェクトでなければ、大体これで丸いものを見つけられるようです。

ただ、みかんが意外と横に長かったり、環境とスレッショルドの兼ね合いによっては検出される輪郭が大きかったり小さかったりするようで、結構パラメータには余裕をもたせた方が良いかも知れません。

僕の身の周りのものだと、縦横比は3:2くらいまで、面積比は0.675から0.875くらいまであたりの設定で良い感じに検出されてくれました。

例えば電球とか、瓶や缶とかだと理想的なので検出されやすいと思います。

ところで、こういう値のリアルタイム調整にはofxParamEditが便利なんで使ってください。(宣伝)

以下ソースです。(抜粋)

(ofxOpenCvを使ってます)

// 輪郭抽出時のパラメータ(適当に初期化してください)
float area_min_;
float area_max_;
int area_count_max_;
int gray_threshold_;

// 丸いかどうかの判定に使用するパラメータ(適当に初期化してください)
struct BlobDetect {
	float aspect_ratio_;
	float area_ratio_min_;
	float area_ratio_max_;
} blob_detect_;

ofPixels buf_;	// カメラ入力画像データ
ofxCvColorImage cv_color_;
ofxCvGrayscaleImage cv_gray_;
ofxCvContourFinder contour_;
ofxCvContourFinder contour_inv_;
vector<ofxCvBlob*> valid_blobs_;	// 有効な領域だけ保持しとく
Animation anime_;	// ふにふにアニメーション

void update()
{
	// カメラ画像をcvImageに読み込み
	cv_color_.setFromPixels(buf_.getPixels(), buf_.getWidth(), buf_.getHeight());
	// グレースケール変換
	cv_gray_ = cv_color_;
	// 二値化
	cv_gray_.threshold(gray_threshold_);
	// 輪郭抽出
	contour_.findContours(cv_gray_, area_min_, area_max_, area_count_max_, false);

	// 逆に二値化して再度チェック
	cv_gray_ = cv_color_;
	cv_gray_.threshold(gray_threshold_, true);
	contour_inv_.findContours(cv_gray_, area_min_, area_max_, area_count_max_, false);

	// 丸い輪郭だけ抽出
	valid_blobs_.clear();
	for(int i = 0; i < contour_.blobs.size(); ++i) {
		if(isValidBlob(contour_.blobs[i])) {
			valid_blobs_.push_back(&contour_.blobs[i]);
		}
	}
	for(int i = 0; i < contour_inv_.blobs.size(); ++i) {
		if(isValidBlob(contour_inv_.blobs[i])) {
			valid_blobs_.push_back(&contour_inv_.blobs[i]);
		}
	}

	// パカってする
	ofPixelsRef pix = cv_color_.getPixelsRef();
	for(vector<ofxCvBlob*>::iterator it = valid_blobs_.begin(); it != valid_blobs_.end(); ++it) {
		const ofRectangle& rect = (*it)->boundingRect;
		for(int x = rect.x; x < rect.x+rect.width; ++x) {
			for(int y = rect.y; y < rect.y+rect.height/2; ++y) {
				if(y-rect.height/2 < 0) {
					continue;
				}
				pix.setColor(x,y-rect.height/2, pix.getColor(x,y));
				pix.setColor(x,y, ofColor(0,0,0,0));
			}
		}
	}
	// テクスチャ更新
	cv_color_.updateTexture();
}

// その領域が丸いかどうかの判定
bool isValidBlob(ofxCvBlob& blob)
{
	// 縦横比
	const ofRectangle& rect = blob.boundingRect;
	if(rect.width < rect.height) {
		if(rect.height/rect.width > blob_detect_.aspect_ratio_) {
			return false;
		}
	}
	else {
		if(rect.width/rect.height > blob_detect_.aspect_ratio_) {
			return false;
		}
	}
	// 面積
	float area_ratio = blob.area/(rect.width*rect.height);
	if(area_ratio > blob_detect_.area_ratio_max_ || area_ratio < blob_detect_.area_ratio_min_) {
		return false;
	}
	return true;
}

void draw()
{
	ofPushMatrix();
	ofPushStyle();
	cv_color_.draw(0,0);
	for(vector<ofxCvBlob*>::iterator it = valid_blobs_.begin(); it != valid_blobs_.end(); ++it) {
		const ofRectangle& rect = (*it)->boundingRect;
		anime_.draw(rect.x, rect.y, rect.width, rect.height/2);
	}
	ofPopStyle();
	ofPopMatrix();
}

ちなみに、画像をパカっと切り取るところはシェーダーを使うとフレームバッファを消費することになるので、可搬性を考慮してCPUでやりましたが、あんまり気持ちよくはないのでうまい方法があれば教えてください。

ofxGuiの拡張addonをつくったよ(ofxParamEdit)

—————————————————————————
(追記 – 20121224)

アップデートによってGithubのバージョンとこの記事の内容とに差異があります。
Githubの方のREADMEを参照ください。
—————————————————————————

ofxGuiという、超便利なaddonがあります。

この動画の画面左のやつです。

Eden 1.2 + ofxComposer + ofxGui from Patricio Gonzalez Vivo on Vimeo.

スライダーやトグルボタンでグラフィカルに変数をコントロールできて、そのために書くコードの量もさほど多くないというスグレモノなんですが

  • int,float,bool としてしか使えない(charとかucharとか使いたいことあるじゃん)
  • 利用する変数の型が気持ち悪い(intが使いたいだけなのにofxIntSliderを持っとかなきゃいけないなんて!)
  • そのせいで利用時に処理が深い(ポインタ参照やら関数呼び出しが3回くらい?)
  • アプリの規模がでかくなった時にまとめて管理しづらい(お手軽に使える故なんですが。。。)

あたりがもうちょっと解消されればなあと思って作りました。こちらです。

ofxParamEdit on Github

良い特徴としては

  • char, uchar, short, ushort, int, uint, float, double, bool が使える(内部的にはint,float,boolなので値域のはみだすバグがあるかも)
  • intとかfloatとかの普通の変数を持ってれば使える(すでに作ったアプリに組み込むのも簡単!)
  • 利用時の処理コストなし(普通に変数を参照するのと同じ)
  • コントロール群をグループとして管理するので、複数箇所で使う際に分離がよい

悪い特徴としては

  • 値を編集したときの処理が増えてる
    →たかだか1フレームに1回のことなので問題にはならないでしょう
  • Guiを作るのにofxParamEditのインスタンスが必要
    →ofxParamEditを複数作れることに利点もあるのでまあ一長一短ってことで。。。
    アプリ側でシングルトンにする運用も可だと思います。

あと、プログラムのコメントにも書いてますが、ofEventのremoveの動作が怪しい(たぶん)ので、ofxGuiの一部ソースをコメントアウトして利用してください。

ofxSlider.cppとofxButton.cppとofxToggle.cppのofRegisterMouseEventsの行です。

Pull Request歓迎なんでプログラマの方よろしくお願いしまーす!

これ、ofxGuiの本家に組み込んでもらいたいなー。

グループ化の処理は置いといたとしても、Guiの機能を持ったクラスを利用側が保持しとく必要があるのはあんまり良くないと思うんだ。

フォーラムのどこで提案すればいいんだろう。

pull requestできるところまで作るには規模のでかい変更になるのでまずはお伺いをたてたいのだが。。。

ofxBox2dとofxOpenCvを同時に使用する際に発生する(かも知れない)コンパイルエラーについて(vc10)

ofxBox2dを使っていたプログラムでofxOpenCvを使用しようとしたらいきなり身に覚えがないところで見に覚えのないコンパイルエラーが発生してしばらくはまっていたのでメモ。

コンパイル時に出るエラーログは下記で始まるやつでした。

1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(441): error C2143: 構文エラー : ')' が 'const' の前にありません。
1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(441): error C3861: 'dot': 識別子が見つかりませんでした
1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(441): error C2059: 構文エラー : ')'
1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(448): error C2065: '_Ty' : 定義されていない識別子です。
1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(449): error C2143: 構文エラー : ';' が '{' の前にありません。
1>c:program files (x86)microsoft visual studio 10.0vcincludexcomplex(449): error C2447: '{' : 対応する関数ヘッダーがありません (旧形式の仮引数リスト?)

で、問題(っぽい)xcomplexの441行目付近は下記。

		// TEMPLATE FUNCTION norm
_TMPLT(_Ty) inline
	_Ty norm(const _CMPLX(_Ty)& _Left)
	{	// return squared magnitude
	return (real(_Left) * real(_Left) + imag(_Left) * imag(_Left));
	}

‘const’の前に’)'がないって言われてるってことは’norm(‘がおかしいんだろうなーと思ったんだけど、ちゃんと関数宣言してるからおかしいところないし・・・と思って次のエラーを見てみると

‘dot’ってなんやねん。

ってことで’#define norm dot’的なやつを探してみると、ありました。(VisualStudio使用なのでF12押しただけですけど)

ofxBox2dPolygonUtils.h(38)

// dot product (3D) which allows vector operations in arguments
//#define dot(u,v)   ((u).x * (v).x + (u).y * (v).y + (u).z * (v).z)
#define norm2(v)   dot(v,v)        // norm2 = squared length of vector
#define norm(v)    sqrt(norm2(v))  // norm = length of vector
#define d2(u,v)    norm2(u-v)      // distance squared = norm2 of difference
#define d(u,v)     norm(u-v)       // distance = norm of difference

で、ここからはもう別に調べる必要ないんですけど一応。問題の行は

_Ty sqrt(dot(const complex<_Ty>& _Left, const complex<_Ty>& _Left))

こんな風に展開されてて、コンパイラさんに「引数あるなら書いて欲しいんですけど’dot(‘とか知らんしwwwww’const’とか書く前に括弧閉じろしwwwwwww」

って言われてたわけですね。すっきり!でも腹立たしい!

で、とりあえずの解決策としてはofxBox2dPolygonUtils.hよりも前にxcomplexを#includeしてほしいので

ofxOpenCv.hをofxBox2d.hの前に#includeしてやれば通るようになりました。

(ちなみに#include <complex>はopencv2/core/core.hppにあります)

とりあえずはこれでいいけど、根本的にはofxBox2dでこんなに一般的な名前を#defineしてるほうが腹立たしくてしょうがないんですけどね!!!せめて#undefしろ!

(追記)

ofxBox2dPolygonUtils.hの#defineあたりのコードはどうやらofPolyline.cppを元にしているようでした。

僕はlibにビルドして使用してるからcppに書いてある分には問題ないけど、

プロジェクトにoFのソースまで含めて作ってる人(少なからずいると思うんだ)の間で問題になってないんだろうか・・・?