OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ 딥러닝과 OpenCV

딥러닝과 OpenCV DNN 모듈

신경망과 딥러닝

  • 딥러닝(deep learning)은 2000년대부터 사용되고 있는 심층 신경망(deep neural network)의 또 다른 이름이다.
    • 신경망(neural network)은 인공 신경망(artificial neural network)라고도 불리며, 이는 사람의 뇌 신경 세포(neuron)에서 일어나는 반응을 모델링하여 만들어진 고전적인 머신 러닝 알고리즘 이다.
    • 즉, 딥러닝이란 신경망을 여러 계층(layer)으로 쌓아서 만든 머신 러닝 알고리즘 일종이다.
    • 컴퓨터 비전 분야에서 딥러닝이 주목 받는 이유는 객체 인식, 얼굴 인식, 객체 검출, 분할 등의 영역에서 딥러닝이 기존 기술보다 월등한 성능을 보여주고 있기 때문
  • 아래 그림은 전통적인 머신 러닝과 딥러닝에 의한 학습 및 인식 과정을 나타낸 것이다.
    • 기존의 머신 러닝 학습에서는 영상으로부터 인식에 적합한 특징을 사람이 추출하며 머신 러닝 알고리즘 입력으로 전달한다.
    • 그러면 머신 러닝 알고리즘이 특징 벡터 공간에서 여러 클래스 영상을 상호 구분하기에 적합한 규칙을 찾아낸다.
    • 이때 사람이 영상에서 추출한 특징이 영상 인식에 적합하지 않다면 어떤 머신 러닝 알고리즘을 사용한다고 하더라도 좋은 인식 성능을 나타내기는 어렵다.
    • 최근의 딥러닝은 특징 추출과 학습을 모두 딥러닝이 알아서 수행한다. 즉 여러 영상을 분류하기 위해 적합한 특징을 찾는 것과 이 특징을 잘 구분하는 규칙까지 딥러닝이 한꺼번에 찾아낼 수 있다.

  • 딥러닝은 신경망을 여러 계층으로 쌓아서 만든 구조이므로 딥러닝을 이해하려면 신경망에 대한 이해가 필요하다.
    • 신경망의 가장 기초적인 형태는 1950년대 개발된 퍼셉트론(perceptron) 구조이다. 퍼셉트론 구조는 기본적으로 다수의 입력으로부터 가중합을 계산하고, 이를 이용하여 하나의 출력을 만들어 내는 구조이다.
    • 단순한 형태의 퍼셉트론 구조가 아래 그림과 같은데, 그림의 원을 노드(node) 또는 정점(vertex)라고 하고, 노드 사이에 연결된 선으 ㄹ에지(edge) 또는 간선이라 한다.
    • 그림 왼쪽의 x_{1}, x_{2} 노드는 입력 노드이고 오른쪽의 y 는 출력 노드이다.
    • 입력 노드로 이루어진 계층을 입력층(input layer)이라 하고, 출력 노드로 이루어진 계층을 출력층(output layer)이라고 한다.
    • 각각의 에지는 가중치(weight)를 가지며, 아래 그림에서는 두 개의 에지에 각각 w_{1}, w_{2} 의 가중치가 지정되어 있다.

  • 이 퍼셉트론의 출력 y 는 다음 수식에 의해 결정된다.
    • 아래 수식에서 b 는 편향(bias)라고 부르며 y 값 결정에 영향을 줄 수 있는 파라미터이다.

y = \begin{cases} 1 & w_{1} x_{1} + w_{2} x_{2} + b \geq 0 \\ -1 & w_{1} x_{1} + w_{2} x_{2} + b < 0 \end{cases}

  • 기본적인 퍼셉트론을 이용하여 분류를 하는 예
    • 아래 그림은 2차원 평면상에 두 개의 클래스로 나눠진 점들의 분포를 나타낸다. 빨간색 점과 파란색 점을 분류하기 위해 퍼셉트론을 사용할 경우 가중치는 w_{1} = w_{2} = 1 로 설정하고, 편항은 b = -0.5 로 설정할 수 있다. 이 경우 출력 y 는 다음과 같이 결정된다.

y = \begin{cases} 1 & x_{1} + x_{2} - 0.5 \geq 0 \\ -1 & x_{1} + x_{2} - 0.5 < 0 \end{cases}

  • 이처럼 기본적인 퍼셉트론은 입력 데이터를 두 개의 클래스로 선형 분류하는 용도로 사용할 수 있는데, 좀 더 복잡한 형태로 분포되어 있는 데이터 집합에 대해서는 노드의 개수를 늘리거나, 입력과 출력 사이에 여러 개의 은닉층(hidden layer)을 추가하는 형태로 구조를 발전시켜 해결 할 수 있다.
    • 아래 그림은 여러 개의 은닉층이 존재하는 다층 퍼셉트론(MLP, Multi-Layer Perceptron) 구조의 예이다.

  • 신경망이 주어진 문제를 제대로 해결하려면 신경망 구조가 문제에 적합해야 하고, 에지에 적절한 가중치가 부여되어야 한다.
    • 에지의 가중치와 편향값은 경사 하강법(gradient descent), 오류 역전파(error backpropagation) 등의 알고리즘에 의해 자동으로 결정할 수 있다.
    • 신경망에서 학습이란 결국 훈련 데이터셋을 이용하여 적절한 에지 가중치와 편향 값을 구하는 과정이라 할 수 있다.
  • 2000년 초반까지 신경망은 크게 발전하지 못했는데, 은닉층이 많아질수록 학습 시간이 오래 걸리고 학습도 제대로 되지 않았기 때문.
    • 그러다가 2000년 후반, 2010년 초반부터 신경망은 심층 신경망 또는 딥러닝이라는 이름으로 크게 발전하기 시작했다.
    • 딥러닝이 크게 발전한 이유는 3가지를 꼽을 수 있는데, 첫째는 딥러닝 알고리즘이 개선되면서 은닉층이 많아져도 –이래서 deep이다– 학습이 제대로 이루어지게 되었다는 점 , 둘째는 하드웨어의 발전 특히 GPU 성능 향상과 GPU를 활용한 방법으로 학습 시간이 크게 단축되었다는 점, 셋째는 인터넷의 발전으로 빅데이터 활용이 용이해졌다는 점이 그것이다.
    • 특히 컴퓨터 비전 분야에서는 Pascal VOC, ImageNet 과 같이 잘 다듬어진 영상 데이터를 활용할 수 있다는 점이 강점으로 작용했다. 대용량 데이터셋을 이용한 영상 인식 대회 등을 통해 알고리즘 경쟁과 공유가 활발하게 이루어졌다는 점도 딥러닝 발전에 긍정적인 영향을 끼쳤다.
  • 다양한 딥러닝 구조 중에서 특히 영상을 입력으로 사용하는 영상 인식, 객체 검출 등의 분야에서는 합성곱 신경망(CNN, Convolutional Neural Network) 구조가 널리 사용되고 있다.
    • CNN 구조는 보통 2차원 영상에서 특징을 추출하는 컨볼루션(convolution) 레이어와 추출된 특징을 분류하는 완전 연결(FC, Fully Connected) 레이어로 구성된다.
    • 아래 그림은 영상 분류를 위한 일반적인 CNN 네트워크의 구조를 나타낸다.
    • CNN 구조에서 컨볼루션은 필터링과 유사한 성격을 가지며, 영상의 지역적인 특징을 추출하는 역할을 담당한다.
    • 풀링(pooling)은 비선형 다운샘플링(down sampling)을 수행하여 데이터양을 줄이고, 일부 특징을 강조하는 역할을 한다.
    • 완전 연결 레이어는 고전적인 다층 퍼셉트론과 비슷한 구조로서 앞서 추출된 특징을 이용하여 출력 값을 결정한다.
    • 보통 컨볼루션 레이어를 여러 개 연결하고, 맨 뒤에 완전 연결 레이어를 연결하는 형태로 CNN 네트워크를 구성한다.

  • 컴퓨터 비전 분야에서 사용되는 딥러닝 알고리즘은 대부분 CNN 구조를 기본으로 사용하면서 인식의 정확도를 높이거나 연산 속도를 빠르게 하는 등의 목적에 맞게 변형된 형태이다.
    • 컨볼루션 단계에서 사용하는 커널을 1 x 1, 3 x 3, 5 x 5 등의 다양한 크기로 구성하기도 하고, 레이어 사이의 연결 방식도 새롭게 설계하여 효과적인 성능을 얻기도 한다.

OpenCV DNN 모듈

  • 딥러닝은 특히 컴퓨터 비전에서 가장 활발하게 적용되고 있는데, OpenCV는 이러한 트렌드를 이해하고 OpenCV 3.1 부터 딥러닝을 활용할 수 있는 dnn(deep neural network) 모듈을 제공하기 시작했다.
    • OpenCV dnn 모듈은 이미 만들어진 네트워크에서 순방향 실행을 위한 용도로 설계되었다. 즉 딥러닝 학습은 기존의 유명한 카페(Caffe), 텐서플로(TensorFlow) 등의 다른 딥러닝 프레임워크에서 진행하고, 학습된 모델을 불러와서 실행할 때에는 dnn 모듈을 사용하는 방식이다.
    • 많은 딥러닝 프레임워크가 파이썬 언어를 사용하고 있지만, OpenCV dnn 모듈은 C/C++ 환경에서도 동작할 수 있기 때문에 프로그램 이식성이 높다는 장점이 있다.
    • dnn 모듈은 OpenCV 3.1에서는 추가 모듈 형태로 지원되었고, 3.3 버전부터는 기본 모듈에 포함되었다.
  • OpenCV Dnn 모듈에서 지원하는 딥러닝 프레임워크는 다음과 같다.
    • 카페(Caffe)
    • 텐서플로(TensorFlow)
    • 토치(Torch)
    • 다크넷(Darknet)
    • DLDT
    • ONNX
  • dnn 모듈에서 딥러닝 네트워크는 cv::dnn::Net 클래스를 이용하여 표현한다. Net 클래스는 dnn 모듈에 포함되어 있고, cv::dnn 네임스페이스 안에 정의되어 있다.
    • Net 클래스는 사용자가 직접 생성하지 않으며 readNet() 등의 함수를 이용하여 생성한다. readNet() 함수는 미리 학습된 딥러닝 모델과 네트워크 구성 파일을 이용하여 Net 객체를 생성한다.
    • readNet() 함수는 훈련된 가중치가 저장된 model 파일과 네트워크 구조를 표현하는 config 파일을 이용하여 Net 객체를 생성한다. 만약 model 파일에 네트워크 훈련 가중치와 네트워크 구조가 함께 저장되어 있다면 config 인자를 생략할 수 있다.
    • framework 인자에는 모델 파일 생성시 사용된 딥러닝 프레임워크 이름을 지정한다. 만약 model 또는 config 파일 이름 확장자를 통해 프레임워크 구분이 가능한 경우에는 framework 인자를 생략할 수 있다.
    • model과 config 인자에 지정할 수 있는 파일 이름 확장자와 framework에 지정 가능한 프레임워크 이름은 아래 표와 같다.
딥러닝 프레임워크 model 파일 확장자 config 파일 확장자 framework 문자열
카페 *.caffemodel *.prototxt “caffe”
텐서플로 *.pb *.pbtxt “tensorflow”
토치 *.t7 또는 *.net   “torch”
다크넷 *.weights *.cfg “darknet”
DLDT *.bin *.xml “dldt”
ONNX *.onnx   “onnx”
  • readNet() 함수는 전달된 framework 문자열, 또는 model과 config 파일 이름 확장자를 분석하여 내부에서 해당 프레임워크에 맞는 readNetFromXXX() 형태의 함수를 다시 호출한다.
    • 예컨대 model 파일 확장자가 .caffemodel 이면 readNetFromCaffe() 함수를 호출한다.
  • Net 객체를 생성한 후에는 Net::empty() 를 이용하여 객체가 정상적으로 생성되었는지를 확인한다.
  • 일단 Net 객체가 정상적으로 생성되었다면 이제 생성된 네트워크에 새로운 데이터를 입력하여 그 결과를 확인할 수 있다. 이때 Net 객체로 표현되는 네트워크 입력으로 Mat 타입의 2차원 영상을 그대로 입력하는 것이 아니라 블롭(blob) 형식으로 변경해야 한다.
    • 블롭이란 영상 등의 데이터를 포함할 수 있는 다차원 데이터 표현 방식으로 OpenCV에서 블롭은 Mat 타입의 4차원 행렬로 표현된다.
    • 이때 각 차원은 NCHW 정보를 표현하는데, N은 영상개수, C는 채널개수, H, W는 영상의 세로와 가로 크기를 의미한다.
  • OpencCV의 blobFromImage()함수를 이용하여 Mat 영상으로부터 블롭을 생성 할 수 있다. 이렇게 생성한 블롭 객체는 Net::setInput() 함수를 이용하여 네트워크 입력으로 설정한다.
    • Net::setInput() 함수 인자에소 blobFromImage() 함수에 있는 scalefactor와 mean 인자가 있어서 추가적인 픽셀 값을 조정할 수 있다. 결국 네트워크에 입력되는 블롭은 다음과 같은 형태로 설정된다.

input(n, c, h, w) = scalefactor \times (blob(n, c, h, w) - mean_{c})

  • 네트워크 입력을 설정한 후에는 네트워크를 순방향으로 실행하여 결과를 예측할 수 있다. 네트워크를 실행할 때는 Net::forward() 함수를 이용하면 된다.
    • Net::forward() 함수는 순방향으로 네트워크를 실행한다는 의미이며, 이를 추론(inference)라고 한다.
    • Net::forward() 함수는 Net::setInput() 함수로 설정한 입력 블롭을 이용하여 네트워크를 실행하고 outputName에 해당하는 레이어에서의 결과를 Mat 객체로 반환한다.
    • 만약 outputName을 지정하지 않으면 전체 네트워크 실행 결과를 반환한다.
    • Net::forward() 함수가 반환하는 Mat 객체의 형태는 사용하는 네트워크 구조에 따라 다르게 나타나므로 Net::forward() 함수가 반환한 Mat 행렬을 제대로 이용하려면 네트워크 구조와 동작 방식에 대해 충분히 이해하고 있어야 한다.

딥러닝 학습과 OpenCV 실행

텐서플로로 필기체 숫자 인식 학습하기

  • 앞선 필기체 인식의 딥러닝 버전
    • 딥러닝 분야에서는 필기체 숫자 인식 훈련을 위해 MNIST 데이터셋을 주로 사용한다.
  • (MNIST 데이터 셋을 학습 시키는 파이썬 코드 예제 생략)

OpenCV에서 학습된 모델 불러와서 실행하기

  • (텐서플로를 이용하여 MNIST 필기체 숫자 인식 학습 결과를 mnist_cnn.pb 파일에 저장한 결과를 이용)
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::dnn;
using namespace std;

void on_mouse(int event, int x, int y, int flags, void* userdata);

int main()
{
Net net = readNet("mnist_cnn.pb");

if (net.empty())
{
cerr << "Network load failed!" << endl;
return -1;
}

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

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

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

if (c == 27)
{
break;
}
else if (c == ' ')
{
Mat inputBlob == blobFromImage(img, 1/255.f, Size(28, 28));
net.setInput(inputBlob);
Mat prob = net.forward();

double maxVal;
Point maxLoc;
minMaxLoc(prob, NULL, &maxVal, NULL, &maxLoc);
int digit = maxLoc.x;

cout << digit << " (" << maxVal * 100 << "%) << endl;

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

return 0;
}

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);
}
}

OpenCV와 딥러닝 활용

구글넷 영상 인식

  • 구글넷(GoogleNet)은 구글에서 발표한 네트워크 구조이며 2014년 ILSVRC 영상 인식 분야에서 1위를 차지했다.
    • 구글넷은 총 22개의 레이어로 구성되어 있으며, 이는 동시에대 발표되었던 딥러닝 네트워크 구조 중에서 가장 많은 레이어를 사용한 형태이다.
    • 레이어를 매우 깊게 설계했지만 완전 연결 레이어가 없는 구조를 통해 기존의 다른 네트워크보다 파라미터 수가 훨씬 적은 것이 특징이다.
    • 구글넷은 특히 다양한 크기의 커널을 한꺼번에 사용하여 영상에서 큰 특징과 작은 특징을 모두 추출할 수 있도록 설계되었다.
    • 구글넷의 전체 네트워크 구조는 아래 그림과 같다.

  • OpenCV에서 구글넷 인식 기능을 사용하려면 다른 딥러닝 프레임워크를 이용하여 미리 훈련된 모델 파일과 구성 파일이 필요하다.
    • 또한 구글넷 인식 기능을 제대로 구현하려면 모델 파일과 구성 파일 외에 인식된 영상 클래스 이름이 적힌 텍스트 파일이 추가로 필요하다. 즉 ILSVRC 대회에서 사용된 1000개의 영상 클래스 이름이 적혀 있는 텍스트 팡리이 필요하며, 이 파일은 OpenCV를 설치할 때 함께 제공된다.
    • 이 텍스트 파일 이름은 classification_classes_ILSVRC2012.txt이며, 이 파일은 <OPENCV-SRC>\samples\data\dnn\ 폴더에서 찾을 수 있다.
  • 구글넷 예제 프로그램을 만들기 위해 필요한 3가지 파일을 정리하면 다음과 같다.
    • 학습 모델 파일: bvlc_googlenet.caffemodel
    • 구성 파일: deploy.prototxt
    • 클래스 이름 파일: classfication_classes_ILSVRC2012.txt
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::dnn;
using namespace std;

int main(int argc, char* argv[])
{
Mat img;

if (argc < 2)
img = imread("space_shuttle.jpg", IMREAD_COLOR);
else
img = imread(argv[1], IMREAD_COLOR);

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

Net net = readNet("bvlc_googlenet.caffemodel", "deploy.prototxt");

if (net.empty())
{
cerr << "Network load failed!" << endl;
return -1;
}

ifstream fp("classification_classes_ILSVRC2012.txt");

if (!fp.is_open())
{
cerr << "Class file load failed!" << endl;
return -1;
}

vector<String> classNames;
string name;
while(!fp.eof())
{
getline(fp, name);

if (name.length())
classnames.push_back(name);
}

fp.close();

Mat inputBlob = blobFromImage(img, 1, Size(224, 224), Scalar(104, 117, 123));
net.setInput(inputBlob);
Mat prob = net.forward();

double maxVal;
Point maxLoc;
minMaxLoc(prob, NULL, &maxVal, NULL, &maxLoc);

String str = format("%s *%4.2lf%)", classNames[maxLoc.x].c_str(), maxVal * 100);
putText(img, str, Point(10, 30), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 0, 255));
imshow("img", img);

waitKey();
return 0;
}

SSD 얼굴 검출

  • OpenCV를 설치하면 <OPENCV-SRC>\samples\dnn\face_detector 폴더에 딥러닝 얼굴 검출을 위한 파일이 함께 설치된다.
    • 이 폴더에는 얼굴 검출에서 사용된 네트워크 정보가 담겨 있는 deploy.prototxt, opencv_face_detector.pbtxt 파일과 훈련된 학습 모델을 내려받을 수 있는 팡이썬 스크립트 download_weights.py 파일이 들어 있다.
  • (학습 모델 내려 받는 방법 설명 생략)
  • 내려 받은 학습 모델 파일은 2016년에 발표된 SSD(Single Shot Detector) 알고리즘을 이용하여 학습된 파일이다.
    • SSD는 입력 영상에서 특정 객체의 클래스와 위치, 크기 정보를 실시간으로 추출할 수 있는객체 검출 딥러닝 알고리즘이다.
    • SSD 알고리즘은 원래 다수의 클래스 객체를 검출할 수 있지만 OpenCV에서 제공하는 얼굴 검출은 오직 얼굴 객체의 위치와 크기를 알아내도록 훈련된 학습 모델을 사용한다.
    • SSD 네트워크 구조는 아래 그림과 같다.

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

using namespace cv;
using namespace cv::dnn;
using namespace std;

const String model = "res10_300x300_ssd_iter_14000_fp16.caffemodel";
const String config = "deploy.prototxt";
//const String model = "opencv_face_detector_uint8.pb";
//const String config = "opencv_face_detector.pbtxt";

int main(void)
{
VideoCapture cap(0);

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

Net net = readNet(model, config);

if (net.empty())
{
cerr << "Net open failed!" << endl;
return -1;
}

Mat frame;

while(true)
{
cap >> fream;

if (frame.empty())
break;

Mat blob = blobFromImage(frame, 1, Size(300, 300), Scalar(104, 177, 123));
net.setInput(blob);
Mat res = net.forward();

Mat detect(res.size[2], res.size[3], CV_32FC1, res.ptr<float>());

for (int i = 0; i < detect.rows; i++)
{
float confidence = detect.at<float>(i, 2);

if (confidence < 0.5)
break;

int x1 = cvRound(detect.at<float>(i, 3) * frame.cols);
int y1 = cvRound(detect.at<float>(i, 4) * frame.rows);
int x2 = cvRound(detect.at<float>(i, 5) * frame.cols);
int y2 = cvRound(detect.at<float>(i, 6) * frame.rows);

rectangle(frame, Rect(Point(x1, y1), Point(x2, y2)), Scalar(0, 255, 0));

String label = format("Face: %4.3f", confidence);
putText(frame, label, Point(x1, y1-1), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 255, 0));
}

imshow("frame", frame);

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

return 0;
}
[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.