References
- https://github.com/opencv/opencv
- https://docs.opencv.org/
- OpenCV 4로 배우는 컴퓨터 비전과 머신러닝
Contents
- Mat 생성 및 초기화
- Mat 복사
- 부분 행렬 추출
- 행렬 원소 값 참조
- 행렬 정보 참조
- 행렬 연산
- 행렬 크기 및 타입 변환
OpenCV에서 가장 많이 사용하는 클래스 중 하나가 행렬을 표현하는 Mat 클래스입니다. 이번 포스팅에서는 Mat 클래스를 이용하여 행렬을 생성하는 기본적인 방법과 행렬의 복사 등에 관련된 내용들에 대해 살펴보겠습니다. 또한 Mat 클래스에 저장된 행렬의 원소(또는 이미지 픽셀값)에 접근하는 방법과 일반적인 행렬 연산 방법도 살펴보겠습니다.
Mat 클래스 기본
Mat 클래스는 일반적인 2차원 행렬뿐만 아니라 고차원 행렬을 표현할 수 있으며, 1개 이상의 채널을 가질 수 있습니다. 또한 정수, 실수, 복소수 등으로 구성된 행렬 또는 벡터를 저장할 수 있고, grayscale 또는 color 이미지를 저장할 수도 있습니다. 경우에 따라서는 vector field, tensor, histogram 등 정보를 저장하는 용도로 사용할 수도 있지만, 실제로는 2차원 이미지 데이터를 저장하고 처리하는 용도로 많이 사용합니다.
Mat 클래스는 core 모듈의 mat.hpp 파일에 정의되어 있습니다. Mat 클래스 전체를 살펴보는 것은 복잡하고 시간도 오래걸리므로, 여기서는 간단하게 살펴보겠습니다.
class Mat
{
public:
Mat();
Mat(int rows, int cols, int type);
Mat(Size size, int type);
Mat(int rows, int cols, int type, const Scalar& s);
Mat(Size size, int type, const Scalar& s);
Mat(const Mat& m);
...
~Mat();
void copyTo(OutputArray m) const;
...
Mat& setTo(InputArray value, InputArray mask=noArray());
...
static MatExpr zeros(int rows, int cols, int type);
...
static MatExpr ones(int rows, int cols, int type);
...
void create(int rows, int cols, int type);
bool empty() const;
template<typename _Tp> _Tp* ptr(int i0 = 0);
template<typename _Tp> _Tp& at(int row, int col);
int dims;
int rows, cols;
uchar* data;
MatSize size;
...
};
전체 정의를 보시면 알겠지만, Mat 클래스는 다양항 형태의 생성자와 엄청 많은 멤버 함수, 변수를 가지고 있습니다. 생성자와 멤버 함수에 대한 내용은 아래에서 예제를 통해 살펴보도록 하고, 여기서는 멤버 변수들에 대해 간단하게 살펴보겠습니다.
Mat::dims 멤버 변수는 Mat 행렬의 차원을 나타내며, 이미지와 같은 2차원 행렬인 경우에 이 값은 2입니다. Mat::rows와 Mat::cols는 2차원 행렬의 크기를 나타냅니다. 만약 Mat 객체에 영상(이미지)이 저장되어 있다면 Mat::rows는 영상의 세로 픽셀 크기이며, Mat::cols는 가로 픽셀 크기입니다. Mat::rows와 Mat::cols는 Mat 객체가 2차원 행렬인 경우에만 의미있는 값을 가지며 3차원 이상의 행렬에서는 -1이 저장됩니다. 3차원 이상의 행렬에서 크기 정보는 Mat::size 멤버 변수를 이용해 참조할 수 있습니다.
Mat::data는 행렬의 원소 데이터가 저장되어 있는 메모리 공간을 가리키는 포인터 타입입니다. 만약 행렬에 아무것도 저장되어 있지 않는 상태라면 Mat::data는 0(NULL) 값을 갖습니다.
다른 멤버 변수들도 많은데 자주 사용되는 것은 아니므로 여기서 더 자세하게 다루지는 않겠습니다.
OpenCV에서 Mat 행렬은 다양한 자료형의 원소를 가질 수 있습니다. 또한 Mat의 원소는 하나의 값을 가질 수도 있고, 여러 개의 값이 모여서 하나의 원소로 표현되기도 합니다. 따라서 이 클래스를 제대로 이용하려면 행렬 원소를 어떻게 구성하고 표현하는지 잘 이해하고 있어야 합니다.
OpenCV는 C/C++ 기본 자료형 중 unsigned char, signed char, unsigned short, signed short, int, float, double을 사용하는 Mat 행렬을 지원하며, OpenCV 4.0부터는 16비트 실수형 타입도 사용할 수 있습니다. Mat 클래스에서 행렬이 어떤 자료형에 사용하는지에 대한 정보를 Depth라고 부르는데 이 depth는 다음과 같은 형식의 매크로 상수를 이용하여 표현합니다.
CV_<bit-depth>{U|S|F}
여기서 bit-depth에는 8, 16, 32, 64라는 숫자를 지정할 수 있으며, 이는 원소 값 하나의 비트 수를 나타냅니다. 그 다음 {U|S|F} 부분에는 U, S, F 세 문자 중 하나를 지정할 수 있는데, U는 부호가 없는 정수형, S는 부호가 있는 정수형, F는 부동소수점 타입을 의미합니다. OpenCV 라이브러리에서는 행렬의 depth를 표현하기 위해 다음의 매크로 상수를 정의하여 사용합니다.
#define CV_8U 0 // unsigned char
#define CV_8S 1 // signed char
#define CV_16U 2 // unsigned short
#define CV_16S 3 // signed short
#define CV_32S 4 // int
#define CV_32F 5 // float
#define CV_64F 6 // double
#define CV_16F 7 // float16_t
Mat 행렬 원소는 하나의 값을 가지거나 여러 개로 구성된 값을 가질 수 있습니다. Mat 행렬 원소를 구성하는 각각의 값을 채널(channel)이라고 부르는데, Mat 행렬은 하나 이상의 채널을 가질 수 있습니다. 이때 하나의 행렬을 구성하는 각 채널은 모두 같은 자료형을 사용해야 합니다. 예를 들어 grayscale 영상은 하나의 픽셀이 밝기 정보 하나만 사용하므로 1채널 행렬 형태로 표현하며, 트루컬러 영상은 하나의 픽셀이 BGR, 3개의 색상 정보를 사용하므로 3채널 행렬로 표현합니다.
Mat 행렬의 depth와 채널 수 정보를 합쳐서 Mat 객체의 타입(type)이라고 부릅니다. OpenCV의 행렬 타입은 다음과 같은 형식의 매크로 상수로 표현합니다.
CV_<bit-depth>{U|S|F}C(<number_of_channels>)
즉, 채널의 depth를 표현하는 매크로에 이어서 C1, C3 같은 채널 정보가 추가로 붙어 있는 형태입니다. 예를 들어, CV8UC1 타입은 8비트 unsigned char 자료형을 사용하고 채널이 1개인 행렬을 의미합니다. BGR 3개의 색상 성분을 가지고 있는 경우에는 unsigned char 자료형을 사용하고 3개의 채널을 가지고 있으므로 CV_8UC3 타입입니다. 복소수와 같이 두 개의 실수 값을 사용하는 행렬은 CV_32FC2 타입으로 만들 수 있습니다.
행렬의 생성과 초기화
기본 생성자
가장 기본적인 Mat 객체의 생성 방법은 Mat 클래스의 기본 생성자를 이용하는 것입니다. 기본 생성자는 아무런 인자를 받지 않으며 실제 코드를 작성할 때는 Mat 클래스 타입의 변수를 선언하는 형태와 같습니다.
cv::Mat mat1;
이렇게 생성된 mat1은 비어 있는 행렬입니다. 즉, mat1.rows와 mat1.cols 값은 0이고, mat1.data에도 0(NULL)이 저장됩니다. 이렇게 비어 있는 행렬을 OpenCV 영상 처리 함수의 입력으로 사용하거나 또는 비어 있는 행렬의 원소 값을 참조하려고 하면 에러가 발생합니다.
Mat 객체를 생성함과 동시에 원소 값을 저장하기 위한 메모리 공간을 할당하려면 다음의 생성자를 사용합니다.
/** @overload
@param rows Number of rows in a 2D array.
@param cols Number of columns in a 2D array.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
*/
Mat(int rows, int cols, int type);
이 생성자는 행의 개수가 rows이고, 열의 개수가 cols인 2차원 행렬을 생성합니다. 이 생성자의 세 번째 인자 type에는 Mat 객체의 타입을 나타내는 매크로 상수를 전달합니다. 이 생성자를 이용해 가로 크기가 640, 세로 크기가 480인 행렬을 생성하려면 다음과 같은 형태로 작성합니다.
cv::Mat mat2(480, 640, CV_8UC1); // unsigned char, 1-channel
cv::Mat mat3(480, 640, CV_8UC3); // unsigned char, 3-channels
위 코드에서 mat2와 mat3은 서로 크기는 같지만 타입이 다릅니다.
Mat 클래스 생성자에서 행렬의 크기를 지정할 때 Size 클래스를 사용할 수도 있습니다.
/** @overload
@param size 2D array size: Size(cols, rows) . In the Size() constructor, the number of rows and the
number of columns go in the reverse order.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
*/
Mat(Size size, int type);
여기서 사용된 Size 클래스는 2차원 사각형의 가로, 세로 크기를 표현하기 위해 사용하는 OpenCV의 클래스이며 가로 크기가 640, 세로 크기가 480인 3채널 컬러 이미지를 저장하기 위한 행렬을 생성하려면 다음과 같이 작성합니다.
cv::Mat mat4(cv::Size(640, 480), CV_8UC3); // Size(width, height)
그런데 위에서 생성한 것처럼 행렬의 크기와 타입을 지정하여 Mat 객체를 생성하는 경우, 생성된 행렬의 모든 원소는 쓰레기 값으로 채워지게 됩니다. 따라서, Mat 객체를 생성하면서 동시에 모든 값을 특정 값으로 초기화하여 사용하는 것이 안전합니다. 정해진 크기와 타입의 Mat 객체를 생성하고 모든 원소값을 초기화하려면 다음의 생성자를 이용합니다.
/** @overload
@param rows Number of rows in a 2D array.
@param cols Number of columns in a 2D array.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
@param s An optional value to initialize each matrix element with. To set all the matrix elements to
the particular value after the construction, use the assignment operator
Mat::operator=(const Scalar& value) .
*/
Mat(int rows, int cols, int type, const Scalar& s);
/** @overload
@param size 2D array size: Size(cols, rows) . In the Size() constructor, the number of rows and the
number of columns go in the reverse order.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
@param s An optional value to initialize each matrix element with. To set all the matrix elements to
the particular value after the construction, use the assignment operator
Mat::operator=(const Scalar& value) .
*/
Mat(Size size, int type, const Scalar& s);
위 생성자들은 행렬의 크기와 타입을 지정하는 생성자에 원소의 초기값을 설정하는 인자 s가 추가되어 있습니다. s의 타입으로 사용된 Scalar 클래스는 4개의 실수 값을 저장할 수 있는 OpenCV의 클래스이며 주로 영상(이미지)의 픽셀 값을 표현하는 용도로 사용됩니다. grayscale의 픽셀값을 표현할 때는 하나의 멤버 변수만을 사용하고, 3채널 컬러를 표현할 때는 3개의 멤버 변수를 사용합니다. 예를 들어, 모든 픽셀값이 128로 초기화된 grayscale 이미지와 모든 픽셀이 빨간색으로 설정된 컬러 이미지의 행렬을 생성하려면 다음과 같이 작성할 수 있습니다.
cv::Mat mat5(480, 640, CV_8UC1, cv::Scalar(128));
cv::Mat mat6(480, 640, CV_8UC3, cv::Scalar(0,0,255));
(cv::Scalar 클래스를 이용하여 컬러 색상을 지정할 때는 BGR 순서로 값을 지정합니다.)
Mat::zeros()
새로운 행렬을 생성할 때 모든 원소 값을 0으로 초기화하는 경우가 많은데, 이러한 경우에 Scalar(0)을 지정하면 됩니다. 하지만 이러한 경우가 많기 때문에 이 용도를 위한 별도의 함수를 지원하며, 모든 원소가 0으로 초기화된 행렬을 만드는 함수의 이름은 Mat::zeros()입니다.
/** @brief Returns a zero array of the specified size and type.
The method returns a Matlab-style zero array initializer. It can be used to quickly form a constant
array as a function parameter, part of a matrix expression, or as a matrix initializer:
@code
Mat A;
A = Mat::zeros(3, 3, CV_32F);
@endcode
In the example above, a new matrix is allocated only if A is not a 3x3 floating-point matrix.
Otherwise, the existing matrix A is filled with zeros.
@param rows Number of rows.
@param cols Number of columns.
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr zeros(int rows, int cols, int type);
/** @overload
@param size Alternative to the matrix size specification Size(cols, rows) .
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr zeros(Size size, int type);
Mat::zeros()는 Mat 클래스의 static 멤버 함수이므로 실제 코드에서는 Mat::을 붙여서 사용해야 합니다. 그리고, 함수의 반환형인 MatExpr은 OpenCV에서 행렬의 대수 연산을 표현하는 클래스이며, 자동으로 Mat 클래스로 형변환됩니다. 따라서 이 함수의 반환값은 Mat 타입의 변수에 할당할 수 있습니다. 예를 들어, 0으로 초기화된 3x3 정수형 행렬을 생성하려면 다음과 같이 작성할 수 있습니다.
cv::Mat mat7 = Mat::zeros(3, 3, CV_32SC1);
Mat::ones(), Mat::eye()
행렬의 모든 원소가 1로 초기화된 행렬을 생성하려면 Mat::ones() 함수를 사용할 수 있습니다. 또한 행렬 연산에서 자주 사용되는 단위 행렬(identity matrix)를 생성하려면 Mat::eye() 함수를 사용할 수 있습니다. 이 함수들의 원형은 다음과 같습니다.
/** @brief Returns an array of all 1's of the specified size and type.
The method returns a Matlab-style 1's array initializer, similarly to Mat::zeros. Note that using
this method you can initialize an array with an arbitrary value, using the following Matlab idiom:
@code
Mat A = Mat::ones(100, 100, CV_8U)*3; // make 100x100 matrix filled with 3.
@endcode
The above operation does not form a 100x100 matrix of 1's and then multiply it by 3. Instead, it
just remembers the scale factor (3 in this case) and use it when actually invoking the matrix
initializer.
@note In case of multi-channels type, only the first channel will be initialized with 1's, the
others will be set to 0's.
@param rows Number of rows.
@param cols Number of columns.
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr ones(int rows, int cols, int type);
/** @overload
@param size Alternative to the matrix size specification Size(cols, rows) .
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr ones(Size size, int type);
/** @brief Returns an identity matrix of the specified size and type.
The method returns a Matlab-style identity matrix initializer, similarly to Mat::zeros. Similarly to
Mat::ones, you can use a scale operation to create a scaled identity matrix efficiently:
@code
// make a 4x4 diagonal matrix with 0.1's on the diagonal.
Mat A = Mat::eye(4, 4, CV_32F)*0.1;
@endcode
@note In case of multi-channels type, identity matrix will be initialized only for the first channel,
the others will be set to 0's
@param rows Number of rows.
@param cols Number of columns.
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr eye(int rows, int cols, int type);
/** @overload
@param size Alternative matrix size specification as Size(cols, rows) .
@param type Created matrix type.
*/
CV_NODISCARD_STD static MatExpr eye(Size size, int type);
위 함수들의 사용법은 Mat::zeros() 함수와 완전 동일하며, 생성되는 행렬의 초기값만 다릅니다.
cv::Mat mat8 = Mat::ones(3, 3, CV_32FC1); // 1's matrix
cv::Mat mat9 = Mat::eye(3, 3, CV_32FC1); // identity matrix
외부 메모리를 사용한 행렬 생성
Mat 객체를 생성할 때, 원소를 저장할 메모리 공간을 새로 할당하는 것이 아닌 기존에 이미 할당된 메모리 공간의 데이터를 사용할 수 있습니다. 외부 메모리 공간을 활용하여 Mat 객체를 생성하면 자체적인 메모리 할당을 수행하지 않고 외부 데이터를 참조하는 방식이기 때문에 객체 생성이 빠르다는 장점이 있습니다. 이러한 Mat 클래스의 생성자는 다음과 같습니다.
/** @overload
@param rows Number of rows in a 2D array.
@param cols Number of columns in a 2D array.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
@param data Pointer to the user data. Matrix constructors that take data and step parameters do not
allocate matrix data. Instead, they just initialize the matrix header that points to the specified
data, which means that no data is copied. This operation is very efficient and can be used to
process external data using OpenCV functions. The external data is not automatically deallocated, so
you should take care of it.
@param step Number of bytes each matrix row occupies. The value should include the padding bytes at
the end of each row, if any. If the parameter is missing (set to AUTO_STEP ), no padding is assumed
and the actual step is calculated as cols*elemSize(). See Mat::elemSize.
*/
Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
/** @overload
@param size 2D array size: Size(cols, rows) . In the Size() constructor, the number of rows and the
number of columns go in the reverse order.
@param type Array type. Use CV_8UC1, ..., CV_64FC4 to create 1-4 channel matrices, or
CV_8UC(n), ..., CV_64FC(n) to create multi-channel (up to CV_CN_MAX channels) matrices.
@param data Pointer to the user data. Matrix constructors that take data and step parameters do not
allocate matrix data. Instead, they just initialize the matrix header that points to the specified
data, which means that no data is copied. This operation is very efficient and can be used to
process external data using OpenCV functions. The external data is not automatically deallocated, so
you should take care of it.
@param step Number of bytes each matrix row occupies. The value should include the padding bytes at
the end of each row, if any. If the parameter is missing (set to AUTO_STEP ), no padding is assumed
and the actual step is calculated as cols*elemSize(). See Mat::elemSize.
*/
Mat(Size size, int type, void* data, size_t step=AUTO_STEP);
void* 타입의 인자인 data는 사용할 외부 데이터의 주소입니다. 그리고 step은 외부 행렬 데이터에서 한 행이 차지하는 바이트 수를 의미합니다. 만약 외부 데이터의 각 행에 여분의 padding이 존재한다면 명시적으로 지정해주어야 하며, 기본값인 AUTO_STEP을 사용하면 padding bytes가 없다고 가정합니다.
외부 메모리를 사용하여 Mat 객체를 생성하는 방법은 다음과 같습니다. 여기서는 여섯 개의 연소를 갖는 float 타입의 배열 data를 먼저 정의하고, 이 배열을 행렬 원소로 사용하는 Mat 객체를 생성합니다.
float data[] = { 1, 2, 3, 4, 5, 6 };
cv::Mat mat10(2, 3, CV_32FC1, data);
이처럼 외부 데이터를 행렬의 원소 값으로 사용하는 경우, 외부 데이터의 크기와 행렬 원소 개수는 동일해야 하며, 서로 사용하는 자료형도 같아야 합니다. 위의 코드에서 data 배열은 6개의 원소를 가지고 있고, mat10 행렬도 2행 3열이므로 원소가 6개로 일치합니다. 또한 모두 float 타입을 사용합니다. 따라서, mat10은 다음과 같은 형태를 같습니다.
\[\text{mat10} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}\]
당연하겠지만, 외부 메모리를 참조하여 Mat 객체를 생성하는 경우, Mat 객체의 원소 값과 외부 메모리 공간의 데이터 값이 서로 공유됩니다. 따라서 외부 메모리 공간의 값을 변경하면 mat10 행렬의 원소 값도 같이 변경됩니다. 동적 할당으로 만든 메모리 공간도 Mat 클래스에서 참조하여 사용할 수 있는데, 이 경우에는 Mat 객체가 소멸될 때 자동으로 해제되지 않으므로 사용자가 직접 메로리를 해제해야 합니다.
Mat_ 사용
사용자가 지정한 원소 값을 이용하여 Mat 객체를 생성하는 방법 중에 Mat_ 클래스를 사용하는 방법도 있습니다. Mat_ 클래스는 Mat 클래스를 상속하여 만든 템플릿 클래스로서 Mat_ 클래스 객체와 Mat 객체는 상호 변환이 가능합니다. 차이점으로 Mat_ 클래스는 '<<' 연산자와 콤마를 이용하여 간단하게 행렬 원소 값을 설정하는 인터페이스를 제공합니다. 따라서 먼저 Mat_ 객체를 만들어 << 연산자로 행렬 원소를 지정해주고, 이를 Mat 객체로 변환하여 사용하기도 합니다.
cv::Mat_<float> mat11_(2, 3);
mat11_ << 1, 2, 3, 4, 5, 6;
cv::Mat mat11 = mat11_;
이렇게 생성된 mat11 행렬은 2x3 크기이며, 타입은 CV32FC1이며 mat11_ 행렬과 원소를 공유합니다. 만약 행렬을 생성한 후 mat11_ 변수를 사용하지 않는다면 다음과 같이 한 줄로 간단하게 작성할 수도 있습니다.
cv::Mat mat11 = (cv::Mat_<float>(2, 3) << 1, 2, 3, 4, 5, 6);
C++11의 초기화 리스트(initializer list)를 사용하여 행렬을 초기화할 수도 있는데, Mat 클래스 또는 Mat_ 클래스의 생성자에 행렬 크기와 초기값을 중괄호를 이용한 리스트 형태로 전달하는 방식입니다. 다만 생성된 Mat 객체의 타입을 명시적으로 지정하기 위해 Mat_ 클래스 형식으로 생성한 후 Mat 타입으로 변경하는 것이 좋습니다.
cv::Mat12 = cv::Mat_<float>({2, 3}, {1, 2, 3, 4, 5, 6});
Mat::create()
비어 있는 Mat 객체 또는 이미 생성된 Mat 객체에 새로운 행렬을 할당하려면 Mat 클래스의 Mat::create() 멤버 함수를 사용할 수 있습니다. 이 함수의 원형은 다음과 같습니다.
void create(int rows, int cols, int type);
void create(Size size, int type);
이미 행렬 데이터가 할당되어 있는 Mat 객체에서 이 함수를 호출할 경우, 함수의 인자로 지정한 행렬의 크기와 타입이 기존 행렬과 모두 같으면 별다른 동작을 하지 않고 함수가 종료됩니다. 그러나 새로 만들 행렬의 크기 또는 타입이 기존 행렬과 다르다면, 일단 기존 메모리 공간을 해제한 후 새로운 행렬 데이터를 위한 메모리 공간을 할당합니다.
예를 들어, 위에서 생성한 mat10, mat11에 새로운 크기와 타입의 행렬을 할당하려면 다음과 같이 작성하면 됩니다.
mat10.create(256, 256, CV_8UC3); // 256x256, uchar, 3-channels
mat11.create(4, 4, CV_32FC1); // 4x4, float, 1-channel
Mat::create() 함수는 새로 만든 행렬의 원소 값을 초기화하는 기능이 없습니다. 따라서 이 함수를 이용하여 행렬을 생성한 후 행렬 전체 원소 값을 초기화하고 싶다면 별도의 함수를 사용해야 합니다. Mat 클래스는 '=' 연산자 또는 Mat::setTo() 멤버 함수를 이용하여 행렬 전체 원소 값을 한꺼번에 설정할 수 있습니다.
/** @brief Sets all or some of the array elements to the specified value.
@param s Assigned scalar converted to the actual array type.
*/
Mat& operator = (const Scalar& s);
/** @brief Sets all or some of the array elements to the specified value.
This is an advanced variant of the Mat::operator=(const Scalar& s) operator.
@param value Assigned scalar converted to the actual array type.
@param mask Operation mask of the same size as \*this. Its non-zero elements indicate which matrix
elements need to be copied. The mask has to be of type CV_8U and can have 1 or multiple channels
*/
Mat& setTo(InputArray value, InputArray mask=noArray());
Mat::setTo() 함수는 두 개의 인자를 가지고 있지만 두 번째 인사 mask는 기본값이 있어 생략할 수 있습니다. mask 인자를 생략하거나 mask 인자에 noArray() 또는 Mat()을 지정하면 행렬 전체 원소를 value 값으로 설정합니다. mask 인자는 행렬의 특정 영역에 대해서만 원소 값을 설정하고 싶을 때 사용할 수 있습니다.
사용하는 방법은 다음과 같습니다.
mat10 = cv::Scalar(255, 0, 0);
mat11.setTo(1.f);
위 코드는 mat10의 모든 픽셀을 파란색에 해당하는 값으로 설정하고, mat11의 모든 원소 값을 1.f로 설정합니다.
행렬의 복사
Mat 클래스 객체에 저장된 이미지 또는 행렬을 복사하는 가장 간단한 방법은 복사 생성자 또는 대입 연산자를 사용하는 것입니다. 먼저 복사 생성자를 이용하여 행렬을 복사하는 코드를 살펴보겠습니다.
cv::Mat img1 = cv::imread("dog.bmp");
cv::Mat img2 = img1; // 복사 생성자(얕은 복사)
위 코드는 img1과 같은 크기, 같은 타입의 새로운 Mat 객체 img2를 생성하고 img1의 픽셀 데이터를 img2가 참조하도록 설정합니다. 즉, img1과 img2는 하나의 이미지를 공유하는 서로 다른 이름의 변수 형태로 동작합니다. 이처럼 Mat 클래스의 복사 생성자는 행렬의 원소 데이터를 공유하는 얕은 복사(shallow copy)를 지원합니다.
Mat 클래스의 대입 연산자도 복사 연산자와 마찬가지로 얕은 복사를 수행합니다.
cv::Mat img3;
img3 = img1; // 대입 연산자(얕은 복사)
만약 복사본을 새로 생성할 때, 데이터를 공유하는 것이 아니라 메모리 공간을 새로 할당하여 픽셀 데이터 전체를 복사하고 싶다면, Mat::clone() 또는 Mat::copyTo() 함수를 사용해야 합니다. 이 함수들의 원형은 다음과 같습니다.
CV_NODISCARD_STD Mat clone() const;
void copyTo( OutputArray m ) const;
void copyTo( OutputArray m, InputArray mask ) const;
clone() 멤버 함수의 경우에는 *this 행렬의 복사본이 반환됩니다.
copyTo() 멤버 함수의 인자 m은 복사본이 저장될 행렬이며, *this 행렬과 크기 및 타입이 다르다면 메모리를 새로 할당한 후 픽셀 값을 복사합니다. mask 인자는 마스크 행렬이며, 마스크 행렬의 원소 값이 0이 아닌 좌표에서만 행렬 원소를 복사합니다.
Mat::clone() 함수는 자기 자신과 동일한 Mat 객체를 완전히 새로 만들어 반환합니다. Mat::copyTo() 함수는 인자로 전달된 m 행렬에 자기 자신을 복사합니다. 만약 copyTo()를 호출한 행렬과 인자로 전달된 행렬 m이 서로 크기와 타입이 다르다면 함수 내부에서 행렬 m을 새로 생성한 후 픽셀 값을 복사합니다.
이를 사용하는 방법은 다음과 같습니다.
cv::Mat img4 = img1.clone(); // 깊은 복사
cv::Mat img5;
img1.copyTo(img5); // 깊은 복사
위 코드에서 img4는 img1.clone() 함수에 의해 반환되는 행렬 객체를 저장합니다.
img5는 먼저 비어 있는 행렬로 생성되고, 이후 copyTo() 함수에 의해 img1에 저장된 이미지가 img5 객체로 복사됩니다. 결과적으로 img4와 img5는 각각 픽셀 데이터를 저장할 메모리 공간을 따로 가지고 있고, img1에 저장된 강아지 이미지의 복사본을 저장합니다(깊은 복사 수행).
아래 코드는 위에서 설명한 방법들을 보여줍니다. 다양한 방법으로 행렬을 복사한 뒤, img1 행렬의 모든 데이터 값을 노란색에 해당하는 값으로 설정합니다. 그리고 모든 행렬들을 화면에 출력합니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat img1 = cv::imread("dog.bmp");
cv::Mat img2 = img1;
cv::Mat img3;
img3 = img1;
cv::Mat img4 = img1.clone();
cv::Mat img5;
img1.copyTo(img5);
img1.setTo(cv::Scalar(0, 255, 255)); // yellow
cv::imshow("img1", img1);
cv::imshow("img2", img2);
cv::imshow("img3", img3);
cv::imshow("img4", img4);
cv::imshow("img5", img5);
cv::waitKey();
cv::destroyAllWindows();
}
결과를 살펴보면, img1만 노란색으로 설정했지만 img2, img3 또한 노란색으로 변경된 것을 확인할 수 있습니다. 반면 img4, img5는 깊은 복사를 수행하기 때문에 강아지 이미지를 그대로 유지하고 있습니다.
부분 행렬 추출
이번에는 이미지에서 사각형 모양의 부분 이미지를 추출하거나 참조하는 방법에 대해 살펴보겠습니다.
Mat 클래스로 정의된 행렬에서 특정 사각형 영역의 부분 행렬을 추출하고 싶을 때에는 Mat 클래스에 정의된 괄호 연산자 오버로딩을 사용합니다. 주로 사용되는 함수의 원형은 다음과 같습니다.
/** @brief Extracts a rectangular submatrix.
The operators make a new header for the specified sub-array of \*this . They are the most
generalized forms of Mat::row, Mat::col, Mat::rowRange, and Mat::colRange . For example,
`A(Range(0, 10), Range::all())` is equivalent to `A.rowRange(0, 10)`. Similarly to all of the above,
the operators are O(1) operations, that is, no matrix data is copied.
@param rowRange Start and end row of the extracted submatrix. The upper boundary is not included. To
select all the rows, use Range::all().
@param colRange Start and end column of the extracted submatrix. The upper boundary is not included.
To select all the columns, use Range::all().
*/
Mat operator()( Range rowRange, Range colRange ) const;
/** @overload
@param roi Extracted submatrix specified as a rectangle.
*/
Mat operator()( const Rect& roi ) const;
rowRange와 colRange는 추출하려는 영역의 행과 열 범위를 뜻하며, roi는 추출하려는 사각형 영역 범위입니다.
다음은 예제 코드입니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat img1 = cv::imread("cat.bmp");
cv::Mat img2 = img1(cv::Rect(220, 120, 340, 240));
cv::imshow("img1", img1);
cv::imshow("img2", img2);
cv::waitKey();
}
먼저 고양이 이미지를 3채널 컬러 형태로 불러와 img1에 저장합니다. 그리고 img1 변수 이름 뒤에 괄호를 붙여 사용했는데, 이 부분이 Mat 클래스의 괄호 연산자 오버로딩으로 동작합니다. 따라서 img1(cv::Rect(220, 120, 340, 240)) 코드는 img1 이미지의 (220, 120) 좌표부터 340x240 크기 만큼의 사각형 영역을 추출하게 되고, 이를 img2 변수에 저장합니다. img1과 img2를 화면에 출력해보면 다음과 같이 출력됩니다.
이렇게 부분 이미지를 추출할 때 주의할 점은 Mat 클래스의 괄호 연산자를 이용하여 얻은 부분 이미지가 독립된 메모리 공간을 확보하여 복사하는 깊은 복사가 아닌 데이터를 공유하는 얕은 복사라는 점입니다. 따라서 추출한 부분 영상의 픽셀을 변경하면 원본 이미지도 함께 변경됩니다.
이러한 특징을 이용하면 입력 이미지의 일부분에만 특정한 영상 처리를 수행할 수 있습니다. 간단하게 이미지 색상을 반전시키는 코드를 살펴보면 다음과 같습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat img1 = cv::imread("cat.bmp");
cv::Mat img2 = img1(cv::Rect(220, 120, 340, 240));
img2 = ~img2;
cv::imshow("img1", img1);
cv::imshow("img2", img2);
cv::waitKey();
}
img2 이미지를 반전하여 다시 img2에 저장한 뒤 화면에 출력하면 다음의 결과를 확인할 수 있습니다.
이처럼 Mat 클래스의 부분 이미지 참조 기능은 입력 이미지에 대해 사각형 모양의 관심 영역(RoI: Region of Interest)을 설정하는 용도로 사용할 수 있습니다. 사각형이 아닌 임의의 모양의 RoI를 설정하고 싶은 경우에는 마스크 연산을 응용할 수 있는데, 이에 대해서는 다른 포스팅에서 다루어 보도록 하겠습니다.
만약 독립된 메모리 영역을 확보하여 부분 이미지를 추출하고자 한다면, 괄호 연산자 뒤에 clone() 멤버 함수를 함께 사용하면 됩니다.
cv::Mat img3 = img1(cv::Rect(220, 120, 340, 240)).clone();
이처럼 코드를 작성하면 img1과 img3은 서로 다른 메모리 공간을 사용하며, img3의 픽셀 값을 변경해도 img1은 변경되지 않습니다.
Mat 행렬에서 특정 범위의 행 또는 열을 부분 행렬로 추출하고자 할 때에는 rowRange() 또는 colRange() 함수를 사용할 수 있습니다. rowRange() 함수는 지정한 범위의 행으로 구성된 행렬을 반환하고, colRange() 함수는 지정한 범위의 열로 구성된 행렬을 반환합니다. 행 또는 열의 범위는 두 개의 int 값으로 지정할 수도 있고, cv::Range 클래스 객체를 사용하여 지정할 수도 있습니다.
함수 원형은 다음과 같습니다.
/** @brief Creates a matrix header for the specified row span.
The method makes a new header for the specified row span of the matrix. Similarly to Mat::row and
Mat::col , this is an O(1) operation.
@param startrow An inclusive 0-based start index of the row span.
@param endrow An exclusive 0-based ending index of the row span.
*/
Mat rowRange(int startrow, int endrow) const;
/** @overload
@param r Range structure containing both the start and the end indices.
*/
Mat rowRange(const Range& r) const;
/** @brief Creates a matrix header for the specified column span.
The method makes a new header for the specified column span of the matrix. Similarly to Mat::row and
Mat::col , this is an O(1) operation.
@param startcol An inclusive 0-based start index of the column span.
@param endcol An exclusive 0-based ending index of the column span.
*/
Mat colRange(int startcol, int endcol) const;
/** @overload
@param r Range structure containing both the start and the end indices.
*/
Mat colRange(const Range& r) const;
위 함수들도 모두 부분 행렬을 얕은 복사 형태로 반환합니다. 따라서 메모리를 따로 할당하여 저장하려면 clone() 함수와 함께 사용해야 합니다.
행렬의 원소 값 참조
OpenCV는 Mat 클래스에 저장된 행렬 원소 값을 참조하고 값을 변경할 수 있는 다양한 인터페이스를 제공합니다. 이번에는 행렬 원소 값에 접근할 수 있는 3가지 방법에 대해 살펴보도록 하겠습니다.
Mat::at()
가장 직관적인 방법은 at() 멤버 함수를 사용하는 것입니다. 이 함수는 보통 행과 열을 나타내는 두 개의 정수를 인자로 받아 해당 위치의 행렬 원소 값을 참조 형식으로 반환합니다. 이 함수는 템플릿을 사용하는 템플릿 함수로서 여러 가지 형태로 재정의 되어 있으며, 주로 사용하는 at() 함수는 다음과 같습니다.
/** @overload
@param row Index along the dimension 0
@param col Index along the dimension 1
*/
template<typename _Tp> _Tp& at(int row, int col);
이 함수는 템플릿 함수로 정의되어 있기 때문에 사용할 때 행렬 원소 자료형을 명시적으로 지정해야 합니다. 예를 들어, Mat 행렬의 타입이 CV_8UC1이라면 uchar 타입을 지정하고 CV_32FC1 타입의 행렬이라면 float 타입을 지정해야 합니다. 만약 CV_8UC3 타입을 사용하는 3채널 이미지라면 OpenCV에서 정의한 Vec3b 타입을 명시해서 사용해야 합니다.
다음은 간단한 예제 코드입니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat mat1 = cv::Mat::zeros(3, 4, CV_8UC1);
for (int r = 0; r < mat1.rows; r++) {
for (int c = 0; c < mat1.cols; c++) {
mat1.at<uchar>(r, c)++;
}
}
std::cout << mat1 << std::endl;
}
위 코드에서 mat1은 0으로 초기화된 3x4 행렬이고, 각 원소는 uchar 타입을 사용합니다. 따라서 Mat::at() 함수를 사용할 때 <> 괄호 안에 uchar 타입을 명시하였습니다. 이 함수에서는 for문을 통해 행렬의 각 원소를 모두 참조하면서 1씩 증가시킵니다.
at() 함수가 참조하는 행렬 원소 위치는 다음과 같습니다.
Mat::ptr()
두 번째 방법은 ptr() 멤버 함수를 이용하는 방법입니다. 이 함수는 Mat 행렬의 특정 행의 첫 번째 원소 주소를 반환합니다. 이 함수 역시 여러 형태로 재정의되어 있지만, 가장 널리 사용되는 함수 형태는 다음과 같습니다.
template<typename _Tp> _Tp* ptr(int i0=0);
i0 인자는 참조할 행 번호이며, _Tp* 타입으로 형 변환된 i0번째 행의 시작 주소를 반환합니다.
이 함수를 사용하여 행렬 원소 값을 참조하는 방법은 다음과 같습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat mat1 = cv::Mat::zeros(3, 4, CV_8UC1);
for (int r = 0; r < mat1.rows; r++) {
uchar* p = mat1.ptr<uchar>(r);
for (int c = 0; c < mat1.cols; c++) {
p[c]++;
}
}
std::cout << mat1 << std::endl;
}
MatIterator_
at() 또는 ptr() 함수를 사용하여 행렬의 원소를 참조하는 경우, 함수 인자로 전달된 값이 행렬의 크기를 벗어나면 에러가 발생합니다. 따라서 코드를 작성할 때 사용자가 행렬의 크기를 충분히 고려해야 하며, 주의하지 않으면 프로그램이 예기치 않게 종료될 수 있습니다. 이러한 단점을 해소하기 위해 이터레이터 개념을 도입하여 행렬을 참조할 수 있는 방법을 제공합니다.
Mat 클래스와 함께 사용할 수 있는 이터레이터 클래스 이름은 MatIterator_입니다. 이 클래스 또한 템플릿으로 정의된 클래스이므로 실제 사용할 때에는 Mat 행렬 타입에 맞는 타입을 명시하여 사용해야 합니다. 사용 방법은 C++의 이터레이터 사용 방법과 유사한데, Mat::begin() 함수를 이용하여 행렬의 첫 번째 위치를 얻을 수 있고, Mat::end() 함수를 이용하여 마지막 원소 바로 다음 위치를 얻을 수 있습니다.
예제 코드는 다음과 같습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat mat1 = cv::Mat::zeros(3, 4, CV_8UC1);
for (cv::MatIterator_<uchar> it = mat1.begin<uchar>(); it != mat1.end<uchar>(); ++it) {
(*it)++;
}
std::cout << mat1 << std::endl;
}
이터레이터를 사용하는 예제 코드에서는 행렬 mat1의 가로 및 세로 크기를 참조하는 부분이 없습니다. 즉, 이터레이터를 사용하면 행렬의 가로 또는 세로 크기에 상관없이 행렬의 모든 원소를 안전하게 방문할 수 있습니다. 다만, 이터레이터를 사용하는 방법은 동작 속도 면에서 ptr()보다는 느린 편이고 at() 함수처럼 임의의 위치에 자유롭게 접근할 수 없기 때문에 사용성은 높지 않습니다.
행렬 정보 참조
이번에는 Mat 클래스로 만든 행렬 객체의 다양한 정보를 참조하는 방법에 대해 살펴보겠습니다.
Mat 객체에서 가장 자주 참조하는 정보는 행렬의 크기 정보입니다. Mat::rows 멤버 변수는 행렬의 행 개수를 나타내고, Mat::cols는 열 개수를 나타냅니다. Mat 객체에 영상이 저장된 경우라면 rows는 영상의 세로 픽셀 크기이며 cols는 가로 픽셀 크기를 나타냅니다. 이들 멤버 변수는 public이므로 클래스 외부에서 자유롭게 접근할 수 있습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat img1 = cv::imread("dog.bmp");
std::cout << "Width: " << img1.cols << std::endl;
std::cout << "Height: " << img1.rows << std::endl;
}
dog.bmp 파일을 읽어, 가로/세로 크기를 출력하는 코드입니다.
Mat 클래스의 data 멤버 변수는 행렬 원소 데이터가 저장되어 있는 메모리 공간의 시작 주소를 가리키는 포인터입니다. data 멤버 변수가 가리키는 메모리 주소를 활용하면 행렬 원소를 사용자가 직접 참조할 수 있습니다. 그러나 포인터 연산을 잘못 사용하면 에러가 발생할 수 있으므로 직접 data 변수를 사용하는 것보다 위에서 언급한 at() 또는 ptr() 등의 함수를 사용하는 것이 좋습니다.
rows와 cols 멤버 변수에 저장된 크기 정보 외에ㅣ 다른 행렬 정보는 Mat 클래스에서 제공하는 멤버 함수를 이용하여 참조할 수 있습니다. 행렬의 정보 확인을 위한 함수들은 다음과 같습니다. 이 함수들은 모두 const이며 객체 정보를 확인하는 용도로만 사용됩니다.
Mat 클래스 멤버 함수 | 설명 |
int Mat::channels() const | 행렬의 채널 수 반환 |
int Mat::depth() const | 행렬의 depth를 반환. ex) CV_8U, CV_32F |
size_t Mat::elemSize() const | 한 개의 원소가 차지하는 메모리 크기를 바이트 단위로 반환 (CV_32SC3 타입 행렬인 경우 4x3=12를 반환) |
size_t Mat::elemSize1() const | 하나의 채널에서 한 개의 원소가 차지하는 메모리 크기를 바이트 단위로 반환 (CV_32SC3 타입 행렬인 경우 4를 반환) |
bool Mat::empty() const | 비어 있는 행렬이라면 true를 반환 |
bool Mat::isContinuous() const | 각 행의 원소가 연속적으로 저장되어 있으면 true를 반환 |
bool Mat::isSubmatrix() const | 행렬이 다른 행렬의 부분 행렬이면 true를 반환 |
Size Mat::size() const | 행렬 크기를 Size 타입으로 반환 |
size_t Mat::total() const | 전체 원소 개수를 반환 |
int Mat::type() const | 행렬의 타입을 반환. ex) CV_32FC1, CV_8UC3 |
예를 들어, 현재 다루고 있는 이미지가 grayscale인지 혹은 3채널 컬러인지 확인하려면 Mat::type() 멤버 함수를 이용할 수 있습니다. 일반적으로 grayscale은 CV_8UC1 타입을 사용하며, 3채널 컬러는 CV_8UC3을 사용합니다. 따라서 다음과 같이 확인할 수 있습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat img1 = cv::imread("dog.bmp");
if (img1.type() == CV_8UC1) {
std::cout << "img1 is a grayscale image.\n";
}
else if (img1.type() == CV_8UC3) {
std::cout << "img1 is a truecolor image.\n";
}
}
행렬 연산
일반적으로 OpenCV에서 Mat 클래스는 이미지나 영상을 표현하는 용도로 많이 사용되지만, 일반적인 행렬 표현과 행렬 연산을 위한 기능도 충분히 제공합니다.
먼저 행렬의 사칙연산부터 살펴보면, OpenCV에서는 Mat 클래스가 표현하는 행렬을 수학 수식을 쓰듯이 사용할 수 있도록 다양한 연산자 오버로딩을 제공합니다. 이를 통해 행렬의 덧셈 또는 곱셈 연산을 수행할 수 있고, 행렬에 상수 값을 곱하는 연산을 수행할 수도 있습니다. 행렬 사칙연산을 위한 주요 함수는 다음과 같습니다.
CV_EXPORTS MatExpr operator + (const Mat& a, const Mat& b);
CV_EXPORTS MatExpr operator + (const Mat& a, const Scalar& s);
CV_EXPORTS MatExpr operator + (const Scalar& s, const Mat& a);
CV_EXPORTS MatExpr operator - (const Mat& a, const Mat& b);
CV_EXPORTS MatExpr operator - (const Mat& a, const Scalar& s);
CV_EXPORTS MatExpr operator - (const Scalar& s, const Mat& a);
CV_EXPORTS MatExpr operator - (const Mat& m);
CV_EXPORTS MatExpr operator * (const Mat& a, const Mat& b);
CV_EXPORTS MatExpr operator * (const Mat& a, double s);
CV_EXPORTS MatExpr operator * (double s, const Mat& a);
CV_EXPORTS MatExpr operator / (const Mat& a, const Mat& b);
CV_EXPORTS MatExpr operator / (const Mat& a, double s);
CV_EXPORTS MatExpr operator / (double s, const Mat& a);
여기서 곱셈은 일반적으로 알고 있는 행렬의 수학적 곱셈 연산을 의미하며, 나눗셈은 행렬의 같은 위치 원소끼리 나눗셈 연산을 수행합니다. 또한 피연산자에 스칼라나 double 타입이 오는 경우에는 행렬의 각 원소 전체에 대해 스칼라 값 연산을 수행합니다.
행렬 곱셈에서 같은 위치에 있는 원소끼리 곱셈 연산을 수행하려면 Mat::mul() 멤버 함수를 사용해야 합니다.
/** @brief Performs an element-wise multiplication or division of the two matrices.
The method returns a temporary object encoding per-element array multiplication, with optional
scale. Note that this is not a matrix multiplication that corresponds to a simpler "\*" operator.
Example:
@code
Mat C = A.mul(5/B); // equivalent to divide(A, B, C, 5)
@endcode
@param m Another array of the same type and the same size as \*this, or a matrix expression.
@param scale Optional scale factor.
*/
MatExpr mul(InputArray m, double scale=1) const;
행렬과 관련된 중요한 연산 중에 역행렬(inverse matrix)을 구하는 연산도 있습니다. 행렬의 역행렬을 구할 때에는 Mat::inv() 멤버 함수를 사용합니다.
/** @brief Inverses a matrix.
The method performs a matrix inversion by means of matrix expressions. This means that a temporary
matrix inversion object is returned by the method and can be used further as a part of more complex
matrix expressions or can be assigned to a matrix.
@param method Matrix inversion method. One of cv::DecompTypes
*/
MatExpr inv(int method=DECOMP_LU) const;
method에는 역행렬 계산 방법을 지정합니다. 역행렬이 존재하는 일반적인 행렬이라면 가우스 소거법을 사용하는 DECOMP_LU를 사용할 수 있으며 기본값입니다. 역행렬이 존재하지 않는 경우 DECOMP_SVD를 지정하는데, 특이값 분해 방법을 사용하여 의사 역행렬(pseudo-inverse matrix)를 구할 수 있씁니다. DECOMP_EIG와 DECOMP_CHOLESKY는 각각 고유값 분해와 Cholesky 분해에 의한 역행렬 계산을 의미합니다.
행렬의 행과 열을 서로 교환해서 만드는 전치 행렬(transpose matrix)은 Mat::t() 멤버 함수를 이용하여 구할 수 있습니다.
/** @brief Transposes a matrix.
The method performs matrix transposition by means of matrix expressions. It does not perform the
actual transposition but returns a temporary matrix transposition object that can be further used as
a part of more complex matrix expressions or can be assigned to a matrix:
@code
Mat A1 = A + Mat::eye(A.size(), A.type())*lambda;
Mat C = A1.t()*A1; // compute (A + lambda*I)^t * (A + lamda*I)
@endcode
*/
MatExpr t() const;
만약 전치 행렬 연산의 입력 행렬이 정방 행렬이면 단순히 행렬의 원소의 행과 열 위치만 서로 바꿉니다.
위에서 언급한 연산들을 사용한 예제 코드는 다음과 같습니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
float data[] = { 1, 1, 2, 3 };
cv::Mat mat1(2, 2, CV_32FC1, data);
std::cout << "mat1:\n" << mat1 << std::endl;
cv::Mat mat2 = mat1.inv();
std::cout << "mat2:\n" << mat2 << std::endl;
std::cout << "mat1.t():\n" << mat1.t() << std::endl;
std::cout << "mat1 + 3:\n" << mat1 + 3 << std::endl;
std::cout << "mat1 + mat2:\n" << mat1 + mat2 << std::endl;
std::cout << "mat1 * mat2:\n" << mat1 * mat2 << std::endl;
}
크기 및 타입 변환
이번에는 Mat 클래스의 크기 또는 타입을 변환하는 멤버 함수에 대해 살펴보겠습니다.
Mat::convertTo()
먼저 행렬의 타입을 변경할 때는 Mat::convertTo() 함수를 사용합니다.
/** @brief Converts an array to another data type with optional scaling.
The method converts source pixel values to the target data type. saturate_cast\<\> is applied at
the end to avoid possible overflows:
\f[m(x,y) = saturate \_ cast<rType>( \alpha (*this)(x,y) + \beta )\f]
@param m output matrix; if it does not have a proper size or type before the operation, it is
reallocated.
@param rtype desired output matrix type or, rather, the depth since the number of channels are the
same as the input has; if rtype is negative, the output matrix will have the same type as the input.
@param alpha optional scale factor.
@param beta optional delta added to the scaled values.
*/
void convertTo( OutputArray m, int rtype, double alpha=1, double beta=0 ) const;
이 함수는 행렬 원소의 타입을 다른 타입으로 변경하고, 추가적으로 모든 원소에 일정한 값을 더하거나 곱할 수 있습니다. Mat::convertTo() 함수에 의해 생성되는 출력 행렬 m의 원소값은 다음 수식에 의해 결정됩니다.
m(x, y) = saturate_cast<rtype>(alpha x (*this)(x, y) + beta)
일반적인 영상은 각 픽셀 값을 uchar 타입을 이용하여 표현하는데, 복잡한 연산을 수행해야 할 경우 연산의 정확도를 위해 픽셀 값을 정수가 아닌 float, double과 같은 실수형으로 변환하여 연산을 수행하기도 합니다. 이러한 경우에 위 함수를 사용하여 CV_8UC1 타입의 행렬을 CV_32FC1 타입으로 변경할 수 있습니다. 또는 0부터 1사이의 실수 값으로구성된 2차원 행렬을 화면에 출력하고 싶을 때, 행렬의 모든 원소에 255를 곱한 후 uchar 타입으로 변환하여 grayscale 형식으로 만든 후 출력할 수 있습니다.
아래 예제 코드는 grayscale로 이미지를 불러온 후, uchar 타입 대신 float 타입을 사용하는 행렬 img1f를 생성하는 예제 코드입니다.
cv::Mat img1 = cv::imread("cat.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat img1f;
img1.convertTo(img1f, CV_32FC1);
Mat::reshape()
다음은 Mat::reshape() 함수를 살펴보겠습니다. 이 함수는 주어진 행렬의 크기 또는 채널 수를 변경합니다. 다양한 형식으로 오버로딩되어 있으며, 주로 사용되는 것은 다음과 같습니다.
/** @brief Changes the shape and/or the number of channels of a 2D matrix without copying the data.
The method makes a new matrix header for \*this elements. The new matrix may have a different size
and/or different number of channels. Any combination is possible if:
- No extra elements are included into the new matrix and no elements are excluded. Consequently,
the product rows\*cols\*channels() must stay the same after the transformation.
- No data is copied. That is, this is an O(1) operation. Consequently, if you change the number of
rows, or the operation changes the indices of elements row in some other way, the matrix must be
continuous. See Mat::isContinuous .
For example, if there is a set of 3D points stored as an STL vector, and you want to represent the
points as a 3xN matrix, do the following:
@code
std::vector<Point3f> vec;
...
Mat pointMat = Mat(vec). // convert vector to Mat, O(1) operation
reshape(1). // make Nx3 1-channel matrix out of Nx1 3-channel.
// Also, an O(1) operation
t(); // finally, transpose the Nx3 matrix.
// This involves copying all the elements
@endcode
@param cn New number of channels. If the parameter is 0, the number of channels remains the same.
@param rows New number of rows. If the parameter is 0, the number of rows remains the same.
*/
Mat reshape(int cn, int rows=0) const;
위 함수는 행렬 원소 데이터를 복사하여 새로운 행렬을 만드는 것이 아니라 하나의 행렬 원소 데이터를 같이 참조하는 행렬을 반환합니다. 따라서 이 함수에 의해 반환된 행렬 원소 값을 변경하면 원본 행렬의 값도 함께 바뀌게 됩니다.
다음 예제 코드는 3x4 크기의 행렬을 1x12 크기의 행렬로 변환하는 예제 코드입니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
uchar data[] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
cv::Mat mat1(3, 4, CV_8UC1, data);
cv::Mat mat2 = mat1.reshape(0, 1);
std::cout << "mat1:\n" << mat1 << std::endl;
std::cout << "mat2:\n" << mat2 << std::endl;
}
Mat::resize()
reshape() 멤버 함수처럼 행렬의 모양을 변경시키는 것이 아니라 단순히 행렬의 행 크기를 변경하고 싶을 때는 Mat::resize() 함수를 사용할 수 있습니다.
/** @brief Changes the number of matrix rows.
The methods change the number of matrix rows. If the matrix is reallocated, the first
min(Mat::rows, sz) rows are preserved. The methods emulate the corresponding methods of the STL
vector class.
@param sz New number of rows.
*/
void resize(size_t sz);
/** @overload
@param sz New number of rows.
@param s Value assigned to the newly added elements.
*/
void resize(size_t sz, const Scalar& s);
resize() 함수는 행렬의 행 개수를 sz개로 변경합니다. 만약 sz가 기존 행렬의 행 개수보다 작으면 아래쪽 행을 제거하고, 기존 행렬의 행 개수보다 크다면 아래쪽에 행을 추가합니다. 이때 추기하는 행 원소의 초기값으로 s를 지정할 수 있습니다.
예를 들어, 위에서 생성한 mat1 행렬의 행을 5로 증가시키면 다음과 같이 작성합니다.
mat1.resize(5, 100);
기존 3x4 행렬이었던 mat1 행렬은 위 코드를 통해 5x4 크기로 변경되고, 새로 추가되는 행의 원소는 모두 100으로 설정됩니다.
Mat::push_back()
이미 존재하는 행렬에 원소 데이터를 추가하고 싶을 때에는 Mat::push_back() 멤버 함수를 사용할 수 있습니다. 이 함수 역시 아래와 같이 다양하게 정의되어 있습니다.
/** @brief Adds elements to the bottom of the matrix.
The methods add one or more elements to the bottom of the matrix. They emulate the corresponding
method of the STL vector class. When elem is Mat , its type and the number of columns must be the
same as in the container matrix.
@param elem Added element(s).
*/
template<typename _Tp> void push_back(const _Tp& elem);
/** @overload
@param elem Added element(s).
*/
template<typename _Tp> void push_back(const Mat_<_Tp>& elem);
/** @overload
@param elem Added element(s).
*/
template<typename _Tp> void push_back(const std::vector<_Tp>& elem);
/** @overload
@param m Added line(s).
*/
void push_back(const Mat& m);
push_back()의 함수 인자로 _Tp& 또는 std::vector<_Tp>& 타입을 사용할 경우, *this 행렬은 1열짜리 행렬이어야 합니다. 만약 Mat_<_Tp>& 또는 Mat& 타입을 인자로 사용할 경우에는 *this 행렬과 인자로 전달된 m 행렬의 열 개수가 같아야 합니다. 위에 나열한 4가지 push_back() 함수 모두 *this 행렬의 타입과 인자로 전달된 데이터의 타입은 같아야 합니다.
다음은 push_back() 함수를 이용하여 3x4 크기의 행렬 mat1에 1x4 크기의 행렬 mat3을 맨 마지막 행으로 추가하는 예제 코드입니다.
cv::Mat mat3 = cv::Mat::ones(1, 4, CV_8UC1) * 255;
mat1.push_back(mat3);
위 코드 실행 후 mat1 행렬은 다음과 같이 변경됩니다.
Mat::pop_back()
push_back()과 반대로 행렬의 맨 아래에 있는 행을 제거할 때는 pop_back() 멤버 함수를 사용할 수 있습니다.
/** @brief Removes elements from the bottom of the matrix.
The method removes one or more rows from the bottom of the matrix.
@param nelems Number of removed rows. If it is greater than the total number of rows, an exception
is thrown.
*/
void pop_back(size_t nelems=1);
'프로그래밍 > OpenCV' 카테고리의 다른 글
[OpenCV] 직선, 도형, 문자 그리기 (0) | 2022.05.04 |
---|---|
[OpenCV] 카메라 & 동영상 파일 처리하기 (0) | 2022.05.03 |
[OpenCV] Vec, Scalar, InputArray/OutputArray (0) | 2022.05.03 |
[OpenCV] 기본 자료형 클래스 (0) | 2022.05.01 |
[OpenCV] OpenCV 4 설치 (설치 파일 사용) (0) | 2022.04.30 |
댓글