Go言語で画像(png, jpg, gif)処理 [ドット絵、リサイズ]

# 初めに
「Go言語で画像処理を書いたことないなー」と思い、ドット絵を作成する [gpixart](https://github.com/hmarf/gpixart) と画像をリサイズする [grimg](https://github.com/hmarf/grimg) を作成したときに苦労したことを書きます。
gpixartは内応が薄いので時間がない方は読み飛ばしてください。

# [gpixart](https://github.com/hmarf/gpixart)
``` github.com/nfnt/resize ``` を使ってリサイズするだけでドット絵になります。
例えば、180x200の画像を18x20の画像にリサイズするとドット絵になります。（ドット絵というより画質の悪い画像ですけどね）
正直これで終わってしまうんです。なので、[ドット絵ナニカ](http://dot-e-nanika.com/) のように減色処理を加えました。手法としては、k-meansを使って減色します。

``` pixart.go
func kmeans(img *image.RGBA, cluster int, size int) *image.RGBA {

	vcolor := rgbaToArray(img)
	npixels := len(vcolor)
	vcluster := make([]color.RGBA, cluster)
	residual := float32(npixels)

	rand.Seed(time.Now().UnixNano())
	vtype := make([]int, npixels)
	for i := 0; i < len(vtype); i++ {
		vtype[i] = rand.Intn(cluster)
	}

	niter := 0
	for residual > 0 && niter < 30 {
		residual = 0
		for i := 0; i < cluster; i++ {
			clusterInt := make([]int, 0)
			for index, typeCluster := range vtype {
				if typeCluster == i {
					clusterInt = append(clusterInt, index)
				}
			}
			if len(clusterInt) == 0 {
				continue
			}
			nclusterInt := float64(len(clusterInt))
			rS, gS, bS, aS := 0.0, 0.0, 0.0, 0.0
			for _, typeCluster := range clusterInt {
				color_ := vcolor[typeCluster]
				rS += float64(color_.R) / nclusterInt
				gS += float64(color_.G) / nclusterInt
				bS += float64(color_.B) / nclusterInt
				aS += float64(color_.A) / nclusterInt
			}
			vcluster[i] = color.RGBA{uint8(rS), uint8(gS), uint8(bS), uint8(aS)}
		}

		for vTypeIndex, color_ := range vcolor {
			clusterIndexMin := vtype[vTypeIndex]
			distanceMin := 1000.0
			for clusterIndex, cluster := range vcluster {
				distance := distance(color_, cluster)
				if distance < distanceMin {
					distanceMin = distance
					clusterIndexMin = clusterIndex
				}
			}
			if clusterIndexMin != vtype[vTypeIndex] {
				residual++
			}
			vtype[vTypeIndex] = clusterIndexMin
		}

		niter++
	}

	for index := 0; index < npixels; index++ {
		vcolor[index] = vcluster[vtype[index]]
	}
	return upQualityImage(img, vcolor, size)
}
```

# [grimg](https://github.com/hmarf/grimg)
```github.com/nfnt/resize``` を使ってリサイズするだけです。
ただ辛かったのが、Gif画像のリサイズです。以下は全てgifのリサイズでのお話です。

## unknown format

``` .go
// ファイルを開く
file, _ := os.Open(o.InputFile)

// formatがpng, jpeg, gifの判定のために Decode()
_, format, _ := image.Decode(file)

// gifのとき DecodeAll()
if format == "gif"{
    gifimg, err := gif.DecodeAll(file)
}
```

上記のコードだと ```unknown format``` が出現します。一度Decodeしたデータを再度Decodeしようとしたからみたいです。
解決策として、
1. ファイルをもう一度Openする
2. bytes.BufferにCopyしておく
3. decodeした後に、encodeし再びdecode
4. file.Seek(0, io.SeekStart)
が思いつきました。今回は４個目のseekメソッドを採用しました。

``` .go
// ファイルを開く
file, _ := os.Open(o.InputFile)

// formatがpng, jpeg, gifの判定のために Decode()
_, format, _ := image.Decode(file)

// gifのとき DecodeAll()
if format == "gif"{
    file.Seek(0, io.SeekStart) // add
    gifimg, err := gif.DecodeAll(file)
}
```

## gifの扱える色
gifは256色(8ビット)までの色を扱うことのできる画像形式です。jpegは1670万色(24ビット)まで扱うことができる画像形式です。gifで使えるカラーがいかに少ないか分かりますね。
gif画像であっても ``` "github.com/nfnt/resize" ``` でリサイズすると256色を超えます。なのでgif画像のカラーを保存しておいて、drawするときにリサイズ済み画像の色を置き換えました。

``` .go

func keys(m map[color.Color]bool) color.Palette {
	var p color.Palette
	for k := range m {
		p = append(p, k)
	}
	return p
}

func getGifColor(rec image.Rectangle, img image.Image, w int, h int) color.Palette {
	cUsedM := make(map[color.Color]bool)
	// The color used in the original image
	for x := 1; x <= w; x++ {
		for y := 1; y <= h; y++ {
			if _, ok := cUsedM[img.At(x, y)]; !ok {
				cUsedM[img.At(x, y)] = true
			}
		}
	}
	return keys(cUsedM)
}
```

## gif の disposal method
一番苦労しました。
最初にgif画像の１フレームずつリサイズして保存すれば、gif画像のリサイズ完成！と思い、実装しました。
結果、用意した10個のGifのうち4つが綺麗にリサイズできたのですが、6個はノイズが発生しました。

リサイズに失敗した(ノイズあり)Gif　*↓*
![output_d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381963/1ed909e3-d299-6f38-43ea-548807afd749.gif)

最初訳がわからなかったのですが、[CGファイル概説 第4章 第2節 その1 - 我楽多頓陳館](http://www.snap-tck.com/room03/c02/cg/cg04_02.html) を読んだら、disposal methodによって処理を変えなきゃいけないっぽいことに気付きました。
[Animated GIFs](https://cs.nyu.edu/courses/fall10/V22.0004-002/animatedGifs.pdf) がdisposal methodを理解する上で参考になったので記載しておきます。
[gif - The Go Programming Language](https://golang.org/pkg/image/gif/) によると

``` .go
const (
    DisposalNone       = 0x01
    DisposalBackground = 0x02
    DisposalPrevious   = 0x03
)
```

その時の私のコードは ```DisposalPrevious``` だけに対応し、他のdisposal methodに対応していませんでした。これが原因でうまくリサイズできていませんでした。
ちなみに、grimgは```DisposalBackground```に対応していません。理由としては、```DisposalBackground```のGifが見つからなかったからです。。。
[補足]　```DisposalPrevious```のときは差分画像しか保存されていないので、前フレームに上書きする形で画像を作成し、リサイズしています。リサイズ後に合成するとノイズが発生しました。

# まとめ　＆　お願い
画像のリサイズだけで時間がかかってしまいました。
gifは ```Disposal Method```, ```Delay Time``` に気をつけて扱いましょう。
disposal method ```DisposalBackground``` のGifが見つかれば僕に送ってほしいです！