From 0fb620f0f0cd2d151e3e998a7f1ed35914b3fe70 Mon Sep 17 00:00:00 2001 From: Anand Mahesh Date: Mon, 13 Jul 2026 07:58:58 +0530 Subject: [PATCH] Fix: BFMatcher isMaskSupported false on crossCheck isMaskSupported now returns false when the matcher is created with crossCheck enabled, because the mask path is unsupported in that mode. knnMatchImpl also drops the mask so it cannot reach batchDistance and trigger its mask.empty() assertion. https://github.com/opencv/opencv/issues/22093 --- modules/features/include/opencv2/features.hpp | 2 +- modules/features/src/matchers.cpp | 3 +- .../test/test_matchers_algorithmic.cpp | 75 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/modules/features/include/opencv2/features.hpp b/modules/features/include/opencv2/features.hpp index 55519cca94..e18198cff2 100644 --- a/modules/features/include/opencv2/features.hpp +++ b/modules/features/include/opencv2/features.hpp @@ -1220,7 +1220,7 @@ public: virtual ~BFMatcher() {} - virtual bool isMaskSupported() const CV_OVERRIDE { return true; } + virtual bool isMaskSupported() const CV_OVERRIDE { return !crossCheck; } /** @brief Brute-force matcher create method. @param normType One of NORM_L1, NORM_L2, NORM_HAMMING, NORM_HAMMING2. L1 and L2 norms are diff --git a/modules/features/src/matchers.cpp b/modules/features/src/matchers.cpp index 2223d59400..476b93f6a5 100644 --- a/modules/features/src/matchers.cpp +++ b/modules/features/src/matchers.cpp @@ -996,8 +996,9 @@ void BFMatcher::knnMatchImpl( InputArray _queryDescriptors, std::vector orb = ORB::create(); + vector keypoints; + Mat descriptors; + orb->detectAndCompute(img, noArray(), keypoints, descriptors); + ASSERT_FALSE(descriptors.empty()); + + Ptr matcher = BFMatcher::create(NORM_HAMMING, true /*crossCheck*/); + ASSERT_FALSE(matcher->isMaskSupported()); + + // Register the descriptors as the train set, then match query against + // train with a non-empty per-image mask. This is the exact call chain + // from the issue (match -> knnMatch -> knnMatchImpl -> batchDistance). + // Before the fix the mask reached batchDistance's CV_Assert(mask.empty()) + // and aborted the process. + matcher->add(descriptors); + Mat mask = Mat::ones(descriptors.rows, descriptors.rows, CV_8UC1); + vector masks(1, mask); + + vector matches; + EXPECT_NO_THROW(matcher->match(descriptors, matches, masks)); + ASSERT_FALSE(matches.empty()); +} + +// OCL coverage for https://github.com/opencv/opencv/issues/22093 +// SIFT is used only to obtain float (CV_32FC1) descriptors, which are then +// wrapped as UMat so the OCL BFMatcher dispatch is eligible. With an empty +// mask the OCL cross-check kernel (ocl_matchWithCrossCheck) runs; with a +// non-empty mask the OCL dispatch refuses it and falls back to the CPU path, +// which (thanks to the fix) must not assert or crash. +TEST(Features2d_BFMatcher_CrossCheck, issue_22093_mask_ocl) +{ + const string imgPath = cvtest::findDataFile( + "cv/detectors_descriptors_evaluation/images_datasets/leuven/img1.png"); + Mat img = imread(imgPath, IMREAD_GRAYSCALE); + ASSERT_FALSE(img.empty()); + + Ptr sift = cv::SIFT::create(); + vector keypoints; + Mat descriptors; + sift->detectAndCompute(img, noArray(), keypoints, descriptors); + ASSERT_EQ(descriptors.type(), CV_32FC1); + ASSERT_FALSE(descriptors.empty()); + + Ptr matcher = BFMatcher::create(NORM_L2, true /*crossCheck*/); + ASSERT_FALSE(matcher->isMaskSupported()); + + matcher->add(descriptors); + UMat query = descriptors.getUMat(ACCESS_READ); + + // (1) explicit empty mask -> OCL cross-check kernel is eligible + vector emptyMasks(1, Mat()); + vector matchesEmpty; + EXPECT_NO_THROW(matcher->match(query, matchesEmpty, emptyMasks)); + ASSERT_FALSE(matchesEmpty.empty()); + + // (2) non-empty mask -> OCL refused, CPU fallback must not crash + Mat mask = Mat::ones(descriptors.rows, descriptors.rows, CV_8UC1); + vector masks(1, mask); + vector matchesMasked; + EXPECT_NO_THROW(matcher->match(query, matchesMasked, masks)); + ASSERT_FALSE(matchesMasked.empty()); +} + }} // namespace