OpenCV를 사용하는 기초적인 방법에 대해 공부했다. 나중에 ROS에서 사용해야 하므로, Docker 내부에서 시스템을 구성해 학습을 진행했다. Docker 환경을 구성하는 것은 이전 블로그 글에서 확인할 수 있고, CMakeListsts.txt와 빌드 설정을 구성하는 것은 같이 수업을 듣는 동료분께서 포스팅한 글을 참고했다.
우분투에서 OpenCV를 사용하기 위해서는 CMakeLists.txt에 OpenCV를 Dependency 모듈로 설정하는 과정이 필요하다.
...
# OpenCV 패키지 찾기
find_package(OpenCV REQUIRED)
# OpenCV의 폴더를 본 프로그램에 include하도록 설정
include_directories(${OpenCV_INCLUDE_DIRS})
...
cpp 소스파일에서는 다음과 같은 OpenCV 헤더를 통해 opencv를 사용할 수 있도록 설정할 수 있으며, namespace를 지정해주면 보다 쉽게 사용할 수 있다.
...
#include "opencv2/opencv.hpp"
...
OpenCV에서 데이터를 불러오기 위해 필요한 함수는 다음과 같다.
# 영상 데이터를 Mat 객체로 불러옴
Mat imread(const String& filename, int flags = IMREAD_COLOR);
--- filename: 이미지 파일 경로
--- flag: 이미지 타입(IMREAD_COLOR, IMREAD_GRAYSCALE, IMREAD_UNCHANGED)
# Mat 객체를 영상 파일로 저장함
bool imwrite(const String& filename, InputArray img, const std::vector<int>& params = std::vector<int>());
--- filename: 이미지 파일 경로
--- img: Mat 객체
--- params: 이미지를 저장하는 옵션(파일 확장자 등)
# Mat 객체가 비어있는지 확인
bool Mat::empty() const
# 새 창 띄우기
void namedWindow(const String& winname, int flags = WINDOW_AUTOSIZE);
--- winname: 창 이름
--- flag: 창 속성(WINDOW_NORMAL, WINDOW_AUTOSIZE, WINDOW_OPENGL)
# 창 없애기(일부 창 또는 전체)
void destoryWindow(const String& winname);
void destoryAllWindows();
# 창 위치 지정
void moveWindow(const String& winname, int x, int y);
# 창 크기 지정
void resizeWindow(const String& winname, int width, int height);
# 영상 출력하기
void imshow(const String& winname, InputArray mat);
# 키보드 입력 대기
int waitKey(int delay = 0);
다음과 같은 코드를 통해 lenna.bmp 파일을 불러와 화면에 출력할 수 있다. 나 같은 경우에는 도커에서 환경을 설정해서인지 waitKey()에 0이란 인자값을 주지 않으면, 바로 창이 꺼지는 문제가 있었다.
// 필요한 헤더 include
#include <bits/stdc++.h>
#include "opencv2/opencv.hpp"
// namespace 지정
using namespace std;
using namespace cv;
int main(int argc, char const *argv[]){
// lenna.bmp를 읽어서 Mat 객체로 저장
Mat img = imread("lenna.bmp");
// image가 비어있는지 확인
if(img.empty()){
cerr << "Image load failed!" << endl;
return -1;
}
namedWindow("images"); // window 이름을 images로 지정
imshow("image", img); // img 객체를 images라는 창으로 출력
waitKey(0); // 키 입력 대기
destroyAllWindows(); // 모든 창 삭제
return 0;
}
OpenCV에서 Drawing을 위해 필요한 주요 클래스들은 다음과 같다. Point, Size, Rect 객체 모두 연산자를 지원하며, Rect 객체의 경우 Size와 Point 객체와는 덧셈과 뺄셈, Rect 객체끼리는 논리 연산을 지원한다.
# Point_ 클래스: 2차원 점 좌표 표현을 위한 클래스
template<typename _Tp> class Point_){
public:
...
_Tp x, y;
};
typedef Point_<int> Point2i;
typedef Point_<int64> Point2l;
typedef Point_<float> Point2f;
typedef Point_<double> Point2d;
typedef Point2i Point;
# Size_ 클래스: 영상 또는 사각형의 크기 표현을 위한 템플릿 클래스
template<typename _Tp> class Size_{
public:
...
_Tp width, height;
};
...
# Rect_ 클래스: 2차원 사각형 표현을 위한 템플릿 클래스
template<typename _Tp> class Rect_{
public:
...
_Tp x, y, width, height;
};
...
OpenCV로 생성된 객체를 다루기 위한 주요 클래스들은 다음과 같다. Vector는 수학적인 Vector가 아닌, C++에서 다루는 std::vector와 유사한 기능을 제공한다. Scalar 클래스는 4 크기의 double 배열을 갖고 있기에 4채널 이하의 영상 데이터를 표현하는 용도로 사용된다.
# Range 클래스: start 위치부터 end 직전 범위를 표현하기 위한 클래스
class Range{
public:
Rnage();
Range(int _start, int _end);
int size() const;
bool empty() consta;
static Range all();
int start, end;
};
# String 클래스: OpenCV 자체적으로 정의해 사용하는 문자열 클래스(4.x 버전부터는 std::string 클래스로 대체)
typedef std::String cv::String;
# C의 printf()와 같은 형식있는 문자열 생성이 가능함
String filename = format("test%02d.bmp", x);
template<typename_ Tp, int m, int n> class Matx{
public:
...
_Tp val[m * x];
};
template<typename _Tp, int cn> class Vec: public Matx<_Tp, cn, 1>{
public:
...
const _Tp& operator [](int i) const;
_Tp& operator[](int i);
};
template<typename _Tp> class Scalar_: public Vec<_Tp, 4>{
public:
Scalar_();
Scalar_(_Tp v0, _Tp v1, _Tp v2 = 0, _Tp v3 = 0);
Scalar_(_Tp v0);
static Scalar_<_Tp> all(_Tp v0);
...
};
typedef Scalar_<double> Scalar;
Mat 클래스는 다채널 행렬을 표현을 위한 클래스이다. 수학적으로 정의되는 행렬과 동일하며, 수학적인 행렬 연산을 지원한다. 영상처리에서는 그레이스케일이나 트루컬러 영상을 표현할 때 사용된다.
class Mat{
public:
Mat();
Mat(int rows, int cols, int type);
...
void create(int rows, int cols, int type);
Mat& operator = (const Mat& m);
Mat clone() const;
void copyTo(OutputArray m) const;
template<typename _Tp> _Tp* ptr(int i0 = 0);
template<typename _Tp> _Tp& at(int row, int col);
...
int dims; // 행렬의 차원 수(영상은 보통 2)
int rows, cols; // 2차원 행렬에 대한 rows, cols 크기
unchar* data; // 실제 데이터(원소 데이터)
...
};
Mat 관련 클래스는 다음과 같이 사용할 수 있다.
Mat img = imread("lenna.bmp");
cout << "Width: " << img.cols << endl;
cout << "Height: " << img.rows << endl;
cout << "Channels: " << img.channels() << endl;
if(img.type() == CV_8UC1) // 8-bit unsigned, 1 channel
...
else if(img.type() == CV_8UC3) // 8-bit unsigned, 3 channel
InputArray 클래스는 주로 Mat클래스를 대체하는 Proxy 클래스로, CV에서 사용되는 함수에서 대부분의 인자는 Mat이 아닌 InputArray 형태로 구성되어 있다. Proxy 클래스이므로, 사용자가 명시적으로 InputArray 객체를 생성할 수 없다.
typedef const _InputArray& InputArray;
typedef InputArray InputArrayOfArrays;
OutputArray 클래스또한 Mat 클래스를 대체하는 Proxy 클래스이다. InputArray와 유사하지만, 출력용으로 사용되는 CV 함수의 인자가 OutputArray로 구성되어 있다. 새로운 행렬을 생성하는 create() 함수가 정의되어 있다.
typedef const _OutputArray& OutputArray;
typedef OutputArray OutputArrayOfArrays;
입력과 출력을 모두 수행하는 CV 함수에서는 InputOutputArray 클래스로 인자가 구성되어있다.
Mat 클래스는 다음과 같은 방식으로 생성할 수 있다. 아래 방식 외에도 다양한 방식을 지원한다. 배열을 만든 뒤 Mat으로 생성할 수도 있고, Scalar를 사용해도 된다.
float data[] = {...}
Mat img1(480, 640, CV_8UC1, data);
Mat 객체는 operator= 연산자를 지원하며, 이 경우 Shallow Copy로 진행된다. Deep Copy를 하고 싶으면, copyTo() 메소드 또는 clone() 메소드를 사용하면 되며, setTo()와 같이 기존 객체가 아닌 Scalar 등을 통해 설정도 가능하다.
# Shallow Copy
Mat img2 = img;
# Deep Copy
img2 = img1.clone();
img1.copyTo(img2);
# Mat Set
img1.setTo(Scalar(0, 255, 255));
Mat 클래스는 행렬이기에, 행렬 연산을 지원한다. 또한, 행렬에 접근하기 위한 메소드들을 다양하게 지원한다.
원소에 대해 직접적으로 접근할 때 다음과 같은 연산을 수행하면 쉽게 접근할 수 있다. 하지만, 이 경우 포인터 오류 등 발생할 수 있는 위험들이 많이 존재한다.
template<typename _Tp> _Tp& Mat::at(int y, int x)
# example
for(int y = 0; y < mat1.rows; y++){
for(int x = 0; x < mat1.cols; x++){
mat1.at<uchar>(y, x)++;
}
}
template<typename _Tp> _Tp* Mat::ptr(int y)
# example
for(int y = 0; < mat1.rows; y++){
uchar* p = mat1.ptr<uchar>(y);
for(int x = 0; x < mat1.cols; x++){
p[x]++;
}
}
# example
for(auto it = mat1.begin<uchar>(); it != mat1.end<ucahr>(); ++it){
(*it)++;
}
이밖에 Mat 클래스는 행렬의 기본 연산도 지원한다.
Mat mat2 = mat1.inv();
cout << mat1.t() << endl;
cout << mat1 + 3 << endl;
cout << mat1 + mat2 << endl;
cout << mat1 * mat2 << endl;
아직은 어려운 점이 없다. 하지만, OpenCV를 본격적으로 사용하면 많이 어려워질 것 같다!
아직은 궁금한 점이 없었다.
오늘 드디어 프로그래머스 데브코스의 한 학습 구간이 끝나는 날이다. 30일동안 수업을 들으며, 오프라인 수업 등 다양한 활동이 있었다. 이전에 배웠던 내용들은 학부 때 많이 접했던 내용들이기에 따라가는데 어려움은 없었다.
이번 주부터 배우기 시작한 OpenCV는 확실히 새로운 내용들이라 재미있었다. 나중에 좀 어렵긴 할 것 같지만, 새로운 내용을 배우는 데에 의미를 두고 열심히 따라가야겠다.
📌 프로그래머스 데브코스 6기 자율주행 인지과정(Perception) 수강 내용을 바탕으로 정리한 TIL 입니다.
📅 Today: 2023.10.17.