■ アフィン変換とは?
* 線形変換と平行移動を組み合わせた変換
線形変換
* 変換前に直線だった箇所は、変換後も直線が保たれる* 図形(今回は画像)に対して、以下の操作が行うことができる
【1】 拡大/縮小(Scaling (up/down) or Zooming (in/out)) 【2】 回転(Rotation) 【3】 平行移動(Translation) 【4】 剪断(せん断、Shear)
数式
| x' | | a b || x | | e | | | = | || | + | | | y' | | c d || y | | f | ~~~~~~~ ~~~~~ (A) (B) (A) : 線形変換 (B) : 平行移動
表
| a b c d e f -----------+--------------------------------- 拡大・縮小 | a 0 0 d 0 0 回転 | cosθ -sinθ sinθ cosθ 0 0 平行移動 | 1 0 0 1 e f 反転 上下 | 1 0 0 -1 0 0 左右 | -1 0 0 1 0 0
■ C#におけるアフィン変換・Matrixクラス
公式サイト
https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.drawing2d.matrix?view=netframework-4.8アフィン変換に関わるメソッド
var matrixAffine = new Matrix(); var scale = 2.0f;アフィン変換行列の初期化(単位行列へ)
matrixAffine.Reset();拡大縮小:Scaleメソッド
matrixAffine.Scale(2.0f, 2.0f, MatrixOrder.Append);回転:Rotateメソッド
matrixAffine.RotateAt( 20.0f, new Point(this.pictureBox1.Width / 2, this.pictureBox1.Height / 2), MatrixOrder.Append);移動:Translateメソッド
matrixAffine.Translate( 0f,(this.pictureBox1.Height - this.targetBitmap.Height * scale) / 2f, MatrixOrder.Append);元の座標(画像上の座標)を求める・逆行列を求める
using (var matrixInvert = matrixAffine.Clone()) { // アフィン変換行列の逆行列を求める matrixInvert.Invert(); // 元の座標(画像上の座標)を求める matrixInvert.TransformPoints(sourcePoints); }アフィン変換の初期化
graphics.ResetTransform();アフィン変換行列を代入
graphics.Transform = matrixAffine;
変換後の座標を調べる
* TransformPoints() を使用するvar targetBitmap = new Bitmap(@"20161215052204.gif"); var sourceArea = new RectangleF(0.0f, 0.0f, targetBitmap.Width, targetBitmap.Height); var destinationPoints = new PointF[4]; destinationPoints[0] = new PointF(sourceArea.Left, sourceArea.Top); destinationPoints[1] = new PointF(sourceArea.Right, sourceArea.Top); destinationPoints[2] = new PointF(sourceArea.Left, sourceArea.Bottom); destinationPoints[3] = new PointF(sourceArea.Right, sourceArea.Bottom); // 描画先の座標をアフィン変換で求める(変換後の座標は上書きされる) matrixAffine.TransformPoints(destinationPoints); var axis = string.Format( "({0:#.#}, {1:0.#}), ({2:#.#}, {3:0.#}), ({4:#.#}, {5:0.#}), ({6:#.#}, {7:0.#})", destinationPoints[0].X, destinationPoints[0].Y, destinationPoints[1].X, destinationPoints[1].Y, destinationPoints[2].X, destinationPoints[2].Y, destinationPoints[3].X, destinationPoints[3].Y);
■ サンプル
例1:画像をピクチャボックスのサイズに合わせて全体に表示する
using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace SampleForm { public partial class Form1 : Form { private Bitmap targetBitmap; // 描画元を指定する4点の座標(左上、右上、左下、右下の順) private PointF[] sourcePoints; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, System.EventArgs e) { // 画像ファイルのImageオブジェクトを作成する this.targetBitmap = new Bitmap(@"20161215052204.gif"); var sourceArea = new RectangleF(-0.5f, -0.5f, this.targetBitmap.Width, this.targetBitmap.Height); // 描画元を指定する4点の座標(左上、右上、左下、右下の順) this.sourcePoints = new PointF[4]; this.sourcePoints[0] = new PointF(sourceArea.Left, sourceArea.Top); this.sourcePoints[1] = new PointF(sourceArea.Right, sourceArea.Top); this.sourcePoints[2] = new PointF(sourceArea.Left, sourceArea.Bottom); this.sourcePoints[3] = new PointF(sourceArea.Right, sourceArea.Bottom); this.Draw(); } private void Draw() { if (this.pictureBox1.Width == 0 || this.pictureBox1.Height == 0) { return; } var matrixAffine = new Matrix(); // 縦に合わせるか?横に合わせるか? if (this.targetBitmap.Height * this.pictureBox1.Width > this.pictureBox1.Height * this.targetBitmap.Width) { // ピクチャボックスの縦方法に画像表示を合わせる場合 var scale = this.pictureBox1.Height / (float)this.targetBitmap.Height; matrixAffine.Scale(scale, scale, MatrixOrder.Append); // 中央へ平行移動 matrixAffine.Translate( (this.pictureBox1.Width - this.targetBitmap.Width * scale) / 2f, 0f, MatrixOrder.Append); } else { // ピクチャボックスの横方法に画像表示を合わせる場合 var scale = this.pictureBox1.Width / (float)this.targetBitmap.Width; matrixAffine.Scale(scale, scale, MatrixOrder.Append); // 中央へ平行移動 matrixAffine.Translate( 0f, (this.pictureBox1.Height - this.targetBitmap.Height * scale) / 2f, MatrixOrder.Append); } // 描画先の座標をアフィン変換で求める(左上、右上、左下の順) var destinationPoints = (PointF[])this.sourcePoints.Clone(); // 描画先の座標をアフィン変換で求める(変換後の座標は上書きされる) matrixAffine.TransformPoints(destinationPoints); this.Text = string.Format( "({0:#.#}, {1:0.#}), ({2:#.#}, {3:0.#}), ({4:#.#}, {5:0.#}), ({6:#.#}, {7:0.#})", destinationPoints[0].X, destinationPoints[0].Y, destinationPoints[1].X, destinationPoints[1].Y, destinationPoints[2].X, destinationPoints[2].Y, destinationPoints[3].X, destinationPoints[3].Y); Bitmap clonedBitmap = new Bitmap(this.pictureBox1.Width, this.pictureBox1.Height); using (var graphics = Graphics.FromImage(clonedBitmap)) { // まずは背景色を黒くする graphics.Clear(Color.Black); graphics.Transform = matrixAffine; // 高品質双三次補間を指定 graphics.InterpolationMode = InterpolationMode.HighQualityBilinear; // 描画(指定された位置に元の物理サイズで描画) graphics.DrawImageUnscaled(targetBitmap, 0, 0); } // 描画 if (this.pictureBox1.Image != null) { this.pictureBox1.Image.Dispose(); } this.pictureBox1.Image = clonedBitmap; this.pictureBox1.Refresh(); } private void Form1_Resize(object sender, System.EventArgs e) { if (this.targetBitmap == null) { return; } this.Draw(); } } }
例2:画像回転ではみ出さないように補正する
https://social.msdn.microsoft.com/Forums/vstudio/ja-JP/25980b27-864f-4b0a-8083-ae442d7589ee/bitmap12434202192484712398352822423012398222353528224418209991242?forum=wpfjaを参考に作成
using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace SampleForm { public partial class Form1 : Form { private PointF[] sourcePoints; private float currentAngle = 0.0f; /// <summary> /// オリジナルのビットマップ /// </summary> private Bitmap originalBitmap = null; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { // 画像ファイルのImageオブジェクトを作成する this.originalBitmap = new Bitmap(@"20161215052204.gif"); var sourceArea = new RectangleF(0.0f, 0.0f, this.originalBitmap.Width, this.originalBitmap.Height); // 描画元を指定する4点の座標(左上、右上、左下、右下の順) this.sourcePoints = new PointF[4]; this.sourcePoints[0] = new PointF(sourceArea.Left, sourceArea.Top); this.sourcePoints[1] = new PointF(sourceArea.Right, sourceArea.Top); this.sourcePoints[2] = new PointF(sourceArea.Left, sourceArea.Bottom); this.sourcePoints[3] = new PointF(sourceArea.Right, sourceArea.Bottom); this.Draw(this.currentAngle); } private void Draw(float angle) { if (this.pictureBox1.Width == 0 || this.pictureBox1.Height == 0) { return; } // アフィン変換 var affineTransformation = new Matrix(); affineTransformation.Rotate(angle); // 描画先の座標をアフィン変換で求める(左上、右上、左下の順) var destinationPoints = (PointF[])this.sourcePoints.Clone(); // 元画像を左上基準で回転させた後の各点の座標を計算 affineTransformation.TransformPoints(destinationPoints); //回転後の画像が収まる範囲を調べる float minX = destinationPoints.Min(point => point.X); float maxX = destinationPoints.Max(point => point.X); float minY = destinationPoints.Min(point => point.Y); float maxY = destinationPoints.Max(point => point.Y); // はみ出さないように平行移動 affineTransformation.Translate(-minX, -minY, MatrixOrder.Append); // 新しい画像の幅と高さ int newWidth = (int)Math.Ceiling(maxX - minX); int newHeight = (int)Math.Ceiling(maxY - minY); // 縦に合わせるか?横に合わせるか? float scale; if (newHeight * this.pictureBox1.Width > this.pictureBox1.Height * newWidth) { // ピクチャボックスの縦方法に画像表示を合わせる場合 scale = this.pictureBox1.Height / (float)newHeight; affineTransformation.Scale(scale, scale, MatrixOrder.Append); // 中央へ平行移動 affineTransformation.Translate( (this.pictureBox1.Width - newWidth * scale) / 2f, 0f, MatrixOrder.Append); } else { // ピクチャボックスの横方法に画像表示を合わせる場合 scale = this.pictureBox1.Width / (float)newWidth; affineTransformation.Scale(scale, scale, MatrixOrder.Append); // 中央へ平行移動 affineTransformation.Translate( 0f, (this.pictureBox1.Height - newHeight * scale) / 2f, MatrixOrder.Append); } var newBitmap = new Bitmap(this.originalBitmap, this.pictureBox1.Width, this.pictureBox1.Height); using (var graphics = Graphics.FromImage(newBitmap)) { // まずは背景色を黒くする graphics.Clear(Color.Black); graphics.Transform = affineTransformation; // 高品質双三次補間を指定 graphics.InterpolationMode = InterpolationMode.HighQualityBilinear; // 描画(指定された位置に元の物理サイズで描画) graphics.DrawImageUnscaled(this.originalBitmap, Point.Empty); } // 描画 if (this.pictureBox1.Image != null) { this.pictureBox1.Image.Dispose(); } this.pictureBox1.Image = newBitmap; this.pictureBox1.Refresh(); } private void button1_Click(object sender, EventArgs e) { var variation = this.checkBox1.Checked ? 20.0f : -20.0f; // 0~360に制限 this.currentAngle = ((this.currentAngle + variation) % 360 + 360) % 360; this.Draw(this.currentAngle); } private void Form1_Resize(object sender, EventArgs e) { if (this.originalBitmap == null) { return; } this.Draw(this.currentAngle); } } }
参考文献
https://imagingsolution.net/program/csharp/affine_mutual_transformation/https://imagingsolution.net/program/globaltransformations/
https://mathwords.net/affine
https://dobon.net/vb/dotnet/graphics/transform.html
http://koshinran.hateblo.jp/entry/2018/04/05/205934
https://interface.cqpub.co.jp/wp-content/uploads/interface/2013/04/if04_043.pdf/wp-content/uploads/interface/2013/04/if04_043.pdf
http://imagingsolution.blog.fc2.com/blog-entry-284.html
関連記事
Windows Form
Windows Form ~ 目次 ~https://blogs.yahoo.co.jp/dk521123/8054245.html
PictureBox [3] ~ マウスホイール で画像の拡大・縮小する ~
https://blogs.yahoo.co.jp/dk521123/37866101.html
PictureBox [9] ~ 画像を任意の角度で回転させる ~
https://blogs.yahoo.co.jp/dk521123/38055503.html
画像処理
画像処理 ~ 回転 ~https://blogs.yahoo.co.jp/dk521123/37853430.html
画像処理 ~ アフィン変換で任意角度の回転を自作する ~
https://blogs.yahoo.co.jp/dk521123/38093149.html