0

I have a string of coordinates, read from a JSON file, which belongs to a certain ROI, drawn by the user of an application. I am supposed to eventually determine if the coordinates represent a polygon or an ellipse (AKA did the user paint a polygon or an ellipse). I have a serious problem of figuring out the correct algorithm, since it is also a math-related problem. The code have written so far, recognizes an ellipse, but then considers a polygon also an ellipse. My initial idea was to put all the coords inside the ellipse equation and check if it matches the characteristics of an ellipse. But somehow I fail to differentiate it from the polygon.

Example for the format of the coordinates that I retrieve from the JSON file:

Ellipse coordinates:

M/322.504/80.6014;L/322.3/84.7773;L/321.684/88.899;L/319.253/96.9595;L/315.292/104.742;L/309.881/112.205;L/303.102/119.309;L/295.036/126.012;L/285.763/132.273;L/275.364/138.052;L/263.921/143.307;L/251.515/147.997;L/238.226/152.082;L/224.136/155.521;L/209.325/158.273;L/193.875/160.297;L/177.866/161.551;L/161.38/161.996;L/144.892/161.603;L/128.88/160.399;L/113.423/158.423;L/98.6038/155.718;L/84.5029/152.323;L/71.2013/148.28;L/58.7804/143.628;L/47.3212/138.409;L/36.9047/132.663;L/27.6122/126.431;L/19.5247/119.753;L/12.7234/112.671;L/7.28933/105.224;L/3.30368/97.4543;L/0.847538/89.4014;L/0.218384/85.2816;L/0.00202717/81.1064;L/0.205307/76.9305;L/0.821556/72.8088;L/3.25246/64.7483;L/7.21376/56.9658;L/12.6245/49.5023;L/19.4036/42.3987;L/27.4701/35.6959;L/36.7431/29.4347;L/47.1415/23.6562;L/58.5843/18.4012;L/70.9906/13.7107;L/84.2794/9.62544;L/98.3697/6.1865;L/113.18/3.43473;L/128.631/1.41106;L/144.639/0.156398;L/161.126/-0.288348;L/177.613/0.104763;L/193.626/1.30929;L/209.083/3.28456;L/223.902/5.98993;L/238.003/9.38472;L/251.304/13.4283;L/263.725/18.08;L/275.185/23.2991;L/285.601/29.045;L/294.894/35.2771;L/302.981/41.9547;L/309.782/49.037;L/315.216/56.4835;L/319.202/64.2535;L/321.658/72.3064;L/322.287/76.4262;L/322.504/80.6014

Polygon coordinates:

M/0.0102565/69.1651;L/19.019/51.4713;L/19.6427/5.25438;L/111.389/0.385824;L/112.778/24.1719;L/288.807/24.6385;L/288.255/129.985;L/242.72/131.399;L/221.009/162.01;L/138.096/166.188;L/116.982/128.833;L/113.55/100.971;L/65.9781/103.747;L/48.9573/79.3007;L/1.3638/64.406

So, the program is supposed to determine that the first set of coordinates belong to an ellipse, and the second one to a polygon.

Note: the number of coordinates are never the same, since it is fully up to the user, how the polygon should look like, therefore it is not safe to say that the length of the coordinates from a polygon will also be fewer than an ellipse.

I thank you guys in advance and hope to be able to find a solution for this problem.

My current code:


#include <iostream>
#include <vector>
#include <sstream>
#include <cmath>

// Structure to hold coordinate data
struct Coordinate {
    double x;
    double y;
};

// Function to check if coordinates satisfy the ellipse equation
bool isEllipse(const std::vector<Coordinate>& coordinates) {
    // Number of coordinates
    int numPoints = coordinates.size();

    // Calculate the centroid
    double sumX = 0.0, sumY = 0.0;
    for (const auto& coord : coordinates) {
        sumX += coord.x;
        sumY += coord.y;
    }
    double centerX = sumX / numPoints;
    double centerY = sumY / numPoints;

    // Calculate the major and minor axes
    double maxDistSqr = 0.0;
    for (const auto& coord : coordinates) {
        double dx = coord.x - centerX;
        double dy = coord.y - centerY;
        double distSqr = dx * dx + dy * dy;
        if (distSqr > maxDistSqr) {
            maxDistSqr = distSqr;
        }
    }
    double a = std::sqrt(maxDistSqr);
    double b = std::sqrt(maxDistSqr);

    // Check if the coordinates satisfy the ellipse equation
    for (const auto& coord : coordinates) {
        double dx = coord.x - centerX;
        double dy = coord.y - centerY;
        double ellipseResult = (dx * dx) / (a * a) + (dy * dy) / (b * b);
        if (ellipseResult > 1.0) {
            return false;
        }
    }

    return true;
}

int main() {
    std::string coordinatesStr = "M/-0.247283/512.418;L/166.935/505.209;L/175.478/415.698;L/141.124/384.968;L/207.244/354.6;L/211.621/292.758;L/269.472/283.712;L/259.446/10.8997;L/773.612/-0.156929;L/773.644/277.548;L/850.632/280.289;L/850.638/335.28;L/927.629/368.266;L/886.392/423.262;L/894.646/470.004;L/1007.98/479.435;L/1015.16/565.507;L/20.5376/572.793;L/-0.0663959/512.529;L/0.189167/513.04";

    std::vector<Coordinate> coordinates;

    // Parse the coordinate string
    std::istringstream iss(coordinatesStr);
    std::string segment;
    while (std::getline(iss, segment, ';')) {
        // Parse M or L coordinates
        if (segment[0] == 'M' || segment[0] == 'L') {
            std::istringstream coordIss(segment.substr(2));
            std::string xStr, yStr;
            if (std::getline(coordIss, xStr, '/') && std::getline(coordIss, yStr, '/')) {
                try {
                    double x = std::stod(xStr);
                    double y = std::stod(yStr);
                    coordinates.push_back({ x, y });
                } catch (const std::invalid_argument& e) {
                    std::cerr << "Failed to parse coordinate: " << segment << std::endl;
                }
            }
        }
    }

    // Check if the coordinates satisfy the ellipse equation
    bool isAnEllipse = isEllipse(coordinates);

    if (isAnEllipse) {
        std::cout << "The coordinates form an ellipse." << std::endl;
    } else {
        std::cout << "The coordinates do not form an ellipse." << std::endl;
    }

    return 0;
}
Marek R
  • 32,568
  • 6
  • 55
  • 140
Melika
  • 15
  • 5
  • Do you have to write the algorithm yourself or can you use a library ? This sound 100% like a problem i would pull some 3rd party code for.... – nick Jul 10 '23 at 14:04
  • Can this be any kind of ellipse or is it just ellipse which main axis is vertical or horizontal? Why are you assuming that all points of ellipse are evenly distributed? You can have part of ellipse, an arc. – Marek R Jul 10 '23 at 14:06
  • @nick I would rather implement it myself, as long as the solution is not an overkill, since I'm working on a personal project. If the library you are talking about exists as a C++ or Qt library, it's ok. Else I would not want any third parties in my program. – Melika Jul 10 '23 at 14:06
  • @MarekR it can be any type of ellipse, AKA horizontal, vertical, or even a circle (as an edge case). – Melika Jul 10 '23 at 14:08
  • Possible duplicate: https://stackoverflow.com/a/47881806/1387438 – Marek R Jul 10 '23 at 14:10
  • I imagine it can be very problematic to tell the difference between some convex polygons and an ellipse. Concave polygons can't be ellipses, so that shouldn't be a problem. – Ted Lyngmo Jul 10 '23 at 14:11
  • @TedLyngmo that's right, and the problem is that the shape of the polygon has no limits, and is not predictable. So it can be drawn concave by the user, or not. Since everything could be possible, the code needs to be flexible and determine the correct shape in any case, which makes it super difficult to solve. – Melika Jul 10 '23 at 14:16
  • @MarekR I appreciate the link you provided, but it does not help much in my case :( – Melika Jul 10 '23 at 14:16
  • You need just find equesion of ellipse then see if all points are close enough to it. So provided link should be helpful. – Marek R Jul 10 '23 at 14:17
  • @MarekR That would classify some convex polygons as ellipses if I'm not mistaken. – Ted Lyngmo Jul 10 '23 at 14:18
  • @MarekR I am doing such thing, but the code mistakenly classifies some polygons as an ellipse as well... – Melika Jul 10 '23 at 14:19
  • The equation of an ellipse with arbitrary orientation is ax^2 +bxy +cy^2 +dx +ey =1, with a and c non-zero and the same sign. If you do this for 5 points (x,y) then you should get a 5x5 matrix equation for a,b,c,d,e. The remaining points then have to fit this equation (to within some floating-point tolerance). If there are less than five points then there are multiple possible ellipses. – lastchance Jul 10 '23 at 14:54
  • @lastchance Can you possibly provide an example or a snippet for better understanding? Thank you so much :) – Melika Jul 10 '23 at 15:03
  • Your code fails even on simple test case: https://godbolt.org/z/Tq9nM9Peq – Marek R Jul 10 '23 at 15:17
  • 2
    Don't check if the coordinates satisfy the ellipse function, check the _error_ between your points and the ellipse function, because humans can't draw perfect geometry. You're aiming for a confidence level (this shape is more likely to be an ellipse than not), not an all-or-nothing check. Each point has an angle wrt the center, and you know the distance to the center for a point on the ellipse with that angle. Aggregate the mean square error between what the distance "should be", and what it actually is, and then you can decide how badly something needs to match to still count as ellipse. – Mike 'Pomax' Kamermans Jul 10 '23 at 15:27

1 Answers1

4

Well, I guess it comes down to fitting the best ellipse (e.g. by least squares) and then deciding whether the (normalised) error is less than some pre-set tolerance.

The general ellipse (which is NOT necessarily aligned with the coordinate axes) is ax2+bxy+cy2+dx+ey+f=0, with b2-4ac<0.

Since the coefficient of x2, a, can't be zero for an ellipse you can divide through by this, or, equivalently, take a=1. You then need to fit the free parameters b, c, d, e, f which you can do by minimising the squared error

sum(over all points) (ax2+bxy+cy2+dx+ey+f)2

Differentiating w.r.t. each parameter in turn you get five linear equations for the five parameters. (If you want an ellipse with axes parallel to the coordinate axes then take b=0 and reduce the number of parameters to 4.)

This gives the best-fit ellipse, but to decide whether your set of points fits it adequately you need to look at the sum-of-squared-errors (or, rather, that normalised, since it would otherwise depend on the units for x and y).

With a tolerance of 0.1%, your first example fits an ellipse and your second doesn't.

#include <iostream>
#include <vector>
#include <utility>
#include <cmath>
using namespace std;

struct Pt{ double x, y; };

bool isEllipse( const vector<Pt> &S );
bool solve( vector<vector<double>> A, vector<double> B, vector<double> &X );

//======================================================================

int main()
{
   bool result;

   vector<Pt> case1 = {
{322.504,80.6014}, {322.3,84.7773}, {321.684,88.899}, {319.253,96.9595}, {315.292,104.742},
{309.881,112.205}, {303.102,119.309}, {295.036,126.012}, {285.763,132.273}, {275.364,138.052}, 
{263.921,143.307}, {251.515,147.997}, {238.226,152.082}, {224.136,155.521}, 
{209.325,158.273}, {193.875,160.297}, {177.866,161.551}, {161.38,161.996}, 
{144.892,161.603}, {128.88,160.399}, {113.423,158.423}, {98.6038,155.718}, 
{84.5029,152.323}, {71.2013,148.28}, {58.7804,143.628}, {47.3212,138.409}, 
{36.9047,132.663}, {27.6122,126.431}, {19.5247,119.753}, {12.7234,112.671}, 
{7.28933,105.224}, {3.30368,97.4543}, {0.847538,89.4014}, {0.218384,85.2816}, 
{0.00202717,81.1064}, {0.205307,76.9305}, {0.821556,72.8088}, {3.25246,64.7483}, 
{7.21376,56.9658}, {12.6245,49.5023}, {19.4036,42.3987}, {27.4701,35.6959}, 
{36.7431,29.4347}, {47.1415,23.6562}, {58.5843,18.4012}, {70.9906,13.7107}, 
{84.2794,9.62544}, {98.3697,6.1865}, {113.18,3.43473}, {128.631,1.41106}, 
{144.639,0.156398}, {161.126,-0.288348}, {177.613,0.104763}, {193.626,1.30929}, 
{209.083,3.28456}, {223.902,5.98993}, {238.003,9.38472}, {251.304,13.4283}, 
{263.725,18.08}, {275.185,23.2991}, {285.601,29.045}, {294.894,35.2771}, 
{302.981,41.9547}, {309.782,49.037}, {315.216,56.4835}, {319.202,64.2535}, 
{321.658,72.3064}, {322.287,76.4262}, {322.504,80.6014} };
   result = isEllipse( case1 );
   cout << boolalpha << result << "\n\n\n";

   vector<Pt> case2 = {
{0.0102565,69.1651}, {19.019,51.4713}, {19.6427,5.25438}, {111.389,0.385824}, {112.778,24.1719}, {288.807,24.6385}, 
{288.255,129.985}, {242.72,131.399}, {221.009,162.01}, {138.096,166.188}, {116.982,128.833}, {113.55,100.971}, {65.9781,103.747}, 
{48.9573,79.3007}, {1.3638,64.406 } };
   result = isEllipse( case2 );
   cout << boolalpha << result << "\n\n\n";
}

//=====================================================================

// Fit best ellipse of form    x^2 + bxy + cy^2 + dx + ey + f = 0
bool isEllipse( const vector<Pt> &S )
{
   const double EPS = 1.0e-3;                    // tolerance on normalised squared error
   int N = 5;                                    // number of parameters (b, c, d, e, f )
   if ( S.size() < N ) return false;             // too small; any ellipse wouldn't be unique

   vector<vector<double>> M( N, vector<double>( N ) );     // N equations for N parameters
   vector<double> rhs( N ), coeff( N );
   for ( Pt p : S )
   {
      double x = p.x, y = p.y;
      vector<double> w = { x * y, y * y, x, y, 1 };        // weights on the parameters
      for ( int i = 0; i < N; i++ )
      {
         for ( int j = 0; j < N; j++ ) M[i][j] += w[i] * w[j];
         rhs[i] -= w[i] * x * x;
      }
   }
   if ( !solve( M, rhs, coeff ) ) return false;            // solve for parameters

   double a = 1.0, b = coeff[0], c = coeff[1], d = coeff[2], e = coeff[3], f = coeff[4];
   cout << "a, b, c, d, e, f = " << a << "  " << b << "  " << c << "  " << d << "  " << e << "  " << f << '\n';
   if ( b * b - 4.0 * a * c >= 0.0 ) return false;         // Wrong conic section

   double sumEsq = 0.0, sumRsq = 0.0;
   for ( Pt p : S )
   {
      double x = p.x, y = p.y;
      vector<double> w = { x * y, y * y, x, y, 1 };
      double error = x * x, rsq = x * x + y * y;
      for ( int j = 0; j < N; j++ ) error += w[j] * coeff[j];
      sumEsq += error * error;
      sumRsq += rsq   * rsq  ;
   }
   sumEsq /= sumRsq;
   cout << "Normalised squared error = " << sumEsq << '\n';
   return sumEsq < EPS;
}

//======================================================================

bool solve( vector<vector<double>> A, vector<double> B, vector<double> &X )
// Solve AX = B by Gaussian elimination
// Note: copies deliberately made of A and B through passing by value
{
   const double SMALL = 1.0e-10;
   int n = A.size();

   // Row operations for i = 0, ,,,, n - 2 (n-1 not needed)
   for ( int i = 0; i < n - 1; i++ )
   {
      // Pivot: find row r below with largest element in column i
      int r = i;
      double maxA = abs( A[i][i] );
      for ( int k = i + 1; k < n; k++ )
      {
         double val = abs( A[k][i] );
         if ( val > maxA )
         {
            r = k;
            maxA = val;
         }
      }
      if ( r != i )
      {
         for ( int j = i; j < n; j++ ) swap( A[i][j], A[r][j] );
         swap( B[i], B[r] );
      }

      // Row operations to make upper-triangular
      double pivot = A[i][i];
      if ( abs( pivot ) < SMALL ) return false;            // Singular matrix

      for ( int r = i + 1; r < n; r++ )                    // On lower rows
      {
         double multiple = A[r][i] / pivot;                // Multiple of row i to clear element in ith column
         for ( int j = i; j < n; j++ ) A[r][j] -= multiple * A[i][j];
         B[r] -= multiple * B[i];
      }
   }

   // Check last row
   if ( abs( A[n-1][n-1] ) < SMALL ) return false;         // Singular matrix

   // Back-substitute
   X = B;
   for ( int i = n - 1; i >= 0; i-- )
   {
      for ( int j = i + 1; j < n; j++ )  X[i] -= A[i][j] * X[j];
      X[i] /= A[i][i];
   }

   return true;
}

Output:

a, b, c, d, e, f = 1  0.00923995  3.94893  -323.252  -640.062  25930.5
Normalised squared error = 6.12962e-09
true


a, b, c, d, e, f = 1  -0.67009  1.80408  -264.498  -137.44  9896.96
Normalised squared error = 0.0197783
false
lastchance
  • 1,436
  • 1
  • 3
  • 12
  • It works for all the cases, thank you so much! Kinda complicated but your explanation is clear enough :) – Melika Jul 11 '23 at 07:31