From fe5ab362453844a77dafd678da4c3b3058ce2215 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:12:16 -0700 Subject: [PATCH 1/2] imgproc: fix IPP HAL warpAffine INTER_NEAREST not bit-exact with native The IPP HAL (hal/ipp/src/warp_ipp.cpp) routed cv::warpAffine with INTER_NEAREST to iwiWarpAffine for CV_16S{C1,C3,C4}, CV_64F{C1,C3,C4} and CV_16UC4 via the non-enforced (default) impl[] dispatch table, whose comment promises results "strictly aligned to OpenCV implementation". That contract was broken: IPP rounds source coordinates at the half-pixel boundary differently from the native fixed-point kernel (warpAffineBlocklineNN, 10-bit AB_BITS), so under rotation or fractional translation about 0.05-0.07% of destination pixels resolve to a different source pixel. Because INTER_NEAREST does no blending, those pixels take entirely different values, so warping identical data as e.g. CV_16S (IPP path) versus CV_16U (native path) produced non-identical output, and x86_64 (IPP) silently diverged from ARM (no IPP). This is the same "Different results" behavior for which IPP warpAffine was disabled from ~2017 through 4.11. Zero the INTER_NEAREST column of the non-enforced dispatch table for the affected types so the dispatch guard falls back to the native kernel, restoring the "strictly aligned" contract. The LINEAR and CUBIC columns for these rows were already 0, so IPP was only ever used for NEAREST here. The IPP_CALLS_ENFORCED table is left untouched so the opt-in performance-benchmarking build still exercises IPP. Adds a regression test (Imgproc_Warp.regression_29279) that warps a gradient image with INTER_NEAREST as each affected type and asserts bit-exact equality with a native (CV_32F) reference across several rotation angles and a fractional translation. Fixes #29279 --- hal/ipp/src/warp_ipp.cpp | 12 +++-- modules/imgproc/test/test_imgwarp.cpp | 69 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/hal/ipp/src/warp_ipp.cpp b/hal/ipp/src/warp_ipp.cpp index 1eea3228ff..b00d75daaa 100644 --- a/hal/ipp/src/warp_ipp.cpp +++ b/hal/ipp/src/warp_ipp.cpp @@ -99,14 +99,20 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //32F {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}}; //64F #else // IPP_CALLS_ENFORCED is not defined, results are strictly aligned to OpenCV implementation + // The INTER_NEAREST column (interpolation index 0) is kept 0 for every type so that + // nearest-neighbor warps always fall back to the native fixed-point kernel. IPP's + // iwiWarpAffine rounds source coordinates at the half-pixel boundary differently from + // the native path (warpAffineBlocklineNN, 10-bit AB_BITS), so its nearest-neighbor + // output is not bit-exact with the native implementation for CV_16S{C1,C3,C4}, + // CV_64F{C1,C3,C4} and CV_16UC4. See https://github.com/opencv/opencv/issues/29279 . /* C1 C2 C3 C4 */ char impl[CV_DEPTH_MAX][4][3]={{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8U {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8S - {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {1, 0, 0}}, //16U - {{1, 0, 0}, {0, 0, 0}, {1, 0, 0}, {1, 0, 0}}, //16S + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //16U + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //16S {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //32S {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //32F - {{1, 0, 0}, {0, 0, 0}, {1, 0, 0}, {1, 0, 0}}}; //64F + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}}; //64F #endif if(impl[CV_TYPE(src_type)][CV_MAT_CN(src_type)-1][interpolation] == 0) diff --git a/modules/imgproc/test/test_imgwarp.cpp b/modules/imgproc/test/test_imgwarp.cpp index 382b74de4d..99f0b2dcc8 100644 --- a/modules/imgproc/test/test_imgwarp.cpp +++ b/modules/imgproc/test/test_imgwarp.cpp @@ -1588,6 +1588,75 @@ TEST(Imgproc_Warp, regression_28554) } +TEST(Imgproc_Warp, regression_29279) +{ + // https://github.com/opencv/opencv/issues/29279 + // warpAffine + INTER_NEAREST must be bit-exact with the native fixed-point kernel for + // every type. The IPP HAL used to route CV_16S{C1,C3,C4}, CV_64F{C1,C3,C4} and CV_16UC4 + // nearest-neighbor warps to iwiWarpAffine, whose source-coordinate rounding differs from + // the native path, so warping identical data as one of those types silently diverged + // from a native warp of the same data (about 0.05-0.07% of pixels under rotation / + // fractional translation). CV_32F is never dispatched to IPP for INTER_NEAREST, so it is + // used here as the native reference. + const Size srcSize(800, 600); + const Size dstSize(900, 900); + + // Gradient values in [1, ~30700]: representable exactly in CV_16S, CV_16U, CV_32F and + // CV_64F, so any change in the selected source pixel changes the resulting value. + Mat base(srcSize, CV_32SC1); + for (int y = 0; y < base.rows; y++) + for (int x = 0; x < base.cols; x++) + base.at(y, x) = 1 + ((x * 7 + y * 13) % 30000); + + const int channels[] = { 1, 3, 4 }; + const int nearestDepth[] = { CV_16S, CV_16U, CV_64F }; // formerly IPP-routed for NEAREST + const double angles[] = { 0.0, 13.7, 45.0, 90.0 }; // 0 and 90 are no-ops, guard regressions + + for (size_t ci = 0; ci < sizeof(channels) / sizeof(channels[0]); ci++) + { + const int cn = channels[ci]; + + std::vector planes(cn); + for (int c = 0; c < cn; c++) + planes[c] = base + c * 101; // distinct per-channel values, still in range + Mat srcInt; + merge(planes, srcInt); // CV_32SC(cn) + + Mat srcRef; + srcInt.convertTo(srcRef, CV_MAKETYPE(CV_32F, cn)); + + for (size_t ai = 0; ai < sizeof(angles) / sizeof(angles[0]); ai++) + { + const double angle = angles[ai]; + + // Same convention as the issue reproducer: rotation about (400, 300) plus a + // fractional translation. + Mat M = getRotationMatrix2D(Point2f(400.f, 300.f), angle, 1.0); + M.at(0, 2) += 37.3; + M.at(1, 2) += -12.8; + + Mat dstRef; + warpAffine(srcRef, dstRef, M, dstSize, INTER_NEAREST, BORDER_CONSTANT, Scalar::all(0)); + + for (size_t di = 0; di < sizeof(nearestDepth) / sizeof(nearestDepth[0]); di++) + { + const int type = CV_MAKETYPE(nearestDepth[di], cn); + Mat src; + srcInt.convertTo(src, type); + Mat dst; + warpAffine(src, dst, M, dstSize, INTER_NEAREST, BORDER_CONSTANT, Scalar::all(0)); + + Mat dstF; + dst.convertTo(dstF, CV_MAKETYPE(CV_32F, cn)); + EXPECT_EQ(0.0, cvtest::norm(dstRef, dstF, NORM_INF)) + << "INTER_NEAREST warp not bit-exact with native: depth=" << nearestDepth[di] + << " cn=" << cn << " angle=" << angle; + } + } + } +} + + TEST(Imgproc_GetAffineTransform, singularity) { Point2f A_sample[3]; From 92d1ebe1a012172421f94b09ef69983219d92ee0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Fri, 31 Jul 2026 08:02:50 -0700 Subject: [PATCH 2/2] docs: trim the NEAREST-fallback comments per review --- hal/ipp/src/warp_ipp.cpp | 9 +++------ modules/imgproc/test/test_imgwarp.cpp | 11 +++-------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/hal/ipp/src/warp_ipp.cpp b/hal/ipp/src/warp_ipp.cpp index b00d75daaa..c437fb94ba 100644 --- a/hal/ipp/src/warp_ipp.cpp +++ b/hal/ipp/src/warp_ipp.cpp @@ -99,12 +99,9 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //32F {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}}; //64F #else // IPP_CALLS_ENFORCED is not defined, results are strictly aligned to OpenCV implementation - // The INTER_NEAREST column (interpolation index 0) is kept 0 for every type so that - // nearest-neighbor warps always fall back to the native fixed-point kernel. IPP's - // iwiWarpAffine rounds source coordinates at the half-pixel boundary differently from - // the native path (warpAffineBlocklineNN, 10-bit AB_BITS), so its nearest-neighbor - // output is not bit-exact with the native implementation for CV_16S{C1,C3,C4}, - // CV_64F{C1,C3,C4} and CV_16UC4. See https://github.com/opencv/opencv/issues/29279 . + // NEAREST column kept 0 for all types: IPP's iwiWarpAffine rounds source coords at the + // half-pixel boundary unlike the native kernel (warpAffineBlocklineNN), so its NN output + // is not bit-exact. Forces fallback to native. See https://github.com/opencv/opencv/issues/29279 /* C1 C2 C3 C4 */ char impl[CV_DEPTH_MAX][4][3]={{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8U {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8S diff --git a/modules/imgproc/test/test_imgwarp.cpp b/modules/imgproc/test/test_imgwarp.cpp index 99f0b2dcc8..9e5e2b3448 100644 --- a/modules/imgproc/test/test_imgwarp.cpp +++ b/modules/imgproc/test/test_imgwarp.cpp @@ -1590,14 +1590,9 @@ TEST(Imgproc_Warp, regression_28554) TEST(Imgproc_Warp, regression_29279) { - // https://github.com/opencv/opencv/issues/29279 - // warpAffine + INTER_NEAREST must be bit-exact with the native fixed-point kernel for - // every type. The IPP HAL used to route CV_16S{C1,C3,C4}, CV_64F{C1,C3,C4} and CV_16UC4 - // nearest-neighbor warps to iwiWarpAffine, whose source-coordinate rounding differs from - // the native path, so warping identical data as one of those types silently diverged - // from a native warp of the same data (about 0.05-0.07% of pixels under rotation / - // fractional translation). CV_32F is never dispatched to IPP for INTER_NEAREST, so it is - // used here as the native reference. + // IPP's iwiWarpAffine rounds source coords at the half-pixel boundary unlike the native + // kernel (warpAffineBlocklineNN), so its NN output is not bit-exact. + // See https://github.com/opencv/opencv/issues/29279 const Size srcSize(800, 600); const Size dstSize(900, 900);