As a part time project I am working on some geometry utilities and have come across a relatively simple question that seems to have a not so simple solution.
The problem involves EPSILON being too small for the problem. To see if two triangle are similar I workout the 3 interior angles in the form of their cosines for each triangle and then sort them. I then test Math.abs(t1[0]-t2[0]) < EPSILON
where t1 is the one triangle and t2 the other each containing the three angles.
I am getting about a 20% - 80% failure rate on triangles I know to be similar. When I bring EPSILON to a larger value, for example still a very small 0.0000001 there is no failure ( well not in the time I have let the tests run).
Below is the extracted relevant triangle function and I have also included the testing code as a demo below that. Click the button and its runs tests and shows the results. The triangles are randomly generated. Every so often two similar triangle are created of which about half are exact copies and the rest are a copy but scaled, mirrored, rotated and vec order shuffled while still maintaining the similarity
I would like to know how to calculate a reasonable EPSILON that will reduce the incorrect results but keep the system as accurate as possible?
Though there is also the 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 a-b };
// 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
Some more information.
Failed similar triangle using cosine of 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
Results output and a bug fix (apologies @lhf I did not sqrt epsilon I was still using the original constant)
This shows the results of tests on the same set of triangles. Inconsistency means that comparing triangle 1 to triangle 2 is a different result than triangle 2 to 1. Incorrect Means that two known similar triangles failed and Incorrect Inconsistency means that two known similar triangle failed one test and passed the other.
Using the ratios of lengths gave the worst result but using cosine was not much better apart from the Incorrect Inconsistency similar triangles which had a very high rate of inconsistency between compare t1 to t2 and t2 to t1 using the ratio of length. But that makes sense are the magnitude of the ratios will vary greatly depending on which order the test is done.
As you can see using the square root of EPSILON completely eliminated the error for both methods.
If lhf wishes to put the sqrt(epsilon) comment as an answer I will accept that as an answer. And thanks to everyone for their input and I have some further reading 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 a-b };
// 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);
Following willywokka's comment. You may be able to just see if there is a single 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
Rather than working with an absolute epsilon you might want one which is within x% of the value. So that the values are considered equal if ka - x% < kb < ka + x%, that is (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 would involve a pretty precise definition of what we mean by similar and examining the distribution of triangles. Statistical shape analysis is a poor starting point. David George Kendall did work examining the shape of triangles.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With