Visual Servoing Platform  version 3.6.1 under development (2024-12-04)
vpCircleHoughTransform_common.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 #if (VISP_CXX_STANDARD == VISP_CXX_STANDARD_98)
39 namespace
40 {
41 
42 // Sorting by decreasing probabilities
43 bool hasBetterProba(std::pair<size_t, float> a, std::pair<size_t, float> b)
44 {
45  return (a.second > b.second);
46 }
47 
48 
49 void
50 scaleFilter(vpArray2D<float> &filter, const float &scale)
51 {
52  unsigned int nbRows = filter.getRows();
53  unsigned int nbCols = filter.getCols();
54  for (unsigned int r = 0; r < nbRows; ++r) {
55  for (unsigned int c = 0; c < nbCols; ++c) {
56  filter[r][c] = filter[r][c] * scale;
57  }
58  }
59 }
60 }
61 #endif
62 
64  : m_algoParams()
65  , mp_mask(nullptr)
66 {
69 }
70 
72  : m_algoParams(algoParams)
73  , mp_mask(nullptr)
74 {
77 }
78 
79 void
81 {
82  m_algoParams = algoParams;
85 }
86 
88 { }
89 
90 #ifdef VISP_HAVE_NLOHMANN_JSON
91 using json = nlohmann::json;
92 
94  : mp_mask(nullptr)
95 {
96  initFromJSON(jsonPath);
97 }
98 
99 void
100 vpCircleHoughTransform::initFromJSON(const std::string &jsonPath)
101 {
102  std::ifstream file(jsonPath);
103  if (!file.good()) {
104  std::stringstream ss;
105  ss << "Problem opening file " << jsonPath << ". Make sure it exists and is readable" << std::endl;
106  throw vpException(vpException::ioError, ss.str());
107  }
108  json j;
109  try {
110  j = json::parse(file);
111  }
112  catch (json::parse_error &e) {
113  std::stringstream msg;
114  msg << "Could not parse JSON file : \n";
115 
116  msg << e.what() << std::endl;
117  msg << "Byte position of error: " << e.byte;
118  throw vpException(vpException::ioError, msg.str());
119  }
120  m_algoParams = j; // Call from_json(const json& j, vpDetectorDNN& *this) to read json
121  file.close();
124 }
125 
126 void
127 vpCircleHoughTransform::saveConfigurationInJSON(const std::string &jsonPath) const
128 {
130 }
131 #endif
132 
133 void
135 {
136  const int filterHalfSize = (m_algoParams.m_gaussianKernelSize + 1) / 2;
137  m_fg.resize(1, filterHalfSize);
138  vpImageFilter::getGaussianKernel(m_fg.data, m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev, true);
139  m_cannyVisp.setGaussianFilterParameters(m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev);
140 }
141 
142 void
144 {
145 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11) // Check if cxx11 or higher
146  // Helper to apply the scale to the raw values of the filters
147  auto scaleFilter = [](vpArray2D<float> &filter, const float &scale) {
148  const unsigned int nbRows = filter.getRows();
149  const unsigned int nbCols = filter.getCols();
150  for (unsigned int r = 0; r < nbRows; ++r) {
151  for (unsigned int c = 0; c < nbCols; ++c) {
152  filter[r][c] = filter[r][c] * scale;
153  }
154  }
155  };
156 #endif
157 
158  const int moduloCheckForOddity = 2;
159  if ((m_algoParams.m_gradientFilterKernelSize % moduloCheckForOddity) != 1) {
160  throw vpException(vpException::badValue, "Gradient filters Kernel size should be odd.");
161  }
162  m_gradientFilterX.resize(m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gradientFilterKernelSize);
163  m_gradientFilterY.resize(m_algoParams.m_gradientFilterKernelSize, m_algoParams.m_gradientFilterKernelSize);
164  m_cannyVisp.setGradientFilterAperture(m_algoParams.m_gradientFilterKernelSize);
165 
166  float scaleX = 1.f;
167  float scaleY = 1.f;
168  unsigned int filterHalfSize = (m_algoParams.m_gradientFilterKernelSize - 1) / 2;
169 
170  if (m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING) {
171  // Compute the Sobel filters
172  scaleX = vpImageFilter::getSobelKernelX(m_gradientFilterX.data, filterHalfSize);
173  scaleY = vpImageFilter::getSobelKernelY(m_gradientFilterY.data, filterHalfSize);
174  }
175  else if (m_algoParams.m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING) {
176  // Compute the Scharr filters
177  scaleX = vpImageFilter::getScharrKernelX(m_gradientFilterX.data, filterHalfSize);
178  scaleY = vpImageFilter::getScharrKernelY(m_gradientFilterY.data, filterHalfSize);
179  }
180  else {
181  std::string errMsg = "[vpCircleHoughTransform::initGradientFilters] Error: gradient filtering method \"";
182  errMsg += vpImageFilter::vpCannyFiltAndGradTypeToStr(m_algoParams.m_filteringAndGradientType);
183  errMsg += "\" has not been implemented yet\n";
185  }
186  scaleFilter(m_gradientFilterX, scaleX);
187  scaleFilter(m_gradientFilterY, scaleY);
188 }
189 
190 std::vector<vpImageCircle>
192 {
193  vpImage<unsigned char> I_gray;
194  vpImageConvert::convert(I, I_gray);
195  return detect(I_gray);
196 }
197 
198 #ifdef HAVE_OPENCV_CORE
199 std::vector<vpImageCircle>
200 vpCircleHoughTransform::detect(const cv::Mat &cv_I)
201 {
202  vpImage<unsigned char> I_gray;
203  vpImageConvert::convert(cv_I, I_gray);
204  return detect(I_gray);
205 }
206 #endif
207 
208 std::vector<vpImageCircle>
210 {
211  std::vector<vpImageCircle> detections = detect(I);
212  size_t nbDetections = detections.size();
213 
214  // Prepare vector of tuple to sort by decreasing probabilities
215  std::vector<std::pair<size_t, float> > v_id_proba;
216  for (size_t i = 0; i < nbDetections; ++i) {
217  std::pair<size_t, float> id_proba(i, m_finalCirclesProbabilities[i]);
218  v_id_proba.push_back(id_proba);
219  }
220 
221 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
222  // Sorting by decreasing probabilities
223  auto hasBetterProba
224  = [](std::pair<size_t, float> a, std::pair<size_t, float> b) {
225  return (a.second > b.second);
226  };
227 #endif
228  std::sort(v_id_proba.begin(), v_id_proba.end(), hasBetterProba);
229 
230  // Clearing the storages containing the detection results
231  // to have it sorted by decreasing probabilities
232  size_t limitMin;
233  if (nbCircles < 0) {
234  limitMin = nbDetections;
235  }
236  else {
237  limitMin = std::min(nbDetections, static_cast<size_t>(nbCircles));
238  }
239 
240  std::vector<vpImageCircle> bestCircles;
241  std::vector<vpImageCircle> copyFinalCircles = m_finalCircles;
242  std::vector<unsigned int> copyFinalCirclesVotes = m_finalCircleVotes;
243  std::vector<float> copyFinalCirclesProbas = m_finalCirclesProbabilities;
244  std::vector<std::vector<std::pair<unsigned int, unsigned int> > > copyFinalCirclesVotingPoints = m_finalCirclesVotingPoints;
245  for (size_t i = 0; i < nbDetections; ++i) {
246  size_t id = v_id_proba[i].first;
247  m_finalCircles[i] = copyFinalCircles[id];
248  m_finalCircleVotes[i] = copyFinalCirclesVotes[id];
249  m_finalCirclesProbabilities[i] = copyFinalCirclesProbas[id];
250  if (m_algoParams.m_recordVotingPoints) {
251  m_finalCirclesVotingPoints[i] = copyFinalCirclesVotingPoints[id];
252  }
253  if (i < limitMin) {
254  bestCircles.push_back(m_finalCircles[i]);
255  }
256  }
257 
258  return bestCircles;
259 }
260 
261 std::vector<vpImageCircle>
263 {
264  // Cleaning results of potential previous detection
265  m_centerCandidatesList.clear();
266  m_centerVotes.clear();
267  m_edgePointsList.clear();
268  m_circleCandidates.clear();
269  m_circleCandidatesVotes.clear();
272  m_finalCircles.clear();
273  m_finalCircleVotes.clear();
276 
277  // Ensuring that the difference between the max and min radii is big enough to take into account
278  // the pixelization of the image
279  const float minRadiusDiff = 3.f;
280  if ((m_algoParams.m_maxRadius - m_algoParams.m_minRadius) < minRadiusDiff) {
281  if (m_algoParams.m_minRadius > (minRadiusDiff / 2.f)) {
282  m_algoParams.m_maxRadius += minRadiusDiff / 2.f;
283  m_algoParams.m_minRadius -= minRadiusDiff / 2.f;
284  }
285  else {
286  m_algoParams.m_maxRadius += minRadiusDiff - m_algoParams.m_minRadius;
287  m_algoParams.m_minRadius = 0.f;
288  }
289  }
290 
291  // Ensuring that the difference between the max and min center position is big enough to take into account
292  // the pixelization of the image
293  const float minCenterPositionDiff = 3.f;
294  if ((m_algoParams.m_centerXlimits.second - m_algoParams.m_centerXlimits.first) < minCenterPositionDiff) {
295  m_algoParams.m_centerXlimits.second += static_cast<int>(minCenterPositionDiff / 2.f);
296  m_algoParams.m_centerXlimits.first -= static_cast<int>(minCenterPositionDiff / 2.f);
297  }
298  if ((m_algoParams.m_centerYlimits.second - m_algoParams.m_centerYlimits.first) < minCenterPositionDiff) {
299  m_algoParams.m_centerYlimits.second += static_cast<int>(minCenterPositionDiff / 2.f);
300  m_algoParams.m_centerYlimits.first -= static_cast<int>(minCenterPositionDiff / 2.f);
301  }
302 
303  // First thing, we need to apply a Gaussian filter on the image to remove some spurious noise
304  // Then, we need to compute the image gradients in order to be able to perform edge detection
305  computeGradients(I);
306 
307  // Using the gradients, it is now possible to perform edge detection
308  // We rely on the Canny edge detector
309  // It will also give us the connected edged points
310  edgeDetection(I);
311 
312  // From the edge map and gradient information, it is possible to compute
313  // the center point candidates
315 
316  // From the edge map and center point candidates, we can compute candidate
317  // circles. These candidate circles are circles whose center belong to
318  // the center point candidates and whose radius is a "radius bin" that got
319  // enough votes by computing the distance between each point of the edge map
320  // and the center point candidate
322 
323  // Finally, we perform a merging operation that permits to merge circles
324  // respecting similarity criteria (distance between centers and similar radius)
326 
327  return m_finalCircles;
328 }
329 
330 bool
331 operator==(const vpImageCircle &a, const vpImageCircle &b)
332 {
333  vpImagePoint aCenter = a.getCenter();
334  vpImagePoint bCenter = b.getCenter();
335  bool haveSameCenter = (std::abs(aCenter.get_u() - bCenter.get_u())
336  + std::abs(aCenter.get_v() - bCenter.get_v())) <= (2. * std::numeric_limits<double>::epsilon());
337  bool haveSameRadius = std::abs(a.getRadius() - b.getRadius()) <= (2.f * std::numeric_limits<float>::epsilon());
338  return (haveSameCenter && haveSameRadius);
339 }
340 
341 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
342 void vpCircleHoughTransform::computeVotingMask(const vpImage<unsigned char> &I, const std::vector<vpImageCircle> &detections,
343  std::optional< vpImage<bool> > &mask, std::optional<std::vector<std::vector<std::pair<unsigned int, unsigned int>>>> &opt_votingPoints) const
344 #else
345 void vpCircleHoughTransform::computeVotingMask(const vpImage<unsigned char> &I, const std::vector<vpImageCircle> &detections,
346  vpImage<bool> **mask, std::vector<std::vector<std::pair<unsigned int, unsigned int> > > **opt_votingPoints) const
347 #endif
348 {
349  if (!m_algoParams.m_recordVotingPoints) {
350  // We weren't asked to remember the voting points
351 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
352  mask = std::nullopt;
353  opt_votingPoints = std::nullopt;
354 #else
355  *mask = nullptr;
356  *opt_votingPoints = nullptr;
357 #endif
358  return;
359  }
360 
361 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
362  mask = vpImage<bool>(I.getHeight(), I.getWidth(), false);
363  opt_votingPoints = std::vector<std::vector<std::pair<unsigned int, unsigned int>>>();
364 #else
365  *mask = new vpImage<bool>(I.getHeight(), I.getWidth(), false);
366  *opt_votingPoints = new std::vector<std::vector<std::pair<unsigned int, unsigned int> > >();
367 #endif
368 
369 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
370  for (const auto &detection : detections)
371 #else
372  const size_t nbDetections = detections.size();
373  for (size_t i = 0; i < nbDetections; ++i)
374 #endif
375  {
376  bool hasFoundSimilarCircle = false;
377  unsigned int nbPreviouslyDetected = static_cast<unsigned int>(m_finalCircles.size());
378  unsigned int id = 0;
379  // Looking for a circle that was detected and is similar to the one given to the function
380  while ((id < nbPreviouslyDetected) && (!hasFoundSimilarCircle)) {
381  vpImageCircle previouslyDetected = m_finalCircles[id];
382 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
383  if (previouslyDetected == detection)
384 #else
385  if (previouslyDetected == detections[i])
386 #endif
387  {
388  hasFoundSimilarCircle = true;
389  // We found a circle that is similar to the one given to the function => updating the mask
390  const unsigned int nbVotingPoints = static_cast<unsigned int>(m_finalCirclesVotingPoints[id].size());
391  for (unsigned int idPoint = 0; idPoint < nbVotingPoints; ++idPoint) {
392  const std::pair<unsigned int, unsigned int> &votingPoint = m_finalCirclesVotingPoints[id][idPoint];
393 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
394  (*mask)[votingPoint.first][votingPoint.second] = true;
395 #else
396  (**mask)[votingPoint.first][votingPoint.second] = true;
397 #endif
398  }
399 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_17)
400  opt_votingPoints->push_back(m_finalCirclesVotingPoints[id]);
401 #else
402  (**opt_votingPoints).push_back(m_finalCirclesVotingPoints[id]);
403 #endif
404  }
405  ++id;
406  }
407  }
408 }
409 
410 std::string
412 {
413  return m_algoParams.toString();
414 }
415 
416 std::ostream &
417 operator<<(std::ostream &os, const vpCircleHoughTransform &detector)
418 {
419  os << detector.toString();
420  std::cout << "\tUse mask: " << (detector.mp_mask == nullptr ? "false" : "true") << std::endl;
421  return os;
422 }
423 
424 END_VISP_NAMESPACE
bool operator==(const vpArray2D< double > &A) const
Definition: vpArray2D.h:1313
unsigned int getCols() const
Definition: vpArray2D.h:337
Type * data
Address of the first element of the data array.
Definition: vpArray2D.h:148
void resize(unsigned int nrows, unsigned int ncols, bool flagNullify=true, bool recopy_=true)
Definition: vpArray2D.h:362
friend std::ostream & operator<<(std::ostream &s, const vpArray2D< Type > &A)
Definition: vpArray2D.h:614
unsigned int getRows() const
Definition: vpArray2D.h:347
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...
Class that gather the algorithm parameters.
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....
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.
virtual void computeCenterCandidates()
Determine the image points that are circle center candidates. Increment the center accumulator based ...
virtual void computeCircleCandidates()
For each center candidate CeC_i, do:
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
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
void init(const vpCircleHoughTransformParams &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....
vpCircleHoughTransform()
Construct a new vpCircleHoughTransform object with default parameters.
vpArray2D< float > m_gradientFilterY
std::vector< vpImageCircle > m_circleCandidates
error that can be emitted by ViSP classes.
Definition: vpException.h:60
@ ioError
I/O error.
Definition: vpException.h:67
@ badValue
Used to indicate that a value is not in the allowed range.
Definition: vpException.h:73
@ notImplementedError
Not implemented.
Definition: vpException.h:69
Class that defines a 2D circle in an image.
Definition: vpImageCircle.h:57
float getRadius() const
vpImagePoint getCenter() const
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
static FilterType getSobelKernelX(FilterType *filter, unsigned int size)
static std::string vpCannyFiltAndGradTypeToStr(const vpCannyFilteringAndGradientType &type)
Cast a vpImageFilter::vpCannyFilteringAndGradientType into a string, to know its name.
@ CANNY_GBLUR_SOBEL_FILTERING
Apply Gaussian blur + Sobel operator on the input image.
Definition: vpImageFilter.h:91
@ CANNY_GBLUR_SCHARR_FILTERING
Apply Gaussian blur + Scharr operator on the input image.
Definition: vpImageFilter.h:92
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 FilterType getScharrKernelX(FilterType *filter, unsigned int size)
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
Definition: vpImagePoint.h:82
double get_u() const
Definition: vpImagePoint.h:136
double get_v() const
Definition: vpImagePoint.h:147
unsigned int getWidth() const
Definition: vpImage.h:242
unsigned int getHeight() const
Definition: vpImage.h:181