Visual Servoing Platform  version 3.6.1 under development (2024-05-03)
vpCircleHoughTransform.cpp
1 /*
2  * ViSP, open source Visual Servoing Platform software.
3  * Copyright (C) 2005 - 2023 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 
31 #include <visp3/core/vpImageConvert.h>
32 #include <visp3/core/vpImageMorphology.h>
33 
34 #include <visp3/imgproc/vpCircleHoughTransform.h>
35 
36 // Static variables
37 const unsigned char vpCircleHoughTransform::edgeMapOn = 255;
38 const unsigned char vpCircleHoughTransform::edgeMapOff = 0;
39 
40 #if (VISP_CXX_STANDARD == VISP_CXX_STANDARD_98)
41 namespace
42 {
43 // Sorting by decreasing probabilities
44 bool hasBetterProba(std::pair<size_t, float> a, std::pair<size_t, float> b)
45 {
46  return (a.second > b.second);
47 }
48 
49 void updateAccumulator(const float &x_orig, const float &y_orig,
50  const int &x, const int &y,
51  const int &offsetX, const int &offsetY,
52  const int &nbCols, const int &nbRows,
53  vpImage<float> &accum, bool &hasToStop)
54 {
55  if (((x - offsetX) < 0) ||
56  ((x - offsetX) >= nbCols) ||
57  ((y - offsetY) < 0) ||
58  ((y - offsetY) >= nbRows)
59  ) {
60  hasToStop = true;
61  }
62  else {
63  float dx = (x_orig - static_cast<float>(x));
64  float dy = (y_orig - static_cast<float>(y));
65  accum[y - offsetY][x - offsetX] += std::abs(dx) + std::abs(dy);
66  }
67 }
68 
69 bool sortingCenters(const std::pair<std::pair<float, float>, float> &position_vote_a,
70  const std::pair<std::pair<float, float>, float> &position_vote_b)
71 {
72  return position_vote_a.second > position_vote_b.second;
73 }
74 
75 float computeEffectiveRadius(const float &votes, const float &weigthedSumRadius)
76 {
77  float r_effective = -1.f;
78  if (votes > std::numeric_limits<float>::epsilon()) {
79  r_effective = weigthedSumRadius / votes;
80  }
81  return r_effective;
82 }
83 
84 void
85 scaleFilter(vpArray2D<float> &filter, const float &scale)
86 {
87  unsigned int nbRows = filter.getRows();
88  unsigned int nbCols = filter.getCols();
89  for (unsigned int r = 0; r < nbRows; ++r) {
90  for (unsigned int c = 0; c < nbCols; ++c) {
91  filter[r][c] = filter[r][c] * scale;
92  }
93  }
94 }
95 };
96 #endif
97 
99  : m_algoParams()
100  , mp_mask(nullptr)
101 {
104 }
105 
107  : m_algoParams(algoParams)
108  , mp_mask(nullptr)
109 {
112 }
113 
114 void
116 {
117  m_algoParams = algoParams;
120 }
121 
123 { }
124 
125 #ifdef VISP_HAVE_NLOHMANN_JSON
126 using json = nlohmann::json;
127 
129 {
130  initFromJSON(jsonPath);
131 }
132 
133 void
134 vpCircleHoughTransform::initFromJSON(const std::string &jsonPath)
135 {
136  std::ifstream file(jsonPath);
137  if (!file.good()) {
138  std::stringstream ss;
139  ss << "Problem opening file " << jsonPath << ". Make sure it exists and is readable" << std::endl;
140  throw vpException(vpException::ioError, ss.str());
141  }
142  json j;
143  try {
144  j = json::parse(file);
145  }
146  catch (json::parse_error &e) {
147  std::stringstream msg;
148  msg << "Could not parse JSON file : \n";
149 
150  msg << e.what() << std::endl;
151  msg << "Byte position of error: " << e.byte;
152  throw vpException(vpException::ioError, msg.str());
153  }
154  m_algoParams = j; // Call from_json(const json& j, vpDetectorDNN& *this) to read json
155  file.close();
158 }
159 
160 void
161 vpCircleHoughTransform::saveConfigurationInJSON(const std::string &jsonPath) const
162 {
164 }
165 #endif
166 
167 void
169 {
170  const int filterHalfSize = (m_algoParams.m_gaussianKernelSize + 1) / 2;
171  m_fg.resize(1, filterHalfSize);
172  vpImageFilter::getGaussianKernel(m_fg.data, m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev, true);
173  m_cannyVisp.setGaussianFilterParameters(m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev);
174 }
175 
176 void
178 {
179 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11) // Check if cxx11 or higher
180  // Helper to apply the scale to the raw values of the filters
181  auto scaleFilter = [](vpArray2D<float> &filter, const float &scale) {
182  const unsigned int nbRows = filter.getRows();
183  const unsigned int nbCols = filter.getCols();
184  for (unsigned int r = 0; r < nbRows; ++r) {
185  for (unsigned int c = 0; c < nbCols; ++c) {
186  filter[r][c] = filter[r][c] * scale;
187  }
188  }
189  };
190 #endif
191 
192  if ((m_algoParams.m_gradientFilterKernelSize % 2) != 1) {
193  throw vpException(vpException::badValue, "Gradient filters Kernel size should be odd.");
194  }
195  m_gradientFilterX.resize(m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gradientFilterKernelSize);
196  m_gradientFilterY.resize(m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gradientFilterKernelSize);
197  m_cannyVisp.setGradientFilterAperture(m_algoParams.m_gradientFilterKernelSize);
198 
199  float scaleX = 1.f;
200  float scaleY = 1.f;
201  unsigned int filterHalfSize = (m_algoParams.m_gradientFilterKernelSize - 1) / 2;
202 
203  if (m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING) {
204  // Compute the Sobel filters
205  scaleX = vpImageFilter::getSobelKernelX(m_gradientFilterX.data, filterHalfSize);
206  scaleY = vpImageFilter::getSobelKernelY(m_gradientFilterY.data, filterHalfSize);
207  }
208  else if (m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING) {
209  // Compute the Scharr filters
210  scaleX = vpImageFilter::getScharrKernelX(m_gradientFilterX.data, filterHalfSize);
211  scaleY = vpImageFilter::getScharrKernelY(m_gradientFilterY.data, filterHalfSize);
212  }
213  else {
214  std::string errMsg = "[vpCircleHoughTransform::initGradientFilters] Error: gradient filtering method \"";
215  errMsg += vpImageFilter::vpCannyFilteringAndGradientTypeToString(m_algoParams.m_filteringAndGradientType);
216  errMsg += "\" has not been implemented yet\n";
218  }
219  scaleFilter(m_gradientFilterX, scaleX);
220  scaleFilter(m_gradientFilterY, scaleY);
221 }
222 
223 std::vector<vpImageCircle>
225 {
226  vpImage<unsigned char> I_gray;
227  vpImageConvert::convert(I, I_gray);
228  return detect(I_gray);
229 }
230 
231 #ifdef HAVE_OPENCV_CORE
232 std::vector<vpImageCircle>
233 vpCircleHoughTransform::detect(const cv::Mat &cv_I)
234 {
235  vpImage<unsigned char> I_gray;
236  vpImageConvert::convert(cv_I, I_gray);
237  return detect(I_gray);
238 }
239 #endif
240 
241 std::vector<vpImageCircle>
243 {
244  std::vector<vpImageCircle> detections = detect(I);
245  size_t nbDetections = detections.size();
246 
247  // Prepare vector of tuple to sort by decreasing probabilities
248  std::vector<std::pair<size_t, float> > v_id_proba;
249  for (size_t i = 0; i < nbDetections; ++i) {
250  std::pair<size_t, float> id_proba(i, m_finalCirclesProbabilities[i]);
251  v_id_proba.push_back(id_proba);
252  }
253 
254 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
255  // Sorting by decreasing probabilities
256  auto hasBetterProba
257  = [](std::pair<size_t, float> a, std::pair<size_t, float> b) {
258  return (a.second > b.second);
259  };
260 #endif
261  std::sort(v_id_proba.begin(), v_id_proba.end(), hasBetterProba);
262 
263  // Clearing the storages containing the detection results
264  // to have it sorted by decreasing probabilities
265  size_t limitMin;
266  if (nbCircles < 0) {
267  limitMin = nbDetections;
268  }
269  else {
270  limitMin = std::min(nbDetections, static_cast<size_t>(nbCircles));
271  }
272 
273  std::vector<vpImageCircle> bestCircles;
274  std::vector<vpImageCircle> copyFinalCircles = m_finalCircles;
275  std::vector<unsigned int> copyFinalCirclesVotes = m_finalCircleVotes;
276  std::vector<float> copyFinalCirclesProbas = m_finalCirclesProbabilities;
277  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > copyFinalCirclesVotingPoints = m_finalCirclesVotingPoints;
278  for (size_t i = 0; i < nbDetections; ++i) {
279  size_t id = v_id_proba[i].first;
280  m_finalCircles[i] = copyFinalCircles[id];
281  m_finalCircleVotes[i] = copyFinalCirclesVotes[id];
282  m_finalCirclesProbabilities[i] = copyFinalCirclesProbas[id];
283  if (m_algoParams.m_recordVotingPoints) {
284  m_finalCirclesVotingPoints[i] = copyFinalCirclesVotingPoints[id];
285  }
286  if (i < limitMin) {
287  bestCircles.push_back(m_finalCircles[i]);
288  }
289  }
290 
291  return bestCircles;
292 }
293 
294 std::vector<vpImageCircle>
296 {
297  // Cleaning results of potential previous detection
298  m_centerCandidatesList.clear();
299  m_centerVotes.clear();
300  m_edgePointsList.clear();
301  m_circleCandidates.clear();
302  m_circleCandidatesVotes.clear();
304  m_finalCircles.clear();
305  m_finalCircleVotes.clear();
306 
307  // Ensuring that the difference between the max and min radii is big enough to take into account
308  // the pixelization of the image
309  const float minRadiusDiff = 3.f;
310  if ((m_algoParams.m_maxRadius - m_algoParams.m_minRadius) < minRadiusDiff) {
311  if (m_algoParams.m_minRadius > (minRadiusDiff / 2.f)) {
312  m_algoParams.m_maxRadius += minRadiusDiff / 2.f;
313  m_algoParams.m_minRadius -= minRadiusDiff / 2.f;
314  }
315  else {
316  m_algoParams.m_maxRadius += minRadiusDiff - m_algoParams.m_minRadius;
317  m_algoParams.m_minRadius = 0.f;
318  }
319  }
320 
321  // Ensuring that the difference between the max and min center position is big enough to take into account
322  // the pixelization of the image
323  const float minCenterPositionDiff = 3.f;
324  if ((m_algoParams.m_centerXlimits.second - m_algoParams.m_centerXlimits.first) < minCenterPositionDiff) {
325  m_algoParams.m_centerXlimits.second += static_cast<int>(minCenterPositionDiff / 2.f);
326  m_algoParams.m_centerXlimits.first -= static_cast<int>(minCenterPositionDiff / 2.f);
327  }
328  if ((m_algoParams.m_centerYlimits.second - m_algoParams.m_centerYlimits.first) < minCenterPositionDiff) {
329  m_algoParams.m_centerYlimits.second += static_cast<int>(minCenterPositionDiff / 2.f);
330  m_algoParams.m_centerYlimits.first -= static_cast<int>(minCenterPositionDiff / 2.f);
331  }
332 
333  // First thing, we need to apply a Gaussian filter on the image to remove some spurious noise
334  // Then, we need to compute the image gradients in order to be able to perform edge detection
335  computeGradients(I);
336 
337  // Using the gradients, it is now possible to perform edge detection
338  // We rely on the Canny edge detector
339  // It will also give us the connected edged points
340  edgeDetection(I);
341 
342  // From the edge map and gradient information, it is possible to compute
343  // the center point candidates
345 
346  // From the edge map and center point candidates, we can compute candidate
347  // circles. These candidate circles are circles whose center belong to
348  // the center point candidates and whose radius is a "radius bin" that got
349  // enough votes by computing the distance between each point of the edge map
350  // and the center point candidate
352 
353  // Finally, we perform a merging operation that permits to merge circles
354  // respecting similarity criteria (distance between centers and similar radius)
356 
357  return m_finalCircles;
358 }
359 
360 bool
361 operator==(const vpImageCircle &a, const vpImageCircle &b)
362 {
363  vpImagePoint aCenter = a.getCenter();
364  vpImagePoint bCenter = b.getCenter();
365  bool haveSameCenter = (std::abs(aCenter.get_u() - bCenter.get_u())
366  + std::abs(aCenter.get_v() - bCenter.get_v())) <= (2. * std::numeric_limits<double>::epsilon());
367  bool haveSameRadius = std::abs(a.getRadius() - b.getRadius()) <= (2.f * std::numeric_limits<float>::epsilon());
368  return (haveSameCenter && haveSameRadius);
369 }
370 
371 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
372 void vpCircleHoughTransform::computeVotingMask(const vpImage<unsigned char> &I, const std::vector<vpImageCircle> &detections,
373  std::optional< vpImage<bool> > &mask, std::optional<std::vector<std::vector<std::pair<unsigned int, unsigned int>>>> &opt_votingPoints) const
374 #else
375 void vpCircleHoughTransform::computeVotingMask(const vpImage<unsigned char> &I, const std::vector<vpImageCircle> &detections,
376  vpImage<bool> **mask, std::vector<std::vector<std::pair<unsigned int, unsigned int> > > **opt_votingPoints) const
377 #endif
378 {
379  if (!m_algoParams.m_recordVotingPoints) {
380  // We weren't asked to remember the voting points
381 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
382  mask = std::nullopt;
383  opt_votingPoints = std::nullopt;
384 #else
385  *mask = nullptr;
386  *opt_votingPoints = nullptr;
387 #endif
388  return;
389  }
390 
391 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
392  mask = vpImage<bool>(I.getHeight(), I.getWidth(), false);
393  opt_votingPoints = std::vector<std::vector<std::pair<unsigned int, unsigned int>>>();
394 #else
395  *mask = new vpImage<bool>(I.getHeight(), I.getWidth(), false);
396  *opt_votingPoints = new std::vector<std::vector<std::pair<unsigned int, unsigned int> > >();
397 #endif
398 
399 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
400  for (const auto &detection : detections)
401 #else
402  const size_t nbDetections = detections.size();
403  for (size_t i = 0; i < nbDetections; ++i)
404 #endif
405  {
406  bool hasFoundSimilarCircle = false;
407  unsigned int nbPreviouslyDetected = static_cast<unsigned int>(m_finalCircles.size());
408  unsigned int id = 0;
409  // Looking for a circle that was detected and is similar to the one given to the function
410  while ((id < nbPreviouslyDetected) && (!hasFoundSimilarCircle)) {
411  vpImageCircle previouslyDetected = m_finalCircles[id];
412 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
413  if (previouslyDetected == detection)
414 #else
415  if (previouslyDetected == detections[i])
416 #endif
417  {
418  hasFoundSimilarCircle = true;
419  // We found a circle that is similar to the one given to the function => updating the mask
420  const unsigned int nbVotingPoints = static_cast<unsigned int>(m_finalCirclesVotingPoints[id].size());
421  for (unsigned int idPoint = 0; idPoint < nbVotingPoints; ++idPoint) {
422  const std::pair<unsigned int, unsigned int> &votingPoint = m_finalCirclesVotingPoints[id][idPoint];
423 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
424  (*mask)[votingPoint.first][votingPoint.second] = true;
425 #else
426  (**mask)[votingPoint.first][votingPoint.second] = true;
427 #endif
428  }
429 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
430  opt_votingPoints->push_back(m_finalCirclesVotingPoints[id]);
431 #else
432  (**opt_votingPoints).push_back(m_finalCirclesVotingPoints[id]);
433 #endif
434  }
435  ++id;
436  }
437  }
438 }
439 
440 void
442 {
443  if ((m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING)
444  || (m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING)) {
445  // Computing the Gaussian blurr
446  vpImage<float> Iblur, GIx;
447  vpImageFilter::filterX(I, GIx, m_fg.data, m_algoParams.m_gaussianKernelSize, mp_mask);
448  vpImageFilter::filterY(GIx, Iblur, m_fg.data, m_algoParams.m_gaussianKernelSize, mp_mask);
449 
450  // Computing the gradients
453  }
454  else {
455  std::string errMsg("[computeGradients] The filtering + gradient operators \"");
456  errMsg += vpImageFilter::vpCannyFilteringAndGradientTypeToString(m_algoParams.m_filteringAndGradientType);
457  errMsg += "\" is not implemented (yet).";
459  }
460 }
461 
462 void
464 {
465  if (m_algoParams.m_cannyBackendType == vpImageFilter::CANNY_VISP_BACKEND) {
466  // This is done to increase the time performances, because it avoids to
467  // recompute the gradient in the vpImageFilter::canny method
468  m_cannyVisp.setFilteringAndGradientType(m_algoParams.m_filteringAndGradientType);
469  m_cannyVisp.setCannyThresholds(m_algoParams.m_lowerCannyThresh, m_algoParams.m_upperCannyThresh);
470  m_cannyVisp.setCannyThresholdsRatio(m_algoParams.m_lowerCannyThreshRatio, m_algoParams.m_upperCannyThreshRatio);
474  }
475  else {
476  if (mp_mask != nullptr) {
477  // Delete pixels that fall outside the mask
478  vpImage<unsigned char> I_masked(I);
479  unsigned int nbRows = I_masked.getHeight();
480  unsigned int nbCols = I_masked.getWidth();
481  for (unsigned int r = 0; r < nbRows; ++r) {
482  for (unsigned int c = 0; c < nbCols; ++c) {
483  if (!((*mp_mask)[r][c])) {
484  I_masked[r][c] = 0;
485  }
486  }
487  }
488 
489  // We will have to recompute the gradient in the desired backend format anyway so we let
490  // the vpImageFilter::canny method take care of it
491  vpImageFilter::canny(I_masked, m_edgeMap, m_algoParams.m_gaussianKernelSize, m_algoParams.m_lowerCannyThresh,
492  m_algoParams.m_upperCannyThresh, m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gaussianStdev,
493  m_algoParams.m_lowerCannyThreshRatio, m_algoParams.m_upperCannyThreshRatio, true,
494  m_algoParams.m_cannyBackendType, m_algoParams.m_filteringAndGradientType);
495  }
496  else {
497  vpImageFilter::canny(I, m_edgeMap, m_algoParams.m_gaussianKernelSize, m_algoParams.m_lowerCannyThresh,
498  m_algoParams.m_upperCannyThresh, m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gaussianStdev,
499  m_algoParams.m_lowerCannyThreshRatio, m_algoParams.m_upperCannyThreshRatio, true,
500  m_algoParams.m_cannyBackendType, m_algoParams.m_filteringAndGradientType);
501  }
502  }
503 
504  for (int i = 0; i < m_algoParams.m_edgeMapFilteringNbIter; ++i) {
505  filterEdgeMap();
506  }
507 }
508 
509 void
511 {
513  const unsigned int height = J.getHeight();
514  const unsigned int width = J.getWidth();
515  const int minNbContiguousPts = 2;
516 
517  for (unsigned int i = 1; i < (height - 1); ++i) {
518  for (unsigned int j = 1; j < (width - 1); ++j) {
519  if (J[i][j] == vpCircleHoughTransform::edgeMapOn) {
520  // Consider 8 neighbors
521  int topLeftPixel = static_cast<int>(J[i - 1][j - 1]);
522  int topPixel = static_cast<int>(J[i - 1][j]);
523  int topRightPixel = static_cast<int>(J[i - 1][j + 1]);
524  int botLeftPixel = static_cast<int>(J[i + 1][j - 1]);
525  int bottomPixel = static_cast<int>(J[i + 1][j]);
526  int botRightPixel = static_cast<int>(J[i + 1][j + 1]);
527  int leftPixel = static_cast<int>(J[i][j - 1]);
528  int rightPixel = static_cast<int>(J[i][j + 1]);
529  if ((topLeftPixel + topPixel + topRightPixel
530  + botLeftPixel + bottomPixel + botRightPixel
531  + leftPixel + rightPixel
532  ) >= (minNbContiguousPts * static_cast<int>(vpCircleHoughTransform::edgeMapOn))) {
533  // At least minNbContiguousPts of the 8-neighbor points are also an edge point
534  // so we keep the edge point
536  }
537  else {
538  // The edge point is isolated => we erase it
540  }
541  }
542  }
543  }
544 }
545 
546 void
548 {
549  // For each edge point EP_i, check the image gradient at EP_i
550  // Then, for each image point in the direction of the gradient,
551  // increment the accumulator
552  // We can perform bilinear interpolation in order not to vote for a "line" of
553  // points, but for an "area" of points
554  unsigned int nbRows = m_edgeMap.getRows();
555  unsigned int nbCols = m_edgeMap.getCols();
556 
557  // Computing the minimum and maximum horizontal position of the center candidates
558  // The miminum horizontal position of the center is at worst -maxRadius outside the image
559  // The maxinum horizontal position of the center is at worst +maxRadiusoutside the image
560  // The width of the accumulator is the difference between the max and the min
561  int minimumXposition = std::max<int>(m_algoParams.m_centerXlimits.first, -1 * static_cast<int>(m_algoParams.m_maxRadius));
562  int maximumXposition = std::min<int>(m_algoParams.m_centerXlimits.second, static_cast<int>(m_algoParams.m_maxRadius + nbCols));
563  minimumXposition = std::min<int>(minimumXposition, maximumXposition - 1);
564  float minimumXpositionFloat = static_cast<float>(minimumXposition);
565  float maximumXpositionFloat = static_cast<float>(maximumXposition);
566  int offsetX = minimumXposition;
567  int accumulatorWidth = (maximumXposition - minimumXposition) + 1;
568  if (accumulatorWidth <= 0) {
569  throw(vpException(vpException::dimensionError, "[vpCircleHoughTransform::computeCenterCandidates] Accumulator width <= 0!"));
570  }
571 
572  // Computing the minimum and maximum vertical position of the center candidates
573  // The miminum vertical position of the center is at worst -maxRadius outside the image
574  // The maxinum vertical position of the center is at worst +maxRadiusoutside the image
575  // The height of the accumulator is the difference between the max and the min
576  int minimumYposition = std::max<int>(m_algoParams.m_centerYlimits.first, -1 * static_cast<int>(m_algoParams.m_maxRadius));
577  int maximumYposition = std::min<int>(m_algoParams.m_centerYlimits.second, static_cast<int>(m_algoParams.m_maxRadius + nbRows));
578  minimumYposition = std::min<int>(minimumYposition, maximumYposition - 1);
579  float minimumYpositionFloat = static_cast<float>(minimumYposition);
580  float maximumYpositionFloat = static_cast<float>(maximumYposition);
581  int offsetY = minimumYposition;
582  int accumulatorHeight = (maximumYposition - minimumYposition) + 1;
583  if (accumulatorHeight <= 0) {
584  std::string errMsg("[vpCircleHoughTransform::computeCenterCandidates] Accumulator height <= 0!");
586  }
587 
588  vpImage<float> centersAccum(accumulatorHeight, accumulatorWidth + 1, 0.);
590  const int nbDirections = 2;
591  for (unsigned int r = 0; r < nbRows; ++r) {
592  for (unsigned int c = 0; c < nbCols; ++c) {
594  // Voting for points in both direction of the gradient
595  // Step from min_radius to max_radius in both directions of the gradient
596  float mag = std::sqrt((m_dIx[r][c] * m_dIx[r][c]) + (m_dIy[r][c] * m_dIy[r][c]));
597 
598  float sx = 0.f, sy = 0.f;
599  if (std::abs(mag) >= std::numeric_limits<float>::epsilon()) {
600  sx = m_dIx[r][c] / mag;
601  sy = m_dIy[r][c] / mag;
602  }
603  else {
604  continue;
605  }
606 
607  // Saving the edge point for further use
608  m_edgePointsList.push_back(std::pair<unsigned int, unsigned int>(r, c));
609 
610  for (int k1 = 0; k1 < nbDirections; ++k1) {
611  bool hasToStopLoop = false;
612  int x_low_prev = std::numeric_limits<int>::max(), y_low_prev, y_high_prev;
613  int x_high_prev = (y_low_prev = (y_high_prev = x_low_prev));
614 
615  float rstart = m_algoParams.m_minRadius, rstop = m_algoParams.m_maxRadius;
616  float min_minus_c = minimumXpositionFloat - static_cast<float>(c);
617  float min_minus_r = minimumYpositionFloat - static_cast<float>(r);
618  float max_minus_c = maximumXpositionFloat - static_cast<float>(c);
619  float max_minus_r = maximumYpositionFloat - static_cast<float>(r);
620  if (sx > 0) {
621  float rmin = min_minus_c / sx;
622  rstart = std::max<float>(rmin, m_algoParams.m_minRadius);
623  float rmax = max_minus_c / sx;
624  rstop = std::min<float>(rmax, m_algoParams.m_maxRadius);
625  }
626  else if (sx < 0) {
627  float rmin = max_minus_c / sx;
628  rstart = std::max<float>(rmin, m_algoParams.m_minRadius);
629  float rmax = min_minus_c / sx;
630  rstop = std::min<float>(rmax, m_algoParams.m_maxRadius);
631  }
632 
633  if (sy > 0) {
634  float rmin = min_minus_r / sy;
635  rstart = std::max<float>(rmin, rstart);
636  float rmax = max_minus_r / sy;
637  rstop = std::min<float>(rmax, rstop);
638  }
639  else if (sy < 0) {
640  float rmin = max_minus_r / sy;
641  rstart = std::max<float>(rmin, rstart);
642  float rmax = min_minus_r / sy;
643  rstop = std::min<float>(rmax, rstop);
644  }
645 
646  float deltar_x = 1.f / std::abs(sx);
647  float deltar_y = 1.f / std::abs(sy);
648  float deltar = std::min<float>(deltar_x, deltar_y);
649 
650  float rad = rstart;
651  while ((rad <= rstop) && (!hasToStopLoop)) {
652  float x1 = static_cast<float>(c) + (rad * sx);
653  float y1 = static_cast<float>(r) + (rad * sy);
654  rad += deltar; // Update rad that is not used below not to forget it
655 
656  if ((x1 < minimumXpositionFloat) || (y1 < minimumYpositionFloat)
657  || (x1 > maximumXpositionFloat) || (y1 > maximumYpositionFloat)) {
658  continue; // It means that the center is outside the search region.
659  }
660 
661  int x_low, x_high;
662  int y_low, y_high;
663 
664  if (x1 > 0.) {
665  x_low = static_cast<int>(std::floor(x1));
666  x_high = static_cast<int>(std::ceil(x1));
667  }
668  else {
669  x_low = -(static_cast<int>(std::ceil(-x1)));
670  x_high = -(static_cast<int>(std::floor(-x1)));
671  }
672 
673  if (y1 > 0.) {
674  y_low = static_cast<int>(std::floor(y1));
675  y_high = static_cast<int>(std::ceil(y1));
676  }
677  else {
678  y_low = -(static_cast<int>(std::ceil(-1. * y1)));
679  y_high = -(static_cast<int>(std::floor(-1. * y1)));
680  }
681 
682  if ((x_low_prev == x_low) && (x_high_prev == x_high)
683  && (y_low_prev == y_low) && (y_high_prev == y_high)) {
684  // Avoid duplicated votes to the same center candidate
685  continue;
686  }
687  else {
688  x_low_prev = x_low;
689  x_high_prev = x_high;
690  y_low_prev = y_low;
691  y_high_prev = y_high;
692  }
693 
694 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
695  auto updateAccumulator =
696  [](const float &x_orig, const float &y_orig,
697  const int &x, const int &y,
698  const int &offsetX, const int &offsetY,
699  const int &nbCols, const int &nbRows,
700  vpImage<float> &accum, bool &hasToStop) {
701  if (((x - offsetX) < 0) ||
702  ((x - offsetX) >= nbCols) ||
703  ((y - offsetY) < 0) ||
704  ((y - offsetY) >= nbRows)
705  ) {
706  hasToStop = true;
707  }
708  else {
709  float dx = (x_orig - static_cast<float>(x));
710  float dy = (y_orig - static_cast<float>(y));
711  accum[y - offsetY][x - offsetX] += std::abs(dx) + std::abs(dy);
712  }
713  };
714 #endif
715 
716  updateAccumulator(x1, y1, x_low, y_low,
717  offsetX, offsetY,
718  accumulatorWidth, accumulatorHeight,
719  centersAccum, hasToStopLoop
720  );
721 
722  updateAccumulator(x1, y1, x_high, y_high,
723  offsetX, offsetY,
724  accumulatorWidth, accumulatorHeight,
725  centersAccum, hasToStopLoop
726  );
727  }
728 
729  sx = -sx;
730  sy = -sy;
731  }
732  }
733  }
734  }
735 
736  // Use dilatation with large kernel in order to determine the
737  // accumulator maxima
738  vpImage<float> centerCandidatesMaxima = centersAccum;
739  int dilatationKernelSize = std::max<int>(m_algoParams.m_dilatationKernelSize, 3); // Ensure at least a 3x3 dilatation operation is performed
740  vpImageMorphology::dilatation(centerCandidatesMaxima, dilatationKernelSize);
741 
742  // Look for the image points that correspond to the accumulator maxima
743  // These points will become the center candidates
744  // find the possible circle centers
745  int nbColsAccum = centersAccum.getCols();
746  int nbRowsAccum = centersAccum.getRows();
747  int nbVotes = -1;
748  std::vector<std::pair<std::pair<float, float>, float> > peak_positions_votes;
749 
750  for (int y = 0; y < nbRowsAccum; ++y) {
751  int left = -1;
752  for (int x = 0; x < nbColsAccum; ++x) {
753  if ((centersAccum[y][x] >= m_algoParams.m_centerMinThresh)
754  && (vpMath::equal(centersAccum[y][x], centerCandidatesMaxima[y][x]))
755  && (centersAccum[y][x] > centersAccum[y][x + 1])
756  ) {
757  if (left < 0) {
758  left = x;
759  }
760  nbVotes = std::max<int>(nbVotes, static_cast<int>(centersAccum[y][x]));
761  }
762  else if (left >= 0) {
763  int cx = static_cast<int>(((left + x) - 1) * 0.5f);
764  float sumVotes = 0.;
765  float x_avg = 0., y_avg = 0.;
766  int averagingWindowHalfSize = m_algoParams.m_averagingWindowSize / 2;
767  int startingRow = std::max<int>(0, y - averagingWindowHalfSize);
768  int startingCol = std::max<int>(0, cx - averagingWindowHalfSize);
769  int endRow = std::min<int>(accumulatorHeight, y + averagingWindowHalfSize + 1);
770  int endCol = std::min<int>(accumulatorWidth, cx + averagingWindowHalfSize + 1);
771  for (int r = startingRow; r < endRow; ++r) {
772  for (int c = startingCol; c < endCol; ++c) {
773  sumVotes += centersAccum[r][c];
774  x_avg += centersAccum[r][c] * c;
775  y_avg += centersAccum[r][c] * r;
776  }
777  }
778  float avgVotes = sumVotes / static_cast<float>(m_algoParams.m_averagingWindowSize * m_algoParams.m_averagingWindowSize);
779  if (avgVotes > m_algoParams.m_centerMinThresh) {
780  x_avg /= static_cast<float>(sumVotes);
781  y_avg /= static_cast<float>(sumVotes);
782  std::pair<float, float> position(y_avg + static_cast<float>(offsetY), x_avg + static_cast<float>(offsetX));
783  std::pair<std::pair<float, float>, float> position_vote(position, avgVotes);
784  peak_positions_votes.push_back(position_vote);
785  }
786  if (nbVotes < 0) {
787  std::stringstream errMsg;
788  errMsg << "nbVotes (" << nbVotes << ") < 0, thresh = " << m_algoParams.m_centerMinThresh;
789  throw(vpException(vpException::badValue, errMsg.str()));
790  }
791  left = -1;
792  nbVotes = -1;
793  }
794  }
795  }
796 
797  unsigned int nbPeaks = static_cast<unsigned int>(peak_positions_votes.size());
798  if (nbPeaks > 0) {
799  std::vector<bool> has_been_merged(nbPeaks, false);
800  std::vector<std::pair<std::pair<float, float>, float> > merged_peaks_position_votes;
801  float squared_distance_max = m_algoParams.m_centerMinDist * m_algoParams.m_centerMinDist;
802  for (unsigned int idPeak = 0; idPeak < nbPeaks; ++idPeak) {
803  float votes = peak_positions_votes[idPeak].second;
804  if (has_been_merged[idPeak]) {
805  // Ignoring peak that has already been merged
806  continue;
807  }
808  else if (votes < m_algoParams.m_centerMinThresh) {
809  // Ignoring peak whose number of votes is lower than the threshold
810  has_been_merged[idPeak] = true;
811  continue;
812  }
813  std::pair<float, float> position = peak_positions_votes[idPeak].first;
814  std::pair<float, float> barycenter;
815  barycenter.first = position.first * peak_positions_votes[idPeak].second;
816  barycenter.second = position.second * peak_positions_votes[idPeak].second;
817  float total_votes = peak_positions_votes[idPeak].second;
818  float nb_electors = 1.f;
819  // Looking for potential similar peak in the following peaks
820  for (unsigned int idCandidate = idPeak + 1; idCandidate < nbPeaks; ++idCandidate) {
821  float votes_candidate = peak_positions_votes[idCandidate].second;
822  if (has_been_merged[idCandidate]) {
823  continue;
824  }
825  else if (votes_candidate < m_algoParams.m_centerMinThresh) {
826  // Ignoring peak whose number of votes is lower than the threshold
827  has_been_merged[idCandidate] = true;
828  continue;
829  }
830  // Computing the distance with the peak of insterest
831  std::pair<float, float> position_candidate = peak_positions_votes[idCandidate].first;
832  float squared_distance = ((position.first - position_candidate.first) * (position.first - position_candidate.first))
833  + ((position.second - position_candidate.second) * (position.second - position_candidate.second));
834 
835  // If the peaks are similar, update the barycenter peak between them and corresponding votes
836  if (squared_distance < squared_distance_max) {
837  barycenter.first += position_candidate.first * votes_candidate;
838  barycenter.second += position_candidate.second * votes_candidate;
839  total_votes += votes_candidate;
840  nb_electors += 1.f;
841  has_been_merged[idCandidate] = true;
842  }
843  }
844 
845  float avg_votes = total_votes / nb_electors;
846  // Only the centers having enough votes are considered
847  if (avg_votes > m_algoParams.m_centerMinThresh) {
848  barycenter.first /= total_votes;
849  barycenter.second /= total_votes;
850  std::pair<std::pair<float, float>, float> barycenter_votes(barycenter, avg_votes);
851  merged_peaks_position_votes.push_back(barycenter_votes);
852  }
853  }
854 
855 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
856  auto sortingCenters = [](const std::pair<std::pair<float, float>, float> &position_vote_a,
857  const std::pair<std::pair<float, float>, float> &position_vote_b) {
858  return position_vote_a.second > position_vote_b.second;
859  };
860 #endif
861 
862  std::sort(merged_peaks_position_votes.begin(), merged_peaks_position_votes.end(), sortingCenters);
863 
864  nbPeaks = static_cast<unsigned int>(merged_peaks_position_votes.size());
865  int nbPeaksToKeep = (m_algoParams.m_expectedNbCenters > 0 ? m_algoParams.m_expectedNbCenters : static_cast<int>(nbPeaks));
866  nbPeaksToKeep = std::min<int>(nbPeaksToKeep, static_cast<int>(nbPeaks));
867  for (int i = 0; i < nbPeaksToKeep; ++i) {
868  m_centerCandidatesList.push_back(merged_peaks_position_votes[i].first);
869  m_centerVotes.push_back(static_cast<int>(merged_peaks_position_votes[i].second));
870  }
871  }
872 }
873 
874 void
876 {
877  size_t nbCenterCandidates = m_centerCandidatesList.size();
878  int nbBins = static_cast<int>(((m_algoParams.m_maxRadius - m_algoParams.m_minRadius) + 1) / m_algoParams.m_mergingRadiusDiffThresh);
879  nbBins = std::max<int>(static_cast<int>(1), nbBins); // Avoid having 0 bins, which causes segfault
880  std::vector<float> radiusAccumList; // Radius accumulator for each center candidates.
881  std::vector<float> radiusActualValueList; // Vector that contains the actual distance between the edge points and the center candidates.
882  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > votingPoints(nbBins); // Vectors that contain the points voting for each radius bin
883 
884  float rmin2 = m_algoParams.m_minRadius * m_algoParams.m_minRadius;
885  float rmax2 = m_algoParams.m_maxRadius * m_algoParams.m_maxRadius;
886  float circlePerfectness2 = m_algoParams.m_circlePerfectness * m_algoParams.m_circlePerfectness;
887 
888  for (size_t i = 0; i < nbCenterCandidates; ++i) {
889  std::pair<float, float> centerCandidate = m_centerCandidatesList[i];
890  // Initialize the radius accumulator of the candidate with 0s
891  radiusAccumList.clear();
892  radiusAccumList.resize(nbBins, 0);
893  radiusActualValueList.clear();
894  radiusActualValueList.resize(nbBins, 0.);
895 
896 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
897  for (auto edgePoint : m_edgePointsList)
898 #else
899  for (size_t e = 0; e < m_edgePointsList.size(); ++e)
900 #endif
901  {
902  // For each center candidate CeC_i, compute the distance with each edge point EP_j d_ij = dist(CeC_i; EP_j)
903 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
904  float rx = edgePoint.second - centerCandidate.second;
905  float ry = edgePoint.first - centerCandidate.first;
906 #else
907  float rx = m_edgePointsList[e].second - centerCandidate.second;
908  float ry = m_edgePointsList[e].first - centerCandidate.first;
909 #endif
910  float r2 = (rx * rx) + (ry * ry);
911  if ((r2 > rmin2) && (r2 < rmax2)) {
912 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
913  float gx = m_dIx[edgePoint.first][edgePoint.second];
914  float gy = m_dIy[edgePoint.first][edgePoint.second];
915 #else
916  float gx = m_dIx[m_edgePointsList[e].first][m_edgePointsList[e].second];
917  float gy = m_dIy[m_edgePointsList[e].first][m_edgePointsList[e].second];
918 #endif
919  float grad2 = (gx * gx) + (gy * gy);
920 
921  float scalProd = (rx * gx) + (ry * gy);
922  float scalProd2 = scalProd * scalProd;
923  if (scalProd2 >= (circlePerfectness2 * r2 * grad2)) {
924  // Look for the Radius Candidate Bin RCB_k to which d_ij is "the closest" will have an additionnal vote
925  float r = static_cast<float>(std::sqrt(r2));
926  int r_bin = static_cast<int>(std::floor((r - m_algoParams.m_minRadius) / m_algoParams.m_mergingRadiusDiffThresh));
927  r_bin = std::min<int>(r_bin, nbBins - 1);
928  if ((r < (m_algoParams.m_minRadius + (m_algoParams.m_mergingRadiusDiffThresh * 0.5f)))
929  || (r >(m_algoParams.m_minRadius + (m_algoParams.m_mergingRadiusDiffThresh * (static_cast<float>(nbBins - 1) + 0.5f))))) {
930  // If the radius is at the very beginning of the allowed radii or at the very end, we do not span the vote
931  radiusAccumList[r_bin] += 1.f;
932  radiusActualValueList[r_bin] += r;
933  if (m_algoParams.m_recordVotingPoints) {
934 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
935  votingPoints[r_bin].push_back(edgePoint);
936 #else
937  votingPoints[r_bin].push_back(m_edgePointsList[e]);
938 #endif
939  }
940  }
941  else {
942  float midRadiusPrevBin = m_algoParams.m_minRadius + (m_algoParams.m_mergingRadiusDiffThresh * ((r_bin - 1.f) + 0.5f));
943  float midRadiusCurBin = m_algoParams.m_minRadius + (m_algoParams.m_mergingRadiusDiffThresh * (r_bin + 0.5f));
944  float midRadiusNextBin = m_algoParams.m_minRadius + (m_algoParams.m_mergingRadiusDiffThresh * (r_bin + 1.f + 0.5f));
945 
946  if ((r >= midRadiusCurBin) && (r <= midRadiusNextBin)) {
947  // The radius is at the end of the current bin or beginning of the next, we span the vote with the next bin
948  float voteCurBin = (midRadiusNextBin - r) / m_algoParams.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the current bin
949  float voteNextBin = 1.f - voteCurBin;
950  radiusAccumList[r_bin] += voteCurBin;
951  radiusActualValueList[r_bin] += r * voteCurBin;
952  radiusAccumList[r_bin + 1] += voteNextBin;
953  radiusActualValueList[r_bin + 1] += r * voteNextBin;
954  if (m_algoParams.m_recordVotingPoints) {
955 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
956  votingPoints[r_bin].push_back(edgePoint);
957  votingPoints[r_bin + 1].push_back(edgePoint);
958 #else
959  votingPoints[r_bin].push_back(m_edgePointsList[e]);
960  votingPoints[r_bin + 1].push_back(m_edgePointsList[e]);
961 #endif
962  }
963  }
964  else {
965  // The radius is at the end of the previous bin or beginning of the current, we span the vote with the previous bin
966  float votePrevBin = (r - midRadiusPrevBin) / m_algoParams.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the previous bin
967  float voteCurBin = 1.f - votePrevBin;
968  radiusAccumList[r_bin] += voteCurBin;
969  radiusActualValueList[r_bin] += r * voteCurBin;
970  radiusAccumList[r_bin - 1] += votePrevBin;
971  radiusActualValueList[r_bin - 1] += r * votePrevBin;
972  if (m_algoParams.m_recordVotingPoints) {
973 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
974  votingPoints[r_bin].push_back(edgePoint);
975  votingPoints[r_bin - 1].push_back(edgePoint);
976 #else
977  votingPoints[r_bin].push_back(m_edgePointsList[e]);
978  votingPoints[r_bin - 1].push_back(m_edgePointsList[e]);
979 #endif
980  }
981  }
982  }
983  }
984  }
985  }
986 
987 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
988  // Lambda to compute the effective radius (i.e. barycenter) of each radius bin
989  auto computeEffectiveRadius = [](const float &votes, const float &weigthedSumRadius) {
990  float r_effective = -1.f;
991  if (votes > std::numeric_limits<float>::epsilon()) {
992  r_effective = weigthedSumRadius / votes;
993  }
994  return r_effective;
995  };
996 #endif
997 
998  // Merging similar candidates
999  std::vector<float> v_r_effective; // Vector of radius of each candidate after the merge step
1000  std::vector<float> v_votes_effective; // Vector of number of votes of each candidate after the merge step
1001  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > v_votingPoints_effective; // Vector of voting points after the merge step
1002  std::vector<bool> v_hasMerged_effective; // Vector indicating if merge has been performed for the different candidates
1003  for (int idBin = 0; idBin < nbBins; ++idBin) {
1004  float r_effective = computeEffectiveRadius(radiusAccumList[idBin], radiusActualValueList[idBin]);
1005  float votes_effective = radiusAccumList[idBin];
1006  std::vector<std::pair<unsigned int, unsigned int> > votingPoints_effective = votingPoints[idBin];
1007  bool is_r_effective_similar = (r_effective > 0.f);
1008  // Looking for potential similar radii in the following bins
1009  // If so, compute the barycenter radius between them
1010  int idCandidate = idBin + 1;
1011  bool hasMerged = false;
1012  while ((idCandidate < nbBins) && is_r_effective_similar) {
1013  float r_effective_candidate = computeEffectiveRadius(radiusAccumList[idCandidate], radiusActualValueList[idCandidate]);
1014  if (std::abs(r_effective_candidate - r_effective) < m_algoParams.m_mergingRadiusDiffThresh) {
1015  r_effective = ((r_effective * votes_effective) + (r_effective_candidate * radiusAccumList[idCandidate])) / (votes_effective + radiusAccumList[idCandidate]);
1016  votes_effective += radiusAccumList[idCandidate];
1017  radiusAccumList[idCandidate] = -.1f;
1018  radiusActualValueList[idCandidate] = -1.f;
1019  is_r_effective_similar = true;
1020  if (m_algoParams.m_recordVotingPoints) {
1021  // Move elements from votingPoints[idCandidate] to votingPoints_effective.
1022  // votingPoints[idCandidate] is left in undefined but safe-to-destruct state.
1023 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
1024  votingPoints_effective.insert(
1025  votingPoints_effective.end(),
1026  std::make_move_iterator(votingPoints[idCandidate].begin()),
1027  std::make_move_iterator(votingPoints[idCandidate].end())
1028  );
1029 #else
1030  votingPoints_effective.insert(
1031  votingPoints_effective.end(),
1032  votingPoints[idCandidate].begin(),
1033  votingPoints[idCandidate].end()
1034  );
1035 #endif
1036  hasMerged = true;
1037  }
1038  }
1039  else {
1040  is_r_effective_similar = false;
1041  }
1042  ++idCandidate;
1043  }
1044 
1045  if ((votes_effective > m_algoParams.m_centerMinThresh) && (votes_effective >= (m_algoParams.m_circleVisibilityRatioThresh * 2.f * M_PIf * r_effective))) {
1046  // Only the circles having enough votes and being visible enough are considered
1047  v_r_effective.push_back(r_effective);
1048  v_votes_effective.push_back(votes_effective);
1049  if (m_algoParams.m_recordVotingPoints) {
1050  v_votingPoints_effective.push_back(votingPoints_effective);
1051  v_hasMerged_effective.push_back(hasMerged);
1052  }
1053  }
1054  }
1055 
1056  unsigned int nbCandidates = static_cast<unsigned int>(v_r_effective.size());
1057  for (unsigned int idBin = 0; idBin < nbCandidates; ++idBin) {
1058  // If the circle of center CeC_i and radius RCB_k has enough votes, it is added to the list
1059  // of Circle Candidates
1060  float r_effective = v_r_effective[idBin];
1061  vpImageCircle candidateCircle(vpImagePoint(centerCandidate.first, centerCandidate.second), r_effective);
1062  float proba = computeCircleProbability(candidateCircle, static_cast<unsigned int>(v_votes_effective[idBin]));
1063  if (proba > m_algoParams.m_circleProbaThresh) {
1064  m_circleCandidates.push_back(candidateCircle);
1065  m_circleCandidatesProbabilities.push_back(proba);
1066  m_circleCandidatesVotes.push_back(static_cast<unsigned int>(v_votes_effective[idBin]));
1067  if (m_algoParams.m_recordVotingPoints) {
1068  if (v_hasMerged_effective[idBin]) {
1069  // Remove potential duplicated points
1070  std::sort(v_votingPoints_effective[idBin].begin(), v_votingPoints_effective[idBin].end());
1071  v_votingPoints_effective[idBin].erase(std::unique(v_votingPoints_effective[idBin].begin(), v_votingPoints_effective[idBin].end()), v_votingPoints_effective[idBin].end());
1072  }
1073  // Save the points
1074  m_circleCandidatesVotingPoints.push_back(v_votingPoints_effective[idBin]);
1075  }
1076  }
1077  }
1078  }
1079 }
1080 
1081 float
1082 vpCircleHoughTransform::computeCircleProbability(const vpImageCircle &circle, const unsigned int &nbVotes)
1083 {
1084  float proba(0.f);
1085  float visibleArc(static_cast<float>(nbVotes));
1086  float theoreticalLenght;
1087  if (mp_mask != nullptr) {
1088  theoreticalLenght = static_cast<float>(circle.computePixelsInMask(*mp_mask));
1089  }
1090  else {
1091  theoreticalLenght = circle.computeArcLengthInRoI(vpRect(vpImagePoint(0, 0), m_edgeMap.getWidth(), m_edgeMap.getHeight()));
1092  }
1093  if (theoreticalLenght < std::numeric_limits<float>::epsilon()) {
1094  proba = 0.f;
1095  }
1096  else {
1097  proba = std::min(visibleArc / theoreticalLenght, 1.f);
1098  }
1099  return proba;
1100 }
1101 
1102 void
1104 {
1105  std::vector<vpImageCircle> circleCandidates = m_circleCandidates;
1106  std::vector<unsigned int> circleCandidatesVotes = m_circleCandidatesVotes;
1107  std::vector<float> circleCandidatesProba = m_circleCandidatesProbabilities;
1108  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > circleCandidatesVotingPoints = m_circleCandidatesVotingPoints;
1109  // First iteration of merge
1110  mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba, circleCandidatesVotingPoints);
1111 
1112  // Second iteration of merge
1113  mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba, circleCandidatesVotingPoints);
1114 
1115  // Saving the results
1116  m_finalCircles = circleCandidates;
1117  m_finalCircleVotes = circleCandidatesVotes;
1118  m_finalCirclesProbabilities = circleCandidatesProba;
1119  m_finalCirclesVotingPoints = circleCandidatesVotingPoints;
1120 }
1121 
1122 void
1123 vpCircleHoughTransform::mergeCandidates(std::vector<vpImageCircle> &circleCandidates, std::vector<unsigned int> &circleCandidatesVotes,
1124  std::vector<float> &circleCandidatesProba, std::vector<std::vector<std::pair<unsigned int, unsigned int> > > &votingPoints)
1125 {
1126  size_t nbCandidates = circleCandidates.size();
1127  size_t i = 0;
1128  while (i < nbCandidates) {
1129  vpImageCircle cic_i = circleCandidates[i];
1130  bool hasPerformedMerge = false;
1131  // // For each other circle candidate CiC_j do:
1132  size_t j = i + 1;
1133  while (j < nbCandidates) {
1134  vpImageCircle cic_j = circleCandidates[j];
1135  // // // Compute the similarity between CiC_i and CiC_j
1136  double distanceBetweenCenters = vpImagePoint::distance(cic_i.getCenter(), cic_j.getCenter());
1137  double radiusDifference = std::abs(cic_i.getRadius() - cic_j.getRadius());
1138  bool areCirclesSimilar = ((distanceBetweenCenters < m_algoParams.m_centerMinDist)
1139  && (radiusDifference < m_algoParams.m_mergingRadiusDiffThresh)
1140  );
1141 
1142  if (areCirclesSimilar) {
1143  hasPerformedMerge = true;
1144  // // // If the similarity exceeds a threshold, merge the circle candidates CiC_i and CiC_j and remove CiC_j of the list
1145  unsigned int totalVotes = circleCandidatesVotes[i] + circleCandidatesVotes[j];
1146  float totalProba = circleCandidatesProba[i] + circleCandidatesProba[j];
1147  float newProba = 0.5f * totalProba;
1148  float newRadius = ((cic_i.getRadius() * circleCandidatesProba[i]) + (cic_j.getRadius() * circleCandidatesProba[j])) / totalProba;
1149  vpImagePoint newCenter = ((cic_i.getCenter() * circleCandidatesProba[i]) + (cic_j.getCenter() * circleCandidatesProba[j])) / totalProba;
1150  cic_i = vpImageCircle(newCenter, newRadius);
1151  circleCandidates[j] = circleCandidates[nbCandidates - 1];
1152  circleCandidatesVotes[i] = totalVotes / 2; // Compute the mean vote
1153  circleCandidatesVotes[j] = circleCandidatesVotes[nbCandidates - 1];
1154  circleCandidatesProba[i] = newProba;
1155  circleCandidatesProba[j] = circleCandidatesProba[nbCandidates - 1];
1156  if (m_algoParams.m_recordVotingPoints) {
1157 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
1158  votingPoints[i].insert(
1159  votingPoints[i].end(),
1160  std::make_move_iterator(votingPoints[j].begin()),
1161  std::make_move_iterator(votingPoints[j].end())
1162  );
1163 #else
1164  votingPoints[i].insert(
1165  votingPoints[i].end(),
1166  votingPoints[j].begin(),
1167  votingPoints[j].end()
1168  );
1169 #endif
1170  votingPoints.pop_back();
1171  }
1172  circleCandidates.pop_back();
1173  circleCandidatesVotes.pop_back();
1174  circleCandidatesProba.pop_back();
1175  --nbCandidates;
1176  // We do not update j because the new j-th candidate has not been evaluated yet
1177  }
1178  else {
1179  // We will evaluate the next candidate
1180  ++j;
1181  }
1182  }
1183  // // Add the circle candidate CiC_i, potentially merged with other circle candidates, to the final list of detected circles
1184  circleCandidates[i] = cic_i;
1185  if (hasPerformedMerge && m_algoParams.m_recordVotingPoints) {
1186  // Remove duplicated points
1187  std::sort(votingPoints[i].begin(), votingPoints[i].end());
1188  votingPoints[i].erase(std::unique(votingPoints[i].begin(), votingPoints[i].end()), votingPoints[i].end());
1189  }
1190  ++i;
1191  }
1192 }
1193 
1194 std::string
1196 {
1197  return m_algoParams.toString();
1198 }
1199 
1200 std::ostream &
1201 operator<<(std::ostream &os, const vpCircleHoughTransform &detector)
1202 {
1203  os << detector.toString();
1204  return os;
1205 }
bool operator==(const vpArray2D< double > &A) const
Definition: vpArray2D.h:1264
unsigned int getCols() const
Definition: vpArray2D.h:327
Type * data
Address of the first element of the data array.
Definition: vpArray2D.h:139
void resize(unsigned int nrows, unsigned int ncols, bool flagNullify=true, bool recopy_=true)
Definition: vpArray2D.h:352
friend std::ostream & operator<<(std::ostream &s, const vpArray2D< Type > &A)
Definition: vpArray2D.h:600
unsigned int getRows() const
Definition: vpArray2D.h:337
vpImage< unsigned char > detect(const vpImage< vpRGBa > &I_color)
Detect the edges in an image. Convert the color image into a gray-scale image.
void setFilteringAndGradientType(const vpImageFilter::vpCannyFilteringAndGradientType &type)
Set the Filtering And Gradient operators to apply to the image before the edge detection operation.
void setMask(const vpImage< bool > *p_mask)
Set a mask to ignore pixels for which the mask is false.
void setCannyThresholdsRatio(const float &lowerThreshRatio, const float &upperThreshRatio)
Set the lower and upper Canny Thresholds ratio that are used to compute them automatically....
void setGradients(const vpImage< float > &dIx, const vpImage< float > &dIy)
Set the Gradients of the image that will be processed.
void setCannyThresholds(const float &lowerThresh, const float &upperThresh)
Set the lower and upper Canny Thresholds used to qualify the edge point candidates....
void setGradientFilterAperture(const unsigned int &apertureSize)
Set the parameters of the gradient filter (Sobel or Scharr) kernel size filters.
void setGaussianFilterParameters(const int &kernelSize, const float &stdev)
Set the Gaussian Filters kernel size and standard deviation and initialize the aforementioned filters...
void saveConfigurationInJSON(const std::string &jsonPath) const
Save the configuration of the detector in a JSON file described by the path jsonPath....
Class that permits to detect 2D circles in a image using the gradient-based Circle Hough transform....
vpCircleHoughTransform()
Construct a new vpCircleHoughTransform object with default parameters.
std::vector< std::vector< std::pair< unsigned int, unsigned int > > > m_circleCandidatesVotingPoints
void computeVotingMask(const vpImage< unsigned char > &I, const std::vector< vpImageCircle > &detections, std::optional< vpImage< bool > > &mask, std::optional< std::vector< std::vector< std::pair< unsigned int, unsigned int >>>> &opt_votingPoints) const
Compute the mask containing pixels that voted for the detections.
std::vector< std::pair< float, float > > m_centerCandidatesList
vpCannyEdgeDetection m_cannyVisp
std::vector< std::vector< std::pair< unsigned int, unsigned int > > > m_finalCirclesVotingPoints
std::vector< float > m_circleCandidatesProbabilities
std::vector< vpImageCircle > m_finalCircles
virtual ~vpCircleHoughTransform()
Destroy the vp Circle Hough Transform object.
virtual void saveConfigurationInJSON(const std::string &jsonPath) const
Save the configuration of the detector in a JSON file described by the path jsonPath....
std::vector< std::pair< unsigned int, unsigned int > > m_edgePointsList
virtual void initGaussianFilters()
Initialize the Gaussian filters used to blur the image and compute the gradient images.
static const unsigned char edgeMapOn
virtual void computeCenterCandidates()
Determine the image points that are circle center candidates. Increment the center accumulator based ...
vpImage< unsigned char > m_edgeMap
virtual float computeCircleProbability(const vpImageCircle &circle, const unsigned int &nbVotes)
Compute the probability of circle given the number of pixels voting for it nbVotes....
virtual void edgeDetection(const vpImage< unsigned char > &I)
Perform edge detection based on the computed gradients. Stores the edge points and the edge points co...
vpArray2D< float > m_gradientFilterX
virtual void computeGradients(const vpImage< unsigned char > &I)
Perform Gaussian smoothing on the input image to reduce the noise that would perturbate the edge dete...
virtual std::vector< vpImageCircle > detect(const vpImage< vpRGBa > &I)
Convert the input image in a gray-scale image and then perform Circle Hough Transform to detect the c...
std::vector< int > m_centerVotes
static const unsigned char edgeMapOff
vpCircleHoughTransformParameters m_algoParams
const vpImage< bool > * mp_mask
std::vector< unsigned int > m_circleCandidatesVotes
std::vector< unsigned int > m_finalCircleVotes
std::vector< float > m_finalCirclesProbabilities
virtual void filterEdgeMap()
Filter the edge map in order to remove isolated edge points.
void init(const vpCircleHoughTransformParameters &algoParams)
Initialize all the algorithm parameters.
virtual void mergeCircleCandidates()
For each circle candidate CiC_i, check if similar circles have also been detected and if so merges th...
virtual void initGradientFilters()
Initialize the gradient filters used to compute the gradient images.
virtual void initFromJSON(const std::string &jsonPath)
Initialize all the algorithm parameters using the JSON file whose path is jsonPath....
virtual void computeCircleCandidates()
For each center candidate CeC_i, do:
virtual void mergeCandidates(std::vector< vpImageCircle > &circleCandidates, std::vector< unsigned int > &circleCandidatesVotes, std::vector< float > &circleCandidatesProba, std::vector< std::vector< std::pair< unsigned int, unsigned int > > > &votingPoints)
For each circle candidate CiC_i do:
vpArray2D< float > m_gradientFilterY
std::vector< vpImageCircle > m_circleCandidates
error that can be emitted by ViSP classes.
Definition: vpException.h:59
@ ioError
I/O error.
Definition: vpException.h:79
@ badValue
Used to indicate that a value is not in the allowed range.
Definition: vpException.h:85
@ dimensionError
Bad dimension.
Definition: vpException.h:83
@ notImplementedError
Not implemented.
Definition: vpException.h:81
Class that defines a 2D circle in an image.
Definition: vpImageCircle.h:56
float getRadius() const
vpImagePoint getCenter() const
float computeArcLengthInRoI(const vpRect &roi, const float &roundingTolerance=0.001f) const
unsigned int computePixelsInMask(const vpImage< bool > &mask) const
Count the number of pixels of the circle whose value in the mask is true.
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
static void canny(const vpImage< unsigned char > &I, vpImage< unsigned char > &Ic, const unsigned int &gaussianFilterSize, const float &thresholdCanny, const unsigned int &apertureSobel)
static FilterType getSobelKernelX(FilterType *filter, unsigned int size)
@ CANNY_GBLUR_SOBEL_FILTERING
Apply Gaussian blur + Sobel operator on the input image.
@ CANNY_GBLUR_SCHARR_FILTERING
Apply Gaussian blur + Scharr operator on the input image.
static void filter(const vpImage< ImageType > &I, vpImage< FilterType > &If, const vpArray2D< FilterType > &M, bool convolve=false, const vpImage< bool > *p_mask=nullptr)
static std::string vpCannyFilteringAndGradientTypeToString(const vpCannyFilteringAndGradientType &type)
Cast a vpImageFilter::vpCannyFilteringAndGradientType into a string, to know its name.
@ CANNY_VISP_BACKEND
Use ViSP.
static void getGaussianKernel(FilterType *filter, unsigned int size, FilterType sigma=0., bool normalize=true)
static FilterType getScharrKernelY(FilterType *filter, unsigned int size)
static FilterType getSobelKernelY(FilterType *filter, unsigned int size)
static void filterY(const vpImage< vpRGBa > &I, vpImage< vpRGBa > &dIx, const double *filter, unsigned int size, const vpImage< bool > *p_mask=nullptr)
static FilterType getScharrKernelX(FilterType *filter, unsigned int size)
static void filterX(const vpImage< ImageType > &I, vpImage< FilterType > &dIx, const FilterType *filter, unsigned int size, const vpImage< bool > *p_mask=nullptr)
static void dilatation(vpImage< Type > &I, Type value, Type value_out, vpConnexityType connexity=CONNEXITY_4)
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
Definition: vpImagePoint.h:82
static double distance(const vpImagePoint &iP1, const vpImagePoint &iP2)
double get_u() const
Definition: vpImagePoint.h:136
double get_v() const
Definition: vpImagePoint.h:147
unsigned int getWidth() const
Definition: vpImage.h:245
unsigned int getCols() const
Definition: vpImage.h:175
unsigned int getHeight() const
Definition: vpImage.h:184
unsigned int getRows() const
Definition: vpImage.h:215
static bool equal(double x, double y, double threshold=0.001)
Definition: vpMath.h:449
Defines a rectangle in the plane.
Definition: vpRect.h:76