diff --git a/doc/tutorial/imgproc/tutorial-imgproc-cht.dox b/doc/tutorial/imgproc/tutorial-imgproc-cht.dox index 852fdf6fe2..0c7411425a 100644 --- a/doc/tutorial/imgproc/tutorial-imgproc-cht.dox +++ b/doc/tutorial/imgproc/tutorial-imgproc-cht.dox @@ -22,7 +22,9 @@ we vote along a straight line that follows the gradient. Then, during the step where the algorithm votes for radius candidates for each center candidate, we check the colinearity between the gradient at a considered point and the line which links the point towards the center candidate. If they are "enough" colinear, we increment the corresponding -radius bin vote by 1. The "enough" characteristic is controlled by the circle perfectness +radius bin vote by 1. (*NB*: instead of incrementing one bin by one, we increment two bins by a number between +0 and 1 in our implementation to be more robust against the limits min and max of the radius and +the bin size). The "enough" characteristic is controlled by the circle perfectness parameter. \image html img-tutorial-cht-radius-votes.png @@ -40,44 +42,53 @@ $ cd tutorial/imgproc/hough-transform $ ./tutorial-circle-hough --help ``` -\subsection imgproc_cht_howto_synthetic How to use synthetic images - - -To run the software on the synthetic images using a JSON configuration file, -please run: +To run the software on an image like `coins2.jpg` provided with the tutorial and using a JSON configuration file, please run: ``` -$ TARGET=full # or TARGET=half # or TARGET=quarter -$ ./tutorial-circle-hough --input ${TARGET}_disks --config config/detector_${TARGET}.json +$ ./tutorial-circle-hough --input coins2.jpg --config config/detector_img.json ``` -To run the software on the synthetic images using the default parameters, -please run: -``` -$ TARGET=full # or TARGET=half # or TARGET=quarter -$ ./tutorial-circle-hough --input ${TARGET}_disks -``` - -\subsection imgproc_cht_howto_images How to use actual images - -To run the software on an actual image like `coins2.jpg` provided with the tutorial and using a JSON configuration file, please run: -``` -$ ./tutorial-circle-hough --input coins2.jpg --config config/detector_img.json +If you would rather use the command line arguments, please run: +``` +$ ./tutorial-circle-hough --input coins2.jpg \ + --averaging-window-size 5 \ + --canny-backend opencv-backend \ + --filtering-type gaussianblur+scharr-filtering \ + --canny-thresh -1 -1 \ + --lower-canny-ratio 0.6 \ + --upper-canny-ratio 0.9 \ + --gaussian-kernel 5 \ + --gaussian-sigma 1 \ + --dilatation-kernel-size 5 \ + --center-thresh 70 \ + --circle-probability-thresh 0.725 \ + --radius-limits 34 75 \ + --merging-thresh 5 5 \ + --circle-perfectness 0.65 \ + --circle-probability-thresh 0.725 \ + --center-xlim 0 1920 \ + --center-ylim 0 1080 \ + --expected-nb-centers -1 \ + --edge-filter 3 \ + --gradient-kernel 3 ``` \note The configuration file `config/detector_img.json` has been tuned to detect circles in the image `coins2.jpg`. -If the detections seem a bit off, you might need to change the parameters in `config/detector_img.json`. +If the detections seem a bit off, you might need to change the parameters in `config/detector_img.json` or in the +command line. -To run the software on an actual image using command line arguments instead, please run: +\note The default values of the program corresponds to these fine-tuned parameters. Running the program +without any additionnal parameters should give the same result: ``` -$ ./tutorial-circle-hough --input /path/to/my/image --gaussian-kernel 5 --gaussian-sigma 1 --canny-thresh 100. 200. --dilatation-repet 1 --center-thresh 200 --radius-bin 2 --circle-probability-thresh 0.75 --radius-limits 80 90 --merging-thresh 15 2 --circle-perfectness 0.9 +./tutorial-circle-hough ``` -If the detections seem a bit off, you might need to change the parameters - \subsection imgproc_cht_howto_video How to use a video You can use the software to run circle detection on a video saved as a -sequence of images that are named `${BASENAME}%d.png`. +sequence of images that are named +``` +${BASENAME}%d.png +``` For instance with `${BASENAME}` = `video_`, you can have the following list of images: `video_0001.png`, `video_0002.png` and so on. @@ -88,22 +99,31 @@ $ ./tutorial-circle-hough --input /path/to/video/${BASENAME}%d.png --config conf To run the software using the command arguments, please run: ``` -./tutorial-circle-hough --input /path/to/video/${BASENAME}%d.png --gaussian-kernel 5 --gaussian-sigma 1 --canny-thresh -1. --dilatation-repet 1 --center-thresh 200 --radius-bin 2 --radius-thresh 2 --radius-limits 80 90 --merging-thresh 15 2 --circle-perfectness 0.9 +$ ./tutorial-circle-hough --input /path/to/video/${BASENAME}%d.png \ + --averaging-window-size 5 \ + --canny-backend opencv-backend \ + --filtering-type gaussianblur+scharr-filtering \ + --canny-thresh -1 -1 \ + --lower-canny-ratio 0.6 \ + --upper-canny-ratio 0.9 \ + --gaussian-kernel 5 \ + --gaussian-sigma 1 \ + --dilatation-kernel-size 5 \ + --center-thresh 70 \ + --circle-probability-thresh 0.725 \ + --radius-limits 34 75 \ + --merging-thresh 5 5 \ + --circle-perfectness 0.65 \ + --circle-probability-thresh 0.725 \ + --center-xlim 0 1920 \ + --center-ylim 0 1080 \ + --expected-nb-centers -1 \ + --edge-filter 3 \ + --gradient-kernel 3 ``` \section imgproc_cht_explanations Detailed explanations about the tutorial -An enumeration permits to choose between the different types of synthetic images -or using actual images or videos: - -\snippet tutorial-circle-hough.cpp Enum input - -You can choose the type you want using the command line arguments. To know how to do it, -please run: -``` -$ ./tutorial-circle-hough --help -``` - If you decide to use a video as input, the relevant piece of code that permits to perform circle detection on the successive images of the video is the following: \snippet tutorial-circle-hough.cpp Manage video @@ -112,16 +132,6 @@ If you decide to use a single image as input, the relevant piece of code that pe perform circle detection on the image is the following: \snippet tutorial-circle-hough.cpp Manage single image -If you decide to use a synthetic image as input, the relevant piece of code that -launches the detection on the synthetic image is the following: -\snippet tutorial-circle-hough.cpp Manage synthetic image - -The function that draws the synthetic image is the following: -\snippet tutorial-circle-hough.cpp Draw synthetic - -It relies on the following function to draw the disks: -\snippet tutorial-circle-hough.cpp Draw disks - If you did not use a JSON file to configure the `vpCircleHoughTransform` detector, the following structure defines the parameters of the algorithm based on the command line arguments: diff --git a/modules/core/include/visp3/core/vpImageMorphology.h b/modules/core/include/visp3/core/vpImageMorphology.h index cc83b2e251..0325cee9c6 100644 --- a/modules/core/include/visp3/core/vpImageMorphology.h +++ b/modules/core/include/visp3/core/vpImageMorphology.h @@ -91,15 +91,15 @@ class VISP_EXPORT vpImageMorphology /** * \brief Modify the image by applying the \b operation on each of its elements on a \b size x \b size - * grid. The connexity that is used is a 8-connexity. + * grid. * * \tparam T Any type such as double, unsigned char ... * \param[out] I The image we want to modify. - * \param[in] size The size of the window with which we want to work. - * \param[in] operation The operation to apply to its elements. + * \param[in] operation The operation to apply to its elements on a the grid. + * \param[in] size Size of the kernel of the operation. */ template - static void imageOperation(vpImage &I, const int &size, const T &(*operation)(const T &, const T &)); + static void imageOperation(vpImage &I, const T &(*operation)(const T &, const T &), const int &size = 3); public: template @@ -408,19 +408,11 @@ void vpImageMorphology::dilatation(vpImage &I, const vpConnexityType &connexi vpImageMorphology::imageOperation(I, std::numeric_limits::min(), operation, connexity); } -/** - * \brief Dilatation of \b size >=3 with 8-connectivity. - * - * \tparam T Any type of image, except vpRGBa . - * \param[out] I The image to which the dilatation must be applied, where the dilatation corresponds - * to a max operator on a window of size \b size. - * \param[in] size The size of the window on which is performed the max operator for each pixel. - */ template -void vpImageMorphology::imageOperation(vpImage &I, const int &size, const T &(*operation)(const T &, const T &)) +void vpImageMorphology::imageOperation(vpImage &I, const T &(*operation)(const T &, const T &), const int &size) { if (size % 2 != 1) { - throw(vpException(vpException::badValue, "Dilatation kernel must be odd.")); + throw(vpException(vpException::badValue, "Dilatation/erosion kernel must be odd.")); } const int width_in = I.getWidth(); @@ -458,6 +450,7 @@ void vpImageMorphology::imageOperation(vpImage &I, const int &size, const T & } /*! + * \brief Erosion of \b size >=3 with 8-connectivity. Erode an image using the given structuring element. The erosion of \f$ A \left( x, y \right) \f$ by \f$ B \left (x, y @@ -475,8 +468,10 @@ void vpImageMorphology::imageOperation(vpImage &I, const int &size, const T & \left ( x+x', y+y' \right ) | \left ( x', y'\right ) \subseteq D_B \right \} \f] - \param I : Image to process. - \param size : The size of the kernel + * \tparam T Any type of image, except vpRGBa . + * \param[out] I The image to which the erosion must be applied, where the erosion corresponds + * to a min operator on a window of size \b size. + * \param[in] size The size of the window on which is performed the min operator for each pixel. \sa dilatation(vpImage &, const int &) */ @@ -484,13 +479,13 @@ template void vpImageMorphology::erosion(vpImage &I, const int &size) { const T &(*operation)(const T & a, const T & b) = std::min; - vpImageMorphology::imageOperation(I, size, operation); + vpImageMorphology::imageOperation(I, operation, size); } -/*! - Dilate an image using the given structuring element. - - The dilatation of \f$ A \left( x, y \right) \f$ by \f$ B \left +/** + * \brief Dilatation of \b size >=3 with 8-connectivity. + * + * The dilatation of \f$ A \left( x, y \right) \f$ by \f$ B \left (x, y \right) \f$ is defined as: \f[ \left ( A \oplus B \right ) \left( x,y \right) = \textbf{max} \left \{ A \left ( x-x', y-y' \right ) + B \left ( x', y'\right ) | \left ( x', y'\right ) \subseteq D_B \right \} \f] where @@ -504,17 +499,19 @@ void vpImageMorphology::erosion(vpImage &I, const int &size) \left ( A \oplus B \right ) \left( x,y \right) = \textbf{max} \left \{ A \left ( x-x', y-y' \right ) | \left ( x', y'\right ) \subseteq D_B \right \} \f] - - \param I : Image to process. - \param size : The size of the kernel. - - \sa erosion(vpImage &, const int &) -*/ -template + * + * \tparam T Any type of image, except vpRGBa . + * \param[out] I The image to which the dilatation must be applied, where the dilatation corresponds + * to a max operator on a window of size \b size. + * \param[in] size The size of the window on which is performed the max operator for each pixel. + * + * \sa erosion(vpImage &, const int &) + */ +template void vpImageMorphology::dilatation(vpImage &I, const int &size) { const T &(*operation)(const T & a, const T & b) = std::max; - vpImageMorphology::imageOperation(I, size, operation); + vpImageMorphology::imageOperation(I, operation, size); } #endif diff --git a/modules/core/src/image/vpImageCircle.cpp b/modules/core/src/image/vpImageCircle.cpp index dfd8b6d7c3..e63c0545fa 100644 --- a/modules/core/src/image/vpImageCircle.cpp +++ b/modules/core/src/image/vpImageCircle.cpp @@ -965,7 +965,6 @@ float vpImageCircle::computeAngularCoverageInRoI(const vpRect &roi, const float computeIntersectionsTopRight(u_c, v_c, vmin_roi, umax_roi, radius, delta_theta); } else if (touchBottomBorder && touchTopBorder && touchLeftBorder && !touchRightBorder) { - std::cout << "DEBUG ici" << std::endl; // Touches/intersects the top, left and bottom borders of the RoI computeIntersectionsTopLeftBottom(u_c, v_c, umin_roi, vmin_roi, vmax_roi, radius, delta_theta); } diff --git a/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h b/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h index c711968932..1567554361 100644 --- a/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h +++ b/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h @@ -97,10 +97,14 @@ class VISP_EXPORT vpCircleHoughTransform // // Center candidates computation attributes std::pair m_centerXlimits; /*!< Minimum and maximum position on the horizontal axis of the center of the circle we want to detect.*/ std::pair m_centerYlimits; /*!< Minimum and maximum position on the vertical axis of the center of the circle we want to detect.*/ - unsigned int m_minRadius; /*!< Minimum radius of the circles we want to detect.*/ - unsigned int m_maxRadius; /*!< Maximum radius of the circles we want to detect.*/ - int m_dilatationNbIter; /*!< Number of times dilatation is performed to detect the maximum number of votes for the center candidates.*/ - float m_centerThresh; /*!< Minimum number of votes a point must exceed to be considered as center candidate.*/ + float m_minRadius; /*!< Minimum radius of the circles we want to detect.*/ + float m_maxRadius; /*!< Maximum radius of the circles we want to detect.*/ + int m_dilatationKernelSize; /*!< Kernel size of the dilatation that is performed to detect the maximum number of votes for the center candidates.*/ + int m_averagingWindowSize; /*!< Size of the averaging window around the maximum number of votes to compute the + center candidate such as it is the barycenter of the window. Must be odd.*/ + float m_centerMinThresh; /*!< Minimum number of votes a point must exceed to be considered as center candidate.*/ + int m_expectedNbCenters; /*!< Expected number of different centers in the image. If negative, all candidates centers + are kept, otherwise only up to this number are kept.*/ // // Circle candidates computation attributes float m_circleProbaThresh; /*!< Probability threshold in order to keep a circle candidate.*/ @@ -128,10 +132,12 @@ class VISP_EXPORT vpCircleHoughTransform , m_upperCannyThreshRatio(0.8f) , m_centerXlimits(std::pair(std::numeric_limits::min(), std::numeric_limits::max())) , m_centerYlimits(std::pair(std::numeric_limits::min(), std::numeric_limits::max())) - , m_minRadius(0) - , m_maxRadius(1000) - , m_dilatationNbIter(1) - , m_centerThresh(50.f) + , m_minRadius(0.f) + , m_maxRadius(1000.f) + , m_dilatationKernelSize(3) + , m_averagingWindowSize(5) + , m_centerMinThresh(50.f) + , m_expectedNbCenters(-1) , m_circleProbaThresh(0.9f) , m_circlePerfectness(0.9f) , m_centerMinDist(15.f) @@ -155,12 +161,14 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] centerYlimits Minimum and maximum position on the vertical axis of the center of the circle we want to detect. * \param[in] minRadius Minimum radius of the circles we want to detect. * \param[in] maxRadius Maximum radius of the circles we want to detect. - * \param[in] dilatationNbIter Number of times dilatation is performed to detect the maximum number of votes for the center candidates + * \param[in] dilatationKernelSize Kernel size of the dilatation that is performed to detect the maximum number of votes for the center candidates. * \param[in] centerThresh Minimum number of votes a point must exceed to be considered as center candidate. * \param[in] circleProbabilityThresh Probability threshold in order to keep a circle candidate. * \param[in] circlePerfectness The scalar product radius RC_ij . gradient(Ep_j) >= m_circlePerfectness * || RC_ij || * || gradient(Ep_j) || to add a vote for the radius RC_ij. * \param[in] centerMinDistThresh Two circle candidates whose centers are closer than this threshold are considered for merging. * \param[in] mergingRadiusDiffThresh Maximum radius difference between two circle candidates to consider merging them. + * \param[in] averagingWindowSize Size of the averaging window around the maximum number of votes to compute the + center candidate such as it is the barycenter of the window. Must be odd. * \param[in] filteringAndGradientMethod The choice of the filter and gradient operator to apply before the edge * detection step. * \param[in] backendType Permits to choose the backend used to compute the edge map. @@ -169,6 +177,8 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] upperCannyThreshRatio If the thresholds must be computed,the upper threshold will be equal to the value * such as the number of pixels of the image times \b upperThresholdRatio have an absolute gradient lower than the * upper threshold. + * \param[in] expectedNbCenters Expected number of centers in the image. If the number is negative, all the centers + * are kept. Otherwise, maximum up to this number of centers are kept. */ vpCircleHoughTransformParameters( const int &gaussianKernelSize @@ -179,18 +189,20 @@ class VISP_EXPORT vpCircleHoughTransform , const int &edgeMapFilterNbIter , const std::pair ¢erXlimits , const std::pair ¢erYlimits - , const unsigned int &minRadius - , const unsigned int &maxRadius - , const int &dilatationNbIter + , const float &minRadius + , const float &maxRadius + , const int &dilatationKernelSize , const float ¢erThresh , const float &circleProbabilityThresh , const float &circlePerfectness , const float ¢erMinDistThresh , const float &mergingRadiusDiffThresh + , const int &averagingWindowSize = 5 , const vpImageFilter::vpCannyFilteringAndGradientType &filteringAndGradientMethod = vpImageFilter::CANNY_GBLUR_SOBEL_FILTERING , const vpImageFilter::vpCannyBackendType &backendType = vpImageFilter::CANNY_OPENCV_BACKEND , const float &lowerCannyThreshRatio = 0.6f , const float &upperCannyThreshRatio = 0.8f + , const int &expectedNbCenters = -1 ) : m_filteringAndGradientType(filteringAndGradientMethod) , m_gaussianKernelSize(gaussianKernelSize) @@ -206,14 +218,201 @@ class VISP_EXPORT vpCircleHoughTransform , m_centerYlimits(centerYlimits) , m_minRadius(std::min(minRadius, maxRadius)) , m_maxRadius(std::max(minRadius, maxRadius)) - , m_dilatationNbIter(dilatationNbIter) - , m_centerThresh(centerThresh) + , m_dilatationKernelSize(dilatationKernelSize) + , m_averagingWindowSize(averagingWindowSize) + , m_centerMinThresh(centerThresh) + , m_expectedNbCenters(expectedNbCenters) , m_circleProbaThresh(circleProbabilityThresh) , m_circlePerfectness(circlePerfectness) , m_centerMinDist(centerMinDistThresh) , m_mergingRadiusDiffThresh(mergingRadiusDiffThresh) { } + /** + * \brief Get the size of the Gaussian filter kernel used to smooth the input image. + * + * \return int The size of the kernel. + */ + inline int getGaussianKernelSize() const + { + return m_gaussianKernelSize; + } + + /** + * \brief Get the standard deviation of the Gaussian filter. + * + * \return float The standard deviation. + */ + inline float getGaussianStdev() const + { + return m_gaussianStdev; + } + + /** + * \brief Get the size of the gradient kernel filters used to compute the gradients. + * + * \return int The size of the kernel. + */ + inline int getGradientKernelSize() const + { + return m_gradientFilterKernelSize; + } + + /** + * \brief Get the lower threshold for the Canny operator. Values lower than this value are rejected. + * A negative value means that the algorithm computes the lower threshold automatically. + * + * \return float The lower Canny threshold. + */ + inline float getLowerCannyThreshold() const + { + return m_lowerCannyThresh; + } + + /** + * \brief Get the upper threshold for the Canny operator. Values lower than this value are rejected. + * A negative value means that the algorithm computes the lower and upper thresholds automatically. + * + * \return float The upper Canny threshold. + */ + inline float getUpperCannyThreshold() const + { + return m_upperCannyThresh; + } + + /** + * \brief Get the number of iterations of 8-neighbor connectivity filtering to apply to the edge map. + * + * \return int The number of iterations. + */ + inline int getEdgeMapFilteringNbIter() const + { + return m_edgeMapFilteringNbIter; + } + + /** + * \brief Get the minimum and maximum position on the horizontal axis of the center of the circle we want to detect. + * + * \return std::pair The min and max x positions. + */ + inline std::pair getCenterXLimits() const + { + return m_centerXlimits; + } + + /** + * \brief Get the minimum and maximum position on the vertical axis of the center of the circle we want to detect. + * + * \return std::pair The min and max y positions. + */ + inline std::pair getCenterYLimits() const + { + return m_centerYlimits; + } + + /** + * \brief Get the minimum radius of the circles we want to detect. + * + * \return float The radius min. + */ + inline float getMinRadius() const + { + return m_minRadius; + } + + /** + * \brief Get the maximum radius of the circles we want to detect. + * + * \return float The radius max. + */ + inline float getMaxRadius() const + { + return m_maxRadius; + } + + /** + * \brief Get the kernel size of the dilatation that is performed to detect the maximum number of votes + * for the center candidates. + * + * \return int The kernel size. + */ + inline int getDilatationKernelSize() const + { + return m_dilatationKernelSize; + } + + /** + * \brief Get the size of the averaging window around the maximum number of votes to compute the + * center candidate such as it is the barycenter of the window. + * + * \return int The size of the averaging window. + */ + inline int getAveragingWindowSize() const + { + return m_averagingWindowSize; + } + + /** + * \brief Get the minimum number of votes a point must exceed to be considered as center candidate. + * + * \return float The threshold. + */ + inline float getCenterMinThreshold() const + { + return m_centerMinThresh; + } + + /** + * \brief Get the expected number of centers in the image. If the number is negative, all the centers + * are kept. Otherwise, maximum up to this number of centers are kept. + * + * \return int The expected number of centers. + */ + inline int getExpectedNbCenters() const + { + return m_expectedNbCenters; + } + + /** + * \brief Get the probability threshold in order to keep a circle candidate. + * + * \return float The threshold. + */ + inline float getProbabilityThreshold() const + { + return m_circleProbaThresh; + } + + /** + * \brief Get the threshold for the scalar product between the radius and the gradient to count a vote. + * + * \return float The threshold. + */ + inline float getCirclePerfectness() const + { + return m_circlePerfectness; + } + + /** + * \brief Get the Maximum distance between two circle candidates centers to consider merging them. + * + * \return float The maximum distance between two centers. + */ + inline float getCenterMinDist() const + { + return m_centerMinDist; + } + + /** + * \brief Get the Maximum radius difference between two circle candidates to consider merging them. + * + * @return float The merging radius difference. + */ + inline float getMergingRadiusDiff() const + { + return m_mergingRadiusDiffThresh; + } + /** * Create a string with all the Hough transform parameters. */ @@ -231,8 +430,10 @@ class VISP_EXPORT vpCircleHoughTransform txt += "\tCenter horizontal position limits: min = " + std::to_string(m_centerXlimits.first) + "\tmax = " + std::to_string(m_centerXlimits.second) +"\n"; txt += "\tCenter vertical position limits: min = " + std::to_string(m_centerYlimits.first) + "\tmax = " + std::to_string(m_centerYlimits.second) +"\n"; txt += "\tRadius limits: min = " + std::to_string(m_minRadius) + "\tmax = " + std::to_string(m_maxRadius) +"\n"; - txt += "\tNumber of repetitions of the dilatation filter = " + std::to_string(m_dilatationNbIter) + "\n"; - txt += "\tCenters votes threshold = " + std::to_string(m_centerThresh) + "\n"; + txt += "\tKernel size of the dilatation filter = " + std::to_string(m_dilatationKernelSize) + "\n"; + txt += "\tAveraging window size for center detection = " + std::to_string(m_averagingWindowSize) + "\n"; + txt += "\tCenters votes threshold = " + std::to_string(m_centerMinThresh) + "\n"; + txt += "\tExpected number of centers = " + (m_expectedNbCenters > 0 ? std::to_string(m_expectedNbCenters) : "no limits") + "\n"; txt += "\tCircle probability threshold = " + std::to_string(m_circleProbaThresh) + "\n"; txt += "\tCircle perfectness threshold = " + std::to_string(m_circlePerfectness) + "\n"; txt += "\tCenters minimum distance = " + std::to_string(m_centerMinDist) + "\n"; @@ -327,14 +528,21 @@ class VISP_EXPORT vpCircleHoughTransform params.m_centerXlimits = j.value("centerXlimits", params.m_centerXlimits); params.m_centerYlimits = j.value("centerYlimits", params.m_centerYlimits); - std::pair radiusLimits = j.value("radiusLimits", std::pair(params.m_minRadius, params.m_maxRadius)); + std::pair radiusLimits = j.value("radiusLimits", std::pair(params.m_minRadius, params.m_maxRadius)); params.m_minRadius = std::min(radiusLimits.first, radiusLimits.second); params.m_maxRadius = std::max(radiusLimits.first, radiusLimits.second); - params.m_dilatationNbIter = j.value("dilatationNbIter", params.m_dilatationNbIter); + params.m_dilatationKernelSize = j.value("dilatationKernelSize", params.m_dilatationKernelSize); - params.m_centerThresh = j.value("centerThresh", params.m_centerThresh); - if (params.m_centerThresh <= 0) { + params.m_averagingWindowSize = j.value("averagingWindowSize", params.m_averagingWindowSize); + if (params.m_averagingWindowSize <= 0 || params.m_averagingWindowSize % 2 == 0) { + throw vpException(vpException::badValue, "Averaging window size must be positive and odd."); + } + + params.m_expectedNbCenters = j.value("expectedNbCenters", params.m_expectedNbCenters); + + params.m_centerMinThresh = j.value("centerThresh", params.m_centerMinThresh); + if (params.m_centerMinThresh <= 0.f) { throw vpException(vpException::badValue, "Votes thresholds for center detection must be positive."); } @@ -365,7 +573,7 @@ class VISP_EXPORT vpCircleHoughTransform */ inline friend void to_json(json &j, const vpCircleHoughTransformParameters ¶ms) { - std::pair radiusLimits = { params.m_minRadius, params.m_maxRadius }; + std::pair radiusLimits = { params.m_minRadius, params.m_maxRadius }; j = json { {"filteringAndGradientType", vpImageFilter::vpCannyFilteringAndGradientTypeToString(params.m_filteringAndGradientType)}, @@ -381,8 +589,10 @@ class VISP_EXPORT vpCircleHoughTransform {"centerXlimits", params.m_centerXlimits}, {"centerYlimits", params.m_centerYlimits}, {"radiusLimits", radiusLimits}, - {"dilatationNbIter", params.m_dilatationNbIter}, - {"centerThresh", params.m_centerThresh}, + {"dilatationKernelSize", params.m_dilatationKernelSize}, + {"averagingWindowSize", params.m_averagingWindowSize}, + {"centerThresh", params.m_centerMinThresh}, + {"expectedNbCenters", params.m_expectedNbCenters}, {"circleProbabilityThreshold", params.m_circleProbaThresh}, {"circlePerfectnessThreshold", params.m_circlePerfectness}, {"centerMinDistance", params.m_centerMinDist}, @@ -417,7 +627,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] I The input gray scale image. * \return std::vector The list of 2D circles detected in the image. */ - std::vector detect(const cv::Mat &cv_I); + virtual std::vector detect(const cv::Mat &cv_I); #endif /** @@ -427,7 +637,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] I The input color image. * \return std::vector The list of 2D circles detected in the image. */ - std::vector detect(const vpImage &I); + virtual std::vector detect(const vpImage &I); /** * \brief Perform Circle Hough Transform to detect the circles in a gray-scale image @@ -435,7 +645,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] I The input gray scale image. * \return std::vector The list of 2D circles detected in the image. */ - std::vector detect(const vpImage &I); + virtual std::vector detect(const vpImage &I); /** * \brief Perform Circle Hough Transform to detect the circles in in a gray-scale image. @@ -447,7 +657,7 @@ class VISP_EXPORT vpCircleHoughTransform * \return std::vector The list of 2D circles with the most number * of votes detected in the image. */ - std::vector detect(const vpImage &I, const int &nbCircles); + virtual std::vector detect(const vpImage &I, const int &nbCircles); //@} /** @name Configuration from files */ @@ -468,7 +678,7 @@ class VISP_EXPORT vpCircleHoughTransform * * \param[in] jsonPath The path towards the JSON configuration file. */ - void initFromJSON(const std::string &jsonPath); + virtual void initFromJSON(const std::string &jsonPath); /** * \brief Save the configuration of the detector in a JSON file @@ -477,7 +687,7 @@ class VISP_EXPORT vpCircleHoughTransform * * \param[in] jsonPath The path towards the JSON output file. */ - void saveConfigurationInJSON(const std::string &jsonPath) const; + virtual void saveConfigurationInJSON(const std::string &jsonPath) const; /** * \brief Read the detector configuration from JSON. All values are optional and if an argument is not present, @@ -647,7 +857,7 @@ class VISP_EXPORT vpCircleHoughTransform */ inline void setCircleMinRadius(const float &circle_min_radius) { - m_algoParams.m_minRadius = static_cast(circle_min_radius); + m_algoParams.m_minRadius = circle_min_radius; } /*! @@ -656,7 +866,7 @@ class VISP_EXPORT vpCircleHoughTransform */ inline void setCircleMaxRadius(const float &circle_max_radius) { - m_algoParams.m_maxRadius = static_cast(circle_max_radius); + m_algoParams.m_maxRadius = circle_max_radius; } /*! @@ -674,17 +884,35 @@ class VISP_EXPORT vpCircleHoughTransform /** * \brief Set the parameters of the computation of the circle center candidates. * - * \param[in] dilatationRepet Number of repetition of the dilatation operation to detect the maxima in the center accumulator. + * \param[in] dilatationSize Kernel size of the dilatation operation used to detect the maxima in the center accumulator. * \param[in] centerThresh Minimum number of votes a point must exceed to be considered as center candidate. + * \param[in] averagingWindowSize Size of the averaging window around the maximum number of votes to compute the + center candidate such as it is the barycenter of the window. Must be odd. + * \param[in] expectedNbCenters Expected number of centers in the image. If the number is negative, all the centers + * are kept. Otherwise, maximum up to this number of centers are kept. */ - inline void setCenterComputationParameters(const int &dilatationRepet, const float ¢erThresh) + inline void setCenterComputationParameters(const int &dilatationSize, const float ¢erThresh, + const int &averagingWindowSize = 5, const int expectedNbCenters = -1) { - m_algoParams.m_dilatationNbIter = dilatationRepet; - m_algoParams.m_centerThresh = centerThresh; + m_algoParams.m_dilatationKernelSize = dilatationSize; + m_algoParams.m_centerMinThresh = centerThresh; + m_algoParams.m_averagingWindowSize = averagingWindowSize; + m_algoParams.m_expectedNbCenters = expectedNbCenters; + + if (m_algoParams.m_dilatationKernelSize < 3) { + throw vpException(vpException::badValue, "Dilatation kernel size for center detection must be greater or equal to 3."); + } + else if ((m_algoParams.m_dilatationKernelSize % 2) == 0) { + throw vpException(vpException::badValue, "Dilatation kernel size for center detection must be odd."); + } - if (m_algoParams.m_centerThresh <= 0) { + if (m_algoParams.m_centerMinThresh <= 0.f) { throw vpException(vpException::badValue, "Votes thresholds for center detection must be positive."); } + + if (m_algoParams.m_averagingWindowSize <= 0 || m_algoParams.m_averagingWindowSize % 2 == 0) { + throw vpException(vpException::badValue, "Averaging window size must be positive and odd."); + } } /** @@ -722,9 +950,9 @@ class VISP_EXPORT vpCircleHoughTransform /** * \brief Get the list of Center Candidates, stored as pair * - * \return std::vector > The list of Center Candidates, stored as pair + * \return std::vector > The list of Center Candidates, stored as pair */ - inline std::vector > getCenterCandidatesList() + inline std::vector > getCenterCandidatesList() { return m_centerCandidatesList; } @@ -810,7 +1038,7 @@ class VISP_EXPORT vpCircleHoughTransform /*! * Get circles min radius in pixels. */ - inline unsigned int getCircleMinRadius() const + inline float getCircleMinRadius() const { return m_algoParams.m_minRadius; } @@ -818,7 +1046,7 @@ class VISP_EXPORT vpCircleHoughTransform /*! * Get circles max radius in pixels. */ - inline unsigned int getCircleMaxRadius() const + inline float getCircleMaxRadius() const { return m_algoParams.m_maxRadius; } @@ -850,11 +1078,11 @@ class VISP_EXPORT vpCircleHoughTransform */ friend VISP_EXPORT std::ostream &operator<<(std::ostream &os, const vpCircleHoughTransform &detector); -private: +protected: /** * \brief Initialize the Gaussian filters used to blur the image. */ - void initGaussianFilters(); + virtual void initGaussianFilters(); /** * \brief Initialize the gradient filters used to compute the gradient images. @@ -868,7 +1096,7 @@ class VISP_EXPORT vpCircleHoughTransform * * \param[in] I The input gray scale image. */ - void computeGradientsAfterGaussianSmoothing(const vpImage &I); + virtual void computeGradientsAfterGaussianSmoothing(const vpImage &I); /** * \brief Perform edge detection based on the computed gradients. @@ -876,19 +1104,19 @@ class VISP_EXPORT vpCircleHoughTransform * * \param[in] I The input gray scale image. */ - void edgeDetection(const vpImage &I); + virtual void edgeDetection(const vpImage &I); /** * \brief Filter the edge map in order to remove isolated edge points. */ - void filterEdgeMap(); + virtual void filterEdgeMap(); /** * \brief Determine the image points that are circle center candidates. * Increment the center accumulator based on the edge points and gradient information. * Perform thresholding to keep only the center candidates that exceed the threshold. */ - void computeCenterCandidates(); + virtual void computeCenterCandidates(); /** * \brief Compute the probability of \b circle given the number of pixels voting for @@ -900,7 +1128,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] nbVotes The number of visible pixels of the given circle. * \return float The probability of the circle. */ - float computeCircleProbability(const vpImageCircle &circle, const unsigned int &nbVotes); + virtual float computeCircleProbability(const vpImageCircle &circle, const unsigned int &nbVotes); /** * \brief For each center candidate CeC_i, do: @@ -910,7 +1138,12 @@ class VISP_EXPORT vpCircleHoughTransform * - If accum_rc[CeC_i][RCB_k] > radius_count_thresh, add the circle candidate (CeC_i, RCB_k) * to the list of circle candidates */ - void computeCircleCandidates(); + virtual void computeCircleCandidates(); + + /** + * \brief For each circle candidate CiC_i, check if similar circles have also been detected and if so merges them. + */ + virtual void mergeCircleCandidates(); /** * \brief For each circle candidate CiC_i do: @@ -918,8 +1151,12 @@ class VISP_EXPORT vpCircleHoughTransform * +- Compute the similarity between CiC_i and CiC_j * +- If the similarity exceeds a threshold, merge the circle candidates CiC_i and CiC_j and remove CiC_j of the list * - Add the circle candidate CiC_i to the final list of detected circles + * \param[out] circleCandidates List of circle candidates in which we want to merge the similar circles. + * \param[out] circleCandidatesVotes List of votes of the circle candidates. + * \param[out] circleCandidatesProba List of probabilities of the circle candidates. */ - void mergeCircleCandidates(); + virtual void mergeCandidates(std::vector &circleCandidates, std::vector &circleCandidatesVotes, + std::vector &circleCandidatesProba); vpCircleHoughTransformParameters m_algoParams; /*!< Attributes containing all the algorithm parameters.*/ @@ -938,7 +1175,7 @@ class VISP_EXPORT vpCircleHoughTransform // // Center candidates computation attributes std::vector > m_edgePointsList; /*!< Vector that contains the list of edge points, to make faster some parts of the algo. They are stored as pair<#row, #col>.*/ - std::vector > m_centerCandidatesList; /*!< Vector that contains the list of center candidates. They are stored as pair<#row, #col>.*/ + std::vector > m_centerCandidatesList; /*!< Vector that contains the list of center candidates. They are stored as pair<#row, #col>.*/ std::vector m_centerVotes; /*!< Number of votes for the center candidates that are kept.*/ // // Circle candidates computation attributes diff --git a/modules/imgproc/src/vpCircleHoughTransform.cpp b/modules/imgproc/src/vpCircleHoughTransform.cpp index ea4839c104..3ca133ab16 100644 --- a/modules/imgproc/src/vpCircleHoughTransform.cpp +++ b/modules/imgproc/src/vpCircleHoughTransform.cpp @@ -175,18 +175,18 @@ vpCircleHoughTransform::detect(const vpImage &I, const int &nbCir size_t nbDetections = detections.size(); // Prepare vector of tuple to sort by decreasing probabilities - std::vector > detectionsWithVotes; - for (size_t i = 0; i < nbDetections; i++) { - std::tuple detectionWithVote(detections[i], m_finalCircleVotes[i], m_finalCirclesProbabilities[i]); - detectionsWithVotes.push_back(detectionWithVote); + std::vector> v_id_proba; + for (size_t i = 0; i < nbDetections; ++i) { + std::pair id_proba(i, m_finalCirclesProbabilities[i]); + v_id_proba.push_back(id_proba); } // Sorting by decreasing probabilities - bool (*hasBetterProba)(std::tuple, std::tuple) - = [](std::tuple a, std::tuple b) { - return (std::get<2>(a) > std::get<2>(b)); + auto hasBetterProba + = [](std::pair a, std::pair b) { + return (a.second > b.second); }; - std::sort(detectionsWithVotes.begin(), detectionsWithVotes.end(), hasBetterProba); + std::sort(v_id_proba.begin(), v_id_proba.end(), hasBetterProba); // Clearing the storages containing the detection results // to have it sorted by decreasing probabilities @@ -199,12 +199,16 @@ vpCircleHoughTransform::detect(const vpImage &I, const int &nbCir } std::vector bestCircles; - for (size_t i = 0; i < nbDetections; i++) { - m_finalCircles[i] = std::get<0>(detectionsWithVotes[i]); - m_finalCircleVotes[i] = std::get<1>(detectionsWithVotes[i]); - m_finalCirclesProbabilities[i] = std::get<2>(detectionsWithVotes[i]); + auto copyFinalCircles = m_finalCircles; + auto copyFinalCirclesVotes = m_finalCircleVotes; + auto copyFinalCirclesProbas = m_finalCirclesProbabilities; + for (size_t i = 0; i < nbDetections; ++i) { + size_t id = v_id_proba[i].first; + m_finalCircles[i] = copyFinalCircles[id]; + m_finalCircleVotes[i] = copyFinalCirclesVotes[id]; + m_finalCirclesProbabilities[i] = copyFinalCirclesProbas[id]; if (i < limitMin) { - bestCircles.push_back(std::get<0>(detectionsWithVotes[i])); + bestCircles.push_back(m_finalCircles[i]); } } @@ -224,6 +228,32 @@ vpCircleHoughTransform::detect(const vpImage &I) m_finalCircles.clear(); m_finalCircleVotes.clear(); + // Ensuring that the difference between the max and min radii is big enough to take into account + // the pixelization of the image + const float minRadiusDiff = 3.f; + if (m_algoParams.m_maxRadius - m_algoParams.m_minRadius < minRadiusDiff) { + if (m_algoParams.m_minRadius > minRadiusDiff / 2.f) { + m_algoParams.m_maxRadius += minRadiusDiff / 2.f; + m_algoParams.m_minRadius -= minRadiusDiff / 2.f; + } + else { + m_algoParams.m_maxRadius += minRadiusDiff - m_algoParams.m_minRadius; + m_algoParams.m_minRadius = 0.f; + } + } + + // Ensuring that the difference between the max and min center position is big enough to take into account + // the pixelization of the image + const float minCenterPositionDiff = 3.f; + if (m_algoParams.m_centerXlimits.second - m_algoParams.m_centerXlimits.first < minCenterPositionDiff) { + m_algoParams.m_centerXlimits.second += minCenterPositionDiff / 2.f; + m_algoParams.m_centerXlimits.first -= minCenterPositionDiff / 2.f; + } + if (m_algoParams.m_centerYlimits.second - m_algoParams.m_centerYlimits.first < minCenterPositionDiff) { + m_algoParams.m_centerYlimits.second += minCenterPositionDiff / 2.f; + m_algoParams.m_centerYlimits.first -= minCenterPositionDiff / 2.f; + } + // First thing, we need to apply a Gaussian filter on the image to remove some spurious noise // Then, we need to compute the image gradients in order to be able to perform edge detection computeGradientsAfterGaussianSmoothing(I); @@ -262,8 +292,8 @@ vpCircleHoughTransform::computeGradientsAfterGaussianSmoothing(const vpImage(m_algoParams.m_maxRadius)); + int maximumXposition = std::min(m_algoParams.m_centerXlimits.second, static_cast(m_algoParams.m_maxRadius + nbCols)); minimumXposition = std::min(minimumXposition, maximumXposition - 1); float minimumXpositionFloat = static_cast(minimumXposition); + float maximumXpositionFloat = static_cast(maximumXposition); int offsetX = minimumXposition; int accumulatorWidth = maximumXposition - minimumXposition + 1; if (accumulatorWidth <= 0) { @@ -364,42 +393,87 @@ vpCircleHoughTransform::computeCenterCandidates() // The miminum vertical position of the center is at worst -maxRadius outside the image // The maxinum vertical position of the center is at worst +maxRadiusoutside the image // The height of the accumulator is the difference between the max and the min - int minimumYposition = std::max(m_algoParams.m_centerYlimits.first, -1 * (int)m_algoParams.m_maxRadius); - int maximumYposition = std::min(m_algoParams.m_centerYlimits.second, (int)(m_algoParams.m_maxRadius + nbRows)); + int minimumYposition = std::max(m_algoParams.m_centerYlimits.first, -1 * static_cast(m_algoParams.m_maxRadius)); + int maximumYposition = std::min(m_algoParams.m_centerYlimits.second, static_cast(m_algoParams.m_maxRadius + nbCols)); minimumYposition = std::min(minimumYposition, maximumYposition - 1); float minimumYpositionFloat = static_cast(minimumYposition); + float maximumYpositionFloat = static_cast(maximumYposition); int offsetY = minimumYposition; int accumulatorHeight = maximumYposition - minimumYposition + 1; if (accumulatorHeight <= 0) { - throw(vpException(vpException::dimensionError, "[vpCircleHoughTransform::computeCenterCandidates] Accumulator height <= 0!")); + std::string errMsg("[vpCircleHoughTransform::computeCenterCandidates] Accumulator height <= 0!"); + throw(vpException(vpException::dimensionError, errMsg)); } - vpImage centersAccum(accumulatorHeight, accumulatorWidth + 1, 0.); /*!< Matrix that contains the votes for the center candidates.*/ + vpImage centersAccum(accumulatorHeight, accumulatorWidth + 1, 0.); /*!< Votes for the center candidates.*/ - for (unsigned int r = 0; r < nbRows; r++) { - for (unsigned int c = 0; c < nbCols; c++) { + const int nbDirections = 2; + for (unsigned int r = 0; r < nbRows; ++r) { + for (unsigned int c = 0; c < nbCols; ++c) { if (m_edgeMap[r][c] == 255) { - // Saving the edge point for further use - m_edgePointsList.push_back(std::pair(r, c)); - // Voting for points in both direction of the gradient // Step from min_radius to max_radius in both directions of the gradient float mag = std::sqrt(m_dIx[r][c] * m_dIx[r][c] + m_dIy[r][c] * m_dIy[r][c]); - float sx = m_dIx[r][c] / mag; - float sy = m_dIy[r][c] / mag; + float sx = 0.f, sy = 0.f; + if (std::abs(mag) >= std::numeric_limits::epsilon()) { + sx = m_dIx[r][c] / mag; + sy = m_dIy[r][c] / mag; + } + else { + continue; + } - int int_minRad = (int)m_algoParams.m_minRadius; - int int_maxRad = (int)m_algoParams.m_maxRadius; + // Saving the edge point for further use + m_edgePointsList.push_back(std::pair(r, c)); - for (int k1 = 0; k1 < 2; k1++) { + for (int k1 = 0; k1 < nbDirections; ++k1) { bool hasToStopLoop = false; - for (int rad = int_minRad; rad <= int_maxRad && !hasToStopLoop; rad++) { - float x1 = (float)c + (float)rad * sx; - float y1 = (float)r + (float)rad * sy; + int x_low_prev = std::numeric_limits::max(), y_low_prev, y_high_prev; + int x_high_prev = y_low_prev = y_high_prev = x_low_prev; + + float rstart = m_algoParams.m_minRadius, rstop = m_algoParams.m_maxRadius; + float min_minus_c = minimumXpositionFloat - static_cast(c); + float min_minus_r = minimumYpositionFloat - static_cast(r); + float max_minus_c = maximumXpositionFloat - static_cast(c); + float max_minus_r = maximumYpositionFloat - static_cast(r); + if (sx > 0) { + float rmin = min_minus_c / sx; + rstart = std::max(rmin, m_algoParams.m_minRadius); + float rmax = max_minus_c / sx; + rstop = std::min(rmax, m_algoParams.m_maxRadius); + } + else if (sx < 0) { + float rmin = max_minus_c / sx; + rstart = std::max(rmin, m_algoParams.m_minRadius); + float rmax = min_minus_c / sx; + rstop = std::min(rmax, m_algoParams.m_maxRadius); + } + + if (sy > 0) { + float rmin = min_minus_r / sy; + rstart = std::max(rmin, rstart); + float rmax = max_minus_r / sy; + rstop = std::min(rmax, rstop); + } + else if (sy < 0) { + float rmin = max_minus_r / sy; + rstart = std::max(rmin, rstart); + float rmax = min_minus_r / sy; + rstop = std::min(rmax, rstop); + } + + float deltar_x = 1.f / std::abs(sx); + float deltar_y = 1.f / std::abs(sy); + float deltar = std::min(deltar_x, deltar_y); - if (x1 < minimumXpositionFloat || y1 < minimumYpositionFloat) { - continue; // If either value is lower than maxRadius, it means that the center is outside the search region. + for (float rad = rstart; rad <= rstop && !hasToStopLoop; rad += deltar) { + float x1 = static_cast(c) + rad * sx; + float y1 = static_cast(r) + rad * sy; + + if ((x1 < minimumXpositionFloat) || (y1 < minimumYpositionFloat) + ||(x1 > maximumXpositionFloat) || (y1 > maximumYpositionFloat)) { + continue; // It means that the center is outside the search region. } int x_low, x_high; @@ -423,20 +497,34 @@ vpCircleHoughTransform::computeCenterCandidates() y_high = -(static_cast(std::floor(-1. * y1))); } + if ((x_low_prev == x_low) && (x_high_prev == x_high) + && (y_low_prev == y_low) && (y_high_prev == y_high)) { + // Avoid duplicated votes to the same center candidate + continue; + } + else { + x_low_prev = x_low; + x_high_prev = x_high; + y_low_prev = y_low; + y_high_prev = y_high; + } + auto updateAccumulator = [](const float &x_orig, const float &y_orig, - const unsigned int &x, const unsigned int &y, + const int &x, const int &y, const int &offsetX, const int &offsetY, - const unsigned int &nbCols, const unsigned int &nbRows, + const int &nbCols, const int &nbRows, vpImage &accum, bool &hasToStop) { - if (x - offsetX >= nbCols || - y - offsetY >= nbRows + if ((x - offsetX < 0) || + (x - offsetX >= nbCols) || + (y - offsetY < 0) || + (y - offsetY >= nbRows) ) { hasToStop = true; } else { - float dx = (x_orig - (float)x); - float dy = (y_orig - (float)y); + float dx = (x_orig - static_cast(x)); + float dy = (y_orig - static_cast(y)); accum[y - offsetY][x - offsetX] += std::abs(dx) + std::abs(dy); } }; @@ -464,10 +552,8 @@ vpCircleHoughTransform::computeCenterCandidates() // Use dilatation with large kernel in order to determine the // accumulator maxima vpImage centerCandidatesMaxima = centersAccum; - int niters = std::max(m_algoParams.m_dilatationNbIter, 1); // Ensure at least one dilatation operation - for (int i = 0; i < niters; i++) { - vpImageMorphology::dilatation(centerCandidatesMaxima, vpImageMorphology::CONNEXITY_4); - } + int dilatationKernelSize = std::max(m_algoParams.m_dilatationKernelSize, 3); // Ensure at least a 3x3 dilatation operation is performed + vpImageMorphology::dilatation(centerCandidatesMaxima, dilatationKernelSize); // Look for the image points that correspond to the accumulator maxima // These points will become the center candidates @@ -475,43 +561,143 @@ vpCircleHoughTransform::computeCenterCandidates() int nbColsAccum = centersAccum.getCols(); int nbRowsAccum = centersAccum.getRows(); int nbVotes = -1; - for (int y = 0; y < nbRowsAccum; y++) { + std::vector, float>> peak_positions_votes; + for (int y = 0; y < nbRowsAccum; ++y) { int left = -1; - for (int x = 0; x < nbColsAccum; x++) { - if ((centersAccum[y][x] >= m_algoParams.m_centerThresh) + for (int x = 0; x < nbColsAccum; ++x) { + if ((centersAccum[y][x] >= m_algoParams.m_centerMinThresh) && (std::fabs(centersAccum[y][x] - centerCandidatesMaxima[y][x]) < std::numeric_limits::epsilon()) && (centersAccum[y][x] > centersAccum[y][x + 1]) ) { - if (left < 0) + if (left < 0) { left = x; - nbVotes = std::max(nbVotes, (int)centersAccum[y][x]); + } + nbVotes = std::max(nbVotes, static_cast(centersAccum[y][x])); } else if (left >= 0) { - int cx = (int)((left + x - 1) * 0.5f); - m_centerCandidatesList.push_back(std::pair(y + offsetY, cx + offsetX)); - m_centerVotes.push_back(nbVotes); + int cx = static_cast((left + x - 1) * 0.5f); + float sumVotes = 0.; + float x_avg = 0., y_avg = 0.; + int averagingWindowHalfSize = m_algoParams.m_averagingWindowSize / 2; + int startingRow = std::max(0, y - averagingWindowHalfSize); + int startingCol = std::max(0, cx - averagingWindowHalfSize); + int endRow = std::min(accumulatorHeight, y + averagingWindowHalfSize + 1); + int endCol = std::min(accumulatorWidth, cx + averagingWindowHalfSize + 1); + for (int r = startingRow; r < endRow; r++) { + for (int c = startingCol; c < endCol; c++) { + sumVotes += centersAccum[r][c]; + x_avg += centersAccum[r][c] * c; + y_avg += centersAccum[r][c] * r; + } + } + float avgVotes = sumVotes / static_cast(m_algoParams.m_averagingWindowSize * m_algoParams.m_averagingWindowSize); + if (avgVotes > m_algoParams.m_centerMinThresh) { + x_avg /= static_cast(sumVotes); + y_avg /= static_cast(sumVotes); + std::pair position(y_avg + static_cast(offsetY), x_avg + static_cast(offsetX)); + std::pair, float> position_vote(position, avgVotes); + peak_positions_votes.push_back(position_vote); + } + if (nbVotes < 0) { + throw(vpException(vpException::badValue, "nbVotes (" + std::to_string(nbVotes) + ") < 0, thresh = " + std::to_string(m_algoParams.m_centerMinThresh))); + } left = -1; nbVotes = -1; } } } + + unsigned int nbPeaks = peak_positions_votes.size(); + if (nbPeaks > 0) { + std::vector has_been_merged(nbPeaks, false); + std::vector, float>> merged_peaks_position_votes; + float squared_distance_max = m_algoParams.m_centerMinDist * m_algoParams.m_centerMinDist; + for (unsigned int idPeak = 0; idPeak < nbPeaks; ++idPeak) { + float votes = peak_positions_votes[idPeak].second; + if (has_been_merged[idPeak]) { + // Ignoring peak that has already been merged + continue; + } + else if (votes < m_algoParams.m_centerMinThresh) { + // Ignoring peak whose number of votes is lower than the threshold + has_been_merged[idPeak] = true; + continue; + } + std::pair position = peak_positions_votes[idPeak].first; + std::pair barycenter; + barycenter.first = position.first * peak_positions_votes[idPeak].second; + barycenter.second = position.second * peak_positions_votes[idPeak].second; + float total_votes = peak_positions_votes[idPeak].second; + float nb_electors = 1.f; + // Looking for potential similar peak in the following peaks + for (unsigned int idCandidate = idPeak + 1; idCandidate < nbPeaks; ++idCandidate) { + float votes_candidate = peak_positions_votes[idCandidate].second; + if (has_been_merged[idCandidate]) { + continue; + } + else if (votes_candidate < m_algoParams.m_centerMinThresh) { + // Ignoring peak whose number of votes is lower than the threshold + has_been_merged[idCandidate] = true; + continue; + } + // Computing the distance with the peak of insterest + std::pair position_candidate = peak_positions_votes[idCandidate].first; + float squared_distance = (position.first - position_candidate.first) * (position.first - position_candidate.first) + + (position.second - position_candidate.second) * (position.second - position_candidate.second); + + // If the peaks are similar, update the barycenter peak between them and corresponding votes + if (squared_distance < squared_distance_max) { + barycenter.first += position_candidate.first * votes_candidate; + barycenter.second += position_candidate.second * votes_candidate; + total_votes += votes_candidate; + nb_electors += 1.f; + has_been_merged[idCandidate] = true; + } + } + + float avg_votes = total_votes / nb_electors; + // Only the centers having enough votes are considered + if (avg_votes > m_algoParams.m_centerMinThresh) { + barycenter.first /= total_votes; + barycenter.second /= total_votes; + std::pair, float> barycenter_votes(barycenter, avg_votes); + merged_peaks_position_votes.push_back(barycenter_votes); + } + } + + auto sortingCenters = [](const std::pair, float> &position_vote_a, + const std::pair, float> &position_vote_b) + { + return position_vote_a.second > position_vote_b.second; + }; + + std::sort(merged_peaks_position_votes.begin(), merged_peaks_position_votes.end(), sortingCenters); + + nbPeaks = merged_peaks_position_votes.size(); + int nbPeaksToKeep = (m_algoParams.m_expectedNbCenters > 0 ? m_algoParams.m_expectedNbCenters : nbPeaks); + nbPeaksToKeep = std::min(nbPeaksToKeep, (int)nbPeaks); + for (int i = 0; i < nbPeaksToKeep; i++) { + m_centerCandidatesList.push_back(merged_peaks_position_votes[i].first); + m_centerVotes.push_back(merged_peaks_position_votes[i].second); + } + } } void vpCircleHoughTransform::computeCircleCandidates() { size_t nbCenterCandidates = m_centerCandidatesList.size(); - unsigned int nbBins = static_cast((m_algoParams.m_maxRadius - m_algoParams.m_minRadius + 1)/ m_algoParams.m_centerMinDist); - nbBins = std::max((unsigned int)1, nbBins); // Avoid having 0 bins, which causes segfault - std::vector radiusAccumList; /*!< Radius accumulator for each center candidates.*/ + int nbBins = static_cast((m_algoParams.m_maxRadius - m_algoParams.m_minRadius + 1)/ m_algoParams.m_mergingRadiusDiffThresh); + nbBins = std::max((int)1, nbBins); // Avoid having 0 bins, which causes segfault + std::vector radiusAccumList; /*!< Radius accumulator for each center candidates.*/ std::vector radiusActualValueList; /*!< Vector that contains the actual distance between the edge points and the center candidates.*/ - unsigned int rmin2 = m_algoParams.m_minRadius * m_algoParams.m_minRadius; - unsigned int rmax2 = static_cast(m_algoParams.m_maxRadius * m_algoParams.m_maxRadius); - int circlePerfectness2 = static_cast(m_algoParams.m_circlePerfectness * m_algoParams.m_circlePerfectness); + float rmin2 = m_algoParams.m_minRadius * m_algoParams.m_minRadius; + float rmax2 = m_algoParams.m_maxRadius * m_algoParams.m_maxRadius; + float circlePerfectness2 = m_algoParams.m_circlePerfectness * m_algoParams.m_circlePerfectness; - for (size_t i = 0; i < nbCenterCandidates; i++) { - std::pair centerCandidate = m_centerCandidatesList[i]; + for (size_t i = 0; i < nbCenterCandidates; ++i) { + std::pair centerCandidate = m_centerCandidatesList[i]; // Initialize the radius accumulator of the candidate with 0s radiusAccumList.clear(); radiusAccumList.resize(nbBins, 0); @@ -520,10 +706,9 @@ vpCircleHoughTransform::computeCircleCandidates() for (auto edgePoint : m_edgePointsList) { // For each center candidate CeC_i, compute the distance with each edge point EP_j d_ij = dist(CeC_i; EP_j) - unsigned int rx = edgePoint.first - centerCandidate.first; - unsigned int ry = edgePoint.second - centerCandidate.second; - unsigned int r2 = rx * rx + ry * ry; - + float rx = edgePoint.first - centerCandidate.first; + float ry = edgePoint.second - centerCandidate.second; + float r2 = rx * rx + ry * ry; if ((r2 > rmin2) && (r2 < rmax2)) { float gx = m_dIx[edgePoint.first][edgePoint.second]; float gy = m_dIy[edgePoint.first][edgePoint.second]; @@ -531,29 +716,95 @@ vpCircleHoughTransform::computeCircleCandidates() float scalProd = rx * gx + ry * gy; float scalProd2 = scalProd * scalProd; - if (scalProd2 >= circlePerfectness2 * r2 * grad2) { + if (scalProd2 >= (circlePerfectness2 * r2 * grad2)) { // Look for the Radius Candidate Bin RCB_k to which d_ij is "the closest" will have an additional vote float r = static_cast(std::sqrt(r2)); - unsigned int r_bin = static_cast(std::ceil((r - m_algoParams.m_minRadius)/ m_algoParams.m_centerMinDist)); + int r_bin = static_cast(std::floor((r - m_algoParams.m_minRadius) / m_algoParams.m_mergingRadiusDiffThresh)); r_bin = std::min(r_bin, nbBins - 1); - radiusAccumList[r_bin]++; - radiusActualValueList[r_bin] += r; + if ((r < (m_algoParams.m_minRadius + m_algoParams.m_mergingRadiusDiffThresh * 0.5f)) + || (r >(m_algoParams.m_minRadius + m_algoParams.m_mergingRadiusDiffThresh * (nbBins - 1 + 0.5f)))) { + // If the radius is at the very beginning of the allowed radii or at the very end, we do not span the vote + radiusAccumList[r_bin] += 1.f; + radiusActualValueList[r_bin] += r; + } + else { + float midRadiusPrevBin = m_algoParams.m_minRadius + m_algoParams.m_mergingRadiusDiffThresh * (r_bin - 1.f + 0.5f); + float midRadiusCurBin = m_algoParams.m_minRadius + m_algoParams.m_mergingRadiusDiffThresh * (r_bin + 0.5f); + float midRadiusNextBin = m_algoParams.m_minRadius + m_algoParams.m_mergingRadiusDiffThresh * (r_bin + 1.f + 0.5f); + + if (r >= midRadiusCurBin && r <= midRadiusNextBin) { + // The radius is at the end of the current bin or beginning of the next, we span the vote with the next bin + float voteCurBin = (midRadiusNextBin - r) / m_algoParams.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the current bin + float voteNextBin = 1.f - voteCurBin; + radiusAccumList[r_bin] += voteCurBin; + radiusActualValueList[r_bin] += r * voteCurBin; + radiusAccumList[r_bin + 1] += voteNextBin; + radiusActualValueList[r_bin + 1] += r * voteNextBin; + } + else { + // The radius is at the end of the previous bin or beginning of the current, we span the vote with the previous bin + float votePrevBin = (r - midRadiusPrevBin) / m_algoParams.m_mergingRadiusDiffThresh; // If the difference is big, it means that we are closer to the previous bin + float voteCurBin = 1.f - votePrevBin; + radiusAccumList[r_bin] += voteCurBin; + radiusActualValueList[r_bin] += r * voteCurBin; + radiusAccumList[r_bin - 1] += votePrevBin; + radiusActualValueList[r_bin - 1] += r * votePrevBin; + } + } } } } - for (unsigned int idBin = 0; idBin < nbBins; idBin++) { + auto computeEffectiveRadius = [](const float &votes, const float &weigthedSumRadius) { + float r_effective = -1.f; + if (votes > std::numeric_limits::epsilon()) { + r_effective = weigthedSumRadius / votes; + } + return r_effective; + }; + + std::vector v_r_effective; + std::vector v_votes_effective; + for (int idBin = 0; idBin < nbBins; ++idBin) { + float r_effective = computeEffectiveRadius(radiusAccumList[idBin], radiusActualValueList[idBin]); + float effective_votes = radiusAccumList[idBin]; + bool is_r_effective_similar = (r_effective > 0.f); + // Looking for potential similar radii in the following bins + // If so, compute the barycenter radius between them + int idCandidate = idBin + 1; + while ((idCandidate < nbBins) && is_r_effective_similar) { + float r_effective_candidate = computeEffectiveRadius(radiusAccumList[idCandidate], radiusActualValueList[idCandidate]); + if (std::abs(r_effective_candidate - r_effective) < m_algoParams.m_mergingRadiusDiffThresh) { + r_effective = (r_effective * effective_votes + r_effective_candidate * radiusAccumList[idCandidate]) / (effective_votes + radiusAccumList[idCandidate]); + effective_votes += radiusAccumList[idCandidate]; + radiusAccumList[idCandidate] = -.1f; + radiusActualValueList[idCandidate] = -1.f; + is_r_effective_similar = true; + } + else { + is_r_effective_similar = false; + } + ++idCandidate; + } + + if (effective_votes > m_algoParams.m_centerMinThresh) { + // Only the circles having enough votes are considered + v_r_effective.push_back(r_effective); + v_votes_effective.push_back(effective_votes); + } + } + + unsigned int nbCandidates = v_r_effective.size(); + for (unsigned int idBin = 0; idBin < nbCandidates; ++idBin) { // If the circle of center CeC_i and radius RCB_k has enough votes, it is added to the list // of Circle Candidates - if (radiusAccumList[idBin] > m_algoParams.m_centerThresh) { - float r_effective = radiusActualValueList[idBin] / (float)radiusAccumList[idBin]; - vpImageCircle candidateCircle(vpImagePoint(centerCandidate.first, centerCandidate.second), r_effective); - float proba = computeCircleProbability(candidateCircle, radiusAccumList[idBin]); - if (proba > m_algoParams.m_circleProbaThresh) { - m_circleCandidates.push_back(candidateCircle); - m_circleCandidatesProbabilities.push_back(proba); - m_circleCandidatesVotes.push_back(radiusAccumList[idBin]); - } + float r_effective = v_r_effective[idBin]; + vpImageCircle candidateCircle(vpImagePoint(centerCandidate.first, centerCandidate.second), r_effective); + float proba = computeCircleProbability(candidateCircle, v_votes_effective[idBin]); + if (proba > m_algoParams.m_circleProbaThresh) { + m_circleCandidates.push_back(candidateCircle); + m_circleCandidatesProbabilities.push_back(proba); + m_circleCandidatesVotes.push_back(v_votes_effective[idBin]); } } } @@ -577,21 +828,36 @@ vpCircleHoughTransform::computeCircleProbability(const vpImageCircle &circle, co void vpCircleHoughTransform::mergeCircleCandidates() { - // For each circle candidate CiC_i do: std::vector circleCandidates = m_circleCandidates; std::vector circleCandidatesVotes = m_circleCandidatesVotes; std::vector circleCandidatesProba = m_circleCandidatesProbabilities; - size_t nbCandidates = m_circleCandidates.size(); - for (size_t i = 0; i < nbCandidates; i++) { + // First iteration of merge + mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba); + + // Second iteration of merge + mergeCandidates(circleCandidates, circleCandidatesVotes, circleCandidatesProba); + + // Saving the results + m_finalCircles = circleCandidates; + m_finalCircleVotes = circleCandidatesVotes; + m_finalCirclesProbabilities = circleCandidatesProba; +} + +void +vpCircleHoughTransform::mergeCandidates(std::vector &circleCandidates, std::vector &circleCandidatesVotes, + std::vector &circleCandidatesProba) +{ + size_t nbCandidates = circleCandidates.size(); + for (size_t i = 0; i < nbCandidates; ++i) { vpImageCircle cic_i = circleCandidates[i]; // // For each other circle candidate CiC_j do: - for (size_t j = i + 1; j < nbCandidates; j++) { + for (size_t j = i + 1; j < nbCandidates; ++j) { vpImageCircle cic_j = circleCandidates[j]; // // // Compute the similarity between CiC_i and CiC_j double distanceBetweenCenters = vpImagePoint::distance(cic_i.getCenter(), cic_j.getCenter()); double radiusDifference = std::abs(cic_i.getRadius() - cic_j.getRadius()); - bool areCirclesSimilar = (distanceBetweenCenters < m_algoParams.m_centerMinDist - && radiusDifference < m_algoParams.m_mergingRadiusDiffThresh + bool areCirclesSimilar = ((distanceBetweenCenters < m_algoParams.m_centerMinDist) + && (radiusDifference < m_algoParams.m_mergingRadiusDiffThresh) ); if (areCirclesSimilar) { @@ -610,52 +876,13 @@ vpCircleHoughTransform::mergeCircleCandidates() circleCandidates.pop_back(); circleCandidatesVotes.pop_back(); circleCandidatesProba.pop_back(); - nbCandidates--; - j--; + --nbCandidates; + --j; } } // // Add the circle candidate CiC_i, potentially merged with other circle candidates, to the final list of detected circles - m_finalCircles.push_back(cic_i); + circleCandidates[i] = cic_i; } - - nbCandidates = m_finalCircles.size(); - for (size_t i = 0; i < nbCandidates; i++) { - vpImageCircle cic_i = m_finalCircles[i]; - // // For each other circle candidate CiC_j do: - for (size_t j = i + 1; j < nbCandidates; j++) { - vpImageCircle cic_j = m_finalCircles[j]; - // // // Compute the similarity between CiC_i and CiC_j - double distanceBetweenCenters = vpImagePoint::distance(cic_i.getCenter(), cic_j.getCenter()); - double radiusDifference = std::abs(cic_i.getRadius() - cic_j.getRadius()); - bool areCirclesSimilar = (distanceBetweenCenters < m_algoParams.m_centerMinDist - && radiusDifference < m_algoParams.m_mergingRadiusDiffThresh - ); - - if (areCirclesSimilar) { - // // // If the similarity exceeds a threshold, merge the circle candidates CiC_i and CiC_j and remove CiC_j of the list - unsigned int totalVotes = circleCandidatesVotes[i] + circleCandidatesVotes[j]; - float totalProba = circleCandidatesProba[i] + circleCandidatesProba[j]; - float newProba = 0.5f * totalProba; - float newRadius = (cic_i.getRadius() * circleCandidatesProba[i] + cic_j.getRadius() * circleCandidatesProba[j]) / totalProba; - vpImagePoint newCenter = (cic_i.getCenter() * circleCandidatesProba[i]+ cic_j.getCenter() * circleCandidatesProba[j]) / totalProba; - cic_i = vpImageCircle(newCenter, newRadius); - m_finalCircles[j] = m_finalCircles[nbCandidates - 1]; - circleCandidatesVotes[i] = totalVotes / 2; - circleCandidatesVotes[j] = circleCandidatesVotes[nbCandidates - 1]; - circleCandidatesProba[i] = newProba; - circleCandidatesProba[j] = circleCandidatesProba[nbCandidates - 1]; - m_finalCircles.pop_back(); - circleCandidatesVotes.pop_back(); - circleCandidatesProba.pop_back(); - nbCandidates--; - j--; - } - } - // // Add the circle candidate CiC_i, potentially merged with other circle candidates, to the final list of detected circles - m_finalCircles[i] = cic_i; - } - m_finalCircleVotes = circleCandidatesVotes; - m_finalCirclesProbabilities = circleCandidatesProba; } std::string diff --git a/tutorial/imgproc/hough-transform/config/detector_full.json b/tutorial/imgproc/hough-transform/config/detector_full.json index 0ac9889943..bff2f20969 100644 --- a/tutorial/imgproc/hough-transform/config/detector_full.json +++ b/tutorial/imgproc/hough-transform/config/detector_full.json @@ -1,8 +1,14 @@ { - "lowerCannyThresh": 50.0, - "upperCannyThresh": 150.0, + "averagingWindowSize": 5, + "cannyBackendType": "opencv-backend", + "filteringAndGradientType": "gaussianblur+scharr-filtering", + "lowerThresholdRatio": 0.6, + "lowerCannyThresh": -1.0, + "upperThresholdRatio": 0.9, + "upperCannyThresh": -1.0, "centerMinDistance": 15.0, "centerThresh": 100.0, + "expectedNbCenters": 4, "centerXlimits": [ 0, 640 @@ -12,7 +18,8 @@ 480 ], "circlePerfectnessThreshold": 0.9, - "dilatationNbIter": 1, + "dilatationKernelSize": 5, + "edgeMapFilteringNbIter" : 0, "gaussianKernelSize": 5, "gaussianStdev": 1.0, "mergingRadiusDiffThresh": 10.0, @@ -21,5 +28,5 @@ 1000 ], "circleProbabilityThreshold": 0.9, - "sobelKernelSize": 3 + "gradientFilterKernelSize": 3 } diff --git a/tutorial/imgproc/hough-transform/config/detector_half.json b/tutorial/imgproc/hough-transform/config/detector_half.json index 78d08a7ac7..afda9ce625 100644 --- a/tutorial/imgproc/hough-transform/config/detector_half.json +++ b/tutorial/imgproc/hough-transform/config/detector_half.json @@ -1,8 +1,14 @@ { - "lowerCannyThresh": 50.0, - "upperCannyThresh": 150.0, + "averagingWindowSize": 5, + "cannyBackendType": "opencv-backend", + "filteringAndGradientType": "gaussianblur+scharr-filtering", + "lowerThresholdRatio": 0.6, + "lowerCannyThresh": -1.0, + "upperThresholdRatio": 0.9, + "upperCannyThresh": -1.0, "centerMinDistance": 15.0, "centerThresh": 50.0, + "expectedNbCenters": 4, "centerXlimits": [ 0, 640 @@ -12,7 +18,8 @@ 480 ], "circlePerfectnessThreshold": 0.9, - "dilatationNbIter": 1, + "dilatationKernelSize": 5, + "edgeMapFilteringNbIter" : 0, "gaussianKernelSize": 5, "gaussianStdev": 1.0, "mergingRadiusDiffThresh": 10.0, @@ -21,5 +28,5 @@ 1000 ], "circleProbabilityThreshold": 0.9, - "sobelKernelSize": 3 + "gradientFilterKernelSize": 3 } diff --git a/tutorial/imgproc/hough-transform/config/detector_img.json b/tutorial/imgproc/hough-transform/config/detector_img.json index 3b0c490eb7..aa428820c2 100644 --- a/tutorial/imgproc/hough-transform/config/detector_img.json +++ b/tutorial/imgproc/hough-transform/config/detector_img.json @@ -1,12 +1,14 @@ { + "averagingWindowSize": 5, "cannyBackendType": "opencv-backend", - "filteringAndGradientType": "gaussianblur+sobel-filtering", - "lowerCannyThresh": 100.0, + "filteringAndGradientType": "gaussianblur+scharr-filtering", + "lowerCannyThresh": -1.0, "lowerThresholdRatio": 0.6, - "upperCannyThresh": 200.0, - "upperThresholdRatio": 0.8, + "upperCannyThresh": -1.0, + "upperThresholdRatio": 0.9, "centerMinDistance": 5.0, - "centerThresh": 100.0, + "centerThresh": 70.0, + "expectedNbCenters": -1, "centerXlimits": [ 0, 1920 @@ -15,16 +17,16 @@ 0, 1080 ], - "circlePerfectnessThreshold": 0.95, - "dilatationNbIter": 1, - "edgeMapFilteringNbIter" : 5, - "gaussianKernelSize": 25, - "gaussianStdev": 2.5, + "circlePerfectnessThreshold": 0.65, + "dilatationKernelSize": 5, + "edgeMapFilteringNbIter" : 3, + "gaussianKernelSize": 5, + "gaussianStdev": 1.0, "mergingRadiusDiffThresh": 5.0, "radiusLimits": [ 34, 75 ], - "circleProbabilityThreshold": 0.75, - "sobelKernelSize": 7 + "circleProbabilityThreshold": 0.725, + "gradientFilterKernelSize": 3 } diff --git a/tutorial/imgproc/hough-transform/config/detector_quarter.json b/tutorial/imgproc/hough-transform/config/detector_quarter.json index 961fe089f8..cc0f6b5761 100644 --- a/tutorial/imgproc/hough-transform/config/detector_quarter.json +++ b/tutorial/imgproc/hough-transform/config/detector_quarter.json @@ -1,8 +1,14 @@ { - "lowerCannyThresh": 50.0, - "upperCannyThresh": 150.0, + "averagingWindowSize": 5, + "cannyBackendType": "opencv-backend", + "filteringAndGradientType": "gaussianblur+scharr-filtering", + "lowerThresholdRatio": 0.6, + "lowerCannyThresh": -1.0, + "upperThresholdRatio": 0.9, + "upperCannyThresh": -1.0, "centerMinDistance": 15.0, "centerThresh": 25.0, + "expectedNbCenters": 4, "centerXlimits": [ 0, 640 @@ -12,14 +18,15 @@ 480 ], "circlePerfectnessThreshold": 0.9, - "dilatationNbIter": 1, + "dilatationKernelSize": 5, + "edgeMapFilteringNbIter" : 0, "gaussianKernelSize": 5, "gaussianStdev": 1.0, - "mergingRadiusDiffThresh": 15.0, + "mergingRadiusDiffThresh": 10.0, "radiusLimits": [ 0, 1000 ], "circleProbabilityThreshold": 0.9, - "sobelKernelSize": 3 + "gradientFilterKernelSize": 3 } diff --git a/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp b/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp index d9e4b13c2b..d89f7bcb51 100644 --- a/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp +++ b/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp @@ -17,148 +17,7 @@ #include "drawingHelpers.h" -//! [Enum input] -typedef enum TypeInputImage -{ - FULL_DISKS = 0, - HALF_DISKS = 1, - QUARTER_DISKS = 2, - USER_IMG = 3 -}TypeInputImage; - -std::string typeInputImageToString(const TypeInputImage &type) -{ - std::string name; - switch (type) { - case FULL_DISKS: - name = "full_disks"; - break; - case HALF_DISKS: - name = "half_disks"; - break; - case QUARTER_DISKS: - name = "quarter_disks"; - break; - case USER_IMG: - name = "path/to/your/image"; - } - return name; -} -//! [Enum input] - -TypeInputImage typeInputImageFromString(const std::string &name) -{ - TypeInputImage type(USER_IMG); - bool hasFound(false); - for (unsigned int id = 0; id < USER_IMG && !hasFound; id++) { - TypeInputImage candidate = (TypeInputImage)id; - if (name == typeInputImageToString(candidate)) { - type = candidate; - hasFound = true; - } - } - return type; -} - -std::string getAvailableTypeInputImage(const std::string &prefix = "<", const std::string &sep = " , ", const std::string &suffix = ">") -{ - std::string list(prefix); - for (unsigned int id = 0; id < USER_IMG; id++) { - list += typeInputImageToString((TypeInputImage)id) + sep; - } - list += typeInputImageToString(USER_IMG) + suffix; - return list; -} - -//! [Draw disks] -void -drawDisk(vpImage &I, const vpImagePoint ¢er, const unsigned int &radius, - const unsigned int &borderColor, const unsigned int &fillingColor, const unsigned int &thickness, const unsigned int &bckg) - //! [Draw disks] -{ - vpImageDraw::drawCircle(I, center, radius, borderColor, thickness); - vp::floodFill(I, - center, - bckg, - fillingColor, - vpImageMorphology::CONNEXITY_4 - ); -} - -//! [Draw synthetic] -vpImage -generateImage(const TypeInputImage &inputType) -//! [Draw synthetic] -{ - // // Image dimensions and background - const unsigned int width = 640; - const unsigned int height = 480; - const unsigned int bckg = 0; - - // // Disks parameters - const unsigned int circleColor = 128; - const unsigned int circleRadius = 50; - const unsigned int circleThickness = 1; - - // // Disks position when full circles - const double topFull = height / 4; - const double bottomFull = 3 * height / 4; - const double leftFull = width / 4; - const double rightFull = 3 * width / 4; - - // // Disks position when Half of circles - const double topHalf = 1; // m_centerThresh(25) , m_radiusBinSize(10) , m_radiusRatioThresh(50) , m_mergingDistanceThresh(15) , m_mergingRadiusDiffThresh(1.5 * (double) m_radiusBinSize) - const double bottomHalf = height - 1; - const double leftHalf = width / 4; - const double rightHalf = 3 * width / 4; - - // // Disks position when Quarter of circles - const double topQuarter = 1; // m_centerThresh(15) , m_radiusBinSize(10) , m_radiusRatioThresh(50) , m_mergingDistanceThresh(15) , m_mergingRadiusDiffThresh(1.5 * (double) m_radiusBinSize) - const double bottomQuarter = height - 1; - const double leftQuarter = 1; - const double rightQuarter = width - 1; - vpImage I_src(height, width, bckg); - - // // Selecting position of the disks depending on their visibility - double top, left, bottom, right; - switch (inputType) { - case FULL_DISKS: - top = topFull; - left = leftFull; - bottom = bottomFull; - right = rightFull; - break; - case HALF_DISKS: - top = topHalf; - left = leftHalf; - bottom = bottomHalf; - right = rightHalf; - break; - case QUARTER_DISKS: - top = topQuarter; - left = leftQuarter; - bottom = bottomQuarter; - right = rightQuarter; - break; - default: - throw(vpException(vpException::badValue, "Using other type of input than the one that has been implemented to generate disks.")); - break; - } - - drawDisk(I_src, vpImagePoint(top, left), circleRadius, circleColor, circleColor, circleThickness, bckg); - drawDisk(I_src, vpImagePoint(top, left), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor); - drawDisk(I_src, vpImagePoint(bottom, left), circleRadius, circleColor, circleColor, circleThickness, bckg); - drawDisk(I_src, vpImagePoint(bottom, left), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor); - drawDisk(I_src, vpImagePoint(top, right), circleRadius, circleColor, circleColor, circleThickness, bckg); - drawDisk(I_src, vpImagePoint(top, right), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor); - drawDisk(I_src, vpImagePoint(bottom, right), circleRadius, circleColor, circleColor, circleThickness, bckg); - drawDisk(I_src, vpImagePoint(bottom, right), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor); - - std::cout << "Done drawing" << std::endl << std::flush; - return I_src; -} - -bool test_detection(const vpImage &I_src, vpCircleHoughTransform &detector, const int &nbCirclesToDetect, const bool &blockingMode, const bool &displayCanny) +bool run_detection(const vpImage &I_src, vpCircleHoughTransform &detector, const int &nbCirclesToDetect, const bool &blockingMode, const bool &displayCanny) { double t0 = vpTime::measureTimeMicros(); //! [Run detection] @@ -195,30 +54,31 @@ bool test_detection(const vpImage &I_src, vpCircleHoughTransform int main(int argc, char **argv) { - const std::string def_input(typeInputImageToString(FULL_DISKS)); + const std::string def_input("coins2.jpg"); const std::string def_jsonFilePath = std::string(""); const int def_nbCirclesToDetect = -1; const int def_gaussianKernelSize = 5; const float def_gaussianSigma = 1.f; const int def_sobelKernelSize = 3; -#ifdef HAVE_OPENCV_IMGPROC - const float def_lowerCannyThresh = 50.f; - const float def_upperCannyThresh = 150.f; -#else - const float def_lowerCannyThresh = 8.f; - const float def_upperCannyThresh = 25.f; -#endif - const int def_nbEdgeFilteringIter = 2; - const std::pair def_centerXlimits = std::pair(0, 640); - const std::pair def_centerYlimits = std::pair(0, 480); - const unsigned int def_minRadius = 0; - const unsigned int def_maxRadius = 1000; - const int def_dilatationRepet = 1; - const float def_centerThresh = -1.f; - const float def_circleProbaThresh = 0.9f; - const float def_circlePerfectness = 0.85f; - const float def_centerDistanceThresh = 15.f; - const float def_radiusDifferenceThresh = 15.f; + const float def_lowerCannyThresh = -1.f; + const float def_upperCannyThresh = -1.f; + const int def_nbEdgeFilteringIter = 3; + const std::pair def_centerXlimits = std::pair(0, 1920); + const std::pair def_centerYlimits = std::pair(0, 1080); + const unsigned int def_minRadius = 34; + const unsigned int def_maxRadius = 75; + const int def_dilatationKernelSize = 5; + const float def_centerThresh = 70.f; + const float def_circleProbaThresh = 0.725f; + const float def_circlePerfectness = 0.65f; + const float def_centerDistanceThresh = 5.f; + const float def_radiusDifferenceThresh = 5.f; + const int def_averagingWindowSize = 5; + const vpImageFilter::vpCannyFilteringAndGradientType def_filteringAndGradientType = vpImageFilter::CANNY_GBLUR_SCHARR_FILTERING; + const vpImageFilter::vpCannyBackendType def_cannyBackendType = vpImageFilter::CANNY_OPENCV_BACKEND; + const float def_lowerCannyThreshRatio = 0.6f; + const float def_upperCannyThreshRatio = 0.9f; + const int def_expectedNbCenters = -1; std::string opt_input(def_input); std::string opt_jsonFilePath = def_jsonFilePath; @@ -233,12 +93,18 @@ int main(int argc, char **argv) std::pair opt_centerYlimits = def_centerYlimits; unsigned int opt_minRadius = def_minRadius; unsigned int opt_maxRadius = def_maxRadius; - int opt_dilatationRepet = def_dilatationRepet; + int opt_dilatationKerneSize = def_dilatationKernelSize; float opt_centerThresh = def_centerThresh; float opt_circleProbaThresh = def_circleProbaThresh; float opt_circlePerfectness = def_circlePerfectness; float opt_centerDistanceThresh = def_centerDistanceThresh; float opt_radiusDifferenceThresh = def_radiusDifferenceThresh; + int opt_averagingWindowSize = def_averagingWindowSize; + vpImageFilter::vpCannyFilteringAndGradientType opt_filteringAndGradientType = def_filteringAndGradientType; + vpImageFilter::vpCannyBackendType opt_cannyBackendType = def_cannyBackendType; + float opt_lowerCannyThreshRatio = def_lowerCannyThreshRatio; + float opt_upperCannyThreshRatio = def_upperCannyThreshRatio; + int opt_expectedNbCenters = def_expectedNbCenters; bool opt_displayCanny = false; for (int i = 1; i < argc; i++) { @@ -265,7 +131,7 @@ int main(int argc, char **argv) opt_gaussianSigma = static_cast(atof(argv[i + 1])); i++; } - else if (argName == "--sobel-kernel" && i + 1 < argc) { + else if (argName == "--gradient-kernel" && i + 1 < argc) { opt_sobelKernelSize = atoi(argv[i + 1]); i++; } @@ -278,8 +144,12 @@ int main(int argc, char **argv) opt_nbEdgeFilteringIter = atoi(argv[i + 1]); i++; } - else if (argName == "--dilatation-repet" && i + 1 < argc) { - opt_dilatationRepet = atoi(argv[i + 1]); + else if (argName == "--dilatation-kernel-size" && i + 1 < argc) { + opt_dilatationKerneSize = atoi(argv[i + 1]); + i++; + } + else if (argName == "--averaging-window-size" && i + 1 < argc) { + opt_averagingWindowSize = atoi(argv[i + 1]); i++; } else if (argName == "--radius-limits" && i + 2 < argc) { @@ -312,6 +182,26 @@ int main(int argc, char **argv) opt_radiusDifferenceThresh = static_cast(atof(argv[i + 2])); i += 2; } + else if (argName == "--filtering-type" && i + 1 < argc) { + opt_filteringAndGradientType = vpImageFilter::vpCannyFilteringAndGradientTypeFromString(std::string(argv[i+1])); + i++; + } + else if (argName == "--canny-backend" && i + 1 < argc) { + opt_cannyBackendType = vpImageFilter::vpCannyBackendTypeFromString(std::string(argv[i+1])); + i++; + } + else if (argName == "--lower-canny-ratio" && i + 1 < argc) { + opt_lowerCannyThreshRatio = atof(argv[i + 1]); + i++; + } + else if (argName == "--upper-canny-ratio" && i + 1 < argc) { + opt_upperCannyThreshRatio = atof(argv[i + 1]); + i++; + } + else if (argName == "--expected-nb-centers" && i + 1 < argc) { + opt_expectedNbCenters = atoi(argv[i + 1]); + i++; + } else if (argName == "--display-edge-map") { opt_displayCanny = true; } @@ -321,31 +211,43 @@ int main(int argc, char **argv) << std::endl; std::cout << "SYNOPSIS" << std::endl; std::cout << "\t" << argv[0] - << "\t [--input " << getAvailableTypeInputImage() << "]" << std::endl + << "\t [--input ]" << std::endl #ifdef VISP_HAVE_NLOHMANN_JSON << "\t [--config ] (default: " << (def_jsonFilePath.empty() ? "unused" : def_jsonFilePath) << ")" << std::endl #endif << "\t [--nb-circles ] (default: " << def_nbCirclesToDetect << ")" << std::endl << "\t [--gaussian-kernel ] (default: " << def_gaussianKernelSize << ")" << std::endl << "\t [--gaussian-sigma ] (default: " << def_gaussianSigma << ")" << std::endl - << "\t [--sobel-kernel ] (default: " << def_sobelKernelSize << ")" << std::endl + << "\t [--gradient-kernel ] (default: " << def_sobelKernelSize << ")" << std::endl << "\t [--canny-thresh ] (default: " << def_lowerCannyThresh << " ; " << def_upperCannyThresh << ")" << std::endl << "\t [--edge-filter ] (default: " << def_nbEdgeFilteringIter << ")" << std::endl << "\t [--radius-limits ] (default: min = " << def_minRadius << ", max = " << def_maxRadius << ")" << std::endl - << "\t [--dilatation-repet ] (default: " << def_dilatationRepet << ")" << std::endl - << "\t [--center-thresh ] (default: " << (def_centerThresh < 0 ? "auto" : std::to_string(def_centerThresh)) << ")" << std::endl + << "\t [--dilatation-kernel-size ] (default: " << def_dilatationKernelSize << ")" << std::endl + << "\t [--averaging-window-size ] (default: " << def_averagingWindowSize << ")" << std::endl + << "\t [--center-thresh ] (default: " << def_centerThresh << ")" << std::endl << "\t [--center-xlim ] (default: " << def_centerXlimits.first << " , " << def_centerXlimits.second << ")" << std::endl << "\t [--center-ylim ] (default: " << def_centerYlimits.first << " , " << def_centerYlimits.second << ")" << std::endl << "\t [--circle-probability-thresh ] (default: " << def_circleProbaThresh << ")" << std::endl << "\t [--circle-perfectness ] (default: " << def_circlePerfectness << ")" << std::endl << "\t [--merging-thresh ] (default: centers distance threshold = " << def_centerDistanceThresh << ", radius difference threshold = " << def_radiusDifferenceThresh << ")" << std::endl + << "\t [--filtering-type ]" + << " (default: " << vpImageFilter::vpCannyFilteringAndGradientTypeToString(def_filteringAndGradientType) << ")" << std::endl + << "\t [--canny-backend ]" + << " (default: " << vpImageFilter::vpCannyBackendTypeToString(def_cannyBackendType) << ")" << std::endl + << "\t [--lower-canny-ratio ]" + << " (default: " << def_lowerCannyThreshRatio<< ")" << std::endl + << "\t [--upper-canny-ratio ]" + << " (default: " << def_upperCannyThreshRatio << ")" << std::endl + << "\t [--expected-nb-centers ]" + << " (default: " << (def_expectedNbCenters < 0 ? "no limits" : std::to_string(def_expectedNbCenters)) << ")" << std::endl << "\t [--display-edge-map]" << std::endl << "\t [--help, -h]" << std::endl << std::endl; std::cout << "DESCRIPTION" << std::endl << "\t--input" << std::endl - << "\t\tPermit to choose the type of input of the Hough Circle Algorithm" << std::endl + << "\t\tPermit to choose the input of the Hough Circle Algorithm." << std::endl + << "\t\tIf you want to use a succession of images as video, their name must be in the format ${BASENAME}\%d.{jpg, png}." << std::endl << "\t\tDefault: " << def_input << std::endl << std::endl #ifdef VISP_HAVE_NLOHMANN_JSON @@ -369,6 +271,11 @@ int main(int argc, char **argv) << "\t\tMust be a positive value." << std::endl << "\t\tDefault: " << def_gaussianSigma << std::endl << std::endl + << "\t--gradient-kernel" << std::endl + << "\t\tPermit to set the size of the Gaussian filter used to smooth the input image and compute its gradients." << std::endl + << "\t\tMust be an odd value." << std::endl + << "\t\tDefault: " << def_gaussianKernelSize << std::endl + << std::endl << "\t--canny-thresh" << std::endl << "\t\tPermit to set the lower and upper thresholds of the Canny edge detector." << std::endl << "\t\tIf a value is negative, it will be automatically computed." << std::endl @@ -383,16 +290,21 @@ int main(int argc, char **argv) << "\t\tPermit to set the minimum and maximum radii of the circles we are looking for." << std::endl << "\t\tDefault: min = " << def_minRadius << ", max = " << def_maxRadius << std::endl << std::endl - << "\t--dilatation-repet" << std::endl - << "\t\tPermit to set the number of iterations of the dilatation operation used to detect the maxima of the centers votes." << std::endl + << "\t--dilatation-kernel-size" << std::endl + << "\t\tPermit to set the size of the kernel of the dilatation operation used to detect the maxima of the centers votes." << std::endl << "\t\tMinimum tolerated value is 1." << std::endl - << "\t\tDefault: " << def_dilatationRepet << std::endl + << "\t\tDefault: " << def_dilatationKernelSize << std::endl + << std::endl + << "\t--averaging-window-size" << std::endl + << "\t\tPermit to set the number size of the averaging window used to detect the maxima of the centers votes." << std::endl + << "\t\tMust be odd." << std::endl + << "\t\tDefault: " << def_averagingWindowSize << std::endl << std::endl << "\t--center-thresh" << std::endl << "\t\tPermit to set the minimum number of votes a point must reach to be considered as a center candidate." << std::endl << "\t\tIf the input is a real image, must be a positive value." << std::endl << "\t\tOtherwise, if the input is a synthetic image and the value is negative, a fine-tuned value will be used." << std::endl - << "\t\tDefault: " << (def_centerThresh < 0 ? "auto" : std::to_string(def_centerThresh)) << std::endl + << "\t\tDefault: " << def_centerThresh << std::endl << std::endl << "\t--center-xlim" << std::endl << "\t\tPermit to set the minimum and maximum horizontal position to be considered as a center candidate." << std::endl @@ -420,6 +332,28 @@ int main(int argc, char **argv) << "\t\tThe radius difference threshold indicates the maximum absolute difference between the two circle candidates in order to be merged." << std::endl << "\t\tTwo circle candidates must met these two conditions in order to be merged together." << std::endl << "\t\tDefault: centers distance threshold = " << def_centerDistanceThresh << ", radius difference threshold = " << def_radiusDifferenceThresh << std::endl + << std::endl + << "\t--filtering-type" << std::endl + << "\t\tPermit to choose the gradient filters." << std::endl + << "\t\tDefault: " << vpImageFilter::vpCannyFilteringAndGradientTypeToString(def_filteringAndGradientType) << ", available: " << vpImageFilter::vpCannyFilteringAndGradientTypeList() << std::endl + << std::endl + << "\t--canny-backend" << std::endl + << "\t\tPermit to choose the backend used to compute the edge map." << std::endl + << "\t\tDefault: " << vpImageFilter::vpCannyBackendTypeToString(def_cannyBackendType) << ", available: " << vpImageFilter::vpCannyBackendTypeList() << std::endl + << std::endl + << "\t--lower-canny-ratio" << std::endl + << "\t\tPermit to choose the ratio for the lower threshold if automatic thresholding is chosen." << std::endl + << "\t\tDefault: " << def_lowerCannyThreshRatio << std::endl + << std::endl + << "\t--upper-canny-ratio" << std::endl + << "\t\tPermit to choose the ratio for the upper threshold if automatic thresholding is chosen." << std::endl + << "\t\tDefault: " << def_upperCannyThreshRatio << std::endl + << std::endl + << "\t--expected-nb-centers" << std::endl + << "\t\tPermit to choose the maximum number of centers having more votes than the threshold that are kept." << std::endl + << "\t\tA negative value makes that all the centers having more votes than the threshold are kept." << std::endl + << "\t\tDefault: " << (def_expectedNbCenters < 0 ? "no limits" : std::to_string(def_expectedNbCenters)) << std::endl + << std::endl << "\t--display-edge-map" << std::endl << "\t\tPermit to display the edge map used to detect the circles" << std::endl << "\t\tDefault: off" << std::endl @@ -428,36 +362,6 @@ int main(int argc, char **argv) } } - if (opt_centerThresh < 0 && opt_jsonFilePath.empty()) { - // The user asked to use the parameter value that has been fine-tuned - TypeInputImage inputType = typeInputImageFromString(opt_input); - switch (inputType) { - case TypeInputImage::FULL_DISKS: -#ifdef HAVE_OPENCV_IMGPROC - opt_centerThresh = 100.; -#else - opt_centerThresh = 75.; -#endif - break; - case TypeInputImage::HALF_DISKS: -#ifdef HAVE_OPENCV_IMGPROC - opt_centerThresh = 50.; -#else - opt_centerThresh = 25.; -#endif - break; - case TypeInputImage::QUARTER_DISKS: -#ifdef HAVE_OPENCV_IMGPROC - opt_centerThresh = 25.; -#else - opt_centerThresh = 15.; -#endif - break; - default: - throw(vpException(vpException::badValue, "Missing center threshold value to use with actual pictures as input. See the help for more information.")); - } - } - //! [Algo params] vpCircleHoughTransform::vpCircleHoughTransformParameters algoParams(opt_gaussianKernelSize @@ -470,12 +374,18 @@ int main(int argc, char **argv) , opt_centerYlimits , opt_minRadius , opt_maxRadius - , opt_dilatationRepet + , opt_dilatationKerneSize , opt_centerThresh , opt_circleProbaThresh , opt_circlePerfectness , opt_centerDistanceThresh , opt_radiusDifferenceThresh + , opt_averagingWindowSize + , opt_filteringAndGradientType + , opt_cannyBackendType + , opt_lowerCannyThreshRatio + , opt_upperCannyThreshRatio + , opt_expectedNbCenters ); //! [Algo params] @@ -497,39 +407,32 @@ int main(int argc, char **argv) std::cout << detector; vpImage I_src; - TypeInputImage inputType = typeInputImageFromString(opt_input); - if (inputType == USER_IMG) { - //! [Manage video] - if (opt_input.find("%") != std::string::npos) { - // The user wants to read a sequence of images from different files - bool hasToContinue = true; - vpVideoReader g; - g.setFileName(opt_input); - g.open(I_src); - while (!g.end() && hasToContinue) { - g.acquire(I_src); - hasToContinue = test_detection(I_src, detector, opt_nbCirclesToDetect, false, opt_displayCanny); - vpTime::wait(40); - } - } - //! [Manage video] - else { - //! [Manage single image] - // Check if opt_input exists - if (!vpIoTools::checkFilename(opt_input)) { - throw(vpException(vpException::ioError, "Input file \"" + opt_input + "\" does not exist !")); - } - // Read the image and perform detection on it - vpImageIo::read(I_src, opt_input); - test_detection(I_src, detector, opt_nbCirclesToDetect, true, opt_displayCanny); - //! [Manage single image] + + //! [Manage video] + if (opt_input.find("%") != std::string::npos) { + // The user wants to read a sequence of images from different files + bool hasToContinue = true; + vpVideoReader g; + g.setFileName(opt_input); + g.open(I_src); + while (!g.end() && hasToContinue) { + g.acquire(I_src); + hasToContinue = run_detection(I_src, detector, opt_nbCirclesToDetect, false, opt_displayCanny); + vpTime::wait(40); } } + //! [Manage video] else { - //! [Manage synthetic image] - I_src = generateImage(inputType); - test_detection(I_src, detector, opt_nbCirclesToDetect, true, opt_displayCanny); - //! [Manage synthetic image] + //! [Manage single image] + // Check if opt_input exists + if (!vpIoTools::checkFilename(opt_input)) { + throw(vpException(vpException::ioError, "Input file \"" + opt_input + "\" does not exist !")); + } + // Read the image and perform detection on it + vpImageIo::read(I_src, opt_input); + run_detection(I_src, detector, opt_nbCirclesToDetect, true, opt_displayCanny); + //! [Manage single image] } + return EXIT_SUCCESS; }