diff --git a/.vscode/settings.json b/.vscode/settings.json index c42037bbb3..aa4bed3ea2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -108,7 +108,8 @@ "__nullptr": "cpp", "__string": "cpp", "compare": "cpp", - "concepts": "cpp" + "concepts": "cpp", + "*.ipp": "cpp" }, "C_Cpp.vcFormat.indent.namespaceContents": false, "editor.formatOnSave": true, diff --git a/ChangeLog.txt b/ChangeLog.txt index 76b03adb3f..c49191a2f7 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -9,12 +9,22 @@ ViSP 3.5.1 (under development) - Contributors: . Fabien Spindler, Souriya Trinh, Romain Lagneau, Antonio Marino, Samuel Felton, Francois Chaumette, Olivier Roussel + - New classes + . vpMocapVicon an interface over Vicon motion capture system + . vpMocapQualisys an interface over Qualisys motion capture system + . vpPclViewer that enables real time plotting of 3D point clouds based on PCL library + . vpRobotUniversalRobots that allows to control an Universal Robots robot + . vpRobotMavsdk that allows to control a robot equiped with a Pixhawk (drone, rover...) + . vpDetectorDNNOpenCV a wrapper over the OpenCV DNN module that allows + to detect objects using Faster-RCNN, SSD-MobileNet, ResNet 10, Yolo v3, Yolo v4, + Yolo v5, Yolo v7 and Yolo v8 convolutional networks + . vpImageCircle that refers to a 2D circle in an image + . vpCircleHoughTransform that allows to detect circles in an image + . vpCannyEdgeDetection that allows to detect Canny edges without using OpenCV + . vpMegaPose and vpMegaPoseTracker classes that are wrapper over MegaPose + https://megapose6d.github.io/ that allows 6D pose estimation using a DNN approach - New features and improvements . Video writer is now able to create the output folder recursively (see vpVideoWriter) - . Introduce Universal Robots support with new vpRobotUniversalRobots class that - allows to control an UR robot - . Introduce Vicon and Qualisys motion capture system interfaces in vpMocapVicon and - vpMocapQualisys classes respectively . Image-based and position-based examples with UR robot . Tutorial for extrinsic calibration improved with UR robot and Panda robot use cases . Tutorials in tutorial/grabber folder have now a new --no-display command line option @@ -24,20 +34,16 @@ ViSP 3.5.1 (under development) . Image-based visual-servoing, position and velocity control examples to control robots equipped with Pixhawk (see vpRobotMavsdk doc) . Windows 11 support - . New vpDetectorDNNOpenCV class a wrapper over the OpenCV DNN module that allows - to detect objects using Faster-RCNN, SSD-MobileNet, ResNet 10, Yolo v3, Yolo v4, - Yolo v5, Yolo v7 and Yolo v8 convolutional networks + . New capabilities to ease C++ inference to detect objects using convolutional networks + (see vpDetectorDNNOpenCV) . Support of JSON for modern C++ third-party to enable serialization capabilities - in vpMbGenericTracker, vpCameraParameters, vpPoseVector, vpHomogeneousMatrix, - vpPolygon3D to load/save internal data or settings from/to JSON files - . New vpPclViewer class that enables real time plotting of 3D point clouds based on - PCL library + in vpArray2D, vpCameraParameters, vpCircleHoughTransform, vpColVector, vpDetectorDNNOpenCV, + vpHomogeneousMatrix, vpMbGenericTracker, vpMe, vpPolygon3D, vpPoseVector to load/save + internal data or settings from/to JSON files . Remove deprecated OpenCV IplImage interfaces . Remove deprecated vpOpenCVGrabber, vpKeyPointSurf classes and corresponding tutorials . Minimum OpenCV version is 2.4.8 - . New vpMegaPose and vpMegaPoseTracker classes that are wrapper over MegaPose - https://megapose6d.github.io/ that allows 6D pose estimation using a DNN approach . Introduce a new moving-edges threshold parameter for the contrast between each side of the feature to track. Its value corresponds to a gray level in range [0; 255] . In moving-edges ellipse tracker (vpMeEllipse and vpMbTracker and its derived classes), @@ -73,6 +79,8 @@ ViSP 3.5.1 (under development) https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-tracking-megapose.html . New tutorial: Exporting a 3D model to MegaPose https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-megapose-model.html + . New tutorial: Gradient-based Circle Hough Transform + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-imgproc-cht.html - Bug fixed . [#1041] [example/device/framegrabber/saveRealSenseData] Wrong camera parameters and depth_M_color homogeneous matrix when aligned depth is requested diff --git a/doc/image/tutorial/imgproc/img-tutorial-cht-center-votes.png b/doc/image/tutorial/imgproc/img-tutorial-cht-center-votes.png new file mode 100644 index 0000000000..273bdbc833 Binary files /dev/null and b/doc/image/tutorial/imgproc/img-tutorial-cht-center-votes.png differ diff --git a/doc/image/tutorial/imgproc/img-tutorial-cht-radius-votes.png b/doc/image/tutorial/imgproc/img-tutorial-cht-radius-votes.png new file mode 100644 index 0000000000..9bf16887df Binary files /dev/null and b/doc/image/tutorial/imgproc/img-tutorial-cht-radius-votes.png differ diff --git a/doc/tutorial/imgproc/tutorial-imgproc-cht.dox b/doc/tutorial/imgproc/tutorial-imgproc-cht.dox new file mode 100644 index 0000000000..d0291ec803 --- /dev/null +++ b/doc/tutorial/imgproc/tutorial-imgproc-cht.dox @@ -0,0 +1,152 @@ +/** + +\page tutorial-imgproc-cht Tutorial: Gradient-based Circle Hough Transform +\tableofcontents + +\section imgproc_cht_intro Introduction + +The Circle Hough Transform (*CHT*) is an image processing algorithm that permits to +detect circles in an image. We refer the interested reader to the +[Wikipedia page](https://en.wikipedia.org/wiki/Circle_Hough_Transform) to have a better +understanding on the principles of the algorithm. + +The ViSP implementation relies on the Gradient-based implementation of the +algorithm. + +During the step where the algorithm votes for center candidates, we use the gradient information +in order to reduce the dimensionality of the search space. Instead of voting in circular pattern, +we vote along a straight line that follows the gradient. + +\image html img-tutorial-cht-center-votes.png + +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 +parameter. + +\image html img-tutorial-cht-radius-votes.png + +\section imgproc_cht_requirements Requirements + +With the current implementation, the `vpCircleHoughTransform` requires ViSP to be compiled with OpenCV. +If you do not know how to do it, please refer to the installation guidelines of \ref soft_vision_opencv. + +\section imgproc_cht_howto How to use the tutorial + +It is possible to configure the `vpCircleHoughTransform` class using a JSON file. +To do so, you need to install [JSON for modern C++](https://visp-doc.inria.fr/doxygen/visp-daily/supported-third-parties.html#soft_tool_json) +and compile ViSP with it. + +You can also configure the `vpCircleHoughTransform` class using command line arguments. +To know what are the different command line arguments the software accept, please run: +``` +$ 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: +``` +$ TARGET=full # or TARGET=half # or TARGET=quarter +$ ./tutorial-circle-hough --input ${TARGET}_disks --config config/detector_${TARGET}.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 +``` + +\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`. + +To run the software on an actual image using command line arguments instead, please run: +``` +$ ./tutorial-circle-hough --input /path/to/my/image --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 +``` + +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`. +For instance with `${BASENAME}` = `video_`, you can have the following list +of images: `video_0001.png`, `video_0002.png` and so on. + +To run the software using a JSON configuration file, please run: +``` +$ ./tutorial-circle-hough --input /path/to/video/${BASENAME}%d.png --config config/detector_img.json +``` + +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 +``` + +\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 + +If you decide to use a single image as input, the relevant piece of code that permits to +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: +\snippet tutorial-circle-hough.cpp Algo params + +The initialization of the algorithm is performed in the following piece of code. +If a JSON configuration file is given as input configuration, it will be preferred +to the command line arguments: +\snippet tutorial-circle-hough.cpp Algo init + +To run the circle detection, you must call the following method: +\snippet tutorial-circle-hough.cpp Run detection + +You could have also used the following method to get only the `num_best` best +detections: +\code +int num_best; // Set it to the number of circles you want to keep +std::vector detections = detector.detect(I, num_best); +\endcode + +Then, you can iterate on the vector of detections using a synthax similar to the following: +\snippet tutorial-circle-hough.cpp Iterate detections +*/ diff --git a/doc/tutorial/imgproc/tutorial-imgproc-count-coins.dox b/doc/tutorial/imgproc/tutorial-imgproc-count-coins.dox index c29d637392..123dd01bdb 100644 --- a/doc/tutorial/imgproc/tutorial-imgproc-count-coins.dox +++ b/doc/tutorial/imgproc/tutorial-imgproc-count-coins.dox @@ -37,7 +37,7 @@ $ ./tutorial-count-coins To run the demo code for the sample image 2: \code -$ ./tutorial-count-coins --input coins2.pgm --white_foreground +$ ./tutorial-count-coins --input coins2.jpg --white_foreground \endcode The functions we will use needs the following includes: diff --git a/doc/tutorial/tutorial-users.dox b/doc/tutorial/tutorial-users.dox index b70e38db58..96609e86b1 100644 --- a/doc/tutorial/tutorial-users.dox +++ b/doc/tutorial/tutorial-users.dox @@ -73,7 +73,7 @@ This page introduces some image processing methods. - \subpage tutorial-imgproc-connected-components
This tutorial will show you how to do a connected-components labeling. - \subpage tutorial-imgproc-flood-fill
This tutorial will show you how to use the flood fill algorithm. - \subpage tutorial-imgproc-count-coins
This tutorial will show you how to count the number of coins in an image. - +- \subpage tutorial-imgproc-cht
This tutorial will show you how to use the gradient-based Circle Hough Transform to detect circles in an image. */ /*! \page tutorial_calib Camera calibration diff --git a/modules/core/include/visp3/core/vpImageFilter.h b/modules/core/include/visp3/core/vpImageFilter.h index ff986616ac..80fb105d4b 100644 --- a/modules/core/include/visp3/core/vpImageFilter.h +++ b/modules/core/include/visp3/core/vpImageFilter.h @@ -1217,6 +1217,8 @@ class VISP_EXPORT vpImageFilter } #if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) + static double computeCannyThreshold(const cv::Mat &cv_I, const cv::Mat *p_cv_blur, double &lowerThresh); + static double computeCannyThreshold(const vpImage &I, double &lowerThresh); static double median(const cv::Mat &cv_I); static double median(const vpImage &Isrc); static std::vector median(const vpImage &Isrc); diff --git a/modules/core/src/image/vpImageFilter.cpp b/modules/core/src/image/vpImageFilter.cpp index 4908dbe0ad..8280b75b48 100644 --- a/modules/core/src/image/vpImageFilter.cpp +++ b/modules/core/src/image/vpImageFilter.cpp @@ -34,6 +34,7 @@ *****************************************************************************/ #include +#include #include #include #include @@ -214,7 +215,7 @@ std::vector vpImageFilter::median(const vpImage &Isrc) * \param[out] lowerThresh The lower threshold for the Canny edge filter. * \return double The upper Canny edge filter threshold. */ -double computeCannyThreshold(const cv::Mat &cv_I, const cv::Mat *p_cv_blur, double &lowerThresh) +double vpImageFilter::computeCannyThreshold(const cv::Mat &cv_I, const cv::Mat *p_cv_blur, double &lowerThresh) { cv::Mat cv_I_blur; if (p_cv_blur != nullptr) { @@ -245,7 +246,7 @@ double computeCannyThreshold(const cv::Mat &cv_I, const cv::Mat *p_cv_blur, doub * \param[in] I : The gray-scale image, in ViSP format. * \return double The upper Canny edge filter threshold. */ -double computeCannyThreshold(const vpImage &I, double &lowerThresh) +double vpImageFilter::computeCannyThreshold(const vpImage &I, double &lowerThresh) { cv::Mat cv_I; vpImageConvert::convert(I, cv_I); @@ -308,6 +309,9 @@ void vpImageFilter::canny(const vpImage &Isrc, vpImage +#include + +// ViSP includes +#include +#include +#include +#include +#include +#include +#include +#include + +// 3rd parties inclue +#ifdef VISP_HAVE_NLOHMANN_JSON +#include +using json = nlohmann::json; +#endif + +/** + * \ingroup group_hough_transform + * \brief Class that permits to detect 2D circles in a image using + * the gradient-based Circle Hough transform. + * Please find more information on the algorithm + * [here](https://theailearner.com/tag/hough-gradient-method-opencv/) + * + */ +class VISP_EXPORT vpCircleHoughTransform +{ +public: + /** + * \brief Class that gather the algorithm parameters. + */ + class CHTransformParameters + { + private: + // // 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 + int m_sobelKernelSize; /*!< Size of the Sobel kernels used to compute the gradients. Must be an odd number.*/ + + // // Edge detection attributes + float m_cannyThresh; /*!< The threshold for the Canny operator. Only value greater than this value are marked as an edge. + A negative value makes the algorithm compute this threshold automatically.*/ + int m_edgeMapFilteringNbIter; /*!< Number of iterations of 8-neighbor connectivity filtering to apply to the edge map*/ + + // // 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.*/ + + // // Circle candidates computation attributes + float m_radiusRatioThresh; /*!< Minimum number of votes per radian a radius candidate RC_ij of a center candidate CeC_i must have in order that the circle of center CeC_i and radius RC_ij must be considered as circle candidate.*/ + float m_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. */ + + // // Circle candidates merging atttributes + float m_centerMinDist; /*!< Maximum distance between two circle candidates centers to consider merging them.*/ + float m_mergingRadiusDiffThresh; /*!< Maximum radius difference between two circle candidates to consider merging them.*/ + + friend vpCircleHoughTransform; + public: + CHTransformParameters() + : m_gaussianKernelSize(5) + , m_gaussianStdev(1.) + , m_sobelKernelSize(3) + , m_cannyThresh(-1) + , m_edgeMapFilteringNbIter(1) + , 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.) + , m_radiusRatioThresh(2.) + , m_circlePerfectness(0.9) + , m_centerMinDist(15.) + , m_mergingRadiusDiffThresh(1.5 * (float)m_centerMinDist) + { + + } + + /** + * \brief Construct a new CHTransformParameters object. + * + * \param[in] gaussianKernelSize Size of the Gaussian filter kernel used to smooth the input image. Must be an odd number. + * \param[in] gaussianStdev Standard deviation of the Gaussian filter. + * \param[in] sobelKernelSize Size of the Sobel kernels used to compute the gradients. Must be an odd number. + * \param[in] cannyThresh The threshold for the Canny operator. Only value greater than this value are marked as an edge. + A negative value makes the algorithm compute this threshold automatically. + * \param[in] edgeMapFilterNbIter Number of 8-neighbor connectivity filtering iterations to apply to the edge map. + * \param[in] centerXlimits Minimum and maximum position on the horizontal axis of the center of the circle we want to detect. + * \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] centerThresh Minimum number of votes a point must exceed to be considered as center candidate. + * \param[in] radiusThreshRatio Minimum number of votes per radian a radius candidate RC_ij of a center candidate CeC_i must have in order that the circle of center CeC_i and radius RC_ij must be considered as 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. + */ + CHTransformParameters( + const int &gaussianKernelSize + , const float &gaussianStdev + , const int &sobelKernelSize + , const float &cannyThresh + , const int &edgeMapFilterNbIter + , const std::pair ¢erXlimits + , const std::pair ¢erYlimits + , const unsigned int &minRadius + , const unsigned int &maxRadius + , const int &dilatationNbIter + , const float ¢erThresh + , const float &radiusThreshRatio + , const float &circlePerfectness + , const float ¢erMinDistThresh + , const float &mergingRadiusDiffThresh + ) + : m_gaussianKernelSize(gaussianKernelSize) + , m_gaussianStdev(gaussianStdev) + , m_sobelKernelSize(sobelKernelSize) + , m_cannyThresh(cannyThresh) + , m_edgeMapFilteringNbIter(edgeMapFilterNbIter) + , m_centerXlimits(centerXlimits) + , m_centerYlimits(centerYlimits) + , m_minRadius(std::min(minRadius, maxRadius)) + , m_maxRadius(std::max(minRadius, maxRadius)) + , m_dilatationNbIter(dilatationNbIter) + , m_centerThresh(centerThresh) + , m_radiusRatioThresh(radiusThreshRatio) + , m_circlePerfectness(circlePerfectness) + , m_centerMinDist(centerMinDistThresh) + , m_mergingRadiusDiffThresh(mergingRadiusDiffThresh) + { + + } + + std::string toString() const + { + std::string txt("Hough Circle Transform Configuration:\n"); + txt += "\tGaussian filter kernel size = " + std::to_string(m_gaussianKernelSize) + "\n"; + txt += "\tGaussian filter standard deviation = " + std::to_string(m_gaussianStdev) + "\n"; + txt += "\tSobel filter kernel size = " + std::to_string(m_sobelKernelSize) + "\n"; + txt += "\tCanny edge filter threshold = " + std::to_string(m_cannyThresh) + "\n"; + txt += "\tEdge map 8-neighbor connectivity filtering number of iterations = " + std::to_string(m_edgeMapFilteringNbIter) + "\n"; + 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 += "\tRadius votes per radian threshold = " + std::to_string(m_radiusRatioThresh) + "\n"; + txt += "\tCircle perfectness threshold = " + std::to_string(m_circlePerfectness) + "\n"; + txt += "\tCenters minimum distance = " + std::to_string(m_centerMinDist) + "\n"; + txt += "\tRadius difference merging threshold = " + std::to_string(m_mergingRadiusDiffThresh) + "\n"; + return txt; + } + + // // Configuration from files +#ifdef VISP_HAVE_NLOHMANN_JSON + /** + * \brief Create a new CHTransformParameters from a JSON file. + * + * \param[in] jsonFile The path towards the JSON file. + * \return CHTransformParameters The corresponding CHTransformParameters object. + */ + inline static CHTransformParameters createFromJSON(const std::string &jsonFile) + { + std::ifstream file(jsonFile); + if (!file.good()) { + std::stringstream ss; + ss << "Problem opening file " << jsonFile << ". Make sure it exists and is readable" << std::endl; + throw vpException(vpException::ioError, ss.str()); + } + json j; + try { + j = json::parse(file); + } + catch (json::parse_error &e) { + std::stringstream msg; + msg << "Could not parse JSON file : \n"; + + msg << e.what() << std::endl; + msg << "Byte position of error: " << e.byte; + throw vpException(vpException::ioError, msg.str()); + } + CHTransformParameters params = j; // Call from_json(const json& j, vpDetectorDNN& *this) to read json + file.close(); + return params; + } + + /** + * \brief Save the configuration of the detector in a JSON file + * described by the path \b jsonPath. Throw a \b vpException + * is the file cannot be created. + * + * \param[in] jsonPath The path towards the JSON output file. + */ + inline void saveConfigurationInJSON(const std::string &jsonPath) const + { + std::ofstream file(jsonPath); + const json j = *this; + file << j.dump(4); + file.close(); + } + + /** + * \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[in] j : The JSON object, resulting from the parsing of a JSON file. + * \param[out] params : The circle Hough transform parameters that will be initialized from the JSON data. + */ + inline friend void from_json(const json &j, CHTransformParameters ¶ms) + { + params.m_gaussianKernelSize = j.value("gaussianKernelSize", params.m_gaussianKernelSize); + if ((params.m_gaussianKernelSize % 2) != 1) { + throw vpException(vpException::badValue, "Gaussian Kernel size should be odd."); + } + + params.m_gaussianStdev = j.value("gaussianStdev", params.m_gaussianStdev); + if (params.m_gaussianStdev <= 0) { + throw vpException(vpException::badValue, "Standard deviation should be > 0"); + } + + params.m_sobelKernelSize = j.value("sobelKernelSize", params.m_sobelKernelSize); + if ((params.m_sobelKernelSize % 2) != 1) { + throw vpException(vpException::badValue, "Sobel Kernel size should be odd."); + } + + params.m_cannyThresh = j.value("cannyThresh", params.m_cannyThresh); + params.m_edgeMapFilteringNbIter = j.value("edgeMapFilteringNbIter", params.m_edgeMapFilteringNbIter); + + 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)); + 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_centerThresh = j.value("centerThresh", params.m_centerThresh); + if (params.m_centerThresh <= 0) { + throw vpException(vpException::badValue, "Votes thresholds for center detection must be positive."); + } + + params.m_radiusRatioThresh = j.value("radiusThreshRatio", params.m_radiusRatioThresh); + + params.m_circlePerfectness = j.value("circlePerfectnessThreshold", params.m_circlePerfectness); + + if (params.m_circlePerfectness <= 0 || params.m_circlePerfectness > 1) { + throw vpException(vpException::badValue, "Circle perfectness must be in the interval ] 0; 1]."); + } + + params.m_centerMinDist = j.value("centerMinDistance", params.m_centerMinDist); + if (params.m_centerMinDist <= 0) { + throw vpException(vpException::badValue, "Centers minimum distance threshold must be positive."); + } + + params.m_mergingRadiusDiffThresh = j.value("mergingRadiusDiffThresh", params.m_mergingRadiusDiffThresh); + if (params.m_mergingRadiusDiffThresh <= 0) { + throw vpException(vpException::badValue, "Radius difference merging threshold must be positive."); + } + } + + /** + * \brief Parse a vpCircleHoughTransform into JSON format. + * + * \param[out] j : A JSON parser object. + * \param[in] params : The circle Hough transform parameters that will be serialized in the json object. + */ + inline friend void to_json(json &j, const CHTransformParameters ¶ms) + { + std::pair radiusLimits = { params.m_minRadius, params.m_maxRadius }; + + j = json { + {"gaussianKernelSize", params.m_gaussianKernelSize}, + {"gaussianStdev", params.m_gaussianStdev}, + {"sobelKernelSize", params.m_sobelKernelSize}, + {"cannyThresh", params.m_cannyThresh}, + {"edgeMapFilteringNbIter", params.m_edgeMapFilteringNbIter}, + {"centerXlimits", params.m_centerXlimits}, + {"centerYlimits", params.m_centerYlimits}, + {"radiusLimits", radiusLimits}, + {"dilatationNbIter", params.m_dilatationNbIter}, + {"centerThresh", params.m_centerThresh}, + {"radiusThreshRatio", params.m_radiusRatioThresh}, + {"circlePerfectnessThreshold", params.m_circlePerfectness}, + {"centerMinDistance", params.m_centerMinDist}, + {"mergingRadiusDiffThresh", params.m_mergingRadiusDiffThresh} }; + } +#endif + }; + + /** + * \brief Construct a new vpCircleHoughTransform object with default parameters. + */ + vpCircleHoughTransform(); + + /** + * \brief Construct a new vpCircleHoughTransform object + * from a \b CHTransformParameters object. + * \param[in] algoParams The parameters of the Circle Hough Transform. + */ + vpCircleHoughTransform(const CHTransformParameters &algoParams); + + /** + * \brief Destroy the vp Circle Hough Transform object + */ + virtual ~vpCircleHoughTransform(); + + // // Detection methods + +#ifdef HAVE_OPENCV_CORE + /** + * \brief Perform Circle Hough Transform to detect the circles in an OpenCV image. + * + * \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); +#endif + + /** + * \brief Convert the input image in a gray-scale image and then + * perform Circle Hough Transform to detect the circles in it + * + * \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); + + /** + * \brief Perform Circle Hough Transform to detect the circles in a gray-scale image + * + * \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); + + /** + * \brief Perform Circle Hough Transform to detect the circles in in a gray-scale image. + * Get only the \b nbCircles circles having the greatest number of votes. + * + * \param[in] I The input gray scale image. + * \param[in] nbCircles The number of circles we want to get. If negative, all the circles will be + * returned, sorted such as result[0] has the highest number of votes and result[end -1] the lowest. + * \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); + + // // Configuration from files +#ifdef VISP_HAVE_NLOHMANN_JSON + /** + * \brief Construct a new vpCircleHoughTransform object configured according to + * 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. + */ + vpCircleHoughTransform(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 Save the configuration of the detector in a JSON file + * described by the path \b jsonPath. Throw a \b vpException + * is the file cannot be created. + * + * \param[in] jsonPath The path towards the JSON output file. + */ + 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, + * the default value defined in the constructor is kept + * + * \param[in] j The JSON object, resulting from the parsing of a JSON file. + * \param[out] detector The detector, that will be initialized from the JSON data. + */ + inline friend void from_json(const json &j, vpCircleHoughTransform &detector) + { + detector.m_algoParams = j; + } + + /** + * \brief Parse a vpCircleHoughTransform into JSON format. + * + * \param[out] j A JSON parser object. + * \param[in] detector The vpCircleHoughTransform that must be parsed into JSON format. + */ + inline friend void to_json(json &j, const vpCircleHoughTransform &detector) + { + j = detector.m_algoParams; + } +#endif + + // // Setters + /** + * \brief Initialize all the algorithm parameters. + * + * \param[in] algoParams The algorithm parameters. + */ + void init(const CHTransformParameters &algoParams); + + /** + * \brief Set the parameters of the Gaussian filter, that computes the + * gradients of the image. + * + * \param[in] kernelSize The size of the Gaussian kernel. Must be an odd value. + * \param[in] stdev The standard deviation of the Gaussian function. + */ + inline void setGaussianParameters(const int &kernelSize, const float &stdev) + { + m_algoParams.m_gaussianKernelSize = kernelSize; + m_algoParams.m_gaussianStdev = stdev; + + if ((m_algoParams.m_gaussianKernelSize % 2) != 1) { + throw vpException(vpException::badValue, "Gaussian Kernel size should be odd."); + } + + if (m_algoParams.m_gaussianStdev <= 0) { + throw vpException(vpException::badValue, "Standard deviation should be > 0"); + } + + initGaussianFilters(); + } + + /*! + * Set the threshold for the Canny operator. + * Only value greater than this value are marked as an edge. + * If negative, the threshold is automatically computed. + * \param[in] canny_threshold : Canny filter upper threshold. When set to -1 (default), compute + * automatically this threshold. + */ + inline void setCannyThreshold(const float &canny_threshold) + { + m_algoParams.m_cannyThresh = canny_threshold; + } + + /*! + * Set circles center min distance. + * Change this value to detect circles with different distances to each other. + * + * \param[in] center_min_dist : Center min distance in pixels. + */ + inline void setCircleCenterMinDist(const float ¢er_min_dist) + { + m_algoParams.m_centerMinDist = center_min_dist; + + if (m_algoParams.m_centerMinDist <= 0) { + throw vpException(vpException::badValue, "Circles center min distance must be positive."); + } + } + + /*! + * Set circles center min and max location in the image. + * If one value is equal to \b std::numeric_limits::min or + * \b std::numeric_limits::max(), the algorithm will set it + * either to -maxRadius or +maxRadius depending on if + * it is the lower or upper limit that is missing. + * + * \param[in] center_min_x : Center min location on the horizontal axis, expressed in pixels. + * \param[in] center_max_x : Center max location on the horizontal axis, expressed in pixels. + * \param[in] center_min_y : Center min location on the vertical axis, expressed in pixels. + * \param[in] center_max_y : Center max location on the vertical axis, expressed in pixels. + */ + void setCircleCenterBoundingBox(const int ¢er_min_x, const int ¢er_max_x, + const int ¢er_min_y, const int ¢er_max_y) + { + m_algoParams.m_centerXlimits.first = center_min_x; + m_algoParams.m_centerXlimits.second = center_max_x; + m_algoParams.m_centerYlimits.first = center_min_y; + m_algoParams.m_centerYlimits.second = center_max_y; + } + + /*! + * Set circles min radius. + * \param[in] circle_min_radius : Min radius in pixels. + */ + inline void setCircleMinRadius(const float &circle_min_radius) + { + m_algoParams.m_minRadius = circle_min_radius; + } + + /*! + * Set circles max radius. + * \param[in] circle_max_radius : Max radius in pixels. + */ + inline void setCircleMaxRadius(const float &circle_max_radius) + { + m_algoParams.m_maxRadius = circle_max_radius; + } + + /*! + * Set circles perfectness. 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] circle_perfectness : Circle perfectness. Value between 0 and 1. A perfect circle has value 1. + */ + void setCirclePerfectness(const float &circle_perfectness) + { + m_algoParams.m_circlePerfectness = circle_perfectness; + if (m_algoParams.m_circlePerfectness <= 0 || m_algoParams.m_circlePerfectness > 1) { + throw vpException(vpException::badValue, "Circle perfectness must be in the interval ] 0; 1]."); + } + } + + /** + * \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] centerThresh Minimum number of votes a point must exceed to be considered as center candidate. + */ + inline void setCenterComputationParameters(const int &dilatationRepet, const float ¢erThresh) + { + m_algoParams.m_dilatationNbIter = dilatationRepet; + m_algoParams.m_centerThresh = centerThresh; + + if (m_algoParams.m_centerThresh <= 0) { + throw vpException(vpException::badValue, "Votes thresholds for center detection must be positive."); + } + } + + /** + * \brief Set the parameters of the computation of the circle radius candidates. + * + * \param[in] radiusRatioThresh Minimum number of votes per radian a radius candidate RC_ij of a center candidate CeC_i must have in order that the circle of center CeC_i and radius RC_ij must be considered as circle candidate. + */ + inline void setRadiusRatioThreshold(const float &radiusRatioThresh) + { + m_algoParams.m_radiusRatioThresh = radiusRatioThresh; + + if (m_algoParams.m_radiusRatioThresh <= 0) { + throw vpException(vpException::badValue, "Radius ratio threshold must be > 0."); + } + } + + /** + * \brief Set the radius merging threshold used during the merging step in order + * to merge the circles that are similar. + * + * \param[in] radiusDifferenceThresh Maximum radius difference between two circle candidates to consider merging them. + */ + inline void setRadiusMergingThresholds(const float &radiusDifferenceThresh) + { + m_algoParams.m_mergingRadiusDiffThresh = radiusDifferenceThresh; + + if (m_algoParams.m_mergingRadiusDiffThresh <= 0) { + throw vpException(vpException::badValue, "Radius difference merging threshold must be positive."); + } + } + + // // Getters + + /** + * \brief Get the list of Center Candidates, stored as pair + * + * \return std::vector> The list of Center Candidates, stored as pair + */ + inline std::vector> getCenterCandidatesList() + { + return m_centerCandidatesList; + } + + /** + * \brief Get the number of votes of each Center Candidates. + * + * \return std::vector The number of votes of each Center Candidates, ordered in the same way than \b m_centerCandidatesList. + */ + inline std::vector getCenterCandidatesVotes() + { + return m_centerVotes; + } + + /** + * \brief Get the Circle Candidates before merging step. + * + * \return std::vector The list of circle candidates + * that were obtained before the merging step. + */ + inline std::vector getCircleCandidates() + { + return m_circleCandidates; + } + + /** + * \brief Get the votes accumulator of the Circle Candidates. + * + * \return std::vector The votes accumulator. + */ + inline std::vector getCircleCandidatesVotes() + { + return m_circleCandidatesVotes; + } + + /** + * \brief Get the gradient along the horizontal axis of the image. + * + * \return vpImage The gradient along the horizontal axis of the image. + */ + inline vpImage getGradientX() + { + return m_dIx; + } + + /** + * \brief Get the gradient along the vertical axis of the image. + * + * \return vpImage The gradient along the vertical axis of the image. + */ + inline vpImage getGradientY() + { + return m_dIy; + } + + /** + * \brief Get the Edge Map computed thanks to the Canny edge filter. + * + * \return vpImage The edge map computed during the edge detection step. + */ + inline vpImage getEdgeMap() + { + return m_edgeMap; + } + + /*! + * Get internal Canny filter upper threshold. When value is equal to -1 (default), it means that the threshold is computed + * automatically. + */ + inline float getCannyThreshold() const + { + return m_algoParams.m_cannyThresh; + } + + /*! + * Get circles center min distance in pixels. + */ + inline float getCircleCenterMinDist() const + { + return m_algoParams.m_centerMinDist; + } + + /*! + * Get circles min radius in pixels. + */ + inline float getCircleMinRadius() const + { + return m_algoParams.m_minRadius; + } + + /*! + * Get circles max radius in pixels. + */ + inline float getCircleMaxRadius() const + { + return m_algoParams.m_maxRadius; + } + + // // Debug methods + std::string toString() const; + + friend VISP_EXPORT std::ostream &operator<<(std::ostream &os, const vpCircleHoughTransform &detector); + +private: + /** + * \brief Initialize the Gaussian filters used to blur the image and + * compute the gradient images. + */ + void initGaussianFilters(); + + /** + * \brief Perform Gaussian smoothing on the input image to reduce the noise + * that would perturbate the edge detection. + * Then, compute the x-gradient and y-gradient of the input images. + * + * \param[in] I The input gray scale image. + */ + void computeGradientsAfterGaussianSmoothing(const vpImage &I); + + /** + * \brief Perform edge detection based on the computed gradients. + * Stores the edge points and the edge points connectivity. + * + * \param[in] I The input gray scale image. + */ + void edgeDetection(const vpImage &I); + + /** + * \brief Filter the edge map in order to remove isolated edge points. + */ + 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(); + + /** + * \brief For each center candidate CeC_i, do: + * - For each edge point EP_j, compute the distance d_ij = distance(CeC_i; EP_j) + * - Determine to which radius candidate bin RCB_k the distance d_ij belongs to + * - Increment the radius candidate accumulator accum_rc[CeC_i][RCB_k] + * - 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(); + + /** + * \brief For each circle candidate CiC_i do: + * - For each other circle candidate CiC_j do: + * +- 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 + */ + void mergeCircleCandidates(); + + + CHTransformParameters m_algoParams; /*!< Attributes containing all the algorithm parameters.*/ + // // Gaussian smoothing attributes + vpArray2D m_fg; + vpArray2D m_fgDg; + vpImage m_Ifilt; /*!< Filtered version of the input image, after having used a Gaussian filter.*/ + + // // Gradient computation attributes + vpImage m_dIx; /*!< Gradient along the x-axis of the input image.*/ + vpImage m_dIy; /*!< Gradient along the y-axis of the input image.*/ + + // // Edge detection attributes + vpImage m_edgeMap; /*!< Edge map resulting from the edge detection algorithm.*/ + + // // 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_centerVotes; /*!< Number of votes for the center candidates that are kept.*/ + + // // Circle candidates computation attributes + std::vector m_circleCandidates; /*!< List of the candidate circles.*/ + std::vector m_circleCandidatesVotes; /*!< Number of votes for the candidate circles.*/ + + // // Circle candidates merging atttributes + std::vector m_finalCircles; /*!< List of the final circles, i.e. the ones resulting from the merge of the circle candidates.*/ + std::vector m_finalCircleVotes; /*!< Number of votes for the candidate circles.*/ +}; +#endif diff --git a/modules/imgproc/src/vpCircleHoughTransform.cpp b/modules/imgproc/src/vpCircleHoughTransform.cpp new file mode 100644 index 0000000000..8bcb7778e6 --- /dev/null +++ b/modules/imgproc/src/vpCircleHoughTransform.cpp @@ -0,0 +1,538 @@ +#include +#include +#include + +#include + +vpCircleHoughTransform::vpCircleHoughTransform() + : m_algoParams() +{ + initGaussianFilters(); +} + +vpCircleHoughTransform::vpCircleHoughTransform(const CHTransformParameters &algoParams) + : m_algoParams(algoParams) +{ + initGaussianFilters(); +} + +void +vpCircleHoughTransform::init(const CHTransformParameters &algoParams) +{ + m_algoParams = algoParams; + initGaussianFilters(); +} + +vpCircleHoughTransform::~vpCircleHoughTransform() +{ } + +#ifdef VISP_HAVE_NLOHMANN_JSON +vpCircleHoughTransform::vpCircleHoughTransform(const std::string &jsonPath) +{ + initFromJSON(jsonPath); +} + +void +vpCircleHoughTransform::initFromJSON(const std::string &jsonPath) +{ + std::ifstream file(jsonPath); + if (!file.good()) { + std::stringstream ss; + ss << "Problem opening file " << jsonPath << ". Make sure it exists and is readable" << std::endl; + throw vpException(vpException::ioError, ss.str()); + } + json j; + try { + j = json::parse(file); + } + catch (json::parse_error &e) { + std::stringstream msg; + msg << "Could not parse JSON file : \n"; + + msg << e.what() << std::endl; + msg << "Byte position of error: " << e.byte; + throw vpException(vpException::ioError, msg.str()); + } + m_algoParams = j; // Call from_json(const json& j, vpDetectorDNN& *this) to read json + file.close(); + initGaussianFilters(); +} + +void +vpCircleHoughTransform::saveConfigurationInJSON(const std::string &jsonPath) const +{ + m_algoParams.saveConfigurationInJSON(jsonPath); +} +#endif + +void +vpCircleHoughTransform::initGaussianFilters() +{ + m_fg.resize(1, (m_algoParams.m_gaussianKernelSize + 1)/2); + vpImageFilter::getGaussianKernel(m_fg.data, m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev, false); + m_fgDg.resize(1, (m_algoParams.m_gaussianKernelSize + 1)/2); + vpImageFilter::getGaussianDerivativeKernel(m_fgDg.data, m_algoParams.m_gaussianKernelSize, m_algoParams.m_gaussianStdev, false); +} + +std::vector +vpCircleHoughTransform::detect(const vpImage &I) +{ + vpImage I_gray; + vpImageConvert::convert(I, I_gray); + return detect(I_gray); +} + +#ifdef HAVE_OPENCV_CORE +std::vector +vpCircleHoughTransform::detect(const cv::Mat &cv_I) +{ + vpImage I_gray; + vpImageConvert::convert(cv_I, I_gray); + return detect(I_gray); +} +#endif + +std::vector +vpCircleHoughTransform::detect(const vpImage &I, const int &nbCircles) +{ + std::vector detections = detect(I); + size_t nbDetections = detections.size(); + std::vector bestCircles; + std::vector> detectionsWithVotes; + for (size_t i = 0; i < nbDetections; i++) { + std::pair detectionWithVote(detections[i], m_finalCircleVotes[i]); + detectionsWithVotes.push_back(detectionWithVote); + } + + bool (*hasMoreVotes)(std::pair, std::pair) + = [](std::pair a, std::pair b) { + // We divide the number of votes by the radius to avoid to favour big circles + return (a.second / a.first.getRadius() > b.second / b.first.getRadius()); + }; + + std::sort(detectionsWithVotes.begin(), detectionsWithVotes.end(), hasMoreVotes); + + size_t limitMin; + if (nbCircles < 0) { + limitMin = nbDetections; + } + else { + limitMin = std::min(nbDetections, (size_t)nbCircles); + } + for (size_t i = 0; i < limitMin; i++) { + bestCircles.push_back(detectionsWithVotes[i].first); + } + + return bestCircles; +} + +std::vector +vpCircleHoughTransform::detect(const vpImage &I) +{ + // Cleaning results of potential previous detection + m_centerCandidatesList.clear(); + m_centerVotes.clear(); + m_edgePointsList.clear(); + m_circleCandidates.clear(); + m_circleCandidatesVotes.clear(); + m_finalCircles.clear(); + m_finalCircleVotes.clear(); + + // 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); + + // Using the gradients, it is now possible to perform edge detection + // We rely on the Canny edge detector + // It will also give us the connected edged points + edgeDetection(I); + + // From the edge map and gradient information, it is possible to compute + // the center point candidates + computeCenterCandidates(); + + // From the edge map and center point candidates, we can compute candidate + // circles. These candidate circles are circles whose center belong to + // the center point candidates and whose radius is a "radius bin" that got + // enough votes by computing the distance between each point of the edge map + // and the center point candidate + computeCircleCandidates(); + + // Finally, we perform a merging operation that permits to merge circles + // respecting similarity criteria (distance between centers and similar radius) + mergeCircleCandidates(); + + return m_finalCircles; +} + +void +vpCircleHoughTransform::computeGradientsAfterGaussianSmoothing(const vpImage &I) +{ + vpImageFilter::getGradXGauss2D(I, + m_dIx, + m_fg.data, + m_fgDg.data, + m_algoParams.m_gaussianKernelSize + ); + vpImageFilter::getGradYGauss2D(I, + m_dIy, + m_fg.data, + m_fgDg.data, + m_algoParams.m_gaussianKernelSize + ); +} + +void +vpCircleHoughTransform::edgeDetection(const vpImage &I) +{ + int cannyThresh = m_algoParams.m_cannyThresh; + // Apply the Canny edge operator to compute the edge map + // The canny method performs Gaussian blur and gradient computation + vpImageFilter::canny(I, m_edgeMap, m_algoParams.m_gaussianKernelSize, cannyThresh, m_algoParams.m_sobelKernelSize); + + for (int i = 0; i < m_algoParams.m_edgeMapFilteringNbIter; i++) { + filterEdgeMap(); + } +} + +void +vpCircleHoughTransform::filterEdgeMap() +{ + vpImage J = m_edgeMap; + + for (unsigned int i = 1; i < J.getHeight() - 1; i++) { + for (unsigned int j = 1; j < J.getWidth() - 1; j++) { + if (J[i][j] == 255) { + // Consider 8 neighbors + int topLeftPixel = (int)J[i - 1][j - 1]; + int topPixel = (int)J[i - 1][j]; + int topRightPixel = (int)J[i - 1][j + 1]; + int botLeftPixel = (int)J[i + 1][j - 1]; + int bottomPixel = (int)J[i + 1][j]; + int botRightPixel = (int)J[i + 1][j + 1]; + int leftPixel = (int)J[i][j - 1]; + int rightPixel = (int)J[i][j + 1]; + if ((topLeftPixel + topPixel + topRightPixel + + botLeftPixel + bottomPixel + botRightPixel + + leftPixel + rightPixel + ) >= 2 * 255) { + // At least 2 of the 8-neighbor points are also an edge point + // so we keep the edge point + m_edgeMap[i][j] = 255; + } + else { + // The edge point is isolated => we erase it + m_edgeMap[i][j] = 0; + } + } + } + } +} + +void +vpCircleHoughTransform::computeCenterCandidates() +{ + // For each edge point EP_i, check the image gradient at EP_i + // Then, for each image point in the direction of the gradient, + // increment the accumulator + // We can perform bilinear interpolation in order not to vote for a "line" of + // points, but for an "area" of points + unsigned int nbRows = m_edgeMap.getRows(); + unsigned int nbCols = m_edgeMap.getCols(); + + m_algoParams.m_maxRadius = std::min(m_algoParams.m_maxRadius, std::min(nbCols, nbRows)); + + // Computing the minimum and maximum horizontal position of the center candidates + // The miminum horizontal position of the center is at worst -maxRadius outside the image + // The maxinum horizontal position of the center is at worst +maxRadiusoutside the image + // The width of the accumulator is the difference between the max and the min + int minimumXposition = std::max(m_algoParams.m_centerXlimits.first, -1 * (int)m_algoParams.m_maxRadius); + int maximumXposition = std::min(m_algoParams.m_centerXlimits.second, (int)(m_algoParams.m_maxRadius + nbCols)); + minimumXposition = std::min(minimumXposition, maximumXposition - 1); + float minimumXpositionDouble = minimumXposition; + int offsetX = minimumXposition; + int accumulatorWidth = maximumXposition - minimumXposition + 1; + if (accumulatorWidth <= 0) { + throw(vpException(vpException::dimensionError, "[vpCircleHoughTransform::computeCenterCandidates] Accumulator width <= 0!")); + } + + // Computing the minimum and maximum vertical position of the center candidates + // 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)); + minimumYposition = std::min(minimumYposition, maximumYposition - 1); + float minimumYpositionDouble = minimumYposition; + int offsetY = minimumYposition; + int accumulatorHeight = maximumYposition - minimumYposition + 1; + if (accumulatorHeight <= 0) { + throw(vpException(vpException::dimensionError, "[vpCircleHoughTransform::computeCenterCandidates] Accumulator height <= 0!")); + } + + vpImage centersAccum(accumulatorHeight, accumulatorWidth + 1, 0.); /*!< Matrix that contains the votes for the center candidates.*/ + + 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; + + int int_minRad = (int)m_algoParams.m_minRadius; + int int_maxRad = (int)m_algoParams.m_maxRadius; + + for (int k1 = 0; k1 < 2; 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; + + if (x1 < minimumXpositionDouble || y1 < minimumYpositionDouble) { + continue; // If either value is lower than maxRadius, it means that the center is outside the search region. + } + + int x_low, x_high; + int y_low, y_high; + + if (x1 > 0.) { + x_low = std::floor(x1); + x_high = std::ceil(x1); + } + else { + x_low = -1 * std::ceil(-1. * x1); + x_high = -1 * std::floor(-1. * x1); + } + + if (y1 > 0.) { + y_low = std::floor(y1); + y_high = std::ceil(y1); + } + else { + y_low = -1 * std::ceil(-1. * y1); + y_high = -1 * std::floor(-1. * y1); + } + + auto updateAccumulator = + [](const float &x_orig, const float &y_orig, + const unsigned int &x, const unsigned int &y, + const int &offsetX, const int &offsetY, + const unsigned int &nbCols, const unsigned int &nbRows, + vpImage &accum, bool &hasToStop) { + if (x - offsetX >= nbCols || + y - offsetY >= nbRows + ) { + hasToStop = true; + } + else { + float dx = (x_orig - (float)x); + float dy = (y_orig - (float)y); + accum[y - offsetY][x - offsetX] += std::abs(dx) + std::abs(dy); + } + }; + + updateAccumulator(x1, y1, x_low, y_low, + offsetX, offsetY, + accumulatorWidth, accumulatorHeight, + centersAccum, hasToStopLoop + ); + + updateAccumulator(x1, y1, x_high, y_high, + offsetX, offsetY, + accumulatorWidth, accumulatorHeight, + centersAccum, hasToStopLoop + ); + } + + sx = -sx; + sy = -sy; + } + } + } + } + + // 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); + } + + // Look for the image points that correspond to the accumulator maxima + // These points will become the center candidates + // find the possible circle centers + int nbColsAccum = centersAccum.getCols(); + int nbRowsAccum = centersAccum.getRows(); + int nbVotes = -1; + 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 + && centersAccum[y][x] == centerCandidatesMaxima[y][x] + && centersAccum[y][x] > centersAccum[y][x + 1] + ) { + if (left < 0) + left = x; + nbVotes = std::max(nbVotes, (int)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); + left = -1; + nbVotes = -1; + } + } + } +} + +void +vpCircleHoughTransform::computeCircleCandidates() +{ + size_t nbCenterCandidates = m_centerCandidatesList.size(); + unsigned int nbBins = (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.*/ + 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 = m_algoParams.m_maxRadius * m_algoParams.m_maxRadius; + int circlePerfectness2 = m_algoParams.m_circlePerfectness * m_algoParams.m_circlePerfectness; + + 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); + radiusActualValueList.clear(); + radiusActualValueList.resize(nbBins, 0.); + + 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; + + if ((r2 > rmin2) && (r2 < rmax2)) { + float gx = m_dIx[edgePoint.first][edgePoint.second]; + float gy = m_dIy[edgePoint.first][edgePoint.second]; + float grad2 = gx * gx + gy * gy; + + int scalProd = rx * gx + ry * gy; + int scalProd2 = scalProd * scalProd; + if (scalProd2 >= circlePerfectness2 * r2 * grad2) { + // Look for the Radius Candidate Bin RCB_k to which d_ij is "the closest" will have an additionnal vote + float r = std::sqrt(r2); + unsigned int r_bin = std::ceil((r - m_algoParams.m_minRadius)/ m_algoParams.m_centerMinDist); + r_bin = std::min(r_bin, nbBins - 1); + radiusAccumList[r_bin]++; + radiusActualValueList[r_bin] += r; + } + } + } + + for (unsigned int idBin = 0; idBin < nbBins; idBin++) { + // If the circle of center CeC_i and radius RCB_k has enough votes, it is added to the list + // of Circle Candidates + float r_effective = radiusActualValueList[idBin] / (float)radiusAccumList[idBin]; + if ((float)radiusAccumList[idBin] / r_effective > m_algoParams.m_radiusRatioThresh) { + m_circleCandidates.push_back(vpImageCircle(vpImagePoint(centerCandidate.first, centerCandidate.second) + , r_effective + ) + ); + m_circleCandidatesVotes.push_back(radiusAccumList[idBin]); + } + } + } +} + +void +vpCircleHoughTransform::mergeCircleCandidates() +{ + // For each circle candidate CiC_i do: + std::vector circleCandidates = m_circleCandidates; + std::vector circleCandidatesVotes = m_circleCandidatesVotes; + size_t nbCandidates = m_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++) { + 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 + ); + + 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 newRadius = (cic_i.getRadius() * circleCandidatesVotes[i] + cic_j.getRadius() * circleCandidatesVotes[j]) / totalVotes; + vpImagePoint newCenter = (cic_i.getCenter() * circleCandidatesVotes[i]+ cic_j.getCenter() * circleCandidatesVotes[j]) / totalVotes; + cic_i = vpImageCircle(newCenter, newRadius); + circleCandidates[j] = circleCandidates[nbCandidates - 1]; + circleCandidatesVotes[i] = totalVotes; + circleCandidatesVotes[j] = circleCandidatesVotes[nbCandidates - 1]; + circleCandidates.pop_back(); + circleCandidatesVotes.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.push_back(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]; + vpImagePoint newCenter = (cic_i.getCenter() * circleCandidatesVotes[i]+ cic_j.getCenter() * circleCandidatesVotes[j]) / totalVotes; + float newRadius = (cic_i.getRadius() * circleCandidatesVotes[i] + cic_j.getRadius() * circleCandidatesVotes[j]) / totalVotes; + cic_i = vpImageCircle(newCenter, newRadius); + m_finalCircles[j] = m_finalCircles[nbCandidates - 1]; + circleCandidatesVotes[i] = totalVotes; + circleCandidatesVotes[j] = circleCandidatesVotes[nbCandidates - 1]; + m_finalCircles.pop_back(); + circleCandidatesVotes.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; +} + +std::string +vpCircleHoughTransform::toString() const +{ + return m_algoParams.toString(); +} + +std::ostream &operator<<(std::ostream &os, const vpCircleHoughTransform &detector) +{ + os << detector.toString(); + return os; +} diff --git a/modules/tracker/me/src/moving-edges/vpMeNurbs.cpp b/modules/tracker/me/src/moving-edges/vpMeNurbs.cpp index bf6c78cc7e..f7cc1bf39d 100644 --- a/modules/tracker/me/src/moving-edges/vpMeNurbs.cpp +++ b/modules/tracker/me/src/moving-edges/vpMeNurbs.cpp @@ -299,7 +299,7 @@ void vpMeNurbs::sample(const vpImage &I, bool doNotTrack) vpImagePoint pt_1(-rows, -cols); while (u <= 1.0) { if (pt != NULL) - delete [] pt; + delete[] pt; pt = nurbs.computeCurveDersPoint(u, 1); double delta = computeDelta(pt[1].get_i(), pt[1].get_j()); @@ -316,7 +316,7 @@ void vpMeNurbs::sample(const vpImage &I, bool doNotTrack) u = u + step; } if (pt != NULL) - delete [] pt; + delete[] pt; } /*! @@ -364,7 +364,7 @@ void vpMeNurbs::updateDelta() u -= step; if (der != NULL) - delete [] der; + delete[] der; der = nurbs.computeCurveDersPoint(u, 1); // vpImagePoint toto(der[0].get_i(),der[0].get_j()); // vpDisplay::displayCross(I,toto,4,vpColor::red); @@ -376,7 +376,7 @@ void vpMeNurbs::updateDelta() d_1 = 1.5e6; } if (der != NULL) - delete [] der; + delete[] der; } /*! @@ -492,8 +492,8 @@ void vpMeNurbs::seekExtremities(const vpImage &I) else { list.pop_front(); } - /*if(begin != NULL)*/ delete [] begin; - /*if(end != NULL) */ delete [] end; + /*if(begin != NULL)*/ delete[] begin; + /*if(end != NULL) */ delete[] end; } /*! @@ -509,7 +509,6 @@ void vpMeNurbs::seekExtremities(const vpImage &I) */ void vpMeNurbs::seekExtremitiesCanny(const vpImage &I) { -#if defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) vpMeSite pt = list.front(); vpImagePoint firstPoint(pt.ifloat, pt.jfloat); pt = list.back(); @@ -635,7 +634,7 @@ void vpMeNurbs::seekExtremitiesCanny(const vpImage &I) me->setRange(memory_range); } - /* if (begin != NULL) */ delete [] begin; + /* if (begin != NULL) */ delete[] begin; beginPtFound = 0; } @@ -711,7 +710,7 @@ void vpMeNurbs::seekExtremitiesCanny(const vpImage &I) // list.end(); vpMeSite s; - for (std::list::iterator it=list.begin(); it!=list.end(); ++it) { + for (std::list::iterator it = list.begin(); it!=list.end(); ++it) { s = *it; vpImagePoint iP(s.ifloat, s.jfloat); if (inRectangle(iP, rect)) { @@ -765,13 +764,9 @@ void vpMeNurbs::seekExtremitiesCanny(const vpImage &I) me->setRange(memory_range); } - /* if (end != NULL) */ delete [] end; + /* if (end != NULL) */ delete[] end; endPtFound = 0; } -#else - (void)I; - throw(vpException(vpException::fatalError, "To use the canny detection, OpenCV has to be installed.")); -#endif } /*! @@ -858,7 +853,7 @@ void vpMeNurbs::localReSample(const vpImage &I) while (vpImagePoint::sqrDistance(iP[0], iPend) > vpMath::sqr(me->getSampleStep()) && u < uend) { u += 0.01; /*if (iP!=NULL)*/ { - delete [] iP; + delete[] iP; iP = NULL; } iP = nurbs.computeCurveDersPoint(u, 1); @@ -876,7 +871,7 @@ void vpMeNurbs::localReSample(const vpImage &I) } } /*if (iP!=NULL)*/ { - delete [] iP; + delete[] iP; iP = NULL; } } diff --git a/tutorial/CMakeLists.txt b/tutorial/CMakeLists.txt index 1a048c2bab..5b811f2ba6 100644 --- a/tutorial/CMakeLists.txt +++ b/tutorial/CMakeLists.txt @@ -44,6 +44,7 @@ visp_add_subdirectory(imgproc/contour REQUIRED_DEPS visp_co visp_add_subdirectory(imgproc/contrast-sharpening REQUIRED_DEPS visp_core visp_io visp_gui visp_imgproc) visp_add_subdirectory(imgproc/count-coins REQUIRED_DEPS visp_core visp_io visp_gui visp_imgproc) visp_add_subdirectory(imgproc/flood-fill REQUIRED_DEPS visp_core visp_io visp_gui visp_imgproc) +visp_add_subdirectory(imgproc/hough-transform REQUIRED_DEPS visp_core visp_gui visp_imgproc) visp_add_subdirectory(munkres REQUIRED_DEPS visp_core visp_gui) visp_add_subdirectory(robot/flir-ptu REQUIRED_DEPS visp_core visp_robot visp_sensor visp_vision visp_gui visp_vs visp_visual_features visp_detection) visp_add_subdirectory(robot/pioneer REQUIRED_DEPS visp_core visp_robot visp_vs visp_gui) diff --git a/tutorial/image/tutorial-image-filter.cpp b/tutorial/image/tutorial-image-filter.cpp index 5b04b64bfc..85bf396ea8 100644 --- a/tutorial/image/tutorial-image-filter.cpp +++ b/tutorial/image/tutorial-image-filter.cpp @@ -84,11 +84,9 @@ int main(int argc, char **argv) display(dIy, "Gradient dIy"); //! [Canny] -#if defined(HAVE_OPENCV_IMGPROC) vpImage C; vpImageFilter::canny(I, C, 5, -1., 3); display(C, "Canny"); -#endif //! [Canny] //! [Convolution kernel] diff --git a/tutorial/imgproc/count-coins/CMakeLists.txt b/tutorial/imgproc/count-coins/CMakeLists.txt index 1b6abe42ec..dbc971d193 100644 --- a/tutorial/imgproc/count-coins/CMakeLists.txt +++ b/tutorial/imgproc/count-coins/CMakeLists.txt @@ -1,4 +1,4 @@ -project(tutorial-) +project(tutorial-counting-coins) cmake_minimum_required(VERSION 3.0) @@ -8,8 +8,8 @@ find_package(VISP REQUIRED visp_core visp_io visp_gui visp_imgproc) set(tutorial_cpp tutorial-count-coins.cpp) -list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/coins1.pgm") -list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/coins2.pgm") +list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/coins1.jpg") +list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/coins2.jpg") foreach(cpp ${tutorial_cpp}) visp_add_target(${cpp}) diff --git a/tutorial/imgproc/count-coins/coins1.jpg b/tutorial/imgproc/count-coins/coins1.jpg new file mode 100644 index 0000000000..bf85bb2da3 Binary files /dev/null and b/tutorial/imgproc/count-coins/coins1.jpg differ diff --git a/tutorial/imgproc/count-coins/coins1.pgm b/tutorial/imgproc/count-coins/coins1.pgm deleted file mode 100644 index 39dd7449d6..0000000000 --- a/tutorial/imgproc/count-coins/coins1.pgm +++ /dev/null @@ -1,5 +0,0 @@ -P5 -# CREATOR: GIMP PNM Filter Version 1.1 -320 240 -255 -¾þÿļľ¾¿½¿þ»¿ÿý¼¿ÿ½Ƽ¹solifklrxÿʽq]VSJLJFFJKFKJSS\Vj{ƿŻľ¼ƺtaNMQSLQNONNOMLNNNNNRRPQTĿ¸ƿbJIJONLONNMLKMKMNMMLLNNNNOMLpô¿żymjnbgiljkmiruv`IMOOMLMNPOMNLLMMMLLKLMLNLMPPPLǻxqh_hZ_`_\^^a``bafijkniip;nLLMNNOQNNNKMMLLIMLMLLMMKLLOOMONLzɾf`ccac_]XXVRWUQU[Z[[[W_aefihdaдlONOLNNNONQOKNNMMNJMMMNJLLNNOMNMPPvÿʿql]`]_ZWYVVSSOOKNQOMMPQTVX\[b_bekaYÐHNOMLNLNOOKLMNMKNNNLMMMNLLLMNKKKRWzþ¾¾ǵvkac`^XRORRQONMKLMMQPOPPOPQOOTV]aeeii^oտTSQMPOOQOOOPNNONNMOONNMMPLNONOPPU[¼ҿscdb_]UTQNLOOMKNMIIKQOQQOOLNLQRPTY_bcff]jɻsYMRPPQPQPOOONNMOMLONNOQONNPOQPSS_m¿¿ηy[ca]ZXTRMKNLNOJLKMJMKJOMMMONQQRPSQW`bee``kɷ}`WSSUWXORKRPORONOONRUY[XUTRQTUXUp|½̴{Yaf`YZVPUPPNQONPOOKLMKMMPQSRRRSQQRQS[_bccabpDZiZYYZfg[VOPNOOPQRQSW[bc_[ZZTXYdqöspiib^_^\_]c[eensy~԰pYdc_ZWVWWXVQQQRPNR\QNNNMNOV][XYQSVUWY^eeb]ahwλ}f]Z_ea^USPNVTSQTUW\cmnidXVXfnļsrjoibY[VUSQURQQUUTTQRNMJ\]aqz͸eZddeZTRTURXYSOQRPQXVUNOQQPR[_eYVSUVY^abdbabcl{ʳvd[\bj`WSSURUVWZZ[_h^X\Yjpxxp\\^aa^WXTQRPLPQOLMOPRROPPMQPRT[X``gpyѿf^dba]VMLNNLTSRNOPQSPhURQOOQTVZXUVUZ[\_`ceaaddl{ƺl^[SSXUQYVYYY]\Wf^tz·ydSRXWRRPNQNLMNNMOOPQJNNRQQQMNMNMMKNMZcd[YbjwϷ}[cbd`[UKNMNNLONJLMMPLLJOOORXPQSTRWY[^`dc_dedghnvǼ{{uuotmj{xuǸ}dUPRROLRMLMLLMMOPNONPOORRQNPOLOMTPQIJMNOOPQWRSLdrʩ^^bdca]QPNMKNOONMKNOMNPONLNLOQTTW[[\_dbbdcfhgjisƻǿyXNQRRQNNOMQOMMPMMKPQMNORRQSPPRQRQQPOLNMMOQRQNTRRUQNbrЫY]cdeb`^XTQQOOONMMNPPLSPPQPSTSW[[cdjhacdfiikmkv̽mITROPPOMONOPPONQPSQNOPPVRRSVSSPTSRSQQTNNPQOSSTTRQNPPOUo¿˫i[`eebcb^ZXTSRPPPNMQLSSRSWXY[`^bfioijjchknoolsĺ̸yTNRQNNOMLKMNOPOROSUTSNOQQRRPRSRTRPQPQQQPNQOPPOPMMMOMPNNPIkζ`cbbcbhe`]aYZXVUUWUXXY[\[`ehlknmnlonmpnjqqsºκqQKPPPMQNORPMORQQQSUVVSVSQQRSRRORRTTRPPPNPPPPOMNKMLQPQQNMOLRlŴvicdedde_aca^^^[[[\]\\_dbhpnqjonowqpqqpmužpeb[NKGGKEFKJWNX`k~MRQNPPOMNPRSRSPNQPSUWYVVQRTSRUTTRQRRQPPPPOQRQONMMMKOQSYTOPNO]yɳ|qjllmpomjefie_bfbadbhhilpqvvvvrmqqqnquɾxkWWTVKIHJLHIHHKJMNLLIMTQ_gʻVTQQQMPQPTQSSUTSPPOOTSVXRSTSTTRTTSRQTRRRQOPPPPQOMOLMQTOTRRRONIl˹yusqvutuzpljkkhhgieekjoossuqnnpnolfqs}Ȼx`SR[XSNHHJKKIGFGHHHHJNQOMMMLRTSZoämOPPPPPRRNRQSSRTSTMPRPQRQTQPRSQRRRQRSRSQRQPQQMPPMNNQKOUXVQOPONL`¾ÿ®mu{{|}rtrtpqooqvruyxz}smolqsusɻyRKLSQOHDFEJIIHJIJIKHHIHKQQNMLKJKQNUQNmĔ\MRQTQRQQSQRTVYWWSQPLRSUSSRRSSTQTSTTTTSRSSPQUQQOPOMKLMOPOMPMMMJayÿīx~~{|uy{wxz{yyxzy{~}xhouv̼lPILNNONKIGLLJIHIHHIIIIKGGGJOTMKILQNNKOSM\̱^LORRRRTUSTUVXXXUSRTSRVVTTSTSURTSSTTTYQSRSURTQUSPPNLNJNNPNNNMMTe|ƺx{|}|~~{|y|~|{{~lJMOKKMNMMLKJHIIIJHIGEGFHHFEHJKJJKLMNJJKJNRWWͪ^SRRRSTXSSSUVXYWYSRTTVTXVVUUVUTSSQTVSRTQRTQTSRTQPONQNQRNMSOMMLWcȼ{|~vxϾPJQNKMMMOLLLKKIKJKGHJIGJHHIGHHFIHKGIMMKKJJMOPJ`̼teUQSSSWUUTSTVWWUVTVUUSUWXXWWVSWTSRTRSURSTSRTSSQSONLMPNQNPNMMNR^k·ѵkAMOPMLHIJLMMMMLHJLJILHHHEJFJIJHIJKJIIKJHKJKKLMPO~ǴziZVSSRTTSTSSUTVUWWXZTWWXWWYYUUTUTTUSSRUSUQUOQNSPMOOQRRPQRNOOSY`{ιYHLKIJMHJHHGJKKKJJMJJJIHGIIJIIHFKIGGJIGGHJIMKMKLNErðpc[WSRRRTSSSSSUTVWXWYWVVUWWVUTXVVSVWUSTSRTPQRNPQPMOOPPMMKMQTX_jĪ`GKGGFEEHJHGHHGIGFGHIIIIHIIIIIIJJJIHJHGGHIIFGEFGHLQjòvke`YYSTRRRRWRSTTWXWUURSTYTVXXUWTPXVTTRUQQPOOPPPNRONOMMONSW\afuѻzEKJIFFFGHKHGHGIFGFFKGGILJJIJKLLIKJIJIIIJEJIHHIHGJKQmų|tldb_\[ZXXQRSUVRSVVRTRSSQSRWVTVYVUXSTRQQPSQRRQRQOMPMNQQVW^bij}ϫbIJNHGGIHIFIGHHIGIHEJJHGGGHGJLLMLLLMKJIHHIGIIMKLJJNXtɺwongadgkmi^RSRRSROQRQQQSSSRRQUTQSSURQPPRUVVXTTTSRSSSTTZ[a`inq̝MMLGIGHGIJIJKJJIJEGHGGIIIHGJIJLLLMKLLMLJKMHMNNMMMOSTųwtpqpsmpqzzg^RRQQRPOPLNPRPRRRRRRNPQPPMU[b]Z[YZUYYZYZaddglhm͆PMKGIIHGHJJKKIIEIGFGIHIIFHEFHGJHKLQMNKMLJKIMNNMMNRUkŭymqos}yok]XXRJNMNPOSMNPPRRRQURRQTYbv{yeafab]_`dedlffltžɿ׽XHMIIBFIJKKLMKJHJDGHGGGHGHGEHFFGKJIKKONPLJOKLKLQQV^uȽytopolopc]^ZWSSSPQQMPRRSWYXWZ]_dp|rhfiigggkjghimzŻѽcULOKJJKNKMLKHEGGHGFGFEFFDHGGFFIHKHGIKMNNLKMPNQTS]lŮ}soomthv~xrkkebZ[YWTVWVSUSXX^_`cdjom||spmkkikmjjpuz¿{mf\TYUWUQ`[qyҼp`TROLLLKKIIJIHGGFGEEDFDHGIGGHGIEJJJLMPNONOQOTSW]lĸrvqnslqnpnkieccb`^^a\]^dcgflops{~tplnmklkipnvļo\WKQOPPTRQURPRQORRYrҿzg\XQQOMMIJFHIGIEGFDEEEDDIIIIIEKKJJPPROOORRS[U\ak~ovrpqmonnoijjlfggbifjpoonrqkknhcgmjnmknmst~¿̼|d]STUQPQPRTSRRQPRRSSRRSMNpǮra\VTSRUQSNJIHGIGFDCFFGFGLMMOQTWXSYZWUTSRY[]`epxpyousqmlpkplnnnkpnnpqsrpnpmmppnoqsoy{¼¿iRKVSPRQPSOPQPPPSPTSSSRSRSTXMiѽhc[WYX\`\TNMJKQJGHIFGJGLMRPTW`cfbaab]\]bcdkpļysursqtuyprstrvtoutrproutpry~¾ư_RWSTRRQQPOQPPNQQPPRTSRSRSPSRSRLyҺfa\_`aaa_TQOLNMKMKKKJLRNRX]eqqlluqfcaajfxȾ}t}}{vx}xv{}zy}~¾ѾeQQVVTQRQPQPPQROPQQRSQQRSRRSRORUUTZv]X`^j`^VXSSRNOQQRRURVVWZ_hmmqkfkjdjwxȽÿĿĿλ]NRRNSRPRPNSQQPPQPQSRURSUPSRRSSRSSSRŵwdaTWZ\RYUYVVYYYZ[\^\\^aegaf]oks|úżÿÿѸrRQSSSSRPRQPSPRPQPQNQRSRUTRURTSRORTQPYûuggf]][\Z[Zccaadfdbckks{~¾¾ÿվqVQTTQQQQVSRSNQRRRRORQRSWTUSTVPSQQRQQPaϽuxrpnlkkptrwot|}ootkkidkdonkp}~ɻ~}x~yxvʭXSUTSRPRRRRRQPRQSSRQSRTSTSUUVRTUTOQQOWTô|ookkhgbfg_a]^[\`^b^_Z]]]bilv~ÿxlb][RQTYTNQXTRXS^^tŸWQWTRSRSRRQRTSSQRRSTQTRTTUTURTRSQSSQPTSkſ¾whcbabcbbcaab___]_]]^]__`\_]Z^\`__`dvþƻ{umgX\UZXXWZ[XXXYZYWYY\[\X^ivӽRYRWUSRTSRQQSROTSSRTSTSWSUTSSRRVRSTSTSYwŶ}qdf]dc`a`aaaaaab```^^a]]^\Z^^_^[[[[^]^`]hjtÿºq`\QVORZXWWXYWZWXWWWUYXZYZZVZYYR^ӸDVUVTRSSQTRRSSSQSVSSSSTTUUVSRRSTSTUSXZkõzlbccdcccd`_abaabdcca___aZ`^^Z`a]_^`[^_[^^]]bed_gx¾sb^Y]\WUZYWYXWUXZXYXXXYYZ[Z[[\YYVY\]ZѺ}WTSRSSTSSQSMQRRUTSSSUUSSTTSTRVTTTSXRZiķkYUW`_bacbaeba``^aa^dc```^_`a``___^]^]_^]_Z\]`a_]^Z__r¿ót]^_[`a`ZYSYXYVWXZ[YZZXZY[[\`[[XXUWZYX]^[յ^YXRUTUUSSQSSORSRSRSSUSUSW]WXWTWXWW_n~ɮvTJLYX_a__abccbb]`__bba_`__^_]]]^\^__]\Z^^^\]^^_^[\\X]``_y˿cY\]aabhb`a[XWUTYV[\^\ZYXXZ[Z[^_^WYVWXXUV\YxϽdYTVXWVTUTTSQQRSRRSRTTW\fkdd[ZWY[\cqƿcSRQXY^`^c``a`a^\_``a_`_^^^`_\^\\__]\[^]\]]]]^[[a^]]YYZY[c[j¿|fVa_\adcpfidaY\ZYUVYZY\[\[ZU][Z^\Z\[ZYUXXYVXYeҿqfZUZ\Y]WWTTVTRTQSTTTX[`ehmfb[[T`mɴx_b]Z\]ad`a_``^]_^\`^_aa]_c[^^Y`Z\\]]]^\[ZY\\[Z[Z]]_\[Z[ZYZ\[`_x¿õnZ_\a^abdggeod_c`[YYZY]\UWYXXZZ[ZTVVXZZ\XYYYXYV`˳ca]^^`_]ZXWWUTUUSTUWY^ho}qaZUdm~̱iecaeeebbdb`_a_a^^_aa__ca`_`c[_]Y\Y\`[\\[Z[Z\Z]\\Z\ZZZ[ZZY[[[Z]\_qĿѽj]XZ]a\^`eaa^^^`]cbc]ZYZ[VVXV\]ZYYXSVXUW]X[VZYVUY\~ɯi^X^`daZYXZWVXXVWVY[]ZZWX]jrīf\^edccd`]b_``\__b\^_bdccdc``___\a]]\[\][^[[YZY]Z[Y[\ZZZY\[[\YZ\]\_tƺnXW[ZY[[[[\ZZ[Z[\]a`hd_ZYZYYX]bega^\WUUTWTVYVWVWVVR`nDZbZ[YZ\\ZYVYYYXWZTY[abhx|ͷf\aa`aab_`a^]a`aa`]`aabaa`cb]__^^^\^^^\]^Z^Z]]\^[^YXY^Z]ZZZZ_Y\ZZ[]Y\yödTY[[YYZ[ZZY[X\YYU\\^^\[WWXXTV\efgd`^\YZY\WWWYZY[YXXbtԿsl^^Y\Y\W]Xb`efqrwιgY\a]_\_^`]_^]aaaa`aa``cccbc`bb^_]\a\^]^Z[Y[Z\\][\Z[\WWZ\\\[\]\[[_[Z[Yc~íx^\XXYZXS[XYTYZY_[[UYZZXYWTSVWVX\alrj`]\YVXXWWW[ZYZUU[coû}z}š}[`\^]]a^[Z\\_``aa`_^`a``bab_c_^_^]]\\Z]\`[_[b_\\][]ZZXZ]ZZ\]\\V^[ZZ]\XXpô{TXZYYXSTTXVXVWXY]\YXY[ZRUTXWVVUXZZ\[Y[\ZVXXYXVX[][ZZ]^guþ͸cW]^^^]]][\_]_`__a]^[Z^^a`b```_^_[`^^[][\Y[^]Z]^^\ZZY[Y\[^\\Z[[]\^Z\]YXW_ƱwX[[XXUVTRSXYWVXYY[ZYXZ[YVUTYX]RXXY[ZZZXXXWXYZ[\XZZXY^]bkx˨z\_[^___\Y[]]_a`_a__^]]^[`_\^^]^^^]Z[\Y[ZZYYZ\[]\_]_^[]\Z\\[]][\Z^]Z[YZXUZu¿ïvc\[YZZUWYYVVZ[YWZ]\[YYZ`WVVXYWZZXX[ZY[ZZYTSVZ][[[[YZ]_bchÚu\\\aaa^\\]]`aaaaa^_]`]^\^^_`]_[[XZZ]Y]YXX[YZ[[]\[__]][]\^\]Y\\]]^]`\YYXXVoɸyU`[\YYWVUWZXVY[]^]]]ZTTY[XWVYYYaXVXXY`Y_[\WUWX\^]\[_]`]balϷfV\\]^^\\]^abaadbd``_````[^]]\\]ZUVU[]YZXXXXXZ]_^^__\Z]\[_\[[Y\\][_]\ZXWVXl̾`YY[[[YYVYZZ\VY[\^]^__YVUUVWYaYWYZZYY_`]^]\YWVZ\Z\]\Z`^Z\ezȬe\\][_]^^\[_^_b_bbb``b_]]\\\^]\ZZYVY[W^YYXU\XZZ]]b\][Z\]^Y^\\Z[\[^\ZYXWUVXpĿɵi][]][Y[\Z[]^[YW[\]_`ce\XXXXUZYYYYYZ[\_a`a`_][[ZZ\^^^a\`]ZpģycYZ^]b[`]]\\^_acca^ac_`da`^^]][[ZZWZZ][WYYYYYXX\^Y`_ZZZ\[^]YZZY\^^]\XWWXX_pĿĿͳ_\\_^^[\__]]b[[X[_\^aba]ZXXYZ[^[YY\[^aabba`_\Z[\Z]_^`acYUoϹ}j_Z][]b_a^`]]]`aa___`dcb`_`_^\a]]^[][\\YVZXYZZZZ]_\]\[\^[^[]Y[Z[_^\XYYSVYavÿ¾ÿs\]^^a`[\a`__]`\ZZ]^^_^b_^]ZZ[__[\__^a`ab_^\]Z]_\bbb``^^[mʹ~nd[_[^_c`_`___]^_``aa`_`a_`a`^_]^]^]__\X^YWVZZ]^c]^\Z\^]\`[[\]\a]^[XWWUVXdy½¼̮a^[^^]]Z\a^`^[[][W\]^\_\_YYZY[Y^____]Y\^[^X`bcbccb`c^^^lDzxhbZ\^c`caaa]``^`^_`e]`ac`b^^`__aa``_][[ZYYZY\^_\^_]][[^___[^_ba]\\YVXWR]c~ɯbZ`]__[\^_a``ZY\\YZ[]__[X[ZY_\``a[a^XZ\a[ceiggefdb_Z]csǵynf^\```cb`b`^_^^\b]bba_`aa^b```c`eec_`_Z^ZZa_a`a``_``_`a`_^^^a`a]\XXVU]aoÿ¾Ʒke`_b_aZ][_a^[\^Y][Z\aW^][YZ]^_]^_`^]^afxslljhgc\bbizȺ~ymhd^a^b`baa_c]^_ababa`cbeb`bbbbdabbba`\_^`aabbccdb^_`_^_]___]a_\YYWX^aeu˼sibaa`\\][\__^Y\]]ZZ[[[`\ZY]aac`__a`c_erqkiec^`mkʺ{uoie^_`ccab`_^bacbeceac`bdedbbbbcddbccab`bbcadbcbbb`ba^_^__]`Y\ZYYUZ\ej¾̼unec]]]ZZZ[^[][[\ZYYZ^]\^^[__c^^bbfhjnwsjaYagnyѿ~|wrijeabcccddebdbcccdcdddcbfbacacab^ac__ebabe`dbdaac^`_a[\[^\ZZZXXY_civ¦zuuld_YRWYV\\]]^^\Z[\]]]_aeaeqoeeknnoiruujefdhtǴ}||yvxsnfbddbe`ecedcbddcdcdbba`aaa`acbchbaaabbbdbcb_a`_\a]Z[\ZX[\^`fio~ïw}vrof`[]^b]]Y\`]__Z`dhfefhnv{{ymjqrrooli`bjiz}x}y~uf`cecc`bbcfcaed_fdcabb_bd`aabdb_ba`dbaaaca_b_Z\[\]]\\^bafioqſʶwnvwuljkgmolikigfgdhjnmllkps~wwsupmjijioyʻsvxvf`b__a]`eegdcgabb`___``_`baa`_a]_Z^Z___``[[]]^_Y_^\`adflrÿſjnjnnrus{vuqtonnlmztsxtvpsyz|upnpngoet{ɵxpnsni]YZ^^`ffeddba^]^_`^^^a``caa`\\[Z\__`[____Z_`a____afcrƪrmqlpmsuuww{{sqsmyz{uuvsrqtiilnppmo{ŵngjwvonla\`_ebccbcdb`b_]]Z\^]_`]`^cYY]Y`^dhgifeefdfdd`_^bfp¿ÿîrlmmjlnoqvyvtwtsuuuurqqoqqqnthtȹuptvp~polhdccfccc^e`_]`__Z[\\Z^\acabaijnqpqtpmmlkfmhgfa^^dtſrokqljqsnxwqytrqtqnrstssr{ƿ̸usrnmqt|oigfdccc`da`_^a`^]_``bdeflpqstx{zzxywtuvnlligge`joĺxsnnlxvuzutwrtz|yyľ|yioplmdlnwy̿qusprmgmelhfffddcbabba\eabeeedjnptvz|}}v|uhhjhjjhhu~̿wkhde`egfjffdedgchmqvtwĿƻ|ypqlnkjjgfgifhihfdegeeddfiijotx{~vokihehdjkqxþĻyjijjjeecb```^^^b]^_a^djhjow~žvmriniihhijhhfkdgfiijniorso|w~zzunmlfjhgfgfksxƹrtpkjgbaa\``_]]]]\\^]^\`cebadagano{xryƿynnhikjlkhkghfeijjijmommlrmnkdhlikjlleiilv{¾smuwjb^Z^Z[]\^Z]ZYYTZ\\\[^_`acfd^^`_aejmqfzxtpmoplllgijmmmnlppnnmnkmmmjlmnmposw{Ǽtkgjjc_\^\[_WVVVVUUUSTTUXZXY\]^`a`b_a`^]^bbjnlk̿|q{uumnrrosrumspolsnmoqotvv~¿¿´lhnifab^_Z[WTWSVUQRQSQRPRQSTRVWUX\^`c`_]_a``acehnfvƽ{yvqv}}y|Ͼhehdffba^[YYSTRRRMRQNRQSSPQQRQRRRSTWVXXa]]__a_acd^fjhožÿÿølghdddab^\ZXWPQPNNPRQQRRPPRPQQNQRPRUTTSQRPQ\^^a`__ab]gelhλtmlgc``__\\YXTQMNONNQQSSRRURQPQOMPPQRQTSSRONPSTX\[^]`]`afifsķhciecca^Z\[Z\ZYUSMMNRQTTTQQQORQQQROMQMQSTSROQPPQV[Z`]\___dfhey¿µq_`ddda`Y]]\[]]a]XSOSQSQRRSRSRRQOONNOPRYQSUTRRQPQRPTY^]^^`acebdi˿m`fedbb_^\cdc`\ad_ZTQOQSSTQTRQTURRPOPPPRSSTVUTRNQNMPRTZ\_[`accfdbr̻e_fdg`cb]_iolhc^\YUUTQSSSSRTSSSTSPSPQLPQPNRVTSSQONNROQSV[^bdebdce_kϾ]aac_ca]YXakq|vk]XURNNSSURSSRRRSUUTRTPNONQSTTUSQONONNNQRWZ[cdbcdcaafäc`b_acb_[Y[\ppm^WTRQPSTVUSVRSRSWTWVTPQJMRPSTVSRQONNMQMOQVZ_`fdeeca_h¨gbca_\^X[WZY[edf^RSQRRMSUYXTWOMQRSTTRRQQOPQNRRSRRRQPNOOOPRVXZ]]c_ba__`İd`bab][_ZZZ\\^d^\YVQTQNPSYZXVQRQQQSRPPNQPQQPRRRTUURNOMOQOQRTW[]_a`bbb]d}ϸnaaa`_]\XY\Z]__[YVSVSSRRRSVYXTRQRQOROPPNPQRPOOQSXYWSQOMOMMPPWVXY]^_a_aZeǾ{qnhedfaXbebelpzy{{|}п]\daa__ZZZ[\]a^[WUTVQQPPPSUWWUSQQQRQUOPQNKRNPQQTY_^SQQNMOPOSSX\\\ca``]^eû}sonkc`ababafgdmlfge`X^]jv˵d]a`b_^^Z^ZY\bd`YSRSQWQQMSQU[WXTSQRTTSRRRQQTMONRSVY]URMQNOPPSUY[]`\a^^]blÿ}vph^ZTXTWUQVW[Xcgijgd^`c`\`Xbn¿­x[aadbb^[\XY[Z`]aZRRQQPQPRTWWWZYTSRQQSSTPRROOOMPPRUWWWVNNNQOPSVZZ]b^a^\`gmrqyri^VSVQPMPQQQRPOSVUTYTTNSSVX\\`bbdkqſѽg``b`e`^[\\Z]]^]a[SQRPPPRRUUXSSRWQRQOQSRPRRQRPNNQRSUXVQQONSRSWY\^`ba_]^`jwùyfakqxpi_USTOXWVSQPNNNNOOQQRSRWTSQPPPRRQRV[]aedkuͯ`_bdd`c]ZYYZ[[`d`]VOPQRONRUVZUUTTTQQSPRSRQQQOROQPOQQQSRQQRQSXY\_`]^`_[]fpzwZabbaUXRNSORSTTVSRONPOONKPNPRSPTRPONMLNNNORTZbdnhjp̭vd^bbba\_[XYYZZcgi_YRQQQOROUWXZXRNPQQQRSSSSSRQRRPOQQOQQQQRRRUY]_aad][\Zdnrǯs^XSTSORNLQRTUSUSQQOQKQMQMPNPRTQSPMLMIMLLMJOSR]`ifierħ{[]cafd`]ZZ[\Zdnrri]VSRURQPRV\^`]YTRSRSSSSUTQRRQPPPPQPQSORWSXZ_b]_]^\Yagkwĸwp`YTSUQPNNOPOORQSQOPJMNPOQOKOQPMMMLKLIKMMLMNPQOU[^dddewl]^bbb`^[\[[[bckxnufYWPQQRQPT\bde]YRRUSUSUQTPRRTRSRSTSRSTVXWZ\]__^]]]bckssųa\quc[YXWVRQRPQOQPPQRPOOSNQQONNNKNMKRLLLINJKIMPMQQROPSW]a`Wog\]^bca`]]XZY]fgnwb\YTTPPRTQU`bc_XTRRRSSUUTTRTTSTXYWZX\\\Z]^^^_\[[Zadhouѿq]Y[\b\Yda_\YXVUSRSQQSQQSMQPQPNOMNRNNNONNKLLJKMMOONRPRSQPNNOUZ__`qܿuaZbab``a^ZX[Z_bh]ac`XRUTSRQRSRUYYTSSRRTTSSSTTS\Zb]\YZT[\`daa]\_][\acinqyѾg\Z\XSUYX`a_^XWZVUUVTSSPRPTRQQONNMLNMMNMOMNRNMMNPOLSPUQRQQNPOOQY_bnæ{^V[]`dbb`\YW]\_^^dd\]XSSSRRRRSUQQTOSRRSSRXT]_efec_[[[\^agef__]Z[^`dglrofVZXWTPQSVYY][Y[VYZRTVTSSRSSRRSPPQPOONNONOONNMQQPOQPONOMNNONLPNNSdlooçi^Y]bcdbb^^\\_^]][_ba^XVSTQRQSRSTUTSQVUTZeloonjnfb_[YY^dffc`^^__ahhlps`\YWURQQQQQTUXXZUUWWYTSRSSSRRTRVRRRQNNOQQQRSQQRTTOUZUUSQPROOMPORRRb||wt°o^^[_aeee`a_Z]XZY]]ajrpZTTSRTUUSYUWVVVXZjqz{xyrtib\][[b`b`c`a]`fjfnrpzèbXVWWRRRQPQSSSWSWVWVVTVSVUWWWWVUPSTRSQQRSSSRSQZWXZ\YXZVVSPQQQPRSTUWWkuwojįwl]Y_`bfdeba^Z\ZXYX_soywc\UUWW\ZWXYZYZ\]is|~zg_b]^^bc`_`\]a_eejllqryƨcRTTSRQNQOPPSRRQSSQSSSSSRTWXYYYWYVYRUSSRRSVXXXY\[^`dfc\]WUSRTVWUWRYYVZbeieWgʵvj\\\`cdadba`_^YXZZqy~g`[\XYYYU[[Z[Y^blnwpfY_badd_d__^efefhmmspvjs˱qVVTSSRRMORRQNPQRSSQRSRRSTVVXZ^YZXXWTSUTSSVV\\]__ceqpxniaZYXXXVXXYZY[[[UYVZXWp̷}udX[[^`edbaaa`_ZXXex}ra^]YbXZ\[ZY^Z^`]cac`]`cdddf`\_kvwqrppspsquvŤ_XTTSSRQSPRQSQRQPRWTRSSUTQVYX[ZXZ[YWUWTUSWR\dhlhdgotssoic\ZXYWZYWZYUZZWXYWYXZbyªyqj^W]\a]aaabd^__]^X[]\[Z[Z[Y[[ZZZ]]\[`aa`bbdedbclwy~~}wuutsszʯhNTTSTSXXSSRTRNRRQRNQRWYUVWXYYYX[Y\ZYYYZZVV\agmkighhkpvqg`\XXZ]ZZ[WW\\Y[YWUUXXUkŲzsmh_[Y^\bedab^bcabbab\ZZ\\^^]^]\]\_bcbbbdd[giy}vuvsnz־y\UUTRSWVUVVVRPQQRSSTSTPXWWWYZYXZZ[Y[^[[\YYX_`giifd_ejtqha[][WUXWXXTSYYSWUWSVWVR^xϽ|rph`^^\[dcdbbdedbdea_^a_abbb_^^`adcbdcdgswtrssz̵iTTOMSTUWXVVSRSSRRWWXXXYVXYYZ_^_\[XX\[_][ZYZ]_fjoi\YZVSVWZYYYWRUUTTTWTUVVUTSQTTYUkʵzzyvssdda__b`c_bceedc``a`dcbb^b__bdfks}ԭysuuz̬XQQQNNRUXYUUTSUUUYZZYZ__`\[[bdcb`^ZXXXY][ZZXUW\VXWYUXUTTUWXXYVPSTSTRRRPQUTVRQSSTUdzĭzgjhc^_`badb````_`a`a^aZZaekq|ƶ~zwpvrvXSOOQOQRVTTTTTVUT[Z]_kefd`[aa`_c_YYXUVVUTUVUTTSSTXXVVVSTUZ\XZVSRQQRRQTQQQRSSQSTPOas­x}}zrncc_]b__`a`a]`_a`abdddksrı||}{wyѻiYRQOMPPSRTSTUUWTW\\adjhfhb^b]\ca_YWXRQMPPRRTSTRRSQYWTXUSVXXY[XUQRQQQNQPQRRTSSSUTS\r{{{tzwpkihg_``_abaced`dejqqw{x~˲fZTQQPQQRQNSWVXUTZ[\`bae`kg]]]Z]ZXXVSQPNRNPQUSRSRRXXWTWVTV\YYWZUSQRRQRPPORRSQRRVQR[rĴyxvvvyssoolkdjekjkkqpptuxz|{ĨeYQTSNVSPVTSVXXYSUY\[^^`b_ab^[YUSXWURYORRRQRUUWXTSUXXXVWVSUY]YUTTQSRQQQQQQROSRSURT[rȵprswxw{y{ww{uupqprtvyvxz{w~}äx`TTUTTTVSRSUWZZ[XV\a[^_\[^a`\[ZTVTXWVQRUSUTUWXVUTSUWYYVUXVQRWVVY`c\WQPOQQRRUSQSRRQXuνxrzuxz}{zwwyvv{uy|}{}}Ûs\SRUUWZWYWXU[]^]]Y\_a\`[[^ac^_ZXY\^[YYXUSXYUWZXXWVVS[YSVUXUSSWbehif_YRPPRSUURQRSRR_yɻtyuvz~}x|zz~z~~ҽxaRSSUX^[WVYZ^_bc^`bdb`]Z^aede^[YY[\]ZZ[WWWWTWYWYWXWXXXX[]a[VRZ\qkigcXTRTSTUTSPSNPLbyɽ|xx||xz~~saRPSW]]a^^[\_cfddabad^]^^_bda[][YX][\YYWUSRSSUWWWZ[YXY]^a`_ZYZadmmpiYTRUTWXTSRQRPPg¹{~}~~}ePTUY]`cd``_adfeee]_`_[^\a_d`ZXYX\\cYVUTTSTQRSVW[]\^XXXcda``[X\bprc[URRSTVRYTPRQPYmĽкl[TVX^adcdbccceecddZ[`YZZZ[ab]UUTOTTTVXZ]YWQOORWZZZZZZX[cgd^WPS[YUVUNOQSSTYTSQQRV`mĿɸp_TUX[bfbd`_``ccccaaW\VUYVVUWWUYZ[SRXU[^^[XSMNQUVY_[\XWUUYZVRTVYWWVUTQQQTTSROQSRTdrʻui[WY\]dfd`b^^^abd]]`Y]ZYXXUPXY`^[YZMY[]ZWWSRNNQVWUVSSRVSTSUWXZYYXVTQQPORRRPQSNV\gz͹|nbYVY^cb_a]]YX\a`a`_l^eea]WVS]a`ZYYQSV]eZVOIMONOOSRUSRSTSTTXZZ\[ZWSUQRRQRNNSPT\_lÿѾsg^TYZ\b]\ZZWUTY^b_bdlnjf^YYUU[_bYWOOQPPTPPNPQNRRWTRVSQUVWWYZ\Y[ZTUUSSSRSNPNQYbhvįyoeZUWYY[ZZXVRSUTd^fglmpif]SRQVYZQMMMLMMQPSRRPQSUTVWYURVZ[\[^^ZZZ[YVXUSQQRMPT\dpĿ̹vmaSSTWXYZZYVWSRT[^chnsrl\TORLNMNKIKMMRRTVZUUTUY[ZZZYZ\ZV^^a][Y[\XWUTSTQMOQZamr¿íyuidXRSWXXYZYVVYSSTZbab`[[YWVUPPKNLNNPRWXZVWXYYX\^\\[XZZ``\YZa]_a]XWTRQPRSW^hnɵvph_YUPTWWXZVXTQSRTTYY]^[[ZZVVSVQQRQS\YZ]\\]ZZZ_Z[[\YY^Z[Y][_`aaa[XURRRTY]_m|~xnjd\WQTTVSVWWSSQLLQSU[\_YYWVSVSRSUUXY\\[[ZZZ[ZZYZ[ZYWWSYX]aebb_[VTSSXY_ais̻zrmdcaWSSTUUTUSRLLJMPPPTXYXTTVSTXRVTWXWXZZY[VWYYYWSQTTUYZ\__`^^XWWWZ[`ago|dz}~xsfbWSSNPRRRONPMLMNQRRRTTRTTPTSTTWYTSZZZXZRTSSRSTTTVWZZ]ZY\XZZZ]^afjovƿµ}pjbYVVQPQPNNNLONNUUUTSPQRNNOPPQNOMYWXWWWUUTRXRWUUWWXUY[VYXZ`[dchloqµ{sjc_\\VQNNORMMNPRTXVVVTNNKKLKJLLNPPTVYXXYWVUWUVWVWXVVWYWZ]cddioow}ñxqjkfb[SNNMMMOQRTTVX\TTPPMMJHKKMMQTUS\[]WYXVSWWXYV]^]Z]bacegklon¾¿Ĵ|xxtpjcZRPPOOLLPRXZZWTUTPMJNLMLNPPRW[\ZX[YYWXXZcdjdc`dccdenhnltʹzwytof^XTQNQRMOQRTUXURONNKLNLNONKSXOZ`]X]anu{sqromikkiilmknvÿ}{|~wuofb`]ZWSQSNQRROQOMLMNOLNOORRP^ihgfchoyyqunqpokmnms~ǻ}}|wrqiie`^]\[YTSSRSQNPPPQSSQURX_fnqqnop~~}}{ytopqir{~̾Ĵ}}zprrojhfcb`__\[ZYWXVTWWXZZX_hosvwvqxzpplolszͻ}{zvvtpnkihjeef`___^\^`__^`hotx}}|wllnjoswǽ~|~}{wuqpopmjjhjdecdgdfefcfkt}utwyxvvprqrv|Ƚ{yvrrqrqoslnkiihkkkojkltpxwywvsvwwywu~}zyvttqstnqkoqpwrrqqruwwz|s{x||x|vyvxwuwqrqxturtxy|ü|y~þĿ¿ÿĿ¾½ý¿þ¿¾ÿľþ»ÿÿ¿ÿÿ¾¼¿Ŀ¾Ŀÿ¿¾¾¾ſ¾¾¿¿¾¼¾¿ĿÿÿĿ½Ŀ¿Ŀ¼ý¾ÿ¿½½¼¾þ¿Žƿ¾¿ĻþĿÿü¿¿ÿ¿þſÿþ¿¿ÿýÿÿ½¿ƿ¿¿¾ÿĺÿ¿ľ¿¿¿ý¾¿ƾ¿ÿ¾¾þ¾ý·¾ü½¿¼ý¿¾ÿý» \ No newline at end of file diff --git a/tutorial/imgproc/count-coins/coins2.jpg b/tutorial/imgproc/count-coins/coins2.jpg new file mode 100644 index 0000000000..5476f6e168 Binary files /dev/null and b/tutorial/imgproc/count-coins/coins2.jpg differ diff --git a/tutorial/imgproc/count-coins/coins2.pgm b/tutorial/imgproc/count-coins/coins2.pgm deleted file mode 100644 index c8f1d2e915..0000000000 Binary files a/tutorial/imgproc/count-coins/coins2.pgm and /dev/null differ diff --git a/tutorial/imgproc/count-coins/tutorial-count-coins.cpp b/tutorial/imgproc/count-coins/tutorial-count-coins.cpp index 3f7df36eed..710cc91c08 100644 --- a/tutorial/imgproc/count-coins/tutorial-count-coins.cpp +++ b/tutorial/imgproc/count-coins/tutorial-count-coins.cpp @@ -21,7 +21,7 @@ int main(int argc, char *argv[]) #if defined(VISP_HAVE_MODULE_IMGPROC) && (defined(VISP_HAVE_X11) || defined(VISP_HAVE_GDI) || defined(VISP_HAVE_OPENCV)) //! [Macro defined] - std::string input_filename = "coins1.pgm"; + std::string input_filename = "coins1.jpg"; vp::vpAutoThresholdMethod method = vp::AUTO_THRESHOLD_OTSU; bool white_foreground = false; diff --git a/tutorial/imgproc/hough-transform/CMakeLists.txt b/tutorial/imgproc/hough-transform/CMakeLists.txt new file mode 100644 index 0000000000..710487c12e --- /dev/null +++ b/tutorial/imgproc/hough-transform/CMakeLists.txt @@ -0,0 +1,26 @@ +project(tutorial-hough) + +cmake_minimum_required(VERSION 3.0) + +find_package(VISP REQUIRED visp_core visp_gui visp_imgproc) + +# set the list of source files +set(tutorial_cpp + tutorial-circle-hough.cpp + ) + +# list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/config") +list(APPEND tutorial_data "${CMAKE_CURRENT_SOURCE_DIR}/coins2.jpg") + +foreach(cpp ${tutorial_cpp}) + visp_add_target(${cpp} drawingHelpers.cpp) + if(COMMAND visp_add_dependency) + visp_add_dependency(${cpp} "tutorials") + endif() +endforeach() + +# Copy the data files to the same location than the target +visp_copy_dir(tutorial-circle-hough "${CMAKE_CURRENT_SOURCE_DIR}" config) +foreach(data ${tutorial_data}) + visp_copy_data(tutorial-circle-hough.cpp ${data}) +endforeach() diff --git a/tutorial/imgproc/hough-transform/README.md b/tutorial/imgproc/hough-transform/README.md new file mode 100644 index 0000000000..9693f7da7a --- /dev/null +++ b/tutorial/imgproc/hough-transform/README.md @@ -0,0 +1,58 @@ +# tutorial-cht + +## Running the circle detection tutorial + +### On synthetic images + +#### Using a JSON file as configuration file +To use a JSON file as configuration, you need to install [JSON for modern C++](https://visp-doc.inria.fr/doxygen/visp-daily/supported-third-parties.html#soft_tool_json) and compile ViSP with it. + +To run the software on the synthetic images, please run: +``` +$ TARGET=full # or TARGET=half # or TARGET=quarter +$ ./tutorial-circle-hough --input ${TARGET}_disks --config config/detector_${TARGET}.json +``` + +#### Using command lines + +To run the software on the synthetic images without a JSON configuration file, please run: +``` +$ TARGET=full # or TARGET=half # or TARGET=quarter +$ ./tutorial-circle-hough --input ${TARGET}_disks +``` + +### On actual images + +#### Using a JSON configuration file +To run the software on an actual image, please run: +``` +$ ./tutorial-circle-hough --input /path/to/my/image --config config/detector_img.json +``` + +If the detections seem a bit off, you might need to change the parameters + +#### Using command lines + +To run the software on an actual image without a JSON configuration file, please run: +``` +$ TARGET=full # or TARGET=half # or TARGET=quarter +$ ./tutorial-circle-hough --input /path/to/my/image --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 +``` + +### On 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`. For instance with `${BASENAME}` = `video_`, you can have the following list of images: `video_0001.png`, `video_0002.png` and so on. + +#### Using a JSON file as input + +To run the software using a JSON configuration file, please run: +``` +$ ./tutorial-circle-hough --input /path/to/video/${BASENAME}%d.png --config config/detector_img.json +``` + +#### Using program arguments +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 +``` \ No newline at end of file diff --git a/tutorial/imgproc/hough-transform/coins2.jpg b/tutorial/imgproc/hough-transform/coins2.jpg new file mode 100644 index 0000000000..5476f6e168 Binary files /dev/null and b/tutorial/imgproc/hough-transform/coins2.jpg differ diff --git a/tutorial/imgproc/hough-transform/config/detector_full.json b/tutorial/imgproc/hough-transform/config/detector_full.json new file mode 100644 index 0000000000..dda67b8b24 --- /dev/null +++ b/tutorial/imgproc/hough-transform/config/detector_full.json @@ -0,0 +1,24 @@ +{ + "cannyThresh": 150.0, + "centerMinDistance": 15.0, + "centerThresh": 100.0, + "centerXlimits": [ + 0, + 640 + ], + "centerYlimits": [ + 0, + 480 + ], + "circlePerfectnessThreshold": 0.9, + "dilatationNbIter": 1, + "gaussianKernelSize": 5, + "gaussianStdev": 1.0, + "mergingRadiusDiffThresh": 10.0, + "radiusLimits": [ + 0, + 1000 + ], + "radiusThreshRatio": 5, + "sobelKernelSize": 3 +} diff --git a/tutorial/imgproc/hough-transform/config/detector_half.json b/tutorial/imgproc/hough-transform/config/detector_half.json new file mode 100644 index 0000000000..4b5f9a4e9f --- /dev/null +++ b/tutorial/imgproc/hough-transform/config/detector_half.json @@ -0,0 +1,24 @@ +{ + "cannyThresh": 150.0, + "centerMinDistance": 15.0, + "centerThresh": 50.0, + "centerXlimits": [ + 0, + 640 + ], + "centerYlimits": [ + 0, + 480 + ], + "circlePerfectnessThreshold": 0.9, + "dilatationNbIter": 1, + "gaussianKernelSize": 5, + "gaussianStdev": 1.0, + "mergingRadiusDiffThresh": 10.0, + "radiusLimits": [ + 0, + 1000 + ], + "radiusThreshRatio": 2, + "sobelKernelSize": 3 +} diff --git a/tutorial/imgproc/hough-transform/config/detector_img.json b/tutorial/imgproc/hough-transform/config/detector_img.json new file mode 100644 index 0000000000..f9537a1d2c --- /dev/null +++ b/tutorial/imgproc/hough-transform/config/detector_img.json @@ -0,0 +1,25 @@ +{ + "cannyThresh": -1.0, + "centerMinDistance": 5.0, + "centerThresh": 100.0, + "centerXlimits": [ + 0, + 1920 + ], + "centerYlimits": [ + 0, + 1080 + ], + "circlePerfectnessThreshold": 0.95, + "dilatationNbIter": 1, + "edgeMapFilteringNbIter" : 5, + "gaussianKernelSize": 25, + "gaussianStdev": 2.5, + "mergingRadiusDiffThresh": 5.0, + "radiusLimits": [ + 34, + 75 + ], + "radiusThreshRatio": 4.0, + "sobelKernelSize": 7 +} diff --git a/tutorial/imgproc/hough-transform/config/detector_quarter.json b/tutorial/imgproc/hough-transform/config/detector_quarter.json new file mode 100644 index 0000000000..a59fcc6263 --- /dev/null +++ b/tutorial/imgproc/hough-transform/config/detector_quarter.json @@ -0,0 +1,24 @@ +{ + "cannyThresh": 150.0, + "centerMinDistance": 15.0, + "centerThresh": 25.0, + "centerXlimits": [ + 0, + 640 + ], + "centerYlimits": [ + 0, + 480 + ], + "circlePerfectnessThreshold": 0.9, + "dilatationNbIter": 1, + "gaussianKernelSize": 5, + "gaussianStdev": 1.0, + "mergingRadiusDiffThresh": 15.0, + "radiusLimits": [ + 0, + 1000 + ], + "radiusThreshRatio": 1, + "sobelKernelSize": 3 +} diff --git a/tutorial/imgproc/hough-transform/drawingHelpers.cpp b/tutorial/imgproc/hough-transform/drawingHelpers.cpp new file mode 100644 index 0000000000..ca1e9fb26c --- /dev/null +++ b/tutorial/imgproc/hough-transform/drawingHelpers.cpp @@ -0,0 +1,54 @@ +#include "drawingHelpers.h" + +#include + +#if defined(VISP_HAVE_X11) +vpDisplayX drawingHelpers::d; +#elif defined(VISP_HAVE_OPENCV) +vpDisplayOpenCV drawingHelpers::d; +#elif defined(VISP_HAVE_GTK) +vpDisplayGTK drawingHelpers::d; +#elif defined(VISP_HAVE_GDI) +vpDisplayGDI drawingHelpers::d; +#elif defined(VISP_HAVE_D3D9) +vpDisplayD3D drawingHelpers::d; +#endif + +vpImage drawingHelpers::I_disp; + +bool drawingHelpers::display(vpImage &I, const std::string &title, const bool &blockingMode) +{ + I_disp = I; + if (!d.isInitialised()) { + d.init(I_disp); + vpDisplay::setTitle(I_disp, title.c_str()); + } + + vpDisplay::display(I_disp); + vpDisplay::displayText(I_disp, 15, 15, "Left click to continue...", vpColor::red); + vpDisplay::displayText(I_disp, 35, 15, "Right click to stop...", vpColor::red); + vpDisplay::flush(I_disp); + vpMouseButton::vpMouseButtonType button; + vpDisplay::getClick(I_disp, button, blockingMode); + bool hasToContinue = true; + if (button == vpMouseButton::button3) { + // Right click => stop the program + hasToContinue = false; + } + + return hasToContinue; +} + +bool drawingHelpers::display(vpImage &D, const std::string &title, const bool &blockingMode) +{ + vpImage I; // Image to display + vpImageConvert::convert(D, I); + return display(I, title, blockingMode); +} + +bool drawingHelpers::display(vpImage &D, const std::string &title, const bool &blockingMode) +{ + vpImage I; // Image to display + vpImageConvert::convert(D, I); + return display(I, title, blockingMode); +} diff --git a/tutorial/imgproc/hough-transform/drawingHelpers.h b/tutorial/imgproc/hough-transform/drawingHelpers.h new file mode 100644 index 0000000000..09b5b7323a --- /dev/null +++ b/tutorial/imgproc/hough-transform/drawingHelpers.h @@ -0,0 +1,30 @@ +#ifndef _drawingHelpers_h_ +#define _drawingHelpers_h_ + +#include +#include +#include +#include + +namespace drawingHelpers +{ +#if defined(VISP_HAVE_X11) +extern vpDisplayX d; +#elif defined(VISP_HAVE_OPENCV) +extern vpDisplayOpenCV d; +#elif defined(VISP_HAVE_GTK) +extern vpDisplayGTK d; +#elif defined(VISP_HAVE_GDI) +extern vpDisplayGDI d; +#elif defined(VISP_HAVE_D3D9) +extern vpDisplayD3D d; +#endif + +extern vpImage I_disp; + +bool display(vpImage &I, const std::string &title, const bool &blockingMode); +bool display(vpImage &I, const std::string &title, const bool &blockingMode); +bool display(vpImage &D, const std::string &title, const bool &blockingMode); +} + +#endif diff --git a/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp b/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp new file mode 100644 index 0000000000..f13afa0c46 --- /dev/null +++ b/tutorial/imgproc/hough-transform/tutorial-circle-hough.cpp @@ -0,0 +1,526 @@ +// ViSP includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 * 0.50, circleColor / 2, circleColor / 2, circleThickness, circleColor); + drawDisk(I_src, vpImagePoint(bottom, left), circleRadius, circleColor, circleColor, circleThickness, bckg); + drawDisk(I_src, vpImagePoint(bottom, left), circleRadius * 0.50, circleColor / 2, circleColor / 2, circleThickness, circleColor); + drawDisk(I_src, vpImagePoint(top, right), circleRadius, circleColor, circleColor, circleThickness, bckg); + drawDisk(I_src, vpImagePoint(top, right), circleRadius * 0.50, circleColor / 2, circleColor / 2, circleThickness, circleColor); + drawDisk(I_src, vpImagePoint(bottom, right), circleRadius, circleColor, circleColor, circleThickness, bckg); + drawDisk(I_src, vpImagePoint(bottom, right), circleRadius * 0.50, 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) +{ + double t0 = vpTime::measureTimeMicros(); + //! [Run detection] + std::vector detectedCircles = detector.detect(I_src, nbCirclesToDetect); + //! [Run detection] + double tF = vpTime::measureTimeMicros(); + std::cout << "Process time = " << (tF - t0) * 0.001 << "ms" << std::endl << std::flush; + vpImage I_disp; + vpImageConvert::convert(I_src, I_disp); + + unsigned int id = 0; + std::vector v_colors = { vpColor::red, vpColor::purple, vpColor::orange, vpColor::yellow, vpColor::blue }; + unsigned int idColor = 0; + //! [Iterate detections] + for (auto circleCandidate : detectedCircles) { + vpImageDraw::drawCircle(I_disp, circleCandidate, v_colors[idColor], 2); + std::cout << "Circle #" << id << ":" << std::endl; + std::cout << "\tCenter: (" << circleCandidate.getCenter() << ")" << std::endl; + std::cout << "\tRadius: (" << circleCandidate.getRadius() << ")" << std::endl; + id++; + idColor = (idColor + 1) % v_colors.size(); + } + //! [Iterate detections] + + if (displayCanny) { + vpImage edgeMap = detector.getEdgeMap(); + drawingHelpers::display(edgeMap, "Edge map", true); + } + return drawingHelpers::display(I_disp, "Detection results", blockingMode); +} + +int main(int argc, char **argv) +{ + const std::string def_input(typeInputImageToString(FULL_DISKS)); + const std::string def_jsonFilePath = std::string(""); + const int def_nbCirclesToDetect = -1; + const int def_gaussianKernelSize = 5; + const double def_gaussianSigma = 1.; + const int def_sobelKernelSize = 3; + const double def_cannyThresh = 150.; + 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 double def_centerThresh = -1.; + const double def_radiusThreshRatio = -1.; + const double def_circlePerfectness = 0.85; + const double def_centerDistanceThresh = 15; + const double def_radiusDifferenceThresh = 15; + + + std::string opt_input(def_input); + std::string opt_jsonFilePath = def_jsonFilePath; + int opt_nbCirclesToDetect = def_nbCirclesToDetect; + int opt_gaussianKernelSize = def_gaussianKernelSize; + double opt_gaussianSigma = def_gaussianSigma; + int opt_sobelKernelSize = def_sobelKernelSize; + double opt_cannyThresh = def_cannyThresh; + int opt_nbEdgeFilteringIter = def_nbEdgeFilteringIter; + std::pair opt_centerXlimits = def_centerXlimits; + std::pair opt_centerYlimits = def_centerYlimits; + unsigned int opt_minRadius = def_minRadius; + unsigned int opt_maxRadius = def_maxRadius; + int opt_dilatationRepet = def_dilatationRepet; + double opt_centerThresh = def_centerThresh; + int opt_radiusThreshRatio = def_radiusThreshRatio; + double opt_circlePerfectness = def_circlePerfectness; + double opt_centerDistanceThresh = def_centerDistanceThresh; + double opt_radiusDifferenceThresh = def_radiusDifferenceThresh; + bool opt_displayCanny = false; + + for (int i = 1; i < argc; i++) { + std::string argName(argv[i]); + if (argName == "--input" && i + 1 < argc) { + opt_input = std::string(argv[i + 1]); + i++; + } +#ifdef VISP_HAVE_NLOHMANN_JSON + else if (argName == "--config" && i + 1 < argc) { + opt_jsonFilePath = std::string(argv[i + 1]); + i++; + } +#endif + else if (argName == "--nb-circles" && i + 1 < argc) { + opt_nbCirclesToDetect = atoi(argv[i + 1]); + i++; + } + else if (argName == "--gaussian-kernel" && i + 1 < argc) { + opt_gaussianKernelSize = atoi(argv[i + 1]); + i++; + } + else if (argName == "--gaussian-sigma" && i + 1 < argc) { + opt_gaussianSigma = atof(argv[i + 1]); + i++; + } + else if (argName == "--sobel-kernel" && i + 1 < argc) { + opt_sobelKernelSize = atoi(argv[i + 1]); + i++; + } + else if (argName == "--canny-thresh" && i + 1 < argc) { + opt_cannyThresh = atof(argv[i + 1]); + i++; + } + else if (argName == "--edge-filter" && i + 1 < argc) { + opt_nbEdgeFilteringIter = atoi(argv[i + 1]); + i++; + } + else if (argName == "--dilatation-repet" && i + 1 < argc) { + opt_dilatationRepet = atoi(argv[i + 1]); + i++; + } + else if (argName == "--radius-limits" && i + 2 < argc) { + opt_minRadius = atoi(argv[i + 1]); + opt_maxRadius = atoi(argv[i + 2]); + i += 2; + } + else if (argName == "--center-thresh" && i + 1 < argc) { + opt_centerThresh = atof(argv[i + 1]); + i++; + } + else if (argName == "--center-xlim" && i + 2 < argc) { + opt_centerXlimits = std::pair(atoi(argv[i + 1]), atoi(argv[i + 2])); + i += 2; + } + else if (argName == "--center-ylim" && i + 2 < argc) { + opt_centerYlimits = std::pair(atoi(argv[i + 1]), atoi(argv[i + 2])); + i += 2; + } + else if (argName == "--radius-thresh" && i + 1 < argc) { + opt_radiusThreshRatio = atof(argv[i + 1]); + i++; + } + else if (argName == "--circle-perfectness" && i + 1 < argc) { + opt_circlePerfectness = atof(argv[i + 1]); + i++; + } + else if (argName == "--merging-thresh" && i + 2 < argc) { + opt_centerDistanceThresh = atof(argv[i + 1]); + opt_radiusDifferenceThresh = atof(argv[i + 2]); + i += 2; + } + else if (argName == "--display-edge-map") { + opt_displayCanny = true; + } + else if (argName == "--help" || argName == "-h") { + std::cout << "NAME" << std::endl; + std::cout << "\t" << argv[0] << " Test program for the home-made Hough Circle Detection algorithm" << std::endl + << std::endl; + std::cout << "SYNOPSIS" << std::endl; + std::cout << "\t" << argv[0] + << "\t [--input " << getAvailableTypeInputImage() << "]" << 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 [--canny-thresh ] (default: " << def_cannyThresh << ")" << 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 [--center-xlim ] (default: " << def_centerXlimits.first << " , " << def_centerXlimits.second << ")" << std::endl + << "\t [--center-ylim ] (default: " << def_centerYlimits.first << " , " << def_centerYlimits.second << ")" << std::endl + << "\t [--radius-thresh ] (default: " << (def_radiusThreshRatio < 0 ? "auto" : std::to_string(def_radiusThreshRatio)) << ")" << std::endl + << "\t [--circle-perfectness ] (default: " << def_radiusThreshRatio << ")" << std::endl + << "\t [--merging-thresh ] (default: centers distance threshold = " << def_centerDistanceThresh << ", radius difference threshold = " << def_radiusDifferenceThresh << ")" << 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\tDefault: " << def_input << std::endl + << std::endl +#ifdef VISP_HAVE_NLOHMANN_JSON + << "\t--config" << std::endl + << "\t\tPermit to configure the Hough Circle Algorithm using a JSON file." << std::endl + << "\t\tDefault: " << (def_jsonFilePath.empty() ? "unused" : def_jsonFilePath) << std::endl + << std::endl +#endif + << "\t--nb-circles" << std::endl + << "\t\tPermit to choose the number of circles we want to detect in the image" << std::endl + << "\t\tThe results will be the circles having the greatest number of votes." << std::endl + << "\t\tDefault: " << def_nbCirclesToDetect << std::endl + << std::endl + << "\t--gaussian-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--gaussian-sigma" << std::endl + << "\t\tPermit to set the standard deviation of the Gaussian filter." << std::endl + << "\t\tMust be a positive value." << std::endl + << "\t\tDefault: " << def_gaussianSigma << std::endl + << std::endl + << "\t--canny-thresh" << std::endl + << "\t\tPermit to set the upper threshold of the Canny edge detector." << std::endl + << "\t\tMust be a positive value." << std::endl + << "\t\tDefault: " << def_cannyThresh << std::endl + << std::endl + << "\t--edge-filter" << std::endl + << "\t\tPermit to set the number of iteration of 8-neighbor filter iterations of the result of the Canny edge detector." << std::endl + << "\t\tIf negative, no filtering is performed." << std::endl + << "\t\tDefault: " << def_nbEdgeFilteringIter << std::endl + << std::endl + << "\t--radius-limits" << std::endl + << "\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\tMinimum tolerated value is 1." << std::endl + << "\t\tDefault: " << def_dilatationRepet << 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 + << 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 + << "\t\tThe search area is limited to [-maxRadius; +image.width + maxRadius]." << std::endl + << "\t\tDefault: " << def_centerXlimits.first << " , " << def_centerXlimits.second << std::endl + << std::endl + << "\t--center-ylim" << std::endl + << "\t\tPermit to set the minimum and maximum vertical position to be considered as a center candidate." << std::endl + << "\t\tThe search area is limited to [-maxRadius; +image.height + maxRadius]." << std::endl + << "\t\tDefault: " << def_centerYlimits.first << " , " << def_centerYlimits.second << std::endl + << std::endl + << "\t--radius-thresh" << std::endl + << "\t\tPermit to to set the minimum number of votes per radian a radius must reach to be considered as a circle candidate a given pair (center candidate, radius candidate)." << std::endl + << "\t\tDefault: " << (def_radiusThreshRatio < 0 ? "auto" : std::to_string(def_radiusThreshRatio)) << std::endl + << std::endl + << "\t--circle-perfectness" << std::endl + << "\t\tPermit to set the set the circle perfectness threshold." << std::endl + << "\t\tThis parameter is used during the radius candidates computation." << std::endl + << "\t\tThe scalar product radius RC_ij . gradient(Ep_j) >= m_circlePerfectness * || RC_ij || * || gradient(Ep_j) || to add a vote for the radius RC_ij." << std::endl + << "\t\tDefault: " << def_circlePerfectness << std::endl + << std::endl + << "\t--merging-thresh" << std::endl + << "\t\tPermit to set the thresholds used during the merging stage of the algorithm." << std::endl + << "\t\tThe center distance threshold indicates the maximum distance the centers can be in order to be merged." << std::endl + << "\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 + << "\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 + << std::endl; + return EXIT_SUCCESS; + } + } + + 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: + opt_centerThresh = 100.; + break; + case TypeInputImage::HALF_DISKS: + opt_centerThresh = 50.; + break; + case TypeInputImage::QUARTER_DISKS: + opt_centerThresh = 25.; + break; + default: + throw(vpException(vpException::badValue, "Missing center threshold value to use with actual pictures as input. See the help for more information.")); + } + } + + if (opt_radiusThreshRatio < 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: + opt_radiusThreshRatio = 5.; + break; + case TypeInputImage::HALF_DISKS: + opt_radiusThreshRatio = 2.; + break; + case TypeInputImage::QUARTER_DISKS: + opt_radiusThreshRatio = 1.; + break; + default: + throw(vpException(vpException::badValue, "Missing radius threshold value to use with actual pictures as input. See the help for more information.")); + } + } + + //! [Algo params] + vpCircleHoughTransform::CHTransformParameters + algoParams(opt_gaussianKernelSize + , opt_gaussianSigma + , opt_sobelKernelSize + , opt_cannyThresh + , opt_nbEdgeFilteringIter + , opt_centerXlimits + , opt_centerYlimits + , opt_minRadius + , opt_maxRadius + , opt_dilatationRepet + , opt_centerThresh + , opt_radiusThreshRatio + , opt_circlePerfectness + , opt_centerDistanceThresh + , opt_radiusDifferenceThresh + ); + //! [Algo params] + + //! [Algo init] + vpCircleHoughTransform detector; + if (opt_jsonFilePath.empty()) { + std::cout << "Initializing detector from the program arguments [...]" << std::endl; + detector.init(algoParams); + } + else { +#ifdef VISP_HAVE_NLOHMANN_JSON + std::cout << "Initializing detector from JSON file \"" << opt_jsonFilePath << "\", some of the program arguments will be ignored [...]" << std::endl; + detector.initFromJSON(opt_jsonFilePath); +#else + throw(vpException(vpException::functionNotImplementedError, "You must install nlohmann JSON library to use this feature, see https://visp-doc.inria.fr/doxygen/visp-daily/supported-third-parties.html#soft_tool_json for more information.")); +#endif + } + //! [Algo init] + 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] + } + } + else { + //! [Manage synthetic image] + I_src = generateImage(inputType); + test_detection(I_src, detector, opt_nbCirclesToDetect, true, opt_displayCanny); + //! [Manage synthetic image] + } + return EXIT_SUCCESS; +}