From e233f0fd96b7222f7ba6372ed8f29b22f4e24009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 18 May 2026 15:03:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BE=B9=E7=BC=98?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E6=8B=9F=E5=90=88=E5=9C=86=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=20+=20=E4=BC=98=E5=8C=96=E6=8B=9F=E5=90=88=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 EdgeCircleFitProcessor 算子(卡尺径向边缘检测 + Kasa/RANSAC圆拟合) - 新增 EdgeCircleFitPanel 辅助面板(拖拽画圆交互) - Ribbon快捷工具组新增「圆拟合」按钮 - 拟合后卡尺保持可编辑状态,支持调整后重新拟合 - 每次拟合自动清除上一次结果 - 拟合方法固定RANSAC,UI不暴露选择 - 结果标注简化:直线显示角度,圆显示半径和圆心坐标 - 不再显示内点/外点小圆点 - 添加中英文本地化资源 --- XP.Common/Resources/Resources.en-US.resx | 62 ++ XP.Common/Resources/Resources.resx | 62 ++ XP.Common/Resources/Resources.zh-CN.resx | 62 ++ .../检测分析/EdgeCircleFitProcessor.cs | 582 ++++++++++++++++++ .../ImageProcessing/EdgeCircleFitViewModel.cs | 497 +++++++++++++++ .../ImageProcessing/EdgeLineFitViewModel.cs | 39 +- XplorePlane/ViewModels/Main/MainViewModel.cs | 22 + .../ImageProcessing/EdgeCircleFitPanel.xaml | 128 ++++ .../EdgeCircleFitPanel.xaml.cs | 52 ++ .../ImageProcessing/EdgeLineFitPanel.xaml | 5 - XplorePlane/Views/Main/MainWindow.xaml | 1 + 11 files changed, 1482 insertions(+), 30 deletions(-) create mode 100644 XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs create mode 100644 XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs create mode 100644 XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml create mode 100644 XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx index 5885aaf..75a2e64 100644 --- a/XP.Common/Resources/Resources.en-US.resx +++ b/XP.Common/Resources/Resources.en-US.resx @@ -1943,4 +1943,66 @@ Reprojection error: {1:F4} pixels Drawing thickness for result visualization + + + + Edge Find Circle Fit + + + Place calipers along estimated circle to detect edge points and fit a circle (supports Least Squares and RANSAC) + + + Caliper Count + + + Number of calipers placed evenly around the circle + + + Caliper Width + + + Search length of each caliper along radial direction (pixels) + + + Edge Polarity + + + Edge direction: BrightToDark, DarkToBright, or Both + + + Edge Threshold + + + Gradient strength threshold; edges below this value are ignored + + + Smoothing Sigma + + + Gaussian smoothing standard deviation for noise suppression + + + Search Direction + + + Caliper search direction: Inward (toward center), Outward (away from center), Both + + + Fit Method + + + Circle fitting algorithm: LeastSquares or RANSAC (robust, rejects outliers) + + + RANSAC Threshold + + + RANSAC inlier distance threshold (pixels); points closer than this to the circle are inliers + + + Line Thickness + + + Drawing thickness for result visualization + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx index eac5543..e102321 100644 --- a/XP.Common/Resources/Resources.resx +++ b/XP.Common/Resources/Resources.resx @@ -1976,4 +1976,66 @@ 绘制结果的线条粗细 + + + + 边缘查找拟合圆 + + + 沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿圆周等角度放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺沿径向的搜索长度(像素) + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声 + + + 搜索方向 + + + 卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向) + + + 拟合方法 + + + 圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx index 0d99bfa..f3ce870 100644 --- a/XP.Common/Resources/Resources.zh-CN.resx +++ b/XP.Common/Resources/Resources.zh-CN.resx @@ -1937,4 +1937,66 @@ 绘制结果的线条粗细 + + + + 边缘查找拟合圆 + + + 沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿圆周等角度放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺沿径向的搜索长度(像素) + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声 + + + 搜索方向 + + + 卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向) + + + 拟合方法 + + + 圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs b/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs new file mode 100644 index 0000000..2386c81 --- /dev/null +++ b/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs @@ -0,0 +1,582 @@ +// ============================================================================ +// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. +// 文件名: EdgeCircleFitProcessor.cs +// 描述: 边缘查找拟合圆算子 +// 功能: +// - 沿预估圆周等角度放置卡尺,每个卡尺沿径向搜索边缘点 +// - 支持亚像素精度(抛物线插值) +// - 支持边缘极性选择和搜索方向(向内/向外) +// - 使用最小二乘或RANSAC算法拟合圆 +// - 输出拟合圆参数、边缘点、内点/外点、拟合误差 +// 算法: 卡尺边缘检测 + 最小二乘/RANSAC圆拟合 +// 作者: 李伟 wei.lw.li@hexagon.com +// ============================================================================ + +using Emgu.CV; +using Emgu.CV.Structure; +using XP.ImageProcessing.Core; +using Serilog; +using System.Drawing; + +namespace XP.ImageProcessing.Processors; + +/// +/// 圆拟合结果 +/// +public class CircleFitResult +{ + /// 拟合是否成功 + public bool Success { get; set; } + + /// 拟合圆心X + public double CenterX { get; set; } + + /// 拟合圆心Y + public double CenterY { get; set; } + + /// 拟合半径 + public double Radius { get; set; } + + /// 所有检测到的边缘点 + public List EdgePoints { get; set; } = new(); + + /// 内点列表 + public List Inliers { get; set; } = new(); + + /// 外点列表 + public List Outliers { get; set; } = new(); + + /// 平均拟合误差(像素) + public double FitError { get; set; } + + /// 有效边缘点数 + public int EdgePointCount { get; set; } +} + +/// +/// 边缘查找拟合圆算子 - 沿预估圆周放置卡尺检测边缘点并拟合圆 +/// +public class EdgeCircleFitProcessor : ImageProcessorBase +{ + private static readonly ILogger _logger = Log.ForContext(); + private static readonly Random _random = new(); + + public EdgeCircleFitProcessor() + { + Name = LocalizationHelper.GetString("EdgeCircleFitProcessor_Name"); + Description = LocalizationHelper.GetString("EdgeCircleFitProcessor_Description"); + } + + protected override void InitializeParameters() + { + // ── 预估圆参数(由UI交互注入,不可见) ── + Parameters.Add("CenterX", new ProcessorParameter( + "CenterX", "CenterX", typeof(int), 200, null, null, "") { IsVisible = false }); + Parameters.Add("CenterY", new ProcessorParameter( + "CenterY", "CenterY", typeof(int), 200, null, null, "") { IsVisible = false }); + Parameters.Add("Radius", new ProcessorParameter( + "Radius", "Radius", typeof(int), 100, null, null, "") { IsVisible = false }); + + // ── 卡尺参数 ── + Parameters.Add("CaliperCount", new ProcessorParameter( + "CaliperCount", + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperCount"), + typeof(int), 36, 3, 360, + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperCount_Desc"))); + + Parameters.Add("CaliperWidth", new ProcessorParameter( + "CaliperWidth", + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperWidth"), + typeof(int), 40, 5, 500, + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperWidth_Desc"))); + + // ── 边缘检测参数 ── + Parameters.Add("EdgePolarity", new ProcessorParameter( + "EdgePolarity", + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgePolarity"), + typeof(string), "Both", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgePolarity_Desc"), + new string[] { "BrightToDark", "DarkToBright", "Both" })); + + Parameters.Add("EdgeThreshold", new ProcessorParameter( + "EdgeThreshold", + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgeThreshold"), + typeof(int), 20, 1, 255, + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgeThreshold_Desc"))); + + Parameters.Add("Sigma", new ProcessorParameter( + "Sigma", + LocalizationHelper.GetString("EdgeCircleFitProcessor_Sigma"), + typeof(double), 1.0, 0.1, 10.0, + LocalizationHelper.GetString("EdgeCircleFitProcessor_Sigma_Desc"))); + + Parameters.Add("SearchDirection", new ProcessorParameter( + "SearchDirection", + LocalizationHelper.GetString("EdgeCircleFitProcessor_SearchDirection"), + typeof(string), "Both", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_SearchDirection_Desc"), + new string[] { "Inward", "Outward", "Both" })); + + // ── 拟合参数 ── + Parameters.Add("FitMethod", new ProcessorParameter( + "FitMethod", + LocalizationHelper.GetString("EdgeCircleFitProcessor_FitMethod"), + typeof(string), "RANSAC", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_FitMethod_Desc"), + new string[] { "LeastSquares", "RANSAC" })); + + Parameters.Add("RansacThreshold", new ProcessorParameter( + "RansacThreshold", + LocalizationHelper.GetString("EdgeCircleFitProcessor_RansacThreshold"), + typeof(double), 2.0, 0.5, 20.0, + LocalizationHelper.GetString("EdgeCircleFitProcessor_RansacThreshold_Desc"))); + + Parameters.Add("Thickness", new ProcessorParameter( + "Thickness", + LocalizationHelper.GetString("EdgeCircleFitProcessor_Thickness"), + typeof(int), 2, 1, 10, + LocalizationHelper.GetString("EdgeCircleFitProcessor_Thickness_Desc"))); + } + + public override Image Process(Image inputImage) + { + int centerX = GetParameter("CenterX"); + int centerY = GetParameter("CenterY"); + int radius = GetParameter("Radius"); + int caliperCount = GetParameter("CaliperCount"); + int caliperWidth = GetParameter("CaliperWidth"); + string edgePolarity = GetParameter("EdgePolarity"); + int edgeThreshold = GetParameter("EdgeThreshold"); + double sigma = GetParameter("Sigma"); + string searchDirection = GetParameter("SearchDirection"); + string fitMethod = GetParameter("FitMethod"); + double ransacThreshold = GetParameter("RansacThreshold"); + + OutputData.Clear(); + + _logger.Debug( + "EdgeCircleFit started: Center=({CX},{CY}), R={R}, Calipers={Count}, Width={Width}", + centerX, centerY, radius, caliperCount, caliperWidth); + + if (radius < 5) + { + _logger.Warning("Radius too small for circle fitting"); + OutputData["CircleFitResult"] = new CircleFitResult { Success = false }; + return inputImage.Clone(); + } + + // 沿圆周等角度放置卡尺 + var edgePoints = new List(); + double angleStep = 2.0 * Math.PI / caliperCount; + + for (int i = 0; i < caliperCount; i++) + { + double angle = angleStep * i; + // 圆周上的采样点 + double sampleX = centerX + radius * Math.Cos(angle); + double sampleY = centerY + radius * Math.Sin(angle); + + // 径向方向(从圆心指向外) + double dirX = Math.Cos(angle); + double dirY = Math.Sin(angle); + + // 根据搜索方向确定卡尺搜索方向 + double searchDirX, searchDirY; + if (searchDirection == "Inward") + { + searchDirX = -dirX; + searchDirY = -dirY; + } + else if (searchDirection == "Outward") + { + searchDirX = dirX; + searchDirY = dirY; + } + else // Both: 搜索方向为径向(从内到外),卡尺中心在圆周上 + { + searchDirX = dirX; + searchDirY = dirY; + } + + var edgePoint = FindEdgeInCaliper( + inputImage, sampleX, sampleY, searchDirX, searchDirY, + caliperWidth, edgePolarity, edgeThreshold, sigma, i); + + if (edgePoint != null) + { + edgePoints.Add(edgePoint); + } + } + + _logger.Debug("Found {Count} edge points from {Total} calipers", edgePoints.Count, caliperCount); + + // 拟合圆 + var result = FitCircle(edgePoints, fitMethod, ransacThreshold); + + // 存储输出 + OutputData["CircleFitResult"] = result; + OutputData["EdgePoints"] = edgePoints.Select(p => p.Position).ToArray(); + OutputData["EdgePointCount"] = edgePoints.Count; + OutputData["Thickness"] = GetParameter("Thickness"); + + if (result.Success) + { + OutputData["FittedCenterX"] = result.CenterX; + OutputData["FittedCenterY"] = result.CenterY; + OutputData["FittedRadius"] = result.Radius; + OutputData["InlierPoints"] = result.Inliers.ToArray(); + OutputData["OutlierPoints"] = result.Outliers.ToArray(); + OutputData["FitError"] = result.FitError; + + _logger.Information( + "EdgeCircleFit completed: Center=({CX:F2},{CY:F2}), R={R:F2}, Inliers={Inliers}/{Total}, Error={Error:F3}px", + result.CenterX, result.CenterY, result.Radius, + result.Inliers.Count, edgePoints.Count, result.FitError); + } + else + { + _logger.Warning("EdgeCircleFit failed: insufficient edge points"); + } + + return inputImage.Clone(); + } + + // ══════════════════════════════════════════════════════════════ + // 卡尺边缘检测(复用直线拟合中的逻辑) + // ══════════════════════════════════════════════════════════════ + + private EdgePointInfo? FindEdgeInCaliper( + Image image, + double centerX, double centerY, + double dirX, double dirY, + int caliperWidth, string polarity, + int threshold, double sigma, int caliperIndex) + { + int halfWidth = caliperWidth / 2; + int profileLength = caliperWidth; + + var profile = new double[profileLength]; + int validCount = 0; + + for (int i = 0; i < profileLength; i++) + { + double offset = i - halfWidth; + double px = centerX + dirX * offset; + double py = centerY + dirY * offset; + + int ix = (int)Math.Round(px); + int iy = (int)Math.Round(py); + + if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height) + { + profile[i] = image.Data[iy, ix, 0]; + validCount++; + } + else + { + profile[i] = 0; + } + } + + if (validCount < profileLength * 0.5) + return null; + + if (sigma > 0.1) + profile = GaussianSmooth1D(profile, sigma); + + var derivative = new double[profileLength]; + for (int i = 1; i < profileLength - 1; i++) + derivative[i] = (profile[i + 1] - profile[i - 1]) / 2.0; + + int bestIdx = -1; + double bestStrength = 0; + + for (int i = 2; i < profileLength - 2; i++) + { + double strength = derivative[i]; + bool validPolarity = polarity switch + { + "BrightToDark" => strength < 0, + "DarkToBright" => strength > 0, + _ => true + }; + + if (!validPolarity) continue; + + double absStrength = Math.Abs(strength); + if (absStrength >= threshold && absStrength > bestStrength) + { + bestStrength = absStrength; + bestIdx = i; + } + } + + if (bestIdx < 0) + return null; + + // 亚像素插值 + double subPixelOffset = 0; + if (bestIdx > 0 && bestIdx < profileLength - 1) + { + double left = Math.Abs(derivative[bestIdx - 1]); + double center = Math.Abs(derivative[bestIdx]); + double right = Math.Abs(derivative[bestIdx + 1]); + double denom = 2.0 * (2.0 * center - left - right); + if (Math.Abs(denom) > 1e-6) + { + subPixelOffset = (left - right) / denom; + subPixelOffset = Math.Clamp(subPixelOffset, -0.5, 0.5); + } + } + + double edgeOffset = (bestIdx + subPixelOffset) - halfWidth; + float edgeX = (float)(centerX + dirX * edgeOffset); + float edgeY = (float)(centerY + dirY * edgeOffset); + + return new EdgePointInfo + { + Position = new PointF(edgeX, edgeY), + Strength = bestStrength, + CaliperIndex = caliperIndex, + IsInlier = true + }; + } + + private static double[] GaussianSmooth1D(double[] data, double sigma) + { + int kernelRadius = (int)Math.Ceiling(sigma * 3); + int kernelSize = kernelRadius * 2 + 1; + var kernel = new double[kernelSize]; + double sum = 0; + + for (int i = 0; i < kernelSize; i++) + { + double x = i - kernelRadius; + kernel[i] = Math.Exp(-x * x / (2.0 * sigma * sigma)); + sum += kernel[i]; + } + for (int i = 0; i < kernelSize; i++) + kernel[i] /= sum; + + var result = new double[data.Length]; + for (int i = 0; i < data.Length; i++) + { + double val = 0, wSum = 0; + for (int k = 0; k < kernelSize; k++) + { + int idx = i + k - kernelRadius; + if (idx >= 0 && idx < data.Length) + { + val += data[idx] * kernel[k]; + wSum += kernel[k]; + } + } + result[i] = wSum > 0 ? val / wSum : data[i]; + } + return result; + } + + // ══════════════════════════════════════════════════════════════ + // 圆拟合 + // ══════════════════════════════════════════════════════════════ + + private CircleFitResult FitCircle(List edgePoints, string method, double ransacThreshold) + { + var result = new CircleFitResult(); + + if (edgePoints.Count < 3) + { + result.Success = false; + return result; + } + + if (method == "RANSAC" && edgePoints.Count >= 4) + return FitCircleRANSAC(edgePoints, ransacThreshold); + else + return FitCircleLeastSquares(edgePoints); + } + + /// + /// 最小二乘拟合圆(Kasa方法) + /// 将 (x-a)² + (y-b)² = r² 展开为: x² + y² = 2ax + 2by + (r²-a²-b²) + /// 令 c = r²-a²-b², 线性方程: 2ax + 2by + c = x² + y² + /// + private CircleFitResult FitCircleLeastSquares(List edgePoints) + { + var points = edgePoints.Select(p => p.Position).ToArray(); + var (cx, cy, r) = KasaFit(points); + + var result = new CircleFitResult + { + Success = true, + CenterX = cx, + CenterY = cy, + Radius = r, + Inliers = points.ToList(), + Outliers = new List(), + EdgePointCount = edgePoints.Count, + EdgePoints = edgePoints + }; + + foreach (var ep in edgePoints) + ep.IsInlier = true; + + result.FitError = ComputeCircleFitError(points, cx, cy, r); + return result; + } + + /// + /// RANSAC 圆拟合 + /// + private CircleFitResult FitCircleRANSAC(List edgePoints, double threshold) + { + var result = new CircleFitResult(); + var points = edgePoints.Select(p => p.Position).ToArray(); + int n = points.Length; + + int maxIterations = Math.Min(2000, n * (n - 1) * (n - 2) / 6); + int bestInlierCount = 0; + double bestCx = 0, bestCy = 0, bestR = 0; + List bestInlierIndices = new(); + + for (int iter = 0; iter < maxIterations; iter++) + { + // 随机选3个点 + int i1 = _random.Next(n), i2 = _random.Next(n), i3 = _random.Next(n); + if (i1 == i2 || i1 == i3 || i2 == i3) continue; + + var (cx, cy, r) = FitCircleFrom3Points(points[i1], points[i2], points[i3]); + if (r <= 0 || double.IsNaN(r)) continue; + + // 统计内点 + var inlierIndices = new List(); + for (int i = 0; i < n; i++) + { + double dist = Math.Abs(Distance(points[i], cx, cy) - r); + if (dist <= threshold) + inlierIndices.Add(i); + } + + if (inlierIndices.Count > bestInlierCount) + { + bestInlierCount = inlierIndices.Count; + bestInlierIndices = inlierIndices; + + // 用所有内点重新拟合 + var inlierPoints = inlierIndices.Select(i => points[i]).ToArray(); + (bestCx, bestCy, bestR) = KasaFit(inlierPoints); + } + + if (bestInlierCount > n * 0.95) + break; + } + + if (bestInlierCount < 3) + { + result.Success = false; + return result; + } + + result.Success = true; + result.CenterX = bestCx; + result.CenterY = bestCy; + result.Radius = bestR; + + var inlierSet = new HashSet(bestInlierIndices); + for (int i = 0; i < n; i++) + { + if (inlierSet.Contains(i)) + { + result.Inliers.Add(points[i]); + edgePoints[i].IsInlier = true; + } + else + { + result.Outliers.Add(points[i]); + edgePoints[i].IsInlier = false; + } + } + + result.FitError = ComputeCircleFitError(result.Inliers.ToArray(), bestCx, bestCy, bestR); + result.EdgePointCount = edgePoints.Count; + result.EdgePoints = edgePoints; + + return result; + } + + /// + /// Kasa 最小二乘圆拟合 + /// + private static (double cx, double cy, double r) KasaFit(PointF[] points) + { + int n = points.Length; + if (n < 3) return (0, 0, 0); + + // 构建线性方程组: A * [a, b, c]^T = B + // 其中 2*a*xi + 2*b*yi + c = xi² + yi² + double sumX = 0, sumY = 0, sumX2 = 0, sumY2 = 0; + double sumXY = 0, sumX3 = 0, sumY3 = 0, sumX2Y = 0, sumXY2 = 0; + + for (int i = 0; i < n; i++) + { + double x = points[i].X, y = points[i].Y; + double x2 = x * x, y2 = y * y; + sumX += x; sumY += y; + sumX2 += x2; sumY2 += y2; + sumXY += x * y; + sumX3 += x2 * x; sumY3 += y2 * y; + sumX2Y += x2 * y; sumXY2 += x * y2; + } + + double A = n * sumX2 - sumX * sumX; + double B = n * sumXY - sumX * sumY; + double C = n * sumY2 - sumY * sumY; + double D = 0.5 * (n * (sumX3 + sumXY2) - sumX * (sumX2 + sumY2)); + double E = 0.5 * (n * (sumX2Y + sumY3) - sumY * (sumX2 + sumY2)); + + double denom = A * C - B * B; + if (Math.Abs(denom) < 1e-10) + return (0, 0, 0); + + double cx = (D * C - B * E) / denom; + double cy = (A * E - B * D) / denom; + double r = Math.Sqrt((sumX2 + sumY2 - 2 * cx * sumX - 2 * cy * sumY) / n + cx * cx + cy * cy); + + return (cx, cy, r); + } + + /// + /// 3点拟合圆 + /// + private static (double cx, double cy, double r) FitCircleFrom3Points(PointF p1, PointF p2, PointF p3) + { + double ax = p1.X, ay = p1.Y; + double bx = p2.X, by = p2.Y; + double cx = p3.X, cy = p3.Y; + + double d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (Math.Abs(d) < 1e-10) + return (0, 0, -1); + + double ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d; + double uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d; + double r = Math.Sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy)); + + return (ux, uy, r); + } + + private static double Distance(PointF p, double cx, double cy) + { + double dx = p.X - cx, dy = p.Y - cy; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static double ComputeCircleFitError(PointF[] points, double cx, double cy, double r) + { + if (points.Length == 0) return 0; + double total = 0; + foreach (var p in points) + total += Math.Abs(Distance(p, cx, cy) - r); + return total / points.Length; + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs new file mode 100644 index 0000000..2bee916 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs @@ -0,0 +1,497 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using Emgu.CV; +using Emgu.CV.Structure; +using Prism.Commands; +using Prism.Mvvm; +using XP.ImageProcessing.Processors; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using Brushes = System.Windows.Media.Brushes; +using Ellipse = System.Windows.Shapes.Ellipse; +using Point = System.Windows.Point; + +namespace XplorePlane.ViewModels.ImageProcessing +{ + /// + /// 边缘查找拟合圆 ViewModel + /// 交互:3点定义预估圆,手柄可调整圆心和半径,点击拟合执行 + /// + public class EdgeCircleFitViewModel : BindableBase + { + private readonly IMainViewportService _viewportService; + private PolygonRoiCanvas _canvas; + private Canvas _mainCanvas; + + // 预估圆 + private Point _center; + private double _radius; + private bool _circleDefined; + + // 可视化 + private readonly List _tempOverlays = new(); + private readonly List _committedOverlays = new(); + + // 手柄位置 + private Point _handleCenterPos; + private Point _handleRadiusPos; // 圆周上0°位置 + + // 交互 + private enum DragTarget { None, Center, Radius } + private DragTarget _dragging = DragTarget.None; + private bool _isDrawing; + private int _fitCount; + + private const double HandleSize = 12; + private const double HitRadius = 10; + private static readonly SolidColorBrush CaliperStroke; + private static readonly SolidColorBrush CaliperFill; + private static readonly SolidColorBrush FitCircleBrush; + private static readonly SolidColorBrush HandleFill; + + static EdgeCircleFitViewModel() + { + CaliperStroke = new SolidColorBrush(Color.FromRgb(0, 255, 0)); + CaliperStroke.Freeze(); + CaliperFill = new SolidColorBrush(Color.FromArgb(15, 0, 255, 0)); + CaliperFill.Freeze(); + FitCircleBrush = new SolidColorBrush(Color.FromRgb(30, 144, 255)); + FitCircleBrush.Freeze(); + HandleFill = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255)); + HandleFill.Freeze(); + } + + public EdgeCircleFitViewModel(IMainViewportService viewportService) + { + _viewportService = viewportService; + FitCommand = new DelegateCommand(ExecuteFit, () => _circleDefined); + ClearAllCommand = new DelegateCommand(ExecuteClearAll); + DrawCircleCommand = new DelegateCommand(ExecuteDrawCircle); + } + + // ── 命令 ── + public DelegateCommand FitCommand { get; } + public DelegateCommand ClearAllCommand { get; } + public DelegateCommand DrawCircleCommand { get; } + + // ── 参数 ── + private int _caliperCount = 36; + public int CaliperCount { get => _caliperCount; set { if (SetProperty(ref _caliperCount, value)) RedrawTemp(); } } + + private int _caliperWidth = 40; + public int CaliperWidth { get => _caliperWidth; set { if (SetProperty(ref _caliperWidth, value)) RedrawTemp(); } } + + private string _edgePolarity = "Both"; + public string EdgePolarity { get => _edgePolarity; set { if (SetProperty(ref _edgePolarity, value)) RedrawTemp(); } } + + private int _edgeThreshold = 20; + public int EdgeThreshold { get => _edgeThreshold; set => SetProperty(ref _edgeThreshold, value); } + + private double _sigma = 1.0; + public double Sigma { get => _sigma; set => SetProperty(ref _sigma, value); } + + private string _searchDirection = "Both"; + public string SearchDirection { get => _searchDirection; set => SetProperty(ref _searchDirection, value); } + + private string _fitMethod = "RANSAC"; + public string FitMethod { get => _fitMethod; set => SetProperty(ref _fitMethod, value); } + + private double _ransacThreshold = 2.0; + public double RansacThreshold { get => _ransacThreshold; set => SetProperty(ref _ransacThreshold, value); } + + private string _resultText = "Ready - click Draw Circle"; + public string ResultText { get => _resultText; set => SetProperty(ref _resultText, value); } + + // ── 初始化 ── + public void SetCanvas(PolygonRoiCanvas canvas) + { + _canvas = canvas; + _mainCanvas = FindChild(canvas, "mainCanvas"); + } + + public void OnPanelClosed() + { + UnregisterAll(); + ClearTempOverlays(); + } + + // ══════════════════════════════════════════════════════════════ + // 命令 + // ══════════════════════════════════════════════════════════════ + + private void ExecuteDrawCircle() + { + ClearTempOverlays(); + UnregisterAll(); + _circleDefined = false; + _dragging = DragTarget.None; + FitCommand.RaiseCanExecuteChanged(); + _isDrawing = true; + ResultText = "Press and drag to define circle (center → radius)"; + RegisterInteraction(); + } + + private void ExecuteFit() + { + if (!_circleDefined) return; + + // 清除上一次拟合结果 + ClearCommitted(); + + var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource; + if (imageSource == null) { ResultText = "Error: no image"; return; } + + try + { + BitmapSource source = imageSource; + if (imageSource.Format != PixelFormats.Gray8) + source = new FormatConvertedBitmap(imageSource, PixelFormats.Gray8, null, 0); + + int w = source.PixelWidth, h = source.PixelHeight; + int stride = w; + byte[] px = new byte[h * stride]; + source.CopyPixels(px, stride, 0); + + using var img = new Image(w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + img.Data[y, x, 0] = px[y * stride + x]; + + var proc = new EdgeCircleFitProcessor(); + proc.SetParameter("CenterX", (int)_center.X); + proc.SetParameter("CenterY", (int)_center.Y); + proc.SetParameter("Radius", (int)_radius); + proc.SetParameter("CaliperCount", CaliperCount); + proc.SetParameter("CaliperWidth", CaliperWidth); + proc.SetParameter("EdgePolarity", EdgePolarity); + proc.SetParameter("EdgeThreshold", EdgeThreshold); + proc.SetParameter("Sigma", Sigma); + proc.SetParameter("SearchDirection", SearchDirection); + proc.SetParameter("FitMethod", FitMethod); + proc.SetParameter("RansacThreshold", RansacThreshold); + + var result = proc.Process(img); + var od = proc.OutputData; + + if (od.ContainsKey("CircleFitResult")) + { + var fr = od["CircleFitResult"] as CircleFitResult; + if (fr != null && fr.Success) + { + _fitCount++; + DrawFitResult(fr); + ResultText = $"[#{_fitCount}] Fit OK\nCenter: ({fr.CenterX:F1}, {fr.CenterY:F1})\n" + + $"Radius: {fr.Radius:F2} px\n" + + $"Inliers: {fr.Inliers.Count}/{fr.EdgePointCount}\n" + + $"Error: {fr.FitError:F3} px\n\nAdjust and fit again, or draw new"; + } + else + { + int ec = od.ContainsKey("EdgePointCount") ? (int)od["EdgePointCount"] : 0; + ResultText = $"Fit failed\nEdge points: {ec}\nAdjust params or circle"; + } + } + result.Dispose(); + } + catch (Exception ex) { ResultText = $"Exception: {ex.Message}"; } + } + + private void ExecuteClearAll() + { + ClearTempOverlays(); + if (_mainCanvas != null) + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + _fitCount = 0; + UnregisterAll(); + _circleDefined = false; + FitCommand.RaiseCanExecuteChanged(); + ResultText = "Cleared"; + } + + // ══════════════════════════════════════════════════════════════ + // 拟合结果绘制(永久) + // ══════════════════════════════════════════════════════════════ + + private void DrawFitResult(CircleFitResult fr) + { + if (_mainCanvas == null) return; + + // 拟合圆(蓝色) + var circle = new Ellipse + { + Width = fr.Radius * 2, Height = fr.Radius * 2, + Stroke = FitCircleBrush, StrokeThickness = 2, Fill = Brushes.Transparent, + IsHitTestVisible = false + }; + Canvas.SetLeft(circle, fr.CenterX - fr.Radius); + Canvas.SetTop(circle, fr.CenterY - fr.Radius); + AddCommitted(circle); + + // 圆心十字 + double cs = 6; + AddCommitted(new Line { X1 = fr.CenterX - cs, Y1 = fr.CenterY, X2 = fr.CenterX + cs, Y2 = fr.CenterY, Stroke = FitCircleBrush, StrokeThickness = 1.5, IsHitTestVisible = false }); + AddCommitted(new Line { X1 = fr.CenterX, Y1 = fr.CenterY - cs, X2 = fr.CenterX, Y2 = fr.CenterY + cs, Stroke = FitCircleBrush, StrokeThickness = 1.5, IsHitTestVisible = false }); + + // 标注 + var lbl = new TextBlock + { + Text = $"R:{fr.Radius:F1} C:({fr.CenterX:F1},{fr.CenterY:F1})", + Foreground = FitCircleBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false + }; + Canvas.SetLeft(lbl, fr.CenterX + 5); Canvas.SetTop(lbl, fr.CenterY - fr.Radius - 18); + AddCommitted(lbl); + } + + private void AddCommitted(UIElement el) { _mainCanvas.Children.Add(el); _committedOverlays.Add(el); } + + private void ClearCommitted() + { + if (_mainCanvas == null) return; + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + } + + // ══════════════════════════════════════════════════════════════ + // 临时卡尺可视化 + // ══════════════════════════════════════════════════════════════ + + private void RedrawTemp() + { + if (!_circleDefined || _mainCanvas == null) return; + ClearTempOverlays(); + DrawTempCaliper(); + } + + private void DrawTempCaliper() + { + if (_mainCanvas == null || _radius < 5) return; + + // 预估圆(虚线) + var previewCircle = new Ellipse + { + Width = _radius * 2, Height = _radius * 2, + Stroke = CaliperStroke, StrokeThickness = 1, + StrokeDashArray = new DoubleCollection { 4, 3 }, + Fill = CaliperFill, IsHitTestVisible = false + }; + Canvas.SetLeft(previewCircle, _center.X - _radius); + Canvas.SetTop(previewCircle, _center.Y - _radius); + AddTemp(previewCircle); + + // 卡尺径向线 + int count = CaliperCount; + double halfW = CaliperWidth / 2.0; + double angleStep = 2.0 * Math.PI / count; + + for (int i = 0; i < count; i++) + { + double angle = angleStep * i; + double dirX = Math.Cos(angle), dirY = Math.Sin(angle); + double cx = _center.X + _radius * dirX; + double cy = _center.Y + _radius * dirY; + + AddTemp(new Line + { + X1 = cx - dirX * halfW, Y1 = cy - dirY * halfW, + X2 = cx + dirX * halfW, Y2 = cy + dirY * halfW, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, IsHitTestVisible = false + }); + } + + // 手柄 + _handleCenterPos = _center; + _handleRadiusPos = new Point(_center.X + _radius, _center.Y); + + AddTemp(MakeHandle(_handleCenterPos)); + AddTemp(MakeHandle(_handleRadiusPos)); + } + + private void CommitCurrentCaliper() + { + if (_mainCanvas == null) return; + foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + + // 绘制永久卡尺(半透明) + var circle = new Ellipse + { + Width = _radius * 2, Height = _radius * 2, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, + Fill = Brushes.Transparent, IsHitTestVisible = false + }; + Canvas.SetLeft(circle, _center.X - _radius); + Canvas.SetTop(circle, _center.Y - _radius); + AddCommitted(circle); + } + + private Ellipse MakeHandle(Point pos) + { + var h = new Ellipse + { + Width = HandleSize, Height = HandleSize, + Fill = HandleFill, Stroke = CaliperStroke, StrokeThickness = 2, + IsHitTestVisible = false + }; + Canvas.SetLeft(h, pos.X - HandleSize / 2); + Canvas.SetTop(h, pos.Y - HandleSize / 2); + return h; + } + + private void AddTemp(UIElement el) { _mainCanvas.Children.Add(el); _tempOverlays.Add(el); } + private void ClearTempOverlays() + { + if (_mainCanvas == null) return; + foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + } + + // ══════════════════════════════════════════════════════════════ + // 鼠标交互 + // ══════════════════════════════════════════════════════════════ + + private bool _interactionRegistered; + + private void RegisterInteraction() + { + if (_canvas == null || _interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown += OnMouseDown; + _canvas.PreviewMouseMove += OnMouseMove; + _canvas.PreviewMouseLeftButtonUp += OnMouseUp; + _interactionRegistered = true; + } + + private void UnregisterAll() + { + if (_canvas == null || !_interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown -= OnMouseDown; + _canvas.PreviewMouseMove -= OnMouseMove; + _canvas.PreviewMouseLeftButtonUp -= OnMouseUp; + _interactionRegistered = false; + _isDrawing = false; + _dragging = DragTarget.None; + } + + private void OnMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + // 绘制模式:按下鼠标确定圆心,拖拽确定半径 + if (_isDrawing) + { + _center = pos; + _radius = 0; + _dragging = DragTarget.Radius; // 复用 Radius 拖拽逻辑 + _canvas.CaptureMouse(); + e.Handled = true; + return; + } + + // 拖拽手柄 + if (_circleDefined) + { + var target = HitTest(pos); + if (target != DragTarget.None) + { + _dragging = target; + _canvas.CaptureMouse(); + e.Handled = true; + } + } + } + + private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (_dragging == DragTarget.None || _mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + if (_dragging == DragTarget.Center) + { + _center = pos; + } + else if (_dragging == DragTarget.Radius) + { + _radius = Math.Max(5, Dist(pos, _center)); + } + + // 实时预览 + if (_radius >= 5) + { + _circleDefined = true; + ClearTempOverlays(); + DrawTempCaliper(); + } + e.Handled = true; + } + + private void OnMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_dragging == DragTarget.None) return; + + // 绘制模式完成 + if (_isDrawing) + { + _isDrawing = false; + _dragging = DragTarget.None; + _canvas.ReleaseMouseCapture(); + + if (_radius >= 5) + { + _circleDefined = true; + FitCommand.RaiseCanExecuteChanged(); + RedrawTemp(); + ResultText = $"Circle defined: R={_radius:F0}px\nDrag handles to adjust\nClick Fit to execute"; + } + else + { + _circleDefined = false; + ResultText = "Circle too small, try again"; + } + e.Handled = true; + return; + } + + _dragging = DragTarget.None; + _canvas.ReleaseMouseCapture(); + ResultText = $"Circle: R={_radius:F0}px\nClick Fit to execute"; + e.Handled = true; + } + + private DragTarget HitTest(Point pos) + { + if (Dist(pos, _handleCenterPos) <= HitRadius) return DragTarget.Center; + if (Dist(pos, _handleRadiusPos) <= HitRadius) return DragTarget.Radius; + return DragTarget.None; + } + + // ══════════════════════════════════════════════════════════════ + // 辅助 + // ══════════════════════════════════════════════════════════════ + + private static double Dist(Point a, Point b) + { + double dx = a.X - b.X, dy = a.Y - b.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static T FindChild(DependencyObject parent, string name) where T : FrameworkElement + { + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T t && t.Name == name) return t; + var r = FindChild(child, name); + if (r != null) return r; + } + return null; + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs index b540dc4..f7fcd8b 100644 --- a/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs @@ -168,6 +168,9 @@ namespace XplorePlane.ViewModels.ImageProcessing { if (!_lineDefined) return; + // 清除上一次拟合结果 + ClearCommitted(); + var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource; if (imageSource == null) { ResultText = "错误:无可用图像"; return; } @@ -209,17 +212,10 @@ namespace XplorePlane.ViewModels.ImageProcessing if (fr != null && fr.Success) { _fitCount++; - // 将当前卡尺从临时转为永久 - CommitCurrentCaliper(); - // 绘制拟合结果(永久) - DrawFitResult(fr, _fitCount); + DrawFitResult(fr); ResultText = $"[#{_fitCount}] 拟合成功\n角度: {fr.AngleDegrees:F2}°\n" + $"内点: {fr.Inliers.Count}/{fr.EdgePointCount}\n" + - $"误差: {fr.FitError:F3} px\n\n点击「画卡尺」继续下一条"; - // 拟合完成后清除编辑状态,准备下一次 - _lineDefined = false; - FitCommand.RaiseCanExecuteChanged(); - UnregisterAll(); + $"误差: {fr.FitError:F3} px\n\n可继续调整后再次拟合"; } else { @@ -310,7 +306,7 @@ namespace XplorePlane.ViewModels.ImageProcessing // 绘制拟合结果(永久) // ══════════════════════════════════════════════════════════════ - private void DrawFitResult(LineFitResult fr, int index) + private void DrawFitResult(LineFitResult fr) { if (_mainCanvas == null) return; @@ -322,17 +318,10 @@ namespace XplorePlane.ViewModels.ImageProcessing Stroke = FitLineBrush, StrokeThickness = 2, IsHitTestVisible = false }); - // 内点 - foreach (var pt in fr.Inliers) - AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Lime)); - // 外点 - foreach (var pt in fr.Outliers) - AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Red)); - // 标注 var lbl = new TextBlock { - Text = $"#{index} ∠{fr.AngleDegrees:F2}° Err:{fr.FitError:F2}px", + Text = $"∠{fr.AngleDegrees:F2}°", Foreground = FitLineBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; Canvas.SetLeft(lbl, (fr.Endpoint1.X + fr.Endpoint2.X) / 2 + 5); @@ -340,19 +329,19 @@ namespace XplorePlane.ViewModels.ImageProcessing AddCommitted(lbl); } - private Ellipse MakeDot(double x, double y, SolidColorBrush fill) - { - var d = new Ellipse { Width = 5, Height = 5, Fill = fill, IsHitTestVisible = false }; - Canvas.SetLeft(d, x - 2.5); Canvas.SetTop(d, y - 2.5); - return d; - } - private void AddCommitted(UIElement el) { _mainCanvas.Children.Add(el); _committedOverlays.Add(el); } + private void ClearCommitted() + { + if (_mainCanvas == null) return; + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + } + // ══════════════════════════════════════════════════════════════ // 临时卡尺可视化(编辑中,带手柄) // ══════════════════════════════════════════════════════════════ diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index a62cb20..60f3c59 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -144,6 +144,7 @@ namespace XplorePlane.ViewModels public DelegateCommand SharpenCommand { get; } public DelegateCommand EnhanceCommand { get; } public DelegateCommand EdgeLineFitCommand { get; } + public DelegateCommand EdgeCircleFitCommand { get; } // 设置命令 public DelegateCommand OpenLanguageSwitcherCommand { get; } @@ -352,6 +353,7 @@ namespace XplorePlane.ViewModels SharpenCommand = new DelegateCommand(ExecuteSharpen); EnhanceCommand = new DelegateCommand(ExecuteEnhance); EdgeLineFitCommand = new DelegateCommand(ExecuteEdgeLineFit); + EdgeCircleFitCommand = new DelegateCommand(ExecuteEdgeCircleFit); AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDoorCommand = new DelegateCommand(ExecuteOpenDoor); @@ -891,6 +893,7 @@ namespace XplorePlane.ViewModels private Window _bgaDetectionPanel; private Window _edgeLineFitPanel; + private Window _edgeCircleFitPanel; private void ExecuteBgaDetection() { @@ -1194,6 +1197,25 @@ namespace XplorePlane.ViewModels _edgeLineFitPanel.Show(); } + private void ExecuteEdgeCircleFit() + { + if (!CheckImageLoaded()) return; + _logger.Info("边缘查找拟合圆功能已触发"); + + if (_edgeCircleFitPanel != null && _edgeCircleFitPanel.IsVisible) + { + _edgeCircleFitPanel.Activate(); + return; + } + + _edgeCircleFitPanel = new Views.ImageProcessing.EdgeCircleFitPanel + { + Owner = System.Windows.Application.Current.MainWindow + }; + _edgeCircleFitPanel.Closed += (_, _) => { _edgeCircleFitPanel = null; }; + _edgeCircleFitPanel.Show(); + } + private Image? BitmapSourceToImage(BitmapSource bitmapSource) { // 转换为可用的图像格式 diff --git a/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml new file mode 100644 index 0000000..c13d41d --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Both + BrightToDark + DarkToBright + + + + Both + Inward + Outward + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs new file mode 100644 index 0000000..5f2bb09 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using Prism.Ioc; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels.ImageProcessing; + +namespace XplorePlane.Views.ImageProcessing +{ + public partial class EdgeCircleFitPanel : Window + { + public EdgeCircleFitPanel() + { + InitializeComponent(); + + var viewportService = ContainerLocator.Current?.Resolve(); + DataContext = new EdgeCircleFitViewModel(viewportService); + + Loaded += (s, e) => + { + var mainWin = Owner as MainWindow; + if (mainWin != null) + { + var canvas = FindChild(mainWin); + if (DataContext is EdgeCircleFitViewModel vm) + { + vm.SetCanvas(canvas); + vm.DrawCircleCommand.Execute(); + } + } + }; + + Closed += (s, e) => + { + if (DataContext is EdgeCircleFitViewModel vm) + vm.OnPanelClosed(); + }; + } + + private static T FindChild(DependencyObject parent) where T : DependencyObject + { + int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i); + if (child is T t) return t; + var result = FindChild(child); + if (result != null) return result; + } + return null; + } + } +} diff --git a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml index f3868a1..e530710 100644 --- a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml +++ b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml @@ -101,11 +101,6 @@ - - - RANSAC - LeastSquares - diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 49122a0..bfa1636 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -537,6 +537,7 @@