2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-03

初めに

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

gpixart

github.com/nfnt/resize を使ってリサイズするだけでドット絵になります。
例えば、180x200の画像を18x20の画像にリサイズするとドット絵になります。(ドット絵というより画質の悪い画像ですけどね)
正直これで終わってしまうんです。なので、ドット絵ナニカ のように減色処理を加えました。手法としては、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

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しようとしたからみたいです。
解決策として、
1. ファイルをもう一度Openする
2. bytes.BufferにCopyしておく
3. decodeした後に、encodeし再びdecode
4. 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個はノイズが発生しました。

リサイズに失敗した(ノイズあり)Gif 
output_d.gif

最初訳がわからなかったのですが、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が見つかれば僕に送ってほしいです!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1