Example: bayes_tracking_example

This example illustrates how to implement an Extended Kalman Filter (EKF) and a particle filter (PF) using mrpt::bayes classes, for the problem of tracking a 2D mobile target with state space being its location and its velocity vector.

Demo video: https://www.youtube.com/watch?v=0_gGXYzjcGE

The equations of this example and the theory behind them are explained in this tutorial.

bayes_tracking_example screenshot bayes_tracking_example screenshot bayes_tracking_example screenshot bayes_tracking_example screenshot

C++ example source code:

/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */
// ------------------------------------------------------
//  Refer to the description in the wiki:
//  https://www.mrpt.org/Kalman_Filters
// ------------------------------------------------------

#include <mrpt/bayes/CKalmanFilterCapable.h>
#include <mrpt/bayes/CParticleFilterData.h>
#include <mrpt/gui/CDisplayWindowPlots.h>
#include <mrpt/math/distributions.h>
#include <mrpt/math/wrap2pi.h>
#include <mrpt/obs/CObservationBearingRange.h>
#include <mrpt/obs/CSensoryFrame.h>
#include <mrpt/random.h>
#include <mrpt/system/os.h>

#include <chrono>
#include <iostream>
#include <thread>

using namespace mrpt::literals;  // _deg
using namespace mrpt;            // format(), square()
using namespace mrpt::bayes;
using namespace mrpt::gui;
using namespace mrpt::math;
using namespace mrpt::obs;
using namespace mrpt::random;
using namespace std;

#define BEARING_SENSOR_NOISE_STD DEG2RAD(15.0f)
#define RANGE_SENSOR_NOISE_STD   0.3f
#define DELTA_TIME               0.1f

#define VEHICLE_INITIAL_X 4.0f
#define VEHICLE_INITIAL_Y 4.0f
#define VEHICLE_INITIAL_V 1.0f
#define VEHICLE_INITIAL_W DEG2RAD(20.0f)

#define TRANSITION_MODEL_STD_XY  0.03f
#define TRANSITION_MODEL_STD_VXY 0.20f

#define NUM_PARTICLES 2000

// Uncomment to save text files with grount truth vs. estimated states
//#define SAVE_GT_LOGS

// ------------------------------------------------------
//      Implementation of the system models as a EKF
// ------------------------------------------------------
class CRangeBearing :
    public mrpt::bayes::CKalmanFilterCapable<4 /* x y vx vy*/, 2 /* range yaw */, 0, 1 /* Atime */>
// <size_t VEH_SIZE,        size_t OBS_SIZE,   size_t FEAT_SIZE, size_t
// ACT_SIZE, size typename kftype = double>
{
 public:
  CRangeBearing();
  ~CRangeBearing() override;

  void doProcess(double DeltaTime, double observationRange, double observationBearing);

  void getState(KFVector& xkk, KFMatrix& pkk)
  {
    xkk = m_xkk;
    pkk = m_pkk;
  }

 protected:
  float m_obsBearing, m_obsRange;
  float m_deltaTime;

  void OnGetAction(KFArray_ACT& out_u) const override;

  void OnTransitionModel(
      const KFArray_ACT& in_u, KFArray_VEH& inout_x, bool& out_skipPrediction) const override;

  void OnTransitionJacobian(KFMatrix_VxV& out_F) const override;

  void OnTransitionNoise(KFMatrix_VxV& out_Q) const override;

  void OnGetObservationNoise(KFMatrix_OxO& out_R) const override;

  void OnGetObservationsAndDataAssociation(
      vector_KFArray_OBS& out_z,
      std::vector<int>& out_data_association,
      const vector_KFArray_OBS& in_all_predictions,
      const KFMatrix& in_S,
      const std::vector<size_t>& in_lm_indices_in_S,
      const KFMatrix_OxO& in_R) override;

  void OnObservationModel(
      const std::vector<size_t>& idx_landmarks_to_predict,
      vector_KFArray_OBS& out_predictions) const override;

  void OnObservationJacobians(
      size_t idx_landmark_to_predict, KFMatrix_OxV& Hx, KFMatrix_OxF& Hy) const override;

  void OnSubstractObservationVectors(KFArray_OBS& A, const KFArray_OBS& B) const override;

};

// ---------------------------------------------------------------
//      Implementation of the system models as a Particle Filter
// ---------------------------------------------------------------
struct CParticleVehicleData
{
  float x, y, vx, vy;  // Vehicle state (position & velocities)
};

class CRangeBearingParticleFilter :
    public mrpt::bayes::CParticleFilterData<CParticleVehicleData>,
    public mrpt::bayes::CParticleFilterDataImpl<
        CRangeBearingParticleFilter,
        mrpt::bayes::CParticleFilterData<CParticleVehicleData>::CParticleList>
{
 public:
  void prediction_and_update_pfStandardProposal(
      const mrpt::obs::CActionCollection* action,
      const mrpt::obs::CSensoryFrame* observation,
      const bayes::CParticleFilter::TParticleFilterOptions& PF_options) override;

  void initializeParticles(size_t numParticles);

  void getMean(float& x, float& y, float& vx, float& vy);
};

// ------------------------------------------------------
//              TestBayesianTracking
// ------------------------------------------------------
void TestBayesianTracking()
{
  getRandomGenerator().randomize();

  CDisplayWindowPlots winEKF("Tracking - Extended Kalman Filter", 450, 400);
  CDisplayWindowPlots winPF("Tracking - Particle Filter", 450, 400);

  winEKF.setPos(10, 10);
  winPF.setPos(480, 10);

  winEKF.axis(-2, 20, -10, 10);
  winEKF.axis_equal();
  winPF.axis(-2, 20, -10, 10);
  winPF.axis_equal();

  // Create EKF
  // ----------------------
  CRangeBearing EKF;
  EKF.KF_options.method = kfEKFNaive;

  EKF.KF_options.verbosity_level = mrpt::system::LVL_DEBUG;
  EKF.KF_options.enable_profiler = true;

  // Create PF
  // ----------------------
  CParticleFilter::TParticleFilterOptions PF_options;
  PF_options.adaptiveSampleSize = false;
  PF_options.PF_algorithm = CParticleFilter::pfStandardProposal;
  PF_options.resamplingMethod = CParticleFilter::prSystematic;

  CRangeBearingParticleFilter particles;
  particles.initializeParticles(NUM_PARTICLES);
  CParticleFilter PF;
  PF.m_options = PF_options;

#ifdef SAVE_GT_LOGS
  CFileOutputStream fo_log_ekf("log_GT_vs_EKF.txt");
  fo_log_ekf.printf("%%%% GT_X  GT_Y  EKF_MEAN_X  EKF_MEAN_Y   EKF_STD_X   EKF_STD_Y\n");
#endif

  // Init. simulation:
  // -------------------------
  float x = VEHICLE_INITIAL_X, y = VEHICLE_INITIAL_Y, phi = -180.0_deg, v = VEHICLE_INITIAL_V,
        w = VEHICLE_INITIAL_W;
  float t = 0;

  while (winEKF.isOpen() && winPF.isOpen() && !mrpt::system::os::kbhit())
  {
    // Update vehicle:
    x += v * DELTA_TIME * (cos(phi) - sin(phi));
    y += v * DELTA_TIME * (sin(phi) + cos(phi));
    phi += w * DELTA_TIME;

    v += 1.0f * DELTA_TIME * cos(t);
    w -= 0.1f * DELTA_TIME * sin(t);

    // Simulate noisy observation:
    float realBearing = atan2(y, x);
    float obsBearing =
        realBearing + BEARING_SENSOR_NOISE_STD * getRandomGenerator().drawGaussian1D_normalized();
    printf(
        "Real/Simulated bearing: %.03f / %.03f deg\n", RAD2DEG(realBearing), RAD2DEG(obsBearing));

    float realRange = sqrt(square(x) + square(y));
    float obsRange = max(
        0.0, realRange + RANGE_SENSOR_NOISE_STD * getRandomGenerator().drawGaussian1D_normalized());
    printf("Real/Simulated range: %.03f / %.03f \n", realRange, obsRange);

    // Process with EKF:
    EKF.doProcess(DELTA_TIME, obsRange, obsBearing);

    // Process with PF:
    CSensoryFrame SF;
    CObservationBearingRange::Ptr obsRangeBear = CObservationBearingRange::Create();
    obsRangeBear->sensedData.resize(1);
    obsRangeBear->sensedData[0].range = obsRange;
    obsRangeBear->sensedData[0].yaw = obsBearing;
    SF.insert(obsRangeBear);  // memory freed by SF.

    EKF.getProfiler().enter("PF:complete_step");
    PF.executeOn(particles, nullptr, &SF);  // Process in the PF
    EKF.getProfiler().leave("PF:complete_step");

    // Show EKF state:
    CRangeBearing::KFVector EKF_xkk;
    CRangeBearing::KFMatrix EKF_pkk;

    EKF.getState(EKF_xkk, EKF_pkk);

    printf("Real: x:%.03f  y=%.03f heading=%.03f v=%.03f w=%.03f\n", x, y, phi, v, w);
    cout << "EKF: " << EKF_xkk.transpose() << endl;

    // Show PF state:
    cout << "Particle filter ESS: " << particles.ESS() << endl;

    // Draw EKF state:
    CRangeBearing::KFMatrix COVXY(2, 2);
    COVXY(0, 0) = EKF_pkk(0, 0);
    COVXY(1, 1) = EKF_pkk(1, 1);
    COVXY(0, 1) = COVXY(1, 0) = EKF_pkk(0, 1);

    winEKF.plotEllipse(EKF_xkk[0], EKF_xkk[1], COVXY, 3, "b-2", "ellipse_EKF");

// Save GT vs EKF state:
#ifdef SAVE_GT_LOGS
    // %% GT_X  GT_Y  EKF_MEAN_X  EKF_MEAN_Y   EKF_STD_X   EKF_STD_Y:
    fo_log_ekf.printf(
        "%f %f %f %f %f %f\n", x, y,  // Real (GT)
        EKF_xkk[0], EKF_xkk[1], std::sqrt(EKF_pkk(0, 0)), std::sqrt(EKF_pkk(1, 1)));
#endif

    // Draw the velocity vector:
    vector<float> vx(2), vy(2);
    vx[0] = EKF_xkk[0];
    vx[1] = vx[0] + EKF_xkk[2] * 1;
    vy[0] = EKF_xkk[1];
    vy[1] = vy[0] + EKF_xkk[3] * 1;
    winEKF.plot(vx, vy, "g-4", "velocityEKF");

    // Draw PF state:
    {
      size_t i, N = particles.m_particles.size();
      vector<float> parts_x(N), parts_y(N);
      for (i = 0; i < N; i++)
      {
        parts_x[i] = particles.m_particles[i].d->x;
        parts_y[i] = particles.m_particles[i].d->y;
      }

      winPF.plot(parts_x, parts_y, "b.2", "particles");

      // Draw PF velocities:
      float avrg_x, avrg_y, avrg_vx, avrg_vy;

      particles.getMean(avrg_x, avrg_y, avrg_vx, avrg_vy);

      vx[0] = avrg_x;
      vx[1] = vx[0] + avrg_vx * 1;
      vy[0] = avrg_y;
      vy[1] = vy[0] + avrg_vy * 1;
      winPF.plot(vx, vy, "g-4", "velocityPF");
    }

    // Draw GT:
    winEKF.plot(vector<float>(1, x), vector<float>(1, y), "k.8", "plot_GT");
    winPF.plot(vector<float>(1, x), vector<float>(1, y), "k.8", "plot_GT");

    // Draw noisy observations:
    vector<float> obs_x(2), obs_y(2);
    obs_x[0] = obs_y[0] = 0;
    obs_x[1] = obsRange * cos(obsBearing);
    obs_y[1] = obsRange * sin(obsBearing);

    winEKF.plot(obs_x, obs_y, "r", "plot_obs_ray");
    winPF.plot(obs_x, obs_y, "r", "plot_obs_ray");

    // Delay:
    std::this_thread::sleep_for(std::chrono::milliseconds((int)(DELTA_TIME * 1000)));
    t += DELTA_TIME;
  }
}

// ------------------------------------------------------
//                      MAIN
// ------------------------------------------------------
int main()
{
  try
  {
    TestBayesianTracking();
    return 0;
  }
  catch (const std::exception& e)
  {
    std::cerr << "MRPT error: " << mrpt::exception_to_str(e) << std::endl;
    return -1;
  }
}

CRangeBearing::CRangeBearing()
{
  // KF_options.method = kfEKFNaive;
  KF_options.method = kfEKFAlaDavison;

  // INIT KF STATE
  m_xkk.resize(4);  // State: (x,y,heading,v,w)
  m_xkk[0] = VEHICLE_INITIAL_X;
  m_xkk[1] = VEHICLE_INITIAL_Y;
  m_xkk[2] = -VEHICLE_INITIAL_V;
  m_xkk[3] = 0;

  // Initial cov:  Large uncertainty
  m_pkk.setIdentity(4);
  m_pkk(0, 0) = m_pkk(1, 1) = square(5.0f);
  m_pkk(2, 2) = m_pkk(3, 3) = square(1.0f);
}

CRangeBearing::~CRangeBearing() {}
void CRangeBearing::doProcess(double DeltaTime, double observationRange, double observationBearing)
{
  m_deltaTime = (float)DeltaTime;
  m_obsBearing = (float)observationBearing;
  m_obsRange = (float)observationRange;

  runOneKalmanIteration();
}

void CRangeBearing::OnGetAction(KFArray_ACT& u) const { u[0] = m_deltaTime; }
void CRangeBearing::OnTransitionModel(
    const KFArray_ACT& in_u, KFArray_VEH& inout_x, bool& out_skipPrediction) const
{
  // in_u[0] : Delta time
  // in_out_x: [0]:x  [1]:y  [2]:vx  [3]: vy
  inout_x[0] += in_u[0] * inout_x[2];
  inout_x[1] += in_u[0] * inout_x[3];
}

void CRangeBearing::OnTransitionJacobian(KFMatrix_VxV& F) const
{
  F.setIdentity();

  F(0, 2) = m_deltaTime;
  F(1, 3) = m_deltaTime;
}

void CRangeBearing::OnTransitionNoise(KFMatrix_VxV& Q) const
{
  Q(0, 0) = Q(1, 1) = square(TRANSITION_MODEL_STD_XY);
  Q(2, 2) = Q(3, 3) = square(TRANSITION_MODEL_STD_VXY);
}

void CRangeBearing::OnGetObservationNoise(KFMatrix_OxO& R) const
{
  R(0, 0) = square(BEARING_SENSOR_NOISE_STD);
  R(1, 1) = square(RANGE_SENSOR_NOISE_STD);
}

void CRangeBearing::OnGetObservationsAndDataAssociation(
    vector_KFArray_OBS& out_z,
    std::vector<int>& out_data_association,
    const vector_KFArray_OBS& in_all_predictions,
    const KFMatrix& in_S,
    const std::vector<size_t>& in_lm_indices_in_S,
    const KFMatrix_OxO& in_R)
{
  out_z.resize(1);
  out_z[0][0] = m_obsBearing;
  out_z[0][1] = m_obsRange;

  out_data_association.clear();  // Not used
}

void CRangeBearing::OnObservationModel(
    const std::vector<size_t>& idx_landmarks_to_predict, vector_KFArray_OBS& out_predictions) const
{
  // predicted bearing:
  kftype x = m_xkk[0];
  kftype y = m_xkk[1];

  kftype h_bear = atan2(y, x);
  kftype h_range = sqrt(square(x) + square(y));

  // idx_landmarks_to_predict is ignored in NON-SLAM problems
  out_predictions.resize(1);
  out_predictions[0][0] = h_bear;
  out_predictions[0][1] = h_range;
}

void CRangeBearing::OnObservationJacobians(
    size_t idx_landmark_to_predict, KFMatrix_OxV& Hx, KFMatrix_OxF& Hy) const
{
  // predicted bearing:
  kftype x = m_xkk[0];
  kftype y = m_xkk[1];

  Hx.setZero();
  Hx(0, 0) = -y / (square(x) + square(y));
  Hx(0, 1) = 1 / (x * (1 + square(y / x)));

  Hx(1, 0) = x / sqrt(square(x) + square(y));
  Hx(1, 1) = y / sqrt(square(x) + square(y));

  // Hy: Not used
}

void CRangeBearing::OnSubstractObservationVectors(KFArray_OBS& A, const KFArray_OBS& B) const
{
  A -= B;
  math::wrapToPiInPlace(A[0]);  // The angular component
}

void CRangeBearingParticleFilter::prediction_and_update_pfStandardProposal(
    const mrpt::obs::CActionCollection* action,
    const mrpt::obs::CSensoryFrame* observation,
    const bayes::CParticleFilter::TParticleFilterOptions& PF_options)
{
  size_t i, N = m_particles.size();

  // Transition model:
  for (i = 0; i < N; i++)
  {
    m_particles[i].d->x +=
        DELTA_TIME * m_particles[i].d->vx +
        TRANSITION_MODEL_STD_XY * getRandomGenerator().drawGaussian1D_normalized();
    m_particles[i].d->y +=
        DELTA_TIME * m_particles[i].d->vy +
        TRANSITION_MODEL_STD_XY * getRandomGenerator().drawGaussian1D_normalized();

    m_particles[i].d->vx +=
        TRANSITION_MODEL_STD_VXY * getRandomGenerator().drawGaussian1D_normalized();
    m_particles[i].d->vy +=
        TRANSITION_MODEL_STD_VXY * getRandomGenerator().drawGaussian1D_normalized();
  }

  CObservationBearingRange::Ptr obs =
      observation->getObservationByClass<CObservationBearingRange>();
  ASSERT_(obs);
  ASSERT_(obs->sensedData.size() == 1);
  float obsRange = obs->sensedData[0].range;
  float obsBearing = obs->sensedData[0].yaw;

  // Update weights
  for (i = 0; i < N; i++)
  {
    float predicted_range = sqrt(square(m_particles[i].d->x) + square(m_particles[i].d->y));
    float predicted_bearing = atan2(m_particles[i].d->y, m_particles[i].d->x);

    m_particles[i].log_w +=
        log(math::normalPDF(predicted_range - obsRange, 0, RANGE_SENSOR_NOISE_STD)) +
        log(math::normalPDF(
            math::wrapToPi(predicted_bearing - obsBearing), 0, BEARING_SENSOR_NOISE_STD));
  }

  // Resample is automatically performed by CParticleFilter when required.
}

void CRangeBearingParticleFilter::initializeParticles(size_t M)
{
  clearParticles();
  m_particles.resize(M);
  for (CParticleList::iterator it = m_particles.begin(); it != m_particles.end(); it++)
    it->d.reset(new CParticleVehicleData());

  for (CParticleList::iterator it = m_particles.begin(); it != m_particles.end(); it++)
  {
    (*it).d->x =
        getRandomGenerator().drawUniform(VEHICLE_INITIAL_X - 2.0f, VEHICLE_INITIAL_X + 2.0f);
    (*it).d->y =
        getRandomGenerator().drawUniform(VEHICLE_INITIAL_Y - 2.0f, VEHICLE_INITIAL_Y + 2.0f);

    (*it).d->vx = getRandomGenerator().drawGaussian1D(-VEHICLE_INITIAL_V, 0.2f);
    (*it).d->vy = getRandomGenerator().drawGaussian1D(0, 0.2f);

    it->log_w = 0;
  }
}

void CRangeBearingParticleFilter::getMean(float& x, float& y, float& vx, float& vy)
{
  double sumW = 0;
  for (CParticleList::iterator it = m_particles.begin(); it != m_particles.end(); it++)
    sumW += exp(it->log_w);

  ASSERT_(sumW > 0);

  x = y = vx = vy = 0;

  for (CParticleList::iterator it = m_particles.begin(); it != m_particles.end(); it++)
  {
    const double w = exp(it->log_w) / sumW;

    x += (float)w * (*it).d->x;
    y += (float)w * (*it).d->y;
    vx += (float)w * (*it).d->vx;
    vy += (float)w * (*it).d->vy;
  }
}