Go 博客
Go 的 image/draw 包
引言
image/draw 包 只定义了一个操作:通过可选的蒙版图像将源图像绘制到目标图像上。这一个操作功能出奇地强大,能够优雅且高效地完成许多常见的图像处理任务。
组合操作采用 Plan 9 图形库和 X Render 扩展的风格,逐像素进行。该模型基于 Porter 和 Duff 的经典论文“Compositing Digital Images”,并增加了一个蒙版参数:dst = (src IN mask) OP dst
。对于完全不透明的蒙版,这可以简化为原始的 Porter-Duff 公式:dst = src OP dst
。在 Go 中,nil 蒙版图像等同于一个无限大、完全不透明的蒙版图像。
Porter-Duff 的论文介绍了 12 种不同的组合运算符,但有了显式的蒙版,实际上只需要其中 2 种:源覆盖目标 (source-over-destination) 和源 (source)。在 Go 中,这些运算符由 Over
和 Src
常量表示。Over
运算符执行将源图像自然地叠加到目标图像上:在源(经过蒙版处理后)越透明(即 alpha 值越低)的地方,对目标图像的改变就越小。Src
运算符仅复制源(经过蒙版处理后),而不考虑目标图像的原始内容。对于完全不透明的源图像和蒙版图像,这两种运算符会产生相同的输出,但 Src
运算符通常更快。
几何对齐
组合操作需要将目标像素与源像素和蒙版像素关联起来。显而易见,这需要目标图像、源图像和蒙版图像,以及一个组合运算符,还需要指定使用每张图像的哪个矩形区域。并非所有绘制操作都应该写入整个目标图像:在更新动画图像时,只绘制已更改的部分图像会更高效。并非所有绘制操作都应该读取整个源图像:在使用将多个小图像合并成一个大图像的精灵图时,只需要图像的一部分。并非所有绘制操作都应该读取整个蒙版:收集字体字形的蒙版图像类似于精灵图。因此,绘制操作还需要知道三个矩形,每个图像一个。由于每个矩形具有相同的宽度和高度,因此只需传递一个目标矩形 r
和两个点 sp
和 mp
:源矩形等于 r
翻译后的结果,使得目标图像中的 r.Min
与源图像中的 sp
对齐,对于 mp
也是如此。有效矩形还会被裁剪到各自坐标空间中每张图像的边界。

DrawMask
函数接受七个参数,但显式的蒙版和蒙版点通常是不必要的,因此 Draw
函数接受五个参数。
// Draw calls DrawMask with a nil mask.
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point,
mask image.Image, mp image.Point, op Op)
目标图像必须是可变的,因此 image/draw 包定义了一个 draw.Image
接口,该接口有一个 Set
方法。
type Image interface {
image.Image
Set(x, y int, c color.Color)
}
填充矩形
要用纯色填充矩形,请使用 image.Uniform
作为源。ColorImage
类型将 Color
重解释为一个该颜色的几乎无限大的 Image
。对于熟悉 Plan 9 图形库设计的人来说,在 Go 的基于切片的图像类型中不需要显式的“重复位”;这个概念被 Uniform
所涵盖。
// image.ZP is the zero point -- the origin.
draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src)
将新图像初始化为全蓝色
m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)
要将图像重置为透明(或者,如果目标图像的颜色模型无法表示透明度,则重置为黑色),请使用 image.Transparent
,它是一个 image.Uniform
。
draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src)

复制图像
要将源图像中的矩形 sr
复制到目标图像中以点 dp
开头的矩形,请将源矩形转换为目标图像的坐标空间。
r := image.Rectangle{dp, dp.Add(sr.Size())}
draw.Draw(dst, r, src, sr.Min, draw.Src)
或者
r := sr.Sub(sr.Min).Add(dp)
draw.Draw(dst, r, src, sr.Min, draw.Src)
要复制整个源图像,请使用 sr = src.Bounds()
。

滚动图像
滚动图像只是将图像复制到自身,但使用不同的目标和源矩形。目标和源图像的重叠是完全有效的,就像 Go 的内置 copy 函数可以处理重叠的目标和源切片一样。要将图像 m 向右滚动 20 像素
b := m.Bounds()
p := image.Pt(0, 20)
// Note that even though the second argument is b,
// the effective rectangle is smaller due to clipping.
draw.Draw(m, b, m, b.Min.Add(p), draw.Src)
dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y))

将图像转换为 RGBA
解码图像格式的结果可能不是 image.RGBA
:解码 GIF 会得到 image.Paletted
,解码 JPEG 会得到 ycbcr.YCbCr
,而解码 PNG 的结果取决于图像数据。要将任何图像转换为 image.RGBA
b := src.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)

通过蒙版绘制
要通过一个以 p
为中心、半径为 r
的圆形蒙版绘制图像
type circle struct {
p image.Point
r int
}
func (c *circle) ColorModel() color.Model {
return color.AlphaModel
}
func (c *circle) Bounds() image.Rectangle {
return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
}
func (c *circle) At(x, y int) color.Color {
xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
if xx*xx+yy*yy < rr*rr {
return color.Alpha{255}
}
return color.Alpha{0}
}
draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over)

绘制字体字形
要从点 p
开始绘制一个蓝色的字体字形,请使用 image.ColorImage
作为源,并使用 image.Alpha mask
作为蒙版。为简单起见,我们没有进行任何亚像素定位或渲染,也没有修正字体高于基线的偏移。
src := &image.Uniform{color.RGBA{0, 0, 255, 255}}
mask := theGlyphImageForAFont()
mr := theBoundsFor(glyphIndex)
draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over)

性能
image/draw 包的实现展示了如何提供一个既通用又在常见情况下高效的图像处理函数。DrawMask
函数接受接口类型的参数,但会立即进行类型断言,以确保其参数是特定结构体类型,对应于常见的操作,例如将一个 image.RGBA
图像绘制到另一个图像上,或者将一个 image.Alpha
蒙版(如字体字形)绘制到 image.RGBA
图像上。如果类型断言成功,则会利用该类型信息运行通用算法的专用实现。如果断言失败,则回退代码路径会使用通用的 At
和 Set
方法。这些快速路径纯粹是为了性能优化;无论哪种方式,最终的目标图像都是相同的。实际上,只需要支持少量特殊情况即可满足典型应用程序的需求。
下一篇文章:从浏览器学习 Go
上一篇文章:Go 的 image 包
博客索引