아두이노 나노 & OpenCV로 만드는 자율 주행 안면 추적 로봇 카
부품 및 소모품
부품 수량
Creator Ci20 보드 1
SparkFun 듀얼 H-브리지 모터 드라이버 L298 1
리튬이온 배터리 1000mAh 1
서보 모터 (Tower Pro MG996R) 2
카메라 (범용, 구형 노트북 카메라 재활용) 1
Dexter Industries GoPiGo 로봇 베이스 키트 1
Arduino Nano R3 1
초음파 거리 센서 HC-SR04 1
앱 및 온라인 서비스
Arduino IDE
OpenCV (Python용)
프로젝트 개요
내 얼굴을 추적하는 자동차입니다. 내가 멀어지면 따라옵니다!
Ci20(임베디드 리눅스 보드) 필수 설치 사항
OpenCV
Python
시리얼 포트 권한 수정
데비안 8 배포판 사용. 설치 명령어:
시스템 업데이트
sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade
OpenCV 설치 (버전 2.4.9.1, 구버전이지만 잘 작동)
sudo apt-get install libopencv-dev python-opencv
소스에서 컴파일하려면 이 튜토리얼 참조.
PySerial 설치
sudo apt-get install python python-serial
시리얼 포트 권한 변경 (USB 또는 내장)
sudo chown ci20:ci20 /dev/ttyUSB0 # 아두이노 USB 시리얼
sudo chown ci20:ci20 /dev/ttyS1 # Ci20 내장 시리얼
파이썬 스크립트 실행 (비디오 소스 인덱스 0부터 시작)
sudo python facetrackingcar.py 0
성능 향상 팁
캡처 이미지 미표시로 약간의 성능 향상. 보려면 코드에서 다음 라인 주석 해제:
#cv.ShowImage("result", img)
#cv.NamedWindow("result", 1)
#cv.DestroyWindow("result")
haarcascade_frontalface_alt.xml 파일을 스크립트와 같은 디렉토리에 둬야 합니다.
코드
faceser.py (Python - OpenCV 안면 추적 및 시리얼 전송)
#!/usr/bin/python
""" haar-like 특징으로 안면/객체 검출 데모.
카메라 이미지/비디오 스트림에서 얼굴 찾아 빨간 박스 표시,
두 서보로 웹캠 중앙에 얼굴 오도록 제어.
OpenCV 샘플 facedetect.py 기반 """
import sys
from optparse import OptionParser
import serial
import cv2.cv as cv
import time
# haar 검출 파라미터
min_size = (20, 20)
image_scale = 2
haar_scale = 1.2
min_neighbors = 2
haar_flags = cv.CV_HAAR_DO_CANNY_PRUNING
max_pwm = 180
min_pwm = 1
midScreenWindow = 20 # 화면 중심 허용 오차
panStepSize = 1 # 팬 스텝 (도)
tiltStepSize = 1 # 틸트 스텝 (도)
servoPanPosition = 90 # 초기 팬 위치 (x)
servoTiltPosition = 45 # 초기 틸트 위치 (y)
panGpioPin = 2 # 아두이노 팬 서보 ID (핀 번호 아님)
tiltGpioPin = 1 # 아두이노 틸트 서보 ID
var = 0
def detect_and_draw(img, cascade):
gray = cv.CreateImage((img.width, img.height), 8, 1)
small_img = cv.CreateImage((cv.Round(img.width / image_scale),
cv.Round(img.height / image_scale)), 8, 1)
cv.CvtColor(img, gray, cv.CV_BGR2GRAY)
cv.Resize(gray, small_img, cv.CV_INTER_LINEAR)
cv.EqualizeHist(small_img, small_img)
midFace = None
if cascade:
t = cv.GetTickCount()
faces = cv.HaarDetectObjects(small_img, cascade, cv.CreateMemStorage(0),
haar_scale, min_neighbors, haar_flags, min_size)
t = cv.GetTickCount() - t
if faces:
for ((x, y, w, h), n) in faces:
pt1 = (int(x * image_scale), int(y * image_scale))
pt2 = (int((x + w) * image_scale), int((y + h) * image_scale))
cv.Rectangle(img, pt1, pt2, cv.RGB(255, 0, 0), 3, 8, 0)
# 얼굴 중심 좌표 계산
x1, x2 = pt1[0], pt2[0]
y1, y2 = pt1[1], pt2[1]
midFaceX = x1 + ((x2 - x1) / 2)
midFaceY = y1 + ((y2 - y1) / 2)
midFace = (midFaceX, midFaceY)
return midFace
def move(servo, angle):
"""지정된 서보를 각도로 이동. servo: 1-4, angle: 0-180"""
if min_pwm <= angle <= max_pwm:
ser.write(chr(255))
ser.write(chr(servo))
ser.write(chr(angle))
else:
print "Servo angle must be an integer between 0 and 180.\n"
if __name__ == '__main__':
ser = serial.Serial(port='/dev/ttyUSB0', baudrate=115200, timeout=1)
# ser = serial.Serial(port='/dev/ttyS0', baudrate=115200, timeout=1) # 내장 시리얼
# ser = serial.Serial(port='COM14', baudrate=115200, timeout=1) # 윈도우
parser = OptionParser(usage="usage: %prog [options] [camera_index]")
parser.add_option("-c", "--cascade", action="store", dest="cascade",
type="str", help="Haar cascade file, default %default",
default="./haarcascade_frontalface_alt.xml")
(options, args) = parser.parse_args()
cascade = cv.Load(options.cascade)
if len(args) != 1:
parser.print_help()
sys.exit(1)
input_name = args[0]
if input_name.isdigit():
capture = cv.CreateCameraCapture(int(input_name))
cv.SetCaptureProperty(capture, cv.CV_CAP_PROP_FRAME_WIDTH, 320)
cv.SetCaptureProperty(capture, cv.CV_CAP_PROP_FRAME_HEIGHT, 240)
else:
print "We need a camera input! Specify camera index e.g. 0"
sys.exit(0)
# cv.NamedWindow("result", 1)
if capture:
frame_copy = None
move(panGpioPin, servoPanPosition)
move(tiltGpioPin, servoTiltPosition)
while True:
start = time.time()
frame = cv.QueryFrame(capture)
if not frame:
cv.WaitKey(0)
break
if not frame_copy:
frame_copy = cv.CreateImage((frame.width, frame.height),
cv.IPL_DEPTH_8U, frame.nChannels)
if frame.origin == cv.IPL_ORIGIN_TL:
cv.Copy(frame, frame_copy)
else:
cv.Flip(frame, frame_copy, 0)
midScreenX = frame.width / 2
midScreenY = frame.height / 2
midFace = detect_and_draw(frame_copy, cascade)
if midFace is not None:
midFaceX, midFaceY = midFace[0], midFace[1]
# 팬 제어 (좌우)
if midFaceX < (midScreenX - midScreenWindow):
servoPanPosition += panStepSize
print str(midFaceX) + " > " + str(midScreenX) + " : Pan Right : " + str(servoPanPosition)
elif midFaceX > (midScreenX + midScreenWindow):
servoPanPosition -= panStepSize
print str(midFaceX) + " < " + str(midScreenX) + " : Pan Left : " + str(servoPanPosition)
else:
print str(midFaceX) + " ~ " + str(midScreenX) + " : " + str(servoPanPosition)
servoPanPosition = min(max(servoPanPosition, min_pwm), max_pwm)
move(panGpioPin, servoPanPosition)
# 틸트 제어 (상하)
if midFaceY < (midScreenY - midScreenWindow):
if servoTiltPosition <= 90:
servoTiltPosition -= tiltStepSize
print str(midFaceY) + " > " + str(midScreenY) + " : Tilt Down : " + str(servoTiltPosition)
elif midFaceY > (midScreenY + midScreenWindow):
if servoTiltPosition >= 1:
servoTiltPosition += tiltStepSize
print str(midFaceY) + " < " + str(midScreenY) + " : Tilt Up : " + str(servoTiltPosition)
else:
print str(midFaceY) + " ~ " + str(midScreenY) + " : " + str(servoTiltPosition)
servoTiltPosition = min(max(servoTiltPosition, min_pwm), max_pwm)
move(tiltGpioPin, servoTiltPosition)
else:
end = time.time() + 0.1
var += 0.1
time_elapsed = int(end - start + var)
print 'time elapsed:\t{}'.format(time_elapsed)
print 'var:\t{}'.format(var)
if time_elapsed == 20:
move(3, servoTiltPosition)
servoPanPosition = 90
servoTiltPosition = 45
var = 0
if cv.WaitKey(1) >= 0:
break
# cv.DestroyWindow("result")
Car.ino (아두이노 나노 - 모터/서보 제어)
#include
#define MA_1 2
#define MA_2 3
#define MB_1 4
#define MB_2 5
#define MC_1 6
#define MC_2 7
#define MD_1 8
#define MD_2 9
#define SERVOX_PIN 11
#define SERVOY_PIN 10
#define trigPin 13
#define echoPin 12
Servo servoy;
Servo servox;
int x = 90, prevX;
int y = 45, prevY;
int userInput[3];
int startbyte, servo, pos, i;
int State = LOW;
unsigned long previousMillis = 0, val = 100;
const long interval = 100;
bool scana = false;
unsigned long currentMillis;
void setup() {
inicializate();
currentMillis = millis();
}
void loop() {
if (Serial.available() > 2) {
startbyte = Serial.read();
if (startbyte == 255) {
for (i = 0; i < 2; i++) {
userInput[i] = Serial.read();
}
servo = userInput[0];
pos = userInput[1];
if (pos == 255) servo = 255; // 패킷 에러 복구
switch (servo) {
case 1: servoy.write(pos); break; // 틸트 서보
case 2: servox.write(pos); range(pos); break; // 팬 서보 + 거리 기반 주행
case 3: scan(45); scan(65); break; // 스캔 모드
default: break;
}
}
}
}
void scan(int val) {
while (Serial.available() == 0) {
servoy.write(val);
currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
servox.write(pos);
if (State == LOW) {
pos += 1;
if (pos == 130) State = HIGH;
} else {
pos -= 1;
if (pos == 40) { State = LOW; break; }
}
}
}
}
long distancia() {
long duration, distance;
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
duration = pulseIn(echoPin, HIGH);
distance = (duration / 2) / 29.1;
return distance;
}
void range(int pos) {
if (pos >= 80 && pos <= 100) moves(); // 정면 → 전진/후진
else if (pos >= 100 && pos <= 180) { left(); delay(10); Stop(); } // 우측 → 좌회전
else if (pos >= 1 && pos <= 80) { right(); delay(10); Stop(); } // 좌측 → 우회전
}
void moves() {
long distance = distancia();
if (val <= 80) { reverse(); delay(50); Stop(); }
else if (val >= 140) { forward(); delay(50); Stop(); }
static long previusdistance = 0;
if (distance >= previusdistance) val = distance - previusdistance;
else val = previusdistance - distance;
previusdistance = distance;
}
void inicializate() {
Serial.begin(115200);
pinMode(MA_1, OUTPUT); pinMode(MA_2, OUTPUT);
pinMode(MB_1, OUTPUT); pinMode(MB_2, OUTPUT);
pinMode(MC_1, OUTPUT); pinMode(MC_2, OUTPUT);
pinMode(MD_1, OUTPUT); pinMode(MD_2, OUTPUT);
pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT);
servox.attach(SERVOX_PIN);
servoy.attach(SERVOY_PIN);
}
void forward() {
digitalWrite(MA_1, HIGH); digitalWrite(MA_2, LOW);
digitalWrite(MB_1, HIGH); digitalWrite(MB_2, LOW);
digitalWrite(MC_1, HIGH); digitalWrite(MC_2, LOW);
digitalWrite(MD_1, HIGH); digitalWrite(MD_2, LOW);
}
void reverse() {
digitalWrite(MA_1, LOW); digitalWrite(MA_2, HIGH);
digitalWrite(MB_1, LOW); digitalWrite(MB_2, HIGH);
digitalWrite(MC_1, LOW); digitalWrite(MC_2, HIGH);
digitalWrite(MD_1, LOW); digitalWrite(MD_2, HIGH);
}
void right() {
digitalWrite(MA_1, LOW); digitalWrite(MA_2, HIGH);
digitalWrite(MB_1, LOW); digitalWrite(MB_2, HIGH);
digitalWrite(MC_1, HIGH); digitalWrite(MC_2, LOW);
digitalWrite(MD_1, HIGH); digitalWrite(MD_2, LOW);
}
void left() {
digitalWrite(MA_1, HIGH); digitalWrite(MA_2, LOW);
digitalWrite(MB_1, HIGH); digitalWrite(MB_2, LOW);
digitalWrite(MC_1, LOW); digitalWrite(MC_2, HIGH);
digitalWrite(MD_1, LOW); digitalWrite(MD_2, HIGH);
}
void Stop() {
digitalWrite(MA_1, LOW); digitalWrite(MA_2, LOW);
digitalWrite(MB_1, LOW); digitalWrite(MB_2, LOW);
digitalWrite(MC_1, LOW); digitalWrite(MC_2, LOW);
digitalWrite(MD_1, LOW); digitalWrite(MD_2, LOW);
}
void StopH() {
digitalWrite(MA_1, HIGH); digitalWrite(MA_2, HIGH);
digitalWrite(MB_1, HIGH); digitalWrite(MB_2, HIGH);
digitalWrite(MC_1, HIGH); digitalWrite(MC_2, HIGH);
digitalWrite(MD_1, HIGH); digitalWrite(MD_2, HIGH);
}
scanlinux.py (리눅스 시리얼 포트 스캔 유틸리티)
#!/usr/bin/env python
""" 리눅스 시리얼 포트 스캔. USB/시리얼 어댑터 포함.
pySerial 일부 (https://pyserial.sf.net) """
import serial
import glob
def scan():
return (glob.glob('/dev/ttyS*') + glob.glob('/dev/ttyUSB*') +
glob.glob('/dev/ttyACM*') + glob.glob('/dev/serial/by-id/*'))
if __name__ == '__main__':
print "Found ports:"
for name in scan():
print name
커스텀 파트 및 인클로저
파이썬 OpenCV 안면 추적 스크립트와 아두이노 코드 압축 파일: FaceTrakingCar.rar
회로도
아두이노로 모터와 서보 제어. 핀아웃은 하드웨어에 따라 다를 수 있음.