WPF+OpenCV 实现精准像素距离测量工具(.NET 4.6.1)

代码链接:https://download.csdn.net/download/ly1h1/92658735

功能介绍

基于 WPF 和 OpenCVSharp 开发的像素距离测量工具,解决图片缩放 / 居中后取点错位问题,核心功能:

  1. 支持选择本地图片(JPG/PNG/BMP 等格式)
  2. 分阶段取点:【取第 1 个点】【取第 2 个点】独立控制,支持多次更新取点
  3. 实时显示取点坐标,独立控件展示第一个点、第二个点坐标及两点像素距离
  4. 精准计算两点间欧几里得像素距离,测量线实时预览
  5. 支持取消取点、清空测量结果等操作

环境准备

  1. .NET Framework 4.6.1

  2. WPF 项目

  3. 安装 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>
  1. 后台逻辑(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 个点】,在图片上点击选取第一个点(可多次点击更新),第一个点坐标实时显示在对应控件;
  3. 点击【取第 2 个点】,在图片上点击选取第二个点(可多次点击更新),第二个点坐标实时显示在对应控件;
  4. 点击【计算 1 和 2 距离】,两点间像素距离会显示在专属控件,同时绘制红色测量线;
  5. 可随时点击【取消取点】终止当前取点模式,点击【清空测量结果】重置所有状态。

注意事项

  1. 需确保安装指定版本的 OpenCVSharp 包,适配.NET 4.6.1;
  2. 图片加载后会自动释放 OpenCV Mat 资源,避免内存泄漏;
  3. 取点坐标做了边界校验,防止超出图片范围。

总结

该工具解决了 WPF 中图片缩放居中后取点错位的核心问题,通过精准的坐标映射实现像素级精准测量,界面布局清晰,操作流程简单,可直接集成到图像处理相关的 WPF 项目中。

相关推荐
咚咚王者4 小时前
人工智能之视觉领域 计算机视觉 第四章 图像基本操作
人工智能·opencv·计算机视觉
Java后端的Ai之路4 小时前
【AI应用开发工程师】-分享Java 转 AI成功经验
java·开发语言·人工智能·ai·ai agent
新新学长搞科研4 小时前
【华南理工大学主办】第十三届先进制造技术与材料工程国际学术会议 (AMTME 2026)
人工智能·机器学习·ai·硬件工程·制造·材料工程·机械工程
AI周红伟4 小时前
周红伟:自媒体的AI时刻到了,Seedance2.0生成AI视频的具体技术原理是什么?抖音终于战胜了Sora2
人工智能·计算机视觉
落羽的落羽4 小时前
【C++】深入浅出“图”——最短路径算法
java·服务器·开发语言·c++·人工智能·算法·机器学习
过期的秋刀鱼!4 小时前
深度学习-更复杂的神经网络
人工智能·深度学习·神经网络
爱打代码的小林4 小时前
基于 Lucas-Kanade 光流法实现视频特征点追踪
opencv·计算机视觉·音视频
Maynor9964 小时前
OpenClaw 第2章:环境搭建
运维·人工智能·飞书
向哆哆4 小时前
100类中药材图像识别数据集分享(适用于目标检测任务)
人工智能·目标检测·计算机视觉