database에 삽입될 LBP 정보는 특징점을 기준으로 11x11의 패턴을 LBP를 이용하여 자신을 제외한 총 120개의 값을 받아서 저장한다.
LBP 패턴의 값을 뽑아올 때는 특징점을 중심으로 일정 거리의 값들을 비교하여 저장하기 때문에 이미지의 일정
범위의 테두리는 검토하지 않는다. Padding을 시도할 경우, 그만큼
저장할 feature이 늘어나기 때문에 사용하지 않는다.
여러가지 LBP 크기를 검토해보았으나 3x3, 5x5는 크기가 너무 작아서
제외하고 7x7은 각 픽셀에 대한 모든 LBP feature를
저장했을 때 이미지 10장 기준 약 300MB의 database가 나왔다. 500장 기준 15GB의 용량을 차지하며, 15x15의 경우 18GB의 용량을 차지한다.
11 x 11을 사용한 이유는 크기가 너무 작거나 크지 않고 넓이가 128에
가장 근접했기 때문이다. 특징점 중복 제거를 위한 값을 이용할 때, 사용하기
적합하다고 판단했다.
BSD500의 이미지를 전부 사용할 경우, Positive feature: 25,921개, Negative feature: 73,214,579개가 나온다.
2) 특징점 중복 제거
특징점이
가질 수 있는 경우의 수는 2(LBP의 넓이-1) 이다.
11 x 11의 경우,1,329,227,995,784,915,872,903,807,060,280,344,576 만큼의 경우의 수가
존재한다. 경우의 수가 많기 때문에 각 패턴이 같을 경우는 거의 없지만 같은 경우가 있다면 가치가 떨어진다고
판단했다.
처음
시도한 방법은 각 특징점의 패턴을 기록하기 전에 Vector 컨테이너에 존재하는 값들과 비교를 한 후, 중복이 없으면 Vector 컨테이너에 추가 삽입을 하면서 모든 패턴을
비교한 후에 training database를 생성하는 것을 시도했다.
LBP는 특징점의 픽셀과 주변 픽셀과의 크기 비교를 통해 1 또는
0의 값만 보관하므로 bitset을 이용하여 값을 보관하도록
했다. Bitset은 각 1비트씩 값을 저장하기 때문에 11x11의 LBP 패턴을 보관할 비트 배열을 생성 및 저장하도록 한다.
Vector 를 이용해서 새로 들어올 패턴과 기존의 패턴들을 비교할 때, Vector는 입력된 자료의 순서대로 저장을 하기 때문에 값을 비교할 때 Vector에 저장된 양이 많아질 수록 느리다. 한 이미지의 특징점
검출 및 Vector에 삽입하기까지 처음에 1초가 걸렸다면
그 다음엔 2초, …., 10초로 점점 숫자가 늘어나더니
한 이미지 당 1분이 넘게 걸리게 된다.
Map 컨테이너는 중복 체크 및 정렬에 특화된 컨테이너이다. 특정
값의 삽입을 시도하면 중복 여부를 체크하고 자동으로 정렬해준다. <key, value>의
형식으로 이루어져 있기 때문에 key가 있어야 한다. value에는
LBP 패턴이 들어가고, key에는 중복 및 순서를 확인하기
위한 정보가 들어간다. 앞서 말했듯이 LBP 패턴의 종류는
2(LBP의 넓이-1) 이다. 이를 구분하기 위해서는 120bit를 넘어가는 값을 저장할 수 있어야
한다. 따라서 별도의 구조체를 생성한다.
typedef struct int256 { __int64 a = 0; __int64 b = 0;__int64
c = 0;__int64 d = 0;
bool operator
< (const int256 &rhs) const {
if
(d != rhs.d) return (d < rhs.d);
if
(c != rhs.c) return (c< rhs.c);
if
(a != rhs.a) return (a < rhs.a); return (a < rhs.a); } };
64bit 변수를 4개를 가지고 있기 때문에 최대 256bit까지 값을 보관할 수 있다. oprator < 함수는
map 컨테이너가 구조체를 가지고서 비교 및 정렬을 할 수 있게 해주는 연산자 오버로딩 함수이다. 최대 256bit까지 허용한 이유는 15x15를 테스트하기 위함이다.
Map 컨테이너를 사용할 경우, feature 검출에 이미지 별로
약 400~600ms의 시간이 소요된다. 많은 양의 이미지를
집어넣어도 빠르게 중복 검출 및 정렬을 할 수 있다는 점이 큰 장점이다.
중복
제거를 할 경우, Positive feature: 23,578개,
Negative feature: 54,438,407개가 나온다. Positive feature의
경우 2,343개의 중복 feature를 제외했으며, Negative feature의 경우 18,776,172개의 중복
feature를 제거했다. 이를 통해 제한된 용량의 database에 들어갈 feature의 종류를 더욱 다양하게 해준다.
3) Database에 삽입될 특징점 선정
중복을
제거했지만 database에 전부 집어넣기엔 많은 용량이 필요하다. 모든
feature를 집어넣었다고 해도 Train에 많은 시간을
필요로 하게 된다. 검출된 feature 중에서 적절한 feature를 선정해서 적은 용량과 짧은 시간으로 model을 만들어야
한다.
Positive
Feature는 약 5000개를 이용하기로 한다.
25000개를 넣고, Negative Featue를 3배
정도 넣었을 때도 상당한 시간이 걸렸기 때문에 1/5로 축소한 값이다.
Negative Feature는 Positive Feature의
약 2000배지만 이를 다 집어넣을 수 없기 때문에 Positive의
3배에 해당하는 양을 database에 삽입한다.
Database에 삽입할 때, 삽입 기준은 Random 및 임계값 사용 등 다양한 방법이 있지만 map이 자동
정렬을 해주기 때문에 조금 더 단순한 방법을 사용한다.
Map은 Key 값에 따라 삽입을 할 때마다 정렬이 된다. Key값은 LBP feature의 값을 나타낸다. LBP의 넓이가 n일 때,
LBP Feature의 Key값은 p[0]*20+p[1]*21+p[2]*22+…+p[n-1]*2n-1이 된다.
즉, LBP 패턴에 따라 Key값이 정해지며 이를 통해 특정 Key값에서의 LBP 패턴 모양을 알 수 있다. Map은 Key값을 자동으로 정렬해주므로 LBP Feature도 점진적인 형태로 정렬이 되어있다. 3x3 LBP의
경우, 00000001(2), 00000010(2), 00000011(2),
…., 11111111(2)와 같이 정렬이 된다.
따라서
일정 간격마다 Negative Feature를 database에
삽입하면 고른 분포로 database에 삽입이 된다.
Map은 Index를 사용할 수 없기 때문에 정렬된 Map 내의 Value를 Vector
컨테이너로 옮긴 후, index를 따라 일정 간격 단위로 값을 database에 저장한다.
for
(auto elem : vectorMap_Negative) negaVector.push_back(elem.second);
for
(int mn = 0; mn < negativeMulti; ++mn)
for (int d = mn *
distanceSmallVal; d < negaVector.size(); d += distanceVal){}
distanceVal은 negative vector가 positive vector의 몇 배인지
확인하는 변수이다. distanceSmallVal는 몇 배인지 알았을 때 negative vector를 positive의 몇 배 만큼 집어넣을지
확인해서 간격을 다시 나눠서 가지는 변수이다.
여기까지 해서 Positive feature 5,128개, Negative feature: 15,385개를
database에 삽입했다.
위의 feature database를 train했을 때, 11,030 줄의 파일이 하나 생성된다.
이미지 측정을 위해서는
Predict 과정을 거쳐야 하는데, 다음과 같은 방법으로
진행하였다.
Sample에 특정 이미지에 내의
모든 LBP feature값을 기록한다. 특징점의 값은 모두
Negative 즉, 0으로 설정한다. 이러면 모든 픽셀의 LBP Pattern을 Predict를 통해 Negative인지 아닌지 측정을 하며 Negative일 경우 1, Positive일 경우 0을 출력한다. 이를 통해서 샘플 이미지에 대한 특징점을 찾아낼 수
있다.
Predict
결과 99%라는 것은 해당 이미지의 Negative가
99% 있음을 나타낸다.
조금 당황스러운 결과가
나왔는데, 이미지가 작아서 위의 조건을 달면 Lenna 128
x128에서는 특징점이 거의 검출되지 않았다. 따라서 기존 SIFT 결과물과의 비교를 진행한다.
기존 SIFT의 결과는 특징점이 140개가 검출되었고, SVM을 통한 결과는 97개가 검출되었다. 개수는 조금 다르지만 Train할 database를 생성할 때 이미지별로 각각 100개가 안되는 Positive를 추출해서 입력했던 것을 감안하고 볼 때, 기존 SIFT보다 더 괜찮을 결과가 나왔다.
Predict
결과물은 눈을 인지하고 벽과 같은 빈 공간에 점이 배치되어 있지 않지만 SIFT 결과물은
눈을 특징점으로 인지하지 않고 죄측 상단의 벽에 적절하지 못한 특징점이 존재한다.
Mat image = imread("Lenna.png");//이미지 보관.
Mat gray; //grayscale 이미지를 저장할 Mat.
cvtColor(image, gray, CV_BGR2GRAY);//grayscale 이미지 사용.
Mat lbp(image.size(), CV_8U, Scalar(0));
CV_8U는 unsigned char => 0~255 사이의 값을 같는 uchar를 나타낸다. Schalar(0) 기본값을 0으로 초기화한다.
lbp에 각 값들을 집어넣은 후에 출력한다.
for (int i = 1; i < gray.rows - 1; ++i) {
for (int j = 1; j < gray.cols - 1; ++j)
{
uchar t[9];
for (int k = 0; k < 3; ++k) {
for (int l = 0; l < 3; ++l) {
t[k * 3 + l] = gray.at<uchar>(i + k - 1, j + l - 1);
}
}
uchar currentValue = ReturnDecimVal(t);
lbp.at<uchar>(i, j) = currentValue;
}
}
사이드에서 계산을 할 경우, 범위를 벗어난다. padding을 하는 방법도있지만 생략한다.
LBP는 패딩을 해도 별 차이가 없다.
copyMakeBorder(gray, gray, 1, 1, 1, 1, BORDER_CONSTANT, Scalar(0)); 다음과 같이 쓰면 제로 패딩을 해볼 수 있다.
uchar 배열에 자기자신과 둘러싼 8개의 값을 집어넣고 이를 LBP를 이용하여 적절한 값을 찾아낸다.
uchar ReturnDecimVal(uchar* t) {
uchar val = 0;
uchar center = t[1 * 3 + 1];
bool check[9];
int calStance[8] = { 3, 6, 7, 8, 5, 2, 1, 0 };//계산 순서를 임의로 설정.
for (int k = 0; k < 3; ++k) {//ture면 주변 색보다 센터가 크거나 같은 경우, false면 아닌 경우.
for (int l = 0; l < 3; ++l) {
check[k * 3 + l] = (center >= t[k * 3 + l]) ? true : false;
}
}
for (int i = 7; i >= 0; --i)
val += check[calStance[7-i]] * pow(2, i);//위의 계산 순서에 따라서 2진수 방식으로 계산. bool은 false = 0, true = 1이므로 그대로 사용.
return val;
}
위의 달팽이 그림같은 패턴을 그대로 사용하지 않아도 된다고 하지만 위에 맞춰서 패턴을 입력해보았다.
간단하게 하고자 계산 순서를 정해놓고 한바퀴 도는 식으로 했다.
달팽이 알고리즘을 짜고 싶다면 정보처리기능사 문제를 보면 된다.
효율적이라고 생각하진 않는다.
#include "stdafx.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>
#include <math.h>
#include<iostream>
#include <string>
using namespace cv;
using namespace std;
uchar ReturnDecimVal(uchar* t) {
uchar val = 0;
uchar center = t[1 * 3 + 1];
bool check[9];
int calStance[8] = { 3, 6, 7, 8, 5, 2, 1, 0 };//계산 순서를 임의로 설정. 계산식이 단순해질 수 있게 해준다.
for (int k = 0; k < 3; ++k) {//먼저 주변 색이 센터보다 큰지 작은지 확인합니다. ture면 주변 색보다 센터가 크거나 같은 경우, false면 아닌 경우.
for (int l = 0; l < 3; ++l) {
check[k * 3 + l] = (center >= t[k * 3 + l]) ? true : false;
}
}
for (int i = 7; i >= 0; --i)
val += check[calStance[7-i]] * pow(2, i);//위의 계산 순서에 따라서 2진수 방식으로 계산. bool은 false = 0, true = 1이므로 그대로 사용해도 된다.
return val;
}
int main() {
Mat image = imread("Lenna.png");//이미지 보관.
Mat gray; //grayscale 이미지를 저장할 Mat.
cvtColor(image, gray, CV_BGR2GRAY);//grayscale 이미지 사용.
//copyMakeBorder(gray, gray, 1, 1, 1, 1, BORDER_CONSTANT, Scalar(0));//zero padding을 해줍니다. 별 차이가 없어서 사용하지 않습니다.
Mat lbp(gray.size(), CV_8U, Scalar(0));//CV_8U 8bit unsigned integer -> 즉, uchar(0~255) Scalar(0) : 0값 삽입.
for (int i = 1; i < lbp.rows - 1; ++i) {//사이드는 무시합니다. 따라서 출력 시에 테두리는 검정(0값)으로 출력됩니다.
for (int j = 1; j < lbp.cols - 1; ++j)
{
uchar t[9];//먼저 9개의 gray값을 빼내옵니다. 1차원 배열 이유는 1차원 배열이 관리가 용이합니다.
for (int k = 0; k < 3; ++k) {
for (int l = 0; l < 3; ++l) {
t[k * 3 + l] = gray.at<uchar>(i + k - 1, j + l - 1);
}
}
uchar currentValue = ReturnDecimVal(t);//색값을 받아옵니다.
lbp.at<uchar>(i, j) = currentValue;
}
}
imwrite("Result.bmp", lbp);
return 0;
}