C++で2次元アフィン変換クラスを作る

2次元座標においてある点を別の点に移す変換のなかで、下の基本変換の合成変換として表現できるものをアフィン変換と呼びます。
例えば、(1,2)だけ平行移動させて、原点を中心に30°回転させる変換は、R(30^.)T(1,2)と表すことができます。


平行移動(x: t_x,y: t_y)
\normalsize\begin{equation}T(t_x,t_y)=\begin{bmatrix}1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}\end{equation}

拡大縮小
\normalsize\begin{equation}S_c(s_x,s_y)=\begin{bmatrix}s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}\end{equation}

回転
\normalsize\begin{equation}R(\theta)=\begin{bmatrix}cos(\theta) & -sin(\theta) & 0 \\ sin(\theta) & cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}\end{equation}

スキュー変換
\normalsize\begin{equation}S_k(k_x,k_y)=\begin{bmatrix}1 & k_x & 0 \\ k_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\end{equation}
JavaではAffineTransformクラスが用意されていて、いくつかの変換を簡単に合成することができます。このAffineTransformクラスにならって、C++で同じようなクラスの簡易版を作ってみたいと思います。アフィン変換を表す行列には、以前作成したクラスを利用します。→C++で作る行列クラス その1 - white wheelsのメモ

利用例
	AffineTransform2D Tr;
	Tr.translate(1,2);
	Tr.rotate(30);
	Tr.scale(2,3);
	Tr.print();
	Tr.setToRotate(90);
	Tr.print();

AffineTransform2Dクラス

3次元の行列クラスであるMatrix3Dクラスを利用してアフィン変換をあらわすには、Matrix3Dを継承させる方法と、メンバとして持たせる方法があります。今回はMatrix3Dクラスの演算を利用して簡単に書きたいのでメンバとして持たせることにします。

class AffineTransform2D{
public:
//...
	void setToIdentity();			//恒等変換に設定
	void setToRotation(float theta);	//回転変換に設定
	void setToRotation(float theta, float x, float y);	//平行移動→回転変換に設定
	void setToScale(float sx, float sy);			//スケーリング変換に設定
	void setToSkew(float skx, float sky);			//スキュー変換に設定
	void setToTranslation(float tx, float ty);		//平行移動変換に設定
	void setToXReflection();		//x軸に関する鏡映変換に設定
	void setToYReflection();		//y軸に関する鏡映変換に設定

	void scale(float sx, float sy);		//スケーリング変換を行う
	void skew(float skx, float sky);	//スキュー変換を行う
	void translate(float tx, float ty);	//平行移動変換を行う
	void rotate(float theta);		//回転変換を行う
	void reflectX();			//x軸に関する鏡映変換を行う
	void reflectY();			//y軸に関する鏡映変換を行う

	void copyTo(AffineTransform2D& transform_dest);		//アフィン変換を複製する
	void setTransform(AffineTransform2D& additionalTrans);	//アフィン変換を合成する
protected:
	Matrix3D m_mat;//行列表現
};
基本変換の定義

AffineTransform2Dクラス内で基本行列の定義を与えておくと、メンバ関数の中ではこれらを利用して簡単にコードを書くことができます。

class AffineTransform2D{
//略
protected:
#define Trans(tx,ty) Matrix3D(	1,	0,	tx, \
				0,	1,	ty, \
				0,	0,	1	)

#define Scale(sx,sy) Matrix3D(	sx,	0,	0, \
				0,	sy,	0, \
				0,	0,	1	)

#define Skew(skx,sky) Matrix3D(	1,	skx,	0, \
				sky,	1,	0, \
				0,	0,	1	)

#define Rot(theta) Matrix3D(	cos(theta),	-sin(theta),	0, \
				sin(theta),	cos(theta),	0, \
				0,		0,		1	)
};
基本変換の実装

例えば平行移動変換はこのように書くことができます。

//平行移動変換に設定
void AffineTransform2D::setToTranslation(float tx, float ty){
	m_mat = Trans(tx,ty);
}

現在のアフィン変換にさらに平行移動を行うには次のように左から行列をかけるとよいです。

//平行移動変換を行う
void AffineTransform2D::translate(float tx, float ty){
	m_mat = Trans(tx,ty) * m_mat;
}

回転変換では引数を「°」で指定できると便利なので、ラジアンと「°」の変換関数を定義しておきます。

#ifndef PI
#define PI 3.141592653589793
#endif

#define RAD(theta) float(theta/180*PI)
#define DEG(phi) float(phi/PI*180)

角度変換用マクロを使うと、回転変換に関するメンバ関数の実装は次のようになります。

//回転変換に設定
void AffineTransform2D::setToRotation(float theta){
	float phi = RAD(theta);
	m_mat = Rot(phi);
}
//平行移動→回転変換に設定
void AffineTransform2D::setToRotation(float theta, float x, float y){
	float phi = RAD(theta);
	m_mat = RotXY(phi,x,y);
}
//回転変換を行う
void AffineTransform2D::rotate(float theta){
	float phi = RAD(theta);
	m_mat = Rot(phi) * m_mat;
}

アフィン変換を複製したり、アフィン変換同士を連結するメンバ関数も次のように行列の演算を使うと簡単に書くことができます。

//アフィン変換を複製する
void AffineTransform2D::copyTo(AffineTransform2D& transform_dest){
		transform_dest.getMatrix() = m_mat;
}
//アフィン変換を合成する
void AffineTransform2D::setTransform(AffineTransform2D& additionalTrans){
	m_mat = additionalTrans.getMatrix() * m_mat;
}

最終的に作成したものです。

AffineTransform.h
#ifndef __AFFINE_TRANSFORM_2D_H
#define __AFFINE_TRANSFORM_2D_H
#include "Matrix3D.h"

class AffineTransform2D{
public:
	AffineTransform2D();

	Matrix3D& getMatrix();		//行列表現を取得する
	float* operator[](int i);	//行列要素を取得する	

	void setToIdentity();			//恒等変換に設定
	void setToRotation(float theta);	//回転変換に設定
	void setToRotation(float theta, float x, float y);	//平行移動→回転変換に設定
	void setToScale(float sx, float sy);			//スケーリング変換に設定
	void setToSkew(float skx, float sky);			//スキュー変換に設定
	void setToTranslation(float tx, float ty);		//平行移動変換に設定
	void setToXReflection();		//x軸に関する鏡映変換に設定
	void setToYReflection();		//y軸に関する鏡映変換に設定

	void scale(float sx, float sy);		//スケーリング変換を行う
	void skew(float skx, float sky);	//スキュー変換を行う
	void translate(float tx, float ty);	//平行移動変換を行う
	void rotate(float theta);		//回転変換を行う
	void reflectX();			//x軸に関する鏡映変換を行う
	void reflectY();			//y軸に関する鏡映変換を行う

	void copyTo(AffineTransform2D& transform_dest);		//アフィン変換を複製する
	void setTransform(AffineTransform2D& additionalTrans);	//アフィン変換を合成する
	void print();						//行列要素を出力する
protected:
	Matrix3D m_mat;

#ifndef PI
#define PI 3.141592653589793
#endif
#define RAD(theta) float(theta/180*PI)
#define DEG(phi) float(phi/PI*180)

#define Id Matrix3D(	1,	0,	0, \
			0,	1,	0, \
			0,	0,	1	)

#define Trans(tx,ty) Matrix3D(	1,	0,	tx, \
				0,	1,	ty, \
				0,	0,	1	)

#define Scale(sx,sy) Matrix3D(	sx,	0,	0, \
				0,	sy,	0, \
				0,	0,	1	)

#define Skew(skx,sky) Matrix3D(	1,	skx,	0, \
				sky,	1,	0, \
				0,	0,	1	)

#define Rot(theta) Matrix3D(	cos(theta),	-sin(theta),	0, \
				sin(theta),	cos(theta),	0, \
				0,		0,		1	)

#define RotXY(theta,x,y) Matrix3D(	cos(theta),	-sin(theta),	x, \
					sin(theta),	cos(theta),	y, \
					0,		0,		1	)

#define RefX Matrix3D(	1,	0,	0, \
			0,	-1,	0, \
			0,	0,	1	)

#define RefY Matrix3D(	-1,	0,	0, \
			0,	1,	0, \
			0,	0,	1	)
};
#endif
AffineTransform2D.cpp
#include "AffineTransform2D.h"
#include <cmath>
//コンストラクタ
AffineTransform2D::AffineTransform2D(){
	setToIdentity();
}
//行列表現の取得
Matrix3D& AffineTransform2D::getMatrix(){
	return m_mat;
}
//添え字演算子
float* AffineTransform2D::operator[](int i){
	return m_mat[i];
}
//基本変換に設定
void AffineTransform2D::setToIdentity(){
	m_mat = Id;
}
void AffineTransform2D::setToRotation(float theta){
	float phi = RAD(theta);
	m_mat = Rot(phi);
}
void AffineTransform2D::setToRotation(float theta, float x, float y){
	float phi = RAD(theta);
	m_mat = RotXY(phi,x,y);
}
void AffineTransform2D::setToScale(float sx, float sy){
	m_mat = Scale(sx,sy);
}
void AffineTransform2D::setToSkew(float skx, float sky){
	m_mat = Skew(skx, sky);
}
void AffineTransform2D::setToTranslation(float tx, float ty){
	m_mat = Trans(tx,ty);
}
void AffineTransform2D::setToXReflection(){
	m_mat = RefX;
}	
void AffineTransform2D::setToYReflection(){
	m_mat = RefX;
}
//基本変換
void AffineTransform2D::scale(float sx, float sy){
	m_mat = Scale(sx,sy) * m_mat;
}
void AffineTransform2D::skew(float skx, float sky){
	m_mat = Skew(skx,sky) * m_mat;
}
void AffineTransform2D::translate(float tx, float ty){
	m_mat = Trans(tx,ty) * m_mat;
}
void AffineTransform2D::rotate(float theta){
	float phi = RAD(theta);
	m_mat = Rot(phi) * m_mat;
}
void AffineTransform2D::reflectX(){
	m_mat = RefX * m_mat;
}
void AffineTransform2D::reflectY(){
	m_mat = RefY * m_mat;
}
//複製
void AffineTransform2D::copyTo(AffineTransform2D& transform_dest){
		transform_dest.getMatrix() = m_mat;
}
//合成
void AffineTransform2D::setTransform(AffineTransform2D& additionalTrans){
	m_mat = additionalTrans.getMatrix() * m_mat;
}
//出力
#include <iostream>
void AffineTransform2D::print(){
	std::cout << m_mat << std::endl;
}

inline関数をヘッダに書く際の注意点

inlineメンバ関数を含むクラスを作成するときにヘッダと実装ファイルを作成する方法について。
下のように、2つのcppファイルで利用するクラスAを作成するとしましょう。

main.cpp
#include "A.h"
int main(){
	A a;
	a.method();
	return 0;
}


A.hA.cpp

#ifndef AUser_H
#define AUser_H

#include "A.h"
class AUser
{
public:
	AUser();
};
#endif


#include "AUser.h"

AUser::AUser()
{
	A a;
	a.method();
}

この状況でコンパイルが通るようにするためには、下の例のようにA.hとA.cppを普通に作ればOKです。

Example1


A.hA.cpp

#ifndef A_H
#define A_H

class A
{
public:
	void method();
};
#endif


#include "A.h"
void A::method(){

}

ここでmethod()をinline関数を使って定義するにはどうすればよいか考えてみます。
下の例ではヘッダにinline関数の定義がないので、A::method()が存在しないことになってしまいます。
そのためmain.obj,AUser.objの2つでリンクエラーが発生します。

Example2 --compile error(未解決の関数名)


A.hA.cpp

#ifndef A_H
#define A_H

class A
{
public:
	void method();
};
#endif


#include "A.h"
inline void A::method(){

}

これを解決するにはExample3のように、inline関数の定義をヘッダに書くか、
Example4のようにクラス宣言の内部に記述して対処することができます。クラス内でメンバ関数を定義すると自動的にinline化されるので、"inline"修飾子を省略することができます。


Example3
Example4

#ifndef A_H
#define A_H

class A
{
public:
	void method();

};

inline void A::method()
{
}
#endif


#ifndef A_H
#define A_H

class A
{
public:
	void method(){
		
	}

};
#endif

例えば、コンストラクタなど他のメンバ関数の一部がinlineではない場合には通常通りA.cppに書かなければなりません。
そうしないと、main.objとAUser.objの両方でmethod()の定義を行うことになってしまいます。
Example5はコンパイルエラーで、Example6はコンパイルが通ります。

Example5 --compile error(複数の関数定義)
A.h
#ifndef A_H
#define A_H

class A
{
public:
	A();
	void method();
};
A::A(){

}
inline void A::method()
{
}
#endif
Example6


A.hA.cpp

#ifndef A_H
#define A_H

class A
{
public:
	A();
	void method();
};
inline void A::method()
{
}
#endif


#include "A.h"
void A::A(){

}

特定の関数はinline化してはいけない場合はExample6のように実装の一部を.cppに記述することになります。
ただそういう制約がなくて、実装部分を全てヘッダ一つだけにまとめたいという場合は、
他のメンバ関数も全てinline関数にすると、ヘッダにまとめることができます。(Example7,8どちらでもOK)
Example7のように、宣言と定義を分離して、実装部分にinlineを付けるのが良いかもしれません。


Example7
Example8

#ifndef A_H
#define A_H
class A
{
public:
	A();
	void method();

};
inline A::A(){

}
inline void A::method()
{
}
#endif


#ifndef A_H
#define A_H
class A
{
public:
	A(){}
	void method();

};
inline void A::method()
{
}
#endif