IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Edge AI trên Pi 5: TensorFlow Lite, MediaPipe, YOLO

Raspberry Pi 5 là một nền tảng edge AI mạnh mẽ với CPU ARM Cortex-A76 tốc độ 2.4GHz. Bài viết này hướng dẫn chi tiết cách chạy TensorFlow Lite, MediaPipe, và YOLO object detection trên Pi 5 — từ cài đặt, code mẫu, đến benchmark hiệu năng thực tế.

Tổng quan

Framework Mục đích Hiệu năng
TensorFlow Lite Object detection, classification, segmentation ~2-15 FPS tùy model
MediaPipe Hand tracking, pose detection, face mesh ~10-30 FPS
YOLOv8n (CPU) Real-time object detection ~2 FPS

Lưu ý: Các con số benchmark trên là chạy CPU thuần (4 cores). Với Hailo-8L NPU (AI Kit), YOLOv8 đạt ~30 FPS — tham khảo bài Pi 5 AI Kit: Chạy YOLOv8 với Hailo-8L.

1. Cài đặt môi trường

Yêu cầu

  • Raspberry Pi 5 (4GB/8GB)
  • Raspberry Pi OS Bookworm (64-bit)
  • Camera Module 3 hoặc USB webcam
  • Nguồn 5V/5A

Cài đặt TensorFlow Lite Runtime

# 1. Kiểm tra Python version
python3 --version
# Phải là Python 3.11 (Bookworm mặc định)

# 2. Cài dependencies
sudo apt update
sudo apt install -y python3-pip python3-opencv python3-picamera2 libatlas-base-dev

# 3. TensorFlow Lite Runtime cho ARM64
pip3 install tflite-runtime --upgrade

# Hoặc cài từ wheel cụ thể
wget https://github.com/nicktajzs/TensorFlow-Lite-Runtime-ARM64/releases/download/v2.17.0/tflite_runtime-2.17.0-cp311-cp311-linux_aarch64.whl
pip3 install tflite_runtime-2.17.0-cp311-cp311-linux_aarch64.whl

# 4. Kiểm tra
python3 -c "import tflite_runtime.interpreter as tflite; print('TFLite OK:', tflite.__version__)"

Cài đặt MediaPipe

# MediaPipe cho ARM64
pip3 install mediapipe --upgrade

# Kiểm tra
python3 -c "import mediapipe as mp; print('MediaPipe OK:', mp.__version__)"

Cài đặt thêm

# Pillow, numpy, các thư viện hỗ trợ
pip3 install numpy pillow matplotlib

# picamera2 (đã có trên Bookworm)
python3 -c "from picamera2 import Picamera2; print('Picamera2 OK')"

2. TensorFlow Lite — Object Detection

Tải model COCO SSD MobileNet

mkdir -p ~/tflite && cd ~/tflite

# Model quantized (tối ưu cho ARM)
wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip
unzip coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip

# Labels
wget -O labels.txt https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/mscoco_complete_label_map.pbtxt

Code Python — Real-time object detection

# tflite_detection.py — TensorFlow Lite object detection
import tflite_runtime.interpreter as tflite
import numpy as np
import cv2
import time
from picamera2 import Picamera2

# ════════════ Cấu hình ════════════

MODEL_PATH = "coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.tflite"
LABELS_PATH = "labels.txt"
CONF_THRESHOLD = 0.5

# ════════════ Load model ════════════

print("🔄 Đang nạp model TFLite...")
interpreter = tflite.Interpreter(model_path=MODEL_PATH)
interpreter.allocate_tensors()

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

_, height, width, _ = input_details[0]['shape']

print(f"✅ Model loaded: {width}x{height}")
print(f"   Input tensor: {input_details[0]['shape']}")
print(f"   Output tensors: {len(output_details)}")

# Load labels
def load_labels(path):
    labels = {}
    with open(path, 'r') as f:
        for line in f:
            if 'id:' in line:
                id_val = int(line.strip().split(':')[1].strip())
            elif 'display_name:' in line:
                name = line.strip().split('"')[1]
                labels[id_val] = name
    return labels

labels = load_labels(LABELS_PATH)
print(f"   {len(labels)} classes")

# ════════════ Camera ════════════

picam2 = Picamera2()
config = picam2.create_video_configuration(
    main={"size": (640, 480)},
    controls={"FrameRate": 30}
)
picam2.configure(config)

# ════════════ Inference ════════════

def detect_objects(frame):
    """Chạy TFLite inference trên frame"""
    # Resize về input size của model
    img = cv2.resize(frame, (width, height))
    img = np.expand_dims(img, axis=0).astype(np.uint8)
    
    # Set input
    interpreter.set_tensor(input_details[0]['index'], img)
    
    # Run inference
    interpreter.invoke()
    
    # Get output
    boxes = interpreter.get_tensor(output_details[0]['index'])[0]
    classes = interpreter.get_tensor(output_details[1]['index'])[0]
    scores = interpreter.get_tensor(output_details[2]['index'])[0]
    num_detections = int(interpreter.get_tensor(output_details[3]['index'])[0])
    
    results = []
    for i in range(num_detections):
        if scores[i] > CONF_THRESHOLD:
            label_id = int(classes[i]) + 1
            label = labels.get(label_id, f"Class {label_id}")
            y1, x1, y2, x2 = boxes[i]
            results.append({
                "label": label,
                "score": float(scores[i]),
                "box": (int(x1 * frame.shape[1]), 
                        int(y1 * frame.shape[0]),
                        int(x2 * frame.shape[1]), 
                        int(y2 * frame.shape[0]))
            })
    
    return results

def draw_detections(frame, detections):
    """Vẽ bounding box lên frame"""
    for d in detections:
        x1, y1, x2, y2 = d["box"]
        color = (0, 255, 0)
        if d["label"] == "person":
            color = (0, 0, 255)
        elif d["label"] in ["car", "truck", "bus"]:
            color = (255, 0, 0)
        
        cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
        label = f"{d['label']} {d['score']:.0%}"
        cv2.putText(frame, label, (x1, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
    return frame

# ════════════ Main loop ════════════

print("✅ Sẵn sàng! Start detection...")
picam2.start()

fps_history = []

while True:
    start = time.time()
    
    # Capture
    frame = picam2.capture_array()
    
    # Detect
    detections = detect_objects(frame)
    
    # Draw
    frame = draw_detections(frame, detections)
    
    # FPS
    fps = 1.0 / (time.time() - start)
    fps_history.append(fps)
    avg_fps = sum(fps_history[-30:]) / min(len(fps_history), 30)
    
    cv2.putText(frame, f"TFLite CPU: {avg_fps:.1f} FPS", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
    cv2.putText(frame, f"Detections: {len(detections)}", (10, 60),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
    
    # Show
    cv2.imshow("TFLite Object Detection", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

picam2.stop()
cv2.destroyAllWindows()

print(f"\n📊 Benchmark: Avg FPS = {sum(fps_history)/len(fps_history):.1f}")

Hiệu năng mong đợi

Model Độ phân giải FPS (Pi 5 CPU)
SSD MobileNet V1 (quantized) 300×300 8-12 FPS
SSD MobileNet V2 (quantized) 300×300 10-15 FPS
EfficientDet-Lite0 320×320 5-8 FPS
EfficientDet-Lite2 448×448 2-4 FPS

3. MediaPipe — Hand Tracking

MediaPipe Hands phát hiện 21 landmarks trên bàn tay với độ chính xác cao.

Code Python — Hand tracking

# mediapipe_hands.py — Hand tracking với MediaPipe
import mediapipe as mp
import cv2
import time
from picamera2 import Picamera2

# ════════════ Khởi tạo MediaPipe ════════════

mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles

hands = mp_hands.Hands(
    static_image_mode=False,
    max_num_hands=2,
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
    model_complexity=1  # 0=light, 1=full
)

# ════════════ Camera ════════════

picam2 = Picamera2()
config = picam2.create_video_configuration(
    main={"size": (640, 480)},
    controls={"FrameRate": 30}
)
picam2.configure(config)

# ════════════ Main loop ════════════

print("🖐️ MediaPipe Hand Tracking — Press 'q' to quit")
picam2.start()

while True:
    start = time.time()
    
    frame = picam2.capture_array()
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    
    # Process với MediaPipe
    result = hands.process(frame_rgb)
    
    # Draw landmarks
    if result.multi_hand_landmarks:
        for hand_landmarks in result.multi_hand_landmarks:
            mp_drawing.draw_landmarks(
                frame,
                hand_landmarks,
                mp_hands.HAND_CONNECTIONS,
                mp_drawing_styles.get_default_hand_landmarks_style(),
                mp_drawing_styles.get_default_hand_connections_style()
            )
        
        # Lấy số ngón tay
        for hand_landmarks in result.multi_hand_landmarks:
            # Đếm ngón tay xòe
            fingers = []
            tips = [4, 8, 12, 16, 20]  # Landmark index của đầu ngón
            
            # Ngón cái
            if hand_landmarks.landmark[4].x < hand_landmarks.landmark[3].x:
                fingers.append(1)
            else:
                fingers.append(0)
            
            # 4 ngón còn lại
            for tip in tips[1:]:
                if hand_landmarks.landmark[tip].y < hand_landmarks.landmark[tip - 2].y:
                    fingers.append(1)
                else:
                    fingers.append(0)
            
            count = sum(fingers)
            cv2.putText(frame, f"Fingers: {count}", (10, 90),
                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
            
            # Tọa độ cổ tay
            wrist = hand_landmarks.landmark[0]
            h, w, _ = frame.shape
            cx, cy = int(wrist.x * w), int(wrist.y * h)
            cv2.circle(frame, (cx, cy), 5, (255, 0, 0), -1)
    
    # FPS
    fps = 1.0 / (time.time() - start)
    cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
    
    cv2.imshow("Hand Tracking", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

picam2.stop()
cv2.destroyAllWindows()
hands.close()

MediaPipe — Pose Detection

# mediapipe_pose.py — Human pose detection
import mediapipe as mp
import cv2
import time
from picamera2 import Picamera2

mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils

pose = mp_pose.Pose(
    static_image_mode=False,
    model_complexity=1,      # 0=light, 1=full, 2=heavy
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5
)

picam2 = Picamera2()
config = picam2.create_video_configuration(
    main={"size": (640, 480)},
    controls={"FrameRate": 30}
)
picam2.configure(config)

print("🧍 Pose Detection — Press 'q' to quit")
picam2.start()

while True:
    start = time.time()
    
    frame = picam2.capture_array()
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    
    result = pose.process(frame_rgb)
    
    if result.pose_landmarks:
        mp_drawing.draw_landmarks(
            frame,
            result.pose_landmarks,
            mp_pose.POSE_CONNECTIONS,
            mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
            mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2)
        )
        
        # Lấy tọa độ mũi (landmark 0)
        nose = result.pose_landmarks.landmark[0]
        h, w, _ = frame.shape
        cx, cy = int(nose.x * w), int(nose.y * h)
        cv2.circle(frame, (cx, cy), 5, (255, 0, 0), -1)
        
        # Phát hiện tư thế raise hands
        left_wrist = result.pose_landmarks.landmark[15]
        right_wrist = result.pose_landmarks.landmark[16]
        
        if left_wrist.y < nose.y and right_wrist.y < nose.y:
            cv2.putText(frame, "BOTH HANDS UP!", (150, 30),
                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
    
    fps = 1.0 / (time.time() - start)
    cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
    
    cv2.imshow("Pose Detection", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

picam2.stop()
cv2.destroyAllWindows()
pose.close()

Benchmark MediaPipe trên Pi 5

Feature Model Complexity FPS (640×480) Ghi chú
Hand Tracking 0 (light) ~25-30 FPS Phát hiện 1 tay
Hand Tracking 1 (full) ~15-20 FPS Phát hiện 2 tay
Pose Detection 0 (light) ~20-25 FPS 33 landmarks
Pose Detection 1 (full) ~12-18 FPS 33 landmarks
Pose Detection 2 (heavy) ~6-10 FPS Chính xác nhất
Face Mesh Default ~15-20 FPS 468 landmarks

4. YOLOv8 trên CPU (ONNX Runtime)

Chạy YOLOv8 trên CPU Pi 5 dùng ONNX Runtime hoặc OpenCV DNN.

Code Python — YOLOv8 với OpenCV DNN

# yolov8_cpu.py — YOLOv8 trên CPU Pi 5
import cv2
import numpy as np
import time
from picamera2 import Picamera2
import urllib.request
import os

# ════════════ Tải model ════════════

YOLO_URL = "https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.onnx"
YOLO_PATH = "yolov8n.onnx"

if not os.path.exists(YOLO_PATH):
    print("⬇️ Đang tải YOLOv8n ONNX model...")
    urllib.request.urlretrieve(YOLO_URL, YOLO_PATH)
    print("✅ OK")

# Load model
net = cv2.dnn.readNetFromONNX(YOLO_PATH)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

# COCO labels
COCO_LABELS = [
    "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck",
    "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",
    "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra",
    "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
    "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove",
    "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
    "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
    "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
    "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
    "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
    "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
    "toothbrush"
]

CONF_THRESHOLD = 0.5
IOU_THRESHOLD = 0.45

# ════════════ Camera ════════════

picam2 = Picamera2()
config = picam2.create_video_configuration(
    main={"size": (640, 640)},
    controls={"FrameRate": 15}
)
picam2.configure(config)

# ════════════ Hàm xử lý ════════════

def detect_yolov8(frame):
    """YOLOv8 inference với OpenCV DNN"""
    h, w = frame.shape[:2]
    
    # Preprocess
    blob = cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRB=True, crop=False)
    net.setInput(blob)
    
    # Inference
    outputs = net.forward()[0]  # (84, 8400)
    
    # Parse output
    boxes = []
    for i in range(outputs.shape[1]):
        scores = outputs[4:, i]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        
        if confidence < CONF_THRESHOLD:
            continue
        
        cx, cy, bw, bh = outputs[:4, i]
        x1 = int((cx - bw/2) * w / 640)
        y1 = int((cy - bh/2) * h / 640)
        x2 = int((cx + bw/2) * w / 640)
        y2 = int((cy + bh/2) * h / 640)
        
        boxes.append({
            "label": COCO_LABELS[int(class_id)],
            "score": float(confidence),
            "box": (x1, y1, x2, y2)
        })
    
    # NMS
    boxes.sort(key=lambda b: b["score"], reverse=True)
    final = []
    for b in boxes:
        if not any(iou(b["box"], fb["box"]) > IOU_THRESHOLD for fb in final):
            final.append(b)
    
    return final

def iou(b1, b2):
    x1 = max(b1[0], b2[0]); y1 = max(b1[1], b2[1])
    x2 = min(b1[2], b2[2]); y2 = min(b1[3], b2[3])
    inter = max(0, x2-x1) * max(0, y2-y1)
    a1 = (b1[2]-b1[0]) * (b1[3]-b1[1])
    a2 = (b2[2]-b2[0]) * (b2[3]-b2[1])
    return inter / (a1 + a2 - inter) if (a1 + a2 - inter) > 0 else 0

# ════════════ Main ════════════

print("🟢 YOLOv8 CPU Detection — Press 'q' to quit")
picam2.start()

while True:
    start = time.time()
    
    frame = picam2.capture_array()
    detections = detect_yolov8(frame)
    
    for d in detections:
        x1, y1, x2, y2 = d["box"]
        cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(frame, f"{d['label']} {d['score']:.0%}", (x1, y1-10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
    fps = 1.0 / (time.time() - start)
    cv2.putText(frame, f"YOLOv8 CPU: {fps:.1f} FPS", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
    
    cv2.imshow("YOLOv8 on Pi 5 CPU", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

picam2.stop()
cv2.destroyAllWindows()

⚠️ Lưu ý: YOLOv8n chạy CPU Pi 5 chỉ đạt ~2 FPS do model 8.9M parameters. Để chạy real-time (≥30 FPS), cần Hailo NPU (AI Kit).

So sánh hiệu năng YOLO inference

Model CPU Pi 5 Hailo-8L NPU Tỉ lệ tăng
YOLOv5n 640px ~3 FPS ~35 FPS 11.7×
YOLOv8n 640px ~2 FPS ~30 FPS 15×
YOLOv8s 640px ~0.8 FPS ~15 FPS 18.8×
YOLOv8m 640px ~0.3 FPS ~8 FPS 26.7×

5. Tối ưu hiệu năng

Giảm độ phân giải

# Thay vì 640×480, dùng 320×240
config = picam2.create_video_configuration(
    main={"size": (320, 240)},  # Nhỏ hơn = nhanh hơn
    controls={"FrameRate": 30}
)
Resolution TFLite FPS MediaPipe Hands FPS
640×480 8-12 15-20
320×240 20-25 25-30
160×120 30+ 30+

Dùng model complexity thấp

# MediaPipe: model_complexity=0 cho speed
hands = mp_hands.Hands(
    model_complexity=0,  # 0=fast, 1=balanced, 2=accurate
    max_num_hands=1      # Chỉ detect 1 tay
)

Threading

# Tách capture và inference trên 2 threads
from threading import Thread
from queue import Queue

frame_queue = Queue(maxsize=2)
result_queue = Queue(maxsize=2)

def capture_thread():
    picam2 = Picamera2()
    picam2.configure(config)
    picam2.start()
    while True:
        frame = picam2.capture_array()
        if not frame_queue.full():
            frame_queue.put(frame)

Xử lý bỏ qua frame (skip frame)

# Chỉ inference mỗi frame thứ N
frame_count = 0
while True:
    frame = picam2.capture_array()
    frame_count += 1
    
    if frame_count % 3 == 0:  # Inference mỗi 3 frame ~ 10 FPS
        detections = detect_objects(frame)

6. Common Mistakes

Sai lầm Hậu quả Giải pháp
Dùng TensorFlow full (không phải TFLite) Chiếm 1GB+ RAM, crash Dùng tflite-runtime (nhẹ)
Không cài libatlas-base-dev Lỗi import numpy/opencv sudo apt install libatlas-base-dev
MediaPipe model_complexity=2 Pose detection ~6 FPS Dùng complexity=0 hoặc 1
Quên swap khi chạy model lớn OOM (Out of Memory) Tăng swap: sudo dphys-swapfile swapoff && edit /etc/dphys-swapfile CONF_SWAPSIZE=2048
Dùng USB 2.0 webcam Camera chậm, FPS thấp Dùng Camera Module 3 (CSI)
Độ phân giải quá cao (1080p) AI chạy <1 FPS Dùng 640×480 hoặc 320×240
Quên tản nhiệt CPU throttle, FPS giảm Heatsink + fan case
Chạy nhiều model cùng lúc CPU 100%, crash Chạy tuần tự, mỗi model cách nhau

7. Ứng dụng thực tế

Smart mirror

# MediaPipe hand tracking + pose detection
# Điều khiển smart mirror bằng cử chỉ tay
# Hiển thị thời tiết, lịch, tin tức

Gesture-controlled robot

# Đếm số ngón tay → gửi lệnh điều khiển
# 1 ngón = tiến, 2 ngón = lùi, 3 ngón = trái, 4 ngón = phải
# Nắm tay = dừng

Fitness tracking

# MediaPipe Pose phát hiện tư thế tập luyện
# Đếm số lần squat, push-up, jumping jack
# Đánh giá kỹ thuật dựa trên angle của khớp

People counter

# YOLO object detection đếm người qua camera
# Hiển thị trên web dashboard (Flask/Node-RED)
# Dùng cho smart retail, office occupancy

8. Chạy tự động với systemd

# Tạo service cho object detection
sudo tee /etc/systemd/system/edge-ai.service << 'EOF'
[Unit]
Description=Edge AI Object Detection on Pi 5
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/tflite
ExecStart=/usr/bin/python3 tflite_detection.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# Kích hoạt
sudo systemctl enable edge-ai
sudo systemctl start edge-ai

# Logs
sudo journalctl -u edge-ai -f

Tổng kết

Raspberry Pi 5 là một nền tảng edge AI cực kỳ linh hoạt:

  • TensorFlow Lite — Object detection, classification ~8-15 FPS với model nhẹ
  • MediaPipe — Hand tracking ~20 FPS, pose detection ~15 FPS
  • YOLOv8 (CPU) — ~2 FPS, cần NPU để đạt real-time

Với CPU mạnh hơn 2-3× so với Pi 4, Pi 5 có thể chạy nhiều tác vụ AI cơ bản mà không cần thêm phần cứng. Khi cần hiệu năng cao hơn (≥30 FPS), kết hợp với Hailo-8L AI Kit là giải pháp tối ưu.

Framework Use case Hiệu năng Khó
TFLite Object detection cơ bản 8-15 FPS Thấp
MediaPipe Hand/pose/face tracking 15-30 FPS Thấp
YOLOv8 (CPU) Detection chính xác ~2 FPS Trung bình
YOLOv8 + Hailo NPU Detection real-time ~30 FPS Trung bình