From b9106acdf0187a93ab13edbe738f163536076158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Wed, 13 May 2026 09:04:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E5=B7=A5=E5=85=B7=E5=A4=84?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=AF=94=E4=BE=8B=E5=B0=BA=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../定位识别/TemplateMatchingProcessor.cs | 171 +++++++++++++++++- .../Controls/PolygonRoiCanvas.xaml | 90 +++++++++ .../Controls/PolygonRoiCanvas.xaml.cs | 121 ++++++++++++- XplorePlane/Assets/Icons/Scale.png | Bin 0 -> 717 bytes XplorePlane/ViewModels/Main/MainViewModel.cs | 7 + XplorePlane/Views/Main/ImagePanelView.xaml | 5 +- XplorePlane/Views/Main/MainWindow.xaml | 7 + XplorePlane/Views/Main/ViewportPanelView.xaml | 6 +- 8 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 XplorePlane/Assets/Icons/Scale.png diff --git a/XP.ImageProcessing.Processors/定位识别/TemplateMatchingProcessor.cs b/XP.ImageProcessing.Processors/定位识别/TemplateMatchingProcessor.cs index b6db3df..a794c81 100644 --- a/XP.ImageProcessing.Processors/定位识别/TemplateMatchingProcessor.cs +++ b/XP.ImageProcessing.Processors/定位识别/TemplateMatchingProcessor.cs @@ -22,18 +22,44 @@ namespace XP.ImageProcessing.Processors; /// /// 模板匹配算子(定位识别) /// +/// +/// 算法原理: +/// 模板匹配是一种基于图像块的匹配方法,通过在待搜索图像上滑动模板, +/// 计算每个位置与模板的相似度,找到最佳匹配位置。 +/// +/// 匹配方法说明: +/// 1. CcoeffNormed (归一化相关系数) - 推荐使用,对光照变化有一定的鲁棒性 +/// 公式: corr = Σ(I(x,y) * T(x,y)) / sqrt(ΣI² * ΣT²) +/// 分数范围: -1 ~ 1,值越大越相似 +/// +/// 2. SqdiffNormed (归一化平方差) - 值越小越相似 +/// 公式: diff = Σ(I(x,y) - T(x,y))² / (ΣI² + ΣT²) +/// 分数范围: 0 ~ 1,值越小越相似 +/// +/// 3. CcorrNormed (归一化相关) - 对模板和图像的亮度变化敏感 +/// 4. Ccoeff (相关系数) - 未归一化版本 +/// 5. Ccorr (相关) - 未归一化版本 +/// 6. Sqdiff (平方差) - 未归一化版本 +/// +/// 性能说明: +/// - 时间复杂度: O(W * H * w * h),其中W/H为图像尺寸,w/h为模板尺寸 +/// - 可通过设置SearchRegion限制搜索范围来提升性能 +/// public class TemplateMatchingProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); + /// + /// 匹配方法选项列表,供UI下拉选择 + /// private static readonly string[] MatchMethodOptions = { - "CcoeffNormed", - "SqdiffNormed", - "CcorrNormed", - "Ccoeff", - "Ccorr", - "Sqdiff" + "CcoeffNormed", // 归一化相关系数(推荐) + "SqdiffNormed", // 归一化平方差 + "CcorrNormed", // 归一化相关 + "Ccoeff", // 相关系数 + "Ccorr", // 相关 + "Sqdiff" // 平方差 }; public TemplateMatchingProcessor() @@ -42,8 +68,16 @@ public class TemplateMatchingProcessor : ImageProcessorBase Description = LocalizationHelper.GetString("TemplateMatchingProcessor_Description"); } + /// + /// 初始化参数定义 + /// protected override void InitializeParameters() { + // ===== 模板相关参数 ===== + + /// + /// 模板图片路径,支持灰度或彩色图片(自动转灰度) + /// Parameters.Add("TemplatePath", new ProcessorParameter( "TemplatePath", LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath"), @@ -53,25 +87,36 @@ public class TemplateMatchingProcessor : ImageProcessorBase null, LocalizationHelper.GetString("TemplateMatchingProcessor_TemplatePath_Desc"))); + /// + /// 匹配算法选择,不同算法对光照和旋转的敏感度不同 + /// Parameters.Add("MatchMethod", new ProcessorParameter( "MatchMethod", LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod"), typeof(string), - "CcoeffNormed", + "CcoeffNormed", // 默认使用归一化相关系数 null, null, LocalizationHelper.GetString("TemplateMatchingProcessor_MatchMethod_Desc"), MatchMethodOptions)); + /// + /// 匹配阈值,判断匹配是否成功的分数门限 + /// - CcoeffNormed/CcorrNormed: 建议 0.75-0.95 + /// - SqdiffNormed: 建议 0.1-0.3 + /// Parameters.Add("MatchThreshold", new ProcessorParameter( "MatchThreshold", LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold"), typeof(double), - 0.75, + 0.75, // 默认阈值 0.0, 1.0, LocalizationHelper.GetString("TemplateMatchingProcessor_MatchThreshold_Desc"))); + /// + /// 是否在输出图像上绘制匹配矩形框 + /// Parameters.Add("DrawRectangle", new ProcessorParameter( "DrawRectangle", LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch"), @@ -81,6 +126,9 @@ public class TemplateMatchingProcessor : ImageProcessorBase null, LocalizationHelper.GetString("TemplateMatchingProcessor_DrawMatch_Desc"))); + /// + /// 匹配矩形框的线条粗细 + /// Parameters.Add("RectangleThickness", new ProcessorParameter( "RectangleThickness", LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness"), @@ -90,6 +138,11 @@ public class TemplateMatchingProcessor : ImageProcessorBase 8, LocalizationHelper.GetString("TemplateMatchingProcessor_RectThickness_Desc"))); + // ===== 搜索区域参数(可选,用于限制搜索范围提升性能)===== + + /// + /// 搜索区域左上角X坐标 + /// Parameters.Add("SearchRegionX", new ProcessorParameter( "SearchRegionX", LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionX"), @@ -99,6 +152,9 @@ public class TemplateMatchingProcessor : ImageProcessorBase null, LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc"))); + /// + /// 搜索区域左上角Y坐标 + /// Parameters.Add("SearchRegionY", new ProcessorParameter( "SearchRegionY", LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionY"), @@ -108,6 +164,9 @@ public class TemplateMatchingProcessor : ImageProcessorBase null, LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc"))); + /// + /// 搜索区域宽度,0表示使用整幅图像 + /// Parameters.Add("SearchRegionWidth", new ProcessorParameter( "SearchRegionWidth", LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionWidth"), @@ -117,6 +176,9 @@ public class TemplateMatchingProcessor : ImageProcessorBase null, LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegion_Desc"))); + /// + /// 搜索区域高度,0表示使用整幅图像 + /// Parameters.Add("SearchRegionHeight", new ProcessorParameter( "SearchRegionHeight", LocalizationHelper.GetString("TemplateMatchingProcessor_SearchRegionHeight"), @@ -129,8 +191,14 @@ public class TemplateMatchingProcessor : ImageProcessorBase _logger.Debug("InitializeParameters"); } + /// + /// 执行模板匹配处理 + /// + /// 输入的灰度图像 + /// 处理后的图像(可选带匹配矩形框) public override Image Process(Image inputImage) { + // ===== 1. 获取参数 ===== var path = (GetParameter("TemplatePath") ?? string.Empty).Trim(); var methodName = GetParameter("MatchMethod") ?? "CcoeffNormed"; var threshold = GetParameter("MatchThreshold"); @@ -141,10 +209,13 @@ public class TemplateMatchingProcessor : ImageProcessorBase var searchRw = GetParameter("SearchRegionWidth"); var searchRh = GetParameter("SearchRegionHeight"); + // 清除上一次的输出数据 OutputData.Clear(); + // 克隆输入图像用于输出(避免修改原图) var output = inputImage.Clone(); + // ===== 2. 参数校验:模板文件 ===== if (string.IsNullOrEmpty(path) || !File.Exists(path)) { _logger.Warning("TemplateMatching: invalid or missing template file: {Path}", path); @@ -153,6 +224,7 @@ public class TemplateMatchingProcessor : ImageProcessorBase return output; } + // ===== 3. 加载模板图像 ===== using var template = LoadTemplate(path); if (template == null) { @@ -161,17 +233,24 @@ public class TemplateMatchingProcessor : ImageProcessorBase return output; } + // ===== 4. 确定搜索区域(ROI)===== + // 如果未指定搜索区域,则使用整幅图像 var searchRoi = ResolveSearchRoi(inputImage.Width, inputImage.Height, searchRx, searchRy, searchRw, searchRh); - var offsetX = searchRoi.X; + var offsetX = searchRoi.X; // 记录ROI偏移,用于还原到全局坐标 var offsetY = searchRoi.Y; + // ===== 5. 在ROI区域内执行模板匹配 ===== Image? roiImage = null; try { + // 设置输入图像的ROI(感兴趣区域) inputImage.ROI = searchRoi; + // 复制ROI区域到新图像 roiImage = inputImage.Copy(); + // 清除ROI设置 inputImage.ROI = Rectangle.Empty; + // ===== 5.1 校验模板尺寸 ===== if (template.Width > roiImage.Width || template.Height > roiImage.Height) { _logger.Warning("TemplateMatching: template larger than search region ({Tw}x{Th} vs {Iw}x{Ih})", @@ -181,20 +260,34 @@ public class TemplateMatchingProcessor : ImageProcessorBase return output; } + // ===== 5.2 执行模板匹配 ===== + // OpenCV MatchTemplate 在ROI上滑动模板,计算每个位置的相似度 + // 结果是一个 (W-w+1) x (H-h+1) 的分数矩阵 var method = ParseMethod(methodName); using var resultMat = new Mat(); CvInvoke.MatchTemplate(roiImage, template, resultMat, method); + + // ===== 5.3 找到最佳匹配位置 ===== + // MinMaxLoc 在分数矩阵中找到最小值和最大值的位置 double minVal = 0, maxVal = 0; Point minLoc = default, maxLoc = default; CvInvoke.MinMaxLoc(resultMat, ref minVal, ref maxVal, ref minLoc, ref maxLoc); + // 根据匹配方法选择使用最小值还是最大值 + // 平方差类方法:值越小越好(使用minLoc) + // 相关类方法:值越大越好(使用maxLoc) var useMin = method == TemplateMatchingType.Sqdiff || method == TemplateMatchingType.SqdiffNormed; var loc = useMin ? minLoc : maxLoc; var score = useMin ? minVal : maxVal; + + // ===== 5.4 判定匹配是否成功 ===== var matched = IsMatchAcceptable(method, minVal, maxVal, threshold); + // ===== 5.5 转换到全局坐标 ===== + // 由于是在ROI内匹配的,需要加上ROI的偏移量 var globalLoc = new Point(loc.X + offsetX, loc.Y + offsetY); + // ===== 5.6 输出结果数据 ===== OutputData["Matched"] = matched; OutputData["MatchScore"] = score; OutputData["MatchX"] = globalLoc.X; @@ -203,6 +296,7 @@ public class TemplateMatchingProcessor : ImageProcessorBase OutputData["TemplateHeight"] = template.Height; OutputData["MatchMethod"] = methodName; + // ===== 5.7 可选:绘制匹配矩形 ===== if (matched && draw) { var rect = new Rectangle(globalLoc.X, globalLoc.Y, template.Width, template.Height); @@ -216,34 +310,71 @@ public class TemplateMatchingProcessor : ImageProcessorBase } finally { + // 释放ROI图像资源 roiImage?.Dispose(); } } + /// + /// 计算实际的搜索ROI区域 + /// + /// 图像宽度 + /// 图像高度 + /// 用户指定的ROI X坐标 + /// 用户指定的ROI Y坐标 + /// 用户指定的ROI宽度,0表示整幅图像 + /// 用户指定的ROI高度,0表示整幅图像 + /// 计算后的有效ROI区域 private static Rectangle ResolveSearchRoi(int imgW, int imgH, int rx, int ry, int rw, int rh) { + // 宽度或高度为0时,使用整幅图像作为搜索区域 if (rw <= 0 || rh <= 0) return new Rectangle(0, 0, imgW, imgH); + // 限制坐标在图像范围内 rx = Math.Clamp(rx, 0, Math.Max(0, imgW - 1)); ry = Math.Clamp(ry, 0, Math.Max(0, imgH - 1)); + + // 限制宽度和高度不超出图像边界 rw = Math.Clamp(rw, 1, Math.Max(1, imgW - rx)); rh = Math.Clamp(rh, 1, Math.Max(1, imgH - ry)); + return new Rectangle(rx, ry, rw, rh); } + /// + /// 判断匹配结果是否满足阈值条件 + /// + /// 匹配方法类型 + /// 最小相似度分数 + /// 最大相似度分数 + /// 用户设定的阈值 + /// 是否满足匹配条件 + /// + /// 不同匹配方法的分数含义不同: + /// - 平方差类(Sqdiff/SqdiffNormed): 分数越小越相似,需要 minVal <= threshold + /// - 相关类(Ccoeff/Ccorr/CcoeffNormed/CcorrNormed): 分数越大越相似,需要 maxVal >= threshold + /// private static bool IsMatchAcceptable(TemplateMatchingType method, double minVal, double maxVal, double threshold) { return method switch { + // 平方差类:值越小越好 TemplateMatchingType.SqdiffNormed => minVal <= threshold, TemplateMatchingType.Sqdiff => minVal <= threshold, + // 相关类:值越大越好 TemplateMatchingType.CcorrNormed or TemplateMatchingType.CcoeffNormed => maxVal >= threshold, TemplateMatchingType.Ccorr or TemplateMatchingType.Ccoeff => maxVal >= threshold, + // 默认按相关类处理 _ => maxVal >= threshold }; } + /// + /// 将字符串方法名转换为OpenCV枚举类型 + /// + /// 方法名称字符串 + /// 对应的TemplateMatchingType枚举值 private static TemplateMatchingType ParseMethod(string? name) { return name?.Trim() switch @@ -253,23 +384,45 @@ public class TemplateMatchingProcessor : ImageProcessorBase "Ccorr" => TemplateMatchingType.Ccorr, "CcorrNormed" => TemplateMatchingType.CcorrNormed, "Ccoeff" => TemplateMatchingType.Ccoeff, + // 默认返回归一化相关系数(推荐) _ => TemplateMatchingType.CcoeffNormed }; } + /// + /// 从磁盘加载模板图像并转换为灰度图 + /// + /// 模板图片文件路径 + /// 灰度模板图像,加载失败返回null + /// + /// 支持的输入格式: + /// - 灰度图像:直接使用 + /// - 彩色图像:自动转换为灰度 + /// - 支持任意位深度的图像 + /// private static Image? LoadTemplate(string path) { try { + // 使用任意格式读取(支持灰度、彩色、16位等) using var raw = CvInvoke.Imread(path, ImreadModes.AnyDepth | ImreadModes.AnyColor); if (raw.IsEmpty) return null; + // 创建灰度图像 var templ = new Image(raw.Width, raw.Height); + + // 根据通道数决定是否需要灰度转换 if (raw.NumberOfChannels == 1) + { + // 已经是灰度图,直接复制 raw.CopyTo(templ.Mat); + } else + { + // 彩色图转灰度 (BGR -> Gray) CvInvoke.CvtColor(raw, templ, ColorConversion.Bgr2Gray); + } return templ; } diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml index bc732a8..af37dcd 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml @@ -14,6 +14,7 @@ + @@ -71,6 +72,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 56e7f05..8b101e1 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -19,6 +19,9 @@ namespace XP.ImageProcessing.RoiControl.Controls private const double ZoomStep = 1.2; private Adorner? currentAdorner; + /// 比例尺内侧竖向刻度线的 X 坐标(像素,相对尺身左端),仅画线不标数字。 + public ObservableCollection ScaleBarMinorTickXs { get; } = new(); + public PolygonRoiCanvas() { InitializeComponent(); @@ -42,6 +45,8 @@ namespace XP.ImageProcessing.RoiControl.Controls } } } + + RefreshScaleBar(); } private void ROIItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -142,6 +147,8 @@ namespace XP.ImageProcessing.RoiControl.Controls // 图像尺寸变化后刷新十字线 if (control.ShowCrosshair) control.AddCrosshair(); + + control.RefreshScaleBar(); } public static readonly DependencyProperty ROIItemsProperty = @@ -204,7 +211,7 @@ namespace XP.ImageProcessing.RoiControl.Controls public static readonly DependencyProperty CanvasWidthProperty = DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(PolygonRoiCanvas), - new PropertyMetadata(800.0)); + new PropertyMetadata(800.0, OnCanvasOrScaleBarParameterChanged)); public double CanvasWidth { @@ -214,7 +221,7 @@ namespace XP.ImageProcessing.RoiControl.Controls public static readonly DependencyProperty CanvasHeightProperty = DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(PolygonRoiCanvas), - new PropertyMetadata(600.0)); + new PropertyMetadata(600.0, OnCanvasOrScaleBarParameterChanged)); public double CanvasHeight { @@ -222,6 +229,116 @@ namespace XP.ImageProcessing.RoiControl.Controls set => SetValue(CanvasHeightProperty, value); } + /// 是否在图像上显示比例尺(叠在图像坐标系内,随缩放与平移移动)。 + public static readonly DependencyProperty ShowScaleBarProperty = + DependencyProperty.Register(nameof(ShowScaleBar), typeof(bool), typeof(PolygonRoiCanvas), + new PropertyMetadata(false, OnCanvasOrScaleBarParameterChanged)); + + public bool ShowScaleBar + { + get => (bool)GetValue(ShowScaleBarProperty); + set => SetValue(ShowScaleBarProperty, value); + } + + /// 单像素对应的物理长度(mm),用于比例尺刻度换算。 + public static readonly DependencyProperty ScaleBarMmPerPixelProperty = + DependencyProperty.Register(nameof(ScaleBarMmPerPixel), typeof(double), typeof(PolygonRoiCanvas), + new PropertyMetadata(0.139, OnCanvasOrScaleBarParameterChanged)); + + public double ScaleBarMmPerPixel + { + get => (double)GetValue(ScaleBarMmPerPixelProperty); + set => SetValue(ScaleBarMmPerPixelProperty, value); + } + + public static readonly DependencyProperty ScaleBarLengthPixelsProperty = + DependencyProperty.Register(nameof(ScaleBarLengthPixels), typeof(double), typeof(PolygonRoiCanvas), + new PropertyMetadata(100.0)); + + public double ScaleBarLengthPixels + { + get => (double)GetValue(ScaleBarLengthPixelsProperty); + private set => SetValue(ScaleBarLengthPixelsProperty, value); + } + + public static readonly DependencyProperty ScaleBarCaptionProperty = + DependencyProperty.Register(nameof(ScaleBarCaption), typeof(string), typeof(PolygonRoiCanvas), + new PropertyMetadata(string.Empty)); + + public string ScaleBarCaption + { + get => (string)GetValue(ScaleBarCaptionProperty); + private set => SetValue(ScaleBarCaptionProperty, value); + } + + private static void OnCanvasOrScaleBarParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((PolygonRoiCanvas)d).RefreshScaleBar(); + } + + /// 按图像宽度选取 1–2–5 标度,使比例尺约占画布宽度的约 22%。 + private void RefreshScaleBar() + { + const double targetFrac = 0.22; + const double maxFrac = 0.88; + const double minPx = 24.0; + + if (!ShowScaleBar || CanvasWidth < 16 || ScaleBarMmPerPixel <= 0 || double.IsNaN(ScaleBarMmPerPixel)) + { + ScaleBarLengthPixels = minPx; + ScaleBarCaption = string.Empty; + ScaleBarMinorTickXs.Clear(); + return; + } + + double maxPx = Math.Max(minPx, CanvasWidth * maxFrac); + double idealPx = Math.Min(CanvasWidth * targetFrac, maxPx); + double idealMm = idealPx * ScaleBarMmPerPixel; + double niceMm = RoundToNiceLengthMm(idealMm); + double barPx = niceMm / ScaleBarMmPerPixel; + + while (barPx > maxPx && niceMm > ScaleBarMmPerPixel * minPx * 1.5) + { + niceMm /= 2.0; + barPx = niceMm / ScaleBarMmPerPixel; + } + + while (barPx < minPx && niceMm < idealMm * 200) + { + niceMm *= 2.0; + barPx = niceMm / ScaleBarMmPerPixel; + } + + if (barPx < minPx) + barPx = minPx; + + ScaleBarLengthPixels = barPx; + ScaleBarCaption = FormatScaleBarCaptionMm(barPx * ScaleBarMmPerPixel); + + ScaleBarMinorTickXs.Clear(); + int divisions = barPx >= 120 ? 10 : barPx >= 60 ? 6 : 4; + for (int i = 1; i < divisions; i++) + ScaleBarMinorTickXs.Add(barPx * i / divisions); + } + + private static double RoundToNiceLengthMm(double mm) + { + if (mm <= 0 || double.IsNaN(mm) || double.IsInfinity(mm)) + return 0.1; + var magnitude = Math.Pow(10.0, Math.Floor(Math.Log10(mm))); + var normalized = mm / magnitude; + var nice = normalized < 1.5 ? 1.0 : normalized < 3.5 ? 2.0 : normalized < 7.5 ? 5.0 : 10.0; + return nice * magnitude; + } + + private static string FormatScaleBarCaptionMm(double mm) + { + if (mm >= 100) return $"{mm:F0} mm"; + if (mm >= 10) return $"{mm:F1} mm"; + if (mm >= 1) return $"{mm:F2} mm"; + return $"{mm:F3} mm"; + } + public static readonly DependencyProperty SelectedROIProperty = DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(PolygonRoiCanvas), new PropertyMetadata(null, OnSelectedROIChanged)); diff --git a/XplorePlane/Assets/Icons/Scale.png b/XplorePlane/Assets/Icons/Scale.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4b3f0fa10c5ef5f3eed42a7aec4639ffa7e35d GIT binary patch literal 717 zcmV;;0y6!HP)utg?6otcUkMtiB3;~;zNeYudCn22ykoZ>y0nzha#gS8-!~s{Wa+~;Z?)m(P!!Y~j zzdr$(PN#Y7xRL|R=kvvAG}`O_5d)DFK&A_s?gxWGe!t(}RqljH1|Tyt7eH~nUf*=j zVjz+LICB|Zj>qHjd_G@px7#uX;$;A5F0(?B&1SPL_gILR0DN;sUXFUv2jXb}XU?=M za?0MQqamIGz)aJ;R}`ny=}o(SCx|We4q`YQeyQCqS4TnpYHvqx08Eq!b9T8MIGX+q z5URHe!^7dQvqJfgI+jTN>=ku~RWAU~yn5BN-rsltp|FieITYjZ*fyvjRshi_8o)D0 zSCD`aQ7GGZYz*~b);KGcK4=tx#0W{i2-j{MfnX2#2}1T%8fW!#*8mS$xj9#1P z0tCVmqS{Q<`5}F$IsJnG5}Ua?6f)(I`Cz?)a2ro;nIM=M9%~_elsTdY0EsJe87yVW zmu;b6GIXP+o5}P^GuE<$m3`>aWWJ4GV>3f-zqXLlF%vD)1&6@fh>+K*k z8^P&Tf4ijbZH~|YFj0X7_NtYm{`4W#-grH0PC^5~P-ZJC{84M_4Pr8xFgr9q#4F|` zGXNg=cmScueF#nchBo^Aj#tgeR{>DKNB}EE1wnl%Qkavs20%p&L@IOq9ssCpgGgzP zR}g@P5JYNod>R30^dajX4P literal 0 HcmV?d00001 diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 90e2dee..f71eeb9 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -122,6 +122,13 @@ namespace XplorePlane.ViewModels public DelegateCommand VoidDetectionCommand { get; } public DelegateCommand BubbleMeasureCommand { get; } + private bool _isScaleBarVisible; + public bool IsScaleBarVisible + { + get => _isScaleBarVisible; + set => SetProperty(ref _isScaleBarVisible, value); + } + // 辅助线命令 public DelegateCommand ToggleCrosshairCommand { get; } diff --git a/XplorePlane/Views/Main/ImagePanelView.xaml b/XplorePlane/Views/Main/ImagePanelView.xaml index 1b49bcf..84f601c 100644 --- a/XplorePlane/Views/Main/ImagePanelView.xaml +++ b/XplorePlane/Views/Main/ImagePanelView.xaml @@ -1,5 +1,6 @@ - + + + diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 7749cb7..c16b92a 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -165,6 +165,13 @@ Size="Medium" SmallImage="/Assets/Icons/crosshair.png" Text="辅助线" /> + - @@ -32,8 +31,9 @@ - + ImageSource="{Binding ImageSource}" + ScaleBarMmPerPixel="0.139" + ShowScaleBar="{Binding DataContext.IsScaleBarVisible, RelativeSource={RelativeSource AncestorType=Window}}" />