張高興的 Raspberry Pi AI 開發指南:(二)使用 Python 和 HailoRT 進行實時目標檢測

张高兴發表於2024-12-09

目錄
  • Python 環境配置
  • 實現 USB 攝像頭的目標檢測
  • 參考

在上一篇部落格中,探討了使用 rpicam-apps 透過 JSON 檔案配置並執行目標檢測示例程式。雖然這種方法可以實現有效的檢測,但它限制了開發者直接在程式碼中利用檢測結果的能力。因此,在本篇部落格中,將深入探討如何藉助 HailoRT Python API 呼叫神經處理單元(NPU),以實現在 Python 程式中的目標檢測功能。

Python 環境配置

在上一篇部落格中已經安裝了 hailo-all,這其中包含了 Hailo NPU 的所有必要元件。然而,根據硬體和作業系統需求,可能需要單獨安裝或更新驅動程式。對於非 Raspberry Pi 裝置或當遇到驅動版本不相容的問題時,此時可以登入 Hailo 的網站 https://hailo.ai/developer-zone/software-downloads,並選擇適合系統的驅動程式進行下載與安裝。

例如,如果正在使用基於 arm64 架構的 Ubuntu 作業系統,並且需要 4.19.0 版本的驅動,那麼可以下載相應的 PCIe 驅動包和 HailoRT 包,並執行以下命令完成安裝:

sudo apt purge -y hailo-all  # 解除安裝現有的整合包
sudo dpkg -i hailort-pcie-driver_4.19.0_all.deb  # 安裝新的驅動
sudo dpkg -i hailort_4.19.0_arm64.deb  # 安裝 HailoRT

為了能夠在 Python 中呼叫 NPU,還需要安裝 Python 相關庫。同樣地,在 Hailo 的官方網站中找到對應 Python 版本的 .whl 檔案,並按照下面的步驟建立虛擬環境並安裝必要的軟體包:

conda create -n hailort python=3.10  # 建立虛擬環境
conda activate hailort  # 啟用虛擬環境
pip install hailort-4.19.0-cp310-cp310-linux_aarch64.whl  # 安裝 HailoRT Python 包

還需要安裝 OpenCV 對影像進行處理。由於 OpenCV 無法讀取 Raspberry Pi 的 CSI 攝像頭,如果需要使用請額外安裝 picamera2rpi-libcamera

pip install opencv-python
pip install picamera2 rpi-libcamera

實現 USB 攝像頭的目標檢測

為了讓目標檢測更加實用,需要將攝像頭獲取的實時影片流作為輸入,並在每幀影像上應用深度學習模型來識別物件。無論是否使用 Hailo-8 進行目標檢測,都需要遵循以下步驟來編寫程式碼。

  1. 開啟攝像頭;
  2. 載入目標檢測模型;
  3. 處理影片流,顯示結果。

這裡提供一個基本的程式碼框架,下面將逐步完成這個程式碼。

import cv2

# TODO: 載入模型

# 開啟預設攝像頭
cap = cv2.VideoCapture(0)

while True:
    # 讀取幀
    ret, frame = cap.read()
    if not ret:
        break
        
    # TODO: 進行推理
        
    # 顯示幀
    cv2.imshow('Detections', frame)
        
    # 按下 'q' 鍵退出迴圈
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 釋放攝像頭並關閉視窗
cap.release()
cv2.destroyAllWindows()

首先來完成第一個 TODO 的內容 載入模型 。在程式碼的頂部引入 HailoRT 中必要的類。

import numpy as np
from hailo_platform import HEF, Device, VDevice, InputVStreamParams, OutputVStreamParams, FormatType, HailoStreamInterface, InferVStreams, ConfigureParams

在 Hailo NPU 上執行的是 .hef 的模型檔案,Hailo 的 GitHub 倉庫 https://github.com/hailo-ai/hailo_model_zoo 提供了大部分主流的預編譯模型,可以直接下載使用。這裡使用 YOLOv8s 作為測試。

# COCO 資料集的標籤
class_names = ['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']
# 載入 YOLOv8s 模型
hef_path = 'yolov8s.hef'
hef = HEF(hef_path)

模型載入完成後,還需要對 Hailo 裝置進行一些配置。

# 初始化 Hailo 裝置
devices = Device.scan()
target = VDevice(device_ids=devices)
# 配置網路組
configure_params = ConfigureParams.create_from_hef(hef, interface=HailoStreamInterface.PCIe)
network_group = target.configure(hef, configure_params)[0]
network_group_params = network_group.create_params()
# 獲取輸入輸出流資訊
input_vstream_info = hef.get_input_vstream_infos()[0]
output_vstream_info = hef.get_output_vstream_infos()[0]
# 建立輸入輸出虛擬流引數
input_vstreams_params = InputVStreamParams.make_from_network_group(network_group, quantized=False, format_type=FormatType.FLOAT32)
output_vstreams_params = OutputVStreamParams.make_from_network_group(network_group, quantized=False, format_type=FormatType.FLOAT32)

到這裡第一個 TODO 的內容已經完成,下面來完成第二個 TODO 的內容 進行推理 。在推理之前,需要對輸入模型中的影像進行變換,調整為模型輸入的大小。

# 對影像進行預處理
resized_frame = cv2.resize(frame, (input_vstream_info.shape[0], input_vstream_info.shape[1]))
input_data = {input_vstream_info.name: np.expand_dims(np.asarray(resized_frame), axis=0).astype(np.float32)}

影像調整完成後,使用 infer() 方法進行推理。tf_nms_format 引數控制結果的輸出形式,預設為 False,輸出 Hailo 格式的資料,一個 numpy.ndarray 列表,每個元素代表類的檢測結果,其格式為 [number_of_detections,BBOX_PARAMS];值為 True 時輸出 TensorFlow 格式的資料,numpy.ndarray 型別的值,其格式為 [class_count, BBOX_PARAMS, detections_count]

# 建立輸入輸出虛擬流並推理
with InferVStreams(network_group, input_vstreams_params, output_vstreams_params, tf_nms_format = True) as infer_pipeline:
    with network_group.activate(network_group_params):
        output_data = infer_pipeline.infer(input_data)

推理後需要對結果進行解析,不論是哪種型別的格式,BBOX_PARAMS 都是歸一化後的值。因此需要計算原始影像和輸入影像的比例,將結果逆歸一化,然後再畫出檢測框。

colors = np.random.uniform(0, 255, size=(len(class_names), 3))

# 根據座標畫出檢測框
def draw_bboxes(image, bboxes, confidences, class_ids, class_names, colors):
    for i, bbox in enumerate(bboxes):
        x1, y1, x2, y2 = bbox
        label = f'{class_names[class_ids[i]]}: {confidences[i]:.2f}'
        color = colors[class_ids[i]]
        cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
        cv2.putText(image, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

# 影像縮放比例
scale_x = frame.shape[1] / input_vstream_info.shape[1]
scale_y = frame.shape[0] / input_vstream_info.shape[0]

# 提取檢測框座標、類別等資訊,並在原始幀上繪製
for key in output_data.keys():
    num_classes, bbox_params, num_detections = output_data[key][0].shape

    boxes = []
    confidences = []
    class_ids = []

    for class_id in range(num_classes):
        for detection_id in range(num_detections):
            bbox = output_data[key][0][class_id, :, detection_id]
            if bbox[4] > 0.5:
                x1, y1, x2, y2, confidence = bbox[:5]

                x1 = int(x1 * input_vstream_info.shape[0] * scale_x)
                y1 = int(y1 * input_vstream_info.shape[1] * scale_y)
                x2 = int(x2 * input_vstream_info.shape[0] * scale_x)
                y2 = int(y2 * input_vstream_info.shape[1] * scale_y)
                    
                print(f'{class_names[class_id]}: {[x1, y1, x2, y2]} {bbox[:5]}')

                boxes.append([x1, y1, x2, y2])
                confidences.append(float(confidence))
                class_ids.append(class_id)

    draw_bboxes(frame, boxes, confidences, class_ids, class_names, colors)

到此,第二個 TODO 的內容也已實現,完整的程式如下:

import cv2
import numpy as np
from hailo_platform import HEF, Device, VDevice, InputVStreamParams, OutputVStreamParams, FormatType, HailoStreamInterface, InferVStreams, ConfigureParams

class_names = ['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']

colors = np.random.uniform(0, 255, size=(len(class_names), 3))

# 根據座標畫出檢測框
def draw_bboxes(image, bboxes, confidences, class_ids, class_names, colors):
    for i, bbox in enumerate(bboxes):
        x1, y1, x2, y2 = bbox
        label = f'{class_names[class_ids[i]]}: {confidences[i]:.2f}'
        color = colors[class_ids[i]]
        cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
        cv2.putText(image, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

# 載入YOLOv8模型
hef_path = 'yolov8s.hef'
hef = HEF(hef_path)
# 初始化Hailo裝置
devices = Device.scan()
target = VDevice(device_ids=devices)
# 配置網路組
configure_params = ConfigureParams.create_from_hef(hef, interface=HailoStreamInterface.PCIe)
network_group = target.configure(hef, configure_params)[0]
network_group_params = network_group.create_params()
# 獲取輸入輸出流資訊
input_vstream_info = hef.get_input_vstream_infos()[0]
output_vstream_info = hef.get_output_vstream_infos()[0]
# 建立輸入輸出虛擬流引數
input_vstreams_params = InputVStreamParams.make_from_network_group(network_group, quantized=False, format_type=FormatType.FLOAT32)
output_vstreams_params = OutputVStreamParams.make_from_network_group(network_group, quantized=False, format_type=FormatType.FLOAT32)

# 使用攝像頭0作為影片源
cap = cv2.VideoCapture(0)

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

    # 對影像進行預處理
    resized_frame = cv2.resize(frame, (input_vstream_info.shape[0], input_vstream_info.shape[1]))
    input_data = {input_vstream_info.name: np.expand_dims(np.asarray(resized_frame), axis=0).astype(np.float32)}
    # 建立輸入輸出虛擬流並推理
    with InferVStreams(network_group, input_vstreams_params, output_vstreams_params, tf_nms_format = True) as infer_pipeline:
        with network_group.activate(network_group_params):
            output_data = infer_pipeline.infer(input_data)

    # 影像縮放比例
    scale_x = frame.shape[1] / input_vstream_info.shape[1]
    scale_y = frame.shape[0] / input_vstream_info.shape[0]

    # 提取邊界框、類別等資訊,並在原始幀上繪製
    for key in output_data.keys():
        num_classes, bbox_params, num_detections = output_data[key][0].shape

        boxes = []
        confidences = []
        class_ids = []

        for class_id in range(num_classes):
            for detection_id in range(num_detections):
                bbox = output_data[key][0][class_id, :, detection_id]
                if bbox[4] > 0.5:
                    x1, y1, x2, y2, confidence = bbox[:5]

                    x1 = int(x1 * input_vstream_info.shape[0] * scale_x)
                    y1 = int(y1 * input_vstream_info.shape[1] * scale_y)
                    x2 = int(x2 * input_vstream_info.shape[0] * scale_x)
                    y2 = int(y2 * input_vstream_info.shape[1] * scale_y)

                    print(f'{class_names[class_id]}: {[x1, y1, x2, y2]} {bbox[:5]}')

                    boxes.append([x1, y1, x2, y2])
                    confidences.append(float(confidence))
                    class_ids.append(class_id)

        draw_bboxes(frame, boxes, confidences, class_ids, class_names, colors)

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

# 釋放資源
cap.release()
cv2.destroyAllWindows()

程式效果如下:

Hailo 的 GitHub 倉庫中也提供了其他型別的應用,更多用法請檢視 https://github.com/hailo-ai/Hailo-Application-Code-Examples 以及官方文件。

參考

  1. Hailo Documentation:https://hailo.ai/developer-zone/documentation/
  2. Hailo Application Code Examples:https://github.com/hailo-ai/Hailo-Application-Code-Examples

相關文章