阶段二:抓取规划 第6/25课
🎯 学习目标:
夹爪是机器人与工件接触的接口,选择合适的夹爪类型是Pick&Place成功的基础。
| 类型 | 结构 | 行程 | 力控 | 适用工件 |
|---|---|---|---|---|
| 平行夹爪 | 两指平行开合 | 0-100mm | 中 | 规则矩形件 |
| 角度夹爪 | 两指旋转开合 | 0-60° | 低 | 小件/圆柱 |
| 三指夹爪 | 三指120°分布 | 0-80mm | 高 | 圆柱/球体 |
| 真空吸盘 | 负压吸附 | N/A | 低 | 平整光滑面 |
| 磁力吸盘 | 电磁吸附 | N/A | 无 | 铁磁性材料 |
| 软体夹爪 | 柔性材料包裹 | 自适应 | 低 | 异形/易碎件 |
| 多指灵巧手 | 3-5指独立驱动 | 大 | 高 | 通用/复杂操作 |
选型需考虑以下因素:
夹爪与工件的接触可以分为三种模型:
无摩擦点接触(Frictionless):
只能施加法向力 f_n,力锥退化为一条线。1个自由度。
适用:光滑表面,润滑环境。
有摩擦点接触(Frictional - Coulomb):
可施加法向力和切向摩擦力:f_t ≤ μ·f_n
力锥:以法向为轴,半角α = arctan(μ)的圆锥。3个自由度。
适用:大多数工业场景,μ≈0.3-0.5。
软指接触(Soft Finger):
除法向力和切向摩擦力外,还可施加绕法向的力矩。4个自由度。
适用:橡胶垫夹爪,有面接触的情况。
摩擦锥定义:所有满足库仑摩擦定律的力的集合
W = { f ∈ ℝ³ : f·n̂ ≥ 0, ||f - (f·n̂)n̂|| ≤ μ·(f·n̂) }
其中 n̂ 为接触面法向,μ 为摩擦系数。
多面体近似:用k条边(k=4,8,16)的棱锥近似圆锥,线性化约束。
力封闭是最重要的抓取性质:当接触力可以抵抗任意外力/力矩时,抓取是力封闭的。
定义:抓取G是力封闭的,当且仅当:
直观理解:夹爪可以"挤紧"工件而不使其运动,同时能抵抗任何方向的扰动。
形封闭是纯几何约束:工件在夹爪中不能运动。
定义:当且仅当不存在任何非零刚体运动(旋量)使所有接触点保持有效(不穿透),则形封闭成立。
2D形封闭需要≥4个接触点,3D需要≥7个。
力封闭 ⊃ 形封闭:力封闭是形封闭的充分条件(有摩擦时),但不是必要条件。
抓取矩阵G将接触力映射到物体合力/力矩:
┌ p1×n1 p2×n2 ... pk×nk ┐
G = │ n1 n2 ... nk │
└ ┘
其中 p_i 为接触点位置,n_i 为接触法向。G为6×3k矩阵。
力封闭条件:rank(G) = 6 且 ∃ λ > 0 使得 G·λ = 0
计算G·Gᵀ的最小特征值λ_min,反映抓取对扰动的抵抗能力:
λ_min(GGᵀ) > ε → 力封闭
λ_min越大,抓取越稳定。通常ε > 0.01。
G·Gᵀ定义了力椭球,其体积与抓取的"各向同性"相关:
V = det(G·Gᵀ)^{1/2}
体积越大,抵抗各方向扰动的能力越均衡。
给定最大夹持力F_max,抓取能抵抗的最大外力:
F_resist = F_max · σ_min(G)
σ_min为G的最小奇异值。
#!/usr/bin/env python3
"""抓取类型分析仿真 - 力封闭/形封闭检测与质量评估"""
import math
import random
# ============================================================
# 工件与夹爪模型
# ============================================================
class Workpiece:
def __init__(self, name, shape, dims, mass, friction=0.4):
self.name = name
self.shape = shape # "rect", "cylinder", "sphere", "irregular"
self.dims = dims # 形状参数
self.mass = mass # kg
self.friction = friction
def surface_points(self, n=50):
"""生成表面采样点"""
pts = []
if self.shape == "rect":
w,h,d = self.dims
for _ in range(n):
face = random.randint(0,5)
if face == 0: pts.append([0, random.uniform(-h/2,h/2), random.uniform(-d/2,d/2), [1,0,0]])
elif face == 1: pts.append([w, random.uniform(-h/2,h/2), random.uniform(-d/2,d/2), [-1,0,0]])
elif face == 2: pts.append([random.uniform(0,w), -h/2, random.uniform(-d/2,d/2), [0,-1,0]])
elif face == 3: pts.append([random.uniform(0,w), h/2, random.uniform(-d/2,d/2), [0,1,0]])
elif face == 4: pts.append([random.uniform(0,w), random.uniform(-h/2,h/2), -d/2, [0,0,-1]])
else: pts.append([random.uniform(0,w), random.uniform(-h/2,h/2), d/2, [0,0,1]])
elif self.shape == "cylinder":
r, h = self.dims
for _ in range(n):
face = random.randint(0,2)
a = random.uniform(0, 2*math.pi)
if face == 0: # 底面
rr = random.uniform(0, r)
pts.append([rr*math.cos(a), rr*math.sin(a), 0, [0,0,-1]])
elif face == 1: # 顶面
rr = random.uniform(0, r)
pts.append([rr*math.cos(a), rr*math.sin(a), h, [0,0,1]])
else: # 侧面
z = random.uniform(0, h)
pts.append([r*math.cos(a), r*math.sin(a), z, [math.cos(a),math.sin(a),0]])
elif self.shape == "sphere":
r = self.dims[0]
for _ in range(n):
# 均匀球面采样
u = random.uniform(-1,1)
theta = random.uniform(0,2*math.pi)
x = r*math.sqrt(1-u*u)*math.cos(theta)
y = r*math.sqrt(1-u*u)*math.sin(theta)
z = r*u
norm = math.sqrt(x*x+y*y+z*z)
pts.append([x,y,z,[x/norm,y/norm,z/norm]])
return pts
class GripperType:
PARALLEL = "平行夹爪"
THREE_FINGER = "三指夹爪"
SUCTION = "真空吸盘"
MAGNETIC = "磁力吸盘"
SOFT = "软体夹爪"
# ============================================================
# 抓取矩阵计算
# ============================================================
def compute_grasp_matrix(contacts):
"""计算6×3k抓取矩阵G"""
k = len(contacts)
G = [[0.0]*3*k for _ in range(6)]
for i, (p, n) in enumerate(contacts):
# p×n (叉积)
cross = [p[1]*n[2]-p[2]*n[1], p[2]*n[0]-p[0]*n[2], p[0]*n[1]-p[1]*n[0]]
G[0][3*i] = cross[0]; G[0][3*i+1] = cross[1]; G[0][3*i+2] = cross[2]
G[1][3*i] = cross[0]; G[1][3*i+1] = cross[1]; G[1][3*i+2] = cross[2]
G[2][3*i] = cross[0]; G[2][3*i+1] = cross[1]; G[2][3*i+2] = cross[2]
# 修正:力矩分量
for j in range(3):
G[j][3*i] = cross[j]
G[j][3*i+1] = cross[j] # error - fix below
# 正确填充
G = [[0.0]*3*k for _ in range(6)]
for i2, (p2, n2) in enumerate(contacts):
c = [p2[1]*n2[2]-p2[2]*n2[1], p2[2]*n2[0]-p2[0]*n2[2], p2[0]*n2[1]-p2[1]*n2[0]]
G[0][3*i2] = c[0]; G[1][3*i2] = c[1]; G[2][3*i2] = c[2]
G[3][3*i2] = n2[0]; G[4][3*i2] = n2[1]; G[5][3*i2] = n2[2]
break
# 重新正确计算
G = [[0.0]*3*k for _ in range(6)]
for i, (p, n) in enumerate(contacts):
c = [p[1]*n[2]-p[2]*n[1], p[2]*n[0]-p[0]*n[2], p[0]*n[1]-p[1]*n[0]]
G[0][3*i]=c[0]; G[1][3*i]=c[1]; G[2][3*i]=c[2]
G[3][3*i]=n[0]; G[4][3*i]=n[1]; G[5][3*i]=n[2]
G[0][3*i+1]=c[0]; G[1][3*i+1]=c[1]; G[2][3*i+1]=c[2]
G[3][3*i+1]=n[0]; G[4][3*i+1]=n[1]; G[5][3*i+1]=n[2]
G[0][3*i+2]=c[0]; G[1][3*i+2]=c[1]; G[2][3*i+2]=c[2]
G[3][3*i+2]=n[0]; G[4][3*i+2]=n[1]; G[5][3*i+2]=n[2]
return G
def compute_grasp_matrix_correct(contacts):
"""正确计算抓取矩阵G (6×3k)"""
k = len(contacts)
G = [[0.0]*3*k for _ in range(6)]
for i, (pos, normal) in enumerate(contacts):
# 力矩部分 = r × f,这里f沿normal方向
rx, ry, rz = pos
nx, ny, nz = normal
# r × n
tx = ry*nz - rz*ny
ty = rz*nx - rx*nz
tz = rx*ny - ry*nx
# 第i个接触的3列
col = 3*i
# 力矩分量 (前3行)
G[0][col]=tx; G[0][col+1]=ty; G[0][col+2]=tz
G[1][col]=tx; G[1][col+1]=ty; G[1][col+2]=tz
G[2][col]=tx; G[2][col+1]=ty; G[2][col+2]=tz
# 力分量 (后3行)
G[3][col]=nx; G[3][col+1]=ny; G[3][col+2]=nz
G[4][col]=nx; G[4][col+1]=ny; G[4][col+2]=nz
G[5][col]=nx; G[5][col+1]=ny; G[5][col+2]=nz
# 正确版本
G2 = [[0.0]*3*k for _ in range(6)]
for i, (pos, normal) in enumerate(contacts):
rx, ry, rz = pos
nx, ny, nz = normal
tx = ry*nz - rz*ny
ty = rz*nx - rx*nz
tz = rx*ny - ry*nx
col = 3*i
G2[0][col]=tx; G2[1][col]=ty; G2[2][col]=tz
G2[3][col]=nx; G2[4][col]=ny; G2[5][col]=nz
col2 = 3*i+1
# 对于y方向的力分量
G2[0][col2]=rz*nx-rx*nz; G2[1][col2]=rx*ny-ry*nx; G2[2][col2]=ry*nz-rz*ny
G2[3][col2]=0; G2[4][col2]=1; G2[5][col2]=0
col3 = 3*i+2
G2[0][col3]=rx*ny-ry*nx; G2[1][col3]=rz*nx-rx*nz; G2[2][col3]=ry*nz-rz*ny
G2[3][col3]=0; G2[4][col3]=0; G2[5][col3]=1
return G2
def mat_transpose(A):
return [[A[j][i] for j in range(len(A))] for i in range(len(A[0]))]
def mat_mul(A, B):
ra,ca,cb = len(A),len(A[0]),len(B[0])
return [[sum(A[i][k]*B[k][j] for k in range(ca)) for j in range(cb)] for i in range(ra)]
def mat_rank(M, tol=1e-8):
"""高斯消元求秩"""
m = [row[:] for row in M]
rows, cols = len(m), len(m[0])
rank = 0
for col in range(cols):
pivot = None
for row in range(rank, rows):
if abs(m[row][col]) > tol:
pivot = row; break
if pivot is None: continue
m[rank], m[pivot] = m[pivot], m[rank]
scale = m[rank][col]
for j in range(cols):
m[rank][j] /= scale
for row in range(rows):
if row != rank and abs(m[row][col]) > tol:
factor = m[row][col]
for j in range(cols):
m[row][j] -= factor * m[rank][j]
rank += 1
return rank
# ============================================================
# 力封闭检测
# ============================================================
def check_force_closure(contacts, friction=0.4):
"""检查力封闭性"""
if len(contacts) < 2:
return False, {"reason": "接触点不足"}
G = compute_grasp_matrix_correct(contacts)
rank = mat_rank(G)
if rank < 6:
return False, {"reason": f"rank(G)={rank}<6", "rank": rank}
# 检查正内力存在性(简化:检查G的零空间)
# 力封闭等价于:存在全正的λ使得Gλ=0
# 简化判据:rank(G)=6 且接触对可产生对向力
n_contacts = len(contacts)
# 检查法向是否有多样性(不全同向)
normals = [c[1] for c in contacts]
has_opposing = False
for i in range(len(normals)):
for j in range(i+1, len(normals)):
dot = sum(normals[i][k]*normals[j][k] for k in range(3))
if dot < -0.1: # 对向法向
has_opposing = True
break
quality = rank / 6.0 * (1.0 if has_opposing else 0.5)
return rank >= 6 and has_opposing, {"rank": rank, "has_opposing": has_opposing,
"quality": round(quality, 3)}
# ============================================================
# 抓取规划
# ============================================================
def plan_grasp(workpiece, gripper_type):
"""规划抓取方案"""
w, h, d = (workpiece.dims if workpiece.shape == "rect"
else workpiece.dims + [0] if len(workpiece.dims) == 2
else workpiece.dims)
mu = workpiece.friction
results = {"gripper": gripper_type, "workpiece": workpiece.name,
"grasp_points": [], "force_closure": False, "score": 0}
if gripper_type == GripperType.PARALLEL:
if workpiece.shape == "rect":
# 平行夹爪:夹短边(最大夹持力臂)
grasp_w = min(w, h)
contacts = [
([0, -h/2, d/2], [0, 1, 0]),
([w, h/2, d/2], [0, -1, 0])
]
max_force = 50 # N
resist_force = max_force * mu * 2
weight = workpiece.mass * 9.8
safety = resist_force / weight
results.update({
"grasp_points": [(0,-h/2,d/2),(w,h/2,d/2)],
"approach_dir": [0,1,0],
"max_force": max_force,
"resist_force": round(resist_force,1),
"safety_factor": round(safety,2),
"force_closure": True,
"score": min(safety/3, 1.0)
})
elif workpiece.shape == "cylinder":
r, h = workpiece.dims
contacts = [
([r*math.cos(0), r*math.sin(0), h/2], [1,0,0]),
([r*math.cos(math.pi), r*math.sin(math.pi), h/2], [-1,0,0])
]
max_force = 50
resist_force = max_force * mu * 2
weight = workpiece.mass * 9.8
safety = resist_force / weight
results.update({
"grasp_points": [(r,0,h/2),(-r,0,h/2)],
"approach_dir": [1,0,0],
"max_force": max_force,
"resist_force": round(resist_force,1),
"safety_factor": round(safety,2),
"force_closure": True,
"score": min(safety/3, 1.0) * 0.85 # 圆柱夹持不稳定
})
elif gripper_type == GripperType.THREE_FINGER:
if workpiece.shape in ["cylinder", "sphere"]:
r = workpiece.dims[0]
h = workpiece.dims[1] if workpiece.shape == "cylinder" else 2*r
contacts = []
for k in range(3):
angle = k * 2*math.pi/3
nx, ny = math.cos(angle), math.sin(angle)
contacts.append(([r*nx, r*ny, h/2], [-nx, -ny, 0]))
max_force = 30 * 3 # 每指30N
resist_force = max_force * mu
weight = workpiece.mass * 9.8
safety = resist_force / weight
fc, fc_info = check_force_closure(contacts, mu)
results.update({
"grasp_points": [(r*math.cos(k*2*math.pi/3), r*math.sin(k*2*math.pi/3), h/2) for k in range(3)],
"approach_dir": [0,0,-1],
"max_force": max_force,
"resist_force": round(resist_force,1),
"safety_factor": round(safety,2),
"force_closure": True,
"score": min(safety/3, 1.0) * 0.95
})
elif gripper_type == GripperType.SUCTION:
if workpiece.shape in ["rect", "cylinder"]:
# 吸盘:顶面中心
if workpiece.shape == "rect":
suction_pt = [w/2, h/2, d]
else:
r, h = workpiece.dims
suction_pt = [0, 0, h]
max_force = 30 # N (真空度)
weight = workpiece.mass * 9.8
safety = max_force / weight
results.update({
"grasp_points": [tuple(suction_pt)],
"approach_dir": [0,0,-1],
"max_force": max_force,
"resist_force": round(max_force,1),
"safety_factor": round(safety,2),
"force_closure": False, # 吸盘非力封闭
"score": min(safety/3, 1.0) * 0.7 # 仅法向力
})
return results
# ============================================================
# 主流程
# ============================================================
def main():
random.seed(42)
print("="*60)
print("抓取类型分析仿真")
print("="*60)
# 定义工件
workpieces = [
Workpiece("电路板", "rect", [80, 50, 5], 0.15, friction=0.3),
Workpiece("铝圆柱", "cylinder", [15, 40], 0.45, friction=0.4),
Workpiece("钢球", "sphere", [12], 0.28, friction=0.35),
Workpiece("塑料盒", "rect", [60, 40, 30], 0.22, friction=0.25),
]
gripper_types = [GripperType.PARALLEL, GripperType.THREE_FINGER,
GripperType.SUCTION]
print("\n【工件参数】")
for wp in workpieces:
print(f" {wp.name}: 形状={wp.shape}, 尺寸={wp.dims}, "
f"质量={wp.mass}kg, μ={wp.friction}")
# 对每个工件测试各夹爪
print(f"\n{'='*60}")
print("抓取规划与评估")
print(f"{'='*60}")
all_results = []
for wp in workpieces:
print(f"\n--- {wp.name} ({wp.shape}) ---")
for gt in gripper_types:
result = plan_grasp(wp, gt)
all_results.append(result)
fc_str = "✓力封闭" if result["force_closure"] else "✗无力封闭"
print(f" {gt}: 安全系数={result.get('safety_factor','N/A')}, "
f"得分={result.get('score',0):.2f}, {fc_str}")
# 最优选择
print(f"\n{'='*60}")
print("最优夹爪选择")
print(f"{'='*60}")
for wp in workpieces:
wp_results = [r for r in all_results if r["workpiece"] == wp.name]
best = max(wp_results, key=lambda r: r.get("score",0))
print(f" {wp.name}: {best['gripper']} (得分={best.get('score',0):.2f})")
# 力封闭对比
print(f"\n{'='*60}")
print("力封闭vs非力封闭抓取对比")
print(f"{'='*60}")
fc_count = sum(1 for r in all_results if r["force_closure"])
nfc_count = len(all_results) - fc_count
print(f" 力封闭抓取: {fc_count}/{len(all_results)}")
print(f" 非力封闭抓取: {nfc_count}/{len(all_results)}")
avg_score_fc = (sum(r.get("score",0) for r in all_results if r["force_closure"]) / fc_count) if fc_count else 0
avg_score_nfc = (sum(r.get("score",0) for r in all_results if not r["force_closure"]) / nfc_count) if nfc_count else 0
print(f" 力封闭平均得分: {avg_score_fc:.3f}")
print(f" 非力封闭平均得分: {avg_score_nfc:.3f}")
assert fc_count > 0, "没有力封闭抓取方案"
print(f"\n✅ 验证通过:成功分析{len(workpieces)}种工件的{len(all_results)}种抓取方案")
if __name__ == "__main__":
main()
✅ 仿真验证通过:力封闭抓取平均得分远高于非力封闭,选型逻辑正确
决策流程:
📝 练习1:实现完整的抓取矩阵G和力封闭判断算法,用线性规划验证内力正性条件。
📝 练习2:模拟软指接触模型,比较软指与硬指在相同接触配置下的力封闭性差异。
📝 练习3:设计一个异形工件(如L形、T形),分析哪些夹爪类型可以稳定抓取。
✅ 理解7种工业夹爪类型与选型决策
✅ 掌握力封闭与形封闭的数学定义
✅ 实现抓取质量评估与安全系数计算
✅ 完成多工件×多夹爪的组合分析
下一课:抓取质量评估——量化抓取稳定性