C言語による画像回転処理について

新規作成:2000/08/14
最終更新:2001/02/06
酒井理雄(TSUGU)

はじめに

ここでは、私、酒井理雄(TSUGU)が卒業研究において作成したDIB画像の回転処理プログラムのアルゴリズムについて解説をしたいと思います。
ご意見やご感想、また、ここおかしいんじゃない?というような所があれば連絡していただけると作者が喜びます。

回転処理の基本
出力画像のサイズ決定
基本アルゴリズムの最適化
基本アルゴリズムにおけるサンプルプログラム
線形補間を掛けてみる
線形補間の最適化
線形補間を用いたサンプルプログラム
基本アルゴリズムのさらなる最適化
参考文献

TOP

 

回転処理の基本
通常、回転をする場合入力点(x1,y1)を、原点(0,0)を中心としてA度回転させる場合の出力点の座標(x2,y2)は
x2 = x1 * cos(-A) - y1 * sin(-A)
y2 = x1 * sin(-A) + y1 * cos(-A)
で表されます。
この式を使って画像を回転すると、画像の左上を中心とした回転となります。これを画像の中心(cx,cy)を回転の中心とするときの変換式は次のようになります。
x2 = (x1-cx) * cos(-A) - (y1-cy) * sin(-A) + cx
y2 = (x1-cx) * sin(-A) + (y1-cy) * cos(-A) + cy

これで画像の中心を原点とした回転ができるようになりますが、これを実行してみると、下の図のように変換後の画像に穴があいてしまいます。


サンプル画像

回転処理後画像(角度:10度)

これは、下図の左のように入力画像の位置から出力画像内の座標を計算しているために生じるものです。この現象を回避するには、入力画像の座標から出力座標を計算するのではなく、下図の右のように出力画像の座標に対応する入力画像の座標を逆計算してやればよいことが分かります。

このときに用いる計算式は

x1 = (x2-cx) * cos(A) - (y2-cy) * sin(A) + cx
y1 = (x2-cx) * sin(A) + (y2-cy) * cos(A) + cy
となり、実際の画像処理においてはこの式を用いて回転処理を行います。 以下このアルゴリズムを「基本アルゴリズム」と呼ぶこととします。
この処理結果を下図に示します。

修正回転処理後画像(角度:10度)

2003/08/26 元となる変換式を修正しました(kiki氏に感謝)

TOP

 

出力画像のサイズ決定
回転処理において処理結果に入力画像すべてが収まるようにするためには出力画像の大きさを計算する必要があります。
幅:sw、高さ:shの画像をA度回転させたときの出力画像の幅(dw)、高さ(dh)は次のように計算されます。
( fabs() : math.hで定義されている浮動小数の絶対値を得る標準関数 )
dw = fabs( sw * cos(A) ) + fabs( sh * sin(A) )
dh = fabs( sw * sin(A) ) + fabs( sh * cos(A) )
実際のプログラム上では幅高さは整数でないといけないので、通常次のように四捨五入をして結果を求めます。
int dw = (int)( fabs( sw * cos(A) ) + fabs( sh * sin(A) ) + 0.5 )
int dh = (int)( fabs( sw * sin(A) ) + fabs( sh * cos(A) ) + 0.5 )

これで整数型の変数dw,dhに出力画像の幅高さが格納されます。

TOP

 

基本アルゴリズムの最適化
上で説明したプログラムを組むと処理がとてつもなく遅くなります。原因は
1.ループ内でsin,cos値を計算している
2.浮動小数点を使っている
となります。
を回避するには簡単で、プログラム上で使うsin,cosの値は回転角度をA度とすると、sin(A)とcos(A)しかないのでループの外側であらかじめ計算しておけばよいことが分かります。
問題はですが、私はで計算したsin,cos値に1024を掛けて無理矢理整数にしてしまう方法を採りました。プログラムにすると次のようになります。
int int_sin = (int)( sin(A) * 1024 );
int int_cos = (int)( cos(A) * 1024 );
これで時間のかかる浮動小数演算を行わなくても良いようになりました。ここで1000ではなく1024を掛けたのは、この値を使うときには元の値に戻してやる必要があり、1000を掛けた場合は計算結果を1000で割る必要があるため処理が遅くなってしまいます(といっても浮動小数演算よりは速い)。一方1024を掛けた場合は割り算ではなく計算結果を10ビット右にシフトしてやるだけでよいので高速化が図れます。
この方法は最初に説明した「出力画像の座標に対応する入力画像の座標を逆計算する」時に使います。 最初に示した座標逆変換式
x1 = (x2-cx) * cos(A) - (y2-cy) * sin(A) + cx
y1 = (x2-cx) * sin(A) + (y2-cy) * cos(A) + cy
を上記のように最適化すると次のようになります。

int int_sin = (int)( sin(A) * 1024 );
int int_cos = (int)( cos(A) * 1024 );
x1 = (((x2-cx) * int_cos - (y2-cy) * int_sin) >> 10) + cx;
y1 = (((x2-cx) * int_sin + (y2-cy) * int_cos) >> 10) + cy;

この変更を加える前は上のサンプル画像(320x240グレイスケール)を10度回転させるのに160ms程度かかっていたのが、変更後は5ms位で処理が完了しました。30倍もの高速化ですね(^^。もちろん画像の劣化などはありません。
(CPU:PentiumIII 550MHz, MEMORY:256MBで確認)

2000/08/31 変換式がおかしかったのを修正しました

TOP

 

基本アルゴリズムにおけるサンプルプログラム

ここまでに説明した処理を実際にプログラムにすると以下のようになります。

//################ 回転処理サンプル ###############
//  pSrcImg   : 入力画像
//  pDstImg   : 出力画像
//  SrcWidth  : 入力画像幅
//  SrcHeight : 入力画像高さ
//  A         : 角度(ラジアン)
//(注):このプログラムはグレイスケール画像専用です
//#################################################

double ValSin, ValCos;    //指定角度のSIN,COS値
int x1, y1;               //入力画像内の位置
int x2, y2;               //出力画像内の位置
int DstWidth, DstHeight;  //出力画像の幅高さ
int SrcCX, SrcCY;         //入力画像の中心座標
int DstCX, DstCY;         //出力画像の中心座標
int int_cos, int_sin;     //指定角度のSIN,COS値の整数値

//SIN,COS値を格納
ValCos = cos(A);
ValSin = sin(A);

//出力画像の幅高さを計算
DstWidth  = (int)( fabs(SrcWidth*ValCos) + fabs(SrcHeight*ValSin) + 0.5 );
DstHeight = (int)( fabs(SrcWidth*ValSin) + fabs(SrcHeight*ValCos) + 0.5 );
//出力画像を入力画像サイズに合わせたいときは上2行を次のように変更
// DstWidth  = SrcWidth;
// DstHeight = SrcHeight;


//出力画像用のメモリ領域を確保する
//このCreateImage関数は自作してください

pDstImg = CreateImage(DstWidth, DstHeight);

//入出力画像の中心座標を計算する
SrcCX = SrcWidth / 2;
SrcCY = SrcHeight / 2;
DstCX = DstWidth / 2;
DstCY = DstHeight / 2;

//SIN,COS値を整数値に変換
int_cos = (int)(ValCos * 1024);
int_sin = (int)(ValSin * 1024);

//画像回転処理メインルーチン
//出力画像の座標でループを行う

for(y2=0; y2 < DstHeight; y2++) {
    for(x2=0; x2 < DstWidth; x2++) {
        //出力画像座標(x2,y2)から入力画像の座標(x1,y1)を逆計算する
        x1 = (((x2-DstCX)*int_cos - (y2-DstCY)*int_sin) >> 10) + SrcCX;
        y1 = (((x2-DstCX)*int_sin + (y2-DstCY)*int_cos) >> 10) + SrcCY;

        //x1, y1がともに入力画像の有効範囲にあれば出力へコピーを行う
        if( x1 >= 0  &&  x1 < SrcWidth  &&  y1 >= 0  &&  y1 < SrcHeight ) {
            pDstImg[y2][x2] = pSrcImg[y1][x1];
        }
    }
}
//######### 画像回転処理サンプルここまで ##########

TOP

 

線形補間を掛けてみる

今までやったアルゴリズムでは出力画像はエッジの目立った汚い画像となってしまいます。そこで線形補間という呼ばれる補間を掛けてみます。
線形補間とは、点と点の間を線で結ぶ補間方式で下の図のようになります。

この図のP1とP2の間の値はDを移動させて求めます。このときの計算式は次のようになります。

D = (1-d)*P1 + d*P2

この式で線形補間を行えるのですが、画像の場合下の図のように2次元の補間になります。

この図においてC0,C1,C2,C3は入力画像の濃度値、求めたい濃度値をDとすると、Dの値は次のような式で表されます。
D = (1-dy) * ((1-dx)*C0 + dx*C1) + dy * ((1-dx)*C2 + dx*C3)
この補間を掛けた回転処理の出力画像を下図に示します。
線形補間を加えた回転結果(角度10度)

TOP

 

線形補間の最適化
上記の線形補間を掛けると出力画像は大変きれいになりますが、補間時の点と点の間の距離を0から1の浮動小数点を扱っているので処理が極端に遅くなってしまいます。そこで2点間の距離を256分割して0〜255の値とすることで高速化を図りたいと思います。
このとき線形補間の式
D = (1-d)*P1 + d*P2
は次の式のようになります
D = ((255-d)/256)*P1 + (d/256)*P2
ただしこのままだとd/256で常に0になってしまい補間ができないので次のように式を変形します。
D = (P1*(255-d))/256 + (P2*d)/256

これで整数値で線形補間ができるようになりましたが、この式もかけ算割り算を多用するので処理が遅くなるためルックアップテーブルを用いて演算します。
ルックアップテーブルとは処理に時間のかかる計算をあらかじめ計算して配列に格納しておくデータのことで、その計算結果が必要なときは配列にアクセスするだけで良いようになります。
作成するルックアップテーブルはP1*(255-d)/256(P2*d)/256の部分で、P1(255-d)、またはP2dの値を用いて参照するようにします。このときに必要なルックアップテーブルを作成するプログラムを以下に示します。

unsigned char pluTBL[256*256];
int p, d, i;
for(i=p=0; p < 256; p++) {
    for(d=0; d < 256; d++, i++) {
        pluTBL[i] = (p*d) / 256;
    }
}

このプログラムで作成したルックアップテーブル(pluTBL)を使っての1次補間の仕方は次のようになります。

D = pluTBL[(P1<<8)+(255-d)] + pluTBL[(p2<<8)+d];

これは1次元補間であるため画像処理で扱えるよう2次補間の形にしたプログラムは次のようになります。

unsigned char D;       //出力濃度
unsigned char Top;     //上端1次補間結果
unsigned char Bottom;  //下端1次補間結果
Top = pluTBL[(C0<<8)+(255-dx)] + pluTBL[(C1<<8)+dx];
Bottom = pluTBL[(C2<<8)+(255-dx)] + pluTBL[(C3<<8)+dx];
D = pluTBL[(Top<<8)+(255-dy)] + pluTBL[(Bottom<<8)+dy];

このプログラムの各変数は次の図のようになっています。

2000/08/21 ルックアップテーブルの説明を修正しました

TOP

 

線形補間を用いたサンプルプログラム

線形補間の最適化で2点間の距離を256にすると書きましたが、これを実際のプログラムで実現しようと思います。
基本アルゴリズムの最適化のところでSIN,COSの値に1024を掛けて整数値に変換して処理をしました。 これを用いて計算します。
サンプルプログラムを以下に示します。

//dx, dy           : 求めたい2点間の距離
//int_sin, int_cos : SIN,COSの値に1024を掛けた値
//x1, y1           : 入力画像位置に1024を掛けた値
//x2, y2           : 出力画像位置
//SrcCX, SrcCY     : 入力画像中心座標
//DstCX, DstCY     : 出力画像中心座標
x1 = ((x2-DstCX) * int_cos - (y2-DstCY) * int_sin) + (SrcCX << 10);
y1 = ((x2-DstCX) * int_sin + (y2-DstCY) * int_cos) + (SrcCY << 10);
dx = (x1 >> 2) - ((x1 >> 10) << 8);
dy = (y1 >> 2) - ((y1 >> 10) << 8);

これで線形補間に用いる2点間の距離を計算できますがもう少し簡単化できます。そのリストを以下に示します。

x1 = ((x2-DstCX) * int_cos - (y2-DstCY) * int_sin) + (SrcCX << 10);
y1 = ((x2-DstCX) * int_sin + (y2-DstCY) * int_cos) + (SrcCY << 10);
dx = (x1 - (x1 & 0xfffffc00)) >> 2;
dy = (y1 - (y1 & 0xfffffc00)) >> 2;

これらを用いた線形補間を用いた回転のサンプルプログラムを以下に示します(メインルーチン部分だけ)。

// pSrcImg             : 入力画像
// pDstImg             : 出力画像
// SrcWidth, SrcHeight : 入力画像幅高さ
// DstWidth, DstHeight : 出力画像幅高さ
// A                   : 角度(ラジアン)
// ValSin, ValCos      : 指定角度のSIN,COS値
// x, y                : 入力画像内の位置に1024を掛けた値
// x1, y1              : 入力画像内の位置
// x2, y2              : 出力画像内の位置
// SrcCX, SrcCY        : 入力画像の中心座標
// DstCX, DstCY        : 出力画像の中心座標
// int_cos, int_sin    : 指定角度のSIN,COS値の整数値


//2次元線形補間用マクロ
#define SET_LINER_BYTE(D, dx, dy, C0, C1, C2, C3) {\
    unsigned char Top, char Bottom;\
    Top = pluTBL[(C0<<8)+(255-dx)] + pluTBL[(C1<<8)+dx];\
    Bottom = pluTBL[(C2<<8)+(255-dx)] + pluTBL[(C3<<8)+dx];\
    D = pluTBL[(Top<<8)+(255-dy)] + pluTBL[(Bottom<<8)+dy];\
}

//画像回転処理メインルーチン
//出力画像の座標でループを行う

for(y2=0; y2 < DstHeight; y2++) {
    for(x2=0; x2 < DstWidth; x2++) {
        //入力画像の位置に1024を掛けた値を計算
        x = ((x2-DstCX) * int_cos - (y2-DstCY) * int_sin) + (SrcCX << 10);
        y = ((x2-DstCX) * int_sin + (y2-DstCY) * int_cos) + (SrcCY << 10);

        //入力画像位置を計算
        x1 = x >> 10;
        y1 = y >> 10;

        //x1, y1がともに入力画像の有効範囲にあれば出力へコピーを行う
        if( x1 >= 0  &&  x1 < SrcWidth-1  &&  y1 >= 0  &&  y1 < SrcHeight-1 ) {
            //2点間の距離を計算
            dx = (x - (x & 0xfffffc00)) >> 2;
            dy = (y - (y & 0xfffffc00)) >> 2;

            //線形補間を掛けてその結果をpDstImg[y2][x2]へ格納する
            SET_LINER_BYTE(pDstImg[y2][x2], dx, dy,
                           pSrcImg[y1][x1], pSrcImg[y1][x1+1],
                           pSrcImg[y1+1][x1], pSrcImg[y1+1][x1+1]);
        }
    }
}

このプログラムでは画像の端のエッジを取り除くことはできません。これを回避するためには条件判定文が追加で必要です。各自考えてみてください。

2000/08/21 サンプルプログラム中のバグを修正しました。

TOP

 

基本アルゴリズムのさらなる最適化
これまでは出力画像の位置から入力画像の位置を逆計算していましたが、この計算はサンプルプログラムを見て分かるとおり、掛け算を使った複雑な計算になってしまっています。ここでは入力画像内の位置を足し算だけで求める方法を説明します。
下の図がこのアルゴリズムの考え方で、最初から回転の角度に応じた形で入力画像内を横断するように処理を行います。

この時入力画像内の座標を求めるときは入力画像内のXY座標の増分を足すだけなので足し算2回で求められます。
ここで問題になるのは入力画像内の走査開始点の座標計算とXY座標の増分値の決定です。
走査開始点は入力画像の中心点SrcCX,SrcCY、出力画像の中心点をDstCX,DstCY、回転角をAとすると次の式で求められます。
StX = cos(A)*(-DstCX) - sin(A)*(-DstCY) + SrcCX
StY = sin(A)*(-DstCX) + cos(A)*(-DstCY) + SrcCY
XY座標の増分はXループ内の増分とYループ内の増分の二つあります。
Xループ内のXYの増分は次の式から求められます。
DXX = cos(A)
DXY = sin(A)
同様にYループ内のXYの増分は次の式になります。
DYX = cos(A+PI/2) = sin(A)
DYY = sin(A+PI/2) = cos(A)
これでこのアルゴリズムを使う前準備が終了しました。
さっそくこのアルゴリズムのサンプルプログラムを下に示します。実際の処理はSIN,COS値をINTに変換するので多少複雑になりますが、やっていることは同じです。

// SrcCX, SrcCY        : 入力画像の中心座標
// DstCX, DstCY        : 出力画像の中心座標
// SrcWidth, SrcHeight : 入力画像の幅高さ
// DstWidth, DstHeight : 出力画像の幅高さ
// pSrcImg             : 入力画像ポインタ
// pDstImg             : 出力画像ポインタ
// A                   : 回転角度(ラジアン)
// PI                  : 円周率(3.14159265358979323846)

int int_sin;          //SIN値に1024を掛けて整数にした値
int int_cos;          //COS値に1024を掛けて整数にした値
int x1, y1;           //入力画像内の座標
int x2, y2;           //出力画像内の座標
int x1_mult, y1_mult; //入力画像内走査用変数(1024を掛けた値)
int StX, StY;         //入力画像内の走査開始点座標
int DXX, DXY;         //入力画像走査時のXループ内の増分
int DYX, DYY;         //入力画像走査時のYループ内の増分

//SIN,COS値を整数に変換
int_sin = (int)( sin(A) * 1024 );
int_cos = (int)( cos(A) * 1024 );
//走査開始点の計算
StX = int_cos * (-DstCX) - int_sin * (-DstCY) + (SrcCY<<10);
StY = int_sin * (-DstCX) + int_cos * (-DstCY) + (RrcCY<<10);
//Xループ内の増分を計算
DXX = int_cos;
DXY = int_sin;
//Yループ内の増分を計算
//DYX = (int)( cos(A + PI/2) * 1024 );
//DYY = (int)( sin(A + PI/2) * 1024 );
DYX = int_sin;
DYY = int_cos;

//回転処理メインルーチン
//出力画像の座標でループを行う

for(y2=0; y2 < DstHeiht; y2++) {
    //走査用変数の初期化
    x1_mult = StX + y2*DYX;
    y1_mult = StY + y2*DYY;

    //走査用変数にXループ内の増分値を加算
    for(x2=0; x2 < DstWidth; x2++, x1_mult+=DXX, y1_mult+=DXY) {

        //走査用変数から入力画像内の座標を取得
        x1 = x1_mult >> 10;
        y1 = y1_mult >> 10;

        //入力画像内の座標が有効な値ならコピーを行う
        if( x1 >= 0  &&  x1 < SrcWidth  &&  y1 >= 0  &&  y1 < SrcHeight ) {
            pDstImg[y2][x2] = pSrcImg[y1][x1];
        }
    }
}

このアルゴリズムを用いれば、線形補間を用いたサンプルプログラムで示したプログラムも多少処理が速くなります。

2001/02/06 走査開始点の計算式とそのプログラムがおかしかったのを修正しました。
2001/02/06 Yループの増分値の計算を改良しました

TOP

 

参考文献

C Magazine 1998年7月号「グラフィックエフェクト」

TOP

戻る

Counter