From 4acd2ed6cc21bcd974a4e5183f7ce2f2abdc78e1 Mon Sep 17 00:00:00 2001 From: Anand Mahesh Date: Sun, 29 Mar 2026 21:43:23 +0530 Subject: [PATCH] features2d: add OpenCL acceleration for BFMatcher cross-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BFMatcher::match() with crossCheck=true previously skipped the OCL dispatch in knnMatchImpl entirely, falling back to CPU even when UMat inputs and an OpenCL device were available. This adds ocl_matchWithCrossCheck() for CV_32FC1 descriptors (e.g. SIFT, SURF): both the forward and reverse nearest-neighbour passes run on the GPU via the existing ocl_matchSingle() kernel, then the cross-check filter runs on the CPU. Only two small index arrays (1×N ints) are downloaded — the O(N²×D) distance work stays on the device. The OCL dispatch in knnMatchImpl is also refactored to unify the Mat/UMat train collection selection before branching on crossCheck. On a NVIDIA RTX 3060 with SIFT descriptors the OCL path is 6–9× faster than CPU at 2k–10k features per image. Binary descriptors (ORB, BRIEF — CV_8U) are unaffected; the existing type guard in ocl_matchSingle keeps them on the CPU path as before. Also adds a correctness test (Features2d_BFMatcher_CrossCheck) and an OCL perf test (BruteForceMatcherFixture/MatchCrossCheck). --- .../perf/opencl/perf_brute_force_matcher.cpp | 21 ++++++ modules/features2d/src/matchers.cpp | 65 +++++++++++++++---- .../test/test_matchers_algorithmic.cpp | 43 ++++++++++++ 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/modules/features2d/perf/opencl/perf_brute_force_matcher.cpp b/modules/features2d/perf/opencl/perf_brute_force_matcher.cpp index 439140a5ff..6af06d37ad 100644 --- a/modules/features2d/perf/opencl/perf_brute_force_matcher.cpp +++ b/modules/features2d/perf/opencl/perf_brute_force_matcher.cpp @@ -123,6 +123,27 @@ OCL_PERF_TEST_P(BruteForceMatcherFixture, RadiusMatch, ::testing::Combine(OCL_PE SANITY_CHECK_MATCHES(matches1, 1e-3); } +OCL_PERF_TEST_P(BruteForceMatcherFixture, MatchCrossCheck, ::testing::Combine(OCL_PERF_ENUM(OCL_SIZE_1, OCL_SIZE_2, OCL_SIZE_3), OCL_PERF_ENUM((MatType)CV_32FC1) ) ) +{ + const Size_MatType_t params = GetParam(); + const Size srcSize = get<0>(params); + const int type = get<1>(params); + + checkDeviceMaxMemoryAllocSize(srcSize, type); + + vector matches; + UMat uquery(srcSize, type), utrain(srcSize, type); + + declare.in(uquery, utrain, WARMUP_RNG); + + BFMatcher matcher(NORM_L2, true /*crossCheck*/); + + OCL_TEST_CYCLE() + matcher.match(uquery, utrain, matches); + + SANITY_CHECK_MATCHES(matches, 1e-3); +} + } // ocl } // cvtest diff --git a/modules/features2d/src/matchers.cpp b/modules/features2d/src/matchers.cpp index 97d0a02417..950fd96d58 100644 --- a/modules/features2d/src/matchers.cpp +++ b/modules/features2d/src/matchers.cpp @@ -752,6 +752,49 @@ static bool ocl_knnMatch(InputArray query, InputArray _train, std::vector< std:: return false; return true; } + +// Run match in both directions on GPU, cross-check filter on CPU. +// Reuses ocl_matchSingle so no new kernel is needed. +static bool ocl_matchWithCrossCheck(InputArray query, InputArray train, + std::vector< std::vector >& matches, int dstType) +{ + // Forward pass: for each query descriptor find nearest in train + UMat fwdIdx, fwdDist; + if (!ocl_matchSingle(query, train, fwdIdx, fwdDist, dstType)) + return false; + + // Reverse pass: for each train descriptor find nearest in query + UMat revIdx, revDist; + if (!ocl_matchSingle(train, query, revIdx, revDist, dstType)) + return false; + + // Download index arrays (1 x N ints each — cheap) + Mat fwdIdxCPU = fwdIdx.getMat(ACCESS_READ); + Mat revIdxCPU = revIdx.getMat(ACCESS_READ); + Mat fwdDistCPU = fwdDist.getMat(ACCESS_READ); + + if (fwdIdxCPU.empty() || revIdxCPU.empty() || fwdDistCPU.empty()) + return false; + + const int nQuery = fwdIdxCPU.cols; + const int nTrain = revIdxCPU.cols; + const int* fwd = fwdIdxCPU.ptr(); + const int* rev = revIdxCPU.ptr(); + const float* dist = fwdDistCPU.ptr(); + + matches.clear(); + matches.reserve(nQuery); + + for (int q = 0; q < nQuery; ++q) + { + int t = fwd[q]; + if (t >= 0 && t < nTrain && rev[t] == q) + { + matches.push_back(std::vector(1, DMatch(q, t, 0, dist[q]))); + } + } + return true; +} #endif void BFMatcher::knnMatchImpl( InputArray _queryDescriptors, std::vector >& matches, int knn, @@ -794,21 +837,15 @@ void BFMatcher::knnMatchImpl( InputArray _queryDescriptors, std::vectorknnMatch(usources, utargets, match, 1, mask, true)); } +// Verify that cross-check BFMatcher gives identical results via Mat (CPU) and UMat (OCL or CPU). +// When OpenCL is active the UMat path exercises ocl_matchWithCrossCheck; when it is not, +// both paths fall through to the same CPU code — either way the results must match. +TEST(Features2d_BFMatcher_CrossCheck, ocl_matches_cpu) +{ + RNG rng(42); + const int nQuery = 200; + const int nTrain = 400; + const int dim = 128; + + // Float descriptors: the OCL dispatch in knnMatchImpl requires CV_32FC1 + Mat queryMat(nQuery, dim, CV_32FC1); + Mat trainMat(nTrain, dim, CV_32FC1); + rng.fill(queryMat, RNG::UNIFORM, 0.f, 1.f); + rng.fill(trainMat, RNG::UNIFORM, 0.f, 1.f); + + // CPU reference: Mat inputs always take the CPU path + Ptr cpuMatcher = BFMatcher::create(NORM_L2, true /*crossCheck*/); + vector cpuMatches; + cpuMatcher->match(queryMat, trainMat, cpuMatches); + + // UMat path: activates OCL dispatch when OpenCL is available + UMat queryUMat = queryMat.getUMat(ACCESS_READ); + UMat trainUMat = trainMat.getUMat(ACCESS_READ); + Ptr oclMatcher = BFMatcher::create(NORM_L2, true /*crossCheck*/); + vector oclMatches; + oclMatcher->match(queryUMat, trainUMat, oclMatches); + + // Both paths must return the same set of matches (order may differ) + ASSERT_EQ(cpuMatches.size(), oclMatches.size()); + + auto byQuery = [](const DMatch& a, const DMatch& b) { return a.queryIdx < b.queryIdx; }; + sort(cpuMatches.begin(), cpuMatches.end(), byQuery); + sort(oclMatches.begin(), oclMatches.end(), byQuery); + + for (size_t i = 0; i < cpuMatches.size(); ++i) + { + EXPECT_EQ(cpuMatches[i].queryIdx, oclMatches[i].queryIdx) << "at index " << i; + EXPECT_EQ(cpuMatches[i].trainIdx, oclMatches[i].trainIdx) << "at index " << i; + EXPECT_NEAR(cpuMatches[i].distance, oclMatches[i].distance, 1e-3f) << "at index " << i; + } +} + }} // namespace