08 - NVIDIA Jetson Nano

Systemy Wbudowane i Przetwarzanie Brzegowe

Politechnika Poznańska, Instytut Robotyki i Inteligencji Maszynowej

Ćwiczenie laboratoryjne 08: NVIDIA Jetson Nano

Powrót do spisu treści ćwiczeń laboratoryjnych

Wstęp

NVIDIA Jetson Nano jest to mały wydajny komputer jednopłytkowy (SBC - Single Board Computer) przeznaczony do zastosowań związanych z uczeniem maszynowym oraz przetwarzaniem obrazów. Urządzenie to jest wyposażone w procesor graficzny (GPU) NVIDIA Maxwell z 128 rdzeniami CUDA, procesor ARM Cortex-A57 4 rdzeniowy, oraz 4GB pamięci RAM. Urządzenie to jest kompatybilne z wysokowydajną biblioteką TensorRT oraz platformą do obliczeń równoległych CUDA.


Źródło grafiki: Getting Started with Jetson Nano Developer Kit

Na powyższym schemacie wymienione są następujące komponenty: 1. slot na kartę microSD, na której znajduje się system operacyjny 2. rozszerzenie do obsługi 40 pinów GPIO 3. micro-USB do obsługi komunikacji z hostem lub zasilania 4. port do obsługi ethernetu 5. porty USB 3.0 6. port HDMI 7. DisplayPort 8. DC Barrel Jack do zasilania z zewnętrznego zasilacza 5V 4A 9. MIPI CSI-2 port do obsługi kamery

Przygotowanie karty SD z obrazem systemu operacyjnego

Wszystkie niezbędne informacje oraz kroki, które należy wykonać aby przygotować urządzenia typu NVIDIA Jetson Nano można znaleźć w instrukcji Getting Started with Jetson Nano Developer Kit.

UWAGA: karty SD z systemem operacyjnym NVIDIA JetPack zostały już przygotowane przez prowadzącego i nie należy ich nadpisywać. Powyższa instrukcja odnosi się jedynie do użytkowania poza zajęciami.

Uruchomienie systemu

Komputery PC oraz NVIDIA Jetson Nano znajdują się w jednej sieci lokalnej. W celu uzyskania dostępu do urządzenia należy wykonać następujące kroki: 1. Zasilić urządzenie i podłączyć je do sieci lokalnej za pomocą kabla ethernetowego lub poprzez adapter do sieci WIFI. 2. Korzystając z komendy ssh połączyć się z urządzeniem. Hasło i login to swpb. 3. W celu przesyłania obrazu poprzez połączenie SSH należy skorzystać z X11 forwarding. W takim przypadku, przy łączeniu z urządzeniem należy dodać opcję -X. Przykładowo: ssh -X swpb@192.168.55.1

CUDA

NVIDIA CUDA jest to uniwersalna architektura graficznych procesorów wielordzeniowych (GPU) umożliwiająca wykorzystanie ich mocy obliczeniowej do rozwiązywania ogólnych problemów numerycznych w wydajniejszy sposób niż przy użyciu tradycyjnych, sekwencyjnych procesorach ogólnego zastosowania (CPU). Zestaw narzędzi CUDA zawiera biblioteki z akceleracją GPU, kompilator, narzędzia deweloperskie oraz środowisko uruchomieniowe. Powyższe komponenty są wspierane przez powszechnie używane języki programowania takie jak C++ lub Python.

TensorRT

TensorRT to oprogramowanie opracowane przez NVIDIA, które umożliwia optymalizację i przyspieszenie modeli sieci neuronowych na platformach GPU. Dzięki TensorRT można skutecznie wykorzystać moc obliczeniową GPU, osiągając wysoką wydajność i niskie opóźnienia. Oprogramowanie to automatycznie optymalizuje modele, redukując liczbę operacji oraz minimalizując zużycie pamięci, co przekłada się na szybsze i bardziej efektywne przetwarzanie danych.


Źródło grafiki: TensorRT

Zadania do samodzielnej realizacji


Zadanie 1.
Korzystając z interaktywnej instrukcji przetestuj i wyeksportuj model detekcji obiektów YOLOv8 do formatu ONNX. Następnie przetestuj jego działanie w środowisku chmurowym z wykorzystaniem biblioteki ONNX Runtime oraz akceleratorów CPU, CUDA, oraz TensorRT. Sprawdzony model pobierz i prześlij na urządzenie NVIDA Jetson Nano korzystając z polecenia scp.


Zadanie 2.
Uzupełnij poniższy skrypt. Uruchom skrypt na urządzeniu NVIDIA Jetson Nano i sprawdź czas wykonywania modelu z wykorzystaniem biblioteki ONNX Runtime oraz akceleratorów CPU, CUDA, oraz TensorRT.

detector_inference.py
import time

import cv2
import numpy as np
import onnxruntime as ort


MODEL_PATH = 'yolov8n.onnx'
CONF_THRESHOLD = 0.5
IOU_THRESHOLD = 0.1
CLASSES = [
 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 
 'traffic light', 'fire hydrant', 'street sign', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 
 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'hat', 'backpack', 'umbrella', 
 'shoe', 'eye glasses', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 
 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'plate', 
 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 
 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'mirror', 
 'dining table', 'window', 'desk', 'toilet', 'door', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 
 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'blender', 'book', 'clock', 'vase',
 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
]


def nms(boxes, scores):
    # Sort by score
    sorted_indices = np.argsort(scores)[::-1]

    keep_boxes = []
    while sorted_indices.size > 0:
        # Pick the last box
        box_id = sorted_indices[0]
        keep_boxes.append(box_id)

        # Compute IoU of the picked box with the rest
        ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :])

        # Remove boxes with IoU over the threshold
        keep_indices = np.where(ious < IOU_THRESHOLD)[0]

        # print(keep_indices.shape, sorted_indices.shape)
        sorted_indices = sorted_indices[keep_indices + 1]

    return keep_boxes


def compute_iou(box, boxes):
    # Compute xmin, ymin, xmax, ymax for both boxes
    xmin = np.maximum(box[0], boxes[:, 0])
    ymin = np.maximum(box[1], boxes[:, 1])
    xmax = np.minimum(box[2], boxes[:, 2])
    ymax = np.minimum(box[3], boxes[:, 3])

    # Compute intersection area
    intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin)

    # Compute union area
    box_area = (box[2] - box[0]) * (box[3] - box[1])
    boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
    union_area = box_area + boxes_area - intersection_area

    # Compute IoU
    iou = intersection_area / union_area

    return iou


def xywh2xyxy(xywh):
    xyxy = np.copy(xywh)
    xyxy[..., 0] = xywh[..., 0] - xywh[..., 2] / 2  # top left x
    xyxy[..., 1] = xywh[..., 1] - xywh[..., 3] / 2  # top left y
    xyxy[..., 2] = xywh[..., 0] + xywh[..., 2] / 2  # bottom right x
    xyxy[..., 3] = xywh[..., 1] + xywh[..., 3] / 2  # bottom right y
    return xyxy


def draw_predictions(image, boxes, scores, class_ids, indices, fps):
    cv2.putText(image, f'FPS: {fps}', (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.60, [225, 255, 255], thickness=2)
    for (bbox, score, label) in zip(xywh2xyxy(boxes[indices]), scores[indices], class_ids[indices]):
        bbox = bbox.round().astype(np.int32).tolist()
        cls_id = int(label)
        cls = CLASSES[cls_id]
        color = (0,255,0)
        cv2.rectangle(image, tuple(bbox[:2]), tuple(bbox[2:]), color, 2)
        cv2.putText(image,
                    f'{cls}: {int(score*100)}%', (bbox[0], bbox[1] - 2),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.60, [225, 255, 255],
                    thickness=1)


def preprocess(image, input_shape):
  """
  Funkcja przetwarzania wstępnego realizuje następujące kroki:
    1. zmienia rozmiar obrazu wejściowego, korzystając ze zmiennej _input_shape_,
    2. jeśli wykorzystywana jest biblioteka OpenCV to konwertuje z formatu BGR do RGB
    3. skaluje wartości w obrazie do zakresu <0; 1>,
    4. zmienia układ wymiarów z _HWC_ na _CHW_ (z formatu _channel last_ na _channel first_)
    5. dodaje dodatkowy wymiar do tensora, tak aby był w formacie _NHWC_ (_N_=1)
    6. ustawia format danych na _float32_
  """
  ##### Student code #####

  input_tensor = None

  ########################

  return input_tensor


def main():
    ##### Student code #####

    providers = ...

    ort_session = ...

    ########################
    print(f'Model loaded with {providers[0]}')

    model_inputs = ort_session.get_inputs()
    input_names = [model_inputs[i].name for i in range(len(model_inputs))]
    input_shape = model_inputs[0].shape

    model_output = ort_session.get_outputs()
    output_names = [model_output[i].name for i in range(len(model_output))]

    input_height, input_width = input_shape[2:]

    print('Model warm-up...')
    for _ in range(10):
        _ = ort_session.run(output_names, {input_names[0]: np.random.rand(1, 3, input_height, input_width).astype(np.float32)})[0]
    print('Model ready!')

    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Cannot open camera")
        exit()

    prev_frame_time = new_frame_time = 0

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Can't receive frame. Exiting...")
            break

        new_frame_time = time.time()
        image_height, image_width = frame.shape[:2]

        input_tensor = preprocess(frame, input_shape=(input_width, input_height))
        outputs = ort_session.run(output_names, {input_names[0]: input_tensor})[0]

        predictions = np.squeeze(outputs).T

        # Filter out object confidence scores below threshold
        scores = np.max(predictions[:, 4:], axis=1)
        predictions = predictions[scores > CONF_THRESHOLD, :]
        scores = scores[scores > CONF_THRESHOLD]

        # Get the class with the highest confidence
        class_ids = np.argmax(predictions[:, 4:], axis=1)

        # Get bounding boxes for each object
        boxes = predictions[:, :4]

        # Rescale box
        input_shape = np.array([input_width, input_height, input_width, input_height])
        boxes = np.divide(boxes, input_shape, dtype=np.float32)
        boxes *= np.array([image_width, image_height, image_width, image_height])
        boxes = boxes.astype(np.int32)

        # Apply non-maxima suppression to suppress weak, overlapping bounding boxes
        indices = nms(boxes, scores)

        # Calculate frames per second rate
        fps = round(1 / (new_frame_time - prev_frame_time), 1)

        draw_predictions(frame, boxes, scores, class_ids, indices, fps)
        cv2.imshow('Predictions', frame)

        if cv2.waitKey(1) == ord('q'):
           break

        prev_frame_time = new_frame_time


if __name__ == '__main__':
    main()


Zadanie 3.
Korzystając ze skryptu z laboratorium 07 - Inferencja sieci neuronowej z wykorzystaniem Raspberry Pi i ONNX Runtime oraz modelu ONNX (w formacie FLOAT32) z instukcji 06 - Metody optymalizacji sieci neuronowych przetestuj działanie i sprawdź czas wykonywania modelu segmentacyjnego na platformie Jetson Nano. Porównaj dostępne w bibliotece ONNX Runtime akceleratory - CPU, CUDA, oraz TensorRT.