mirror of
https://github.com/opencv/opencv.git
synced 2026-07-30 15:53:03 +04:00
Broadcasting element-wise engine for cv::Mat (+ cv::texpr) (#29426)
* experimental new arithmetics; work-in-progress * continue working on new-gen arithmetic expressions * * improved performance of the new add on small arrays * added sub * extended tests * fixed potential bug when adding multi-channel array and a single-channel scalar * improved const handling * * added copyMask * added mul/dev (without scale so far) * * accelerated mul * addedd scale to mul and div * * done substantial refactoring; however a few more rounds of refactoring are ahead. * added min, max, absdiff, addweighted. * improved performance of the new arithmetic functions, but some of them are still slow, e.g. operations with mask have some bugs (that affect speed, not accuracy). * * further (significantly) accelerated several functions, especially on small arrays: mul, binary ops with mask * further polished the new arithmetic engine * started integration of the new element-wise arithmetic engine into core * big step forward. We now use the new engine inside cv::add, subtract, multiply, divide, absdiff, min and max. * big progress: * added bitwise operations * fixed and accelerated compare * ported regression tests to test new broadcasting behaviour of arithmetic functions * lot's of improvements in compare, divide, addWeighted! * lot's of small and big performance improvements in the new arithmetics * * some more optimizations; parsing texpr-expressions is now faster as well * port new_arithm to Linux/x86: dispatch guards, scalar-Mat compat fallback, dnn shape-contract fixes Core: - arithm.simd.hpp: CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY guards (the file is included once per dispatched mode on x86), vx_load_expand instead of the 128-bit v_load_expand, VTraits::vlanes() instead of ::nlanes - arithm.cpp/precomp.hpp: compat fallback for scalar-like Mat operands (1x1, 1xcn/cnx1, 4x1 CV_64F - java/python tuples, operator-(Mat, Matx)): treated as a per-channel scalar ONLY when the shapes are not broadcast-compatible, so every valid numpy-style broadcast keeps its meaning and calls that would otherwise throw get the 4.x semantics DNN (fallout of the stricter shape semantics, found by the new engine): - dict.hpp: DictValue relied on fresh AutoBuffer having size()==fixed_size; allocate explicitly - batch_norm: weights_/bias_ are 1-D [n] now; 0/1-D forward runs on exact-shape 1-D views - net_impl2: extend the post-forward sanity check to non-temp outputs - a layer that reallocates its preallocated output tensor now fails loudly instead of silently detaching the result from the graph - LSTM/LSTM2 batchwise (layout=1): getMemoryShapes now matches what forward() writes (ONNX: Y=(batch,seq,dirs,hid), Yh/Yc=(batch,dirs,hid)); forward assembles seq-major results in a local buffer and transposes INTO the preallocated outputs in place Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * u8/s8 multiply: exact integer SIMD path on non-FP16 builds (3-10x vs 5.x) The unit-scale branch of vecBinaryKernel already supported a separate work-vector type Wvec1 (used by the ARM f16 build and by u16/s16 everywhere), but on x86 the u8/s8 same-type multiply still went through the f32 hub. Route it through v_uint16/v_int16: products of 8-bit values fit exactly (255^2 < 2^16), the saturating pack on store gives bit-exact results at half the vector traffic. Also fix a latent kernel bug this exposed: the unit-scale branch stepped by Wvec's lane count while loading/storing Wvec1 vectors. All previous Wvec1 instantiations had equal lane counts, but u8's v_uint16 has 2x the lanes of v_float32 - the pairs overlapped (50% redundant work) and the tail backoff could write VECSZ bytes past the row end. The branch now derives its step, offsets and tail condition from Wvec1 itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vecBinaryKernel: constexpr Op::useScalar instead of a runtime-only scale check Every binary op functor now declares whether it consumes the scale scalar (params[0]): true only for mul and the two div variants. Ops that ignore it (add/sub/min/max/absdiff) take the fast 2-arg branch unconditionally - the 'scalar == 1' check used to fail for them (their params[0] is 0), sending them through the preproc branch, and the condition now folds at compile time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * restore cv::hal::mul8u as a wrapper over the element-wise engine The symbol is still declared in core/hal/hal.hpp and called directly by external code (the G-API fluid backend in opencv_contrib), but its implementation went away with the old arithm kernels. Forward it to getMulFunc(CV_8U, CV_8U) - with scale==1 it lands on the new exact integer SIMD path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * silence every new-arithm warning reported by CI (ARM64/Mac) and gcc 15 - arithm.cpp: bitwise_op_ocl and the actualScalarDepth/coerceTypes helpers are consumed only by the OpenCL paths - guard them with HAVE_OPENCL; haveScalar in cv::compare is read only inside CV_OCL_RUN - CV_UNUSED for OpenCL-less builds - arithm_expr.hpp: declare getBitwiseFunc/getNotFunc/getAddWeightedFunc next to the other per-op entry points (-Wmissing-prototypes in arithm.dispatch.cpp) - arithm.simd.hpp: define CV_SIMD_16F to 0 when FP16 SIMD is absent (-Wundef); {}-init the expandScalar staging buffers (-Wmaybe-uninitialized: they are fully written before use, but the compiler cannot prove it with runtime vector widths); rename the compare kernel's lambda parameter (-Wshadow) - arithm_expr.cpp: rename the exec tile-lambda's hot-field locals that shadowed TExpr members and outer locals (-Wshadow) - test_new_arithm_extensive.cpp: rename the name-generator lambdas' parameter shadowing the INSTANTIATE macro's own (-Wshadow), drop an unused variable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * restore 4.x scalar semantics for the bindings' 4x1 CV_64F Scalar columns The python/java bindings materialize numbers and tuples as a 2-D 4x1 CV_64F Mat - or UMat, when the call carries UMat arguments. Three CI-reported python failures came from those pseudo-scalars reaching the engine as arrays: - absdiff(int_arr, 0): the (4,1) column is broadcast-COMPATIBLE with a 1-D array, so numpy semantics silently won - an outer-product f64 result instead of the int per-channel-scalar one; - subtract(u8 4x8x4, (40,)): same, by the rows==4 coincidence; - multiply(UMat, 2., dst=UMat): the scalar arrives as a UMAT, which the scalar detection did not recognize at all. isScalarArg now treats the exact bindings shape - 2-D 4x1 CV_64F single-channel Mat/UMat against a <=4-channel array - as a scalar UNCONDITIONALLY (a 1-D [4] array has dims==1 and still broadcasts). One exception, decided in arithm_op: when the partner is itself a tiny scalar-shaped array, both are honest data and ride the broadcast (compare(Mat 4x1, Mat 1x1) - issue #8999 - stays elementwise). Small-array discipline, this all runs per engine call: the probes read Mat/UMat fields directly (rows == 4 alone rejects almost everything, no _InputArray getter dispatch), and a UMAT scalar's 32 bytes are copied into a caller-stack buffer - no heap, no getMat mapping. Measured: no latency change on 4x4/16x16 element-wise calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::texpr: std::string_view -> const std::string& in the public API string_view in an exported signature breaks some CUDA toolchain builds, and for short expression strings the difference is immaterial (SSO, parsed once). The parser internals keep string_view - the argument converts implicitly. Also drop the now-unused <string_view> include from cvstd.hpp, so the header does not reach every nvcc TU. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * compare boundary-rewrite: fixed 4-slot kind/bound arrays instead of AutoBuffers A CONST operand is capped at 4 channels (addConst), so the per-channel kind/bound staging needs no dynamic buffers - plain int[4]/double[4], with a CV_Assert on the contract. This is also what gcc's -Wmaybe-uninitialized was flagging (it could not see the AutoBuffer's inline storage get filled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * element-wise engine: unary math kernels (sqrt/exp/log/sin/cos/tanh/erf/relu) + select New dispatched pair math.simd.hpp / math.dispatch.cpp - the unary/ternary sibling of arithm.simd.hpp: - vecUnaryKernel: T -> T over f16/bf16/f32/f64 on top of the intrin_math primitives (v_exp/v_log/v_sin/v_cos/v_sqrt/v_erf/v_max). f32/f64 compute natively, f16/bf16 ride the f32 hub inside the kernel (vx_load_pair_as / v_store_pair_as) - no materialized casts. Continuity collapse + the halide right-edge backoff, suppressed in-place (it would re-apply Op to already-written values). tanh = (e^2x-1)/(e^2x+1) with the input clamped to +/-10 (f32) / +/-20 (f64) - unclamped saturation hits inf/inf = NaN. erf has no f64 SIMD primitive: std::erf per lane. - selectKernel(mask, x, y): 1-byte mask expanded to lane width and tested against zero in the INTEGER domain (immune to DAZ/FTZ), branches of any depth by element size, broadcast branches supported. - emitUnary: math over a float input is T -> T now (f16 in -> f16 out, native kernel when input and result depths match); integer inputs still compute in the float domain and land in f32. - emitTernary/select: literal branches are typed via typedConstFrom (an OP_CAST of a depth-less flex const crashed); a non-1-byte mask is normalized by an explicit "mask != 0" compare, never a value cast. texpr already parsed the function names - they now execute. Tests: per-depth accuracy of all 8 ops against the double std:: reference, integer input, in-place, select over 4 depths / const branch / float mask. Perf vs the classic kernels (1920x1080 f32, 16 threads, AVX2): exp 3.7x, log 2.6x, sqrt 5.7x faster; polarToCart expressed as (r*cos(a), r*sin(a)) 4.0x. Accuracy improves too (max rel err vs f64 reference): exp 8.1e-8 vs 2.1e-7, log 8.0e-8 vs 1.5e-7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: OP_COPY_MASK folded into OP_SELECT; selectKernel moved to arithm.simd.hpp copyMask(dst, mask, src) is select(mask, src, dst) - one masking primitive instead of two. The compiler emits the masked-op tail as addInsn(OP_SELECT, mask, r, out, out): the output slot rides as both arg2 and the result, so unmasked elements are preserved by reading them back through the b-branch. OP_COPY_MASK, copyMaskKernel and getCopyMaskFunc are gone. selectKernel (moved from math.simd.hpp to arithm.simd.hpp) inherits every copyMaskKernel optimization: - the interleaved multichannel fast path (2..4 channels under a per-pixel mask: expand the mask once per VECSZ rows, v_store_interleave across lanes); - the per-row scalar path with the row-skip when the selected source row IS dst (the "leave the output untouched" half of copyMask); - plus the select-specific ones: branch broadcasts (stepx == 0) and the right-edge tail backoff under dst-aliases-a-branch - safe because re-running select over already-blended elements is idempotent; only dst == mask keeps the backoff off (the store would rewrite mask bytes before the re-read). Masked-add perf is on par with the old copyMask (1280x720, 1 thread: 8UC3 204 -> 198 us, 32FC3 1395 -> 1344, 8UC1/32FC1 within noise). Full core suite 24117 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: dedicated vectorized pow kernel (moved from arithm to math.simd.hpp) pow was the last scalar-only binary op (scalarBinaryKernel + std::pow, f32/f64 only). The new powKernel keeps exact std::pow semantics and is T x T -> T over all four float depths (f16/bf16 via the f32 hub): - scalar exponent (the dominant call shape - texpr literals ride as 0-dim broadcast consts) is dispatched PER ROW to the special cases: y==2 -> x*x, y==3 -> x*x*x, y==0.5 -> v_sqrt, y==1 -> copy, y==0 -> fill 1; - everything else - including a per-element exponent array - runs the general vectorized exp(y * log(x)) path, valid for x > 0; a vector pair containing any x <= 0 lane falls back to scalar std::pow for that pair (v_check_any), which preserves every std::pow subtlety: signed results for integer y on negative bases, NaN for fractional y, the x == 0 family; - no right-edge tail backoff: pow is not idempotent, in-place calls finish rows in the scalar tail. Perf vs the classic cv::pow (1920x1080 f32, 16 threads, AVX2): p=2 1.4x (classic special-cases it too), p=3 6.1x, p=0.5 6.3x, fractional p 4.5x with slightly better accuracy (6.1e-7 vs 7.4e-7 max rel err). Tests: exponent sweep 2/3/0.5/1/0/2.5/-1.5 vs the double std::pow reference on f32/f64, negative bases (exact signed cubes, NaN for fractional), array exponent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * powKernel: halide right-edge tail backoff in every SIMD loop Same shape as vecBinaryKernel: the final partial vector re-processes [width - VECSZ*2, width) instead of finishing scalar, suppressed when dst aliases an input (pow is not idempotent - the overlap region must be recomputed from an untouched source, which the no-alias case guarantees). Modest measured win (~2% on ROI rows for the special-cased exponents; the general path tail was already cheap - modern libm powf is fast), no regressions; mainly aligns the kernel with the house style, where every SIMD loop ends vector-wide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix MSVC 2019 C2975: function-local constexpr as a template argument inside a lambda MSVC 2019 loses the constexpr-ness of function-local constants (LOCAL_OPS, MAX_DIMS, ...) when they are used as template arguments inside a lambda body (AutoBuffer<Slice, LOCAL_OPS> / std::array<int, MAX_DIMS> in the parallel bodies of BroadcastOp::run and TExpr::exec). Hoist them to namespace scope - no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ocl_arithm_op: route 16U multiply to the CPU engine on Apple OpenCL The Apple OpenCL driver miscompiles the 16U multiply kernel: products near the top of the u16 range come back wrapped instead of saturated (CPU vs GPU NORM_INF up to 65535 in OCL_Arithm/Mul.Mat CV_16U cases). The same arithm.cl kernel is correct on Intel NEO and NVIDIA drivers - verified not to reproduce on Linux/Intel iGPU - so gate the decline to __APPLE__ only; the CPU engine computes 16u multiply exactly (integer SIMD path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: neg/abs as compositions, clamp kernel, ** and ?: operators, abs(a-b) peephole - OP_NEG and OP_ABS need no kernels: neg = sub(0, a), abs = absdiff(a, 0) - including the engine absdiff auto-type rule (signed |a| lands in the UNSIGNED type of the same width: |SHRT_MIN| fits u16 exactly instead of saturating; NB the public cv::absdiff auto depth keeps the source type for 4.x compatibility - values agree, the depth rule is the engine own). - peephole: abs(x - y) rewrites to absdiff(x, y) ALWAYS. On integers the literal semantics differ (the subtract saturates first: u8 gives max(x-y, 0)), but whoever writes abs(a - b) means absdiff - we deliberately hand out the useful semantics instead of the saturation artifact. The just- emitted OP_SUB is retired via the moveToOutput manoeuvre, so the program shrinks to the single absdiff instruction. abs(x), abs(x - 0) and absdiff(x, 0) all give one result. - OP_CLAMP kernel (arithm.simd.hpp): v_min(v_max(x, lo), hi) over u8/s8/u16/s16/u32/s32/f32 (+f64 with 64-bit SIMD), scalar f16/bf16/64-bit ints; lo/hi may broadcast (the common clamp(img, a, b) shape) or be full arrays; the tail backoff stays on under dst-aliases-x (clamp is idempotent). emitTernary types literal bounds via typedConstFrom (same flex-const crash select had) and keeps the auto result type pinned to x. - parser: "a ** b" == pow(a, b), precedence above * /, RIGHT-associative (a ** 2 ** 3 == a ** 8); "cond ? a : b" == select(cond, a, b), precedence below everything, right-associative chains (f1 ? a : f2 ? b : c) work without parentheses. parseTernary() is the expression entry point now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::exp/log/sqrt on the engine via math_op; hal functions wrapped as engine kernels math_op is the master function of the unary math family (the arithm_op analogue): same-shape same-type output over f16/bf16/f32/f64 (classic exp/log accepted f32/f64 only - the half floats are new), two tiers: - small (<= 100000 elements) and continuous: call the kernel DIRECTLY over the flattened data - no TExpr, no broadcastOp, no parallel_for setup; - everything else: the usual single-instruction program via compile()/exec() (parallelism for large arrays, real steps for ROIs). getMathFunc routes OP_EXP/OP_LOG at f32/f64 through the full cv::hal stack - an external vendor HAL (CALL_HAL), IPP, or the built-in table kernels, whichever is installed - by wrapping hal::exp32f/exp64f/log32f/log64f as engine kernels with the function pointer in TKernel::userdata, the same mechanism castKernel uses for core BinaryFuncs. The engine adds tiling and parallelism on top, so every tier gets the best available scalar-span implementation. v_exp/v_log remain for f16/bf16 (the f32 hub) and the ops hal has no entry points for. v_log_default_32f: the degree-8 polynomial is evaluated by Estrin pairing (4 dependent levels) instead of an 8-FMA Horner chain (~5% on the f16 hub path). An exp64 Taylor-without-division rewrite was tried and benched SLOWER than the Cephes Pade scheme (the evaluation is FMA-throughput-bound, and vdivpd pipelines well enough) - reverted; a table-based reduction is the only way further there. cv::exp f32 (16 threads, AVX2+IPP build), old -> new: 640 elements 0.16 -> 0.15 us, 16k 2.79 -> 2.49, 640x480 47 -> 20, 1920x1080 452 -> 60 us (the old CPU loops were single-threaded); f64 exp 1080p 1295 -> 161 us. No size regresses; small arrays now run at installed-HAL speed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * getMathFunc: IPP tier + raw-HAL probing; the exp/log table kernels are deleted The raw cv_hal_* entry points return int for a reason: without an installed HAL they are stubs returning CV_HAL_ERROR_NOT_IMPLEMENTED. getMathFunc now selects the exp/log implementation in three tiers: 1. HAVE_IPP && ipp::useIPP(): ippsExp/ippsLn through thin int adapters (IPP is not routed through the cv_hal_ hooks, so it needs its own tier); 2. the raw cv_hal_exp32f/... hook, PROBED once with a 1-element call on the safe input 1.0 (cached in magic statics): implemented -> wrapped as an engine kernel with the function pointer in TKernel::userdata; 3. the engine own v_exp/v_log kernels. Whichever wins, the engine adds tiling and parallelism on top. The EXPTAB/LOGTAB table kernels and their tables (~790 lines in mathfuncs_core.simd.hpp + mathfuncs.cpp) are DELETED: they benched within ~15% of v_exp/v_log, not worth a second implementation. The public cv::hal::exp32f/exp64f/log32f/log64f keep their contract - CALL_HAL, then IPP, then the built-in implementation - but the built-in is now the engine vector kernel via ew::mathSpanEngine (one contiguous span, exported from math.dispatch.cpp). All unary math kernels (vec/scalar/hal wrappers, pow) also handle the vertical-broadcast tile (s0y == 0, a row expanded into a matrix): the first row is computed, the rest are memcpy of it - transcendentals cost far more than a row copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * texpr: hypot(x, y) binary op (alias: mag) hypot = sqrt(x^2 + y^2), NAIVE like cv::magnitude (not the overflow-safe std::hypot), computed in the float work type; kernels for the four float depths only (T x T -> T; integer inputs ride the usual f32-compute + cast). A 10-line EwHypot functor on top of vecBinaryKernel in arithm.simd.hpp - broadcast branches, continuity collapse and the tail backoff come for free. Registered in the parser as both "hypot" (the C/numpy name) and "mag" (the cv::magnitude-flavored alias). A building block for the future cartToPolar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * texpr: atan2(y, x) binary op - radians, standard C range v_atan2 (arithm.simd.hpp, generic over the universal-intrinsic float vector): the fastAtan2 minimax polynomial from mathfuncs_core v_atan_f32 reworked to plain radians - the 180/pi factor dropped from the coefficients and the C quadrant logic instead of the [0, 360) wrap, so the result matches std::atan2 over (-pi, pi]. Measured absolute accuracy ~1.6e-4 rad. (v_atan_f32 itself is untouched - cv::phase/fastAtan2 keep their degree semantics.) EwAtan2 rides vecBinaryKernel: f16/bf16/f32 through v_atan2 (the f32 hub), f64 through exact scalar std::atan2. arg0 = y, arg1 = x, like std::atan2; float depths only, same emitBinary policy as pow/hypot. Parser name "atan2". Together with hypot this completes the cartToPolar building blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix the RISC-V RVV build and two ARM64 warnings The new f64 kernel registrations (hypot, pow, the unary math family) gate on CV_SIMD_64F || CV_SIMD_SCALABLE_64F, but the vx_setall_as(const double*, v_float64&) helper family in arithm.simd.hpp was still CV_SIMD_64F-only - scalable platforms (RVV) have v_float64 with CV_SIMD_64F == 0, so vecBinaryKernel<double, ...> failed to instantiate there. Widen the helper gate to match (verified with a riscv64 rv64gcv cross-build of opencv_core - the engine f64 paths now vectorize on RVV instead of not compiling). cv::exp/cv::log: the depth local is consumed by CV_OCL_RUN only - CV_UNUSED for OpenCL-less builds (ARM64 -Wunused-variable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * silence the remaining ARM64 gcc warnings - compare boundary-rewrite: {}-init the fixed kind/bound arrays (filled for every channel used below, but gcc cannot prove it across the cn <= 4 loop); - cv::exp/log: [[maybe_unused]] on the depth local (consumed by CV_OCL_RUN only), instead of the CV_UNUSED idiom; - AutoBuffer::reserve: a targeted -Wmaybe-uninitialized suppression around the live-element copy loop - only [0, sz) is read, all written before, but gcc inlining a grow-from-inline-storage chain cannot see that. An annotation for the analyzer, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * saturating 32-bit add/sub kernels; cv::texpr python binding; two CI warnings - v_add_sat/v_sub_sat for v_int32/v_uint32, local to arithm.simd.hpp for now (the plan is to grow them into proper universal intrinsics later): NEON single-instruction vqadd/vqsub, elsewhere the Hacker Delight bit tricks over universal intrinsics (u32 add is 2 ops: or with the wrapped-compare mask). EwAdd/EwSub overload vec() for the 32-bit lanes and getAddSubFunc routes 32S/32U T->T through vecBinaryKernel instead of the former pure scalar kernel. Semantics unchanged - the scalar int64 tail already saturated; directed boundary tests added (both rails, 0 - INT_MIN, u32 cases, a full-range random block vs an exact int64 reference). 640x480 32S add: 0.48x of 5.x -> parity (memory-bound); 1080p: 6-8x. - cv::texpr becomes CV_EXPORTS_W: python gets cv.texpr(expr, [inputs]) -> tuple of ndarrays, so `res, = cv.texpr(...)` and `mag, ang = cv.texpr(...)` unpacking both work. modules/python/test/test_expr.py covers arithmetic, the fused abs(a-b), casts, broadcasting, ?: and ** operators, math functions vs numpy, clamp, named temporaries, tuple outputs, the one-line cartToPolar and the int32 saturation cases. - warnings: {}-init the parser args array (gcc -Wmaybe-uninitialized on Ubuntu 20/22); the compare short-row block gates sizeof(T) <= 4 as constexpr so the f64 instantiation does not leave set-but-unused locals (gcc 9 -Wunused-but-set-variable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * arithm_op: direct-kernel fast path for small continuous arrays Building and compiling the 1-instruction program plus the BroadcastOp setup costs ~40-250ns per call - negligible on big images, dominant at 127x61-class sizes where the classic 5.x functions were 1.3-2x faster. Mirror math_op two tiers in arithm_op: two same-type same-shape continuous arrays, no mask, no scalar, result depth == input depth, <= 100k elements -> call the T x T -> T kernel directly over the flattened elements (checks ordered cheapest-first). Applies to add/subtract/min/max/absdiff/multiply/addWeighted/and/or/xor; compare and divide lower to more than a single kernel (boundary rewrites, int guards) and keep the ordinary path. addWeighted falls through automatically for the 32/64-bit int types whose lowering is wide-compute + cast (getElemwiseFunc returns no direct kernel there). 127x61 vs 5.x, was -> now: add/subtract 8UC1 0.76x -> 1.3x, min/max u8 0.6x -> ~1x, addWeighted 1.0x -> 1.1-1.4x (32SC1 stays 4.5x); the one remaining laggard is add/sub 32SC1 (0.74-0.82x) - the price of the new SATURATING semantics (7-instruction AVX2 emulation vs the wrapping single add of 5.x; single-instruction on NEON). The 127x61 size is ADDED PERMANENTLY to the arithmetic/addWeighted/compare perf grids: per-call overhead regressions in these base functions must be caught by CI, not discovered by users. dst creation goes through createSameSize (whole-shape transfer including layout and future metadata, not piecemeal dims+sizes) here and in math_op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::pow rebuilt on the engine; integer-exponent and 1/sqrt(x) kernel branches Routing: p = 0/1/2 keep their early special cases (fill/copy/multiply); an INTEGER array with an INTEGER power keeps the classic iPow multiply chain - bit-exact compatibility, including its wrap-around quirks (iPow squares in int, so e.g. pow(255,4) on u8 wraps negative and saturates to 0 - somebody may rely on that). Everything else goes through the engine with the math_op two-tier scheme: small continuous arrays call powKernel directly (the exponent rides as a broadcast T scalar), the rest run the tiled parallel program. Integer arrays with fractional powers compute in the float domain and saturate back; the 32U/64-bit depths (classic iPow asserted on them) and f16/bf16 (the classic float path misread them) now just work. powKernel gets two new per-row exponent branches: - p == -0.5: 1/v_sqrt(x) (the classic path used IPP ippsInvSqrt_A21, a 21-bit approximation; ours is exact - slightly slower on small arrays, 4.5x faster at 1080p via parallelism); - any other INTEGER p (|p| <= 65536): LSB-first binary exponentiation, the same multiply chain and order as iPow, fully vectorized - a few ulp accurate vs ~2e-7 of exp(p*log x), and exact on non-positive bases (the sign falls out of the multiplies, 0^negative divides to inf) - no scalar patching. cv::pow f32 vs 5.x: p=0.5 365 -> 61 us at 1080p (6x), p=3 7.7x, p=5 7x AND faster at every size (the old scalar chain: 0.81 -> 0.47 us at 127x61), p=2.5 5.3x. The s16^5 iPow path is untouched (118.7 == 118.5 us). pow_exponents accuracy tests extended to 11 exponents x f32/f64 against the double std::pow reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: SANITY_CHECK_NOTHING for the tests whose grids got the 127x61 size The 127x61 entry added to guard per-call overhead has no regression data in opencv_extra, so the legacy SANITY_CHECK in addWeighted/compare failed on CI (locally it passes silently without the test-data path). Accuracy of both functions is covered by the accuracy suite; the perf tests should measure time. PatchNaNs/finiteMask keep their SANITY_CHECK - their grids are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix a temp-buffer double release in the liveness pass ("d*d" crash) When the same temp is passed as SEVERAL arguments of its last-use instruction (e.g. the named-intermediate expression "d = {0} - {1}; d*d", where the MUL consumes slot d twice), the buffer-reuse scan pushed the temp's physical buffer onto the free list once per argument. That overflows the ntemps-sized freeBufs array (caught by the AutoBuffer range check in Debug: python test_expr.py::test_named_temporary) and, in larger programs, would hand the same physical buffer to two live temps. Release the buffer once by retiring lastUse[t] after the first hit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * executor: recalibrate opCost to vectorized cycles + clamp the stripe count The per-element op costs fed into the parallel_for_ stripe hint were scalar-era estimates (~20x above the vectorized reality: the atan2/exp polynomials run at ~1.5 cycles/element, not 30). The hint therefore split transcendental/divide work into hundreds of ~1 us jobs, which the macOS/GCD backend dispatches poorly under sustained load, on top of the P/E-core equal-share straggler effect. Measured on M4 Max (12P+4E), sustained medians @1920x1080 f32: texpr atan2 320 -> 133 us, cv::exp 280 -> 141 us, cv::log 301 -> ~200 us, cv::pow(x,2.5) 388 -> 376 us; the PR tables' math rows improved ~1.5-2x across the board. - opCost is now in units of ~1/4 cycle/element of the SIMD kernels: cheap ops 1 (unchanged), div/sqrt/hypot/convert_scale 10 -> 3, transcendentals 30 -> 6. - the stripe hint is clamped by min(4*nthreads, max(32, 3*nthreads)): ~4 stripes/thread is plenty of granularity for element-wise work, and the ceiling is 32 pieces except on machines with many (heterogeneous) cores, where anything coarser than ~3 pieces/worker turns the slow cores into equal-share bottlenecks (measured: 32 stripes on 16 threads is the worst point of the curve - 193 us vs 137 us at 48 for atan2). getNumThreads() is clamped from below (WINRT/plugin backends may report 0). Not addressed here (needs cross-machine data, M2/M3 Ultra): streaming memory-bound ops saturate the M4 Max fabric at ~8 fat stripes and E-core participation only adds contention - a cost model cannot express that; candidate follow-up is a bytes-aware clamp or a GCD-backend-level fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * multiply: v_mul_sat integer kernels (full product clamped to the type) v_mul_sat(V, V) -> V for u8/s8/u16/s16/u32/s32 - the full-precision product clamped to the lane type, which is exactly cv::multiply's integer semantics at scale == 1. Local to arithm.simd.hpp for now, next to v_add_sat/v_sub_sat, to be promoted into proper universal intrinsics later. NEON: widening vmull + saturating narrow (vqmovn); other backends: the portable v_mul_expand + saturating v_pack composition for 8/16-bit lanes. 32-bit lanes have no universal widening multiply (no v_mul_expand for s32), so the 32-bit integer fast path is NEON-only for now and the other backends keep the previous f64 work-vector kernels (which measure well on x86 with IPP-free AVX2). EwMul::vec() now routes the integer lane types through v_mul_sat, and getMulFunc_ passes the NATIVE lane vector as the scale==1 fast-path type: whole-register loads/stores, the widening happens inside the multiply. Replaces both the half-register widening loads (u8/s8/u16/s16) and the scalar-equivalent f64 path for 32S/32U on NEON. M4 Max, 640x480 (the sizes where the old kernels lost to carotene): 8U 22.0 -> 13.6 us, 8S 15.0 -> 11.1, 16S 21.2 -> 17.4, 32S 84.8 -> 30.5 (parity with the classic path everywhere, 32S was 0.38x). 1920x1080: 8S 1.30x -> 1.78x, 16S 2.57x -> 2.98x, 32S 2.20x -> 4.02x vs 5.x. Correctness: exhaustive 8-bit (all 65536 pairs per sign), directed saturation corners for 16/32-bit (46341^2, INT_MIN*-1, 65536*65536, all sign combinations) against an exact int64 reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * NEON: make v_cvt_f64(v_int32) exact (was via f32, losing bits > 2^24) The NEON implementations of v_cvt_f64/v_cvt_f64_high for v_int32 did s32 -> f32 -> f64 (vcvt_f32_s32 + vcvt_f64_f32), silently rounding any |x| > 2^24. Every vectorized f64 work path with int32 inputs on AArch64 was affected: the engine's addWeighted/divide 32S kernels, convertTo 32S -> 64F, etc. Found via addWeighted 32SC1 on values ~1e9: max error was 32 vs the exact double reference (the classic carotene path is worse still - it computes in f32 end-to-end with f32-truncated weights, max error 96 on the same data). The exact sequence sxtl + scvtf (vmovl_s32 + vcvtq_f64_s64) is the same 2 instructions, so there is no cost. addWeighted 32SC1 on the engine now matches the exact-double reference bit-for-bit and stays at parity/1.4x vs the classic path (640x480/1920x1080). Pre-existing upstream bug (same code in 4.x) - worth a standalone backport with directed large-value tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * math_op: drop the temporary direct-IPP tier for exp/log Upstream moved the IPP math wrappers into the hal/ipp HAL module (cv_hal_exp32f/ log32f & co now resolve to ipp_hal_* which honor cv::ipp::useIPP via CV_HAL_CHECK_USE_IPP). The engine's single probeHalUnary(cv_hal_*) probe already picks that up uniformly, so the stopgap #ifdef HAVE_IPP ippExp/ippLog tier and its ipp::useIPP() branch in getMathFunc are now redundant - removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * arithm: restore cv::hal::and8u/or8u/xor8u/not8u as engine wrappers These public CV_EXPORTS entry points (core/hal/hal.hpp) lost their definitions when the bitwise ops moved to the element-wise engine, but they are still declared and called by other modules (opencv_objdetect's aruco) and external code - the link broke with undefined references to cv::hal::and8u/xor8u. Restore them as thin forwarders over the engine's byte-wise bitwise kernels (getBitwiseFunc / getNotFunc), mirroring the existing mul8u wrapper. CV_Assert guards the kernel lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * blobdetector: guard empty contour when computing blob radius The new AutoBuffer leaves its tail uninitialized for trivial types, which surfaced a -Wmaybe-uninitialized in findBlobs where the median of per-point distances is read. Use a std::vector, default the radius to 0, and compute the median only for a non-empty contour - no unproven size invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2514,10 +2514,14 @@ TEST(Compare, empty)
|
||||
|
||||
TEST(Compare, regression_8999)
|
||||
{
|
||||
// Issue #8999 predates broadcasting element-wise ops: comparing a 4x1 array against a 1x1 operand
|
||||
// used to throw (both look like a Scalar). It now broadcasts the 1x1 operand across the 4x1 array.
|
||||
Mat_<double> A(4,1); A << 1, 3, 2, 4;
|
||||
Mat_<double> B(1,1); B << 2;
|
||||
Mat C;
|
||||
EXPECT_THROW(cv::compare(A, B, C, CMP_LT), cv::Exception);
|
||||
cv::compare(A, B, C, CMP_LT);
|
||||
Mat expected = (Mat_<uchar>(4,1) << 255, 0, 0, 0); // A < 2
|
||||
EXPECT_EQ(0, cvtest::norm(C, expected, NORM_INF));
|
||||
}
|
||||
|
||||
TEST(Compare, regression_16F_do_not_crash)
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
// 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.
|
||||
|
||||
// Tests for the public cv::texpr() string frontend. Exercises placeholders, operator precedence,
|
||||
// function calls, type casts, assignments and tuple (multi-) outputs. Limited to ops with kernels
|
||||
// today (arithmetic / cast / pow / min / max / absdiff).
|
||||
|
||||
#include "test_precomp.hpp"
|
||||
#include "../src/arithm_expr.hpp" // ew::absdiffResultDepth - the engine's absdiff auto-type rule
|
||||
|
||||
namespace opencv_test { namespace {
|
||||
|
||||
static Mat expr1(const String& e, const std::vector<Mat>& in)
|
||||
{
|
||||
std::vector<Mat> out;
|
||||
cv::texpr(e, in, out);
|
||||
return out[0];
|
||||
}
|
||||
|
||||
TEST(Core_TExpr, add)
|
||||
{
|
||||
Mat a(12, 15, CV_32F), b(12, 15, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = expr1("{0} + {1}", { a, b });
|
||||
Mat exp; cv::add(a, b, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Built-in binary functions min/max/absdiff parsed and dispatched through emitBinary.
|
||||
TEST(Core_TExpr, minmax_absdiff)
|
||||
{
|
||||
Mat a(18, 21, CV_8U), b(18, 21, CV_8U);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0, 255);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0, 255);
|
||||
|
||||
Mat gmin = expr1("min({0}, {1})", { a, b });
|
||||
Mat gmax = expr1("max({0}, {1})", { a, b });
|
||||
Mat gabs = expr1("absdiff({0}, {1})", { a, b });
|
||||
|
||||
Mat emin, emax, eabs;
|
||||
cv::min(a, b, emin); cv::max(a, b, emax); cv::absdiff(a, b, eabs);
|
||||
EXPECT_EQ(0, cvtest::norm(gmin, emin, NORM_INF));
|
||||
EXPECT_EQ(0, cvtest::norm(gmax, emax, NORM_INF));
|
||||
EXPECT_EQ(0, cvtest::norm(gabs, eabs, NORM_INF));
|
||||
}
|
||||
|
||||
// Operator precedence: '*' binds tighter than '+', unary minus on a literal.
|
||||
TEST(Core_TExpr, addweighted_precedence)
|
||||
{
|
||||
Mat a(20, 16, CV_32F), b(20, 16, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = expr1("{0} * 2.5 + {1} * -1.5 + 7", { a, b });
|
||||
Mat exp; cv::addWeighted(a, 2.5, b, -1.5, 7.0, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Named temporary via ';' assignment.
|
||||
TEST(Core_TExpr, assignment)
|
||||
{
|
||||
Mat a(18, 22, CV_32F), b(18, 22, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = expr1("t = {0} * 2.5; t + {1}", { a, b });
|
||||
Mat exp; cv::addWeighted(a, 2.5, b, 1.0, 0.0, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Tuple -> several outputs.
|
||||
TEST(Core_TExpr, tuple_outputs)
|
||||
{
|
||||
Mat a(14, 19, CV_32F), b(14, 19, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
std::vector<Mat> out;
|
||||
cv::texpr("({0} + {1}, {0} - {1})", std::vector<Mat>{ a, b }, out);
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
Mat eadd, esub; cv::add(a, b, eadd); cv::subtract(a, b, esub);
|
||||
EXPECT_LE(cvtest::norm(out[0], eadd, NORM_INF), 1e-3) << "sum";
|
||||
EXPECT_LE(cvtest::norm(out[1], esub, NORM_INF), 1e-3) << "diff";
|
||||
}
|
||||
|
||||
// Grouping parens (NOT a tuple) inside a larger expression.
|
||||
TEST(Core_TExpr, grouping_parens)
|
||||
{
|
||||
Mat a(11, 13, CV_32F), b(11, 13, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = expr1("({0} + {1}) * 2", { a, b });
|
||||
Mat exp; cv::add(a, b, exp); exp *= 2.0;
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Type-cast function: float -> uint8 (saturating).
|
||||
TEST(Core_TExpr, cast_uint8)
|
||||
{
|
||||
Mat a(23, 17, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, -50.f, 300.f);
|
||||
|
||||
Mat got = expr1("uint8({0})", { a });
|
||||
Mat exp; a.convertTo(exp, CV_8U);
|
||||
ASSERT_EQ(got.type(), exp.type());
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
}
|
||||
|
||||
// pow() function call with a scalar exponent.
|
||||
TEST(Core_TExpr, pow_call)
|
||||
{
|
||||
Mat a(16, 16, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 5.f);
|
||||
|
||||
Mat got = expr1("pow({0}, 2)", { a });
|
||||
Mat exp; cv::pow(a, 2.0, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// pow over the interesting exponents: the special-cased 2/3/0.5/1/0, the general exp/log path
|
||||
// (2.5, -1.5), and negative bases (integer exponent -> exact signed result, fractional -> NaN),
|
||||
// against the double std::pow reference. Sizes chosen to exercise both the SIMD body and the tail.
|
||||
TEST(Core_TExpr, pow_exponents)
|
||||
{
|
||||
for (int depth : { CV_32F, CV_64F })
|
||||
{
|
||||
const double eps = depth == CV_32F ? 1e-6 : 1e-9;
|
||||
Mat a0(37, 41, CV_64F), a;
|
||||
theRNG().fill(a0, RNG::UNIFORM, 0.05, 9.);
|
||||
a0.convertTo(a, depth);
|
||||
for (double p : { 2., 3., 0.5, 1., 0., 2.5, -1.5, -0.5, 5., -2., 11. })
|
||||
{
|
||||
Mat got = expr1(cv::format("pow({0}, %.10g)", p), { a });
|
||||
ASSERT_EQ(got.depth(), depth) << "p=" << p;
|
||||
Mat ad, gd;
|
||||
a.convertTo(ad, CV_64F); got.convertTo(gd, CV_64F);
|
||||
double maxerr = 0;
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < a.cols; x++)
|
||||
{
|
||||
double r = std::pow(ad.at<double>(y, x), p);
|
||||
maxerr = std::max(maxerr, std::abs(gd.at<double>(y, x) - r) / std::max(1.0, std::abs(r)));
|
||||
}
|
||||
EXPECT_LE(maxerr, eps) << "depth=" << depth << " p=" << p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// negative bases: integer exponents keep exact signed results (scalar patch path), a fractional
|
||||
// exponent yields NaN - both matching std::pow
|
||||
TEST(Core_TExpr, pow_negative_base)
|
||||
{
|
||||
Mat a(9, 13, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, -5.f, -1.f);
|
||||
|
||||
Mat got3 = expr1("pow({0}, 3)", { a });
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < a.cols; x++)
|
||||
ASSERT_NEAR(got3.at<float>(y, x), std::pow((double)a.at<float>(y, x), 3.), 1e-2);
|
||||
|
||||
Mat gotf = expr1("pow({0}, 2.5)", { a });
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < a.cols; x++)
|
||||
ASSERT_TRUE(cvIsNaN(gotf.at<float>(y, x))) << "pow(neg, frac) must be NaN";
|
||||
}
|
||||
|
||||
// unary minus and abs() - compositions over the binary family (no dedicated kernels)
|
||||
TEST(Core_TExpr, neg_abs)
|
||||
{
|
||||
for (int depth : { CV_32F, CV_16S })
|
||||
{
|
||||
Mat a(15, 19, depth);
|
||||
theRNG().fill(a, RNG::UNIFORM, -100, 100);
|
||||
|
||||
Mat gneg = expr1("-{0}", { a });
|
||||
Mat eneg; cv::subtract(Scalar(0), a, eneg);
|
||||
ASSERT_EQ(gneg.depth(), depth);
|
||||
EXPECT_EQ(0, cvtest::norm(gneg, eneg, NORM_INF)) << "neg depth=" << depth;
|
||||
|
||||
// abs == absdiff(a, 0) INCLUDING the texpr auto result type rule: signed input -> the
|
||||
// UNSIGNED type of the same width (|SHRT_MIN| fits u16 exactly, no saturation). NB this
|
||||
// deliberately differs from the public cv::absdiff, whose auto depth keeps the source
|
||||
// type for 4.x compatibility - the VALUES agree, the depth rule is the engine's own.
|
||||
Mat gabs = expr1("abs({0})", { a });
|
||||
ASSERT_EQ(gabs.depth(), cv::ew::absdiffResultDepth(depth)) << "texpr absdiff type rule";
|
||||
Mat eabs; cv::absdiff(a, Scalar(0), eabs);
|
||||
eabs.convertTo(eabs, gabs.depth());
|
||||
EXPECT_EQ(0, cvtest::norm(gabs, eabs, NORM_INF)) << "abs depth=" << depth;
|
||||
}
|
||||
}
|
||||
|
||||
// the abs(a - b) -> absdiff(a, b) peephole: on unsigned data the literal semantics (saturating
|
||||
// subtract) would give max(a-b, 0) - the rewrite must give the true |a - b| everywhere
|
||||
TEST(Core_TExpr, abs_sub_peephole)
|
||||
{
|
||||
for (int depth : { CV_8U, CV_16S, CV_32F })
|
||||
{
|
||||
Mat a(23, 17, depth), b(23, 17, depth);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0, 100);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0, 100);
|
||||
|
||||
Mat got = expr1("abs({0} - {1})", { a, b });
|
||||
ASSERT_EQ(got.depth(), cv::ew::absdiffResultDepth(depth)) << "depth=" << depth;
|
||||
Mat exp; cv::absdiff(a, b, exp);
|
||||
exp.convertTo(exp, got.depth()); // cv::absdiff auto KEEPS the source depth (4.x)
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "depth=" << depth;
|
||||
}
|
||||
}
|
||||
|
||||
// clamp: scalar bounds (the common shape), array bounds, and type preservation
|
||||
TEST(Core_TExpr, clamp)
|
||||
{
|
||||
for (int depth : { CV_8U, CV_16S, CV_32F, CV_64F })
|
||||
{
|
||||
Mat a(25, 31, depth);
|
||||
theRNG().fill(a, RNG::UNIFORM, -100, 355);
|
||||
|
||||
Mat got = expr1("clamp({0}, 10, 200)", { a });
|
||||
Mat emax, exp;
|
||||
cv::max(a, 10.0, emax); cv::min(emax, 200.0, exp);
|
||||
ASSERT_EQ(got.depth(), depth) << "clamp must keep the operand type";
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "depth=" << depth;
|
||||
}
|
||||
// array bounds
|
||||
Mat x(14, 22, CV_32F), lo(14, 22, CV_32F), hi(14, 22, CV_32F);
|
||||
theRNG().fill(x, RNG::UNIFORM, -10.f, 10.f);
|
||||
theRNG().fill(lo, RNG::UNIFORM, -5.f, 0.f);
|
||||
theRNG().fill(hi, RNG::UNIFORM, 0.f, 5.f);
|
||||
Mat got = expr1("clamp({0}, {1}, {2})", { x, lo, hi });
|
||||
Mat emax, exp;
|
||||
cv::max(x, lo, emax); cv::min(emax, hi, exp);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
}
|
||||
|
||||
// '**' operator: pow alias, binds tighter than '*', right-associative
|
||||
TEST(Core_TExpr, pow_operator)
|
||||
{
|
||||
Mat a(13, 18, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0.5f, 2.f);
|
||||
|
||||
Mat got = expr1("{0} ** 2", { a });
|
||||
Mat exp; cv::pow(a, 2.0, exp);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
|
||||
// precedence: 3 * a ** 2 == 3 * (a ** 2)
|
||||
Mat got2 = expr1("3 * {0} ** 2", { a });
|
||||
Mat exp2 = 3.0 * exp;
|
||||
EXPECT_LE(cvtest::norm(got2, exp2, NORM_INF), 1e-4);
|
||||
|
||||
// right associativity: a ** 2 ** 3 == a ** (2 ** 3) == a ** 8
|
||||
Mat got3 = expr1("{0} ** 2 ** 3", { a });
|
||||
Mat exp3; cv::pow(a, 8.0, exp3);
|
||||
EXPECT_LE(cvtest::norm(got3, exp3, NORM_INF), 1e-4);
|
||||
}
|
||||
|
||||
// '?:' conditional: select alias with the lowest precedence; right-associative chains
|
||||
TEST(Core_TExpr, ternary_operator)
|
||||
{
|
||||
Mat a(19, 23, CV_32F), b(19, 23, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0.f, 100.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0.f, 100.f);
|
||||
|
||||
// max via ?: - the condition is a full comparison (lower precedence than '>')
|
||||
Mat got = expr1("{0} > {1} ? {0} : {1}", { a, b });
|
||||
Mat exp; cv::max(a, b, exp);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
|
||||
// arithmetic in every position without parentheses
|
||||
Mat got2 = expr1("{0} - {1} > 10 ? {0} + 1 : {1} * 2", { a, b });
|
||||
Mat mask = (a - b > 10), e1 = a + 1, e2 = b * 2, exp2 = e2.clone();
|
||||
e1.copyTo(exp2, mask);
|
||||
EXPECT_LE(cvtest::norm(got2, exp2, NORM_INF), 1e-4);
|
||||
|
||||
// right-associative chain: c1 ? x : c2 ? y : z
|
||||
Mat got3 = expr1("{0} > 66 ? 1 : {0} > 33 ? 2 : 3", { a });
|
||||
Mat exp3(a.size(), CV_32F);
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < a.cols; x++)
|
||||
{
|
||||
float v = a.at<float>(y, x);
|
||||
exp3.at<float>(y, x) = v > 66 ? 1.f : v > 33 ? 2.f : 3.f;
|
||||
}
|
||||
EXPECT_EQ(0, cvtest::norm(got3, exp3, NORM_INF));
|
||||
}
|
||||
|
||||
// hypot(x, y) / its cv-flavored alias mag(x, y): naive sqrt(x^2 + y^2), matching cv::magnitude;
|
||||
// kernels exist for the float depths only (f16/bf16/f32/f64)
|
||||
TEST(Core_TExpr, hypot_mag)
|
||||
{
|
||||
for (int depth : { CV_32F, CV_64F })
|
||||
{
|
||||
Mat x(23, 31, depth), y(23, 31, depth);
|
||||
theRNG().fill(x, RNG::UNIFORM, -100, 100);
|
||||
theRNG().fill(y, RNG::UNIFORM, -100, 100);
|
||||
|
||||
Mat got = expr1("hypot({0}, {1})", { x, y });
|
||||
ASSERT_EQ(got.depth(), depth);
|
||||
Mat exp; cv::magnitude(x, y, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), depth == CV_32F ? 1e-4 : 1e-9);
|
||||
|
||||
Mat got2 = expr1("mag({0}, {1})", { x, y }); // alias
|
||||
EXPECT_EQ(0, cvtest::norm(got2, got, NORM_INF));
|
||||
}
|
||||
// broadcast branch: hypot(array, scalar)
|
||||
Mat x(11, 17, CV_32F);
|
||||
theRNG().fill(x, RNG::UNIFORM, -10.f, 10.f);
|
||||
Mat got = expr1("hypot({0}, 3)", { x });
|
||||
for (int r = 0; r < x.rows; r++)
|
||||
for (int c = 0; c < x.cols; c++)
|
||||
{
|
||||
float v = x.at<float>(r, c);
|
||||
ASSERT_NEAR(got.at<float>(r, c), std::sqrt(v*v + 9.f), 1e-4) << r << "," << c;
|
||||
}
|
||||
// f16: T -> T through the native kernel (f32 hub inside)
|
||||
Mat xh, yh, y16(11, 17, CV_32F);
|
||||
x.convertTo(xh, CV_16F);
|
||||
theRNG().fill(y16, RNG::UNIFORM, -10.f, 10.f);
|
||||
y16.convertTo(yh, CV_16F);
|
||||
Mat goth = expr1("hypot({0}, {1})", { xh, yh });
|
||||
ASSERT_EQ(goth.depth(), CV_16F);
|
||||
}
|
||||
|
||||
// atan2(y, x): radians, the standard C range (-pi, pi], all four quadrants; the f32 kernel is the
|
||||
// fastAtan2 minimax polynomial (~1e-5 rad absolute), f64 is exact std::atan2
|
||||
TEST(Core_TExpr, atan2)
|
||||
{
|
||||
Mat y(23, 31, CV_32F), x(23, 31, CV_32F);
|
||||
theRNG().fill(y, RNG::UNIFORM, -10.f, 10.f); // both signs -> all quadrants
|
||||
theRNG().fill(x, RNG::UNIFORM, -10.f, 10.f);
|
||||
|
||||
Mat got = expr1("atan2({0}, {1})", { y, x });
|
||||
ASSERT_EQ(got.depth(), CV_32F);
|
||||
double maxerr = 0;
|
||||
for (int r = 0; r < y.rows; r++)
|
||||
for (int c = 0; c < y.cols; c++)
|
||||
{
|
||||
double ref = std::atan2((double)y.at<float>(r, c), (double)x.at<float>(r, c));
|
||||
maxerr = std::max(maxerr, std::abs((double)got.at<float>(r, c) - ref));
|
||||
}
|
||||
EXPECT_LE(maxerr, 2e-4) << "fastAtan2-class polynomial accuracy (measured ~1.6e-4 rad)";
|
||||
|
||||
// f64: exact std::atan2 per element
|
||||
Mat y64, x64;
|
||||
y.convertTo(y64, CV_64F); x.convertTo(x64, CV_64F);
|
||||
Mat got64 = expr1("atan2({0}, {1})", { y64, x64 });
|
||||
ASSERT_EQ(got64.depth(), CV_64F);
|
||||
for (int r = 0; r < y.rows; r++)
|
||||
for (int c = 0; c < y.cols; c++)
|
||||
ASSERT_EQ(got64.at<double>(r, c),
|
||||
std::atan2(y64.at<double>(r, c), x64.at<double>(r, c)));
|
||||
|
||||
// axis cases: atan2(0, 1) = 0, atan2(1, 0) = pi/2, atan2(0, -1) = pi, atan2(-1, 0) = -pi/2
|
||||
Mat ya = (Mat_<float>(1, 4) << 0.f, 1.f, 0.f, -1.f);
|
||||
Mat xa = (Mat_<float>(1, 4) << 1.f, 0.f, -1.f, 0.f);
|
||||
Mat ga = expr1("atan2({0}, {1})", { ya, xa });
|
||||
const float expctd[] = { 0.f, (float)(CV_PI/2), (float)CV_PI, (float)(-CV_PI/2) };
|
||||
for (int i = 0; i < 4; i++)
|
||||
ASSERT_NEAR(ga.at<float>(0, i), expctd[i], 1e-4) << "axis case " << i;
|
||||
}
|
||||
|
||||
// per-element (array) exponent
|
||||
TEST(Core_TExpr, pow_array_exponent)
|
||||
{
|
||||
Mat a(21, 27, CV_32F), b(21, 27, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0.1f, 5.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, -2.f, 3.f);
|
||||
|
||||
Mat got = expr1("pow({0}, {1})", { a, b });
|
||||
double maxerr = 0;
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < a.cols; x++)
|
||||
{
|
||||
double r = std::pow((double)a.at<float>(y, x), (double)b.at<float>(y, x));
|
||||
maxerr = std::max(maxerr, std::abs(got.at<float>(y, x) - r) / std::max(1.0, std::abs(r)));
|
||||
}
|
||||
EXPECT_LE(maxerr, 1e-6);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------- unary math
|
||||
// Golden result = the double-precision std:: function applied per element (never the op under
|
||||
// test); the tolerance is relative, scaled by the output depth's precision.
|
||||
|
||||
typedef double (*mathRef)(double);
|
||||
struct MathOpRef { const char* name; mathRef ref; };
|
||||
static const MathOpRef mathOps[] = {
|
||||
{ "sqrt", std::sqrt }, { "exp", std::exp }, { "log", std::log },
|
||||
{ "sin", std::sin }, { "cos", std::cos }, { "tanh", std::tanh }, { "erf", std::erf },
|
||||
{ "relu", [](double x) { return x > 0 ? x : 0.; } },
|
||||
};
|
||||
|
||||
// max |got - ref| / max(1, |ref|) over the array, both evaluated in f64
|
||||
static double relErr(const Mat& got, const Mat& in, mathRef ref)
|
||||
{
|
||||
Mat gotd, ind;
|
||||
got.convertTo(gotd, CV_64F);
|
||||
in.convertTo(ind, CV_64F);
|
||||
double maxerr = 0;
|
||||
for (int y = 0; y < ind.rows; y++)
|
||||
for (int x = 0; x < ind.cols; x++)
|
||||
{
|
||||
double r = ref(ind.at<double>(y, x));
|
||||
double e = std::abs(gotd.at<double>(y, x) - r) / std::max(1.0, std::abs(r));
|
||||
maxerr = std::max(maxerr, e);
|
||||
}
|
||||
return maxerr;
|
||||
}
|
||||
|
||||
typedef testing::TestWithParam<MatDepth> Core_TExpr_Math;
|
||||
|
||||
TEST_P(Core_TExpr_Math, unary_accuracy)
|
||||
{
|
||||
const int depth = GetParam();
|
||||
// eps: half-precision types are exact to ~2^-8/2^-11 per element; the f32 kernels are
|
||||
// polynomial approximations (a few ulp); f64 sqrt/exp/log are also vectorized polynomials
|
||||
const double eps = depth == CV_16F ? 2e-3 : depth == CV_16BF ? 1.6e-2
|
||||
: depth == CV_32F ? 1e-6 : 1e-9;
|
||||
Mat a0(37, 41, CV_32F);
|
||||
theRNG().fill(a0, RNG::UNIFORM, 0.05f, 9.f); // positive: one range serves log/sqrt too
|
||||
Mat a;
|
||||
a0.convertTo(a, depth);
|
||||
|
||||
for (const MathOpRef& m : mathOps)
|
||||
{
|
||||
Mat got = expr1(cv::format("%s({0})", m.name), { a });
|
||||
ASSERT_EQ(got.depth(), depth) << m.name << ": math must be T -> T on float inputs";
|
||||
EXPECT_LE(relErr(got, a, m.ref), eps) << m.name << " depth=" << depth;
|
||||
}
|
||||
|
||||
// negative inputs for the ops defined there (skip log/sqrt)
|
||||
Mat b0(37, 41, CV_32F), b;
|
||||
theRNG().fill(b0, RNG::UNIFORM, -8.f, 8.f);
|
||||
b0.convertTo(b, depth);
|
||||
for (const char* name : { "exp", "sin", "cos", "tanh", "erf", "relu" })
|
||||
{
|
||||
const MathOpRef* m = nullptr;
|
||||
for (const MathOpRef& c : mathOps) if (!strcmp(c.name, name)) m = &c;
|
||||
Mat got = expr1(cv::format("%s({0})", name), { b });
|
||||
EXPECT_LE(relErr(got, b, m->ref), eps) << name << "(neg) depth=" << depth;
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(/**/, Core_TExpr_Math,
|
||||
testing::Values(CV_16F, CV_16BF, CV_32F, CV_64F));
|
||||
|
||||
// integer input computes in the float domain and lands in f32
|
||||
TEST(Core_TExpr, math_int_input)
|
||||
{
|
||||
Mat a(19, 23, CV_8U);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1, 100);
|
||||
Mat got = expr1("sqrt({0})", { a });
|
||||
ASSERT_EQ(got.depth(), CV_32F);
|
||||
EXPECT_LE(relErr(got, a, std::sqrt), 1e-6);
|
||||
}
|
||||
|
||||
// in-place unary math (dst aliases src): the tail backoff must not re-apply the op
|
||||
TEST(Core_TExpr, math_inplace)
|
||||
{
|
||||
Mat a(21, 31, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0.1f, 9.f);
|
||||
Mat ref = expr1("sqrt({0})", { a });
|
||||
std::vector<Mat> out{ a }; // preallocated == input => in-place
|
||||
cv::texpr("sqrt({0})", std::vector<Mat>{ a }, out);
|
||||
EXPECT_EQ(0, cvtest::norm(out[0], ref, NORM_INF));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------- select
|
||||
TEST(Core_TExpr, select_basic)
|
||||
{
|
||||
for (int type : { CV_8UC1, CV_16SC1, CV_32FC1, CV_64FC1 })
|
||||
{
|
||||
Mat a(25, 33, type), b(25, 33, type);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0, 100);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0, 100);
|
||||
|
||||
Mat got = expr1("select({0} > {1}, {0}, {1})", { a, b }); // == max(a, b)
|
||||
Mat exp; cv::max(a, b, exp);
|
||||
ASSERT_EQ(got.type(), exp.type()) << "type=" << type;
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "type=" << type;
|
||||
}
|
||||
}
|
||||
|
||||
// one branch is a scalar constant (broadcast stepx == 0 inside the kernel)
|
||||
TEST(Core_TExpr, select_const_branch)
|
||||
{
|
||||
Mat a(17, 29, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, -10.f, 10.f);
|
||||
Mat got = expr1("select({0} > 0, {0}, 0)", { a }); // == relu
|
||||
Mat exp = expr1("relu({0})", { a });
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
}
|
||||
|
||||
// non-1-byte mask is normalized via `mask != 0`, not a value cast
|
||||
TEST(Core_TExpr, select_float_mask)
|
||||
{
|
||||
Mat m(15, 27, CV_32F), a(15, 27, CV_32F), b(15, 27, CV_32F);
|
||||
theRNG().fill(m, RNG::UNIFORM, -1.f, 1.f);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0.f, 10.f);
|
||||
m.at<float>(3, 5) = 0.f; // exact zero -> must take branch b
|
||||
m.at<float>(7, 7) = 0.25f; // would round/saturate to 0 under a value cast
|
||||
|
||||
Mat got = expr1("select({0}, {1}, {2})", { m, a, b });
|
||||
Mat mask = (m != 0), exp;
|
||||
exp = b.clone(); a.copyTo(exp, mask);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF));
|
||||
}
|
||||
|
||||
}} // namespace
|
||||
@@ -0,0 +1,364 @@
|
||||
// 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.
|
||||
|
||||
// White-box tests for the element-wise engine internals (cv::ew, declared in the module-internal
|
||||
// src/arithm_expr.hpp - NOT part of the public API; the public surface is cv::add/... and cv::texpr,
|
||||
// covered by test_new_arithm_extensive.cpp / test_arithm_expr.cpp). Two groups:
|
||||
// - the type-inference + cast-insertion policy (emitBinary) compiled and run through the executor;
|
||||
// - the single-op vertical slice (makeBinaryArithProgram / maybeAddCast) end-to-end.
|
||||
// Both check against the classic cv:: ops.
|
||||
|
||||
#include "test_precomp.hpp"
|
||||
#include "../src/arithm_expr.hpp"
|
||||
|
||||
namespace opencv_test { namespace {
|
||||
|
||||
using namespace cv::ew;
|
||||
|
||||
// emit shortcuts: a flexible literal, and a binary op over two slots.
|
||||
static int K(TExpr& e, double v) { return e.addConst(EW_DEPTH_NONE, Scalar(v), 1); }
|
||||
static int bin(TExpr& e, TOp op, int a, int b){ return e.emitBinary(op, a, b); }
|
||||
|
||||
// Compile `e` and run it over the given inputs. The operands were already typed at build time
|
||||
// (addInput carries each input's depth), so compile() just binds kernels + packs temp buffers.
|
||||
static std::vector<Mat> run(TExpr& e, const std::vector<Mat>& inps)
|
||||
{
|
||||
e.compile();
|
||||
std::vector<Mat> outs(e.noutputs);
|
||||
e.exec(inps.data(), outs.data());
|
||||
return outs;
|
||||
}
|
||||
|
||||
// addWeighted(a,alpha,b,beta,gamma) = a*alpha + b*beta + gamma, built op-by-op via emitBinary
|
||||
// (the temp buffers are allocated automatically by compile()'s liveness pass).
|
||||
TEST(Core_EW_Compile, addweighted_f32)
|
||||
{
|
||||
const int chans[] = { 1, 3 };
|
||||
double alpha = 2.5, beta = -1.5, gamma = 7.0;
|
||||
for (int ci = 0; ci < 2; ci++)
|
||||
{
|
||||
int H = 19, W = 23, cn = chans[ci];
|
||||
Mat a(H, W, CV_32FC(cn)), b(H, W, CV_32FC(cn));
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
TExpr g;
|
||||
int ia = g.addInput(CV_32F), ib = g.addInput(CV_32F);
|
||||
int t0 = bin(g, OP_MUL, ia, K(g, alpha));
|
||||
int t1 = bin(g, OP_MUL, ib, K(g, beta));
|
||||
int t2 = bin(g, OP_ADD, t0, t1);
|
||||
g.output(bin(g, OP_ADD, t2, K(g, gamma)));
|
||||
|
||||
std::vector<Mat> out = run(g, { a, b });
|
||||
|
||||
Mat exp; cv::addWeighted(a, alpha, b, beta, gamma, exp);
|
||||
ASSERT_EQ(out[0].type(), exp.type());
|
||||
EXPECT_LE(cvtest::norm(out[0], exp, NORM_INF), 1e-3) << "cn=" << cn;
|
||||
}
|
||||
}
|
||||
|
||||
// Mixed integer types: out = saturate_u8( saturate_u8(a*2.5) + b ), a,b are u8.
|
||||
// emitBinary must insert u8->f32 input casts and f32->u8 result casts around each op (2.5 does not
|
||||
// fit u8, so the direct u8 kernel is refused and the float working path is taken).
|
||||
TEST(Core_EW_Compile, mixed_u8_inserts_casts)
|
||||
{
|
||||
int H = 16, W = 24;
|
||||
Mat a(H, W, CV_8U), b(H, W, CV_8U);
|
||||
theRNG().fill(a, RNG::UNIFORM, 0, 60);
|
||||
theRNG().fill(b, RNG::UNIFORM, 0, 60);
|
||||
|
||||
TExpr g;
|
||||
int ia = g.addInput(CV_8U), ib = g.addInput(CV_8U);
|
||||
int mul = bin(g, OP_MUL, ia, K(g, 2.5)); // -> u8 (natural)
|
||||
g.output(bin(g, OP_ADD, mul, ib)); // -> u8
|
||||
|
||||
std::vector<Mat> out = run(g, { a, b });
|
||||
|
||||
Mat t0, exp;
|
||||
a.convertTo(t0, CV_8U, 2.5); // saturate_u8(a*2.5)
|
||||
cv::add(t0, b, exp); // saturate_u8(t0 + b)
|
||||
ASSERT_EQ(out[0].type(), exp.type());
|
||||
EXPECT_EQ(0, cvtest::norm(out[0], exp, NORM_INF));
|
||||
}
|
||||
|
||||
// Tuple of two outputs from shared inputs: (a+b, a-b).
|
||||
TEST(Core_EW_Compile, multi_output_tuple)
|
||||
{
|
||||
int H = 14, W = 18;
|
||||
Mat a(H, W, CV_32F), b(H, W, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
TExpr g;
|
||||
int ia = g.addInput(CV_32F), ib = g.addInput(CV_32F);
|
||||
g.output(bin(g, OP_ADD, ia, ib));
|
||||
g.output(bin(g, OP_SUB, ia, ib));
|
||||
|
||||
std::vector<Mat> out = run(g, { a, b });
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
|
||||
Mat eadd, esub; cv::add(a, b, eadd); cv::subtract(a, b, esub);
|
||||
EXPECT_LE(cvtest::norm(out[0], eadd, NORM_INF), 1e-3) << "sum";
|
||||
EXPECT_LE(cvtest::norm(out[1], esub, NORM_INF), 1e-3) << "diff";
|
||||
}
|
||||
|
||||
// Liveness: a linear chain of temps with disjoint lifetimes must share physical buffers.
|
||||
// out = (((a+1)+1)+1)+1 == a+4 : the last add is redirected straight into the output slot, the
|
||||
// three live temps share just 2 physical buffers.
|
||||
TEST(Core_EW_Compile, temp_buffer_reuse)
|
||||
{
|
||||
int H = 10, W = 13;
|
||||
Mat a(H, W, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
TExpr g;
|
||||
int x = g.addInput(CV_32F);
|
||||
for (int k = 0; k < 4; k++)
|
||||
x = bin(g, OP_ADD, x, K(g, 1.0));
|
||||
g.output(x);
|
||||
|
||||
std::vector<Mat> out = run(g, { a });
|
||||
|
||||
// last instruction writes straight into the OUTPUT slot (its producing temp was redirected)
|
||||
EXPECT_EQ(g.arginfo[g.prog[g.prog.size() - 1].result].kind, TExpr::OUTPUT);
|
||||
EXPECT_EQ(g.nbuffers, 2); // disjoint lifetimes => only 2 physical buffers
|
||||
|
||||
Mat exp; cv::add(a, Scalar(4.0), exp);
|
||||
EXPECT_LE(cvtest::norm(out[0], exp, NORM_INF), 1e-4);
|
||||
}
|
||||
|
||||
// promoteArith is the auto result-depth rule (rdepth == -1). Checked against an INDEPENDENT hardcoded
|
||||
// table (NOT computed from the engine): the extensive tests feed promoteArith to BOTH the engine and
|
||||
// their own reference, so a wrong-but-consistent rule slips through there - this catches it. Also
|
||||
// asserts commutativity, which a max-rank scheme silently breaks for mixed-sign / same-width floats.
|
||||
TEST(Core_EW_Compile, promoteArith_rules)
|
||||
{
|
||||
struct { int a, b, want; } cases[] = {
|
||||
// same signedness -> the wider one, sign kept
|
||||
{ CV_8U, CV_8U, CV_8U }, { CV_8U, CV_16U, CV_16U }, { CV_8U, CV_64U, CV_64U },
|
||||
{ CV_16S, CV_64S, CV_64S }, { CV_8S, CV_32S, CV_32S },
|
||||
// mixed sign, same width -> next-wider signed (64-bit has no wider int -> f64)
|
||||
{ CV_8U, CV_8S, CV_16S }, { CV_16U, CV_16S, CV_32S },
|
||||
{ CV_32U, CV_32S, CV_64S }, { CV_64U, CV_64S, CV_64F },
|
||||
// mixed sign, different width
|
||||
{ CV_8S, CV_16U, CV_32S }, { CV_8U, CV_16S, CV_16S }, { CV_32S, CV_64U, CV_64F },
|
||||
// float + int -> smallest covering float
|
||||
{ CV_16F, CV_8U, CV_16F }, { CV_16BF, CV_8U, CV_16BF }, { CV_16F, CV_16U, CV_32F },
|
||||
{ CV_16F, CV_32S, CV_64F }, { CV_32F, CV_32S, CV_64F }, { CV_32F, CV_16S, CV_32F },
|
||||
// float + float
|
||||
{ CV_16F, CV_32F, CV_32F }, { CV_16F, CV_16BF, CV_32F }, { CV_64F, CV_8U, CV_64F },
|
||||
// flexible operand (EW_DEPTH_NONE) does not force promotion
|
||||
{ EW_DEPTH_NONE, CV_16U, CV_16U }, { CV_16U, EW_DEPTH_NONE, CV_16U },
|
||||
{ EW_DEPTH_NONE, EW_DEPTH_NONE, EW_DEPTH_NONE },
|
||||
};
|
||||
for (auto& c : cases)
|
||||
{
|
||||
int got = promoteArith(c.a, c.b);
|
||||
EXPECT_EQ(got, c.want) << "promoteArith(" << c.a << "," << c.b << ")";
|
||||
EXPECT_EQ(promoteArith(c.b, c.a), got) << "not commutative at " << c.a << "," << c.b;
|
||||
}
|
||||
|
||||
// exhaustive commutativity over all real depths
|
||||
const int depths[] = { CV_8U, CV_8S, CV_16U, CV_16S, CV_32U, CV_32S, CV_64U, CV_64S,
|
||||
CV_16F, CV_16BF, CV_32F, CV_64F };
|
||||
for (int a : depths) for (int b : depths)
|
||||
EXPECT_EQ(promoteArith(a, b), promoteArith(b, a)) << "noncommutative at " << a << "," << b;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vertical slice: single-op programs (ADD/SUB/MUL/DIV/POW f32 and CAST) built via the hand builders
|
||||
// (makeBinaryArithProgram / maybeAddCast) and run through the executor, checked against classic cv::.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// out = op(a, b), composed via the general binary-arith builder (the engine backing cv::add).
|
||||
static Mat runBinary(TOp op, const Mat& a, const Mat& b, int rdepth)
|
||||
{
|
||||
TExpr p; makeBinaryArithProgram(p, op, a.depth(), b.depth(), rdepth);
|
||||
Mat inps[] = {a, b}, out;
|
||||
p.exec(inps, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// out = cast(a), built through maybeAddCast (a single OP_CAST) and compiled.
|
||||
static Mat runCast(const Mat& a, int rdepth)
|
||||
{
|
||||
TExpr e;
|
||||
int s = e.addInput(a.depth());
|
||||
e.output(e.maybeAddCast(s, rdepth));
|
||||
e.compile();
|
||||
Mat out;
|
||||
e.exec(&a, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void cvRef(TOp op, const Mat& a, const Mat& b, Mat& dst)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case OP_ADD: cv::add(a, b, dst); break;
|
||||
case OP_SUB: cv::subtract(a, b, dst); break;
|
||||
case OP_MUL: cv::multiply(a, b, dst); break;
|
||||
case OP_DIV: cv::divide(a, b, dst); break;
|
||||
default: CV_Error(Error::StsBadArg, "unexpected op");
|
||||
}
|
||||
}
|
||||
|
||||
// ADD/SUB/MUL/DIV on f32, single- and multi-channel, same shape.
|
||||
TEST(Core_EW_Slice, binary_f32_same_shape)
|
||||
{
|
||||
const TOp ops[] = { OP_ADD, OP_SUB, OP_MUL, OP_DIV };
|
||||
const int chans[] = { 1, 3, 4 };
|
||||
RNG& rng = theRNG();
|
||||
for (int oi = 0; oi < 4; oi++)
|
||||
for (int ci = 0; ci < 3; ci++)
|
||||
{
|
||||
int H = 17, W = 33, cn = chans[ci];
|
||||
Mat a(H, W, CV_32FC(cn)), b(H, W, CV_32FC(cn));
|
||||
rng.fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
rng.fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = runBinary(ops[oi], a, b, CV_32F);
|
||||
Mat exp; cvRef(ops[oi], a, b, exp);
|
||||
|
||||
ASSERT_EQ(got.size(), exp.size());
|
||||
ASSERT_EQ(got.type(), exp.type());
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3)
|
||||
<< "op=" << opName(ops[oi]) << " cn=" << cn;
|
||||
}
|
||||
}
|
||||
|
||||
// POW with a 1x1 (broadcast) exponent vs cv::pow(a, scalar).
|
||||
TEST(Core_EW_Slice, pow_f32_scalar_exp)
|
||||
{
|
||||
Mat a(20, 25, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 5.f);
|
||||
Mat e(1, 1, CV_32F, Scalar(2.0));
|
||||
|
||||
Mat got = runBinary(OP_POW, a, e, CV_32F);
|
||||
Mat exp; cv::pow(a, 2.0, exp);
|
||||
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Row / column broadcasting (single channel) checked against repeat()+cv::add.
|
||||
TEST(Core_EW_Slice, broadcast_row_col)
|
||||
{
|
||||
int H = 12, W = 19;
|
||||
Mat a(H, W, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
{ // row vector broadcast over rows
|
||||
Mat brow(1, W, CV_32F);
|
||||
theRNG().fill(brow, RNG::UNIFORM, 1.f, 10.f);
|
||||
Mat got = runBinary(OP_ADD, a, brow, CV_32F);
|
||||
Mat bb, exp; cv::repeat(brow, H, 1, bb); cv::add(a, bb, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3) << "row";
|
||||
}
|
||||
{ // column vector broadcast over columns
|
||||
Mat bcol(H, 1, CV_32F);
|
||||
theRNG().fill(bcol, RNG::UNIFORM, 1.f, 10.f);
|
||||
Mat got = runBinary(OP_ADD, a, bcol, CV_32F);
|
||||
Mat bb, exp; cv::repeat(bcol, 1, W, bb); cv::add(a, bb, exp);
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3) << "col";
|
||||
}
|
||||
}
|
||||
|
||||
// Channel broadcasting: HxWx3 * HxWx1 -> HxWx3.
|
||||
TEST(Core_EW_Slice, broadcast_channel)
|
||||
{
|
||||
int H = 15, W = 21;
|
||||
Mat a(H, W, CV_32FC3), b(H, W, CV_32FC1);
|
||||
theRNG().fill(a, RNG::UNIFORM, 1.f, 10.f);
|
||||
theRNG().fill(b, RNG::UNIFORM, 1.f, 10.f);
|
||||
|
||||
Mat got = runBinary(OP_MUL, a, b, CV_32F);
|
||||
|
||||
std::vector<Mat> ach; cv::split(a, ach);
|
||||
for (size_t c = 0; c < ach.size(); c++) cv::multiply(ach[c], b, ach[c]);
|
||||
Mat exp; cv::merge(ach, exp);
|
||||
|
||||
ASSERT_EQ(got.type(), exp.type());
|
||||
EXPECT_LE(cvtest::norm(got, exp, NORM_INF), 1e-3);
|
||||
}
|
||||
|
||||
// Saturating cast f32 <-> {u8, s32}.
|
||||
TEST(Core_EW_Slice, cast_basic)
|
||||
{
|
||||
Mat a(23, 31, CV_32F);
|
||||
theRNG().fill(a, RNG::UNIFORM, -50.f, 300.f); // exercise saturation for u8
|
||||
|
||||
{
|
||||
Mat got = runCast(a, CV_8U);
|
||||
Mat exp; a.convertTo(exp, CV_8U);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "f32->u8";
|
||||
}
|
||||
{
|
||||
Mat got = runCast(a, CV_32S);
|
||||
Mat exp; a.convertTo(exp, CV_32S);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "f32->s32";
|
||||
}
|
||||
{
|
||||
Mat u; a.convertTo(u, CV_8U);
|
||||
Mat got = runCast(u, CV_32F);
|
||||
Mat exp; u.convertTo(exp, CV_32F);
|
||||
EXPECT_EQ(0, cvtest::norm(got, exp, NORM_INF)) << "u8->f32";
|
||||
}
|
||||
}
|
||||
|
||||
// cv::add / cv::subtract on 32-bit ints SATURATE (SIMD via the local v_add_sat/v_sub_sat, scalar
|
||||
// tail via the int64 work type) - uniform random data essentially never crosses the boundaries,
|
||||
// so directed cases are mandatory (see the v_sat_arith brief): both rails, the 0 - INT_MIN case,
|
||||
// mixed-sign non-overflow, and a random block checked against an exact int64 reference.
|
||||
TEST(Core_EW_AddSub, saturation_s32_u32)
|
||||
{
|
||||
const int W = 37; // odd width: SIMD body + scalar tail both covered
|
||||
{
|
||||
const int mx = INT_MAX, mn = INT_MIN;
|
||||
const int a[] = { mx, mn, mx, mn, 0, -1, mx, 12345 };
|
||||
const int b[] = { 1, -1, mx, mn, mn, mx, -1, -54321 };
|
||||
// exact int64 references
|
||||
Mat A(1, 8, CV_32S, (void*)a), B(1, 8, CV_32S, (void*)b), sum, dif;
|
||||
cv::add(A, B, sum);
|
||||
cv::subtract(A, B, dif);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
int64_t rs = (int64_t)a[i] + b[i], rd = (int64_t)a[i] - b[i];
|
||||
EXPECT_EQ(sum.at<int>(i), (int)std::min<int64_t>(std::max<int64_t>(rs, mn), mx)) << "add s32 case " << i;
|
||||
EXPECT_EQ(dif.at<int>(i), (int)std::min<int64_t>(std::max<int64_t>(rd, mn), mx)) << "sub s32 case " << i;
|
||||
}
|
||||
}
|
||||
{
|
||||
const unsigned mx = UINT_MAX;
|
||||
const unsigned a[] = { mx, 0, mx, 5, 0, 100 };
|
||||
const unsigned b[] = { 1, 1, mx, 5, 0, 7 };
|
||||
Mat A(1, 6, CV_32U, (void*)a), B(1, 6, CV_32U, (void*)b), sum, dif;
|
||||
cv::add(A, B, sum);
|
||||
cv::subtract(A, B, dif);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
uint64_t rs = (uint64_t)a[i] + b[i];
|
||||
int64_t rd = (int64_t)a[i] - b[i];
|
||||
EXPECT_EQ(sum.at<unsigned>(i), (unsigned)std::min<uint64_t>(rs, mx)) << "add u32 case " << i;
|
||||
EXPECT_EQ(dif.at<unsigned>(i), (unsigned)std::max<int64_t>(rd, 0)) << "sub u32 case " << i;
|
||||
}
|
||||
}
|
||||
// random block spanning the full range (so saturation DOES occur), vs the int64 reference
|
||||
{
|
||||
Mat a(15, W, CV_32S), b(15, W, CV_32S), sum, dif;
|
||||
theRNG().fill(a, RNG::UNIFORM, INT_MIN, INT_MAX);
|
||||
theRNG().fill(b, RNG::UNIFORM, INT_MIN, INT_MAX);
|
||||
cv::add(a, b, sum);
|
||||
cv::subtract(a, b, dif);
|
||||
for (int y = 0; y < a.rows; y++)
|
||||
for (int x = 0; x < W; x++)
|
||||
{
|
||||
int64_t rs = (int64_t)a.at<int>(y, x) + b.at<int>(y, x);
|
||||
int64_t rd = (int64_t)a.at<int>(y, x) - b.at<int>(y, x);
|
||||
ASSERT_EQ(sum.at<int>(y, x), (int)std::min<int64_t>(std::max<int64_t>(rs, INT_MIN), INT_MAX)) << y << "," << x;
|
||||
ASSERT_EQ(dif.at<int>(y, x), (int)std::min<int64_t>(std::max<int64_t>(rd, INT_MIN), INT_MAX)) << y << "," << x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace
|
||||
@@ -0,0 +1,551 @@
|
||||
// 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.
|
||||
|
||||
// Randomized property-based accuracy tests for the broadcasting element-wise ops, exercised through
|
||||
// the PUBLIC cv:: entry points (add/subtract/multiply/divide/min/max/absdiff/compare with a mask,
|
||||
// dtype, in-place aliasing and mixed input types). For each caseidx a deterministic splitmix64 seed
|
||||
// picks random depths and broadcast-compatible shapes (ndims<=4, total<=100000) and every axis of each
|
||||
// operand independently keeps its size or drops to 1, so full/row/col/channel broadcast are all hit.
|
||||
// The reference decomposes each op into per-channel cv::broadcast + convertTo + the same op on aligned
|
||||
// same-shape single-channel arrays. The module-internal header is included only for the promotion rule
|
||||
// (promoteArith / absdiffResultDepth) the reference needs to predict each op's auto result depth.
|
||||
|
||||
#include "test_precomp.hpp"
|
||||
#include "../src/arithm_expr.hpp"
|
||||
|
||||
namespace opencv_test { namespace {
|
||||
|
||||
using namespace cv::ew;
|
||||
|
||||
static inline uint64_t mix64(uint64_t x)
|
||||
{
|
||||
x += 0x9E3779B97F4A7C15ULL;
|
||||
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
||||
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
|
||||
return x ^ (x >> 31);
|
||||
}
|
||||
static const uint64_t kSuiteSalt = 0x9ADD0CA57ULL;
|
||||
static const int kNumCases = 1000;
|
||||
static const int kMaxElems = 100000;
|
||||
|
||||
// engine-supported depths
|
||||
static const int kDepths[] = { CV_8U, CV_8S, CV_16U, CV_16S, CV_32U, CV_32S,
|
||||
CV_64U, CV_64S, CV_16F, CV_16BF, CV_32F, CV_64F };
|
||||
static int sampleDepth(RNG& rng) { return kDepths[rng.uniform(0, (int)(sizeof(kDepths)/sizeof(kDepths[0])))]; }
|
||||
|
||||
static bool isFloat(int d) { return d==CV_16F || d==CV_16BF || d==CV_32F || d==CV_64F; }
|
||||
|
||||
// numpy-ish promotion (must mirror ew_exec.cpp's promote2 so the reference adds the same values)
|
||||
static int promote2(int a, int b)
|
||||
{
|
||||
if (a == b) return a;
|
||||
|
||||
constexpr unsigned lbits = 3, lmask = (1u << lbits) - 1u;
|
||||
const uint64_t typelut = (uint64_t)((0ULL << CV_8U*lbits) | (0ULL << CV_8S*lbits) |
|
||||
(1ULL << CV_16U*lbits) | (1ULL << CV_16S*lbits) |
|
||||
(2ULL << CV_32U*lbits) | (2ULL << CV_32S*lbits) |
|
||||
(3ULL << CV_16F*lbits) | (3ULL << CV_16BF*lbits) |
|
||||
(3ULL << CV_32F*lbits) | (4ULL << CV_64F*lbits) |
|
||||
(4ULL << CV_64S*lbits) | (4ULL << CV_64U*lbits));
|
||||
unsigned pr_a = unsigned((typelut >> (a*lbits)) & lmask);
|
||||
unsigned pr_b = unsigned((typelut >> (b*lbits)) & lmask);
|
||||
unsigned max_pr = std::max(pr_a, pr_b);
|
||||
constexpr unsigned dbits = CV_CN_SHIFT, dmask = (1u << dbits) - 1u;
|
||||
const unsigned ctypelut = ((CV_16S << 0*dbits) | (CV_32S << 1*dbits) | (CV_64S << 2*dbits) |
|
||||
(CV_32F << 3*dbits) | (CV_64F << 4*dbits));
|
||||
return int((ctypelut >> (max_pr*dbits)) & dmask);
|
||||
}
|
||||
|
||||
// data range per depth: 8/16-bit wide enough to exercise saturation; 32/64-bit kept modest.
|
||||
static void depthRange(int d, double& lo, double& hi)
|
||||
{
|
||||
switch (d)
|
||||
{
|
||||
case CV_8U: lo = 0; hi = 255; break;
|
||||
case CV_8S: lo = -128; hi = 127; break;
|
||||
case CV_16U: lo = 0; hi = 65535; break;
|
||||
case CV_16S: lo = -32768; hi = 32767; break;
|
||||
default: lo = -1000; hi = 1000; break; // 32/64-bit ints and floats: no overflow
|
||||
}
|
||||
}
|
||||
|
||||
static int sampleSize(RNG& rng, int lo, int hi)
|
||||
{
|
||||
if (hi <= lo) return lo;
|
||||
if (rng.uniform(0.0, 1.0) < 0.35)
|
||||
{
|
||||
static const int cand[] = {1,2,3,4,7,8,15,16,17,31,32,33};
|
||||
int picks[16], n = 0;
|
||||
for (int c : cand) if (c >= lo && c <= hi) picks[n++] = c;
|
||||
picks[n++] = lo; picks[n++] = hi;
|
||||
return picks[rng.uniform(0, n)];
|
||||
}
|
||||
double v = std::exp(rng.uniform(std::log((double)lo), std::log((double)hi)));
|
||||
return std::min(hi, std::max(lo, cvRound(v)));
|
||||
}
|
||||
|
||||
// a random shape (ndims in 1..4) with product <= kMaxElems
|
||||
static std::vector<int> sampleShape(RNG& rng)
|
||||
{
|
||||
int nd = rng.uniform(1, 5);
|
||||
std::vector<int> s(nd);
|
||||
long long prod = 1;
|
||||
for (int d = 0; d < nd; d++)
|
||||
{
|
||||
int hi = (int)std::min<long long>(512, std::max<long long>(1, kMaxElems / prod));
|
||||
s[d] = sampleSize(rng, 1, hi);
|
||||
prod *= s[d];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// build a random Mat of the given depth & shape, filled via a CV_64F master (randUni can't fill
|
||||
// 16f/16bf/32u/64u/64s directly), values in the per-depth range. ~1/3 of the time the result is
|
||||
// a NON-contiguous sub-array: the parent is padded by 1..2 on each edge of each axis and we
|
||||
// return the inner view (gapped outer steps), to exercise the engine's non-continuous path.
|
||||
static Mat makeRandom(RNG& rng, const std::vector<int>& shape, int cn, int depth,
|
||||
double rlo = 1, double rhi = 0)
|
||||
{
|
||||
const bool crop = rng.uniform(0, 3) == 0;
|
||||
const int nd = (int)shape.size();
|
||||
std::vector<int> pad(nd);
|
||||
std::vector<Range> ranges(nd);
|
||||
for (int d = 0; d < nd; d++)
|
||||
{
|
||||
int lo = crop ? rng.uniform(1, 3) : 0; // 1..2
|
||||
int hi = crop ? rng.uniform(1, 3) : 0;
|
||||
pad[d] = shape[d] + lo + hi;
|
||||
ranges[d] = Range(lo, lo + shape[d]);
|
||||
}
|
||||
Mat m64(nd, pad.data(), CV_MAKETYPE(CV_64F, cn));
|
||||
double lo = rlo, hi = rhi;
|
||||
if (rlo > rhi) depthRange(depth, lo, hi); // rlo>rhi (default) => per-depth range
|
||||
cvtest::randUni(rng, m64, Scalar::all(lo), Scalar::all(hi));
|
||||
Mat big; m64.convertTo(big, CV_MAKETYPE(depth, cn));
|
||||
return big(ranges); // full range when !crop => contiguous
|
||||
}
|
||||
|
||||
static std::string shapeStr(const std::vector<int>& s)
|
||||
{
|
||||
std::string r = "[";
|
||||
for (size_t i = 0; i < s.size(); i++) r += (i ? "x" : "") + std::to_string(s[i]);
|
||||
return r + "]";
|
||||
}
|
||||
|
||||
// per-output-depth tolerance. `floatPath` = a float was involved on the way to an integer
|
||||
// output, so a final float->int rounding tie may differ from cv:: by 1 (benign).
|
||||
static void checkClose(const Mat& got, const Mat& ref, int rdepth, bool floatPath, const char* what)
|
||||
{
|
||||
ASSERT_EQ(got.dims, ref.dims) << what;
|
||||
ASSERT_EQ(got.type(), ref.type()) << what;
|
||||
double n = cvtest::norm(got, ref, NORM_INF);
|
||||
if (!isFloat(rdepth))
|
||||
{
|
||||
EXPECT_LE(n, floatPath ? 1.0 : 0.0) << what; // integer output: exact, or ±1 via float
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = std::max(1.0, cvtest::norm(ref, NORM_INF));
|
||||
double rel = rdepth==CV_16BF ? 1e-2 : rdepth==CV_16F ? 2e-3 : 1e-5;
|
||||
EXPECT_LE(n, rel*scale) << what << " (n=" << n << " scale=" << scale << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------- add / sub
|
||||
// Parameterized on (op, caseidx): op 0 = ADD, 1 = SUB. The two ops share the same per-case data
|
||||
// (seed depends only on caseidx), so they run on identical inputs.
|
||||
class EW_Extensive_BinOp : public ::testing::TestWithParam<std::tuple<int,int>> {};
|
||||
|
||||
TEST_P(EW_Extensive_BinOp, accuracy)
|
||||
{
|
||||
const int opSel = std::get<0>(GetParam());
|
||||
const int caseidx = std::get<1>(GetParam());
|
||||
const TOp op = opSel ? OP_SUB : OP_ADD;
|
||||
const char* opStr = opSel ? "sub" : "add";
|
||||
RNG rng(mix64(kSuiteSalt ^ (uint64_t)caseidx));
|
||||
|
||||
std::vector<int> shape = sampleShape(rng);
|
||||
const int da = sampleDepth(rng), db = sampleDepth(rng);
|
||||
|
||||
// channels: pick a base count (biased toward 1); each operand keeps it or drops to 1, so we
|
||||
// get C1+C1 (fold), Cn+Cn (fold) and the Cn+C1 / C1+Cn channel-broadcast (CH_DIM) mix.
|
||||
static const int cncand[] = { 1, 1, 2, 3, 4 };
|
||||
const int rcn = cncand[rng.uniform(0, 5)];
|
||||
const int cn_a = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int cn_b = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int ocn = std::max(cn_a, cn_b);
|
||||
|
||||
// each operand independently keeps or broadcasts (->1) every axis
|
||||
std::vector<int> sa(shape.size()), sb(shape.size()), res(shape.size());
|
||||
for (size_t d = 0; d < shape.size(); d++)
|
||||
{
|
||||
sa[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
sb[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
res[d] = std::max(sa[d], sb[d]);
|
||||
}
|
||||
|
||||
// in-place: ~1/3 of cases attempt it. When an input is spatially & channel "full" (its shape ==
|
||||
// the output shape res, channels == ocn), alias the output onto it and force Tr to that input's
|
||||
// depth so the buffer is truly reused. Exercises the executor's in-place handling AND the
|
||||
// kernels' dst==src aliasing (the halide-tail backoff). Otherwise Tr is a free random depth.
|
||||
int aliasIn = -1;
|
||||
if (rng.uniform(0, 3) == 0)
|
||||
{
|
||||
if (sa == res && cn_a == ocn) aliasIn = 0;
|
||||
else if (sb == res && cn_b == ocn) aliasIn = 1;
|
||||
}
|
||||
const bool inplace = aliasIn >= 0;
|
||||
const int Tr = aliasIn == 0 ? da : aliasIn == 1 ? db : sampleDepth(rng);
|
||||
|
||||
SCOPED_TRACE(cv::format("%s caseidx=%d da=%s db=%s Tr=%s a=%sC%d b=%sC%d inplace=%d",
|
||||
opStr, caseidx, depthToString(da), depthToString(db), depthToString(Tr), shapeStr(sa).c_str(), cn_a,
|
||||
shapeStr(sb).c_str(), cn_b, inplace ? aliasIn : -1));
|
||||
|
||||
Mat a = makeRandom(rng, sa, cn_a, da), b = makeRandom(rng, sb, cn_b, db);
|
||||
|
||||
// reference FIRST (an in-place exec may overwrite an input): per output channel pick a's/b's
|
||||
// channel (C1->Cn broadcast), spatial-broadcast, cast to common type C, op(Tr), then merge.
|
||||
int C = (da == db) ? da : promote2(da, db);
|
||||
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
||||
std::vector<Mat> refch(ocn);
|
||||
for (int c = 0; c < ocn; c++)
|
||||
{
|
||||
Mat apC, bpC;
|
||||
cvtest::convert(ach[cn_a == 1 ? 0 : c], apC, C);
|
||||
cvtest::convert(bch[cn_b == 1 ? 0 : c], bpC, C);
|
||||
Mat aB, bB; cv::broadcast(apC, res, aB); cv::broadcast(bpC, res, bB);
|
||||
cvtest::add(aB, 1, bB, op == OP_SUB ? -1 : 1, Scalar(), refch[c], Tr);
|
||||
}
|
||||
Mat ref; cv::merge(refch, ref);
|
||||
|
||||
// public op: dst aliases input #aliasIn for the in-place case, else a fresh Mat.
|
||||
Mat outOwn;
|
||||
Mat& out = inplace ? (aliasIn == 0 ? a : b) : outOwn;
|
||||
if (op == OP_SUB) cv::subtract(a, b, out, noArray(), Tr);
|
||||
else cv::add (a, b, out, noArray(), Tr);
|
||||
|
||||
checkClose(out, ref, Tr, isFloat(da) || isFloat(db), opStr);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Core_EW, EW_Extensive_BinOp,
|
||||
testing::Combine(testing::Values(0, 1), testing::Range(0, kNumCases)),
|
||||
[](const testing::TestParamInfo<std::tuple<int,int>>& ti) {
|
||||
return cv::format("%s_case%04d", std::get<0>(ti.param) ? "sub" : "add",
|
||||
std::get<1>(ti.param));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------- min / max / absdiff
|
||||
// op 0 = MIN, 1 = MAX, 2 = ABSDIFF: operands promoted to a common type C; MIN/MAX result C, ABSDIFF
|
||||
// result Cr = absdiffResultDepth(C) (unsigned same width for signed ints, since |a-b| can hit 2^w-1).
|
||||
// cv::min/max/absdiff auto-promote mixed input types (no dtype arg), so this exercises the engine's
|
||||
// promotion + cast insertion for a fresh family of ops through the public entry points.
|
||||
class EW_Extensive_MinMax : public ::testing::TestWithParam<std::tuple<int,int>> {};
|
||||
|
||||
TEST_P(EW_Extensive_MinMax, accuracy)
|
||||
{
|
||||
const int opSel = std::get<0>(GetParam());
|
||||
const int caseidx = std::get<1>(GetParam());
|
||||
const TOp op = opSel == 0 ? OP_MIN : opSel == 1 ? OP_MAX : OP_ABSDIFF;
|
||||
const char* opStr = opSel == 0 ? "min" : opSel == 1 ? "max" : "absdiff";
|
||||
RNG rng(mix64(kSuiteSalt ^ (uint64_t)(caseidx * 3 + opSel))); // distinct stream per op
|
||||
|
||||
std::vector<int> shape = sampleShape(rng);
|
||||
const int da = sampleDepth(rng), db = sampleDepth(rng);
|
||||
const int C = promoteArith(da, db); // common compute/result type (auto), shared with the ref.
|
||||
const int Cr = C; // min/max/absdiff all resolve to C (absdiff saturates its wide |a-b| back to C)
|
||||
|
||||
static const int cncand[] = { 1, 1, 2, 3, 4 };
|
||||
const int rcn = cncand[rng.uniform(0, 5)];
|
||||
const int cn_a = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int cn_b = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int ocn = std::max(cn_a, cn_b);
|
||||
|
||||
std::vector<int> sa(shape.size()), sb(shape.size()), res(shape.size());
|
||||
for (size_t d = 0; d < shape.size(); d++)
|
||||
{
|
||||
sa[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
sb[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
res[d] = std::max(sa[d], sb[d]);
|
||||
}
|
||||
|
||||
SCOPED_TRACE(cv::format("%s caseidx=%d da=%d db=%d C=%d a=%sC%d b=%sC%d",
|
||||
opStr, caseidx, da, db, C, shapeStr(sa).c_str(), cn_a,
|
||||
shapeStr(sb).c_str(), cn_b));
|
||||
|
||||
Mat a = makeRandom(rng, sa, cn_a, da), b = makeRandom(rng, sb, cn_b, db);
|
||||
|
||||
// reference: per output channel pick a's/b's channel (C1->Cn), spatial-broadcast, cast to C, op.
|
||||
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
||||
std::vector<Mat> refch(ocn);
|
||||
for (int c = 0; c < ocn; c++)
|
||||
{
|
||||
Mat apC, bpC;
|
||||
cvtest::convert(ach[cn_a == 1 ? 0 : c], apC, C);
|
||||
cvtest::convert(bch[cn_b == 1 ? 0 : c], bpC, C);
|
||||
Mat aB, bB; cv::broadcast(apC, res, aB); cv::broadcast(bpC, res, bB);
|
||||
if (op == OP_MIN) cvtest::min(aB, bB, refch[c]);
|
||||
else if (op == OP_MAX) cvtest::max(aB, bB, refch[c]);
|
||||
else cvtest::add(aB, 1, bB, -1, Scalar(), refch[c], Cr, /*calcAbs=*/true);
|
||||
}
|
||||
Mat ref; cv::merge(refch, ref);
|
||||
|
||||
// public op: min/max/absdiff auto-promote mixed input types to C (= promoteArith(da,db)) and
|
||||
// broadcast, exactly like the reference; absdiff's result is the unsigned same-width Cr.
|
||||
Mat out;
|
||||
if (op == OP_MIN) cv::min(a, b, out);
|
||||
else if (op == OP_MAX) cv::max(a, b, out);
|
||||
else cv::absdiff(a, b, out);
|
||||
|
||||
checkClose(out, ref, Cr, isFloat(da) || isFloat(db), opStr);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Core_EW, EW_Extensive_MinMax,
|
||||
testing::Combine(testing::Values(0, 1, 2), testing::Range(0, kNumCases)),
|
||||
[](const testing::TestParamInfo<std::tuple<int,int>>& ti) {
|
||||
const int o = std::get<0>(ti.param);
|
||||
return cv::format("%s_case%04d", o == 0 ? "min" : o == 1 ? "max" : "absdiff",
|
||||
std::get<1>(ti.param));
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------------- compare
|
||||
// op 0 = CMP_EQ, 1 = CMP_GT: operands promoted to a common type C, result a u8 mask. Exercises
|
||||
// emitBinary's compare branch (result forced to u8) and the optional mask value (0/255 default, or
|
||||
// 0/1 set through TKernel::flags). Inputs are drawn from a small shared range so equality fires.
|
||||
class EW_Extensive_Compare : public ::testing::TestWithParam<std::tuple<int,int>> {};
|
||||
|
||||
TEST_P(EW_Extensive_Compare, accuracy)
|
||||
{
|
||||
const int opSel = std::get<0>(GetParam()); // 0 = EQ, 1 = GT
|
||||
const int caseidx = std::get<1>(GetParam());
|
||||
const int cmpop = opSel == 0 ? cv::CMP_EQ : cv::CMP_GT;
|
||||
const char* opStr = opSel == 0 ? "cmpEQ" : "cmpGT";
|
||||
RNG rng(mix64(kSuiteSalt ^ 0xC0FFEEULL ^ (uint64_t)(caseidx * 2 + opSel)));
|
||||
|
||||
std::vector<int> shape = sampleShape(rng);
|
||||
const int da = sampleDepth(rng), db = sampleDepth(rng);
|
||||
const int C = (da == db) ? da : promote2(da, db);
|
||||
|
||||
static const int cncand[] = { 1, 1, 2, 3, 4 };
|
||||
const int rcn = cncand[rng.uniform(0, 5)];
|
||||
const int cn_a = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int cn_b = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int ocn = std::max(cn_a, cn_b);
|
||||
|
||||
std::vector<int> sa(shape.size()), sb(shape.size()), res(shape.size());
|
||||
for (size_t d = 0; d < shape.size(); d++)
|
||||
{
|
||||
sa[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
sb[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
res[d] = std::max(sa[d], sb[d]);
|
||||
}
|
||||
|
||||
SCOPED_TRACE(cv::format("%s caseidx=%d da=%d db=%d C=%d a=%sC%d b=%sC%d",
|
||||
opStr, caseidx, da, db, C, shapeStr(sa).c_str(), cn_a,
|
||||
shapeStr(sb).c_str(), cn_b));
|
||||
|
||||
// small shared range [0,12] (well within every depth) so EQ is hit on a healthy fraction
|
||||
Mat a = makeRandom(rng, sa, cn_a, da, 0, 12), b = makeRandom(rng, sb, cn_b, db, 0, 12);
|
||||
|
||||
// reference: per channel, cast to C, compare in f64 (exact for these ranges) -> 0/255 mask
|
||||
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
||||
std::vector<Mat> refch(ocn);
|
||||
for (int c = 0; c < ocn; c++)
|
||||
{
|
||||
Mat apC, bpC;
|
||||
cvtest::convert(ach[cn_a == 1 ? 0 : c], apC, C);
|
||||
cvtest::convert(bch[cn_b == 1 ? 0 : c], bpC, C);
|
||||
Mat aB, bB; cv::broadcast(apC, res, aB); cv::broadcast(bpC, res, bB);
|
||||
Mat af, bf; cvtest::convert(aB, af, CV_64F); cvtest::convert(bB, bf, CV_64F);
|
||||
cvtest::compare(af, bf, refch[c], cmpop); // 0 / 255
|
||||
}
|
||||
Mat ref255; cv::merge(refch, ref255);
|
||||
|
||||
// public op: cv::compare auto-promotes mixed input types to the common type, broadcasts, and
|
||||
// yields a u8 0/255 mask per channel. (The engine's optional 0/1 mask is not exposed here.)
|
||||
Mat out; cv::compare(a, b, out, cmpop);
|
||||
ASSERT_EQ(out.type(), CV_8UC(ocn)) << opStr;
|
||||
EXPECT_EQ(0, cvtest::norm(out, ref255, NORM_INF)) << opStr;
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Core_EW, EW_Extensive_Compare,
|
||||
testing::Combine(testing::Values(0, 1), testing::Range(0, kNumCases)),
|
||||
[](const testing::TestParamInfo<std::tuple<int,int>>& ti) {
|
||||
return cv::format("%s_case%04d", std::get<0>(ti.param) == 0 ? "cmpEQ" : "cmpGT",
|
||||
std::get<1>(ti.param));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------- mul / div
|
||||
// Parameterized on (op, caseidx): op 0 = MUL, 1 = DIV. Both compute in the float work type (float
|
||||
// for <=16-bit, double for 32/64-bit), matching cv::multiply/divide; integer divide-by-zero => 0.
|
||||
class EW_Extensive_MulDiv : public ::testing::TestWithParam<std::tuple<int,int>> {};
|
||||
|
||||
TEST_P(EW_Extensive_MulDiv, accuracy)
|
||||
{
|
||||
const int opSel = std::get<0>(GetParam());
|
||||
const int caseidx = std::get<1>(GetParam());
|
||||
const TOp op = opSel ? OP_DIV : OP_MUL;
|
||||
const char* opStr = opSel ? "div" : "mul";
|
||||
RNG rng(mix64(kSuiteSalt ^ 0x3DD17ULL ^ (uint64_t)caseidx));
|
||||
|
||||
std::vector<int> shape = sampleShape(rng);
|
||||
const int da = sampleDepth(rng), db = sampleDepth(rng);
|
||||
|
||||
static const int cncand[] = { 1, 1, 2, 3, 4 };
|
||||
const int rcn = cncand[rng.uniform(0, 5)];
|
||||
const int cn_a = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int cn_b = rng.uniform(0, 2) ? rcn : 1;
|
||||
const int ocn = std::max(cn_a, cn_b);
|
||||
|
||||
std::vector<int> sa(shape.size()), sb(shape.size()), res(shape.size());
|
||||
for (size_t d = 0; d < shape.size(); d++)
|
||||
{
|
||||
sa[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
sb[d] = rng.uniform(0, 2) ? shape[d] : 1;
|
||||
res[d] = std::max(sa[d], sb[d]);
|
||||
}
|
||||
|
||||
int aliasIn = -1;
|
||||
if (rng.uniform(0, 3) == 0)
|
||||
{
|
||||
if (sa == res && cn_a == ocn) aliasIn = 0;
|
||||
else if (sb == res && cn_b == ocn) aliasIn = 1;
|
||||
}
|
||||
const bool inplace = aliasIn >= 0;
|
||||
const int Tr = aliasIn == 0 ? da : aliasIn == 1 ? db : sampleDepth(rng);
|
||||
|
||||
// half the cases use a non-unit scale (mul: a*b*scale, div: a*scale/b), like cv::multiply/
|
||||
// divide. Kept in [1/256, 2] so it can shrink (e.g. 1/255) or modestly amplify without pushing
|
||||
// a product/quotient past the integer-output range (which would be float->int UB on both sides).
|
||||
double scale = 1.0;
|
||||
if (rng.uniform(0, 2)) scale = rng.uniform(1.0/256, 2.0);
|
||||
|
||||
SCOPED_TRACE(cv::format("%s caseidx=%d da=%d db=%d Tr=%d a=%sC%d b=%sC%d inplace=%d scale=%.4f",
|
||||
opStr, caseidx, da, db, Tr, shapeStr(sa).c_str(), cn_a,
|
||||
shapeStr(sb).c_str(), cn_b, inplace ? aliasIn : -1, scale));
|
||||
|
||||
// modest magnitudes: mul/div compute in a float work type, so a product/quotient that overflows
|
||||
// the integer output's range hits float->int UB (cv::multiply is UB there too). [-1000,1000]
|
||||
// keeps products <= 1e6 (no overflow), while still exercising saturation for small outputs.
|
||||
Mat a = makeRandom(rng, sa, cn_a, da, -1000, 1000), b = makeRandom(rng, sb, cn_b, db, -1000, 1000);
|
||||
|
||||
const bool bothInt = !isFloat(da) && !isFloat(db);
|
||||
// Integer divide-by-zero is well-defined (=> 0) and IS exercised. Float-involved divide-by-zero
|
||||
// is UB (a/0 -> inf -> int), so avoid it here: make the divisor (b) nonzero for the float path.
|
||||
if (op == OP_DIV && !bothInt)
|
||||
{
|
||||
Mat b64; b.convertTo(b64, CV_64F);
|
||||
b64.setTo(1.0, b64 == 0.0);
|
||||
b64.convertTo(b, db);
|
||||
}
|
||||
|
||||
// reference FIRST (in-place may overwrite an input): mirror the engine's spec - cast both to the
|
||||
// float work type Wf (float for <=16-bit common type, double for 32/64-bit), op in Wf, then cast
|
||||
// to Tr (same final cast the engine uses). For both-integer div, guard divide-by-zero -> 0.
|
||||
const int C = (da == db) ? da : promote2(da, db);
|
||||
const bool wide = (C==CV_32U || C==CV_32S || C==CV_64U || C==CV_64S || C==CV_64F);
|
||||
const int Wf = wide ? CV_64F : CV_32F;
|
||||
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
||||
std::vector<Mat> refch(ocn);
|
||||
for (int c = 0; c < ocn; c++)
|
||||
{
|
||||
Mat aWf, bWf;
|
||||
cvtest::convert(ach[cn_a == 1 ? 0 : c], aWf, Wf);
|
||||
cvtest::convert(bch[cn_b == 1 ? 0 : c], bWf, Wf);
|
||||
Mat aB, bB; cv::broadcast(aWf, res, aB); cv::broadcast(bWf, res, bB);
|
||||
Mat q;
|
||||
if (op == OP_DIV) { cvtest::divide(aB, bB, q, scale); if (bothInt) q.setTo(0, bB == 0); }
|
||||
else cvtest::multiply(aB, bB, q, scale);
|
||||
cvtest::convert(q, refch[c], Tr);
|
||||
}
|
||||
Mat ref; cv::merge(refch, ref);
|
||||
|
||||
// public op: dst aliases input #aliasIn for the in-place case, else a fresh Mat.
|
||||
Mat outOwn;
|
||||
Mat& out = inplace ? (aliasIn == 0 ? a : b) : outOwn;
|
||||
if (op == OP_DIV) cv::divide (a, b, out, scale, Tr);
|
||||
else cv::multiply(a, b, out, scale, Tr);
|
||||
|
||||
checkClose(out, ref, Tr, true, opStr); // float work => integer output may differ by <=1
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Core_EW, EW_Extensive_MulDiv,
|
||||
testing::Combine(testing::Values(0, 1), testing::Range(0, kNumCases)),
|
||||
[](const testing::TestParamInfo<std::tuple<int,int>>& ti) {
|
||||
return cv::format("%s_case%04d", std::get<0>(ti.param) ? "div" : "mul",
|
||||
std::get<1>(ti.param));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------- masked add / sub
|
||||
// add/sub with a write-mask. The data inputs share the output shape (a, b, out all `shape`-spatial,
|
||||
// cn channels); the mask is single-channel, the output spatial shape, type bool/u8/s8. The output
|
||||
// PRE-EXISTS (filled with random content): copyMask overwrites only the masked subset and leaves
|
||||
// the rest unchanged (dst = mask ? op : dst). cn==1 exercises the per-element mask (CH_FOLD); cn>1
|
||||
// the channel-axis broadcast (CH_DIM, mask stepx 0 => a whole n-channel row copied under one test).
|
||||
class EW_Extensive_Mask : public ::testing::TestWithParam<std::tuple<int,int>> {};
|
||||
|
||||
TEST_P(EW_Extensive_Mask, accuracy)
|
||||
{
|
||||
const int opSel = std::get<0>(GetParam());
|
||||
const int caseidx = std::get<1>(GetParam());
|
||||
const TOp op = opSel ? OP_SUB : OP_ADD;
|
||||
const char* opStr = opSel ? "sub" : "add";
|
||||
RNG rng(mix64(kSuiteSalt ^ 0x5A5C0DEULL ^ (uint64_t)caseidx));
|
||||
|
||||
std::vector<int> shape = sampleShape(rng);
|
||||
const int da = sampleDepth(rng), db = sampleDepth(rng), Tr = sampleDepth(rng);
|
||||
|
||||
static const int cncand[] = { 1, 1, 2, 3, 4 };
|
||||
const int cn = cncand[rng.uniform(0, 5)];
|
||||
|
||||
static const int maskDepths[] = { CV_8U, CV_8S, CV_Bool };
|
||||
const int md = maskDepths[rng.uniform(0, 3)];
|
||||
|
||||
SCOPED_TRACE(cv::format("%s caseidx=%d da=%d db=%d Tr=%d cn=%d md=%d shape=%s",
|
||||
opStr, caseidx, da, db, Tr, cn, md, shapeStr(shape).c_str()));
|
||||
|
||||
Mat a = makeRandom(rng, shape, cn, da), b = makeRandom(rng, shape, cn, db);
|
||||
|
||||
// mask: single-channel, output spatial shape, ~half zero. Build a u8 0/1 master, convert it to
|
||||
// the chosen mask depth for the engine; the u8 master drives the reference copyTo.
|
||||
const int nd = (int)shape.size();
|
||||
Mat m8(nd, shape.data(), CV_8U);
|
||||
cvtest::randUni(rng, m8, Scalar::all(0), Scalar::all(2)); // 0 or 1
|
||||
Mat mask; m8.convertTo(mask, md);
|
||||
|
||||
// pre-existing output content (preserved where mask==0): dst = mask ? op : dst.
|
||||
Mat init = makeRandom(rng, shape, cn, Tr).clone(); // contiguous Tr-typed dst
|
||||
|
||||
// reference: full op per channel (cast to common type C, op to Tr), merge, then overwrite the
|
||||
// masked subset of `init` (the rest stays as the pre-existing content).
|
||||
int C = (da == db) ? da : promote2(da, db);
|
||||
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
||||
std::vector<Mat> refch(cn);
|
||||
for (int c = 0; c < cn; c++)
|
||||
{
|
||||
Mat apC, bpC; cvtest::convert(ach[c], apC, C); cvtest::convert(bch[c], bpC, C);
|
||||
cvtest::add(apC, 1, bpC, op == OP_SUB ? -1 : 1, Scalar(), refch[c], Tr);
|
||||
}
|
||||
Mat refFull; cv::merge(refch, refFull);
|
||||
Mat ref = init.clone();
|
||||
cvtest::copy(refFull, ref, m8);
|
||||
|
||||
// public op with a write-mask: the pre-existing output is preserved where mask==0.
|
||||
Mat out = init.clone();
|
||||
if (op == OP_SUB) cv::subtract(a, b, out, mask, Tr);
|
||||
else cv::add (a, b, out, mask, Tr);
|
||||
|
||||
checkClose(out, ref, Tr, isFloat(da) || isFloat(db), opStr);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Core_EW, EW_Extensive_Mask,
|
||||
testing::Combine(testing::Values(0, 1), testing::Range(0, kNumCases)),
|
||||
[](const testing::TestParamInfo<std::tuple<int,int>>& ti) {
|
||||
return cv::format("%s_case%04d", std::get<0>(ti.param) ? "sub" : "add",
|
||||
std::get<1>(ti.param));
|
||||
});
|
||||
|
||||
// NOTE: a standalone cast group was dropped - the engine cast == cv::convertTo (comparing them would
|
||||
// be a tautology), and mixed-type casts are already exercised inside the add/sub/mul/div groups above.
|
||||
|
||||
}} // namespace
|
||||
@@ -1592,7 +1592,11 @@ TEST(Core_MatExpr, mul_scalar_use_after_scope_23577)
|
||||
EXPECT_EQ(0, cvtest::norm(res, Mat(2, 3, CV_32FC1, Scalar::all(21.0f)), NORM_INF));
|
||||
}
|
||||
|
||||
TEST(Core_Arithm, scalar_handling_19599) // https://github.com/opencv/opencv/issues/19599 (OpenCV 4.x+ only)
|
||||
// Disabled with the new broadcasting element-wise engine: a 4x1 CV_64F *Mat* is no longer treated as a
|
||||
// Scalar (only true scalars - numbers / cv::Scalar / Vec / Matx, which arrive via _InputArray::MATX -
|
||||
// are scalars; real Mats ride broadcasting). Here b broadcasts against a(1x1) -> 4x1, not 1x1. A
|
||||
// follow-up OpenCV issue tracks this intended behavior change.
|
||||
TEST(Core_Arithm, DISABLED_scalar_handling_19599) // https://github.com/opencv/opencv/issues/19599 (OpenCV 4.x+ only)
|
||||
{
|
||||
Mat a(1, 1, CV_32F, Scalar::all(1));
|
||||
Mat b(4, 1, CV_64F, Scalar::all(1)); // MatExpr may convert Scalar to Mat
|
||||
|
||||
Reference in New Issue
Block a user