[OpenCV] RGBA와 이미지 블렌딩(Image Blending)

RGB에서 Alpha 채널이 포함된 RGBA 개념에 대해 알아보고, 두 이미지를 합쳐보자.

April 25, 2018 - 8 minute read -
opencv

RGB에서 A(Alpha)를 추가해서 색을 표현하는 RGBA 표현 방법이 있는데, 여기서 Alpha투명도(Transparent)를 추가한 것이다. RGBA 에 대해 더 자세히 알아보고, 두 이미지를 합치는(Blending 이라 한다) 실습을 진행할 것이다.

Previous Post

[OpenCV] 이미지 RGB 분리하기

RGBA

우리가 기본적으로 아는 RGBAlpha 채널을 추가한 것이다. Alpha는 이미지의 투명도(Transparent)를 나타낸 것이다. 즉, 우리가 일상 생활을 하면서 자동차 유리같이 투명한 물체를 표현하기 위해서 얼마나 투명한지를 나타내는 값이다. 알파 채널 값은 RGB 처럼 색을 표현하는 값이 아니라 컴퓨터에서 렌더링할 때 쓰이는 보조적인 값이다. 결과적으로는 배경 이미지와 값을 연산하여 RGB로 나타낸 후 출력하게 된다.

일반적으로 0.0-1.0 또는 0-255 값으로 구분되며, 0은 완전 투명(completely transparent), 그 반대인 1.0(또는 255 등 제일 높은 값)은 완전 불투명(fully opaque)한 것을 의미한다.

알파 혼합(Alpha Blending)

알파 혼합이란 투명도를 가진 2개 이상의 이미지를 혼합하는 것을 의미한다.

이미지 ab 위에 있고(a over b) 두 이미지를 합성했을 때 알파값과 RGB 값은 다음과 같다. (alpha 값은 0.0-1.0 사이이며, 0.0을 완전 투명, 1.0을 완전 불투명으로 가정한다.)

opencv result

opencv result

결합 법칙(associative rule)은 성립한다. 이미지가 a-b-c 순서대로 겹쳐있읊 때, 즉, 이미지가 a가 가장 위에 있고, 그 다음에 b, c가 밑에 있을 때, a-b를 계산한 후에 c를 계산하든지 b-c 를 계산한 후에 a 와 계산하든지 결과 값은 동일하다.

그러나 교환 법칙(commutative rule)은 성립하지 않는다. 간단하게 생각해보면 a완전 불투명한 종이라 하고, b셀로판지라고 한다면, a가 위에 있으면 a불투명하기 때문에 a만 보일 것이고, ba에 가려 보이지 않을 것이다. 그러나 반대로 b가 위에 있으면 b투명한 셀로판지기 때문에 a가 보일 것이다.

이 때, b 이미지, 즉 background 이미지가 완전 불투명(알파값 1.0) 일 때는 단순하게 저 식에서 대입해보면

opencv result

opencv result

이렇게 더 간단한 식이 된다! 알파값은 1이 되며, 이는 완전 불투명한 이미지를 의미한다.

OpenCV에서 사용하기

OpenCV 에서 알파값이 포함된 이미지를 로딩하기 위해서는 이미지를 로딩하는 함수인 cv::imread에서 flag 파라미터를 IMREAD_UNCHANGED 로 넘겨줘야 한다.

cv::Mat image = cv::imread("../src/target.png", cv::IMREAD_UNCHANGED);

단, 이미지 파일에서 alpha 값이 들어가있는 경우에만 alpha 값이 포함되어 나오며, 흑백 이미지의 경우 1 채널알파값이 없는 컬러 이미지의 경우 3 채널로 로딩이 될 수 있다. 이 때는 cv::cvtColor 로 alpha 값이 들어간 4 채널 Mat로 변경이 가능하다.

#include <opencv2/imgproc.hpp>
...
// BGR to BGRA image
cv::cvtColor(image, image, cv::COLOR_BGR2BGRA);
// gray image to BGRA iamge
cv::cvtColor(image, image, cv::COLOR_GRAY2BGRA);

이미지를 합성해보자. 다음은 이미지 합성 결과이다. 반투명 구(이미지 제작자 홈페이지 : 링크)와 반투명 하트 이미지를 빨간색 배경 안에 넣을 것이다.

opencv result

코드는 다음과 같다.

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;

void blendImageToMiddle(cv::Mat background, cv::Mat image) {
    // blend image into background (image over background)
    // background is fully opaque (don't have alpha channel, so must have 3 channels)

    int bgStartX = background.cols / 2 - image.cols / 2;
    int bgStartY = background.rows / 2 - image.rows / 2;

    // get the sub-area for blending in background
    cv::Mat blendMat = background(cv::Range(bgStartY, bgStartY + image.rows),
                                  cv::Range(bgStartX, bgStartX + image.cols));

    for (int y = 0; y < image.rows; ++y) {
        for (int x = 0; x < image.cols; ++x) {
            cv::Vec3b& backgroundPixel = blendMat.at<cv::Vec3b>(y, x);
            cv::Vec4b& imagePixel = image.at<cv::Vec4b>(y, x);

            // get alpha value (divide 255 since the image of alpha value is 0 to 255. Change it to 0.0-1.0)
            float alpha = imagePixel[3] / 255.;

            backgroundPixel[0] = cv::saturate_cast<uchar>(alpha * imagePixel[0] + (1.0 - alpha) * backgroundPixel[0]);
            backgroundPixel[1] = cv::saturate_cast<uchar>(alpha * imagePixel[1] + (1.0 - alpha) * backgroundPixel[1]);
            backgroundPixel[2] = cv::saturate_cast<uchar>(alpha * imagePixel[2] + (1.0 - alpha) * backgroundPixel[2]);
        }
    }
}

int main(int argc, char* argv[]) {

    // create 1024 x 1024 red background
    cv::Mat background(1024, 1024, CV_8UC3, cv::Scalar(0, 0, 255));

    // load a and b image
    cv::Mat a = cv::imread("../src/a.png", cv::IMREAD_UNCHANGED);
    cv::Mat b = cv::imread("../src/b.png", cv::IMREAD_UNCHANGED);

    blendImageToMiddle(background, a);
    blendImageToMiddle(background, b);

    cv::namedWindow("result", cv::WINDOW_NORMAL);
    cv::imshow("result", background);
    cv::waitKey(0);
}

+코드 해석

// create 1024 x 1024 red background
cv::Mat background(1024, 1024, CV_8UC3, cv::Scalar(0, 0, 255));

1024x1024의 빨간 background 이미지를 만든다.

// load a and b image
cv::Mat a = cv::imread("../src/a.png", cv::IMREAD_UNCHANGED);
cv::Mat b = cv::imread("../src/b.png", cv::IMREAD_UNCHANGED);

먼저 이미지를 cv::IMREAD_UNCHANGED flag로 로딩한다. 이 코드에서는 이미지는 32bit (RGB 각각 8 bit + alpha 8 bit) 이어야만 한다. 가끔 이미지의 RGBA 값의 범위가 0-255(8 bit) 가 아니라 0-65535(16 bit)일수도 있는데, 이런 경우 8 bit 일 때와 16 bit 일 때 다르게 처리해야 한다. 하지만 일단 여기서는 생략하겠다.

int bgStartX = background.cols / 2 - image.cols / 2;
int bgStartY = background.rows / 2 - image.rows / 2;

// get the sub-area for blending in background
cv::Mat blendMat = background(cv::Range(bgStartY, bgStartY + image.rows),
                              cv::Range(bgStartX, bgStartX + image.cols));

다음으로 blendImageToMiddle 함수를 살펴보겠다. 이 함수는 특정 이미지를 background 에 혼합(blending)하는 함수인데, background 가운데에 혼합한다. 처음에 background 이미지의 가운데에 이미지를 넣기 위해서는 background 이미지의 가운데 점에서 image 의 가운데 점을 뺀 값이 시작점이다.

background(cv::Range(..), cv::Range(..)) 는 background Matrix에서 Range에 해당하는 특정 영역을 가져온다. 예를 들면, background(cv::Range(1, 2), cv::Range(2, 3)) 이라 한다면, background 행렬에서 세로로 1부터 2까지 가로로 2부터 3까지의 부분을 참조하는 새로운 행렬을 생성한다. 다만, 이전 강의에서 말했듯이 Mat 클래스는 copyToclone 을 하지 않으면 행렬에 관한 데이터가 똑같은 포인터를 가지기 때문에 이 blendMat 행렬의 값을 바꾸면 background 행렬도 바뀐다!(동일한 데이터를 참조하니깐)

대신 blendMat(0, 0) 위치는 background 행렬의 (bgStartY, bgStartX)이므로 blendMat(0, 0) 값을 바꾸면 background 행렬의 (bgStart, bgStartX) 값이 바뀐다. 이 방법은 이미지의 특정 영역을 바꿀 때 코드를 간결하게 만들어줄 수 있는 좋은 방법이다.

for (int y = 0; y < image.rows; ++y) {
    for (int x = 0; x < image.cols; ++x) {
        cv::Vec3b& backgroundPixel = blendMat.at<cv::Vec3b>(y, x);
        cv::Vec4b& imagePixel = image.at<cv::Vec4b>(y, x);

        // get alpha value (divide 255 since the image of alpha value is 0 to 255. Change it to 0.0-1.0)
        float alpha = imagePixel[3] / 255.;

        backgroundPixel[0] = cv::saturate_cast<uchar>(alpha * imagePixel[0] + (1.0 - alpha) * backgroundPixel[0]);
        backgroundPixel[1] = cv::saturate_cast<uchar>(alpha * imagePixel[1] + (1.0 - alpha) * backgroundPixel[1]);
        backgroundPixel[2] = cv::saturate_cast<uchar>(alpha * imagePixel[2] + (1.0 - alpha) * backgroundPixel[2]);
    }
}

위의 공식대로 이미지 값을 연산한다. background 이미지완전 불투명(fully-opaque)하기 때문에 alpha 값이 없는 BGR(3 채널) 값이다. at 함수나 saturate_cast 함수는 각각 이미지 로딩과 Mat 클래스 파헤쳐보기 3이미지 로딩과 Mat 클래스 파헤쳐보기 4에 설명이 나와있다.

알파값으로 계산하기 위해서는 알파값이 0.0-1.0이어야 하는데 RGBA는 0-255 (16bit에선 0-65535) 값이다. 따라서 이 값을 0.0-1.0 사이로 바꿔주기 위해서 255를 나눈다. 그리고 BGR을 각각 아까의 공식대로 바꾼다.

이미지 결과는 다음과 같다.

opencv result

이미지가 깔끔하게 잘 나온 것을 확인할 수 있다. 여기서는 background 이미지완전 불투명한 빨간 이미지를 사용해서 BGR 채널만 사용했는데 만약에 투명한 2개의 이미지를 사용해서 합성하고 그 이미지를 BGRA 채널로 저장하기 위해서는 처음에 등장한 공식을 사용해서 쓰면 된다.