Adovent Calendar 1日目の記事です。
Go言語で行う画像処理の話をしたいと思います。
- ImageMagickを使う
- 様々な言語でC言語のwrapperライブラリが有り幅広く使われている
- 昔からあるので使用実績が広い
- 半年ほど前任意のコマンドを実行できるようにする脆弱性が見つかり多くのエンジニアを悩ませた
- phthon, ruby, perl, PHPなど
- 裏側はC言語などで作られたライブラリを使用する
- mac -> linuxなどの開発が少し億劫になる
- Javaで頑張る
- OpenCVを使う
- C/C++, Java, Pythonに対応したライブラリである
- ImageMagick同様様々な言語で使えるようになっている
- 環境構築が大掛かりになる
- Go言語で書く
- 公式のパッケージで
image.Image
インターフェイスが用意されている - クロスコンパイルができるのでpure Goで作れば他の環境に置き換えるのが比較的楽
- 並列処理が簡単にかけるのでIO待ちとかを潰しやすい
- 公式のパッケージで
- 優秀なGC
- 優秀な並列処理機能
- 優秀な画像処理パッケージ群
Go言語のGCは優秀でほとんどプログラムを止めること無くメモリを管理してくれる
ゴルーチンという機能を備えており非同期的な処理が強い 例えばワーカーを作って順次何かの画像からPNGにして保存する関数だとこう
func convertPng(wg *sync.WaitGroup, queue chan image.Image){
defer wg.Done()
for {
img, ok := <- queue// queueがcloseされるとokがfalseになる
if !ok {
return
}
file, err := os.Create(fmt.Sprintf("%v.png", time.Now().UnixNano()))
if err != nil {
panic(err)
}
err = png.Encode(file, img)
if err != nil {
panic(err)
}
}
}
func main(){
var wg sync.WaitGroup
queue := make(chan image.Image, 16)
// cpuの数ぶんワーカーを生成する
for i := 0; i < runtime.NumCPU(); i ++ {
wg.Add(1)
go convertPng(&wg, queue)
}
/*
* 適当にワーカーに画像を投げる処理を書く
*/
close(queue)
wg.Wait()
}
ここでは単純にPNGで保存しているがリサイズ処理とかにしても良い
pure Go, C言語ベースのパッケージ問わず使えるものが結構多い
pure Goなパッケージだけでも意外と困らない事が多い
- Go言語として使える画像フォーマット
- jpeg
- png
- gif
- webp
- bmp
- 便利な画像処理パッケージ
- github.com/nfnt/resize
- pure Goである
- 画像のリサイズができる
- リサイズアルゴリズムの対応が多い
- Nearest-Neighbor
- Bilinear
- Bicubic
- Mitchell-Netravali
- Lanczos2
- Lanczos3
- github.com/DAddYE/vips
- libvips wrapperである
- 速い
- https://github.com/fawick/speedtest-resize
- github.com/pixiv/go-libjpeg
- libjpeg wrapperである
- buildinのライブラリより速い
- github.com/lucasb-eyer/go-colorful
- golangでhsvとかを扱うのに使う
- 画像認識とかでいらないデータを削ったりするのに便利
- github.com/nfnt/resize
最後にローカルにある画像をリサイズしてbrowserに表示する画像サーバーの実装を載せておく
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"image"
"image/jpeg"
"image/png"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"github.com/nfnt/resize"
)
var (
port = flag.String("p", "8081", "port")
imgDir = flag.String("i", "./img", "img dir")
)
func getImage(path string) (image.Image, string, error) {
path, err := filepath.Abs(path)
if err != nil {
return nil, "", err
}
file, err := os.Open(path)
if err != nil {
return nil, "", err
}
return image.Decode(file)
}
func getSize(req *http.Request) (uint, uint, error) {
query := req.URL.Query()
w := query.Get("w")
if w == "" {
w = "0"
}
wid, err := strconv.Atoi(w)
if err != nil {
return 0, 0, err
}
h := query.Get("h")
if h == "" {
h = "0"
}
hig, err := strconv.Atoi(h)
if err != nil {
return 0, 0, err
}
return uint(wid), uint(hig), nil
}
func errorHandle(wr http.ResponseWriter, err error) {
wr.Header().Set("Content-Type", "application/json")
wr.WriteHeader(404)
errJSON, _ := json.Marshal(map[string]string{
"error": err.Error(),
})
wr.Write(errJSON)
}
func resizeImage(wr http.ResponseWriter, req *http.Request) {
//query := req.URL.Query()
path := *imgDir + req.URL.Path
img, t, err := getImage(path)
if err != nil {
errorHandle(wr, err)
return
}
w, h, err := getSize(req)
if err != nil {
errorHandle(wr, err)
return
}
resizeImg := resize.Resize(w, h, img, resize.Lanczos3)
out := new(bytes.Buffer)
switch t {
case "png":
err = png.Encode(out, resizeImg)
if err != nil {
errorHandle(wr, err)
return
}
wr.Header().Set("Content-Type", "image/png")
case "jpeg":
err = jpeg.Encode(out, resizeImg, nil)
if err != nil {
errorHandle(wr, err)
return
}
wr.Header().Set("Content-Type", "image/jpeg")
default:
errorHandle(wr, errors.New("Unsupported image format"))
return
}
wr.Write(out.Bytes())
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
http.HandleFunc("/", resizeImage)
http.ListenAndServe(":"+*port, nil)
}