FEATURED · 精选文章

中文字符图片分割方法实战:从投影法到CRNN隐式分割

发布时间 / 2026/9/16 3:12:58
来源 / 创域科博编辑部
栏目 / 资讯中心
中文字符图片分割方法实战:从投影法到CRNN隐式分割 做中文OCR的人应该都有过这种经历拿到一张中文字符图片识别结果五花八门第一反应是换更强的识别模型折腾一圈之后发现精度还是上不去。我踩过不少这类坑后逐渐意识到一个常被忽略的事实——分割方法才是很多中文OCR项目里决定上限的环节。文本检测框准了行切出来了但字符与字符之间的边界没划对后面接再厉害的识别器也是白搭。这篇文章想跟各位认真梳理一遍中文字符图片的分割方法从传统像素层面怎么用投影法、连通域分析切字符到现代开源OCR方案尤其是CRNN那套路线里“隐式分割”的核心原理再到我自己在落地开源中文OCR工具时的完整实操过程和排错经验。不管你是刚入门的视觉算法工程师还是正在选型开源OCR方案的技术负责人这篇文章应该都能帮你少走一些弯路。1. 为什么中文字符分割是个真问题而不是一个伪需求1.1 OCR流水线里分割问题其实无处不在一条典型的OCR流水线是这样的图像预处理、文本检测、文本识别、后处理。如果用的是老式的逐字识别架构文本检测之后还有一步很关键的“字符分割”就是把文本行切成一个个单独的字符再逐个送进字符识别器。这里有个容易被忽视的事实即使你今天用的是PaddleOCR、CRNN这类端到端模型分割也没有消失它只是被“隐式”地处理了。CRNN不显式切字但它在训练和推理时本质上要解决一个序列对齐问题——输入图像的特征序列怎么和最终的中文字符串对应起来。理解这一点之后你再看那些识别错误很多都能追溯到“对齐”也就是“隐式分割”出错上。所以我一直觉得讨论中文字符图片的分割方法不是在聊一个过时的传统OCR话题而是在拆解现代OCR模型的底层逻辑。无论显式还是隐式分割都是中文OCR绕不开的一环。1.2 中文和英文在分割难度上差了一个量级英文字母和数字的字符分割相对好做因为英文单词之间有天然空格字母宽度相对固定笔画又简单投影法一刀下去基本就分干净了。中文就完全不一样了。中文是方块字字符之间没有明确的物理间隔全都紧密排列在一个矩形网格里。更麻烦的是汉字内部结构复杂左右结构明、汉、她、上下结构吉、志、花、包围结构国、圆、回、左中右结构湖、树这些结构在像素层面上会造成笔画的高密度交织。我举个最常见的例子把“明”字做垂直投影日字旁和月字旁之间的空白区可能并不明显如果阈值设置不当“明”会被硬生生切成“日”和“月”两个伪字符。这就是为什么用英文OCR的逻辑去处理中文经常会在字符分割阶段翻车。下表是我整理的中英文分割难点对比维度英文/数字中文汉字字符间隔有天然空格边界清晰无空格字符紧密排列字符宽度相对固定分布集中等宽但笔画复杂度差异大笔画密度低二值化后噪声少高连通域复杂且易粘连内部结构简单投影谷底明显左右/上下/包围结构投影谷底不明显标点符号半角宽度小全角与汉字宽度相同容易误判1.3 什么场景下仍然需要显式的字符分割端到端模型已经很强的今天为什么还要会显式的字符分割我实际接触过的场景主要有这么几类。第一类是古籍和手写文档数字化这类文本往往没有规整的排版字符大小不一、倾斜严重、笔画断裂端到端模型在没有足够训练数据时表现并不好先按字切出来再逐字识别反而是更稳的方案。第二类是字符级样本生成比如你要做合成数据来训练识别模型需要先把单个字渲染出来再拼成文本行这个“拼”的过程本质上就是逆分割。第三类是某些对字符位置有精确要求的场景比如表格识别、公式识别、印章比对光有识别结果不够还要告诉下游每个字符的精确坐标这时就必须做显式分割。另外CRNN这类开源中文OCR方案里的分割思想也值得单独说。CRNN虽然不显式输出字符框但CTC解码的过程其实是一个“字符级别的路径切分”理解它你才能真正理解中文OCR的识别结果为什么会错以及怎么调优。2. 像素层面的分割实战投影法与连通域分析的细节2.1 预处理决定了分割的上限很多人一上来就直接对原图做投影分割这是最容易犯的错误。字符分割的上限不是由分割算法决定的而是由预处理决定的。一张有阴影、有噪声、对比度低的中文字符图片任何分割算法都很难取到干净的结果。我常用的预处理流程是灰度化、二值化、形态学去噪。二值化这里要特别注意全局OTSU阈值并不总是好用。拍照文档经常有光照不均同一张图里上亮下暗OTSU会把暗部背景直接变成黑色文字区域导致字符粘连。这种场景下我一般推荐自适应阈值cv2.adaptiveThreshold或者先做背景估计和差影法把光照不均匀去掉再做全局二值化。形态学操作也很关键。中文字符笔画断裂很常见我会用一个小的闭运算核比如3x3或5x5的矩形核把断笔连起来对于噪声点则先用一个更小的开运算核去掉。这里的核大小不能拍脑袋定要根据字符高度的比例来选我一般取字符高度的1/50到1/30作为核尺寸大一点会把本来相邻的字符粘在一起小一点又起不到连接断笔的作用。2.2 投影法切行与切字原理与代码实现投影法的本质很朴素把二维图像降到一维看前景像素在某个方向上的累计分布。水平投影就是把每一行的前景像素数量加起来得到一个高度为图像高度的一维数组。文本行之间的空白区域在这个数组里表现为波谷因此可以通过波谷位置把图像切成一行一行的独立文本行。垂直投影同理把每一列的前景像素数量加起来字符之间的空隙就是波谷据此可以把一行文本切成单个字符。这个思路用生活化的方式理解就是一间写字楼里每层住了多少人水平投影是数每一层的人数垂直投影是数每一列的人数。空白楼层就是没人住对应文本行的间隔空白列就是没人办公对应字符间隔。下面是我用OpenCV实现一个简单投影切分函数的参考代码能直接跑通import cv2 import numpy as np def project_split(gray_img, min_char_width10): # 自适应阈值二值化白色为前景 bin_img cv2.adaptiveThreshold( gray_img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 15 ) h, w bin_img.shape # 水平投影切行 hor_proj np.sum(bin_img, axis1) // 255 rows [] in_text False for y in range(h): if hor_proj[y] 0 and not in_text: start_y y in_text True elif hor_proj[y] 0 and in_text: end_y y in_text False if end_y - start_y 5: rows.append((start_y, end_y)) if in_text: rows.append((start_y, h)) # 对每一行做垂直投影切字 char_boxes [] for (start_y, end_y) in rows: line bin_img[start_y:end_y, :] ver_proj np.sum(line, axis0) // 255 in_char False for x in range(w): if ver_proj[x] 0 and not in_char: start_x x in_char True elif ver_proj[x] 0 and in_char: end_x x in_char False if end_x - start_x min_char_width: char_boxes.append((start_x, start_y, end_x, end_y)) if in_char: if w - start_x min_char_width: char_boxes.append((start_x, start_y, w, end_y)) return bin_img, rows, char_boxes这段代码的核心逻辑很简单按行扫描投影值遇到0值就切断。但用的时候要注意两个问题。第一个问题是左右结构汉字的误切。前面提到过“明”字的垂直投影中间可能没有明显的0值谷底但可能会出现一个“相对低”的谷值。如果简单地用“投影值是否为0”来切分会漏切如果用固定阈值比如投影值小于某个数就切又容易把“明”这种字拦腰切断。我通常会在切完之后加一个“合并”步骤如果连续两个字符的宽度都明显小于该行平均字符宽度并且它们刚好是一个左右结构汉字的两个部分就把它们合并回一个字符。判断左右结构可以用一个朴素规则两个子区域的投影区间距离很小几个像素以内合并后的宽度不超过该行字符平均宽度的1.2倍且合并前两个子区域的高度接近。第二个问题是标点符号。中文句号“。”、逗号“”、引号“””在二值化后有时会和前面的汉字粘连导致垂直投影谷底消失。这种情况我也会在分割后处理里单独判断如果某个候选字符的宽度特别小明显小于平均宽度但高度又很大就把它并到前一个字符或者过滤掉视下游识别器的需求而定。2.3 连通域分析作为投影法的重要补充投影法虽然快但遇到字符倾斜、笔画断裂、字符内部空洞时就容易出错。这时候连通域分析能帮上大忙。连通域分析的核心思想是把二值图中所有像素值相同且位置相邻的像素聚成一个块每个块就是一个连通域。OpenCV里可以直接用connectedComponentsWithStats拿到每个连通域的包围盒、面积、质心等统计信息。num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(bin_img, connectivity8) # stats: (x, y, width, height, area)拿到了所有连通域之后我可以做几件有价值的事第一过滤噪声。面积过小的连通域基本都是二值化的噪声点直接删掉。第二按空间位置聚类。每个汉字的主体一般会对应一个或几个大连通域左右结构的字可能被笔画断裂分成两个连通域通过分析连通域之间的水平距离和垂直重叠度可以把属于同一个字的多个连通域合并。第三用连通域的包围盒去修正投影法得到的字符边界。我通常的策略是先用投影法粗切出候选字符框然后用连通域信息做二次校准。比如某个字符框边界恰好落在笔画中间投影法看不出问题但连通域分析能发现框内某个大连通域被框截断了——它的包围盒左边刚好顶着字符框左边界这个信息提示我字符框可能切偏了应该根据连通域的包围盒微调边界到最近的波谷位置。这套“投影粗切连通域校准”的组合拳处理印刷体中文票据、截图里的中文字符图片已经比较够用了。但如果是手写体、艺术字、弯曲文本像素层面的方法就开始力不从心需要上深度学习方案。3. 深度学习路线从CRNN的CTC对齐到分割式检测3.1 端到端模型如何把“分割”变成“对齐”近年来开源社区最火的轻量中文OCR方案基本绕不开CRNN这套结构。CRNN把特征提取、序列建模、序列解码串在一起其中最关键的设计是用CTC损失函数来处理“输入特征序列长度远大于字符序列长度”的问题。CRNN的处理流程大致是图像经过CNN主干网络提特征由于特征图在宽度方向上被压缩每一列特征对应原图的一个竖直条带这些特征序列再送入双向LSTM学习字符之间的上下文依赖最后序列进入CTC层CTC负责把模型的逐帧输出和最终的中文字符串对齐。CTC的核心机制是引入一个“空白符号blank”并允许重复字符合并。简单来说模型对每个时间步输出一个概率分布分布里的类别包括中文字符集和blank。解码时先把每一帧概率最大的类别串起来然后合并连续的重复类别最后去掉空blank符号得到最终字符串。比如模型连续输出“中中中_文_文”这里_代表blank经过去重和去blank后得到“中文”。这个过程里CTC其实隐式地学会了字符分割它知道哪些帧对应“中”这个字符的开始哪些帧对应结束。但它学到的不是像素级的边界而是时间步级别的对齐关系。这也解释了为什么CRNN对字符边界不需要显式标注——训练时只需要文本行级别的中文label模型自己通过对齐规则学会“切分”。对于中文OCR来说这种隐式分割的价值是巨大的。传统显式分割一旦切错一个字错误就传导到后续所有字符而CRNN是序列到序列的建模上下文信息可以帮助模型在某些字符模糊时靠前后文猜出来不会因为一次切分错误导致整行崩溃。3.2 基于分割的文本检测DBNet的思路除了字符分割现代OCR里还有一个“分割”层面容易被混淆那就是文本检测图中的分割。PaddleOCR等开源中文方案里常用的DBNet检测器就是典型的基于分割思想做的文本检测虽然它分割的不是字符而是“文字区域”。DBNet让网络对每个像素输出一个概率值表示该像素属于文字区域的置信度这样就得到一张概率图。然后网络同时学习一个“阈值图”对每个像素预测一个自适应阈值最终通过可微分二值化把概率图转成二值图再从中提取连通域得到文本检测框。我为什么要把这一步拿出来讲因为在实际项目中很多人把文本检测框看作矩形坐标却忽略了检测框内部其实是一个概率分布。当你拿一段中文图片做识别检测框边界的微小偏差直接会影响后续识别和分割的输入质量。比如DBNet检测模型把两列文字检测成一个框或者把一个文本行从中间截断都会导致最终中文字符图片的识别结果错得离谱。理解了这个“分割思想”你会更容易接受一个事实现代OCR的各个环节是耦合的字符分割不止发生在“识别前”也可能发生在“检测中”。3.3 开源方案里字符级分割到底要不要做很多朋友看到CRNN不需要显式字符分割就问那我是不是根本不用管字符分割了答案是看场景。如果你做的是常规印刷体文本行识别用开源中文OCRPaddleOCR、MMOCR、Tesseract的端到端路线就够了确实不需要自己再切字符。但如果你需要的是字符级坐标或者你的中文图片里包含了大量竖排文本、手写字符、形近字那就必须做一些显式的字符级后处理。一个实用的组合思路是先让PaddleOCR检测并识别整个文本行拿到识别结果和置信度然后对置信度较低的文本行单独做图像裁切和字符级切分检查是不是字符边界出了问题。这相当于用显式分割去兜底端到端模型的失败样本两者并不冲突。另外如果你想在开源CRNN基础上微调训练数据通常只需要文本行级别的标签这比做字符级标注要轻松太多。我见过有的团队为了做自定义中文OCR吭哧吭哧标了几十万个单字框其实完全没必要——走CRNN路线标文本行就够了。3.4 用分割网络直接做字符级实例分割如果你想更进一步还可以用现代语义分割/实例分割网络直接做字符级分割把输入图像的每个像素分类为“某个字符”或“背景”输出每个字符的掩膜。U-Net、PSPNet都是常见的骨干结构。实际操作时训练数据要准备成“字符级掩膜标注”也就是每个字符一个mask。这一步标注成本最高但好处也很明显字符重叠、粘连严重的中文图片也能切出来而且可以直接输出字符形状不只是矩形框。我在测试古籍文档分割时试过这条路效果确实比像素级方法稳定但工程复杂度高了不少需要自己标注数据、设计网络、做后处理。字符掩膜出来之后还要用连通域分析把每个字符的掩膜转成独立的包围盒或轮廓。对于一般应用来说这个成本不一定划算除非你的中文图片质量太差传统方法完全无解才建议走这条路。所以我的建议始终是先评估需求再选择方法不要为了追“网络越深越好”而盲目上分割网络。4. 实操指南基于开源中文OCR做分割落地4.1 环境准备与模型选择把原理聊完了进入实操环节。我近期的项目里最常用的开源中文OCR方案是PaddleOCR其次是MMOCR。选择PaddleOCR的原因很简单中文识别效果好、模型下载方便、部署方案丰富而且它内部已经集成了DBNet检测和基于CRNN改进的识别模型。安装PaddleOCR时最容易出问题的环节是依赖版本冲突。建议用Python 3.8到3.10之间的版本用virtualenv或conda单独建环境避免污染其他项目。安装命令很简单pip install paddlepaddle paddleocr模型下载时要注意PaddleOCR的模型是按检测、方向分类、识别分开下载的。如果网络情况一般建议提前把模型文件下载好放到固定目录下通过det_model_dir、rec_model_dir等参数指定本地路径避免每次初始化都去远端拉取。实际使用中发现识别模型有大、中、小等规格轻量模型速度快但识别中文的准确性下降明显。如果对精度有要求直接上精度版模型。4.2 一条完整的中文图片识别加字符切分流水线我以一张中文票据截图为例演示从图像输入到文本行识别再到字符级分割的完整流程。票据类图像的特点很典型有表格线干扰、字体是印刷体、文本行横平竖直但长短不一。第一步加载OCR模型并识别整图from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) result ocr.ocr(receipt.png, clsTrue) for line in result[0]: box, (text, conf) line print(f文本: {text}, 置信度: {conf:.4f})第二步拿到检测框坐标后对于置信度低于0.9的文本行裁出文本行图像做灰度化和二值化预处理然后用投影法做字符级切分检查是否存在误切import cv2 import numpy as np def debug_char_split(cropped_gray, line_text): # 自适应二值化 bin_img cv2.adaptiveThreshold( cropped_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 15 ) # 垂直投影 ver_proj np.sum(bin_img, axis0) // 255 # 找到所有投影值为0的列作为切分点 split_points [] for x in range(len(ver_proj)): if ver_proj[x] 0 and (x 0 or ver_proj[x-1] 0): split_points.append(x) print(f文本行: {line_text}, 垂直投影切分点数量: {len(split_points)}) return split_points, bin_img第三步把切分结果用cv2.imwrite保存下来拼成一张对比图人工检查切分是否合理。对于检查中发现的问题比如“明”被切成“日”和“月”记录下来调整合并规则后重新切。这条流程的关键不是代码本身而是“识别置信度”这个信号。我用PaddleOCR的置信度作为触发器只有低置信度样本才走显式分割检查。这样可以避免对全部图片做冗余处理节省算力又能针对性地发现分割问题。4.3 文本行图像质量对分割结果的直接影响再往下走一步你会发现很多分割问题根本不是算法问题而是输入图像质量太差了。这里我分享几个提升文本行图像质量的实用技巧。第一个是倾斜校正。票据图像经常有轻微旋转导致文本行不是水平的。你可以用检测框的坐标计算文本行的倾斜角度然后用仿射变换校正。更简单的方法是先用cv2.minAreaRect拿到文本行图像的最小外接矩形根据矩形的角度信息做旋转。第二个是去阴影。前面提到的文档阴影问题可以通过顶帽变换TopHat配合形态学操作解决。顶帽变换能提取出比背景更亮的细节对白底黑字的文档来说可以有效消除大面积背景阴影让二值化结果更干净。第三个是插值方式。裁切文本行图像做缩放时用cv2.INTER_AREA插值比默认的线性插值更适合文字场景边缘锯齿少识别和分割的输入质量都会好一些。这些细节每一单独拿出来都不起眼但叠加在一起对分割和识别效果的影响非常大。4.4 数据增强与模型微调要点如果你的项目对中文OCR的效果有进一步要求微调开源模型通常是提升效果的关键一条路。这里只讲和分割相关的最重要的几点经验。第一图像高度要统一。CRNN结构的输入图像高度是固定的开源模型一般用32或48像素。我微调时会把所有训练文本行缩放成固定高度宽度等比缩放但设一个上限比如320像素超过上限的截断处理。这里要特别注意缩放时要保持长宽比否则中文字符会变形标签和图像的对齐关系也会出问题。第二字符集要固定。中文OCR的字典文件决定了模型能识别哪些字。微调时不要频繁增删字典否则CTC输出的概率分布维度会变之前的预训练权重基本就废了需要重新训练。要增字就一次性把可能用到的字全部加进去。第三标签编码用空格还是用字典索引。PaddleOCR的label文件里默认是字典值的索引序列字符之间用空格分隔。我在微调时踩过坑如果字典里本身有空格字符或者标签里误加了空格模型的对齐就会错乱训练损失降不下去。所以生成标签文件后一定要随机抽几条检查索引映射是否正确。5. 分割效果验证与排错典型错误切分的案例分析5.1 症状与根因对照表做中文OCR字符分割排错最重要的是能根据症状快速定位根因。我整理了一张在实际项目中反复用到的对照表希望对大家有帮助错误现象可能原因解决思路文本行内有字符被切成两半左右结构字投影谷底不明显/阈值过严增加过分割合并规则基于平均字符宽度修正多个字符粘连无法切开图像分辨率低/笔画断裂后闭运算过度降低闭运算核大小改用连通域分析辅助分割标点符号被吞掉或切进汉字标点与汉字距离太近投影谷底被填平使用全角标点宽度规则单独处理标点区域字符边界包含明显笔画残缺二值化阈值偏高细笔画被去掉改用自适应阈值或调整OTSU偏移量整体切分偏移所有框都偏左/右字符压缩或拉伸后边界漂移检查缩放插值方式统一字符框左右调整规则竖排文本或倾斜文本切分全乱用的是水平投影没有旋转校正先做文本方向检测和倾斜校正再分割5.2 一次完整的排查链路从错误识别到定位分割问题我发现直接讲排查过程比干巴巴列规则更容易让人理解。去年我处理过一张银行回单截图PaddleOCR识别其中一行“转账金额”相关信息时置信度一直很低输出的文本还夹杂乱码。我做的第一件事是把原始文本行裁出来单独保存灰度图然后用matplotlib画出垂直投影曲线。看完曲线就发现这一行里既有“”符号又有数字还有汉字三种字符的宽度差异很大垂直投影的谷底分布极不均匀。单独用固定宽度判断字符边界完全不适用需要根据投影波谷的相对深度和局部均值来动态决定切分点。接着我做了连通域分析看到“”符号和阿拉伯数字之间存在明显的连通域交叠说明二值化之后符号和数字的笔画粘连在一起了。我进一步检查发现这个文本行在原始图片里是倾斜的检测框虽然框住了文本但没有做倾斜校正导致垂直投影的方向和字符排列方向不一致投影曲线噪声极大。定位到这一步问题就清楚了不是我识别模型不行而是检测框内的图像本身是倾斜的直接用垂直投影做字符切分天然会出错。最终的修正是先根据检测框四点坐标做透视校正把文本行转正然后再切分置信度立刻从0.4提到了0.98。这个案例让我印象很深因为它说明了一个常见误区分割出错时不要只盯着分割算法本身先检查上游的检测框、图像方向和预处理是不是有偏差。5.3 可视化调试工具的重要性做中文字符图片分割千万不要“黑盒操作”。我见过太多同事直接打印一两个数字就下结论其实中间过程一片模糊。我习惯在分割流程里加一个“中间结果保存”环节二值化结果、投影曲线、候选字符框覆在原图上的叠加图全部保存到调试目录。画投影曲线用matplotlib最方便比如import matplotlib.pyplot as plt def plot_projection(ver_proj, save_path): plt.figure(figsize(12, 4)) plt.plot(ver_proj, colorblue) plt.xlabel(column index) plt.ylabel(foreground pixel count) plt.title(Vertical Projection Curve) plt.savefig(save_path, dpi150, bbox_inchestight) plt.close()字符框叠加图可以直接用cv2.rectangle在二值图或灰度图上画框尤其要注意同时保存“切分前”和“合并后”两个版本不然你不知道中间改了什么规则导致结果变化。除了肉眼观察我还会算几个客观指标字符平均宽度、切分间隔方差、过分割率。切分间隔方差大说明切割点分布不稳定可能存在误切过分割率统计的是被切成两半的汉字数量可以通过和标准文本长度对比来估算。5.4 处理中文图片时值得长期坚持的几个习惯最后分享几条我踩过很多坑之后沉淀下来的经验谈不上高大上但都很实用。第一先修图像再谈分割。大部分中文字符图片的分割失败根源是图像质量问题而不是分割算法不够先进。先确保光照均匀、文本方向水平、分辨率足够再考虑算法选型。第二选型从简到繁。先试投影法这套开源社区用得最成熟的方法不够用再上连通域分析再不够才考虑深度学习分割。直接上最复杂的方案不仅成本高排查问题也更困难。第三保留每次实验的中间结果和参数配置。分割算法里阈值、核大小、合并规则这些参数非常敏感同一个参数换一张图可能结果天差地别。我会给每次实验记录一份参数文件附在输出目录里方便复现和回滚。第四定期人工检查识别结果不只是看准确率数字。准确率可能会掩盖系统性的字符分割错误比如某类汉字总是被切成两半但整体准确率看起来还能接受。抽出一些典型图片看实际效果比盯着一个平均指标管用。做中文OCR这几年我越来越觉得字符分割是一项“脏活累活”没有太多花哨的技术但正是这些基础细节决定了项目的最终效果。希望这篇关于中文字符图片分割方法的分享能帮在做OCR的你减少一点反复试错的时间。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻