gpt4 book ai didi

WPF 图像平移、缩放和滚动 Canvas 上的图层

转载 作者:行者123 更新时间:2023-12-04 00:38:52 25 4
gpt4 key购买 nike

我希望有人可以在这里帮助我。我正在构建一个 WPF 成像应用程序,该应用程序从相机获取实时图像,允许用户查看图像,然后突出显示该图像上的感兴趣区域 (ROI)。然后将有关 ROI 的信息(宽度、高度、相对于图像上某个点的位置等)发送回相机,实际上是告诉/训练相机固件在哪里寻找条形码、文本、液位、转弯等信息图像上的螺钉等)。所需的功能是能够平移和缩放图像及其 ROI,以及在图像缩放到大于查看区域时进行滚动。 ROI 的 StrokeThickness 和 FontSize 需要保持原始比例,但 ROI 内形状的宽度和高度需要随图像缩放(这对于捕获精确的像素位置以传输到相机至关重要)。除了滚动和其他一些问题外,我已经解决了大部分问题。我关心的两个方面是:

  • 当我引入 ScrollViewer 时,我没有得到任何滚动行为。据我了解,我需要引入一个 LayoutTransform 以获得正确的 ScrollViewer 行为。但是,当我这样做时,其他区域开始崩溃(例如,ROI 没有在图像上保持正确的位置,或者鼠标指针在平移时开始远离图像上的选定点,或者我的图像的左角弹跳到 MouseDown 上的当前鼠标位置。)
  • 我不能完全按照我需要的方式来扩展我的投资返回率。我有这个工作,但它并不理想。我所拥有的没有保留确切的笔画粗细,而且我没有考虑忽略文本 block 上的比例。希望您会在代码示例中看到我在做什么。

  • 我确定我的问题与我对变换及其与 WPF 布局系统的关系缺乏了解有关。希望展示我到目前为止所完成的代码的再现会有所帮助(见下文)。

    仅供引用,如果建议使用装饰器,那在我的场景中可能不起作用,因为我最终可能会得到比支持更多的装饰器(谣言 144 装饰器是事情开始崩溃的时候)。

    首先,下面是显示具有 ROI(文本和形状)的图像的屏幕截图。矩形、椭圆和文本需要跟随图像上的区域进行缩放和旋转,但它们不应该在粗细或字体大小上缩放。

    Screen shot showing sample image with ROIs

    这是显示上图的 XAML,以及用于缩放的 slider (鼠标滚轮缩放稍后会出现)
    <Window x:Class="PanZoomStackOverflow.MainWindow"
    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"
    mc:Ignorable="d"
    Title="MainWindow" Height="768" Width="1024">

    <DockPanel>
    <Slider x:Name="_ImageZoomSlider" DockPanel.Dock="Bottom"
    Value="2"
    HorizontalAlignment="Center" Margin="6,0,0,0"
    Width="143" Minimum=".5" Maximum="20" SmallChange=".1"
    LargeChange=".2" TickFrequency="2"
    TickPlacement="BottomRight" Padding="0" Height="23"/>

    <!-- This resides in a user control in my solution -->
    <Grid x:Name="LayoutRoot">
    <ScrollViewer Name="border" HorizontalScrollBarVisibility="Auto"
    VerticalScrollBarVisibility="Auto">
    <Grid x:Name="_ImageDisplayGrid">
    <Image x:Name="_DisplayImage" Margin="2" Stretch="None"
    Source="Untitled.bmp"
    RenderTransformOrigin ="0.5,0.5"
    RenderOptions.BitmapScalingMode="NearestNeighbor"
    MouseLeftButtonDown="ImageScrollArea_MouseLeftButtonDown"
    MouseLeftButtonUp="ImageScrollArea_MouseLeftButtonUp"
    MouseMove="ImageScrollArea_MouseMove">
    <Image.LayoutTransform>
    <TransformGroup>
    <ScaleTransform />
    <TranslateTransform />
    </TransformGroup>
    </Image.LayoutTransform>
    </Image>
    <AdornerDecorator> <!-- Using this Adorner Decorator for Move, Resize and Rotation and feedback adornernments -->
    <Canvas x:Name="_ROICollectionCanvas"
    Width="{Binding ElementName=_DisplayImage, Path=ActualWidth, Mode=OneWay}"
    Height="{Binding ElementName=_DisplayImage, Path=ActualHeight, Mode=OneWay}"
    Margin="{Binding ElementName=_DisplayImage, Path=Margin, Mode=OneWay}">

    <!-- This is a user control in my solution -->
    <Grid IsHitTestVisible="False" Canvas.Left="138" Canvas.Top="58" Height="25" Width="186">
    <TextBlock Text="Rectangle ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
    Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
    <Rectangle StrokeThickness="2" Stroke="Orange"/>
    </Grid>

    <!-- This is a user control in my solution -->
    <Grid IsHitTestVisible="False" Canvas.Left="176" Canvas.Top="154" Height="65" Width="69">
    <TextBlock Text="Ellipse ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
    Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
    <Ellipse StrokeThickness="2" Stroke="Orange"/>
    </Grid>
    </Canvas>
    </AdornerDecorator>
    </Grid>
    </ScrollViewer>
    </Grid>
    </DockPanel>

    这是管理平移和缩放的 C#。
    public partial class MainWindow : Window
    {
    private Point origin;
    private Point start;
    private Slider _slider;

    public MainWindow()
    {
    this.InitializeComponent();

    //Setup a transform group that we'll use to manage panning of the image area
    TransformGroup group = new TransformGroup();
    ScaleTransform st = new ScaleTransform();
    group.Children.Add(st);
    TranslateTransform tt = new TranslateTransform();
    group.Children.Add(tt);
    //Wire up the slider to the image for zooming
    _slider = _ImageZoomSlider;
    _slider.ValueChanged += _ImageZoomSlider_ValueChanged;
    st.ScaleX = _slider.Value;
    st.ScaleY = _slider.Value;
    //_ImageScrollArea.RenderTransformOrigin = new Point(0.5, 0.5);
    //_ImageScrollArea.LayoutTransform = group;
    _DisplayImage.RenderTransformOrigin = new Point(0.5, 0.5);
    _DisplayImage.RenderTransform = group;
    _ROICollectionCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
    _ROICollectionCanvas.RenderTransform = group;
    }

    //Captures the mouse to prepare for panning the scrollable image area
    private void ImageScrollArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
    _DisplayImage.ReleaseMouseCapture();
    }

    //Moves/Pans the scrollable image area assuming mouse is captured.
    private void ImageScrollArea_MouseMove(object sender, MouseEventArgs e)
    {
    if (!_DisplayImage.IsMouseCaptured) return;

    var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);

    Vector v = start - e.GetPosition(border);
    tt.X = origin.X - v.X;
    tt.Y = origin.Y - v.Y;
    }

    //Cleanup for Move/Pan when mouse is released
    private void ImageScrollArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
    _DisplayImage.CaptureMouse();
    var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
    start = e.GetPosition(border);
    origin = new Point(tt.X, tt.Y);
    }

    //Zoom according to the slider changes
    private void _ImageZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
    //Panel panel = _ImageScrollArea;
    Image panel = _DisplayImage;

    //Set the scale coordinates on the ScaleTransform from the slider
    ScaleTransform transform = (ScaleTransform)((TransformGroup)panel.RenderTransform).Children.First(tr => tr is ScaleTransform);
    transform.ScaleX = _slider.Value;
    transform.ScaleY = _slider.Value;


    //Set the zoom (this will affect rotate too) origin to the center of the panel
    panel.RenderTransformOrigin = new Point(0.5, 0.5);

    foreach (UIElement child in _ROICollectionCanvas.Children)
    {
    //Assume all shapes are contained in a panel
    Panel childPanel = child as Panel;

    var x = childPanel.Children;

    //Shape width and heigh should scale, but not StrokeThickness
    foreach (var shape in childPanel.Children.OfType<Shape>())
    {
    if (shape.Tag == null)
    {
    //Hack: This is be a property on a usercontrol in my solution
    shape.Tag = shape.StrokeThickness;
    }
    double orignalStrokeThickness = (double)shape.Tag;

    //Attempt to keep the underlying shape border/stroke from thickening as well
    double newThickness = shape.StrokeThickness - (orignalStrokeThickness / transform.ScaleX);

    shape.StrokeThickness -= newThickness;
    }
    }
    }
    }

    假设没有剪切/粘贴错误,代码应该在 .NET 4.0 或 4.5 项目和解决方案中工作。

    有什么想法吗?欢迎提出建议。

    最佳答案

    行。这是我对你所描述的看法。

    它看起来像这样:

    enter image description here

  • 由于我没有应用任何 RenderTransforms ,因此我获得了所需的 Scrollbar/ScrollViewer 功能。
  • MVVM,这是进入 WPF 的方式。 UI 和数据是独立的,因此 DataItems 只有 X、Y、宽度、高度等的 doubleint 属性,您可以将它们用于任何目的,甚至将它们存储在数据库中。
  • 我在 Thumb 中添加了所有内容来处理平移。您仍然需要对通过 ResizerControl 拖动/调整 ROI 大小时发生的平移做一些事情。我想你可以检查一下 Mouse.DirectlyOver 什么的。
  • 我实际上使用了 ListBox 来处理 ROI,因此您可以在任何给定时间选择 1 个 ROI。这将切换调整大小功能。因此,如果您单击 ROI,您将看到调整大小。
  • 缩放在 ViewModel 级别处理,因此无需自定义 Panels 或类似的东西(尽管@Clemens 的解决方案也很好)
  • 我使用 Enum 和一些 DataTriggers 来定义形状。请参阅 DataTemplate DataType={x:Type local:ROI} 部分。
  • WPF 摇滚。只需将我的代码复制并粘贴到 File -> New Project -> WPF Application 中,然后自己查看结果。
    <Window x:Class="MiscSamples.PanZoomStackOverflow_MVVM"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MiscSamples"
    Title="PanZoomStackOverflow_MVVM" Height="300" Width="300">
    <Window.Resources>
    <DataTemplate DataType="{x:Type local:ROI}">
    <Grid Background="#01FFFFFF">
    <Path x:Name="Path" StrokeThickness="2" Stroke="Black"
    Stretch="Fill"/>
    <local:ResizerControl Visibility="Collapsed" Background="#30FFFFFF"
    X="{Binding X}" Y="{Binding Y}"
    ItemWidth="{Binding Width}"
    ItemHeight="{Binding Height}"
    x:Name="Resizer"/>
    </Grid>
    <DataTemplate.Triggers>
    <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
    <Setter TargetName="Resizer" Property="Visibility" Value="Visible"/>
    </DataTrigger>
    <DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Square}">
    <Setter TargetName="Path" Property="Data">
    <Setter.Value>
    <RectangleGeometry Rect="0,0,10,10"/>
    </Setter.Value>
    </Setter>
    </DataTrigger>

    <DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Round}">
    <Setter TargetName="Path" Property="Data">
    <Setter.Value>
    <EllipseGeometry RadiusX="10" RadiusY="10"/>
    </Setter.Value>
    </Setter>
    </DataTrigger>
    </DataTemplate.Triggers>
    </DataTemplate>

    <Style TargetType="ListBox" x:Key="ROIListBoxStyle">
    <Setter Property="ItemsPanel">
    <Setter.Value>
    <ItemsPanelTemplate>
    <Canvas/>
    </ItemsPanelTemplate>
    </Setter.Value>
    </Setter>
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate>
    <ItemsPresenter/>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>
    <Style TargetType="ListBoxItem" x:Key="ROIItemStyle">
    <Setter Property="Canvas.Left" Value="{Binding ActualX}"/>
    <Setter Property="Canvas.Top" Value="{Binding ActualY}"/>
    <Setter Property="Height" Value="{Binding ActualHeight}"/>
    <Setter Property="Width" Value="{Binding ActualWidth}"/>

    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate TargetType="ListBoxItem">
    <ContentPresenter ContentSource="Content"/>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>

    </Window.Resources>

    <DockPanel>
    <Slider VerticalAlignment="Center"
    Maximum="2" Minimum="0" Value="{Binding ScaleFactor}" SmallChange=".1"
    DockPanel.Dock="Bottom"/>

    <ScrollViewer VerticalScrollBarVisibility="Visible"
    HorizontalScrollBarVisibility="Visible" x:Name="scr"
    ScrollChanged="ScrollChanged">
    <Thumb DragDelta="Thumb_DragDelta">
    <Thumb.Template>
    <ControlTemplate>
    <Grid>
    <Image Source="/Images/Homer.jpg" Stretch="None" x:Name="Img"
    VerticalAlignment="Top" HorizontalAlignment="Left">
    <Image.LayoutTransform>
    <TransformGroup>
    <ScaleTransform ScaleX="{Binding ScaleFactor}" ScaleY="{Binding ScaleFactor}"/>
    </TransformGroup>
    </Image.LayoutTransform>
    </Image>

    <ListBox ItemsSource="{Binding ROIs}"
    Width="{Binding ActualWidth, ElementName=Img}"
    Height="{Binding ActualHeight,ElementName=Img}"
    VerticalAlignment="Top" HorizontalAlignment="Left"
    Style="{StaticResource ROIListBoxStyle}"
    ItemContainerStyle="{StaticResource ROIItemStyle}"/>
    </Grid>
    </ControlTemplate>
    </Thumb.Template>
    </Thumb>
    </ScrollViewer>
    </DockPanel>


  • 代码背后:
    public partial class PanZoomStackOverflow_MVVM : Window
    {
    public PanZoomViewModel ViewModel { get; set; }

    public PanZoomStackOverflow_MVVM()
    {
    InitializeComponent();
    DataContext = ViewModel = new PanZoomViewModel();

    ViewModel.ROIs.Add(new ROI() {ScaleFactor = ViewModel.ScaleFactor, X = 150, Y = 150, Height = 200, Width = 200, Shape = Shapes.Square});

    ViewModel.ROIs.Add(new ROI() { ScaleFactor = ViewModel.ScaleFactor, X = 50, Y = 230, Height = 102, Width = 300, Shape = Shapes.Round });
    }

    private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
    //TODO: Detect whether a ROI is being resized / dragged and prevent Panning if so.
    IsPanning = true;
    ViewModel.OffsetX = (ViewModel.OffsetX + (((e.HorizontalChange/10) * -1) * ViewModel.ScaleFactor));
    ViewModel.OffsetY = (ViewModel.OffsetY + (((e.VerticalChange/10) * -1) * ViewModel.ScaleFactor));

    scr.ScrollToVerticalOffset(ViewModel.OffsetY);
    scr.ScrollToHorizontalOffset(ViewModel.OffsetX);

    IsPanning = false;
    }

    private bool IsPanning { get; set; }

    private void ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
    if (!IsPanning)
    {
    ViewModel.OffsetX = e.HorizontalOffset;
    ViewModel.OffsetY = e.VerticalOffset;
    }
    }
    }

    主视图模型:
    public class PanZoomViewModel:PropertyChangedBase
    {
    private double _offsetX;
    public double OffsetX
    {
    get { return _offsetX; }
    set
    {
    _offsetX = value;
    OnPropertyChanged("OffsetX");
    }
    }

    private double _offsetY;
    public double OffsetY
    {
    get { return _offsetY; }
    set
    {
    _offsetY = value;
    OnPropertyChanged("OffsetY");
    }
    }

    private double _scaleFactor = 1;
    public double ScaleFactor
    {
    get { return _scaleFactor; }
    set
    {
    _scaleFactor = value;
    OnPropertyChanged("ScaleFactor");
    ROIs.ToList().ForEach(x => x.ScaleFactor = value);
    }
    }

    private ObservableCollection<ROI> _rois;
    public ObservableCollection<ROI> ROIs
    {
    get { return _rois ?? (_rois = new ObservableCollection<ROI>()); }
    }
    }

    投资返回率 View 模型:
    public class ROI:PropertyChangedBase
    {
    private Shapes _shape;
    public Shapes Shape
    {
    get { return _shape; }
    set
    {
    _shape = value;
    OnPropertyChanged("Shape");
    }
    }

    private double _scaleFactor;
    public double ScaleFactor
    {
    get { return _scaleFactor; }
    set
    {
    _scaleFactor = value;
    OnPropertyChanged("ScaleFactor");
    OnPropertyChanged("ActualX");
    OnPropertyChanged("ActualY");
    OnPropertyChanged("ActualHeight");
    OnPropertyChanged("ActualWidth");
    }
    }

    private double _x;
    public double X
    {
    get { return _x; }
    set
    {
    _x = value;
    OnPropertyChanged("X");
    OnPropertyChanged("ActualX");
    }
    }

    private double _y;
    public double Y
    {
    get { return _y; }
    set
    {
    _y = value;
    OnPropertyChanged("Y");
    OnPropertyChanged("ActualY");
    }
    }

    private double _height;
    public double Height
    {
    get { return _height; }
    set
    {
    _height = value;
    OnPropertyChanged("Height");
    OnPropertyChanged("ActualHeight");
    }
    }

    private double _width;
    public double Width
    {
    get { return _width; }
    set
    {
    _width = value;
    OnPropertyChanged("Width");
    OnPropertyChanged("ActualWidth");
    }
    }

    public double ActualX { get { return X*ScaleFactor; }}
    public double ActualY { get { return Y*ScaleFactor; }}
    public double ActualWidth { get { return Width*ScaleFactor; }}
    public double ActualHeight { get { return Height * ScaleFactor; } }
    }

    形状枚举:
    public enum Shapes
    {
    Round = 1,
    Square = 2,
    AnyOther
    }

    PropertyChangedBase(MVVM Helper 类):
        public class PropertyChangedBase:INotifyPropertyChanged
    {
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
    Application.Current.Dispatcher.BeginInvoke((Action) (() =>
    {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }));
    }
    }

    调整大小控制:
    <UserControl x:Class="MiscSamples.ResizerControl"
    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"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
    <Thumb DragDelta="Center_DragDelta" Height="10" Width="10"
    VerticalAlignment="Center" HorizontalAlignment="Center"/>

    <Thumb DragDelta="UpperLeft_DragDelta" Height="10" Width="10"
    VerticalAlignment="Top" HorizontalAlignment="Left"/>

    <Thumb DragDelta="UpperRight_DragDelta" Height="10" Width="10"
    VerticalAlignment="Top" HorizontalAlignment="Right"/>

    <Thumb DragDelta="LowerLeft_DragDelta" Height="10" Width="10"
    VerticalAlignment="Bottom" HorizontalAlignment="Left"/>

    <Thumb DragDelta="LowerRight_DragDelta" Height="10" Width="10"
    VerticalAlignment="Bottom" HorizontalAlignment="Right"/>

    </Grid>
    </UserControl>

    代码背后:
     public partial class ResizerControl : UserControl
    {
    public static readonly DependencyProperty XProperty = DependencyProperty.Register("X", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public static readonly DependencyProperty YProperty = DependencyProperty.Register("Y", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public double X
    {
    get { return (double) GetValue(XProperty); }
    set { SetValue(XProperty, value); }
    }

    public double Y
    {
    get { return (double)GetValue(YProperty); }
    set { SetValue(YProperty, value); }
    }

    public double ItemHeight
    {
    get { return (double) GetValue(ItemHeightProperty); }
    set { SetValue(ItemHeightProperty, value); }
    }

    public double ItemWidth
    {
    get { return (double) GetValue(ItemWidthProperty); }
    set { SetValue(ItemWidthProperty, value); }
    }

    public ResizerControl()
    {
    InitializeComponent();
    }

    private void UpperLeft_DragDelta(object sender, DragDeltaEventArgs e)
    {
    X = X + e.HorizontalChange;
    Y = Y + e.VerticalChange;

    ItemHeight = ItemHeight + e.VerticalChange * -1;
    ItemWidth = ItemWidth + e.HorizontalChange * -1;
    }

    private void UpperRight_DragDelta(object sender, DragDeltaEventArgs e)
    {
    Y = Y + e.VerticalChange;

    ItemHeight = ItemHeight + e.VerticalChange * -1;
    ItemWidth = ItemWidth + e.HorizontalChange;
    }

    private void LowerLeft_DragDelta(object sender, DragDeltaEventArgs e)
    {
    X = X + e.HorizontalChange;

    ItemHeight = ItemHeight + e.VerticalChange;
    ItemWidth = ItemWidth + e.HorizontalChange * -1;
    }

    private void LowerRight_DragDelta(object sender, DragDeltaEventArgs e)
    {
    ItemHeight = ItemHeight + e.VerticalChange;
    ItemWidth = ItemWidth + e.HorizontalChange;
    }

    private void Center_DragDelta(object sender, DragDeltaEventArgs e)
    {
    X = X + e.HorizontalChange;
    Y = Y + e.VerticalChange;
    }
    }

    关于WPF 图像平移、缩放和滚动 Canvas 上的图层,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16930074/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com