본문 바로가기
프로그래밍/OpenCV

[OpenCV] 컬러 이미지 처리

by 별준 2022. 5. 9.

References

Contents

  • 컬러 이미지의 픽셀 값 참조
  • 색상 공간 변환
  • 색상 채널 나누기

이전까지 포스팅에서는 대부분 그레이스케일 대상의 영상 처리 기법에 대해 살펴봤습니다. 이번 포스팅에서는 컬러 이미지를 처리하는 방법과 다양한 컬러 공간에 대해 살펴보도록 하겠습니다.

 


컬러 이미지 픽셀 값 참조

OpenCV에서 영상을 불러와 Mat 객체를 생성할 때는 imread() 함수를 사용하는데, 이때 함수의 두 번째 파라미터에 IMREAD_COLOR를 전달하면 영상을 3채널 컬러 영상 타입으로 불러옵니다. 예를 들면 다음과 같습니다.

cv::Mat img = cv::imread("butterfly.jpg", cv::IMREAD_COLOR);

일반적으로 컬러 영상은 RGB 색상 성분의 조합으로 픽셀 값을 표현하는데, OpenCV에서는 RGB 순서가 아니라 BGR 순서로 픽셀 값을 표현합니다. 즉, imread() 함수로 이미지를 3채널 영상 형식으로 불러오면 각 픽셀의 색상 값이 파란색(B), 초록색(G), 빨간색(R) 순서로 저장된 Mat 객체가 생성됩니다.

 

컬러 영상에서 각 R, G, B 성분은 0부터 255 사이의 값을 가지며, 값이 0이라면 해당 색상 성분이 전혀 없다는 것을 의미하고 255면 해당 색상 성분이 가득 차 있다는 것을 의미합니다. OpenCV에서는 uchar 타입을 사용하여 이 값을 표현합니다. 그리고 컬러 이미지에서 하나의 픽셀은 3개의 색상 성분을 가지고 있으므로 컬러 이미지의 한 픽셀을 정확하게 표현하려면 Vec3b 타입을 사용해야 합니다. Vec3b 클래스는 크기가 3인 uchar 타입의 배열을 멤버 변수로 가지고 있는 클래스입니다. Vec3b 클래스의 바이트 크기는 정확히 3바이트이며, 이는 실제 3채널 컬러 이미지의 한 픽셀이 차지하는 바이트와 같습니다.

 

픽셀 값을 참조할 때에는 Mat::at() 함수를 사용합니다. 이 함수는 템플릿으로 정의된 함수이므로 3채널 컬러 이미지에 대해 이 함수를 사용하려면 Vec3b 타입을 명시해야 하며, 위에서 'butterfly.jpg' 파일을 읽어서 저장하고 있는 img의 (0, 0) 위치의 픽셀 값을 참조하려면 다음과 같이 작성하면 됩니다.

cv::Vec3b& pixel = img.at<Vec3b>(0, 0);

위에서 선언한 pixel 변수는 img 변수에 저장된 이미지의 (0, 0) 좌표에서 BGR 색상 정보를 가리킵니다. Mat::at() 함수는 레퍼런스로 Vec3b 타입을 반환하기 때문에 pixel 값을 변경하면 img의 (0, 0) 좌표 픽셀 값도 같이 변경됩니다. Vec3b 클래스는 [] 연산자 오버로딩을 통해 각 멤버 변수에 접근할 수 있으며, pixel에 저장된 BGR 성분 값을 알고 있다면 다음과 같이 작성하면 됩니다.

uchar b1 = pixel[0];
uchar g1 = pixel[1];
uchar r1 = pixel[2];

 

Mat::ptr() 함수를 이용하여 컬러 이미지의 특정 행 시작 주소를 얻어 올 때에도 Vec3b 타입을 명시하여 사용해야 합니다. 예를 들어, img에서 0번째 행 시작 픽셀 주소를 알고 싶다면 다음과 같이 작성합니다.

cv::Vec3b* ptr = img.ptr<Vec3b>(0);

ptr을 이용하여 (0, 0) 좌표 픽셀을 참조하려면 ptr[0]을 사용하면 되며, 이는 Vec3b 타입에 해당합니다. 따라서, ptr[0] 코드 뒤에 다시 [] 연산자를 사용하여 해당 픽셀의 BGR 성분 값을 얻을 수 있습니다.

uchar b2 = ptr[0][0];
uchar g2 = ptr[0][1];
uchar r2 = ptr[0][2];

 

아래 예제 코드는 픽셀 값을 참조하는 방법을 이용하여 컬러 이미지를 반전시켜 화면에 출력합니다. 아래 코드에서는 입력 영상의 모든 픽셀 값을 반전시켜 화면에 출력하며, 각 색상 성분 값을 각각 255 빼는 연산을 수행합니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::Mat src = cv::imread("butterfly.jpg", cv::IMREAD_COLOR);

    if (src.empty()) {
        std::cout << "Image load failed!\n";
        return -1;
    }

    cv::Mat dst(src.rows, src.cols, src.type());

    for (int r = 0; r < src.rows; r++) {
        for (int c = 0; c < src.cols; c++) {
            cv::Vec3b& p1 = src.at<cv::Vec3b>(r, c);
            cv::Vec3b& p2 = dst.at<cv::Vec3b>(r, c);

            p2[0] = 255 - p1[0];
            p2[1] = 255 - p1[1];
            p2[2] = 255 - p1[2];
        }
    }

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    cv::waitKey();
    cv::destroyAllWindows();
}

위의 코드는 영상의 픽셀 값을 반전시키기 위해 for문을 사용하고 있습니다. 이 코드에서는 가독성을 위해 src와 dst의 (r, c) 위치의 픽셀 값을 각각 p1과 p2으로 참조하여 반전시켰지만, 다음과 같이 간략하게 작성할 수도 있습니다.

for (int r = 0; r < src.rows; r++) {
    for (int c = 0; c < src.cols; c++) {
        dst.at<cv::Vec3b>(r, c) = cv::Vec3b(255, 255, 255) - src.at<cv::Vec3b>(r, c);
    }
}

사실 위 코드들은 픽셀 참조 방법을 설명하기 위해 for문으로 작성되었지만, 다음과 같이 - 연산자 오버로딩을 사용하는 것이 더 효율적입니다.

cv::Mat dst = cv::Scalar(255, 255, 255) - src;

 


색 공간 변환

컴퓨터에서는 색을 표현할 때 빛의 삼원색이라고 부르는 빨간색(R), 초록색(G), 파란색(B)의 조합으로 표현합니다. OpenCV에서는 컬러 영상을 Mat 객체에 저장할 때 파란색, 녹색, 빨간색 순서(BGR)로 색을 표현하기 때문에 정확하게 표현하면 BGR로 표현해야 하지만, 빛의 삼원색 조합으로 색을 표현한다는 것은 동일합니다. 이렇게 색을 표현하는 방식을 BGR 색 모델(color model) 또는 RGB 색 공간(color space)라고 합니다. 우리가 사용하는 모니터 역시 이렇게 세 가지 색상 성분을 조합하여 색을 표현하며, 디지털 카메라에서 컬러 영상을 획득할 때도 마찬가지 입니다. 이처럼 RGB 색상 공간은 일상에서 널리 사용되지만, 영상 처리 관점에서는 그리 환영받지는 못합니다. 컬러 영상 처리에서는 보통 색상 구분이 용이한 HSV, HSL 색상 공간을 사용하거나, 휘도 성분이 구분되어 있는 YCrCb, YUV 등의 색상 공간을 사용하는 것이 유리합니다. 따라서 OpenCV에서는 BGR 순서로 색상이 저장된 컬러 이미지의 색상 공간을 HSV, YCrCb 등 다른 색상 공간으로 변환하는 인터페이스를 제공합니다.

 

OpenCV에서 색상 공간을 변환할 때는 cvtColor() 함수를 사용합니다.

이 함수는 입력 src의 색상 공간을 변환하여 dst를 생성합니다. 어떻게 변환할 지는 code를 통해서 지정하는데, 여기에는 ColorConversionCodes 열거형 상수를 지정할 수 있습니다.

많은 코드들이 있으므로, 자세한 사항은 공식 문서를 참조바랍니다.

 

아래에서는 그중에서 자주 사용되는 색상 공간 변환에 대해 살펴보겠습니다.

 

BGR2GRAY and GRAY2BGR

BGR2GRAY 코드는 BGR 컬러 영상을 그레이스케일 영상으로 변환할 때 사용합니다. 컬러 영상을 그레이스케일로 변환하는 주된 이유는 연산 속도와 메모리 사용량을 줄이기 위해서 입니다. 기본적으로 컬러 영상을 그레이스케일에 비해 3배 많은 메모리를 필요로 하고, 또한 세 개의 채널에 대한 연산을 수행해야 하므로 더 많은 연산이 필요합니다. 따라서 입력 영상에서 색상 정보의 활용도가 크기 않는 경우에는 입력 영상을 그레이스케일로 변환하여 처리하는 것이 효율적입니다.

 

BGR 3채널 컬러를 그레이스케일로 변환할 때는 다음의 공식을 사용합니다.

\[Y = 0.299R + 0.587G + 0.114B\]

위 공식에서 RGB는 각각 픽셀의 빨간색, 초록색, 파란색 성분의 값을 나타내며, Y는 해당 픽셀의 그레이스케일 성분 크기를 나타냅니다. BGR2GRAY 코드에 의해 만들어지는 결과 영상은 CV_8UC1 타입으로 지정됩니다.

 

반대로 GRAY2BGR 코드는 그레이스케일 영상을 BGR 컬러 영상으로 변환할 때 사용됩니다. 이 경우 결과 영상은 CV_8UC3 타입으로 지정되고, 각 픽셀의 BGR 성분 값을 다음과 같습니다.

\[B = G = R = Y\]

 

BGR2HSV and HSV2BGR

HSV 색상 모델은 색상(hue), 채도(saturation), 명도(value)로 색을 표현하는 방식입니다. 색상은 빨간색, 노란색, 녹색과 같은 색의 종류를 의미합니다. 채도는 색의 순도를 나타내며, 예를 들어, 빨간색에 대하여 채도가 높으면 맑은 선홍색이고 채도가 낮으면 탁한 빨간색으로 보이게 됩니다. 명도는 빛의 세기를 나타내며, 명도가 높으면 밝고 명도가 낮으면 어둡게 느껴집니다.

출처 : https://ko.wikipedia.org/wiki/HSV_%EC%83%89_%EA%B3%B5%EA%B0%84

HSV 색상 공간은 위와 같이 원뿔 모양으로 표현할 수 있습니다. 이 색상 공간 모형에서 색상은 원뿔을 가로로 잘랐을 때 나타내는 원형의 각도로 정의됩니다. 각도가 0도에 해당할 때 빨간색을 나타내고, 각도가 증가할수록 노란색, 녹색, 하늘색, 파란색, 보라색을 거쳐 360도에 가까워지면 다시 빨간색으로 표현됩니다. 채도는 원뿔을 가로로 잘랐을 때 나타나는 원 모양의 중심에서 최솟값을 갖고 원의 중심에서 방사형으로 멀어지는 방향으로 값이 증가합니다. 명도는 원뿔 아래쪽 꼭지점에서 최솟값을 갖고 원뿔의 축을 따라 올라가면서 증가합니다.

 

OpenCV에서 BGR2HSV 코드를 이용하여 8비트 BGR 영상을 HSV 영상으로 변환할 경우, H 값은 0부터 179 사이의 정수로 표현되고, S와 V는 0부터 255 사이의 정수로 표현됩니다. 색상 값은 보통 0도에서 360도 사이의 각도로 표현하지만 uchar 타입에서 256 이상의 정수를 표현할 수 없기 때문에 OpenCV에서는 각도를 2로 나눈 값을 H 성분으로 저장합니다. 만약 cvtColor() 함수의 입력 BGR 영상이 0에서 1사이 값으로 정규화된 CV_32FC3 타입의 행렬이라면, H 값은 0에서 360사이의 실수로 표현되고 S와 V는 0에서 1사이의 실수값으로 표현됩니다.

 

BGR2YCrCb and YCrCb2BGR

YCrCb 색상 공간에서 Y 성분은 밝기 또는 휘도(luminance) 정보를 나타내고, Cr과 Cb는 색상 또는 색차(chrominance) 정보를 나타냅니다. RGB 색상 성분으로부터 Y 성분을 계산하는 공식은 그레이스케일 계산 공식과 완전히 같습니다. Cr과 Cb 성분은 밝기에 대한 정보는 포함하지 않으며 오직 색상에 대한 정보만을 가지고 있습니다. 따라서 YCrCb 색상 공간은 영상을 그레이스케일 정보와 색상 정보로 분리하여 처리할 때 유용합니다.

 

OpenCV에서 RGB2YCrCb 코드를 이용하여 8비트 BGR 영상을 YCrCb 영상으로 변환할 경우, Y, Cr, Cb 각각의 성분 값은 0부터 255사이의 값으로 표현됩니다. 만약 입력 영상이 0에서 1사이의 값으로 정규화된 CV_32FC3 타입의 행렬이라면 Y, Cr, Cb 각각의 성분 값도 0에서 1사이의 실수로 표현됩니다.

위 이미지는 Y 성분의 값을 128로 고정한 상태에서 Cr과 Cb 값에 따른 색상 표현을 보여줍니다. HSV 색상 공간에서는 H 값만을 이용하여 색 종류를 구분할 수 있었지만, YCrCb 색상 공간에서는 Cr과 Cb를 함께 조합하여 색을 구분합니다.

 

 

 

cvtColor() 함수를 이용하여 BGR 영상을 그레이스케일로 변경하는 코드는 다음과 같습니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::Mat src = cv::imread("butterfly.jpg", cv::IMREAD_COLOR);

    if (src.empty()) {
        std::cout << "Image load failed!\n";
        return -1;
    }

    cv::Mat dst;
    cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);

    cv::imshow("src", src);
    cv::imshow("dst", dst);

    cv::waitKey();
    cv::destroyAllWindows();
}

 


색상 채널 나누기

imread() 함수로부터 생성된 컬러 영상은 하나의 픽셀이 BGR 세 개의 색상 정보를 가지고 있습니다. 따라서 OpenCV에서 컬러 영상은 보통 uchar 타입을 사용하고 세 개의 채널을 갖는 Mat 객체로 표현합니다. 그런데 컬러 영상을 다루다보면 빨간색 성분만을 이용하거나 HSV 색상 공간으로 변환한 후 H 성분만을 이용하는 경우가 종종 있습니다. 이러한 경우에는 3채널 Mat 객체를 1차원 Mat 객체 세 개로 분리해서 다루는 것이 효율적입니다.

 

OpenCV에서 다채널 행렬을 1채널 행렬 여러 개로 변환할 때에는 split() 함수를 사용합니다.

void cv::split(const Mat& src, Mat* mvbegin);
void cv::split(InputArray src, OutputArrayOfArrays mv);

split() 함수는 두 종류가 있는데, 하나는 Mat 타입의 배열로 결과 영상을 받는 것이고, 다른 하나는 vector<Mat> 타입의 변수로 받는 것입니다. Mat 타입의 배열을 사용하는 경우, 배열의 크기가 입력 영상 채널의 수보다 같거나 커야 합니다.

 

split() 함수와 반대로 1채널 행렬 여러 개를 합쳐서 다채널 행렬 하나를 생성하려면 merge() 함수를 사용합니다. 이 함수도 두 종류가 있습니다.

void cv::merge(const Mat* mv, size_t count, OutputArray dst);
void cv::merge(InputArrayOfArrays mv, OutputArray dst);

 

split() 함수를 사용하여 BGR 컬러 영상으로부터 B, G, R 채널을 분리하여 화면에 출력하는 코드는 다음과 같습니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::Mat src = cv::imread("candies.png", cv::IMREAD_COLOR);

    if (src.empty()) {
        std::cout << "Image load failed!\n";
        return -1;
    }

    std::vector<cv::Mat> bgr_planes;
    cv::split(src, bgr_planes);

    cv::imshow("src", src);
    cv::imshow("B_plane", bgr_planes[0]);
    cv::imshow("G_plane", bgr_planes[1]);
    cv::imshow("R_plane", bgr_planes[2]);

    cv::waitKey();
    cv::destroyAllWindows();
}

실행 결과는 위와 같습니다. 원본 영상 src에서 파란색 초콜릿 영역은 파란색 성분 값이 크기 때문에 B_plane에서 밝은 흰색으로 표시됩니다. 반면에 노란색 캔디 영역은 빨간색과 녹색 성분 값이 크기 때문에 R_plane과 G_plane에서 밝게 표현됩니다.

'프로그래밍 > OpenCV' 카테고리의 다른 글

[OpenCV] 서포트 벡터 머신 (SVM)  (0) 2022.05.11
[OpenCV] 머신러닝과 KNearest 클래스  (0) 2022.05.10
[OpenCV] 에지 검출  (0) 2022.05.08
[OpenCV] 어파인 변환 / 투시 변환  (0) 2022.05.07
[OpenCV] 영상 필터링  (0) 2022.05.06

댓글