代码之家  ›  专栏  ›  技术社区  ›  Mevia

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

  •  0
  • Mevia  · 技术社区  · 6 年前

    我开始构建一个使用svg资产的小部件,它是一个足球场。到目前为止,我一直在使用常规的二维矩形,效果很好。不过,我想用这个资产来替换这个资产:

    enter image description here

    我开始研究如何在这种svg中计算球的位置,但进展并不顺利。我想我需要的是从普通的二维矩形模型转换成其他的梯形模型。

    也许有人能帮你理解它是怎么做的。假设我有以下几个坐标 {x: 0.2, y: 0.2} 这意味着我必须把球放在球场宽度的20%和高度的20%。在这个例子中我该怎么做?

    编辑#1

    1. 秋千是反向的(较短的水平线在底部),我试图修复它,但没有成功,经过几次尝试,我有这个我想,但后来 0.2, 0.2 库德出现在底部,而不是接近顶部。

    2. 总的来说,我不确定计算是否正确,中心坐标似乎非常倾向于底部(至少这是我的视觉印象)

    3. YShift = Hg / 4; 但它会引起各种各样的问题。想知道这到底是怎么回事吗

    4. 据我所知,这个脚本的工作方式是指定更长的水平线 Wd 和高度 Hg 这就产生了一个秋千,对吗?

    Wd = 600; // width of source
    Hg = 200; // height of source
    

    实际吊架较小(宽度和高度较小),

    YShift = Hg / 4;
    

    它只是那时很难实现,好像我已经给了svg法院与一定的规模,我需要能够提供实际大小的功能,那么坐标计算将是准确的。

    让我说,我将得到法院,我知道4个角落,并在此基础上,我需要能够计算坐标。不幸的是,我的演示片段中的这个实现没有成功。

    这是最终的实现:

    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>
    3 回复  |  直到 6 年前
        1
  •  1
  •   coproc    6 年前

    您正在从中寻找投影映射 (x,y) (u,v)

    1. 将1附加到球场坐标以获得同质坐标 (x,y,1)
    2. M (u',v',l) 屏幕像素的
    3. (u,v) = (u'/l, v'/l)

    适当的矩阵 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 /
    

    (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))
    
        2
  •  1
  •   MBo    6 年前

    here

    让我们用坐标来映射矩形 (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);
    
        3
  •  0
  •   Munim Munna    6 年前

    我已经用纯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://i.stack.imgur.com/ciekU.png" />
      <div class="ball"></div>
    </div>
    <div class="field2d">
      Hover over this div to see corresponding ball position
    </div>