视觉ROI调试工具

视觉ROI调试工具

项目整体介绍

左区为功能区和信息显示,右区为ROI交互调试

项目总体为ImageViewBox+ROIRectangle两个自定义工具和主界面MainWindow

图像显示工具

///图像数据结构
public struct ImgStruct_t
{
    public int Channels;

    public int Width;

    public int Height;

    public IntPtr ImgDataAddr;

    public ImgStruct_t()
    {
        Channels = 0;
        Width = 0;
        Height = 0;
        ImgDataAddr = IntPtr.Zero;
    }
}

MainWindow中点击选着图像触发事件

<Button Content="选择图像" Command="{Binding SelectImageCommand}" />
/// <summary>
/// 源图
/// </summary>
public WriteableBitmap SourceBitmap { get; set; }

/// <summary>
/// 源图数据
/// </summary>
internal ImgStruct_t SourceImgData = new ImgStruct_t();


public string BMPFileName
{
    get => bmpFileName; set
    {

        if (string.IsNullOrEmpty(value) || !File.Exists(value)) return;
        //读取图像信息
        ImageHelper.ReadImage(value, ref SourceImgData, true);
        //初始化WriteableBitmap
        SourceBitmap = ImageHelper.UpdateBitmapSize(SourceBitmap, ref SourceImgData);
        //读取图像并获取指针
        HalconTool.ReadImage(0, value, "", ref SourceImgData);

        ///锁定内存位置,写入数据
        SourceBitmap.Lock();
        SourceBitmap.AddDirtyRect(new Int32Rect(0, 0, SourceImgData.Width, SourceImgData.Height));
        ///解锁用于显示
        SourceBitmap.Unlock();
        bmpFileName = value;
        //DrawingType = DrawingType.OriginalDrawing;
    }
}
string bmpFileName;

public ICommand SelectImageCommand
{
    get => new DelegateCommand(() =>
    {
        string initDir = System.IO.Path.GetDirectoryName(BMPFileName);
        BMPFileName = ControlHelper.SelectFile("请选择图像文件", "图像文件(*.bmp, *.jpg,*.tiff,*.png,*.jpeg)|*.bmp;*.jpg;*.tiff;*.png;*.jpeg", initDir);
        //UpdateFileList();
    });
}

Halcon读取本地图像

//
// 摘要:
//     读取本地图片,若Img的Ptr为0,则自动申请。若img的Ptr不为0,则按照img参数读取
//
// 参数:
//   path:
//     文件路径
//
//   img:
//     图像数据
//
//   readInfoOnly:
//     是否只读信息
//
// 返回结果:
//     是否读取成功
public static bool ReadImage(string path, ref ImgStruct_t img, bool readInfoOnly = false)
{
    if (string.IsNullOrEmpty(path) || !File.Exists(path))
    {
        return false;
    }

    if (img.ImgDataAddr == IntPtr.Zero || readInfoOnly)
    {
        FileStream fileStream = File.OpenRead(path);
        Image image = Image.FromStream(fileStream, useEmbeddedColorManagement: false, validateImageData: false);
        fileStream.Close();
        string text = image.PixelFormat.ToString();
        if (!text.StartsWith("Format"))
        {
            MessageBox.Show($"{image.PixelFormat}不支持!");
            return false;
        }

        int num = text.IndexOf('b');
        int channels = int.Parse(text.Substring(6, num - 6)) / 8;
        img.Width = image.Width;
        img.Height = image.Height;
        img.Channels = channels;
        if (readInfoOnly)
        {
            return true;
        }

        img.ImgDataAddr = Marshal.AllocHGlobal(img.Width * img.Height * img.Channels);
    }

    //ContainerLocator.Container.Resolve<IVisualOperator>().ReadImage(0, path, "", ref img);
    HalconTool.ReadImage(0, path, "", ref img);
    return true;
}

根据图像信息初始化WriteableBitmap

public static WriteableBitmap UpdateBitmapSize(WriteableBitmap source, ref ImgStruct_t imgStruct_T)
{
    if (source == null || source.Format.BitsPerPixel / 8 != imgStruct_T.Channels || source.PixelWidth != imgStruct_T.Width || source.PixelHeight != imgStruct_T.Height)
    {
        PixelFormat pixelFormat = PixelFormats.Indexed8;
        if (imgStruct_T.Channels == 1)
        {
            pixelFormat = PixelFormats.Gray8;
        }
        else if (imgStruct_T.Channels == 2)
        {
            pixelFormat = PixelFormats.Gray16;
        }
        else if (imgStruct_T.Channels == 3)
        {
            pixelFormat = PixelFormats.Rgb24;
        }
        else if (imgStruct_T.Channels == 4)
        {
            pixelFormat = PixelFormats.Bgr32;
        }

        source = new WriteableBitmap(imgStruct_T.Width, imgStruct_T.Height, 96.0, 96.0, pixelFormat, null);
    }

    imgStruct_T.ImgDataAddr = source.BackBuffer;
    return source;
}

ImageViewBox代码

<Grid x:Class="ROITool.ImageViewBox.ImageViewBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ROITool.ImageViewBox"
             xmlns:local1="clr-namespace:ROITool.ROIRectangle"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800" >
    <Border Grid.Row="1" ClipToBounds="True" ><!--ClipToBounds="True"限制Viewbox显示范围在Border内-->
        <Viewbox x:Name="MyViewbox" Stretch="Uniform" >
            <Viewbox.RenderTransform>
                <TransformGroup>
                    <ScaleTransform x:Name="ScaleTransform"  />
                    <TranslateTransform x:Name="TranslateTransform" />
                </TransformGroup>
            </Viewbox.RenderTransform>
            <Grid local1:ROIRectangle.Selected="ROI_SelectedHandle">
                <Image x:Name="DisplayImage" Stretch="UniformToFill" />
            </Grid>
        </Viewbox>
    </Border>
</Grid>

 ImageViewBox添加ImageSource依赖属性和鼠标事件

public partial class ImageViewBox : Grid
{
    public ImageViewBox()
    {
        InitializeComponent();
        // 监听鼠标滚轮事件
        this.MouseWheel += MainWindow_PreviewMouseWheel;

        // 监听鼠标按下事件
        this.MouseLeftButtonDown += MainWindow_PreviewMouseDown;

        // 监听鼠标移动事件
        this.MouseMove += MainWindow_PreviewMouseMove;

        // 监听鼠标释放事件
        this.MouseLeftButtonUp += MainWindow_PreviewMouseUp;

    }
    
    public static DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(WriteableBitmap), typeof(ImageViewBox), new PropertyMetadata(null, delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ImageViewBox imageViewBox6)
        {
            imageViewBox6.UpdateDisplayBitmap((d as ImageViewBox).Name);
        }
    }));

    public WriteableBitmap ImageSource
    {
        get
        {
            return (WriteableBitmap)GetValue(ImageSourceProperty);
        }
        set
        {
            SetValue(ImageSourceProperty, value);
        }
    }
    private void MainWindow_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        // 检查是否按下了 Ctrl 键
        //if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
        //{
            // 获取鼠标相对于 Viewbox 的位置
            Point mousePosition = e.GetPosition(MyViewbox);

            // 计算缩放因子
            double scaleFactor = e.Delta > 0 ? 1.1 : 0.9;

            // 以鼠标为中心进行缩放
            ZoomAtPosition(mousePosition, scaleFactor);

            // 标记事件已处理,防止继续传递
            e.Handled = true;
        //}
    }

    private void ZoomAtPosition(Point position, double scaleFactor)
    {
        // 获取当前的缩放和平移变换
        double currentScale = ScaleTransform.ScaleX;
        double newScale = currentScale * scaleFactor;

        // 计算缩放后的偏移量
        double offsetX = position.X * (1 - scaleFactor);
        double offsetY = position.Y * (1 - scaleFactor);

        // 应用新的缩放比例
        ScaleTransform.ScaleX = newScale;
        ScaleTransform.ScaleY = newScale;

        // 更新平移变换
        TranslateTransform.X += offsetX;
        TranslateTransform.Y += offsetY;
    }

    private void MainWindow_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        // 检查是否按下了鼠标左键
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            // 记录鼠标按下的初始位置
            _startPoint = e.GetPosition(this);
            _isDragging = true;

            // 捕获鼠标,确保即使鼠标移出窗口也能继续拖动
            this.CaptureMouse();
        }
    }

    private void MainWindow_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        Point Point = e.GetPosition(DisplayImage);
        // 如果正在拖动
        if (_isDragging)
        {
            // 获取当前鼠标位置
            Point currentPoint = e.GetPosition(this);

            // 计算鼠标移动的偏移量
            double offsetX = currentPoint.X - _startPoint.X;
            double offsetY = currentPoint.Y - _startPoint.Y;

            // 更新平移变换
            TranslateTransform.X += offsetX;
            TranslateTransform.Y += offsetY;

            // 更新初始位置
            _startPoint = currentPoint;
        }
    }

    private void MainWindow_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        // 如果释放了鼠标左键
        if (e.LeftButton == MouseButtonState.Released && _isDragging)
        {
            // 结束拖动
            _isDragging = false;

            // 释放鼠标捕获
            this.ReleaseMouseCapture();
        }
    }

}

MainWindow使用控件

<controls:ImageViewBox Grid.Column="1" ImageSource="{Binding SourceBitmap}"  SelectedROIChanged="ROI_SelectedROIChanged">
</controls:ImageViewBox>

ROI调试工具

ROIRectangle继承Canvas,添加ROI位置信息相关的依赖属性,添加鼠标事件

    /// <summary>
    /// ROIRectangle.xaml 的交互逻辑
    /// </summary>
    public partial class ROIRectangle : Canvas
    {
        /// <summary>
        /// ROI宽度依赖属性
        /// </summary>
        public static DependencyProperty ROIWidthProperty =
            DependencyProperty.Register(
                "ROIWidth",
                typeof(double),
                typeof(ROIRectangle),
                new FrameworkPropertyMetadata(
                    0.0,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
                    {
                        if (d is ROIRectangle rOIRectangle)
                        {
                            rOIRectangle.UpdateROI();
                        }
                    },
                    null,
                    false,
                UpdateSourceTrigger.PropertyChanged)
            );

        public double ROIWidth
        {
            get
            {
                return (double)GetValue(ROIWidthProperty);
            }
            set
            {
                SetValue(ROIWidthProperty, value);
            }
        }

        /// <summary>
        /// ROI高度依赖属性
        /// </summary>
        public static DependencyProperty ROIHeightProperty = 
            DependencyProperty.Register(
                "ROIHeight", 
                typeof(double), 
                typeof(ROIRectangle),
                new FrameworkPropertyMetadata(
                    0.0,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
                    {
                        if (d is ROIRectangle rOIRectangle)
                        {
                            rOIRectangle.UpdateROI();
                        }
                    },
                    null,
                    false,
                UpdateSourceTrigger.PropertyChanged)
            );

        public double ROIHeight
        {
            get
            {
                return (double)GetValue(ROIHeightProperty);
            }
            set
            {
                SetValue(ROIHeightProperty, value);
            }
        }

        /// <summary>
        /// ROI左上角X坐标
        /// </summary>
        public static DependencyProperty ROILeftTopXProperty = DependencyProperty.Register(
            "ROILeftTopX", 
            typeof(double), 
            typeof(ROIRectangle),
                new FrameworkPropertyMetadata(
                    0.0,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
                    {
                        if (d is ROIRectangle rOIRectangle)
                        {
                            rOIRectangle.UpdateROI();
                        }
                    },
                    null,
                    false,
                UpdateSourceTrigger.PropertyChanged)
            );

        public double ROILeftTopX
        {
            get
            {
                return (double)GetValue(ROILeftTopXProperty);
            }
            set
            {
                SetValue(ROILeftTopXProperty, value);
            }
        }

        /// <summary>
        /// ROI右上角Y坐标
        /// </summary>
        public static DependencyProperty ROILeftTopYProperty = 
            DependencyProperty.Register(
                "ROILeftTopY", 
                typeof(double), 
                typeof(ROIRectangle),
                new FrameworkPropertyMetadata(
                    0.0,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
                    {
                        if (d is ROIRectangle rOIRectangle)
                        {
                            rOIRectangle.UpdateROI();
                        }
                    },
                    null,
                    false,
                UpdateSourceTrigger.PropertyChanged)
            );

        public double ROILeftTopY
        {
            get
            {
                return (double)GetValue(ROILeftTopYProperty);
            }
            set
            {
                SetValue(ROILeftTopYProperty, value);
            }
        }

        /// <summary>
        /// 更新ROI位置
        /// </summary>
        private void UpdateROI()
        {
                this.grid.Width = ROIWidth;
                this.grid.Height = ROIHeight;
                this.grid.SetValue(Canvas.LeftProperty, ROILeftTopX);
                this.grid.SetValue(Canvas.TopProperty, ROILeftTopY);
        }

        /// <summary>
        /// 自定义路由事件
        /// </summary>
        public static readonly RoutedEvent SelectedEvent =
                EventManager.RegisterRoutedEvent(
                "Selected",
                RoutingStrategy.Bubble,
                typeof(RoutedEventHandler),
                typeof(ROIRectangle));

        public event RoutedEventHandler Selected
        {
            add
            {
                AddHandler(SelectedEvent, value);
            }
            remove
            {
                RemoveHandler(SelectedEvent, value);
            }
        }

        /// <summary>
        /// 触发路由事件
        /// </summary>
        protected virtual void OnCustomClick()
        {
            RoutedEventArgs args = new RoutedEventArgs(SelectedEvent, this);
            RaiseEvent(args);
        }

        public ROIRectangle()
        {
            InitializeComponent();
        }
        Rectangle rectangle;
        private Point _startPoint; // 记录鼠标按下的初始位置
        private bool _isDragging; // 标记是否正在拖动
        /// <summary>
        /// 鼠标按下事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
        {
            rectangle = (Rectangle)e.OriginalSource;
            _startPoint = e.GetPosition(canvas);
            _isDragging = true;
            //锁定鼠标焦点在控件上
            rectangle.CaptureMouse();
            //触发路由事件
            OnCustomClick();
            e.Handled = true;
        }

        private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e)
        {
            // 如果释放了鼠标左键
            if (e.LeftButton == MouseButtonState.Released && _isDragging)
            {
                // 结束拖动
                _isDragging = false;

                // 释放鼠标捕获
                rectangle.ReleaseMouseCapture();
            }
            e.Handled = true;
        }
        private void Rectangle_MouseMove(object sender, MouseEventArgs e)
        {
            // 如果正在拖动
            if (_isDragging)
            {
                // 获取当前鼠标位置
                Point currentPoint = e.GetPosition(canvas);

                // 计算鼠标移动的偏移量
                var delta = currentPoint - _startPoint;

                _startPoint = currentPoint;

                // 更新
                switch (rectangle.Name)
                {
                    case "CenterCenter":
                        ROILeftTopX += delta.X;
                        ROILeftTopY += delta.Y;
                        break;
                    case "LeftBottom":
                        ROILeftTopX += delta.X;
                        ROIWidth = Math.Max(60, ROIWidth - delta.X);
                        ROIHeight = Math.Max(60, ROIHeight + delta.Y);
                        break;
                    case "LeftCenter":
                        ROILeftTopX += delta.X;
                        ROIWidth = Math.Max(60, ROIWidth - delta.X);
                        break;
                    case "LeftTop":
                        ROILeftTopX += delta.X;
                        ROIWidth = Math.Max(60, ROIWidth - delta.X);
                        ROILeftTopY += delta.Y;
                        ROIHeight = Math.Max(60, ROIHeight - delta.Y);
                        break;
                    case "RightBottom":
                        ROIWidth = Math.Max(60, ROIWidth + delta.X);
                        ROIHeight = Math.Max(60, ROIHeight + delta.Y);
                        break;
                    case "RightCenter":
                        ROIWidth = Math.Max(60, ROIWidth + delta.X);
                        break;
                    case "RightTop":
                        ROILeftTopY += delta.Y;
                        ROIWidth = Math.Max(60, ROIWidth + delta.X);
                        ROIHeight = Math.Max(60, ROIHeight - delta.Y);
                        break;
                    case "CenterBottom":
                        ROIHeight = Math.Max(60, ROIHeight + delta.Y);
                        break;
                    case "CenterTop":
                        ROILeftTopX += delta.X;
                        ROIHeight = Math.Max(60, ROIHeight + delta.Y);
                        break;
                    default:
                        break;
                }
            }
            //e.Handled = true;
        }

    }

Xaml代码

<Canvas x:Class="ROITool.ROIRectangle.ROIRectangle"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ROITool.ROIRectangle" x:Name="canvas"
             mc:Ignorable="d" MouseMove="Rectangle_MouseMove" MouseLeftButtonUp="Rectangle_MouseUp">
    <Grid x:Name="grid">
        <Rectangle Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="CenterCenter" Cursor="SizeAll"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="LeftBottom" Cursor="SizeNESW" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="LeftCenter" Cursor="SizeWE" HorizontalAlignment="Left" VerticalAlignment="Center"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="LeftTop" Cursor="SizeNWSE" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="RightBottom" Cursor="SizeNWSE" HorizontalAlignment="Right" VerticalAlignment="Bottom"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="RightCenter" Cursor="SizeWE" HorizontalAlignment="Right" VerticalAlignment="Center"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="RightTop" Cursor="SizeNESW" HorizontalAlignment="Right" VerticalAlignment="Top"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="CenterBottom" Cursor="SizeNS" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
        <Rectangle Width="20"  Height="20" Fill="Transparent" Stroke="Red" StrokeThickness="1" PreviewMouseLeftButtonDown="Rectangle_MouseDown" x:Name="CenterTop" Cursor="SizeNS" HorizontalAlignment="Center" VerticalAlignment="Top"/>
    </Grid>
</Canvas>

 Cursor属性,设置鼠标悬停在控件上方时的样式(WIN系统定义)

ImageViewBox容器控件

控件中将ROI部分使用依赖属性开放到外部设置

    /// <summary>
    /// ImageViewBox.xaml 的交互逻辑
    /// </summary>
    public partial class ImageViewBox : Grid
    {
        /// <summary>
        /// 内容依赖属性
        /// </summary>
        public static readonly DependencyProperty ContentProperty =
                    DependencyProperty.Register(
                        "Content",
                        typeof(object),
                        typeof(ImageViewBox),
                        new PropertyMetadata(null, OnContentChanged));

        public object Content
        {
            get { return GetValue(ContentProperty); }
            set { SetValue(ContentProperty, value); }
        }

        /// <summary>
        /// 将内容添加到内部控件中
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as ImageViewBox;
            if (control != null)
            {
                control.PART_ContentHost.Content = e.NewValue;
            }
        }

        /// <summary>
        /// 图像源依赖属性
        /// </summary>
        public static DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(WriteableBitmap), typeof(ImageViewBox), new PropertyMetadata(null, delegate (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is ImageViewBox imageViewBox6)
            {
                imageViewBox6.UpdateDisplayBitmap((d as ImageViewBox).Name);
            }
        }));

        public WriteableBitmap ImageSource
        {
            get
            {
                return (WriteableBitmap)GetValue(ImageSourceProperty);
            }
            set
            {
                SetValue(ImageSourceProperty, value);
            }
        }

        private Point _startPoint; // 记录鼠标按下的初始位置
        private bool _isDragging; // 标记是否正在拖动
        public ImageViewBox()
        {
            InitializeComponent();
            
            // 监听鼠标滚轮事件
            this.MouseWheel += MainWindow_PreviewMouseWheel;

            // 监听鼠标按下事件
            this.MouseLeftButtonDown += MainWindow_PreviewMouseDown;

            // 监听鼠标移动事件
            this.MouseMove += MainWindow_PreviewMouseMove;

            // 监听鼠标释放事件
            this.MouseLeftButtonUp += MainWindow_PreviewMouseUp;
        }

        /// <summary>
        /// 更新信息栏图像尺寸信息
        /// </summary>
        /// <param name="ImageName"></param>
        private void UpdateDisplayBitmap(string ImageName)
        {
            this.DisplayImage.Source = ImageSource;
            this.measureLabel.Content = "尺寸:" + ImageSource.PixelWidth + "*" + ImageSource.PixelHeight;
        }

        /// <summary>
        /// 鼠标滚轮事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainWindow_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            // 检查是否按下了 Ctrl 键
            //if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
            //{
                // 获取鼠标相对于 Viewbox 的位置
                Point mousePosition = e.GetPosition(MyViewbox);

                // 计算缩放因子
                double scaleFactor = e.Delta > 0 ? 1.1 : 0.9;

                // 以鼠标为中心进行缩放
                ZoomAtPosition(mousePosition, scaleFactor);

                // 标记事件已处理,防止继续传递
                e.Handled = true;
            //}
        }


        private void ZoomAtPosition(Point position, double scaleFactor)
        {
            // 获取当前的缩放和平移变换
            double currentScale = ScaleTransform.ScaleX;
            double newScale = currentScale * scaleFactor;

            // 计算缩放后的偏移量
            double offsetX = position.X * (1 - scaleFactor);
            double offsetY = position.Y * (1 - scaleFactor);

            // 应用新的缩放比例
            ScaleTransform.ScaleX = newScale;
            ScaleTransform.ScaleY = newScale;

            // 更新平移变换
            TranslateTransform.X += offsetX;
            TranslateTransform.Y += offsetY;
        }

        /// <summary>
        /// 鼠标按下事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainWindow_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            // 检查是否按下了鼠标左键
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                // 记录鼠标按下的初始位置
                _startPoint = e.GetPosition(this);
                _isDragging = true;

                // 捕获鼠标,确保即使鼠标移出窗口也能继续拖动
                this.CaptureMouse();
            }
        }

        /// <summary>
        /// 鼠标拖动事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainWindow_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            Point Point = e.GetPosition(DisplayImage);
            GetGrayValueAt(ImageSource, (int)Point.X, (int)Point.Y);
            // 如果正在拖动
            if (_isDragging)
            {
                // 获取当前鼠标位置
                Point currentPoint = e.GetPosition(this);

                // 计算鼠标移动的偏移量
                double offsetX = currentPoint.X - _startPoint.X;
                double offsetY = currentPoint.Y - _startPoint.Y;

                // 更新平移变换
                TranslateTransform.X += offsetX;
                TranslateTransform.Y += offsetY;

                // 更新初始位置
                _startPoint = currentPoint;
            }
        }

        /// <summary>
        /// 鼠标抬起事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainWindow_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            // 如果释放了鼠标左键
            if (e.LeftButton == MouseButtonState.Released && _isDragging)
            {
                // 结束拖动
                _isDragging = false;

                // 释放鼠标捕获
                this.ReleaseMouseCapture();
            }
        }


        private void Rectangle_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var canvas = (Canvas)sender;
            var parent = (Panel)canvas.Parent;

            // 获取当前最大ZIndex
            int maxZ = parent.Children.OfType<UIElement>().Select(x => Panel.GetZIndex(x)).Max();

            // 设置点击的矩形在最前
            Panel.SetZIndex(canvas, maxZ + 1);

            // 可选:添加动画效果
            //var anim = new DoubleAnimation(1.1, TimeSpan.FromSeconds(0.1));
            //anim.AutoReverse = true;
            //rectangle.RenderTransform = new ScaleTransform();
            //rectangle.RenderTransform.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
            //rectangle.RenderTransform.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
        }

        /// <summary>
        /// 更新信息栏灰度值信息
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        private void GetGrayValueAt(WriteableBitmap bitmap, int x, int y)
        {
            if (bitmap == null) return;
            if (x < 0 || x >= bitmap.PixelWidth || y < 0 || y >= bitmap.PixelHeight) return;
            bitmap.Lock();

            try
            {
                // 获取像素缓冲区的指针
                IntPtr backBuffer = bitmap.BackBuffer;

                // 计算每像素的字节数
                int bytesPerPixel = (bitmap.Format.BitsPerPixel + 7) / 8;

                // 计算指定位置的字节偏移量
                int offset = y * bitmap.BackBufferStride + x * bytesPerPixel;

                // 读取像素值
                byte B = System.Runtime.InteropServices.Marshal.ReadByte(backBuffer, offset);
                byte G = System.Runtime.InteropServices.Marshal.ReadByte(backBuffer, offset + 1);
                byte R = System.Runtime.InteropServices.Marshal.ReadByte(backBuffer, offset + 2);

                this.locationLabel.Content = "位置:" + x + "*" + y;
                this.BLabel.Content = "B通道:" + B;
                this.GLabel.Content = "G通道:" + G;
                this.RLabel.Content = "R通道:" + R;
                // 计算灰度值(使用加权平均法)
                this.GrayLabel.Content = "Gray:" + (byte)(0.299 * R + 0.587 * G + 0.114 * B);

                return;
            }
            finally
            {
                // 确保无论如何都解锁位图
                bitmap.Unlock();
            }
        }

        /// <summary>
        /// ROI选择变化事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="selectedROI"></param>
        public delegate void SelectedROIChangedEvent(ImageViewBox? sender, ROIRectangle.ROIRectangle selectedROI);
        public event SelectedROIChangedEvent SelectedROIChanged;
        private void ROI_SelectedHandle(object sender, RoutedEventArgs arg)
        {
            var SelectedROI = arg.OriginalSource as ROIRectangle.ROIRectangle;
            this.SelectedROIChanged?.Invoke(this, SelectedROI);
            arg.Handled = true;
        }
    }

Xaml代码

<Grid x:Class="ROITool.ImageViewBox.ImageViewBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ROITool.ImageViewBox"
             xmlns:local1="clr-namespace:ROITool.ROIRectangle"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800" >
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <DockPanel DockPanel.Dock="Top" Height="25" Grid.Row="0" Background="White">
        <Label x:Name="measureLabel" Content="尺寸:0*0"/>
        <Label x:Name="locationLabel" Content="位置:0*0"/>
        <Label x:Name="GrayLabel" Content="Gray:0"/>
        <Label x:Name="RLabel" Content="R通道:0"/>
        <Label x:Name="GLabel" Content="G通道:0"/>
        <Label x:Name="BLabel" Content="B通道:0"/>
    </DockPanel>
    <Border Grid.Row="1" ClipToBounds="True" ><!--ClipToBounds="True"限制Viewbox显示范围在Border内-->
        <Viewbox x:Name="MyViewbox" Stretch="Uniform" >
            <Viewbox.RenderTransform>
                <TransformGroup>
                    <ScaleTransform x:Name="ScaleTransform"  />
                    <TranslateTransform x:Name="TranslateTransform" />
                </TransformGroup>
            </Viewbox.RenderTransform>
            <Grid local1:ROIRectangle.Selected="ROI_SelectedHandle">
                <Image x:Name="DisplayImage" Stretch="UniformToFill" />
                <ContentPresenter x:Name="PART_ContentHost"/>
            </Grid>
        </Viewbox>
    </Border>
</Grid>

MainWindow控件使用

Xaml代码

<Window x:Class="ROITool.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ROITool"
        xmlns:controls="clr-namespace:ROITool.ImageViewBox" 
        xmlns:controls1="clr-namespace:ROITool.ROIRectangle" 
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 
        mc:Ignorable="d"
        Title="MainWindow" Height="900" Width="1600">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding ClosingCommand}" PassEventArgsToCommand="True"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="800"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <DockPanel Grid.Column="0">
            <DockPanel DockPanel.Dock="Top">
                <TextBox Text="本地图像:" IsHitTestVisible="False"/>
                <TextBox Text="{Binding BMPFileName}" IsReadOnly="True"/>
            </DockPanel >
            <UniformGrid Columns="4" Height="20" DockPanel.Dock="Top" >
                <Button Content="选择图像" Command="{Binding SelectImageCommand}" />
                <Button Content="执行检测" Command="{Binding ExecuteDetectCommand}" />
                <Button Content="全览ROI" Command="{Binding AllROICommand}" />
                <Button Content="保存设置" Command="{Binding SaveConfigCommand}" />
            </UniformGrid>
            <DockPanel DockPanel.Dock="Top" Name="dplSizeDetectPram">
                <UniformGrid Columns="2">
                    <Label Content="ROI左上(X)"/>
                    <TextBox Text="{Binding ROILeftTopX,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    <Label Content="ROI左上(Y)"/>
                    <TextBox Text="{Binding ROILeftTopY,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    <Label Content="宽度"/>
                    <TextBox Text="{Binding ROIWidth,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    <Label Content="高度"/>
                    <TextBox Text="{Binding ROIHeight,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                </UniformGrid>
            </DockPanel>
            <UniformGrid>
            </UniformGrid>
        </DockPanel>
        <controls:ImageViewBox Grid.Column="1" ImageSource="{Binding SourceBitmap}"  SelectedROIChanged="ROI_SelectedROIChanged">
            <controls:ImageViewBox.Resources>
                <Style TargetType="controls1:ROIRectangle" x:Key="sizeDetectGroupStyle">
                    <Setter Property="ROIHeight" Value="{Binding ROIHeight,Mode=TwoWay}"/>
                    <Setter Property="ROIWidth" Value="{Binding ROIWidth,Mode=TwoWay}"/>
                    <Setter Property="ROILeftTopX" Value="{Binding ROILeftTopX,Mode=TwoWay}"/>
                    <Setter Property="ROILeftTopY" Value="{Binding ROILeftTopY,Mode=TwoWay}"/>
                </Style>
            </controls:ImageViewBox.Resources>
            <controls:ImageViewBox.Content>
                <Grid>
                    <controls1:ROIRectangle DataContext="{Binding ROIData1}">
                        <controls1:ROIRectangle.Style>
                            <Style TargetType="controls1:ROIRectangle" BasedOn="{StaticResource sizeDetectGroupStyle}"/>
                        </controls1:ROIRectangle.Style>
                    </controls1:ROIRectangle>
                    <controls1:ROIRectangle DataContext="{Binding ROIData2}">
                        <controls1:ROIRectangle.Style>
                            <Style TargetType="controls1:ROIRectangle" BasedOn="{StaticResource sizeDetectGroupStyle}"/>
                        </controls1:ROIRectangle.Style>
                    </controls1:ROIRectangle>
                </Grid>
            </controls:ImageViewBox.Content>
        </controls:ImageViewBox>
    </Grid>
</Window>
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainWindowModel();
    }

    /// <summary>
    /// 更新绑定数据源
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="selectedROI"></param>
    private void ROI_SelectedROIChanged(ImageViewBox.ImageViewBox sender, ROIRectangle.ROIRectangle selectedROI)
    {
        if (selectedROI.DataContext is ROIRectangleData)
        {
            dplSizeDetectPram.DataContext = selectedROI.DataContext;
        }
    }

ROIRectangle控件单击ROI触发SelectedEvent路由事件冒泡传递,ImageViewBox控件使用local1:ROIRectangle.Selected="ROI_SelectedHandle"进行监听并触发ImageViewBox控件的SelectedROIChanged继续向外传递选中的ROIRectangle控件,最后在MainWindow中SelectedROIChanged="ROI_SelectedROIChanged"订阅事件对dplSizeDetectPram的数据源进行修改。

MainWindowModel代码

    [PropertyChanged.AddINotifyPropertyChangedInterface]
    public class MainWindowModel
    {
        /// <summary>
        /// 源图
        /// </summary>
        public WriteableBitmap SourceBitmap { get; set; }

        /// <summary>
        /// 效果图
        /// </summary>
        public WriteableBitmap EffectBitmap { get; set; }

        /// <summary>
        /// 源图数据
        /// </summary>
        internal ImgStruct_t SourceImgData = new ImgStruct_t();

        public ROIRectangleData ROIData1 { get; set; } = new ROIRectangleData();

        public ROIRectangleData ROIData2 { get; set; } = new ROIRectangleData();

        public string BMPFileName
        {
            get => bmpFileName; set
            {

                if (string.IsNullOrEmpty(value) || !File.Exists(value)) return;
                //读取图像信息
                ImageHelper.ReadImage(value, ref SourceImgData, true);
                //初始化WriteableBitmap
                SourceBitmap = ImageHelper.UpdateBitmapSize(SourceBitmap, ref SourceImgData);
                //读取图像并获取指针
                HalconTool.ReadImage(0, value, "", ref SourceImgData);
                SourceBitmap.Lock();
                SourceBitmap.AddDirtyRect(new Int32Rect(0, 0, SourceImgData.Width, SourceImgData.Height));
                SourceBitmap.Unlock();
                bmpFileName = value;
                //DrawingType = DrawingType.OriginalDrawing;
            }
        }
        string bmpFileName;
        public ICommand SelectImageCommand
        {
            get => new DelegateCommand(() =>
            {
                string initDir = System.IO.Path.GetDirectoryName(BMPFileName);
                BMPFileName = ControlHelper.SelectFile("请选择图像文件", "图像文件(*.bmp, *.jpg,*.tiff,*.png,*.jpeg)|*.bmp;*.jpg;*.tiff;*.png;*.jpeg", initDir);
                //UpdateFileList();
            });
        }

        public ICommand ExecuteDetectCommand
        {
            get => new DelegateCommand(() =>
            {
                var a = ROIData1;
                var b = ROIData2;
            });
        }

        public ICommand LoadedCommand
        {
            get => new DelegateCommand(() =>
            {
                InitModel();
            });
        }

        internal void InitModel()
        {
            ROIData1 = new ROIRectangleData()
            {
                ROIHeight = 300,
                ROIWidth = 300,
                ROILeftTopX = 200,
                ROILeftTopY = 200,
            };
            ROIData2 = new ROIRectangleData()
            {
                ROIHeight = 300,
                ROIWidth = 300,
                ROILeftTopX = 300,
                ROILeftTopY = 300,
            };
        }
    }

    public enum DrawingType
    {
        //
        // 摘要:
        //     原图
        [Description("原图")]
        OriginalDrawing,
        //
        // 摘要:
        //     效果图
        [Description("效果图")]
        EffectDrawing
    }

 

posted @ 2025-04-10 10:37  ZHIZRL  阅读(83)  评论(0)    收藏  举报