1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-21 19:33:03 +04:00

features2d: add OpenCL acceleration for BFMatcher cross-check

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).
This commit is contained in:
Anand Mahesh
2026-03-29 21:43:23 +05:30
parent c7732e1043
commit 4acd2ed6cc
3 changed files with 115 additions and 14 deletions
@@ -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<DMatch> 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
+51 -14
View File
@@ -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<DMatch> >& 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<int>();
const int* rev = revIdxCPU.ptr<int>();
const float* dist = fwdDistCPU.ptr<float>();
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<DMatch>(1, DMatch(q, t, 0, dist[q])));
}
}
return true;
}
#endif
void BFMatcher::knnMatchImpl( InputArray _queryDescriptors, std::vector<std::vector<DMatch> >& matches, int knn,
@@ -794,21 +837,15 @@ void BFMatcher::knnMatchImpl( InputArray _queryDescriptors, std::vector<std::vec
{
if(knn == 1)
{
if(trainDescCollection.empty())
InputArray train = trainDescCollection.empty() ?
(InputArray)utrainDescCollection[0] : (InputArray)trainDescCollection[0];
bool oclOk = crossCheck ?
ocl_matchWithCrossCheck(_queryDescriptors, train, matches, normType) :
ocl_match(_queryDescriptors, train, matches, normType);
if (oclOk)
{
if(ocl_match(_queryDescriptors, utrainDescCollection[0], matches, normType))
{
CV_IMPL_ADD(CV_IMPL_OCL);
return;
}
}
else
{
if(ocl_match(_queryDescriptors, trainDescCollection[0], matches, normType))
{
CV_IMPL_ADD(CV_IMPL_OCL);
return;
}
CV_IMPL_ADD(CV_IMPL_OCL);
return;
}
}
else
@@ -629,4 +629,47 @@ TEST(Features2d_DMatch, issue_17771)
EXPECT_NO_THROW(ubf->knnMatch(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<BFMatcher> cpuMatcher = BFMatcher::create(NORM_L2, true /*crossCheck*/);
vector<DMatch> 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<BFMatcher> oclMatcher = BFMatcher::create(NORM_L2, true /*crossCheck*/);
vector<DMatch> 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