Go image パッケージ(The Go image package)

Go image パッケージ #

The Go image package By Nigel Tao

はじめに #

imageimage/color パッケージはいくつかの型を定義しています。color.Colorcolor.Model は色を、image.Pointimage.Rectangle は基本的な2次元幾何学をそれぞれ記述しており、image.Image は色の長方形格子を表現するためにそれら2つの概念を1つにまとめます。別記事 では image/draw を用いた画像の構成について取り上げています。

Color と Color Model #

Color は色として考えられる任意の型をまとめた最小限のメソッドの組を定義したインターフェースです。つまり、赤、緑、青、アルファ値に変換できるものです。CMYK や YCbCr 色空間から変換するといったある種の変換は不可逆かもしれません。

type Color interface {
    // RGBA returns the alpha-premultiplied red, green, blue and alpha values
    // for the color. Each value ranges within [0, 0xFFFF], but is represented
    // by a uint32 so that multiplying by a blend factor up to 0xFFFF will not
    // overflow.
    RGBA() (r, g, b, a uint32)
}

戻り値には3つの重要な巧妙さがあります。1つ目は、赤、緑、青はプリマルチプライド・アルファであることです。25% 透明な完全飽和赤は 75% の r を返す RGBA によって表現されます。2つ目に、チャネルの有効範囲は 16 ビットです。100% の赤は 255 ではなく 65535 の r を返す RGBA によって表現されているので、CMYK や YCbCr からの変換はさほど情報量を失いません。3つ目に、最大値は 65535 ですが、戻り値の型は uint32 となっていることです。これは2つの値の積がオーバーフローしないことを保証するためです。Porter-Duff の古典代数学のスタイルでアルファマスクに従う2つの色を混ぜ合わせて第3の色を生成する乗算の際に起ります。

dstr, dstg, dstb, dsta := dst.RGBA()
srcr, srcg, srcb, srca := src.RGBA()
_, _, _, m := mask.RGBA()
const M = 1<<16 - 1
// The resultant red value is a blend of dstr and srcr, and ranges in [0, M].
// The calculation for green, blue and alpha is similar.
dstr = (dstr*(M-m) + srcr*m) / M

もし非プリマルチプライド・アルファを使って処理を行おうとすると、コード片の最後の行はより複雑になるでしょう。それが Color がプリマルチプライド・アルファな値を使う理由です。

image/color パッケージも Color インターフェースを実装するいくつかの具象型を定義しています。例えば、RGBA は古典的な “1 チャネルあたり 8 ビット” の色を表現する構造になっています。

type RGBA struct {
    R, G, B, A uint8
}

RGBAR フィールドは 0 から 255 の範囲の値をとる 8 ビットのプリマルチプライド・アルファな色であることに注意してください。0 から 65535 の範囲の値をとる 16 ビットのプリマルチプライド・アルファな色を生成するために 0x101 を値に掛けることによって RGBAColor インターフェースを満たします。同様に、PNG の画像形式で使われるように NRGBA 構造型は 8 ビットの非プリマルチプライド・アルファな色を表現します。NRGBA のフィールドを直接操作する際は、その値は非プリマルチプライド・アルファですが、RGBA メソッドを呼ぶ際は、その戻り値はプリマルチプライド・アルファです。

Model は単純なもので、おそらく非可逆的に を別の に変換することができます。例えば、GrayModel は 任意の を不飽和化された Gray に変換できます。 Palette は制限されたパレットにより任意の をある に変換できます。

type Model interface {
    Convert(c Color) Color
}

type Palette []Color

Point と Rectangle #

Point は右方向と下方向に増加する軸に従った整数格子上の (x, y) 座標です。それはピクセルでも格子で区切られた正方形でもありません。Point は固有の幅や高さ、色を持っていませんが、以下の図では色付きの小さな正方形を用いています。

type Point struct {
    X, Y int
}

go-image-package_image-package-01

p := image.Point{2, 1}

Rectangle は左上と右下の Point によって定義される整数格子上にある軸に平行な長方形です。Rectangle も固有の色を持っていませんが、以下の図では色付きの細い線を用いて長方形の輪郭を描き、最小 および 最大Point を呼びます。

type Rectangle struct {
    Min, Max Point
}

便利のため image.Rect(x0, y0, x1, y1)image.Rectangle{image.Point{x0, y0}, image.Point{x1, y1}} と同値としますが、前者のほうが入力するのは簡単です。

Rectangle は左上の点を排他し、右下の点を包含しています。Point pRectangle r に対し、r.Min.X <= p.X && p.X < r.Max.X を満たす場合のみ p.In(r) と表します、Y についても同様です。これはスライス s[i0:i1] がどうやって下限を含み上限を排他するのかということに似ています。(配列やスライスと違い、Rectangle はよく非ゼロの原点を含みます。)

go-image-package_image-package-02

r := image.Rect(2, 1, 5, 5)
// Dx and Dy return a rectangle's width and height.
fmt.Println(r.Dx(), r.Dy(), image.Pt(0, 0).In(r)) // prints 3 4 false

PointRectangle に加えることでその Rectangle を変化させます。点と長方形は右下の象限内に制限されていません。

go-image-package_image-package-03

r := image.Rect(2, 1, 5, 5).Add(image.Pt(-4, -2))
fmt.Println(r.Dx(), r.Dy(), image.Pt(0, 0).In(r)) // prints 3 4 true

2つの長方形が交わると、もう1つの長方形が現れます、それは空かもしれません。

go-image-package_image-package-04

r := image.Rect(0, 0, 4, 3).Intersect(image.Rect(2, 2, 5, 5))
// Size returns a rectangle's width and height, as a Point.
fmt.Printf("%#v\n", r.Size()) // prints image.Point{X:2, Y:1}

点と長方形は値型によって関数に代入されたり戻されたりします。Rectangle を引数に取る関数は2つの Point か4つの int を引数にとる関数と同様に優れているでしょう。

Image #

ImageRectangle 内の格子で区切られた正方形を Model から作られる Color に射影します。"(x, y) のピクセル" は点 (x, y), (x+1, y), (x+1, y+1), (x, y+1) によって定義される格子で区切られた正方形の色を参照します。

type Image interface {
    // ColorModel returns the Image's color model.
    ColorModel() color.Model
    // Bounds returns the domain for which At can return non-zero color.
    // The bounds do not necessarily contain the point (0, 0).
    Bounds() Rectangle
    // At returns the color of the pixel at (x, y).
    // At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
    // At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
    At(x, y int) color.Color
}

よくある誤りは Image の範囲が (0, 0) から始まると思い込むことです。例えば、アニメーション GIF が画像列を含み、典型的に初めの画像以降のそれぞれの Image が変化した領域のピクセルデータだけ保持し、その領域は (0, 0) から始まる必要はありません。Image m のピクセル全体を走査するための正しい方法は以下のような感じです:

b := m.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
    for x := b.Min.X; x < b.Max.X; x++ {
        doStuffWith(m.At(x, y))
    }
}

Image の実装はピクセルデータのインメモリスライスを元にする必要はありません。例えば、Uniform は広大で一様な色の Image で、そのインメモリ表現は単なるその色です。

type Uniform struct {
    C color.Color
}

それでもなお典型的に、プログラムはスライスベースの画像を求めています。RGBAGray のような構造型(他のパッケージでは image.RGBAimage.Gray として参照する)はピクセルデータのスライスを保持し Image インターフェースを実装します。

type RGBA struct {
    // Pix holds the image's pixels, in R, G, B, A order. The pixel at
    // (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4].
    Pix []uint8
    // Stride is the Pix stride (in bytes) between vertically adjacent pixels.
    Stride int
    // Rect is the image's bounds.
    Rect Rectangle
}

それらの型も一度に1ピクセルを修正する Set(x, y int, c color.Color) メソッドを提供します。

m := image.NewRGBA(image.Rect(0, 0, 640, 480))
m.Set(5, 5, color.RGBA{255, 0, 0, 255})

たくさんのピクセルデータを読み書きしたい場合はより効率的にできますが、構造型の Pix フィールドに直接アクセスするのはより複雑になります。

スライスベースの Image の実装も同じ配列を元にした Image を返す SubImage メソッドを提供しています。サブスライス s[i0:i1] の内容を修正すると元のスライス s の内容に影響が及ぶことと同様に、サブ画像のピクセルを修正すると元の画像のピクセルに影響が及びます。

go-image-package_image-package-05

m0 := image.NewRGBA(image.Rect(0, 0, 8, 5))
m1 := m0.SubImage(image.Rect(1, 2, 5, 5)).(*image.RGBA)
fmt.Println(m0.Bounds().Dx(), m1.Bounds().Dx()) // prints 8, 4
fmt.Println(m0.Stride == m1.Stride)             // prints true

画像の Pix フィールドで動作する下層のコードを扱ううえで、Pix の範囲を超えることは画像の範囲外のピクセルに影響を与えるということを理解しておいてください。上記の例では、m1.Pix によって変換されたピクセルは青で共有されます。AtSet メソッドまたは image/draw パッケージ のような上層のコードは画像の範囲に対する操作を制限します。

画像の形式 #

標準パッケージライブラリは GIF、JPEG、PNG など多くのありふれた画像形式をサポートしています。もとの画像ファイルのフォーマットが分かっている場合、io.Reader から直接デコードできます。

import (
    "image/jpeg"
    "image/png"
    "io"
)

// convertJPEGToPNG converts from JPEG to PNG.
func convertJPEGToPNG(w io.Writer, r io.Reader) error {
    img, err := jpeg.Decode(r)
    if err != nil {
        return err
    }
    return png.Encode(w, img)
}

形式不明な画像データがある場合、image.Decode 関数は形式を検出することができます。認識される形式の集合は実行時に構築され、標準パッケージライブラリ内の形式に制限されていません。画像形式パッケージは典型的に init 関数内で自身の形式を登録し、main パッケージは形式の登録が副作用するようにそのようなパッケージ単体を “アンダースコアインポート” できます。

import (
    "image"
    "image/png"
    "io"

    _ "code.google.com/p/vp8-go/webp"
    _ "image/jpeg"
)

// convertToPNG converts from any recognized format to PNG.
func convertToPNG(w io.Writer, r io.Reader) error {
    img, _, err := image.Decode(r)
    if err != nil {
        return err
    }
    return png.Encode(w, img)
}

By Nigel Tao

あわせて読む #