做药片抓取这个项目,是我接触Halcon以来最考验“匹配稳定性”的一单活。振动盘把白底红字的药片打进传送带,相机要在300毫秒内给出每一粒药片的中心和角度,机械臂的真空吸嘴再按顺序把它吸走——整个过程里,视觉部分用的就是Halcon的相关性匹配,也就是灰度归一化互相关(NCC)模板匹配。今天把整个方案的选型逻辑、标定流程、模板制作和现场踩坑都整理出来,给后面要做药片、胶囊、糖果这类小物件定位抓取的朋友一个可复现的参考。
这类项目看着简单,但药片直径只有6到10毫米,表面又有刻字、刻痕、糖衣颜色这些细节,光源稍微一偏,分数就往下掉。更麻烦的是传送带一直在动,药片角度随机、位置随机,偶尔还有半遮挡和反光。你要做的不是“在实验室里跑通一次匹配”,而是“让它在连续生产里一天跑几千次都不出错”。这两个目标之间的差距,全靠对NCC原理的理解和现场调试的经验来填。
1. 为什么是相关性匹配:药片视觉定位方案选型的全链路思考
1.1 药片视觉定位的需求拆解
先把需求盘清楚。药片抓取项目在视觉端要回答三个问题:视野里有哪些药片、每颗药片的中心点在哪、每颗药片相对于模板转了多少度。输出给机器人的就是一组X、Y、Angle坐标,机器人按顺序抓取。
药片这类目标的特殊之处在于:
- 尺寸小,常规视野按30到50厘米计算,单颗药片可能只占几十个像素;
- 外观依赖表面印刷,圆形外轮廓的区分度很低,所有药片从形状上看都是个圆盘;
- 随机性高,药片进入视野时角度完全随机,正反面也可能随机;
- 节拍要求高,从触发拍照到坐标输出一般只有200到400毫秒。
这四点决定了你不能草率地选一个匹配算子就开干。药片是典型的“形状简单、纹理丰富”目标,这个特点直接影响了方案选型的方向。
1.2 形状匹配、NCC、深度学习的取舍逻辑
Halcon里做定位,常见三条路线:基于形状的模板匹配(shape-based matching)、基于灰度相关性的NCC匹配(normalized cross correlation)、以及基于深度学习的实例分割或者对象检测。
传统经验是:有清晰边缘的五金件选形状匹配,背景复杂、目标纹理统一的场景选深度学习。但药片恰好站在一个有趣的中间地带——它的边缘太规整了,几乎所有药的圆形边缘都长得一样,形状匹配把一颗圆形药片转0度和转180度得到的轮廓几乎完全一致,算法本身就难以区分方向,更别提区分同尺寸不同厂家的药片。
而药片表面往往有人为设计的丰富灰度信息:刻字、刻痕、压印图案、糖衣的渐变色。这些信息在形状匹配里完全用不上,却恰恰是NCC最擅长利用的。NCC算法是拿整块模板区域的灰度与搜索图像逐像素做归一化互相关计算,模板里每个像素的灰度值都参与了分数计算,所以表面有一行字、一个Logo、甚至一道压痕,都会被当作特征计入匹配结果。
深度学习当然也行,而且对遮挡、光照的表现可能更好。但对药片抓取这个场景,深度学习有两个绕不开的问题:一是样本标注成本高,一个项目里可能就几千粒药片要上线,收集异常样本和标注的时间比调NCC长得多;二是推理速度虽然不慢,但要跑在工控机的CPU上还得挂着GPU优化,小型产线项目里投入产出比不高。NCC方案在这个量级下是最务实的。
1.3 “相关性”在这里到底算了什么
很多朋友把NCC理解成“图片像不像”,这不够准确。Halcon里create_ncc_model创建的模板,记录的是模板区域每个像素位置的灰度参考值,以及这些像素之间的相对布局。到了搜索阶段,find_ncc_model会把模板放到图像的每个可能位置,计算归一化互相关值。这个值对线性光照变化的容忍度很高——简单说,如果整张图稍微变亮或变暗,NCC分数不会剧烈波动,因为它做了灰度均值与方差的归一化。
理解了这一点,很多调试行为就有方向了:
- 药片表面出现一颗反光点,它影响的是局部灰度,归一化计算会把局部影响摊薄,但摊不没;
- 模板本身模糊,那分数天花板就低,再怎么调参数也救不回来;
- 光照不均匀导致药片半边亮半边暗,NCC反而可能因为这种局部灰度模式与模板不一致而丢分。
所以,NCC不是“无脑匹配”,它是把药片表面的灰度当作一种指纹去比对。指纹的清晰度、完整性、一致性,决定了匹配上限。
2. 坐标系与手眼标定:把像素坐标变成机器人能用的坐标
2.1 相机安装方式与坐标规划
药片抓取项目里,最常用的相机安装方式是相机固定在传送带正上方,镜头垂直朝下,视野覆盖机器人抓取工位。麦克纳姆轮、皮带、直振供料器都可以,只要保证拍照时药片在视野内且在相机焦平面上即可。这种安装方式属于典型的“眼在手外”,相机坐标系和机器人基座标系是两套独立的坐标系。
视觉输出的是一组像素行列坐标,机器人要的是自己坐标系下的X、Y位置,以及吸嘴旋转角度。中间必须完成两步:
- 相机畸变校正,把镜头本身的桶形/枕形畸变消除;
- 单应性映射(或者完整的手眼矩阵),把校正后的像素坐标映射到机器人平面坐标。
很多新手会忽略拓扑方向问题。像素坐标系的行坐标向下增长,而机器人基座标系的Y轴方向取决于安装方式,可能向上也可能向下;像素列向右增长,机器人X轴方向同样取决于实际布局。如果在标定时不把方向对应关系理清楚,后期会发现机器人永远朝反方向抓。
2.2 用标定板建立像素到机器人坐标的映射
具体做法分三步:
第一步:把标定板放在机器人抓取平面上,调整位置,让标定板上的特征点落在视野中心区域。标定板规格根据药片尺寸选择,药片只有几毫米,那就选边长10毫米或者更小的陶瓷标定板,而不是棋盘格间距20毫米的大标定板,否则视野里的点数太少,映射精度不够。
第二步:在Halcon里打开标定助手,导入标定板描述文件,拍摄5到10张不同位姿的标定板图像做相机内参标定,得到相机内参和畸变参数。这一步做完,用change_radial_distortion做去畸变校正,后面讨论的所有像素坐标都基于校正后图像。
第三步:把标定板放回抓取平面,依次用机器人示教笔尖对齐标定板上至少3个特征点(推荐4个角点和中心点,共5个点),记录每个点的机器人坐标Xr,Yr,同时在去畸变图像里用find_calib_object提取对应特征点的像素坐标Row,Col。然后执行:
* 像素点集与机器人点集 vector_to_rigid (Px, Py, Px2, Py2, HomMat2D)这个算子计算两个点集之间的刚性变换矩阵。后面每个匹配结果都走同一个变换:
affine_trans_point_2d (HomMat2D, Row, Col, X, Y)这里有个容易犯的错:Row是行坐标,先传给三维里的第一个输入,Col是列坐标,传第二个输入,输出对应机器人坐标X、Y。我在项目里见过有人把行列传反了,结果所有点位整体做了一次镜像,抓取时吸嘴全部偏到药片边缘。这个顺序看起来不起眼,但错一次就是半天工作量。
2.3 标定精度验收方法
标定做完不能直接上产线,得先验收。我的做法是:在抓取平面上放5个已知间距的定位点,用机器人依次示教它们的真实坐标,再用视觉分别拍一遍,计算变换后的坐标与真实坐标的偏差。正常情况下,静态重复精度应该小于0.3毫米。如果偏差是固定方向的大值,比如全部偏1毫米,先查标定点是否选错;如果偏差呈发散状,比如不同位置偏的方向不一致,要么是标定板的特征点提取偏差大,要么是镜头畸变没校正干净。
药片抓取对绝对坐标精度要求反而没那么苛刻,因为真空吸嘴有一定吸合范围,但姿态角度精度要高,角度错了,吸嘴边缘会蹭到相邻药片。所以标定之后,角度映射最好单独验证一遍:放一个方向已知的药片,视觉出的角度和机器人实测角度对不上时,优先检查标定板是否摆歪了。
3. NCC模板的创建与预处理:细节决定匹配上限
3.1 模板采集环境:先用一张好图打底
建模板之前,先把图像质量搞定。药片体积小,相机分辨率建议500万像素以上,镜头工作距离控制在200到400毫米,让单颗药片在图像里占到80到120像素宽度。太小了刻字看不清,NCC分数压不上去;太大了视野变小,节拍又跟不上。
模板图像必须是现场光照条件下的真实图像,不能在实验室台灯下拍了回车间用。药片的糖衣颜色对光源角度极其敏感,正面光容易在弧面最高点产生高光,低角度环形光又会在底部形成阴影。我最开始用的是白色环形光,结果白色药片在白色背景上几乎融在一起,模板分数只有0.5。后来改成低角度红色环形光加蓝色背光板,药片边缘清晰,表面纹理也立起来了。
3.2 模板图像预处理
拿到一张理想的药片图像后,别急着建模板,先做三步预处理。
第一步:裁剪ROI。ROI只框住药片主体,不要包含传送带背景。NCC模板的区域就是将来参与相关计算的区域,背景一旦被框进去,药片位置变化时模板边缘的背景灰度也在变,分数会像过山车一样抖。
第二步:平滑降噪。药片表面压纹在低分辨率下会出现椒盐噪声,建模板之前用一次轻微的中值滤波:
median_filter (ImageReduced, ImageMedian, 'circle', 3, 'mirrored')注意滤波半径不要大,3像素足够,半径太大容易把刻字边缘磨平,反而丢了特征。
第三步:光照归一化。如果传送带不同位置亮度有差异,用illuminate算子对图像做光照校正:
illuminate (ImageReduced, ImageIllum, 21, 21, 1.0)这个算子的思路是估计背景照度并归一化,适合表面纹理相对平稳的物体。药片这种小而均一的目标很适合,但不要用在背景纹理很杂的图上,否则会把背景的随机纹理也当成特征。
3.3 创建NCC模板的算子与参数
预处理完,直接建模板:
* 创建NCC模板 create_ncc_model (ImageMedian, 'auto', rad(-10), rad(20), 0, 'use_polarity', ModelID)AngleStart和AngleExtent要特别当心。NCC匹配对旋转比较敏感,因为模板是像素级灰度对齐,旋转后同一位置的像素对应关系会变化。如果药片进入视野的角度范围是±10度,那就给±10度,不要为了图省事设为0到360度,角度范围越大,搜索越慢,而且0度模板和350度模板在相近角度下会形成重复响应,反而干扰匹配。
如果你的产线药片完全是随机朝向,没有定向机构,那更合理的方式不是建一个360度模板,而是建四个模板——每90度朝向建一个,然后并行搜索,速度更快,匹配稳定性也更高。
NumLevels用'auto'就好,Halcon会按金字塔层级自动分配。Metric选'use_polarity',意思是要求药片表面的亮暗关系与模板一致,药片表面字符凸起在光照下的高光和阴影模式是唯一的,用极性约束能极大排除错误匹配。
3.4 多模板策略
同一款药片有正面和背面,或者不同批次的药片印刷深浅不同,不要试图用单一模板通吃。Halcon里create_ncc_model返回的ModelID可以是一个数组,find_ncc_model传入模型数组时,会依次搜索所有模板并汇总匹配结果。
比如正反面模板都建:
create_ncc_model (ImageFront, 'auto', rad(-10), rad(20), 0, 'use_polarity', ModelIDFront) create_ncc_model (ImageBack, 'auto', rad(-10), rad(20), 0, 'use_polarity', ModelIDBack) * 搜索时传入数组 find_ncc_model (Image, [ModelIDFront, ModelIDBack], rad(-10), rad(20), 0.7, 50, 0.3, 'true', 4, Row, Column, Angle, Score)多模板搜索的分数会取响应最好的那个,这对正反面随机出现的场景非常有效。但要注意,模板之间不要长得太像,否则同一个位置会同时命中两个模板,输出两倍数量的点位,后面的排序逻辑就乱了。这时可以输出结果对比,手动剔除掉冗余模板。
4. find_ncc_model参数调优:从0.5分数到稳定0.9的实战过程
4.1 每个参数到底在控制什么
匹配搜索的一句核心调用长这样:
find_ncc_model (Image, ModelID, rad(-10), rad(20), MinScore, NumMatches, MaxOverlap, SubPixel, NumLevels, Row, Column, Angle, Score)把参数逐个拆开看:
| 参数 | 作用 | 我的经验值 |
|---|---|---|
AngleStart/AngleExtent | 搜索角度范围,必须覆盖实际可能出现的角度偏差 | 略大于模板创建范围,±10到±20度足够 |
MinScore | 匹配分数阈值,低于此值的候选被丢弃 | 先用0.7观察,逐步上调到0.8以上 |
NumMatches | 最多返回多少个匹配目标 | 设为视野内可能出现的最大药片数加余量 |
MaxOverlap | 两个匹配结果允许重叠的最大比例 | 0.3到0.5,药片轻微重叠时很关键 |
SubPixel | 是否计算亚像素精度的位置和角度 | 设为'true',抓取精度从像素级提升到亚像素级 |
NumLevels | 金字塔搜索层级 | 4到6,层级过大易漏小目标,层级过小速度慢 |
这里我吃过一个亏。最初把NumMatches设成了10,想着药片最多也就七八颗,结果有一张图里传送带缝线的灰度模式与药片表面某个局部纹理高度相似,NCC匹配直接输出了十几个候选点,把后续排序给淹了。后来我把NumMatches压到实际最大药片数加2,同时在排序前过滤掉超出抓取区域范围的坐标,问题才解决。
4.2 分数阈值不是越高越好
很多人直觉上觉得MinScore设到0.95才叫牛。药片这类小目标,表面又有弧面反光,强行设0.95的结果是漏检,药片明明在视野里,就是找不到,导致机器人空抓。实际调试应该从0.6开始跑,看现场几百帧图像里的分数分布,再决定阈值:
- 如果所有真实目标分数都在0.85以上,且误检都在0.6以下,那0.75是舒服的;
- 如果真实目标偶尔掉到0.72,误检又到0.7,那就不该调阈值,该回头查模板质量或光照。
我们项目最后定在0.8,因为传送带震动导致偶尔糊帧,真实分数会掉到0.78左右,0.8刚好卡在“偶尔漏一两帧但绝不误检”的平衡点。
4.3 MaxOverlap与重叠药片的博弈
药片在传送带上偶尔会叠在一起。有的叠得很重,上面一片几乎盖住下面一片。这时候有两个处理方向:一个是尽量不抓被压住的药片,避免吸嘴带起两片;另一个是强行算两个点位,寄希望于机器人能分开吸。NCC的做法是:MaxOverlap控制在0.3,重叠超过30%的药片不再输出点位,这样抓取的药片都是可吸的独立药片。
但要注意,重叠药片之间的角度往往基本一致,间距又小,即使分数很高,机械臂吸嘴的圆形端面很可能压到邻片。我在排序逻辑里额外加了一道“安全距离检查”:
distance_pp (Row[i], Col[i], Row[j], Col[j], Distance)所有距离小于吸嘴半径加2毫米余量的点对,只保留分数高的那个。这个逻辑比单纯依赖MaxOverlap更贴近实际抓取安全。
4.4 亚像素精度和角度输出的稳定性
SubPixel='true'时,Halcon返回的行列坐标和角度都做了亚像素插值。实际测试中,静止药片连续拍20次,坐标标准偏差能控制在0.05像素以内,换算到机器人坐标大概是0.05到0.1毫米。如果不需要这么高的精度,设'false'会让速度快15%左右,但药片抓取一般都对精度敏感,还是建议开。
角度输出有一个容易被忽略的坑:药片是旋转对称的,0度和180度的刻字图案有可能完全一样。如果角度对抓取不重要,那无所谓;如果药片表面有方向标记需要保持某个朝向入瓶,要让模板包含非对称特征,或者在输出角度后做一次180度模数归一化,把重复的角度区间合并掉。
5. 多药片点位排序与机器人通信:让抓取不撞车、不遗漏
5.1 抓取顺序的排序策略
一帧图里同时出现七八颗药片,视觉把点位全给了机器人,机器人从哪个开始抓?这是一个容易被忽略但实际很关键的问题。
最简单的思路是“距离优先”:算机器人在等待位到每个点位的距离,选最近的抓。这个策略看着合理,但落在一个坑里——如果来源方向固定,最外沿的药片永远是最近的,机器人会一直抓外侧的药片,靠近出料口的药片被推走或被下一颗药片盖上,反而抓不到。
我们在项目里改成了“扫描线优先”:把所有点位按Y坐标从大到小排序,Y接近的按X从小到大排,相当于从传送带远端往近端逐行扫。这样抓取路径有规律,吸嘴不会跨来跨去,也不容易碰撞。
排序用Halcon自带元组函数:
tuple_sort_index (Ys, Indices)然后按Indices重新组织行列坐标即可。排序之后必须重算安全距离检查,因为排序本身不做防碰撞。
5.2 与机器人的Socket通信
机器视觉给机器人发坐标,最通用的是TCP/IP Socket通信,Halcon侧的开通信流程非常简单:
open_socket_connect ( '192.168.1.10', 5000, 'protocol', 'TCP', Socket)发送前把坐标格式化成ASCII字符串,一行一组:
tuple_string (X, '.3f', SX) tuple_string (Y, '.3f', SY) tuple_string (Angle, '.3f', SA) Message := SX + ',' + SY + ',' + SA + '\n' send_string (Socket, Message)这里要注意三点:
- 一个点一组发送,不要攒一批一起发,万一一个点位抓失败,后面全乱;
- 通信协议要约定好单位是毫米还是厘米,角度是度还是弧度。我们在这上面吃过亏,机器人收到的角度单位是弧度,机器人程序按度解析,结果所有姿态偏了57度,误以为自己标定错了,查了半天;
- 发送后等机器人回一个ACK,用
receive_string阻塞等待,确认收到再发下一个点。不要省这一步,网络闪断一次,整个抓取序列就会错乱。
5.3 抓空与漏抓的视觉二次确认
机器人吸嘴下去了,到底有没有吸到药片,视觉可以帮忙确认。常见做法是拍照后与抓取后的图像做差分,或者干脆再跑一次NCC匹配看该点位是否还存在高响应目标。
我们用的方案是:每次抓取完成,机器人移出视野后触发一次快速拍照,只对刚才的抓取位置附近做小范围find_ncc_model。如果该点位还能匹配到分数大于0.8的药片,说明没抓起来,停止下一轮抓取并报警。这个二次确认的时间很短,因为只在局部区域搜索,而且只查一个模板,耗时控制在50毫秒以内。
6. 现场光照干扰与误匹配排查:一次完整的故障处理记录
6.1 问题现象:同一套程序,换了产线就误检
项目推进到第二个月,客户把同一套视觉方案挪到另一条更快的产线。结果一上电,误检率从0.1%猛增到3%,而且都是同一种表现:药片侧面出现一个分数很高的误匹配点,跟着吸嘴就朝那个位置猛扎下去,连吸带推,一排药片全乱了。
我把现场图像抓回来,在Halcon里一帧帧跑,发现误检的位置都集中在药片边缘与背景高光交界的地方。那条产线的传送带颜色更深,药片掉落路径上有一块金属导流板,反射的杂光形成了一条高亮带。NCC模板里药片表面刻字的灰度模式,与高光带局部区域的灰度模式极度相似,分数直接冲到0.85。
6.2 逐步排查链路
我的排查顺序是有讲究的:
第一步,确认图像源。用grab_image连续采集100帧现场图像,挑出误检发生的帧,保存到本地。这一步省不得,没有原始图像,后面全是猜谜。
第二步,视觉化匹配过程。把find_ncc_model的结果用dev_display叠加在图像上,同时打印每个候选点的Score值。通过可变阈值循环扫描,观察分数上升路径,能看出误检点是否从一开始就存在。
第三步,缩小范围。把误检点位的ROI单独扩出来,与模板做差分。我发现误检区域的灰度梯度方向与模板刻字的梯度方向高度一致,但形状完全不同——模板里的刻字是一排规则的短线,误检区域只是几条平行的光带。这说明use_polarity在局部区域分辨不了“形似”和“神似”的差别。
第四步,从源头压掉干扰。先在光源上动手,给金属导流板贴了一层哑光膜,高光带亮度下降后,误检分数降到0.6以下。同时在匹配结果上增加一个后置掩膜:只保留圆心落在药片工作区间内的匹配点。两个动作一起上,误检率回落到0.05%以下。
6.3 高光药片的专门处理
更麻烦的是表面光滑的糖衣药片。这类药片本身就像小镜子,任何直射光源都会在弧顶形成强高光。NCC模板建的时候是亮的,但换一批药片糖衣颜色深浅略有不同,高光位置偏移,分数就掉。
后来查Halcon文档,发现有一个被忽略的选项:ignore_global_polarity。它允许目标是模板的整体反色,对“药片表面颜色深浅不一致”有一定包容度。但用这个选项会损失部分区分度,因为模板不再校验亮暗关系。我的建议是:只有同一款药片批量颜色波动很大时才用,平时老老实实用use_polarity,再配合光源选型——低角度环形光比顶光对弧面高光的抑制效果好得多。
6.4 半遮挡、出视野药片的稳健处理
传送带边缘的药片经常只露出一半,NCC照样会给出一个分数,但位置可能是药片完整中心与可见部分的加权中心,偏差会很大。我们不能让机器人去抓一个不完整的药片。
处理思路是在匹配结果上做ROI合法性校验。定义抓取区域为一个矩形或者多边形区域,匹配点的中心必须落在区域内,否则丢弃。
gen_rectangle1 (GrabRegion, Row1, Col1, Row2, Col2) * 逐点判断 tuple_in_range (Col, Col1, Col2, IsInX) tuple_in_range (Row, Row1, Row2, IsInY)只有同时满足行列范围的匹配点才进入发送队列。这个逻辑完全不需要改NCC参数,就能把边缘半露的药片排除掉,非常实用。
这套方案上线后,视觉系统的综合识别率稳定在99.95%以上,单帧处理耗时平均80毫秒,最坏120毫秒,远小于300毫秒的节拍预算。我在这个项目里最深的体会是:NCC匹配的成败,三分在算法参数,七分在模板图像和现场光照。你可以在代码里花一周调MinScore,但不如花一天把导流板上的反光贴纸贴掉来得彻底。药片抓取这类小目标视觉,拼的本质上是对成像环境的理解和对灰度细节的敏感度,这块功夫到位了,模型搬运到哪条产线都不虚。