Visual Servoing Platform  version 3.6.1 under development (2024-04-29)
vpCannyEdgeDetection.cpp
1 /****************************************************************************
2  *
3  * ViSP, open source Visual Servoing Platform software.
4  * Copyright (C) 2005 - 2023 by Inria. All rights reserved.
5  *
6  * This software is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  * See the file LICENSE.txt at the root directory of this source
11  * distribution for additional information about the GNU GPL.
12  *
13  * For using ViSP with software that can not be combined with the GNU
14  * GPL, please contact Inria about acquiring a ViSP Professional
15  * Edition License.
16  *
17  * See https://visp.inria.fr for more information.
18  *
19  * This software was developed at:
20  * Inria Rennes - Bretagne Atlantique
21  * Campus Universitaire de Beaulieu
22  * 35042 Rennes Cedex
23  * France
24  *
25  * If you have questions regarding the use of this file, please contact
26  * Inria at visp@inria.fr
27  *
28  * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
29  * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
30  *
31 *****************************************************************************/
32 
33 #include <visp3/core/vpCannyEdgeDetection.h>
34 
35 #include <visp3/core/vpImageConvert.h>
36 
37 #if (VISP_CXX_STANDARD == VISP_CXX_STANDARD_98) // Check if cxx98
38 namespace
39 {
40 // Helper to apply the scale to the raw values of the filters
41 template <typename FilterType>
42 static void scaleFilter(vpArray2D<FilterType> &filter, const float &scale)
43 {
44  const unsigned int nbRows = filter.getRows();
45  const unsigned int nbCols = filter.getCols();
46  for (unsigned int r = 0; r < nbRows; ++r) {
47  for (unsigned int c = 0; c < nbCols; ++c) {
48  filter[r][c] = filter[r][c] * scale;
49  }
50  }
51 }
52 };
53 #endif
54 
55 // // Initialization methods
56 
58  : m_filteringAndGradientType(vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING)
59  , m_gaussianKernelSize(3)
60  , m_gaussianStdev(1.f)
61  , m_areGradientAvailable(false)
62  , m_gradientFilterKernelSize(3)
63  , m_lowerThreshold(-1.f)
64  , m_lowerThresholdRatio(0.6f)
65  , m_upperThreshold(-1.f)
66  , m_upperThresholdRatio(0.8f)
67  , mp_mask(nullptr)
68 {
69  initGaussianFilters();
70  initGradientFilters();
71 }
72 
73 vpCannyEdgeDetection::vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev
74  , const unsigned int &sobelAperture, const float &lowerThreshold, const float &upperThreshold
75  , const float &lowerThresholdRatio, const float &upperThresholdRatio
77 )
78  : m_filteringAndGradientType(filteringType)
79  , m_gaussianKernelSize(gaussianKernelSize)
80  , m_gaussianStdev(gaussianStdev)
81  , m_areGradientAvailable(false)
82  , m_gradientFilterKernelSize(sobelAperture)
83  , m_lowerThreshold(lowerThreshold)
84  , m_lowerThresholdRatio(lowerThresholdRatio)
85  , m_upperThreshold(upperThreshold)
86  , m_upperThresholdRatio(upperThresholdRatio)
87  , mp_mask(nullptr)
88 {
89  initGaussianFilters();
90  initGradientFilters();
91 }
92 
93 #ifdef VISP_HAVE_NLOHMANN_JSON
94 
95 using json = nlohmann::json;
96 
97 vpCannyEdgeDetection::vpCannyEdgeDetection(const std::string &jsonPath)
98 {
99  initFromJSON(jsonPath);
100 }
101 
102 void
103 vpCannyEdgeDetection::initFromJSON(const std::string &jsonPath)
104 {
105  std::ifstream file(jsonPath);
106  if (!file.good()) {
107  std::stringstream ss;
108  ss << "Problem opening file " << jsonPath << ". Make sure it exists and is readable" << std::endl;
109  throw vpException(vpException::ioError, ss.str());
110  }
111  json j;
112  try {
113  j = json::parse(file);
114  }
115  catch (json::parse_error &e) {
116  std::stringstream msg;
117  msg << "Could not parse JSON file : \n";
118  msg << e.what() << std::endl;
119  msg << "Byte position of error: " << e.byte;
120  throw vpException(vpException::ioError, msg.str());
121  }
122  from_json(j, *this);
123  file.close();
124  initGaussianFilters();
125  initGradientFilters();
126 }
127 #endif
128 
129 void
130 vpCannyEdgeDetection::initGaussianFilters()
131 {
132  if ((m_gaussianKernelSize % 2) == 0) {
133  throw(vpException(vpException::badValue, "The Gaussian kernel size should be odd"));
134  }
135  m_fg.resize(1, (m_gaussianKernelSize + 1) / 2);
136  vpImageFilter::getGaussianKernel(m_fg.data, m_gaussianKernelSize, m_gaussianStdev, true);
137 }
138 
139 void
140 vpCannyEdgeDetection::initGradientFilters()
141 {
142  if ((m_gradientFilterKernelSize % 2) != 1) {
143  throw vpException(vpException::badValue, "Gradient filters kernel size should be odd.");
144  }
145  m_gradientFilterX.resize(m_gradientFilterKernelSize, m_gradientFilterKernelSize);
146  m_gradientFilterY.resize(m_gradientFilterKernelSize, m_gradientFilterKernelSize);
147 
148 #if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
149  auto scaleFilter = [](vpArray2D<float> &filter, const float &scale) {
150  unsigned int filter_rows = filter.getRows();
151  unsigned int filter_col = filter.getCols();
152  for (unsigned int r = 0; r < filter_rows; ++r) {
153  for (unsigned int c = 0; c < filter_col; ++c) {
154  filter[r][c] = filter[r][c] * scale;
155  }
156  }
157  };
158 #endif
159 
160  float scaleX = 1.f;
161  float scaleY = 1.f;
162 
163  if (m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING) {
164  scaleX = vpImageFilter::getSobelKernelX(m_gradientFilterX.data, (m_gradientFilterKernelSize - 1) / 2);
165  scaleY = vpImageFilter::getSobelKernelY(m_gradientFilterY.data, (m_gradientFilterKernelSize - 1) / 2);
166  }
167  else if (m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING) {
168  // Compute the Scharr filters
169  scaleX = vpImageFilter::getScharrKernelX(m_gradientFilterX.data, (m_gradientFilterKernelSize - 1) / 2);
170  scaleY = vpImageFilter::getScharrKernelY(m_gradientFilterY.data, (m_gradientFilterKernelSize - 1) / 2);
171  }
172  else {
173  std::string errMsg = "[vpCannyEdgeDetection::initGradientFilters] Error: gradient filtering method \"";
174  errMsg += vpImageFilter::vpCannyFilteringAndGradientTypeToString(m_filteringAndGradientType);
175  errMsg += "\" has not been implemented yet\n";
177  }
178 
179  scaleFilter(m_gradientFilterX, scaleX);
180  scaleFilter(m_gradientFilterY, scaleY);
181 }
182 
183 // // Detection methods
184 #ifdef HAVE_OPENCV_CORE
186 vpCannyEdgeDetection::detect(const cv::Mat &cv_I)
187 {
188  vpImage<unsigned char> I_gray;
189  vpImageConvert::convert(cv_I, I_gray);
190  return detect(I_gray);
191 }
192 #endif
193 
196 {
197  vpImage<unsigned char> I_gray;
198  vpImageConvert::convert(I_color, I_gray);
199  return detect(I_gray);
200 }
201 
204 {
205  // // Clearing the previous results
206  m_edgeMap.resize(I.getHeight(), I.getWidth(), 0);
207  m_edgeCandidateAndGradient.clear();
208  m_edgePointsCandidates.clear();
209 
210  // // Step 1 and 2: filter the image and compute the gradient, if not given by the user
211  if (!m_areGradientAvailable) {
212  performFilteringAndGradientComputation(I);
213  }
214  m_areGradientAvailable = false; // Reset for next call
215 
216  // // Step 3: edge thining
217  float upperThreshold = m_upperThreshold;
218  float lowerThreshold = m_lowerThreshold;
219  if (upperThreshold < 0) {
220  upperThreshold = vpImageFilter::computeCannyThreshold(I, lowerThreshold, &m_dIx, &m_dIy, m_gaussianKernelSize,
221  m_gaussianStdev, m_gradientFilterKernelSize, m_lowerThresholdRatio,
222  m_upperThresholdRatio, m_filteringAndGradientType, mp_mask);
223  }
224  else if (m_lowerThreshold < 0) {
225  // Applying Canny recommendation to have the upper threshold 3 times greater than the lower threshold.
226  lowerThreshold = m_upperThreshold / 3.f;
227  }
228  // To ensure that if lowerThreshold = 0, we reject null gradient points
229  lowerThreshold = std::max<float>(lowerThreshold, std::numeric_limits<float>::epsilon());
230  performEdgeThinning(lowerThreshold);
231 
232  // // Step 4: hysteresis thresholding
233  performHysteresisThresholding(lowerThreshold, upperThreshold);
234 
235  // // Step 5: edge tracking
236  performEdgeTracking();
237  return m_edgeMap;
238 }
239 
240 void
241 vpCannyEdgeDetection::performFilteringAndGradientComputation(const vpImage<unsigned char> &I)
242 {
243  if ((m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING)
244  || (m_filteringAndGradientType == vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING)) {
245  // Computing the Gaussian blur
246  vpImage<float> Iblur;
247  vpImage<float> GIx;
248  vpImageFilter::filterX<unsigned char, float>(I, GIx, m_fg.data, m_gaussianKernelSize, mp_mask);
249  vpImageFilter::filterY<float, float>(GIx, Iblur, m_fg.data, m_gaussianKernelSize, mp_mask);
250 
251  // Computing the gradients
252  vpImageFilter::filter(Iblur, m_dIx, m_gradientFilterX, true, mp_mask);
253  vpImageFilter::filter(Iblur, m_dIy, m_gradientFilterY, true, mp_mask);
254  }
255  else {
256  std::string errmsg("Currently, the filtering operation \"");
257  errmsg += vpImageFilter::vpCannyFilteringAndGradientTypeToString(m_filteringAndGradientType);
258  errmsg += "\" is not handled.";
260  }
261 }
262 
275 void
276 getInterpolationWeightsAndOffsets(const float &gradientOrientation,
277  float &alpha, float &beta,
278  int &dRowGradAlpha, int &dRowGradBeta,
279  int &dColGradAlpha, int &dColGradBeta
280 )
281 {
282  float thetaMin = 0.f;
283  if (gradientOrientation < M_PI_4f) {
284  // Angles between 0 and 45 deg rely on the horizontal and diagonal points
285  dColGradAlpha = 1;
286  dColGradBeta = 1;
287  dRowGradAlpha = 0;
288  dRowGradBeta = -1;
289  }
290  else if ((gradientOrientation >= M_PI_4f) && (gradientOrientation < M_PI_2f)) {
291  // Angles between 45 and 90 deg rely on the diagonal and vertical points
292  thetaMin = M_PI_4f;
293  dColGradAlpha = 1;
294  dColGradBeta = 0;
295  dRowGradAlpha = -1;
296  dRowGradBeta = -1;
297  }
298  else if ((gradientOrientation >= M_PI_2f) && (gradientOrientation < (3.f * M_PI_4f))) {
299  // Angles between 90 and 135 deg rely on the vertical and diagonal points
300  thetaMin = M_PI_2f;
301  dColGradAlpha = 0;
302  dColGradBeta = -1;
303  dRowGradAlpha = -1;
304  dRowGradBeta = -1;
305  }
306  else if ((gradientOrientation >= (3.f * M_PI_4f)) && (gradientOrientation < M_PIf)) {
307  // Angles between 135 and 180 deg rely on the vertical and diagonal points
308  thetaMin = 3.f * M_PI_4f;
309  dColGradAlpha = -1;
310  dColGradBeta = -1;
311  dRowGradAlpha = -1;
312  dRowGradBeta = 0;
313  }
314  beta = (gradientOrientation - thetaMin) / M_PI_4f;
315  alpha = 1.f - beta;
316 }
317 
327 float
328 getManhattanGradient(const vpImage<float> &dIx, const vpImage<float> &dIy, const int &row, const int &col)
329 {
330  float grad = 0.;
331  int nbRows = dIx.getRows();
332  int nbCols = dIx.getCols();
333  if ((row >= 0)
334  && (row < nbRows)
335  && (col >= 0)
336  && (col < nbCols)
337  ) {
338  float dx = dIx[row][col];
339  float dy = dIy[row][col];
340  grad = std::abs(dx) + std::abs(dy);
341  }
342  return grad;
343 }
344 
356 float
357 getGradientOrientation(const vpImage<float> &dIx, const vpImage<float> &dIy, const int &row, const int &col)
358 {
359  float gradientOrientation = 0.f;
360  float dx = dIx[row][col];
361  float dy = dIy[row][col];
362 
363  if (std::abs(dx) < std::numeric_limits<float>::epsilon()) {
364  gradientOrientation = M_PI_2f;
365  }
366  else {
367  // -dy because the y-axis of the image is oriented towards the bottom of the screen
368  // while we later work with a y-axis oriented towards the top when getting the theta quadrant.
369  gradientOrientation = static_cast<float>(std::atan2(-dy, dx));
370  if (gradientOrientation < 0.f) {
371  gradientOrientation += M_PIf; // + M_PI in order to be between 0 and M_PIf
372  }
373  }
374  return gradientOrientation;
375 }
376 
377 void
378 vpCannyEdgeDetection::performEdgeThinning(const float &lowerThreshold)
379 {
380  int nbRows = m_dIx.getRows();
381  int nbCols = m_dIx.getCols();
382 
383  for (int row = 0; row < nbRows; ++row) {
384  for (int col = 0; col < nbCols; ++col) {
385  if (mp_mask != nullptr) {
386  if (!(*mp_mask)[row][col]) {
387  // The mask tells us to ignore the current pixel
388  continue;
389  }
390  }
391 
392  // Computing the gradient orientation and magnitude
393  float grad = getManhattanGradient(m_dIx, m_dIy, row, col);
394 
395  if (grad < lowerThreshold) {
396  // The gradient is lower than minimum threshold => ignoring the point
397  continue;
398  }
399 
400  // Getting the offset along the horizontal and vertical axes
401  // depending on the gradient orientation
402  int dRowAlphaPlus = 0, dRowBetaPlus = 0;
403  int dColAphaPlus = 0, dColBetaPlus = 0;
404  float gradientOrientation = getGradientOrientation(m_dIx, m_dIy, row, col);
405  float alpha = 0.f, beta = 0.f;
406  getInterpolationWeightsAndOffsets(gradientOrientation, alpha, beta, dRowAlphaPlus, dRowBetaPlus, dColAphaPlus, dColBetaPlus);
407  int dRowAlphaMinus = -dRowAlphaPlus, dRowBetaMinus = -dRowBetaPlus;
408  int dColAphaMinus = -dColAphaPlus, dColBetaMinus = -dColBetaPlus;
409  float gradAlphaPlus = getManhattanGradient(m_dIx, m_dIy, row + dRowAlphaPlus, col + dColAphaPlus);
410  float gradBetaPlus = getManhattanGradient(m_dIx, m_dIy, row + dRowBetaPlus, col + dColBetaPlus);
411  float gradAlphaMinus = getManhattanGradient(m_dIx, m_dIy, row + dRowAlphaMinus, col + dColAphaMinus);
412  float gradBetaMinus = getManhattanGradient(m_dIx, m_dIy, row + dRowBetaMinus, col + dColBetaMinus);
413  float gradPlus = (alpha * gradAlphaPlus) + (beta * gradBetaPlus);
414  float gradMinus = (alpha * gradAlphaMinus) + (beta * gradBetaMinus);
415 
416  if ((grad >= gradPlus) && (grad >= gradMinus)) {
417  // Keeping the edge point that has the highest gradient
418  std::pair<unsigned int, unsigned int> bestPixel(row, col);
419  m_edgeCandidateAndGradient[bestPixel] = grad;
420  }
421  }
422  }
423 }
424 
425 void
426 vpCannyEdgeDetection::performHysteresisThresholding(const float &lowerThreshold, const float &upperThreshold)
427 {
428  std::map<std::pair<unsigned int, unsigned int>, float>::iterator it;
429  for (it = m_edgeCandidateAndGradient.begin(); it != m_edgeCandidateAndGradient.end(); ++it) {
430  if (it->second >= upperThreshold) {
431  m_edgePointsCandidates[it->first] = STRONG_EDGE;
432  }
433  else if ((it->second >= lowerThreshold) && (it->second < upperThreshold)) {
434  m_edgePointsCandidates[it->first] = WEAK_EDGE;
435  }
436  }
437 }
438 
439 void
440 vpCannyEdgeDetection::performEdgeTracking()
441 {
442  std::map<std::pair<unsigned int, unsigned int>, EdgeType>::iterator it;
443  for (it = m_edgePointsCandidates.begin(); it != m_edgePointsCandidates.end(); ++it) {
444  if (it->second == STRONG_EDGE) {
445  m_edgeMap[it->first.first][it->first.second] = 255;
446  }
447  else if (it->second == WEAK_EDGE) {
448  if (recursiveSearchForStrongEdge(it->first)) {
449  m_edgeMap[it->first.first][it->first.second] = 255;
450  }
451  }
452  }
453 }
454 
455 bool
456 vpCannyEdgeDetection::recursiveSearchForStrongEdge(const std::pair<unsigned int, unsigned int> &coordinates)
457 {
458  bool hasFoundStrongEdge = false;
459  int nbRows = m_dIx.getRows();
460  int nbCols = m_dIx.getCols();
461  m_edgePointsCandidates[coordinates] = ON_CHECK;
462  for (int dr = -1; (dr <= 1) && (!hasFoundStrongEdge); ++dr) {
463  for (int dc = -1; (dc <= 1) && (!hasFoundStrongEdge); ++dc) {
464  int idRow = dr + static_cast<int>(coordinates.first);
465  idRow = std::max<int>(idRow, 0); // Avoid getting negative pixel ID
466  int idCol = dc + static_cast<int>(coordinates.second);
467  idCol = std::max<int>(idCol, 0); // Avoid getting negative pixel ID
468 
469  // Checking if we are still looking for an edge in the limit of the image
470  if (((idRow < 0) || (idRow >= nbRows))
471  || ((idCol < 0) || (idCol >= nbCols))
472  || ((dr == 0) && (dc == 0))
473  ) {
474  continue;
475  }
476 
477  try {
478  std::pair<unsigned int, unsigned int> key_candidate(idRow, idCol);
479  // Checking if the 8-neighbor point is in the list of edge candidates
480  EdgeType type_candidate = m_edgePointsCandidates.at(key_candidate);
481  if (type_candidate == STRONG_EDGE) {
482  // The 8-neighbor point is a strong edge => the weak edge becomes a strong edge
483  hasFoundStrongEdge = true;
484  }
485  else if (type_candidate == WEAK_EDGE) {
486  // Checking if the WEAK_EDGE neighbor has a STRONG_EDGE neighbor
487  hasFoundStrongEdge = recursiveSearchForStrongEdge(key_candidate);
488  }
489  }
490  catch (...) {
491  continue;
492  }
493  }
494  }
495  if (hasFoundStrongEdge) {
496  m_edgePointsCandidates[coordinates] = STRONG_EDGE;
497  m_edgeMap[coordinates.first][coordinates.second] = 255;
498  }
499  return hasFoundStrongEdge;
500 }
Implementation of a generic 2D array used as base class for matrices and vectors.
Definition: vpArray2D.h:126
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
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.
friend void from_json(const nlohmann::json &j, vpCannyEdgeDetection &detector)
Read the detector configuration from JSON. All values are optional and if an argument is not present,...
void initFromJSON(const std::string &jsonPath)
Initialize all the algorithm parameters using the JSON file whose path is jsonPath....
vpCannyEdgeDetection()
Default constructor of the vpCannyEdgeDetection class. The thresholds used during the hysteresis thre...
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
@ notImplementedError
Not implemented.
Definition: vpException.h:81
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
Various image filter, convolution, etc...
Definition: vpImageFilter.h:70
static FilterType getSobelKernelX(FilterType *filter, unsigned int size)
vpCannyFilteringAndGradientType
Canny filter and gradient operators to apply on the image before the edge detection stage.
@ 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.
static float computeCannyThreshold(const cv::Mat &cv_I, const cv::Mat *p_cv_dIx, const cv::Mat *p_cv_dIy, float &lowerThresh, const unsigned int &gaussianKernelSize=5, const float &gaussianStdev=2.f, const unsigned int &apertureGradient=3, const float &lowerThresholdRatio=0.6, const float &upperThresholdRatio=0.8, const vpCannyFilteringAndGradientType &filteringType=CANNY_GBLUR_SOBEL_FILTERING)
Compute the upper Canny edge filter threshold, using Gaussian blur + Sobel or + Scharr operators to c...
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)
unsigned int getWidth() const
Definition: vpImage.h:245
void resize(unsigned int h, unsigned int w)
resize the image : Image initialization
Definition: vpImage.h:783
unsigned int getCols() const
Definition: vpImage.h:175
unsigned int getHeight() const
Definition: vpImage.h:184
unsigned int getRows() const
Definition: vpImage.h:215