HALCON字符串处理实战:从基础操作到正则表达式应用

1. 字符串处理:工业视觉中的"文字游戏"

在工业视觉项目里,我们打交道最多的往往是图像和坐标。但不知道你有没有遇到过这样的场景:相机拍到的产品标签需要读取、日志文件里混杂着关键的错误代码、或者是一堆杂乱无章的文件名需要按规则整理。这时候,处理图像之外,处理"文字"就成了刚需。HALCON作为强大的机器视觉库,它的字符串处理能力,就像是给视觉系统装上了一颗会"阅读理解"的大脑,能让你的程序变得更智能、更自动化。

我刚开始用HALCON时,也以为它只管"看",不管"读"。直到有一次,客户需要从流水线上抓取产品序列号,那个序列号是刻在金属上的,通过字符识别(OCR)读出来是一串像"SN-20240315-A1B2"这样的字符串。后续的流程需要把这串字符拆开,提取出日期"20240315"和批次代码"A1B2"。如果不会处理字符串,难道要再训练一个视觉模型去"看"日期吗?显然不现实。这时候,HALCON内置的字符串操作函数就成了救命稻草,它能让你像在高级编程语言里一样,轻松地切割、查找、替换和格式化文本。

所以,别小看HALCON里的字符串(String)类型。它虽然不像图像对象那样占据视觉中心,但却是串联起整个自动化流程、实现信息流转的关键纽带。从最简单的数字转文本,到用正则表达式实现复杂的模式匹配,掌握这套工具,能让你的视觉解决方案从"看得见"升级到"看得懂且会处理"。

2. 从数字到文本:格式化输出的艺术

把数字变成字符串,听起来很简单,但要想变得"好看"和"符合规范",就需要一点技巧了。这就像打印收据,金额"23"你可能想显示为"23.00元",并且要右对齐才整齐。HALCON的 tuple_string 操作符(或者它对应的赋值操作符 $)就是干这个的,它的核心在于一个叫做 格式字符串(Format String) 的东西。

这个格式字符串的规则,可以理解为给数字"化妆"的步骤说明。它主要包含四个部分:<flags><field width>.<precision><conversion character>。咱们拆开揉碎了说。

flags(标志) 决定了文本的"对齐方式"和"外观修饰"。比如:

  • - 标志:让结果在字段宽度内左对齐。这是最常用的,因为我们通常习惯从左开始阅读。
  • + 标志:正数前面也强制显示 + 号。在需要明确显示正负的工程数据里很管用。
  • 0 标志:用数字0而不是空格来填充宽度。比如想把数字5显示为三位数005,这个标志就派上用场了。

field width(字段宽度)precision(精度) 就好理解了。宽度控制最终字符串最少占几个字符位,精度控制小数点后保留几位,或者整数最少显示几位数字。

最关键的 conversion character(转换字符),它决定了数字被看成什么"类型"来转换:

  • d:当成有符号十进制整数。比如 255$'d' 得到 '255'
  • f:当成浮点数。这是最常用的,3.1415926$'.2f' 就能得到我们熟悉的 '3.14'
  • xX:当成无符号十六进制整数。255$'x' 得到 'ff'255$'X' 得到 'FF'。这在处理一些硬件寄存器地址或颜色值时特别有用。
  • s:这个其实是针对字符串本身的格式化,比如给字符串补空格对齐。

光说理论有点干,我举几个实际项目中常用的例子。假设我们检测到一个零件的长度是23.0毫米,要在报表里规范显示:

cpp 复制代码
// 显示为总宽度10位,保留2位小数,右对齐(默认):'     23.00'
LengthString := 23.0 $ '10.2f'

// 左对齐,更美观:'23.00     '
LengthString := 23.0 $ '-10.2f'

// 不关心总宽度,只保留2位小数:'23.00'
LengthString := 23.0 $ '.2f'

// 如果是十六进制的颜色值,比如从相机获取的某个状态码255:
// 输出为小写十六进制 'ff'
StatusHex := 255 $ 'x'
// 输出为大写十六进制,并带有0x前缀 '0xFF'
StatusHexWithPrefix := 255 $ '#X'

对于字符串本身,格式化同样重要。比如你要生成一个固定宽度的日志表头:

cpp 复制代码
// 将'Result'格式化为宽度15,右对齐的字符串 '         Result'
HeaderRight := 'Result' $ '15s'
// 左对齐 'Result         '
HeaderLeft := 'Result' $ '-15s'
// 只取前3个字符,并左对齐到宽度10 'tot       '
Abbreviation := 'total' $ '-10.3s'

这些操作看似基础,却是构建清晰、可读的检测报告、日志文件和数据库记录的基础。把数据打扮得整整齐齐,后续的存储、传输和查看才会省心。

2.1 元组与字符串的批量转换

单个数字的转换还好说,但HALCON里数据往往以元组(Tuple)形式存在,比如一组测量的直径 [10.2, 20.5, 30.1]。这时候就需要批量转换。tuple_string 可以直接处理整个元组,非常高效。

cpp 复制代码
// 将一组测量值全部格式化为保留1位小数的字符串
Measurements := [10.2, 20.5, 30.1]
StringTuple := Measurements $ '.1f'
// 结果 StringTuple 是 ['10.2', '20.5', '30.1']

反过来,我们也经常需要把字符串元组转回数字元组,比如从配置文件读入的阈值参数。这里要注意,转换前最好用 tuple_is_numberis_number 检查一下,避免因为非数字字符导致程序崩溃。

cpp 复制代码
// 假设从文件读取的参数是字符串元组 ['100', '255', 'invalid']
Params := ['100', '255', 'invalid']
// 检查每个元素是否为有效数字,得到布尔值元组 [1, 1, 0]
IsNum := is_number(Params)

// 安全地转换为数字元组,可以配合条件语句过滤无效值
if (sum(IsNum) == |Params|) // 如果全部是数字
    NumParams := number(Params) // 得到 [100, 255, 0],注意invalid会变成0
endif

字符和ASCII码的转换在特定场景下也很有用,比如处理通信协议或加密数据时:

cpp 复制代码
// 将单个字符转换为ASCII码
AsciiCode := ord('A') // 得到 65
// 将字符串每个字符转为ASCII码元组
CodeTuple := ords('HALCON') // 得到 [72, 65, 76, 67, 79, 78]
// 将ASCII码元组转回字符串
OriginalString := chr([72, 65, 76, 67, 79, 78]) // 得到 'HALCON'

3. 字符串的"外科手术":分割、截取与搜索

当字符串数据进来后,我们经常需要对其进行"解剖",提取出有用的部分。这就涉及到分割、截取和搜索这几项核心操作。

字符串分割 是最常用的操作之一。想象一下,你从扫码枪得到一个字符串 "MODEL=XC-100;DATE=20240315;STATUS=PASS",你需要提取出型号和日期。用 tuple_split 就能轻松搞定。

cpp 复制代码
DataString := 'MODEL=XC-100;DATE=20240315;STATUS=PASS'
// 首先按分号';'分割,得到三个字段
Fields := split(DataString, ';')
// Fields 为 ['MODEL=XC-100', 'DATE=20240315', 'STATUS=PASS']

// 然后对第一个字段按等号'='再次分割
ModelInfo := split(Fields[0], '=')
// ModelInfo 为 ['MODEL', 'XC-100'],这样型号'XC-100'就拿到了

split 函数会"吃掉"分隔符,返回分隔符之间的子字符串元组。它还可以一次用多个字符作为分隔符,功能很灵活。

字符串截取 用于提取固定位置的信息。比如你知道产品序列号的前6位是批次号,就可以用 str_first_nstr_last_n

cpp 复制代码
SerialNumber := '230415A1B2C3D4'
// 提取前6位批次号
BatchCode := str_first_n(SerialNumber, 6) // 得到 '230415'
// 提取后4位校验码
CheckCode := str_last_n(SerialNumber, 4) // 得到 'C3D4'

更强大的是,这些函数也支持元组操作,可以一次性处理多个字符串,并指定每个字符串不同的截取位置,这在批量处理文件名时效率极高。

字符串搜索 是定位信息的利器。HALCON提供了从前往后搜(strchr, strstr)和从后往前搜(strrchr, strrstr)两组函数。strchr 找单个字符,strstr 找子字符串。

举个实际例子,在完整的文件路径中提取文件名(不含扩展名):

cpp 复制代码
FullPath := 'C:/Projects/Vision/Images/part_001.png'
// 从后往前找到最后一个斜杠的位置
LastSlashPos := strrchr(FullPath, '/') // 在Windows下可能是'\\'
// 从后往前找到最后一个点号(扩展名分隔符)的位置
LastDotPos := strrchr(FullPath, '.')

// 提取文件名部分(斜杠后,点号前)
if (LastSlashPos != -1 and LastDotPos != -1 and LastDotPos > LastSlashPos)
    FileName := str_first_n(str_last_n(FullPath, |FullPath|-LastSlashPos-1), LastDotPos-LastSlashPos-1)
    // 得到 'part_001'
endif

虽然看起来步骤多,但这是理解字符串位置操作的经典案例。在实际中,我们可能会用更高效的正则表达式来做,但掌握这些基础函数,能让你更透彻地理解字符串处理的本质。

3.1 实战:解析视觉系统日志

让我们用一个综合案例把分割、搜索和转换串起来。假设视觉系统生成一条日志:"2024-03-15 14:30:25 [ERROR] Camera 2 trigger timeout at line 1023"。我们需要从中提取出错误时间、设备号和行号。

cpp 复制代码
LogEntry := '2024-03-15 14:30:25 [ERROR] Camera 2 trigger timeout at line 1023'

// 1. 按空格分割,得到各个单词/部分
Parts := split(LogEntry, ' ')
// Parts = ['2024-03-15', '14:30:25', '[ERROR]', 'Camera', '2', 'trigger', 'timeout', 'at', 'line', '1023']

// 2. 提取时间(前两个部分组合)
ErrorTime := Parts[0] + ' ' + Parts[1] // '2024-03-15 14:30:25'

// 3. 提取相机编号('Camera'后面的数字)
// 先找到'Camera'的位置
CamIndex := find(Parts, 'Camera') // 返回3(元组索引,从0开始)
if (CamIndex != -1 and CamIndex+1 < |Parts|)
    CameraNum := number(Parts[CamIndex+1]) // 得到数字 2
endif

// 4. 提取行号('line'后面的数字)
LineIndex := find(Parts, 'line')
if (LineIndex != -1 and LineIndex+1 < |Parts|)
    LineNum := number(Parts[LineIndex+1]) // 得到数字 1023
endif

通过这个例子,你可以看到,即使没有正则表达式,通过组合基础字符串操作,也能完成复杂的文本解析任务。这是构建稳健日志分析功能的基础。

4. 正则表达式:字符串处理的"终极武器"

如果说基础的字符串函数是"瑞士军刀",那么正则表达式就是一把"激光剑"。它能让你用极简的规则描述复杂的文本模式,实现精准的匹配、提取和替换。在HALCON里,正则表达式功能主要通过 tuple_regexp_test(测试)、tuple_regexp_match(匹配提取)、tuple_regexp_replace(替换)和 tuple_regexp_select(选择)这四个算子来实现。

刚接触正则表达式时,那些 \d, \w, ^, $, .* 符号看起来像天书。别怕,咱们从最简单的开始。你可以把正则表达式理解为一种"文本模具",它定义了你要找的字符串长什么样。

几个最核心的元字符:

  • ^:匹配字符串的开头^Error 只匹配以"Error"开头的行。
  • $:匹配字符串的结尾\.png$ 匹配所有以".png"结尾的字符串。
  • .:匹配任意单个 字符(除了换行符)。img.\.bmp 可以匹配"img1.bmp", "imgA.bmp"。
  • *:表示前面的字符可以出现0次或多次ab* 能匹配 "a", "ab", "abb"...
  • +:表示前面的字符可以出现1次或多次ab+ 能匹配 "ab", "abb",但不能匹配"a"。
  • \d:匹配一个数字 。等价于 [0-9]
  • \w:匹配一个字母、数字或下划线
  • ()捕获分组。括号里的内容匹配后可以被单独提取出来,这是最强大的功能之一。

4.1 实战:智能文件管理

在视觉项目中,我们经常要处理大量图像文件。文件命名可能杂乱无章,比如 "part_001.png", "scan-20240315.jpg", "defect_1.bmp"。用正则表达式来筛选和重命名,简直不要太方便。

假设我们要从一个文件夹里找出所有以"part_"开头、后面跟着三位数字、并以".png"结尾的图片文件:

cpp 复制代码
// 假设Files是一个包含所有文件名的元组
Files := ['part_001.png', 'scan.jpg', 'part_123.png', 'notes.txt', 'part_99.png']

// 使用正则表达式选择匹配的文件
// 解释:^part_ 以part_开头;\d{3} 精确匹配3个数字;\.png$ 以.png结尾
SelectedFiles := regexp_select(Files, '^part_\d{3}\.png$')
// 结果 SelectedFiles 为 ['part_001.png', 'part_123.png']
// 注意 'part_99.png' 因为只有两位数字,没有被选中。

更常见的是提取文件名中的关键部分。比如,从 "img_20240315_001.bmp" 中提取日期和序号:

cpp 复制代码
FileName := 'img_20240315_001.bmp'
// 使用捕获分组 () 来提取我们感兴趣的部分
// 模式:img_(\d{8})_(\d{3})\.bmp
Matches := regexp_match(FileName, 'img_(\d{8})_(\d{3})\.bmp')
// 结果 Matches 为 ['20240315', '001']
// Matches[0] 是第一个括号捕获的 '20240315'
// Matches[1] 是第二个括号捕获的 '001'

这样,日期和序号就被干净利落地分离出来了,可以直接用于数据库存储或生成报告。

4.2 实战:复杂文本替换与格式化

替换操作 regexp_replace 结合捕获分组,能实现强大的文本重构功能。比如,我们有一批旧系统生成的序列号,格式是 "SN/1234567-X",需要转换成更可读的格式 "Product Model X, Serial Number 1234567"

cpp 复制代码
OldSerialNumbers := ['SN/1234567-X', 'SN/2345678-Y', 'SN/3456789-Z']
// 模式:SN/(\d{7})-([A-Z])
// 第一个分组 (\d{7}) 捕获7位数字序列号
// 第二个分组 ([A-Z]) 捕获单个大写字母型号
NewDescriptions := regexp_replace(OldSerialNumbers, 'SN/(\d{7})-([A-Z])', 'Product Model $2, Serial Number $1')
// 结果:
// ['Product Model X, Serial Number 1234567',
//  'Product Model Y, Serial Number 2345678',
//  'Product Model Z, Serial Number 3456789']

这里的 $1$2 分别指代第一个和第二个捕获分组的内容。这种反向引用让替换变得极其灵活。

再举一个日期格式转换的例子,把"MM/DD/YYYY"转换成"YYYY-MM-DD":

cpp 复制代码
USDates := ['01/04/2000', '06/30/2007']
ISODates := regexp_replace(USDates, '(\d{2})/(\d{2})/(\d{4})', '$3-$1-$2')
// 结果: ['2000-01-04', '2007-06-30']

4.3 避坑指南与性能考量

正则表达式功能强大,但也要小心使用,不然容易掉进坑里。

第一个坑是"贪婪匹配" 。默认情况下,*+ 这类量词是"贪婪"的,它们会尽可能多地匹配字符。比如,对于字符串 "<tag>content</tag>",模式 "<.*>" 会匹配整个 "<tag>content</tag>",而不是你期望的单个标签 "<tag>"。解决方法是使用非贪婪匹配 ,在量词后面加一个 ?,变成 "<.*?>",这样它就会匹配尽可能少的字符。

第二个坑是特殊字符转义 。正则表达式里,点号.、星号*、加号+、问号?、括号()、方括号[]、花括号{}、反斜杠\、脱字符^、美元符$、竖线| 这些都有特殊含义。如果你想匹配它们本身,必须在前面加上反斜杠\进行转义。比如要匹配一个真实的点号(作为文件名分隔符),必须写成 \.

第三个是关于性能 。复杂的正则表达式,尤其是包含大量回溯的表达式,在匹配超长字符串时可能会很慢。在工业视觉的实时场景下,如果对性能要求苛刻,对于简单的固定模式匹配(比如找固定前缀),有时用 strstr 这类基础函数反而更快。正则表达式更适合模式复杂多变、规则性强的文本处理。

我个人的经验是,在HALCON脚本中,先用正则表达式快速实现功能原型,确保逻辑正确。如果后续在批量处理大量数据时发现性能瓶颈,再考虑针对特定场景优化,或者将最耗时的部分拆解为更高效的基础字符串操作组合。毕竟,在机器视觉项目里,稳定和可维护性永远是第一位的。掌握了从基础到正则这一套完整的字符串处理方法,你就能从容应对项目中各种"文字游戏",让视觉系统真正地"能看会想"。

相关推荐
吴声子夜歌1 天前
正则指引——Ruby
开发语言·正则表达式·ruby
吴声子夜歌1 天前
正则指引——Golang
开发语言·golang·正则表达式
吴声子夜歌3 天前
正则指引——PHP
开发语言·正则表达式·php
吴声子夜歌4 天前
正则指引——断言
正则表达式
吴声子夜歌5 天前
正则指引——匹配原理
java·正则表达式
吴声子夜歌6 天前
正则表达式——环视
开发语言·正则表达式
吴声子夜歌6 天前
正则表达式——边界
正则表达式
营养充电站7 天前
C++23 新特性在 CLion 中的实战体验
macos·docker·jupyter·正则表达式
吴声子夜歌7 天前
正则表达式——字符组
正则表达式