Visual Servoing Platform  version 3.6.1 under development (2024-12-17)
catchLuminanceMapping.cpp
1 
2 /*
3  * ViSP, open source Visual Servoing Platform software.
4  * Copyright (C) 2005 - 2024 by Inria. All rights reserved.
5  *
6  * This software is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  * See the file LICENSE.txt at the root directory of this source
11  * distribution for additional information about the GNU GPL.
12  *
13  * For using ViSP with software that can not be combined with the GNU
14  * GPL, please contact Inria about acquiring a ViSP Professional
15  * Edition License.
16  *
17  * See https://visp.inria.fr for more information.
18  *
19  * This software was developed at:
20  * Inria Rennes - Bretagne Atlantique
21  * Campus Universitaire de Beaulieu
22  * 35042 Rennes Cedex
23  * France
24  *
25  * If you have questions regarding the use of this file, please contact
26  * Inria at visp@inria.fr
27  *
28  * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
29  * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
30  *
31  * Description:
32  * Performs various tests on the point class.
33  */
34 
40 #include <visp3/visual_features/vpFeatureLuminanceMapping.h>
41 
42 #include <visp3/core/vpSubMatrix.h>
43 
44 #include <visp3/core/vpUniRand.h>
45 #include <visp3/core/vpIoTools.h>
46 #if defined(VISP_HAVE_CATCH2)
47 
48 #include <catch_amalgamated.hpp>
49 
50 #ifdef ENABLE_VISP_NAMESPACE
51 using namespace VISP_NAMESPACE_NAME;
52 #endif
53 
54 vpMatrix orthogonalBasis(unsigned n, unsigned seed)
55 {
56  vpUniRand rand(seed);
57  vpMatrix basis(n, n);
58  vpColVector norms(n);
59 
60  //start with random basis
61  for (unsigned int row = 0; row < n; ++row) {
62  double norm = 0.0;
63  for (unsigned int col = 0; col < n; ++col) {
64  basis[row][col] = rand.uniform(-1.0, 1.0);
65  norm += basis[row][col] * basis[row][col];
66  }
67  norm = 1.0 / sqrt(norm);
68  for (unsigned int col = 0; col < n; ++col) {
69  basis[row][col] *= norm;
70  }
71 
72  }
73  // Apply gram schmidt process
74  norms[0] = basis.getRow(0).sumSquare();
75  for (unsigned i = 1; i < n; ++i) {
76  vpColVector uit = basis.getRow(i).t();
77 
78  for (unsigned j = 0; j < i; ++j) {
79  vpRowVector vj = basis.getRow(j);
80  vpRowVector res = vj * ((vj * uit) / (norms[j]));
81  for (unsigned k = 0; k < n; ++k) {
82  basis[i][k] -= res[k];
83  }
84  }
85  norms[i] = basis.getRow(i).sumSquare();
86  }
87  for (unsigned int row = 0; row < n; ++row) {
88  double norm = sqrt(norms[row]);
89 
90  for (unsigned int col = 0; col < n; ++col) {
91  basis[row][col] /= norm;
92  }
93  }
94  return basis;
95 
96 }
97 
98 SCENARIO("Using PCA features", "[visual_features]")
99 {
100  GIVEN("A matrix containing simple data")
101  {
102  const unsigned h = 16, w = 16;
103  const unsigned numDataPoints = 4;
104  const unsigned int dataDim = h * w;
105  const unsigned int trueComponents = 3;
106  // Generate numDataPoints vectors in a "dataDim"-dimensional space.
107  // The data is generated from "trueComponents" vectors, that are orthogonal
108  const vpMatrix orthoFull = (orthogonalBasis(dataDim, 42) + vpMatrix(dataDim, dataDim, 1.0)) * 127.5; // dataDim x dataDim
109  const vpMatrix ortho(orthoFull, 0, 0, trueComponents, dataDim); // trueComponents X dataDim
110  const vpMatrix coefficients(numDataPoints, trueComponents);
111  vpUniRand rand(17);
112  for (unsigned int i = 0; i < coefficients.getRows(); ++i) {
113  double sum = 0.0;
114  for (unsigned int j = 0; j < coefficients.getCols(); ++j) {
115  coefficients[i][j] = rand.uniform(0.0, 1.0);
116  sum += coefficients[i][j] * coefficients[i][j];
117  }
118  const double inv_norm = 1.0 / sqrt(sum);
119  for (unsigned int j = 0; j < coefficients.getCols(); ++j) {
120  coefficients[i][j] *= inv_norm;
121  }
122  }
123 
124  vpMatrix data = coefficients * ortho;
125 
126  WHEN("Learning PCA basis with too many components")
127  {
128  unsigned int k = data.getCols() + 1;
129  THEN("An exception is thrown")
130  {
131  REQUIRE_THROWS(vpLuminancePCA::learn(data.transpose(), k));
132  }
133  }
134  WHEN("Learning with more images than pixels")
135  {
136  vpMatrix wrongData(20, 50);
137  THEN("An exception is thrown")
138  {
139  REQUIRE_THROWS(vpLuminancePCA::learn(wrongData.transpose(), 32));
140  }
141  }
142  WHEN("Learning PCA basis")
143  {
144  for (unsigned int k = 2; k <= trueComponents; ++k) {
145 
147  const vpMatrix &basis = *pca.getBasis();
148 
149  THEN("Basis has correct dimensions")
150  {
151  REQUIRE(basis.getRows() == k);
152  REQUIRE(basis.getCols() == dataDim);
153  }
154  THEN("The basis is orthonormal")
155  {
156  const vpMatrix Iapprox = basis * basis.t();
157  vpMatrix I;
158  I.eye(basis.getRows());
159  bool matrixSame = true;
160  for (unsigned int row = 0; row < I.getRows(); ++row) {
161  for (unsigned int col = 0; col < I.getCols(); ++col) {
162  if (fabs(I[row][col] - Iapprox[row][col]) > 1e-6) {
163  matrixSame = false;
164  break;
165  }
166  }
167  }
168  REQUIRE(matrixSame);
169  }
170  THEN("Mean vector has correct dimensions")
171  {
172  REQUIRE(pca.getMean()->getRows() == dataDim);
173  REQUIRE(pca.getMean()->getCols() == 1);
174  }
175  THEN("Modifying the basis size (number of inputs) by hand and saving")
176  {
177  const std::string tempDir = vpIoTools::makeTempDirectory("visp_test_pca_wrong");
178  const std::string basisFile = vpIoTools::createFilePath(tempDir, "basis.txt");
179  const std::string meanFile = vpIoTools::createFilePath(tempDir, "mean.txt");
180  const std::string varFile = vpIoTools::createFilePath(tempDir, "var.txt");
181  pca.getBasis()->resize(pca.getBasis()->getRows(), pca.getBasis()->getCols() - 1);
182  REQUIRE_THROWS(pca.save(basisFile, meanFile, varFile));
183  }
184  THEN("Modifying the mean Columns by hand")
185  {
186  const std::string tempDir = vpIoTools::makeTempDirectory("visp_test_pca_wrong");
187  const std::string basisFile = vpIoTools::createFilePath(tempDir, "basis.txt");
188  const std::string meanFile = vpIoTools::createFilePath(tempDir, "mean.txt");
189  const std::string varFile = vpIoTools::createFilePath(tempDir, "var.txt");
190 
191  std::shared_ptr<vpColVector> mean = pca.getMean();
192  mean->resize(mean->getRows() + 1, false);
193  REQUIRE_THROWS(pca.save(basisFile, meanFile, varFile));
194 
195  }
196  THEN("Saving and loading pca leads to same basis and mean")
197  {
198  const std::string tempDir = vpIoTools::makeTempDirectory("visp_test_pca");
199  const std::string basisFile = vpIoTools::createFilePath(tempDir, "basis.txt");
200  const std::string meanFile = vpIoTools::createFilePath(tempDir, "mean.txt");
201  const std::string varFile = vpIoTools::createFilePath(tempDir, "var.txt");
202 
203  pca.save(basisFile, meanFile, varFile);
204 
205  const vpLuminancePCA pca2 = vpLuminancePCA::load(basisFile, meanFile, varFile);
206  const vpMatrix basisDiff = *pca.getBasis() - *pca2.getBasis();
207  const vpColVector meanDiff = *pca.getMean() - *pca2.getMean();
208  const vpColVector explainedVarDiff = pca.getExplainedVariance() - pca2.getExplainedVariance();
209  bool basisSame = true;
210  bool meanSame = true;
211  bool explainedVarSame = true;
212 
213  for (unsigned int i = 0; i < basisDiff.getRows(); ++i) {
214  for (unsigned int j = 0; j < basisDiff.getCols(); ++j) {
215  if (fabs(basisDiff[i][j]) > 1e-10) {
216  basisSame = false;
217  break;
218  }
219  }
220  }
221  REQUIRE(basisSame);
222 
223  for (unsigned int i = 0; i < meanDiff.getRows(); ++i) {
224  if (fabs(meanDiff[i]) > 1e-10) {
225  std::cout << meanDiff << std::endl;
226  meanSame = false;
227  break;
228  }
229  }
230  REQUIRE(meanSame);
231 
232  for (unsigned int i = 0; i < explainedVarDiff.getRows(); ++i) {
233  if (fabs(explainedVarDiff[i]) > 1e-10) {
234  explainedVarSame = false;
235  break;
236  }
237  }
238  REQUIRE(explainedVarSame);
239  }
240 
241  THEN("Explained variance is below 1 and sorted in descending order")
242  {
243  const vpColVector var = pca.getExplainedVariance();
244  REQUIRE(var.sum() < 1.0);
245  for (int i = 1; i < (int)var.getRows() - 1; ++i) {
246  REQUIRE(var[i] >= var[i + 1]);
247  }
248  }
249  if (k == trueComponents) {
250  WHEN("K is the true manifold dimensionality")
251  {
252  THEN("explained variance is close to 1")
253  {
254  REQUIRE(pca.getExplainedVariance().sum() > 0.99);
255  }
256  THEN("Inverse mapping leads back to the same data")
257  {
258  for (unsigned int i = 0; i < numDataPoints; ++i) {
259  vpImage<unsigned char> I(h, w);
260  for (unsigned int j = 0; j < data.getCols(); ++j) {
261  I.bitmap[j] = static_cast<unsigned char>(data[i][j]);
262  }
263  vpColVector s;
264  pca.setBorder(0);
265  pca.map(I, s);
267  pca.inverse(s, Irec);
268  for (unsigned int j = 0; j < data.getCols(); ++j) {
269  REQUIRE(abs(static_cast<int>(I.bitmap[j]) - static_cast<int>(Irec.bitmap[j])) < 2);
270  }
271  }
272  }
273  }
274  }
275 
276  THEN("Projecting data is correct")
277  {
278  {
279  vpColVector s;
280  pca.setBorder(0);
281  vpImage<unsigned char> I(h, w);
282  pca.map(I, s);
283  REQUIRE(s.size() == pca.getProjectionSize());
284  }
285  {
286  vpColVector s;
287  const unsigned border = 3;
288  pca.setBorder(border);
289  REQUIRE(pca.getBorder() == border);
290  vpImage<unsigned char> I(h + 2 * border, w + 2 * border);
291  pca.map(I, s);
292  REQUIRE(s.size() == pca.getProjectionSize());
293  }
294  }
295  }
296  }
297  }
298  WHEN("Saving unintialized PCA")
299  {
300  vpLuminancePCA pca;
301  const std::string tempDir = vpIoTools::makeTempDirectory("visp_test_pca");
302  const std::string basisFile = vpIoTools::createFilePath(tempDir, "basis.txt");
303  const std::string meanFile = vpIoTools::createFilePath(tempDir, "mean.txt");
304  const std::string varFile = vpIoTools::createFilePath(tempDir, "var.txt");
305  THEN("an exception is thrown")
306  {
307  REQUIRE_THROWS(pca.save(basisFile, meanFile, varFile));
308  }
309  }
310 }
311 
312 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_11)
313 SCENARIO("Using DCT features", "[visual_features]")
314 {
315  GIVEN("A matrix")
316  {
317  std::vector<std::tuple<vpMatrix, vpColVector, vpMatrix>> data = {
318  {
319  vpMatrix({
320  {0.0, 1.0, 2.0},
321  {3.0, 4.0, 5.0},
322  {6.0, 7.0, 8.0}
323  }),
324  vpColVector(
325  { 0.0, 1.0, 3.0, 6.0, 4.0, 2.0, 5.0, 7.0, 8.0 }
326  ),
327  vpMatrix({
328  {0.0, 1.0, 5.0},
329  {2.0, 4.0, 6.0},
330  {3.0, 7.0, 8.0}
331  })
332  }
333  };
334  for (unsigned int i = 0; i < data.size(); ++i) {
335  WHEN("Building the associated zigzag indexing matrix")
336  {
337  vpMatrix m = std::get<0>(data[i]);
338  vpColVector contentAsZigzag = std::get<1>(data[i]);
339  const vpMatrix mAfterWriterVec = std::get<2>(data[i]);
341  zigzag.init(m.getRows(), m.getCols());
342  vpColVector s;
343  THEN("Calling getValues with wrong matrix rows throws")
344  {
345  vpMatrix wrongM(m.getRows() + 1, m.getCols());
346  REQUIRE_THROWS(zigzag.getValues(wrongM, 0, 2, s));
347  }
348  THEN("Calling getValues with wrong matrix cols throws")
349  {
350  vpMatrix wrongM(m.getRows(), m.getCols() + 1);
351  REQUIRE_THROWS(zigzag.getValues(wrongM, 0, 2, s));
352  }
353  THEN("Calling getValues with wrong start and end arguments throws")
354  {
355  REQUIRE_THROWS(zigzag.getValues(m, 2, 1, s));
356  }
357  THEN("Calling getValues and querying all values returns correct result")
358  {
359  REQUIRE_NOTHROW(zigzag.getValues(m, 0, m.size(), s));
360  REQUIRE(s == contentAsZigzag);
361  }
362  THEN("Calling getValues and querying a subset of the values is correct")
363  {
364  REQUIRE_NOTHROW(zigzag.getValues(m, 0, m.size() / 2, s));
365  REQUIRE(s == contentAsZigzag.extract(0, m.size() / 2));
366  REQUIRE_NOTHROW(zigzag.getValues(m, m.size() / 2, m.size(), s));
367  REQUIRE(s == contentAsZigzag.extract(m.size() / 2, m.size() - m.size() / 2));
368  }
369  THEN("Calling setValues with wrong matrix rows throws")
370  {
371  vpMatrix wrongM(m.getRows() + 1, m.getCols());
372  REQUIRE_THROWS(zigzag.setValues(contentAsZigzag, 0, wrongM));
373  }
374  THEN("Calling setValues with wrong matrix cols throws")
375  {
376  vpMatrix wrongM(m.getRows(), m.getCols() + 1);
377  REQUIRE_THROWS(zigzag.setValues(contentAsZigzag, 0, wrongM));
378  }
379 
380  THEN("Calling setValues with wrong start and vector size arguments throws")
381  {
382  REQUIRE_THROWS(zigzag.setValues(contentAsZigzag, m.size() - contentAsZigzag.size() + 1, m));
383  }
384 
385  THEN("Calling setValues leads to expected result")
386  {
387  vpMatrix mWrite(m.getRows(), m.getCols());
388  vpColVector powered = contentAsZigzag;
389  for (unsigned i = 0; i < powered.size(); ++i) {
390  powered[i] *= powered[i];
391  }
392  vpColVector poweredRead;
393  REQUIRE_NOTHROW(zigzag.setValues(powered, 0, mWrite));
394  REQUIRE_NOTHROW(zigzag.getValues(mWrite, 0, mWrite.size(), poweredRead));
395  REQUIRE(powered == poweredRead);
396 
397  vpColVector indices = contentAsZigzag;
398  for (unsigned i = 0; i < powered.size(); ++i) {
399  indices[i] = static_cast<double>(i);
400  }
401  vpColVector indicesRead;
402  REQUIRE_NOTHROW(zigzag.setValues(indices, 0, mWrite));
403  REQUIRE(mWrite == mAfterWriterVec);
404 
405  vpMatrix m2(m.getRows(), m.getCols(), 0.0);
406  zigzag.setValues(contentAsZigzag.extract(0, 3), 0, m2);
407 
408  vpColVector s2;
409  zigzag.getValues(m2, 0, 3, s2);
410  REQUIRE(s2 == contentAsZigzag.extract(0, 3));
411 
412  }
413 
414  }
415  }
416 
417 
418  GIVEN("A constant image")
419  {
420  vpImage<unsigned char> I(32, 64, 20);
421  WHEN("Computing DCT")
422  {
423  vpLuminanceDCT dct(32);
424  dct.setBorder(0);
425  vpColVector s;
426  dct.map(I, s);
427  THEN("resulting feature vector has correct size")
428  {
429  REQUIRE(s.size() == 32);
430  }
431  THEN("The only non zero component is the first")
432  {
433  REQUIRE(s.sum() == Catch::Approx(s[0]).margin(1e-5));
434  }
435 
437  dct.inverse(s, Ir);
438  REQUIRE((Ir.getRows() == I.getRows() && Ir.getCols() == I.getCols()));
439  for (unsigned i = 0; i < I.getRows(); ++i) {
440  for (unsigned j = 0; j < I.getCols(); ++j) {
441  const int diff = abs(static_cast<int>(I[i][j]) - static_cast<int>(Ir[i][j]));
442  REQUIRE(diff < 2);
443  INFO("i = " + std::to_string(i) + ", j = " + std::to_string(j));
444  }
445  }
446  }
447  }
448  }
449 }
450 #endif
451 
452 int main(int argc, char *argv[])
453 {
454  Catch::Session session; // There must be exactly one instance
455  session.applyCommandLine(argc, argv);
456 
457  int numFailed = session.run();
458  return numFailed;
459 }
460 
461 #else
462 
463 int main()
464 {
465  return EXIT_SUCCESS;
466 }
467 #endif
unsigned int getCols() const
Definition: vpArray2D.h:337
unsigned int size() const
Return the number of elements of the 2D array.
Definition: vpArray2D.h:349
unsigned int getRows() const
Definition: vpArray2D.h:347
Implementation of column vector and the associated operations.
Definition: vpColVector.h:191
vpColVector extract(unsigned int r, unsigned int colsize) const
Definition: vpColVector.h:405
vpRowVector t() const
double sum() const
unsigned int getCols() const
Definition: vpImage.h:171
Type * bitmap
points toward the bitmap
Definition: vpImage.h:135
unsigned int getRows() const
Definition: vpImage.h:212
static std::string createFilePath(const std::string &parent, const std::string &child)
Definition: vpIoTools.cpp:1427
static std::string makeTempDirectory(const std::string &dirname)
Definition: vpIoTools.cpp:708
Helper class to iterate and get/set the values from a matrix, following a zigzag pattern.
void init(unsigned rows, unsigned cols)
Initialize the ZigZag object. Computes and stores the zigzag indexing for a given matrix size.
void setValues(const vpColVector &s, unsigned int start, vpMatrix &m) const
set the values in the matrix, according to the values stored in the vector s and the zigzag indexing ...
void getValues(const vpMatrix &m, unsigned int start, unsigned int end, vpColVector &s) const
Fill the vector s with (end - start) values, according to the zigzag matrix indexing strategy.
Implementation of .
unsigned int getProjectionSize() const
Returns the size of the space to which an image is mapped to.
unsigned int getBorder() const
Returns the number of pixels that are removed by the photometric VS computation.
void setBorder(unsigned border)
Set the number of pixels that are removed by the photometric VS computation This function should be c...
Implementation of .
static vpLuminancePCA learn(const std::vector< vpImage< unsigned char >> &images, const unsigned int projectionSize, const unsigned int imageBorder=0)
Compute a new Principal Component Analysis on set of images.
void inverse(const vpColVector &s, vpImage< unsigned char > &I) VP_OVERRIDE
Reconstruct I from a representation s.
void save(const std::string &basisFilename, const std::string &meanFileName, const std::string &explainedVarianceFile) const
Save the PCA basis to multiple text files, for later use via the load function.
std::shared_ptr< vpColVector > getMean() const
Get , the mean image computed from the dataset.
void map(const vpImage< unsigned char > &I, vpColVector &s) VP_OVERRIDE
Map an image I to a representation s. This representation s has getProjectionSize() rows.
std::shared_ptr< vpMatrix > getBasis() const
Get , the subspace projection matrix ( )
static vpLuminancePCA load(const std::string &basisFilename, const std::string &meanFileName, const std::string &explainedVarianceFile)
Save the PCA basis to multiple text files, for later use via the load function.
vpColVector getExplainedVariance() const
Get the values of explained variance by each of the eigen vectors.
Implementation of a matrix and operations on matrices.
Definition: vpMatrix.h:169
vpMatrix transpose() const
vpMatrix t() const
Implementation of row vector and the associated operations.
Definition: vpRowVector.h:124
double sumSquare() const
Class for generating random numbers with uniform probability density.
Definition: vpUniRand.h:127