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보다는 느립니다.
정성태

... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1756정성태9/23/201427489기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434281오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424669VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420612오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438957.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423834.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423733.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425386개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428502오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426106.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423039개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431065.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426976개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421334.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432525.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426564.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422054.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419765VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425581VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418238.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419899오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426363.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434481Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427087개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...