Go 博客
Go image/draw 包
引言
image/draw 包 只定义了一个操作:通过可选的遮罩图像将源图像绘制到目标图像上。这个操作出奇地多才多艺,可以优雅高效地执行许多常见的图像处理任务。
合成操作按照 Plan 9 图形库和 X Render 扩展的风格逐像素进行。该模型基于 Porter 和 Duff 经典的“数字图像合成”论文,并增加了一个遮罩参数: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 包定义了一个具有 Set
方法的 draw.Image
接口。
type Image interface {
image.Image
Set(x, y int, c color.Color)
}
填充矩形
要用纯色填充矩形,请使用 image.Uniform
源。ColorImage
类型将 Color
重新解释为该颜色的几乎无限大的 Image
。对于熟悉 Plan 9 绘制库设计的人来说,Go 基于切片的图像类型中不需要显式的“重复位”;这个概念已被 Uniform
subsumed(涵盖/包含)。
// 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
遮罩进行绘制。为简单起见,我们不进行任何子像素定位或渲染,也不校正字体高于基线的高度。
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 包
博客索引