Go 部落格

Go image/draw 套件

Nigel Tao
2011 年 9 月 29 日

簡介

套件 image/draw 僅定義一個操作:透過一個選擇性的遮罩影像,將一個來源影像繪製到一個目標影像上。這個操作 überraschend地具有多功能性,並且能夠優雅且有效率地執行許多常見的影像處理作業。

合成是以類似於 Plan 9 函式庫和 X Render 延伸模組的樣式,一個像素接著一個像素執行的。此模型是基於 Porter 和 Duff 發表的經典論文「數位影像合成」,其中增加了遮罩參數:dst = (src IN mask) OP dst。對於完全不透明的遮罩,這會簡化為原本的 Porter-Duff 公式:dst = src OP dst。在 Go 中,一個 nil 遮罩影像等同於一個無窮大、完全不透明的遮罩影像。

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.Uniform 的 image.Transparent

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 內建的複製函式可以處理重疊的目標和來源切片。將影像 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 套件
部落格索引