[2023-05-02] 2. 머신러닝과 필기체 숫자 인식
머신러닝
- 주어진 데이터를 분석하여 규칙성, 패턴 등을 찾고, 이를 이용하여 의미 있는 정보를 추출하는 과정
머신러닝의 예
- 머신러닝에 의한 붓꽃 분류
- 붓꽃(IRIS) 품종 분류 : Setosa vs Versicolor
- 꽃잎(petal)과 꽃받침(sepal)의 길이에 의한 분류
- Adaline : Adaptive Linear Neruon
머신러닝의 종류
머신러닝의 단계
- 훈련(Train) : 훈련 데이터를 이용하여 모델 을 학습하는 과정
- 예측(Predict) : 학습된 모델을 이용하여 새로운 데이터로부터 적절한 값을 예측하는 과정. 추론(Inference)
머신러닝학습의 목적
- 미래의 새로운 데이터를 더 정확하게 예측하기 위함
- 모델의 일반화 성능을 향상시키는 방향으로 학습해야 함
-
과적합(overfitting)
-
훈련 데이터 셋을 지나치게 정확하게 구분하도록 학습하여 모델의 일반화 성능이 떨어지게 되는 현상
-
훈련 데이터 셋이 너무 적은 경우에 발생함
-
훈련 데이터 셋이 전체 데이터 셋의 특성/분포를 반영하지 않은 경우에도 발생함
-
-
모델이 복잡할수록 과적합 발생확률이 높음
-
훈련 데이터의 분할
- 학습 가능한 데이터를 훈련, 검증, 테스트 데이터 셋으로 분할하여 사용
- K-폴드 교차 검증(k-fold cross-validation)
- 훈련 데이터를 k개로 분할하여 여러 번 검증 수행
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 : 각 입력 샘플에 대한 예측(분류 or 회귀) 결과를 저장한 행렬
- flags : 추가적인 플래그 상수
- 반환값 : 입력 벡터가 하나인 경우에 대한 응답
서포터 벡터 머신(SVM) 알고리즘
- 기본적으로 2개의 그룹(데이터)을 분리하는 방법으로 데이터들과 거리가 가장 먼 초평면(hyperplane)을 선택하여 분리하는 방법
- 최대 마진 초평면 구하기
- 오분류 에러 허용하기
- 주어진 샘플을 완벽하게 두 개의 그룹으로 선형 분리를 할 수 없을 경우, 오분류 에러를 허용(Soft margin, C-SVM)
- 비선형 데이터 분류하기
- SVM은 선형 분류 알고리즘이지만 실제 데이터는 비선형으로 분포할 수 있음
- 비선형 데이터의 차원을 확장하면 선형으로 분리가 가능함
SVM 객체 생성
static Ptr<SVM> SVM::create();
- 비어있는 SVM 객체를 생성
- StatModel::train() 메소드를 이용하여 훈련을 해서 사용해야 함
SVM 타입 지정
Virtual void SVM::setType(int val)
- val : SVM::Types 열거형 상수 중 하나를 지정
SVM 커널 지정
Virtual void SVM::setKernel(int kernelType);
- kernelType : 커널 함수 종류. SVM::KernelTypes 열거형 상수 지정
SVM 자동 훈련
- K-폴드 교차 검증을 통해 최선의 파라미터를 찾아 훈련함
SVM을 이용한 2차원 점 분류 예제 코드
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::ml;
int main()
{
Mat train = Mat_<float>({ 8, 2 }, {
150, 200, 200, 250, 100, 250, 150, 300,
350, 100, 400, 200, 400, 300, 350, 400 });
// 첫번째 점의 x, y, 두번째 점의 x, y 순서로 총 8개 점의 좌표
Mat label = Mat_<int>({ 8, 1 }, { 0, 0, 0, 0, 1, 1, 1, 1 });
// 각각의 점들의 클래스 번호로 처음 4개의 점은 0번, 그 뒤에 4개의 점은 1번
Ptr<SVM> svm = SVM::create(); // svm 객체 생성
#if 1
svm->setType(SVM::C_SVC); // svm 객체 타입 설정
svm->setKernel(SVM::RBF); // svm 객체 커널 설정
svm->trainAuto(train, ROW_SAMPLE, label);
// 자동으로 최적의 하이퍼파라미터를 찾아 모델을 학습하는 함수
#else
svm->setType(SVM::C_SVC);
svm->setKernel(SVM::LINEAR);
svm->trainAuto(train, ROW_SAMPLE, label);
#endif
cout << svm->getC() << endl;
cout << svm->getGamma() << endl;
Mat img = Mat::zeros(Size(500, 500), CV_8UC3); 500 x 500의 비어있는 영상을 만듬
for (int y = 0; y < img.rows; y++) { // 각각의 픽셀값 좌표를
for (int x = 0; x < img.cols; x++) {
Mat test = Mat_<float>({ 1, 2 }, { (float)x, (float)y });
int res = cvRound(svm->predict(test));
if (res == 0)
img.at<Vec3b>(y, x) = Vec3b(128, 128, 255); // R 빨강색으로 채우기
else
img.at<Vec3b>(y, x) = 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();
}
- 점들의 분포는 다음과 같음
- 결과
- svm->setKernel(SVM::LINEAR);
HOG 알고리즘
-
Histogram of Oriented Gradients
- 영상의 지역적 그래디언트 방향 정보를 벡터로 사용
- 2005년 CVPR 학회에서 보행자 검출 방법으로 소개되어 널리 사용되기 시작함
- 이후 다양한 객체 인식에서 활용됨
- 방향성에 대한 히스토그램을 계산할 때는 20도로 나눠서 계산함
- 아래 그림과 같은 방법으로 9개 성분에 대한 히스토그램을 구함
- 블록 히스토그램
- 하나의 부분 영상 패치에서의 특징 벡터 크기는?
- 76(=64/8 -1)* 15(=128/8 -1) * 36(=4*9) = 3780
HOG & SVM 필기체 숫자 인식
- 만약 정해진 포트로 인쇄된 숫자라면?
- 템플릿 매칭으로도 가능함
- 하지만 필기체 인식은?
- 다수의 필기체 숫자 데이터가 필요함
HOG 특징 벡터를 이용한 SVM 학습
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::ml;
Mat img;
Point ptPrev(-1, -1);
void on_mouse(int event, int x, int y, int flags, void*)
{
if (x < 0 || x >= img.cols || y < 0 || y >= img.rows)
return;
if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) // 마우스가 떼질 때
ptPrev = Point(-1, -1);
else if (event == EVENT_LBUTTONDOWN) // 마우스가 눌릴 때
ptPrev = Point(x, y);
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) // 마우스가 눌려있으면서 움직일 때
{
Point pt(x, y);
if (ptPrev.x < 0)
ptPrev = pt;
line(img, ptPrev, pt, Scalar::all(255), 40, LINE_AA, 0);
ptPrev = pt;
imshow("img", img);
}
}
int main()
{
Mat digits = imread("digits.png", IMREAD_GRAYSCALE);
if (digits.empty()) {
cerr << "Image load failed!" << endl;
return -1;
}
#if _DEBUG // 디버그 모드일때 실행
HOGDescriptor hog(Size(20, 20), // _winSize
Size(8, 8), // _blockSize
Size(4, 4), // _blockStride,
Size(4, 4), // _cellSize,
9); // _nbins,
#else
HOGDescriptor hog(Size(20, 20), // _winSize
Size(10, 10), // _blockSize
Size(5, 5), // _blockStride,
Size(5, 5), // _cellSize,
9); // _nbins,
#endif
size_t descriptor_size = hog.getDescriptorSize();
cout << "Descriptor Size : " << descriptor_size << endl;
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)).clone();
// digits 영상의 (i*20, j*20) 좌표에서 시작해서 가로 20px, 세로 20px 를 짤라내서 roi에 저장함
vector<float> desc;
hog.compute(roi, desc); // 이 잘라낸 roi로부터 hog 계산
Mat desc_mat(desc, true);
// vector <float> desc로 계산이되는데 Mat 형태로 변환, 324 x 1 세로로 긴 형태로 만듬
train_hog.push_back(desc_mat.t());
// train_hog에 desc_mat(324 x 1)의 transpose(1 x 324)를 새로운 행을 하나씩 추가함
train_labels.push_back(j / 5); // 각각의 숫자자 이미지에 대한 클래스 번호를 담고 있음
// 처음 500개의 행렬은 0, 그 다음 500개의 행렬은 1 ... 이렇게 9까지
}
}
Ptr<SVM> svm = SVM::create(); // SVM 객체 생성
// Ptr<SVM> svm = SVM::load("svmdigits.yml");
svm->setType(SVM::C_SVC); // SVM 타입
svm->setKernel(SVM::RBF); // SVM 커널
#if 1
svm->setGamma(0.50625);
svm->setC(2.5);
svm->train(train_hog, ROW_SAMPLE, train_labels); // 위의 parameter 값을 이용해 train만 진행
#else
svm->trainAuto(train_hog, ROW_SAMPLE, train_labels);
// 이 함수를 실행하게되면 너무 오래걸려서 이 코드를 통해 계산된 결과 파라미터 값을 위에서 직접 입력하여 train만 진행함
#endif
// svm->save("svmdigits.yml"); // 학습된 결과는 sml 또는 yml 파일로 저장할 수 있음
// 이 파일을 불러오게되면 학습된 데이터가 다 들어있으므로 따로 학습할 필요는 없음
// 입력 이미지 생성
img = Mat::zeros(400, 400, CV_8U);
imshow("img", img);
setMouseCallback("img", on_mouse); // mouse콜백함수를 등록함
while (true) {
int c = waitKey();
if (c == 27) { // esc 키를 누르면 종료
break;
} else if (c == ' ') { // 사용자가 스페이스바를 누 르면
Mat img_blur, img_resize;
GaussianBlur(img, img_blur, Size(), 1); // 400 x 400 이미지를 블러랑함
resize(img_blur, img_resize, Size(20, 20), 0, 0, INTER_AREA); // 20 x 20 으로 resize를 함
vector<float> desc;
hog.compute(img_resize, desc); // resize된 이미지에서 hog 특징벡터를 계산함
Mat desc_mat(desc, true); // hog 특징벡터를 매트리스 형식으로 변환
float res = svm->predict(desc_mat.t()); // 324 x 1 형태의 매트리스를 1 x 324로 transpose한 행렬을 SVM알고리즘의 predict 함수로 전달하여 그 결과 값일 res로 저장
cout << cvRound(res) << endl; // res를 반올림한 후 출력
img.setTo(0);
imshow("img", img);
} else if (c == 'c') {
img.setTo(0);
imshow("img", img);
}
}
}
- 실행된 창에서 숫자를 입력하고 space바를 누르면 인식된 숫자를 출력함
- 입력할 때 가운데에 입력해야 제대로 인식함
- 한쪽에 치우쳐서 입력하면 다른 쪽에서 그 숫자가 가지는 엣지 정보를 얻을 수 없으므로 인식률이 떨어질 수 밖에 없음
- digits.png 데이터를 80 : 20 으로 나눠서 검증하면 98.7%의 정확도를 가짐
숫자 영상 정규화
-
훈련 데이터 영상과 테스트 데이터 영상의 위치, 크기, 회전 등의 요소를 정규화하여 인식 성능을 향상 시킬 수 있음
-
숫자 영상의 바운딩 박스를 기준으로 중앙으로 위치를 보정
-
무게중심을 중앙에 위치하도록 위치 보정
-
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
using namespace cv::ml;
Mat img;
Point ptPrev(-1, -1);
void on_mouse(int event, int x, int y, int flags, void*)
{
if (x < 0 || x >= img.cols || y < 0 || y >= img.rows)
return;
if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) // 마우스가 떼질 때
ptPrev = Point(-1, -1);
else if (event == EVENT_LBUTTONDOWN) // 마우스가 눌릴 때
ptPrev = Point(x, y);
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) // 마우스가 눌려있으면서 움직일 때
{
Point pt(x, y);
if (ptPrev.x < 0)
ptPrev = pt;
line(img, ptPrev, pt, Scalar::all(255), 40, LINE_AA, 0);
ptPrev = pt;
imshow("img", img);
}
}
Mat norm_digit(Mat& src) // 작은 크기의 이미지를 인자로 받아와서
{
CV_Assert(!src.empty() && src.type() == CV_8UC1);
Mat src_bin;
threshold(src, src_bin, 0, 255, THRESH_BINARY | THRESH_OTSU); // 이진화를 수행
Mat labels, stats, centroids;
int n = connectedComponentsWithStats(src_bin, labels, stats, centroids);
// connectedCompentsWithStats를 통해 lalbeling 작업을 진행함
// centroids에 각각의 객체의 무게중심 좌표를 double 값으로 결과를 줌
Mat dst = Mat::zeros(src.rows, src.cols, src.type());
for (int i = 1; i < n; i++) {
if (stats.at<int>(i, 4) < 10) continue;
int cx = cvRound(centroids.at<double>(i, 0)); // 무게중심 좌표의 x 좌표를 받아옴
int cy = cvRound(centroids.at<double>(i, 1)); // 무게중심 좌표의 y 좌표를 받아옴
double dx = 10 - cx;
double dy = 10 - cy; // (10,10) 위치가 되도록
Mat warpMat = (Mat_<double>(2, 3) << 1, 0, dx, 0, 1, dy); // (10,10) 위치가 되도록 이동 변환을 수행함
warpAffine(src, dst, warpMat, dst.size());
}
return dst;
}
int main()
{
Mat digits = imread("digits.png", IMREAD_GRAYSCALE);
if (digits.empty()) {
cerr << "Image load failed!" << endl;
return -1;
}
#if _DEBUG // 디버그 모드일때 실행
HOGDescriptor hog(Size(20, 20), // _winSize
Size(8, 8), // _blockSize
Size(4, 4), // _blockStride,
Size(4, 4), // _cellSize,
9); // _nbins,
#else
HOGDescriptor hog(Size(20, 20), // _winSize
Size(10, 10), // _blockSize
Size(5, 5), // _blockStride,
Size(5, 5), // _cellSize,
9); // _nbins,
#endif
size_t descriptor_size = hog.getDescriptorSize();
cout << "Descriptor Size : " << descriptor_size << endl;
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)).clone();
// digits 영상의 (i*20, j*20) 좌표에서 시작해서 가로 20px, 세로 20px 를 짤라내서 roi에 저장함
vector<float> desc;
hog.compute(norm_digit(roi), desc); // 이 잘라낸 roi로부터 hog 계산
Mat desc_mat(desc, true);
// vector <float> desc로 계산이되는데 Mat 형태로 변환, 324 x 1 세로로 긴 형태로 만듬
train_hog.push_back(desc_mat.t());
// train_hog에 desc_mat(324 x 1)의 transpose(1 x 324)를 새로운 행을 하나씩 추가함
train_labels.push_back(j / 5); // 각각의 숫자자 이미지에 대한 클래스 번호를 담고 있음
// 처음 500개의 행렬은 0, 그 다음 500개의 행렬은 1 ... 이렇게 9까지
}
}
Ptr<SVM> svm = SVM::create(); // SVM 객체 생성
// Ptr<SVM> svm = SVM::load("svmdigits.yml");
svm->setType(SVM::C_SVC); // SVM 타입
svm->setKernel(SVM::RBF); // SVM 커널
#if 1
svm->setGamma(0.50625);
svm->setC(2.5);
svm->train(train_hog, ROW_SAMPLE, train_labels); // 위의 parameter 값을 이용해 train만 진행
#else
svm->trainAuto(train_hog, ROW_SAMPLE, train_labels);
// 이 함수를 실행하게되면 너무 오래걸려서 이 코드를 통해 계산된 결과 파라미터 값을 위에서 직접 입력하여 train만 진행함
#endif
// svm->save("svmdigits.yml"); // 학습된 결과는 sml 또는 yml 파일로 저장할 수 있음
// 이 파일을 불러오게되면 학습된 데이터가 다 들어있으므로 따로 학습할 필요는 없음
// 입력 이미지 생성
img = Mat::zeros(400, 400, CV_8U);
imshow("img", img);
setMouseCallback("img", on_mouse); // mouse콜백함수를 등록함
while (true) {
int c = waitKey();
if (c == 27) { // esc 키를 누르면 종료
break;
} else if (c == ' ') { // 사용자가 스페이스바를 누 르면
Mat img_blur, img_resize;
GaussianBlur(img, img_blur, Size(), 1); // 400 x 400 이미지를 블러랑함
resize(img_blur, img_resize, Size(20, 20), 0, 0, INTER_AREA); // 20 x 20 으로 resize를 함
vector<float> desc;
hog.compute(norm_digit(img_resize), desc); // resize된 이미지에서 hog 특징벡터를 계산함
Mat desc_mat(desc, true); // hog 특징벡터를 매트리스 형식으로 변환
float res = svm->predict(desc_mat.t()); // 324 x 1 형태의 매트리스를 1 x 324로 transpose한 행렬을 SVM알고리즘의 predict 함수로 전달하여 그 결과 값일 res로 저장
cout << cvRound(res) << endl; // res를 반올림한 후 출력
img.setTo(0);
imshow("img", img);
} else if (c == 'c') {
img.setTo(0);
imshow("img", img);
}
}
}