Поделиться через


Неаффинные преобразования

Создание эффектов перспективы и касания с помощью третьего столбца матрицы преобразования

Преобразование, масштабирование, поворот и перемыка все классифицируются как аффинные преобразования. Аффинные преобразования сохраняют параллельные линии. Если две строки параллельны перед преобразованием, они остаются параллельными после преобразования. Прямоугольники всегда преобразуются в параллелограммы.

Тем не менее, SkiaSharp также способен неаффинные преобразования, которые имеют возможность преобразовать прямоугольник в любой выпуклый квадрилятор:

Растровое изображение, преобразованное в выпуклый квадратик

Выпуклая четырехсторонняя фигура с углами интерьера всегда меньше 180 градусов и сторон, которые не пересекают друг друга.

Неаффинные преобразования приводят к результату, если третья строка матрицы преобразования имеет значения, отличные от 0, 0 и 1. SKMatrix Полное умножение:

              │ ScaleX  SkewY   Persp0 │
| x  y  1 | × │ SkewX   ScaleY  Persp1 │ = | x'  y'  z' |
              │ TransX  TransY  Persp2 │

Результирующий формулы преобразования:

x' = ScaleX·x + SkewX·y + TransX

y' = SkewY·x + ScaleY·y + TransY

z' = Persp0·x + Persp1·y + Persp2

Основное правило использования матрицы 3-к-3 для двухмерных преобразований заключается в том, что все остается на плоскости, где Z равно 1. Если Persp0 значение не Persp1 равно 0 и Persp2 равно 1, преобразование переместило координаты Z из этой плоскости.

Чтобы восстановить это в двухмерное преобразование, координаты должны быть перемещены обратно в эту плоскость. Требуется еще один шаг. Значения x', y'и z должны быть разделены на z':

x" = x' / z'

y" = y' / z'

z" = z' / z' = 1

Они известны как однородные координаты , и они были разработаны математиком Августом Фердинандом Мёбиусом, гораздо более известным за его топологическую странность, Мёбиус полосы.

Если z' равно 0, деление приводит к бесконечным координатам. На самом деле, одна из мотиваций Möbius для разработки однородных координат была способность представлять бесконечные значения с конечными числами.

Однако при отображении графики необходимо избежать отрисовки чего-либо с координатами, которые преобразуются в бесконечные значения. Эти координаты не будут отображаться. Все в окрестностях этих координат будет очень большим и, вероятно, не визуально последовательным.

В этом уравнении не требуется, чтобы значение z стало нулевым:

z' = Persp0·x + Persp1·y + Persp2

Следовательно, эти значения имеют некоторые практические ограничения:

Ячейка Persp2 может быть нулевой или не нулевой. Если Persp2 значение равно нулю, то z ' равно нулю для точки (0, 0), и это обычно не желательно, потому что эта точка очень распространена в двухмерной графике. Если Persp2 не равно нулю, то при исправлении 1 отсутствует потеря общего характера Persp2 . Например, если определить, что Persp2 должно быть 5, можно просто разделить все ячейки в матрице на 5, что равно Persp2 1, и результат будет одинаковым.

По этим причинам Persp2 часто фиксируется значение 1, которое является одинаковым значением в матрице удостоверений.

Как правило, Persp0 и Persp1 это небольшие числа. Например, предположим, что вы начинаете с матрицы удостоверений, но установите значение Persp0 0,01:

| 1  0   0.01 |
| 0  1    0   |
| 0  0    1   |

Формулы преобразования:

x ' = x / (0.01·x + 1)

y' = y / (0,01·x + 1)

Теперь используйте это преобразование для отрисовки прямоугольник с 100 пикселей, расположенный в источнике. Вот как преобразуются четыре угла:

(0, 0) → (0, 0)

(0, 100) → (0, 100)

(100, 0) → (50, 0)

(100, 100) → (50, 50)

Если значение x равно 100, то знаменатель z равен 2, поэтому координаты x и y эффективно сокращены. Правая сторона поля становится короче левой стороны:

Поле, подвергаемое неаффинному преобразованию

Persp Часть этих имен ячеек ссылается на "перспективу", потому что форешортенинг предполагает, что поле теперь наклоняется с правой стороны дальше от средства просмотра.

Страница "Проверка перспективы" позволяет экспериментировать со значениями Persp0 и Pers1 получить представление о том, как они работают. Разумные значения этих матрицных ячеек настолько малы, что Slider в универсальная платформа Windows не может правильно обрабатывать их. Чтобы решить проблему UWP, необходимо инициализировать два Slider элемента в Файле TestPerspective.xaml в диапазоне от –1 до 1:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Transforms.TestPerspectivePage"
             Title="Test Perpsective">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Minimum" Value="-1" />
                    <Setter Property="Maximum" Value="1" />
                    <Setter Property="Margin" Value="20, 0" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="persp0Slider"
                Grid.Row="0"
                ValueChanged="OnPersp0SliderValueChanged" />

        <Label x:Name="persp0Label"
               Text="Persp0 = 0.0000"
               Grid.Row="1" />

        <Slider x:Name="persp1Slider"
                Grid.Row="2"
                ValueChanged="OnPersp1SliderValueChanged" />

        <Label x:Name="persp1Label"
               Text="Persp1 = 0.0000"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Обработчики событий для ползунка в TestPerspectivePage файле программной части разделяют значения на 100, чтобы они были диапазоном от –0,01 до 0,01. Кроме того, конструктор загружается в растровом рисунке:

public partial class TestPerspectivePage : ContentPage
{
    SKBitmap bitmap;

    public TestPerspectivePage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }

    void OnPersp0SliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        Slider slider = (Slider)sender;
        persp0Label.Text = String.Format("Persp0 = {0:F4}", slider.Value / 100);
        canvasView.InvalidateSurface();
    }

    void OnPersp1SliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        Slider slider = (Slider)sender;
        persp1Label.Text = String.Format("Persp1 = {0:F4}", slider.Value / 100);
        canvasView.InvalidateSurface();
    }
    ...
}

Обработчик PaintSurface вычисляет SKMatrix значение с именем perspectiveMatrix на основе значений этих двух ползунков, разделенных на 100. Это в сочетании с двумя преобразованиями перевода, которые помещают центр этого преобразования в центр растрового изображения:

public partial class TestPerspectivePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Calculate perspective matrix
        SKMatrix perspectiveMatrix = SKMatrix.MakeIdentity();
        perspectiveMatrix.Persp0 = (float)persp0Slider.Value / 100;
        perspectiveMatrix.Persp1 = (float)persp1Slider.Value / 100;

        // Center of screen
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        SKMatrix matrix = SKMatrix.MakeTranslation(-xCenter, -yCenter);
        SKMatrix.PostConcat(ref matrix, perspectiveMatrix);
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(xCenter, yCenter));

        // Coordinates to center bitmap on canvas
        float x = xCenter - bitmap.Width / 2;
        float y = yCenter - bitmap.Height / 2;

        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, x, y);
    }
}

Ниже приведены некоторые примеры изображений:

Тройной снимок экрана страницы

Поэкспериментируя с ползунками, вы обнаружите, что значения, превышающие 0,0066 или ниже –0,00666, приводят к тому, что изображение внезапно становится переломным и несогласованным. Преобразуемая растровая карта составляет 300 пикселей. Он преобразуется относительно центра, поэтому координаты растрового изображения варьируются от –150 до 150. Помните, что значение z':

z' = Persp0·x + Persp1·y + 1

Если Persp0 или Persp1 больше 0,0066 или ниже –0,0066, то всегда имеется некоторая координата растрового изображения, которая приводит к значению z от нуля. Это приводит к делении на нулю, и отрисовка становится беспорядок. При использовании неаффинных преобразований необходимо избежать отрисовки с координатами, которые вызывают деление на ноль.

Как правило, вы не будете устанавливать Persp0 и Persp1 в изоляции. Также часто необходимо задать другие ячейки в матрице для достижения определенных типов неаффинных преобразований.

Одним из таких неаффинных преобразований является преобразование касания. Этот тип неаффинного преобразования сохраняет общие размеры прямоугольника, но коснитесь одной стороны:

Поле, подвергаемое преобразованию касания

Класс TaperTransform выполняет обобщенное вычисление неаффинного преобразования на основе следующих параметров:

  • прямоугольный размер преобразуемого изображения,
  • перечисление, указывающее сторону прямоугольника, который касается,
  • другое перечисление, указывающее, как он касается, и
  • степень сцепивания.

Вот этот код:

enum TaperSide { Left, Top, Right, Bottom }

enum TaperCorner { LeftOrTop, RightOrBottom, Both }

static class TaperTransform
{
    public static SKMatrix Make(SKSize size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction)
    {
        SKMatrix matrix = SKMatrix.MakeIdentity();

        switch (taperSide)
        {
            case TaperSide.Left:
                matrix.ScaleX = taperFraction;
                matrix.ScaleY = taperFraction;
                matrix.Persp0 = (taperFraction - 1) / size.Width;

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewY = size.Height * matrix.Persp0;
                        matrix.TransY = size.Height * (1 - taperFraction);
                        break;

                    case TaperCorner.Both:
                        matrix.SkewY = (size.Height / 2) * matrix.Persp0;
                        matrix.TransY = size.Height * (1 - taperFraction) / 2;
                        break;
                }
                break;

            case TaperSide.Top:
                matrix.ScaleX = taperFraction;
                matrix.ScaleY = taperFraction;
                matrix.Persp1 = (taperFraction - 1) / size.Height;

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewX = size.Width * matrix.Persp1;
                        matrix.TransX = size.Width * (1 - taperFraction);
                        break;

                    case TaperCorner.Both:
                        matrix.SkewX = (size.Width / 2) * matrix.Persp1;
                        matrix.TransX = size.Width * (1 - taperFraction) / 2;
                        break;
                }
                break;

            case TaperSide.Right:
                matrix.ScaleX = 1 / taperFraction;
                matrix.Persp0 = (1 - taperFraction) / (size.Width * taperFraction);

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewY = size.Height * matrix.Persp0;
                        break;

                    case TaperCorner.Both:
                        matrix.SkewY = (size.Height / 2) * matrix.Persp0;
                        break;
                }
                break;

            case TaperSide.Bottom:
                matrix.ScaleY = 1 / taperFraction;
                matrix.Persp1 = (1 - taperFraction) / (size.Height * taperFraction);

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewX = size.Width * matrix.Persp1;
                        break;

                    case TaperCorner.Both:
                        matrix.SkewX = (size.Width / 2) * matrix.Persp1;
                        break;
                }
                break;
        }
        return matrix;
    }
}

Этот класс используется на странице преобразования taper. Xaml-файл создает экземпляры двух Picker элементов для выбора значений перечисления, а также Slider для выбора дроби касания. Обработчик PaintSurface объединяет преобразование касания с двумя преобразованиями перевода, чтобы сделать преобразование относительно левого верхнего угла растрового изображения:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    TaperSide taperSide = (TaperSide)taperSidePicker.SelectedItem;
    TaperCorner taperCorner = (TaperCorner)taperCornerPicker.SelectedItem;
    float taperFraction = (float)taperFractionSlider.Value;

    SKMatrix taperMatrix =
        TaperTransform.Make(new SKSize(bitmap.Width, bitmap.Height),
                            taperSide, taperCorner, taperFraction);

    // Display the matrix in the lower-right corner
    SKSize matrixSize = matrixDisplay.Measure(taperMatrix);

    matrixDisplay.Paint(canvas, taperMatrix,
        new SKPoint(info.Width - matrixSize.Width,
                    info.Height - matrixSize.Height));

    // Center bitmap on canvas
    float x = (info.Width - bitmap.Width) / 2;
    float y = (info.Height - bitmap.Height) / 2;

    SKMatrix matrix = SKMatrix.MakeTranslation(-x, -y);
    SKMatrix.PostConcat(ref matrix, taperMatrix);
    SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(x, y));

    canvas.SetMatrix(matrix);
    canvas.DrawBitmap(bitmap, x, y);
}

Далее приводятся некоторые примеры.

Тройной снимок экрана страницы преобразования taper

Другим типом обобщенных неаффинных преобразований является трехмерная смена, которая показана в следующей статье, трехмерные повороты.

Неаффинное преобразование может преобразовывать прямоугольник в любой выпуклый четырехлатеральный. Это демонстрируется на странице "Показать неаффинированные матрицы ". Это очень похоже на страницу Show Affine Matrix из статьи "Преобразования матрицы", за исключением того, что он имеет четвертый объект для управления четвертым TouchPoint углом растрового изображения:

Тройной снимок экрана: страница

Если вы не пытаетесь сделать внутренний угол одного из углов растрового изображения больше 180 градусов, или сделать две стороны пересекаются друг с другом, программа успешно вычисляет преобразование с помощью этого метода из ShowNonAffineMatrixPage класса:

static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL, SKPoint ptLR)
{
    // Scale transform
    SKMatrix S = SKMatrix.MakeScale(1 / size.Width, 1 / size.Height);

    // Affine transform
    SKMatrix A = new SKMatrix
    {
        ScaleX = ptUR.X - ptUL.X,
        SkewY = ptUR.Y - ptUL.Y,
        SkewX = ptLL.X - ptUL.X,
        ScaleY = ptLL.Y - ptUL.Y,
        TransX = ptUL.X,
        TransY = ptUL.Y,
        Persp2 = 1
    };

    // Non-Affine transform
    SKMatrix inverseA;
    A.TryInvert(out inverseA);
    SKPoint abPoint = inverseA.MapPoint(ptLR);
    float a = abPoint.X;
    float b = abPoint.Y;

    float scaleX = a / (a + b - 1);
    float scaleY = b / (a + b - 1);

    SKMatrix N = new SKMatrix
    {
        ScaleX = scaleX,
        ScaleY = scaleY,
        Persp0 = scaleX - 1,
        Persp1 = scaleY - 1,
        Persp2 = 1
    };

    // Multiply S * N * A
    SKMatrix result = SKMatrix.MakeIdentity();
    SKMatrix.PostConcat(ref result, S);
    SKMatrix.PostConcat(ref result, N);
    SKMatrix.PostConcat(ref result, A);

    return result;
}

Чтобы упростить вычисление, этот метод получает общее преобразование в виде продукта трех отдельных преобразований, которые символизируются здесь со стрелками, показывающими, как эти преобразования изменяют четыре угла растрового изображения:

(0, 0) → (0, 0) → (0, 0) → (x0, y0) (в верхнем левом углу)

(0, H) → (0, 1) → (0, 1) → (x1, y1) (нижний слева)

(W, 0) → (1, 0) → (1, 0) → (x2, y2) (в правом верхнем углу)

(W, H) → (1, 1) → (a, b) → (x3, y3) (в правом нижнем углу)

Окончательные координаты справа — четыре точки, связанные с четырьмя точками касания. Это окончательные координаты углов растрового изображения.

W и H представляют ширину и высоту растрового изображения. Первое преобразование S просто масштабирует растровое изображение до квадрата 1 пикселя. Второе преобразование — это неаффинное преобразование N, а третье — аффинное преобразование A. Это аффинное преобразование основано на трех точках, поэтому это так же, как и предыдущий аффинный ComputeMatrix метод и не включает четвертую строку с точкой (a, b).

Вычисляются a и b значения, чтобы третье преобразование было аффинным. Код получает обратное преобразование аффина, а затем использует его для сопоставления нижнего правого угла. Это точка (a, b).

Другое использование неаффинных преобразований заключается в имитации трехмерной графики. В следующей статье трехмерные повороты показано, как повернуть двухмерный рисунок в трехмерном пространстве.