머신 러닝과 OpenCV
머신 러닝 개요
•
머신 러닝(machine learning)이란 주어진 데이터를 분석하여 규칙성, 패턴 등을 찾고 이를 이용하여 의미 있는 정보를 추출하는 과정.
◦
데이터로부터 규칙을 찾아내는 과정을 학습(train) 또는 훈련이라고 하고, 학습에 의해 결정된 규칙을 모델(model)이라 한다. 그리고 새로운 데이터를 학습된 모델에 입력으로 전달하고 결과를 판단하는 과정을 예측(predict) 또는 추론(inference)라고 한다.
•
머신 러닝은 크게 지도 학습(supervised learning)과 비지도 학습(unsupervised learning)으로 구분된다.
◦
지도 학습은 정답을 알고 있는 데이터를 이용하여 학습을 진행하는 방식으로 훈련 데이터에 대한 정답에 해당하는 내용을 레이블(label)이라고 한다.
◦
아래 그림은 지도 학습 방식으로 영상을 인식하는 과정을 나타낸다.
•
영상 데이터는 픽셀로 구성되어 있지만, 이 픽셀 값을 그대로 머신 러닝 입력으로 사용하지는 않는다. 영상의 픽셀 값은 조명 변화, 객체의 이동 및 회전 등에 의해 매우 민감하게 변화하기 때문.
◦
때문에 많은 머신 러닝 응용에서는 영상의 다양한 변환에도 크게 변경되지 않는 특징 정보를 추출하여 머신 러닝으로 전달한다.
◦
이처럼 영상 데이터를 사용하는 지도 학습에서는 먼저 다수의 훈련 영상에서 특징 벡터를 추출하고, 이를 이용하여 머신 러닝 알고리즘을 학습 시킨다.
◦
학습의 결과로 생성된 학습 모델은 이후 예측 과정에서 사용된다. 예측 과정에서도 입력 영상으로부터 특징 벡터를 추출하고, 이 특징 벡터를 학습 모델 입력으로 전달하면 입력 영상이 어떤 영상인지에 대한 예측 결과를 얻을 수 있다.
•
지도 학습은 주로 회귀(regression) 또는 분류(classfication)에 사용된다.
◦
회귀는 연속된 수치 값을 예측하는 작업으로 학생들의 키와 몸무게의 상관관계를 학습하고, 새로운 학생의 키를 입력으로 주었을 때 몸무게를 예측하는 것과 같은 것이다.
◦
분류는 이산적인 값을 결과로 출력하는 머신 러닝으로 사과와 바나나를 구분 –또는 인식(recognition)– 하는 것이 이에 해당한다.
•
비지도 학습은 훈련 데이터의 정답에 대한 정보 없이 오로지 데이터 자체만을 이용하는 학습 방식이다.
◦
예컨대 무작위로 섞여 있는 사과와 바나나 사진을 두 개의 그룹으로 나누도록 학습시키는 방식이다. 이 경우 분리된 두 개의 사진 집합이 무엇을 의미하는지는 알수 없고, 단지 두 사진 집합에서 서로 구분되는 특징을 이용하여 서로 분리하는 작업만 수행한다.
◦
비지도 학습은 주로 군집화(clustering) 에 사용된다.
•
머신 러닝 알고리즘 종류에 따라 내부적으로 사용하는 많은 파라미터에 의해 성능이 달라지기도 한다. 그러므로 최적의 파라미터를 찾는 것이 해결해야 하는 과제가 되기도 한다.
◦
이런 경우 훈련 데이터를 k개의 부분 집합으로 분할하여 학습과 검증(validation)을 반복하면서 최적의 파라미터를 찾을 수 있다.
◦
예컨대 8000개의 훈련 영상을 800개씩 열 개의 부분 집합으로 분할하고 이 중 아홉 개의 부분 집합으로 학습하고 나머지 한 개의 집합을 이용하여 성능을 검증한다. 그리고 검증을 위한 부분 집합을 바꿔가면서 여러 번 학습과 검증을 수행한다.
◦
이처럼 훈련 데이터를 k개의 부분 집합으로 분할하여 학습과 검증을 반복하는 작업을 k-폴드 교차 검증(k-fold cross-validation)이라 한다.
•
머신 러닝 알고리즘으로 훈련 데이터를 학습할 경우 훈련 데이터에 포함된 잡음 또는 이상치(outlier)의 영향을 고려해야 한다.
OpenCV 머신 러닝 클래스
•
OpenCV는 다양한 머신 러닝 알고리즘을 클래스로 구현하여 제공한다.
◦
OpenCV에서 제공하는 머신 러닝 클래스는 주로 ml 모듈에 포함되어 있고, cv::ml::StatModel 추상 클래스를 상속받아 만들어진다.
◦
StatModel 클래스 이름은 통계적 모델(statistical model)을 의미한다.
•
StatModel 추상 클래스를 상속 받아 만들어진 머신 러닝 알고리즘 구현 클래스는 아래 그림과 같다.
◦
StatModel 클래스는 머신 러닝 알고리즘을 학습시키는 StatModel::train() 멤버 함수를 갖고 있다. StatModel 클래스를 상속 받아 만든 머신 러닝 구현 클래스는 각각의 머신 러닝 알고리즘에 해당하는 train()과 predict() 기능을 재정의하고 있다.
•
StatModel::train() 함수는 samples에 저장된 다수의 훈련 데이터를 사용하여 머신 러닝 알고리즘을 학습한다.
◦
이때 훈련 데이터에 대한 정답 또는 레이블 정보는 response 인자로 전달한다.
◦
보통 samples와 responses 인자는 Mat 타입 객체로 전달한다.
◦
Mat 행렬에 훈련 데이터가 어떤 방식으로 저장되어 있는지를 layout 인자로 설정한다. layout에는 RAW_SAMPLE(행 단위)과 COL_SAMPLE(열 단위) 상수를 지정할 수 있다.
◦
StatModel 클래스를 상속받은 클래스 객체에서 train() 함수를 호출하면 각 머신 러닝 알고리즘에 해당하는 방식으로 학습을 진행한다.
•
이미 학습된 모델에 대해 테스트 데이터의 응답을 얻고 싶으면 StatModel::predict() 함수를 사용하면 된다.
◦
StatModel::predict() 함수는 순수 가상 함수로 선언되었으며, 각각의 머신 러닝 알고리즘 구현 클래스는 자신만의 알고리즘을 이용한 예측을 수행하도록 predict() 함수를 재정의하고 있다.
◦
일부 머신 러닝 알고리즘 구현 클래스는 predict(0 대신 고유의 예측 함수를 이용하기도 한다.
•
OpenCV에서 StatModel 클래스를 상속받아 만들어진 머신 러닝 알고리즘 구현 클래스에 대한 설명은 아래 표에 있다.
k 최근접 이웃
k 최근접 이웃 알고리즘
•
k 최근접 이웃(kNN, k-Nearest Neighbor) 알고리즘은 분류 또는 회귀에 사용되는 지도 학습 알고리즘의 하나이다.
◦
kNN 알고리즘을 분류에 사용할 경우 특징 공간에서 테스트 데이터와 가장 가까운 k개의 훈련 데이터를 찾고, k개의 훈련 데이터 중에서 가장 많은 클래스를 테스트 데이터의 클래스로 지정한다.
◦
kNN 알고리즘으 ㄹ회귀 문제에 적용할 경우에는 테스트 데이터에 인접합 k개의 훈련 데이터 평균을 테스트 데이터 값으로 설정한다.
•
아래 그림은 kNN 알고리즘 동작 방식에 대한 예시이다.
◦
아래 그림은 2차원 평면상에 파란색 사각형과 빨간색 삼각형 두 종류의 데이터가 분포되어 있는데, 파란색과 빨간 점들이 훈련된 데이터이고, 이 훈련된 데이터는 2개의 클래스로 구분되어 있다.
◦
각 점들은 (x, y) 좌표로 표현되므로, 이들 데이터는 2차원 특징 곤간에 정의되어 있다고 할 수 있다.
◦
여기에 녹색으로 새로운 점을 추가할 경우, 이 점을 파란색으로 분류 할지 빨간색으로 분류할지를 결정해야 하는데, 간단한 방법은 새로 들어온 점과 가장 가까이 있는 점을 찾아 해당 데이터와 같은 클래스로 분류하는 방법이다.
◦
아래 그림 상 녹색 점과 가장 가까운 점은 빨간색 삼각형이므로 녹색 점을 빨간색 삼각형과 같은 클래스로 지정할 수 있다.
◦
이러한 방법은 최근접 이웃(NN, Nearest Neighbor) 알고리즘이라 한다.
◦
그러나 녹색 점 주변에 분포로는 빨간색 삼각형보다 파란색 사각형이 더 많은데, 이와 같은 이유로 녹색점을 파란색 사각형으로 분류하는 방식을 kNN 알고리즘이라고 한다.
•
kNN 알고리즘에서 k를 1로 설정하면 최근접 이웃 알고리즘이 된다. 그러므로 보통 k는 1보다 큰 값을 설정하며, k값을 어떻게 설정하느냐에 따라 분류 및 회귀 결과가 달라질 수 있다.
◦
최선의 k 값을 결정하는 것은 주어진 데이터에 의존적이며, 보통 k값이 커질수록 잡음 또는 이상치 데이터의 영향이 감소한다. 그러나 k값이 어느 정도 이상으로 커질 경우 오히려 분류 및 회귀 성능이 떨어질 수 있다.
KNearest 클래스 사용하기
•
OpenCV에서 k 최근접 이웃 알고리즘은 KNearest 클래스에 구현되어 있다.
◦
(KNearest의 함수 설명 생략)
◦
KNearest 객체는 기본적으로 분류를 위한 용도로 사용된다. 만일 KNearest 객체를 분류가 아닌 회귀에 적용하려면 KNearest::setIsClassifier() 멤버 함수에 false를 지정하여 호출하면 된다.
◦
KNearest 객체를 생성하고 속성을 설정한 후에는 StatModel::train() 함수를 통해 학습을 진행할 수 있는데, KNearest 클래스의 경우에는 train() 함수에서 실제적인 학습이 진행되지는 않으며 단순히 훈련 데이터와 레이블 데이터를 KNearest 클래스 멤버 변수에 모두 저장하는 작업이 이루어진다.
•
KNearest 클래스에서 훈련 데이터를 학습한 후 테스트 데이터에 대한 예측을 수행할 때는 KNearest::findNearest() 멤버 함수를 사용한다.
◦
이는 StatModel::predict() 보다 KNearest::findNearest() 함수가 예측 결과와 관련된 정보를 더 많이 반환하기 때문이다.
◦
KNearest::findNearest() 함수는 samples 행렬 각 행에 저장된 테스트 데이터와 가까운 k개의 훈련 데이터를 찾아 분류 또는 회귀 응답을 반환한다.
#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);
else if (response == 1)
img.at<Vec3b>(i, j) = Vec3b(128, 255, 128);
else if (response == 2)
img.at<Vec3b>(i, j) = Vec3b(255, 128, 128);
}
}
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);
}
C++
kNN을 이용한 필기체 숫자 인식
•
20 x 20 숫자 영상 픽셀값 자체를 kNN 알고리즘 입력으로 사용하는 예시
◦
5000개의 숫자 영상 데이터의 한 장의 숫자 영상은 20 x 20 픽셀 크기이고, 이 픽셀 값을 모두 일렬로 늘어 놓으면 1 x 400 크기의 행렬로 변환할 수 있다.
◦
즉 필기체 숫자 훈련 데이터 하나는 400개의 숫자 값으로 표현되고, 이는 400차원 공간에서의 한 점과 같다.
◦
digits.png 영상에 있는 각각의 숫자 영상을 1 x 400 행렬로 바꾸고, 이 행렬을 모두 세로로 쌓으면 전체 숫자 영상 데이터를 표현하는 5000 x 400 크기의 행렬을 만들 수 있다. 그리고 이 행렬을 KNearest 클래스의 훈련 데이터로 전달한다.
◦
kNN 알고리즘으로 필기체 숫자 영상을 학습시키려면 각 필기체 숫자 영상이 나타내는 숫자 값을 레이블 행렬로 함께 전달해야 한다. 이 레이블 행렬의 행 크기는 훈련 데이터 영상 개수와 같고, 열 크기는 1이된다.
◦
아래 그림에서 첫 행은 0, 그 다음 행은 1에 대한 데이터이므로 레이블 행렬도 첫 행의 원소는 0으로 설정하고 그 다음 해으이 원소는 1로 설정한다. 그렇게 모든 행의 원소를 설정한 후, KNearest 클래스의 레이블 데이터로 전달한다.
#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;
}
Ptr<KNearrest> 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;
}
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);
}
}
C++
서포트 벡터 머신
서프트 벡터 머신 알고리즘
•
서포트 벡터 머신(SVM, Support, Vector Machine)은 기본적으로 두 개의 클래스로 구성된 데이터를 가장 여유 있게 분리하는 초평면(hyperplane)을 찾는 머신 러닝 알고리즘이다.
◦
초평면이란 두 클래스의 데이터를 분리하는 N차원 공간상의 평면을 의미한다. 예컨대 2차원 공간상의 점들을 분리하는 초평면은 단순한 직선 형태로 정의되며, 3차원 공간상의 점들을 분리하는 초평면은 3차원 공간에서의 평면의 방정식으로 표현할 수 있다.
◦
SVM 알고리즘은 지도 학습의 일종으로 분류와 회귀에 사용될 수 있다.
•
아래 그림은 SVM 알고리즘에 대한 예시이다.
◦
아래 그림은 파란색 사각형과 빨간색 삼각형으로 표시된 두 클래스의 점들의 분포를 나타내는데, 이 두 클래스 점들을 구분하기 위한 직선은 매우 다양하게 만들 수 있다.
◦
그림 (a)의 1, 2번 직선은 모두 두 종류의 점들을 잘 분리하지만, 1번 직선은 조금만 왼쪽이나 오른쪽으로 이동해도 분리에 실패하게 되고, 2번 직선도 오른쪽으로 조금만 이동하면 분리에 실패하게 된다.
◦
이는 1, 2번 직선이 모두 입력 점 데이터에너무 가까이 위치하고 있기 때문인데, 그림 (b)의 3번 직선은 두 클래스 점들 사이를 충분히 여유 있게 분할하고 있어서 그런 문제가 없다.
◦
이때 3번 직선에 해당하는 초평면과 가장 가까이 있는 빨간색 또는 파란색 점들과의 거리를 마진(margin)이라 하며, SVM은 이 마진을 최대로 만드는 초평면을 구하는 알고리즘이다.
•
SVM은 기본적으로 선형으로 분리 가능한 데이터에 적용할 수 있다.
◦
그러나 실생활에서 사용하는 데이터는 선형으로 분리되지 않는 경우가 많으며, 이러한 경우에도 SVM 알고리즘을 적용하기 위해 SVM에서는 커널 트릭(kernel trick)이라는 기법을 사용한다.
◦
커널 트릭이란 적절한 커널 함수를 이용하여 입력 데이터 특징 공간 차원을 늘리는 방식이다. 원본 데이터 차원에서는 선형으로 분리할 수 없었던 데이터를 커널 트릭으로 고차원 특징 공간으로 이동하면 선형으로 분리 가능한 형태로 바뀔 수 있다.
•
데이터 특징 공간 차원을 증가시켜서 데이터를 선형 분리하는 예는 다음과 같다.
◦
2차원 좌표 평면 상의 점 집합 X = { (0, 0), (1, 1) }과 Y = { (1, 0), (0, 1) }이 있다고 가정하고, 이 두 클래스 점들을 아래 그림 처럼 각각 파란색과 빨간색 점으로 나타냈다.
◦
2차원 평면상에서 X, Y 두 클래스 점들을 분리할 수 있는 직선은 존재하지 않는데, 입력 점들의 좌표에 가상의 z축 좌표를 형태로 추가할 경우, X = { (0, 0, 0), (1, 1, 0) }과 Y = { (1, 0, 1), (0, 1, 1) } 형태로 3차원 공간상에서의 점 집합으로 바뀌게 된다.
◦
이렇게 차원 공간으로 변경된 X와 Y 점들을 아래 그림의 (b)처럼 그릴 수 있다. 그리고 이 두 클래스 점들은 z = 0.5 평면의 방정식을 이용하여 효과적으로 분리할 수 있다.
◦
2차원 평면에서 선형 분리할 수 없었던 X와 Y 데이터 집합이 가상의 차원을 추가함으로써 선형으로 분리될 수 있게 된 것이다.
•
SVM 알고리즘에서 사용할 수 있는 커널 함수의 종류는 아래 표와 같다.
◦
아래 표에서 가장 널리 사용되는 커널은 방사 기저 함수이며, 이 커널을 사용할 때는 인자 값을 적절히 설정해야 한다.
◦
만약 입력 데이터가 선형으로 분리 가능하다면 선형 커널을 사용하는 것이 가장 빠르게 동작한다.
SVM 클래스 사용하기
•
OpenCV에서 SVM 알고리즘은 SVM 클래스에 구현되어 있다. OpenCV에 구현된 SVM 클래스는 오픈소스 라이브러리인 LIBSVM을 기반으로 만들어졌다.
◦
SVM 클래스는 기본적으로 SVM::Types::C_SVC 타입을 사용하도록 초기화되며 다른 타입을 사용하려면 SVM::setType() 함수를 이용하여 타입을 변경할 수 있다.
◦
SVM::Types::C_SVC 타입을 사용하는 경우 SVM 알고리즘 내부에서 사용하는 C 파라미터 값을 적절하게 설정해야 하는데, C 값을 작게 설정하면 훈련 데이터 중에 잘못 분류된느 데이터가 있어도 최대 마진을 선택하고, C 값을 크게 설정하면 마진이 작아지더라도 잘못 분류되는 데이터가 적어지도록 분류한다.
◦
만약 훈련 샘플 데이터에 잡음 또는 이상치 데이터가 많이 포함된 경우에는 C 파라미터 값을 크게 설정하는 것이 유리하다.
•
SVM 타입 설정 후에 SVM 알고리즘에서 사용할 커널 함수를 지정해야 한다. 함수 지정은 SVM::setKernel() 함수를 이용하면 된다.
•
SVM 타입과 커널 함수 종류를 설정한 후에는 각각의 타입과 커널 함수 정의에 필요한 파라미터를 설정해야 한다.
◦
SVM 클래스에서 설정할 수 있는 파라미터는 C, Nu, P, Degree, Gamma, Coef0 등이 있으며, 이들 파라미터는 차례대로 1, 0, 0, 0, 1, 0으로 초기화 된다.
◦
각각의 파라미터는 파라미터 이름에 해당하는 setXXX()와 getXXX(0 함수를 이용하여 값을 설정하거나 읽어올 수 있다.
•
SVM 객체를 생성하고 타입과 커널 함수, 파라미터를 설정한 후에는 StatModel::train() 함수를 이용하여 학습을 시킬 수 있다.
◦
그러나 SVM에서 사용하는 파라미터를 적절하게 설정하지 않으면 학습이 제대로 되지 않는데, OpenCV에서는 각각의 파라미터에 대해 설정 가능한 값을 적용해 보고 그중 가장 성능이 좋은 파라미터를 자동으로 찾아 학습하는 SVM::trainAuto() 함수를 제공한다.
◦
다만 SVM::trainAuto() 함수는 매우 느리기 때문에 한 번 학습이 완료된 후 선택된 파라미터를 저장했다가 재사용하는 편이 좋다.
•
SVM 학습이 완료되었다면 StatModel::predict()를 통해 테스트 데이터에 대한 예측을 수행할 수 있다.
#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::Types::C_SVC);
svm->setKernel(SVM::KernelTypes::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);
else
img.at<Vec3b>(j, i) = Vec3b(128, 255, 128);
}
}
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)
cicle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA);
else
cicle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA);
}
imshow("svm", img);
waitKey();
return 0;
}
C++
HOG & SVM 필기체 숫자 인식
•
kNN으로 했던 필기체 인식의 SVM 버전
◦
각 숫자 영상에서 HOG 특징 벡터를 추출한 후 SVM 알고리즘 입력 데이터로 사용한다.
◦
HOG 특징 벡터 추출을 위해 HOGDescriptor 클래스를 사용한다.
#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()
{
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);
setMoustCallback("img", on_mouse, (void*)&img);
while(true)
{
int c = waitKey(0);
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);
}
}
// 아래 상수값은 SVM::trainAuto()를 통해 구한 값이다.
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);
}
}
C++