학부 수업 내용 정리/전기전자심화설계및소프트웨어실습

#9 Face Verification with landmark points

supersumin 2024. 11. 15. 16:38

오늘의 지식 1: Visual Studio에서 오른쪽에 위치한 솔루션 탐색기를 다시 키는 방법은 두 가지이다.

  • 상단 메뉴에서 [보기] -> [솔루션 탐색기]를 클릭
  • 단축키 Ctrl + Alrt + L

오늘의 지식 2: OpenCV 코드를 Visual Studio에서 실행하는 법 [ex) SDM and Head Pose Estimation)]

  1. https://github.com/chengzhengxin/sdm를 들어간다.
  2. [examples]에 들어있는 [roboman-landmark-model.bin], [haar_roboman_ff_alt2.xml] 프로젝트 폴더 내의 소코드 파일이 있는 폴더에 넣는다.
  3. [src]includ 파일을 다운받아 프로젝트 파일에 첨부한다.
  4. [src] -> [test_model.cpp]소스 코드 파일이 들어있는 폴더에 첨부한다. 
  5. [프로젝트 속성] ->[C/C++]->[추가포함디렉토리]에 들어가서 include의 주소를 첨부한다.

0. Facial Landmark Detection

얼굴 랜드마크 탐지는 이미지에서 얼굴의 중요한 부위(예: 눈, 코, 입 등)가 어떤 픽셀에 위치하는지 찾는 과정이다.

0.1. SDM(Supervised Descent Method) for Landmark Detection

SDM(Statistical Deformation Model)은 얼굴 랜드마크를 찾기 위해 기울기(gradient)를 활용하여 최적의 위치를 찾는 방법이다. 이 과정에서 기울기가 0인 지점을 목표로 하며, 이는 예상한 랜드마크 위치와 실제 위치 간의 차이를 최소화하는 것을 의미한다. 예측과 실제 상황이 완벽하게 일치된다면 좋겠지만 모든 상황에 대해서 이는 불가능에 가깝기 때문에 SDM은 기울기가 0에 가까운 지점을 찾기 위해 Newton method를 사용하여 점진적으로 랜드마크 위치를 수정한다.

하지만 Hessian 행렬을 계산하는 것은 계산적으로 부담이 크기 때문에, SDM에서는 Hessian 대신 Taylor 급수를 이용한 근사법을 사용한다. 초기 랜드마크 위치에서의 차이를 델타 x로 정의하고, 이를 통해 최적의 'descent direction'인 R을 계산한다. 학습 과정을 통해 R 값과 바이어스를 알게 되면, 복잡한 계산 없이 초기 값을 기반으로 랜드마크의 정확한 위치를 신속하게 추정할 수 있다. 또한, 델타 x는 테일러 급수와 SIFT를 통해 얻은 특징을 포함하여 점답과 예측 간의 차이를 반영한다.

0.2. SDM을 이용한 Landmark 위치 갱신

학습이 완료된 후, 새로운 이미지가 주어지면 R과 바이어스를 사용하여 초기 랜드마크 위치를 수정할 수 있다. 이 과정은 복잡한 계산 없이도 빠르게 정확한 랜드마크 위치를 찾아내는 데 기여한다.

1. chengzhengxin의 SDM

#include <vector>
#include <iostream>
#include <fstream>

#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/objdetect/objdetect.hpp"

#include "ldmarkmodel.h"

using namespace std;
using namespace cv;


int main()
{
    // 모델 로드
    ldmarkmodel modelt;
    std::string modelFilePath = "roboman-landmark-model.bin";
    while (!load_ldmarkmodel(modelFilePath, modelt)) {
        std::cout << "파일 열기 오류: 경로를 다시 입력하세요." << std::endl;
        std::cin >> modelFilePath;
    }

    // 카메라 초기화
    cv::VideoCapture mCamera(0);
    if (!mCamera.isOpened()) {
        std::cerr << "카메라 열기 실패." << std::endl;
        system("pause");
        return -1;
    }

    cv::Mat Image;
    cv::Mat current_shape;

    // Main Loop
    for(;;){
        // 프레임 캡처
        mCamera >> Image;

        // 얼굴 추적 및 landmark 감지
        modelt.track(Image, current_shape);

        // 헤드 포즈 추정 및 시각화
        cv::Vec3d eav;
        modelt.EstimateHeadPose(current_shape, eav);
        modelt.drawPose(Image, current_shape, 50);

        // landmark 표시
        int numLandmarks = current_shape.cols/2;
        for(int j=0; j<numLandmarks; j++){
            int x = current_shape.at<float>(j);
            int y = current_shape.at<float>(j + numLandmarks);
            cv::circle(Image, cv::Point(x, y), 2, cv::Scalar(0, 0, 255), -1);
        }

        // 결과 표시
        cv::imshow("Camera", Image);

        // 종료 조건
        if(27 == cv::waitKey(5)){
            mCamera.release();
            cv::destroyAllWindows();
            break;
        }
    }

    system("pause");
    return 0;
}
  • Image를 받으며 landmark의 좌표를 (x, y)로 알 수 있다. 이를 바탕으로 Face Verification을 진행해보자.

2. landmark 주위의 pixel 값에 대하여 16x16 window에 대해 Crop을 통한 LBP 계산 후 refernece로 저장

이를 통해 reference histogram을 구했다. 이제 비교할 target histogram을 만들어보자

 

3. flag를 통해 처음 landmark가 검출되면 그 얼굴을 refernce로 잡는다.

flag를 0으로 설정하고 landmark의 숫자가 처음으로 0이 아니게 된 순간을 reference로 잡게 된다.

 

4. target HOG를 만든 뒤 cosine similarity를 통한 비교

  • landmark의 (x, y)좌표를 알면 Window 크기의 2를 더한만큼 crop한다.
  • Crop한 이미지를 바탕으로 LBP값을 구해 Window의 크기만큼의 값을 받는다.
  • 계산된 LBP 값을 HOG로 계산한다.
  • 기준이 되는 얼굴과 대상이 되는 얼굴에 대해 cosine similarity를 계산한다.
  • 유사도 점수를 기준으로 맞으면 초록색, 아니면 빨간색이 나온다.

5. 코드

#include <vector>
#include <iostream>
#include <fstream>

#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/objdetect/objdetect.hpp"

#include "ldmarkmodel.h"

using namespace std;
using namespace cv;

#define WINDOW_SIZE 16	// LBP와 HOG를 계산할 윈도우 크기
#define EPSILON 0.001	// 정규화 과정에서 0으로 나누는 것을 방지하기 위한 작은 값
#define THRESHOLD 70	// 얼굴 비교에서 cosine similarity를 계산할 기준 점수

void computeLBP(int** crop, int** LBP_value);
void computeHOG(float* ref_histogram, int** ref_LBP_value, int offset);
void normalizeBlock(float* histogram, int landmark_num);
float cosineSimilarity(float* ref_histogram, float* tar_histogram);

int main()
{
	// 모델 로드
	ldmarkmodel modelt;
	std::string modelFilePath = "roboman-landmark-model.bin";
	while (!load_ldmarkmodel(modelFilePath, modelt)) {
		std::cout << "파일 열기 오류: 경로를 다시 입력하세요." << std::endl;
		std::cin >> modelFilePath;
	}

	// 카메라 초기화
	cv::VideoCapture mCamera(0);
	if (!mCamera.isOpened()) {
		std::cerr << "카메라 열기 실패." << std::endl;
		system("pause");
		return -1;
	}

	cv::Mat Image;
	cv::Mat current_shape;

	float* ref_histogram = (float*)calloc(68 * 256, sizeof(float));  // 참조 히스토그램 (각 랜드마크마다 256개의 LBP 값을 저장)
	int flag = 0; // reference 얼굴 설정 여부
	float score = 0.0;

	// Main Loop
	for (;;) {
		// 프레임 캡처
		mCamera >> Image;

		// 얼굴 추적 및 landmark 감지
		modelt.track(Image, current_shape);

		// 헤드 포즈 추정 및 시각화
		cv::Vec3d eav;
		modelt.EstimateHeadPose(current_shape, eav);
		modelt.drawPose(Image, current_shape, 50);

		// landmark 수 계산
		int numLandmarks = current_shape.cols / 2;

		// 첫 번째 얼굴을 ref로 설정
		if (numLandmarks != 0 && flag == 0) {
			for (int j = 0; j < numLandmarks; j++) {
				int x = current_shape.at<float>(j);					// landmark의 x 좌표
				int y = current_shape.at<float>(j + numLandmarks);	// landmark의 y 좌표
				cv::circle(Image, cv::Point(x, y), 2, cv::Scalar(0, 0, 255), -1);

				//  LBP 계산을 위한 crop 영역 동적 메모리 할당
				int** crop = (int**)calloc(WINDOW_SIZE + 2, sizeof(int*));
				for (int i = 0; i < WINDOW_SIZE + 2; i++) {
					crop[i] = (int*)calloc(WINDOW_SIZE + 2, sizeof(int));
				}

				// landmark 주변 pixel 값을 crop 배열에 저장
				for (int yy = y - (WINDOW_SIZE + 2) / 2; yy < y + (WINDOW_SIZE + 2) / 2; yy++) {
					for (int xx = x - (WINDOW_SIZE + 2) / 2; xx < x + (WINDOW_SIZE + 2) / 2; xx++) {
						if (xx > 0 && yy > 0 && xx < Image.cols && yy < Image.rows)
							crop[yy - (y - (WINDOW_SIZE + 2) / 2)][xx - (x - (WINDOW_SIZE + 2) / 2)]
							= (Image.at<Vec3b>(yy, xx)[0] + Image.at<Vec3b>(yy, xx)[1] + Image.at<Vec3b>(yy, xx)[2]) / 3;
					}
				}

				// 계산된 LBP 값을 받기 위한 동적할당
				int** ref_LBP_value = (int**)calloc(WINDOW_SIZE, sizeof(int*));
				for (int i = 0; i < WINDOW_SIZE; i++) {
					ref_LBP_value[i] = (int*)calloc(WINDOW_SIZE, sizeof(int));
				}

				// LBP 계산
				computeLBP(crop, ref_LBP_value);
				// LBP 기반으로 HOG 계산
				computeHOG(ref_histogram, ref_LBP_value, j);


				// 메모리 관리를 위한 해제
				for (int i = 0; i < WINDOW_SIZE; i++) {
					free(ref_LBP_value[i]);
				}
				for (int i = 0; i < WINDOW_SIZE + 2; i++) {
					free(crop[i]);
				}
				free(ref_LBP_value);
				free(crop);

				flag = 1;	// reference 얼굴 설정 완료
			}
		}

		// target에 대해 HOG 계산
		float* tar_histogram = (float*)calloc(68 * 256, sizeof(float));
		for (int j = 0; j < numLandmarks; j++) {
			// landmark 좌표
			int x = current_shape.at<float>(j);
			int y = current_shape.at<float>(j + numLandmarks);

			// crop을 위한 동적할당
			int** crop = (int**)calloc(WINDOW_SIZE + 2, sizeof(int*));
			for (int i = 0; i < WINDOW_SIZE + 2; i++) {
				crop[i] = (int*)calloc(WINDOW_SIZE + 2, sizeof(int));
			}

			// Image에서 해당 landmark 주위 영역을 자르고 LBP 계산
			for (int yy = y - (WINDOW_SIZE + 2) / 2; yy < y + (WINDOW_SIZE + 2) / 2; yy++) {
				for (int xx = x - (WINDOW_SIZE + 2) / 2; xx < x + (WINDOW_SIZE + 2) / 2; xx++) {
					if (xx > 0 && yy > 0 && xx < Image.cols && yy < Image.rows)
						crop[yy - (y - (WINDOW_SIZE + 2) / 2)][xx - (x - (WINDOW_SIZE + 2) / 2)]
						= (Image.at<Vec3b>(yy, xx)[0] + Image.at<Vec3b>(yy, xx)[1] + Image.at<Vec3b>(yy, xx)[2]) / 3;
				}
			}

			// 계산된 LBP 값을 받기 위한 동적할당
			int** tar_LBP_value = (int**)calloc(WINDOW_SIZE, sizeof(int*));
			for (int i = 0; i < WINDOW_SIZE; i++) {
				tar_LBP_value[i] = (int*)calloc(WINDOW_SIZE, sizeof(int));
			}

			// LBP 계산
			computeLBP(crop, tar_LBP_value);

			// LBP 기반으로 HOG 계산
			computeHOG(tar_histogram, tar_LBP_value, j);

			// 메모리 관리를 위한 해제
			for (int i = 0; i < WINDOW_SIZE; i++) {
				free(tar_LBP_value[i]);
			}
			for (int i = 0; i < WINDOW_SIZE + 2; i++) {
				free(crop[i]);
			}
			free(tar_LBP_value);
			free(crop);

		}

		// cosine similarity 계산
		score = cosineSimilarity(ref_histogram, tar_histogram);
		// 점수 출력
		printf("score = %f\n", score);

		// 점수를 비교한 뒤 비교 대상 얼굴에 대한 HOG는 필요없으므로 메모리 해제
		free(tar_histogram);


		// 유사도 점수에 따른 landmark 표시 색상 변경
		for (int j = 0; j < numLandmarks; j++) {
			int x = current_shape.at<float>(j);
			int y = current_shape.at<float>(j + numLandmarks);
			
			if (score > THRESHOLD) { // 점수가 기준 이상인 경우 초록색
				cv::circle(Image, cv::Point(x, y), 2, cv::Scalar(0, 255, 0), -1);
			}
			else { // 점수가 기준 미만일 경우 빨간색
				cv::circle(Image, cv::Point(x, y), 2, cv::Scalar(0, 0, 255), -1);
			}
		}

		// 결과 이미지 출력
		cv::imshow("Camera", Image);

		// 종료 조건
		if (27 == cv::waitKey(5)) {
			mCamera.release();
			cv::destroyAllWindows();
			break;
		}

	}

	// 동적할당 해제
	free(ref_histogram);
	system("pause");
	return 0;
}

void computeLBP(int** Image, int** LBP_value) {
	for (int y = 1; y < WINDOW_SIZE - 1; y++) {
		for (int x = 1; x < WINDOW_SIZE - 1; x++) {
			// 중심 픽셀 주변 8개 픽셀을 비교하여 LBP 값을 계산
			int value = 0;
			if (Image[y][x] < Image[y - 1][x]) value += 1;
			if (Image[y][x] < Image[y - 1][x + 1]) value += 2;
			if (Image[y][x] < Image[y][x + 1]) value += 4;
			if (Image[y][x] < Image[y + 1][x + 1]) value += 8;
			if (Image[y][x] < Image[y + 1][x]) value += 16;
			if (Image[y][x] < Image[y + 1][x - 1]) value += 32;
			if (Image[y][x] < Image[y][x - 1]) value += 64;
			if (Image[y][x] < Image[y - 1][x - 1]) value += 128;

			LBP_value[y - 1][x - 1] = value;
		}
	}
}


void computeHOG(float* ref_histogram, int** ref_LBP_value, int landmark_num) {
	for (int x = 0; x < WINDOW_SIZE; x++) {
		for (int y = 0; y < WINDOW_SIZE; y++) {
			int histogram_index = ref_LBP_value[y][x] + landmark_num * 256;;
			ref_histogram[histogram_index] += 1;
		}
	}

	normalizeBlock(ref_histogram, landmark_num);
}

void normalizeBlock(float* histogram, int landmark_num) {
	float norm = 0.0;
	for (int i = 0; i < 256; i++) {
		norm += histogram[i + landmark_num * 256] * histogram[i + landmark_num * 256];
	}

	norm = sqrt(norm + EPSILON); // epsilon은 작은 값으로 0으로 나누는 것 방지
	for (int i = 0; i < 256; i++) {
		histogram[i + landmark_num * 256] /= norm;
	}
}

float cosineSimilarity(float* ref_histogram, float* tar_histogram) {
	int k;
	float nomi = 0, denomi = 0;
	float refMag = 0, tarMag = 0;
	int dim = 68 * 256;
	float score = 0.0;

	for (k = 0; k < dim; k++) {
		nomi += ref_histogram[k] * tar_histogram[k];	// 내적값
		refMag += ref_histogram[k] * ref_histogram[k];	// a의 크기
		tarMag += tar_histogram[k] * tar_histogram[k];// b의 크기
	}

	denomi = sqrt(refMag) * sqrt(tarMag); // 크기
	if (denomi != 0) {
		score = nomi / denomi; // Calculate cosine similarity
	}

	return exp(5 * score); // 코사인 값 사이의 차이가 너무 적어서 크게 해줌. 이 값을 정규화해주면 됨
}