
1. 项目概述从一张图片到七个字符车牌识别是计算机视觉领域一个非常经典且实用的课题它连接着图像处理、模式识别和实际工程应用。我们平时在停车场、高速路口看到的自动识别系统其核心流程通常都包含“车牌定位”、“字符分割”和“字符识别”这三个关键步骤。今天要聊的就是其中承上启下的“字符分割”环节具体来说是如何用Matlab这个强大的工具实现对国内常见的蓝色和绿色车牌的字符切割。为什么是蓝色和绿色车牌因为这是目前国内道路上最主要的两种车牌类型。蓝牌是普通小型汽车绿牌是新能源车。它们的共同特点是底色单一蓝或绿、字符为白色这种高对比度为我们进行图像分割提供了天然的便利。但即便如此想把车牌上的7个字符新能源车牌可能是8位干净利落地切分开也并非易事。光照不均、车牌污损、拍摄角度倾斜、边框干扰等因素都会给分割带来挑战。这个项目的目标很明确给定一张已经定位并校正好的车牌区域图像写一套Matlab算法能稳定地把上面的每一个字符单独提取出来为后续的OCR识别准备好“原料”。整个过程就像把一串紧密排列的珠子字符从绳子上车牌背景一颗颗解下来。下面我就结合自己的实践把从思路设计到代码实现再到踩坑填坑的全过程拆解一遍。2. 核心思路与方案设计为何选择这条路径面对车牌字符分割问题有很多条路可以走。比如基于投影的方法、基于连通域分析的方法、基于模板匹配的方法甚至现在直接用深度学习做端到端的检测。对于蓝色和绿色车牌这种规整场景我选择了“图像预处理 - 二值化 - 形态学处理 - 垂直投影分析 - 字符区域定位与切割”这条经典路径。为什么这么选首先经典方法透明、可控。每一步操作的结果都肉眼可见出了什么问题很容易追溯到具体环节进行调整。这对于算法调试和教学理解都非常友好。其次对于蓝、绿车牌这种高对比度、字符排列整齐的场景经典方法的效率和效果已经足够好没必要上更复杂的模型。最后用Matlab实现这套流程有天然优势其强大的图像处理工具箱Image Processing Toolbox和直观的矩阵操作能让算法原型快速搭建起来。整个方案的核心思想是利用字符与背景的灰度差异将字符区域变成白色前景值为1背景变成黑色值为0然后分析白色像素在水平方向上的分布情况。字符所在的位置白色像素会密集分布形成波峰字符之间的间隙白色像素很少形成波谷。找到这些波谷也就找到了切割点。2.1 针对蓝绿车牌的预处理考量虽然都是单底色车牌但蓝色和绿色的光谱特性不同直接转灰度图的效果有差异。蓝色在灰度图中通常较暗绿色则相对亮一些。如果使用默认的rgb2gray加权平均法可能无法最大化前景与背景的对比度。因此预处理的第一步往往是尝试从RGB通道中分离出对比度最高的那个分量。对于蓝牌蓝色通道B的背景值很高字符白色在三个通道的值都很高所以在B通道上字符与背景的对比度反而最差而在红色通道R上蓝色背景很暗白色字符很亮对比度最好。绿牌也是类似道理在红色和蓝色通道上绿色背景都较暗。因此一个常见的技巧是优先使用红色通道图像作为后续处理的输入这通常能获得比直接灰度化更好的初始效果。2.2 二值化策略的选择得到灰度图像后需要将其转化为黑白二值图。二值化的关键在于阈值的选取。全局阈值如imbinarize使用Otsu方法简单快捷但对于光照不均或底色略有渐变的车牌效果可能不稳定。自适应阈值如imbinarize的‘adaptive’选项能更好地处理局部光照变化但可能会引入更多噪声。我的经验是对于拍摄质量较好的车牌图片全局Otsu阈值法完全够用且速度快。如果图像质量较差可以尝试自适应阈值但需要仔细调整邻域大小和常数参数。注意二值化后的目标是什么是让字符区域为白色1背景为黑色0。但有时由于原图字符颜色或光照问题二值化结果可能反了字符黑背景白。这时务必记得使用imcomplement函数进行反转确保后续投影分析的对象是字符像素。3. 详细实现步骤拆解与代码解析接下来我们一步步走通整个流程。假设我们读入的车牌图像变量名为plate_img它是一个已经裁剪好的RGB图像。3.1 图像预处理与增强第一步是提取红色通道并增强对比度。% 1. 提取红色通道 red_channel plate_img(:, :, 1); % 2. 对比度拉伸增强前景背景差异 % 使用imadjust自动拉伸强度范围也可以手动指定范围[low_in high_in] red_enhanced imadjust(red_channel); % 3. 中值滤波去除细小噪声点如灰尘、图像压缩噪点 red_filtered medfilt2(red_enhanced, [3, 3]);这里选择medfilt2进行中值滤波是因为它能有效去除“椒盐噪声”同时较好地保留边缘而车牌字符的边缘正是我们需要保护的关键信息。滤波器的窗口大小[3,3]是一个常用起点如果图像噪点较大可以适当增大到[5,5]。3.2 稳健的二值化处理对滤波后的图像进行二值化。% 4. 使用Otsu方法计算全局阈值并进行二值化 level graythresh(red_filtered); % 计算全局阈值 bw imbinarize(red_filtered, level); % 二值化 % 5. 检查二值化结果确保字符为白色(1) % 计算图像中心一小块区域的像素均值如果背景是白色则均值0.5 center_region bw(round(end/4):round(3*end/4), round(end/4):round(3*end/4)); if mean(center_region(:)) 0.5 % 背景为白字符为黑需要反转 bw imcomplement(bw); end这一步的检查机制很重要能自动处理少数反色的情况提高算法的鲁棒性。3.3 形态学处理连接字符断点与去除干扰二值化后的图像字符笔画可能因为污损或光照出现断裂车牌边框、螺丝钉等非字符区域可能被误识别为前景。我们需要用形态学操作来“修补”和“清洁”。% 6. 形态学闭运算连接字符内部可能的断点 se_close strel(rectangle, [2, 1]); % 创建一个2行1列的矩形结构元素 bw_closed imclose(bw, se_close); % 7. 形态学开运算去除细小孤立的噪声点如边框上的点 se_open strel(square, 2); % 创建一个2x2的正方形结构元素 bw_cleaned imopen(bw_closed, se_open);闭运算先膨胀后腐蚀用[2,1]的竖长条结构元素主要目的是连接字符在垂直方向上可能出现的细小断裂比如数字“1”的笔画不连续而对水平方向影响小避免将两个字符误连。开运算先腐蚀后膨胀用小的方形结构元素目的是消除那些比字符小得多的孤立白点这些通常是噪声。实操心得结构元素的大小和形状是调参关键。[2,1]的矩形是一个针对中文字符和数字笔画的常用起点。如果车牌图像分辨率很高可能需要适当增大。务必通过imshow在每一步后查看结果确保字符笔画连通良好且没有明显误连。3.4 垂直投影分析与切割点定位这是分割的核心。我们计算二值图像每一列上白色像素的个数得到一个投影直方图。% 8. 计算垂直投影 vertical_projection sum(bw_cleaned, 1); % 对每一列求和 % 9. 平滑投影曲线减少毛刺干扰 window_size 5; vertical_smooth movmean(vertical_projection, window_size); % 10. 寻找波谷切割候选点 % 波谷即投影值局部最小的点。这里用一个简单的方法投影值低于平均值的点可能是间隙。 mean_val mean(vertical_smooth); valley_candidates find(vertical_smooth mean_val * 0.5); % 阈值设为平均值的一半可根据情况调整 % 11. 合并过于接近的候选点并确定最终切割点 min_char_width 10; % 预估最小字符宽度像素用于合并过近间隙 final_valleys []; if ~isempty(valley_candidates) final_valleys(1) valley_candidates(1); for i 2:length(valley_candidates) if valley_candidates(i) - final_valleys(end) min_char_width final_valleys [final_valleys, valley_candidates(i)]; end end end平滑操作是为了避免因单个像素列的噪声产生虚假波谷。寻找波谷的策略有很多比如找局部最小值点或者像上面一样设置一个阈值。这里使用动态阈值与平均值相关比固定阈值适应性更强。3.5 字符区域提取与后处理找到切割点后我们需要根据这些点把字符框出来。但切割点只是间隙位置字符的左右边界还需要确定。% 12. 确定字符左右边界 % 假设切割点数组final_valleys包含了字符之间的间隙位置。 % 那么第一个字符的左边界可能是图像起点右边界是第一个波谷。 % 最后一个字符的右边界是图像终点左边界是最后一个波谷。 % 中间字符的左右边界是相邻的两个波谷。 char_bboxes {}; % 用于存储每个字符的边界框[x, y, width, height] img_height size(bw_cleaned, 1); all_valleys [1, final_valleys, size(bw_cleaned, 2)]; % 在首尾添加边界 for i 1:length(all_valleys)-1 left all_valleys(i); right all_valleys(i1); width right - left; % 如果宽度太窄可能是干扰或残留边框跳过 if width min_char_width continue; end % 在垂直方向上截取有字符的部分去除上下多余空白 column_segment bw_cleaned(:, left:right); row_proj sum(column_segment, 2); % 行投影 row_idx find(row_proj 0); % 找到有像素的行 if isempty(row_idx) continue; end top min(row_idx); bottom max(row_idx); height bottom - top 1; % 存储边界框信息 char_bboxes{end1} [left, top, width, height]; end % 13. 根据边界框提取字符图像 char_images {}; for i 1:length(char_bboxes) bbox char_bboxes{i}; % 从原始灰度图或二值图上提取保留更多信息供后续识别 char_img bw_cleaned(bbox(2):bbox(2)bbox(4)-1, bbox(1):bbox(1)bbox(3)-1); % 可选统一字符图像大小归一化便于后续识别 target_size [40, 20]; % 目标高度和宽度 char_img_resized imresize(char_img, target_size); char_images{i} char_img_resized; end这一步有几个关键点宽度过滤用min_char_width过滤掉因边框残留或噪声产生的过窄区域。垂直裁剪对每个候选字符区域进行行投影只保留有像素的行这样可以去除字符上下方多余的空白区域使字符居中。归一化将切割出的字符图像缩放到统一尺寸如40x20这是很多传统字符识别算法如模板匹配、神经网络的常见预处理要求。4. 难点突破与优化策略实录在实际测试中直接套用上述流程可能会遇到各种问题。下面分享几个我遇到过的典型难题及解决方案。4.1 车牌边框与螺丝钉干扰问题二值化后车牌的白色边框和固定螺丝也可能被识别为白色前景干扰垂直投影导致在字符两侧产生错误的波峰。解决方案在形态学处理前或后增加去除边框的步骤。% 方法A利用先验知识车牌字符通常位于图像中部上下边框可以裁剪掉一定比例 [height, width] size(bw_cleaned); crop_ratio 0.1; % 裁剪掉上下各10% crop_height_start round(height * crop_ratio); crop_height_end round(height * (1 - crop_ratio)); bw_no_border bw_cleaned(crop_height_start:crop_height_end, :); % 方法B投影辅助去除。计算水平投影找到投影值持续很高的区域可能是边框将其置黑。 horizontal_proj sum(bw_cleaned, 2); border_threshold max(horizontal_proj) * 0.8; % 阈值 border_rows horizontal_proj border_threshold; bw_cleaned(border_rows, :) 0; % 将这些行全部设为背景通常方法A简单有效适合边框较规则的情况。方法B更自适应但需要调整阈值。4.2 字符粘连问题问题特别是像“京”、“苏”这样的汉字或者数字“8”其内部可能存在闭合区域经过形态学闭运算后可能导致两个字符如“8”和“A”因为距离太近而被错误地连接成一个连通域。解决方案调整形态学参数减小闭运算结构元素的宽度避免过度连接。采用更精细的切割策略当投影法切出一个宽度明显大于平均字符宽度的区域时怀疑是粘连字符。可以在这个区域内再次使用垂直投影寻找内部的波谷进行二次分割。连通域分析辅助使用bwlabel和regionprops函数标记并分析每个白色连通域。如果一个连通域的宽度远大于高度且其宽高比异常则可能是粘连字符可以尝试在其质心位置进行强制分割。% 检测并处理粘连字符示例 stats regionprops(bw_cleaned, BoundingBox, Area); avg_char_width mean([stats.BoundingBox(3)]); % 平均宽度 for i 1:length(stats) if stats(i).BoundingBox(3) avg_char_width * 1.8 % 宽度超过平均1.8倍 % 疑似粘连在该连通域内部进行二次投影分割 % ... (具体代码略) end end4.3 光照不均导致二值化失败问题在强光或阴影下车牌图像部分区域过亮或过暗全局阈值二值化会导致字符部分缺失或背景误识别。解决方案使用自适应二值化Matlab的imbinarize函数支持自适应阈值。bw_adaptive imbinarize(red_filtered, adaptive, Sensitivity, 0.6);调整‘Sensitivity’参数0到1之间值越大越多的像素被归为前景。需要根据图像质量反复试验。预处理增强在二值化前使用imtophat和imbothat进行顶帽和底帽变换可以校正不均匀光照。se strel(disk, 15); % 结构元素半径要大于字符大小 background imopen(red_filtered, se); % 估计背景 corrected red_filtered - background; % 减去背景 % 然后再对corrected进行二值化尝试其他颜色空间如果RGB通道效果都不好可以转换到HSV或YCrCb颜色空间在亮度分量(Y或V)或色度分量上进行处理有时对光照变化更鲁棒。5. 完整代码整合与测试要点将上述所有步骤整合成一个函数并加入一些异常处理使其更健壮。function [char_images, char_bboxes] segmentLicensePlate(plate_img) % SEGMENTLICENSEPLATE 分割蓝色或绿色车牌字符 % 输入plate_img - RGB格式的车牌区域图像 % 输出char_images - 单元格数组每个元素是一个二值字符图像 % char_bboxes - 单元格数组每个元素是字符在原图中的边界框[x,y,w,h] % 参数预设可根据实际情况调整 min_char_width_px 15; target_char_size [40, 20]; % [高度 宽度] % 1. 预处理 red_channel plate_img(:, :, 1); red_enhanced imadjust(red_channel); red_filtered medfilt2(red_enhanced, [3, 3]); % 2. 二值化与矫正 level graythresh(red_filtered); bw imbinarize(red_filtered, level); % 自动判断是否需要反转 center_region bw(round(end/4):round(3*end/4), round(end/4):round(3*end/4)); if mean(center_region(:)) 0.5 bw imcomplement(bw); end % 3. 形态学处理 se_close strel(rectangle, [2, 1]); bw imclose(bw, se_close); se_open strel(square, 2); bw imopen(bw, se_open); % 4. 去除上下边框简单裁剪法 [h, w] size(bw); crop_h round(h * 0.1); if crop_h 0 bw bw(crop_h:end-crop_h, :); [h, ~] size(bw); % 更新高度 end % 5. 垂直投影与平滑 vertical_proj sum(bw, 1); smooth_proj movmean(vertical_proj, 5); % 6. 寻找切割点波谷 mean_proj mean(smooth_proj); valley_idx find(smooth_proj mean_proj * 0.4); % 阈值系数可调 % 7. 合并邻近波谷确定最终切割间隙 if isempty(valley_idx) % 如果没有找到明显波谷可能字符间距极小或粘连严重 % 可以尝试按预估字符数等分作为保底策略效果可能不佳 estimated_char_num 7; valley_idx round(linspace(1, w, estimated_char_num1)); valley_idx valley_idx(2:end-1); else final_valleys valley_idx(1); for i 2:length(valley_idx) if valley_idx(i) - final_valleys(end) min_char_width_px final_valleys [final_valleys, valley_idx(i)]; end end valley_idx final_valleys; end % 8. 生成所有边界图像左、右边界 切割点 all_bounds [1, valley_idx, w]; % 9. 提取字符区域 char_images {}; char_bboxes {}; for i 1:length(all_bounds)-1 left all_bounds(i); right all_bounds(i1); width right - left; if width min_char_width_px continue; end % 垂直方向裁剪 col_seg bw(:, left:right); row_proj sum(col_seg, 2); nonzero_rows find(row_proj 0); if isempty(nonzero_rows) continue; end top nonzero_rows(1); bottom nonzero_rows(end); height bottom - top 1; % 存储边界框注意坐标是相对于裁剪后的bw图像 % 如果需要相对于原plate_img的坐标需要加上crop_h的偏移 bbox [left, top crop_h, width, height]; char_bboxes{end1} bbox; % 提取并归一化字符图像 char_img bw(top:bottom, left:right); char_img_resized imresize(char_img, target_char_size); char_images{end1} char_img_resized; end % 10. 后处理如果分割出的字符数量异常太多或太少可能是分割失败 % 这里可以添加一些启发式规则比如车牌字符数通常是7或8个 % 如果数量偏差太大可以返回空或尝试其他分割参数本例略 end测试要点构建测试集收集几十张不同光照、角度、清晰度的蓝牌和绿牌图片手动裁剪出车牌区域。可视化调试在代码关键节点如二值化后、形态学后、投影曲线、最终切割结果加入imshow和plot语句直观观察每一步的效果。评估指标人工检查分割出的字符图像是否正确、完整。可以计算字符分割的准确率正确分割的字符数 / 总字符数。参数调优重点关注min_char_width_px、投影平滑窗口大小、波谷检测阈值系数、形态学结构元素尺寸这几个参数。针对测试集中效果差的图片分析原因并调整对应参数。6. 常见问题排查与经验技巧在实际运行中你可能会遇到以下问题这里提供排查思路问题1分割出的字符数量不对多了或少了。可能原因1边框干扰。检查二值化图像看上下或左右是否有白色边框残留。加强预处理中的边框去除步骤。可能原因2字符粘连或断裂。检查形态学处理步骤。如果字符断了尝试增大闭运算结构元素的高度如果不同字符连在一起尝试减小其宽度或先进行开运算去除细小连接。可能原因3波谷检测阈值不合适。绘制平滑后的垂直投影曲线观察字符间隙处的波谷是否明显低于阈值线。调整mean_proj * 0.4中的系数0.4这个值越小对波谷的要求越严格找到的切割点越少值越大则更敏感可能找到更多切割点包括噪声产生的假波谷。问题2切割位置不准切到了字符身上或包含多余背景。可能原因1投影曲线不平滑毛刺多。增大movmean的平滑窗口大小。可能原因2字符区域垂直方向裁剪不准。检查行投影row_proj的计算和nonzero_rows的查找逻辑确保能准确找到字符的顶行和底行。解决方案在确定左右边界后可以不仅仅用行投影的非零点而是设定一个阈值比如行投影值大于最大行投影的1/5这样能更精确地界定字符上下边界避免包含笔画上方或下方的零星噪声点。问题3对于某些深蓝色或亮绿色车牌红色通道对比度不佳。解决方案实现一个自动选择最佳通道的机制。分别计算RGB三个通道的对比度例如用标准差来衡量选择对比度最高的通道进行后续处理。% 计算各通道标准差作为对比度粗略估计 contrast_r std2(plate_img(:,:,1)); contrast_g std2(plate_img(:,:,2)); contrast_b std2(plate_img(:,:,3)); [~, best_channel] max([contrast_r, contrast_g, contrast_b]); gray_img plate_img(:,:,best_channel);问题4算法速度慢处理一张图要好几秒。优化点1调整图像大小。如果输入图像分辨率很高如超过200像素宽可以先用imresize缩放到一个固定宽度如100像素再进行所有处理。这能极大减少计算量且对分割精度影响不大。优化点2向量化操作。Matlab中避免使用循环尤其是对像素的操作。本算法中的投影计算sum(bw,1)和sum(column_segment,2)都是向量化操作效率很高。检查代码中是否还有不必要的循环。优化点3预编译。将核心算法部分封装成函数并考虑使用Matlab Coder工具将其编译为MEX文件可以显著提升运行速度。最后的经验之谈车牌字符分割没有“一招鲜吃遍天”的万能参数。上面给出的所有参数形态学结构元素大小、投影平滑窗口、波谷阈值系数等都需要在你的具体数据集上进行微调。最好的方法是准备一个具有代表性的测试集编写一个简单的评估脚本批量运行并统计分割准确率然后有方向地调整参数。这个过程虽然繁琐但却是算法工程化必经的一步。记住鲁棒性往往来自于对大量 corner cases边缘情况的处理和经验积累。