安裝準備
本部分主要介紹在使用工具鏈前必須的環(huán)境準備工作,包含開發(fā)機部署(個人電腦)和開發(fā)板部署(例如旭日開發(fā)板等包含BPU設備)兩個部分。
開發(fā)機部署(個人電腦)
官方的示例教程的開發(fā)機都是Linux系統(tǒng),實際上Windows系統(tǒng)也是可以的。最建議的方式是利用docker,模型轉(zhuǎn)換過程主要還是基于CPU,用不到GPU,所以用docker就夠了。
(1)安裝docker
考慮到用戶多數(shù)是基于個人電腦,所以相關環(huán)境的配置都是基于Windows的。相關文檔內(nèi)提供了Docker Desktop Installer.exe安裝文件(見地平線開發(fā)者社區(qū)),安裝之后,用管理員方式啟動得到如下界面。
我們可以從地平線天工開物cpu docker hub獲取部署所需要的CentOS Docker鏡像。使用最新鏡像v1.13.6,以管理員模式運行CMD,輸入docker,可以顯示出docker的幫助信息。
選擇最新版本,則在cmd中輸入命令docker pull openexplorer/ai_toolchain_centos_7:v1.13.6,即可自動開始docker的安裝。
安裝成功之后,即可在docker中查看成功安裝的工具鏈鏡像:
(2)配置天工開物OpenExplorer
OpenExplorer工具包的下載,需要wget支持,wget的下載鏈接為GNU Wget for Windows,安裝好之后即可在cmd中通過如下命令下載工具包,解壓后,工具包的內(nèi)容如下所示,如果需要其他版本的,可以參考官網(wǎng)信息資料下載專區(qū)。
docker除了要掛載OpenExplorer工具包,還要掛載數(shù)據(jù)集文件夾,通過如下指令可以下載官方提供的數(shù)據(jù)集,或者從相關文檔中的的OpenExplorer/dataset文件夾中下載,下載之后記得解壓。
# cifar wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/cifar-10.tar.gz # cityscapes wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/cityscapes.tar.gz # coco wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/coco.tar.gz # imagenet wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/imagenet.tar.gz # VOC wget -c ftp://vrftp.horizon.ai/Open_Explorer/eval_dataset/VOC.tar.gz
(PS.由于作者在Windows下解壓導致部分軟連接消失,因此補充一些必要的軟連接更新)
# 重新構建model_zoo軟連接 rm /open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/model_zoo ln -s /open_explorer/ddk/samples/ai_toolchain/model_zoo /open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/model_zoo
(3) 啟動Docker
按照教程,啟動docker要執(zhí)行run_docker.sh,可直接按照本文教程直接配置好指令即可。在進入docker之前,先記錄兩個內(nèi)容:
天工開物OpenExplorer根目錄:我的環(huán)境下是"D:\05 - 項目\01 - 旭日x3派\horizon_xj3_open_explorer_v2.2.3_20220617",記得加雙引號防止出現(xiàn)空格,該目錄要掛載在docker中/open_explorer目錄下;
dataset根目錄:我的環(huán)境下是"D:\01 - datasets",記得加雙引號防止出現(xiàn)空格,該目錄需要掛載在docker中的/data/horizon_x3/data目錄下;
*輔助文件夾根目錄:官方教程其實是沒有這個過程的,我把這個掛載在docker里,就是充當個類似U盤的介質(zhì)。比如在我的環(huán)境下是"D:\05 - 項目\01 - 旭日x3派\BPUCodes",我可以在windows里面往這個文件夾拷貝數(shù)據(jù),這些數(shù)據(jù)就可以在docker中使用,在docker中的路徑為/data/horizon_x3/codes。
那么,在cmd(管理員)中輸入如下指令即可進入docker(切記要確保剛剛安裝的軟件docker desktop是開啟的),值得注意的是CMD不支持換行,記得刪掉后面的\然后整理為一行,這時我們可以看到由命令行掛載的3個目錄。
import cv2 # 打開攝像頭并顯示 docker run -it --rm \ -v "D:\05 - 項目\01 - 旭日x3派\horizon_xj3_open_explorer_v2.2.3_20220617":/open_explorer \ -v "D:\01 - datasets":/data/horizon_x3/data \ -v "D:\05 - 項目\01 - 旭日x3派\BPUCodes":/data/horizon_x3/codes \ openexplorer/ai_toolchain_centos_7:v1.13.6
至此,已經(jīng)成功通過Docker鏡像進入了完整的工具鏈開發(fā)環(huán)境??梢枣I入 hb_mapper --help 命令驗證下是否可以正常得到幫助信息,hb_mapper 是工具鏈的一個常用工具, 在后文的模型轉(zhuǎn)換部分對其有詳細介紹。
除了通過掛載個額外的文件夾來實現(xiàn)文件的拷貝,還有一個方法可以直接將文件拷貝到目標目錄。
假如我們要拷貝一個文件"C:\Users\Zhaoxi-Li\Downloads\Pangolin-0.8.tar.gz"到docker中的/root/downloads下(目錄要存在),那么用管理員權限新開一個cmd,輸入docker ps,記錄CONTAINER ID,然后按照docker cp 本地文件的路徑 container_id:的方式輸入docker cp "C:\Users\Zhaoxi-Li\Downloads\Pangolin-0.8.tar.gz" 677de3a8b719:/root/downloads即可完成文件拷貝。
開發(fā)板部署(旭日3派為例)
在使用之前,一定要按照教程多方位玩轉(zhuǎn)《地平線新發(fā)布AIoT開發(fā)板——旭日X3派(Sunrise x3 Pi)》(可于「地平線開發(fā)者社區(qū)」-「開發(fā)者論壇」搜索查看)完成系統(tǒng)的啟動。
工具鏈的部分補充工具未包含在系統(tǒng)鏡像中,這些工具已經(jīng)放置在Open Explorer發(fā)布包中。因此在我們剛剛拉取的docker中,輸入cd /open_explorer/ddk/package/board/,執(zhí)行命令bash install.sh 192.168.0.104,其中192.168.0.104為開發(fā)板IP地址,可用ifconfig查看。
這個功能主要就是拷貝hrt_bin_dump和hrt_model_exec到開發(fā)板,并在開發(fā)板的/etc/profile里面添加幾個環(huán)境,添加內(nèi)容如下所示。
#Horizon Open Explorer ENV export PATH=/userdata/.horizon/:/userdata/.horizon/ai_express_webservice_display/sbin/:$PATH export HORIZON_APP_PATH=/userdata/.horizon/:$HORIZON_APP_PATH #Horizon Open Explorer ENV
在開發(fā)板里輸入hrt_model_exec,如果有如下輸出,說明開發(fā)板部署完成。
模型部署
BPU的工具鏈是非常長的,在部署之前一定要先理解下每個流程的含義。
模型準備
支持Caffe模型和ONNX模型,Caffe模型的支持度是最高的。咱們常用的Pytorch模型是可以轉(zhuǎn)為ONNX模型的。實際上,OpenCV內(nèi)部集成的dnn模塊也是以caffe為主的,所以盡管Caffe在學術圈不火了,但它在工業(yè)圈一直廣泛使用。
驗證模型
驗證模型中所用的層是否可以在BPU中使用。需要利用hb_mapper checker后面跟一堆參數(shù)來對模型進行配置。配置信息如下:
--model-type:輸入的模型類型,onnx或caffe ;
--march:芯片類型,這個板子只能填bernoulli2;
--proto:若模型為caffe,則填入caffe所需的prototxt文件。onnx模型就不用寫這個參數(shù);
--model:模型文件,caffe就是*.caffemodel,onnx模型就是.onnx;
--input-shape:模型數(shù)據(jù)輸入的名稱和維度,比如輸入層名稱叫input1,維度為1x3x128x128,那么該參數(shù)就可以寫為--input-shape input1 1x3x128x128。如果我們的模型有多個輸入,比如第二個輸入層名稱叫input2,維度為1x96x28x28,那么參數(shù)設置就寫為--input-shape input1 1x3x128x128 --input-shape input2 1x96x28x28(該參數(shù)可選,不寫的話程序會自動識別參數(shù),如果指定以指定為主);
--output:設置輸出日志文件(已經(jīng)移除,默認存在根目錄的hb_mapper_checker.log中);
注意:如果模型檢查不通過,控制臺會有明顯的ERROR信息,一般都會檢查出某些層不支持BPU,這時候可以寫個自定義層來解決,后面會提供個例子來展示不通過情況的處理辦法。
轉(zhuǎn)換模型
模型檢查通過之后,就可以通過配置一個yaml文件來將模型文件轉(zhuǎn)為可以在BPU上運行的文件了,后面配置模型時候會進行詳細介紹。
--model-type:根據(jù)模型類型指定caffe或onnx;
--config:模型編譯的配置文件,內(nèi)容采用yaml格式,文件名使用.yaml后綴。
模型性能、精度分析與調(diào)優(yōu)
初時BPU的時候都會疑惑,為什么轉(zhuǎn)換模型后精度會有變化?因為模型轉(zhuǎn)換后是由float轉(zhuǎn)為int8計算的,這個過程必有精度損失。如果精度差異較大,就需要按照官方教程進行調(diào)優(yōu)。
Yolov3部署示例
將yolov3放置在docker文件中的/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper路徑下,以官方示例,來初步了解下BPU的相關操作流程。
模型準備
prototxt和caffemodel文件放置在docker中的/open_explorer/ddk/samples/ai_toolchain/model_zoo/mapper/detection/yolov3_darknet53路徑下。
驗證模型
/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper,進入該路徑后,輸入 ./01_check.sh,遇到下述這些輸出,就代表轉(zhuǎn)換完成了。
前面已經(jīng)介紹了,模型驗證需要利用hb_mapper checker后面跟一堆參數(shù)來對模型進行配置,下面這些就是 ./01_check.sh的主要內(nèi)容。
下面帶各位來理解這些參數(shù):
--model-type:我們這些模型是Caffe,所以填caffe;
--march:旭日X3派只能填bernoulli2;
--proto:填prototxt文件路徑
即../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt;
--model:填caffemodel文件路徑
即../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel;
--input-shape:這里沒有指定,代碼可以自動去查找。
轉(zhuǎn)換模型
在轉(zhuǎn)換模型之前需要準備校準數(shù)據(jù),輸入./02_preprocess.sh會自動從docker的open_explorer包中抽取數(shù)據(jù);再輸入./03_build.sh,輸出一大堆的命令行,等待一段時間之后會輸出。
這里我們可以發(fā)現(xiàn)每一層網(wǎng)絡都要評估一個相似度,這也是為什么要準備校準數(shù)據(jù),因為BPU是INT8計算,所以注定會有精度損失。而且這些誤差也是可以傳遞的,所以到后面精度是越來越低的。如果網(wǎng)絡深度過高,也會導致整體精度的下降。
為了更好的理解這些轉(zhuǎn)換流程,將對其中的準備校準數(shù)據(jù)、模型轉(zhuǎn)換過程進行一個完全解讀。
(1)原理解讀:準備校準數(shù)據(jù)
這個過程調(diào)用了腳本./02_preprocess.sh,這個腳本核心調(diào)用的是python文件,data_preprocess.py的源碼可以自行去查看。
python3 ../../../data_preprocess.py \ --src_dir ../../../01_common/calibration_data/coco \ --dst_dir ./calibration_data_rgb_f32 \ --pic_ext .rgb \ --read_mode opencv
然而data_preprocess.py并不適合初學者進行閱讀,因為其兼容了太多東西,很簡單的一些功能硬是寫復雜了,那么就圍繞這個模型,給各位縷縷校準數(shù)據(jù)到底要準備啥。
首先要搞清楚,我們要準備的校準數(shù)據(jù)是什么樣的: 校準數(shù)據(jù)要將圖像數(shù)據(jù)按照目標尺寸、目標顏色(rgb or bgr等)、目標排布(CHW or HWC)進行存儲。那么下面,帶著這些問題進行處理,先構建一個基本處理流程:
①加載一個文件夾下的所有圖像地址信息。圖像目錄為/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/calibration_data/coco;
②對每個圖像按照校準格式進行輸出。從prototxt我們知道圖像的尺寸為416x416,從./03_build.sh調(diào)用的yaml文件可知圖像輸入格式為rgb,數(shù)據(jù)排布為CHW;
③將轉(zhuǎn)換后的圖像利用numpy.tofile函數(shù)存到目標文件夾下(你在哪轉(zhuǎn)換的,就要在哪個目錄存校準數(shù)據(jù)文件夾calibration_data)。
開始寫我們自己的Python代碼,每個步驟都寫了注釋,各位可以直接理解。
# prepare_calibration_data.py import os import cv2 import numpy as np src_root = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/calibration_data/coco' cal_img_num = 100 # 想要的圖像個數(shù) dst_root = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/02_yolov3_darknet53/mapper/calibration_data' ## 1. 從原始圖像文件夾中獲取100個圖像作為校準數(shù)據(jù) num_count = 0 img_names = [] for src_name in sorted(os.listdir(src_root)): if num_count > cal_img_num: break img_names.append(src_name) num_count += 1 # 檢查目標文件夾是否存在,如果不存在就創(chuàng)建 if not os.path.exists(dst_root): os.system('mkdir {0}'.format(dst_root)) ## 2 為每個圖像轉(zhuǎn)換 # 參考了OE中/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/python/data/下的相關代碼 # 轉(zhuǎn)換代碼寫的很棒,很智能,考慮它并不是官方python包,所以我打算換一種寫法 ## 2.1 定義圖像縮放函數(shù),返回為np.float32 # 圖像縮放為目標尺寸(W, H) # 值得注意的是,縮放時候,長寬等比例縮放,空白的區(qū)域填充顏色為pad_value, 默認127 def imequalresize(img, target_size, pad_value=127.): target_w, target_h = target_size image_h, image_w = img.shape[:2] img_channel = 3 if len(img.shape) > 2 else 1 # 確定縮放尺度,確定最終目標尺寸 scale = min(target_w * 1.0 / image_w, target_h * 1.0 / image_h) new_h, new_w = int(scale * image_h), int(scale * image_w) resize_image = cv2.resize(img, (new_w, new_h)) # 準備待返回圖像 pad_image = np.full(shape=[target_h, target_w, img_channel], fill_value=pad_value) # 將圖像resize_image放置在pad_image的中間 dw, dh = (target_w - new_w) // 2, (target_h - new_h) // 2 pad_image[dh:new_h + dh, dw:new_w + dw, :] = resize_image return pad_image ## 2.2 開始轉(zhuǎn)換 for each_imgname in img_names: img_path = os.path.join(src_root, each_imgname) img = cv2.imread(img_path) # BRG, HWC img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # RGB, HWC img = imequalresize(img, (416, 416)) img = np.transpose(img, (2, 0, 1)) # RGB, CHW # 將圖像保存到目標文件夾下 dst_path = os.path.join(dst_root, each_imgname + '.rgbchw') print("write:%s" % dst_path) # 圖像加載默認就是uint8,但是不加這個astype的話轉(zhuǎn)換模型就會出錯 # 轉(zhuǎn)換模型時候,加載進來的數(shù)據(jù)竟然是float64,不清楚內(nèi)部是怎么加載的。 img.astype(np.uint8).tofile(dst_path) print('finish')
(2)原理解讀:轉(zhuǎn)換配置
模型轉(zhuǎn)換的核心在于配置目標的yaml文件,官方也提供了一個yolov3_darknet53_config.yaml可供用戶直接試用,每個參數(shù)都給了注釋,我能感受到開發(fā)者的誠意。然而模型轉(zhuǎn)換的配置文件參數(shù)太多,如果想改參數(shù)都不知道如何下手。
本節(jié)目的是引導各位快速上手,因此一些參數(shù)我暫時不解釋意義,用默認即可。該模板可將待配置的30多個參數(shù)壓縮到9個參數(shù),方便各位快速的配置簡單模型。本yaml模板適用于的模型具有如下屬性:
- 無自定義層,換句話說,BPU支持該模型的所有層;
- 輸入節(jié)點只有1個,且輸入是圖像。
先復制這個模板到代碼根目錄,命名為"yolov3_simple.yaml",然后根據(jù)后面的思維導圖進行配置具體參數(shù)。
model_parameters: # [待配置參數(shù)],見思維導圖"模型參數(shù)組"部分 prototxt: '***.prototxt' caffe_model: '****.caffemodel' onnx_model: '****.onnx' output_model_file_prefix: 'mobilenetv1' # 默認參數(shù),暫不需要理解 march: 'bernoulli2' input_parameters: # [待配置參數(shù)],見思維導圖"輸入信息參數(shù)組/原始模型參數(shù)"部分 input_type_train: 'bgr' input_layout_train: 'NCHW' # [待配置參數(shù)],見思維導圖"輸入信息參數(shù)組/轉(zhuǎn)換后模型參數(shù)"部分 input_type_rt: 'yuv444' # [待配置參數(shù)],見思維導圖"輸入信息參數(shù)組/輸入數(shù)據(jù)預處理"部分 norm_type: 'data_mean_and_scale' mean_value: '103.94 116.78 123.68' scale_value: '0.017' # 默認參數(shù),暫不需要理解 input_layout_rt: 'NHWC' # 校準參數(shù)組,全部默認 calibration_parameters: cal_data_dir: './calibration_data' calibration_type: 'max' max_percentile: 0.9999 # 編譯參數(shù)組,全部默認 compiler_parameters: compile_mode: 'latency' optimize_level: 'O3' debug: False # 別看官網(wǎng)寫的可選,實際上不寫這個出bug
思維導圖如下所示,帶著這個圖,請各位耐心地跟我一步步配置,僅需要配置9個即可。
模型參數(shù)組參數(shù)model_parameters配置:
output_model_file_prefix:給轉(zhuǎn)換后的模型起個名,這里叫做'yolov3_selfyaml',注意字符串前后都要有個單引號;
prototxt:caffe的prototxt,這里為'../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt';
caffe_model:caffe的模型文件,這里為'../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel';
onnx_model:刪掉。因為我們用的是Caffe。
輸入信息組參數(shù)配置input_parameters:
input_type_train:原始浮點模型的輸入數(shù)據(jù)格式,支持多種圖像格式,這里設置為'rgb'(這就是前文校準模型時為什么要將BGR轉(zhuǎn)為RGB); input_layout_train:從前文的prototxt可以看出,數(shù)據(jù)輸入排布為'NCHW'(所以在模型校準時我們將圖像數(shù)據(jù)由HWC轉(zhuǎn)為CHW) input_type_rt:模型轉(zhuǎn)換后,我們期望輸入的圖像格式。我們在訓練模型和部署模型的時候,圖像輸入格式是可以變的,NV12是一些相機返回的原始數(shù)據(jù)格式,作為嘗試設置為'nv12'; norm_type:網(wǎng)絡不可能拿原始圖像數(shù)據(jù)作為輸入的,一般都要進行一個歸一化操作。這里用的模型對應的歸一化代碼為inpBlob = cv2.dnn.blobFromImage(frame, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRB=False, crop=False),無減均值項,只有尺度項。因此,該屬性設置為'data_scale'; mean_value:刪掉,因為網(wǎng)絡沒有均值項; scale_value:尺度為1.0 / 255,因此設置為0.003921568627451。
最終,我們的yaml文件內(nèi)容如下所示:
model_parameters: prototxt: '../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3_transposed.prototxt' caffe_model: '../../../01_common/model_zoo/mapper/detection/yolov3_darknet53/yolov3.caffemodel' output_model_file_prefix: 'yolov3_selfyaml' march: 'bernoulli2' input_parameters: input_type_train: 'rgb' input_layout_train: 'NCHW' input_type_rt: 'nv12' norm_type: 'data_scale' scale_value: 0.003921568627451 input_layout_rt: 'NHWC' calibration_parameters: cal_data_dir: './calibration_data' calibration_type: 'max' max_percentile: 0.9999 compiler_parameters: compile_mode: 'latency' optimize_level: 'O3' debug: False
之后,用我們親手準備的校準數(shù)據(jù)和配置的輕量yaml進行模型轉(zhuǎn)換,在控制臺輸入指令hb_mapper makertbin --config yolov3_simple.yaml --model-type caffe。
模型推理
在官方給的demo中,04_inference.sh可以直接調(diào)用執(zhí)行好的模型進行推理,但是為了我覺得這種方案對于未來要如何部署自己的模型是無意義的。因此我閱讀了官方推理的demo之后,自己寫個完整的推理過程。模型推理流程主要可以分為以下三個步驟:
①數(shù)據(jù)預處理,生成推理所需數(shù)據(jù);
②利用處理好的數(shù)據(jù)進行模型推理,得到輸出;
③將輸出轉(zhuǎn)換成最終數(shù)據(jù),也就是后處理過程。
(PS.使用的測試圖像路徑為/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample/01_common/test_data/det_images/kite.jpg)
在上一節(jié)中,模型轉(zhuǎn)換后有三個關鍵文件:
yolov3_selfyaml_original_float_model.onnx:圖像量化前的模型; yolov3_selfyaml_quantized_model.onnx:圖像量化后的模型; yolov3_selfyaml.bin:在BPU上用于推理的模型文件,輸出結果與yolov3_selfyaml_quantized_model.onnx一致。
下面將給出推理一張圖像的相關代碼,其中我把圖像格式轉(zhuǎn)換,以及yolo后處理的細節(jié)封裝在一個包里,相關的代碼已經(jīng)放在社區(qū)里供大家參考。
以下是inference_model.py的代碼細節(jié),在每個關鍵過程中都給出了相關的注釋:
從代碼可以獲知模型會輸出三層,每層的維度為(1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255),對著下圖,可以很容易對應的是網(wǎng)絡的哪一層。
import numpy as np import cv2 import os from horizon_tc_ui import HB_ONNXRuntime from bputools.format_convert import imequalresize, bgr2nv12_opencv, nv122yuv444 from bputools.yolo_postproc import modelout2predbbox, recover_boxes, nms, draw_bboxs modelpath_prefix = '/open_explorer/ddk/samples/ai_toolchain/horizon_model_convert_sample' # img_path 圖像完整路徑 img_path = os.path.join(modelpath_prefix, '01_common/test_data/det_images/kite.jpg') # model_path 量化模型完整路徑 model_root = os.path.join(modelpath_prefix, '04_detection/02_yolov3_darknet53/mapper/model_output') model_path = os.path.join(model_root, 'yolov3_selfyaml_quantized_model.onnx') # 1. 加載模型,獲取所需輸出HW sess = HB_ONNXRuntime(model_file=model_path) sess.set_dim_param(0, 0, '?') model_h, model_w = sess.get_hw() # 2 加載圖像,根據(jù)前面模型,轉(zhuǎn)換后的模型是以NV12作為輸入的 # 但在OE驗證的時候,需要將圖像再由NV12轉(zhuǎn)為YUV444 imgOri = cv2.imread(img_path) img = imequalresize(imgOri, (model_w, model_h)) nv12 = bgr2nv12_opencv(img) yuv444 = nv122yuv444(nv12, [model_w, model_h]) # 3 模型推理 input_name = sess.input_names[0] output_name = sess.output_names output = sess.run(output_name, {input_name: np.array([yuv444])}, input_offset=128) print(output_name) print(output[0].shape, output[1].shape, output[2].shape) # ['layer82-conv-transposed', 'layer94-conv-transposed', 'layer106-conv-transposed'] # (1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255) # 4 檢測結果后處理 # 由output恢復416*416模式下的目標框 pred_bbox = modelout2predbbox(output) # 將目標框恢復到原始分辨率 bboxes = recover_boxes(pred_bbox, (imgOri.shape[0], imgOri.shape[1]), input_shape=(model_h, model_w), score_threshold=0.3) # 對檢測出的框進行非極大值抑制,抑制后得到的框就是最終檢測框 nms_bboxes = nms(bboxes, 0.45) print("detected item num: {0}".format(len(nms_bboxes))) # 繪制檢測框 draw_bboxs(imgOri, nms_bboxes) cv2.imwrite('detected.png', imgOri)
上板運行
我們將下圖所示的一些文件拖到旭日X3派開發(fā)板中,注意inference_model_bpu.py跟docker中是有微小的改動的。
注意,在執(zhí)行前要安裝一些包sudo pip3 install EasyDict pycocotools,切記要加sudo,這樣安裝的路徑不是用戶目錄,在運行BPU模型時候,也是必須要加sudo的。
inference_model_bpu.py的源碼如下所示,與在docker中不同,nv12不需要再轉(zhuǎn)為yuv444了,模型的運行也有一些差別,而后處理幾乎沒有變化。
import numpy as np import cv2 import os from hobot_dnn import pyeasy_dnn as dnn from bputools.format_convert import imequalresize, bgr2nv12_opencv, nv122yuv444 from bputools.yolo_postproc import modelout2predbbox, recover_boxes, nms, draw_bboxs def get_hw(pro): if pro.layout == "NCHW": return pro.shape[2], pro.shape[3] else: return pro.shape[1], pro.shape[2] modelpath_prefix = '' # img_path 圖像完整路徑 img_path = 'COCO_val2014_000000181265.jpg' # model_path 量化模型完整路徑 model_path = 'yolov3_selfyaml.bin' # 1. 加載模型,獲取所需輸出HW models = dnn.load(model_path) model_h, model_w = get_hw(models[0].inputs[0].properties) # 2 加載圖像,根據(jù)前面模型,轉(zhuǎn)換后的模型是以NV12作為輸入的 # 但在OE驗證的時候,需要將圖像再由NV12轉(zhuǎn)為YUV444 imgOri = cv2.imread(img_path) img = imequalresize(imgOri, (model_w, model_h)) nv12 = bgr2nv12_opencv(img) # 3 模型推理 t1 = cv2.getTickCount() outputs = models[0].forward(nv12) t2 = cv2.getTickCount() outputs = (outputs[0].buffer, outputs[1].buffer, outputs[2].buffer) print(outputs[0].shape, outputs[1].shape, outputs[2].shape) # (1, 13, 13, 255) (1, 26, 26, 255) (1, 52, 52, 255) print('time consumption {0} ms'.format((t2-t1)*1000/cv2.getTickFrequency())) # 4 檢測結果后處理 # 由output恢復416*416模式下的目標框 pred_bbox = modelout2predbbox(outputs) # 將目標框恢復到原始分辨率 bboxes = recover_boxes(pred_bbox, (imgOri.shape[0], imgOri.shape[1]), input_shape=(model_h, model_w), score_threshold=0.3) # 對檢測出的框進行非極大值抑制,抑制后得到的框就是最終檢測框 nms_bboxes = nms(bboxes, 0.45) print("detected item num: {0}".format(len(nms_bboxes))) # 繪制檢測框 draw_bboxs(imgOri, nms_bboxes) cv2.imwrite('detected.png', imgOri)
手部關鍵點檢測網(wǎng)絡
手部關鍵點檢測是做手勢識別的一個關鍵過程,該代碼基于Caffe,而且無自定義層,因此作為個引子,帶領各位先初步使用BPU。
模型準備
在前期安裝準備中,我們掛載了一個目錄-v "D:\05 - 項目\01 - 旭日x3派\BPUCodes":/data/horizon_x3/codes,下載好代碼后,按照如下方式放置相關文件,此時可以發(fā)現(xiàn)docker中也有這些文件。
驗證模型
驗證前,先將docker根目錄切換到模型根目錄下cd /data/horizon_x3/codes/HandKeypointDetection/hand/。模型驗證需要利用hb_mapper checker后面跟一堆參數(shù)來對模型進行配置。下面帶各位來配置這些參數(shù):
--model-type:我們這些模型是Caffe,所以填caffe
--march:旭日3派只能填bernoulli2
--proto:填prototxt文件名,即pose_deploy.prototxt
--model:填caffemodel文件名,即pose_iter_102000.caffemodel
--input-shape:打開prototxt文件,查找input屬性,可以發(fā)現(xiàn)模型只有一個輸入,輸入層的名稱為image,輸入圖像的維度大小為1x3x368x368,那么這個參數(shù)設置就寫為image 1x3x368x368。
綜上所述,在docker中需要輸入如下指令來完成模型驗證過程:
hb_mapper checker \ --model-type caffe \ --march bernoulli2 \ --proto pose_deploy.prototxt \ --model pose_iter_102000.caffemodel \ --input-shape image 1x3x368x368
輸出結果如下所示,可以看到整個流程的轉(zhuǎn)換狀態(tài)以及每個節(jié)點是在BPU還是CPU上運行的。整個控制臺的運行結果默認存在根目錄的hb_mapper_checker.log中。
轉(zhuǎn)換模型
與前面流程不同,這里先配置yaml文件,再準備校準數(shù)據(jù)。
(1)配置yaml文件
模型參數(shù)組參數(shù)model_parameters配置:
output_model_file_prefix:給轉(zhuǎn)換后的模型起個名,這里叫做'handkpdet'(hand keypoint detection);
prototxt:caffe的prototxt,這里為'pose_deploy.prototxt';
caffe_model:caffe的模型文件,這里為'pose_iter_102000.caffemodel'
onnx_model:刪掉。因為我們用的是Caffe。
輸入信息組參數(shù)配置input_parameters:
input_type_train:原始浮點模型的輸入數(shù)據(jù)格式,支持多種圖像格式。我們這個模型,輸入的是彩色圖,考慮到opencv加載圖像默認是BGR通道,因此這里設置為'bgr';
input_layout_train:從前文的prototxt可以看出,數(shù)據(jù)輸入排布為'NCHW';
input_type_rt:模型轉(zhuǎn)換后,我們期望輸入的圖像格式。我們在訓練模型和部署模型的時候,圖像輸入格式是可以變的,NV12是一些相機返回的原始數(shù)據(jù)格式,考慮到我們測試仍然基于本地圖像,因此這里仍然設置為'bgr';
norm_type:網(wǎng)絡不可能拿原始圖像數(shù)據(jù)作為輸入的,一般都要進行一個歸一化操作。這里用的模型對應的歸一化代碼為inpBlob = cv2.dnn.blobFromImage(frame, 1.0 / 255, (inWidth, inHeight), (0, 0, 0), swapRB=False, crop=False),無減均值項,只有尺度項。因此,該屬性設置為'data_scale';
mean_value:刪掉,因為網(wǎng)絡沒有均值項;
scale_value:尺度為1.0 / 255,因此設置為'0.0039'。
最終,我們的yaml文件handpoint.yaml內(nèi)容為:
model_parameters: prototxt: 'pose_deploy.prototxt' caffe_model: 'pose_iter_102000.caffemodel' output_model_file_prefix: 'handkpdet' march: 'bernoulli2' input_parameters: input_type_train: 'bgr' input_layout_train: 'NCHW' input_type_rt: 'bgr' norm_type: 'data_scale' scale_value: '0.0039' input_layout_rt: 'NHWC' calibration_parameters: cal_data_dir: './calibration_data' calibration_type: 'max' max_percentile: 0.9999 compiler_parameters: compile_mode: 'latency' optimize_level: 'O3' debug: False
(2)準備校準數(shù)據(jù)
考慮到這個模型的輸入只有一個,因此,準備校準數(shù)據(jù)部分的代碼可以參考上一節(jié)的內(nèi)容,需要修改的只有兩個地方,原始數(shù)據(jù)地址,和顏色轉(zhuǎn)換部分(取消了BGR轉(zhuǎn)RGB的過程),數(shù)據(jù)集用的是FreiHAND_pub_v2_eval.zip。
docker中,校準數(shù)據(jù)形式如下圖所示,共計100張。
(3)開始轉(zhuǎn)換
數(shù)據(jù)準備就緒,輸入命令hb_mapper makertbin --config handpoint.yaml --model-type caffe開始轉(zhuǎn)換我們的模型!等待一段時間之后,模型轉(zhuǎn)換成功,從結果可以看出來,模型的損失并不是很高??!感覺有戲,(☆▽☆)。
模型推理
由于該模型與前面的模型相似,都是以一張圖像作為輸入的,因此自己要補充的工作主要有兩點:
- 完成圖像預處理部分。前面的yaml文件指明了,量化后的模型是以BGR、NHWC格式作為輸入的。因此,只需要調(diào)用resize成目標模型大小就行,opencv加載圖像時候默認是HWC格式。
- 完成圖像后處理部分。圖像后處理一般與推理平臺沒有太大的關系,完整的流程都會有這個過程。
在docker中推理的完整代碼如下所示:
import numpy as np import cv2 import os from horizon_tc_ui import HB_ONNXRuntime import copy # img_path 圖像完整路徑 img_path = '/data/horizon_x3/codes/HandKeypointDetection/hand/FreiHAND_pub_v2_eval/evaluation/rgb/00000253.jpg' # model_path 量化模型完整路徑 model_path = '/data/horizon_x3/codes/HandKeypointDetection/hand/model_output/handkpdet_quantized_model.onnx' # 1. 加載模型,獲取所需輸出HW sess = HB_ONNXRuntime(model_file=model_path) sess.set_dim_param(0, 0, '?') model_h, model_w = sess.get_hw() # 2 加載圖像,根據(jù)前面yaml,量化后的模型以BGR NHWC形式輸入 imgOri = cv2.imread(img_path) img = cv2.resize(imgOri, (model_w, model_h)) # 3 模型推理 input_name = sess.input_names[0] output_name = sess.output_names output = sess.run(output_name, {input_name: np.array([img])}, input_offset=128) print(output_name) print(output[0].shape) # ['net_output'] # (1, 22, 46, 46) # 4 檢測結果后處理 # 繪制關鍵點 nPoints = 22 threshold = 0.1 POSE_PAIRS = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [0, 9], [9, 10], [10, 11], [11, 12], [0, 13], [13, 14], [14, 15], [15, 16], [0, 17], [17, 18], [18, 19], [19, 20]] imgh, imgw = imgOri.shape[:2] points = [] imgkp = copy.deepcopy(imgOri) for i in range(nPoints): probMap = output[0][0, i, :, :] probMap = cv2.resize(probMap, (imgw, imgh)) minVal, prob, minLoc, point = cv2.minMaxLoc(probMap) if prob > threshold: cv2.circle(imgkp, (int(point[0]), int(point[1])), 8, (0, 255, 255), thickness=-1, lineType=cv2.FILLED) cv2.putText(imgkp, "{}".format(i), (int(point[0]), int(point[1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, lineType=cv2.LINE_AA) points.append((int(point[0]), int(point[1]))) else: points.append(None) # 繪制骨架 imgskeleton = copy.deepcopy(imgOri) for pair in POSE_PAIRS: partA = pair[0] partB = pair[1] if points[partA] and points[partB]: cv2.line(imgskeleton, points[partA], points[partB], (0, 255, 255), 2) cv2.circle(imgskeleton, points[partA], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED) cv2.circle(imgskeleton, points[partB], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED) # 保存關鍵點和骨架圖 cv2.imwrite('handkeypoint.png', imgkp) cv2.imwrite('imgskeleton.png', imgskeleton)
上板運行
在開發(fā)板運行的程序與上述推理代碼差異不大,注意好模型的輸入數(shù)據(jù)格式即可,這里要注意,輸出的outputs與docker中有差異,要做output = (outputs[0].buffer,)轉(zhuǎn)換,這樣可以直接兼容后面的后處理部分,進而生成結果圖。
import numpy as np import cv2 import os from hobot_dnn import pyeasy_dnn as dnn import copy def get_hw(pro): if pro.layout == "NCHW": return pro.shape[2], pro.shape[3] else: return pro.shape[1], pro.shape[2] # img_path 圖像完整路徑 img_path = '20220806023323.jpg' # model_path 量化模型完整路徑 model_path = 'handkpdet.bin' # 1. 加載模型,獲取所需輸出HW models = dnn.load(model_path) model_h, model_w = get_hw(models[0].inputs[0].properties) # 2 加載圖像,根據(jù)前面yaml,量化后的模型以BGR NHWC形式輸入 imgOri = cv2.imread(img_path) img = cv2.resize(imgOri, (model_w, model_h)) # 3 模型推理 t1 = cv2.getTickCount() outputs = models[0].forward(img) t2 = cv2.getTickCount() output = (outputs[0].buffer,) print(outputs[0].buffer.shape) # (1, 22, 46, 46) print('time consumption {0} ms'.format((t2-t1)*1000/cv2.getTickFrequency())) # 4 檢測結果后處理 # 繪制關鍵點 nPoints = 22 threshold = 0.1 POSE_PAIRS = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [0, 9], [9, 10], [10, 11], [11, 12], [0, 13], [13, 14], [14, 15], [15, 16], [0, 17], [17, 18], [18, 19], [19, 20]] imgh, imgw = imgOri.shape[:2] points = [] imgkp = copy.deepcopy(imgOri) for i in range(nPoints): probMap = output[0][0, i, :, :] probMap = cv2.resize(probMap, (imgw, imgh)) minVal, prob, minLoc, point = cv2.minMaxLoc(probMap) if prob > threshold: cv2.circle(imgkp, (int(point[0]), int(point[1])), 8, (0, 255, 255), thickness=-1, lineType=cv2.FILLED) cv2.putText(imgkp, "{}".format(i), (int(point[0]), int(point[1])), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, lineType=cv2.LINE_AA) points.append((int(point[0]), int(point[1]))) else: points.append(None) # 繪制骨架 imgskeleton = copy.deepcopy(imgOri) for pair in POSE_PAIRS: partA = pair[0] partB = pair[1] if points[partA] and points[partB]: cv2.line(imgskeleton, points[partA], points[partB], (0, 255, 255), 2) cv2.circle(imgskeleton, points[partA], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED) cv2.circle(imgskeleton, points[partB], 8, (0, 0, 255), thickness=-1, lineType=cv2.FILLED) # 保存關鍵點和骨架圖 cv2.imwrite('handkeypoint.png', imgkp) cv2.imwrite('imgskeleton.png', imgskeleton)
我自己拍了兩張圖進行測試,第一排是晚上拍的,手指頭有點串味哈哈,整體檢測耗時在480ms左右,網(wǎng)絡深度沒有yolo高,也許是橫向的特征比較多。
原作者:小璽璽
原鏈接:本文轉(zhuǎn)自地平線開發(fā)者社區(qū)
-
模型
+關注
關注
1文章
3226瀏覽量
48806 -
BPU
+關注
關注
0文章
4瀏覽量
1951
發(fā)布評論請先 登錄
相關推薦
評論