1.1 项目开发背景
随着智能交通、停车场管理、违章监测等领域的快速发展,车牌识别作为交通信息采集的核心技术,已成为智能化管理的关键环节。传统车牌识别方案存在三大痛点:一是依赖专用硬件(如高清工业相机、专用识别终端),成本高且部署灵活度低;二是识别稳定性受环境影响大(如雨天、逆光、车牌污损场景识别率骤降);三是交互界面缺失,多为后端服务,普通用户无法直观操作与监控。
基于 Linux+OpenCV+Qt+EasyPR-master 的车牌识别系统应运而生——EasyPR-master 是一款开源的中文车牌识别项目,集成车牌检测、定位、字符分割与识别全流程算法,搭配 OpenCV 的图像处理能力、Qt 的可视化界面优势及 Linux 系统的稳定性与硬件兼容性,可实现 “低成本部署、高鲁棒性识别、可视化交互” 的核心目标。
该系统适用于小区停车场管理、路边停车计费、单位门禁车辆登记等场景,所有识别逻辑在 Linux 本地完成,无需依赖云端服务,同时通过 Qt 构建直观操作界面,支持实时预览、识别结果展示、记录查询导出,兼顾技术实用性与用户体验,具有显著的落地价值与推广前景。
1.2 设计实现的功能
(1)实时车牌检测与识别:支持通过 Linux 摄像头实时采集车辆图像,每秒处理 10-15 帧,自动完成车牌定位、字符分割与识别,2 秒内返回识别结果(车牌号码 /“识别失败”),支持蓝牌、黄牌、新能源车牌识别。
(2)多场景适配:通过算法参数调节,适配顺光、逆光、夜间补光等不同光照场景,对轻微污损、倾斜(≤15°)的车牌仍保持较高识别率(≥90%)。
(3)识别记录管理:自动记录每次识别的车牌号码、识别时间、置信度、拍摄图像路径,存储于本地数据库,支持按时间、车牌号码检索记录。
(4)可视化交互:通过 Qt 界面显示实时摄像头画面(含车牌定位框)、识别结果(车牌号码 + 置信度)、识别记录列表,提供 “开始识别”“暂停识别”“导出记录”“查看历史图像” 等操作按钮。
(5)数据导出与备份:支持将识别记录导出为 CSV 文件,拍摄的车牌图像可按 “日期 + 车牌” 命名存储,支持数据库定期自动备份,防止数据丢失。
(6)异常处理:摄像头未连接时弹出提示框;图像中未检测到车牌时标记 “无车牌”;识别置信度低于阈值(如 70%)时标记 “待确认”,并保留图像供人工复核。
1.3 项目软件模块组成
(1)核心算法层:EasyPR-master+OpenCV 模块
作为车牌识别的技术核心,EasyPR-master 封装车牌检测(基于颜色与形状特征)、字符分割(投影法 + 连通域分析)、字符识别(SVM 分类器)全流程算法;OpenCV 负责图像预处理(灰度化、降噪、直方图均衡化),为 EasyPR 提供高质量图像输入,同时辅助完成车牌定位框绘制、图像格式转换等操作。
(2)界面交互层:Qt GUI 模块
包含主窗口(功能控制区 + 预览区 + 结果显示区)、记录查询窗口(按条件检索记录)、图像查看窗口(查看识别时的原始图像)、参数配置窗口(调节识别算法阈值、摄像头参数),通过组件化设计实现直观的人机交互。
(3)数据存储层:SQLite 本地数据库模块
存储两类核心数据:一是车牌识别记录表(记录 ID、车牌号码、识别时间、置信度、图像存储路径);二是系统配置表(摄像头分辨率、识别置信度阈值、自动备份周期),支持数据的增删改查、导出与备份。
(4)系统适配层:Linux 硬件与资源管理模块
负责 Linux 摄像头设备枚举与初始化(基于 v4l2 驱动)、图像帧采集与格式转换(YUV→RGB→cv::Mat)、系统资源监控(CPU / 内存占用,避免识别算法卡顿)、图像文件管理(按日期分类存储车牌图像),保障系统在 Linux 环境下稳定运行。
1.4 设计思路
本项目以 “低成本、高鲁棒性、可视化” 为核心目标,构建 “图像采集→预处理→车牌识别→结果反馈→数据存储” 的全链路车牌识别系统,整体设计逻辑如下:
1. 层间协作逻辑
用户通过 Qt 界面发起 “开始识别” 操作→系统适配层初始化 Linux 摄像头,采集实时图像帧→OpenCV 对图像帧进行预处理(灰度化、高斯降噪、直方图均衡化)→核心算法层(EasyPR-master)接收预处理图像,完成车牌检测→定位→字符分割→字符识别→识别结果(车牌号码 + 置信度)反馈至 Qt 界面显示→同时将识别记录(含车牌、时间、置信度、图像路径)写入 SQLite 数据库,原始图像按规则存储至本地文件夹。
2. 核心流程设计
(1)车牌识别流程:
Qt 界面触发 “开始识别”→Linux 摄像头启动(分辨率设为 1280×720)→系统适配层每秒采集 10-15 帧图像→OpenCV 预处理(降低噪声、增强对比度)→EasyPR-master 调用CPlateRecognize类的PlateRecognize接口→输出车牌信息(号码、颜色、置信度)→若置信度≥70%,直接显示结果;若 70%> 置信度≥50%,标记 “待确认”;若 < 50%,显示 “识别失败”→同步记录数据与图像。
(2)记录管理流程:
识别完成后,系统自动将记录写入数据库→用户可通过 Qt 界面输入车牌号码 / 选择时间范围,检索历史记录→点击记录可查看对应的原始图像→支持批量导出记录为 CSV(含车牌、时间、置信度字段)→数据库每日凌晨 3 点自动备份至指定路径。
3. 性能优化逻辑
通过 Linux 的 v4l2 驱动直接读取摄像头原始帧,减少图像采集延迟;OpenCV 预处理阶段采用高斯滤波(3×3 核)快速降噪,避免复杂算法占用资源;Qt 采用多线程设计(主线程负责界面刷新,子线程负责图像采集与识别),防止界面卡顿;EasyPR-master 启用 “快速识别模式”,关闭冗余检测步骤,平衡识别速度与精度。
1.5 开发环境介绍
环境类别 | 具体配置 | 选择理由 |
操作系统 | Ubuntu 22.04 LTS | LTS 版本稳定性强,对 OpenCV、Qt、EasyPR-master 兼容性好,支持 v4l2 摄像头驱动 |
编程语言 | C++ 11 | EasyPR-master、OpenCV、Qt 的核心接口均为 C++,执行效率高,适配实时识别需求 |
核心库 | OpenCV 4.8.0、Qt 5.15 LTS、EasyPR-master、SQLite 3.37.2 | OpenCV 提供图像处理能力,Qt 实现可视化界面,EasyPR-master 封装车牌识别算法,SQLite 轻量易集成 |
开发工具 | Qt Creator 10.0.2、VS Code(C++ 插件) | Qt Creator 集成 Qt 编译与界面设计,VS Code 支持代码高亮与调试,提升开发效率 |
辅助工具 | v4l2-utils、cmake、git | v4l2-utils 测试摄像头,cmake 编译 EasyPR-master,git 获取 EasyPR 源码 |
1.6 环境部署关键步骤
(1)安装依赖库
# 安装OpenCV、Qt、SQLite基础依赖sudo apt-get install libopencv-dev qt5-default qtcreator sqlite3 libsqlite3-dev# 安装编译工具与摄像头驱动sudo apt-get install cmake git v4l2-utils
(2)获取并编译 EasyPR-master:
# 克隆源码git clone https://github.com/liuruoze/EasyPR.git EasyPR-mastercd EasyPR-master# 创建编译目录mkdir build && cd build# 配置编译(指定OpenCV路径,默认/usr/local)cmake .. -DOpenCV_DIR=/usr/local/share/opencv4# 编译生成库文件(libeasypr.so)make -j4# 将库文件复制到系统库路径sudo cp libeasypr.so /usr/local/lib/
(3)Qt 项目配置:
在 Qt 项目的.pro文件中添加依赖:
QT += core gui widgets sql multimedia# 链接EasyPR、OpenCV、SQLite库LIBS += -L/usr/local/lib -leasypr `pkg-config --libs opencv4` -lsqlite3# 包含头文件路径INCLUDEPATH += /usr/local/include \/path/to/EasyPR-master/include
1.7 模块技术详情介绍
1. 核心算法层:EasyPR-master+OpenCV
EasyPR-master 通过CPlateRecognize类封装全流程识别接口,核心技术流程如下:
(1)车牌检测:采用 “颜色定位 + 形状筛选” 双阶段检测
颜色定位:在 HSV 颜色空间中,根据蓝牌(H:100-140, S:43-255, V:46-255)、黄牌(H:20-34, S:43-255, V:46-255)的颜色范围,提取候选车牌区域;
形状筛选:计算候选区域的宽高比(车牌标准宽高比约 3.2-3.8)与边缘特征,过滤非车牌区域,得到精准车牌位置。
(2)字符分割:基于投影法 + 连通域分析
对车牌区域进行二值化处理,沿水平方向投影,分割出单个字符区域;
通过连通域分析去除噪声干扰,修正倾斜字符,得到标准化字符图像(16×32 像素)。
(3)字符识别:基于 SVM 分类器
加载预训练的字符识别模型(svm.xml),将标准化字符图像输入 SVM,输出字符(含汉字、字母、数字),拼接得到完整车牌号码。
2. OpenCV 辅助处理
(1)图像预处理:
灰度化:cvtColor(frame, gray, COLOR_BGR2GRAY),减少 3 倍计算量;
(2)高斯降噪:GaussianBlur(gray, blur, Size(3,3), 0),消除椒盐噪声;
(3)直方图均衡化:equalizeHist(blur, eq_gray),增强逆光场景对比度。
(4)结果可视化:
通过rectangle(frame, plateRect, Scalar(0,255,0), 2)绘制绿色车牌定位框,putText(frame, plateNum, Point(10,30), FONT_HERSHEY_SIMPLEX, 1, Scalar(0,0,255), 2)叠加显示车牌号码。
3. 界面交互层
(1)核心界面组件
主窗口(MainWindow):
左侧功能控制区:“开始识别”“暂停识别”“导出记录”“参数配置” 按钮;
中间预览区:QVideoWidget显示带车牌定位框的实时画面;
右侧结果显示区:QLabel显示当前识别结果(车牌号码 + 置信度),QTableWidget显示最近 10 条识别记录。
记录查询窗口(QueryWindow):
提供车牌号码输入框、时间范围选择器,查询结果以表格形式展示,支持双击记录查看原始图像。
参数配置窗口(ConfigWindow):
调节摄像头分辨率(640×480/1280×720)、识别置信度阈值(50%-90%)、图像存储路径,设置后自动保存至数据库。
(2)摄像头交互
基于 Qt 的:
QCamera类
QCameraViewfinder类
QCameraImageCapture类
实现摄像头控制:初始化摄像头并设置分辨率;每秒捕获 10-15 帧图像,转换为cv::Mat格式传递给算法层;接收算法层处理后的图像(含定位框),转换为QImage显示在QVideoWidget。
二、Linux 下代码设计
2.1 相关模块驱动代码
void MainWindow::my_face_car_id(Mat src){ // 1. 绿牌颜色筛选(HSV精准阈值) Mat green_mask, green_roi, img_HSV; cvtColor(src, img_HSV, COLOR_BGR2HSV); Scalar green_low(40, 60, 50); // 绿牌低阈值(避免蓝牌干扰) Scalar green_high(70, 255, 255); // 绿牌高阈值 inRange(img_HSV, green_low, green_high, green_mask); // 形态学闭操作(填充绿色区域空洞,确保绿牌区域完整) Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5)); morphologyEx(green_mask, green_mask, MORPH_CLOSE, kernel); src.copyTo(green_roi, green_mask); // 提取绿色ROI // 2. 基于绿色ROI定位绿牌轮廓 vector <vector> contours; </vector Mat gray, edges; cvtColor(green_roi, gray, COLOR_BGR2GRAY); equalizeHist(gray, gray); GaussianBlur(gray, gray, Size(5, 5), 0); Canny(gray, edges, 50, 150); // 边缘检测 findContours(edges, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 找最外层轮廓 vector plateVec; // 筛选绿牌轮廓(按尺寸:宽高比2.5-3.5,面积15000-50000) for(size_t i = 0; i < contours.size(); i++) { Rect rect = boundingRect(contours[i]); float ratio = (float)rect.width / rect.height; int area = rect.width * rect.height; if(ratio >= 2.5 && ratio <= 3.5 && area >= 15000 && area <= 50000) { rectangle(src, rect, Scalar(0, 255, 0), 2); // 绿框标记绿牌 Mat plate_crop = src(rect); // 截取绿牌区域 vector temp; pr.plateRecognize(plate_crop, temp); // 识别截取的绿牌 if(!temp.empty()) plateVec.push_back(temp[0]); } } // 3. 通用车牌识别(补充识别蓝牌/黄牌) if(plateVec.empty()) { pr.plateRecognize(src, plateVec); } // 4. 处理识别结果(核心修复:不依赖getPlateRect()) if(!plateVec.empty()) { CPlate plate = plateVec[0]; Mat plateImg = plate.getPlateMat(); // 获取车牌图像(存在的接口) if(!plateImg.empty()) { ui->label_5->setPixmap(QPixmap::fromImage(Mat2Image(plateImg))); } // 解析车牌信息 string str = plate.getPlateStr(); QString lice = QString::fromLocal8Bit(str.c_str()); int colon = lice.indexOf(':'); if(colon == -1) return; QString type = lice.left(colon); num = lice.mid(colon+1).trimmed(); // 绿牌判定(修复:用绿色ROI的面积+字符长度,替代getPlateRect()) int greenArea = countNonZero(green_mask); // 绿色区域总面积 bool isGreenAreaValid = (greenArea > 15000 && greenArea < 50000); // 绿牌面积范围 bool is8Char = (num.length() == 8); // 绿牌字符为8位(蓝牌7位) // 精准判定绿牌:绿色面积达标 + 字符8位 if(isGreenAreaValid && is8Char) { lice = "绿牌:" + num; } // 强制修正蓝牌(避免误判) else if (type == "蓝牌" && num.length() == 7) { lice = "蓝牌:" + num; } // 其他情况视为无效(如识别错误) else { ui->label_5->clear(); ui->lineEdit_2->clear(); return; } // 5. 多帧验证(2帧一致,提高准确率) if(old_id == lice) { count++; if(count >= 2) { ui->lineEdit_2->setText(lice); id = lice;// face_flag = 0; count = 0; query = QSqlQuery(db); if(!db.isOpen()) { qDebug() << "数据库连接已关闭,无法执行操作"; return; } db.transaction(); // 开始事务 // 检查数据库中是否已经存在该车牌信息 query.prepare("SELECT * FROM my_car WHERE id = :id"); query.bindValue(":id", id); if(query.exec() && query.next()) { save_car_info(); } else{ // 车辆进入停车场,插入新记录 query.prepare("INSERT INTO my_car (id, input, output, money) VALUES (:id, :input, NULL, NULL);"); query.bindValue(":user", user); query.bindValue(":id", id); query.bindValue(":phone_id", phone_id); // 将 QDateTime 转换为字符串 query.bindValue(":input", input_time.toString("yyyy-MM-dd HH:mm:ss")); bool res = query.exec(); if(!res) { qDebug() << "添加失败:" << query.lastError().text(); qDebug() << "最后执行的 SQL:" << query.lastQuery(); db.rollback(); // 回滚事务 } else { qDebug() << "添加成功,车辆进入停车场"; QMessageBox *m_box = new QMessageBox(QMessageBox::Information,QString("车辆信息提示"),QString("车辆已进入停车场")); QTimer::singleShot(500,m_box,SLOT(accept())); m_box->exec(); ui->label_5->setPixmap(QPixmap()); db.commit(); // 提交事务 if(fd >= 0) { // 先查询数据库,判断该车牌是否已出库(即不在my_car表中) query.prepare("SELECT * FROM my_car WHERE id = :id AND output IS NULL"); query.bindValue(":id", num); // num是车牌号码(不含类型前缀) bool isInParking = false; if(query.exec() && query.next()) { // 查询到记录:车辆仍在场(未出库),不发送 isInParking = true; qDebug() << "车牌" << num << "仍在场,不发送串口数据"; } // 若不在数据库(已出库),则发送串口数据 if(!isInParking) { QTextCodec *codec = QTextCodec::codecForName("GB2312"); if(!codec) { codec = QTextCodec::codecForName("GBK"); // 兼容处理 } QByteArray data = "P:" + (codec ? codec->fromUnicode(num) : num.toLocal8Bit()); data.append('\0'); // 结束符 write(fd, data.data(), data.length()); qDebug() << "车牌" << num << "已出库,发送串口数据:" << data< <endl;< span> </endl;<> } } currentParkingSpaces--; if(currentParkingSpaces < 0) { currentParkingSpaces = 0; QMessageBox::warning(this, "车位已满", "停车场已满,请等待其他车辆离场"); } } } } } else { old_id = lice; count = 1; } } else { // 未识别到车牌 ui->label_5->clear(); ui->lineEdit_2->clear(); }}//打开摄像头void MainWindow::on_open_button_clicked(){ if(capeture.isOpened()) { QMessageBox::information(this, "提示", "摄像头已处于打开状态"); return; } capeture.open(0); if(!capeture.isOpened()) { QMessageBox::critical(this, "错误", "无法打开摄像头,请检查设备连接"); return; } // 使用第二段代码的摄像头参数设置 capeture.set(CAP_PROP_FRAME_WIDTH, 1280); capeture.set(CAP_PROP_FRAME_HEIGHT, 720); capeture.set(CAP_PROP_AUTO_EXPOSURE, 1); capeture.set(CAP_PROP_AUTO_WB, 1); timer->start(30);// QMessageBox::information(this, "成功", "摄像头已打开");}void MainWindow::readFrame(){ capeture >> frame; // 读取帧// if(face_flag==1)// { my_face_car_id(frame); imag = Mat2Image(frame); // 将Mat转换为QImage ui->label_2->setPixmap(QPixmap::fromImage(imag)); // 显示在界面上 ui->label_5->clear(); ui->lineEdit_2->clear();}// --------------- 新增:计算停车时长并格式化 ---------------// 输入:入场时间(input_time_db)、出场时间(output_time)// 输出:格式化的时长字符串(如"2小时30分钟")QString calculateParkDuration(QDateTime inputTime, QDateTime outputTime) { // 1. 计算两个时间的总秒数差(确保outputTime >= inputTime,避免负数) qint64 totalSeconds = inputTime.secsTo(outputTime); if(totalSeconds < 0) totalSeconds = 0; // 异常情况处理(时间倒序) // 2. 转换为“小时、分钟、秒”(日常停车时长可忽略秒,保留到分钟) int hours = totalSeconds / 3600; // 小时(1小时=3600秒) int minutes = (totalSeconds % 3600) / 60; // 剩余秒数转换为分钟 // int seconds = totalSeconds % 60; // 剩余秒数(可选,按需保留) // 3. 格式化字符串(根据时长灵活显示,如“1小时5分钟”“30分钟”“1小时”) QString durationStr; if(hours > 0 && minutes > 0) { durationStr = QString("%1小时%2分钟").arg(hours).arg(minutes); } else if (hours > 0) { durationStr = QString("%1小时").arg(hours); } else if (minutes > 0) { durationStr = QString("%1分钟").arg(minutes); } else { durationStr = "1分钟以内"; // 不足1分钟按“1分钟以内”处理 } return durationStr;}
2.2 运行结果


