一、引言

1.1 应用背景

车牌识别(LPR)是智能交通系统的核心技术,广泛应用于停车场自动收费、违章抓拍、高速公路ETC、城市道路监控等场景。随着智慧城市建设的推进,车牌识别技术的需求日益增长,掌握其原理对从事计算机视觉和图像处理的同学非常有价值。本文将带你从零实现一个完整的车牌识别系统,涵盖车牌定位、字符切割、字符识别全流程。

1.2 技术路线

步骤 方法 作用
预处理 HSV颜色转换 + Canny边缘检测 突出车牌区域,抑制背景噪声
车牌定位 形态学处理 + Hough变换 精确定位并校正倾斜的车牌
字符切割 连通域分析 + 投影法 将车牌切割为单个字符
字符识别 HOG特征 + SVM分类器 识别每个字符的内容

在这里插入图片描述

1.3 开源地址

本文章完整代码下载地址,测试集包含黑夜,雨天,倾斜车牌,正常拍照四种类型,适用于简单的车牌识别。

基于MATLAB的车牌识别系统
├── Main.m                  # 主程序入口(启动 GUI)
├── Step1_ReadImage.m       # 步骤1:读取图片
├── Step2_Preprocess.m      # 步骤2:图像预处理(HSV+Canny双验证)
├── Step3_Locate.m          # 步骤3:车牌定位 + 倾斜校正
├── Step4_Segment.m         # 步骤4:字符分割(连通域定位法)
├── Step5_Recognize.m       # 步骤5:字符识别(HOG+SVM)
├── Step6_ResultGUI.m       # 结果展示GUI界面
├── classifierChinese.mat   # SVM 汉字分类器
├── classifierLetter.mat    # SVM 字母分类器
├── classifierAlphanum.mat  # SVM 字母数字分类器
├── ChePaiKu/               # 车牌图片库
├── temp_segments/          # 分割后的字符图片(运行时生成)
└── 字符模板(4020)/         # 字符模板库

二、图像预处理

2.1 为什么需要预处理?

原始图像包含大量干扰信息(如车身、树木、天空等),预处理的目标是突出车牌区域,抑制背景噪声,为后续定位做准备。

预处理流程图:
在这里插入图片描述

2.2 蓝色检测(HSV颜色空间)

采用HSV颜色空间进行蓝色车牌检测,相比RGB对光照变化更鲁棒。
HSV阈值设置:

通道 阈值范围 说明
H(色调) 0.50 ~ 0.80 蓝色色调范围
S(饱和度) > 0.15 过滤灰色区域
V(亮度) > 0.15 过滤过暗区域

核心代码:

imgHSV = rgb2hsv(imgRGB);
imgBlue = (H >= 0.50 & H <= 0.80) & (S > 0.15) & (V > 0.15);
imgBlue = imfill(imgBlue, 'holes');  % 填充孔洞

2.3 边缘检测(Canny算子)

使用Canny算子检测图像边缘,提取车牌的轮廓信息。
处理流程:

  1. RGB转灰度图
  2. Canny边缘检测(阈值0.5)
    核心代码:
imgGray = rgb2gray(imgRGB);
imgEdge = edge(imgGray, 'canny', 0.5);

2.4 形态学处理

对边缘检测结果进行形态学处理,完善车牌区域。
处理步骤:

  1. 闭运算:使用15×15矩形结构元素,连接断裂的边缘
  2. 填充孔洞:填充封闭区域内的孔洞
  3. 面积滤除:去除小于1000像素的噪声区域

核心代码:

imgClosed = imclose(imgEdge, strel('rectangle', [15, 15]));
imgFilled = imfill(imgClosed, 'holes');
imgFiltered = bwareaopen(imgFilled, 1000);

2.5 双重验证(颜色+边缘取交集)

将蓝色检测结果与形态学处理后的边缘结果取交集,提高定位准确性。
核心代码:

imgCombined = imgFiltered & imgBlue;
imgPreprocessed = bwareaopen(imgCombined, 1000);  % 再次滤除小区域

下图为正确实现上述预处理之后的结果展示:
在这里插入图片描述


三、车牌定位

在这里插入图片描述

3.1 粗定位:膨胀 + 连通域分析

通过膨胀将分散的字符连成一片,再通过连通域分析找出车牌区域。

核心代码:

imgDilated = imdilate(imgPreprocessed, strel('rectangle', [20, 20]));
cc = bwconncomp(imgDilated);
stats = regionprops(cc, 'Area', 'BoundingBox');
[~, maxIdx] = max([stats.Area]);

3.2 灰度化与二值化

对定位后的车牌图像进行灰度化和二值化处理。

核心代码:

imgCropGray = rgb2gray(imgCropRGB);
level = graythresh(imgCropGray);
imgCropBinary = im2bw(imgCropGray, level);

3.3 倾斜校正:Hough变换

倾斜校正的理论原理:

1. 边缘检测 使用Canny算子提取图像中的边缘点,这些边缘点包含了车牌的轮廓信息。
2. Hough变换映射 将图像空间中的每个边缘点映射到参数空间(ρ-θ空间)的一条曲线。图像空间中同一条直线上的点,在参数空间中会交于同一点。
3. 投票机制 在参数空间中建立累加器矩阵,对每条曲线经过的参数组合进行投票。投票数越多,说明对应的直线越显著。
4. 寻找峰值 在累加器中找到投票数最高的点,该点对应的(ρ, θ)参数就是原图像中最显著直线的参数。
5. 计算倾斜角度 筛选出接近水平的线段(|θ| < 45°),使用加权中位数法计算最终倾斜角度。长线段权重更大,更可靠。
6. 旋转校正 根据计算出的角度,使用双线性插值对图像进行旋转,使车牌水平对齐。

核心代码:

% --- Step 3.2: Hough变换检测倾斜角度 ---

% 1. 边缘检测:提取车牌轮廓
imgEdge = edge(imgCropGray, 'canny');

% 2. Hough变换:将图像空间映射到参数空间
[H, theta, rho] = hough(imgEdge);

% 3. 寻找峰值:在参数空间中找投票数最高的点
peakThreshold = ceil(0.15 * max(H(:)));
peaks = houghpeaks(H, 20, 'Threshold', peakThreshold);

% 4. 提取线段:根据峰值还原实际线段
[cropH, cropW] = size(imgCropGray);
minLen = max(10, round(cropW * 0.15));
lines = houghlines(imgEdge, theta, rho, peaks, ...
    'FillGap', round(cropW * 0.05), 'MinLength', minLen);

% 5. 计算倾斜角度:使用加权中位数法
skewAngle = 0;
if ~isempty(lines)
    % 只保留接近水平的线段(|theta| < 45°)
    horizontalLines = lines(abs([lines.theta]) < 45);
    if isempty(horizontalLines)
        horizontalLines = lines;
    end
    
    % 提取每条线段的角度和权重(线段长度)
    angles = zeros(1, length(horizontalLines));
    weights = zeros(1, length(horizontalLines));
    for k = 1:length(horizontalLines)
        angles(k) = horizontalLines(k).theta;
        weights(k) = norm(horizontalLines(k).point1 - horizontalLines(k).point2);
    end
    
    % 加权中位数:按角度排序,累加权重找中位数
    [~, sortIdx] = sort(angles);
    sortedAngles = angles(sortIdx);
    sortedWeights = weights(sortIdx);
    cumWeight = cumsum(sortedWeights);
    medianIdx = find(cumWeight >= cumWeight(end) / 2, 1);
    skewAngle = sortedAngles(medianIdx);
end

% 6. 角度过滤:超过30°认为是误检
if abs(skewAngle) > 30
    skewAngle = 0;
end

% --- Step 3.3: 旋转校正 ---
% 7. 执行旋转:使用双线性插值保证图像质量
if abs(skewAngle) > 0.5
    imgCropBinary = imrotate(imgCropBinary, skewAngle, 'bilinear', 'crop');
    imgCropGray = imrotate(imgCropGray, skewAngle, 'bilinear', 'crop');
end

3.4 去噪处理:形态学操作

使用形态学操作去除小的噪声区域。
核心代码:

imgCropBinary = bwareaopen(imgCropBinary, 50);

3.5 投影法去边框

通过水平和垂直投影去除车牌边框,阈值为最大投影值的5%。

核心代码:

% 水平投影:找左右边界
projHorizontal = sum(imgCropBinary, 1);
projThreshold = max(projHorizontal) * 0.05;
colLeft = find(projHorizontal >= projThreshold, 1, 'first');
colRight = find(projHorizontal >= projThreshold, 1, 'last');

% 垂直投影:找上下边界
projVertical = sum(imgCropBinary, 2);
projVThreshold = max(projVertical) * 0.05;
rowTop = find(projVertical >= projVThreshold, 1, 'first');
rowBottom = find(projVertical >= projVThreshold, 1, 'last');

% 裁剪
imgLocated = imgCropBinary(rowTop:rowBottom, colLeft:colRight);

下图为正确实现上述车牌定位之后的结果展示:
在这里插入图片描述


四、字符切割

在这里插入图片描述

4.1 预处理:去除干扰

对二值化后的车牌图像进行预处理,去除铆钉和边框干扰。

核心代码:

imgReady = bwareaopen(imgLocated, 20);  % 去除小于20像素的噪声
[segH, segW] = size(imgReady);

% 上下裁剪17%,去除铆钉
cropTop = round(segH * 0.17);
cropBottom = segH - round(segH * 0.17);
imgReady = imgReady(cropTop:cropBottom, :);

% 左右裁剪5%,去除边框
cropLeft = round(segW * 0.05);
cropRight = segW - round(segW * 0.05);
imgReady = imgReady(:, cropLeft:cropRight);

4.2 连通域分析

标记所有连通区域,按面积过滤保留有效字符区域。

核心代码:

cc = bwconncomp(imgReady);
stats = regionprops(cc, 'Area', 'BoundingBox', 'PixelIdxList');
areas = [stats.Area];

% 过滤:保留面积 >= 中位数15%的区域
medianArea = median(areas);
validIdx = find(areas >= medianArea * 0.15);
stats = stats(validIdx);
 
% 按x坐标排序(从左到右)
bboxes = cat(1, stats.BoundingBox);
[~, sortIdx] = sort(bboxes(:, 1));
stats = stats(sortIdx);

4.3 合并过近的连通域

字符笔画断裂会导致一个字符被分成多个区域,需要合并间距过小的连通域。

核心代码:

mergeGap = segW * 0.02;  % 合并阈值:车牌宽度的2%
merged = true;
while merged
    merged = false;
    n = length(stats);
    if n <= 7, break; end
    newStats = [];
    k = 1;
    
    while k <= n
        curBox = stats(k).BoundingBox;
        curRight = curBox(1) + curBox(3);
        curPixels = stats(k).PixelIdxList;
        
        while k < n
            nextBox = stats(k+1).BoundingBox;
            gap = nextBox(1) - curRight;
            
            if gap < mergeGap
                % 合并两个连通域
                newX = min(curBox(1), nextBox(1));
                newRight = max(curRight, nextBox(1) + nextBox(3));
                curBox = [newX, min(curBox(2), nextBox(2)), ...
                    newRight - newX, max(curBox(4), nextBox(4))];
                curRight = newRight;
                curPixels = union(curPixels, stats(k+1).PixelIdxList);
                k = k + 1;
                merged = true;
            else
                break;
            end
        end
        s.BoundingBox = curBox;
        s.Area = length(curPixels);
        s.PixelIdxList = curPixels;
        newStats = [newStats, s];
        k = k + 1;
    end
    stats = newStats;
end

4.4 选择最佳7个字符

从候选连通域中选择7个最佳字符(中国车牌固定7位)。

核心代码:

nCandidates = length(stats);

if nCandidates == 7
    charStats = stats;
elseif nCandidates > 7
    % 按面积降序,取前7个
    allAreas = arrayfun(@(s) s.Area, stats);
    [~, areaIdx] = sort(allAreas, 'descend');
    charStats = stats(areaIdx(1:7));
    
    % 重新按x坐标排序
    bboxes = cat(1, charStats.BoundingBox);
    [~, xIdx] = sort(bboxes(:, 1));
    charStats = charStats(xIdx);
else
    charStats = stats;  % 不足7个时保留所有
end

4.5 裁剪并保存

据边界框裁剪每个字符并保存到临时目录。

核心代码:

for i = 1:min(7, length(charStats))
    bbox = charStats(i).BoundingBox;
    x1 = max(1, round(bbox(1)));
    y1 = max(1, round(bbox(2)));
    x2 = min(segW, round(bbox(1) + bbox(3)));
    y2 = min(segH, round(bbox(2) + bbox(4)));
    imgChar = imgReady(y1:y2, x1:x2);
    imwrite(imgChar, fullfile('temp_segments', strcat(int2str(i), '.jpg')));
end

下图为正确实现上述车牌切割之后的结果展示:
在这里插入图片描述


五、字符识别

5.1 加载SVM分类器

系统使用三个SVM分类器分别识别不同类型的字符,使用SVM向量机对大量字符模板提取特征值并训练得到下述三个训练模型,每次识别直接调用训练好的模型无需重复训练。

[注]:训练所使用的字符模板位于目录的字符模板(4020)文件夹内。

  • classifierChinese.mat:省份汉字(第1位)
  • classifierLetter.mat:字母(第2位)
  • classifierAlphanum.mat:字母数字(第3-7位)

核心代码:

models.chinese = loadSVM('classifierChinese.mat');
models.letter = loadSVM('classifierLetter.mat');
models.alphanum = loadSVM('classifierAlphanum.mat');

5.2 HOG特征提取

HOG(方向梯度直方图)通过统计图像局部区域的梯度方向分布来描述形状特征。
参数设置:

参数 说明
图像尺寸 40×20 统一输入大小
Cell大小 4×4 基本单元
Bins数量 9 梯度方向 bin 数

核心代码:

imgR = imresize(img, [40 20]);
hogFeat = extractHOGFeatures(imgR, 'NumBins', 9, 'CellSize', [4 4]);

5.3 分类预测

根据字符位置选择对应的分类器进行预测。

核心代码:

for i = 1:7
    img = charImgs{i};
    if ~islogical(img), img = imbinarize(img); end
    
    switch i
        case 1
            label = svmPredict(img, models.chinese);    % 省份汉字
        case 2
            label = svmPredict(img, models.letter);     % 字母
        otherwise
            label = svmPredict(img, models.alphanum);   % 字母数字
    end
    
    plateResult = [plateResult, label];
end

完整的带gui设计界面的结果展示如下:
在这里插入图片描述


六、总结与优化方向

6.1 本系统技术总结

步骤 方法 核心作用
预处理 HSV颜色转换 + Canny边缘 突出车牌区域
车牌定位 形态学 + Hough变换 精确定位+倾斜校正
字符切割 连通域分析 + 投影法 分离单个字符
字符识别 HOG特征 + SVM分类 识别字符内容

6.2 教学价值 vs 商用方案

教学价值:
本系统采用传统图像处理方法,商用系统通常采用 YOLO + PLR 的两阶段方案:YOLO负责定位,PLR负责识别,两者配合可达到99%以上的准确率。虽然本系统在复杂场景下准确率有限,但具有重要的教学意义:

  1. 理解图像处理基础:HSV颜色空间、边缘检测、形态学操作、Hough变换等都是图像处理的核心知识
  2. 掌握完整流程:从预处理到识别,涵盖车牌识别的完整技术链路
  3. 培养工程思维:参数调优、边界情况处理、模块化设计等工程实践能力
  4. 为深度学习打基础:理解传统方法的局限性,才能更好地理解深度学习的优势
Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐