このページでは画像のスキューの原理とスキューを行うプログラムの紹介を行います。
スキューとは
画像のスキューとは、画像を歪ませ傾ける処理です。入力画像が四角形であれば、スキューするこことで画像は平行四辺形に変形します。
例えば平行方向に30度スキューさせた場合は下記のように画像が変形します。
また垂直方向に15度スキューさせた場合は下記のように画像が変形します。
スキューの原理
ではどのように処理をすればこのスキュー処理が実現できるでしょうか。この原理を説明していきたいと思います。
スポンサーリンク
水平方向のスキュー
まず水平方向へのスキューについて説明します。下の図は元画像と水平方向へθ角度分スキューした画像の関係を示した図になります。オレンジの枠が元画像を表しています。スキュー後の画像は、元画像に対してx方向のみx’ピクセル分シフトした画像になっていることが分かると思います。このx’ピクセル分のシフト処理が、水平方向へのスキュー処理となります。ただしこのシフト量であるx’はyによって変わります。またスキューする角度θによってもx’は変わります。
では、このx’はyとθからどうやって求められるかを考えていきたいと思います。上の図の関係より、下記の式が成立します。
$$ \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 ) $$
つまり、水平方向へのスキュー処理は、上記のように座標を変換する処理と言えます。
垂直方向のスキュー
続いて垂直方向へのスキューについて説明します。下の図は元画像と垂直方向へφ角度分スキューした画像の関係を示した図になります。オレンジの枠が元画像を表しています。スキュー後の画像は、元画像に対してy方向のみy’ピクセル分シフトした画像になっていることが分かると思います。このy’ピクセル分のシフト処理が、垂直方向へのスキュー処理となります。このシフト量であるy’はxおよびスキューする角度φによって変わります。
こちらも同様に、y’をxとφから求める方法を考えていきます。上の図の関係より、下記の式が成立します。
$$ \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 = x + y \tan \theta \\ Y = x \tan \phi + y $$
行列演算で表すと下記のようになります。
$$ \left ( \begin{array}{c} X \\ Y \end{array} \right ) = \left ( \begin{array}{cc} 1 & \tan \theta \\ \tan \phi & 1 \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$
水平方向および垂直方向へのスキュー処理は、上記のように座標を変換する処理と言えます。
プログラムでは、下図のようにスキュー後画像の(X, Y)座標に対してループを行い、そのループの中でその(X, Y)座標に対応する(スキューする前の)元画像の座標(x, y)を求めながら処理を行っていきます。
つまり、座標(X, Y)から座標(x, y)を求める必要があります。これは上の行列演算で使用する行列の逆行列を用いて下記の式で実現することができます。
$$ \left ( \begin{array}{c} x \\ y \end{array} \right ) = \frac{1}{1 – \tan \theta \tan \phi} \left ( \begin{array}{cc} 1 &- \tan \theta \\ – \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.14
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, phai;
double originalm, originaln;
double radt, radp; /* 入力された角度をラジアンに変換したもの */
double denominator; /* 1 / (1-tanθtanφ) */
double numeratort; /* tanθ */
double numeratorp; /* tanφ */
char outname[256];
if(argc != 4){
printf("ファイル名と回転角度(0 - 359)を2つ引数に指定してください\n");
return -1;
}
theta = atoi(argv[2]);
if(theta > 359 || theta < 0){
printf("ファイル名と回転角度(0 - 359)を2つ引数に指定してください\n");
return -1;
}
phai = atoi(argv[3]);
if(phai > 359 || phai < 0){
printf("ファイル名と回転角度(0 - 359)を2つ引数に指定してください\n");
return -1;
}
radt = (double)theta * PI / (double)180;
radp = (double)phai * PI / (double)180;
denominator = 1 - tan(radt) * tan(radp);
numeratort = tan(radt);
numeratorp = tan(radp);
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
/* スキュー後の画像サイズを計算 */
skewedBitmap.width = (int)((double)bitmap.width + (double)bitmap.height * tan(radt));
skewedBitmap.height = (int)((double)bitmap.height + (double)bitmap.width * tan(radp));
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);
/* ここから画像処理 */
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 =
((double)nm - (double)nn * numeratort) / denominator + (int)bitmap.width / 2;
/*最近傍補間 */
im = (int)(originalm + 0.5);
/* 元画像をはみ出る画素の場合は次の座標に飛ばす */
/* スキュー後画像は全画素のRGBが0xFFにmemsetされているので、飛ばされた画素は白色になる */
if(im >= bitmap.width || im < 0){
continue;
}
/* 元画像における横方向座標を計算 */
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originaln =
(-(double)nm * numeratorp + (double)nn) / denominator + (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)に対応しています。
スポンサーリンク
入力JPEG画像の読み込みとデコード
下記で引数で渡されたファイル名のファイルを読み込み、さらにデコードした BITMAP形式の画像データの先頭アドレスをbitmap.dataポインタに指させています。
if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
printf("jpegFileReadDecode error\n");
return -1;
}
この関数は私の自作の関数でmyJpeg.hで定義し、myJpeg.cで実装を行っています。これらについては下記で説明およびソースコードを公開していますので必要に応じて参照してください。

座標変換
下記でスキュー後画像の座標を入力画像の座標に変換しています。
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originalm =
((double)nm - (double)nn * numeratort) / denominator + (int)bitmap.width / 2;
/* 座標変換を行ってから原点(width / 2, height / 2)基準の値に変換 */
originaln =
(-(double)nm * numeratorp + (double)nn) / denominator + (int)bitmap.height / 2;
補間処理
また、スキュー処理においても補間が必要ですので、最近傍補間により補間処理を行っています。
最近傍補間につきましては下記のページで説明&ソースコードを載せていますので参考にしてください。

このプログラムでは下記の部分で座標変換後の座標を最近傍補間で元画像に存在する座標へ補間している箇所になります。
/*最近傍補間 */
im = (int)(originalm + 0.5);
in = (int)(originaln + 0.5);
スポンサーリンク
実行結果
実行ファイルをskew.exeとした時、プログラムは下記のコマンドで実行できます。第一引数には入力するJPEG画像のファイルパスを第二引数には水平方向にスキューする角度を、第三引数には垂直方向にスキューする角度を指定します。
./skew.exe cat.jpg 30 15
実行結果は次のようになります。
・入力画像
・出力画像
まとめ
このページでは画像のスキューと、C言語で画像をスキューさせる方法およびプログラムについて解説しました。θ や φ の値を変更すると色んな形に変わりますので、ぜひ色々試して楽しんでいただければと思います!