Compartir vía


Transformaciones no afines

Crear efectos de perspectiva y cónicos con la tercera columna de la matriz de transformación

Todas las transformaciones de traslación, escalado, rotación y distorsión se clasifican como transformaciones afines. Las transformaciones afines conservan las líneas paralelas. Si dos líneas son paralelas antes de la transformación, siguen siéndolo después de la transformación. Los rectángulos siempre se transforman en paralelogramos.

Sin embargo, SkiaSharp también es capaz de transformaciones no afines, que tienen la capacidad de transformar un rectángulo en cualquier cuadrilátero convexo:

Un mapa de bits transformado en un cuadrilátero convex

Un cuadrilátero convexo es una figura de cuatro caras con ángulos interiores siempre inferiores a 180 grados y lados que no se cruzan entre sí.

Las transformaciones no afines ocurren cuando la tercera fila de la matriz de transformación se establece en valores distintos de 0, 0 y 1. La multiplicación completa de SKMatrix es:

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

Las fórmulas de transformación resultantes son:

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

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

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

La regla fundamental de usar una matriz de 3 x 3 para transformaciones bidimensionales es que todo permanece en el plano donde Z es igual a 1. A no ser que Persp0 y Persp1 sean 0 y Persp2 sea igual a 1, la transformación moverá las coordenadas Z fuera de ese plano.

Para restaurarlo a una transformación bidimensional, las coordenadas se deben mover de nuevo a ese plano. Se requiere otro paso. Los valores x', y', y z' deben dividirse por z':

x" = x' / z'

y" = y' / z'

z" = z' / z' = 1

Estas son conocidas como coordenadas homogéneas y fueron desarrolladas por el matemático August Fernando Möbius, mucho más conocido por su paradoja de topología, la tira de Möbius.

Si z' es 0, la división da como resultado coordenadas infinitas. De hecho, una de las motivaciones de Möbius para desarrollar coordenadas homogéneas era la capacidad de representar valores infinitos con números finitos.

Sin embargo, al mostrar gráficos, queremos evitar representar algo con coordenadas que se transforman en valores infinitos. Esas coordenadas no se representarán. Todo lo que se encuentra en las proximidades de esas coordenadas será muy grande y probablemente no coherente visualmente.

En esta ecuación, no queremos que el valor de z' se convierta en cero:

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

Por consiguiente, estos valores tienen algunas restricciones prácticas:

La celda Persp2 puede ser cero o no ser cero. Si Persp2 es cero, z' es cero para el punto (0, 0) y normalmente no es lo que buscamos, porque ese punto es muy común en gráficos bidimensionales. Si Persp2 no es igual a cero, no se pierde la generalidad si Persp2 se fija en 1. Por ejemplo, si determina que Persp2 debe ser 5, simplemente puede dividir todas las celdas de la matriz en 5, lo que hace Persp2 igual a 1 y el resultado será el mismo.

Por estas razones, Persp2 a menudo se fija en 1, que es el mismo valor en la matriz de identidad.

Por lo general, Persp0 y Persp1 son números pequeños. Por ejemplo, supongamos que comienza con una matriz de identidades, pero establece Persp0 en 0.01:

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

Las fórmulas de transformación son:

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

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

Ahora use esta transformación para representar un cuadro de 100 píxeles colocado en el origen. Así es como se transforman las cuatro esquinas:

(0, 0) → (0, 0)

(0, 100) → (0, 100)

(100, 0) → (50, 0)

(100, 100) → (50, 50)

Cuando x es 100, el denominador z' es 2, por lo que las coordenadas x e y se dividen a la mitad. El lado derecho del cuadro se vuelve más corto que el lado izquierdo:

Un cuadro sujeto a una transformación no afín

La parte Persp de estos nombres de celda hace referencia a "perspectiva", porque el escorzo sugiere que el cuadro ahora está inclinado con el lado derecho más allá de la persona que lo mira.

La página Perspectiva de prueba le permite experimentar con valores de Persp0 y Pers1 y desarrollar su intuición sobre cómo funcionan. Los valores razonables de estas celdas de matriz son tan pequeños que el Slider de la Plataforma universal de Windows no puede controlarlos correctamente. Para solucionar este problema con la UWP, los dos elementos Slider del archivo TestPerspective.xaml deben inicializarse para que oscilen entre –1 y 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>

Los controladores de eventos de los controles deslizantes del archivo de código subyacente de TestPerspectivePage dividen los valores en 100 para que oscilan entre –0,01 y 0,01. Además, el constructor se carga en un mapa de bits:

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();
    }
    ...
}

El controlador PaintSurface calcula un valor SKMatrix denominado perspectiveMatrix en función de los valores de estos dos controles deslizantes divididos por 100. Esto se combina con dos transformaciones de traslación que colocan el centro de esta transformación en el centro del mapa de bits:

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);
    }
}

Estas son algunas imágenes de ejemplo:

Captura de pantalla triple de la página Perspectiva de prueba

A medida que experimente con los controles deslizantes, encontrará que los valores más allá de 0,0066 o por debajo de –0,0066 hacen que la imagen se fracture y sea incoherente de repente. El mapa de bits que se va a transformar es un cuadrado de 300 píxeles. Se transforma en relación con su centro, por lo que las coordenadas del intervalo de mapa de bits van de –150 a 150. Recuerde que el valor de z' es:

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

Si Persp0 o Persp1 es mayor que 0,0066 o inferior a –0,0066, siempre hay alguna coordenada del mapa de bits que da como resultado un valor z de cero. Esto provoca divisiones por cero y la representación se convierte en un desastre. Al usar transformaciones no afines, queremos evitar representar algo con coordenadas que provoque la división por cero.

Por lo general, no establecerá Persp0 ni Persp1 se aislará. También es necesario establecer otras celdas en la matriz para lograr ciertos tipos de transformaciones no afines.

Un ejemplo de transformación no afín es una transformación cónica. Este tipo de transformación no afín conserva las dimensiones generales de un rectángulo, pero reduce un lado:

Un cuadro sujeto a una transformación de pulsador

La clase TaperTransform realiza un cálculo generalizado de una transformación no afín basada en estos parámetros:

  • el tamaño rectangular de la imagen que se va a transformar.
  • una enumeración que indica el lado del rectángulo que se reduce,
  • otra enumeración que indica cómo se hace la transformación cónica
  • la extensión de la transformación cónica.

Este es el código :

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;
    }
}

Esta clase se usa en la página Transformación cónica. El archivo XAML crea una instancia de dos elementos Picker para seleccionar los valores de enumeración y un Slider para elegir la fracción de transformación cónica. El controlador PaintSurface combina la transformación cónica con dos transformaciones de traslación para convertir la transformación en relación con la esquina superior izquierda del mapa de bits:

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);
}

Estos son algunos ejemplos:

Captura de pantalla triple de la página Transformación de Taper

Otro tipo de transformaciones no afines generalizadas es la rotación en 3D, que se muestra en el siguiente artículo Rotaciones 3D.

Una transformación no afín puede transformar un rectángulo en cualquier cuadrilátero convexo. Esto se muestra en la página Mostrar matriz no afín. Es muy similar a la página Mostrar matriz afín del artículo Transformaciones de matriz, excepto que tiene un cuarto objeto TouchPoint para manipular la cuarta esquina del mapa de bits:

Captura de pantalla triple de la página Mostrar matriz no afín

Siempre que no haga que un ángulo interior de una de las esquinas del mapa de bits sea superior a 180 grados, o que dos lados se crucen entre sí, el programa calcula correctamente la transformación mediante este método de la clase 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;
}

Para facilitar el cálculo, este método obtiene la transformación total como producto de tres transformaciones independientes, que se simbolizan aquí con flechas que muestran cómo estas transformaciones modifican las cuatro esquinas del mapa de bits:

(0, 0) → (0, 0) → (0, 0) → (x0, y0) (superior izquierda)

(0, H) → (0, 1) → (0, 1) → (x1, y1) (inferior izquierda)

(W, 0) → (1, 0) → (1, 0) → (x2, y2) (superior derecha)

(W, H) → (1, 1) → (a, b) → (x3, y3) (inferior derecha)

Las coordenadas finales a la derecha son los cuatro puntos asociados a los cuatro puntos táctiles. Estas son las coordenadas finales de las esquinas del mapa de bits.

W y H representan el ancho y el alto del mapa de bits. La primera transformación S simplemente escala el mapa de bits a un cuadrado de 1 píxel. La segunda transformación es la transformación no afín N y la tercera es la transformación afín A. Esa transformación afín se basa en tres puntos, por lo que es igual que el método afín ComputeMatrix anterior y no implica la cuarta fila con el punto (a, b).

Los valores a y b se calculan para que la tercera transformación sea afín. El código obtiene el inverso de la transformación afín y, a continuación, lo usa para asignar la esquina inferior derecha. Ese es el punto (a, b).

Otro uso de transformaciones no afines es imitar gráficos tridimensionales. En el siguiente artículo, Rotaciones 3D verá cómo girar un gráfico bidimensional en un espacio 3D.