mirror of
https://github.com/opencv/opencv.git
synced 2026-09-29 04:09:43 +03:00
imgproc: accept CV_Bool masks in matchTemplate (#25895) - #29677 ### Problem `cv::Mat_<bool>::depth()` returns `CV_Bool` in 5.0, where it returned `CV_8U` in 4.x. `matchTemplateMask` gates the mask at `templmatch.cpp:737`, so passing a boolean mask now fails with: ``` (-215:Assertion failed) _mask.depth() == CV_8U || _mask.depth() == CV_32F in function 'cv::matchTemplateMask' ``` A binary mask is a normal input for masked template matching, so this is a regression against 4.x. ### Fix Allow `CV_Bool` in the assertion and widen the mask to `CV_8U` before the existing binarization step. The widening is required rather than passing `CV_Bool` straight through, because `cv::threshold()` does not accept `CV_Bool`. Once widened, the existing `THRESH_BINARY` path treats any non-zero entry as selected, which is exactly the documented `CV_8U` mask semantics. `CV_8U` and `CV_32F` masks take an unchanged path. The masked path is the only one affected: `cv::matchTemplate` routes every non-empty mask through `matchTemplateMask`, and there is no OpenCL mask variant. ### Test `Imgproc_MatchTemplateBoolMask.matches_uchar_mask` compares a `Mat_<bool>` mask against the equivalent `CV_8UC1` mask and requires the results to agree, across all six match methods (`TM_SQDIFF`, `TM_SQDIFF_NORMED`, `TM_CCORR`, `TM_CCORR_NORMED`, `TM_CCOEFF`, `TM_CCOEFF_NORMED`) for `CV_8UC1`, `CV_8UC3` and `CV_32FC1` images. 18 parameter combinations. Verified locally on 5.x: all 18 fail without the source change (with the assertion above) and pass with it. The full `*MatchTemplate*` set, 165 tests, passes. No test data needed, the test is synthetic. Part of #25895, and follows the same approach as #29580, #29597 and #29622. ### Pull Request Readiness Checklist - [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 - [x] 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. - [x] The feature is well documented and sample code can be built with the project CMake
325 lines
11 KiB
C++
325 lines
11 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.
|
|
|
|
#include "test_precomp.hpp"
|
|
|
|
namespace opencv_test { namespace {
|
|
|
|
CV_ENUM(MatchTemplType, cv::TM_CCORR, cv::TM_CCORR_NORMED,
|
|
cv::TM_SQDIFF, cv::TM_SQDIFF_NORMED,
|
|
cv::TM_CCOEFF, cv::TM_CCOEFF_NORMED)
|
|
|
|
class Imgproc_MatchTemplateWithMask : public TestWithParam<std::tuple<MatType,MatType>>
|
|
{
|
|
protected:
|
|
// Member functions inherited from ::testing::Test
|
|
void SetUp() override;
|
|
|
|
// Matrices for test calculations (always CV_32)
|
|
Mat img_;
|
|
Mat templ_;
|
|
Mat mask_;
|
|
Mat templ_masked_;
|
|
Mat img_roi_masked_;
|
|
// Matrices for call to matchTemplate (have test type)
|
|
Mat img_testtype_;
|
|
Mat templ_testtype_;
|
|
Mat mask_testtype_;
|
|
Mat result_;
|
|
|
|
// Constants
|
|
static const Size IMG_SIZE;
|
|
static const Size TEMPL_SIZE;
|
|
static const Point TEST_POINT;
|
|
};
|
|
|
|
// Arbitraryly chosen test constants
|
|
const Size Imgproc_MatchTemplateWithMask::IMG_SIZE(160, 100);
|
|
const Size Imgproc_MatchTemplateWithMask::TEMPL_SIZE(21, 13);
|
|
const Point Imgproc_MatchTemplateWithMask::TEST_POINT(8, 9);
|
|
|
|
void Imgproc_MatchTemplateWithMask::SetUp()
|
|
{
|
|
int type = std::get<0>(GetParam());
|
|
int type_mask = std::get<1>(GetParam());
|
|
|
|
// Matrices are created with the depth to test (for the call to matchTemplate()), but are also
|
|
// converted to CV_32 for the test calculations, because matchTemplate() also only operates on
|
|
// and returns CV_32.
|
|
img_testtype_.create(IMG_SIZE, type);
|
|
templ_testtype_.create(TEMPL_SIZE, type);
|
|
mask_testtype_.create(TEMPL_SIZE, type_mask);
|
|
|
|
randu(img_testtype_, 0, 10);
|
|
randu(templ_testtype_, 0, 10);
|
|
randu(mask_testtype_, 0, 5);
|
|
|
|
img_testtype_.convertTo(img_, CV_32F);
|
|
templ_testtype_.convertTo(templ_, CV_32F);
|
|
mask_testtype_.convertTo(mask_, CV_32F);
|
|
if (CV_MAT_DEPTH(type_mask) == CV_8U)
|
|
{
|
|
// CV_8U masks are interpreted as binary masks
|
|
mask_.setTo(Scalar::all(1), mask_ != 0);
|
|
}
|
|
if (mask_.channels() != templ_.channels())
|
|
{
|
|
std::vector<Mat> mask_channels(templ_.channels(), mask_);
|
|
merge(mask_channels.data(), templ_.channels(), mask_);
|
|
}
|
|
|
|
Rect roi(TEST_POINT, TEMPL_SIZE);
|
|
img_roi_masked_ = img_(roi).mul(mask_);
|
|
templ_masked_ = templ_.mul(mask_);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplSQDIFF)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_SQDIFF, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Mat temp = img_roi_masked_ - templ_masked_;
|
|
Scalar temp_s = sum(temp.mul(temp));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplSQDIFF_NORMED)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_SQDIFF_NORMED, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Mat temp = img_roi_masked_ - templ_masked_;
|
|
Scalar temp_s = sum(temp.mul(temp));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
// Normalization
|
|
temp_s = sum(templ_masked_.mul(templ_masked_));
|
|
double norm = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
temp_s = sum(img_roi_masked_.mul(img_roi_masked_));
|
|
norm *= temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
norm = sqrt(norm);
|
|
val /= norm;
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplCCORR)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_CCORR, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Scalar temp_s = sum(templ_masked_.mul(img_roi_masked_));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplCCORR_NORMED)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_CCORR_NORMED, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Scalar temp_s = sum(templ_masked_.mul(img_roi_masked_));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
// Normalization
|
|
temp_s = sum(templ_masked_.mul(templ_masked_));
|
|
double norm = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
temp_s = sum(img_roi_masked_.mul(img_roi_masked_));
|
|
norm *= temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
norm = sqrt(norm);
|
|
val /= norm;
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplCCOEFF)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_CCOEFF, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Scalar temp_s = sum(mask_);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (temp_s[i] != 0.0)
|
|
temp_s[i] = 1.0 / temp_s[i];
|
|
else
|
|
temp_s[i] = 1.0;
|
|
}
|
|
Mat temp = mask_.clone(); temp = temp_s; // Workaround to multiply Mat by Scalar
|
|
Mat temp2 = mask_.clone(); temp2 = sum(templ_masked_); // Workaround to multiply Mat by Scalar
|
|
Mat templx = templ_masked_ - mask_.mul(temp).mul(temp2);
|
|
temp2 = sum(img_roi_masked_); // Workaround to multiply Mat by Scalar
|
|
Mat imgx = img_roi_masked_ - mask_.mul(temp).mul(temp2);
|
|
temp_s = sum(templx.mul(imgx));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask, CompareNaiveImplCCOEFF_NORMED)
|
|
{
|
|
matchTemplate(img_testtype_, templ_testtype_, result_, cv::TM_CCOEFF_NORMED, mask_testtype_);
|
|
// Naive implementation for one point
|
|
Scalar temp_s = sum(mask_);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (temp_s[i] != 0.0)
|
|
temp_s[i] = 1.0 / temp_s[i];
|
|
else
|
|
temp_s[i] = 1.0;
|
|
}
|
|
Mat temp = mask_.clone(); temp = temp_s; // Workaround to multiply Mat by Scalar
|
|
Mat temp2 = mask_.clone(); temp2 = sum(templ_masked_); // Workaround to multiply Mat by Scalar
|
|
Mat templx = templ_masked_ - mask_.mul(temp).mul(temp2);
|
|
temp2 = sum(img_roi_masked_); // Workaround to multiply Mat by Scalar
|
|
Mat imgx = img_roi_masked_ - mask_.mul(temp).mul(temp2);
|
|
temp_s = sum(templx.mul(imgx));
|
|
double val = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
|
|
// Normalization
|
|
temp_s = sum(templx.mul(templx));
|
|
double norm = temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
temp_s = sum(imgx.mul(imgx));
|
|
norm *= temp_s[0] + temp_s[1] + temp_s[2] + temp_s[3];
|
|
norm = sqrt(norm);
|
|
val /= norm;
|
|
|
|
EXPECT_NEAR(val, result_.at<float>(TEST_POINT), TEMPL_SIZE.area()*abs(val)*FLT_EPSILON);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(SingleChannelMask, Imgproc_MatchTemplateWithMask,
|
|
Combine(
|
|
Values(CV_32FC1, CV_32FC3, CV_8UC1, CV_8UC3),
|
|
Values(CV_32FC1, CV_8UC1)));
|
|
|
|
INSTANTIATE_TEST_CASE_P(MultiChannelMask, Imgproc_MatchTemplateWithMask,
|
|
Combine(
|
|
Values(CV_32FC3, CV_8UC3),
|
|
Values(CV_32FC3, CV_8UC3)));
|
|
|
|
class Imgproc_MatchTemplateWithMask2 : public TestWithParam<std::tuple<MatType,MatType,
|
|
MatchTemplType>>
|
|
{
|
|
protected:
|
|
// Member functions inherited from ::testing::Test
|
|
void SetUp() override;
|
|
|
|
// Data members
|
|
Mat img_;
|
|
Mat templ_;
|
|
Mat mask_;
|
|
Mat result_withoutmask_;
|
|
Mat result_withmask_;
|
|
|
|
// Constants
|
|
static const Size IMG_SIZE;
|
|
static const Size TEMPL_SIZE;
|
|
};
|
|
|
|
// Arbitraryly chosen test constants
|
|
const Size Imgproc_MatchTemplateWithMask2::IMG_SIZE(160, 100);
|
|
const Size Imgproc_MatchTemplateWithMask2::TEMPL_SIZE(21, 13);
|
|
|
|
void Imgproc_MatchTemplateWithMask2::SetUp()
|
|
{
|
|
int type = std::get<0>(GetParam());
|
|
int type_mask = std::get<1>(GetParam());
|
|
|
|
img_.create(IMG_SIZE, type);
|
|
templ_.create(TEMPL_SIZE, type);
|
|
mask_.create(TEMPL_SIZE, type_mask);
|
|
|
|
randu(img_, 0, 100);
|
|
randu(templ_, 0, 100);
|
|
|
|
if (CV_MAT_DEPTH(type_mask) == CV_8U)
|
|
{
|
|
// CV_8U implies binary mask, so all nonzero values should work
|
|
randu(mask_, 1, 255);
|
|
}
|
|
else
|
|
{
|
|
mask_ = Scalar(1, 1, 1, 1);
|
|
}
|
|
}
|
|
|
|
TEST_P(Imgproc_MatchTemplateWithMask2, CompareWithAndWithoutMask)
|
|
{
|
|
int method = std::get<2>(GetParam());
|
|
|
|
matchTemplate(img_, templ_, result_withmask_, method, mask_);
|
|
matchTemplate(img_, templ_, result_withoutmask_, method);
|
|
|
|
// Get maximum result for relative error calculation
|
|
double min_val, max_val;
|
|
minMaxLoc(abs(result_withmask_), &min_val, &max_val);
|
|
|
|
// Get maximum of absolute diff for comparison
|
|
double mindiff, maxdiff;
|
|
minMaxLoc(abs(result_withmask_ - result_withoutmask_), &mindiff, &maxdiff);
|
|
|
|
EXPECT_LT(maxdiff, max_val*TEMPL_SIZE.area()*FLT_EPSILON);
|
|
}
|
|
|
|
|
|
INSTANTIATE_TEST_CASE_P(SingleChannelMask, Imgproc_MatchTemplateWithMask2,
|
|
Combine(
|
|
Values(CV_32FC1, CV_32FC3, CV_8UC1, CV_8UC3),
|
|
Values(CV_32FC1, CV_8UC1),
|
|
Values(cv::TM_SQDIFF, cv::TM_SQDIFF_NORMED, cv::TM_CCORR, cv::TM_CCORR_NORMED,
|
|
cv::TM_CCOEFF, cv::TM_CCOEFF_NORMED)));
|
|
|
|
INSTANTIATE_TEST_CASE_P(MultiChannelMask, Imgproc_MatchTemplateWithMask2,
|
|
Combine(
|
|
Values(CV_32FC3, CV_8UC3),
|
|
Values(CV_32FC3, CV_8UC3),
|
|
Values(cv::TM_SQDIFF, cv::TM_SQDIFF_NORMED, cv::TM_CCORR, cv::TM_CCORR_NORMED,
|
|
cv::TM_CCOEFF, cv::TM_CCOEFF_NORMED)));
|
|
|
|
TEST(Imgproc_MatchTemplateWithMask, bug_26389) {
|
|
const Mat image = Mat::ones(Size(10, 10), CV_8UC1);
|
|
const Mat templ = Mat::ones(Size(10, 7), CV_8UC1);
|
|
const Mat mask = Mat::ones(Size(10, 7), CV_8UC1);
|
|
|
|
for (const int method : {TM_CCOEFF, TM_CCOEFF_NORMED})
|
|
{
|
|
Mat result;
|
|
matchTemplate(image, templ, result, method, mask);
|
|
}
|
|
}
|
|
|
|
typedef testing::TestWithParam<tuple<perf::MatType, int>> Imgproc_MatchTemplateBoolMask;
|
|
|
|
TEST_P(Imgproc_MatchTemplateBoolMask, matches_uchar_mask)
|
|
{
|
|
const int type = get<0>(GetParam());
|
|
const int method = get<1>(GetParam());
|
|
|
|
Mat image(20, 20, type);
|
|
Mat templ(7, 7, type);
|
|
cv::randu(image, Scalar::all(0), Scalar::all(255));
|
|
cv::randu(templ, Scalar::all(0), Scalar::all(255));
|
|
|
|
Mat_<bool> mask_bool(templ.size(), false);
|
|
mask_bool(Rect(1, 1, 5, 5)) = true;
|
|
ASSERT_EQ(mask_bool.depth(), CV_Bool);
|
|
|
|
Mat mask_uchar(templ.size(), CV_8UC1, Scalar::all(0));
|
|
mask_uchar(Rect(1, 1, 5, 5)) = 255;
|
|
|
|
Mat result_bool, result_uchar;
|
|
ASSERT_NO_THROW(matchTemplate(image, templ, result_bool, method, mask_bool));
|
|
matchTemplate(image, templ, result_uchar, method, mask_uchar);
|
|
|
|
ASSERT_EQ(result_bool.size(), result_uchar.size());
|
|
ASSERT_EQ(result_bool.type(), result_uchar.type());
|
|
EXPECT_LE(cv::norm(result_bool, result_uchar, NORM_INF), 1e-5);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgproc_MatchTemplateBoolMask,
|
|
Combine(
|
|
Values(CV_8UC1, CV_8UC3, CV_32FC1),
|
|
Values(cv::TM_SQDIFF, cv::TM_SQDIFF_NORMED, cv::TM_CCORR, cv::TM_CCORR_NORMED,
|
|
cv::TM_CCOEFF, cv::TM_CCOEFF_NORMED)));
|
|
|
|
}} // namespace
|