亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Manufacturing Technology >> 制造工艺

Arduino Nano + OpenCV 人脸追踪自主机器人小车

元器件与物料清单

元器件 数量
Creator Ci20 开发板 (运行 OpenCV) 1
SparkFun 双 H 桥驱动器 L298 1
锂离子电池 1000mAh 1
舵机 Tower Pro MG996R 2
摄像头 (通用,旧笔记本摄像头改装) 1
Dexter Industries GoPiGo 机器人底盘套件 1
Arduino Nano R3 1
超声波测距模块 HC-SR04 1

应用与在线服务

项目简介

这是一款能追踪人脸的自主小车:人脸移动,小车跟随。核心架构为 Ci20 (运行 OpenCV 人脸检测) + Arduino Nano (控制电机/舵机),通过串口通讯协作。

软件环境准备 (Ci20 Debian 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

# 串口权限设置 (根据实际设备节点调整)
sudo chown ci20:ci20 /dev/ttyUSB0  # Arduino USB 串口
sudo chown ci20:ci20 /dev/ttyS1    # Ci20 原生串口

若想从源码编译 OpenCV,可参考相关教程。

运行方法

# 运行人脸追踪脚本 (0 为摄像头索引,多摄像头时调整)
sudo python facetrackingcar.py 0

性能优化:默认不显示捕获画面,如需预览,取消代码中 cv.ShowImage 等行的注释。脚本同目录需放置 haarcascade_frontalface_alt.xml 分类器文件。

硬件连接

代码

faceser.py - OpenCV 人脸检测与云台控制 (Python)

#!/usr/bin/python
""" 人脸检测演示:摄像头画面中检测人脸并用红框标记,
    通过两个舵机将云台转动使人脸居中
    基于 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     # 初始水平位置
servoTiltPosition = 45    # 初始垂直位置
panGpioPin = 2            # Arduino 端舵机 ID (非引脚号)
tiltGpioPin = 1

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):
    """发送舵机指令:起始字节 255 + 舵机号 + 角度"""
    if min_pwm <= angle <= max_pwm:
        ser.write(chr(255))
        ser.write(chr(servo))
        ser.write(chr(angle))
    else:
        print("Servo angle must be 0-180")

if __name__ == '__main__':
    ser = serial.Serial(port='/dev/ttyUSB0', baudrate=115200, timeout=1)
    
    parser = OptionParser(usage="usage: %prog [options] [camera_index]")
    parser.add_option("-c", "--cascade", dest="cascade", type="str",
                      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("Need camera index, e.g. 0")
        sys.exit(0)
    
    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
            
            # 水平追踪
            if midFaceX < (midScreenX - midScreenWindow):
                servoPanPosition += panStepSize
                print(f"{midFaceX} > {midScreenX} : Pan Right : {servoPanPosition}")
            elif midFaceX > (midScreenX + midScreenWindow):
                servoPanPosition -= panStepSize
                print(f"{midFaceX} < {midScreenX} : Pan Left : {servoPanPosition}")
            else:
                print(f"{midFaceX} ~ {midScreenX} : {servoPanPosition}")
            
            servoPanPosition = min(max(servoPanPosition, min_pwm), max_pwm)
            move(panGpioPin, servoPanPosition)
            
            # 垂直追踪
            if midFaceY < (midScreenY - midScreenWindow):
                if servoTiltPosition <= 90:
                    servoTiltPosition -= tiltStepSize
                    print(f"{midFaceY} > {midScreenY} : Tilt Down : {servoTiltPosition}")
            elif midFaceY > (midScreenY + midScreenWindow):
                if servoTiltPosition >= 1:
                    servoTiltPosition += tiltStepSize
                    print(f"{midFaceY} < {midScreenY} : Tilt Up : {servoTiltPosition}")
            else:
                print(f"{midFaceY} ~ {midScreenY} : {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(f'time elapsed:\t{time_elapsed}')
            print(f'var:\t{var}')
            
            # 20秒无人脸自动复位
            if time_elapsed == 20:
                move(3, servoTiltPosition)
                servoPanPosition = 90
                servoTiltPosition = 45
                var = 0
        
        if cv.WaitKey(1) >= 0:
            break

Car.ino - Arduino 电机/舵机/超声波控制


#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()  { setMotors(HIGH, LOW, HIGH, LOW, HIGH, LOW, HIGH, LOW); }
void reverse()  { setMotors(LOW, HIGH, LOW, HIGH, LOW, HIGH, LOW, HIGH); }
void right()    { setMotors(LOW, HIGH, LOW, HIGH, HIGH, LOW, HIGH, LOW); }
void left()     { setMotors(HIGH, LOW, HIGH, LOW, LOW, HIGH, LOW, HIGH); }
void Stop()     { setMotors(LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW); }
void StopH()    { setMotors(HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH); }

void setMotors(int a1, int a2, int b1, int b2, int c1, int c2, int d1, int d2) {
  digitalWrite(MA_1, a1); digitalWrite(MA_2, a2);
  digitalWrite(MB_1, b1); digitalWrite(MB_2, b2);
  digitalWrite(MC_1, c1); digitalWrite(MC_2, c2);
  digitalWrite(MD_1, d1); digitalWrite(MD_2, d2);
}

scanlinux.py - 串口扫描工具


#!/usr/bin/env python
""" 扫描 Linux 可用串口设备 """
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)

自定义零件与外壳

打包文件: FaceTrakingCar.rar (含 Python 脚本和 Arduino 代码)

原理图

Arduino 控制电机和舵机,引脚分配可能因硬件而异

Arduino Nano + OpenCV 人脸追踪自主机器人小车

制造工艺

  1. Arduino 数字骰子:LCD 显示随机点数的电子骰子
  2. 用 Arduino Nano 自制 37 灯珠轮盘赌游戏机
  3. ATtiny85 迷你街机:贪吃蛇 — 打造掌上 OLED 游戏机
  4. 便携测距仪 DIY 套件:Arduino Nano + HC-SR04 + 18650 电池 + 3D 打印外壳
  5. MobBob:3D 打印智能机器人,Android 手机无缝控制,语音视觉一体
  6. PiBot:Arduino驱动的88键钢琴机器人,精准演奏MIDI乐曲
  7. 铜电镀智能系统:基于 Arduino UNO 的可控电解装置
  8. Arduino Nano 驱动 NeoPixel 矩阵:自制霓虹乒乓游戏
  9. 全向轮跟随机器人:OpenCV 人脸检测与安全陪伴
  10. 使用 Arduino 移位寄存器创建绚丽灯光序列
  11. 智能门禁系统:Windows + Face++ 人脸识别 + Bolt IoT + Arduino 舵机开锁
  12. 智能插座:基于 Arduino 与 RTC 的 120V 定时开关插座