Go 部落格

Go image 包

Nigel Tao
2011 年 9 月 21 日

引言

imageimage/color 包定義了許多型別:color.Colorcolor.Model 描述顏色,image.Pointimage.Rectangle 描述基本的二維幾何,而 image.Image 將這兩個概念結合起來表示一個矩形顏色網格。另一篇文章涵蓋了使用 image/draw 包進行的影像合成。

顏色和顏色模型

Color 是一個介面,它定義了任何可以被認為是顏色的型別的最小方法集:一個可以轉換為紅色、綠色、藍色和 alpha 值的方法集。這種轉換可能是有損的,例如從 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)
}

關於返回值有三個重要的細微之處。首先,紅色、綠色和藍色是預乘 alpha 值:一個完全飽和的紅色,同時具有 25% 的透明度,由 RGBA 返回 75% 的 r 來表示。其次,通道的有效範圍是 16 位:100% 的紅色由 RGBA 返回 65535 的 r 來表示,而不是 255,這樣從 CMYK 或 YCbCr 轉換的損耗就不會那麼大。第三,返回的型別是 uint32,即使最大值是 65535,以保證兩個值相乘不會溢位。這種乘法發生在根據第三種顏色的 alpha 蒙版混合兩種顏色時,風格類似於 Porter 和 Duff 的經典代數。

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

如果使用非預乘 alpha 的顏色,上面的程式碼片段的最後一行會更復雜,這就是 Color 使用預乘 alpha 值的原因。

image/color 包還定義了許多實現了 Color 介面的具體型別。例如,RGBA 是一個表示經典“每通道 8 位”顏色的結構體。

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

注意,RGBAR 欄位是一個範圍在 [0, 255] 的 8 位預乘 alpha 顏色。RGBA 透過將該值乘以 0x101 來滿足 Color 介面,生成一個範圍在 [0, 65535] 的 16 位預乘 alpha 顏色。類似地,NRGBA 結構體型別表示一個 8 位非預乘 alpha 顏色,如 PNG 影像格式所使用的那樣。直接操作 NRGBA 的欄位時,值是非預乘 alpha 的,但呼叫 RGBA 方法時,返回的值是預乘 alpha 的。

一個 Model 簡單來說就是可以將 Color 轉換為其他 Color 的東西,轉換過程可能是有損的。例如,GrayModel 可以將任何 Color 轉換為去飽和的 GrayPalette 可以將任何 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 也沒有固有的顏色,但下面的視覺化圖用細彩色線勾勒出矩形,並標出其 MinMax 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 p 和一個 Rectangle r,當且僅當 r.Min.X <= p.X && p.X < r.Max.X 且 Y 的條件類似時,p.In(r) 為真。這類似於切片 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

將一個 Point 新增到一個 Rectangle 會平移該 Rectangle。點和矩形不限於在右下象限。

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

兩個矩形相交會產生另一個矩形,該矩形可能是空的。

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 引數的函式與一個接收兩個 Point 引數或四個 int 引數的函式效率相同。

影像

一個 Image 將一個 Rectangle 中的每個網格方塊對映到一個 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,其中第一個 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
}

這些型別還提供了一個 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 覆蓋的畫素區域以藍色陰影表示。更高級別的程式碼,例如 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 函式中註冊其格式,而主包會“下劃線匯入”這樣的包,僅為了其格式註冊的副作用。

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 包
上一篇文章:反射定律
部落格索引