From c3eb4a1565ad921c89e764b99b31b403b00b081a Mon Sep 17 00:00:00 2001 From: rlagneau Date: Fri, 11 Aug 2023 15:32:49 +0200 Subject: [PATCH 1/8] [CORPS] Added a ViSP implementation of the Canny edge detector, that does not rely on OpenCV imgproc --- .../image/tutorial-image-filtering.dox | 25 +- .../include/visp3/core/vpCannyEdgeDetection.h | 287 +++++++++++++ .../core/include/visp3/core/vpImageFilter.h | 36 +- .../core/src/image/vpCannyEdgeDetection.cpp | 391 ++++++++++++++++++ modules/core/src/image/vpImageFilter.cpp | 141 +++---- tutorial/image/tutorial-image-filter.cpp | 8 +- 6 files changed, 784 insertions(+), 104 deletions(-) create mode 100644 modules/core/include/visp3/core/vpCannyEdgeDetection.h create mode 100644 modules/core/src/image/vpCannyEdgeDetection.cpp diff --git a/doc/tutorial/image/tutorial-image-filtering.dox b/doc/tutorial/image/tutorial-image-filtering.dox index 2652b26e42..4e6f3aa952 100644 --- a/doc/tutorial/image/tutorial-image-filtering.dox +++ b/doc/tutorial/image/tutorial-image-filtering.dox @@ -19,25 +19,25 @@ Let us consider the following source code that comes from tutorial-image-filter. \include tutorial-image-filter.cpp -Once build, you should have \c tutorial-image-filter binary. It shows how to apply different filters on an input image. Here we will consider monkey.pgm as input image. +Once build, you should have \c tutorial-image-filter binary. It shows how to apply different filters on an input image. Here we will consider monkey.pgm as input image. \image html img-monkey-gray.png To see the resulting filtered images, just run: -\code +\code ./tutorial-image-filter monkey.pgm -\endcode +\endcode The following sections give a line by line explanation of the source code dedicated to image filtering capabilities. - + \section blur Gaussian blur -Monkey input image is read from disk and is stored in \c I which is a gray level image declared as +Monkey input image is read from disk and is stored in \c I which is a gray level image declared as \snippet tutorial-image-filter.cpp vpImage construction -To apply a Gaussian blur to this image we first have to declare a resulting floating-point image \c F. Then the blurred image could be obtained using the default Gaussian filter: +To apply a Gaussian blur to this image we first have to declare a resulting floating-point image \c F. Then the blurred image could be obtained using the default Gaussian filter: \snippet tutorial-image-filter.cpp Gaussian blur @@ -47,9 +47,9 @@ The resulting image is the following: It is also possible to specify the Gaussian filter kernel size and the Gaussian standard deviation (sigma) using: -\code +\code vpImageFilter::gaussianBlur(I, F, 7, 2); // Kernel size: 7, sigma: 2 -\endcode +\endcode We thus obtain the following image: @@ -71,7 +71,8 @@ The resulting floating-point images \c dIx, \c dIy are the following: \section canny Canny edge detector -Canny edge detector function is only available if ViSP was build with OpenCV 2.1 or higher. +Canny edge detector function relies on OpenCV if ViSP was build with OpenCV 2.1 or higher. Otherwise, +it relies on the ViSP implementation vpCannyEdgeDetector class. After the declaration of a new image container \c C, Canny edge detector is applied using: \snippet tutorial-image-filter.cpp Canny @@ -82,10 +83,10 @@ Where: - 3: is the size of the Sobel kernel used internally. The resulting image \c C is the following: - + \image html img-monkey-canny.png -\section convolution Convolution +\section convolution Convolution To apply a convolution to an image, we first have to define a kernel. For example, let us consider the 3x3 Sobel kernel defined in \c K. @@ -104,7 +105,7 @@ For example, let us consider the 3x3 Sobel kernel defined in \c K. \snippet tutorial-image-filter.cpp Convolution kernel -After the declaration of a new floating-point image \c Gx, the convolution is obtained using: +After the declaration of a new floating-point image \c Gx, the convolution is obtained using: \snippet tutorial-image-filter.cpp Convolution The content of the filtered image \c Gx is the following. diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h new file mode 100644 index 0000000000..d12d833144 --- /dev/null +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -0,0 +1,287 @@ +/**************************************************************************** + * + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * +*****************************************************************************/ + +#ifndef _vpCannyEdgeDetection_h_ +#define _vpCannyEdgeDetection_h_ + +// System includes +#include +#include + +// ViSP include +#include +#include +#include + +// 3rd parties include +#ifdef VISP_HAVE_NLOHMANN_JSON +#include +using json = nlohmann::json; +#endif + +class vpCannyEdgeDetection +{ +private: + typedef enum EdgeType + { + STRONG_EDGE, + WEAK_EDGE, + ON_CHECK + } EdgeType; + + // // Gaussian smoothing attributes + int m_gaussianKernelSize; /*!< Size of the Gaussian filter kernel used to smooth the input image. Must be an odd number.*/ + float m_gaussianStdev; /*!< Standard deviation of the Gaussian filter.*/ + + // // Gradient computation attributes + bool m_areGradientAvailable; /*!< Set to true if the user gave the gradient images, false otherwise. In the later case, the class will compute the gradients.*/ + vpArray2D m_fg; /*!< Array that contains the Gaussian kernel.*/ + vpArray2D m_fgDg; /*!< Array that contains the derivative of the Gaussian kernel.*/ + vpImage m_dIx; /*!< X-axis gradient.*/ + vpImage m_dIy; /*!< Y-axis gradient.*/ + + // // Edge thining attributes + std::map, float> m_edgeCandidateAndGradient; /*!< Map that contains point image coordinates and corresponding gradient value.*/ + + // // Histeresis thresholding attributes + float m_lowerThreshold; /*!< Lower threshold for the histeresis step. If negative, it will be deduced as from m_upperThreshold. */ + float m_upperThreshold; /*!< Upper threshold for the histeresis step.*/ + + // // Edge tracking attributes + std::map, EdgeType> m_edgePointsCandidates; /*!< Map that contains the strong edge points, i.e. the points for which we know for sure they are edge points, + and the weak edge points, i.e. the points for which we still must determine if they are actual edge points.*/ + vpImage m_edgeMap; /*!< Final edge map that results from the whole Canny algorithm.*/ + + /** @name Constructors and initialization */ + //@{ + /** + * \brief Initialize the Gaussian filters used to filter the input image and + * to compute its gradients. + */ + void initGaussianFilters(); + //@} + + /** @name Different steps methods */ + /** + * \brief Step 1: filtering + Step 2: gradient computation + * \details First, perform Gaussian blur to the input image. + * Then, compute the x-axis and y-axis gradients of the image. + * \param[in] I The image we want to compute the gradients. + */ + void performFilteringAndGradientComputation(const vpImage &I); + + /** + * \brief Step 3: edge thining + * \details Perform the edge thining step. + * Perform a non-maximum suppresion to keep only local maxima as edge candidates. + */ + void performEdgeThining(); + + /** + * \brief Perform hysteresis thresholding. + * \details Edge candidates that are greater than \b m_upperThreshold are saved in \b m_strongEdgePoints + * and will be kept in the final edge map. + * Edge candidates that are bewteen \b m_lowerThreshold and \b m_upperThreshold are saved in + * \b m_weakEdgePoints and will be kept in the final edge map only if they are connected + * to a strong edge point. + * Edge candidates that are below \b m_lowerThreshold are discarded. + * \param lowerThreshold Edge candidates that are below this threshold are definitely not + * edges. + * \param upperThreshold Edge candidates that are greater than this threshold are classified + * as strong edges. + */ + void performHysteresisThresholding(const float &lowerThreshold, const float &upperThreshold); + + /** + * @brief Search recursively for a strong edge in the neighborhood of a weak edge. + * + * \param[in] coordinates The coordinates we are checking. + * \return true We found a strong edge point in its 8-connected neighborhood. + * \return false We did not found a strong edge point in its 8-connected neighborhood. + */ + bool recursiveSearchForStrongEdge(const std::pair &coordinates); + + /** + * \brief Perform edge tracking. + * \details For each weak edge, we will recursively check if they are 8-connected to a strong edge point. + * If so, the weak edge will be saved in \b m_strongEdgePoints and will be kept in the final edge map. + * Otherwise, the edge point will be discarded. + */ + void performEdgeTracking(); + //@} + +public: + /** @name Constructors and initialization */ + //@{ + /** + * \brief Default constructor of the vpCannyEdgeDetection class. + * The thresholds used during the hysteresis thresholding step are set to be automatically computed. + */ + vpCannyEdgeDetection(); + + /** + * \brief Construct a new vpCannyEdgeDetection object. + * + * \param[in] gaussianKernelSize The size of the Gaussian filter kernel. Must be odd. + * \param[in] gaussianStdev The standard deviation of the Gaussian filter. + * \param[in] lowerThreshold The lower threshold of the hysteresis thresholding step. If negative, will be computed from the upper threshold. + * \param[in] upperThreshold The upper threshold of the hysteresis thresholding step. If negative, will be computed from the median of the gray values of the image. + */ + vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev + , const float &lowerThreshold = -1., const float &upperThreshold = -1.); + + // // Configuration from files +#ifdef VISP_HAVE_NLOHMANN_JSON + /** + * \brief Construct a new vpCannyEdgeDetection object. + * + * \param[in] jsonPath The path towards the JSON file to use to initialize the vpCannyEdgeDetection object. + */ + vpCannyEdgeDetection(const std::string &jsonPath); + + /** + * \brief Initialize all the algorithm parameters using the JSON file + * whose path is \b jsonPath. Throw a \b vpException error if the file + * does not exist. + * + * \param[in] jsonPath The path towards the JSON configuration file. + */ + void initFromJSON(const std::string &jsonPath); + + /** + * \brief Read the detector configuration from JSON. All values are optional and if an argument is not present, + * the default value defined in the constructor is kept + * + * \param j The JSON object, resulting from the parsing of a JSON file. + * \param detector The detector, that will be initialized from the JSON data. + */ + inline friend void from_json(const json &j, vpCannyEdgeDetection &detector) + { + detector.m_gaussianKernelSize = j.value("gaussianSize", detector.m_gaussianKernelSize); + detector.m_gaussianStdev = j.value("gaussianStdev", detector.m_gaussianStdev); + detector.m_lowerThreshold = j.value("lowerThreshold", detector.m_lowerThreshold); + detector.m_upperThreshold = j.value("upperThreshold", detector.m_upperThreshold); + } + + /** + * \brief Parse a vpCircleHoughTransform into JSON format. + * + * \param j A JSON parser object. + * \param config The vpCircleHoughTransform that must be parsed into JSON format. + */ + inline friend void to_json(json &j, const vpCannyEdgeDetection &detector) + { + j = json { + {"gaussianSize", detector.m_gaussianKernelSize}, + {"gaussianStdev", detector.m_gaussianStdev}, + {"lowerThreshold", detector.m_lowerThreshold}, + {"upperThreshold", detector.m_upperThreshold} }; + } +#endif + //@} + + /** @name Detection methods */ + //@{ + /** + * \brief Detect the edges in an image. + * Convert the color image into a ViSP gray-scale image. + * + * \param[in] cv_I A color image, in OpenCV format. + * \return vpImage 255 means an edge, 0 means not an adge. + */ + vpImage detect(const cv::Mat &cv_I); + + /** + * \brief Detect the edges in an image. + * Convert the color image into a gray-scale image. + * + * \param[in] I_color An RGB image, in ViSP format. + * \return vpImage 255 means an edge, 0 means not an adge. + */ + vpImage detect(const vpImage &I_color); + + /** + * \brief Detect the edges in a gray-scale image. + * + * \param[in] I A gray-scale image, in ViSP format. + * \return vpImage 255 means an edge, 0 means not an adge. + */ + vpImage detect(const vpImage &I); + //@} + + /** @name Setters */ + //@{ + /** + * \brief Set the Gradients of the image that will be processed. + * + * \param[in] dIx Gradient along the horizontal axis of the image. + * \param[in] dIy Gradient along the vertical axis of the image. + */ + inline void setGradients(const vpImage &dIx, const vpImage &dIy) + { + m_dIx = dIx; + m_dIy = dIy; + m_areGradientAvailable = true; + } + + /** + * \brief Set the lower and upper Canny Thresholds used to qualify the edge point candidates. + * Edge point candidates whose gradient is between these two values is kept only if it + * linked somehow to a strong edge point. + * + * \param[in] lowerThresh The lower threshold: each point whose gradient is below this threshold is discarded. + * \param[in] upperThresh The upper threshold: each point whose gradient is greater than this threshold is + * said to be a strong edge point and is kept. + */ + inline void setCannyThresholds(const float &lowerThresh, const float &upperThresh) + { + m_lowerThreshold = lowerThresh; + m_upperThreshold = upperThresh; + } + + /** + * @brief Set the Gaussian Filters kernel size and standard deviation + * and initialize the aforementioned filters. + * + * \param[in] kernelSize The size of the Gaussian filters kernel. + * \param[in] stdev The standard deviation of the Gaussian filters used to blur and + * compute the gradient of the image. + */ + inline void setGaussianFilterParameters(const int &kernelSize, const float &stdev) + { + m_gaussianKernelSize = kernelSize; + m_gaussianStdev = stdev; + initGaussianFilters(); + } + //@} +}; +#endif diff --git a/modules/core/include/visp3/core/vpImageFilter.h b/modules/core/include/visp3/core/vpImageFilter.h index 014f3a717c..7629b6a192 100644 --- a/modules/core/include/visp3/core/vpImageFilter.h +++ b/modules/core/include/visp3/core/vpImageFilter.h @@ -69,10 +69,8 @@ class VISP_EXPORT vpImageFilter { public: -#if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) static void canny(const vpImage &I, vpImage &Ic, unsigned int gaussianFilterSize, double thresholdCanny, unsigned int apertureSobel); -#endif /*! Apply a 1x3 derivative filter to an image pixel. @@ -85,7 +83,7 @@ class VISP_EXPORT vpImageFilter { return (2047.0 * (I[r][c + 1] - I[r][c - 1]) + 913.0 * (I[r][c + 2] - I[r][c - 2]) + 112.0 * (I[r][c + 3] - I[r][c - 3])) / - 8418.0; + 8418.0; } /*! @@ -99,7 +97,7 @@ class VISP_EXPORT vpImageFilter { return (2047.0 * (I[r + 1][c] - I[r - 1][c]) + 913.0 * (I[r + 2][c] - I[r - 2][c]) + 112.0 * (I[r + 3][c] - I[r - 3][c])) / - 8418.0; + 8418.0; } /*! @@ -133,7 +131,7 @@ class VISP_EXPORT vpImageFilter /*! Apply a size x 1 Derivative Filter in Y to an image pixel. - + \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. \param I : Image to filter \param r : coordinates (row) of the pixel @@ -289,7 +287,7 @@ class VISP_EXPORT vpImageFilter static void sepFilter(const vpImage &I, vpImage &If, const vpColVector &kernelH, const vpColVector &kernelV); - + /*! Apply a separable filter. \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. @@ -353,7 +351,7 @@ class VISP_EXPORT vpImageFilter } } } - + template static void filterX(const vpImage &I, vpImage &dIx, const FilterType *filter, unsigned int size) { @@ -628,12 +626,12 @@ class VISP_EXPORT vpImageFilter } } } - + static void filterY(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterYR(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterYG(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterYB(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); - + template static void filterY(const vpImage &I, vpImage &dIy, const FilterType *filter, unsigned int size) { @@ -907,12 +905,12 @@ class VISP_EXPORT vpImageFilter vpImageFilter::filterX(I, GIx, fg, size); vpImageFilter::filterY(GIx, GI, fg, size); GIx.destroy(); - delete [] fg; + delete[] fg; } static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, double sigma = 0., bool normalize = true); - + /*! Apply a Gaussian blur to a double image. \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. @@ -935,9 +933,9 @@ class VISP_EXPORT vpImageFilter vpImageFilter::filterX(I, GIx, fg, size); vpImageFilter::filterY(GIx, GI, fg, size); GIx.destroy(); - delete [] fg; + delete[] fg; } - + /*! Apply a 5x5 Gaussian filter to an image pixel. @@ -954,7 +952,7 @@ class VISP_EXPORT vpImageFilter 4.0 * (fr[r - 2][c + 1] + fr[r - 2][c - 1] + fr[r - 1][c - 2] + fr[r + 1][c - 2] + fr[r + 2][c - 1] + fr[r + 2][c + 1] + fr[r - 1][c + 2] + fr[r + 1][c + 2]) + 2.0 * (fr[r - 2][c - 2] + fr[r + 2][c - 2] + fr[r - 2][c + 2] + fr[r + 2][c + 2])) / - 159.0; + 159.0; } // Gaussain pyramid operation static void getGaussPyramidal(const vpImage &I, vpImage &GI); @@ -1067,7 +1065,7 @@ class VISP_EXPORT vpImageFilter } } } - + template static void getGradX(const vpImage &I, vpImage &dIx, const FilterType *filter, unsigned int size) @@ -1085,7 +1083,7 @@ class VISP_EXPORT vpImageFilter } } } - + /*! Compute the gradient along X after applying a gaussian filter along Y. \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. @@ -1104,7 +1102,7 @@ class VISP_EXPORT vpImageFilter vpImageFilter::filterY(I, GIy, gaussianKernel, size); vpImageFilter::getGradX(GIy, dIx, gaussianDerivativeKernel, size); } - + // Gradient along Y template static void getGradY(const vpImage &I, vpImage &dIy) @@ -1126,7 +1124,7 @@ class VISP_EXPORT vpImageFilter } } } - + template static void getGradY(const vpImage &I, vpImage &dIy, const FilterType *filter, unsigned int size) { @@ -1166,7 +1164,7 @@ class VISP_EXPORT vpImageFilter vpImageFilter::filterX(I, GIx, gaussianKernel, size); vpImageFilter::getGradY(GIx, dIy, gaussianDerivativeKernel, size); } - + /*! Get Sobel kernel for X-direction. \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp new file mode 100644 index 0000000000..2affac4ea9 --- /dev/null +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -0,0 +1,391 @@ +#include + +#include + +// // Initialization methods + +vpCannyEdgeDetection::vpCannyEdgeDetection() + : m_gaussianKernelSize(3) + , m_gaussianStdev(1.) + , m_areGradientAvailable(false) + , m_lowerThreshold(-1.) + , m_upperThreshold(-1.) +{ + initGaussianFilters(); +} + +vpCannyEdgeDetection::vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev + , const float &lowerThreshold, const float &upperThreshold) + : m_gaussianKernelSize(gaussianKernelSize) + , m_gaussianStdev(gaussianStdev) + , m_areGradientAvailable(false) + , m_lowerThreshold(lowerThreshold) + , m_upperThreshold(upperThreshold) +{ + initGaussianFilters(); +} + +#ifdef VISP_HAVE_NLOHMANN_JSON +vpCannyEdgeDetection::vpCannyEdgeDetection(const std::string &jsonPath) +{ + initFromJSON(jsonPath); +} + +void +vpCannyEdgeDetection::initFromJSON(const std::string &jsonPath) +{ + std::ifstream file(jsonPath); + if (!file.good()) { + std::stringstream ss; + ss << "Problem opening file " << jsonPath << ". Make sure it exists and is readable" << std::endl; + throw vpException(vpException::ioError, ss.str()); + } + json j; + try { + j = json::parse(file); + } + catch (json::parse_error &e) { + std::stringstream msg; + msg << "Could not parse JSON file : \n"; + msg << e.what() << std::endl; + msg << "Byte position of error: " << e.byte; + throw vpException(vpException::ioError, msg.str()); + } + *this = j; // Call from_json(const json& j, vpDetectionCircle2D& *this) to read json + file.close(); + initGaussianFilters(); +} +#endif + +void +vpCannyEdgeDetection::initGaussianFilters() +{ + m_fg.resize(1, (m_gaussianKernelSize + 1)/2); + vpImageFilter::getGaussianKernel(m_fg.data, m_gaussianKernelSize, m_gaussianStdev, true); + m_fgDg.resize(1, (m_gaussianKernelSize + 1)/2); + vpImageFilter::getGaussianDerivativeKernel(m_fgDg.data, m_gaussianKernelSize, m_gaussianStdev, true); +} + +// // Detection methods +vpImage +vpCannyEdgeDetection::detect(const cv::Mat &cv_I) +{ + vpImage I_gray; + vpImageConvert::convert(cv_I, I_gray); + return detect(I_gray); +} + +vpImage +vpCannyEdgeDetection::detect(const vpImage &I_color) +{ + vpImage I_gray; + vpImageConvert::convert(I_color, I_gray); + return detect(I_gray); +} + +vpImage +vpCannyEdgeDetection::detect(const vpImage &I) +{ + // // Clearing the previous results + m_edgeMap.resize(I.getHeight(), I.getWidth(), 0); + m_edgeCandidateAndGradient.clear(); + m_edgePointsCandidates.clear(); + + // // Step 1 and 2: filter the image and compute the gradient, if not given by the user + if (!m_areGradientAvailable) { + performFilteringAndGradientComputation(I); + } + m_areGradientAvailable = false; // Reset for next call + + // // Step 3: edge thining + performEdgeThining(); + + // // Step 4: hysteresis thresholding + float upperThreshold = m_upperThreshold; + + float lowerThreshold = m_lowerThreshold; + if (m_lowerThreshold < 0) { + // Applying Canny recommandation to have the upper threshold 3 times greater than the lower threshold. + lowerThreshold = m_upperThreshold / 3.; + } + + performHysteresisThresholding(lowerThreshold, upperThreshold); + + // // Step 5: edge tracking + performEdgeTracking(); + return m_edgeMap; +} + +void +vpCannyEdgeDetection::performFilteringAndGradientComputation(const vpImage &I) +{ + vpImageFilter::getGradXGauss2D(I, + m_dIx, + m_fg.data, + m_fgDg.data, + m_gaussianKernelSize + ); + vpImageFilter::getGradYGauss2D(I, + m_dIy, + m_fg.data, + m_fgDg.data, + m_gaussianKernelSize + ); +} + +/** + * \brief Get the Theta Quadrant in which lies absoluteTheta and the offset along the horizontal + * and vertical direction where to look for the neighbors. + * + * \param[in] absoluteTheta The absolute value of the angle of the edge, expressed in degrees. + * \param[out] dRowGradPlus The offset in the vertical positive direction. + * \param[out] dRowGradMinus The offset in the vertical negative direction. + * \param[out] dColGradPlus The offset in the horizontal positive direction. + * \param[out] dColGradMinus The offset in the horizontal negative direction. + * \return float The quadrant in which lies the angle of the edge, expressed in degrees. + */ +float +getThetaQuadrant(const float &absoluteTheta, int &dRowGradPlus, int &dRowGradMinus, + int &dColGradPlus, int &dColGradMinus) +{ + if (absoluteTheta < 22.5) { + // Angles between -22.5 and 22.5 are mapped to be horizontal axis + dColGradMinus = -1; + dColGradPlus = 1; + dRowGradPlus = dRowGradMinus = 0; + return 0; + } + else if (absoluteTheta >= 22.5 && absoluteTheta < 67.5) { + // Angles between 22.5 and 67.5 are mapped to the diagonal 45degree + dRowGradMinus = dColGradMinus = -1; + dRowGradPlus = dColGradPlus = 1; + return 45.; + } + else if (absoluteTheta >= 67.5 && absoluteTheta < 112.5) { + // Angles between 67.5 and 112.5 are mapped to the vertical axis + dColGradMinus = dColGradPlus = 0; + dRowGradMinus = -1; + dRowGradPlus = 1; + return 90.; + } + else if (absoluteTheta >= 112.5 && absoluteTheta < 157.5) { + // Angles between 112.5 and 157.5 are mapped to the diagonal -45degree + dRowGradMinus = -1; + dColGradMinus = 1; + dRowGradPlus = 1; + dColGradPlus = -1; + return 135.; + } + else { + // Angles greater than 157.5 are mapped to be horizontal axis + dColGradMinus = 1; + dColGradPlus = -1; + dRowGradMinus = dRowGradPlus = 0; + return 180.; + } + return -1.; // Should not reach this point +} + +/** + * @brief Get the Manhattan Gradient, i.e. abs(dIx) + abs(dIy) at the index \b row \b col . + * If one of the index is outside the limits of the image, return 0. + * @param dIx Gradient along the horizontal axis. + * @param dIy Gradient along the vertical axis. + * @param row Index along the vertical axis. + * @param col Index along the horizontal axis. + * @return float grad = abs(dIx) + abs(dIy) if row and col are valid, 0 otherwise. + */ +float +getManhattanGradient(const vpImage &dIx, const vpImage &dIy, const int &row, const int &col) +{ + float grad = 0.; + int nbRows = dIx.getRows(); + int nbCols = dIx.getCols(); + if (row >= 0 + && row < nbRows + && col >= 0 + && col < nbCols + ) { + float dx = dIx[row][col]; + float dy = dIy[row][col]; + grad = std::abs(dx) + std::abs(dy); + } + return grad; +} + +/** + * @brief Get the absolute value of the gradient orientation. + * + * @param dIx Gradient along the horizontal axis. + * @param dIy Gradient along the vertical axis. + * @param row Index along the vertical axis. + * @param col Index along the horizontal axis. + * @return float The absolute value of the gradient orientation, expressed in degrees. + */ +float +getAbsoluteTheta(const vpImage &dIx, const vpImage &dIy, const int &row, const int &col) +{ + float absoluteTheta; + float dx = dIx[row][col]; + float dy = dIy[row][col]; + + if (std::abs(dx) < std::numeric_limits::epsilon()) { + absoluteTheta = 90.; + } + else { + absoluteTheta = vpMath::deg(std::abs(std::atan(dy / dx))); + } + return absoluteTheta; +} +void +vpCannyEdgeDetection::performEdgeThining() +{ + vpImage dIx = m_dIx; + vpImage dIy = m_dIy; + int nbRows = m_dIx.getRows(); + int nbCols = m_dIx.getCols(); + + for (int row = 0; row < nbRows; row++) { + for (int col = 0; col < nbCols; col++) { + // Computing the gradient orientation and magnitude + float grad = getManhattanGradient(dIx, dIy, row, col); + + if (grad < std::numeric_limits::epsilon()) { + // The gradient is almost null => ignoring the point + continue; + } + + float absoluteTheta = getAbsoluteTheta(dIx, dIy, row, col); + + // Getting the offset along the horizontal and vertical axes + // depending on the gradient orientation + int dRowGradPlus = 0, dRowGradMinus = 0; + int dColGradPlus = 0, dColGradMinus = 0; + float thetaQuadrant = getThetaQuadrant(absoluteTheta, dRowGradPlus, dRowGradMinus, dColGradPlus, dColGradMinus); + + bool isGradientInTheSameDirection = true; + std::vector> pixelsSeen; + std::pair bestPixel(row, col); + float bestGrad = grad; + int rowCandidate = row + dRowGradPlus; + int colCandidate = col + dColGradPlus; + + while (isGradientInTheSameDirection) { + // Getting the gradients around the edge point + float gradPlus = getManhattanGradient(dIx, dIy, rowCandidate, colCandidate); + if (std::abs(gradPlus) < std::numeric_limits::epsilon()) { + // The gradient is almost null => ignoring the point + isGradientInTheSameDirection = false; + break; + } + int dRowGradPlusCandidate = 0, dRowGradMinusCandidate = 0; + int dColGradPlusCandidate = 0, dColGradMinusCandidate = 0; + float absThetaPlus = getAbsoluteTheta(dIx, dIy, rowCandidate, colCandidate); + float thetaQuadrantCandidate = getThetaQuadrant(absThetaPlus, dRowGradPlusCandidate, dRowGradMinusCandidate, dColGradPlusCandidate, dColGradMinusCandidate); + if (thetaQuadrantCandidate != thetaQuadrant) { + isGradientInTheSameDirection = false; + break; + } + + std::pair pixelCandidate(rowCandidate, colCandidate); + if (gradPlus > bestGrad) { + // The gradient is higher with the nex pixel candidate + // Saving it + bestGrad = gradPlus; + pixelsSeen.push_back(bestPixel); + bestPixel = pixelCandidate; + } + else { + // Best pixel is still the best + pixelsSeen.push_back(pixelCandidate); + } + rowCandidate += dRowGradPlus; + colCandidate += dColGradPlus; + } + + // Keeping the edge point that has the highest gradient + m_edgeCandidateAndGradient[bestPixel] = bestGrad; + + // Suppressing non-maximum gradient + for (std::vector>::iterator it = pixelsSeen.begin(); it != pixelsSeen.end(); it++) { + // Suppressing non-maximum gradient + int row_temp = it->first; + int col_temp = it->second; + dIx[row_temp][col_temp] = 0.; + dIy[row_temp][col_temp] = 0.; + } + } + } +} + +void +vpCannyEdgeDetection::performHysteresisThresholding(const float &lowerThreshold, const float &upperThreshold) +{ + std::map, float>::iterator it; + for (it = m_edgeCandidateAndGradient.begin(); it != m_edgeCandidateAndGradient.end(); it++) { + if (it->second >= upperThreshold) { + m_edgePointsCandidates[it->first] = STRONG_EDGE; + } + else if (it->second >= lowerThreshold && it->second < upperThreshold) { + m_edgePointsCandidates[it->first] = WEAK_EDGE; + } + } +} + +void +vpCannyEdgeDetection::performEdgeTracking() +{ + std::map, EdgeType>::iterator it; + for (it = m_edgePointsCandidates.begin(); it != m_edgePointsCandidates.end(); it++) { + if (it->second == STRONG_EDGE) { + m_edgeMap[it->first.first][it->first.second] = 255; + } + else if (recursiveSearchForStrongEdge(it->first)) { + m_edgeMap[it->first.first][it->first.second] = 255; + } + } +} + +bool +vpCannyEdgeDetection::recursiveSearchForStrongEdge(const std::pair &coordinates) +{ + bool hasFoundStrongEdge = false; + int nbRows = m_dIx.getRows(); + int nbCols = m_dIx.getCols(); + m_edgePointsCandidates[coordinates] = ON_CHECK; + for (int dr = -1; dr <= 1 && !hasFoundStrongEdge; dr++) { + for (int dc = -1; dc <= 1 && !hasFoundStrongEdge; dc++) { + int idRow = dr + (int)coordinates.first; + int idCol = dc + (int)coordinates.second; + + // Checking if we are still looking for an edge in the limit of the image + if ((idRow < 0 || idRow >= nbRows) + || (idCol < 0 || idCol >= nbCols) + || (dr == 0 && dc == 0) + ) { + continue; + } + + try { + std::pair key_candidate(idRow, idCol); + // Checking if the 8-neighbor point is in the list of edge candidates + EdgeType type_candidate = m_edgePointsCandidates.at(key_candidate); + if (type_candidate == STRONG_EDGE) { + // The 8-neighbor point is a strong edge => the weak edge becomes a strong edge + hasFoundStrongEdge = true; + } + else if (type_candidate == WEAK_EDGE) { + hasFoundStrongEdge = recursiveSearchForStrongEdge(key_candidate); + } + } + catch (std::out_of_range &e) { + continue; + } + } + } + if (hasFoundStrongEdge) { + m_edgePointsCandidates[coordinates] = STRONG_EDGE; + m_edgeMap[coordinates.first][coordinates.second] = 255; + } + return hasFoundStrongEdge; +} \ No newline at end of file diff --git a/modules/core/src/image/vpImageFilter.cpp b/modules/core/src/image/vpImageFilter.cpp index dfa5140ae1..56f583537c 100644 --- a/modules/core/src/image/vpImageFilter.cpp +++ b/modules/core/src/image/vpImageFilter.cpp @@ -33,6 +33,7 @@ * *****************************************************************************/ +#include #include #include #include @@ -57,58 +58,58 @@ void vpImageFilter::filter(const vpImage &I, vpImage &Iu * \endcond */ -/*! - Apply a filter to an image using two separable kernels. For instance, - the Sobel kernel can be decomposed to: - \f[ - \left [ - \begin{matrix} - 1 & 0 & -1 \\ - 2 & 0 & -2 \\ - 1 & 0 & -1 - \end{matrix} - \right ] = - \left [ - \begin{matrix} - 1 \\ - 2 \\ - 1 - \end{matrix} - \right ] \ast - \left [ - \begin{matrix} - 1 && 0 && -1 - \end{matrix} - \right ] - \f] - Thus, the convolution operation can be performed as: - \f[ - G_x = - \left [ - \begin{matrix} - 1 \\ - 2 \\ - 1 - \end{matrix} - \right ] \ast - \left ( - \left [ - \begin{matrix} - 1 && 0 && -1 - \end{matrix} - \right ] \ast I - \right ) - \f] - Using two separable kernels reduce the number of operations and can be - faster for large kernels. - - \param I : Image to filter - \param If : Filtered image. - \param kernelH : Separable kernel (performed first). - \param kernelV : Separable kernel (performed last). - \note Only pixels in the input image fully covered by the kernel are - considered. -*/ + /*! + Apply a filter to an image using two separable kernels. For instance, + the Sobel kernel can be decomposed to: + \f[ + \left [ + \begin{matrix} + 1 & 0 & -1 \\ + 2 & 0 & -2 \\ + 1 & 0 & -1 + \end{matrix} + \right ] = + \left [ + \begin{matrix} + 1 \\ + 2 \\ + 1 + \end{matrix} + \right ] \ast + \left [ + \begin{matrix} + 1 && 0 && -1 + \end{matrix} + \right ] + \f] + Thus, the convolution operation can be performed as: + \f[ + G_x = + \left [ + \begin{matrix} + 1 \\ + 2 \\ + 1 + \end{matrix} + \right ] \ast + \left ( + \left [ + \begin{matrix} + 1 && 0 && -1 + \end{matrix} + \right ] \ast I + \right ) + \f] + Using two separable kernels reduce the number of operations and can be + faster for large kernels. + + \param I : Image to filter + \param If : Filtered image. + \param kernelH : Separable kernel (performed first). + \param kernelV : Separable kernel (performed last). + \note Only pixels in the input image fully covered by the kernel are + considered. + */ void vpImageFilter::sepFilter(const vpImage &I, vpImage &If, const vpColVector &kernelH, const vpColVector &kernelV) { @@ -141,7 +142,6 @@ void vpImageFilter::sepFilter(const vpImage &I, vpImage & } } -#if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) /*! Apply the Canny edge operator on the image \e Isrc and return the resulting image \e Ires. @@ -154,7 +154,6 @@ void vpImageFilter::sepFilter(const vpImage &I, vpImage & int main() { -#if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) // Constants for the Canny operator. const unsigned int gaussianFilterSize = 5; const double thresholdCanny = 15; @@ -168,7 +167,6 @@ int main() // Apply the Canny edge operator and set the Icanny image. vpImageFilter::canny(Isrc, Icanny, gaussianFilterSize, thresholdCanny, apertureSobel); -#endif return (0); } \endcode @@ -184,13 +182,18 @@ int main() void vpImageFilter::canny(const vpImage &Isrc, vpImage &Ires, unsigned int gaussianFilterSize, double thresholdCanny, unsigned int apertureSobel) { +#if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) cv::Mat img_cvmat, edges_cvmat; vpImageConvert::convert(Isrc, img_cvmat); cv::GaussianBlur(img_cvmat, img_cvmat, cv::Size((int)gaussianFilterSize, (int)gaussianFilterSize), 0, 0); cv::Canny(img_cvmat, edges_cvmat, thresholdCanny, thresholdCanny, (int)apertureSobel); vpImageConvert::convert(edges_cvmat, Ires); -} +#else + (void)apertureSobel; + vpCannyEdgeDetection edgeDetector(gaussianFilterSize, 0.1, thresholdCanny * 0.5, thresholdCanny); + Ires = edgeDetector.detect(Isrc); #endif +} /** * \cond DO_NOT_DOCUMENT @@ -326,17 +329,17 @@ void vpImageFilter::gaussianBlur(const vpImage &I, vpImag * \endcond */ -/*! - Apply a Gaussian blur to RGB color image. - \param I : Input image. - \param GI : Filtered image. - \param size : Filter size. This value should be odd. - \param sigma : Gaussian standard deviation. If it is equal to zero or - negative, it is computed from filter size as sigma = (size-1)/6. - \param normalize : Flag indicating whether to normalize the filter coefficients or not. - - \sa getGaussianKernel() to know which kernel is used. - */ + /*! + Apply a Gaussian blur to RGB color image. + \param I : Input image. + \param GI : Filtered image. + \param size : Filter size. This value should be odd. + \param sigma : Gaussian standard deviation. If it is equal to zero or + negative, it is computed from filter size as sigma = (size-1)/6. + \param normalize : Flag indicating whether to normalize the filter coefficients or not. + + \sa getGaussianKernel() to know which kernel is used. + */ void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, bool normalize) { @@ -346,7 +349,7 @@ void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, vpImageFilter::filterX(I, GIx, fg, size); vpImageFilter::filterY(GIx, GI, fg, size); GIx.destroy(); - delete [] fg; + delete[] fg; } /** @@ -440,7 +443,7 @@ void vpImageFilter::getGradYGauss2D(const vpImage &I, vp * \endcond */ -// operation pour pyramide gaussienne + // operation pour pyramide gaussienne void vpImageFilter::getGaussPyramidal(const vpImage &I, vpImage &GI) { vpImage GIx; diff --git a/tutorial/image/tutorial-image-filter.cpp b/tutorial/image/tutorial-image-filter.cpp index da0cb8d76e..40f8c51e89 100644 --- a/tutorial/image/tutorial-image-filter.cpp +++ b/tutorial/image/tutorial-image-filter.cpp @@ -54,7 +54,8 @@ int main(int argc, char **argv) try { vpImageIo::read(I, argv[1]); - } catch (...) { + } + catch (...) { std::cout << "Cannot read image \"" << argv[1] << "\"" << std::endl; return EXIT_FAILURE; } @@ -83,11 +84,9 @@ int main(int argc, char **argv) display(dIy, "Gradient dIy"); //! [Canny] - #if defined(HAVE_OPENCV_IMGPROC) vpImage C; vpImageFilter::canny(I, C, 5, 15, 3); display(C, "Canny"); - #endif //! [Canny] //! [Convolution kernel] @@ -118,7 +117,8 @@ int main(int argc, char **argv) } //! [Gaussian pyramid] return EXIT_SUCCESS; - } catch (const vpException &e) { + } + catch (const vpException &e) { std::cout << "Catch an exception: " << e << std::endl; return EXIT_FAILURE; } From 3773530a08efc5fff5b37eb98e1b8a505b40bea3 Mon Sep 17 00:00:00 2001 From: rlagneau Date: Thu, 24 Aug 2023 11:34:03 +0200 Subject: [PATCH 2/8] [FIX] Remvoed normalization of the Gaussian filters used to compute the gradietns --- modules/core/src/image/vpCannyEdgeDetection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp index 2affac4ea9..56b2ab6b28 100644 --- a/modules/core/src/image/vpCannyEdgeDetection.cpp +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -61,9 +61,9 @@ void vpCannyEdgeDetection::initGaussianFilters() { m_fg.resize(1, (m_gaussianKernelSize + 1)/2); - vpImageFilter::getGaussianKernel(m_fg.data, m_gaussianKernelSize, m_gaussianStdev, true); + vpImageFilter::getGaussianKernel(m_fg.data, m_gaussianKernelSize, m_gaussianStdev, false); m_fgDg.resize(1, (m_gaussianKernelSize + 1)/2); - vpImageFilter::getGaussianDerivativeKernel(m_fgDg.data, m_gaussianKernelSize, m_gaussianStdev, true); + vpImageFilter::getGaussianDerivativeKernel(m_fgDg.data, m_gaussianKernelSize, m_gaussianStdev, false); } // // Detection methods From a5112edf7ce870f4805a73e19d51ae51f73042c8 Mon Sep 17 00:00:00 2001 From: rlagneau Date: Mon, 28 Aug 2023 10:19:47 +0200 Subject: [PATCH 3/8] vpCannyEdgeDetection::initGaussianFilters checks that m_gaussianKernelSize is odd --- modules/core/src/image/vpCannyEdgeDetection.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp index 56b2ab6b28..7826a7f906 100644 --- a/modules/core/src/image/vpCannyEdgeDetection.cpp +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -60,6 +60,9 @@ vpCannyEdgeDetection::initFromJSON(const std::string &jsonPath) void vpCannyEdgeDetection::initGaussianFilters() { + if ((m_gaussianKernelSize % 2) == 0) { + throw(vpException(vpException::badValue, "The Gaussian kernel size should be odd")); + } m_fg.resize(1, (m_gaussianKernelSize + 1)/2); vpImageFilter::getGaussianKernel(m_fg.data, m_gaussianKernelSize, m_gaussianStdev, false); m_fgDg.resize(1, (m_gaussianKernelSize + 1)/2); From 92513c9d1cfde2289dea6ef19e39ddac0c4a1041 Mon Sep 17 00:00:00 2001 From: rlagneau Date: Mon, 28 Aug 2023 14:51:51 +0200 Subject: [PATCH 4/8] [CLEAN] Corrected typos [DOC] added documentation for the EdgeType enum --- .../include/visp3/core/vpCannyEdgeDetection.h | 24 +++++++++---------- .../core/src/image/vpCannyEdgeDetection.cpp | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h index d12d833144..3ac075ff13 100644 --- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -53,9 +53,9 @@ class vpCannyEdgeDetection private: typedef enum EdgeType { - STRONG_EDGE, - WEAK_EDGE, - ON_CHECK + STRONG_EDGE, /*!< This pixel exceeds the upper threshold of the double hysteresis phase, it is thus for sure an edge point.*/ + WEAK_EDGE,/*!< This pixel is between the lower and upper threshold of the double hysteresis phase, it is an edge point only if it is linked at some point to an edge point.*/ + ON_CHECK /*!< This pixel is currently tested to know if it is linked to a strong edge point.*/ } EdgeType; // // Gaussian smoothing attributes @@ -63,7 +63,7 @@ class vpCannyEdgeDetection float m_gaussianStdev; /*!< Standard deviation of the Gaussian filter.*/ // // Gradient computation attributes - bool m_areGradientAvailable; /*!< Set to true if the user gave the gradient images, false otherwise. In the later case, the class will compute the gradients.*/ + bool m_areGradientAvailable; /*!< Set to true if the user provides the gradient images, false otherwise. In the latter case, the class will compute the gradients.*/ vpArray2D m_fg; /*!< Array that contains the Gaussian kernel.*/ vpArray2D m_fgDg; /*!< Array that contains the derivative of the Gaussian kernel.*/ vpImage m_dIx; /*!< X-axis gradient.*/ @@ -72,9 +72,9 @@ class vpCannyEdgeDetection // // Edge thining attributes std::map, float> m_edgeCandidateAndGradient; /*!< Map that contains point image coordinates and corresponding gradient value.*/ - // // Histeresis thresholding attributes - float m_lowerThreshold; /*!< Lower threshold for the histeresis step. If negative, it will be deduced as from m_upperThreshold. */ - float m_upperThreshold; /*!< Upper threshold for the histeresis step.*/ + // // Hysteresis thresholding attributes + float m_lowerThreshold; /*!< Lower threshold for the hysteresis step. If negative, it will be deduced as from m_upperThreshold. */ + float m_upperThreshold; /*!< Upper threshold for the hysteresis step.*/ // // Edge tracking attributes std::map, EdgeType> m_edgePointsCandidates; /*!< Map that contains the strong edge points, i.e. the points for which we know for sure they are edge points, @@ -156,8 +156,8 @@ class vpCannyEdgeDetection * \param[in] lowerThreshold The lower threshold of the hysteresis thresholding step. If negative, will be computed from the upper threshold. * \param[in] upperThreshold The upper threshold of the hysteresis thresholding step. If negative, will be computed from the median of the gray values of the image. */ - vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev - , const float &lowerThreshold = -1., const float &upperThreshold = -1.); + vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev, + const float &lowerThreshold = -1., const float &upperThreshold = -1.); // // Configuration from files #ifdef VISP_HAVE_NLOHMANN_JSON @@ -216,7 +216,7 @@ class vpCannyEdgeDetection * Convert the color image into a ViSP gray-scale image. * * \param[in] cv_I A color image, in OpenCV format. - * \return vpImage 255 means an edge, 0 means not an adge. + * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const cv::Mat &cv_I); @@ -225,7 +225,7 @@ class vpCannyEdgeDetection * Convert the color image into a gray-scale image. * * \param[in] I_color An RGB image, in ViSP format. - * \return vpImage 255 means an edge, 0 means not an adge. + * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const vpImage &I_color); @@ -233,7 +233,7 @@ class vpCannyEdgeDetection * \brief Detect the edges in a gray-scale image. * * \param[in] I A gray-scale image, in ViSP format. - * \return vpImage 255 means an edge, 0 means not an adge. + * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const vpImage &I); //@} diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp index 7826a7f906..195fc5ce2b 100644 --- a/modules/core/src/image/vpCannyEdgeDetection.cpp +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -391,4 +391,4 @@ vpCannyEdgeDetection::recursiveSearchForStrongEdge(const std::pair Date: Tue, 29 Aug 2023 08:55:32 +0200 Subject: [PATCH 5/8] Added safeguards to check if OpenCV core is available --- modules/core/include/visp3/core/vpCannyEdgeDetection.h | 2 ++ modules/core/src/image/vpCannyEdgeDetection.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h index 3ac075ff13..852813a1d0 100644 --- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -211,6 +211,7 @@ class vpCannyEdgeDetection /** @name Detection methods */ //@{ +#ifdef HAVE_OPENCV_CORE /** * \brief Detect the edges in an image. * Convert the color image into a ViSP gray-scale image. @@ -219,6 +220,7 @@ class vpCannyEdgeDetection * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const cv::Mat &cv_I); +#endif /** * \brief Detect the edges in an image. diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp index 195fc5ce2b..908ce22a54 100644 --- a/modules/core/src/image/vpCannyEdgeDetection.cpp +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -70,6 +70,7 @@ vpCannyEdgeDetection::initGaussianFilters() } // // Detection methods +#ifdef HAVE_OPENCV_CORE vpImage vpCannyEdgeDetection::detect(const cv::Mat &cv_I) { @@ -77,6 +78,7 @@ vpCannyEdgeDetection::detect(const cv::Mat &cv_I) vpImageConvert::convert(cv_I, I_gray); return detect(I_gray); } +#endif vpImage vpCannyEdgeDetection::detect(const vpImage &I_color) From 4c258eb4492361cabfa41f5029c5e87d8148df40 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 29 Aug 2023 14:48:10 +0200 Subject: [PATCH 6/8] Minor changes: - code indentation - typo - doxygen doc improvement --- .../image/tutorial-image-filtering.dox | 24 +- .../include/visp3/core/vpCannyEdgeDetection.h | 48 ++-- .../core/include/visp3/core/vpImageFilter.h | 237 ++++++++---------- .../core/src/image/vpCannyEdgeDetection.cpp | 64 +++-- modules/core/src/image/vpImageFilter.cpp | 68 ++--- 5 files changed, 209 insertions(+), 232 deletions(-) diff --git a/doc/tutorial/image/tutorial-image-filtering.dox b/doc/tutorial/image/tutorial-image-filtering.dox index 4e6f3aa952..2e79b193e4 100644 --- a/doc/tutorial/image/tutorial-image-filtering.dox +++ b/doc/tutorial/image/tutorial-image-filtering.dox @@ -72,7 +72,7 @@ The resulting floating-point images \c dIx, \c dIy are the following: \section canny Canny edge detector Canny edge detector function relies on OpenCV if ViSP was build with OpenCV 2.1 or higher. Otherwise, -it relies on the ViSP implementation vpCannyEdgeDetector class. +it relies on the ViSP implementation in vpCannyEdgeDetector class. After the declaration of a new image container \c C, Canny edge detector is applied using: \snippet tutorial-image-filter.cpp Canny @@ -91,17 +91,17 @@ The resulting image \c C is the following: To apply a convolution to an image, we first have to define a kernel. For example, let us consider the 3x3 Sobel kernel defined in \c K. - \f[ - {\bf K} = \begin{tabular}{|c|c|c|} - \hline - 1 & 0 & -1 \\ - \hline - 2 & 0 & -2 \\ - \hline - 1 & 0 & -1 \\ - \hline - \end{tabular} - \f] +\f[ +{\bf K} = \left[ \begin{matrix} +\hline +1 & 0 & -1 \\ +\hline +2 & 0 & -2 \\ +\hline +1 & 0 & -1 \\ +\hline +\end{matrix} \right] +\f] \snippet tutorial-image-filter.cpp Convolution kernel diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h index 852813a1d0..6f2b1c2773 100644 --- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -92,17 +92,17 @@ class vpCannyEdgeDetection /** @name Different steps methods */ /** - * \brief Step 1: filtering + Step 2: gradient computation + * \brief Step 1: filtering + Step 2: gradient computation * \details First, perform Gaussian blur to the input image. * Then, compute the x-axis and y-axis gradients of the image. - * \param[in] I The image we want to compute the gradients. + * \param[in] I : The image we want to compute the gradients. */ void performFilteringAndGradientComputation(const vpImage &I); /** - * \brief Step 3: edge thining + * \brief Step 3: Edge thining. * \details Perform the edge thining step. - * Perform a non-maximum suppresion to keep only local maxima as edge candidates. + * Perform a non-maximum suppression to keep only local maxima as edge candidates. */ void performEdgeThining(); @@ -110,7 +110,7 @@ class vpCannyEdgeDetection * \brief Perform hysteresis thresholding. * \details Edge candidates that are greater than \b m_upperThreshold are saved in \b m_strongEdgePoints * and will be kept in the final edge map. - * Edge candidates that are bewteen \b m_lowerThreshold and \b m_upperThreshold are saved in + * Edge candidates that are between \b m_lowerThreshold and \b m_upperThreshold are saved in * \b m_weakEdgePoints and will be kept in the final edge map only if they are connected * to a strong edge point. * Edge candidates that are below \b m_lowerThreshold are discarded. @@ -124,7 +124,7 @@ class vpCannyEdgeDetection /** * @brief Search recursively for a strong edge in the neighborhood of a weak edge. * - * \param[in] coordinates The coordinates we are checking. + * \param[in] coordinates : The coordinates we are checking. * \return true We found a strong edge point in its 8-connected neighborhood. * \return false We did not found a strong edge point in its 8-connected neighborhood. */ @@ -151,10 +151,10 @@ class vpCannyEdgeDetection /** * \brief Construct a new vpCannyEdgeDetection object. * - * \param[in] gaussianKernelSize The size of the Gaussian filter kernel. Must be odd. - * \param[in] gaussianStdev The standard deviation of the Gaussian filter. - * \param[in] lowerThreshold The lower threshold of the hysteresis thresholding step. If negative, will be computed from the upper threshold. - * \param[in] upperThreshold The upper threshold of the hysteresis thresholding step. If negative, will be computed from the median of the gray values of the image. + * \param[in] gaussianKernelSize : The size of the Gaussian filter kernel. Must be odd. + * \param[in] gaussianStdev : The standard deviation of the Gaussian filter. + * \param[in] lowerThreshold : The lower threshold of the hysteresis thresholding step. If negative, will be computed from the upper threshold. + * \param[in] upperThreshold : The upper threshold of the hysteresis thresholding step. If negative, will be computed from the median of the gray values of the image. */ vpCannyEdgeDetection(const int &gaussianKernelSize, const float &gaussianStdev, const float &lowerThreshold = -1., const float &upperThreshold = -1.); @@ -164,7 +164,7 @@ class vpCannyEdgeDetection /** * \brief Construct a new vpCannyEdgeDetection object. * - * \param[in] jsonPath The path towards the JSON file to use to initialize the vpCannyEdgeDetection object. + * \param[in] jsonPath : The path towards the JSON file to use to initialize the vpCannyEdgeDetection object. */ vpCannyEdgeDetection(const std::string &jsonPath); @@ -173,7 +173,7 @@ class vpCannyEdgeDetection * whose path is \b jsonPath. Throw a \b vpException error if the file * does not exist. * - * \param[in] jsonPath The path towards the JSON configuration file. + * \param[in] jsonPath : The path towards the JSON configuration file. */ void initFromJSON(const std::string &jsonPath); @@ -181,8 +181,8 @@ class vpCannyEdgeDetection * \brief Read the detector configuration from JSON. All values are optional and if an argument is not present, * the default value defined in the constructor is kept * - * \param j The JSON object, resulting from the parsing of a JSON file. - * \param detector The detector, that will be initialized from the JSON data. + * \param j : The JSON object, resulting from the parsing of a JSON file. + * \param detector : The detector that will be initialized from the JSON data. */ inline friend void from_json(const json &j, vpCannyEdgeDetection &detector) { @@ -195,8 +195,8 @@ class vpCannyEdgeDetection /** * \brief Parse a vpCircleHoughTransform into JSON format. * - * \param j A JSON parser object. - * \param config The vpCircleHoughTransform that must be parsed into JSON format. + * \param j : A JSON parser object. + * \param detector : The vpCannyEdgeDetection object that must be parsed into JSON format. */ inline friend void to_json(json &j, const vpCannyEdgeDetection &detector) { @@ -226,7 +226,7 @@ class vpCannyEdgeDetection * \brief Detect the edges in an image. * Convert the color image into a gray-scale image. * - * \param[in] I_color An RGB image, in ViSP format. + * \param[in] I_color : An RGB image, in ViSP format. * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const vpImage &I_color); @@ -234,7 +234,7 @@ class vpCannyEdgeDetection /** * \brief Detect the edges in a gray-scale image. * - * \param[in] I A gray-scale image, in ViSP format. + * \param[in] I : A gray-scale image, in ViSP format. * \return vpImage 255 means an edge, 0 means not an edge. */ vpImage detect(const vpImage &I); @@ -245,8 +245,8 @@ class vpCannyEdgeDetection /** * \brief Set the Gradients of the image that will be processed. * - * \param[in] dIx Gradient along the horizontal axis of the image. - * \param[in] dIy Gradient along the vertical axis of the image. + * \param[in] dIx : Gradient along the horizontal axis of the image. + * \param[in] dIy : Gradient along the vertical axis of the image. */ inline void setGradients(const vpImage &dIx, const vpImage &dIy) { @@ -260,8 +260,8 @@ class vpCannyEdgeDetection * Edge point candidates whose gradient is between these two values is kept only if it * linked somehow to a strong edge point. * - * \param[in] lowerThresh The lower threshold: each point whose gradient is below this threshold is discarded. - * \param[in] upperThresh The upper threshold: each point whose gradient is greater than this threshold is + * \param[in] lowerThresh : The lower threshold: each point whose gradient is below this threshold is discarded. + * \param[in] upperThresh : The upper threshold: each point whose gradient is greater than this threshold is * said to be a strong edge point and is kept. */ inline void setCannyThresholds(const float &lowerThresh, const float &upperThresh) @@ -274,8 +274,8 @@ class vpCannyEdgeDetection * @brief Set the Gaussian Filters kernel size and standard deviation * and initialize the aforementioned filters. * - * \param[in] kernelSize The size of the Gaussian filters kernel. - * \param[in] stdev The standard deviation of the Gaussian filters used to blur and + * \param[in] kernelSize : The size of the Gaussian filters kernel. + * \param[in] stdev : The standard deviation of the Gaussian filters used to blur and * compute the gradient of the image. */ inline void setGaussianFilterParameters(const int &kernelSize, const float &stdev) diff --git a/modules/core/include/visp3/core/vpImageFilter.h b/modules/core/include/visp3/core/vpImageFilter.h index 7629b6a192..81ed7ed742 100644 --- a/modules/core/include/visp3/core/vpImageFilter.h +++ b/modules/core/include/visp3/core/vpImageFilter.h @@ -96,14 +96,13 @@ class VISP_EXPORT vpImageFilter template static double derivativeFilterY(const vpImage &I, unsigned int r, unsigned int c) { return (2047.0 * (I[r + 1][c] - I[r - 1][c]) + 913.0 * (I[r + 2][c] - I[r - 2][c]) + - 112.0 * (I[r + 3][c] - I[r - 3][c])) / - 8418.0; + 112.0 * (I[r + 3][c] - I[r - 3][c])) / 8418.0; } /*! Apply a 1 x size Derivative Filter in X to an image pixel. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I : Image to filter \param r : coordinates (row) of the pixel \param c : coordinates (column) of the pixel @@ -115,8 +114,7 @@ class VISP_EXPORT vpImageFilter */ template - static FilterType derivativeFilterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static FilterType derivativeFilterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, unsigned int size) { unsigned int i; FilterType result; @@ -130,21 +128,20 @@ class VISP_EXPORT vpImageFilter } /*! - Apply a size x 1 Derivative Filter in Y to an image pixel. + Apply a size x 1 Derivative Filter in Y to an image pixel. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. - \param I : Image to filter - \param r : coordinates (row) of the pixel - \param c : coordinates (column) of the pixel - \param filter : coefficients of the filter to be initialized using - vpImageFilter::getGaussianDerivativeKernel(). \param size : size of the - filter + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + \param I : Image to filter. + \param r : Coordinates (row) of the pixel. + \param c : Coordinates (column) of the pixel. + \param filter : Coefficients of the filter to be initialized using + vpImageFilter::getGaussianDerivativeKernel(). + \param size : Size of the filter. - \sa vpImageFilter::getGaussianDerivativeKernel() - */ + \sa vpImageFilter::getGaussianDerivativeKernel() + */ template - static FilterType derivativeFilterY(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static FilterType derivativeFilterY(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, unsigned int size) { unsigned int i; FilterType result; @@ -158,32 +155,32 @@ class VISP_EXPORT vpImageFilter } /*! - Apply a filter to an image. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. - \param I : Image to filter - \param If : Filtered image. - \param M : Filter kernel. - \param convolve : If true, perform a convolution otherwise a correlation. - - \note By default it performs a correlation: - \f[ - \textbf{I\_filtered} \left( u,v \right) = - \sum_{y=0}^{\textbf{kernel\_h}} - \sum_{x=0}^{\textbf{kernel\_w}} - \textbf{M} \left( x,y \right ) \times - \textbf{I} \left( - u-\frac{\textbf{kernel\_w}}{2}+x,v-\frac{\textbf{kernel\_h}}{2}+y \right) - \f] - The convolution is almost the same operation: - \f[ - \textbf{I\_filtered} \left( u,v \right) = - \sum_{y=0}^{\textbf{kernel\_h}} - \sum_{x=0}^{\textbf{kernel\_w}} - \textbf{M} \left( x,y \right ) \times - \textbf{I} \left( - u+\frac{\textbf{kernel\_w}}{2}-x,v+\frac{\textbf{kernel\_h}}{2}-y \right) - \f] - Only pixels in the input image fully covered by the kernel are considered. + Apply a filter to an image. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + \param I : Image to filter + \param If : Filtered image. + \param M : Filter kernel. + \param convolve : If true, perform a convolution otherwise a correlation. + + \note By default it performs a correlation: + \f[ + \textbf{I\_filtered} \left( u,v \right) = + \sum_{y=0}^{\textbf{kernel\_h}} + \sum_{x=0}^{\textbf{kernel\_w}} + \textbf{M} \left( x,y \right ) \times + \textbf{I} \left( + u-\frac{\textbf{kernel\_w}}{2}+x,v-\frac{\textbf{kernel\_h}}{2}+y \right) + \f] + The convolution is almost the same operation: + \f[ + \textbf{I\_filtered} \left( u,v \right) = + \sum_{y=0}^{\textbf{kernel\_h}} + \sum_{x=0}^{\textbf{kernel\_w}} + \textbf{M} \left( x,y \right ) \times + \textbf{I} \left( + u+\frac{\textbf{kernel\_w}}{2}-x,v+\frac{\textbf{kernel\_h}}{2}-y \right) + \f] + Only pixels in the input image fully covered by the kernel are considered. */ template static void filter(const vpImage &I, vpImage &If, const vpArray2D &M, bool convolve = false) @@ -226,16 +223,16 @@ class VISP_EXPORT vpImageFilter } /*! - Apply a filter to an image: - \f[ - \textbf{I}_u = \textbf{M} \ast \textbf{I} \textbf{ and } \textbf{I}_v = - \textbf{M}^t \ast \textbf{I} \f] - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. - \param I : Image to filter - \param Iu : Filtered image along the horizontal axis (u = columns). - \param Iv : Filtered image along the vertical axis (v = rows). - \param M : Filter kernel. - \param convolve : If true, perform a convolution otherwise a correlation. + Apply a filter to an image: + \f[ + \textbf{I}_u = \textbf{M} \ast \textbf{I} \textbf{ and } \textbf{I}_v = + \textbf{M}^t \ast \textbf{I} \f] + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + \param I : Image to filter + \param Iu : Filtered image along the horizontal axis (u = columns). + \param Iv : Filtered image along the vertical axis (v = rows). + \param M : Filter kernel. + \param convolve : If true, perform a convolution otherwise a correlation. */ template static void filter(const vpImage &I, vpImage &Iu, vpImage &Iv, const vpArray2D &M, @@ -285,20 +282,18 @@ class VISP_EXPORT vpImageFilter } } - static void sepFilter(const vpImage &I, vpImage &If, const vpColVector &kernelH, - const vpColVector &kernelV); + static void sepFilter(const vpImage &I, vpImage &If, const vpColVector &kernelH, const vpColVector &kernelV); /*! Apply a separable filter. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. - \param I: The original image. - \param GI: The filtered image. - \param filter: The separable filter. - \param size: The size of the filter. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + \param I : The original image. + \param GI : The filtered image. + \param filter : The separable filter. + \param size : The size of the filter. */ template - static void filter(const vpImage &I, vpImage &GI, const FilterType *filter, - unsigned int size) + static void filter(const vpImage &I, vpImage &GI, const FilterType *filter, unsigned int size) { vpImage GIx; filterX(I, GIx, filter, size); @@ -308,7 +303,7 @@ class VISP_EXPORT vpImageFilter /*! Apply a separable filter. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I: The original image. \param GI: The filtered image. \param filter: The separable filter. @@ -325,18 +320,15 @@ class VISP_EXPORT vpImageFilter static inline unsigned char filterGaussXPyramidal(const vpImage &I, unsigned int i, unsigned int j) { - return (unsigned char)((1. * I[i][j - 2] + 4. * I[i][j - 1] + 6. * I[i][j] + 4. * I[i][j + 1] + 1. * I[i][j + 2]) / - 16.); + return (unsigned char)((1. * I[i][j - 2] + 4. * I[i][j - 1] + 6. * I[i][j] + 4. * I[i][j + 1] + 1. * I[i][j + 2]) / 16.); } static inline unsigned char filterGaussYPyramidal(const vpImage &I, unsigned int i, unsigned int j) { - return (unsigned char)((1. * I[i - 2][j] + 4. * I[i - 1][j] + 6. * I[i][j] + 4. * I[i + 1][j] + 1. * I[i + 2][j]) / - 16.); + return (unsigned char)((1. * I[i - 2][j] + 4. * I[i - 1][j] + 6. * I[i][j] + 4. * I[i + 1][j] + 1. * I[i + 2][j]) / 16.); } template - static void filterX(const vpImage &I, vpImage &dIx, const FilterType *filter, - unsigned int size) + static void filterX(const vpImage &I, vpImage &dIx, const FilterType *filter, unsigned int size) { dIx.resize(I.getHeight(), I.getWidth()); for (unsigned int i = 0; i < I.getHeight(); i++) { @@ -375,8 +367,7 @@ class VISP_EXPORT vpImageFilter static void filterXB(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); template - static inline FilterType filterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, unsigned int size) { FilterType result; @@ -388,8 +379,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c]; } - static inline double filterXR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterXR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -401,8 +391,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c].R; } - static inline double filterXG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterXG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -414,8 +403,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c].G; } - static inline double filterXB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterXB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -429,7 +417,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterXLeftBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -494,7 +482,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterXRightBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -558,8 +546,8 @@ class VISP_EXPORT vpImageFilter } template - static inline FilterType filterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterX(const vpImage &I, unsigned int r, unsigned int c, + const FilterType *filter, unsigned int size) { FilterType result; @@ -572,8 +560,8 @@ class VISP_EXPORT vpImageFilter } template - static inline FilterType filterXLeftBorder(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterXLeftBorder(const vpImage &I, unsigned int r, unsigned int c, + const FilterType *filter, unsigned int size) { FilterType result; @@ -590,7 +578,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterXRightBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -606,8 +594,7 @@ class VISP_EXPORT vpImageFilter } template - static void filterY(const vpImage &I, vpImage &dIy, const FilterType *filter, - unsigned int size) + static void filterY(const vpImage &I, vpImage &dIy, const FilterType *filter, unsigned int size) { dIy.resize(I.getHeight(), I.getWidth()); for (unsigned int i = 0; i < (size - 1) / 2; i++) { @@ -654,8 +641,7 @@ class VISP_EXPORT vpImageFilter } template - static inline FilterType filterY(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterY(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, unsigned int size) { FilterType result; @@ -667,8 +653,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c]; } - static inline double filterYR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterYR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -679,8 +664,7 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c].R; } - static inline double filterYG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterYG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -691,8 +675,7 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c].G; } - static inline double filterYB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + static inline double filterYB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -706,7 +689,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterYTopBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -721,8 +704,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c]; } - double static inline filterYTopBorderR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + double static inline filterYTopBorderR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -737,8 +719,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c].R; } - double static inline filterYTopBorderG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + double static inline filterYTopBorderG(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -753,8 +734,7 @@ class VISP_EXPORT vpImageFilter return result + filter[0] * I[r][c].G; } - double static inline filterYTopBorderB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, - unsigned int size) + double static inline filterYTopBorderB(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -771,7 +751,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterYBottomBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -835,8 +815,8 @@ class VISP_EXPORT vpImageFilter } template - static inline FilterType filterYTopBorder(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterYTopBorder(const vpImage &I, unsigned int r, unsigned int c, + const FilterType *filter, unsigned int size) { FilterType result; @@ -853,7 +833,7 @@ class VISP_EXPORT vpImageFilter template static inline FilterType filterYBottomBorder(const vpImage &I, unsigned int r, unsigned int c, - const FilterType *filter, unsigned int size) + const FilterType *filter, unsigned int size) { FilterType result; @@ -869,8 +849,8 @@ class VISP_EXPORT vpImageFilter } template - static inline FilterType filterY(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, - unsigned int size) + static inline FilterType filterY(const vpImage &I, unsigned int r, unsigned int c, + const FilterType *filter, unsigned int size) { FilterType result; @@ -884,7 +864,7 @@ class VISP_EXPORT vpImageFilter /*! Apply a Gaussian blur to an image. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I : Input image. \param GI : Filtered image. \param size : Filter size. This value should be odd. @@ -896,8 +876,7 @@ class VISP_EXPORT vpImageFilter \sa getGaussianKernel() to know which kernel is used. */ template - static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, FilterType sigma = 0., - bool normalize = true) + static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, FilterType sigma = 0., bool normalize = true) { FilterType *fg = new FilterType[(size + 1) / 2]; vpImageFilter::getGaussianKernel(fg, size, sigma, normalize); @@ -908,12 +887,11 @@ class VISP_EXPORT vpImageFilter delete[] fg; } - static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, double sigma = 0., - bool normalize = true); + static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, double sigma = 0., bool normalize = true); /*! Apply a Gaussian blur to a double image. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I : Input double image. \param GI : Filtered image. \param size : Filter size. This value should be odd. @@ -924,8 +902,7 @@ class VISP_EXPORT vpImageFilter \sa getGaussianKernel() to know which kernel is used. */ template - static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, FilterType sigma = 0., - bool normalize = true) + static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, FilterType sigma = 0., bool normalize = true) { FilterType *fg = new FilterType[(size + 1) / 2]; vpImageFilter::getGaussianKernel(fg, size, sigma, normalize); @@ -945,23 +922,21 @@ class VISP_EXPORT vpImageFilter */ template static double gaussianFilter(const vpImage &fr, unsigned int r, unsigned int c) { - // filter Gaussien return (15.0 * fr[r][c] + 12.0 * (fr[r - 1][c] + fr[r][c - 1] + fr[r + 1][c] + fr[r][c + 1]) + 9.0 * (fr[r - 1][c - 1] + fr[r + 1][c - 1] + fr[r - 1][c + 1] + fr[r + 1][c + 1]) + 5.0 * (fr[r - 2][c] + fr[r][c - 2] + fr[r + 2][c] + fr[r][c + 2]) + 4.0 * (fr[r - 2][c + 1] + fr[r - 2][c - 1] + fr[r - 1][c - 2] + fr[r + 1][c - 2] + fr[r + 2][c - 1] + fr[r + 2][c + 1] + fr[r - 1][c + 2] + fr[r + 1][c + 2]) + - 2.0 * (fr[r - 2][c - 2] + fr[r + 2][c - 2] + fr[r - 2][c + 2] + fr[r + 2][c + 2])) / - 159.0; + 2.0 * (fr[r - 2][c - 2] + fr[r + 2][c - 2] + fr[r - 2][c + 2] + fr[r + 2][c + 2])) / 159.0; } - // Gaussain pyramid operation + // Gaussian pyramid operation static void getGaussPyramidal(const vpImage &I, vpImage &GI); static void getGaussXPyramidal(const vpImage &I, vpImage &GI); static void getGaussYPyramidal(const vpImage &I, vpImage &GI); /*! Return the coefficients \f$G_i\f$ of a Gaussian filter. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param[out] filter : Pointer to the half size filter kernel that should refer to a (size+1)/2 array. The first value refers to the central coefficient, the next one to the right coefficients. Left coefficients could be deduced by @@ -972,7 +947,7 @@ class VISP_EXPORT vpImageFilter \param[in] normalize : Flag indicating whether to normalize the filter coefficients or not. In that case \f$\Sigma G_i = 1 \f$. - The function computes the \e (size+1)/2 values of the Gaussian filter cooefficients \f$ G_i \f$ as: + The function computes the \e (size+1)/2 values of the Gaussian filter coefficients \f$ G_i \f$ as: \f[ G_i = \frac{1}{\sigma \sqrt{2 \pi}} \exp{(-i^2 / (2. * \sigma^2))}\f] */ template @@ -1007,7 +982,7 @@ class VISP_EXPORT vpImageFilter Return the coefficients of a Gaussian derivative filter that may be used to compute spatial image derivatives after applying a Gaussian blur. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param filter : Pointer to the filter kernel that should refer to a (size+1)/2 array. The first value refers to the central coefficient, the next one to the right coefficients. Left coefficients could be deduced by @@ -1086,7 +1061,7 @@ class VISP_EXPORT vpImageFilter /*! Compute the gradient along X after applying a gaussian filter along Y. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I : Input image \param dIx : Gradient along X. \param gaussianKernel : Gaussian kernel which values should be computed using vpImageFilter::getGaussianKernel(). @@ -1148,7 +1123,7 @@ class VISP_EXPORT vpImageFilter /*! Compute the gradient along Y after applying a gaussian filter along X. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param I : Input image \param dIy : Gradient along Y. \param gaussianKernel : Gaussian kernel which values should be computed using vpImageFilter::getGaussianKernel(). @@ -1167,7 +1142,7 @@ class VISP_EXPORT vpImageFilter /*! Get Sobel kernel for X-direction. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. \param filter : Pointer to a double array already allocated. \param size : Kernel size computed as: kernel_size = size*2 + 1 (max size is 20). \return Scaling factor. @@ -1187,12 +1162,12 @@ class VISP_EXPORT vpImageFilter } /*! - Get Sobel kernel for Y-direction. - \tparam FilterType: Either float, to accelerate the computation time, or double, to have greater precision. - \param filter : Pointer to a double array already allocated. - \param size : Kernel size computed as: kernel_size = size*2 + 1 (max size is 20). - \return Scaling factor. - */ + Get Sobel kernel for Y-direction. + \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + \param filter : Pointer to a double array already allocated. + \param size : Kernel size computed as: kernel_size = size*2 + 1 (max size is 20). + \return Scaling factor. + */ template inline static FilterType getSobelKernelY(FilterType *filter, unsigned int size) { diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp index 908ce22a54..9d4584f0d6 100644 --- a/modules/core/src/image/vpCannyEdgeDetection.cpp +++ b/modules/core/src/image/vpCannyEdgeDetection.cpp @@ -1,3 +1,35 @@ +/**************************************************************************** + * + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * +*****************************************************************************/ + #include #include @@ -110,7 +142,7 @@ vpCannyEdgeDetection::detect(const vpImage &I) float lowerThreshold = m_lowerThreshold; if (m_lowerThreshold < 0) { - // Applying Canny recommandation to have the upper threshold 3 times greater than the lower threshold. + // Applying Canny recommendation to have the upper threshold 3 times greater than the lower threshold. lowerThreshold = m_upperThreshold / 3.; } @@ -139,14 +171,14 @@ vpCannyEdgeDetection::performFilteringAndGradientComputation(const vpImage &dIx, const vpImage &dIy, const /** * @brief Get the absolute value of the gradient orientation. * - * @param dIx Gradient along the horizontal axis. - * @param dIy Gradient along the vertical axis. - * @param row Index along the vertical axis. - * @param col Index along the horizontal axis. + * @param dIx : Gradient along the horizontal axis. + * @param dIy : Gradient along the vertical axis. + * @param row : Index along the vertical axis. + * @param col : Index along the horizontal axis. * @return float The absolute value of the gradient orientation, expressed in degrees. */ float diff --git a/modules/core/src/image/vpImageFilter.cpp b/modules/core/src/image/vpImageFilter.cpp index 56f583537c..9657f8c4a3 100644 --- a/modules/core/src/image/vpImageFilter.cpp +++ b/modules/core/src/image/vpImageFilter.cpp @@ -78,7 +78,7 @@ void vpImageFilter::filter(const vpImage &I, vpImage &Iu \right ] \ast \left [ \begin{matrix} - 1 && 0 && -1 + 1 & 0 & -1 \end{matrix} \right ] \f] @@ -95,7 +95,7 @@ void vpImageFilter::filter(const vpImage &I, vpImage &Iu \left ( \left [ \begin{matrix} - 1 && 0 && -1 + 1 & 0 & -1 \end{matrix} \right ] \ast I \right ) @@ -176,11 +176,11 @@ int main() \param gaussianFilterSize : The size of the mask of the Gaussian filter to apply (an odd number). \param thresholdCanny : The threshold for the Canny operator. Only value - greater than this value are marked as an edge). + greater than this value are marked as an edge. \param apertureSobel : Size of the mask for the Sobel operator (odd number). */ void vpImageFilter::canny(const vpImage &Isrc, vpImage &Ires, - unsigned int gaussianFilterSize, double thresholdCanny, unsigned int apertureSobel) + unsigned int gaussianFilterSize, double thresholdCanny, unsigned int apertureSobel) { #if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) cv::Mat img_cvmat, edges_cvmat; @@ -319,12 +319,10 @@ void vpImageFilter::filterY(const vpImage &I, vpImage &d } template<> -void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, float sigma, - bool normalize); +void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, float sigma, bool normalize); template<> -void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, - bool normalize); +void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, bool normalize); /** * \endcond */ @@ -340,8 +338,7 @@ void vpImageFilter::gaussianBlur(const vpImage &I, vpImag \sa getGaussianKernel() to know which kernel is used. */ -void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, - bool normalize) +void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, bool normalize) { double *fg = new double[(size + 1) / 2]; vpImageFilter::getGaussianKernel(fg, size, sigma, normalize); @@ -356,12 +353,10 @@ void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, * \cond DO_NOT_DOCUMENT */ template<> -void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, float sigma, - bool normalize); +void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, float sigma, bool normalize); template<> -void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, - bool normalize); +void vpImageFilter::gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size, double sigma, bool normalize); template<> void vpImageFilter::getGaussianKernel(float *filter, unsigned int size, float sigma, bool normalize); @@ -410,40 +405,40 @@ void vpImageFilter::getGradY(const vpImage &I, vpImage void vpImageFilter::getGradXGauss2D(const vpImage &I, vpImage &dIx, const float *gaussianKernel, - const float *gaussianDerivativeKernel, unsigned int size); + const float *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradXGauss2D(const vpImage &I, vpImage &dIx, const double *gaussianKernel, - const double *gaussianDerivativeKernel, unsigned int size); + const double *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradXGauss2D(const vpImage &I, vpImage &dIx, const float *gaussianKernel, - const float *gaussianDerivativeKernel, unsigned int size); + const float *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradXGauss2D(const vpImage &I, vpImage &dIx, const double *gaussianKernel, - const double *gaussianDerivativeKernel, unsigned int size); + const double *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradYGauss2D(const vpImage &I, vpImage &dIy, const float *gaussianKernel, - const float *gaussianDerivativeKernel, unsigned int size); + const float *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradYGauss2D(const vpImage &I, vpImage &dIy, const double *gaussianKernel, - const double *gaussianDerivativeKernel, unsigned int size); + const double *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradYGauss2D(const vpImage &I, vpImage &dIy, const float *gaussianKernel, - const float *gaussianDerivativeKernel, unsigned int size); + const float *gaussianDerivativeKernel, unsigned int size); template<> void vpImageFilter::getGradYGauss2D(const vpImage &I, vpImage &dIy, const double *gaussianKernel, - const double *gaussianDerivativeKernel, unsigned int size); + const double *gaussianDerivativeKernel, unsigned int size); /** * \endcond */ - // operation pour pyramide gaussienne + // Operation for Gaussian pyramid void vpImageFilter::getGaussPyramidal(const vpImage &I, vpImage &GI) { vpImage GIx; @@ -467,16 +462,6 @@ void vpImageFilter::getGaussPyramidal(const vpImage &I, vpImage &I, vpImage &GI) { -#if 0 - GI.resize(I.getHeight(), (int)((I.getWidth() + 1.) / 2.)); - for (unsigned int i = 0; i < I.getHeight(); i++) { - GI[i][0] = I[i][0]; - for (unsigned int j = 1; j < ((I.getWidth() + 1.) / 2.) - 1; j++) { - GI[i][j] = vpImageFilter::filterGaussXPyramidal(I, i, 2 * j); - } - GI[i][(int)((I.getWidth() + 1.) / 2.) - 1] = I[i][2 * ((int)((I.getWidth() + 1.) / 2.) - 1)]; - } -#else unsigned int w = I.getWidth() / 2; GI.resize(I.getHeight(), w); @@ -487,23 +472,9 @@ void vpImageFilter::getGaussXPyramidal(const vpImage &I, vpImage< } GI[i][w - 1] = I[i][2 * w - 1]; } - -#endif } void vpImageFilter::getGaussYPyramidal(const vpImage &I, vpImage &GI) { - -#ifdef ORIG - GI.resize((int)((I.getHeight() + 1.) / 2.), I.getWidth()); - for (unsigned int j = 0; j < I.getWidth(); j++) { - GI[0][j] = I[0][j]; - for (unsigned int i = 1; i < ((I.getHeight() + 1.) / 2.) - 1; i++) { - GI[i][j] = vpImageFilter::filterGaussYPyramidal(I, 2 * i, j); - } - GI[(int)((I.getHeight() + 1.) / 2.) - 1][j] = I[2 * ((int)((I.getHeight() + 1.) / 2.) - 1)][j]; - } - -#else unsigned int h = I.getHeight() / 2; GI.resize(h, I.getWidth()); @@ -514,7 +485,6 @@ void vpImageFilter::getGaussYPyramidal(const vpImage &I, vpImage< } GI[h - 1][j] = I[2 * h - 1][j]; } -#endif } /** @@ -533,4 +503,4 @@ template<> float vpImageFilter::getSobelKernelY(float *filter, unsigned int size); /** * \endcond - */ \ No newline at end of file + */ From ad4cbc7d27f1af85c2ba2eea36e2e8de073ea339 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 29 Aug 2023 22:26:12 +0200 Subject: [PATCH 7/8] Add missing VISP_EXPORT --- modules/core/include/visp3/core/vpCannyEdgeDetection.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h index 6f2b1c2773..cc2b1f396f 100644 --- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -48,7 +48,7 @@ using json = nlohmann::json; #endif -class vpCannyEdgeDetection +class VISP_EXPORT vpCannyEdgeDetection { private: typedef enum EdgeType From 7d618c22e7386e28a39c17c7f3fe5f7cc6f7c4c6 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 30 Aug 2023 09:57:27 +0200 Subject: [PATCH 8/8] Fix test result --- modules/core/test/image-with-dataset/testImageFilter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/test/image-with-dataset/testImageFilter.cpp b/modules/core/test/image-with-dataset/testImageFilter.cpp index e2b895f595..e6c2be967d 100644 --- a/modules/core/test/image-with-dataset/testImageFilter.cpp +++ b/modules/core/test/image-with-dataset/testImageFilter.cpp @@ -556,7 +556,7 @@ int main(int argc, const char *argv[]) std::vector expected_median_rgba = { 4, 8, 12 }; for (unsigned int i = 0; i < 3; i++) { bool test_local = (median_rgba[i] == expected_median_rgba[i]); - test &= test; + test &= test_local; std::cout << "(median_rgba[" << i << "] (=" << median_rgba[i] << ") == expected_median_rgba[" << i << "] ( " << expected_median_rgba[i] << "))? " << test_local << std::endl; } if (!test) {