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")