feat: 新增边缘查找拟合圆工具 + 优化拟合交互
- 新增 EdgeCircleFitProcessor 算子(卡尺径向边缘检测 + Kasa/RANSAC圆拟合) - 新增 EdgeCircleFitPanel 辅助面板(拖拽画圆交互) - Ribbon快捷工具组新增「圆拟合」按钮 - 拟合后卡尺保持可编辑状态,支持调整后重新拟合 - 每次拟合自动清除上一次结果 - 拟合方法固定RANSAC,UI不暴露选择 - 结果标注简化:直线显示角度,圆显示半径和圆心坐标 - 不再显示内点/外点小圆点 - 添加中英文本地化资源
This commit is contained in:
@@ -1943,4 +1943,66 @@ Reprojection error: {1:F4} pixels</value>
|
||||
<data name="EdgeLineFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>Drawing thickness for result visualization</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeCircleFitProcessor -->
|
||||
<data name="EdgeCircleFitProcessor_Name" xml:space="preserve">
|
||||
<value>Edge Find Circle Fit</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Description" xml:space="preserve">
|
||||
<value>Place calipers along estimated circle to detect edge points and fit a circle (supports Least Squares and RANSAC)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount" xml:space="preserve">
|
||||
<value>Caliper Count</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount_Desc" xml:space="preserve">
|
||||
<value>Number of calipers placed evenly around the circle</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth" xml:space="preserve">
|
||||
<value>Caliper Width</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth_Desc" xml:space="preserve">
|
||||
<value>Search length of each caliper along radial direction (pixels)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity" xml:space="preserve">
|
||||
<value>Edge Polarity</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity_Desc" xml:space="preserve">
|
||||
<value>Edge direction: BrightToDark, DarkToBright, or Both</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold" xml:space="preserve">
|
||||
<value>Edge Threshold</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold_Desc" xml:space="preserve">
|
||||
<value>Gradient strength threshold; edges below this value are ignored</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma" xml:space="preserve">
|
||||
<value>Smoothing Sigma</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma_Desc" xml:space="preserve">
|
||||
<value>Gaussian smoothing standard deviation for noise suppression</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection" xml:space="preserve">
|
||||
<value>Search Direction</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection_Desc" xml:space="preserve">
|
||||
<value>Caliper search direction: Inward (toward center), Outward (away from center), Both</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod" xml:space="preserve">
|
||||
<value>Fit Method</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod_Desc" xml:space="preserve">
|
||||
<value>Circle fitting algorithm: LeastSquares or RANSAC (robust, rejects outliers)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold" xml:space="preserve">
|
||||
<value>RANSAC Threshold</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold_Desc" xml:space="preserve">
|
||||
<value>RANSAC inlier distance threshold (pixels); points closer than this to the circle are inliers</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness" xml:space="preserve">
|
||||
<value>Line Thickness</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>Drawing thickness for result visualization</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1976,4 +1976,66 @@
|
||||
<data name="EdgeLineFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeCircleFitProcessor -->
|
||||
<data name="EdgeCircleFitProcessor_Name" xml:space="preserve">
|
||||
<value>边缘查找拟合圆</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Description" xml:space="preserve">
|
||||
<value>沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount" xml:space="preserve">
|
||||
<value>卡尺数量</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount_Desc" xml:space="preserve">
|
||||
<value>沿圆周等角度放置的卡尺数量</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth" xml:space="preserve">
|
||||
<value>卡尺宽度</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth_Desc" xml:space="preserve">
|
||||
<value>每个卡尺沿径向的搜索长度(像素)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity" xml:space="preserve">
|
||||
<value>边缘极性</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity_Desc" xml:space="preserve">
|
||||
<value>边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold" xml:space="preserve">
|
||||
<value>边缘阈值</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold_Desc" xml:space="preserve">
|
||||
<value>边缘梯度强度阈值,低于此值的边缘将被忽略</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma" xml:space="preserve">
|
||||
<value>平滑Sigma</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma_Desc" xml:space="preserve">
|
||||
<value>高斯平滑的标准差,用于抑制噪声</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection" xml:space="preserve">
|
||||
<value>搜索方向</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection_Desc" xml:space="preserve">
|
||||
<value>卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod" xml:space="preserve">
|
||||
<value>拟合方法</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod_Desc" xml:space="preserve">
|
||||
<value>圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold" xml:space="preserve">
|
||||
<value>RANSAC阈值</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold_Desc" xml:space="preserve">
|
||||
<value>RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness" xml:space="preserve">
|
||||
<value>线条粗细</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1937,4 +1937,66 @@
|
||||
<data name="EdgeLineFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeCircleFitProcessor -->
|
||||
<data name="EdgeCircleFitProcessor_Name" xml:space="preserve">
|
||||
<value>边缘查找拟合圆</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Description" xml:space="preserve">
|
||||
<value>沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount" xml:space="preserve">
|
||||
<value>卡尺数量</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperCount_Desc" xml:space="preserve">
|
||||
<value>沿圆周等角度放置的卡尺数量</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth" xml:space="preserve">
|
||||
<value>卡尺宽度</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_CaliperWidth_Desc" xml:space="preserve">
|
||||
<value>每个卡尺沿径向的搜索长度(像素)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity" xml:space="preserve">
|
||||
<value>边缘极性</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgePolarity_Desc" xml:space="preserve">
|
||||
<value>边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold" xml:space="preserve">
|
||||
<value>边缘阈值</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_EdgeThreshold_Desc" xml:space="preserve">
|
||||
<value>边缘梯度强度阈值,低于此值的边缘将被忽略</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma" xml:space="preserve">
|
||||
<value>平滑Sigma</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Sigma_Desc" xml:space="preserve">
|
||||
<value>高斯平滑的标准差,用于抑制噪声</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection" xml:space="preserve">
|
||||
<value>搜索方向</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_SearchDirection_Desc" xml:space="preserve">
|
||||
<value>卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod" xml:space="preserve">
|
||||
<value>拟合方法</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_FitMethod_Desc" xml:space="preserve">
|
||||
<value>圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合)</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold" xml:space="preserve">
|
||||
<value>RANSAC阈值</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_RansacThreshold_Desc" xml:space="preserve">
|
||||
<value>RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness" xml:space="preserve">
|
||||
<value>线条粗细</value>
|
||||
</data>
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 圆拟合结果
|
||||
/// </summary>
|
||||
public class CircleFitResult
|
||||
{
|
||||
/// <summary>拟合是否成功</summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>拟合圆心X</summary>
|
||||
public double CenterX { get; set; }
|
||||
|
||||
/// <summary>拟合圆心Y</summary>
|
||||
public double CenterY { get; set; }
|
||||
|
||||
/// <summary>拟合半径</summary>
|
||||
public double Radius { get; set; }
|
||||
|
||||
/// <summary>所有检测到的边缘点</summary>
|
||||
public List<EdgePointInfo> EdgePoints { get; set; } = new();
|
||||
|
||||
/// <summary>内点列表</summary>
|
||||
public List<PointF> Inliers { get; set; } = new();
|
||||
|
||||
/// <summary>外点列表</summary>
|
||||
public List<PointF> Outliers { get; set; } = new();
|
||||
|
||||
/// <summary>平均拟合误差(像素)</summary>
|
||||
public double FitError { get; set; }
|
||||
|
||||
/// <summary>有效边缘点数</summary>
|
||||
public int EdgePointCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 边缘查找拟合圆算子 - 沿预估圆周放置卡尺检测边缘点并拟合圆
|
||||
/// </summary>
|
||||
public class EdgeCircleFitProcessor : ImageProcessorBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<EdgeCircleFitProcessor>();
|
||||
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<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
int centerX = GetParameter<int>("CenterX");
|
||||
int centerY = GetParameter<int>("CenterY");
|
||||
int radius = GetParameter<int>("Radius");
|
||||
int caliperCount = GetParameter<int>("CaliperCount");
|
||||
int caliperWidth = GetParameter<int>("CaliperWidth");
|
||||
string edgePolarity = GetParameter<string>("EdgePolarity");
|
||||
int edgeThreshold = GetParameter<int>("EdgeThreshold");
|
||||
double sigma = GetParameter<double>("Sigma");
|
||||
string searchDirection = GetParameter<string>("SearchDirection");
|
||||
string fitMethod = GetParameter<string>("FitMethod");
|
||||
double ransacThreshold = GetParameter<double>("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<EdgePointInfo>();
|
||||
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<int>("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<Gray, byte> 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<EdgePointInfo> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小二乘拟合圆(Kasa方法)
|
||||
/// 将 (x-a)² + (y-b)² = r² 展开为: x² + y² = 2ax + 2by + (r²-a²-b²)
|
||||
/// 令 c = r²-a²-b², 线性方程: 2ax + 2by + c = x² + y²
|
||||
/// </summary>
|
||||
private CircleFitResult FitCircleLeastSquares(List<EdgePointInfo> 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<PointF>(),
|
||||
EdgePointCount = edgePoints.Count,
|
||||
EdgePoints = edgePoints
|
||||
};
|
||||
|
||||
foreach (var ep in edgePoints)
|
||||
ep.IsInlier = true;
|
||||
|
||||
result.FitError = ComputeCircleFitError(points, cx, cy, r);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RANSAC 圆拟合
|
||||
/// </summary>
|
||||
private CircleFitResult FitCircleRANSAC(List<EdgePointInfo> 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<int> 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<int>();
|
||||
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<int>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kasa 最小二乘圆拟合
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 3点拟合圆
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 边缘查找拟合圆 ViewModel
|
||||
/// 交互:3点定义预估圆,手柄可调整圆心和半径,点击拟合执行
|
||||
/// </summary>
|
||||
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<UIElement> _tempOverlays = new();
|
||||
private readonly List<UIElement> _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>(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<Gray, byte>(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<T>(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<T>(child, name);
|
||||
if (r != null) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 临时卡尺可视化(编辑中,带手柄)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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<Gray, byte>? BitmapSourceToImage(BitmapSource bitmapSource)
|
||||
{
|
||||
// 转换为可用的图像格式
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.ImageProcessing.EdgeCircleFitPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="边缘查找拟合圆" Width="300" Height="600"
|
||||
ResizeMode="CanResize" WindowStartupLocation="CenterOwner"
|
||||
Topmost="True" ShowInTaskbar="False"
|
||||
Background="#F5F5F5" FontFamily="Microsoft YaHei UI">
|
||||
<Window.Resources>
|
||||
<Style x:Key="IconBtnStyle" TargetType="ButtonBase">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ButtonBase">
|
||||
<Border x:Name="Bd" Background="#FFFFFF" BorderBrush="#E0E0E0"
|
||||
BorderThickness="1" CornerRadius="6" Padding="8,6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="CardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="BorderBrush" Value="#E8E8E8" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
</Style>
|
||||
<Style x:Key="ParamLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Foreground" Value="#555" />
|
||||
<Setter Property="Margin" Value="0,0,0,3" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="10">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- 工具栏 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<Button Style="{StaticResource IconBtnStyle}" Command="{Binding DrawCircleCommand}" ToolTip="画圆(3点)" Margin="0,0,6,0">
|
||||
<TextBlock Text="⊙ 画圆" FontSize="11" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" Command="{Binding FitCommand}" ToolTip="执行拟合" Margin="0,0,6,0">
|
||||
<TextBlock Text="▶ 拟合" FontSize="11" FontWeight="SemiBold" Foreground="#005FB8" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" Command="{Binding ClearAllCommand}" ToolTip="清除全部">
|
||||
<TextBlock Text="✕ 清除全部" FontSize="11" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 卡尺参数 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="卡尺参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="卡尺数量" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding CaliperCount, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="3" Maximum="180" Value="{Binding CaliperCount}" VerticalAlignment="Center" SmallChange="1" IsSnapToTickEnabled="True" TickFrequency="1" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="卡尺宽度 (px)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding CaliperWidth, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="10" Maximum="200" Value="{Binding CaliperWidth}" VerticalAlignment="Center" SmallChange="1" IsSnapToTickEnabled="True" TickFrequency="1" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 边缘检测参数 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="边缘检测参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="边缘极性" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox SelectedValue="{Binding EdgePolarity}" Margin="0,0,0,6">
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">Both</sys:String>
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">BrightToDark</sys:String>
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">DarkToBright</sys:String>
|
||||
</ComboBox>
|
||||
<TextBlock Text="搜索方向" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox SelectedValue="{Binding SearchDirection}" Margin="0,0,0,6">
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">Both</sys:String>
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">Inward</sys:String>
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">Outward</sys:String>
|
||||
</ComboBox>
|
||||
<TextBlock Text="边缘阈值" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding EdgeThreshold, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="1" Maximum="200" Value="{Binding EdgeThreshold}" VerticalAlignment="Center" SmallChange="1" IsSnapToTickEnabled="True" TickFrequency="1" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="平滑 Sigma" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding Sigma, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0.1" Maximum="5.0" Value="{Binding Sigma}" SmallChange="0.1" LargeChange="0.5" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 拟合参数 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="拟合参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="RANSAC 阈值 (px)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding RansacThreshold, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0.5" Maximum="20" Value="{Binding RansacThreshold}" SmallChange="0.5" LargeChange="2" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 结果 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="结果" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,6" />
|
||||
<TextBlock Text="{Binding ResultText}" FontSize="11.5" Foreground="#333" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -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<IMainViewportService>();
|
||||
DataContext = new EdgeCircleFitViewModel(viewportService);
|
||||
|
||||
Loaded += (s, e) =>
|
||||
{
|
||||
var mainWin = Owner as MainWindow;
|
||||
if (mainWin != null)
|
||||
{
|
||||
var canvas = FindChild<PolygonRoiCanvas>(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<T>(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<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,11 +101,6 @@
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="拟合参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="拟合方法" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox SelectedValue="{Binding FitMethod}" Margin="0,0,0,6">
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">RANSAC</sys:String>
|
||||
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">LeastSquares</sys:String>
|
||||
</ComboBox>
|
||||
<TextBlock Text="RANSAC 阈值 (px)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="50" Text="{Binding RansacThreshold, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
|
||||
@@ -537,6 +537,7 @@
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="沿搜索线放置卡尺检测边缘点并拟合圆"
|
||||
telerik:ScreenTip.Title="拟合圆"
|
||||
Command="{Binding EdgeCircleFitCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/FittedCircle.png"
|
||||
Text="拟合圆" />
|
||||
|
||||
Reference in New Issue
Block a user