非仿射轉換
使用轉換矩陣的第三欄建立檢視方塊和點選效果
翻譯、縮放、旋轉和扭曲全都分類為 仿 射轉換。 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 的平面上會保留一切。 除非 Persp0
和 Persp1
是 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,也就是識別矩陣中的相同值。
一般而言, 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 問題,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
如果 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 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);
}
以下列出一些範例:
另一種類型的一般化非仿射轉換是 3D 旋轉,如下一篇文章 3D 旋轉所示。
非仿射轉換可以將矩形轉換成任何凸四邊形。 這會由 [顯示非 Affine 矩陣 ] 頁面示範。 它與 [矩陣轉換] 文章中的 [顯示 Affine 矩陣] 頁面非常類似,不同之處在於它有第四個物件可操作位圖的第四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) 。
非仿射轉換的另一個用法是模擬三維圖形。 在下一篇文章中, 您會瞭解如何在 3D 空間中旋轉二維圖形。