Dự án ESP32 Webcam gọi Video Call Skype / Zoom hoặc làm Camera giám sát

Trong dự án ESP32 Webcam này, IoTZone sẽ hướng dẫn bạn cách sử dụng ESP32 Cam để làm Webcam trong các cuộc gọi video trên Skype/Zoom hoặc thậm chí làm Camera giám sát trong nhà. Cùng theo dõi nhé!

ESP32 Webcam gọi Video Call Skype/Zoom

Với demo dự án này, bạn có thể ứng dụng trong Linux OS và sử dụng thiết bị v4l2loopback. Bạn có thể sử dụng với tất cả các dự án Deep Learning trên ESP32 Camera.

Bạn có thể sử dụng filter trước khi chuyển hình ảnh sang ứng dụng Zoom/Skype. Ví dụ, mình sử dụng filter 2 dấu chấm tròn đỏ để che mắt trong demo dự án này:

Hình ảnh gốc từ ESP32 Webcam
Hình ảnh gốc từ ESP32 Webcam
Hình ảnh từ ESP32 Webcam đã dùng filter
Hình ảnh từ ESP32 Webcam đã dùng filter

Chuẩn bị phần cứng & phần mềm

Về phần cứng, bạn cần:

  • Máy tính chạy trên Linux
  • ESP32 Camera

Về phần mềm, bạn cần cài sẵn các phần mềm sau:

  • sudo apt install python3-pip
  • pip3 install git+https://github.com/antmicro/python3-v4l2 
  • pip3 install –user cv2 
  • pip3 install –user numpy
  • git clone https://github.com/umlaeute/v4l2loopback.git (sau đó thực hiện && sudo make install)

Bạn có thể gõ Teminal sau để tạo thiết bị ảo /dev/video6:

sudo modprobe v4l2loopback video_nr=6

Bạn có thể dùng dòng lệnh sau để kiểm tra:

ls /dev/video*

Các ứng dụng Zoom/Skype sẽ đọc thiết bị dev/video6 mà bạn đã tạo để lấy source hình ảnh. Dưới đây là hình minh họa cách giao tiếp giữa ESP32 Webcam và PC (có tên là TCP Server / Client):

ESP32 WebCam TCP Server >> PC TCP Client /dev/video >> Skype /dev/video

Nạp code

Bạn nạp code sau vào mạch ESP32:

#include "esp_camera.h"
#include <WiFi.h>

#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

WiFiServer server(8088);
bool connected = false;
WiFiClient live_client;

void configCamera(){
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  config.frame_size = FRAMESIZE_QVGA;
  config.jpeg_quality = 9;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
}

//continue sending camera frame
void liveCam(WiFiClient &client){
  //capture a frame
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
      Serial.println("Frame buffer could not be acquired");
      return;
  }
  client.write(fb->buf, fb->len);
  client.flush();
  client.print("\r\n");
  client.flush();
  //return the frame buffer back to be reused
  esp_camera_fb_return(fb);
}

void setup() {
  Serial.begin(115200);
  WiFi.begin("I3.41", "xxx");
  Serial.println("");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  String IP = WiFi.localIP().toString();
  Serial.println("IP address: " + IP);
  server.begin();
  configCamera();
}

void loop() {
  WiFiClient client = server.available(); 
  if (client.connected()) {
    live_client = client;
    connected = true;
  }
  if(live_client.connected() == false) {
    connected = false;
  }
  if(connected) {
    liveCam(live_client);
  }
}

Bạn hãy nạp code trên vào mạch ESP32 và kiểm tra kết quả nhé!

ESP32 WebCam phát video trực tuyến – làm Camera giám sát

Trong dự án này, bạn có thể sử dụng ESP32 Camera làm một Web Server phát video trực tuyến, và bạn có thể truy cập vào xem video đó bằng bất kỳ thiết bị nào trong mạng của mình.

Chuẩn bị phần cứng & phần mềm

Về phần cứng, bạn cần chuẩn bị:

  • ESP32 Camera
  • FTDI programmer
  • Dây jumper female to female
  • Nguồn 5V cho ESP32 Webcam

Về phần mềm, bạn có thể cài đặt tiện ích ESP32 trong phần mềm Arduino IDE trước nhé!

Chương trình code mẫu

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h" //disable brownout problems
#include "soc/rtc_cntl_reg.h"  //disable brownout problems
#include "esp_http_server.h"

//Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

#define PART_BOUNDARY "123456789000000000000987654321"

// This project was tested with the AI Thinker Model, M5STACK PSRAM Model and M5STACK WITHOUT PSRAM
#define CAMERA_MODEL_AI_THINKER
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WITHOUT_PSRAM

// Not tested with this model
//#define CAMERA_MODEL_WROVER_KIT

#if defined(CAMERA_MODEL_WROVER_KIT)
  #define PWDN_GPIO_NUM    -1
  #define RESET_GPIO_NUM   -1
  #define XCLK_GPIO_NUM    21
  #define SIOD_GPIO_NUM    26
  #define SIOC_GPIO_NUM    27
  
  #define Y9_GPIO_NUM      35
  #define Y8_GPIO_NUM      34
  #define Y7_GPIO_NUM      39
  #define Y6_GPIO_NUM      36
  #define Y5_GPIO_NUM      19
  #define Y4_GPIO_NUM      18
  #define Y3_GPIO_NUM       5
  #define Y2_GPIO_NUM       4
  #define VSYNC_GPIO_NUM   25
  #define HREF_GPIO_NUM    23
  #define PCLK_GPIO_NUM    22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       32
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_M5STACK_WITHOUT_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       17
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_AI_THINKER)
  #define PWDN_GPIO_NUM     32
  #define RESET_GPIO_NUM    -1
  #define XCLK_GPIO_NUM      0
  #define SIOD_GPIO_NUM     26
  #define SIOC_GPIO_NUM     27
  
  #define Y9_GPIO_NUM       35
  #define Y8_GPIO_NUM       34
  #define Y7_GPIO_NUM       39
  #define Y6_GPIO_NUM       36
  #define Y5_GPIO_NUM       21
  #define Y4_GPIO_NUM       19
  #define Y3_GPIO_NUM       18
  #define Y2_GPIO_NUM        5
  #define VSYNC_GPIO_NUM    25
  #define HREF_GPIO_NUM     23
  #define PCLK_GPIO_NUM     22
#else
  #error "Camera model not selected"
#endif

static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t stream_httpd = NULL;

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
    //Serial.printf("MJPG: %uB\n",(uint32_t)(_jpg_buf_len));
  }
  return res;
}

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;

  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  
  //Serial.printf("Starting web server on port: '%d'\n", config.server_port);
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &index_uri);
  }
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
 
  Serial.begin(115200);
  Serial.setDebugOutput(false);
  
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG; 
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  // Wi-Fi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.print(WiFi.localIP());
  
  // Start streaming web server
  startCameraServer();
}

void loop() {
  delay(1);
}

Trước khi nạp code, bạn nhớ thay đổi thông tin xác thực mạng WiFi của mình nhé, tại dòng code sau:

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Sau đó, bạn chọn đúng module Camera của mình. Hiện tại, mình đang dùng mô hình AI_Thinker:

ESP32 Webcam làm Camera giám sát

Nếu bạn đang dùng cùng module máy ảnh, thì bạn không cần thay đổi gì về code. Nếu không, bạn cần chỉnh sửa đoạn code sau để phù hợp:

#define CAMERA_MODEL_AI_THINKER

Kết nối phần cứng và nạp code

Kết nối mạch ESP32 Webcam với FTDI theo sơ đồ sau:

Sơ đồ kết nối ESP32 Webcam với FTDI
Sơ đồ kết nối ESP32 Webcam với FTDI

Lưu ý rằng bạn cần phải kết nối GPIO 0 với chân GND, để có thể upload code lên. Sơ đồ kết nối như sau:

ESP32 CameraFTDI
GNDGND
5VVCC (5V)
U0RTX
U0TRX
GPIO 0GND

Để upload code, bạn làm như sau:

  1. Chọn Tools >> Board và click vào AI-Thinker ESP32 CAM
  2. Vào Tools >> Port và chọn cổng COM đã kết nối với ESP32
  3. Click vào nút upload code
  4. Trên cửa sổ Serial xuất hiện các dấu chấm, bạn nhấn vào nút reset (RST) trên mạch ESP32 Webcam

Sau đó, bạn chờ vài giây để upload code thành công.

Lấy địa chỉ IP

Sau khi upload code, bạn phải ngắt kết nối GPIO 0 từ GND và mở Serial Monitor ở tốc độ 115200, nhấn nút RST trên ESP32 Webcam.

Sau đó, địa chỉ IP ESP32 được in trên Serial Monitor:

Lấy địa chỉ IP của ESP32 Webcam
Lấy địa chỉ IP của ESP32 Webcam

Truy cập vào Web Server truyền phát video

Bây giờ, bạn có thể dùng laptop hoặc điện thoại, máy tính bảng để truy cập vào xem video đang được truyền phát trực tiếp:

  • Mở trình duyệt Internet
  • Nhập địa chỉ ESP32 Webcam vào thanh tìm kiếm
  • Nhấn OK và chờ loading, một trang web có luồng video từ ESP32 Camera sẽ hiển thị:
Truy cập vào xem video trực tuyến từ ESP32 Webcam
Truy cập vào xem video trực tuyến từ ESP32 Webcam

Lời kết

Đây là hướng dẫn cách làm dự án ESP32 Webcam cơ bản, bạn có thể nâng cấp nó lên thành những dự án nâng cao hơn, chẳng hạn như trợ lý ảo trong Smart home, dùng thêm mô hình máy ảnh và gắn ESP32 Webcam vào bên trong để làm thành một camera giám sát hiện đại:

Cách dùng ESP32 Webcam nâng cao hơn

Bạn hãy thử nhé! Chúc các bạn thành công!

IoTZone – Chuyên cung cấp thiết bị điện tử & tài liệu cho Makers

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *