1 นับวัตถุด้วย AI
ใช้ YOLO ตรวจจับรถในวิดีโอ แล้วนับรถที่ผ่านเส้นนับ เทียบกับการนับด้วยคน
โจทย์
การนับชิ้นงานที่เคลื่อนผ่านจุดหนึ่ง เช่น ขวดบนสายพาน หรือรถที่ผ่านด่าน เป็นงานพื้นฐานของระบบกล้องในอุตสาหกรรม ใบงานนี้ใช้ 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)
(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 |
รถคันเดียวกันถูกตรวจพบในหลาย 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 นับ
- เลือกเฉพาะ Bounding Box ที่จุดกึ่งกลางแนวนอนอยู่ในช่วง
LINE_X0ถึงLINE_X1 - ถ้า Bounding Box คร่อมเส้น (
y1 <= LINE_Y <= y2) และสถานะ “มีรถบนเส้น” ยังเป็นFalseให้นับเพิ่ม 1 และเปลี่ยนสถานะเป็นTrue - ถ้าไม่มี 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 | ||||||
| วิดีโอที่เลือกเพิ่ม |
- เหตุใดต้องมีสถานะ
car_on_lineถ้าไม่มีจะเกิดอะไรกับค่าที่นับได้ - logic นี้นับผิดในกรณีใดบ้าง (เช่น รถ 2 คันทับเส้นพร้อมกัน รถบังกัน)
- ถ้าจะนำระบบนี้ไปนับขวดบนสายพานในโรงงานจริง ยังขาดอะไรบ้าง เขียนเป็นรายการไว้ แล้วกลับมาเทียบเมื่อจบใบงาน 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 จะตอบเรื่องเหล่านี้ทีละข้อ