home search
OpenCV
Computer Vision
C++
[OpenCV] OpenCV 시작하기
2022. 03. 25

실습 환경

  • Visual Studio 2022
  • OpenCV ver. 4.5.5.
  • C++ 언어 사용

프로젝트 만들기

💜 프로젝트 생성

Visual Studio에서 ‘myProject’라는 이름의 새 프로젝트를 만든다. ‘솔루션 및 프로젝트를 같은 디렉터리에 배치’를 꼭 선택하고 만든다.

상단 툴바의 ‘활성 솔루션 플랫폼’이 ‘x64’로 되어 있지 않으면 ‘x64’로 바꾼다.

사용할 이미지인 ‘lenna.bmp’를 프로젝트 폴더에 저장(이동)해둔다.

💜 코드 작성

‘main.cpp’ 라는 이름의 cpp 파일을 만든 뒤 기본적 코드를 작성해둔다.

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return -1;
	}

	Mat dst = src;

	imshow("src", src);
	imshow("dst", dst);

	waitKey();
}

아직 위 사진처럼 빨간색 밑줄이 뜰 것이다.

💜 프로젝트 속성 편집

[프로젝트] - [myProject 속성] 창에서 구성을 Debug으로 하고 아래 내용을 수정한다.

시스템 환경변수 OPENCV_DIR는 opencv 소스코드의 build 폴더로 지정해뒀다. 필자의 경우 C:\opencv\build의 경로이다.

아래에서 ‘입력’이라 하는 것은 입력창의 왼쪽 토글을 눌러 <편집...> 버튼을 누르고 그 창에서 작업하라는 뜻이다.

  • 헤더파일이 위치한 디렉터리 설정: [구성 속성] - [C/C++] - [일반] 선택 후 ‘추가 포함 디렉터리’에 $(OPENCV_DIR)\include 입력
  • OpenCV 라이브러리 파일이 위치한 폴더 설정: [구성 속성] - [링커] - [일반] 선택 후 ‘추가 라이브러리 디렉터리’에 $(OPENCV_DIR)\x64\vc15\lib 입력
  • 해당 프로젝트에서 사용할 OpenCV 라이브러리 이름 설정: [구성 속성] - [링커] - [입력] 선택 후 ‘추가 종속성’에서 ‘opencv_world455d.lib’ 입력

opencv_world455d.lib에서 숫자는 버전을 말한다. 본인의 버전이 만약 4.0.0이면 opencv_world400d.lib라고 바꾸면 된다. 지금은 디버그 모드이므로 숫자 다음에 d가 붙는다.

하단의 버튼 ‘적용’을 한 번 눌러주고, 이번엔 구성을 Release로 바꾼다. 수정 방법은 위와 다 똑같다. OpenCV 라이브러리 이름을 설정할 때만 opencv_world455d.lib가 아니라 opencv_world455.lib으로 d를 빼고 입력하면 된다.

‘확인’을 눌러 저장하고 나간다. 이제 오류를 의미하는 빨간 밑줄이 사라졌다.

💜 프로젝트 속성 편집

상단 메뉴바에서 [빌드] - [솔루션 빌드] 버튼을 눌러 정상적으로 빌드가 이루어지는지 확인하고, [디버그] - [디버그 하지 않고 시작] 버튼을 눌러 정상적으로 실행이 되는지 확인한다.

Debug 모드에서는 자잘한 문장들이 콘솔 창에 많이 나온다. 에러가 아니라면 무시해도 좋다.


코드의 기본 구조

정지 영상(이미지)를 불러와 출력 영상을 내보내는 기본적인 구조는 아래와 같다.

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);
        // 그레이 스케일로 이미지를 불러옴

	if (src.empty()) {
        // 입력 영상이 비었다면(없다면) 에러 메시지 출력 후 종료
		cerr << "Image laod failed!" << endl;
		return -1;
	}

	Mat dst = src;  // 입력 영상을 그대로 출력

	imshow("src", src); // 입력 영상 창
	imshow("dst", dst); // 출력 영상 창

	waitKey();
        // 키보드 입력이 있을 때까지 대기
    destroyAllWindows();
        // 키 입력이 있으면 모든 창을 닫고 종료
}

💜 imread: 영상 파일 불러오기

Mat imread(const String& filename, int flags = IMREAD_COLOR);

flags는 영상 파일을 불러오는 옵션 플래그로, IMREAD_UNCHANGED(영상 속성 그대로 읽기), IMREAD_GRAYSCALE(1채널 그레이스케일), IMREAD_COLOR(3채널 BGR 컬러 영상) 등이 있다. Mat 클래스로 영상을 반환한다.

💜 imwrite: 영상 파일 저장하기

bool imwrite(const String& filename, InputArray img, const std::vector<int>& params  = std::vector<int>());

img는 영상 데이터 Mat 객체이다. params는 파일 저장 옵션(‘속성&값’의 정수쌍)이다.

💜 namedWindow: 새 창 띄우기

void namesWindow(const String& winname, int flags = WINDOW_AUTOSIZE);

winname은 창을 구분하는 고유 이름이다. 따라서 중복되면 안된다. flags는 창 속성 지정 플래그로, WINDOW_NORMAL(영상 크기가 창 크기에 맞게 지정됨, 창 크기 드래그로 변경 가능), WINDOW_AUTOSIZE(창 크기가 영상 크기에 맞게 변경됨) 등이 있다. imshow() 함수로도 창 띄울 수 있다.

💜 imshow: 영상 출력

void imshow(const String& winname, InputArray mat);

winname에 해당하는 창이 없으면 WINDOW_AUTOSIZE 속성의 창을 만들어 영상을 출력한다. waitKey() 함수가 실행되어야 화면에 영상이 나타난다.

💜 waitKey: 키보드 입력 대기

int waitKey(int delay = 0);

delay는 ms 단위의 대기 시간이며, 0 이하이면 무한히 기다린다. 눌린 키 값을 반환하며, 눌리지 않으면 -1이다(ESC=27, ENTER=13, TAB=9). OpenCV 창이 하나라도 있어야 정상 동작한다.

특정 키를 눌렀을 때의 동작을 구현하려면 아래와 같이 하면 된다.

while(true)
{
    if(waitKey() == 'q') break;
    // 또는 waitKey() == 27
}

[실습] 영상 파일의 형식(확장자) 변환하기

사용자의 입력을 받아 특정 이미지 파일을 다른 형식(확장자)로 변환하는 실습을 한다. 프로젝트의 이름은 ocvrt로 했다.

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main(int argc, char* argv[])
{
	// argc는 cmd line에서 사용자에게 입력을 받은 개수
    // 의도하기로는 '실행파일' + '원본파일' + '목표파일'로 3개를 받아야 하므로
    // 그보다 인자가 작으면 사용법을 출력하고 종료함
	if (argc < 3) {
		cout << "Usage: ocvrt.exe <src_image> <dst_image>" << endl;
		return 0;
	}

	// 원본 파일(두 번째 인자)을 읽어 img에 저장함
	Mat img = imread(argv[1], IMREAD_UNCHANGED);

    // 만약 원본 파일이 비었다면 에러를 출력하고 종료함
	if (img.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	// 목표 파일(세 번째 인자)에 영상을 저장함
	bool ret = imwrite(argv[2], img);
	
    // 만약 목표 영상이 잘 만들어지지 않았다면 실패했다고 출력하고,
    // 잘 만들어졌다면 성공했다고 출력
	if (ret) {
		cout << argv[1] << " is successfully saved as " << argv[2] << endl;
	} else {
		cout << "File save failed!" << endl;
	}

	return 0;
}

코드 작성이 끝나면 빌드를 한다. 프로젝트 폴더로 이동하면 x64 폴더가 만들어지고, x64 - Debug 경로로 가면 exe 파일이 있다. 해당 파일을 프로젝트 폴더로(이미지 파일, sln 파일, cpp 파일이 있는 곳)으로 복사해온다. 주소표시줄에 cmd를 입력해 명령프롬프트를 열고 프로그램을 실행한다.

올바른 사용법은 <exe 파일 이름> <원본 파일 이름> <목표 파일 이름>이므로 그렇게 입력하면 아래와 같이 결과가 나온다.

>ocvrt.exe lenna.bmp lenna.jpg
lenna.bmp is successfully saved as lenna.jpg


OpenCV 프로젝트 템플릿 만들기

Visual Studio에서 프로젝트 템플릿 기능을 이용하면 프로젝트 속성이나 기본 소스 코드 등이 미리 설정된 프로젝트를 자동으로 생성할 수 있다. ‘템플릿 내보내기 마법사’를 이용해 zip 파일로 패키징 된 자체 템플릿 제작할 수 있다.

OpenCV의 경우 추가 포함 디렉터리, 추가 라이브러리 디렉터리, 추가 종속성 등을 매 프로젝트마다 설정해줘야 했는데, 이를 미리 설정해놓고 더불어 기본 소스코드(main.cpp)와 테스트 영상 파일(lenna.bmp)도 함께 생성되도록 할 수 있다.

💜 프로젝트 생성

템플릿으로 만들 예시 프로젝트를 생성할 것이다. Visual Studio에서 ‘OpenCVTemplate’라는 이름의 새 프로젝트를 만든다. 빌드까지의 과정은 위에서 다룬 ‘프로젝트 만들기’ 내용과 동일하다.

‘lenna.bmp’ 파일은 리소스 파일에 등록되어 있어야 하는데, 솔루션 탐색기에서 ‘리소스 파일’에 해당 프로젝트 폴더에 있는 이미지를 드래그해서 놓으면 자동으로 추가가 된다.

💜 템플릿 내보내기

상단바에서 [프로젝트] - [템플릿 내보내기..] 메뉴를 선택한다. 템플릿 이름이나 설명은 원하는 대로 설정하고, 나머지는 기본 설정으로 둔 뒤 마친다.

💜 새 프로젝트 만들기

기존에 열려있던 솔루션을 닫고, ‘새 프로젝트 만들기’ 창을 열어보면, 템플릿에 내가 만든 ‘OpenCVTemplate’이 뜬다. 맨 처음에는 가장 상단에 뜨고, 다음부터는 아래쪽 어딘가에 뜬다. 이를 선택해서 프로젝트를 만들면 아까 만들어둔 템플릿 그대로 빌드와 실행이 가능하다.

💜 추가 수정

C:\Users\<사용자 ID>\Documents\Visual Studio 2022\Templates\ProjectTemplates 경로로 이동하면 OpenCVTemplate.zip이라는 zip 파일이 나온다. MyTemplate.vstemplate 파일을 편집한다. 압축을 풀고 작업한 뒤에 다시 압축해 놓아야 한다.

아래는 편집할 문자열만 따로 뽑아낸 것이다.

<!--상단 생략-->

    <Name>OpenCV 콘솔 응용 프로그램</Name>
    <Description>OpenCV 콘솔 응용 프로그램을 생성합니다.</Description>

<!--중간 생략-->

    <DefaultName>OpenCVTemplate</DefaultName>

<!--중간 생략-->

      <ProjectItem ReplaceParameters="false" TargetFileName="main.cpp">main.cpp</ProjectItem>
      <ProjectItem ReplaceParameters="false" TargetFileName="lenna.bmp">lenna.bmp</ProjectItem>

ProjectItem 태그의 경우 자동으로 생성되는 경우도 있고, 아닌 경우도 있다. Visual Studio 2022는 자동 생성되지만, 그렇지 않다면 추가해준다.


Mat 클래스

Mat 객체 생성

Mat 객체를 생성하는 방법에는 아래와 같은 방식이 있다.

// 1. 빈 객체 생성하기
Mat img1; 	// empty matrix

// 2. 특수 행렬 생성하기
Mat mat1 = Mat::zeros(3, 3, CV_32SC1);	// 0으로 채워진 3X3 Mat 객체
Mat mat2 = Mat::ones(3, 3, CV_32FC1);	// 1로 채워진 3X3 Mat 객체
Mat mat3 = Mat::eye(3, 3, CV_32FC1);	// 항등행렬 3X3 Mat 객체

// 3. 가로 & 세로 & 채널(깊이) 지정해 생성하기
Mat img2(480, 640, CV_8UC1);		// 가로 640 X 세로 480, 1채널, unsigned char
Mat img4(Size(640, 480), CV_8UC3);	// 가로 640 X 세로 480, 3채널, unsigned char

// 4. 픽셀값 지정해 생성하기
Mat img5(480, 640, CV_8UC1, Scalar(128));		// 1채널이므로 GrayScale, 밝기 128
Mat img6(480, 640, CV_8UC3, Scalar(0, 0, 255));	// 3채널이므로 RGB 값으로 빨강

// 5. 픽셀값 지정해 생성하기2
float data[] = {1, 2, 3, 4, 5, 6};	// data 배열 값이 바뀌면 mat4의 값도 바뀜
Mat mat4(2, 3, CV_32FC1, data);

// 6. Mat_ 템플릿 시용해 생성하기
Mat mat5 = (Mat_<float>(2, 3) << 1, 2, 3, 4, 5, 6);	//Shift 연산자 재정의 이용
Mat mat6 = Mat_<uchar>({2, 3}, {1, 2, 3, 4, 5, 6});

// 7. create 함수 이용하기
mat4.create(256, 256, CV_8UC3);
	// 기존 3X2에서 256X256 크기로, 기존 CV_32FC2에서 CV_8UC3으로 변경

// 8. setTo 함수 이용하기
mat5.setTo(1.f);	// 모든 픽셀의 값이 1.0으로 변경

// 9. 픽셀값 직접 대입하기
mat5 = Scalar(255, 0, 0);

💜 create() : 기존 값을 밀어버리고 새롭게 지정하고 싶을 때 사용. 초깃값 지정은 불가

💜 setTo() : 모든 원소 값을 한 번에 지정

참조와 복사

💜 얕은 복사(참조)는 = 연산자를 사용한다. 얕은 복사를 하면 원본 객체가 변경이 되었을 때(예: img1.setTo(Scalar(0, 255, 255))) 복사된 객체도 똑같이 변한다.

💜 깊은 복사는 clone()이나 copyTo()를 사용한다.

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

// 얕은 복사
Mat img2 = img1;

// 깊은 복사(1)
Mat img3 = img1.clone();

// 깊은 복사(2)
Mat img4;
img1.copyTo(img4);

부분 추출

💜 () 연산자 : 괄호 안에서 Rect 객체를 지정해 부분 영상의 크기와 위치를 지정한다.

Mat img2 = img1(Rect(220, 120, 340, 240));
Mat img3 = img1(Rect(220, 120, 340, 240)).clone();

픽셀값 참조

💜 Mat::data를 사용해 직접 k 번째 원소의 위치를 계산하기. 실수의 위험성이 높음

💜 MatIterator_ 반복자 쓰기

for (MatIterator_<uchar> it = mat1.begin<uchar>(); it != mat1.end<uchar>(); ++it)
	(*it)++;

💜 ptr 사용하기: at 보다는 빠름. 행 단위 연산에 유리

for (int y = 0; y < mat1.rows; y++) {
	uchar* p = mat1.ptr<uchar>(y);
	for (int x = 0; x < mat1.cols; x++) {
		p[x]++;
	}
}

💜 at 사용하기: 좌표 지정이 직관적

for (int y = 0; y < mat1.rows; y++) {
	for (int x = 0; x < mat1.cols; x++) {
		mat1.at<uchar>(y, x) = 1;
	}
}

카메라, 동영상 다루기

카메라 작동 예시

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	VideoCapture cap;
	cap.open(0);
	    //VideoCapture cap(0)으로 한 번에 선언 가능

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

    Mat frame;
	while (true) {
		cap >> frame;

		if (frame.empty()) {
			cerr << "Empty frame!" << endl;
				// 영상 맨 마지막 프레임이라면 frame이 empty가 됨.
				// 이 예외처리 없다면 아래 코드로 넘어가 에러로 비정상 종료
			break;
		}

		imshow("frame", frame);

		if (waitKey(1) == 27)
			break;
	} 
	
	cap.release();  // 가급적 직접 호출해서 닫아주기!
	destroyAllWindows();
}

💜 VideoCapture 클래스

  • 카메라와 동영상 모두에서 프레임을 받아오는 작업이 이 클래스 하나로 처리된다.
  • 카메라: VideoCapture(장치 ID)
  • 동영상: VideoCapture(파일 이름)

💜 open

  • 카메라 열기: open(장치 ID)
  • 동영상 열기: open(파일 이름)

💜 현재 프레임 받아오기

  • read(현재 프레임)
  • VideoCapture객체 >> 프레임 연산자

💜 waitkey

  • waitKey()만 넣으면 영상이 뜨긴 뜨지만 키를 누를 때만 화면이 변하고 동영상처럼 연속되서는 보이지 않는다.
  • 인자로 주는 초(ms 단위) 마다 프레임을 갱신한다. while 처음으로 간다는 뜻이다.
  • 만약 카메라가 30 FPS라고 하고 1초는 1000ms이므로 1000/30=33ms이기 때문에, waitKey() 인자로 10을 준다고 하면 waitKey()에서 10ms를 기다리고, cap >> frame에서 23ms를 기다린 뒤 다음 프레임을 받아온다.

💜 get, set

각각 속성 참조와 설정을 함

// get : 가로(width) 알아내기
int w = cvRound(cap.get(CAP_PROP_FRAME_WIDTH)); //cvRound()는 반올림 함수

// get: 동영상에서 fps 알아내기
double fps = cap.get(CAP_PROP_FPS);

// set: 가로 길이 지정하기
cap.set(CAP_PROP_FRAME_WIDTH, 1280);

💜 VideoWriter 클래스

몇 개의 정지 영상을 동영상 파일로 저장하는 클래스이다. fourcc는 four character code의 준말로, 4개의 문자로 만든 하나의 정수값을 말한다.

VideoCapture cap(0);

int  fourcc = VideoWriter::fourcc('X', 'V', 'I', 'D');
	// 압축 방식을 나타내는 문자 fourcc 생성. 여기선 XVID MPEG-4 코덱
double fps = 30;
Size sz((int)cap.get(CAP_PROP_FRAME_WIDTH), (int)cap.get(CAP_PROP_FRAME_HEIGHT));   // 바로 초기화하며 선언

VideoWriter output("output.avi", fourcc, fps, sz);

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

int delay = cvRound(1000 / fps);

Mat frame;
while (true) {
	cap >> frame;
	if (frame.empty())
		break;

	output << frame;	// 프레임 저장
	imshow("frame", frame);

	if (waitKey(delay) == 27)
		break;
}

output.release();
cap.release();
destroyAllWindows();
arrow_upward arrow_downward
loading