Visual Servoing Platform  version 3.6.1 under development (2024-12-03)
ukf-nonlinear-complex-example.cpp
1 /*
2  * ViSP, open source Visual Servoing Platform software.
3  * Copyright (C) 2005 - 2024 by Inria. All rights reserved.
4  *
5  * This software is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  * See the file LICENSE.txt at the root directory of this source
10  * distribution for additional information about the GNU GPL.
11  *
12  * For using ViSP with software that can not be combined with the GNU
13  * GPL, please contact Inria about acquiring a ViSP Professional
14  * Edition License.
15  *
16  * See https://visp.inria.fr for more information.
17  *
18  * This software was developed at:
19  * Inria Rennes - Bretagne Atlantique
20  * Campus Universitaire de Beaulieu
21  * 35042 Rennes Cedex
22  * France
23  *
24  * If you have questions regarding the use of this file, please contact
25  * Inria at visp@inria.fr
26  *
27  * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
28  * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
29  */
30 
77 #include <iostream>
78 
79 // UKF includes
80 #include <visp3/core/vpUKSigmaDrawerMerwe.h>
81 #include <visp3/core/vpUnscentedKalman.h>
82 
83 // ViSP includes
84 #include <visp3/core/vpConfig.h>
85 #include <visp3/core/vpGaussRand.h>
86 #ifdef VISP_HAVE_DISPLAY
87 #include <visp3/gui/vpPlot.h>
88 #endif
89 
90 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
91 
92 #ifdef ENABLE_VISP_NAMESPACE
93 using namespace VISP_NAMESPACE_NAME;
94 #endif
95 
96 namespace
97 {
104 double normalizeAngle(const double &angle)
105 {
106  double angleIn0to2pi = vpMath::modulo(angle, 2. * M_PI);
107  double angleInMinPiPi = angleIn0to2pi;
108  if (angleInMinPiPi > M_PI) {
109  // Substract 2 PI to be in interval [-Pi; Pi]
110  angleInMinPiPi -= 2. * M_PI;
111  }
112  return angleInMinPiPi;
113 }
114 
122 vpColVector measurementMean(const std::vector<vpColVector> &measurements, const std::vector<double> &wm)
123 {
124  const unsigned int nbPoints = static_cast<unsigned int>(measurements.size());
125  const unsigned int sizeMeasurement = measurements[0].size();
126  const unsigned int nbLandmarks = sizeMeasurement / 2;
127  vpColVector mean(sizeMeasurement, 0.);
128  std::vector<double> sumCos(nbLandmarks, 0.);
129  std::vector<double> sumSin(nbLandmarks, 0.);
130  for (unsigned int i = 0; i < nbPoints; ++i) {
131  for (unsigned int j = 0; j < nbLandmarks; ++j) {
132  mean[2*j] += wm[i] * measurements[i][2*j];
133  sumCos[j] += wm[i] * std::cos(measurements[i][(2*j)+1]);
134  sumSin[j] += wm[i] * std::sin(measurements[i][(2*j)+1]);
135  }
136  }
137  for (unsigned int j = 0; j < nbLandmarks; ++j) {
138  mean[(2*j)+1] = std::atan2(sumSin[j], sumCos[j]);
139  }
140  return mean;
141 }
142 
151 vpColVector measurementResidual(const vpColVector &meas, const vpColVector &toSubtract)
152 {
153  vpColVector res = meas - toSubtract;
154  unsigned int nbMeasures = res.size();
155  for (unsigned int i = 1; i < nbMeasures; i += 2) {
156  res[i] = normalizeAngle(res[i]);
157  }
158  return res;
159 }
160 
169 vpColVector stateAdd(const vpColVector &state, const vpColVector &toAdd)
170 {
171  vpColVector add = state + toAdd;
172  add[2] = normalizeAngle(add[2]);
173  return add;
174 }
175 
183 vpColVector stateMean(const std::vector<vpColVector> &states, const std::vector<double> &wm)
184 {
185  vpColVector mean(3, 0.);
186  unsigned int nbPoints = static_cast<unsigned int>(states.size());
187  double sumCos = 0.;
188  double sumSin = 0.;
189  for (unsigned int i = 0; i < nbPoints; ++i) {
190  mean[0] += wm[i] * states[i][0];
191  mean[1] += wm[i] * states[i][1];
192  sumCos += wm[i] * std::cos(states[i][2]);
193  sumSin += wm[i] * std::sin(states[i][2]);
194  }
195  mean[2] = std::atan2(sumSin, sumCos);
196  return mean;
197 }
198 
207 vpColVector stateResidual(const vpColVector &state, const vpColVector &toSubtract)
208 {
209  vpColVector res = state - toSubtract;
210  res[2] = normalizeAngle(res[2]);
211  return res;
212 }
213 
221 vpColVector fx(const vpColVector &x, const double & /*dt*/)
222 {
223  return x;
224 }
225 
235 std::vector<vpColVector> generateTurnCommands(const double &v, const double &angleStart, const double &angleStop, const unsigned int &nbSteps)
236 {
237  std::vector<vpColVector> cmds;
238  double dTheta = vpMath::rad(angleStop - angleStart) / static_cast<double>(nbSteps - 1);
239  for (unsigned int i = 0; i < nbSteps; ++i) {
240  double theta = vpMath::rad(angleStart) + dTheta * static_cast<double>(i);
241  vpColVector cmd(2);
242  cmd[0] = v;
243  cmd[1] = theta;
244  cmds.push_back(cmd);
245  }
246  return cmds;
247 }
248 
254 std::vector<vpColVector> generateCommands()
255 {
256  std::vector<vpColVector> cmds;
257  // Starting by an straight line acceleration
258  unsigned int nbSteps = 30;
259  double dv = (1.1 - 0.001) / static_cast<double>(nbSteps - 1);
260  for (unsigned int i = 0; i < nbSteps; ++i) {
261  vpColVector cmd(2);
262  cmd[0] = 0.001 + static_cast<double>(i) * dv;
263  cmd[1] = 0.;
264  cmds.push_back(cmd);
265  }
266 
267  // Left turn
268  double lastLinearVelocity = cmds[cmds.size() -1][0];
269  std::vector<vpColVector> leftTurnCmds = generateTurnCommands(lastLinearVelocity, 0, 2, 15);
270  cmds.insert(cmds.end(), leftTurnCmds.begin(), leftTurnCmds.end());
271  for (unsigned int i = 0; i < 100; ++i) {
272  cmds.push_back(cmds[cmds.size() -1]);
273  }
274 
275  // Right turn
276  lastLinearVelocity = cmds[cmds.size() -1][0];
277  std::vector<vpColVector> rightTurnCmds = generateTurnCommands(lastLinearVelocity, 2, -2, 15);
278  cmds.insert(cmds.end(), rightTurnCmds.begin(), rightTurnCmds.end());
279  for (unsigned int i = 0; i < 200; ++i) {
280  cmds.push_back(cmds[cmds.size() -1]);
281  }
282 
283  // Left turn again
284  lastLinearVelocity = cmds[cmds.size() -1][0];
285  leftTurnCmds = generateTurnCommands(lastLinearVelocity, -2, 0, 15);
286  cmds.insert(cmds.end(), leftTurnCmds.begin(), leftTurnCmds.end());
287  for (unsigned int i = 0; i < 150; ++i) {
288  cmds.push_back(cmds[cmds.size() -1]);
289  }
290 
291  lastLinearVelocity = cmds[cmds.size() -1][0];
292  leftTurnCmds = generateTurnCommands(lastLinearVelocity, 0, 1, 25);
293  cmds.insert(cmds.end(), leftTurnCmds.begin(), leftTurnCmds.end());
294  for (unsigned int i = 0; i < 150; ++i) {
295  cmds.push_back(cmds[cmds.size() -1]);
296  }
297 
298  return cmds;
299 }
300 }
301 
306 {
307 public:
313  vpBicycleModel(const double &w)
314  : m_w(w)
315  { }
316 
325  vpColVector computeMotion(const vpColVector &u, const vpColVector &x, const double &dt)
326  {
327  double heading = x[2];
328  double vel = u[0];
329  double steeringAngle = u[1];
330  double distance = vel * dt;
331 
332  if (std::abs(steeringAngle) > 0.001) {
333  // The robot is turning
334  double beta = (distance / m_w) * std::tan(steeringAngle);
335  double radius = m_w / std::tan(steeringAngle);
336  double sinh = std::sin(heading);
337  double sinhb = std::sin(heading + beta);
338  double cosh = std::cos(heading);
339  double coshb = std::cos(heading + beta);
340  vpColVector motion(3);
341  motion[0] = -radius * sinh + radius * sinhb;
342  motion[1] = radius * cosh - radius * coshb;
343  motion[2] = beta;
344  return motion;
345  }
346  else {
347  // The robot is moving in straight line
348  vpColVector motion(3);
349  motion[0] = distance * std::cos(heading);
350  motion[1] = distance * std::sin(heading);
351  motion[2] = 0.;
352  return motion;
353  }
354  }
355 
365  vpColVector move(const vpColVector &u, const vpColVector &x, const double &dt)
366  {
367  vpColVector motion = computeMotion(u, x, dt);
368  vpColVector newX = x + motion;
369  newX[2] = normalizeAngle(newX[2]);
370  return newX;
371  }
372 private:
373  double m_w; // The length of the wheelbase.
374 };
375 
381 {
382 public:
391  vpLandmarkMeasurements(const double &x, const double &y, const double &range_std, const double &rel_angle_std)
392  : m_x(x)
393  , m_y(y)
394  , m_rngRange(range_std, 0., 4224)
395  , m_rngRelativeAngle(rel_angle_std, 0., 2112)
396  { }
397 
405  {
406  vpColVector meas(2);
407  double dx = m_x - chi[0];
408  double dy = m_y - chi[1];
409  meas[0] = std::sqrt(dx * dx + dy * dy);
410  meas[1] = normalizeAngle(std::atan2(dy, dx) - chi[2]);
411  return meas;
412  }
413 
422  {
423  double dx = m_x - pos[0];
424  double dy = m_y - pos[1];
425  double range = std::sqrt(dx * dx + dy * dy);
426  double orientation = normalizeAngle(std::atan2(dy, dx) - pos[2]);
427  vpColVector measurements(2);
428  measurements[0] = range;
429  measurements[1] = orientation;
430  return measurements;
431  }
432 
441  {
442  vpColVector measurementsGT = measureGT(pos);
443  vpColVector measurementsNoisy = measurementsGT;
444  measurementsNoisy[0] += m_rngRange();
445  measurementsNoisy[1] += m_rngRelativeAngle();
446  measurementsNoisy[1] = normalizeAngle(measurementsNoisy[1]);
447  return measurementsNoisy;
448  }
449 
450 private:
451  double m_x; // The position along the x-axis of the landmark
452  double m_y; // The position along the y-axis of the landmark
453  vpGaussRand m_rngRange; // Noise simulator for the range measurement
454  vpGaussRand m_rngRelativeAngle; // Noise simulator for the relative angle measurement
455 };
456 
462 {
463 public:
469  vpLandmarksGrid(const std::vector<vpLandmarkMeasurements> &landmarks)
470  : m_landmarks(landmarks)
471  { }
472 
480  {
481  unsigned int nbLandmarks = static_cast<unsigned int>(m_landmarks.size());
482  vpColVector measurements(2*nbLandmarks);
483  for (unsigned int i = 0; i < nbLandmarks; ++i) {
484  vpColVector landmarkMeas = m_landmarks[i].state_to_measurement(chi);
485  measurements[2*i] = landmarkMeas[0];
486  measurements[(2*i) + 1] = landmarkMeas[1];
487  }
488  return measurements;
489  }
490 
500  {
501  unsigned int nbLandmarks = static_cast<unsigned int>(m_landmarks.size());
502  vpColVector measurements(2*nbLandmarks);
503  for (unsigned int i = 0; i < nbLandmarks; ++i) {
504  vpColVector landmarkMeas = m_landmarks[i].measureGT(pos);
505  measurements[2*i] = landmarkMeas[0];
506  measurements[(2*i) + 1] = landmarkMeas[1];
507  }
508  return measurements;
509  }
510 
520  {
521  unsigned int nbLandmarks = static_cast<unsigned int>(m_landmarks.size());
522  vpColVector measurements(2*nbLandmarks);
523  for (unsigned int i = 0; i < nbLandmarks; ++i) {
524  vpColVector landmarkMeas = m_landmarks[i].measureWithNoise(pos);
525  measurements[2*i] = landmarkMeas[0];
526  measurements[(2*i) + 1] = landmarkMeas[1];
527  }
528  return measurements;
529  }
530 
531 private:
532  std::vector<vpLandmarkMeasurements> m_landmarks;
533 };
534 
535 int main(const int argc, const char *argv[])
536 {
537  bool opt_useDisplay = true;
538  for (int i = 1; i < argc; ++i) {
539  std::string arg(argv[i]);
540  if (arg == "-d") {
541  opt_useDisplay = false;
542  }
543  else if ((arg == "-h") || (arg == "--help")) {
544  std::cout << "SYNOPSIS" << std::endl;
545  std::cout << " " << argv[0] << " [-d][-h]" << std::endl;
546  std::cout << std::endl << std::endl;
547  std::cout << "DETAILS" << std::endl;
548  std::cout << " -d" << std::endl;
549  std::cout << " Deactivate display." << std::endl;
550  std::cout << std::endl;
551  std::cout << " -h, --help" << std::endl;
552  return 0;
553  }
554  }
555 
556  const double dt = 0.1; // Period of 0.1s
557  const double step = 1.; // Number of update of the robot position between two UKF filtering
558  const double sigmaRange = 0.3; // Standard deviation of the range measurement: 0.3m
559  const double sigmaBearing = vpMath::rad(0.5); // Standard deviation of the bearing angle: 0.5deg
560  const double wheelbase = 0.5; // Wheelbase of 0.5m
561  const std::vector<vpLandmarkMeasurements> landmarks = { vpLandmarkMeasurements(5, 10, sigmaRange, sigmaBearing)
562  , vpLandmarkMeasurements(10, 5, sigmaRange, sigmaBearing)
563  , vpLandmarkMeasurements(15, 15, sigmaRange, sigmaBearing)
564  , vpLandmarkMeasurements(20, 5, sigmaRange, sigmaBearing)
565  , vpLandmarkMeasurements(0, 30, sigmaRange, sigmaBearing)
566  , vpLandmarkMeasurements(50, 30, sigmaRange, sigmaBearing)
567  , vpLandmarkMeasurements(40, 10, sigmaRange, sigmaBearing) }; // Vector of landmarks constituting the grid
568  const unsigned int nbLandmarks = static_cast<unsigned int>(landmarks.size()); // Number of landmarks constituting the grid
569  std::vector<vpColVector> cmds = generateCommands();
570  const unsigned int nbCmds = static_cast<unsigned int>(cmds.size());
571 
572  // Initialize the attributes of the UKF
573  std::shared_ptr<vpUKSigmaDrawerAbstract> drawer = std::make_shared<vpUKSigmaDrawerMerwe>(3, 0.1, 2., 0, stateResidual, stateAdd);
574 
575  vpMatrix R1landmark(2, 2, 0.); // The covariance of the noise introduced by the measurement with 1 landmark
576  R1landmark[0][0] = sigmaRange*sigmaRange;
577  R1landmark[1][1] = sigmaBearing*sigmaBearing;
578  vpMatrix R(2*nbLandmarks, 2 * nbLandmarks);
579  for (unsigned int i = 0; i < nbLandmarks; ++i) {
580  R.insert(R1landmark, 2*i, 2*i);
581  }
582 
583  const double processVariance = 0.0001;
584  vpMatrix Q; // The covariance of the process
585  Q.eye(3);
586  Q = Q * processVariance;
587 
588  vpMatrix P0(3, 3); // The initial guess of the process covariance
589  P0.eye(3);
590  P0[0][0] = 0.1;
591  P0[1][1] = 0.1;
592  P0[2][2] = 0.05;
593 
594  vpColVector X0(3);
595  X0[0] = 2.; // x = 2m
596  X0[1] = 6.; // y = 6m
597  X0[2] = 0.3; // robot orientation = 0.3 rad
598 
600  vpLandmarksGrid grid(landmarks);
601  vpBicycleModel robot(wheelbase);
602  using std::placeholders::_1;
603  using std::placeholders::_2;
604  using std::placeholders::_3;
607 
608  // Initialize the UKF
609  vpUnscentedKalman ukf(Q, R, drawer, f, h);
610  ukf.init(X0, P0);
611  ukf.setCommandStateFunction(bx);
612  ukf.setMeasurementMeanFunction(measurementMean);
613  ukf.setMeasurementResidualFunction(measurementResidual);
614  ukf.setStateAddFunction(stateAdd);
615  ukf.setStateMeanFunction(stateMean);
616  ukf.setStateResidualFunction(stateResidual);
617 
618 #ifdef VISP_HAVE_DISPLAY
619  vpPlot *plot = nullptr;
620  if (opt_useDisplay) {
621  // Initialize the plot
622  plot = new vpPlot(1);
623  plot->initGraph(0, 2);
624  plot->setTitle(0, "Position of the robot");
625  plot->setUnitX(0, "Position along x(m)");
626  plot->setUnitY(0, "Position along y (m)");
627  plot->setLegend(0, 0, "GT");
628  plot->setLegend(0, 1, "Filtered");
629  }
630 #endif
631 
632  // Initialize the simulation
633  vpColVector robot_pos = X0;
634 
635  for (unsigned int i = 0; i < nbCmds; ++i) {
636  robot_pos = robot.move(cmds[i], robot_pos, dt / step);
637  if (i % static_cast<int>(step) == 0) {
638  // Perform the measurement
639  vpColVector z = grid.measureWithNoise(robot_pos);
640 
641  // Use the UKF to filter the measurement
642  ukf.filter(z, dt, cmds[i]);
643 
644 #ifdef VISP_HAVE_DISPLAY
645  if (opt_useDisplay) {
646  // Plot the filtered state
647  vpColVector Xest = ukf.getXest();
648  plot->plot(0, 1, Xest[0], Xest[1]);
649  }
650 #endif
651  }
652 
653 #ifdef VISP_HAVE_DISPLAY
654  if (opt_useDisplay) {
655  // Plot the ground truth
656  plot->plot(0, 0, robot_pos[0], robot_pos[1]);
657  }
658 #endif
659  }
660 
661  if (opt_useDisplay) {
662  std::cout << "Press Enter to quit..." << std::endl;
663  std::cin.get();
664  }
665 
666 #ifdef VISP_HAVE_DISPLAY
667  if (opt_useDisplay) {
668  delete plot;
669  }
670 #endif
671 
672  vpColVector finalError = grid.state_to_measurement(ukf.getXest()) - grid.measureGT(robot_pos);
673  const double maxError = 0.3;
674  if (finalError.frobeniusNorm() > maxError) {
675  std::cerr << "Error: max tolerated error = " << maxError << ", final error = " << finalError.frobeniusNorm() << std::endl;
676  return -1;
677  }
678  return 0;
679 }
680 #else
681 int main()
682 {
683  std::cout << "This example is only available if you compile ViSP in C++11 standard or higher." << std::endl;
684  return 0;
685 }
686 #endif
unsigned int size() const
Return the number of elements of the 2D array.
Definition: vpArray2D.h:349
Class that approximates a 4-wheel robot using a bicycle model.
vpColVector computeMotion(const vpColVector &u, const vpColVector &x, const double &dt)
Models the effect of the command on the state model.
vpBicycleModel(const double &w)
Construct a new vpBicycleModel object.
vpColVector move(const vpColVector &u, const vpColVector &x, const double &dt)
Move the robot according to its current position and the commands.
Implementation of column vector and the associated operations.
Definition: vpColVector.h:191
double frobeniusNorm() const
Class for generating random number with normal probability density.
Definition: vpGaussRand.h:117
Class that permits to convert the position + heading of the 4-wheel robot into measurements from a la...
vpColVector measureGT(const vpColVector &pos)
Perfect measurement of the range and relative orientation of the robot located at pos.
vpColVector state_to_measurement(const vpColVector &chi)
Convert the prior of the UKF into the measurement space.
vpLandmarkMeasurements(const double &x, const double &y, const double &range_std, const double &rel_angle_std)
Construct a new vpLandmarkMeasurements object.
vpColVector measureWithNoise(const vpColVector &pos)
Noisy measurement of the range and relative orientation that correspond to pos.
Class that represent a grid of landmarks that measure the distance and relative orientation of the 4-...
vpColVector measureGT(const vpColVector &pos)
Perfect measurement from each landmark of the range and relative orientation of the robot located at ...
vpLandmarksGrid(const std::vector< vpLandmarkMeasurements > &landmarks)
Construct a new vpLandmarksGrid object.
vpColVector measureWithNoise(const vpColVector &pos)
Noisy measurement from each landmark of the range and relative orientation that correspond to pos.
vpColVector state_to_measurement(const vpColVector &chi)
Convert the prior of the UKF into the measurement space.
static double rad(double deg)
Definition: vpMath.h:129
static float modulo(const float &value, const float &modulo)
Gives the rest of value divided by modulo when the quotient can only be an integer.
Definition: vpMath.h:177
Implementation of a matrix and operations on matrices.
Definition: vpMatrix.h:169
This class enables real time drawing of 2D or 3D graphics. An instance of the class open a window whi...
Definition: vpPlot.h:112
void initGraph(unsigned int graphNum, unsigned int curveNbr)
Definition: vpPlot.cpp:203
void setUnitY(unsigned int graphNum, const std::string &unity)
Definition: vpPlot.cpp:530
void setLegend(unsigned int graphNum, unsigned int curveNum, const std::string &legend)
Definition: vpPlot.cpp:552
void plot(unsigned int graphNum, unsigned int curveNum, double x, double y)
Definition: vpPlot.cpp:270
void setUnitX(unsigned int graphNum, const std::string &unitx)
Definition: vpPlot.cpp:520
void setTitle(unsigned int graphNum, const std::string &title)
Definition: vpPlot.cpp:510
std::function< vpColVector(const vpColVector &, const vpColVector &, const double &)> vpCommandStateFunction
Command model function, which projects effect of the command on the state, when the effect of the com...
std::function< vpColVector(const vpColVector &)> vpMeasurementFunction
Measurement function, which converts the prior points in the measurement space. The argument is a poi...
std::function< vpColVector(const vpColVector &, const double &)> vpProcessFunction
Process model function, which projects the sigma points forward in time. The first argument is a sigm...