メンバ関数の戻り値に対応したアニメーションを描画

オブジェクトの状態をOpenCVのウィンドウ上に色々な方法で表示させたい.たとえば,

class Foo {
public:
    int foo() const;
    const std::string& bar() const;
    double buzz() const;
private:
    int foo;
    std::string bar;
    double buzz;
};

こういうクラスが有るときに,「fooの値に連動して画像が回転する」とか,「barやbuzzの戻り値を画面にテキストで表示する」とか,「buzzの戻り値がある閾値以上のときにだけ表示するアイコンが欲しい」みたいな要求を満たしてくれるモノがあると,ロジックとビューを分離できて嬉しいなーと思ったので,車輪の再発明とは思いつつ実装してみた.

ベースクラス

デザインパターンの教科書通りに,まずは文字と画像に共通の基底クラスを用意.IplImageのポインタを渡すと,そこに自身のエレメントを描画するようなインターフェースにしてみる.

#ifndef IELEMENT_H_
#define IELEMENT_H_

class IElement {
public:
	IElement(int x, int y) : x_(x), y_(y){}

	virtual ~IElement(){}

	/**
	 *  渡された画像の上に自身のエレメントを加える
	 */
	virtual void draw(IplImage* image) = 0;

	/**
	 *  エレメントの親要素に対する水平方向相対位置を取得する
	 */
	int x() const { return x_; }

	/**
	 *  エレメントの親要素に対する垂直方向相対位置を取得する
	 */
	int y() const { return y_; }

	/**
	 *  エレメントの親要素に対する相対位置を設定する
	 */
	void setPoint(int x, int y) { x_ = x; y_ = y; }

protected:
	IElement();
	int x_;
	int y_;
};

#endif

文字の表示

ostringstreamで変換可能な型であれば何でも表示させたい,ということで内部にテンプレートクラスを持たせる.

#ifndef TEXT_ELEMENT_H_
#define TEXT_ELEMENT_H_

#include <sstream>
#include <string>
#include <opencv/cv.h>
#include <opencv/highgui.h>

#include "IElement.h"

/**
 *  文字を表示するエレメント。
 */
class TextElement : public IElement {
public:
	TextElement(const std::string& s, int x, int y, 
	    int fontFace = CV_FONT_HERSHEY_SIMPLEX, CvScalar color = CV_RGB(0, 0, 0), int thickness = 1) : IElement(x, y){
		cvInitFont(&font_, fontFace, 1.0, 1.0, 0.0, thickness, 8);
	}

	virtual void draw(IplImage* image){
		cvPutText(image, s_.c_str(), cvPoint(x_, y_), &font_, color_);
	}

protected:
	std::string s_;  //! 表示する文字列
	CvFont font_;    //! フォント
	CvScalar color_; //! 文字色
};

/**
 * 動的に情報を更新して文字を表示するエレメント。
 */
class DynamicTextElement : public TextElement{
	class _Holder {
	public:
		virtual std::string value() = 0;
	};

	template<class T, class Func>
	class _HolderImpl : public _Holder {
	public:
		_HolderImpl(const T& src, Func f) : src_(src), f_(f) {}
		virtual std::string value(){
			std::ostringstream ss;
			ss << (src_.*f_)();
			return ss.str();
		}
	private:
		Func f_;
		const T& src_;
	};

public:
	template<class T, class Func>
	DynamicTextElement(const T& src, Func f, int x, int y, int fontFace, CvScalar color, int thickness = 1)
		: TextElement("", x, y, fontFace, color, thickness){
		holder_ = new _HolderImpl<T, Func>(src, f);
	}

	template<class T, class Func>
	DynamicTextElement(const T& src, Func f, int x, int y) : TextElement("", x, y){
		holder_ = new _HolderImpl<T, Func>(src, f);
	}

	virtual void draw(IplImage* image) {
		s_ = holder_->value();
		TextElement::draw(image);
	}

private:
	_Holder* holder_;
};

#endif

これで,こんな感じにメンバ関数の戻り値をテキスト表示できるようになった.

#include <string>
#include <vector>
#include <opencv/cv.h>
#include <opencv/highgui.h>
#include "TextElement.h"

class Foo {
public:
    int foo() const;
    const std::string& bar() const;
    double buzz() const;
private:
    int foo;
    std::string bar;
    double buzz;
};

// てきとうなウィンドウ描画クラス
class Window {
public:
    void addElement(IElement* element){
        elements_.push_back(element);
    }

    void draw(IplImage* image){
        for(auto it = elements_.begin(); it != elements_.end(); ++it){
            (*it)->draw(image);
        }
    }
private:
    std::vector<IElement*> elements_;
};

int main(void){
    Foo f;
    Window w;

    w.addElement(new DynamicTextElement(f, &Foo::foo, 0, 0));    // Foo::fooの戻り値を(0,0)に表示
    w.addElement(new DynamicTextElement(f, &Foo::bar, 100, 0));  // Foo::barの戻り値を(100,0)に表示
    w.addElement(new DynamicTextElement(f, &Foo::buzz, 200, 0)); // Foo::buzzの戻り値を(200,0)に表示

    IplImage* img = cvCreateImage(cvSize(720, 480), 8, 3);
    cvNamedWindow("foo");

    while(1){
        cvWaitKey(10);
        w.draw(img);
        cvShowImage("foo", img);
    }
}

画像

画像の表示も基本的にアイデアは同じで,クラスの内部に非テンプレートな基底クラスと,テンプレートな派生クラスを持たせておけばOK.リソースの解放や例外処理をぜんぜんケアしていなかったり,冗長で汚らしい感じだけど例えばこんな風に書いてみる.

#ifndef IMAGE_ELEMENT_H_
#define IMAGE_ELEMENT_H_
#include <string>
#include <vector>
#include <opencv/cv.h>
#include <opencv/highgui.h>

#include "IElement.h"
#include "cvHelper.h"

/**
 * 画像を表示する基本エレメント。
 */
class ImageElement : public IElement{
public:
	ImageElement(const std::string& filename, int x = 0, int y = 0)
	    : IElement(x, y), mode_(NORMAL), parent_(NULL), image_(NULL){
		imageOrigin_ = loadImage(filename);
		if(imageOrigin_ != NULL){
			image_ = cvCreateImage(cvSize(imageOrigin_->width, imageOrigin_->height), 8, 3);
			cvCopy(imageOrigin_, image_);
		}
		this->affineMat_ = cvCreateMat(2, 3, CV_32FC1);
	}

	ImageElement(const std::string& filename, ImageElement* parent, int x, int y)
	    : IElement(x, y), mode_(NORMAL), parent_(parent), image_(NULL){
		imageOrigin_ = loadImage(filename);
		if(imageOrigin_ != NULL){
			image_ = cvCreateImage(cvSize(imageOrigin_->width, imageOrigin_->height), 8, 3);
			cvCopy(imageOrigin_, image_);
		}	
		this->affineMat_ = cvCreateMat(2, 3, CV_32FC1);
	}

	~ImageElement(){
		if(image_ != NULL) cvReleaseImage(&image_);
		if(imageOrigin_ != NULL) cvReleaseImage(&imageOrigin_);
		cvReleaseMat(&affineMat_);
	}

	virtual void draw(IplImage* image){
		if(!enable_) return;

		if(parent_ == NULL) setImage(src, image_, x_, y_, mode_);
		else                setImage(src, image_, x_ + parent_->x(), y_ + parent_->y(), mode_);

		for(auto it = child_.begin(); it != child_.end(); ++it) (*it)->draw(src);
	}

	void setMode(DrawingMode mode){ mode_ = mode; }

	void addElement(const std::string& fileName, const std::string& name, int x = 0, int y = 0){
		ImageElement* image = new ImageElement(fileName, name, this, x, y);
		this->child_.push_back(image);
	}

	void addElement(ImageElement* element){
		element->parent_ = this;
		this->child_.push_back(element);
	}

	void rotate(double angle){
		cv2DRotationMatrix(cvPoint2D32f(imageOrigin_->width / 2, imageOrigin_->height / 2), angle, 1, affineMat_);
		cvWarpAffine(imageOrigin_, image_, affineMat_, 9, CV_RGB(255, 255, 255));
	}

	void translate(double dx, double dy){
		CvPoint2D32f before[] = {cvPoint2D32f(0, 0), cvPoint2D32f(0, 1), cvPoint2D32f(1, 0)};
		CvPoint2D32f after[]  = {cvPoint2D32f(dx, dy), cvPoint2D32f(dx, 1 + dy), cvPoint2D32f(1 + dx, dy)};
		cvGetAffineTransform(before, after, affineMat_);
		cvWarpAffine(imageOrigin_, image_, affineMat_, 9, CV_RGB(255, 255, 255));
	}

	void setROI(const CvRect& rect){
		cvSetImageROI(image_, rect);
		cvSetImageROI(imageOrigin_, rect);
	}

	void resetROI(){
		cvResetImageROI(image_);
		cvResetImageROI(imageOrigin_);
	}

protected:
	DrawingMode mode_;
	_IplImage* image_;
	_IplImage* imageOrigin_;
	CvMat* affineMat_;
	std::vector<ImageElement*> child_;
	ImageElement* parent_;
};

/**
 * アニメーションを行う基本エレメント
 */ 
class AnimationElement : public ImageElement{
public:
	AnimationElement(const std::string& animationImage,int x = 0, int y = 0) : ImageElement(animationImage, x, y){}
	virtual void draw(IplImage* image);
protected:
	virtual void drawAnimation() = 0;
};

/**
 * 連続的に動くアニメーションエレメント.具体的な動作はポリシー・クラスで指定する
 */
template<class AnimationPolicy>
class DynamicAnimationElement : public AnimationElement, public AnimationPolicy{
	class _Holder{
	public:
		virtual double getValue() = 0;
	};

	template<class R, class T>
	class _HolderImpl : public _Holder {
	public:
		typedef R (T::*Func)(void) const;
		_HolderImpl(const T& src, Func func, R min, R max) : src_(src), func_(func), min_(min), max_(max){}
		virtual double getValue(){
			R value = (src_.*func_)();
			return 100.0 * (value - min_) / (max_ - min_);
		}
	private:
		const T& src_;
		Func func_;
		R min_;
		R max_;
	};

public:
	template<class R, class T>
	DynamicAnimationElement(const std::string& animationImage, const T& src, R (T::*func)(void) const, R min, R max)
		: AnimationElement(animationImage, 0, 0) {
			holder_ = new _HolderImpl<R, T>(src, func, min, max);
	}

private:
	virtual void drawAnimation(){
		animate(this, holder_->getValue());//このメソッドはAnimationPolicy側に実装されている
	}
	_Holder* holder_;
};

/**
 * メンバ関数の戻り値が条件を満たした場合に画像を表示するエレメント。
 */
class DynamicImageElement : public ImageElement {
	class _Holder {
	public:
		virtual bool check() = 0;
	};
	template<class T, class Func, class Pred, typename Ref>
	class _GenericHolderImpl : public _Holder{
	public:
		_GenericHolderImpl(const T& src, Func f, Pred p, Ref r) : src_(src), f_(f), p_(p), r_(r){}
		virtual bool check(){
			return (*p_)((src_.*f_)(), r_);
		}
	private:
		const T& src_;
		Func f_;
		Pred p_;
		Ref r_;
	};

	template<class T, class Func>
	class _BoolHolderImpl : public _Holder{
	public:
		_BoolHolderImpl(const T& src, Func f) : src_(src), f_(f){}
		virtual bool check(){
			return (src_.*f_)();
		}
	private:
		const T& src_;
		Func f_;
	};

public:
	DynamicImageElement(const std::string& defaultFileName, int x = 0, int y = 0) : ImageElement(defaultFileName, x, y){}

	template<class T, class Func, class Pred, typename Ref>
	void addElementWithFunctor(const std::string& fileName, const T& src, Func f, Pred pred, Ref ref){
		ImageElement* image = new ImageElement(fileName, this, 0, 0);
		addElement(image);
		holder_.push_back(std::make_pair(new _GenericHolderImpl<T, Func, Pred, Ref>(src, f, pred, ref), image));
	}

	template<class T, class Func>
	void addElementWithFunctor(const std::string& fileName, const T& src, Func f){
		ImageElement* image = new ImageElement(fileName, this, 0, 0);
		addElement(image);
		holder_.push_back(std::make_pair(new _BoolHolderImpl<T, Func>(src, f), image));
	}

	virtual void draw(_IplImage* image){
		auto it = holder_.begin();

		while(it != holder_.end()){
			if((*it).first->check()){
				(*it).second->draw(image);
				return;
			}
			++it;
		}
		ImageElement::draw(image);
	}

private:
	std::vector<std::pair<_Holder*, ImageElement*> > holder_;
};

#endif

あとは,アニメーションの具体的な操作を実装するポリシー・クラスをさっくり書けばOK.

#ifndef ANIMATION_POLICY_H_
#define ANIMATION_POLICY_H_

#include "ImageElement.h"

/** 
 *  ステータス値を画像の回転に変換するアニメーションポリシー。
 */
class RotationPolicy {
public:
	RotationPolicy(){}

	/**
	 * アニメーションのパラメータ設定を行う.
	 *
	 * @param amin  ステータス最小時の,画像の回転角度(deg).
	 * @param amax  ステータス最大時の,画像の回転角度(deg).
	 */
	void setRotationParam(int amin = 0, int amax = 180){
		amin_ = amin;
		amax_ = amax;
	}

protected:
	void animate(ImageElement* part, double value){
		part->rotate( amin_ + value * (amax_ - amin_) / 100.0);
	}

private:
	int amin_;
	int amax_;
};

/** 
 *  ステータス値を画像の平行移動に変換するアニメーションポリシー。
 */
class TranslationPolicy {
public:
	TranslationPolicy() {}

	/**
	 * アニメーションのパラメータ設定を行う.
	 *
	 * @param min  ステータス最小時の,画像の左上隅座標.
	 * @param max  ステータス最大時の,画像の左上隅座標.
	 */
	void setTranslationParam(CvPoint2D32f min, CvPoint2D32f max){
		xmin_ = min.x;
		ymin_ = min.y;
		xmax_ = max.x;
		ymax_ = max.y;
	}
protected:
	void animate(ImageElement* part, double value){
		double x = value * (xmax_ - xmin_) / 100.0 + (double)xmin_;
		double y = value * (ymax_ - ymin_) / 100.0 + (double)ymin_;
		part->translate(x, y);
	}

private:
	double xmin_;
	double xmax_;
	double ymin_;
	double ymax_;
};

/**
 * ステータス値を画像のROI移動に変換するアニメーションポリシー.
 */
class ROIMovePolicy {
public:
	ROIMovePolicy() {}

	/**
	 * アニメーションのパラメータ設定を行う.
	 *
	 * @param size ROIの矩形サイズ.
	 * @param min  ステータス最小時の,ROIの左上隅座標.
	 * @param max  ステータス最大時の,ROIの左上隅座標.
	 */
	void setROIParam(CvSize size, CvPoint2D32f min, CvPoint2D32f max){
		size_ = size;
		xmin_ = min.x;
		ymin_ = min.y;
		xmax_ = max.x;
		ymax_ = max.y;
	}

protected:
	void animate(ImageElement* part, double value){
		double x = value * (xmax_ - xmin_) / 100.0 + (double)xmin_;
		double y = value * (ymax_ - ymin_) / 100.0 + (double)ymin_;
		part->resetROI();
		part->setROI(cvRect((int)x, (int)y, size_.width, size_.height));
	}

private:
	double xmin_;
	double xmax_;
	double ymin_;
	double ymax_;
	CvSize size_;
};
#endif

あとは適当な画像を用意してやれば,メンバ関数に応じて回ったり消えたりスライドしたりしてくれるはず.

#include <string>
#include <opencv/cv.h>
#include <opencv/highgui.h>

#include "ImageElement.h"

class Foo {
public:
    int foo() const;
    const std::string& bar() const;
    double buzz() const;
    bool isFOO() const;
private:
    int foo;
    std::string bar;
    double buzz;
};

template<typename T>
bool Greater(const T& x, const T& y){
	return x > y;
}

int main(void){
    Foo f;

    // f.foo()の戻り値に応じて,画像を0〜120度の範囲で回転させる
    auto rotation = new DynamicAnimationElement<RotationPolicy>("triangle.png", f, &Foo::foo, 0, 100);
    rotation.setRotationParam(0, 120);

    // f.buzz()の戻り値に応じて,画像を(0,0)から(200,200)まで平行移動させる
    auto translation = new DynamicAnimationElement<TranslationPolicy>("circle.png", f, &Foo::buzz, 0.0, 1.0);
    translation.setTranslationParam(cvPoint2D32f(0, 0), cvPoint2D32f(200, 200));


    auto indicator = new DynamicImageElement("default.png", 0, 0);
    indicator.addElementWithFunctor("1.png", f, &Foo::buzz, Greater<double>, 0.5); // f.buzz()の戻り値が0.5より大きかったら画像を表示
    indicator.addElementWithFunctor("2.png", f, &Foo::isFoo); // f.isFoo()の戻り値がtrueなら画像を表示
    IplImage* img = cvCreateImage(cvSize(720, 480), 8, 3);
    cvNamedWindow("foo");

    while(1){
        cvWaitKey(10);
        rotation.draw(img);
        translation.draw(img);
        indicator.draw(img);
        cvShowImage("foo", img);
    }
}