преобразовать обычные 2d прямоугольные координаты в трапецию - PullRequest
0 голосов
/ 13 сентября 2018

Я начал создавать виджет, который использует svg asset, футбольный корт.До сих пор я работал с обычным двухмерным прямоугольником, и все прошло хорошо.Однако я хотел заменить этот актив следующим:

enter image description here

Я начал создавать прототипы для расчета позиции мяча в этом виде SVG и егоне идет хорошо.Я предполагаю, что мне нужно какое-то преобразование из обычной 2-мерной модели прямоугольника во что-то еще, что учитывало бы фигуру трапеции.

Может быть, кто-то может помочь понять, как это делается.Допустим, у меня есть следующие координаты {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 court с определенным размером, мне нужно иметь возможность предоставить действительный размер функции так, чтобывычисления координат будут точными.

Позвольте мне сказать, что мне дадут суд, где я знаю 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>

Ответы [ 3 ]

0 голосов
/ 14 сентября 2018

Чтобы сделать конкретную перспективную проекцию, которая имеет осевую симметрию и отображает прямоугольник на равнобедренную трапецию, мы можем построить более простую модель, как я описал здесь .

Пусть мы хотим отобразить прямоугольник с координатами (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);
0 голосов
/ 17 сентября 2018

Я реализовал это в простом 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>
0 голосов
/ 13 сентября 2018

Вы ищете проекционное отображение от (x,y) в плоскости суда до (u,v) в плоскости экрана. Проективное отображение работает так:

  1. Добавить 1 к координатам суда, чтобы получить однородные координаты (x,y,1)
  2. Умножьте эти однородные координаты на соответствующую матрицу 3x3 M слева, чтобы получить однородные координаты (u',v',l) пикселя экрана
  3. Dehomogenize координаты, чтобы получить фактические координаты экрана (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))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...