From 575d3cab16cf47982a7dda20d383580134f01102 Mon Sep 17 00:00:00 2001
From: rlagneau <romain.lagneau@inria.fr>
Date: Mon, 20 Nov 2023 16:06:06 +0100
Subject: [PATCH 1/3] [CORPS] Changed the edge-thinning method

---
 .../include/visp3/core/vpCannyEdgeDetection.h |   4 +-
 .../core/src/image/vpCannyEdgeDetection.cpp   | 220 +++++++-----------
 2 files changed, 83 insertions(+), 141 deletions(-)

diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h
index 18f4920c5b..4bc4ebedfa 100644
--- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h
+++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h
@@ -116,8 +116,10 @@ class VISP_EXPORT vpCannyEdgeDetection
    * \brief Step 3: Edge thining.
    * \details Perform the edge thining step.
    * Perform a non-maximum suppression to keep only local maxima as edge candidates.
+   * \param[in] lowerThreshold Edge candidates that are below this threshold are definitely not
+   * edges.
    */
-  void performEdgeThining();
+  void performEdgeThinning(const float &lowerThreshold);
 
   /**
    * \brief Perform hysteresis thresholding.
diff --git a/modules/core/src/image/vpCannyEdgeDetection.cpp b/modules/core/src/image/vpCannyEdgeDetection.cpp
index 274ba7f6b6..84f955f491 100644
--- a/modules/core/src/image/vpCannyEdgeDetection.cpp
+++ b/modules/core/src/image/vpCannyEdgeDetection.cpp
@@ -186,9 +186,6 @@ vpCannyEdgeDetection::detect(const vpImage<unsigned char> &I)
   m_areGradientAvailable = false; // Reset for next call
 
   // // Step 3: edge thining
-  performEdgeThining();
-
-  // // Step 4: hysteresis thresholding
   float upperThreshold = m_upperThreshold;
   float lowerThreshold = m_lowerThreshold;
   if (upperThreshold < 0) {
@@ -200,7 +197,11 @@ vpCannyEdgeDetection::detect(const vpImage<unsigned char> &I)
     // Applying Canny recommendation to have the upper threshold 3 times greater than the lower threshold.
     lowerThreshold = m_upperThreshold / 3.f;
   }
+  // To ensure that if lowerThreshold = 0, we reject null gradient points
+  lowerThreshold = std::max(lowerThreshold, std::numeric_limits<float>::epsilon());
+  performEdgeThinning(lowerThreshold);
 
+  // // Step 4: hysteresis thresholding
   performHysteresisThresholding(lowerThreshold, upperThreshold);
 
   // // Step 5: edge tracking
@@ -220,65 +221,69 @@ vpCannyEdgeDetection::performFilteringAndGradientComputation(const vpImage<unsig
     vpImageFilter::filterY<float, float>(GIx, Iblur, m_fg.data, m_gaussianKernelSize);
 
     // Computing the gradients
-    vpImageFilter::filter(Iblur, m_dIx, m_gradientFilterX);
-    vpImageFilter::filter(Iblur, m_dIy, m_gradientFilterY);
+    vpImageFilter::filter(Iblur, m_dIx, m_gradientFilterX, true);
+    vpImageFilter::filter(Iblur, m_dIy, m_gradientFilterY, true);
   }
   else {
-    std::string errmsg("Currently, the only filtering and gradient operators are Gaussian blur + Sobel");
+    std::string errmsg("Currently, the filtering operation \"");
+    errmsg += vpImageFilter::vpCannyFilteringAndGradientTypeToString(m_filteringAndGradientType);
+    errmsg += "\" is not handled.";
     throw(vpException(vpException::notImplementedError, errmsg));
   }
 }
 
 /**
- * \brief Get the theta quadrant in which lies absoluteTheta and the offset along the horizontal
- * and vertical direction where to look for the neighbors.
+ * \brief Get the interpolation weights and offsets.
  *
  * \param[in] absoluteTheta : The absolute value of the angle of the edge, expressed in degrees.
- * \param[out] dRowGradPlus : The offset in the vertical positive direction.
- * \param[out] dRowGradMinus : The offset in the vertical negative direction.
- * \param[out] dColGradPlus : The offset in the horizontal positive direction.
- * \param[out] dColGradMinus : The offset in the horizontal negative direction.
- * \return The quadrant in which lies the angle of the edge, expressed in degrees.
+ * \param[out] alpha : The weight of the first point used for the interpolation.
+ * \param[out] beta : The weight of the second point used for the interpolation.
+ * \param[out] dRowGradAlpha : The offset along the row attached to the alpha weight.
+ * \param[out] dRowGradBeta : The offset along the row attached to the beta weight.
+ * \param[out] dColGradAlpha : The offset along the column attached to the alpha weight.
+ * \param[out] dColGradBeta : The offset along the column attached to the beta weight.
  */
-int
-getThetaQuadrant(const float &absoluteTheta, int &dRowGradPlus, int &dRowGradMinus, int &dColGradPlus, int &dColGradMinus)
+void
+getInterpolationWeightsAndOffsets(const float &absoluteTheta,
+                 float &alpha, float &beta,
+                 int &dRowGradAlpha, int &dRowGradBeta,
+                 int &dColGradAlpha, int &dColGradBeta
+)
 {
-  if (absoluteTheta < 22.5) {
-    // Angles between -22.5 and 22.5 are mapped to be horizontal axis
-    dColGradMinus = -1;
-    dColGradPlus = 1;
-    dRowGradPlus = dRowGradMinus = 0;
-    return 0;
+  float thetaMin = 0.f;
+  if (absoluteTheta < 45.f) {
+    // Angles between 0 and 45 deg rely on the horizontal and diagonal points
+    dColGradAlpha = 1;
+    dColGradBeta = 1;
+    dRowGradAlpha = 0;
+    dRowGradBeta = -1;
   }
-  else if (absoluteTheta >= 22.5 && absoluteTheta < 67.5) {
-    // Angles between 22.5 and 67.5 are mapped to the diagonal 45degree
-    dRowGradMinus = dColGradMinus = -1;
-    dRowGradPlus = dColGradPlus = 1;
-    return 45;
+  else if (absoluteTheta >= 45.f && absoluteTheta < 90.f) {
+    // Angles between 45 and 90 deg rely on the diagonal and vertical points
+    thetaMin = 45.f;
+    dColGradAlpha = 1;
+    dColGradBeta = 0;
+    dRowGradAlpha = -1;
+    dRowGradBeta = -1;
   }
-  else if (absoluteTheta >= 67.5 && absoluteTheta < 112.5) {
-    // Angles between 67.5 and 112.5 are mapped to the vertical axis
-    dColGradMinus = dColGradPlus = 0;
-    dRowGradMinus = -1;
-    dRowGradPlus = 1;
-    return 90;
+  else if (absoluteTheta >= 90.f && absoluteTheta < 135.f) {
+    // Angles between 90 and 135 deg rely on the vertical and diagonal points
+    thetaMin = 90.f;
+    dColGradAlpha = 0;
+    dColGradBeta = -1;
+    dRowGradAlpha = -1;
+    dRowGradBeta = -1;
   }
-  else if (absoluteTheta >= 112.5 && absoluteTheta < 157.5) {
-    // Angles between 112.5 and 157.5 are mapped to the diagonal -45degree
-    dRowGradMinus = -1;
-    dColGradMinus = 1;
-    dRowGradPlus = 1;
-    dColGradPlus = -1;
-    return 135;
+  else if (absoluteTheta >= 135.f && absoluteTheta < 180.f) {
+    // Angles between 135 and 180 deg rely on the vertical and diagonal points
+    thetaMin = 135.f;
+    dColGradAlpha = -1;
+    dColGradBeta = -1;
+    dRowGradAlpha = -1;
+    dRowGradBeta = 0;
   }
-  else {
-    // Angles greater than 157.5 are mapped to be horizontal axis
-    dColGradMinus = 1;
-    dColGradPlus = -1;
-    dRowGradMinus = dRowGradPlus = 0;
-    return 180;
-  }
-  return -1; // Should not reach this point
+  beta = (absoluteTheta - thetaMin) / 45.f;
+  alpha = 1.f - beta;
 }
 
 /**
@@ -320,123 +325,55 @@ getManhattanGradient(const vpImage<float> &dIx, const vpImage<float> &dIy, const
 float
 getAbsoluteTheta(const vpImage<float> &dIx, const vpImage<float> &dIy, const int &row, const int &col)
 {
-  float absoluteTheta;
+  float absoluteTheta = 0.f;
   float dx = dIx[row][col];
   float dy = dIy[row][col];
 
   if (std::abs(dx) < std::numeric_limits<float>::epsilon()) {
-    absoluteTheta = 90.;
+    absoluteTheta = 90.f;
   }
   else {
-    absoluteTheta = static_cast<float>(vpMath::deg(std::abs(std::atan(dy / dx))));
+    absoluteTheta = static_cast<float>(vpMath::deg(std::abs(std::atan2(dy, dx))));
   }
   return absoluteTheta;
 }
 
-/**
- * \brief Search in the direction of the gradient for the highest value of the gradient.
- *
- * \param[in] dIx The gradient image along the x-axis.
- * \param[in] dIy The gradient image along the y-axis.
- * \param[in] row The row of the initial point that is considered.
- * \param[in] col The column of the initial point that is considered.
- * \param[in] thetaQuadrant The gradient orientation quadrant of the initial point.
- * \param[in] dRowGrad The direction of the gradient for the vertical direction.
- * \param[in] dColGrad The direction of the gradient for the horizontal direction.
- * \param[out] pixelsSeen The list of pixels that are of same gradient orientation quadrant.
- * \param[out] bestPixel The pixel having the highest absolute value of gradient.
- * \param[out] bestGrad The highest absolute value of gradient.
- */
 void
-searchForBestGradientInGradientDirection(const vpImage<float> &dIx, const vpImage<float> &dIy,
-const int &row, const int &col, const int &thetaQuadrant, const int &dRowGrad, const int &dColGrad,
-std::vector<std::pair<int, int> > &pixelsSeen, std::pair<int, int> &bestPixel, float &bestGrad)
+vpCannyEdgeDetection::performEdgeThinning(const float &lowerThreshold)
 {
-  bool isGradientInTheSameDirection = true;
-  int rowCandidate = row + dRowGrad;
-  int colCandidate = col + dColGrad;
-
-  while (isGradientInTheSameDirection) {
-    // Getting the gradients around the edge point
-    float gradPlus = getManhattanGradient(dIx, dIy, rowCandidate, colCandidate);
-    if (std::abs(gradPlus) < std::numeric_limits<float>::epsilon()) {
-      // The gradient is almost null => ignoring the point
-      isGradientInTheSameDirection = false;
-      break;
-    }
-    int dRowGradPlusCandidate = 0, dRowGradMinusCandidate = 0;
-    int dColGradPlusCandidate = 0, dColGradMinusCandidate = 0;
-    float absThetaPlus = getAbsoluteTheta(dIx, dIy, rowCandidate, colCandidate);
-    int thetaQuadrantCandidate = getThetaQuadrant(absThetaPlus, dRowGradPlusCandidate, dRowGradMinusCandidate, dColGradPlusCandidate, dColGradMinusCandidate);
-    if (thetaQuadrantCandidate != thetaQuadrant) {
-      isGradientInTheSameDirection = false;
-      break;
-    }
-
-    std::pair<int, int> pixelCandidate(rowCandidate, colCandidate);
-    if (gradPlus > bestGrad) {
-      // The gradient is higher with the next pixel candidate
-      // Saving it
-      bestGrad = gradPlus;
-      pixelsSeen.push_back(bestPixel);
-      bestPixel = pixelCandidate;
-    }
-    else {
-      // Best pixel is still the best
-      pixelsSeen.push_back(pixelCandidate);
-    }
-    rowCandidate += dRowGrad;
-    colCandidate += dColGrad;
-  }
-}
-
-void
-vpCannyEdgeDetection::performEdgeThining()
-{
-  vpImage<float> dIx = m_dIx;
-  vpImage<float> dIy = m_dIy;
   int nbRows = m_dIx.getRows();
   int nbCols = m_dIx.getCols();
 
   for (int row = 0; row < nbRows; row++) {
     for (int col = 0; col < nbCols; col++) {
       // Computing the gradient orientation and magnitude
-      float grad = getManhattanGradient(dIx, dIy, row, col);
+      float grad = getManhattanGradient(m_dIx, m_dIy, row, col);
 
-      if (grad < std::numeric_limits<float>::epsilon()) {
-        // The gradient is almost null => ignoring the point
+      if (grad < lowerThreshold) {
+        // The gradient is lower than minimum threshold => ignoring the point
         continue;
       }
 
-      float absoluteTheta = getAbsoluteTheta(dIx, dIy, row, col);
-
       // Getting the offset along the horizontal and vertical axes
       // depending on the gradient orientation
-      int dRowGradPlus = 0, dRowGradMinus = 0;
-      int dColGradPlus = 0, dColGradMinus = 0;
-      int thetaQuadrant = getThetaQuadrant(absoluteTheta, dRowGradPlus, dRowGradMinus, dColGradPlus, dColGradMinus);
-
-      std::vector<std::pair<int, int> > pixelsSeen;
-      std::pair<int, int> bestPixel(row, col);
-      float bestGrad = grad;
-
-      // iterate over all the pixels having the same gradient orientation quadrant
-      searchForBestGradientInGradientDirection(dIx, dIy, row, col, thetaQuadrant, dRowGradPlus, dColGradPlus,
-        pixelsSeen, bestPixel, bestGrad);
-
-      searchForBestGradientInGradientDirection(dIx, dIy, row, col, thetaQuadrant, dRowGradMinus, dColGradMinus,
-        pixelsSeen, bestPixel, bestGrad);
-
-      // Keeping the edge point that has the highest gradient
-      m_edgeCandidateAndGradient[bestPixel] = bestGrad;
-
-      // Suppressing non-maximum gradient
-      for (std::vector<std::pair<int, int> >::iterator it = pixelsSeen.begin(); it != pixelsSeen.end(); it++) {
-        // Suppressing non-maximum gradient
-        int row_temp = it->first;
-        int col_temp = it->second;
-        dIx[row_temp][col_temp] = 0.;
-        dIy[row_temp][col_temp] = 0.;
+      int dRowAlphaPlus = 0, dRowBetaPlus = 0;
+      int dColAphaPlus = 0, dColBetaPlus = 0;
+      float absTheta = getAbsoluteTheta(m_dIx, m_dIy, row, col);
+      float alpha = 0.f, beta = 0.f;
+      getInterpolationWeightsAndOffsets(absTheta, alpha, beta, dRowAlphaPlus, dRowBetaPlus, dColAphaPlus, dColBetaPlus);
+      int dRowAlphaMinus = -dRowAlphaPlus, dRowBetaMinus = -dRowBetaPlus;
+      int dColAphaMinus = -dColAphaPlus, dColBetaMinus = -dColBetaPlus;
+      float gradAlphaPlus = getManhattanGradient(m_dIx, m_dIy, row + dRowAlphaPlus, col + dColAphaPlus);
+      float gradBetaPlus = getManhattanGradient(m_dIx, m_dIy, row + dRowBetaPlus, col + dColBetaPlus);
+      float gradAlphaMinus = getManhattanGradient(m_dIx, m_dIy, row + dRowAlphaMinus, col + dColAphaMinus);
+      float gradBetaMinus = getManhattanGradient(m_dIx, m_dIy, row + dRowBetaMinus, col + dColBetaMinus);
+      float gradPlus = alpha * gradAlphaPlus + beta * gradBetaPlus;
+      float gradMinus = alpha * gradAlphaMinus + beta * gradBetaMinus;
+
+      if (grad >= gradPlus && grad >= gradMinus) {
+        // Keeping the edge point that has the highest gradient
+        std::pair<unsigned int, unsigned int> bestPixel(row, col);
+        m_edgeCandidateAndGradient[bestPixel] = grad;
       }
     }
   }
@@ -482,7 +419,9 @@ vpCannyEdgeDetection::recursiveSearchForStrongEdge(const std::pair<unsigned int,
   for (int dr = -1; dr <= 1 && !hasFoundStrongEdge; dr++) {
     for (int dc = -1; dc <= 1 && !hasFoundStrongEdge; dc++) {
       int idRow = dr + (int)coordinates.first;
+      idRow = std::max(idRow, 0); // Avoid getting negative pixel ID
       int idCol = dc + (int)coordinates.second;
+      idCol = std::max(idCol, 0); // Avoid getting negative pixel ID
 
       // Checking if we are still looking for an edge in the limit of the image
       if ((idRow < 0 || idRow >= nbRows)
@@ -501,6 +440,7 @@ vpCannyEdgeDetection::recursiveSearchForStrongEdge(const std::pair<unsigned int,
           hasFoundStrongEdge = true;
         }
         else if (type_candidate == WEAK_EDGE) {
+          // Checking if the WEAK_EDGE neighbor has a STRONG_EDGE neighbor
           hasFoundStrongEdge = recursiveSearchForStrongEdge(key_candidate);
         }
       }

From 747ab0dc3e008e0fcad2a38106dc04b1dbadbc23 Mon Sep 17 00:00:00 2001
From: rlagneau <romain.lagneau@inria.fr>
Date: Tue, 21 Nov 2023 10:12:28 +0100
Subject: [PATCH 2/3] [TUTO] Permits simultaneous display of the input, Canny
 and gradient images

---
 tutorial/image/drawingHelpers.cpp | 93 +++++++++++++++++--------------
 tutorial/image/drawingHelpers.h   | 77 ++++++++++++-------------
 tutorial/image/tutorial-canny.cpp | 44 +++++++++++----
 3 files changed, 123 insertions(+), 91 deletions(-)

diff --git a/tutorial/image/drawingHelpers.cpp b/tutorial/image/drawingHelpers.cpp
index 98602dc10c..d4c22abd6d 100644
--- a/tutorial/image/drawingHelpers.cpp
+++ b/tutorial/image/drawingHelpers.cpp
@@ -33,37 +33,67 @@
 #include <visp3/core/vpImageConvert.h>
 
 #if defined(VISP_HAVE_X11)
-vpDisplayX drawingHelpers::d;
+vpDisplayX drawingHelpers::d_Iinput;
+vpDisplayX drawingHelpers::d_dIx;
+vpDisplayX drawingHelpers::d_dIy;
+vpDisplayX drawingHelpers::d_IcannyVisp;
+vpDisplayX drawingHelpers::d_IcannyImgFilter;
 #elif defined(HAVE_OPENCV_HIGHGUI)
-vpDisplayOpenCV drawingHelpers::d;
+vpDisplayOpenCV drawingHelpers::d_Iinput;
+vpDisplayOpenCV drawingHelpers::d_dIx;
+vpDisplayOpenCV drawingHelpers::d_dIy;
+vpDisplayOpenCV drawingHelpers::d_IcannyVisp;
+vpDisplayOpenCV drawingHelpers::d_IcannyImgFilter;
 #elif defined(VISP_HAVE_GTK)
-vpDisplayGTK drawingHelpers::d;
+vpDisplayGTK drawingHelpers::d_Iinput;
+vpDisplayGTK drawingHelpers::d_dIx;
+vpDisplayGTK drawingHelpers::d_dIy;
+vpDisplayGTK drawingHelpers::d_IcannyVisp;
+vpDisplayGTK drawingHelpers::d_IcannyImgFilter;
 #elif defined(VISP_HAVE_GDI)
-vpDisplayGDI drawingHelpers::d;
+vpDisplayGDI drawingHelpers::d_Iinput;
+vpDisplayGDI drawingHelpers::d_dIx;
+vpDisplayGDI drawingHelpers::d_dIy;
+vpDisplayGDI drawingHelpers::d_IcannyVisp;
+vpDisplayGDI drawingHelpers::d_IcannyImgFilter;
 #elif defined(VISP_HAVE_D3D9)
-vpDisplayD3D drawingHelpers::d;
+vpDisplayD3D drawingHelpers::d_Iinput;
+vpDisplayD3D drawingHelpers::d_dIx;
+vpDisplayD3D drawingHelpers::d_dIy;
+vpDisplayD3D drawingHelpers::d_IcannyVisp;
+vpDisplayD3D drawingHelpers::d_IcannyImgFilter;
 #endif
 
-vpImage<vpRGBa> drawingHelpers::I_disp;
-
-bool drawingHelpers::display(vpImage<vpRGBa> &I, const std::string &title, const bool &blockingMode)
+void drawingHelpers::init(vpImage<unsigned char> &Iinput, vpImage<unsigned char> &IcannyVisp, vpImage<unsigned char> *p_dIx,
+            vpImage<unsigned char> *p_dIy, vpImage<unsigned char> *p_IcannyimgFilter)
 {
-  I_disp = I;
-#if defined(VISP_HAVE_DISPLAY)
-  if (!d.isInitialised()) {
-    d.init(I_disp);
-    vpDisplay::setTitle(I_disp, title);
+  d_Iinput.init(Iinput, 10, 10);
+  d_IcannyVisp.init(IcannyVisp, 10, Iinput.getHeight() + 10 * 2);
+  if (p_dIx != nullptr) {
+    d_dIx.init(*p_dIx, Iinput.getWidth() + 2 * 10, 10);
   }
-#else
-  (void)title;
-#endif
+  if (p_dIy != nullptr) {
+    d_dIy.init(*p_dIy, 2 * Iinput.getWidth() + 3 * 10, 10);
+  }
+  if (p_IcannyimgFilter != nullptr) {
+    d_IcannyImgFilter.init(*p_IcannyimgFilter, Iinput.getWidth() + 2 * 10, Iinput.getHeight() + 10 * 2);
+  }
+}
 
-  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);
+void drawingHelpers::display(vpImage<unsigned char> &I, const std::string &title)
+{
+  vpDisplay::display(I);
+  vpDisplay::setTitle(I, title);
+  vpDisplay::flush(I);
+}
+
+bool drawingHelpers::waitForClick(const vpImage<unsigned char> &I, const bool &blockingMode)
+{
+  vpDisplay::displayText(I, 15, 15, "Left click to continue...", vpColor::red);
+  vpDisplay::displayText(I, 35, 15, "Right click to stop...", vpColor::red);
+  vpDisplay::flush(I);
   vpMouseButton::vpMouseButtonType button;
-  vpDisplay::getClick(I_disp, button, blockingMode);
+  vpDisplay::getClick(I, button, blockingMode);
   bool hasToContinue = true;
   if (button == vpMouseButton::button3) {
     // Right click => stop the program
@@ -72,24 +102,3 @@ bool drawingHelpers::display(vpImage<vpRGBa> &I, const std::string &title, const
 
   return hasToContinue;
 }
-
-bool drawingHelpers::display(vpImage<unsigned char> &D, const std::string &title, const bool &blockingMode)
-{
-  vpImage<vpRGBa> I; // Image to display
-  vpImageConvert::convert(D, I);
-  return display(I, title, blockingMode);
-}
-
-bool drawingHelpers::display(vpImage<double> &D, const std::string &title, const bool &blockingMode)
-{
-  vpImage<unsigned char> I; // Image to display
-  vpImageConvert::convert(D, I);
-  return display(I, title, blockingMode);
-}
-
-bool drawingHelpers::display(vpImage<float> &F, const std::string &title, const bool &blockingMode)
-{
-  vpImage<unsigned char> I; // Image to display
-  vpImageConvert::convert(F, I);
-  return display(I, title, blockingMode);
-}
diff --git a/tutorial/image/drawingHelpers.h b/tutorial/image/drawingHelpers.h
index e612b20237..55bf4c5eba 100644
--- a/tutorial/image/drawingHelpers.h
+++ b/tutorial/image/drawingHelpers.h
@@ -38,66 +38,67 @@
 namespace drawingHelpers
 {
 #if defined(VISP_HAVE_X11)
-extern vpDisplayX d;
+extern vpDisplayX d_Iinput;
+extern vpDisplayX d_dIx;
+extern vpDisplayX d_dIy;
+extern vpDisplayX d_IcannyVisp;
+extern vpDisplayX d_IcannyImgFilter;
 #elif defined(HAVE_OPENCV_HIGHGUI)
-extern vpDisplayOpenCV d;
+extern vpDisplayOpenCV d_Iinput;
+extern vpDisplayOpenCV d_dIx;
+extern vpDisplayOpenCV d_dIy;
+extern vpDisplayOpenCV d_IcannyVisp;
+extern vpDisplayOpenCV d_IcannyImgFilter
 #elif defined(VISP_HAVE_GTK)
-extern vpDisplayGTK d;
+extern vpDisplayGTK d_Iinput;
+extern vpDisplayGTK d_dIx;
+extern vpDisplayGTK d_dIy;
+extern vpDisplayGTK d_IcannyVisp;
+extern vpDisplayGTK d_IcannyImgFilter
 #elif defined(VISP_HAVE_GDI)
-extern vpDisplayGDI d;
+extern vpDisplayGDI d_Iinput;
+extern vpDisplayGDI d_dIx;
+extern vpDisplayGDI d_dIy;
+extern vpDisplayGDI d_IcannyVisp;
+extern vpDisplayGDI d_IcannyImgFilter
 #elif defined(VISP_HAVE_D3D9)
-extern vpDisplayD3D d;
+extern vpDisplayD3D d_Iinput;
+extern vpDisplayD3D d_dIx;
+extern vpDisplayD3D d_dIy;
+extern vpDisplayD3D d_IcannyVisp;
+extern vpDisplayD3D d_IcannyImgFilter
 #endif
 
-extern vpImage<vpRGBa> I_disp; /*!< Displayed image.*/
-
 /**
- * \brief Display a RGB image and catch the user clicks to know if
- * the user wants to stop the program.
+ * \brief Initialize the different displays.
  *
- * \param[out] I The RGB image to display.
- * \param[in] title The title of the window.
- * \param[in] blockingMode If true, wait for a click to switch to the next image.
- * \return true The user wants to continue the application.
- * \return false The user wants to stop the application.
+ * \param[out] Iinput Input image of the program.
+ * \param[out] IcannyVisp Image resulting from the vpCannyEdgeDetection method.
+ * \param[out] p_dIx If different from nullptr, pointer towards the gradient along the horizontal axis.
+ * \param[out] p_dIy If different from nullptr, pointer towards the gradient along the vertical axis.
+ * \param[out] p_IcannyimgFilter If different from nullptr, pointer towards the result of the vpImageFilter::canny
+ * method.
  */
-bool display(vpImage<vpRGBa> &I, const std::string &title, const bool &blockingMode);
+  void init(vpImage<unsigned char> &Iinput, vpImage<unsigned char> &IcannyVisp, vpImage<unsigned char> *p_dIx,
+            vpImage<unsigned char> *p_dIy, vpImage<unsigned char> *p_IcannyimgFilter);
 
 /**
- * \brief Display a gray-scale image and catch the user clicks to know if
- * the user wants to stop the program.
+ * \brief Display a gray-scale image.
  *
  * \param[out] I The gray-scale image to display.
  * \param[in] title The title of the window.
- * \param[in] blockingMode If true, wait for a click to switch to the next image.
- * \return true The user wants to continue the application.
- * \return false The user wants to stop the application.
  */
-bool display(vpImage<unsigned char> &I, const std::string &title, const bool &blockingMode);
+void display(vpImage<unsigned char> &I, const std::string &title);
 
 /**
- * \brief Display a double precision image and catch the user clicks to know if
- * the user wants to stop the program.
+ * \brief Catch the user clicks to know if the user wants to stop the program.
  *
- * \param[out] D The double precision image to display.
- * \param[in] title The title of the window.
- * \param[in] blockingMode If true, wait for a click to switch to the next image.
- * \return true The user wants to continue the application.
- * \return false The user wants to stop the application.
- */
-bool display(vpImage<double> &D, const std::string &title, const bool &blockingMode);
-
-/**
- * \brief Display a floating-point precision image and catch the user clicks to know if
- * the user wants to stop the program.
- *
- * \param[out] F The floating-point precision image to display.
- * \param[in] title The title of the window.
+ * \param[in] I The gray-scale image to display.
  * \param[in] blockingMode If true, wait for a click to switch to the next image.
  * \return true The user wants to continue the application.
  * \return false The user wants to stop the application.
  */
-bool display(vpImage<float> &F, const std::string &title, const bool &blockingMode);
+bool waitForClick(const vpImage<unsigned char> &I, const bool &blockingMode);
 }
 
 #endif
diff --git a/tutorial/image/tutorial-canny.cpp b/tutorial/image/tutorial-canny.cpp
index 2e74f9ac36..c5ece751ae 100644
--- a/tutorial/image/tutorial-canny.cpp
+++ b/tutorial/image/tutorial-canny.cpp
@@ -66,8 +66,11 @@ void computeMeanMaxStdev(const vpImage<T> &I, float &mean, float &max, float &st
   stdev = std::sqrt(stdev);
 }
 
-void setGradientOutsideClass(const vpImage<unsigned char> &I, const int &gaussianKernelSize, const float &gaussianStdev, vpCannyEdgeDetection &cannyDetector,
-                             const unsigned int apertureSize, const vpImageFilter::vpCannyFilteringAndGradientType &filteringType)
+void setGradientOutsideClass(const vpImage<unsigned char> &I, const int &gaussianKernelSize, const float &gaussianStdev,
+                             vpCannyEdgeDetection &cannyDetector, const unsigned int apertureSize,
+                             const vpImageFilter::vpCannyFilteringAndGradientType &filteringType,
+                             vpImage<unsigned char> &dIx_uchar, vpImage<unsigned char> &dIy_uchar
+)
 {
   // Computing the gradients
   vpImage<float> dIx, dIy;
@@ -82,11 +85,13 @@ void setGradientOutsideClass(const vpImage<unsigned char> &I, const int &gaussia
   computeMeanMaxStdev(dIx, mean, max, stdev);
   std::string title = "Gradient along the horizontal axis. Mean = " + std::to_string(mean)
     + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max);
-  drawingHelpers::display(dIx, title, true);
+  vpImageConvert::convert(dIx, dIx_uchar);
+  drawingHelpers::display(dIx_uchar, title);
   computeMeanMaxStdev(dIy, mean, max, stdev);
   title = "Gradient along the horizontal axis. Mean = " + std::to_string(mean)
     + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max);
-  drawingHelpers::display(dIy, title, true);
+  vpImageConvert::convert(dIy, dIy_uchar);
+  drawingHelpers::display(dIy_uchar, title);
 }
 
 void usage(const std::string &softName, int gaussianKernelSize, float gaussianStdev, float lowerThresh, float upperThresh,
@@ -222,7 +227,7 @@ int main(int argc, const char *argv[])
   vpCannyEdgeDetection cannyDetector(opt_gaussianKernelSize, opt_gaussianStdev, opt_apertureSize,
                                      opt_lowerThresh, opt_upperThresh, opt_lowerThreshRatio, opt_upperThreshRatio,
                                      opt_filteringType);
-  vpImage<unsigned char> I_canny_input;
+  vpImage<unsigned char> I_canny_input, I_canny_visp, dIx_uchar, dIy_uchar, I_canny_imgFilter;
   if (!opt_img.empty()) {
     // Detection on the user image
     vpImageIo::read(I_canny_input, opt_img);
@@ -237,24 +242,41 @@ int main(int argc, const char *argv[])
     }
   }
 
+  // Initialization of the displays
+  I_canny_visp = I_canny_imgFilter = dIx_uchar = dIy_uchar = I_canny_input;
+  vpImage<unsigned char> *p_dIx = nullptr, *p_dIy = nullptr, *p_IcannyImgFilter = nullptr;
+
+  if (opt_gradientOutsideClass) {
+    p_dIx = &dIx_uchar;
+    p_dIy = &dIy_uchar;
+  }
+
+  if (opt_useVpImageFilterCanny) {
+    p_IcannyImgFilter = &I_canny_imgFilter;
+  }
+  drawingHelpers::init(I_canny_input, I_canny_visp, p_dIx, p_dIy, p_IcannyImgFilter);
+
+  // Computing the gradient outside the vpCannyEdgeDetection class if asked
   if (opt_gradientOutsideClass) {
-    setGradientOutsideClass(I_canny_input, opt_gaussianKernelSize, opt_gaussianStdev, cannyDetector, opt_apertureSize, opt_filteringType);
+    setGradientOutsideClass(I_canny_input, opt_gaussianKernelSize, opt_gaussianStdev, cannyDetector, opt_apertureSize,
+                            opt_filteringType, dIx_uchar, dIy_uchar);
   }
-  vpImage<unsigned char> I_canny = cannyDetector.detect(I_canny_input);
+  I_canny_visp = cannyDetector.detect(I_canny_input);
   float mean, max, stdev;
   computeMeanMaxStdev(I_canny_input, mean, max, stdev);
   std::string title("Input of the Canny edge detector. Mean = " + std::to_string(mean) + "+/-" + std::to_string(stdev) + " Max = " + std::to_string(max));
-  drawingHelpers::display(I_canny_input, title, true);
-  drawingHelpers::display(I_canny, "Canny results on image " + opt_img, true);
+  drawingHelpers::display(I_canny_input, title);
+  drawingHelpers::display(I_canny_visp, "Canny results on image " + opt_img);
 
   if (opt_useVpImageFilterCanny) {
     float cannyThresh = opt_upperThresh;
     float lowerThresh(opt_lowerThresh);
-    vpImageFilter::canny(I_canny_input, I_canny, opt_gaussianKernelSize, lowerThresh, cannyThresh,
+    vpImageFilter::canny(I_canny_input, I_canny_imgFilter, opt_gaussianKernelSize, lowerThresh, cannyThresh,
                          opt_apertureSize, opt_gaussianStdev, opt_lowerThreshRatio, opt_upperThreshRatio, true,
                          opt_backend, opt_filteringType);
-    drawingHelpers::display(I_canny, "Canny results with \"" + vpImageFilter::vpCannyBackendTypeToString(opt_backend) + "\" backend", true);
+    drawingHelpers::display(I_canny_imgFilter, "Canny results with \"" + vpImageFilter::vpCannyBackendTypeToString(opt_backend) + "\" backend");
   }
 
+  drawingHelpers::waitForClick(I_canny_input, true);
   return EXIT_SUCCESS;
 }

From 7e4437123c838dec2b5b6ce2bcd5500396f93767 Mon Sep 17 00:00:00 2001
From: rlagneau <romain.lagneau@inria.fr>
Date: Tue, 21 Nov 2023 15:19:57 +0100
Subject: [PATCH 3/3] [TUTO-FIX] Fix the tutorial tutorial-canny on Windows CI

---
 tutorial/image/drawingHelpers.h | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tutorial/image/drawingHelpers.h b/tutorial/image/drawingHelpers.h
index 55bf4c5eba..cc2619554a 100644
--- a/tutorial/image/drawingHelpers.h
+++ b/tutorial/image/drawingHelpers.h
@@ -48,25 +48,25 @@ extern vpDisplayOpenCV d_Iinput;
 extern vpDisplayOpenCV d_dIx;
 extern vpDisplayOpenCV d_dIy;
 extern vpDisplayOpenCV d_IcannyVisp;
-extern vpDisplayOpenCV d_IcannyImgFilter
+extern vpDisplayOpenCV d_IcannyImgFilter;
 #elif defined(VISP_HAVE_GTK)
 extern vpDisplayGTK d_Iinput;
 extern vpDisplayGTK d_dIx;
 extern vpDisplayGTK d_dIy;
 extern vpDisplayGTK d_IcannyVisp;
-extern vpDisplayGTK d_IcannyImgFilter
+extern vpDisplayGTK d_IcannyImgFilter;
 #elif defined(VISP_HAVE_GDI)
 extern vpDisplayGDI d_Iinput;
 extern vpDisplayGDI d_dIx;
 extern vpDisplayGDI d_dIy;
 extern vpDisplayGDI d_IcannyVisp;
-extern vpDisplayGDI d_IcannyImgFilter
+extern vpDisplayGDI d_IcannyImgFilter;
 #elif defined(VISP_HAVE_D3D9)
 extern vpDisplayD3D d_Iinput;
 extern vpDisplayD3D d_dIx;
 extern vpDisplayD3D d_dIy;
 extern vpDisplayD3D d_IcannyVisp;
-extern vpDisplayD3D d_IcannyImgFilter
+extern vpDisplayD3D d_IcannyImgFilter;
 #endif
 
 /**
@@ -79,8 +79,8 @@ extern vpDisplayD3D d_IcannyImgFilter
  * \param[out] p_IcannyimgFilter If different from nullptr, pointer towards the result of the vpImageFilter::canny
  * method.
  */
-  void init(vpImage<unsigned char> &Iinput, vpImage<unsigned char> &IcannyVisp, vpImage<unsigned char> *p_dIx,
-            vpImage<unsigned char> *p_dIy, vpImage<unsigned char> *p_IcannyimgFilter);
+void init(vpImage<unsigned char> &Iinput, vpImage<unsigned char> &IcannyVisp, vpImage<unsigned char> *p_dIx,
+          vpImage<unsigned char> *p_dIy, vpImage<unsigned char> *p_IcannyimgFilter);
 
 /**
  * \brief Display a gray-scale image.