📔 오늘 공부한 내용

OpenCV를 사용하는 기초적인 방법에 대해 공부했다. 나중에 ROS에서 사용해야 하므로, Docker 내부에서 시스템을 구성해 학습을 진행했다. Docker 환경을 구성하는 것은 이전 블로그 글에서 확인할 수 있고, CMakeListsts.txt와 빌드 설정을 구성하는 것은 같이 수업을 듣는 동료분께서 포스팅한 글을 참고했다.

OpenCV 개발환경 설정하기

우분투에서 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 주요 클래스

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 관련 주요 클래스

Mat 클래스는 다채널 행렬을 표현을 위한 클래스이다. 수학적으로 정의되는 행렬과 동일하며, 수학적인 행렬 연산을 지원한다. 영상처리에서는 그레이스케일이나 트루컬러 영상을 표현할 때 사용된다.

  • 깊이(depth): 행렬 원소가 사용하는 자료형 정보를 가르키는 매크로 상수(CV_8U, CV_8S, CV_16U, ...)
  • 채널(Channel): 픽셀 원소 하나가 몇개의 값으로 구서되어 있는지를 나타냄(Grayscale: 1, TrueColor: 3)
  • 타입(Type): 행렬의 깊이와 채널 수를 한번에 나타내는 매크로 상수(CV_8UC1, CV_8UC3, ...)
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 클래스는 다음과 같은 방식으로 생성할 수 있다. 아래 방식 외에도 다양한 방식을 지원한다. 배열을 만든 뒤 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 클래스는 행렬이기에, 행렬 연산을 지원한다. 또한, 행렬에 접근하기 위한 메소드들을 다양하게 지원한다.

원소 직접 접근

원소에 대해 직접적으로 접근할 때 다음과 같은 연산을 수행하면 쉽게 접근할 수 있다. 하지만, 이 경우 포인터 오류 등 발생할 수 있는 위험들이 많이 존재한다.

addr(Mi,j)=M.data+M.step[0]i+M.step[1]jaddr(M_{i,j})=M.data + M._{step[0] * i} + M._{step[1] * j}

Mat::at() 함수: 개열 원소 접근

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)++;
    }
}

Mat::ptr() 함수: 행 접근

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]++;
    }
}

MatIterator_<T> 반복자: 원소 차례 접근

# 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;

📝 TIL을 정리하며

🤕 어려웠던 점

아직은 어려운 점이 없다. 하지만, OpenCV를 본격적으로 사용하면 많이 어려워질 것 같다!

🤔 궁금한 점

아직은 궁금한 점이 없었다.

😁 느낀 점

오늘 드디어 프로그래머스 데브코스의 한 학습 구간이 끝나는 날이다. 30일동안 수업을 들으며, 오프라인 수업 등 다양한 활동이 있었다. 이전에 배웠던 내용들은 학부 때 많이 접했던 내용들이기에 따라가는데 어려움은 없었다.
이번 주부터 배우기 시작한 OpenCV는 확실히 새로운 내용들이라 재미있었다. 나중에 좀 어렵긴 할 것 같지만, 새로운 내용을 배우는 데에 의미를 두고 열심히 따라가야겠다.


📌 프로그래머스 데브코스 6기 자율주행 인지과정(Perception) 수강 내용을 바탕으로 정리한 TIL 입니다.
📅 Today: 2023.10.17.

profile
그냥 끄적여보는 블로그

0개의 댓글