這篇展現 Go 標準庫的圖像包的使用。建立一系列的位圖圖像,而後將位圖序列編碼爲 GIF 動畫。示例要建立的圖像叫作利薩如圖形(Lissajous-Figure),是20世紀60年代科幻片中的纖維狀視覺效果。利薩如圖形是參數化的二維諧振曲線,如示波器x軸和y軸饋電輸入的兩個正弦波。 web
先放上完整的示例:canvas
package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "os" "time" ) var palette = []color.Color{color.White, color.Black} const ( whiteIndex = 0 // 畫板中的第一種顏色 blackIndex = 1 // 畫板中的下一種顏色 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) if len(os.Args) > 1 && os.Args[1] == "web" { handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w) } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil)) return } lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 // 完整的x振盪器變化的個數 res = 0.001 // 角度分辨率 size = 100 // 圖像畫布包含[-size, size] nframes = 64 // 動畫中的幀數 delay = 8 // 以10ms爲單位的幀間延遲 ) freq := rand.Float64() * 3.0 // y振盪器的相對頻率 anim := gif.GIF{LoopCount: nframes} phase := 0.0 // phase differencs for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex) } phase += 0.1 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // 注意:忽略編碼錯誤 }
函數有兩個嵌套的循環。外層有64個迭代,每一個迭代產生一幀。建立一個201×201大小的畫板,使用黑白兩種顏色。全部的像素值默認設置爲0,就是默認的顏色,這裏就是白色。每個內存循環經過設置一些像素爲黑色產生一個新的圖像。結果用append追加到anim的幀列表中,而且指定80ms的延遲。最後,幀和延遲的序列被編碼成GIF格式,而後寫入輸出流out。
內層循環運行兩個振盪器。x方向的振盪器是正弦函數,y方法也是正弦化的。可是它的頻率頻率相對於x的震動週期是0~3之間的一個隨機數。它的相位相對於x的初始值爲0,而後隨着每一個動畫幀增長。該循環在x振盪器完成5個完整週期後中止。每一步它都調用SetColorIndex將對應畫板上畫的(x,y)位置設置爲黑色,即值爲1。 windows
main函數調用 lissajous 函數,直接寫到標準輸出,而後用輸出重定向指向一個文件名,就生成gif文件了:瀏覽器
$ go build gopl/ch1/liaasjous $ ./lissajous >out.gif
不過windows貌似不支持gif了。加上web參數調用程序,直接打開瀏覽器就能查看,每次刷新都是一張新的圖形。網絡
該篇舉了一個浮點繪圖運算的例子。根據傳入兩個參數的函數 z=f(x,y),繪出三維的網線狀曲面,繪製過程當中運用了可縮放矢量圖形(Scalable Vector Graphics, SVG),繪製線條的一種標準XML格式。app
先放上完整的示例:ide
// 根據一個三維曲面函數計算並生成SVG,並輸出到Web頁面 package main import ( "fmt" "io" "log" "math" "net/http" ) const ( width, height = 600, 320 // 以像素表示的畫布大小 cells = 100 // 網格單元的個數 xyrange = 30.0 // 座標軸的範圍,-xyrange ~ xyrange xyscale = width / 2 / xyrange // x 或 y 軸上每一個單位長度的像素 zscale = height * 0.4 // z軸上每一個單位長度的像素 angle = math.Pi / 6 // x、y軸的角度,30度 color = "grey" // 線條的顏色 ) var sin30, cos30 = math.Sin(angle), math.Cos(angle) func svg(w io.Writer) { fmt.Fprintf(w, "<svg xmlns='http://www.w3.org/2000/svg' "+ "style='stroke: %s; fill: white; stroke-width: 0.7' "+ "width='%d' height='%d'>", color, width, height) for i := 0; i < cells; i++ { for j := 0; j < cells; j++ { ax, ay := corner(i+1, j) bx, by := corner(i, j) cx, cy := corner(i, j+1) dx, dy := corner(i+1, j+1) fmt.Fprintf(w, "<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n", ax, ay, bx, by, cx, cy, dx, dy) } } fmt.Fprintln(w, "</svg>") } func corner(i, j int) (float64, float64) { // 求出網格單元(i,j)的頂點座標(x,y) x := xyrange * (float64(i)/cells - 0.5) y := xyrange * (float64(j)/cells - 0.5) // 計算曲面高度z z := f(x, y) // 將(x,y,z)等角投射到二維SVG繪圖平面上,座標是(sx,sy) sx := width/2 + (x-y)*cos30*xyscale sy := height/2 + (x+y)*sin30*xyscale - z*zscale return sx, sy } func f(x, y float64) float64 { r := math.Hypot(x, y) // 到(0,0)的距離 return math.Sin(r) / r } func main() { handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") svg(w) } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil)) return }
corner函數返回兩個值,構成網格單元其中一個格子的座標。
理解這段程序須要一些幾何知識。這段程序本質上是三套不一樣座標系的相互映射,見下圖。首先是一個包含 100×100 個單元的二維網絡,每一個網絡單元用整數座標 (i, j) 標記,從最遠處靠後的角落 (0, 0) 開始。從後向前繪製,就如左側的圖,於是後方的多邊形可能被前方的遮住。 svg
再看中間的圖,在這個座標系內,網絡由三維浮點數 (x, y, z) 決定,其中x和y由i和j的線性函數決定,通過座標轉換,原點處於中央,而且座標系按照xyrange進行縮放。高度值z由曲面函數 f(x,y) 決定。
最右邊的圖,這個座標系是二維成像繪圖平面(image canvas),原點在左上角。這個平面中點的座標記做 (sx, sy)。這裏用等角投影(isometric projection)將三維座標點 (x, y, z) 映射到二維繪圖平面上。若一個點的x值越大,y值越小,則其在繪圖平面上看起來就越接近右方。而若一個點的x值或y值越大,且z值越小,則其在繪圖平面上看起來就越接近下方。縱向 (x) 與橫向 (y) 的縮放係數是由30度角的正弦值和餘弦值推導而得。z方向的縮放係數爲0.4,是個隨意決定的參數值。
回到左邊那張圖的小圖,二維網絡中的單元由main函數處理,它算出多邊形ABCD在繪圖平面上四個頂點的座標,其中B對應 (i, j) ,A、C、D則爲其它三個頂點,而後再輸出一條SVG指令將其繪出。 函數
該篇經過複數的計算,生成 PNG 格式的分形圖。oop
Go具有了兩種大小的複數 complex64 和 complex128,兩者分別由 float32 和 float64 構成。內置的 complex 函數根據給定的實部和虛部建立複數,而內置的 real 函數和 imag 函數則分別提取複數的實部和虛部:
var x complex128 = complex(1, 2) // 1+2i // x := 1 + 2i var y complex128 = complex(3, 4) // 3+4i // y := 3 + 4i fmt.Println(x*y) // -5+10i fmt.Println(real(x*y)) // -5 fmt.Println(imag(x*y)) // 10 fmt.Println(1i * 1i) // -1
先放上完整的示例:
// 生成一個PNG格式的Mandelbrot分形圖 package main import ( "fmt" "image" "image/color" "image/png" "math/cmplx" "os" ) func main() { const ( xmin, ymin, xmax, ymax = -2, -2, +2, +2 width, height = 1024, 1024 ) img := image.NewRGBA(image.Rect(0, 0, width, height)) for py := 0; py < height; py++ { y := float64(py)/height*(ymax-ymin) + ymin for px := 0; px < width; px++ { x := float64(px)/height*(xmax-xmin) + xmin z := complex(x, y) // 點(px, py)表示複數值z img.Set(px, py, mandelbrot(z)) } } f, err := os.OpenFile("p1.png", os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Println("ERROR", err) return } defer f.Close() png.Encode(f, img) // 注意:忽略錯誤 } func mandelbrot(z complex128) color.Color { const iterations = 200 const contrast = 15 var v complex128 for n := uint8(0); n < iterations; n++ { v = v*v + z if cmplx.Abs(v) > 2 { return color.Gray{255 - contrast*n} } } return color.Black }
這個程序用 complex128 運算生成一個 Mandelbrot 集。
兩個嵌套循環在 1024×1024 的灰度圖上逐行掃描每一個點,這個圖表示覆平面上-2~+2的區域,每一個點都對應一個複數,該程序針對各個點反覆迭代計算其平方與自身的和,判斷其最終可否超出半徑爲2的圓(取模)。而後根據超出邊界所需的迭代次數設定點的灰度。在設定的迭代次數內沒有超出的那部分點,這些點屬於 Mandelbrot 集,就是黑色的內些部分。最後輸出PNG圖片。
此次將PNG寫到img標籤裏,而且不生成圖片文件,而是用base64對圖片進行編碼:
package main import ( "encoding/base64" "fmt" "image" "image/color" "image/png" "log" "math/cmplx" "net/http" ) var f func(z complex128) color.Color func main() { fmt.Println("http://localhost:8000/?f=newton") handler := func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { log.Print(err) } if v, ok := r.Form["f"]; ok { switch v[0] { case "newton", "2": f = newton default: f = mandelbrot } } fmt.Fprint(w, `<body>`) fmt.Fprint(w, `<img src="data:image/png;base64,`) createPng(w) fmt.Fprint(w, `" />`) fmt.Fprint(w, `</body>`) } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil)) return } func createPng(w http.ResponseWriter) { const ( xmin, ymin, xmax, ymax = -2, -2, +2, +2 width, height = 1024, 1024 ) img := image.NewRGBA(image.Rect(0, 0, width, height)) for py := 0; py < height; py++ { y := float64(py)/height*(ymax-ymin) + ymin for px := 0; px < width; px++ { x := float64(px)/height*(xmax-xmin) + xmin z := complex(x, y) // 點(px, py)表示複數值z img.Set(px, py, f(z)) } } b64w := base64.NewEncoder(base64.StdEncoding, w) // 往b64w裏寫,就是編碼後寫入到w defer b64w.Close() png.Encode(b64w, img) // 注意:忽略錯誤 } func mandelbrot(z complex128) color.Color { const iterations = 200 const contrast = 15 var v complex128 for n := uint8(0); n < iterations; n++ { v = v*v + z if cmplx.Abs(v) > 2 { x := 255 - contrast*n switch n % 3 { case 0: return color.RGBA{x, 0, 0, x} case 1: return color.RGBA{0, x, 0, x} case 2: return color.RGBA{0, 0, x, x} } } } return color.Black } // f(x) = x^4 - 1 // // z' = z - f(z)/f'(z) // = z - (z^4 - 1) / (4 * z^3) // = z - (z - 1/z^3) / 4 func newton(z complex128) color.Color { const iterations = 37 const contrast = 7 for i := uint8(0); i < iterations; i++ { z -= (z - 1/(z*z*z)) / 4 if cmplx.Abs(z*z*z*z-1) < 1e-6 { // return color.Gray{255 - contrast*i} x := contrast*i switch i % 3 { case 0: return color.RGBA{x, 0, 0, x} case 1: return color.RGBA{0, x, 0, x} case 2: return color.RGBA{0, 0, x, x} } } } return color.Black }
這裏還增長一個的圖形,運用牛頓法求某個函數的複數解(z^4-1=0)。原來的圖形此次作成了彩圖。
image 包下有3個子包:
因此,這3種圖片格式是標準庫原生支持的。
標準庫的 image 包導出了 Decode 函數,它從 io.Reader 讀取數據,而且識別使用哪種圖像格式來編碼數據,調用適當的解碼器,返回 image.Image 對象做爲結果。使用 image.Decode 能夠構建一個簡單的圖像轉換器,讀取某一種格式的圖像,而後輸出爲另一個格式:
// 讀取 PNG 圖像,並把它做爲 JPEG 圖像保存 package main import ( "fmt" "image" "image/jpeg" _ "image/png" // 註冊 PNG ××× "io" "os" "path/filepath" ) func main() { fileName := "test" // 不要擴展名 dir, _ := os.Getwd() // 返回當前文件路徑的字符串和一個err信息,忽略err pngPath := filepath.Join(dir, fileName+".png") jpgPath := filepath.Join(dir, fileName+".jpg") // 打開 png 文件 pngFile, err := os.Open(pngPath) if err != nil { // 文件可能不存在 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } defer pngFile.Close() // 建立 jpg 文件 jpgFile, err := os.Create(jpgPath) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } defer jpgFile.Close() // 調用文件轉換 if err := toJPEG(pngFile, jpgFile); err != nil { fmt.Fprintf(os.Stderr, "jpeg: %v\n", err) os.Exit(1) } } func toJPEG(in io.Reader, out io.Writer) error { img, kind, err := image.Decode(in) if err != nil { return err } fmt.Fprintln(os.Stderr, "Input format =", kind) return jpeg.Encode(out, img, &jpeg.Options{Quality: 95}) }
該程序打開一個png文件,再建立一個新的jpg文件,而後進行圖像格式的轉換。
注意空白導入"image/png"。若是沒有這一行,程序能夠正常編譯和連接,可是不能識別和解碼 PNG 格式的輸入:
PS H:\Go\src\gopl\ch10\jpeg> go run main.go jpeg: image: unknown format exit status 1 PS H:\Go\src\gopl\ch10\jpeg>
這個例子裏是解碼png格式的圖片,程序能識別png格式是由於上面的一行空導入。也能夠支持其餘格式,而且是同時支持的,只要多導入幾個包。具體看下面的展開。
接下來解釋它是如何工做的。標準庫提供 GIF、PNG、JPEG 等格式的解碼庫,用戶本身能夠提供其餘格式的,可是爲了使可執行程序簡短,除非明確須要,不然解碼器不會被包含進應用程序。image.Decode 函數查閱一個關於支持格式的表格。每個表項由4個部分組成:
對於每一種格式,一般經過在其支持的包的初始化函數中來調用 image.RegisterFormat 來向表格添加項。例如 image.png 中的實現以下:
package png // image/png func Decode(r io.Reader) (image.Image, error) func DecodeConfig(r io.Reader) (image.Config, error) const pngHeader = "\x89PNG\r\n\x1a\n" func init() { image.RegisterFormat("png", pngHeader, Decode, DecodeConfig) }
這個效果就是,一個應用只須要空白導入格式化所需的包,就可讓 image.Decode 函數具有應對格式的解碼能力。 因此,能夠多導入幾個空包,這樣程序就能夠支持更多格式的解碼了。