Go image/draw パッケージ

The Go image/draw package by Nigel Tao

はじめに

image/draw パッケージが定義している操作はただ一つです。それは、ソース画像をマスク画像(省略可能)を介して、デスティネーション画像上に描画するというものです。この一つの操作は驚くほど汎用性が高く、一般的な画像操作のタスクの多くを、エレガントかつ効率的にこなせます。

合成はPlan 9のグラフィックスライブラリやX Render拡張と同じ流儀で、ピクセル単位で行われます。このモデルは、Porter and Duffによる古典的な論文「Compositing Digital Images」を基にしており、マスクパラメータを追加した式は次の通りです。dst = (src IN mask) OP dst。マスクが完全に不透明であれば、これは元々のPorter-Duffの式である dst = src OP dst に帰着します。Goでは、マスク画像がnilであることは、無限に大きく完全に不透明なマスク画像と等価です。

Porter-Duffの論文では12種類の合成演算子が示されていますが、明示的なマスクがある場合、実際に必要になるのはそのうち2つ、source-over-destinationとsourceだけです。Goでは、これらの演算子はOverSrcという定数で表現されています。Over演算子はソース画像をデスティネーション画像の上に自然に重ねる処理を行います。マスク適用後のソース画像がより透明である、つまりアルファ値がより低いほど、デスティネーション画像への変更は小さくなります。一方Src演算子は、デスティネーション画像の元の内容を一切考慮せず、マスク適用後のソース画像を単純にコピーするだけです。ソースとマスクの両方が完全に不透明な場合、この2つの演算子は同じ出力になりますが、Src演算子の方が通常は高速です。

幾何学的な位置合わせ

合成を行うには、デスティネーション画像のピクセルと、ソース画像やマスク画像のピクセルとを対応付ける必要があります。当然ながら、これにはデスティネーション、ソース、マスクという3つの画像と合成演算子が必要ですが、それに加えて各画像のどの矩形範囲を使うかも指定する必要があります。すべての描画がデスティネーション全体に書き込むべきとは限りません。アニメーションする画像を更新する場合、変化した部分だけを描画する方が効率的です。すべての描画がソース全体から読み込むべきとも限りません。多数の小さな画像を1つの大きな画像にまとめたスプライトを使う場合、必要なのは画像の一部分だけです。すべての描画がマスク全体から読み込むべきとも限りません。フォントのグリフを集めたマスク画像もスプライトと似た性質を持ちます。したがって、描画には画像ごとに1つずつ、計3つの矩形も必要になります。それぞれの矩形は同じ幅と高さを持つので、デスティネーション矩形rと2つの点spmpを渡すだけで十分です。ソース矩形は、デスティネーション画像におけるr.Minがソース画像におけるspに一致するようにrを平行移動したものと等しくなり、mpについても同様です。有効な矩形は、それぞれの画像の座標空間における境界にもクリップされます。

DrawMask関数は7つの引数を取りますが、明示的なマスクとマスクの点を指定する必要がないことが多いため、Draw関数は5つの引数を取ります。

// DrawはnilマスクでDrawMaskを呼び出します。
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point,
 mask image.Image, mp image.Point, op Op)

デスティネーション画像は変更可能でなければならないため、image/drawパッケージはSetメソッドを持つdraw.Imageインターフェースを定義しています。

type Image interface {
    image.Image
    Set(x, y int, c color.Color)
}

矩形を塗りつぶす

矩形を単色で塗りつぶすには、image.Uniformをソースとして使います。ColorImage型は、Colorを実質的に無限のサイズを持つその色のImageとして再解釈します。Plan 9の描画ライブラリの設計に馴染みがある人向けに補足すると、Goのスライスベースの画像型では明示的な「repeat bit」は不要です。この概念はUniformに包含されています。

// image.ZPはゼロ点、つまり原点です。
draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src)

新しい画像をすべて青色で初期化するには次のようにします。

m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)

画像を透明(デスティネーション画像の色モデルが透明度を表現できない場合は黒)にリセットするには、image.Uniformであるimage.Transparentを使います。

draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src)

画像のコピー

ソース画像内の矩形srから、デスティネーション内の点dpを起点とする矩形へコピーするには、ソース矩形をデスティネーション画像の座標空間に変換します。

r := image.Rectangle{dp, dp.Add(sr.Size())}
draw.Draw(dst, r, src, sr.Min, draw.Src)

あるいは次のようにも書けます。

r := sr.Sub(sr.Min).Add(dp)
draw.Draw(dst, r, src, sr.Min, draw.Src)

ソース画像全体をコピーするには、sr = src.Bounds()とします。

画像のスクロール

画像のスクロールとは、デスティネーションとソースの矩形を変えて画像を自分自身にコピーするだけの処理です。デスティネーションとソースの画像が重なっていても問題ありません。ちょうどGoの組み込みのcopy関数が、デスティネーションとソースのスライスが重なっている場合を扱えるのと同じです。画像mを20ピクセルスクロールするには次のようにします。

b := m.Bounds()
p := image.Pt(0, 20)
// 2番目の引数はbですが、クリッピングにより
// 実際に有効な矩形はそれよりも小さくなることに注意してください。
draw.Draw(m, b, m, b.Min.Add(p), draw.Src)
dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y))

画像をRGBAに変換する

ある画像形式をデコードした結果は、必ずしもimage.RGBAになるとは限りません。GIFをデコードするとimage.Palettedになり、JPEGをデコードするとycbcr.YCbCrになり、PNGをデコードした結果は画像データによって変わります。任意の画像をimage.RGBAに変換するには次のようにします。

b := src.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)

マスクを通した描画

中心p、半径rの円形マスクを通して画像を描画するには、次のようにします。

type circle struct {
    p image.Point
    r int
}

func (c *circle) ColorModel() color.Model {
    return color.AlphaModel
}

func (c *circle) Bounds() image.Rectangle {
    return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
}

func (c *circle) At(x, y int) color.Color {
    xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
    if xx*xx+yy*yy < rr*rr {
        return color.Alpha{255}
    }
    return color.Alpha{0}
}

draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over)

フォントのグリフを描画する

pを起点として青色でフォントのグリフを描画するには、image.ColorImageをソース、image.Alphaをマスクとして描画します。ここでは単純化のため、サブピクセル単位の位置調整やレンダリング、ベースラインからのフォントの高さの補正は一切行いません。

src := &image.Uniform{color.RGBA{0, 0, 255, 255}}
mask := theGlyphImageForAFont()
mr := theBoundsFor(glyphIndex)
draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over)

パフォーマンス

image/drawパッケージの実装は、汎用性を保ちながらも一般的なケースでは効率的に動作する画像操作関数をどのように提供するかを示す好例です。DrawMask関数はインターフェース型の引数を取りますが、内部ではただちに、引数が特定の構造体型であるという型アサーションを行います。これは、あるimage.RGBA画像を別のimage.RGBA画像に描画する、あるいは(フォントのグリフのような)image.Alphaマスクをimage.RGBA画像に描画するといった一般的な操作に対応するものです。型アサーションが成功すれば、その型情報を使って一般アルゴリズムの特化した実装を実行します。アサーションが失敗した場合のフォールバック処理では、汎用的なAtメソッドとSetメソッドを使います。この高速パスは純粋にパフォーマンス最適化のためのものであり、結果として得られるデスティネーション画像はどちらの経路でも同じです。実際のアプリケーションでは、こうした特殊なケースはごく少数用意するだけで十分です。

By Nigel Tao