Partager via


Transformations non affines

Créer des effets de perspective et de taper avec la troisième colonne de la matrice de transformation

La traduction, la mise à l’échelle, la rotation et la rotation sont toutes classées comme des transformations affine . Les transformations affine préservent les lignes parallèles. Si deux lignes sont parallèles avant la transformation, elles restent parallèles après la transformation. Les rectangles sont toujours transformés en parallélismes.

Toutefois, SkiaSharp est également capable de transformers non affine, qui ont la capacité de transformer un rectangle en n’importe quel quadrilatère convex :

Image bitmap transformée en quadrilatère convex

Un quadrilatère convex est une figure à quatre côtés avec des angles intérieurs toujours inférieurs à 180 degrés et des côtés qui ne se croisent pas.

Les transformations non affine se traduisent lorsque la troisième ligne de la matrice de transformation est définie sur des valeurs autres que 0, 0 et 1. La multiplication complète SKMatrix est la suivante :

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

Les formules de transformation résultantes sont les suivantes :

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

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

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

La règle fondamentale de l’utilisation d’une matrice 3 par 3 pour les transformations bidimensionnelles est que tout reste sur le plan où Z est égal à 1. Sauf si Persp0 Persp1 0 et Persp2 est égal à 1, la transformation a déplacé les coordonnées Z hors de ce plan.

Pour restaurer cette transformation à deux dimensions, les coordonnées doivent être déplacées vers ce plan. Une autre étape est requise. Les valeurs x', y’et z' doivent être divisées par z' :

x" = x' / z'

y" = y' / z'

z" = z' / z' = 1

Ces coordonnées sont connues sous le nom de coordonnées homogènes et ont été développées par le mathématique August Ferdinand Möbius, beaucoup mieux connu pour sa bizarreité topologique, la bande de Möbius.

Si z' est 0, la division entraîne des coordonnées infinies. En fait, l’une des motivations de Möbius pour développer des coordonnées homogènes était la capacité de représenter des valeurs infinies avec des nombres finis.

Toutefois, lorsque vous affichez des graphiques, vous souhaitez éviter de rendre quelque chose avec des coordonnées qui se transforment en valeurs infinies. Ces coordonnées ne seront pas rendues. Tout ce qui se trouve à proximité de ces coordonnées sera très volumineux et probablement pas visuellement cohérent.

Dans cette équation, vous ne souhaitez pas que la valeur de z' devienne zéro :

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

Par conséquent, ces valeurs ont des restrictions pratiques :

La Persp2 cellule peut être égale à zéro ou non zéro. S’il Persp2 s’agit de zéro, z' est égal à zéro pour le point (0, 0), et ce n’est généralement pas souhaitable, car ce point est très courant dans les graphiques à deux dimensions. S’il Persp2 n’est pas égal à zéro, il n’y a pas de perte de généralité si Persp2 elle est fixée à 1. Par exemple, si vous déterminez qu’il Persp2 doit s’agir de 5, vous pouvez simplement diviser toutes les cellules de la matrice par 5, ce qui équivaut Persp2 à 1, et le résultat sera le même.

Pour ces raisons, Persp2 est souvent fixe à 1, qui est la même valeur dans la matrice d’identité.

En règle générale, Persp0 et Persp1 sont de petits nombres. Par exemple, supposons que vous commencez par une matrice d’identité, mais que vous avez la valeur Persp0 0.01 :

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

Les formules de transformation sont les suivantes :

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

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

Utilisez maintenant cette transformation pour afficher une zone carrée de 100 pixels positionnée à l’origine. Voici comment les quatre coins sont transformés :

(0, 0) → (0, 0)

(0, 100) → (0, 100)

(100, 0) → (50, 0)

(100, 100) → (50, 50)

Lorsque x est 100, le dénominateur z est 2, de sorte que les coordonnées x et y sont effectivement divisées en deux. Le côté droit de la boîte devient plus court que le côté gauche :

Une boîte soumise à une transformation non affine

La Persp partie de ces noms de cellules fait référence à « perspective », car le foreshortening suggère que la boîte est maintenant inclinée avec le côté droit plus loin de la visionneuse.

La page Perspective de test vous permet d’expérimenter des valeurs et Persp0 Pers1 d’avoir une idée de leur fonctionnement. Les valeurs raisonnables de ces cellules de matrice sont si petites que les Slider plateforme Windows universelle ne peuvent pas les gérer correctement. Pour prendre en charge le problème UWP, les deux Slider éléments du testPerspective.xaml doivent être initialisés pour être comprises entre –1 et 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>

Les gestionnaires d’événements pour les curseurs du TestPerspectivePage fichier code-behind divisent les valeurs par 100 afin qu’elles varient entre –0.01 et 0.01. En outre, le constructeur se charge dans une bitmap :

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

Le PaintSurface gestionnaire calcule une SKMatrix valeur nommée perspectiveMatrix en fonction des valeurs de ces deux curseurs divisés par 100. Cette opération est combinée à deux transformations de traduction qui placent le centre de cette transformation au centre de la bitmap :

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

Voici quelques exemples d’images :

Capture d’écran triple de la page Perspective de test

Lorsque vous expérimentez les curseurs, vous constaterez que les valeurs au-delà de 0,0066 ou inférieures à –0,0066 provoquent soudainement la fracture et l’incohérence de l’image. La bitmap transformée est carrée de 300 pixels. Il est transformé par rapport à son centre, de sorte que les coordonnées de la plage bitmap de –150 à 150. Rappelez-vous que la valeur de z' est :

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

Si Persp0 ou Persp1 est supérieur à 0,0066 ou inférieur à –0,0066, il existe toujours une coordonnée de la bitmap qui entraîne une valeur z' égale à zéro. Cela provoque la division par zéro, et le rendu devient un désordre. Lorsque vous utilisez des transformations non affine, vous souhaitez éviter d’afficher quoi que ce soit avec des coordonnées qui provoquent la division par zéro.

En règle générale, vous ne définirez pas et Persp1 ne vous isolerez pasPersp0. Il est également souvent nécessaire de définir d’autres cellules dans la matrice pour obtenir certains types de transformations non affines.

Une telle transformation non affine est une transformation taper. Ce type de transformation non affine conserve les dimensions globales d’un rectangle, mais s’affine d’un côté :

Boîte soumise à une transformation de taper

La TaperTransform classe effectue un calcul généralisé d’une transformation non affine en fonction de ces paramètres :

  • la taille rectangulaire de l’image transformée,
  • énumération qui indique le côté du rectangle qui tapera,
  • une autre énumération qui indique comment elle tapera et
  • l’étendue du tapereau.

Voici le code  :

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

Cette classe est utilisée dans la page Taper Transform . Le fichier XAML instancie deux Picker éléments pour sélectionner les valeurs d’énumération et un Slider pour choisir la fraction taper. Le PaintSurface gestionnaire combine la transformation taper avec deux transformations de traduction pour rendre la transformation par rapport au coin supérieur gauche de la bitmap :

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

Voici quelques exemples :

Capture d’écran triple de la page Taper Transform

Un autre type de transformations généralisées non affine est la rotation 3D, qui est illustrée dans l’article suivant, Rotations 3D.

La transformation non affine peut transformer un rectangle en n’importe quel quadrilatère convex. Ceci est illustré par la page Show Non-Affine Matrix . Il est très similaire à la page Show Affine Matrix de l’article Matrix Transforms , sauf qu’il a un quatrième TouchPoint objet pour manipuler le quatrième angle de la bitmap :

Capture d’écran triple de la page Show Non-Affine Matrix

Tant que vous n’essayez pas d’effectuer un angle intérieur de l’un des angles de la bitmap supérieure à 180 degrés, ou que vous faites deux côtés se croisent, le programme calcule correctement la transformation à l’aide de cette méthode à partir de la ShowNonAffineMatrixPage classe :

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

Pour faciliter le calcul, cette méthode obtient la transformation totale en tant que produit de trois transformations distinctes, qui sont symbolisées ici avec des flèches montrant comment ces transformations modifient les quatre angles de la bitmap :

(0, 0) → (0, 0) → (0, 0) → (x0, y0) (en haut à gauche)

(0, H) → (0, 1) → (0, 1) → (x1, y1) (inférieur gauche)

(W, 0) → (1, 0) → (1, 0) → (x2, y2) (en haut à droite)

(W, H) → (1, 1) → (a, b) → (x3, y3) (en bas à droite)

Les coordonnées finales à droite sont les quatre points associés aux quatre points tactiles. Il s’agit des coordonnées finales des angles de la bitmap.

W et H représentent la largeur et la hauteur de la bitmap. La première transformation S met simplement à l’échelle la bitmap sur un carré de 1 pixels. La deuxième transformation est la transformation Nnon affine, et la troisième est la transformation Aaffine . Cette transformation affine est basée sur trois points, donc c’est comme la méthode affine ComputeMatrix antérieure et n’implique pas la quatrième ligne avec le point (a, b).

Les a valeurs et b les valeurs sont calculées afin que la troisième transformation soit affine. Le code obtient l’inverse de la transformation affine, puis l’utilise pour mapper le coin inférieur droit. C’est le point (a, b).

Une autre utilisation des transformations non affine consiste à imiter des graphiques tridimensionnels. Dans l’article suivant, les rotations 3D vous voient comment faire pivoter un graphique à deux dimensions dans l’espace 3D.