C#にはアフィン変換に用いる行列用の
Matrixクラス (名前空間:System.Drawing.Drawing2D)
というクラスがあり、これを駆使すると、以下のようなプログラムを比較的簡単に作成することができます。
プログラムの実行ファイルはこちらより→ AffineImage101.zip
ソースコードはGitHubで公開しています。
https://github.com/ImagingSolution/AffineImage
プログラムの機能としては
画像ファイルのDrag&Dropで画像をピクチャボックス全体に表示
マウスのドラッグで表示画像の移動
マウスホイールの回転でマウスポインタを中心に画像の拡大縮小
マウスホイールの回転 + Shiftキー でマウスポインタを中心に画像の回転
マウスのダブルクリックで画像をピクチャボックス全体に表示
ステータスバーにマウスポインタのピクチャボックス座標と、画像上の座標を表示
となります。
ここで用いていたMatrixクラスの主なメソッドは
Translate
平行移動
Scale
拡大縮小
RotateAt
指定した点を中心に回転
Reset
リセット、単位行列の代入
Invert
逆行列を求める
TransformPoints
点の配列をアフィン変換する
となります。
ただ、これだけだと少し物足りないので、以下のメソッドを拡張メソッドとして追加しています。
ScaleAt
指定した点を中心に拡大縮小
ZoomFit
画像をピクチャボックスの大きさに合わせる
以下、ソースコードの解説です。
アフィン変換行列に基づいて画像の表示
DrawImageメソッド(名前空間:System.Drawing)には、描画する画像の領域と、描画先の座標を画像の左上、右上、左下の順でPointFの配列で指定することで、画像を任意場所や大きさで表示することが可能となります。
この描画先の座標をアフィン変換行列で求めて画像を表示しています。
public static void DrawImage(this Graphics g, Bitmap bmp, System.Drawing.Drawing2D.Matrix mat)
{
if ((g == null) || (bmp == null))return;
// 画像の画素の外側の領域
var srcRect = new RectangleF(
-0.5f,
-0.5f,
bmp.Width,
bmp.Height
);
// 画像の左上、右上、左下の座標
var points = new PointF[] {
new PointF(srcRect.Left, srcRect.Top), // 左上
new PointF(srcRect.Right, srcRect.Top), // 右上
new PointF(srcRect.Left, srcRect.Bottom)// 左下
};
// アフィン変換で描画先の座標に変換する
mat.TransformPoints(points);
// 描画
g.DrawImage(
bmp,
points,
srcRect,
GraphicsUnit.Pixel
);
}
ただし、画像の左上の座標は(-0.5, -0.5)となるのでご注意下さい。
(参考)
【C#】画像の座標系
続きを見る
指定した点を中心に拡大縮小
アフィン変換で行う拡大縮小の変換行列は座標の原点を中心に拡大縮小するため、単に拡大縮小のアフィン変換行列を掛けただけでは、意図しない拡大縮小となります。
そこで、アフィン変換を組み合わせて、
中心座標を原点へ移動→拡大縮小→原点から元の位置へ移動
の変換を行うと、指定して点中心の拡大縮小となります。
public static void ScaleAt(this System.Drawing.Drawing2D.Matrix mat, float scale, PointF center)
{
// 原点へ移動
mat.Translate(-center.X, -center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);
// 拡大縮小
mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
// 元へ戻す
mat.Translate(center.X, center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);
}
(参考)
任意点周りの回転移動(アフィン変換)
続きを見る
画像をピクチャボックスに合わせて表示する
ここでのアフィン変換の考え方は、まず、画像の左上の座標をピクチャボックスの原点に合わせるため、(+0.5, +0.5)だけ平行移動(Translate)します。
↓ (+0.5, +0.5)だけ平行移動
次に、画像の縦横比とピクチャボックスの縦横比を比較して、画像が縦長の場合、画像の縦の長さとピクチャボックスの縦の長さが揃うように画像を拡大縮小(Scale)します。
画像が横長の場合は、横の長さが合うように画像を拡大縮小(Scale)します。
最後に、画像がピクチャボックスの中心に来るように平行移動(Translate)します。
public static void ZoomFit(this System.Drawing.Drawing2D.Matrix mat, PictureBox pic, Bitmap bmp)
{
if (bmp == null) return;
// アフィン変換行列の初期化(単位行列へ)
mat.Reset();
// 0.5画素分移動
mat.Translate(0.5f, 0.5f, System.Drawing.Drawing2D.MatrixOrder.Append);
int srcWidth = bmp.Width;
int srcHeight = bmp.Height;
int dstWidth = pic.Width;
int dstHeight = pic.Height;
float scale;
// 縦に合わせるか?横に合わせるか?
if (srcHeight * dstWidth > dstHeight * srcWidth)
{
// ピクチャボックスの縦方法に画像表示を合わせる場合
scale = dstHeight / (float)srcHeight;
mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
// 中央へ平行移動
mat.Translate((dstWidth - srcWidth * scale) / 2f, 0f, System.Drawing.Drawing2D.MatrixOrder.Append);
}
else
{
// ピクチャボックスの横方法に画像表示を合わせる場合
scale = dstWidth / (float)srcWidth;
mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
// 中央へ平行移動
mat.Translate(0f, (dstHeight - srcHeight * scale) / 2f, System.Drawing.Drawing2D.MatrixOrder.Append);
}
}
ピクチャボックスの座標から、元の画像の座標を求める
これまで説明してきたアフィン変換の行列は、画像の座標をピクチャボックスの座標へ変換するためのアフィン変換行列を求めています。
それでは、その逆のピクチャボックスの座標を元の画像の座標へ変換するには、アフィン変換行列の逆行列を求めて座標変換することで、元の画像を求める事ができます。
この処理をサンプルプログラムでは、MouseMoveイベントで行っています。
その一部抜粋です。
private void picImage_MouseMove(object sender, MouseEventArgs e)
{
////////////////////////////
// マウスポインタの座標と画像の座標を表示する
// マウスポインタの座標
lblMousePointer.Text = $"Mouse {e.Location}";
// アフィン変換行列(画像座標→ピクチャボックス座標)の逆行列(ピクチャボックス座標→画像座標)を求める
// Invertで元の行列が上書きされるため、Cloneを取ってから逆行列
var invert = _matAffine.Clone();
invert.Invert();
var pf = new PointF[] { e.Location };
// ピクチャボックス座標を画像座標に変換する
invert.TransformPoints(pf);
// 画像の座標
lblImagePointer.Text = $"Image {pf[0]}";
}
(参考)
【C#】アフィン変換の相互座標変換
続きを見る
【C#.NET】マイクロソフト仕様のアフィン変換
続きを見る
以上、如何だったでしょうか?
「アフィン変換」 と言うと、「えっ?行列の計算??」 「なんか難しそう」 と言われて何かと敬遠されがちなのですが、アフィン変換に少し慣れると、計算も比較的スッキリとするかと思います。
私からすると、ここでやっている計算を幾何学的に求めようとすると、そっちの方がはるかに大変。。
画像をピクチャボックスに合わせて表示する で紹介しているようにアフィン変換行列を一発で求めようとするのではなく、変換前の状態から変換後の状態へと変換するまでの手順を考え、その手順を行列で表して掛けていく部分がポイントとなります。
アフィン変換の行列の計算部分では、画像そのものを拡大縮小/平行移動している訳ではなく、たかだか3x3の行列の積を計算しているだけなので、計算量としても少なくて済みます。
←画像処理のためのC#テクニック へ戻る