2014/06/18

大眼仔, 人臉追蹤
大眼仔的人臉追蹤功能, 雖然還有個明顯缺點, 但至少能拿出來見人了. XD. (YouTube 影片)

解釋一下運作原理:
  1. 將大眼仔相對 web cam 的位置, 分成五種級距 (左二, 左一, 中, 右一, 右二).
  2. 將 Processing + OpenCV 傳回來的人臉框位置, 也分成五種級距 (左二, 左一, 中, 右一, 右二).
  3. 定義不同位置的大眼仔如何回應人臉框位置.
    不同位置的大眼仔, 轉頭的角度會有不同.


這個示範, 萊恩大兵用了兩片 Arduino 板子, 每片板子接兩個大眼仔, 各定義其位置為左二, 左一與右一, 右二. (兩片板子則透過 I2C 溝通). 
搖頭晃腦..

程式碼

(Arduino, for master device)
//
// OT108Eyeballs_i2c_m.ino
// - Use I2C bus to control multiple devices
// - Master device
//

#include <Wire.h>
#include <Servo.h>
#include <AniEyeball.h>

#define N_SLAVE   1
#define N_EYEBALL 2
#define FACE_TRACKING_MODE 1

AniEyeball ot108eyeball[N_EYEBALL];

// 眼皮_PIN # i
// 眼球_PIN # i 1
const byte servoPin[N_EYEBALL*2] = {2,3,4,5};

// 定義大眼仔的相對位置
const byte flagAnyEyeballPos[N_EYEBALL] = {FT_R1, FT_R2};
const int WORKING_MODE = FACE_TRACKING_MODE;

// for command string handling (from serial port)
char inData[80];
byte index = 0;
byte count = 0;

#define NO_PREFIX       0
#define S_PREFIX        1
#define P_PREFIX        1
#define E_PREFIX        1

#define TOKEN_S         "s"
#define TOKEN_P         "p"
#define TOKEN_E         "e"

byte pfxS = NO_PREFIX;
byte pfxP = NO_PREFIX; 
byte pfxE = NO_PREFIX; 
byte sAll = 0; // all slaves mode
byte sNum = 0; // slave-#
byte pNum = 0; // pattern-#
byte eNum = 0; // eyeball-#
byte fFinish = 0;

void setup()
{
  Wire.begin();
  
  Serial.begin(9600);

  for (int i=0; i<N_EYEBALL; i  )
  {
   ot108eyeball[i].setPinP(servoPin[i*2]);
   ot108eyeball[i].setPinB(servoPin[i*2 1]);

    if (WORKING_MODE == FACE_TRACKING_MODE)
      ot108eyeball[i].setFaceTrackingFlag(flagAnyEyeballPos[i]);    
  }
}

void loop()
{
  while (Serial.available() > 0)
  {
    char aChar = Serial.read();
    if (aChar == '\n')
    {
      // End of string detected. Time to parse
      char *p = inData; //assign the string to *p
      int counter = 0;  //initialise the counter
      String str;
            
      str = strtok(p, ",");
      while (str != NULL)
      {
        Serial.println(str);
        ParseCommand(str);
        counter  ;
        str = strtok(NULL, ",");
      }

      index = 0;
      inData[index] = NULL;      
      Serial.write("done\n");
    }
    else
    {
      inData[index] = aChar;
      index  ;
      inData[index] = '\0'; // Keep the string NULL terminated
    }
  }

  if (fFinish)
  {
    // 全部命令解析完成, 來操控 slave 吧
    if (sAll)
    {
      for (int i=0; i<N_SLAVE; i  )
      {   
        //
        // 暫且忽略 e 命令吧, 以後有空再補上
        //
        Wire.beginTransmission(i 1);
        Wire.write(pNum);
        Wire.endTransmission();        
      }
    }
    else
    {
      // 特定 salves
      Wire.beginTransmission(sNum);      
      Wire.write(pNum);   
      Wire.endTransmission();
    }

    // 不光是 slave 要做事, master 自己也要做啊.
    for (int i=0; i<N_EYEBALL; i  )
    {
      if (WORKING_MODE == FACE_TRACKING_MODE)
        ot108eyeball[i].faceTrackingMoving(pNum);
      else
        ot108eyeball[i].acting(pNum);
    } 
  
    resetFlags();    
  }

  // if no inputs from remote, how about letting master device's eyeballs moving 
  // tweeting();
}

void ParseCommand(String str)
{
  // the input commands formats:
  //   * s
  //      - all: all slaves
  //      - (num): slave-#(num)   
  //   * e
  //      - (num): eyeball-#(num)
  //   * p
  //      - (num): pattern-#(num)  
  //
  // the command example:
  //   s,all,p,1    -> all slaves, run pattern-1
  //   s,5,e,1,p,3  -> slave-5, eyeball-1, run pattern-3
  //   s,5,p,3      -> slave-5, all eyeballs, run pattern-3   

   if (str == TOKEN_S && pfxS == NO_PREFIX) // 開始 s 命令
   {
      pfxS = S_PREFIX; 
   }
   else if (str == "all" && pfxS == S_PREFIX) // s 命令, all
   {                               
      sAll = 1;
      pfxS = NO_PREFIX; // s 命令結束      
   }
   else if (pfxP == NO_PREFIX && pfxE == NO_PREFIX && pfxS == S_PREFIX) // 仍在解析 s 命令, 尚未走到 p 或 e 命令
   {
      sNum = str.toInt();
      pfxS = NO_PREFIX; // s 命令結束
   }
   else if (str == TOKEN_E && pfxE == NO_PREFIX && pfxP == NO_PREFIX && pfxS == NO_PREFIX) // 開始 e 命令
   {
      pfxE = E_PREFIX;
   }
   else if (str == TOKEN_P && pfxP == NO_PREFIX && pfxE == NO_PREFIX && pfxS == NO_PREFIX) // 開始 p 命令
   {
      pfxP = P_PREFIX;
   }
   else if (pfxE == E_PREFIX && pfxP == NO_PREFIX && pfxS == NO_PREFIX) // 解析 e 命令的參數
   {
     eNum = str.toInt();
     pfxE = NO_PREFIX; // e 命令結束
   }
   else if (pfxP == P_PREFIX && pfxE == NO_PREFIX && pfxS == NO_PREFIX) // 解析 p 命令的參數
   {
      pNum = str.toInt();
      pfxP = NO_PREFIX; // p 命令結束
      fFinish = 1;
   }
}

void resetFlags()
{
  pfxS = NO_PREFIX;
  pfxP = NO_PREFIX; 
  pfxE = NO_PREFIX; 
  sAll = 0;
  sNum = 0;
  pNum = 0;
  eNum = 0;
  fFinish = 0;
}


(Arduino, for slave device)
//
// OT108Eyeballs_i2c_s.ino
// - Use I2C bus to control multiple devices
// - Slave device
//

#include <Wire.h>
#include <Servo.h>
#include <AniEyeball.h>

#define N_EYEBALL 2
#define FACE_TRACKING_MODE 1

const int SLAVE_ADDRESS = 1;

AniEyeball ot108eyeball[N_EYEBALL];

// 眼皮_PIN # i
// 眼球_PIN # i 1
const byte servoPin[N_EYEBALL*2] = {2,3,4,5};

// 定義大眼仔的相對位置
const byte flagAnyEyeballPos[N_EYEBALL] = {FT_R1, FT_R2};
const int WORKING_MODE = FACE_TRACKING_MODE;

char inByte = 0;

void setup()
{
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onReceive(receiveEvent); // register event
  
  Serial.begin(9600);

  for (int i=0; i<N_EYEBALL; i  )
  {
    ot108eyeball[i].setPinP(servoPin[i*2]);
    ot108eyeball[i].setPinB(servoPin[i*2 1]);

    if (WORKING_MODE == FACE_TRACKING_MODE)
      ot108eyeball[i].setFaceTrackingFlag(flagAnyEyeballPos[i]);
  }
}

void loop()
{
  // 可能要有緩衝, 以免灌進太多指令, 來不及處理
  if (inByte)
  {
    for (int i=0; i<N_EYEBALL; i  )
    {
      if (WORKING_MODE == FACE_TRACKING_MODE)
        ot108eyeball[i].faceTrackingMoving(inByte);
      else
        ot108eyeball[i].acting(inByte);
    }    
    inByte = 0;    
  }
  else
  {
    //ot108eyeball[i].tweeting();
  }

}

void receiveEvent(int howMany)
{
  // master 傳來的命令格式為一數值, 代表 pattern-#.
  while (Wire.available()) 
  {
    inByte = Wire.read();
  }
}


(Processing + OpenCV) (感謝 Philip Zheng 提供程式與大力幫忙)
 

import processing.serial.*;
import processing.video.*;
 
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.CvType;
import org.opencv.core.Size;
import org.opencv.objdetect.CascadeClassifier;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.Objdetect;
 
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.awt.image.Raster;

Serial myPort;  // Create object from Serial class
String cmdstring = "";
int timeInterval = 500;
int lastUpdate;

Capture cap;
int pixCnt;
BufferedImage bm;
 
CascadeClassifier faceDetector;
MatOfRect faceDetections;
 
void setup() {
  size(640, 480);
  System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
  println(Core.VERSION);
 
  cap = new Capture(this, width, height);
  cap.start();
  bm = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
  pixCnt = width*height*4;
 
  faceDetector = new CascadeClassifier(dataPath("haarcascade_frontalface_default.xml"));
  faceDetections = new MatOfRect();
  
  myPort = new Serial(this, Serial.list()[5], 9600);
  lastUpdate = millis() - timeInterval;
}
 
void convert(PImage _i) {
  bm.setRGB(0, 0, _i.width, _i.height, _i.pixels, 0, _i.width);
  Raster rr = bm.getRaster();
  byte [] b1 = new byte[pixCnt];
  rr.getDataElements(0, 0, _i.width, _i.height, b1);
  Mat m1 = new Mat(_i.height, _i.width, CvType.CV_8UC4);
  m1.put(0, 0, b1);
 
  Mat m2 = new Mat(_i.height, _i.width, CvType.CV_8UC1);
  Imgproc.cvtColor(m1, m2, Imgproc.COLOR_BGRA2GRAY);   
 
  faceDetector.detectMultiScale(m2, faceDetections, 3, 1, 
  Objdetect.CASCADE_DO_CANNY_PRUNING, new Size(40, 40), new Size(240, 240));
 
  bm.flush();
  m2.release();
  m1.release();
}
 
void draw() 
{
  if (!cap.available()) 
    return;
  background(0);
  cap.read();
  convert(cap);
  image(cap, 0, 0);
  for (Rect rect: faceDetections.toArray()) 
  {
    noFill();
    stroke(255, 0, 0);
    rect(rect.x, rect.y, rect.width, rect.height);
    
    //arduino serial command will be here
    if (millis()-lastUpdate >= timeInterval)
    {
      print("1 sec... ");
      println(rect.x);
      if (rect.x <= 170)
        cmdstring = "s,1,p,-2\n";
      else if (rect.x > 170 && rect.x <= 270)
        cmdstring = "s,1,p,-1\n";
      else if (rect.x > 270 && rect.x <= 370)
        cmdstring = "s,1,p,0\n";
      else if (rect.x > 370 && rect.x <= 470)
        cmdstring = "s,1,p,1\n";
      else
        cmdstring = "s,1,p,2\n";

      myPort.write(cmdstring);
      lastUpdate = millis();
    }
  }
}



後記

這示範有幾個地方待改進, 先記下來, 以後有空再慢慢改.

  • 人臉框與實際人臉的移動方向左右相反.
  • 人臉得很靠近 webcam 才辨識的準.
  • 應該改成人群追蹤, 會比較適合開放空間的展示.

[萊恩大兵的其它文章]

自製大四軸

自製大四軸, (1) 零組件篇, 遙控器 (Drone, Quadcopter, Futaba, Maker, Arduino, Animatronic Eye)
自製大四軸, (2) 零組件篇, 飛控板 (Drone, Quadcopter, MultiWii, Arduino, Futaba, Maker)
自製大四軸, (3) 零組件篇, 自行雷切木質機架 (Drone, Quadcopter, Maker, Laser Cut)
自製大四軸, (4) 零組件篇, 馬達與電變調整 (Drone, Quadcopter, Maker, Electric Speed Control, Motor)
自製大四軸, (5) 組裝篇, 四軸飛行器成形 (Drone, Quadcopter, MultiWii, Arduino, Maker, Electric Speed Control, Motor)
自製大四軸, (6) 調整篇, 飛行前兩三事 (Drone, Quadcopter, Maker, Futaba, Arduino, MultiWii)
自製大四軸, (7) 充電篇, iMax B6 充電器操作記要 (Charger, Battery)

自動報球速的棒球



CC2540 Bluetooth Low Energy
筆記, CC2540 Bluetooth Low Energy, (1) 開發環境 架設 (Bluetooth, CC2540)
筆記, CC2540 Bluetooth Low Energy, (2) 跑第一個範例程式 (Bluetooth, CC2540)
筆記, CC2540 Bluetooth Low Energy, (3) SimpleBLEPeripheral 簡單介紹 (Bluetooth, CC2540)
筆記, CC2540 Bluetooth Low Energy, (4) 在智慧手機上執行範例程式 (Bluetooth, CC2540)
筆記, CC2540 Bluetooth Low Energy, (5) 偵測與發送 iBeacon 訊號 (Bluetooth, CC2540, iBeacon)
實作, iBeacon 發訊器 x 防丟器 (Bluetooth, CC2540, iBeacon)
實作, iBeacon 尋寶遊戲 (Bluetooth, CC2540, iBeacon, iOS app)
實作, BLE + iOS app, 遙控燈泡君 (Bluetooth, CC2540, iOS app)
做實驗, 用 iBeacon 做自動控制的可行性 (Bluetooth, iBeacon, CC2540, Automation, URL Scheme, iOS app)

藍色小鋪一起來做

藍色小鋪一起來做, (1) 用 beacon 控制開關的枱燈
藍色小鋪一起來做, (2) 講解 BLE CC2540 UART 通訊範例程式 (Bluetooth, CC2540, UART)
藍色小鋪一起來做, (3) 藍牙枱燈專案實作 (上) (Bluetooth, CC2540)
藍色小鋪一起來做, (4) 藍牙枱燈專案實作 (下) (Bluetooth, CC2540)
藍色小鋪一起來做, (5) iBeacon scanner 專案示範與解說 (Bluetooth, CC2540, iBeacon)
藍色小鋪一起來做, (6) 完成, 用 iBeacon 控制開關的枱燈 (Bluetooth, CC2540, iBeacon)

小惡魔 無線溫度感測器

108 大眼仔
Plot Clock
體驗, 原住民互動故事書@宜蘭大同鄉泰雅生活館
體驗, 蛋生音互動裝置@兒童美術館 (Arduino, 3D Printing, HC-SR04, Interactive)



實作, 電容感應音樂樹

0 意見:

張貼留言