ESPIno32CAM : Face Recognition

ESPIno32CAM : รู้จำใบหน้า

ตัวอย่างการใช้งาน ESPIno32CAM เพื่อถ่ายภาพ และ ตรวจหาพิกัดของใบหน้ามนุษย์ และ จดจำใบหน้า

เริ่มต้นการทดลอง

-เปิดโปรแกรม Arduino IDE

-เปิดตัวอย่าง File > Example >ESPIno32CAM > EX_Face_Recognition

– ตั้งค่า Arduino IDE ให้ใช้งานกับ ESPIno32CAM (Link)

– Upload Program ลงไปยัง ESPIno32CAM

– เปิด Program JPG Serial Streaming

1 เลือก Comport ให้ตรงกับ Comport ของบอร์ด ESPIno32

2 เลือก Baudrate ให้ตรงกับ Baudrate ใน Code Arduino (Default =2000000 )

3 กด Button Connect

!!! ก่อนกด Button Connect จะต้องปิด Program อื่นๆ ที่ใช้งาน Comport อยู่ เช่น หน้าต่าง Serial Monitor ของ ArduinoIDE เป็นต้น

– โปรแกรม  JPG Serial Streaming จะแสดงรูปภาพที่ถ่ายจากบอร์ด ESPIno32CAM บน PictureBox และ เมื่อตรวจพบใบหน้าในภาพ จะแสดงข้อความ Intruder Alert (แจ้งเตือนผู้บุกรุก) เนื่องจากยังไม่มีการ Enroll ใบหน้า ทุกใบหน้าที่ตรวจพบ จึงถูกตีความหมายเป็นผู้บุกรุก

–  Enroll ใบหน้าเข้าไปในระบบ โดย

1 นำกล้อง ถ่ายรูปใบหน้าให้สามารถตรวจจับใบหน้าได้

2 กด Button Program บนบอร์ดค้างไว้

3 รอจนขึ้นข้อความ ID x Sample 1 จึงปล่อยมือ แล้วรอจนกว่าจะ Enroll จนถึง ID x Sample 5

5 เมื่อ Enroll เสร็จเรียบร้อบแล้ว เมื่อโมดูลตรวจจับใบหน้าที่เรา Enroll ได้ก็จะแสดงเป็นหมายเลข ID ตามลำดับที่ได้ทำการ Enroll เอาไว้ เช่น Subject 0 

ตัวอย่างโปรแกรมที่ใช้

#include "ESPino32CAM.h"
ESPino32CAM cam;
#define BUTTON 0
static mtmn_config_t mtmn_config = {0};
bool is_enrolling = false;
static face_id_list id_list = {0};
void setup() {
  Serial.begin(2000000);
  cam.printDebug("\r\nESPino32CAM");  
  if (cam.init() != ESP_OK)
  {
    cam.printDebug(F("Fail"));
    return;
  }
  sensor_t *s = cam.sensor();
  s->set_framesize(s, FRAMESIZE_QVGA);
  mtmn_config.min_face = 80;
  mtmn_config.pyramid = 0.7;
  mtmn_config.p_threshold.score = 0.6;
  mtmn_config.p_threshold.nms = 0.7;
  mtmn_config.r_threshold.score = 0.7;
  mtmn_config.r_threshold.nms = 0.7;
  mtmn_config.r_threshold.candidate_number = 4;
  mtmn_config.o_threshold.score = 0.7;
  mtmn_config.o_threshold.nms = 0.4;
  mtmn_config.o_threshold.candidate_number = 1;
  cam.faceIDInit(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
}
void loop()
{
  if (digitalRead(BUTTON) == 0)
  {
    is_enrolling = true;
  }
  camera_fb_t *fb = cam.capture();
  if (!fb)
  {
    cam.printDebug("Camera capture failed\r\n");
    return;
  }
  dl_matrix3du_t *rgb888;
  if (cam.jpg2rgb(fb,&rgb888))
  {
    cam.clearMemory(fb);
    box_array_t *net_boxes = cam.faceDetect(rgb888, &mtmn_config);
    if (net_boxes)
    {
      cam.drawFaceBoxes(rgb888, net_boxes, true);
      dl_matrix3du_t *aligned_face;
      if ( cam.alignFace(rgb888, net_boxes, &aligned_face) == ESP_OK)
      {
        if (is_enrolling)
        {
            int8_t left_sample_face = enroll_face(&id_list, aligned_face);
            cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW, "ID[%u] Sample[%u]", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
            cam.printDebug("ID "+String(id_list.tail)+"Sample "+String(ENROLL_CONFIRM_TIMES - left_sample_face));
            if (left_sample_face == 0){
                is_enrolling = false;
                cam.rgbGoto(10,50);
                cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW,"Enrolled Face ID: %d\n", id_list.tail);
            }
            if (left_sample_face == -2){
                is_enrolling = false;
                cam.rgbGoto(10,50);
                cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW,"Enrolled Fail\n", id_list.tail);
            }
             cam.rgbGoto(0,0);
        }
        else
        {
          int matched_id = cam.recognizeFace(&id_list, aligned_face);
          if (matched_id >= 0) 
          {
            cam.printDebug("Match Face ID: "+String(matched_id));
            cam.rgbPrintf(rgb888, FACE_COLOR_GREEN, "Hello Subject %u", matched_id);
          } 
          else 
          {
            cam.rgbPrintf(rgb888, FACE_COLOR_RED, "Intruder Alert!");
            cam.printDebug("Intruder Alert!");
          }
        }
      }
      cam.clearMemory(aligned_face);
      cam.clearMemory(net_boxes);
    }
  }
  uint8_t *jpgout;
  size_t  jpglen;
  if (cam.rgb2jpg(rgb888,&jpgout, &jpglen))
    Serial.write(jpgout, jpglen);
  cam.clearMemory(jpgout);
  cam.clearMemory(rgb888);
}

คำอธิบายโปรแกรม

บรรทัดที่ 1: ประกาศใช้ Library ESPIno32CAM

บรรทัดที่ 2: สร้าง Object cam

บรรทัดที่ 3: กำหนดให้ Button ที่ใช้รับ input สั่ง Enroll ใช้ io 0 

บรรทัดที่ 4: สร้าง Structure mtmn_config ชื่อ mtmn_config

บรรทัดที่ 5: สร้างตัวแปร เก็บสถานะสั่งงานEnroll

บรรทัดที่ 6: สร้างตัวแปร   face_id_list เพื่อเก็บใบหน้าที่ Enroll แล้ว

บรรทัดที่ 10: Initial โมดูลกล้อง

บรรทัดที่ 16: กำหนดให้ถ่ายภาพขนาด QVGA (320×240 Pixel)

บรรทัดที่ 17-26: กำหนดค่า threshold ให้กับ mtmn

บรรทัดที่ 27: initial face_id_list โดยกำหนด จำนวนใบหน้าสูงสุดที่เก็บได้ และ จำนวนครั้งที่ Enroll ต่อหนึ่งใบหน้า

FACE_ID_SAVE_NUMBER มีค่าเท่ากับ 7

ENROLL_CONFIRM_TIMES มีค่าเท่ากับ 5

บรรทัดที่ 31-34: ตรวจสอบว่ามีการกด Button ที่ IO 0 หรือไม่ ถ้าหากมีการกด จะกำหนดให้ตัวแปร  is_enrolling = true

บรรทัดที่ 35: ถ่ายรูปภาพ และ เก็บภาพ JPG ลงใน fb

บรรทัดที่ 42: แปลง JPG File ให้อยู่ในรูปแบบ RGB (R 8 bits , G 8 bits , B 8 bits) แล้วเก็บลงใน dl_matrix3du_t *rgb888

บรรทัดที่ 44: คืนค่า Memory ตัวแปล fb ที่เก็บ jpg ให้ส่วนกลาง

บรรทัดที่ 45: นำภาพที่แปลงเป็น RGB 888 และ ค่า mtmn_config_t เข้าไปประมวลผลหาใบหน้าในภาพ แล้วเก็บผลลัพธ์ลงใน net_boxes

บรรทัดที่ 46: ถ้าหากตรวจพบใบหน้า net_boxes จะมีค่ามากกว่า 0

บรรทัดที่ 48: วาดภาพกรอบปิดล้อมใบหน้า

บรรทัดที่ 50: จัดเรียงข้อมูลใบหน้า

บรรทัดที่ 52: ตรวจสอบตัวแปร  is_enrolling ว่ามีการสั่งให้ Enroll หรือไม่

บรรทัดที่ 54: Enroll ใบหน้า โดย Function  enroll_face จะ Return จำนวนครั้ง ที่เหลือที่ต้อง Enroll

บรรทัดที่ 55: เขียนข้อความแสดงสถานะการ Enroll ลงบนภาพในตัวแปร rgb888 

id_list.tail = หมายเลข ID ของใบหน้าที่กำลัง Enroll

ENROLL_CONFIRM_TIMES – left_sample_face = จำนวนครั้งที่กำลัง Enroll ใบหน้า

บรรทัดที่ 56: ส่งข้อความแสดงสถานะการ Enroll ออกไปทาง Serial เพื่อแสดงบน Program  JPG Serial Streaming

บรรทัดที่ 57: ตรวจสอบ ว่า Enroll เสร็จแล้วหรือไม่ จากค่าในตัวแปร  left_sample_face จะมีค่าเท่ากับ 0

บรรทัดที่ 58: ให้ is_enrolling = false ยกเลิก Enroll

บรรทัดที่ 59: กำหนดตำแหน่ง x,y จุดเริ่มต้น สำหรับเขียนข้อความลงในภาพ

บรรทัดที่ 60: เขียนข้อความ แสดงหมายเลข ID ที่ Enroll เสร็จแล้วลงในภาพ

บรรทัดที่ 62: ตรวจสอบ ค่าในตัวแปร  left_sample_face ถ้าหากมีค่า เท่ากับ – 2 ซึ่งหมายถึงการ Enroll ผิดพลาดหรือไม่

บรรทัดที่ 71: เปรียบเทียบใบหน้าที่ตรวจจับได้ว่าตรงกับ ใบหน้าใดใน id_list หรือไม่ และ Return หมายเลข ID ของใบหน้าที่ตรงกับใบหน้าใน id_list มาเก็บในตัวแปร matched_id

บรรทัดที่ 72: ถ้าหาก matched_id มีค่า มากกว่าหรือเท่ากับ 0 หมายถึงมีใบหน้าที่ตรงกับใบหน้าใน id_list 

บรรทัดที่ 74-75: แสดงข้อความ หมายเลข ID ของใบหน้าที่ตรงกับใบหน้าใน id_list

บรรทัดที่ 79-80: แสดงข้อความ “Intruder Alert!” เนื่องจากไม่พบใบหน้าที่ตรงกับใบหน้าใน id_list

บรรทัดที่ 84-85: คืนค่า Memory

บรรทัดที่ 90 : แปลงภาพ ในรูปแบบ RGB888 ในตัวแปร rgb888 ให้เป็น JPG 

บรรทัดที่ 91 : ส่งรูปภาพ JPG ออกไปทาง Serial เพื่อแสดงภาพบน Program JPG Serial Streaming

บรรทัดที่ 92-93: คืนค่า Memory

ในการทดลองข้างต้น จะเห็นว่าเราเก็บค่า  face_id_list  id_list เอาไว้ใน RAM เมื่อเราไม่ได้จ่ายไฟให้กับ ESPIno32CAM ก็จะทำให้ ใบหน้าใน  id_list หายไป 

ทางเราจึงได้ทำ ตัวอย่าง ที่ชื่อ Ex_Face_Recognition_save_to_SPIFFS
สามารถดูตัวอย่างได้ที่
File > Example >ESPIno32CAM > Ex_Face_Recognition_save_to_SPIFFS

ในตัวอย่างนี้จะใช้วิธีการ Save Data ที่อยู่ภายใน face_id_list  ลงไป บน Flash Memory โดยใช้ SPIFFS สร้างเป็น File ขึ้นมา

#include "ESPino32CAM.h"
ESPino32CAM cam;
#define BUTTON 0
static mtmn_config_t mtmn_config = {0};
bool is_enrolling = false;
static face_id_list id_list = {0};
void setup() {
  Serial.begin(2000000);
  cam.printDebug("\r\nESPino32CAM");
  if (cam.init() != ESP_OK)
  {
    cam.printDebug(F("Fail"));
    return;
  }
  sensor_t *s = cam.sensor();
  s->set_framesize(s, FRAMESIZE_QVGA);
  mtmn_config.min_face = 80;
  mtmn_config.pyramid = 0.7;
  mtmn_config.p_threshold.score = 0.6;
  mtmn_config.p_threshold.nms = 0.7;
  mtmn_config.r_threshold.score = 0.7;
  mtmn_config.r_threshold.nms = 0.7;
  mtmn_config.r_threshold.candidate_number = 4;
  mtmn_config.o_threshold.score = 0.7;
  mtmn_config.o_threshold.nms = 0.4;
  mtmn_config.o_threshold.candidate_number = 1;
  cam.faceIDInit(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
  if (cam.faceIDInitFlash())
  {
    //cam.deleteFaceIDinFlash();   //Delete face id list
    if (!cam.readFaceIDFromFlash(&id_list))
      cam.printDebug("Load face id list fail");
  }
  else
    cam.printDebug("SPIFFS Fail");
}
void loop()
{
  if (digitalRead(BUTTON) == 0)
  {
    is_enrolling = true;
  }
  camera_fb_t *fb = cam.capture();
  if (!fb)
  {
    cam.printDebug("Camera capture failed\r\n");
    return;
  }
  dl_matrix3du_t *rgb888;
  if (cam.jpg2rgb(fb, &rgb888))
  {
    cam.clearMemory(fb);
    box_array_t *net_boxes = cam.faceDetect(rgb888, &mtmn_config);
    if (net_boxes)
    {
      cam.drawFaceBoxes(rgb888, net_boxes, true);
      dl_matrix3du_t *aligned_face;
      if ( cam.alignFace(rgb888, net_boxes, &aligned_face) == ESP_OK)
      {
        if (is_enrolling)
        {
          int8_t left_sample_face = enroll_face(&id_list, aligned_face);
          cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW, "ID[%u] Sample[%u]", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
          cam.printDebug("ID " + String(id_list.tail) + "Sample " + String(ENROLL_CONFIRM_TIMES - left_sample_face));
          if (left_sample_face == 0) {
            is_enrolling = false;
            cam.rgbGoto(10, 50);
            cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW, "Enrolled Face ID: %d\n", id_list.tail);
            cam.writeFaceIDToFlash(&id_list);
          }
          if (left_sample_face == -2) {
            is_enrolling = false;
            cam.rgbGoto(10, 50);
            cam.rgbPrintf(rgb888, FACE_COLOR_YELLOW, "Enrolled Fail\n", id_list.tail);
          }
          cam.rgbGoto(0, 0);
        }
        else
        {
          int matched_id = cam.recognizeFace(&id_list, aligned_face);
          if (matched_id >= 0)
          {
            cam.printDebug("Match Face ID: " + String(matched_id));
            cam.rgbPrintf(rgb888, FACE_COLOR_GREEN, "Hello Subject %u", matched_id);
          }
          else
          {
            cam.rgbPrintf(rgb888, FACE_COLOR_RED, "Intruder Alert!");
            cam.printDebug("Intruder Alert!");
          }
        }
      }
      cam.clearMemory(aligned_face);
      cam.clearMemory(net_boxes);
    }
  }
  uint8_t *jpgout;
  size_t  jpglen;
  if (cam.rgb2jpg(rgb888, &jpgout, &jpglen))
    Serial.write(jpgout, jpglen);
  cam.clearMemory(jpgout);
  cam.clearMemory(rgb888);
}

การทำงานโดยหลักๆแล้วจะไม่แตกต่างจากการใช้งานเดิม เพียงแต่เพิ่ม Code ในส่วนการ อ่าน , เขียน ข้อมูลใบหน้าลงไปยัง SPIFFS เท่านั้น โดยจะมีส่วนที่แตกต่างจากของเดิม ดังนี้ 

บรรทัดที่ 27: initial face_id_list โดยกำหนด จำนวนใบหน้าสูงสุดที่เก็บได้ และ จำนวนครั้งที่ Enroll ต่อหนึ่งใบหน้า 

บรรทัดที่ 28: faceIDInitFlash() เพื่อ initial เริ่มต้นใช้งาน SPIFFS ถ้าหากสามารถ initial สำเร็จ จะ return true

บรรทัดที่ 30: deleteFaceIDinFlash() ตัวอย่างคำสั่งหากต้องการลบ File ข้อมูลใบหน้าออกจาก SPIFFS

บรรทัดที่ 31: อ่านค่า File ข้อมูลใบหน้าที่ถูกเก็บอยู่ใน SPIFFS แล้ว Copy มาเก็บในตัวแปร face_id_list ถ้าหากอ่านค่าได้สำเร็จจะ return true

บรรทัดที่ 69: ทุกครั้งที่ Enroll สำเร็จ ใช้ Function writeFaceIDToFlash เก็บข้อมูลของใบหน้าลงไปยัง SPIFFS