feat(可视化): 添加线段端点对可视化功能

refactor(参数): 简化孔洞检测和过滤参数结构

新增线段端点对检测回调函数和可视化方法,用于显示检测到的线段对。同时简化了孔洞检测参数结构,移除了不常用的聚类参数和半径过滤参数,使接口更加简洁。

修改了线段端点对结构体名称从SPeakValleyPair改为更准确的SSegmentPair,并更新了相关代码。添加了.gitignore文件忽略.ace-tool目录。
This commit is contained in:
MaJunwei 2026-03-12 15:45:50 +08:00
parent ceb02b935f
commit e9ef8d2cc9
9 changed files with 492 additions and 798 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
.claude
nul
Algo\DetectHole\Export
build
build
Algo/DetectHole/Export/

1
Algo/DetectHole/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.ace-tool/

View File

@ -169,13 +169,9 @@ int main(int argc, char* argv[]) {
detectionParams.minRadius = 5.0f; // 最小半径 5mm
detectionParams.maxRadius = 50.0f; // 最大半径 50mm
detectionParams.minPitDepth = 1.0f; // 最小深度 1mm
detectionParams.clusterEps = 5.0f; // 聚类距离 5mm
detectionParams.clusterMinPoints = 5; // 最小聚类点数
// 设置过滤参数
SHoleFilterParams filterParams;
filterParams.minHoleRadius = 3.0f; // 最小孔洞半径 3mm
filterParams.maxHoleRadius = 100.0f; // 最大孔洞半径 100mm
filterParams.maxEccentricity = 0.9f; // 最大离心率
filterParams.minQualityScore = 0.3f; // 最小质量分数
@ -183,7 +179,6 @@ int main(int argc, char* argv[]) {
std::cout << " 半径范围: " << detectionParams.minRadius << " - "
<< detectionParams.maxRadius << " mm" << std::endl;
std::cout << " 最小深度: " << detectionParams.minPitDepth << " mm" << std::endl;
std::cout << " 聚类距离: " << detectionParams.clusterEps << " mm" << std::endl;
std::cout << std::endl;
// 执行孔洞检测

View File

@ -244,6 +244,151 @@ void HoleDetectionVisualizer::VisualizeBoundaryPoints(
}
}
void HoleDetectionVisualizer::VisualizeSegmentPairs(
const SVzNLPointXYZ* points,
int rows,
int cols,
const std::vector<SSegmentPair>& segmentPairs,
const std::string& title)
{
int totalPoints = rows * cols;
// Create original point cloud (gray)
vtkSmartPointer<vtkPoints> vtkPointsOrig = vtkPoints::New();
for (int i = 0; i < totalPoints; i++) {
if (std::isfinite(points[i].x) && std::isfinite(points[i].y) && std::isfinite(points[i].z)) {
vtkPointsOrig->InsertNextPoint(points[i].x, points[i].y, points[i].z);
}
}
vtkSmartPointer<vtkPolyData> polyDataOrig = vtkPolyData::New();
polyDataOrig->SetPoints(vtkPointsOrig);
vtkSmartPointer<vtkVertexGlyphFilter> vertexFilterOrig = vtkVertexGlyphFilter::New();
vertexFilterOrig->SetInputData(polyDataOrig);
vertexFilterOrig->Update();
vtkSmartPointer<vtkPolyDataMapper> mapperOrig = vtkPolyDataMapper::New();
mapperOrig->SetInputConnection(vertexFilterOrig->GetOutputPort());
vtkSmartPointer<vtkActor> actorOrig = vtkActor::New();
actorOrig->SetMapper(mapperOrig);
actorOrig->GetProperty()->SetColor(0.8, 0.8, 0.8); // Gray
actorOrig->GetProperty()->SetPointSize(2);
// Create renderer
vtkSmartPointer<vtkRenderer> renderer = vtkRenderer::New();
renderer->AddActor(actorOrig);
renderer->SetBackground(0.1, 0.1, 0.1);
// Generate colors for each segment pair
std::vector<double> colors = {
1.0, 0.0, 0.0, // Red
0.0, 1.0, 0.0, // Green
0.0, 0.0, 1.0, // Blue
1.0, 1.0, 0.0, // Yellow
1.0, 0.0, 1.0, // Magenta
0.0, 1.0, 1.0, // Cyan
1.0, 0.5, 0.0, // Orange
0.5, 0.0, 1.0, // Purple
0.0, 1.0, 0.5, // Spring Green
1.0, 0.0, 0.5 // Rose
};
// Visualize each segment pair with different color
for (size_t i = 0; i < segmentPairs.size(); i++) {
const SSegmentPair& pair = segmentPairs[i];
// Choose color (cycle through colors)
int colorIdx = (i % (colors.size() / 3)) * 3;
double r = colors[colorIdx];
double g = colors[colorIdx + 1];
double b = colors[colorIdx + 2];
// Create start point sphere
vtkSmartPointer<vtkSphereSource> startSphere = vtkSphereSource::New();
startSphere->SetCenter(pair.startPoint.x, pair.startPoint.y, pair.startPoint.z);
startSphere->SetRadius(0.5);
startSphere->SetPhiResolution(10);
startSphere->SetThetaResolution(10);
vtkSmartPointer<vtkPolyDataMapper> startMapper = vtkPolyDataMapper::New();
startMapper->SetInputConnection(startSphere->GetOutputPort());
vtkSmartPointer<vtkActor> startActor = vtkActor::New();
startActor->SetMapper(startMapper);
startActor->GetProperty()->SetColor(r, g, b);
// Create end point sphere
vtkSmartPointer<vtkSphereSource> endSphere = vtkSphereSource::New();
endSphere->SetCenter(pair.endPoint.x, pair.endPoint.y, pair.endPoint.z);
endSphere->SetRadius(0.5);
endSphere->SetPhiResolution(10);
endSphere->SetThetaResolution(10);
vtkSmartPointer<vtkPolyDataMapper> endMapper = vtkPolyDataMapper::New();
endMapper->SetInputConnection(endSphere->GetOutputPort());
vtkSmartPointer<vtkActor> endActor = vtkActor::New();
endActor->SetMapper(endMapper);
endActor->GetProperty()->SetColor(r, g, b);
// Create line connecting start and end points
vtkSmartPointer<vtkLineSource> line = vtkLineSource::New();
line->SetPoint1(pair.startPoint.x, pair.startPoint.y, pair.startPoint.z);
line->SetPoint2(pair.endPoint.x, pair.endPoint.y, pair.endPoint.z);
vtkSmartPointer<vtkPolyDataMapper> lineMapper = vtkPolyDataMapper::New();
lineMapper->SetInputConnection(line->GetOutputPort());
vtkSmartPointer<vtkActor> lineActor = vtkActor::New();
lineActor->SetMapper(lineMapper);
lineActor->GetProperty()->SetColor(r, g, b);
lineActor->GetProperty()->SetLineWidth(2);
// Add actors to renderer
renderer->AddActor(startActor);
renderer->AddActor(endActor);
renderer->AddActor(lineActor);
}
// Create text annotation
vtkSmartPointer<vtkTextActor> textActor = vtkTextActor::New();
std::ostringstream oss;
oss << "Segment Pairs: " << segmentPairs.size();
textActor->SetInput(oss.str().c_str());
textActor->SetPosition(10, 10);
textActor->GetTextProperty()->SetFontSize(20);
textActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0);
renderer->AddActor2D(textActor);
renderer->ResetCamera();
// Create render window
vtkSmartPointer<vtkRenderWindow> renderWindow = vtkRenderWindow::New();
renderWindow->AddRenderer(renderer);
renderWindow->SetSize(1024, 768);
renderWindow->SetWindowName(title.c_str());
if (m_interactive) {
vtkSmartPointer<vtkRenderWindowInteractor> interactor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
interactor->SetRenderWindow(renderWindow);
// Set trackball camera interaction style
vtkSmartPointer<vtkInteractorStyleTrackballCamera> style =
vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New();
interactor->SetInteractorStyle(style);
renderWindow->Render();
interactor->Initialize();
interactor->Start();
} else {
renderWindow->Render();
SaveScreenshot(renderer, title + ".png");
}
}
void HoleDetectionVisualizer::VisualizeClusters(
const SVzNLPointXYZ* points,
int rows,

View File

@ -54,6 +54,23 @@ public:
const std::string& title = "Detected Boundary Points"
);
/**
* @brief Visualize segment endpoint pairs with different colors
*
* @param points Original point cloud
* @param rows Number of rows
* @param cols Number of columns
* @param segmentPairs Detected segment pairs
* @param title Window title
*/
void VisualizeSegmentPairs(
const SVzNLPointXYZ* points,
int rows,
int cols,
const std::vector<SSegmentPair>& segmentPairs,
const std::string& title = "Detected Segment Pairs"
);
/**
* @brief Visualize clustered boundary points with different colors
*

View File

@ -270,6 +270,33 @@ void OnHoleFitted(const SHoleResult* hole, int holeIndex, void* userData) {
delete[] tempResult.holes;
}
// Callback: Segment pairs detected
void OnSegmentPairsDetected(const SSegmentPair* segmentPairs, int count, void* userData) {
CallbackUserData* data = static_cast<CallbackUserData*>(userData);
if (!data || !data->enableVisualization || !data->visualizer) {
return;
}
std::cout << "[DEBUG] Segment pairs detected: " << count << std::endl;
// Convert to vector for visualization
std::vector<SSegmentPair> pairs;
pairs.reserve(count);
for (int i = 0; i < count; i++) {
pairs.push_back(segmentPairs[i]);
}
// Visualize segment endpoint pairs
data->visualizer->VisualizeSegmentPairs(
data->points,
data->rows,
data->cols,
pairs,
"Step 0: Detected Segment Endpoint Pairs"
);
}
// Process a single file
int ProcessSingleFile(const std::string& inputFile,
bool interactive,
@ -328,10 +355,11 @@ int ProcessSingleFile(const std::string& inputFile,
callbackData.enableVisualization = interactive; // Only enable visualization in interactive mode
SHoleDetectionDebugCallbacks debugCallbacks;
debugCallbacks.onBoundaryDetected = OnBoundaryDetected;
debugCallbacks.onClustersFound = OnClustersFound;
debugCallbacks.onExpandedRegion = OnExpandedRegion;
debugCallbacks.onHoleFitted = OnHoleFitted;
debugCallbacks.onBoundaryDetected = nullptr;// OnBoundaryDetected;
debugCallbacks.onClustersFound = nullptr;// OnClustersFound;
debugCallbacks.onExpandedRegion = nullptr;// OnExpandedRegion;
debugCallbacks.onHoleFitted = nullptr;// OnHoleFitted;
debugCallbacks.onSegmentPairsDetected = nullptr;//OnSegmentPairsDetected;
debugCallbacks.userData = &callbackData;
SMultiHoleResult result;
@ -495,12 +523,8 @@ int main(int argc, char* argv[]) {
std::cout << " angleThresholdPos: " << detectionParams.angleThresholdPos << "°" << std::endl;
std::cout << " angleThresholdNeg: " << detectionParams.angleThresholdNeg << "°" << std::endl;
std::cout << " minPitDepth: " << detectionParams.minPitDepth << " mm" << std::endl;
std::cout << " clusterEps: " << detectionParams.clusterEps << " mm" << std::endl;
std::cout << " clusterMinPoints: " << detectionParams.clusterMinPoints << std::endl;
std::cout << "\nFilter Parameters:" << std::endl;
std::cout << " minHoleRadius: " << filterParams.minHoleRadius << " mm" << std::endl;
std::cout << " maxHoleRadius: " << filterParams.maxHoleRadius << " mm" << std::endl;
std::cout << " maxEccentricity: " << filterParams.maxEccentricity << std::endl;
int result = ProcessSingleFile(inputPath, interactive, outputDir, showLines,

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,14 @@ struct SHoleDetectionDebugCallbacks {
*/
void (*onHoleFitted)(const SHoleResult* hole, int holeIndex, void* userData);
/**
* @brief Called when segment endpoint pairs are detected
* @param segmentPairs Array of segment pairs (each pair has start and end points)
* @param count Number of segment pairs
* @param userData User-provided context pointer
*/
void (*onSegmentPairsDetected)(const SSegmentPair* segmentPairs, int count, void* userData);
/**
* @brief User-provided context pointer, passed to all callbacks
*/

View File

@ -2,141 +2,103 @@
#define HOLE_DETECTION_PARAMS_H
#include <cmath>
#include "../include/VZNL_Types.h" // Use types from VZNL_Types.h
#include "../include/VZNL_Types.h" // 使用 VZNL_Types.h 中的类型
/**
* @brief Sorting mode for detected holes
* @brief
*/
enum ESortMode {
keSortMode_None = 0, // No sorting
keSortMode_ByRadius = 1, // Sort by radius (largest first)
keSortMode_ByDepth = 2, // Sort by depth (deepest first)
keSortMode_ByQuality = 3 // Sort by quality score (highest first)
keSortMode_None = 0, // 不排序
keSortMode_ByRadius = 1, // 按半径排序(最大的在前)
keSortMode_ByDepth = 2, // 按深度排序(最深的在前)
keSortMode_ByQuality = 3 // 按质量分数排序(最高的在前)
};
/**
* @brief Detection parameters for hole detection algorithm
* @brief
*/
struct SHoleDetectionParams {
// Pit detection parameters
int neighborCount; // Adjacent points for line connection (default: 3)
float angleThresholdPos; // Positive angle threshold in degrees (default: 70.0)
float angleThresholdNeg; // Negative angle threshold in degrees (default: -70.0)
float minPitDepth; // Minimum pit depth in mm (default: 5.0)
// 凹坑检测参数
int neighborCount; // 线连接的相邻点数默认值3
float angleThresholdPos; // 正角度阈值单位默认值10.0
float angleThresholdNeg; // 负角度阈值,单位:度(默认值:-10.0
float minPitDepth; // 最小凹坑深度单位mm默认值1.0
// Radial scanning parameters
float angleStep; // Angular step for radial scan in degrees (default: 1.0)
float maxScanRadius; // Maximum scan radius in mm (default: 100.0)
// 椭圆拟合参数
float minRadius; // 最小孔洞半径单位mm默认值2.0
float maxRadius; // 最大孔洞半径单位mm默认值50.0
// Clustering parameters (DBSCAN)
float clusterEps; // DBSCAN epsilon in mm (default: 10.0)
int clusterMinPoints; // DBSCAN min points (default: 5)
// 平面拟合参数
int expansionSize1; // 第一环扩展大小单位mm默认值5
int expansionSize2; // 第二环扩展大小单位mm默认值10
// Ellipse fitting parameters
float minRadius; // Minimum hole radius in mm (default: 5.0)
float maxRadius; // Maximum hole radius in mm (default: 50.0)
// V型检测参数
int minVTransitionPoints; // V型端点之间的最小有效过渡点数默认值1
// Plane fitting parameters
int expansionSize1; // First ring expansion in mm (default: 10.0)
int expansionSize2; // Second ring expansion in mm (default: 20.0)
// Validation parameters
float validZThreshold; // Valid Z-value threshold (default: 1e-4)
// V-type detection parameters
int minVTransitionPoints; // Minimum valid transition points between V-shape endpoints (default: 3)
// Corner-based angle detection parameters (similar to cornerMethod)
float cornerScale; // Search distance for forward/backward points in mm (default: 5.0)
float cornerAngleThreshold; // Minimum corner angle change in degrees (default: 15.0)
float jumpCornerTh_1; // Small angle threshold for jump detection (default: 10.0)
float jumpCornerTh_2; // Large angle threshold for jump detection (default: 30.0)
float minEndingGap; // Y-direction distance threshold for jump pairing in mm (default: 5.0)
float minEndingGap_z; // Z-direction height threshold for jump validation in mm (default: 1.0)
// Constructor with defaults
// 构造函数,设置默认值
SHoleDetectionParams()
: neighborCount(3)
, angleThresholdPos(10.0f)
, angleThresholdNeg(-10.0f)
, minPitDepth(1.0f)
, angleStep(1.0f)
, maxScanRadius(100.0f)
, clusterEps(10.0f)
, clusterMinPoints(3)
, minRadius(2.0f)
, maxRadius(50.0f)
, expansionSize1(5)
, expansionSize2(10)
, validZThreshold(1e-4f)
, minVTransitionPoints(1)
, cornerScale(5.0f)
, cornerAngleThreshold(45.0f)
, jumpCornerTh_1(10.0f)
, jumpCornerTh_2(30.0f)
, minEndingGap(5.0f)
, minEndingGap_z(1.0f)
{}
};
/**
* @brief Filter parameters for hole filtering
* @brief
*/
struct SHoleFilterParams {
// Size range filtering
float minHoleRadius; // Minimum hole radius in mm (default: 5.0)
float maxHoleRadius; // Maximum hole radius in mm (default: 50.0)
// Quality threshold filtering
float maxEccentricity; // Maximum eccentricity (default: 0.5, standard e=sqrt(1-(b/a)^2))
// 质量阈值过滤
float maxEccentricity; // 最大离心率默认值0.99995,标准公式 e=sqrt(1-(b/a)^2)
// Shape filtering (pre-fitting)
float maxCornerRatio; // Maximum corner ratio for rectangularity (default: 0.15)
// Higher = more rectangular. Set to 1.0 to disable.
float minAngularCoverage; // Minimum angular coverage in degrees (default: 300.0)
// Used to filter non-closed boundaries. Set to 0 to disable.
float maxRadiusFitRatio; // Maximum radiusVariance/radius ratio (default: 0.3)
// Measures how well points fit the circle. Set to 1.0 to disable.
float minQualityScore; // Minimum overall quality score (default: 0.3)
// Weighted combination of shape metrics. Set to 0 to disable.
// 形状过滤(拟合前)
float minAngularCoverage; // 最小角度覆盖范围单位默认值10.0
// 用于过滤非闭合边界。设置为 0 可禁用。
float maxRadiusFitRatio; // 最大半径拟合比率 radiusVariance/radius默认值1.0
// 衡量点与圆的拟合程度。设置为 1.0 可禁用。
float minQualityScore; // 最小整体质量分数默认值0.0
// 形状指标的加权组合。设置为 0 可禁用。
// Planarity filtering (pre-projection)
float maxPlaneResidual; // Maximum point-to-plane residual in mm (default: 10.0)
// Rejects non-planar clusters (e.g. cliffs, step edges).
float maxAngularGap; // Maximum angular gap in degrees (default: 90.0)
// Rejects L-shaped or non-closed boundaries.
float minInlierRatio; // Minimum inlier ratio for ellipse fit (default: 0.7)
// Fraction of points within tolerance of fitted ellipse.
// 平面性过滤(投影前)
float maxPlaneResidual; // 最大点到平面残差单位mm默认值10.0
// 拒绝非平面簇(例如悬崖、台阶边缘)。
float maxAngularGap; // 最大角度间隙单位默认值90.0
// 拒绝 L 型或非闭合边界。
float minInlierRatio; // 椭圆拟合的最小内点比率默认值0.0
// 在拟合椭圆容差范围内的点的比例。
// Constructor with defaults
// 构造函数,设置默认值
SHoleFilterParams()
: minHoleRadius(1.0f)
, maxHoleRadius(10.0f)
, maxEccentricity(0.99995f)
, maxCornerRatio(0.15f)
: maxEccentricity(0.99995f)
, minAngularCoverage(10.f)
, maxRadiusFitRatio(0.3f)
, minQualityScore(0.3f)
, maxRadiusFitRatio(1.f)
, minQualityScore(0.f)
, maxPlaneResidual(10.0f)
, maxAngularGap(90.0f)
, minInlierRatio(0.3f)
, minInlierRatio(0.f)
{}
};
/**
* @brief Single hole detection result
* @brief
*
* Note: SVzNL3DPointF and SVzNL2DPointF are defined in VZNL_Types.h
* SVzNL3DPointF SVzNL2DPointF VZNL_Types.h
*/
struct SHoleResult {
SVzNL3DPointF center; // Hole center point (x, y, z)
SVzNL3DPointF normal; // Plane normal vector (unit length)
float radius; // Hole radius in mm
float depth; // Pit depth in mm
float eccentricity; // Eccentricity (0 = perfect circle)
float radiusVariance; // Radius variance in mm
float angularSpan; // Angular coverage in degrees
float qualityScore; // Quality score (0-1, higher = better)
SVzNL3DPointF center; // 孔洞中心点 (x, y, z)
SVzNL3DPointF normal; // 平面法向量(单位长度)
float radius; // 孔洞半径,单位:mm
float depth; // 凹坑深度,单位:mm
float eccentricity; // 离心率0 = 完美圆形)
float radiusVariance; // 半径方差,单位:mm
float angularSpan; // 角度覆盖范围,单位:度
float qualityScore; // 质量分数0-1越高越好
SHoleResult()
: center()
@ -151,16 +113,16 @@ struct SHoleResult {
};
/**
* @brief Multiple hole detection result
* @brief
*
* @note Memory management: The holes array is NOT automatically freed.
* Caller must call FreeMultiHoleResult() or manually delete[] holes.
* @note holes
* FreeMultiHoleResult() delete[] holes
*/
struct SMultiHoleResult {
int holeCount; // Number of detected holes
SHoleResult* holes; // Array of hole results (caller must free)
int totalCandidates; // Total candidate holes before filtering
int filteredCount; // Number of holes filtered out
int holeCount; // 检测到的孔洞数量
SHoleResult* holes; // 孔洞结果数组(调用者必须释放)
int totalCandidates; // 过滤前的候选孔洞总数
int filteredCount; // 被过滤掉的孔洞数量
SMultiHoleResult()
: holeCount(0)
@ -171,9 +133,9 @@ struct SMultiHoleResult {
};
/**
* @brief Free memory allocated by DetectMultipleHoles
* @brief DetectMultipleHoles
*
* @param [in,out] result Result structure to free
* @param [in,out] result
*/
inline void FreeMultiHoleResult(SMultiHoleResult* result) {
if (result != nullptr && result->holes != nullptr) {
@ -184,28 +146,28 @@ inline void FreeMultiHoleResult(SMultiHoleResult* result) {
}
/**
* @brief Segment endpoints pair structure
* @brief 线
*
* Represents a segment detected in a line scan, with start and end points.
* Note: Despite the name "PeakValley", this structure stores segment endpoints,
* not necessarily peak/valley points. The naming is kept for compatibility.
* 线线
* "PeakValley"线
* /
*/
struct SPeakValleyPair {
SVzNLPointXYZ peakPoint; // Segment start point
SVzNLPointXYZ valleyPoint; // Segment end point
int peakRow; // Start point row index
int peakCol; // Start point column index
int valleyRow; // End point row index
int valleyCol; // End point column index
float depth; // Depth difference between start and end points
struct SSegmentPair {
SVzNLPointXYZ startPoint; // 线段起点
SVzNLPointXYZ endPoint; // 线段终点
int startRow; // 起点行索引
int startCol; // 起点列索引
int endRow; // 终点行索引
int endCol; // 终点列索引
float depth; // 起点和终点之间的深度差
SPeakValleyPair()
: peakPoint()
, valleyPoint()
, peakRow(0)
, peakCol(0)
, valleyRow(0)
, valleyCol(0)
SSegmentPair()
: startPoint()
, endPoint()
, startRow(0)
, startCol(0)
, endRow(0)
, endCol(0)
, depth(0.0f)
{}
};