はなちるのマイノート

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

【Unity】ComputeShaderで Laplacianフィルタを実装してみる【Q17】

Laplacianフィルタとは

ラプラシアンフィルタは2次微分を利用してエッジを検出するフィルタになります。

最近はフィルタの説明を結構省き気味だったので、今回は少し詳しく説明しようと思います。

高校数学で習う微分ですが、こんな公式で表されます。

f:id:hanaaaaaachiru:20200221233720p:plain

ただし画像の世界ではピクセルで表されているので、h->0というのは「隣同士」が限界になってしまいます。

それを考慮すると、むしろ小学生でも分かりそうなほど簡単な式で表されます。

f'(x) = f(x+1) - f(x)

これを画像処理の分野では、fIに置き換えて、x方向に微分したものをIx,y方向に微分したものをIyと表現します。

f:id:hanaaaaaachiru:20200221234519p:plain

次は大学で習うラプラシアンですが、公式はこちら。

f:id:hanaaaaaachiru:20200221234927p:plain

偏微分が使われているので習っていないと難しく感じるかもしれませんが、複数の変数(f(x,y)みたいな関数で)のうち一つの変数だけが変化するものとみなし、残りの変数を定数として扱う手法です。

あんまり難しく考えずに微分のお兄ちゃん的なものだと思えばOKです。

これを画像処理の世界で表すと、

f:id:hanaaaaaachiru:20200222000700p:plain

になります。

これをフィルタに表すとこちらになります。

f:id:hanaaaaaachiru:20200222000930p:plain

コード

ComputeShaderはこちら。

using UnityEngine;
using UnityEngine.UI;

public class LaplacianFilter : 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();

        // カーネルインデックスを取得
        var kernelIndex = _computeShader.FindKernel("LaplacianFilter");

        // 一つのグループの中に何個のスレッドがあるか
        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;
    }
}


C#側のコードはこちら。

#pragma kernel LaplacianFilter

RWTexture2D<float4> Result;
Texture2D<float4> Texture;

[numthreads(32,16,1)]
void LaplacianFilter (uint3 id : SV_DispatchThreadID)
{
    float3x3 filter = float3x3(0, 1, 0, 1, -4, 1, 0, 1, 0);
    float4 rgb2gray = float4(0.2126, 0.7152, 0.0722, 0);
    uint i;

    float array[9] = {
        dot(Texture[id.xy + int2(-1, -1)], rgb2gray) * filter[0][0],
        dot(Texture[id.xy + int2(0, -1)], rgb2gray) * filter[0][1],
        dot(Texture[id.xy + int2(1, -1)], rgb2gray) * filter[0][2],
        dot(Texture[id.xy + int2(-1, 0)], rgb2gray) * filter[1][0],
        dot(Texture[id.xy], rgb2gray) * filter[1][1],
        dot(Texture[id.xy + int2(1, 0)], rgb2gray) * filter[1][2],
        dot(Texture[id.xy + int2(-1, 1)], rgb2gray) * filter[2][0],
        dot(Texture[id.xy + int2(0, 1)], rgb2gray) * filter[2][1],
        dot(Texture[id.xy + int2(1, 1)], rgb2gray) * filter[2][2]
    };

    float tmp = 0;
    for(i = 0; i < 9; i++){
        tmp += array[i];
    }
    Result[id.xy] = float4(tmp, tmp, tmp, 1);
}

さいごに

フィルタがどのように設計されているかを紹介しましたが、実際のコードはほとんどいつものと同じものです。

いままでの様々なフィルタにも色んな仕組みがあってフィルタが設計されているというわけですね。

ただ今回のLaplacianフィルタは結構数学のとっかかりとしては良いと思うので覚えておくと良いかもしれません。

ではまた。