สร้างระบบตรวจจับวัตถุ (Object Detection) ด้วย ESP32-CAM แบบไม่ง้อ Dataset!

Object Detection Using ESP32-Cam
ระบบตรวจจับวัตถุแบบเรียลไทม์ด้วยกล้อง ESP32-CAM และ CircuitDigest Cloud

สวัสดีชาว Maker และสาย AI ทุกคนครับ! 💡 ถ้าพูดถึงการทำระบบตรวจจับวัตถุ (Object Detection) หลายคนคงนึกถึงการเทรนโมเดล AI ที่แสนจะยุ่งยาก ต้องหา Dataset มานั่งตีกรอบ (Bounding Box) และต้องใช้บอร์ดคอมพิวเตอร์แรงๆ ใช่ไหมครับ?

แต่วันนี้เราจะพาคุณไปทางลัด! ด้วยการใช้โมดูลกล้องจิ๋วยอดฮิตอย่าง ESP32-CAM ร่วมกับ CircuitDigest Cloud API ที่จะเปลี่ยนภาพถ่ายให้กลายเป็นผลลัพธ์การตรวจจับวัตถุพร้อมค่าความแม่นยำ (Confidence) ส่งกลับมาที่ Serial Monitor ได้ทันที โดยไม่ต้องเทรนโมเดลเอง ไม่ต้องใช้ Edge Impulse และไม่ต้องมี Dataset!

Object Detection คืออะไร? 🤔

มันคือเทคนิคด้าน Computer Vision ที่คอมพิวเตอร์สามารถระบุได้ว่าในภาพนั้นมีวัตถุอะไรอยู่บ้าง (เช่น โทรศัพท์, แก้วน้ำ, รถยนต์, สุนัข) โดยระบบจะวาดกรอบสี่เหลี่ยมล้อมรอบวัตถุพร้อมระบุเปอร์เซ็นต์ความมั่นใจ สมัยก่อนเทคนิคนี้กินทรัพยากรสูงมาก แต่ด้วยพลังของ Cloud API เราจึงสามารถจับมันยัดลงบอร์ดไมโครคอนโทรลเลอร์ตัวเล็กๆ ได้สบายครับ

ระบบนี้ทำงานยังไง? ⚙️

  1. เมื่อเรากด Push Button บอร์ด ESP32-CAM จะทำการถ่ายภาพ
  2. ส่งภาพนั้นผ่านอินเทอร์เน็ต (Wi-Fi) ไปยังเซิร์ฟเวอร์ของ CircuitDigest Cloud
  3. เซิร์ฟเวอร์จะประมวลผลด้วย AI Object Detection (รองรับกว่า 75+ คลาส)
  4. ส่งผลลัพธ์กลับมาในรูปแบบข้อความ (เช่น พบมือถือ 1 เครื่อง, ความแม่นยำ 95%) และแสดงผลผ่าน Serial Monitor ของ Arduino IDE ทันที!

💡 Maker's Tip สำหรับมือใหม่: บอร์ด ESP32-CAM รุ่นมาตรฐานจะไม่มีพอร์ต USB ในตัวนะครับ คุณจะต้องใช้ FTDI Adapter (USB-to-Serial) ในการอัปโหลดโค้ด (อย่าลืมจั๊มพิน GPIO0 ลง GND ตอนอัปโหลดด้วยล่ะ!)

หากเพื่อนๆ กำลังมองหา บอร์ด ESP32-CAM, FTDI Adapter, ปุ่มกด (Push Button) หรือสายจัมเปอร์ แวะมาช้อปของแท้คุณภาพดีได้ที่ Globalbyte เลยครับ! ของครบพร้อมทำโปรเจกต์ทันที

วิดีโอสาธิตการทำงาน (Demo) 🎬

อุปกรณ์และผังวงจร (Circuit Diagram) 🔌

Circuit Diagram of ESP32-CAM Hardware Connection

การต่อวงจรนั้นง่ายมากครับ เพียงแค่ต่อ Push Button เข้ากับขา GPIO13 ของ ESP32-CAM เพื่อใช้เป็นปุ่มกดสั่งถ่ายภาพ และจ่ายไฟ 5V ให้ระบบอย่างเพียงพอ

ขั้นตอนการตั้งค่า CircuitDigest Cloud ☁️

  1. สมัครสมาชิก: เข้าไปที่ CircuitDigest Cloud สร้างแอคเคานต์และไปที่โหมด Object Detection
  2. ตั้งค่า Confidence Threshold: ปรับเปอร์เซ็นต์ความมั่นใจขั้นต่ำที่ยอมรับได้ (เช่น 0.2 คือ 20%) ถ้ายิ่งตั้งสูง AI ก็จะยิ่งกรองเฉพาะวัตถุที่มันมั่นใจจริงๆ
  3. เลือก Target Classes: เลือกว่าจะให้ตรวจจับอะไรบ้าง (มีให้เลือกมากกว่า 75 ชนิด)
  4. รับโค้ดและ API Key: เลือกบอร์ดเป็น ESP32-CAM ระบบจะ Generate โค้ดสำหรับ Arduino IDE มาให้พร้อม API Key นำไปวางในโปรแกรมและแฟลชลงบอร์ดได้เลยครับ!
API Settings Serial Monitor Output

เจาะลึกโค้ดการทำงาน (Code Explanation) 💻

ตัวโค้ดที่ระบบสร้างให้จะทำงานร่วมกับไลบรารี esp_camera.h โดยมีหัวใจหลักคือการสร้าง MJPEG Live Preview ให้เราส่องกล้องผ่าน Web Browser ก่อนถ่าย และเมื่อกดปุ่ม ระบบจะแพ็กภาพแบบ Multipart Form-Data ส่ง HTTPS POST Request ไปยัง API ของเซิร์ฟเวอร์ครับ

ในโค้ดมีการใช้เทคนิค Semaphore (Mutex) ด้วย เพื่อป้องกันไม่ให้การสตรีมภาพ (Core 0) ไปตีกับการส่งภาพขึ้น Cloud (Core 1) ทำให้ระบบไม่แครชครับ

ปัญหาพบบ่อยและการแก้ไข (Troubleshooting) 🛠️

  • ESP32-CAM รีสตาร์ทเองบ่อย (Brownout): เกิดจากไฟเลี้ยงไม่พอครับ ตอนต่อ Wi-Fi และใช้กล้อง บอร์ดอาจดึงกระแสไฟทะลุ 500mA ควรใช้แหล่งจ่ายไฟ 5V/1A แบบแยก หรือเปลี่ยนสาย USB ให้สั้นและคุณภาพดีขึ้น
  • ภาพเบลอ ตรวจจับไม่เจอ: เลนส์ของ ESP32-CAM หมุนปรับโฟกัสด้วยมือได้นะครับ ให้ลองหมุนช้าๆ จนภาพชัด และควรใช้งานในที่แสงสว่างเพียงพอ
  • Camera init failed: เช็คว่าเลือกโมเดลกล้องเป็น AI Thinker ESP32-CAM ใน Arduino IDE หรือยัง และสายไฟต่างๆ แน่นดีไหม

⚠️ อยากรู้ลึกกว่านี้? ขอเชียร์ให้ตามไปอ่านต้นฉบับเลย!

เนื่องจากการตั้งค่า API บนหน้าเว็บคลาวด์ การจัดการบอร์ดใน Arduino IDE และการแก้ปัญหาเฉพาะหน้า (Troubleshooting) มีรายละเอียดเชิงลึกค่อนข้างเยอะ เพื่อให้เพื่อนๆ ทำตามได้ง่ายและไม่พลาดสเต็ปสำคัญ เราขอแนะนำให้เข้าไปศึกษาขั้นตอนแบบเจาะลึก Step-by-Step จากบทความต้นฉบับ ครับ!

👉 คลิกอ่านคู่มือฉบับเต็มบน CircuitDigest ได้ที่นี่

ObjectDetection_ESP32CAM.ino
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include <WebServer.h>

const char* ssid       = "yourssidname";
const char* password   = "yourwifipassword";
const char* serverName = "www.circuitdigest.cloud";
const char* serverPath = "/api/v1/object-detection/detect";
const int   serverPort = 443;
const char* apiKey     = "yourapikey";
const char* classes    = "[]";
const char* confidence = "0.2";

#define TRIGGER_BTN 13
unsigned long lastTriggerTime = 0;
const unsigned long debounceDelay = 500;

#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

WiFiClientSecure client;
WebServer previewServer(80);

// ── Thread-safe camera lock ────────────────────────────────────────────────
SemaphoreHandle_t camMutex = NULL;
inline bool lockCam(TickType_t wait = portMAX_DELAY) {
 return xSemaphoreTake(camMutex, wait) == pdTRUE;
}
inline void unlockCam() { xSemaphoreGive(camMutex); }

// ── Camera init ───────────────────────────────────────────────────────────
void initCamera() {
 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_SVGA;
 config.jpeg_quality = 8;
 config.fb_count     = 2;
 
 esp_err_t err = esp_camera_init(&config);
 if (err != ESP_OK) {
   Serial.printf("Camera init failed: 0x%x\n", err);
   return;
 }
 
 sensor_t* s = esp_camera_sensor_get();
 s->set_brightness(s, 1);
 s->set_contrast(s, 2);
 s->set_saturation(s, 0);
 s->set_whitebal(s, 1);
 s->set_exposure_ctrl(s, 1);
 s->set_gain_ctrl(s, 1);
 s->set_aec2(s, 1);
 s->set_ae_level(s, 1);
 Serial.println("Camera initialized.");
}

// ── Root page (with auto-refresh fallback JS) ─────────────────────────────
void handleRoot() {
 previewServer.send(200, "text/html",
   ""
   ""
   "
"
   ""
   ""
   "

Camera Preview

"
   "

Press the button to detect objects

"
   ""
   ""
 );
}

// ── MJPEG stream handler ──────────────────────────────────────────────────
void handleStream() {
 WiFiClient streamClient = previewServer.client();
 streamClient.println("HTTP/1.1 200 OK");
 streamClient.println("Content-Type: multipart/x-mixed-replace; boundary=frame");
 streamClient.println("Cache-Control: no-cache");
 streamClient.println("Connection: close");
 streamClient.println();
 
 const TickType_t lockWait  = pdMS_TO_TICKS(200);  
 const uint32_t   frameGap  = 50;                  
 const uint32_t   yieldGap  = 80;                  
 const uint32_t   watchdog  = 8000;                
 uint32_t lastFrame = millis();
 
 while (streamClient.connected()) {
   if (millis() - lastFrame > watchdog) {
     Serial.println("[stream] watchdog timeout, dropping client");
     break;
   }
   
   if (!lockCam(lockWait)) {
     vTaskDelay(pdMS_TO_TICKS(yieldGap));
     continue;
   }
   
   camera_fb_t* discard = esp_camera_fb_get();
   if (discard) esp_camera_fb_return(discard);
   camera_fb_t* fb = esp_camera_fb_get();
   unlockCam();
   
   if (!fb) {
     vTaskDelay(pdMS_TO_TICKS(10));
     continue;
   }
   
   streamClient.print("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: ");
   streamClient.print(fb->len);
   streamClient.print("\r\n\r\n");
   size_t written = streamClient.write(fb->buf, fb->len);
   streamClient.print("\r\n");
   esp_camera_fb_return(fb);
   
   if (written == 0) {
     Serial.println("[stream] write failed, dropping client");
     break;
   }
   
   lastFrame = millis();
   vTaskDelay(pdMS_TO_TICKS(frameGap));
 }
 streamClient.stop();
 Serial.println("[stream] client disconnected");
}

// ── Web server task (Core 0) ──────────────────────────────────────────────
void webServerTask(void* param) {
 while (true) {
   previewServer.handleClient();
   vTaskDelay(1);
 }
}

// ── API call ──────────────────────────────────────────────────────────────
String sendImageToAPI(camera_fb_t* fb) {
 if (!client.connect(serverName, serverPort)) {
   return "Connection failed";
 }
 
 String boundary    = "----ESP32Boundary";
 String head        = "--" + boundary + "\r\n"
                      "Content-Disposition: form-data; name=\"imageFile\"; filename=\"photo.jpg\"\r\n"
                      "Content-Type: image/jpeg\r\n\r\n";
 String classesPart = "\r\n--" + boundary + "\r\n"
                      "Content-Disposition: form-data; name=\"classes\"\r\n\r\n"
                    + String(classes) + "\r\n";
 String confPart    = "--" + boundary + "\r\n"
                      "Content-Disposition: form-data; name=\"confidence\"\r\n\r\n"
                    + String(confidence) + "\r\n";
 String tail        = "--" + boundary + "--\r\n";
 
 int contentLen = head.length() + fb->len
                + classesPart.length() + confPart.length() + tail.length();
                
 client.println("POST " + String(serverPath) + " HTTP/1.1");
 client.println("Host: " + String(serverName));
 client.println("X-API-Key: " + String(apiKey));
 client.println("Content-Type: multipart/form-data; boundary=" + boundary);
 client.println("Content-Length: " + String(contentLen));
 client.println("Connection: close");
 client.println();
 client.print(head);
 client.write(fb->buf, fb->len);
 client.print(classesPart);
 client.print(confPart);
 client.print(tail);
 
 uint32_t timeout = millis();
 while (client.available() == 0) {
   if (millis() - timeout > 15000) {
     client.stop();
     return "Timeout";
   }
   delay(10);
 }
 
 String response = "";
 while (client.available()) {
   response += (char)client.read();
 }
 client.stop();
 
 int jsonStart = response.indexOf("\r\n\r\n");
 return (jsonStart != -1) ? response.substring(jsonStart + 4) : response;
}

// ── Setup ─────────────────────────────────────────────────────────────────
void setup() {
 Serial.begin(115200);
 pinMode(TRIGGER_BTN, INPUT_PULLUP);
 camMutex = xSemaphoreCreateMutex();
 
 initCamera();
 WiFi.begin(ssid, password);
 Serial.print("Connecting to WiFi");
 while (WiFi.status() != WL_CONNECTED) {
   delay(500);
   Serial.print(".");
 }
 Serial.println("\nWiFi connected: " + WiFi.localIP().toString());
 Serial.println("Preview: http://" + WiFi.localIP().toString());
 
 client.setInsecure();
 previewServer.on("/",        handleRoot);
 previewServer.on("/stream", handleStream);
 previewServer.begin();
 
 xTaskCreatePinnedToCore(webServerTask, "webTask", 8192, NULL, 1, NULL, 0);
}

// ── Main loop (Core 1) ────────────────────────────────────────────────────
void loop() {
 if (digitalRead(TRIGGER_BTN) != LOW) return;
 uint32_t now = millis();
 if (now - lastTriggerTime < debounceDelay) return;
 lastTriggerTime = now;
 
 Serial.println("Button pressed — capturing...");
 
 if (WiFi.status() != WL_CONNECTED) {
   Serial.println("WiFi lost, reconnecting...");
   WiFi.begin(ssid, password);
   for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) {
     delay(500); Serial.print(".");
   }
   if (WiFi.status() != WL_CONNECTED) {
     Serial.println("Reconnect failed!");
     return;
   }
   Serial.println("WiFi back.");
 }
 
 if (!lockCam(pdMS_TO_TICKS(3000))) {
   Serial.println("Could not lock camera — try again");
   return;
 }
 
 sensor_t* s = esp_camera_sensor_get();
 s->set_framesize(s, FRAMESIZE_VGA);
 s->set_quality(s, 10);
 delay(150);
 
 camera_fb_t* warmup = esp_camera_fb_get();
 if (warmup) esp_camera_fb_return(warmup);
 delay(80);
 
 camera_fb_t* fb = esp_camera_fb_get();
 if (!fb) {
   Serial.println("Capture failed");
   s->set_framesize(s, FRAMESIZE_SVGA);
   s->set_quality(s, 8);
   unlockCam();
   return;
 }
 
 Serial.printf("Captured %u bytes — sending to API...\n", fb->len);
 String result = sendImageToAPI(fb);
 esp_camera_fb_return(fb);
 
 s->set_framesize(s, FRAMESIZE_SVGA);
 s->set_quality(s, 8);
 unlockCam();
 
 Serial.println("Response: " + result);
 int idx = result.indexOf("\"detection_count\":");
 if (idx != -1) {
   int start = result.indexOf(':', idx) + 1;
   int end   = result.indexOf(',', start);
   String count = result.substring(start, end);
   count.trim();
   Serial.println("Objects detected: " + count);
 } else {
   Serial.println("No detection data in response.");
 }
}

อ้างอิงและเรียบเรียงข้อมูลจาก: Globalbyteshop Blog

แหล่งที่มาบทความต้นฉบับ: CircuitDigest - ESP32-CAM Object Detection

ศึกษาเพิ่มเติม: หากคุณอยากลองเทรนโมเดลเองแบบไม่พึ่งคลาวด์ แนะนำให้อ่านเรื่อง การใช้ ESP32-CAM ร่วมกับ Edge Impulse ครับ

*คำเตือน: เนื้อหานี้เป็นการสรุปและแปลมาจากบทความ DIY โปรเจกต์ต้นฉบับภาษาอังกฤษ โค้ดและขั้นตอนการเรียกใช้ API อาจมีการเปลี่ยนแปลงได้ตามนโยบายของแพลตฟอร์มคลาวด์ แนะนำให้ตรวจสอบรายละเอียดการตั้งค่าและรับ API Key ด้วยตนเองจากเว็บไซต์ต้นฉบับอีกครั้งก่อนนำไปใช้งานจริงครับ

 

แท็ก


Blog posts

เข้าสู่ระบบ

ลืมรหัสผ่านใช่ไหม?

ยังไม่มีบัญชีใช่ไหม?
สร้างบัญชี