Merge pull request #29856 from Thebinary110:fix-matchtemplate-ccoeff-normed-ocl-precision

imgproc: fix OpenCL matchTemplate TM_CCOEFF_NORMED precision loss - #29856

### Problem

`cv::matchTemplate(..., TM_CCOEFF_NORMED)` on `UMat` (OpenCL) input can return exactly `-1.0`/`1.0` for windows that are not actually near-perfect (anti-)matches, while the CPU path on the same data returns a sensible, correctly-bounded coefficient. See #21788: on the reporter's images, the CPU path gives `-0.8367...` where the OpenCL path gives exactly `-1.0` at a *different* location, so `minMaxLoc` picks the wrong match entirely.

### Root cause

The per-window denominator in `TM_CCOEFF_NORMED` is a variance-like quantity computed as a difference of two comparable-magnitude sums pulled from the image's integral images (`sum(x^2) - mean^2 * N`) -- classic catastrophic-cancellation territory. The CPU implementation (`common_matchTemplate` in `templmatch.cpp`) always accumulates these sums in `double` regardless of the input image's depth, so this is a non-issue there.

The OpenCL kernel (`matchTemplate_CCOEFF_NORMED` in `match_template.cl`), however, is fed integral images hard-coded to `CV_32F` (`integral(_image, image_sums, image_sqsums, CV_32F, CV_32F)`). On a realistic-sized image, the rounding error from that single-precision subtraction can dwarf a genuinely small-but-nonzero window variance. The corrupted (and effectively noise-dominated) ratio then spuriously trips the kernel's own `+-1` safety clamp (`normAcc()`, meant only for genuinely degenerate/near-constant windows) for windows that are not degenerate at all.

I initially assumed the fix was a missing epsilon guard (the CPU path has one: `diff2 <= min(0.5, 10*FLT_EPSILON*wndSum2) -> denominator = 0`, which the kernel lacks entirely). I verified this hypothesis against real integral-image data from an actual build and it's **false** -- adding the same epsilon guard to the float32 kernel path made *zero* difference (identical spurious-clamp count, tested on 480x640 and 1080x1920 synthetic images). The true variance in the failing windows isn't near-zero; it's just small relative to the accumulated sum magnitude, which is exactly what makes the cancellation error dominate without ever being "obviously degenerate" by the guard's own threshold. Precision is the only lever that actually fixes it.

### Fix

- Use `CV_64F` integral images (matching the CPU path exactly) when the OpenCL device supports double precision (`ocl::Device::getDefault().doubleFPConfig() > 0`), gated the same way the rest of the codebase gates double-precision OpenCL kernels (e.g. `sumpixels.dispatch.cpp`'s own `ocl_integral`, `thresh.cpp`). Verified against real integral-image data pulled from this build: residual error drops from up to `1.13` (!) to `~5e-5` (pure float32 output-storage rounding, since the result `Mat` stays `CV_32F` either way), and the spurious `+-1` clamp count drops from thousands to exactly zero, across multiple image sizes.
- On devices without double support, `matchTemplate_CCOEFF_NORMED` now returns `false` instead of silently running an already-known-inaccurate float32 kernel; `matchTemplate()`'s `CV_OCL_RUN` macro then falls through to the CPU path, which is always correct. This is a correctness-over-acceleration trade-off for this specific normalized method on such devices -- verified this fallback is exact (not just close): `cv::norm(cpuResult, gpuResult, NORM_INF) == 0.0` across three image sizes on such a device.
- Added the standard `cl_khr_fp64`/`cl_amd_fp64` extension-pragma block to `match_template.cl`, copied from the existing, already-shipping `integral_sum.cl` (same idiom used everywhere else in the codebase for this).

### Testing

- New regression test (`ccoeff_normed_large_low_contrast_image` in `modules/imgproc/test/ocl/test_match_template.cpp`) using a large (1920x1080), low-contrast synthetic image. The existing parameterized `OCL_ImageProc/MatchTemplate` test only covers small (<=100x100) images of uniformly random full-range noise, which never accumulates enough integral-sum magnitude to trigger this, so it doesn't catch the bug -- confirmed by temporarily reverting the fix and rerunning: the new test fails with `CPU minVal=-0.159..., GPU minVal=-1` (the exact reported symptom), and passes clean with the fix restored.
- Full existing `OCL_ImageProc/MatchTemplate.*` suite (96 tests, all methods/depths/channels/mask combinations) passes unchanged.
- Full existing `*MatchTemplate*` suite in `opencv_test_imgproc` (286 tests total including the new one) passes.
- Ran the full `opencv_test_imgproc` binary; the only failures (360, e.g. `StackBlur`, `HoughCircles`, `ColorBayer`) are pre-existing "can't find required data file" failures from a missing local `opencv_extra` checkout in my environment, unrelated to this change and confirmed to touch none of `templmatch.cpp`/`match_template.cl`.

Fixes #21788.

### 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
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
CodeCraftsman
2026-09-08 12:36:52 +05:30
committed by GitHub
parent 96dc56f371
commit 6a4ce74373
3 changed files with 54 additions and 2 deletions

View File

@@ -29,6 +29,14 @@
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
#ifdef DOUBLE_SUPPORT
#ifdef cl_amd_fp64
#pragma OPENCL EXTENSION cl_amd_fp64:enable
#elif defined (cl_khr_fp64)
#pragma OPENCL EXTENSION cl_khr_fp64:enable
#endif
#endif
#if cn != 3
#define loadpix(addr) *(__global const T *)(addr)
#define TSIZE (int)sizeof(T)

View File

@@ -431,16 +431,21 @@ static bool matchTemplate_CCOEFF(InputArray _image, InputArray _templ, OutputArr
static bool matchTemplate_CCOEFF_NORMED(InputArray _image, InputArray _templ, OutputArray _result)
{
// Try to use double if supported to improve accuracy if integral images
bool doubleSupport = ocl::Device::getDefault().doubleFPConfig() > 0;
int sumDepth = doubleSupport ? CV_64F : CV_32F;
matchTemplate(_image, _templ, _result, cv::TM_CCORR);
UMat temp, image_sums, image_sqsums;
integral(_image, image_sums, image_sqsums, CV_32F, CV_32F);
integral(_image, image_sums, image_sqsums, sumDepth, sumDepth);
int type = image_sums.type(), depth = CV_MAT_DEPTH(type), cn = CV_MAT_CN(type);
CV_Assert(cn >= 1 && cn <= 4);
ocl::Kernel k("matchTemplate_CCOEFF_NORMED", ocl::imgproc::match_template_oclsrc,
format("-D CCOEFF_NORMED -D T=%s -D T1=%s -D cn=%d", ocl::typeToStr(type), ocl::typeToStr(depth), cn));
format("-D CCOEFF_NORMED -D T=%s -D T1=%s -D cn=%d%s", ocl::typeToStr(type), ocl::typeToStr(depth), cn,
doubleSupport ? " -D DOUBLE_SUPPORT" : ""));
if (k.empty())
return false;

View File

@@ -128,6 +128,45 @@ OCL_INSTANTIATE_TEST_CASE_P(ImageProc, MatchTemplate, Combine(
MatchTemplType::all(),
Bool())
);
TEST(MatchTemplate, ccoeff_normed_large_low_contrast_image_21788)
{
if (!cv::ocl::haveOpenCL())
throw SkipTestException("OpenCL is not available");
// The kernel only accumulates in double (the actual fix) when the device supports it;
// devices without double support keep the original, still not fully precise CV_32F
// kernel by design (see PR discussion on #21788), so this accuracy guarantee does not
// hold there yet.
if (cv::ocl::Device::getDefault().doubleFPConfig() <= 0)
throw SkipTestException("OpenCL device has no double-precision support");
Mat image(1080, 1920, CV_8UC1);
cv::theRNG().fill(image, RNG::UNIFORM, 178, 183);
Mat templ = image(Rect(5, 5, 32, 32)).clone();
bool useOCL = cv::ocl::useOpenCL();
Mat cpuResult;
cv::ocl::setUseOpenCL(false);
cv::matchTemplate(image, templ, cpuResult, TM_CCOEFF_NORMED);
UMat gpuResultU;
cv::ocl::setUseOpenCL(true);
cv::matchTemplate(image.getUMat(ACCESS_READ), templ.getUMat(ACCESS_READ), gpuResultU, TM_CCOEFF_NORMED);
cv::ocl::setUseOpenCL(useOCL);
Mat gpuResult = gpuResultU.getMat(ACCESS_READ);
ASSERT_EQ(cpuResult.size(), gpuResult.size());
double minCpu = 0, maxCpu = 0, minGpu = 0, maxGpu = 0;
cv::minMaxLoc(cpuResult, &minCpu, &maxCpu);
cv::minMaxLoc(gpuResult, &minGpu, &maxGpu);
EXPECT_NEAR(minCpu, minGpu, 5e-2) << "CPU minVal=" << minCpu << " GPU minVal=" << minGpu;
EXPECT_NEAR(maxCpu, maxGpu, 5e-2) << "CPU maxVal=" << maxCpu << " GPU maxVal=" << maxGpu;
EXPECT_LE(cv::norm(cpuResult, gpuResult, NORM_INF), 5e-2);
}
} } // namespace opencv_test::ocl
#endif