15

誰かがここで私を助けてくれることを願っています。カメラからライブ画像を取得して、ユーザーが画像を表示し、その後その画像の関心領域 (ROI) を強調表示する WPF 画像処理アプリケーションを構築しています。次に、ROI に関する情報 (幅、高さ、画像上のポイントに対する位置など) がカメラに送り返され、バーコード、テキスト、液体レベル、ターンなどを探す場所をカメラ ファームウェアに指示/トレーニングします。画像のネジなど)。望ましい機能は、画像とその ROI をパンおよびズームする機能と、画像が表示領域よりも大きくズームされたときにスクロールする機能です。ROI の StrokeThickness と FontSize は、元のスケールを維持する必要があります。ただし、ROI 内の形状の幅と高さは、画像に合わせてスケーリングする必要があります (これは、正確なピクセル位置をキャプチャしてカメラに送信するために重要です)。スクロールとその他のいくつかの問題を除いて、これのほとんどはうまくいきました。私の懸念事項は次の 2 つです。

  1. ScrollViewer を導入すると、スクロール動作が得られません。私が理解しているように、正しい ScrollViewer の動作を得るには LayoutTransform を導入する必要があります。ただし、これを行うと、他の領域が壊れ始めます (たとえば、ROI が画像上で正しい位置を保持しない、またはパン時にマウス ポインターが画像上の選択したポイントから離れ始める、または画像の左隅) MouseDown の現在のマウス位置にバウンスします。)

  2. ROI を必要な方法でスケーリングすることができません。私はこれを機能させていますが、理想的ではありません。私が持っているものは正確なストロークの太さを保持しておらず、テキストブロックのスケールを無視することを検討していません. コード サンプルで私が何をしているかを確認していただければ幸いです。

私の問題は、変換と WPF レイアウト システムとの関係を理解し​​ていないことに関係していると確信しています。これまでに達成したことを示すコードの表現が役立つことを願っています (以下を参照)。

参考までに、Adorners が提案されている場合、サポートされているよりも多くの adorners になる可能性があるため、私のシナリオでは機能しない可能性があります (144 個の adorners は、物事が崩壊し始めるときの噂です)。

まず、以下は ROI (テキストと形状) を含む画像を示すスクリーンショットです。長方形、楕円、およびテキストは、拡大縮小と回転で画像上の領域に従う必要がありますが、太さやフォントサイズを拡大縮小してはいけません。

ROI を含むサンプル画像を示すスクリーン ショット

上記の画像を表示している XAML と、ズーム用のスライダーを次に示します (マウスホイールによるズームは後で行います)。

<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 プロジェクトおよびソリューションで機能するはずです。

何かご意見は?提案は大歓迎です。

4

3 に答える 3

1

ストロークの太さを変えずにシェイプを変形するには、Pathジオメトリを変形したオブジェクトを使用できます。

次の XAML は、Image と 2 つのパスを Canvas に配置します。Image は RenderTransform によってスケーリングおよび変換されます。同じ変換がTransform、2 つのパスのジオメトリのプロパティにも使用されます。

<Canvas>
    <Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg">
        <Image.RenderTransform>
            <TransformGroup x:Name="transform">
                <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
                <TranslateTransform X="100" Y="50"/>
            </TransformGroup>
        </Image.RenderTransform>
    </Image>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <RectangleGeometry Rect="50,100,100,50"
                               Transform="{Binding ElementName=transform}"/>
        </Path.Data>
    </Path>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
                             Transform="{Binding ElementName=transform}"/>
        </Path.Data>
    </Path>
</Canvas>

アプリケーションは、transformMouseMove や MouseWheel などの入力イベントに応答してオブジェクトを変更するだけです。

適切な場所に移動するだけで、スケーリングする必要のない TextBlocks やその他の要素も変換する場合は、少し複雑になります。

この種の変換を子要素に適用できる特殊な Panel を作成できます。このような Panel は、子要素の位置を制御する添付プロパティを定義し、子のRenderTransformまたはLayoutTransformの代わりにこの位置に変換を適用します。

これにより、そのようなパネルをどのように実装できるかがわかります。

public class TransformPanel : Panel
{
    public static readonly DependencyProperty TransformProperty =
        DependencyProperty.Register(
            "Transform", typeof(Transform), typeof(TransformPanel),
            new FrameworkPropertyMetadata(Transform.Identity,
                FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty PositionProperty =
        DependencyProperty.RegisterAttached(
            "Position", typeof(Point?), typeof(TransformPanel),
            new PropertyMetadata(PositionPropertyChanged));

    public Transform Transform
    {
        get { return (Transform)GetValue(TransformProperty); }
        set { SetValue(TransformProperty, value); }
    }

    public static Point? GetPosition(UIElement element)
    {
        return (Point?)element.GetValue(PositionProperty);
    }

    public static void SetPosition(UIElement element, Point? value)
    {
        element.SetValue(PositionProperty, value);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        var infiniteSize = new Size(double.PositiveInfinity,
                                    double.PositiveInfinity);

        foreach (UIElement element in InternalChildren)
        {
            element.Measure(infiniteSize);
        }

        return new Size();
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement element in InternalChildren)
        {
            ArrangeElement(element, GetPosition(element));
        }

        return finalSize;
    }

    private void ArrangeElement(UIElement element, Point? position)
    {
        var arrangeRect = new Rect(element.DesiredSize);

        if (position.HasValue && Transform != null)
        {
            arrangeRect.Location = Transform.Transform(position.Value);
        }

        element.Arrange(arrangeRect);
    }

    private static void PositionPropertyChanged(
        DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var element = (UIElement)obj;
        var panel = VisualTreeHelper.GetParent(element) as TransformPanel;

        if (panel != null)
        {
            panel.ArrangeElement(element, (Point?)e.NewValue);
        }
    }
}

XAML では次のように使用されます。

<local:TransformPanel>
    <local:TransformPanel.Transform>
        <TransformGroup>
            <ScaleTransform ScaleX="0.5" ScaleY="0.5" x:Name="scale"/>
            <TranslateTransform X="100"/>
        </TransformGroup>
    </local:TransformPanel.Transform>
    <Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"
           RenderTransform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <RectangleGeometry Rect="50,100,100,50"
                               Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
        </Path.Data>
    </Path>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
                             Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
        </Path.Data>
    </Path>
    <TextBlock Text="Rectangle" local:TransformPanel.Position="50,150"/>
    <TextBlock Text="Ellipse" local:TransformPanel.Position="200,150"/>
</local:TransformPanel>
于 2013-06-05T11:15:34.250 に答える