资讯动态

Halcon OCR产线识别实战:字符分割、分类器选型与置信度调参

发布时间:2026/10/26 3:52:08 来源:尧图企业网站定制
产线上跑 OCR十次里有八次不是算法不行而是图没给对。我见过太多人把 Halcon 的光学字符识别例程当黑盒用读一张图、调一个do_ocr_multi_class_mlp识别率上不去就回头怀疑分类器笨其实问题往往卡在前面那道分割上字符压根没被干净地切出来。Halcon 这套 OCR 体系的思路很朴素——先把字符从背景里剥成一个个规整的连通域再把这个区域连同灰度一起喂给分类器判类别两段式分工明确谁出问题一目了然。这篇内容我想把例程里那些看起来一笔带过、实际藏着门道的地方全摊开讲分类器怎么选、字符高度怎么反推、训练文件怎么攒、粘连字符怎么拆、置信度低于多少该判废。适合刚上手 HDevelop 的视觉工程师照着抄也适合已经能跑通例程、但一换产品就翻车的朋友对着排查。1. 为什么 OCR 例程不能拿来就用1.1 OCR 在视觉检测里到底卡在哪一环很多人对 OCR 的预期是拍一张、出字符串这个预期在实验室干净白纸上成立到了产线立刻崩。因为产线上的图有反光、有油污、有丝印偏移、有字符压到金属拉丝纹理上还有最常见的一种——相机装反了极性浅底深字的图直接喂进去分类器照样输出一堆字符但全是错的而且错得很有规律你会以为是字体不匹配。把 OCR 的失败拆开看无非三类一是成像不够字符高度不到 15 像素、笔画宽度只有 1 个像素信息量根本不够分类器判断二是分割不准字符粘连成一坨、或者被阈值切断了笔画、或者背景噪点混进来冒充字符三是分类器不匹配用点阵喷码的分类器去认激光雕刻的斜体字怎么调都是白费。这三类里第三类最容易查第一类最难改要动硬件第二类最考验功力。搞清这个优先级你排查的时候心里就有底了。1.2 Halcon 把 OCR 拆成分割 分类两段式的道理Halcon 没有走端到端的黑盒路线而是把流程硬切成了两半前半段用经典算子把字符区域抠出来后半段交给 MLP、SVM 或 CNN 判类别。这个设计看着不先进但在工业场景里非常实用因为每一段都可以单独验证。分割这一段的输出是一组 Region你完全可以把它们可视化出来一眼看出哪几个字符连在一起了、哪个被切多了、哪个方向不对。分类这一段的输入是Region 灰度图输出是类别和置信度你也可以单独拿已知答案的样本来测算出混淆矩阵。两段各自有独立的评价指标调参时就不会出现改了预处理反而更差、但不知道差在哪的尴尬。对比端到端的深度学习 OCR两段式的短板是抗形变能力弱、换字体要重训优势是样本需求小、推理快、可解释性强、出问题时能定位到具体哪个字符哪个环节。产线上绝大多数印刷体、喷码、钢印、标签场景两段式完全够用而且部署成本低得多。1.3 例程的真正用法当成调参的脚手架HDevelop 自带的 OCR 例程有十几二十个从最简单的ocr_number到带 Text Finder 的多行文本定位覆盖了不同难度梯度。新手常见的错误是拿一个例程改改图路径就丢到项目里然后开始抱怨例程不实用。例程的真正价值在于它展示了一条完整的、参数取值的合理区间字符高度一般在多少到多少之间、select_shape的宽度范围怎么给、分类器输入尺寸给 8×10 还是 10×12、置信度阈值卡在 0.3 还是 0.5。这些数值不是随便写的是大量样本试出来的经验值。你要做的是把这些锚点记住然后针对自己的图重新算一遍。我自己的习惯是先跑通例程确认环境没问题再把图换成自己的看在哪一步断掉断在哪就知道该补哪块的功课。这个顺序比一上来就抄代码高效得多。2. 例程里的分类器怎么选、怎么读、怎么校验2.1 三种分类器的脾气摸清楚Halcon 提供三类 OCR 分类器各自适用面差别很大选错了后面怎么调都是事倍功半。MLP多层感知机是最常见的一类训练快、样本需求低每类字符三五十张就能出个能用的模型。它对输入归一化极其敏感因为内部会把字符缩放成固定尺寸常见 8×10 或 10×12再提取灰度、投影、孔洞等特征。印刷体、喷码、点阵字用它最稳缺点是遇到形变、倾斜、噪声多的图就吃力。SVM在小样本、字符集小比如只有 0-9 加几个字母的场景下泛化能力往往比 MLP 好一点训练慢一些模型文件也大。它对特征尺度不像 MLP 那么敏感我一般把它当 MLP 的备选两边都训一遍看验证集谁高用谁。CNN是这几年用得越来越多的路线自带特征提取抗形变、抗噪声、抗多字体混排能力强代价是样本量需求高一个数量级每类几百张起步训练时间长而且训练走的是深度学习模块那一套流程跟 MLP 的训法完全不同。自然场景文字、低质量钢印、多字体混排值得上 CNN规整的印刷标签用 CNN 是杀鸡用牛刀。分类器每类样本量训练耗时抗形变典型场景MLP30~100秒级到分钟级弱印刷体、喷码、点阵SVM30~80分钟级中小字符集、样本稀缺CNN300小时级强自然场景、钢印、多字体2.2 预训练字体文件的命名规则藏着关键信息Halcon 安装目录下的ocr文件夹里躺着一堆.occ文件名字不是乱起的拆开看是字体_字符集_拒识策略三段。比如带Industrial的是工业印刷体带Document的是文档扫描体带DotPrint的是点阵喷码体中间的0-9A-Z说明它只认数字和大写字母你拿小写字母去喂它不会报错只会给你一个莫名其妙的结果最后一段NoRej表示不拒识不管多离谱都强行给一个答案Rej表示允许拒识置信度低于内部阈值时返回rej。这个命名规则直接影响你的选型。做药品监管码识别字符集是纯数字就用纯数字的分类器不要用带字母的因为字母分类器在判数字时把0和O搞混的概率明显更高。做标签识别如果字符集里同时有数字和大写字母那O和0、I和1、S和5、B和8这几组就是天然的雷区必须在后处理里加规则校验——比如产品编号的第 3 位一定是数字那就用正则把它锁死。实际可用的文件名以本地安装目录为准不同版本会有增减别照着别人的清单硬套。挑的时候按字符集匹配 字体匹配 拒识策略的顺序筛。2.3 置信度不是万能钥匙但能救急do_ocr_multi_class_mlp的输出除了类别还有每个字符的置信度。这个值反映的是分类器对自己判断的把握程度范围大致 0 到 1。很多人拿到置信度就设个 0.5 一刀切低于就判 NG这做法太粗。我的经验值是同一批样本里正确识别的置信度通常集中在 0.6 以上错识的往往掉到 0.2 到 0.4 之间中间 0.4 到 0.6 这一段是灰区需要拿实际样本统计一下分布再定阈值。更靠谱的做法是画个直方图看正确样本和错误样本的置信度分布有没有明显分界有就取分界点没有就说明这批图质量本身有问题先回头改成像。还有个细节NoRej分类器不会返回rej所有字符都会给一个类别。如果你需要识不出就报警的能力要么换Rej版本要么自己在后处理里用置信度加规则双重把关。产线上我一般倾向后者因为规则可以把这个位置不该出现这个字符这种业务逻辑加进去比单纯看置信度准得多。3. 从读图到出结果的完整链路拆解3.1 预处理极性、对比度、ROI 三件事预处理阶段最容易翻车的是极性。Halcon 的 OCR 分类器在训练时样本统一是亮字符、暗背景也就是字符像素灰度高于背景。如果你的原图是浅底深字白纸黑字直接喂进去分类器拿到的是一堆反向的灰度特征识别率会掉得非常离谱而且掉得很隐蔽——它不报错只是错。处理办法有两个一是分割前用invert_image把整图反相二是通过threshold的方向控制让分割出来的 Region 落在字符笔画上然后配合reduce_domain取字符区域的灰度。两条路都行前者简单粗暴后者更干净。我一般用前者因为反相之后后面所有步骤的极性假设都统一了不容易出错。对比度这块工业场景很少需要做直方图均衡因为均衡会把噪声一起放大。更实用的做法是局部对比度增强或者干脆靠打光解决。真要处理用emphasize或scale_image做个轻度拉伸就够别过度。ROI是最省事也最有效的一步。字符在画面里的位置通常是固定的用gen_rectangle1框一个区域reduce_domain限死能直接屏蔽掉 90% 的背景干扰。这一步做完后面的阈值范围就好给多了。ROI 不要框得太紧四周留出 5 到 10 个像素余量防止产品位置有轻微漂移时把字符切掉。3.2 字符分割阈值、形态学、投影三条路怎么选分割是整个流程里最需要动手试的环节没有万能参数。三条主流路线各有适用面。全局阈值最简单threshold(Image, Region, MinGray, MaxGray)一行搞定适合背景均匀、字符与背景对比度稳定的场景比如白底黑字的标签。给参数的时候不要凭感觉用gray_histo看一眼直方图找双峰之间的谷底那就是阈值的位置。如果直方图只有一个峰说明对比度不够先回去改打光。局部阈值适合光照不均的场景比如金属表面有高光的字符。local_threshold或dyn_threshold用局部均值做参考能压掉缓慢变化的背景。参数里的MaskSize要给得比字符笔画宽度大一些比如笔画宽 5 像素MaskSize给 15 到 25太小会把笔画内部也判成背景。形态学 投影用于处理粘连。字符粘连是 OCR 的头号杀手两个字符粘一起分割出来就是一个宽得离谱的 Region分类器只能给一个答案。常规处理是先用opening_rectangle1做一次开运算把细连接处切断再用connection分离如果开运算把笔画也切断了就改用垂直投影——统计每一列的字符像素数投影值接近 0 的位置就是字符间隙在那些位置做垂直切割。投影法对等宽字体特别有效对比例字体要配合字符宽度统计做自适应。Halcon 里还有个segment_characters算子把预处理和分割打包成一步参数里能指定方法、对比度、过渡宽度等。它省事但参数之间互相牵制调起来反而不如自己搭流水线看得清楚。我一般只在快速验证时用它正式项目还是手写分割链。分割完记得用select_shape过滤一遍按height和width卡范围把噪点、边框碎片、标签线全部筛掉。这一步的参数要结合你算出来的字符像素尺寸来给不要照抄例程。3.3 排序与倾斜校正分割出来的 Regionconnection之后的顺序是按连通域扫描顺序排的跟阅读顺序对不上。必须用sort_region重新排。单行文本用sort_region(Regions, Sorted, character, true, row)character模式会按字符从左到右排比first_point更贴合阅读顺序。多行文本要先按行分组再在行内排序用row做第一级、column做第二级。倾斜是另一个隐形杀手。轻微倾斜1 到 2 度对 MLP 影响不大超过 5 度识别率就开始明显下降。校正用text_line_orientation输入字符区域的联合体和原图它会返回文本行的倾角弧度然后rotate_image反向旋转。这个算子的原理是统计文本行外接矩形的主轴方向对整行文本效果很好对单个字符无效。如果没有text_line_orientation老版本可以用smallest_rectangle2求整行区域的最小外接矩形取Phi角度再rotate_image。精度差一点但够用。旋转之后要重新做一次分割因为旋转会引入插值边缘会变糊。3.4 识别与结果拼装分割、排序、校正都做完才轮到do_ocr_multi_class_mlp。调用形式是传入排好序的 Region 数组、对应的灰度图、分类器句柄输出类别数组和置信度数组。注意Region 数组和灰度图必须尺寸一致如果前面做过reduce_domain或裁剪图也要跟着裁否则会报尺寸不匹配。结果拼装这一步看着简单实际有不少讲究。类别数组是逐个字符给的直接拼成字符串就行但要注意空字符和拒识标记的处理。Rej分类器返回rej的时候是保留占位符还是直接判整串无效取决于业务需求。监管码这类场景一个字符识不出就该整串判废而有些场景只要能读出大部分就行那就用占位符标记。再往后就是规则校验把业务逻辑加进去长度对不对、校验位算不算得通、这个位置该不该出现这个字符、和数据库里的编码能不能对上。这一层能挡掉大量分类器层面的错识是产线 OCR 真正稳定运行的关键。我见过不少项目分类器识别率只有 92%加上规则校验之后整串通过率能到 99.5% 以上。4. 手把手参数怎么算代码怎么写4.1 成像参数倒推字符高度与像素密度调参之前先算账这一步跳过后面全是瞎调。核心公式就一个字符像素高度 字符实际高度 ÷ 单像素物理尺寸而单像素物理尺寸 视野宽度 ÷ 相机横向分辨率。举个例子。视野 80 mm 宽相机 2448 像素单像素 80 ÷ 2448 ≈ 0.0327 mm也就是 32.7 微米。产品上的字符高度 1.5 mm那像素高度 1.5 ÷ 0.0327 ≈ 46 像素。这个值落在 30 到 60 的理想区间可以直接干。如果字符高度只有 0.8 mm像素高度 ≈ 24勉强能用但笔画宽度会成为瓶颈。假设笔画宽 0.15 mm换算过来只有 4.6 像素边缘一模糊就只剩 2 到 3 个有效像素分类器很难分辨8和B。这时候要么换更长焦的镜头缩小视野要么换更高分辨率的相机光靠调参救不回来。经验阈值记牢字符高度不低于 20 像素最好 30 到 60笔画宽度不低于 2 像素最好 3 以上字符间隙不低于 1 像素。低于这些线先把硬件搞定再谈算法。另外字符高度在整幅图里要尽量一致如果同一批产品有的 20 像素有的 60 像素分类器的归一化会把小的那个放大到糊掉识别率必然参差。4.2 核心代码骨架逐段注释下面这段是单行印刷体 OCR 的通用骨架把变量换成自己的就能跑。注释里写的是为什么这么给不是这行做了什么。* ---------- 1. 读图与灰度化 ---------- read_image (Image, print_sample) get_image_size (Image, Width, Height) rgb1_to_gray (Image, GrayImage) * ---------- 2. 锁定 ROI ---------- * 字符位置固定先框死屏蔽背景干扰 gen_rectangle1 (ROI, 180, 120, 320, 1680) reduce_domain (GrayImage, ROI, ImageOCR) * ---------- 3. 极性统一 ---------- * 分类器按亮字暗底训练浅底深字必须先反相 invert_image (ImageOCR, ImageOCRInv) * ---------- 4. 字符分割 ---------- * 阈值取直方图双峰谷底这里假设谷底在 110 附近 threshold (ImageOCRInv, RegionRaw, 110, 255) connection (RegionRaw, ConnectedRegions) * 按算出来的字符尺寸卡范围高 30~70宽 8~45 select_shape (ConnectedRegions, Chars, height, and, 30, 70) select_shape (Chars, Chars2, width, and, 8, 45) * ---------- 5. 排序 ---------- * character 模式比 first_point 更贴合阅读顺序 sort_region (Chars2, SortedChars, character, true, row) * ---------- 6. 倾斜校正可选 ---------- * 整行联合求倾角超过 3 度才转避免插值损失 union1 (SortedChars, LineRegion) text_line_orientation (LineRegion, ImageOCRInv, 30, -0.35, 0.35, Orientation) if (abs(Orientation) 0.05) rotate_image (ImageOCRInv, ImageRotated, -deg(Orientation), constant) * 旋转后必须重做分割这里省略重复步骤 endif * ---------- 7. 识别 ---------- read_ocr_class_mlp (Industrial_0-9A-Z_NoRej.occ, OCRHandle) do_ocr_multi_class_mlp (SortedChars, ImageOCRInv, OCRHandle, Class, Confidence) * ---------- 8. 结果拼装与过滤 ---------- * 置信度低于 0.4 的标出来人工复核 tuple_length (Class, Count) ResultString : for Index : 0 to Count - 1 by 1 if (Confidence[Index] 0.4) ResultString : ResultString ? else ResultString : ResultString Class[Index] endif endfor clear_ocr_class_mlp (OCRHandle)几个容易踩的坑标一下。do_ocr_multi_class_mlp的第二个参数传的是灰度图不是二值图二值图喂进去特征全丢了识别率会莫名其妙地低。select_shape的height范围一定要按你自己算出来的像素值给例程里的 20 到 100 是通用值卡太松会让噪点混进来。sort_region的排序方向要根据文本走向定横向文本用row竖向文本要改成column。4.3 自训练一个字体库的完整动作预训练分类器认不了你的字体就得自己训。整个流程分五步每一步都有讲究。第一步采样。每类字符至少 30 张理想 50 到 100 张要覆盖实际产线的各种变化——不同批次、不同光照、不同位置、轻微污损。只采一张图切出来的样本训出来的模型过拟合严重换个工位就废。采样时建议分布在不同时间段把设备漂移也覆盖进去。第二步标注。用 HDevelop 的 OCR 助手最省事界面上框选字符、输入对应类别它会自动把样本追加到训练文件。手写也行用append_ocr_trainf把字符区域、原图、类别逐个写进.trf文件。注意训练文件是二进制格式不要试图用文本编辑器打开修改改坏了整个文件都得重来。第三步建模型。create_ocr_class_mlp的关键参数是字符归一化尺寸和隐层节点数。归一化尺寸给 8×10 是通用值字符笔画细的可以提到 10×12再大意义不大反而拖慢训练。隐层节点数给 10 到 20 之间样本多可以加样本少必须减否则过拟合。字符集参数要严格按你的实际字符数给多给了会拖慢收敛少给了直接报错。第四步训练与验证。trainf_ocr_class_mlp会返回训练误差和误差日志误差降到 0.01 以下基本收敛但训练误差低不代表泛化好。必须留一批没参与训练的样本做验证逐个字符比对真值算出每类的识别率和混淆情况。0和O混淆、1和I混淆是高频问题如果混淆严重说明样本里这两类的形态差异没被充分覆盖得补样本或者在后处理里加规则。第五步导出与回归。write_ocr_class_mlp存成.omc文件项目里用read_ocr_class_mlp加载。上线前拿一批历史数据跑回归对比新旧模型的表现确认没有退化再替换。这一步别省我见过训练集涨了 2 个点、实际产线掉了 5 个点的情况。5. 踩坑实录与排查速查5.1 分割阶段字符切不干净的所有可能字符粘连是最常见的。表现是识别结果里少字符比如A1234读成A123因为两个字符粘成一个 Region分类器只给了一个答案。排查方法很简单把SortedChars可视化出来数一下 Region 个数和实际字符数对不对得上。对不上就是粘连。处理按严重程度分三档轻度粘连用opening_rectangle1开运算切断宽度给 1 到 2 像素中度粘连用垂直投影找间隙切重度粘连字符重叠基本没救只能改打光或者换字体。字符断裂是反过来的问题一个字符被切成两半识别结果多字符。常见于笔画细、阈值给高了的场景。处理办法是closing_rectangle1做闭运算把断口接上或者把阈值下限调低。但阈值调低会引入背景噪点所以要配合select_shape一起调。噪点混入表现为结果里偶尔冒出莫名字符。根源是背景有污点、有标签边框、有反光斑点尺寸恰好落在select_shape的范围里。处理是收紧select_shape的宽高范围或者加select_shape的area和compactness条件。更彻底的办法是在 ROI 里再框一层内缩的区域把边框直接排除在外。Region 数量对但顺序乱这个最隐蔽识别率看起来正常但拼出来的字符串是乱序的。根源是sort_region的模式选错或者多行文本没做行分组。单行用character多行要先按row分行再按column排。5.2 识别阶段置信度正常但结果错极性反了是最典型的看起来一切正常但全错。分割出来的 Region 位置对、数量对、顺序对置信度也不低就是字符全错而且错得有规律——0认成O、1认成I这种。原因是分类器拿到的灰度特征整个反了。检查方法把 Region 叠加到原图上看字符像素是亮的还是暗的。暗的就invert_image。字符高度超出训练范围也会导致系统性错识。分类器内部会把字符缩放到固定尺寸如果你的字符实际是 15 像素高缩放后笔画糊成一团如果是 120 像素高缩放后细节全丢。两种情况识别率都会掉。检查方法select_shape之后统计height的分布看中位数落在哪跟分类器的归一化尺寸对比。易混字符是分类器层面的固有问题靠调参解决不了只能靠后处理。0/O、1/I/l、5/S、8/B、2/Z、6/G这几组是公认的雷区。处理办法是加规则如果业务上这个位置只能是数字就把所有字母候选强制映射成数字如果有校验位用校验位反推纠错如果编码有固定格式用正则表达式约束。这三招组合起来能救回绝大部分错识。置信度分布异常是个信号。正常情况下正确识别的置信度应该在 0.6 以上如果整批样本的置信度普遍在 0.3 到 0.5 之间说明图质量有问题或者分类器跟字体严重不匹配。这时候先别调阈值回头查成像和分类器选型。5.3 常见问题速查表现象大概率原因处理动作识别结果少字符字符粘连开运算切断、投影切割、改打光识别结果多字符字符断裂或噪点混入闭运算、收紧 select_shape、内缩 ROI结果全错但置信度高灰度极性反了invert_image 反相结果顺序乱sort_region 模式错单行用 character多行先分行0/O、1/I 混淆字体本身易混规则约束 校验位纠错置信度普遍偏低字符太小或极性错查像素高度查反相换个工位就崩训练样本覆盖不足补多批次样本重训倾斜文本识别差未做校正text_line_orientation rotate_image中文识别乱码用了拉丁字符集分类器换 CJK 分类器或自训练训练集好产线差过拟合减隐层节点、加样本多样性5.4 工程化落地让例程变成能跑三年的工具例程跑通只是起点真正上线还要补几块。参数外置是第一条把所有阈值、尺寸范围、分类器文件名写进配置文件别硬编码在脚本里否则换个产品就要改代码重编译。日志留痕是第二条每张图的识别结果、置信度、耗时都要落盘出问题时能回溯是图的问题还是参数的问题。模型版本管理是第三条每次重训的模型文件、训练样本、验证结果都要归档产线出问题时能快速定位到是哪次更新引入的。还有一条容易被忽略定期回归。产线的光源会衰减、镜头会积灰、产品批次会换供应商这些变化是缓慢累积的等到识别率掉到报警线才处理就晚了。我的做法是每周拿一批固定样本跑一次回归记录识别率曲线一旦连续三次下降就介入排查。这套机制不复杂但能把大部分问题挡在爆发之前。最后分享一个实操小技巧。调试分割参数时别一个字符一个字符地看把SortedChars直接dev_display出来叠在原图上用不同颜色区分 Region 和背景一眼就能看出是粘了、断了、还是多了。把Confidence也打印在旁边哪个字符把握低一目了然。这个可视化习惯能省掉大量试错时间比盯着识别结果猜原因高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑