Are two triangles similar?

As a partial-time project, I work on some geometry utilities and come across a relatively simple question that seems to have a not-so-simple solution.

The problem is that EPSILON is too small for the problem. To see if the two triangles are similar, I train the 3 internal corners as their cosines for each triangle, and then sort them. Then I test Math.abs(t1[0]-t2[0]) < EPSILON , where t1 is one triangle and t2 is another, each of which contains three angles.

I get about 20% - 80% failures on triangles, which, as I know, are similar. When I bring EPSILON to a larger value, for example, still very small 0.0000001, there is no failure (well, not at the time I gave the tests).

Below is the extracted relevant triangle function, and I also included the test code as a demo below. Press the button and run the tests and show the results. Triangles are randomly generated. Each one so often forms two identical triangles, of which approximately half are exact copies, and the rest are copies, but the scaled, mirrored, rotated and vec-order are shuffled, while maintaining similarity

I would like to know how to calculate a reasonable EPSILON that will reduce incorrect results but keep the system as accurate as possible?

Although there is a possibility that there is another error in the test code, which I will continue to check.

  const EPSILON = Number.EPSILON function Triangle(p1,p2,p3){ this.p1 = p1; this.p2 = p2; this.p3 = p3; } Triangle.prototype.p1 = undefined; Triangle.prototype.p2 = undefined; Triangle.prototype.p3 = undefined; Triangle.prototype.isSimilar = function(triangle){ var a1,b1,c1,a2,b2,c2,aa1,bb1,cc1,aa2,bb2,cc2; // var t1 = []; var t2 = []; var sortF = function(a,b){ return ab }; // get the length squared and length of each side a1 = Math.sqrt(aa1 = Math.pow(this.p1.x - this.p2.x, 2) + Math.pow(this.p1.y - this.p2.y, 2)); b1 = Math.sqrt(bb1 = Math.pow(this.p2.x - this.p3.x, 2) + Math.pow(this.p2.y - this.p3.y, 2)); c1 = Math.sqrt(cc1 = Math.pow(this.p3.x - this.p1.x, 2) + Math.pow(this.p3.y - this.p1.y, 2)); a2 = Math.sqrt(aa2 = Math.pow(triangle.p1.x - triangle.p2.x, 2) + Math.pow(triangle.p1.y - triangle.p2.y, 2)); b2 = Math.sqrt(bb2 = Math.pow(triangle.p2.x - triangle.p3.x, 2) + Math.pow(triangle.p2.y - triangle.p3.y, 2)); c2 = Math.sqrt(cc2 = Math.pow(triangle.p3.x - triangle.p1.x, 2) + Math.pow(triangle.p3.y - triangle.p1.y, 2)); // get the cosin of each angle for both triangle t1[0] = (cc1 - (aa1 + bb1)) / (-2 * a1 * b1); t1[1] = (aa1 - (cc1 + bb1)) / (-2 * c1 * b1); t1[2] = (bb1 - (cc1 + aa1)) / (-2 * c1 * a1); t2[0] = (cc2 - (aa2 + bb2)) / (-2 * a2 * b2); t2[1] = (aa2 - (cc2 + bb2)) / (-2 * c2 * b2); t2[2] = (bb2 - (cc2 + aa2)) / (-2 * c2 * a2); t1.sort(sortF); t2.sort(sortF); if(Math.abs(t1[0] - t2[0]) < EPSILON && Math.abs(t1[1] - t2[1]) < EPSILON && Math.abs(t1[2] - t2[2]) < EPSILON){ return true; } return false; } function Vec(x,y){ this.x = x; this.y = y; } Vec.prototype.x = undefined; Vec.prototype.y = undefined; 

UPDATE

Additional Information.

Error in a similar triangle using the cosine of the angles EPSILON: 2.220446049250313e-16

 Failed Triangles ID : 94 Method : compare cosine of angles Both Compare T1 to T2 and T2 to T1 failed Both Triangles known to be similare Triangle 1 p1.x = -149241116087155.97; p1.y = -1510074922190599.8; p2.x = -2065214078816255.8; p2.y = 6756872141691895; p3.x = -7125027429739231; p3.y = -5622578541875555; Triangle 2 p1.x = -307440480802857.2; p1.y = -404929352172871.56; p2.x = -3020163594243123; p2.y = -355583557775981.75; p3.x = 595422457974710.8; p3.y = 2291176238828451.5; Compare T1 to T2 Result : false Computed values Triangle 1 length of side and square length length a : 8486068945686473 squared : 7.201336615094433e+31 length b : 13373575078230092 squared : 1.78852510373057e+32 length c : 8097794805726894 squared : 6.557428071565746e+31 Unsorted cosines C is angle opposite side c cosine C : 0.8163410767815653 cosine A : 0.7960251614312384 cosine B : -0.30024590551189423 ratio a : undefined ratio b : undefined ratio c : undefined Triangle2 length a : 2713171888697380.5 squared : 7.36130169761771e+30 length b : 4480825808030667.5 squared : 2.0077799921913682e+31 length c : 2843263414467020.5 squared : 8.08414684404666e+30 Unsorted cosines C is angle opposite side c cosine C : 0.7960251614312384 cosine A : 0.8163410767815651 cosine B : -0.3002459055118942 Compare T2 to T1 Result : false Triangle1 Computed values Triangle 1 length of side and square length length a : 2713171888697380.5 squared : 7.36130169761771e+30 length b : 4480825808030667.5 squared : 2.0077799921913682e+31 length c : 2843263414467020.5 squared : 8.08414684404666e+30 Unsorted cosines C is angle opposite side c cosine a : 0.7960251614312384 cosine b : 0.8163410767815651 cosine c : -0.3002459055118942 ratio a : undefined ratio b : undefined ratio c : undefined Triangle2 length a : 8486068945686473 squared : 7.201336615094433e+31 length b : 13373575078230092 squared : 1.78852510373057e+32 length c : 8097794805726894 squared : 6.557428071565746e+31 cosine a : 0.8163410767815653 cosine b : 0.7960251614312384 cosine c : -0.30024590551189423 

UPDATE 2

Outputting results and fixing errors (apologies @lhf I did not use sqrt epsilon, I still used the original constant)

Here are the test results for the same set of triangles. Inconsistency means that comparing triangle 1 with triangle 2 is a different result than triangle 2 to 1. False means that two well-known similar triangles failed, and incorrect inconsistency means that two well-known similar triangles did not pass one test and passed the other.

Using length relationships gave the worst result, but using cosine was not much better than similar triangles with irregular inconsistencies, which had a very high inconsistency rate between comparing t1 and t2 and t2-t1 using the length ratio. But it makes sense that the magnitude of the coefficients will vary greatly depending on which order is executed.

As you can see, using the square root of EPSILON completely eliminated the error for both methods.

If lhf wants to put sqrt (epsilon) comment as an answer, I agree as an answer. And thanks to everyone for their input, and I have some further readings thanks to Salix

 ====================================== Default EPSILON : 2.220446049250313e-16 ====================================== Via cosine of angles All Inconsistency failed : 0 of 10000 Similar Incorrect failed : 1924 of 5032 Similar Incorrect Inconsistency failed : 0 of 5032 ====================================== Via ratio of lengths All Inconsistency failed : 1532 of 10000 Similar Incorrect failed : 2082 of 5032 Similar Incorrect Inconsistency failed : 1532 of 5032 ====================================== Squaring EPSILON : 1.4901161193847656e-8 ====================================== Via cosine of angles All Inconsistency failed : 0 of 10000 Similar Incorrect failed : 0 of 5032 Similar Incorrect Inconsistency failed : 0 of 5032 ====================================== Via ratio of lengths All Inconsistency failed : 0 of 10000 Similar Incorrect failed : 0 of 5032 Similar Incorrect Inconsistency failed : 0 of 5032 

 const EPSILON = Number.EPSILON function Triangle(p1,p2,p3){ this.p1 = p1; this.p2 = p2; this.p3 = p3; } Triangle.prototype.p1 = undefined; Triangle.prototype.p2 = undefined; Triangle.prototype.p3 = undefined; Triangle.prototype.isSimilar = function(triangle){ var a1,b1,c1,a2,b2,c2,aa1,bb1,cc1,aa2,bb2,cc2; // var t1 = []; var t2 = []; var sortF = function(a,b){ return ab }; // get the length squared and length of each side a1 = Math.sqrt(aa1 = Math.pow(this.p1.x - this.p2.x, 2) + Math.pow(this.p1.y - this.p2.y, 2)); b1 = Math.sqrt(bb1 = Math.pow(this.p2.x - this.p3.x, 2) + Math.pow(this.p2.y - this.p3.y, 2)); c1 = Math.sqrt(cc1 = Math.pow(this.p3.x - this.p1.x, 2) + Math.pow(this.p3.y - this.p1.y, 2)); a2 = Math.sqrt(aa2 = Math.pow(triangle.p1.x - triangle.p2.x, 2) + Math.pow(triangle.p1.y - triangle.p2.y, 2)); b2 = Math.sqrt(bb2 = Math.pow(triangle.p2.x - triangle.p3.x, 2) + Math.pow(triangle.p2.y - triangle.p3.y, 2)); c2 = Math.sqrt(cc2 = Math.pow(triangle.p3.x - triangle.p1.x, 2) + Math.pow(triangle.p3.y - triangle.p1.y, 2)); // get the cosin of each angle for both triangle t1[0] = (cc1 - (aa1 + bb1)) / (-2 * a1 * b1); t1[1] = (aa1 - (cc1 + bb1)) / (-2 * c1 * b1); t1[2] = (bb1 - (cc1 + aa1)) / (-2 * c1 * a1); t2[0] = (cc2 - (aa2 + bb2)) / (-2 * a2 * b2); t2[1] = (aa2 - (cc2 + bb2)) / (-2 * c2 * b2); t2[2] = (bb2 - (cc2 + aa2)) / (-2 * c2 * a2); t1.sort(sortF); t2.sort(sortF); if(Math.abs(t1[0] - t2[0]) < EPSILON && Math.abs(t1[1] - t2[1]) < EPSILON && Math.abs(t1[2] - t2[2]) < EPSILON){ return true; } return false; } function Vec(x,y){ this.x = x; this.y = y; } Vec.prototype.x = undefined; Vec.prototype.y = undefined; var iterations = 1000; // number of tests var presentSimilar = 1/2; // odds of similar triangle var presentSuperSimilar = 1/2; // odds of triangles being identical var presentInfinity = 0;//1/20; // odds of a divide by zero var presentDegenerate = 0;//1/100; // odds of a degenerate triangle can be colinear or degenerate right triangle var v; // temp for swap var xdx,xdy,ydx,ydy; // vars for rotation var copyVec = [["p1","p2","p3"],["p2","p3","p1"],["p3","p1","p2"]]; // pick a vec for selecting vecs // the triangles for testing; var tri1 = new Triangle(new Vec(0,0), new Vec(0,0), new Vec(0,0)); var tri2 = new Triangle(new Vec(0,0), new Vec(0,0), new Vec(0,0)); // max Random function rMax(){ return ((Math.random()*2)-1) * Number.MAX_SAFE_INTEGER; } // rotate function function rotate(vec){ var x = vec.x; var y = vec.y; vec.x = x * xdx + y * ydx; vec.y = x * xdy + y * ydy; }; function translateVec(vec,x,y){ vec.x += x; vec.y += y; } function translateTriangle(tri,x,y){ translateVec(tri.p1); translateVec(tri.p2); translateVec(tri.p3); } // make infinite vec to simulate the result of a divide by zero function doInfinity(vec){ if(Math.random() < presentInfinity){ if(Math.random() < 0.5){ vec.x = Infinity; vec.y = Infinity; }else{ vec.x = -Infinity; vec.y = -Infinity; } } } // create a random vector; function randomVec(vec){ vec.x = rMax(); vec.y = rMax(); doInfinity(vec); } // create a random triangle function randomTriangle(tri){ var p,r; randomVec(tri.p1); randomVec(tri.p2); randomVec(tri.p3); if(Math.random() < presentDegenerate){ r = Math.random(); if(r < 1/3){ // Degenerate right triangle p = copyVec[Math.floor(Math.random()*3)]; // get two vec to be at the same location tri[p[0]].x = tri[p[1]].x; tri[p[0]].y = tri[p[1]].y; }else // Degenerate colinear triangle if(r < 2/3){ p = copyVec[Math.floor(Math.random()*3)]; // get two vec to be at the same location r = Math.random(); tri[p[0]].x = (tri[p[1]].x - tri[p[2]].x) * r + tri[p[2]].x; tri[p[0]].y = (tri[p[1]].y - tri[p[2]].y) * r + tri[p[2]].y; }else{ // degenerate undimentioned triangle. Has not area tri.p1.x = tri.p2.x = tri.p3.x; tri.p1.y = tri.p2.y = tri.p3.y; } } } function runTest(){ var result1,result2,mustBeSimilar; var countSimilar = 0; var countNorm = 0; var error1 = 0; var error2 = 0; for(var i = 0; i < iterations; i ++){ randomTriangle(tri1); if(Math.random() < presentSimilar){ mustBeSimilar = true; countSimilar += 1; tri2.p1.x = tri1.p1.x; tri2.p1.y = tri1.p1.y; tri2.p2.x = tri1.p2.x; tri2.p2.y = tri1.p2.y; tri2.p3.x = tri1.p3.x; tri2.p3.y = tri1.p3.y; if(Math.random() >= presentSuperSimilar){ if(Math.random() < 0.5){ // swap two v = tri2.p1; tri2.p1 = tri2.p2; tri2.p2 = v; } if(Math.random() < 0.5){ // swap two v = tri2.p2; tri2.p2 = tri2.p3; tri2.p3 = v; } if(Math.random() < 0.5){ // swap two v = tri2.p1; tri2.p1 = tri2.p3; tri2.p3 = v; } // scale and or mirror the second triangle v = Math.random() * 2 - 1; tri2.p1.x *= v; tri2.p1.y *= v; tri2.p2.x *= v; tri2.p2.y *= v; tri2.p3.x *= v; tri2.p3.y *= v; // rotate the triangle v = (Math.random()- 0.5) * Math.PI * 4; ydy = xdx = Math.cos(v); ydx = -(xdy = Math.sin(v)); rotate(tri2.p1); rotate(tri2.p2); rotate(tri2.p3); } }else{ randomTriangle(tri2); mustBeSimilar = false; } countNorm += 1; result1 = tri1.isSimilar(tri2); result2 = tri2.isSimilar(tri1); if(result1 !== result2){ error1 += 1; } if(mustBeSimilar && (!result1 || !result2)){ error2 += 1; } for(var j = 0; j < 10; j++){ translateTriangle(tri1,Math.random(),Math.random()); translateTriangle(tri2,Math.random(),Math.random()); if(mustBeSimilar){ countSimilar += 1; } countNorm += 1; result1a = tri1.isSimilar(tri2); result2a = tri2.isSimilar(tri1); if(result1a !== result2a || result1 !== result1a || result2 !== result2a){ error1 += 1; } if(mustBeSimilar && (!result1a || !result2a)){ error2 += 1; } } } divResult1.textContent = "Inconsistancy result failed : "+error1 + " of "+ countNorm; divResult2.textContent = "Incorrect result failed : "+error2 + " of "+ countSimilar } var button = document.createElement("input"); button.type = "button" button.value = "Run test" button.onclick = runTest; var divResult1 = document.createElement("div"); var divResult2 = document.createElement("div"); document.body.appendChild(button); document.body.appendChild(divResult1); document.body.appendChild(divResult2); 
+5
source share
2 answers

Next willywokka comment. You can just see if there is one scale factor.

 // get the length squared and length of each side a1 = Math.sqrt(...); .... // Sort the values so a1 < b1 < c1, a2 < b2 < c2 if(b1 < a1) { tmp = b1; b1 = a1; a1 = tmp } if(c1 < a1) { tmp = c1; c1 = a1; a1 = tmp } if(c1 < b1) { tmp = c1; c1 = b1; b1 = tmp } if(b2 < a2) { tmp = b2; b2 = a2; a2 = tmp } if(c2 < a2) { tmp = c2; c2 = a2; a2 = tmp } if(c2 < b2) { tmp = c2; c2 = b2; b2 = tmp } // work out the scale factors ka = a2 / a1; kb = b2 / b1; kc = c2 / c1; if( abs(ka - kb) < epsilon && abs(kc - ka) < epsilon && abs(kc - kb) < epsilon ) // similar else // not similar 

Instead of working with absolute epsilon, you may need one that is within x% of the value. Thus, the values ​​are considered equal if ka - x% <kb <ka + x%, i.e. (1-x / 100) ka <kb <(1 + x / 100) ka. Or (1-x / 100) kb / ka <(1 + x / 100) or abs (kb / ka) x / 100.


There is a statistically more rigorous approach to the problem. This implies a fairly accurate definition of what we mean by a similar and consider the distribution of triangles. Statistical form analysis is a poor starting point. David George Kendall worked on looking at the shape of triangles.

+2
source

If these are really just angles, you can simply calculate the three internal angles of both triangles. Just use the cosine

 cos(angle) = dot (normalized(edge[i]),normalized(edge[(i+1)%3])). with edge[i] = p[(i+1)%3] - p[i]. 

So, you have three cos (angle) for each triangle, one and two. Then just check each permutation. A triangle has only six permutations. ( http://mathworld.wolfram.com/Permutation.html )

 besterr = max; for i=1..6 perm(i) in tri1 for j=1..6 perm(j) in tri2 err = 0 for k=1..3 angle err += abs(angletri1[perm[i,k]] - angletri2[perm[j,k]]) if (err<besterr) besterr = err; return besterr; 

Does this mean your expected result? We certainly can do more efficient. But this is brute force testing algorithm. It should be noted that it works only for triangles - any permutation of the vertices in the triangle is the same contour of the triangle. This would not be for a larger landfill.

After that, you can start experimenting. Do you get the same result for angle and cos (angle)? For err + = abs (d) and err + = d * d? Can you just check 2 permutations by sorting the angles? Remember the sum of the angles of the triangle ( https://en.wikipedia.org/wiki/Sum_of_angles_of_a_triangle ). Which calculations are redundant?

And finally: is this the metric you want? Are two triangles with opposite windings really similar? Huge and tiny?

0
source

Source: https://habr.com/ru/post/1247511/