imgproc: add IntelligentScissors
authorAlexander Alekhin <alexander.a.alekhin@gmail.com>
Wed, 16 Dec 2020 00:53:52 +0000 (00:53 +0000)
committerAlexander Alekhin <alexander.a.alekhin@gmail.com>
Fri, 25 Dec 2020 10:57:11 +0000 (10:57 +0000)
14 files changed:
doc/js_tutorials/js_assets/js_intelligent_scissors.html [new file with mode: 0644]
doc/js_tutorials/js_imgproc/js_intelligent_scissors/js_intelligent_scissors.markdown [new file with mode: 0644]
doc/js_tutorials/js_imgproc/js_table_of_contents_imgproc.markdown
doc/opencv.bib
modules/imgproc/include/opencv2/imgproc.hpp
modules/imgproc/include/opencv2/imgproc/segmentation.hpp [new file with mode: 0644]
modules/imgproc/src/intelligent_scissors.cpp [new file with mode: 0644]
modules/imgproc/test/test_intelligent_scissors.cpp [new file with mode: 0644]
modules/js/src/core_bindings.cpp
modules/js/test/test_imgproc.js
platforms/js/opencv_js.config.py
samples/cpp/CMakeLists.txt
samples/cpp/tutorial_code/snippets/imgproc_segmentation.cpp [new file with mode: 0644]
samples/samples_utils.cmake

diff --git a/doc/js_tutorials/js_assets/js_intelligent_scissors.html b/doc/js_tutorials/js_assets/js_intelligent_scissors.html
new file mode 100644 (file)
index 0000000..1782dc6
--- /dev/null
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Intelligent Scissors Example</title>
+<link href="js_example_style.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<h2>Intelligent Scissors Example</h2>
+<p>
+    Click <b>Start</b> button to launch the code below.<br>
+    Then click on image to pick source point. After that you can hover mouse pointer over canvas to specify target point candidate.<br>
+    You can change the code in the &lt;textarea&gt; to investigate more. You can choose another image (need to "Stop" first).
+</p>
+<div>
+<div class="control"><button id="tryIt" disabled>Start</button> <button id="stopIt" disabled>Stop</button></div>
+<textarea class="code" rows="20" cols="100" id="codeEditor" spellcheck="false">
+</textarea>
+<p class="err" id="errorMessage"></p>
+</div>
+<div id="inputParams">
+  <div class="caption">canvasInput <input type="file" id="fileInput" name="file" accept="image/*" /></div>
+  <canvas id="canvasInput"></canvas>
+</div>
+<div id="result" style="display:none">
+  <canvas id="canvasOutput"></canvas>
+</div>
+
+<script src="utils.js" type="text/javascript"></script>
+
+<script id="codeSnippet" type="text/code-snippet">
+let src = cv.imread('canvasInput');
+//cv.resize(src, src, new cv.Size(1024, 1024));
+cv.imshow('canvasOutput', src);
+
+let tool = new cv.segmentation_IntelligentScissorsMB();
+tool.setEdgeFeatureCannyParameters(32, 100);
+tool.setGradientMagnitudeMaxLimit(200);
+tool.applyImage(src);
+
+let hasMap = false;
+
+let canvas = document.getElementById('canvasOutput');
+canvas.addEventListener('click', e => {
+    let startX = e.offsetX, startY = e.offsetY; console.log(startX, startY);
+    if (startX < src.cols && startY < src.rows)
+    {
+        console.time('buildMap');
+        tool.buildMap(new cv.Point(startX, startY));
+        console.timeEnd('buildMap');
+        hasMap = true;
+    }
+});
+canvas.addEventListener('mousemove', e => {
+    let x = e.offsetX, y = e.offsetY; //console.log(x, y);
+    let dst = src.clone();
+    if (hasMap && x >= 0 && x < src.cols && y >= 0 && y < src.rows)
+    {
+        let contour = new cv.Mat();
+        tool.getContour(new cv.Point(x, y), contour);
+        let contours = new cv.MatVector();
+        contours.push_back(contour);
+        let color = new cv.Scalar(0, 255, 0, 255);  // RGBA
+        cv.polylines(dst, contours, false, color, 1, cv.LINE_8);
+        contours.delete(); contour.delete();
+    }
+    cv.imshow('canvasOutput', dst);
+    dst.delete();
+});
+canvas.addEventListener('dispose', e => {
+    src.delete();
+    tool.delete();
+});
+</script>
+
+<script type="text/javascript">
+let utils = new Utils('errorMessage');
+
+utils.loadCode('codeSnippet', 'codeEditor');
+utils.loadImageToCanvas('lena.jpg', 'canvasInput');
+utils.addFileInputHandler('fileInput', 'canvasInput');
+
+let disposeEvent = new Event('dispose');
+
+let tryIt = document.getElementById('tryIt');
+let stopIt = document.getElementById('stopIt');
+
+tryIt.addEventListener('click', () => {
+    let e_input = document.getElementById('inputParams');
+    e_input.style.display = 'none';
+
+    let e_result = document.getElementById("result")
+    e_result.style.display = '';
+
+    var e = document.getElementById("canvasOutput");
+    var e_new = e.cloneNode(true);
+    e.parentNode.replaceChild(e_new, e);  // reset event handlers
+
+    stopIt.removeAttribute('disabled');
+    tryIt.setAttribute('disabled', '');
+
+    utils.executeCode('codeEditor');
+});
+
+stopIt.addEventListener('click', () => {
+    let e_input = document.getElementById('inputParams');
+    e_input.style.display = '';
+
+    let e_result = document.getElementById("result")
+    e_result.style.display = 'none';
+
+    var e = document.getElementById("canvasOutput");
+    e.dispatchEvent(disposeEvent);
+
+    var e_new = e.cloneNode(true);
+    e.parentNode.replaceChild(e_new, e);  // reset event handlers
+
+    tryIt.removeAttribute('disabled');
+    stopIt.setAttribute('disabled', '');
+});
+
+utils.loadOpenCv(() => {
+    tryIt.removeAttribute('disabled');
+});
+</script>
+</body>
+</html>
diff --git a/doc/js_tutorials/js_imgproc/js_intelligent_scissors/js_intelligent_scissors.markdown b/doc/js_tutorials/js_imgproc/js_intelligent_scissors/js_intelligent_scissors.markdown
new file mode 100644 (file)
index 0000000..97ffca5
--- /dev/null
@@ -0,0 +1,14 @@
+Intelligent Scissors Demo {#tutorial_js_intelligent_scissors}
+=========================
+
+Goal
+----
+
+- Here you can check how to use IntelligentScissors tool for image segmentation task.
+- Available methods and parameters: @ref cv::segmentation::IntelligentScissorsMB
+
+\htmlonly
+<iframe src="../../js_intelligent_scissors.html" width="100%"
+        onload="this.style.height=this.contentDocument.body.scrollHeight +'px';">
+</iframe>
+\endhtmlonly
index 3bb809b..b06eb95 100644 (file)
@@ -77,3 +77,7 @@ Image Processing {#tutorial_js_table_of_contents_imgproc}
 -   @subpage tutorial_js_imgproc_camera
 
     Learn image processing for video capture.
+
+-   @subpage tutorial_js_intelligent_scissors
+
+    Learn how to use IntelligentScissors tool for image segmentation task.
index 6212ea5..d3d6f78 100644 (file)
   pages = {432--441},
   publisher = {Springer}
 }
+@INPROCEEDINGS{Mortensen95intelligentscissors,
+  author = {Eric N. Mortensen and William A. Barrett},
+  title = {Intelligent Scissors for Image Composition},
+  booktitle = {In Computer Graphics, SIGGRAPH Proceedings},
+  year = {1995},
+  pages = {191--198}
+}
 @inproceedings{Muja2009,
   author = {Muja, Marius and Lowe, David G},
   title = {Fast Approximate Nearest Neighbors with Automatic Algorithm Configuration},
index c607a18..533c223 100644 (file)
@@ -185,6 +185,7 @@ location of points on the plane, building special graphs (such as NNG,RNG), and
     @defgroup imgproc_motion Motion Analysis and Object Tracking
     @defgroup imgproc_feature Feature Detection
     @defgroup imgproc_object Object Detection
+    @defgroup imgproc_segmentation Image Segmentation
     @defgroup imgproc_c C API
     @defgroup imgproc_hal Hardware Acceleration Layer
     @{
@@ -3227,6 +3228,9 @@ CV_EXPORTS_AS(EMD) float wrapperEMD( InputArray signature1, InputArray signature
 
 //! @} imgproc_hist
 
+//! @addtogroup imgproc_segmentation
+//! @{
+
 /** @example samples/cpp/watershed.cpp
 An example using the watershed algorithm
 */
@@ -3254,11 +3258,11 @@ function.
 size as image .
 
 @sa findContours
-
-@ingroup imgproc_misc
  */
 CV_EXPORTS_W void watershed( InputArray image, InputOutputArray markers );
 
+//! @} imgproc_segmentation
+
 //! @addtogroup imgproc_filter
 //! @{
 
@@ -3304,7 +3308,7 @@ CV_EXPORTS_W void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
 
 //! @}
 
-//! @addtogroup imgproc_misc
+//! @addtogroup imgproc_segmentation
 //! @{
 
 /** @example samples/cpp/grabcut.cpp
@@ -3334,6 +3338,11 @@ CV_EXPORTS_W void grabCut( InputArray img, InputOutputArray mask, Rect rect,
                            InputOutputArray bgdModel, InputOutputArray fgdModel,
                            int iterCount, int mode = GC_EVAL );
 
+//! @} imgproc_segmentation
+
+//! @addtogroup imgproc_misc
+//! @{
+
 /** @example samples/cpp/distrans.cpp
 An example on using the distance transform
 */
@@ -4876,4 +4885,8 @@ Point LineIterator::pos() const
 
 } // cv
 
+
+#include "./imgproc/segmentation.hpp"
+
+
 #endif
diff --git a/modules/imgproc/include/opencv2/imgproc/segmentation.hpp b/modules/imgproc/include/opencv2/imgproc/segmentation.hpp
new file mode 100644 (file)
index 0000000..26882f4
--- /dev/null
@@ -0,0 +1,141 @@
+// This file is part of OpenCV project.
+// It is subject to the license terms in the LICENSE file found in the top-level directory
+// of this distribution and at http://opencv.org/license.html.
+
+#ifndef OPENCV_IMGPROC_SEGMENTATION_HPP
+#define OPENCV_IMGPROC_SEGMENTATION_HPP
+
+#include "opencv2/imgproc.hpp"
+
+namespace cv {
+
+namespace segmentation {
+
+//! @addtogroup imgproc_segmentation
+//! @{
+
+
+/** @brief Intelligent Scissors image segmentation
+ *
+ * This class is used to find the path (contour) between two points
+ * which can be used for image segmentation.
+ *
+ * Usage example:
+ * @snippet snippets/imgproc_segmentation.cpp usage_example_intelligent_scissors
+ *
+ * Reference: <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.138.3811&rep=rep1&type=pdf">"Intelligent Scissors for Image Composition"</a>
+ * algorithm designed by Eric N. Mortensen and William A. Barrett, Brigham Young University
+ * @cite Mortensen95intelligentscissors
+ */
+class CV_EXPORTS_W_SIMPLE IntelligentScissorsMB
+{
+public:
+    CV_WRAP
+    IntelligentScissorsMB();
+
+    /** @brief Specify weights of feature functions
+     *
+     * Consider keeping weights normalized (sum of weights equals to 1.0)
+     * Discrete dynamic programming (DP) goal is minimization of costs between pixels.
+     *
+     * @param weight_non_edge Specify cost of non-edge pixels (default: 0.43f)
+     * @param weight_gradient_direction Specify cost of gradient direction function (default: 0.43f)
+     * @param weight_gradient_magnitude Specify cost of gradient magnitude function (default: 0.14f)
+     */
+    CV_WRAP
+    IntelligentScissorsMB& setWeights(float weight_non_edge, float weight_gradient_direction, float weight_gradient_magnitude);
+
+    /** @brief Specify gradient magnitude max value threshold
+     *
+     * Zero limit value is used to disable gradient magnitude thresholding (default behavior, as described in original article).
+     * Otherwize pixels with `gradient magnitude >= threshold` have zero cost.
+     *
+     * @note Thresholding should be used for images with irregular regions (to avoid stuck on parameters from high-contract areas, like embedded logos).
+     *
+     * @param gradient_magnitude_threshold_max Specify gradient magnitude max value threshold (default: 0, disabled)
+     */
+    CV_WRAP
+    IntelligentScissorsMB& setGradientMagnitudeMaxLimit(float gradient_magnitude_threshold_max = 0.0f);
+
+    /** @brief Switch to "Laplacian Zero-Crossing" edge feature extractor and specify its parameters
+     *
+     * This feature extractor is used by default according to article.
+     *
+     * Implementation has additional filtering for regions with low-amplitude noise.
+     * This filtering is enabled through parameter of minimal gradient amplitude (use some small value 4, 8, 16).
+     *
+     * @note Current implementation of this feature extractor is based on processing of grayscale images (color image is converted to grayscale image first).
+     *
+     * @note Canny edge detector is a bit slower, but provides better results (especially on color images): use setEdgeFeatureCannyParameters().
+     *
+     * @param gradient_magnitude_min_value Minimal gradient magnitude value for edge pixels (default: 0, check is disabled)
+     */
+    CV_WRAP
+    IntelligentScissorsMB& setEdgeFeatureZeroCrossingParameters(float gradient_magnitude_min_value = 0.0f);
+
+    /** @brief Switch edge feature extractor to use Canny edge detector
+     *
+     * @note "Laplacian Zero-Crossing" feature extractor is used by default (following to original article)
+     *
+     * @sa Canny
+     */
+    CV_WRAP
+    IntelligentScissorsMB& setEdgeFeatureCannyParameters(
+            double threshold1, double threshold2,
+            int apertureSize = 3, bool L2gradient = false
+    );
+
+    /** @brief Specify input image and extract image features
+     *
+     * @param image input image. Type is #CV_8UC1 / #CV_8UC3
+     */
+    CV_WRAP
+    IntelligentScissorsMB& applyImage(InputArray image);
+
+    /** @brief Specify custom features of imput image
+     *
+     * Customized advanced variant of applyImage() call.
+     *
+     * @param non_edge Specify cost of non-edge pixels. Type is CV_8UC1. Expected values are `{0, 1}`.
+     * @param gradient_direction Specify gradient direction feature. Type is CV_32FC2. Values are expected to be normalized: `x^2 + y^2 == 1`
+     * @param gradient_magnitude Specify cost of gradient magnitude function: Type is CV_32FC1. Values should be in range `[0, 1]`.
+     * @param image **Optional parameter**. Must be specified if subset of features is specified (non-specified features are calculated internally)
+     */
+    CV_WRAP
+    IntelligentScissorsMB& applyImageFeatures(
+            InputArray non_edge, InputArray gradient_direction, InputArray gradient_magnitude,
+            InputArray image = noArray()
+    );
+
+    /** @brief Prepares a map of optimal paths for the given source point on the image
+     *
+     * @note applyImage() / applyImageFeatures() must be called before this call
+     *
+     * @param sourcePt The source point used to find the paths
+     */
+    CV_WRAP void buildMap(const Point& sourcePt);
+
+    /** @brief Extracts optimal contour for the given target point on the image
+     *
+     * @note buildMap() must be called before this call
+     *
+     * @param targetPt The target point
+     * @param[out] contour The list of pixels which contains optimal path between the source and the target points of the image. Type is CV_32SC2 (compatible with `std::vector<Point>`)
+     * @param backward Flag to indicate reverse order of retrived pixels (use "true" value to fetch points from the target to the source point)
+     */
+    CV_WRAP void getContour(const Point& targetPt, OutputArray contour, bool backward = false) const;
+
+#ifndef CV_DOXYGEN
+    struct Impl;
+    inline Impl* getImpl() const { return impl.get(); }
+protected:
+    std::shared_ptr<Impl> impl;
+#endif
+};
+
+//! @}
+
+}  // namespace segmentation
+}  // namespace cv
+
+#endif // OPENCV_IMGPROC_SEGMENTATION_HPP
diff --git a/modules/imgproc/src/intelligent_scissors.cpp b/modules/imgproc/src/intelligent_scissors.cpp
new file mode 100644 (file)
index 0000000..38acfd7
--- /dev/null
@@ -0,0 +1,772 @@
+// This file is part of OpenCV project.
+// It is subject to the license terms in the LICENSE file found in the top-level directory
+// of this distribution and at http://opencv.org/license.html.
+//
+// Copyright (C) 2020, Intel Corporation, all rights reserved.
+// Third party copyrights are property of their respective owners.
+
+
+#include "precomp.hpp"
+//#include "opencv2/imgproc/segmentation.hpp"
+
+#include <opencv2/core/utils/logger.hpp>
+
+#include <queue>  // std::priority_queue
+
+namespace cv {
+namespace segmentation {
+
+namespace {
+
+// 0 1 2
+// 3 x 4
+// 5 6 7
+static const int neighbors[8][2] = {
+    { -1, -1 },
+    {  0, -1 },
+    {  1, -1 },
+    { -1,  0 },
+    {  1,  0 },
+    { -1,  1 },
+    {  0,  1 },
+    {  1,  1 },
+};
+
+// encoded reverse direction
+static const int neighbors_encode[8] = {
+    7+1, 6+1, 5+1,
+    4+1,      3+1,
+    2+1, 1+1, 0+1
+};
+
+#define ACOS_TABLE_SIZE 64
+// acos_table[x + ACOS_TABLE_SIZE] = acos(x / ACOS_TABLE_SIZE) / CV_PI (see local_cost)
+//    x = [ -ACOS_TABLE_SIZE .. ACOS_TABLE_SIZE ]
+float* getAcosTable()
+{
+    constexpr int N = ACOS_TABLE_SIZE;
+    static bool initialized = false;
+    static float acos_table[2*N + 1] = { 0 };
+    if (!initialized)
+    {
+        const float CV_PI_inv = static_cast<float>(1.0 / CV_PI);
+        for (int i = -N; i <= N; i++)
+        {
+           acos_table[i + N] = acosf(i / (float)N) * CV_PI_inv;
+        }
+        initialized = true;
+    }
+    return acos_table;
+}
+
+} // namespace anon
+
+struct IntelligentScissorsMB::Impl
+{
+    // proposed weights from the article (sum = 1.0)
+    float weight_non_edge = 0.43f;
+    float weight_gradient_direction = 0.43f;
+    float weight_gradient_magnitude = 0.14f;
+
+    enum EdgeFeatureMode {
+        FEATURE_ZERO_CROSSING = 0,
+        FEATURE_CANNY
+    };
+    EdgeFeatureMode edge_mode = FEATURE_ZERO_CROSSING;
+
+    // FEATURE_ZERO_CROSSING
+    float edge_gradient_magnitude_min_value = 0.0f;
+
+    // FEATURE_CANNY
+    double edge_canny_threshold1 = 10;
+    double edge_canny_threshold2 = 100;
+    int edge_canny_apertureSize = 3;
+    bool edge_canny_L2gradient = false;
+
+
+    float gradient_magnitude_threshold_max = 0.0f;  // disabled thresholding
+
+    int sobelKernelSize = 3;  // 1 or 3
+    int laplacianKernelSize = 3;  // 1 or 3
+
+    // image features
+    Mat_<Point2f> gradient_direction;  //< I: normalized laplacian x/y components
+    Mat_<float> gradient_magnitude;  //< Fg: gradient cost function
+    Mat_<uchar> non_edge_feature;  //< Fz: zero-crossing function
+
+    float weight_non_edge_compute = 0.0f;
+
+    // encoded paths map (produced by `buildMap()`)
+    Mat_<uchar> optimalPathsMap;
+
+    void resetFeatures_()
+    {
+        CV_TRACE_FUNCTION();
+
+        gradient_direction.release();
+        gradient_magnitude.release();
+        non_edge_feature.release();
+
+        weight_non_edge_compute = weight_non_edge;
+
+        optimalPathsMap.release();
+    }
+
+    Size src_size;
+    Mat image_;
+    Mat grayscale_;
+    void initImage_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+        if (!image_.empty())
+            return;
+        CV_CheckType(image.type(), image.type() == CV_8UC1 || image.type() == CV_8UC3 || image.type() == CV_8UC4, "");
+        src_size = image.size();
+        image_ = image.getMat();
+    }
+    void initGrayscale_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+        if (!grayscale_.empty())
+            return;
+        CV_Assert(!image.empty());
+        CV_CheckType(image.type(), image.type() == CV_8UC1 || image.type() == CV_8UC3 || image.type() == CV_8UC4, "");
+        src_size = image.size();
+        if (image.channels() > 1)
+            cvtColor(image, grayscale_, COLOR_BGR2GRAY);
+        else
+            grayscale_ = image.getMat();
+    }
+    Mat Ix_, Iy_;
+    void initImageDerives_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+        if (!Ix_.empty())
+            return;
+        initGrayscale_(image);
+        Sobel(grayscale_, Ix_, CV_32FC1, 1, 0, sobelKernelSize);
+        Sobel(grayscale_, Iy_, CV_32FC1, 0, 1, sobelKernelSize);
+    }
+    Mat image_magnitude_;
+    void initImageMagnitude_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+        if (!image_magnitude_.empty())
+            return;
+        initImageDerives_(image);
+        magnitude(Ix_, Iy_, image_magnitude_);
+    }
+
+    void cleanupFeaturesTemporaryArrays_()
+    {
+        CV_TRACE_FUNCTION();
+        image_.release();
+        grayscale_.release();
+        Ix_.release();
+        Iy_.release();
+        image_magnitude_.release();
+    }
+
+    Impl()
+    {
+        // nothing
+        CV_TRACE_FUNCTION();
+    }
+
+    void setWeights(float weight_non_edge_, float weight_gradient_direction_, float weight_gradient_magnitude_)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_CheckGE(weight_non_edge_, 0.0f, "");
+        CV_CheckGE(weight_gradient_direction_, 0.0f, "");
+        CV_CheckGE(weight_gradient_magnitude_, 0.0f, "");
+        CV_CheckGE(weight_non_edge_ + weight_gradient_direction_ + weight_gradient_magnitude_, FLT_EPSILON, "Sum of weights must be greater than zero");
+        weight_non_edge = weight_non_edge_;
+        weight_gradient_direction = weight_gradient_direction_;
+        weight_gradient_magnitude = weight_gradient_magnitude_;
+        resetFeatures_();
+    }
+
+    void setGradientMagnitudeMaxLimit(float gradient_magnitude_threshold_max_)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_CheckGE(gradient_magnitude_threshold_max_, 0.0f, "");
+        gradient_magnitude_threshold_max = gradient_magnitude_threshold_max_;
+        resetFeatures_();
+    }
+
+    void setEdgeFeatureZeroCrossingParameters(float gradient_magnitude_min_value_)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_CheckGE(gradient_magnitude_min_value_, 0.0f, "");
+        edge_mode = FEATURE_ZERO_CROSSING;
+        edge_gradient_magnitude_min_value = gradient_magnitude_min_value_;
+        resetFeatures_();
+    }
+
+    void setEdgeFeatureCannyParameters(
+            double threshold1, double threshold2,
+            int apertureSize = 3, bool L2gradient = false
+    )
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_CheckGE(threshold1, 0.0, "");
+        CV_CheckGE(threshold2, 0.0, "");
+        edge_mode = FEATURE_CANNY;
+        edge_canny_threshold1 = threshold1;
+        edge_canny_threshold2 = threshold2;
+        edge_canny_apertureSize = apertureSize;
+        edge_canny_L2gradient = L2gradient;
+        resetFeatures_();
+    }
+
+    void applyImageFeatures(
+            InputArray non_edge, InputArray gradient_direction_, InputArray gradient_magnitude_,
+            InputArray image
+    )
+    {
+        CV_TRACE_FUNCTION();
+
+        resetFeatures_();
+        cleanupFeaturesTemporaryArrays_();
+
+        src_size = Size(0, 0);
+        if (!non_edge.empty())
+            src_size = non_edge.size();
+        if (!gradient_direction_.empty())
+        {
+            Size gradient_direction_size = gradient_direction_.size();
+            if (!src_size.empty())
+                CV_CheckEQ(src_size, gradient_direction_size, "");
+            else
+                src_size = gradient_direction_size;
+        }
+        if (!gradient_magnitude_.empty())
+        {
+            Size gradient_magnitude_size = gradient_magnitude_.size();
+            if (!src_size.empty())
+                CV_CheckEQ(src_size, gradient_magnitude_size, "");
+            else
+                src_size = gradient_magnitude_size;
+        }
+        if (!image.empty())
+        {
+            Size image_size = image.size();
+            if (!src_size.empty())
+                CV_CheckEQ(src_size, image_size, "");
+            else
+                src_size = image_size;
+        }
+        // src_size must be filled
+        CV_Assert(!src_size.empty());
+
+        if (!non_edge.empty())
+        {
+            CV_CheckTypeEQ(non_edge.type(), CV_8UC1, "");
+            non_edge_feature = non_edge.getMat();
+        }
+        else
+        {
+            if (weight_non_edge == 0.0f)
+            {
+                non_edge_feature.create(src_size);
+                non_edge_feature.setTo(0);
+            }
+            else
+            {
+                if (image.empty())
+                    CV_Error(Error::StsBadArg, "Non-edge feature parameter is missing. Input image parameter is required to extract this feature");
+                extractEdgeFeature_(image);
+            }
+        }
+
+        if (!gradient_direction_.empty())
+        {
+            CV_CheckTypeEQ(gradient_direction_.type(), CV_32FC2, "");
+            gradient_direction = gradient_direction_.getMat();
+        }
+        else
+        {
+            if (weight_gradient_direction == 0.0f)
+            {
+                gradient_direction.create(src_size);
+                gradient_direction.setTo(Scalar::all(0));
+            }
+            else
+            {
+                if (image.empty())
+                    CV_Error(Error::StsBadArg, "Gradient direction feature parameter is missing. Input image parameter is required to extract this feature");
+                extractGradientDirection_(image);
+            }
+        }
+
+        if (!gradient_magnitude_.empty())
+        {
+            CV_CheckTypeEQ(gradient_magnitude_.type(), CV_32FC1, "");
+            gradient_magnitude = gradient_magnitude_.getMat();
+        }
+        else
+        {
+            if (weight_gradient_magnitude == 0.0f)
+            {
+                gradient_magnitude.create(src_size);
+                gradient_magnitude.setTo(Scalar::all(0));
+            }
+            else
+            {
+                if (image.empty())
+                    CV_Error(Error::StsBadArg, "Gradient magnitude feature parameter is missing. Input image parameter is required to extract this feature");
+                extractGradientMagnitude_(image);
+            }
+        }
+
+        cleanupFeaturesTemporaryArrays_();
+    }
+
+
+    void extractEdgeFeature_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+
+        if (edge_mode == FEATURE_CANNY)
+        {
+            CV_LOG_DEBUG(NULL, "Canny(" << edge_canny_threshold1 << ", " << edge_canny_threshold2 << ")");
+            Mat img_canny;
+            Canny(image, img_canny, edge_canny_threshold1, edge_canny_threshold2, edge_canny_apertureSize, edge_canny_L2gradient);
+#if 0
+            threshold(img_canny, non_edge_feature, 254, 1, THRESH_BINARY_INV);
+#else
+            // Canny result values are 0 or 255
+            bitwise_not(img_canny, non_edge_feature);
+            weight_non_edge_compute = weight_non_edge * (1.0f / 255.0f);
+#endif
+        }
+        else // if (edge_mode == FEATURE_ZERO_CROSSING)
+        {
+            initGrayscale_(image);
+            Mat_<short> laplacian;
+            Laplacian(grayscale_, laplacian, CV_16S, laplacianKernelSize);
+            Mat_<uchar> zero_crossing(src_size, 1);
+
+            const size_t zstep = zero_crossing.step[0];
+            for (int y = 0; y < src_size.height - 1; y++)
+            {
+                const short* row0 = laplacian.ptr<short>(y);
+                const short* row1 = laplacian.ptr<short>(y + 1);
+                uchar* zrow0 = zero_crossing.ptr<uchar>(y);
+                //uchar* zrow1 = zero_crossing.ptr<uchar>(y + 1);
+                for (int x = 0; x < src_size.width - 1; x++)
+                {
+                    const int v = row0[x];
+                    const int neg_v = -v;
+                    //  - * 1
+                    //  2 3 4
+                    const int v1 = row0[x + 1];
+                    const int v2 = (x > 0) ? row1[x - 1] : v;
+                    const int v3 = row1[x + 0];
+                    const int v4 = row1[x + 1];
+                    if (v < 0)
+                    {
+                        if (v1 > 0)
+                        {
+                            zrow0[x + ((v1 < neg_v) ? 1 : 0)] = 0;
+                        }
+                        if (v2 > 0)
+                        {
+                            zrow0[x + ((v2 < neg_v) ? (zstep - 1) : 0)] = 0;
+                        }
+                        if (v3 > 0)
+                        {
+                            zrow0[x + ((v3 < neg_v) ? (zstep + 0) : 0)] = 0;
+                        }
+                        if (v4 > 0)
+                        {
+                            zrow0[x + ((v4 < neg_v) ? (zstep + 1) : 0)] = 0;
+                        }
+                    }
+                    else
+                    {
+                        if (v1 < 0)
+                        {
+                            zrow0[x + ((v1 > neg_v) ? 1 : 0)] = 0;
+                        }
+                        if (v2 < 0)
+                        {
+                            zrow0[x + ((v2 > neg_v) ? (zstep - 1) : 0)] = 0;
+                        }
+                        if (v3 < 0)
+                        {
+                            zrow0[x + ((v3 > neg_v) ? (zstep + 0) : 0)] = 0;
+                        }
+                        if (v4 < 0)
+                        {
+                            zrow0[x + ((v4 > neg_v) ? (zstep + 1) : 0)] = 0;
+                        }
+                    }
+                }
+            }
+
+            if (edge_gradient_magnitude_min_value > 0)
+            {
+                initImageMagnitude_(image);
+                Mat mask = image_magnitude_ < edge_gradient_magnitude_min_value;
+                zero_crossing.setTo(1, mask);  // reset low-amplitude noise
+            }
+
+            non_edge_feature = zero_crossing;
+        }
+    }
+
+
+    void extractGradientDirection_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+
+        initImageMagnitude_(image);  // calls internally: initImageDerives_(image);
+        gradient_direction.create(src_size);
+        for (int y = 0; y < src_size.height; y++)
+        {
+            const float* magnutude_row = image_magnitude_.ptr<float>(y);
+            const float* Ix_row = Ix_.ptr<float>(y);
+            const float* Iy_row = Iy_.ptr<float>(y);
+            Point2f* gradient_direction_row = gradient_direction.ptr<Point2f>(y);
+            for (int x = 0; x < src_size.width; x++)
+            {
+                const float m = magnutude_row[x];
+                if (m > FLT_EPSILON)
+                {
+                    float m_inv = 1.0f / m;
+                    gradient_direction_row[x] = Point2f(Ix_row[x] * m_inv, Iy_row[x] * m_inv);
+                }
+                else
+                {
+                    gradient_direction_row[x] = Point2f(0, 0);
+                }
+            }
+        }
+    }
+
+    void extractGradientMagnitude_(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+
+        initImageMagnitude_(image);  // calls internally: initImageDerives_(image);
+        Mat m;
+        double max_m = 0;
+        if (gradient_magnitude_threshold_max > 0)
+        {
+            threshold(image_magnitude_, m, gradient_magnitude_threshold_max, 0, THRESH_TRUNC);
+            max_m = gradient_magnitude_threshold_max;
+        }
+        else
+        {
+            m = image_magnitude_;
+            minMaxLoc(m, 0, &max_m);
+        }
+        if (max_m <= FLT_EPSILON)
+        {
+            CV_LOG_INFO(NULL, "IntelligentScissorsMB: input image gradient is almost zero")
+            gradient_magnitude.create(src_size);
+            gradient_magnitude.setTo(0);
+        }
+        else
+        {
+            m.convertTo(gradient_magnitude, CV_32F, -1.0 / max_m, 1.0);  // normalize and inverse to range 0..1
+        }
+    }
+
+    void applyImage(InputArray image)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_CheckType(image.type(), image.type() == CV_8UC1 || image.type() == CV_8UC3 || image.type() == CV_8UC4, "");
+
+        resetFeatures_();
+        cleanupFeaturesTemporaryArrays_();
+        extractEdgeFeature_(image);
+        extractGradientDirection_(image);
+        extractGradientMagnitude_(image);
+        cleanupFeaturesTemporaryArrays_();
+    }
+
+
+    // details: see section 3.1 of the article
+    const float* acos_table = getAcosTable();
+    float local_cost(const Point& p, const Point& q) const
+    {
+        const bool isDiag = (p.x != q.x) && (p.y != q.y);
+
+        float fG = gradient_magnitude.at<float>(q);
+
+        const Point2f diff((float)(q.x - p.x), (float)(q.y - p.y));
+
+        const Point2f Ip = gradient_direction(p);
+        const Point2f Iq = gradient_direction(q);
+
+        const Point2f Dp(Ip.y, -Ip.x);  // D(p) - 90 degrees clockwise
+        const Point2f Dq(Iq.y, -Iq.x);  // D(q) - 90 degrees clockwise
+
+        float dp = Dp.dot(diff);  // dp(p, q)
+        float dq = Dq.dot(diff);  // dq(p, q)
+        if (dp < 0)
+        {
+            dp = -dp;  // ensure dp >= 0
+            dq = -dq;
+        }
+
+        const float sqrt2_inv = 0.7071067811865475f; // 1.0 / sqrt(2)
+        if (isDiag)
+        {
+            dp *= sqrt2_inv;  // normalize length of (q - p)
+            dq *= sqrt2_inv;  // normalize length of (q - p)
+        }
+        else
+        {
+            fG *= sqrt2_inv;
+        }
+
+#if 1
+        int dp_i = cvFloor(dp * ACOS_TABLE_SIZE);  // dp is in range 0..1
+        dp_i = std::min(ACOS_TABLE_SIZE, std::max(0, dp_i));
+        int dq_i = cvFloor(dq * ACOS_TABLE_SIZE);  // dq is in range -1..1
+        dq_i = std::min(ACOS_TABLE_SIZE, std::max(-ACOS_TABLE_SIZE, dq_i));
+        const float fD = acos_table[dp_i + ACOS_TABLE_SIZE] + acos_table[dq_i + ACOS_TABLE_SIZE];
+#else
+        const float CV_PI_inv = static_cast<float>(1.0 / CV_PI);
+        const float fD = (acosf(dp) + acosf(dq)) * CV_PI_inv;  // TODO optimize acos calls (through tables)
+#endif
+
+        float cost =
+            weight_non_edge_compute * non_edge_feature.at<uchar>(q) +
+            weight_gradient_direction * fD +
+            weight_gradient_magnitude * fG;
+        return cost;
+    }
+
+    struct Pix
+    {
+        Point pt;
+        float cost;  // NOTE: do not remove cost from here through replacing by cost(pt) map access
+
+        inline bool operator > (const Pix &b) const
+        {
+            return cost > b.cost;
+        }
+    };
+
+    void buildMap(const Point& start_point)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_Assert(!src_size.empty());
+        CV_Assert(!gradient_magnitude.empty() && "Features are missing. applyImage() must be called first");
+
+        CV_CheckGE(weight_non_edge + weight_gradient_direction + weight_gradient_magnitude, FLT_EPSILON, "");
+
+#if 0  // debug
+        Rect wholeImage(0, 0, src_size.width, src_size.height);
+        Rect roi = Rect(start_point.x - 5, start_point.y - 5, 11, 11) & wholeImage;
+        std::cout << roi << std::endl;
+        std::cout << gradient_magnitude(roi) << std::endl;
+        std::cout << gradient_direction(roi) << std::endl;
+        std::cout << non_edge_feature(roi) << std::endl;
+#endif
+
+        optimalPathsMap.release();
+        optimalPathsMap.create(src_size);
+        optimalPathsMap.setTo(0);  // optimalPathsMap(start_point) = 0;
+
+        //
+        // Section 3.2
+        // Live-Wire 2-D DP graph search.
+        //
+
+        Mat_<float> cost_map(src_size, FLT_MAX);  // g(q)
+        Mat_<uchar> processed(src_size, (uchar)0);  // e(q)
+
+        // Note: std::vector is faster than std::deque
+        // TODO check std::set
+        std::priority_queue< Pix, std::vector<Pix>, std::greater<Pix> > L;
+
+        cost_map(start_point) = 0;
+        L.emplace(Pix{ start_point, 0/*cost*/ });
+
+        while (!L.empty())
+        {
+            Pix pix = L.top(); L.pop();
+            Point q = pix.pt;  // 'q' from the article
+            if (processed(q))
+                continue;  // already processed (with lower cost, see note below)
+            processed(q) = 1;
+#if 1
+            const float cost_q = pix.cost;
+#else
+            const float cost_q = cost_map(q);
+            CV_Assert(cost_q == pix.cost);
+#endif
+            for (int n = 0; n < 8; n++)  // scan neighbours
+            {
+                Point r(q.x + neighbors[n][0], q.y + neighbors[n][1]);  // 'r' from the article
+                if (r.x < 0 || r.x >= src_size.width || r.y < 0 || r.y >= src_size.height)
+                    continue;  // out of range
+
+#if !defined(__EMSCRIPTEN__)  // slower in JS
+                float& cost_r = cost_map(r);
+                if (cost_r < cost_q)
+                    continue;  // already processed
+#else
+                if (processed(r))
+                    continue;  // already processed
+
+                float& cost_r = cost_map(r);
+                CV_DbgCheckLE(cost_q, cost_r, "INTERNAL ERROR: sorted queue is corrupted");
+#endif
+
+                float cost = cost_q + local_cost(q, r);  // TODO(opt): compute partially until cost < cost_r
+                if (cost < cost_r)
+                {
+#if 0  // avoid compiler warning
+                    if (cost_r != FLT_MAX)
+                    {
+                        // In article the point 'r' is removed from the queue L
+                        // to be re-inserted again with sorting against new optimized cost.
+                        // We can do nothing, because "new point" will be placed before in the sorted queue.
+                        // Old point will be skipped through "if (processed(q))" check above after processing of new optimal candidate.
+                        //
+                        // This approach leads to some performance impact, however it is much smaller than element removal from the sorted queue.
+                        // So, do nothing.
+                    }
+#endif
+                    cost_r = cost;
+                    L.emplace(Pix{ r, cost });
+                    optimalPathsMap(r) = (uchar)neighbors_encode[n];
+                }
+            }
+        }
+    }
+
+    void getContour(const Point& target, OutputArray contour_, bool backward)
+    {
+        CV_TRACE_FUNCTION();
+
+        CV_Assert(!optimalPathsMap.empty() && "buildMap() must be called before getContour()");
+
+        const int cols = optimalPathsMap.cols;
+        const int rows = optimalPathsMap.rows;
+
+        std::vector<Point> result; result.reserve(512);
+
+        size_t loop_check = 4096;
+        Point pt = target;
+        for (size_t i = 0; i < (size_t)rows * cols; i++)  // don't hang on invalid maps
+        {
+            CV_CheckLT(pt.x, cols, "");
+            CV_CheckLT(pt.y, rows, "");
+            result.push_back(pt);
+            int direction = (int)optimalPathsMap(pt);
+            if (direction == 0)
+                break;  // stop, start point is reached
+            CV_CheckLT(direction, 9, "Map is invalid");
+            Point next(pt.x + neighbors[direction - 1][0], pt.y + neighbors[direction - 1][1]);
+            pt = next;
+
+            if (result.size() == loop_check)  // optional sanity check of invalid maps with loops (don't eat huge amount of memory)
+            {
+                loop_check *= 4;  // next limit for loop check
+                for (const auto& pt_check : result)
+                {
+                    CV_CheckNE(pt_check, pt, "Map is invalid. Contour loop is detected");
+                }
+            }
+        }
+
+        if (backward)
+        {
+            _InputArray(result).copyTo(contour_);
+        }
+        else
+        {
+            const int N = (int)result.size();
+            const int sz[1] = { N };
+            contour_.create(1, sz, CV_32SC2);
+            Mat_<Point> contour = contour_.getMat();
+            for (int i = 0; i < N; i++)
+            {
+                contour.at<Point>(i) = result[N - (i + 1)];
+            }
+        }
+    }
+};
+
+
+
+IntelligentScissorsMB::IntelligentScissorsMB()
+    : impl(std::make_shared<Impl>())
+{
+    // nothing
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::setWeights(float weight_non_edge, float weight_gradient_direction, float weight_gradient_magnitude)
+{
+    CV_DbgAssert(impl);
+    impl->setWeights(weight_non_edge, weight_gradient_direction, weight_gradient_magnitude);
+    return *this;
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::setGradientMagnitudeMaxLimit(float gradient_magnitude_threshold_max)
+{
+    CV_DbgAssert(impl);
+    impl->setGradientMagnitudeMaxLimit(gradient_magnitude_threshold_max);
+    return *this;
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::setEdgeFeatureZeroCrossingParameters(float gradient_magnitude_min_value)
+{
+    CV_DbgAssert(impl);
+    impl->setEdgeFeatureZeroCrossingParameters(gradient_magnitude_min_value);
+    return *this;
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::setEdgeFeatureCannyParameters(
+        double threshold1, double threshold2,
+        int apertureSize, bool L2gradient
+)
+{
+    CV_DbgAssert(impl);
+    impl->setEdgeFeatureCannyParameters(threshold1, threshold2, apertureSize, L2gradient);
+    return *this;
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::applyImage(InputArray image)
+{
+    CV_DbgAssert(impl);
+    impl->applyImage(image);
+    return *this;
+}
+
+IntelligentScissorsMB& IntelligentScissorsMB::applyImageFeatures(
+        InputArray non_edge, InputArray gradient_direction, InputArray gradient_magnitude,
+        InputArray image
+)
+{
+    CV_DbgAssert(impl);
+    impl->applyImageFeatures(non_edge, gradient_direction, gradient_magnitude, image);
+    return *this;
+}
+
+void IntelligentScissorsMB::buildMap(const Point& pt)
+{
+    CV_DbgAssert(impl);
+    impl->buildMap(pt);
+}
+
+void IntelligentScissorsMB::getContour(const Point& target, OutputArray contour, bool backward) const
+{
+    CV_DbgAssert(impl);
+    impl->getContour(target, contour, backward);
+}
+
+}}  // namespace
diff --git a/modules/imgproc/test/test_intelligent_scissors.cpp b/modules/imgproc/test/test_intelligent_scissors.cpp
new file mode 100644 (file)
index 0000000..c6b51fd
--- /dev/null
@@ -0,0 +1,467 @@
+// This file is part of OpenCV project.
+// It is subject to the license terms in the LICENSE file found in the top-level directory
+// of this distribution and at http://opencv.org/license.html.
+
+#include "test_precomp.hpp"
+//#include "opencv2/imgproc/segmentation.hpp"
+
+namespace opencv_test { namespace {
+
+
+Mat getTestImageGray()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m = imread(findDataFile("shared/lena.png"), IMREAD_GRAYSCALE);
+    }
+    return m.clone();
+}
+
+Mat getTestImageColor()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m = imread(findDataFile("shared/lena.png"), IMREAD_COLOR);
+    }
+    return m.clone();
+}
+
+Mat getTestImage1()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m.create(Size(200, 100), CV_8UC1);
+        m.setTo(Scalar::all(128));
+        Rect roi(50, 30, 100, 40);
+        m(roi).setTo(Scalar::all(0));
+#if 0
+        imshow("image", m);
+        waitKey();
+#endif
+    }
+    return m.clone();
+}
+
+Mat getTestImage2()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m.create(Size(200, 100), CV_8UC1);
+        m.setTo(Scalar::all(128));
+        Rect roi(40, 30, 100, 40);
+        m(roi).setTo(Scalar::all(255));
+#if 0
+        imshow("image", m);
+        waitKey();
+#endif
+    }
+    return m.clone();
+}
+
+Mat getTestImage3()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m.create(Size(200, 100), CV_8UC1);
+        m.setTo(Scalar::all(128));
+        Scalar color(0,0,0,0);
+        line(m, Point(30, 50), Point(50, 50), color, 1);
+        line(m, Point(50, 50), Point(80, 30), color, 1);
+        line(m, Point(150, 50), Point(80, 30), color, 1);
+        line(m, Point(150, 50), Point(180, 50), color, 1);
+
+        line(m, Point(80, 10), Point(80, 90), Scalar::all(200), 1);
+        line(m, Point(100, 10), Point(100, 90), Scalar::all(200), 1);
+        line(m, Point(120, 10), Point(120, 90), Scalar::all(200), 1);
+#if 0
+        imshow("image", m);
+        waitKey();
+#endif
+    }
+    return m.clone();
+}
+
+Mat getTestImage4()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m.create(Size(200, 100), CV_8UC1);
+        for (int y = 0; y < m.rows; y++)
+        {
+            for (int x = 0; x < m.cols; x++)
+            {
+                float dx = (float)(x - 100);
+                float dy = (float)(y - 100);
+                float d = sqrtf(dx * dx + dy * dy);
+                m.at<uchar>(y, x) = saturate_cast<uchar>(100 + 100 * sin(d / 10 * CV_PI));
+            }
+        }
+#if 0
+        imshow("image", m);
+        waitKey();
+#endif
+    }
+    return m.clone();
+}
+
+Mat getTestImage5()
+{
+    static Mat m;
+    if (m.empty())
+    {
+        m.create(Size(200, 100), CV_8UC1);
+        for (int y = 0; y < m.rows; y++)
+        {
+            for (int x = 0; x < m.cols; x++)
+            {
+                float dx = (float)(x - 100);
+                float dy = (float)(y - 100);
+                float d = sqrtf(dx * dx + dy * dy);
+                m.at<uchar>(y, x) = saturate_cast<uchar>(x / 2 + 100 * sin(d / 10 * CV_PI));
+            }
+        }
+#if 0
+        imshow("image", m);
+        waitKey();
+#endif
+    }
+    return m.clone();
+}
+
+void show(const Mat& img, const std::vector<Point> pts)
+{
+    if (cvtest::debugLevel >= 10)
+    {
+        Mat dst = img.clone();
+        std::vector< std::vector<Point> > contours;
+        contours.push_back(pts);
+        polylines(dst, contours, false, Scalar::all(255));
+        imshow("dst", dst);
+        waitKey();
+    }
+}
+
+TEST(Imgproc_IntelligentScissorsMB, rect)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    tool.applyImage(getTestImage1());
+
+    Point source_point(50, 30);
+    tool.buildMap(source_point);
+
+    Point target_point(100, 30);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    tool.applyImage(getTestImage2());
+
+    tool.buildMap(source_point);
+
+    std::vector<Point> pts2;
+    tool.getContour(target_point, pts2, true/*backward*/);
+
+    EXPECT_EQ(pts.size(), pts2.size());
+}
+
+TEST(Imgproc_IntelligentScissorsMB, lines)
+{
+    segmentation::IntelligentScissorsMB tool;
+    Mat image = getTestImage3();
+    tool.applyImage(image);
+
+    Point source_point(30, 50);
+    tool.buildMap(source_point);
+
+    Point target_point(150, 50);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    EXPECT_EQ((size_t)121, pts.size());
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, circles)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setGradientMagnitudeMaxLimit(10);
+
+    Mat image = getTestImage4();
+    tool.applyImage(image);
+
+    Point source_point(50, 50);
+    tool.buildMap(source_point);
+
+    Point target_point(150, 50);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    EXPECT_EQ((size_t)101, pts.size());
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, circles_gradient)
+{
+    segmentation::IntelligentScissorsMB tool;
+    Mat image = getTestImage5();
+    tool.applyImage(image);
+
+    Point source_point(50, 50);
+    tool.buildMap(source_point);
+
+    Point target_point(150, 50);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    EXPECT_EQ((size_t)101, pts.size());
+    show(image, pts);
+}
+
+#define PTS_SIZE_EPS 2
+
+TEST(Imgproc_IntelligentScissorsMB, grayscale)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    Mat image = getTestImageGray();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 206;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, check_features_grayscale_1_0_0_zerro_crossing_with_limit)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setEdgeFeatureZeroCrossingParameters(64);
+    tool.setWeights(1.0f, 0.0f, 0.0f);
+
+    Mat image = getTestImageGray();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 207;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, check_features_grayscale_1_0_0_canny)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setEdgeFeatureCannyParameters(50, 100);
+    tool.setWeights(1.0f, 0.0f, 0.0f);
+
+    Mat image = getTestImageGray();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 201;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, check_features_grayscale_0_1_0)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setWeights(0.0f, 1.0f, 0.0f);
+
+    Mat image = getTestImageGray();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 166;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, check_features_grayscale_0_0_1)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setWeights(0.0f, 0.0f, 1.0f);
+
+    Mat image = getTestImageGray();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 197;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, color)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    Mat image = getTestImageColor();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 205;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, color_canny)
+{
+    segmentation::IntelligentScissorsMB tool;
+    tool.setEdgeFeatureCannyParameters(32, 100);
+
+    Mat image = getTestImageColor();
+    tool.applyImage(image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 200;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+
+TEST(Imgproc_IntelligentScissorsMB, color_custom_features_invalid)
+{
+    segmentation::IntelligentScissorsMB tool;
+    ASSERT_ANY_THROW(tool.applyImageFeatures(noArray(), noArray(), noArray()));
+}
+
+TEST(Imgproc_IntelligentScissorsMB, color_custom_features_edge)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    Mat image = getTestImageColor();
+
+    Mat canny_edges;
+    Canny(image, canny_edges, 32, 100, 5);
+    Mat binary_edge_feature;
+    cv::threshold(canny_edges, binary_edge_feature, 254, 1, THRESH_BINARY_INV);
+    tool.applyImageFeatures(binary_edge_feature, noArray(), noArray(), image);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 201;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, color_custom_features_all)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    tool.setWeights(0.9f, 0.0f, 0.1f);
+
+    Mat image = getTestImageColor();
+
+    Mat canny_edges;
+    Canny(image, canny_edges, 50, 100, 5);
+    Mat binary_edge_feature; // 0, 1 values
+    cv::threshold(canny_edges, binary_edge_feature, 254, 1, THRESH_BINARY_INV);
+
+    Mat_<Point2f> gradient_direction(image.size(), Point2f(0, 0));  // normalized
+    Mat_<float> gradient_magnitude(image.size(), 0);  // cost function
+    tool.applyImageFeatures(binary_edge_feature, gradient_direction, gradient_magnitude);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 201;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+TEST(Imgproc_IntelligentScissorsMB, color_custom_features_edge_magnitude)
+{
+    segmentation::IntelligentScissorsMB tool;
+
+    tool.setWeights(0.9f, 0.0f, 0.1f);
+
+    Mat image = getTestImageColor();
+
+    Mat canny_edges;
+    Canny(image, canny_edges, 50, 100, 5);
+    Mat binary_edge_feature; // 0, 1 values
+    cv::threshold(canny_edges, binary_edge_feature, 254, 1, THRESH_BINARY_INV);
+
+    Mat_<float> gradient_magnitude(image.size(), 0);  // cost function
+    tool.applyImageFeatures(binary_edge_feature, noArray(), gradient_magnitude);
+
+    Point source_point(275, 63);
+    tool.buildMap(source_point);
+
+    Point target_point(413, 155);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+
+    size_t gold = 201;
+    EXPECT_GE(pts.size(), gold - PTS_SIZE_EPS);
+    EXPECT_LE(pts.size(), gold + PTS_SIZE_EPS);
+    show(image, pts);
+}
+
+
+}} // namespace
index c660253..4409778 100644 (file)
@@ -87,6 +87,8 @@ namespace hal {
 using namespace emscripten;
 using namespace cv;
 
+using namespace cv::segmentation;  // FIXIT
+
 #ifdef HAVE_OPENCV_DNN
 using namespace cv::dnn;
 #endif
index 9ba5cd4..ada315b 100644 (file)
@@ -977,3 +977,26 @@ QUnit.test('warpPolar', function(assert) {
      96,  83,  64,  45,  32
   ]);
 });
+
+
+QUnit.test('IntelligentScissorsMB', function(assert) {
+  const lines = new cv.Mat(50, 100, cv.CV_8U, new cv.Scalar(0));
+  lines.row(10).setTo(new cv.Scalar(255));
+  assert.ok(lines instanceof cv.Mat);
+
+  let tool = new cv.segmentation_IntelligentScissorsMB();
+  tool.applyImage(lines);
+  assert.ok(lines instanceof cv.Mat);
+  lines.delete();
+
+  tool.buildMap(new cv.Point(10, 10));
+
+  let contour = new cv.Mat();
+  tool.getContour(new cv.Point(50, 10), contour);
+  assert.equal(contour.type(), cv.CV_32SC2);
+  assert.ok(contour.total() == 41, contour.total());
+
+  tool.getContour(new cv.Point(80, 10), contour);
+  assert.equal(contour.type(), cv.CV_32SC2);
+  assert.ok(contour.total() == 71, contour.total());
+});
index 6e4677f..2bcc7de 100644 (file)
@@ -18,8 +18,21 @@ imgproc = {'': ['Canny', 'GaussianBlur', 'Laplacian', 'HoughLines', 'HoughLinesP
                 'matchShapes', 'matchTemplate','medianBlur', 'minAreaRect', 'minEnclosingCircle', 'moments', 'morphologyEx', \
                 'pointPolygonTest', 'putText','pyrDown','pyrUp','rectangle','remap', 'resize','sepFilter2D','threshold', \
                 'undistort','warpAffine','warpPerspective','warpPolar','watershed', \
-                'fillPoly', 'fillConvexPoly'],
-           'CLAHE': ['apply', 'collectGarbage', 'getClipLimit', 'getTilesGridSize', 'setClipLimit', 'setTilesGridSize']}
+                'fillPoly', 'fillConvexPoly', 'polylines',
+    ],
+    'CLAHE': ['apply', 'collectGarbage', 'getClipLimit', 'getTilesGridSize', 'setClipLimit', 'setTilesGridSize'],
+    'segmentation_IntelligentScissorsMB': [
+        'IntelligentScissorsMB',
+        'setWeights',
+        'setGradientMagnitudeMaxLimit',
+        'setEdgeFeatureZeroCrossingParameters',
+        'setEdgeFeatureCannyParameters',
+        'applyImage',
+        'applyImageFeatures',
+        'buildMap',
+        'getContour'
+    ],
+}
 
 objdetect = {'': ['groupRectangles'],
              'HOGDescriptor': ['load', 'HOGDescriptor', 'getDefaultPeopleDetector', 'getDaimlerPeopleDetector', 'setSVMDetector', 'detectMultiScale'],
index 14ab614..5f40dba 100644 (file)
@@ -23,6 +23,9 @@ if(NOT BUILD_EXAMPLES OR NOT OCV_DEPENDENCIES_FOUND)
   return()
 endif()
 
+set(DEPS_example_snippet_imgproc_segmentation opencv_core opencv_imgproc)
+set(DEPS_example_cpp_intelligent_scissors opencv_core opencv_imgproc opencv_imgcodecs opencv_highgui)
+
 project(cpp_samples)
 ocv_include_modules_recurse(${OPENCV_CPP_SAMPLES_REQUIRED_DEPS})
 file(GLOB_RECURSE cpp_samples RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp)
@@ -32,11 +35,17 @@ endif()
 ocv_list_filterout(cpp_samples "real_time_pose_estimation/")
 foreach(sample_filename ${cpp_samples})
   set(package "cpp")
-  if(sample_filename MATCHES "tutorial_code")
+  if(sample_filename MATCHES "tutorial_code/snippet")
+    set(package "snippet")
+  elseif(sample_filename MATCHES "tutorial_code")
     set(package "tutorial")
   endif()
   ocv_define_sample(tgt ${sample_filename} ${package})
-  ocv_target_link_libraries(${tgt} PRIVATE ${OPENCV_LINKER_LIBS} ${OPENCV_CPP_SAMPLES_REQUIRED_DEPS})
+  set(deps ${OPENCV_CPP_SAMPLES_REQUIRED_DEPS})
+  if(DEFINED DEPS_${tgt})
+    set(deps ${DEPS_${tgt}})
+  endif()
+  ocv_target_link_libraries(${tgt} PRIVATE ${OPENCV_LINKER_LIBS} ${deps})
   if(sample_filename MATCHES "/gpu/" AND HAVE_opencv_cudaarithm AND HAVE_opencv_cuda_filters)
     ocv_target_link_libraries(${tgt} PRIVATE opencv_cudaarithm opencv_cudafilters)
   endif()
diff --git a/samples/cpp/tutorial_code/snippets/imgproc_segmentation.cpp b/samples/cpp/tutorial_code/snippets/imgproc_segmentation.cpp
new file mode 100644 (file)
index 0000000..b81ba34
--- /dev/null
@@ -0,0 +1,35 @@
+#include "opencv2/imgproc.hpp"
+#include "opencv2/imgproc/segmentation.hpp"
+
+using namespace cv;
+
+static
+void usage_example_intelligent_scissors()
+{
+    Mat image(Size(1920, 1080), CV_8UC3, Scalar::all(128));
+
+    //! [usage_example_intelligent_scissors]
+    segmentation::IntelligentScissorsMB tool;
+    tool.setEdgeFeatureCannyParameters(16, 100)  // using Canny() as edge feature extractor
+        .setGradientMagnitudeMaxLimit(200);
+
+    // calculate image features
+    tool.applyImage(image);
+
+    // calculate map for specified source point
+    Point source_point(200, 100);
+    tool.buildMap(source_point);
+
+    // fast fetching of contours
+    // for specified target point and the pre-calculated map (stored internally)
+    Point target_point(400, 300);
+    std::vector<Point> pts;
+    tool.getContour(target_point, pts);
+    //! [usage_example_intelligent_scissors]
+}
+
+int main()
+{
+    usage_example_intelligent_scissors();
+    return 0;
+}
index 9459099..2780b47 100644 (file)
@@ -4,6 +4,9 @@
 function(ocv_define_sample out_target source sub)
   get_filename_component(name "${source}" NAME_WE)
   set(the_target "example_${sub}_${name}")
+  if(OPENCV_DUMP_EXAMPLE_TARGET)
+    message(STATUS "Example: ${the_target}    (${source})")
+  endif()
   add_executable(${the_target} "${source}")
   if(TARGET Threads::Threads AND NOT OPENCV_EXAMPLES_DISABLE_THREADS)
     target_link_libraries(${the_target} PRIVATE Threads::Threads)