[OpenCV] 이미지 로딩과 Mat 클래스 파헤쳐보기 4

OpenCV에서 이미지를 로딩하는 방법과 cv::Mat 클래스의 구조와 사용법에 대해 알아보자

April 21, 2018 - 7 minute read -
opencv

이전 Post에서 Mat 인스턴스에서 값을 변경하는 방법에 대해서 알아보았다. 이번 포스트에서는 실제로 이미지에서 픽셀 값(matrix 값)을 변경해볼 것이며, look-up table 을 사용해서 최적화 시킬 수 있는 방법에 대해 포스팅할 것이다.

Previous Post

[OpenCV] 이미지 로딩과 Mat 클래스 파헤쳐보기 1
[OpenCV] 이미지 로딩과 Mat 클래스 파헤쳐보기 2
[OpenCV] 이미지 로딩과 Mat 클래스 파헤쳐보기 3

이미지 밝기 변경하기

이미지 값을 변경해서 이미지의 밝기를 조정해보려고 한다. 이미지의 픽셀은 각각 특정 RGB 값으로 이루어져 있다. R(Red), G(Green), B(Blue) 값은 각각 0-255 값을 가진다. RGB 값이 모두 0일 때는 빨강, 초록, 파랑이 모두 없다는 것이므로 검정색이 나오게 되며, RGB 값이 모두 255인 경우 빨강, 초록, 파랑의 빛을 합치면 흰색이 되므로 흰색이 나온다.

또한 빨강 255, 초록 0, 파랑 0 의 경우 빨강만 존재하므로 순수한 빨강색이 나오고, 빨강 255, 초록 255, 파랑 0인 경우, 빨간 빛과 초록 빛이 합치면 나오는 색인 노란색이 나오게 된다. 이렇게 RGB 값의 변경으로 색상을 표현할 수 있으며, 255에 가까울수록 밝은 색, 0에 가까울수록 어두운 색이 나오게 된다.

따라서 이미지를 밝게 하기 위해서는 R, G, B 의 값을 높이면 된다. 즉, 이미지 행렬 값을 모두 일정 크기만큼 곱하거나 더하기만 하면 된다. 우리는 픽셀의 값을 다음과 같이 바꿀 것이다.

pixel = (alpha) x pixel + (bias)

이미지 밝기 정도는 alpha, bias 값에 따라서 정해지며, alpha 값이 1을 기준으로 높으면 더 밝아지고, 1보다 작으면 어두워진다. 마찬가지로 bias 의 값은 0을 기준으로 더 커질수록 밝아지며, 더 작아질수록 어두워진다.

이전 포스트에서 배운대로 이미지 하나를 로딩해서 픽셀값을 다음 공식을 이용해서 밝기를 밝게 해보자.

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

#define ALPHA   1.2
#define BIAS    50

int main(int argc, char* argv[]) {
    // create two matrices for image (original image, image to convert)
    cv::Mat image = cv::imread("../src/lena.jpg", cv::IMREAD_COLOR);
    cv::Mat convertedImage = image.clone();

    for (int i = 0; i < convertedImage.rows; ++i) {
        for (int j = 0; j < convertedImage.cols; ++j) {
            auto& pixel = convertedImage.at<cv::Vec3b>(i, j);
            // Get R, G, B value
            auto& B = pixel[0];
            auto& G = pixel[1];
            auto& R = pixel[2];

            // Calculate new pixel R, G, B value
            R = cv::saturate_cast<uchar>(ALPHA * R + BIAS);
            G = cv::saturate_cast<uchar>(ALPHA * G + BIAS);
            B = cv::saturate_cast<uchar>(ALPHA * B + BIAS);
        }
    }

    // show the images
    cv::namedWindow("color image");
    cv::imshow("color image", image);

    cv::namedWindow("converted image");
    cv::imshow("converted image", convertedImage);
    cv::waitKey(0);

    return 0;
}

이 예제에서는 임의로 alpha 값을 1.2, bias 값을 50으로 지정했으며, 그 결과 다음과 같이 밝게 나온 이미지를 얻을 수 있었다.

cv::saturate_cast<uchar> 는 공식을 보면 알 수 있듯이 실수형(float)을 곱하기 때문에 결과가 float 인데다가 255를 넘길 수 있으니 255을 넘기면 255로 설정해주고, float 도 알아서 uchar(byte) 형태로 바꿔주는 역할을 하는 함수이다. 즉, 이 함수를 사용하면 255가 넘기면 255로 바꾸고 uchar 형변환도 해줘야하는 귀차니즘을 해결할 수 있다.

opencv result

결과는 잘 나왔다.(…) 그런데 이미지의 크기가 512x512이다. 따라서 픽셀 수는 총 262144개이며, 한 픽셀 당 R, G, B 3개를 계산해야 하므로 총 786432번 곱하고 더하고 대입해야 한다. 현재 이미지는 작은 편이라 금방 계산이 되지만 크기가 커지거나 많은 이미지를 처리해야 할 경우 속도 문제가 생길 수 있다.

+ Lookup Table 사용하기

그런데 한가지 간과한 것이 있는데 같은 계산을 여러 번한다는 것이다. 픽셀에서 R, G, B 값은 0-255 사이밖에 안되는데 예를 들면 100이란 값이면 100x1.2+50=170 이란 값이 나오는데 100이란 값이 나올 때마다 곱하고 더해준다. 100이란 값이 1000번 나왔으면 이전에 동일한 연산(100x1.2+50=170)을 1000번 한다는 것이다.(…)

그래서 look-up table 이란 256개 배열을 만들고 그 배열로 계산을 먼저한 후 그 이후에 이미지 픽셀 값 처리를 할 때는 대입만 할 것이다. 다음과 같이

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

#define ALPHA   1.2
#define BIAS    50

int main(int argc, char* argv[]) {
    // create two matrices for image
    cv::Mat image = cv::imread("../src/lena.jpg", cv::IMREAD_COLOR);
    cv::Mat convertedImage = image.clone();
    int table[256];     // lookup table

    // set lookup table
    // calculate pixel value before converting
    for (int i = 0; i < 256; ++i)
        table[i] = cv::saturate_cast<uchar>(ALPHA * i + BIAS);

    // set new pixel R, G, B value
    for (int i = 0; i < convertedImage.rows; ++i) {
        for (int j = 0; j < convertedImage.cols; ++j) {
            auto& pixel = convertedImage.at<cv::Vec3b>(i, j);
            auto& B = pixel[0];
            auto& G = pixel[1];
            auto& R = pixel[2];

            // get the new value from lookuptable (not calculating!)
            R = table[R];
            G = table[G];
            B = table[B];
        }
    }

    // show the images
    cv::namedWindow("color image");
    cv::imshow("color image", image);

    cv::namedWindow("converted image");
    cv::imshow("converted image", convertedImage);
    cv::waitKey(0);

    return 0;
}

이렇게 하면 먼저 각각 값마다 결과 값을 먼저 계산한 후에 대입만 하면 되므로 곱하기와 더하기 연산을 줄일 수 있다.

cv::LUT 함수 사용하기

cv::LUT 함수는 위와 같은 방법을 사용하여 대입해주는 역할을 해주는 함수로 내부에 GPU를 사용한 건지 몰라도 속도를 훨씬 더 높여준다. 먼저 cv::Mat 으로 1x256 행렬을 만들어서 각각 픽셀 값에 대응하는 결과 값을 넣어준 후 cv::LUT([원본], [LookUpTable 행렬], [결과]) 함수를 실행시켜주면 된다.

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

#define ALPHA   1.2
#define BIAS    50

int main(int argc, char* argv[]) {
    // create two matrices for image
    cv::Mat image = cv::imread("../src/lena.jpg", cv::IMREAD_COLOR);
    cv::Mat convertedImage;

    // create 1x256 matrix for lookup
    cv::Mat lookUpTable(1, 256, CV_8UC3);

    // create a matrix for lookup
    for (int i = 0; i < 256; ++i) {
        auto& pixelValue = lookUpTable.at<cv::Vec3b>(0, i);
        // calculate value for B, G, R
        pixelValue[0] = cv::saturate_cast<uchar>(ALPHA * i + BIAS);
        pixelValue[1] = cv::saturate_cast<uchar>(ALPHA * i + BIAS);
        pixelValue[2] = cv::saturate_cast<uchar>(ALPHA * i + BIAS);
    }

    // convert the pixel value by lookup table matrix
    cv::LUT(image, lookUpTable, convertedImage);

    // show the images
    cv::namedWindow("color image");
    cv::imshow("color image", image);

    cv::namedWindow("converted image");
    cv::imshow("converted image", convertedImage);
    cv::waitKey(0);

    return 0;
}

결과를 보면 알 수 있듯이 모두 동일한 결과가 출력됨을 알 수 있다. 하지만 cv::LUT 가 가장 빠르게 수행될 것이다.

다만 look-up Tablecv::LUT 는 0-255같이 작은 범위에만 적용이 가능하다. 위 예제처럼 RGB의 각 요소 R, G, B 에 대해서 각각 look-up table 을 생성하는 게 아니라 RGB 전체에 대해서 look-up-table을 생성한다면 R, G, B는 각각 0-255 값을 가지므로 한 픽셀 당 16777216(256 x 256 x 256)개의 값을 가질 수 있으므로 look-up table 을 사용하기 보다는 그냥 픽셀마다 계산하는게 더 나을 것이다.
(물론 저 정도면 현재 컴퓨터 시스템에서는 충분히 계산하고 메모리에 넣을 수 있는 속도와 크기지만 look-up table 생성하는데만 시간이 더 걸릴 것이다(…) 차라리 픽셀마다 계산하는게 더 빠를 때가 많을 것이다..)