CloudCompare源码分析_八叉树(Octree)算法基础CC中的八叉树结构

本文详细解析了CloudCompare中的Octree算法,涉及预计算位移值、cell codes结构、层级划分和cellPos编码。理解了如何通过这些结构快速定位和索引点云数据,提升计算性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

官方参考地址:CloudCompare octree - CloudCompareWiki

CC的octree算法主要体现在DgmOctree.h和DgmOctree.cpp中,他采用了一种分级的结构,最大支持21级,如下,

static const int MAX_OCTREE_LEVEL = 21;

然后,会事先计算得到一个分级表,

预先计算好的数据表

CC这么做的原因是,把事先能计算好的数据先存储起来,用空间换时间的办法,来加速运算速度。

(1) PRE_COMPUTED_BIT_SHIFT_VALUES

//! Pre-computed bit shift values (one for each level)
struct BitShiftValues
{
    //! Default initialization
    BitShiftValues()
    {
        //we compute all possible values
        for (unsigned char level = 0; level <= DgmOctree::MAX_OCTREE_LEVEL; ++level)
        {
            values[level] = (3 * (DgmOctree::MAX_OCTREE_LEVEL - level));
        }
    }

    //! Values
    unsigned char values[DgmOctree::MAX_OCTREE_LEVEL+1];
};
static BitShiftValues PRE_COMPUTED_BIT_SHIFT_VALUES;

unsigned char DgmOctree::GET_BIT_SHIFT(unsigned char level)
{
    //return (3 * (DgmOctree::MAX_OCTREE_LEVEL - level));
    return PRE_COMPUTED_BIT_SHIFT_VALUES.values[level];
}

所以这个value实际相当于这样一个表,

[0]    63
[1]    60
[2]    57
[3]    54
[4]    51
[5]    48
[6]    45
[7]    42
[8]    39
[9]    36
[10]    33
[11]    30
[12]    27
[13]    24
[14]    21
[15]    18
[16]    15
[17]    12
[18]    9
[19]    6
[20]    3
[21]    0

(2) PRE_COMPUTED_POS_CODES

static const int MAX_OCTREE_LENGTH = (1 << MAX_OCTREE_LEVEL);

//! Pre-computed cell codes for all potential cell positions (along a unique dimension)
struct MonoDimensionalCellCodes
{
    //! Total number of positions/values
    /** There are 1024 possible values at level 10, and 2M. at level 21.
        \warning Never pass a 'constant initializer' by reference
    **/
    static const int VALUE_COUNT = DgmOctree::MAX_OCTREE_LENGTH;

    //! Default initialization
    MonoDimensionalCellCodes()
    {
        //we compute all possible values for cell codes
        //(along a unique dimension, the other ones are just shifted)
        for (int value = 0; value < VALUE_COUNT; ++value)
        {
            int mask = VALUE_COUNT;
            DgmOctree::CellCode code = 0;
            for (unsigned char k = 0; k < DgmOctree::MAX_OCTREE_LEVEL; k++)
            {
                mask >>= 1;
                code <<= 3;
                if (value & mask)
                {
                    code |= 1;
                }
            }
            values[value] = code;
        }

        //we compute all possible masks as well! (all dimensions)
        //DgmOctree::CellCode baseMask = (1 << (3 * DgmOctree::MAX_OCTREE_LEVEL));
        //for (int level = DgmOctree::MAX_OCTREE_LEVEL; level >= 0; --level)
        //{
        //    masks[level] = baseMask - 1;
        //    baseMask >>= 3;
        //}
    }

    //! Mono-dimensional cell codes
    DgmOctree::CellCode values[VALUE_COUNT];

    //! Mono-dimensional cell masks
    //DgmOctree::CellCode masks[DgmOctree::MAX_OCTREE_LEVEL + 1];
};
static MonoDimensionalCellCodes PRE_COMPUTED_POS_CODES;

这里,MAX_OCTREE_LENGTH == (1<<21) == 2097152 == 0x200000 ,是一个位置的极限值,这个表实际上相当于,

[0]    0
[1]    1
[2]    8
[3]    9
[4]    64
[5]    65
[6]    72
[7]    73
[8]    512
[9]    513
[10]    520
[11]    521
[12]    576
[13]    577
[14]    584
[15]    585
[16]    4096
[17]    4097
[18]    4104
[19]    4105
[20]    4160
[21]    4161
[22]    4168
[23]    4169
[24]    4608
[25]    4609
[26]    4616
[27]    4617
[28]    4672
[29]    4673
[30]    4680
[31]    4681
......

计算基础之cellCode

在设计算法之前,我们需要了解一下OcTree层级的概念。

比如我的所有点都在一个64x64x64的立方体内,那第0层级的边长就是64,也就是最大的cube框的边长,那么一个立方体包含8个cube框,下一级第1层级框的边长大小就是64/2,依此类推,再下一级就是64/4,64/8......

另外,给一个相关讲解的参考地址,

参考:数据立方体_点云空间数据组织——八叉树-白红宇的个人博客

作者对八叉树进行了讲解,我摘录了其中的一点。如下,

八叉树将空间分割成八块,根据2进制, 3位2进制即可表示8个数字,3位中的顺序:zyx ,顺序区分,从小递增到大,如: 011,即z为0,x为1,y为 1的块位置。所以 64位操作系统最多可分割为64/3 = 21级32位操作系统最多可分割为32/3 = 10级

建立单维索引( CellCode)

将原有的1位2进制转为3位,如:001转为000 000 001,010 转为 000 001 000,以此类推。另外两个维度的code分别左移1位和2位即可。

软件初始化时就计算完了所有CellCode。

具体可参考前面讲到的结构体
static MonoDimensionalCellCodes PRE_COMPUTED_POS_CODES;

好了,前面的这个要怎么理解和使用呢?

比如,我计算得到cellPos=(2,2,2),那
PRE_COMPUTED_POS_CODES.values[2] == 8 == 000 001 000
假设对应的层级也是21,那么,GenerateTruncatedCellCode中,返回的数据结果就是

return    (
     PRE_COMPUTED_POS_CODES.values[cellPos.x << shift]
|    (PRE_COMPUTED_POS_CODES.values[cellPos.y << shift] << 1)
|    (PRE_COMPUTED_POS_CODES.values[cellPos.z << shift] << 2)
) >> GET_BIT_SHIFT(level);

具体可以参考DgmOctree::GenerateTruncatedCellCode函数。
也就是
000 001 000 | 000 001 000 << 1 | 000 001 000 << 2 == 000 111 000

再比如,我计算得到cellPos=(3,3,3),那
PRE_COMPUTED_POS_CODES.values[3] == 9 == 000 001 001
那么,在21级,相应的cellcode就是
000 001 001 | 000 001 001 << 1 | 000 001 001 << 2 == 000 111 111

又比如,我计算得到cellPos=(4,4,4),那
PRE_COMPUTED_POS_CODES.values[3] == 64 == 001 000 000
那么,在21级,相应的cellcode就是
001 000 000 | 001 000 000 << 1 | 001 000 000 << 2 == 111 000 000

看出规律了吗?

作者的意思,就是要设计一个像c++中std::map这样的一个结构,能够迅速通过这个cellCode,找到任意层级的cellPos。

例如,前面在第21层级的cellPos=(4,4,4),到了第20层级,就要右移3位,因为

GET_BIT_SHIFT(20) == 3

不难判断,这个cellCode对应的cellPos正是(2,2,2)。

好了,有了这些相关基础知识之后, 我们开始讲解点云的八叉树算法的架构与原理。

ccPointCloud : ccGenericPointCloud

class QCC_DB_LIB_API ccPointCloud : public CCCoreLib::PointCloudTpl<ccGenericPointCloud, QString>
{
}

这里,ccGenericPointCloud的父类是GenericIndexedCloudPersist

class QCC_DB_LIB_API ccGenericPointCloud : public ccShiftedObject,  public CCCoreLib::GenericIndexedCloudPersist
{
    friend class ccMesh;

这是一个纯虚类,GenericIndexedCloudPersist,其所有的上级父类也都是纯虚类,或者说是接口,包括GenericIndexedCloud,GenericIndexedCloud, 以及GenericCloud,

class CC_CORE_LIB_API GenericIndexedCloudPersist : virtual public GenericIndexedCloud
{
    ......
}

class CC_CORE_LIB_API GenericIndexedCloud : virtual public GenericCloud {
    ......
}

class CC_CORE_LIB_API GenericCloud{
    ......
}

计算过程(程序结构)

当在CloudCompare中按下Edit-->Octree-->Compute之后,就会启动ocTree的计算,

void MainWindow::doActionComputeOctree()
{
    if ( !ccEntityAction::computeOctree(m_selectedEntities, this) )
        return;

    refreshAll();
    updateUI();
}

computeOctree会对所有选中的实际进行计算,在该函数中,先是得到entity的点云clouds,然后逐个对其进行计算,octree = cloud->computeOctree(..),或者直接生成octree = ccOctree::Shared(new ccOctree(cloud)),

bool computeOctree(const ccHObject::Container &selectedEntities, QWidget* parent)
{

    for (ccHObject* ent : selectedEntities)
    {
        ......
        ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(ent, &lockedVertices);
        ......
        clouds.insert(cloud);
        ......
    }

    .......
    for (const auto cloud : clouds)
    {
        ......
        switch (coDlg.getMode())
        {
        case ccComputeOctreeDlg::DEFAULT:
            octree = cloud->computeOctree(&pDlg);
            break;
        case ccComputeOctreeDlg::MIN_CELL_SIZE:
        case ccComputeOctreeDlg::CUSTOM_BBOX:
        {
            ......
            octree = ccOctree::Shared(new ccOctree(cloud));
            ......
        }
        break;
        }
    }
    ......
}

接下,调用ccGenericPointCloud::computeOctree,

ccOctree::Shared ccGenericPointCloud::computeOctree(CCCoreLib::GenericProgressCallback* progressCb, bool autoAddChild/*=true*/)
{
    deleteOctree();

    ccOctree::Shared octree = ccOctree::Shared(new ccOctree(this));
    if (octree->build(progressCb) > 0)
    {
        setOctree(octree, autoAddChild);
    }
    else
    {
        octree.clear();
    }

    return octree;
}

在这里面,会先new出一个ccOcTree,并把this也就是ccGenericPointCloud传入进去给m_theAssociatedCloud,然后通过build进行octree的计算,

int DgmOctree::build(GenericProgressCallback* progressCb)
{
    if (!m_theAssociatedCloud)
    {
        assert(false);
        return -1;
    }

    if (!m_thePointsAndTheirCellCodes.empty())
    {
        clear();
    }

    m_theAssociatedCloud->getBoundingBox(m_pointsMin, m_pointsMax);

    m_dimMin = m_pointsMin;
    m_dimMax = m_pointsMax;

    //we make this bounding-box cubical (+0.1% growth to avoid round-off issues when projecting points in the octree)
    CCMiscTools::MakeMinAndMaxCubical(m_dimMin, m_dimMax, 0.001);

    return genericBuild(progressCb);
}

这里要注意,ccOctree的父类正是DgmOctree,

class QCC_DB_LIB_API ccOctree : public QObject, public CCCoreLib::DgmOctree

计算OcTree

结构体IndexAndCode中,给出的是点的index和所在的cube的code,大体上是这样定义的,

    struct IndexAndCode
    {
        //! index
        unsigned theIndex;
        //! cell code
        CellCode theCode;
        ......
    }

    using cellsContainer = std::vector<IndexAndCode>;

    //! The coded octree structure
    cellsContainer m_thePointsAndTheirCellCodes;

接下来我们看一下DgmOctree::genericBuild函数的主体,

int DgmOctree::genericBuild(GenericProgressCallback* progressCb)
{
    ......
    try
    {
        m_thePointsAndTheirCellCodes.resize(pointCount); 
    }
    ......

    updateCellSizeTable();


    //for all points
    cellsContainer::iterator it = m_thePointsAndTheirCellCodes.begin();
    for (unsigned i = 0; i < pointCount; i++)
    {
        const CCVector3* P = m_theAssociatedCloud->getPoint(i);

        //does the point falls in the 'accepted points' box?
        //(potentially different from the octree box - see DgmOctree::build)
        if (    (P->x >= m_pointsMin[0]) && (P->x <= m_pointsMax[0])
            &&    (P->y >= m_pointsMin[1]) && (P->y <= m_pointsMax[1])
            &&    (P->z >= m_pointsMin[2]) && (P->z <= m_pointsMax[2]) )
        {
            //compute the position of the cell that includes this point
            Tuple3i cellPos;
            getTheCellPosWhichIncludesThePoint(P, cellPos);

            ......

            it->theIndex = i;
            it->theCode = GenerateTruncatedCellCode(cellPos, MAX_OCTREE_LEVEL);
            ......

            ++it;
            ++m_numberOfProjectedPoints;
        }

    ......

    //we sort the 'cells' by ascending code order
    ParallelSort(m_thePointsAndTheirCellCodes.begin(), m_thePointsAndTheirCellCodes.end(), IndexAndCode::codeComp);

    //update the pre-computed 'number of cells per level of subdivision' array
    updateCellCountTable();

    ......    
}

下面我们来看这个函数是如何计算octree的,

(1) 函数中先是通过m_thePointsAndTheirCellCodes.resize分配内存,

m_thePointsAndTheirCellCodes.resize(pointCount); 

(2)然后,通过updateCellSizeTable不断分割尺寸,得到每一级cube框的大小,例如,假设最大的框的边长为64,那么一个立方体包含8个cube框,下一级框的边长大小就是64/2,依此类推,再下一级就是64/4,64/8......

void DgmOctree::updateCellSizeTable()
{
    //update the cell dimension for each subdivision level
    m_cellSize[0] = m_dimMax.x - m_dimMin.x;

    unsigned long long d = 1;
    for (int k = 1; k <= MAX_OCTREE_LEVEL; k++)
    {
        d <<= 1;
        m_cellSize[k] = m_cellSize[0] / d;
    }
}

然后,函数开始逐个点逐个点地计算。

(3) 通过getTheCellPosWhichIncludesThePoint计算点的cellPos。这里的cellPos是指最小的cube的序号,和前面的讲解一样,假设0级cell大小是64,那么第20级的cell大小就是cs = 64/2^20 = 0.00006103515625,那么在某个维度上其序号就是(x-xmin)/cs,如下,

//! Type of the coordinates of a (N-D) point
using PointCoordinateType = float;

//! Returns the position FOR THE DEEPEST LEVEL OF SUBDIVISION of the cell that includes a given point
    /** The cell coordinates can be negative or greater than 2^MAX_OCTREE_LEVEL-1
    as the point can lie outside the octree bounding-box.
    \param thePoint the query point
    \param cellPos the computed position
    **/
inline void getTheCellPosWhichIncludesThePoint(const CCVector3* thePoint, Tuple3i& cellPos) const
{
    const PointCoordinateType& cs = getCellSize(MAX_OCTREE_LEVEL);
    //DGM: if we admit that cs >= 0, then the 'floor' operator is useless (int cast = truncation)
    cellPos.x = static_cast<int>(/*floor*/(thePoint->x - m_dimMin.x) / cs);
    cellPos.y = static_cast<int>(/*floor*/(thePoint->y - m_dimMin.y) / cs);
    cellPos.z = static_cast<int>(/*floor*/(thePoint->z - m_dimMin.z) / cs);
}

(4) 获取点的cellCode,

这个,是我们在基础部分讲过的函数DgmOctree::GenerateTruncatedCellCode,

DgmOctree::CellCode DgmOctree::GenerateTruncatedCellCode(const Tuple3i& cellPos, unsigned char level)
{
    assert(cellPos.x >= 0 && cellPos.x < MonoDimensionalCellCodes::VALUE_COUNT
    &&cellPos.y >= 0 && cellPos.y < MonoDimensionalCellCodes::VALUE_COUNT
    &&cellPos.z >= 0 && cellPos.z < MonoDimensionalCellCodes::VALUE_COUNT);

    const unsigned char shift = MAX_OCTREE_LEVEL - level;

    return(PRE_COMPUTED_POS_CODES.values[cellPos.x << shift]
          |(PRE_COMPUTED_POS_CODES.values[cellPos.y << shift] << 1)
          |(PRE_COMPUTED_POS_CODES.values[cellPos.z << shift] << 2)
    ) >> GET_BIT_SHIFT(level);
}

(5)按cellCode大小排序

ParallelSort(m_thePointsAndTheirCellCodes.begin(), m_thePointsAndTheirCellCodes.end(), IndexAndCode::codeComp);

值得说明的是,在函数中,对变量m_fillIndexes也进行了计算,因为源码比较简单,一目了然,这里就不展开讲了。

本文结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值