這篇展示 Go 標(biāo)準庫的圖像包的使用。創(chuàng)建一系列的位圖圖像,然后將位圖序列編碼為 GIF 動畫。示例要創(chuàng)建的圖像叫做利薩如圖形(Lissajous-Figure),是20世紀60年代科幻片中的纖維狀視覺效果。利薩如圖形是參數(shù)化的二維諧振曲線,如示波器x軸和y軸饋電輸入的兩個正弦波。
創(chuàng)新互聯(lián)公司為企業(yè)級客戶提高一站式互聯(lián)網(wǎng)+設(shè)計服務(wù),主要包括網(wǎng)站制作、網(wǎng)站建設(shè)、重慶APP開發(fā)公司、小程序開發(fā)、宣傳片制作、LOGO設(shè)計等,幫助客戶快速提升營銷能力和企業(yè)形象,創(chuàng)新互聯(lián)各部門都有經(jīng)驗豐富的經(jīng)驗,可以確保每一個作品的質(zhì)量和創(chuàng)作周期,同時每年都有很多新員工加入,為我們帶來大量新的創(chuàng)意。先放上完整的示例:
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振蕩器變化的個數(shù)
res = 0.001 // 角度分辨率
size = 100 // 圖像畫布包含[-size, size]
nframes = 64 // 動畫中的幀數(shù)
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) // 注意:忽略編碼錯誤
}
函數(shù)有兩個嵌套的循環(huán)。外層有64個迭代,每個迭代產(chǎn)生一幀。創(chuàng)建一個201×201大小的畫板,使用黑白兩種顏色。所有的像素值默認設(shè)置為0,就是默認的顏色,這里就是白色。每一個內(nèi)存循環(huán)通過設(shè)置一些像素為黑色產(chǎn)生一個新的圖像。結(jié)果用append追加到anim的幀列表中,并且指定80ms的延遲。最后,幀和延遲的序列被編碼成GIF格式,然后寫入輸出流out。
內(nèi)層循環(huán)運行兩個振蕩器。x方向的振蕩器是正弦函數(shù),y方法也是正弦化的。但是它的頻率頻率相對于x的震動周期是0~3之間的一個隨機數(shù)。它的相位相對于x的初始值為0,然后隨著每個動畫幀增加。該循環(huán)在x振蕩器完成5個完整周期后停止。每一步它都調(diào)用SetColorIndex將對應(yīng)畫板上畫的(x,y)位置設(shè)置為黑色,即值為1。
main函數(shù)調(diào)用 lissajous 函數(shù),直接寫到標(biāo)準輸出,然后用輸出重定向指向一個文件名,就生成gif文件了:
$ go build gopl/ch2/liaasjous
$ ./lissajous >out.gif
不過windows貌似不支持gif了。加上web參數(shù)調(diào)用程序,直接打開瀏覽器就能查看,每次刷新都是一張新的圖形。
該篇舉了一個浮點繪圖運算的例子。根據(jù)傳入兩個參數(shù)的函數(shù) z=f(x,y),繪出三維的網(wǎng)線狀曲面,繪制過程中運用了可縮放矢量圖形(Scalable Vector Graphics, SVG),繪制線條的一種標(biāo)準XML格式。
先放上完整的示例:
// 根據(jù)一個三維曲面函數(shù)計算并生成SVG,并輸出到Web頁面
package main
import (
"fmt"
"io"
"log"
"math"
"net/http"
)
const (
width, height = 600, 320 // 以像素表示的畫布大小
cells = 100 // 網(wǎng)格單元的個數(shù)
xyrange = 30.0 // 坐標(biāo)軸的范圍,-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, "")
}
func corner(i, j int) (float64, float64) {
// 求出網(wǎng)格單元(i,j)的頂點坐標(biāo)(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繪圖平面上,坐標(biāo)是(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函數(shù)返回兩個值,構(gòu)成網(wǎng)格單元其中一個格子的坐標(biāo)。
理解這段程序需要一些幾何知識。這段程序本質(zhì)上是三套不同坐標(biāo)系的相互映射,見下圖。首先是一個包含 100×100 個單元的二維網(wǎng)絡(luò),每個網(wǎng)絡(luò)單元用整數(shù)坐標(biāo) (i, j) 標(biāo)記,從最遠處靠后的角落 (0, 0) 開始。從后向前繪制,就如左側(cè)的圖,因而后方的多邊形可能被前方的遮住。
再看中間的圖,在這個坐標(biāo)系內(nèi),網(wǎng)絡(luò)由三維浮點數(shù) (x, y, z) 決定,其中x和y由i和j的線性函數(shù)決定,經(jīng)過坐標(biāo)轉(zhuǎn)換,原點處于中央,并且坐標(biāo)系按照xyrange進行縮放。高度值z由曲面函數(shù) f(x,y) 決定。
最右邊的圖,這個坐標(biāo)系是二維成像繪圖平面(image canvas),原點在左上角。這個平面中點的坐標(biāo)記作 (sx, sy)。這里用等角投影(isometric projection)將三維坐標(biāo)點 (x, y, z) 映射到二維繪圖平面上。若一個點的x值越大,y值越小,則其在繪圖平面上看起來就越接近右方。而若一個點的x值或y值越大,且z值越小,則其在繪圖平面上看起來就越接近下方??v向 (x) 與橫向 (y) 的縮放系數(shù)是由30度角的正弦值和余弦值推導(dǎo)而得。z方向的縮放系數(shù)為0.4,是個隨意決定的參數(shù)值。
回到左邊那張圖的小圖,二維網(wǎng)絡(luò)中的單元由main函數(shù)處理,它算出多邊形ABCD在繪圖平面上四個頂點的坐標(biāo),其中B對應(yīng) (i, j) ,A、C、D則為其它三個頂點,然后再輸出一條SVG指令將其繪出。
該篇通過復(fù)數(shù)的計算,生成 PNG 格式的分形圖。
Go具備了兩種大小的復(fù)數(shù) complex64 和 complex128,二者分別由 float32 和 float64 構(gòu)成。內(nèi)置的 complex 函數(shù)根據(jù)給定的實部和虛部創(chuàng)建復(fù)數(shù),而內(nèi)置的 real 函數(shù)和 imag 函數(shù)則分別提取復(fù)數(shù)的實部和虛部:
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)表示復(fù)數(shù)值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 集。
兩個嵌套循環(huán)在 1024×1024 的灰度圖上逐行掃描每個點,這個圖表示復(fù)平面上-2~+2的區(qū)域,每個點都對應(yīng)一個復(fù)數(shù),該程序針對各個點反復(fù)迭代計算其平方與自身的和,判斷其最終能否超出半徑為2的圓(取模)。然后根據(jù)超出邊界所需的迭代次數(shù)設(shè)定點的灰度。在設(shè)定的迭代次數(shù)內(nèi)沒有超出的那部分點,這些點屬于 Mandelbrot 集,就是黑色的內(nèi)些部分。最后輸出PNG圖片。
這次將PNG寫到img標(biāo)簽里,并且不生成圖片文件,而是用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, ``)
fmt.Fprint(w, ``)
fmt.Fprint(w, ``)
}
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)表示復(fù)數(shù)值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
}
這里還增加一個的圖形,運用牛頓法求某個函數(shù)的復(fù)數(shù)解(z^4-1=0)。原來的圖形這次做成了彩圖。
image 包下有3個子包:
所以,這3種圖片格式是標(biāo)準庫原生支持的。
標(biāo)準庫的 image 包導(dǎo)出了 Decode 函數(shù),它從 io.Reader 讀取數(shù)據(jù),并且識別使用哪一種圖像格式來編碼數(shù)據(jù),調(diào)用適當(dāng)?shù)慕獯a器,返回 image.Image 對象作為結(jié)果。使用 image.Decode 可以構(gòu)建一個簡單的圖像轉(zhuǎn)換器,讀取某一種格式的圖像,然后輸出為另外一個格式:
// 讀取 PNG 圖像,并把它作為 JPEG 圖像保存
package main
import (
"fmt"
"image"
"image/jpeg"
_ "image/png" // 注冊 PNG ×××
"io"
"os"
"path/filepath"
)
func main() {
fileName := "test" // 不要擴展名
dir, _ := os.Getwd() // 返回當(dāng)前文件路徑的字符串和一個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()
// 創(chuàng)建 jpg 文件
jpgFile, err := os.Create(jpgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(1)
}
defer jpgFile.Close()
// 調(diào)用文件轉(zhuǎn)換
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文件,再創(chuàng)建一個新的jpg文件,然后進行圖像格式的轉(zhuǎn)換。
注意空白導(dǎo)入"image/png"。如果沒有這一行,程序可以正常編譯和鏈接,但是不能識別和解碼 PNG 格式的輸入:
PS H:\Go\src\gopl\ch20\jpeg> go run main.go
jpeg: image: unknown format
exit status 1
PS H:\Go\src\gopl\ch20\jpeg>
這個例子里是解碼png格式的圖片,程序能識別png格式是因為上面的一行空導(dǎo)入。也可以支持其他格式,并且是同時支持的,只要多導(dǎo)入幾個包。具體看下面的展開。
接下來解釋它是如何工作的。標(biāo)準庫提供 GIF、PNG、JPEG 等格式的解碼庫,用戶自己可以提供其他格式的,但是為了使可執(zhí)行程序簡短,除非明確需要,否則解碼器不會被包含進應(yīng)用程序。image.Decode 函數(shù)查閱一個關(guān)于支持格式的表格。每一個表項由4個部分組成:
對于每一種格式,通常通過在其支持的包的初始化函數(shù)中來調(diào)用 image.RegisterFormat 來向表格添加項。例如 image.png 中的實現(xiàn)如下:
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)
}
這個效果就是,一個應(yīng)用只需要空白導(dǎo)入格式化所需的包,就可以讓 image.Decode 函數(shù)具備應(yīng)對格式的解碼能力。
所以,可以多導(dǎo)入幾個空包,這樣程序就可以支持更多格式的解碼了。
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國云服務(wù)器,動態(tài)BGP最優(yōu)骨干路由自動選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機房獨有T級流量清洗系統(tǒng)配攻擊溯源,準確進行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動現(xiàn)已開啟,新人活動云服務(wù)器買多久送多久。