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 包
部落格索引