Works by

Ren's blog

アプリケーションバックエンド中心に書いていきます

【Golang】Goで画像処理: 画素勾配によるエッジ検出

画素勾配でエッジ検出

画像上に現れる物体の境界(エッジ)を検出する方法に、隣接する画素間の輝度の変化量を利用する方法があります。
この輝度の変化量が大きい部分をエッジとみなし、画像の分類や画像マッチングなどに使用できる特徴量として扱うことができます。

今回はこの方法を用いて、画像中のエッジを可視化してみました。

勾配

シンプルに輝度の勾配の大きさを求めるため、輝度勾配ベクトルのノルムを指標として使用します。

wピクセル、高さhピクセルの画像における、画素(i,j)の輝度をI_{ij}としたとき( 0 \leq i < w,  0 \leq j < h )、画素の輝度の勾配ノルム |\Delta|は以下のように求めます。

 d_x = I_{i + 1 , j} - I_{i, j}
 d_y = I_{i , j + 1} - I_{i, j}
 |\Delta| = \sqrt{d_x^2+d_y^2}

 d_x, d_yはそれぞれ水平・垂直方向の画素輝度差分を表しています。これらの二乗和平方根を輝度勾配ベクトルのノルム、すなわち勾配輝度の大きさとします。

実装

上記のエッジ検出をGoで実装しました。

画素(i,j)の右隣、下隣の画素との差分を求めているので、出力する輝度勾配ノルムの画像サイズは幅w-1、高さh-1となります。今回は簡単のため、末尾ピクセルi=wまたはj=hピクセル)はノルム0としました。
また、あくまで勾配の大きさを可視化するにとどめ、しきい値を決定した上でのエッジ判定は行いません。

package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "math"
    "os"
)

// 勾配
type Gradient struct {
    dr int64
    dg int64
    db int64
    da int64
}

func main() {

    // 入力画像パス
    img, _ := jpeg.Decode(os.Stdin)

    // 出力画像
    bounds := img.Bounds()
    dest := image.NewRGBA(bounds)

    for y := bounds.Min.Y; y < bounds.Max.Y-1; y++ {
        for x := bounds.Min.X; x < bounds.Max.X-1; x++ {

            lt := img.At(x, y)
            rt := img.At(x+1, y)
            lb := img.At(x, y+1)

            dx := createGradient(&lt, &rt)
            dy := createGradient(&lb, &rt)

            d := createGradientNormRGBA(dx, dy)

            dest.Set(x, y, d)
        }
    }

    err := jpeg.Encode(os.Stdout, dest, nil)
    if err != nil {
        panic("Failed to encode JPEG gradient image.")
    }
}

// 画素RGBAの差分を求める関数
func createGradient(c1, c2 *color.Color) *Gradient {
    r1, g1, b1, a1 := (*c1).RGBA()
    r2, g2, b2, a2 := (*c2).RGBA()
    return &Gradient{
        int64(r2) - int64(r1),
        int64(g2) - int64(g1),
        int64(b2) - int64(b1),
        int64(a2) - int64(a1),
    }
}

// 画素のRGBA輝度勾配のノルムを求める関数
func createGradientNormRGBA(dx, dy *Gradient) *color.RGBA {
    dr := createGradientNorm(dx.dr, dy.dr)
    dg := createGradientNorm(dx.dg, dy.dg)
    db := createGradientNorm(dx.db, dy.db)
    da := createGradientNorm(dx.da, dy.da)
    return &color.RGBA{R: dr, G: dg, B: db, A: da}
}

// 輝度勾配のノルムを求める関数
func createGradientNorm(dx, dy int64) uint8 {
    d := math.Sqrt(float64(dx*dx + dy*dy))
    return uint8(float64(d) / math.Pow(2, 17) * 255)
}

出力画像はRGBAそれぞれの規模勾配ノルムを求めた上で、256階調のRGBA画像として出力したものとしています。 最終的な輝度はuint8に収まるように変換しているのですが、正規化のための最大値は2 ^17と決めでやっちゃってます。。。 ちゃんと正規化するなら、輝度勾配の最大値で正規化します。

結果

$ go run grad.go < gophar.jpeg > edge.jpeg

gophar.jpeg

f:id:rennnosukesann:20190815213704j:plain

edge.jpeg

f:id:rennnosukesann:20190816002917j:plain

入力した画像のエッジが取れているのが見て取れます。
Gopharくんの境界はやや太めなので、境界中に輝度勾配の変化のない領域があるのがわかりますね。

参考文献

ja.wikipedia.org

www.motorwarp.com