4#include <visp3/core/vpConfig.h>
5#include <visp3/core/vpException.h>
6#include <visp3/core/vpImage.h>
7#include <visp3/core/vpImageConvert.h>
8#include <visp3/core/vpImageDraw.h>
9#include <visp3/core/vpIoTools.h>
10#include <visp3/core/vpTime.h>
11#include <visp3/imgproc/vpCircleHoughTransform.h>
12#include <visp3/imgproc/vpImgproc.h>
13#include <visp3/io/vpImageIo.h>
14#include <visp3/io/vpVideoReader.h>
16#if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11)
18#include "drawingHelpers.h"
21typedef enum TypeInputImage
29std::string typeInputImageToString(
const TypeInputImage &type)
40 name =
"quarter_disks";
43 name =
"path/to/your/image";
49TypeInputImage typeInputImageFromString(
const std::string &name)
51 TypeInputImage type(USER_IMG);
53 for (
unsigned int id = 0;
id < USER_IMG && !hasFound;
id++) {
54 TypeInputImage candidate = (TypeInputImage)
id;
55 if (name == typeInputImageToString(candidate)) {
63std::string getAvailableTypeInputImage(
const std::string &prefix =
"<",
const std::string &sep =
" , ",
const std::string &suffix =
">")
65 std::string list(prefix);
66 for (
unsigned int id = 0;
id < USER_IMG;
id++) {
67 list += typeInputImageToString((TypeInputImage)
id) + sep;
69 list += typeInputImageToString(USER_IMG) + suffix;
76 const unsigned int &borderColor,
const unsigned int &fillingColor,
const unsigned int &thickness,
const unsigned int &bckg)
90generateImage(
const TypeInputImage &inputType)
94 const unsigned int width = 640;
95 const unsigned int height = 480;
96 const unsigned int bckg = 0;
99 const unsigned int circleColor = 128;
100 const unsigned int circleRadius = 50;
101 const unsigned int circleThickness = 1;
104 const double topFull = height / 4;
105 const double bottomFull = 3 * height / 4;
106 const double leftFull = width / 4;
107 const double rightFull = 3 * width / 4;
110 const double topHalf = 1;
111 const double bottomHalf = height - 1;
112 const double leftHalf = width / 4;
113 const double rightHalf = 3 * width / 4;
116 const double topQuarter = 1;
117 const double bottomQuarter = height - 1;
118 const double leftQuarter = 1;
119 const double rightQuarter = width - 1;
123 double top, left, bottom, right;
140 bottom = bottomQuarter;
141 right = rightQuarter;
148 drawDisk(I_src,
vpImagePoint(top, left), circleRadius, circleColor, circleColor, circleThickness, bckg);
149 drawDisk(I_src,
vpImagePoint(top, left), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor);
150 drawDisk(I_src,
vpImagePoint(bottom, left), circleRadius, circleColor, circleColor, circleThickness, bckg);
151 drawDisk(I_src,
vpImagePoint(bottom, left), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor);
152 drawDisk(I_src,
vpImagePoint(top, right), circleRadius, circleColor, circleColor, circleThickness, bckg);
153 drawDisk(I_src,
vpImagePoint(top, right), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor);
154 drawDisk(I_src,
vpImagePoint(bottom, right), circleRadius, circleColor, circleColor, circleThickness, bckg);
155 drawDisk(I_src,
vpImagePoint(bottom, right), circleRadius / 2, circleColor / 2, circleColor / 2, circleThickness, circleColor);
157 std::cout <<
"Done drawing" << std::endl << std::flush;
165 std::vector<vpImageCircle> detectedCircles = detector.
detect(I_src, nbCirclesToDetect);
168 std::cout <<
"Process time = " << (tF - t0) * 0.001 <<
"ms" << std::endl << std::flush;
174 unsigned int idColor = 0;
176 for (
auto circleCandidate : detectedCircles) {
178 std::cout <<
"Circle #" <<
id <<
":" << std::endl;
179 std::cout <<
"\tCenter: (" << circleCandidate.getCenter() <<
")" << std::endl;
180 std::cout <<
"\tRadius: (" << circleCandidate.getRadius() <<
")" << std::endl;
182 idColor = (idColor + 1) % v_colors.size();
193int main(
int argc,
char **argv)
195 const std::string def_input(typeInputImageToString(FULL_DISKS));
196 const std::string def_jsonFilePath = std::string(
"");
197 const int def_nbCirclesToDetect = -1;
198 const int def_gaussianKernelSize = 5;
199 const float def_gaussianSigma = 1.f;
200 const int def_sobelKernelSize = 3;
201#ifdef HAVE_OPENCV_IMGPROC
202 const float def_lowerCannyThresh = 50.f;
203 const float def_upperCannyThresh = 150.f;
205 const float def_lowerCannyThresh = 8.f;
206 const float def_upperCannyThresh = 25.f;
208 const int def_nbEdgeFilteringIter = 2;
209 const std::pair<int, int> def_centerXlimits = std::pair<int, int>(0, 640);
210 const std::pair<int, int> def_centerYlimits = std::pair<int, int>(0, 480);
211 const unsigned int def_minRadius = 0;
212 const unsigned int def_maxRadius = 1000;
213 const int def_dilatationRepet = 1;
214 const float def_centerThresh = -1.f;
215 const float def_radiusThreshRatio = -1.f;
216 const float def_circlePerfectness = 0.85f;
217 const float def_centerDistanceThresh = 15.f;
218 const float def_radiusDifferenceThresh = 15.f;
220 std::string opt_input(def_input);
221 std::string opt_jsonFilePath = def_jsonFilePath;
222 int opt_nbCirclesToDetect = def_nbCirclesToDetect;
223 int opt_gaussianKernelSize = def_gaussianKernelSize;
224 float opt_gaussianSigma = def_gaussianSigma;
225 int opt_sobelKernelSize = def_sobelKernelSize;
226 float opt_lowerCannyThresh = def_lowerCannyThresh;
227 float opt_upperCannyThresh = def_upperCannyThresh;
228 int opt_nbEdgeFilteringIter = def_nbEdgeFilteringIter;
229 std::pair<int, int> opt_centerXlimits = def_centerXlimits;
230 std::pair<int, int> opt_centerYlimits = def_centerYlimits;
231 unsigned int opt_minRadius = def_minRadius;
232 unsigned int opt_maxRadius = def_maxRadius;
233 int opt_dilatationRepet = def_dilatationRepet;
234 float opt_centerThresh = def_centerThresh;
235 float opt_radiusThreshRatio = def_radiusThreshRatio;
236 float opt_circlePerfectness = def_circlePerfectness;
237 float opt_centerDistanceThresh = def_centerDistanceThresh;
238 float opt_radiusDifferenceThresh = def_radiusDifferenceThresh;
239 bool opt_displayCanny =
false;
241 for (
int i = 1; i < argc; i++) {
242 std::string argName(argv[i]);
243 if (argName ==
"--input" && i + 1 < argc) {
244 opt_input = std::string(argv[i + 1]);
247#ifdef VISP_HAVE_NLOHMANN_JSON
248 else if (argName ==
"--config" && i + 1 < argc) {
249 opt_jsonFilePath = std::string(argv[i + 1]);
253 else if (argName ==
"--nb-circles" && i + 1 < argc) {
254 opt_nbCirclesToDetect = atoi(argv[i + 1]);
257 else if (argName ==
"--gaussian-kernel" && i + 1 < argc) {
258 opt_gaussianKernelSize = atoi(argv[i + 1]);
261 else if (argName ==
"--gaussian-sigma" && i + 1 < argc) {
262 opt_gaussianSigma =
static_cast<float>(atof(argv[i + 1]));
265 else if (argName ==
"--sobel-kernel" && i + 1 < argc) {
266 opt_sobelKernelSize = atoi(argv[i + 1]);
269 else if (argName ==
"--canny-thresh" && i + 2 < argc) {
270 opt_lowerCannyThresh =
static_cast<float>(atof(argv[i + 1]));
271 opt_upperCannyThresh =
static_cast<float>(atof(argv[i + 2]));
274 else if (argName ==
"--edge-filter" && i + 1 < argc) {
275 opt_nbEdgeFilteringIter = atoi(argv[i + 1]);
278 else if (argName ==
"--dilatation-repet" && i + 1 < argc) {
279 opt_dilatationRepet = atoi(argv[i + 1]);
282 else if (argName ==
"--radius-limits" && i + 2 < argc) {
283 opt_minRadius = atoi(argv[i + 1]);
284 opt_maxRadius = atoi(argv[i + 2]);
287 else if (argName ==
"--center-thresh" && i + 1 < argc) {
288 opt_centerThresh =
static_cast<float>(atof(argv[i + 1]));
291 else if (argName ==
"--center-xlim" && i + 2 < argc) {
292 opt_centerXlimits = std::pair<int, int>(atoi(argv[i + 1]), atoi(argv[i + 2]));
295 else if (argName ==
"--center-ylim" && i + 2 < argc) {
296 opt_centerYlimits = std::pair<int, int>(atoi(argv[i + 1]), atoi(argv[i + 2]));
299 else if (argName ==
"--radius-thresh" && i + 1 < argc) {
300 opt_radiusThreshRatio =
static_cast<float>(atof(argv[i + 1]));
303 else if (argName ==
"--circle-perfectness" && i + 1 < argc) {
304 opt_circlePerfectness =
static_cast<float>(atof(argv[i + 1]));
307 else if (argName ==
"--merging-thresh" && i + 2 < argc) {
308 opt_centerDistanceThresh =
static_cast<float>(atof(argv[i + 1]));
309 opt_radiusDifferenceThresh =
static_cast<float>(atof(argv[i + 2]));
312 else if (argName ==
"--display-edge-map") {
313 opt_displayCanny =
true;
315 else if (argName ==
"--help" || argName ==
"-h") {
316 std::cout <<
"NAME" << std::endl;
317 std::cout <<
"\t" << argv[0] <<
" Test program for the home-made Hough Circle Detection algorithm" << std::endl
319 std::cout <<
"SYNOPSIS" << std::endl;
320 std::cout <<
"\t" << argv[0]
321 <<
"\t [--input " << getAvailableTypeInputImage() <<
"]" << std::endl
322#ifdef VISP_HAVE_NLOHMANN_JSON
323 <<
"\t [--config <path/to/json/file>] (default: " << (def_jsonFilePath.empty() ?
"unused" : def_jsonFilePath) <<
")" << std::endl
325 <<
"\t [--nb-circles <number-circles-to-detect>] (default: " << def_nbCirclesToDetect <<
")" << std::endl
326 <<
"\t [--gaussian-kernel <kernel-size>] (default: " << def_gaussianKernelSize <<
")" << std::endl
327 <<
"\t [--gaussian-sigma <stddev>] (default: " << def_gaussianSigma <<
")" << std::endl
328 <<
"\t [--sobel-kernel <kernel-size>] (default: " << def_sobelKernelSize <<
")" << std::endl
329 <<
"\t [--canny-thresh <lower-canny-thresh upper-canny-thresh>] (default: " << def_lowerCannyThresh <<
" ; " << def_upperCannyThresh <<
")" << std::endl
330 <<
"\t [--edge-filter <nb-iter>] (default: " << def_nbEdgeFilteringIter <<
")" << std::endl
331 <<
"\t [--radius-limits <radius-min> <radius-max>] (default: min = " << def_minRadius <<
", max = " << def_maxRadius <<
")" << std::endl
332 <<
"\t [--dilatation-repet <nb-repetitions>] (default: " << def_dilatationRepet <<
")" << std::endl
333 <<
"\t [--center-thresh <center-detection-threshold>] (default: " << (def_centerThresh < 0 ?
"auto" : std::to_string(def_centerThresh)) <<
")" << std::endl
334 <<
"\t [--center-xlim <center-horizontal-min center-horizontal-max>] (default: " << def_centerXlimits.first <<
" , " << def_centerXlimits.second <<
")" << std::endl
335 <<
"\t [--center-ylim <center-vertical-min center-vertical-max>] (default: " << def_centerYlimits.first <<
" , " << def_centerYlimits.second <<
")" << std::endl
336 <<
"\t [--radius-thresh <radius-detection-threshold>] (default: " << (def_radiusThreshRatio < 0 ?
"auto" : std::to_string(def_radiusThreshRatio)) <<
")" << std::endl
337 <<
"\t [--circle-perfectness <circle-perfectness-threshold>] (default: " << def_radiusThreshRatio <<
")" << std::endl
338 <<
"\t [--merging-thresh <center-distance-thresh> <radius-difference-thresh>] (default: centers distance threshold = " << def_centerDistanceThresh <<
", radius difference threshold = " << def_radiusDifferenceThresh <<
")" << std::endl
339 <<
"\t [--display-edge-map]" << std::endl
340 <<
"\t [--help, -h]" << std::endl
343 std::cout <<
"DESCRIPTION" << std::endl
344 <<
"\t--input" << std::endl
345 <<
"\t\tPermit to choose the type of input of the Hough Circle Algorithm" << std::endl
346 <<
"\t\tDefault: " << def_input << std::endl
348#ifdef VISP_HAVE_NLOHMANN_JSON
349 <<
"\t--config" << std::endl
350 <<
"\t\tPermit to configure the Hough Circle Algorithm using a JSON file." << std::endl
351 <<
"\t\tDefault: " << (def_jsonFilePath.empty() ?
"unused" : def_jsonFilePath) << std::endl
354 <<
"\t--nb-circles" << std::endl
355 <<
"\t\tPermit to choose the number of circles we want to detect in the image" << std::endl
356 <<
"\t\tThe results will be the circles having the greatest number of votes." << std::endl
357 <<
"\t\tDefault: " << def_nbCirclesToDetect << std::endl
359 <<
"\t--gaussian-kernel" << std::endl
360 <<
"\t\tPermit to set the size of the Gaussian filter used to smooth the input image and compute its gradients." << std::endl
361 <<
"\t\tMust be an odd value." << std::endl
362 <<
"\t\tDefault: " << def_gaussianKernelSize << std::endl
364 <<
"\t--gaussian-sigma" << std::endl
365 <<
"\t\tPermit to set the standard deviation of the Gaussian filter." << std::endl
366 <<
"\t\tMust be a positive value." << std::endl
367 <<
"\t\tDefault: " << def_gaussianSigma << std::endl
369 <<
"\t--canny-thresh" << std::endl
370 <<
"\t\tPermit to set the lower and upper thresholds of the Canny edge detector." << std::endl
371 <<
"\t\tIf a value is negative, it will be automatically computed." << std::endl
372 <<
"\t\tDefault: " << def_upperCannyThresh << std::endl
374 <<
"\t--edge-filter" << std::endl
375 <<
"\t\tPermit to set the number of iteration of 8-neighbor filter iterations of the result of the Canny edge detector." << std::endl
376 <<
"\t\tIf negative, no filtering is performed." << std::endl
377 <<
"\t\tDefault: " << def_nbEdgeFilteringIter << std::endl
379 <<
"\t--radius-limits" << std::endl
380 <<
"\t\tPermit to set the minimum and maximum radii of the circles we are looking for." << std::endl
381 <<
"\t\tDefault: min = " << def_minRadius <<
", max = " << def_maxRadius << std::endl
383 <<
"\t--dilatation-repet" << std::endl
384 <<
"\t\tPermit to set the number of iterations of the dilatation operation used to detect the maxima of the centers votes." << std::endl
385 <<
"\t\tMinimum tolerated value is 1." << std::endl
386 <<
"\t\tDefault: " << def_dilatationRepet << std::endl
388 <<
"\t--center-thresh" << std::endl
389 <<
"\t\tPermit to set the minimum number of votes a point must reach to be considered as a center candidate." << std::endl
390 <<
"\t\tIf the input is a real image, must be a positive value." << std::endl
391 <<
"\t\tOtherwise, if the input is a synthetic image and the value is negative, a fine-tuned value will be used." << std::endl
392 <<
"\t\tDefault: " << (def_centerThresh < 0 ?
"auto" : std::to_string(def_centerThresh)) << std::endl
394 <<
"\t--center-xlim" << std::endl
395 <<
"\t\tPermit to set the minimum and maximum horizontal position to be considered as a center candidate." << std::endl
396 <<
"\t\tThe search area is limited to [-maxRadius; +image.width + maxRadius]." << std::endl
397 <<
"\t\tDefault: " << def_centerXlimits.first <<
" , " << def_centerXlimits.second << std::endl
399 <<
"\t--center-ylim" << std::endl
400 <<
"\t\tPermit to set the minimum and maximum vertical position to be considered as a center candidate." << std::endl
401 <<
"\t\tThe search area is limited to [-maxRadius; +image.height + maxRadius]." << std::endl
402 <<
"\t\tDefault: " << def_centerYlimits.first <<
" , " << def_centerYlimits.second << std::endl
404 <<
"\t--radius-thresh" << std::endl
405 <<
"\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
406 <<
"\t\tDefault: " << (def_radiusThreshRatio < 0 ?
"auto" : std::to_string(def_radiusThreshRatio)) << std::endl
408 <<
"\t--circle-perfectness" << std::endl
409 <<
"\t\tPermit to set the set the circle perfectness threshold." << std::endl
410 <<
"\t\tThis parameter is used during the radius candidates computation." << std::endl
411 <<
"\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
412 <<
"\t\tDefault: " << def_circlePerfectness << std::endl
414 <<
"\t--merging-thresh" << std::endl
415 <<
"\t\tPermit to set the thresholds used during the merging stage of the algorithm." << std::endl
416 <<
"\t\tThe center distance threshold indicates the maximum distance the centers can be in order to be merged." << std::endl
417 <<
"\t\tThe radius difference threshold indicates the maximum absolute difference between the two circle candidates in order to be merged." << std::endl
418 <<
"\t\tTwo circle candidates must met these two conditions in order to be merged together." << std::endl
419 <<
"\t\tDefault: centers distance threshold = " << def_centerDistanceThresh <<
", radius difference threshold = " << def_radiusDifferenceThresh << std::endl
420 <<
"\t--display-edge-map" << std::endl
421 <<
"\t\tPermit to display the edge map used to detect the circles" << std::endl
422 <<
"\t\tDefault: off" << std::endl
428 if (opt_centerThresh < 0 && opt_jsonFilePath.empty()) {
430 TypeInputImage inputType = typeInputImageFromString(opt_input);
432 case TypeInputImage::FULL_DISKS:
433#ifdef HAVE_OPENCV_IMGPROC
434 opt_centerThresh = 100.;
436 opt_centerThresh = 75.;
439 case TypeInputImage::HALF_DISKS:
440#ifdef HAVE_OPENCV_IMGPROC
441 opt_centerThresh = 50.;
443 opt_centerThresh = 25.;
446 case TypeInputImage::QUARTER_DISKS:
447#ifdef HAVE_OPENCV_IMGPROC
448 opt_centerThresh = 25.;
450 opt_centerThresh = 15.;
458 if (opt_radiusThreshRatio < 0 && opt_jsonFilePath.empty()) {
460 TypeInputImage inputType = typeInputImageFromString(opt_input);
462 case TypeInputImage::FULL_DISKS:
463#ifdef HAVE_OPENCV_IMGPROC
464 opt_radiusThreshRatio = 5.;
466 opt_radiusThreshRatio = 1.;
469 case TypeInputImage::HALF_DISKS:
470 opt_radiusThreshRatio = 2.;
472 case TypeInputImage::QUARTER_DISKS:
473 opt_radiusThreshRatio = 1.;
482 algoParams(opt_gaussianKernelSize
484 , opt_sobelKernelSize
485 , opt_lowerCannyThresh
486 , opt_upperCannyThresh
487 , opt_nbEdgeFilteringIter
492 , opt_dilatationRepet
494 , opt_radiusThreshRatio
495 , opt_circlePerfectness
496 , opt_centerDistanceThresh
497 , opt_radiusDifferenceThresh
503 if (opt_jsonFilePath.empty()) {
504 std::cout <<
"Initializing detector from the program arguments [...]" << std::endl;
505 detector.
init(algoParams);
508#ifdef VISP_HAVE_NLOHMANN_JSON
509 std::cout <<
"Initializing detector from JSON file \"" << opt_jsonFilePath <<
"\", some of the program arguments will be ignored [...]" << std::endl;
516 std::cout << detector;
519 TypeInputImage inputType = typeInputImageFromString(opt_input);
520 if (inputType == USER_IMG) {
522 if (opt_input.find(
"%") != std::string::npos) {
524 bool hasToContinue =
true;
528 while (!g.
end() && hasToContinue) {
530 hasToContinue = test_detection(I_src, detector, opt_nbCirclesToDetect,
false, opt_displayCanny);
543 test_detection(I_src, detector, opt_nbCirclesToDetect,
true, opt_displayCanny);
549 I_src = generateImage(inputType);
550 test_detection(I_src, detector, opt_nbCirclesToDetect,
true, opt_displayCanny);
558 std::cout <<
"This tutorial needs to be build at least with cxx 11 standard!" << std::endl;
static const vpColor orange
static const vpColor blue
static const vpColor purple
static const vpColor yellow
error that can be emitted by ViSP classes.
@ badValue
Used to indicate that a value is not in the allowed range.
@ functionNotImplementedError
Function not implemented.
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
static void drawCircle(vpImage< unsigned char > &I, const vpImageCircle &circle, unsigned char color, unsigned int thickness=1)
static void read(vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
Definition of the vpImage class member functions.
Class that enables to manipulate easily a video file or a sequence of images. As it inherits from the...
void acquire(vpImage< vpRGBa > &I)
void open(vpImage< vpRGBa > &I)
void setFileName(const std::string &filename)
VISP_EXPORT void floodFill(vpImage< unsigned char > &I, const vpImagePoint &seedPoint, const unsigned char oldValue, const unsigned char newValue, const vpImageMorphology::vpConnexityType &connexity=vpImageMorphology::CONNEXITY_4)
bool display(vpImage< vpRGBa > &I, const std::string &title, const bool &blockingMode)
VISP_EXPORT int wait(double t0, double t)
VISP_EXPORT double measureTimeMicros()