はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】ComputeShaderでガウシアンフィルタを実装してみる【Q9】

ガウシアンフィルタとは

ガウシアンフィルタは画像の平滑化などに使われるフィルタの一つです。

仕組みとしては空間フィルタリングと呼ばれる周辺の画素も含めた領域内の画素値を用いて,出力画像の対応する画素値を求めます。

f:id:hanaaaaaachiru:20200213155752p:plain

数式としてはこんな感じ。

f:id:hanaaaaaachiru:20200213160456p:plain

f(i,j)入力画像g(i,j)出力画像h(m,n)フィルタです。

今回用いる3x3フィルタならw=1となります。

f:id:hanaaaaaachiru:20200213161216p:plain

この計算のことを畳み込み演算マスク演算とも呼びます。

一見難しく感じるかもしれませんが、実際にコードをみてみるとやっていることはシンプルだと感じるかもしれません。


数式内のh(m,n)に対応する3x3のガウシアンフィルタはこちら。

f:id:hanaaaaaachiru:20200213161721p:plain

これを用いてコードを書いていきましょう。

コード

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内でのOpenCVComputeShaderのどちらが速いか試してみるのも面白そうです。

時間があったら試してみようかなと思ったり。

とりあえず今回はここまで。