Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 13개 있습니다.)
Graphics: 2. Unity로 실습하는 Shader
; https://www.sysnet.pe.kr/2/0/11607

Graphics: 3. Unity로 실습하는 Shader (1) - 컬러 반전 및 상하/좌우 뒤집기
; https://www.sysnet.pe.kr/2/0/11608

Graphics: 4. Unity로 실습하는 Shader (2) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model)
; https://www.sysnet.pe.kr/2/0/11609

Graphics: 5. Unity로 실습하는 Shader (3) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model) + Texture
; https://www.sysnet.pe.kr/2/0/11610

Graphics: 6. Unity로 실습하는 Shader (4) - 퐁 셰이딩(phong shading)
; https://www.sysnet.pe.kr/2/0/11611

Graphics: 7. Unity로 실습하는 Shader (5) - Flat Shading
; https://www.sysnet.pe.kr/2/0/11613

Graphics: 8. Unity Shader - Texture의 UV 좌표에 대응하는 Pixel 좌표
; https://www.sysnet.pe.kr/2/0/11614

Graphics: 9. Unity Shader - 전역 변수의 초기화
; https://www.sysnet.pe.kr/2/0/11616

Graphics: 10. Unity로 실습하는 Shader (6) - Mosaic Shading
; https://www.sysnet.pe.kr/2/0/11619

Graphics: 11. Unity로 실습하는 Shader (7) - Blur (평균값, 가우스, 중간값) 필터
; https://www.sysnet.pe.kr/2/0/11620

Graphics: 12. Unity로 실습하는 Shader (8) - 다중 패스(Multi-Pass Shader)
; https://www.sysnet.pe.kr/2/0/11628

Graphics: 13. Unity로 실습하는 Shader (9) - 투명 배경이 있는 텍스처 입히기
; https://www.sysnet.pe.kr/2/0/11631

Graphics: 19. Unity로 실습하는 Shader (10) - 빌보드 구현
; https://www.sysnet.pe.kr/2/0/11641




Unity로 실습하는 Shader (7) - Blur (평균값, 가우스, 중간값) 필터

지난 글에서,

Unity로 실습하는 Shader (6) - Mosaic Shading
; https://www.sysnet.pe.kr/2/0/11619

for 루프를 이용한 처리를 봤는데, 그렇다면 texture를 본연의 이미지 데이터라 보고 OpenCV를 통해했던 이미지 프로세싱까지 - 가령 mask 처리 같은 것도 가능하다는 시나리오가 나옵니다. 그중에서 3가지 스무딩 효과를 Unity shader로 구현해 보겠습니다.




우선, OpenCV의 box filter인 평균값 필터를 다음과 같이 구현해 볼 수 있습니다.

fixed4 frag(v2f i) : SV_Target
{
    float maskWidth = 5; // 외부 변수 처리 권장
    float maskOffset = -floor(maskWidth / 2.0);
            
    float homogeneousBlurFilter = 1.0 / (maskWidth * maskWidth);
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float x = 0; x < maskWidth; x++)
    {
        float offsetX = x + maskOffset;

        for (float y = 0; y < maskWidth; y++)
        {
            float offsetY = y + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            color += (tex2D(_MainTex, newUV) * homogeneousBlurFilter);
        }
    }

    color = color * i.diffuse + i.specular;
    return color;
}

위의 코드는 maskWidth == 5이므로 5x5 크기의 1.0 / 25 평균값을 지정한 것과 같습니다.

${
\begin{split}K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix}\end{split}
}$


이것을 적용해 보면, 지구본의 이미지 texture가 너무 커서(2048 * 1024) 별로 차이가 안 납니다.

[그림 1: 왼쪽은 원본, 오른쪽은 5x5 box filter가 적용된 셰이딩]
blur_effect_shader_1.png

변수를 조정해서 25로 늘리면,

maskWidth = 25;

즉, 25x25 크기의 1.0 / 625 필터를 적용하면 제법 눈에 띄는 blurring 효과가 나오는 것을 확인할 수 있습니다.

[그림 1: 왼쪽은 원본, 오른쪽은 25x25 box filter가 적용된 셰이딩]
blur_effect_shader_2.png

그나저나, 실습을 위해 texture 크기를 2048 * 1024 크기에서 512 * 256으로 해상도를 낮춘 지구본 이미지를 사용하겠습니다. 다음은 512 * 256이었을 때 고로 셰이딩(왼쪽)과 5x5 평균값 필터를 적용했을 때의 결과를 보여줍니다.

blur_effect_shader_3.png

위의 코드를 기반으로 하면, 가중 평균 값 필터(weighted mean filter)도 어렵지 않게 적용할 수 있습니다.




두 번째로, blurring 효과 하면 유명한 가우스 필터를 안 해볼 수 없습니다. ^^ 우선, 멋있게 1차원과 2차원 가우스 분포 함수를 써 두지만,

${ G(x) = \frac{1}{{\sigma \sqrt {2\pi } }}e^{{{ -{x}^2 } \mathord{\left/ \right. } {2\sigma ^2 }}}
}$


${ G(x, y) = \frac{1}{{\sigma ^2 {2\pi } }}e^{{{ - \left( x^2 + y^2 \right) } \mathord{\left/ \right. } {2\sigma ^2 }}}
}$


tex2D 함수는 2차원 texture를 대상으로 하기 때문에 2차원 가우스 분포 함수가 적용됩니다. 따라서, σ 값에 따라 적절한 gaussian kernel을 생성하는데,

Gaussian Kernel Calculator
; http://dev.theomader.com/gaussian-kernel-calculator/

일례로 σ = 1.0으로 5x5 크기의 커널을 사용해 다음과 같이 코딩을 할 수 있습니다.

fixed4 frag(v2f i) : SV_Target
{
    // sigma = 1.0
    float gaussian5x5BlurFilter[25] =
    {
        0.003765, 0.015019, 0.023792, 0.015019, 0.003765,
        0.015019, 0.059912, 0.094907, 0.059912, 0.015019,
        0.023792, 0.094907, 0.150342, 0.094907, 0.023792,
        0.015019, 0.059912, 0.094907, 0.059912, 0.015019,
        0.003765, 0.015019, 0.023792, 0.015019, 0.003765,
    };

    float maskWidth = 5;
    float maskOffset = -floor(maskWidth / 2.0);
            
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float y = 0; y < maskWidth; y++)
    {
        float offsetY = y + maskOffset;

        for (float x = 0; x < maskWidth; x++)
        {
            float offsetX = x + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            fixed filterPos = y * 5 + x;

            float filter = gaussian5x5BlurFilter[filterPos];
            color += (tex2D(_MainTex, newUV) * filter);
        }
    }

    color = color * i.diffuse + i.specular;
    return color;
}

아래의 그림은 좌측부터, 원본, 5x5 평균 값 필터링, 5x5 가우스 필터링 효과를 보여줍니다. (3개의 이미지를 함께 출력하느라 이미지가 이지러지는 것을 볼 수 있는데 그 부분은 무시하시면 됩니다.)

blur_effect_shader_4.png

참고로, sigma = 25.0으로 9x9의 mask를 적용하면,

// sigma = 25.0
float gaussian9x9BlurFilter[81] =
{
    0.012162, 0.012231, 0.01228 , 0.012309, 0.012319, 0.012309, 0.01228 , 0.012231, 0.012162,
    0.012231, 0.012299, 0.012349, 0.012378, 0.012388, 0.012378, 0.012349, 0.012299, 0.012231,
    0.01228 , 0.012349, 0.012398, 0.012428, 0.012438, 0.012428, 0.012398, 0.012349, 0.01228 ,
    0.012309, 0.012378, 0.012428, 0.012458, 0.012468, 0.012458, 0.012428, 0.012378, 0.012309,
    0.012319, 0.012388, 0.012438, 0.012468, 0.012478, 0.012468, 0.012438, 0.012388, 0.012319,
    0.012309, 0.012378, 0.012428, 0.012458, 0.012468, 0.012458, 0.012428, 0.012378, 0.012309,
    0.01228 , 0.012349, 0.012398, 0.012428, 0.012438, 0.012428, 0.012398, 0.012349, 0.01228 ,
    0.012231, 0.012299, 0.012349, 0.012378, 0.012388, 0.012378, 0.012349, 0.012299, 0.012231,
    0.012162, 0.012231, 0.01228 , 0.012309, 0.012319, 0.012309, 0.01228 , 0.012231, 0.012162,
};

float maskWidth = 9;

다음과 같이 좀 더 blurring된 것을 볼 수 있습니다.

[좌: 원본, 중간: 평균값, 우: σ = 25.0, 9x9 gaussian filter]
blur_effect_shader_5.png

그런데 2차원 가우시안 함수는 x와 y 방향에 대해 각각 1차원의 가우시안 함수의 곱으로 나타낼 수 있으므로 NxN 마스크가 아닌, 1xN 마스크를 x축과 (2 pass를 이용해) y 축 방향으로 사용할 수 있습니다.(단일 객체로 2 pass를 지정해 blurring 처리하는 것이 가능할까요?)




마지막으로, 중간값 필터(Median Filter)를 적용해 보겠습니다. 중간값이 의미하는 것처럼, mask에 속하는 영역 중 중간값에 해당하는 컬러를 선택하는 것이기 때문에 정렬이 필요한 기능이기도 합니다. 중간값을 반환하기 위한 정렬은 간단하게 다음과 같이 해결할 수 있습니다.

float4 getMedian(float4 buf[25], fixed len)
{
    float4 temp;

    // 버블 정렬
    for (float i = 0; i < len - 1; i ++)
    {
        for (float j = i + 1 ; j < len; j ++)
        {
            if (colorToFloat(buf[i]) < colorToFloat(buf[j]))
            {
                temp = buf[i];
                buf[i] = buf[j];
                buf[j] = temp;
            }
        }
    }

    return buf[len / 2]; // 중간값 반환
}

아쉬운 것은 함수의 인자로 배열을 전달할 때 반드시 크기를 명시해야 한다는 점입니다.

// 반드시 배열 크기 명시
float4 getMedian(float4 buf[25], fixed len)

// 아래와 같이 전달하면 (컴파일 오류 없이) 동작하지 않음
float4 getMedian(float4 buf[], fixed len)

그래서 getMedian과 같이 함수를 만들면 배열 크기에 따른 함수를 별도로 만들어줘야 합니다. 이하 나머지 코드는 평균값 필터와 크게 다르지 않습니다.

fixed4 frag(v2f i) : SV_Target
{
    float maskWidth = 5;
    float4 colorArray[5 * 5];

    float maskOffset = -floor(maskWidth / 2.0);
            
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float x = 0; x < maskWidth; x++)
    {
        float offsetX = x + maskOffset;

        for (float y = 0; y < maskWidth; y++)
        {
            float offsetY = y + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            colorArray[y * maskWidth + x] = tex2D(_MainTex, newUV);
        }
    }

    float4 col = getMedian(colorArray, maskWidth * maskWidth);
    color = col * i.diffuse + i.specular;

    return color;
}

실행 결과는 다음과 같습니다.

[좌측부터 원본, 5x5 평균값 필터, σ = 25.0 and 9x9 가우스 필터, 5x5 중간값 필터]
blur_effect_shader_6.png

참고로, 아래의 소스 코드를 보면 min, max를 활용해 (정렬 없이) 3x3 크기의 중간값 필터를 구현하고 있습니다.

3x3 Median - Morgan McGuire and Kyle Whitson
; http://casual-effects.com/research/McGuire2008Median/median.pix

(첨부 파일은 이 글에서 구현한 3가지 shader 소스 코드를 모두 포함합니다.)




shader에서 컬러를 비교하려고 다음과 같이 코딩을 하면,

fixed4 frag(v2f i) : SV_TARGET
{
    float4 color1 = tex2D(_MainTex, uv1);
    float4 color2 = tex2D(_MainTex, uv2);

    if (color1 < color2)
    {
    }
}

컴파일 오류가 발생합니다.

if statement conditional expressions must evaluate to a scalar

사실 단색이 아닌 color 값 비교라는 게 좀 이상하긴 하지만, 다음과 같이 우회해 볼 수는 있습니다.

float colorToFloat(float4 color)
{
    // tex2D가 반환하는 컬러는 0 ~ 1 사이의 값임.
    return color.r + color.g + color.b;
}

fixed4 frag(v2f i) : SV_TARGET
{
    float4 color1 = tex2D(_MainTex, uv1);
    float4 color2 = tex2D(_MainTex, uv2);

    if (colorToFloat(color1) < colorToFloat(color2))
    {
    }
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/15/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-04-18 08시43분
Very Fast Image Blur in C# using Direct Bit Manipulation in Pure .NET
; https://www.codeproject.com/Tips/5359189/Very-Fast-Image-Blur-in-Csharp-using-Direct-Bit

당연히 shader보다는 느립니다.
정성태

... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13147정성태10/25/20224761오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225594.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
13145정성태10/21/20225860오류 유형: 822. db2 - Password validation for user db2inst1 failed with rc = -2146500508
13144정성태10/20/20225715.NET Framework: 2059. ClrMD를 이용해 윈도우 환경의 메모리 덤프로부터 닷넷 모듈을 추출하는 방법파일 다운로드1
13143정성태10/19/20226227오류 유형: 821. windbg/sos - Error code - 0x000021BE
13142정성태10/18/20224958도서: 시작하세요! C# 12 프로그래밍
13141정성태10/17/20226711.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기파일 다운로드1
13140정성태10/11/20226092C/C++: 159. C/C++ - 리눅스 환경에서 u16string 문자열을 출력하는 방법 [2]
13139정성태10/9/20225925.NET Framework: 2057. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 모든 닷넷 모듈을 추출하는 방법파일 다운로드1
13138정성태10/8/20227213.NET Framework: 2056. C# - await 비동기 호출을 기대한 메서드가 동기로 호출되었을 때의 부작용 [1]
13137정성태10/8/20225601.NET Framework: 2055. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 닷넷 모듈을 추출하는 방법
13136정성태10/7/20226175.NET Framework: 2054. .NET Core/5+ SDK 설치 없이 dotnet-dump 사용하는 방법
13135정성태10/5/20226405.NET Framework: 2053. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프를 분석하는 방법 - 두 번째 이야기
13134정성태10/4/20225135오류 유형: 820. There is a problem with AMD Radeon RX 5600 XT device. For more information, search for 'graphics device driver error code 31'
13133정성태10/4/20225454Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법 [1]
13132정성태10/3/20225327스크립트: 42. 파이썬 - latexify-py 패키지 소개 - 함수를 mathjax 식으로 표현
13131정성태10/3/20227988.NET Framework: 2052. C# - Windows Forms의 데이터 바인딩 지원(DataBinding, DataSource) [2]파일 다운로드1
13130정성태9/28/20225094.NET Framework: 2051. .NET Core/5+ - 에러 로깅을 위한 Middleware가 동작하지 않는 경우파일 다운로드1
13129정성태9/27/20225391.NET Framework: 2050. .NET Core를 IIS에서 호스팅하는 경우 .NET Framework CLR이 함께 로드되는 환경
13128정성태9/23/20227966C/C++: 158. Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import파일 다운로드1
13127정성태9/22/20226417Windows: 210. WSL에 systemd 도입
13126정성태9/15/20227030.NET Framework: 2049. C# 11 - 정적 메서드에 대한 delegate 처리 시 cache 적용
13125정성태9/14/20227236.NET Framework: 2048. C# 11 - 구조체 필드의 자동 초기화(auto-default structs)
13124정성태9/13/20226978.NET Framework: 2047. Golang, Python, C#에서의 CRC32 사용
13123정성태9/8/20227415.NET Framework: 2046. C# 11 - 멤버(속성/필드)에 지정할 수 있는 required 예약어 추가
13122정성태8/26/20227416.NET Framework: 2045. C# 11 - 메서드 매개 변수에 대한 nameof 지원
... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...