Files
opencv-MIRROR/modules/imgproc/test/test_templmatchmask.cpp
Taiwei Zhang 690f3d25c2 Merge pull request #29071 from zitonwei:fix-masked-ccoeff-normed-constant-template
imgproc: avoid NaN in masked TM_CCOEFF_NORMED for constant templates - #29071
    
### 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
- [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

### Summary

Fixes #23257.

This patch handles a degenerate masked `TM_CCOEFF_NORMED` case in `matchTemplate()`. When the template is constant over the effective mask area, the template norm can become zero or NaN, which may propagate NaN/Inf values into the result. The masked path now returns all ones for this case, matching the existing behavior of the unmasked `TM_CCOEFF_NORMED` implementation for constant templates.

### Tests

- `cmake --build build_project4 --target opencv_test_imgproc -j4`
- `./build_project4/bin/opencv_test_imgproc '--gtest_filter=Imgproc_MatchTemplateWithMask.regression_23257_constant_template'`
- `./build_project4/bin/opencv_test_imgproc '--gtest_filter=*MatchTemplate*'`

The MatchTemplate-related test filter ran 147 tests successfully.
2026-08-18 08:42:28 +03:00

332 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::tuple<MatType, MatType, Scalar> ConstantTemplateParams;
typedef testing::TestWithParam<ConstantTemplateParams> ConstantTemplateWithMask;
TEST_P(ConstantTemplateWithMask, regression_23257)
{
int type = std::get<0>(GetParam());
int maskType = std::get<1>(GetParam());
Scalar templValue = std::get<2>(GetParam());
Mat image(Size(16, 16), type);
randu(image, 0, 255);
const Mat templ(Size(3, 3), type, templValue);
Mat mask = Mat::ones(templ.size(), maskType);
mask(Rect(1, 0, 1, 3)) = Scalar::all(0);
Mat result;
matchTemplate(image, templ, result, TM_CCOEFF_NORMED, mask);
for (int y = 0; y < result.rows; ++y)
{
for (int x = 0; x < result.cols; ++x)
{
const float value = result.at<float>(y, x);
EXPECT_FALSE(cvIsNaN(value));
EXPECT_FALSE(cvIsInf(value));
EXPECT_EQ(1.0f, value);
}
}
}
std::vector<ConstantTemplateParams> ctparams = {
{CV_8UC1, CV_8UC1, Scalar::all(1)},
{CV_32FC1, CV_32FC1, Scalar::all(3.5)},
{CV_8UC3, CV_8UC1, Scalar(1, 2, 3)},
{CV_32FC3, CV_32FC3, Scalar(1.5, 2.5, 3.5)}
};
INSTANTIATE_TEST_CASE_P(/**/, ConstantTemplateWithMask, testing::ValuesIn(ctparams));
}} // namespace