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