JINWOOJUNG

[ 영상 처리 ] Part2-1. OpenCV Mask Processing(C++) 본문

2024/Study

[ 영상 처리 ] Part2-1. OpenCV Mask Processing(C++)

Jinu_01 2024. 4. 7. 03:07
728x90
반응형

Before This Episode

Part1. 에서는 Image Processing 중 Point Processing에 대해서 공부하였다.Part2. 에서는 하나의 픽셀을 처리하는데, 인접 픽셀을 고려하는 Mask Processing에 대해서 공부할 것이다.

 

https://jinwoo-jung.tistory.com/58

 

[ 영상 처리 ] Part1-1. OpenCV Image Processing +

Before This Episode 지난 포스팅에서 가장 기본적인 OpenCV 기반의 Image Processing 과정을 공부하였다. 이미지 처리를 위한 cv::Mat 객체를 처음 접하는 과정에서 약간의 혼동이 있을 것 같아 Mat 객체의 픽

jinwoo-jung.com


 

Mask Convolution

  • Mask : Filter, Kernel 이라고도 불림. Convolution을 위한 Weight를 의미.
  • Convolution : 합성곱

Mask Convolution이란, 특정 픽셀이 Mask의 중심에 오도록 하여 Mask의 Weight에 따라서 인접 픽셀의 값을 반영하여 특정 픽셀의 값을 변경하는 과정이다. Weight에 따라서 인접 픽셀의 값을 반영하는 비율은 다르지만, Blur 효과를 일으켜 노이즈 제거를 위해 많이 사용된다.

 

인접 픽셀을 반영하기 때문에 가장자리 문제가 발생하는데, Mask의 크기가 3x3인 경우 0,0 픽셀을 처리할 순 없다. 따라서 이미지의 가장자리는 제외하고, 유효한 범위 내에서 Convolution 연산을 진행한다.

 

myKernelConv3x3()
int myKernelConv3x3(uchar* arr, int kernel[][3], int x, int y, int width, int height) 
{
    int sum = 0, sumKernel = 0, s32_J, s32_I;

    // 3x3 Kernel 
    for (s32_J = -1; s32_J <= 1; s32_J++) {
        for (s32_I = -1; s32_I <= 1; s32_I++) {
            // 해당 픽셀에서 커널을 입혔을 때 Image를 벗어나지 않는 유효성 판단
            if ((y + s32_J) >= 0 && (y + s32_J) < height && (x + s32_I) >= 0 && (x + s32_I) < width) {
                // Kernel의 Weight를 고려하여 원본 픽셀 Value에서 Weight를 고려한 합을 계산
                sum += arr[(y + s32_J) * width + (x + s32_I)] * kernel[s32_J + 1][s32_I + 1];
                // Kernel의 Weight의 합을 계산
                sumKernel += kernel[s32_J + 1][s32_I + 1];
            }
        }
    }
    // Kernel의 합이 0이 아니면 나눠주면서 평균값 계산
    if (sumKernel != 0) { return sum / sumKernel; }
    else { return sum; }
}

 

Mask Processing은 특정 픽셀이 Mask의 중심에 오도록 한다고 설명 하였다. 따라서 인자로 특정 픽셀의 좌표를 x,y로 받아오기 때문에 이중 for문의 index 범위가 -1~1인 것이다. 또한, 가장자리 문제를 방지하여 유효성을 판단하기 위해, 특정 픽셀의 커널 범위의 픽셀 좌표가 이미지를 벗어나지 않는지 확인한다.

 

유효한 범위 내에서 sum은 결국 Mask의 Weight와 픽셀의 Value의 곱을 전부 더하고, sumKernel은 Mask의 Weight의 합을 의미한다. 만약 sumKernel이 0이라면 즉, Mask의 Weight의 합이 0인 경우 나눠서 정규화를 할 수 없기 때문에 sum을 반환하며, 아닐 경우 sumKernel으로 나눠서 정규화 된 값을 반환한다.

 

int main()
{
    Mat src_img = imread("gear.jpg", 0);
    Mat st_ResultImage, result_img;
    int s32_X, s32_Y;
    
    int kernel[3][3] = { 1, 1, 1,
                         1, 1, 1,
                         1, 1, 1 };
    st_ResultImage.create(src_img.size(), CV_8UC1);

    for (s32_Y = 0; s32_Y < src_img.rows; s32_Y++) {
        for (s32_X = 0; s32_X < src_img.cols; s32_X++) {
            st_ResultImage.data[s32_Y * src_img.cols + s32_X] = myKernelConv3x3(src_img.data, kernel, s32_X, s32_Y, src_img.cols, src_img.rows);
        }
    }

    imshow("st_ResultImage", st_ResultImage);
    imshow("src_img", src_img);
    waitKey(0);
    destroyAllWindows();

	return 0;
}

 

반환되는 값이 결국 원본 이미지의 특정 픽셀의 값이 된다. 하나의 픽셀의 처리 과정에서 인접 픽셀의 값을 고려하기 때문에 Mask Processing이다. 이때, st_ResultImage.create()를 해준 이유는, 처음에 Mat 객체를 선언만 해주고 크기 등 초기화를 해 주지 않았기 때문이다. 아래의 코드는 동일한 기능을 하는 코드이다.

 

Mat st_ResultImage(src_img.size(), CV_8UC1);

Mat st_ResultImage;
st_ResultImage.create(src_img.size(), CV_8UC1);;

 

만약 Kernel의 모든 Weight가 1인 경우 동일한 기능을 하는 OpenCV 함수가 있는데, 이는 cv::blur()이다.

cv::Size kernelSize(3, 3);
cv::blur(src_img, st_ResultImage, kernelSize);

 


Gaussian Filter

Mask가 Gaussian Distribution 형태의 Mask Weight를 가진다.  즉, 현재 픽셀과 가까울수록 더 높은 Weight를 부여하며, Smoothing(Blur) 효과로 인한 Noise 제거에 효과적이다. Blur의 정도는 표준편차 $\sigma$에 의해 결정된다.

Gaussian Filtering에 대한 이론적인 공부는 다른 포스팅에서 더 구체적으로 할 계획이다.

 

코드 구현은 위에서 진행한 Mask Convolution에서 구현한 myKernelConv3x3을 이용한다.

 

myGaussianFilter()
Mat myGaussianFilter(Mat srcImg) {

    int width = srcImg.cols;
    int height = srcImg.rows;
    int Kernel[3][3] = {
       1,2,1,
       2,4,2,
       1,2,1
    };
    Mat dstImg(srcImg.size(), CV_8UC1);
    uchar* srcData = srcImg.data;
    uchar* dstData = dstImg.data;
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            dstData[y * width + x] = myKernelConv3x3(srcData, Kernel, x, y, width, height);
        }
    }
    return dstImg;
}

 

현재는 $\sigma$값에 의해 결정되지 않는다. 단순히 Gaussian Distribution에 기반하여 선형적인 Filtering만 진행된다.

 

Original Image -> Gaussian

 

이미지가 전반적으로 Blur 처리가 됬음을 확인할 수 있다. 

$\sigma$에 따른 Blur 정도는 추후 다루도록 하자.


Sobel Filter

Sobel Filter는 대표적인 Edge Detector 중 하나이다. Image에서 특정 방향에 대한 미분의 기능을 가지는 Filter로, 특정 방향에 대한 Edge 검출을 별도로 수행 가능하다.

 

이는 Mask의 Weight를 보면 더 쉽게 이해 가능하다.

 

 

 

왼쪽의 Sobel Mask의 경우 왼쪽에서 오른쪽으로 Pixel의 변화가 줄어드는(밝은 -> 어두운) Edge를 검출하고, 오른쪽의 Sobel Mask는 위에서 아래로 Pixel의 변화가 줄어드는(밝은 어두운) Edge를 검출하는 Mask이다.

 

Gradient 즉, 기울기는 미분값을 통해 알 수 있고, 이는 변화량이 큰 것을 의미한다. 실제로 왼쪽 Sobel Mask의 첫번째 행만 가지고 간단한 계산을 해 보면 아래와 같다.

 

 

즉 Sobel Mask와 유사한 상관관계를 지니는 Pixel Value 분포에서 더 큰 Response 즉, Mask Convolution 결과를 보임을 알 수 있다.  이는 더 큰 변화량을 보임을 의미하고, 결국 Gradient와 동일한 결과를 나타내기에 미분의 성격을 지님을 알 수 있다. 

 

따라서 앞서 언급한 두 Mask 중 왼쪽은 이미지에서 왼쪽에서 오른쪽으로 밝아지는 Edge를 찾는 Mask임을 쉽게 유추할 수 있다. 

 

mySobelFilter()
Mat mySobelFilter(Mat srcImg) {
        int kernelX[3][3] = {
           {-1, 0, 1},
           {-2, 0, 2},
           {-1, 0, 1} 
        };
        int kernelY[3][3] = {
           {-1, -2, -1},
           { 0,  0,  0},
           { 1,  2,  1} 
        };
        Mat dstImg(srcImg.size(), CV_8UC1);
        uchar* srcData = srcImg.data;
        uchar* dstData = dstImg.data;
        int width = srcImg.cols;
        int height = srcImg.rows;

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int gx = abs(myKernelConv3x3(srcData, kernelX, x, y, width, height));
                int gy = abs(myKernelConv3x3(srcData, kernelY, x, y, width, height));
                dstData[y * width + x] = (gx + gy) / 2; 
            }
        }
        return dstImg;
    #endif
}

 

동일하게 myKernelConv3x3()를 사용하지만, 이때 Mask가 달라짐을 확인할 수 있다.

 

현재 x 방향으로의 Sobel Mask의 결과 Mask 자체는 왼쪽에서 오른쪽으로 갈수록 밝아지는 이미지 분포에서 Response가 크지만, 어두워 지는 이미지 분포에서 역시 음의 방향으로 Response가 크다. 따라서 해당 결과에 절댓값(abs())를 씌워줌으로써 결국 변화량의 크기를 기반으로 Edge를 추출한다.

 

현재 y 방향으로의 Sobel Mask 역시 동시에 수행하였기 때문에, 해당 결과를 평균내어 픽셀의 값으로 설정함을 확인할 수 있다. 만약 한 방향으로의 Edge만 추출하고 싶다면 gx, gy 중 하나의 값만 활용하면 된다.

 

Original Image -> Sobel X Filtering

 

Original Image -> Sobel Y Filtering

 

각 방향으로의 Filtering 결과 Sobel Filtering의 동작 과정을 더 잘 이해할 수 있다.

또한, 각 방향에서 추출하는 Edge가 다르기 때문에 두 방향 모두 진행 후 평균을 계산하여 모든 Edge를 추출한다.

 

이와 동일하게 동작을 하는 OpenCV 함수는 cv::Soble()이 있다.

Mat mySobelFilter(Mat srcImg) 
{
    Mat dstImg(srcImg.size(), CV_8UC1);
    Mat sobelX, sobelY;
    Sobel(srcImg, sobelX, CV_8UC1, 1, 0);
    Sobel(srcImg, sobelY, CV_8UC1, 0, 1);
    dstImg = abs(sobelX) + abs(sobelY);
    dstImg = dstImg / 2;
    return dstImg;
}

 

728x90
반응형