位置:首页 > C# > WPF基于Canvas绘制多边形ROI

WPF基于Canvas绘制多边形ROI

时间:2026-08-27  |  作者:火苗实验室  |  阅读:0

目录

  1. WPF基于Canvas绘制多边形ROI
  2. ROI定义与实现概述
  3. 核心组件定义
  4. 交互逻辑处理

前言

在图像处理中,感兴趣区域(ROI)通常指需重点处理的矩形或多边形部分。本文介绍如何在WPF中利用Canvas控件实现多边形ROI的绘制。通过定义PolygonROIDrawingVisual工具类并初始化画布,开发者可灵活控制绘制过程。核心逻辑绑定于Canvas的鼠标交互事件,支持单击添加顶点、双击结束以及实时平移操作,确保顶点及多边形始终在边界内,从而构建高效的自定义ROI选择功能。

WPF基于Canvas绘制多边形ROI 的核心流程信息图
WPF基于Canvas绘制多边形ROI用简体中文信息图概括WPF基于Canvas绘制多边形ROI的核心流程、关键规则与实践要点。

WPF基于Canvas绘制多边形ROI

ROI定义与实现概述

ROI(Region of Interest)指图像或视频帧中需重点处理的部分,通常为矩形区域,也可是多边形、圆形或掩码定义的任意形状。本文基于WPF的Canvas绘制多边形ROI,具体实现步骤如下:

WPF基于Canvas绘制多边形ROI 对应的技术说明图
WPF基于Canvas绘制多边形ROI概括WPF基于Canvas绘制多边形ROI的核心概念、关键要点与实践提示。

核心组件定义

首先定义绘制工具类,并初始化画布Canvas。主要属性包括EnableDraw用于启动绘画,PointCollection用于存储多边形顶点坐标。

交互逻辑处理

绘制逻辑主要在Canvas的MouseMove和MouseLeftButtonDown事件中实现:

  • MouseLeftButtonDown:单击添加顶点,双击结束绘画。
  • MouseMove:处理实时绘制、平移顶点及整个多边形。

添加顶点时需处理边界限制及现有点重复问题;平移顶点时需确保顶点及多边形区域不超出Canvas边界。

首先定义一个用来绘制的工具

 internal class PolygonROIDrawingVisual : DrawingVisual
{
    private readonly double _radius = 5.0;

    /// 
    /// 绘制多边形边框
    /// 
    public void Draw(List points, Brush brushes = null)
    {
        Pen _strokePen = new Pen(brushes  Brushes.Red, 2.0);
        using (DrawingContext dc = RenderOpen())
        {
            if (points.Count == 0)
            {
                return;
            }
            if (points.Count == 1)
            {
                dc.DrawEllipse(brushes  Brushes.Red, _strokePen, points[0], _radius, _radius);
                return;
            }

            // 创建多边形边框
            StreamGeometry geometry = new StreamGeometry();

            using (StreamGeometryContext ctx = geometry.Open())
            {
                // 移动到第一个点
                ctx.BeginFigure(points[0], false, points.Count >= 3); // 只有3个以上点才闭合

                // 绘制所有线段
                for (int i = 1; i < points.Count; i++)
                {
                    ctx.LineTo(points[i], true, false);
                }
            }

            // 绘制边框
            dc.DrawGeometry(null, _strokePen, geometry);

            // 绘制顶点
            foreach (Point point in points)
            {
                dc.DrawEllipse(brushes  Brushes.Red, _strokePen, point, _radius, _radius);
            }
        }
    }
}

定义画布 Canvas

    public enum PolygonROIOperateType
    {
        None,
        ReadyToDraw,
        DrawDone,
    }

    public class PolygonROICanvas : Canvas
    {
        private readonly int _minGap = 10; // 两顶点的最小距离

        private Point lastPoint;
        private readonly PolygonROIDrawingVisual roi;
        private PolygonROIOperateType operate = PolygonROIOperateType.None;
        protected override int VisualChildrenCount => 1;

        protected override Visual GetVisualChild(int index)
        {
            return roi;
        }

        public PolygonROICanvas()
        {
            roi = new PolygonROIDrawingVisual();
            this.AddLogicalChild(roi);
            this.AddVisualChild(roi);
            Background = Brushes.Transparent;
            PointCollection = new ObservableCollection();
        }

        /// 
        /// 是否可以开始绘制ROI
        /// 
        public bool EnableDraw
        {
            get { return (bool)GetValue(EnableDrawProperty); }
            set { SetValue(EnableDrawProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Enable.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EnableDrawProperty =
            DependencyProperty.Register("EnableDraw",
                typeof(bool), typeof(PolygonROICanvas),
                new PropertyMetadata(false, OnEnableDrawPropertyChanged));

        private static void OnEnableDrawPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not PolygonROICanvas canvas)
                return;
            if (e.NewValue == e.OldValue)
            {
                return;
            }
            if (e.NewValue is bool enable && enable == true)
            {
                canvas.operate = PolygonROIOperateType.ReadyToDraw;
                canvas.PointCollection.Clear();
            }
        }

        public static readonly DependencyProperty PointCollectionProperty =
           DependencyProperty.Register(
               nameof(PointCollection),
               typeof(ObservableCollection),
               typeof(PolygonROICanvas),
               new PropertyMetadata(null,
                  OnItemsPropertyChanged));

        public ObservableCollection PointCollection
        {
            get => (ObservableCollection)GetValue(PointCollectionProperty);
            set => SetValue(PointCollectionProperty, value);
        }

        private static void OnItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not PolygonROICanvas canvas)
                return;

            if (e.OldValue is ObservableCollection oldCollection)
            {
                oldCollection.CollectionChanged -= canvas.PointCollection_CollectionChanged;
            }

            if (e.NewValue is ObservableCollection newCollection)
            {
                newCollection.CollectionChanged += canvas.PointCollection_CollectionChanged;
            }

            // 初始更新(包括 newCollection == null 的情况)
            canvas.UpdateDrawing();
        }

        private void PointCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            UpdateDrawing();
        }

        private void UpdateDrawing()
        {
            // 确保在 UI 线程执行绘制
            if (!Dispatcher.CheckAccess())
            {
                Dispatcher.Invoke(UpdateDrawing);
                return;
            }

            var points = PointCollection.ToList();
            if (points == null || points.Count == 0)
            {
                // 清空绘制(传入空列表)
                roi.Draw(new List());
                return;
            }

            roi.Draw(points);
        }
        .....
     }

主要的属性 EnableDraw 用来启动绘画; PointCollection 用来存放多边形的顶点。

绘制的逻辑 主要是在Canvas的 MouseMove 和 MouseLeftButtonDown事件中处理。

MouseLeftButtonDown 主要是单击添加顶点 和 双击结束绘画

       private DateTime _lastClickTime;
       private const int DoubleClickThreshold = 200; // 双击时间间隔阈值,单位为毫秒

       protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
       {
           base.OnMouseLeftButtonDown(e);
           // 双击
           if ((DateTime.Now - _lastClickTime).TotalMilliseconds < DoubleClickThreshold)
           {
               EnableDraw = false;
               operate = PolygonROIOperateType.DrawDone;
               e.Handled = true;
           }
           else // 单击
           {
               if (operate == PolygonROIOperateType.ReadyToDraw)
               {
                   Point point = e.GetPosition(this);
                   if (point.X < 0 || point.X > this.ActualWidth)
                   {
                       return;
                   }
                   if (point.Y < 0 || point.Y > this.ActualHeight)
                   {
                       return;
                   }
                   if (IsPointTooClose(point, PointCollection.ToList()))
                   {
                       return;
                   }
                   // 单击增加点
                   PointCollection.Add(point);
                   lastPoint = e.GetPosition(this);
               }
           }
           _lastClickTime = DateTime.Now;
       }

       // 判断新点是否与已有点过近
       private bool IsPointTooClose(Point newPoint, List existingPoints)
       {
           if (existingPoints == null || existingPoints.Count == 0)
           {
               return false;
           }
           foreach (var point in existingPoints)
           {
               if (GetDistance(newPoint, point) < _minGap)
                   return true;
           }
           return false;
       }

       private double GetDistance(Point p1, Point p2)
       {
           double dx = p1.X - p2.X;
           double dy = p1.Y - p2.Y;
           return System.Math.Sqrt(dx * dx + dy * dy);
       }

添加顶点的时候要考虑边界问题以及现有点的重复问题。

MouseMove事件主要是处理 绘图中的实时绘制 以及 平移顶点和整个多边形。

     protected override void OnMouseMove(MouseEventArgs e)
     {
         Point point = e.GetPosition(this);
         if (operate == PolygonROIOperateType.ReadyToDraw)
         {
             List tempList = PointCollection.ToList();
             if (tempList == null || tempList.Count == 0)
             {
                 return;
             }
             tempList.Add(point);
             tempList = SortPointsForClosedPolygon(tempList);
             roi.Draw(tempList);
         }
         else if (operate == PolygonROIOperateType.DrawDone)
         {
             var res = GetNearestPoint(point, PointCollection.ToList());
             if (res.Item1 >= 0 && res.Item2 <= 8) // 是否选中点
             {
                 this.Cursor = Cursors.Hand;
                 if (Mouse.LeftButton == MouseButtonState.Pressed)
                 {
                     double x = point.X;
                     double y = point.Y;
                     if (x < 5)
                     {
                         x = 5;
                     }
                     else if (x > this.ActualWidth - 5)
                     {
                         x = this.ActualWidth - 5;
                     }
                     if (y < 5)
                     {
                         y = 5;
                     }
                     else if (y > this.ActualHeight - 5)
                     {
                         y = this.ActualHeight - 5;
                     }
                     // 移动点
                     PointCollection[res.Item1] = new Point(x, y);
                     //对集合进行重新排序,保证多边形闭合且不自交
                     PointCollection = new ObservableCollection(SortPointsForClosedPolygon(PointCollection.ToList()));
                     roi.Draw(PointCollection.ToList());
                 }
             }
             else if (IsPointInPolygon(point, PointCollection.ToList())) // 是否在多边形内
             {
                 this.Cursor = Cursors.SizeAll;
                 if (Mouse.LeftButton == MouseButtonState.Pressed)
                 {
                     double minX = PointCollection.Min(p => p.X);
                     double minY = PointCollection.Min(p => p.Y);
                     double maxX = PointCollection.Max(p => p.X);
                     double maxY = PointCollection.Max(p => p.Y);
                     double stepX = point.X - lastPoint.X;
                     double stepY = point.Y - lastPoint.Y;
                     double finalXStep = stepX;
                     double finalYStep = stepY;
                     if (stepX < 0) // 左移
                     {
                         if (minX + stepX < 5)
                         {
                             finalXStep = 5 - minX;
                         }
                     }
                     else // 右
                     {
                         if (maxX + stepX > this.ActualWidth - 5)
                         {
                             finalXStep = this.ActualWidth - 5 - maxX;
                         }
                     }

                     if (stepY < 0) // 上
                     {
                         if (minY + stepY < 5)
                         {
                             finalYStep = 5 - minY;
                         }
                     }
                     else // 下
                     {
                         if (maxY + stepY > this.ActualHeight - 5)
                         {
                             finalYStep = this.ActualHeight - 5 - maxY;
                         }
                     }

                     PointCollection = new ObservableCollection(
                         PointCollection.Select(point => new Point(point.X + finalXStep, point.Y + finalYStep)));
                     roi.Draw(PointCollection.ToList());
                 }
             }
             else
             {
                 this.Cursor = Cursors.Arrow;
             }
         }
         lastPoint = point;
     }

平移顶点时要考虑边界问题,不要将顶点以及多边形区域移出canvas区域。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多