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