@page "/opencvsharp_sample"
@using OpenCvSharp
@inject IJSRuntime jsRuntime;
@inject HttpClient httpClient;
@implements IDisposable

<PageTitle>Gabor Filter Test</PageTitle>

<div>
    <h1 id="-gabor-filter-"><strong>Gabor Filter란?</strong></h1><br />
    <p>Gabor Filter는 영상처리에서 Bio-inspired라는 키워드가 있으면 빠지지않고 등장한다.</p>
    <p>외곽선을 검출하는 기능을 하는 필터로, 사람의 시각체계가 반응하는 것과 비슷하다는 이유로 널리 사용되고 있다.</p>
    <p>Gabor Fiter는 간단히 말해서 사인 함수로 모듈레이션 된 Gaussian Filter라고 생각할 수 있다.</p>
    <p>파라미터를 조절함에 따라 Edge의 크기나 방향성을 바꿀 수 있으므로 Bio-inspired 영상처리 알고리즘에서 특징점 추출 알고리즘으로 핵심적인 역할을 하고 있다.</p>
    <p>2D Gabor Filter의 수식은 아래와 같다.</p>
    <p>
        <img src="images/gaborfilter.png" alt="gaborfilter"><br />
        <img src="images/gaborfilter2.png" alt="gaborfilter"><br />
        <img src="images/gaborfilter3.png" alt="gaborfilter"><br />
    </p><br /><br />
    <h3 id="-parameters-"><strong>함수 원형</strong></h3>
    <pre style ="background-color: #f8f8f8;">
        <code class="lang-cpp">
  cv::Mat cv::getGaborKernel(cv::Size ksize, <span class="hljs-keyword">double</span> sigma, <span class="hljs-keyword">double</span> theta, <span class="hljs-keyword">double</span> lambd, <span class="hljs-keyword">double</span> gamma, <span class="hljs-keyword">double</span> psi = CV_PI*<span class="hljs-number">0.5</span>, <span class="hljs-keyword">int</span> ktype = CV_64F)  
        </code>
    </pre><br />
    <p>cv::getGaborKernel 함수는 OpenCV에서 가버필터(Gabor filter)를 생성하는 데 사용된다.</p>
    <p>가버필터는 이미지 처리와 컴퓨터 비전에서 특정 방향성과 주파수의 특징을 강조하는 데 사용되는 선형 필터이다. </p><br /><br />
    <h3 id="-parameters-"><strong>Parameters</strong></h3><br />
    <ul>
        <li>
            <p>ksize: 커널의 크기로, cv::Size 타입. 커널의 너비와 높이를 지정.</p>
        </li>
        <li>
            <p>sigma: 가우시안 함수의 표준 편차. 값이 커질수록 커널의 크기가 커지고, 필터의 감도가 낮아진다.</p>
        </li>
        <li>
            <p>theta: 필터의 방향을 라디안 단위로 지정한다. 0은 수평 방향, CV_PI/2는 수직 방향을 의미한다.</p>
        </li>
        <li>
            <p>lambd: 가버필터의 파장. 이미지의 특정 패턴과 얼마나 잘 매치할지를 결정한다.</p>
        </li>
        <li>
            <p>gamma: 공간종횡비(Spatial aspect ratio)로, 타원형 가버 함수의 타원성을 결정한다. (gamma=1은 원형, gamma&lt;1은 타원형)</p>
        </li>
        <li>
            <p>psi: 위상변위(Phase offset)로, 가버 필터의 위상을 조절한다. (일반적으로 CV_PI*0.5가 기본값으로 사용)</p>
        </li>
        <li>
            <p>ktype: 커널 타입. (보통 CV_64F (64-비트 부동소수점)를 사용)</p><br />
        </li>
    </ul>
</div>
<br />
<div>
    <h3 id="-parameters-"><strong>Gabor Filter 실행 (속도 느림 주의)</strong></h3>
    <canvas @ref="srcCanvas2" width="256" height="256" style="border:1px solid gray;">
        Your browser does not support the HTML5 canvas tag.
    </canvas>
    <canvas @ref="dstCanvas2" width="256" height="256" style="border:1px solid gray;">
        Your browser does not support the HTML5 canvas tag.
    </canvas>
</div>
<div>
    <p>Kernel Size(0 이상 홀수)</p>
    <input type="range" min="3" max="127" step="2" @bind="@valKernelSize" @bind:event="oninput" />
    <input type="text" @bind="@valKernelSize" /><br />
    <p>Sigma (0.0 이상)</p>
    <input type="range" min="0" max="1000" step="0.01" @bind="@valSigma" @bind:event="oninput" />
    <input type="text" @bind="@valSigma" /><br />
    <p>Theta (0도 ~ 180도)</p>
    <input type="range" min="0" max="180" step="0.01" @bind="@valTheta" @bind:event="oninput" />
    <input type="text" @bind="@valTheta" /><br />
    <p>Lambda (0.0 이상 이미지 사이즈 미만(256.0))</p>
    <input type="range" min="0" max="255" step="0.01" @bind="@valLambd" @bind:event="oninput" />
    <input type="text" @bind="@valLambd" /><br />
    <p>Gamma (0.0 ~ 1.0)</p>
    <input type="range" min="0.0" max="1.0" step="0.01" @bind="@valGamma" @bind:event="oninput" />
    <input type="text" @bind="@valGamma" /><br />
    <p>Psi (0도 ~ 360도)</p>
    <input type="range" min="0.0" max="360" step="0.01" @bind="@valPsi" @bind:event="oninput" />
    <input type="text" @bind="@valPsi" /><br />
    <button @onclick="@(async () => await GaborFilter())">GaborFilter</button>
</div>
<br />
<div>
<canvas @ref="srcCanvas" width="256" height="256" style="border:1px solid gray;">
    Your browser does not support the HTML5 canvas tag.
</canvas>
<canvas @ref="dstCanvas" width="256" height="256" style="border:1px solid gray;">
    Your browser does not support the HTML5 canvas tag.
</canvas>
</div>
<div>
    <p>Threshold (0 ~ 255)</p>
    <input type="range" min="0" max="255" step="1" @bind="@valthreshold" @bind:event="oninput" />
    <input type="text" @bind="@valthreshold" /><br />
    <button @onclick="@(async () => await Grayscale())">Grayscale</button>
    <button @onclick="@(async () => await PseudoColor())">PseudoColor</button>
    <button @onclick="@(async () => await Threshold())">Threshold</button>
</div>


@code {
    private Mat? srcMat;
    private Mat? srcMat2;
    private ElementReference srcCanvas;
    private ElementReference dstCanvas;
    private ElementReference srcCanvas2;
    private ElementReference dstCanvas2;
    private CanvasClient? srcCanvasClient;
    private CanvasClient? dstCanvasClient;
    private CanvasClient? srcCanvasClient2;
    private CanvasClient? dstCanvasClient2;
    private int valKernelSize = 3;
    private double valSigma = 0.0;
    private double valTheta = 0.0;
    private double valLambd = 0.0;
    private double valGamma = 0.0;
    private double valPsi = 0.0;
    private int valthreshold = 127;

    public void Dispose()
    {
        srcMat?.Dispose();
        srcMat2?.Dispose();
    }

    protected override async Task OnInitializedAsync()
    {        
        await base.OnInitializedAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
            return;
        await base.OnAfterRenderAsync(firstRender);

        var imageBytes = await httpClient.GetByteArrayAsync("/images/mandrill.bmp");
        srcMat ??= Mat.FromImageData(imageBytes);
        if(srcMat.Width < 256 || srcMat.Height < 256)
        {
            Cv2.PyrUp(srcMat, srcMat, new Size(256, 256));
        }
        srcCanvasClient ??= new CanvasClient(jsRuntime, srcCanvas);
        dstCanvasClient ??= new CanvasClient(jsRuntime, dstCanvas);

        await srcCanvasClient.DrawMatAsync(srcMat);


        var imageBytes2 = await httpClient.GetByteArrayAsync("/images/lenna.bmp");
        srcMat2 ??= Mat.FromImageData(imageBytes2);
        if(srcMat2.Width > 256 || srcMat2.Height > 256)
        {
            Cv2.PyrDown(srcMat2, srcMat2, new Size(256, 256));
        }

        srcCanvasClient2 ??= new CanvasClient(jsRuntime, srcCanvas2);
        dstCanvasClient2 ??= new CanvasClient(jsRuntime, dstCanvas2);

        await srcCanvasClient2.DrawMatAsync(srcMat2);
    }

    private async Task Grayscale()
    {
        if (srcMat is null)
            throw new InvalidOperationException($"{nameof(srcMat)} is null");
        if (dstCanvasClient is null)
            throw new InvalidOperationException($"{nameof(dstCanvasClient)} is null");

        using var grayMat = new Mat();
        Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.BGR2GRAY);

        await dstCanvasClient.DrawMatAsync(grayMat);
    }

    private async Task PseudoColor()
    {
        if (srcMat is null)
            throw new InvalidOperationException($"{nameof(srcMat)} is null");
        if (dstCanvasClient is null)
            throw new InvalidOperationException($"{nameof(dstCanvasClient)} is null");

        using var grayMat = new Mat();
        using var dstMat = new Mat();
        Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.BGR2GRAY);
        Cv2.ApplyColorMap(grayMat, dstMat, ColormapTypes.Jet);

        await dstCanvasClient.DrawMatAsync(dstMat);
    }

    private async Task Threshold()
    {
        if (srcMat is null)
            throw new InvalidOperationException($"{nameof(srcMat)} is null");
        if (dstCanvasClient is null)
            throw new InvalidOperationException($"{nameof(dstCanvasClient)} is null");

        using var grayMat = new Mat();
        using var dstMat = new Mat(srcMat.Size(), MatType.CV_8UC1);
        Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.BGR2GRAY);
        //Cv2.Threshold(grayMat, dstMat, 127, 255, ThresholdTypes.Binary);

        for(int i=0; i<grayMat.Rows; i++)
        {
            for(int j=0; j<grayMat.Cols; j++)
            {
                byte pixelValue = grayMat.At<byte>(i, j);
                byte newValue = (Convert.ToInt32(pixelValue) > valthreshold) ? (byte)255 : (byte)0;
                dstMat.Set<byte>(i, j, newValue);
            }
        }

        await dstCanvasClient.DrawMatAsync(dstMat);
    }

    private async Task GaborFilter()
    {
        if (srcMat2 is null)
            throw new InvalidOperationException($"{nameof(srcMat2)} is null");
        if (dstCanvasClient2 is null)
            throw new InvalidOperationException($"{nameof(dstCanvasClient2)} is null");

        using var grayMat = new Mat(srcMat2.Size(), MatType.CV_32F);
        Cv2.CvtColor(srcMat2, grayMat, ColorConversionCodes.BGR2GRAY);

        int kernel_size = valKernelSize;
        double sigma = valSigma;
        double theta = valTheta * Cv2.PI / 180.0;
        double lambd = valLambd;
        double gamma = valGamma;
        double psi = valPsi * Cv2.PI / 180.0;


        Mat rst = new Mat();
        try
        {
            //Mat gabor_filter = Cv2.GetGaborKernel(new Size(kernel_size, kernel_size), sigma, theta, lambd, gamma, psi, ktype);
            //Mat kenel = new Mat(5, 5, MatType.CV_8UC1);
            //Cv2.Filter2D(grayMat, rst, grayMat.Type(), kenel);

            Mat gabor_filter = Gabor2DFilter.CreateGaborKernel(kernel_size, sigma, theta, lambd, gamma, psi);
            rst = Gabor2DFilter.Filter2D(grayMat, gabor_filter);
        }
        catch(Exception e)
        {
            String log = e.Message;
        }
        await dstCanvasClient2.DrawMatAsync(rst);
    }

    public class Gabor2DFilter
    {
        public static Mat CreateGaborKernel(int ksize, double sigma, double theta, double lambd, double gamma, double psi)
        {
            int center = ksize / 2;
            Mat kernel = new Mat(ksize, ksize, MatType.CV_32F);
            double sigmaX = sigma;
            double sigmaY = sigma / gamma;

            //for (int y = -center; y <= center; y++)
            //{
            //    for (int x = -center; x <= center; x++)
            //    {
            //        double xPrime = x * Math.Cos(theta) + y * Math.Sin(theta);
            //        double yPrime = -x * Math.Sin(theta) + y * Math.Cos(theta);
            //        double value = Math.Exp(-0.5 * (xPrime * xPrime / (sigmaX * sigmaX) + yPrime * yPrime / (sigmaY * sigmaY)));
            //        value *= Math.Cos(2 * Math.PI * xPrime / lambd + psi);
            //        kernel.Set<float>(center + y, center + x, Convert.ToSingle(value));
            //    }
            //}
            unsafe
            {
                for (int y = -center; y <= center; y++)
                {
                    for (int x = -center; x <= center; x++)
                    {
                        double xPrime = x * Math.Cos(theta) + y * Math.Sin(theta);
                        double yPrime = -x * Math.Sin(theta) + y * Math.Cos(theta);
                        double value = Math.Exp(-0.5 * (xPrime * xPrime / (sigmaX * sigmaX) + yPrime * yPrime / (sigmaY * sigmaY)));
                        value *= Math.Cos(2 * Math.PI * xPrime / lambd + psi);

                        float* kernelPtr = (float*)(kernel.DataPointer + (center + y) * kernel.Step() + (center + x) * sizeof(float));
                        *kernelPtr = Convert.ToSingle(value);
                    }
                }
            }
            return kernel;
        }

        public static Mat Filter2D(Mat src, Mat kernel)
        {
            int kernelRows = kernel.Rows;
            int kernelCols = kernel.Cols;
            int kernelCenterX = kernelCols / 2;
            int kernelCenterY = kernelRows / 2;
            MatType type = src.Type();
            Mat dst = new Mat(src.Size(), type);

            try
            {
                //for (int y = 0; y < src.Rows; y++)
                //{
                //    for (int x = 0; x < src.Cols; x++)
                //    {
                //        double sum = 0.0;
                //
                //        for (int kRow = 0; kRow < kernelRows; kRow++)
                //        {
                //            int yy = y + kRow - kernelCenterY;
                //
                //            for (int kCol = 0; kCol < kernelCols; kCol++)
                //            {
                //                int xx = x + kCol - kernelCenterX;
                //
                //                if (xx >= 0 && xx < src.Cols && yy >= 0 && yy < src.Rows)
                //                {
                //                    byte pixelValue = src.At<byte>(yy, xx);
                //                    double kernelValue = kernel.At<double>(kRow, kCol);
                //                    sum += Convert.ToDouble(pixelValue) * kernelValue;
                //                }
                //            }
                //        }
                //
                //        byte resultValue = dst.At<byte>(y, x);
                //        resultValue = (byte)Math.Clamp(sum, 0, 255);
                //        dst.Set<byte>(y, x, resultValue);
                //    }
                //}
                unsafe
                {
                    for (int y = 0; y < src.Rows; y++)
                    {
                        for (int x = 0; x < src.Cols; x++)
                        {
                            double sum = 0.0;

                            for (int kRow = 0; kRow < kernelRows; kRow++)
                            {
                                int yy = y + kRow - kernelCenterY;
                                for (int kCol = 0; kCol < kernelCols; kCol++)
                                {
                                    int xx = x + kCol - kernelCenterX;

                                    if (xx >= 0 && xx < src.Cols && yy >= 0 && yy < src.Rows)
                                    {
                                        byte* pixelValue = src.DataPointer + yy * src.Step() + xx * src.ElemSize();
                                        double kernelValue = kernel.At<double>(kRow, kCol);
                                        sum += Convert.ToDouble(*pixelValue) * kernelValue;
                                    }
                                }
                            }

                            byte* resultValue = dst.DataPointer + y * dst.Step() + x * dst.ElemSize();
                            *resultValue = (byte)Math.Clamp(sum, 0, 255);
                        }
                    }
                }
            }
            catch(Exception ex)
            {
                String log = ex.Message;
            }

            return dst;
        }
 


    }
}