Advent Art:北斎の絵を生成する
ここ数年、日本のF# コミュニティは「F# Advent Calendar」というイベントを開催しています (2010年、 2011年、 2012年、 そして 今年)。 これはadvent dayごとに1人ずつ、F#に関連した何かしら興味深い記事を作成するというものです。 私は去年からTwitterでadvent calendarをチェックしていて、 今年からは私も参加しようと思い、記事を書きたいと申し出ました。 そうしたところ、数名の方からの協力を得ることができました。 @igeta には参加手続きの諸々とレビューを、 @yukitos にはこの記事の翻訳を、そして @gab_km には翻訳のレビューをしていただきました。 ありがとう!
けれども何についての記事を書くのがよいのでしょう? 過去一年にわたって、F#コミュニティで開発されているF#のオープンソースライブラリやプロジェクトを いくつか紹介できるような記事がよさそうです。 それと同時に、日本に関連のあるトピックが何かないものでしょうか? 少し考えてみたところ、以下のようなプランを思いつきました:
-
まず、日本の絵画について学ぶために F# Data ライブラリと Freebase を組み合わせて使う。 このライブラリにはいまや 日本語ドキュメント があり、作成してくれた @yukitos に感謝しています。
-
そして絵画作品を1つ選択して、F#でその作品を再生成する。 私の絵画スキルでは到底無理なのですが、試してみることはできます :-)
-
最後に、 FunScriptプロジェクト を使って F#コードをJavaScriptに変換します。 そうすると純粋なHTML Webアプリケーションとして実行できるようになり、 携帯電話やその他のデバイスでも動作するようになります。
日本の芸術の探索
Freebase は主にWikipediaを情報源とする 体系的な情報を持ったオンラインのグラフデータベースです。 このデータベースには(政治、芸能などの)社会や(多種多様な)スポーツ、 (コンピュータや化学などの)サイエンス、そして芸術など、様々な分野に関する情報が 含まれています。
F# DataにあるFreebase用のF# 型プロバイダーを使用すると、
コードエディタ上から直接これらの情報すべてにアクセスできます。
この型プロバイダーを使うには、まず FSharp.Data
のNuGetパッケージをインストールした後、
以下のようにしてアセンブリへの参照を追加します:
1: 2: 3: 4: 5: |
|
FSharp.Data
とは別に、いくつかLINQメソッドを使ってデータを探索します。
Freebaseへのアクセスには FreebaseData
または FreebaseDataProvider
を使います。
後者には大量のリクエストを発行するコードを作成する場合に必要になるAPIキーを指定します。
サンプルをきちんと動かすためには、登録を行う必要があるでしょう:
1: 2: |
|
そして fb.
と入力すると、Freebase上で利用できる各分野が一覧表示されます。
たとえば絵画や画家が含まれている「Visual Art」の分野を参照して、
(「Visual Artists」という)リストから1番目の画家を取得し、
画家に関する情報を調べることができます:
1: 2: 3: 4: 5: |
|
このコードを実行してみると、リストの1番目の画家が
レオナルド・ダ・ビンチ(Leonardo da Vinci)であることが確認できるでしょう。
Country of nationality
プロパティは実際にはlistを返しますが、
今回の場合は単にイタリア(Italy)という1カ国の名前だけが含まれます。
また、 Artworks
プロパティを使うと画家の作品一覧を取得できます。
次に、日本国籍を持った画家一覧を見つけるクエリを作成します (簡単のために、国籍リストの最初の1つだけしかチェックしていません):
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
このスニペットを実行すると、Yoshitaka Amano(天野喜孝)やIsamu Noguchi(イサムノグチ)、
Takashi Murakami(村上隆)といった20世紀生まれの画家が最初に表示されます。
sortBy
や sortByDescending
を追加して、特定の順序に並び替えることもできます。
また、特定の1人の画家を検索するクエリに head
を組み合わせて、1人の画家に関する
詳細情報を取得することもできます:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
スニペットの後半では北斎の様々な情報をHTML形式で出力しています。 結果は以下のようになります:
Katsushika Hokusai (葛飾 北斎, September 23, 1760 – May 10, 1849) was a Japanese artist, ukiyo-e painter and printmaker of the Edo period. He was influenced by such painters as Sesshu, and other styles of Chinese painting. Born in Edo (now Tokyo), Hokusai is best known as author of the woodblock print series Thirty-six Views of Mount Fuji (富嶽三十六景, Fugaku Sanjūroku-kei, c. 1831) which includes the internationally recognized print, The Great Wave off Kanagawa, created during the 1820s.
The Dream of the Fisherman's Wife
The Great Wave off Kanagawa
Travellers Crossing the Oi River
Red Fuji
Feminine Wave
Masculine Wave
Black Fuji
Oceans of Wisdom
北斎とフラクタル
北斎の作品を見てみると、フラクタルのように見えるものがいくつかあることがわかります。 Hokusai と fractal でGoogle画像検索 してみると、北斎の作品とよく似たフラクタルが多数見つかることがわかります。
この記事ではジュリア集合のフラクタルを使います。 このフラクタルは(特にF#を使用すると)非常に簡単に描画できます。 神奈川沖浪裏(The Great Wave off Kanagawa)によく似たフラクタルになるようにするため、 フラクタルを色づけるためのカラーパレットを慎重に選択することになります。
ジュリア集合の計算
ジュリア集合については Wikipediaのページ に詳しいので、ここでは動作の詳細については説明しません。 大まかにいえば、複素数のシーケンスを生成して、何らかの定数 c に対して 各ステップ毎に cnext = cprev2 + c という式で次の値を計算していきます。 この反復処理は無限のシーケンスを生成する(可能性のある) 再帰的なシーケンス式を書くことでいい具合に作成できます:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
この関数は(-1から+1までの)座標を引数にとり、画面上の1ピクセルに対する
シーケンスを生成します。
次に、絶対値が2以上になるまで(あるいは反復回数の最大値が特定の制限値を超えるまで)
反復回数を数える必要があります。
具体的には Seq
モジュールの関数を以下のように使います:
1: 2: 3: 4: 5: |
|
まず、反復回数を特定の値に制限していて、
次に絶対値が小さい値を取得しつづけて、
最後にシーケンスの長さを計算しています。
(絶対値が2.0を超えると takeWhile
関数はシーケンスの生成を終了します。
また、 length
の値は max - 1
以下になります。)
このコードは命令的な方法でも実装できます。
実際その方がパフォーマンスがいいかもしれません。
しかし今回の方法の方がジュリア集合の定義に則していることがはっきりわかります。
カラーパレットの生成
フラクタルを描画するためには、描画したいすべてのピクセルを走査して、
それぞれのピクセルに対して特定の x
および y
を引数に指定して
countInterations
を呼び出し、反復回数に応じた色をパレットから
取得することになります。
北斎の絵画をフラクタルで表現するためには、このパレットを慎重に生成する必要があります。
アイデアとしては神奈川沖浪裏(The Great Wave off Kanagawa)で使われている
多数の色のグラデーションを作成します。
そこで、 clr1 -- count --> clr2
というような記述ができるように
2つのカスタムF#演算子を用意します。
この式は clr1
から clr2
まで、ステップ数 count
で
グラデーションするような色を表します:
1: 2: 3: 4: 5: 6: 7: 8: |
|
--
演算子はタプルを生成する構文的なトリックにすぎません。
式は (clr1 -- count) --> clr2
というように解析されるので、
2番目の演算子は初期色とステップ数を受け取って、
グラデーションを構成する色のリストを生成することができます。
そうすると配列式と yield!
を使って、各グラデーションを組み合わせることで
うまい具合にパレットを生成することができます。
まず空色から始めて、(水を表す)青系のグラデーションをいくつか生成し、
最後に白系のグラデーションを追加します:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
フラクタルの描画
さてこれでフラクタルを描画するためのものがすべて揃いました。
最初のバージョンをシンプルなものにするために、まずはWindowsフォームと
System.Drawing
の Bitmap
を使うことにしましょう。
フォームを作成するコードについては省略します
(ただし GitHub上に完全なソースコード が置いてあります)。
フォームとビットマップを作成した後は、ビットマップの各ピクセルを走査して、 (パレットの長さを最大値として)反復の回数を数えて、 その値をインデックスにしてパレットから色を取得します:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
|
引数 w
と h
はそれぞれ描画するフラクタルの部分を表すタプルです。
これらの値を -2.0, 2.0
と -1.5, 1.5
に変更すればフラクタル全体が表示されます。
ここでは以下のような素敵な絵が表示されるように、フラクタルの特別な場所を
選択しています:
Fun(Script)の追加
さてこれでフラクタルを描画するコードが出来上がったわけですが、
Windowsフォームプロジェクトを新しく作成することなしに、
ライブで実行される様子を皆さんも見てみたいのではないかと思います。
幸いにも私たちはF#コードをJavaScriptに変換する FunScript
というコンパイラが使えるわけなので、HTML5の <canvas>
要素に
フラクタルを描画するようにできます。
FunScriptには既にHTML5でマンデルブロ集合を描画するようなサンプルがあるので、
これもまた実に簡単に実装できます。
ただし重要なのは、これまで作成してきたコードを単に再利用することができる、という点です。
詳細をすべて説明するスペースもないので、重要なリンクをいくつか紹介するにとどめます:
- FunScriptの詳細については プロジェクトのホームページ を参照してください。
- FunScriptバージョンのソースコード は私のGitHubにあります。
- そして、 実際にコードが動作しているバージョン を見ることもできます。
まず描画関数を変更するところから見ていきましょう。 実際にJavaScriptで動作しているバージョンをみてもわかるように、 描画処理がやや遅いため、列を描画する毎に表示している絵を更新しています。 (元々のコードを動的型付けであるJavaScriptへとコピーしているだけなので、 パフォーマンスの低下は想定通りです。 命令的なスタイルに書き直せばコードのパフォーマンスを改善できるでしょう。)
しかし描画関数をF#の async
ワークフローでラップして、1列描画が終わる度に
Async.Sleep
を呼んで絵を更新するようにすればかなりいい感じのコードになります:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
|
このコードは先ほどのものとほとんど同じです。
なおFunScriptにはJavaScriptライブラリのほとんどの機能に対する型付きのラッパーがあるため、
コード中の変数はいずれも静的に型付けされます(つまり canv
は HTMLCanvasElement
型で、
描画コンテキストを取得する getContext_2d
メソッドなどを持ちます)。
また、1つのピクセルに色を設定するためのヘルパー関数 setPixel
や、
IDによってHTML要素を得るための動的検索演算子 ?
を使っています
(いずれも次の節で紹介します)。
描画を開始するにはイベントハンドラをセットアップして StartImmediate
メソッドを呼び出します:
1: 2: 3: 4: |
|
ここで使っている2つのヘルパー関数もかなり単純なものです。
setPixel
は ImageData
配列のオフセットを計算してR,G,Bのコンポーネント
(とアルファチャネル)を設定しているだけです。
動的演算子も、 getElementById
を呼んで、期待される型に要素をキャストして返しているだけです:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
まとめ
まず最初に、私がこの記事を楽しんで書くことが出来たのと同じように、 皆さんが私の記事を楽しく読むことが出来ていますように。 そしてとても素敵なクリスマスシーズンを皆さんが過ごせていますように!
冒頭でも触れましたが、私はここ最近F#コミュニティで開発されている 興味深いライブラリを紹介したいと思っていて、また日本に関連のあるテーマと 絡めて紹介できたらいいなと思っていました。 最初は F# Data 、 特にFreebase型プロバイダを使って日本の画家とそれぞれの作品のリストを取得しました。
次に北斎を題材にして、適切に選択したパレットを使ったジュリア集合で 彼の作である神奈川沖浪裏の再作成に挑戦しました。 最初のバージョンでは、Windowsフォームを使って描画しました。 Webブラウザ上で直接実行できるコードにするためには、 FunScriptを使ってF#をJavaScriptへと変換しました。 ここでは既存のコードを変更する必要がほとんどありませんでした。 フラクタルの描画進捗を確認できるようにするには、 (ボーナスとして)非同期ワークフローを追加するだけでした。
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Data
--------------------
namespace Microsoft.FSharp.Data
Full name: Japan-advent-art.FreebaseData
Full name: FSharp.Data.FreebaseDataProvider
<summary>Typed representation of Freebase data with additional configuration parameters</summary>
<param name='Key'>The API key for the MQL metadata service (default: none)</param>
<param name='ServiceUrl'>The service URL for the MQL metadata service (default: https://www.googleapis.com/freebase/v1)</param>
<param name='NumIndividuals'>The maximum number of sample individuals for each Freebase type (default: 1000)</param>
<param name='UseUnitsOfMeasure'>Use the unit-of-measure annotations from the data source metadata (default: true)</param>
<param name='Pluralize'>Use adhoc rules to pluralize the names of types when forming names of collections (default: true)</param>
<param name='SnapshotDate'>Use a snapshot of the web data store at the given date and/or time in ISO8601 format, e.g., 2012-01-18, 2012-09-15T21:11:32. A value of 'now' indicates the compile time of the code. (default: no snapshot)</param>
<param name='LocalCache'>Use a persistent local cache for schema requests. Also provides the default for whether a persistent local cache is used at runtime. A per-session cache is always used for schema data but it will not persist if this is set to 'false'. (default: true)</param>
<param name='AllowLocalQueryEvaluation'>Allow local evalution of some parts of a query. If false, then an exception will be raised if a query can't be evaluated fully on the server. If true, data sets may be implicitly brought to the client for processing. (default: true)</param>
Full name: Japan-advent-art.fb
FreebaseDataProvider<...>.GetDataContext() : FreebaseDataProvider<...>.ServiceTypes.FreebaseService
--------------------
FreebaseData.GetDataContext() : FreebaseData.ServiceTypes.FreebaseService
Full name: Japan-advent-art.art
Full name: Japan-advent-art.ldv
Full name: Japan-advent-art.artists
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.query
Calls Linq.QueryBuilder.Where
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Full name: Japan-advent-art.hok
Calls Linq.QueryBuilder.Head
Full name: Japan-advent-art.c
それらしいフラクタルを生成するための定数
type Complex =
struct
new : real:float * imaginary:float -> Complex
member Equals : obj:obj -> bool + 1 overload
member GetHashCode : unit -> int
member Imaginary : float
member Magnitude : float
member Phase : float
member Real : float
member ToString : unit -> string + 3 overloads
static val Zero : Complex
static val One : Complex
...
end
Full name: System.Numerics.Complex
--------------------
Complex()
Complex(real: float, imaginary: float) : unit
Full name: Japan-advent-art.iterate
特定の座標に対するシーケンスを生成する
val seq : sequence:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Core.Operators.seq
--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>
Full name: Microsoft.FSharp.Collections.seq<_>
Full name: Japan-advent-art.countIterations
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.take
Full name: Microsoft.FSharp.Collections.Seq.takeWhile
Full name: Microsoft.FSharp.Collections.Seq.length
val float : value:'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float = System.Double
Full name: Microsoft.FSharp.Core.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
struct
member A : byte
member B : byte
member Equals : obj:obj -> bool
member G : byte
member GetBrightness : unit -> float32
member GetHashCode : unit -> int
member GetHue : unit -> float32
member GetSaturation : unit -> float32
member IsEmpty : bool
member IsKnownColor : bool
...
end
Full name: System.Drawing.Color
Color.FromArgb(alpha: int, baseColor: Color) : Color
Color.FromArgb(red: int, green: int, blue: int) : Color
Color.FromArgb(alpha: int, red: int, green: int, blue: int) : Color
Full name: Japan-advent-art.palette
let f = new Form(Visible=true, ClientSize=Size(400, 300))
let i = new PictureBox()
i.SizeMode <- PictureBoxSizeMode.Zoom
i.Dock <- DockStyle.Fill
f.Controls.Add(i)
Full name: Japan-advent-art.w
Full name: Japan-advent-art.h
Full name: Japan-advent-art.width
Full name: Japan-advent-art.f
Full name: Japan-advent-art.height
Full name: Japan-advent-art.bmp
type Bitmap =
inherit Image
new : filename:string -> Bitmap + 11 overloads
member Clone : rect:Rectangle * format:PixelFormat -> Bitmap + 1 overload
member GetHbitmap : unit -> nativeint + 1 overload
member GetHicon : unit -> nativeint
member GetPixel : x:int * y:int -> Color
member LockBits : rect:Rectangle * flags:ImageLockMode * format:PixelFormat -> BitmapData + 1 overload
member MakeTransparent : unit -> unit + 1 overload
member SetPixel : x:int * y:int * color:Color -> unit
member SetResolution : xDpi:float32 * yDpi:float32 -> unit
member UnlockBits : bitmapdata:BitmapData -> unit
...
Full name: System.Drawing.Bitmap
--------------------
Bitmap(filename: string) : unit
(+0 other overloads)
Bitmap(stream: System.IO.Stream) : unit
(+0 other overloads)
Bitmap(original: Image) : unit
(+0 other overloads)
Bitmap(filename: string, useIcm: bool) : unit
(+0 other overloads)
Bitmap(type: System.Type, resource: string) : unit
(+0 other overloads)
Bitmap(stream: System.IO.Stream, useIcm: bool) : unit
(+0 other overloads)
Bitmap(width: int, height: int) : unit
(+0 other overloads)
Bitmap(original: Image, newSize: Size) : unit
(+0 other overloads)
Bitmap(width: int, height: int, format: Imaging.PixelFormat) : unit
(+0 other overloads)
Bitmap(width: int, height: int, g: Graphics) : unit
(+0 other overloads)
Full name: Microsoft.FSharp.Core.Operators.snd
Full name: Microsoft.FSharp.Core.Operators.fst
Full name: Japan-advent-art.i
Full name: Japan-advent-art.palette
Full name: Japan-advent-art.setPixel
Full name: ImageData
interface
inherit MSNodeExtensions
inherit MSResourceMetadata
inherit DocumentEvent
inherit MSEventAttachmentTarget
inherit NodeSelector
inherit Node
end
Full name: Document
Full name: Microsoft.FSharp.Core.Operators.failwith
Full name: Japan-advent-art.render
列毎にスリープしつつ、非同期的にフラクタルを描画する
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
interface
inherit HTMLElement
end
Full name: HTMLCanvasElement
Full name: Globals
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Japan-advent-art.go
interface
inherit MSDataBindingExtensions
inherit HTMLElement
end
Full name: HTMLButtonElement
Full name: Japan-advent-art.setPixel
ImageDataにあるピクセルの値を特定の色に設定する