このページでは画像のスキューの原理とスキューを行うプログラムの紹介を行います。
Contents
スキューとは
画像のスキューとは、画像を歪ませ傾ける処理です。
入力画像が四角形であれば、スキューすることで画像は平行四辺形に変形します。
例えば平行方向に 30 度スキューさせた場合は下記のように画像が変形します。
また垂直方向に 15 度スキューさせた場合は下記のように画像が変形します。
スキューの原理
ではどのように処理をすれば、このスキュー処理が実現できるでしょうか?
次は、このスキューの原理を説明していきたいと思います。
スポンサーリンク
水平方向のスキュー
まず、水平方向へのスキューについて説明します。
下の図は、元画像と水平方向へ角度 θ
分スキューした画像の関係を示した図になります。
オレンジの枠が元画像を表しています。
スキュー後の画像は、元画像に対して x
方向のみ x'
ピクセル分シフトした画像になっていることが分かると思います。この x'
ピクセル分のシフト処理が、水平方向へのスキュー処理となります。
ただし、このシフト量である x'
は y
によって変わります。またスキューする角度 θ
によっても x'
は変わります。
では、この x'
は y
と θ
からどうやって求められるかを考えていきたいと思います。
まず、上の図を見ていただければ分かるように、スキューによって生じた画像の左側の余白は、底辺の長さが y
、高さが x'
、さらに斜辺と底辺との間の角度が θ
の直角三角形ですので、下記の式が成立することになります。
$$ \tan \theta = \frac{x’}{y} $$
これを x'
に対して解くと下記の式で x'
は求められることが分かります。
$$ x’ = y \tan \theta $$
つまり、θ
角度分スキューした画像の座標を (X
, Y
)、元画像の画像の座標を (x
, y
) とした時、(X
, Y
) は下記により求めることが可能です。
$$ X = x + y \tan \theta \\ Y = y $$
この式を行列演算で表せば、下記のように変更することができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} 1 & \tan \theta \\ 0 & 1 \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
つまり、元画像の全座標 (x
, y
) の画素を全て、上記の行列演算で求まる座標 (X
, Y
) に移動(コピー)すれば、水平方向のスキューを実現することができることになります。
ただし、実際にスキューのような画像処理を実装する際は、元画像の座標 (x
, y
) からスキュー後画像の座標 (X
, Y
) を求めると画質が低下する可能性があるため、スキュー後画像の座標 (X
, Y
) から元画像の座標 (x
, y
) を求めながら処理を行う方が良いです。
そのため、実際にスキューを実装する際には、まず上記の行列の逆行列を求め、その逆行列とスキュー後画像の座標 (X
, Y
) との積により元画像の座標 (x
, y
) を求めながら、座標 (x
, y
) の画素を (X
, Y
) に移動する処理を繰り返していくことになります。
具体的には、座標 (X
, Y
) に下記の行列演算で求まる座標 (x
, y
) の画素を移動していくことで、スキューを実現していくことになります。
$$ \left ( \begin{array}{c} x \\ y \end{array} \right ) = \left ( \begin{array}{cc} 1 & – \tan \theta \\ 0 & 1 \end{array} \right ) \left ( \begin{array}{c} X \\ Y \end {array} \right ) $$
なぜわざわざ逆行列を利用する必要があるのかについては下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
画像処理プログラミングにおける逆行列の重要性垂直方向のスキュー
続いて垂直方向へのスキューについて説明します。
下の図は、元画像と垂直方向へ φ
角度分スキューした画像の関係を示した図になります。
オレンジの枠が元画像を表しています。
スキュー後の画像は、元画像に対して y
方向のみ y'
ピクセル分シフトした画像になっていることが分かると思います。
この y'
ピクセル分のシフト処理が、垂直方向へのスキュー処理であると言えます。
ただし、このシフト量である y'
は x
およびスキューする角度 φ
によって変わります。
こちらも水平方向の時と同様に、y'
を x
と φ
から求める方法を考えていきます。
まず、上の図を見れば分かるように、スキューによって生じた画像の下側の余白は、底辺の長さが x
、高さが y'
、さらに斜辺と底辺との間の角度が φ
の直角三角形ですので、下記の式が成立することになります。
$$ \tan \phi = \frac{y’}{x} $$
これを y'
に対して解くと、下記の式で y'
は求められることが分かります。
$$ y’ = x \tan \phi $$
つまり、φ
角度分スキューした画像の座標を (X
, Y
)、元画像の画像の座標を (x
, y
) とした時、(X
, Y
))は下記により求めることが可能です。
$$ X = x \\ Y = x \tan \phi + y $$
この式を行列演算で表せば、下記のように変更することができます。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} 1 & 0 \\ \tan \phi & 1 \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
つまり、元画像の全座標 (x
, y
) の画素を全て、上記の行列演算で求まる座標 (X
, Y
) に移動(コピー)すれば、水平方向のスキューを実現することができることになります。
で、こちらも水平方向のスキュー同様に、実際にプログラミングを行う際には上記の行列の逆行列を用い、座標 (X
, Y
) に下記の行列演算で求まる座標 (x
, y
) の画素を移動していくことで、スキューを実現していくことになります。
$$ \left ( \begin{array}{c} x \\ y \end{array} \right ) = \left ( \begin{array}{cc} 1 & 0 \\ – \tan \phi & 1 \end{array} \right ) \left ( \begin{array}{c} X \\ Y \end {array} \right ) $$
水平方向のスキューのプログラム
では、ここまで説明してきた画像のスキューを行うプログラムの紹介をしていきたいと思います。
まずは水平方向のスキューのプログラムを紹介していきます。
スポンサーリンク
ソースコード
下記が画像の水平方向へのスキューを行うプログラムのソースコードとなります。
#include "myJpeg.h"
#include <math.h>
#include <string.h>
#define PI 3.14159
int main(int argc, char *argv[]){
BITMAPDATA_t bitmap, skewedBitmap;
int om, on; /* スキュー後画像の座標 */
int im, in; /* 元画像の座標 */
int nm, nn; /* スキュー後画像の座標を(0, 0)原点基準に変換した座標 */
int c;
int theta;
double originalm, originaln;
double rad_theta; /* 入力された角度をラジアンに変換したもの */
double a11, a12, a21, a22; /* 行列の各成分 */
char outname[256];
if(argc != 3){
printf("ファイル名とスキュー角度(-45 - 45)を引数に指定してください\n");
return -1;
}
theta = atoi(argv[2]);
if(theta > 45 || theta < -45){
printf("ファイル名とスキュー角度(-45 - 45)を引数に指定してください\n");
return -1;
}
/* 角度の単位をラジアンに変換 */
rad_theta = (double)theta * PI / (double)180;
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
/* スキュー後の画像サイズを計算(水平方向のスキューは横方向に画像が広がる) */
skewedBitmap.width = (int)((double)bitmap.width + (double)bitmap.height * fabs(tan(rad_theta)));
skewedBitmap.height = bitmap.height;
skewedBitmap.ch = bitmap.ch;
skewedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * skewedBitmap.width * skewedBitmap.height * skewedBitmap.ch);
if(skewedBitmap.data == NULL){
printf("malloc skewedBitmap error\n");
freeBitmapData(&bitmap);
return -1;
}
/* 全ての画素を白色にセット */
memset(skewedBitmap.data, 0xFF, skewedBitmap.width * skewedBitmap.height * skewedBitmap.ch);
/* 行列の各成分の設定 */
a11 = 1;
a12 = -tan(rad_theta);
a21 = 0;
a22 = 1;
/* ここから画像処理 */
for(on = 0; on < skewedBitmap.height; on++){
/* 原点0基準の値に変換 */
nn = on - (int)skewedBitmap.height / 2;
for(om = 0; om < (int)skewedBitmap.width; om++){
/* 原点0基準の値に変換 */
nm = om - (int)skewedBitmap.width / 2;
/* 元画像における横方向座標を行列演算により計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originalm =
(nm * a11 + nn * a12) + (int)bitmap.width / 2;
/*最近傍補間 */
im = (int)(originalm + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* スキュー後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(im >= bitmap.width || im < 0){
continue;
}
/* 元画像における横方向座標を行列演算により計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originaln =
(nm * a21 + nn * a22) + (int)bitmap.height / 2;
/*最近傍補間 */
in = (int)(originaln + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* スキュー後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(in >= bitmap.height || in < 0){
continue;
}
/* スキュー後画像の座標(om, on)に対応する元画像の座標(im, in)の画素値をコピー */
for(c = 0; c < skewedBitmap.ch; c++){
skewedBitmap.data[skewedBitmap.ch * (om + on * skewedBitmap.width) + c]
= bitmap.data[bitmap.ch * (im + in * bitmap.width) + c];
}
}
}
/* ここまで画像処理 */
sprintf(outname, "%s", "skewed.jpg");
if(jpegFileEncodeWrite(&skewedBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
freeBitmapData(&skewedBitmap);
freeBitmapData(&bitmap);
return 0;
}
ソースコードの説明
基本的にはコメントを参照していたければと思いますが、ポイントになる部分のみソースコードの解説を行なっておきます。
座標
スキュー後画像の座標を(om
, on
)、入力画像の座標を(im
, in
)としています。なのでスキューの原理の説明で用いた座標(X
, Y
)が(om
, on
)に、(x
, y
)が(im
, in
)に対応しています。
特に im
と in
を求める過程で様々な座標に関する変数を利用していますので注意してください。
入力 JPEG 画像の読み込みと保存
下記で引数で渡されたファイル名の JPEG ファイルを読み込み、さらにデコードした BITMAP
形式の画像データの先頭アドレスを bitmap.data
ポインタに指させています。
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
また下記で、スキュー後画像の JPEG エンコードおよびファイル保存を行なっています。
if(jpegFileEncodeWrite(&skewedBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
これらで用いている jpegFileReadDecode
関数と jpegFileEncodeWrite
関数は私の自作の関数で myJpeg.h
で定義し、myJpeg.c
で実装を行っています。
これらのファイルについては下記ページで説明およびソースコードを公開していますので必要に応じて参照していただければと思います。
【C言語】libjpegのインストールと使用方法・使用例行列の準備
下記でスキューを行う際に使用する行列の各成分の定義を行なっています。
/* 行列の各成分の設定 */
a11 = 1;
a12 = -tan(rad_theta);
a21 = 0;
a22 = 1;
これは、水平方向のスキュー の最後で紹介した行列の各成分を、1つ1つ変数に設定しているだけになります。
行列演算
さらに、下記でスキュー後画像の座標 (nm
, nn
) を入力画像の座標 (originalm
, originaln
) に変換しています。(nm
, nn
) は原点を画像の中心とした時の座標で、(originalm
, originaln
) は行列演算で求まった実数の座標、つまり整数に丸める前の座標になります)。
originalm =
(nm * a11 + nn * a12) + (int)bitmap.width / 2;
originaln =
(nm * a21 + nn * a22) + (int)bitmap.height / 2;
括弧内の演算が特に、行列の掛け算を行なって横方向と縦方向の座標を算出する部分になります。
補間処理
また、スキュー処理においても補間が必要ですので、最近傍補間により補間処理を行っています。
最近傍補間につきましては下記のページで説明&ソースコードを載せていますので参考にしてください。
C言語で画像の拡大縮小(最近傍補間編)このプログラムでは下記の部分で座標変換後の座標を最近傍補間で元画像に存在する座標へ補間している箇所になります。
/*最近傍補間 */
im = (int)(originalm + 0.5);
in = (int)(originaln + 0.5);
実行結果
実行ファイルを skew.exe
とした時、プログラムは下記のコマンドで実行できます。
第1引数には入力する JPEG 画像のファイルパスを、第2引数には水平方向にスキューする角度を指定します。
./skew.exe cat.jpg 30
第2引数には -45
〜 45
を指定できるようにしています。
プログラムを実行すると、カレントディレクトリの skew.jpg
に水平方向にスキューした結果の画像が保存されます(既に画像が存在する場合、上書きされることになるので注意してください)。
例えば入力画像が下の図のものである時、
プログラム実行後に保存される skew.jpg
は下の図のようなものになります。
スポンサーリンク
垂直方向のスキューのプログラム
次は、垂直方向に対する画像のスキューのプログラムを紹介していきます。
スポンサーリンク
ソースコード
下記が、画像の垂直方向へのスキューを行うプログラムのソースコードとなります。
ちなみに、垂直方向へのスキューを行うプログラムのソースコードは、ほぼ水平方向へのスキューのソースコードと変わりませんので(幅と高さの計算が違う・用意する行列が違う、くらい)、ソースコードの解説は省略させていただきます。
#include "myJpeg.h"
#include <math.h>
#include <string.h>
#define PI 3.14159
int main(int argc, char *argv[]){
BITMAPDATA_t bitmap, skewedBitmap;
int om, on; /* スキュー後画像の座標 */
int im, in; /* 元画像の座標 */
int nm, nn; /* スキュー後画像の座標を(0, 0)原点基準に変換した座標 */
int c;
int phai;
double originalm, originaln;
double rad_phai; /* 入力された角度をラジアンに変換したもの */
double a11, a12, a21, a22; /* 行列の各成分 */
char outname[256];
if(argc != 3){
printf("ファイル名とスキュー角度(-45 - 45)を引数に指定してください\n");
return -1;
}
phai = atoi(argv[2]);
if(phai > 45 || phai < -45){
printf("ファイル名とスキュー角度(-45 - 45)を引数に指定してください\n");
return -1;
}
/* 角度の単位をラジアンに変換 */
rad_phai = (double)phai * PI / (double)180;
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
/* スキュー後の画像サイズを計算(垂直方向のスキューは縦方向に画像が広がる) */
skewedBitmap.width = bitmap.width;
skewedBitmap.height = (int)((double)bitmap.height + (double)bitmap.width * fabs(tan(rad_phai)));
skewedBitmap.ch = bitmap.ch;
skewedBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * skewedBitmap.width * skewedBitmap.height * skewedBitmap.ch);
if(skewedBitmap.data == NULL){
printf("malloc skewedBitmap error\n");
freeBitmapData(&bitmap);
return -1;
}
/* 全ての画素を白色にセット */
memset(skewedBitmap.data, 0xFF, skewedBitmap.width * skewedBitmap.height * skewedBitmap.ch);
/* 行列の各成分の設定 */
a11 = 1;
a12 = 0;
a21 = -tan(rad_phai);;
a22 = 1;
/* ここから画像処理 */
for(on = 0; on < skewedBitmap.height; on++){
/* 原点0基準の値に変換 */
nn = on - (int)skewedBitmap.height / 2;
for(om = 0; om < (int)skewedBitmap.width; om++){
/* 原点0基準の値に変換 */
nm = om - (int)skewedBitmap.width / 2;
/* 元画像における横方向座標を行列演算により計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originalm =
(nm * a11 + nn * a12) + (int)bitmap.width / 2;
/*最近傍補間 */
im = (int)(originalm + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* スキュー後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(im >= bitmap.width || im < 0){
continue;
}
/* 元画像における横方向座標を行列演算により計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originaln =
(nm * a21 + nn * a22) + (int)bitmap.height / 2;
/*最近傍補間 */
in = (int)(originaln + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* スキュー後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(in >= bitmap.height || in < 0){
continue;
}
/* スキュー後画像の座標(om, on)に対応する元画像の座標(im, in)の画素値をコピー */
for(c = 0; c < skewedBitmap.ch; c++){
skewedBitmap.data[skewedBitmap.ch * (om + on * skewedBitmap.width) + c]
= bitmap.data[bitmap.ch * (im + in * bitmap.width) + c];
}
}
}
/* ここまで画像処理 */
sprintf(outname, "%s", "skewed.jpg");
if(jpegFileEncodeWrite(&skewedBitmap, outname) == -1){
printf("jpegFileEncodeWrite error\n");
freeBitmapData(&bitmap);
return -1;
}
freeBitmapData(&skewedBitmap);
freeBitmapData(&bitmap);
return 0;
}
実行結果
実行ファイルを skew.exe
とした時、プログラムは下記のコマンドで実行できます。
第1引数には入力する JPEG 画像のファイルパスを、第2引数には垂直方向にスキューする角度を指定します。
./skew.exe cat.jpg -15
プログラムを実行すると、カレントディレクトリの skew.jpg
に垂直方向にスキューした結果の画像が保存されます(既に画像が存在する場合、上書きされることになるので注意してください)。
例えば入力画像が下の図のものである時、
プログラム実行後に保存される skew.jpg
は下の図のようなものになります。
スポンサーリンク
まとめ
このページでは画像のスキューと、C言語で画像をスキューさせる方法およびプログラムについて解説しました。
スキューはそこまでメジャーなものではないかもしれませんが、画像を平行四辺形に変形することができ、結果画像を見るのが楽しくて私は結構好きです。
今回紹介したスキューに関しても、画像の回転や拡大縮小同様にアフィン変換の一種となります。そのアフィン変換についても下記ページで解説していますので、興味があればぜひ読んでみてください!
C言語で画像をアフィン変換