【iOS】CGAffineTransformを使ったので、アフィン変換について調べた

【iOS】CGAffineTransformを使う機会があったので、アフィン変換について調べました。
2019.11.13

大阪オフィスの山田です。アフィン変換についてがんばって調べたので、その過程やら理解したことをメモしてます。

背景

QRコードの記事を書いた時に、CGAffineTransform が出てきたんですが、これがなんなのか全然わからなかったので、アフィン変換も併せて調べることにしました。筆者は数学が苦手なので苦労しながら書いてます。

まず最初に

とりあえずググってWikipediaにあたりました。アフィン写像

一般に、アフィン変換は線型変換(回転、拡大縮小、剪断(せん断))と平行移動の組み合わせである。

へーなるほど。わからない。というわけでここで出てくる回転拡大縮小せん断平行移動を数式と共に紹介していきます。 その後、アフィン変換行列について解説します。

画像の加工と数式の紹介

加工する前の画像

こちらの画像を加工していきます。ちょっと前に撮影に成功したNecoの「無」です。 画像は正方形です。

平行移動

元画像 変換後

画像を平行移動させる場合は、以下の式で表すことができます。

\[ x' = x + t_x \\ y' = y + t_y \]

これを行列で表すと以下のようになります。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} x \\ y \end{pmatrix} + \begin{pmatrix} t_x \\ t_y \end{pmatrix} \]

拡大縮小

元画像 変換後

画像を拡大縮小させる場合は、以下の式で表すことができます。

\[ x' = s_xx \\ y' = s_yy \]

これを行列で表すと以下のようになります。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} s_x & 0 \\ 0 & s_y \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} \]

回転

元画像 変換後

回転は少しややこしいです。まず下記の図を見てください。

回転はパッと理解できなかったので少し丁寧に数式を変化させてみます。 上記の場合に、半径をrとしてx'について求めると

\[ x' = r\cos(\alpha+\theta) \] この式を変形させていきます。加法定理から

\[ x' = r\cos\alpha\cos\theta\ - r\sin\alpha\sin\theta\\ \]

それぞれ以下のように置き換えが可能なので

\[ x = r\cos\alpha\\ y = r\sin\alpha\\ \]

以下のようになります。

\[ x' = x\cos\theta\ - y\sin\theta\\ \]

同様にy'について求めます。

\[ y' = r\sin(\alpha+\theta)\\ = r\sin\alpha\cos\theta+r\cos\alpha\sin\theta\\ = y\cos\theta+x\sin\theta \]

x'y'の算出方法を行列の形で記述すると以下のようになります。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \\ \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} \]

せん断

せん断は平行四辺形のように画像を変形させます。

元画像 変換後(水平) 変換後(垂直)

数式では以下のように表すことができます。

水平方向のせん断です。

\[ (x' y') = (x + my, y) \\ \]

行列にて表現します。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} x + my \\ y \end{pmatrix} = \begin{pmatrix} 1 & m \\ 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} \]

垂直方向のせん断はこちらの数式です

\[ (x' y') = (x, mx + y) \]

同様に行列で表現します。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} x \\ mx + y \end{pmatrix} = \begin{pmatrix} 1 & 0 \\ m & 1 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} \]

アフィン変換行列

回転、拡大縮小、せん断、平行移動についてそれぞれ全て行列の式で変換を表してきました。ここまでの変換の全ては以下の行列で表現できます。

\[ \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} + \begin{pmatrix} t_x \\ t_y \end{pmatrix} \]

2x2の行列では、水平移動を積で表現することができません。しかし、1次元追加することで、水平移動部分も積で表現することができます。

\[ \begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{pmatrix} a & b & t_x \\ c & d & t_y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} \tag{1} \] この行列の3行目は計算結果が常に1をとり、無視できます。これで回転、拡大縮小、せん断、平行移動を上記の行列にて表現できます。

アフィン変換行列の合成

こちらで詳細に触れられています。アフィン変換は3x3の小さな行列の計算です。一方で画像の変換処理は大きな計算量が必要になります。10回、変換が必要な処理があった場合、10回アフィン変換行列に対し積をとって、合成した行列を最後に一度画像に適用することで処理が軽くなります。

ここまでで導出したアフィン変換行列とCGAffineTransformについて

せん断の画像を作っている時に、CGAffineTransformで指定する引数が、導出した変数と合致してないことに気づきました。 CGAffineTransform

結果から言うと、私がここまで書いた行列のbとcが逆になっていました。 公式ドキュメントに書かれているのは以下の行列式です。

\[ \begin{pmatrix} x' & y' & 1 \\ \end{pmatrix} = \begin{pmatrix} x & y & 1 \\ \end{pmatrix} \begin{pmatrix} a & b & 0 \\ c & d & 0 \\ t_x & t_y & 1 \end{pmatrix} \tag{2} \]

この行列式に対し、積の転置行列を考えます。

\[ (AB)^T = B^TA^T \]

なので、

\[ \begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{pmatrix} a & c & t_x \\ b & d & t_y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} \tag{3} \]

となります。私が導出した行列(1)と(3)の式を比較すると、b, cが入れ替わっています。

CGAffineTransformを使って画像変換

この記事に記載していた画像はCGAffineTransformを実際に使って画像変換していました。 そのコードを記載します。

平行移動

let affine = CGAffineTransform(a: 1, b: 0,
                                       c: 0, d: 1,
                                       tx: 50, ty: 50)
        necoImageView.transform = affine

拡大縮小

let affine = CGAffineTransform(a: 1.5, b: 0,
                                       c: 0, d: 1.5,
                                       tx: 0, ty: 0)
        necoImageView.transform = affine

回転

let angle = 45.0 * CGFloat.pi / 180  // Radian
        let affine = CGAffineTransform(a: cos(angle), b:sin(angle),
                                       c: -sin(angle), d: cos(angle),
                                       tx: 0, ty: 0)
        necoImageView.transform = affine

せん断(垂直)

let affine = CGAffineTransform(a: 1, b: 0.5,
                                       c: 0, d: 1,
                                       tx: 0, ty: 0)
        necoImageView.transform = affine

せん断(水平)

let affine = CGAffineTransform(a: 1, b: 0,
                                       c: 0.5, d: 1,
                                       tx: 0, ty: 0)
        necoImageView.transform = affine

CGAffineTransformには便利なメソッドがあります

イニシャライザやメソッドとして、回転、拡大縮小、平行移動が用意されています。せん断は見当たりませんでした。 それぞれ目的の動作のパラメータ名やメソッド名がついていて、わかりやすくなっています。

init(rotationAngle: CGFloat)
init(scaleX: CGFloat, y: CGFloat)
init(translationX: CGFloat, y: CGFloat)

また、インスタンスメソッドとして、concatenatinginvertedメソッドが用意されています。詳細は公式ドキュメントを参照してください。

おわり

数学をちゃんと勉強しとけばよかった。

参考