はじめに
画像処理の続きをやっていきましょう。
前回 -> 【Unity】ComputeShaderでMaxプーリングをする【Q8】 - はなちるのマイノート
次回 -> 【Unity】ComputeShaderでメディアンフィルタを実装してみる【Q10】 - はなちるのマイノート
今回はガウシアンフィルタについて取り組んでいきます。
ガウシアンフィルタとは
ガウシアンフィルタは画像の平滑化などに使われるフィルタの一つです。
仕組みとしては空間フィルタリングと呼ばれる周辺の画素も含めた領域内の画素値を用いて,出力画像の対応する画素値を求めます。
数式としてはこんな感じ。
f(i,j)
が入力画像,g(i,j)
が出力画像,h(m,n)
がフィルタです。
今回用いる3x3フィルタならw=1
となります。
この計算のことを畳み込み演算やマスク演算とも呼びます。
一見難しく感じるかもしれませんが、実際にコードをみてみるとやっていることはシンプルだと感じるかもしれません。
数式内のh(m,n)
に対応する3x3のガウシアンフィルタはこちら。
これを用いてコードを書いていきましょう。
コード
ComputeShader
はこちら。
#pragma kernel Gaussian RWTexture2D<float4> Result; Texture2D<float4> Texture; [numthreads(8,8,1)] void Gaussian (uint3 id : SV_DispatchThreadID) { float3x3 filter = (1.0 / 16.0) * float3x3(1, 2, 1, 2, 4, 2, 1, 2, 1); float4 upperLeft = Texture[id.xy + int2(-1, 1)] * filter[0][2]; float4 up = Texture[id.xy + int2(0, 1)] * filter[1][2]; float4 upperRight = Texture[id.xy + int2(1, 1)] * filter[2][2]; float4 left = Texture[id.xy + int2(-1, 0)] * filter[0][1]; float4 middle = Texture[id.xy] * filter[1][1]; float4 right = Texture[id.xy + int2(1, 0)] * filter[2][1]; float4 lowerLeft = Texture[id.xy + int2(-1, -1)] * filter[0][0]; float4 down = Texture[id.xy + int2(0, -1)] * filter[1][0]; float4 lowerRight = Texture[id.xy + int2(1, -1)] * filter[2][0]; Result[id.xy] = upperLeft + up + upperRight + left + middle + right + lowerLeft + down + lowerRight; }
CPU側のコードはこれ。
using UnityEngine; using UnityEngine.UI; public class Gaussian : MonoBehaviour { [SerializeField] private ComputeShader _computeShader; [SerializeField] private Texture2D _tex; [SerializeField] private RawImage _renderer; struct ThreadSize { public uint x; public uint y; public uint z; public ThreadSize(uint x, uint y, uint z) { this.x = x; this.y = y; this.z = z; } } private void Start() { if (!SystemInfo.supportsComputeShaders) { Debug.LogError("Comppute Shader is not support."); return; } // RenderTextueの初期化 var result = new RenderTexture(_tex.width, _tex.height, 0, RenderTextureFormat.ARGB32); result.enableRandomWrite = true; result.Create(); // Gaussianのカーネルインデックス(0)を取得 var kernelIndex = _computeShader.FindKernel("Gaussian"); // 一つのグループの中に何個のスレッドがあるか ThreadSize threadSize = new ThreadSize(); _computeShader.GetKernelThreadGroupSizes(kernelIndex, out threadSize.x, out threadSize.y, out threadSize.z); // GPUにデータをコピーする _computeShader.SetTexture(kernelIndex, "Texture", _tex); _computeShader.SetTexture(kernelIndex, "Result", result); // GPUの処理を実行する _computeShader.Dispatch(kernelIndex, _tex.width / (int)threadSize.x, _tex.height / (int)threadSize.y, (int)threadSize.z); // テクスチャを適応 _renderer.texture = result; } }
追加説明
ComputeShader
内でTexture[int(-1,-1)]
やTexture[int2(5000,5000)]
みたいな画像の範囲外では、float4(0, 0, 0, 0)
が勝手に代入されています。
なので画素が足りない部分は0で埋める0パディングを明示的にする必要はありません。
ただもしかしたらアルファ値は1
の方が良いんでしょうか?
あんまりよく分かっていないので、間違っていたらあとで修正しようと思います。
さいごに
ちなみにこちらの記事でCPUのみでガウシアンフィルタを実装していらっしゃる方がいました。
qiita.com
こちらの記事によるとCPUのみでは10秒
ぐらいかかっているみたいですが、今回の実装では体感では分からないほどすぐに終わります。
またよく画像処理で用いられるライブラリのOpenCV
はネイティブプラグインを用いてC++
で実装されているから高速なのだと思いますが、ComputeShader
を用いればGPUを駆使しているのでそれよりも速い可能性もあるのではないのかと思います。
Unity内でのOpenCV
とComputeShader
のどちらが速いか試してみるのも面白そうです。
時間があったら試してみようかなと思ったり。
とりあえず今回はここまで。