Обобщение ScaleTransform (WPF), когда изображение не выровнено по осям XY
Моя линейная алгебра слаба. WPF - отличная система для рендеринга различных трансформаций на изображении. Однако стандартный ScaleTransform будет масштабировать изображения только по осям XY. Когда края сначала повернуты, результат применения ScaleTransform приведет к перекосу преобразования (как показано ниже), поскольку края больше не выровнены.
Итак, если у меня есть изображение, которое подверглось нескольким различным преобразованиям, а результат отображается системой рендеринга WPF, как рассчитать правильное матричное преобразование, чтобы взять (окончательно повернутое изображение) и масштабировать его по осям визуализированного изображения?
Любая помощь или предложения будут оценены по достоинству.
ТИА
(Полный код см. В моем предыдущем вопросе.)
Edit # 1: чтобы увидеть вышеупомянутый эффект:
- Перетащите изображение на Inkcavas. - не видно перекоса.
- Поверните изображение против часовой стрелки (примерно до 45 градусов) - не наблюдается перекоса.
- Увеличьте изображение (примерно вдвое больше его предварительно масштабированного размера - искажения не видно).
- Поверните изображение по часовой стрелке (примерно туда, откуда оно началось) - перекос сразу виден во время и после поворота.
Если шаг 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
Примечание. Я добавляю каждое новое преобразование в первый список детей. Это облегчает вычисления на изображении.
1 ответ
Я не смог найти в Google или в тексте все элементы, необходимые для полного ответа на этот вопрос. Итак, для всех остальных новичков, таких как я, я опубликую этот (очень длинный) ответ. (Редакторы и Гуру, пожалуйста, не стесняйтесь исправлять).
Слово к настройке. У меня есть чернильный холст, на который изображение падает и добавляется как потомок чернильного холста. Во время отбрасывания для окончательного позиционирования изображения добавляется украшение, содержащее большой палец в каждом углу для изменения размера, верхний-средний большой палец для поворота и средний большой палец для перевода. Наряду с "контуром", сконструированным как элемент пути, большие пальцы и контур завершают Adorner и создают своего рода проволочную рамку вокруг украшенного элемента.
Есть несколько ключевых моментов:
- Сначала WPF использует проход макета для размещения элементов в их родительском контейнере, а затем проход рендеринга для упорядочивания элемента. Преобразования могут применяться к одному или обоим проходам макета и рендеринга. Тем не менее, следует отметить, что этап макета использует систему координат xy с началом координат в верхнем левом углу родительского элемента, где в качестве системы рендеринга по своей сути ссылается верхний левый угол дочернего элемента. Если положение макета отброшенного элемента не определено конкретно, оно будет по умолчанию добавлено к "источнику" родительского контейнера.
- RenderTransform по умолчанию является MatrixTransform, но может быть заменен TransformGroup. Использование одного или обоих из них позволяет применять матрицы (в MatrixTransform) или Transforms (в TransformGroup) в любом порядке. Я предпочел использовать MatrixTransforms, чтобы лучше видеть взаимосвязь между масштабированием, вращением и переводом.
Рендеринг украшателя следует за элементом, который он украшает. То есть рендеринг элемента также будет применен к Adorner. Это поведение может быть отменено с помощью
public override 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.
};
Просто чтобы прояснить, моя "Матрица роста" определена таким образом, чтобы привести к "положительному" росту, когда курсор отодвинут от центра изображения. Например, угол TopLeft будет "увеличивать" изображение при перемещении влево и вверх. следовательно
матрица роста = новая матрица (-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);
Наконец, поскольку я масштабирую от угла, необходимо переместить центр изображения, чтобы угол оставался под курсором.
Извините за длину этого ответа, но есть много, чтобы покрыть (и учиться:)). Надеюсь, это кому-нибудь поможет.