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 0000000..6c4b3f0
Binary files /dev/null and b/XplorePlane/Assets/Icons/Scale.png differ
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}}" />