
简介基于MATLAB的图片字符提取源代码PDF面向有图像处理需求的MATLAB初学者与开发者专门解决图片中字符自动定位与提取的问题可广泛应用于文字识别、文档扫描和图像预处理等场景。资源压缩包仅296KB含1个PDF文档内容紧凑、便于离线查阅已有1273人学习/下载。文档以函数形式组织主程序与子函数调用关系清晰完整演示了从读取图片、灰度化与二值化到通过行方向和列方向投影检测字符区域再到字符裁剪、统一尺寸与横向拼接输出的全过程代码中还包含边界限定子函数用于去除空白边界、提升提取清晰度。无论是学习投影法分割原理、二值化处理还是作为字符识别与图像处理项目的参考实现这份代码都能帮助读者快速上手并二次开发迁移至车牌识别、验证码处理等场景。1. 图片中字符提取为什么不能直接交给一个ocr函数很多开发者的第一反应是用 MATLAB 自带ocr直接把图片转成文字代码短到只有三行。可真正常见的场景——票据照片、车牌抓拍、翻拍的文档——很少能直接识别成功。拍照角度导致文本行倾斜光照阴影把字符和背景黏在一起螺丝、水印又带来多余笔画直接调用函数的识别率往往低到不可用。把“图片中字符的提取”拆成预处理、定位、识别、校验四个阶段每个阶段都有独立源代码和可视化结果参数影响可观测这才是 MATLAB 图像处理里更可靠的落地思路。按这条路线讲透每个环节的参数选择和常见坑适合做字符识别验证、课程设计和自动化票据解析的开发者。2. 图片中字符提取的流水线拆分与MATLAB二值化、形态学参数选型2.1 为什么先拆流程再写识别代码写图片字符提取时不建议先把识别函数调通再回头补预处理。这不是好的工作顺序因为 OCR 引擎对输入图像的容忍度是一个黑盒最后识别错了很难判断是图像本身质量差还是参数设得不对。常见的做法是把流程固定成四个输入输出都很明确的块读图与灰度化、二值化与去噪、文字区域定位、字符识别与校验。每个块输出一张图或一组坐标中间用imshow随时检查。比如二值化之后字符还不完整问题一定出在形态学参数而不是识别函数。这样做的好处是换一张测试图时不需要重写算法只需要按新图的文字尺寸、背景特征调整几个参数。下面按这个顺序把常规写法和参数边界说清楚。2.2 灰度化与二值化imbinarize的Sensitivity怎么调先从读图开始。图片可能是 jpg、png 或 tifMATLAB 都能用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.400.50字符笔画很细adaptive 且调高灵敏度Sensitivity 0.500.55白字深底adaptiveForegroundPolarity, bright2.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 像素的圆盘结构元。开运算能滤掉单像素级噪点但对正常字符笔画几乎无损伤如果图像分辨率低、笔画本身只有 12 像素就不能加大半径否则字符会被磨掉。imclose的半径取 2比开运算略大目的是把因阴影或反光断开的笔画重新接上。bwareaopen的面积阈值根据图片分辨率调整300dpi 的扫描图里一个字符动辄上千像素阈值可以设到 100 以上手机拍的小图一个字符可能只有几十像素阈值设 40 左右比较稳妥。这组参数没有全局最优值要以实际字符高度为基准估算。参考经验是字符高度 24 像素左右时开运算半径 1、闭运算半径 12、bwareaopen 设 2040字符高度 60 像素以上时开闭运算半径放到 23面积阈值从 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 配置识别场景CharacterSetTextLayout车牌号码0-9 A-Z 及省份汉字line银行卡号0-9 和空格line票据填写0-9 A-Z 常见符号word扫描文档段落不加限制block3.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 像素时半径取 3060 都能工作。用除法而不是减法是因为除法保留的是局部对比度而不是绝对亮度这样在暗角区域字和背景之间的比例不会失真。这个方法的坑在于如果图片里有大面积纯黑色块估计出的背景会偏低处理前先考虑是否屏蔽这些区域。表 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面积阈值上只需在外面再套一层循环把多参数变成半小时内能跑完的网格搜索。字符提取任务的参数空间一般不大穷举的性能开销完全可接受。找到最优参数后把它回填到预处理函数里再对整张测试图重新跑一遍识别确认文本前后一致就把这套源码里的固定参数替换掉了。本文还有配套的精品资源点击获取