1 นับวัตถุด้วย AI

ใช้ YOLO ตรวจจับรถในวิดีโอ แล้วนับรถที่ผ่านเส้นนับ เทียบกับการนับด้วยคน

Author

ผศ.ดร.ศุภชัย วรพจน์พิศุทธิ์

Published

15 ตุลาคม 2569

Scene + CameraPixelsFeaturesMeasurandCalibrationUncertaintyDecision

โจทย์

การนับชิ้นงานที่เคลื่อนผ่านจุดหนึ่ง เช่น ขวดบนสายพาน หรือรถที่ผ่านด่าน เป็นงานพื้นฐานของระบบกล้องในอุตสาหกรรม ใบงานนี้ใช้ YOLO ซึ่งเป็นโมเดล AI ที่เทรนมาแล้ว ตรวจจับรถในวิดีโอ แล้วเขียน logic นับรถที่ผ่านเส้น สุดท้ายเทียบค่าที่นับได้กับการนับด้วยคน

Image -> NumPy array -> YOLO -> Bounding Box           (ส่วนที่ 1)
Video -> frame by frame -> YOLO -> vehicles in view    (ส่วนที่ 2)
Counting line -> counting logic -> Count -> Error      (ส่วนที่ 3)

เมื่อจบใบงาน นักศึกษาควรสามารถ

  • อ่านภาพและวิดีโอด้วย OpenCV และอธิบายว่าภาพคือ NumPy array ที่เก็บสีแบบ BGR
  • ใช้ YOLO ตรวจจับวัตถุ และอ่านค่า Bounding Box กับ confidence
  • เขียนเงื่อนไขนับวัตถุที่ผ่านเส้น และคำนวณ error เทียบค่าอ้างอิง

ไฟล์ที่ใช้: car.jpg, sports.jpg และวิดีโอถนนจาก Pexels (ค้นด้วยคำว่า car road เลือกวิดีโอที่ถนนตั้งฉากหรือขนานกับขอบภาพ) ดูรายการใน data/README.md

ใบบันทึกผล: ดาวน์โหลด PDF เปิดบน tablet แล้วเขียนผลลงในตารางและกรอบคำตอบ ส่งเป็น PDF เมื่อจบคาบ

โค้ดตั้งต้น

รันโค้ดนี้ให้ได้ผลก่อนเริ่มส่วนที่ 1 ดาวน์โหลดได้ที่ code/01_start.py โค้ดนี้รันได้ด้วยตัวเอง ไม่ต้องใช้โค้ดจากใบงานก่อนหน้า แก้ SOURCE ให้ตรงกับกล้องหรือไฟล์ที่ใช้ (ใบงาน 0)

"""Worksheet 1 starter: detect objects in every frame of a video with YOLO.

Run: python 01_start.py      (press q in the image window to quit)
"""
import cv2
from ultralytics import YOLO

SOURCE = "road_03.mp4"          # a video file, or 0 for a webcam
SCALE = 4                       # shrink each frame by this factor

model = YOLO("yolo26n.pt")      # downloaded automatically on first run
cap = cv2.VideoCapture(SOURCE)

while True:
    ret, frame = cap.read()
    if not ret:                 # end of the video, or no camera
        break
    frame = cv2.resize(frame, (frame.shape[1] // SCALE, frame.shape[0] // SCALE))

    results = model(frame, verbose=False)
    view = results[0].plot()    # frame with boxes drawn

    cv2.imshow("Worksheet 1", view)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
ปุ่ม ทำอะไร
q หยุดโปรแกรม

ตรวจว่าพร้อม: เห็นวิดีโอพร้อมกรอบรอบรถ

ส่วนที่ 1: ภาพคือตัวเลข

อ่านภาพและดูข้อมูลของภาพ

import cv2

img = cv2.imread("sports.jpg")
print(img.shape)        # (height, width, channels)
print(img.dtype)        # uint8 = 0..255

cv2.imshow("Image", img)
cv2.waitKey(0)          # wait for any key
cv2.destroyAllWindows()

OpenCV เก็บภาพเป็น NumPy array ขนาด (height, width, 3) แต่ละ pixel มี 3 ค่า เรียงเป็น B, G, R (ไม่ใช่ R, G, B)

Importantพิกัด (x, y) กับ img[y, x]

array ใช้ แถวก่อนคอลัมน์ จึงเขียน img[y, x] แต่ฟังก์ชันวาดของ OpenCV (cv2.line, cv2.rectangle, cv2.putText) และค่า Bounding Box ใช้ (x, y) ทุกใบงานในชุดนี้เขียนพิกัดเป็น (x, y) และใช้ img[y, x] เฉพาะตอนเข้าถึง array

เปิดดู pixel

small = cv2.resize(img, (img.shape[1] // 4, img.shape[0] // 4))   # (width, height)
print(small[100, 200])                  # B, G, R at x=200, y=100

small[50:100, 50:150] = [0, 0, 255]     # paint a red block: rows 50-99, columns 50-149
cv2.imshow("Small", small)
cv2.imshow("Red channel", small[:, :, 2])
cv2.waitKey(0)
รายการ ค่า
Height x Width (pixel)
จำนวน channel
จำนวนตัวเลขทั้งภาพ
ขนาดหลังย่อ 4 เท่า

ตรวจจับวัตถุด้วย YOLO

from ultralytics import YOLO

model = YOLO("yolo26n.pt")              # downloaded automatically on first run

img = cv2.imread("car.jpg")
img = cv2.resize(img, (img.shape[1] // 4, img.shape[0] // 4))
results = model(img)

for obj in results[0].boxes:
    x1, y1, x2, y2 = map(int, obj.xyxy[0])
    name = model.names[int(obj.cls[0])]
    print(name, round(float(obj.conf[0]), 2), (x1, y1), (x2, y2))

cv2.imshow("Detection", results[0].plot())
cv2.waitKey(0)
ภาพ จำนวนที่ตรวจพบ ชนิด ตัวอย่าง Bounding Box confidence
car.jpg
sports.jpg

ส่วนที่ 2: วิดีโอคือภาพหลายภาพ

cap = cv2.VideoCapture("road_03.mp4")

while True:
    ret, frame = cap.read()
    if not ret:                 # end of the video
        break
    frame = cv2.resize(frame, (frame.shape[1] // 4, frame.shape[0] // 4))
    results = model(frame, verbose=False)
    print(len(results[0].boxes), "objects")

    cv2.imshow("Video", results[0].plot())
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

เปลี่ยนตัวหารที่ใช้ย่อภาพ แล้วสังเกตความเร็วและจำนวนรถที่ตรวจพบ

ย่อภาพ ขนาด frame ความเร็ว (ช้า/ปานกลาง/เร็ว) จำนวนรถใน frame ตัวอย่าง
หาร 2
หาร 4
หาร 8
Noteคำถาม

รถคันเดียวกันถูกตรวจพบในหลาย frame ถ้ารวมจำนวนที่ตรวจพบทุก frame จะได้จำนวนรถที่ผ่านหรือไม่ เพราะเหตุใด

ส่วนที่ 3: เส้นนับและ logic นับ

กำหนดเส้นนับ

เส้นนับคือ ตำแหน่งตัดสินใจ รถที่ Bounding Box ทับเส้นถือว่ากำลังผ่านจุดตรวจ เลือกตำแหน่งที่รถผ่านชัดและไม่มีรถจอดทับ

LINE_Y = 300                     # y of the counting line (pixel, after resizing)
LINE_X0, LINE_X1 = 250, 400      # x range of the lane to count

# inside the loop, after resizing
cv2.line(frame, (LINE_X0, LINE_Y), (LINE_X1, LINE_Y), (255, 0, 0), 3)

logic นับ

  1. เลือกเฉพาะ Bounding Box ที่จุดกึ่งกลางแนวนอนอยู่ในช่วง LINE_X0 ถึง LINE_X1
  2. ถ้า Bounding Box คร่อมเส้น (y1 <= LINE_Y <= y2) และสถานะ “มีรถบนเส้น” ยังเป็น False ให้นับเพิ่ม 1 และเปลี่ยนสถานะเป็น True
  3. ถ้าไม่มี Bounding Box ใดคร่อมเส้นแล้ว ให้เปลี่ยนสถานะกลับเป็น False เพื่อรอรถคันต่อไป
car_on_line = False     # is a car on the line now?
count = 0

# inside the loop, after results = model(frame)
on_line_now = False
for obj in results[0].boxes:
    x1, y1, x2, y2 = map(int, obj.xyxy[0])
    cx = (x1 + x2) // 2

    # 1. is the box inside the lane?           ______________________________
    # 2. does the box cover the line?          ______________________________
    #    -> on_line_now = True

# 3. count when a car arrives on the line      ______________________________
#    -> count += 1
car_on_line = on_line_now

cv2.putText(frame, f"Count: {count}", (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)

เทียบกับค่าอ้างอิง

ให้ 2 คนนับรถที่ผ่านเส้นเดียวกันด้วยตนเอง ใช้ค่าเฉลี่ยเป็นค่าอ้างอิง

e = N_{\mathrm{measured}} - N_{\mathrm{reference}} \qquad e_{\mathrm{rel}} = \frac{|e|}{N_{\mathrm{reference}}} \times 100\%

วิดีโอ ผู้นับ 1 ผู้นับ 2 Reference Measured e e_{\mathrm{rel}} (%)
road_03.mp4
วิดีโอที่เลือกเพิ่ม
Noteคำถาม
  1. เหตุใดต้องมีสถานะ car_on_line ถ้าไม่มีจะเกิดอะไรกับค่าที่นับได้
  2. logic นี้นับผิดในกรณีใดบ้าง (เช่น รถ 2 คันทับเส้นพร้อมกัน รถบังกัน)
  3. ถ้าจะนำระบบนี้ไปนับขวดบนสายพานในโรงงานจริง ยังขาดอะไรบ้าง เขียนเป็นรายการไว้ แล้วกลับมาเทียบเมื่อจบใบงาน 5

ส่วนขยาย: ติดตามวัตถุด้วย ID

logic ข้างบนใช้สถานะตัวเดียว จึงนับผิดเมื่อมีรถหลายคันบนเส้นพร้อมกัน ระบบนับจริงใช้ tracking ซึ่งให้ ID ประจำวัตถุตลอดวิดีโอ แล้วนับเมื่อจุดศูนย์กลางของ ID หนึ่ง ข้ามเส้น จากด้านหนึ่งไปอีกด้าน

last_y = {}             # track id -> previous center y
counted = set()

results = model.track(frame, persist=True, verbose=False)
if results[0].boxes.id is not None:
    for box, tid in zip(results[0].boxes.xyxy, results[0].boxes.id.int().tolist()):
        x1, y1, x2, y2 = map(int, box)
        cy = (y1 + y2) // 2
        prev = last_y.get(tid)
        if prev is not None and prev < LINE_Y <= cy and tid not in counted:
            counted.add(tid)            # crossed downward
        last_y[tid] = cy
count = len(counted)

ทบทวน

  • ภาพคือ NumPy array (height, width, 3) แบบ BGR, เข้าถึงด้วย img[y, x] แต่วาดด้วย (x, y)
  • YOLO ให้ Bounding Box, ชนิด และ confidence ของวัตถุในแต่ละ frame แต่ ไม่ได้นับ ให้ การนับต้องมี logic และสถานะ
  • ค่าที่ระบบวัดได้ต้องเทียบกับค่าอ้างอิงเสมอ
  • AI ให้ผลเร็ว แต่บอกไม่ได้ว่าทำไมพลาด และค่าที่ได้เป็น pixel ไม่ใช่หน่วยจริง ใบงาน 2–5 จะตอบเรื่องเหล่านี้ทีละข้อ