失敗は一時の恥

パッケージソフト開発をしているプログラマが気の赴くままに何かを投稿するブログ.

Hello Selenium!

Hello Selenium!

この記事について

Web ブラウザを操作するテスト自動化ツールである Selenium を,初めて触ってみて試した内容を紹介します.

プログラミング言語Ruby を使っています.

最終的に下の gif アニメのように「検索エンジンに特定をキーワードを与えてやると,期待したサイトがトップヒットする」といった簡単なテスト (?) が実行できるようになりました.

f:id:snobutaka:20180325164930g:plain
Selenium を使い DuckDuckGo で "Ruby" と検索する様子

今回行った実装は GitHub のリポジトリにも置いてあります.

Selenium とは

Selenium とは……

Selenium automates browsers. That's it! What you do with that power is entirely up to you. Primarily, it is for automating web applications for testing purposes, but is certainly not limited to just that. Boring web-based administration tasks can (and should!) be automated as well.

(https://www.seleniumhq.org より)

Selenium とはブラウザを自動化するもので,Web アプリのテスト自動化に使えるし,それだけでなくブラウザを使った単純作業の自動化もできる,といったところでしょうか.

RubySelenium を動かす

セットアップ

RubySelenium を動かすには,selenium-webdriver という gem を使用します.

gem install selenium-webdriver

動かす Web ブラウザは,事前にインストールされている必要があります.今回は Firefox で試しました.

また,動かすブラウザを操作するための Selenium のドライバをインストールし,パスを通しておく必要があります.

Firefox を動かすには geckodriver というライブラリが必要なので,https://github.com/mozilla/geckodriver/releases からダウンロードし,パスが通ったところに配置します.

macOS を使っているならば,homebrew でインストールできます.

brew install geckodriver

実際に動かす

Ruby のコードから Selenium の driver オブジェクトを取得します.

requrie 'selenium-webdriver' 
driver = Selenium::WebDriver.for(:firefox)
driver.manage.timeouts.implicit_wait = 5

ここでは,Web ページの応答のタイムアウトを 5 秒に設定しています. implicit_wait を設定しておくと,プログラムを書く側で明示的に待ちを実装しなくても,ライブラリが勝手に 5 秒まで待ってくれるようになります.

driver を作成できたので,ブラウザを操作してみます. Selenium でブラウザを操作するには,要素の ID や XPath で対象を選択し, それに対して入力やクリックのメソッドを呼び出す形になります.

次のコードでは,

  • 検索エンジンDuckDuckGo を開く
  • 検索ワードに "Ruby" と入力する
  • 検索実行ボタンをクリックする
  • 検索結果一覧から,トップヒットしているものをクリックする

という操作を行っています.

driver.get('https://duckduckgo.com')
driver.find_element(:id => 'search_form_input_homepage').send_keys('Ruby')
driver.find_element(:id => 'search_button_homepage').click()
driver.find_element(:id => 'r1-0').click()

ついでに開いている画面のスクリーンショットを撮ることもできます. テストのエビデンスが求められるシチュエーションで活躍しそうですね!

せっかくなのでエビデンスとしてスクショを撮って,driver を quit して終わらせましょう.

driver.save_screenshot('./duckduckgo-ruby.png')
driver.quit()

エビデンスを見ると,プログラミング言語 Ruby のサイトが最後に表示されていたことが確認できますね.

f:id:snobutaka:20180325165058p:plain
エビデンス

そういえば今回は,検索ワードに対して期待した結果がトップヒットするという「テスト」を書こうとしていたのでした. driver から現在のページの URL を取得すれば,検証が行えます.

assert_equal('www.ruby-lang.org', URI.parse(@driver.current_url).host)

ここまでの内容をまとめると,次のようなテストクラスにできます.

require 'selenium-webdriver'
require 'test/unit'
require 'uri'

class SeleniumFirefoxTest < Test::Unit::TestCase
  def setup()
    @driver = Selenium::WebDriver.for(:firefox)
    @driver.manage.timeouts.implicit_wait = 5 # seconds
  end

  def teardown()
    @driver.quit()
  end

  def test_duckduckgo()
    @driver.get('https://duckduckgo.com')
    @driver.find_element(:id => 'search_form_input_homepage').send_keys('Ruby')
    @driver.find_element(:id => 'search_button_homepage').click()
    @driver.find_element(:id => 'r1-0').click()
    @driver.save_screenshot('./duckduckgo-ruby.png')
    assert_equal('www.ruby-lang.org', URI.parse(@driver.current_url).host)
  end
end

GitHub のコード

ここまでの内容をもう少し拡張したものを GitHub のリポジトリ に置いてあります. リポジトリの中では,今回使用した DuckDuckGo 以外に

といったサイトでも検索を行うテストメソッドを用意しています.

ブラウザについては,今回の Firefox 以外に

で動かすテストクラスがあります. GUI がない環境でテストを実行する場合は,Headless モードで動かすことになります.

実行環境

上述のリポジトリにはテストコード以外に,Docker と Vagrant による実行環境がそれぞれ用意してあります. いずれの環境も Firefox が使えるようにしてあります.

画面にブラウザを表示して動く様子が見たい場合は Vagrant仮想マシン,Headless モードを動かして見たい場合は Docker イメージが使えます.

Docker イメージは Docker Hub にも公開してあります: snobutaka/hello-selenium

おわり

今回は初めての Selenium ということで非常に簡単なテストを書いてみました.

仕事で使ってみたり,ブラウザ作業自動化ライフハックに使ったりしてみたいものです.

MySQL 備忘録

最近,DB による getGeneratedKeys の挙動の差 を調べるために, ほぼ初めて (?) MySQL を使用しました.

これはその際に使ったコマンド等の備忘録です.

MySQL の起動とクライアントからの接続

Docker を使って MySQL を起動します.

Docker Hub にある公式イメージ の説明の通りに使わせてもらいます.

MySQL 起動:

$ docker run -d --rm --name mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test_db mysql:8.0.3

ここでは,

  • MYSQL_ROOT_PASSWORD は root
  • 作成するデータベース名は test_db

とします.

上記で起動した MySQL にクライアントから mysql コマンドで接続します:

$ docker run -it --link mysql:mysql --rm mysql:8.0.3 sh -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD"'

MySQL 起動コマンドを実行した直後だと,まだ接続できる状態になっておらずエラーとなることがあります.

クライアント接続後の操作

データベース一覧を表示する:

$ SHOW DATABASES;

テーブルを作成する.列 id は自動採番にする:

$ CREATE TABLE test_db.test_table (id BIGINT AUTO_INCREMENT , value TEXT, INDEX(id));

存在するテーブルの一覧を取得する:

$ SHOW TABLES FROM test_db;

テーブルを削除する:

$ DROP TABLE test_db.test_table;

データを挿入する

$ INSERT INTO test_db.test_table (value) VALUES ('TEST_VALUE1');

データを取得する

$ SELECT * FROM test_db.test_table;

DB による getGeneratedKeys の挙動の差

DB による getGeneratedKeys の挙動の差

Java で DB に書き込みを行ったあと,Statement#getGeneratedKeys() によって返される結果が DB 毎に違うのか調べてみました

調べた結果,実際に DB 毎に違いがあるようでした.

調査概要

DB は PostgreSQLMySQL を使用.

INSERT と UPDATE を実行し,getGeneratedKeys() に何が含まれているのかを確認します.

作成するテーブルは次のような定義です.

CREATE SEQUENCE test_seq START 1;
CREATE TABLE test_table (
  id     BIGINT PRIMARY KEY DEFAULT nextval('test_seq'),
  value  TEXT
);
CREATE TABLE test_table (
  id BIGINT AUTO_INCREMENT,
  value TEXT,
  INDEX(id)
);

結果

DB INSERT した id INSERT した value UPDATE した行
PostgreSQL 取得できる (*1) 取得できる (*1) 取得できる (*1)
MySQL 取得できる (*2) 取得できない 取得できない

(*1) 値の取得には,列のインデックスと名前のどちらでも使うことができる.
(*2) 値の取得には,列のインデックスのみを使うことができる.

調査内容の詳細

実装は GitHub のリポジトリ においてあります.

// 実は MySQL はほぼ初めて扱ったので,何か勘違いをしているかも…… (不安)

所感

ふだん PostgreSQL しか使っていないのですが, 他の DB や SQL の標準仕様なんかはどうなっているのか? ってことをたまには気にしたいと思いました.

「たまたま PostgreSQL だから動いている」 ような実装をしないようにしたいですね.

そもそもですが,こういう問題は DB や SQL に限った話でもないので, 何事も広い視野を持ちたいものです.

codewars で遊んでみた

codewars で遊んでみた

f:id:snobutaka:20180204151152p:plain
codewars

きっかけ

Qiita に投稿されていた紹介記事 を読んで面白そうだと思った.

サービスの内容

プログラミングの問題を解いてランクアップを目指していく.

問題は「型 (Kata)」などと呼ばれているようで,難易度が「n 級」として設定されている.

対応しているプログラミング言語は,C, C++, C#, Java, Python, Ruby, Haskell, Swift, ... を始め数多く用意されている.

問題の内容は,(今のところ) 数行から数十行で回答できるような, ちょっとしたユーティティを実装したり,計算問題を解いたりといったものが出題されている.

ブラウザ上でコードを書いて提出すると,すぐにユニットテストが走ってフィードバックが得られるのがとても良い.

f:id:snobutaka:20180204150854p:plain
コーディングとテスト

現時点での取り組み内容

JavaRuby で問題を解いている.

たしか最初は 9 級から始まり, 11 個くらい問題を解いて 6 級になっている.

感想

ちょっとしたパズル感覚で挑戦できて楽しいし, 他の人の解答から学びが得られて面白い.

例えば,「入力された数字の各桁を n 乗した和をとる (695 という入力に対して 6^2 + 9^3 + 5^4 を計算する)」という処理を含む問題に,自分はこんな回答をした:

public static long digPow(int n, int p) {
    String nStr = Integer.toString(n);
    long value = 0;
    for (int i = 0; i < nStr.length(); i++) {
        int d = (int)nStr.charAt(i) - (int)'1' + 1;
        value += Math.pow(d, p + i);
    }
    if (value % n ==0) {
        return value / n;
    } else {
        return -1;
    }
}

他の人の回答を見ると,もっとスマートなものが見つかったりする:

public static long digPow(int n, int p) {
    String intString = String.valueOf(n);
    long sum = 0;
    for (int i = 0; i < intString.length(); ++i, ++p)
      sum += Math.pow(Character.getNumericValue(intString.charAt(i)), p);
    return (sum % n == 0) ? sum / n : -1;
  }

これを見て,Character#getnumericValue(char) なんていうメソッドが使えることを知らなかったなぁと思い知らされたりする.

他にも,ふだん仕事で Java を書くときはいまだに Java SE 7 を使っている (2018 年 2 月時点) のもあり, ラムダやストリーム API を使ったエレガントな回答を見ると, もっと新しい書き方に慣れないとなぁという危機感を感じたりもする (汗)

それなりに慣れているはずの Java でも気づきが得られるし, 趣味でちょっと書いている Ruby なんかはもっと色々な学びが得られている.

まとめ

codewars は無料で始められて,すぐにブラウザ上で快適にコーディングとテストができる.

使い始めてばかりの言語はもちろん,それなりに慣れている言語でも学べることがあると思うので, 興味があるならちょっとやってみるのがおすすめ.

ZooKeeper に触ってみよう

ZooKeeper に触ってみよう

ZooKeeper という,分散アプリケーション協調サービスがあります. どんなことができるのか,簡単に ZooKeeper に触れてみましょう.

この記事では ZooKeeper についての簡単な紹介と, 実際に ZooKeeper を起動して,簡単なコマンドを実行するところまで試してみます.

ZooKeeper とは何か

ZooKeeper というのは,分散アプリケーションを協調させるためのサービスです.

分散アプリケーションというのは,1 台のマシンで実行されるのではなく,複数のマシンで別々のプロセスとして実行されるといったアプリケーションです. 単に別個のプロセスとして走らせているだけではデータの整合性などで問題が出たりするため,各プロセスは何らかの形で「協調」することが求めらたりします.

ZooKeeper は,その「協調」のための仕組みを提供してくれます. ZooKeeper Recipes and Solution というページでは,次のようなケースに ZooKeeper を使用できるという例が挙げられています:

  • バリア (Barriers)
  • キュー (Queues)
  • ロック (Locks)
  • 2 相コミット (Two-phased Commit)
  • リーダー選出 (Leader Election)

実際に ZooKeeper が提供しているのは,ネットワーク越しにアクセスする小さなファイルシステムのようなものです.

ルートディレクト/ に相当するものがあり,その配下に znode と呼ばれるエントリーを作成していきます.

各 znode は最大 1 MB のデータを保持することができます. また,各エントリーは子エントリーを持つことができます.

分散アプリケーション達は,この znode と呼ばれるデータ構造を介して協調をすることになります.

実際にどんなところで使われている?

ZooKeeper はこんなところで使われているようです:

Apache Solr

Solrオープンソースの検索システムです. Solr には,SolrCloud モードという仕組みがあり,複数台の Solr を組み合わせて冗長化を行うことができます.

この冗長化を行うにあたり,各 Solr ノードの協調に ZooKeeper を利用しています.

Apache Hadoop

Hadoop は,MapReduce でデータを分散処理することなどができて,いわゆるビッグデータ絡みでよく名前が挙がりますね.

この Hadoop も ZooKeeper を利用しています.

Netflix のシステムのどこか

curator という ZooKeeper を利用するライブラリを,NetflixOSS として公開しています.あの動画配信の Netflix です.

具体的に Netflix がどういう風に ZooKeeper を利用しているのかは明らかではない (ちょっと調べれば事例とかあるかも?) のですが,ライブラリを作り込むくらいには ZooKeeper が利用されているものと思われます. (いい加減な説明ですみません...)

他にも,ZooKeeper/PoweredBy - Hadoop Wiki などを参照すると事例が多く見つかります.

ZooKeeper を起動する

それでは ZooKeeper を起動してみましょう.

方法はいくつかあります.

といった選択肢がありますが, 今回は使い捨てのお試し程度の利用なので,Docker を利用します. (Docker 自体の導入方法や細かい使い方の説明は,ここではしません)

ZooKeeper の Docker イメージは, Docker Hub の公式イメージを使います.現在 (2018 年 1 月時点) の最新安定板は 3.4.11 となっているので,それを利用します.

起動するには

$ docker run -d --rm --name zoo -p 2181:2181 zookeeper:3.4.11

などとします.

ここでは,

  • バッググランドで動作させる (-d)
  • 停止時に自動でコンテナを破棄する (--rm)
  • コンテナに "zoo" という名前をつける (--name zoo)
  • コンテナのポート 2181 をホストのポート 2181 として公開する (-p 2181:2181)

というオプションにしています.

$ docker ps

とすれば,"zoo" というコンテナが動作していることがわかるはずです.

停止するときは,

$ docker stop zoo

とします.

zkcli で ZooKeeper に接続する

ZooKeeper サーバが起動したので,クライアントからアクセスしてみましょう.

$ docker run -it --rm --link zoo:zookeeper zookeeper:3.4.11 zkCli.sh -server zookeeper

上記のコマンドを実行すると,Docker コンテナとして zkCli.sh という対話環境に入ることができます.

ここでは Docker で zkCli.sh を実行しましたが,zoo コンテナのポートをホストに公開してあるので,ホストマシンから zkcli コマンドで (インストールしてあれば) 接続することもできます.

zkcli のコマンド

クライアントの準備もできたので,ZooKeeper のコマンドを使ってみましょう.

基本的なコマンドをあげてみます.

  • 指定した znode の子ノードを取得する:
$ ls /
# => [zookeeper]
  • znode を作成する:
# create /test 'Test'
# ls /
# => [zookeeper, test]
  • znode のデータを取得する:
# get /test
# => 先にセットした Test という値と,そのほかのメタデータが得られる
  • 対話環境から抜ける:
$ quit

極めて基本的なコマンドではありますが,これで ZooKeeper のデータを登録・取得することができます.

協調のためのエッセンス

ZooKeeper でファイルシステムのようなデータツリーを作成できることはわかりましたが,それだけならわざわざ ZooKeeper なんていう不慣れなものを持ち出す必要はありません.

ここでは,分散アプリケーションの協調に役立つ ZooKeeper の特徴的な機能を見ていきます.

分散アプリケーションのための機能ですから,コンソールを 2 つ立ち上げて,2 つの zkCli で実験をしてみましょう.

セッションが有効な間だけ存続するデータ

ZooKeeper では,クライアントのセッションが有効な間だけ存続するデータを作成することができます. これを短命 (ephemeral) znode と呼びます.

まず,1 つ目の zkcli で短命 znode を作成してみましょう. 短命 znode を作成するには,-e オプションを使用します.

$ create -e /ephemeral_test 'This is ephemeral znode'

2 つ目の zkcli で,作成した znode を確認します.

$ ls /
# => [ephemeral_test, zookeeper]

ここで,1 つ目の zkcli を終了します.

$ quit

再度 2 つ目の zkcli で znode を確認するとどうでしょう.

$ ls /
# => [zookeeper]

/ephemeral_test という znode が消えています.

このような短命 znode は,例えば動的に増減するアプリケーションインスタンスの管理に役立てることができます.

データに対する変更の検知

ZooKeeper には,データに変更が加えられたときそれを検知するための仕組みがあります.

1 つ目の zkcli でデータの作成,監視 (watch) を行います.監視を行うには,get コマンドに watch オプションを付加します.

$ create /watch_test 'Watch Test'
$ get /watch_test watch

2 つ目の zkcli で,/watch_test のデータを変更してみましょう.

$ set /watch_test

すると,1 つ目の zkcli の方で

WatchedEvent state:SyncConnected type:NodeDataChanged path:/watch_test

というメッセージが表示されます. メッセージから「/watch_test という znode のデータが変更された」ということが読み取れます.

検知できる変更は他にも

  • znode が作成された
  • znode が削除された
  • znode に子 znode が作成された

といったイベントがあります.

この監視の仕組みを利用して,データの追加・変更・削除についてアプリケーション達は協調動作をすることができるようになります.

実際にアプリケーションを協調させるには

ここまででは zkcli コマンドを使って ZooKeeper にアクセスしていましたが,実際に分散アプリケーションを協調動作させるには,アプリケーションの中で ZooKeeper を利用した実装を行う必要があります.

ZooKeeper の公式な API としては,Java と C が提供されています. これ以外の言語についてもサードパーティーが提供している場合があります.

おわり

今回は ZooKeeper を起動して,簡単なコマンドを叩いてみました.

短命 znode や変更の検知など,分散アプリケーションを協調させるうえで役立ちそうな機能も確認しました.

いずれ,実際に動くプログラムの紹介もしてみようと思います.

アフィン変換の仕組み

アフィン変換の仕組み

この記事では行列の計算とアフィン変換の仕組みについて簡単に解説します.

この記事について

最近,仕事でアフィン変換を扱うことがありました. アフィン変換で画像のようなデータを回転するといった内容です.

仕事をしながら昔に習ったベクトルと行列を話を思い出したので, 座標の回転やアフィン変換の仕組みについて書いてみようと思います.

この記事では,アフィン変換の回転と平行移動についてのみ触れます.拡大やせん断については触れません.

まずは,行列の基本的な計算の定義を思い出し,回転行列がなぜ回転を表しているのか,点の平行移動はどうするのかといった話をしつつ, アフィン変換はどういう仕組みでデータを変換するのか説明をしていきます.

行列の計算のおさらい

行列 {A} とベルトル {\vec{v}} (平面上の点 {(x, y)}) があったとします.

\[ A = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix}, \vec{v} = \begin{pmatrix} x \\ y \end{pmatrix} \]

行列 {A} とベクトル {\vec{v}} の積は,次のように計算します.

\[ A \vec{v} = \begin{pmatrix} a_{11}x + a_{12}y \\ a_{21}x + a_{22}y \end{pmatrix} \]

行列 {A} は, 平面上の点 {(x, y)} を 点 {(a_{11}x + a_{12}y,\ a_{21}x + a_{22}y)} に移動させる操作とみなすことができます.

回転行列

行列によって回転を表すことができます.点 {(x, y)} を原点中心に角 {\theta} だけ回転させたいといった場合は,次のような行列を使用します.

\[ \begin{pmatrix} \cos\theta & - \sin\theta \\ \sin\theta & \cos\theta \end{pmatrix} \]

{(x,y)} を原点中心に角 {\theta} 回転させると,

\[ \begin{pmatrix} x \cos\theta - y \sin\theta\\ x \sin\theta + y \cos\theta \end{pmatrix} \]

という点になります.

この回転行列自体はよく知られていて,今回の話のテーマであるアフィン変換でも利用するものです.

回転行列はなぜ回転を表すのか?

回転行列は実際に角度 {\theta} の回転を表すのですが,この {\sin}{\cos} 関数を含む行列の積は,どういう理由で回転になるのでしょう?

いま,原点からの距離が 1 で,x 軸の正の向きとなす角が {\theta} である点 {p} があるとします. 三角関数を使うと,{p} の座標は次のように表すことができます:

\[ p = \begin{pmatrix}
 \cos\theta \\ \sin\theta \end{pmatrix} \]

この点を,原点中心に角 {\varphi} だけ回転した点を {q} とすると,

\[ q = \begin{pmatrix} \cos(\theta + \varphi) \\ \sin(\theta + \varphi) \end{pmatrix} \]

となります.

f:id:snobutaka:20171230161024p:plain

回転図

ここで,三角関数の加法定理を利用すると,{q} の座標は次のように表すことができます:

\[ q = \begin{pmatrix} \cos\theta\cos\varphi - \sin\theta\sin\varphi \\ \sin\theta\cos\varphi + \cos\theta\sin\varphi \end{pmatrix} \]

加法定理自体の詳しい解説はここでは行いませんが,このように式を展開できるということだけ利用させてもらいます.

これを踏まえて,実際に回転行列と点 {p} の積を計算してみましょう:

\[ \begin{align} \begin{pmatrix} \cos\varphi & -\sin\varphi \\ \sin\varphi & \cos\varphi \end{pmatrix} \begin{pmatrix} \cos\theta \\ \sin\theta \end{pmatrix} &= \begin{pmatrix} \cos\varphi\cos\theta - \sin\varphi\sin\theta \\ \sin\varphi\cos\theta + \cos\varphi\sin\theta \end{pmatrix} \\ &= \begin{pmatrix} \cos\theta\cos\varphi - \sin\theta\sin\varphi \\ \sin\theta\cos\varphi + \cos\theta\sin\varphi \end{pmatrix} \\ &= q \end{align} \]

定義通りに行列の積を計算し,要素の和と積の順番を整理すると,計算結果は {q} そのものであることが分かります.

これで,回転行列が実際に回転を表しているということが確かめられました.

平行移動

{(x, y)}

  • x 軸の正の向きに {\Delta x}
  • y 軸の正の向きに {\Delta y}

だけ平行移動するには,ベクトル {\vec{\Delta v} = \begin{pmatrix} \Delta x \\ \Delta y \end{pmatrix}} との和を取ることになります.

\[ \vec{v} + \vec{\Delta v} = \begin{pmatrix} x + \Delta x \\ y + \Delta y \end{pmatrix} \]

平行移動を単純に行列で表現することはできない

先ほど,原点中心に角 {\theta} の回転を行うには,回転行列を使用すればいいという話をしました. それでは,平行移動も行列で表現できるのでしょうか?

この問いに対しては,「そのままではできない」というのが答えになります.

できない例を挙げると,例えば原点 {(0 ,0)} を行列を使って平行移動することはできません. 原点と任意の行列の積を計算すると,必ず原点自身になってしまうからです.

\[ \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix} \begin{pmatrix} 0 \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ 0 \end{pmatrix} \]

この事実は,行列の線型性という性質が関係しているのですが,ここでは深くは立ち入りません. 興味がある人は,線形代数の教科書・参考書などを読んでみると詳しい話がわかると思います.

一方で,アフィン変換は行列を使いますが平行移動を実現しています. アフィン変換では,ちょっとした工夫をすることで行列で平行移動を計算できるようになっているのです.

アフィン変換

前置きが長くなりましたが,ここからアフィン変換の仕組みについて説明して行きます.

3 × 3 行列と 3 次元ベクトル

アフィン変換は 2 次元平面上の点を対象とした操作ですが,計算自体は 3 次元で行います.

ここで,3 × 3 行列と 3 次元ベクトルの積の計算を確認しておきましょう:

\[ \begin{pmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{pmatrix} \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} a_{11}x + a_{12}y + a_{13}z \\ a_{21}x + a_{22}y + a_{23}z \\ z_{31}x + a_{32}y + a_{33}z \end{pmatrix} \]

ここで,{z = 0} の場合を考え,{x, y} 要素のみに着目すると,積の結果は最初に扱っていた 2 次元の場合と一致します:

\[ \begin{pmatrix} a_{11}x + a_{12}y \\ a_{21}x + a_{22}y \end{pmatrix} \]

アフィン変換の計算は 3 次元で行うといいましたが, 実際の変換対象の値としては {x, y} 成分のみに着目することになります.

回転と平行移動のアフィン変換

それでは,回転と平行移動を行うアフィン変換はどんなものか見てみましょう. それは,次のような行列で表されます:

\[ \begin{pmatrix} \cos\theta & -\sin\theta & \Delta x \\ \sin\theta & \cos\theta & \Delta y \\ 0 & 0 & 1 \end{pmatrix} \]

左上の 2 次元相当部分が回転行列,右上の部分が平行移動の量,他の部分は計算の帳尻合わせのために 0 や 1 として定義します.

アフィン変換で点 {(x, y)} を扱うとき,実際に計算に使うのは次のようなベクトルです:

\[ \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} \]

実際に積を計算してみましょう:

\[ \begin{align} \begin{pmatrix} \cos\theta & -\sin\theta & \Delta x \\ \sin\theta & \cos\theta & \Delta y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} &= \begin{pmatrix} x\cos\theta - y\sin\theta + \Delta x \\ x\sin\theta + y\cos\theta + \Delta y \\ 1 \end{pmatrix} \\ &= \begin{pmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \\ 1 \end{pmatrix} + \begin{pmatrix} \Delta x \\ \Delta y \\ 0 \end{pmatrix} \end{align} \]

計算の結果ですが,{x, y} 成分に着目すると,前述の回転行列の結果に平行移動を加えた値になっています.

回転は,{x, y} 成分に対して回転行列がそのまま適用された格好になっています.

平行移動については,{z} 成分をわざと 1 にしていることによって,平行移動させたい量だけ値が加算されるようになっています. 前の記述で,行列で平行移動を行うことは「そのままではできない」としていましたが,あえて {z} 座標を計算に利用するという工夫によって問題を解決しています.

また,{z} 成分は再び 1 となっています.これは帳尻合わせということで 0 や 1 といった値を当てはめていた部分がうまく働いた結果です. アフィン変換の結果 {z} 座標が 1 になるということは,計算結果に対して繰り返しアフィン変換を適用していくことができるということです.

演習問題

具体的な例として,簡単で分かりやすい例を計算してみましょう. 中途半端な角度を使うと,計算結果が正しいのかよくわからなくなるので,90 度か 45 度の倍数の角と使うと分かりやすいでしょう.

例えば

  • {(1, 0)} を 90 度回転させたら {(0, 1)} になるはずです.そのような変換を行うアフィン変換の行列を書いてみて,実際に計算してみてください.
  • {(1, 0)} を 90 度回転させ,さらに x 軸の正の向きに 1,y 軸の正の向きに 1 だけ平行移動をすると,{(1, 2)} になるはずです.そのような変換を行うアフィン変換の行列を書いてみて,実際に計算してみてください.

といった例を試すと,実際に点が移動するということが理解できると思います.

おわりに

今回は行列の計算の定義にしたがって,アフィン変換がどのように回転と平行移動を実現しているのかを見てみました.

アフィン変換には,今回紹介した回転と平行移動以外にも,拡大やせん断といった使い方があります. 数学的には,行列自体がそういう変換を行う性質を持っているという話であり,仕組みとしては今回説明した範囲と同様 (のはず) です.

行列の一般的な性質は,線形代数と呼ばれる分野でよく扱われる話です. こちらも興味がある方は詳しく調べて見るとよいでしょう.

今回扱わなかったこれらの話題と,実際にプログラムでアフィン変換をしてみるというのは,今後このブログで扱うこともあるかもしれません.

参考文献