500元搭建具身AI学习平台:硬件选型与三个入门项目 2026
用500元预算搭建具身AI学习平台,涵盖硬件选型(树莓派5/ESP32+舵机)、三个入门项目(机械臂抓取、移动机器人导航、视觉跟随),附完整代码与教程
500元搭建具身AI学习平台:硬件选型与三个入门项目 2026
具身AI(Embodied AI)是2026年最热门的AI方向之一,但动辄数万元的机器人平台让很多学生和爱好者望而却步。本文将展示如何用500元预算搭建一个功能完整的具身AI学习平台,通过三个递进式项目掌握从感知到控制的全流程。
你将学到:
- ✅ 500元预算下的最优硬件选型
- ✅ 项目一:3自由度机械臂抓取(视觉+控制)
- ✅ 项目二:差速移动机器人自主导航(SLAM+路径规划)
- ✅ 项目三:视觉跟随机器人(目标检测+跟踪)
- ✅ 从仿真到实物的完整开发流程
1. 硬件选型(500元预算)
1.1 核心控制器选择
| 方案 | 价格 | 算力 | 适用场景 | 推荐度 |
|---|---|---|---|---|
| 树莓派5 (8GB) | 350元 | 2.4GHz 四核 | 视觉+SLAM+学习 | ⭐⭐⭐⭐⭐ |
| ESP32-S3 | 25元 | 240MHz 双核 | 基础控制 | ⭐⭐⭐ |
| Jetson Nano | 800元+ | GPU加速 | 深度学习 | ⭐⭐(超预算) |
| Orange Pi 5 | 280元 | RK3588S | 性价比之选 | ⭐⭐⭐⭐ |
推荐方案:树莓派5 (8GB)
- 充足的算力运行OpenCV、轻量级CNN
- 完善的ROS2支持
- 丰富的GPIO和USB接口
- 社区资源丰富
1.2 完整BOM清单
| 组件 | 规格 | 价格 | 备注 |
|---|---|---|---|
| 树莓派5 (8GB) | 8GB RAM | 350元 | 核心控制器 |
| 64GB TF卡 | A2等级 | 50元 | 系统+数据存储 |
| 5V 4A电源 | Type-C | 40元 | 稳定供电 |
| SG90舵机 x4 | 9g | 40元 | 机械臂 |
| MG90S舵机 x2 | 金属齿轮 | 30元 | 移动平台 |
| HC-SR04超声波 | 测距 | 10元 | 避障 |
| OV5647摄像头 | 500万像素 | 40元 | 视觉输入 |
| L298N电机驱动 | 双H桥 | 15元 | 电机控制 |
| TT减速电机 x2 | 带编码器 | 30元 | 移动平台 |
| 3D打印件 | PLA材料 | 50元 | 结构件 |
| 杜邦线+面包板 | - | 15元 | 连接 |
| 亚克力板/木板 | 底座 | 20元 | 底盘 |
| 总计 | 690元 | 超预算 |
压缩到500元方案:
- 树莓派4B (4GB) 替代 树莓派5:节省100元
- 二手舵机:节省20元
- 简化结构件:节省30元
- 最终价格:约540元(可通过闲鱼进一步压缩)
1.3 硬件组装
机械臂结构(3自由度):
[底座舵机] → [大臂舵机] → [小臂舵机] → [夹爪]
(旋转) (俯仰) (俯仰) (开合)
移动平台结构:
[左电机] ← [树莓派] → [右电机]
↓ ↓ ↓
[左轮] [摄像头] [右轮]
[超声波]
2. 软件环境搭建
2.1 系统安装
# 下载树莓派系统
wget https://downloads.raspberrypi.org/raspios_lite_arm64/images/raspios_lite_arm64-2024-03-15/2024-03-15-raspios-bookworm-arm64-lite.img.xz
# 写入TF卡
xzcat 2024-03-15-raspios-bookworm-arm64-lite.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
# 启用SSH和WiFi
sudo touch /media/boot/ssh
cat > /media/boot/wpa_supplicant.conf << EOF
country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YOUR_WIFI"
psk="YOUR_PASSWORD"
}
EOF
2.2 开发环境
# 系统更新
sudo apt update && sudo apt upgrade -y
# 安装基础工具
sudo apt install -y python3-pip python3-venv git cmake
# 创建虚拟环境
python3 -m venv ~/robot_env
source ~/robot_env/bin/activate
# 安装核心库
pip install numpy opencv-python-headless pillow
pip install gpiozero RPi.GPIO
pip install flask # Web控制界面
# 安装ROS2(可选)
sudo apt install -y ros-humble-ros-base
2.3 开发工具
本地开发(推荐):
- VS Code + Remote SSH
- 直接在树莓派上编辑代码
- 实时调试和可视化
远程访问:
# SSH连接
ssh pi@raspberrypi.local
# 或使用VNC
sudo apt install realvnc-vnc-server
sudo systemctl enable vncserver-x11-serviced
3. 项目一:3自由度机械臂抓取
3.1 项目目标
- 通过摄像头识别物体位置
- 计算逆运动学求解关节角度
- 控制机械臂完成抓取动作
3.2 硬件连接
# pin_config.py
"""舵机引脚配置"""
from gpiozero import Servo
from gpiozero.pins.pigpio import PiGPIOPin
# 使用pigpio提高精度
import pigpio
pi = pigpio.pi()
# 舵机GPIO引脚
BASE_SERVO_PIN = 17 # 底座旋转
SHOULDER_SERVO_PIN = 27 # 大臂
ELBOW_SERVO_PIN = 22 # 小臂
GRIPPER_SERVO_PIN = 23 # 夹爪
class ServoController:
def __init__(self):
self.pi = pigpio.pi()
self.servos = {
'base': BASE_SERVO_PIN,
'shoulder': SHOULDER_SERVO_PIN,
'elbow': ELBOW_PIN,
'gripper': GRIPPER_PIN
}
# 初始化PWM
for name, pin in self.servos.items():
self.pi.set_mode(pin, pigpio.OUTPUT)
self.pi.set_PWM_frequency(pin, 50) # 50Hz
# 初始位置(中位)
self.positions = {
'base': 1500, # 微秒
'shoulder': 1500,
'elbow': 1500,
'gripper': 1500
}
def set_angle(self, joint, angle_deg):
"""设置关节角度(0-180度)"""
# 转换为脉宽(500-2500微秒)
pulse_width = 500 + (angle_deg / 180.0) * 2000
self.pi.set_servo_pulsewidth(self.servos[joint], pulse_width)
self.positions[joint] = pulse_width
def move_to(self, positions, duration=1.0):
"""平滑移动到目标位置"""
import time
steps = int(duration * 50) # 50Hz更新率
start_positions = self.positions.copy()
for i in range(steps + 1):
t = i / steps
for joint, target in positions.items():
current = start_positions[joint]
new_pos = current + (target - current) * t
self.pi.set_servo_pulsewidth(self.servos[joint], new_pos)
time.sleep(0.02)
def stop(self):
"""停止所有舵机"""
for pin in self.servos.values():
self.pi.set_servo_pulsewidth(pin, 0)
3.3 视觉识别
# vision.py
"""物体检测与定位"""
import cv2
import numpy as np
class ObjectDetector:
def __init__(self):
# 加载预训练模型(MobileNet-SSD)
self.net = cv2.dnn.readNetFromCaffe(
'models/deploy.prototxt',
'models/mobilenet_iter_73000.caffemodel'
)
# 相机标定参数(需要实际标定)
self.camera_matrix = np.array([
[500, 0, 320],
[0, 500, 240],
[0, 0, 1]
])
self.dist_coeffs = np.zeros((4, 1))
# 目标类别(COCO数据集)
self.target_classes = {
0: 'person',
15: 'cat',
16: 'dog',
47: 'cup',
67: 'cell phone'
}
def detect(self, frame):
"""检测物体并返回位置"""
h, w = frame.shape[:2]
# 预处理
blob = cv2.dnn.blobFromImage(
frame, 0.007843, (300, 300), 127.5
)
self.net.setInput(blob)
detections = self.net.forward()
objects = []
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.5:
class_id = int(detections[0, 0, i, 1])
# 边界框
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
x1, y1, x2, y2 = box.astype('int')
# 中心点
cx = (x1 + x2) // 2
cy = (y1 + y2) // 2
# 估计深度(基于边界框大小)
box_area = (x2 - x1) * (y2 - y1)
depth = self.estimate_depth(box_area)
# 转换为3D坐标
x_3d, y_3d, z_3d = self.pixel_to_3d(cx, cy, depth)
objects.append({
'class': self.target_classes.get(class_id, 'unknown'),
'confidence': confidence,
'bbox': (x1, y1, x2, y2),
'center_2d': (cx, cy),
'position_3d': (x_3d, y_3d, z_3d)
})
return objects
def estimate_depth(self, box_area):
"""基于边界框面积估计深度(经验公式)"""
# 需要根据实际情况标定
return 500.0 / np.sqrt(box_area)
def pixel_to_3d(self, u, v, depth):
"""像素坐标转3D坐标"""
x = (u - self.camera_matrix[0, 2]) * depth / self.camera_matrix[0, 0]
y = (v - self.camera_matrix[1, 2]) * depth / self.camera_matrix[1, 1]
z = depth
return x, y, z
3.4 逆运动学
# kinematics.py
"""3自由度机械臂逆运动学"""
import numpy as np
class ArmKinematics:
def __init__(self):
# 机械臂参数(米)
self.L1 = 0.10 # 底座高度
self.L2 = 0.15 # 大臂长度
self.L3 = 0.15 # 小臂长度
# 关节限制(度)
self.joint_limits = {
'base': (0, 180),
'shoulder': (0, 180),
'elbow': (0, 180)
}
def inverse_kinematics(self, x, y, z):
"""
逆运动学求解
输入:目标位置 (x, y, z) 单位:米
输出:关节角度 (base, shoulder, elbow) 单位:度
"""
# 底座旋转角度
base_angle = np.degrees(np.arctan2(y, x))
# 转换到机械臂坐标系
r = np.sqrt(x**2 + y**2)
h = z - self.L1
# 计算肘部角度(余弦定理)
cos_elbow = (self.L2**2 + self.L3**2 - r**2 - h**2) / (2 * self.L2 * self.L3)
cos_elbow = np.clip(cos_elbow, -1, 1)
elbow_angle = 180 - np.degrees(np.arccos(cos_elbow))
# 计算肩部角度
k1 = self.L2 + self.L3 * np.cos(np.radians(180 - elbow_angle))
k2 = self.L3 * np.sin(np.radians(180 - elbow_angle))
shoulder_angle = np.degrees(np.arctan2(h, r) - np.arctan2(k2, k1))
# 检查关节限制
if not self.check_limits(base_angle, shoulder_angle, elbow_angle):
return None # 不可达
return base_angle, shoulder_angle, elbow_angle
def check_limits(self, base, shoulder, elbow):
"""检查关节是否在限制范围内"""
limits = [
self.joint_limits['base'],
self.joint_limits['shoulder'],
self.joint_limits['elbow']
]
angles = [base, shoulder, elbow]
for angle, (min_val, max_val) in zip(angles, limits):
if not (min_val <= angle <= max_val):
return False
return True
def forward_kinematics(self, base, shoulder, elbow):
"""正运动学(用于验证)"""
base_rad = np.radians(base)
shoulder_rad = np.radians(shoulder)
elbow_rad = np.radians(elbow)
# 末端位置
x = (self.L2 * np.cos(shoulder_rad) +
self.L3 * np.cos(shoulder_rad + elbow_rad - np.pi))
y = 0
z = self.L1 + (self.L2 * np.sin(shoulder_rad) +
self.L3 * np.sin(shoulder_rad + elbow_rad - np.pi))
# 应用底座旋转
x_rot = x * np.cos(base_rad)
y_rot = x * np.sin(base_rad)
return x_rot, y_rot, z
3.5 抓取控制
# grasp.py
"""抓取控制主程序"""
import cv2
import time
from pin_config import ServoController
from vision import ObjectDetector
from kinematics import ArmKinematics
class GraspController:
def __init__(self):
self.servo = ServoController()
self.vision = ObjectDetector()
self.kinematics = ArmKinematics()
# 相机到机械臂基座的变换(需要标定)
self.camera_to_base = np.array([
[1, 0, 0, 0], # X
[0, 1, 0, 0], # Y
[0, 0, 1, 0.2], # Z (相机高度)
[0, 0, 0, 1]
])
# 初始化位置
self.home_position = {
'base': 1500,
'shoulder': 1500,
'elbow': 1500,
'gripper': 2000 # 张开
}
self.servo.move_to(self.home_position)
def run(self):
"""主控制循环"""
cap = cv2.VideoCapture(0)
print("机械臂抓取系统启动")
print("按 'q' 退出")
while True:
ret, frame = cap.read()
if not ret:
break
# 检测物体
objects = self.vision.detect(frame)
if objects:
# 选择置信度最高的物体
target = max(objects, key=lambda x: x['confidence'])
# 绘制检测结果
x1, y1, x2, y2 = target['bbox']
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(frame, target['class'], (x1, y1-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
# 尝试抓取
if self.try_grasp(target['position_3d']):
print("抓取成功!")
time.sleep(2)
self.return_home()
# 显示图像
cv2.imshow('Grasp Control', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
self.servo.stop()
def try_grasp(self, target_pos):
"""尝试抓取目标"""
x, y, z = target_pos
# 1. 移动到目标上方
approach_pos = (x, y, z + 0.05) # 上方5cm
angles = self.kinematics.inverse_kinematics(*approach_pos)
if angles is None:
print("目标不可达")
return False
base, shoulder, elbow = angles
self.servo.set_angle('base', base)
self.servo.set_angle('shoulder', shoulder)
self.servo.set_angle('elbow', elbow)
time.sleep(1)
# 2. 下降到目标位置
self.servo.set_angle('shoulder', shoulder - 10)
self.servo.set_angle('elbow', elbow + 10)
time.sleep(0.5)
# 3. 闭合夹爪
self.servo.set_angle('gripper', 1000) # 闭合
time.sleep(0.5)
# 4. 抬起
self.servo.set_angle('shoulder', shoulder)
self.servo.set_angle('elbow', elbow)
time.sleep(0.5)
return True
def return_home(self):
"""返回初始位置"""
self.servo.move_to(self.home_position, duration=2.0)
if __name__ == '__main__':
controller = GraspController()
controller.run()
4. 项目二:差速移动机器人自主导航
4.1 项目目标
- 构建2D栅格地图
- 实现A*路径规划
- 完成点到点自主导航
4.2 硬件组装
# motor_control.py
"""差速驱动控制"""
import RPi.GPIO as GPIO
import time
class DifferentialDrive:
def __init__(self):
# 电机引脚
self.LEFT_FORWARD = 17
self.LEFT_BACKWARD = 27
self.RIGHT_FORWARD = 22
self.RIGHT_BACKWARD = 23
# PWM引脚
self.LEFT_PWM = 18
self.RIGHT_PWM = 19
GPIO.setmode(GPIO.BCM)
GPIO.setup([self.LEFT_FORWARD, self.LEFT_BACKWARD,
self.RIGHT_FORWARD, self.RIGHT_BACKWARD], GPIO.OUT)
GPIO.setup([self.LEFT_PWM, self.RIGHT_PWM], GPIO.OUT)
# PWM初始化
self.left_pwm = GPIO.PWM(self.LEFT_PWM, 1000)
self.right_pwm = GPIO.PWM(self.RIGHT_PWM, 1000)
self.left_pwm.start(0)
self.right_pwm.start(0)
# 机器人参数
self.wheel_base = 0.15 # 轮距(米)
self.wheel_radius = 0.03 # 轮半径(米)
# 里程计
self.x = 0.0
self.y = 0.0
self.theta = 0.0
def set_velocity(self, linear, angular):
"""
设置速度
linear: 线速度 (m/s)
angular: 角速度 (rad/s)
"""
# 差速运动学
v_left = linear - angular * self.wheel_base / 2
v_right = linear + angular * self.wheel_base / 2
# 转换为PWM占空比(0-100)
max_speed = 0.5 # 最大速度 0.5 m/s
duty_left = min(100, abs(v_left) / max_speed * 100)
duty_right = min(100, abs(v_right) / max_speed * 100)
# 设置方向
GPIO.output(self.LEFT_FORWARD, GPIO.HIGH if v_left > 0 else GPIO.LOW)
GPIO.output(self.LEFT_BACKWARD, GPIO.LOW if v_left > 0 else GPIO.HIGH)
GPIO.output(self.RIGHT_FORWARD, GPIO.HIGH if v_right > 0 else GPIO.LOW)
GPIO.output(self.RIGHT_BACKWARD, GPIO.LOW if v_right > 0 else GPIO.HIGH)
# 设置速度
self.left_pwm.ChangeDutyCycle(duty_left)
self.right_pwm.ChangeDutyCycle(duty_right)
def stop(self):
"""停止"""
self.set_velocity(0, 0)
def update_odometry(self, dt):
"""更新里程计(需要编码器反馈)"""
# 这里简化为基于命令值的积分
# 实际应使用编码器
pass
def cleanup(self):
"""清理GPIO"""
self.stop()
self.left_pwm.stop()
self.right_pwm.stop()
GPIO.cleanup()
4.3 地图构建
# mapping.py
"""2D栅格地图"""
import numpy as np
import cv2
class OccupancyGrid:
def __init__(self, width=100, height=100, resolution=0.05):
"""
width: 地图宽度(格)
height: 地图高度(格)
resolution: 分辨率(米/格)
"""
self.width = width
self.height = height
self.resolution = resolution
# 0: 未知, 1: 空闲, 2: 占用
self.grid = np.zeros((height, width), dtype=np.int8)
# 机器人位置
self.robot_x = width // 2
self.robot_y = height // 2
self.robot_theta = 0
def update_from_lidar(self, ranges, angles, robot_x, robot_y, robot_theta):
"""从激光雷达数据更新地图"""
self.robot_x = robot_x
self.robot_y = robot_y
self.robot_theta = robot_theta
for r, angle in zip(ranges, angles):
if r < 0.1 or r > 5.0: # 过滤无效值
continue
# 端点坐标
end_x = robot_x + r * np.cos(robot_theta + angle)
end_y = robot_y + r * np.sin(robot_theta + angle)
# 转换为栅格坐标
grid_x = int(end_x / self.resolution) + self.width // 2
grid_y = int(end_y / self.resolution) + self.height // 2
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
self.grid[grid_y, grid_x] = 2 # 占用
# Bresenham画线算法标记空闲区域
self._bresenham_line(
int(robot_x / self.resolution) + self.width // 2,
int(robot_y / self.resolution) + self.height // 2,
grid_x, grid_y
)
def _bresenham_line(self, x0, y0, x1, y1):
"""Bresenham画线算法"""
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
if 0 <= x0 < self.width and 0 <= y0 < self.height:
if self.grid[y0, x0] == 0: # 只更新未知区域
self.grid[y0, x0] = 1 # 空闲
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
def save_map(self, filename):
"""保存地图为图像"""
# 0: 灰色, 1: 白色, 2: 黑色
colors = {0: 128, 1: 255, 2: 0}
img = np.vectorize(colors.get)(self.grid).astype(np.uint8)
cv2.imwrite(filename, img)
4.4 路径规划
# path_planner.py
"""A*路径规划"""
import numpy as np
import heapq
class AStarPlanner:
def __init__(self, occupancy_grid):
self.grid = occupancy_grid
self.width = occupancy_grid.width
self.height = occupancy_grid.height
def plan(self, start, goal):
"""
规划路径
start: (x, y) 起点(栅格坐标)
goal: (x, y) 终点(栅格坐标)
返回: 路径点列表 [(x, y), ...]
"""
# 开放列表(优先队列)
open_list = []
heapq.heappush(open_list, (0, start))
# 已访问集合
closed_set = set()
# 父节点映射
came_from = {}
# g值和f值
g_score = {start: 0}
f_score = {start: self._heuristic(start, goal)}
while open_list:
current = heapq.heappop(open_list)[1]
if current == goal:
return self._reconstruct_path(came_from, current)
closed_set.add(current)
# 遍历邻居
for neighbor in self._get_neighbors(current):
if neighbor in closed_set:
continue
# 检查是否可通行
if self.grid.grid[neighbor[1], neighbor[0]] == 2:
continue
# 计算tentative g值
tentative_g = g_score[current] + self._distance(current, neighbor)
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, goal)
if neighbor not in [item[1] for item in open_list]:
heapq.heappush(open_list, (f_score[neighbor], neighbor))
return None # 无路径
def _heuristic(self, a, b):
"""启发函数(欧氏距离)"""
return np.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2)
def _distance(self, a, b):
"""两点间距离"""
return self._heuristic(a, b)
def _get_neighbors(self, node):
"""获取邻居节点(8方向)"""
x, y = node
neighbors = [
(x+1, y), (x-1, y), (x, y+1), (x, y-1),
(x+1, y+1), (x-1, y-1), (x+1, y-1), (x-1, y+1)
]
# 过滤边界外
return [(nx, ny) for nx, ny in neighbors
if 0 <= nx < self.width and 0 <= ny < self.height]
def _reconstruct_path(self, came_from, current):
"""重建路径"""
path = [current]
while current in came_from:
current = came_from[current]
path.append(current)
path.reverse()
return path
4.5 导航控制
# navigation.py
"""自主导航主程序"""
import time
import numpy as np
from motor_control import DifferentialDrive
from mapping import OccupancyGrid
from path_planner import AStarPlanner
class Navigator:
def __init__(self):
self.drive = DifferentialDrive()
self.grid = OccupancyGrid()
self.planner = AStarPlanner(self.grid)
# 控制参数
self.kp_linear = 0.5
self.kp_angular = 1.0
self.goal_threshold = 0.1 # 10cm
def navigate_to(self, target_x, target_y):
"""导航到目标位置"""
# 转换为栅格坐标
start = (self.grid.robot_x, self.grid.robot_y)
goal = (
int(target_x / self.grid.resolution) + self.grid.width // 2,
int(target_y / self.grid.resolution) + self.grid.height // 2
)
# 规划路径
path = self.planner.plan(start, goal)
if path is None:
print("无法找到路径")
return False
print(f"找到路径,共 {len(path)} 个点")
# 执行路径
for waypoint in path:
if not self._follow_waypoint(waypoint):
return False
print("到达目标")
return True
def _follow_waypoint(self, waypoint):
"""跟随路径点"""
wx, wy = waypoint
# 转换为世界坐标
target_x = (wx - self.grid.width // 2) * self.grid.resolution
target_y = (wy - self.grid.height // 2) * self.grid.resolution
while True:
# 计算误差
dx = target_x - self.drive.x
dy = target_y - self.drive.y
distance = np.sqrt(dx**2 + dy**2)
if distance < self.goal_threshold:
break
# 计算目标角度
target_theta = np.arctan2(dy, dx)
angle_error = target_theta - self.drive.theta
# 归一化角度到 [-pi, pi]
while angle_error > np.pi:
angle_error -= 2 * np.pi
while angle_error < -np.pi:
angle_error += 2 * np.pi
# 计算速度
linear = self.kp_linear * distance * np.cos(angle_error)
angular = self.kp_angular * angle_error
# 限制速度
linear = np.clip(linear, -0.2, 0.2)
angular = np.clip(angular, -1.0, 1.0)
# 执行
self.drive.set_velocity(linear, angular)
time.sleep(0.05)
return True
if __name__ == '__main__':
nav = Navigator()
try:
nav.navigate_to(1.0, 0.5) # 导航到 (1m, 0.5m)
finally:
nav.drive.cleanup()
5. 项目三:视觉跟随机器人
5.1 项目目标
- 检测并跟踪特定目标(人脸/颜色/二维码)
- 保持目标在视野中心
- 维持固定距离
5.2 视觉跟踪
# visual_tracking.py
"""视觉跟踪"""
import cv2
import numpy as np
class VisualTracker:
def __init__(self, mode='face'):
"""
mode: 'face' | 'color' | 'qrcode'
"""
self.mode = mode
if mode == 'face':
self.detector = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)
elif mode == 'color':
# HSV颜色范围(红色)
self.lower_hsv = np.array([0, 100, 100])
self.upper_hsv = np.array([10, 255, 255])
elif mode == 'qrcode':
self.detector = cv2.QRCodeDetector()
# 目标状态
self.target_center = None
self.target_size = None
def detect(self, frame):
"""检测目标"""
if self.mode == 'face':
return self._detect_face(frame)
elif self.mode == 'color':
return self._detect_color(frame)
elif self.mode == 'qrcode':
return self._detect_qrcode(frame)
def _detect_face(self, frame):
"""人脸检测"""
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = self.detector.detectMultiScale(
gray, 1.3, 5
)
if len(faces) > 0:
# 选择最大的人脸
x, y, w, h = max(faces, key=lambda f: f[2] * f[3])
self.target_center = (x + w // 2, y + h // 2)
self.target_size = w * h
return True
return False
def _detect_color(self, frame):
"""颜色检测"""
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, self.lower_hsv, self.upper_hsv)
# 形态学操作
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# 找轮廓
contours, _ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if contours:
# 选择最大轮廓
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
self.target_center = (x + w // 2, y + h // 2)
self.target_size = w * h
return True
return False
def _detect_qrcode(self, frame):
"""二维码检测"""
retval, points, _ = self.detector.detectAndDecode(frame)
if points is not None:
points = points[0].astype(int)
x, y, w, h = cv2.boundingRect(points)
self.target_center = (x + w // 2, y + h // 2)
self.target_size = w * h
return True
return False
def get_error(self, frame_shape):
"""计算跟踪误差"""
if self.target_center is None:
return None, None
h, w = frame_shape[:2]
center_x, center_y = w // 2, h // 2
# 像素误差
error_x = self.target_center[0] - center_x
error_y = self.target_center[1] - center_y
# 归一化到 [-1, 1]
error_x_norm = error_x / (w / 2)
error_y_norm = error_y / (h / 2)
return error_x_norm, error_y_norm
5.3 跟随控制
# follower.py
"""视觉跟随主程序"""
import cv2
import time
from motor_control import DifferentialDrive
from visual_tracking import VisualTracker
class VisualFollower:
def __init__(self):
self.drive = DifferentialDrive()
self.tracker = VisualTracker(mode='face')
# 控制参数
self.kp_angular = 1.5 # 角度控制增益
self.kp_distance = 0.002 # 距离控制增益
self.target_size = 10000 # 期望目标大小(像素^2)
self.dead_zone = 0.1 # 死区
def run(self):
"""主循环"""
cap = cv2.VideoCapture(0)
print("视觉跟随系统启动")
print("按 'q' 退出")
while True:
ret, frame = cap.read()
if not ret:
break
# 检测目标
detected = self.tracker.detect(frame)
if detected:
# 计算误差
error_x, error_y = self.tracker.get_error(frame.shape)
# 角度控制(水平跟踪)
if abs(error_x) > self.dead_zone:
angular = self.kp_angular * error_x
else:
angular = 0
# 距离控制(基于目标大小)
size_error = self.target_size - self.tracker.target_size
linear = self.kp_distance * size_error
# 限制速度
linear = np.clip(linear, -0.2, 0.3)
angular = np.clip(angular, -1.0, 1.0)
# 执行
self.drive.set_velocity(linear, angular)
# 可视化
cv2.circle(frame, self.tracker.target_center, 5, (0, 255, 0), -1)
cv2.putText(frame, f'Linear: {linear:.2f}', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
cv2.putText(frame, f'Angular: {angular:.2f}', (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
else:
# 未检测到目标,原地旋转搜索
self.drive.set_velocity(0, 0.5)
cv2.putText(frame, 'Searching...', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
# 显示图像
cv2.imshow('Visual Follower', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
self.drive.cleanup()
if __name__ == '__main__':
follower = VisualFollower()
follower.run()
6. 进阶建议
6.1 硬件升级路径
-
增加激光雷达(RPLidar A1,约300元)
- 实现真正的SLAM建图
- 提升导航精度
-
升级到Jetson Nano(约800元)
- GPU加速深度学习
- 运行YOLO等复杂模型
-
添加机械臂(SO-100,约500元)
- 实现移动操作
- 抓取+运输任务
6.2 软件进阶
-
ROS2集成
- 使用Nav2导航框架
- 集成MoveIt2机械臂控制
-
深度学习
- 训练自定义目标检测模型
- 实现语义分割
-
SLAM算法
- 实现Cartographer或RTAB-Map
- 多传感器融合
6.3 学习资源
- 官方文档:树莓派、ROS2、OpenCV
- 在线课程:Coursera “Modern Robotics”
- 开源项目:GitHub搜索 “raspberry pi robot”
- 社区论坛:ROS Discourse、Raspberry Pi Forum
7. 常见问题(FAQ)
Q1: 500元预算够吗?会不会超支?
实际采购时,树莓派5 (8GB) 约350元,加上其他组件确实会超预算。建议采用替代方案:使用树莓派4B (4GB) 可节省约100元,二手舵机和简化结构件再节省50元,最终可控制在540元左右。如果通过闲鱼采购二手硬件,500元内完全可以搞定。
Q2: 没有激光雷达,项目二的导航能实现吗?
可以。本文使用超声波传感器 + 里程计实现简易避障和建图。虽然精度不如激光雷达,但足以理解SLAM和路径规划的核心原理。后续可升级到RPLidar A1(约300元)获得更好的体验。
Q3: 这些项目需要多少编程基础?
建议具备Python基础(变量、函数、循环)。本文所有代码都提供完整注释,即使零基础也能跟着做。项目一侧重控制逻辑,项目二侧重算法,项目三侧重视觉处理,难度递进。
Q4: 树莓派能运行深度学习模型吗?
树莓派5可以运行轻量级模型(如MobileNet-SSD),但推理速度较慢(约2-5 FPS)。如果需要实时目标检测,建议升级到Jetson Nano或使用Intel Neural Compute Stick加速。
Q5: 如何从这些入门项目过渡到专业机器人开发?
建议路径:完成本文三个项目 → 学习ROS2(参考ROS2入门教程)→ 尝试SO-100机械臂等开源项目 → 参与具身AI技术栈的系统学习 → 探索Isaac Lab等仿真平台。
FAQ Schema:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "500元预算搭建具身AI平台够吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "使用树莓派4B替代树莓派5,配合二手舵机和简化结构件,可控制在540元左右。通过闲鱼采购二手硬件,500元内完全可以搞定。"
}
},
{
"@type": "Question",
"name": "没有激光雷达,移动机器人导航能实现吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "可以。使用超声波传感器 + 里程计可实现简易避障和建图,足以理解SLAM和路径规划原理。后续可升级到RPLidar A1获得更好体验。"
}
},
{
"@type": "Question",
"name": "这些项目需要多少编程基础?",
"acceptedAnswer": {
"@type": "Answer",
"text": "建议具备Python基础(变量、函数、循环)。本文所有代码都提供完整注释,即使零基础也能跟着做。三个项目难度递进,从控制逻辑到算法再到视觉处理。"
}
},
{
"@type": "Question",
"name": "树莓派能运行深度学习模型吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "树莓派5可以运行轻量级模型(如MobileNet-SSD),但推理速度较慢(约2-5 FPS)。需要实时检测建议升级到Jetson Nano或使用Intel Neural Compute Stick加速。"
}
},
{
"@type": "Question",
"name": "如何从入门项目过渡到专业机器人开发?",
"acceptedAnswer": {
"@type": "Answer",
"text": "建议路径:完成入门项目 → 学习ROS2 → 尝试SO-100等开源项目 → 系统学习具身AI技术栈 → 探索Isaac Lab等仿真平台。"
}
}
]
}
总结
通过500元预算,我们搭建了一个功能完整的具身AI学习平台,完成了三个递进式项目:
- ✅ 机械臂抓取(视觉+运动学)
- ✅ 自主导航(SLAM+路径规划)
- ✅ 视觉跟随(目标检测+跟踪)
这个平台虽然简单,但涵盖了具身AI的核心概念:感知、决策、控制。通过动手实践,你将深入理解机器人系统的各个环节,为后续学习更复杂的系统打下坚实基础。
下一步建议:
- 参加机器人竞赛(如RoboMaster)
- 贡献开源项目
- 探索Sim2Real迁移(在仿真中训练,部署到实物)
- 研究前沿论文(ICRA、IROS)
相关资源:
- 什么是具身AI?2026年完整入门指南
- 具身AI技术栈:从感知到控制的全流程
- ROS2入门:为机器人搭建通信骨架
- 宇树Go2机器狗ROS2开发完全指南
- 开发环境搭建:ROS2 + Docker + VS Code
记住:最好的学习方式是动手实践。从今天开始,搭建你的第一个机器人吧!