본문 바로가기
  • 우당탕탕속의 잔잔함
Programming/Computer Vision

[OpenCV] Landmark를 이용한 Face Mapping 수행

by zpstls 2023. 2. 16.
반응형

Unity와 같은 게임 엔진이나 OpenGL과 같은 Graphics Library를 이용하지 않고,

어떠한 2D Image를 Camera Image에 Mapping 시키는 방법에 대해 다뤄보고자 합니다.

 

Pictures Of Example

요즘 정말 흔하게 얼굴 이미지에 어떠한 이미지(Filter 등)를 덮어 씌워 웃긴 모습을 만들거나 하는 등의 작업을 많이 수행하고는 합니다. 이러한 작업은 보통, Face Landmark를 Detection 하여 해당 Position Data를 이용해 구현합니다.

이때, Face Landmark를 Detection 하고 해당 Data를 이용해 어떠한 연출을 부과할 때는 그래픽 관련 라이브러리나 Tool을 사용하곤 합니다. 그러나 이번 포스트에서는 순수 OpenCV만을 이용해 Image를 Mapping 해 보고자 합니다.

 

전반적인 수행 Flow는 다음과 같습니다.

Flow Of Program

과정은 위와 같이 정말 간단합니다. Landmark를 추출한 후 해당 Data 값으로 Homograph 및 Warp를 수행하면 됩니다. 그리고 결과 값을 이용해 이미지 합성을 수행합니다.

 

그럼 이제 본격적으로 진행해보겠습니다.

먼저, 검출한 얼굴 위에 출력할 어떠한 Texture가 필요합니다. 이 Texture와 Texture에서의 Landmark Position을 CSV File에 저장합니다.

Create files related to landmark

Face Texture와 Face Texture에 해당하는 68개의 Landmark Position이 활용될 것입니다.

 

다음으로는, Camera에서 Frame을 받아오거나 CSV File을 Read 하는 등의 기본적인 부분부터 구현해 봅니다.

우선, CSV 파일을 Read 하는 부분에 대한 코드는 다음과 같습니다.

vector<string> csv_read_row(istream &file, char delimiter) {
	stringstream ss;
	bool inquotes = false;
	vector<string> row;

	while (file.good()) {
		char c = file.get();
		if (!inquotes && c == '"') { inquotes = true; }
		else if (inquotes && c == '"') {
			if (file.peek() == '"') { ss << (char)file.get(); }
			else { inquotes = false; }
		}
		else if (!inquotes && c == delimiter) {
			row.push_back(ss.str());
			ss.str("");
		}
		else if (!inquotes && (c == '\r' || c == '\n')) {
			if (file.peek() == '\n') { file.get(); }
			row.push_back(ss.str());
			return row;
		}
		else { ss << c; }
	}
}

int main(int argc, char** argv) {
	vector<Point2f> corners(68);
	ifstream file("labels_face_base.csv");
	if (file.fail()) { return (cout << "There is no csv file." << endl) && 0; }

	int colsNum = 0;
	while (file.good()) {
		vector<string> row = csv_read_row(file, ',');
		if (!row[0].find("#")) { continue; }
		else {
			corners[colsNum] = Point2f(stof(row[1]), stof(row[2]));
			colsNum++;
		}
	}
	file.close();
}

 

다음은 Webcam으로부터 Image Frame을 받아와서 Face Landmark를 추정하는 부분에 대한 코드입니다.

참고로 본 포스트는 Face Landmark의 추정에 초점을 두고 있진 않으므로 해당 부분에 대한 설명은 따로 진행하지 않을 것입니다. 또한 Detection 성능에 대해서도 신경 쓰지 않을 것이기에 OpenCV에서 제공하는 기본적인 방식인 CascadeClassifierFacemarkLBF를 사용할 것입니다.

int main(int argc, char** argv) {
	// Load Face Detector
	CascadeClassifier faceDetector("haarcascade_frontalface_alt2.xml");

	// Create an instance of Facemark & Load landmark detector
	Ptr<Facemark> facemark = FacemarkLBF::create();
	facemark->loadModel("lbfmodel.yaml");

	// Set up webcam for video capture
	VideoCapture cam(0);
	cam.set(cv::CAP_PROP_FRAME_WIDTH, 640);
	cam.set(cv::CAP_PROP_FRAME_HEIGHT, 480);

	// Load Face Texture Image
	Mat mask_img = imread("face-base-test.png");

	// Read a frame
	Mat frame, gray;
	while (cam.read(frame)) {
		// Find face
		vector<Rect> faces;
		cvtColor(frame, gray, COLOR_BGR2GRAY);

		// Detect faces
		faceDetector.detectMultiScale(gray, faces);

		vector< vector<Point2f> > landmarks;
		bool success = facemark->fit(frame, faces, landmarks);
		if (success) {
			/*
				Do Warp & Homograph with frame & landmarks
				. . .
 			*/
		}

		// Exit loop if ESC is pressed
		if (waitKey(1) == 27) break;
	}
	return 0;
}

아주 간단합니다! Webcam에서 Image를 640 x 480 Size로 받아오고, 이 이미지에서 Face Detection 및 Face Landmark를 추정하기 위한 xml 및 yaml 파일들을 사용해 먼저 Face를 Detection 하고 해당 범위에서 Face Landmark를 추정합니다.

만일 성공적으로 추정되면 Webcam 이미지에 이미지 변환을 수행하여 Texture를 Face에 맞게 생성해 주고 생성 결과를 원본 이미지에 Draw 할 것입니다.

결론적으로는, 다음과 같은 과정을 수행하고자 하는 것이지요.ㅎ

 

 

 

 

 

먼저, 이미지 변환의 과정을 수행하기 위해서는 다음과 같은 코드를 적용해 줍니다.

Size warpSize(640, 480);
Mat warpImg(warpSize, frame.type());
        
for (int i = 0; i < landmarks.size(); i++) {
	// Mapping the landmark position
	vector<Point2f> warpCorners(landmarks[i].size());
	for (int j = 0; j < landmarks[i].size(); j++) {
		warpCorners[j] = landmarks[i][j];
	}

	// Homography
	Mat trans = findHomography(corners, warpCorners);

	// Warping
	warpPerspective(mask_img, warpImg, trans, warpSize);

	// Draw Result	
	imshow("Result", warpImg);
}

Image에서 추출한 Landmark Position 값을 warpCorners라는 Vector에 넣습니다. 이 Vector와 CSV에 저장해 놓은 Position 값을 Homograph 할 것입니다. Homograph를 통해 생성된 행렬 값은 Warp를 수행할 때 사용되며, 이 값을 Face Texture Image에 적용하여 warpImg에 저장합니다. 이때의 warpImg가 원본 이미지 속의 얼굴 위에 Draw 할 Image입니다.

 

잠시, HomographWarp에 대해 짧게 설명하고 넘어가도록 하겠습니다.

예를 들어 다음과 같은 상황을 가정해 보겠습니다.

Homograph

planat surface라고 하는 평면 이미지가 있을 때, v1 위치에서 이 평면을 찍어 image1을 저장했습니다. 이때 이 평면과 image1 사진과의 관계를 H1이라고 합시다. 마찬가지로 v2 위치에서 평면을 촬영한 이미지는 image2이고 평면 이미지와 image2와의 관계는 H2입니다.

이때 H1과 H2와 같은 어떠한 관계를 Homograph라고 합니다. 또한 이러한 값은 image1과 image2에서도 H12라는 관계로 생성될 수 있습니다.

Warp는 변환 행렬 값을 이용해 이미지를 기하학적으로 변환시키는 것을 의미합니다. 즉, 다음과 같이 어떠한 Position (= 최소 4개의 값)을 다른 어떠한 Position 값으로 변환시키는 것입니다.

Warp

따라서 Face Texture의 Landark Data와 실제 Face의 Landmark Data의 관계를 계산하고 이 값을 이용해 Warp를 수행시켜 주면 Face Texture가 실제 얼굴 각도나 크기 등에 맞게 변환되게 되는 것입니다.

 

위와 같은 변환 과정이 마무리되었다면, 다음으로는 변환이 완료된 Image를 원본 이미지에 Draw 하기 위한 과정을 수행합니다.

Image Synthesis

// Make masking image
Mat warpImg_gray;
cvtColor(warpImg, warpImg_gray, COLOR_BGR2GRAY);

Mat warpImg_mask, warpImg_mask_inv, masking_img;
threshold(warpImg_gray, warpImg_mask, 10, 255, THRESH_BINARY);
bitwise_not(warpImg_mask, warpImg_mask_inv);
bitwise_and(frame, frame, masking_img, warpImg_mask_inv);

// final result image
Mat final_result;
add(warpImg, masking_img, final_result);

Masking 이미지를 생성하기 위해, 앞서 생성했던 warpImg를 Gray Scale로 변환한 후 Binary화 해줍니다. 그리고 이 이미지를 반전시킨 warpImg_mask_inv를 bitwise_not 연산을 통해 생성합니다.

이 warpImg_mask_inv 이미지를 원본이미지에 AND 연산을 이용해 합성해 줍니다. 이 이미지가 최종적으로 사용될 Masking 이미지입니다.

마지막으로 warpImg와 masking_img를 ADD 하여 합성시켜 주면, 원본이미지에 Face Texture가 Draw 된 최종 결과 이미지를 얻을 수 있게 됩니다.

 

최종 결과물을 보면 다음과 같습니다.

2D 이미지이기 때문에 입체감은 없지만, 그래도 얼굴에 잘 Mapping 되어 출력되는 것을 확인할 수 있습니다. 저처럼 얼굴 모자이크 처리할 때 사용하면 편리할 것도 같습니다ㅎㅎ

 

 

이로써, 간단하게 OpenCV만을 이용하여, 얼굴에 어떠한 Texture를 Draw 하는 프로그램을 만들어 보았습니다.

이번 포스트는 여기서 마무리하도록 하겠습니다.

반응형

댓글