#凌智视觉模块(基于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,全称为卷积递归神经网络(Convolutional Recurrent Neural Network),是一种专门设计用于解决基于图像的序列识别问题的深度学习模型。它特别适用于如文本识别这样的任务,其中输入数据不仅包含空间信息(例如图像中的字符布局),还包含序列信息(例如字符之间的顺序)。CRNN结合了卷积神经网络(CNN)和循环神经网络(RNN)的优点,以一种端到端的方式进行训练,无需复杂的预处理和分割步骤。
卷积层(CNN):CRNN的前端是一个标准的卷积神经网络,用于从输入图像中提取特征。通过一系列卷积操作、激活函数以及池化操作,该网络能够有效地捕捉输入图像的局部特征,并将原始图像转换为一个特征序列。这个过程极大地减少了数据维度,同时保留了对后续文字识别至关重要的特征信息。
循环层(RNN):在卷积层之后,CRNN使用了一个或多个循环神经网络层来处理由卷积层生成的特征序列。这些循环层通常采用长短期记忆单元(LSTM)或门控循环单元(GRU),它们擅长捕捉时间序列数据中的依赖关系。在这个阶段,每个时间步的输入是卷积层输出的特征序列中的一个向量,而输出则是一系列特征向量,每个都包含了关于特定位置上字符的信息。
转录层(CTC层):最后,CRNN模型使用连接时序分类(Connectionist Temporal Classification, CTC)层来将循环层的输出转化为最终的标签序列。CTC损失函数允许模型直接从图像中学习如何预测标签序列,而不需要对每个字符的位置进行精确标注。这使得模型可以处理不定长的序列数据,并且简化了训练过程。
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);
#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;
}