将常规的二维矩形坐标转换为梯形。

3
我开始制作一个使用足球场svg素材的小部件。到目前为止,我一直在使用常规的2D矩形,效果不错。但是,我想用这个素材替换它:enter image description here
我开始尝试计算在这种svg中球的位置,但进展并不顺利。我想我需要一种从常规的2D矩形模型转换为其他模型的方法,以考虑梯形图形。
也许有人可以帮助理解如何实现。假设我有以下坐标{x: 0.2, y: 0.2},这意味着我必须将球放在球场宽度的20%和高度的20%处。在这个例子中该怎么做?
编辑#1
我阅读了MBo发布的答案,并努力将Delphi代码重写为JavaScript。我完全不懂Delphi,但我认为这很成功,但在尝试代码时遇到了几个问题:
1.梯形是反向的(底部是较短的水平线),我试图修复它,但没有成功,尝试了几次后,我得到了我想要的结果,但是0.2,0.2坐标出现在底部而不是靠近顶部。
2.我不确定计算是否正确,中心坐标似乎奇怪地向下倾斜(至少是我的视觉印象)。
3.我试图通过调整YShift = Hg / 4;来解决反向梯形问题,但这会引起各种问题。想知道这究竟是如何工作的。
4.从我所理解的来看,此脚本的工作方式是您指定较长的水平线Wd和高度Hg,然后为您生成一个梯形,这正确吗?
编辑#2
我更新了演示片段,它似乎以某种方式运行,目前唯一的问题是如果我指定...
Wd = 600; // width of source
Hg = 200; // height of source

实际上,梯形的大小较小(宽度和高度较小),同时以某种奇怪的方式操作该行:
YShift = Hg / 4;

更改梯形的实际高度。

这个实现起来有些困难,因为如果我已经有了一个具有特定大小的SVG图像,我需要能够向函数提供实际大小,以便坐标计算准确。

假设我会得到一个球场,在那里我知道4个角落,基于此我需要能够计算坐标。不幸的是,我的演示片段中的实现并不能做到这一点。

有人可以帮忙修改代码或提供更好的实现吗?

编辑#3-分辨率

这是最终的实现:

var math = {
 inv: function (M){
  if(M.length !== M[0].length){return;}

  var i=0, ii=0, j=0, dim=M.length, e=0, t=0;
  var I = [], C = [];
  for(i=0; i<dim; i+=1){
   I[I.length]=[];
   C[C.length]=[];
   for(j=0; j<dim; j+=1){

    if(i==j){ I[i][j] = 1; }
    else{ I[i][j] = 0; }

    C[i][j] = M[i][j];
   }
  }

  for(i=0; i<dim; i+=1){
   e = C[i][i];

   if(e==0){
    for(ii=i+1; ii<dim; ii+=1){
     if(C[ii][i] != 0){
      for(j=0; j<dim; j++){
       e = C[i][j];
       C[i][j] = C[ii][j];
       C[ii][j] = e;
       e = I[i][j];
       I[i][j] = I[ii][j];
       I[ii][j] = e;
      }
      break;
     }
    }
    e = C[i][i];
    if(e==0){return}
   }

   for(j=0; j<dim; j++){
    C[i][j] = C[i][j]/e;
    I[i][j] = I[i][j]/e;
   }

   for(ii=0; ii<dim; ii++){
    if(ii==i){continue;}

    e = C[ii][i];

    for(j=0; j<dim; j++){
     C[ii][j] -= e*C[i][j];
     I[ii][j] -= e*I[i][j];
    }
   }
  }

  return I;
 },
 multiply: function(m1, m2) {
  var temp = [];
  for(var p = 0; p < m2.length; p++) {
   temp[p] = [m2[p]];
  }
  m2 = temp;

  var result = [];
  for (var i = 0; i < m1.length; i++) {
   result[i] = [];
   for (var j = 0; j < m2[0].length; j++) {
    var sum = 0;
    for (var k = 0; k < m1[0].length; k++) {
     sum += m1[i][k] * m2[k][j];
    }
    result[i][j] = sum;
   }
  }
  return result;
 }
};

// standard soccer court dimensions
var soccerCourtLength = 105; // [m]
var soccerCourtWidth  =  68; // [m]

// soccer court corners in clockwise order with center = (0,0)
var courtCorners = [
    [-soccerCourtLength/2., soccerCourtWidth/2.], 
    [ soccerCourtLength/2., soccerCourtWidth/2.], 
    [ soccerCourtLength/2.,-soccerCourtWidth/2.], 
    [-soccerCourtLength/2.,-soccerCourtWidth/2.]];

// screen corners in clockwise order (unit: pixel)
var screenCorners = [
    [50., 150.], 
    [450., 150.],
    [350., 50.],
    [ 150., 50.]
];

// compute projective mapping M from court to screen
//      / a b c \
// M = (  d e f  )
//      \ g h 1 /

// set up system of linear equations A X = B for X = [a,b,c,d,e,f,g,h]
var A = [];
var B = [];
var i;
for (i=0; i<4; ++i)
{
  var cc = courtCorners[i];
  var sc = screenCorners[i];
  A.push([cc[0], cc[1], 1., 0., 0., 0., -sc[0]*cc[0], -sc[0]*cc[1]]);
  A.push([0., 0., 0., cc[0], cc[1], 1., -sc[1]*cc[0], -sc[1]*cc[1]]);
  B.push(sc[0]);
  B.push(sc[1]);
}

var AInv = math.inv(A);
var X = math.multiply(AInv, B); // [a,b,c,d,e,f,g,h]

// generate matrix M of projective mapping from computed values
X.push(1);
M = [];
for (i=0; i<3; ++i)
    M.push([X[3*i], X[3*i+1], X[3*i+2]]);

// given court point (array [x,y] of court coordinates): compute corresponding screen point
function calcScreenCoords(pSoccer) {
  var ch = [pSoccer[0],pSoccer[1],1]; // homogenous coordinates
  var sh = math.multiply(M, ch);      // projective mapping to screen
  return [sh[0]/sh[2], sh[1]/sh[2]];  // dehomogenize
}

function courtPercToCoords(xPerc, yPerc) {
    return [(xPerc-0.5)*soccerCourtLength, (yPerc-0.5)*soccerCourtWidth];
}

var pScreen = calcScreenCoords(courtPercToCoords(0.2,0.2));
var hScreen = calcScreenCoords(courtPercToCoords(0.5,0.5));

// Custom code
document.querySelector('.trapezoid').setAttribute('d', `
 M ${screenCorners[0][0]} ${screenCorners[0][1]}
 L ${screenCorners[1][0]} ${screenCorners[1][1]}
 L ${screenCorners[2][0]} ${screenCorners[2][1]}
 L ${screenCorners[3][0]} ${screenCorners[3][1]}
 Z
`);

document.querySelector('.point').setAttribute('cx', pScreen[0]);
document.querySelector('.point').setAttribute('cy', pScreen[1]);
document.querySelector('.half').setAttribute('cx', hScreen[0]);
document.querySelector('.half').setAttribute('cy', hScreen[1]);

document.querySelector('.map-pointer').setAttribute('style', 'left:' + (pScreen[0] - 15) + 'px;top:' + (pScreen[1] - 25) + 'px;');

document.querySelector('.helper.NW-SE').setAttribute('d', `M ${screenCorners[3][0]} ${screenCorners[3][1]} L ${screenCorners[1][0]} ${screenCorners[1][1]}`);
document.querySelector('.helper.SW-NE').setAttribute('d', `M ${screenCorners[0][0]} ${screenCorners[0][1]} L ${screenCorners[2][0]} ${screenCorners[2][1]}`);
body {
 margin:0;
}

.container {
 width:500px;
 height:200px;
 position:relative;
 border:solid 1px #000;
}

.view {
 background:#ccc;
 width:100%;
 height:100%;
}

.trapezoid {
 fill:none;
 stroke:#000;
}

.point {
 stroke:none;
 fill:red;
}

.half {
 stroke:none;
 fill:blue;
}

.helper {
 fill:none;
 stroke:#000;
}

.map-pointer {
 background-image:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaWQ9IkxheWVyXzEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48Zz48cGF0aCBkPSJNMjU1LjksNmMtMjEuNywwLTQzLjQsNS4zLTYyLjMsMTZjLTMzLjksMTkuMi01Ny45LDU1LjMtNjEuOSw5NC4xYy0zLjcsMzYuMSw4LjksNzEuOCwyMiwxMDUuNyAgIGMxNS4xLDM4LjksMTAyLjEsMjI4LjksMTAyLjEsMjI4LjlzODcuNi0xOTEuNCwxMDIuOC0yMzAuOWMxMy4xLTM0LjIsMjUuNy03MC4yLDIxLjItMTA2LjVjLTUuMi00Mi4xLTM0LjctNzkuOS03My42LTk2LjggICBDMjkwLjUsOS41LDI3My4yLDYsMjU1LjksNnogTTI1NS45LDE4OS44Yy0zMywwLTU5LjgtMjYuOC01OS44LTU5LjhzMjYuOC01OS44LDU5LjgtNTkuOFMzMTUuNyw5NywzMTUuNywxMzAgICBTMjg5LDE4OS44LDI1NS45LDE4OS44eiIvPjxwYXRoIGQ9Ik0yOTIuMiwzOTcuMWMtNC4xLDguOS03LjksMTcuMi0xMS40LDI0LjdjMzYuOCwzLjYsNjMuNiwxNS4yLDYzLjYsMjguOGMwLDE2LjYtMzkuNiwzMC04OC40LDMwICAgYy00OC44LDAtODguNC0xMy40LTg4LjQtMzBjMC0xMy42LDI2LjgtMjUuMiw2My41LTI4LjhjLTMuNS03LjQtNy40LTE1LjgtMTEuNC0yNC43Yy02MC4yLDYuMy0xMDQuNSwyNy45LTEwNC41LDUzLjUgICBjMCwzMC42LDYzLjEsNTUuNCwxNDAuOCw1NS40czE0MC44LTI0LjgsMTQwLjgtNTUuNEMzOTYuOCw0MjUsMzUyLjQsNDAzLjQsMjkyLjIsMzk3LjF6IiBpZD0iWE1MSURfMV8iLz48L2c+PC9zdmc+');
 display:block;
 width:32px;
 height:32px;
 background-repeat:no-repeat;
 background-size:32px 32px;
 position:absolute;
 opacity:.3;
}
<div class="container">
 <svg class="view">
  <path class="trapezoid"></path>
  <circle class="point" r="3"></circle>
  <circle class="half" r="3"></circle>
  <path class="helper NW-SE"></path>
  <path class="helper SW-NE"></path>
 </svg>
 <span class="map-pointer"></span>
</div>


2
你可能想要研究一下透视投影。 - meowgoesthedog
或者对于这种情况,可能只需将其呈现为矩形,然后投影到梯形上。(虽然我不知道SVG是否能做到这一点) - apple apple
你能展示一个例子吗?怎么做的? ;) - Mevia
理论 http://graphics.cs.cmu.edu/courses/15-463/2008_fall/Papers/proj.pdf 简化的例子适用于类似你的轴对称情况:https://stackoverflow.com/questions/36760997/perspective-coords-for-2d-hex-grid/36761829#36761829 - MBo
请将以下与编程相关的内容从英文翻译成中文。只返回翻译后的文本:不要将答案发布到问题上,而是发布一个答案。 - Munim Munna
3个回答

3
您正在寻找从球场平面的 (x,y) 到屏幕平面的 (u,v) 的投影映射。 投影映射的工作方式如下:
  1. 将1附加到球场坐标以获取齐次坐标 (x,y,1)
  2. 将这些齐次坐标从左侧与适当的3x3矩阵 M 相乘,以获取屏幕像素的齐次坐标 (u',v',l)
  3. 去齐次化坐标以获取实际的屏幕坐标 (u,v) = (u'/l, v'/l)
可以通过解决例如角落的相应方程来计算适当的矩阵 M。选择球场中心与原点重合,并且 x 轴沿着较长的一侧指向并测量图像的角落坐标,我们得到标准105x68球场的以下相应坐标:
(-52.5, 34) -> (174, 57)
( 52.5, 34) -> (566, 57)
( 52.5,-34) -> (690,214)
(-52.5,-34) -> ( 50,214)

使用矩阵设置投影映射方程

     / a b c \
M = (  d e f  )
     \ g h 1 /

这导致了一个线性系统,有8个方程和8个未知数,因为每个点的对应关系 (x,y) -> (u,v) 贡献了两个方程:

x*a + y*b + 1*c + 0*d + 0*e + 0*f - (u*x)*g - (u*y)*h = u
0*a + 0*b + 0*c + x*d + y*e + 1*f - (v*x)*g - (v*y)*h = v

你可以通过将M(x y 1)^T = (l*u l*v l*1)^T展开为三个方程,并将第三个方程中的l值替换到前两个方程中来获得这两个方程。

将8个未知数a,b,c,...,h的解放入矩阵中得出:

     / 4.63  2.61    370    \
M = (  0    -1.35   -116.64  )
     \ 0     0.00707   1    /

因此,例如将法院中心设为{x: 0.5, y: 0.5},您必须先将其转换为上述描述的坐标系,以获得(x,y) = (0,0)。然后,您必须计算:

   / x \     / 4.63  2.61    370    \   / 0 \      / 370    \
M (  y  ) = (  0    -1.35   -116.64  ) (  0  ) =  (  116.64  )
   \ 1 /     \ 0     0.00707   1    /   \ 1 /      \   1    /

通过去除坐标的归一化,您可以得到中心点的屏幕坐标,如下所示:
(u,v) = (370/1, 116.64/1) ~= (370,117)

一个JavaScript实现可能是这样的:
// using library https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.2.1/math.js

// standard soccer court dimensions
var soccerCourtLength = 105; // [m]
var soccerCourtWidth  =  68; // [m]

// soccer court corners in clockwise order with center = (0,0)
var courtCorners = [
    [-soccerCourtLength/2., soccerCourtWidth/2.], 
    [ soccerCourtLength/2., soccerCourtWidth/2.], 
    [ soccerCourtLength/2.,-soccerCourtWidth/2.], 
    [-soccerCourtLength/2.,-soccerCourtWidth/2.]];

// screen corners in clockwise order (unit: pixel)
var screenCorners = [
    [174., 57.], 
    [566., 57.],
    [690.,214.],
    [ 50.,214.]];

// compute projective mapping M from court to screen
//      / a b c \
// M = (  d e f  )
//      \ g h 1 /

// set up system of linear equations A X = B for X = [a,b,c,d,e,f,g,h]
var A = [];
var B = [];
var i;
for (i=0; i<4; ++i)
{
  var cc = courtCorners[i];
  var sc = screenCorners[i];
  A.push([cc[0], cc[1], 1., 0., 0., 0., -sc[0]*cc[0], -sc[0]*cc[1]]);
  A.push([0., 0., 0., cc[0], cc[1], 1., -sc[1]*cc[0], -sc[1]*cc[1]]);
  B.push(sc[0]);
  B.push(sc[1]);
}

var AInv = math.inv(A);
var X = math.multiply(AInv, B); // [a,b,c,d,e,f,g,h]

// generate matrix M of projective mapping from computed values
X.push(1);
M = [];
for (i=0; i<3; ++i)
    M.push([X[3*i], X[3*i+1], X[3*i+2]]);

// given court point (array [x,y] of court coordinates): compute corresponding screen point
function calcScreenCoords(pSoccer) {
  var ch = [pSoccer[0],pSoccer[1],1]; // homogenous coordinates
  var sh = math.multiply(M, ch);      // projective mapping to screen
  return [sh[0]/sh[2], sh[1]/sh[2]];  // dehomogenize
}

function courtPercToCoords(xPerc, yPerc) {
    return [(xPerc-0.5)*soccerCourtLength, (yPerc-0.5)*soccerCourtWidth];
}

var pScreen = calcScreenCoords(courtPercToCoords(0.2,0.2))

非常感谢您迄今为止的工作,我在这里准备了一些基本的工作环境https://jsfiddle.net/nr03opbd/1/。您能否继续帮助,因为我不明白这些值是从哪里来的(`M = {}内部的值,如4.632.61-1.35等),我之所以问是因为我想尝试将其放入代码中,但我不知道如何将您编写的javascript函数翻译成最后生成正确(转换后的)x,y`的代码。 - Mevia
在本质上,我非常希望理解这些 (-52.5, 34) -> (174, 57) 是如何计算的,以及进一步如何计算 M = { } 部分,我认为这对于重新访问这个主题的其他人(数学背景较少)来说,理解它是如何完成并用JavaScript代码实现的将非常有帮助 ;) - Mevia
@Mevia 我已经添加了一些提示,关于如何获取矩阵元素的线性方程以及如何计算从场地坐标到屏幕坐标的转换。当屏幕坐标系统固定时,可以离线计算矩阵元素的方程组(我使用了电子表格)。 - coproc
我尝试过对其进行调试,但是我不知道需要传递什么样的输入才能显示出0.2, 0.2的坐标。这里有一个可工作的示例:https://jsfiddle.net/8chpfs2q/ 。总的来说,在这种方法中有很多我不知道的地方,例如,我不知道如何传递宽度和高度或者角落来构建梯形,也不知道如何传递坐标使其正确地放置在梯形上,所有的内容都在演示中。目前,我唯一能做的就是使用您的答案中的值来绘制梯形,但它并不具备自适应性。 - Mevia
我已经尝试着完成了一个完整的实现。根据你的需求调整屏幕坐标的位置。 - coproc
亲爱的先生,这真是一件艺术品,它完全按照我所需的方式从头到尾运行;)晚上我会发布准备好的代码,其中将包含对52,000行库的绕过,并且一旦我能够,我将授予奖励;) - Mevia

1
为了制作具有轴对称性并将矩形映射到等腰梯形的特定透视投影,我们可以按照我在这里描述的方式构建更简单的模型。
假设我们想要将坐标为(0,0)-(SrcWdt, SrcHgt)且带有轴线SrcWdt/2的矩形 enter image description here 映射到轴线位于DstWdt/2且右上角坐标为RBX, RBY, RTX, RTY的区域。

enter image description here

这里我们需要(部分)透视变换:

X' = DstXCenter + A * (X - XCenter) / (H * Y + 1)
Y' = (RBY +  E * Y) / (H * Y + 1)

我们可以通过梯形两个角的坐标来计算系数A,E,H,而无需解决八个线性方程组。

以下是使用Delphi代码演示如何找到系数并将一些点映射到新区域(Y轴向下,因此透视视图从上边缘看):

enter image description here

  procedure CalcAxialSymPersp(SrcWdt, SrcHgt, DstWdt, RBX, RBY, RTX, RTY: Integer;
                              var A, H, E: Double);
  begin
     A := (2 * RBX - DstWdt) / SrcWdt;
     H := (A * SrcWdt/ (2 * RTX - DstWdt) - 1) / SrcHgt;
     E := (RTY * (H * SrcHgt + 1) - RBY) / SrcHgt;
  end;

  procedure PerspMap(SrcWdt, DstWdt, RBY: Integer; A, H, E: Double; 
                     PSrc: TPoint; var PPersp: TPoint);
  begin
     PPersp.X := Round(DstWdt / 2 + A * (PSrc.X - SrcWdt/2) / (H * PSrc.Y + 1));
     PPersp.Y := Round((RBY +  E * PSrc.Y) / (H * PSrc.Y + 1));
  end;


  var
  Wd, Hg, YShift: Integer;
  A, H, E: Double;
  Pts: array[0..3] of TPoint;

begin
  //XPersp = XPCenter + A * (X - XCenter) / (H * Y + 1)
  //YPersp = (YShift +  E * Y) / (H * Y + 1)
  Wd := Image1.Width;
  Hg := Image1.Height;
  YShift := Hg div 4;

  CalcAxialSymPersp(Wd, Hg, Wd,
                    Wd * 9 div 10, YShift, Wd * 8 div 10, Hg * 3 div 4,
                    A, H, E);
 //map 4 corners
  PerspMap(Wd, Wd, YShift, A, H, E, Point(Wd, 0), Pts[0]);
  PerspMap(Wd, Wd,  YShift, A, H, E, Point(Wd, Hg), Pts[1]);
  PerspMap(Wd, Wd,  YShift, A, H, E, Point(0, Hg), Pts[2]);
  PerspMap(Wd, Wd,  YShift, A, H, E, Point(0, 0), Pts[3]);

  //draw trapezoid
  Image1.Canvas.Brush.Style := bsClear;
  Image1.Canvas.Polygon(Pts);

  //draw trapezoid diagonals
  Image1.Canvas.Polygon(Slice(Pts, 3));
  Image1.Canvas.Polygon([Pts[1], Pts[2], Pts[3]]);

  //map and draw central point
  PerspMap(Wd,  Wd, YShift, A, H, E, Point(Wd div 2, Hg div 2), Pts[0]);
  Image1.Canvas.Ellipse(Pts[0].X - 3, Pts[0].Y - 3, Pts[0].X + 4, Pts[0].Y + 4);

  //map and draw point at (0.2,0.2)
  PerspMap(Wd,  Wd, YShift, A, H, E, Point(Wd * 2 div 10, Hg * 2 div 10), Pts[0]);
  Image1.Canvas.Ellipse(Pts[0].X - 3, Pts[0].Y - 3, Pts[0].X + 4, Pts[0].Y + 4);

非常感谢您的贡献 :) 您能否请查看我对问题所做的编辑?我已经将代码改写成了JavaScript,并运行时遇到了一些问题,也许您知道如何解答它们 :) - Mevia
在默认坐标系统中,Y轴的方向是向下的,这就是为令我们的梯形看起来颠倒的原因。尝试更改垂直映射:PPersp.Y = DstHgt - Math.round... - MBo
中心坐标看起来太远了,因为远距离看起来更短 - 我故意画了对角线和它们的交点。 - MBo

0

我已经用纯HTML和JavaScript实现了它。你需要根据自己的需求调整变量。A和B分别是梯形的短边和长边的长度,H是梯形的高度。x0和y0是场地左下角的坐标。如果对你有用的话,我可以解释一下数学原理。

jQuery(function($){
    var $field2d = $('.field2d'), $ball = $('.ball');
    $field2d.on('mousemove', function(e){
        var pos = translateBallPosition(e.offsetX, e.offsetY);
        $ball.css({left: pos.x, top: pos.y});
    });
    var FB = {x0: 50, y0: 215, B: 640, A: 391, H: 158, P: 0};
    FB.Wd = $field2d.width();
    FB.Ht = $field2d.height();
    FB.P = FB.B * FB.H / (FB.B - FB.A);
    function translateBallPosition(X, Y){
        var x = X / FB.Wd * FB.B, y = (FB.Ht - Y) / FB.Ht * FB.H;
        y = y * FB.B * FB.H / (FB.A * FB.H + y * (FB.B - FB.A));
        x = x / FB.P * (FB.P - y) + y * FB.B / FB.P / 2;
        return {x: FB.x0 + x, y: FB.y0 - y};
    }
});
.field2d {
  position: relative;
  border: 1px dashed gray;
  background: #b0fdb5;
  width: 400px;
  height: 200px;
  margin: 5px auto;
  cursor: crosshair;
  text-align: center;
}

.field3d {
  position: relative;
  width: 743px;
  margin: auto;
}

.field3d>img {
  width: 100%;
  height: auto;
}

.ball {
  position: absolute;
  top: 0;
  left: 0;
  height: 20px;
  width: 20px;
  background: red;
  border-radius: 10px;
  margin: -20px 0 0 -10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="field3d">
  <img src="https://istack.dev59.com/ciekU.webp" />
  <div class="ball"></div>
</div>
<div class="field2d">
  Hover over this div to see corresponding ball position
</div>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接