c++algorithmmathpolygonellipse

Determine if provided coordinates represent a polygon or an ellipse


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;
}

Solution

  • 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