Visual Servoing Platform  version 3.6.1 under development (2024-07-27)
vpHomographyRansac.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  * Description:
31  * Homography estimation.
32  */
33 
34 #include <visp3/core/vpDebug.h>
35 #include <visp3/core/vpColVector.h>
36 #include <visp3/core/vpRansac.h>
37 #include <visp3/vision/vpHomography.h>
38 
39 #include <visp3/core/vpDisplay.h>
40 #include <visp3/core/vpImage.h>
41 #include <visp3/core/vpMeterPixelConversion.h>
42 
43 #define VPEPS 1e-6
44 
46 
52 #ifndef DOXYGEN_SHOULD_SKIP_THIS
53 
54 bool iscolinear(double *x1, double *x2, double *x3);
55 bool isColinear(vpColVector &p1, vpColVector &p2, vpColVector &p3);
56 
57 bool iscolinear(double *x1, double *x2, double *x3)
58 {
59  vpColVector p1(3), p2(3), p3(3);
60  p1 << x1;
61  p2 << x2;
62  p3 << x3;
63  // Assume inhomogeneous coords, or homogeneous coords with equal
64  // scale.
65  return ((vpColVector::cross(p2 - p1, p3 - p1).sumSquare()) < VPEPS);
66 }
67 bool isColinear(vpColVector &p1, vpColVector &p2, vpColVector &p3)
68 {
69  return ((vpColVector::cross(p2 - p1, p3 - p1).sumSquare()) < VPEPS);
70 }
71 
72 bool vpHomography::degenerateConfiguration(const vpColVector &x, unsigned int *ind, double threshold_area)
73 {
74 
75  unsigned int i, j, k;
76 
77  for (i = 1; i < 4; ++i) {
78  for (j = 0; j < i; ++j) {
79  if (ind[i] == ind[j]) {
80  return true;
81  }
82  }
83  }
84 
85  unsigned int n = x.getRows() / 4;
86  double pa[4][3];
87  double pb[4][3];
88 
89  for (i = 0; i < 4; ++i) {
90  pb[i][0] = x[2 * ind[i]];
91  pb[i][1] = x[(2 * ind[i]) + 1];
92  pb[i][2] = 1;
93 
94  pa[i][0] = x[(2 * n) + (2 * ind[i])];
95  pa[i][1] = x[(2 * n) + (2 * ind[i]) + 1];
96  pa[i][2] = 1;
97  }
98 
99  i = 0, j = 1, k = 2;
100 
101  double area012 = ((((((-pa[j][0] * pa[i][1]) + (pa[k][0] * pa[i][1])) + (pa[i][0] * pa[j][1])) - (pa[k][0] * pa[j][1])) +
102  (-pa[i][0] * pa[k][1])) + (pa[1][j] * pa[k][1]));
103 
104  i = 0;
105  j = 1, k = 3;
106  double area013 = ((((((-pa[j][0] * pa[i][1]) + (pa[k][0] * pa[i][1])) + (pa[i][0] * pa[j][1])) - (pa[k][0] * pa[j][1])) +
107  (-pa[i][0] * pa[k][1])) + (pa[1][j] * pa[k][1]));
108 
109  i = 0;
110  j = 2, k = 3;
111  double area023 = ((((((-pa[j][0] * pa[i][1]) + (pa[k][0] * pa[i][1])) + (pa[i][0] * pa[j][1])) - (pa[k][0] * pa[j][1])) +
112  (-pa[i][0] * pa[k][1])) + (pa[1][j] * pa[k][1]));
113 
114  i = 1;
115  j = 2, k = 3;
116  double area123 = ((((((-pa[j][0] * pa[i][1]) + (pa[k][0] * pa[i][1])) + (pa[i][0] * pa[j][1])) - (pa[k][0] * pa[j][1])) +
117  (-pa[i][0] * pa[k][1])) + (pa[1][j] * pa[k][1]));
118 
119  double sum_area = area012 + area013 + area023 + area123;
120 
121  if (sum_area < threshold_area) {
122  return true;
123  }
124  else if (iscolinear(pa[0], pa[1], pa[2])) {
125  return true;
126  }
127  else if (iscolinear(pa[0], pa[1], pa[3])) {
128  return true;
129  }
130  else if (iscolinear(pa[0], pa[2], pa[3])) {
131  return true;
132  }
133  else if (iscolinear(pa[1], pa[2], pa[3])) {
134  return true;
135  }
136  else if (iscolinear(pb[0], pb[1], pb[2])) {
137  return true;
138  }
139  else if (iscolinear(pb[0], pb[1], pb[3])) {
140  return true;
141  }
142  else if (iscolinear(pb[0], pb[2], pb[3])) {
143  return true;
144  }
145  else if (iscolinear(pb[1], pb[2], pb[3])) {
146  return true;
147  }
148  else {
149  return false;
150  }
151 }
152 
153 /*
154  \brief
155  Function to determine if a set of 4 pairs of matched points give rise
156  to a degeneracy in the calculation of a homography as needed by RANSAC.
157  This involves testing whether any 3 of the 4 points in each set is
158  colinear.
159 
160  point are coded this way
161  x1b,y1b, x2b, y2b, ... xnb, ynb
162  x1a,y1a, x2a, y2a, ... xna, yna
163  leading to 2*2*n
164 */
165 bool vpHomography::degenerateConfiguration(const vpColVector &x, unsigned int *ind)
166 {
167  for (unsigned int i = 1; i < 4; ++i) {
168  for (unsigned int j = 0; j < i; ++j) {
169  if (ind[i] == ind[j]) {
170  return true;
171  }
172  }
173  }
174 
175  const unsigned int val_4 = 4;
176  unsigned int n = x.getRows() / val_4;
177  double pa[4][3];
178  double pb[4][3];
179  unsigned int n2 = 2 * n;
180  for (unsigned int i = 0; i < val_4; ++i) {
181  unsigned int ind2 = 2 * ind[i];
182  pb[i][0] = x[ind2];
183  pb[i][1] = x[ind2 + 1];
184  pb[i][2] = 1;
185 
186  pa[i][0] = x[n2 + ind2];
187  pa[i][1] = x[n2 + ind2 + 1];
188  pa[i][2] = 1;
189  }
190 
191  if (iscolinear(pa[0], pa[1], pa[2])) {
192  return true;
193  }
194  else if (iscolinear(pa[0], pa[1], pa[3])) {
195  return true;
196  }
197  else if (iscolinear(pa[0], pa[2], pa[3])) {
198  return true;
199  }
200  else if (iscolinear(pa[1], pa[2], pa[3])) {
201  return true;
202  }
203  else if (iscolinear(pb[0], pb[1], pb[2])) {
204  return true;
205  }
206  else if (iscolinear(pb[0], pb[1], pb[3])) {
207  return true;
208  }
209  else if (iscolinear(pb[0], pb[2], pb[3])) {
210  return true;
211  }
212  else if (iscolinear(pb[1], pb[2], pb[3])) {
213  return true;
214  }
215  else {
216  return false;
217  }
218 }
219 
220 bool vpHomography::degenerateConfiguration(const std::vector<double> &xb, const std::vector<double> &yb,
221  const std::vector<double> &xa, const std::vector<double> &ya)
222 {
223  unsigned int n = static_cast<unsigned int>(xb.size());
224  if (n < 4) {
225  throw(vpException(vpException::fatalError, "There must be at least 4 matched points"));
226  }
227 
228  std::vector<vpColVector> pa(n), pb(n);
229  for (unsigned i = 0; i < n; ++i) {
230  pa[i].resize(3);
231  pa[i][0] = xa[i];
232  pa[i][1] = ya[i];
233  pa[i][2] = 1;
234  pb[i].resize(3);
235  pb[i][0] = xb[i];
236  pb[i][1] = yb[i];
237  pb[i][2] = 1;
238  }
239 
240  for (unsigned int i = 0; i < (n - 2); ++i) {
241  for (unsigned int j = i + 1; j < (n - 1); ++j) {
242  for (unsigned int k = j + 1; k < n; ++k) {
243  if (isColinear(pa[i], pa[j], pa[k])) {
244  return true;
245  }
246  if (isColinear(pb[i], pb[j], pb[k])) {
247  return true;
248  }
249  }
250  }
251  }
252  return false;
253 }
254 // Fit model to this random selection of data points.
255 void vpHomography::computeTransformation(vpColVector &x, unsigned int *ind, vpColVector &M)
256 {
257  const unsigned int val_4 = 4;
258  unsigned int n = x.getRows() / val_4;
259  std::vector<double> xa(4), xb(4);
260  std::vector<double> ya(4), yb(4);
261  unsigned int n2 = n * 2;
262  for (unsigned int i = 0; i < val_4; ++i) {
263  unsigned int ind2 = 2 * ind[i];
264  xb[i] = x[ind2];
265  yb[i] = x[ind2 + 1];
266 
267  xa[i] = x[n2 + ind2];
268  ya[i] = x[n2 + ind2 + 1];
269  }
270 
271  vpHomography aHb;
272  try {
273  vpHomography::HLM(xb, yb, xa, ya, true, aHb);
274  }
275  catch (...) {
276  aHb.eye();
277  }
278 
279  M.resize(9);
280  for (unsigned int i = 0; i < 9; ++i) {
281  M[i] = aHb.data[i];
282  }
283  aHb /= aHb[2][2];
284 }
285 
286 // Evaluate distances between points and model.
287 double vpHomography::computeResidual(vpColVector &x, vpColVector &M, vpColVector &d)
288 {
289  unsigned int n = x.getRows() / 4;
290  unsigned int n2 = n * 2;
291  vpColVector *pa;
292  vpColVector *pb;
293 
294  pa = new vpColVector[n];
295  pb = new vpColVector[n];
296 
297  for (unsigned int i = 0; i < n; ++i) {
298  unsigned int i2 = 2 * i;
299  pb[i].resize(3);
300  pb[i][0] = x[i2];
301  pb[i][1] = x[i2 + 1];
302  pb[i][2] = 1;
303 
304  pa[i].resize(3);
305  pa[i][0] = x[n2 + i2];
306  pa[i][1] = x[n2 + i2 + 1];
307  pa[i][2] = 1;
308  }
309 
310  vpMatrix aHb(3, 3);
311 
312  for (unsigned int i = 0; i < 9; ++i) {
313  aHb.data[i] = M[i];
314  }
315 
316  aHb /= aHb[2][2];
317 
318  d.resize(n);
319 
320  vpColVector Hpb;
321  for (unsigned int i = 0; i < n; ++i) {
322  Hpb = aHb * pb[i];
323  Hpb /= Hpb[2];
324  d[i] = sqrt((pa[i] - Hpb).sumSquare());
325  }
326 
327  delete[] pa;
328  delete[] pb;
329 
330  return 0;
331 }
332 #endif //#ifndef DOXYGEN_SHOULD_SKIP_THIS
333 
334 void vpHomography::initRansac(unsigned int n, double *xb, double *yb, double *xa, double *ya, vpColVector &x)
335 {
336  x.resize(4 * n);
337  unsigned int n2 = n * 2;
338  for (unsigned int i = 0; i < n; ++i) {
339  unsigned int i2 = 2 * i;
340  x[i2] = xb[i];
341  x[i2 + 1] = yb[i];
342  x[n2 + i2] = xa[i];
343  x[n2 + i2 + 1] = ya[i];
344  }
345 }
346 
347 bool vpHomography::ransac(const std::vector<double> &xb, const std::vector<double> &yb, const std::vector<double> &xa,
348  const std::vector<double> &ya, vpHomography &aHb, std::vector<bool> &inliers,
349  double &residual, unsigned int nbInliersConsensus, double threshold, bool normalization)
350 {
351  unsigned int n = static_cast<unsigned int>(xb.size());
352  if ((yb.size() != n) || (xa.size() != n) || (ya.size() != n)) {
353  throw(vpException(vpException::dimensionError, "Bad dimension for robust homography estimation"));
354  }
355 
356  // 4 point are required
357  if (n < 4) {
358  throw(vpException(vpException::fatalError, "There must be at least 4 matched points"));
359  }
360 
361  vpUniRand random(static_cast<long>(time(nullptr)));
362 
363  std::vector<unsigned int> best_consensus;
364  std::vector<unsigned int> cur_consensus;
365  std::vector<unsigned int> cur_outliers;
366  std::vector<unsigned int> cur_randoms;
367 
368  std::vector<unsigned int> rand_ind;
369 
370  unsigned int nbMinRandom = 4;
371  unsigned int ransacMaxTrials = 1000;
372  unsigned int maxDegenerateIter = 1000;
373 
374  unsigned int nbTrials = 0;
375  unsigned int nbDegenerateIter = 0;
376  unsigned int nbInliers = 0;
377 
378  bool foundSolution = false;
379 
380  std::vector<double> xa_rand(nbMinRandom);
381  std::vector<double> ya_rand(nbMinRandom);
382  std::vector<double> xb_rand(nbMinRandom);
383  std::vector<double> yb_rand(nbMinRandom);
384 
385  if (inliers.size() != n) {
386  inliers.resize(n);
387  }
388 
389  while ((nbTrials < ransacMaxTrials) && (nbInliers < nbInliersConsensus)) {
390  cur_outliers.clear();
391  cur_randoms.clear();
392 
393  bool degenerate = true;
394  while (degenerate == true) {
395  std::vector<bool> usedPt(n, false);
396 
397  rand_ind.clear();
398  for (unsigned int i = 0; i < nbMinRandom; ++i) {
399  // Generate random indicies in the range 0..n
400  unsigned int r = static_cast<unsigned int>(ceil(random() * n)) - 1;
401  while (usedPt[r]) {
402  r = static_cast<unsigned int>(ceil(random() * n)) - 1;
403  }
404  usedPt[r] = true;
405  rand_ind.push_back(r);
406 
407  xa_rand[i] = xa[r];
408  ya_rand[i] = ya[r];
409  xb_rand[i] = xb[r];
410  yb_rand[i] = yb[r];
411  }
412 
413  try {
414  if (!vpHomography::degenerateConfiguration(xb_rand, yb_rand, xa_rand, ya_rand)) {
415  vpHomography::DLT(xb_rand, yb_rand, xa_rand, ya_rand, aHb, normalization);
416  degenerate = false;
417  }
418  }
419  catch (...) {
420  degenerate = true;
421  }
422 
423  ++nbDegenerateIter;
424 
425  if (nbDegenerateIter > maxDegenerateIter) {
426  vpERROR_TRACE("Unable to select a nondegenerate data set");
427  throw(vpException(vpException::fatalError, "Unable to select a nondegenerate data set"));
428  }
429  }
430 
431  aHb /= aHb[2][2];
432 
433  // Computing Residual
434  double r = 0;
435  vpColVector a(3), b(3), c(3);
436  for (unsigned int i = 0; i < nbMinRandom; ++i) {
437  a[0] = xa_rand[i];
438  a[1] = ya_rand[i];
439  a[2] = 1;
440  b[0] = xb_rand[i];
441  b[1] = yb_rand[i];
442  b[2] = 1;
443 
444  c = aHb * b;
445  c /= c[2];
446  r += (a - c).sumSquare();
447  }
448 
449  // Finding inliers & ouliers
450  r = sqrt(r / nbMinRandom);
451  if (r < threshold) {
452  unsigned int nbInliersCur = 0;
453  for (unsigned int i = 0; i < n; ++i) {
454  a[0] = xa[i];
455  a[1] = ya[i];
456  a[2] = 1;
457  b[0] = xb[i];
458  b[1] = yb[i];
459  b[2] = 1;
460 
461  c = aHb * b;
462  c /= c[2];
463  double error = sqrt((a - c).sumSquare());
464  if (error <= threshold) {
465  ++nbInliersCur;
466  cur_consensus.push_back(i);
467  inliers[i] = true;
468  }
469  else {
470  cur_outliers.push_back(i);
471  inliers[i] = false;
472  }
473  }
474 
475  if (nbInliersCur > nbInliers) {
476  foundSolution = true;
477  best_consensus = cur_consensus;
478  nbInliers = nbInliersCur;
479  }
480 
481  cur_consensus.clear();
482  }
483 
484  ++nbTrials;
485  if (nbTrials >= ransacMaxTrials) {
486  vpERROR_TRACE("Ransac reached the maximum number of trials");
487  foundSolution = true;
488  }
489  }
490 
491  if (foundSolution) {
492  if (nbInliers >= nbInliersConsensus) {
493  const unsigned int nbConsensus = static_cast<unsigned int>(best_consensus.size());
494  std::vector<double> xa_best(nbConsensus);
495  std::vector<double> ya_best(nbConsensus);
496  std::vector<double> xb_best(nbConsensus);
497  std::vector<double> yb_best(nbConsensus);
498 
499  for (unsigned i = 0; i < nbConsensus; ++i) {
500  xa_best[i] = xa[best_consensus[i]];
501  ya_best[i] = ya[best_consensus[i]];
502  xb_best[i] = xb[best_consensus[i]];
503  yb_best[i] = yb[best_consensus[i]];
504  }
505 
506  vpHomography::DLT(xb_best, yb_best, xa_best, ya_best, aHb, normalization);
507  aHb /= aHb[2][2];
508 
509  residual = 0;
510  vpColVector a(3), b(3), c(3);
511 
512  for (unsigned int i = 0; i < nbConsensus; ++i) {
513  a[0] = xa_best[i];
514  a[1] = ya_best[i];
515  a[2] = 1;
516  b[0] = xb_best[i];
517  b[1] = yb_best[i];
518  b[2] = 1;
519 
520  c = aHb * b;
521  c /= c[2];
522  residual += (a - c).sumSquare();
523  }
524 
525  residual = sqrt(residual / nbConsensus);
526  return true;
527  }
528  else {
529  return false;
530  }
531  }
532  else {
533  return false;
534  }
535 }
536 
537 END_VISP_NAMESPACE
Type * data
Address of the first element of the data array.
Definition: vpArray2D.h:148
unsigned int getRows() const
Definition: vpArray2D.h:347
Implementation of column vector and the associated operations.
Definition: vpColVector.h:191
static vpColVector cross(const vpColVector &a, const vpColVector &b)
Definition: vpColVector.h:1259
void resize(unsigned int i, bool flagNullify=true)
Definition: vpColVector.h:1143
error that can be emitted by ViSP classes.
Definition: vpException.h:60
@ dimensionError
Bad dimension.
Definition: vpException.h:71
@ fatalError
Fatal error.
Definition: vpException.h:72
Implementation of an homography and operations on homographies.
Definition: vpHomography.h:174
static void DLT(const std::vector< double > &xb, const std::vector< double > &yb, const std::vector< double > &xa, const std::vector< double > &ya, vpHomography &aHb, bool normalization=true)
static void HLM(const std::vector< double > &xb, const std::vector< double > &yb, const std::vector< double > &xa, const std::vector< double > &ya, bool isplanar, vpHomography &aHb)
static bool ransac(const std::vector< double > &xb, const std::vector< double > &yb, const std::vector< double > &xa, const std::vector< double > &ya, vpHomography &aHb, std::vector< bool > &inliers, double &residual, unsigned int nbInliersConsensus, double threshold, bool normalization=true)
Implementation of a matrix and operations on matrices.
Definition: vpMatrix.h:169
Class for generating random numbers with uniform probability density.
Definition: vpUniRand.h:125
#define vpERROR_TRACE
Definition: vpDebug.h:409