Files
opencv-MIRROR/modules/imgproc/perf/perf_warp.cpp
Arne Baeyens 755546643a Merge pull request #29779 from abaeyens:abaeyens/speed-up-warp
Speed up imgproc warpAffine and warpPerspective for BORDER_TRANSPARENT - #29779

### Pull Request Readiness Checklist

- [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
- [ ] 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
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake

## Why
I was using `warpPerspective` to draw several source images on a large destination image in mode `BORDER_TRANSPARENT` and ran into `warpPerspective` being surprisingly slow. Upon reading the code, it turns out that `warpPerspective`, as well as `warpAffine`, iterates over all the destination image's pixels even if the source image gets projected to only a small part of the destination image, resulting in considerable overhead for my use case. I believe other users would also benefit from making this case more efficient.

## Changes
d19c343704 calculates the ROI of the source image in the destination image and then limits the destination image walk to that area. Given that the existing tests didn't cover `BORDER_TRANSPARENT`, I extended that in e451f59cda. Next to that, I added a small performance test dedicated to this use case (c4e271c2dc).

## Performance improvement
The following table show the timing difference before and after, generated using the added perf test (source image projects to 64x64, drawn in a 512x512 destination image):

| function | type | interp | base [ms] | opt [ms] | speedup |
| --- | --- | --- | --- | --- | --- |
| warpAffine | 8UC1 | NEAREST | 0.204 | 0.010 | 19.8× |
| warpAffine | 8UC1 | LINEAR | 0.428 | 0.026 | 16.6× |
| warpAffine | 8UC4 | NEAREST | 0.239 | 0.021 | 11.5× |
| warpAffine | 8UC4 | LINEAR | 0.433 | 0.031 | 14.0× |
| warpPerspective | 8UC1 | NEAREST | 0.759 | 0.033 | 23.2× |
| warpPerspective | 8UC1 | LINEAR | 1.125 | 0.067 | 16.8× |
| warpPerspective | 8UC4 | NEAREST | 0.786 | 0.040 | 19.5× |
| warpPerspective | 8UC4 | LINEAR | 1.110 | 0.101 | 11.0× |

In short, a 10 to 20x speedup.

## Notes
- This is my first PR for the OpenCV project, I'm sorry in case I didn't respect all contribution guidelines.
- If relevant, Clause Opus 4.8 was used for exploring the codebase, some code and style suggestions and review.
2026-08-29 11:52:17 +03:00

370 lines
12 KiB
C++

// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "perf_precomp.hpp"
namespace opencv_test {
enum{HALF_SIZE=0, UPSIDE_DOWN, REFLECTION_X, REFLECTION_BOTH};
CV_ENUM(BorderMode, BORDER_CONSTANT, BORDER_REPLICATE)
CV_ENUM(InterType, INTER_NEAREST, INTER_LINEAR)
CV_ENUM(InterTypeExtended, INTER_NEAREST, INTER_LINEAR, WARP_RELATIVE_MAP)
CV_ENUM(RemapMode, HALF_SIZE, UPSIDE_DOWN, REFLECTION_X, REFLECTION_BOTH)
typedef TestBaseWithParam< tuple<MatType, Size, InterType, BorderMode> > TestWarpAffine;
typedef TestBaseWithParam< tuple<Size, InterType, BorderMode, int> > TestWarpPerspective;
typedef TestBaseWithParam< tuple<Size, InterType, BorderMode, MatType> > TestWarpPerspectiveNear_t;
typedef TestBaseWithParam< tuple<MatType, Size, InterTypeExtended, BorderMode, RemapMode> > TestRemap;
typedef TestBaseWithParam< tuple<MatType, Size, InterType> > TestWarpAffineSmallSrc;
typedef TestBaseWithParam< tuple<MatType, Size, InterType> > TestWarpPerspectiveSmallSrc;
void update_map(const Mat& src, Mat& map_x, Mat& map_y, const int remapMode, bool relative = false );
PERF_TEST_P( TestWarpAffine, WarpAffine,
Combine(
Values(CV_8UC1, CV_8UC4),
Values( szVGA, sz720p, sz1080p ),
InterType::all(),
BorderMode::all()
)
)
{
Size sz, szSrc(512, 512);
int borderMode, interType, dataType;
dataType = get<0>(GetParam());
sz = get<1>(GetParam());
interType = get<2>(GetParam());
borderMode = get<3>(GetParam());
Scalar borderColor = Scalar::all(150);
Mat src(szSrc, dataType), dst(sz, dataType);
cvtest::fillGradient(src);
if(borderMode == BORDER_CONSTANT) cvtest::smoothBorder(src, borderColor, 1);
Mat warpMat = getRotationMatrix2D(Point2f(src.cols/2.f, src.rows/2.f), 30., 2.2);
declare.in(src).out(dst);
TEST_CYCLE() warpAffine( src, dst, warpMat, sz, interType, borderMode, borderColor );
SANITY_CHECK(dst, 1);
}
PERF_TEST_P(TestWarpAffine, DISABLED_WarpAffine_ovx,
Combine(
Values(CV_8UC1, CV_8UC4),
Values(szVGA, sz720p, sz1080p),
InterType::all(),
BorderMode::all()
)
)
{
Size sz, szSrc(512, 512);
int borderMode, interType, dataType;
dataType = get<0>(GetParam());
sz = get<1>(GetParam());
interType = get<2>(GetParam());
borderMode = get<3>(GetParam());
Scalar borderColor = Scalar::all(150);
Mat src(szSrc, dataType), dst(sz, dataType);
cvtest::fillGradient(src);
if (borderMode == BORDER_CONSTANT) cvtest::smoothBorder(src, borderColor, 1);
Mat warpMat = getRotationMatrix2D(Point2f(src.cols / 2.f, src.rows / 2.f), 30., 2.2);
declare.in(src).out(dst);
TEST_CYCLE() warpAffine(src, dst, warpMat, sz, interType, borderMode, borderColor);
SANITY_CHECK(dst, 1);
}
PERF_TEST_P( TestWarpPerspective, WarpPerspective,
Combine(
Values( szVGA, sz720p, sz1080p ),
InterType::all(),
BorderMode::all(),
Values(1, 3, 4)
)
)
{
Size sz, szSrc(512, 512);
int borderMode, interType, channels;
sz = get<0>(GetParam());
interType = get<1>(GetParam());
borderMode = get<2>(GetParam());
channels = get<3>(GetParam());
Scalar borderColor = Scalar::all(150);
Mat src(szSrc, CV_8UC(channels)), dst(sz, CV_8UC(channels));
cvtest::fillGradient(src);
if(borderMode == BORDER_CONSTANT) cvtest::smoothBorder(src, borderColor, 1);
Mat rotMat = getRotationMatrix2D(Point2f(src.cols/2.f, src.rows/2.f), 30., 2.2);
Mat warpMat(3, 3, CV_64FC1);
for(int r=0; r<2; r++)
for(int c=0; c<3; c++)
warpMat.at<double>(r, c) = rotMat.at<double>(r, c);
warpMat.at<double>(2, 0) = .3/sz.width;
warpMat.at<double>(2, 1) = .3/sz.height;
warpMat.at<double>(2, 2) = 1;
declare.in(src).out(dst);
TEST_CYCLE() warpPerspective( src, dst, warpMat, sz, interType, borderMode, borderColor );
SANITY_CHECK(dst, 1);
}
PERF_TEST_P(TestWarpPerspective, DISABLED_WarpPerspective_ovx,
Combine(
Values(szVGA, sz720p, sz1080p),
InterType::all(),
BorderMode::all(),
Values(1)
)
)
{
Size sz, szSrc(512, 512);
int borderMode, interType, channels;
sz = get<0>(GetParam());
interType = get<1>(GetParam());
borderMode = get<2>(GetParam());
channels = get<3>(GetParam());
Scalar borderColor = Scalar::all(150);
Mat src(szSrc, CV_8UC(channels)), dst(sz, CV_8UC(channels));
cvtest::fillGradient(src);
if (borderMode == BORDER_CONSTANT) cvtest::smoothBorder(src, borderColor, 1);
Mat rotMat = getRotationMatrix2D(Point2f(src.cols / 2.f, src.rows / 2.f), 30., 2.2);
Mat warpMat(3, 3, CV_64FC1);
for (int r = 0; r<2; r++)
for (int c = 0; c<3; c++)
warpMat.at<double>(r, c) = rotMat.at<double>(r, c);
warpMat.at<double>(2, 0) = .3 / sz.width;
warpMat.at<double>(2, 1) = .3 / sz.height;
warpMat.at<double>(2, 2) = 1;
declare.in(src).out(dst);
TEST_CYCLE() warpPerspective(src, dst, warpMat, sz, interType, borderMode, borderColor);
SANITY_CHECK(dst, 1);
}
PERF_TEST_P( TestWarpPerspectiveNear_t, WarpPerspectiveNear,
Combine(
Values( Size(640,480), Size(1920,1080), Size(2592,1944) ),
InterType::all(),
BorderMode::all(),
Values( CV_8UC1, CV_8UC4 )
)
)
{
Size size;
int borderMode, interType, type;
size = get<0>(GetParam());
interType = get<1>(GetParam());
borderMode = get<2>(GetParam());
type = get<3>(GetParam());
Scalar borderColor = Scalar::all(150);
Mat src(size, type), dst(size, type);
cvtest::fillGradient(src);
if(borderMode == BORDER_CONSTANT) cvtest::smoothBorder(src, borderColor, 1);
int shift = static_cast<int>(src.cols*0.04);
Mat srcVertices = (Mat_<Vec2f>(1, 4) << Vec2f(0, 0),
Vec2f(static_cast<float>(size.width-1), 0),
Vec2f(static_cast<float>(size.width-1), static_cast<float>(size.height-1)),
Vec2f(0, static_cast<float>(size.height-1)));
Mat dstVertices = (Mat_<Vec2f>(1, 4) << Vec2f(0, static_cast<float>(shift)),
Vec2f(static_cast<float>(size.width-shift/2), 0),
Vec2f(static_cast<float>(size.width-shift), static_cast<float>(size.height-shift)),
Vec2f(static_cast<float>(shift/2), static_cast<float>(size.height-1)));
Mat warpMat = getPerspectiveTransform(srcVertices, dstVertices);
declare.in(src).out(dst);
declare.time(100);
TEST_CYCLE()
{
warpPerspective( src, dst, warpMat, size, interType, borderMode, borderColor );
}
SANITY_CHECK(dst, 1);
}
PERF_TEST_P( TestRemap, remap,
Combine(
Values( CV_8UC1, CV_8UC3, CV_8UC4, CV_32FC1 ),
Values( szVGA, sz1080p ),
InterTypeExtended::all(),
BorderMode::all(),
RemapMode::all()
)
)
{
int type = get<0>(GetParam());
Size size = get<1>(GetParam());
int interpolationType = get<2>(GetParam());
int borderMode = get<3>(GetParam());
int remapMode = get<4>(GetParam());
unsigned int height = size.height;
unsigned int width = size.width;
Mat source(height, width, type);
Mat destination;
Mat map_x(height, width, CV_32F);
Mat map_y(height, width, CV_32F);
declare.in(source, WARMUP_RNG);
update_map(source, map_x, map_y, remapMode, ((interpolationType & WARP_RELATIVE_MAP) != 0));
TEST_CYCLE()
{
remap(source, destination, map_x, map_y, interpolationType, borderMode);
}
SANITY_CHECK_NOTHING();
}
void update_map(const Mat& src, Mat& map_x, Mat& map_y, const int remapMode, bool relative )
{
for( int j = 0; j < src.rows; j++ )
{
for( int i = 0; i < src.cols; i++ )
{
switch( remapMode )
{
case HALF_SIZE:
if( i > src.cols*0.25 && i < src.cols*0.75 && j > src.rows*0.25 && j < src.rows*0.75 )
{
map_x.at<float>(j,i) = 2*( i - src.cols*0.25f ) + 0.5f ;
map_y.at<float>(j,i) = 2*( j - src.rows*0.25f ) + 0.5f ;
}
else
{
map_x.at<float>(j,i) = 0 ;
map_y.at<float>(j,i) = 0 ;
}
break;
case UPSIDE_DOWN:
map_x.at<float>(j,i) = static_cast<float>(i) ;
map_y.at<float>(j,i) = static_cast<float>(src.rows - j) ;
break;
case REFLECTION_X:
map_x.at<float>(j,i) = static_cast<float>(src.cols - i) ;
map_y.at<float>(j,i) = static_cast<float>(j) ;
break;
case REFLECTION_BOTH:
map_x.at<float>(j,i) = static_cast<float>(src.cols - i) ;
map_y.at<float>(j,i) = static_cast<float>(src.rows - j) ;
break;
} // end of switch
if( relative )
{
map_x.at<float>(j,i) -= static_cast<float>(i);
map_y.at<float>(j,i) -= static_cast<float>(j);
}
}
}
}
PERF_TEST(Transform, getPerspectiveTransform_1000)
{
unsigned int size = 8;
Mat source(1, size/2, CV_32FC2);
Mat destination(1, size/2, CV_32FC2);
Mat transformCoefficient;
declare.in(source, destination, WARMUP_RNG);
PERF_SAMPLE_BEGIN()
for (int i = 0; i < 1000; i++)
{
transformCoefficient = getPerspectiveTransform(source, destination);
}
PERF_SAMPLE_END()
SANITY_CHECK_NOTHING();
}
PERF_TEST(Transform, getPerspectiveTransform_QR_1000)
{
unsigned int size = 8;
Mat source(1, size/2, CV_32FC2);
Mat destination(1, size/2, CV_32FC2);
Mat transformCoefficient;
declare.in(source, destination, WARMUP_RNG);
PERF_SAMPLE_BEGIN()
for (int i = 0; i < 1000; i++)
{
transformCoefficient = getPerspectiveTransform(source, destination, DECOMP_QR);
}
PERF_SAMPLE_END()
SANITY_CHECK_NOTHING();
}
PERF_TEST_P( TestWarpAffineSmallSrc, WarpAffine_SmallSrc,
Combine(
Values(CV_8UC1, CV_8UC4),
Values( Size(512, 512) ),
InterType::all()
)
)
{
const Size szSrc(64, 64);
const int dataType = get<0>(GetParam());
const Size sz = get<1>(GetParam());
const int interType = get<2>(GetParam());
const int borderMode = BORDER_TRANSPARENT;
Mat src(szSrc, dataType), dst(sz, dataType, Scalar::all(0));
cvtest::fillGradient(src);
const double offset = (sz.width - szSrc.width) / 2.0;
const Mat warpMat = (Mat_<double>(2, 3) << 1, 0, offset,
0, 1, offset);
declare.in(src).out(dst);
TEST_CYCLE() warpAffine( src, dst, warpMat, sz, interType, borderMode );
SANITY_CHECK_NOTHING();
}
PERF_TEST_P( TestWarpPerspectiveSmallSrc, WarpPerspective_SmallSrc,
Combine(
Values(CV_8UC1, CV_8UC4),
Values( Size(512, 512) ),
InterType::all()
)
)
{
const Size szSrc(64, 64);
const int dataType = get<0>(GetParam());
const Size sz = get<1>(GetParam());
const int interType = get<2>(GetParam());
const int borderMode = BORDER_TRANSPARENT;
Mat src(szSrc, dataType), dst(sz, dataType, Scalar::all(0));
cvtest::fillGradient(src);
const float w = (float)szSrc.width, h = (float)szSrc.height;
const float off = (sz.width - szSrc.width) / 2.0f;
const Point2f sp[] = { Point2f(0, 0), Point2f(w, 0), Point2f(0, h), Point2f(w, h) };
const Point2f dp[] = { Point2f(off, off), Point2f(off + w, off),
Point2f(off, off + h), Point2f(off + w, off + h) };
const Mat warpMat = getPerspectiveTransform(sp, dp);
declare.in(src).out(dst);
TEST_CYCLE() warpPerspective( src, dst, warpMat, sz, interType, borderMode );
SANITY_CHECK_NOTHING();
}
} // namespace