mirror of
https://github.com/opencv/opencv.git
synced 2026-09-25 04:09:57 +03:00
Added Backend Agnostic fusion in DNN - #29656 # What This Adds A way for any layer, on any backend, to absorb trailing per-element math — with the pass knowing nothing about either layer and holding no list of op types. - **Description**: `LayerMath` (one layer's math) → `AdjacencyGraph` (hash-consed chain) - **Contract**: two `bool` virtuals on `Layer` - **CPU implementations**: three, behind that contract Pointwise fusion is the first consumer, not the point. ## The Problem ```cpp ActivationLayer* activ = dynamic_cast<ActivationLayer*>(layer_ptr); Conv2Layer* conv = getLayer<Conv2Layer>(newprog, conv_layer_idx); if (conv) conv->fuseActivation(layer); ``` Guest type, host type, method name all hardcoded in the pass. `Clip` never fused (fails the cast), `Gemm` never fused (no virtual), no 2-op chain fused at all, and no backend could fuse anything without a CPU-specific method. ## Results | Model | 5.x | this PR | speedup | |-----------------------|----------|----------|---------| | MPHand | 2.34 ms | 1.03 ms | 2.27 | | EfficientNet | 9.82 ms | 5.00 ms | 1.96 | | MPPose | 5.14 ms | 2.81 ms | 1.83 | | MobileNet_SSD_v1_ONNX | 12.58 ms | 8.38 ms | 1.50 | | BlazeFace | 0.95 ms | 0.77 ms | 1.23 | | DenseNet_121 | 20.56 ms | 19.08 ms | 1.08 | | MobileNetv2_ONNX | 2.07 ms | 1.99 ms | 1.04 | | MobileViT_XS | 6.33 ms | 6.11 ms | 1.04 | | YuNet_320 | 1.33 ms | 1.27 ms | 1.04 | | PPOCRv3 | 43.05 ms | 42.15 ms | 1.02 | | BERT | 9.78 ms | 9.68 ms | 1.01 | | BEiT_Base_Patch16_224 | 27.48 ms | 27.08 ms | 1.01 | | DeiT_Tiny_Patch16_224 | 4.74 ms | 4.72 ms | 1.01 | | MPPalm | 1.88 ms | 1.85 ms | 1.01 | | SSD | 50.19 ms | 49.82 ms | 1.01 | | YOLOv4_tiny | 7.10 ms | 7.03 ms | 1.01 | ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake
417 lines
14 KiB
C++
417 lines
14 KiB
C++
// This file is part of OpenCV project.
|
|
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
|
// of this distribution and at http://opencv.org/license.html.
|
|
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
|
// Third party copyrights are property of their respective owners.
|
|
|
|
#include "test_precomp.hpp"
|
|
#include "../src/adjacency_graph.hpp"
|
|
#include "../src/layers/cpu_kernels/fusion_apply.hpp"
|
|
|
|
namespace opencv_test { namespace {
|
|
|
|
using namespace cv::dnn;
|
|
|
|
static const std::vector<const float*> kNoBufs;
|
|
|
|
// A standalone graph from one layer's math. Only tests build graphs this way;
|
|
// the pass always goes through a shared arena and fusion::extract.
|
|
static Ptr<AdjacencyGraph> graphOf(const LayerMath& m)
|
|
{
|
|
AdjacencyGraphBuilder b;
|
|
const int in = b.internNode(FusionEltwiseOp::INPUT, {});
|
|
const int root = fusion::instantiate(b, in, m);
|
|
if (root < 0)
|
|
return Ptr<AdjacencyGraph>();
|
|
return fusion::extract(b.graph(), root, std::vector<Mat>());
|
|
}
|
|
|
|
// A layer states its math through the table registered for its type, not a virtual,
|
|
// so tests ask for it exactly the way the pass does.
|
|
static bool unfoldOf(const Ptr<Layer>& l, LayerMath& m, const ConstOperand& side)
|
|
{
|
|
const FusionOps* ops = fusionOpsFor(l.get());
|
|
return ops && ops->unfold && ops->unfold(l.get(), m, side);
|
|
}
|
|
|
|
static float eval1(const LayerMath& r, float x)
|
|
{
|
|
Ptr<AdjacencyGraph> g = graphOf(r);
|
|
CV_Assert(g);
|
|
return fusion::evalElement(*g, x, kNoBufs, 0);
|
|
}
|
|
|
|
TEST(Fusion, IdenticalMathCollapsesToTheSameNode)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
LayerMath r;
|
|
const int zero = r.constant(0.f);
|
|
r.binary(FusionEltwiseOp::MAX, LayerMath::INPUT_VALUE, zero);
|
|
|
|
EXPECT_EQ(fusion::instantiate(arena, in, r), fusion::instantiate(arena, in, r));
|
|
EXPECT_EQ(fusion::instantiate(arena, in, r), fusion::instantiate(arena, in, r));
|
|
EXPECT_EQ(3u, arena.size());
|
|
|
|
AdjacencyGraphBuilder b;
|
|
const int i2 = b.internNode(FusionEltwiseOp::INPUT, {});
|
|
const int k = b.internNode(FusionEltwiseOp::CONST, {}, 7.f);
|
|
EXPECT_EQ(b.internNode(FusionEltwiseOp::ADD, {i2, k}), b.internNode(FusionEltwiseOp::ADD, {k, i2}));
|
|
EXPECT_NE(b.internNode(FusionEltwiseOp::SUB, {i2, k}), b.internNode(FusionEltwiseOp::SUB, {k, i2}));
|
|
}
|
|
|
|
TEST(Fusion, ConeIsBoundedIndependentlyOfArenaSize)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
LayerMath a, b;
|
|
const int zero = a.constant(0.f);
|
|
a.binary(FusionEltwiseOp::MAX, LayerMath::INPUT_VALUE, zero);
|
|
fusion::detail::gelu(b);
|
|
const int rootA = fusion::instantiate(arena, in, a);
|
|
const int rootB = fusion::instantiate(arena, in, b);
|
|
ASSERT_GE(rootA, 0);
|
|
ASSERT_GE(rootB, 0);
|
|
|
|
std::vector<char> scratch;
|
|
EXPECT_EQ(3, fusion::detail::markLive(arena.graph(), rootA, scratch));
|
|
EXPECT_EQ(9, fusion::detail::markLive(arena.graph(), rootB, scratch));
|
|
EXPECT_GT(arena.size(), (size_t)9);
|
|
}
|
|
|
|
TEST(Fusion, ExtractionYieldsAStandaloneGraph)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
LayerMath a, b;
|
|
const int zero = a.constant(0.f);
|
|
a.binary(FusionEltwiseOp::MAX, LayerMath::INPUT_VALUE, zero);
|
|
fusion::detail::gelu(b);
|
|
fusion::instantiate(arena, in, a);
|
|
const int rootB = fusion::instantiate(arena, in, b);
|
|
|
|
Ptr<AdjacencyGraph> g = fusion::extract(arena.graph(), rootB, std::vector<Mat>());
|
|
ASSERT_TRUE(g);
|
|
EXPECT_EQ(9u, g->size());
|
|
EXPECT_EQ(FusionEltwiseOp::INPUT, g->nodes()[0].op);
|
|
EXPECT_EQ(FusionEltwiseOp::MUL, g->nodes()[g->outputNode()].op);
|
|
EXPECT_NEAR(0.5f * 1.5f * (1.f + std::erf(1.5f * 0.70710678118654752440f)),
|
|
fusion::evalElement(*g, 1.5f, kNoBufs, 0), 1e-5);
|
|
|
|
EXPECT_FALSE(fusion::extract(arena.graph(), -1, std::vector<Mat>()));
|
|
EXPECT_FALSE(fusion::extract(arena.graph(), (int)arena.size(), std::vector<Mat>()));
|
|
}
|
|
|
|
TEST(Fusion, OverLimitConeIsRefusedNotEvaluated)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
int cur = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
std::vector<char> scratch;
|
|
int steps = 0;
|
|
while (fusion::detail::markLive(arena.graph(), cur, scratch) <= FUSION_MAX_EXPR_NODES && steps < 200) {
|
|
LayerMath r;
|
|
fusion::detail::gelu(r);
|
|
const int next = fusion::instantiate(arena, cur, r);
|
|
ASSERT_GE(next, 0);
|
|
cur = next;
|
|
steps++;
|
|
}
|
|
ASSERT_GT(fusion::detail::markLive(arena.graph(), cur, scratch), FUSION_MAX_EXPR_NODES);
|
|
EXPECT_FALSE(fusion::extract(arena.graph(), cur, std::vector<Mat>()));
|
|
}
|
|
|
|
TEST(Fusion, MathMatchesClosedForm)
|
|
{
|
|
const float xs[] = { -3.f, -0.5f, 0.f, 0.25f, 1.f, 4.f };
|
|
LayerMath r;
|
|
for (float x : xs) {
|
|
r = LayerMath();
|
|
r.binary(FusionEltwiseOp::MAX, LayerMath::INPUT_VALUE, r.constant(0.f));
|
|
EXPECT_FLOAT_EQ(std::max(x, 0.f), eval1(r, x)) << "relu " << x;
|
|
|
|
r = LayerMath();
|
|
r.clamp(LayerMath::INPUT_VALUE, 0.f, 6.f);
|
|
EXPECT_FLOAT_EQ(std::min(std::max(x, 0.f), 6.f), eval1(r, x)) << "clip " << x;
|
|
|
|
r = LayerMath(); fusion::detail::sigmoid(r);
|
|
EXPECT_NEAR(1.f / (1.f + std::exp(-x)), eval1(r, x), 1e-5) << "sigmoid " << x;
|
|
|
|
r = LayerMath(); fusion::detail::gelu(r);
|
|
EXPECT_NEAR(0.5f * x * (1.f + std::erf(x * 0.70710678118654752440f)), eval1(r, x), 1e-5)
|
|
<< "gelu " << x;
|
|
|
|
r = LayerMath();
|
|
r.unary(FusionEltwiseOp::TANH, LayerMath::INPUT_VALUE);
|
|
EXPECT_NEAR(std::tanh(x), eval1(r, x), 1e-6) << "tanh " << x;
|
|
|
|
r = LayerMath();
|
|
const int scaled = r.binary(FusionEltwiseOp::MUL, LayerMath::INPUT_VALUE, r.constant(2.f));
|
|
r.unary(FusionEltwiseOp::EXP, r.binary(FusionEltwiseOp::ADD, scaled, r.constant(5.f)));
|
|
EXPECT_NEAR(std::exp(2.f * x + 5.f), eval1(r, x), 1e-2) << "scaled exp " << x;
|
|
}
|
|
}
|
|
|
|
TEST(Fusion, EmptyMathIsRefused)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
EXPECT_EQ(-1, fusion::instantiate(arena, in, LayerMath()));
|
|
EXPECT_EQ(1u, arena.size());
|
|
}
|
|
|
|
TEST(Fusion, ReversedSubIsRefused)
|
|
{
|
|
LayerParams lp;
|
|
lp.set("operation", "sub");
|
|
Ptr<Layer> sub = NaryEltwiseLayer::create(lp);
|
|
ASSERT_TRUE(sub);
|
|
sub->inputs.assign(2, Arg());
|
|
|
|
LayerMath r;
|
|
ConstOperand vs;
|
|
vs.count = 1;
|
|
vs.consts[0].value = 3.f;
|
|
vs.flowIsFirstInput = false;
|
|
EXPECT_FALSE(unfoldOf(sub, r, vs));
|
|
|
|
vs.flowIsFirstInput = true;
|
|
r = LayerMath();
|
|
ASSERT_TRUE(unfoldOf(sub, r, vs));
|
|
EXPECT_FLOAT_EQ(-1.f, eval1(r, 2.f));
|
|
}
|
|
|
|
TEST(Fusion, VariadicNaryIsRefused)
|
|
{
|
|
LayerParams lp;
|
|
lp.set("operation", "sum");
|
|
Ptr<Layer> sum = NaryEltwiseLayer::create(lp);
|
|
ASSERT_TRUE(sum);
|
|
|
|
LayerMath r;
|
|
ConstOperand vs;
|
|
vs.count = 1;
|
|
vs.consts[0].value = 3.f;
|
|
|
|
sum->inputs.assign(3, Arg());
|
|
EXPECT_FALSE(unfoldOf(sum, r, vs));
|
|
|
|
r = LayerMath();
|
|
sum->inputs.assign(2, Arg());
|
|
EXPECT_TRUE(unfoldOf(sum, r, vs));
|
|
}
|
|
|
|
TEST(Fusion, ClipWithOneDynamicBoundIsRefused)
|
|
{
|
|
LayerParams lp;
|
|
Ptr<Layer> clip = ClipLayer::create(lp);
|
|
ASSERT_TRUE(clip);
|
|
|
|
LayerMath r;
|
|
ConstOperand vs;
|
|
vs.count = 2;
|
|
vs.consts[0].value = 2.f;
|
|
vs.consts[1].value = 0.f;
|
|
|
|
clip->inputs = { Arg(1), Arg(2) };
|
|
EXPECT_FALSE(unfoldOf(clip, r, vs));
|
|
|
|
// Clip(x, "", max): the omitted min is an empty Arg, not a missing one
|
|
r = LayerMath();
|
|
clip->inputs = { Arg(1), Arg(0), Arg(2) };
|
|
EXPECT_FALSE(unfoldOf(clip, r, vs));
|
|
|
|
r = LayerMath();
|
|
vs.consts[1].value = 6.f;
|
|
clip->inputs = { Arg(1), Arg(2), Arg(3) };
|
|
ASSERT_TRUE(unfoldOf(clip, r, vs));
|
|
EXPECT_FLOAT_EQ(2.f, eval1(r, 1.f));
|
|
EXPECT_FLOAT_EQ(6.f, eval1(r, 9.f));
|
|
}
|
|
|
|
// Every layer now states which kernel computes its own math, so this goes through
|
|
// the real path: the layer fills LayerMath, and PreparedFusion picks the kernel up.
|
|
TEST(Fusion, LayersDeclareTheirOwnKernel)
|
|
{
|
|
struct { const char* name; const char* type; int nInputs; } cases[] = {
|
|
{ "sigmoid", "Sigmoid", 1 },
|
|
{ "gelu", "Gelu", 1 },
|
|
{ "tanh", "TanH", 1 },
|
|
{ "relu", "ReLU", 1 },
|
|
};
|
|
|
|
for (const auto& c : cases) {
|
|
LayerParams lp;
|
|
Ptr<Layer> l = LayerFactory::createLayerInstance(c.type, lp);
|
|
ASSERT_TRUE(l) << c.name;
|
|
l->inputs.assign(c.nInputs, Arg(1));
|
|
|
|
LayerMath m;
|
|
ConstOperand side;
|
|
ASSERT_TRUE(unfoldOf(l, m, side)) << c.name;
|
|
EXPECT_TRUE(m.kernel.fn != nullptr) << c.name << ": no kernel declared";
|
|
|
|
Ptr<AdjacencyGraph> expr = graphOf(m);
|
|
ASSERT_TRUE(expr) << c.name;
|
|
expr->kernel = m.kernel;
|
|
|
|
PreparedFusion pf;
|
|
const bool took = pf.take(expr);
|
|
EXPECT_TRUE(took) << c.name;
|
|
EXPECT_TRUE(pf.activationFn != nullptr) << c.name << ": fell to the interpreter";
|
|
}
|
|
}
|
|
|
|
// Clip and NaryEltwise are not ElementWiseLayers, so they declare explicitly rather
|
|
// than through the wrapper. They must end up on the same fast path.
|
|
TEST(Fusion, NonElementwiseLayersDeclareToo)
|
|
{
|
|
LayerParams lp;
|
|
Ptr<Layer> clip = ClipLayer::create(lp);
|
|
ASSERT_TRUE(clip);
|
|
clip->inputs = { Arg(1), Arg(2), Arg(3) };
|
|
LayerMath cm;
|
|
ConstOperand cs;
|
|
cs.count = 2; cs.consts[0].value = 0.f; cs.consts[1].value = 6.f;
|
|
ASSERT_TRUE(unfoldOf(clip, cm, cs));
|
|
EXPECT_TRUE(cm.kernel.fn != nullptr) << "clip declared no kernel";
|
|
EXPECT_EQ(2, cm.kernel.nparams);
|
|
|
|
LayerParams np;
|
|
np.set("operation", "max");
|
|
Ptr<Layer> mx = NaryEltwiseLayer::create(np);
|
|
ASSERT_TRUE(mx);
|
|
mx->inputs.assign(2, Arg(1));
|
|
LayerMath nm;
|
|
ConstOperand ns;
|
|
ns.count = 1; ns.consts[0].value = 0.f;
|
|
ASSERT_TRUE(unfoldOf(mx, nm, ns));
|
|
EXPECT_TRUE(nm.kernel.fn != nullptr) << "Max(x,0) declared no kernel";
|
|
}
|
|
|
|
TEST(Fusion, ApplyTakesKernelPathThenInterpreterPath)
|
|
{
|
|
LayerMath r;
|
|
r.setKernel(cv::dnn::getActivationFunc(ACTIV_CLIP), { 0.f, 6.f });
|
|
r.clamp(LayerMath::INPUT_VALUE, 0.f, 6.f);
|
|
Ptr<AdjacencyGraph> ce = graphOf(r);
|
|
ce->kernel = r.kernel;
|
|
PreparedFusion kern;
|
|
ASSERT_TRUE(kern.take(ce));
|
|
ASSERT_TRUE(kern.activationFn != nullptr);
|
|
|
|
int n = 5;
|
|
Mat y(1, &n, CV_32F);
|
|
const float src[] = { -2.f, 0.f, 3.f, 6.f, 9.f };
|
|
std::copy(src, src + n, y.ptr<float>());
|
|
kern.run(y);
|
|
const float want[] = { 0.f, 0.f, 3.f, 6.f, 6.f };
|
|
for (int i = 0; i < n; i++)
|
|
EXPECT_FLOAT_EQ(want[i], y.ptr<float>()[i]) << "clip i=" << i;
|
|
|
|
r = LayerMath();
|
|
r.unary(FusionEltwiseOp::SQRT, LayerMath::INPUT_VALUE);
|
|
PreparedFusion interp;
|
|
ASSERT_TRUE(interp.take(graphOf(r)));
|
|
EXPECT_TRUE(interp.activationFn == nullptr);
|
|
|
|
int big = (1 << 16) + 17;
|
|
Mat z(1, &big, CV_32F);
|
|
for (int i = 0; i < big; i++)
|
|
z.ptr<float>()[i] = (float)(i % 100);
|
|
interp.run(z);
|
|
for (int i = 0; i < big; i += 997)
|
|
EXPECT_NEAR(std::sqrt((float)(i % 100)), z.ptr<float>()[i], 1e-5) << "sqrt i=" << i;
|
|
}
|
|
|
|
TEST(Fusion, PerChannelConstIndexesTheLastAxis)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
LayerMath r;
|
|
r.binary(FusionEltwiseOp::MUL, LayerMath::INPUT_VALUE, r.perChannelConstant(1));
|
|
const int root = fusion::instantiate(arena, in, r);
|
|
ASSERT_GE(root, 0);
|
|
|
|
int one = 1, three = 3;
|
|
Mat b0(1, &one, CV_32F);
|
|
b0.ptr<float>()[0] = 1.f;
|
|
Mat b1(1, &three, CV_32F);
|
|
b1.ptr<float>()[0] = 2.f;
|
|
b1.ptr<float>()[1] = 3.f;
|
|
b1.ptr<float>()[2] = 4.f;
|
|
|
|
std::vector<Mat> tooFew(1, b0);
|
|
EXPECT_FALSE(fusion::extract(arena.graph(), root, tooFew));
|
|
|
|
std::vector<Mat> bufs;
|
|
bufs.push_back(b0);
|
|
bufs.push_back(b1);
|
|
Ptr<AdjacencyGraph> expr = fusion::extract(arena.graph(), root, bufs);
|
|
ASSERT_TRUE(expr);
|
|
bool seen = false;
|
|
for (const FusionNode& nd : expr->nodes()) {
|
|
if (nd.op == FusionEltwiseOp::PER_CHANNEL_CONST) {
|
|
EXPECT_EQ(1, nd.constBufferId);
|
|
seen = true;
|
|
}
|
|
}
|
|
EXPECT_TRUE(seen);
|
|
|
|
PreparedFusion fa;
|
|
ASSERT_TRUE(fa.take(expr));
|
|
EXPECT_TRUE(fa.activationFn == nullptr);
|
|
|
|
int sz[] = { 2, 3 };
|
|
Mat y(2, sz, CV_32F);
|
|
for (int i = 0; i < 6; i++)
|
|
y.ptr<float>()[i] = 1.f;
|
|
fa.run(y);
|
|
const float want[] = { 2.f, 3.f, 4.f, 2.f, 3.f, 4.f };
|
|
for (int i = 0; i < 6; i++)
|
|
EXPECT_FLOAT_EQ(want[i], y.ptr<float>()[i]) << "i=" << i;
|
|
}
|
|
|
|
TEST(Fusion, SharedRootKeepsEachChainsOwnBuffers)
|
|
{
|
|
AdjacencyGraphBuilder arena;
|
|
const int in = arena.internNode(FusionEltwiseOp::INPUT, {});
|
|
|
|
LayerMath r;
|
|
r.binary(FusionEltwiseOp::MUL, LayerMath::INPUT_VALUE, r.perChannelConstant(0));
|
|
|
|
const int rootA = fusion::instantiate(arena, in, r);
|
|
const int rootB = fusion::instantiate(arena, in, r);
|
|
ASSERT_GE(rootA, 0);
|
|
EXPECT_EQ(rootA, rootB);
|
|
|
|
int three = 3;
|
|
Mat ba(1, &three, CV_32F), bb(1, &three, CV_32F);
|
|
for (int i = 0; i < 3; i++) { ba.ptr<float>()[i] = 2.f; bb.ptr<float>()[i] = 10.f; }
|
|
|
|
Ptr<AdjacencyGraph> ea = fusion::extract(arena.graph(), rootA, std::vector<Mat>(1, ba));
|
|
Ptr<AdjacencyGraph> eb = fusion::extract(arena.graph(), rootB, std::vector<Mat>(1, bb));
|
|
ASSERT_TRUE(ea);
|
|
ASSERT_TRUE(eb);
|
|
|
|
ASSERT_EQ(1u, ea->constBufs.size());
|
|
ASSERT_EQ(1u, eb->constBufs.size());
|
|
EXPECT_FLOAT_EQ(2.f, ea->constBufs[0].ptr<float>()[0]);
|
|
EXPECT_FLOAT_EQ(10.f, eb->constBufs[0].ptr<float>()[0]);
|
|
|
|
PreparedFusion fa, fb;
|
|
ASSERT_TRUE(fa.take(ea));
|
|
ASSERT_TRUE(fb.take(eb));
|
|
|
|
int sz[] = { 1, 3 };
|
|
Mat ya(2, sz, CV_32F), yb(2, sz, CV_32F);
|
|
for (int i = 0; i < 3; i++) { ya.ptr<float>()[i] = 1.f; yb.ptr<float>()[i] = 1.f; }
|
|
fa.run(ya);
|
|
fb.run(yb);
|
|
for (int i = 0; i < 3; i++) {
|
|
EXPECT_FLOAT_EQ(2.f, ya.ptr<float>()[i]) << "A i=" << i;
|
|
EXPECT_FLOAT_EQ(10.f, yb.ptr<float>()[i]) << "B i=" << i;
|
|
}
|
|
}
|
|
|
|
}} // namespace opencv_test
|