This commit is contained in:
cool609 2026-04-01 01:05:40 +08:00
parent fb53e932dc
commit d6e11828a9
9 changed files with 138 additions and 140 deletions

View File

@ -477,10 +477,10 @@ int ProcessSingleFile(const std::string& inputFile,
callbackData.showLines = showLines;
SHoleDetectionDebugCallbacks debugCallbacks;
debugCallbacks.onBoundaryDetected = nullptr;//OnBoundaryDetected;
debugCallbacks.onClustersFound = nullptr;//OnClustersFound;
debugCallbacks.onExpandedRegion = nullptr;//OnExpandedRegion;
debugCallbacks.onHoleFitted = nullptr;// OnHoleFitted;
debugCallbacks.onBoundaryDetected = OnBoundaryDetected;
debugCallbacks.onClustersFound = OnClustersFound;
debugCallbacks.onExpandedRegion = OnExpandedRegion;
debugCallbacks.onHoleFitted = OnHoleFitted;
debugCallbacks.onSegmentPairsDetected = OnSegmentPairsDetected;
debugCallbacks.onLineAngleProfileDetected = nullptr;// OnLineAngleProfileDetected;
debugCallbacks.onPlanesSegmented = OnPlanesSegmented;

View File

@ -174,7 +174,7 @@ int DetectMultipleHoles(
// ============ Step 2: 法向量过滤 ============
NormalFilterParams normalParams;
FilterPlanesByNormal(planes, normalParams);
FilterPlanesByNormal(planes, normalParams, points, totalPointCount);
// 如果没有找到有效平面,回退到处理整个点云
if (planes.empty()) {
@ -296,7 +296,7 @@ int DetectMultipleHoles(
SHoleResult hole;
ret = hole_detection::internal::FitHoleFromExtremePoints(
maskedPoints.data(),
points,
rows,
cols,
clusterPoints,

View File

@ -31,6 +31,7 @@ void EvaluateLine(
int count,
const SHoleDetectionParams& params,
std::vector<SSegmentPair>& segmentPairs,
bool& bIsInvalidLine,
EPanelType panelType
) {
segmentPairs.clear();
@ -75,6 +76,10 @@ void EvaluateLine(
pts[i].signedAngleDeg = 0.0f;
pts[i].hasAngle = false;
pts[i].trend = keLineAngleTrend_Invalid;
if (pts[i].valid) {
bIsInvalidLine = false;
}
}
for (int i = 0; i < count; i++) {
@ -444,23 +449,48 @@ int DetectPitBoundaries(
#if 1
std::vector<std::vector<SSegmentPair>> allRowSegmentPairs(rows);
int startIdx = 0;
int startIdx = 0; int nFirstValidLine = -1; int nLastValidLine = -1;
for (int row = 0; row < rows; row++) {
std::vector<SSegmentPair> curSegmentPairs;
EvaluateLine(points, rows, cols, startIdx, 1, cols, params, curSegmentPairs, EPanelType::YZ_PANEL);
std::vector<SSegmentPair> curSegmentPairs; bool bIsInvalidLine = true;
EvaluateLine(points, rows, cols, startIdx, 1, cols, params, curSegmentPairs, bIsInvalidLine, EPanelType::YZ_PANEL);
if (false == bIsInvalidLine)
{
if (nFirstValidLine < 0) {
nFirstValidLine = row;
}
nLastValidLine = row;
}
allRowSegmentPairs[row] = curSegmentPairs;
startIdx += cols;
}
if (nFirstValidLine >= 0) {
allRowSegmentPairs[nFirstValidLine].clear();
}
if (nLastValidLine >= 0) {
allRowSegmentPairs[nLastValidLine].clear();
}
// 列扫描:逐列调用 EvaluateLine
nFirstValidLine = -1; nLastValidLine = -1;
std::vector<std::vector<SSegmentPair>> allColSegmentPairs(cols);
for (int col = 0; col < cols; col++) {
std::vector<SSegmentPair> curSegmentPairs;
EvaluateLine(points, rows, cols, col, cols, rows, params, curSegmentPairs, EPanelType::XZ_PANEL);
std::vector<SSegmentPair> curSegmentPairs; bool bIsInvalidLine = true;
EvaluateLine(points, rows, cols, col, cols, rows, params, curSegmentPairs, bIsInvalidLine, EPanelType::XZ_PANEL);
if (false == bIsInvalidLine)
{
if (nFirstValidLine < 0) {
nFirstValidLine = col;
}
nLastValidLine = col;
}
allColSegmentPairs[col] = curSegmentPairs;
}
if (nFirstValidLine >= 0) {
allColSegmentPairs[nFirstValidLine].clear();
}
if (nLastValidLine >= 0) {
allColSegmentPairs[nLastValidLine].clear();
}
if (debugCallbacks && debugCallbacks->onSegmentPairsDetected) {
std::vector<SSegmentPair> allSegmentPairs;

View File

@ -177,108 +177,5 @@ int ExtractClusterExtremePoints(
return HD_SUCCESS;
}
int ExpandClusterBoundingBox(
const SVzNLPointXYZ* points,
int rows,
int cols,
const SHoleDetectionParams& params,
std::vector<unsigned char>& isBoundary,
std::vector<SHoleBoundaryPoint>& boundaryPoints,
std::vector<SHoleBoundaryPoint>& clusterPoints,
int expandSize,
int* errCode
) {
if (points == nullptr || rows <= 0 || cols <= 0 || errCode == nullptr) {
if (errCode) *errCode = HD_ERR_INVALID_INPUT;
return HD_ERR_INVALID_INPUT;
}
if (clusterPoints.empty()) {
*errCode = HD_SUCCESS;
return HD_SUCCESS;
}
if (expandSize < 0) {
*errCode = HD_ERR_INVALID_INPUT;
return HD_ERR_INVALID_INPUT;
}
int minRow = INT_MAX;
int maxRow = INT_MIN;
int minCol = INT_MAX;
int maxCol = INT_MIN;
for (const auto& cp : clusterPoints) {
minRow = std::min(minRow, cp.row);
maxRow = std::max(maxRow, cp.row);
minCol = std::min(minCol, cp.col);
maxCol = std::max(maxCol, cp.col);
}
int expandedMinCol = std::max(0, minCol);
int expandedMaxCol = std::min(cols - 1, maxCol);
int expandedMinRow = std::max(0, minRow - expandSize);
int expandedMaxRow = std::min(rows - 1, maxRow + expandSize);
size_t originalSize = clusterPoints.size();
size_t originalBoundarySize = boundaryPoints.size();
std::cout << " [EXPAND] Original BBox=[" << minRow << "," << maxRow << "]x["
<< minCol << "," << maxCol << "]" << std::endl;
std::cout << " [EXPAND] Expanded BBox=[" << expandedMinRow << "," << expandedMaxRow << "]x["
<< expandedMinCol << "," << expandedMaxCol << "]" << std::endl;
std::set<std::pair<int, int>> existingPoints;
for (const auto& cp : clusterPoints) {
existingPoints.insert(std::make_pair(cp.row, cp.col));
}
int scannedColumns = 0;
for (int col = expandedMinCol; col <= expandedMaxCol; col++) {
int startIdx = expandedMinRow * cols + col;
int step = cols;
int count = expandedMaxRow - expandedMinRow + 1;
std::vector<SSegmentPair> segmentPairs;
EvaluateLine(points, rows, cols, startIdx, step, count, params, segmentPairs, EPanelType::XZ_PANEL);
for (const auto& bp : segmentPairs) {
if (existingPoints.find(std::make_pair(bp.startRow, bp.startCol)) == existingPoints.end()) {
SHoleBoundaryPoint pt;
pt.point = bp.startPoint;
pt.row = bp.startRow;
pt.col = bp.startCol;
clusterPoints.push_back(pt);
existingPoints.insert(std::make_pair(pt.row, pt.col));
}
if (existingPoints.find(std::make_pair(bp.endRow, bp.endCol)) == existingPoints.end()) {
SHoleBoundaryPoint pt;
pt.point = bp.endPoint;
pt.row = bp.endRow;
pt.col = bp.endCol;
clusterPoints.push_back(pt);
existingPoints.insert(std::make_pair(pt.row, pt.col));
}
}
if (!segmentPairs.empty()) {
scannedColumns++;
}
}
size_t addedCount = clusterPoints.size() - originalSize;
size_t newBoundaryCount = boundaryPoints.size() - originalBoundarySize;
std::cout << " [EXPAND] Scanned " << (expandedMaxCol - expandedMinCol + 1)
<< " columns, " << scannedColumns << " columns found boundary points" << std::endl;
std::cout << " [EXPAND] Cluster points: " << originalSize << " -> "
<< clusterPoints.size() << " (added " << addedCount << ")" << std::endl;
std::cout << " [EXPAND] Global boundary points: " << originalBoundarySize << " -> "
<< boundaryPoints.size() << " (added " << newBoundaryCount << ")" << std::endl;
*errCode = HD_SUCCESS;
return HD_SUCCESS;
}
} // namespace internal
} // namespace hole_detection

View File

@ -273,26 +273,28 @@ int FitHoleFromExtremePoints(
std::vector<SHoleBoundaryPoint> filteredExtremePoints = extremePoints;
int minRow = INT_MAX;
int maxRow = INT_MIN;
int minCol = INT_MAX;
int maxCol = INT_MIN;
for (const auto& ep : extremePoints) {
minRow = std::min(minRow, ep.row);
maxRow = std::max(maxRow, ep.row);
minCol = std::min(minCol, ep.col);
maxCol = std::max(maxCol, ep.col);
int innerMinRow = INT_MAX, innerMaxRow = INT_MIN;
int innerMinCol = INT_MAX, innerMaxCol = INT_MIN;
for (const auto& ep : filteredExtremePoints) {
innerMinRow = std::min(innerMinRow, ep.row);
innerMaxRow = std::max(innerMaxRow, ep.row);
innerMinCol = std::min(innerMinCol, ep.col);
innerMaxCol = std::max(innerMaxCol, ep.col);
}
innerMinRow = std::max(0, innerMinRow);
innerMaxRow = std::min(rows - 1, innerMaxRow);
innerMinCol = std::max(0, innerMinCol);
innerMaxCol = std::min(cols - 1, innerMaxCol);
int minExpandRow = std::max(0, minRow - detectionParams.expansionSize2);
int maxExpandRow = std::min(rows - 1, maxRow + detectionParams.expansionSize2);
int minExpandCol = std::max(0, minCol - detectionParams.expansionSize2);
int maxExpandCol = std::min(cols - 1, maxCol + detectionParams.expansionSize2);
int minExpandRow = std::max(0, innerMinRow - detectionParams.expansionSize2);
int maxExpandRow = std::min(rows - 1, innerMaxRow + detectionParams.expansionSize2);
int minExpandCol = std::max(0, innerMinCol - detectionParams.expansionSize2);
int maxExpandCol = std::min(cols - 1, innerMaxCol + detectionParams.expansionSize2);
minRow -= detectionParams.expansionSize1;
maxRow += detectionParams.expansionSize1;
minCol -= detectionParams.expansionSize1;
maxCol += detectionParams.expansionSize1;
int minRow = innerMinRow - detectionParams.expansionSize1;
int maxRow = innerMaxRow + detectionParams.expansionSize1;
int minCol = innerMinCol - detectionParams.expansionSize1;
int maxCol = innerMaxCol + detectionParams.expansionSize1;
std::vector<SVzNLPointXYZ> expandedPoints;
for (int r = minExpandRow; r <= maxExpandRow; r++) {
@ -647,6 +649,40 @@ int FitHoleFromExtremePoints(
return HD_ERR_NO_VALID_HOLES;
}
// 搜索圆孔内部点(距圆心 < radius计算各点到参考平面的垂直距离沿法向
// 取最大值作为孔洞实际深度,支持斜面场景。
if (filterParams.minHoleDepth > 0.0f && hasReferencePlane) {
float maxInteriorDist = 0.0f;
float r2 = radius * radius;
float invNorm = static_cast<float>(1.0 / planeResidualNorm);
for (int r = innerMinRow; r <= innerMaxRow; r++) {
for (int c = innerMinCol; c <= innerMaxCol; c++) {
const SVzNLPointXYZ& pt = points[r * cols + c];
if (!IsValidPoint(pt)) continue;
float dx = pt.x - center.x;
float dy = pt.y - center.y;
if (dx * dx + dy * dy < r2) {
// 点到参考平面的有符号垂直距离:(pz - (A*px + B*py + C)) / ||n||
// 正值:点在平面下方(孔洞内部方向);负值:点在平面上方,跳过
float planeZAtPt = static_cast<float>(planeA * pt.x + planeB * pt.y + planeC);
float perpDist = (pt.z - planeZAtPt) * invNorm;
if (perpDist <= 0.0f) continue;
if (perpDist > maxInteriorDist) maxInteriorDist = perpDist;
}
}
}
float holeActualDepth = maxInteriorDist;
std::cout << " [DIAG] holeActualDepth=" << holeActualDepth
<< " (minHoleDepth=" << filterParams.minHoleDepth << ")" << std::endl;
if (holeActualDepth < filterParams.minHoleDepth) {
std::cout << " -> FAILED: holeActualDepth " << holeActualDepth
<< " < minHoleDepth " << filterParams.minHoleDepth << std::endl;
*errCode = HD_ERR_NO_VALID_HOLES;
return HD_ERR_NO_VALID_HOLES;
}
}
hole.center.x = center.x;
hole.center.y = center.y;
hole.center.z = center.z;

View File

@ -37,6 +37,7 @@ void EvaluateLine(
int count,
const SHoleDetectionParams& params,
std::vector<SSegmentPair>& segmentPairs,
bool& bIsInvalidLine,
EPanelType panelType
);

View File

@ -33,6 +33,9 @@ struct SHoleDetectionParams {
// 当曲率角未超过 angleThreshold 时,若前后参考点间的
// 净 Z 坡度角超过此阈值,则将该点判定为下降或上升趋势。
// 设置为 0 可禁用坡度补充判断。
float edgeBoundaryFilterDist; // 边缘过滤距离单位mm默认值0.0,即不过滤)
// pair 中任意端点到首/尾有效线(行扫描取 y列扫描取 x
// 的距离小于此值时,该 pair 将被剔除。
// 构造函数,设置默认值
SHoleDetectionParams()
@ -51,6 +54,7 @@ struct SHoleDetectionParams {
, minFeatureSpan(2.0f)
, residualSmoothWindow(5)
, slopeAngleThreshold(3.0f)
, edgeBoundaryFilterDist(0.0f)
{}
};
@ -78,6 +82,10 @@ struct SHoleFilterParams {
float minInlierRatio; // 圆拟合的最小内点比率默认值0.0
// 表示落在拟合圆容差范围内的点所占比例。
// 深度验证
float minHoleDepth; // 圆孔内部最大 Z 与参考平面 Z 的最小差值单位mm默认值2.5
// 若差值小于此阈值则认为是假孔,返回失败。<=0 表示不过滤。
// 构造函数,设置默认值
SHoleFilterParams()
: maxEccentricity(0.99995f)
@ -87,6 +95,7 @@ struct SHoleFilterParams {
, maxPlaneResidual(10.0f)
, maxAngularGap(90.0f)
, minInlierRatio(0.f)
, minHoleDepth(2.5f)
{}
};

View File

@ -1375,7 +1375,9 @@ int SegmentPlanesByRansac(
int FilterPlanesByNormal(
std::vector<PlaneSegment>& planes,
const NormalFilterParams& params
const NormalFilterParams& params,
const SVzNLPointXYZ* points,
int pointCount
) {
constexpr float kPi = 3.14159265358979323846f;
@ -1412,6 +1414,22 @@ int FilterPlanesByNormal(
<< " planes by normal direction, "
<< planes.size() << " remaining" << std::endl;
// 去除各平面 pointIndices 中距离平面过远的点(上方/下方均过滤)
if (params.maxDistFromPlane > 0.0f && points != nullptr && pointCount > 0) {
for (auto& plane : planes) {
auto& indices = plane.pointIndices;
auto removeIt = std::remove_if(indices.begin(), indices.end(),
[&](int idx) {
if (idx < 0 || idx >= pointCount) return true;
const SVzNLPointXYZ& pt = points[idx];
float signedDist = plane.a * pt.x + plane.b * pt.y + plane.c * pt.z + plane.d;
return std::abs(signedDist) > params.maxDistFromPlane;
});
indices.erase(removeIt, indices.end());
plane.pointCount = static_cast<int>(indices.size());
}
}
for (size_t i = 0; i < planes.size(); i++) {
std::cout << " Plane " << (i + 1)
<< ": normal angle=" << planes[i].normalAngleDeg << " deg"

View File

@ -311,12 +311,14 @@ struct RansacPlaneSegmentationParams {
struct NormalFilterParams {
float maxAngleDeg; // 法向量与参考方向的最大夹角 (度), 建议 30-60
float refNx, refNy, refNz; // 参考法向量方向 (默认Z轴)
float maxDistFromPlane; // 点到平面的最大允许距离;超出则从 pointIndices 中移除;<=0 表示不过滤
NormalFilterParams()
: maxAngleDeg(45.0f)
, refNx(0.0f)
, refNy(0.0f)
, refNz(1.0f)
, maxDistFromPlane(2.0f)
{}
};
@ -366,18 +368,23 @@ int SegmentPlanesByRansac(
);
/**
* @brief
* @brief
*
*
* 使
* 1.
* 2. pointIndices maxDistFromPlane
* maxDistFromPlane <= 0
*
* @param planes ()
* @param params
* @param planes ()
* @param params
* @param points
* @param pointCount
* @return
*/
int FilterPlanesByNormal(
std::vector<PlaneSegment>& planes,
const NormalFilterParams& params
const NormalFilterParams& params,
const SVzNLPointXYZ* points,
int pointCount
);
#endif // PLANE_SEGMENTATION_H