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

相关推荐
半新半旧2 小时前
正则表达式
正则表达式
程序员Sonder2 小时前
黑马java----正则表达式(一文弄懂)
java·正则表达式·新人首发
doris82042 小时前
Python 正则表达式 re.findall()
java·python·正则表达式
python_chai2 小时前
正则表达式从入门到实战:Python高效处理文本的终极秘籍
正则表达式·re模块·文本处理·pothon·贪婪匹配
Mrliu__2 小时前
Python高级技巧(六):正则表达式
开发语言·python·正则表达式
YC运维2 小时前
Shell 正则表达式完全指南
正则表达式
AhoJustLikeU2 小时前
萌新学习正则表达式日志
正则表达式
禹凕3 小时前
MySQL——基础知识(正则表达式)
数据库·mysql·正则表达式
Jerry_Gao9211 天前
【CTF】【ez-rce】无字母数字绕过正则表达式
正则表达式·php·ctf