Fitting your model

Goal

In this tutorial we will you explain how to add a new fitting model, using the example of creating circleFitting.

Task details

The input of the algorithm is a set of 2D points, which are the measured coordinates of a circle. This set can be very noisy and distorted (outliers are present). So, we need to implement the main body of the circleFitting class.

Code

The tutorial code’s is shown lines below.

  • Header file code:
/**
*  @brief Circle fitting to a set of 2D points
*
*  @author  Andrey Kudryavtsev (avkudr.github.io)
*  @author  Rahima Djahel (github:rahma24000)
*  @date    20/03/2018
*  @version 1.0
*/

#include <list>
#include <vector>
#include <cmath>
#include <iostream>

#include "robust_estim.hpp"

struct Point2d{
    double x;
    double y;
};

typedef std::vector<Point2d> Point2Dvector;

class CircleFittingProblem : public robest::EstimationProblem{

public:
    CircleFittingProblem();
    ~CircleFittingProblem();

    void setData(std::vector<double> & x, std::vector<double> & y);

    double estimErrorForSample(int i);
    void   estimModelFromSamples(const std::vector<int> & samplesIdx);

    int getTotalNbSamples() const{
        return (int) points.size();
    }

    void getResult(double & res_cx, double & res_cy, double & res_r) const{
        res_cx = this->cx;
        res_cy = this->cy;
        res_r  = this->r;
    }

    bool isDegenerate(const std::vector<int> & samplesIdx);

private:

    Point2Dvector points; // input data
    double cx;
    double cy;
    double r; //radius
};
  • Source file code:
#include "CircleFitting.hpp"

CircleFittingProblem::CircleFittingProblem(){
    setNbParams(3);
    setNbMinSamples(3);
}

CircleFittingProblem::~CircleFittingProblem(){

}

void CircleFittingProblem::setData(std::vector<double> & x, std::vector<double> & y)
{
    points.clear();
    for (int i = 0; i < x.size(); i++){
        Point2d data;
        data.x=x[i];
        data.y=y[i];
        points.push_back(data);
    }
}

inline double CircleFittingProblem::estimErrorForSample(int i)
{
    // distance circle-point = abs(<distance point-center> - radius)
    Point2d & p = points[i];
    return std::abs(sqrt((p.x-cx)*(p.x-cx)+(p.y-cy)*(p.y-cy)) - r);
}

inline void CircleFittingProblem::estimModelFromSamples(const std::vector<int> & samplesIdx){
    if( !isDegenerate(samplesIdx)){
        Point2d & P = points[samplesIdx[0]];
        Point2d & V = points[samplesIdx[1]];
        Point2d & K = points[samplesIdx[2]];
        //calculation of the coefficients of the mediating straight lines
        double a = -(V.x - P.x)/(V.y - P.y);
        double b = (V.x * V.x - P.x * P.x + V.y * V.y - P.y * P.y)/(2* (V.y - P.y));
        double c = -(K.x - V.x)/(K.y - V.y);
        double d = (K.x * K.x - V.x * V.x + K.y * K.y - V.y * V.y)/(2* (K.y - V.y));

        //calculate the coordinates of the center of the circle O(A,B)
        cx = (b-d)/(c-a);
        cy = a*cx + b;

        //calculate the radius of a circle
        r = sqrt((P.x - cx)*(P.x - cx)+(P.y - cy)*(P.y - cy));
    }
}

inline bool CircleFittingProblem::isDegenerate(const std::vector<int> & samplesIdx)
{
    Point2d & P = points[samplesIdx[0]];
    Point2d & V = points[samplesIdx[1]];
    Point2d & K = points[samplesIdx[2]];

    // verify that points P, V and K are not at the line -> verify that PV and PK are colinear:

    //1. calculate the directing coefficient of the line PV
    double f = (V.y-P.y)/(V.x-P.x);

    //2. calculate the directing coefficient of the line PK
    double h = (K.y-P.y)/(K.x-P.x);

    //3. PV and PK Are colineaire if and only if f = h
    return ( f - h < 1e-3 );
}
  • Test file code:
 #include "gtest/gtest.h"

 #include <random>

 #include "CircleFitting/CircleFitting.hpp"

 //gaussian noise generation
 void generateCircleData(
         const double cx,
         const double cy,
         const double r,
         const double noiseVar,
         std::vector<double> & x, std::vector<double> & y)
 {
     std::default_random_engine generator;
     std::normal_distribution<double> distribution(0,noiseVar);

     for(double i = 0 ; i < 3.1415*2.0 ; i += 3.1415/18.0){
         double xnoise = distribution(generator);
         double ynoise = distribution(generator);
         x.push_back(r*cos(double(i)) + cx); //cx
         y.push_back(r*sin(double(i)) + cy); //cy

         if (noiseVar != 0){
             x[i] += xnoise;
             y[i] += ynoise;
         }
     }
 }

 TEST(CircleFitting, idealCase)
 {
     std::vector<double> x;
     std::vector<double> y;

     double cx = 0; // circle center C:(cx,cy)
     double cy = 0;
     double radius = 1; //circle radius
     double noiseVar = 0.0;

     generateCircleData(cx,cy,radius,noiseVar,x,y);

     CircleFittingProblem * circleFitting = new CircleFittingProblem();
     circleFitting->setData(x,y);

     robest::LMedS solver;
     solver->solve(circleFitting);

     double res_cx,res_cy,res_r;
     circleFitting->getResult(res_cx,res_cy,res_r);

     ASSERT_NEAR(    cx, res_cx, 1.0e-11);
     ASSERT_NEAR(    cy, res_cy, 1.0e-11);
     ASSERT_NEAR(radius,  res_r, 1.0e-11);
 }

 TEST(CircleFitting, idealCase2)
 {
     std::vector<double> x;
     std::vector<double> y;

     double cx = 24.8; // circle center C:(cx,cy)
     double cy = 8.10;
     double radius = 26.03; //circle radius
     double noiseVar = 0.0;

     generateCircleData(cx,cy,radius,noiseVar,x,y);

     CircleFittingProblem * circleFitting = new CircleFittingProblem();
     circleFitting->setData(x,y);

     double thres = 0.001;
     int nbIter = 20;
     robest::LMedS solver;
     solver->solve(circleFitting, thres, nbIter);

     double res_cx,res_cy,res_r;
     circleFitting->getResult(res_cx,res_cy,res_r);

     ASSERT_NEAR(    cx, res_cx, 1.0e-11);
     ASSERT_NEAR(    cy, res_cy, 1.0e-11);
     ASSERT_NEAR(radius,  res_r, 1.0e-11);
 }

 TEST(CircleFitting, smallNoise)
{
     std::vector<double> x;
     std::vector<double> y;

     double cx = 3.552356; // circle center C:(cx,cy)
     double cy = 1.58452;
     double radius = 13.2548; //circle radius
     double noiseVar = 0.001;

     generateCircleData(cx,cy,radius,noiseVar,x,y);

     CircleFittingProblem * circleFitting = new CircleFittingProblem();
     circleFitting->setData(x,y);

     robest::LMedS solver;
     solver->solve(circleFitting);

     double res_cx,res_cy,res_r;
     circleFitting->getResult(res_cx,res_cy,res_r);

     ASSERT_NEAR(    cx, res_cx, 1.0e-3);
     ASSERT_NEAR(    cy, res_cy, 1.0e-3);
     ASSERT_NEAR(radius,  res_r, 1.0e-3);
 }

 TEST(CircleFitting, outliers)
 {
     std::vector<double> x = {1,0,-1, 0, sqrt(2)/2.0, 24,  8, 26};
     std::vector<double> y = {0,1, 0,-1, sqrt(2)/2.0,  8, 10,  3};

     CircleFittingProblem * circleFitting = new CircleFittingProblem();
     circleFitting->setData(x,y);

     robest::LMedS solver;
     auto nbIter = solver->calculateIterationsNb(x.size(),0.99,0.45);
     solver->solve(circleFitting, 0.1, nbIter);

     double res_cx,res_cy,res_r;
     circleFitting->getResult(res_cx,res_cy,res_r);

     ASSERT_NEAR( 0.0, res_cx, 1.0e-11);
     ASSERT_NEAR( 0.0, res_cy, 1.0e-11);
     ASSERT_NEAR( 1.0,  res_r, 1.0e-11);
 }

 TEST(CircleFitting, isDegenerate)
 {
     // Generate data
     // y = k*x + b
     std::vector<double> x1 = {0,1,2};
     std::vector<double> y1 = {0,1,2};

     // Define estimation problem
     CircleFittingProblem * circleFitting = new CircleFittingProblem();
     circleFitting->setData(x1,y1);
     ASSERT_TRUE(circleFitting->isDegenerate({0,1,2}));

     std::vector<double> x2 = {0,1,2};
     std::vector<double> y2 = {0,1.001,2};
     circleFitting->setData(x2,y2);
     ASSERT_TRUE(circleFitting->isDegenerate({0,1,2}));

     std::vector<double> x3 = {0,1,5};
     std::vector<double> y3 = {0,1,2};
     circleFitting->setData(x3,y3);
     ASSERT_TRUE(!circleFitting->isDegenerate({0,1,2}));
 }

Explanation

Step 1: Declaration

In this step, we will demonstrate one of the ways to organize the structure of the header file for the circleFitting class.

  • Including libraries
#include <list>
#include <vector>
#include <cmath>
#include <iostream>

#include "robust_estim.hpp"
  • Defining global class parameters
// Creating a new data structure - 2D point
struct Point2d{
    double x;
    double y;
};

// Definition of a new data type - vector of 2D points
typedef std::vector<Point2d> Point2Dvector;
  • Inheritance from EstimationProblem
class CircleFittingProblem : public robest::EstimationProblem
  • Declaring public class attributes
public:
    // Class constructor
    CircleFittingProblem();

    // Class destructor
    ~CircleFittingProblem();

    // Data setting function
    void setData(std::vector<double> & x, std::vector<double> & y);

    // Residual calculation function
    double estimErrorForSample(int i);

    // Function of calculating the parameters of the model
    void   estimModelFromSamples(const std::vector<int> & samplesIdx);

    // Data size calculation function
    int getTotalNbSamples() const{
        return (int) points.size();
    }

    // Output function calculated parameters
    void getResult(double & res_cx, double & res_cy, double & res_r) const{
        res_cx = this->cx;
        res_cy = this->cy;
        res_r  = this->r;
    }

    // Function to check the correctness of the selected points
    bool isDegenerate(const std::vector<int> & samplesIdx);
  • Declaring private class attributes
private:

    Point2Dvector points; // input data
    double cx;   // x coordinate of the center
    double cy;   // y coordinate of the center
    double r;    // circle radius

Step 2: Definition

Once all the major class attributes are declared, the next step is to define them inside the source file.

  • Definition of the constructor and destructor
// Don't forget to include the header file!
#include "CircleFitting.hpp"

// Class constructor definition
CircleFittingProblem::CircleFittingProblem(){

    // Setting the number of parameters for the equation of a circle
    setNbParams(3);

    // Setting the minimum number of points needed to calculate a circle model
    setNbMinSamples(3);
}

// Class destructor definition
CircleFittingProblem::~CircleFittingProblem(){

}
  • Function of setting the data
void CircleFittingProblem::setData(std::vector<double> & x, std::vector<double> & y)
{
    // Clearing the internal vector of 2D points
    points.clear();

    // Filling the internal vector of 2D points by external values
    for (int i = 0; i < x.size(); i++){
        Point2d data;
        data.x=x[i];
        data.y=y[i];
        points.push_back(data);
    }
}
  • Defining the function of calculating the residual

    The main task of this function is to calculate the distance between a given point and the surface of a circle.This value is called the residual. Knowledge of this value is necessary for further error calculation using the loss function.

    What is the distance between a circle C(x,y) and and a point P(xp,yp) ?

    The equation of this circle is:

    \[(x - x_c)^2+(y - y_c)^2 = r^2\]

    where xc and yc are the coordinates of the centre of circle, and r is a radius of the circle.

    calculated model

    The ray OP , starting at the origin O and passing through the point P, intersects the circle at the point closest to P. So, the distance between the circle and the point will be the difference of the distance of the point from the origin and the radius of the circle - D.

    Using the Distance Formula between two point in Cartesian system of coordinates, the final form of the residual function is:

    \[D = |\sqrt{(x_p - x_c)^2 + (y_p - y_c)^2} - r|\]

    Thus, the function code for calculating the residual takes the following form:

    inline double CircleFittingProblem::estimErrorForSample(int i)
    {
       // distance circle-point = abs(<distance point-center> - radius)
       Point2d & p = points[i];
       return std::abs(sqrt((p.x-cx)*(p.x-cx)+(p.y-cy)*(p.y-cy)) - r);
    }
    
  • Function to calculate the model

    The task of this function is to calculate the basic parameters of the circle model, which has the following form:

    \[(x_i - x_c)^2 + (y_i - y_c)^2 = r^2\]

    Here, the parameters are the coordinates of the center and the radius of a circle.

    It is easy to see that the equation has three unknown parameters, so in order to determine these parameters, it is enough to know the coordinates of three points.

    After expanding and rearranging the terms, the new equation of a circle is expressed below.

    \[x_i^2 + y_i^2 = 2x_ix_c + 2y_iy_c + r^2 - x_c^2 - y_c^2\]

    where xi and yi are the coordinates of ith point.

    Taking into account the fact that we need three points to determine the parameters of the model, this equation can now be expressed in vector/matrix notation:

    \[\begin{split}f = \begin{bmatrix} x_1^2 + y_1^2 \\ x_2^2 + y_2^2 \\ x_3^2 + y_3^2 \end{bmatrix}\end{split}\]
    \[\begin{split}A = \begin{bmatrix} 2x_1 & 2y_1 & 1 \\ 2x_2 & 2y_2 & 1 \\ 2x_3 & 2y_3 & 1 \end{bmatrix}\end{split}\]
    \[\begin{split}p = \begin{bmatrix} x_c \\ y_c \\ r^2 - x_c^2 - y_c^2 \end{bmatrix}\end{split}\]

    The f vector, the A matrix, and the p vector represents the consolidated terms of the expanded circle equation.

    The new equation is seen below. We can calculate the circle’s parameters using the terms in the p.

    \[ \begin{align}\begin{aligned}f = Ap\\p = A^{-1}f\end{aligned}\end{align} \]

    Thus, the final form of the parameter calculation function will be as follows:

    inline void CircleFittingProblem::estimModelFromSamples(const std::vector<int> & samplesIdx){
    
        // Validation of selected points
        if( !isDegenerate(samplesIdx)){
    
            Point2d & P = points[samplesIdx[0]];
            Point2d & V = points[samplesIdx[1]];
            Point2d & K = points[samplesIdx[2]];
    
            //calculation of the coefficients of the mediating straight lines
            double a = -(V.x - P.x)/(V.y - P.y);
            double b = (V.x * V.x - P.x * P.x + V.y * V.y - P.y * P.y)/(2* (V.y - P.y));
            double c = -(K.x - V.x)/(K.y - V.y);
            double d = (K.x * K.x - V.x * V.x + K.y * K.y - V.y * V.y)/(2* (K.y - V.y));
    
            //calculate the coordinates of the center of the circle O(A,B)
            cx = (b-d)/(c-a);
            cy = a*cx + b;
    
            //calculate the radius of a circle
            r = sqrt((P.x - cx)*(P.x - cx)+(P.y - cy)*(P.y - cy));
        }
    }
    
  • Verification of a degenerate set of points

    Considering that the main estimation algorithm is based on the principle of random selection of points, and for the correct construction of the model, this set should consist of points that do not lie on one straight line. We need to create a function that will check this condition.

    In order to check this condition, it is sufficient to determine whether the two vectors, formed by these points, are collier or not.

    So, the function isDegenerated takes the following form:

    inline bool CircleFittingProblem::isDegenerate(const std::vector<int> & samplesIdx)
    {
        Point2d & P = points[samplesIdx[0]];
        Point2d & V = points[samplesIdx[1]];
        Point2d & K = points[samplesIdx[2]];
    
        // verify that points P, V and K are not at the line -> verify that PV and PK are colinear:
    
    
        //1. calculate the directing coefficient of the line PV
        double f = (V.y-P.y)/(V.x-P.x);
    
        //2. calculate the directing coefficient of the line PK
        double h = (K.y-P.y)/(K.x-P.x);
    
        //3. PV and PK Are colineaire if and only if f = h
        return ( f - h < 1e-3 );
    }
    

Step 3: Testing

Now that the circleFitting class is ready, you need to test it. In our library, we use Google tests.

  • Ideal cases

    The first two tests are aimed at general verification of the correctness of the class. We are testing the so-called ideal situation when the input set of points corresponds to a very simple example of a circle, there are no outliers and noises.

    TEST(CircleFitting, idealCase)
    {
        std::vector<double> x;
        std::vector<double> y;
    
        double cx = 0; // circle center C:(cx,cy)
        double cy = 0;
        double radius = 1; //circle radius
        double noiseVar = 0.0;
    
        generateCircleData(cx,cy,radius,noiseVar,x,y);
    
        CircleFittingProblem * circleFitting = new CircleFittingProblem();
        circleFitting->setData(x,y);
    
        // Solving
        robest::LMedS solver;
        solver->solve(circleFitting);
    
        // Getting the results
        double res_cx,res_cy,res_r;
        circleFitting->getResult(res_cx,res_cy,res_r);
    
        // Verifying the results
        ASSERT_NEAR(    cx, res_cx, 1.0e-11);
        ASSERT_NEAR(    cy, res_cy, 1.0e-11);
        ASSERT_NEAR(radius,  res_r, 1.0e-11);
    }
    
    TEST(CircleFitting, idealCase2)
    {
        std::vector<double> x;
        std::vector<double> y;
    
        double cx = 24.8; // circle center C:(cx,cy)
        double cy = 8.10;
        double radius = 26.03; //circle radius
        double noiseVar = 0.0;
    
        generateCircleData(cx,cy,radius,noiseVar,x,y);
    
        CircleFittingProblem * circleFitting = new CircleFittingProblem();
        circleFitting->setData(x,y);
    
        // Solving
        double thres = 0.001;
        int nbIter = 20;
        robest::LMedS solver;
        solver->solve(circleFitting, thres, nbIter);
    
        // Getting the results
        double res_cx,res_cy,res_r;
        circleFitting->getResult(res_cx,res_cy,res_r);
    
        // Verifying the results
        ASSERT_NEAR(    cx, res_cx, 1.0e-11);
        ASSERT_NEAR(    cy, res_cy, 1.0e-11);
        ASSERT_NEAR(radius,  res_r, 1.0e-11);
    }
    
  • Small noises case

    Now we will test the class on noisy data.

    TEST(CircleFitting, smallNoise)
    {
        std::vector<double> x;
        std::vector<double> y;
    
        double cx = 3.552356; // circle center C:(cx,cy)
        double cy = 1.58452;
        double radius = 13.2548; //circle radius
        double noiseVar = 0.001;
    
        generateCircleData(cx,cy,radius,noiseVar,x,y);
    
        CircleFittingProblem * circleFitting = new CircleFittingProblem();
        circleFitting->setData(x,y);
    
        // Solving
        robest::LMedS solver;
        solver->solve(circleFitting);
    
        // Getting the results
        double res_cx,res_cy,res_r;
        circleFitting->getResult(res_cx,res_cy,res_r);
    
        // Verifying the results
        ASSERT_NEAR(    cx, res_cx, 1.0e-3);
        ASSERT_NEAR(    cy, res_cy, 1.0e-3);
        ASSERT_NEAR(radius,  res_r, 1.0e-3);
    }
    
  • Data with an outliers

    As the next text, we can check how effectively the algorithm copes with the corrupted data, in which there are outliers.

    TEST(CircleFitting, outliers)
    {
        std::vector<double> x = {1,0,-1, 0, sqrt(2)/2.0, 24,  8, 26};
        std::vector<double> y = {0,1, 0,-1, sqrt(2)/2.0,  8, 10,  3};
    
        CircleFittingProblem * circleFitting = new CircleFittingProblem();
        circleFitting->setData(x,y);
    
        // Solving
        robest::LMedS solver;
        auto nbIter = solver->calculateIterationsNb(x.size(),0.99,0.45);
        solver->solve(circleFitting, 0.1, nbIter);
    
        // Getting the results
        double res_cx,res_cy,res_r;
        circleFitting->getResult(res_cx,res_cy,res_r);
    
        // Verifying the results
        ASSERT_NEAR( 0.0, res_cx, 1.0e-11);
        ASSERT_NEAR( 0.0, res_cy, 1.0e-11);
        ASSERT_NEAR( 1.0,  res_r, 1.0e-11);
    }
    
  • Degenerate case

    The last thing to check is the function of checking the correctness of the selected set of points.

    TEST(CircleFitting, isDegenerate)
    {
        // Generate data
        // y = k*x + b
        std::vector<double> x1 = {0,1,2};
        std::vector<double> y1 = {0,1,2};
    
        // Define estimation problem
        CircleFittingProblem * circleFitting = new CircleFittingProblem();
        circleFitting->setData(x1,y1);
    
        // Verifying a degenerate set 1
        ASSERT_TRUE(circleFitting->isDegenerate({0,1,2}));
    
        std::vector<double> x2 = {0,1,2};
        std::vector<double> y2 = {0,1.001,2};
        circleFitting->setData(x2,y2);
    
        // Verifying a degenerate set 2
        ASSERT_TRUE(circleFitting->isDegenerate({0,1,2}));
    
        std::vector<double> x3 = {0,1,5};
        std::vector<double> y3 = {0,1,2};
        circleFitting->setData(x3,y3);
    
        // Verifying a non-degenerate set
        ASSERT_TRUE(!circleFitting->isDegenerate({0,1,2}));
    }