Go調(diào)用C++動態(tài)庫實現(xiàn)車牌識別的示例代碼
1. 前言
很久沒更新博客,這次正好趁著這次機會來更新一個稍微有點意思的內(nèi)容,利用C++中Opencv、TensorRT等庫編譯出動態(tài)庫供Go調(diào)用,再寫個簡單的api對上傳的車輛圖片進行車牌識別。究其原因,天下苦Java久矣,每次寫JNI去給公司Java后端服務(wù)調(diào)用,而我不喜歡Java那我每次寫好的模型動態(tài)庫就到此為止了?白白浪費之前那么多計算資源于心不忍,因此打算收集一些已有模型,做一個自己的模型服務(wù)倉庫。
主要內(nèi)容如下:
- 模型部分:利用yolov8-pose對車牌數(shù)據(jù)集進行訓練,然后利用OCR模型對檢測矯正后的車牌字符識別,主要參考這個項目yolov8車牌識別算法,支持12種中文車牌類型
- C++部分:實現(xiàn)TensorRT推理以及對應模型的前后處理,最后寫cgo對應接口以及實現(xiàn)
- Go部分:調(diào)用C++編譯后的動態(tài)庫,加載模型,實現(xiàn)輔助功能函數(shù)以及完成接口
2 . 開始
2.1 模型部分
打開上面的鏈接,README中也提到了pytorch1.8以上的版本會有問題,實際嘗試確實如此,總會在一個Conv的地方報錯,魔改了一番代碼還是無法解決,因此下載了車牌檢測的數(shù)據(jù)集本地重新訓練模型。需要注意的是,和官方y(tǒng)olov8-pose的輸出結(jié)果中類別數(shù)目不同,因為按照該倉庫的yaml文件設(shè)置會有兩類,因此后處理階段需要注意。
訓練參數(shù)等不過多介紹,yolov8文檔十分詳細可以自己去查看。來看看最后導出的onnx模型。

最后輸出為14*8400,其中14=4+2+8,含義分別是bbox的四個點,對應兩個類別概率以及四個關(guān)鍵點的(x,y)坐標,后處理階段就要注意對應的偏移量分別是4,2,8.
然后OCR模型直接用它提供的預訓練權(quán)重導出就好,精度基本一致。得到onnx之后可以直接利用trtexec轉(zhuǎn)為對應的engine文件。
2.2 C++部分
為推理引擎反序列化構(gòu)建,host以及device的內(nèi)存分配等共有操作實現(xiàn)基類,然后重載不同模型的構(gòu)造函數(shù)和前后處理函數(shù)。這個部分可以去參考網(wǎng)上一些開源教程,大多模板一致。在這里有兩個點需要注意:
- 如果希望兩個模型運行在不同顯卡上,記得在所有有關(guān)上下文操作前后加上
cudaSetDevice()。 - 對于不同模型,構(gòu)造函數(shù)傳參大多不一致,目前幾種解決方法:工廠模式輸入modelType對應不同實例化,讀取json/yaml等配置文件參數(shù)實例化,最后一種惡心辦法無腦統(tǒng)一實例化接口,大不了某些參數(shù)不用。最優(yōu)方法當然是寫配置文件,用
yaml-cpp或者其他文件解析庫實現(xiàn)對配置文件參數(shù)解析,然后入?yún)⒕徒y(tǒng)一為配置文件路徑以及一些共有參數(shù)(如deviceId可以在服務(wù)端或者前端設(shè)置因此保留)。可惜這個意見沒被接受,不得已提交的那一版寫的是最惡心的方式,后來改成了第一種通過傳入模型類別去實例化。
稍微說說前后處理部分,對于yolov8-pose之前說了注意偏移量的問題,另外就是對輸出轉(zhuǎn)置處理一下方便解析,當然這個操作也可以在模型導出前改一下源碼實現(xiàn)。偏移部分實現(xiàn)大致如下
auto row_ptr = output.row(i).ptr<float>(); auto bboxes_ptr = row_ptr; auto scores_ptr = row_ptr + 4; auto max_s_ptr = std::max_element(scores_ptr, scores_ptr + this->class_nums); auto kps_ptr = row_ptr + 6;
然后將所有結(jié)果經(jīng)過nms篩選,得到最終保留結(jié)果。保存目標的結(jié)構(gòu)體定義如下:
struct Object {
int label = 0;
float prob = 0.0;
std::vector<cv::Point2f> kps;
cv::Rect_<int> rect;
std::string plateContent;
std::string colorType;
};
對于OCR模型

模型輸入大小為(48,168),輸出為5和(21,78),其中5代表黑藍綠白黃五種車牌顏色,78代表78個可識別的字符包括開頭的#號占位符,0-9的數(shù)字,英文字母以及中文漢字,21為最大識別車牌字符長度。然后來看看OCR模型的前后處理,由于大貨車存在雙行車牌的情況,因此需要對車牌上下部分切分然后橫向拼接再給模型推理,大致實現(xiàn)如下:
// merge double plate
void mergePlate(const cv::Mat& src,cv::Mat& dst) {
int width = src.cols;
int height = src.rows;
cv::Mat upper = src(cv::Rect(0,0,width,int(height*5.0/12)));
cv::Mat lower = src(cv::Rect(0,int(height*1.0/3.),width,height-int(height*1.0/3.0)));
?
cv::resize(upper,upper,lower.size());
dst = cv::Mat(lower.rows,lower.cols+upper.cols,CV_8UC3,cv::Scalar(114,114,114));
upper.copyTo(dst(cv::Rect(0,0,upper.cols,upper.rows)));
lower.copyTo(dst(cv::Rect(upper.cols,0,lower.cols,lower.rows)));
}
?
?
/*
preprocess
?
0. Perspective
1. merge plate if label is double
2. resize to (48,168)
3. normalize to 0-1 and standard (mean = 0.588 , std = 0.193)
*/
if(obj.label == 1) {
mergePlate(dst,dst);
}
僅僅對于label為1也就是雙行車牌進行拼接操作,當然這個是透視變換后的車牌。關(guān)于透視變換可以根據(jù)倉庫中Python代碼翻譯出對應的C++版本代碼,
// Perspective
// the kps means pose model's KeyPoints,which is (tl,tr,br,bl)
void Transform(const cv::Mat& src,cv::Mat& dst,const std::vector<cv::Point2f>& kps) {
float widthA = sqrt(pow((kps[2].x-kps[3].x),2)+pow((kps[2].y-kps[3].y),2));
float widthB = sqrt(pow((kps[1].x-kps[0].x),2)+pow((kps[1].y-kps[0].y),2));
float maxWidth = std::max(int(widthA),int(widthB));
?
float heightA = sqrt(powf((kps[1].x-kps[2].x),2)+powf((kps[1].y-kps[2].y),2));
float heightB = sqrt(powf((kps[0].x-kps[3].x),2)+powf((kps[0].y-kps[3].y),2));
float maxHeight = std::max(int(heightA),int(heightB));
?
std::vector<cv::Point2f> dstTri {
cv::Point2f(0,0),cv::Point2f(maxWidth,0),
cv::Point2f(maxWidth,maxHeight),cv::Point2f(0,maxHeight)
};
cv::Mat M = cv::getPerspectiveTransform(kps,dstTri); cv::warpPerspective(src,dst,M,cv::Size(maxWidth,maxHeight),cv::INTER_LINEAR,cv::BORDER_REPLICATE);
}
Blob部分和Python一樣,減去均值除以方差。然后后處理解析部分,0輸出的是5維顏色,1輸出的是(21,78),和分類任務(wù)后處理一致,找最大值下標即為對應類別。注意遍歷識別字符時需要過濾操作,即對于下標0和已識別出的相鄰同樣字符進行過濾。找最大值下標可以利用std::distance()很方便的找到。
最后就是書寫對應的cgo接口,相比起JNI直接根據(jù)類定義使用javah生成的頭文件來寫而言,cgo并沒有生成頭文件的工具,這也讓我們有更多的靈活性去定義對應的接口。比如我的接口定義如下:
#include<stdio.h>
#include<string.h>
#ifndef GOWRAP_H
#define GOWRAP_H
#ifdef __cplusplus
extern "C"
{
#endif
extern void* init(const char* modelType, const char* enginePath, int deviceId, int classNums, int kps);
extern char* detect(void* model1,void* model2,const char* base64Img,float score,float iou);
extern void release(void*);
?
#ifdef __cplusplus
}
#endif
?
#endif //GOWRAP_H
?
因為go不能調(diào)用c++的類,也不能使用c++的std::string等,所以這里全部是char*。然后實現(xiàn)對應接口
#include "../include/gowrap.h"
#include "../include/plate.hpp"
#include "../include/pose.hpp"
#include "../include/factory.hpp"
#include "../include/base64.h"
void* init(const char* modelType, const char* enginePath, int deviceId, int classNums, int kps) {
std::string type(modelType);
std::string engine(enginePath);
auto model = modelInit(type,engine,deviceId,classNums,kps);
model->make_pipe(true);
return (void*)model;
}
?
char* detect(void* m1, void* m2,const char* base64Img, float score, float iou) {
std::string base64(base64Img);
cv::Mat image = Base2Mat(base64);
std::vector<Object> objs;
?
// get model
auto* model1 = (YOLOV8_Pose*)m1;
auto* model2 = (Plate*)m2;
?
model1->predict(image, objs, score,iou,100);
model2->predict(image,objs);
?
// obj trans to json
Json::Value root;
Json::Value resObjs;
Json::Value resObj;
Json::Value objRec;
Json::FastWriter writer;
?
for(const auto&obj : objs){
Json::Value attrObj;
attrObj["color"] = obj.colorType;
attrObj["lineType"] = obj.label;
attrObj["plate"] = obj.plateContent;
resObj["attr"] = Json::Value(attrObj);
resObj["class_id"] = (int)obj.label;
resObj["conf"] = (float)obj.prob;
int x = (int)obj.rect.x;
int y = (int)obj.rect.y;
int width = (int)obj.rect.width;
int height = (int)obj.rect.height;
?
objRec["x"] = x;
objRec["y"] = y;
objRec["width"] = width;
objRec["height"] = height;
?
resObj["position"]=Json::Value(objRec);
resObjs.append(resObj);
}
?
root["result"] = Json::Value(resObjs);
std::string resObjs_str = writer.write(root);
return strdup(resObjs_str.c_str());
}
?
?
void release(void* modelHandle) {
auto model = (TRTInfer*) modelHandle;
delete model;
}
這里分別實現(xiàn)了模型實例化,推理以及模型銷毀,最后推理結(jié)果返回的是json格式的字符串,這部分大多還是沿用之前JNI的寫法。最后就是寫個CMakeLists然后編譯,現(xiàn)在來看看C++上的推理結(jié)果圖


對于這種角度的車牌人眼都需要細看才能識別正確,模型居然也能正確識別,看來模型還是可以的,而且在家里這個服務(wù)器上推理耗時也僅僅1.3ms左右,速度與精度都完全可以接受。
2.3 Go部分
經(jīng)過一系列操作,我們終于編譯得到了.so動態(tài)庫文件,現(xiàn)在就是加載這個動態(tài)庫然后寫個服務(wù)今天的任務(wù)就算完成啦。來看看go調(diào)用動態(tài)庫的部分,首先需要調(diào)用C的庫,并且上面需要添加編譯注釋,同時保證二者之間不能有空行
/* #cgo LDFLAGS: -L./ -lshelgi_plate -lstdc++ #cgo CPPFLAGS: -I ../include -I /usr/include -I /usr/local/include #cgo CFLAGS: -std=gnu11 #include<stdio.h> #include<stdlib.h> #include "gowrap.h" */ import "C"
其實最主要就是第一行LDFLAGS去加載對應的動態(tài)庫。剩下的步驟就是根據(jù)剛才C++定義的函數(shù)來對應寫Go的實現(xiàn)
type Object struct {
p unsafe.Pointer
}
?
func NewModel(modelType, enginePath string, deviceId int, classNums int, kps int) *Object {
obj := &Object{p: C.init(C.CString(modelType), C.CString(enginePath), C.int(deviceId), C.int(classNums), C.int(kps))}
return obj
}
?
func detect(m1, m2 *Object, img string, score, iou float32) string {
res := C.detect(m1.p, m2.p, C.CString(img), C.float(score), C.float(iou))
result := C.GoString(res)
return result
}
?
func release(m *Object) {
C.release(m.p)
}
剩余一些函數(shù),比如base64,unicode與string的轉(zhuǎn)換,對于推理后json字符串的解析等等略過,最后用gin寫個簡單的POST推理路由以及上傳路由。下面來看看效果:

傳入圖片:

推理結(jié)果:

成功識別出兩輛車的車牌,響應延時為153ms,經(jīng)過多次測試,平均在100ms左右,對于單個車輛的圖片延時在50ms左右,基本滿足需求。
3. 最后
其實這部分內(nèi)容也是臨時想到的,后期打算用Rust也試試,看看到底哪個實現(xiàn)性能最高,再次挖坑。
以上就是Go調(diào)用C++動態(tài)庫實現(xiàn)車牌識別的示例代碼的詳細內(nèi)容,更多關(guān)于Go調(diào)用C++實現(xiàn)車牌識別的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang提示dial?tcp?172?.217.163.49:443:?connectex:?A?con
這篇文章主要為大家介紹了golang提示dial?tcp?172?.217.163.49:443:?connectex:?A?connection?attempt?failed解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
Go語言動態(tài)并發(fā)控制sync.WaitGroup的靈活運用示例詳解
本文將講解 sync.WaitGroup 的使用方法、原理以及在實際項目中的應用場景,用清晰的代碼示例和詳細的注釋,助力讀者掌握并發(fā)編程中等待組的使用技巧2023-11-11

