Artificial Intelligence 12 min read

Building a License Plate Recognition Service with C++, TensorRT, and Go

This article details how to train a YOLOv8‑pose model for license‑plate detection, convert it to TensorRT engine, implement C++ inference and preprocessing, expose the functionality via CGO to Go, and assemble a lightweight web service for real‑time plate recognition.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Building a License Plate Recognition Service with C++, TensorRT, and Go

After a long hiatus, the author updates the blog with a practical project that compiles C++ libraries (OpenCV, TensorRT) into a shared object for Go, creating an API that performs license‑plate recognition on uploaded vehicle images.

Model part: The YOLOv8‑pose model is retrained on a custom plate dataset, producing an ONNX output of shape 14×8400 (4 bbox coordinates, 2 class scores, 8 key‑point coordinates). The ONNX model is then converted to a TensorRT engine using trtexec . An OCR model, using pretrained weights, is also exported to ONNX for character recognition.

C++ part: A base class handles engine deserialization, memory allocation, and common inference steps. Specific model subclasses implement construction and pre/post‑processing. Important notes include using cudaSetDevice() for multi‑GPU setups and employing yaml-cpp (or similar) for configuration files. The post‑processing extracts bounding boxes, scores, and key points, then applies NMS. Sample code snippets illustrate pointer handling and the Object struct definition.

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;

The OCR pipeline resizes plates to (48,168), predicts color (5 classes) and characters (78 classes), and merges double‑row plates when necessary. Perspective transformation and plate merging functions are provided.

// 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)));
}

Go part: The shared library is loaded via CGO with appropriate LDFLAGS and CPPFLAGS. Wrapper functions init , detect , and release are exposed, and Go structs/methods call these functions to perform inference and obtain JSON results.

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))
    return C.GoString(res)
}

A simple Gin HTTP server provides POST endpoints for image upload and inference, achieving average latencies around 100 ms (≈50 ms for a single image) and correctly recognizing multiple plates.

Future work includes experimenting with Rust implementations to compare performance.

GoC++TensorRTModel InferencecgoLicense Plate RecognitionYOLOv8
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.