简介:基于MATLAB的图片字符提取源代码PDF,面向有图像处理需求的MATLAB初学者与开发者,专门解决图片中字符自动定位与提取的问题,可广泛应用于文字识别、文档扫描和图像预处理等场景。资源压缩包仅296KB,含1个PDF文档,内容紧凑、便于离线查阅,已有1273人学习/下载。文档以函数形式组织,主程序与子函数调用关系清晰,完整演示了从读取图片、灰度化与二值化,到通过行方向和列方向投影检测字符区域,再到字符裁剪、统一尺寸与横向拼接输出的全过程;代码中还包含边界限定子函数,用于去除空白边界、提升提取清晰度。无论是学习投影法分割原理、二值化处理,还是作为字符识别与图像处理项目的参考实现,这份代码都能帮助读者快速上手并二次开发,迁移至车牌识别、验证码处理等场景。
1. 图片中字符提取为什么不能直接交给一个ocr函数
很多开发者的第一反应是用 MATLAB 自带ocr直接把图片转成文字,代码短到只有三行。可真正常见的场景——票据照片、车牌抓拍、翻拍的文档——很少能直接识别成功。拍照角度导致文本行倾斜,光照阴影把字符和背景黏在一起,螺丝、水印又带来多余笔画,直接调用函数的识别率往往低到不可用。把“图片中字符的提取”拆成预处理、定位、识别、校验四个阶段,每个阶段都有独立源代码和可视化结果,参数影响可观测,这才是 MATLAB 图像处理里更可靠的落地思路。按这条路线讲透每个环节的参数选择和常见坑,适合做字符识别验证、课程设计和自动化票据解析的开发者。
2. 图片中字符提取的流水线拆分与MATLAB二值化、形态学参数选型
2.1 为什么先拆流程再写识别代码
写图片字符提取时,不建议先把识别函数调通再回头补预处理。这不是好的工作顺序,因为 OCR 引擎对输入图像的容忍度是一个黑盒,最后识别错了,很难判断是图像本身质量差,还是参数设得不对。
常见的做法是把流程固定成四个输入输出都很明确的块:读图与灰度化、二值化与去噪、文字区域定位、字符识别与校验。每个块输出一张图或一组坐标,中间用imshow随时检查。比如二值化之后字符还不完整,问题一定出在形态学参数,而不是识别函数。这样做的好处是:换一张测试图时不需要重写算法,只需要按新图的文字尺寸、背景特征调整几个参数。下面按这个顺序把常规写法和参数边界说清楚。
2.2 灰度化与二值化:imbinarize的Sensitivity怎么调
先从读图开始。图片可能是 jpg、png 或 tif,MATLAB 都能用imread一次性读入。如果是彩色图,第一步是压缩成单通道灰度图。新版 MATLAB 里im2gray和rgb2gray结果一致,对 GPU 数组兼容性更好,老项目用rgb2gray也没问题。
% 读取图片并转为灰度图 img = imread('plate.jpg'); % 支持常见位图和tif if size(img, 3) == 3 gray = im2gray(img); % RGB 转单通道灰度 else gray = img; % 本身是灰度图就直接用 end % 全局阈值二值化,适合白底、光照均匀的扫描图 bwGlobal = imbinarize(gray); % 自适应阈值二值化,适合照片、阴影、渐变背景 bwAdapt = imbinarize(gray, 'adaptive', ... 'Sensitivity', 0.45, 'ForegroundPolarity', 'dark');第一个imbinarize不带额外参数时,用 Otsu 方法自动算一个全局阈值。只要图像背景干净、亮度一致,这个默认值就够用。第二个则是按像素邻域统计计算局部阈值,Sensitivity控制“把多少比例的像素判为前景字符”。这是调试时第一个要动的参数:调得越大,字符笔画选得越全,但水印、噪点也越容易被当文字;调得太小,浅色笔画会被吞掉。
ForegroundPolarity用来告诉函数字符是深色还是浅色。深色文字浅色背景填'dark',反色图填'bright'。这个参数填反,识别结果基本就是背景噪声,属于最典型的低级错误。各场景参数经验见表 2-1。
表 2-1 二值化方式与参数选择
| 图片特征 | 推荐方式 | 关键参数 |
|---|---|---|
| 白底扫描件、截图 | imbinarize(gray)全局阈值 | 不需要额外参数 |
| 有阴影或渐变背景 | adaptive 自适应 | Sensitivity 0.40~0.50 |
| 字符笔画很细 | adaptive 且调高灵敏度 | Sensitivity 0.50~0.55 |
| 白字深底 | adaptive | 'ForegroundPolarity', 'bright' |
2.3 形态学开闭运算与bwareaopen的配合
二值化结果里往往同时存在两类问题:细小噪点和笔画断裂。这一步用形态学操作处理,一般不需要引入复杂的滤波器。
bw = ~bwAdapt; % 反色,让字符为白色前景,背景为黑色 seOpen = strel('disk', 1); % 圆盘结构元,半径1像素 bw = imopen(bw, seOpen); % 开运算:先腐蚀后膨胀,移除小噪点 seClose = strel('disk', 2); % 半径取2,闭合细小的笔画缺口 bw = imclose(bw, seClose); % 闭运算:先膨胀后腐蚀,缝合断裂 bw = bwareaopen(bw, 40); % 删除面积小于40像素的孤立连通域strel('disk', 1)是半径 1 像素的圆盘结构元。开运算能滤掉单像素级噪点,但对正常字符笔画几乎无损伤;如果图像分辨率低、笔画本身只有 1~2 像素,就不能加大半径,否则字符会被磨掉。imclose的半径取 2,比开运算略大,目的是把因阴影或反光断开的笔画重新接上。bwareaopen的面积阈值根据图片分辨率调整:300dpi 的扫描图里一个字符动辄上千像素,阈值可以设到 100 以上;手机拍的小图一个字符可能只有几十像素,阈值设 40 左右比较稳妥。
这组参数没有全局最优值,要以实际字符高度为基准估算。参考经验是:字符高度 24 像素左右时,开运算半径 1、闭运算半径 1~2、bwareaopen 设 20~40;字符高度 60 像素以上时,开闭运算半径放到 2~3,面积阈值从 100 起步。
2.4 用regionprops按几何特征锁定文字区域
二值化做完后,图片里除了字符还会留下一些非文字块。定位文字区域时,regionprops是最直接的函数。
% 找连通域并提取几何属性 cc = bwconncomp(bw, 8); % 8邻域更适合连通笔画 stats = regionprops(cc, 'BoundingBox', 'Area', 'Extent'); boxes = reshape([stats.BoundingBox], 4, [])'; % 每个连通域的外接框 areas = [stats.Area]'; widths = boxes(:, 3); heights = boxes(:, 4); hImg = size(bw, 1); % 按面积、高度、宽高比过滤非文字区域 textIdx = areas > 40 & ... % 面积过小的是噪点 heights > 6 & heights < 0.25 * hImg & ... % 字符高度范围 widths ./ heights > 0.15 & ... % 宽高比下限:竖线 widths ./ heights < 4; % 宽高比上限:横条 figure; imshow(bw); hold on; for i = find(textIdx)' rectangle('Position', boxes(i, :), 'EdgeColor', 'r', 'LineWidth', 1); end hold off;bwconncomp的第二个参数 8 表示八邻域连通。只用上下左右判断,斜向相连的笔画会被当成两个区域,所以字符区域一般用 8。过滤条件里的宽高比很关键:拉丁字母和数字的宽高比在 0.15 到 3 之间,连在一起的横线、装饰边框往往超出这个范围,可以直接排除。把过滤后的外接框叠加在原图上,能清楚看到哪些区域被算法保留。如果漏选,优先调bwareaopen的面积阈值,而不是去放宽宽高比。
3. 用OCRText对象完成字符识别与模板匹配兜底:MATLAB提取源代码的核心
3.1 ocr函数的最小调用方式和工具箱前提
预处理和区域定位完成后,识别逻辑可以先用 MATLAB 自带 OCR 函数跑通。这个函数在计算机视觉工具箱里,连同前面用到的图像处理工具箱,安装 MATLAB 时最好一起勾选。如果报“未定义函数或变量”,先检查工具箱许可证,而不是怀疑代码。
% 对预处理后的二值图直接OCR results = ocr(bw); recognizedText = results.Text; % 指定ROI,只识别坐标范围内的字符 resultsRoi = ocr(bw, [10, 10, 200, 50]);把二值图直接传给ocr比传灰度图更稳:背景里的细微渐变在二值化后已经消失,OCR 引擎只面对黑和白,输入状态简单。results是一个OCRText对象,常用字段有三个:Text是完整识别结果字符串,WordBoundingBoxes是每个词的坐标,WordConfidences是对应置信度。ocr的第二参数可以传入[x, y, w, h]的 ROI,固定格式票证识别里常用它限定字段区域,防止相邻字段干扰。
3.2 CharacterSet和TextLayout:限制范围就是提升准确率
OCR 引擎在候选字符池越小的情况下越不容易认错。车牌、编号、验证码场景下,可以明确告诉引擎只允许哪些字符出现。
% 只识别英文字母和数字,跳过中文和特殊符号 results = ocr(bw, 'CharacterSet', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', ... 'TextLayout', 'line');CharacterSet会覆盖默认字母表。实际调试中“O和0不分”“I和1不分”的问题,靠限定字符集能解决一部分,剩下的靠置信度判断。TextLayout告诉引擎按什么粒度解析:'line'适合单行文本,如车牌、货单;'word'适合表格单元格里的短字段;'block'适合段落型文书。布局选错,多行文字会被识别成混乱的一串。
表 3-1 场景与 CharacterSet、TextLayout 配置
| 识别场景 | CharacterSet | TextLayout |
|---|---|---|
| 车牌号码 | 0-9 A-Z 及省份汉字 | line |
| 银行卡号 | 0-9 和空格 | line |
| 票据填写 | 0-9 A-Z 常见符号 | word |
| 扫描文档段落 | 不加限制 | block |
3.3 中文与固定字体场景的模板匹配兜底
内置 OCR 对英文数字支持较好,对中文字符的支持则依赖语言包,配置较麻烦。项目里常见做法是:中文字符走模板匹配兜底,在准备好的字符模板库里找出最相似的图像,输出对应字符。这句话的核心在于“模板库是有限集合”,所以匹配速度很快,也容易追溯错误。
function ch = matchByTemplate(charImg, templateDir) % 输入 charImg 为预处理后的单个字符图像,逻辑类型 % 输出匹配到的字符 charImg = im2double(charImg); % 统一转成double便于计算 files = dir(fullfile(templateDir, '*.png')); scores = zeros(numel(files), 1); for i = 1:numel(files) tplPath = fullfile(templateDir, files(i).name); tpl = imread(tplPath); tpl = imresize(im2gray(tpl), size(charImg)); % 模板缩放到同一尺寸 tpl = im2double(imbinarize(tpl)); v = tpl(:); c = charImg(:); if std(c) == 0 || std(v) == 0 scores(i) = 0; % 常数图像不能用相关系数 else scores(i) = corr2(c, v); end end [~, idx] = max(scores); [~, name] = fileparts(files(idx).name); ch = name(1); % 模板文件名即字符 end这段代码先把每个模板缩放到与待识别字符相同的尺寸,再用corr2计算相关系数。相关系数接近 1 表示相似度高,低于 0.6 基本就是模板库里没有这个字符。模板文件名直接存成字符本身,比如A.png、京.png,识别结果直接从文件名取,省掉维护一套字符映射表的功夫。模板匹配的局限是字体依赖强,同一字符换一种字体就可能配对错误。所以模板库要按字体分类存放,识别前先明确目标字体。
3.4 置信度过滤:识别和校验要分开写
OCR 对象返回的置信度是后续校验环节最重要的输入。
% 按词过滤低置信度结果 words = results.Words; conf = results.WordConfidences; box = results.WordBoundingBoxes; highConfIdx = find(conf > 0.6); for i = 1:numel(highConfIdx) k = highConfIdx(i); fprintf('%s 置信度%.2f 位置[%d,%d]\n', ... words{k}, conf(k), box(k, 1), box(k, 2)); end低置信度结果要分两类处理:如果整行都低,说明预处理环节出了问题,回去检查二值化和形态学参数;如果只有个别字低,说明切割或字符集有问题,可以只针对该词的外接框重新做一次ocr。把识别和校验拆成两步,后续想加规则、加字典才不会被识别函数的结构绑死。很多字符提取项目里“准确率差一点”的根源不在 OCR 引擎,在没人看置信度,直接拿Text当最终结果。
4. 字符提取实战中4个典型参数坑:倾斜校正、光照不均、粘连字符与透视畸变
4.1 文本倾斜时用radon变换估计角度
手拍图片几乎都有轻微旋转,OCR 对倾斜尤其敏感。常用做法是先用radon变换找出文本行的主方向,再做反向旋转。
% 输入bw为预处理后的二值图,假定文字基本水平但有小角度倾斜 theta = -30:0.5:30; [R, ~] = radon(bw, theta); % radon输出矩阵的列对应theta中的角度 [~, idx] = max(R(:)); [~, col] = ind2sub(size(R), idx); angle = theta(col); % 反向旋转,把文字转回水平 corrected = imrotate(bw, -angle, 'bilinear', 'crop');radon在某个角度上的投影会把该方向上最亮的线性结构累加出来。文本行是一条较强的直线,所以峰值所在列对应的角度就是文字倾斜角。搜索范围限制在-30°到30°,避免把竖排文字误判成倾斜文本。imrotate的'bilinear'插值对字符边缘更平滑,'crop'保持输出尺寸不变。角度检测结果可能与实际倾斜方向相差 180°,旋转后检查前景面积是否明显下降,如果下降就把角度加上 180° 再转一次。
4.2 光照严重不均:先估计背景再做除法归一化
自适应二值化能处理大部分阴影,但遇到光照梯度特别大的图,局部阈值会把同一个字符切割成明暗两段。更稳的做法是背景归一化法。
grayD = double(gray); % 转成double参与除法 seBg = strel('disk', 60); % 半径必须大于字符笔画 bg = imclose(grayD, seBg); % 估计背景亮度分布 normImg = grayD ./ (bg + 1); % 用除法消除背景梯度 bwNorm = imbinarize(normImg); % 背景拉平后再二值化imclose用一个大半径结构元,把字符当作前景噪声滤掉,留下背景的亮度变化。半径取值关系到归一化效果,一般要大于最粗字符笔画的两倍;字符高度 30 像素时,半径取 30~60 都能工作。用除法而不是减法,是因为除法保留的是局部对比度而不是绝对亮度,这样在暗角区域,字和背景之间的比例不会失真。这个方法的坑在于:如果图片里有大面积纯黑色块,估计出的背景会偏低,处理前先考虑是否屏蔽这些区域。
表 4-1 自适应二值化与背景归一化对比
| 项目 | 自适应二值化 | 背景归一化+全局阈值 |
|---|---|---|
| 适用场景 | 中等阴影、渐变背景 | 光照梯度大、暗角明显 |
| 需要调的参数 | Sensitivity | 结构元半径 |
| 典型缺陷 | 大阴影下字符断裂 | 大面积深色块干扰背景估算 |
4.3 字符粘连时用距离变换加watershed拆开
印刷体和手写体都常有字符粘连,尤其两条竖笔画靠得很近时,OCR 会把两个字符当整体,输出错误结果。分水岭分割是常见的解决手段。
% 对二值图计算距离变换 dist = bwdist(~bw); dist(~bw) = -Inf; % 让背景区域不参与分水岭 L = watershed(dist); % 返回按集水盆划分的标签矩阵bwdist计算每个前景像素到最近背景像素的距离,字符中心处距离最大。watershed把这个距离图当地形,两个字符中心之间的低洼处会被切开,从而分离粘连字符。这套方法对“两个字符并排放置”非常有效,对“上下笔画交叉”容易切坏。分割后使用regionprops重新提取外接框时,注意每个框是否只包含一个字符;如果两个中心点还在同一个框里,先用imerode把字形缩窄 1 像素再切一次。
4.4 透视畸变:找四个角点做射影变换
拍歪的文档除了旋转,还有透视,表现在字符两边宽窄不一致。更规范的处理是用四个角点做射影变换,把图像拉回正面视角。
% 原图上检测或手动选取的四个角点 movingPoints = [x1 y1; x2 y2; x3 y3; x4 y4]; % 目标矩形的四个角点,顺序必须对应 fixedPoints = [0 0; w 0; w h; 0 h]; tform = fitgeotrans(movingPoints, fixedPoints, 'projective'); rectified = imwarp(img, tform, 'OutputView', imref2d([h w]));fitgeotrans的'projective'类型用四对对应点估计单应矩阵;imwarp按矩阵对图像重采样。角点可以手动选,也可以在预处理图上用corner函数自动找。这里最大的坑是角点顺序必须与fixedPoints一一对应,否则图像会发生镜像翻转。透视校正完成后,再检查一次 4.1 的 radon 角度,因为射影变换本身可能引入新的旋转。
5. 用编辑距离验证字符提取效果,让参数选择不再靠猜
5.1 Levenshtein距离的纯MATLAB实现
调参中最烦的问题:同一张图,改了一版预处理代码,识别结果到底是变好还是变差?靠眼睛盯控制台看当然可以,但参数一多容易看错。把这个问题量化,常见的度量就是编辑距离,即“把识别结果变成标准答案最少需要多少次插入、删除、替换”。
function d = levenshteinDist(a, b) % 计算字符串a和b之间的编辑距离 a = char(a); b = char(b); m = numel(a); n = numel(b); D = zeros(m + 1, n + 1); D(:, 1) = (0:m)'; D(1, :) = 0:n; for i = 1:m for j = 1:n cost = ~strcmp(a(i), b(j)); D(i + 1, j + 1) = min([D(i, j + 1) + 1, ... % 删除 D(i + 1, j) + 1, ... % 插入 D(i, j) + cost]); % 替换 end end d = D(m + 1, n + 1); end矩阵D(i, j)表示 a 的前 i 个字符和 b 的前 j 个字符的最小编辑次数。初始化时,空字符串到任意字符串的距离等于其长度;递推则分别考虑删除、插入、替换三种操作的成本。这里不用文本分析工具箱自带的editDistance,原因是自己实现可以随时改代价系数。例如想把“字符替换”的代价从 1 改成 2,直接在cost那行乘一个权重即可,这在 OCR 场景里很有用,因为相近字符的替换代价本来就应该更低。
标准答案文本准备好后,与 OCR 输出比较一次,就能得到精确的错误计数:
gtText = '京A12345'; % 标准答案 outText = upper(strtrim(results.Text)); % 去除首尾空白并统一大小写 d = levenshteinDist(outText, gtText); fprintf('编辑距离 %d\n', d);5.2 自动搜索最优Sensitivity参数
有了可量化的指标,参数调优就可以从手工试探变成自动搜索。下面这段代码对Sensitivity从 0.35 到 0.60 做网格搜索,选出编辑距离最小的值:
sensList = 0.35:0.05:0.60; bestDist = Inf; bestSens = sensList(1); for s = sensList bwT = imbinarize(gray, 'adaptive', ... 'Sensitivity', s, 'ForegroundPolarity', 'dark'); txtT = upper(strtrim(ocr(bwT).Text)); d = levenshteinDist(txtT, gtText); if d < bestDist bestDist = d; bestSens = s; end end fprintf('最优Sensitivity = %.2f,编辑距离 = %d\n', bestSens, bestDist);循环体里每次只改动一个参数,保持其他预处理环节不变,这样找到的bestSens针对当前这套流程最优。同样的思路可以套在形态学结构元半径、bwareaopen面积阈值上,只需在外面再套一层循环,把多参数变成半小时内能跑完的网格搜索。字符提取任务的参数空间一般不大,穷举的性能开销完全可接受。找到最优参数后把它回填到预处理函数里,再对整张测试图重新跑一遍识别,确认文本前后一致,就把这套源码里的固定参数替换掉了。
本文还有配套的精品资源,点击获取