From 9a78d45a2f74b7d2544a97d7ca6435f22aa40345 Mon Sep 17 00:00:00 2001 From: Lurie97 <109333972+Lurie97@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:10:00 +0800 Subject: [PATCH] Merge pull request #30022 from Lurie97:carotene-resize-left-edge-clamp carotene: clamp left-edge weight in downsample_bilinear_8uc1 - #30022 For the leftmost pixel the sample coordinate c = 0.5*wr - 0.5 can be negative, so the left-column weight overflows full-scale and interpolation extrapolates beyond the source, corrupting the first output column. Clamp the weight to full-scale when c < 0, matching the scalar/OpenCL edge handling. Affects both the 16- and 8-wide column loops. ### 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 - [ ] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake --- hal/carotene/src/resize.cpp | 14 +++++++ modules/imgproc/test/test_imgwarp.cpp | 56 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/hal/carotene/src/resize.cpp b/hal/carotene/src/resize.cpp index aa5b756c75..35dad04439 100644 --- a/hal/carotene/src/resize.cpp +++ b/hal/carotene/src/resize.cpp @@ -1607,6 +1607,15 @@ void downsample_bilinear_8uc1(const Size2D &ssize, const Size2D &dsize, cwi[k] = (short)floorf((col2[k] - c) * (1 << SHIFT_BITS) + 0.5f); + // When the sample point falls left of the first source column + // (c < 0, happens for the leftmost destination pixel when + // upscaling horizontally, wr < 1), the left-column weight cwi + // overflows past the full-scale value (1<= (ptrdiff_t)ssize.width) col2[k] = ssize.width-1; } @@ -1770,6 +1779,11 @@ downsample_bilinear_8uc1_col_loop8: cwi[k] = (s16)floorf((col2[k] - c) * (1 << SHIFT_BITS) + 0.5f); + // See the 16-wide loop above: clamp the left-column weight when the + // sample point is left of the first source column (horizontal + // upscaling, wr < 1) to avoid extrapolation on the leftmost pixel. + if(c < 0.f) cwi[k] = (1 << SHIFT_BITS); + if(col1[k] < 0) col1[k] = 0; if(col2[k] >= (ptrdiff_t)ssize.width) col2[k] = (ptrdiff_t)ssize.width-1; } diff --git a/modules/imgproc/test/test_imgwarp.cpp b/modules/imgproc/test/test_imgwarp.cpp index d92e4c8362..6be12f8c3b 100644 --- a/modules/imgproc/test/test_imgwarp.cpp +++ b/modules/imgproc/test/test_imgwarp.cpp @@ -1545,6 +1545,62 @@ TEST(Resize, nearest_regression_15075) EXPECT_EQ(C, cvtest::norm(dst, NORM_L1)) << src.size; } +// Horizontal upscaling of 8UC1 (fx > 1) exercises the carotene NEON path +// (downsample_bilinear_8uc1, wr < 1) on ARM. The leftmost output column used +// to extrapolate because the negative sample coordinate produced an +// out-of-range interpolation weight. Compare against a double-precision +// bilinear reference; the first column must match like any other. +// Uses sizes where sw*fx is integral so only the horizontal-upscale path is +// under test (no dsize-rounding effects). On non-ARM builds this simply +// verifies the scalar path stays correct. +TEST(Resize, horizontal_upscale_left_edge_regression) +{ + RNG& rng = theRNG(); + const struct { int sw, sh; double fx, fy; } cases[] = { + { 100, 100, 1.5, 0.5 }, + { 200, 100, 1.5, 0.5 }, + { 64, 64, 1.5, 1.0 }, + { 128, 96, 2.0, 0.5 }, + }; + + for (const auto& c : cases) + { + SCOPED_TRACE(cv::format("%dx%d fx=%.1f fy=%.1f", c.sw, c.sh, c.fx, c.fy)); + Mat src(c.sh, c.sw, CV_8UC1); + rng.fill(src, RNG::UNIFORM, 0, 256); + + Mat dst; + cv::resize(src, dst, Size(), c.fx, c.fy, INTER_LINEAR); + + const double rx = (double)dst.cols / c.sw, ry = (double)dst.rows / c.sh; + double maxdiff = 0; + for (int dy = 0; dy < dst.rows; dy++) + { + double sy = (dy + 0.5) / ry - 0.5; + int y0 = cvFloor(sy); double v = sy - y0; + if (y0 < 0) { y0 = 0; v = 0; } + if (y0 >= c.sh) { y0 = c.sh - 1; v = 0; } + int y1 = std::min(y0 + 1, c.sh - 1); + for (int dx = 0; dx < dst.cols; dx++) + { + double sx = (dx + 0.5) / rx - 0.5; + int x0 = cvFloor(sx); double u = sx - x0; + if (x0 < 0) { x0 = 0; u = 0; } + if (x0 >= c.sw) { x0 = c.sw - 1; u = 0; } + int x1 = std::min(x0 + 1, c.sw - 1); + double ref = (1 - u) * (1 - v) * src.at(y0, x0) + + u * (1 - v) * src.at(y0, x1) + + (1 - u) * v * src.at(y1, x0) + + u * v * src.at(y1, x1); + maxdiff = std::max(maxdiff, std::abs(ref - dst.at(dy, dx))); + } + } + // Allow small fixed-point rounding differences; the pre-fix carotene + // bug produced first-column errors of ~100+. + EXPECT_LE(maxdiff, 3.0); + } +} + TEST(Imgproc_Warp, multichannel) { static const int inter_types[] = {INTER_NEAREST, INTER_AREA, INTER_CUBIC,