【C#】画像処理 ~ アフィン変換・Matrixクラス ~

■ はじめに

 * 画像処理を行う際に、アフィン変換が避けては通れないので、まとめる

■ アフィン変換とは?

 * 線形変換と平行移動を組み合わせた変換

線形変換

 * 変換前に直線だった箇所は、変換後も直線が保たれる
* 図形(今回は画像)に対して、以下の操作が行うことができる
【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);
    }
  }
}


関連記事

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