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
This commit is contained in:
Lurie97
2026-09-28 14:10:00 +08:00
committed by GitHub
parent a61df2c9ae
commit 9a78d45a2f
2 changed files with 70 additions and 0 deletions

View File

@@ -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<<SHIFT_BITS) and the
// interpolation extrapolates beyond the source range. Clamp the
// weight so the leftmost pixel is taken as-is, matching the
// OpenCV scalar / OpenCL behaviour (if (x<0) x=0, u=0).
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] = 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;
}

View File

@@ -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<uchar>(y0, x0)
+ u * (1 - v) * src.at<uchar>(y0, x1)
+ (1 - u) * v * src.at<uchar>(y1, x0)
+ u * v * src.at<uchar>(y1, x1);
maxdiff = std::max(maxdiff, std::abs(ref - dst.at<uchar>(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,