GIF、SVG、PNG、圖片格式轉換

GIF 動畫

這篇展現 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)  // 注意:忽略編碼錯誤
}

lissajous函數

函數有兩個嵌套的循環。外層有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參數調用程序,直接打開瀏覽器就能查看,每次刷新都是一張新的圖形。網絡

浮點數生成 SVG

該篇舉了一個浮點繪圖運算的例子。根據傳入兩個參數的函數 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

GIF、SVG、PNG、圖片格式轉換

再看中間的圖,在這個座標系內,網絡由三維浮點數 (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圖片。

輸出到Web頁面

此次將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個子包:

  • image.gif
  • image.jpeg
  • image.png

因此,這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個部分組成:

  • 格式的名字
  • 某種格式中所使用的相同的前綴字符串,用來識別編碼格式
  • 一個用來解碼被編碼圖像的函數 Decode
  • 另外一個函數 DecodeConfig,它僅僅解碼圖像的元數據,好比尺寸和色域

對於每一種格式,一般經過在其支持的包的初始化函數中來調用 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 函數具有應對格式的解碼能力。 因此,能夠多導入幾個空包,這樣程序就能夠支持更多格式的解碼了。

相關文章
相關標籤/搜索