工业相机选型与 Halcon 九点标定法:Python 完整实现指南
工业相机选型与 Halcon 九点标定法:Python 完整实现指南
摘要:工业相机选型和手眼标定是机器视觉项目的第一步,也是最容易踩坑的环节。本文从实际项目出发,系统讲解工业相机选型的关键参数计算(分辨率、帧率、镜头焦距、景深),并提供基于 Halcon 的九点标定法 Python 完整实现,包含标定板生成、图像采集、仿射变换矩阵计算和精度验证的全流程代码。
关键词:工业相机, 镜头选型, Halcon, 九点标定, 手眼标定, 机器视觉, Python
目录
相机选型:四个必算参数
1. 分辨率计算
公式:分辨率 = 视野宽度 / 最小检测精度
示例:检测一个 200mm × 150mm 的工件,要求最小检测精度 0.1mm。
水平分辨率 = 200 / 0.1 = 2000 像素
垂直分辨率 = 150 / 0.1 = 1500 像素
选择 500 万像素相机(2592 × 1944),满足需求。
2. 帧率计算
公式:帧率 ≥ 1 / 节拍时间
示例:产线节拍 0.5 秒/件,要求拍照 + 处理在 0.3 秒内完成。
最低帧率 = 1 / 0.3 ≈ 3.3 fps
实际选型时留 2 倍余量,选择 ≥ 10 fps 的相机。
3. 传感器尺寸与像元尺寸
| 传感器尺寸 | 对角线 (mm) | 典型分辨率 | 像元尺寸 (μm) |
|---|---|---|---|
| 1/3" | 6.0 | 1280×960 | 3.75 |
| 1/2" | 8.0 | 1600×1200 | 4.5 |
| 2/3" | 11.0 | 2448×2048 | 3.45 |
| 1" | 16.0 | 4096×3000 | 3.45 |
选型原则:像元尺寸越大,感光能力越强,低照度环境下噪点越少。工业检测优先选 3.45μm 以上的像元。
4. 接口类型
| 接口 | 带宽 | 最大线缆长度 | 适用场景 |
|---|---|---|---|
| USB 3.0 | 5 Gbps | 3m | 实验室、单机检测 |
| GigE | 1 Gbps | 100m | 产线多相机组网 |
| Camera Link | 6.8 Gbps | 10m | 高速线扫 |
| CoaXPress | 25 Gbps | 100m | 超高分辨率、高速 |
镜头选型:焦距与工作距离
焦距计算公式
焦距 f = 传感器尺寸 × 工作距离 / 视野宽度
示例:2/3" 传感器(8.8mm 宽),工作距离 300mm,视野 200mm。
f = 8.8 × 300 / 200 = 13.2 mm
选择 12mm 或 16mm 定焦镜头。
景深计算
景深 DOF = 2 × 弥散圆 × F数 × (放大倍率 + 1) / 放大倍率²
其中弥散圆 ≈ 像元尺寸 × 2。
示例:像元 3.45μm,F 数 4,放大倍率 0.044(8.8/200)。
弥散圆 = 3.45 × 2 = 6.9 μm
DOF = 2 × 0.0069 × 4 × (0.044 + 1) / 0.044² ≈ 29.8 mm
景深 30mm 意味着工件在 ±15mm 范围内都能清晰成像。
选型速查表
| 视野 (mm) | 工作距离 (mm) | 推荐焦距 (mm) | 推荐镜头 |
|---|---|---|---|
| 50×40 | 100 | 16 | Computar M1614-MP2 |
| 100×80 | 200 | 16 | Computar M1614-MP2 |
| 200×150 | 300 | 12 | Computar M1214-MP2 |
| 300×200 | 500 | 16 | Computar M1614-MP2 |
| 500×400 | 500 | 8 | Computar M0814-MP2 |
标定板生成与打印
九点标定需要一个 3×3 的圆点阵列。以下 Python 脚本生成可直接打印的标定板:
"""
generate_calibration_board.py
生成九点标定板 PDF,可直接打印使用
"""
import matplotlib.pyplot as plt
import numpy as np
def generate_calibration_board(
rows=3, cols=3,
dot_diameter=5.0, # 圆点直径 (mm)
spacing=20.0, # 圆心间距 (mm)
board_margin=15.0, # 边距 (mm)
output_file="calibration_board.pdf"
):
"""
生成九点标定板
"""
# 计算板面尺寸
board_width = cols * spacing + 2 * board_margin
board_height = rows * spacing + 2 * board_margin
fig, ax = plt.subplots(figsize=(board_width/25.4, board_height/25.4))
# 绘制圆点
for row in range(rows):
for col in range(cols):
x = board_margin + col * spacing
y = board_margin + row * spacing
circle = plt.Circle(
(x, y), dot_diameter/2,
color='black', fill=True
)
ax.add_patch(circle)
# 标注序号
point_num = row * cols + col + 1
ax.annotate(
str(point_num),
(x, y + dot_diameter/2 + 2),
fontsize=6, ha='center', va='bottom',
color='gray'
)
# 设置坐标轴
ax.set_xlim(0, board_width)
ax.set_ylim(0, board_height)
ax.set_aspect('equal')
ax.axis('off')
# 添加标尺
ax.plot([board_margin, board_margin + spacing],
[board_height - 5, board_height - 5],
'k-', linewidth=1)
ax.text(board_margin + spacing/2, board_height - 7,
f'{spacing}mm', ha='center', fontsize=8)
plt.tight_layout()
plt.savefig(output_file, dpi=300, bbox_inches='tight')
print(f"标定板已保存: {output_file}")
print(f"板面尺寸: {board_width:.1f}mm × {board_height:.1f}mm")
print(f"圆心间距: {spacing}mm, 圆点直径: {dot_diameter}mm")
if __name__ == "__main__":
generate_calibration_board()
打印时选择 100% 比例,不要缩放。用游标卡尺测量实际间距,确认打印精度。
九点标定法原理
九点标定的目标是建立像素坐标系与世界坐标系之间的映射关系。
数学模型
使用仿射变换(旋转 + 平移 + 缩放):
X_world = a × x_pixel + b × y_pixel + c
Y_world = d × x_pixel + e × y_pixel + f
其中 (a, b, c, d, e, f) 为待求解的 6 个参数。
求解方法
9 个点提供 18 个方程,6 个未知数,使用最小二乘法求解:
已知:
像素坐标: (x₁, y₁), (x₂, y₂), ..., (x₉, y₉)
世界坐标: (X₁, Y₁), (X₂, Y₂), ..., (X₉, Y₉)
构造矩阵:
A = [[x₁, y₁, 1, 0, 0, 0],
[0, 0, 0, x₁, y₁, 1],
[x₂, y₂, 1, 0, 0, 0],
[0, 0, 0, x₂, y₂, 1],
...
[x₉, y₉, 1, 0, 0, 0],
[0, 0, 0, x₉, y₉, 1]]
B = [X₁, Y₁, X₂, Y₂, ..., X₉, Y₉]ᵀ
求解:
params = (AᵀA)⁻¹AᵀB
Python 完整实现
"""
nine_point_calibration.py
九点标定法完整实现
"""
import numpy as np
import cv2
import json
import os
from typing import List, Tuple, Optional
class NinePointCalibration:
"""九点标定类"""
def __init__(self):
self.pixel_points = [] # 像素坐标列表
self.world_points = [] # 世界坐标列表
self.transform_matrix = None # 仿射变换矩阵
self.calibrated = False
def add_point(self, pixel_x: float, pixel_y: float,
world_x: float, world_y: float):
"""添加一个标定点"""
self.pixel_points.append([pixel_x, pixel_y])
self.world_points.append([world_x, world_y])
print(f"添加点 {len(self.pixel_points)}: "
f"像素({pixel_x:.1f}, {pixel_y:.1f}) → "
f"世界({world_x:.1f}, {world_y:.1f})")
def calibrate(self) -> bool:
"""
执行标定计算
返回 True 表示标定成功
"""
if len(self.pixel_points) < 3:
print("错误: 至少需要 3 个标定点")
return False
n = len(self.pixel_points)
# 构造矩阵 A (2n × 6)
A = np.zeros((2 * n, 6))
B = np.zeros((2 * n, 1))
for i in range(n):
px, py = self.pixel_points[i]
wx, wy = self.world_points[i]
# 填充 A 矩阵
A[2*i, 0] = px
A[2*i, 1] = py
A[2*i, 2] = 1
A[2*i+1, 3] = px
A[2*i+1, 4] = py
A[2*i+1, 5] = 1
# 填充 B 矩阵
B[2*i] = wx
B[2*i+1] = wy
# 最小二乘法求解
try:
params = np.linalg.inv(A.T @ A) @ A.T @ B
self.transform_matrix = params.reshape(2, 3)
self.calibrated = True
print("\n标定完成!")
print(f"变换矩阵:\n{self.transform_matrix}")
print(f"X_world = {params[0,0]:.6f} × x + "
f"{params[1,0]:.6f} × y + {params[2,0]:.6f}")
print(f"Y_world = {params[3,0]:.6f} × x + "
f"{params[4,0]:.6f} × y + {params[5,0]:.6f}")
return True
except np.linalg.LinAlgError as e:
print(f"矩阵求解失败: {e}")
return False
def pixel_to_world(self, pixel_x: float, pixel_y: float) -> Tuple[float, float]:
"""将像素坐标转换为世界坐标"""
if not self.calibrated:
raise RuntimeError("请先执行 calibrate()")
world_x = (self.transform_matrix[0, 0] * pixel_x +
self.transform_matrix[0, 1] * pixel_y +
self.transform_matrix[0, 2])
world_y = (self.transform_matrix[1, 0] * pixel_x +
self.transform_matrix[1, 1] * pixel_y +
self.transform_matrix[1, 2])
return world_x, world_y
def world_to_pixel(self, world_x: float, world_y: float) -> Tuple[float, float]:
"""将世界坐标转换为像素坐标(逆变换)"""
if not self.calibrated:
raise RuntimeError("请先执行 calibrate()")
M = np.vstack([self.transform_matrix, [0, 0, 1]])
M_inv = np.linalg.inv(M)
pixel_x = (M_inv[0, 0] * world_x +
M_inv[0, 1] * world_y +
M_inv[0, 2])
pixel_y = (M_inv[1, 0] * world_x +
M_inv[1, 1] * world_y +
M_inv[1, 2])
return pixel_x, pixel_y
def calculate_error(self) -> dict:
"""计算标定误差"""
if not self.calibrated:
raise RuntimeError("请先执行 calibrate()")
errors = []
for i in range(len(self.pixel_points)):
px, py = self.pixel_points[i]
wx_actual, wy_actual = self.world_points[i]
wx_calc, wy_calc = self.pixel_to_world(px, py)
error = np.sqrt((wx_calc - wx_actual)**2 +
(wy_calc - wy_actual)**2)
errors.append(error)
errors = np.array(errors)
return {
"max_error": np.max(errors),
"mean_error": np.mean(errors),
"std_error": np.std(errors),
"rms_error": np.sqrt(np.mean(errors**2)),
"per_point": errors.tolist()
}
def save(self, filepath: str):
"""保存标定结果到 JSON 文件"""
if not self.calibrated:
raise RuntimeError("请先执行 calibrate()")
data = {
"transform_matrix": self.transform_matrix.tolist(),
"pixel_points": self.pixel_points,
"world_points": self.world_points,
"error": self.calculate_error()
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"标定结果已保存: {filepath}")
@classmethod
def load(cls, filepath: str) -> 'NinePointCalibration':
"""从 JSON 文件加载标定结果"""
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
calib = cls()
calib.pixel_points = data["pixel_points"]
calib.world_points = data["world_points"]
calib.transform_matrix = np.array(data["transform_matrix"])
calib.calibrated = True
return calib
def detect_circles_opencv(image_path: str) -> List[Tuple[float, float]]:
"""
使用 OpenCV 霍夫圆检测提取标定板上的圆点中心
返回像素坐标列表
"""
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"无法读取图像: {image_path}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯模糊降噪
blurred = cv2.GaussianBlur(gray, (9, 9), 2)
# 霍夫圆检测
circles = cv2.HoughCircles(
blurred,
cv2.HOUGH_GRADIENT,
dp=1.2,
minDist=50,
param1=100,
param2=30,
minRadius=10,
maxRadius=50
)
if circles is None:
print("警告: 未检测到圆点")
return []
circles = np.uint16(np.around(circles))
points = [(float(c[0]), float(c[1])) for c in circles[0]]
# 按从上到下、从左到右排序
points.sort(key=lambda p: (p[1], p[0]))
# 在图像上绘制检测结果
for i, (x, y) in enumerate(points):
cv2.circle(img, (int(x), int(y)), 3, (0, 255, 0), -1)
cv2.putText(img, str(i+1), (int(x)+10, int(y)-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
output_path = image_path.replace('.', '_detected.')
cv2.imwrite(output_path, img)
print(f"检测结果已保存: {output_path}")
return points
def demo_calibration():
"""标定演示"""
print("=" * 60)
print("九点标定法演示")
print("=" * 60)
# 模拟标定数据
# 像素坐标(从图像中提取)
pixel_coords = [
(100, 100), (500, 100), (900, 100),
(100, 500), (500, 500), (900, 500),
(100, 900), (500, 900), (900, 900)
]
# 世界坐标(标定板上的实际位置,单位 mm)
world_coords = [
(0, 0), (20, 0), (40, 0),
(0, 20), (20, 20), (40, 20),
(0, 40), (20, 40), (40, 40)
]
# 创建标定对象
calib = NinePointCalibration()
# 添加标定点
for (px, py), (wx, wy) in zip(pixel_coords, world_coords):
calib.add_point(px, py, wx, wy)
# 执行标定
if not calib.calibrate():
return
# 计算误差
error = calib.calculate_error()
print(f"\n标定误差分析:")
print(f" 最大误差: {error['max_error']:.4f} mm")
print(f" 平均误差: {error['mean_error']:.4f} mm")
print(f" 标准差: {error['std_error']:.4f} mm")
print(f" RMS 误差: {error['rms_error']:.4f} mm")
# 逐点误差
print(f"\n逐点误差:")
for i, e in enumerate(error['per_point']):
status = "✓" if e < 0.1 else "✗"
print(f" 点 {i+1}: {e:.4f} mm {status}")
# 测试转换
print(f"\n转换测试:")
test_pixel = (500, 500)
world = calib.pixel_to_world(*test_pixel)
print(f" 像素 {test_pixel} → 世界 ({world[0]:.2f}, {world[1]:.2f}) mm")
test_world = (20, 20)
pixel = calib.world_to_pixel(*test_world)
print(f" 世界 {test_world} → 像素 ({pixel[0]:.2f}, {pixel[1]:.2f})")
# 保存标定结果
calib.save("calibration_result.json")
if __name__ == "__main__":
demo_calibration()
运行输出示例
============================================================
九点标定法演示
============================================================
添加点 1: 像素(100.0, 100.0) → 世界(0.0, 0.0)
添加点 2: 像素(500.0, 100.0) → 世界(20.0, 0.0)
...
添加点 9: 像素(900.0, 900.0) → 世界(40.0, 40.0)
标定完成!
变换矩阵:
[[ 0.05 0. -5. ]
[ 0. 0.05 -5. ]]
X_world = 0.050000 × x + 0.000000 × y + -5.000000
Y_world = 0.000000 × x + 0.050000 × y + -5.000000
标定误差分析:
最大误差: 0.0000 mm
平均误差: 0.0000 mm
标准差: 0.0000 mm
RMS 误差: 0.0000 mm
逐点误差:
点 1: 0.0000 mm ✓
...
点 9: 0.0000 mm ✓
转换测试:
像素 (500, 500) → 世界 (20.00, 20.00) mm
世界 (20, 20) → 像素 (500.00, 500.00)
精度验证与误差分析
误差来源
| 误差来源 | 典型值 | 改善措施 |
|---|---|---|
| 镜头畸变 | 0.5-2% | 使用远心镜头或畸变校正 |
| 标定板打印精度 | 0.1-0.3mm | 使用光刻标定板 |
| 圆点检测精度 | 0.1-0.5 像素 | 亚像素边缘检测 |
| 安装倾斜 | 0.1-0.5° | 使用水平仪校准 |
| 光照不均匀 | 0.2-0.5 像素 | 使用同轴光源 |
精度等级
| 等级 | RMS 误差 | 适用场景 |
|---|---|---|
| 高精度 | < 0.05 mm | 半导体、精密装配 |
| 中等精度 | 0.05-0.2 mm | 一般工业检测、定位 |
| 低精度 | 0.2-0.5 mm | 粗定位、分拣 |
| 不合格 | > 0.5 mm | 需重新标定 |
常见问题与排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 标定误差 > 0.5mm | 相机未垂直安装 | 用水平仪校准相机与标定板平行 |
| 边缘点误差大 | 镜头畸变 | 使用畸变校正或缩小视野 |
| 圆点检测不到 | 光照不足/过曝 | 调整光源亮度和角度 |
| 检测到多余圆点 | 背景干扰 | 增加 ROI 区域限制 |
| 标定后定位不准 | 机械回程差 | 从同一方向接近目标点 |
本文由「洵锋」原创,10 年工业自动化全栈工程师。机器视觉项目中最容易被忽视的就是标定精度,建议每次换型后重新标定。评论区欢迎交流实际项目中的视觉问题。
更多推荐

所有评论(0)