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

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

April 21, 2018 - 7 minute read -
opencv

이전 Post에서 Mat 클래스의 구조에 대해서 설명하였다. 이번 포스트에서는 Mat 인스턴스의 데이터, 즉 행렬(matrix) 값을 변경하는 방법에 대해 포스팅할 것이다.

matrix 값을 변경하는 방법에는 여러가지 방법이 있는데 경우에 따라 더 빠르게 변경할 수 있는 방법이 있으니 여러 방법을 포스팅할 것 이다.

Previous Post

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

Matrix 데이터 구조

cv::Mat 내부에는 uchar* 타입의 data 라는 포인터 변수가 존재하는데 이 포인터는 실제 matrix 데이터를 가르킨다. 이 포인터 또한 public 이므로 직접적으로 접근이 가능하다. (하지만 이 포인터에 직접 접근해서 값을 읽거나 변경하는 것은 좋은 방법은 아니다(…) 잘만 사용하면 상관없지만 실수하면 처음에는 모르다가 나중에 골치아픈 일이 생길 수 있다.)

처음에 matrix 를 생성할 때 CV_32S1, CV_8UC3, … 등 flag 를 넘겨준다고 이전 포스트에서 설명했었다. 예를 들어 3x3 행렬을 생성할 경우

opencv result

다음과 같이 1차원 형태로 데이터가 저장된다.

CV_8UC3 같이 행렬 요소 하나에 3개의 데이터(3 채널)가 있는 경우엔 (보통 이미지에서는 컬러 이미지를 로딩했을 때 픽셀 하나당 R(Red), G(Green), B(Blue) 3개를 저장해야 하므로 픽셀(행렬 요소) 하나 당 3개의 값이 저장되어 있다. 단, 첫 번째 값이 아래 그림처럼 R(Red) 가 아니라 B(Blue) 이다(…) 이렇게 순서를 바꾼 의도는 잘 모르겠으나 R(Red), B(Blue) 의 순서가 바껴서 B, G, R 로 저장되어 있다.)

opencv result

이런 식으로 저장이 된다.

at 메소드 활용

Mat 클래스는 at 메소드를 제공하는데 데이터 값을 변경할 수 있도록 reference 형태로 제공한다. at은 템플릿 함수이기 때문에 at<데이터 타입> (ex. at, at) 등 타입을 붙여서 실행해야 한다.

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

int main(int argc, char* argv[]) {
    // create a 2x2 matrix filled with 0
    cv::Mat M1 = cv::Mat::zeros(2, 2, CV_32SC1);

    // change value
    M1.at<int>(0, 0) = 1;
    M1.at<int>(0, 1) = 2;
    M1.at<int>(1, 0) = 3;
    M1.at<int>(1, 1) = 4;

    cout << "M1 = " << endl << M1 << endl;
}

결과는 다음과 같다.

M1 = 
[1, 2;
 3, 4]

flag 가 CV_8UC3 처럼 행렬 요소 하나가 3개의 값을 가지는 경우에는, 즉 채널 수가 2개 이상인 경우, Vec 클래스를 사용하면 된다. 이름에서 유추 가능하듯이 벡터를 나타낸 클래스라 보면 되고, 3 채널인 경우, 다음과 같이 3차원 벡터 타입으로 at 메소드로 접근해서 배열처럼 사용하면 된다.

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

int main(int argc, char* argv[]) {
    // create a 2x2 matrix filled with 0
    cv::Mat M1 = cv::Mat::zeros(2, 2, CV_8UC3);

    // change value
    M1.at<cv::Vec3b>(0, 0)[0] = 1;
    M1.at<cv::Vec3b>(0, 0)[1] = 2;
    M1.at<cv::Vec3b>(0, 0)[2] = 3;

    cout << "M1 = " << endl << M1 << endl;
}

처럼 바꾸면 된다. cv::Vec3b 의 경우 Byte(unsigned char) 3개를 나타내는 벡터라고 보면 되며,

Vec[채널 수][b : byte, s : short, i : int, f : float, d: double]

을 의미한다. 즉, 예를 들면 Vec4i 는 채널 수(데이터 개수) 4, 데이터 타입 integer 형을 의미하고, Vec3f 는 채널 수(데이터 개수) 3개, 데이터 타입 float 등을 의미한다고 볼 수 있으며, 배열 접근하듯이 Vec3f v 이면, v[0], v[1], v[2] 같이 접근하면 된다. 말그대로 벡터를 나타낸 것이다.

실제로 openCV 라이브러리 헤더파일 내부에는 다음과 같이 정의되어 있다.

typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;

typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;

typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;

typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;

typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;

typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;

operator 활용

cv::Mat 값 생성 시 작은 크기의 경우 Mat_ 클래스의 operator« 를 사용하여 값을 쉽게 초기화할 수 있다.

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

int main(int argc, char* argv[]) {
    // create a 2x2 matrix
    cv::Mat M1 = (cv::Mat_<int>(2, 2) << 1, 2, 3, 4);
    cout << "M1 = " << endl << M1 << endl;
}

Iterator 활용

MatIterator_<type> 을 통해 matrix 의 모든 요소에 접근이 가능하며, 사용 방법은 일반적인 Iterator 사용 방법과 비슷하다.

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

int main(int argc, char* argv[]) {
    // create 2x2 matrix
    cv::Mat M1 = cv::Mat::zeros(2, 2, CV_8UC3);

    int sequence = 0;

    // Use Iterator
    cv::MatIterator_<cv::Vec3b> it, end;
    for (it = M1.begin<cv::Vec3b>(), end = M1.end<cv::Vec3b>(); it != end; ++it) {
        (*it)[0] = sequence++;
        (*it)[1] = sequence++;
        (*it)[2] = sequence++;
    }

    cout << "M1 = " << endl << M1 << endl;
    return 0;
}

Mat_ class 활용

at<type> 메소드를 사용할 경우 메소드를 사용할 때마다 at<int> 처럼 type 을 선언해줘야 되는 번거로움이 있는데 이를 해결하기 위해서 Mat_ class를 활용할 수 있다.

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

int main(int argc, char* argv[]) {
    // create a 2x2 matrix filled with 0
    cv::Mat M = cv::Mat::zeros(2, 2, CV_8SC3);
    cv::Mat_<cv::Vec3b> _M = M;

    // change value
    _M(0, 0)[0] = 1;
    _M(0, 0)[1] = 2;
    _M(0, 0)[2] = 3;
    M = _M;

    cout << "M = " << endl << M << endl;
}

다만 Mat_ 클래스의 경우 대입 연산자(=)나 복사 생성자 과정에서 얕은 복사(shallow)를 하는 것이 아니라 깊은 복사(deep copy)를 하기 때문에 항상 값을 변경한 후에는 다시 기존 matrix 에 대입해야 한다. (위 코드의 M = _M 처럼 말이다!)

다음 포스트에서는 이미지를 로드해서 이미지 matrix 를 직접 변경해볼 것이며, LookUpTable 을 사용한 변경과 마지막으로 LUT 를 사용하여 이미지 픽셀값을 건드릴 것이다. 이번 포스트에서 설명한 방법을 사용해서 픽셀값을 변경해도 되지만 이미지의 경우 matrix 의 크기가 크기때문에 모두 건드리려면 연산량이 많아진다. 따라서 연산량을 줄이고 속도를 향상 시킬 수 있는, 즉 최적화 시킬 수 있는 방법을 사용할 수 있는데 그런 방법을 다음 포스트에서 포스팅할 것이다.

Next Post

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