[凌智视觉模块] 基于rv1106实现车牌识别

#凌智视觉模块(基于rv1106) 车牌识别
我们的仓库地址LockzhinerAI,如有需要,可以前往我们的仓库进行查看。
车牌识别是一种基于计算机视觉和深度学习的技术,它利用图像处理、字符分割以及光学字符识别(OCR)算法自动提取车辆牌照中的文字与数字信息。该技术能够实时识别在不同光照条件、角度变化以及复杂背景下的车牌,广泛应用于智能交通管理、停车场收费系统及电子警察执法等领域。

在AI Rockchip的RKNN模型库中虽然支持LPRNet模型,但在使用RV1106板子进行测试时,遇到了一个特定算子ReduceMean不被支持的问题,导致无法直接应用LPRNet模型。为了解决这一问题并实现字符识别功能,我们转向采用CRNN OCR模型,并通过NCNN框架进行了部署。

在此过程中,我们在Lockzhiner Vision Module上基于PaddleDet目标检测类和CRNN文本识别模型实现了一个车牌识别案例。选择CRNN OCR模型的原因在于其对不同环境下的字符识别具有较高的鲁棒性。尽管转换过程中面临了一些挑战,如模型适配和性能调优等,但最终成功地克服了这些问题。

为了进一步优化我们的解决方案,我们对比了新旧方案在准确率和处理速度方面的表现。结果显示,采用CRNN OCR模型的新方案不仅解决了硬件兼容性问题,还在识别准确率上有所提升。

对于有兴趣深入了解或尝试类似解决方案的朋友,建议查阅PaddleDet官方文档和NCNN项目页面,这些资源提供了详细的指南和示例代码,有助于快速上手并应用于实际项目中。

文本识别CRNN模型介绍

CRNN,全称为卷积递归神经网络(Convolutional Recurrent Neural Network),是一种专门设计用于解决基于图像的序列识别问题的深度学习模型。它特别适用于如文本识别这样的任务,其中输入数据不仅包含空间信息(例如图像中的字符布局),还包含序列信息(例如字符之间的顺序)。CRNN结合了卷积神经网络(CNN)和循环神经网络(RNN)的优点,以一种端到端的方式进行训练,无需复杂的预处理和分割步骤。

模型架构

  1. 卷积层(CNN):CRNN的前端是一个标准的卷积神经网络,用于从输入图像中提取特征。通过一系列卷积操作、激活函数以及池化操作,该网络能够有效地捕捉输入图像的局部特征,并将原始图像转换为一个特征序列。这个过程极大地减少了数据维度,同时保留了对后续文字识别至关重要的特征信息。

  2. 循环层(RNN):在卷积层之后,CRNN使用了一个或多个循环神经网络层来处理由卷积层生成的特征序列。这些循环层通常采用长短期记忆单元(LSTM)或门控循环单元(GRU),它们擅长捕捉时间序列数据中的依赖关系。在这个阶段,每个时间步的输入是卷积层输出的特征序列中的一个向量,而输出则是一系列特征向量,每个都包含了关于特定位置上字符的信息。

  3. 转录层(CTC层):最后,CRNN模型使用连接时序分类(Connectionist Temporal Classification, CTC)层来将循环层的输出转化为最终的标签序列。CTC损失函数允许模型直接从图像中学习如何预测标签序列,而不需要对每个字符的位置进行精确标注。这使得模型可以处理不定长的序列数据,并且简化了训练过程。

车牌识别流程

  • 车牌检测:
    • 深度学习​​:YOLO、Faster R-CNN等检测模型定位车牌区域。
    • 传统方法:颜色空间分析(蓝色/黄色车牌)+边缘检测。
  • 车牌矫正:透视变换调整倾斜角度,Hough直线检测旋转角度。
  • 字符分割:​​垂直投影法分割字符,连通域分析处理粘连字符。
  • 字符识别:
    • CRNN​​:CNN+RNN+CTC结构,端到端识别。

3. 车牌识别代码解析

3.2 核心代码解析

  • 初始化车牌检测模型
lockzhiner_vision_module::vision::PaddleDet detector;
if (!detector.Initialize(argv[1])) {
    cerr << "Failed to load detection model: " << argv[1] << endl;
    return 1;
}
  • 车牌检测模型推理
auto results = detector.Predict(image);
  • 可视化并显示推理结果
cv::Mat output_image = image.clone();
for (const auto& det : results) {
    cv::Rect rect(
        static_cast<int>(det.box.x),
        static_cast<int>(det.box.y),
        static_cast<int>(det.box.width),
        static_cast<int>(det.box.height)
    );
    cv::rectangle(
        output_image,
        rect,
        cv::Scalar(0, 255, 0),
        1,
        cv::LINE_AA
    );
}
cv::imshow("Detection Result", output_image);
  • 加载字符识别参数和模型
ocr_net.load_param(param_path.c_str())
ocr_net.load_model(model_path.c_str())
  • 归一化处理
const float mean[3] = {127.5f, 127.5f, 127.5f};
const float norm[3] = {0.0078125f, 0.0078125f, 0.0078125f};
in.substract_mean_normalize(mean, norm);
  • 解码预测结果
string license;
vector<int> preb;
for (int c = 0; c < feat.c; c++) {
    const float* data = feat.channel(c);
    for (int w = 0; w < feat.w; w++) {
        float max_val = -FLT_MAX;
        int max_idx = 0;
        for (int h = 0; h < feat.h; h++) {
            float val = data[w + h * feat.w];
            if (val > max_val) {
                max_val = val;
                max_idx = h;
            }
        }
        preb.push_back(max_idx);
    }
}

// 后处理去重
vector<int> valid_labels;
int prev = -1;
for (size_t i = 0; i < preb.size(); ++i) {
    if (preb[i] != 67 && preb[i] != prev) {
        valid_labels.push_back(preb[i]);
    }
    prev = preb[i];
}

for (int idx : valid_labels) {
    license += plate_chars[idx];
}

自定义函数说明

  • 车牌字符识别
string RecognizePlate(cv::Mat plate_img);
  • 作用:对分割出来的车牌进行识别。
  • 参数说明:
    • plate_image:待识别的车牌图像。
  • 返回值:返回一串字符串类型的数据,表示识别到的车牌是什么。

3.3 完整代码实现

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "myfontface.h"

using namespace std;
using namespace std::chrono;

// OCR字符集配置
const string plate_chars[68] = {
    "京", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙",
    "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵",
    "云", "藏", "陕", "甘", "青", "宁", "新", "0",  "1",  "2",  "3",  "4",
    "5",  "6",  "7",  "8",  "9",  "A",  "B",  "C",  "D",  "E",  "F",  "G",
    "H",  "J",  "K",  "L",  "M",  "N",  "P",  "Q",  "R",  "S",  "T",  "U",
    "V",  "W",  "X",  "Y",  "Z",  "I",  "O",  "-"};

ncnn::Net ocr_net;

bool InitOCRModel(const string& param_path, const string& model_path) {
    return ocr_net.load_param(param_path.c_str()) == 0 &&
           ocr_net.load_model(model_path.c_str()) == 0;
}

string RecognizePlate(cv::Mat plate_img) {
    cv::resize(plate_img, plate_img, cv::Size(94, 24));
    
    ncnn::Mat in = ncnn::Mat::from_pixels(plate_img.data, 
                                        ncnn::Mat::PIXEL_BGR,
                                        plate_img.cols, 
                                        plate_img.rows);

    const float mean[3] = {127.5f, 127.5f, 127.5f};
    const float norm[3] = {0.0078125f, 0.0078125f, 0.0078125f};
    in.substract_mean_normalize(mean, norm);

    ncnn::Extractor ex = ocr_net.create_extractor();
    ex.input("input.1", in);
    
    ncnn::Mat feat;
    ex.extract("131", feat);

    string license;
    vector<int> preb;
    for (int c = 0; c < feat.c; c++) {
        const float* data = feat.channel(c);
        for (int w = 0; w < feat.w; w++) {
            float max_val = -FLT_MAX;
            int max_idx = 0;
            for (int h = 0; h < feat.h; h++) {
                float val = data[w + h * feat.w];
                if (val > max_val) {
                    max_val = val;
                    max_idx = h;
                }
            }
            preb.push_back(max_idx);
        }
    }

    // 后处理去重
    vector<int> valid_labels;
    int prev = -1;
    for (size_t i = 0; i < preb.size(); ++i) {
        if (preb[i] != 67 && preb[i] != prev) {
            valid_labels.push_back(preb[i]);
        }
        prev = preb[i];
    }

    for (int idx : valid_labels) {
        license += plate_chars[idx];
    }

    return license.empty() ? "UNKNOWN" : license;
}

int main(int argc, char** argv) {
    // 参数验证
    if (argc < 4 || argc > 5) {
        cerr << "Usage: " << argv[0] 
             << "    [image_path]\n"
             << "Example:\n"
             << "  Realtime: " << argv[0] << " det_model ocr.param ocr.bin\n"
             << "  Image:    " << argv[0] << " det_model ocr.param ocr.bin test.jpg\n";
        return 1;
    }

    // 初始化检测模型
    lockzhiner_vision_module::vision::PaddleDet detector;
    if (!detector.Initialize(argv[1])) {
        cerr << "Failed to load detection model: " << argv[1] << endl;
        return 1;
    }

    // 初始化OCR模型
    if (!InitOCRModel(argv[2], argv[3])) {
        cerr << "Failed to load OCR model: " << argv[2] << " and " << argv[3] << endl;
        return 1;
    }
    MyFontFace myfont;
    // 设置文字参数
    double font_scale = 0.6;
    int thickness = 1;

    // 图片处理模式
    if (argc == 5) {
        cv::Mat image = cv::imread(argv[4]);
        if (image.empty()) {
            cerr << "Failed to read image: " << argv[4] << endl;
            return 1;
        }

        auto results = detector.Predict(image);
        // 可视化并显示结果
        cv::Mat output_image = image.clone();
        for (const auto& det : results) {
            cv::Rect rect(
                static_cast<int>(det.box.x),
                static_cast<int>(det.box.y),
                static_cast<int>(det.box.width),
                static_cast<int>(det.box.height)
            );
            cv::rectangle(
                output_image,
                rect,
                cv::Scalar(0, 255, 0),
                1,
                cv::LINE_AA
            );
        }
        cout << "\n===== 检测到 " << results.size() << " 个车牌 =====" << endl;

        for (size_t i = 0; i < results.size(); ++i) {
            const auto& det = results[i];
            cv::Rect roi(det.box.x, det.box.y, det.box.width, det.box.height);
            roi &= cv::Rect(0, 0, image.cols, image.rows);

            if (roi.area() > 0) {
                cv::Mat plate_img = image(roi);
                cv::imshow("DetectionSeg Result", plate_img);
                string plate_num = RecognizePlate(plate_img);
                // 左上角偏移
                cv::Point text_org(roi.x + 2, roi.y - 2); 
                // 先绘制黑色背景提升可读性
                cv::putText(output_image, plate_num, text_org,
                    cv::Scalar(0, 0, 0),   // 颜色
                    myfont,                // 字体对象
                    font_scale,            // 字体尺寸
                    thickness + 2,         // 线宽
                    cv::PutTextFlags::PUT_TEXT_ALIGN_LEFT,  // 对齐方式
                    cv::Range(0, 0));                     // 自动换行范围(0表示不换行)
                cv::putText(output_image, plate_num, text_org, cv::Scalar(127, 0, 127), myfont, 10);

                cout << "车牌 " << i+1 << ":\n"
                     << "  位置: [" << roi.x << ", " << roi.y 
                     << ", " << roi.width << "x" << roi.height << "]\n"
                     << "  置信度: " << det.score << "\n"
                     << "  识别结果: " << plate_num << "\n" << endl;

                cv::imshow("Detection Result", output_image);
            }
        }
        cv::waitKey(0);
    }
    // 实时摄像头模式
    else {
        // 初始化设备连接
        lockzhiner_vision_module::edit::Edit edit;
        if (!edit.StartAndAcceptConnection()) {
            std::cerr << "Error: Failed to start and accept connection." << std::endl;
            return EXIT_FAILURE;
        }
        std::cout << "Device connected successfully." << std::endl;

        cv::VideoCapture cap;
        cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
        cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
        cap.open(0);
        if (!cap.isOpened()) {
            cerr << "Error: Could not open camera" << endl;
            return 1;
        }

        cout << "实时识别模式启动 (按ESC退出)..." << endl;

        cv::Mat frame;
        while (true) {
            cap >> frame;
            if (frame.empty()) continue;

            auto results = detector.Predict(frame);

            cv::Mat display_frame = frame.clone();
            for (const auto& det : results) {
                cv::Rect rect(
                    static_cast<int>(det.box.x),
                    static_cast<int>(det.box.y),
                    static_cast<int>(det.box.width),
                    static_cast<int>(det.box.height)
                );
                cv::rectangle(
                    display_frame,
                    rect,
                    cv::Scalar(0, 255, 0),
                    1,
                    cv::LINE_AA
                );
            }

            // 添加时间戳
            auto now = system_clock::now();
            time_t now_time = system_clock::to_time_t(now);
            cout << "\n===== " << ctime(&now_time)
                 << "检测到 " << results.size() << " 个车牌 =====" << endl;

            for (const auto& det : results) {
                cv::Rect roi(det.box.x, det.box.y, det.box.width, det.box.height);
                roi &= cv::Rect(0, 0, frame.cols, frame.rows);

                if (roi.area() > 0) {
                    cv::Mat plate_img = frame(roi);
                    string plate_num = RecognizePlate(plate_img);
                    // 左上角偏移
                    cv::Point text_org(roi.x + 2, roi.y - 2); 
                    // 先绘制黑色背景提升可读性
                    cv::putText(display_frame, plate_num, text_org,
                        cv::Scalar(0, 0, 0),   // 颜色
                        myfont,                // 字体对象
                        font_scale,            // 字体尺寸
                        thickness + 2,         // 线宽
                        cv::PutTextFlags::PUT_TEXT_ALIGN_LEFT,  // 对齐方式
                        cv::Range(0, 0));                     // 自动换行范围(0表示不换行)
                    cv::putText(display_frame, plate_num, text_org, cv::Scalar(127, 0, 127), myfont, 10);

                    cout << "[实时结果] 位置(" << roi.x << "," << roi.y 
                         << ") 识别: " << plate_num 
                         << " (置信度: " << det.score << ")" << endl;
                }
            }
            edit.Print(display_frame);
            // 退出检测
            if (cv::waitKey(1) == 27) break;
        }
    }
    return 0;
}

结果

[凌智视觉模块] 基于rv1106实现车牌识别_第1张图片

你可能感兴趣的:(视觉模型部署实践,嵌入式硬件,iot,c++)