Go
golang

Goでのなんちゃって画像処理

はじめに

こちらは アイスタイル Advent Calendar 2017 13日目の投稿です!
今日はアイスタイル入社から一年とちょっと、Goに触りだしてからは半年とちょっとな私 @grassy が、業務では全く使うことのないGoでのなんちゃって画像処理の話を書き連ねます。

この記事でやったこと

Goの標準パッケージのみを使い、簡単な画像処理を実際にやってみます。

用意するもの

  • Go実行環境
  • 入力用画像(gopherくんでも良いのですが色が少なくてつまらない 分かりづらいため、今回はあらゆる画像処理でおなじみレナ様をお借りします)

lenna.jpg

下準備

  • Goの実行環境を作ってください
  • CUI上でコマンドを叩ける用にしてください(筆者はurfave/cliを使ってます)
  • あなただけの素敵な入力画像を用意してください

なんちゃって画像処理

基本的なファイル操作

画像ファイルの入出力

まずは用意した画像ファイルを読み込んで、別なファイルとして出力してみます。

    file, err := os.Open("source/sample.jpg")
    if err != nil {
        return err
    }
    defer file.Close()

    dstfile, err := os.Create("destination/sample_origin.jpg")
    if err != nil {
        return err
    }
    defer dstfile.Close()

    _, err = io.Copy(dstfile, file)
    if err != nil {
        return err
    }

ちょこっと解説

  • os.Open("source/sample.jpg") で入力画像を開きます
  • os.Create("destination/sample_origin.jpg") で出力先の入れ物を作ります
  • io.Copy(dstfile, file) で入れ物にコピーします

拡張子の変更

次にjpgから他のファイル形式に変更してみます。
ここでの形式build-inに内包されている jpeg(jpg) , gif , png に限ります。他を使いたい場合は パッケージ を追加しましょう。

    extention := "png" // sample extention
    file, err := os.Open("source/sample.jpg")
    if err != nil {
        return err
    }
    defer file.Close()

    dstfile, err := os.Create(fmt.Sprintf("destination/sample_origin.%s", extention))
    if err != nil {
        return err
    }
    defer dstfile.Close()

    switch extention {
    case "jpeg", "jpg":
        err = jpeg.Encode(dstfile, img, nil)
    case "gif":
        err = gif.Encode(dstfile, img, nil)
    case "png":
        err = png.Encode(dstfile, img)
    }
    if err != nil {
        return err
    }

ちょこっと解説

  • hoge.Encode(dstfile, img, options) で変換されます
  • jpeg.Encode()gif.Encode() のときの第三引数は圧縮オプションです。それぞれ画質や色数などが設定できます。

カラー変換

ここから少しだけそれっぽい画像処理の話です。(長くなるので繰り返しのコードは適宜省略します)

グレースケール

まずは基本のキ、グレースケール化から。

    file, err := os.Open("source/sample.jpg")
    if err != nil {
        return err
    }
    defer file.Close()
    img, _, err := image.Decode(file)
    if err != nil {
        return err
    }

    dstfile, err := os.Create("destination/sample_gray.jpg")
    if err != nil {
        return err
    }
    defer dstfile.Close()

    bounds := img.Bounds()
    dest := image.NewGray16(bounds)
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            c := color.Gray16Model.Convert(img.At(x, y))
            gray, _ := c.(color.Gray16)
            dest.Set(x, y, gray)
        }
    }
    return jpeg.Encode(c.DstFile, dest, nil)

ちょこっと解説

  • os.File のままでは何も手が出せないので image.Image で画像ファイルに変換します
  • Image.Bounds() で画像の境界を取得します(bounds.Minからbounds.Maxまでをループさせると全画素走査ができます)
  • 後は全ての画素について color.Gray16Model.Convert(img.At(x, y)) でグレースケールへコンバートした色を dest.Set(x, y, gray) で同じ画素にセット

出力結果

lena_gray.png
この方法でしたら内部で計算してくれたグレー濃度をセットしてるだけなので楽ちんですね。
詳しい計算式は このへん をご覧ください。

セピア変換

グレースケール化で画素走査が出来ました。ということは、各画素のRGB値さえ取れればセピア変換もできますね。
ということでやってみました。

    // (省略)
    bounds := img.Bounds()
    dest := image.NewRGBA(bounds)
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            c := color.RGBAModel.Convert(img.At(x, y))
            col := c.(color.RGBA)
            avg := (float32(col.R) + float32(col.G) + float32(col.B)) / 3
            cg := float32(col.G) * 0.7
            cb := float32(col.B) * 0.4
            dest.Set(x, y, color.RGBA{uint8(avg), uint8(cg), uint8(cb), col.A})
        }
    }
    // (省略)

ちょこっと解説

  • color.RGBAModel.Convert(img.At(x, y)) で今度はRGBAを取得します
  • RとGとBの値から平均値を取得しセピア風に出力するために、それっぽい係数をGとBに乗算します(この平均値をそのままRGB全部にセットするとグレースケールにできます)
    • セピアのRGB値が(107, 74, 43)らしいので( Wikipedia 調べ)、Rを基準にしてGに0.7とBに0.4を掛けてます

出力結果

lena_sepia.png
いい感じにセピア調に見えますね!

ネガポジ反転

各画素のRGB値さえ取れたらなんでも出来ます。
というわけで、画像処理の課題でよく見るネガポジ反転。
(ここまでくるとGoの話というよりただの画像処理の話)

    // (省略)
    bounds := img.Bounds()
    dest := image.NewRGBA(bounds)
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            c := color.RGBAModel.Convert(img.At(x, y))
            col := c.(color.RGBA)
            cr := uint8(255 - int(col.R))
            cg := uint8(255 - int(col.G))
            cb := uint8(255 - int(col.B))
            dest.Set(x, y, color.RGBA{cr, cg, cb, col.A})
        }
    }
    // (省略)

出力結果

lena_negaposi.png
反転なので各色、MAX値との差分を出すだけです。簡単ですね。

モザイク加工

せっかくなのでもう少し加工っぽいこともやってみます。


    // (省略)
    bounds := img.Bounds()
    dest := image.NewRGBA(bounds)
    block := 11 // モザイクの粒度

    for y := bounds.Min.Y + (block-1)/2; y < bounds.Max.Y; y = y + block {
        for x := bounds.Min.X + (block-1)/2; x < bounds.Max.X; x = x + block {
            var cr, cg, cb float32
            var alpha uint8
            for j := y - (block-1)/2; j <= y+(block-1)/2; j++ {
                for i := x - (block-1)/2; i <= x+(block-1)/2; i++ {
                    if i >= 0 && j >= 0 && i < bounds.Max.X && j < bounds.Max.Y {
                        c := color.RGBAModel.Convert(img.At(i, j))
                        col := c.(color.RGBA)
                        cr += float32(col.R)
                        cg += float32(col.G)
                        cb += float32(col.B)
                        alpha = col.A
                    }
                }
            }
            cr = cr / float32(block*block)
            cg = cg / float32(block*block)
            cb = cb / float32(block*block)
            for j := y - (block-1)/2; j <= y+(block-1)/2; j++ {
                for i := x - (block-1)/2; i <= x+(block-1)/2; i++ {
                    if i >= 0 && j >= 0 && i < bounds.Max.X && j < bounds.Max.Y {
                        dest.Set(i, j, color.RGBA{uint8(cr), uint8(cg), uint8(cb), alpha})
                    }
                }
            }
        }
    }
    // (省略)

ちょこっと解説

  • block で指定されたモザイクの粒度ごとに、そのブロック内での各色の平均値を算出してます。
  • 次のループで取得したcolorでそのブロックを塗りつぶしています。
  • 詳しい計算式はこちらのサイトを参考にさせていただきました

出力結果

lena_mosaic_5.png block = 5 出力例

lena_mosaic_11.png block = 11 出力例

モザイク加工なんてそうそう使うことはなさそうですが、モザイク粒度を大きくする=解像度を下げることなので画像のリサイザーとかを作る際には必要な加工です。どこかで使う日が来るかもしれません。

おわりに

上記に挙げた計算手法は なんちゃって です。ですので実用性は保証しません。
筆者がこれをやってみたかった理由は、A Tour of Go で触ったimageパッケージが面白そうだな~と思ったからです。
結果的にGoの話がしたかったのか画像処理の話がしたかったのかわからない記事になってしまいましたが、今回使用したimageパッケージに限らずbuild-inパッケージだけでこんなにいろいろできるのがGoのすごいとこですね。ということで。

明日の記事は @tanit5699 さんの「kaggleで拾ってきたdatasetをpythonで可視化する」です!お楽しみに(^^)