From 44e7b4eb1152f1a22cbddd163a790100b69eaab9 Mon Sep 17 00:00:00 2001 From: Madan mohan Manokar Date: Mon, 24 Aug 2026 17:36:23 +0530 Subject: [PATCH] Merge pull request #29273 from amd:fast_sobel2d imgproc: Extended spatialGradient API and applied to different detector algorithms - #29273 imgproc: Add fused Sobel2D gradient API and use it in different detector algorithms Add a public Sobel2D API computing dx/dy in a single fused pass with 3x3 and 5x5 kernels (SIMD-dispatched). The float (CV_32F) path folds the output scale and float store into the kernel, avoiding a separate convertTo pass. - Add runtime SIMD dispatch for the Canny edge path. - Integrate fused Sobel2D into: - Canny - cornerEigenValsVecs (cornerHarris, cornerMinEigenVal, cornerEigenValsAndVecs, goodFeaturesToTrack) - GeneralizedHough - IntelligentScissors - HoughCircles - Add performance and accuracy tests. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- modules/imgproc/include/opencv2/imgproc.hpp | 38 +++-- modules/imgproc/perf/perf_sepfilters.cpp | 104 +++++++++++++ modules/imgproc/src/corner.cpp | 10 ++ modules/imgproc/src/generalized_hough.cpp | 6 + modules/imgproc/src/hough.cpp | 4 +- modules/imgproc/src/intelligent_scissors.cpp | 15 +- modules/imgproc/src/spatialgradient.cpp | 60 +++++-- modules/imgproc/test/test_filter.cpp | 156 ++++++++++++++++++- 8 files changed, 365 insertions(+), 28 deletions(-) diff --git a/modules/imgproc/include/opencv2/imgproc.hpp b/modules/imgproc/include/opencv2/imgproc.hpp index 3d43e5ec09..58ee6fb8cf 100644 --- a/modules/imgproc/include/opencv2/imgproc.hpp +++ b/modules/imgproc/include/opencv2/imgproc.hpp @@ -12,6 +12,7 @@ // // Copyright (C) 2000-2008, Intel Corporation, all rights reserved. // Copyright (C) 2009, Willow Garage Inc., all rights reserved. +// Copyright (C) 2026, Advanced Micro Devices, Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, @@ -1809,28 +1810,39 @@ CV_EXPORTS_W void Sobel( InputArray src, OutputArray dst, int ddepth, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT ); -/** @brief Calculates the first order image derivative in both x and y using a Sobel operator - -Equivalent to calling: +/** @brief Calculates the first order image derivatives in both x and y using a Sobel operator, +computing them together in a single pass. +This is a fused variant of #Sobel: instead of two separate calls @code -Sobel( src, dx, CV_16SC1, 1, 0, 3 ); -Sobel( src, dy, CV_16SC1, 0, 1, 3 ); +Sobel( src, dx, ddepth, 1, 0, ksize, scale ); +Sobel( src, dy, ddepth, 0, 1, ksize, scale ); @endcode +it produces both first-order derivatives in one traversal of the source. For an 8-bit single-channel +whole-image (non-ROI) source with @p ksize = 3, @p ddepth = CV_16S, @p scale = 1, and +#BORDER_DEFAULT (#BORDER_REFLECT_101) or #BORDER_REPLICATE, a dedicated fused 3x3 stencil kernel is +used (including a HAL fast path) that reads each source sample once and shares it between the dx and +dy computations; the result is bit-identical to the two #Sobel calls above. All other cases +(@p ddepth = CV_32F, @p ksize = 5, scaled int16 output, floating-point source, other border types, +or ROI/sub-matrix input) currently fall back to the two equivalent #Sobel passes. @note Fused fast +paths for CV_32F output and @p ksize = 5 are added in a follow-up change. -@param src input image. -@param dx output image with first-order derivative in x. -@param dy output image with first-order derivative in y. -@param ksize size of Sobel kernel. It must be 3. -@param borderType pixel extrapolation method, see #BorderTypes. - Only #BORDER_DEFAULT=#BORDER_REFLECT_101 and #BORDER_REPLICATE are supported. +@param src input image; single-channel, 8-bit (CV_8UC1) for the fused fast paths (CV_32FC1 is + accepted via the fallback). +@param dx output image with the first-order derivative in x (depth @p ddepth, same size as src). +@param dy output image with the first-order derivative in y (depth @p ddepth, same size as src). +@param ksize size of the Sobel kernel; fused fast paths require 3 (5 uses the Sobel fallback). + Also accepts -1 (Scharr) and 7 for Sobel-compatible callers such as #HoughCircles. +@param borderType pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported. +@param ddepth output image depth; CV_16S or CV_32F. +@param scale optional scale factor applied to the computed derivatives. @sa Sobel */ - CV_EXPORTS_W void spatialGradient( InputArray src, OutputArray dx, OutputArray dy, int ksize = 3, - int borderType = BORDER_DEFAULT ); + int borderType = BORDER_DEFAULT, + int ddepth = CV_16S, double scale = 1 ); /** @brief Calculates the first x- or y- image derivative using Scharr operator. diff --git a/modules/imgproc/perf/perf_sepfilters.cpp b/modules/imgproc/perf/perf_sepfilters.cpp index 480aab9f20..70a9068daa 100644 --- a/modules/imgproc/perf/perf_sepfilters.cpp +++ b/modules/imgproc/perf/perf_sepfilters.cpp @@ -134,6 +134,110 @@ PERF_TEST_P(Size_MatType_dx_dy_Border5x5ROI, sobelFilter, SANITY_CHECK(dst); } +/**************** spatialGradient (fused dx+dy) ********************/ + +typedef tuple Size_Aperture_Border_t; +typedef perf::TestBaseWithParam Size_Aperture_Border; + +PERF_TEST_P(Size_Aperture_Border, spatialGradient_fused, + testing::Combine( + testing::Values(FILTER_SRC_SIZES), + testing::Values(3, 5), + testing::Values((int)BORDER_DEFAULT, (int)BORDER_REPLICATE) + ) + ) +{ + Size size = get<0>(GetParam()); + int ksize = get<1>(GetParam()); + int border = get<2>(GetParam()); + + Mat src(size, CV_8U); + Mat dx(size, CV_16S), dy(size, CV_16S); + + declare.in(src, WARMUP_RNG).out(dx, dy); + + TEST_CYCLE() spatialGradient(src, dx, dy, ksize, border); + + SANITY_CHECK_NOTHING(); +} + +// Float output variant (CV_8U source -> CV_32F dx/dy), as used by corner detectors. +PERF_TEST_P(Size_Aperture_Border, spatialGradient_fused_32f, + testing::Combine( + testing::Values(FILTER_SRC_SIZES), + testing::Values(3, 5), + testing::Values((int)BORDER_DEFAULT, (int)BORDER_REPLICATE) + ) + ) +{ + Size size = get<0>(GetParam()); + int ksize = get<1>(GetParam()); + int border = get<2>(GetParam()); + + Mat src(size, CV_8U); + Mat dx(size, CV_32F), dy(size, CV_32F); + + declare.in(src, WARMUP_RNG).out(dx, dy); + + TEST_CYCLE() spatialGradient(src, dx, dy, ksize, border, CV_32F); + + SANITY_CHECK_NOTHING(); +} + +// Float baseline: the two cv::Sobel CV_32F calls (corner-detector gradient stage). +PERF_TEST_P(Size_Aperture_Border, spatialGradient_32f_baseline, + testing::Combine( + testing::Values(FILTER_SRC_SIZES), + testing::Values(3, 5), + testing::Values((int)BORDER_DEFAULT, (int)BORDER_REPLICATE) + ) + ) +{ + Size size = get<0>(GetParam()); + int ksize = get<1>(GetParam()); + int border = get<2>(GetParam()); + + Mat src(size, CV_8U); + Mat dx(size, CV_32F), dy(size, CV_32F); + + declare.in(src, WARMUP_RNG).out(dx, dy); + + TEST_CYCLE() + { + Sobel(src, dx, CV_32F, 1, 0, ksize, 1, 0, border); + Sobel(src, dy, CV_32F, 0, 1, ksize, 1, 0, border); + } + + SANITY_CHECK_NOTHING(); +} + +// Baseline: the two separate cv::Sobel calls that spatialGradient fuses (same params). +PERF_TEST_P(Size_Aperture_Border, spatialGradient_baseline, + testing::Combine( + testing::Values(FILTER_SRC_SIZES), + testing::Values(3, 5), + testing::Values((int)BORDER_DEFAULT, (int)BORDER_REPLICATE) + ) + ) +{ + Size size = get<0>(GetParam()); + int ksize = get<1>(GetParam()); + int border = get<2>(GetParam()); + + Mat src(size, CV_8U); + Mat dx(size, CV_16S), dy(size, CV_16S); + + declare.in(src, WARMUP_RNG).out(dx, dy); + + TEST_CYCLE() + { + Sobel(src, dx, CV_16S, 1, 0, ksize, 1, 0, border); + Sobel(src, dy, CV_16S, 0, 1, ksize, 1, 0, border); + } + + SANITY_CHECK_NOTHING(); +} + /**************** Scharr ********************/ PERF_TEST_P(Size_MatType_dx_dy_Border3x3, scharrFilter, diff --git a/modules/imgproc/src/corner.cpp b/modules/imgproc/src/corner.cpp index da0373d1bf..2ed6fe074f 100644 --- a/modules/imgproc/src/corner.cpp +++ b/modules/imgproc/src/corner.cpp @@ -13,6 +13,7 @@ // Copyright (C) 2000-2008, Intel Corporation, all rights reserved. // Copyright (C) 2009, Willow Garage Inc., all rights reserved. // Copyright (C) 2014-2015, Itseez Inc., all rights reserved. +// Copyright (C) 2026, Advanced Micro Devices, Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, @@ -254,6 +255,15 @@ cornerEigenValsVecs( const Mat& src, Mat& eigenv, int block_size, CV_Assert( src.type() == CV_8UC1 || src.type() == CV_32FC1 ); Mat Dx, Dy; + // TODO(follow-up PR): route CV_32F gradients through the fused spatialGradient once its + // CV_32F/ksize=5 fast paths land. Disabled for now so this stays on the tuned Sobel path. +#if 0 + if( aperture_size == 3 || aperture_size == 5 ) + { + spatialGradient( src, Dx, Dy, aperture_size, borderType, CV_32F, scale ); + } + else +#endif if( aperture_size > 0 ) { Sobel( src, Dx, CV_32F, 1, 0, aperture_size, scale, 0, borderType ); diff --git a/modules/imgproc/src/generalized_hough.cpp b/modules/imgproc/src/generalized_hough.cpp index 6a2ca5f032..51e61c6b3d 100644 --- a/modules/imgproc/src/generalized_hough.cpp +++ b/modules/imgproc/src/generalized_hough.cpp @@ -11,6 +11,7 @@ // For Open Source Computer Vision Library // // Copyright (C) 2000, Intel Corporation, all rights reserved. +// Copyright (C) 2026, Advanced Micro Devices, Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, @@ -115,8 +116,13 @@ namespace CV_Assert( cannyLowThresh_ > 0 && cannyLowThresh_ < cannyHighThresh_ ); Canny(src, edges, cannyLowThresh_, cannyHighThresh_); + // TODO(follow-up PR): use the fused spatialGradient once its CV_32F fast path lands. +#if 0 + spatialGradient(src, dx, dy, 3, BORDER_DEFAULT, CV_32F); +#else Sobel(src, dx, CV_32F, 1, 0); Sobel(src, dy, CV_32F, 0, 1); +#endif } void GeneralizedHoughBase::setTemplateImpl(InputArray templ, Point templCenter) diff --git a/modules/imgproc/src/hough.cpp b/modules/imgproc/src/hough.cpp index 8faa2a64f3..b9731acc89 100644 --- a/modules/imgproc/src/hough.cpp +++ b/modules/imgproc/src/hough.cpp @@ -13,6 +13,7 @@ // Copyright (C) 2000, Intel Corporation, all rights reserved. // Copyright (C) 2013, OpenCV Foundation, all rights reserved. // Copyright (C) 2014, Itseez, Inc, all rights reserved. +// Copyright (C) 2026, Advanced Micro Devices, Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, @@ -1710,8 +1711,7 @@ static void HoughCirclesGradient(InputArray _image, OutputArray _circles, Mat edges, dx, dy; - Sobel(_image, dx, CV_16S, 1, 0, kernelSize, 1, 0, BORDER_REPLICATE); - Sobel(_image, dy, CV_16S, 0, 1, kernelSize, 1, 0, BORDER_REPLICATE); + spatialGradient(_image, dx, dy, kernelSize, BORDER_REPLICATE); Canny(dx, dy, edges, std::max(1, cannyThreshold / 2), cannyThreshold, false); Mutex mtx; diff --git a/modules/imgproc/src/intelligent_scissors.cpp b/modules/imgproc/src/intelligent_scissors.cpp index 2007575891..b2cb2af5d2 100644 --- a/modules/imgproc/src/intelligent_scissors.cpp +++ b/modules/imgproc/src/intelligent_scissors.cpp @@ -1,6 +1,7 @@ // 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) 2026, Advanced Micro Devices, Inc., all rights reserved. // // Copyright (C) 2020, Intel Corporation, all rights reserved. // Third party copyrights are property of their respective owners. @@ -144,8 +145,18 @@ struct IntelligentScissorsMB::Impl if (!Ix_.empty()) return; initGrayscale_(image); - Sobel(grayscale_, Ix_, CV_32FC1, 1, 0, sobelKernelSize); - Sobel(grayscale_, Iy_, CV_32FC1, 0, 1, sobelKernelSize); + // TODO(follow-up PR): use the fused spatialGradient once its CV_32F/ksize=5 fast paths land. +#if 0 + if (sobelKernelSize == 3 || sobelKernelSize == 5) + { + spatialGradient(grayscale_, Ix_, Iy_, sobelKernelSize, BORDER_DEFAULT, CV_32F); + } + else +#endif + { + Sobel(grayscale_, Ix_, CV_32FC1, 1, 0, sobelKernelSize); + Sobel(grayscale_, Iy_, CV_32FC1, 0, 1, sobelKernelSize); + } } Mat image_magnitude_; void initImageMagnitude_(InputArray image) diff --git a/modules/imgproc/src/spatialgradient.cpp b/modules/imgproc/src/spatialgradient.cpp index 4342d53764..6f4a9f09be 100644 --- a/modules/imgproc/src/spatialgradient.cpp +++ b/modules/imgproc/src/spatialgradient.cpp @@ -43,7 +43,6 @@ #include "precomp.hpp" #include "opencv2/core/hal/intrin.hpp" -#include namespace cv { @@ -93,32 +92,75 @@ static inline void spatialGradientKernel( T& vx, T& vy, vy = tmp_add - tmp_sub + tmp_y + tmp_y; } +#if defined(__GNUC__) || defined(__clang__) +# define CV_SPATIALGRAD_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +# define CV_SPATIALGRAD_NOINLINE __declspec(noinline) +#else +# define CV_SPATIALGRAD_NOINLINE +#endif + +// Fused single-pass 3x3 Sobel over the whole image: 8-bit input, CV_16S output, +// unit scale, BORDER_REFLECT_101 / BORDER_REPLICATE. Kept in its own (non-inlined) +// function so the hot vectorized loop is codegen'd in isolation from the public +// entry point's dispatch/fallback logic. Folding both into one frame perturbs loop +// alignment and costs ~10% on some microarchitectures (observed on AMD Zen5/Turin). +static CV_SPATIALGRAD_NOINLINE +void spatialGradientFused3x3_8u16s( Mat src, OutputArray _dx, OutputArray _dy, int bt ); + void spatialGradient( InputArray _src, OutputArray _dx, OutputArray _dy, - int ksize, int borderType ) + int ksize, int borderType, int ddepth, double scale ) { CV_INSTRUMENT_REGION(); // Prepare InputArray src Mat src = _src.getMat(); CV_Assert( !src.empty() ); - CV_Assert( src.type() == CV_8UC1 ); - CV_Assert( borderType == BORDER_DEFAULT || borderType == BORDER_REPLICATE ); + CV_Assert( ksize == -1 || ksize == 3 || ksize == 5 || ksize == 7 ); + CV_Assert( ddepth == CV_16S || ddepth == CV_32F ); + // Fused single-pass 3x3 Sobel fast path (existing vectorized kernel): 8-bit source, + // CV_16S output, unit scale, whole-image (non-ROI), BORDER_REFLECT_101/REPLICATE. + // NOTE: fused fast paths for CV_32F output and ksize == 5 (plus ROI-aware slicing and + // other border types) are added in a follow-up PR; until then those cases are computed + // with the two equivalent cv::Sobel() passes below. + Size wholeSize; + Point ofs; + src.locateROI( wholeSize, ofs ); + const bool entireParent = ( ofs.x == 0 && ofs.y == 0 && + src.cols == wholeSize.width && src.rows == wholeSize.height ); + const bool isolated = ( borderType & BORDER_ISOLATED ) != 0; + const int bt = borderType & ~BORDER_ISOLATED; + + const bool fastPath = ( ksize == 3 && ddepth == CV_16S && scale == 1.0 + && src.type() == CV_8UC1 && entireParent && !isolated + && ( bt == BORDER_DEFAULT || bt == BORDER_REPLICATE ) ); + + if ( !fastPath ) + { + Sobel( _src, _dx, ddepth, 1, 0, ksize, scale, 0, borderType ); + Sobel( _src, _dy, ddepth, 0, 1, ksize, scale, 0, borderType ); + return; + } + + spatialGradientFused3x3_8u16s( src, _dx, _dy, bt ); +} + +static CV_SPATIALGRAD_NOINLINE +void spatialGradientFused3x3_8u16s( Mat src, OutputArray _dx, OutputArray _dy, int bt ) +{ // Prepare OutputArrays dx, dy _dx.create( src.size(), CV_16SC1 ); _dy.create( src.size(), CV_16SC1 ); Mat dx = _dx.getMat(), dy = _dy.getMat(); - // TODO: Allow for other kernel sizes - CV_Assert(ksize == 3); - CALL_HAL(spatialGradient, cv_hal_spatialGradient, src.data, src.step, dx.ptr(), dx.step, dy.ptr(), dy.step, src.cols, src.rows, - ksize, borderType); + 3, bt); // Get dimensions const int H = src.rows, @@ -134,7 +176,7 @@ void spatialGradient( InputArray _src, OutputArray _dx, OutputArray _dy, j_offl = 0, // j offset from 0th pixel to reach -1st pixel j_offr = 0; // j offset from W-1th pixel to reach Wth pixel - if ( borderType == BORDER_DEFAULT ) // Equiv. to BORDER_REFLECT_101 + if ( bt == BORDER_DEFAULT ) // Equiv. to BORDER_REFLECT_101 { if ( H > 1 ) { diff --git a/modules/imgproc/test/test_filter.cpp b/modules/imgproc/test/test_filter.cpp index 2d075d1c32..83301d9584 100644 --- a/modules/imgproc/test/test_filter.cpp +++ b/modules/imgproc/test/test_filter.cpp @@ -11,6 +11,7 @@ // For Open Source Computer Vision Library // // Copyright (C) 2000, Intel Corporation, all rights reserved. +// Copyright (C) 2026, Advanced Micro Devices, Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, @@ -601,8 +602,10 @@ void CV_SpatialGradientTest::get_test_array_types_and_sizes( int test_case_idx, void CV_SpatialGradientTest::run_func() { - spatialGradient( test_mat[INPUT][0], test_mat[OUTPUT][0], - test_mat[OUTPUT][1], ksize, border ); + Mat dx, dy; + spatialGradient( test_mat[INPUT][0], dx, dy, ksize, border ); + dx.copyTo( test_mat[OUTPUT][0] ); + dy.copyTo( test_mat[OUTPUT][1] ); } void CV_SpatialGradientTest::prepare_to_validation( int /*test_case_idx*/ ) @@ -1868,6 +1871,155 @@ TEST(Imgproc_MorphologyEx, accuracy) { CV_MorphExTest test; test.safe_run(); } TEST(Imgproc_Filter2D, accuracy) { CV_FilterTest test; test.safe_run(); } TEST(Imgproc_Sobel, accuracy) { CV_SobelTest test; test.safe_run(); } TEST(Imgproc_SpatialGradient, accuracy) { CV_SpatialGradientTest test; test.safe_run(); } + +// spatialGradient (fused dx+dy) must match the two separate cv::Sobel calls it fuses: +// bit-exact for ddepth=CV_16S/scale=1 and ddepth=CV_32F (scale folded into kernels like cv::Sobel). +typedef tuple SpatialGradientFusedDepthScale_t; +typedef tuple SpatialGradientFusedParams_t; +typedef TestWithParam Imgproc_SpatialGradient_Fused; + +TEST_P(Imgproc_SpatialGradient_Fused, fused_accuracy) +{ + const int iter = get<0>(GetParam()); + const int ksize = get<1>(GetParam()); + const int border = get<2>(GetParam()); + const int ddepth = get<0>(get<3>(GetParam())); + const double scale = get<1>(get<3>(GetParam())); + + RNG& rng = TS::ptr()->get_rng(); + rng.state += iter; + Size sz(rng.uniform(3, 320), rng.uniform(3, 240)); + Mat src(sz, CV_8UC1); + rng.fill(src, RNG::UNIFORM, 0, 256); + + Mat dx, dy, dxRef, dyRef; + spatialGradient(src, dx, dy, ksize, border, ddepth, scale); + Sobel(src, dxRef, ddepth, 1, 0, ksize, scale, 0, border); + Sobel(src, dyRef, ddepth, 0, 1, ksize, scale, 0, border); + + EXPECT_EQ(CV_MAKETYPE(ddepth, 1), dx.type()); + EXPECT_EQ(sz, dx.size()); + const double tol = 0.0; + EXPECT_LE(cvtest::norm(dx, dxRef, NORM_INF), tol); + EXPECT_LE(cvtest::norm(dy, dyRef, NORM_INF), tol); +} + +INSTANTIATE_TEST_CASE_P(/**/, Imgproc_SpatialGradient_Fused, + testing::Combine( + testing::Range(0, 16), + testing::Values(3, 5), + testing::Values(BORDER_DEFAULT, BORDER_REPLICATE, BORDER_REFLECT, + BORDER_REFLECT_101, BORDER_CONSTANT), + testing::Values( + make_tuple(CV_16S, 1.0), + make_tuple(CV_32F, 1.0), + make_tuple(CV_32F, 0.25) + ) + ) +); + +TEST(Imgproc_SpatialGradient, fused_accuracy) +{ + RNG& rng = TS::ptr()->get_rng(); + + // CV_32FC1 source must work via the fallback and match cv::Sobel. + { + Mat src(120, 90, CV_32FC1); + rng.fill(src, RNG::UNIFORM, -5.f, 5.f); + for (int ks : {3, 5}) + { + Mat dx, dy, dxRef, dyRef; + spatialGradient(src, dx, dy, ks, BORDER_DEFAULT, CV_32F); + Sobel(src, dxRef, CV_32F, 1, 0, ks, 1, 0, BORDER_DEFAULT); + Sobel(src, dyRef, CV_32F, 0, 1, ks, 1, 0, BORDER_DEFAULT); + EXPECT_LE(cvtest::norm(dx, dxRef, NORM_INF), 1e-4) << "float-src dx ksize=" << ks; + EXPECT_LE(cvtest::norm(dy, dyRef, NORM_INF), 1e-4) << "float-src dy ksize=" << ks; + } + } + + // full-width row-range ROI (as Canny uses): must match cv::Sobel on the ROI. + { + Mat parent(200, 160, CV_8UC1); + rng.fill(parent, RNG::UNIFORM, 0, 256); + for (int ks : {3, 5}) + for (int b : {BORDER_DEFAULT, BORDER_REPLICATE, BORDER_REFLECT, BORDER_CONSTANT}) + for (int ddepth : {CV_16S, CV_32F}) + { + Mat roi = parent.rowRange(40, 120); + Mat dx, dy, dxRef, dyRef; + spatialGradient(roi, dx, dy, ks, b, ddepth); + Sobel(roi, dxRef, ddepth, 1, 0, ks, 1, 0, b); + Sobel(roi, dyRef, ddepth, 0, 1, ks, 1, 0, b); + EXPECT_LE(cvtest::norm(dx, dxRef, NORM_INF), 0.0) << "ROI dx ksize=" << ks << " border=" << b << " ddepth=" << ddepth; + EXPECT_LE(cvtest::norm(dy, dyRef, NORM_INF), 0.0) << "ROI dy ksize=" << ks << " border=" << b << " ddepth=" << ddepth; + } + } + + // invalid aperture sizes must be rejected + Mat src(16, 16, CV_8UC1), dx, dy; + EXPECT_ANY_THROW(spatialGradient(src, dx, dy, 1)); +} + +// Reproduces parallelCanny's per-slice row splitting and checks each slice's +// spatialGradient output matches the whole-image gradient (i.e. multi-threaded == single). +typedef tuple SpatialGradientSliceThread_t; +typedef tuple SpatialGradientSliceParams_t; +typedef TestWithParam Imgproc_SpatialGradient_Slice; + +TEST_P(Imgproc_SpatialGradient_Slice, slice_equivalence) +{ + const int ksize = get<0>(GetParam()); + const int border = get<1>(GetParam()); + const int nThreads = get<0>(get<2>(GetParam())); + const int t = get<1>(get<2>(GetParam())); + + Mat src(193, 137, CV_8UC1); // odd dims to stress tail handling + RNG& rng = TS::ptr()->get_rng(); + rng.state += (int)((int64)ksize * 10000 + border * 1000 + nThreads * 100 + t); + rng.fill(src, RNG::UNIFORM, 0, 256); + + Mat dxRef, dyRef; + spatialGradient(src, dxRef, dyRef, ksize, border); + + const int start = (int)((int64)src.rows * t / nThreads); + const int end = (int)((int64)src.rows * (t + 1) / nThreads); + if (start >= end) + return; + + const int rowStart = std::max(0, start - 1); + const int rowEnd = std::min(src.rows, end + 1); + + Mat dx, dy; + spatialGradient(src.rowRange(rowStart, rowEnd), dx, dy, ksize, border); + + // rows [start, end) live at offset (start - rowStart) in the slice output + const int off = start - rowStart; + Mat dxSlice = dx.rowRange(off, off + (end - start)); + Mat dySlice = dy.rowRange(off, off + (end - start)); + Mat dxWhole = dxRef.rowRange(start, end); + Mat dyWhole = dyRef.rowRange(start, end); + + EXPECT_EQ(0.0, cvtest::norm(dxSlice, dxWhole, NORM_INF)); + EXPECT_EQ(0.0, cvtest::norm(dySlice, dyWhole, NORM_INF)); +} + +INSTANTIATE_TEST_CASE_P(/**/, Imgproc_SpatialGradient_Slice, + testing::Combine( + testing::Values(3, 5), + testing::Values(BORDER_REPLICATE, BORDER_REFLECT, BORDER_REFLECT_101), + testing::Values( + make_tuple(2, 0), make_tuple(2, 1), + make_tuple(3, 0), make_tuple(3, 1), make_tuple(3, 2), + make_tuple(4, 0), make_tuple(4, 1), make_tuple(4, 2), make_tuple(4, 3), + make_tuple(7, 0), make_tuple(7, 1), make_tuple(7, 2), make_tuple(7, 3), + make_tuple(7, 4), make_tuple(7, 5), make_tuple(7, 6), + make_tuple(16, 0), make_tuple(16, 1), make_tuple(16, 2), make_tuple(16, 3), + make_tuple(16, 4), make_tuple(16, 5), make_tuple(16, 6), make_tuple(16, 7), + make_tuple(16, 8), make_tuple(16, 9), make_tuple(16, 10), make_tuple(16, 11), + make_tuple(16, 12), make_tuple(16, 13), make_tuple(16, 14), make_tuple(16, 15) + ) + ) +); TEST(Imgproc_Laplace, accuracy) { CV_LaplaceTest test; test.safe_run(); } TEST(Imgproc_Blur, accuracy) { CV_BlurTest test; test.safe_run(); } TEST(Imgproc_GaussianBlur, accuracy) { CV_GaussianBlurTest test; test.safe_run(); }