📔 오늘 공부한 내용

OpenCV 기초를 공부하는 두번째 날이다. 카메라, 동영상, 이벤트 처리 방법에 대한 학습을 진행했다.

카메라와 동영상 처리하기

VideoCapture 클래스

OpenCV에서는 카메라와 동영상으로부터 프레임을 받아오는 작업을 VideoCapture 클래스 하나로 같이 사용한다. 카메라를 사용할 때는 장치의 ID를, 동영상을 사용할 때에는 파일을 지정해주면 된다.

class VideoCapture{
public:
	// index: 사용할 캡처 장치의 ID(0, 1, 2, ...)
	// apiPreference: 선호하는 카메라 처리 방법을 지정(CAP_DSHOW, CAP_MSMF, CAP_V4L, ...)
	VideoCapture();
    VideoCapture(const String& filename, int apiPreference = CAP_ANY);
    VideoCapture(int index, int apiPreference = CAP_ANY);
    virtual ~VideoCapture();
    
    virtual bool open(const String& filename, int apiPreference = CAP_ANY);
    virtual bool open(int index, int apiPreference = CAP_ANY);
    virtual release();
    
    // 프레임 정보 가져오기
    virtual VideoCapture% operator >> (Mat& image);	
    virtual bool VideoCapture::read(OutputArray image);
    
    // 속성 가져오기
    virtual bool set(int propId, double value);
    virtual double get(int propId) const;
    ...

카메라를 이용해 영상을 받아 출력하는 코드는 다음과 같다.

...
int main(int argc, char const *argv[]){
	// 카메라 open
    VideoCapture cap;
    cap.open(0);

    if(!cap.isOpened()){
        cerr << "Camera open failed!" << endl;
        return -1;
    }

	// 프레임 받아오기
    Mat frame;
    while(true){
        cap >> frame;

        if(frame.empty()){
    	    cerr << "Frame empty!" << endl;
            break;
        }

        imshow("frame", frame);
        if(waitKey(1) == 27)     break;
    }
    
    // 장치 종료하기
    cap.release();
    destroyAllWindows();

    return 0;
}
...

동영상 파일을 사용할 때에는 다음과 같이 장치 대신 파일명을 추가하면 된다.

VideoCapture cap;
cap.open("test_video.mp4");

OpenCV에서는 Canny Edge Detection을 쉽게 처리할 수 있는 함수를 지원한다.

Canny(frame, edge, 50, 150);

VideoCapture 속성 지정

카메라나 영상은 가로, 세로 등 다양한 속성을 갖고 있다. VideoCapture의 get()이나 set()을 통해 속성을 가져오거나 설정할 수 있다.

// 속성 정보 가져오기
int w = cvRound(cap.get(CAP_PROP_FRAME_WIDTH));		// 프레임 가로 크기
int h = cvRound(cap.get(CAP_PROP_FRAME_HEIGHT));	// 프레임 세로 크기
--- CAP_PROP_FPS: 초당 프레임 수
--- CAP_PROP_FRAME_COUNT: 비디오 파일의 총 프레임 수
--- CAP_PROP_POS_FRAMES: 현재 프레임 번호
--- CAP_PROP_EXPOSURE: 노출
...

// 속성 설정하기
cap.set(CAP_PROP_FRAME_WIDTH, 1280);
cap.set(CAP_PROP_FRAME_HEIGHT, 720);

VideoWriter 클래스

VideoWriter 클래스는 Video Capture와 반대로 frame들을 모아 동영상 파일로 저장하는 기능을 수행할 수 있다.

class VideoWriter{
public:
	VideoWriter();
    VideoWriter(cosnt String& filename, int fourcc, double fps, Size frameSize, bool isColor=true);
    virtual ~VideoWriter();
    
    virtual bool open(const String& filename, int fourcc, double fps, Size frameSize, bool isColor = true);
    virtual void release();
}
...

영상을 저장할 때에는 Width, Height, fps 등의 옵션을 지정해주어야 한다. 다음과 같이 현재 카메라의 데이터를 받아와 지정해줄 수도 있다.

// 옵션 지정
int  fourcc = VideoWriter::fourcc('X', 'V', 'I', 'D');
--- DIVX: DIVX MPEG-4 코덱
--- XVID: XVID MPEG-4 코덱
--- X264: H.264/AVC 코덱
--- MJPG: Motion-JPEG 코덱

double fps = 30;
Size sz((int)cap.get(CAP_PROP_FRAME_WIDTH), (int)cap.get(CAP_PROP_FRAME_HEIGHT));

// 저장하기 위한 VideoWriter 객체 생성
VideoWriter output("output.avi", fourcc, fps, sz);

// 파일 정상 생성 여부 확인
if (!output.isOpened()) {
	cerr << "output.avi open failed!" << endl;
	return -1;
}

int delay = cvRound(1000 / fps);			// MatFrame의 시간 간격
Mat frame;
while (true) {
	cap >> frame;

	output << frame;						// 현재 프레임을 output에 저장
}

output.release();
cap.release();

그리기 함수

OpenCV를 사용할 때, 객체등을 인식하면 객체 테두리에 사각형을 그리거나 정보를 출력하는 경우가 자주 있다. 따라서, 그리기에 대한 일정 함수는 알고 있어야 한다.

// 직선 그리기
void line(InputOutputArray img, Point pt1, Point pt2, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0);
--- pt1: 시작점 좌표
--- pt2: 끝점 좌표
--- lineType: 선 타입(LINE_4, LINE_8, LINE_AA)

// 사각형 그리기
void rectangle(InputOutputArray img, Rect rec, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0);
--- rec: 사각형 정보
--- thickness: -1을 지정시 채워진 사각형, 나머지는 두께

// 원 그리기
void circle(InputOutputArray img, Point center, int radius, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0);
--- center: 원 중심 좌표
--- radius: 원 반지름

// 다각형 그리기
void polylines(InputOutputArray img, InputArrayOfArrays pts, bool isClosed, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0);
--- pts: 다각형의 꼭지점 정보
--- isClosed: pts의 시작점과 끝점을 이을지에 대한 여부(폐곡선)

// 문자열 출력하기
void putText(InputOutputArray img, const String& text, Point org, int fontFace, double fontScale, Scalar color, int thickness = 1, int lineType = LINE_8, bool bottomLeftOrigin = false);
--- text: 출력할 문자열
--- fontFace: 폰트 종류(cv::HersheyFonts)
--- fontScale: 폰트 크기(배수)

이들을 직접적으로 사용하면 다음과 같이 사용할 수 있다.

...
line(frame, Point(570, 280), Point(0, 560), Scalar(255, 0, 0), 2);
line(frame, Point(570, 280), Point(1024, 720), Scalar(255, 0, 0), 2);

int pos = cvRound(cap.get(CAP_PROP_POS_FRAMES));
String text = format("frame number: %d", pos);
putText(frame, text, Point(20, 50), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 0, 255), 1, LINE_AA);

imshow("frame", frame);
...

이벤트 처리

OpenCV로 만든 창에서 어떤 항목을 클릭해서 상호작용을 한다거나, 키보드에 따른 상호작용이 필요할 수 있다. OpenCV에서는 이러한 이벤트 처리를 수행할 수 있는 함수가 만들어져 있다.

마우스 이벤트 처리하기

마우스 이벤트 처리를 위한 함수는 다음과 같다. 이를 사용하기 위해서는 마우스 이벤트를 받고 싶은 창이 생성된 상태 뒤에 생성해줘야 한다.

void setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);

typedef void(*MouseCallback)(int event, int x, int y, int flags, void* userdata);
--- event: 이벤트 종류(EVENT_MOUSEMOVE = 0, EVENT_LBUTTONDOWN = 1, EVENT_RBUTTONDOWN = 2, ...) 
--- x, y: 이벤트 발생 좌표
--- flags: 이벤트 플래그(EVENT_FLAG_LBUTTON = 1, EVENT_FLGA_RBUTTON = 2, EVENT_FLAG_MBUTTON = 4...)
--- userdata: setMouseCallback() 함수에서 지정한 사용자 지정 데이터

마우스를 클릭하면, 클릭한 위치를 반환하는 기능을 구현하면 다음과 같이 구현할 수 있다.

int main(){
	...
    namedWindow("src")
    setMouseCallback("src", on_mouse);
    ...
}

void on_mouse(int event, int x, int y, int flags, void*){
	switch(event){
    case EVENT_LBUTTONDOWN:				// 좌측 버튼을 눌렀을 때
        cout << "EVENT_LBUTONDOWN: " << x << ", " << y << endl;
        break;
    case EVENT_LBUTTONUP:				// 좌측 버튼을 누른 뒤 뗐을 때
    	cout << "EVENT_LBUTONUP: " << x << ", " << y << endl;
        break;
    case EVENT_MOUSEMOVE:				// 마우스가 움직이는 경우
    	if(flag & EVENT_FLAG_LBUTTON){	// 마우스를 누르고 움직이는 경우
        	cout << "EVENT_MOUSEMOVE AND EVENT_FLAG_LBUTTON: " << x << ", " << y << endl;
        	...
        }
     	break;
    ...
   	}
}

트랙바

트랙바(trackbar)는 영상 출력창에 부착되어 프로그램 동작 중에 사용자가 지정한 범위 안의 값을 선택할 수 있는 UI 기능이다.

int createTrackbar(const Strin& trackbarname, const String& winname, int* value, int count, TrackbarCallback onChange = 0, void* userdata = 0);
--- value: 트랙바 위치 값을 받을 정수형 변수의 주소
--- cound: 트랙바 최대 위치(최소는 항상 0)

typedef void (*TrackbarCallback)(int pos, void* userdata);

트랙바에 따라 그림의 밝기가 변하는 기능을 구현하면 다음과 같이 구현할 수 있다.

int main(){
	...
    createTrackbar("level", "image", 0, 16, on_level_change, (void*)&img);
	imshow("image", img);
    ...
}

void on_level_change(int pos, void* userdata)
{
	Mat img = *(Mat*)userdata;

	img.setTo(pos * 16);
	imshow("image", img);
}

연산 시간 측정

프로그램의 성능을 추산하기 위해 연산 시간을 측정해야 할 일이 자주 있다. OpenCV는 정밀한 시간 측정을 위한 기능을 제공한다.

기본적인 프로그램의 연산 시간을 측정하는 방법은 다음과 같다.

int64 t1 = getTickCount();

my_func()

int64 t2 = getTickCount();
double ms = (ts2 - t1) * 1000 / getTickFrequency();

TickMeter 클래스

TickMeter 클래스는 시간을 측정하기 위한 클래스이다.

class TickMeter{
public:
	TickMeter();
    
    void start();
    void stop();
    void reset();
    
    double getTimeMicro() const;
    double getTimeMilli() const;
    double getTimeSec() const;
    ...
}

이를 통해 연산 시간을 측정하면 다음과 같이 측정할 수 있다.

TickMeter tm;
tm.start();

func();					// 시간을 측정할 함수

tm.stop();
cout << tm.getTimeMilli() << "ms." << endl;

기타 OpenCV 함수

Mat img = imread("lenna.bmp");

// 행렬의 합 구하기
Scalar sum(InputArray src);

// 행렬의 합 구하기 - example
uchar data[] = {1, 2, 3, 4, 5, 6};
Mat mat1(2, 3, CV_8UC1, data);

int sum1 = (int)sum(mat1)[0];

// 행렬의 평균 구하기
Scalar mean(InputArray src, InputArray mask = noArray());

// 행렬의 평균 구하기 - example
double mean1 = mean(img)[0];

// 행렬의 최대값/최소값 구하기
void minMaxLoc(InputArray src, double* minVal, double* maxVal = 0, Point* minLoc = 0, Point* maxLoc = 0, InputArray mask = noArray());

// 행렬의 최대값/최소값 구하기 - example
double minv, maxv;
Point minLoc, maxLoc;
minMaxLoc(img, &minv, &maxv, &minLoc, &maxLoc);

// 행렬의 자료형 변환
void Mat::convertTo(OutpuArray m, int rtype, double alpha = 1, double beta = 0) const;

// 행렬의 자료형 변환 - example
img.convertTo(img, CV32FC1);

// 행렬의 정규화
void normalize(InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());

// 행렬의 정규화 - example
Mat dst;
normalize(src, img, 0, 255, NORM_MINMAX);

// 색 공간 변환
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0);

// 색 공간 변환 - example
Mat dst;
cvtColor(img, dst, COLOR_BGR2GRAY);

// 채널 분리
void split(const Mat& src, Mat* mvbegin);
void split(InputArray src, OutputArrayOfArrays mv);

// 채널 병합
void merge(const Mat* mv, size_t count, OutputArray dst);
void merge(InputArrayOfArrays mv, OutputArray dst);

// 채널 분리/병합 - example
vector<Mat> planes;
split(img, planes);

swap(planes[0], planes[2]);

Mat dst;
merge(planes, dst);

Mask 연산과 ROI

ROIRegion of Interest의 약자로 관심 영역을 말한다. 특정 연산을 수행하고자 하는 임의의 부분 영역이다. OpenCV는 일부 함수에 대해 ROI 연산을 지원하며, 이 때에는 마스크 영상(Mask Image)를 인자로 전달해줘야한다.
Mask Image는 0과 255로만 구성된 이미지로, 마스크 이미지를 전달해 ROI 연산을 수행하면, 값이 255인 부분에 대해서 ROI를 지정하게 되는 것이다.

📝 TIL을 정리하며

🤕 어려웠던 점

초기에 OpenCV 3.X 버전을 가지고 수업을 들었는데, Mat_ 클래스가 없어 환경을 잡는데 어려움이 있었다. 지금은 물론 4.5.4를 쓰고 있기에 해결됬다!

🤔 궁금한 점

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

😁 느낀 점

실습을 해보면 수업을 따라가니 OpenCV가 재미있다. Python에서 사용하는 OpenCV와 크게 차이가 없는것 같아 다행이다. 근데, CMakeLists.txt를 작성하는 거는 아직도 어렵고 ChatGPT의 도움을 받아야 된다.


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

profile
그냥 끄적여보는 블로그

0개의 댓글