引言
对比两组点云的相似度,一般都是使用距离来量化重合度-重合度越高越相似。
最简单直接的方法 (左图
):求一组点云上的点到另一组点云上最近点的距离,但是由于部分点云的缺失,可能有很大误差。兼顾误差和效率的方法 (中间图
),并且对噪声鲁棒:求点到平面的距离1。
本文重点介绍Cloudcompare-2.11.3中以下两点:
- 计算点到平面的距离(平面-由参考点云上的部分点拟合求得)
- 求得点到平面的投影点坐标
一、软件操作
选中两个点云 -> Tools -> Distances -> Cloud/Cloud Dist:
设置局部建模为平面,并且选上split X,Y and Z components便于求投影点坐标:
然后点计算 -> OK 即可:
最后保存为txt 选中点云 -> File -> Save:
建议选中 columns title
, 要不然真不知道txt文档每一列是什么意思
二、源码解读
2.1 准备工作
忽略许多if等语句
直入正题 CCLib::DistanceComputationTools::computeCloud2CloudDistance
里面调用八叉树加速搜索计算comparedOctree->executeFunctionForAllCellsAtLevel
,需要选择距离计算方式:
params.localModel == NO_MODEL ? computeCellHausdorffDistance : computeCellHausdorffDistanceWithLocalModel,
大致过程:将点云分为若干个Cell (块),每分好一个Cell就调用计算距离的函数,并创建新的Cell
// 略................... 这里以单线程为例
//for each point
for (; p != m_thePointsAndTheirCellCodes.end(); ++p){
//check if it belongs to the current cell
CellCode nextCode = (p->theCode >> bitDec);
if (nextCode != cell.truncatedCode){
//if not, we call the user function on the previous cell
result = (*func)(cell, additionalParameters, &nprogress);
if (!result)
break;
//and we start a new cell
cell.index += cell.points->size();
cell.points->clear();
cell.truncatedCode = nextCode;
}
cell.points->addPointIndex(p->theIndex);
}
//don't forget last cell!
if (result)
result = (*func)(cell, additionalParameters, &nprogress);
// 略...................
*func
计算距离的函数,点到平面的距离函数为computeCellHausdorffDistanceWithLocalModel
,
2.2 计算距离
computeCellHausdorffDistanceWithLocalModel
中会计算Cell中每个点云到参考点云的距离
主要包含三个部分:初始化、局部建模 (local model)、距离计算。
需要注意:每个点云对应参考点云的一个local model,但是local model可对应多个点云 (避免重复计算)。
重点关注距离计算:
- 没有local model就退化成了最近点距离
- 有local model计算点到平面的距离 distToModel
- 如果点到参考点上最近点的距离 distToNearestPoint 小于 distToModel,那么就用distToNearestPoint (
稳健
)
//if we have a local model
if (lm)
{
CCVector3 nearestModelPoint;
ScalarType distToModel = lm->computeDistanceFromModelToPoint(&nNSS.queryPoint, computeSplitDistances ? &nearestModelPoint : nullptr);
//we take the best estimation between the nearest neighbor and the model!
//this way we only reduce any potential noise (that would be due to sampling)
//instead of 'adding' noise if the model is badly shaped
if (distToNearestPoint <= distToModel)
{
distPt = distToNearestPoint;
}
else
// 略.......................
ScalarType DistanceComputationTools::computePoint2PlaneDistance(const CCVector3* P,
const PointCoordinateType* planeEquation)
{
//point to plane distance: d = (a0*x+a1*y+a2*z - a3) / sqrt(a0^2+a1^2+a2^2)
assert(std::abs(CCVector3::vnorm(planeEquation) - PC_ONE) <= std::numeric_limits<PointCoordinateType>::epsilon());
return static_cast<ScalarType>((CCVector3::vdot(P->u, planeEquation) - planeEquation[3])/*/CCVector3::vnorm(planeEquation)*/); //norm == 1.0!
}
2.3 投影点坐标
2.2末尾提到了computePoint2PlaneDistance
,知道距离dist (可能是负的
) 以后就可以求投影点坐标:
ScalarType computeDistanceFromModelToPoint(const CCVector3* P, CCVector3* nearestPoint = nullptr) const override{
ScalarType dist = DistanceComputationTools::computePoint2PlaneDistance(P, m_eq);
if (nearestPoint){ // dist在三个轴平面上的投影长度
*nearestPoint = *P - dist * CCVector3(m_eq);
}
return std::abs(dist);
}
点到投影点的向量和平面的法向量是平行的,可以手推公式。或者简单点画个图就明白了:核心就是求dist在三个坐标轴平面的投影
d
i
s
t
⋅
C
C
V
e
c
t
o
r
3
(
m
e
q
)
dist \cdot CCVector3(m_eq)
dist⋅CCVector3(meq),注意平面法向量(m_eq前三个数
)已经是归一化后的
- 源码并没有保存投影点的坐标
nearestPoint
,而是变成了相对点坐标的三个分量 X, Y, Z的距离。所以选择split X,Y and Z components之后,求投影点坐标还需要逆操作:
n e a r e s t P o i n t = n N S S . q u e r y P o i n t − ( X , Y , Z ) nearestPoint = nNSS.queryPoint - (X, Y, Z) nearestPoint=nNSS.queryPoint−(X,Y,Z)
if (computeSplitDistances){
unsigned index = cell.points->getPointGlobalIndex(i);
if (params->splitDistances[0])
params->splitDistances[0]->setValue(index, static_cast<ScalarType>(nNSS.queryPoint.x - nearestPoint.x));
if (params->splitDistances[1])
params->splitDistances[1]->setValue(index, static_cast<ScalarType>(nNSS.queryPoint.y - nearestPoint.y));
if (params->splitDistances[2])
params->splitDistances[2]->setValue(index, static_cast<ScalarType>(nNSS.queryPoint.z - nearestPoint.z));
}