画像処理の基本的なアルゴリズムをGo言語で復習 ４（バイリニア補間）

画像処理の代表的なアルゴリズムをGO言語で実装しながら復習するシリーズです。

#はじめに
前回に引き続き、画像の拡大縮小に用いられる線形補間について振り返ります。
今回はバイリニア補間(双一次補間)についてです。

# バイリニア補間(双一次補間)
注目画素の周辺２×２画素、合計で４画素を使って、画素値を直線的に補間する手法です。
前回よりも周辺画素の影響も考えた処理になっています。
以下図は、画像を横方向4倍にバイリニアで拡大する時の簡単なイメージです。

![img1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308887/8fad1b37-ea81-9b09-df71-30bec5a9a0ff.png)

厳密には異なる表現もあるかもしれないですが、ざっくりこんな感じの理解で良いかと。

##検証方法
以下手順で検証を行いました。

```
① 元画像をPhotoshopのバイキュービック法で1/2に縮小
② 縮小した元画像を今回実装したバイリニア補間で2倍に拡大
③ 元画像とバイリニア補間で2倍に拡大した画像を比較
```

##ソースコード
__Bilinear.go__

```Go
package Bilinear

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

func Init() {

}

// バイリニア法
func Execution(preImg gocv.Mat, inputImageRows int, inputImageCols int, scalingRows float64, scalingCols float64) gocv.Mat {

	// 重み値を定義
	var x float64
	var y float64
	// リサイズ後画像サイズ
	resizeImageRows := int(float64(inputImageRows) * scalingRows)
	resizeImageCols := int(float64(inputImageCols) * scalingCols)

	// 逆数
	reciprocalScalingRows := 1 / scalingRows
	reciprocalScalingCols := 1 / scalingCols

	// アウトプット画像を定義
	outputImg := gocv.NewMatWithSize(resizeImageRows, resizeImageCols, gocv.IMReadGrayScale)

	// 画像の左上から順に画素を読み込む
	for imgRows := 0; imgRows < resizeImageRows; imgRows++ {
		for imgCols := 0; imgCols < resizeImageCols; imgCols++ {

			// 双一次補完式

			// 元画像の座標定義
			// 元画像の縦の座標
			inputRows := int(float64(imgRows) / scalingRows)
			// 元画像の横の座標
			inputCols := int(float64(imgCols) / scalingCols)


			// 補完式で使う元画像のpixel
			// point(0, 0)
			src00 := float64(preImg.GetUCharAt(inputRows+inputImageRows, inputCols+inputImageCols))
			// point(0, 1)
			src01 := float64(preImg.GetUCharAt(inputRows+inputImageRows, inputCols+inputImageCols+1))
			// point(1, 0)
			src10 := float64(preImg.GetUCharAt(inputRows+inputImageRows+1, inputCols+inputImageCols))
			// point(1, 1)
			src11 := float64(preImg.GetUCharAt(inputRows+inputImageRows+1, inputCols+inputImageCols+1))

			// 重み値を算出
			x = float64(imgCols) * reciprocalScalingCols
			y = float64(imgRows) * reciprocalScalingRows
			// 小数点以下を抽出
			x = x - float64(int(x))
			y = y - float64(int(y))

			// 拡大後の画素を算出
			pixel1 := (1 - x) * (1 - y) * src00
			pixel2 := x * (1 - y) * src01
			pixel3 := (1 - x) * y * src10
			pixel4 := x * y * src11

			pixel := pixel1 + pixel2 + pixel3 + pixel4

			outputImg.SetUCharAt(imgRows, imgCols, uint8(pixel))
		}
	}

	return outputImg
}

```

##実行結果
###元画像
![Lennagrey.png](https://qiita-image-store.s3.amazonaws.com/0/308887/f8829e3f-c6b8-daf7-848f-d769ac6c96b4.png)

###実行後画像
![Bili2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308887/5a4bc849-bbf8-5bb9-2bc3-76adb88020cf.png)

ニアレストネイバーと比べると、多少元画像に近づいているように見えます。
エッジ部分はバイリニア補間の方が滑らかになっています。

###元画像と実行後画像の差分
![Bili2_dis.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308887/0fbc6a4a-1b94-8560-4b4f-a71edcc028e2.png)


元画像との差を見てみると、満遍なく差が出ているように見えます。
エッジが滑らかになっている影響でしょうか。

###MSE/PSNR
```
[MSE] value :  +6.462402e+001 [PSNR] value :  +3.002686e+001
```
前回のニアレストネイバーよりPSNRが上がっています。
今回使った画像では、周辺画素の影響を考えることで、元画像との差分を減らすことが出来ることが分かります。

#まとめ
バイリニア補間ではニアレストネイバーに比べ、エッジのギザギザが緩和されていました。
ただこの結果で、「バイリニアの方が優れている」となるよりかは、場合によって使い分けていくのが無難かと思います。処理する画像によっても性質は色々なので。