优化平面分割

This commit is contained in:
cool609 2026-03-30 00:22:24 +08:00
parent d2f86fe60d
commit 3887aba425
2 changed files with 220 additions and 1 deletions

View File

@ -1034,7 +1034,224 @@ int SegmentPlanesByRansac(
<< ", after growth=" << plane.pointCount << std::endl;
}
std::cout << "[RANSAC] Found " << outPlanes.size() << " planes" << std::endl;
std::cout << "[RANSAC] Found " << outPlanes.size() << " planes (before merge)" << std::endl;
// ===== 后处理:将小平面合并到兼容的大平面中 =====
// 解决孔洞穿透问题孔洞对面的点因BFS不可达而被分为单独小平面
// 但实际上它们属于同一物理表面。
if (outPlanes.size() > 1) {
// 按点数降序排序,最大平面在前
std::sort(outPlanes.begin(), outPlanes.end(),
[](const PlaneSegment& a, const PlaneSegment& b) {
return a.pointCount > b.pointCount;
});
// 为每个平面计算Z值统计中位数和范围
struct PlaneZStats {
float medianZ;
float minZ;
float maxZ;
};
std::vector<PlaneZStats> planeStats(outPlanes.size());
for (size_t i = 0; i < outPlanes.size(); i++) {
std::vector<float> zValues;
zValues.reserve(outPlanes[i].pointIndices.size());
for (int idx : outPlanes[i].pointIndices) {
zValues.push_back(points[idx].z);
}
std::sort(zValues.begin(), zValues.end());
size_t n = zValues.size();
planeStats[i].medianZ = (n % 2 == 0)
? (zValues[n/2 - 1] + zValues[n/2]) / 2.0f
: zValues[n/2];
planeStats[i].minZ = zValues.front();
planeStats[i].maxZ = zValues.back();
}
// 尝试将小平面合并到大平面
constexpr float kNormalDotThreshold = 0.95f; // cos(~18°), 法向量相似性
constexpr float kMergeZRangeMultiplier = 2.0f; // Z差容忍倍数
std::vector<bool> merged(outPlanes.size(), false);
for (size_t i = 1; i < outPlanes.size(); i++) {
if (merged[i]) continue;
// 寻找最佳合并目标
int bestTarget = -1;
float bestDot = 0.0f;
for (size_t j = 0; j < i; j++) {
if (merged[j]) continue;
// 检查法向量相似性(绝对值,因为法向可能反向)
float dot = std::abs(
outPlanes[i].a * outPlanes[j].a +
outPlanes[i].b * outPlanes[j].b +
outPlanes[i].c * outPlanes[j].c
);
if (dot < kNormalDotThreshold) continue;
// 检查Z范围兼容性小平面的中位Z应在大平面Z范围附近
float largeRange = planeStats[j].maxZ - planeStats[j].minZ;
float tolerance = std::max(largeRange * 0.5f,
params.growthZThreshold * kMergeZRangeMultiplier);
float zDist = std::abs(planeStats[i].medianZ - planeStats[j].medianZ);
if (zDist > tolerance) continue;
if (dot > bestDot) {
bestDot = dot;
bestTarget = static_cast<int>(j);
}
}
if (bestTarget >= 0) {
std::cout << " Merging small plane (" << outPlanes[i].pointCount
<< " pts, medZ=" << planeStats[i].medianZ
<< ") into plane (" << outPlanes[bestTarget].pointCount
<< " pts, medZ=" << planeStats[bestTarget].medianZ
<< "), normalDot=" << bestDot << std::endl;
// 合并点索引
outPlanes[bestTarget].pointIndices.insert(
outPlanes[bestTarget].pointIndices.end(),
outPlanes[i].pointIndices.begin(),
outPlanes[i].pointIndices.end()
);
outPlanes[bestTarget].pointCount += outPlanes[i].pointCount;
// 更新合并后的Z统计
float newMinZ = std::min(planeStats[bestTarget].minZ, planeStats[i].minZ);
float newMaxZ = std::max(planeStats[bestTarget].maxZ, planeStats[i].maxZ);
planeStats[bestTarget].minZ = newMinZ;
planeStats[bestTarget].maxZ = newMaxZ;
merged[i] = true;
}
}
// 移除已合并的平面
std::vector<PlaneSegment> finalPlanes;
for (size_t i = 0; i < outPlanes.size(); i++) {
if (!merged[i]) {
finalPlanes.push_back(std::move(outPlanes[i]));
}
}
outPlanes = std::move(finalPlanes);
std::cout << "[RANSAC] After merge: " << outPlanes.size() << " planes" << std::endl;
}
// ===== 后处理:对每个平面做栅格连通域标注,只保留最大连通域 =====
for (size_t pi = 0; pi < outPlanes.size(); pi++) {
PlaneSegment& plane = outPlanes[pi];
// 建立该平面的栅格掩码
std::vector<bool> mask(pointCount, false);
for (int idx : plane.pointIndices) {
mask[idx] = true;
}
// BFS连通域标注 (4-连通)
std::vector<int> label(pointCount, -1);
std::vector<int> componentSizes;
int numComponents = 0;
for (int idx : plane.pointIndices) {
if (label[idx] >= 0) continue;
int compId = numComponents++;
int compSize = 0;
std::queue<int> q;
q.push(idx);
label[idx] = compId;
while (!q.empty()) {
int cur = q.front();
q.pop();
compSize++;
int cr = cur / cols;
int cc = cur % cols;
for (int d = 0; d < 4; d++) {
int nr = cr + dr[d];
int nc = cc + dc[d];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
int nIdx = nr * cols + nc;
if (!mask[nIdx] || label[nIdx] >= 0) continue;
label[nIdx] = compId;
q.push(nIdx);
}
}
componentSizes.push_back(compSize);
}
if (numComponents <= 1) continue;
// 找到最大连通域
int maxCompId = 0;
for (int c = 1; c < numComponents; c++) {
if (componentSizes[c] > componentSizes[maxCompId]) {
maxCompId = c;
}
}
// 只保留最大连通域的点
int removedCount = plane.pointCount - componentSizes[maxCompId];
if (removedCount > 0) {
std::vector<int> filteredIndices;
filteredIndices.reserve(componentSizes[maxCompId]);
for (int idx : plane.pointIndices) {
if (label[idx] == maxCompId) {
filteredIndices.push_back(idx);
}
}
plane.pointIndices = std::move(filteredIndices);
plane.pointCount = static_cast<int>(plane.pointIndices.size());
std::cout << " Plane " << (pi + 1)
<< ": CCL removed " << removedCount
<< " pts from " << (numComponents - 1)
<< " small components, kept " << plane.pointCount << std::endl;
}
}
// ===== 按相对大小过滤:去除远小于最大平面的平面 =====
if (outPlanes.size() > 1) {
// 找最大平面的点数
int maxPoints = 0;
for (const auto& p : outPlanes) {
if (p.pointCount > maxPoints) maxPoints = p.pointCount;
}
int sizeThreshold = static_cast<int>(maxPoints * params.minPlaneRatio);
sizeThreshold = std::max(sizeThreshold, params.minPlanePoints);
size_t beforeCount = outPlanes.size();
outPlanes.erase(
std::remove_if(outPlanes.begin(), outPlanes.end(),
[sizeThreshold](const PlaneSegment& p) {
return p.pointCount < sizeThreshold;
}),
outPlanes.end()
);
if (outPlanes.size() < beforeCount) {
std::cout << "[RANSAC] Size filter (threshold=" << sizeThreshold
<< ", ratio=" << params.minPlaneRatio
<< " of max=" << maxPoints
<< "): removed " << (beforeCount - outPlanes.size())
<< " small planes" << std::endl;
}
}
std::cout << "[RANSAC] Final: " << outPlanes.size() << " planes" << std::endl;
*errCode = HD_SUCCESS;
return static_cast<int>(outPlanes.size());

View File

@ -293,6 +293,7 @@ struct RansacPlaneSegmentationParams {
int minPlanePoints; // 最小平面点数, 建议 50-200
int maxPlanes; // 最大提取平面数量, 建议 3-10
float growthZThreshold; // 区域生长时相邻点Z差阈值 (mm), 建议 0.5-2.0
float minPlaneRatio; // 平面最小点数占比 (相对最大平面), 建议 0.05-0.2
RansacPlaneSegmentationParams()
: distanceThreshold(1.0f)
@ -300,6 +301,7 @@ struct RansacPlaneSegmentationParams {
, minPlanePoints(100)
, maxPlanes(5)
, growthZThreshold(1.0f)
, minPlaneRatio(0.1f)
{}
};