2007.10.21.
石立 喬
Visual C++ 2005 Express Edition を用いた易しい画像処理(17)
―――― 画像を回転し、拡大する ――――
概要
画像処理で残っていた話題で、画像の回転がある。画像の回転は、既知のデータからの補間と拡大によって行う。
画像の回転の必要性
カメラなどで撮影した画像は、傾いている場合がある。これを、画像を見ながら任意の角度だけ回転できると便利である。画像を回転すると、以前と同様なサイズでは、データの欠如する部分が生じるので、画像を自動的に拡大して、それを防止する。
画像の回転方法
図1で、太い黒で表したのは回転後の画像の座標である。P点を、回転後の座標(画像の中心から測った)で見た位置を(i,j)とし、回転前の座標(赤で表示)で見た位置を(p,q)とすると、図中の式で示すような関係が得られる。図では、回転前の画像から見て時計方向に角度θだけ画像を回転させた場合を示しているが、θがマイナスになっても、式はそのまま使用できる。i、jは整数であるが、p、qは一般に実数である。
画像の回転とは、回転後の画像の座標位置から回転前の原画像の座標位置を逆算し、その場所の色情報で回転後の画像の座標位置に点を打つことである。
図1 回転のための座標変換
画像の拡大
画像を回転させて、回転前と同じ大きさで画像を表示しようとすると、画像データの欠ける部分が生じる。図2の左の図で、黒は回転後の画像の範囲、赤は回転前の原画像を示す。ここでは、原画像も回転後の画像も同じサイズで示してある。原画像を、画像の中心を固定してθだけ回転すると(図の場合は反時計方向に)、回転後の画像には、原画像データのない部分が生じる。緑色で示した部分は、原画像の形状と相似であり、かつデータが存在する範囲である。 したがって、緑色の範囲を黒の範囲に拡大する操作を行なえば、欠陥部分を見かけ上で無くすることができる。
拡大倍率の計算は、下の右の図を参照して欲しい。右の図は、左の図の一部分を取り出したもので、左の図の赤い画像(すなわち原画像)の対角線の長さの半分を1とし、緑の画像範囲(すなわち、欠陥部分が無く元の画像と相似形)の対角線の半分をxとしている。このxを求めて、逆数を取れば拡大倍率が得られる。xは1より小さい値であり、回転画像は、原画像の、より狭い範囲を、元のサイズに対応させるので、結局、1/xの倍率で拡大されることになる。
回転角θには、正(反時計方向)と負(時計方向)があるが、図の左の図から想像できるように、左右を反転しただけであるので、式の形は変わらない。したがって、実際の計算ではθの絶対値を取れば良い。
図2 画像の拡大の必要性と、必要な拡大倍率
最終的な座標変換の式
以上の結果から、最終的に使用する座標変換の式は、下記の通りになる。width/2とheight/2は、画像の中心点を表す。画像を拡大するには、原画像の座標に換算する際に拡大倍率zoomで割れば良い。

画像データの補間
画像回転の式により、回転後の画像の座標(i,j)から、原画像の座標(p,q)を求めると、p、qは一般には整数とならない。整数以外の場所には画像データ(ピクセル値)が存在しないので、図3に示すように、座標(p,q)の画像データを求めるために、周辺データからの補間を使用する。ただし、pp=p-int(p)、qq=q-int(q)
であり、それぞれ1未満の実数値である。
point[0]は((int)p,(int)q)の点を表し、point[1]は((int)p+1,(int)q)を表す。point[2]やpoiunt[3]についても同様で、図に示してある。
red[0]は、原画像の点point[0]の赤の値であり、red[1]なども同様である。これらの値を比例配分で求めたのが補間値rである。
図3 実数値で与えられた場所のデータを補間で求める方法
プログラムで使用した主な変数の説明
width ----- ファイルから原画像bmap_srcを読み込んだ後に、bmap_src->Widthで求めた画像の幅
height ---- 同様に、bmap_src->Heightで求めた画像の高さ
i,j -------- 回転後の画像の注目点Pの座標(回転後画像の中心点を基準とした)
p,q ------- i,jを原画像へ換算した点Pの座標(原画像の中心点を基準とした)
pp,qq ----- p,qは一般には整数でないので、pp=p-(int)p、qq=q-(int)qで求めた端数値
alpha ----- arctan(width/height) で求める画像の縦横比固有の角度(ラジアン)
zoom ----- 拡大倍率
theta ----- 回転角(度)
point[0],point[1],point[2],point[3] ---- 点P(p,q)を囲む4点
red[k],green[k],blue[k] ---- 点P(p,q)を取り巻く周辺4点(k=0〜3)の各RGB値
r,g,b ----- 補間によって求めたP点のRGB値
プログラムの説明
◎Form1_Load
ここでは、原画像が読み込まれる前には、メニューの「上書き保存」と「名前を付けて保存」を無効にし、ラジオボタンの初期設定を「5度/クリック」にしておく。また、原画像が読み込まれたことを示すフラグimage_flagをfalseにしておく。
◎Form1_Paint
1) 最初にこのプログラムを実行した時は、image_flag(原画像を読み込むとtrue)がfalseのため、何もしない。
2) 原画像を読み込んでimage_flagがtrueになると、下記の処理を実行する。
・原画像のサイズに従って、Form1のClientSize(ユーザが使用する範囲)を設定する。横幅があまり小さいと、メニューやラジオボタンの表示ができないので、幅の最小を590にする。
・原画像のサイズ(width,height)から、alphaとzoomを計算する。計算誤差によっては、zoomが相対的に小さくなり過ぎて、p=(i*cost+j*sint)/zoom+width/2
などでpが画像の座標の範囲外に出て実行時エラーになる恐れがあるので、1.004を掛けて防止している。
・回転後の画像を想定し、横方向を-width/2からwidth/2まで、縦方向を-height/2からheight/2まで変化させて、それが原画像のどの位置に相当するかを計算し、pおよびqとする。これらは実数であるので、整数の点(point[0]など)と実数の端数(ppおよびqq)とに分ける。
・整数の点の4個の色情報と端数の値から、pおよびq点の色情報を補間して求める。
・色情報により回転画像bmap_destに書き込む。
3)回転後の画像bmap_destを画面に表示し、そのときの回転角thetaと拡大率zoomを表示する。
◎menuFileOpen_Click
原画像bmap_srcを読み込むのに、bmap_src=gcnew Bitmap(file_name); を直接使用しても、読込みは正常に行われたが、「上書き保存」、同一ファイル名での「名前を付けて保存」で実行時エラーが発生した。
そこで、
Image^ img=Image::FromFile(file_name);
bmap_src=gcnew Bitmap(img);
を使用したところ、正常に動作した。
この他、以下の処理を行っている。
・「上書き保存」のために、ファイル名(file_name)とファイルの形式(file_type)を保存する。
・「上書き保存」と「名前を付けて保存」のメニューを有効にする。
・原画像bmap_srcの幅と高さを取得して、widthとheightを設定する。
・初期値として、回転角thetaを0.0に、拡大率zoomを1.0に設定する。
・image_flagをtrueに設定して、Form1_Paintで画像の回転を実行させる。
プログラム
◎ImageFormatクラスを使用するために、下記のように名前空間を追加しておく
using namespace System::Drawing::Imaging;
◎グローバル変数、クラスを宣言しておく
double theta,zoom;
int height,width;
String^ file_name;
ImageFormat^ format1;
int file_type;
Bitmap^ bmap_src;
Bitmap^ bmap_dest;
Boolean image_flag;
◎プログラム起動時に一回だけ実行されるForm1_Load
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e)
{
menuFileOpen->Enabled=true;
menuSave->Enabled=false;
menuSaveAs->Enabled=false;
radioFiveDegree->Checked=true;
image_flag=false;
}
◎プログラム起動時と、Invalidate()で呼び出されるForm1_Paint
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^
e) {
Graphics^ gr=e->Graphics;
int X0=10,Y0=40;
int i,j;
double p,q,pp,qq;
Color color1;
int r,g,b;
array<int>^ red=gcnew array<int>(4);
array<int>^ green=gcnew array<int>(4);
array<int>^ blue=gcnew array<int>(4);
array<Point>^ point=gcnew array<Point>(4);
double theta_rad=theta*Math::PI/180.0;
double sint=Math::Sin(theta_rad);
double cost=Math::Cos(theta_rad);
//原画像が読み込まれている場合には、回転を実行する
if(image_flag){
//読み込んだ画像のサイズに合わせてフォームのサイズを設定する
this->ClientSize = System::Drawing::Size(Math::Max(width+80,590), height+80);
double alpha=Math::Atan((double)width/height);
zoom=Math::Cos(alpha-Math::Abs(theta_rad))/Math::Cos(alpha)*1.004;
//拡大倍率
bmap_dest=gcnew Bitmap(width,height);
//回転した画像を描画する
for(i=-width/2;i<width/2;i++)
for(j=-height/2;j<height/2;j++){
//座標換算を行い、拡大倍率を掛ける
p=(i*cost+j*sint)/zoom+width/2;
q=(j*cost-i*sint)/zoom+height/2;
//補間に必要な端数を求める
pp=p-(int)p;
qq=q-(int)q;
//点(p,q)を囲む周辺4点を設定する
point[0]=Point((int)p,(int)q);
point[1]=Point((int)p+1,(int)q);
point[2]=Point((int)p,(int)q+1);
point[3]=Point((int)p+1,(int)q+1);
//点(p,q)を囲む周辺4点の色情報を取得する
for(int k=0;k<4;k++){
color1=bmap_src->GetPixel(point[k].X,point[k].Y);
red[k]=color1.R;
green[k]=color1.G;
blue[k]=color1.B;
}
//補間を行って、回転後の画像bmap_destに描画する
r=(int)((red[0]*(1-pp)+red[1]*pp)*(1-qq)+(red[2]*(1-pp)+red[3]*pp)*qq+0.5);
g=(int)((green[0]*(1-pp)+green[1]*pp)*(1-qq)+(green[2]*(1-pp)+green[3]*pp)*qq+0.5);
b=(int)((blue[0]*(1-pp)+blue[1]*pp)*(1-qq)+(blue[2]*(1-pp)+blue[3]*pp)*qq+0.5);
color1=Color::FromArgb(r,g,b);
bmap_dest->SetPixel(i+width/2,j+height/2,color1);
}
//回転後の画像bmap_destを表示する
gr->DrawImage(bmap_dest,X0,Y0);
String^ string1=String::Format("回転角={0}度、拡大率={1:F2}倍",theta,zoom);
gr->DrawString(string1,Font,Brushes::Black,X0,Y0+height+20);
}
}
◎メニュー「開く」をクリックした時の処理
private: System::Void menuFileOpen_Click(System::Object^ sender, System::EventArgs^
e) {
openFileDialog1->Filter="JPGファイル(*.jpg)|*.jpg|GIFファイル(*.gif)|*.gif|PNGファイル(*.png)|*.png|BMPファイル(*.bmp)|*.bmp";
if(openFileDialog1->ShowDialog()==System::Windows::Forms::DialogResult::OK){
file_name=openFileDialog1->FileName;
file_type=openFileDialog1->FilterIndex;
Image^ img=Image::FromFile(file_name);
bmap_src=gcnew Bitmap(img);
menuSave->Enabled=true;
menuSaveAs->Enabled=true;
width=bmap_src->Width;
height=bmap_src->Height;
theta=0.0;
zoom=1.0;
image_flag=true;
Invalidate();
}
else return;
}
◎「上書き保存」をクリックした時の処理
private: System::Void menuSave_Click(System::Object^ sender, System::EventArgs^
e) {
ImageFormat^ format1;
switch(file_type){
case 1:format1=ImageFormat::Jpeg;break;
case 2:format1=ImageFormat::Gif;break;
case 3:format1=ImageFormat::Png;break;
case 4:format1=ImageFormat::Bmp;
}
bmap_dest->Save(file_name,ImageFormat::Jpeg);
}
◎メニュー「名前を付けて保存」をクリックした時の処理
private: System::Void menuSaveAs_Click(System::Object^ sender, System::EventArgs^
e) {
ImageFormat^ format1;
saveFileDialog1->Filter="JPGファイル(*.jpg)|*.jpg|GIFファイル(*.gif)|*.gif|PNGファイル(*.png)|*.png|BMPファイル(*.bmp)|*.bmp";
if(saveFileDialog1->ShowDialog()==System::Windows::Forms::DialogResult::OK){
switch(saveFileDialog1->FilterIndex){
case 1:format1=ImageFormat::Jpeg;break;
case 2:format1=ImageFormat::Gif;break;
case 3:format1=ImageFormat::Png;break;
case 4:format1=ImageFormat::Bmp;
}
bmap_dest->Save(saveFileDialog1->FileName,format1);
}
else return;
}
◎メニュー「時計方向に回転」をクリックした時の処理
private: System::Void menuRoteateClockwise_Click(System::Object^ sender, System::EventArgs^
e) {
double step;
if(radioOneDegree->Checked==true) step=1.0;
else step=5.0;
theta+=step;
Invalidate();
}
◎メニュー「反時計方向に回転」をクリックした時の処理
private: System::Void menuRotateAnticlockwise_Click(System::Object^ sender, System::EventArgs^
e) {
double step;
if(radioOneDegree->Checked==true) step=1.0;
else step=5.0;
theta-=step;
Invalidate();
}
◎メニュー「原画像に戻す」をクリックした時の処理
private: System::Void menuUndo_Click(System::Object^ sender, System::EventArgs^
e) {
theta=0.0;
Invalidate();
}
得られた結果
図4は、原画像を読み込んだ直後の画面を示す。デフォルトでは、「5度/クリック」のラジオボタンが選択されている。小刻みに画像を回転させるためには、「1度/クリック」を選択しておく必要がある。
図4 原画像を読み込んだままの画面
図5は、ラジオボタン「1度/クリック」をクリックしてから、画面を見ながらメニュー「時計回りに回転」を2回クリックした結果である。下方には、回転角と拡大率が表示されている。
図5 回転させた後の画面
図6は、メニュー「ファイル」をクリックして、プルダウンメニューを示したものである。左は、原画像の読み込み前であるので、「開く」以外は、グレイアウトされている。右は、原画像読込み後の状態である。
図6 原画像の読み込み前後で、メニューが変わる
付録
すでに別講で述べてあるが、参照するのが面倒なので、再度述べておく。ラジオボタンの作り方は新しい。
◎メニューの作り方
1) 「Form1.h[デザイン]」の画面を出しておく。
2) 「メニュー」がら、「表示」→「ツールボックス」で、「ツールボックス」を表示する。
3) 「ツールボックス」で、「メニューとツールバー」の欄の「MenuStrip」をダブルクリックする。
4) タイトルバーの下にMenuStripができ、「ここへ入力」と表示される。
5) 「ここへ入力」の場所にカーソルを持って行き、メニューのキャプションを入力する。入力し終わると、次のメニューが開くので、必要に応じて、さらにキャプションの入力を続ける。
6) メニューを選択して右クリックし、「プロパティ」を選択する。「デザイン」欄の「(Name)」の右の名称を「開くOToolStripMenuItem」などから、「menuOpenFile」などに書き換える(日本語が含まれているのを無くし、短縮して分かりやすくする)。
7) メニューをダブルクリックすると、menuOpenFile_Click()などのスケルトンができているので、そこへコードを記述する。
◎OpenFileDialogを用意する
「ツールボックス」から「OpenFileDialog」を選択し、フォーム上でクリックすると、コンポーネントトレイにopenFileDialog1ができるので、プログラムで、それを使用する。
◎SaveFileDialogを用意する
「ツールボックス」から「SaveFileDialog」を選択し、フォーム上でクリックすると、コンポーネントトレイにsaveFileDialog1ができるので、それを使用する。
◎ラジオボタンの作り方
1) 「Form1.h[デザイン]」画面を出しておく。
2) 「メニュー」がら、「表示」→「ツールボックス」で、「ツールボックス」を表示する。
3) 「ツールボックス」で、「コモンコントロール」の欄の「RadioButton」を選択する。
4) MenuStrip上の希望する場所へポインタを持って行きクリックする。
5) ラジオボタンを選択して右クリックし、「プロパティ」を選択する。「デザイン」欄の「(Name)」の右の名称を「radioButton1」などから、「radioOneDegree」などの分かりやすいものに書き換える。
6) 同様に、「表示」欄の「Text」の右の名称を「1度/クリック」などに書き換える。
7) 同様に、「表示」欄の「BackColor」を「LightBlue」に設定する(「BackColor」をクリックすると、右側に「▼」が現れるので、これをクリックし、「Web」タブの「LightBlue」を選ぶ)。