Visual Servoing Platform  version 3.6.1 under development (2025-02-18)
tutorial-canny.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 
32 
33 #include <visp3/core/vpConfig.h>
34 
35 #include <visp3/core/vpCannyEdgeDetection.h>
36 #include <visp3/core/vpImageFilter.h>
37 #include <visp3/io/vpImageIo.h>
38 
39 #ifdef HAVE_OPENCV_IMGPROC
40 #include <opencv2/imgproc/imgproc.hpp>
41 #endif
42 
43 #include "drawingHelpers.h"
44 
45 #ifdef ENABLE_VISP_NAMESPACE
46 using namespace VISP_NAMESPACE_NAME;
47 #endif
48 
49 typedef struct SoftwareArguments
50 {
51  std::string m_img;
64 
66  : m_img("")
67  , m_gradientOutsideClass(false)
68  , m_useVpImageFilterCanny(false)
69  , m_saveEdgeList(false)
70  , m_gaussianKernelSize(3)
71  , m_apertureSize(3)
72  , m_filteringType(vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING)
73  , m_gaussianStdev(1.)
74  , m_lowerThresh(-1.)
75  , m_upperThresh(-1.)
76  , m_lowerThreshRatio(0.6f)
77  , m_upperThreshRatio(0.8f)
78 #ifdef VISP_HAVE_OPENCV
79  , m_backend(vpImageFilter::CANNY_OPENCV_BACKEND)
80 #else
81  , m_backend(vpImageFilter::CANNY_VISP_BACKEND)
82 #endif
83  { }
85 
86 template <class T>
87 void computeMeanMaxStdev(const vpImage<T> &I, float &mean, float &max, float &stdev)
88 {
89  max = std::numeric_limits<float>::epsilon();
90  mean = 0.;
91  stdev = 0.;
92  unsigned int nbRows = I.getRows();
93  unsigned int nbCols = I.getCols();
94  float scale = 1.f / (static_cast<float>(nbRows) * static_cast<float>(nbCols));
95  for (unsigned int r = 0; r < nbRows; r++) {
96  for (unsigned int c = 0; c < nbCols; c++) {
97  mean += I[r][c];
98  max = std::max<float>(max, static_cast<float>(I[r][c]));
99  }
100  }
101  mean *= scale;
102  for (unsigned int r = 0; r < nbRows; r++) {
103  for (unsigned int c = 0; c < nbCols; c++) {
104  stdev += (I[r][c] - mean) * (I[r][c] - mean);
105  }
106  }
107  stdev *= scale;
108  stdev = std::sqrt(stdev);
109 }
110 
111 void setGradientOutsideClass(const vpImage<unsigned char> &I, const int &gaussianKernelSize, const float &gaussianStdev,
112  vpCannyEdgeDetection &cannyDetector, const unsigned int apertureSize,
114  vpImage<unsigned char> &dIx_uchar, vpImage<unsigned char> &dIy_uchar
115 )
116 {
117  // Computing the gradients
118  vpImage<float> dIx, dIy;
119  vpImageFilter::computePartialDerivatives(I, dIx, dIy, true, true, true, gaussianKernelSize, gaussianStdev,
120  apertureSize, filteringType);
121 
122  // Set the gradients of the vpCannyEdgeDetection
123  cannyDetector.setGradients(dIx, dIy);
124 
125  // Display the gradients
126  float mean, max, stdev;
127  computeMeanMaxStdev(dIx, mean, max, stdev);
128 
129 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
130  std::string title = "Gradient along the horizontal axis. Mean = " + std::to_string(mean)
131  + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max);
132 #else
133  std::string title;
134  {
135  std::stringstream ss;
136  ss << "Gradient along the horizontal axis. Mean = " << mean<< "+/-" << stdev<< " Max = " << max;
137  title = ss.str();
138  }
139 #endif
140  vpImageConvert::convert(dIx, dIx_uchar);
141  drawingHelpers::display(dIx_uchar, title);
142  computeMeanMaxStdev(dIy, mean, max, stdev);
143 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
144  title = "Gradient along the horizontal axis. Mean = " + std::to_string(mean)
145  + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max);
146 #else
147  {
148  std::stringstream ss;
149  ss << "Gradient along the horizontal axis. Mean = " << mean<< "+/-" << stdev<< " Max = " << max;
150  title = ss.str();
151  }
152 #endif
153  vpImageConvert::convert(dIy, dIy_uchar);
154  drawingHelpers::display(dIy_uchar, title);
155 }
156 
157 bool sortImagePoints(const vpImagePoint &a, const vpImagePoint &b)
158 {
159  if (a.get_i() < b.get_i()) {
160  return true;
161  }
162  else if (vpMath::equal(a.get_i(), b.get_i()) && (a.get_j() < b.get_j())) {
163  return true;
164  }
165  return false;
166 }
167 
168 void checkEdgeList(const vpCannyEdgeDetection &cannyDetector, const vpImage<unsigned char> &I_canny_visp)
169 {
170  std::vector<vpImagePoint> listEdgePoints = cannyDetector.getEdgePointsList();
171  // Check if the edge points are uniquely present in the edge lists
172  std::vector<vpImagePoint> cpyListEdgePoints = listEdgePoints;
173  std::sort(cpyListEdgePoints.begin(), cpyListEdgePoints.end(), sortImagePoints);
174  std::vector<vpImagePoint>::iterator last = std::unique(cpyListEdgePoints.begin(), cpyListEdgePoints.end());
175  static_cast<void>(cpyListEdgePoints.erase(last, cpyListEdgePoints.end()));
176  if (listEdgePoints.size() != cpyListEdgePoints.size()) {
177  throw(vpException(vpException::fatalError, "There are duplicated points in the edge points list !"));
178  }
179  // Check if all the edge points in the list have been set in the image
180  std::vector<vpImagePoint>::iterator start = listEdgePoints.begin();
181  std::vector<vpImagePoint>::iterator stop = listEdgePoints.end();
182  for (std::vector<vpImagePoint>::iterator it = start; it != stop; ++it) {
183  if (I_canny_visp[static_cast<unsigned int>(it->get_i())][static_cast<unsigned int>(it->get_j())] != 255) {
184  throw(vpException(vpException::fatalError, "A point of the edge-point list has not been set in the image !"));
185  }
186  }
187  // Check if all the edge points in the image have been set in the list
188  unsigned int nbRows = I_canny_visp.getRows();
189  unsigned int nbCols = I_canny_visp.getCols();
190  for (unsigned int i = 0; i < nbRows; ++i) {
191  for (unsigned int j = 0; j < nbCols; ++j) {
192  if (I_canny_visp[i][j] == 255) {
193  vpImagePoint searchedPoint(i, j);
194  std::vector<vpImagePoint>::iterator idx = std::find(listEdgePoints.begin(), listEdgePoints.end(), searchedPoint);
195  if (idx == listEdgePoints.end()) {
196  throw(vpException(vpException::fatalError, "A point of the image has not been set in the edge-point list !"));
197  }
198  }
199  }
200  }
201 
202  std::cout << "All the edge-point list tests went well !" << std::endl;
203 }
204 
205 void usage(const std::string &softName, const SoftwareArguments &options)
206 {
207  std::cout << "NAME" << std::endl;
208  std::cout << softName << ": software to test the vpCannyEdgeComputation class and vpImageFilter::canny method" << std::endl;
209  std::cout << "SYNOPSIS" << std::endl;
210  std::cout << "\t" << softName
211  << " [-i, --image <pathToImg>]"
212  << " [-g, --gradient <kernelSize stdev>]"
213  << " [-t, --thresh <lowerThresh upperThresh>]"
214  << " [-a, --aperture <apertureSize>]"
215  << " [-f, --filter <filterName>]"
216  << " [-r, --ratio <lowerThreshRatio upperThreshRatio>]"
217  << " [-b, --backend <backendName>]"
218  << " [-e, --edge-list]" << std::endl
219  << " [-h, --help]" << std::endl
220  << std::endl;
221  std::cout << "DESCRIPTION" << std::endl;
222  std::cout << "\t-i, --image <pathToImg>" << std::endl
223  << "\t\tPermits to load an image on which will be tested the vpCanny class." << std::endl
224  << "\t\tWhen empty uses a simulated image." << std::endl
225  << std::endl;
226  std::cout << "\t-g, --gradient <kernelSize stdev>" << std::endl
227  << "\t\tPermits to compute the gradients of the image outside the vpCanny class." << std::endl
228  << "\t\tFirst parameter is the size of the Gaussian kernel used to compute the gradients." << std::endl
229  << "\t\tSecond parameter is the standard deviation of the Gaussian kernel used to compute the gradients." << std::endl
230  << "\t\tDefault: " << options.m_gaussianKernelSize << " " << options.m_gaussianStdev << std::endl
231  << std::endl;
232  std::cout << "\t-t, --thresh <lowerThresh upperThresh>" << std::endl
233  << "\t\tPermits to set the lower and upper thresholds of the vpCanny class." << std::endl
234  << "\t\tFirst parameter is the lower threshold." << std::endl
235  << "\t\tSecond parameter is the upper threshold." << std::endl
236  << "\t\tWhen set to -1 thresholds are computed automatically." << std::endl
237  << "\t\tDefault: " << options.m_lowerThresh << " " << options.m_upperThresh << std::endl
238  << std::endl;
239  std::cout << "\t-a, --aperture <apertureSize>" << std::endl
240  << "\t\tPermits to set the size of the gradient filter kernel." << std::endl
241  << "\t\tParameter must be odd and positive." << std::endl
242  << "\t\tDefault: " << options.m_apertureSize << std::endl
243  << std::endl;
244  std::cout << "\t-f, --filter <filterName>" << std::endl
245  << "\t\tPermits to choose the type of filter to apply to compute the gradient." << std::endl
246  << "\t\tAvailable values: " << vpImageFilter::vpGetCannyFiltAndGradTypes("<", " | ", ">") << std::endl
247  << "\t\tDefault: " << vpImageFilter::vpCannyFiltAndGradTypeToStr(options.m_filteringType) << std::endl
248  << std::endl;
249  std::cout << "\t-r, --ratio <lowerThreshRatio upperThreshRatio>" << std::endl
250  << "\t\tPermits to set the lower and upper thresholds ratio of the vpCanny class." << std::endl
251  << "\t\tFirst parameter is the lower threshold ratio." << std::endl
252  << "\t\tSecond parameter is the upper threshold ratio." << std::endl
253  << "\t\tDefault: " << options.m_lowerThreshRatio << " " << options.m_upperThreshRatio << std::endl
254  << std::endl;
255  std::cout << "\t-b, --backend <backendName>" << std::endl
256  << "\t\tPermits to use the vpImageFilter::canny method for comparison." << std::endl
257  << "\t\tAvailable values: " << vpImageFilter::vpCannyBackendTypeList("<", " | ", ">") << std::endl
258  << "\t\tDefault: " << vpImageFilter::vpCannyBackendTypeToString(options.m_backend) << std::endl
259  << std::endl;
260  std::cout << "\t-e, --edge-list" << std::endl
261  << "\t\tPermits to save the edge list." << std::endl
262  << "\t\tDefault: OFF" << std::endl
263  << std::endl;
264  std::cout << "\t-h, --help" << std::endl
265  << "\t\tPermits to display the different arguments this software handles." << std::endl
266  << std::endl;
267 }
268 
269 int main(int argc, const char *argv[])
270 {
271  SoftwareArguments options;
272  for (int i = 1; i < argc; i++) {
273  std::string argv_str = std::string(argv[i]);
274  if ((argv_str == "-i" || argv_str == "--image") && i + 1 < argc) {
275  options.m_img = std::string(argv[i + 1]);
276  i++;
277  }
278  else if ((argv_str == "-g" || argv_str == "--gradient") && i + 2 < argc) {
279  options.m_gradientOutsideClass = true;
280  options.m_gaussianKernelSize = atoi(argv[i + 1]);
281  options.m_gaussianStdev = static_cast<float>(atof(argv[i + 2]));
282  i += 2;
283  }
284  else if ((argv_str == "-t" || argv_str == "--thresh") && i + 2 < argc) {
285  options.m_lowerThresh = static_cast<float>(atof(argv[i + 1]));
286  options.m_upperThresh = static_cast<float>(atof(argv[i + 2]));
287  i += 2;
288  }
289  else if ((argv_str == "-a" || argv_str == "--aperture") && i + 1 < argc) {
290  options.m_apertureSize = std::atoi(argv[i + 1]);
291  i++;
292  }
293  else if ((argv_str == "-f" || argv_str == "--filter") && i + 1 < argc) {
294  options.m_filteringType = vpImageFilter::vpCannyFiltAndGradTypeFromStr(std::string(argv[i + 1]));
295  i++;
296  }
297  else if ((argv_str == "-r" || argv_str == "--ratio") && i + 2 < argc) {
298  options.m_lowerThreshRatio = static_cast<float>(std::atof(argv[i + 1]));
299  options.m_upperThreshRatio = static_cast<float>(std::atof(argv[i + 2]));
300  i += 2;
301  }
302  else if ((argv_str == "-b" || argv_str == "--backend") && i + 1 < argc) {
303  options.m_useVpImageFilterCanny = true;
304  options.m_backend = vpImageFilter::vpCannyBackendTypeFromString(std::string(argv[i+1]));
305  i++;
306  }
307  else if ((argv_str == "-e") || (argv_str == "--edge-list")) {
308  options.m_saveEdgeList = true;
309  }
310  else if (argv_str == "-h" || argv_str == "--help") {
311  usage(std::string(argv[0]), options);
312  return EXIT_SUCCESS;
313  }
314  else {
315  std::cerr << "Argument \"" << argv_str << "\" is unknown." << std::endl;
316  return EXIT_FAILURE;
317  }
318  }
319 
320  std::string configAsTxt("Canny Configuration:\n");
321  configAsTxt += "\tFiltering + gradient operators = " + vpImageFilter::vpCannyFiltAndGradTypeToStr(options.m_filteringType) + "\n";
322 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
323  configAsTxt += "\tGaussian filter kernel size = " + std::to_string(options.m_gaussianKernelSize) + "\n";
324  configAsTxt += "\tGaussian filter standard deviation = " + std::to_string(options.m_gaussianStdev) + "\n";
325  configAsTxt += "\tGradient filter kernel size = " + std::to_string(options.m_apertureSize) + "\n";
326  configAsTxt += "\tCanny edge filter thresholds = [" + std::to_string(options.m_lowerThresh) + " ; " + std::to_string(options.m_upperThresh) + "]\n";
327  configAsTxt += "\tCanny edge filter thresholds ratio (for auto-thresholding) = [" + std::to_string(options.m_lowerThreshRatio) + " ; " + std::to_string(options.m_upperThreshRatio) + "]\n";
328 #else
329  {
330  std::stringstream ss;
331  ss << "\tGaussian filter kernel size = " << options.m_gaussianKernelSize << "\n";
332  ss << "\tGaussian filter standard deviation = " << options.m_gaussianStdev << "\n";
333  ss << "\tGradient filter kernel size = " << options.m_apertureSize << "\n";
334  ss << "\tCanny edge filter thresholds = [" << options.m_lowerThresh << " ; " << options.m_upperThresh << "]\n";
335  ss << "\tCanny edge filter thresholds ratio (for auto-thresholding) = [" << options.m_lowerThreshRatio << " ; " << options.m_upperThreshRatio << "]\n";
336  configAsTxt += ss.str();
337  }
338 #endif
339  std::cout << configAsTxt << std::endl;
340 
341  vpCannyEdgeDetection cannyDetector(options.m_gaussianKernelSize, options.m_gaussianStdev, options.m_apertureSize,
342  options.m_lowerThresh, options.m_upperThresh, options.m_lowerThreshRatio, options.m_upperThreshRatio,
343  options.m_filteringType, options.m_saveEdgeList);
344  vpImage<unsigned char> I_canny_input, I_canny_visp, dIx_uchar, dIy_uchar, I_canny_imgFilter;
345  if (!options.m_img.empty()) {
346  // Detection on the user image
347  vpImageIo::read(I_canny_input, options.m_img);
348  }
349  else {
350  // Detection on a fake image of a square
351  I_canny_input.resize(500, 500, 0);
352  for (unsigned int r = 150; r < 350; r++) {
353  for (unsigned int c = 150; c < 350; c++) {
354  I_canny_input[r][c] = 125;
355  }
356  }
357  }
358 
359  // Initialization of the displays
360  I_canny_visp = I_canny_imgFilter = dIx_uchar = dIy_uchar = I_canny_input;
361  vpImage<unsigned char> *p_dIx = nullptr, *p_dIy = nullptr, *p_IcannyImgFilter = nullptr;
362 
363  if (options.m_gradientOutsideClass) {
364  p_dIx = &dIx_uchar;
365  p_dIy = &dIy_uchar;
366  }
367 
368  if (options.m_useVpImageFilterCanny) {
369  p_IcannyImgFilter = &I_canny_imgFilter;
370  }
371  drawingHelpers::init(I_canny_input, I_canny_visp, p_dIx, p_dIy, p_IcannyImgFilter);
372 
373  // Computing the gradient outside the vpCannyEdgeDetection class if asked
374  if (options.m_gradientOutsideClass) {
375  setGradientOutsideClass(I_canny_input, options.m_gaussianKernelSize, options.m_gaussianStdev, cannyDetector, options.m_apertureSize,
376  options.m_filteringType, dIx_uchar, dIy_uchar);
377  }
378  I_canny_visp = cannyDetector.detect(I_canny_input);
379  float mean, max, stdev;
380  computeMeanMaxStdev(I_canny_input, mean, max, stdev);
381  if (options.m_saveEdgeList) {
382  checkEdgeList(cannyDetector, I_canny_visp);
383  }
384 #if (VISP_CXX_STANDARD > VISP_CXX_STANDARD_98)
385  std::string title("Input of the Canny edge detector. Mean = " + std::to_string(mean) + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max));
386 #else
387  std::string title;
388  {
389  std::stringstream ss;
390  ss << "Input of the Canny edge detector. Mean = " << mean << "+/-" << stdev << " Max = " << max;
391  title = ss.str();
392  }
393 #endif
394  drawingHelpers::display(I_canny_input, title);
395  drawingHelpers::display(I_canny_visp, "Canny results on image " + options.m_img);
396 
397  if (options.m_useVpImageFilterCanny) {
398  float cannyThresh = options.m_upperThresh;
399  float lowerThresh(options.m_lowerThresh);
400  vpImageFilter::canny(I_canny_input, I_canny_imgFilter, options.m_gaussianKernelSize, lowerThresh, cannyThresh,
401  options.m_apertureSize, options.m_gaussianStdev, options.m_lowerThreshRatio, options.m_upperThreshRatio, true,
402  options.m_backend, options.m_filteringType);
403  drawingHelpers::display(I_canny_imgFilter, "Canny results with \"" + vpImageFilter::vpCannyBackendTypeToString(options.m_backend) + "\" backend");
404  }
405 
406  drawingHelpers::waitForClick(I_canny_input, true);
407  return EXIT_SUCCESS;
408 }
Class that implements the Canny's edge detector. It is possible to use a boolean mask to ignore some ...
vpImage< unsigned char > detect(const vpImage< vpRGBa > &I_color)
Detect the edges in an image. Convert the color image into a gray-scale image.
std::vector< vpImagePoint > getEdgePointsList() const
Get the list of edge-points that have been detected.
void setGradients(const vpImage< float > &dIx, const vpImage< float > &dIy)
Set the Gradients of the image that will be processed.
error that can be emitted by ViSP classes.
Definition: vpException.h:60
@ fatalError
Fatal error.
Definition: vpException.h:72
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
Various image filter, convolution, etc...
Definition: vpImageFilter.h:73
static std::string vpCannyBackendTypeToString(const vpCannyBackendType &type)
Cast a vpImageFilter::vpCannyBackendTypeToString into a string, to know its name.
static void canny(const vpImage< unsigned char > &I, vpImage< unsigned char > &Ic, const unsigned int &gaussianFilterSize, const float &thresholdCanny, const unsigned int &apertureSobel)
static std::string vpCannyBackendTypeList(const std::string &pref="<", const std::string &sep=" , ", const std::string &suf=">")
Get the list of available vpCannyBackendType.
static std::string vpCannyFiltAndGradTypeToStr(const vpCannyFilteringAndGradientType &type)
Cast a vpImageFilter::vpCannyFilteringAndGradientType into a string, to know its name.
vpCannyFilteringAndGradientType
Canny filter and gradient operators to apply on the image before the edge detection stage.
Definition: vpImageFilter.h:92
vpCannyBackendType
Canny filter backends for the edge detection operations.
Definition: vpImageFilter.h:77
static vpCannyFilteringAndGradientType vpCannyFiltAndGradTypeFromStr(const std::string &name)
Cast a string into a vpImageFilter::vpCannyFilteringAndGradientType.
static vpCannyBackendType vpCannyBackendTypeFromString(const std::string &name)
Cast a string into a vpImageFilter::vpCannyBackendTypeToString.
static std::string vpGetCannyFiltAndGradTypes(const std::string &pref="<", const std::string &sep=" , ", const std::string &suf=">")
Get the list of available vpCannyFilteringAndGradientType.
static void computePartialDerivatives(const cv::Mat &cv_I, cv::Mat &cv_dIx, cv::Mat &cv_dIy, const bool &computeDx=true, const bool &computeDy=true, const bool &normalize=true, const unsigned int &gaussianKernelSize=5, const float &gaussianStdev=2.f, const unsigned int &apertureGradient=3, const vpCannyFilteringAndGradientType &filteringType=CANNY_GBLUR_SOBEL_FILTERING)
Compute the partial derivatives (i.e. horizontal and vertical gradients) of the input image.
static void read(vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Definition: vpImageIo.cpp:147
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_j() const
Definition: vpImagePoint.h:125
double get_i() const
Definition: vpImagePoint.h:114
Definition of the vpImage class member functions.
Definition: vpImage.h:131
void resize(unsigned int h, unsigned int w)
resize the image : Image initialization
Definition: vpImage.h:544
unsigned int getCols() const
Definition: vpImage.h:171
unsigned int getRows() const
Definition: vpImage.h:212
static bool equal(double x, double y, double threshold=0.001)
Definition: vpMath.h:459
vpImageFilter::vpCannyFilteringAndGradientType m_filteringType
vpImageFilter::vpCannyBackendType m_backend