diff --git a/Algo/DetectHole/examples/visualization_demo/VisualizationDemo.cpp b/Algo/DetectHole/examples/visualization_demo/VisualizationDemo.cpp index 80ca02f..47528cb 100644 --- a/Algo/DetectHole/examples/visualization_demo/VisualizationDemo.cpp +++ b/Algo/DetectHole/examples/visualization_demo/VisualizationDemo.cpp @@ -477,6 +477,7 @@ int ProcessSingleFile(const std::string& inputFile, callbackData.showLines = showLines; SHoleDetectionDebugCallbacks debugCallbacks; +#if 1 debugCallbacks.onBoundaryDetected = OnBoundaryDetected; debugCallbacks.onClustersFound = OnClustersFound; debugCallbacks.onExpandedRegion = OnExpandedRegion; @@ -485,6 +486,16 @@ int ProcessSingleFile(const std::string& inputFile, debugCallbacks.onLineAngleProfileDetected = nullptr;// OnLineAngleProfileDetected; debugCallbacks.onPlanesSegmented = OnPlanesSegmented; debugCallbacks.onMaskedPointsReady = nullptr;// OnMaskedPointsReady; +#else + debugCallbacks.onBoundaryDetected = nullptr; + debugCallbacks.onClustersFound = nullptr; + debugCallbacks.onExpandedRegion = nullptr; + debugCallbacks.onHoleFitted = nullptr; + debugCallbacks.onSegmentPairsDetected = nullptr; + debugCallbacks.onLineAngleProfileDetected = nullptr; + debugCallbacks.onPlanesSegmented = nullptr; + debugCallbacks.onMaskedPointsReady = nullptr; +#endif debugCallbacks.userData = &callbackData; SMultiHoleResult result; diff --git a/Algo/DetectHole/src/HoleDetectionBoundary.cpp b/Algo/DetectHole/src/HoleDetectionBoundary.cpp index 44d6d01..232a51d 100644 --- a/Algo/DetectHole/src/HoleDetectionBoundary.cpp +++ b/Algo/DetectHole/src/HoleDetectionBoundary.cpp @@ -343,6 +343,13 @@ void EvaluateLine( if (segs[ei].type == ESegType::Descending) { // 若已见过内部 Flat 且尚未找到上升沿,说明这是第二个独立特征的下降,不应桥接 if (seenInnerFlat && !seenAsc) break; + // 已完成一个完整的 Desc→Asc 凹坑,再遇到 Desc 表示新的孔洞开始, + // 应在此停止,避免将两个相邻孔洞桥接成一个 pair。 + // bestExit 指向第二个 Desc,后续 isPit 分支会回溯找到 Asc 的末端作为真正右边界。 + if (seenDesc && seenAsc && descFirst) { + bestExit = ei; + break; + } if (!seenAsc) descFirst = true; seenDesc = true; continue; @@ -392,27 +399,47 @@ void EvaluateLine( } } - // depth: 表面与坑底的 z 极差 - float refZ = std::min(pts[startPos].point.z, pts[endPos].point.z); - float minZ = refZ, maxZ = refZ; - for (int k = startPos; k <= endPos; k++) { - if (pts[k].valid) { - minZ = std::min(minZ, pts[k].point.z); - maxZ = std::max(maxZ, pts[k].point.z); + // depth: 使用线性插值补偿表面斜度 + // 旧方案(min/max Z 相对 refZ)会把表面倾斜误判为坑深; + // 新方案以 startPos→endPos 的 Z 直线为基准,取最大偏差, + // 仅在真正存在弯折/凹陷处才得到显著 depth。 + float depth = 0.0f; + { + float startZ = pts[startPos].point.z; + float endZ = pts[endPos].point.z; + float posSpanF = static_cast(endPos - startPos); + if (posSpanF < 1.0f) posSpanF = 1.0f; + for (int k = startPos; k <= endPos; k++) { + if (!pts[k].valid) continue; + float t = static_cast(k - startPos) / posSpanF; + float expectedZ = startZ + (endZ - startZ) * t; + float dev = std::abs(pts[k].point.z - expectedZ); + if (dev > depth) depth = dev; } } - float depth = std::max(refZ - minZ, maxZ - refZ); if (isPit && depth < params.minPitDepth) continue; float span = planeDist(pts[startPos].point, pts[endPos].point); if (span < params.minFeatureSpan) continue; - // 纯空洞特征:检查物理跨度不超过最大孔洞直径 - // 原先用点数(maxGapPointsInLine)过滤,但点数与传感器密度相关, - // 导致真实孔洞在点密度较低或孔洞稍大时被误杀(如 33 点 gap ≈ 12mm 被 12 点上限拒绝)。 - // 改用物理跨度与 maxRadius 比较,保证检测上限与孔洞尺寸参数一致。 - if (isGapFeature && !isPit && span > params.maxRadius * 2.0f) continue; + // 所有类型的 pair 跨度不应超过最大孔洞直径。 + // 超过 maxRadius * 2 的 pair 通常是平坦区域内散落无效点和角度噪声 + // 组合成 Desc+Gap+Asc 模式后形成的误检,应当过滤。 + if (span > params.maxRadius * 2.0f) continue; + + // 纯空洞特征(无 Desc+Asc 边缘):要求内部无效点占比达到一定比例。 + // 真正的穿透孔洞内部绝大多数点无效(传感器无法采集数据), + // 而平坦区域散落的少量无效点不应构成空洞特征。 + if (isGapFeature && !isPit) { + int invalidCount = 0; + int totalCount = endPos - startPos + 1; + for (int k = startPos; k <= endPos; k++) { + if (!pts[k].valid) invalidCount++; + } + float invalidRatio = static_cast(invalidCount) / static_cast(totalCount); + if (invalidRatio < 0.3f) continue; + } SSegmentPair pair; pair.startPoint = pts[startPos].point; @@ -515,63 +542,9 @@ int DetectPitBoundaries( } // ================================================================ - // 聚类:行-行重叠、列-列重叠、行-列交叉 + // 聚类:行-列交叉聚类 + 空间点云子聚类 // ================================================================ - // 行 pair 列重叠判定 - auto hasColOverlap = [](const SSegmentPair& pair1, const SSegmentPair& pair2, int tolerance = 0, float minOverlapRatio = 0.6f) -> bool { - int min1 = std::min(pair1.startCol, pair1.endCol); - int max1 = std::max(pair1.startCol, pair1.endCol); - int min2 = std::min(pair2.startCol, pair2.endCol); - int max2 = std::max(pair2.startCol, pair2.endCol); - - int length1 = max1 - min1 + 1; - int length2 = max2 - min2 + 1; - - min1 -= tolerance; - max1 += tolerance; - min2 -= tolerance; - max2 += tolerance; - - if (max1 < min2 || max2 < min1) return false; - - int overlapStart = std::max(min1, min2); - int overlapEnd = std::min(max1, max2); - int overlapSize = overlapEnd - overlapStart + 1; - - int minLength = std::min(length1, length2); - float overlapRatio = static_cast(overlapSize) / static_cast(minLength); - - return overlapRatio >= minOverlapRatio; - }; - - // 列 pair 行重叠判定 - auto hasRowOverlap = [](const SSegmentPair& pair1, const SSegmentPair& pair2, int tolerance = 0, float minOverlapRatio = 0.6f) -> bool { - int min1 = std::min(pair1.startRow, pair1.endRow); - int max1 = std::max(pair1.startRow, pair1.endRow); - int min2 = std::min(pair2.startRow, pair2.endRow); - int max2 = std::max(pair2.startRow, pair2.endRow); - - int length1 = max1 - min1 + 1; - int length2 = max2 - min2 + 1; - - min1 -= tolerance; - max1 += tolerance; - min2 -= tolerance; - max2 += tolerance; - - if (max1 < min2 || max2 < min1) return false; - - int overlapStart = std::max(min1, min2); - int overlapEnd = std::min(max1, max2); - int overlapSize = overlapEnd - overlapStart + 1; - - int minLength = std::min(length1, length2); - float overlapRatio = static_cast(overlapSize) / static_cast(minLength); - - return overlapRatio >= minOverlapRatio; - }; - // 行-列交叉判定:行pair的行号在列pair的行范围内,且列pair的列号在行pair的列范围内 auto hasCrossOverlap = [](const SSegmentPair& rowPair, const SSegmentPair& colPair, int tolerance = 1) -> bool { int colMinRow = std::min(colPair.startRow, colPair.endRow) - tolerance; @@ -584,7 +557,7 @@ int DetectPitBoundaries( return (colCol >= rowMinCol && colCol <= rowMaxCol); }; - // shouldKeep 标记 + // shouldKeep 标记:仅参与了行-列交叉匹配的 pair 才保留 std::vector> shouldKeepRow(rows); for (int row = 0; row < rows; row++) { shouldKeepRow[row].resize(allRowSegmentPairs[row].size(), false); @@ -649,53 +622,7 @@ int DetectPitBoundaries( return id + pairIndex; }; - int lookAheadWindow = 3; - - // 行-行聚类(现有逻辑) - for (int row = 0; row < rows; row++) { - const auto& currentRowPairs = allRowSegmentPairs[row]; - - for (int nextRow = row + 1; nextRow < std::min(row + lookAheadWindow + 1, rows); nextRow++) { - const auto& nextRowPairs = allRowSegmentPairs[nextRow]; - - for (size_t i = 0; i < currentRowPairs.size(); i++) { - for (size_t j = 0; j < nextRowPairs.size(); j++) { - if (hasColOverlap(currentRowPairs[i], nextRowPairs[j])) { - shouldKeepRow[row][i] = true; - shouldKeepRow[nextRow][j] = true; - - int pairId1 = getRowPairId(row, static_cast(i)); - int pairId2 = getRowPairId(nextRow, static_cast(j)); - unite(pairId1, pairId2); - } - } - } - } - } - - // 列-列聚类 - for (int col = 0; col < cols; col++) { - const auto& currentColPairs = allColSegmentPairs[col]; - - for (int nextCol = col + 1; nextCol < std::min(col + lookAheadWindow + 1, cols); nextCol++) { - const auto& nextColPairs = allColSegmentPairs[nextCol]; - - for (size_t i = 0; i < currentColPairs.size(); i++) { - for (size_t j = 0; j < nextColPairs.size(); j++) { - if (hasRowOverlap(currentColPairs[i], nextColPairs[j])) { - shouldKeepCol[col][i] = true; - shouldKeepCol[nextCol][j] = true; - - int pairId1 = getColPairId(col, static_cast(i)); - int pairId2 = getColPairId(nextCol, static_cast(j)); - unite(pairId1, pairId2); - } - } - } - } - } - - // 行-列交叉聚类 + // 行-列交叉聚类:行 pair 与列 pair 在空间上交叉则合并 for (int row = 0; row < rows; row++) { const auto& rowPairs = allRowSegmentPairs[row]; for (size_t i = 0; i < rowPairs.size(); i++) { @@ -715,33 +642,6 @@ int DetectPitBoundaries( } } - // 收集有效 pair → cluster - std::map> rootToPairIds; - - for (int row = 0; row < rows; row++) { - const auto& rowPairs = allRowSegmentPairs[row]; - for (size_t i = 0; i < rowPairs.size(); i++) { - int pairId = getRowPairId(row, static_cast(i)); - int root = find(pairId); - rootToPairIds[root].push_back(pairId); - } - } - for (int col = 0; col < cols; col++) { - const auto& colPairs = allColSegmentPairs[col]; - for (size_t i = 0; i < colPairs.size(); i++) { - int pairId = getColPairId(col, static_cast(i)); - int root = find(pairId); - rootToPairIds[root].push_back(pairId); - } - } - - std::set validRoots; - - for (const auto& entry : rootToPairIds) { - int root = entry.first; - validRoots.insert(root); - } - // 从 pairId 反查 SSegmentPair auto getPairById = [&](int pairId) -> const SSegmentPair& { if (pairId < totalRowPairs) { @@ -769,49 +669,95 @@ int DetectPitBoundaries( return dummy; }; - std::map> pairIdToBoundaryIndices; - - for (const auto& entry : rootToPairIds) { - int root = entry.first; - const auto& pairIds = entry.second; - - if (validRoots.find(root) == validRoots.end()) continue; - - for (int pairId : pairIds) { - const auto& pair = getPairById(pairId); - - SHoleBoundaryPoint peakBp; - peakBp.point = pair.startPoint; - peakBp.row = pair.startRow; - peakBp.col = pair.startCol; - int peakIndex = static_cast(boundaryPoints.size()); - boundaryPoints.push_back(peakBp); - pairIdToBoundaryIndices[pairId].push_back(peakIndex); - - SHoleBoundaryPoint valleyBp; - valleyBp.point = pair.endPoint; - valleyBp.row = pair.endRow; - valleyBp.col = pair.endCol; - int valleyIndex = static_cast(boundaryPoints.size()); - boundaryPoints.push_back(valleyBp); - pairIdToBoundaryIndices[pairId].push_back(valleyIndex); + // ================================================================ + // 空间点云子聚类:对每个 union-find 组按空间距离做连通分量分析 + // 防止错误 pair(横跨两个孔洞)把两个独立孔洞桥接成同一 cluster。 + // 以每个 pair 的中点为特征,连通半径 = maxRadius * 3, + // 使同一孔洞内的 pair 聚在一起,而分属不同孔洞的 pair 自然断开。 + // ================================================================ + { + // 1. 仅收集参与了行-列交叉匹配的 pair,按 union-find root 分组 + std::map> rootToPairIds; + for (int row = 0; row < rows; row++) { + for (size_t i = 0; i < allRowSegmentPairs[row].size(); i++) { + if (!shouldKeepRow[row][i]) continue; + int pairId = getRowPairId(row, static_cast(i)); + rootToPairIds[find(pairId)].push_back(pairId); + } } - } - - std::map> rootToCluster; - - for (const auto& entry : pairIdToBoundaryIndices) { - int pairId = entry.first; - const auto& boundaryIndices = entry.second; - int root = find(pairId); - - for (int idx : boundaryIndices) { - rootToCluster[root].push_back(idx); + for (int col = 0; col < cols; col++) { + for (size_t j = 0; j < allColSegmentPairs[col].size(); j++) { + if (!shouldKeepCol[col][j]) continue; + int pairId = getColPairId(col, static_cast(j)); + rootToPairIds[find(pairId)].push_back(pairId); + } } - } - for (const auto& entry : rootToCluster) { - clusters.push_back(entry.second); + // pair 中点(XY 平面),用于空间聚类 + auto getPairCenter2D = [&](int pairId) -> std::pair { + const auto& p = getPairById(pairId); + return {(p.startPoint.x + p.endPoint.x) * 0.5f, + (p.startPoint.y + p.endPoint.y) * 0.5f}; + }; + + // 连通半径:孔洞直径的 1.5 倍,可分离间距大于一个孔洞直径的相邻孔洞 + const float splitRadius = params.maxRadius * 3.0f; + + // 2. 对每个 union-find 组做空间连通分量,各子组输出为独立 cluster + for (const auto& entry : rootToPairIds) { + const auto& pairIds = entry.second; + int n = static_cast(pairIds.size()); + + // 子组内空间 union-find + std::vector sp(n); + for (int i = 0; i < n; i++) sp[i] = i; + + std::function sf = [&sp, &sf](int x) -> int { + if (sp[x] != x) sp[x] = sf(sp[x]); + return sp[x]; + }; + + std::vector> centers(n); + for (int a = 0; a < n; a++) { + centers[a] = getPairCenter2D(pairIds[a]); + } + + for (int a = 0; a < n; a++) { + for (int b = a + 1; b < n; b++) { + float dx = centers[a].first - centers[b].first; + float dy = centers[a].second - centers[b].second; + if (std::sqrt(dx * dx + dy * dy) <= splitRadius) { + int ra = sf(a), rb = sf(b); + if (ra != rb) sp[ra] = rb; + } + } + } + + // 按子组收集 boundary points,每个子组形成一个独立 cluster + std::map> subGroupBoundaries; + for (int a = 0; a < n; a++) { + const auto& pair = getPairById(pairIds[a]); + int sg = sf(a); + + SHoleBoundaryPoint bp1; + bp1.point = pair.startPoint; + bp1.row = pair.startRow; + bp1.col = pair.startCol; + subGroupBoundaries[sg].push_back(static_cast(boundaryPoints.size())); + boundaryPoints.push_back(bp1); + + SHoleBoundaryPoint bp2; + bp2.point = pair.endPoint; + bp2.row = pair.endRow; + bp2.col = pair.endCol; + subGroupBoundaries[sg].push_back(static_cast(boundaryPoints.size())); + boundaryPoints.push_back(bp2); + } + + for (auto& sg_entry : subGroupBoundaries) { + clusters.push_back(sg_entry.second); + } + } } #endif