ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Java与OpenCV的车牌识别停车场收费系统实战

基于Java与OpenCV的车牌识别停车场收费系统实战 简介这份资源是基于Java与OpenCV实现车牌识别的停车场收费管理系统完整源码面向计算机相关专业正在做毕业设计的学生以及需要项目实战练习的学习者也可直接用作课程设计或期末大作业。项目围绕车牌识别与停车收费两大核心业务展开涵盖车辆进出管理、费用结算、用户与权限控制等模块适合作为Java Web与图像处理方向的中等难度综合实践。压缩包共164个文件约25.04MB包含24个java源文件、24个class编译文件、47个jar依赖库、20个xml配置、15个jsp页面以及properties配置、png与jpg图片素材、css与js静态资源等源码、依赖与前端页面齐全下载后可直接导入运行。目前已有198人学习关注。项目经过严格调试可直接作为毕设使用读者能借此掌握OpenCV车牌识别调用、Java后端接口设计与停车场收费逻辑的完整实现思路并在此基础上进行二次开发与功能扩展。1. 停车场收费系统里车牌识别到底卡在哪一步很多做过停车场项目的同行都有个共识收费逻辑本身不难难的是让系统稳定地认出那块铁皮上的几个字符。基于 Java 和 OpenCV 实现车牌识别的停车场收费管理系统本质上要解决三件事——从摄像头拿到画面、从画面里切出车牌、把车牌字符转成文本最后再把文本交给计费模块算出该收多少钱。这套链路里OpenCV 负责图像处理Java 负责业务编排和数据库落库两者通过 JavaCV 或 JNI 桥接。适合谁看如果你正在做 Java 课程设计、毕业设计或者公司接了个中小型停车场的私有化项目预算有限不想上商用一体机那这套方案值得投入时间。它不追求 99% 的识别率但能在光照可控、角度固定的场景下做到 90% 以上配合人工复核足够跑起来。下面按「先跑通最小链路再逐个环节调优」的顺序拆开讲。2. 环境搭建与最小可运行链路从装 OpenCV 到读出第一帧2.1 Java 侧依赖怎么选JavaCV 还是原生 JNIJava 调 OpenCV 有两条路。第一条是 JavaCV它把 OpenCV、FFmpeg 等库打包成 Maven 依赖加几行坐标就能用跨平台省心缺点是包体积大、版本更新滞后。第二条是原生 JNI自己编译 opencv_java 动态库性能好、版本可控但 Windows 和 Linux 要分别编译新手容易在 cmake 编译步骤上翻车。我一般推荐毕业设计和小型项目直接用 JavaCV先把业务跑通等识别率成为瓶颈再考虑换 JNI。Maven 坐标如下dependency groupIdorg.bytedeco/groupId artifactIdjavacv-platform/artifactId version1.5.9/version /dependency这个依赖会自动拉取对应平台的 OpenCV 和 FFmpeg 本地库不需要你单独安装 OpenCV。参数说明javacv-platform是聚合包包含所有平台的 native 库打包出来会比较大200MB 以上如果确定只跑 Linux 服务器可以换成javacv加opencv-platform的 Linux 分类器体积能压到几十兆。2.2 用 JavaCV 打开摄像头并抓取一帧最小验证目标是能打开视频源、抓一帧、存成图片。这一步跑通后面所有图像处理才有输入。import org.bytedeco.javacv.*; import org.bytedeco.opencv.opencv_core.Mat; import org.bytedeco.opencv.global.opencv_imgcodecs; public class CaptureTest { public static void main(String[] args) throws Exception { // 0 表示默认摄像头也可以传 rtsp 地址或视频文件路径 OpenCVFrameGrabber grabber new OpenCVFrameGrabber(0); grabber.setImageWidth(1280); grabber.setImageHeight(720); grabber.start(); // 抓 10 帧丢弃等摄像头曝光稳定避免第一帧偏暗 Frame frame null; for (int i 0; i 10; i) { frame grabber.grab(); } // Frame 转 MatOpenCV 的所有处理都基于 Mat OpenCVFrameConverter.ToMat converter new OpenCVFrameConverter.ToMat(); Mat mat converter.convert(frame); opencv_imgcodecs.imwrite(snapshot.jpg, mat); grabber.stop(); System.out.println(抓帧完成尺寸 mat.cols() x mat.rows()); } }逻辑说明OpenCVFrameGrabber封装了 VideoCapture传 0 走本地摄像头传 RTSP 地址走网络摄像头。抓 10 帧再取图是关键细节摄像头刚启动时自动曝光还没收敛第一帧往往过暗或过曝直接拿去识别会得到一堆噪声。参数说明setImageWidth/Height要和摄像头实际支持的分辨率匹配设成不支持的值有些驱动会静默回退到默认分辨率导致后续 ROI 坐标全部错位。2.3 图像预处理灰度、高斯模糊、Sobel 边缘车牌识别的第一步不是找车牌而是把图像变成「车牌区域突出、背景干扰弱化」的形态。标准流程是灰度化 → 高斯模糊去噪 → Sobel 提取垂直边缘。import org.bytedeco.opencv.opencv_core.Mat; import static org.bytedeco.opencv.global.opencv_imgproc.*; public class Preprocess { public static Mat preprocess(Mat src) { Mat gray new Mat(); cvtColor(src, gray, COLOR_BGR2GRAY); // 高斯核 5x5去噪同时保留边缘 Mat blurred new Mat(); GaussianBlur(gray, blurred, new Size(5, 5), 0); // Sobel 只取 x 方向车牌字符的垂直边缘最密集 Mat gradX new Mat(); Sobel(blurred, gradX, CV_8U, 1, 0, 3); // 二值化把边缘变成黑白 Mat binary new Mat(); threshold(gradX, binary, 0, 255, THRESH_BINARY THRESH_OTSU); return binary; } }逻辑说明车牌区域的特征是「水平方向上字符边缘密集」所以 Sobel 取 x 方向导数比取 y 方向有效得多。高斯核用 5x5 是经验值太小去噪不干净太大把字符边缘也糊掉了。二值化用 OTSU 自动阈值避免固定阈值在不同光照下失效。参数说明Sobel的 ksize 用 3 即可用 5 或 7 会让边缘变粗后续形态学闭运算容易把相邻字符粘成一团。3. 车牌定位与字符分割从整图到七个字符的完整链路3.1 形态学闭运算把车牌连成一个矩形块Sobel 之后图像里全是零散边缘需要形态学操作把车牌区域的边缘连成实心块。这里用一个大核的闭运算先膨胀再腐蚀。import org.bytedeco.opencv.opencv_core.Mat; import org.bytedeco.opencv.opencv_core.Size; import static org.bytedeco.opencv.global.opencv_imgproc.*; public static Mat locatePlate(Mat binary) { // 17x5 的核水平方向长因为车牌是扁长形 Mat kernel getStructuringElement(MORPH_RECT, new Size(17, 5)); Mat closed new Mat(); morphologyEx(binary, closed, MORPH_CLOSE, kernel); // 再腐蚀一次去掉细小的非车牌区域 Mat eroded new Mat(); erode(closed, eroded, getStructuringElement(MORPH_RECT, new Size(3, 3))); return eroded; }逻辑说明核的宽高比要接近车牌宽高比国内蓝牌约 440:140接近 3:117:5 约等于 3.4:1比较合适。闭运算把字符之间的缝隙填上让整个车牌变成一个白色矩形块。参数说明如果摄像头角度偏斜严重核要适当加大如果车牌在画面里很小核要减小否则会把车牌和背景粘在一起。3.2 轮廓筛选用宽高比和面积两个条件卡出候选车牌形态学之后用 findContours 找轮廓然后按宽高比和面积过滤。这一步是定位准确率的关键。import org.bytedeco.opencv.opencv_core.*; import org.bytedeco.javacpp.indexer.IntIndexer; import static org.bytedeco.opencv.global.opencv_imgproc.*; public static Rect findPlateRect(Mat binary, Mat src) { MatVector contours new MatVector(); Mat hierarchy new Mat(); findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); Rect best null; double bestScore 0; for (int i 0; i contours.size(); i) { Rect r boundingRect(contours.get(i)); double ratio (double) r.width() / r.height(); double area (double) r.width() * r.height(); // 宽高比 2.5~5.0面积占全图 0.5%~10% if (ratio 2.5 ratio 5.0 area src.cols() * src.rows() * 0.005 area src.cols() * src.rows() * 0.1) { double score area * (1 - Math.abs(ratio - 3.14) / 3.14); if (score bestScore) { bestScore score; best r; } } } return best; }逻辑说明宽高比 2.5~5.0 覆盖了蓝牌、黄牌、新能源绿牌的常见比例。面积下限 0.5% 防止把远处的小色块误判上限 10% 防止把整个车身当成车牌。评分公式用面积乘以比例接近度优先选又大又接近标准比例的候选。参数说明如果画面里有多辆车这个逻辑只会返回一个车牌需要改成返回候选列表再逐个识别。3.3 字符分割垂直投影法切出七个字符拿到车牌矩形后裁剪出来做二值化然后用垂直投影找字符之间的空白列。public static ListMat splitChars(Mat plateGray) { Mat binary new Mat(); threshold(plateGray, binary, 0, 255, THRESH_BINARY THRESH_OTSU); // 垂直投影统计每一列白色像素个数 int[] colSum new int[binary.cols()]; for (int x 0; x binary.cols(); x) { int sum 0; for (int y 0; y binary.rows(); y) { if (binary.ptr(y, x).get() ! 0) sum; } colSum[x] sum; } // 找连续的非零区间作为字符 ListMat chars new ArrayList(); int start -1; for (int x 0; x colSum.length; x) { if (colSum[x] 2 start -1) { start x; } else if (colSum[x] 2 start ! -1) { if (x - start 5) { // 宽度小于 5 像素的当噪声丢掉 chars.add(new Mat(binary, new Rect(start, 0, x - start, binary.rows()))); } start -1; } } return chars; }逻辑说明垂直投影的核心假设是「字符列有白色像素字符间隙列没有」。阈值 2 是容忍少量噪声如果二值化后噪点多可以调到 3 或 4。宽度过滤 5 像素是为了去掉车牌上的螺丝孔和边框残留。参数说明中文车牌第一个字符是汉字宽度通常比后面六个字符宽分割后要单独处理不能统一缩放到同一尺寸。4. 字符识别与计费联动把识别结果变成一笔停车费4.1 模板匹配做字符识别够用但要知道边界毕业设计级别的方案字符识别用模板匹配最省事。准备 0-9、A-Z、各省简称的模板图把分割出的字符逐个和模板算相似度。public static String matchChar(Mat charImg, MapString, Mat templates) { Mat resized new Mat(); resize(charImg, resized, new Size(20, 40)); String bestChar ?; double bestScore Double.MAX_VALUE; for (Map.EntryString, Mat entry : templates.entrySet()) { Mat tpl new Mat(); resize(entry.getValue(), tpl, new Size(20, 40)); Mat result new Mat(); matchTemplate(resized, tpl, result, TM_CCOEFF_NORMED); double score result.ptr(0, 0).get(); if (score bestScore) { bestScore score; bestChar entry.getKey(); } } return bestChar; }逻辑说明TM_CCOEFF_NORMED返回归一化相关系数越接近 0 越相似这里取最小值。模板图要提前统一尺寸和字体最好用和实际场景同源的字体渲染。参数说明模板数量决定了识别速度37 个字符模板每帧要算 37 次匹配如果每秒处理 25 帧会吃力实际项目里通常只在检测到车牌后才做识别不是每帧都跑。模板匹配的边界很明确字体变化、模糊、倾斜都会让准确率断崖下跌。如果项目要求高建议换成轻量 CNN比如用 PaddleOCR 的 Java 接口或者自己训练一个 7 分类的小网络。4.2 识别结果落库与计费逻辑识别出车牌号后Java 侧要做的是查库、算时长、算费用。表结构至少要有三张车辆入场记录、车辆出场记录、费率规则。CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(10) NOT NULL, entry_time DATETIME NOT NULL, exit_time DATETIME, fee DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0 在场 1 已出场 INDEX idx_plate (plate_no), INDEX idx_status (status) );入场时插入一条 status0 的记录出场时按 plate_no 查最近一条 status0 的记录算出停车时长再按费率表算钱。参数说明plate_no 要加索引高峰期每秒可能有几十次查询status 加索引是为了快速筛出在场车辆避免全表扫描。计费逻辑里有个容易忽略的点识别错误的车牌会生成一条永远匹配不上的入场记录出场时查不到就按「临时车」全额计费车主会投诉。常见做法是加一个「置信度低于阈值时转人工复核」的分支把可疑记录推到待处理队列。4.3 用定时任务清理异常记录系统跑久了会积累大量 status0 但实际车已经走了的记录这些脏数据会拖慢查询。加一个每天凌晨跑的清理任务。Scheduled(cron 0 0 3 * * ?) public void cleanStaleRecords() { // 超过 7 天还在场的记录标记为异常 LocalDateTime deadline LocalDateTime.now().minusDays(7); int count recordMapper.markAbnormal(deadline); log.info(清理异常在场记录 {} 条, count); }逻辑说明正常停车不会超过 7 天超过的基本是识别错误或系统故障导致的脏数据。标记为异常而不是直接删除保留追溯能力。参数说明cron 表达式0 0 3 * * ?表示每天凌晨 3 点执行避开白天高峰期。5. 避坑与排查车牌识别项目里最容易翻车的五个地方5.1 识别率白天正常晚上暴跌现象白天识别率 90% 以上一到晚上掉到 50% 以下。原因夜间车灯和路灯造成局部过曝OTSU 全局阈值失效二值化后车牌区域和背景混在一起。解决改用自适应阈值adaptiveThreshold或者加一个基于图像平均亮度的分支暗光时先做直方图均衡化再二值化。5.2 摄像头分辨率设了不生效现象代码里设了 1920x1080实际抓到的图还是 640x480。原因部分 USB 摄像头驱动只支持特定分辨率组合设了不支持的值会静默回退。解决启动后打印实际分辨率和预期不符就换一个摄像头支持的值或者用grabber.getImageWidth()读实际值再动态调整 ROI 坐标。5.3 中文省份简称识别不出来现象后面六个字符都对第一个汉字总是错。原因模板匹配对汉字这种笔画复杂的字符效果很差而且很多模板库根本没有省份简称。解决单独训练一个省份简称的分类器或者用 OCR 库专门处理第一个字符。如果项目允许也可以只识别后六位入场时人工确认省份。5.4 多辆车同时出现在画面里只识别到一辆现象画面里两辆车系统只处理了其中一辆。原因轮廓筛选逻辑只返回评分最高的一个候选。解决把findPlateRect改成返回候选列表对每个候选都做字符分割和识别然后用「识别置信度 车牌格式校验」来筛选。格式校验可以用正则比如蓝牌^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-Z][A-Z0-9]{5}$。5.5 内存持续增长最后 OOM现象跑几个小时之后 Java 进程内存爆掉。原因JavaCV 的 Mat 对象不受 JVM 垃圾回收管理每次循环 new 出来的 Mat 如果不手动 releasenative 内存会一直涨。解决所有 Mat 用完调mat.release()或者用 try-finally 包起来。更稳妥的做法是用Mat的AutoCloseable特性配合 try-with-resources。6. 把识别率从 90% 推到 95% 的三个调参技巧第一个技巧是动态 ROI。固定 ROI 在摄像头有轻微震动或车辆停偏时会失效改成每次先做一次粗定位用定位到的车牌位置反推 ROI比写死坐标稳得多。具体做法是把第 3 章的定位逻辑跑一遍拿到 Rect 之后再裁剪而不是一开始就裁固定区域。第二个技巧是识别结果投票。同一辆车入场时连续抓 5 帧每帧独立识别取出现次数最多的结果作为最终车牌号。这个逻辑对模板匹配特别有效因为单帧的偶然错误在投票后会被过滤掉。代价是入场处理时间从 40ms 增加到 200ms 左右对停车场场景完全可以接受。第三个技巧是建立误识别黑名单。把人工复核时确认的错误识别结果记录下来如果某个「车牌号」反复出现且从没匹配到真实记录就加入黑名单后续识别到直接转人工。这个机制跑一周左右就能把大部分系统性误识别挡掉。调参项默认值推荐值影响Sobel ksize33加大边缘变粗字符粘连形态学核17x5按分辨率缩放核太大车牌和背景粘连投影阈值22~4噪点多时调大投票帧数15帧数越多越准但越慢我自己的习惯是每换一个摄像头就重新跑一遍参数标定把上面这几个值记在配置文件里而不是硬编码在代码里。车牌识别这个方向算法本身的门槛在降低真正拉开差距的是对具体场景的调参耐心。希望帮到你。本文还有配套的精品资源点击获取
返回列表