Share via


非仿射轉換

使用轉換矩陣的第三欄建立檢視方塊和點選效果

翻譯、縮放、旋轉和扭曲全都分類為 仿 射轉換。 Affine 轉換會保留平行線條。 如果在轉換之前平行兩行,它們會在轉換之後保持平行。 矩形一律會轉換成平行投影。

不過,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 by-3 矩陣的基本規則是,Z 等於 1 的平面上會保留一切。 除非 Persp0Persp1 是 0,且 Persp2 等於 1,否則轉換已將 Z 座標移出該平面。

若要將此還原至二維轉換,座標必須移回該平面。 需要另一個步驟。 x'、y'和 z' 值必須除以 z':

x“ = x' / z'

y“ = y' / z'

z“ = z' / z' = 1

這些被稱為 同質座標 ,由數學家8月費迪南德·莫比烏斯開發,更出名的是他的拓撲奇數,莫比烏斯大道。

如果 z' 為 0,則除法會產生無限座標。 事實上,Möbius 開發同質座標的動機之一,就是能夠以有限數位來代表無限值。

不過,在顯示圖形時,您想要避免轉譯具有轉換成無限值之座標的內容。 這些座標不會轉譯。 這些座標附近的一切都會非常大,而且可能不是視覺上連貫的。

在此方程式中,您不希望 z 的值變成零:

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

因此,這些值有一些實際限制:

單元格 Persp2 可以是零或非零。 如果 Persp2 為零,則 z' 是點的零 (0, 0),這通常不理想,因為二維圖形中很常見。 如果 Persp2 不等於零,則如果 Persp2 固定在1,則不會遺失一般性。 例如,如果您判斷應該為 Persp2 5,則只要將矩陣中的所有儲存格除以5,這會 Persp2 等於1,結果會相同。

基於這些原因, Persp2 通常固定在 1,也就是識別矩陣中的相同值。

一般而言, Persp0Persp1 是小數位。 例如,假設您從身分識別矩陣開始,但設定 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 問題,TestPerspective.xaml 中的Slider個元素必須初始化,範圍從 –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 滑桿的值除以 100 來計算名為 perspectiveMatrix 的值。 這會結合兩個平移轉換,將這個轉換的中心放在點陣圖中央:

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.0066的值會導致影像突然骨折且不連貫。 正在轉換的點陣圖是300像素平方。 它會與其中心相對轉換,因此點圖範圍的座標範圍從 –150 到 150。 回想一下,z' 的值是:

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

如果 Persp0Persp1 大於 0.0066 或低於 –0.0066,則一律會有一些點陣圖座標會導致 z' 值為零。 這會導致除以零,而轉譯會變成一團糟。 使用非仿射轉換時,您想要避免以座標轉譯任何導致零除的座標。

一般而言,您不會設定 Persp0Persp1 隔離。 通常也需要在矩陣中設定其他儲存格,以達到特定類型的非仿射轉換。

這類非仿射轉換之一 是點選轉換。 這種類型的非仿射轉換會保留矩形的整體維度,但一邊會點選:

接受點選轉換的方塊

類別 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 Transform 頁面。 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 轉換頁面的三重螢幕快照

另一種類型的一般化非仿射轉換是 3D 旋轉,如下一篇文章 3D 旋轉所示。

非仿射轉換可以將矩形轉換成任何凸四邊形。 這會由 [顯示非 Affine 矩陣 ] 頁面示範。 它與 [矩陣轉換] 文章中的 [顯示 Affine 矩陣] 頁面非常類似,不同之處在於它有第四個物件可操作位圖的第四TouchPoint個角落:

[顯示非 Affine 矩陣] 頁面的三個螢幕快照

只要您不嘗試使位圖的其中一個角落的內部角度大於 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) 點。

ab 值會計算,讓第三個轉換是仿射。 程序代碼會取得仿射轉換的反轉,然後使用它來對應右下角。 這就是關鍵 (a, b) 。

非仿射轉換的另一個用法是模擬三維圖形。 在下一篇文章中, 您會瞭解如何在 3D 空間中旋轉二維圖形。