前言

在 Hailo 的 AI 晶片上運行神經網路模型時,不能直接使用原始的模型檔案,需要使用 Hailo Dataflow Compiler 將模型轉換為 Hailo 晶片可執行格式(HEF)。
Dataflow Compiler 負責最佳化模型結構與資料流,提升推論效率並降低功耗,使訓練好的 AI 模型能在 Hailo 硬體上高效運行。
主要功能:
- 支援 TensorFlow、ONNX 模型匯入
- 自動圖最佳化與分割
- 產生 Hailo 執行檔(HEF)
- 提供資源使用與效能預估報告
支援多層模型分析與調校
獲得HEF後,下一步就是用 Hailo API 去運行模型。
由於我們是透過 NT98336 去操控 Hailo 硬體,所以必須先以 NT98336 的環境去編譯 Hailo 的 driver 和 HailoRT (Hailo 提供的執行時函式庫),用於在裝置端載入 HEF 檔並進行高效能推論。
HailoRT 裡有包含 Hailo 各語言的 API,有 Python、C、C++ 等 ,這篇使用的是 C++。
獲取 YOLOV8 模型
由 Hailo Model Zoo 下載 YOLOV8m pretrained weight,下載下來的檔案是 ONNX 格式,另外還有一個 NMS config 檔。
下載頁面有附上模型的來源連結,以 YOLOV8m 為例就是 ultralytics,可以藉由來源知道推論時的參數。
如以下參考範例的前處理部分:
def preprocess(self) -> Tuple[np.ndarray, Tuple[int, int]]:
"""
Preprocess the input image before performing inference.
This method reads the input image, converts its color space, applies letterboxing to maintain aspect ratio,
normalizes pixel values, and prepares the image data for model input.
Returns:
image_data (np.ndarray): Preprocessed image data ready for inference with shape (1, 3, height, width).
pad (Tuple[int, int]): Padding values (top, left) applied during letterboxing.
"""
# Read the input image using OpenCV
self.img = cv2.imread(self.input_image)
# Get the height and width of the input image
self.img_height, self.img_width = self.img.shape[:2]
# Convert the image color space from BGR to RGB
img = cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB)
img, pad = self.letterbox(img, (self.input_width, self.input_height))
# Normalize the image data by dividing it by 255.0
image_data = np.array(img) / 255.0
# Transpose the image to have the channel dimension as the first dimension
image_data = np.transpose(image_data, (2, 0, 1)) # Channel first
# Expand the dimensions of the image data to match the expected input shape
image_data = np.expand_dims(image_data, axis=0).astype(np.float32)
# Return the preprocessed image data
return image_data, pad
由此前處理函數可以得知以下資訊:
- 模型輸入的圖片格式是 RGB
- 圖片有經過 letterbox 處理,圖片不會形變。Padding value=(114,114,114)。
mean subtract=0、normalize=255
Hailo Dataflow Compiler

1. ONNX inference
可以直接使用上面提到的 ONNX inference 範例,或是參考範例去自己撰寫推論程式,確認模型預測結果正確再進行下一步的轉換。
考慮到 Hailo 內建的前處理沒有 padding 的功能,所以需要自行加入。加入的位置可以分為兩種:

前者是在板端取得影像後,用C或其他方式加入padding,再餵給 Hailo 模型。
考慮到用板端 CPU 加入 padding 可能會較慢,所以這邊第二種方式 - 將 padding 加在模型中。

模型的原始輸入長寬是 W=640, H=360,所以在原圖的上下各加 H=140 的 padding,補成 W=640, H=640。
另外原本前處理過程中 padding 補的值是 114,但由於第二種方式是先 normalize 才 padding,所以 padding 的值也要做 normalize - 所以是 114/255 = 0.447。
依照上述前處理處理圖片,並輸入修改過的 ONNX 模型用 onnxrunime 做推論,確認預測結果是正確的。
2. Parsing
依照 Dataflow Compiler 輸出 log 的建議,將模型的輸出裁切到指定的六個卷積層(即輸出分類分數和預測框座標的那六個 head)。
之後用到的 Hailo 內建 YOLOv8m 的後處理程式,輸入就是這六個卷積層。下圖是經過 parsing 後,模型的最前面和最尾端:




後續的後處理部分(如下圖)使用 Python 寫完,確認預測結果和原始 ONNX 的輸出結果相同。

3. Optimize
Optimize 的內容分為兩個部分: 修改模型和增加模型精度/壓縮模型。
修改模型
前面提到的前處理部分,可以使用 Hailo 的 model script 功能,將前處理加在模型前面。
這邊使用 mean=[0.0, 0.0, 0.0]、std=[255.0, 255.0, 255.0],是由前面來源程式前處理得出的資訊。
model_script_lines = [
"normalization1 = normalization({}, {})\\n".format(str([0.0, 0.0, 0.0]), str([255.0, 255.0, 255.0])),
]
另外在模型的最後加入 NMS:
momodel_script_lines.append("nms_postprocess('{}', meta_arch=yolov8)\\n".format(nms_cfg_path))
模型的架構選擇 yolov8,nms_cfg_path 是和 ONNX 模型一起載下來的 nms config 檔案路徑,內容如下:
{
"nms_scores_th": 0.2,
"nms_iou_th": 0.6,
"image_dims": [
640,
640
],
"max_proposals_per_class": 100,
"classes": 80,
"regression_length": 16,
"background_removal": false,
"background_removal_index": 0,
"bbox_decoders": [
{
"name": "yolov8m/bbox_decoder57",
"stride": 8,
"reg_layer": "yolov8m_cut/conv57",
"cls_layer": "yolov8m_cut/conv58"
},
{
"name": "yolov8m/bbox_decoder70",
"stride": 16,
"reg_layer": "yolov8m_cut/conv70",
"cls_layer": "yolov8m_cut/conv71"
},
{
"name": "yolov8m/bbox_decoder82",
"stride": 32,
"reg_layer": "yolov8m_cut/conv82",
"cls_layer": "yolov8m_cut/conv83"
}
]
}
其中 nms_score_th, nms_iou_th 可以根據需要去調整,而 bbox decoder 部分的名稱須和 parsing 後模型的輸出節點名稱一致,後處理和 NMS 才會正確運作。
將 Parsing 後模型存成 har 檔,再解壓縮此檔,用 Netron 打開 [model name].hn 後,就可以看到輸出節點名稱,如下圖:

優化模型/壓縮模型
Hailo 的模型優化程度有分 0~4 級,越高級,模型優化的程度越大,但也需要比較多的資料去訓練,轉換模型的時間也會比較久。如果發現模型精度不夠的話,可以選擇比較高的優化層級。
Hailo 的模型壓縮程度分為 0~4 級,越高級,模型壓縮的程度越大,推論的速度會越快,模型檔案大小也越小。但也可能造成模型精度不夠,所以建議搭配比較高的優化層級。
這邊選擇 optimization level = 1,compression level = 0,batch_size=1。這邊的 batch size 是指優化的過程中每次運算幾筆,如果用來訓練的 GPU 記憶體不足,可以將 batch size 調小。
model_script_lines.append(
"model_optimization_flavor(optimization_level={}, compression_level={}, batch_size={})\\n".format(1, 0, 1),
)
確認預測結果
在這個階段確認預測結果是很重要的,因為模型的輸入、輸出、模型精度和 parsing 時相比,可能會有產生變化。
舉例來說,用 python 做圖片前處理 → 用 model script 做前處理、用 python 做模型後處理 → 用 model script 做後處理、模型精度由 float32 → int8,所以做推論檢查結果是必要的。
模型後處理的部分,由於用了 Hailo 內建針對 YOLOv8 的後處理和 NMS,所以輸出不會是原本的 (84, 8400),而是 (80, 5, 100)。
原始輸出 / Hailo NMS 後的輸出


每個維度的意義如下:
| 長度 | 意義 | nms config 中的名稱 |
| 80 | 物體類別數量 | "classes" |
| 5 | 依序是 ymin, xmin, ymax, xmax, score 這五個值。 座標的值是 0~1,要自行換算回原本圖片大小。 | 無 |
| 100 | 每個類別最多輸出幾個框 (分數高的優先) | "max_proposals_per_class" |
4. Compile
確定前面優化後的結果 ok 後,就可以將模型編譯為 HEF 檔,也就是可以在 Hailo 硬體上執行的模型檔。
Hailo C++ API

基礎概念
Hailo 跑模型的方式,是先建立一個 Vdevice,並使用 HEF 中的資料去設定這個 Vdevice,相當於有了一個 network group,這個 network group 中有將轉換好的模型綁定在 Vdevice 上。
auto vdevice = VDevice::create();
auto network_group = configure_network_group(*vdevice.value());
設定輸入輸出資料流
接下來根據 network group 中模型輸入輸出資料的 shape、格式,為模型建立對應的 input、output stream。
這邊設定格式 = HAILO_FORMAT_TYPE_UINT8,是因為輸入資料是 RGB888 的圖片,值域是 0~255。預設的資料順序是 NHWC,也就是 opencv 讀取圖片的順序,如果確定和自己要使用的輸入順序一樣,就不用特別修改。
也可以在建立 Vstream 後,印出設定好的參數來確認,例如像圖片格式或量化資訊。
# get parameters
auto input_vstream_params = network_group.value()->make_input_vstream_params({}, HAILO_FORMAT_TYPE_UINT8, HAILO_DEFAULT_VSTREAM_TIMEOUT_MS, HAILO_DEFAULT_VSTREAM_QUEUE_SIZE);
# set parameters if need
# ...
# default setting of dimension order is HAILO_FORMAT_ORDER_NHWC
# create input stream
auto input_vstreams = VStreamsBuilder::create_input_vstreams(*network_group.value(), *input_vstream_params);
# print parameters
auto quant_info = input_vstreams.value()[0].get_quant_infos();
std::cout << "quant info" << quant_info[0].qp_zp << quant_info[0].qp_scale << std::endl;
auto buf_info = input_vstreams.value()[0].get_user_buffer_format();
std::cout << "buf info" << buf_info.type << buf_info.order << std::endl;
後處理
再來就是板端輸出的資料。和 Dataflow compiler 輸出的形狀 (80, 5, 100) 不同,板端輸出的預測資料是按照物體類別。
第一筆資料會是第一個類別預測框的數量。如果是 0 的話,那下一筆資料就是第二個類別預測框的數量;如果是 n 的話,代表這個類別預測出 n 個框,就往後取 n*5 筆資料,框的 5 筆資料的順序是 y min, x min, y max, 分類分數。重複持續這個步驟直到遍歷完所有類別。
另外輸入圖片前面有經過 padding,所以在計算座標時要將 padding 的寬高減掉,換算成原本圖片的比例,才是正確的座標。
while (class_id<NUM_CLS)
{
// Lets check how many prediction we have for current class
size_t bbox_num = (size_t)host_data[infer_result_ptr];
if (bbox_num)
{
std::cout << "class id=" << class_id << ", bbox num=" << bbox_num << std::endl;
// point to first bbox of this class
infer_result_ptr++;
// For each box lets obtain its value
for (size_t i = 0; i < bbox_num; i++)
{
float y_min = (host_data[infer_result_ptr++]* MODEL_H - pad_h) / IN_H * IMG_H;
float x_min = (host_data[infer_result_ptr++]* MODEL_W - pad_w) / IN_W * IMG_W;
float y_max = (host_data[infer_result_ptr++]* MODEL_H - pad_h) / IN_H * IMG_H;
float x_max = (host_data[infer_result_ptr++]* MODEL_W - pad_w) / IN_W * IMG_W;
float score = host_data[infer_result_ptr++];
printf("class=%d, score=%.2f, x1y1x2y2=(%.0f,%.0f,%.0f,%.0f)\\n",
class_id, score, x_min, y_min, x_max, y_max);
}
class_id++;
}
else
{
// No box, let's move pointer to point to next class
// std::cout << "class id=" << class_id << ", bbox num=" << bbox_num << std::endl;
infer_result_ptr++;
class_id++;
}
}
板端結果
輸入的圖片是以下這張:

經過 resize 後的圖片 / 經過 padding 之後的圖片分別是以下這樣:


板端輸出結果:
root@NVTEVM:/mnt/sd/hailo$ ./vstreams_example
input dimension order:0
quant info01
buf info11
out info:801000
is nms:1
out frame size=160320
in frame size=691200, read count=1, data size=691200
host data size: 160320
class id=0, bbox num=1
class=0, score=0.653584, x1y1x2y2=(411.295105,176.469574,464.450531,335.425629)
class id=56, bbox num=4
class=56, score=0.873893, x1y1x2y2=(361.553802,245.822861,414.377930,359.939880)
class=56, score=0.873893, x1y1x2y2=(293.240021,244.682877,352.341064,361.477722)
class=56, score=0.804128, x1y1x2y2=(406.900665,246.173965,441.395111,350.217346)
class=56, score=0.229641, x1y1x2y2=(605.263062,344.131622,639.606628,401.352020)
class id=58, bbox num=2
class=58, score=0.618550, x1y1x2y2=(225.565735,199.580795,267.379395,240.284622)
class=58, score=0.437059, x1y1x2y2=(333.365936,198.335266,369.140442,258.700653)
class id=60, bbox num=3
class=60, score=0.594344, x1y1x2y2=(319.164978,253.414352,440.177490,361.077820)
class=60, score=0.480047, x1y1x2y2=(460.838989,392.677612,640.098938,474.594635)
class=60, score=0.400020, x1y1x2y2=(321.528564,255.731003,389.850616,362.502838)
class id=62, bbox num=2
class=62, score=0.903268, x1y1x2y2=(5.459776,187.548935,154.427689,295.019165)
class=62, score=0.308433, x1y1x2y2=(557.459534,235.226852,639.649902,320.040375)
class id=72, bbox num=2
class=72, score=0.525070, x1y1x2y2=(489.745239,193.772583,512.715515,322.100586)
class=72, score=0.282730, x1y1x2y2=(445.716278,191.540573,512.652588,321.692108)
class id=74, bbox num=1
class=74, score=0.596326, x1y1x2y2=(448.367432,135.705215,461.649811,159.726837)
class id=75, bbox num=5
class=75, score=0.763001, x1y1x2y2=(549.617371,334.043182,587.285828,452.867737)
class=75, score=0.477802, x1y1x2y2=(241.101868,221.628113,253.103348,239.250519)
class=75, score=0.414836, x1y1x2y2=(167.387421,263.066193,185.631714,302.091888)
class=75, score=0.318535, x1y1x2y2=(351.765869,243.634598,362.438721,260.351318)
class=75, score=0.211122, x1y1x2y2=(360.254456,241.549683,374.494354,260.354523)
Inference finished successfully
root@NVTEVM:/mnt/sd/hailo$
將座標畫到原本的圖上:

評論