このページでは、画像の回転についての説明と、そのプログラムの例の紹介を行います。
紹介するプログラムを使えば、下の図のように画像を任意の角度で回転することができます。
それじゃあ早速画像の回転について解説していくよー
Contents
画像の回転
まず画像の回転とはどのような処理なのかについて説明します。
画像の回転とは
画像とは画素の集まりであることを下の記事で紹介しました。
画像データの構造・画素・ビットマップデータについて解説画像の回転は、この画素全てを「回転後の座標に移動させる処理」になります。
下の図で考えると、座標 (x
, y
) の画素を θ
度分回転させて座標 (x'
, y'
) に移動させる処理になります。
なので、下記を回転後の画像の全座標に対して行うことで、回転後の画像を得ることができます。
- 回転後の画像の座標 (
x'
,y'
) に対して、回転前の画像の座標 (x
,y
) の画素をコピー
プログラム的には、x'
と y'
の2重ループの中でこのコピーを行うことになります。
理屈はなんとなく分かるよ…
だけどこれを行うためには、回転前の画像の座標 (x
, y
) が分からないとダメだよね?
そうだね!
この回転前の画像の座標 (x
, y
) は、回転後の画像の座標 (x'
, y'
) と回転角度から求めることができるんだ
要は上記のように画素(の輝度値)をコピーすれば良いだけなのですが、このコピーを行うためには、回転後の画像の座標 (x'
, y'
) から回転前の画像の座標 (x
, y
) を求める必要があります。
この求め方を次に説明していきます。
スポンサーリンク
“回転後” の座標を求める
では、ここから回転後の座標から “回転前の座標” の求め方について解説していきます。
が、まずは回転前の座標から “回転後の座標” の求め方について説明し、その後に本題の「回転後の座標から “回転前の座標”」の求め方を解説していきたいと思います。
“回転後の座標” の求め方の方が解説しやすかったから…
あと、”回転後の座標” の求め方さえ分かれば、”回転前の座標” の求め方は簡単に導くことができるよ
最初にお見せした図を下記に再掲します。画像の回転とは、要は、座標 (x
, y
) の画素を θ
度分回転させて座標 (x'
, y'
) に移動させる処理でしたね!
この図にもう1つパラメータ φ
を追加して考えていきたいと思います。
各パラメータはそれぞれ下記を表しています。
O
:原点- 座標 (
x
,y
):回転前の座標 - 座標 (
x'
,y'
):回転後画像の座標 θ
:回転角度φ
:O - (x, y)
の直線とx
軸とがなす角r
:原点と座標 (x
,y
) との距離
この時、回転前の座標 x
と y
は φ
を用いてそれぞれ下の式で表すことができます。三角関数とか懐かしいですね!
x = r * cosφ
y = r * sinφ
さらに、回転後の座標 x'
と y'
は下記の式で表すことができます。
x' = r * cos(θ + φ)
y' = r * sin(θ + φ)
ここで、さらに下記の三角関数の加法定理を思い出しましょう。サイタコスモスコスモスサイタで覚えましたよね…?
sin(θ + φ) = sinθ * cosφ + cosθ * sinφ
cos(θ + φ) = cosθ * cosφ - sinθ * sinφ
つまり、x'
と y'
の計算式は加法定理を適用して下記のように変形することができます。
x' = r * cosθ * cosφ - r * sinθ * sinφ
y' = r * sinθ * cosφ + r * cosθ * sinφ
さらに x = r * cosφ
、y = r * sinφ
ですので、回転後座標 (x'
, y'
) は回転前座標 (x
, y
) を用いて下記の式により計算することができることになります。
x' = x * cosθ - y * sinθ
y' = x * sinθ + y * cosθ
つまり、(x
, y
) 座標の画素を角度 θ
で回転させれば、上式で求まる (x'
, y'
) 座標に移動します。
うわぁ…
三角関数とか超苦手…
だとしたら、画像の回転に興味を持ったのはむしろチャンスだよ!
教科書で単に公式を覚える時とは違って、
三角関数を用いる意義や効果を実感しながら楽しく学べるからね!
確かに…!
画像の回転楽しそうだから三角関数の意味も調べながらプログラミングしてみるよ!
“回転前” の座標を求める
続いて本題の “回転前” の座標 (x
, y
) を “回転後” の座標 (x'
, y'
) から求める方法について解説していきます。
“回転後” の座標 (x'
, y'
) は、前述の通り座標 (x
, y
) と回転角度 θ
を用いて下記の式で求めることができます。
x' = x * cosθ - y * sinθ
y' = x * sinθ + y * cosθ
この式は、下記の行列の積の形式で表すことも可能です。右辺の行列は「回転行列」とも呼ばれます。
$$ \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 ) $$
この式の両辺の左側に下記の行列を掛けてみましょう!この行列は上の式の行列の逆行列になります。
$$ \left ( \begin{array}{cc} \cos \theta & \sin \theta \\ – \sin \theta & \cos \theta \end{array} \right ) $$
行列の掛け算を行い、左辺と右辺を入れ替えたものが下記になります。
$$ \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 ) $$
で、これを解くと、下記の式を導くことができます。
x = x' * cosθ + y' * sinθ
y = -x' * sinθ + y' * cosθ
つまり、角度 θ
で回転した後の座標 (x'
, y'
) は、回転前には上式で求まる座標 (x
, y
) に存在していたことになります。
なので、回転後の画像の座標 (x'
, y'
) に、上式で求まる回転前の画像の座標 (x
, y
) の画素をコピーする、そしてそれを回転後の画像の全座標に対して繰り返すことにより、画像の回転を実現することができます。
今回は行列の重要性を知ってもらうためにも、あえて行列を使ってみた
足りない画素は補間で求める
前述の通り、回転前画像の座標 (x
, y
) は回転後画像の座標 (x'
, y'
) を用いて下記の式で求めることができます。
x = x' * cosθ + y' * sinθ
y = -x' * sinθ + y' * cosθ
さらに、画像の回転とはで説明した通り、回転後の画像の全座標に対して行うことで、回転後の画像を得ることができます。
- 回転後の画像の座標 (
x'
,y'
) に対して、回転前の画像の座標 (x
,y
) の画素をコピー
ただし、上の式で求まる x
と y
は整数になるとは限りません。一方で、画像の画素は整数の座標にのみ存在するため、存在しない画素ををコピーする必要があることになります。
じゃあダメじゃん
存在しないんだからコピーなんてできない…
このように存在しない画素は、周囲の画素から推測して補ってからコピーしてやる必要があります。このような存在しないデータを補うことを「補間」と呼びます。
下記では補間のアルゴリズムの1つである「最近傍補間」について解説していますし、
C言語で画像の拡大縮小(最近傍補間編)下記では補間のアルゴリズムの1つである「線形補間」について解説していますので、補間について詳しく知りたい方は是非読んでみてください。
C言語で画像の拡大縮小(線形補間編)スポンサーリンク
画像の回転プログラム
ではここまで解説してきた「画像の回転」をC言語でプログラミングするとどのようになるかについて解説していきたいと思います。
ソースコード
画像の回転を行うプログラムのソースコード例は下記のようになります。
#include "myJpeg.h"
#include <math.h>
#include <string.h>
int main(int argc, char *argv[]){
BITMAPDATA_t bitmap, rotatedBitmap;
int m, n, c;
int angle;
int m0, n0;
double originalm, originaln;
double rad;
char outname[256];
if(argc != 3){
printf("ファイル名と回転角度(0 - 359)を引数に指定してください\n");
return -1;
}
angle = atoi(argv[2]);
if(angle > 359 || angle < 0){
printf("ファイル名と回転角度(0 - 359)を引数に指定してください\n");
return -1;
}
/* 単位をラジアンへ変換 */
rad = (double)angle * M_PI / (double)180;
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
/* 回転後画像の情報設定 */
rotatedBitmap.width = bitmap.width;
rotatedBitmap.height = bitmap.height;
rotatedBitmap.ch = bitmap.ch;
if(rotatedBitmap.width == 0 || rotatedBitmap.height == 0){
printf("回転後の幅もしくは高さが0です\n");
freeBitmapData(&bitmap);
return -1;
}
/* 回転後画像用のメモリ確保 */
rotatedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);
if(rotatedBitmap.data == NULL){
printf("malloc rotatedBitmap error\n");
freeBitmapData(&bitmap);
return -1;
}
/* 事前に回転後画像の前画素を白色にしておく */
memset(rotatedBitmap.data, 0xFF, rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);
/* ここから画像の回転 */
for(n = 0; n < rotatedBitmap.height; n++){
for(m = 0; m < rotatedBitmap.width; m++){
/* 回転前画像の座標を算出 */
originalm =
(m - (int)rotatedBitmap.width / 2) * cos(rad) +
(n - (int)rotatedBitmap.height / 2) * sin(rad) + bitmap.width/ 2;
originaln =
- (m - (int)rotatedBitmap.width / 2) * sin(rad) +
(n - (int)rotatedBitmap.height / 2) * cos(rad) + bitmap.height / 2;
/* 一番近い座標を四捨五入で算出 */
m0 = originalm + 0.5;
n0 = originaln + 0.5;
/* 画像外にはみ出ている場合はコピーしない */
if(m0 >= bitmap.width || m0 < 0) continue;
if(n0 >= bitmap.height || n0 < 0) continue;
/* 最近傍補間した画素の輝度値をコピー */
for(c = 0; c < rotatedBitmap.ch; c++){
rotatedBitmap.data[rotatedBitmap.ch * (m + n * rotatedBitmap.width) + c]
= bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
}
}
}
/* ここまで画像の回転 */
sprintf(outname, "%s", "rotated.jpeg");
if(jpegFileEncodeWrite(&rotatedBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
freeBitmapData(&bitmap);
return 0;
}
ここまで解説を読んでくださった方であれば、変数名を下記のように対応づけて読むと分かりやすいかと思います。
m
,n
:x’, y’originalm
,originaln
:x, yangle
:θ
コンパイル
コンパイルを行う上で必要なソースコードファイルは下記の3つです。
main.c
:このページで紹介しているソースコードmyJpeg.c
:JPEG 読み込み・書き込み用のソースコードmyJpeg.h
:JPEG 読み込み・書き込みようのヘッダーファイル
myJpeg.c
と myJpeg.h
は下記ページで公開していますので、コピペして同じファイル名で保存して使用していただければと思います。
また JPEG の読み込みと書き込みを行うため、libjpeg
をインストールしておく必要があります。
libjpeg
をインストールしておくとC言語でJPEGファイルを使ったプログラムが簡単に作れるようになりますので興味があればインストールしておくことをオススメします。
この libjpeg
のインストール方法についても上記のページで紹介していますので、こちらを参考にしてインストールしていただければと思います。
gcc を用いたコマンドラインからのコンパイルは下記で行うことができます。
> gcc myJpeg.c -c > gcc main.c -c > gcc myJpeg.o main.o -ljpeg -o main.exe
–ljpeg
を付けることで libjpeg
ライブラリをリンクしています。
スポンサーリンク
実行
プログラムの実行は、コンパイルで生成した実行可能ファイル(main.exe
)を下記のように 2つの引数を指定して実行します。
./main.exe cat.jpeg 120
指定する引数は下記の2つになります。
- 第1引数:入力する JPEG ファイルへのパス
- 第2引数:回転角度
実行すると、回転後の画像が rotated.jpeg
という名前の JPEG ファイルとして保存されます。
プログラムの説明
紹介したソースコードでどのようなことを行なっているかを、ポイントになるところだけ説明していきたいと思います。
回転角度の取得
プログラム実行時に下記の2つの引数を受け取れるようにしています。
- 第1引数:入力する JPEG ファイルへのパス
- 第2引数:回転角度
引数で渡された回転角度は文字列のため、数値として扱えるように下記で整数型に変換しています。
angle = atof(argv[2]);
また、sin
関数と cos
関数の引数の単位はラジアン [rad] ですので、下記で入力された角度をラジアンへ変換しています。
rad = (double)angle * M_PI / (double)180;
M_PI
は math.h
で定義されている円周率の値になります。
入力 JPEG 画像の読み込みとデコード
下記で jpegFileReadDecode
関数を実行し、引数で渡されたパス JPEG ファイルを読み込みとデコードを行なっています。
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
デコード後の BITMAP データは BITMAPDATA_t
型の変数 bitmap
のメンバである data
ポインタが指すことになります。
この data
ポインタが指すアドレスのデータを回転することになります。
また、変数 bitmap
の各メンバには jpegFileReadDecode
関数内で読み込んだ JPEG 画像(元画像)の情報(幅や高さなど)が格納されます。
この jpegFileReadDecode
関数や BITMAPDATA_t
構造体については下記ページで説明していますので必要に応じて参照してください。
回転後の画像データのメモリ領域確保
下記で、回転後画像の幅と高さを設定しています。
rotatedBitmap.width = bitmap.width;
rotatedBitmap.height = bitmap.height;
単純に元画像(読み込んだ JPEG 画像)の幅・高さとを設定しているだけですので、回転することにより画像がはみ出る場合があります。
はみ出るのが嫌な場合は rotatedBitmap.width
と rotatedBitmap.height
の値をもっと大きな値に変更してみてください。角度を考慮して設定するより良いと思います!
さらに下記で、回転後画像を格納するのに必要なメモリを malloc
関数により取得し、rotatedBitmap.data
ポインタにそのメモリの先頭アドレスを指させています。
rotatedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * rotatedBitmap.width * rotatedBitmap.height * rotatedBitmap.ch);
回転後画像を格納するのに必要なメモリのサイズは「回転後画像の画素数 * 1画素あたりのサイズ」から計算することができます。
回転処理の際には、回転後の各画素のデータを、rotatedBitmap.data
が指すメモリに格納していきます。
回転処理
ここからいよいよ画像の回転を行なっていきます。
回転後の全画素&全色に対して回転前の画像の画素をコピーしていきますので、下記のループで繰り返し処理を行います。
for(n = 0; n < rotatedBitmap.height; n++){
for(m = 0; m < rotatedBitmap.width; m++){
for(c = 0; c < rotatedBitmap.ch; c++){
下記では回転小後の座標(m
, n
)が、元画像(回転前の画像)ではどの座標 (originalm
, originaln
) にあたるかを計算しています。
/* 回転前画像の座標を算出 */
originalm =
(m - (int)rotatedBitmap.width / 2) * cos(rad) +
(n - (int)rotatedBitmap.height / 2) * sin(rad) + bitmap.width/ 2;
originaln =
- (m - (int)rotatedBitmap.width / 2) * sin(rad) +
(n - (int)rotatedBitmap.height / 2) * cos(rad) + bitmap.height / 2
originalm
と originaln
は純粋に回転角度と回転後画像の座標 (m
, n
) から計算した元画像の座標になります。
rotatedBitmap.width / 2
、rotatedBitmap.height / 2
、bitmap.width/ 2
、bitmap.height/ 2
の足し算や引き算を行なっているのは、画像の中心を座標の原点 (0
, 0
) に移動させた状態で回転を行うためです。
プログラミング等で扱う画像は下図のように画像の (0
,0
) 座標は画像の左上になります。
一方で、ここまで解説してきた画像の回転は、画像の中心が原点 (0
,0
) に存在する場合のものになります。
ですので、上記の足し算を行うことで画像の中心を座標の中心に移動させた上で回転前の座標を計算するようにする必要があります。
また、元画像のこの座標に画素が存在しない場合があります。これは具体的には originalm
と originaln
が整数でない場合です。
この時は、前述で解説した通り補間を行う必要があります。行う補間は最近傍補間です。
最近傍補間では、この画素の輝度値を一番近い画素の輝度値と同じだろうと推測して補間処理を行います。
このために、下記で originalm
と originaln
の小数点未満を四捨五入し、元画像に画素が存在する座標 (m0
, n0
) を求めています。
/* 一番近い座標を四捨五入で算出 */
m0 = originalm + 0.5;
n0 = originaln + 0.5;
また、座標 (m0
, n0
) が回転前画像の外側に存在する場合は画素のコピーをすることができませんので、そのような場合は下記でコピーを行わず次の画素に対する処理にスキップするようにしています。
/* 画像外にはみ出ている場合はコピーしない */
if(m0 >= bitmap.width || m0 < 0) continue;
if(n0 >= bitmap.height || n0 < 0) continue;
以上により回転後の座標(m
, n
)へコピーを行う元画像の座標 (m0
, n0
) が決まりましたので、下記で実際にコピーを行なっています。
/* 最近傍補間した画素の輝度値をコピー */
for(c = 0; c < rotatedBitmap.ch; c++){
rotatedBitmap.data[rotatedBitmap.ch * (m + n * rotatedBitmap.width) + c]
= bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
}
c
に対するループの中で上記を実行するため、各色の輝度値がコピーされる、つまり画素がコピーされることになります。
あとはこれを回転後の画像の全画素に対して実行すれば良いだけです。
回転後画像データのJPEGエンコードとファイル作成
下記で jpegFileEncodeWrite
関数を実行し、回転後の画像を JPEG ファイルとして保存しています。
if(jpegFileEncodeWrite(&scaledBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
この jpegFileEncodeWrite
関数についても下記ページで説明していますので必要に応じて参照してください。
回転前後の画像
最後に紹介したプログラムを実行することでどのような結果が得られるのかを紹介しておきます。
プログラム実行時の第1引数に指定する JPEG ファイルは下図のようなものにしたいと思います。
さらに、回転角度を 120
として引数に指定した場合、下図のような回転後の画像が得られます。
スポンサーリンク
画像の回転方向
で、この回転後の画像を見て違和感を覚えた人もいるのではないでしょうか?
確かに!
画像が反時計回りに回転すると思ったら時計回りに回転してる!
そうだね
最初に解説したのとちょっと話が違うよね!
最後にこの理由と解決方法について解説するよ
このページの最初の画像の回転とはで解説したときは、回転角度は反時計回り方向を正方向として解説を行なってきました。
しかし、画像の回転プログラムで紹介したプログラムでは、実は “時計回り方向が正方向” として回転が行われます。
この理由はプログラミング等で扱う画像の座標と数学で習う座標が異なる点にあります。
数学で習う座標は縦方向の正方向が上になります。
一方で画像の座標は縦方向の正方向が下になります。
この違いがあるので、回転角度の正方向が時計回りになっています。
これが気になる方は、rad
の計算式を下記のように変更してみてください。
rad = - (double)angle * PI / (double)180;
これにより、引数で指定した回転角度に対して反時計回りに画像を回転することができます。
まとめ
このページでは「画像を回転する方法」とC言語における「画像の回転プログラム」について解説しました!
sin
や cos
・行列などでてきて戸惑った方もいるかもしれませんが、三角関数や行列は画像の変形を行うのではあれば知っておいた方が良い知識です。
プログラミングと一緒に学べば難しかった三角関数や行列も楽しく学ぶことができます!
苦手意識を持たず是非この機会に数学を利用したプログラミングにも挑戦してみてください!
丁寧な説明で初心者にも分かりやく、参考にさせてもらっています。
ところで、画像Rawデーターの型宣言でしょうか、
RAWDATA_t raw, rotatedRaw;
のところでエラーが出ます。
RAWDATA_tという変数が見つからないエラーです。
私の環境はMacOsX xcodeです。
ccdcmosさん
ご質問ありがとうございます。
また説明が不明瞭なところがあり申し訳ございません。
おそらくRAWDATA_t構造体の型宣言がうまく認識されていないのだと思います。
こちらのソースコードはRAWDATA_tの型宣言は下記ページのmyJpeg.hで行うことを前提としています。
https://daeudaeu.com/programming/c-language/libjpeg/#LibJPEG-5
以下を行えばおそらくエラーが消えるはずです。
・↑からmyJpeg.hのソースコードのコピペしてmyJpeg.hのファイルを作る
・さらに↑からmyJpeg.cのソースコードをコピペしてmyJpeg.cのファイルを作る
・main.cとmyJpeg.cをコンパイルし、コンパイル結果とlibjpegをリンクして実行ファイル生成
ターミナルでなら下記のコマンドで実行ファイルが生成されます。
gcc main.c -c
gcc myJpeg.c -c
gcc main.o myJpeg.o -ljpeg -o rotation.exe
ただし、myJpeg.hとmyJpeg.cはlibjpegを使用することを前提としています。libjpegを使用してJPEGファイルの読み込み等を行うのであれば、こちらも下記ページでインストール手順を紹介していますので参考にしてください。MacOSであれば同様の手順でインストールすることができると思います。
https://daeudaeu.com/programming/c-language/libjpeg/
正直XCodeに関してはあまり使用したことがないため勉強不足なのですが、libjpegをXCodeで使用する方法も上記ページの最後に追記してみましたので、こちらも参考にしてください。
もし質問内容が解決出来ないようであればまたコメントしていただければと思います。
お手数おかけしますがよろしくお願いいたします。