当您仔细观察印刷电路板(PCB)时,常常会发现一些金色的小圆盘独立于走线之外。这些圆盘就是基准标记(又称基准点),其设计目的非常纯粹:帮助视觉系统快速识别,进而精确锁定整块PCB在图像中的位置。
本文将详细介绍如何利用 OpenCV 的 matchTemplate() 函数,精准识别这些基准标记。完整代码可从文末链接的 GitHub 仓库获取,但在此之前,我们先逐步拆解核心实现逻辑。
为什么 PCB 需要基准标记?
在PCB自动化组装流水线上,机器必须将电阻、电容、连接器等分立元件精确放置到焊盘上,然后进行焊接。基准标记正是为这些自动化系统提供定位指引——它向机器传达板子的位置与角度信息,确保元件不会放偏。元件贴装完成后,自动检查系统还需逐一验证每个元件的坐标是否在公差范围内。此时,基准标记再次发挥作用,提供从PCB设计图的毫米坐标到相机图像像素坐标的转换参考。
简而言之,没有这些金色小圆盘,机器将无法准确作业。
预处理
我们首先将图像转换为灰度图,因为基准标记的核心特征在于形状,与颜色关联不大。使用 OpenCV 的 cvtColor() 函数即可轻松完成。
以本例中的PCB为例,其基准标记由两个同心圆构成,外圆直径为68像素,内圆直径为26像素。
matchTemplate() 函数需要一个模板图像,即我们要寻找的目标样本。您可能会想:直接裁剪一个现有的基准标记作为模板不就够了?但这种方法存在风险:如果裁剪的标记因光照、磨损或加工偏差与其他标记存在差异,就可能导致漏检。更可靠的做法是人工合成一个“理想基准标记”的图像:
# 创建一个合成的基准标记图案
pattern_sizeHW = [args.fiducialOuterDiameterInPixels, args.fiducialOuterDiameterInPixels]
if args.fiducialOuterDiameterInPixels % 2 == 0: # 保证尺寸是奇数
pattern_sizeHW[0] += 1
pattern_sizeHW[1] += 1
fiducial_pattern = np.zeros(pattern_sizeHW, dtype=np.uint8)
cv2.circle(fiducial_pattern, (pattern_sizeHW[1]//2, pattern_sizeHW[0]//2),
args.fiducialOuterDiameterInPixels//2, 70, cv2.FILLED) # 外圆暗灰色
cv2.circle(fiducial_pattern, (pattern_sizeHW[1]//2, pattern_sizeHW[0]//2),
args.fiducialInnerDiameterInPixels//2, 255, cv2.FILLED) # 内圆白色
# 标准化:让图案均值为0,标准差为1
standardized_fiducial_pattern = (fiducial_pattern.astype(np.float32) - fiducial_pattern.mean()) / fiducial_pattern.std()
标准化处理至关重要。我们期望 matchTemplate() 的输出在图像平坦区域趋近于0,仅在真正匹配到基准标记的位置接近1。这样,后续的阈值筛选将变得清晰明确。
模板匹配
有了模板,就可以调用 matchTemplate() 了:
# 模板匹配
match_img = cv2.matchTemplate(grayscale_img.astype(np.float32),
standardized_fiducial_pattern,
cv2.TM_CCOEFF_NORMED)
# 为了可视化,把匹配结果填充到与原图相同尺寸
padded_match_8bits_img = np.zeros((img_shapeHWC[0], img_shapeHWC[1]), dtype=np.uint8)
padded_match_8bits_img[fiducial_pattern.shape[0]//2 : fiducial_pattern.shape[0]//2 + match_img.shape[0],
fiducial_pattern.shape[1]//2 : fiducial_pattern.shape[1]//2 + match_img.shape[1]] = \
(128 * (match_img + 1.0)).astype(np.uint8)
这里有个细节需注意:matchTemplate() 基于卷积运算,输出图像的尺寸小于原图。具体而言,若模板为69×69,输出尺寸将在宽度和高度上各减少68像素。我们通过外围补零将其对齐到原图尺寸,同时将匹配结果数值从[-1,1]映射到[0,255],便于人眼观察。
上图所示的亮斑即为匹配度较高的区域。我们需要从这些亮斑中筛选出真正的基准标记——简而言之,就是要设定一个合适的阈值。
手动调整阈值并不明智。既然已知这块PCB上共有3个基准标记,我们可以逆向思维:从255开始逐步降低阈值,每次统计二值图像中的斑点数量,直到恰好检测到3个斑点为止。这样阈值便能自动确定。
# 自动寻找能检测出预期数量基准标记的最优阈值
blob_detector = blob_analysis.BinaryBlobDetector()
optimal_threshold = None
optimal_seedPoint_boundingBox_list = None
optimal_annotated_blobs_img = None
for threshold in range(255, 1, -1):
_, thresholded_img = cv2.threshold(padded_match_8bits_img, threshold, 255, cv2.THRESH_BINARY)
seedPoint_boundingBox_list, annotated_blobs_img = blob_detector.DetectBlobs(thresholded_img)
logging.info("threshold = %d; number of blobs = %d", threshold, len(seedPoint_boundingBox_list))
if len(seedPoint_boundingBox_list) >= args.numberOfFiducials:
optimal_threshold = threshold
optimal_seedPoint_boundingBox_list = seedPoint_boundingBox_list
optimal_annotated_blobs_img = annotated_blobs_img
break
logging.info("Optimal match threshold = %d, found %d blobs",
optimal_threshold, len(optimal_seedPoint_boundingBox_list))
变换矩阵的计算
至此,我们获得了PCB上三个基准标记的物理坐标(毫米)及其对应的像素坐标。三个点恰好满足计算单应性矩阵的最低要求。该矩阵可实现毫米坐标与像素坐标之间的相互转换。利用它,我们便能在图像上精确标注PCB的轮廓,或裁切出任意感兴趣的区域。
上图标注结果证实三个基准标记均被准确识别,计算出的变换矩阵精度可靠。后续,只需获知某个元件的物理坐标,便可从图像中精确裁切出对应区域进行检验。
在实际生产线上,待检测的PCB通常在传送带上移动,每块板子的位置和角度可能不同。然而,借助基准标记计算出的变换矩阵,无论发生平移还是旋转,我们都能稳定地还原指定的感兴趣区域。
