【C#】アフィン変換を用いて画像ビューアを作ろう!

C#にはアフィン変換に用いる行列用の

Matrixクラス(名前空間:System.Drawing.Drawing2D)

というクラスがあり、これを駆使すると、以下のようなプログラムを比較的簡単に作成することができます。

Affine Image Transformations

プログラムの実行ファイルはこちらより→ AffineImage.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#】画像の座標系
画像を描画するにはDrawImageメソッドを用いますが、DrawImageメソッドはいくつものオバーロードが定義されていますが、画像の拡大...

指定した点を中心に拡大縮小

アフィン変換で行う拡大縮小の変換行列は座標の原点を中心に拡大縮小するため、単に拡大縮小のアフィン変換行列を掛けただけでは、意図しない拡大縮小となります。

そこで、アフィン変換を組み合わせて、

中心座標を原点へ移動→拡大縮小→原点から元の位置へ移動

の変換を行うと、指定して点中心の拡大縮小となります。

/// <summary>
/// 指定した点を中心に拡大縮小するアフィン変換行列の計算
/// </summary>
/// <param name="mat">アフィン変換行列</param>
/// <param name="scale">拡大縮小の倍率</param>
/// <param name="center">拡大縮小の中心座標</param>
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);

}

(参考)

任意点周りの回転移動
回転行列では原点周りに点を回転させますが、任意の点(Cx、Cy)周りに回転させたい場合にはどうするのか? これまでの知...

画像をピクチャボックスに合わせて表示する

ここでのアフィン変換の考え方は、まず、画像の左上の座標をピクチャボックスの原点に合わせるため、(+0.5, +0.5)だけ平行移動(Translate)します。

次に、画像の縦横比とピクチャボックスの縦横比を比較して、画像が縦長の場合、画像の縦の長さとピクチャボックスの縦の長さが揃うように画像を拡大縮小(Scale)します。

画像が横長の場合は、横の長さが合うように画像を拡大縮小(Scale)します。

最後に、画像がピクチャボックスの中心に来るように平行移動(Translate)します。

/// <summary>
/// 画像をピクチャボックスに合わせて表示するアフィン変換行列の計算(拡張メソッド)
/// </summary>
/// <param name="mat">アフィン変換行列</param>
/// <param name="pic">描画先のピクチャボックス</param>
/// <param name="bmp">描画するBitmapオブジェクト</param>
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】マイクロソフト仕様のアフィン変換
.NETでは座標のアフィン変換用にMatrixクラス(名前空間:System.Drawing.Drawing2D)が用意されています。 ...

以上、如何だったでしょうか?

「アフィン変換」と言うと、「えっ?行列の計算??」「なんか難しそう」と言われて何かと敬遠されがちなのですが、アフィン変換に少し慣れると、計算も比較的スッキリとするかと思います。

私からすると、ここでやっている計算を幾何学的に求めようとすると、そっちの方がはるかに大変。。

画像処理のためのC#テクニックへ戻る

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

関連記事

関連記事

関連記事
スポンサーリンク

同一カテゴリ人気記事