Skip to content

Commit

Permalink
[CORPS] Added a ViSP implementation of the Canny edge detector, that …
Browse files Browse the repository at this point in the history
…does not rely on OpenCV imgproc
  • Loading branch information
rlagneau committed Aug 11, 2023
1 parent 6794698 commit c3eb4a1
Show file tree
Hide file tree
Showing 6 changed files with 784 additions and 104 deletions.
25 changes: 13 additions & 12 deletions doc/tutorial/image/tutorial-image-filtering.dox
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
287 changes: 287 additions & 0 deletions modules/core/include/visp3/core/vpCannyEdgeDetection.h
Original file line number Diff line number Diff line change
@@ -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 [email protected]
*
* 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 <map>
#include <vector>

// ViSP include
#include <visp3/core/vpConfig.h>
#include <visp3/core/vpImage.h>
#include <visp3/core/vpImageFilter.h>

// 3rd parties include
#ifdef VISP_HAVE_NLOHMANN_JSON
#include <nlohmann/json.hpp>
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<float> m_fg; /*!< Array that contains the Gaussian kernel.*/
vpArray2D<float> m_fgDg; /*!< Array that contains the derivative of the Gaussian kernel.*/
vpImage<float> m_dIx; /*!< X-axis gradient.*/
vpImage<float> m_dIy; /*!< Y-axis gradient.*/

// // Edge thining attributes
std::map<std::pair<unsigned int, unsigned int>, 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<std::pair<unsigned int, unsigned int>, 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<unsigned char> 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<unsigned char> &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<unsigned int, unsigned int> &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<unsigned char> 255 means an edge, 0 means not an adge.
*/
vpImage<unsigned char> 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<unsigned char> 255 means an edge, 0 means not an adge.
*/
vpImage<unsigned char> detect(const vpImage<vpRGBa> &I_color);

/**
* \brief Detect the edges in a gray-scale image.
*
* \param[in] I A gray-scale image, in ViSP format.
* \return vpImage<unsigned char> 255 means an edge, 0 means not an adge.
*/
vpImage<unsigned char> detect(const vpImage<unsigned char> &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<float> &dIx, const vpImage<float> &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
Loading

0 comments on commit c3eb4a1

Please sign in to comment.