Back

เทรน YOLO ตรวจจับวัตถุ แล้ว deploy เป็น API บน VPSBlur image

บทความนี้จะพาสร้างระบบตรวจจับวัตถุ (Object Detection) ตั้งแต่เตรียมภาพ ติดป้ายกำกับ และเทรนโมเดล YOLO บนโน้ตบุ๊ก จากนั้นสร้าง REST API ด้วย FastAPI และนำไปรันบน VPS ด้วย Docker

เมื่อทำครบทุกขั้นตอน เราจะได้ API ที่รับไฟล์ภาพและส่งกลับพิกัดกรอบวัตถุ (bounding box) พร้อมชื่อคลาสและค่าความมั่นใจ (confidence) รวมถึงหน้าเว็บสำหรับวาดกรอบผลตรวจจับลงบนภาพ ตัวอย่างนี้ใช้สุนัข 2 สายพันธุ์ ได้แก่ corgi และ chihuahua อย่างละ 50 ภาพ

ดาวน์โหลดชุดภาพสำหรับทำตามได้จากลิงก์ด้านล่าง ทั้ง 100 ภาพยังไม่มีป้ายกำกับ เราจะเพิ่มป้ายกำกับด้วย Label Studio ในหัวข้อ 3:

ภาพรวม pipeline ตั้งแต่เตรียมภาพจนถึง deploy บน VPS

1. เตรียมสภาพแวดล้อม#

เริ่มจากติดตั้ง Python สร้างสภาพแวดล้อมแยกสำหรับโปรเจกต์ แล้วติดตั้งไลบรารีที่ใช้เตรียมข้อมูล เทรนโมเดล และสร้าง API ขั้นตอนในหัวข้อนี้ทำบนเครื่องของเรา ส่วน VPS จะใช้ในขั้นตอน deploy

1.1 เลือกเวอร์ชัน Python#

บทความนี้ใช้ Python 3.13 ให้ใช้เวอร์ชันเดียวกันเพื่อทำตามคำสั่งและตัวอย่างได้สอดคล้องกัน

Windows — ติดตั้งด้วย winget แล้วเปิด terminal ใหม่เพื่อตรวจเวอร์ชัน:

winget install Python.Python.3.13
py -3.13 --version
bash

macOS — ติดตั้งด้วย Homebrew (หรือดาวน์โหลด installer จาก https://www.python.org/downloads/ ↗):

brew install python@3.13
python3.13 --version
bash

1.2 สร้าง virtual environment#

สร้างโฟลเดอร์โปรเจกต์และเปิด terminal ในโฟลเดอร์นั้น จากนั้นสร้าง virtual environment ด้วย Python 3.13 เพื่อแยกไลบรารีของโปรเจกต์ออกจากโปรเจกต์อื่น เปิดใช้งาน environment แล้วตรวจสอบเวอร์ชัน Python:

py -3.13 -m venv .venv          # Windows; mac/linux: python3.13 -m venv .venv
# Windows: .venv\Scripts\activate   |  mac/linux: source .venv/bin/activate
python --version                # confirm 3.13.x
bash

1.3 ติดตั้ง dependency หลัก#

เมื่อเปิดใช้งาน virtual environment แล้ว ให้ติดตั้งไลบรารีสำหรับเทรนโมเดล ทำป้ายกำกับ และสร้าง API:

pip install ultralytics label-studio fastapi uvicorn python-multipart
bash

ultralytics จะติดตั้ง PyTorch มาด้วย ตัวอย่างในบทความเทรนบน CPU จึงทำตามได้โดยไม่ต้องมี GPU ส่วนระยะเวลาที่ใช้จะขึ้นอยู่กับสเปกเครื่อง

2. เตรียมภาพ#

ขั้นต่อไปคือเตรียมชุดข้อมูล (dataset) สำหรับเทรนโมเดล เลือกภาพให้เห็นลักษณะของแต่ละคลาสชัดเจน และมีพื้นหลัง แสง และมุมกล้องที่หลากหลาย

2.1 เลือกคลาสและจำนวนภาพ#

เราจะใช้ corgi และ chihuahua อย่างละ 50 ภาพ รวม 100 ภาพ โดยแบ่งแต่ละคลาสเป็นภาพสำหรับเทรน 40 ภาพ และภาพสำหรับตรวจสอบผล (validation set) 10 ภาพในหัวข้อ 3.7 ชุดข้อมูลนี้ใช้ฝึกทำตามขั้นตอน ส่วนการประเมินว่าโมเดลพร้อมใช้งานจริงหรือไม่ยังต้องอาศัยภาพทดสอบที่มากและหลากหลายกว่านี้

ชุดภาพในบทความสร้างด้วย AI image generator หากเตรียมชุดข้อมูลเอง สามารถใช้ภาพถ่ายจริงได้เช่นกัน ด้านล่างคือตัวอย่างสายพันธุ์ละ 2 ภาพ:

ตัวอย่างภาพ corgi ตัวอย่างภาพ corgi ตัวอย่างภาพ chihuahua ตัวอย่างภาพ chihuahua

หากใช้ชุดภาพจากลิงก์ต้นบทความ ให้แตกไฟล์ไว้ก่อน แล้วเตรียมนำเข้า Label Studio ตามขั้นตอนถัดไป

2.2 เลือกภาพที่เห็นลักษณะของแต่ละสายพันธุ์#

ภาพที่ครอปเฉพาะหน้าหรือลำตัวบางส่วนอาจไม่แสดงลักษณะที่ช่วยแยกสองสายพันธุ์ได้ชัดเจน สำหรับตัวอย่างนี้ ให้เลือกภาพ เต็มตัว (full-body) เพื่อให้เห็นรูปทรงและสัดส่วนของสุนัข

สังเกตความต่าง เช่น ลำตัวยาวและขาสั้นของ corgi เทียบกับรูปทรงของ chihuahua พร้อมเลือกภาพที่มีสีขน พื้นหลัง แสง และมุมกล้องหลากหลาย เพื่อให้ชุดข้อมูลแสดงลักษณะของแต่ละสายพันธุ์ได้ครอบคลุมขึ้น

รูปทรงลำตัว corgi vs chihuahua สำคัญกว่าสี

3. ติดป้ายกำกับภาพด้วย Label Studio#

การเทรนโมเดลตรวจจับวัตถุต้องมีทั้งภาพและข้อมูลกำกับว่าวัตถุอยู่ตรงไหนและเป็นคลาสใด เราจะใช้ Label Studio วาดกรอบรอบตัวสุนัข ระบุสายพันธุ์ แล้วส่งออกข้อมูลในรูปแบบ YOLO

3.1 เปิดโปรแกรมและสร้างโปรเจกต์#

รันคำสั่งด้านล่างเพื่อเปิด Label Studio แล้วเข้า http://localhost:8080 ในเบราว์เซอร์ หากใช้งานครั้งแรก ให้สร้างบัญชีและเข้าสู่ระบบ:

label-studio start
bash

หน้าลงทะเบียนของ Label Studio หน้าเข้าสู่ระบบของ Label Studio

ที่หน้า Welcome/Home กด Create Project กล่องตั้งค่ามี 3 แท็บ:

  1. Project Name — ตั้งชื่อโปรเจกต์ (เช่น “My Dogs”) คำอธิบายใส่หรือไม่ก็ได้ ข้าม Workspace (มีเฉพาะรุ่น Enterprise)
  2. Data Import — Upload More Files แล้วเลือกภาพทั้ง 100 รอจนขึ้น “100 files uploaded”
  3. Labeling Setup — เมนูซ้าย Computer Vision แล้วเลือก Object Detection with Bounding Boxes

กด Create Project แท็บ Project Name แท็บ Data Import อัปโหลดภาพ แท็บ Labeling Setup เลือก Object Detection

3.2 ตั้งชื่อคลาส#

ในแท็บ Labeling Setup ให้ลบป้ายกำกับเริ่มต้น แล้วเพิ่ม corgi และ chihuahua ผ่านช่อง Add label names โดยกด Add ทีละชื่อ จากนั้นกด Save ส่วนลำดับหมายเลขคลาสที่จะใช้เทรน ให้ตรวจจากไฟล์ที่ส่งออกอีกครั้งในหัวข้อ 3.6

แทนที่ label เริ่มต้นด้วยชื่อสายพันธุ์

3.3 ตีกรอบภาพ#

หลังบันทึกโปรเจกต์ จะเห็นรายการภาพ 100 รายการพร้อมภาพตัวอย่าง กด Label All Tasks เพื่อเริ่มติดป้ายกำกับ

ทำตามขั้นตอนนี้จนครบทุกภาพ:

  1. เลือกคลาสให้ตรงกับสายพันธุ์ในภาพ หรือใช้ปุ่มลัด 1–2
  2. ลากกรอบให้กระชับรอบตัวสุนัข โดยครอบคลุมส่วนที่มองเห็นทั้งหมด
  3. ตรวจชื่อคลาสและตำแหน่งกรอบ แล้วกด Submit เพื่อบันทึกและไปยังภาพถัดไป

รายการ task พร้อม label ตัวอย่างตีกรอบ chihuahua ตัวอย่างตีกรอบ corgi

3.4 Export เป็น YOLO#

เมื่อติดป้ายกำกับครบแล้ว กด Export และเลือก YOLO with Images เพื่อส่งออกทั้งภาพและป้ายกำกับในชุดเดียวกัน ตัวอย่างนี้ใช้กรอบสี่เหลี่ยมปกติ จึงไม่เลือกแบบ OBB ซึ่งเป็นกรอบแบบหมุน

Export แบบ YOLO with Images

3.5 จัดโฟลเดอร์ dataset#

สร้างโฟลเดอร์ dataset/ ในโฟลเดอร์โปรเจกต์ แล้วแตกไฟล์ ZIP ที่ export มาลงในโฟลเดอร์นี้ทั้งหมด ไฟล์ ZIP มีทั้งภาพ ป้ายกำกับ และไฟล์ประกอบอยู่แล้ว จึงได้โครงสร้างที่พร้อมใช้งานทันที ในขั้นนี้ยังไม่ต้องแยกโฟลเดอร์ย่อย train และ val:

dataset/
  images/
    0031badb-chihuahua_35.jpg
    06a37ae3-corgi_49.jpg
    ...                          # 100 files
  labels/
    0031badb-chihuahua_35.txt
    06a37ae3-corgi_49.txt
    ...                          # 100 files
  classes.txt
  notes.json

images/ เก็บภาพ labels/ เก็บไฟล์ป้ายกำกับ .txt พร้อมด้วยไฟล์ classes.txt และ notes.json ตัวอย่างนี้ใช้ภาพ JPG และสคริปต์ในหัวข้อ 3.6 และ 3.7 จะค้นหาไฟล์ .jpg หากใช้ PNG ให้ปรับนามสกุลในสคริปต์ให้ตรงกัน

ไฟล์ภาพและป้ายกำกับที่ส่งออกในตัวอย่างมี hash prefix เดียวกัน เช่น 06a37ae3-corgi_49.jpg จับคู่กับ 06a37ae3-corgi_49.txt ให้ใช้ภาพจากชุดที่ส่งออกนี้ในการเทรน เพื่อให้ชื่อไฟล์ตรงกับป้ายกำกับ

3.6 ตรวจข้อมูลที่ส่งออกก่อนเทรน#

ก่อนเทรน ให้ตรวจสองจุดนี้เพื่อให้ชื่อคลาสและไฟล์ป้ายกำกับตรงกับภาพ

จุดที่ 1: ลำดับคลาส ให้ยึดลำดับใน classes.txt เป็นหลักในการกำหนดหมายเลขคลาส (class ID) โดยเริ่มนับจาก 0 ไฟล์ที่ส่งออกในตัวอย่างมีลำดับดังนี้:

chihuahua
corgi

ดังนั้น chihuahua คือคลาส 0 และ corgi คือคลาส 1 ซึ่งตรงกับ ID ใน notes.json เมื่อตั้งค่า data.yaml ในหัวข้อ 4 ต้องใช้ลำดับเดียวกัน หากสลับลำดับ ชื่อคลาสที่แสดงจะไม่ตรงกับป้ายกำกับ แม้คำสั่งเทรนจะยังทำงานได้

จุดที่ 2: ภาพและป้ายกำกับจับคู่กันครบ ชื่อไฟล์ส่วนที่ไม่รวมนามสกุล (stem) ของภาพและป้ายกำกับต้องตรงกันตาม hash prefix ที่ export มาในหัวข้อ 3.5

สร้าง check_export.py ในโฟลเดอร์โปรเจกต์เพื่อตรวจทั้งสองจุดพร้อมกัน:

# check_export.py — run from the folder that contains dataset/
from pathlib import Path

ds = Path("dataset")
# 1) class order: class id = line number (starts at 0)
classes = [c.strip() for c in (ds / "classes.txt").read_text().splitlines() if c.strip()]
print("class order:", {i: c for i, c in enumerate(classes)})

# 2) image <-> label stems must match
imgs = {p.stem for p in (ds / "images").glob("*.jpg")}
lbls = {p.stem for p in (ds / "labels").glob("*.txt")}
if imgs == lbls:
    print(f"OK: {len(imgs)} images all have matching labels")
else:
    print("image without label:", sorted(imgs - lbls)[:5])
    print("label without image:", sorted(lbls - imgs)[:5])
python

รัน python check_export.py จะได้ผลแบบนี้:

class order: {0: 'chihuahua', 1: 'corgi'}
OK: 100 images all have matching labels

ข้อความ OK หมายถึงชื่อไฟล์ภาพและป้ายกำกับจับคู่กันครบ จากนั้นลองเปิด dataset/labels/06a37ae3-corgi_49.txt เพื่อตรวจเนื้อหา แต่ละบรรทัดแทนกรอบหนึ่งกรอบในรูปแบบ class x_center y_center w h โดยค่าพิกัดและขนาดกรอบปรับให้อยู่ในช่วง 0–1 เทียบกับขนาดภาพ:

1 0.462936 0.516904 0.335756 0.802619

ค่าแรกคือ 1 จึงหมายถึง corgi ตามลำดับใน classes.txt ให้ตรวจว่าคลาสนี้ตรงกับสุนัขในภาพด้วย

3.7 แยก train / val#

แบ่งภาพแต่ละคลาสเป็น train 40 ภาพ สำหรับเทรน และ val 10 ภาพ สำหรับตรวจสอบผล รวมเป็น train 80 ภาพและ val 20 ภาพ โดยแยกไฟล์ทั้งสองชุดออกจากกัน

แบ่ง 100 ภาพเป็น train 80 และ val 20

สร้างไฟล์ split_dataset.py ในโฟลเดอร์โปรเจกต์ สคริปต์นี้จะค้นหาภาพตามชื่อสายพันธุ์ เรียงตามชื่อไฟล์ แล้วแยก 10 ภาพท้ายของแต่ละคลาสไว้ในชุด val โดยย้ายภาพและป้ายกำกับไปยังโฟลเดอร์ย่อยที่ตรงกัน ชื่อไฟล์ที่มี hash prefix ใช้งานได้เช่นเดิม:

# split_dataset.py — run from the folder that contains dataset/
import shutil
from pathlib import Path

ds = Path("dataset")
CLASSES = ["corgi", "chihuahua"]
VAL_PER_CLASS = 10

for sub in ["images/train", "images/val", "labels/train", "labels/val"]:
    (ds / sub).mkdir(parents=True, exist_ok=True)

for c in CLASSES:
    imgs = sorted((ds / "images").glob(f"*{c}_*.jpg"))   # matches names with/without hash prefix
    for i, img in enumerate(imgs):
        split = "train" if i < len(imgs) - VAL_PER_CLASS else "val"
        lbl = ds / "labels" / f"{img.stem}.txt"
        shutil.move(str(img), str(ds / "images" / split / img.name))
        if lbl.exists():
            shutil.move(str(lbl), str(ds / "labels" / split / lbl.name))

for split in ["train", "val"]:
    ni = len(list((ds / "images" / split).glob("*.jpg")))
    nl = len(list((ds / "labels" / split).glob("*.txt")))
    print(f"{split}: {ni} images, {nl} labels")
python

รัน python split_dataset.py จะได้ผลแบบนี้:

train: 80 images, 80 labels
val: 20 images, 20 labels

ผลลัพธ์ควรมีภาพและป้ายกำกับอย่างละ 80 ไฟล์ในชุด train และอย่างละ 20 ไฟล์ในชุด val โดยมีโครงสร้างดังนี้:

dataset/
  images/
    train/
      0031badb-chihuahua_35.jpg
      06a37ae3-corgi_49.jpg
      ...                          # 80 files, 40 per class
    val/
      075cda1f-chihuahua_12.jpg
      08ced0b4-corgi_30.jpg
      ...                          # 20 files, 10 per class
  labels/
    train/
      0031badb-chihuahua_35.txt
      06a37ae3-corgi_49.txt
      ...                          # 80 files
    val/
      075cda1f-chihuahua_12.txt
      08ced0b4-corgi_30.txt
      ...                          # 20 files
  classes.txt
  notes.json

4. เทรนด้วย YOLO#

เมื่อภาพและป้ายกำกับพร้อมแล้ว เราจะกำหนดตำแหน่งชุดข้อมูล เทรนโมเดล และตรวจผลก่อนนำไปสร้าง API

4.1 สร้าง data.yaml#

สร้าง data.yaml ในโฟลเดอร์โปรเจกต์เพื่อระบุตำแหน่งชุดข้อมูลและชื่อคลาส กำหนด train กับ val ให้ตรงกับโฟลเดอร์ในหัวข้อ 3.7 และเรียง names ตาม classes.txt:

path: ./dataset
train: images/train
val: images/val
names:            # MUST match classes.txt order (Label Studio alphabetizes!)
  0: chihuahua
  1: corgi
yaml

4.2 สั่งเทรน#

รันคำสั่งต่อไปนี้จากโฟลเดอร์โปรเจกต์เพื่อเทรนโมเดล yolo11n.pt จำนวน 100 รอบ (epoch) โดยกำหนดขนาดภาพสำหรับเทรนเป็น 640 พิกเซล:

yolo detect train data=data.yaml model=yolo11n.pt epochs=100 imgsz=640
bash

ในการรันครั้งแรก ระบบจะดาวน์โหลด yolo11n.pt และแสดงการตั้งค่า ตัวอย่าง log ด้านล่างมาจากการเทรนบน CPU ให้สังเกต Overriding model.yaml nc=80 with nc=2 ซึ่งระบุว่าปรับโมเดลให้รองรับ 2 คลาส และจำนวนภาพ 80 train, 20 val ที่ตรงกับชุดข้อมูลของเรา:

Ultralytics 8.4.161  Python-3.13.15 torch-2.14.0+cpu CPU (Intel Core i5-10300H 2.50GHz)
Overriding model.yaml nc=80 with nc=2
YOLO11n summary: 181 layers, 2,590,230 parameters, 2,590,214 gradients, 6.5 GFLOPs
Transferred 448/499 items from pretrained weights
train: Scanning dataset\labels\train... 80 images, 0 backgrounds, 0 corrupt
val: Scanning dataset\labels\val... 20 images, 0 backgrounds, 0 corrupt
optimizer: AdamW(lr=0.001667, momentum=0.9) with parameter groups ...
Using 80 train, 20 val images for fraction=1.0 at imgsz=640
Starting training for 100 epochs...

ระหว่างเทรน แต่ละ epoch จะแสดงค่า loss ได้แก่ box_loss สำหรับตำแหน่งและขนาดกรอบ, cls_loss สำหรับการจำแนกคลาส และ dfl_loss ที่เกี่ยวกับการระบุตำแหน่งกรอบ พร้อมค่าประเมินผล เช่น mAP50 ซึ่งจะอธิบายในหัวข้อ 4.3 ผลการเทรนรอบนี้บันทึกไว้ที่ runs\detect\train:

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
      1/100         0G     0.6871      2.941      1.235         42        640: 100% 5/5 27.7s
                   all         20         20    0.00338          1      0.754      0.664

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
      7/100         0G     0.8248      1.661      1.309         38        640: 100% 5/5 25.4s
                   all         20         20      0.873       0.15      0.429      0.218

ค่าประเมินผลในช่วงแรกอาจขึ้นลงได้ ให้ดูแนวโน้มตลอดการเทรนประกอบกัน แทนการตัดสินจาก epoch ใด epoch หนึ่ง เราจะดูกราฟรวมในหัวข้อ 4.3

เมื่อเทรนเสร็จ จะได้ไฟล์น้ำหนัก best.pt และ last.pt พร้อมผลประเมิน best.pt บนชุด val แยกตามคลาส:

    100/100         0G     0.2438     0.5331     0.9026         16        640: 100% 5/5 25.6s
                   all         20         20      0.993          1      0.995      0.963

100 epochs completed in 0.760 hours.
Optimizer stripped from runs\detect\train\weights\best.pt, 5.5MB

Validating runs\detect\train\weights\best.pt...
YOLO11n summary (fused): 100 layers, 2,582,542 parameters, 0 gradients, 6.4 GFLOPs
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95)
                   all         20         20      0.993          1      0.995       0.97
             chihuahua         10         10      0.994          1      0.995      0.944
                 corgi         10         10      0.992          1      0.995      0.995
Speed: 1.4ms preprocess, 53.9ms inference, 1.4ms postprocess per image

การเทรนตัวอย่างนี้ใช้เวลาประมาณ 0.76 ชั่วโมง หรือ 46 นาทีบน CPU และได้ best.pt ขนาด 5.5 MB โดย yolo11n เป็นโมเดลรุ่น nano ระยะเวลาเทรนและความเร็วในการทำนายบนเครื่องของคุณอาจต่างจากตัวอย่าง

ระหว่างเทรน สามารถเปิด runs/detect/train/train_batch0.jpg เพื่อดูตัวอย่างภาพที่ป้อนเข้าโมเดล พร้อมตรวจว่ากรอบและชื่อคลาสตรงกับข้อมูลที่เตรียมไว้:

training batch พร้อมกล่องที่ label

4.3 อ่านผลการเทรน#

ผลประเมินต่อไปนี้วัดจากชุด val จำนวน 20 ภาพที่แยกไว้ ภาพเหล่านี้มาจากชุดภาพ AI เดียวกัน แต่ไม่ได้อยู่ในชุด train:

metricค่า (all)ความหมาย
precision0.993สัดส่วนผลตรวจจับที่ถูกต้องจากผลตรวจจับทั้งหมด
recall1.000สัดส่วนวัตถุที่ตรวจพบจากวัตถุจริงทั้งหมด
mAP500.995ค่า mAP ที่เกณฑ์ IoU 0.5
mAP50-950.970ค่า mAP เฉลี่ยหลายเกณฑ์ IoU ตั้งแต่ 0.5 ถึง 0.95

IoU (Intersection over Union) คือพื้นที่ส่วนที่กรอบทำนายกับกรอบอ้างอิงทับซ้อนกัน หารด้วยพื้นที่รวมของทั้งสองกรอบ มีค่าระหว่าง 0–1 ยิ่งใกล้ 1 ตำแหน่งกรอบยิ่งตรงกัน

mAP (mean Average Precision) สรุปผลจากความสัมพันธ์ระหว่าง precision และ recall แล้วเฉลี่ยทุกคลาส โดย mAP50 ใช้เกณฑ์ IoU 0.5 ในการจับคู่กรอบ ส่วน mAP50-95 เฉลี่ยผลที่เกณฑ์ IoU ตั้งแต่ 0.5 ถึง 0.95 จึงประเมินตำแหน่งกรอบเข้มงวดกว่า ควรอ่านค่าทั้งหมดร่วมกับภาพผลทำนาย

เมื่อแยกตามคลาส ทั้งสองสายพันธุ์ได้ mAP50 เท่ากันที่ 0.995 ส่วน mAP50-95 ของ corgi อยู่ที่ 0.995 และ chihuahua อยู่ที่ 0.944 หากต้องการหาสาเหตุของความต่าง ควรตรวจกรอบทำนายเทียบกับป้ายกำกับในภาพเพิ่มเติม

นอกจากตัวเลข ให้เปิดกราฟและภาพผลทำนายใน runs/detect/train/ ประกอบด้วย ตัวอย่างต่อไปนี้ช่วยให้เห็นภาพรวมของการเทรนและผลตรวจจับ

results.png รวมกราฟ loss ของชุด train และ val รวมถึง precision, recall และ mAP ใช้ดูแนวโน้มว่าความคลาดเคลื่อนลดลงและผลประเมินดีขึ้นอย่างไรตลอดการเทรน:

กราฟสรุปผลการเทรน

confusion matrix แบบ normalize แสดงผลการจำแนกแต่ละคลาส ในตัวอย่างนี้ค่าของทั้งสองสายพันธุ์อยู่บนแนวทแยง และไม่พบการสับสนข้ามคลาสในชุด val:

confusion matrix

ส่วน val_batch0_pred.jpg แสดงกรอบทำนาย ชื่อคลาส และค่า confidence บนภาพในชุด val ให้ตรวจทั้งสายพันธุ์ที่ทำนายและความพอดีของกรอบ:

ผลทำนายบน val set

ผลที่ได้สูงในชุด val นี้ยังไม่เพียงพอที่จะสรุปความแม่นยำในการใช้งานจริง เพราะมีเพียง 10 ภาพต่อคลาสและทั้งหมดเป็นภาพที่สร้างด้วย AI ขั้นต่อไปจึงเป็นการลองกับภาพถ่ายจริงที่แยกจากชุดข้อมูลเดิม

4.4 ทดสอบกับภาพถ่ายจริง#

คัดลอก best.pt มาไว้ในโฟลเดอร์โปรเจกต์ เพื่อใช้ทดสอบและสร้าง API ในหัวข้อ 5 หากเทรนหลายครั้ง ให้เลือกไฟล์จากโฟลเดอร์รอบที่ต้องการ เช่น train, train2 หรือ train3:

# ปรับ train เป็น train2 / train3 ... ตามรอบที่เทรนที่ต้องการ
cp runs/detect/train/weights/best.pt best.pt        # Windows: copy runs\detect\train\weights\best.pt best.pt
bash

ดาวน์โหลดภาพทดสอบด้านล่างมาไว้ในโฟลเดอร์โปรเจกต์ โดยบันทึกเป็น test1.jpg และ test2.jpg ตามลำดับ ทั้งสองเป็นภาพถ่ายจริงที่ไม่ได้ใช้เทรน:

จากนั้นรันคำสั่งทำนายทีละภาพ:

yolo detect predict model=best.pt source=test1.jpg
yolo detect predict model=best.pt source=test2.jpg
bash

แต่ละคำสั่งจะพิมพ์ว่าเจออะไรในภาพ พร้อมเวลา inference และตำแหน่งที่เซฟผล เช่นของ test1.jpg:

image 1/1 C:\yolo-project\test1.jpg: 640x640 1 corgi, 96.9ms
Speed: 10.1ms preprocess, 96.9ms inference, 12.6ms postprocess per image at shape (1, 3, 640, 640)
Results saved to runs\detect\predict

ในตัวอย่างนี้ โมเดลประมวลผลภาพที่ขนาด 640×640 และตรวจพบ corgi 1 ตัว ใช้เวลาทำนาย (inference) ประมาณ 97 มิลลิวินาที ส่วน Results saved to ... ระบุโฟลเดอร์ที่บันทึกภาพผลลัพธ์

ภาพผลลัพธ์จะใช้ชื่อไฟล์เดิมและมีกรอบตรวจจับวาดไว้แล้ว เมื่อใช้คำสั่งข้างต้น โฟลเดอร์ผลลัพธ์จะเพิ่มเลข เช่น predict/, predict2/ และ predict3/ หากยังไม่มีผลจากการรันครั้งก่อน สองคำสั่งนี้จะบันทึกภาพไว้ที่ predict/test1.jpg และ predict2/test2.jpg ให้ดูตำแหน่งจริงจาก log ของแต่ละคำสั่ง:

ทำนายภาพถ่ายจริง corgi ทำนายภาพถ่ายจริง chihuahua

corgi ในตัวอย่างได้ confidence 0.86 และ chihuahua ได้ 0.99 ผลนี้แสดงว่าโมเดลตรวจจับสุนัขในภาพทดสอบทั้งสองได้ แต่ยังไม่เพียงพอที่จะสรุปว่าโมเดลใช้ได้ดีกับภาพถ่ายจริงทั่วไป ควรทดสอบเพิ่มด้วยภาพที่มีมุมกล้อง แสง และพื้นหลังต่างกัน

5. สร้าง API และ deploy บน VPS#

ตอนนี้เรามีโมเดลที่รับภาพและตรวจจับสุนัขได้แล้ว ขั้นต่อไปคือเปิดให้แอปพลิเคชันอื่นเรียกใช้ผ่าน REST API โดยใช้ FastAPI รับภาพที่ endpoint /predict ส่งให้โมเดล best.pt ทำนาย แล้วตอบกลับเป็น JSON

เราจะเริ่มจากทดสอบ API บนเครื่อง จากนั้นเตรียม Docker image และนำไปรันบน VPS ก่อนเชื่อมต่อกับหน้าเว็บแสดงผล

client ส่งภาพเข้า FastAPI แล้วได้ JSON กลับ

5.1 เขียน main.py#

สร้าง main.py ไว้ในโฟลเดอร์เดียวกับ best.pt ที่คัดลอกมาในหัวข้อ 4.4 โค้ดนี้โหลดโมเดลและสร้าง endpoint POST /predict สำหรับรับไฟล์ภาพ แล้วส่งกลับรายการวัตถุที่ตรวจพบ:

from fastapi import FastAPI, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from ultralytics import YOLO
from PIL import Image
import io

app = FastAPI()

# allow browser frontends on other origins to call this API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],      # tighten to your frontend domain in production
    allow_methods=["*"],
    allow_headers=["*"],
)

model = YOLO("best.pt")

@app.post("/predict")
async def predict(file: UploadFile):
    img = Image.open(io.BytesIO(await file.read()))
    r = model(img)[0]
    return [
        {"class": r.names[int(b.cls)], "conf": float(b.conf), "box": b.xyxy[0].tolist()}
        for b in r.boxes
    ]
python

CORSMiddleware กำหนดให้หน้าเว็บที่อยู่คนละ origin เรียก API ผ่านเบราว์เซอร์ได้ ตัวอย่างนี้ใช้ allow_origins=["*"] เพื่อรองรับหน้าเว็บทดสอบในหัวข้อ 5.7 เมื่อนำไปใช้งานจริง ควรกำหนด origin ของ frontend ที่อนุญาตให้ชัดเจน

ทดสอบบนเครื่องโดยรันคำสั่งนี้จากโฟลเดอร์โปรเจกต์:

uvicorn main:app --reload
bash

เปิด Swagger UI ที่ http://127.0.0.1:8000/docs เลือก POST /predict กด Try it out เลือกไฟล์ภาพ แล้วกด Execute:

อัปโหลดภาพผ่าน Swagger UI

API จะตอบกลับเป็นรายการ JSON โดยแต่ละรายการมี class เป็นชื่อคลาส, conf เป็นค่าความมั่นใจ และ box เป็นพิกัดกรอบ [x1, y1, x2, y2] หากไม่พบวัตถุ รายการผลลัพธ์จะว่าง:

ผล JSON ที่ API ตอบกลับ

เมื่อตรวจว่า API รับภาพและส่งผลกลับได้แล้ว ให้เตรียมไฟล์สำหรับรันด้วย Docker

5.2 เตรียม requirements.txt และ Dockerfile#

Docker image จะรวมโค้ด ไลบรารี และไฟล์น้ำหนักไว้ด้วยกัน โดยใช้ requirements.txt ระบุไลบรารี และ Dockerfile กำหนดขั้นตอนติดตั้งกับคำสั่งเริ่ม API

สร้าง requirements.txt ในโฟลเดอร์โปรเจกต์ โดยระบุไลบรารีที่ API ต้องใช้:

ultralytics
fastapi
uvicorn
pillow
python-multipart

หากต้องการบันทึกเวอร์ชันแพ็กเกจจาก environment ที่ใช้อยู่ สามารถใช้คำสั่งต่อไปนี้แทนได้ แต่ไฟล์ที่ได้จะรวมทุกแพ็กเกจใน environment รวมถึงเครื่องมืออย่าง Label Studio จึงควรตรวจรายการก่อนใช้กับ API:

pip freeze > requirements.txt
bash

จากนั้นสร้าง Dockerfile ในโฟลเดอร์เดียวกัน:

FROM python:3.13-slim
RUN apt-get update && apt-get install -y libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
dockerfile

บรรทัดติดตั้ง libgl1 และ libglib2.0-0 เตรียมไลบรารีระบบสำหรับ OpenCV ที่ใช้ร่วมกับ Ultralytics ส่วนคำสั่ง CMD เริ่ม API ที่พอร์ต 8000 ภายใน container

เมื่อจบขั้นตอนนี้ ให้ตรวจว่ามีไฟล์ครบ 4 ไฟล์ ได้แก่ main.py, requirements.txt, Dockerfile และ best.pt เราจะอัปโหลดไฟล์เหล่านี้ไป build image บน VPS

5.3 เตรียม VPS และติดตั้ง Docker#

ฝั่ง VPS จะรับไฟล์โปรเจกต์มา build เป็น Docker image แล้วรัน container โดยเชื่อมพอร์ต 80 ของ VPS เข้ากับพอร์ต 8000 ของ API เพื่อให้เรียกใช้ผ่านเบราว์เซอร์ได้

ภาพรวมการ deploy ขึ้น VPS

ตัวอย่างนี้ใช้ VPS ที่รัน Ubuntu หรือ Docker template ของผู้ให้บริการ แทนที่ <vps-ip> ในคำสั่งด้วย IP ของ VPS แล้วเชื่อมต่อผ่าน SSH:

ssh root@<vps-ip>
bash

ติดตั้ง Docker หากยังไม่ได้ติดตั้งมากับ template:

curl -fsSL https://get.docker.com | sh
bash

5.4 อัปโหลดไฟล์ build และรันบน VPS#

กลับมาที่เครื่องของเรา แล้วอัปโหลด main.py, requirements.txt, Dockerfile และ best.pt ไปยัง /root/yolo-api บน VPS ผ่าน SFTP โดยใช้โปรแกรม เช่น FileZilla, WinSCP หรือ Cyberduck เชื่อมต่อด้วย IP ของ VPS และชื่อผู้ใช้ root จากนั้นสร้างโฟลเดอร์ปลายทางและอัปโหลดไฟล์ทั้งสี่

อัปโหลดไฟล์ขึ้น VPS ด้วย WinSCP

หากต้องการอัปโหลดผ่าน terminal ให้เปิด terminal บนเครื่องของเราในโฟลเดอร์โปรเจกต์ แล้วใช้ scp แทน:

ssh root@<vps-ip> "mkdir -p /root/yolo-api"
scp main.py requirements.txt Dockerfile best.pt root@<vps-ip>:/root/yolo-api/
bash

เมื่ออัปโหลดเสร็จ ให้กลับไปที่ terminal ที่เชื่อมต่อ VPS ผ่าน SSH แล้ว build image และรัน container:

cd /root/yolo-api
docker build -t yolo-api .
docker run -d --restart unless-stopped --name yolo-api -p 80:8000 yolo-api
bash

คำสั่งนี้ตั้งชื่อ container ว่า yolo-api เพื่อใช้อ้างอิงตอนดู log หรืออัปเดต และกำหนด --restart unless-stopped ให้เริ่มทำงานอีกครั้งหลังรีบูต เว้นแต่สั่งหยุดไว้ ส่วน -p 80:8000 เชื่อมพอร์ต 80 ของ VPS กับพอร์ต 8000 ภายใน container

หาก VPS มี firewall ให้เปิดรับการเชื่อมต่อที่พอร์ต 80 ด้วย โดยตั้งค่าผ่านแผงควบคุมของผู้ให้บริการ หรือใช้ ufw allow 80 หากจัดการ firewall ด้วย UFW

5.5 อัปเดตโค้ดหรือโมเดลบน VPS#

เมื่อต้องการอัปเดต main.py หรือเปลี่ยนโมเดล best.pt ให้อัปโหลดไฟล์ใหม่ไปยังโฟลเดอร์เดิมตามหัวข้อ 5.4 จากนั้นรันคำสั่งต่อไปนี้บน VPS เพื่อ build image และแทนที่ container เดิม หากเป็นการ deploy ครั้งแรก สามารถข้ามไปทดสอบในหัวข้อ 5.6 ได้เลย:

cd /root/yolo-api
docker build -t yolo-api .              # rebuild image with the new code
docker rm -f yolo-api                   # stop and remove the old container
docker run -d --restart unless-stopped --name yolo-api -p 80:8000 yolo-api
bash

Docker สามารถใช้ layer cache เพื่อลดเวลาการ build ซ้ำได้ หากแก้เฉพาะโค้ดหรือไฟล์โมเดล โดยที่ layer ก่อนหน้าและ requirements.txt ไม่เปลี่ยน ก็สามารถใช้ผลการติดตั้งไลบรารีเดิมได้ ส่วน docker rm -f yolo-api จะหยุดและลบ container เดิมก่อนเริ่มตัวใหม่ จึงมีช่วงที่ API หยุดให้บริการระหว่างเปลี่ยน container

หลังเริ่ม container ให้ตรวจสถานะและดู log ด้วยคำสั่ง:

docker ps
docker logs yolo-api
bash

5.6 ทดสอบผ่านเบราว์เซอร์#

เปิด http://<vps-ip>/docs ในเบราว์เซอร์ จะเห็น Swagger UI เช่นเดียวกับตอนทดสอบบนเครื่อง เลือก POST /predict กด Try it out อัปโหลดภาพ แล้วกด Execute เพื่อตรวจผล JSON ที่ส่งกลับ

Swagger UI ของ API ที่รันบน VPS

หากได้รับผล JSON แสดงว่า API บน VPS รับภาพและเรียกโมเดลได้แล้ว ขั้นตอนสุดท้ายคือสร้างหน้าเว็บเพื่อแสดงกรอบตรวจจับบนภาพ

5.7 สร้างหน้าเว็บแสดงผลด้วย index.html#

เราจะสร้างหน้าเว็บที่รับไฟล์ภาพ เรียก API แล้ววาดกรอบพร้อมชื่อคลาสและค่า confidence ลงบน <canvas> โดยใช้ค่า box ที่ API ส่งกลับเป็นพิกัดพิกเซล [x1, y1, x2, y2] ของภาพต้นฉบับ และกำหนดขนาด canvas ให้ตรงกับภาพ

ลำดับการทำงานคือ เลือกภาพ → แสดงภาพบน canvas → ส่งภาพไปยัง POST /predict → รับผล JSON → วาดกรอบและชื่อคลาสทับบนภาพ โดยใช้การตั้งค่า CORS จากหัวข้อ 5.1 เพื่อเรียก API ข้าม origin

ขั้นตอนการทำงานฝั่งเบราว์เซอร์ของ index.html

สร้างไฟล์ index.html บนเครื่องของเรา โค้ดนี้ใช้ Bootstrap 5 ผ่าน CDN สำหรับจัดหน้า และใช้ JavaScript เรียก API กับวาดผลตรวจจับ:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>YOLO Detect</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body class="bg-light">
    <div class="container py-5" style="max-width: 900px">
      <h1 class="h3 mb-4 text-center">YOLO Object Detection</h1>

      <div class="card shadow-sm">
        <div class="card-body">
          <input type="file" id="file" accept="image/*" class="form-control mb-3" />
          <div id="status" class="text-muted small mb-3">Select an image to start</div>
          <canvas id="canvas" class="img-fluid rounded border"></canvas>
        </div>
      </div>
    </div>

    <script>
      const API = "http://localhost:8000/predict"; // change to http://<vps-ip>/predict
      const fileInput = document.getElementById("file");
      const canvas = document.getElementById("canvas");
      const statusEl = document.getElementById("status");
      const ctx = canvas.getContext("2d");

      fileInput.addEventListener("change", async () => {
        const file = fileInput.files[0];
        if (!file) return;

        // draw the selected image onto the canvas
        const img = new Image();
        img.src = URL.createObjectURL(file);
        await img.decode();
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        // send the image to the API
        statusEl.textContent = "Detecting...";
        const form = new FormData();
        form.append("file", file); // field name must match the FastAPI parameter
        const res = await fetch(API, { method: "POST", body: form });
        const boxes = await res.json();

        // draw each detection box + label
        ctx.lineWidth = Math.max(2, img.width / 300);
        ctx.font = `${Math.max(16, img.width / 40)}px sans-serif`;
        ctx.textBaseline = "top";
        for (const b of boxes) {
          const [x1, y1, x2, y2] = b.box;
          const label = `${b.class} ${b.conf.toFixed(2)}`;
          ctx.strokeStyle = "#00e676";
          ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
          const tw = ctx.measureText(label).width;
          const th = parseInt(ctx.font, 10);
          ctx.fillStyle = "#00e676";
          ctx.fillRect(x1, y1, tw + 8, th + 6);
          ctx.fillStyle = "#000";
          ctx.fillText(label, x1 + 4, y1 + 3);
        }
        statusEl.textContent = `Found ${boxes.length} object(s)`;
      });
    </script>
  </body>
</html>
html

ก่อนเปิดหน้าเว็บ ให้เปลี่ยนค่า API ให้ตรงกับเซิร์ฟเวอร์ที่ต้องการเรียกใช้:

ตำแหน่งที่รัน APIค่า API
บนเครื่องด้วย Uvicorn ตามหัวข้อ 5.1http://localhost:8000/predict
บน VPS ด้วย -p 80:8000 ตามหัวข้อ 5.4http://<vps-ip>/predict

สำหรับ VPS ให้ใช้พอร์ต 80 ตามที่เปิดไว้ จึงไม่ต้องใส่ :8000 ใน URL เพราะพอร์ต 8000 เป็นพอร์ตภายใน container

บันทึกไฟล์แล้วเปิด index.html ด้วยเบราว์เซอร์ เมื่อเลือกภาพ หน้าเว็บจะส่งภาพไปตรวจจับและแสดงกรอบสีเขียวพร้อมชื่อคลาส ค่า confidence และจำนวนวัตถุที่พบ:

หน้าเว็บเรียก API แล้ววาดกล่องผลตรวจจับ chihuahua

6. ต่อยอดจากระบบตัวอย่าง#

ตอนนี้เรามีทั้งโมเดล YOLO ที่เทรนจากชุดข้อมูลของเรา API ที่รันบน VPS และหน้าเว็บที่แสดงผลตรวจจับ เส้นทางตั้งแต่เตรียมภาพจนถึงเรียกใช้งานจึงเชื่อมต่อกันครบแล้ว

หากต้องการพัฒนาต่อ ให้เริ่มจากเพิ่มภาพถ่ายจริงและทดสอบกับภาพที่หลากหลายขึ้น จากนั้นจึงลองเพิ่มคลาสหรือเปรียบเทียบโมเดลขนาดอื่น โดยดูทั้งคุณภาพผลตรวจจับและความเร็วบน VPS ส่วนการเปิดใช้งานผ่านโดเมนสามารถต่อยอดด้วย HTTPS ผ่าน Caddy หรือ nginx

เทรน YOLO ตรวจจับวัตถุ แล้ว deploy เป็น API บน VPS
ผู้เขียน กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
เผยแพร่เมื่อ September 24, 2026
ลิขสิทธิ์ CC BY-NC-SA 4.0

กำลังโหลดความคิดเห็น...

ความคิดเห็น 0