3

我的线性代数很弱。WPF 是一个很棒的系统,用于在图像上呈现不同的转换。但是,标准 ScaleTransform 只会沿 xy 轴缩放图像。当边缘第一次旋转时,应用 ScaleTransform 的结果将导致倾斜变换(如下所示),因为边缘不再对齐。

因此,如果我的图像经过了几次不同的变换,结果由 WPF 渲染系统显示,我如何计算正确的矩阵变换以获取(最终旋转的图像)并沿渲染图像的轴缩放? 在此处输入图像描述

任何帮助或建议将不胜感激。

TIA

(完整代码请看我之前的问题。)

编辑#1:要查看上述效果:

  1. 将图像拖放到 Inkcavas 上。- 没有看到歪斜。
  2. 逆时针旋转图像(大约 45 度)——看不到歪斜。
  3. 使图像更大(大约是其预缩放尺寸的两倍——看不到歪斜。
  4. 顺时针旋转图像(大约回到它开始的位置)——在旋转期间和之后立即看到倾斜。

如果跳过第 3 步,简单的旋转——无论完成多少次——都不会导致倾斜效果。其实,这是有道理的。ScaleTransform 保留从中心到图像边缘的距离。如果图像处于某个角度,则与变换边缘的 xy 距离在渲染图像的宽度和长度上不再是恒定的。所以边缘得到适当的缩放,但角度发生了变化。

这是最相关的代码:

private ImageResizing(Image image)
        {
            if (image == null)
                throw new ArgumentNullException("image");

           _image = image;
            TransformGroup tg = new TransformGroup();

            image.RenderTransformOrigin = new Point(0.5, 0.5);  // All transforms will be based on the center of the rendered element.
            tg.Children.Add(image.RenderTransform);             // Keeps whatever transforms have already been applied.
            image.RenderTransform = tg; 
            _adorner = new MyImageAdorner(image);               // Create the adorner.

            InstallAdorner();                                   // Get the Adorner Layer and add the Adorner.
        }

注意:image.RenderTransformOrigin = new Point(0.5, 0.5) 设置为渲染图像的中心。所有变换都将基于变换时图像的中心。

 public MyImageAdorner(UIElement adornedElement)
      : base(adornedElement)
    {
        visualChildren = new VisualCollection(this);


        // Initialize the Movement and Rotation thumbs.
        BuildAdornerRotate(ref moveHandle, Cursors.SizeAll);
        BuildAdornerRotate(ref rotateHandle, Cursors.Hand);

        // Add handlers for move and rotate.
        moveHandle.DragDelta += new DragDeltaEventHandler(moveHandle_DragDelta);
        moveHandle.DragCompleted += new DragCompletedEventHandler(moveHandle_DragCompleted);
        rotateHandle.DragDelta += new DragDeltaEventHandler(rotateHandle_DragDelta);
        rotateHandle.DragCompleted += new DragCompletedEventHandler(rotateHandle_DragCompleted);


        // Initialize the Resizing (i.e., corner) thumbs with specialized cursors.
        BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);

        // Add handlers for resizing.
        topLeft.DragDelta += new DragDeltaEventHandler(TopLeft_DragDelta);

        topLeft.DragCompleted += TopLeft_DragCompleted;

        // Put the outline border arround the image. The outline will be moved by the DragDelta's
        BorderTheImage();
    }

  #region [Rotate]
    /// <summary>
    /// Rotate the Adorner Outline about its center point. The Outline rotation will be applied to the image
    /// in the DragCompleted event.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void rotateHandle_DragDelta(object sender, DragDeltaEventArgs e)
    {
        // Get the position of the mouse relative to the Thumb.  (All cooridnates in Render Space)
        Point pos = Mouse.GetPosition(this);

        // Render origin is set at center of the adorned element. (all coordinates are in rendering space).
        double CenterX = AdornedElement.RenderSize.Width / 2;
        double CenterY = AdornedElement.RenderSize.Height / 2;

        double deltaX = pos.X - CenterX;
        double deltaY = pos.Y - CenterY;

        double angle;
        if (deltaY.Equals(0))
        {
            if (!deltaX.Equals(0))
                angle = 90;
            else
                return;

        }
        else
        {
            double tan = deltaX / deltaY;
            angle = Math.Atan(tan);  angle = angle * 180 / Math.PI;
        }

        // If the mouse crosses the vertical center, 
        // find the complementary angle.
        if (deltaY > 0)
            angle = 180 - Math.Abs(angle);

        // Rotate left if the mouse moves left and right
        // if the mouse moves right.
        if (deltaX < 0)
            angle = -Math.Abs(angle);
        else
            angle = Math.Abs(angle);

        if (double.IsNaN(angle))
            return;

        // Apply the rotation to the outline.  All Transforms are set to Render Center.
        rotation.Angle = angle;
        rotation.CenterX = CenterX;
        rotation.CenterY = CenterY;
        outline.RenderTransform = rotation;
    }

    /// Rotates image to the same angle as outline arround the render origin.
    void rotateHandle_DragCompleted(object sender, DragCompletedEventArgs e)
    {
        // Get Rotation Angle from outline. All element rendering is set to rendering center.
        RotateTransform _rt = outline.RenderTransform as RotateTransform;

        // Add RotateTransform to the adorned element.
        TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
        RotateTransform rT = new RotateTransform(_rt.Angle);
        gT.Children.Insert(0, rT);
        AdornedElement.RenderTransform = gT;

        outline.RenderTransform = Transform.Identity;  // clear transform from outline.
    }
    #endregion  //Rotate


 #region [TopLeft Corner
    // Top Left Corner is being dragged. Anchor is Bottom Right.
    void TopLeft_DragDelta(object sender, DragDeltaEventArgs e)
    {
        ScaleTransform sT = new ScaleTransform(1 - e.HorizontalChange / outline.ActualWidth, 1 - e.VerticalChange / outline.ActualHeight,
            outline.ActualWidth, outline.ActualHeight);

        outline.RenderTransform = sT;   // This will immediately show the new outline without changing the Image.
    }



    /// The resizing outline for the TopLeft is based on the bottom right-corner. The resizing transform for the
    /// element, however, is based on the render origin being in the center. Therefore, the Scale transform 
    /// received from the outling must be recalculated to have the same effect--only from the rendering center.
    /// 
    /// TopLeft_DragCompleted resize the element rendering.
    private void TopLeft_DragCompleted(object sender, DragCompletedEventArgs e)
    {
        // Get new scaling from the Outline.
        ScaleTransform _sT = outline.RenderTransform as ScaleTransform;
        scale.ScaleX *= _sT.ScaleX; scale.ScaleY *= _sT.ScaleY;

        Point Center = new Point(AdornedElement.RenderSize.Width/2, AdornedElement.RenderSize.Height/2);

        TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;

        ScaleTransform sT = new ScaleTransform( _sT.ScaleX, _sT.ScaleY, Center.X, Center.Y);
        gT.Children.Insert(0, sT);

        AdornedElement.RenderTransform = gT;
        outline.RenderTransform = Transform.Identity;           // Clear outline transforms. (Same as null).
    }
    #endregion

注意:我将每个新转换添加到子列表的第一个。这使得对图像的计算更容易。

4

1 回答 1

2

我无法通过 Google 或在文本中找到完全回答这个问题所需的所有元素。所以,对于像我这样的所有其他新手,我会发布这个(很长的)答案。(编辑和大师请随时纠正)。

关于设置的一句话。我有一个 inkcanvas,将图像拖放到其上并作为 inkcanvas 的子项添加。在放置的时候,一个装饰器在每个角落包含一个用于调整大小的拇指,一个用于旋转的顶部-中间拇指和一个用于平移的中间拇指,用于最终定位图像。除了设计为路径元素的“轮廓”外,拇指和轮廓完成了装饰器,并在装饰元素周围创建了一种线框。

有多个关键点:

  1. WPF 首先使用布局传递在其父容器中定位元素,然后使用呈现传递来排列元素。变换可以应用于布局和渲染通道中的一个或两个。但是,需要注意的是,布局传递使用 xy 坐标系,原点位于父元素的左上角,而渲染系统固有地引用子元素的左上角。如果放置元素的布局位置没有特别定义,默认会添加到父容器的“原点”。
  2. 默认情况下,RenderTransform 是一个 MatrixTransform,但可以替换为一个 TransformGroup。使用其中一个或两个允许以任何顺序应用矩阵(在 MatrixTransform 中)或变换(在 TransformGroup 中)。我的偏好是使用 MatrixTransforms 来更好地了解缩放、旋转和平移之间的关系。
  3. 装饰器的渲染遵循它所装饰的元素。也就是说,元素的渲染也会应用到 Adorner。可以通过使用覆盖此行为

    公共覆盖 GeneralTransform GetDesiredTransform(GeneralTransform 变换)

如最初的问题所述,我避免使用 SetTop() 和 SetLeft() 因为它们弄乱了我的其他矩阵。事后看来,我的矩阵失败的原因是因为 SetTop() 和 SetLeft() 显然在布局阶段工作——所以我所有的渲染坐标都关闭了。(我使用 TransalateTransform 在拖放时定位图像。)但是,使用 SetTop() 和 SetLeft() 显然在布局阶段起作用。使用它大大简化了渲染阶段所需的计算,因为所有坐标都可以引用图像,而无需考虑画布上的位置。

private void IC_Drop(object sender, DragEventArgs e)
    {
        InkCanvas ic = sender as InkCanvas;

        // Setting InkCanvasEditingMode.None is necessary to capture DrawingLayer_MouseDown.
        ic.EditingMode = InkCanvasEditingMode.None;

        ImageInfo image_Info = e.Data.GetData(typeof(ImageInfo)) as ImageInfo;
        if (image_Info != null)
        {
            // Display enlarged image on ImageLayer
            // This is the expected format for the Uri:
            //      ImageLayer.Source = new BitmapImage(new Uri("/Images/Female - Front.png", UriKind.Relative));
            // Source = new BitmapImage(image_Info.Uri);

            Image image = new Image();
            image.Width = image_Info.Width * 4;

            // Stretch.Uniform keeps the Aspect Ratio but totally screws up resizing the image.
            // Stretch.Fill allows for resizing the Image without keeping the Aspect Ratio.    
            image.Stretch = Stretch.Fill;
            image.Source = new BitmapImage(image_Info.Uri);

            // Position the drop. Note that SetLeft and SetTop are active during the Layout phase of the image drop and will
            // be applied before the Image hits its Rendering stage.
            Point position = e.GetPosition(ic);
            InkCanvas.SetLeft(image, position.X);
            InkCanvas.SetTop(image, position.Y);
            ic.Children.Add(image);
            ImageResizing imgResize = ImageResizing.Create(image);
        }
    }

由于我希望能够从任何方向调整图像大小,因此使用 Stretch.Fill 设置图像。当使用 Stretch.Uniform 时,图像似乎首先被调整大小,然后跳回其初始大小。

  • 由于我使用的是 MatrixTransform,因此矩阵的顺序很重要。所以在应用矩阵时,供我使用

       // Make new render transform. The Matrix order of multiplication is extremely important.
            // Scaling should be done first, followed by (skewing), rotation and translation -- in 
            // that order.
            MatrixTransform gT = new MatrixTransform
            {
                Matrix = sM * rM * tM
            };
    
            ele.RenderTransform = gT;
    

缩放 (sM) 在旋转 (rM) 之前执行。最后应用翻译。(C# 从左到右进行矩阵乘法)。

回顾矩阵,很明显旋转矩阵也涉及倾斜元素。(这是有道理的,因为显然 RotationTransform 旨在保持边缘的角度不变)。因此,旋转矩阵取决于图像的大小。

在我的情况下,旋转后缩放导致倾斜的原因是因为缩放变换乘以图像点与 xy 轴之间的距离。因此,如果图像的边缘与 xy 轴的距离不是恒定的,缩放将扭曲(即歪斜)图像。

将它们放在一起,会产生以下调整图像大小的方法:

Action<Matrix, Vector> DragCompleted = (growthMatrix, v) =>
        {
            var ele = AdornedElement;

            // Get the change vector.  Transform (i.e, Rotate) change vector into x-y axes.
            // The Horizontal and Vertical changes give the distance between the the current cursor position
            // and the Thumb.
            Matrix m = new Matrix();
            m.Rotate(-AngleDeg);
            Vector v1 = v * m;

            // Calculate Growth Vector.
            var gv = v1 * growthMatrix;

            // Apply new scaling along the x-y axes to obtain the rendered size. 
            // Use the current Image size as the reference to calculate the new scaling factors.
            var scaleX = sM.M11; var scaleY = sM.M22;
            var W = ele.RenderSize.Width * scaleX; var H = ele.RenderSize.Height * scaleY;
            var sx = 1 + gv.X/ W; var sy = 1 + gv.Y / H;

            // Change ScalingTransform by applying the new scaling factors to the existing scaling transform.
            // Do not add offsets to the scaling transform matrix as they will be included in future scalings.
            // With RenderTransformOrigin set to the image center (0.5, 0.5), scalling occurs from the center out.
            // Move the new center of the new resized image to its correct position such that the image's thumb stays
            // underneath the cursor.
            sM.Scale(sx, sy);


            tM.Translate(v.X / 2, v.Y / 2);


            // New render transform. The order of the transform's is extremely important.
            MatrixTransform gT = new MatrixTransform
            {
                Matrix = sM * rM * tM
            };
            ele.RenderTransform = gT;
            outline.RenderTransform = Transform.Identity;  // clear this transform from the outline.

        };

为了清楚起见,我的“增长矩阵”的定义方式是当光标远离图像中心时会导致“正”增长。例如,左上角将在向左和向上移动时“增长”图像。因此

增长矩阵 = new Matrix(-1, 0, 0, -1, 0, 0) 用于左上角。

最后一个问题是正确计算旋转中心(即我想旋转,而不是轨道)。这通过使用大大简化了

  // All transforms will be based on the center of the rendered element.
        AdornedElement.RenderTransformOrigin = new Point(0.5, 0.5);

最后,由于我是从一个角落缩放,所以需要平移图像的中心以将角落保持在光标下方。

很抱歉这个答案的长度,但有很多东西要涵盖(和学习:))。希望这可以帮助某人。

于 2017-07-12T03:06:36.797 回答