このページではアフィン変換についての解説とアフィン変換を用いたC言語の画像処理プログラムの紹介をします。
Contents
アフィン変換とは
まずアフィン変換がどのようなものであるかを説明します。
アフィン変換とは「線形変換+平行移動」
画像に対するアフィン変換とは線形変換による画像の変形と平行移動を行う変換です。
具体的には、アフィン変換は下記の式により行うことが可能です。これはアフィン変換後の座標(X, Y)を元画像の座標(x, y)で表した式になります。元画像の全座標を下記式で計算したアフィン変換後の座標(X, Y)に移動することが画像に対するアフィン変換となります。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} a & b \\ c & d \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) + \left ( \begin{array}{c} m \\ n \end {array} \right ) $$
ここで出てくる下記の行列が線形変換を行うための行列です。
$$\left ( \begin{array}{cc} a & b \\ c & d \end{array} \right ) $$
さらに、下記が並行移動量を表すベクトルになります。
$$\left ( \begin{array}{c} m \\ n \end {array} \right ) $$
スポンサーリンク
線形変換とは
平行移動についてはイメージつきやすいと思いますが、線形変換がどういったものであるかはイメージつきにくいかもしれません。ここではこの線形変換がイメージできるように解説していきたいと思います。
線形変換とは座標を変形する変換
まず思い浮かべていただきたいのが、「座標」です。これを聞いてどのようなものをイメージするでしょうか?
大半の方は、下の図のようなものを思い浮かべると思います。
線形変換とは、簡単に言ってしまうと、この座標を変形する(座標変換する)演算になります。
画像で考えると、この座標を変形することにより、各画素が他の座標に移動することになります。
この移動を全画素に対して行うことにより画像が変形します。これが画像に対する線形変換です。
基底ベクトルが変わると座標が変形する
ではどのような形に変形する事ができるのでしょうか?このあたりを深掘りしていきたいと思います。
ここからは数学的要素を取り入れます。まず最初に思い浮かべた座標について考えましょう。この座標を形成しているのは、2つのベクトルになります。具体的にはベクトル\(\left ( \begin{array}{c} 1 \\ 0 \end {array} \right )\)とベクトル\( \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)から形成されています。
上の図で青矢印が下向きであることに注意してください
数学等で教わる一般的な座標では、縦方向の正の向きは上ですが、画像データにおける座標では、縦方向の正の向きが下となります
そのため、このページでも縦方向の正の向きは下方向として考えます
この座標における全ての点は、\(a \left ( \begin{array}{c} 1 \\ 0 \end {array} \right )\)+\(b \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)の形で表す事が可能です。例えば座標(3, 2)を指すベクトル\( \left ( \begin{array}{c} 3 \\ 2 \end {array} \right ) \)は、
$$ \left ( \begin{array}{c} 3 \\ 2 \end {array} \right )=3 \left ( \begin{array}{c} 1 \\ 0 \end {array} \right )+2 \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) $$
で表す事が可能です。
このような座標を形成する2つの平行でないベクトルを基底ベクトルと呼びます。この基底ベクトルを変更することにより座標の変形が可能です。
例えば基底ベクトルを\(\left ( \begin{array}{c} 1 \\ -1 \end {array} \right )\)とベクトル\( \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)に変更してみましょう。座標は下記のように変形されます。
この時に先ほどと同様に(3, 2)座標の点について考えてみましょう。基底ベクトル変更による座標変換後の(3, 2)座標は下の図のようになります。
これを最初の基底ベクトル\( \left ( \begin{array}{c} 1 \\ 0 \end {array} \right )\)と\( \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)の座標で考えると、下の図のようになります。
もともと(3, 2)座標にあった点が(3, -1)座標に移動していることが確認できると思います。このように、基底ベクトルを変更することで元の点が異なる位置に移動します。
画像データで考えると、基底ベクトル変更すると座標が変形し、各画素が異なる位置に移動することになります。
この画素の移動を全画素に対して行うと、結果的に画像が変形することになります。
上の例の右側の画像は、実際に左側の画像を基底ベクトルを\(\left ( \begin{array}{c} 1 \\ -1 \end {array} \right )\)と\( \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)を用いて変形した画像になります。
このように基底ベクトルを変更することにより、画像データを様々な形に変更する事が可能です。そして、この基底ベクトルを変更する事こそが線形変換です。
ただし線形変換で変形できるのは基底ベクトルで表現できる座標のみです。例えば曲線状の座標等には変形することができません。
行列の積により基底ベクトルが変えられる
基底ベクトルを変更することで座標が変形し、点や画素が移動するので画像や図形の形が変わることはイメージできてきたのではないかと思います。ではどうすれば基底ベクトルを変更することができるのでしょうか?ここを掘り下げます。
この基底ベクトルの変更に用いられるのが「行列」です。基底ベクトルは行列の各係数によって指定する事ができます。そして、この行列と点や画素の位置(座標・ベクトル)との積を取ることで、点や画素を指定された基底ベクトルに従って移動させることが可能です。つまり、行列の積とは線形変換です。
具体的には行列の1列目と2列目にそれぞれ基底ベクトルを指定することが可能です。
下の式で考えると、(x, y)座標の点が基底ベクトルが変わることにより(X, Y)座標に移動することになります。変更後の基底ベクトルは\( \left ( \begin{array}{c} a \\ b \end {array} \right )\)と\( \left ( \begin{array}{c} c \\ d \end {array} \right ) \)になります。
画像では(x, y)座標の画素が(X, Y)座標に移動することになります。これを全画素に対して繰り返し行うことにより、指定された基底ベクトルに従って画像が変形することになります。
例えば基底ベクトルを\(\left ( \begin{array}{c} -1 \\ 0 \end {array} \right )\)と\( \left ( \begin{array}{c} 0 \\ 1 \end {array} \right ) \)とした場合は、線形変換に用いる行列は
$$ \left ( \begin{array}{cc} -1 &0 \\ 0 & 1 \end{array} \right ) $$
となり、これによりy軸を中心に反転した画像を得る事ができます。
また基底ベクトルを\(\left ( \begin{array}{c} 1 \\ 0 \end {array} \right )\)と\( \left ( \begin{array}{c} 0 \\ 2 \end {array} \right ) \)とした場合は、線形変換に用いる行列は
$$ \left ( \begin{array}{cc} 1 &0 \\ 0 & 2 \end{array} \right ) $$
となり、これにより縦方向に2倍に拡大した画像を得る事ができます。
線形変換まとめ
線形変換についての説明が長くなってしまったのでまとめておきます。
- 線形変換は座標を変形する
- 座標が変形するとデータ(画像や図形)も変形する
- 基底ベクトルを変更することで、座標を変形することが可能
- 基底ベクトルによってどのような形に座標を変形するかが決まる
- 行列の係数で基底ベクトルを指定し、その行列で積を取ることによって基底ベクトルが変わる
このように、行列で基底ベクトルを指定する事で画像を様々な形に変形する事が可能です。行列って面白いですよね!
この行列等の考え方は「線形代数」という学問で学ぶ事ができます。線形代数の知識は、特に画像を扱う際に必要になることも多く、線形代数の知識は持っておくと今後役に立つ可能性が高いです。
おすすめ参考書(PR)
ちなみに私の線形代数のオススメ書籍は下記の ゼロから学ぶ線形代数 です。私を線形代数好きにしてくれた本であり、「直感的に行列や線形代数について理解したい人」にオススメです!
平行移動とは
さてアフィン変換についての解説に戻ります。次はアフィン変換の一部である平行移動です。
平行移動はイメージしやすいですね。画像を指定されたベクトル分(mとn分)移動させる処理になります。
この平行移動と前述と線形変換を行うのがアフィン変換です。
アフィン変換の例
下記のページで紹介している画像の回転や画像の拡大縮小もアフィン変換の一例になります。
スポンサーリンク
画像の拡大縮小
画像の拡大縮小・リサイズの原理、アルゴリズムによる違いを解説!画像の拡大縮小(横方向にH倍・縦方向にV倍に拡大縮小)についてはアフィン変換において、a をH、d をVにし、そのほかのb、c、m、nを0としたものとして考えることができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} H & 0 \\ 0 & V \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
画像の回転
C言語で画像を回転画像の回転(\(\theta\)度で回転)についてはアフィン変換において、a を\( \cos \theta \)、b を\( – \sin \theta \)、c を\(\sin \theta \)、d を\( \cos \theta \)にし、m と n は0にしたものとして考えることができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} \cos \theta & – \sin \theta \\ \sin \theta & \cos \theta \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
画像のスキュー
C言語で画像のスキュー画像を水平方向に \( \phi \) 角度分スキューしたい場合、下記の行列演算により実現することができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} 1 & \tan \phi \\ 0 & 1 \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
また、垂直方向に\( \psi \) 角度分スキューしたい場合は、下記の行列演算により実現することができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} 1 & 0 \\ \tan \psi & 1 \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
これらはアフィン変換の例であり、行列のa, b, c, dに用いる値(正確に言うとa, b, c, dで指定される2つの基底ベクトル)によって様々な変形を行うことが可能です。
スポンサーリンク
アフィン変換プログラムの作り方
プログラムでは、下図のようにアフィン変換後画像の(X, Y)座標に対してループを行い、そのループの中で(X, Y)座標に対応する(アフィン変換する前の)元画像の座標(x, y)を求めながら処理を行っていきます。
つまり、座標(X, Y)から座標(x, y)を求める必要があります。これは行列演算で使用する行列の逆行列を用いて下記の式で実現することができます。
$$ \left ( \begin{array}{c} x \\ y \end{array} \right ) = A^{-1} \left ( \begin{array}{c} X \\ Y \end {array} \right ) – A^{-1} \left ( \begin{array}{c} m \\ n \end {array} \right ) $$
\( A \)はアフィン変換における線形変換部分を表す行列です。
$$ A = \left ( \begin{array}{cc} a & b \\ c & d \end{array} \right ) $$
さらに\( A^{-1} \)は行列\( A \)の逆行列です。\( A \)が2次元正方行列の場合、下記で求められます。
$$ A^{-1} = \frac{1}{ad – bc} \left ( \begin{array}{cc} d & -b \\ -c & a \end{array} \right ) $$
なので、アフィン変換のプログラムは下記のステップで作成することができます。
行列\( A \)と平行移動量mとnを決める
行列\( A \)の逆行列\( A^{-1} \)を求める
アフィン変換後座標(X, Y)からアフィン変換前座標(x, y)を求める
(必要に応じて補間を行う)
アフィン変換前座標(x, y)の画素値に基づいてアフィン変換後座標(X, Y)に画素値を代入
(画素の移動)
全てのアフィン変換後座標(X, Y)に画素値が代入されるまでSTEP.3に戻る
(STEP.3とSTEP.4を繰り返す)
アフィン変換のプログラム例
では画像をアフィン変換するプログラム例を紹介し、その説明を行っていきます。本プログラムでは「ファイル名」、行列の係数「a」「b」「c」「d」、平行移動量「m」「n」の7つのパラメータを引数で指定し、それに応じてアフィン変換を行うものになっています。
補間処理には最近傍補間法を使用します。
#include "myJpeg.h"
#include <string.h>
#define ARG_ERROR "ファイル名・行列の4要素A[0] - A[3]・横方向平行移動量・縦方向平行移動量の7つを引数に指定してください\n"
int main(int argc, char *argv[]){
BITMAPDATA_t bitmap, affinedBitmap;
int oX, oY; /* アフィン変換後画像の座標 */
int noX, noY; /* アフィン変換後画像の座標 (0, 0)基準 */
int ix, iy; /* 元画像の座標 */
double rx, ry; /* 元画像の座標(実数) */
int c;
double A[4]; /* 2x2行列 a:A[0],b:A[1],c:A[2],d:A[3] */
double iA[4]; /* Aの逆行列 */
int m; /* 横方向平行移動量 */
int n; /* 縦方向平行移動量 */
double det;
char outname[256];
/* 引数チェック */
if(argc != 8){
printf(ARG_ERROR);
return -1;
}
/* STEP.1 アフィン変換の行列と平行移動量の決定 */
/* a */
A[0] = atof(argv[2]);
/* b */
A[1] = atof(argv[3]);
/* c */
A[2] = atof(argv[4]);
/* d */
A[3] = atof(argv[5]);
/* m */
m = atof(argv[6]);
/* n */
n = atof(argv[7]);
/* STEP.2 逆行列の計算 */
det = A[0] * A[3] - A[1] * A[2];
if(det == 0) {
printf("divided by 0!!!\n");
return -1;
}
iA[0] = A[3] / det;
iA[1] = - A[1] / det;
iA[2] = - A[2] / det;
iA[3] = A[0] / det;
/* 入力JPEGファイル読み込み */
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
/* アフィン変換後の画像サイズを計算 */
affinedBitmap.width = bitmap.width;
affinedBitmap.height = bitmap.height;
affinedBitmap.ch = bitmap.ch;
/* アフィン変換後の画像データ用のメモリ領域確保 */
affinedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * affinedBitmap.width * affinedBitmap.height * affinedBitmap.ch);
if(affinedBitmap.data == NULL){
printf("malloc affinedBitmap error\n");
freeBitmapData(&bitmap);
return -1;
}
/* 全画素を白色に設定 */
memset(affinedBitmap.data, 0xFF, affinedBitmap.width * affinedBitmap.height * affinedBitmap.ch);
/* ここからSTEP.5(STEP.3とSTEP.4の繰り返し)処理 */
for(oY = 0; oY < affinedBitmap.height; oY++){
/* 原点0基準の値に変換 */
noY = oY - (int)affinedBitmap.height / 2;
for(oX = 0; oX < (int)affinedBitmap.width; oX++){
/* 原点0基準の値に変換 */
noX = oX - (int)affinedBitmap.width / 2;
/* 元画像における横方向座標を計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
rx = (double)noX * iA[0] + (double)noY * iA[1]
- (double)m * iA[0] - (double)n * iA[1] + (double)bitmap.width / 2;
/* 最近傍補間 */
ix= (int)(rx + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* アフィン変換後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(ix >= bitmap.width || ix < 0){
continue;
}
/* 元画像における縦方向座標を計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
ry = (double)noX * iA[2] + (double)noY * iA[3]
- (double)m * iA[2] - (double)n * iA[3] + (double)bitmap.height / 2;
/* 最近傍補間 */
iy = (int)(ry + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* アフィン変換後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(iy >= bitmap.height || iy < 0){
continue;
}
/* アフィン変換後画像の座標(om, on)に対応する元画像の座標(im, in)の画素値をコピー */
for(c = 0; c < affinedBitmap.ch; c++){
affinedBitmap.data[affinedBitmap.ch * (oX + oY * affinedBitmap.width) + c]
= bitmap.data[bitmap.ch * (ix + iy * bitmap.width) + c];
}
}
}
/* ここまで画像処理 */
sprintf(outname, "%s", "affined.jpg");
if(jpegFileEncodeWrite(&affinedBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
freeBitmapData(&affinedBitmap);
freeBitmapData(&bitmap);
return 0;
}
これらは下記ページで紹介したLibJPEGを用いた入力・出力関数になります。
【C言語】libjpegのインストールと使用方法・使用例行列の各要素と平行移動量の代入
アフィン変換プログラムの作り方のSTEP.1です。下記でアフィン変換に用いる行列の各要素の値と平行移動量の値を代入しています。
/* STEP.1 アフィン変換の行列と平行移動量の決定 */
/* a */
A[0] = atof(argv[2]);
/* b */
A[1] = atof(argv[3]);
/* c */
A[2] = atof(argv[4]);
/* d */
A[3] = atof(argv[5]);
/* m */
m = atof(argv[6]);
/* n */
n = atof(argv[7]);
スポンサーリンク
逆行列の計算
アフィン変換プログラムの作り方のSTEP.2です。下記で行列から逆行列の計算を行なっています。
/* STEP.2 逆行列の計算 */
det = A[0] * A[3] - A[1] * A[2];
if(det == 0) {
printf("divided by 0!!!\n");
return -1;
}
iA[0] = A[3] / det;
iA[1] = - A[1] / det;
iA[2] = - A[2] / det;
iA[3] = A[0] / det;
入力画像の座標計算
アフィン変換プログラムの作り方のSTEP.3です。STEP.2で計算した逆行列を用いて計算します。
/* 元画像における横方向座標を計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
rx = (double)noX * iA[0] + (double)noY * iA[1]
- (double)m * iA[0] - (double)n * iA[1] + (double)bitmap.width / 2;
/* 元画像における横方向座標を計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
ry = (double)noX * iA[2] + (double)noY * iA[3]
- (double)m * iA[2] - (double)n * iA[3] + (double)bitmap.height / 2;
画素の移動
アフィン変換プログラムの作り方のSTEP.4です。下記で画素値の代入を行なうことにより画素の移動を行なっています。
/* アフィン変換後画像の座標(om, on)に対応する元画像の座標(im, in)の画素値をコピー */
for(c = 0; c < affinedBitmap.ch; c++){
affinedBitmap.data[affinedBitmap.ch * (oX + oY * affinedBitmap.width) + c]
= bitmap.data[bitmap.ch * (ix + iy * bitmap.width) + c];
}
スポンサーリンク
プログラム実行結果
実行結果は下記の通りになります。回転・スキュー ・平行移動が行われていることが確認できると思います。
入力画像
実行コマンド
./affine.exe cat.jpg 0.5 -0.5 0.5 1 -100 0
出力画像
まとめ
アフィン変換とは線形変換+平行移動であり、どのような形に変形されるかは行列の係数(基底ベクトル)によって決まります。画像などの2次元データに対する線形変換には回転・拡大縮小などがあり、2次元行列を用いてアフィン変換を行う事ができます。アフィン変換は画像処理等においては重要な変換ですので是非プログラミングできるようになっておきましょう!