简介基于OpenCV的车牌识别停车场管理系统是C语言完成的毕业设计项目适合计算机、通信、人工智能、自动化等专业的学生作为期末课程设计、课程大作业或毕业设计参考。项目整体采用C结合OpenCV实现车牌定位、字符分割与识别并搭建停车场管理界面代码经过反复调试测试答辩评审分达到98分既能帮助初学者理解图像处理与模式识别的基本流程也具备较高的扩展与二次开发价值。资源共493个文件压缩包104.81MB主要包含jpg图像样本、cpp/h/hpp源码、xml模型与配置文件、ui界面文件、sql数据库脚本及论文文档等其中图像样本用于模型训练与效果测试xml保存SVM/ANN训练结果sql负责停车数据管理整体目录按功能模块划分便于对照源码快速上手。已有201人学习下载适合需要完整项目参考、代码复现或在此基础上进行功能改进的读者附带的论文也有助于把握设计思路与答辩要点。1. 从毕业设计到能跑的车牌识别停车场管理系统我当年做C毕业设计时也选过车牌识别这个方向。选它不是因为论文好写而是停车场管理这个场景足够具体车辆进入时摄像机抓拍系统识别车牌再决定抬不抬杆、记不记录时间、算不算费用。这套流程用OpenCV在C里实现比Java、Python方案在预处理和实时性上更可控而且网上能找到的源码包往往附带论文环境配好就能跑起来。对要做C相关课设或毕业设计的人说一条由“OpenCV图像处理 字符识别 SQLite数据管理”组成的完整链路既能体现编程能力又能把工程化思路写进论文。下面按我拆这个项目的顺序把车牌识别的每个环节和停车场管理系统的落地方式讲清楚。2. 车牌识别核心流水线从图像预处理到字符分割这里先立住一个观点车牌识别不是单个算法而是一条流水线。光照不均匀、车牌倾斜、字符粘连都会导致后续识别失效。所以先把每一步的输入输出定义清楚再谈优化。2.1 车牌定位的三种思路与选型定位车牌是整个系统成败的第一步。常见做法有三种我实际对比过定位方法原理优点缺点边缘检测 形态学用Sobel或Canny求边缘然后闭运算连接相邻区域再按面积和宽高比筛出车牌区域速度快适合低配置环境容易受车标、车灯干扰颜色特征定位在HSV空间筛出蓝色或黄色像素再用轮廓提取车牌候选区对国内蓝底白字、黄底黑字车牌针对性强受光照影响明显夜间需要补偿深度学习检测用YOLO等模型直接预测车牌框鲁棒性好能应对倾斜、模糊需要标注数据和训练环境不适合“纯C源码”的毕业设计毕业设计场景里我推荐先做“颜色 形态学”的组合定位。原因是OpenCV的API在C里直接可用逻辑也方便写进论文的算法章节。用HSV颜色过滤能得到一个二值图再用morphologyEx做闭运算把断裂的车牌区域连起来之后findContours筛出宽高比在2.5到5.5之间的区域就基本锁定了车牌。2.2 基于颜色与形态学的定位实现下面这段C代码展示了最基础的车牌定位流程。它针对蓝色车牌做处理输入是摄像头帧或图片路径输出是车牌区域裁剪后的Mat。#include opencv2/opencv.hpp #include vector using namespace cv; using namespace std; Mat locatePlate(const Mat src) { Mat dst src.clone(); // 缩小处理减少计算量 Mat scaled; resize(dst, scaled, Size(dst.cols * 0.5, dst.rows * 0.5)); // 转HSV方便提取蓝色区域 Mat hsv; cvtColor(scaled, hsv, COLOR_BGR2HSV); // 蓝色范围H在100~124S大于43V大于46 Mat mask; inRange(hsv, Scalar(100, 43, 46), Scalar(124, 255, 255), mask); // 闭运算连接相邻区域输出二值图 Mat kernel getStructuringElement(MORPH_RECT, Size(15, 5)); morphologyEx(mask, mask, MORPH_CLOSE, kernel); // 找轮廓并筛选 vectorvectorPoint contours; findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); for (const auto contour : contours) { RotatedRect rect minAreaRect(contour); float ratio max(rect.size.width, rect.size.height) / (float)min(rect.size.width, rect.size.height); float area rect.size.area(); if (area 2000 ratio 2.0 ratio 6.0) { // 按角度矫正后返回车牌区域 Mat rotated; rotationMatrixToAffineTransform(rect); return src(Rect(rect.boundingRect())); } } return Mat(); }这段代码里有两个关键参数需要说明。inRange的蓝色阈值范围来自HSV色彩空间(100, 43, 46)是低阈值(124, 255, 255)是高阈值。如果系统运行在夜间或黄光场景可以手动调节H通道下限比如折半到95或者先把图像转到YCrCb空间做亮度均衡。getStructuringElement的核大小(15, 5)决定相邻区域能被连到多远核越大越容易合并干扰区域比较合适的范围是宽10~20、高4~6。这里rotationMatrixToAffineTransform是示意实际应该用warpAffine配合getRotationMatrix2D来做旋转矫正后面第4章细说。2.3 字符分割投影法到底怎么切定位到车牌后下一步是分割字符。国内车牌是7个字符其中汉字占一个位置后面是字母和数字。投影法是最容易理解的一类方法把二值化后的车牌图像按列累加像素数得到一个垂直投影直方图波谷就是字符间隙波峰就是字符区域。vectorMat splitChars(const Mat plateBin) { // plateBin 是经过灰度化、二值化后的车牌图 Mat bin plateBin.clone(); // 计算垂直投影每一列白点数 vectorint colSum(bin.cols, 0); for (int j 0; j bin.cols; j) { for (int i 0; i bin.rows; i) { if (bin.atuchar(i, j) 255) { colSum[j]; } } } // 根据阈值找到连续的白点区间 vectorRect charRects; bool inChar false; int start 0; for (int j 0; j bin.cols; j) { if (colSum[j] 10) { if (!inChar) { inChar true; start j; } } else { if (inChar) { inChar false; if (j - start 10) { charRects.push_back(Rect(start, 0, j - start, bin.rows)); } } } } // 按字符宽度过滤太窄或太宽的区域 vectorMat chars; for (Rect r : charRects) { // 汉字宽度一般接近高度字母数字宽度约为高度的一半 if (r.width bin.rows * 0.3 r.width bin.rows) { chars.push_back(bin(r)); } } return chars; }这里判断连续区间的阈值10是经验值。如果车牌二值化后有噪声白点数可能会在字符间隙也超过10导致两个字符被连成一个区域。解决办法是先做一次erode操作把细小的噪点消掉。字符宽度的过滤条件bin.rows * 0.3是因为一般车牌预处理后尺寸统一为宽高比3.14字符间隔相对固定。如果分割出来的字符个数不是7最常见原因是第一个汉字被切碎或者字母“A”中间空白过大被切成两段。这时可以改成“先按连通域找候选块再按宽度归并”的做法。3. 停车场管理系统的工程实现模块划分与数据流车牌识别只是视觉部分停车场管理系统更看重“识别成功后怎么处理”。我建议把系统拆成四个部分相机采集模块、车牌识别模块、数据库模块、界面与道闸控制模块。这样论文架构清楚源码也更可读。3.1 系统模块划分从摄像头到数据库表采集模块负责读取摄像头帧或测试视频识别模块输出车牌字符串和置信度数据库模块保存进出记录、计算费用界面模块负责展示剩余车位和车辆信息。模块之间用接口串起来比如识别模块只返回一个PlateInfo结构体不关心数据库怎么存。我见过很多毕业设计源码把这些逻辑全部写在main.cpp里这样虽然能跑但论文里不好画架构图导师也容易追问“如果摄像头掉线怎么办”。合理的做法是先定义好数据结构和接口哪怕实现很简单代码层次也是清晰的。3.2 车牌识别模块的接口封装识别接口我一般写成这样struct PlateInfo { std::string plate; // 识别的车牌字符串例如粤B12345 double confidence; // 置信度0.0~1.0 cv::Mat plateImage; // 用于调试的车牌裁剪图 }; class PlateRecognizer { public: PlateRecognizer(const std::string opencvModelPath) { // 初始化颜色定位参数、字符识别模板库 } PlateInfo Recognize(const cv::Mat frame) { PlateInfo info; // 1. 定位车牌 cv::Mat plate locatePlate(frame); if (plate.empty()) { info.confidence 0.0; return info; } // 2. 预处理 分割字符 // 3. 对每个字符做归一化再与模板匹配或交给SVM分类 // 4. 综合结果得到plate字符串和confidence return info; } };这里的PlateRecognizer在入口和出口各实例化一份但共享同一套参数配置。参数我会放在配置文件或常量里比如定位时形态学核大小、字符匹配模板路径。confidence的计算方式可以定成“匹配到的最优字符相似度均值”这样后续判定要不要放行就有了依据。3.3 计时计费与剩余车位更新停车场系统核心表至少有三张vehicle_record保存进出记录parking_space保存车位总数和已占数量还有一张payment_record记录收费流水。下面这段SQLite操作用C实现进出场逻辑#include sqlite3.h void OnVehicleEnter(sqlite3* db, const std::string plate) { char sql[512]; // 插入入场记录注意参数化避免注入 snprintf(sql, sizeof(sql), INSERT INTO vehicle_record(plate, enter_time, status) VALUES(%s, datetime(now, localtime), 1);, plate.c_str()); sqlite3_exec(db, sql, nullptr, nullptr, nullptr); // 剩余车位减一 sqlite3_exec(db, UPDATE parking_space SET occupied occupied 1 WHERE space_id 1;, nullptr, nullptr, nullptr); } void OnVehicleExit(sqlite3* db, const std::string plate) { // 查入场时间 sqlite3_stmt* stmt; sqlite3_prepare_v2(db, SELECT enter_time FROM vehicle_record WHERE plate ? AND status 1 ORDER BY id DESC LIMIT 1;, -1, stmt, nullptr); // 时间差计算费用这里省略具体时长解析 }这段代码里有两个细节值得注意。第一插入车牌字符串时没有做字符过滤如果识别模块返回了空串或带特殊符号的结果就会污染数据库。所以入口处要么拒绝confidence低于阈值的结果要么先判断plate是否符合字母数字的正则规则。第二UPDATE parking_space如果执行成功应先用sqlite3_changes确认实际影响的记录行数防止因为表不存在或者事务未提交导致刷不出来数据。剩余车位数的更新和车辆记录插入应该放在同一个事务里否则系统崩溃后悔出现“有车辆记录但车位没扣”的情况。3.4 给论文画结构图之前先把数据一致性做对很多课设源码里剩余车位只是界面上的一个整数变量程序重启就清零。稍微往前走一步就应该把车位数据持久化到SQLite。这里我用一个事务保证两条写操作同时成功sqlite3_exec(db, BEGIN;, nullptr, nullptr, nullptr); sqlite3_exec(db, sqlInsertRecord, nullptr, nullptr, nullptr); sqlite3_exec(db, sqlUpdateSpace, nullptr, nullptr, nullptr); sqlite3_exec(db, COMMIT;, nullptr, nullptr, nullptr);BEGIN和COMMIT之间的语句要么全部成功要么全部回滚。这个点写进论文的“数据库设计”章节比单谈技术更有说服力。同时道闸控制模块只需要监听vehicle_record表的新增记录如果识别成功且车位未满就给继电器一个高电平信号否则保持关闭。这样视觉和硬件控制的耦合度就降低了。4. 车牌识别算法在实战中的调参与坑位凡是拿真实停车场图片跑过车牌识别的人都会遇到同一个问题实验室里测试用干净图片效果好一到实际场景就识别率暴跌。原因多数不是算法不对而是参数和预处理策略没有根据现场调整。4.1 光照与过曝自适应阈值代替固定阈值固定阈值threshold(src, dst, 128, 255, THRESH_BINARY)在夏日强光下会把数字和背景混在一起。更稳的做法是用自适应阈值或先计算图像的局部亮度做归一化Mat gray, denoised, bin; cvtColor(plateImg, gray, COLOR_BGR2GRAY); // 双边滤波保留边缘同时去掉噪声 bilateralFilter(gray, denoised, 9, 75, 75); // OTSU自动阈值 threshold(denoised, bin, 0, 255, THRESH_BINARY | THRESH_OTSU);bilateralFilter的直径设为9颜色空间标准差和坐标空间标准差都设为75是平衡去噪和细节的常用起点。OTSU的优点是不需要手动挑阈值但它假设图像直方图是双峰的。当车牌反光严重时直方图可能变成单峰这时候就要回到自适应阈值adaptiveThreshold(denoised, bin, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 25, 10);其中25是邻域尺寸必须是奇数。它决定每个像素的阈值参考范围太小会留下噪声太大会丢失笔画。10是减去的常数用来压低背景。我一般从25开始调邻域太大或太小通过观察二值化后的字符是否完整来定。4.2 倾斜矫正minAreaRect和仿射变换的配合车牌在拍摄中几乎不可能完全水平。minAreaRect可以拿到最小外接矩形的角度但要注意角度范围是[-90, 0)需要把它换算成补角。RotatedRect rrect minAreaRect(contour); float angle rrect.angle; if (rrect.size.width rrect.size.height) { angle 90 angle; // 转成正角度 } Mat rotMat getRotationMatrix2D(rrect.center, angle, 1.0); Mat corrected; warpAffine(plateImg, corrected, rotMat, plateImg.size(), INTER_CUBIC, BORDER_REPLICATE);这里的getRotationMatrix2D三个参数分别是旋转中心、角度和缩放比例。角度取反是因为minAreaRect返回的是坐标轴到长边的旋转量实际要旋转的角度可能是它的负值或补角取决于长边是宽还是高。warpAffine的插值方式用INTER_CUBIC边缘填充选BORDER_REPLICATE避免旋转后边缘出现黑边影响字符分割。如果车牌倾斜超过15度建议先做投射矫正而不是简单旋转否则字符会被拉伸变形。4.3 识别率上不去时先查这些我把排错思路整理成下面这张表格可以作为调试手册现象可能原因排查方向定位框完全找不到图像分辨率过高或对比度过低先resize到800宽再测试检查HSV阈值范围找到多个候选框车灯、车标被误判增加宽高比限制对候选框做纹理密度筛选字符分割后数量不是7二值化失败或字符粘连尝试erode检查二值化后的平均黑像素密度汉字识别总是错汉字模板太少或字体差异收集不同车牌字体扩增字符模板库程序运行一帧后崩溃内存越界或Mat未判空在调用plate.atuchar前检查plate.empty()调试时我习惯把每一步的中间结果都用imwrite保存下来。比如定位后的区域、二值化后的图片、分割后的字符。保存中间图的好处是看到底是哪一步把信息丢了而不是靠猜。另外OpenCV版本差异也会导致行为不同我的建议是固定一个C版本的OpenCV环境比如4.x版本把安装步骤写进环境说明文档这样源码在对方电脑上更容易跑通。5. 让系统更稳识别置信度与并发出入场处理调完算法还剩两个细节决定系统能不能真正在停车场门口用住一是分类结果不可靠时怎么办二是入口和出口同时触发时数据会不会乱。5.1 用置信度过滤误识别字符识别模块无论是用KNN、SVM还是模板匹配都会返回每个字符的决策距离或概率。统一换算成置信度后设一个0.6的阈值if (info.confidence 0.6 || info.plate.length() ! 7) { // 丢给人工复核或重新抓拍 TriggerReCapture(); return; }车牌字符串长度校验很关键。蓝牌是7位含汉字新能源绿牌是8位在入口就要区分车型并选择对应的校验规则。我见过把雨滴反光识别成字符导致长度为8的蓝牌结果如果不用长度约束这条错误记录就会直接进数据库。5.2 道闸排队与重复识别处理实际停车场中前车还没走后车车牌已经出现在镜头里这时系统可能在一秒内返回两次识别结果。处理方式是给入口状态机加“正在处理”锁bool processing false; if (processing) return; processing true; PlateInfo info recognizer.Recognize(frame); if (info.confidence 0.6) { OnVehicleEnter(db, info.plate); } processing false;这个简单布尔量只能挡住单线程时序问题。再多想一步如果识别成功了但数据库插入失败道闸是否要抬杆我倾向是抬杆但不记录车牌同时抓拍图片保存到本地留作后台补录。这样可以避免车道堵死也不会丢失现场图像证据。最后一个实用技巧是摄像头画面的帧间隔。连续帧做识别会浪费CPU常见做法是每3帧取一帧或者根据运动检测结果决定是否要进入识别流程。这样在嵌入式板卡上也能维持15FPS左右的处理速度同时保证入口道闸拿到的是一次可用的识别结果。本文还有配套的精品资源点击获取
