프로젝트

나도 메이커! 메이커스 여러분들의 작품/프로젝트를 공유하는 공간입니다.

얼굴 추적 카메라

2014-08-25 13:20:41

개요

개요

웹캠으로 얼굴을 인식하여 얼굴을 따라다니는 카메라를 구현해보자.
이 프로젝트를 진행하기 위해서 필요한 환경설정으로 OpenCV와 Processing이 필요하다. 
OpenCV는 컴퓨터상에서 프로세싱이 얼굴을 인식할때 필요한 대표적인 라이브러리이며 프로세싱뿐만 아니라 C나 Java에서도 쓰인다.
이번 프로젝트에서는 프로세싱이 아두이노와 호환성이 좋으며 개발하기 쉬운 언어이기 때문에 OpenCV를 사용하기 위한 개발툴로 쓰였다.
물론 굳이 프로세싱이 아니더라도 다른 언어로도 아두이노와 시리얼통신이 가능하며 OpenCV라이브러리를 사용할 수 있다면 얼마든지 개발 가능하다. 

기본적으로 영상처리쪽은 사람얼굴이 셀 수 없이 다양하여, 제대로 인식하여 자료로 사용하기 위해서는 다양한 표본의 데이터베이스가 필요하고, 빛의 반사, 위치, 주위 환경에 따라 똑같은 사물이라도 다양한 모습으로 비춰질 수 있어 결과값에 대한 불확실성이 매우 크기 때문에 상당히 어려운 분야에 속한다.




하지만 이 프로젝트에 사용하는 얼굴인식의 경우에는 OpenCV안에 기본 예제소스로 주어져 있기 때문에 예제소스를 프로세싱내에서 구동할 수 있게 환경설정을 해줄 수만 있다면 어렵지는 않게 구현이 가능한 프로젝트이다. 예제소스로 주어져 있는 만큼 정밀한 인식은 어렵고 얼굴인식에 대한 소스코드 수정이 OpenCV에 대한 전문적인 지식이 없는 한 거의 불가능하다. 그래도 정면으로 카메라를 바라본다면 대부분의 얼굴은 인식할만큼 괜찮은 성능을 나타낸다.

얼굴인식카메라는 주위 환경에서 CCTV분야에 많이 쓰이며 최근에는 웹캠도 개발되었다. 최근에는 얼굴뿐만 아니라 소리에 따라 소리를 추적하는 카메라까지 개발되고 있다.




아두이노 서보모터를 활용하여 카메라를 얼굴추적카메라를 만들어서 사용하면 응용하여 얼굴을 추적하여 자동으로 캡쳐해주는 카메라나 CCTV의 구현이 가능하다. 어려우면서도 쉬운 얼굴추적카메라를 만들어 보자.

동영상



필요한 사전지식

서보 모터
시리얼 통신
프로세싱
OpenCV

부품 목록

NO 부품명 수량 상세정보
1 아두이노 보드 1 UNO, Mega2560, MegaADK
2 브레드 보드 1  
3 서보 모터 2  
4 웹캠 1  
5 케이블 6~10  



부품명 아두이노 보드 브레드 보드 서보 모터 2개 웹캠 케이블
부품 사진


※서보 모터 선택 사항

위 프로젝트를 진행할 때에는 서보 모터 하나는 수직으로 움직이고 다른 하나는 수평으로만 움직이게 하여 상하좌우를 움직일 수 있게 한다. 이때 서보 모터2개를 조합해서 만들어야 하는데 이때 서보모터를 브라켓이 달린것으로 구매하면 좀 더 수월하게 작업 할 수 있다.

하드웨어 making

브레드보드


전자회로도



소프트웨어 coding

이번 프로젝트는 코딩을 하기전에 환경설정이 복잡하지만 많이 필요하다.
일단 프로세싱이 있어야 하고 OpenCV라는 것을 받아서 설치해 주어야 한다.

프로세싱 설치하기  <- 링크

OpenCV에는 다양한 버전이 있지만 여기에서는 프로세싱과 사용할 것이기 때문에, 프로세싱과 관련된 OpenCV를 받아야 한다.

OpenCV받기 <- 링크

설치를 하다가 이런창이 뜨게 되면 체크박스에 체크하고 설치를 계속해 준다. 
(체크를 안 할 경우나 나중에 제대로 실행이 안 될 경우에는 다음과 같은 방법을 실행한다.
컴퓨터-속성-고급 시스템 설정-고급-환경변수에서 시스템변수 Path에 편집버튼을 눌러 OpenCV가 설치된 공간의 디렉토리 주소를 입력한다.
ex) OpenCV가 Program Files에 설치됐을 경우 -> C:\Program Files\OpenCV\bin;    
bin폴더까지 입력해 주어야 한다. Path에서 디렉토리 구분은 ;세미콜론으로 한다)




OpenCV가 설치되었으면 재부팅을 하고 이제는 OpenCV라이브러리를 받아야 한다.

OpenCV라이브러리 <- 내려받기 링크 
OpenCV라이브러리를 내려받았으면 프로세싱2가 설치된 위치\modes\java\libraries\  에 OpenCV라이브러리폴더를 옮긴다.

OpenCV라이브러리 예제 안에도 FaceDetection이 있지만 아마 제대로 실행되지 않을 것이다.
FaceDetection예제를 실행하기 위해서는 GSvideo라는 라이브러리가 필요하다.

GSVideo라이브러리 <- 내려받기 링크

GSVideo라이브러리를 내려받았으면 아래 위치에 붙여넣기 한다. (이 프로젝트는 프로세싱2, Win7 32bit를 기준으로 한다.)
프로세싱2가 설치된 위치\modes\java\libraries\  에 GSVideo를 넣으면 라이브러리 설치도 완료된다. 




라이브러리가 제대로 설치됐는지 알아보기 위해 프로세싱을 켜고 예제를 실행해 보자.


예제소스에 GSVideo가 보이면 일단 라이브러리설치는 성공적으로 완료된 것이다.
웹캠이나 노트북안에 내장캠이 존재한다면 GSVideo안의 FaceDetection을 불러서 실행해보자.




불러서 실행시키면 haarcascade_frontalface_alt.xml이 없다는 메시지가 뜰지도 모른다. 
위와 같은 오류 메시지가 뜨게 되면 OpenCV가 설치된 위치에서 data\haarcascades안에 있는 그 파일을 가져와서 
ex) C:\Program Files\OpenCV\data\haarcascades

예제소스가 존재하는 디렉토리안의 data폴더에 붙여넣기 하면 오류는 뜨지 않고 제대로 실행 될 것이다.
ex) C:\Users\유저이름\Desktop\processing-2.2.1\modes\java\libraries\GSVideo\examples\OpenCV\FaceDetection\data

실행시켰을때 제대로 얼굴을 인식하고 얼굴을 빨간색 사각형으로 인식한다면 환경설정은 끝난것이다.



프로세싱 코드

import processing.serial.*;

// Combining GSVideo capture with the OpenCV library for face detection
// http://ubaa.net/shared/processing/opencv/
import hypermedia.video.*;
import java.awt.Rectangle;
import codeanticode.gsvideo.*;

OpenCV opencv;
GSCapture cam;

// 대비, 밝기 조절 변수
int contrast_value    = 0;
int brightness_value  = 0;

//수직, 수평으로 움직이는 서보모터를 구별하기 위한 변수 char verticalSignal = 0; char horizonSignal = 1;
//서보모터 각도의 초기값 지정 char servoHPosition = 90; char servoVPosition = 90; //얼굴 중앙값 초기화 int midFaceY=0; int midFaceX=0;
//화면의 중심좌표값 지정 int midScreenY = (480/2); int midScreenX = (640/2); int midScreenWindow = 10; //화면 중앙에서 어느정도 위치안에 얼굴의 중앙위치점가 들어올 경우
//스크린에서 중앙으로 들어왔다고 인식할 것인지 오차범위 지정 int stepSize=1; //모터 이동값 지정 Serial port; void setup() { size(640, 480); cam = new GSCapture(this, 640, 480); cam.start(); //OpenCV사용 초기화 opencv = new OpenCV(this); opencv.allocate(640,480); // "haarcascade_frontalface_alt.xml"을 불러와서 얼굴의 앞을 인식한다 opencv.cascade( OpenCV.CASCADE_FRONTALFACE_ALT ); println(Serial.list()); println(midScreenX); println(midScreenY);
//시리얼통신을 위한 포트 생성 port = new Serial(this, Serial.list()[0], 57600); //메시지 프린트 println("Drag mouse on X-axis inside this sketch window to change contrast"); println("Drag mouse on Y-axis inside this sketch window to change brightness");
//서보모터 초기각도 전송 port.write(horizonSignal); port.write(servoHPosition); port.write(verticalSignal); port.write(servoVPosition); } void captureEvent(GSCapture c) { c.read(); } public void stop() { opencv.stop(); super.stop(); } void draw() { opencv.copy(cam); opencv.convert(GRAY); opencv.contrast(contrast_value); opencv.brightness(brightness_value); //OpenCV라이브러리를 이용하여 사람의 얼굴 앞면을 인식한다. Rectangle[] faces = opencv.detect(1.2, 2, OpenCV.HAAR_DO_CANNY_PRUNING, 40, 40); //캠에서 인식하는 이미지를 화면에 출력하여 영상으로 만든다 image(cam, 0, 0); //인식한 얼굴의 테두리에 사각형을 그린다. noFill(); stroke(0, 255, 0); strokeWeight(5); for(int i = 0; i < faces.length; i++) { rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height); }
//얼굴의 길이가 0보다 클 경우(사람의 얼굴을 인식하고 있을 경우) if(faces.length > 0){
//현재 인식하고 있는 얼굴의 중앙 점을 구하여 midFaceX와 midFaceY에 저장
midFaceX = faces[0].x + (faces[0].width/2); midFaceY = faces[0].y + (faces[0].height/2);
//현재 얼굴의 위치가 스크린의 중앙보다 아래에 위치할 경우 수직으로 움직이는 서보모터의 각도를 1도씩 감소시킨다 if(midFaceY < (midScreenY - midScreenWindow)){ if(servoVPosition >= 5) servoVPosition -= stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 위에 위치할 경우 수직으로 움직이는 서보모터의 각도를 1도씩 증가시킨다 else if(midFaceY > (midScreenY + midScreenWindow)){ if(servoVPosition <= 175) servoVPosition +=stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 왼쪽에 위치할 경우 수평으로 움직이는 서보모터의 각도를 1도씩 감소시킨다 if(midFaceX < (midScreenX - midScreenWindow)){ if(servoHPosition >= 5) servoHPosition -= stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 아래에 위치할 경우 수평으로 움직이는 서보모터의 각도를 1도씩 증가시킨다 else if(midFaceX > midScreenX + midScreenWindow){ if(servoHPosition <= 175) servoHPosition +=stepSize; } }
//각자 서보모터의 각도를 시리얼통신을 통해 전송 port.write(horizonSignal); port.write(servoHPosition); port.write(verticalSignal); port.write(servoVPosition); delay(1); } //밝기 대조값 변경 void mouseDragged() { contrast_value = int(map(mouseX, 0, width, -128, 128)); brightness_value = int(map(mouseY, 0, width, -128, 128)); }

얼굴인식을 하기 위한 프로세싱코드로 OpenCV라이브러리를 통한 소스가 반정도 존재한다.

얼굴인식을 하여 서보모터를 움직이게 하기 위해서는 사실상 2개의 점만 알고 있으면 된다. 
화면중앙점의 좌표값과 현재 인식하고 있는 얼굴의 중앙좌표값을 알고 있으면 서보모터를 움직일 수 있다. 계속적으로 두점을 비교하여 서보모터에 달린 카메라를 움직여서 궁극적으로는 화면의 중앙 좌표값과 현재 얼굴의 중앙좌표값을 비스무리하게(오차범위안으로) 맞출 수만 있다면 얼굴을 추적하는 카메라를 만들 수 있다.

//수직, 수평으로 움직이는 서보모터를 구별하기 위한 변수
char verticalSignal = 0;
char horizonSignal = 1;

//서보모터 각도의 초기값 지정 char servoHPosition = 90; char servoVPosition = 90; //얼굴 중앙값 초기화 int midFaceY=0; int midFaceX=0;
//화면의 중심좌표값 지정 int midScreenY = (480/2); int midScreenX = (640/2); int midScreenWindow = 10; //화면 중앙에서 어느정도 위치안에 얼굴의 중앙위치점가 들어올 경우
//스크린에서 중앙으로 들어왔다고 인식할 것인지 오차범위 지정
서보모터를 사용하는데 아두이노 에서는 프로세싱에서 전송한 값이 서보모터 두개 중 어느 모터에 전송하는 값인지 구별할 방법이 없다. 따라서 프로세싱에서는 모터의 각도값을 전송하기 전에 어느모터에게 전송하는 것인지 일종의 시그널을 보낸다. 
0을 보내고 그 다음 오는 값은 수직으로 움직이는 모터를 움직이는 값으로 약속을 하고 
1을 보내고 그 다음 오는 값은 수평으로 움직이는 모터를 움직이는 값으로 약속을 하는 것이다. 이런 방법을 사용하면 각 모터마다 제대로 된 값을 보낼 수 가 있다. 화면의 중앙 좌표값은 스크린의 너비와 높이를 /2해 주면 중앙 좌표값을 구할 수 있다. 

 //OpenCV라이브러리를 이용하여 사람의 얼굴 앞면을 인식한다.
  Rectangle[] faces = opencv.detect(1.2, 2, OpenCV.HAAR_DO_CANNY_PRUNING, 40, 40);

  //캠에서 인식하는 이미지를 화면에 출력하여 영상으로 만든다
  image(cam, 0, 0);

  //인식한 얼굴의 테두리에 사각형을 그린다.
  noFill(); //채우기 없음
  stroke(0, 255, 0); //R:0, G:255, B:0 녹색의 사각형을 그린다.
  strokeWeight(5);  //사각형의 테두리는 5로 지정한다
  for(int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height); 
  }
위 소스는 사람 얼굴을 인식하고 얼굴에 사각형 테두리를 치는 부분을 나타내는 소스이다. 얼굴인식은 OpenCV에서 haar_cascades에서 담당한다.
사용자는 영상처리분야나 컴퓨터 공각이 전공이 아닌이상 라이브러리를 분석하고 자세하게 알 필요는 없다.
detect함수를 통해 얼굴을 인식하면 프로세싱 함수를 통해 사각형을 그린다.
stroke()를 통해 사각형의 색을 지정하고, strokeWeight()을 통해 사각형의 두께를 지정한다.
for문을 통하여 인식된 얼굴 좌표에서 구한 너비와 높이만큼 사각형을 그리게 된다.

if(faces.length > 0){
//현재 인식하고 있는 얼굴의 중앙 점을 구하여 midFaceX와 midFaceY에 저장
midFaceX = faces[0].x + (faces[0].width/2); midFaceY = faces[0].y + (faces[0].height/2);
//현재 얼굴의 위치가 스크린의 중앙보다 아래에 위치할 경우 수직으로 움직이는 서보모터의 각도를 1도씩 감소시킨다 if(midFaceY < (midScreenY - midScreenWindow)){ if(servoVPosition >= 5) servoVPosition -= stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 위에 위치할 경우 수직으로 움직이는 서보모터의 각도를 1도씩 증가시킨다 else if(midFaceY > (midScreenY + midScreenWindow)){ if(servoVPosition <= 175) servoVPosition +=stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 왼쪽에 위치할 경우 수평으로 움직이는 서보모터의 각도를 1도씩 감소시킨다 if(midFaceX < (midScreenX - midScreenWindow)){ if(servoHPosition >= 5) servoHPosition -= stepSize; }
//현재 얼굴의 위치가 스크린의 중앙보다 아래에 위치할 경우 수평으로 움직이는 서보모터의 각도를 1도씩 증가시킨다 else if(midFaceX > midScreenX + midScreenWindow){ if(servoHPosition <= 175) servoHPosition +=stepSize; } }
카메라가 얼굴을 인식하고 있을 경우 화면의 중앙좌표값과 현재 인식하고 있는 얼굴의 중앙좌표값을 비교하는데 4가지의 경우를 비교하게 된다.

얼굴이 화면의 중앙에서 아래에 위치 할 경우
얼굴이 화면의 중앙에서 위에 위치 할 경우
얼굴이 화면의 중앙에서 왼쪽에 위치 할 경우
얼굴이 화면의 중앙에서 오르녹에 위치 할 경우

각 경우에 따라 if문을 통해 서보모터의 각도를 제어하게 된다. 사실 루프 한번당 서보모터 하나만 움직이지만 루프가 빠르게 돌기 때문에 서보모터 두개가 동시에 움직이는 것처럼 보이게 된다.
서보모터는 0~180도까지 움직이게 되는데 서보모터의 안정성을 위하여 각도를 최소 5도 최대 175도로 제한하였다.
위에 4가지 경우에 따라 
예를 들어 얼굴이 화면의 중앙에서 아래에 위치 할 경우 카메라가 아래로 내려가야 얼굴이 카메라 중심으로 오는 것처럼 보이기 때문에 수직으로 움직이는 서보모터의 각도값을 설치한 방법에 따라 +나 -하여 서보모터를 아래로 내린다.
각도값의 제어는 stepSize를 통해 1도씩 제어한다. 1도씩 제어한다해도 루프가 빠르게 돌기 때문에 서보모터는 실제로 휙휙 돈다.

이 같은 방법으로 나머지 3가지 경우도 제어하여 그 값을 시리얼통신을 통해 아두이노로 전송한다. 

아두이노 코드

#include <Servo.h> //서보모터 라이브러리 사용
//수직, 수평으로 움직이는 서보모터를 구별하기 위한 변수
char verticalSignal=0, horizonSignal=1; Servo servoV, servoH; //서보모터 객체 생성 char serialChar=0; 
//서보모터의 핀번호를 설정하고, 시리얼통신을 초기화하며 모터의 초기각도를 지정한다. void setup(){ servoV.attach(9); servoH.attach(10); servoV.write(90); servoH.write(90); Serial.begin(57600); } void loop(){ while(Serial.available() <=0); //시리얼통신을 통해 데이터를 받을때까지 대기한다 serialChar = Serial.read(); //데이터를 받았을 경우 if(serialChar == verticalSignal){ //수직모터를 움직이라는 신호를 받았을 경우 while(Serial.available() <=0); //각도값을 받을 때까지 대기 servoV.write(Serial.read()); //각도값을 받았을 경우 모터를 전송받은 값만큼 움직인다 } else if(serialChar == horizonSignal){ //수평모터를 움직이라는 신호를 받았을 경우 while(Serial.available() <= 0); //각도값을 받을 때까지 대기 servoH.write(Serial.read()); //각도값을 받았을 경우 모터를 전송받은 값만큼 움직인다 } }
 
아두이노의 경우에는 프로세싱보다 소스가 간단하다. 
아두이노는 프로세싱에서 시리얼통신을 통해 전송한 값을 각 모터로 전송하는 역할을 담당한다.

void loop(){
  while(Serial.available() <=0); //시리얼통신을 통해 데이터를 받을때까지 대기한다
  serialChar = Serial.read(); //데이터를 받았을 경우
  if(serialChar == verticalSignal){  //수직모터를 움직이라는 신호를 받았을 경우
    while(Serial.available() <=0); //각도값을 받을 때까지 대기
    servoV.write(Serial.read()); //각도값을 받았을 경우 모터를 전송받은 값만큼 움직인다
  }
  else if(serialChar == horizonSignal){ //수평모터를 움직이라는 신호를 받았을 경우
    while(Serial.available() <= 0);  //각도값을 받을 때까지 대기
    servoH.write(Serial.read());  //각도값을 받았을 경우 모터를 전송받은 값만큼 움직인다
  }
}
루프에서는 시리얼통신을 통해 값이 전송될 때까지 while문을 통해 조건을 Serial.available() <=0을 주어 무한대기하게 하였다. 
값이 전송되면 그 값을 serialChar로 전송하여 어느 모터에 전송하는 값인지 일단 판별한다. 
먼저 보낸값이 0이면 수직으로 움직이는 모터를 제어하라는 명령이고 1일 경우에는 수평으로 움직이는 모터를 제어하라는 명령으로 판별한다.

0이나 1이 전송된 다음에는 선택된 모터를 움직일 각도값을 전송받는다. 
각도값을 전송받으면 그 전송한 값만큼 서보모터를 제어한다.

기술문서

  • 부품목록
  • 회로도
  • 브레드보드 레이아웃
  • 스케치

수박쨈

아두이노, 서보 모터, 시리얼 통신, OpenCV, 프로세싱