convert regular 2d rectangle coords to trapeze - javascript
I started to build a widget that uses svg asset that is a soccer court. I was working with regular 2d rectangle so far and it went well. However i wanted to replace that asset with this one:
I started to prototype on how to calculate the ball position in this kind of svg and its not going well. I guess that what i need is some kind of conversion from regular 2d rectangle model to something else that would account trapeze figure.
Maybe someone could help with understanding how its done. Lets say i have following coords {x: 0.2, y: 0.2} which means i have to put the ball in 20% of width of court and 20% of its height. How do i do in this example?
EDIT #1
I read an answer posted by MBo and i made effort to rewrite delphi code to JavaScript.I dont know delphi at all but i think it went well, however after trying code out i bumped onto couple of problems:
trapeze is reversed (shorter horizotal line on the bottom), i attempted to fix it but without success, after couple of tries i had this as i wanted but then 0.2, 0.2 coord showed up on the bottom instead of closer to the top.
i am not sure if the calculation works correctly in general, center coord seems odly gravitating towards bottom (at least it is my visual impresion)
I attempted to fix reversed trapeze problem by playing with YShift = Hg / 4; but it causes variety of issues. Would like to know how this works exactly
From what i understand, this script works in a way that you specify longer horizontal line Wd and height Hg and this produces a trapeze for you, is that correct?
EDIT #2
I updated demo snippet, it seems to work in some way, the only problem currently i have is that if i specify
Wd = 600; // width of source
Hg = 200; // height of source
the actuall trapeze is smaller (has less width and height),
also in some weird way manipulating this line:
YShift = Hg / 4;
changes the actuall height of trapeze.
its just then difficult to implement, as if i have been given svg court with certain size i need to be able to provide the actuall size to the function so then coords calculations will be accurate.
Lets say that i will be given court where i know 4 corners and based on that i need to be able to calculate coords. This implementation from my demo snippet, doesnt o it unfortunately.
Anyone could help alter the code or provide better implementation?
EDIT #3 - Resolution
this is final implementation:
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>
You are looking for a projective mapping from (x,y) in the court plane to (u,v) in the screen plane. A projective mapping works like this:
Append 1 to the court coordinates to get homogenous coordinates (x,y,1)
Multiply these homogenous coordinates with an appropriate 3x3 matrix M from the left to get homogenous coordinates (u',v',l) of the screen pixel
Dehomogenize the coordinates to get the actual screen coordinates (u,v) = (u'/l, v'/l)
The appropriate matrix M can be computed from solving the cooresponding equations for e.g. the corners. Choosing the court center to coincide with origin and the the x-axis pointing along the longer side and measuring the corner coordinates from your image, we get the following corresponding coordinates for a standard 105x68 court:
(-52.5, 34) -> (174, 57)
( 52.5, 34) -> (566, 57)
( 52.5,-34) -> (690,214)
(-52.5,-34) -> ( 50,214)
Setting up the equations for a projective mapping with matrix
/ a b c \
M = ( d e f )
\ g h 1 /
leads to a linear system with 8 equations and 8 unknowns, since each point correspondence (x,y) -> (u,v) contributes two equations:
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
(You get these two equations by expanding M (x y 1)^T = (l*u l*v l*1)^T into three equations and substituting the value for l from the third equation into the first two equations.)
The solution for the 8 unknowns a,b,c,...,h put into the matrix gives:
/ 4.63 2.61 370 \
M = ( 0 -1.35 -116.64 )
\ 0 0.00707 1 /
So given e.g. the court center as {x: 0.5, y: 0.5} you must first transform it into the above described coordinate system to get (x,y) = (0,0). Then you must compute
/ 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 /
By dehomogenizing you get the screen coordinates of the center as
(u,v) = (370/1, 116.64/1) ~= (370,117)
A JavaScript implementation could look like this:
// 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))
To make specific pespective projection that has axial symmetry and maps rectangle to isosceles trapezoid, we can build simpler model as I described here.
Let we want to map rectangle with coordinates (0,0)-(SrcWdt, SrcHgt) with axial line at SrcWdt/2
into region with axial line at DstWdt/2 and coordinates of right corners RBX, RBY, RTX, RTY
Here we need (partial) perspective transformation:
X' = DstXCenter + A * (X - XCenter) / (H * Y + 1)
Y' = (RBY + E * Y) / (H * Y + 1)
and we can calculate coefficients A, E, H without solving of eight linear equation system using coordinates of two corners of trapezoid.
Here is demonstration with Delphi code which finds coefficients and calculates mapping of some points into new region (Y-axis down, so perspective sight is from upper edge):
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);
I have implemented it in plain HTML and JavaScript. You have to adjust the variables to your need. A and B are the lengths of small and large parallel sides and H is the height of trapeze. x0, y0 are the co-ordinates of left-bottom corner of the field. If it works out for you I will explain the math.
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>
Related
Color quantization using euclidean distance gives jumpy results
I'm working on an art project which converts pixels of live video feed into corporate logos based on the distance (in RGB) between the colors of the two. While this functions, it gives a jittery result. Seem like some points in the color space teeter in a sort of superposition between two "closest" points. I'm attempting a sort of naive clustering solution right now because I believe any proper one will be too slow for live video. I'm wondering if anyone has any good ideas to solve this problem? I'll include my code and an example of the result. Thank you! (imgs array is the logos) current result: https://gifyu.com/image/fk2y function distance(r1, g1, b1, bright1, r2, g2, b2, bright2) { d = ((r2 - r1) * 0.3) ** 2 + ((g2 - g1) * 0.59) ** 2 + ((b2 - b1) * 0.11) ** 2 + ((bright2 - bright1) * 0.75) ** 2; return Math.round(d); } function draw() { if (x > 100 && z == true) { video.loadPixels(); for (var y = 0; y < video.height; y++) { for (var x = 0; x < video.width; x++) { var index = (video.width - x - 1 + y * video.width) * 4; var r = video.pixels[index]; var g = video.pixels[index + 1]; var b = video.pixels[index + 2]; var bright = (r + g + b) / 3; let least = 9999999; for (var i = 0; i < imgs.length; i++) { if ( distance( imgs[i].r, imgs[i].g, imgs[i].b, imgs[i].bright, r, g, b, bright ) < least ) { least = distance( imgs[i].r, imgs[i].g, imgs[i].b, imgs[i].bright, r, g, b, bright ); place = imgs[i].img; } } image(place, round(x * vScale), y * vScale, vScale, vScale); } } } }
Draw Map in Browser out of 2 Dimensional Array of Distances
I'm receiving all distances between a random number of points in a 2 dimensional coordinate system. How can I visualize this as coordinates on a map in my browser? In case there are many solutions I just want to see the first possible one that my algorithm can come up with. So here's an extremely easy example: PointCount = 3 Distances: 0-1 = 2 0-2 = 4 1-2 = 2 Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement? I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points. The above example is simplified - Real distance values could look like this: (0) (1) (2) (3) (0) 0 2344 3333 10000 (1) 0 3566 10333 (2) 0 12520
I'm not sure this is relevant for SO, but anyway... The way to do this is quite simply to place the points one by one using the data: Pick a random location for the first point (let's say it's 0,0). The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0). The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions. The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution. Continue like this until all points have been placed. Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference. A quick and dirty implementation (not handling quite a few cases, and tested very little): function distance(p1, p2) { return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2)); } // adapted from https://stackoverflow.com/a/12221389/3527940 function intersection(x0, y0, r0, x1, y1, r1) { var a, dx, dy, d, h, rx, ry; var x2, y2; /* dx and dy are the vertical and horizontal distances between * the circle centers. */ dx = x1 - x0; dy = y1 - y0; /* Determine the straight-line distance between the centers. */ d = Math.sqrt((dy * dy) + (dx * dx)); /* Check for solvability. */ if (d > (r0 + r1)) { /* no solution. circles do not intersect. */ return false; } if (d < Math.abs(r0 - r1)) { /* no solution. one circle is contained in the other */ return false; } /* 'point 2' is the point where the line through the circle * intersection points crosses the line between the circle * centers. */ /* Determine the distance from point 0 to point 2. */ a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d); /* Determine the coordinates of point 2. */ x2 = x0 + (dx * a / d); y2 = y0 + (dy * a / d); /* Determine the distance from point 2 to either of the * intersection points. */ h = Math.sqrt((r0 * r0) - (a * a)); /* Now determine the offsets of the intersection points from * point 2. */ rx = -dy * (h / d); ry = dx * (h / d); /* Determine the absolute intersection points. */ var xi = x2 + rx; var xi_prime = x2 - rx; var yi = y2 + ry; var yi_prime = y2 - ry; return [ [xi, yi], [xi_prime, yi_prime] ]; } function generateData(nbPoints) { var i, j, k; var originalPoints = []; for (i = 0; i < nbPoints; i++) { originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]); } var data = []; var distances; for (i = 0; i < nbPoints; i++) { distances = []; for (j = 0; j < i; j++) { distances.push(distance(originalPoints[i], originalPoints[j])); } data.push(distances); } //console.log("original points", originalPoints); //console.log("distance data", data); return data; } function findPointsForDistances(data, threshold) { var points = []; var solutions; var solutions1, solutions2; var point; var i, j, k; if (!threshold) threshold = 0.01; // First point, arbitrarily set at 0,0 points.push([0, 0]); // Second point, arbitrarily set at d(0,1),0 points.push([data[1][0], 0]); // Third point, intersection of two circles, pick any solution solutions = intersection( points[0][0], points[0][1], data[2][0], points[1][0], points[1][1], data[2][1]); //console.log("possible solutions for point 3", solutions); points.push(solutions[0]); //console.log("solution for points 1, 2 and 3", points); found = true; // Subsequent points, intersections of n-1 circles, use first two to find 2 solutions, // the 3rd to pick one of the two // then use others to check it's valid for (i = 3; i < data.length; i++) { // distances to points 1 and 2 give two circles and two possible solutions solutions = intersection( points[0][0], points[0][1], data[i][0], points[1][0], points[1][1], data[i][1]); //console.log("possible solutions for point " + (i + 1), solutions); // try to find which solution is compatible with distance to point 3 found = false; for (j = 0; j < 2; j++) { if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) { point = solutions[j]; found = true; break; } } if (!found) { console.log("could not find solution for point " + (i + 1)); console.log("distance data", data); console.log("solution for points 1, 2 and 3", points); console.log("possible solutions for point " + (i + 1), solutions); console.log("distances to point 3", distance(solutions[0], points[2]), distance(solutions[1], points[2]), data[i][2] ); break; } // We have found a solution, we need to check it's valid for (j = 3; j < i; j++) { if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) { console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1)); found = false; break; } } if (!found) { console.log("stopping"); break; } points.push(point); } if (found) { //console.log("complete solution", points); return points; } } console.log(findPointsForDistances([ [], [2344], [3333, 3566], [10000, 10333, 12520], ])); console.log(findPointsForDistances([ [], [2], [4, 2], ])); console.log(findPointsForDistances([ [], [4000], [5000, 3000], [3000, 5000, 4000] ])); console.log(findPointsForDistances([ [], [2928], [4938, 3437], [10557, 10726, 13535] ])); var nbPoints, i; for (nbPoints = 4; nbPoints < 8; nbPoints++) { for (i = 0; i < 10; i++) { console.log(findPointsForDistances(generateData(nbPoints))); } } Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like: y = canvasHeight - y If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis. let canvas = document.getElementById("canvas"); let ctx = canvas.getContext("2d"); let scale = 10; let radius = 10; function point(x, y) { ctx.fillRect(x*scale, y*scale, radius, radius); } // test point(10, 15); point(20, 8); <html> <body> <canvas id="canvas" width=1000 height=1000></canvas> </body> </html>
There are plenty of libraries out there. chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes. chartJS is another better looking library. And you can use html5 canvas it's easy and fun but it will take time especially in scaling. To scale and position, you should use the minimum and maximum values for x and y. Good luck
Find regions of similar color in image
I've been working on this problem for some time now with little promising results. I am trying to split up an image into connected regions of similar color. (basically split a list of all the pixels into multiple groups (each group containing the coordinates of the pixels that belong to it and share a similar color). For example: http://unsplash.com/photos/SoC1ex6sI4w/ In this image the dark clouds at the top would probably fall into one group. Some of the grey rock on the mountain in another, and some of the orange grass in another. The snow would be another - the red of the backpack - etc. I'm trying to design an algorithm that will be both accurate and efficient (it needs to run in a matter of ms on midrange laptop grade hardware) Below is what I have tried: Using a connected component based algorithm to go through every pixel from top left scanning every line of pixels from left to right (and comparing the current pixel to the top pixel and left pixel). Using the CIEDE2000 color difference formula if the pixel at the top or left was within a certain range then it would be considered "similar" and part of the group. This sort of worked - but the problem is it relies on color regions having sharp edges - if any color groups are connected by a soft gradient it will travel down that gradient and continue to "join" the pixels as the difference between the individual pixels being compared is small enough to be considered "similar". To try to fix this I chose to set every visited pixel's color to the color of most "similar" adjacent pixel (either top or left). If there are no similar pixels than it retains it's original color. This somewhat fixes the issue of more blurred boundaries or soft edges because the first color of a new group will be "carried" along as the algorithm progresses and eventually the difference between that color and the current compared color will exceed the "similarity" threashold and no longer be part of that group. Hopefully this is making sense. The problem is neither of these options are really working. On the image above what is returned are not clean groups but noisy fragmented groups that is not what I am looking for. I'm not looking for code specifically - but more ideas as to how an algorithm could be structured to successfully combat this problem. Does anyone have ideas about this? Thanks!
You could convert from RGB to HSL to make it easier to calculate the distance between the colors. I'm setting the color difference tolerance in the line: if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {...} If you change 0.3, you can get different results. See it working. Please, let me know if it helps. function hsl_to_rgb(h, s, l) { // from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion var r, g, b; if (s == 0) { r = g = b = l; // achromatic } else { var hue2rgb = function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } function rgb_to_hsl(r, g, b) { // from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion r /= 255, g /= 255, b /= 255; var max = Math.max(r, g, b), min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max == min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; } function color_distance(v1, v2) { // from http://stackoverflow.com/a/13587077/1204332 var i, d = 0; for (i = 0; i < v1.length; i++) { d += (v1[i] - v2[i]) * (v1[i] - v2[i]); } return Math.sqrt(d); }; function round_to_groups(group_nr, x) { var divisor = 255 / group_nr; return Math.ceil(x / divisor) * divisor; }; function pixel_data_to_key(pixel_data) { return pixel_data[0].toString() + '-' + pixel_data[1].toString() + '-' + pixel_data[2].toString(); } function posterize(context, image_data, palette) { for (var i = 0; i < image_data.data.length; i += 4) { rgb = image_data.data.slice(i, i + 3); hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]); key = pixel_data_to_key(hsl); if (key in palette) { new_hsl = palette[key]; new_rgb = hsl_to_rgb(new_hsl[0], new_hsl[1], new_hsl[2]); rgb = hsl_to_rgb(hsl); image_data.data[i] = new_rgb[0]; image_data.data[i + 1] = new_rgb[1]; image_data.data[i + 2] = new_rgb[2]; } } context.putImageData(image_data, 0, 0); } function draw(img) { var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); context.drawImage(img, 0, 0, canvas.width, canvas.height); img.style.display = 'none'; var image_data = context.getImageData(0, 0, canvas.width, canvas.height); var data = image_data.data; context.drawImage(target_image, 0, 0, canvas.width, canvas.height); data = context.getImageData(0, 0, canvas.width, canvas.height).data; original_pixels = []; for (i = 0; i < data.length; i += 4) { rgb = data.slice(i, i + 3); hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]); original_pixels.push(hsl); } group_headers = []; groups = {}; for (i = 0; i < original_pixels.length; i += 1) { if (group_headers.length == 0) { group_headers.push(original_pixels[i]); } group_found = false; for (j = 0; j < group_headers.length; j += 1) { // if a similar color was already observed if (color_distance(original_pixels[i], group_headers[j]) < 0.3) { group_found = true; if (!(pixel_data_to_key(original_pixels[i]) in groups)) { groups[pixel_data_to_key(original_pixels[i])] = group_headers[j]; } } if (group_found) { break; } } if (!group_found) { if (group_headers.indexOf(original_pixels[i]) == -1) { group_headers.push(original_pixels[i]); } if (!(pixel_data_to_key(original_pixels[i]) in groups)) { groups[pixel_data_to_key(original_pixels[i])] = original_pixels[i]; } } } posterize(context, image_data, groups) } var target_image = new Image(); target_image.crossOrigin = ""; target_image.onload = function() { draw(target_image) }; target_image.src = "http://i.imgur.com/zRzdADA.jpg"; canvas { width: 300px; height: 200px; } <canvas id="canvas"></canvas>
You can use "Mean Shift Filtering" algorithm to do the same. Here's an example. You will have to determine function parameters heuristically. And here's the wrapper for the same in node.js npm Wrapper for meanshift algorithm Hope this helps!
The process you are trying to complete is called Image Segmentation and it's a well studied area in computer vision, with hundreds of different algorithms and implementations. The algorithm you mentioned should work for simple images, however for real world images such as the one you linked to, you will probably need a more sophisticated algorithm, maybe even one that is domain specific (are all of your images contains a view?). I have little experience in Node.js, however from Googling a bit I found the GraphicsMagic library, which as a segment function that might do the job (haven't verified). In any case, I would try looking for "Image segmentation" libraries, and if possible, not limit myself only to Node.js implementations, as this language is not the common practice for writing vision applications, as opposed to C++ / Java / Python.
I would try a different aproach. Check out this description of how a flood fill algorithm could work: Create an array to hold information about already colored coordinates. Create a work list array to hold coordinates that must be looked at. Put the start position in it. When the work list is empty, we are done. Remove one pair of coordinates from the work list. If those coordinates are already in our array of colored pixels, go back to step 3. Color the pixel at the current coordinates and add the coordinates to the array of colored pixels. Add the coordinates of each adjacent pixel whose color is the same as the starting pixel’s original color to the work list. Return to step 3. The "search approach" is superior because it does not only search from left to right, but in all directions.
You might look at k-means clustering. http://docs.opencv.org/3.0-beta/modules/core/doc/clustering.html
Why is Firefox 30 times slower than Chrome, when calculating Perlin noise?
I have written a map generator in javascript, using classical perlin noise scripts I have found in various places, to get the functionality I want. I have been working in chrome, and have not experienced any problems with the map. However, when I tested it in firefox, it was incredibly slow - almost hanging my system. It fared better in the nightly build, but still 30 times slower than Chrome. You can find a test page of it here: http://jsfiddle.net/7Gq3s/ Here is the html code: <!DOCTYPE html> <html> <head> <title>PerlinMapTest</title> </head> <body> <canvas id="map" width="100" height="100" style="border: 1px solid red">My Canvas</canvas> <script src="//code.jquery.com/jquery-2.0.0.min.js"></script> <script> $(document).ready(function(){ //Log time in two ways var startTime = new Date().getTime(); console.time("Map generated in: "); var canvas = $("#map")[0]; var ctx = canvas.getContext("2d"); var id = ctx.createImageData(canvas.width, canvas.height); var noiseMap = new PerlinNoise(500); var startx = 0; var starty = 0; var value = 0; for(var i = startx; i < canvas.width; i++){ for(var j = starty; j < canvas.height; j++){ value = noiseMap.noise(i,j, 0, 42); value = linear(value,-1,1,0,255); setPixel(id, i, j, 0,0,0,value); } } ctx.putImageData(id,0,0); var endTime = new Date().getTime(); console.timeEnd("Map generated in: "); alert("Map generated in: " + (endTime - startTime) + "milliseconds"); }); function setPixel(imageData, x, y, r, g, b, a) { index = (x + y * imageData.width) * 4; imageData.data[index+0] = r; imageData.data[index+1] = g; imageData.data[index+2] = b; imageData.data[index+3] = a; } //This is a port of Ken Perlin's "Improved Noise" //http://mrl.nyu.edu/~perlin/noise/ //Originally from http://therandomuniverse.blogspot.com/2007/01/perlin-noise-your-new-best-friend.html //but the site appears to be down, so here is a mirror of it //Converted from php to javascript by Christian Moe //Patched the errors with code from here: http://asserttrue.blogspot.fi/2011/12/perlin-noise-in-javascript_31.html var PerlinNoise = function(seed) { this._default_size = 64; this.seed = seed; //Initialize the permutation array. this.p = new Array(512); this.permutation = [ 151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 ]; for (var i=0; i < 256 ; i++) { this.p[256+i] = this.p[i] = this.permutation[i]; } }; PerlinNoise.prototype.noise = function(x,y,z,size) { if (size == undefined) { size = this._default_size; } //Set the initial value and initial size var value = 0.0; var initialSize = size; //Add finer and finer hues of smoothed noise together while(size >= 1) { value += this.smoothNoise(x / size, y / size, z / size) * size; size /= 2.0; } //Return the result over the initial size return value / initialSize; }; //This function determines what cube the point passed resides in //and determines its value. PerlinNoise.prototype.smoothNoise = function(x, y, z){ //Offset each coordinate by the seed value x += this.seed; y += this.seed; z += this.seed; var orig_x = x; var orig_y = y; var orig_z = z; var X = Math.floor(x) & 255, // FIND UNIT CUBE THAT Y = Math.floor(y) & 255, // CONTAINS POINT. Z = Math.floor(z) & 255; x -= Math.floor(x); // FIND RELATIVE X,Y,Z y -= Math.floor(y); // OF POINT IN CUBE. z -= Math.floor(z); var u = this.fade(x), // COMPUTE FADE CURVES v = this.fade(y), // FOR EACH OF X,Y,Z. w = this.fade(z); var A = this.p[X ]+Y, AA = this.p[A]+Z, AB = this.p[A+1]+Z, // HASH COORDINATES OF B = this.p[X+1]+Y, BA = this.p[B]+Z, BB = this.p[B+1]+Z; // THE 8 CUBE CORNERS, return this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.p[AA ], x , y , z ), // AND ADD this.grad(this.p[BA ], x-1, y , z )), // BLENDED this.lerp(u, this.grad(this.p[AB ], x , y-1, z ), // RESULTS this.grad(this.p[BB ], x-1, y-1, z ))),// FROM 8 this.lerp(v, this.lerp(u, this.grad(this.p[AA+1], x , y , z-1 ), // CORNERS this.grad(this.p[BA+1], x-1, y , z-1 )), // OF CUBE this.lerp(u, this.grad(this.p[AB+1], x , y-1, z-1 ), this.grad(this.p[BB+1], x-1, y-1, z-1 )))); }; PerlinNoise.prototype.fade = function(t) { return t * t * t * ( ( t * ( (t * 6) - 15) ) + 10); }; PerlinNoise.prototype.lerp = function(t, a, b) { //Make a weighted interpolaton between points return a + t * (b - a); }; PerlinNoise.prototype.grad = function(hash, x, y, z) { h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE u = h<8 ? x : y; // INTO 12 GRADIENT DIRECTIONS. v = h<4 ? y : (h==12||h==14 ? x : z); return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v); }; PerlinNoise.prototype.scale = function(n) { return (1 + n)/2; }; function linear(int, s1, s2, t1, t2) { t = [t1, t2]; s = [s1, s2]; rangeS = s1 - s2; rangeT = t1 - t2; if((s1 < s2 && t1 > t2) || (s1>s2 && t1<t2)) { interpolated = ((int - s1) / rangeS*rangeT) + t1; } else { interpolated = ((int - s1) / rangeS)*rangeT + t1; } if(interpolated > Math.max.apply(Math, t)) { interpolated = Math.max.apply(Math, t); } if(interpolated < Math.min.apply(Math, t)) { interpolated = Math.min.apply(Math, t); } return interpolated; } </script> </body> </html> I get 33 ms on Chrome, and 1051ms on Firefox 24 Nightly The results are inconsistent though. Sometimes the Nightly results is as fast as chrome... Do you know why there is so much variation in this particular instance? I don't know enough about the theory of perlin noise to try optimizing the code, so don't know what to do.
I have found the culprit. The slowdown occurs when I have Firebug enabled. That extension must weigh it down.
JS/ CSS Matrix cube with axis scale
I have this code that generates the div. By applying the css transform property using matrix, I would to get the three faces of a cube, aligning the div properly. The problem is in the left div. Setting array leftArr scale (d * scale), I can not align vertically correctly div left side of the top div. Can anyone tell me the best way to get a simulation of a cube. Thank you. CSS: .face { height: 50px; overflow: hidden; position: absolute; width: 50px; } JS: var angle = 45, r = parseFloat(angle) * (Math.PI / 180), cos_theta = Math.cos(r), sin_theta = Math.sin(r); var a = cos_theta, b = sin_theta, c = -sin_theta, d = cos_theta; var face = 50, //reference to .face class k = 0, j = 100; //constant var scale = 3; var dX = face * Math.SQRT2 * scale; var dY = face * Math.SQRT2; for(var i = 0; i < 3; i++){ var tx = j + k; var ty = j; var lx = j + k - dX/4; var ly = ty; var topArr = [a * scale, b, c * scale, d, tx, ty]; var leftArr = [a * scale, b, 0, d * scale, lx, ly]; var top = 'matrix(' + topArr.join(',') + ')'; var left = 'matrix(' + leftArr.join(',') + ')'; k += dX; $('<div/>', { id : 'top_'+i, 'class' : 'face', css : { 'background' : 'hsla( ' + parseInt(Math.random() * 90) + ', 100%, 50%, 0.5 )', 'transform' : top } }).appendTo('body'); $('<div/>', { id : 'left_'+i, 'class' : 'face', css : { 'background' : 'hsla( ' + parseInt(Math.random() * 90) + ', 100%, 50%, 0.5 )', 'transform' : left } }).appendTo('body'); } Example: Scale = 1 Scale = 2 Scale = 3 UPDATE: After some test with: var ly = ty + dY/2 + ( ( (dY/2)*(scale-1) ) / 2); the code take sense, but if there are better solution, any help is appreciate.
You are using 2d transforms to rotate in 3d. If you want an elegant solution, you should use 3d matrices, that are of dimension 4. Then you would have the left side come from a 90 degrees turn from the down side; and the same translation would be applied to that. If you want to use 2d transforms, then the best way to go would be to precalculate the 2 d transforms for each face. Then calculate the translation matrix for all the cube (only 1 matrix, you are moving all faces at the same time). The matrices for each face will the product of the translation matrix and the face matrix. (keep in mind that this is not conmutative, the order is important)