← Quay lại Blog
aiai-perceptionnvidiatutorial

Deploy YOLOv8 lên Jetson Orin Nano trong 30 phút

Hướng dẫn step-by-step deploy YOLOv8 với TensorRT lên NVIDIA Jetson Orin Nano — từ export model đến inference real-time 60 FPS.

Nguyễn Anh Tuấn15 tháng 2, 20269 phút đọc
Deploy YOLOv8 lên Jetson Orin Nano trong 30 phút

Deploy YOLOv8 lên Jetson Orin Nano — Tại sao lại quan trọng?

Deploy YOLOv8 lên Jetson Orin Nano là bước quyết định để đưa AI perception từ laptop vào robot thật. Nếu bạn đang build robot di động, drone, hoặc hệ thống kiểm tra chất lượng, bạn cần inference real-time ngay trên edge device — không thể chờ round-trip tới cloud 200ms mỗi frame.

Trong bài này, mình sẽ đi từ A đến Z: chuẩn bị hardware, export model, tối ưu TensorRT, build camera pipeline với GStreamer, và cuối cùng wrap thành ROS 2 node sẵn sàng chạy trên robot.

NVIDIA Jetson Orin Nano developer kit cho AI perception robotics

Prerequisites — Bạn cần gì?

Hardware

Thiết bị Ghi chú
NVIDIA Jetson Orin Nano 8GB Developer Kit hoặc module + carrier board
MicroSD 64GB+ (UHS-I) Hoặc NVMe SSD cho performance tốt hơn
USB camera hoặc CSI camera Logitech C920 hoặc IMX219 CSI
Nguồn 5V/4A USB-C Đi kèm Dev Kit

Software

# Cài JetPack (nếu chưa flash)
sudo apt update && sudo apt install -y nvidia-jetpack

# Verify CUDA
nvcc --version
# cuda_12.2.r12.2/compiler.33191640_0

# Cài Ultralytics
pip3 install ultralytics

# Verify PyTorch + CUDA
python3 -c "import torch; print(f'CUDA: {torch.cuda.is_available()}, Device: {torch.cuda.get_device_name(0)}')"
# CUDA: True, Device: Orin (nvgpu)

Bước 1: Export YOLOv8 sang TensorRT Engine

Đây là bước quan trọng nhất. TensorRT sẽ tối ưu model cho GPU Ampere trên Orin Nano, bao gồm layer fusion, kernel auto-tuning, và precision calibration.

Export FP16 (khuyến nghị)

from ultralytics import YOLO

# Load pretrained model (hoặc model bạn đã train custom)
model = YOLO("yolov8n.pt")  # nano version, tối ưu cho edge

# Export sang TensorRT FP16
# Quá trình này mất 5-10 phút trên Orin Nano
model.export(
    format="engine",
    device=0,
    half=True,        # FP16 precision
    imgsz=640,        # Input resolution
    workspace=4,      # GB workspace cho TensorRT optimizer
    verbose=True
)
print("Export xong! File: yolov8n.engine")

Export INT8 (maximum FPS)

INT8 cần calibration dataset — một tập 200-500 ảnh đại diện cho data thực tế:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")

# Export INT8 với calibration
model.export(
    format="engine",
    device=0,
    int8=True,
    data="coco128.yaml",  # Dataset cho calibration
    imgsz=640,
    workspace=4
)

Lưu ý quan trọng: File .engine được build riêng cho từng GPU architecture. Engine build trên Orin Nano không chạy được trên RTX 4090 và ngược lại. Luôn export trực tiếp trên target device.

Bước 2: Benchmark — FP32 vs FP16 vs INT8

Mình đã benchmark trên Jetson Orin Nano 8GB, power mode MAXN (15W), jetson_clocks enabled:

Format Model FPS (640x640) Latency mAP@50 GPU Memory
PyTorch FP32 yolov8n 15 FPS 67ms 37.3 1.2GB
TensorRT FP32 yolov8n 32 FPS 31ms 37.3 680MB
TensorRT FP16 yolov8n 58 FPS 17ms 37.2 420MB
TensorRT INT8 yolov8n 72 FPS 14ms 36.8 310MB
PyTorch FP32 yolov8s 8 FPS 125ms 44.9 2.1GB
TensorRT FP16 yolov8s 35 FPS 29ms 44.8 780MB

Kết luận: FP16 là sweet spot — tăng gần 4x so với PyTorch mà gần như không mất accuracy. INT8 chỉ nên dùng khi cần >60 FPS hoặc chạy nhiều model cùng lúc.

# Benchmark nhanh trên Jetson
yolo benchmark model=yolov8n.engine imgsz=640 half=True device=0

Bước 3: Python Inference Script hoàn chỉnh

"""
YOLOv8 TensorRT Inference trên Jetson Orin Nano
Tối ưu cho real-time robot perception
"""
import cv2
import time
import numpy as np
from ultralytics import YOLO
from collections import deque

class JetsonDetector:
    def __init__(self, model_path: str = "yolov8n.engine", conf: float = 0.5):
        self.model = YOLO(model_path)
        self.conf = conf
        self.fps_buffer = deque(maxlen=30)

    def detect(self, frame: np.ndarray) -> list[dict]:
        """Detect objects và trả về list of detections."""
        start = time.perf_counter()

        results = self.model.predict(
            frame,
            conf=self.conf,
            verbose=False,
            half=True
        )

        elapsed = time.perf_counter() - start
        self.fps_buffer.append(1.0 / elapsed)

        detections = []
        for box in results[0].boxes:
            det = {
                "class_id": int(box.cls[0]),
                "class_name": results[0].names[int(box.cls[0])],
                "confidence": float(box.conf[0]),
                "bbox": box.xyxy[0].cpu().numpy().tolist(),  # [x1, y1, x2, y2]
            }
            detections.append(det)

        return detections

    @property
    def fps(self) -> float:
        return np.mean(self.fps_buffer) if self.fps_buffer else 0.0


# --- Main ---
if __name__ == "__main__":
    detector = JetsonDetector("yolov8n.engine", conf=0.45)
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        detections = detector.detect(frame)

        # Vẽ bounding boxes
        for det in detections:
            x1, y1, x2, y2 = map(int, det["bbox"])
            label = f'{det["class_name"]} {det["confidence"]:.2f}'
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
            cv2.putText(frame, label, (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

        # FPS overlay
        cv2.putText(frame, f"FPS: {detector.fps:.1f}", (10, 30),
                     cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

        cv2.imshow("YOLOv8 Jetson", frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

Bước 4: Camera Pipeline với GStreamer

USB camera qua OpenCV hoạt động, nhưng CSI camera cần GStreamer pipeline để decode hardware-accelerated trên Jetson. Đây cũng là cách tối ưu nhất cho cả USB camera.

CSI Camera (IMX219/IMX477)

def gstreamer_pipeline(
    sensor_id=0,
    capture_width=1280,
    capture_height=720,
    display_width=1280,
    display_height=720,
    framerate=60,
    flip_method=0,
):
    """Build GStreamer pipeline string cho CSI camera trên Jetson."""
    return (
        f"nvarguscamerasrc sensor-id={sensor_id} ! "
        f"video/x-raw(memory:NVMM), width=(int){capture_width}, "
        f"height=(int){capture_height}, framerate=(fraction){framerate}/1 ! "
        f"nvvidconv flip-method={flip_method} ! "
        f"video/x-raw, width=(int){display_width}, height=(int){display_height}, "
        f"format=(string)BGRx ! videoconvert ! "
        f"video/x-raw, format=(string)BGR ! appsink drop=1"
    )

# Sử dụng
cap = cv2.VideoCapture(gstreamer_pipeline(), cv2.CAP_GSTREAMER)

USB Camera (tối ưu với V4L2)

def usb_gstreamer_pipeline(device="/dev/video0", width=1280, height=720, fps=30):
    """GStreamer pipeline cho USB camera, decode trên GPU."""
    return (
        f"v4l2src device={device} ! "
        f"video/x-raw, width={width}, height={height}, framerate={fps}/1 ! "
        f"videoconvert ! video/x-raw, format=BGR ! appsink drop=1"
    )

cap = cv2.VideoCapture(usb_gstreamer_pipeline(), cv2.CAP_GSTREAMER)

Multi-camera setup

# Chạy 2 camera song song
cap_left = cv2.VideoCapture(gstreamer_pipeline(sensor_id=0), cv2.CAP_GSTREAMER)
cap_right = cv2.VideoCapture(gstreamer_pipeline(sensor_id=1), cv2.CAP_GSTREAMER)

# Hoặc dùng threading để không block
import threading

class CameraThread:
    def __init__(self, pipeline):
        self.cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)
        self.frame = None
        self.running = True
        self.thread = threading.Thread(target=self._update, daemon=True)
        self.thread.start()

    def _update(self):
        while self.running:
            ret, self.frame = self.cap.read()

    def read(self):
        return self.frame

    def stop(self):
        self.running = False
        self.cap.release()

Camera pipeline AI perception cho robot di động

Bước 5: Deploy thành ROS 2 Node

Đây là phần cuối — wrap detector thành ROS 2 node để integrate vào robot stack.

"""
ROS 2 YOLOv8 Detection Node cho Jetson Orin Nano
Publish detections lên topic /yolo/detections
"""
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose
from cv_bridge import CvBridge
import cv2
import numpy as np
from ultralytics import YOLO


class YoloDetectionNode(Node):
    def __init__(self):
        super().__init__("yolo_detection_node")

        # Parameters
        self.declare_parameter("model_path", "yolov8n.engine")
        self.declare_parameter("confidence", 0.5)
        self.declare_parameter("input_topic", "/camera/image_raw")

        model_path = self.get_parameter("model_path").value
        self.conf = self.get_parameter("confidence").value
        input_topic = self.get_parameter("input_topic").value

        # Load TensorRT model
        self.model = YOLO(model_path)
        self.bridge = CvBridge()

        # Subscriber & Publisher
        self.sub = self.create_subscription(
            Image, input_topic, self.image_callback, 10
        )
        self.pub = self.create_publisher(Detection2DArray, "/yolo/detections", 10)
        self.img_pub = self.create_publisher(Image, "/yolo/annotated", 10)

        self.get_logger().info(
            f"YOLOv8 node started — model: {model_path}, conf: {self.conf}"
        )

    def image_callback(self, msg: Image):
        # Convert ROS Image → OpenCV
        frame = self.bridge.imgmsg_to_cv2(msg, "bgr8")

        # Inference
        results = self.model.predict(frame, conf=self.conf, verbose=False, half=True)

        # Build Detection2DArray message
        det_array = Detection2DArray()
        det_array.header = msg.header

        for box in results[0].boxes:
            det = Detection2D()
            x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()

            # Bounding box (center + size format)
            det.bbox.center.position.x = float((x1 + x2) / 2)
            det.bbox.center.position.y = float((y1 + y2) / 2)
            det.bbox.size_x = float(x2 - x1)
            det.bbox.size_y = float(y2 - y1)

            # Classification result
            hyp = ObjectHypothesisWithPose()
            hyp.hypothesis.class_id = str(int(box.cls[0]))
            hyp.hypothesis.score = float(box.conf[0])
            det.results.append(hyp)

            det_array.detections.append(det)

        self.pub.publish(det_array)

        # Publish annotated image (optional, cho visualization)
        annotated = results[0].plot()
        self.img_pub.publish(self.bridge.cv2_to_imgmsg(annotated, "bgr8"))


def main(args=None):
    rclpy.init(args=args)
    node = YoloDetectionNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

Launch file

# launch/yolo_detection.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package="yolo_perception",
            executable="yolo_node",
            name="yolo_detection",
            parameters=[{
                "model_path": "/opt/models/yolov8n.engine",
                "confidence": 0.5,
                "input_topic": "/camera/image_raw",
            }],
            output="screen",
        ),
    ])
# Build và chạy
cd ~/ros2_ws
colcon build --packages-select yolo_perception
source install/setup.bash
ros2 launch yolo_perception yolo_detection.launch.py

Tips tối ưu thêm

Power management

# MAXN mode (15W) — hiệu năng cao nhất
sudo nvpmodel -m 0
sudo jetson_clocks

# Kiểm tra nhiệt độ (throttle ở 80°C)
cat /sys/devices/virtual/thermal/thermal_zone*/temp
# Chia cho 1000 để ra °C

Pre-process trên GPU

# Dùng cv2.cuda thay vì CPU resize
gpu_frame = cv2.cuda_GpuMat()
gpu_frame.upload(frame)
gpu_resized = cv2.cuda.resize(gpu_frame, (640, 640))
frame_resized = gpu_resized.download()

Monitor resource usage

# Jetson stats — tương tự htop nhưng cho Jetson
pip3 install jetson-stats
jtop

Tổng kết

Pipeline hoàn chỉnh: Camera → GStreamer → YOLOv8 TensorRT → ROS 2 topic — tất cả chạy ở 58+ FPS trên Jetson Orin Nano 8GB với FP16. Đủ nhanh cho robot di động, drone, và hệ thống kiểm tra công nghiệp.

Key takeaways:


Bài viết liên quan

Bài viết liên quan

TutorialSim-to-Real Pipeline: Từ training đến robot thật
simulationsim2realtutorialPhần 5

Sim-to-Real Pipeline: Từ training đến robot thật

End-to-end guide: train policy trong sim, evaluate, domain randomization, deploy lên robot thật và iterate.

2/4/202615 phút đọc
Sim-to-Real Transfer: Train simulation, chạy thực tế
ai-perceptionresearchrobotics

Sim-to-Real Transfer: Train simulation, chạy thực tế

Kỹ thuật chuyển đổi mô hình từ simulation sang robot thật — domain randomization, system identification và best practices.

1/4/202612 phút đọc
TutorialBắt đầu với MuJoCo: Cài đặt đến mô phỏng robot đầu tiên
simulationmujocotutorialPhần 2

Bắt đầu với MuJoCo: Cài đặt đến mô phỏng robot đầu tiên

Hands-on tutorial MuJoCo — cài đặt, tạo MJCF model, simulate robot arm và visualize kết quả với Python.

30/3/202611 phút đọc