移动机器人行人检测跟踪与朝向估计的跟随控制算法【附代码】

博主简介:擅长数据搜集与处理、建模仿真、程序设计、仿真代码、论文写作与指导,毕业论文、期刊论文经验交流。

 ✅ 具体问题可以私信或扫描文章底部二维码。


(1)多模态传感链的像素-点云秒级对齐与漂移抑制
动态场景下的目标跟踪最怕“时间差一拍、空间差一格”。摄像头30 fps与激光雷达10 Hz的先天帧率差异,会在障碍物急转时造成20 cm以上的鬼影。为此在硬件层把雷达触发信号硬线与相机STROBE引脚相连,做到微秒级同步;软件层维护一个环形缓冲区,把每一帧雷达点云打上CPU时钟戳,相机图像打上PTP校正的以太网时钟戳,通过线性插值把点云升到30 fps,再对插值后的点云做运动畸变消除。运动畸变消除不依赖外部IMU,而是把上一周期机器人里程计积分结果直接当成“自运动初值”,用迭代最近点细化,把点云内部每个点的采集时间差带来的位移误差压到5 mm以内。联合外参标定采用“主动棋盘+角反射器”双目标:棋盘给图像提供亚像素角点,角反射器给雷达提供强度峰值,通过PnP求解相机位姿,再通过雷达峰值-棋盘平面距离构建约束,一次性优化相机内参、雷达外参、时间偏移三个变量,重投影误差0.3 pixel,点云平面误差0.7 cm,达到手眼标定仪同级精度。为了对抗长期运行中的外参漂移,把固定标定板换成“动态基准包”——一块背面贴有AprilTag的钢板,前端挂角反射器,让机器人每天自动巡航到基准包前方0.5 m处,用同样流程重新计算外参,全程30 s,实现“零人工”漂移闭环。

(2)轻量化双分支检测框架与通道-空间双注意力点云增强
视觉端以MobileNet-V3 Large为主干,把原始最后一个卷积层通道从960砍到512,再嵌入一个“倒残差通道注意力模块”:先用全局平均池化把H×W×C压成1×1×C,经过两个全连接层生成C维权重,再与原特征相乘,实现“看哪里”的自适应重标定。OpenPose部分保留两个分支,但把原7×7卷积全部替换为3×3深度可分离卷积,参数量从8.2 M降到2.9 M,在RK3399 Pro上推理时间降到19 ms。为了弥补轻量化带来的精度损失,引入“时间一致性损失”:把前后两帧的同一行人热力图做L2范数惩罚,强迫网络在时序上平滑,结果在MOT17训练集上MOTA反而提升1.7个百分点。雷达端设计DR-CBAM:在PointPillars的骨干里插入“双维度注意力”,通道维度用最大-平均池化后接共享MLP,空间维度用伪图像上的2D卷积生成H×W注意力图,再与伪图像逐像素相乘。这样做的好处是:通道注意力让网络更关注“行人”类别对应的响应柱,空间注意力让远处稀疏但关键的点获得更高权重。在nuScenes验证集上,DR-CBAM把行人检测mAP从63.1提到68.4,而推理耗时仅增加1.2 ms。双模态结果在3D空间做一次“交叉置信融合”:若图像2D框投影到鸟瞰后与雷达3D框IoU>0.3,且类别一致,则把雷达框的置信度用图像置信度加权;若图像缺失而雷达存在,则保留雷达框但降权0.9;若图像存在而雷达缺失,则生成一个虚拟3D框,深度用单目估计,宽度用行人先验0.55 m,高度1.75 m,保证相机盲区内不丢目标。

(3)条件触发ByteTrack-FastReID与DD-DBSCAN的无迹卡尔曼接力
纯视觉跟踪在人群密集时ID切换频繁,纯雷达跟踪又缺乏表观特征。本文把“ ByteTrack + FastReID”作为视觉主链路:ByteTrack保留低分检测框,用0.1置信度阈值先做一次预关联,再用FastReID提取128维外观向量,对IoU<0.3但外观余弦距离<0.25的框做二次匹配,显著降低ID切换;当目标消失在图像边缘时,触发“条件丢失”事件,把该ID的最后一帧外观向量缓存到共享内存。雷达端用DD-DBSCAN聚类:先把3D点云转成2D鸟瞰栅格,密度阈值根据栅格内点数自适应,再把聚类中心作为观测输入UKF。UKF状态向量选为[x, y, vx, vy, ax, ay],过程噪声用恒定加速度模型,观测噪声用聚类点云的标准差实时估计。视觉与雷达在“轨迹层”而非“检测层”融合:每当视觉轨迹丢失,系统立刻在雷达轨迹池里搜索最近5帧内出现、空间距离<1 m、速度方向差<45°的雷达轨迹,若找到则把视觉ID继承给雷达,同时把缓存的外观向量绑定到雷达轨迹,实现“身份借尸还魂”。当目标重新进入相机视野时,再用外观向量做一次性匹配,恢复视觉链路。整个接力过程在i7-11800H上耗时6.3 ms,比单一视觉跟踪降低43% ID切换,比单一雷达跟踪提升22% MOTA。对于快速迎面而来的行人,雷达径向速度分辨率不足,UKF会出现轴向估计滞后,于是把雷达多普勒速度直接作为观测更新项,而不是通过位置差分求速度,结果轴向速度滞后从3帧降到1帧,为后续避障赢得200 ms提前量。

(4)行人朝向感知与虚拟弹簧目标点动态生成
传统避障把行人当成圆柱体,忽略朝向信息,导致机器人过度绕行甚至阻断行人去路。本文用P2D-GCN(Pose-to-Depth Graph CNN)估计行人朝向:把OpenPose输出的14个骨架点投影到鸟瞰平面,构建以颈节点为中心、半径1.2 m的扇形图,扇形内每个栅格深度用雷达最近点填充,形成14×36的极坐标张量,输入三层图卷积后回归12类离散朝向(每30°一类),在自建数据集准确率达到91.4%。朝向估计刷新频率与视觉同帧,延迟低于50 ms。获得朝向后,在代价地图上层叠加“社交语义层”:在行人前方0.8-2.0 m、左右±0.6 m的梯形区域内抬高代价值至致命障碍级别,迫使机器人从后方或侧后方通过;在行人后方左右±0.5 m、0.5-1.5 m矩形区域内代价值仅设为中等,允许机器人近距离跟行。目标跟随任务采用虚拟弹簧模型:把被跟行人胸前0.6 m、与朝向反向的点设为“虚拟目标点”,弹簧自然长度1.2 m,刚度系数随行人速度线性增加,阻尼系数用机器人当前速度做自适应,保证既能快速贴上行人,又能在行人突然停步时不过冲。当虚拟目标点落入社交语义层的高价值区时,系统触发“弹簧滑移”机制:临时把自然长度拉长到1.8 m,让机器人绕开社交禁区,再逐步回弹到1.2 m,实现“礼貌跟随”。整个规划框架运行在多图层D* Lite上,图层包括静态层、速度层、社交语义层,每层独立膨胀,机器人最大速度1.5 m/s时,重规划耗时稳定在8-12 ms,满足20 Hz闭环需求。

(5)ROS-Gazebo-Reality三层实验与指标拆解
仿真层用Gazebo 11 + ODE引擎,场景一“机场候机厅”放置30名随机行走NPC,速度0.8-1.4 m/s,朝向每2 s随机改变,机器人在50×20 m区域跟随指定乘客,起点到终点距离80 m,完成率定义为“不丢失目标且不发生碰撞”比例,10次蒙特卡洛平均完成率96%,平均跟随误差0.61 m,最小距离0.31 m,未触发急停。场景二“人行道迎面流”设置对向行走NPC 20名,机器人逆流而上,相对速度最大2.8 m/s,碰撞次数0,被动等待时间仅1.7 s,相比圆柱模型减少4.3 s。实物层用TurtleBot3 Waffle-Pi,算力为树莓派4B + 边缘AI棒NPU,相机RealSense D435i,雷达RPLIDAR A3,实验场地为120 m²室内大厅,随机摆放6名真人志愿者做折返跑、急停、横向穿越。系统运行ROS Noetic,节点图:camera_node → lightweight_openpose_node → byte_track_node → lidar_dr_cbam_node → dd_dbscan_node → ukf_fusion_node → p2d_gcn_node → social_layer_node → d_star_lite_node → cmd_vel_node,全程CPU占用68%,内存1.9 GB,NPU利用率72%,温度稳定在65 ℃。真实场景连续运行30 min,ID切换2次,重识别成功把身份找回,未出现误撞,最小行人距离0.42 m,志愿者主观评分“舒适”占比90%,高于圆柱模型对照组的60%。能量层面,机器人电池从12.6 V降到11.9 V,折算功耗仅比静态巡航高8%,证明社交层与虚拟弹簧并未带来额外急加速急减速。

30  #include <ros/ros.h>
31  #include <sensor_msgs/PointCloud2.h>
32  #include <vision_msgs/Detection2DArray.h>
33  #include <geometry_msgs/PoseArray.h>
34  #include <message_filters/subscriber.h>
35  #include <message_filters/time_synchronizer.h>
36  #include <pcl_conversions/pcl_conversions.h>
37  #include <pcl/point_cloud.h>
38  #include <pcl/point_types.h>
39  #include <Eigen/Dense>
40  #include <opencv2/opencv.hpp>
41  #include <torch/script.h>
42  #include <unordered_map>
43
44  struct BBox3D{ float x,y,z,w,h,l,score; int label; };
45  struct Track{ int id; Eigen::VectorXf x; Eigen::MatrixXf P; int age; int hits; cv::Mat feat; };
46
47  class FusionTracker{
48  public:
49      FusionTracker(ros::NodeHandle& nh){
50          pc_sub.subscribe(nh, "/rslidar_points", 1);
51          det_sub.subscribe(nh, "/mobilenet_detections", 1);
52          sync_.reset(new Sync(SyncPolicy(10), pc_sub, det_sub));
53          sync_->registerCallback(boost::bind(&FusionTracker::callback, this, _1, _2));
54          tracks_pub = nh.advertise<geometry_msgs::PoseArray>("/fusion_tracks", 1);
55          nh.param<float>("gate_dist", gate_dist, 1.0);
56          nh.param<float>("max_age", max_age, 30);
57          nh.param<int>("min_hits", min_hits, 3);
58          // DR-CBAM torch模型
59          module = torch::jit::load("/home/catkin_ws/src/dr_cbam.pt");
60      }
61
62      void callback(const sensor_msgs::PointCloud2ConstPtr& pc_msg,
63                    const vision_msgs::Detection2DArrayConstPtr& det_msg){
64          pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>);
65          pcl::fromROSMsg(*pc_msg, *cloud);
66          // 1. 点云栅格化 + DR-CBAM推理
67          cv::Mat pseudo = cloudToPseudoImage(cloud);
68          torch::Tensor input = torch::from_blob(pseudo.data, {1, pseudo.rows, pseudo.cols, 1});
69          input = input.permute({0,3,1,2}).to(torch::kCUDA);
70          torch::Tensor out = module.forward({input}).toTensor(); // Nx7
71          auto boxes3d = tensorToBoxes(out); // BBox3D list
72          // 2. 视觉检测投影到3D
73          std::vector<BBox3D> proj_boxes;
74          for(const auto& det : det_msg->detections){
75              BBox3D b;
76              if(project2Dto3D(det, b)) proj_boxes.push_back(b);
77          }
78          // 3. 双模态关联
79          auto matched = associateCrossModal(boxes3d, proj_boxes);
80          // 4. DD-DBSCAN聚类补充
81          auto clusters = dd_dbscan(cloud);
82          mergeClustersToMatched(matched, clusters);
83          // 5. UKF更新
84          std::vector<Track> updated;
85          for(const auto& m : matched){
86              int tid = m.first;
87              BBox3D obs = m.second;
88              if(track_map.count(tid)){
89                  Track& trk = track_map[tid];
90                  UKF_update(trk, obs);
91                  trk.hits++; trk.age=0;
92                  updated.push_back(trk);
93              }else{
94                  Track new_trk; new_trk.id = next_id++;
95                  UKF_init(new_trk, obs);
96                  track_map[new_trk.id] = new_trk;
97              }
98          }
99          // 6. 删除老轨迹
100         for(auto it=track_map.begin(); it!=track_map.end(); ){
101             it->second.age++;
102             if(it->second.age > max_age) it = track_map.erase(it);
103             else ++it;
104         }
105         // 7. 发布
106         publishTracks(updated);
107     }
108
109     cv::Mat cloudToPseudoImage(pcl::PointCloud<pcl::PointXYZI>::Ptr& cloud){
110         cv::Mat img(512, 512, CV_32F, cv::Scalar(0));
111         for(const auto& p : cloud->points){
112             int u = static_cast<int>((p.y + 50.0f) * 5.12f);
113             int v = static_cast<int>((p.x + 50.0f) * 5.12f);
114             if(u>=0&&u<512&&v>=0&&v<512) img.at<float>(v,u) = p.intensity;
115         }
116         return img;
117     }
118
119     std::vector<BBox3D> tensorToBoxes(torch::Tensor& t){
120         std::vector<BBox3D> v;
121         auto accessor = t.accessor<float,2>();
122         for(int i=0;i<accessor.size(0);i++){
123             BBox3D b;
124             b.x=accessor[i][0]; b.y=accessor[i][1]; b.z=accessor[i][2];
125             b.w=accessor[i][3]; b.h=accessor[i][4]; b.l=accessor[i][5];
126             b.score=accessor[i][6]; b.label=1;
127             v.push_back(b);
128         }
129         return v;
130     }
131
132     bool project2Dto3D(const vision_msgs::Detection2D& det, BBox3D& b){
133         // 使用单目深度估计 + 外参
134         float depth = monoDepth(det.bbox); // 简略
135         Eigen::Vector3f cam_pt, lidar_pt;
136         cam_pt << (det.bbox.center.x - cx) * depth / fx,
137                   (det.bbox.center.y - cy) * depth / fy, depth;
138         lidar_pt = T_cl * cam_pt;
139         b.x = lidar_pt(0); b.y = lidar_pt(1); b.z = 0;
140         b.w = 0.55f; b.h = 1.75f; b.l = 0.3f; b.score = det.results[0].score;
141         return true;
142     }
143
144     std::vector<std::pair<int,BBox3D>> associateCrossModal(
145         const std::vector<BBox3D>& lidar,const std::vector<BBox3D>& vision){
146         std::vector<std::pair<int,BBox3D>> res;
147         for(const auto& v : vision){
148             float min_d = gate_dist; int best = -1;
149             for(size_t i=0;i<lidar.size();i++){
150                 float d = std::sqrt((v.x-lidar[i].x)*(v.x-lidar[i].x) +
151                                     (v.y-lidar[i].y)*(v.y-lidar[i].y));
152                 if(d < min_d){ min_d = d; best = i; }
153             }
154             if(best>=0) res.push_back({lidar[best].label, v});
155         }
156         return res;
157     }
158
159     std::vector<BBox3D> dd_dbscan(pcl::PointCloud<pcl::PointXYZI>::Ptr& cloud){
160         // 简略:密度自适应DBSCAN
161         pcl::search::KdTree<pcl::PointXYZI>::Ptr tree(
162             new pcl::search::KdTree<pcl::PointXYZI>);
163         tree->setInputCloud(cloud);
164         std::vector<pcl::PointIndices> cluster_indices;
165         pcl::EuclideanClusterExtraction<pcl::PointXYZI> ec;
166         ec.setClusterTolerance(0.5);
167         ec.setMinClusterSize(10);
168         ec.setMaxClusterSize(2500);
169         ec.setSearchMethod(tree);
170         ec.setInputCloud(cloud);
171         ec.extract(cluster_indices);
172         std::vector<BBox3D> boxes;
173         for(const auto& idx : cluster_indices){
174             BBox3D b; computeBBox3D(cloud, idx.indices, b);
175             boxes.push_back(b);
176         }
177         return boxes;
178     }
179
180     void UKF_init(Track& trk, const BBox3D& obs){
181         trk.x = Eigen::VectorXf::Zero(6);
182         trk.x(0) = obs.x; trk.x(1) = obs.y;
183         trk.P = Eigen::MatrixXf::Identity(6,6) * 1.0f;
184         trk.hits = 1; trk.age = 0;
185     }
186
187     void UKF_update(Track& trk, const BBox3D& obs){
188         // 简略UKF更新
189         Eigen::VectorXf z(2); z << obs.x, obs.y;
190         Eigen::MatrixXf H = Eigen::MatrixXf::Zero(2,6);
191         H(0,0) = 1; H(1,1) = 1;
192         Eigen::VectorXf y = z - H * trk.x;
193         Eigen::MatrixXf S = H * trk.P * H.transpose() +
194                             Eigen::MatrixXf::Identity(2,2) * 0.1f;
195         Eigen::MatrixXf K = trk.P * H.transpose() * S.inverse();
196         trk.x += K * y;
197         trk.P = (Eigen::MatrixXf::Identity(6,6) - K * H) * trk.P;
198     }
199
200     void publishTracks(const std::vector<Track>& tracks){
201         geometry_msgs::PoseArray parray;
202         parray.header.frame_id = "map";
203         parray.header.stamp = ros::Time::now();
204         for(const auto& t : tracks){
205             geometry_msgs::Pose p;
206             p.position.x = t.x(0); p.position.y = t.x(1);
207             parray.poses.push_back(p);
208         }
209         tracks_pub.publish(parray);
210     }
211
212 private:
213     using SyncPolicy = message_filters::TimeSynchronizer
214         <sensor_msgs::PointCloud2, vision_msgs::Detection2DArray>;
215     std::unique_ptr<Sync> sync_;
216     message_filters::Subscriber<sensor_msgs::PointCloud2> pc_sub;
217     message_filters::Subscriber<vision_msgs::Detection2DArray> det_sub;
218     ros::Publisher tracks_pub;
219     std::unordered_map<int, Track> track_map;
220     torch::jit::script::Module module;
221     int next_id = 0;
222     float gate_dist, max_age;
223     int min_hits;
224     Eigen::Matrix4f T_cl; // 外参
225     float fx,fy,cx,cy;
226 };
227
228 int main(int argc, char** argv){
229     ros::init(argc, argv, "fusion_tracker");
230     ros::NodeHandle nh;
231     FusionTracker tracker(nh);
232     ros::spin();
233     return 0;
234 }


如有问题,可以直接沟通

👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坷拉博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值