亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Go調(diào)用C++動(dòng)態(tài)庫(kù)實(shí)現(xiàn)車牌識(shí)別的示例代碼

 更新時(shí)間:2023年12月12日 08:52:00   作者:shelgi  
本文主要介紹了如何利用C++中Opencv、TensorRT等庫(kù)編譯出動(dòng)態(tài)庫(kù)供Go調(diào)用,再寫個(gè)簡(jiǎn)單的api對(duì)上傳的車輛圖片進(jìn)行車牌識(shí)別,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下

1. 前言

很久沒(méi)更新博客,這次正好趁著這次機(jī)會(huì)來(lái)更新一個(gè)稍微有點(diǎn)意思的內(nèi)容,利用C++中Opencv、TensorRT等庫(kù)編譯出動(dòng)態(tài)庫(kù)供Go調(diào)用,再寫個(gè)簡(jiǎn)單的api對(duì)上傳的車輛圖片進(jìn)行車牌識(shí)別。究其原因,天下苦Java久矣,每次寫JNI去給公司Java后端服務(wù)調(diào)用,而我不喜歡Java那我每次寫好的模型動(dòng)態(tài)庫(kù)就到此為止了?白白浪費(fèi)之前那么多計(jì)算資源于心不忍,因此打算收集一些已有模型,做一個(gè)自己的模型服務(wù)倉(cāng)庫(kù)。

主要內(nèi)容如下:

  • C++部分:實(shí)現(xiàn)TensorRT推理以及對(duì)應(yīng)模型的前后處理,最后寫cgo對(duì)應(yīng)接口以及實(shí)現(xiàn)
  • Go部分:調(diào)用C++編譯后的動(dòng)態(tài)庫(kù),加載模型,實(shí)現(xiàn)輔助功能函數(shù)以及完成接口

2 . 開(kāi)始

2.1 模型部分

打開(kāi)上面的鏈接,README中也提到了pytorch1.8以上的版本會(huì)有問(wèn)題,實(shí)際嘗試確實(shí)如此,總會(huì)在一個(gè)Conv的地方報(bào)錯(cuò),魔改了一番代碼還是無(wú)法解決,因此下載了車牌檢測(cè)的數(shù)據(jù)集本地重新訓(xùn)練模型。需要注意的是,和官方y(tǒng)olov8-pose的輸出結(jié)果中類別數(shù)目不同,因?yàn)榘凑赵搨}(cāng)庫(kù)的yaml文件設(shè)置會(huì)有兩類,因此后處理階段需要注意。

訓(xùn)練參數(shù)等不過(guò)多介紹,yolov8文檔十分詳細(xì)可以自己去查看。來(lái)看看最后導(dǎo)出的onnx模型。

最后輸出為14*8400,其中14=4+2+8,含義分別是bbox的四個(gè)點(diǎn),對(duì)應(yīng)兩個(gè)類別概率以及四個(gè)關(guān)鍵點(diǎn)的(x,y)坐標(biāo),后處理階段就要注意對(duì)應(yīng)的偏移量分別是4,2,8.

然后OCR模型直接用它提供的預(yù)訓(xùn)練權(quán)重導(dǎo)出就好,精度基本一致。得到onnx之后可以直接利用trtexec轉(zhuǎn)為對(duì)應(yīng)的engine文件。

2.2 C++部分

為推理引擎反序列化構(gòu)建,host以及device的內(nèi)存分配等共有操作實(shí)現(xiàn)基類,然后重載不同模型的構(gòu)造函數(shù)和前后處理函數(shù)。這個(gè)部分可以去參考網(wǎng)上一些開(kāi)源教程,大多模板一致。在這里有兩個(gè)點(diǎn)需要注意:

  • 如果希望兩個(gè)模型運(yùn)行在不同顯卡上,記得在所有有關(guān)上下文操作前后加上cudaSetDevice()
  • 對(duì)于不同模型,構(gòu)造函數(shù)傳參大多不一致,目前幾種解決方法:工廠模式輸入modelType對(duì)應(yīng)不同實(shí)例化,讀取json/yaml等配置文件參數(shù)實(shí)例化,最后一種惡心辦法無(wú)腦統(tǒng)一實(shí)例化接口,大不了某些參數(shù)不用。最優(yōu)方法當(dāng)然是寫配置文件,用yaml-cpp或者其他文件解析庫(kù)實(shí)現(xiàn)對(duì)配置文件參數(shù)解析,然后入?yún)⒕徒y(tǒng)一為配置文件路徑以及一些共有參數(shù)(如deviceId可以在服務(wù)端或者前端設(shè)置因此保留)??上н@個(gè)意見(jiàn)沒(méi)被接受,不得已提交的那一版寫的是最惡心的方式,后來(lái)改成了第一種通過(guò)傳入模型類別去實(shí)例化。

稍微說(shuō)說(shuō)前后處理部分,對(duì)于yolov8-pose之前說(shuō)了注意偏移量的問(wèn)題,另外就是對(duì)輸出轉(zhuǎn)置處理一下方便解析,當(dāng)然這個(gè)操作也可以在模型導(dǎo)出前改一下源碼實(shí)現(xiàn)。偏移部分實(shí)現(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)過(guò)nms篩選,得到最終保留結(jié)果。保存目標(biāo)的結(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;
};

對(duì)于OCR模型

模型輸入大小為(48,168),輸出為5和(21,78),其中5代表黑藍(lán)綠白黃五種車牌顏色,78代表78個(gè)可識(shí)別的字符包括開(kāi)頭的#號(hào)占位符,0-9的數(shù)字,英文字母以及中文漢字,21為最大識(shí)別車牌字符長(zhǎng)度。然后來(lái)看看OCR模型的前后處理,由于大貨車存在雙行車牌的情況,因此需要對(duì)車牌上下部分切分然后橫向拼接再給模型推理,大致實(shí)現(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);
}

僅僅對(duì)于label為1也就是雙行車牌進(jìn)行拼接操作,當(dāng)然這個(gè)是透視變換后的車牌。關(guān)于透視變換可以根據(jù)倉(cāng)庫(kù)中Python代碼翻譯出對(duì)應(yīng)的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ù)后處理一致,找最大值下標(biāo)即為對(duì)應(yīng)類別。注意遍歷識(shí)別字符時(shí)需要過(guò)濾操作,即對(duì)于下標(biāo)0和已識(shí)別出的相鄰?fù)瑯幼址M(jìn)行過(guò)濾。找最大值下標(biāo)可以利用std::distance()很方便的找到。

最后就是書寫對(duì)應(yīng)的cgo接口,相比起JNI直接根據(jù)類定義使用javah生成的頭文件來(lái)寫而言,cgo并沒(méi)有生成頭文件的工具,這也讓我們有更多的靈活性去定義對(duì)應(yīng)的接口。比如我的接口定義如下:

#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
?

因?yàn)間o不能調(diào)用c++的類,也不能使用c++的std::string等,所以這里全部是char*。然后實(shí)現(xiàn)對(duì)應(yīng)接口

#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;
}

這里分別實(shí)現(xiàn)了模型實(shí)例化,推理以及模型銷毀,最后推理結(jié)果返回的是json格式的字符串,這部分大多還是沿用之前JNI的寫法。最后就是寫個(gè)CMakeLists然后編譯,現(xiàn)在來(lái)看看C++上的推理結(jié)果圖

對(duì)于這種角度的車牌人眼都需要細(xì)看才能識(shí)別正確,模型居然也能正確識(shí)別,看來(lái)模型還是可以的,而且在家里這個(gè)服務(wù)器上推理耗時(shí)也僅僅1.3ms左右,速度與精度都完全可以接受。

2.3 Go部分

經(jīng)過(guò)一系列操作,我們終于編譯得到了.so動(dòng)態(tài)庫(kù)文件,現(xiàn)在就是加載這個(gè)動(dòng)態(tài)庫(kù)然后寫個(gè)服務(wù)今天的任務(wù)就算完成啦。來(lái)看看go調(diào)用動(dòng)態(tài)庫(kù)的部分,首先需要調(diào)用C的庫(kù),并且上面需要添加編譯注釋,同時(shí)保證二者之間不能有空行

/*
#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"

其實(shí)最主要就是第一行LDFLAGS去加載對(duì)應(yīng)的動(dòng)態(tài)庫(kù)。剩下的步驟就是根據(jù)剛才C++定義的函數(shù)來(lái)對(duì)應(yīng)寫Go的實(shí)現(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)換,對(duì)于推理后json字符串的解析等等略過(guò),最后用gin寫個(gè)簡(jiǎn)單的POST推理路由以及上傳路由。下面來(lái)看看效果:

傳入圖片:

推理結(jié)果:

成功識(shí)別出兩輛車的車牌,響應(yīng)延時(shí)為153ms,經(jīng)過(guò)多次測(cè)試,平均在100ms左右,對(duì)于單個(gè)車輛的圖片延時(shí)在50ms左右,基本滿足需求。

3. 最后

其實(shí)這部分內(nèi)容也是臨時(shí)想到的,后期打算用Rust也試試,看看到底哪個(gè)實(shí)現(xiàn)性能最高,再次挖坑。

以上就是Go調(diào)用C++動(dòng)態(tài)庫(kù)實(shí)現(xiàn)車牌識(shí)別的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Go調(diào)用C++實(shí)現(xiàn)車牌識(shí)別的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 源碼剖析Golang如何fork一個(gè)進(jìn)程

    源碼剖析Golang如何fork一個(gè)進(jìn)程

    創(chuàng)建一個(gè)新進(jìn)程分為兩個(gè)步驟,一個(gè)是fork系統(tǒng)調(diào)用,一個(gè)是execve?系統(tǒng)調(diào)用,本文將從源碼的角度帶大家剖析一下Golang是如何fork一個(gè)進(jìn)程的
    2023-06-06
  • 淺談go build后加文件和目錄的區(qū)別

    淺談go build后加文件和目錄的區(qū)別

    這篇文章主要介紹了淺談go build后加文件和目錄的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-12-12
  • Golang壓縮Jpeg圖片和PNG圖片的操作

    Golang壓縮Jpeg圖片和PNG圖片的操作

    這篇文章主要介紹了Golang壓縮Jpeg圖片和PNG圖片的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-12-12
  • Go Plugins插件的實(shí)現(xiàn)方式

    Go Plugins插件的實(shí)現(xiàn)方式

    目前 Plugins 僅在 Linux、FreeBSD 和 macOS 上受支持,且只支持 golang 調(diào)用,今天通過(guò)本文給大家介紹Go Plugins插件的實(shí)現(xiàn)方式,感興趣的朋友一起看看吧
    2021-08-08
  • 詳解Go語(yǔ)言中ErrGroup的使用

    詳解Go語(yǔ)言中ErrGroup的使用

    本文主要為大家詳細(xì)介紹了Go語(yǔ)言中errGroup的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2023-07-07
  • golang提示dial?tcp?172?.217.163.49:443:?connectex:?A?connection?attempt?failed解決

    golang提示dial?tcp?172?.217.163.49:443:?connectex:?A?con

    這篇文章主要為大家介紹了golang提示dial?tcp?172?.217.163.49:443:?connectex:?A?connection?attempt?failed解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • 淺析Golang中Gin框架存在的必要性

    淺析Golang中Gin框架存在的必要性

    在Go語(yǔ)言中,net/http?包提供了一個(gè)強(qiáng)大且靈活的標(biāo)準(zhǔn)HTTP庫(kù),那為什么還出現(xiàn)了像?Gin?這樣的,方便我們構(gòu)建Web應(yīng)用程序的第三方庫(kù),下面就來(lái)和大家簡(jiǎn)單分析一下
    2023-08-08
  • Golang學(xué)習(xí)之平滑重啟

    Golang學(xué)習(xí)之平滑重啟

    這篇文章主要介紹了Golang學(xué)習(xí)之平滑重啟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-08-08
  • Go語(yǔ)言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解

    Go語(yǔ)言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解

    本文將講解 sync.WaitGroup 的使用方法、原理以及在實(shí)際項(xiàng)目中的應(yīng)用場(chǎng)景,用清晰的代碼示例和詳細(xì)的注釋,助力讀者掌握并發(fā)編程中等待組的使用技巧
    2023-11-11
  • 一文帶你讀懂Golang?sync包之sync.Mutex

    一文帶你讀懂Golang?sync包之sync.Mutex

    sync.Mutex可以說(shuō)是sync包的核心了,?sync.RWMutex,?sync.WaitGroup...都依賴于他,?本章我們將帶你一文讀懂sync.Mutex,快跟隨小編一起學(xué)習(xí)一下吧
    2023-04-04

最新評(píng)論