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 中,空遮罩图像等效于大小无限的、完全不透明的遮罩图像。
Porter-Duff 论文提出了 12 种不同的合成运算符,但通过显式遮罩,实际上只需要其中的 2 种:源上目标和源。在 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 函数可以处理重叠的目标切片和源切片一样。要将图像向左滚动 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 包
博客索引