AIoT Object Detection ด้วย ESP32-CAM และ YOLO-World

เอกสารประกอบการสอน

LAB: AIoT Object Detection ด้วย ESP32-CAM และ YOLO-World

การตรวจจับ Apple และ Mango เพื่อควบคุม LED แบบอัจฉริยะ

รายวิชา: การประยุกต์ใช้ปัญญาประดิษฐ์ในวิศวกรรมเครื่องจักกลเกษตร
ผู้สอน: ผศ.ดร.เฉลิมชัย หล่อวงศ์ตระกูล
หัวข้อ: Computer Vision, Object Detection และ AIoT
ระดับ: นักศึกษามหาวิทยาลัย
ระยะเวลาแนะนำ: 3–4 ชั่วโมง

1. ที่มาและความสำคัญ

ปัจจุบันเทคโนโลยีปัญญาประดิษฐ์ (Artificial Intelligence: AI) โดยเฉพาะ Computer Vision สามารถทำให้คอมพิวเตอร์หรืออุปกรณ์อิเล็กทรอนิกส์เข้าใจข้อมูลจากภาพได้ เช่น การตรวจจับวัตถุ การจำแนกประเภทของผลผลิตทางการเกษตร การตรวจสอบคุณภาพผลไม้ และการตรวจจับโรคหรือศัตรูพืช

ในงานด้านการเกษตร เทคโนโลยีดังกล่าวสามารถนำมาประยุกต์ใช้กับระบบ Smart Farm เพื่อช่วยลดการพึ่งพาการตรวจสอบด้วยมนุษย์ และทำให้ระบบสามารถตอบสนองต่อสิ่งที่ตรวจพบได้โดยอัตโนมัติ

การทดลองนี้จึงนำ ESP32-CAM ซึ่งเป็นไมโครคอนโทรลเลอร์ที่มีกล้องในตัว มาใช้เป็นอุปกรณ์สำหรับรับภาพ และใช้คอมพิวเตอร์เป็นหน่วยประมวลผล AI โดยใช้ YOLO-World สำหรับตรวจจับวัตถุจากภาพ

หาก AI ตรวจพบ Apple ระบบจะสั่งให้ LED สีแดงติด และหากตรวจพบ Mango ระบบจะสั่งให้ LED สีเขียวติด

การทดลองนี้ช่วยให้นักศึกษาเข้าใจแนวคิดสำคัญของระบบ AIoT (Artificial Intelligence of Things) ซึ่งเป็นการนำ AI มาทำงานร่วมกับอุปกรณ์ IoT เพื่อให้ระบบสามารถ รับข้อมูล วิเคราะห์ ตัดสินใจ สั่งงานอุปกรณ์ ได้อย่างอัตโนมัติ

2. วัตถุประสงค์

เมื่อจบการทดลอง นักศึกษาสามารถ:

  1. อธิบายหลักการทำงานของ Computer Vision และ Object Detection ได้
  2. อธิบายหลักการทำงานของ YOLO-World ในการตรวจจับวัตถุได้
  3. เชื่อมต่อ ESP32-CAM เข้ากับเครือข่าย Wi-Fi ได้
  4. ใช้ ESP32-CAM ถ่ายภาพและส่งภาพผ่าน HTTP ได้
  5. ใช้ Python และ YOLO-World วิเคราะห์ภาพจาก ESP32-CAM ได้
  6. เชื่อมโยงผลการตรวจจับ AI กับการควบคุมอุปกรณ์ IoT
  7. เขียนโปรแกรมควบคุม LED ตามผลการตรวจจับวัตถุได้
  8. อธิบายสถาปัตยกรรมของระบบ AIoT ได้

3. อุปกรณ์ที่ต้องเตรียม

3.1 Hardware

ลำดับอุปกรณ์จำนวนหน้าที่
1ESP32-CAM AI-Thinker1กล้องและ IoT Controller
2USB-to-TTL Programmer1Upload โปรแกรม
3LED สีแดง1แสดงผล Apple
4LED สีเขียว1แสดงผล Mango
5Resistor 220Ω2จำกัดกระแส LED
6Breadboard1ต่อวงจรทดลอง
7Jumper Wireหลายเส้นเชื่อมต่อวงจร
8Computer/Laptop1รัน YOLO-World
9Wi-Fi Router/Hotspot1เชื่อม ESP32-CAM กับ Computer

หมายเหตุ: การทดลองนี้ไม่ใช้ MicroSD เพราะ GPIO12 และ GPIO13 ถูกนำมาใช้ควบคุม LED

4. Software ที่ต้องเตรียม

ฝั่ง ESP32-CAM

  • Arduino IDE
  • ESP32 Board Package

ฝั่ง Computer

  • Python 3.x
  • Ultralytics
  • OpenCV
  • Requests
  • NumPy

ติดตั้งด้วยคำสั่ง:

pip install ultralytics opencv-python requests numpy

5. โครงสร้างระบบ

ระบบแบ่งออกเป็น 2 ฝั่ง

ฝั่งที่ 1: ESP32-CAM

ทำหน้าที่

  • ถ่ายภาพ
  • เชื่อม Wi-Fi
  • เปิด HTTP Server
  • ส่งภาพให้ Computer
  • รับคำสั่งควบคุม LED

ฝั่งที่ 2: Computer

ทำหน้าที่

  • Request ภาพจาก ESP32-CAM
  • ใช้ YOLO-World วิเคราะห์ภาพ
  • ตรวจว่าเป็น Apple หรือ Mango
  • ตรวจ Confidence
  • ส่งคำสั่งกลับไปยัง ESP32-CAM

6. การกำหนด IP Address

ตัวอย่างเครือข่าย:

Router

192.168.1.1

กำหนด:

Computer

192.168.1.100

ESP32-CAM

192.168.1.101

ดังนั้นใน Python:

ESP32_CAM_IP = “192.168.1.101”

ข้อควรจำ: IP 192.168.1.100 เป็น IP ของ Computer ไม่ใช่ IP ของ ESP32-CAM

ทั้ง Computer และ ESP32-CAM ต้องเชื่อมต่อ Wi-Fi Network เดียวกัน

7. การต่อวงจร GPIO

สำหรับ ESP32-CAM AI-Thinker กำหนดดังนี้

อุปกรณ์ESP32-CAM GPIO
🔴 Red LEDGPIO13
🟢 Green LEDGPIO12
LED GNDGND
LED แต่ละดวงผ่าน Resistor 220Ω

วงจร

             ESP32-CAM

GPIO13 ─── 220Ω ───► 🔴 RED LED ───► GND

GPIO12 ─── 220Ω ───► 🟢 GREEN LED ─► GND

เมื่อ:

digitalWrite(13, HIGH);

→ LED แดงติด

และ

digitalWrite(12, HIGH);

→ LED เขียวติด

8. ข้อควรระวังเกี่ยวกับ GPIO12 และ GPIO13

ESP32-CAM AI-Thinker มี GPIO หลายขาที่ถูกใช้งานร่วมกับกล้องและ MicroSD

ใน Lab นี้

GPIO12 → Green LED

GPIO13 → Red LED

ดังนั้น

ไม่ใช้ MicroSD Card

เพื่อหลีกเลี่ยงปัญหา GPIO ทำงานซ้ำซ้อนกัน

9. ทฤษฎีที่เกี่ยวข้อง

9.1 Artificial Intelligence

ในการทดลองนี้ AI ทำหน้าที่:

9.2 Computer Vision

9.3 Object Detection

9.4 YOLO

9.5 YOLO-World

YOLO-World เป็นแนวทางของ YOLO ที่รองรับ Open-Vocabulary Object Detection

จุดเด่นสำหรับการเรียนการสอนคือ สามารถกำหนดคำที่ต้องการตรวจจับ เช่น

CLASSES = [“apple”, “mango”]

ทำให้เราสามารถทดลอง Object Detection กับวัตถุที่สนใจได้อย่างรวดเร็ว โดยไม่จำเป็นต้องเริ่มต้นจากการสร้าง Dataset และ Train โมเดลใหม่ทุกครั้ง

9.6 Confidence Score

9.7 AIoT

10. ขั้นตอนการทำ LAB

ขั้นที่ 1: เตรียม ESP32-CAM

ติดตั้ง Arduino IDE และ ESP32 Board Package

เลือก Board ให้ตรงกับบอร์ดที่ใช้

สำหรับ ESP32-CAM AI-Thinker ให้เลือก:

AI Thinker ESP32-CAM

ขั้นที่ 2: ต่อ LED

GPIO13 → 220Ω → Red LED → GND

GPIO12 → 220Ω → Green LED → GND

ตรวจสอบขั้ว LED ให้ถูกต้อง

ขั้นที่ 3: แก้ Wi-Fi ใน ESP32-CAM

เปิด:

AppleMango_ESP32CAM.ino

แก้:

const char* WIFI_SSID     = “YOUR_WIFI_NAME”;

const char* WIFI_PASSWORD = “YOUR_WIFI_PASSWORD”;

เป็น Wi-Fi จริง เช่น:

const char* WIFI_SSID     = “SmartFarm”;

const char* WIFI_PASSWORD = “12345678”;

ขั้นที่ 4: Upload โปรแกรม

เชื่อม USB-to-TTL กับ ESP32-CAM

จากนั้น Upload:

AppleMango_ESP32CAM.ino

เปิด Serial Monitor:

115200 baud

หากสำเร็จจะเห็น:

[WiFi] Connected!

[WiFi] IP Address: 192.168.1.101

[CAMERA] Camera initialized.

[HTTP] Server started.

SYSTEM READY

ขั้นที่ 5: ทดสอบ ESP32-CAM

เปิด Browser บน Computer

ใส่:

http://192.168.1.101/

ควรพบหน้า:

ESP32-CAM Apple & Mango

RED ON

GREEN ON

LED OFF

Test Camera Capture

ขั้นที่ 6: ทดสอบ LED

กด:

RED ON

ต้องได้:

🔴 RED LED ON

กด:

GREEN ON

ต้องได้:

🟢 GREEN LED ON

ถ้าทำงานได้ แสดงว่า ESP32-CAM + GPIO + LED + Wi-Fi ทำงานถูกต้อง

ขั้นที่ 7: ทดสอบ Camera

เปิด:

http://192.168.1.101/capture

ถ้าถูกต้อง Browser จะแสดงภาพจากกล้อง

ขั้นที่ 8: เตรียม Computer

ติดตั้ง Library:

pip install ultralytics opencv-python requests numpy

เตรียมไฟล์:

AppleMango_YOLOWorld.py

และไฟล์โมเดล:

yolov8s-worldv2.pt

ขั้นที่ 9: กำหนด IP ของ ESP32-CAM

ใน Python:

ESP32_CAM_IP = “192.168.1.101”

ต้องเป็น IP ของ ESP32-CAM

ขั้นที่ 10: กำหนด Object ที่ต้องการตรวจจับ

ใน Python:

CLASSES = [“apple”, “mango”]

หมายความว่าเราสนใจเพียง 2 Class:

0 = apple

1 = mango

ขั้นที่ 11: Run YOLO-World

เปิด Terminal

พิมพ์ : python AppleMango_YOLOWorld.py

ขั้นที่ 12: ทดลองด้วย Apple

ขั้นที่ 13: ทดลองด้วยมะม่วง

11. โค้ดระบบฝั่งที่ 1: ESP32-CAM

ไฟล์:

AppleMango_ESP32CAM.ino

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

const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

#define RED_LED_PIN 13
#define GREEN_LED_PIN 12

#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

WebServer server(80);

void ledOff() {
  digitalWrite(RED_LED_PIN, LOW);
  digitalWrite(GREEN_LED_PIN, LOW);
}

void ledRed() {
  digitalWrite(RED_LED_PIN, HIGH);
  digitalWrite(GREEN_LED_PIN, LOW);
}

void ledGreen() {
  digitalWrite(RED_LED_PIN, LOW);
  digitalWrite(GREEN_LED_PIN, HIGH);
}

void handleCapture() {
  camera_fb_t* fb = esp_camera_fb_get();

  if (!fb) {
    server.send(500, "text/plain", "Camera capture failed");
    return;
  }

  server.send_P(
    200,
    "image/jpeg",
    (const char*)fb->buf,
    fb->len
  );

  esp_camera_fb_return(fb);
}

void handleLED() {

  if (!server.hasArg("color")) {
    server.send(400, "text/plain", "Missing color");
    return;
  }

  String color = server.arg("color");
  color.toLowerCase();

  if (color == "red") {
    ledRed();
    server.send(200, "text/plain", "RED LED ON");
  } else if (color == "green") {
    ledGreen();
    server.send(200, "text/plain", "GREEN LED ON");
  } else if (color == "off") {
    ledOff();
    server.send(200, "text/plain", "ALL LED OFF");
  } else {
    server.send(400, "text/plain", "Unknown color");
  }
}

bool 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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_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_VGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  return esp_camera_init(&config) == ESP_OK;
}

void setup() {

  Serial.begin(115200);

  pinMode(RED_LED_PIN, OUTPUT);
  pinMode(GREEN_LED_PIN, OUTPUT);

  ledOff();

  if (!initCamera()) {
    Serial.println("Camera initialization failed.");
    while (true) {
      delay(1000);
    }
  }

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  Serial.print("Connecting WiFi");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi Connected");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  server.on("/capture", HTTP_GET, handleCapture);
  server.on("/led", HTTP_GET, handleLED);

  server.begin();

  Serial.println("HTTP Server Started");
}

void loop() {
  server.handleClient();
}

โค้ดเต็มที่จัดเตรียมไว้ก่อนหน้านี้อยู่ในไฟล์ AppleMango_ESP32CAM.ino และมีหน้า Web สำหรับทดสอบ LED ด้วย

12. โค้ดระบบฝั่งที่ 2: Computer + YOLO-World

ไฟล์:

AppleMango_YOLOWorld.py

"""
AppleMango_YOLOWorld.py
ESP32-CAM + YOLO-World + Red/Green LED

Install:
    pip install ultralytics opencv-python requests

Run:
    python AppleMango_YOLOWorld.py

Change ESP32_CAM_IP to the IP address of your ESP32-CAM.

Expected ESP32-CAM HTTP endpoints:
    GET /capture
    GET /led?color=red
    GET /led?color=green
    GET /led?color=off
"""

import time
import cv2
import requests
import numpy as np
from ultralytics import YOLO

# ============================================================
# SETTINGS
# ============================================================

ESP32_CAM_IP = "192.168.1.100"       # <-- CHANGE THIS
CAPTURE_URL = f"http://{ESP32_CAM_IP}/capture"
LED_URL = f"http://{ESP32_CAM_IP}/led"

MODEL_FILE = "yolov8s-worldv2.pt"

# YOLO-World vocabulary
CLASSES = ["apple", "mango"]

CONFIDENCE_THRESHOLD = 0.50
HTTP_TIMEOUT = 5
LOOP_DELAY = 0.05

# If both Apple and Mango are detected:
# BOTH_OFF, APPLE_PRIORITY, or MANGO_PRIORITY
BOTH_POLICY = "BOTH_OFF"

SAVE_RESULT_IMAGE = True
RESULT_IMAGE_FILE = "AppleMango_result.jpg"

# ============================================================
# ESP32-CAM
# ============================================================

def capture_from_esp32():
    try:
        response = requests.get(CAPTURE_URL, timeout=HTTP_TIMEOUT)

        if response.status_code != 200:
            print(f"[ESP32] Capture error: HTTP {response.status_code}")
            return None

        image_array = np.frombuffer(response.content, dtype=np.uint8)
        frame = cv2.imdecode(image_array, cv2.IMREAD_COLOR)

        if frame is None:
            print("[ESP32] Invalid JPEG received.")
            return None

        return frame

    except requests.RequestException as e:
        print(f"[ESP32] Connection error: {e}")
        return None


def set_led(color):
    try:
        response = requests.get(
            LED_URL,
            params={"color": color},
            timeout=HTTP_TIMEOUT
        )

        if response.status_code == 200:
            print(f"[LED] {color.upper()}")
        else:
            print(f"[LED] Error: HTTP {response.status_code}")

    except requests.RequestException as e:
        print(f"[LED] Connection error: {e}")

# ============================================================
# YOLO-WORLD
# ============================================================

print("=" * 60)
print(" Apple + Mango Object Detection")
print(" ESP32-CAM + YOLO-World")
print("=" * 60)
print(f"[INFO] ESP32-CAM : {ESP32_CAM_IP}")
print(f"[INFO] Model     : {MODEL_FILE}")
print(f"[INFO] Classes   : {CLASSES}")
print(f"[INFO] Threshold : {CONFIDENCE_THRESHOLD}")
print()

print("[AI] Loading YOLO-World model...")
model = YOLO(MODEL_FILE)
model.set_classes(CLASSES)
print("[AI] Model loaded.")
print("[INFO] Press Q to quit.")
print()

# ============================================================
# MAIN LOOP
# ============================================================

last_led = "off"

while True:
    frame = capture_from_esp32()

    if frame is None:
        if last_led != "off":
            set_led("off")
            last_led = "off"
        time.sleep(1)
        continue

    # YOLO-World inference
    results = model.predict(
        source=frame,
        conf=CONFIDENCE_THRESHOLD,
        verbose=False
    )

    result = results[0]

    apple_found = False
    mango_found = False
    best_apple_conf = 0.0
    best_mango_conf = 0.0

    # Process detections
    if result.boxes is not None:
        for box in result.boxes:
            cls_id = int(box.cls[0])
            confidence = float(box.conf[0])

            x1, y1, x2, y2 = map(
                int,
                box.xyxy[0].tolist()
            )

            if 0 <= cls_id < len(CLASSES):
                class_name = CLASSES[cls_id]
            else:
                continue

            if class_name == "apple":
                apple_found = True
                best_apple_conf = max(best_apple_conf, confidence)
            elif class_name == "mango":
                mango_found = True
                best_mango_conf = max(best_mango_conf, confidence)

            # Bounding box
            cv2.rectangle(
                frame,
                (x1, y1),
                (x2, y2),
                (255, 255, 255),
                2
            )

            label = f"{class_name}: {confidence:.2f}"

            cv2.putText(
                frame,
                label,
                (x1, max(y1 - 10, 20)),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.7,
                (255, 255, 255),
                2
            )

    # ========================================================
    # DECISION
    # ========================================================

    if apple_found and mango_found:
        print(
            f"[AI] Apple={best_apple_conf:.2f}, "
            f"Mango={best_mango_conf:.2f}"
        )

        if BOTH_POLICY == "APPLE_PRIORITY":
            decision = "apple"
        elif BOTH_POLICY == "MANGO_PRIORITY":
            decision = "mango"
        else:
            decision = "off"

    elif apple_found:
        decision = "apple"
        print(f"[AI] APPLE detected: {best_apple_conf:.2f}")

    elif mango_found:
        decision = "mango"
        print(f"[AI] MANGO detected: {best_mango_conf:.2f}")

    else:
        decision = "off"
        print("[AI] No Apple/Mango detected.")

    # ========================================================
    # LED CONTROL
    # ========================================================

    if decision == "apple":
        if last_led != "red":
            set_led("red")
            last_led = "red"

    elif decision == "mango":
        if last_led != "green":
            set_led("green")
            last_led = "green"

    else:
        if last_led != "off":
            set_led("off")
            last_led = "off"

    # ========================================================
    # DISPLAY
    # ========================================================

    if decision == "apple":
        decision_text = "APPLE -> RED LED"
    elif decision == "mango":
        decision_text = "MANGO -> GREEN LED"
    else:
        decision_text = "NO TARGET -> LED OFF"

    cv2.putText(
        frame,
        decision_text,
        (20, 40),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.9,
        (255, 255, 255),
        2
    )

    if SAVE_RESULT_IMAGE:
        cv2.imwrite(RESULT_IMAGE_FILE, frame)

    cv2.imshow(
        "ESP32-CAM + YOLO-World | Apple & Mango",
        frame
    )

    key = cv2.waitKey(1) & 0xFF
    if key == ord("q") or key == ord("Q"):
        break

    time.sleep(LOOP_DELAY)

# ============================================================
# EXIT
# ============================================================

print("[INFO] Turning LEDs OFF...")
set_led("off")
cv2.destroyAllWindows()
print("[INFO] Program stopped.")
CLASSES = [
    "apple",
    "mango",
    "glass",
    "chair",
    "person",
    "eyeglasses"
]

เราสามารถทำให้ LED แสดงผลตาม Object ได้ เช่น

if apple_found:
    set_led("red")
elif mango_found:
    set_led("green")
elif chair_found:
    set_led("blue")
elif person_found:
    set_led("yellow")
else:
    set_led("off")