diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index 517b0f31a5..1b3f574275 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -6,6 +6,7 @@ ocv_add_dispatched_file(arithm SSE2 SSE4_1 AVX2 VSX3) ocv_add_dispatched_file(convert SSE2 AVX2 VSX3) ocv_add_dispatched_file(convert_scale SSE2 AVX2) ocv_add_dispatched_file(count_non_zero SSE2 AVX2) +ocv_add_dispatched_file(has_non_zero SSE2 AVX2) ocv_add_dispatched_file(matmul SSE2 SSE4_1 AVX2 AVX512_SKX NEON_DOTPROD) ocv_add_dispatched_file(mean SSE2 AVX2) ocv_add_dispatched_file(merge SSE2 AVX2) diff --git a/modules/core/include/opencv2/core.hpp b/modules/core/include/opencv2/core.hpp index 9b94c72a43..d9a21701f2 100644 --- a/modules/core/include/opencv2/core.hpp +++ b/modules/core/include/opencv2/core.hpp @@ -572,6 +572,14 @@ independently for each channel. */ CV_EXPORTS_AS(sumElems) Scalar sum(InputArray src); +/** @brief Checks for the presence of at least one non-zero array element. + +The function returns whether there are non-zero elements in src +@param src single-channel array. +@sa mean, meanStdDev, norm, minMaxLoc, calcCovarMatrix +*/ +CV_EXPORTS_W bool hasNonZero( InputArray src ); + /** @brief Counts non-zero array elements. The function returns the number of non-zero elements in src : diff --git a/modules/core/perf/opencl/perf_arithm.cpp b/modules/core/perf/opencl/perf_arithm.cpp index 526bc4e874..8d1e7a6288 100644 --- a/modules/core/perf/opencl/perf_arithm.cpp +++ b/modules/core/perf/opencl/perf_arithm.cpp @@ -460,6 +460,30 @@ OCL_PERF_TEST_P(CountNonZeroFixture, CountNonZero, SANITY_CHECK(result); } +///////////// countNonZero //////////////////////// + +typedef Size_MatType HasNonZeroFixture; + +OCL_PERF_TEST_P(HasNonZeroFixture, HasNonZero, + ::testing::Combine(OCL_TEST_SIZES, + OCL_PERF_ENUM(CV_8UC1, CV_32FC1))) +{ + const Size_MatType_t params = GetParam(); + const Size srcSize = get<0>(params); + const int type = get<1>(params); + + checkDeviceMaxMemoryAllocSize(srcSize, type); + + UMat src(srcSize, type); + /*bool result = false;*/ + randu(src, 0, 10); + declare.in(src); + + OCL_TEST_CYCLE() /*result =*/ cv::hasNonZero(src); + + SANITY_CHECK_NOTHING(); +} + ///////////// Phase //////////////////////// typedef Size_MatType PhaseFixture; diff --git a/modules/core/perf/perf_stat.cpp b/modules/core/perf/perf_stat.cpp index 15ca2e6559..025700c989 100644 --- a/modules/core/perf/perf_stat.cpp +++ b/modules/core/perf/perf_stat.cpp @@ -101,4 +101,20 @@ PERF_TEST_P(Size_MatType, countNonZero, testing::Combine( testing::Values( TYPIC SANITY_CHECK(cnt); } +PERF_TEST_P(Size_MatType, hasNonZero, testing::Combine( testing::Values( TYPICAL_MAT_SIZES ), testing::Values( CV_8UC1, CV_8SC1, CV_16UC1, CV_16SC1, CV_32SC1, CV_32FC1, CV_64FC1 ) )) +{ + Size sz = get<0>(GetParam()); + int matType = get<1>(GetParam()); + + Mat src(sz, matType); + /*bool hnz = false;*/ + + declare.in(src, WARMUP_RNG); + + int runs = (sz.width <= 640) ? 8 : 1; + TEST_CYCLE_MULTIRUN(runs) /*hnz =*/ hasNonZero(src); + + SANITY_CHECK_NOTHING(); +} + } // namespace diff --git a/modules/core/src/has_non_zero.dispatch.cpp b/modules/core/src/has_non_zero.dispatch.cpp new file mode 100644 index 0000000000..6de78ec7a3 --- /dev/null +++ b/modules/core/src/has_non_zero.dispatch.cpp @@ -0,0 +1,107 @@ +// 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 "precomp.hpp" +#include "opencl_kernels_core.hpp" +#include "stat.hpp" + +#include "has_non_zero.simd.hpp" +#include "has_non_zero.simd_declarations.hpp" // defines CV_CPU_DISPATCH_MODES_ALL=AVX2,...,BASELINE based on CMakeLists.txt content + +namespace cv { + +static HasNonZeroFunc getHasNonZeroTab(int depth) +{ + CV_INSTRUMENT_REGION(); + CV_CPU_DISPATCH(getHasNonZeroTab, (depth), + CV_CPU_DISPATCH_MODES_ALL); +} + +#ifdef HAVE_OPENCL +static bool ocl_hasNonZero( InputArray _src, bool & res ) +{ + int type = _src.type(), depth = CV_MAT_DEPTH(type), kercn = ocl::predictOptimalVectorWidth(_src); + bool doubleSupport = ocl::Device::getDefault().doubleFPConfig() > 0; + + if (depth == CV_64F && !doubleSupport) + return false; + + int dbsize = ocl::Device::getDefault().maxComputeUnits(); + size_t wgs = ocl::Device::getDefault().maxWorkGroupSize(); + + int wgs2_aligned = 1; + while (wgs2_aligned < (int)wgs) + wgs2_aligned <<= 1; + wgs2_aligned >>= 1; + + ocl::Kernel k("reduce", ocl::core::reduce_oclsrc, + format("-D srcT=%s -D srcT1=%s -D cn=1 -D OP_COUNT_NON_ZERO" + " -D WGS=%d -D kercn=%d -D WGS2_ALIGNED=%d%s%s", + ocl::typeToStr(CV_MAKE_TYPE(depth, kercn)), + ocl::typeToStr(depth), (int)wgs, kercn, + wgs2_aligned, doubleSupport ? " -D DOUBLE_SUPPORT" : "", + _src.isContinuous() ? " -D HAVE_SRC_CONT" : "")); + if (k.empty()) + return false; + + UMat src = _src.getUMat(), db(1, dbsize, CV_32SC1); + k.args(ocl::KernelArg::ReadOnlyNoSize(src), src.cols, (int)src.total(), + dbsize, ocl::KernelArg::PtrWriteOnly(db)); + + size_t globalsize = dbsize * wgs; + if (k.run(1, &globalsize, &wgs, true)) + return res = (saturate_cast(cv::sum(db.getMat(ACCESS_READ))[0])>0), true; + return false; +} +#endif + +bool hasNonZero(InputArray _src) +{ + CV_INSTRUMENT_REGION(); + + int type = _src.type(), cn = CV_MAT_CN(type); + CV_Assert( cn == 1 ); + + bool res = false; + +#ifdef HAVE_OPENCL + CV_OCL_RUN_(OCL_PERFORMANCE_CHECK(_src.isUMat()) && _src.dims() <= 2, + ocl_hasNonZero(_src, res), + res) +#endif + + Mat src = _src.getMat(); + + HasNonZeroFunc func = getHasNonZeroTab(src.depth()); + CV_Assert( func != 0 ); + + if (src.dims == 2)//fast path to avoid creating planes of single rows + { + if (src.isContinuous()) + res |= func(src.ptr(0), src.total()); + else + for(int row = 0, rowsCount = src.rows ; !res && (row(row), src.cols); + } + else//if (src.dims != 2) + { + const Mat* arrays[] = {&src, nullptr}; + Mat planes[1]; + NAryMatIterator itNAry(arrays, planes, 1); + for(size_t p = 0 ; !res && (p(0), plane.total()); + else + for(int row = 0, rowsCount = plane.rows ; !res && (row(row), plane.cols); + } + } + + return res; +} + +} // namespace diff --git a/modules/core/src/has_non_zero.simd.hpp b/modules/core/src/has_non_zero.simd.hpp new file mode 100644 index 0000000000..6ea8bcd7d2 --- /dev/null +++ b/modules/core/src/has_non_zero.simd.hpp @@ -0,0 +1,327 @@ +// 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 "precomp.hpp" + +namespace cv { + +typedef bool (*HasNonZeroFunc)(const uchar*, size_t); + + +CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN + +HasNonZeroFunc getHasNonZeroTab(int depth); + + +#ifndef CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY + +template +inline bool hasNonZero_(const T* src, size_t len ) +{ + bool res = false; + if (len > 0) + { + size_t i=0; + #if CV_ENABLE_UNROLLED + for(; !res && (i+4 <= len); i += 4 ) + res |= ((src[i] | src[i+1] | src[i+2] | src[i+3]) != 0); + #endif + for( ; !res && (i < len); i++ ) + res |= (src[i] != 0); + } + return res; +} + +template<> +inline bool hasNonZero_(const float* src, size_t len ) +{ + bool res = false; + if (len > 0) + { + size_t i=0; + if (sizeof(float) == sizeof(unsigned int)) + { + #if CV_ENABLE_UNROLLED + typedef unsigned int float_as_uint_t; + const float_as_uint_t* src_as_ui = reinterpret_cast(src); + for(; !res && (i+4 <= len); i += 4 ) + { + const float_as_uint_t gathered = (src_as_ui[i] | src_as_ui[i+1] | src_as_ui[i+2] | src_as_ui[i+3]); + res |= ((gathered<<1) != 0);//remove what would be the sign bit + } + #endif + } + for( ; !res && (i < len); i++ ) + res |= (src[i] != 0); + } + return res; +} + +template<> +inline bool hasNonZero_(const double* src, size_t len ) +{ + bool res = false; + if (len > 0) + { + size_t i=0; + if (sizeof(double) == sizeof(uint64_t)) + { + #if CV_ENABLE_UNROLLED + typedef uint64_t double_as_uint_t; + const double_as_uint_t* src_as_ui = reinterpret_cast(src); + for(; !res && (i+4 <= len); i += 4 ) + { + const double_as_uint_t gathered = (src_as_ui[i] | src_as_ui[i+1] | src_as_ui[i+2] | src_as_ui[i+3]); + res |= ((gathered<<1) != 0);//remove what would be the sign bit + } + #endif + } + for( ; !res && (i < len); i++ ) + res |= (src[i] != 0); + } + return res; +} + +static bool hasNonZero8u( const uchar* src, size_t len ) +{ + bool res = false; + const uchar* srcEnd = src+len; +#if CV_SIMD + typedef v_uint8 v_type; + const v_type v_zero = vx_setzero_u8(); + constexpr const int unrollCount = 2; + int step = v_type::nlanes * unrollCount; + int len0 = len & -step; + const uchar* srcSimdEnd = src+len0; + + int countSIMD = static_cast((srcSimdEnd-src)/step); + while(!res && countSIMD--) + { + v_type v0 = vx_load(src); + src += v_type::nlanes; + v_type v1 = vx_load(src); + src += v_type::nlanes; + res = v_check_any(((v0 | v1) != v_zero)); + } + + v_cleanup(); +#endif + return res || hasNonZero_(src, srcEnd-src); +} + +static bool hasNonZero16u( const ushort* src, size_t len ) +{ + bool res = false; + const ushort* srcEnd = src+len; +#if CV_SIMD + typedef v_uint16 v_type; + const v_type v_zero = vx_setzero_u16(); + constexpr const int unrollCount = 4; + int step = v_type::nlanes * unrollCount; + int len0 = len & -step; + const ushort* srcSimdEnd = src+len0; + + int countSIMD = static_cast((srcSimdEnd-src)/step); + while(!res && countSIMD--) + { + v_type v0 = vx_load(src); + src += v_type::nlanes; + v_type v1 = vx_load(src); + src += v_type::nlanes; + v_type v2 = vx_load(src); + src += v_type::nlanes; + v_type v3 = vx_load(src); + src += v_type::nlanes; + v0 |= v1; + v2 |= v3; + res = v_check_any(((v0 | v2) != v_zero)); + } + + v_cleanup(); +#endif + return res || hasNonZero_(src, srcEnd-src); +} + +static bool hasNonZero32s( const int* src, size_t len ) +{ + bool res = false; + const int* srcEnd = src+len; +#if CV_SIMD + typedef v_int32 v_type; + const v_type v_zero = vx_setzero_s32(); + constexpr const int unrollCount = 8; + int step = v_type::nlanes * unrollCount; + int len0 = len & -step; + const int* srcSimdEnd = src+len0; + + int countSIMD = static_cast((srcSimdEnd-src)/step); + while(!res && countSIMD--) + { + v_type v0 = vx_load(src); + src += v_type::nlanes; + v_type v1 = vx_load(src); + src += v_type::nlanes; + v_type v2 = vx_load(src); + src += v_type::nlanes; + v_type v3 = vx_load(src); + src += v_type::nlanes; + v_type v4 = vx_load(src); + src += v_type::nlanes; + v_type v5 = vx_load(src); + src += v_type::nlanes; + v_type v6 = vx_load(src); + src += v_type::nlanes; + v_type v7 = vx_load(src); + src += v_type::nlanes; + v0 |= v1; + v2 |= v3; + v4 |= v5; + v6 |= v7; + + v0 |= v2; + v4 |= v6; + res = v_check_any(((v0 | v4) != v_zero)); + } + + v_cleanup(); +#endif + return res || hasNonZero_(src, srcEnd-src); +} + +static bool hasNonZero32f( const float* src, size_t len ) +{ + bool res = false; + const float* srcEnd = src+len; +#if CV_SIMD + typedef v_float32 v_type; + const v_type v_zero = vx_setzero_f32(); + constexpr const int unrollCount = 8; + int step = v_type::nlanes * unrollCount; + int len0 = len & -step; + const float* srcSimdEnd = src+len0; + + int countSIMD = static_cast((srcSimdEnd-src)/step); + while(!res && countSIMD--) + { + v_type v0 = vx_load(src); + src += v_type::nlanes; + v_type v1 = vx_load(src); + src += v_type::nlanes; + v_type v2 = vx_load(src); + src += v_type::nlanes; + v_type v3 = vx_load(src); + src += v_type::nlanes; + v_type v4 = vx_load(src); + src += v_type::nlanes; + v_type v5 = vx_load(src); + src += v_type::nlanes; + v_type v6 = vx_load(src); + src += v_type::nlanes; + v_type v7 = vx_load(src); + src += v_type::nlanes; + v0 |= v1; + v2 |= v3; + v4 |= v5; + v6 |= v7; + + v0 |= v2; + v4 |= v6; + //res = v_check_any(((v0 | v4) != v_zero));//beware : (NaN != 0) returns "false" since != is mapped to _CMP_NEQ_OQ and not _CMP_NEQ_UQ + res = !v_check_all(((v0 | v4) == v_zero)); + } + + v_cleanup(); +#endif + return res || hasNonZero_(src, srcEnd-src); +} + +static bool hasNonZero64f( const double* src, size_t len ) +{ + bool res = false; + const double* srcEnd = src+len; +#if CV_SIMD_64F + typedef v_float64 v_type; + const v_type v_zero = vx_setzero_f64(); + constexpr const int unrollCount = 16; + int step = v_type::nlanes * unrollCount; + int len0 = len & -step; + const double* srcSimdEnd = src+len0; + + int countSIMD = static_cast((srcSimdEnd-src)/step); + while(!res && countSIMD--) + { + v_type v0 = vx_load(src); + src += v_type::nlanes; + v_type v1 = vx_load(src); + src += v_type::nlanes; + v_type v2 = vx_load(src); + src += v_type::nlanes; + v_type v3 = vx_load(src); + src += v_type::nlanes; + v_type v4 = vx_load(src); + src += v_type::nlanes; + v_type v5 = vx_load(src); + src += v_type::nlanes; + v_type v6 = vx_load(src); + src += v_type::nlanes; + v_type v7 = vx_load(src); + src += v_type::nlanes; + v_type v8 = vx_load(src); + src += v_type::nlanes; + v_type v9 = vx_load(src); + src += v_type::nlanes; + v_type v10 = vx_load(src); + src += v_type::nlanes; + v_type v11 = vx_load(src); + src += v_type::nlanes; + v_type v12 = vx_load(src); + src += v_type::nlanes; + v_type v13 = vx_load(src); + src += v_type::nlanes; + v_type v14 = vx_load(src); + src += v_type::nlanes; + v_type v15 = vx_load(src); + src += v_type::nlanes; + v0 |= v1; + v2 |= v3; + v4 |= v5; + v6 |= v7; + v8 |= v9; + v10 |= v11; + v12 |= v13; + v14 |= v15; + + v0 |= v2; + v4 |= v6; + v8 |= v10; + v12 |= v14; + + v0 |= v4; + v8 |= v12; + //res = v_check_any(((v0 | v8) != v_zero));//beware : (NaN != 0) returns "false" since != is mapped to _CMP_NEQ_OQ and not _CMP_NEQ_UQ + res = !v_check_all(((v0 | v8) == v_zero)); + } + + v_cleanup(); +#endif + return res || hasNonZero_(src, srcEnd-src); +} + +HasNonZeroFunc getHasNonZeroTab(int depth) +{ + static HasNonZeroFunc hasNonZeroTab[] = + { + (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero8u), (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero8u), + (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero16u), (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero16u), + (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero32s), (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero32f), + (HasNonZeroFunc)GET_OPTIMIZED(hasNonZero64f), 0 + }; + + return hasNonZeroTab[depth]; +} + +#endif + +CV_CPU_OPTIMIZATION_NAMESPACE_END +} // namespace diff --git a/modules/core/test/test_hasnonzero.cpp b/modules/core/test/test_hasnonzero.cpp new file mode 100644 index 0000000000..9834117ddf --- /dev/null +++ b/modules/core/test/test_hasnonzero.cpp @@ -0,0 +1,201 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2000-2008, Intel Corporation, all rights reserved. +// Copyright (C) 2009, Willow Garage 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, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + +#include "test_precomp.hpp" + +namespace opencv_test { namespace { + +typedef testing::TestWithParam > HasNonZeroAllZeros; + +TEST_P(HasNonZeroAllZeros, hasNonZeroAllZeros) +{ + const int type = std::get<0>(GetParam()); + const Size size = std::get<1>(GetParam()); + + Mat m = Mat::zeros(size, type); + EXPECT_FALSE(hasNonZero(m)); +} + +INSTANTIATE_TEST_CASE_P(Core, HasNonZeroAllZeros, + testing::Combine( + testing::Values(CV_8UC1, CV_8SC1, CV_16UC1, CV_16SC1, CV_32SC1, CV_32FC1, CV_64FC1), + testing::Values(Size(1, 1), Size(320, 240), Size(127, 113), Size(1, 113)) + ) +); + +typedef testing::TestWithParam > HasNonZeroNegZeros; + +TEST_P(HasNonZeroNegZeros, hasNonZeroNegZeros) +{ + const int type = std::get<0>(GetParam()); + const Size size = std::get<1>(GetParam()); + + Mat m = Mat(size, type); + m.setTo(Scalar::all(-0.)); + EXPECT_FALSE(hasNonZero(m)); +} + +INSTANTIATE_TEST_CASE_P(Core, HasNonZeroNegZeros, + testing::Combine( + testing::Values(CV_32FC1, CV_64FC1), + testing::Values(Size(1, 1), Size(320, 240), Size(127, 113), Size(1, 113)) + ) +); + +typedef testing::TestWithParam > HasNonZeroLimitValues; + +TEST_P(HasNonZeroLimitValues, hasNonZeroLimitValues) +{ + const int type = std::get<0>(GetParam()); + const Size size = std::get<1>(GetParam()); + + Mat m = Mat(size, type); + + m.setTo(Scalar::all(std::numeric_limits::infinity())); + EXPECT_TRUE(hasNonZero(m)); + + m.setTo(Scalar::all(-std::numeric_limits::infinity())); + EXPECT_TRUE(hasNonZero(m)); + + m.setTo(Scalar::all(std::numeric_limits::quiet_NaN())); + EXPECT_TRUE(hasNonZero(m)); + + m.setTo((CV_MAT_DEPTH(type) == CV_64F) ? Scalar::all(std::numeric_limits::epsilon()) : Scalar::all(std::numeric_limits::epsilon())); + EXPECT_TRUE(hasNonZero(m)); + + m.setTo((CV_MAT_DEPTH(type) == CV_64F) ? Scalar::all(std::numeric_limits::min()) : Scalar::all(std::numeric_limits::min())); + EXPECT_TRUE(hasNonZero(m)); + + m.setTo((CV_MAT_DEPTH(type) == CV_64F) ? Scalar::all(std::numeric_limits::denorm_min()) : Scalar::all(std::numeric_limits::denorm_min())); + EXPECT_TRUE(hasNonZero(m)); +} + +INSTANTIATE_TEST_CASE_P(Core, HasNonZeroLimitValues, + testing::Combine( + testing::Values(CV_32FC1, CV_64FC1), + testing::Values(Size(1, 1), Size(320, 240), Size(127, 113), Size(1, 113)) + ) +); + +typedef testing::TestWithParam > HasNonZeroRandom; + +TEST_P(HasNonZeroRandom, hasNonZeroRandom) +{ + const int type = std::get<0>(GetParam()); + const Size size = std::get<1>(GetParam()); + + RNG& rng = theRNG(); + + const size_t N = std::min(100, size.area()); + for(size_t i = 0 ; i > HasNonZeroNd; + +TEST_P(HasNonZeroNd, hasNonZeroNd) +{ + const int type = get<0>(GetParam()); + const int ndims = get<1>(GetParam()); + const bool continuous = get<2>(GetParam()); + + RNG& rng = theRNG(); + + const size_t N = 10; + for(size_t i = 0 ; i steps(ndims); + std::vector sizes(ndims); + size_t totalBytes = 1; + for(int dim = 0 ; dim(length))*CV_ELEM_SIZE(type); + sizes[dim] = (isFirstDim || continuous) ? length : rng.uniform(1, length); + totalBytes *= steps[dim]*static_cast(sizes[dim]); + } + + std::vector buffer(totalBytes); + void* data = buffer.data(); + + Mat m = Mat(ndims, sizes.data(), type, data, steps.data()); + + std::vector nzRange(ndims); + for(int dim = 0 ; dim0), hasNonZero(m)); + } +} + +INSTANTIATE_TEST_CASE_P(Core, HasNonZeroNd, + testing::Combine( + testing::Values(CV_8UC1), + testing::Values(2, 3), + testing::Values(true, false) + ) +); + +}} // namespace