cartographer:occupancy_grid_node_main
本文分析cartographer
使用二維激光傳感器的SLAM, google cartographer的地圖生成在occupancy_grid_node_main中實現。先放上生成的地圖,走廊約爲90m*60m
。左邊的range_data_inserter
中的insert_free_space = true
,右邊false
。表示是否更新raytracy
中的free
柵格的概率。
系統簡介
1. 系統採用local和global的方法來優化位姿
2. 在local的匹配中,採用的是類似hector_slam
的線性插值的方式,不過爲了解決雙線性插值的不連續性,採用了雙三線性插值; 在global的方法中借鑑了olson的相關性掃描匹配方式,通過全局位姿優化來解決誤差累計。
3. 在子圖構建完成(已經加入足夠的激光數據幀), 會用來做loop closure
,當激光現在的位姿估計和之前的子圖足夠近的時候(再次訪問一個地圖),在搜索窗口內,完成類似與olson的相關性掃描匹配,如果匹配結構很好就增加loop closing constraint
。
子圖介紹
首先分析的是輸出的地圖,由次來分析採用的算法結構。這個ROS節點的任務是接受cartographer_node
節點運行計算得到的子圖序列和子圖內容,拼接成一個大的地圖的工作。如下圖所示:上面的柵格地圖是cartographer
得到的整體的地圖,下面的兩張是其中兩個子圖對應的柵格地圖。
occupancy_grid_node_main中維護的全局地圖數據:
std::map<SubmapId, SubmapSlice> submap_slices_ GUARDED_BY(mutex_);
在構造函數中,地圖的生成需要子圖序列之間的關係,以及每個子圖的數據。訂閱子圖服務的client_
和子圖序列的話題關係的submap_list_subscriber_
client_(node_handle_.serviceClient<::cartographer_ros_msgs::SubmapQuery>(kSubmapQueryServiceName)),
submap_list_subscriber_(node_handle_.subscribe(kSubmapListTopic, kLatestOnlyPublisherQueueSize,
boost::function<void(const cartographer_ros_msgs::SubmapList::ConstPtr&)>(
[this](const cartographer_ros_msgs::SubmapList::ConstPtr& msg) {HandleSubmapList(msg);}))),
occupancy_grid_publisher_(node_handle_.advertise<::nav_msgs::OccupancyGrid>(
kOccupancyGridTopic, kLatestOnlyPublisherQueueSize,true /* latched */)),
occupancy_grid_publisher_timer_(node_handle_.createWallTimer(
::ros::WallDuration(publish_period_sec),&Node::DrawAndPublish, this)) {}
子圖序列如下,可以看出子圖序列包含所有子圖在map
座標系下的位姿,已經完成構建的子圖,不再改變submap_version
的值也不改變了submap_version:180
。如下圖中Trajectory 0: 0.180 1.180……
,表示0軌跡下的子圖。每一次激光數據到達的時候會修改最新的兩個子圖,例如:submap_index的33和34
的submap_version
的還會繼續更新,直到submap_index
爲33的地圖不再修改而增添新的子圖。在序列中也能看出子圖的角度也有變化。
子圖序列的主題回調函數的任務是將子圖的數據存入到submap_slices_
。首先判斷每個子圖的submap_version
是否不再更新,當子圖存在且內容不再變化,只改變子圖在map
座標系下的位姿:submap_slice.pose=ToRigid3d(submap_msg.pose)
關於ActiveSubmaps代碼中的註釋是這樣說的,除了地圖在初始化的時候,只有一個子圖。其它的時刻,數據都有兩個子圖插入,一個old submap
和一個new submap
。 然後當確定數量的數據幀加入後,old submap
不再變化,new submap
開始初始化。這個時候new submap
完成初始化變成old submap
,用來完成scan-to-map
匹配,也會生成一個new submap
。 也就是說有個新老交替的過程用來對接scan-to-map
匹配。
在cartographer_ros/cartographer_ros/submap.cc
中,使用了FastGunzipString
可以看出子圖的數據被壓縮了,數據量大的情況下是應該的。
auto response = ::cartographer::common::make_unique<SubmapTextures>();
response->version = srv.response.submap_version;
for (const auto& texture : srv.response.textures) {
std::string compressed_cells(texture.cells.begin(), texture.cells.end());
std::string cells;
::cartographer::common::FastGunzipString(compressed_cells, &cells);
const int num_pixels = texture.width * texture.height;
CHECK_EQ(cells.size(), 2 * num_pixels);
…………………………………………………………………………………………………………………………………………
}
response->textures.emplace_back(
SubmapTexture{intensity, alpha, texture.width, texture.height,
texture.resolution, ToRigid3d(texture.slice_pose)});
解壓的函數如下,完成數據的SubmapTexture
數據的解壓縮。
inline void FastGunzipString(const string& compressed, string* decompressed) {
boost::iostreams::filtering_ostream out;
out.push(boost::iostreams::gzip_decompressor());
out.push(boost::iostreams::back_inserter(*decompressed));
boost::iostreams::write(out, reinterpret_cast<const char*>(compressed.data()),
compressed.size());
}
map
主題的發佈由定時器設定,執行DrawAndPublish
,在這個過程中,首先獲得由submap_slices_
子圖組成的整體地圖:auto painted_slices=PaintSubmapSlices(&submap_slices_,resolution_)
;然後發佈出去Node::PublishOccupancyGrid
。
地圖格式
在這個過程中,我們可以單獨的獲取到每一個子圖。每一個子圖都是在統一的map
座標系下。如下圖,我發佈了三個子圖的topic
:map0,map3,map10
。黑色是0表示unknown
,白色是254表示free
,63表示occupied
,其它的在63–>200之間吧。因此在這裏分析一下ROS
中的地圖的數據格式:nav_msgs::OccupancyGrid
std_msgs/Header header ##
nav_msgs/MapMetaData info ## width,height,resolution,origin
int8[] data ## 行須優先,以(0,0)左上爲origin開始,概率在[0,100],未知爲-1
如下:左邊爲數據存在int8[] data中的index,右邊是對應的map座標系下的座標。(0,0)爲origin
因此遞推,可以得到其餘各個柵格點的數據。
0 1 2 3 4 (0,0) (0,1) (0,2) (0,3)
5 6 7 8 9 (1,0) (1,1) (1,2) (1,3)
……………………… …………………………………………………………
在使用rosrun map_server map_saver -f filename
的時候,執行如下的代碼,注意這裏保存地圖的時候座標系發生了變換,以左下爲origin;ROS map_server,因此可以知道在保存地圖的時候做了一次上下對調。使得
for(unsigned int y = 0; y < map->info.height; y++) {
for(unsigned int x = 0; x < map->info.width; x++) {
unsigned int i = x + (map->info.height - y - 1) * map->info.width;
int pixel = (double)(100.0-map->data[i]) *2.54;
fputc(pixel, out);
}
}
如下:地圖的origin爲左下,可遞推得到每一柵格的位置
……………………… …………………………………………………………
5 6 7 8 9 (1,0) (1,1) (1,2) (1,3)
0 1 2 3 4 (0,0) (0,1) (0,2) (0,3)
之後在rosrun map_server map_server filename.yaml
加載柵格地圖的時候,爲了還原到之前topic
的數據格式,需要將座標系再次變換,變換爲nav_msgs::OccupancyGrid
的格式。因此在其代碼image_loader.cpp
中:
for(j = 0; j < resp->map.info.height; j++)……
for (i = 0; i < resp->map.info.width; i++)……
resp->map.data[MAP_IDX(resp->map.info.width,i,resp->map.info.height - j - 1)] = value;
#define MAP_IDX(sx, i, j) ((sx) * (j) + (i))
然後將子圖對應的柵格數據轉換成sensor_msgs::PointCloud2
類型的cloud
數據發佈出來,如右下圖的彩色點雲,點雲的參考座標系爲map
,這樣做是爲了將柵格地圖轉換成點雲,同時在RVIZ
中觀察轉換的正確性。此時是按照nav_msgs::OccupancyGrid
的數據格式了,因此柵格轉換成點雲的計算如下:
for(c=0;c<sizeMap;c++){
int y = c / map_resp_.map.info.width;
int x = c - y * map_resp_.map.info.width;
double xx = origin[0] + x * res;
double yy = origin[1] + y * res;
在下面的RVIZ
中的顯示得到了驗證
修改使用rosrun map_server map_server
加載各個子圖,爲了保證能夠正常加載,需要將不同的子圖的topic
作爲參數傳入,保證topic
的frame_id
是統一的即可。子圖放在一起如下:
保存子圖
在cartographer
中一旦子圖生成,子圖內的數據目前不會發生改變,那麼怎麼保存子圖的數據?我在occupancy_grid_node_main
中加入了一個服務rosservice call /getsubmap index
來完成這個任務。
1. 使用srv文件來完成服務的使用,在cartographer_ros/cartographer_ros_msgs/srv/
中添加GetSubmap.srv
,如下:
# Get the map as a nav_msgs/OccupancyGrid
int32 submap_index
bool write_map
---
nav_msgs/OccupancyGrid map
在對應的CMakeLists.txt
中加入對nav_msgs
的依賴,因爲使用了nav_msgs/OccupancyGird
,包括package.xml
中<depend>nav_msgs</depend>
。
2. 在occupancy_grid_node_main
中加入advertiseService
,對應的服務定義和回調函數:
submap_service_(node_handle_.advertiseService("getsubmap",&Node::submapCallback,this));//回調函數如下
bool Node::submapCallback(cartographer_ros_msgs::GetSubmap::Request &req,
cartographer_ros_msgs::GetSubmap::Response &res){
int submap_index = req.submap_index;
auto painted_slices = PaintSubmapSlices(&submap_slices_, resolution_,submap_index);
nav_msgs::OccupancyGrid occupancy_grid;
cairo_surface_t *surface = painted_slices.surface.get();
Eigen::Array2f& origin=painted_slices.origin;
const int width = cairo_image_surface_get_width(surface);
const int height = cairo_image_surface_get_height(surface);
occupancy_grid.header.stamp = ros::Time::now();
occupancy_grid.header.frame_id = "map"; //所有的子圖都要在統一的座標系下表示
occupancy_grid.info.map_load_time = ros::Time::now();
occupancy_grid.info.resolution = resolution_;
……………………………………………………………………………………………………………………
const uint32_t* pixel_data = reinterpret_cast<uint32_t*>(cairo_image_surface_get_data(surface));
occupancy_grid.data.reserve(width * height);
for (int y = height - 1; y >= 0; --y) {
for (int x = 0; x < width; ++x) {
const uint32_t packed = pixel_data[y * width + x];
const unsigned char color = packed >> 16;
const unsigned char observed = packed >> 8;
const int value = observed == 0 ? -1
: ::cartographer::common::RoundToInt((1. - color / 255.) * 100.);
CHECK_LE(-1, value); CHECK_GE(100, value);
occupancy_grid.data.push_back(value);
}
}
res.map=occupancy_grid;
if(true) { // 默認一直執行保存地圖
std::string mapname_ ="map" ;
mapname_.append(boost::lexical_cast<std::string>(submap_index));
ROS_INFO("Received a %d X %d map @ %.3f m/pix", width,height,resolution_);
std::string mapdatafile = mapname_ + ".pgm";
ROS_INFO("Writing map occupancy data to %s", mapdatafile.c_str());
FILE* out = fopen(mapdatafile.c_str(), "w");
if (!out){
ROS_ERROR("Couldn't save map file to %s", mapdatafile.c_str());
return false;
}
fprintf(out, "P5\n# CREATOR: Map_generator.cpp %.3f m/pix\n%d %d\n255\n",resolution_,width,height);
// 參考ROS 的map_server保存地圖
……………………………………………………………………………………………………………………
}
return true;
}
同時知道的是if (occupancy_grid_publisher_.getNumSubscribers() == 0) {return;}
,因此需要訂閱map
主題。
3. 修改cartographer/cartographer/io/submap_painter.h(.cpp)
中的PaintSubmapSlices
函數,增加index
用來傳遞對應的submap_index
。以及這個函數調用的需要index
參數的函數。
PaintSubmapSlicesResult PaintSubmapSlices(
std::map<::cartographer::mapping::SubmapId, SubmapSlice>* submaps,
const double resolution,int index);
子圖加載
在bash中加載所有的子圖:
#!/bin/bash
source /home/robot/tempRos_ws/devel/setup.bash
for ((i=0;i<16;i++))
do
echo $i
# map_file="/home/robot/Documents/submapPgm/used_submap/map${i}.yaml"
# echo ${map_file}
# mapTopic="map${i}"
rosrun map_server map_server "/home/robot/Documents/submapPgm/used_submap/map${i}.yaml" "map${i}" &
done