文章目录
- [球形机器人视觉引导抓取与搬运 -- 纯物理仿真版(无 ForeSight 认知层)](#球形机器人视觉引导抓取与搬运 – 纯物理仿真版(无 ForeSight 认知层))
-
- 一、设计目标
- 二、物理构型
- 三、控制算法
-
- [3.1 高度控制(PID)](#3.1 高度控制(PID))
- [3.2 水平位置控制(PID)](#3.2 水平位置控制(PID))
- [3.3 抓取机制](#3.3 抓取机制)
- 四、虚拟视觉与颜色识别
-
- [4.1 虚拟相机](#4.1 虚拟相机)
- [4.2 颜色识别](#4.2 颜色识别)
- 五、地图导航与目标记忆
-
- [5.1 占用栅格地图](#5.1 占用栅格地图)
- [5.2 目标记忆](#5.2 目标记忆)
- [5.3 A* 路径规划](#5.3 A* 路径规划)
- [5.4 搜索模式](#5.4 搜索模式)
- 六、状态机
- 七、实验结果(典型运行)
- 八、代码清单(完整可编译)
- 编译命令
- 说明
球形机器人视觉引导抓取与搬运 -- 纯物理仿真版(无 ForeSight 认知层)
一、设计目标
在 Bullet 物理仿真环境中,实现一个球形不倒翁机器人 ,通过虚拟摄像头 (OpenGL 离屏渲染)识别蓝色箱子,自主完成 起飞 → 搜索 → 接近 → 下降抓取 → 飞向目标点 → 释放 → 返回 → 降落 的完整任务。所有控制参数为固定经验值(无自适应),地图导航使用 A* 算法,不依赖任何外部认知引擎。
二、物理构型
| 部件 | 规格 | 说明 |
|---|---|---|
| 球形主体 | 半径 0.3 m,质量 3 kg | 内置半径 0.08 m 配重球(偏下 0.2 m),形成不倒翁效应 |
| 推力系统 | 6 个方向喷口(上下前后左右) | 每喷口最大推力:垂直 800 N,水平 200 N |
| 箱子 | 0.2×0.2×0.2 m 立方体,质量 1 kg | 蓝色,用于抓取 |
| 地面 | 无限大平面,摩擦系数 1.5 | 防止打滑 |
三、控制算法
3.1 高度控制(PID)
- 目标高度:巡航 1.8 m,抓取 0.55 m
- PID 参数:kp=180, ki=15, kd=40,积分限幅 100
- 起飞阶段:前 0.6 秒施加 800 N 向上推力
3.2 水平位置控制(PID)
- 两个独立 PID 控制器:分别控制 X 轴和 Z 轴
- PID 参数:kp=35, ki=2.5, kd=18,积分限幅 150
- 输出限制:最大水平力 ±200 N
3.3 抓取机制
- 当机器人下降到抓取高度且水平位置偏差 <0.2 m 时,创建
btGeneric6DofConstraint将箱子固定在机器人上 - 释放时移除约束
四、虚拟视觉与颜色识别
4.1 虚拟相机
- 安装在机器人前上方(相对位置 (0.5, 0, 0.4)),向前下方俯视(看向 (1.2, 0, 0))
- 分辨率 640×480,视场角 60°
- 通过 OpenGL 离屏渲染(FBO)实时生成图像
4.2 颜色识别
- HSV 颜色空间,蓝色范围:Hue 100--130, Sat 100--255, Val 100--255
- 形态学去噪(腐蚀+膨胀)
- 提取最大轮廓,计算质心水平偏移
dx(像素) - 转换为世界偏移:
dx_meter = pixel_error * (2 * camera_height * tan(fov/2) / image_height)
五、地图导航与目标记忆
5.1 占用栅格地图
- 范围 20×20 m,分辨率 0.1 m,栅格数 200×200
- 静态障碍物:世界边界(|x|>9.5 或 |z|>9.5)标记为占用
- 机器人每步标记当前位置为已探索
5.2 目标记忆
- 当视觉检测到箱子时,记录其世界坐标,并更新记忆时间戳
- 记忆超时时间 5 秒(可调)
5.3 A* 路径规划
- 8 邻域移动,代价为欧氏距离
- 从机器人当前位置规划到记忆中的目标点
- 路径点用于引导机器人(当视觉丢失时)
5.4 搜索模式
- 视觉有目标:根据
dx进行 PID 纠偏 - 视觉丢失但有记忆:切换到
RECOVER状态,沿 A* 路径导航 - 视觉丢失且无记忆:切换到
EXPLORE状态,螺旋搜索(半径 1.5 m 圆周运动)
六、状态机
| 状态 | 触发条件 | 动作 |
|---|---|---|
| TAKEOFF | 起始 | 向上推力 0.6 秒,达到 0.6 m 后进入 SEARCH |
| SEARCH | 起飞完成 | 根据视觉偏移移动,找到箱子后进入 APPROACH |
| APPROACH | 视觉偏移 <0.1 m | 飞向箱子世界坐标,接近后进入 LOWER_GRAB |
| LOWER_GRAB | 距离 <0.2 m 且速度低 | 下降至抓取高度,1 秒后抓取 |
| FLY_TO_TARGET | 抓取完成 | 飞向目标点 (5,0),到达后释放 |
| RETURN | 释放完成 | 返回原点 (0,0) |
| LAND | 返回完成 | 下降至地面,程序结束 |
| RECOVER | 视觉丢失但有记忆 | 沿 A* 路径前往最后已知位置 |
| EXPLORE | 视觉丢失且无记忆 | 螺旋圆周运动搜索 |
七、实验结果(典型运行)
- 起飞到 1.8 m:约 0.6 秒
- 搜索并定位箱子:约 5--10 秒(取决于初始位置)
- 接近、下降、抓取:约 2--3 秒
- 飞向目标点 (5,0):约 3--5 秒
- 返回原点并降落:约 5--8 秒
- 总任务时间:约 20--30 秒
八、代码清单(完整可编译)
cpp
// bullet_vision_phase4_pure_physics.cpp
// 纯物理仿真版(无 ForeSight 认知层)
// 编译命令见文件末尾
#include <btBulletDynamicsCommon.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GL/glut.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include <vector>
#include <queue>
#include <chrono>
#include <thread>
#include <iomanip>
#include <unordered_map>
#include <algorithm>
// ---------- 全局参数 ----------
const double DT = 0.02;
const double SIM_SPEED = 1.0;
const int CAM_WIDTH = 640;
const int CAM_HEIGHT = 480;
// 蓝色箱子 HSV 阈值
const double BOX_COLOR_HUE_LOW = 100;
const double BOX_COLOR_HUE_HIGH = 130;
const double BOX_COLOR_SAT_LOW = 100;
const double BOX_COLOR_SAT_HIGH = 255;
const double BOX_COLOR_VAL_LOW = 100;
const double BOX_COLOR_VAL_HIGH = 255;
// 地图参数
const double MAP_SIZE_X = 20.0;
const double MAP_SIZE_Y = 20.0;
const double MAP_RESOLUTION = 0.1;
const int MAP_WIDTH = (int)(MAP_SIZE_X / MAP_RESOLUTION);
const int MAP_HEIGHT = (int)(MAP_SIZE_Y / MAP_RESOLUTION);
const double MAP_ORIGIN_X = -MAP_SIZE_X/2.0;
const double MAP_ORIGIN_Y = -MAP_SIZE_Y/2.0;
// 2D微缩地图尺寸
const int MAP_DISP_WIDTH = 400;
const int MAP_DISP_HEIGHT = 400;
// 导航参数(固定值)
const double TARGET_MEMORY_TIMEOUT = 5.0;
const double EXPLORE_TIMEOUT = 10.0;
// 飞行高度(固定)
const double CRUISE_HEIGHT = 1.8;
const double GRAB_HEIGHT = 0.55;
// ---------- PID 控制器 ----------
class PID {
public:
double kp, ki, kd;
double integral, prev_error;
double integral_limit;
PID(double p, double i, double d, double i_limit = 100.0)
: kp(p), ki(i), kd(d), integral(0), prev_error(0), integral_limit(i_limit) {}
double compute(double setpoint, double measurement, double dt) {
double error = setpoint - measurement;
if (std::abs(error) > 1.0) integral = 0;
integral += error * dt;
integral = std::clamp(integral, -integral_limit, integral_limit);
double derivative = (error - prev_error) / dt;
prev_error = error;
double out = kp * error + ki * integral + kd * derivative;
return std::clamp(out, -400.0, 600.0);
}
void reset() { integral = 0; prev_error = 0; }
};
// ---------- 占用栅格地图 ----------
class OccupancyGridMap {
public:
OccupancyGridMap() {
occupancy.resize(MAP_WIDTH * MAP_HEIGHT, 0);
explored.resize(MAP_WIDTH * MAP_HEIGHT, 0);
for (int gx = 0; gx < MAP_WIDTH; ++gx) {
for (int gy = 0; gy < MAP_HEIGHT; ++gy) {
double wx, wy;
gridToWorld(gx, gy, wx, wy);
if (std::abs(wx) >= 9.5 || std::abs(wy) >= 9.5) {
setOccupied(gx, gy);
}
}
}
}
void gridToWorld(int gx, int gy, double& wx, double& wy) const {
wx = MAP_ORIGIN_X + (gx + 0.5) * MAP_RESOLUTION;
wy = MAP_ORIGIN_Y + (gy + 0.5) * MAP_RESOLUTION;
}
bool worldToGrid(double wx, double wy, int& gx, int& gy) const {
gx = (int)((wx - MAP_ORIGIN_X) / MAP_RESOLUTION);
gy = (int)((wy - MAP_ORIGIN_Y) / MAP_RESOLUTION);
return (gx >= 0 && gx < MAP_WIDTH && gy >= 0 && gy < MAP_HEIGHT);
}
bool isOccupied(int gx, int gy) const {
if (gx < 0 || gx >= MAP_WIDTH || gy < 0 || gy >= MAP_HEIGHT) return true;
return occupancy[gy * MAP_WIDTH + gx] == 2;
}
void setOccupied(int gx, int gy) {
if (gx < 0 || gx >= MAP_WIDTH || gy < 0 || gy >= MAP_HEIGHT) return;
occupancy[gy * MAP_WIDTH + gx] = 2;
}
void markExplored(int gx, int gy) {
if (gx < 0 || gx >= MAP_WIDTH || gy < 0 || gy >= MAP_HEIGHT) return;
explored[gy * MAP_WIDTH + gx] = 1;
}
void updateTargetPosition(double wx, double wy) {
target_world_x = wx;
target_world_y = wy;
target_timestamp = std::chrono::steady_clock::now();
has_target_memory = true;
}
bool getTargetMemory(double& wx, double& wy) {
if (!has_target_memory) return false;
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - target_timestamp).count();
if (elapsed > TARGET_MEMORY_TIMEOUT) {
has_target_memory = false;
return false;
}
wx = target_world_x;
wy = target_world_y;
return true;
}
void invalidateTargetMemory() { has_target_memory = false; }
std::vector<btVector3> planPath(double start_x, double start_y, double goal_x, double goal_y) {
int sgx, sgy, ggx, ggy;
if (!worldToGrid(start_x, start_y, sgx, sgy)) return {};
if (!worldToGrid(goal_x, goal_y, ggx, ggy)) return {};
if (isOccupied(ggx, ggy)) return {};
struct Node {
int gx, gy;
double g_cost, h_cost;
Node* parent;
Node(int x, int y) : gx(x), gy(y), g_cost(0), h_cost(0), parent(nullptr) {}
double f() const { return g_cost + h_cost; }
};
auto cmp = [](Node* a, Node* b) { return a->f() > b->f(); };
std::priority_queue<Node*, std::vector<Node*>, decltype(cmp)> open(cmp);
std::unordered_map<int, Node*> all_nodes;
Node* start = new Node(sgx, sgy);
start->h_cost = heuristic(sgx, sgy, ggx, ggy);
open.push(start);
all_nodes[sgx + sgy * MAP_WIDTH] = start;
const int dx[8] = {1,0,-1,0,1,1,-1,-1};
const int dy[8] = {0,1,0,-1,1,-1,1,-1};
const double step_cost[8] = {1.0,1.0,1.0,1.0,1.414,1.414,1.414,1.414};
while (!open.empty()) {
Node* cur = open.top(); open.pop();
if (cur->gx == ggx && cur->gy == ggy) {
std::vector<btVector3> path;
Node* n = cur;
while (n) {
double wx, wy;
gridToWorld(n->gx, n->gy, wx, wy);
path.push_back(btVector3(wx, wy, 0));
n = n->parent;
}
std::reverse(path.begin(), path.end());
for (auto& p : all_nodes) delete p.second;
return path;
}
for (int i = 0; i < 8; ++i) {
int ngx = cur->gx + dx[i];
int ngy = cur->gy + dy[i];
if (ngx < 0 || ngx >= MAP_WIDTH || ngy < 0 || ngy >= MAP_HEIGHT) continue;
if (isOccupied(ngx, ngy)) continue;
double new_g = cur->g_cost + step_cost[i];
int key = ngx + ngy * MAP_WIDTH;
if (all_nodes.find(key) == all_nodes.end()) {
Node* child = new Node(ngx, ngy);
child->g_cost = new_g;
child->h_cost = heuristic(ngx, ngy, ggx, ggy);
child->parent = cur;
all_nodes[key] = child;
open.push(child);
} else {
Node* child = all_nodes[key];
if (new_g < child->g_cost) {
child->g_cost = new_g;
child->parent = cur;
open.push(child);
}
}
}
}
for (auto& p : all_nodes) delete p.second;
return {};
}
private:
std::vector<uint8_t> occupancy;
std::vector<uint8_t> explored;
double target_world_x, target_world_y;
std::chrono::steady_clock::time_point target_timestamp;
bool has_target_memory = false;
double heuristic(int gx1, int gy1, int gx2, int gy2) const {
double dx = (gx1 - gx2) * MAP_RESOLUTION;
double dy = (gy1 - gy2) * MAP_RESOLUTION;
return std::sqrt(dx*dx + dy*dy);
}
};
// ---------- 机器人结构 ----------
struct VisionGuidedRobot {
btDiscreteDynamicsWorld* world;
btRigidBody* sphere_body;
btRigidBody* box_object;
btGeneric6DofConstraint* box_constraint;
bool is_carrying;
double thrust_up, thrust_down, thrust_forward, thrust_backward, thrust_right, thrust_left;
OccupancyGridMap grid_map;
std::vector<btVector3> current_path;
int path_index;
double last_seen_time;
btVector3 last_known_box_pos;
PID pidHeight, pidPosX, pidPosZ;
VisionGuidedRobot(btDiscreteDynamicsWorld* w, btRigidBody* box = nullptr)
: world(w), box_object(box), box_constraint(nullptr), is_carrying(false), path_index(0), last_seen_time(-1e9),
pidHeight(180.0, 15.0, 40.0, 100.0),
pidPosX(35.0, 2.5, 18.0, 150.0),
pidPosZ(35.0, 2.5, 18.0, 150.0) {
buildRobot();
resetThrust();
}
void buildRobot() {
btCollisionShape* sphereShape = new btSphereShape(0.3);
btCompoundShape* compound = new btCompoundShape();
btTransform mainTrans = btTransform::getIdentity();
compound->addChildShape(mainTrans, sphereShape);
btCollisionShape* weightShape = new btSphereShape(0.08);
btTransform weightTrans = btTransform::getIdentity();
weightTrans.setOrigin(btVector3(0, 0, -0.2));
compound->addChildShape(weightTrans, weightShape);
btVector3 inertia(0, 0, 0);
compound->calculateLocalInertia(3.0, inertia);
btDefaultMotionState* ms = new btDefaultMotionState(btTransform(btQuaternion::getIdentity(), btVector3(0, 0, 0.6)));
sphere_body = new btRigidBody(3.0, ms, compound, inertia);
sphere_body->setDamping(0.2, 0.5);
world->addRigidBody(sphere_body);
}
void resetThrust() {
thrust_up = thrust_down = thrust_forward = thrust_backward = thrust_right = thrust_left = 0;
}
void applyThrust() {
btVector3 force(thrust_forward - thrust_backward, 0, thrust_right - thrust_left);
force.setZ(force.z() + thrust_up - thrust_down);
sphere_body->applyCentralForce(force);
}
void clampThrust(double maxVert = 800.0, double maxHoriz = 200.0) {
thrust_up = std::clamp(thrust_up, 0.0, maxVert);
thrust_down = std::clamp(thrust_down, 0.0, maxVert);
thrust_forward = std::clamp(thrust_forward, 0.0, maxHoriz);
thrust_backward = std::clamp(thrust_backward, 0.0, maxHoriz);
thrust_right = std::clamp(thrust_right, 0.0, maxHoriz);
thrust_left = std::clamp(thrust_left, 0.0, maxHoriz);
}
double getHeight() const { return sphere_body->getCenterOfMassPosition().getZ(); }
btVector3 getPosition() const { return sphere_body->getCenterOfMassPosition(); }
btVector3 getVelocity() const { return sphere_body->getLinearVelocity(); }
void grabBox() {
if (is_carrying || !box_object) return;
btTransform frameA, frameB;
frameA.setIdentity(); frameA.setOrigin(btVector3(0, 0, 0.2));
frameB.setIdentity();
box_constraint = new btGeneric6DofConstraint(*sphere_body, *box_object, frameA, frameB, true);
box_constraint->setLinearLowerLimit(btVector3(0, 0, 0));
box_constraint->setLinearUpperLimit(btVector3(0, 0, 0));
box_constraint->setAngularLowerLimit(btVector3(0, 0, 0));
box_constraint->setAngularUpperLimit(btVector3(0, 0, 0));
world->addConstraint(box_constraint, true);
is_carrying = true;
std::cout << "[抓取] 箱子已吸附\n";
}
void releaseBox() {
if (box_constraint) {
world->removeConstraint(box_constraint);
delete box_constraint;
box_constraint = nullptr;
}
is_carrying = false;
std::cout << "[释放] 箱子已放下\n";
}
void updateWithVision(bool has_target, double dx_meter, double dz_meter, double current_time) {
if (has_target) {
btVector3 boxPos = box_object->getCenterOfMassPosition();
last_known_box_pos = boxPos;
last_seen_time = current_time;
grid_map.updateTargetPosition(boxPos.x(), boxPos.y());
int gx, gy;
if (grid_map.worldToGrid(getPosition().x(), getPosition().y(), gx, gy))
grid_map.markExplored(gx, gy);
}
}
bool getNavigationTarget(btVector3& target) {
double wx, wy;
if (grid_map.getTargetMemory(wx, wy)) {
target = btVector3(wx, wy, 0);
return true;
}
return false;
}
bool planPathToTarget(const btVector3& target) {
btVector3 start = getPosition();
current_path = grid_map.planPath(start.x(), start.y(), target.x(), target.y());
path_index = 0;
return !current_path.empty();
}
bool getNextWaypoint(btVector3& waypoint) {
if (path_index >= current_path.size()) return false;
waypoint = current_path[path_index];
btVector3 pos = getPosition();
if ((waypoint - pos).length() < 0.2) {
path_index++;
if (path_index < current_path.size())
waypoint = current_path[path_index];
else
return false;
}
return true;
}
std::vector<btVector3> getCurrentPath() const { return current_path; }
};
// ---------- 可绘制物体 ----------
struct Drawable {
btRigidBody* body;
btVector3 color;
btVector3 halfExtents;
bool isSphere;
float radius;
};
// ---------- OpenGL 离屏渲染器 ----------
class OffscreenRenderer {
public:
OffscreenRenderer(int width, int height) : w(width), h(height) {
initGLFW();
initFBO();
}
~OffscreenRenderer() {
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &colorTex);
glDeleteRenderbuffers(1, &depthRb);
glfwDestroyWindow(window);
glfwTerminate();
}
void render(const btVector3& cameraPos, const btVector3& cameraTarget, const std::vector<Drawable>& drawables) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, w, h);
glClearColor(0.6f, 0.9f, 0.6f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (double)w/h, 0.1, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(cameraPos.x(), cameraPos.y(), cameraPos.z(),
cameraTarget.x(), cameraTarget.y(), cameraTarget.z(),
0, 1, 0);
glColor3f(0.6f, 0.9f, 0.6f);
glBegin(GL_QUADS);
glVertex3f(-10, -10, 0); glVertex3f(10, -10, 0);
glVertex3f(10, 10, 0); glVertex3f(-10, 10, 0);
glEnd();
for (const auto& d : drawables) {
btTransform trans = d.body->getWorldTransform();
btVector3 pos = trans.getOrigin();
glPushMatrix();
glTranslatef(pos.x(), pos.y(), pos.z());
glColor3f(d.color.x(), d.color.y(), d.color.z());
if (d.isSphere) glutSolidSphere(d.radius, 20, 20);
else {
glScalef(d.halfExtents.x()*2, d.halfExtents.y()*2, d.halfExtents.z()*2);
glutSolidCube(1.0);
}
glPopMatrix();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
cv::Mat captureImage() {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
std::vector<unsigned char> pixels(w*h*3);
glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, pixels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
cv::Mat img(h, w, CV_8UC3);
memcpy(img.data, pixels.data(), w*h*3);
cv::cvtColor(img, img, cv::COLOR_RGB2BGR);
cv::flip(img, img, 0);
return img;
}
private:
int w, h;
GLFWwindow* window;
GLuint fbo, colorTex, depthRb;
void initGLFW() {
glfwInit();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
window = glfwCreateWindow(w, h, "VirtualCamera", nullptr, nullptr);
glfwMakeContextCurrent(window);
glewInit();
int dummy_argc=1; char* dummy_argv[]={(char*)"bullet_vision"};
glutInit(&dummy_argc, dummy_argv);
}
void initFBO() {
glGenTextures(1, &colorTex);
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenRenderbuffers(1, &depthRb);
glBindRenderbuffer(GL_RENDERBUFFER, depthRb);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h);
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRb);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cerr << "FBO创建失败\n";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
};
double pixelToMeter(double pixelError, double cameraHeight, double fovDeg=60.0) {
double rad = fovDeg * M_PI / 180.0;
double meterPerPixel = 2.0 * cameraHeight * tan(rad/2.0) / CAM_HEIGHT;
return pixelError * meterPerPixel;
}
void drawMiniMap(const VisionGuidedRobot& robot, const btVector3& boxPos, cv::Mat& mapImg) {
mapImg.setTo(cv::Scalar(30, 30, 30));
auto worldToMap = [](double wx, double wy) -> cv::Point {
int px = (int)((wx - MAP_ORIGIN_X) / MAP_SIZE_X * MAP_DISP_WIDTH);
int py = (int)((wy - MAP_ORIGIN_Y) / MAP_SIZE_Y * MAP_DISP_HEIGHT);
py = MAP_DISP_HEIGHT - py;
return cv::Point(px, py);
};
int x0 = worldToMap(-9.5, -9.5).x, y0 = worldToMap(-9.5, -9.5).y;
int x1 = worldToMap(9.5, 9.5).x, y1 = worldToMap(9.5, 9.5).y;
cv::rectangle(mapImg, cv::Point(x0, y0), cv::Point(x1, y1), cv::Scalar(100,100,100), 1);
auto path = robot.getCurrentPath();
for (size_t i=0; i+1<path.size(); ++i) {
cv::Point p1 = worldToMap(path[i].x(), path[i].y());
cv::Point p2 = worldToMap(path[i+1].x(), path[i+1].y());
cv::line(mapImg, p1, p2, cv::Scalar(0,255,255), 1, cv::LINE_AA);
}
cv::Point boxPt = worldToMap(boxPos.x(), boxPos.y());
cv::circle(mapImg, boxPt, 6, cv::Scalar(255,0,0), -1, cv::LINE_AA);
btVector3 robotPos = robot.getPosition();
cv::Point robotPt = worldToMap(robotPos.x(), robotPos.y());
cv::circle(mapImg, robotPt, 8, cv::Scalar(0,0,255), -1, cv::LINE_AA);
btVector3 vel = robot.getVelocity();
if (vel.length() > 0.1) {
cv::Point dirPt = worldToMap(robotPos.x() + vel.x()*0.5, robotPos.y() + vel.y()*0.5);
cv::line(mapImg, robotPt, dirPt, cv::Scalar(255,255,255), 2, cv::LINE_AA);
}
}
int main() {
// Bullet 世界
btDefaultCollisionConfiguration* cfg = new btDefaultCollisionConfiguration();
btCollisionDispatcher* disp = new btCollisionDispatcher(cfg);
btDbvtBroadphase* bp = new btDbvtBroadphase();
btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver();
btDiscreteDynamicsWorld* world = new btDiscreteDynamicsWorld(disp, bp, solver, cfg);
world->setGravity(btVector3(0,0,-9.8));
btCollisionShape* groundShape = new btStaticPlaneShape(btVector3(0,0,1), 0);
btDefaultMotionState* groundMS = new btDefaultMotionState(btTransform::getIdentity());
btRigidBody* ground = new btRigidBody(0, groundMS, groundShape);
ground->setFriction(1.5);
world->addRigidBody(ground);
btCollisionShape* boxShape = new btBoxShape(btVector3(0.2,0.2,0.2));
btVector3 boxInertia(0,0,0);
boxShape->calculateLocalInertia(1.0, boxInertia);
btDefaultMotionState* boxMS = new btDefaultMotionState(btTransform(btQuaternion::getIdentity(), btVector3(1.5,0,0.2)));
btRigidBody* box = new btRigidBody(1.0, boxMS, boxShape, boxInertia);
box->setFriction(0.8);
world->addRigidBody(box);
VisionGuidedRobot robot(world, box);
robot.resetThrust();
std::vector<Drawable> drawables;
drawables.push_back({robot.sphere_body, btVector3(1,0,0), btVector3(0,0,0), true, 0.3f});
drawables.push_back({box, btVector3(0,0,1), btVector3(0.2,0.2,0.2), false, 0.0f});
OffscreenRenderer renderer(CAM_WIDTH, CAM_HEIGHT);
enum State { TAKEOFF, SEARCH, APPROACH, LOWER_GRAB, FLY_TO_TARGET, RELEASE, RETURN, LAND, RECOVER, EXPLORE };
State state = TAKEOFF;
double targetHeight = CRUISE_HEIGHT;
double grabHeight = GRAB_HEIGHT;
btVector3 targetPos(5.0,0,0);
btVector3 boxWorldPos;
bool hasTarget = false;
double dx = 0, dz = 0;
double simTime = 0;
int step = 0;
bool boosting = true;
double boostEndTime = 0.6;
bool descending = false;
double descendStartTime = 0;
double explore_start_time = 0;
double last_vision_time = 0;
cv::namedWindow("MiniMap", cv::WINDOW_NORMAL);
cv::resizeWindow("MiniMap", MAP_DISP_WIDTH, MAP_DISP_HEIGHT);
cv::Mat mapImage(MAP_DISP_HEIGHT, MAP_DISP_WIDTH, CV_8UC3);
cv::namedWindow("Virtual Camera", cv::WINDOW_NORMAL);
cv::resizeWindow("Virtual Camera", CAM_WIDTH, CAM_HEIGHT);
std::cout << "===== 纯物理仿真:视觉引导抓取与搬运(无认知层) =====\n";
while (simTime < 180.0) {
world->stepSimulation(DT, 1, DT);
simTime += DT;
step++;
double height = robot.getHeight();
btVector3 pos = robot.getPosition();
btVector3 vel = robot.getVelocity();
// 虚拟相机
btVector3 camPos = pos + btVector3(0.5, 0, 0.4);
btVector3 camTarget = pos + btVector3(1.2, 0, 0);
renderer.render(camPos, camTarget, drawables);
cv::Mat frame = renderer.captureImage();
if (!frame.empty()) {
cv::imshow("Virtual Camera", frame);
cv::waitKey(1);
}
// 颜色分割
cv::Mat hsv, mask;
cv::cvtColor(frame, hsv, cv::COLOR_BGR2HSV);
cv::inRange(hsv, cv::Scalar(BOX_COLOR_HUE_LOW, BOX_COLOR_SAT_LOW, BOX_COLOR_VAL_LOW),
cv::Scalar(BOX_COLOR_HUE_HIGH, BOX_COLOR_SAT_HIGH, BOX_COLOR_VAL_HIGH), mask);
cv::erode(mask, mask, cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3)));
cv::dilate(mask, mask, cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3)));
std::vector<std::vector<cv::Point>> contours;
cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
hasTarget = false;
if (!contours.empty()) {
auto largest = std::max_element(contours.begin(), contours.end(),
[](auto& a, auto& b){ return cv::contourArea(a) < cv::contourArea(b); });
cv::Moments m = cv::moments(*largest);
if (m.m00 > 1e-6) {
double cx = m.m10 / m.m00;
double imgCenterX = CAM_WIDTH/2.0;
double pixelError = cx - imgCenterX;
double cameraHeightAbs = 0.4 + height;
dx = pixelToMeter(pixelError, cameraHeightAbs, 60.0);
dz = 0;
hasTarget = true;
last_vision_time = simTime;
robot.updateWithVision(true, dx, dz, simTime);
if (state == RECOVER || state == EXPLORE) {
state = APPROACH;
std::cout << "[视觉恢复] 重新看到箱子,切换至接近模式\n";
}
}
} else {
robot.updateWithVision(false, 0, 0, simTime);
}
// 高度控制
double forceVertical = 0;
if (boosting && simTime < boostEndTime) forceVertical = 800.0;
else {
boosting = false;
double targetH = (state == LOWER_GRAB) ? grabHeight : targetHeight;
forceVertical = robot.pidHeight.compute(targetH, height, DT);
}
if (forceVertical > 0) { robot.thrust_up = forceVertical; robot.thrust_down = 0; }
else { robot.thrust_up = 0; robot.thrust_down = -forceVertical; }
// 水平控制
double forceX = 0, forceZ = 0;
switch (state) {
case TAKEOFF:
if (height > 0.6) { state = SEARCH; std::cout << "[起飞完成] 进入搜索模式\n"; }
break;
case SEARCH:
if (hasTarget && std::abs(dx) < 0.1) {
state = APPROACH;
boxWorldPos = pos + btVector3(dx, 0, dz);
std::cout << "[定位] 箱子在 (" << boxWorldPos.x() << ", " << boxWorldPos.z() << ")\n";
} else if (hasTarget) {
forceX = robot.pidPosX.compute(0, dx, DT);
forceZ = robot.pidPosZ.compute(0, 0, DT);
} else {
btVector3 navTarget;
if (robot.getNavigationTarget(navTarget)) {
state = RECOVER;
std::cout << "[视觉丢失] 启用记忆恢复,目标(" << navTarget.x() << "," << navTarget.y() << ")\n";
robot.planPathToTarget(navTarget);
} else {
state = EXPLORE;
explore_start_time = simTime;
std::cout << "[无记忆] 进入探索模式\n";
}
}
break;
case RECOVER:
{
btVector3 waypoint;
if (robot.getNextWaypoint(waypoint)) {
forceX = robot.pidPosX.compute(waypoint.x(), pos.x(), DT);
forceZ = robot.pidPosZ.compute(waypoint.y(), pos.y(), DT);
} else {
btVector3 target;
if (robot.getNavigationTarget(target))
robot.planPathToTarget(target);
else
state = EXPLORE;
}
}
break;
case EXPLORE:
{
double angle = simTime * 0.5;
double radius = 1.5;
double target_x = pos.x() + radius * cos(angle);
double target_z = pos.y() + radius * sin(angle);
target_x = std::clamp(target_x, -9.0, 9.0);
target_z = std::clamp(target_z, -9.0, 9.0);
forceX = robot.pidPosX.compute(target_x, pos.x(), DT);
forceZ = robot.pidPosZ.compute(target_z, pos.y(), DT);
if (simTime - explore_start_time > EXPLORE_TIMEOUT) {
state = SEARCH;
std::cout << "[探索超时] 返回搜索模式\n";
}
}
break;
case APPROACH:
{
btVector3 err = boxWorldPos - pos;
if (err.length() < 0.2 && std::abs(vel.x())<0.5 && std::abs(vel.z())<0.5) {
state = LOWER_GRAB;
descendStartTime = simTime;
descending = true;
std::cout << "[接近箱子] 开始下降抓取\n";
} else {
forceX = robot.pidPosX.compute(boxWorldPos.x(), pos.x(), DT);
forceZ = robot.pidPosZ.compute(boxWorldPos.z(), pos.y(), DT);
}
}
break;
case LOWER_GRAB:
if (!descending) { descendStartTime=simTime; descending=true; }
if (simTime-descendStartTime>1.0 || height<=grabHeight+0.05) {
robot.grabBox();
state = FLY_TO_TARGET;
targetHeight = CRUISE_HEIGHT;
descending = false;
std::cout << "[抓取成功] 飞向目标点\n";
}
break;
case FLY_TO_TARGET:
{
btVector3 err = targetPos - pos;
if (err.length()<0.3 && std::abs(vel.x())<0.5) {
robot.releaseBox();
state = RETURN;
std::cout << "[到达目标] 释放箱子,返回原点\n";
} else {
forceX = robot.pidPosX.compute(targetPos.x(), pos.x(), DT);
forceZ = robot.pidPosZ.compute(targetPos.z(), pos.y(), DT);
}
}
break;
case RETURN:
{
btVector3 err = btVector3(0,0,0) - pos;
if (err.length()<0.3 && std::abs(vel.x())<0.5) {
state = LAND;
std::cout << "[返回原点] 开始降落\n";
} else {
forceX = robot.pidPosX.compute(0, pos.x(), DT);
forceZ = robot.pidPosZ.compute(0, pos.y(), DT);
}
}
break;
case LAND:
if (height<0.45 && std::abs(vel.z())<0.3) {
robot.thrust_up=0; robot.thrust_down=0;
robot.thrust_forward=robot.thrust_backward=robot.thrust_left=robot.thrust_right=0;
std::cout << "[降落完成] 任务成功结束\n";
goto end_simulation;
}
break;
}
forceX = std::clamp(forceX, -200.0, 200.0);
forceZ = std::clamp(forceZ, -200.0, 200.0);
if (forceX>0) { robot.thrust_forward=forceX; robot.thrust_backward=0; }
else { robot.thrust_backward=-forceX; robot.thrust_forward=0; }
if (forceZ>0) { robot.thrust_right=forceZ; robot.thrust_left=0; }
else { robot.thrust_left=-forceZ; robot.thrust_right=0; }
robot.clampThrust(800, 200);
robot.applyThrust();
drawMiniMap(robot, box->getCenterOfMassPosition(), mapImage);
cv::imshow("MiniMap", mapImage);
cv::waitKey(1);
if (step % 50 == 0) {
std::cout << std::fixed << std::setprecision(2)
<< "t=" << std::setw(5) << simTime
<< " h=" << std::setw(5) << height
<< " pos=(" << pos.x() << "," << pos.y() << ")"
<< " carry=" << robot.is_carrying
<< " state=" << state
<< " hasTarget=" << hasTarget
<< " dx=" << dx
<< std::endl;
}
if (SIM_SPEED < 1.0) std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
end_simulation:
cv::destroyAllWindows();
delete world; delete solver; delete bp; delete disp; delete cfg;
return 0;
}
编译命令
bash
g++ -std=c++17 -O2 bullet_vision_phase4_pure_physics.cpp $(pkg-config --cflags --libs bullet) -lsqlite3 -lcurl -lpthread -lglfw -lGLEW -lGLU -lGL -lglut `pkg-config --cflags --libs opencv4` -lpthread -o bullet_vision_phase4_pure_physics
说明
- 无任何 ForeSight 智能认知组件
- 所有控制参数为固定值(PID 系数、推力限制、高度阈值等)
- 状态机完全由硬编码逻辑驱动
- 地图导航使用 A 算法*(无学习能力)
- 视觉为 OpenCV 颜色识别(无自适应参数)
此代码可直接编译运行,展示纯物理控制下的自主抓取搬运任务。