关于栅格GridMap结构设计的一点思考

关于栅格GridMap结构设计的一点思考

1.不同结构体的设计讨论

GridMap常用于lidar-slam建图,泊车freespace跟踪,ros里也有相应接口。最近occ也很火,用的也很多。

一般的,讨论2D GridMap,分辨率为0.1m,长宽各10m,每个格子存储一个int值。

通常的结构体设计可能会有如下

  1. 稠密的二维数组或一维数组
  2. 稀疏的map结构
  3. 压缩的bitmap

其中,稠密图的比较简单,固定大小的栈内存(vector实现为堆内存),查找O(1)。

稀疏图用map或unordered_map实现,底层是红黑树和哈希表,对应查找O(logn)和一般情况下O(1)。动态堆内存。

bitmap内存占用极少,用int的每一位存储,但只能表示0或1,适合极致压缩后的输出结果,需要根据实际应用场景使用。这里暂不做展开。

// 稠密grid
int grid1[100][100] = {0};
int grid2[10000] = {0};
std::array<int, 10000> grid3 = {0};
std::vector<int> grid4(10000, 0);
std::vector<std::vector<int>> grid4_2;

// 稀疏grid
std::map<Index, int> grid5;
std::unordered_map<Index, int, IndexHash> grid6;

// bitmap
const int size = 100 * 100;
std::bitset<size> bit_map1("100001");
std::bitset<size> bit_map2(0);
std::bitset<size> bit_map3(100);
std::cout << bit_map1 << std::endl; // 0000100001
std::cout << bit_map2 << std::endl; // 0000000000
std::cout << bit_map3 << std::endl; // 0001100100

关于稀疏grid的优缺点

  • 缺点:一般情况下都是使用稠密结构多一点,关于稀疏结构的实际使用会存在一些问题,比如map的在大量栅格下查找效率不高,而unordered_map的哈希表的内存实际也远远大于红黑树和稠密图,和实际的稀疏减少内存的初衷相违背。
  • 优点:但同时带来的优点是不需要考虑key的取值范围。栅格图一般是世界坐标系下的,如果自车的index或者障碍物的index超出规定范围,稠密grid是需要重新映射到另外一个grid中,重新定义一个原点,一般是双层buffer相互交换,减少内存的申请。而稀疏grid的key不需要考虑取值范围是否超过规定size,比较偷懒的做法。虽然不考虑key的取值范围,但是也需要对一些不用的key做删除,防止内存一直扩张。

如果是相同数量的栅格,稀疏结构的内存反而实际是远大于稠密结构的。但正因为是稀疏结构,比如在slam建图应用场景中,如果只需要考虑占据的栅格,不考虑大量的非占据和unknown栅格,那么内存理论上是少于稠密结构的。

另外对于稠密grid:

理论上一维数组模拟的二维数组访问速度更快,但实际性能差异取决于具体的访问模式和编译器优化。在某些情况下,现代编译器能够很好地优化二维数组的访问,使得它们的性能差异不明显。

一维数组在内存中是连续的,这意味着访问一维数组的元素时,缓存命中率更高。二维数组在内存中也是连续存储的,但访问方式涉及更多的偏移计算,二维数组在内存中是连续存储的(按行优先),但由于多维数组的访问涉及更多的计算和潜在的缓存未命中,可能会导致性能稍差。

测试代码有如下:

// 计算 std::unordered_map 的内存使用情况
template<typename K, typename V, typename H>
size_t unordered_map_memory_usage(const std::unordered_map<K, V, H>& um) {
    size_t size = sizeof(um); // unordered_map 对象本身的大小
    size += um.size() * sizeof(typename std::unordered_map<K, V>::value_type); // 元素的大小
    // 估算桶数组的大小
    size += um.bucket_count() * sizeof(void*); // 每个桶是一个指针
    return size;
}

int main() {
    constexpr double size_x = 10.0;
    constexpr double size_y = 10.0;
    constexpr double resolution = 0.1;
    constexpr double inv_resolution = 1 / resolution;

    constexpr int large_size_x = static_cast<int>(size_x * inv_resolution);
    constexpr int large_size_y = static_cast<int>(size_y * inv_resolution);

    // 1. dense grid
    int grid1[large_size_x][large_size_y] = {0};
    int grid2[large_size_x * large_size_y] = {0};
    std::array<int, large_size_x * large_size_y> grid3 = {0};
    std::cout << "grid1 size: " << sizeof(grid1) << std::endl; // 40000
    std::cout << "grid2 size: " << sizeof(grid2) << std::endl; // 40000
    std::cout << "grid3 size: " << sizeof(grid3) << std::endl; // 40000

    // 2. sparse gird
    using Index = std::pair<int, int>;
    struct IndexHash {
        size_t operator()(Index const &key) const {
            // return std::hash<int64_t>{}(((int64_t)key.first << 32) + (int64_t)key.second);
            return std::hash<int>{}(key.first) ^ std::hash<int>{}(key.second);
        }
    };
    std::map<Index, int> grid4;
    std::unordered_map<Index, int, IndexHash> grid5;
    std::cout << "grid4 init size: " << sizeof(grid4) << std::endl;
    std::cout << "grid5 init size: " << sizeof(grid5) << std::endl;

    for (int i = 0; i < 50000; ++i){
        grid5[Index(i, i)] = 1;
        if (grid5.size() > large_size_x * large_size_y){
            // todo:改为随机删除
            grid5.erase(grid5.begin()->first);
        }
        // max  grid5 size: 202240-10000
        std::cout << "grid5 size: " << unordered_map_memory_usage(grid5) << "-" << grid5.size() << std::endl;
    }

    return 0;
}

那么实际看下来常规的稠密结构,在大量数据应用时,是比较好一点的。唯一的点是需要考虑移动原有grid原点,遍历耗时一点。

那么下面我们用稠密结构一维数组来实现一个可以移动的grid map

2.可移动稠密栅格的实现

需求

建立一个带双buffer一维数组的grid map,实现:

  1. 初始化固定长宽范围和分辨率的栅格图。
  2. pos坐标系为前X左Y,pos原点为grid的中心点。index坐标系为右X下Y。
  3. 通过index或pos获取相应value,超出范围的return false。
  4. 移动原点复制原有图层相交区域的功能。
    1. 即有index超出栅格图范围时,可以以该index为原点建立等大的grid,交叠部分的栅格复制到新栅格。
  5. 双buffer grid实现,调用过程中禁止new新内存。

坐标系转换

三个坐标系:

  1. 自动驾驶坐标系中的全局global坐标系(前x左y)
  2. pos坐标系为平移global原点后的坐标系(前x左y)
  3. index坐标系为grid的坐标系(右x下y),同opencv图像坐标系。

以当前grid中心点为pos原点,对应点O(x0, y0),那么pos坐标系中任意一点P(x1,y1),其grid坐标为半个长宽减去对应差值。R为grid的分辨率,WH分别为宽度和高度。

index=(W/2-(x1-x0)/R, H/2-(y1-y0)/R)

那么grid index对应一维数组里的序号为:

arr[index.y * width + index.x]

注意,如果想和index坐标系保持一致,则更换index计算公式即可。根据实际应用场景挑选,这里用第一个。

在这里插入图片描述

原点/中心点移动

我们的grid map并不能无限大,都有固定的尺寸。我们可以假设自车在这个grid里运动,快要出尺寸范围时,做一次移动原点操作,让自车回到中心点位置,其看到的障碍物也需要重新移动。或者假设自车一直在grid中心,当自车移动时,移动整个grid,保证自车一直在原点,就能一直更新自车固定范围内的栅格。

为什么不考虑直接使用自车坐标系呢?因为全局坐标系下修改原点只涉及到移动,如果使用自车坐标系,自车转弯时,对应栅格的移动需要平移+旋转,增加了时间复杂度。

将grid从原点O移动到M,使用data和buffer两个grid来固定占用内存。将相交部分的data grid赋值到buffer grid。

建立buffer grid index到data grid index的映射,buffer上的index如果映射到data上,且在尺寸范围内即可赋值过来,然后swap一下,实现交换。即:

data_index_x = x - index_dx;
data_index_y = y - index_dy;
if (!(data_index_x < 0 || data_index_x >= width_ || data_index_y < 0 || data_index_y >= height_)){
    buffer_[y * width_ + x] = data_[data_index_y * width_ + data_index_x];
}

对于复制相交部分,如果是小的grid,可以直接遍历赋值;大的grid,可以先求出相交顶点,再遍历相交区域赋值给新的grid。

在这里插入图片描述

复现

如下图,建立一个5*5的栅格,分辨率0.1,对角线设置value。以及移动到(0.2, 0.1)后的栅格。

在这里插入图片描述在这里插入图片描述

注意对double/float栅格化成int时,必须加ceil/floor/round取整,static_cat会进行截断,并非四舍五入或向上向下取整。

static_cat(0.9) = 0

static_cat(-0.9) = 0

grid_map.h

#pragma once

#include <iostream>
#include <vector>
#include <cmath>

struct GridIndex {
    GridIndex() : x(0), y(0) {}

    GridIndex(int _x, int _y) : x(_x), y(_y) {}

    int x = 0;
    int y = 0;
};

// index coordinate and position coordinate.
// ------------------------->index x
// |            pos x
// |             ^
// |             |
// | pos y<------|
// |
// index y
class GridMap {
public:
    GridMap(int width, int height, double resolution, int default_value = 0);

    ~GridMap();

    inline bool GetIndex(double pos_x, double pos_y, GridIndex &index);

    bool GetValue(double pos_x, double pos_y, int &value);

    bool GetValue(GridIndex index, int &value);

    bool SetValue(double pos_x, double pos_y, int value);

    bool SetValue(GridIndex index, int value);

    void MoveOrigin(double new_pos_x, double new_pos_y);

    void MoveOriginV2(double new_pos_x, double new_pos_y);

    inline bool IsValidIndex(int index_x, int index_y);

    void Print();

    void ToImage();

private:
    int width_;
    int height_;
    double origin_pos_x_;
    double origin_pos_y_;
    double resolution_;
    double inv_resolution_;
    int default_value_;

    int size_;
    int *data_;
    int *buffer_;
};

grid_map.cpp

#include "grid_map.h"
#include <iomanip>

GridMap::GridMap(int width, int height, double resolution, int default_value) : width_(width), height_(height),
    origin_pos_x_(0), origin_pos_y_(0), resolution_(resolution),
    inv_resolution_(1 / resolution), default_value_(default_value), size_(width * height) {
    data_ = new int[size_];
    buffer_ = new int[size_];
    std::fill_n(data_, size_, default_value_);
    std::fill_n(buffer_, size_, default_value_);
}

GridMap::~GridMap() {
    delete[] data_;
    delete[] buffer_;
}

bool GridMap::GetIndex(double pos_x, double pos_y, GridIndex &index) {
    // U=W/2 - (y1-y0)
    // V=H/2 - (x1-x0)
    // ------------------------->index x(width)
    // |            pos x
    // |             ^
    // |             |
    // | pos y<------|
    // |
    // index y(height)

    // must have ceil or floor. cast(0.9)=0 cast(-0.9)=0
    int index_x = (width_ >> 1) - static_cast<int>(std::ceil((pos_y - origin_pos_y_) * inv_resolution_));
    int index_y = (height_ >> 1) - static_cast<int>(std::ceil((pos_x - origin_pos_x_) * inv_resolution_));
    if (IsValidIndex(index_x, index_y)) {
        index.x = index_x;
        index.y = index_y;
        return true;
    }
    return false;
}

bool GridMap::GetValue(double pos_x, double pos_y, int &value) {
    GridIndex index;
    if (GetIndex(pos_x, pos_y, index)) {
        value = data_[index.y * width_ + index.x];
        return true;
    }
    return false;
}

bool GridMap::GetValue(GridIndex index, int &value) {
    if (IsValidIndex(index.x, index.y)) {
        value = data_[index.y * width_ + index.x];
        return true;
    }
    return false;
}

bool GridMap::SetValue(double pos_x, double pos_y, int value) {
    GridIndex index;
    if (GetIndex(pos_x, pos_y, index)) {
        data_[index.y * width_ + index.x] = value;
        return true;
    }
    return false;
}

bool GridMap::SetValue(GridIndex index, int value) {
    if (IsValidIndex(index.x, index.y)) {
        data_[index.y * width_ + index.x] = value;
        return true;
    }
    return false;
}

void GridMap::MoveOrigin(double new_pos_x, double new_pos_y) {
    int index_dx = std::ceil((new_pos_y - origin_pos_y_) * inv_resolution_);
    int index_dy = std::ceil((new_pos_x - origin_pos_x_) * inv_resolution_);
    origin_pos_x_ = new_pos_x;
    origin_pos_y_ = new_pos_y;
    std::fill_n(buffer_, size_, default_value_);

    int data_index_x = 0;
    int data_index_y = 0;
#pragma omp parallel for
    for (int y = 0; y < height_; ++y) {
        for (int x = 0; x < width_; ++x) {
            // buffer grid index to data grid index
            data_index_x = x - index_dx;
            data_index_y = y - index_dy;
            if (IsValidIndex(data_index_x, data_index_y)) {
                buffer_[y * width_ + x] = data_[data_index_y * width_ + data_index_x];
            }
        }
    }
    std::swap(buffer_, data_);
}

void GridMap::MoveOriginV2(double new_pos_x, double new_pos_y) {
    int index_dx = std::ceil((new_pos_y - origin_pos_y_) * inv_resolution_);
    int index_dy = std::ceil((new_pos_x - origin_pos_x_) * inv_resolution_);
    int half_width = width_ >> 1;
    int half_height = height_ >> 1;
    int data_index_x = half_width - index_dx;
    int data_index_y = half_height - index_dy;

    // calculate intersection area
    int x_left = std::max(data_index_x - half_width, 0);
    int x_right = std::min(data_index_x + half_width, width_);
    int y_top = std::max(data_index_y - half_height, 0);
    int y_bottom = std::min(data_index_y + half_height, height_);
    bool is_intersected = (x_left <= x_right) && (y_top <= y_bottom);

    origin_pos_x_ = new_pos_x;
    origin_pos_y_ = new_pos_y;
    std::fill_n(buffer_, size_, default_value_);

    if (is_intersected) {
        printf("[%d %d] [%d %d] \n", x_left, x_right, y_top, y_bottom);
        int buffer_index_x = 0;
        int buffer_index_y = 0;
#pragma omp parallel for
        for (int y = y_top; y <= y_bottom; ++y) {
            for (int x = x_left; x <= x_right; ++x) {
                // data grid index to buffer grid index
                buffer_index_x = x + index_dx;
                buffer_index_y = y + index_dy;
                buffer_[buffer_index_y * width_ + buffer_index_x] = data_[y * width_ + x];
            }
        }
    }
    std::swap(buffer_, data_);
}

void GridMap::Print() {
    for (int y = 0; y < height_; ++y) {
        for (int x = 0; x < width_; ++x) {
            std::cout << std::setw(2) << data_[y * width_ + x] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << std::endl;
}

bool GridMap::IsValidIndex(int index_x, int index_y) {
    if (index_x < 0 || index_x >= width_ || index_y < 0 || index_y >= height_) {
        return false;
    }
    return true;
}

拓展

如果用opencv的cv::mat来实现则更为简单,只需要加一个映射世界坐标系到图片坐标系的函数即可,原点的移动调用opencv的warpAffine移动接口就行,还能直接可视化。

如果用Eigen::Matrix 来实现则更为高效,矩阵行列求和,移动时使用上/下三角直接赋值。

当然最省事的还是调用现有的grid_map库,封装的Matrix底层实现,也支持cv::mat的相互转换,以及ros可视化,及其他实用接口。https://github.com/ANYbotics/grid_map

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值