LoginSignup
18
6

More than 3 years have passed since last update.

画像処理の基本的なアルゴリズムをGo言語で復習 1(平滑化)

Last updated at Posted at 2018-12-13

ZOZOテクノロジーズその2 Advent Calendar 2018 の記事です。
ここでは、画像処理の代表的なアルゴリズムをGO言語で実装しながら復習したいと思います。

はじめに

今回実装するのは、画像処理の中でも基本的な平滑化に関するアルゴリズムです。
普段はノイズの除去等に使われていますが、実際どのような効果があるのか見ていきたいと思います。

OpenCVのインストール

gocvlogo.jpg
by Renée French
Reference GoCV

自分がMacで行なったセットアップ方法と、その他のセットアップ方法です。

MacOSの場合

以下のコマンドをターミナルで実行

OpenCVのインストール

$ brew install opencv

※ インストールしたバージョンが低い場合は、

$ brew upgrade opencv 

を実行。

パッケージを取得

$ go get -u -d gocv.io/x/gocv

その他OSの場合

https://github.com/hybridgroup/gocv
上記URL先のREADEMEを参照

補足

OpenCV4をインストールする場合は手法が変わってきます。
詳しくはこちらの方の記事が参考になります。

画像端の処理

フィルタ処理をコーディングするときに、まず考えるのが「画像の端ってどう処理するの?」だと思います。
画素が存在しない端を0で埋めて対応する場合もありますが、今回は自分が使っている手法を記載します。

ソースコード

package Preparation

import "gocv.io/x/gocv"

var ImgRows int32
var ImgCols int32

func Init() {
    ImgRows = 0
    ImgCols = 0
}

func Execution(mat gocv.Mat) gocv.Mat {

    // 上下反転
    centerImg := gocv.NewMat()
    gocv.Flip(mat, &centerImg, 0)

    // 左右反転
    sideImg := gocv.NewMat()
    gocv.Flip(mat, &sideImg, 1)

    // 左右上下反転
    diagonalImg := gocv.NewMat()
    gocv.Flip(mat, &diagonalImg, -1)

    // 上結合
    upImg := gocv.NewMat()
    gocv.Hconcat(diagonalImg, centerImg, &upImg)
    gocv.Hconcat(upImg, diagonalImg, &upImg)

    // 真ん中結合
    middleImg := gocv.NewMat()
    gocv.Hconcat(sideImg, mat, &middleImg)
    gocv.Hconcat(middleImg, sideImg, &middleImg)

    // 下結合
    downImg := gocv.NewMat()
    gocv.Hconcat(diagonalImg, centerImg, &downImg)
    gocv.Hconcat(downImg, diagonalImg, &downImg)

    // 全結合
    preparationImg := gocv.NewMat()
    gocv.Vconcat(upImg, middleImg, &preparationImg)
    gocv.Vconcat(preparationImg, downImg, &preparationImg)

    return preparationImg

}

実行結果

元画像

Lennagrey.png

実行後

base.png

以下に記載しているソースコードでは上記画像に変換してからフィルタ処理を行なっています。実際の処理の中では真ん中の画像のみを参照しています。

MSE / PSNR

かなり大雑把に説明すると、2枚の画像で、同じ位置のピクセルの輝度の差分の2乗を集計し、その平均を算出するのがMSE。これを更に人の目の感覚に近づけるように算出したのがPSNRです。PSNRは値が大きいほど画像の劣化が少ないといわれています。
今回は各フィルタ処理の違いを数値上で見る為に実装しました。

ソースコード

package PSNR

import (
    "gocv.io/x/gocv"
    "math"
)

const MAX float64 = 255

func Init(){

}


// MSE/PSNR
func Execution(firstImg gocv.Mat, secondImg gocv.Mat) bool {

    // 画像サイズが異なる場合
    if firstImg.Rows() != secondImg.Rows() || firstImg.Cols() != secondImg.Cols() {
        println("[ERROR] Different Size Img")
        return false
    }

    // 初期化
    mseSum := float64(0)

    // 画像の左上から順に画素を読み込む
    for imgRows := 0; imgRows < firstImg.Rows(); imgRows++ {
        for imgCols := 0; imgCols < firstImg.Cols(); imgCols++ {

            // 差分を算出
            distance := float64(firstImg.GetUCharAt(imgRows, imgCols)) - float64(secondImg.GetUCharAt(imgRows, imgCols))
            mseSum += math.Pow(distance,2)
        }
    }

    // MSEを算出
    temp := float64(firstImg.Rows() * firstImg.Cols())
    mse := mseSum / temp

    // PSNRを算出
    psnr := (20 * math.Log10(MAX)) - (10 * math.Log10(mse))

    // コンソール上に出力
    println("[MSE] value : ", mse, "[PSNR] value : ", psnr)

    return true
}

平滑化

作ったソースコードのメイン文を以下に記載します。

ソースコード

main.go

package main

import (
    "ImageFilter/Average"
    "ImageFilter/PSNR"
    "ImageFilter/Preparation"
    "fmt"
    "os"

    "gocv.io/x/gocv"
)


// main処理
func main() {

    // インプット先、アウトプット先が指定されていない場合
    if len(os.Args) < 2 {
        fmt.Println("Argument is missing [input path] [output path]")
        return
    }

    // 画像をインプット
    inputFileName := os.Args[1]
    inputImg := gocv.IMRead(inputFileName, gocv.IMReadGrayScale)

    // 画像がインプットできなかった場合
    if inputImg.Empty() {
        fmt.Println("Error reading image from: %v", inputFileName)
        return
    }

    // 画像の端の処理
    preImg := Preparation.Execution(inputImg)

    // アウトプット画像
    outputImg := Average.Execution(preImg, inputImg.Rows(), inputImg.Cols())
    //outputImg := Gaussian.Execution(preImg, inputImg.Rows(), inputImg.Cols())
    //outputImg := Median.Execution(preImg, inputImg.Rows(), inputImg.Cols())

    // 画像をアウトプット
    output := os.Args[2]
    gocv.IMWrite(output, outputImg)

    // PSNRを算出
    PSNR.Execution(inputImg, outputImg)

}

平均値フィルタ

平均値フィルタとは、画像をぼかしてノイズを除去するもので、注目画素の近くの画素値の平均から新しい画素を算出します。

ソースコード

Average.go

package Average

import "gocv.io/x/gocv"

var filterWeight = map[int]map[int]float64{}

// 重み値の合計
const weightSum int = 9

func Init() {

    // フィルタ処理で使用する重み値
    filterWeight[-1] = make(map[int]float64)
    filterWeight[0] = make(map[int]float64)
    filterWeight[1] = make(map[int]float64)
    filterWeight[-1][-1] = 1
    filterWeight[-1][0] = 1
    filterWeight[-1][1] = 1
    filterWeight[0][-1] = 1
    filterWeight[0][0] = 1
    filterWeight[0][1] = 1
    filterWeight[1][-1] = 1
    filterWeight[1][0] = 1
    filterWeight[1][1] = 1

}

// 平均値フィルタ処理
func Execution(preImg gocv.Mat, inputImageRows int, inputImageCols int) gocv.Mat {

    // 初期化
    Init()
    // アウトプット画像を定義
    outputImg := gocv.NewMatWithSize(inputImageRows, inputImageCols, gocv.IMReadGrayScale)

    // 画像のどこまで処理するか定義
    processRows := preImg.Rows() - inputImageRows
    processCols := preImg.Cols() - inputImageCols

    // 画像の左上から順に画素を読み込む
    for imgRows := inputImageRows; imgRows < processRows; imgRows++ {
        for imgCols := inputImageCols; imgCols < processCols; imgCols++ {

            attentionPixel := int(0)

            // 3 × 3の平滑化フィルタ
            for filterRows := -1; filterRows < 2; filterRows++ {
                for filterCols := -1; filterCols < 2; filterCols++ {

                    // 注目画素からみた画素を取得
                    inputPixel := float64(preImg.GetUCharAt(imgRows + filterRows, imgCols + filterCols))
                    // 取得した画素に重み値を掛け、足し合わせる
                    attentionPixel += int(inputPixel * filterWeight[filterRows][filterCols])
                }
            }

            // 重み値の合計分割る
            attentionPixel = attentionPixel / weightSum
            // 結果に画素を格納する
            outputImg.SetUCharAt(imgRows - inputImageRows, imgCols - inputImageCols, uint8(attentionPixel))
        }
    }

    return outputImg
}

実行結果

以下の結果より、画像が少しぼやけているのが分かります。

実行前

Lennagrey.png

実行後

Average.png

元画像と実行後画像の差分

Ave_Distance.png

MSE/PSNR

[MSE] value:+2.756442e+001 [PSNR] value:+3.372732e+001

補足

注目画素の重み値のみ2倍にすることで、より自然な平滑化が行えます。

// 重み値の合計
const weightSum int = 10

func Init() {

    // フィルタ処理で使用する重み値
    filterWeight[-1] = make(map[int]float64)
    filterWeight[0] = make(map[int]float64)
    filterWeight[1] = make(map[int]float64)
    filterWeight[-1][-1] = 1
    filterWeight[-1][0] = 1
    filterWeight[-1][1] = 1
    filterWeight[0][-1] = 1
    filterWeight[0][0] = 2
    filterWeight[0][1] = 1
    filterWeight[1][-1] = 1
    filterWeight[1][0] = 1
    filterWeight[1][1] = 1

}

実行結果

重み値が均一の平均値フィルタ処理よりもエッジ部分の差が少なく見えます。

実行前

Lennagrey.png

実行後

Average2.png

元画像と実行後画像の差分

Ave2_Distance.png

MSE/PSNR

[MSE] value:+2.239090e+001 [PSNR] value:+3.463009e+001

ガウシアンフィルタ

ガウシアンフィルタとは、注目画素からどれだけ離れてるかによって、重み値をつけるということを、ガウス分布を利用して行っています。
また、標準偏差の値が大きいほど平滑化の効果も大きくなります。
以下は標準偏差を1.3とした場合の実装です。

ソースコード

Gaussian.go

package Gaussian

import "gocv.io/x/gocv"

var filterWeight = map[int]map[int]float64{}

// 重み値の合計
const weightSum int = 16

func Init() {

    // フィルタ処理で使用する重み値
    filterWeight[-1] = make(map[int]float64)
    filterWeight[0] = make(map[int]float64)
    filterWeight[1] = make(map[int]float64)
    filterWeight[-1][-1] = 1
    filterWeight[-1][0] = 2
    filterWeight[-1][1] = 1
    filterWeight[0][-1] = 2
    filterWeight[0][0] = 4
    filterWeight[0][1] = 2
    filterWeight[1][-1] = 1
    filterWeight[1][0] = 2
    filterWeight[1][1] = 1

}

// ガウシアンフィルタ
func Execution(preImg gocv.Mat, inputImageRows int, inputImageCols int) gocv.Mat {

    // 初期化
    Init()
    // アウトプット画像を定義
    outputImg := gocv.NewMatWithSize(inputImageRows, inputImageCols, gocv.IMReadGrayScale)

    // 画像のどこまで処理するか定義
    processRows := preImg.Rows() - inputImageRows
    processCols := preImg.Cols() - inputImageCols

    // 画像の左上から順に画素を読み込む
    for imgRows := inputImageRows; imgRows < processRows; imgRows++ {
        for imgCols := inputImageCols; imgCols < processCols; imgCols++ {

            attentionPixel := int(0)

            // 3 × 3の平滑化フィルタ
            for filterRows := -1; filterRows < 2; filterRows++ {
                for filterCols := -1; filterCols < 2; filterCols++ {

                    // 注目画素からみた画素を取得
                    inputPixel := float64(preImg.GetUCharAt(imgRows + filterRows, imgCols + filterCols))
                    // 取得した画素に重み値を掛け、足し合わせる
                    attentionPixel += int(inputPixel * filterWeight[filterRows][filterCols])
                }
            }

            // 重み値の合計分割る
            attentionPixel = attentionPixel / weightSum
            // 結果画像に画素を格納する
            outputImg.SetUCharAt(imgRows - inputImageRows, imgCols - inputImageCols, uint8(attentionPixel))
        }
    }

    return outputImg
}

実行結果

以下の結果より、平均値フィルタに比べてぼやけが自然に出ているように見えます。

実行前

Lennagrey.png

実行後

Gaussian.png

元画像と実行後画像の差分

Gaussian_Distance.png

MSE/PSNR

[MSE] value:+1.746352e+001 [PSNR] value:+3.570948e+001

メディアンフィルタ

注目画素の近くの画素値を小さい順に並べて、真ん中の画素値を抽出し、注目画素の新しい画素値とする手法です。注目画像周辺の、ごま塩ノイズのような極端な値に左右されないという性質があります。

ソースコード

Median.go

package Median

import (
    "gocv.io/x/gocv"
    "sort"
)

var filterWeight = map[int]map[int]float64{}

// 重み値の合計
const weightSum int = 9

func Init() {

}

func Execution(preImg gocv.Mat, inputImageRows int, inputImageCols int) gocv.Mat {

    // 初期化
    Init()
    // アウトプット画像を定義
    outputImg := gocv.NewMatWithSize(inputImageRows, inputImageCols, gocv.IMReadGrayScale)

    // 画像のどこまで処理するか定義
    processRows := preImg.Rows() - inputImageRows
    processCols := preImg.Cols() - inputImageCols

    // 画像の左上から順に画素を読み込む
    for imgRows := inputImageRows; imgRows < processRows; imgRows++ {
        for imgCols := inputImageCols; imgCols < processCols; imgCols++ {

            var attentionPixel []int

            // 3 × 3の平滑化フィルタ
            for filterRows := -1; filterRows < 2; filterRows++ {
                for filterCols := -1; filterCols < 2; filterCols++ {

                    // 注目画素からみた画素を取得
                    inputPixel := int(preImg.GetUCharAt(imgRows + filterRows, imgCols + filterCols))
                    attentionPixel = append(attentionPixel, inputPixel)
                }
            }

            // 昇順にソートし、真ん中の値を注目画素の値と入れ替える
            sort.Ints(attentionPixel)
            centerNumberPoint := int(len(attentionPixel)/2)
            // 結果に画素を格納する
            outputImg.SetUCharAt(imgRows - inputImageRows, imgCols - inputImageCols, uint8(attentionPixel[centerNumberPoint]))
        }
    }

    return outputImg
}

実行結果

以下の結果より、他のフィルタ処理に比べてぼやけていないように見えます。
ただ、黒と白が混在している部分ではぼやけが出ているようにも見えます。

実行前

Lennagrey.png

実行後

Median.png

元画像と実行後画像の差分

Median_Distance.png

MSE/PSNR

[MSE] value:+1.798315e+001 [PSNR] value:+3.558214e+001

まとめ

同じ画像にそれぞれ違った平滑化を行うことで、同じノイズ除去のジャンルでも、それぞれの特徴や効果があることが改めて復習することが出来たと思います。

image.png

最後に、今回記事を書く中で、ボリュームが多くなってしまったこともあり、かなり基本的な技術しか記載することが出来ませんでした。
他のアルゴリズム等も、これから順にあげていけたらなと思っています!

18
6
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
18
6