Go 博客
Go image 包
引言
image 和 image/color 包定义了许多类型:color.Color
和 color.Model
描述颜色,image.Point
和 image.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
}
注意,RGBA
的 R
字段是一个范围在 [0, 255] 的 8 位预乘 alpha 颜色。RGBA
通过将该值乘以 0x101 来满足 Color
接口,生成一个范围在 [0, 65535] 的 16 位预乘 alpha 颜色。类似地,NRGBA
结构体类型表示一个 8 位非预乘 alpha 颜色,如 PNG 图像格式所使用的那样。直接操作 NRGBA
的字段时,值是非预乘 alpha 的,但调用 RGBA
方法时,返回的值是预乘 alpha 的。
一个 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
也没有固有的颜色,但下面的可视化图用细彩色线勾勒出矩形,并标出其 Min
和 Max
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
}
然而,通常情况下,程序会需要基于切片的图像。像 RGBA
和 Gray
(其他包称其为 image.RGBA
和 image.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
覆盖的像素区域以蓝色阴影表示。更高级别的代码,例如 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 包
上一篇文章:反射定律
博客索引