在视觉项目中,字符分割往往是 OCR 流程里最脆弱的环节。最近我就遇到了一个典型案例:Halcon 原脚本的分割功能能正常工作,移植到 .NET 程序后却找不到有效区域,导致后续识别完全无法进行。最终通过逐层定位问题、对比 Halcon 脚本、并引入更稳健的 find_text 算子,彻底解决了困扰。本文把整个过程和思考记录下来,希望能帮助遇到类似问题的开发者。

问题初现:筛选后的矩形区域数量为0

程序的原始逻辑是这样的:

  1. 用户手动绘制一个矩形 ROI 框住目标文本。
  2. 对图像进行反转、裁取 ROI,然后 Otsu 二值化。
  3. 连通域分割 → 矩形结构开运算 → 二次连通域 → 形状转换(最小外接矩形)。
  4. SelectShape 按宽度 [35,60]、高度 [60,100] 筛选字符区域。

就是最后这一步,每次执行后得到的 selectedRegions 对象数量都是 0。

逐层排查:哪一步开始丢失了区域?

为了定位,在每一步处理后都加入了 CountObj 并弹窗显示区域数量。结果显示:

  • 二值化后:存在大量区域(数量 > 0)
  • 连通域后:区域数量正常
  • 开运算后:区域数量略微减少,但仍有多个
  • 形状转换矩形后:区域数量依然 > 0
  • 筛选后:突然变成 0

问题锁定在 SelectShape 这里。显然,没有任何一个矩形的尺寸落入了设定的宽高范围。于是我在 SelectShape 前打印了所有矩形的实际尺寸,得到的数据让人大跌眼镜:

区域0: 宽=141.0, 高=8.0
区域1: 宽=126.0, 高=16.0
区域2: 宽=152.0, 高=9.0
区域3: 宽=126.0, 高=25.0
区域4: 宽=8.0,  高=0.0
区域5: 宽=43.0, 高=0.0

高度普遍极低(甚至为零),宽度却异常的大(120~150像素)。这些哪里是字符,分明是一条条细长的噪声或背景粘连。说明前期的二值化+开运算并没有把真正的字符分离出来,而是生成了一堆横向的碎片。

对比 Halcon 原始脚本

同样的参数,在 Halcon 中运行却是成功的。

read_image (Image, 'C:/Users/chao.tang/Desktop/OCR/1.bmp')

* rgb1_to_gray(Image, Image)
* 旋转图像
rotate_image(Image, Image, 180, 'constant')

* 1. 仿射变换
get_image_size(Image,Width, Height)
gen_rectangle1 (ROI_0, 724.733, 1143.99, 850.022, 1515.02)



text_line_orientation(ROI_0, Image, 75, -0.4, 0.4, OrientationAngle)
vector_angle_to_rigid(Width/2, Height/2, OrientationAngle, Width/2, Height/2, 0, HomMat2D)
affine_trans_image(Image, ImageAffineTrans, HomMat2D, 'bilinear', 'false')

* 2. 分割区域
* 图像反转
invert_image(ImageAffineTrans,ImageAffineTrans)

reduce_domain(ImageAffineTrans,ROI_0,ImageReduced)

binary_threshold(ImageReduced, Region, 'max_separability', 'dark', UsedThreshold)
connection(Region, ConnectedRegions)
opening_rectangle1(ConnectedRegions, RegionOpening, 4, 4)

connection(ConnectedRegions,ConnectedRegions)

* 将区域转成矩形
shape_trans(ConnectedRegions, RegionTrans, 'rectangle1')
count_obj(RegionTrans, Number)

select_shape (RegionTrans, SelectedRegions, ['width','height'], 'and', [35,60], [60,100])
count_obj(SelectedRegions, Number)

*分割区域
* partition_rectangle(SelectedRegions, Partitioned, 70, 90)

* 3. 读取omc文件并应用
read_ocr_class_mlp('Industrial_0-9A-Z_NoRej.omc', OCRHandle)
* 字符排序
sort_region(SelectedRegions, SortedRegions, 'character', 'true', 'row')
do_ocr_multi_class_mlp(SortedRegions, ImageAffineTrans, OCRHandle, Class, Confidence)

换个思路:用文本模型取代手动分割

手动分割虽然灵活,但参数(阈值、结构元尺寸、宽高范围)对环境极度敏感。一旦图像稍有变化,就得重新调整。既然我们已经知道字符的外观特征(宽约51,高约98,笔画宽约9.7像素),为什么不直接用 Halcon 自带的 find_text 算子来完成定位?它内部已经集成了可靠的文本分割算法,只需设定参数就能自动找出字符区域,省去繁琐的手动流程。

于是我顺着这条思路重构了分割代码,核心步骤如下:

  1. 创建文本模型:使用 create_text_model_reader 并传入字符形态参数(极性、宽高、笔画宽度等)。
  2. 图像预处理:仅保留 180° 旋转(如有必要,也可是恒等变换裁剪)。
  3. 使用 find_text 定位字符:直接返回所有字符连通域,无需二值化、开运算、形状转换。
  4. 反转图像用于 OCR:因为分类器是在暗背景亮字符上训练的,而原图是亮背景暗字符。
  5. 排序并识别:与原来一致。

C# 代码实现

下面是整合后的 btnOCR_Click 核心逻辑(文本模型全局初始化一次即可):

// 初始化文本模型(在窗体加载或单独按钮中调用一次)
private void InitializeTextModel()
{
    HOperatorSet.CreateTextModelReader("manual", new HTuple(), out textModelHandle);
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_polarity", "light_on_dark");        // 暗底亮字
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_char_width", 51);                   // 字符宽度
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_char_height", 98);                  // 字符高度
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_stroke_width", 9.7);                // 字符笔画宽度
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_return_punctuation", "false");      // 不返回标点符号
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_return_separators", "false");       // 不返回分隔符
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_uppercase_only", "true");           // 仅识别大写字母
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_fragment_size_min", 47);            // 最小字符碎片尺寸
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_eliminate_border_blobs", "true");   // 消除边界上的小块
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_base_line_tolerance", 0.2);         // 基线容差,剔除错位字符
    HOperatorSet.SetTextModelParam(textModelHandle, "manual_max_line_num", 1);                  // 识别行数限制为1行
}

OCR识别

if (ho_Image == null || ocrHandle == null || ocrHandle.Length == 0 || drawing_objects.Count == 0)
{
    MessageBox.Show("请先加载图像、OCR模型并绘制ROI!");
    return;
}

// 1. 旋转 180° 在外面先旋转,再进行OCR识别
//HOperatorSet.RotateImage(ho_Image, out ho_Image, 180, "constant");

// 2. 获取用户绘制的 ROI
HObject roi = GetDrawingObjectRegion(drawing_objects.Last());
if (roi == null) return;
//HOperatorSet.GetImageSize(ho_Image, out HTuple width, out HTuple height);
//HOperatorSet.TransposeRegion(roi, out roi,
//    width / 2, height / 2);

//_mainWindow.ClearWindow();
//_mainWindow.DispObj(ho_Image);
//_mainWindow.DispObj(roi);

// 3. 提取单通道(若为彩色图像)
HOperatorSet.CountChannels(ho_Image, out HTuple channels);
HObject imageMono;
if (channels.I == 3)
    HOperatorSet.AccessChannel(ho_Image, out imageMono, 1);
else
    imageMono = ho_Image.CopyObj(1, -1);

// 4. 缩小定义域到 ROI
HOperatorSet.ReduceDomain(imageMono, roi, out HObject imageReduced);

// 5. 边界扩展(模拟 Halcon 的恒等变换裁剪步骤)
// 膨胀 ROI 区域 48 像素,得到矩形边界,然后 crop_part
HOperatorSet.GetDomain(imageReduced, out HObject domain);
HOperatorSet.DilationCircle(domain, out HObject domainExpanded, 48);
HOperatorSet.SmallestRectangle1(domainExpanded, out HTuple r1, out HTuple c1, out HTuple r2, out HTuple c2);
HOperatorSet.CropPart(imageReduced, out HObject imageCropped, r1, c1, c2 - c1 + 1, r2 - r1 + 1);

// 6. find_text 分割
HOperatorSet.FindText(imageCropped, textModelHandle, out HTuple resultHandle);
HOperatorSet.GetTextObject(out HObject symbols, resultHandle, "manual_all_lines");

// 7. 反转图像(暗底亮字,用于 OCR)
HOperatorSet.InvertImage(imageCropped, out HObject imageInverted);

// 8. 字符排序
HOperatorSet.SortRegion(symbols, out HObject sortedRegions, "character", "true", "row");

// 9. OCR 识别
HOperatorSet.DoOcrMultiClassMlp(sortedRegions, imageInverted, ocrHandle,
                                out HTuple classResult, out HTuple confidence);


// 11. 拼接字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < classResult.Length; i++)
    sb.Append(classResult[i].S);
string text = sb.ToString();

if (confidence.Length > 0)
{
    double avgConf = confidence.TupleMean().D;

    lblResult.Text = $"结果: {text}\n平均置信度: {avgConf:F4}";
    MessageBox.Show($"识别结果: {text}\n平均置信度: {avgConf:F4}");
}
else
{
    lblResult.Text = $"结果: ";
    MessageBox.Show($"识别识失败!");
}

效果与总结

新方案运行后,无需繁琐的尺寸筛选,find_text 准确返回了所有字符区域,OCR 识别率完全达到 Halcon 脚本的水平。此次调坑有几点经验值得记下:

  • “0区域”的根源往往是预处理不到位
  • 逐层输出中间结果是最快的定位手段。如果一开始就在二值化后查看区域形状,可能更早发现问题。
  • Halcon 的 find_text 是处理规律文本的利器。当字符形态规律、符合预设模型时,用它替代手动分割可以大幅提升稳定性,减少调参时间。
  • C# 移植 Halcon 脚本时,务必逐行核对预处理步骤,尤其是图像变换顺序、ROI 坐标是否随图像变换而同步更新。

希望这次记录能为同样在 Halcon 二次开发中摸爬滚打的伙伴提供一点参考。调试视觉算法,耐心和对比验证永远是最可靠的方法。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐