🔍 机器人感知与传感器¶
学习时间:4小时 | 难度:⭐⭐⭐ 中级 | 前置知识:计算机视觉基础、深度学习基础
本章目标¶
- 掌握机器人视觉感知的核心技术栈(检测、分割、位姿估计)
- 理解深度感知与3D点云处理方法
- 了解触觉传感器原理与力估计
- 掌握多传感器融合与SLAM基础
- 学会使用开放词汇检测实现通用物体理解
1. 视觉感知¶
1.1 RGB视觉处理流水线¶
机器人视觉与传统CV的区别: - 实时性要求高:控制频率通常10-100Hz,检测延迟直接影响操作 - 精度要求高:抓取误差>5mm就可能失败 - 三维理解为核心:需要从2D图像推断3D世界信息
1.2 目标检测(YOLO系列)¶
Python
from ultralytics import YOLO
# 加载预训练模型
model = YOLO("yolov8n.pt") # YOLOv8 nano版,适合实时场景
# 推理
results = model("robot_workspace.jpg")
# 提取检测结果用于机器人决策
for r in results:
for box in r.boxes:
cls_name = model.names[int(box.cls)]
x1, y1, x2, y2 = box.xyxy[0].tolist()
conf = float(box.conf)
# 计算物体中心(像素坐标)
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
print(f"检测到 {cls_name} (置信度{conf:.2f}) 中心: ({cx:.0f}, {cy:.0f})")
1.3 语义分割(SAM2)¶
Python
from sam2.build_sam import build_sam2
from sam2.sam2_image_predictor import SAM2ImagePredictor
import numpy as np
# SAM2 分割任意物体(需要模型配置文件 + 权重文件)
sam2_model = build_sam2("sam2.1_hiera_l.yaml", "sam2.1_hiera_large.pt")
predictor = SAM2ImagePredictor(sam2_model)
predictor.set_image(image)
# 给定一个点提示 → 分割出对应物体
masks, scores, logits = predictor.predict(
point_coords=np.array([[cx, cy]]), # 检测框中心
point_labels=np.array([1]), # 1=前景
multimask_output=True
)
best_mask = masks[np.argmax(scores)] # 取最高分的mask
1.4 6DoF位姿估计¶
物体的6自由度位姿 = 3D位置(x,y,z) + 3D旋转(roll,pitch,yaw)
Python
import numpy as np
class PoseEstimator:
"""基于深度图 + 检测框的简化位姿估计"""
def __init__(self, camera_intrinsics):
"""
camera_intrinsics: 相机内参矩阵 K (3x3)
[[fx, 0, cx],
[0, fy, cy],
[0, 0, 1]]
"""
self.K = camera_intrinsics
self.fx = camera_intrinsics[0, 0]
self.fy = camera_intrinsics[1, 1]
self.cx = camera_intrinsics[0, 2]
self.cy = camera_intrinsics[1, 2]
def pixel_to_3d(self, u, v, depth):
"""像素坐标 + 深度 → 3D相机坐标"""
z = depth
x = (u - self.cx) * z / self.fx
y = (v - self.cy) * z / self.fy
return np.array([x, y, z])
def estimate_grasp_pose(self, mask, depth_image):
"""从分割mask和深度图估计抓取位姿"""
# 获取mask区域的深度值
masked_depth = depth_image[mask > 0]
median_depth = np.median(masked_depth[masked_depth > 0])
# 计算mask质心
ys, xs = np.where(mask > 0)
cu, cv = xs.mean(), ys.mean()
# 转换为3D坐标
position = self.pixel_to_3d(cu, cv, median_depth)
return {
'position': position, # [x, y, z] 米
'approach_direction': np.array([0, 0, -1]), # 默认从上方接近
'grasp_width': self._estimate_width(xs, median_depth)
}
def _estimate_width(self, xs, depth):
"""根据mask宽度估计物体尺寸"""
pixel_width = xs.max() - xs.min()
real_width = pixel_width * depth / self.fx
return real_width
2. 深度感知与3D处理¶
2.1 深度相机对比¶
| 传感器 | 原理 | 精度 | 范围 | 适用场景 |
|---|---|---|---|---|
| Intel RealSense D435 | 立体视觉 | ±2mm@1m | 0.2-10m | 桌面操作 |
| Azure Kinect DK | ToF | ±5mm@1m | 0.5-5m | 全身感知 |
| Livox LiDAR | 旋转镜 | ±2cm | 1-200m | 移动机器人 |
| 结构光 | 编码投射 | ±0.1mm | 0.3-1.5m | 高精度抓取 |
2.2 点云处理(Open3D)¶
Python
import open3d as o3d
import numpy as np
def process_point_cloud(depth_image, rgb_image, intrinsics):
"""从深度图和RGB图生成彩色点云"""
# 创建Open3D相机内参
pinhole = o3d.camera.PinholeCameraIntrinsic(
width=640, height=480,
fx=intrinsics[0, 0], fy=intrinsics[1, 1],
cx=intrinsics[0, 2], cy=intrinsics[1, 2]
)
# 从RGBD图像创建点云
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
o3d.geometry.Image(rgb_image),
o3d.geometry.Image(depth_image.astype(np.float32)),
depth_scale=1000.0, # mm → m
depth_trunc=2.0, # 截断距离
convert_rgb_to_intensity=False
)
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, pinhole)
# 下采样 + 去噪
pcd = pcd.voxel_down_sample(voxel_size=0.005) # 5mm体素下采样
pcd, inliers = pcd.remove_statistical_outlier(
nb_neighbors=20, std_ratio=2.0
)
# 平面分割(移除桌面)
plane_model, plane_inliers = pcd.segment_plane(
distance_threshold=0.01, ransac_n=3, num_iterations=1000
)
objects_pcd = pcd.select_by_index(plane_inliers, invert=True)
# 聚类分割(分离不同物体)
labels = np.array(objects_pcd.cluster_dbscan(
eps=0.02, min_points=50
))
return objects_pcd, labels
def extract_object_features(pcd, label, labels):
"""提取单个物体的几何特征"""
obj_pcd = pcd.select_by_index(np.where(labels == label)[0])
# 包围盒
obb = obj_pcd.get_oriented_bounding_box()
# 中心点
center = obb.center
# 尺寸(长宽高)
extent = obb.extent # [length, width, height]
# 法向量估计
obj_pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.02, max_nn=30)
)
return {
'center': center,
'size': extent,
'bbox': obb,
'num_points': len(obj_pcd.points)
}
2.3 PointNet/PointNet++(深度学习处理点云)¶
Python
import torch
import torch.nn as nn
class PointNetEncoder(nn.Module): # 继承nn.Module定义网络层
"""PointNet: 直接处理无序点云的深度学习模型"""
def __init__(self, in_channels=3, global_feat_dim=1024):
super().__init__() # super()调用父类方法
# 逐点MLP(共享参数)
self.mlp1 = nn.Sequential(
nn.Conv1d(in_channels, 64, 1),
nn.BatchNorm1d(64), nn.ReLU(),
nn.Conv1d(64, 128, 1),
nn.BatchNorm1d(128), nn.ReLU(),
nn.Conv1d(128, global_feat_dim, 1),
nn.BatchNorm1d(global_feat_dim), nn.ReLU()
)
def forward(self, x):
"""
x: (B, N, 3) 点云坐标
return: (B, global_feat_dim) 全局特征
"""
x = x.transpose(1, 2) # (B, 3, N)
x = self.mlp1(x) # (B, 1024, N)
x = torch.max(x, dim=2)[0] # 全局max pooling → (B, 1024)
return x
# PointNet的关键创新:
# 1. 对称函数(max pooling)实现点云置换不变性
# 2. 共享MLP(1x1卷积)逐点提取特征
# 3. T-Net学习输入/特征对齐变换
3. 触觉感知¶
3.1 触觉传感器类型¶
| 传感器 | 原理 | 分辨率 | 应用 |
|---|---|---|---|
| GelSight | 弹性凝胶+相机 | 亚毫米级 | 纹理识别、形状感知 |
| DIGIT | 改良GelSight | 高 | Meta开源触觉传感器 |
| BioTac | 仿生指尖 | 中等 | 力/温度/振动 |
| 力矩传感器 | 应变片 | 力/力矩 | 力控抓取 |
3.2 触觉反馈控制¶
Python
class TactileFeedbackController:
"""基于触觉反馈的力控抓取(PID控制)"""
def __init__(self, target_force=5.0, kp=0.01, ki=0.001, kd=0.002,
dt=0.01, integral_limit=100.0, derivative_filter_N=10):
self.target_force = target_force # 目标抓取力(N)
self.kp = kp # 比例增益
self.ki = ki # 积分增益
self.kd = kd # 微分增益
self.dt = dt # 控制周期(秒)
self.integral_limit = integral_limit # 积分限幅
self.N = derivative_filter_N # 微分滤波系数
self.error_integral = 0.0
self.prev_force = 0.0 # 上一次力测量值
self.prev_derivative = 0.0 # 上一次滤波微分值
def compute_grip_adjustment(self, current_force):
"""
PID控制器:根据当前力反馈调整夹爪开合量
- 比例项: 响应误差
- 积分项: 消除稳态误差(含抗饱和限幅)
- 微分项: 对测量值微分 + 一阶低通滤波抑制传感器噪声
"""
error = self.target_force - current_force
# 积分项(含抗饱和限幅)
self.error_integral += error * self.dt
# 限幅防积分饱和:当积分项超限时停止累积
self.error_integral = max(-self.integral_limit,
min(self.integral_limit, self.error_integral))
# 微分项:对测量值微分(避免设定值突变冲击)+ 低通滤波
raw_deriv = -(current_force - self.prev_force) / self.dt
alpha = self.N * self.dt / (1.0 + self.N * self.dt)
filtered_deriv = alpha * raw_deriv + (1 - alpha) * self.prev_derivative
# PID输出
adjustment = (self.kp * error
+ self.ki * self.error_integral
+ self.kd * filtered_deriv)
# 更新状态
self.prev_force = current_force
self.prev_derivative = filtered_deriv
return adjustment # 正值=收紧,负值=松开
def grasp_with_feedback(self, gripper, tactile_sensor, max_steps=100):
"""闭环力控抓取"""
for step in range(max_steps):
force = tactile_sensor.read_force()
if abs(force - self.target_force) < 0.5: # 容差0.5N
print(f"抓取稳定: {force:.1f}N (目标{self.target_force}N)")
return True
adj = self.compute_grip_adjustment(force)
gripper.move_relative(adj)
return False
4. 多传感器融合¶
4.1 卡尔曼滤波(传感器融合基础)¶
Python
import numpy as np
class KalmanFilter:
"""简化版卡尔曼滤波,用于融合IMU和视觉定位"""
def __init__(self, state_dim, obs_dim):
self.x = np.zeros(state_dim) # 状态向量
self.P = np.eye(state_dim) * 1.0 # 状态协方差
self.Q = np.eye(state_dim) * 0.01 # 过程噪声
self.R = np.eye(obs_dim) * 0.1 # 观测噪声
self.F = np.eye(state_dim) # 状态转移矩阵
self.H = np.eye(obs_dim, state_dim) # 观测矩阵
def predict(self, u=None):
"""预测步骤(使用运动模型)"""
self.x = self.F @ self.x
if u is not None:
self.x += u # 控制输入
self.P = self.F @ self.P @ self.F.T + self.Q
def update(self, z):
"""更新步骤(使用传感器观测)"""
y = z - self.H @ self.x # 观测残差
S = self.H @ self.P @ self.H.T + self.R # 残差协方差
K = self.P @ self.H.T @ np.linalg.inv(S) # 卡尔曼增益
self.x = self.x + K @ y # 状态更新
I = np.eye(len(self.x))
self.P = (I - K @ self.H) @ self.P # 协方差更新
# 使用示例:融合IMU加速度和视觉位置
kf = KalmanFilter(state_dim=6, obs_dim=3) # [x,y,z,vx,vy,vz] 状态, [x,y,z] 观测
# 设置运动模型(匀速模型, dt=0.01s)
dt = 0.01
kf.F = np.array([
[1, 0, 0, dt, 0, 0],
[0, 1, 0, 0, dt, 0],
[0, 0, 1, 0, 0, dt],
[0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1]
])
kf.H = np.eye(3, 6) # 只观测位置
# 循环
for imu_accel, visual_position in sensor_stream:
kf.predict(u=np.concatenate([np.zeros(3), imu_accel * dt]))
if visual_position is not None: # 视觉更新频率低于IMU
kf.update(visual_position)
fused_pose = kf.x[:3] # 融合后的位置估计
4.2 SLAM概述¶
Text Only
视觉SLAM流水线:
前端(帧间跟踪) 后端(全局优化)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 特征提取 │────→│ 特征匹配 │────→│ 位姿估计 │────→│ 图优化 │
│ (ORB/ │ │ (最近邻) │ │ (PnP/ICP)│ │(g2o/GTSAM)│
│ SuperPt)│ └──────────┘ └──────────┘ └──────────┘
└──────────┘ │
↑ ↓
┌──────────┐ ┌──────────┐
│ 回环检测 │←────────────────────────────────────────│ 地图构建 │
│ (DBoW2) │ │ (点云/网格)│
└──────────┘ └──────────┘
主流SLAM系统:
- ORB-SLAM3: 视觉/视惯SLAM经典方案
- LIO-SAM: LiDAR-惯性SLAM
- RTAB-Map: RGB-D SLAM,适合室内
- Neural SLAM: 基于NeRF/3DGS的学习式SLAM
5. 开放词汇感知¶
5.1 Grounding DINO + SAM = 通用物体检测+分割¶
Python
# 机器人语言指令 → 视觉定位 → 分割
# 例:人说"把红色的杯子拿给我"
from groundingdino.util.inference import load_model, predict
from segment_anything import sam_model_registry, SamPredictor
class OpenVocabPerception:
"""开放词汇感知:理解任意文本描述的物体"""
def __init__(self):
self.grounding_model = load_model(
"GroundingDINO_SwinB_cfg.py",
"groundingdino_swinb_cogcoor.pth"
)
sam = sam_model_registry["vit_h"](
checkpoint="sam_vit_h.pth"
)
self.sam_predictor = SamPredictor(sam)
def detect_and_segment(self, image, text_prompt,
box_threshold=0.3, text_threshold=0.25):
"""
image: RGB图像 (H, W, 3)
text_prompt: 自然语言描述, 如 "red cup"
返回: 检测框列表 + 分割mask列表
"""
# Step 1: Grounding DINO 定位
boxes, logits, phrases = predict(
model=self.grounding_model,
image=image,
caption=text_prompt,
box_threshold=box_threshold,
text_threshold=text_threshold
)
# Step 2: SAM 精细分割
self.sam_predictor.set_image(image)
masks = []
for box in boxes:
mask, score, _ = self.sam_predictor.predict(
box=box.numpy(),
multimask_output=False
)
masks.append(mask[0])
return boxes, masks, phrases
# 使用
perception = OpenVocabPerception()
boxes, masks, labels = perception.detect_and_segment(
image=camera.capture(),
text_prompt="red cup . blue bottle . white plate"
)
# → 机器人现在知道每个物体的精确位置和形状
5.2 为什么开放词汇感知对机器人至关重要?¶
传统方法需要预定义类别(只能识别训练过的物体),而现实世界中: - 家庭场景物体种类几乎无限 - 用户不可能只让机器人操作固定物体 - VLA模型需要通用的视觉理解能力
6. 练习题¶
代码实践¶
- 入门:使用YOLOv8检测桌面上的物体,输出每个物体的3D位置(需要深度相机或估计深度)
- 进阶:用Open3D处理点云——平面分割+物体聚类+最小包围盒
- 高级:实现Grounding DINO + SAM的Pipeline,输入"the object closest to the robot"
面试题¶
- 为什么机器人通常不直接使用ImageNet预训练模型?(域差距:光照、遮挡、物体种类不同)
- 对比RGB相机和深度相机在机械臂抓取中的作用
- 卡尔曼滤波中Q和R矩阵如何调参?实际效果是什么?
- 为什么PointNet用max pooling而不是mean pooling?
- 解释SLAM中回环检测的作用
最后更新:2026年2月