Hole Filling Filtering

Seunghui Han·2024년 10월 27일

Unity Shader

목록 보기
1/4
post-thumbnail

Hole filling filtering 이란?

이미지나 3D 데이터(특히 깊이 데이터)에서 발생하는 결손 부분 또는 비어 있는 영역을 주변의 값을 이용해 보완하는 필터링 기법이다.

흔히, 센서 데이터에서 특정 픽셀이 비거나 노이즈가 발생할 때, 해당 영역을 다른 픽셀의 값으로 보완하여 시각적 일관성을 유지하거나 분석의 정확성을 높이기 위해 사용된다.

어떻게 적용?

이 필터는 다음과 같이 사용된다.
1. 깊이 데이터에서 검은색에 해당하는 0값을 찾는다.
2. 0값을 특정 깊이 값으로 대체하여 검은색 부분을 채운다.
3. 필요에 따라 깊이 값을 재조정하여 시각적으로 더 일관된 표현이 가능하도록 한다.

즉, 결론은
깊이 데이터를 받은 값 중 검은색 부분은 일반적으로 0값(깊이 정보가 없는 값)으로 표현이 된다.
그렇기 때문에 해당 값을 다른 값으로 대체하면 검은색 부분을 채울 수 있다.

대체하는 방식도 여러가지가 있다.
1. 왼쪽 값으로 대체하는 방식
2. 주변 깊이값 중 작은값으로 대체하는 방식
3. 주변 깊이값 중 큰 값으로 대체하는 방식

나는 1번 방식을 택했고, 유니티의 셰이더 코드를 수정하여 적용하는 방식을 사용했다.

적용한 결과

다음과 같은 순서로 코드를 작성했다.

  1. 현재와 왼쪽 픽셀의 깊이 값을 가져온다.
  2. 깊이 값을 정규화 한다.

참고

정규화는 깊이 값이 특정 범위 안에서 일정하게 분포되도록 조정하는 과정이다.
정규화 과정이 필요한 이유는 컬러맵과의 매핑때문이다.
정규화를 하지 않으면 색상 샘플링이 잘못되어 의도한 시각적 표현이 어려워질 수도 있다.

첫번째 시도

half4 frag(v2f_img pix) : SV_Target
{
    // 현재 픽셀의 깊이 값을 가져옴
    float z = tex2D(_MainTex, pix.uv).r * 0xffff * _DepthScale;

    // 왼쪽 픽셀의 깊이 값을 가져옴
    float leftZ = tex2D(_MainTex, pix.uv + float2(-_Colormaps_TexelSize.x, 0)).r * 0xffff * _DepthScale;

    // 깊이 값을 MinRange와 MaxRange 사이로 정규화
    z = (z - _MinRange) / (_MaxRange - _MinRange);
    leftZ = (leftZ - _MinRange) / (_MaxRange - _MinRange);

    // 유효하지 않은 깊이 값(0 이하 또는 1 이상)은 왼쪽 값으로 대체
    if (z <= 0)
    {
        z = leftZ;
    }

    // 깊이 값을 MinRange와 MaxRange 사이로 정규화
    z = (z - _MinRange) / (_MaxRange - _MinRange);

    // 컬러맵 텍스처에서 깊이 값에 해당하는 색상 샘플링
    float colormapIndex = 1 - (_Colormap + 0.5) * _Colormaps_TexelSize.y;
    half4 color = tex2D(_Colormaps, float2(z, colormapIndex));

    return color;
}

위와 같은 코드로 실행할 경우
다음과 같은 결과가 나온다는 것을 볼 수 있다.
(왼쪽 : 원본 깊이값 | 오른쪽 : hole fillig 적용한 값)

검은 색 부분이 다른 색으로 변하긴 했지만, 원하는 결과 값이 아니다.
그래서 다음과 같은 방법을 시도했다.

두번째 시도

half4 frag(v2f_img pix) : SV_Target
{
    // 현재 깊이 텍스처의 값을 가져옴
    float zRaw = tex2D(_MainTex, pix.uv).r;
    float z = zRaw * 0xffff * _DepthScale;

    // 왼쪽 깊이 텍스처의 값을 가져옴
    float leftZRaw = tex2D(_MainTex, pix.uv + float2(-_Colormaps_TexelSize.x, 0)).r;
    float leftZ = leftZRaw * 0xffff * _DepthScale;

    // 깊이 값을 MinRange와 MaxRange 사이로 정규화
    z = (z - _MinRange) / (_MaxRange - _MinRange);
    leftZ = (leftZ - _MinRange) / (_MaxRange - _MinRange);

    if (leftZ <= 0)
    {
        // 샘플링 거리 증가 변수
        float offset = _Colormaps_TexelSize.x;

        // 최대 1024번까지 샘플링 시도
        for (int i = 0; i < 1023; i++)
        {
            leftZRaw = tex2D(_MainTex, pix.uv + float2(-offset, 0)).r;
            leftZ = leftZRaw * 0xffff * _DepthScale;
            
            // 정규화
            leftZ = (leftZ - _MinRange) / (_MaxRange - _MinRange);

            // 유효한 값이 나올 때까지 샘플링 위치를 증가시킴
            if (leftZ > 0)
            {
                break;
            }

            // 샘플링 위치를 더 멀리 이동
            offset += _Colormaps_TexelSize.x;
        }

        // 여전히 유효하지 않은 경우 특정 색상 반환 (디버그용)
        if (leftZ <= 0)
        {
            return half4(1, 0, 0, 1); // 빨간색으로 표시
        }
    }

    // 유효하지 않은 깊이 값일 경우 (디버그용으로 특정 색상으로 표시)
    if (z <= 0)
    {
        z = leftZ;
    }

    // 컬러맵 텍스처에서 깊이 값에 해당하는 색상 샘플링
    float colormapIndex = 1 - (_Colormap + 0.5) * _Colormaps_TexelSize.y;
    half4 color = tex2D(_Colormaps, float2(z, colormapIndex));

    return color;
}

이번 코드는 offset이라는 거리 변수를 이용하였다.
만약 왼쪽 픽셀 값이 유효하지 않으면 계속 증가하여 그것을 최대 1024번까지 시도하는 것이다.
원래는 1024보다 낮게 하여 샘플링 시도를 하는 횟수를 최대한 줄여보려고 했다.
(하지만 1024보다 낮게 하면 hole filling 이 제대로 적용이 안된다는..)

다음 결과물을 보면 샘플링 시도를 높여서 hole filling을 적용한 것을 볼 수 있다.
(위 : 원본 깊이 데이터 | 아래 : 샘플링 시도 횟수 증가한 결과)

이 외에도 주변 픽셀 값들 중 깊이 값이 작은 값, 먼 값으로 대체하는 방식도 사용했다.
(하지만.. 왼쪽 값을 대체하는 방식을 사용했을 때의 결과와 비슷하므로 생략하겠..)

소감

이번에 유니티 셰이더 코드 처음으로 작성해봤는데 실시간으로 필터링이 어떻게 적용되는지 볼 수 있음에 또 다른 재미를 느꼈다.
또, 학교에서 컴퓨터비젼및실습 과목을 수강했었는데 이번 hole filling filtering에 대해 알아보면서 그때의 기억을 상기시킬 수 있었던 것 같다.

0개의 댓글