代码链接:https://download.csdn.net/download/ly1h1/92658735
功能介绍

基于 WPF 和 OpenCVSharp 开发的像素距离测量工具,解决图片缩放 / 居中后取点错位问题,核心功能:
- 支持选择本地图片(JPG/PNG/BMP 等格式)
- 分阶段取点:【取第 1 个点】【取第 2 个点】独立控制,支持多次更新取点
- 实时显示取点坐标,独立控件展示第一个点、第二个点坐标及两点像素距离
- 精准计算两点间欧几里得像素距离,测量线实时预览
- 支持取消取点、清空测量结果等操作
环境准备
-
.NET Framework 4.6.1
-
WPF 项目
-
安装 OpenCVSharp NuGet 包(适配.NET 4.6.1):
Install-Package OpenCvSharp4 -Version 4.5.5.20220408 Install-Package OpenCvSharp4.Extensions -Version 4.5.5.20220408 Install-Package OpenCvSharp4.runtime.win -Version 4.5.5.20220408
完整代码实现
1. XAML 布局(MainWindow.xaml)
<Window x:Class="WpfPixelDistanceTool.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="像素距离检测调试工具" Height="900" Width="1300"
WindowStartupLocation="CenterScreen">
<Grid>
<TabControl>
<TabItem Header="点到点像素距离检测调试工具">
<Grid Margin="10">
<!-- 顶部操作区 -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="0,0,0,10">
<Button x:Name="btnSelectDistanceImage" Content="选择测试图片" Width="150" Height="40"
Click="BtnSelectDistanceImage_Click" Margin="0,0,10,0"/>
<Button x:Name="btnGetPoint1" Content="取第1个点" Width="100" Height="40"
Click="BtnGetPoint1_Click" Margin="0,0,10,0" Background="LightBlue" Foreground="Black"/>
<Button x:Name="btnGetPoint2" Content="取第2个点" Width="100" Height="40"
Click="BtnGetPoint2_Click" Margin="0,0,10,0" Background="LightGreen" Foreground="Black"/>
<Button x:Name="btnCancelPick" Content="取消取点" Width="100" Height="40"
Click="BtnCancelPick_Click" Margin="0,0,10,0" Background="Orange" Foreground="Black"/>
<Button x:Name="btnCalculateDistance" Content="计算1和2距离" Width="120" Height="40"
Click="BtnCalculateDistance_Click" Margin="0,0,10,0" Background="LightSeaGreen" Foreground="White"/>
<Button x:Name="btnClearDistanceMeasure" Content="清空测量结果" Width="120" Height="40"
Click="BtnClearDistanceMeasure_Click" Background="LightCoral" Foreground="White"/>
</StackPanel>
<!-- 坐标独立显示区 -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="0,50,0,0">
<!-- 第一个点坐标显示 -->
<StackPanel Orientation="Vertical">
<TextBlock Text="第一个点坐标:" FontSize="14" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBox x:Name="txtPoint1X" Width="80" Height="30" FontSize="14" IsReadOnly="True" Margin="0,0,5,0"/>
<TextBlock Text="X" FontSize="14"/>
<TextBox x:Name="txtPoint1Y" Width="80" Height="30" FontSize="14" IsReadOnly="True" Margin="10,0,5,0"/>
<TextBlock Text="Y" FontSize="14"/>
</StackPanel>
</StackPanel>
<!-- 第二个点坐标显示 -->
<StackPanel Orientation="Vertical">
<TextBlock Text="第二个点坐标:" FontSize="14" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBox x:Name="txtPoint2X" Width="80" Height="30" FontSize="14" IsReadOnly="True" Margin="0,0,5,0"/>
<TextBlock Text="X" FontSize="14"/>
<TextBox x:Name="txtPoint2Y" Width="80" Height="30" FontSize="14" IsReadOnly="True" Margin="10,0,5,0"/>
<TextBlock Text="Y" FontSize="14"/>
</StackPanel>
</StackPanel>
<!-- 两点距离显示 -->
<StackPanel Orientation="Vertical">
<TextBlock Text="两点像素距离:" FontSize="14" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBox x:Name="txtDistanceValue" Width="100" Height="30" FontSize="14" IsReadOnly="True" Margin="0,0,5,0"/>
<TextBlock Text="px" FontSize="14"/>
</StackPanel>
</StackPanel>
</StackPanel>
<!-- 图片显示区 -->
<Canvas x:Name="canvasDistance" Background="White" Margin="0,120,0,200">
<Image x:Name="imgDistanceDisplay" Stretch="Uniform"
MouseDown="ImgDistanceDisplay_MouseDown"
MouseMove="ImgDistanceDisplay_MouseMove"
Width="850" Height="650"/>
<!-- 测量线(红色) -->
<Line x:Name="distanceLine" Stroke="Red" StrokeThickness="2" Visibility="Collapsed"/>
<!-- 测量点标记 -->
<Ellipse x:Name="point1Marker" Width="8" Height="8" Fill="Blue" Visibility="Collapsed"/>
<Ellipse x:Name="point2Marker" Width="8" Height="8" Fill="Blue" Visibility="Collapsed"/>
</Canvas>
<!-- 操作日志显示区 -->
<TextBox x:Name="txtDistanceResult" Width="1200" Height="150"
VerticalAlignment="Bottom" FontSize="12" IsReadOnly="True"
Margin="10" TextWrapping="Wrap"/>
<!-- 操作提示 -->
<TextBlock VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0,0,20,20" FontSize="12"
Text="提示:1.选择图片→2.点击【取第1个点】选点→3.点击【取第2个点】选点→4.点击【计算1和2距离】→5.可随时点击【取消取点】终止选点"/>
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>
-
后台逻辑(MainWindow.xaml.cs)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Win32;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System.Drawing;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Windows.Controls;
using System.Windows.Shapes;namespace WpfPixelDistanceTool
{
public partial class MainWindow : System.Windows.Window
{
// OpenCV图像矩阵
private Mat _distanceMat;
// 取点状态枚举
private enum PickPointState { None, PickingPoint1, PickingPoint2 }
private PickPointState _currentPickState = PickPointState.None;// 存储两个测量点的实际像素坐标 private System.Windows.Point _point1 = new System.Windows.Point(-1, -1); private System.Windows.Point _point2 = new System.Windows.Point(-1, -1); // 记录测量次数 private int _measureCount; public MainWindow() { InitializeComponent(); // 初始化变量 _measureCount = 0; // 禁用计算按钮(初始无点) btnCalculateDistance.IsEnabled = false; // 清空所有独立显示控件 ClearCoordinateControls(); } /// <summary> /// 选择测试图片按钮点击事件 /// </summary> private void BtnSelectDistanceImage_Click(object sender, RoutedEventArgs e) { try { // 清空之前的测量状态 ClearMeasureState(); // 打开文件选择对话框 OpenFileDialog openFileDialog = new OpenFileDialog { Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.tif|所有文件|*.*", Title = "选择测试图片", Multiselect = false }; if (openFileDialog.ShowDialog() == true) { // 读取图片到OpenCV矩阵 _distanceMat = Cv2.ImRead(openFileDialog.FileName, ImreadModes.Color); if (_distanceMat.Empty()) { MessageBox.Show("图片读取失败,请选择有效的图片文件!", "错误", MessageBoxButton.OK, MessageBoxImage.Error); return; } // 将OpenCV Mat转换为WPF的BitmapSource并显示 imgDistanceDisplay.Source = BitmapSourceConverter.ToBitmapSource(_distanceMat); txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 成功加载图片:{openFileDialog.SafeFileName} | 图片尺寸:{_distanceMat.Width}x{_distanceMat.Height}px\r\n"); } } catch (Exception ex) { MessageBox.Show($"加载图片出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } /// <summary> /// 取第1个点按钮点击事件 /// </summary> private void BtnGetPoint1_Click(object sender, RoutedEventArgs e) { if (_distanceMat == null || _distanceMat.Empty()) { MessageBox.Show("请先选择测试图片!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } // 激活取第一个点的状态 _currentPickState = PickPointState.PickingPoint1; txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 已激活【取第1个点】模式,点击图片选取点(可多次点击更新)\r\n"); // 清空当前第一个点的标记(如果有) point1Marker.Visibility = Visibility.Collapsed; // 禁用计算按钮(未选完两个点) btnCalculateDistance.IsEnabled = false; } /// <summary> /// 取第2个点按钮点击事件 /// </summary> private void BtnGetPoint2_Click(object sender, RoutedEventArgs e) { if (_distanceMat == null || _distanceMat.Empty()) { MessageBox.Show("请先选择测试图片!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } // 激活取第二个点的状态 _currentPickState = PickPointState.PickingPoint2; txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 已激活【取第2个点】模式,点击图片选取点(可多次点击更新)\r\n"); // 清空当前第二个点的标记(如果有) point2Marker.Visibility = Visibility.Collapsed; // 禁用计算按钮(未确认两个点) btnCalculateDistance.IsEnabled = false; } /// <summary> /// 取消取点按钮点击事件 /// </summary> private void BtnCancelPick_Click(object sender, RoutedEventArgs e) { if (_currentPickState != PickPointState.None) { txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 已取消当前取点模式\r\n"); // 重置取点状态 _currentPickState = PickPointState.None; // 隐藏实时预览线 distanceLine.Visibility = Visibility.Collapsed; } else { MessageBox.Show("当前未处于取点模式!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); } } /// <summary> /// 计算1和2距离按钮点击事件 /// </summary> private void BtnCalculateDistance_Click(object sender, RoutedEventArgs e) { // 校验两个点是否有效 if (_point1.X < 0 || _point1.Y < 0 || _point2.X < 0 || _point2.Y < 0) { MessageBox.Show("请先选取有效的第1个点和第2个点!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // 计算实际像素距离 double pixelDistance = CalculateEuclideanDistance(_point1, _point2); _measureCount++; // 将距离显示到独立控件 txtDistanceValue.Text = pixelDistance.ToString("F2"); // 日志记录 txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 第{_measureCount}次测量结果:\r\n"); txtDistanceResult.AppendText($" → 第1个点像素坐标:X={_point1.X:F2}, Y={_point1.Y:F2}\r\n"); txtDistanceResult.AppendText($" → 第2个点像素坐标:X={_point2.X:F2}, Y={_point2.Y:F2}\r\n"); txtDistanceResult.AppendText($" → 两点间实际像素距离:{pixelDistance:F2}px\r\n"); // 绘制最终测量线 DrawFinalDistanceLine(); } /// <summary> /// 清空测量结果按钮点击事件 /// </summary> private void BtnClearDistanceMeasure_Click(object sender, RoutedEventArgs e) { ClearMeasureState(); ClearCoordinateControls(); // 清空独立显示控件 txtDistanceResult.Clear(); txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 已清空所有测量结果\r\n"); } /// <summary> /// 鼠标点击图片事件(选取/更新测量点) /// </summary> private void ImgDistanceDisplay_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { try { if (_distanceMat == null || _distanceMat.Empty() || _currentPickState == PickPointState.None) { return; // 无图片或未激活取点模式,不处理 } // 1. 获取鼠标在Image控件上的原始坐标 System.Windows.Point rawClickPoint = e.GetPosition(imgDistanceDisplay); // 2. 转换为图片的实际像素坐标 System.Windows.Point pixelPoint = ConvertToImagePixelCoordinate(rawClickPoint); // 根据当前取点状态处理 if (_currentPickState == PickPointState.PickingPoint1) { // 更新第一个点的像素坐标 _point1 = pixelPoint; // 将第一个点坐标显示到独立控件 txtPoint1X.Text = pixelPoint.X.ToString("F2"); txtPoint1Y.Text = pixelPoint.Y.ToString("F2"); // 转换为显示坐标,更新标记点 System.Windows.Point displayPoint = ConvertToDisplayCoordinate(pixelPoint); UpdateMarkerPosition(point1Marker, displayPoint); point1Marker.Visibility = Visibility.Visible; txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 更新第1个点:像素坐标X={pixelPoint.X:F2}, Y={pixelPoint.Y:F2}\r\n"); } else if (_currentPickState == PickPointState.PickingPoint2) { // 更新第二个点的像素坐标 _point2 = pixelPoint; // 将第二个点坐标显示到独立控件 txtPoint2X.Text = pixelPoint.X.ToString("F2"); txtPoint2Y.Text = pixelPoint.Y.ToString("F2"); // 转换为显示坐标,更新标记点 System.Windows.Point displayPoint = ConvertToDisplayCoordinate(pixelPoint); UpdateMarkerPosition(point2Marker, displayPoint); point2Marker.Visibility = Visibility.Visible; txtDistanceResult.AppendText($"[{DateTime.Now:HH:mm:ss}] 更新第2个点:像素坐标X={pixelPoint.X:F2}, Y={pixelPoint.Y:F2}\r\n"); } // 如果两个点都已选取,启用计算按钮 if (_point1.X >= 0 && _point1.Y >= 0 && _point2.X >= 0 && _point2.Y >= 0) { btnCalculateDistance.IsEnabled = true; } } catch (Exception ex) { MessageBox.Show($"选取测量点出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } /// <summary> /// 鼠标移动:实时预览取点位置 /// </summary> private void ImgDistanceDisplay_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { if (_distanceMat == null || _distanceMat.Empty() || _currentPickState == PickPointState.None) { distanceLine.Visibility = Visibility.Collapsed; return; } // 获取鼠标当前显示坐标 System.Windows.Point rawMovePoint = e.GetPosition(imgDistanceDisplay); // 转换为像素坐标 System.Windows.Point pixelMovePoint = ConvertToImagePixelCoordinate(rawMovePoint); // 转换回显示坐标(用于预览) System.Windows.Point displayMovePoint = ConvertToDisplayCoordinate(pixelMovePoint); // 实时预览线逻辑 if (_currentPickState == PickPointState.PickingPoint1) { // 取第一个点时,预览线从鼠标位置(临时)出发 distanceLine.X1 = displayMovePoint.X; distanceLine.Y1 = displayMovePoint.Y; distanceLine.X2 = displayMovePoint.X; distanceLine.Y2 = displayMovePoint.Y; distanceLine.Visibility = Visibility.Visible; } else if (_currentPickState == PickPointState.PickingPoint2 && _point1.X >= 0) { // 取第二个点时,预览线从第一个点到当前鼠标位置 System.Windows.Point displayPoint1 = ConvertToDisplayCoordinate(_point1); distanceLine.X1 = displayPoint1.X; distanceLine.Y1 = displayPoint1.Y; distanceLine.X2 = displayMovePoint.X; distanceLine.Y2 = displayMovePoint.Y; distanceLine.Visibility = Visibility.Visible; } } /// <summary> /// 绘制最终的测量线(计算距离后) /// </summary> private void DrawFinalDistanceLine() { if (_point1.X < 0 || _point2.X < 0) return; // 转换为显示坐标 System.Windows.Point displayPoint1 = ConvertToDisplayCoordinate(_point1); System.Windows.Point displayPoint2 = ConvertToDisplayCoordinate(_point2); // 更新测量线 distanceLine.X1 = displayPoint1.X; distanceLine.Y1 = displayPoint1.Y; distanceLine.X2 = displayPoint2.X; distanceLine.Y2 = displayPoint2.Y; distanceLine.Visibility = Visibility.Visible; } /// <summary> /// 核心方法:将Image控件上的显示坐标转换为图片的实际像素坐标 /// </summary> private System.Windows.Point ConvertToImagePixelCoordinate(System.Windows.Point displayPoint) { if (imgDistanceDisplay.Source == null) return displayPoint; // 获取图片的实际尺寸 double imageWidth = _distanceMat.Width; double imageHeight = _distanceMat.Height; // 获取Image控件的显示尺寸 double controlWidth = imgDistanceDisplay.ActualWidth; double controlHeight = imgDistanceDisplay.ActualHeight; // 计算缩放比例(等比例缩放) double scaleX = controlWidth / imageWidth; double scaleY = controlHeight / imageHeight; double scale = Math.Min(scaleX, scaleY); // Uniform模式下的实际缩放比例 // 计算图片在控件中的偏移(居中显示) double offsetX = (controlWidth - imageWidth * scale) / 2; double offsetY = (controlHeight - imageHeight * scale) / 2; // 转换为实际像素坐标 double pixelX = (displayPoint.X - offsetX) / scale; double pixelY = (displayPoint.Y - offsetY) / scale; // 边界校验(防止超出图片范围) pixelX = Math.Max(0, Math.Min(imageWidth, pixelX)); pixelY = Math.Max(0, Math.Min(imageHeight, pixelY)); return new System.Windows.Point(pixelX, pixelY); } /// <summary> /// 核心方法:将图片的实际像素坐标转换为Image控件上的显示坐标 /// </summary> private System.Windows.Point ConvertToDisplayCoordinate(System.Windows.Point pixelPoint) { if (imgDistanceDisplay.Source == null) return pixelPoint; // 获取图片的实际尺寸 double imageWidth = _distanceMat.Width; double imageHeight = _distanceMat.Height; // 获取Image控件的显示尺寸 double controlWidth = imgDistanceDisplay.ActualWidth; double controlHeight = imgDistanceDisplay.ActualHeight; // 计算缩放比例 double scaleX = controlWidth / imageWidth; double scaleY = controlHeight / imageHeight; double scale = Math.Min(scaleX, scaleY); // 计算图片在控件中的偏移 double offsetX = (controlWidth - imageWidth * scale) / 2; double offsetY = (controlHeight - imageHeight * scale) / 2; // 转换为显示坐标 double displayX = pixelPoint.X * scale + offsetX; double displayY = pixelPoint.Y * scale + offsetY; return new System.Windows.Point(displayX, displayY); } /// <summary> /// 计算两点间的欧几里得距离(像素) /// </summary> private double CalculateEuclideanDistance(System.Windows.Point p1, System.Windows.Point p2) { double dx = p2.X - p1.X; double dy = p2.Y - p1.Y; return Math.Sqrt(dx * dx + dy * dy); } /// <summary> /// 更新标记点的位置(精准居中) /// </summary> private void UpdateMarkerPosition(Ellipse marker, System.Windows.Point displayPoint) { // 精准计算标记点的左上角坐标,让标记点中心对齐点击位置 Canvas.SetLeft(marker, displayPoint.X - marker.Width / 2); Canvas.SetTop(marker, displayPoint.Y - marker.Height / 2); } /// <summary> /// 清空所有坐标/距离显示控件 /// </summary> private void ClearCoordinateControls() { txtPoint1X.Clear(); txtPoint1Y.Clear(); txtPoint2X.Clear(); txtPoint2Y.Clear(); txtDistanceValue.Clear(); } /// <summary> /// 清空测量状态(重置变量和UI) /// </summary> private void ClearMeasureState() { // 重置取点状态 _currentPickState = PickPointState.None; _measureCount = 0; // 重置点坐标 _point1 = new System.Windows.Point(-1, -1); _point2 = new System.Windows.Point(-1, -1); // 释放OpenCV资源 _distanceMat?.Release(); _distanceMat = new Mat(); // 清空UI元素 imgDistanceDisplay.Source = null; distanceLine.Visibility = Visibility.Collapsed; point1Marker.Visibility = Visibility.Collapsed; point2Marker.Visibility = Visibility.Collapsed; // 禁用计算按钮 btnCalculateDistance.IsEnabled = false; } /// <summary> /// 窗口关闭时释放资源 /// </summary> protected override void OnClosed(EventArgs e) { base.OnClosed(e); _distanceMat?.Release(); _distanceMat?.Dispose(); } } /// <summary> /// OpenCV Mat与WPF BitmapSource转换工具类 /// </summary> public static class BitmapSourceConverter { [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject); public static BitmapSource ToBitmapSource(Mat mat) { if (mat == null || mat.Empty()) return null; // 转换Mat到Bitmap using (var bitmap = BitmapConverter.ToBitmap(mat)) { var hBitmap = bitmap.GetHbitmap(); try { // 转换为WPF的BitmapSource return Imaging.CreateBitmapSourceFromHBitmap( hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally { // 释放HBitmap资源,避免内存泄漏 DeleteObject(hBitmap); } } } }}
核心原理说明
1. 坐标转换(解决取点错位问题)
图片在 WPF 的 Image 控件中设置Stretch="Uniform"后会等比例缩放并居中,直接使用鼠标坐标会错位,核心通过两个方法实现坐标映射:
ConvertToImagePixelCoordinate:将控件显示坐标转换为图片实际像素坐标(用于计算)ConvertToDisplayCoordinate:将图片像素坐标转换为控件显示坐标(用于绘制标记 / 线条)
关键计算逻辑:
// 缩放比例(保证图片等比例显示)
scale = Math.Min(控件宽度/图片宽度, 控件高度/图片高度)
// 居中偏移(图片在控件中居中的空白距离)
offsetX = (控件宽度 - 图片宽度×scale) / 2
offsetY = (控件高度 - 图片高度×scale) / 2
// 像素坐标 → 显示坐标
displayX = 像素X × scale + offsetX
displayY = 像素Y × scale + offsetY
// 显示坐标 → 像素坐标
pixelX = (显示X - offsetX) / scale
pixelY = (显示Y - offsetY) / scale
2. 欧几里得距离计算
两点间像素距离采用欧几里得公式:distance=(x2−x1)2+(y2−y1)2
使用步骤
- 运行程序,点击【选择测试图片】加载本地图片;
- 点击【取第 1 个点】,在图片上点击选取第一个点(可多次点击更新),第一个点坐标实时显示在对应控件;
- 点击【取第 2 个点】,在图片上点击选取第二个点(可多次点击更新),第二个点坐标实时显示在对应控件;
- 点击【计算 1 和 2 距离】,两点间像素距离会显示在专属控件,同时绘制红色测量线;
- 可随时点击【取消取点】终止当前取点模式,点击【清空测量结果】重置所有状态。
注意事项
- 需确保安装指定版本的 OpenCVSharp 包,适配.NET 4.6.1;
- 图片加载后会自动释放 OpenCV Mat 资源,避免内存泄漏;
- 取点坐标做了边界校验,防止超出图片范围。
总结
该工具解决了 WPF 中图片缩放居中后取点错位的核心问题,通过精准的坐标映射实现像素级精准测量,界面布局清晰,操作流程简单,可直接集成到图像处理相关的 WPF 项目中。