เปลี่ยนคนให้เป็นอนุภาค! สร้างงานอาร์ต Real-Time AI Energy Field ด้วย XIAO ESP32S3 ✨

Real-Time AI Energy Field (เมื่อคนสองคนเข้ามาในเฟรม อนุภาคจะทำปฏิกิริยากัน)

สวัสดีชาว Maker และสาย Interactive Art ทุกคนครับ! ปกติแล้วเวลาเราพูดถึงระบบ Computer Vision หรือ AI ตรวจจับคน (Human Detection) สิ่งที่เราเห็นบนจอมักจะเป็น "กรอบสี่เหลี่ยม (Bounding Box)" ตีล้อมรอบตัวเราพร้อมตัวเลขพิกัดแข็งๆ ระบบรับรู้แค่ว่า "คุณอยู่ตรงนี้" แต่มันไม่ได้ "สัมผัส" ถึงการมีอยู่ของเราจริงๆ เลย

แล้วถ้าเราเปลี่ยนมุมมองใหม่ล่ะ? แทนที่จะมองคนเป็นแค่วัตถุในกรอบสี่เหลี่ยม เราลองแปลงพวกเขาให้กลายเป็น "สนามพลังงาน (Energy Field)" ที่ส่งผลกระทบต่อพื้นที่รอบๆ ตัวดูล่ะ?

โปรเจกต์ "Human Energy Field" นี้ คือการนำข้อมูลจาก AI อย่าง จำนวนคน, ขนาด, และตำแหน่งพิกัด มาตีความใหม่ผ่านภาษาของภาพแบบ อนุภาค (Particles) เมื่อมีคนเดินเข้ามาในกล้อง อนุภาคเหล่านี้จะมีชีวิตและไหลเวียน หากมีคนคนที่สองเข้ามา สนามพลังงานของทั้งคู่ก็จะเชื่อมต่อและสอดแทรกกัน กลายเป็นงานศิลปะแบบเรียลไทม์สุดล้ำครับ!

สถาปัตยกรรมระบบ (System Architecture)

ระบบนี้แบ่งการทำงานออกเป็น 3 เลเยอร์หลักๆ ครับ:

  • 1. Vision Recognition Layer: ใช้ XIAO ESP32S3 Sense รับหน้าที่เป็น "ดวงตา" ในการจับภาพและรันโมเดล AI ตรวจจับคน (เปรียบเสมือนเซนเซอร์ ไม่ใช่สมองหลัก)
  • 2. Data Transmission Layer: ส่งข้อมูลที่ได้ผ่านพอร์ต UART ไปยังบอร์ด XIAO ESP32C3
  • 3. Particle Generation Layer: บอร์ด C3 จะจัดฟอร์แมตข้อมูลแล้วส่งเข้าคอมพิวเตอร์ให้โปรแกรม Processing นำพิกัดไปวาดเป็นอนุภาค (Particle) สีสันต่างๆ ตามการเคลื่อนไหว

⚠️ บทเรียนราคาแพง (Important Note): ทำไมเราถึงต้องใช้บอร์ดถึง 2 ตัว? ทำไมไม่เอา S3 Sense ส่งเข้า Processing ตรงๆ เลย?

คำตอบคือ: S3 Sense ที่รันเฟิร์มแวร์ AI สำเร็จรูป (Packaged AI firmware) ไม่สามารถรันโค้ดลอจิก Arduino ของเราควบคู่ไปพร้อมกันได้ครับ! เราเลยต้องมี ESP32C3 มาเป็นตัวกลาง (Intermediate layer) ในการรับข้อมูลมาจัดเรียงใหม่แล้วค่อยส่งเข้าคอมนั่นเอง

💡 Maker's Tip: ในการทำโปรเจกต์ที่ใช้กล้อง หรือบอร์ดตัวจิ๋วอย่าง XIAO Series การจัดการสายไฟและหาจุดยึดบอร์ดเป็นเรื่องสำคัญมาก แนะนำให้ใช้ 3D Printer ปริ้นท์เคสสวยๆ มาจัดระเบียบอุปกรณ์ครับ หากใครกำลังหา บอร์ด ESP32, สาย Jumper Wires, หรือเส้นพลาสติก 3D Print (Filament) คุณภาพดี แวะไปช้อปของแท้พร้อมลุยโปรเจกต์ได้ที่ Globalbyte เลยครับ!

ขั้นตอนการสร้าง (Detailed Build Process)

Step 1 & 2: ลงโมเดล AI และเตรียมซอฟต์แวร์

เริ่มต้นจากการนำบอร์ด XIAO ESP32S3 Sense ไปดีพลอยโมเดล AI "Personnel Detection" ผ่านแพลตฟอร์ม SenseCraft AI ของ Seeed Studio (เลือกรุ่นให้ตรงกับบอร์ดนะครับ) เมื่อลงเสร็จ บอร์ดจะเริ่มยิงข้อมูลพิกัดออกมา

สำหรับซอฟต์แวร์ ให้เตรียม Arduino IDE (ดูวิธีตั้งค่าบอร์ด ได้ที่นี่) และโปรแกรม Processing (The Processing Foundation) สำหรับทำกราฟิกครับ

Step 3: การเชื่อมต่อฮาร์ดแวร์ (Hardware Connection)

เตรียมสาย USB สองเส้นเสียบบอร์ดทั้งสองเข้ากับคอมพิวเตอร์ และใช้สาย Jumper (Female-to-Female) 3 เส้น เพื่อเชื่อมต่อบอร์ด S3 Sense เข้ากับ C3 ผ่าน UART ดังนี้:

  • TX ของ S3 Sense ➡️ RX ของ C3
  • RX ของ S3 Sense ➡️ TX ของ C3
  • GND ➡️ GND
Bounding Box vs Energy Field
จาก Bounding Box แข็งๆ สู่ Energy Field
AI Generated Concept
ภาพคอนเซปต์ AI-Generated ของสนามพลังงานมนุษย์
SenseCraft AI Model Deployment
การใช้ SenseCraft AI เลือกโมเดล Personnel Detection
SenseCraft Output Data
ข้อมูล Output จาก SenseCraft AI
USB Connections
ต่อสาย USB ทั้งสองบอร์ดเข้ากับคอมพิวเตอร์
UART Wiring Diagram
ผังการต่อสาย UART ระหว่างบอร์ด S3 Sense และ C3
Wiring Close up 1 Wiring Close up 2

Step 4: จัดฟอร์แมตข้อมูลด้วย ESP32C3 (Arduino Code)

เปิด Arduino IDE เลือกบอร์ดและพอร์ตของ XIAO ESP32C3 ให้ถูกต้อง จากนั้นอัปโหลดโค้ดด้านล่างนี้ลงไป เพื่อให้บอร์ดทำหน้าที่รับค่าจาก S3 Sense มาเรียบเรียงใหม่ครับ (ถ้าทำงานปกติ จะเห็นข้อมูลวิ่งใน Serial Monitor)

C++ (Arduino IDE - for ESP32C3)
#include <Seeed_Arduino_SSCMA.h>
#ifdef ESP32
#include <HardwareSerial.h>
// Define two Serial devices mapped to the two internal UARTs
HardwareSerial atSerial(0);
#else
#define atSerial Serial1
#endif

SSCMA AI;

void setup()
{
  Serial.begin(9600);
  AI.begin(&atSerial);
}

void loop()
{
  if (!AI.invoke(1,false,true))
  {
    Serial.println("invoke success");
    Serial.print("perf: prepocess=");
    Serial.print(AI.perf().prepocess);
    Serial.print(", inference=");
    Serial.print(AI.perf().inference);
    Serial.print(", postpocess=");
    Serial.println(AI.perf().postprocess);
    
    for (int i = 0; i < AI.boxes().size(); i++)
    {
      Serial.print("Box["); Serial.print(i);
      Serial.print("] target="); Serial.print(AI.boxes()[i].target);
      Serial.print(", score="); Serial.print(AI.boxes()[i].score);
      Serial.print(", x="); Serial.print(AI.boxes()[i].x);
      Serial.print(", y="); Serial.print(AI.boxes()[i].y);
      Serial.print(", w="); Serial.print(AI.boxes()[i].w);
      Serial.print(", h="); Serial.println(AI.boxes()[i].h);
    }
    
    for (int i = 0; i < AI.classes().size(); i++)
    {
      Serial.print("Class["); Serial.print(i);
      Serial.print("] target="); Serial.print(AI.classes()[i].target);
      Serial.print(", score="); Serial.println(AI.classes()[i].score);
    }
    
    for (int i = 0; i < AI.points().size(); i++)
    {
      Serial.print("Point["); Serial.print(i);
      Serial.print("]: target="); Serial.print(AI.points()[i].target);
      Serial.print(", score="); Serial.print(AI.points()[i].score);
      Serial.print(", x="); Serial.print(AI.points()[i].x);
      Serial.print(", y="); Serial.println(AI.points()[i].y);
    }
    
    for (int i = 0; i < AI.keypoints().size(); i++)
    {
      Serial.print("keypoint["); Serial.print(i);
      Serial.print("] target="); Serial.print(AI.keypoints()[i].box.target);
      Serial.print(", score="); Serial.print(AI.keypoints()[i].box.score);
      Serial.print(", box:[x="); Serial.print(AI.keypoints()[i].box.x);
      Serial.print(", y="); Serial.print(AI.keypoints()[i].box.y);
      Serial.print(", w="); Serial.print(AI.keypoints()[i].box.w);
      Serial.print(", h="); Serial.print(AI.keypoints()[i].box.h);
      Serial.print("], points:[");
      for (int j = 0; j < AI.keypoints()[i].points.size(); j++)
      {
        Serial.print("["); Serial.print(AI.keypoints()[i].points[j].x);
        Serial.print(","); Serial.print(AI.keypoints()[i].points[j].y);
        Serial.print("],");
      }
      Serial.println("]");
    }
    
    if(!AI.last_image().isEmpty())
    {
      Serial.print("Last image:");
      Serial.println(AI.last_image().c_str());
    }
  }
}

Step 5: สร้างอนุภาคพลังงานด้วย Processing (Particle Generation Layer)

ก่อนจะรันโค้ด Processing อย่าลืมปิดโปรแกรม Arduino IDE ให้สนิท เพื่อไม่ให้พอร์ต Serial ชนกันนะครับ โปรแกรม Processing จะทำหน้าที่รับค่าพิกัดจากบอร์ด C3 มาตีความใหม่ แปลงเป็นสัดส่วนของหน้าจอ กำหนดสี และสร้างเอฟเฟกต์อนุภาค (Particle effects)

ข้อควรระวังเรื่องพอร์ต (Serial Port): ในโปรแกรม Processing จะไม่มีเมนูให้เลือกพอร์ตง่ายๆ แบบ Arduino เราต้องใส่ตัวเลข Index ลงไปในโค้ดตรงบรรทัด myPort = new Serial(this, Serial.list()[1], BAUD); ซึ่งตอนรันครั้งแรก ให้ลองใส่ 0 ไปก่อน แล้วดูในแถบ Console ด้านล่างว่าคอมเรามีพอร์ตอะไรบ้าง ถ้าบอร์ด C3 ของคุณต่ออยู่ที่ COM7 และในลิสต์เขียนว่า [0] "COM5", [1] "COM7" คุณก็ต้องเปลี่ยนเลขในโค้ดเป็น 1 ครับ

Java (Processing Code)
import processing.serial.*;
import java.util.*;
import java.util.regex.*;

Serial myPort;

final int BAUD = 115200;
final float CAM_W = 320.0;
final float CAM_H = 240.0;

final int MAX_PARTICLES = 1200;
final int MAX_DETECTIONS = 50;

ArrayList<Detection> detections = new ArrayList<Detection>();
ArrayList<Track> tracks = new ArrayList<Track>();

Particle[] pool = new Particle[MAX_PARTICLES];
int nextTrackId = 0;

Pattern boxPattern = Pattern.compile(
  "Box\\[(\\d+)\\].*x=(\\d+), y=(\\d+), w=(\\d+), h=(\\d+)"
);

void setup() {
  size(1280, 720);
  frameRate(60);
  background(0);

  colorMode(HSB, 360, 100, 100, 100);

  for (int i = 0; i < pool.length; i++) {
    pool[i] = new Particle();
  }

  println(Serial.list());
  // CHANGE THIS INDEX TO MATCH YOUR COM PORT!
  myPort = new Serial(this, Serial.list()[1], BAUD);
  myPort.bufferUntil('\n');
}

void draw() {
  fill(0, 0, 0, 25);
  rect(0, 0, width, height);

  drawHumans();
  updateParticles();
  drawConnections();
}

void serialEvent(Serial p) {
  String line = p.readStringUntil('\n');
  if (line == null) return;

  line = trim(line);
  if (line.length() > 200) return;

  if (line.startsWith("Box")) {
    Matcher m = boxPattern.matcher(line);
    if (m.find()) {

      if (detections.size() >= MAX_DETECTIONS) return;

      Detection d = new Detection();

      d.cx = float(m.group(2)) + float(m.group(4)) / 2.0;
      d.cy = float(m.group(3)) + float(m.group(5)) / 2.0;
      d.w = float(m.group(4));
      d.h = float(m.group(5));

      d.sx = map(d.cx, 0, CAM_W, 0, width);
      d.sy = map(d.cy, 0, CAM_H, 0, height);
      d.sw = map(d.w, 0, CAM_W, 0, width);
      d.sh = map(d.h, 0, CAM_H, 0, height);

      detections.add(d);
    }
  }

  if (line.contains("invoke success")) {

    while (tracks.size() < detections.size()) {
      Track t = new Track();
      t.id = nextTrackId++;
      tracks.add(t);
    }

    for (int i = 0; i < detections.size(); i++) {
      tracks.get(i).update(detections.get(i));
    }

    for (int i = detections.size(); i < tracks.size(); i++) {
      tracks.get(i).active = false;
    }

    detections.clear();
  }
}

void drawHumans() {
  blendMode(ADD);

  ArrayList<Track> activeTracks = new ArrayList<Track>();
  ArrayList<Integer> spawnCounts = new ArrayList<Integer>();

  for (Track t : tracks) {
    if (!t.active) continue;

    activeTracks.add(t);
    drawHumanShape(t);

    int count = int(t.w * t.h / 1500.0);
    count = constrain(count, 10, 40);
    spawnCounts.add(count);
  }

  int maxSpawn = 0;
  for (int c : spawnCounts) maxSpawn = max(maxSpawn, c);

  for (int s = 0; s < maxSpawn; s++) {
    for (int i = 0; i < activeTracks.size(); i++) {
      if (s < spawnCounts.get(i)) {
        activateParticle(activeTracks.get(i));
      }
    }
  }

  blendMode(BLEND);
}

void drawHumanShape(Track t) {
  float cx = t.x;
  float cy = t.y;

  float w = t.w * 1.2;
  float h = t.h * 1.5;

  stroke(t.c, 80);
  noFill();

  ellipse(cx, cy - h * 0.3, w * 0.2, w * 0.2);
  line(cx, cy - h * 0.2, cx, cy + h * 0.3);
  line(cx, cy, cx - w * 0.3, cy + h * 0.1);
  line(cx, cy, cx + w * 0.3, cy + h * 0.1);
  line(cx, cy + h * 0.3, cx - w * 0.2, cy + h * 0.6);
  line(cx, cy + h * 0.3, cx + w * 0.2, cy + h * 0.6);
}

boolean activateParticle(Track t) {
  for (Particle p : pool) {
    if (!p.active) {
      p.init(t);
      return true;
    }
  }
  return false;
}

void updateParticles() {
  blendMode(ADD);

  for (Particle p : pool) {
    if (p.active) {
      p.update();
      p.draw();
    }
  }

  blendMode(BLEND);
}

void drawConnections() {
  if (tracks.size() < 2) return;

  blendMode(ADD);

  for (int i = 0; i < tracks.size(); i++) {
    Track a = tracks.get(i);
    if (!a.active) continue;

    for (int j = i + 1; j < tracks.size(); j++) {
      Track b = tracks.get(j);
      if (!b.active) continue;

      float d = dist(a.x, a.y, b.x, b.y);

      if (d < 300) {
        stroke(lerpColor(a.c, b.c, 0.5), 60);

        beginShape();
        for (int k = 0; k < 20; k++) {
          float tt = k / 20.0;
          float x = lerp(a.x, b.x, tt);
          float y = lerp(a.y, b.y, tt);

          float offset = (noise(tt * 5, frameCount * 0.02) - 0.5) * 40;
          y += offset;

          vertex(x, y);
        }
        endShape();
      }
    }
  }

  blendMode(BLEND);
}

class Detection {
  float cx, cy, w, h;
  float sx, sy, sw, sh;
}

class Track {
  int id = -1;
  float x, y, w, h;
  color c;
  boolean active = true;

  void update(Detection d) {
    x = d.sx;
    y = d.sy;
    w = d.sw;
    h = d.sh;
    active = true;

    float seed = id * 37.0 + d.cx * 0.13 + d.cy * 0.17;
    float hue = (noise(seed) * 360.0) % 360.0;

    c = color(hue, 80, 100);
  }
}

class Particle {
  boolean active = false;
  Track t;
  float x, y;
  float vx, vy;
  float life;
  color c;

  void init(Track t_) {
    t = t_;
    active = true;
    c = t.c;

    float cx = t.x;
    float cy = t.y;

    float w = t.w * 1.2;
    float h = t.h * 1.5;

    int type = int(random(5));

    if (type == 0) {
      float angle = random(TWO_PI);
      float r = w * 0.1;

      x = cx + cos(angle) * r;
      y = cy - h * 0.3 + sin(angle) * r;

      vx = cos(angle) * random(1, 3);
      vy = sin(angle) * random(1, 3);

    } else if (type == 1) {
      float tLine = random(1);

      x = cx;
      y = lerp(cy - h * 0.2, cy + h * 0.3, tLine);

      vx = random(-1.5, 1.5);
      vy = random(0.5, 2);

    } else if (type == 2) {
      float tLine = random(1);

      x = lerp(cx, cx - w * 0.3, tLine);
      y = lerp(cy, cy + h * 0.1, tLine);

      vx = random(-2, -0.5);
      vy = random(-0.5, 1.5);

    } else if (type == 3) {
      float tLine = random(1);

      x = lerp(cx, cx + w * 0.3, tLine);
      y = lerp(cy, cy + h * 0.1, tLine);

      vx = random(0.5, 2);
      vy = random(-0.5, 1.5);

    } else {
      float side = random(1) < 0.5 ? -1 : 1;
      float tLine = random(1);

      x = lerp(cx, cx + side * w * 0.2, tLine);
      y = lerp(cy + h * 0.3, cy + h * 0.6, tLine);

      vx = side * random(0.5, 2);
      vy = random(1, 3);
    }

    life = random(60, 120);
  }

  void update() {
    if (t == null || !t.active) {
      active = false;
      return;
    }

    float dx = t.x - x;
    float dy = t.y - y;

    vx += dx * 0.0005;
    vy += dy * 0.0005;

    x += vx;
    y += vy;

    life--;

    if (life <= 0) active = false;
  }

  void draw() {
    fill(c, 85);
    noStroke();
    ellipse(x, y, 3, 3);
  }
}

บทสรุป (Conclusion)

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

ลองเอาไปทำตามกันดูนะครับ! ถ้าใครสามารถเขียนปรับโค้ดอนุภาคให้มีพฤติกรรมแปลกๆ หรือเอฟเฟกต์เท่ๆ กว่าเดิมได้ อย่าลืมเอามาแชร์ให้ดูกันด้วยนะครับ

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

แหล่งที่มาบทความต้นฉบับ: Hackster.io - Real-Time AI Energy Field

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

 

แท็ก


Blog posts

เข้าสู่ระบบ

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

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