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

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

April 21, 2018 - 3 minute read -
opencv

이전 Post에서 이미지를 로딩하는 방법과 Mat 클래스의 생성, 출력하는 방법, 이미지와 연관성 등에 대해서 알아보았다. 이번에는 행렬의 값을 바꾸는 방법을 알아보기에 앞서 cv::Mat 의 내부 구조에 대해 일부 살펴볼 것이다.

Previous Post

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

행렬(cv::Mat) 클래스의 데이터 포인터 변수

cv::Mat 클래스(인스턴스)를 다룰 때 주의해야 될 점이 있는데 모든 인스턴스마다 각각의 고유한 행렬 데이터를 가지고 있지 않다는 점이다. (…) 무슨 말인지 처음엔 이해를 못하겠지만 다음을 예제로 한 번 보자.

// create a 3x3 matrix
cv::Mat M1(3, 3, CV_8UC1);
// deep copy? 
cv::Mat M2 = M;
cv::Mat M3(M1);

위 코드에서 M1, M2, M3 변수(인스턴스) 모두 고유한 행렬을 가지고 있다고 생각하기 쉽지만 아니다. cv::Mat 클래스 내부에는 data 라는 행렬 데이터를 가르키는 포인터가 존재하는데 동일한 메모리 위치를 가르킨다. 즉, M1 의 데이터 값을 변경하면 M2, M3 또한 동시에 변한다!

아래 코드와 같이 포인터 주소를 출력해보자.

cv::Mat M1(3, 3, CV_8UC1);
cv::Mat M2 = M1;
cv::Mat M3(M1);

cout << "M1 데이터 주소 : " << (void*)M1.data << endl;
cout << "M2 데이터 주소 : " << (void*)M2.data << endl;
cout << "M3 데이터 주소 : " << (void*)M3.data << endl;

그 결과는 필자의 컴퓨터에서는 다음과 같이 나왔다.

M1 데이터 주소 : 0x7fa715411a80
M2 데이터 주소 : 0x7fa715411a80
M3 데이터 주소 : 0x7fa715411a80

메모리 주소값이 중요한 게 아니라 M1, M2, M3 모든 주소가 같다는 것이 중요하다. 유식한 말로 깊은 복사(deep copy) 가 아닌 얕은 복사(shallow copy)를 한다고 하는데 이렇게 하는 이유는 쓸데없는 메모리 주소 낭비와 쓸데없는 deep copy 로 인한 속도 저하를 방지하기 위해서이다.

깊은 복사(deep copy)를 하기위해서는 clone 또는 copyTo 메소드를 사용하면 된다.

// shallow copy
cv::Mat M2 = M1;

// deep copy by "copyTo" method
cv::Mat M3, M4;
M1.copyTo(M3);

// deep copy by "clone" method
M4 = M1.clone();

cout << "M1 데이터 주소 : " << (void*)M1.data << endl;
cout << "M2 데이터 주소 : " << (void*)M2.data << endl;
cout << "M3 데이터 주소 : " << (void*)M3.data << endl;
cout << "M4 데이터 주소 : " << (void*)M4.data << endl;

컴파일 후 수행한 결과는

M1 데이터 주소 : 0x7fa508411b40
M2 데이터 주소 : 0x7fa508411b40
M3 데이터 주소 : 0x7fa508412ec0
M4 데이터 주소 : 0x7fa508412f40

위와 같이 깊은 복사(deep copy)를 수행한 M3, M4 의 경우 메모리 주소가 다르게 나오는 것을 볼 수 있다.

행렬(cv::Mat) 클래스의 행렬 헤더(Matrix Header)

cv::Mat 클래스에는 Matrix Header 라고 불리는 행렬에 대한 정보(메타 데이터)가 존재한다. 행렬의 타입, 행렬 세로, 가로 길이, 채널 수 등에 대한 정보가 담겨있으며, public 이므로 직접적으로 접근(access) 및 변경이 가능하다.

+ flags

우리가 행렬을 생성할 때 CV_32SC1, CV_8UC3 등 flag 인자를 넘겨주었는데 이런 정보가 들어있다.

+ dims

행렬의 차원 수를 의미한다. 항상 2 이상이며, 일반적으로 2인 경우가 많다.

+ rows, cols

2차원(2-dimension)일 경우 각각 세로 길이(rows)가로 길이(cols)를 의미하며, 3차원 이상일 경우는 -1 이다. 일반적으로 이미지 처리할 때 2차원을 가장 많이 쓰기도 하고, rows, cols 가 각각 이미지의 세로 길이, 가로 길이를 의미하므로 다른 변수에 비해 사용도가 비교적 높다.

clone 또는 copyTo 메소드를 사용하지 않고 생성자나 대입 연산자(=)로 새로운 cv::Mat 인스턴스에 넘긴 경우, 데이터는 공유하고(포인터가 같은 메모리를 가르키고) 헤더를 복사하게 된다.

Next Post

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