TensorRT 使用 - 以 yolov9 為例 === # 摘要 * 本文紀錄了 tensorRT 的使用並以 yolov9 當作範例來去實踐, 在 v10 已經有 NMS free 達成但NMS 這一經典的方法覺得還是該寫進此筆記作為紀錄, 有需要也可以回頭複習 * 後續也可基於此專案改寫不同的推論引擎 # 環境建制 ## 硬體環境 : * Dell G15 * GPU : RTX3050 Laptop 4G * CPU : 12th Gen Intel® Core™ i5-12500H, 16 cores * Memory : 16GB ## 軟體套件 : * opencv : 4.10 * tensorRT : release/10.6 * cuda : 12.6 * yolov9 ## 編譯方式 : * 目前相關程式紀錄在 [github](https://github.com/GU-Lin/AI_engine.git), 首先使用 make_build.sh 這個 script 檔指令如下, 會使用 docker 來建立相關的 library 預設會是建立在 build_x86_64 的資料夾下 * 執行完後可在 builx_x86_64 底下看到 opencv、tensorRT的資料夾 ```script= ./make_build.sh --opencv --tensorRT ``` # TensorRT 使用 ## 模型轉換 * TensorRT 使用的檔案為 .trt, 而這部份可透過將 .onnx 轉換成 .trt 達成過程中也可根據想要答標的速度或是精度做選擇(例如 fp32、fp16、int8等), * 然而 .trt 會根據當前硬體條件(也就是 GPU的算例)做對應的轉換, 以自身筆電為例所使用的算力為 Compute Capability : 8.6 如果放到 7.x 或其他版本的硬體上此 .trt 則無法是用 ![Screenshot from 2024-12-22 11-56-23](https://hackmd.io/_uploads/Hk0bC-rrJx.png) * Compute Capability 可在 https://developer.nvidia.com/cuda-gpus 下查詢, 或是轉換時即可在資訊中顯示 * 執行以下指令即可將 .onnx 轉為 .trt, yolov9-c-converted.onnx可到 yolov9 的 github 下載模型並自行轉成 onnx ```bash= cd build_x86_64 export LD_LIBRARY_PATH=`pwd`/lib ./bin/trtexec --onnx=../../data/yolov9-c-converted.onnx --saveEngine=../../yolov9-c-converted.trt ``` ## 模型讀取 * 在讀取時有以下物件需要使用, 讀取模型的方式放在 trtInferenceTool.cpp 內 * IExecutionContext : 用於執行實際推論 * ICudaEngine : 提供創建上下文的方法及表示靜態的模型推理 * IRuntime : 反序列化模型 ```cpp= std::unique_ptr<IExecutionContext> m_context; std::shared_ptr<ICudaEngine> m_engine; std::unique_ptr<IRuntime> m_runtime; ``` * 首先給定路徑讀取模型 ```cpp= std::ifstream file(path, std::ios::binary); if (!file.is_open()) { std::cerr << "Error opening engine file: " << path << std::endl; return ; } // 讀取文件內容 file.seekg(0, file.end); size_t size = file.tellg(); file.seekg(0, file.beg); std::vector<char> engineData(size); file.read(engineData.data(), size); file.close(); ``` * 創建 runtime ```cpp= // 創建 TensorRT runtime m_runtime.reset(createInferRuntime(gLogger)); if (!m_runtime) { std::cerr << "Failed to create InferRuntime" << std::endl; return ; } ``` * 創建 engine 即將模型反序列化, 反序列化是只將模型轉換為物件的過程 * 舉例 : 讀取 .json 檔將檔案內容反序列化成字典的形式做使用 ```cpp= m_engine = std::shared_ptr<ICudaEngine>( m_runtime->deserializeCudaEngine(engineData.data(), size), samplesCommon::InferDeleter()); if (!m_engine) { std::cerr << "Failed to deserialize CUDA engine" << std::endl; return ; } ``` * 創建 context, 可透過 getIOTensorName 來得到 input/output 的名稱, 可用於獲取模型輸入輸出的資訊 ```cpp= m_context.reset(m_engine->createExecutionContext()); inputName = m_engine->getIOTensorName(0); outputName = m_engine->getIOTensorName(1); std::cout << "Input Name : " << inputName << std::endl; std::cout << "Output Name : " << outputName << std::endl; ``` * 記憶體分配 : 在此透過 cuda 分配記憶體給 GPU 目的是讓 .trt 使用特定 GPU 記憶體位置進行推論加速 * buffers[0] 為 input, buffers[1] 為 output ```cpp= m_inputSize = getIOSize(inputName); m_outputSize = getIOSize(outputName); cudaMalloc(&buffers[0],m_inputSize*sizeof(float)); cudaMalloc(&buffers[1],m_outputSize*sizeof(float)); m_context->setTensorAddress(inputName,buffers[0]); m_context->setTensorAddress(outputName,buffers[1]); hostData = new float[m_outputSize]; std::cout << "Input size is " << m_inputSize << std::endl; std::cout << "Output size is " << m_outputSize << std::endl; std::cout << "Load " << path << " successful" << std::endl; ``` # yolov9 模型的 IO * Input : 為一個 [1,3,640,640] 的彩色三通道圖片, 但需要經過 normalize 到 [0,1] 之間且轉為 RGB 應向 CHW 的格式 * Output : 為 [1,84,8400] 的樣子, 而其中輸出數字代表的意義如下 * Cx : Bounding Box 的中心象素 x 座標 * Cy : Bounding Box 的中心象素 y 座標 * W : Bounding Box 的寬 * H : Bounding Box 的高 * 以第一直行(column)的方式來看, 第一個物件前4項就是 Bounding box 的位置, 後面 80 項就是對應類別的分數, 如果 Class 10 的分數最高則第一個物件就是被判定為類別 10, 而從輸出可以得到 yolov9 可以一口氣偵測 8400 個物件, 附上 [yolov9 類別對應表](https://github.com/WongKinYiu/yolov9/blob/main/data/coco.yaml) ![image](https://hackmd.io/_uploads/BkLO7mBr1l.png) # yolov9 c++ 實現 ## Preprocess * 預處理的過程為了讓每張影像都縮放成 640 x 640 並使用 warpAffine 的方式進行縮放, 而縮放的過程中如果還有不足的邊框(例如縮放完變成 640 x 512) 則圖片會置中並把多餘的剩下的象素補邊, 象素內容為 (114,114,114) ```cpp= void objectDetection::preprocess(cv::Mat input, cv::Mat &output) { cv::Mat src = input.clone(); cv::Size sizeDst(640,640); cv::Size sizeSrc = src.size(); float ratioWidth = float(sizeDst.width)/float(sizeSrc.width); float ratioHeight = float(sizeDst.height)/float(sizeSrc.height); float r = ratioWidth < ratioHeight ? ratioWidth : ratioHeight; cv::Size sizeEnlarge(std::round(r*sizeSrc.width),std::round(r*sizeSrc.height)); // Compute padding int dw = (sizeDst.width - sizeEnlarge.width)/2; int dh = (sizeDst.height - sizeEnlarge.height)/2; cv::Mat M = cv::Mat::zeros(2,3,CV_32F); M.at<float>(0,0) = r; M.at<float>(1,1) = r; M.at<float>(0,2) = dw; M.at<float>(1,2) = dh; cv::warpAffine(src,src,M,(cv::Size(640,640)),cv::INTER_LINEAR,cv::BORDER_CONSTANT,cv::Scalar(114,114,114)); src.copyTo(output) ; } ``` * 再來需要將影像轉成正確的通道 ```cpp= void objectDetection::HWC2NormalCHW(cv::Mat input, std::vector<float> &data) { std::vector<cv::Mat> inputChannel(3); cv::Mat img; cv::cvtColor(input, img, cv::COLOR_BGR2RGB); img.convertTo(img, CV_32F, 1.0f/255.f); cv::split(img, inputChannel); std::vector<float>result; for(int i = 0; i < 3; i++) { std::vector<float> temp = std::vector<float>(inputChannel[i].reshape(1, 1)); result.insert(result.end(),temp.begin(),temp.end()); } data = result; } ``` ## Inference * 在做推論時最常需要做的就是 cpu 與 gpu 之間資料的拷貝, 具體流程如下 * Host to Device : cpu -> gpu * AI inference : m_context 推論 * Device to Host : gpu -> cpu ```cpp= void TRTInferenceTool::run(std::vector<float> &input, cv::Mat &output) { cudaStream_t stream; cudaStreamCreate(&stream); // From host to device cudaMemcpyAsync(buffers[0], input.data(), m_inputSize * sizeof(float), cudaMemcpyHostToDevice, stream); // Enqueue excute m_context->enqueueV3(stream); // From device to host cudaMemcpyAsync(hostData, buffers[1], m_outputSize * sizeof(float), cudaMemcpyDeviceToHost, stream); cudaStreamSynchronize(stream); cv::Mat m(m_outputClass,m_outputBoxNum, CV_32FC1, hostData); output = m.clone(); } ``` ## Postprocess * 創建了以下 struct 紀錄 bounding box ```cpp= struct PredBox { float cx; float cy; float width; float height; float score; int label; }; ``` * 後處理分成以下三個部分 * 挑選是否過 class 閥值的物件框 * 物件框進行排序 * NMS 將重複框消除 ## 挑選是否過 class 閥值的物件框 * 可看到有使用 minMaxLoc 來找尋最大的類別及對應的類別分數, 如果分數有過閥值則紀錄下來到 m_PredBox_vector 內 ```cpp= for(int i = 0; i < input.cols; i++) { cv::Mat res = input.col(i); float cx = res.at<float>(0); float cy = res.at<float>(1); float w = res.at<float>(2); float h = res.at<float>(3); cv::Mat cls = res(cv::Range(4,84),cv::Range::all()); cv::Point minIdx, maxIdx; double minVal, maxVal; cv::minMaxLoc(cls, &minVal, &maxVal, &minIdx, &maxIdx); if(maxVal > m_conf_thres) { PredBox pbox; pbox.cx = cx; pbox.cy = cy; pbox.width = w; pbox.height = h; pbox.score = maxVal; pbox.label = maxIdx.y; m_PredBox_vector.push_back(pbox); } // break; } ``` ## 物件框進行排序 * 根據比分由大至小進行排序 ```cpp= if(m_PredBox_vector.size()>0) { std::sort(m_PredBox_vector.begin(), m_PredBox_vector.end(), [](PredBox a, PredBox b) { return a.score > b.score; }); } ``` ## NMS 將重複框消除 * NMS 會將兩個物件框做 IOU 的計算, 如果 IOU 重疊比例過閥值則會跳過不紀錄, 紀錄的方式是用一 ```vector<bool> table ```的方式來看是否需要跳過此框 ```cpp= void objectDetection::NMS(std::vector<PredBox> &pred) { std::vector<PredBox> result; std::vector<bool> table(pred.size(),false); for(int i = 0; i < pred.size(); i++) { if(table[i])continue; PredBox temp = pred[i]; result.push_back(temp); for(int j = i+1; j < pred.size(); j++) { if(table[j])continue; if(iou(temp,pred[j]) > m_iou_thres) { table[j] = true; } } } pred = result; } ``` # 畫框 * 目的需要將物件框畫到原圖上而不是補上邊的圖, 所以會計算縮放的比例大小即位移多寡 * 計算完後用 cv::rectangle 畫取 bounding box * 寫上類別後利用 cv::getTextSize 來得到畫上的字串佔據多少的寬高, 目的是自動讓填滿可視化的所呈現的字體周邊象素, 不然有的字長是 10 有的是 3 不太好調整一個通用值 ```cpp= void objectDetection::draw(cv::Mat &input, cv::Mat &output, std::vector<PredBox> &pred) { output = input.clone(); float ration_w = 640/float(input.cols); float ration_h = 640/float(input.rows); for(int i = pred.size()-1; i >= 0 ; i--) { int left = int(pred[i].cx-pred[i].width/2); int top = int(pred[i].cy-pred[i].height/2); int width = pred[i].width; int height = pred[i].height; if(ration_h > ration_w) { left = left /ration_w; top = (top-(640-ration_w*input.rows)/2)/ration_w; width = width / ration_w; height = height / ration_w; }else { left = left /ration_h; top = (top-(640-ration_h*input.cols)/2)/ration_h; width = width / ration_h; height = height / ration_h; } // Rectangle cv::rectangle(output, cv::Point(left,top), cv::Point(left + width,top + height), cv::Scalar(0,255,0),2); // Name std::string class_string = coconame[pred[i].label] + ' ' + std::to_string(pred[i].score).substr(0, 4); int baseline = 0; int fontFace = cv::FONT_HERSHEY_SIMPLEX; // 字體類型 double fontScale = 1.0; // 字體大小 int thickness = 2; // 字體粗細 cv::Size textSize = cv::getTextSize(class_string, fontFace, fontScale, thickness, &baseline); cv::Rect textRect(left, top- textSize.height, textSize.width, textSize.height); cv::rectangle(output,textRect,cv::Scalar(0,255,0),cv::FILLED); putText(output, class_string, cv::Point(left , top), cv::FONT_HERSHEY_DUPLEX, 1, cv::Scalar(0, 0, 0), 1, 0); } } ``` # 編譯程式並執行 ```cpp= cmake CMakeList.txt make ./main ``` # 實際輸出 ![image](https://hackmd.io/_uploads/Sk0go7rByx.png) source : https://github.com/WongKinYiu/yolov9 https://blog.csdn.net/qq_40672115/article/details/136492338 github : https://github.com/GU-Lin/AI_engine ###### tags : `AI`, `AI 部屬`