Dove si trova il TurtleBot 4?
Un localizzatore ICP in ROS 2, misurato contro la ground truth OptiTrack.
- OptiTrack
- Stima ICP
- Mappa del laboratorio
1. Conoscere il robot
Un TurtleBot 4 Standard, con il LiDAR montato a 90° rispetto alla marcia.
- Base
- iRobot Create 3
- Computer di bordo
- Raspberry Pi 4 Model B
- LiDAR
- RPLIDAR A1, 720 raggi per giro
- Camera
- OAK-D Pro
2. Misurare il laser
Dove il fascio incontra i pilastrini del telaio la lettura manca, e ai bordi oscilla.
- Distanza media, in metri
- Deviazione standard
3. Agganciare la mappa
Il nodo icp_map allinea ogni scansione a una mappa di 569 punti.
- Ricerca iniziale
- griglia da 1 m, 8 orientamenti
- Corrispondenze
- entro 0,5 m
- Iterazioni
- 15 in ricerca, 50 in tracking
- Raggi usati
- oltre 0,3 m dal sensore
pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
pcl::registration::TransformationEstimation2D<pcl::PointXYZ, pcl::PointXYZ>::Ptr est(
new pcl::registration::TransformationEstimation2D<pcl::PointXYZ, pcl::PointXYZ>);
icp.setTransformationEstimation(est);
icp.setInputSource(current_cloud);
icp.setInputTarget(map_cloud_);
icp.setMaxCorrespondenceDistance(0.5);
icp.setMaximumIterations(50);
4. Chiedere la verità a OptiTrack
Dodici telecamere a infrarossi seguono i marker. A robot fermo l’ICP sbaglia di circa 4 cm e 1°.
5. Confrontare tre percorsi
Sul cerchio l’errore medio dell’ICP è di 2,39 cm.
| Confronto | Errore medio | σ |
|---|---|---|
| OptiTrack sull’ideale | 0,30 cm | 0,32 cm |
| ICP sull’ideale | 1,34 cm | 1,35 cm |
| ICP su OptiTrack | 2,39 cm | 1,74 cm |
- Ideale
- OptiTrack
- ICP
| Confronto | Errore medio | σ |
|---|---|---|
| OptiTrack sull’ideale | 3,87 cm | 3,56 cm |
| ICP sull’ideale | 5,71 cm | 4,79 cm |
| ICP su OptiTrack | 3,42 cm | 3,18 cm |
| Chiusura, OptiTrack | 6,78 cm | |
| Chiusura, ICP | 8,80 cm |
| Confronto | Errore medio | σ |
|---|---|---|
| OptiTrack sull’ideale | 5,19 cm | 3,98 cm |
| ICP sull’ideale | 7,87 cm | 5,34 cm |
| ICP su OptiTrack | 6,01 cm | 4,05 cm |
| Chiusura, OptiTrack | 10,30 cm | |
| Chiusura, ICP | 10,62 cm |
Otto nodi ROS 2 in C++
Apri una riga per leggere il passaggio che conta.
icp_map
Localizza il robot sulla mappa con ICP planare262 righeLa ricerca globale che aggancia la prima posa double step = 1.0; double test_angles[8] = {0.0, M_PI_4, M_PI_2, 3*M_PI_4, M_PI, -3*M_PI_4, -M_PI_2, -M_PI_4}; // iteriamo su tutta la superficie della stanza for (double x = min_pt.x; x <= max_pt.x; x += step) { for (double y = min_pt.y; y <= max_pt.y; y += step) { for (int i = 0; i < 8; ++i) { double cos_y = std::cos(test_angles[i]); double sin_y = std::sin(test_angles[i]); Eigen::Matrix4f guess = Eigen::Matrix4f::Identity(); guess(0, 0) = cos_y; guess(0, 1) = -sin_y; guess(1, 0) = sin_y; guess(1, 1) = cos_y; guess(0, 3) = x; guess(1, 3) = y; pcl::PointCloud<pcl::PointXYZ> temp_final; icp.setMaximumIterations(15); icp.align(temp_final, guess); if (icp.hasConverged()) { double score = icp.getFitnessScore(); if (score < best_score) { best_score = score; best_guess = guess; } } } } }laser_stat
Media e deviazione standard di ogni raggio79 righeLe somme accumulate a ogni scansione for (int i = 0; i < num_raggi; i++) { float distanza = msg->ranges[i]; if (std::isfinite(distanza)) { somme_[i] += distanza; somme_quadrati_[i] += (distanza * distanza); conteggi_[i]++; } }virtual_bumper
Ferma il robot a 0,35 m dall’ostacolo96 righeIl settore frontale e la soglia di arresto if (a_robot > -0.5 && a_robot < 0.5) { if (r < min_distance) { min_distance = r; } } } if (min_distance < threshold_) { cmd.linear.x = 0.0; cmd.angular.z = 0.0; } else { cmd.linear.x = speed_; cmd.angular.z = 0.0; }obstacle_avoider
Gira verso il lato più libero105 righeLa scelta tra sinistra e destra if (min_front < threshold_) { cmd.linear.x = 0.0; if (min_left > min_right) { cmd.angular.z = turnrate_; } else { cmd.angular.z = -turnrate_; } } else { cmd.linear.x = speed_; cmd.angular.z = 0.0; }quadrato_nodo
Quadrato da 1 m a tempo, senza feedback88 righeLa curva di 90° contata in cicli // FASE 1: GIRA DI 90 GRADI else if (fase_ == 1) { messaggio.angular.z = 0.785; contatore_++; // CURVA: 2 secondi (20 cicli) if (contatore_ >= 20) { contatore_ = 0; lati_completati_++; if (lati_completati_ >= 4) { fase_ = 2; // Ha fatto 4 lati completi } else { fase_ = 0; RCLCPP_INFO(this->get_logger(), "==> PARTENZA LATO %d", lati_completati_ + 1); } } }quadrato_odom_nodo
Lo stesso quadrato, chiuso sull’odometria95 righeIl lato da 1 m misurato su /odom // FASE 0: VAI DRITTO FINO A 1 METRO if (fase_ == 0) { double distance = std::sqrt(std::pow(current_x - start_x_, 2) + std::pow(current_y - start_y_, 2)); if (distance < 1.0) { cmd.linear.x = 0.15; cmd.angular.z = 0.0; } else { cmd.linear.x = 0.0; fase_ = 1; init_pose_captured_ = false; RCLCPP_INFO(this->get_logger(), "inizio curva %d ", lati_completati_ + 1); } }cerchio_nodo
Cerchio di raggio 50 cm a velocità costante45 righeDue velocità costanti bastano void cerchio_robot() { geometry_msgs::msg::Twist messaggio; messaggio.linear.x = 0.2; messaggio.angular.z = 0.4; publisher_->publish(messaggio); }slalom_nodo
Curve alternate ogni due secondi69 righeDestra e sinistra, quattro cicli per parte // fase 0 destra if (fase_==0){ messaggio.linear.x = 0.45; messaggio.angular.z = 1.2; cont_=cont_+1; if (cont_>=4){ fase_=1; cont_=0; } }