Visual Servoing Platform  version 3.6.1 under development (2024-10-18)
vpCircleHoughTransform_circles.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 
31 #include <visp3/core/vpImageConvert.h>
32 #include <visp3/core/vpImageMorphology.h>
33 
34 #include <visp3/imgproc/vpCircleHoughTransform.h>
35 
36 BEGIN_VISP_NAMESPACE
37 
38 #ifndef DOXYGEN_SHOULD_SKIP_THIS
39 namespace
40 {
41 #if (VISP_CXX_STANDARD == VISP_CXX_STANDARD_98)
42 float computeEffectiveRadius(const float &votes, const float &weigthedSumRadius)
43 {
44  float r_effective = -1.f;
45  if (votes > std::numeric_limits<float>::epsilon()) {
46  r_effective = weigthedSumRadius / votes;
47  }
48  return r_effective;
49 }
50 #endif
51 
52 typedef struct vpDataUpdateRadAccum
53 {
54  int m_nbBins;
55  std::pair<float, float> m_centerCandidate;
56  std::pair<unsigned int, unsigned int> m_edgePoint;
57  const vpImage<float> &m_dIx;
58  const vpImage<float> &m_dIy;
59  float m_rx;
60  float m_ry;
61  float m_circlePerfectness2;
62  float m_r2;
63  float m_minRadius;
64  float m_mergingRadiusDiffThresh;
65  bool m_recordVotingPoints;
66 
67  vpDataUpdateRadAccum(const vpImage<float> &dIx, const vpImage<float> &dIy)
68  : m_dIx(dIx)
69  , m_dIy(dIy)
70  { }
71 } vpDataUpdateRadAccum;
72 
73 void
74 updateRadiusAccumulator(const vpDataUpdateRadAccum &data, std::vector<float> &radiusAccumList,
75  std::vector<float> &radiusActualValueList,
76  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > &votingPoints)
77 {
78  float gx = data.m_dIx[data.m_edgePoint.first][data.m_edgePoint.second];
79  float gy = data.m_dIy[data.m_edgePoint.first][data.m_edgePoint.second];
80  float grad2 = (gx * gx) + (gy * gy);
81  float scalProd = (data.m_rx * gx) + (data.m_ry * gy);
82  float scalProd2 = scalProd * scalProd;
83  if (scalProd2 >= (data.m_circlePerfectness2 * data.m_r2 * grad2)) {
84  // Look for the Radius Candidate Bin RCB_k to which d_ij is "the closest" will have an additionnal vote
85  float r = static_cast<float>(std::sqrt(data.m_r2));
86  int r_bin = static_cast<int>(std::floor((r - data.m_minRadius) / data.m_mergingRadiusDiffThresh));
87  r_bin = std::min<int>(r_bin, data.m_nbBins - 1);
88  if ((r < (data.m_minRadius + (data.m_mergingRadiusDiffThresh * 0.5f)))
89  || (r >(data.m_minRadius + (data.m_mergingRadiusDiffThresh * (static_cast<float>(data.m_nbBins - 1) + 0.5f))))) {
90  // If the radius is at the very beginning of the allowed radii or at the very end, we do not span the vote
91  radiusAccumList[r_bin] += 1.f;
92  radiusActualValueList[r_bin] += r;
93  if (data.m_recordVotingPoints) {
94  votingPoints[r_bin].push_back(data.m_edgePoint);
95  }
96  }
97  else {
98  float midRadiusPrevBin = data.m_minRadius + (data.m_mergingRadiusDiffThresh * ((r_bin - 1.f) + 0.5f));
99  float midRadiusCurBin = data.m_minRadius + (data.m_mergingRadiusDiffThresh * (r_bin + 0.5f));
100  float midRadiusNextBin = data.m_minRadius + (data.m_mergingRadiusDiffThresh * (r_bin + 1.f + 0.5f));
101 
102  if ((r >= midRadiusCurBin) && (r <= midRadiusNextBin)) {
103  // The radius is at the end of the current bin or beginning of the next, we span the vote with the next bin
104  float voteCurBin = (midRadiusNextBin - r) / data.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the current bin
105  float voteNextBin = 1.f - voteCurBin;
106  radiusAccumList[r_bin] += voteCurBin;
107  radiusActualValueList[r_bin] += r * voteCurBin;
108  radiusAccumList[r_bin + 1] += voteNextBin;
109  radiusActualValueList[r_bin + 1] += r * voteNextBin;
110  if (data.m_recordVotingPoints) {
111  votingPoints[r_bin].push_back(data.m_edgePoint);
112  votingPoints[r_bin + 1].push_back(data.m_edgePoint);
113  }
114  }
115  else {
116  // The radius is at the end of the previous bin or beginning of the current, we span the vote with the previous bin
117  float votePrevBin = (r - midRadiusPrevBin) / data.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the previous bin
118  float voteCurBin = 1.f - votePrevBin;
119  radiusAccumList[r_bin] += voteCurBin;
120  radiusActualValueList[r_bin] += r * voteCurBin;
121  radiusAccumList[r_bin - 1] += votePrevBin;
122  radiusActualValueList[r_bin - 1] += r * votePrevBin;
123  if (data.m_recordVotingPoints) {
124  votingPoints[r_bin].push_back(data.m_edgePoint);
125  votingPoints[r_bin - 1].push_back(data.m_edgePoint);
126  }
127  }
128  }
129  }
130 }
131 }
132 #endif
133 
134 void
136 {
137  size_t nbCenterCandidates = m_centerCandidatesList.size();
138  int nbBins = static_cast<int>(((m_algoParams.m_maxRadius - m_algoParams.m_minRadius) + 1) / m_algoParams.m_mergingRadiusDiffThresh);
139  nbBins = std::max<int>(static_cast<int>(1), nbBins); // Avoid having 0 bins, which causes segfault
140  std::vector<float> radiusAccumList; // Radius accumulator for each center candidates.
141  std::vector<float> radiusActualValueList; // Vector that contains the actual distance between the edge points and the center candidates.
142  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > votingPoints(nbBins); // Vectors that contain the points voting for each radius bin
143 
144  float rmin2 = m_algoParams.m_minRadius * m_algoParams.m_minRadius;
145  float rmax2 = m_algoParams.m_maxRadius * m_algoParams.m_maxRadius;
146  float circlePerfectness2 = m_algoParams.m_circlePerfectness * m_algoParams.m_circlePerfectness;
147 
148  vpDataUpdateRadAccum data(m_dIx, m_dIy);
149  data.m_nbBins = nbBins;
150  data.m_circlePerfectness2 = circlePerfectness2;
151  data.m_minRadius = m_algoParams.m_minRadius;
152  data.m_mergingRadiusDiffThresh = m_algoParams.m_mergingRadiusDiffThresh;
153  data.m_recordVotingPoints = m_algoParams.m_recordVotingPoints;
154 
155  for (size_t i = 0; i < nbCenterCandidates; ++i) {
156  std::pair<float, float> centerCandidate = m_centerCandidatesList[i];
157  // Initialize the radius accumulator of the candidate with 0s
158  radiusAccumList.clear();
159  radiusAccumList.resize(nbBins, 0);
160  radiusActualValueList.clear();
161  radiusActualValueList.resize(nbBins, 0.);
162 
163  const unsigned int nbEdgePoints = static_cast<unsigned int>(m_edgePointsList.size());
164  for (unsigned int e = 0; e < nbEdgePoints; ++e) {
165  const std::pair<unsigned int, unsigned int> &edgePoint = m_edgePointsList[e];
166 
167  // For each center candidate CeC_i, compute the distance with each edge point EP_j d_ij = dist(CeC_i; EP_j)
168  float rx = edgePoint.second - centerCandidate.second;
169  float ry = edgePoint.first - centerCandidate.first;
170  float r2 = (rx * rx) + (ry * ry);
171  if ((r2 > rmin2) && (r2 < rmax2)) {
172  data.m_centerCandidate = centerCandidate;
173  data.m_edgePoint = edgePoint;
174  data.m_rx = rx;
175  data.m_ry = ry;
176  data.m_r2 = r2;
177  updateRadiusAccumulator(data, radiusAccumList, radiusActualValueList, votingPoints);
178  }
179  }
180 
181 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
182  // Lambda to compute the effective radius (i.e. barycenter) of each radius bin
183  auto computeEffectiveRadius = [](const float &votes, const float &weigthedSumRadius) {
184  float r_effective = -1.f;
185  if (votes > std::numeric_limits<float>::epsilon()) {
186  r_effective = weigthedSumRadius / votes;
187  }
188  return r_effective;
189  };
190 #endif
191 
192  // Merging similar candidates
193  std::vector<float> v_r_effective; // Vector of radius of each candidate after the merge step
194  std::vector<float> v_votes_effective; // Vector of number of votes of each candidate after the merge step
195  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > v_votingPoints_effective; // Vector of voting points after the merge step
196  std::vector<bool> v_hasMerged_effective; // Vector indicating if merge has been performed for the different candidates
197  for (int idBin = 0; idBin < nbBins; ++idBin) {
198  float r_effective = computeEffectiveRadius(radiusAccumList[idBin], radiusActualValueList[idBin]);
199  float votes_effective = radiusAccumList[idBin];
200  std::vector<std::pair<unsigned int, unsigned int> > votingPoints_effective = votingPoints[idBin];
201  bool is_r_effective_similar = (r_effective > 0.f);
202  // Looking for potential similar radii in the following bins
203  // If so, compute the barycenter radius between them
204  int idCandidate = idBin + 1;
205  bool hasMerged = false;
206  while ((idCandidate < nbBins) && is_r_effective_similar) {
207  float r_effective_candidate = computeEffectiveRadius(radiusAccumList[idCandidate], radiusActualValueList[idCandidate]);
208  if (std::abs(r_effective_candidate - r_effective) < m_algoParams.m_mergingRadiusDiffThresh) {
209  r_effective = ((r_effective * votes_effective) + (r_effective_candidate * radiusAccumList[idCandidate])) / (votes_effective + radiusAccumList[idCandidate]);
210  votes_effective += radiusAccumList[idCandidate];
211  radiusAccumList[idCandidate] = -.1f;
212  radiusActualValueList[idCandidate] = -1.f;
213  is_r_effective_similar = true;
214  if (m_algoParams.m_recordVotingPoints) {
215  // Move elements from votingPoints[idCandidate] to votingPoints_effective.
216  // votingPoints[idCandidate] is left in undefined but safe-to-destruct state.
217 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
218  votingPoints_effective.insert(
219  votingPoints_effective.end(),
220  std::make_move_iterator(votingPoints[idCandidate].begin()),
221  std::make_move_iterator(votingPoints[idCandidate].end())
222  );
223 #else
224  votingPoints_effective.insert(
225  votingPoints_effective.end(),
226  votingPoints[idCandidate].begin(),
227  votingPoints[idCandidate].end()
228  );
229 #endif
230  hasMerged = true;
231  }
232  }
233  else {
234  is_r_effective_similar = false;
235  }
236  ++idCandidate;
237  }
238 
239  if ((votes_effective > m_algoParams.m_centerMinThresh) && (votes_effective >= (m_algoParams.m_circleVisibilityRatioThresh * 2.f * M_PI_FLOAT * r_effective))) {
240  // Only the circles having enough votes and being visible enough are considered
241  v_r_effective.push_back(r_effective);
242  v_votes_effective.push_back(votes_effective);
243  if (m_algoParams.m_recordVotingPoints) {
244  v_votingPoints_effective.push_back(votingPoints_effective);
245  v_hasMerged_effective.push_back(hasMerged);
246  }
247  }
248  }
249 
250  unsigned int nbCandidates = static_cast<unsigned int>(v_r_effective.size());
251  for (unsigned int idBin = 0; idBin < nbCandidates; ++idBin) {
252  // If the circle of center CeC_i and radius RCB_k has enough votes, it is added to the list
253  // of Circle Candidates
254  float r_effective = v_r_effective[idBin];
255  vpImageCircle candidateCircle(vpImagePoint(centerCandidate.first, centerCandidate.second), r_effective);
256  float proba = computeCircleProbability(candidateCircle, static_cast<unsigned int>(v_votes_effective[idBin]));
257  if (proba > m_algoParams.m_circleProbaThresh) {
258  m_circleCandidates.push_back(candidateCircle);
259  m_circleCandidatesProbabilities.push_back(proba);
260  m_circleCandidatesVotes.push_back(static_cast<unsigned int>(v_votes_effective[idBin]));
261  if (m_algoParams.m_recordVotingPoints) {
262  if (v_hasMerged_effective[idBin]) {
263  // Remove potential duplicated points
264  std::sort(v_votingPoints_effective[idBin].begin(), v_votingPoints_effective[idBin].end());
265  v_votingPoints_effective[idBin].erase(std::unique(v_votingPoints_effective[idBin].begin(), v_votingPoints_effective[idBin].end()), v_votingPoints_effective[idBin].end());
266  }
267  // Save the points
268  m_circleCandidatesVotingPoints.push_back(v_votingPoints_effective[idBin]);
269  }
270  }
271  }
272  }
273 }
274 
275 float
276 vpCircleHoughTransform::computeCircleProbability(const vpImageCircle &circle, const unsigned int &nbVotes)
277 {
278  float proba(0.f);
279  float visibleArc(static_cast<float>(nbVotes));
280  float theoreticalLenght;
281  if (mp_mask != nullptr) {
282  theoreticalLenght = static_cast<float>(circle.computePixelsInMask(*mp_mask));
283  }
284  else {
285  theoreticalLenght = circle.computeArcLengthInRoI(vpRect(vpImagePoint(0, 0), m_edgeMap.getWidth(), m_edgeMap.getHeight()));
286  }
287  if (theoreticalLenght < std::numeric_limits<float>::epsilon()) {
288  proba = 0.f;
289  }
290  else {
291  proba = std::min(visibleArc / theoreticalLenght, 1.f);
292  }
293  return proba;
294 }
295 
296 void
298 {
299  std::vector<vpImageCircle> circleCandidates = m_circleCandidates;
300  std::vector<unsigned int> circleCandidatesVotes = m_circleCandidatesVotes;
301  std::vector<float> circleCandidatesProba = m_circleCandidatesProbabilities;
302  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > circleCandidatesVotingPoints = m_circleCandidatesVotingPoints;
303  // First iteration of merge
304  mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba, circleCandidatesVotingPoints);
305 
306  // Second iteration of merge
307  mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba, circleCandidatesVotingPoints);
308 
309  // Saving the results
310  m_finalCircles = circleCandidates;
311  m_finalCircleVotes = circleCandidatesVotes;
312  m_finalCirclesProbabilities = circleCandidatesProba;
313  m_finalCirclesVotingPoints = circleCandidatesVotingPoints;
314 }
315 
316 void
317 vpCircleHoughTransform::mergeCandidates(std::vector<vpImageCircle> &circleCandidates, std::vector<unsigned int> &circleCandidatesVotes,
318  std::vector<float> &circleCandidatesProba, std::vector<std::vector<std::pair<unsigned int, unsigned int> > > &votingPoints)
319 {
320  size_t nbCandidates = circleCandidates.size();
321  size_t i = 0;
322  while (i < nbCandidates) {
323  vpImageCircle cic_i = circleCandidates[i];
324  bool hasPerformedMerge = false;
325  // // For each other circle candidate CiC_j do:
326  size_t j = i + 1;
327  while (j < nbCandidates) {
328  vpImageCircle cic_j = circleCandidates[j];
329  // // // Compute the similarity between CiC_i and CiC_j
330  double distanceBetweenCenters = vpImagePoint::distance(cic_i.getCenter(), cic_j.getCenter());
331  double radiusDifference = std::abs(cic_i.getRadius() - cic_j.getRadius());
332  bool areCirclesSimilar = ((distanceBetweenCenters < m_algoParams.m_centerMinDist)
333  && (radiusDifference < m_algoParams.m_mergingRadiusDiffThresh)
334  );
335 
336  if (areCirclesSimilar) {
337  hasPerformedMerge = true;
338  // // // If the similarity exceeds a threshold, merge the circle candidates CiC_i and CiC_j and remove CiC_j of the list
339  unsigned int totalVotes = circleCandidatesVotes[i] + circleCandidatesVotes[j];
340  float totalProba = circleCandidatesProba[i] + circleCandidatesProba[j];
341  float newProba = 0.5f * totalProba;
342  float newRadius = ((cic_i.getRadius() * circleCandidatesProba[i]) + (cic_j.getRadius() * circleCandidatesProba[j])) / totalProba;
343  vpImagePoint newCenter = ((cic_i.getCenter() * circleCandidatesProba[i]) + (cic_j.getCenter() * circleCandidatesProba[j])) / totalProba;
344  cic_i = vpImageCircle(newCenter, newRadius);
345  circleCandidates[j] = circleCandidates[nbCandidates - 1];
346  const unsigned int var2 = 2;
347  circleCandidatesVotes[i] = totalVotes / var2; // Compute the mean vote
348  circleCandidatesVotes[j] = circleCandidatesVotes[nbCandidates - 1];
349  circleCandidatesProba[i] = newProba;
350  circleCandidatesProba[j] = circleCandidatesProba[nbCandidates - 1];
351  if (m_algoParams.m_recordVotingPoints) {
352 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
353  votingPoints[i].insert(
354  votingPoints[i].end(),
355  std::make_move_iterator(votingPoints[j].begin()),
356  std::make_move_iterator(votingPoints[j].end())
357  );
358 #else
359  votingPoints[i].insert(
360  votingPoints[i].end(),
361  votingPoints[j].begin(),
362  votingPoints[j].end()
363  );
364 #endif
365  votingPoints.pop_back();
366  }
367  circleCandidates.pop_back();
368  circleCandidatesVotes.pop_back();
369  circleCandidatesProba.pop_back();
370  --nbCandidates;
371  // We do not update j because the new j-th candidate has not been evaluated yet
372  }
373  else {
374  // We will evaluate the next candidate
375  ++j;
376  }
377  }
378  // // Add the circle candidate CiC_i, potentially merged with other circle candidates, to the final list of detected circles
379  circleCandidates[i] = cic_i;
380  if (hasPerformedMerge && m_algoParams.m_recordVotingPoints) {
381  // Remove duplicated points
382  std::sort(votingPoints[i].begin(), votingPoints[i].end());
383  votingPoints[i].erase(std::unique(votingPoints[i].begin(), votingPoints[i].end()), votingPoints[i].end());
384  }
385  ++i;
386  }
387 }
388 
389 END_VISP_NAMESPACE
std::vector< std::vector< std::pair< unsigned int, unsigned int > > > m_circleCandidatesVotingPoints
std::vector< std::pair< float, float > > m_centerCandidatesList
std::vector< std::vector< std::pair< unsigned int, unsigned int > > > m_finalCirclesVotingPoints
std::vector< float > m_circleCandidatesProbabilities
std::vector< vpImageCircle > m_finalCircles
std::vector< std::pair< unsigned int, unsigned int > > m_edgePointsList
virtual void computeCircleCandidates()
For each center candidate CeC_i, do:
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....
const vpImage< bool > * mp_mask
std::vector< unsigned int > m_circleCandidatesVotes
std::vector< unsigned int > m_finalCircleVotes
vpCircleHoughTransformParams m_algoParams
std::vector< float > m_finalCirclesProbabilities
virtual void mergeCircleCandidates()
For each circle candidate CiC_i, check if similar circles have also been detected and if so merges th...
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:
std::vector< vpImageCircle > m_circleCandidates
Class that defines a 2D circle in an image.
Definition: vpImageCircle.h:57
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.
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)
unsigned int getWidth() const
Definition: vpImage.h:242
unsigned int getHeight() const
Definition: vpImage.h:181
Defines a rectangle in the plane.
Definition: vpRect.h:79