初めに
「Go言語で画像処理を書いたことないなー」と思い、ドット絵を作成する gpixart と画像をリサイズする grimg を作成したときに苦労したことを書きます。
gpixartは内応が薄いので時間がない方は読み飛ばしてください。
gpixart
github.com/nfnt/resize
を使ってリサイズするだけでドット絵になります。
例えば、180x200の画像を18x20の画像にリサイズするとドット絵になります。(ドット絵というより画質の悪い画像ですけどね)
正直これで終わってしまうんです。なので、ドット絵ナニカ のように減色処理を加えました。手法としては、k-meansを使って減色します。
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
github.com/nfnt/resize
を使ってリサイズするだけです。
ただ辛かったのが、Gif画像のリサイズです。以下は全てgifのリサイズでのお話です。
unknown format
// ファイルを開く
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しようとしたからみたいです。
解決策として、
- ファイルをもう一度Openする
- bytes.BufferにCopyしておく
- decodeした後に、encodeし再びdecode
- file.Seek(0, io.SeekStart)
が思いつきました。今回は4個目のseekメソッドを採用しました。
// ファイルを開く
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するときにリサイズ済み画像の色を置き換えました。
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画像の1フレームずつリサイズして保存すれば、gif画像のリサイズ完成!と思い、実装しました。
結果、用意した10個のGifのうち4つが綺麗にリサイズできたのですが、6個はノイズが発生しました。
最初訳がわからなかったのですが、CGファイル概説 第4章 第2節 その1 - 我楽多頓陳館 を読んだら、disposal methodによって処理を変えなきゃいけないっぽいことに気付きました。
Animated GIFs がdisposal methodを理解する上で参考になったので記載しておきます。
gif - The Go Programming Language によると
const (
DisposalNone = 0x01
DisposalBackground = 0x02
DisposalPrevious = 0x03
)
その時の私のコードは DisposalPrevious
だけに対応し、他のdisposal methodに対応していませんでした。これが原因でうまくリサイズできていませんでした。
ちなみに、grimgはDisposalBackground
に対応していません。理由としては、DisposalBackground
のGifが見つからなかったからです。。。
[補足] DisposalPrevious
のときは差分画像しか保存されていないので、前フレームに上書きする形で画像を作成し、リサイズしています。リサイズ後に合成するとノイズが発生しました。
まとめ & お願い
画像のリサイズだけで時間がかかってしまいました。
gifは Disposal Method
, Delay Time
に気をつけて扱いましょう。
disposal method DisposalBackground
のGifが見つかれば僕に送ってほしいです!