19f1b3cb3b9b1.png

*📂 **源代码地址:** Skeleton_Virtual-Pet-Dog*

*🎯 **参赛信息:** 第二届3D视觉创新应用竞赛 | 参赛者:叶顶强*

📌 项目亮点

"不是冷冰冰的遥控玩具,而是能'看懂'你、'跟着'你、'保护'你的智能搭档!"

亮点 内容
🐾 有灵性的宠物 招手→前进、抬手→起立、跌倒→预警,一套完整的人体动作语义映射
🦴 18点骨骼感知 Astra SDK实时追踪人体18个关节点 + HandPose握拳状态
🚶 智能跟随 自动追踪主人位置和朝向,始终保持在视野正前方
🛡️ 安全优先 跌倒检测毫秒级响应,第一时间触发停车警报
🚧 环境感知 RANSAC深度平面拟合,实时检测障碍物方向并自主绕行
🧠 学术级算法 OpenCV 3D最小二乘法平面拟合 + 体素网格下采样,数学严谨
☁️ 点云可视化 Open3D实时渲染带语义标签的彩色点云,直观理解环境

一、项目背景

1.1 从"机械宠物"到"有灵性的搭档" 🐾

"传统机器狗只会执行命令——而这台机器狗,能'读懂'你的肢体语言。"

你有没有想过,如果宠物狗能够:

  • 👋 看到你招手就屁颠屁颠跑过来
  • 🙆 看到你抬手就知道该站起来"打招呼"
  • 😰 看到你跌倒就立刻发出警报求援

这些在真实宠物身上理所当然的能力,在机器狗身上实现,需要解决一个核心问题:让机器狗"看懂"人的姿态和动作。

本项目正是为此而生——通过Astra深度相机给机器狗装上"智慧之眼",让它能够实时感知主人的姿态和意图,真正成为一个有感知、有反应、有温度的智能搭档。

1.2 系统核心目标

目标 描述 技术实现
🦴 人体骨骼感知 实时检测人体18个关节点 Astra SDK Body Tracking
🚶 智能跟随 追踪主人位置和朝向 人体-机器狗相对角度计算
🙋 姿态动作识别 招手/抬手/跌倒等动作分类 手部位置变化 + 身体高度分析
🚧 障碍物检测 检测前、左、右障碍物方向 RANSAC深度平面拟合
🛡️ 安全保护 人体异常姿态自动报警 COM-Hip高度差检测
📊 点云可视化 彩色语义点云实时渲染 Open3D + OpenCV 3D

1.3 为什么选择Astra SDK?

README中有一段非常坦诚的技术选型记录:

"Due to the dependence on the hardware performance of electronic pet dogs for 
tracking, detection, and human skeleton recognition, it is not easy to make 
comparisons. We can only conduct some testing on the algorithm frame rate..."

实测结论:

  自研方案(Yolo + DeepSort + Dasiampn):约0.045 FPS(22秒/帧)❌
  Astra SDK内置Body Tracking:            约13 FPS ✅

Overall, using AstraSDK is more cost-effective and has lower development costs.
(综合来看,Astra SDK性价比更高,开发成本更低)

二、系统整体架构

2.1 架构概览

19f1b4c07e0a3.png

2.2 硬件平台

组件 型号 参数
📷 深度相机 Orbbec Astra Pro USB连接,640×480@30fps,红外补光
🧠 计算平台 NVIDIA Jetson Nano ARM Cortex-A57,4GB RAM
🐕 执行机构 电子宠物狗(四足) UDP控制,IP地址192.168.1.120
💾 软件环境 Ubuntu + ROS + Astra SDK 嵌入式Linux环境

2.3 软件技术栈

层级 技术选型 说明
深度感知 Astra SDK + OpenNI2 深度流 + 骨骼关节点
人体追踪 Astra Body Tracking API 18点骨骼 + HandPose
可视化 SFML(骨骼叠加)+ Open3D(点云) 双轨可视化
点云处理 OpenCV 3D Module RANSAC + 最小二乘平面拟合
下采样 体素网格(VoxelGrid)+ 最远点采样(FPS) 加速RANSAC
网络通信 BSD Socket UDP 指令下发到机器狗
构建工具 CMake + Catkin ROS标准构建系统

三、骨骼追踪模块 🦴

这是整个系统的"眼睛",让机器狗能够实时"看见"人体的每一个关节。

3.1 Astra SDK 18点骨骼系统

19f1b573da7bc.png

Astra SDK内置了专业的深度学习人体骨骼追踪系统,检测18个关节点:

索引 节点名称 英文名 关键用途
0 头部 Head —
1 颈部 Neck —
2 肩胛脊柱 ShoulderSpine 身体朝向参考
3 左肩 LeftShoulder 右手坐标系
4 右肩 RightShoulder —
5 左肘 LeftElbow —
6 右肘 RightElbow —
7 左手腕 LeftWrist 手势追踪基准
8 右手腕 RightWrist 手势追踪基准
9 左手 LeftHand 跟随方向判定
10 右手 RightHand HandPose握拳检测
11 脊柱中点 MidSpine —
12 骨盆 BaseSpine 跌倒检测基准点
13 左髋 LeftHip —
14 右髋 RightHip —
15 左膝 LeftKnee —
16 右膝 RightKnee —
17 左脚 LeftFoot —
18 右脚 RightFoot —

3.2 骨骼可视化实现(SFML)

// main.cpp — SFML骨骼可视化核心
class sfLine : public sf::Drawable {
public:
    sfLine(const sf::Vector2f &point1, const sf::Vector2f &point2,
           sf::Color color, float thickness)
        : color_(color)
    {
        // 计算骨骼线段方向的单位法向量
        const sf::Vector2f direction = point2 - point1;
        const sf::Vector2f unitDirection = direction /
            std::sqrt(direction.x*direction.x + direction.y*direction.y);
        const sf::Vector2f normal(-unitDirection.y, unitDirection.x);

        // 沿法线扩展为矩形带(模拟线条粗细,解决SFML原生不支持线宽的问题)
        const sf::Vector2f offset = (thickness / 2.f) * normal;
        vertices_[0].position = point1 + offset;
        vertices_[1].position = point2 + offset;
        vertices_[2].position = point2 - offset;
        vertices_[3].position = point1 - offset;

        for (int i = 0; i < 4; ++i)
            vertices_[i].color = color;
    }

    void draw(sf::RenderTarget &target, sf::RenderStates states) const override
    {
        target.draw(vertices_, 4, sf::Quads, states);
    }
private:
    sf::Vertex vertices_[4];
    sf::Color color_;
};

3.3 HandPose手部状态与骨骼连线

// main.cpp — 手部Grip状态高亮
void update_body(astra::Body body, const float jointScale)
{
    for (const auto &joint : joints)
    {
        auto radius = jointRadius_ * jointScale;
        sf::Color color = sf::Color(0x00, 0xFF, 0x00, 0xFF);  // 默认绿色

        // Grip握拳状态 → 蓝色高亮 + 1.5倍放大
        if ((type == astra::JointType::RightHand &&
             astra::HandPose::Grip == body.hand_poses().right_hand()))
        {
            radius *= 1.5f;
            color = sf::Color(0x00, 0xAA, 0xFF, 0xFF);  // 蓝色
        }

        sf::CircleShape circle(radius);
        circle.setFillColor(color);
        circle.setPosition(pos.x - radius, pos.y - radius);
        circles_.push_back(circle);
    }

    // 18条骨骼连线
    update_bone(joints, jointScale, astra::JointType::Head, astra::JointType::Neck);
    update_bone(joints, jointScale, astra::JointType::ShoulderSpine, astra::JointType::LeftShoulder);
    update_bone(joints, jointScale, astra::JointType::ShoulderSpine, astra::JointType::RightShoulder);
    update_bone(joints, jointScale, astra::JointType::LeftShoulder, astra::JointType::LeftElbow);
    update_bone(joints, jointScale, astra::JointType::RightShoulder, astra::JointType::RightElbow);
    update_bone(joints, jointScale, astra::JointType::LeftElbow, astra::JointType::LeftWrist);
    update_bone(joints, jointScale, astra::JointType::RightElbow, astra::JointType::RightWrist);
    update_bone(joints, jointScale, astra::JointType::ShoulderSpine, astra::JointType::MidSpine);
    // ... 完整18条骨骼连线 ...
}

四、人体姿态语义映射 🎮

这是机器狗的"大脑",把骨骼动作翻译成有意义的指令。

4.1 姿态→动作映射表

检测到的姿态 触发条件 机器狗行为
👋 招手 handY > 220(右手Y方向速度) 前进跟近
🙋 抬手起立 双手腕高度 > 头部高度 执行站立指令
😰 跌倒 COM高度 – Hip高度 < 400mm 立即停车 + 警报
🖐️ 伸手跟随 waistM < 50 + handZ > 150 开启跟随模式
✋ 举手停止 双手腕高度 > 头部高度 停止跟随

4.2 三帧运动追踪

为了过滤抖动并准确判断运动趋势,系统维护了3帧历史缓冲:

// main.cpp — 手部运动追踪(3帧滑动窗口)
float rHand[3][3];  // [帧索引][XYZ] — 右手世界坐标(3帧历史)
float waist[3][3];  // [帧索引][XYZ] — 骨盆世界坐标(参考基准)

// 速度估算:相邻帧位置差分的平均
float handY = ((rHand[2][1] - rHand[1][1]) + (rHand[1][1] - rHand[0][1])) / 2;
float handZ = ((rHand[2][2] - rHand[1][2]) + (rHand[1][2] - rHand[0][2])) / 2;
float handX = ((rHand[2][0] - rHand[1][0]) + (rHand[1][0] - rHand[0][0])) / 2;

// 腰部运动幅度(过滤"整个身体晃动"导致的误判)
float waistM = sqrt(waistX*waistX + waistZ*waistZ);

// 判定逻辑:只有腰部稳定(waistM < 50)时,才认手部动作
if ((waistM < 50 && waistM > -50) && (handZ > 150 || handZ < -150))
    follow = 1;  // 伸手向前 → 开启跟随模式

// 双手高举 → 取消跟随
if (body.joints()[7].world_position().y > body.joints()[0].world_position().y &&
    body.joints()[4].world_position().y > body.joints()[0].world_position().y)
    follow = 0;

> 技术要点: 腰部运动过滤是关键!如果不加这个判断,当主人整个身体前后晃动时,机器狗会误判为"伸手跟随",导致跟踪混乱。waistM < 50 阈值确保只有手部独立运动才被认可 🤚

4.3 跟随距离判断

// main.cpp — 人体-机器狗距离计算
// 关节9(左手)在世界坐标系中的位置:
// X: 左右(相对机器狗)
// Y: 上下(高度)
// Z: 前后(距离)

// 计算水平距离(忽略Y方向)
manDis = sqrt(
    body.joints()[9].world_position().x * body.joints()[9].world_position().x +
    body.joints()[9].world_position().z * body.joints()[9].world_position().z
);
// 除以1000转换为米
manDis = manDis / 1000.0;

五、机器狗控制协议 🔧

这是机器狗的"神经系统",负责把感知决策转化为机器狗能理解的UDP指令。

5.1 指令协议结构

// main.cpp — UDP指令协议
#define SERVER_IP   "192.168.1.120"
#define SERVER_PORT  43893

struct CommandHead {
    uint32_t code;          // 指令码
    union {
        int value;           // 参数值(带符号整数)
        int paramters_size;
    };
    struct {
        uint32_t type : 8;  // 指令类型(占8位)
        uint32_t count : 24; // 参数数量(占24位)
    };
};

// 三大指令类型
namespace user_command {
    uint32_t upOrDown = 1;   // 俯仰指令(起立/趴下)
    uint32_t rotate   = 9;  // 转向指令(左右旋转)
    uint32_t move     = 10;  // 移动指令(前进/后退)
}

// 指令参数值
constexpr int zeroVal         = 0;      // 停止/中立
constexpr int leftRotateVal   = -15000; // 左转
constexpr int rightRotateVal  = 15000;  // 右转
constexpr int backVal         = -10000; // 后退
constexpr int forwardVal      = 10000;  // 前进

5.2 运动决策核心逻辑

// main.cpp — 运动决策函数
void robotMove(double fps, long double frameDuration)
{
    command_head.code = user_command::rotate;
    command_head.value = user_command::zeroVal;

    // 角度判断:关节9的世界坐标X值作为旋转角度
    if (angle > 80 && rotateState != 1) {
        // 偏左超过80° → 向左转
        command_head.code = user_command::rotate;
        command_head.value = user_command::leftRotateVal;
        rotateState = 1;
    }
    else if (angle < -80 && rotateState != -1) {
        // 偏右超过80° → 向右转
        command_head.code = user_command::rotate;
        command_head.value = user_command::rightRotateVal;
        rotateState = -1;
    }
    else {
        fc++;
        if (fc < 3) {
            // 连续3帧居中 → 停止旋转
            rotateState = 0;
            command_head.code = user_command::rotate;
            command_head.value = user_command::zeroVal;
        } else {
            // 距离判断:决定前进还是停止
            command_head.code = user_command::move;
            if (manDis > 1.5)
                command_head.value = user_command::forwardVal;  // 跟上来
            else
                command_head.value = user_command::zeroVal;   // 保持距离
        }
    }

    // 指令发送(FPS自适应限流)
    if (fps > 15) {
        if (!np) {
            np = true;
            sendto(client_fd, &command_head, sizeof(command_head),
                   0, (struct sockaddr *)&ser_addr, addrLen);
        } else { np = false; }
    } else {
        sendto(client_fd, &command_head, sizeof(command_head),
               0, (struct sockaddr *)&ser_addr, addrLen);
    }
}

> FPS限流机制: 当帧率高于15FPS时,系统采用"隔帧发送"策略(if (!np) { np = true; send; } else { np = false; }),避免指令发送过于频繁导致机器狗响应不过来;当帧率低于15FPS时,每帧都发送确保实时性 🎯

六、安全保护模块 🛡️

"安全第一"——这是机器狗最重要的行为准则。

6.1 人体跌倒检测算法

> 原理: 正常站立时,人体重心(Center of Mass)在髋关节上方;当跌倒时重心急剧下降,与髋关节高度差急剧缩小。通过检测 COM_y - Hip_y < 400mm 来判断是否发生跌倒。

// main.cpp — 跌倒检测
if (body.center_of_mass().y - body.joints()[12].world_position().y < 400) {
    message = "accident";  // 跌倒/异常
    acc = 1;              // 触发安全标志
} else {
    message = "safe";
    acc = 0;
}

6.2 安全状态的视觉反馈

// main.cpp — 渲染时体现安全状态
void draw_help_message(sf::RenderWindow &window) const
{
    std::stringstream str;
    str << "nstatus:" << message        // 安全状态
        << "nfollow:" << follow;        // 跟随状态

    sf::Text text(str.str(), font_);
    text.setCharacterSize(50);
    text.setStyle(sf::Text::Bold);

    // 跌倒时文字显示红色(危险警告)
    if (acc == 1)
        draw_text(window, text, sf::Color::Red, 0, 0);   // 🔴 警告!
    else
        draw_text(window, text, sf::Color::White, 0, 0); // 🟢 安全
}

七、障碍物检测模块 🚧

这是机器狗的"前视雷达"——通过分析深度图像的空洞区域,实时感知前方障碍物。

7.1 基于深度空洞的障碍物检测

// main.cpp — obstacleDetection:深度图空洞分析
int obstacleDetection(const int16_t* depthPtr, int width, int height)
{
    int tt = width * height;        // 总像素数
    int zerocnt = 0;                // 零深度像素计数
    int totalxsleft = 0, totalxsright = 0;

    // 扫描所有像素,统计深度值为0的区域
    for (int i = 0; i < tt; i++) {
        if (depthPtr[i] == 0) {    // 深度为0 = 过近/过远,测量不到
            zerocnt++;
            int f = i % width;     // 列索引(X方向)
            if (f < width/16)      totalxsleft++;    // 左边缘
            else if (f > 15*width/16) totalxsright++; // 右边缘
        }
    }

    float zeroDetect = (float)zerocnt / (float)tt;
    float leftDetect  = (float)totalxsleft  / (float)tt * 16;
    float rightDetect = (float)totalxsright / (float)tt * 16;

    if (zeroDetect > 0.8)
        return 2;  // 全前方障碍 → 向右转绕行
    else if (leftDetect > 0.6 && leftDetect > rightDetect)
        return 1;  // 左方障碍 → 向右转绕行
    else if (rightDetect > 0.6 && leftDetect < rightDetect)
        return 2;  // 右方障碍 → 向左转绕行
    return 0;      // 无障碍
}

> 算法原理: 当障碍物距离相机过近(小于最小测距)时,Astra相机返回深度值0。通过统计画面左/右/全区域的零深度像素占比,可以判断障碍物的主要方位。简单高效,无需GPU加速!📡

八、OpenCV 3D平面拟合(核心算法)🧮

> 以下是项目中用到的学术级RANSAC平面拟合实现,代码来自OpenCV 3D Module,包含完整的数学推导。

8.1 总体最小二乘法拟合平面

> 数学原理: 给定点云集合,拟合平面 ax + by + cz + d = 0,使得所有点到平面的正交距离平方和最小。

// plane_utils.cpp — totalLeastSquaresPlaneEstimate
// 核心思想:用协方差矩阵的特征向量找到最佳平面法向量

bool totalLeastSquaresPlaneEstimate(const cv::Mat &input_pts,
                                    std::vector<int> &inlier_idxs,
                                    cv::Vec4f &plane_coeffs)
{
    // 1. 计算所有内点的几何中心
    double sum_x = 0, sum_y = 0, sum_z = 0;
    for (int i = 0; i < inlier_cnt; ++i) {
        const float *ptr_base = pts_ptr + 3 * inlier_idxs[i];
        sum_x += *ptr_base;
        sum_y += *(ptr_base + 1);
        sum_z += *(ptr_base + 2);
    }
    float mean_x = sum_x / inlier_cnt;
    float mean_y = sum_y / inlier_cnt;
    float mean_z = sum_z / inlier_cnt;

    // 2. 去中心化:每个点减去几何中心
    cv::Mat U(inlier_cnt, 3, CV_32F);
    for (int i = 0; i < inlier_cnt; ++i) {
        const float *ptr_base = pts_ptr + 3 * inlier_idxs[i];
        U.at<float>(i, 0) = *ptr_base - mean_x;
        U.at<float>(i, 1) = *(ptr_base + 1) - mean_y;
        U.at<float>(i, 2) = *(ptr_base + 2) - mean_z;
    }

    // 3. 计算协方差矩阵 U^T * U
    cv::Mat pd_mat = U.t() * U;

    // 4. 特征分解:最小特征值对应的特征向量 = 平面法向量
    // (因为该方向上方差最小,即点在该方向上分布最集中)
    cv::Mat eigenvectors, eigenvalues;
    cv::eigen(pd_mat, eigenvalues, eigenvectors);
    const float *eig_ptr = (float *)eigenvectors.data;

    // 特征向量按列存储,第三个向量(索引6,7,8)对应最小特征值
    float a = eig_ptr[6], b = eig_ptr[7], c = eig_ptr[8];

    // 5. 构建平面方程 ax + by + cz + d = 0
    plane_coeffs = cv::Vec4f(a, b, c, -a*mean_x - b*mean_y - c*mean_z);
    return true;
}

> 数学直觉: 想象一把刀切进一团云点——最小特征值对应的特征向量方向,就是这团点"最薄"的方向,也就是平面的法线方向。这比直接用三点叉乘更鲁棒,因为它是全局最优而非局部最优!🎯

8.2 内点判定

// plane_utils.cpp — getPlaneInliers:计算所有内点
int getPlaneInliers(const cv::Vec4f &plane_coeffs,
                    const cv::Mat &input_pts, float thr,
                    std::vector<bool> &inliers)
{
    // 归一化平面方程系数
    float a = plane_coeffs(0), b = plane_coeffs(1),
          c = plane_coeffs(2), d = plane_coeffs(3);
    float hom = sqrt(a*a + b*b + c*c);
    a /= hom; b /= hom; c /= hom; d /= hom;

    // 对每个点计算点到平面距离 |ax+by+cz+d| / ||n||
    for (int i = 0; i < pts_size; ++i)
    {
        float *ptr_base = pts_ptr + 3 * i;
        float dist = a*(*ptr_base) + b*(*(ptr_base+1)) + c*(*(ptr_base+2)) + d;
        if (dist > -thr && dist < thr) {  // 在阈值内 = 内点
            inliers[i] = true;
            ++num_inliers;
        } else {
            inliers[i] = false;
        }
    }
    return num_inliers;
}

8.3 RANSAC多平面拟合

// segmentation.cpp — ransacFitPlane:RANSAC单平面拟合
int ransacFitPlane(const cv::Mat &input_pts, float thr,
                   int max_iterations, cv::Vec4f &plane_coeffs,
                   cv::Vec3f *normal, double normal_diff_thr)
{
    int best_inls = 0;
    for (int iter = 0; iter < max_iterations; ++iter)
    {
        // 随机选择3个最小样本点
        for (int i = 0; i < 3; ++i)
            min_sample[i] = rng.uniform(0, pts_size);

        // 用最小二乘法拟合平面
        if (!cv::_3d::totalLeastSquaresPlaneEstimate(input_pts, min_sample,
                                                      plane_coeffs_))
            continue;

        // 统计内点数量
        num_inls = cv::_3d::getPlaneInlierIdxs(plane_coeffs_, input_pts,
                                               thr, inlier_idxs);
        if (num_inls > best_inls)
        {
            // 局部优化:用所有内点重新拟合
            localOptimization(input_pts, plane_coeffs_, inlier_idxs,
                             thr, max_lo_iters, max_lo_inliers,
                             normal, normal_diff_thr);
            plane_coeffs = plane_coeffs_;
            best_inls = num_inls;

            // 自适应迭代次数:内点越多,置信度越高,迭代越快收敛
            const double iter_lower = 3 * log(1 - 0.96) /
                log(1 - pow(float(best_inls) / pts_size, 3));
            if (!std::isinf(iter_lower) && iter_lower < max_iterations)
                max_iterations = static_cast<int>(iter_lower);
        }
    }
    return best_inls;
}

九、点云下采样技术 ⚡

体素网格下采样是RANSAC加速的关键——用更少的点做同样的事情。

9.1 体素网格下采样(VoxelGrid)

// sampling.cpp — voxelGrid:体素网格下采样
void voxelGrid(cv::InputArray &input_pts,
               const float length, const float width, const float height,
               cv::OutputArray &sampled_pts)
{
    // 1. 计算点云包围盒边界
    float x_min, x_max, y_min, y_max, z_min, z_max;
    // ... 扫描所有点找min/max ...

    // 2. 将每个点分配到对应的体素格子
    std::unordered_map<keyType, std::vector<int>> grids;
    for (int i = 0; i < ori_pts_size; ++i) {
        long hx = (ori_pts_ptr[3*i]   - x_min) / length;
        long hy = (ori_pts_ptr[3*i+1] - y_min) / width;
        long hz = (ori_pts_ptr[3*i+2] - z_min) / height;

        // 三维坐标 → 一维哈希key(空间哈希)
        keyType key = hx + hy*offset_y + hz*offset_z;
        grids[key].push_back(i);
    }

    // 3. 每个格子保留一个代表点(距离质心最近的点)
    for (auto &grid : grids) {
        std::vector<int> &cell = grid.second;
        if (cell.size() > 2) {
            // 计算该格子内所有点的质心
            float cx=0, cy=0, cz=0;
            for (int idx : cell) { cx += pts[3*idx]; cy += pts[3*idx+1]; cz += pts[3*idx+2]; }
            cx /= cell.size(); cy /= cell.size(); cz /= cell.size();

            // 找距离质心最近的点作为代表
            float min_dist = FLT_MAX;
            for (int idx : cell) {
                float d = dist(pts[3*idx], pts[3*idx+1], pts[3*idx+2], cx, cy, cz);
                if (d < min_dist) { min_dist = d; best = idx; }
            }
        }
        // 将best点添加到采样结果
    }
}

9.2 最远点采样(Farthest Point Sampling)

// sampling.cpp — farthestPointSampling:保持空间分布均匀的下采样
void farthestPointSampling(cv::InputArray &input_pts,
                          const int sampled_pts_size,
                          cv::OutputArray &sampled_pts,
                          const float dist_lower_limit)
{
    // 核心思想:每次选择距离当前采样集合最远的点
    // 保证采样点均匀覆盖整个空间分布

    // 1. 随机选择第一个种子点
    int seed = cv::abs(cv::randu<int>()) % ori_pts_size;
    idxs[0] = seed;
    sampled_cnt = 1;

    // 2. 迭代选择最远点
    while (sampled_cnt < sampled_pts_size)
    {
        // 计算未采样点到已采样集合S的距离(= 到S中最近点的距离)
        for (int i = sampled_cnt; i < ori_pts_size; ++i) {
            float d = dist(point_i, last_sampled_point);
            if (d < dist_square[i])   // 更新最近距离
                dist_square[i] = d;
            if (dist_square[i] > max_dist) { // 更新最远点
                max_dist = dist_square[i];
                farthest_idx = i;
            }
        }

        // 将最远点加入采样集合
        idxs[sampled_cnt] = farthest_idx;
        ++sampled_cnt;
    }
}

十、Open3D点云可视化 📊

19f1b6be5c2b7.png

项目中还包含一个独立的Open3D点云可视化脚本,用于分析带语义标签的点云数据:

# Pointcloud-Visualization-With-Open3D.py — 语义标签点云渲染
import open3d as o3d

# 支持7类语义标签(彩色渲染)
label_colors = {
    0: [0.0, 0.0, 0.0],       # 标签0: 黑色(背景)
    1: [0.259, 0.522, 0.957], # 标签1: 蓝色(地面)
    2: [0.859, 0.267, 0.216], # 标签2: 红色(左障碍物)
    3: [0.959, 0.651, 0.0],  # 标签3: 橙色(右障碍物)
    4: [0.059, 0.616, 0.345], # 标签4: 绿色(天花板/上方平面)
    5: [0.4, 0.0, 0.8],       # 标签5: 紫色
    6: [0.0, 1.0, 0.8],       # 标签6: 青色(人体)
}

pcd = o3d.io.read_point_cloud(args.cloud_path)  # 读取点云文件
labels = open(args.label_path).read().split("n")  # 读取标签文件

# 为每个点赋予对应颜色
colors = [label_colors[int(l)] for l in labels if l != '']
pcd.colors = o3d.utility.Vector3dVector(colors)

# 启动Open3D交互式可视化窗口
o3d.visualization.draw_geometries([pcd])

十一、完整主函数 🚀

// main.cpp — 程序入口
int main(int argc, char **argv)
{
    // 1. 创建SFML窗口(1280×960分辨率)
    sf::RenderWindow window(sf::VideoMode(1280, 960), "Simple Body Viewer");

    // 2. 初始化UDP socket(控制机器狗)
    client_fd = socket(AF_INET, SOCK_DGRAM, 0);
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    ser_addr.sin_port = htons(SERVER_PORT);

    // 3. 询问是否需要机器狗站立初始化
    cout << "Do the dog need to stand up? (yes or no)";
    string standUp = "";
    while (cin >> standUp) {
        if (standUp == "yes") { squat(); break; }
        else if (standUp == "no") break;
    }

    // 4. 初始化Astra SDK
    astra::initialize();

    // 5. 设置奥比中光商业License
    const char *licenseString = "7QpqAnYc1T4cQXV6kApUx...";
    orbbec_body_tracking_set_license(licenseString);

    // 6. 配置深度流和人体追踪流
    astra::StreamSet sensor;
    astra::StreamReader reader = sensor.create_reader();

    auto depthStream = configure_depth(reader);
    depthStream.start();

    auto bodyStream = reader.stream<astra::BodyStream>();
    bodyStream.start();
    reader.add_listener(listener);

    // 7. 主循环
    while (window.isOpen()) {
        astra_update();
        window.clear(sf::Color::Black);
        listener.draw_to(window);  // 渲染:深度图 + Mask + 骨骼
        window.display();
    }

    astra::terminate();
    return 0;
}

astra::DepthStream configure_depth(astra::StreamReader &reader)
{
    auto depthStream = reader.stream<astra::DepthStream>();
    astra::ImageStreamMode depthMode;
    depthMode.set_width(640);
    depthMode.set_height(480);
    depthMode.set_pixel_format(astra_pixel_formats::ASTRA_PIXEL_FORMAT_DEPTH_MM);
    depthMode.set_fps(30);
    depthStream.set_mode(depthMode);
    return depthStream;
}

十二、测试结果与性能分析 🧪

12.1 算法性能对比

测试方案 平台 算法 FPS 含骨骼 结论
自研方案A Jetson Nano Yolo + DeepSort ~0.045 FPS ❌ ❌ 完全不可用
自研方案B Jetson Nano Dasiampn ❌ ❌ 未测试完成
Astra SDK Jetson Nano 内置Body Tracking ~13 FPS ✅ ✅ 选用!

12.2 功能验证矩阵

功能 状态 体验评价
🦴 18点骨骼追踪 ✅ 稳定 实时流畅,Grip手部高亮准确
🚶 主人跟随 ✅ 基本可用 角度>80°触发旋转
🛡️ 跌倒检测 ✅ 已实现 COM – Hip差<400mm触发
🚧 障碍物检测 ✅ 已实现 深度空洞分析,左右方向判断
🖐️ 手势跟随开关 ✅ 部分可用 handZ>150 开启跟随
🎨 点云可视化 ✅ Open3D可用 7类标签彩色渲染

十三、技术创新与改进方向 🚀

13.1 核心创新

# 创新点 技术方案 效果
1️⃣ 实测选型 Yolo vs Astra SDK实测对比FPS,务实决策 性能提升约288倍
2️⃣ 腰部过滤 waistM < 50 判断手部独立运动 消除身体晃动误判
3️⃣ 学术级平面拟合 协方差矩阵特征分解(TLS最小二乘法) 全局最优平面法向量
4️⃣ 自适应RANSAC 内点数越多 → 迭代次数自适应减少 加速收敛
5️⃣ 深度空洞检测 零深度像素占比统计障碍物方向 无GPU实时可用
6️⃣ 双轨可视化 SFML骨骼叠加 + Open3D语义点云 既能实时又能事后分析

13.2 改进方向

方向 目标 预期价值
🔄 平滑跟随 引入PID控制替代阈值判断 消除"抽搐式"跟随
🗣️ 语音交互 融合语音识别,语音+手势双模态 更自然的交互方式
😃 表情互动 增加面部表情检测 机器狗有"情绪反馈"
🗺️ SLAM自主导航 融合IMU+深度SLAM 从跟随进化到自主探索
🤝 多目标追踪 同时追踪多人体 多主人场景支持

十四、总结 🌅

"让机器狗'看见'你的姿态,'理解'你的意图,'保护'你的安全。"

本项目基于 Jetson Nano + Astra Pro 深度相机,为电子宠物狗打造了一套完整的人体感知与智能控制系统:

维度 成果
🦴 骨骼感知 18点人体骨骼实时追踪,含HandPose握拳状态
🎮 姿态语义 招手→前进、抬手→站立、跌倒→预警,完整动作映射
🚶 智能跟随 角度+距离双条件判断,自动追踪主人
🛡️ 安全优先 跌倒检测毫秒级响应,停车警报联动
🚧 环境感知 RANSAC深度平面拟合 + 空洞分析双重避障
🧠 学术严谨 TLS最小二乘法 + VoxelGrid + FPS下采样
📊 务实选型 实测对比选定Astra SDK,性价比最优解

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注