OpenCV에서 cv::Mat 클래스는 매우 자주 사용되며 그만큼 중요합니다. 영상과 행렬을 표현할 때 사용하는 클래스이므로 OpenCV로 개발을 할 때 안 쓸 수가 없지요. 이번 포스팅에서는 cv::Mat 클래스에 대한 소개 및 전반적인 사용법을 다루도록 하겠습니다. 사용법은 꽤 직관적이라서 크게 어렵지는 않은데 내용은 많네요. 보다 자세한 내용은 OpenCV 홈페이지에서 cv::Mat Class Reference를 참고하시기 바랍니다.
1) Mat 클래스 개요
Mat 클래스는 행렬(Matrix)를 표현하기 위한 용도로써, n차원의 단일 채널 또는 멀티 채널 배열을 나타낼 수 있습니다. Mat 클래스의 용도로는 정수, 실수 또는 복소수 벡터 및 행렬, 회색조 또는 컬러 이미지, 복셀 볼륨, 벡터 필드, 포인트 클라우드, 텐서, 히스토그램을 저장하는 데 사용할 수 있습니다.
– Depth: 지원하는 행렬 원소(Element) 자료형
Mat Class의 행렬 원소는 char(unsigned 포함), short(unsigned 포함), int, float, double형의 자료형을 지원합니다. 이 정보를 Depth라고 합니다. Depth 정보를 쉽게 식별하기 위해, 다음과 같이 이름이 정의되어 있습니다.
재정의 이름 | 의미 | Depth_mask | 범위 |
CV_8U | unsigned char 8-bit unsigned integers |
0 | 0 ~ 255 |
CV_8S | signed char 8-bit signed integers |
1 | -128 ~ 127 |
CV_16U | unsigned short 16-bit unsigned integers |
2 | 0 ~ 65535 |
CV_16S | signed short 16-bit signed integers |
3 | -32768 ~ 32767 |
CV_32S | int 32-bit signed integers |
4 | -2147483648 ~ 2147483647 |
CV_32F | float 32-bit floating-point numbers |
5 | -FLT_MAX ~ FLT_MAX, INF, NAN |
CV_64F | double 64-bit floating-point numbers |
6 | -DBL_MAX ~ DBL_MAX, INF, NAN |
CV_16F | float16 16-bit floating-point numbers |
7 | -OpenCV 4부터 지원 / 정보 없음 |
– Channel: 행렬 원소를 구성하는 값
Mat 행렬 원소를 구성하는 값을 의미합니다. 행렬 원소는 단일 값일 수도 있고(단일 채널), 여러 개의 값을 가지고 있을 수 있습니다(멀티 채널). 쉽게 설명하기 위해 예를 들면, 컬러 영상은 1개의 픽셀에 대해 RGB값을 각각 가지고 있어야 하므로 3채널로 표현하고, 그레이스케일 영상은 1채널로 표현합니다.
앞에서 설명한 Depth와 Channel 정보를 합쳐서 Mat 클래스를 선언할 때 사용하며, 이름이 미리 정의되어 있습니다. 예를 들면 아래와 같습니다.
CV_8UC1: unsigned char 자료형 + 채널 1개인 행렬
CV_32FC3: float 자료형 + 채널 3개인 행렬
– Public 멤버 변수
Public 멤버 변수를 소개합니다. 일반적으로는 2차원 영상을 저장하고 접근하는 데 가장 많이 쓰이기 때문에, 2차원 행렬일 때만 유효한 rows와 cols 변수가 있습니다.
// 행렬의 차원 수를 의미합니다.
int dims;
// 행렬의 row값과 col값을 의미합니다. 3차원 이상 행렬에서는 -1로 설정됩니다.
int rows, cols;
// 행렬의 Size를 의미합니다.
MatSize size;
// 행렬의 데이터를 가리키는 포인터입니다.
uchar* data;
2) Mat 객체 생성하기
Mat 객체를 생성하고, 생성 시점에서 값을 초기화하는 방법은 여러가지가 있습니다. 1채널 행렬을 생성하는 방법은 아래 코드와 주석을 참고하시기 바랍니다.
// 비어있음
Mat mat;
// 320x480 사이즈 행렬 (unsigned char, 1 channel)
Mat mat1(320, 480, CV_8UC1);
cout << "mat1: " << mat1.rows << ", " << mat1.cols << ", " << mat1.dims << endl;
// 320x480 사이즈 행렬 (unsigned char, 1 channel)
Mat mat2(Size(320, 480), CV_8UC1); // 가로/세로 주의
cout << "mat2: " << mat2.rows << ", " << mat2.cols << ", " << mat2.dims << endl;
// 원소가 0으로 초기화 된 3x3 행렬 (float, 1 channel)
Mat mat3 = Mat::zeros(3, 3, CV_32FC1);
cout << "mat3: " << endl << mat3 << endl;
// 원소가 1으로 초기화 된 3x3 행렬 (float, 1 channel)
Mat mat4 = Mat::ones(3, 3, CV_32FC1);
cout << "mat4: " << endl << mat4 << endl;
// 단위행렬로 초기화 된 3x3 행렬 (int, 1 channel)
Mat mat5 = Mat::eye(3, 3, CV_32SC1);
cout << "mat5: " << endl << mat5 << endl;
// 1~6 값으로 초기화 된 2x3 행렬 (float, 1 channel)
// 외부 배열을 사용할 때, 메모리 관리는 알아서 해야 하므로 주의
float data[] = {1, 2, 3, 4, 5, 6};
Mat mat6(2, 3, CV_32FC1, data);
cout << "mat6: " << endl << mat6 << endl;
// 1~6 값으로 초기화 된 2x3 행렬 (float, 1 channel)
Mat mat7 = Mat_<float>({2, 3}, {1, 2, 3, 4, 5, 6});
cout << "mat7: " << endl << mat7 << endl;
실행 결과3채널 영상을 만드는 방법은 아래와 같습니다. Scalar 클래스를 사용해서 왼쪽은 1채널, 오른쪽은 3채널 영상을 만들어 보았습니다.
Scalar 에는 컬러값을 담는데, Blue, Green, Red 순으로 넣으면 됩니다.
Mat img1(320, 480, CV_8UC1, Scalar(128));
Mat img2(320, 480, CV_8UC3, Scalar(255, 100, 100));
3) Mat 객체 복사하기
Mat 객체를 복사하는 방법에 대해 소개합니다. Shallow copy와 Deep copy가 있으며, 기본적으로 대입 연산자, 복사 생성자는 Shallow copy를 수행합니다. Mat 객체가 영상을 나타낼 경우 데이터의 양이 많기 때문에, Deep copy는 필요에 따라 사용하면 될 것 같습니다.
// Shallow copy
Mat mat1(3, 3, CV_8UC1);
Mat mat2 = mat1;
// mat1 객체의 원소(1, 1)를 100으로 변경
mat1.at<uchar>(1, 1) = 100;
cout << mat2 << endl;
mat2는 mat1을 shallow copy하였으므로, mat1의 원소를 수정하더라도 mat2 행렬에서 참조할 수 있습니다.
실행 결과Deep copy를 수행하려면 별도의 함수를 사용해야 합니다. Mat::clone() 또는 Mat::copyTo() 함수가 있으며, 사용 예제는 다음과 같습니다.
// Deep copy
// Clone 함수 사용
Mat mat3 = mat1.clone();
// CopyTo 함수 사용
Mat mat4;
mat3.copyTo(mat4);
// mat3 객체의 원소(0, 0)을 50으로 변경
mat3.at<uchar>(0, 0)=50;
cout << mat3 << endl;
cout << mat4 << endl;
mat3 객체의 원소(0, 0)을 50으로 바꾸더라도 mat4는 별개의 데이터이므로 영향이 없습니다.
실행 결과
4) Mat 객체에 새로운 행렬을 할당하기
이미 생성되어 있는 Mat 객체에 새로운 행렬을 할당하려면, Mat 클래스의 create(rows, col, type) 멤버 함수를 사용합니다. 단, 현재 행렬의 사이즈와 타입이 기존 행렬과 다를 경우에만 새로운 공간을 할당합니다.
// 7x7 사이즈 float형 2채널 행렬 선언
// 각 원소는 (1, 3)값을 갖고 있음
Mat mat(7, 7, CV_32FC2, Scalar(1, 3));
cout << mat << endl;
// 선언된 행렬에 새로운 2x2, unsigned char형 4채널 행렬을 할당
mat.create(2, 2, CV_8UC(4));
cout << mat << endl;
실행 결과create 함수에는 행렬의 원소 값을 초기화하는 기능이 없기 때문에, 0으로 셋팅되어 있습니다. 값을 대입하거나 setTo() 멤버함수를 사용하여 원소 값을 셋팅할 수 있습니다.
// 위 코드와 이어짐 - mat은 4채널 행렬
// 원소를 1로 셋팅
mat.setTo(1);
cout << mat << endl;
// 4채널 행렬이므로 원소 값으로 Scalar 객체를 대입
mat = Scalar(100, 100, 255, 128);
cout << mat << endl;
0이었던 원소 값이 바뀜
5) Mat 객체 원소에 접근하기
Mat 객체를 만들었으면, 원소에 접근하는 방법도 알아야 하겠죠. at() 멤버함수를 사용하여 접근할 수 있으며, 예제를 통해 확인하시기 바랍니다.
// 1채널 행렬의 원소값 접근
Mat mat1(3, 3, CV_8UC1);
for(int j=0 ; j<mat1.rows ; j++) {
for(int i=0 ; i<mat1.cols ; i++) {
mat1.at<uchar>(j, i) = j*3+i;
}
}
cout << mat1 << endl << endl;
// 3채널 행렬의 원소값 접근
Mat mat2(3, 3, CV_32FC3);
for (int j=0 ; j<mat2.rows; j++) {
for(int i=0 ; i<mat2.cols; i++) {
mat2.at<Vec3f>(j, i)[0] = j*3+i;
mat2.at<Vec3f>(j, i)[1] = j*3+i+0.3f;
mat2.at<Vec3f>(j, i)[2] = j*3+i+0.6f;
}
}
cout << mat2 << endl;
실행 결과ptr() 멤버함수를 통해서도 원소에 접근할 수 있습니다. 사용 방법은 역시 예제를 통해서 확인하시기 바랍니다.
Mat mat3(3, 3, CV_8UC1, 1);
for(int j=0 ; j<mat3.rows ; j++) {
uchar* p = mat3.ptr<uchar>(j);
for(int i=0 ; i<mat3.cols ; i++) {
p[i]*=i*2;
}
}
cout << mat3 << endl << endl;
실행 결과※ Iterator를 활용하는 방법도 있지만, 잘 사용하지는 않아서 소개하지 않았습니다. 필요할 경우 reference 사이트를 참고하시기 바랍니다.