From d19c3437045ecc654de08a18417488a868fe0122 Mon Sep 17 00:00:00 2001 From: Arne Baeyens Date: Sun, 23 Aug 2026 12:08:44 +0200 Subject: [PATCH] imgproc restrict warp walk to src footprint for BORDER_TRANSPARENT warpAffine and warPerspective walked the full dst image even when the src image got projected on only a small part of dst; these changes limit the dst image walking to the area the src image gets projected to, thus delivering a considerable (>10x for TestWarpAffineSmallSrc) speedup. Behavior for the other border types was left as-is. Signed-off-by: Arne Baeyens --- modules/imgproc/src/imgwarp.cpp | 90 ++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/modules/imgproc/src/imgwarp.cpp b/modules/imgproc/src/imgwarp.cpp index 17f77c1599..d5fd16e2f6 100644 --- a/modules/imgproc/src/imgwarp.cpp +++ b/modules/imgproc/src/imgwarp.cpp @@ -2168,15 +2168,57 @@ void cv::convertMaps( InputArray _map1, InputArray _map2, namespace cv { +static Rect warpSourceFootprintRoi(Size srcSize, Size dstSize, const double* M, bool isPerspective) +{ + const Rect fullRoi(0, 0, dstSize.width, dstSize.height); + + // Invert M because it maps destination to source while we need the reverse + const Matx33d fullM = isPerspective + ? Matx33d(M[0], M[1], M[2], M[3], M[4], M[5], M[6], M[7], M[8]) + : Matx33d(M[0], M[1], M[2], M[3], M[4], M[5], 0.0, 0.0, 1.0); + bool invertible = false; + const Matx33d invM = fullM.inv(DECOMP_LU, &invertible); + if (!invertible) + return fullRoi; + + const int INTERPOLATION_PADDING = 4; // assume worst case: INTER_LANCZOS4 + const double x0 = -INTERPOLATION_PADDING, x1 = srcSize.width + INTERPOLATION_PADDING; + const double y0 = -INTERPOLATION_PADDING, y1 = srcSize.height + INTERPOLATION_PADDING; + const Point2d corners[4] = { {x0, y0}, {x1, y0}, {x1, y1}, {x0, y1} }; + double minX = DBL_MAX, maxX = -DBL_MAX, minY = DBL_MAX, maxY = -DBL_MAX; + int zsign = 0; + for (const Point2d& c : corners) + { + const double z = invM(2, 0) * c.x + invM(2, 1) * c.y + invM(2, 2); + if (std::abs(z) < 1e-9) + return fullRoi; // z small => xy range close to inf + const int s = z > 0 ? 1 : -1; + if (zsign == 0) + zsign = s; + else if (s != zsign) + return fullRoi; // sign changed => corners front and rear + const double x = (invM(0, 0) * c.x + invM(0, 1) * c.y + invM(0, 2)) / z; + const double y = (invM(1, 0) * c.x + invM(1, 1) * c.y + invM(1, 2)) / z; + minX = std::min(minX, x); maxX = std::max(maxX, x); + minY = std::min(minY, y); maxY = std::max(maxY, y); + } + const int x0i = cvFloor(std::min(std::max(minX, -1.0), dstSize.width + 1.0)); + const int x1i = cvCeil (std::min(std::max(maxX, -1.0), dstSize.width + 1.0)); + const int y0i = cvFloor(std::min(std::max(minY, -1.0), dstSize.height + 1.0)); + const int y1i = cvCeil (std::min(std::max(maxY, -1.0), dstSize.height + 1.0)); + return Rect(x0i, y0i, x1i - x0i, y1i - y0i) & fullRoi; +} + class WarpAffineInvoker : public ParallelLoopBody { public: WarpAffineInvoker(const Mat &_src, Mat &_dst, int _interpolation, int _borderType, - const Scalar &_borderValue, int *_adelta, int *_bdelta, const double *_M) : + const Scalar &_borderValue, int *_adelta, int *_bdelta, const double *_M, + const Range &_colRange) : ParallelLoopBody(), src(_src), dst(_dst), interpolation(_interpolation), borderType(_borderType), borderValue(_borderValue), adelta(_adelta), bdelta(_bdelta), - M(_M) + M(_M), colRange(_colRange) { } @@ -2195,9 +2237,9 @@ public: for( y = range.start; y < range.end; y += bh0 ) { - for( x = 0; x < dst.cols; x += bw0 ) + for( x = colRange.start; x < colRange.end; x += bw0 ) { - int bw = std::min( bw0, dst.cols - x); + int bw = std::min( bw0, colRange.end - x); int bh = std::min( bh0, range.end - y); Mat _XY(bh, bw, CV_16SC2, XY); @@ -2233,6 +2275,7 @@ private: Scalar borderValue; int *adelta, *bdelta; const double *M; + Range colRange; }; #ifdef HAVE_OPENCL @@ -2448,11 +2491,20 @@ void warpAffine(int src_type, bdelta[x] = saturate_cast(M[3]*x*AB_SCALE); } - Range range(0, dst.rows); + Range rowRange(0, dst.rows); + Range colRange(0, dst.cols); + double nbPixels = (double)dst.total(); + if ((borderType & ~BORDER_ISOLATED) == BORDER_TRANSPARENT) + { + const Rect dstRoi = warpSourceFootprintRoi(src.size(), dst.size(), M, false); + rowRange = Range(dstRoi.y, dstRoi.y + dstRoi.height); + colRange = Range(dstRoi.x, dstRoi.x + dstRoi.width); + nbPixels = dstRoi.area(); + } WarpAffineInvoker invoker(src, dst, interpolation, borderType, Scalar(borderValue[0], borderValue[1], borderValue[2], borderValue[3]), - adelta, bdelta, M); - parallel_for_(range, invoker, dst.total()/(double)(1<<16)); + adelta, bdelta, M, colRange); + parallel_for_(rowRange, invoker, nbPixels/(1<<16)); } CV_DISABLE_UBSAN @@ -2854,9 +2906,9 @@ class WarpPerspectiveInvoker : { public: WarpPerspectiveInvoker(const Mat &_src, Mat &_dst, const double *_M, int _interpolation, - int _borderType, const Scalar &_borderValue) : + int _borderType, const Scalar &_borderValue, const Range &_colRange) : ParallelLoopBody(), src(_src), dst(_dst), M(_M), interpolation(_interpolation), - borderType(_borderType), borderValue(_borderValue) + borderType(_borderType), borderValue(_borderValue), colRange(_colRange) { #if defined(_MSC_VER) && _MSC_VER == 1800 /* MSVS 2013 */ && CV_AVX // details: https://github.com/opencv/opencv/issues/11026 @@ -2877,9 +2929,9 @@ public: for( y = range.start; y < range.end; y += bh0 ) { - for( x = 0; x < width; x += bw0 ) + for( x = colRange.start; x < colRange.end; x += bw0 ) { - int bw = std::min( bw0, width - x); + int bw = std::min( bw0, colRange.end - x); int bh = std::min( bh0, range.end - y); // height Mat _XY(bh, bw, CV_16SC2, XY); @@ -2915,6 +2967,7 @@ private: const double* M; int interpolation, borderType; Scalar borderValue; + Range colRange; }; @@ -2929,9 +2982,18 @@ void warpPerspective(int src_type, Mat src(Size(src_width, src_height), src_type, const_cast(src_data), src_step); Mat dst(Size(dst_width, dst_height), src_type, dst_data, dst_step); - Range range(0, dst.rows); - WarpPerspectiveInvoker invoker(src, dst, M, interpolation, borderType, Scalar(borderValue[0], borderValue[1], borderValue[2], borderValue[3])); - parallel_for_(range, invoker, dst.total()/(double)(1<<16)); + Range rowRange(0, dst.rows); + Range colRange(0, dst.cols); + double nbPixels = (double)dst.total(); + if ((borderType & ~BORDER_ISOLATED) == BORDER_TRANSPARENT) + { + const Rect dstRoi = warpSourceFootprintRoi(src.size(), dst.size(), M, true); + rowRange = Range(dstRoi.y, dstRoi.y + dstRoi.height); + colRange = Range(dstRoi.x, dstRoi.x + dstRoi.width); + nbPixels = dstRoi.area(); + } + WarpPerspectiveInvoker invoker(src, dst, M, interpolation, borderType, Scalar(borderValue[0], borderValue[1], borderValue[2], borderValue[3]), colRange); + parallel_for_(rowRange, invoker, nbPixels/(1<<16)); } void warpPerspectiveBlocklineNN(const double *M, short* xy, double X0, double Y0, double W0, int bw)