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 中,nil 遮罩图像等同于无限大且完全不透明的遮罩图像。

Porter-Duff 的论文提出了 12 种不同的合成运算符,但在有显式遮罩的情况下,实际只需使用其中 2 种:源叠置目标(source-over-destination)和源(source)。在 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 包定义了一个具有 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 图像上等常见操作。如果类型断言成功,则使用该类型信息运行通用算法的专门实现。如果断言失败,则备用代码路径使用通用的 AtSet 方法。快速路径纯粹是为了性能优化;最终的目标图像结果是相同的。实际上,只需要少量特殊情况即可支持典型应用程序。

下一篇文章:在浏览器中学习 Go 语言
上一篇文章:Go image 包
博客索引