The Go Blog

Go imageパッケージ

Nigel Tao
2011年9月21日

はじめに

imageおよびimage/colorパッケージは、いくつかの型を定義しています。color.Colorcolor.Modelは色を記述し、image.Pointimage.Rectangleは基本的な2次元幾何学を記述し、image.Imageはこれら2つの概念を組み合わせて色の長方形グリッドを表します。別の記事では、image/drawパッケージを使用した画像の構成について説明しています。

色とカラーモデル

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つの重要な微妙な点があります。まず、赤、緑、青はアルファ乗算されています。完全に飽和した赤で、かつ25%透明な場合は、RGBAが75%のrを返すことで表されます。次に、チャネルは16ビットの有効範囲を持っています。100%の赤は、RGBAが255ではなく65535のrを返すことで表されるため、CMYKやYCbCrからの変換で情報が失われることが少なくなります。3つ目に、戻り値の型は、最大値が65535であってもuint32であり、2つの値を乗算してもオーバーフローしないことを保証するためです。このような乗算は、Porter and Duffの古典的な代数に倣って、3番目の色のアルファマスクに従って2つの色をブレンドする場合に発生します。

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は、従来の「チャネルあたり8ビット」の色を表す構造体です。

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

RGBARフィールドは、[0, 255]の範囲の8ビットのアルファ乗算された色であることに注意してください。RGBAは、その値を0x101で乗算して、[0, 65535]の範囲の16ビットのアルファ乗算された色を生成することにより、Colorインターフェースを満たします。同様に、NRGBA構造体型は、PNG画像形式で使用される8ビットの非アルファ乗算された色を表します。NRGBAのフィールドを直接操作する場合、値は非アルファ乗算されますが、RGBAメソッドを呼び出す場合、戻り値はアルファ乗算されます。

Modelは、単純にColorを他のColorに変換できるもので、場合によっては情報が失われることもあります。たとえば、GrayModelは任意のColorを彩度を落としたGrayに変換できます。Paletteは任意のColorを限られたパレットのいずれかに変換できます。

type Model interface {
    Convert(c Color) Color
}

type Palette []Color

点と長方形

Pointは、整数グリッド上の(x, y)座標であり、軸は右と下に増加します。これはピクセルでもグリッドの四角でもありません。Pointには固有の幅、高さ、色はありませんが、以下の視覚化では小さな色の付いた四角形を使用しています。

type Point struct {
    X, Y int
}
p := image.Point{2, 1}

Rectangleは、整数グリッド上の軸に沿った長方形であり、その左上と右下のPointによって定義されます。Rectangleにも固有の色はありませんが、以下の視覚化では、細い色の線で長方形の輪郭を描き、そのMinMaxPointを強調表示しています。

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の場合、p.In(r)r.Min.X <= p.X && p.X < r.Max.Xの場合にのみ真であり、Yについても同様です。これは、スライスs[i0:i1]が下端は含まれ、上端は排他的であるのと似ています。(配列やスライスとは異なり、Rectangleはしばしばゼロ以外の原点を持つことがあります。)

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が移動します。PointsとRectanglesは右下の四分円に限定されるものではありません。

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つのRectanglesを交差させると、別のRectangleが得られますが、これは空になることがあります。

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}

PointsとRectanglesは値で渡され、値で返されます。Rectangle引数を取る関数は、2つのPoint引数、または4つのint引数を取る関数と同じくらい効率的です。

画像

Imageは、Rectangle内のすべてのグリッドの四角形をModelColorにマッピングします。「(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が含まれており、最初のImage以降の各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実装もSubImageメソッドを提供し、同じ配列によってバックアップされたImageを返します。サブ画像のピクセルを変更すると、元の画像のピクセルに影響を与えます。これは、サブスライスs[i0:i1]の内容を変更すると、元のスライスsの内容に影響を与えるのと同様です。

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でカバーされるピクセルは青色で陰影が付けられています。AtおよびSetメソッドや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関数でその形式を登録し、メインパッケージは形式登録の副次的な効果のみのためにそのようなパッケージを「アンダースコアインポート」します。

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)
}

次の記事:Go image/drawパッケージ
前の記事:反射の法則
ブログインデックス