post Image
間違い探しはGoにおまかせ❤️

はじめに

この記事はCyberAgent 19新卒 エンジニア Advent Calendar 2018の15日目の記事です。
Screen Shot 2018-12-11 at 15.44.38.png

間違い探しはGoにおまかせ❤️

Python+OpenCVでサイゼの間違い探しをしてみた話という記事が、非常に面白かったのと、画像処理を何年もやっておらず久々にやってみたかったので、絶賛勉強中のGoで似たようなことをやってみました。
Goなら標準パッケージだけで大体のことができますしね。
二番煎じですが、やっていきます。

間違い探し用の画像は、サイゼの画像は著作権的にあれかとおもったので、ちびむすドリル:幼児の学習素材館さんからお借りしています。こちらのサイトでは、間違い探しだけでなく赤ちゃん向けから中学生向けまでの様々な教材が提供されています。素敵です。

処理の手順

はじめに考えていたのは、間違い探しの画像をカメラで撮って、入力にしようと思いましたが、そうなると射影変換や特徴点の抽出、マッチングなどの処理が必要になって大変なので、素直に2つの画像を画像ファイルとして入力する形にしました。

処理の手順としては、

  1. 各画像データをグレースケールに変換,グレー化された二つの画像を比較、同一の座標のグレー値が一致していれば黒、一致していなければ白の二値画像を生成
  2. モルフォロジー変換(後述)を行なって、二値画像をならす
  3. 二値画像で、白の多かった部分を赤くするフィルターを作って、元画像に貼り付ける

の手順でいきます。

割とコードが長くなったので、ソースコードはこちらに置いておきます。

実際に使用した画像は、以下の二つです。間違いは5つですが、皆さん、すぐに見つかりましたか?


picture_d.pngpicture_C.png


グレー化とグレー画像の比較

前置きが長くなりましたが、実際の処理に進んでいきましょう。
まずは、二つの画像を読み込んでグレー化します。

grayImage.go
func (g *Gray) Graying(imgObjectA image.Image, imgObjectB image.Image) {

    g.Rec = imgObjectA.Bounds()
    g.ImageA = image.NewGray(g.Rec)
    g.ImageB = image.NewGray(g.Rec)

    // グレー化したものSetして返却
    for v := g.Rec.Min.Y; v < g.Rec.Max.Y; v++ {
        for h := g.Rec.Min.X; h < g.Rec.Max.X; h++ {
            ca := color.GrayModel.Convert(imgObjectA.At(h, v))
            cb := color.GrayModel.Convert(imgObjectB.At(h, v))
            grayA, _ := ca.(color.Gray)
            grayB, _ := cb.(color.Gray)
            g.ImageA.Set(h, v, grayA)
            g.ImageB.Set(h, v, grayB)

        }
    }
}

image.ImageBoundというメソッドを使うことで、ImageRectangleというイメージの大きさに関するデータを取得できます。そのRectangleのデータを元に、二重のforループを使って全てのピクセル?に対してグレー化を行なっています。
実際にグレー画像に変換しているのは、color.GrayModel.Convertの部分ですね。
グレー化したものはそれぞれImageA,ImageBに格納しておきます。

作成したグレー画像は以下のようになっています。
picture_C.pngpicture_C.png

人間からすると逆にわかりにくくなりましたね。

次に、この二つのグレー画像を比較して、二値画像を作成します。

grayImage.go
func (g *Gray) GrayDiff() {
    diffBinary := image.NewGray(g.Rec)
    gray := color.Gray{Y: 255}

    for v := g.Rec.Min.Y; v < g.Rec.Max.Y; v++ {
        for h := g.Rec.Min.X; h < g.Rec.Max.X; h++ {
            if g.ImageA.GrayAt(h, v) != g.ImageB.GrayAt(h, v) {
                diffBinary.SetGray(h, v, gray)
            }
        }
    }
    g.Image = diffBinary
}

 先に、二値画像をセットするためのGrayImageを用意しておいて、先ほどと同様に全てのピクセルに対してImageAImageBのグレー値を比較して、一致していなければGray{Y:255}=白色をセットしています。
 こうしておけば、一致する部分は黒、一致していない(間違ってるかもしれない)部分が白になって二値画像が出来上がります。
(入力データが写真から読み取ったりしていて、完全一致が難しい場合は、このへんを少し工夫する必要はあると思います)

そんなこんなで出来上がったものがこちらになります。
picture_C.png

それなりに綺麗にでましたが、輪郭の部分が白くなってしまっていますね。おそらく画像を切り取ってくる時に、完全に同じ部分を切り取ってこれてないのが原因でしょう。この部分に関してはあとで処理をします。

この時点でどこが間違っているのかは大体わかりますが、次の手順に進みます。


モルフォロジー変換(ErosionとDilation処理)

上の二値画像の中で

  • 間違いではないが、白くなってしまっている輪郭部分を消しおきたい

という気持ちが湧いてくると思うので、Erosion(縮小)、Dilation(膨張)という二つの処理をしていきます。
この二つの処理は、順番を変えたり、差分を撮ったりすることで二値画像を変換することができる便利なやつです。
こういった処理は、モルフォロジー変換という呼ばれており、他にも勾配を抽出したり、穴埋めをしたりいろんな使い方があります。気になった方はリンクを覗くと幸せになります。

今回は、邪魔な部分を消してしまいたいので、先に3回Erosion処理をしたあと、3回Dilation処理を行なっていきます。Erosionを行う部分は、

BinaryImage.go

func (g *Gray) ErosioningImage(times int) {

    for i := 0; i < times; i++ {
        resImage := image.NewGray(g.Rec)
        gray := color.Gray{Y: 255}
        for v := g.Rec.Min.Y; v < g.Rec.Max.Y; v++ {
            for h := g.Rec.Min.X; h < g.Rec.Max.X; h++ {
                g1 := g.Image.GrayAt(h-1, v).Y == 255
                g2 := g.Image.GrayAt(h+1, v).Y == 255
                g3 := g.Image.GrayAt(h, v-1).Y == 255
                g4 := g.Image.GrayAt(h, v+1).Y == 255
                g5 := g.Image.GrayAt(h-1, v-1).Y == 255
                g6 := g.Image.GrayAt(h-1, v+1).Y == 255
                g7 := g.Image.GrayAt(h+1, v-1).Y == 255
                g8 := g.Image.GrayAt(h+1, v+1).Y == 255
                //周りのピクセルが全部白の場合のみ明るくする
                if g1 && g2 && g3 && g4 && g5 && g6 && g7 && g8 {
                    resImage.SetGray(h, v, gray)
                }
            }
        }
        g.Image = resImage
    }

}

こんな感じです。網羅的に書いてしまっているので長いですが、コードをみれば何しているのかすぐわかりますね。
ある点とその周囲全てに対して、255かどうか、つまり白色かどうかを判断して、全て白である場合のみ白色を再セットしています。
引数にあるtimesとはErosionの回数で、回数をするほど、より黒っぽくなっていきます。

次にDilationを行なっている部分のコードは以下のようになります。

Dilation.go
func (g *Gray) DilationingImage(times int) {

    gray := color.Gray{Y: 255}
    for i := 0; i < times; i++ {
        resImage := image.NewGray(g.Rec)
        for v := g.Rec.Min.Y; v < g.Rec.Max.Y; v++ {
            for h := g.Rec.Min.X; h < g.Rec.Max.X; h++ {
                //その点が白なら周りも全部白にする
                if g.Image.GrayAt(h, v).Y == 255 {
                    resImage.SetGray(h, v, gray)
                    resImage.SetGray(h-1, v, gray)
                    resImage.SetGray(h+1, v, gray)
                    resImage.SetGray(h, v-1, gray)
                    resImage.SetGray(h, v+1, gray)
                    resImage.SetGray(h-1, v-1, gray)
                    resImage.SetGray(h-1, v+1, gray)
                    resImage.SetGray(h+1, v-1, gray)
                    resImage.SetGray(h+1, v+1, gray)
                }
            }
        }
        g.Image = resImage
    }
}

Erosion処理の時とは真反対に、ある点が白色であった場合に、周囲の全てを白色で塗りつぶしています。
そうすることで、Erotion処理で生き残った白色の部分を強調することができます。

Erosionを3回、Dilationを3回行うと二値画像はこんな感じになります。
picture_C.png

いいですね〜。余計だった輪郭の白の部分はほとんど消えて、間違いらしき部分が白くなっています。右上の方に少し白く残ってしまった部分がありますが、Erotionの回数などを増やせば消えるとは思います。


二値画像で白い部分にフィルターを作って元の画像に貼り合わせる

間違いであろう部分は大体洗い出せたので、今度はこれを元に間違っている部分を示すフィルターを作っていきたいと思います。
今回は、全体の縦横を20*20=400の区画に分割して、それぞれの区画の中の10%以上が白だった場合は、該当する区画を赤っぽくフィルターする感じでいきます。
説明だとわかりにくいので、先にフィルターかけた画像を載せておきます。
picture_C.png
こんな感じでフィルターをかけていきます。
実際に区画の10%以上が白かどうかをみているのは、

filter.go
func (f Filter) WatchArea(g *image.Gray, width int, height int, p image.Point) bool {

    x := p.X
    y := p.Y

    var count = 0
    gray := color.Gray{Y: 255}

    for v := 0; v < width; v++ {
        for h := 0; h < height; h++ {
            if g.GrayAt(x+h, y+v) == gray {
                count++
            }
        }
    }
    var result bool
    if count > (width*height)/f.Threshold {
        result = true
    } else {
        result = false
    }
    return result
}

こんな感じになります。
この中のwidthheightは一つの区画の縦横の大きさです。引数のpという変数に、調べたい区画の左上端の座標が入っているので、そこからWidht,heightの分のピクセルを全て見ていって、各ピクセルの色が白ならcountを増やすといった流れです。
countの値をみてbooleanで値を返すので、最終的にはboolean型の20*20二次元配列が得られます。

その二次元配列をつかって、

filter.go
    for h := 0; h < f.Division; h++ {
        for w := 0; w < f.Division; w++ {
         // その区画に対応する配列の要素がTrueであれば以下の処理
            if f.Lists[h][w] == true {
                x := boxWidth * w
                y := boxHeight * h
                for i := y; i < y+boxHeight; i++ {
                    for t := x; t < x+boxWidth; t++ {
                        _, G, B, _ := srcImg.At(t, i).RGBA()
                        img.Set(t, i, color.RGBA{
                            R: uint8(255),
                            G: uint8(G),
                            B: uint8(B),
                            A: 255,
                        })
                    }
                }
            }
        }
    }

こんな感じで元画像に対して赤っぽいフィルターをかけます。(ネストの深さは闇の深さ)

その配列のある要素がTrueであれば、その配列の要素に対応する画像の区画に対して、RGMのRを255にしています。そうすることで赤っぽい感じにできます。GBAはそのまま再代入しています。

あとは、その画像を出力して完成!!お疲れ様です。
別の画像に対して同じ処理をした結果、

picture_A.pngpicture_B.png
picture_A.pngpicture_A.png

こんな感じです。いいですね。間違いの部分がすぐわかります。

終わりに

標準パッケージだけでも簡単に実装できるあたりはさすがですね。ただ、私の書き方が悪い部分もあり、forループがかなり多くて嫌になりました。
Goのコレクション処理はScalaなんかに比べるとあまり強くないですからね(完全な言い訳)

長くなりましたがこれで終わります。最後まで読んでいただいてありがとうございます!!


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。