Go 博客

Go image/draw 包

Nigel Tao
2011 年 9 月 29 日

介绍

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 中,这些运算符分别由 OverSrc 常量表示。Over 运算符执行源图像在目标图像上的自然叠加:目标图像的变化在源图像(遮罩后)更透明(即 alpha 值更低)的地方更小。Src 运算符只是简单地复制源图像(遮罩后),而不考虑目标图像的原始内容。对于完全不透明的源图像和遮罩图像,这两个运算符会产生相同的输出,但 Src 运算符通常更快。

几何对齐

合成需要将目标像素与源像素和遮罩像素相关联。显然,这需要目标图像、源图像和遮罩图像以及一个合成运算符,但也需要指定要使用的每个图像的矩形区域。并非所有绘制操作都应该写入整个目标图像:在更新动画图像时,只绘制已更改的图像部分更有效。并非所有绘制操作都应该从整个源图像读取:当使用将许多小图像组合成一个大图像的精灵时,只需要图像的一部分。并非所有绘制操作都应该从整个遮罩图像读取:收集字体字形的遮罩图像类似于精灵。因此,绘制操作还需要知道三个矩形,每个图像一个。由于每个矩形的宽度和高度相同,所以只需要传递一个目标矩形 r 和两个点 spmp:源矩形等于 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 图像上。如果类型断言成功,则使用该类型信息来运行通用算法的专门实现。如果断言失败,则回退代码路径将使用泛型 AtSet 方法。快速路径纯粹是性能优化;无论哪种方式,结果目标图像都是相同的。在实践中,只需要很少的特殊情况即可支持典型的应用程序。

下一篇文章:从浏览器学习 Go
上一篇文章:Go image 包
博客索引