From 9e2ae7cba0bc52882436cd3462b64c98950202e3 Mon Sep 17 00:00:00 2001 From: Oscar Amoros Huguet Date: Wed, 26 Aug 2026 22:32:05 +0200 Subject: [PATCH 1/2] Adding inputless warp functions --- include/cvGPUSpeedup.cuh | 72 +++++++- include/cvGPUSpeedupHelpers.cuh | 16 +- tests/warping/test_warping_opencv.cu | 241 ++++++++++++++++++++++++++- 3 files changed, 326 insertions(+), 3 deletions(-) diff --git a/include/cvGPUSpeedup.cuh b/include/cvGPUSpeedup.cuh index 703b74bc..ec4e8f60 100644 --- a/include/cvGPUSpeedup.cuh +++ b/include/cvGPUSpeedup.cuh @@ -290,7 +290,7 @@ inline constexpr auto warp(const cv::cuda::GpuMat& input, const cv::Mat& transfo if (transform_matrix.type() != CV_64FC1) { throw std::runtime_error("Transform matrix type should be CV_64FC1."); } - const auto read = fk::PerThreadRead::build(fk::RawPtr{ (CUDA_T(InputType)*)input.data, { static_cast(input.cols), static_cast(input.rows), static_cast(input.step) } }); + const auto read = getReader(input); if constexpr (WT == fk::WarpType::Affine) { cv::Mat inverse_transform_matrix; cv::invertAffineTransform(transform_matrix, inverse_transform_matrix); @@ -307,6 +307,28 @@ inline constexpr auto warp(const cv::cuda::GpuMat& input, const cv::Mat& transfo } } +template +inline constexpr auto warp(const cv::Mat& transform_matrix, const cv::Size& dstSize) { + if (transform_matrix.type() != CV_64FC1) { + throw std::runtime_error("Transform matrix type should be CV_64FC1."); + } + + if constexpr (WT == fk::WarpType::Affine) { + cv::Mat inverse_transform_matrix; + cv::invertAffineTransform(transform_matrix, inverse_transform_matrix); + const double* const tm_raw = inverse_transform_matrix.ptr(); + const auto params = internal::warp_getWarpingAffineParameters(tm_raw, dstSize); + + return fk::Warping::build(params); + } else { + const cv::Mat inverse_transform_matrix(transform_matrix.inv()); + const double* const tm_raw = inverse_transform_matrix.ptr(); + const auto params = internal::warp_getWarpingPerspectiveParameters(tm_raw, dstSize); + + return fk::Warping::build(params); + } +} + namespace internal { template inline constexpr auto warp_batchAffineParameters_helper_rt(const std::array& transform_matrices, @@ -400,6 +422,22 @@ inline constexpr auto warp(const std::array& inputs, return readBatch.then(fk_batch_warp); } +template +inline constexpr auto warp(const std::array& transform_matrices, + const std::array& dstSize) { + for (int i = 0; i < BATCH; ++i) { + if (transform_matrices[i].type() != CV_64FC1) { + throw std::runtime_error("Transform matrix type should be CV_64FC1."); + } + } + + const auto fk_warpParams = internal::warp_batchParameters(transform_matrices, dstSize); + + const auto fk_batch_warp = fk::Warping::build(fk_warpParams); + + return fk_batch_warp; +} + template inline constexpr auto warp(const std::array& inputs, const std::array& transform_matrices, @@ -407,6 +445,12 @@ inline constexpr auto warp(const std::array& inputs, return warp(inputs, transform_matrices, fk::make_set_std_array(dstSize)); } +template +inline constexpr auto warp(const std::array& transform_matrices, + const cv::Size& dstSize) { + return warp(transform_matrices, fk::make_set_std_array(dstSize)); +} + template inline constexpr auto warp(const std::array& inputs, const std::array& transform_matrices, @@ -433,6 +477,25 @@ inline constexpr auto warp(const std::array& inputs, return readBatch.then(fk_batch_warp); } +template +inline constexpr auto warp(const std::array& transform_matrices, + const std::array& dstSize, + const int& usedPlanes, const cv::Scalar& defaultValue) { + for (int i = 0; i < usedPlanes; ++i) { + if (transform_matrices[i].type() != CV_64FC1) { + throw std::runtime_error("Transform matrix type should be CV_64FC1."); + } + } + using DefaultType = CUDA_T(DEFAULT_TYPE); + const auto fk_defaultValue = defaultValue == cv::Scalar() ? fk::make_set(0.f) : cvScalar2CUDAV::get(defaultValue); + + const auto fk_warpParams = internal::warp_batchParameters(transform_matrices, dstSize, usedPlanes); + + const auto fk_batch_warp = fk::Warping::build(usedPlanes, fk_defaultValue, fk_warpParams); + + return fk_batch_warp; +} + template inline constexpr auto warp(const std::array& inputs, const std::array& transform_matrices, @@ -441,6 +504,13 @@ inline constexpr auto warp(const std::array& inputs, return warp(inputs, transform_matrices, fk::make_set_std_array(dstSize), usedPlanes, defaultValue); } +template +inline constexpr auto warp(const std::array& transform_matrices, + const cv::Size& dstSize, + const int& usedPlanes, const cv::Scalar& defaultValue) { + return warp(transform_matrices, fk::make_set_std_array(dstSize), usedPlanes, defaultValue); +} + template inline constexpr auto crop(const BackIOp& backIOp, const std::array& rects) { return backIOp.then(crop(rects)); diff --git a/include/cvGPUSpeedupHelpers.cuh b/include/cvGPUSpeedupHelpers.cuh index 13849065..b3cd0f01 100644 --- a/include/cvGPUSpeedupHelpers.cuh +++ b/include/cvGPUSpeedupHelpers.cuh @@ -1,4 +1,5 @@ -/* Copyright 2023-2025 Oscar Amoros Huguet +/* Copyright 2023-2026 Oscar Amoros Huguet + * Copyright 2026 Grup Mediapro S.L.U Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +21,19 @@ #include namespace cvGS { + + template + inline constexpr auto getReader(const cv::cuda::GpuMat& input) { + return fk::PerThreadRead::build( + fk::RawPtr{ + (CUDA_T(InputType)*)input.data, + { static_cast(input.cols), + static_cast(input.rows), + static_cast(input.step) } + } + ); + } + template inline cv::Scalar cvScalar_set(const BASE_CUDA_T(T)& value) { if constexpr (CV_MAT_CN(T) == 1) { diff --git a/tests/warping/test_warping_opencv.cu b/tests/warping/test_warping_opencv.cu index ffc13d66..7d413c42 100644 --- a/tests/warping/test_warping_opencv.cu +++ b/tests/warping/test_warping_opencv.cu @@ -259,12 +259,251 @@ bool testPerspectiveBatchNotAll() { return true; } +bool testPerspectiveNoInput() { + // Load the image + const cv::Mat img = cv::imread(getSourceDir() + "/images/NSightSystemsTimeline1.png"); + if (img.empty()) { + std::cerr << "Error loading image" << std::endl; + return false; + } + + cv::cuda::Stream stream; + + // Upload the image to GPU + const cv::cuda::GpuMat d_img(img); + + // Define the source and destination points for perspective transformation + cv::Point2f src_points[4] = { cv::Point2f(56, 65), cv::Point2f(368, 52), cv::Point2f(28, 387), cv::Point2f(389, 390) }; + cv::Point2f dst_points[4] = { cv::Point2f(0, 0), cv::Point2f(300, 0), cv::Point2f(0, 300), cv::Point2f(300, 300) }; + + // Get the perspective transformation matrix + cv::Mat perspective_matrix = cv::getPerspectiveTransform(src_points, dst_points); + + // Preallocate the result images + cv::cuda::GpuMat d_resultcv(img.size(), CV_8UC3); + cv::cuda::GpuMat d_resultcvGS(img.size(), CV_8UC3); + + // Apply the perspective transformation + cv::cuda::warpPerspective(d_img, d_resultcv, perspective_matrix, img.size(), 1, 0, cv::Scalar(), stream); + + const auto readIOp = cvGS::getReader(d_img); + + const auto warpFunc = cvGS::warp(perspective_matrix, img.size()); + + auto writeFunc = cvGS::write(d_resultcvGS); + cvGS::executeOperations(stream, readIOp, warpFunc, fk::Cast::build(), writeFunc); + + stream.waitForCompletion(); + + // Download the result back to CPU + cv::Mat resultcv(d_resultcv); + cv::Mat resultcvGS(d_resultcvGS); + + const bool correct = compareAndCheck(resultcv, resultcvGS); + + std::cout << "Perspective transformation: " << (correct ? "PASS" : "EXPECTED_FAIL") << std::endl; + + return true; +} + +bool testAffineNoInput() { + // Load the image + const cv::Mat img = cv::imread(getSourceDir() + "/images/NSightSystemsTimeline1.png"); + if (img.empty()) { + std::cerr << "Error loading image" << std::endl; + return false; + } + + cv::cuda::Stream stream; + + // Upload the image to GPU + const cv::cuda::GpuMat d_img(img); + + // Define the translation values + double tx = 50, ty = 100; + + // Get the affine transformation matrix + cv::Mat affine_matrix = (cv::Mat_(2, 3) << 1, 0, tx, 0, 1, ty); + + // Preallocate the result images + cv::cuda::GpuMat d_resultcv(img.size(), CV_8UC3); + cv::cuda::GpuMat d_resultcvGS(img.size(), CV_8UC3); + + // Apply the affine transformation + cv::cuda::GpuMat d_result; + cv::cuda::warpAffine(d_img, d_resultcv, affine_matrix, img.size()); + + const auto readIOp = cvGS::getReader(d_img); + const auto warpFunc = cvGS::warp(affine_matrix, img.size()); + auto writeFunc = cvGS::write(d_resultcvGS); + cvGS::executeOperations(stream, readIOp, warpFunc, fk::Cast::build(), writeFunc); + + stream.waitForCompletion(); + + // Download the result back to CPU + cv::Mat resultcv(d_resultcv); + cv::Mat resultcvGS(d_resultcvGS); + + const bool correct = compareAndCheck(resultcv, resultcvGS); + + std::cout << "Affine transformation: " << (correct ? "PASS" : "FAIL") << std::endl; + + return correct; +} + +bool testPerspectiveBatchNoInput() { + constexpr size_t NUM_IMGS = 5; + + // Load the image + const cv::Mat img = cv::imread(getSourceDir() + "/images/NSightSystemsTimeline1.png"); + if (img.empty()) { + std::cerr << "Error loading image" << std::endl; + return false; + } + + cv::cuda::Stream stream; + + // Upload the image to GPU + const cv::cuda::GpuMat d_img(img); + // Compiler bug: can't use NUM_IMGS with std::array + const std::array d_imgs = { d_img, d_img, d_img, d_img, d_img }; + + // Define the source and destination points for perspective transformation + cv::Point2f src_points1[4] = { cv::Point2f(56, 65), cv::Point2f(368, 52), cv::Point2f(28, 387), cv::Point2f(389, 390) }; + cv::Point2f dst_points1[4] = { cv::Point2f(0, 0), cv::Point2f(300, 0), cv::Point2f(0, 300), cv::Point2f(300, 300) }; + + cv::Point2f src_points2[4] = { cv::Point2f(50, 50), cv::Point2f(400, 50), cv::Point2f(50, 400), cv::Point2f(400, 400) }; + cv::Point2f dst_points2[4] = { cv::Point2f(0, 0), cv::Point2f(300, 0), cv::Point2f(0, 300), cv::Point2f(300, 300) }; + + cv::Point2f src_points3[4] = { cv::Point2f(30, 30), cv::Point2f(350, 30), cv::Point2f(30, 350), cv::Point2f(350, 350) }; + cv::Point2f dst_points3[4] = { cv::Point2f(0, 0), cv::Point2f(250, 0), cv::Point2f(0, 250), cv::Point2f(250, 250) }; + + cv::Point2f src_points4[4] = { cv::Point2f(70, 70), cv::Point2f(370, 70), cv::Point2f(70, 370), cv::Point2f(370, 370) }; + cv::Point2f dst_points4[4] = { cv::Point2f(0, 0), cv::Point2f(280, 0), cv::Point2f(0, 280), cv::Point2f(280, 280) }; + + cv::Point2f src_points5[4] = { cv::Point2f(20, 20), cv::Point2f(320, 20), cv::Point2f(20, 320), cv::Point2f(320, 320) }; + cv::Point2f dst_points5[4] = { cv::Point2f(0, 0), cv::Point2f(200, 0), cv::Point2f(0, 200), cv::Point2f(200, 200) }; + + // Get the perspective transformation matrix + std::array perspective_matrices = { cv::getPerspectiveTransform(src_points1, dst_points1), + cv::getPerspectiveTransform(src_points2, dst_points2), + cv::getPerspectiveTransform(src_points3, dst_points3), + cv::getPerspectiveTransform(src_points4, dst_points4), + cv::getPerspectiveTransform(src_points5, dst_points5) }; + + // Preallocate the result images + std::array d_resultscv{ cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3) }; + std::array d_resultscvGS{ cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3) }; + + // Apply the perspective transformation + + for (int i = 0; i < NUM_IMGS; ++i) { + cv::cuda::warpPerspective(d_imgs[i], d_resultscv[i], perspective_matrices[i], img.size(), 1, 0, cv::Scalar(), stream); + } + + const auto readIOp = cvGS::getReader(d_img); + const auto warpFunc = cvGS::warp(perspective_matrices, img.size()); + + auto fk_outputs = cvGS::gpuMat2RawPtr2D_arr(d_resultscvGS); + auto writeFunc = fk::PerThreadWrite::build(fk_outputs); + cvGS::executeOperations(stream, readIOp.then(warpFunc), fk::Cast::build(), writeFunc); + + stream.waitForCompletion(); + + // Download the result back to CPU + for (int i = 0; i < NUM_IMGS; ++i) { + cv::Mat resultcv(d_resultscv[i]); + cv::Mat resultcvGS(d_resultscvGS[i]); + const bool correct = compareAndCheck(resultcv, resultcvGS); + std::cout << "Perspective transformation batch " << i << ": " << (correct ? "PASS" : "EXPECTED_FAIL") << std::endl; + } + + return true; +} + +bool testPerspectiveBatchNotAllNoInput() { + constexpr size_t NUM_IMGS2 = 10; + + // Load the image + const cv::Mat img = cv::imread(getSourceDir() + "/images/NSightSystemsTimeline1.png"); + if (img.empty()) { + std::cerr << "Error loading image" << std::endl; + return false; + } + + cv::cuda::Stream stream; + + // Upload the image to GPU + const cv::cuda::GpuMat d_img(img); + const std::array d_imgs = { d_img, d_img, d_img, d_img, d_img, d_img, d_img, d_img, d_img, d_img }; + + int usedPlanes = 3; + + // Define the source and destination points for perspective transformation + cv::Point2f src_points1[4] = { cv::Point2f(56, 65), cv::Point2f(368, 52), cv::Point2f(28, 387), cv::Point2f(389, 390) }; + cv::Point2f dst_points1[4] = { cv::Point2f(0, 0), cv::Point2f(300, 0), cv::Point2f(0, 300), cv::Point2f(300, 300) }; + + cv::Point2f src_points2[4] = { cv::Point2f(50, 50), cv::Point2f(400, 50), cv::Point2f(50, 400), cv::Point2f(400, 400) }; + cv::Point2f dst_points2[4] = { cv::Point2f(0, 0), cv::Point2f(300, 0), cv::Point2f(0, 300), cv::Point2f(300, 300) }; + + cv::Point2f src_points3[4] = { cv::Point2f(30, 30), cv::Point2f(350, 30), cv::Point2f(30, 350), cv::Point2f(350, 350) }; + cv::Point2f dst_points3[4] = { cv::Point2f(0, 0), cv::Point2f(250, 0), cv::Point2f(0, 250), cv::Point2f(250, 250) }; + + cv::Point2f src_points4[4] = { cv::Point2f(70, 70), cv::Point2f(370, 70), cv::Point2f(70, 370), cv::Point2f(370, 370) }; + cv::Point2f dst_points4[4] = { cv::Point2f(0, 0), cv::Point2f(280, 0), cv::Point2f(0, 280), cv::Point2f(280, 280) }; + + cv::Point2f src_points5[4] = { cv::Point2f(20, 20), cv::Point2f(320, 20), cv::Point2f(20, 320), cv::Point2f(320, 320) }; + cv::Point2f dst_points5[4] = { cv::Point2f(0, 0), cv::Point2f(200, 0), cv::Point2f(0, 200), cv::Point2f(200, 200) }; + + // Get the perspective transformation matrix + std::array perspective_matrices2{ cv::getPerspectiveTransform(src_points1, dst_points1), + cv::getPerspectiveTransform(src_points2, dst_points2), + cv::getPerspectiveTransform(src_points3, dst_points3), + {}, {}, {}, {}, {}, {}, {} }; + + // Preallocate the result images + std::array d_resultscv{ cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), + cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3) }; + std::array d_resultscvGS{ cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), + cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3), cv::cuda::GpuMat(img.size(), CV_8UC3) }; + + // Apply the perspective transformation + for (int i = 0; i < usedPlanes; ++i) { + cv::cuda::warpPerspective(d_imgs[i], d_resultscv[i], perspective_matrices2[i], img.size(), 1, 0, cv::Scalar(), stream); + } + + const auto warpFunc = cvGS::warp(d_imgs, perspective_matrices2, img.size(), usedPlanes, cv::Scalar()); + + auto fk_outputs = cvGS::gpuMat2RawPtr2D_arr(d_resultscvGS); + auto writeFunc = fk::PerThreadWrite::build(fk_outputs); + + cvGS::executeOperations(stream, warpFunc, fk::Cast::build(), writeFunc); + + stream.waitForCompletion(); + + // Download the result back to CPU + for (int i = 0; i < NUM_IMGS2; ++i) { + cv::Mat resultcv(d_resultscv[i]); + cv::Mat resultcvGS(d_resultscvGS[i]); + const bool correct = compareAndCheck(resultcv, resultcvGS); + std::cout << "Perspective transformation batch " << i << ": " << (correct ? "PASS" : "EXPECTED_FAIL") << std::endl; + } + + return true; +} + int launch() { const bool correctPerspective = testPerspective(); const bool correctAffine = testAffine(); const bool correctPerspectiveBatch = testPerspectiveBatch(); const bool correctPerspectiveBatchNotAll = testPerspectiveBatchNotAll(); - const bool correctAll = correctPerspective && correctAffine && correctPerspectiveBatch && correctPerspectiveBatchNotAll; + const bool correctPerspectiveNoInput = testPerspectiveNoInput(); + const bool correctAffineNoInput = testAffineNoInput(); + const bool correctPerspectiveBatchNoInput = testPerspectiveBatchNoInput(); + const bool correctPerspectiveBatchNotAllNoInput = testPerspectiveBatchNotAllNoInput(); + const bool correctAll = correctPerspective && correctAffine && correctPerspectiveBatch && correctPerspectiveBatchNotAll + && correctPerspectiveNoInput && correctAffineNoInput && correctPerspectiveBatchNoInput && correctPerspectiveBatchNotAllNoInput; + // warpPerspective is almost identical to OpenCV's implementation, but there are a few pixels of difference in // the border. The reason is hard to find, since OpenCV is using NPP for the warping. return correctAll ? 0 : -1; From 589f58c64dbf673831e6bac8f11c7888ec3581ae Mon Sep 17 00:00:00 2001 From: Oscar Amoros Huguet Date: Wed, 26 Aug 2026 22:53:30 +0200 Subject: [PATCH 2/2] Finished missing test adaptation for warp tests --- tests/warping/test_warping_opencv.cu | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/warping/test_warping_opencv.cu b/tests/warping/test_warping_opencv.cu index 7d413c42..0284831f 100644 --- a/tests/warping/test_warping_opencv.cu +++ b/tests/warping/test_warping_opencv.cu @@ -406,7 +406,7 @@ bool testPerspectiveBatchNoInput() { auto fk_outputs = cvGS::gpuMat2RawPtr2D_arr(d_resultscvGS); auto writeFunc = fk::PerThreadWrite::build(fk_outputs); - cvGS::executeOperations(stream, readIOp.then(warpFunc), fk::Cast::build(), writeFunc); + cvGS::executeOperations(stream, readIOp, warpFunc, fk::Cast::build(), writeFunc); stream.waitForCompletion(); @@ -472,12 +472,14 @@ bool testPerspectiveBatchNotAllNoInput() { cv::cuda::warpPerspective(d_imgs[i], d_resultscv[i], perspective_matrices2[i], img.size(), 1, 0, cv::Scalar(), stream); } - const auto warpFunc = cvGS::warp(d_imgs, perspective_matrices2, img.size(), usedPlanes, cv::Scalar()); + const auto readIOp = cvGS::getReader(d_img); + + const auto warpFunc = cvGS::warp(perspective_matrices2, img.size(), usedPlanes, cv::Scalar()); auto fk_outputs = cvGS::gpuMat2RawPtr2D_arr(d_resultscvGS); auto writeFunc = fk::PerThreadWrite::build(fk_outputs); - cvGS::executeOperations(stream, warpFunc, fk::Cast::build(), writeFunc); + cvGS::executeOperations(stream, readIOp, warpFunc, fk::Cast::build(), writeFunc); stream.waitForCompletion();