【C#】画像処理 ~ アフィン変換で任意角度の回転を自作する ~

■ はじめに

https://blogs.yahoo.co.jp/dk521123/38055503.html
の続き。

今回は、実用面でなく、画像の回転を通してアフィン変換を理解する。

■ 自作で任意角度で画像を回転する

実装方法
【1】画像の中心を原点に平行移動
【2】指定した角度で回転
【3】描画領域の中心に表示するように平行移動
* 画像を回転する場合、原点(0, 0)ではなく画像の中心周りに回転させる
  => [1] 画像の中心を原点(0, 0)にするため、
         入力画像の幅 width/2 と 高さ height/2 を引く

  => [2] 変換後の座標x', y'は、出力画像の中心にするため、
         出力画像の幅 width'/2と高さ height'/2 を加える
https://algorithm.joho.info/image-processing/affine-transformation-rotation/
* 画像に穴があくので、その対策をする
  => 理由は、転送元から転送先にピクセルを移動した際に、
     小数の座標があるとそこで穴が開いてしまう

  => 以下のサイトが図があって分かりやすい
http://yaju3d.hatenablog.jp/entry/2013/07/14/133031
【解決案】
 * 転送先から転送元にピクセルを埋める

■ サンプル

using System;
using System.Drawing;
using System.Windows.Forms;

namespace SampleForm
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      try
      {
        string imagePath = this.textBox1.Text;
        double angle = double.Parse(this.textBox2.Text);

        int pictureBoxWidth = this.pictureBox1.Width;
        int pictureBoxHeight = this.pictureBox1.Height;
        // 描画領域の中心
        int centerPictureBoxX = pictureBoxWidth / 2;
        int centerPictureBoxY = pictureBoxHeight / 2;

        // 2倍の描画領域サイズの空の描画領域を生成
        Bitmap bitmap = new Bitmap(pictureBoxWidth * 2, pictureBoxHeight * 2);

        Bitmap sourceBitmap = new Bitmap(imagePath);
        // 画像サイズ
        int imageWidth = sourceBitmap.Width;
        int imageHeight = sourceBitmap.Height;
        // 画像の中心
        int centerImageX = imageWidth / 2;
        int centerImageY = imageHeight / 2;

        double theta = angle * (Math.PI / 180);
        double cos = Math.Cos(theta);
        double sin = Math.Sin(theta);

        for (int x = 0; x < bitmap.Width; x++)
        {
          for (int y = 0; y < bitmap.Height; y++)
          {

            int dx = (int)(((x - centerImageX) * cos) - ((y - centerImageY) * sin) + centerImageX - centerPictureBoxX);
            int dy = (int)(((x - centerImageX) * sin) + ((y - centerImageY) * cos) + centerImageY - centerPictureBoxY);

            if ((0 < dx && dx < imageWidth) && (0 < dy && dy < imageHeight))
            {
              Color color = sourceBitmap.GetPixel(dx, dy);
              bitmap.SetPixel(x, y, color);
            }
          }
        }

        if (this.pictureBox1.Image != null)
        {
          this.pictureBox1.Image.Dispose();
        }
        this.pictureBox1.Image = bitmap;
      }
      catch (Exception ex)
      {
        this.label1.Text = ex.Message;
      }
    }
  }
}

■ 補足:回転の数式的意味

https://mathwords.net/heimenkaiten
https://algorithm.joho.info/image-processing/affine-transformation-rotation/
が分かりやすい

行列の掛け算

 * ベクトル a に行列を掛けると、以下のようになる

 | -1 0 || 2 |   | (-1)*2 + 0*3 |   | -2 |
 |      ||   | = |              | = |    |
 |  0 1 || 3 |   |    0*2 + 1*3 |   |  3 |
  ^^^^^^  ^^^                        ~^^^
   行列    a                           b

 => 行列を掛けることにより、ベクトル a の終点位置が変わる
  => 行列を掛けることにより、ベクトル a が ベクトル b に変換される

原点から元にした回転を求める

| x' |   | cosθ -sinθ || x |
|    | = |              ||   |
| y' |   | sinθ  cosθ || y |

  x' = x cosθ - y sinθ
  y' = x sinθ + y cosθ
【パラメータ説明】
 x , y  : 回転前の座標

 x', y' : 回転後の座標

 | cosθ -sinθ | 
 |              | : 回転行列
 | sinθ  cosθ |

回転座標から元の座標を求める(逆変換)

| x |   |  cosθ sinθ || x' |
|   | = |              ||    |
| y |   | -sinθ cosθ || y' |

  x =   x' cosθ + y' sinθ
  y = - x' sinθ + y' cosθ
【計算で導く】
| x' |   | cosθ -sinθ || x |
|    | = |              ||   |
| y' |   | sinθ  cosθ || y |
 ^^^^     ^^^^^^^^^^^^^^  ^^^
  X    =        A          x
とし、「A-1をAの逆行列」とすると...

 A-1 X = A-1 A x
 => A-1 X = E x (A-1 A = E(E:単位行列。数字でいうと「1」))
 => x = A-1 X ... (※1)

     | a b |
 P = |     | の逆行列 P-1 は...
     | c d |

           1    | d -b |
 P-1 =  --------|      |
        ad - bc | -c a |
である。

 なので、A-1 は、Aの逆行列なので...

                    1                | cosθ -sinθ |
 A-1 = ----------------------------- |              |
        cosθ* cosθ - sinθ* sinθ  | sinθ  cosθ |

          |  cosθ sinθ |
 => A-1 = |              | (分母の「sin^2θ+cos^2θ=1」... ※2)
          | -sinθ cosθ |

よって、※1は、

| x |   |  cosθ sinθ || x' |
|   | = |              ||    |
| y |   | -sinθ cosθ || y' |
 ^^^^     ^^^^^^^^^^^^^^ ~^^^
  x    =        A-1       X
http://www4.airnet.ne.jp/tmt/mathself/matrix8.pdf
※2 : 分母の「sin^2θ+cos^2θ=1」について
https://juken-mikata.net/how-to/mathematics/sin2-cos2-1.html

関連記事

C#】【Form】PictureBox [9] ~ 画像を任意の角度で回転させる ~

https://blogs.yahoo.co.jp/dk521123/38055503.html

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

https://blogs.yahoo.co.jp/dk521123/38061211.html

C#】画像処理 ~ 幾何補正 / 2次元アフィン変換編 ~

https://blogs.yahoo.co.jp/dk521123/38069294.html