3학년/OpenCV

[OpenCV] 15장 머신 러닝

천도복숭아에이드 2023. 7. 3. 16:01

 

 

 

15.1 머신 러닝과 OpenCV

 

 

15.1.1 머신 러닝 개요

 

 

머신러닝(machine learning): 주어진 데이터를 분석하여 규칙성, 패턴 등을 찾고, 이를 이용하여 의미 있는 정보를 추출하는 과정

 

 

나머지는 심층학습 내용과 동일

 

 

 

15.1.2 OpenCV 머신 러닝 클래스

 

 

 

 

virtual bool StatModel::train(InputArray samples, int layout, InputArray responses);
  • samples: 훈련 데이터 행렬
  • layout: 훈련 데이터 배치 방법. ROW_SAMPLE 또는 COL_SAMPLE를 지정
  • responses: 각 훈련 데이터에 대응되는 응답(레이블) 행렬
  • 반환값: 정상적으로 학습이 완료되면 true를 반환

 

 

 

 

virtual float StatModel::predict(InputArray samples, 
	OutputArray results = noArray(), int flags = 0) const;
  • samples: 입력 벡터가 행 단위로 저장된 행렬. CV_32F
  • results: 각 입력 샘플에 대한 예측 결과가 저장된 행렬
  • flags: 추가적인 플래그 상수. StatModel::Flags 열거형 상수 중 하나를 지정할 수 있으며, 모델에 따라 사용법이 다름
  • 반환값: 알고리즘에 따라 다르다

 

 

 

 

 

15.2 k 최근접 이웃

 

15.2.1 k 최근접 이웃 알고리즘

 

 

k 최근접 이웃(kNN) 알고리즘: 분류 또는 회귀에 사용되는 지도 학습 알고리즘 중 하나

 

 

15.2.2 KNearest 클래스 사용

 

 

static Ptr<KNearest> KNearest::create();
  • 반환값: KNearest 객체를 참조하는 Ptr 스마트 포인터 객체

 

vitural void KNearest::setDefault(int val);
  • val: kNN 알고리즘에서 사용할 k 값. StatModel::predict() 함수를 사용할 경우 미리 k 값을 적절하게 설정

 

virtual void KNearest::setIsClassifier(bool val);
  • val: 이 값이 true이면 분류로 사용하고, false 이면 회귀로 사용

 

virtual float KNearest::findNearest(InputArray samples, int k, OutputArray results, 
	OutputArray neighborResponses = noArray(), OutputArray dist = noArray()) const;
  • samples: 테스트 데이터 벡터가 행 단위로 저장된 행렬. 입력 벡터의 차원은 훈련 벡터의 차원과 같아야 하며, 행렬 타입은 CV_32FC1이어야 한다
  • k: 사용할 최근접 이웃 개수. 1보다 같거나 커야 한다
  • results: 각 입력 샘플에 대한 예측(분류 또는 회귀) 결과를 저장한 행렬. samples.rows x 1 크기를 갖고, 타입은 CV_32FC1
  • neighborResponses: 예측에 사용된 k개의 최근접 이웃 클래스 정보를 담고 있는 행렬. samples.rows x k 크기를 갖고, 타입은 CV_32FC1
  • dist: 입력 벡터와 예측에 사용된 k개의 최근접 이웃과의 거리를 저장한 행렬. samples.rows x k 크기를 갖고, 타입은 CV_32FC1
  • 반환값: 입력 벡터가 하나인 경우에 대한 응답이 반환

 

 

 

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

using namespace cv;
using namespace cv::ml;
using namespace std;

Mat img;
Mat train, label;
Ptr<KNearest> knn;
int k_value = 1;

void on_k_changed(int, void*);
void addPoint(const Point& pt, int cls);
void trainAndDisplay();

int main(void)
{
	img = Mat::zeros(Size(500, 500), CV_8UC3);
	knn = KNearest::create();

	namedWindow("knn");
	createTrackbar("k", "knn", &k_value, 5, on_k_changed);

	const int NUM = 30;
	Mat rn(NUM, 2, CV_32SC1);

	randn(rn, 0, 50);
	for (int i = 0; i < NUM; i++)
		addPoint(Point(rn.at<int>(i, 0) + 150, rn.at<int>(i, 1) + 150), 0);

	randn(rn, 0, 50);
	for (int i = 0; i < NUM; i++)
		addPoint(Point(rn.at<int>(i, 0) + 350, rn.at<int>(i, 1) + 150), 1);

	randn(rn, 0, 70); 
	for (int i = 0; i < NUM; i++) 
		addPoint(Point(rn.at<int>(i, 0) + 250, rn.at<int>(i, 1) + 400), 2);

	trainAndDisplay();

	waitKey();
	return 0;
}

void on_k_changed(int, void*)
{
	if (k_value < 1) k_value = 1;
	trainAndDisplay();
}

void addPoint(const Point& pt, int cls)
{
	Mat new_sample = (Mat_<float>(1, 2) << pt.x, pt.y);
	train.push_back(new_sample);

	Mat new_label = (Mat_<int>(1, 1) << cls);
	label.push_back(new_label);
}

void trainAndDisplay()
{
	knn->train(train, ROW_SAMPLE, label);

	for (int i = 0; i < img.rows; ++i) {
		for (int j = 0; j < img.cols; ++j) {
			Mat sample = (Mat_<float>(1, 2) << j, i);

			Mat res;
			knn->findNearest(sample, k_value, res);

			int response = cvRound(res.at<float>(0, 0));
			if (response == 0)
				img.at<Vec3b>(i, j) = Vec3b(128, 128, 255); // R
			else if (response == 1)
				img.at<Vec3b>(i, j) = Vec3b(128, 255, 128); // G
			else if (response == 2)
				img.at<Vec3b>(i, j) = Vec3b(255, 128, 128); // B
		}
	}

	for (int i = 0; i < train.rows; i++)
	{
		int x = cvRound(train.at<float>(i, 0));
		int y = cvRound(train.at<float>(i, 1));
		int l = label.at<int>(i, 0);

		if (l == 0)
			circle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA);
		else if (l == 1)
			circle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA);
		else if (l == 2)
			circle(img, Point(x, y), 5, Scalar(128, 0, 0), -1, LINE_AA);
	}

	imshow("knn", img);
}
  • 5행: cv::ml 네임스페이스를 사용하도록 설정
  • 22~23행: kNN 이름의 창에 트랙바를 부착하고, 트랙바가 움직이면 on_k_changed() 함수가 실행되도록 함
  • 25~38행: (150, 150) 좌표를 중심으로 하는 0번 클래스 점, (350, 150) 좌표를 중심으로 하는 1번 클래스 점, (250, 400) 좌표를 중심으로 하는 2번 클래스 점을 각각 30개씩 생성하여 훈련 데이터에 추가. 0번과 1번 클래스 점은 각각의 중심을 기준으로 표준 편차 50에 해당하는 가우시안 분포를 따르고, 2번 클래스 점은 중심을 기준으로 표준 편차 70에 해당하는 가우시안 분포를 따름
  • 40행: 프로그램이 처음 실행되자마자 kNN알고리즘으로 분류된 결과를 보여주도록 trainAndDisplay()함수를 호출
  • 46~50행: 트랙바를 움직여서 k 값이 바뀌면 다시 kNN 알고리즘을 학습시키고 그 결과를 화면에 나타냄
  • 52~59행: addPoint() 함수는 특정 좌표 점 pt를 cls 클래스로 등록
  • 63행: StatModel::train() 함수를 이용하여 kNN 알고리즘을 학습
  • 65~80행: img 영상 전체 좌표에 대해 kNN 분류가 응답을 조사하여 빨간색, 녹색, 파란색으로 표시
  • 82~94행: 25~38행에서 추가한 훈련 데이터 점 좌표에 반지름 5인 원을 각각 빨간색, 녹색, 파란색으로 표시

 

 

 

 

 

15.2.3 kNN을 이용한 필기체 숫자 인식

 

 

 

Ptr<KNearest> train_knn()
{
	Mat digits = imread("digits.png", IMREAD_GRAYSCALE);

	if (digits.empty()) {
		cerr << "Image load failed!" << endl;
		return 0;
	}

	Mat train_images, train_labels;

	for (int j = 0; j < 50; j++) {
		for (int i = 0; i < 100; i++) {
			Mat roi, roi_float, roi_flatten;
			roi = digits(Rect(i * 20, j * 20, 20, 20));
			roi.convertTo(roi_float, CV_32F);
			roi_flatten = roi_float.reshape(1, 1);

			train_images.push_back(roi_flatten);
			train_labels.push_back(j / 5);
		}
	}

	Ptr<KNearest> knn = KNearest::create();
	knn->train(train_images, ROW_SAMPLE, train_labels);

	return knn;
}
  • 3행: digits.png 영상을 불러와 digits에 저장
  • 12~13행: digits.png 영상에 가로 100개, 세로 50개의 필기체 숫자가 적혀 있으므로 for 반복문 범위를 동일하게 설정
  • 15행: 가로 i 번째, 세로 j번째 필기체 숫자 영상을 roi에 저장
  • 16행: roi 영상 자료형을 float 로 변환하여 roi_float 저장
  • 17행: 20x20 roi_float 영상을 400 x 1 크기의 영상으로 변환하여 roi_flatten에 저장
  • 19행: roi_flatten 영상을 train_images 행렬의 맨 아래 행으로 추가
  • 20행: train_labels 행렬의 맨 아래에 현재 추가한 필기체 숫자 영상의 정답(레이블)을 추가
  • 24~25행: KNearest 객체 KNN 학습을 수행. knn->train() 함수 인자로 사용되는 train_images 행렬 크기는 5000 x 400 이고, train_labels 행렬 크기는 5000x1임.
  • 27행: 학습된 knn 스마트 포인터를 반환

 

 

Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void* userdata)
{
	Mat img = *(Mat*)userdata;

	if (event == EVENT_LBUTTONDOWN) {
		ptPrev = Point(x, y);
	} else if (event == EVENT_LBUTTONUP) {
		ptPrev = Point(-1, -1);
	} else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {
		line(img, ptPrev, Point(x, y), Scalar::all(255), 40, LINE_AA, 0);
		ptPrev = Point(x, y);

		imshow("img", img);
	}
}
  • 7~8행: 마우스 왼쪽 버튼을 누른 위치를 ptPrev에 저장
  • 9~10행: 마우스 왼쪽 버튼을 떼면 ptPrev 좌표를 (-1, -1)로 초기화
  • 11~13행: 마우스 왼쪽 버튼을 누른 상태로 마우스가 움직이면 ptPrev 좌표부터 (x, y) 좌표까지 직선을 그림. 그리고 ptPrev 좌표를 (x, y)로 변경

 

 

 

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

using namespace cv;
using namespace cv::ml;
using namespace std;

Ptr<KNearest> train_knn();
void on_mouse(int event, int x, int y, int flags, void* userdata);

int main()
{
	Ptr<KNearest> knn = train_knn();

	if (knn.empty()) {
		cerr << "Training failed!" << endl;
		return -1;
	}

	Mat img = Mat::zeros(400, 400, CV_8U);

	imshow("img", img);
	setMouseCallback("img", on_mouse, (void*)&img);

	while (true) {
		int c = waitKey(0);
		
		if (c == 27) {
			break;
		} else if (c == ' ') {
			Mat img_resize, img_float, img_flatten, res;

			resize(img, img_resize, Size(20, 20), 0, 0, INTER_AREA);
			img_resize.convertTo(img_float, CV_32F);
			img_flatten = img_float.reshape(1, 1);

			knn->findNearest(img_flatten, 3, res);
			cout << cvRound(res.at<float>(0, 0)) << endl;

			img.setTo(0);
			imshow("img", img);
		} 
	}

	return 0;
}

 

  • 13행: train_knn() 함수를 실행하여 필기체 숫자를 학습한 결과를 knn에 저장
  • 20행: 400x400 크기의 영상 img를 생성. img 영상에 마우스로 글씨를 쓰고 숫자를 인식
  • 28~29행: 키보드에서 esc 키를 누르면 프로그램을 종료
  • 30행: 키보드에서 space 키를 누르면 필기체 숫자 인식을 수행
  • 33행: 숫자가 쓰여진 img 영상을 20x20 크기로 변환하여 img_resize에 저장
  • 34행: img_resize 영상 자료형을 float로 변환하여 img_float에 저장
  • 35행: 20x20 img_float 영상을 400x1 크기의 영상으로 변환하여 img_flatten에 저장
  • 37~38행: kNN 알고리즘으로 분류한 결과를 콘솔 창에 출력
  • 40~41행: img 영상을 검은색으로 초기화한 후 화면에 나타냄

 

 

 

 

15.3 서포트 벡터 머신

 

15.3.1 서포트 벡터 머신 알고리즘

 

 

서포트 벡터 머신: 기본적으로 두 개의 클래스로 구성된 데이터를 가장 여유 있게 분리하는 초평면을 찾는 머신 러닝 알고리즘

 

 

 

 

 

 

 

15.3.2 SVM 클래스 사용하기

 

 

 

static Ptr<SVM> SVM::create();
  • 반환값: SVM 객체를 참조하는 Ptr 스마트 포인터 객체

 

 

virtual void SVM::setType(int val)
  • val: SVM 타입. SVM::Types 열거형 상수 중 하나를 지정

 

 

 

virtual void SVM::setKernel(int kernelType);
  • kernelType: 커널 함수 종류. SVM::KernelTypes 열거형 상수 중 하나를 지정

 

 

 

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

using namespace cv;
using namespace cv::ml;
using namespace std;

int main(void)
{
	Mat train = Mat_<float>({ 8, 2 }, {
		150, 200, 200, 250, 100, 250, 150, 300,
		350, 100, 400, 200, 400, 300, 350, 400 });
	Mat label = Mat_<int>({ 8, 1 }, { 0, 0, 0, 0, 1, 1, 1, 1 });

	Ptr<SVM> svm = SVM::create();
	svm->setType(SVM::C_SVC);
	svm->setKernel(SVM::RBF);
	svm->trainAuto(train, ROW_SAMPLE, label);

	Mat img = Mat::zeros(Size(500, 500), CV_8UC3);

	for (int j = 0; j < img.rows; j++) {
		for (int i = 0; i < img.cols; i++) {
			Mat test = Mat_<float>({ 1, 2 }, { (float)i, (float)j });
			int res = cvRound(svm->predict(test));

			if (res == 0)
				img.at<Vec3b>(j, i) = Vec3b(128, 128, 255); // R
			else
				img.at<Vec3b>(j, i) = Vec3b(128, 255, 128); // G
		}
	}

	for (int i = 0; i < train.rows; i++) {
		int x = cvRound(train.at<float>(i, 0));
		int y = cvRound(train.at<float>(i, 1));
		int l = label.at<int>(i, 0);

		if (l == 0)
			circle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA); // R
		else
			circle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA); // G
	}

	imshow("svm", img);

	waitKey();
	return 0;
}
  • 10~12행: 여덟 개의 점 좌표를 포함하는 train 행렬을 생성. train 행렬은 CV_32FC1 타입이며 크기는 8x2 
  • 13행: 훈련 데이터 점들의 클래스를 정의한 label 행렬을 생성. 처음 네 개 점의 클래스는 0이고, 나머지 네 개 점의 클래스는 1. label 행렬은 CV_32SC1 타입이며 크기는 8x1
  • 15행: SW 객체를 생성하여 svm에 저장
  • 16행: SVM 타입을 C_SVC로 설정
  • 17행: SVM 커널 함수를 RBF로 설정
  • 18행: SVM::trainAuto() 함수를 사용하여 최적의 파라미터 C와 gamma를 자동으로 찾아 학습
  • 20행: SVM 분류 결과를 나타낼 img 영상을 생성 
  • 22~32행: img 영상의 모든 픽셀 좌표에 대해 SVM 응답을 구하여 빨간색 또는 녹색으로 표현
  • 34~43행: train 행렬에 저장된 훈련 데이터 점을 반지름 5인 원으로 표시한다. 0번 클래스 점은 빨간색 원으로, 1번 클래스 점은 녹색 원으로 그림

 

 

 

 

 

15.3.3 HOG&SVM 필기체 숫자 인식

 

 

 

HOGDescriptor::HOGDescriptor(Size _winSize, Size _blockSize, Size _blockStride, Size _cellSize,
	int _nbins, int _derivAperture = 1, double _winSigma = -1, 
    		HOGDescriptor::HistogramNormType _histogramNormType = HOGDescriptor::L2Hys, 
            double _L2HysThreshold = 0.2, bool _gammaCorrection = false, bool _signedGradient = false)
  • _winSize: 검출 윈도우 크기
  • _blockSize: 블록 크기
  • _blockStride: 블록 이동 크기
  • _cellSize: 셀 크기
  • _nbins: 히스토그램 빈 개수
  • _derivAperture: 현재 사용되지 않음
  • _winSigma: 가우시안 블러를 위한 표준 편차
  • _histogramNormType: 현재 사용되지 않음
  • _L2HysThreshold: L2-Hys 정규화 임계값
  • _gammaCorrection: 감마 보정 수행 여부
  • _nlevels: 검출 윈도우 증가 최대 횟수. 기본값은 64
  • _signedGradient: 그래디언트 방향 부호 사용 여부

 

virtual void HOGDescriptor::compute(InputArray img, std::vector<float>& descriptors, 
	Size winStride = Size(), Size padding = Size(), 
    	const std::vector<Point>& locations = std::vector<Point>()) const;
  • img: 입력 영상
  • descriptors: 출력 HOG 기술자. CV_32F
  • winStride: 윈도우 이동 크기. 블록 이동 크기의 배수여야 함
  • padding: 영상 가장자리 패딩 크기
  • locations: 계산 시작 위치

 

 

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

using namespace cv;
using namespace cv::ml;
using namespace std;

Ptr<SVM> train_hog_svm(const HOGDescriptor& hog);
void on_mouse(int event, int x, int y, int flags, void* userdata);

int main()
{
#if _DEBUG
	cout << "svmdigit.exe should be built as Relase mode!" << endl;
	return 0;
#endif

	HOGDescriptor hog(Size(20, 20), Size(10, 10), Size(5, 5), Size(5, 5), 9);

	Ptr<SVM> svm = train_hog_svm(hog);

	if (svm.empty()) {
		cerr << "Training failed!" << endl;
		return -1;
	}

	Mat img = Mat::zeros(400, 400, CV_8U);

	imshow("img", img);
	setMouseCallback("img", on_mouse, (void*)&img);

	while (true) {
		int c = waitKey();

		if (c == 27) {
			break;
		} else if (c == ' ') {
			Mat img_resize;
			resize(img, img_resize, Size(20, 20), 0, 0, INTER_AREA);

			vector<float> desc;
			hog.compute(img_resize, desc);

			Mat desc_mat(desc);
			int res = cvRound(svm->predict(desc_mat.t()));
			cout << res << endl;

			img.setTo(0);
			imshow("img", img);
		}
	}

	return 0;
}

Ptr<SVM> train_hog_svm(const HOGDescriptor& hog)
{
	Mat digits = imread("digits.png", IMREAD_GRAYSCALE);

	if (digits.empty()) {
		cerr << "Image load failed!" << endl;
		return 0;
	}

	Mat train_hog, train_labels;

	for (int j = 0; j < 50; j++) {
		for (int i = 0; i < 100; i++) {
			Mat roi = digits(Rect(i * 20, j * 20, 20, 20));

			vector<float> desc;
			hog.compute(roi, desc);

			Mat desc_mat(desc);
			train_hog.push_back(desc_mat.t());
			train_labels.push_back(j / 5);
		}
	}

	Ptr<SVM> svm = SVM::create();
	svm->setType(SVM::Types::C_SVC);
	svm->setKernel(SVM::KernelTypes::RBF);
	svm->setC(2.5);
	svm->setGamma(0.50625);
	svm->train(train_hog, ROW_SAMPLE, train_labels);

	return svm;
}

Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void* userdata)
{
	Mat img = *(Mat*)userdata;

	if (event == EVENT_LBUTTONDOWN)
		ptPrev = Point(x, y);
	else if (event == EVENT_LBUTTONUP)
		ptPrev = Point(-1, -1);
	else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
	{
		line(img, ptPrev, Point(x, y), Scalar::all(255), 40, LINE_AA, 0);
		ptPrev = Point(x, y);

		imshow("img", img);
	}
}

 

  • 13~16행: HOGDescriptor 클래스 구현상의 문제로 svmdigits 프로그램은 디버그 모드에서 실행 시 에러가 발생. 그러므로 디버그 모드로 실행할 때에는 문자열을 출력한 후 종료
  • 18행: HOGDesciptor 객체 hog를 생성
  • 20행: train_hog_svm() 함수를 이용하여 SVM을 학습
  • 37~46행: img 창에서 space 키를 누르면 img 영상을 20x20 크기로 변환한 후 HOG 특징 벡터를 계산. 계산된 HOG 특징 벡터를 1x324 크기의 행렬로 변환하여 SVM 결과를 예측하고, 그 결과를 콘솔창에 출력
  • 65~78행: digits.png 에 포함된 5000개의 필기체 숫자 부분 영상으로부터 각각 HOG 특징 벡터를 추출하여 5000x324 크기의 train_hog 행렬과 5000x1 크기의 train_labels 행렬을 생성
  • 80행: SVM 객체를 생성
  • 81~82행: SVM 타입은 C_SVC로 설정, 커널 함수는 RBF로 설정
  • 83~84행: 파라미터 C와 GAMMA 값을 각각 2.5, 0.50625로 설정
  • 85행: SVM 학습을 진행
  • 90~107행: 마우스를 이용하여 숫자 영상을 그림

 

 

]