From 439b6af379ef81af4687d5a3a92dd1183fc879f9 Mon Sep 17 00:00:00 2001 From: Rita Melo Date: Thu, 5 Jun 2025 18:14:56 +0100 Subject: [PATCH 01/29] feat: G-API: Custom stream sources in Python (#27276) Implemented: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A C++ proxy class PythonCustomStreamSource that implements the IStreamSource interface. This class acts as a bridge between G-API’s internal streaming engine and user-defined Python objects. Internally, it stores a reference to a Python object (PyObject*) and is responsible for: calling the Python object’s pull() method to retrieve the next frame, calling the descr_of() method to obtain the frame format description, acquiring and releasing the Python GIL as needed, converting the returned numpy.ndarray into cv::Mat, and handling any exceptions or conversion errors with proper diagnostics. - A Python-facing factory function, cv.gapi.wip.make_py_src(), which takes a Python object as an argument and wraps it into a cv::Ptr. Internally, this function constructs a PythonCustomStreamSource instance and passes the Python object to it. This design allows Python users to define any class that implements two methods: pull() and descr_of(). No subclassing or special decorators are required on the Python side. The user simply needs to implement the expected interface. Co-authored-by: Leonor Francisco --- modules/gapi/CMakeLists.txt | 4 + .../gapi/pysrc/python_stream_source.hpp | 65 ++++++++ modules/gapi/misc/python/pyopencv_gapi.hpp | 155 ++++++++++++++++++ .../misc/python/test/test_gapi_streaming.py | 31 ++++ .../gapi/src/pysrc/python_stream_source.cpp | 17 ++ 5 files changed, 272 insertions(+) create mode 100644 modules/gapi/include/opencv2/gapi/pysrc/python_stream_source.hpp create mode 100644 modules/gapi/src/pysrc/python_stream_source.cpp diff --git a/modules/gapi/CMakeLists.txt b/modules/gapi/CMakeLists.txt index f18290ca7d..2a8d9185c0 100644 --- a/modules/gapi/CMakeLists.txt +++ b/modules/gapi/CMakeLists.txt @@ -66,6 +66,7 @@ file(GLOB gapi_ext_hdrs "${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/streaming/onevpl/*.hpp" "${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/plaidml/*.hpp" "${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/util/*.hpp" + "${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/pysrc/*.hpp" ) set(gapi_srcs @@ -242,6 +243,9 @@ set(gapi_srcs src/streaming/gstreamer/gstreamer_media_adapter.cpp src/streaming/gstreamer/gstreamerenv.cpp + # Python Custom Stream source + src/pysrc/python_stream_source.cpp + # Utils (ITT tracing) src/utils/itt.cpp ) diff --git a/modules/gapi/include/opencv2/gapi/pysrc/python_stream_source.hpp b/modules/gapi/include/opencv2/gapi/pysrc/python_stream_source.hpp new file mode 100644 index 0000000000..b43dea5305 --- /dev/null +++ b/modules/gapi/include/opencv2/gapi/pysrc/python_stream_source.hpp @@ -0,0 +1,65 @@ +#ifndef OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP +#define OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP +#include +#include + +namespace cv { +namespace gapi { +namespace wip { + +/** + * @brief Creates a G-API IStreamSource that delegates to a Python-defined source. + * + * This factory function wraps a Python object (for example, an instance of a class + * implementing a `pull()` and a `descr_of()` method) into a `cv::gapi::wip::IStreamSource`, + * enabling it to be used within a G-API computation graph. The OpenCV Python bindings + * automatically convert the PyObject into a `cv::Ptr`. + * + * @param src + * A `cv::Ptr` that internally holds the original Python object. + * + * @return + * A `cv::Ptr` that wraps the provided Python object. On each frame pull, + * G-API will: + * - Acquire the Python GIL + * - Call the Python object’s `pull()` method + * - Convert the resulting NumPy array to a `cv::Mat` + * - Pass the `cv::Mat` into the G-API pipeline + * + * @note + * In Python, you can use the returned `make_py_src` as follows: + * + * @code{.py} + * class MyClass: + * def __init__(self): + * # Initialize your source + * def pull(self): + * # Return the next frame as a numpy.ndarray or None for end-of-stream + * def descr_of(self): + * # Return a numpy.ndarray that describes the format of the frames + * + * # Create a G-API source from a Python class + * py_src = cv.gapi.wip.make_py_src(MyClass()) + * + * # Define a simple graph: input → copy → output + * g_in = cv.GMat() + * g_out = cv.gapi.copy(g_in) + * graph = cv.GComputation(g_in, g_out) + * + * # Compile the pipeline for streaming and assign the source + * pipeline = graph.compileStreaming() + * pipeline.setSource([py_src]) + * pipeline.start() + * @endcode + */ + +CV_EXPORTS_W cv::Ptr +make_py_src(const cv::Ptr& src); + + +} // namespace wip +} // namespace gapi +} // namespace cv + + +#endif // OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP diff --git a/modules/gapi/misc/python/pyopencv_gapi.hpp b/modules/gapi/misc/python/pyopencv_gapi.hpp index 66c3910756..6dcf3081e3 100644 --- a/modules/gapi/misc/python/pyopencv_gapi.hpp +++ b/modules/gapi/misc/python/pyopencv_gapi.hpp @@ -1162,6 +1162,161 @@ bool pyopencv_to(PyObject* obj, cv::GProtoOutputArgs& value, const ArgInfo& info } } +namespace cv { +namespace gapi { +namespace wip { + +/** + * @class PythonCustomStreamSource + * @brief Wraps a Python-defined frame source as an IStreamSource for G-API. + * + * This class allows a G-API pipeline to pull frames from a Python object. The Python object + * must implement at a `pull()` method which must return a `numpy.ndarray` containing + * the next frame, or `None` to signal end-of-stream. It can also implement a `descr_of()` + * method which must return a `numpy.ndarray` that describes the format + * (data type, number of channels, height, width) of the frames produced. + */ + +class PythonCustomStreamSource : public IStreamSource +{ + public: + PythonCustomStreamSource(PyObject* _obj = nullptr) : obj(_obj) + { + if (obj) + Py_INCREF(obj); + } + + ~PythonCustomStreamSource() + { + if (obj) + Py_DECREF(obj); + } + + bool pull(cv::gapi::wip::Data& data) CV_OVERRIDE + { + if (!obj) + return false; + + PyObject* src = reinterpret_cast(obj); + + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); + + PyObject* result = PyObject_CallMethodObjArgs(src, PyUnicode_FromString("pull"), NULL); + bool hasPyPullError = PyErr_Occurred() != nullptr; + + if (!result) + { + PyErr_Print(); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): call to .pull() failed"); + } + + if (result == Py_None) + { + Py_DECREF(result); + PyGILState_Release(gstate); + return false; + } + + if (!PyArray_Check(result)) + { + PyErr_Format(PyExc_TypeError, "Expected numpy.ndarray from .pull()"); + PyErr_Print(); + Py_DECREF(result); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): .pull() did not return a numpy.ndarray"); + } + + cv::Mat mat; + ArgInfo info("pull return", 0); + if (!pyopencv_to(result, mat, info) || PyErr_Occurred()) + { + PyErr_Print(); + Py_DECREF(result); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): failed to convert numpy to cv::Mat"); + } + + if (mat.empty()) + { + Py_DECREF(result); + PyGILState_Release(gstate); + return false; + } + + data = mat; + Py_DECREF(result); + PyGILState_Release(gstate); + + if (hasPyPullError) + CV_Error(cv::Error::StsError, "Python .pull() call error"); + + return true; + } + + GMetaArg descr_of() const CV_OVERRIDE + { + if (!obj) + return cv::GMetaArg(cv::GFrameDesc{cv::MediaFormat::BGR, cv::Size(640, 480)}); + + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* result = PyObject_CallMethodObjArgs(obj, PyUnicode_FromString("descr_of"), NULL); + if (!result) + { + PyErr_Print(); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): conversion error"); + } + + if (!PyArray_Check(result)) { + PyErr_Format(PyExc_TypeError, "Expected numpy.ndarray from .descr_of()"); + PyErr_Print(); + Py_DECREF(result); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): did not return a numpy.ndarray"); + } + + cv::Mat mat; + ArgInfo info("descr_of return", 0); + if (!pyopencv_to(result, mat, info)) + { + PyErr_Print(); + Py_DECREF(result); + PyGILState_Release(gstate); + CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): conversion error"); + } + + Py_DECREF(result); + PyGILState_Release(gstate); + cv::GMatDesc mdesc = cv::descr_of(mat); + + return cv::GMetaArg(mdesc); + } + +private: + PyObject* obj; +}; + +inline cv::Ptr make_pysrc_from_pyobject(PyObject* obj) +{ + return cv::makePtr(obj); +} + +} // namespace wip +} // namespace gapi +} // namespace cv + +template<> +bool pyopencv_to(PyObject* obj, cv::Ptr& p, const ArgInfo&) +{ + if (!obj) + return false; + + p = cv::makePtr(obj); + return true; +} + // extend cv.gapi methods #define PYOPENCV_EXTRA_METHODS_GAPI \ {"kernels", CV_PY_FN_WITH_KW(pyopencv_cv_gapi_kernels), "kernels(...) -> GKernelPackage"}, \ diff --git a/modules/gapi/misc/python/test/test_gapi_streaming.py b/modules/gapi/misc/python/test/test_gapi_streaming.py index 7f9de5f767..2d3f8f2785 100644 --- a/modules/gapi/misc/python/test/test_gapi_streaming.py +++ b/modules/gapi/misc/python/test/test_gapi_streaming.py @@ -539,7 +539,38 @@ try: self.assertEqual(0.0, cv.norm(convertNV12p2BGR(expected1), actual1, cv.NORM_INF)) self.assertEqual(0.0, cv.norm(convertNV12p2BGR(expected2), actual2, cv.NORM_INF)) + def test_python_custom_stream_source(self): + class MySource: + def __init__(self): + self.count = 0 + def pull(self): + if self.count >= 3: + return None + self.count += 1 + return np.ones((10, 10, 3), np.uint8) * self.count + + def descr_of(self): + return np.zeros((10, 10, 3), np.uint8) + + g_in = cv.GMat() + g_out = cv.gapi.copy(g_in) + c = cv.GComputation(g_in, g_out) + + comp = c.compileStreaming() + + src = cv.gapi.wip.make_py_src(MySource()) + comp.setSource([src]) + comp.start() + + frames = [] + while True: + has_frame, frame = comp.pull() + if not has_frame: + break + frames.append(frame) + + self.assertEqual(len(frames), 3) except unittest.SkipTest as e: diff --git a/modules/gapi/src/pysrc/python_stream_source.cpp b/modules/gapi/src/pysrc/python_stream_source.cpp new file mode 100644 index 0000000000..6ede2ae201 --- /dev/null +++ b/modules/gapi/src/pysrc/python_stream_source.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include + +namespace cv { +namespace gapi { +namespace wip { + +cv::Ptr make_py_src(const cv::Ptr& src) +{ + return src; +} + +} // namespace wip +} // namespace gapi +} // namespace cv From b5e96d76ebbea7996091780657f5ac09faf6a7e4 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Thu, 9 Oct 2025 11:54:10 +0300 Subject: [PATCH 02/29] Fixed warnings produced by x86 builds on Windows. --- modules/imgcodecs/src/grfmt_gif.cpp | 2 +- modules/imgcodecs/src/utils.cpp | 4 ++-- modules/imgcodecs/src/utils.hpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_gif.cpp b/modules/imgcodecs/src/grfmt_gif.cpp index f5307ca39b..f9c5744466 100644 --- a/modules/imgcodecs/src/grfmt_gif.cpp +++ b/modules/imgcodecs/src/grfmt_gif.cpp @@ -764,7 +764,7 @@ bool GifEncoder::lzwEncode() { //initialize int32_t prev = imgCodeStream[0]; - for (int64_t i = 1; i < height * width; i++) { + for (size_t i = 1; i < size_t(height * width); i++) { // add the output code to the output buffer while (bitLeft >= 8) { buffer[bufferLen++] = (uchar)output; diff --git a/modules/imgcodecs/src/utils.cpp b/modules/imgcodecs/src/utils.cpp index 1e14e86610..cfbd62b7bc 100644 --- a/modules/imgcodecs/src/utils.cpp +++ b/modules/imgcodecs/src/utils.cpp @@ -51,10 +51,10 @@ int validateToInt(size_t sz) return valueInt; } -int64_t validateToInt64(size_t sz) +int64_t validateToInt64(ptrdiff_t sz) { int64_t valueInt = static_cast(sz); - CV_Assert((size_t)valueInt == sz); + CV_Assert((ptrdiff_t)valueInt == sz); return valueInt; } diff --git a/modules/imgcodecs/src/utils.hpp b/modules/imgcodecs/src/utils.hpp index b5a00c47b3..f2649f0ecd 100644 --- a/modules/imgcodecs/src/utils.hpp +++ b/modules/imgcodecs/src/utils.hpp @@ -45,7 +45,7 @@ namespace cv { int validateToInt(size_t step); -int64_t validateToInt64(size_t step); +int64_t validateToInt64(ptrdiff_t step); template static inline size_t safeCastToSizeT(const _Tp v_origin, const char* msg) From e7d046f31a19fc5873effe0146fcdd16577d0165 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Fri, 10 Oct 2025 13:43:38 +0300 Subject: [PATCH 03/29] Skip LimitedSourceInfer.ReleaseFrameAsync test in CI as it hangs sporadically. --- modules/gapi/test/infer/gapi_infer_ie_test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/gapi/test/infer/gapi_infer_ie_test.cpp b/modules/gapi/test/infer/gapi_infer_ie_test.cpp index ff27489434..26241c1185 100644 --- a/modules/gapi/test/infer/gapi_infer_ie_test.cpp +++ b/modules/gapi/test/infer/gapi_infer_ie_test.cpp @@ -2343,6 +2343,9 @@ TEST_F(LimitedSourceInfer, ReleaseFrame) TEST_F(LimitedSourceInfer, ReleaseFrameAsync) { + if (cvtest::skipUnstableTests) + throw SkipTestException("Skip LimitedSourceInfer.ReleaseFrameAsync as it hangs sporadically"); + constexpr int max_frames = 50; constexpr int resources_limit = 4; constexpr int nireq = 8; From f74d4817244b266d0a1cb0225a738dafda4ef839 Mon Sep 17 00:00:00 2001 From: xcm4 Date: Sat, 11 Oct 2025 14:22:27 +0800 Subject: [PATCH 04/29] fix: Refactor tuple creation in NLM CUDA kernel for fixing nvcc build error --- modules/photo/src/cuda/nlm.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/photo/src/cuda/nlm.cu b/modules/photo/src/cuda/nlm.cu index 2c36239616..07e9a742cd 100644 --- a/modules/photo/src/cuda/nlm.cu +++ b/modules/photo/src/cuda/nlm.cu @@ -199,7 +199,7 @@ namespace cv { namespace cuda { namespace device static __device__ __forceinline__ const thrust::tuple, plus > op() { plus op; - return thrust::make_tuple(op, op); + return { op, op }; } }; template <> struct Unroll<2> @@ -218,7 +218,7 @@ namespace cv { namespace cuda { namespace device static __device__ __forceinline__ const thrust::tuple, plus, plus > op() { plus op; - return thrust::make_tuple(op, op, op); + return { op, op, op }; } }; template <> struct Unroll<3> @@ -237,7 +237,7 @@ namespace cv { namespace cuda { namespace device static __device__ __forceinline__ const thrust::tuple, plus, plus, plus > op() { plus op; - return thrust::make_tuple(op, op, op, op); + return { op, op, op, op }; } }; template <> struct Unroll<4> From 514d362ad88023d6f7b543b909e1ea1bc363e2f3 Mon Sep 17 00:00:00 2001 From: Maxim Smolskiy Date: Mon, 13 Oct 2025 09:09:56 +0300 Subject: [PATCH 05/29] Merge pull request #27876 from MaximSmolskiy:fix_charuco_board_pattern_in_generate_pattern.py Fix charuco_board_pattern in generate_pattern.py #27876 ### Pull Request Readiness Checklist Fix #27871 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 - [x] The PR is proposed to the proper branch - [x] 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 --- apps/pattern-tools/generate_pattern.py | 34 +++++++++++++++++++++---- doc/charuco_board_pattern.png | Bin 60003 -> 42616 bytes 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/pattern-tools/generate_pattern.py b/apps/pattern-tools/generate_pattern.py index 1123f5f116..deef8acb13 100755 --- a/apps/pattern-tools/generate_pattern.py +++ b/apps/pattern-tools/generate_pattern.py @@ -207,12 +207,36 @@ class PatternMaker: square = SVG("rect", x=x_pos+ch_ar_border, y=y_pos+ch_ar_border, width=self.aruco_marker_size, height=self.aruco_marker_size, fill="black", stroke="none") self.g.append(square) + + # BUG: https://github.com/opencv/opencv/issues/27871 + # The loop bellow merges white squares horizontally and vertically to exclude visible grid on the final pattern for x_ in range(len(img_mark[0])): - for y_ in range(len(img_mark)): - if (img_mark[y_][x_] != 0): - square = SVG("rect", x=x_pos+ch_ar_border+(x_)*side, y=y_pos+ch_ar_border+(y_)*side, width=side, - height=side, fill="white", stroke="white", stroke_width = spacing*0.01) - self.g.append(square) + y_ = 0 + while y_ < len(img_mark): + y_start = y_ + while y_ < len(img_mark) and img_mark[y_][x_] != 0: + y_ += 1 + + if y_ > y_start: + rect = SVG("rect", x=x_pos+ch_ar_border+(x_)*side, y=y_pos+ch_ar_border+(y_start)*side, width=side, + height=(y_ - y_start)*side, fill="white", stroke="none") + self.g.append(rect) + + y_ += 1 + + for y_ in range(len(img_mark)): + x_ = 0 + while x_ < len(img_mark[0]): + x_start = x_ + while x_ < len(img_mark[0]) and img_mark[y_][x_] != 0: + x_ += 1 + + if x_ > x_start: + rect = SVG("rect", x=x_pos+ch_ar_border+(x_start)*side, y=y_pos+ch_ar_border+(y_)*side, width=(x_-x_start)*side, + height=side, fill="white", stroke="none") + self.g.append(rect) + + x_ += 1 def save(self): c = canvas(self.g, width="%d%s" % (self.width, self.units), height="%d%s" % (self.height, self.units), diff --git a/doc/charuco_board_pattern.png b/doc/charuco_board_pattern.png index e97418d1ef2d266fcc06ca3b80e518e7426ab27c..3f97e7858792362095ab678cb6e522aa326a0c40 100644 GIT binary patch literal 42616 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 literal 60003 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 From 029c0817199ee393e28120e59856caede3d4e727 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Mon, 13 Oct 2025 10:35:00 +0300 Subject: [PATCH 06/29] Fixed -Wretrun-type warning in Highgui. --- modules/highgui/src/window.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/highgui/src/window.cpp b/modules/highgui/src/window.cpp index cf21feefbd..ad05fc1ef9 100644 --- a/modules/highgui/src/window.cpp +++ b/modules/highgui/src/window.cpp @@ -1115,9 +1115,9 @@ const std::string cv::currentUIFramework() return std::string("COCOA"); #elif defined (HAVE_WAYLAND) return std::string("WAYLAND"); -#else - return std::string(); #endif + + return std::string(); } // Without OpenGL From 34aee6cb28242817c86365f7ba7c58f75cc15a2b Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Mon, 13 Oct 2025 11:53:42 +0300 Subject: [PATCH 07/29] Removed unreachible code reported by MS Visual Studio on Windows. --- modules/gapi/src/api/s11n.cpp | 1 - .../gapi/src/backends/common/serialization.cpp | 17 ++++++----------- .../gapi/src/backends/fluid/gfluidbackend.cpp | 4 ++-- .../gapi/src/backends/fluid/gfluidbuffer.cpp | 2 +- .../gapi/src/executor/gstreamingexecutor.cpp | 1 - .../test/streaming/gapi_streaming_tests.cpp | 1 - 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/modules/gapi/src/api/s11n.cpp b/modules/gapi/src/api/s11n.cpp index 989cc79118..b86cd8f278 100644 --- a/modules/gapi/src/api/s11n.cpp +++ b/modules/gapi/src/api/s11n.cpp @@ -141,5 +141,4 @@ cv::GRunArg cv::gapi::bind(cv::GRunArgP &out) GAPI_Error("This value type is UNKNOWN!"); break; } - return cv::GRunArg(); } diff --git a/modules/gapi/src/backends/common/serialization.cpp b/modules/gapi/src/backends/common/serialization.cpp index 6fe924e61b..a9cfd1a564 100644 --- a/modules/gapi/src/backends/common/serialization.cpp +++ b/modules/gapi/src/backends/common/serialization.cpp @@ -206,22 +206,20 @@ IOStream& operator<< (IOStream& os, const cv::RMat& mat) { mat.serialize(os); return os; } -IIStream& operator>> (IIStream& is, cv::RMat&) { +IIStream& operator>> (IIStream&, cv::RMat&) { util::throw_error(std::logic_error("operator>> for RMat should never be called. " "Instead, cv::gapi::deserialize() " "should be used")); - return is; } IOStream& operator<< (IOStream& os, const cv::MediaFrame &frame) { frame.serialize(os); return os; } -IIStream& operator>> (IIStream& is, cv::MediaFrame &) { +IIStream& operator>> (IIStream&, cv::MediaFrame &) { util::throw_error(std::logic_error("operator>> for MediaFrame should never be called. " "Instead, cv::gapi::deserialize() " "should be used")); - return is; } namespace @@ -400,22 +398,19 @@ IOStream& operator<< (IOStream& os, const cv::UMat &) GAPI_Error("Serialization: Unsupported << for UMat"); return os; } -IIStream& operator >> (IIStream& is, cv::UMat &) +IIStream& operator >> (IIStream&, cv::UMat &) { GAPI_Error("Serialization: Unsupported >> for UMat"); - return is; } #endif // !defined(GAPI_STANDALONE) -IOStream& operator<< (IOStream& os, const cv::gapi::wip::IStreamSource::Ptr &) +IOStream& operator<< (IOStream&, const cv::gapi::wip::IStreamSource::Ptr &) { GAPI_Error("Serialization: Unsupported << for IStreamSource::Ptr"); - return os; } -IIStream& operator >> (IIStream& is, cv::gapi::wip::IStreamSource::Ptr &) +IIStream& operator >> (IIStream&, cv::gapi::wip::IStreamSource::Ptr &) { - GAPI_Assert("Serialization: Unsupported >> for IStreamSource::Ptr"); - return is; + GAPI_Error("Serialization: Unsupported >> for IStreamSource::Ptr"); } namespace diff --git a/modules/gapi/src/backends/fluid/gfluidbackend.cpp b/modules/gapi/src/backends/fluid/gfluidbackend.cpp index d24dcd599a..f363c6165c 100644 --- a/modules/gapi/src/backends/fluid/gfluidbackend.cpp +++ b/modules/gapi/src/backends/fluid/gfluidbackend.cpp @@ -313,7 +313,7 @@ static int maxLineConsumption(const cv::GFluidKernel::Kind kind, int window, int } } break; case cv::GFluidKernel::Kind::YUV420toRGB: return inPort == 0 ? 2 : 1; break; - default: GAPI_Error("InternalError"); return 0; + default: GAPI_Error("InternalError"); } } @@ -325,7 +325,7 @@ static int borderSize(const cv::GFluidKernel::Kind kind, int window) // Resize never reads from border pixels case cv::GFluidKernel::Kind::Resize: return 0; break; case cv::GFluidKernel::Kind::YUV420toRGB: return 0; break; - default: GAPI_Error("InternalError"); return 0; + default: GAPI_Error("InternalError"); } } diff --git a/modules/gapi/src/backends/fluid/gfluidbuffer.cpp b/modules/gapi/src/backends/fluid/gfluidbuffer.cpp index 2bdbbbecd6..6cbe2dc369 100644 --- a/modules/gapi/src/backends/fluid/gfluidbuffer.cpp +++ b/modules/gapi/src/backends/fluid/gfluidbuffer.cpp @@ -90,7 +90,7 @@ void fillBorderConstant(int borderSize, cv::Scalar borderValue, cv::Mat& mat) case CV_16S: return &fillConstBorderRow< int16_t>; break; case CV_16U: return &fillConstBorderRow; break; case CV_32F: return &fillConstBorderRow< float >; break; - default: GAPI_Error("InternalError"); return &fillConstBorderRow; + default: GAPI_Error("InternalError"); } }; diff --git a/modules/gapi/src/executor/gstreamingexecutor.cpp b/modules/gapi/src/executor/gstreamingexecutor.cpp index 6a397faca6..67ad18dfa2 100644 --- a/modules/gapi/src/executor/gstreamingexecutor.cpp +++ b/modules/gapi/src/executor/gstreamingexecutor.cpp @@ -1830,7 +1830,6 @@ bool cv::gimpl::GStreamingExecutor::pull(cv::GRunArgsP &&outs) default: GAPI_Error("Unsupported cmd type in pull"); } - GAPI_Error("Unreachable code"); } bool cv::gimpl::GStreamingExecutor::pull(cv::GOptRunArgsP &&outs) diff --git a/modules/gapi/test/streaming/gapi_streaming_tests.cpp b/modules/gapi/test/streaming/gapi_streaming_tests.cpp index 10f8df820f..343790cc88 100644 --- a/modules/gapi/test/streaming/gapi_streaming_tests.cpp +++ b/modules/gapi/test/streaming/gapi_streaming_tests.cpp @@ -327,7 +327,6 @@ public: if (m_curr_frame_id % m_throw_every_nth_frame == 0) { throw std::logic_error(InvalidSource::exception_msg()); - return true; } else { d = cv::Mat(m_mat); } From ca34212b68fb3c5aeb1c729ede232e5c1a525613 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Mon, 13 Oct 2025 16:50:55 +0300 Subject: [PATCH 08/29] More warning fixes iG-API on Windows. --- modules/gapi/src/backends/common/serialization.cpp | 3 +-- modules/gapi/src/compiler/gcompiler.cpp | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/gapi/src/backends/common/serialization.cpp b/modules/gapi/src/backends/common/serialization.cpp index a9cfd1a564..d3a4845175 100644 --- a/modules/gapi/src/backends/common/serialization.cpp +++ b/modules/gapi/src/backends/common/serialization.cpp @@ -393,10 +393,9 @@ IOStream& operator<< (IOStream& os, const cv::GArrayDesc &) {return os;} IIStream& operator>> (IIStream& is, cv::GArrayDesc &) {return is;} #if !defined(GAPI_STANDALONE) -IOStream& operator<< (IOStream& os, const cv::UMat &) +IOStream& operator<< (IOStream&, const cv::UMat &) { GAPI_Error("Serialization: Unsupported << for UMat"); - return os; } IIStream& operator >> (IIStream&, cv::UMat &) { diff --git a/modules/gapi/src/compiler/gcompiler.cpp b/modules/gapi/src/compiler/gcompiler.cpp index 666271d7ba..592bf43d54 100644 --- a/modules/gapi/src/compiler/gcompiler.cpp +++ b/modules/gapi/src/compiler/gcompiler.cpp @@ -341,7 +341,6 @@ void cv::gimpl::GCompiler::validateInputMeta() default: GAPI_Error("InternalError"); } - return false; // should never happen }; GAPI_LOG_DEBUG(nullptr, "Total count: " << m_metas.size()); From 0a25225b7640cb55ec866b788dfce9b84efd63f2 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov <2536374+asmorkalov@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:40:11 +0300 Subject: [PATCH 09/29] Merge pull request #27897 from asmorkalov:as/alernative_win_arm_neon_check Enabled fp16 conversions, but disabled NEON FP16 arithmetics on Windows for ARM for now #27897 ### 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 --- cmake/checks/cpu_neon_dotprod.cpp | 4 +++- cmake/checks/cpu_neon_fp16.cpp | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmake/checks/cpu_neon_dotprod.cpp b/cmake/checks/cpu_neon_dotprod.cpp index 2db6c4b1cc..71f2cb3b38 100644 --- a/cmake/checks/cpu_neon_dotprod.cpp +++ b/cmake/checks/cpu_neon_dotprod.cpp @@ -1,6 +1,8 @@ #include -#if (defined __GNUC__ && (defined __arm__ || defined __aarch64__)) || (defined _MSC_VER && (defined _M_ARM64 || defined _M_ARM64EC)) +#if (defined __GNUC__ && (defined __arm__ || defined __aarch64__))/* || (defined _MSC_VER && (defined _M_ARM64 || defined _M_ARM64EC)) */ +// Windows + ARM64 case disabled: https://github.com/opencv/opencv/issues/25052 + #include "arm_neon.h" int test() { diff --git a/cmake/checks/cpu_neon_fp16.cpp b/cmake/checks/cpu_neon_fp16.cpp index 45f5b05906..3c3d5da1e3 100644 --- a/cmake/checks/cpu_neon_fp16.cpp +++ b/cmake/checks/cpu_neon_fp16.cpp @@ -1,6 +1,7 @@ #include -#if (defined __GNUC__ && (defined __arm__ || defined __aarch64__)) || (defined _MSC_VER && (defined _M_ARM64 || defined _M_ARM64EC)) +#if (defined __GNUC__ && (defined __arm__ || defined __aarch64__)) /* || (defined _MSC_VER && (defined _M_ARM64 || defined _M_ARM64EC)) */ +// Windows + ARM64 case disabled: https://github.com/opencv/opencv/issues/25052 #include "arm_neon.h" float16x8_t vld1q_as_f16(const float* src) @@ -36,7 +37,7 @@ void test() vprintreg("s1*s2[0]+s1*s2[1] + ... + s1*s2[7]", d); } #else -#error "FP16 is not supported" +#error "NEON FP16 is not supported" #endif int main() From 0ee9c2796698346e78c640565ec6a0dfb7a95151 Mon Sep 17 00:00:00 2001 From: Dave Merchant <131936113+D00E@users.noreply.github.com> Date: Tue, 14 Oct 2025 07:56:06 +0100 Subject: [PATCH 10/29] Merge pull request #27810 from D00E:known-foreground-mask 2025-10-14T05:53:31.5387050Z C:\GHA-OCV-1\_work\ci-gha-workflow\ci-gha-workflow\opencv\modules\imgcodecs\src\bitstrm.cpp(156,57): warning C4244: 'argument': conversion from 'int64_t' to 'ptrdiff_t', possible loss of data [C:\GHA-OCV-1\_work\ci-gha-workflow\ci-gha-workflow\build\modules\imgcodecs\opencv_imgcodecs.vcxproj] ### Pull Request Readiness Checklist Optional Known Foreground Mask for Background Subtractors #27810 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 - [x] The PR is proposed to the proper branch - [x] 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 ### Description This adds an optional foreground input mask parameter to the MOG2 and KNN background subtractors, in line with issue https://github.com/opencv/opencv/issues/26476 4 tests are added under test_bgfg2.cpp: 2 for each subtractor type (1 with shadow detection and 1 without) A demo shows the feature with only 3 parameters and with a 4th optional foreground mask for both core subtractor types. Note: To patch contrib inheritance of the background subtraction class, empty apply method which throws a not implemented error is added to contrib subclasses. This is done to keep the overloaded apply function as pure virtual. Contrib PR to be made and linked shortly. Contrib Repo Paired Pull Request: https://github.com/opencv/opencv_contrib/pull/4017 --- .../include/opencv2/video/background_segm.hpp | 27 +++++ modules/video/src/bgfg_KNN.cpp | 36 +++++- modules/video/src/bgfg_gaussmix2.cpp | 35 +++++- modules/video/test/test_bgfg2.cpp | 108 ++++++++++++++++++ samples/python/background_subtractor_mask.py | 68 +++++++++++ 5 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 modules/video/test/test_bgfg2.cpp create mode 100644 samples/python/background_subtractor_mask.py diff --git a/modules/video/include/opencv2/video/background_segm.hpp b/modules/video/include/opencv2/video/background_segm.hpp index e1dfa15a9a..73409f27d4 100644 --- a/modules/video/include/opencv2/video/background_segm.hpp +++ b/modules/video/include/opencv2/video/background_segm.hpp @@ -71,6 +71,21 @@ public: */ CV_WRAP virtual void apply(InputArray image, OutputArray fgmask, double learningRate=-1) = 0; + /** @brief Computes a foreground mask with known foreground mask input. + + @param image Next video frame. Floating point frame will be used without scaling and should be in range \f$[0,255]\f$. + @param fgmask The output foreground mask as an 8-bit binary image. + @param knownForegroundMask The mask for inputting already known foreground, allows model to ignore pixels. + @param learningRate The value between 0 and 1 that indicates how fast the background model is + learnt. Negative parameter value makes the algorithm to use some automatically chosen learning + rate. 0 means that the background model is not updated at all, 1 means that the background model + is completely reinitialized from the last frame. + + @note This method has a default virtual implementation that throws a "not impemented" error. + Foreground masking may not be supported by all background subtractors. + */ + CV_WRAP virtual void apply(InputArray image, InputArray knownForegroundMask, OutputArray fgmask, double learningRate=-1) = 0; + /** @brief Computes a background image. @param backgroundImage The output background image. @@ -206,6 +221,18 @@ public: is completely reinitialized from the last frame. */ CV_WRAP virtual void apply(InputArray image, OutputArray fgmask, double learningRate=-1) CV_OVERRIDE = 0; + + /** @brief Computes a foreground mask and skips known foreground in evaluation. + + @param image Next video frame. Floating point frame will be used without scaling and should be in range \f$[0,255]\f$. + @param fgmask The output foreground mask as an 8-bit binary image. + @param knownForegroundMask The mask for inputting already known foreground, allows model to ignore pixels. + @param learningRate The value between 0 and 1 that indicates how fast the background model is + learnt. Negative parameter value makes the algorithm to use some automatically chosen learning + rate. 0 means that the background model is not updated at all, 1 means that the background model + is completely reinitialized from the last frame. + */ + CV_WRAP virtual void apply(InputArray image, InputArray knownForegroundMask, OutputArray fgmask, double learningRate=-1) CV_OVERRIDE = 0; }; /** @brief Creates MOG2 Background Subtractor diff --git a/modules/video/src/bgfg_KNN.cpp b/modules/video/src/bgfg_KNN.cpp index 5ec2266921..cdf4eb3a51 100644 --- a/modules/video/src/bgfg_KNN.cpp +++ b/modules/video/src/bgfg_KNN.cpp @@ -132,6 +132,8 @@ public: //! the update operator void apply(InputArray image, OutputArray fgmask, double learningRate) CV_OVERRIDE; + void apply(InputArray image, InputArray knownForegroundMask, OutputArray fgmask, double learningRate) CV_OVERRIDE; + //! computes a background image which are the mean of all background gaussians virtual void getBackgroundImage(OutputArray backgroundImage) const CV_OVERRIDE; @@ -526,7 +528,9 @@ public: int _nkNN, float _fTau, bool _bShadowDetection, - uchar _nShadowDetection) + uchar _nShadowDetection, + const Mat& _knownForegroundMask) + : knownForegroundMask(_knownForegroundMask) { src = &_src; dst = &_dst; @@ -587,6 +591,17 @@ public: m_nShortCounter, include ); + // Check that foreground mask exists + if (!knownForegroundMask.empty()) { + // If input mask states pixel is foreground + if (knownForegroundMask.at(y, x) > 0) + { + mask[x] = 255; // ensure output mask marks this pixel as FG + data += nchannels; + m_aModel += m_nN*3*ndata; + continue; + } + } switch (result) { case 0: @@ -626,6 +641,7 @@ public: int m_nkNN; bool m_bShadowDetection; uchar m_nShadowDetection; + const Mat& knownForegroundMask; }; #ifdef HAVE_OPENCL @@ -728,7 +744,12 @@ void BackgroundSubtractorKNNImpl::create_ocl_apply_kernel() #endif -void BackgroundSubtractorKNNImpl::apply(InputArray _image, OutputArray _fgmask, double learningRate) +// Base 3 version class +void BackgroundSubtractorKNNImpl::apply(InputArray _image, OutputArray _fgmask, double learningRate) { + apply(_image, noArray(), _fgmask, learningRate); +} + +void BackgroundSubtractorKNNImpl::apply(InputArray _image, InputArray _knownForegroundMask, OutputArray _fgmask, double learningRate) { CV_INSTRUMENT_REGION(); @@ -757,6 +778,14 @@ void BackgroundSubtractorKNNImpl::apply(InputArray _image, OutputArray _fgmask, _fgmask.create( image.size(), CV_8U ); Mat fgmask = _fgmask.getMat(); + Mat knownForegroundMask = _knownForegroundMask.getMat(); + + if(!knownForegroundMask.empty()) + { + CV_Assert(knownForegroundMask.type() == CV_8UC1); + CV_Assert(knownForegroundMask.size() == image.size()); + } + ++nframes; learningRate = learningRate >= 0 && nframes > 1 ? learningRate : 1./std::min( 2*nframes, history ); CV_Assert(learningRate >= 0); @@ -791,7 +820,8 @@ void BackgroundSubtractorKNNImpl::apply(InputArray _image, OutputArray _fgmask, nkNN, fTau, bShadowDetection, - nShadowDetection), + nShadowDetection, + knownForegroundMask), image.total()/(double)(1 << 16)); nShortCounter++;//0,1,...,nShortUpdate-1 diff --git a/modules/video/src/bgfg_gaussmix2.cpp b/modules/video/src/bgfg_gaussmix2.cpp index 00a65fcf8f..48d8ee8286 100644 --- a/modules/video/src/bgfg_gaussmix2.cpp +++ b/modules/video/src/bgfg_gaussmix2.cpp @@ -178,6 +178,8 @@ public: //! the update operator void apply(InputArray image, OutputArray fgmask, double learningRate) CV_OVERRIDE; + void apply(InputArray image, InputArray knownForegroundMask, OutputArray fgmask, double learningRate) CV_OVERRIDE; + //! computes a background image which are the mean of all background gaussians virtual void getBackgroundImage(OutputArray backgroundImage) const CV_OVERRIDE; @@ -546,7 +548,8 @@ public: float _Tb, float _TB, float _Tg, float _varInit, float _varMin, float _varMax, float _prune, float _tau, bool _detectShadows, - uchar _shadowVal) + uchar _shadowVal, const Mat& _knownForegroundMask) + : knownForegroundMask(_knownForegroundMask) { src = &_src; dst = &_dst; @@ -590,6 +593,18 @@ public: for( int x = 0; x < ncols; x++, data += nchannels, gmm += nmixtures, mean += nmixtures*nchannels ) { + + // Check that foreground mask exists + if (!knownForegroundMask.empty()) + { + // If input mask states pixel is foreground + if (knownForegroundMask.at(y, x) > 0) + { + mask[x] = 255; // ensure output mask marks this pixel as FG + continue; + } + } + //calculate distances to the modes (+ sort) //here we need to go in descending order!!! bool background = false;//return value -> true - the pixel classified as background @@ -766,6 +781,7 @@ public: bool detectShadows; uchar shadowVal; + const Mat& knownForegroundMask; }; #ifdef HAVE_OPENCL @@ -844,7 +860,12 @@ void BackgroundSubtractorMOG2Impl::create_ocl_apply_kernel() #endif -void BackgroundSubtractorMOG2Impl::apply(InputArray _image, OutputArray _fgmask, double learningRate) +// Base 3 version class +void BackgroundSubtractorMOG2Impl::apply(InputArray _image, OutputArray _fgmask, double learningRate) { + apply(_image, noArray(), _fgmask, learningRate); +} + +void BackgroundSubtractorMOG2Impl::apply(InputArray _image, InputArray _knownForegroundMask, OutputArray _fgmask, double learningRate) { CV_INSTRUMENT_REGION(); @@ -867,6 +888,14 @@ void BackgroundSubtractorMOG2Impl::apply(InputArray _image, OutputArray _fgmask, _fgmask.create( image.size(), CV_8U ); Mat fgmask = _fgmask.getMat(); + Mat knownForegroundMask = _knownForegroundMask.getMat(); + + if(!knownForegroundMask.empty()) + { + CV_Assert(knownForegroundMask.type() == CV_8UC1); + CV_Assert(knownForegroundMask.size() == image.size()); + } + ++nframes; learningRate = learningRate >= 0 && nframes > 1 ? learningRate : 1./std::min( 2*nframes, history ); CV_Assert(learningRate >= 0); @@ -879,7 +908,7 @@ void BackgroundSubtractorMOG2Impl::apply(InputArray _image, OutputArray _fgmask, (float)varThreshold, backgroundRatio, varThresholdGen, fVarInit, fVarMin, fVarMax, float(-learningRate*fCT), fTau, - bShadowDetection, nShadowDetection), + bShadowDetection, nShadowDetection, knownForegroundMask), image.total()/(double)(1 << 16)); } diff --git a/modules/video/test/test_bgfg2.cpp b/modules/video/test/test_bgfg2.cpp new file mode 100644 index 0000000000..eda022abf4 --- /dev/null +++ b/modules/video/test/test_bgfg2.cpp @@ -0,0 +1,108 @@ +// 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 "test_precomp.hpp" +#include "opencv2/video/background_segm.hpp" + +namespace opencv_test { namespace { + +using namespace cv; + +///////////////////////// MOG2 ////////////////////////////// +TEST(BackgroundSubtractorMOG2, KnownForegroundMaskShadowsTrue) +{ + Ptr mog2 = createBackgroundSubtractorMOG2(500, 16, true); + + //Black Frame + Mat input = Mat::zeros(480,640 , CV_8UC3); + + //White Rectangle + Mat knownFG = Mat::zeros(input.size(), CV_8U); + + rectangle(knownFG, Rect(3,3,5,5), Scalar(255,255,255), -1); + + Mat output; + mog2->apply(input, knownFG, output); + + for(int y = 3; y < 8; y++) + { + for (int x = 3; x < 8; x++){ + EXPECT_EQ(255,output.at(y,x)) << "Expected foreground at (" << x << "," << y << ")"; + } + } +} + +TEST(BackgroundSubtractorMOG2, KnownForegroundMaskShadowsFalse) +{ + Ptr mog2 = createBackgroundSubtractorMOG2(500, 16, false); + + //Black Frame + Mat input = Mat::zeros(480,640 , CV_8UC3); + + //White Rectangle + Mat knownFG = Mat::zeros(input.size(), CV_8U); + + rectangle(knownFG, Rect(3,3,5,5), Scalar(255,255,255), FILLED); + + Mat output; + mog2->apply(input, knownFG, output); + + for(int y = 3; y < 8; y++) + { + for (int x = 3; x < 8; x++){ + EXPECT_EQ(255,output.at(y,x)) << "Expected foreground at (" << x << "," << y << ")"; + } + } +} + +///////////////////////// KNN ////////////////////////////// + +TEST(BackgroundSubtractorKNN, KnownForegroundMaskShadowsTrue) +{ + Ptr knn = createBackgroundSubtractorKNN(500, 400.0, true); + + //Black Frame + Mat input = Mat::zeros(480,640 , CV_8UC3); + + //White Rectangle + Mat knownFG = Mat::zeros(input.size(), CV_8U); + + rectangle(knownFG, Rect(3,3,5,5), Scalar(255,255,255), FILLED); + + Mat output; + knn->apply(input, knownFG, output); + + for(int y = 3; y < 8; y++) + { + for (int x = 3; x < 8; x++){ + EXPECT_EQ(255,output.at(y,x)) << "Expected foreground at (" << x << "," << y << ")"; + } + } +} + +TEST(BackgroundSubtractorKNN, KnownForegroundMaskShadowsFalse) +{ + Ptr knn = createBackgroundSubtractorKNN(500, 400.0, false); + + //Black Frame + Mat input = Mat::zeros(480,640 , CV_8UC3); + + //White Rectangle + Mat knownFG = Mat::zeros(input.size(), CV_8U); + + rectangle(knownFG, Rect(3,3,5,5), Scalar(255,255,255), FILLED); + + Mat output; + knn->apply(input, knownFG, output); + + for(int y = 3; y < 8; y++) + { + for (int x = 3; x < 8; x++){ + EXPECT_EQ(255,output.at(y,x)) << "Expected foreground at (" << x << "," << y << ")"; + } + } +} + +}} // namespace +/* End of file. */ diff --git a/samples/python/background_subtractor_mask.py b/samples/python/background_subtractor_mask.py new file mode 100644 index 0000000000..5d4aae4701 --- /dev/null +++ b/samples/python/background_subtractor_mask.py @@ -0,0 +1,68 @@ + +''' +Showcases the use of background subtraction from a live video feed, +aswell as pass through of a known foreground parameter +''' + +# Python 2/3 compatibility +from __future__ import print_function + +import numpy as np +import cv2 as cv + +def main(): + cap = cv.VideoCapture(0) + if not cap.isOpened: + print("Capture source avaialable.") + exit() + + # Create background subtractor + mog2_bg_subtractor = cv.createBackgroundSubtractorMOG2(history=300, varThreshold=50, detectShadows=False) + knn_bg_subtractor = cv.createBackgroundSubtractorKNN(history=300, detectShadows=False) + + frame_count = 0 + # Allows for a frame buffer for the mask to learn pre known foreground + show_count = 10 + + while True: + ret, frame = cap.read() + if not ret: + break + + x = 100 + (frame_count % 10) * 3 + + frame = cv.resize(frame, (640, 480)) + aKnownForegroundMask = np.zeros(frame.shape[:2], dtype=np.uint8) + + # Allow for models to "settle"/learn + if frame_count > show_count: + cv.rectangle(aKnownForegroundMask, (x,200), (x+50,300), 255, -1) + cv.rectangle(aKnownForegroundMask, (540,180), (640,480), 255, -1) + + #MOG2 Subtraction + mog2_with_mask = mog2_bg_subtractor.apply(frame,knownForegroundMask=aKnownForegroundMask) + mog2_without_mask = mog2_bg_subtractor.apply(frame) + + #KNN Subtraction + knn_with_mask = knn_bg_subtractor.apply(frame,knownForegroundMask=aKnownForegroundMask) + knn_without_mask = knn_bg_subtractor.apply(frame) + + # Display the 3 parameter apply and the 4 parameter apply for both subtractors + cv.imshow("MOG2 With a Foreground Mask", mog2_with_mask) + cv.imshow("MOG2 Without a Foreground Mask", mog2_without_mask) + cv.imshow("KNN With a Foreground Mask", knn_with_mask) + cv.imshow("KNN Without a Foreground Mask", knn_without_mask) + + key = cv.waitKey(30) + if key == 27: # ESC + break + + frame_count += 1 + + cap.release() + cv.destroyAllWindows() + +if __name__ == '__main__': + print(__doc__) + main() + cv.destroyAllWindows() From 9e515caeac2e80fe2d04837f6abcacccbe404107 Mon Sep 17 00:00:00 2001 From: Victor Getmanskiy Date: Thu, 2 Oct 2025 10:53:29 -0700 Subject: [PATCH 11/29] - Fixed incorrect implementation of multithreaded call for warp perspective in IPP HAL. - Changed splitting logic to improved performance for all warp related functions in IPP HAL for multithreaded mode. - Removed IPP 7.0 preprocessor condition from warp in IPP HAL, since unsupported. - Added preprocessor condition to enforce IPP warp calls for custom builds. --- hal/ipp/include/ipp_hal_imgproc.hpp | 8 +- hal/ipp/src/precomp_ipp.hpp | 26 ++ hal/ipp/src/warp_ipp.cpp | 408 +++++++++++++++++----------- 3 files changed, 278 insertions(+), 164 deletions(-) diff --git a/hal/ipp/include/ipp_hal_imgproc.hpp b/hal/ipp/include/ipp_hal_imgproc.hpp index 29ebee241d..f3d5723bc1 100644 --- a/hal/ipp/include/ipp_hal_imgproc.hpp +++ b/hal/ipp/include/ipp_hal_imgproc.hpp @@ -8,22 +8,19 @@ #include #include "ipp_utils.hpp" -#ifdef HAVE_IPP_IW +#if IPP_VERSION_X100 >= 810 +#if defined(HAVE_IPP_IW) int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int src_width, int src_height, uchar *dst_data, size_t dst_step, int dst_width, int dst_height, const double M[6], int interpolation, int borderType, const double borderValue[4]); - #undef cv_hal_warpAffine #define cv_hal_warpAffine ipp_hal_warpAffine #endif -#if IPP_VERSION_X100 >= 810 int ipp_hal_warpPerspective(int src_type, const uchar *src_data, size_t src_step, int src_width, int src_height, uchar *dst_data, size_t dst_step, int dst_width, int dst_height, const double M[9], int interpolation, int borderType, const double borderValue[4]); #undef cv_hal_warpPerspective #define cv_hal_warpPerspective ipp_hal_warpPerspective -#endif - int ipp_hal_remap32f(int src_type, const uchar *src_data, size_t src_step, int src_width, int src_height, uchar *dst_data, size_t dst_step, int dst_width, int dst_height, @@ -32,5 +29,6 @@ int ipp_hal_remap32f(int src_type, const uchar *src_data, size_t src_step, int s #undef cv_hal_remap32f #define cv_hal_remap32f ipp_hal_remap32f +#endif //IPP_VERSION_X100 >= 810 #endif //__IPP_HAL_IMGPROC_HPP__ diff --git a/hal/ipp/src/precomp_ipp.hpp b/hal/ipp/src/precomp_ipp.hpp index bff2d499b1..8136c2869d 100644 --- a/hal/ipp/src/precomp_ipp.hpp +++ b/hal/ipp/src/precomp_ipp.hpp @@ -73,11 +73,37 @@ static inline int ippiSuggestThreadsNum(size_t width, size_t height, size_t elem return 1; } +static inline int ippiSuggestRowThreadsNum(size_t width, size_t height, size_t elemSize, size_t payloadSize) +{ + int num_threads = cv::getNumThreads(); + if(num_threads > 1) + { + long rowThreads = static_cast(height); + + // row-based range shall not allow to split rows + num_threads = (rowThreads < num_threads) ? rowThreads : num_threads; + long rows_per_thread = (rowThreads + num_threads - 1) / num_threads; + size_t item_size = width * elemSize; // row size in bytes + + if(static_cast(item_size * rows_per_thread) < payloadSize) + { + long items_per_thread = IPP_MAX(1L, static_cast(payloadSize / item_size )); + num_threads = static_cast((height + items_per_thread - 1L) / items_per_thread); + } + } + return num_threads; +} + #ifdef HAVE_IPP_IW static inline int ippiSuggestThreadsNum(const ::ipp::IwiImage &image, double multiplier) { return ippiSuggestThreadsNum(image.m_size.width, image.m_size.height, image.m_typeSize*image.m_channels, multiplier); } + +static inline int ippiSuggestRowThreadsNum(const ::ipp::IwiImage &image, size_t payloadSize) +{ + return ippiSuggestRowThreadsNum(image.m_size.width, image.m_size.height, image.m_typeSize*image.m_channels, payloadSize); +} #endif #endif //__PRECOMP_IPP_HPP__ diff --git a/hal/ipp/src/warp_ipp.cpp b/hal/ipp/src/warp_ipp.cpp index 867d28d954..63f3dfddd0 100644 --- a/hal/ipp/src/warp_ipp.cpp +++ b/hal/ipp/src/warp_ipp.cpp @@ -4,24 +4,28 @@ #include "ipp_hal_imgproc.hpp" +#if IPP_VERSION_X100 >= 810 // integrated IPP warping/remap ABI is available since IPP v8.1 + #include -#include #include "precomp_ipp.hpp" -#ifdef HAVE_IPP_IW -#include "iw++/iw.hpp" -#endif +// Uncomment to enforce IPP calls for all supported by IPP configurations +// #define IPP_CALLS_ENFORCED -#define IPP_WARPAFFINE_PARALLEL 1 +#define CV_IPP_SAFE_CALL(pFunc, pFlag, ...) if (pFunc(__VA_ARGS__) != ippStsNoErr) {*pFlag = false; return;} #define CV_TYPE(src_type) (src_type & (CV_DEPTH_MAX - 1)) + #ifdef HAVE_IPP_IW +// Warp affine section + +#include "iw++/iw.hpp" class ipp_warpAffineParallel: public cv::ParallelLoopBody { public: ipp_warpAffineParallel(::ipp::IwiImage &src, ::ipp::IwiImage &dst, IppiInterpolationType _inter, double (&_coeffs)[2][3], ::ipp::IwiBorderType _borderType, IwTransDirection _iwTransDirection, bool *_ok):m_src(src), m_dst(dst) { - pOk = _ok; + ok = _ok; inter = _inter; borderType = _borderType; @@ -31,15 +35,14 @@ public: for( int j = 0; j < 3; j++ ) coeffs[i][j] = _coeffs[i][j]; - *pOk = true; + *ok = true; } ~ipp_warpAffineParallel() {} virtual void operator() (const cv::Range& range) const CV_OVERRIDE { //CV_INSTRUMENT_REGION_IPP(); - - if(*pOk == false) + if(*ok == false) return; try @@ -49,9 +52,10 @@ public: } catch(const ::ipp::IwException &) { - *pOk = false; + *ok = false; return; } + CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); } private: ::ipp::IwiImage &m_src; @@ -62,20 +66,29 @@ private: ::ipp::IwiBorderType borderType; IwTransDirection iwTransDirection; - bool *pOk; + bool *ok; const ipp_warpAffineParallel& operator= (const ipp_warpAffineParallel&); }; -#if (IPP_VERSION_X100 >= 700) int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int src_width, int src_height, uchar *dst_data, size_t dst_step, int dst_width, int dst_height, const double M[6], int interpolation, int borderType, const double borderValue[4]) { //CV_INSTRUMENT_REGION_IPP(); - IppiInterpolationType ippInter = ippiGetInterpolation(interpolation); + IppiInterpolationType ippInter = ippiGetInterpolation(interpolation); if((int)ippInter < 0 || interpolation > 2) return CV_HAL_ERROR_NOT_IMPLEMENTED; +#if defined(IPP_CALLS_ENFORCED) + /* C1 C2 C3 C4 */ + char impl[CV_DEPTH_MAX][4][3]={{{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //8U + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8S + {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //16U + {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //16S + {{1, 1, 0}, {0, 0, 0}, {1, 1, 0}, {1, 1, 0}}, //32S + {{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 /* 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 @@ -84,6 +97,7 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int {{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 +#endif if(impl[CV_TYPE(src_type)][CV_MAT_CN(src_type)-1][interpolation] == 0) { @@ -107,21 +121,24 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int for( int j = 0; j < 3; j++ ) coeffs[i][j] = M[i*3 + j]; - const int threads = ippiSuggestThreadsNum(iwDst, 2); + int min_payload = 1 << 16; // 64KB shall be minimal per thread to maximize scalability for warping functions + const int threads = ippiSuggestRowThreadsNum(iwDst, min_payload); - if(IPP_WARPAFFINE_PARALLEL && threads > 1) + if (threads > 1) { - bool ok = true; + bool ok = true; cv::Range range(0, (int)iwDst.m_size.height); ipp_warpAffineParallel invoker(iwSrc, iwDst, ippInter, coeffs, ippBorder, iwTransDirection, &ok); if(!ok) return CV_HAL_ERROR_NOT_IMPLEMENTED; - parallel_for_(range, invoker, threads*4); + parallel_for_(range, invoker, threads); if(!ok) return CV_HAL_ERROR_NOT_IMPLEMENTED; - } else { + } + else + { CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpAffine, iwSrc, iwDst, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpAffineParams(), ippBorder); } } @@ -132,8 +149,10 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int return CV_HAL_ERROR_OK; } -#endif -#endif + +#endif // HAVE_IPP_IW + +// End of Warp affine section typedef IppStatus (CV_STDCALL* ippiSetFunc)(const void*, void *, int, IppiSize); @@ -194,18 +213,46 @@ static bool IPPSet(const double value[4], void *dataPointer, int step, IppiSize return false; } -#if (IPP_VERSION_X100 >= 810) +// Warp perspective section + typedef IppStatus (CV_STDCALL* ippiWarpPerspectiveFunc)(const Ipp8u*, int, Ipp8u*, int,IppiPoint, IppiSize, const IppiWarpSpec*,Ipp8u*); +typedef IppStatus (CV_STDCALL* ippiWarpPerspectiveInitFunc)(IppiSize, IppiRect, IppiSize, IppDataType,const double [3][3], IppiWarpDirection, int, IppiBorderType, const Ipp64f *, int, IppiWarpSpec*); class IPPWarpPerspectiveInvoker : public cv::ParallelLoopBody { + // Mem object ot simplify IPP memory lifetime control + struct IPPWarpPerspectiveMem + { + IppiWarpSpec* pSpec = nullptr; + Ipp8u* pBuffer = nullptr; + + IPPWarpPerspectiveMem() = default; + IPPWarpPerspectiveMem (const IPPWarpPerspectiveMem&) = delete; + IPPWarpPerspectiveMem& operator= (const IPPWarpPerspectiveMem&) = delete; + + void AllocateSpec(int size) + { + pSpec = (IppiWarpSpec*)ippMalloc_L(size); + } + + void AllocateBuffer(int size) + { + pBuffer = (Ipp8u*)ippMalloc_L(size); + } + + ~IPPWarpPerspectiveMem() + { + if (nullptr != pSpec) ippFree(pSpec); + if (nullptr != pBuffer) ippFree(pBuffer); + } + }; public: IPPWarpPerspectiveInvoker(int _src_type, cv::Mat &_src, size_t _src_step, cv::Mat &_dst, size_t _dst_step, IppiInterpolationType _interpolation, - double (&_coeffs)[3][3], int &_borderType, const double _borderValue[4], ippiWarpPerspectiveFunc _func, + double (&_coeffs)[3][3], int &_borderType, const double _borderValue[4], ippiWarpPerspectiveFunc _func, ippiWarpPerspectiveInitFunc _initFunc, bool *_ok) : ParallelLoopBody(), src_type(_src_type), src(_src), src_step(_src_step), dst(_dst), dst_step(_dst_step), inter(_interpolation), coeffs(_coeffs), - borderType(_borderType), func(_func), ok(_ok) + borderType(_borderType), func(_func), initFunc(_initFunc), ok(_ok) { memcpy(this->borderValue, _borderValue, sizeof(this->borderValue)); *ok = true; @@ -214,9 +261,13 @@ public: virtual void operator() (const cv::Range& range) const CV_OVERRIDE { //CV_INSTRUMENT_REGION_IPP(); - IppiWarpSpec* pSpec = 0; - int specSize = 0, initSize = 0, bufSize = 0; Ipp8u* pBuffer = 0; - IppiPoint dstRoiOffset = {0, 0}; + if (*ok == false) + return; + + IPPWarpPerspectiveMem mem; + + int specSize = 0, initSize = 0, bufSize = 0; + IppiWarpDirection direction = ippWarpBackward; //fixed for IPP const Ipp32u numChannels = CV_MAT_CN(src_type); @@ -225,48 +276,34 @@ public: IppiRect srcroi = {0, 0, src.cols, src.rows}; /* Spec and init buffer sizes */ - IppStatus status = ippiWarpPerspectiveGetSize(srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, inter, ippWarpBackward, ippiGetBorderType(borderType), &specSize, &initSize); + CV_IPP_SAFE_CALL(ippiWarpPerspectiveGetSize, ok, srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, inter, ippWarpBackward, ippiGetBorderType(borderType), &specSize, &initSize); - pSpec = (IppiWarpSpec*)ippMalloc_L(specSize); + mem.AllocateSpec(specSize); - if (inter == ippLinear) + CV_IPP_SAFE_CALL(initFunc, ok, srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, direction, numChannels, ippiGetBorderType(borderType), + borderValue, 0, mem.pSpec); + + CV_IPP_SAFE_CALL(ippiWarpGetBufferSize, ok, mem.pSpec, dstsize, &bufSize); + + mem.AllocateBuffer(bufSize); + + IppiPoint dstRoiOffset = {0, range.start}; + IppiSize dstRoiSize = {dst.cols, range.size()}; + auto* pDst = dst.ptr(range.start); + if (borderType == cv::BorderTypes::BORDER_CONSTANT && + !IPPSet(borderValue, pDst, (int)dst_step, dstRoiSize, src.channels(), src.depth())) { - status = ippiWarpPerspectiveLinearInit(srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, direction, numChannels, ippiGetBorderType(borderType), - borderValue, 0, pSpec); - } else - { - status = ippiWarpPerspectiveNearestInit(srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, direction, numChannels, ippiGetBorderType(borderType), - borderValue, 0, pSpec); - } - - status = ippiWarpGetBufferSize(pSpec, dstsize, &bufSize); - pBuffer = (Ipp8u*)ippMalloc_L(bufSize); - IppiSize dstRoiSize = dstsize; - - int cnn = src.channels(); - - if( borderType == cv::BorderTypes::BORDER_CONSTANT ) - { - IppiSize setSize = {dst.cols, range.end - range.start}; - void *dataPointer = dst.ptr(range.start); - if( !IPPSet( borderValue, dataPointer, (int)dst.step[0], setSize, cnn, src.depth() ) ) - { - *ok = false; - ippsFree(pBuffer); - ippsFree(pSpec); - return; - } - } - - status = CV_INSTRUMENT_FUN_IPP(func, src.ptr(), (int)src_step, dst.ptr(), (int)dst_step, dstRoiOffset, dstRoiSize, pSpec, pBuffer); - if (status != ippStsNoErr) *ok = false; - else - { - CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); + return; } - ippsFree(pBuffer); - ippsFree(pSpec); + + if (ippStsNoErr != CV_INSTRUMENT_FUN_IPP(func, src.ptr(), (int)src_step, pDst, (int)dst_step, dstRoiOffset, dstRoiSize, mem.pSpec, mem.pBuffer)) + { + *ok = false; + return; + } + + CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); } private: int src_type; @@ -279,8 +316,8 @@ private: int borderType; double borderValue[4]; ippiWarpPerspectiveFunc func; + ippiWarpPerspectiveInitFunc initFunc; bool *ok; - const IPPWarpPerspectiveInvoker& operator= (const IPPWarpPerspectiveInvoker&); }; @@ -288,87 +325,114 @@ int ipp_hal_warpPerspective(int src_type, const uchar *src_data, size_t src_step int dst_width, int dst_height, const double M[9], int interpolation, int borderType, const double borderValue[4]) { //CV_INSTRUMENT_REGION_IPP(); - ippiWarpPerspectiveFunc ippFunc = 0; + + if (src_height <= 1 || src_width <= 1) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + + ippiWarpPerspectiveFunc ippFunc = nullptr; + ippiWarpPerspectiveInitFunc ippInitFunc = nullptr; + if (interpolation == cv::InterpolationFlags::INTER_NEAREST) { - ippFunc = src_type == CV_8UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C1R : - src_type == CV_8UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C3R : - src_type == CV_8UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C4R : - src_type == CV_16UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C1R : - src_type == CV_16UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C3R : - src_type == CV_16UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C4R : - src_type == CV_16SC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C1R : - src_type == CV_16SC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C3R : - src_type == CV_16SC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C4R : - src_type == CV_32FC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C1R : - src_type == CV_32FC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C3R : - src_type == CV_32FC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C4R : 0; + ippInitFunc = ippiWarpPerspectiveNearestInit; + ippFunc = + src_type == CV_8UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C1R : + src_type == CV_8UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C3R : + src_type == CV_8UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_8u_C4R : + src_type == CV_16UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C1R : + src_type == CV_16UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C3R : + src_type == CV_16UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16u_C4R : + src_type == CV_16SC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C1R : + src_type == CV_16SC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C3R : + src_type == CV_16SC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_16s_C4R : + src_type == CV_32FC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C1R : + src_type == CV_32FC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C3R : + src_type == CV_32FC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveNearest_32f_C4R : nullptr; } else if (interpolation == cv::InterpolationFlags::INTER_LINEAR) { - ippFunc = src_type == CV_8UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C1R : - src_type == CV_8UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C3R : - src_type == CV_8UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C4R : - src_type == CV_16UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C1R : - src_type == CV_16UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C3R : - src_type == CV_16UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C4R : - src_type == CV_16SC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C1R : - src_type == CV_16SC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C3R : - src_type == CV_16SC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C4R : - src_type == CV_32FC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C1R : - src_type == CV_32FC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C3R : - src_type == CV_32FC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C4R : 0; + ippInitFunc = ippiWarpPerspectiveLinearInit; + ippFunc = + src_type == CV_8UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C1R : + src_type == CV_8UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C3R : + src_type == CV_8UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_8u_C4R : + src_type == CV_16UC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C1R : + src_type == CV_16UC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C3R : + src_type == CV_16UC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16u_C4R : + src_type == CV_16SC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C1R : + src_type == CV_16SC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C3R : + src_type == CV_16SC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_16s_C4R : + src_type == CV_32FC1 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C1R : + src_type == CV_32FC3 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C3R : + src_type == CV_32FC4 ? (ippiWarpPerspectiveFunc)ippiWarpPerspectiveLinear_32f_C4R : nullptr; } else { return CV_HAL_ERROR_NOT_IMPLEMENTED; } - if(src_height == 1 || src_width == 1) return CV_HAL_ERROR_NOT_IMPLEMENTED; - - int mode = - interpolation == cv::InterpolationFlags::INTER_NEAREST ? IPPI_INTER_NN : - interpolation == cv::InterpolationFlags::INTER_LINEAR ? IPPI_INTER_LINEAR : 0; - - if (mode == 0 || ippFunc == 0) + if (ippFunc == nullptr) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } +#if defined(IPP_CALLS_ENFORCED) /* C1 C2 C3 C4 */ - char impl[CV_DEPTH_MAX][4][2]={{{0, 0}, {1, 1}, {0, 0}, {0, 0}}, //8U - {{1, 1}, {1, 1}, {1, 1}, {1, 1}}, //8S - {{0, 0}, {1, 1}, {0, 1}, {0, 1}}, //16U - {{1, 1}, {1, 1}, {1, 1}, {1, 1}}, //16S - {{1, 1}, {1, 1}, {1, 0}, {1, 1}}, //32S - {{1, 0}, {1, 0}, {0, 0}, {1, 0}}, //32F - {{1, 1}, {1, 1}, {1, 1}, {1, 1}}}; //64F + char impl[CV_DEPTH_MAX][4][2]={{{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //8U + {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, //8S + {{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //16U + {{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //16S + {{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //32S + {{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //32F + {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}; //64F +#else // IPP_CALLS_ENFORCED is not defined, results are strictly aligned to OpenCV implementation + /* C1 C2 C3 C4 */ + char impl[CV_DEPTH_MAX][4][2]={{{0, 0}, {0, 0}, {0, 0}, {0, 0}}, //8U + {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, //8S + {{0, 0}, {0, 0}, {0, 1}, {0, 1}}, //16U + {{1, 1}, {0, 0}, {1, 1}, {1, 1}}, //16S + {{1, 1}, {0, 0}, {1, 0}, {1, 1}}, //32S + {{1, 0}, {0, 0}, {0, 0}, {1, 0}}, //32F + {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}; //64F +#endif + const char type_size[CV_DEPTH_MAX] = {1,1,2,2,4,4,8}; if(impl[CV_TYPE(src_type)][CV_MAT_CN(src_type)-1][interpolation] == 0) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } - double coeffs[3][3]; for( int i = 0; i < 3; i++ ) for( int j = 0; j < 3; j++ ) coeffs[i][j] = M[i*3 + j]; - bool ok; + bool ok = true; cv::Range range(0, dst_height); cv::Mat src(cv::Size(src_width, src_height), src_type, const_cast(src_data), src_step); cv::Mat dst(cv::Size(dst_width, dst_height), src_type, dst_data, dst_step); - IppiInterpolationType ippInter = ippiGetInterpolation(interpolation); - IPPWarpPerspectiveInvoker invoker(src_type, src, src_step, dst, dst_step, ippInter, coeffs, borderType, borderValue, ippFunc, &ok); - parallel_for_(range, invoker, dst.total()/(double)(1<<16)); + IppiInterpolationType ippInter = ippiGetInterpolation(interpolation); - if( ok ) + int min_payload = 1 << 16; // 64KB shall be minimal per thread to maximize scalability for warping functions + int num_threads = ippiSuggestRowThreadsNum(dst_width, dst_height, type_size[CV_TYPE(src_type)]*CV_MAT_CN(src_type), min_payload); + + IPPWarpPerspectiveInvoker invoker(src_type, src, src_step, dst, dst_step, ippInter, coeffs, borderType, borderValue, ippFunc, ippInitFunc, &ok); + + (num_threads > 1) ? parallel_for_(range, invoker, num_threads) : invoker(range); + + if (ok) { - CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); + CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); + return CV_HAL_ERROR_OK; } - return CV_HAL_ERROR_OK; + + return CV_HAL_ERROR_NOT_IMPLEMENTED; } -#endif + +// End of Warp perspective section + +// Remap section typedef IppStatus(CV_STDCALL *ippiRemap)(const void *pSrc, IppiSize srcSize, int srcStep, IppiRect srcRoi, const Ipp32f *pxMap, int xMapStep, const Ipp32f *pyMap, int yMapStep, @@ -391,6 +455,10 @@ public: virtual void operator()(const cv::Range &range) const { + //CV_INSTRUMENT_REGION_IPP(); + if (*ok == false) + return; + IppiRect srcRoiRect = {0, 0, src_width, src_height}; uchar *dst_roi_data = dst + range.start * dst_step; IppiSize dstRoiSize = ippiSize(dst_width, range.size()); @@ -403,14 +471,15 @@ public: return; } - if (CV_INSTRUMENT_FUN_IPP(ippFunc, src, {src_width, src_height}, (int)src_step, srcRoiRect, + if (ippStsNoErr != CV_INSTRUMENT_FUN_IPP(ippFunc, src, {src_width, src_height}, (int)src_step, srcRoiRect, mapx, (int)mapx_step, mapy, (int)mapy_step, - dst_roi_data, (int)dst_step, dstRoiSize, ippInterpolation) < 0) - *ok = false; - else + dst_roi_data, (int)dst_step, dstRoiSize, ippInterpolation)) { - CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); + *ok = false; + return; } + + CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); } private: @@ -436,53 +505,74 @@ int ipp_hal_remap32f(int src_type, const uchar *src_data, size_t src_step, int s float *mapx, size_t mapx_step, float *mapy, size_t mapy_step, int interpolation, int border_type, const double border_value[4]) { - if ((interpolation == cv::INTER_LINEAR || interpolation == cv::INTER_CUBIC || interpolation == cv::INTER_NEAREST) && - (border_type == cv::BORDER_CONSTANT || border_type == cv::BORDER_TRANSPARENT)) + if (!((interpolation == cv::INTER_LINEAR || interpolation == cv::INTER_CUBIC || interpolation == cv::INTER_NEAREST) && + (border_type == cv::BORDER_CONSTANT || border_type == cv::BORDER_TRANSPARENT))) { - int ippInterpolation = - interpolation == cv::INTER_NEAREST ? IPPI_INTER_NN : interpolation == cv::INTER_LINEAR ? IPPI_INTER_LINEAR - : IPPI_INTER_CUBIC; + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } - /* 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}, {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 - {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}}; //64F + int ippInterpolation = ippiGetInterpolation(interpolation); - if (impl[CV_TYPE(src_type)][CV_MAT_CN(src_type) - 1][interpolation] == 0) +#if defined(IPP_CALLS_ENFORCED) + /* C1 C2 C3 C4 */ + char impl[CV_DEPTH_MAX][4][3] = {{{1, 1, 1}, {0, 0, 0}, {1, 1, 1}, {1, 1, 1}}, //8U + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //8S + {{1, 1, 1}, {0, 0, 0}, {1, 1, 1}, {1, 1, 1}}, //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 + {{1, 1, 1}, {0, 0, 0}, {1, 1, 1}, {1, 1, 1}}, //32F + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}}; //64F +#else // IPP_CALLS_ENFORCED is not defined, results are strictly aligned to OpenCV implementation + /* 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}, {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 + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}}; //64F +#endif + const char type_size[CV_DEPTH_MAX] = {1,1,2,2,4,4,8}; + + if (impl[CV_TYPE(src_type)][CV_MAT_CN(src_type) - 1][interpolation] == 0) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + + ippiRemap ippFunc = + src_type == CV_8UC1 ? (ippiRemap)ippiRemap_8u_C1R : + src_type == CV_8UC3 ? (ippiRemap)ippiRemap_8u_C3R : + src_type == CV_8UC4 ? (ippiRemap)ippiRemap_8u_C4R : + src_type == CV_16UC1 ? (ippiRemap)ippiRemap_16u_C1R : + src_type == CV_16UC3 ? (ippiRemap)ippiRemap_16u_C3R : + src_type == CV_16UC4 ? (ippiRemap)ippiRemap_16u_C4R : + src_type == CV_32FC1 ? (ippiRemap)ippiRemap_32f_C1R : + src_type == CV_32FC3 ? (ippiRemap)ippiRemap_32f_C3R : + src_type == CV_32FC4 ? (ippiRemap)ippiRemap_32f_C4R : 0; + + if (ippFunc) + { + bool ok = true; + + IPPRemapInvoker invoker(src_type, src_data, src_step, src_width, src_height, dst_data, dst_step, dst_width, + mapx, mapx_step, mapy, mapy_step, ippFunc, ippInterpolation, border_type, border_value, &ok); + cv::Range range(0, dst_height); + + int min_payload = 1 << 16; // 64KB shall be minimal per thread to maximize scalability for warping functions + int num_threads = ippiSuggestRowThreadsNum(dst_width, dst_height, type_size[CV_TYPE(src_type)]*CV_MAT_CN(src_type), min_payload); + + cv::parallel_for_(range, invoker, num_threads); + + if (ok) { - return CV_HAL_ERROR_NOT_IMPLEMENTED; - } - - ippiRemap ippFunc = - src_type == CV_8UC1 ? (ippiRemap)ippiRemap_8u_C1R : src_type == CV_8UC3 ? (ippiRemap)ippiRemap_8u_C3R - : src_type == CV_8UC4 ? (ippiRemap)ippiRemap_8u_C4R - : src_type == CV_16UC1 ? (ippiRemap)ippiRemap_16u_C1R - : src_type == CV_16UC3 ? (ippiRemap)ippiRemap_16u_C3R - : src_type == CV_16UC4 ? (ippiRemap)ippiRemap_16u_C4R - : src_type == CV_32FC1 ? (ippiRemap)ippiRemap_32f_C1R - : src_type == CV_32FC3 ? (ippiRemap)ippiRemap_32f_C3R - : src_type == CV_32FC4 ? (ippiRemap)ippiRemap_32f_C4R - : 0; - - if (ippFunc) - { - bool ok; - - IPPRemapInvoker invoker(src_type, src_data, src_step, src_width, src_height, dst_data, dst_step, dst_width, - mapx, mapx_step, mapy, mapy_step, ippFunc, ippInterpolation, border_type, border_value, &ok); - cv::Range range(0, dst_height); - cv::parallel_for_(range, invoker, dst_width * dst_height / (double)(1 << 16)); - - if (ok) - { - CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); - return CV_HAL_ERROR_OK; - } + CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); + return CV_HAL_ERROR_OK; } } + return CV_HAL_ERROR_NOT_IMPLEMENTED; } + +// End of Remap section + +#endif // IPP_VERSION_X100 >= 810 From 1b09d7390f5bc8514c14099951406f074e73d0c7 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Wed, 15 Oct 2025 01:34:14 +0300 Subject: [PATCH 12/29] Fix minEnclosingCircle --- modules/imgproc/src/shapedescr.cpp | 21 ++------------ modules/imgproc/test/test_convhull.cpp | 38 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/modules/imgproc/src/shapedescr.cpp b/modules/imgproc/src/shapedescr.cpp index f2de38168a..75e8582521 100644 --- a/modules/imgproc/src/shapedescr.cpp +++ b/modules/imgproc/src/shapedescr.cpp @@ -62,25 +62,8 @@ static void findCircle3pts(Point2f *pts, Point2f ¢er, float &radius) float det = v1.x * v2.y - v1.y * v2.x; if (fabs(det) <= EPS) { - // v1 and v2 are colinear, so the longest distance between any 2 points - // is the diameter of the minimum enclosing circle. - float d1 = normL2Sqr(pts[0] - pts[1]); - float d2 = normL2Sqr(pts[0] - pts[2]); - float d3 = normL2Sqr(pts[1] - pts[2]); - radius = sqrt(std::max(d1, std::max(d2, d3))) * 0.5f + EPS; - if (d1 >= d2 && d1 >= d3) - { - center = (pts[0] + pts[1]) * 0.5f; - } - else if (d2 >= d1 && d2 >= d3) - { - center = (pts[0] + pts[2]) * 0.5f; - } - else - { - CV_DbgAssert(d3 >= d1 && d3 >= d2); - center = (pts[1] + pts[2]) * 0.5f; - } + // triangle is degenerate, so this is 2-points case + // 2-points case should be taken into account in previous step return; } float cx = (c1 * v2.y - c2 * v1.y) / det; diff --git a/modules/imgproc/test/test_convhull.cpp b/modules/imgproc/test/test_convhull.cpp index 654df9167d..e5d3f8a482 100644 --- a/modules/imgproc/test/test_convhull.cpp +++ b/modules/imgproc/test/test_convhull.cpp @@ -127,6 +127,44 @@ TEST(Imgproc_minEnclosingCircle, regression_16051) { EXPECT_NEAR(2.1024551f, radius, 1e-3); } +TEST(Imgproc_minEnclosingCircle, regression_27891) { + { + const vector pts = { {219, 301}, {639, 635}, {740, 569}, {740, 569}, {309, 123}, {349, 88} }; + + Point2f center; + float radius; + minEnclosingCircle(pts, center, radius); + + EXPECT_NEAR(center.x, 522.476f, 1e-3f); + EXPECT_NEAR(center.y, 346.4029f, 1e-3f); + EXPECT_NEAR(radius, 311.2331f, 1e-3f); + } + + { + const vector pts = { {219, 301}, {639, 635}, {740, 569}, {740, 569}, {349, 88} }; + + Point2f center; + float radius; + minEnclosingCircle(pts, center, radius); + + EXPECT_NEAR(center.x, 522.476f, 1e-3f); + EXPECT_NEAR(center.y, 346.4029f, 1e-3f); + EXPECT_NEAR(radius, 311.2331f, 1e-3f); + } + + { + const vector pts = { {639, 635}, {740, 569}, {740, 569}, {349, 88} }; + + Point2f center; + float radius; + minEnclosingCircle(pts, center, radius); + + EXPECT_NEAR(center.x, 522.476f, 1e-3f); + EXPECT_NEAR(center.y, 346.4029f, 1e-3f); + EXPECT_NEAR(radius, 311.2331f, 1e-3f); + } +} + PARAM_TEST_CASE(ConvexityDefects_regression_5908, bool, int) { public: From 8f0373816aefac01cca3cc2e87166dd5016f66de Mon Sep 17 00:00:00 2001 From: Maxim Smolskiy Date: Wed, 15 Oct 2025 09:48:49 +0300 Subject: [PATCH 13/29] Merge pull request #27900 from MaximSmolskiy:refactor-minEnclosingCircle-tests Refactor minEnclosingCircle tests #27900 ### Pull Request Readiness Checklist Separate input points for tests Before this, next input points depended on previous ones and it was not obvious which input points specific test checked 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 - [x] 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 --- modules/imgproc/test/test_convhull.cpp | 75 ++++++++++++++------------ 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/modules/imgproc/test/test_convhull.cpp b/modules/imgproc/test/test_convhull.cpp index 654df9167d..90889db044 100644 --- a/modules/imgproc/test/test_convhull.cpp +++ b/modules/imgproc/test/test_convhull.cpp @@ -52,10 +52,6 @@ namespace opencv_test { namespace { TEST(minEnclosingCircle, basic_test) { - vector pts; - pts.push_back(Point2f(0, 0)); - pts.push_back(Point2f(10, 0)); - pts.push_back(Point2f(5, 1)); const float EPS = 1.0e-3f; Point2f center; float radius; @@ -64,19 +60,24 @@ TEST(minEnclosingCircle, basic_test) // 2 // 0 1 // NB: The triangle is obtuse, so the only pts[0] and pts[1] are on the circle. - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 5, EPS); - EXPECT_NEAR(center.y, 0, EPS); - EXPECT_NEAR(5, radius, EPS); + { + const vector pts = { {0, 0}, {10, 0}, {5, 1} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 0, EPS); + EXPECT_NEAR(5, radius, EPS); + } // pts[2] is on the circle with diameter pts[0] - pts[1]. // 2 // 0 1 - pts[2] = Point2f(5, 5); - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 5, EPS); - EXPECT_NEAR(center.y, 0, EPS); - EXPECT_NEAR(5, radius, EPS); + { + const vector pts = { {0, 0}, {10, 0}, {5, 5} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 0, EPS); + EXPECT_NEAR(5, radius, EPS); + } // pts[2] is outside the circle with diameter pts[0] - pts[1]. // 2 @@ -84,32 +85,40 @@ TEST(minEnclosingCircle, basic_test) // // 0 1 // NB: The triangle is acute, so all 3 points are on the circle. - pts[2] = Point2f(5, 10); - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 5, EPS); - EXPECT_NEAR(center.y, 3.75, EPS); - EXPECT_NEAR(6.25f, radius, EPS); + { + const vector pts = { {0, 0}, {10, 0}, {5, 10} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 3.75, EPS); + EXPECT_NEAR(6.25f, radius, EPS); + } // The 3 points are colinear. - pts[2] = Point2f(3, 0); - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 5, EPS); - EXPECT_NEAR(center.y, 0, EPS); - EXPECT_NEAR(5, radius, EPS); + { + const vector pts = { {0, 0}, {10, 0}, {3, 0} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 0, EPS); + EXPECT_NEAR(5, radius, EPS); + } // 2 points are the same. - pts[2] = pts[1]; - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 5, EPS); - EXPECT_NEAR(center.y, 0, EPS); - EXPECT_NEAR(5, radius, EPS); + { + const vector pts = { {0, 0}, {10, 0}, {10, 0} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 0, EPS); + EXPECT_NEAR(5, radius, EPS); + } // 3 points are the same. - pts[0] = pts[1]; - minEnclosingCircle(pts, center, radius); - EXPECT_NEAR(center.x, 10, EPS); - EXPECT_NEAR(center.y, 0, EPS); - EXPECT_NEAR(0, radius, EPS); + { + const vector pts = { {10, 0}, {10, 0}, {10, 0} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 10, EPS); + EXPECT_NEAR(center.y, 0, EPS); + EXPECT_NEAR(0, radius, EPS); + } } TEST(Imgproc_minEnclosingCircle, regression_16051) { From e7728bb27db9732629bb3f7c7c2c33fe3930b31a Mon Sep 17 00:00:00 2001 From: Atri Bhattacharya Date: Wed, 15 Oct 2025 21:49:04 +0530 Subject: [PATCH 14/29] Fixed linking for HighGUI against Qt 6.9 and newer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `link_libraries` instead of `add_defintions` to link against Qt 6.9 and newer to avoid un-expanded generator expressions from Qt cmake files being appended to linker flags when building the HighGUI module. The actual bug is likely in how Qt cmake files end up with these un-expanded generator expressions in the first place — see discussion in https://bugreports.qt.io/browse/QTBUG-134774 — but the recommended way to link against the library is to use `link_libraries` anyway, so this fix should do the trick. Fixes issue #27223. --- modules/highgui/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/highgui/CMakeLists.txt b/modules/highgui/CMakeLists.txt index f0a668e4b5..ae9c49c1cf 100644 --- a/modules/highgui/CMakeLists.txt +++ b/modules/highgui/CMakeLists.txt @@ -125,7 +125,7 @@ elseif(HAVE_QT) endif() foreach(dt_dep ${qt_deps}) - add_definitions(${Qt${QT_VERSION_MAJOR}${dt_dep}_DEFINITIONS}) + link_libraries(${Qt${QT_VERSION_MAJOR}${dt_dep}}) include_directories(${Qt${QT_VERSION_MAJOR}${dt_dep}_INCLUDE_DIRS}) list(APPEND HIGHGUI_LIBRARIES ${Qt${QT_VERSION_MAJOR}${dt_dep}_LIBRARIES}) endforeach() From da69f6748e52d3e01240d460dc38c46064966402 Mon Sep 17 00:00:00 2001 From: xybuild Date: Wed, 15 Oct 2025 19:15:01 +0100 Subject: [PATCH 15/29] Fix ambiguous operator error in Rect assignment for C++ modules --- modules/core/include/opencv2/core/types.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/core/include/opencv2/core/types.hpp b/modules/core/include/opencv2/core/types.hpp index df4fd21885..dab6a82dcc 100644 --- a/modules/core/include/opencv2/core/types.hpp +++ b/modules/core/include/opencv2/core/types.hpp @@ -1931,7 +1931,7 @@ template static inline Rect_<_Tp>& operator &= ( Rect_<_Tp>& a, const Rect_<_Tp>& b ) { if (a.empty() || b.empty()) { - a = Rect(); + a = Rect_<_Tp>(); return a; } const Rect_<_Tp>& Rx_min = (a.x < b.x) ? a : b; @@ -1945,7 +1945,7 @@ Rect_<_Tp>& operator &= ( Rect_<_Tp>& a, const Rect_<_Tp>& b ) // Let us first deal with the following case. if ((Rx_min.x < 0 && Rx_min.x + Rx_min.width < Rx_max.x) || (Ry_min.y < 0 && Ry_min.y + Ry_min.height < Ry_max.y)) { - a = Rect(); + a = Rect_<_Tp>(); return a; } // We now know that either Rx_min.x >= 0, or @@ -1957,7 +1957,7 @@ Rect_<_Tp>& operator &= ( Rect_<_Tp>& a, const Rect_<_Tp>& b ) a.x = Rx_max.x; a.y = Ry_max.y; if (a.empty()) - a = Rect(); + a = Rect_<_Tp>(); return a; } From b4d3488b02a6c52b7acec313b103aca422be8ae8 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Wed, 15 Oct 2025 22:27:27 +0300 Subject: [PATCH 16/29] Add corner cases tests for minEnclosingCircle --- modules/imgproc/test/test_convhull.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/imgproc/test/test_convhull.cpp b/modules/imgproc/test/test_convhull.cpp index c230ec96dd..e6466aa283 100644 --- a/modules/imgproc/test/test_convhull.cpp +++ b/modules/imgproc/test/test_convhull.cpp @@ -56,6 +56,22 @@ TEST(minEnclosingCircle, basic_test) Point2f center; float radius; + { + const vector pts = { {5, 10} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 5, EPS); + EXPECT_NEAR(center.y, 10, EPS); + EXPECT_NEAR(radius, 0, EPS); + } + + { + const vector pts = { {5, 10}, {11, 18} }; + minEnclosingCircle(pts, center, radius); + EXPECT_NEAR(center.x, 8, EPS); + EXPECT_NEAR(center.y, 14, EPS); + EXPECT_NEAR(radius, 5, EPS); + } + // pts[2] is within the circle with diameter pts[0] - pts[1]. // 2 // 0 1 From d0d9bd20ed03e6559ef61aa2ac1aceaa6245a64a Mon Sep 17 00:00:00 2001 From: Kumataro Date: Thu, 16 Oct 2025 18:03:02 +0900 Subject: [PATCH 17/29] Merge pull request #27890 from Kumataro:fix26899 core: support 16 bit LUT #27890 Close https://github.com/opencv/opencv/issues/26899 ### 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 - [x] The PR is proposed to the proper branch - [x] 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 --- modules/core/include/opencv2/core.hpp | 6 +- modules/core/src/hal_replacement.hpp | 26 +++++ modules/core/src/lut.cpp | 148 +++++++++++++------------- modules/core/test/test_arithm.cpp | 121 +++++++++++++-------- 4 files changed, 176 insertions(+), 125 deletions(-) diff --git a/modules/core/include/opencv2/core.hpp b/modules/core/include/opencv2/core.hpp index 153bd17320..7f73c71388 100644 --- a/modules/core/include/opencv2/core.hpp +++ b/modules/core/include/opencv2/core.hpp @@ -540,9 +540,9 @@ The function LUT fills the output array with values from the look-up table. Indi are taken from the input array. That is, the function processes each element of src as follows: \f[\texttt{dst} (I) \leftarrow \texttt{lut(src(I) + d)}\f] where -\f[d = \fork{0}{if \(\texttt{src}\) has depth \(\texttt{CV_8U}\)}{128}{if \(\texttt{src}\) has depth \(\texttt{CV_8S}\)}\f] -@param src input array of 8-bit elements. -@param lut look-up table of 256 elements; in case of multi-channel input array, the table should +\f[d = \forkthree{0}{if \(\texttt{src}\) has depth \(\texttt{CV_8U}\) or \(\texttt{CV_16U}\)}{128}{if \(\texttt{src}\) has depth \(\texttt{CV_8S}\)}{32768}{if \(\texttt{src}\) has depth \(\texttt{CV_16S}\)}\f] +@param src input array of 8-bit or 16-bit integer elements. +@param lut look-up table of 256 elements (if src has depth CV_8U or CV_8S) or 65536 elements(if src has depth CV_16U or CV_16S); in case of multi-channel input array, the table should either have a single channel (in this case the same table is used for all channels) or the same number of channels as in the input array. @param dst output array of the same size and number of channels as src, and the same depth as lut. diff --git a/modules/core/src/hal_replacement.hpp b/modules/core/src/hal_replacement.hpp index f809351550..0b4b660667 100644 --- a/modules/core/src/hal_replacement.hpp +++ b/modules/core/src/hal_replacement.hpp @@ -282,6 +282,32 @@ inline int hal_ni_lut(const uchar *src_data, size_t src_step, size_t src_type, c #define cv_hal_lut hal_ni_lut //! @endcond +/** +Lookup table replacement +Table consists of 65536 elements of a size from 1 to 8 bytes having 1 channel or src_channels +For 16s input typea 32768 is added to LUT index +Destination should have the same element type and number of channels as lookup table elements +@param src_data Source image data +@param src_step Source image step +@param src_type Source image type +@param lut_data Pointer to lookup table +@param lut_channel_size Size of each channel in bytes +@param lut_channels Number of channels in lookup table +@param dst_data Destination data +@param dst_step Destination step +@param width Width of images +@param height Height of images +@sa LUT +*/ +//! @addtogroup core_hal_interface_lut16 Lookup table for 16 bit index +//! @{ +inline int hal_ni_lut16(const ushort *src_data, size_t src_step, size_t src_type, const ushort* lut_data, size_t lut_channel_size, size_t lut_channels, uchar *dst_data, size_t dst_step, int width, int height) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } +//! @} + +//! @cond IGNORED +#define cv_hal_lut16 hal_ni_lut16 +//! @endcond + /** Hamming norm of a vector @param a pointer to vector data diff --git a/modules/core/src/lut.cpp b/modules/core/src/lut.cpp index 090ba50d5e..30958fca92 100644 --- a/modules/core/src/lut.cpp +++ b/modules/core/src/lut.cpp @@ -6,6 +6,7 @@ #include "precomp.hpp" #include "opencl_kernels_core.hpp" #include "convert.hpp" +#include /****************************************************************************************\ * LUT Transform * @@ -14,8 +15,8 @@ namespace cv { -template static void -LUT8u_( const uchar* src, const T* lut, T* dst, int len, int cn, int lutcn ) +template static void +LUT_( const Ti* src, const T* lut, T* dst, const int len, const int cn, const int lutcn ) { if( lutcn == 1 ) { @@ -30,53 +31,45 @@ LUT8u_( const uchar* src, const T* lut, T* dst, int len, int cn, int lutcn ) } } -static void LUT8u_8u( const uchar* src, const uchar* lut, uchar* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_8s( const uchar* src, const schar* lut, schar* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_16u( const uchar* src, const ushort* lut, ushort* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_16s( const uchar* src, const short* lut, short* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_32s( const uchar* src, const int* lut, int* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_16f( const uchar* src, const hfloat* lut, hfloat* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_32f( const uchar* src, const float* lut, float* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - -static void LUT8u_64f( const uchar* src, const double* lut, double* dst, int len, int cn, int lutcn ) -{ - LUT8u_( src, lut, dst, len, cn, lutcn ); -} - typedef void (*LUTFunc)( const uchar* src, const uchar* lut, uchar* dst, int len, int cn, int lutcn ); -static LUTFunc lutTab[CV_DEPTH_MAX] = +static LUTFunc getLUTFunc(const int srcDepth, const int dstDepth) { - (LUTFunc)LUT8u_8u, (LUTFunc)LUT8u_8s, (LUTFunc)LUT8u_16u, (LUTFunc)LUT8u_16s, - (LUTFunc)LUT8u_32s, (LUTFunc)LUT8u_32f, (LUTFunc)LUT8u_64f, (LUTFunc)LUT8u_16f -}; + LUTFunc ret = nullptr; + if((srcDepth == CV_8U) || (srcDepth == CV_8S)) + { + switch(dstDepth) + { + case CV_8U: ret = (LUTFunc)LUT_; break; + case CV_8S: ret = (LUTFunc)LUT_; break; + case CV_16U: ret = (LUTFunc)LUT_; break; + case CV_16S: ret = (LUTFunc)LUT_; break; + case CV_32S: ret = (LUTFunc)LUT_; break; + case CV_32F: ret = (LUTFunc)LUT_; break; // float + case CV_64F: ret = (LUTFunc)LUT_; break; // double + case CV_16F: ret = (LUTFunc)LUT_; break; // hfloat + default: ret = nullptr; break; + } + } + else if((srcDepth == CV_16U) || (srcDepth == CV_16S)) + { + switch(dstDepth) + { + case CV_8U: ret = (LUTFunc)LUT_; break; + case CV_8S: ret = (LUTFunc)LUT_; break; + case CV_16U: ret = (LUTFunc)LUT_; break; + case CV_16S: ret = (LUTFunc)LUT_; break; + case CV_32S: ret = (LUTFunc)LUT_; break; + case CV_32F: ret = (LUTFunc)LUT_; break; // float + case CV_64F: ret = (LUTFunc)LUT_; break; // double + case CV_16F: ret = (LUTFunc)LUT_; break; // hfloat + default: ret = nullptr; break; + } + } + + CV_CheckTrue(ret != nullptr, "An unexpected type combination was specified."); + return ret; +} #ifdef HAVE_OPENCL @@ -107,24 +100,19 @@ static bool ocl_LUT(InputArray _src, InputArray _lut, OutputArray _dst) class LUTParallelBody : public ParallelLoopBody { public: - bool* ok; const Mat& src_; const Mat& lut_; Mat& dst_; - LUTFunc func; + LUTFunc func_; - LUTParallelBody(const Mat& src, const Mat& lut, Mat& dst, bool* _ok) - : ok(_ok), src_(src), lut_(lut), dst_(dst) + LUTParallelBody(const Mat& src, const Mat& lut, Mat& dst, LUTFunc func) + : src_(src), lut_(lut), dst_(dst), func_(func) { - func = lutTab[lut.depth()]; - *ok = (func != NULL); } void operator()( const cv::Range& range ) const CV_OVERRIDE { - CV_Assert(*ok); - const int row0 = range.start; const int row1 = range.end; @@ -140,7 +128,7 @@ public: int len = (int)it.size; for( size_t i = 0; i < it.nplanes; i++, ++it ) - func(ptrs[0], lut_.ptr(), ptrs[1], len, cn, lutcn); + func_(ptrs[0], lut_.ptr(), ptrs[1], len, cn, lutcn); } private: LUTParallelBody(const LUTParallelBody&); @@ -155,39 +143,47 @@ void cv::LUT( InputArray _src, InputArray _lut, OutputArray _dst ) int cn = _src.channels(), depth = _src.depth(); int lutcn = _lut.channels(); + const size_t lut_size = _lut.total(); - CV_Assert( (lutcn == cn || lutcn == 1) && - _lut.total() == 256 && _lut.isContinuous() && - (depth == CV_8U || depth == CV_8S) ); + CV_Assert( (lutcn == cn || lutcn == 1) && _lut.isContinuous() && + ( + ((lut_size == 256) && ((depth == CV_8U)||(depth == CV_8S))) || + ((lut_size == 65536) && ((depth == CV_16U)||(depth == CV_16S))) + ) + ); - CV_OCL_RUN(_dst.isUMat() && _src.dims() <= 2, + CV_OCL_RUN(_dst.isUMat() && _src.dims() <= 2 && (lut_size == 256), ocl_LUT(_src, _lut, _dst)) Mat src = _src.getMat(), lut = _lut.getMat(); _dst.create(src.dims, src.size, CV_MAKETYPE(_lut.depth(), cn)); Mat dst = _dst.getMat(); - CALL_HAL(LUT, cv_hal_lut, src.data, src.step, src.type(), lut.data, - lut.elemSize1(), lutcn, dst.data, dst.step, src.cols, src.rows); + if(lut_size == 256) + { + CALL_HAL(LUT, cv_hal_lut, src.data, src.step, src.type(), lut.data, + lut.elemSize1(), lutcn, dst.data, dst.step, src.cols, src.rows); + } + else + { + CALL_HAL(LUT16, cv_hal_lut16, src.ptr(), src.step, src.type(), lut.ptr(), + lut.elemSize1(), lutcn, dst.data, dst.step, src.cols, src.rows); + } + + const LUTFunc func = getLUTFunc(src.depth(), dst.depth()); + CV_Assert( func != nullptr ); if (_src.dims() <= 2) { - bool ok = false; - LUTParallelBody body(src, lut, dst, &ok); - if (ok) - { - Range all(0, dst.rows); - if (dst.total() >= (size_t)(1<<18)) - parallel_for_(all, body, (double)std::max((size_t)1, dst.total()>>16)); - else - body(all); - if (ok) - return; - } - } + LUTParallelBody body(src, lut, dst, func); + Range all(0, dst.rows); + if (dst.total() >= (size_t)(1<<18)) + parallel_for_(all, body, (double)std::max((size_t)1, dst.total()>>16)); + else + body(all); - LUTFunc func = lutTab[lut.depth()]; - CV_Assert( func != 0 ); + return; + } const Mat* arrays[] = {&src, &dst, 0}; uchar* ptrs[2] = {}; diff --git a/modules/core/test/test_arithm.cpp b/modules/core/test/test_arithm.cpp index 88d646b09f..d2ce1f03fd 100644 --- a/modules/core/test/test_arithm.cpp +++ b/modules/core/test/test_arithm.cpp @@ -3221,11 +3221,12 @@ INSTANTIATE_TEST_CASE_P(Core_CartPolar, Core_PolarToCart_inplace, ) ); -CV_ENUM(LutMatType, CV_8U, CV_16U, CV_16F, CV_32S, CV_32F, CV_64F) +CV_ENUM(LutIdxType, CV_8U, CV_8S, CV_16U, CV_16S) +CV_ENUM(LutMatType, CV_8U, CV_8S, CV_16U, CV_16S, CV_32S, CV_32F, CV_64F, CV_16F) -struct Core_LUT: public testing::TestWithParam +struct Core_LUT: public testing::TestWithParam< std::tuple > { - template + template cv::Mat referenceWithType(cv::Mat input, cv::Mat table) { cv::Mat ref(input.size(), CV_MAKE_TYPE(table.depth(), ch)); @@ -3235,7 +3236,7 @@ struct Core_LUT: public testing::TestWithParam { if(ch == 1) { - ref.at(i, j) = table.at(input.at(i, j)); + ref.at(i, j) = table.at(input.at(i, j)); } else { @@ -3244,11 +3245,11 @@ struct Core_LUT: public testing::TestWithParam { if (same_cn) { - val[k] = table.at>(input.at>(i, j)[k])[k]; + val[k] = table.at>(input.at>(i, j)[k])[k]; } else { - val[k] = table.at(input.at>(i, j)[k]); + val[k] = table.at(input.at>(i, j)[k]); } } ref.at>(i, j) = val; @@ -3261,86 +3262,114 @@ struct Core_LUT: public testing::TestWithParam template cv::Mat reference(cv::Mat input, cv::Mat table) { - if (table.depth() == CV_8U) + cv::Mat ret = cv::Mat(); + if ((input.depth() == CV_8U) || (input.depth() == CV_8S)) // Index type for LUT operation { - return referenceWithType(input, table); + switch(table.depth()) // Value type for LUT operation + { + case CV_8U: ret = referenceWithType(input, table); break; + case CV_8S: ret = referenceWithType(input, table); break; + case CV_16U: ret = referenceWithType(input, table); break; + case CV_16S: ret = referenceWithType(input, table); break; + case CV_32S: ret = referenceWithType(input, table); break; + case CV_32F: ret = referenceWithType(input, table); break; + case CV_64F: ret = referenceWithType(input, table); break; + case CV_16F: ret = referenceWithType(input, table); break; + default: ret = cv::Mat(); break; + } } - else if (table.depth() == CV_16U) + else if ((input.depth() == CV_16U) || (input.depth() == CV_16S)) { - return referenceWithType(input, table); - } - else if (table.depth() == CV_16F) - { - return referenceWithType(input, table); - } - else if (table.depth() == CV_32S) - { - return referenceWithType(input, table); - } - else if (table.depth() == CV_32F) - { - return referenceWithType(input, table); - } - else if (table.depth() == CV_64F) - { - return referenceWithType(input, table); + switch(table.depth()) // Value type for LUT operation + { + case CV_8U: ret = referenceWithType(input, table); break; + case CV_8S: ret = referenceWithType(input, table); break; + case CV_16U: ret = referenceWithType(input, table); break; + case CV_16S: ret = referenceWithType(input, table); break; + case CV_32S: ret = referenceWithType(input, table); break; + case CV_32F: ret = referenceWithType(input, table); break; + case CV_64F: ret = referenceWithType(input, table); break; + case CV_16F: ret = referenceWithType(input, table); break; + default: ret = cv::Mat(); break; + } } - return cv::Mat(); + return ret; } }; TEST_P(Core_LUT, accuracy) { - int type = GetParam(); - cv::Mat input(117, 113, CV_8UC1); - randu(input, 0, 256); + int idx_type = get<0>(GetParam()); + int value_type = get<1>(GetParam()); - cv::Mat table(1, 256, CV_MAKE_TYPE(type, 1)); - randu(table, 0, getMaxVal(type)); + ASSERT_TRUE((idx_type == CV_8U) || (idx_type == CV_8S) || (idx_type == CV_16U ) || (idx_type == CV_16S)); + const int tableSize = ((idx_type == CV_8U) || (idx_type == CV_8S)) ? 256: 65536; + + cv::Mat input(117, 113, CV_MAKE_TYPE(idx_type, 1)); + randu(input, getMinVal(idx_type), getMaxVal(idx_type)); + + cv::Mat table(1, tableSize, CV_MAKE_TYPE(value_type, 1)); + randu(table, getMinVal(value_type), getMaxVal(value_type)); cv::Mat output; - cv::LUT(input, table, output); + ASSERT_NO_THROW(cv::LUT(input, table, output)); + ASSERT_FALSE(output.empty()); cv::Mat gt = reference(input, table); + ASSERT_FALSE(gt.empty()); ASSERT_EQ(0, cv::norm(output, gt, cv::NORM_INF)); } TEST_P(Core_LUT, accuracy_multi) { - int type = (int)GetParam(); - cv::Mat input(117, 113, CV_8UC3); - randu(input, 0, 256); + int idx_type = get<0>(GetParam()); + int value_type = get<1>(GetParam()); - cv::Mat table(1, 256, CV_MAKE_TYPE(type, 1)); - randu(table, 0, getMaxVal(type)); + ASSERT_TRUE((idx_type == CV_8U) || (idx_type == CV_8S) || (idx_type == CV_16U) || (idx_type == CV_16S)); + const int tableSize = ((idx_type == CV_8U) || (idx_type == CV_8S) ) ? 256: 65536; + + cv::Mat input(117, 113, CV_MAKE_TYPE(idx_type, 3)); + randu(input, getMinVal(idx_type), getMaxVal(idx_type)); + + cv::Mat table(1, tableSize, CV_MAKE_TYPE(value_type, 1)); + randu(table, getMinVal(value_type), getMaxVal(value_type)); cv::Mat output; - cv::LUT(input, table, output); + ASSERT_NO_THROW(cv::LUT(input, table, output)); + ASSERT_FALSE(output.empty()); cv::Mat gt = reference<3>(input, table); + ASSERT_FALSE(gt.empty()); ASSERT_EQ(0, cv::norm(output, gt, cv::NORM_INF)); } TEST_P(Core_LUT, accuracy_multi2) { - int type = (int)GetParam(); - cv::Mat input(117, 113, CV_8UC3); - randu(input, 0, 256); + int idx_type = get<0>(GetParam()); + int value_type = get<1>(GetParam()); - cv::Mat table(1, 256, CV_MAKE_TYPE(type, 3)); - randu(table, 0, getMaxVal(type)); + ASSERT_TRUE((idx_type == CV_8U) || (idx_type == CV_8S) || (idx_type == CV_16U) || (idx_type == CV_16S)); + const int tableSize = ((idx_type == CV_8U) || (idx_type == CV_8S)) ? 256: 65536; + + cv::Mat input(117, 113, CV_MAKE_TYPE(idx_type, 3)); + randu(input, getMinVal(idx_type), getMaxVal(idx_type)); + + cv::Mat table(1, tableSize, CV_MAKE_TYPE(value_type, 3)); + randu(table, getMinVal(value_type), getMaxVal(value_type)); cv::Mat output; - cv::LUT(input, table, output); + ASSERT_NO_THROW(cv::LUT(input, table, output)); + ASSERT_FALSE(output.empty()); cv::Mat gt = reference<3, true>(input, table); + ASSERT_FALSE(gt.empty()); ASSERT_EQ(0, cv::norm(output, gt, cv::NORM_INF)); } -INSTANTIATE_TEST_CASE_P(/**/, Core_LUT, LutMatType::all()); +INSTANTIATE_TEST_CASE_P(/**/, Core_LUT, testing::Combine( LutIdxType::all(), LutMatType::all())); }} // namespace From 703f0bb0f2bc64abd24a26a05971751f43ede0ff Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov <2536374+asmorkalov@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:06:35 +0300 Subject: [PATCH 18/29] Merge pull request #27906 from asmorkalov:as/cairosvg Use cairosvg for pattern rendering to get rid of double resize. #27906 Depends on https://github.com/opencv/ci-gha-workflow/pull/269 ### 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 --- apps/pattern-tools/test_charuco_board.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/pattern-tools/test_charuco_board.py b/apps/pattern-tools/test_charuco_board.py index 7d5602705b..e92a38a216 100644 --- a/apps/pattern-tools/test_charuco_board.py +++ b/apps/pattern-tools/test_charuco_board.py @@ -11,10 +11,9 @@ class aruco_objdetect_test(NewOpenCVTests): def test_aruco_dicts(self): try: - from svglib.svglib import svg2rlg - from reportlab.graphics import renderPM + import cairosvg except: - raise self.skipTest("libraies svglib and reportlab not found") + raise self.skipTest("cairosvg library was not found") else: cols = 3 rows = 5 @@ -49,8 +48,7 @@ class aruco_objdetect_test(NewOpenCVTests): os.path.join(basedir, aruco_type_str[aruco_type_i]+'.json.gz'), 0) pm.make_charuco_board() pm.save() - drawing = svg2rlg(filesvg) - renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72) + cairosvg.svg2png(url=filesvg, write_to=filepng, background_color="white") from_svg_img = cv.imread(filepng) _charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img) _charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img) @@ -72,10 +70,9 @@ class aruco_objdetect_test(NewOpenCVTests): def test_aruco_marker_sizes(self): try: - from svglib.svglib import svg2rlg - from reportlab.graphics import renderPM + import cairosvg except: - raise self.skipTest("libraies svglib and reportlab not found") + raise self.skipTest("cairosvg library was not found") else: cols = 3 rows = 5 @@ -106,8 +103,7 @@ class aruco_objdetect_test(NewOpenCVTests): board_height, "charuco_checkboard", marker_size, os.path.join(basedir, aruco_type_str+'.json.gz'), 0) pm.make_charuco_board() pm.save() - drawing = svg2rlg(filesvg) - renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72) + cairosvg.svg2png(url=filesvg, write_to=filepng, background_color="white") from_svg_img = cv.imread(filepng) #test From ec630504a7dcc9063715b594c113a24c09a4000b Mon Sep 17 00:00:00 2001 From: Kumataro Date: Thu, 16 Oct 2025 21:19:12 +0900 Subject: [PATCH 19/29] core: fix lut_data data type --- modules/core/src/hal_replacement.hpp | 2 +- modules/core/src/lut.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/hal_replacement.hpp b/modules/core/src/hal_replacement.hpp index 0b4b660667..c573a97117 100644 --- a/modules/core/src/hal_replacement.hpp +++ b/modules/core/src/hal_replacement.hpp @@ -301,7 +301,7 @@ Destination should have the same element type and number of channels as lookup t */ //! @addtogroup core_hal_interface_lut16 Lookup table for 16 bit index //! @{ -inline int hal_ni_lut16(const ushort *src_data, size_t src_step, size_t src_type, const ushort* lut_data, size_t lut_channel_size, size_t lut_channels, uchar *dst_data, size_t dst_step, int width, int height) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } +inline int hal_ni_lut16(const ushort *src_data, size_t src_step, size_t src_type, const uchar* lut_data, size_t lut_channel_size, size_t lut_channels, uchar *dst_data, size_t dst_step, int width, int height) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } //! @} //! @cond IGNORED diff --git a/modules/core/src/lut.cpp b/modules/core/src/lut.cpp index 30958fca92..feae254358 100644 --- a/modules/core/src/lut.cpp +++ b/modules/core/src/lut.cpp @@ -166,7 +166,7 @@ void cv::LUT( InputArray _src, InputArray _lut, OutputArray _dst ) } else { - CALL_HAL(LUT16, cv_hal_lut16, src.ptr(), src.step, src.type(), lut.ptr(), + CALL_HAL(LUT16, cv_hal_lut16, src.ptr(), src.step, src.type(), lut.data, lut.elemSize1(), lutcn, dst.data, dst.step, src.cols, src.rows); } From f5014c179f0d75984b838c17fef422b7f8d1c854 Mon Sep 17 00:00:00 2001 From: s-trinh Date: Thu, 16 Oct 2025 14:21:15 +0200 Subject: [PATCH 20/29] Merge pull request #27736 from s-trinh:use_USAC_P3P_in_solvePnP Update Gao P3P with Ding P3P #27736 ### 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 - [x] The PR is proposed to the proper branch - [x] 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 --- The current Gao P3P implementation does not cover all the degenerate cases, **see last line** in: https://github.com/opencv/opencv/blob/6d889ee74c94124f6492eb8f0d50946d9c31d8e9/modules/calib3d/src/p3p.cpp#L211-L221 See also: - https://github.com/opencv/opencv/issues/4854 ---

OBSOLETE To fix this, the USAC P3P from OpenCV 5 is used instead: https://github.com/opencv/opencv/blob/7e6da007cddcf83a527dfda95d57228fa5a535d3/modules/3d/src/usac/pnp_solver.cpp#L282 --- ## Some results ### Old P3P vs new In the following video, I have tried to highlight the viewpoints which cause issues: https://github.com/user-attachments/assets/97bec6a6-4043-4509-b50e-a9856d6423bd | | Old P3P | New P3P | | -------- | ------- | ------- | | Mean (ms) | 0.045701 | 0.024816 | | Median (ms) | 0.025146 | 0.023193 | | Std (ms) | 0.028953 | 0.006124 | ### New P3P vs AP3P https://github.com/user-attachments/assets/eaeb21dc-3ffd-4b6c-9902-4352f824aa45 The AP3 method is superior both in term of accuracy and computation time: | | New P3P | AP3P | | -------- | ------- | ------- | | Mean (ms) | 0.043750 | 0.023442 | | Median (ms) | 0.023193 | 0.021484 | | Std (ms) | 0.039920 | 0.005265 | ### New P3P vs AP3P (range test) https://github.com/user-attachments/assets/572e7b7a-2966-4bed-8e0c-b93d863987dc The implemented P3P method does not work well when the tag is small, at long range. | | New P3P | AP3P | | -------- | ------- | ------- | | Mean (ms) | 0.031351 | 0.025189 | | Median (ms) | 0.022217 | 0.020996 | | Std (ms) | 0.024920 | 0.009633 | --- - I have tried to simplify the P3P code, hope I did not break the implementation code - calculations are performed using double type for simplicity. - code such as the following are redundant and no more needed and should be replaced by `cv::Rodrigues`: https://github.com/opencv/opencv/blob/6d889ee74c94124f6492eb8f0d50946d9c31d8e9/modules/calib3d/src/usac/pnp_solver.cpp#L395
--- modules/calib3d/doc/calib3d.bib | 47 +- modules/calib3d/doc/solvePnP.markdown | 4 +- modules/calib3d/include/opencv2/calib3d.hpp | 6 +- modules/calib3d/src/p3p.cpp | 827 ++++++++---------- modules/calib3d/src/p3p.h | 69 +- modules/calib3d/src/solvepnp.cpp | 4 +- modules/calib3d/src/usac.hpp | 2 - modules/calib3d/src/usac/dls_solver.cpp | 10 +- modules/calib3d/src/usac/pnp_solver.cpp | 7 +- modules/calib3d/src/usac/utils.cpp | 40 - modules/calib3d/test/test_solvepnp_ransac.cpp | 8 +- 11 files changed, 431 insertions(+), 593 deletions(-) diff --git a/modules/calib3d/doc/calib3d.bib b/modules/calib3d/doc/calib3d.bib index c7b2a8cfde..123316e2ee 100644 --- a/modules/calib3d/doc/calib3d.bib +++ b/modules/calib3d/doc/calib3d.bib @@ -1,3 +1,11 @@ +@inproceedings{ding2023revisiting, + title={Revisiting the P3P Problem}, + author={Ding, Yaqing and Yang, Jian and Larsson, Viktor and Olsson, Carl and {\AA}str{\"o}m, Kalle}, + booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, + pages={4872--4880}, + year={2023}, + url={https://openaccess.thecvf.com/content/CVPR2023/papers/Ding_Revisiting_the_P3P_Problem_CVPR_2023_paper.pdf} +} @article{lepetit2009epnp, title={Epnp: An accurate o (n) solution to the pnp problem}, author={Lepetit, Vincent and Moreno-Noguer, Francesc and Fua, Pascal}, @@ -6,18 +14,8 @@ number={2}, pages={155--166}, year={2009}, - publisher={Springer} -} - -@article{gao2003complete, - title={Complete solution classification for the perspective-three-point problem}, - author={Gao, Xiao-Shan and Hou, Xiao-Rong and Tang, Jianliang and Cheng, Hang-Fei}, - journal={Pattern Analysis and Machine Intelligence, IEEE Transactions on}, - volume={25}, - number={8}, - pages={930--943}, - year={2003}, - publisher={IEEE} + publisher={Springer}, + url={https://www.tugraz.at/fileadmin/user_upload/Institute/ICG/Images/team_lepetit/publications/lepetit_ijcv08.pdf} } @inproceedings{hesch2011direct, @@ -26,7 +24,8 @@ booktitle={Computer Vision (ICCV), 2011 IEEE International Conference on}, pages={383--390}, year={2011}, - organization={IEEE} + organization={IEEE}, + url={https://www-users.cse.umn.edu/~stergios/papers/ICCV-11-DLS-PnP.pdf} } @article{penate2013exhaustive, @@ -37,16 +36,8 @@ number={10}, pages={2387--2400}, year={2013}, - publisher={IEEE} -} - -@inproceedings{Terzakis2020SQPnP, - title={A Consistently Fast and Globally Optimal Solution to the Perspective-n-Point Problem}, - author={George Terzakis and Manolis Lourakis}, - booktitle={European Conference on Computer Vision}, - pages={478--494}, - year={2020}, - publisher={Springer International Publishing} + publisher={IEEE}, + url={https://www.researchgate.net/publication/235402233_Exhaustive_Linearization_for_Robust_Camera_Pose_and_Focal_Length_Estimation} } @inproceedings{strobl2011iccv, @@ -61,3 +52,13 @@ url={https://elib.dlr.de/71888/1/strobl_2011iccv.pdf}, doi={10.1109/ICCVW.2011.6130369} } + +@inproceedings{Terzakis2020SQPnP, + title={A Consistently Fast and Globally Optimal Solution to the Perspective-n-Point Problem}, + author={George Terzakis and Manolis Lourakis}, + booktitle={European Conference on Computer Vision}, + pages={478--494}, + year={2020}, + publisher={Springer International Publishing}, + url={https://www.ecva.net/papers/eccv_2020/papers_ECCV/papers/123460460.pdf} +} diff --git a/modules/calib3d/doc/solvePnP.markdown b/modules/calib3d/doc/solvePnP.markdown index a7fdbd0fb8..f462e4bf26 100644 --- a/modules/calib3d/doc/solvePnP.markdown +++ b/modules/calib3d/doc/solvePnP.markdown @@ -104,8 +104,8 @@ this case the function finds such a pose that minimizes reprojection error, that of squared distances between the observed projections "imagePoints" and the projected (using cv::projectPoints ) "objectPoints". Initial solution for non-planar "objectPoints" needs at least 6 points and uses the DLT algorithm. Initial solution for planar "objectPoints" needs at least 4 points and uses pose from homography decomposition. -- cv::SOLVEPNP_P3P Method is based on the paper of X.S. Gao, X.-R. Hou, J. Tang, H.-F. Chang -"Complete Solution Classification for the Perspective-Three-Point Problem" (@cite gao2003complete). +- cv::SOLVEPNP_P3P Method is based on the paper of Ding, Y., Yang, J., Larsson, V., Olsson, C., & Åstrom, K. +"Revisiting the P3P Problem" (@cite ding2023revisiting). In this case the function requires exactly four object and image points. - cv::SOLVEPNP_AP3P Method is based on the paper of T. Ke, S. Roumeliotis "An Efficient Algebraic Solution to the Perspective-Three-Point Problem" (@cite Ke17). diff --git a/modules/calib3d/include/opencv2/calib3d.hpp b/modules/calib3d/include/opencv2/calib3d.hpp index 0c2d10cd71..8a0edb0c69 100644 --- a/modules/calib3d/include/opencv2/calib3d.hpp +++ b/modules/calib3d/include/opencv2/calib3d.hpp @@ -565,7 +565,7 @@ enum SolvePnPMethod { //!< Initial solution for non-planar "objectPoints" needs at least 6 points and uses the DLT algorithm. \n //!< Initial solution for planar "objectPoints" needs at least 4 points and uses pose from homography decomposition. SOLVEPNP_EPNP = 1, //!< EPnP: Efficient Perspective-n-Point Camera Pose Estimation @cite lepetit2009epnp - SOLVEPNP_P3P = 2, //!< Complete Solution Classification for the Perspective-Three-Point Problem @cite gao2003complete + SOLVEPNP_P3P = 2, //!< Revisiting the P3P Problem @cite ding2023revisiting SOLVEPNP_DLS = 3, //!< **Broken implementation. Using this flag will fallback to EPnP.** \n //!< A Direct Least-Squares (DLS) Method for PnP @cite hesch2011direct SOLVEPNP_UPNP = 4, //!< **Broken implementation. Using this flag will fallback to EPnP.** \n @@ -1154,8 +1154,8 @@ assumed. the model coordinate system to the camera coordinate system. A P3P problem has up to 4 solutions. @param tvecs Output translation vectors. @param flags Method for solving a P3P problem: -- @ref SOLVEPNP_P3P Method is based on the paper of X.S. Gao, X.-R. Hou, J. Tang, H.-F. Chang -"Complete Solution Classification for the Perspective-Three-Point Problem" (@cite gao2003complete). +- @ref SOLVEPNP_P3P Method is based on the paper of Ding, Y., Yang, J., Larsson, V., Olsson, C., & Åstrom, K. +"Revisiting the P3P Problem" (@cite ding2023revisiting). - @ref SOLVEPNP_AP3P Method is based on the paper of T. Ke and S. Roumeliotis. "An Efficient Algebraic Solution to the Perspective-Three-Point Problem" (@cite Ke17). diff --git a/modules/calib3d/src/p3p.cpp b/modules/calib3d/src/p3p.cpp index 9a7e96cca2..20449a178f 100644 --- a/modules/calib3d/src/p3p.cpp +++ b/modules/calib3d/src/p3p.cpp @@ -2,465 +2,386 @@ #include #include -#include "polynom_solver.h" #include "p3p.h" -void p3p::init_inverse_parameters() + +using namespace cv; + +// Copyright (c) 2020, Viktor Larsson +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: Yaqing Ding +// Mark Shachkov + +// https://github.com/PoseLib/PoseLib/blob/79fe59ada3122c50383ac06e043a5e04072c6711/PoseLib/solvers/p3p.cc +namespace yaqding { - inv_fx = 1. / fx; - inv_fy = 1. / fy; - cx_fx = cx / fx; - cy_fy = cy / fy; -} -p3p::p3p(cv::Mat cameraMatrix) -{ - if (cameraMatrix.depth() == CV_32F) - init_camera_parameters(cameraMatrix); - else - init_camera_parameters(cameraMatrix); - init_inverse_parameters(); -} - -p3p::p3p(double _fx, double _fy, double _cx, double _cy) -{ - fx = _fx; - fy = _fy; - cx = _cx; - cy = _cy; - init_inverse_parameters(); -} - -bool p3p::solve(cv::Mat& R, cv::Mat& tvec, const cv::Mat& opoints, const cv::Mat& ipoints) -{ - CV_INSTRUMENT_REGION(); - - double rotation_matrix[3][3] = {}, translation[3] = {}; - std::vector points; - if (opoints.depth() == ipoints.depth()) - { - if (opoints.depth() == CV_32F) - extract_points(opoints, ipoints, points); - else - extract_points(opoints, ipoints, points); - } - else if (opoints.depth() == CV_32F) - extract_points(opoints, ipoints, points); - else - extract_points(opoints, ipoints, points); - - bool result = solve(rotation_matrix, translation, - points[0], points[1], points[2], points[3], points[4], - points[5], points[6], points[7], points[8], points[9], - points[10], points[11], points[12], points[13], points[14], - points[15], points[16], points[17], points[18], points[19]); - cv::Mat(3, 1, CV_64F, translation).copyTo(tvec); - cv::Mat(3, 3, CV_64F, rotation_matrix).copyTo(R); - return result; -} - -int p3p::solve(std::vector& Rs, std::vector& tvecs, const cv::Mat& opoints, const cv::Mat& ipoints) -{ - CV_INSTRUMENT_REGION(); - - double rotation_matrix[4][3][3] = {}, translation[4][3] = {}; - std::vector points; - if (opoints.depth() == ipoints.depth()) - { - if (opoints.depth() == CV_32F) - extract_points(opoints, ipoints, points); - else - extract_points(opoints, ipoints, points); - } - else if (opoints.depth() == CV_32F) - extract_points(opoints, ipoints, points); - else - extract_points(opoints, ipoints, points); - - const bool p4p = std::max(opoints.checkVector(3, CV_32F), opoints.checkVector(3, CV_64F)) == 4; - int solutions = solve(rotation_matrix, translation, - points[0], points[1], points[2], points[3], points[4], - points[5], points[6], points[7], points[8], points[9], - points[10], points[11], points[12], points[13], points[14], - points[15], points[16], points[17], points[18], points[19], - p4p); - - for (int i = 0; i < solutions; i++) { - cv::Mat R, tvec; - cv::Mat(3, 1, CV_64F, translation[i]).copyTo(tvec); - cv::Mat(3, 3, CV_64F, rotation_matrix[i]).copyTo(R); - - Rs.push_back(R); - tvecs.push_back(tvec); - } - - return solutions; -} - -bool p3p::solve(double R[3][3], double t[3], - double mu0, double mv0, double X0, double Y0, double Z0, - double mu1, double mv1, double X1, double Y1, double Z1, - double mu2, double mv2, double X2, double Y2, double Z2, - double mu3, double mv3, double X3, double Y3, double Z3) -{ - double Rs[4][3][3] = {}, ts[4][3] = {}; - - const bool p4p = true; - int n = solve(Rs, ts, mu0, mv0, X0, Y0, Z0, mu1, mv1, X1, Y1, Z1, mu2, mv2, X2, Y2, Z2, mu3, mv3, X3, Y3, Z3, p4p); - - if (n == 0) - return false; - - for(int i = 0; i < 3; i++) { - for(int j = 0; j < 3; j++) - R[i][j] = Rs[0][i][j]; - t[i] = ts[0][i]; - } - - return true; -} - -int p3p::solve(double R[4][3][3], double t[4][3], - double mu0, double mv0, double X0, double Y0, double Z0, - double mu1, double mv1, double X1, double Y1, double Z1, - double mu2, double mv2, double X2, double Y2, double Z2, - double mu3, double mv3, double X3, double Y3, double Z3, - bool p4p) -{ - double mk0, mk1, mk2; - double norm; - - mu0 = inv_fx * mu0 - cx_fx; - mv0 = inv_fy * mv0 - cy_fy; - norm = sqrt(mu0 * mu0 + mv0 * mv0 + 1); - mk0 = 1. / norm; mu0 *= mk0; mv0 *= mk0; - - mu1 = inv_fx * mu1 - cx_fx; - mv1 = inv_fy * mv1 - cy_fy; - norm = sqrt(mu1 * mu1 + mv1 * mv1 + 1); - mk1 = 1. / norm; mu1 *= mk1; mv1 *= mk1; - - mu2 = inv_fx * mu2 - cx_fx; - mv2 = inv_fy * mv2 - cy_fy; - norm = sqrt(mu2 * mu2 + mv2 * mv2 + 1); - mk2 = 1. / norm; mu2 *= mk2; mv2 *= mk2; - - mu3 = inv_fx * mu3 - cx_fx; - mv3 = inv_fy * mv3 - cy_fy; - - double distances[3]; - distances[0] = sqrt( (X1 - X2) * (X1 - X2) + (Y1 - Y2) * (Y1 - Y2) + (Z1 - Z2) * (Z1 - Z2) ); - distances[1] = sqrt( (X0 - X2) * (X0 - X2) + (Y0 - Y2) * (Y0 - Y2) + (Z0 - Z2) * (Z0 - Z2) ); - distances[2] = sqrt( (X0 - X1) * (X0 - X1) + (Y0 - Y1) * (Y0 - Y1) + (Z0 - Z1) * (Z0 - Z1) ); - - // Calculate angles - double cosines[3]; - cosines[0] = mu1 * mu2 + mv1 * mv2 + mk1 * mk2; - cosines[1] = mu0 * mu2 + mv0 * mv2 + mk0 * mk2; - cosines[2] = mu0 * mu1 + mv0 * mv1 + mk0 * mk1; - - double lengths[4][3] = {}; - int n = solve_for_lengths(lengths, distances, cosines); - - int nb_solutions = 0; - double reproj_errors[4]; - for(int i = 0; i < n; i++) { - double M_orig[3][3]; - - M_orig[0][0] = lengths[i][0] * mu0; - M_orig[0][1] = lengths[i][0] * mv0; - M_orig[0][2] = lengths[i][0] * mk0; - - M_orig[1][0] = lengths[i][1] * mu1; - M_orig[1][1] = lengths[i][1] * mv1; - M_orig[1][2] = lengths[i][1] * mk1; - - M_orig[2][0] = lengths[i][2] * mu2; - M_orig[2][1] = lengths[i][2] * mv2; - M_orig[2][2] = lengths[i][2] * mk2; - - if (!align(M_orig, X0, Y0, Z0, X1, Y1, Z1, X2, Y2, Z2, R[nb_solutions], t[nb_solutions])) - continue; - - if (p4p) { - double X3p = R[nb_solutions][0][0] * X3 + R[nb_solutions][0][1] * Y3 + R[nb_solutions][0][2] * Z3 + t[nb_solutions][0]; - double Y3p = R[nb_solutions][1][0] * X3 + R[nb_solutions][1][1] * Y3 + R[nb_solutions][1][2] * Z3 + t[nb_solutions][1]; - double Z3p = R[nb_solutions][2][0] * X3 + R[nb_solutions][2][1] * Y3 + R[nb_solutions][2][2] * Z3 + t[nb_solutions][2]; - double mu3p = X3p / Z3p; - double mv3p = Y3p / Z3p; - reproj_errors[nb_solutions] = (mu3p - mu3) * (mu3p - mu3) + (mv3p - mv3) * (mv3p - mv3); - } - - nb_solutions++; - } - - if (p4p) { - //sort the solutions - for (int i = 1; i < nb_solutions; i++) { - for (int j = i; j > 0 && reproj_errors[j-1] > reproj_errors[j]; j--) { - std::swap(reproj_errors[j], reproj_errors[j-1]); - std::swap(R[j], R[j-1]); - std::swap(t[j], t[j-1]); - } - } - } - - return nb_solutions; -} - -/// Given 3D distances between three points and cosines of 3 angles at the apex, calculates -/// the lengths of the line segments connecting projection center (P) and the three 3D points (A, B, C). -/// Returned distances are for |PA|, |PB|, |PC| respectively. -/// Only the solution to the main branch. -/// Reference : X.S. Gao, X.-R. Hou, J. Tang, H.-F. Chang; "Complete Solution Classification for the Perspective-Three-Point Problem" -/// IEEE Trans. on PAMI, vol. 25, No. 8, August 2003 -/// \param lengths Lengths of line segments up to four solutions. -/// \param distances Distance between 3D points in pairs |BC|, |AC|, |AB|. -/// \param cosines Cosine of the angles /_BPC, /_APC, /_APB. -/// \returns Number of solutions. -/// WARNING: NOT ALL THE DEGENERATE CASES ARE IMPLEMENTED - -int p3p::solve_for_lengths(double lengths[4][3], double distances[3], double cosines[3]) -{ - double p = cosines[0] * 2; - double q = cosines[1] * 2; - double r = cosines[2] * 2; - - double inv_d22 = 1. / (distances[2] * distances[2]); - double a = inv_d22 * (distances[0] * distances[0]); - double b = inv_d22 * (distances[1] * distances[1]); - - double a2 = a * a, b2 = b * b, p2 = p * p, q2 = q * q, r2 = r * r; - double pr = p * r, pqr = q * pr; - - // Check reality condition (the four points should not be coplanar) - if (p2 + q2 + r2 - pqr - 1 == 0) - return 0; - - double ab = a * b, a_2 = 2*a; - - double A = -2 * b + b2 + a2 + 1 + ab*(2 - r2) - a_2; - - // Check reality condition - if (A == 0) return 0; - - double a_4 = 4*a; - - double B = q*(-2*(ab + a2 + 1 - b) + r2*ab + a_4) + pr*(b - b2 + ab); - double C = q2 + b2*(r2 + p2 - 2) - b*(p2 + pqr) - ab*(r2 + pqr) + (a2 - a_2)*(2 + q2) + 2; - double D = pr*(ab-b2+b) + q*((p2-2)*b + 2 * (ab - a2) + a_4 - 2); - double E = 1 + 2*(b - a - ab) + b2 - b*p2 + a2; - - double temp = (p2*(a-1+b) + r2*(a-1-b) + pqr - a*pqr); - double b0 = b * temp * temp; - // Check reality condition - if (b0 == 0) - return 0; - - double real_roots[4]; - int n = solve_deg4(A, B, C, D, E, real_roots[0], real_roots[1], real_roots[2], real_roots[3]); - - if (n == 0) - return 0; - - int nb_solutions = 0; - double r3 = r2*r, pr2 = p*r2, r3q = r3 * q; - double inv_b0 = 1. / b0; - - // For each solution of x - for(int i = 0; i < n; i++) { - double x = real_roots[i]; - - // Check reality condition - if (x <= 0) - continue; - - double x2 = x*x; - - double b1 = - ((1-a-b)*x2 + (q*a-q)*x + 1 - a + b) * - (((r3*(a2 + ab*(2 - r2) - a_2 + b2 - 2*b + 1)) * x + - - (r3q*(2*(b-a2) + a_4 + ab*(r2 - 2) - 2) + pr2*(1 + a2 + 2*(ab-a-b) + r2*(b - b2) + b2))) * x2 + - - (r3*(q2*(1-2*a+a2) + r2*(b2-ab) - a_4 + 2*(a2 - b2) + 2) + r*p2*(b2 + 2*(ab - b - a) + 1 + a2) + pr2*q*(a_4 + 2*(b - ab - a2) - 2 - r2*b)) * x + - - 2*r3q*(a_2 - b - a2 + ab - 1) + pr2*(q2 - a_4 + 2*(a2 - b2) + r2*b + q2*(a2 - a_2) + 2) + - p2*(p*(2*(ab - a - b) + a2 + b2 + 1) + 2*q*r*(b + a_2 - a2 - ab - 1))); - - // Check reality condition - if (b1 <= 0) - continue; - - double y = inv_b0 * b1; - double v = x2 + y*y - x*y*r; - - if (v <= 0) - continue; - - double Z = distances[2] / sqrt(v); - double X = x * Z; - double Y = y * Z; - - lengths[nb_solutions][0] = X; - lengths[nb_solutions][1] = Y; - lengths[nb_solutions][2] = Z; - - nb_solutions++; - } - - return nb_solutions; -} - -bool p3p::align(double M_end[3][3], - double X0, double Y0, double Z0, - double X1, double Y1, double Z1, - double X2, double Y2, double Z2, - double R[3][3], double T[3]) -{ - // Centroids: - double C_start[3] = {}, C_end[3] = {}; - for(int i = 0; i < 3; i++) C_end[i] = (M_end[0][i] + M_end[1][i] + M_end[2][i]) / 3; - C_start[0] = (X0 + X1 + X2) / 3; - C_start[1] = (Y0 + Y1 + Y2) / 3; - C_start[2] = (Z0 + Z1 + Z2) / 3; - - // Covariance matrix s: - double s[3 * 3] = {}; - for(int j = 0; j < 3; j++) { - s[0 * 3 + j] = (X0 * M_end[0][j] + X1 * M_end[1][j] + X2 * M_end[2][j]) / 3 - C_end[j] * C_start[0]; - s[1 * 3 + j] = (Y0 * M_end[0][j] + Y1 * M_end[1][j] + Y2 * M_end[2][j]) / 3 - C_end[j] * C_start[1]; - s[2 * 3 + j] = (Z0 * M_end[0][j] + Z1 * M_end[1][j] + Z2 * M_end[2][j]) / 3 - C_end[j] * C_start[2]; - } - - double Qs[16] = {}, evs[4] = {}, U[16] = {}; - - Qs[0 * 4 + 0] = s[0 * 3 + 0] + s[1 * 3 + 1] + s[2 * 3 + 2]; - Qs[1 * 4 + 1] = s[0 * 3 + 0] - s[1 * 3 + 1] - s[2 * 3 + 2]; - Qs[2 * 4 + 2] = s[1 * 3 + 1] - s[2 * 3 + 2] - s[0 * 3 + 0]; - Qs[3 * 4 + 3] = s[2 * 3 + 2] - s[0 * 3 + 0] - s[1 * 3 + 1]; - - Qs[1 * 4 + 0] = Qs[0 * 4 + 1] = s[1 * 3 + 2] - s[2 * 3 + 1]; - Qs[2 * 4 + 0] = Qs[0 * 4 + 2] = s[2 * 3 + 0] - s[0 * 3 + 2]; - Qs[3 * 4 + 0] = Qs[0 * 4 + 3] = s[0 * 3 + 1] - s[1 * 3 + 0]; - Qs[2 * 4 + 1] = Qs[1 * 4 + 2] = s[1 * 3 + 0] + s[0 * 3 + 1]; - Qs[3 * 4 + 1] = Qs[1 * 4 + 3] = s[2 * 3 + 0] + s[0 * 3 + 2]; - Qs[3 * 4 + 2] = Qs[2 * 4 + 3] = s[2 * 3 + 1] + s[1 * 3 + 2]; - - jacobi_4x4(Qs, evs, U); - - // Looking for the largest eigen value: - int i_ev = 0; - double ev_max = evs[i_ev]; - for(int i = 1; i < 4; i++) - if (evs[i] > ev_max) - ev_max = evs[i_ev = i]; - - // Quaternion: - double q[4]; - for(int i = 0; i < 4; i++) - q[i] = U[i * 4 + i_ev]; - - double q02 = q[0] * q[0], q12 = q[1] * q[1], q22 = q[2] * q[2], q32 = q[3] * q[3]; - double q0_1 = q[0] * q[1], q0_2 = q[0] * q[2], q0_3 = q[0] * q[3]; - double q1_2 = q[1] * q[2], q1_3 = q[1] * q[3]; - double q2_3 = q[2] * q[3]; - - R[0][0] = q02 + q12 - q22 - q32; - R[0][1] = 2. * (q1_2 - q0_3); - R[0][2] = 2. * (q1_3 + q0_2); - - R[1][0] = 2. * (q1_2 + q0_3); - R[1][1] = q02 + q22 - q12 - q32; - R[1][2] = 2. * (q2_3 - q0_1); - - R[2][0] = 2. * (q1_3 - q0_2); - R[2][1] = 2. * (q2_3 + q0_1); - R[2][2] = q02 + q32 - q12 - q22; - - for(int i = 0; i < 3; i++) - T[i] = C_end[i] - (R[i][0] * C_start[0] + R[i][1] * C_start[1] + R[i][2] * C_start[2]); - - return true; -} - -bool p3p::jacobi_4x4(double * A, double * D, double * U) -{ - double B[4] = {}, Z[4] = {}; - double Id[16] = {1., 0., 0., 0., - 0., 1., 0., 0., - 0., 0., 1., 0., - 0., 0., 0., 1.}; - - memcpy(U, Id, 16 * sizeof(double)); - - B[0] = A[0]; B[1] = A[5]; B[2] = A[10]; B[3] = A[15]; - memcpy(D, B, 4 * sizeof(double)); - - for(int iter = 0; iter < 50; iter++) { - double sum = fabs(A[1]) + fabs(A[2]) + fabs(A[3]) + fabs(A[6]) + fabs(A[7]) + fabs(A[11]); - - if (sum == 0.0) +static bool solve_cubic_single_real(double c2, double c1, double c0, double &root) { + double a = c1 - c2 * c2 / 3.0; + double b = (2.0 * c2 * c2 * c2 - 9.0 * c2 * c1) / 27.0 + c0; + double c = b * b / 4.0 + a * a * a / 27.0; + if (c != 0) { + if (c > 0) { + c = std::sqrt(c); + b *= -0.5; + root = std::cbrt(b + c) + std::cbrt(b - c) - c2 / 3.0; return true; - - double tresh = (iter < 3) ? 0.2 * sum / 16. : 0.0; - for(int i = 0; i < 3; i++) { - double * pAij = A + 5 * i + 1; - for(int j = i + 1 ; j < 4; j++) { - double Aij = *pAij; - double eps_machine = 100.0 * fabs(Aij); - - if ( iter > 3 && fabs(D[i]) + eps_machine == fabs(D[i]) && fabs(D[j]) + eps_machine == fabs(D[j]) ) - *pAij = 0.0; - else if (fabs(Aij) > tresh) { - double hh = D[j] - D[i], t; - if (fabs(hh) + eps_machine == fabs(hh)) - t = Aij / hh; - else { - double theta = 0.5 * hh / Aij; - t = 1.0 / (fabs(theta) + sqrt(1.0 + theta * theta)); - if (theta < 0.0) t = -t; - } - - hh = t * Aij; - Z[i] -= hh; - Z[j] += hh; - D[i] -= hh; - D[j] += hh; - *pAij = 0.0; - - double c = 1.0 / sqrt(1 + t * t); - double s = t * c; - double tau = s / (1.0 + c); - for(int k = 0; k <= i - 1; k++) { - double g = A[k * 4 + i], h = A[k * 4 + j]; - A[k * 4 + i] = g - s * (h + g * tau); - A[k * 4 + j] = h + s * (g - h * tau); - } - for(int k = i + 1; k <= j - 1; k++) { - double g = A[i * 4 + k], h = A[k * 4 + j]; - A[i * 4 + k] = g - s * (h + g * tau); - A[k * 4 + j] = h + s * (g - h * tau); - } - for(int k = j + 1; k < 4; k++) { - double g = A[i * 4 + k], h = A[j * 4 + k]; - A[i * 4 + k] = g - s * (h + g * tau); - A[j * 4 + k] = h + s * (g - h * tau); - } - for(int k = 0; k < 4; k++) { - double g = U[k * 4 + i], h = U[k * 4 + j]; - U[k * 4 + i] = g - s * (h + g * tau); - U[k * 4 + j] = h + s * (g - h * tau); - } - } - pAij++; - } + } else { + c = 3.0 * b / (2.0 * a) * std::sqrt(-3.0 / a); + root = 2.0 * std::sqrt(-a / 3.0) * std::cos(std::acos(c) / 3.0) - c2 / 3.0; } - - for(int i = 0; i < 4; i++) B[i] += Z[i]; - memcpy(D, B, 4 * sizeof(double)); - memset(Z, 0, 4 * sizeof(double)); + } else { + root = -c2 / 3.0 + (a != 0 ? (3.0 * b / a) : 0); } - return false; } + +static bool root2real(double b, double c, double &r1, double &r2) { + const double THRESHOLD = -1.0e-12; + double v = b * b - 4.0 * c; + if (v < THRESHOLD) { + r1 = r2 = -0.5 * b; + return v >= 0; + } + if (v > THRESHOLD && v < 0.0) { + r1 = -0.5 * b; + r2 = -2; + return true; + } + + double y = std::sqrt(v); + if (b < 0) { + r1 = 0.5 * (-b + y); + r2 = 0.5 * (-b - y); + } else { + r1 = 2.0 * c / (-b + y); + r2 = 2.0 * c / (-b - y); + } + return true; +} + +static std::array compute_pq(Matx33d C) { + std::array pq; + Matx33d C_adj; + + C_adj(0, 0) = C(1, 2) * C(2, 1) - C(1, 1) * C(2, 2); + C_adj(1, 1) = C(0, 2) * C(2, 0) - C(0, 0) * C(2, 2); + C_adj(2, 2) = C(0, 1) * C(1, 0) - C(0, 0) * C(1, 1); + C_adj(0, 1) = C(0, 1) * C(2, 2) - C(0, 2) * C(2, 1); + C_adj(0, 2) = C(0, 2) * C(1, 1) - C(0, 1) * C(1, 2); + C_adj(1, 0) = C_adj(0, 1); + C_adj(1, 2) = C(0, 0) * C(1, 2) - C(0, 2) * C(1, 0); + C_adj(2, 0) = C_adj(0, 2); + C_adj(2, 1) = C_adj(1, 2); + + Matx31d v; + if (C_adj(0, 0) > C_adj(1, 1)) { + if (C_adj(0, 0) > C_adj(2, 2)) { + v = C_adj.col(0) / std::sqrt(C_adj(0, 0)); + } else { + v = C_adj.col(2) / std::sqrt(C_adj(2, 2)); + } + } else if (C_adj(1, 1) > C_adj(2, 2)) { + v = C_adj.col(1) / std::sqrt(C_adj(1, 1)); + } else { + v = C_adj.col(2) / std::sqrt(C_adj(2, 2)); + } + + C(0, 1) -= v(2); + C(0, 2) += v(1); + C(1, 2) -= v(0); + C(1, 0) += v(2); + C(2, 0) -= v(1); + C(2, 1) += v(0); + + pq[0](0) = C.col(0)(0); + pq[0](1) = C.col(0)(1); + pq[0](2) = C.col(0)(2); + pq[1](0) = C.row(0)(0); + pq[1](1) = C.row(0)(1); + pq[1](2) = C.row(0)(2); + + return pq; +} + +// Performs a few Newton steps on the equations +static void refine_lambda(double &lambda1, double &lambda2, double &lambda3, const double a12, const double a13, + const double a23, const double b12, const double b13, const double b23) { + for (int iter = 0; iter < 5; ++iter) { + double r1 = (lambda1 * lambda1 - 2.0 * lambda1 * lambda2 * b12 + lambda2 * lambda2 - a12); + double r2 = (lambda1 * lambda1 - 2.0 * lambda1 * lambda3 * b13 + lambda3 * lambda3 - a13); + double r3 = (lambda2 * lambda2 - 2.0 * lambda2 * lambda3 * b23 + lambda3 * lambda3 - a23); + + if (std::abs(r1) + std::abs(r2) + std::abs(r3) < 1e-10) + return; + + double x11 = lambda1 - lambda2 * b12; + double x12 = lambda2 - lambda1 * b12; + double x21 = lambda1 - lambda3 * b13; + double x23 = lambda3 - lambda1 * b13; + double x32 = lambda2 - lambda3 * b23; + double x33 = lambda3 - lambda2 * b23; + double detJ = 0.5 / (x11 * x23 * x32 + x12 * x21 * x33); // half minus inverse determinant + + // This uses the closed form of the inverse for the jacobian. + // Due to the zero elements this actually becomes quite nice. + lambda1 += (-x23 * x32 * r1 - x12 * x33 * r2 + x12 * x23 * r3) * detJ; + lambda2 += (-x21 * x33 * r1 + x11 * x33 * r2 - x11 * x23 * r3) * detJ; + lambda3 += (x21 * x32 * r1 - x11 * x32 * r2 - x12 * x21 * r3) * detJ; + } +} + +}; + +void p3p::calibrateAndNormalizePointsPnP(const Mat &opoints_, const Mat &ipoints_) { + auto convertPoints = [] (const Mat &points_input, Mat &points, int pt_dim) { + points_input.convertTo(points, CV_64F); // convert points to have float precision + if (points.channels() > 1) + points = points.reshape(1, (int)points.total()); // convert point to have 1 channel + if (points.rows < points.cols) + transpose(points, points); // transpose so points will be in rows + CV_CheckGE(points.cols, pt_dim, "Invalid dimension of point"); + if (points.cols != pt_dim) // in case when image points are 3D convert them to 2D + points = points.colRange(0, pt_dim); + }; + + Mat ipoints; + convertPoints(ipoints_, ipoints, 2); + for (int i = 0; i < ipoints.rows; i++) { + const double k_inv_u = ipoints.at(i, 0); + const double k_inv_v = ipoints.at(i, 1); + double x_norm = 1.0 / sqrt(k_inv_u*k_inv_u + k_inv_v*k_inv_v + 1); + x_copy[i](0) = k_inv_u * x_norm; + x_copy[i](1) = k_inv_v * x_norm; + x_copy[i](2) = x_norm; + } + + Mat opoints; + convertPoints(opoints_, opoints, 3); + X_copy[0](0) = opoints.at(0, 0); + X_copy[0](1) = opoints.at(0, 1); + X_copy[0](2) = opoints.at(0, 2); + + X_copy[1](0) = opoints.at(1, 0); + X_copy[1](1) = opoints.at(1, 1); + X_copy[1](2) = opoints.at(1, 2); + + X_copy[2](0) = opoints.at(2, 0); + X_copy[2](1) = opoints.at(2, 1); + X_copy[2](2) = opoints.at(2, 2); +} + +p3p::p3p() : + x_copy(), X_copy() +{ +} + +int p3p::estimate(std::vector& Rs, std::vector& ts, const cv::Mat& opoints, const cv::Mat& ipoints) { + CV_INSTRUMENT_REGION(); + calibrateAndNormalizePointsPnP(opoints, ipoints); + + Rs.reserve(4); + ts.reserve(4); + + Vec3d X01 = X_copy[0] - X_copy[1]; + Vec3d X02 = X_copy[0] - X_copy[2]; + Vec3d X12 = X_copy[1] - X_copy[2]; + + double a01 = norm(X01, NORM_L2SQR); + double a02 = norm(X02, NORM_L2SQR); + double a12 = norm(X12, NORM_L2SQR); + + std::array X = {X_copy[0], X_copy[1], X_copy[2]}; + std::array x = {x_copy[0], x_copy[1], x_copy[2]}; + + // Switch X,x so that BC is the largest distance among {X01, X02, X12} + if (a01 > a02) { + if (a01 > a12) { + std::swap(x[0], x[2]); + std::swap(X[0], X[2]); + std::swap(a01, a12); + X01 = -X12; + X02 = -X02; + } + } else if (a02 > a12) { + std::swap(x[0], x[1]); + std::swap(X[0], X[1]); + std::swap(a02, a12); + X01 = -X01; + X02 = X12; + } + + const double a12d = 1.0 / a12; + const double a = a01 * a12d; + const double b = a02 * a12d; + + const double m01 = x[0].dot(x[1]); + const double m02 = x[0].dot(x[2]); + const double m12 = x[1].dot(x[2]); + + // Ugly parameters to simplify the calculation + const double m12sq = -m12 * m12 + 1.0; + const double m02sq = -1.0 + m02 * m02; + const double m01sq = -1.0 + m01 * m01; + const double ab = a * b; + const double bsq = b * b; + const double asq = a * a; + const double m013 = -2.0 + 2.0 * m01 * m02 * m12; + const double bsqm12sq = bsq * m12sq; + const double asqm12sq = asq * m12sq; + const double abm12sq = 2.0 * ab * m12sq; + + const double k3_inv = 1.0 / (bsqm12sq + b * m02sq); + const double k2 = k3_inv * ((-1.0 + a) * m02sq + abm12sq + bsqm12sq + b * m013); + const double k1 = k3_inv * (asqm12sq + abm12sq + a * m013 + (-1.0 + b) * m01sq); + const double k0 = k3_inv * (asqm12sq + a * m01sq); + + double s; + bool G = yaqding::solve_cubic_single_real(k2, k1, k0, s); + + Matx33d C; + C(0, 0) = -a + s * (1 - b); + C(0, 1) = -m02 * s; + C(0, 2) = a * m12 + b * m12 * s; + C(1, 0) = C(0, 1); + C(1, 1) = s + 1; + C(1, 2) = -m01; + C(2, 0) = C(0, 2); + C(2, 1) = C(1, 2); + C(2, 2) = -a - b * s + 1; + + std::array pq = yaqding::compute_pq(C); + + // XX << X01, X02, X01.cross(X02); + // XX = XX.inverse().eval(); + Matx33d XX; + XX(0,0) = X01(0); XX(1,0) = X01(1); XX(2,0) = X01(2); + XX(0,1) = X02(0); XX(1,1) = X02(1); XX(2,1) = X02(2); + Vec3d X01_X02 = X01.cross(X02); + XX(0,2) = X01_X02(0); XX(1,2) = X01_X02(1); XX(2,2) = X01_X02(2); + XX = XX.inv(); + + int n_sols = 0; + for (int i = 0; i < 2; ++i) { + // [p0 p1 p2] * [1; x; y] = 0, or [p0 p1 p2] * [d2; d0; d1] = 0 + double p0 = pq[i](0); + double p1 = pq[i](1); + double p2 = pq[i](2); + // here we run into trouble if p0 is zero, + // so depending on which is larger, we solve for either d0 or d1 + // The case p0 = p1 = 0 is degenerate and can be ignored + bool switch_12 = std::abs(p0) <= std::abs(p1); + + if (switch_12) { + // eliminate d0 + double w0 = -p0 / p1; + double w1 = -p2 / p1; + double ca = 1.0 / (w1 * w1 - b); + double cb = 2.0 * (b * m12 - m02 * w1 + w0 * w1) * ca; + double cc = (w0 * w0 - 2 * m02 * w0 - b + 1.0) * ca; + double taus[2]; + + if (!yaqding::root2real(cb, cc, taus[0], taus[1])) + continue; + + for (double tau : taus) { + if (tau <= 0) + continue; + + // positive only + double d2 = std::sqrt(a12 / (tau * (tau - 2.0 * m12) + 1.0)); + double d1 = tau * d2; + double d0 = (w0 * d2 + w1 * d1); + if (d0 < 0) + continue; + + yaqding::refine_lambda(d0, d1, d2, a01, a02, a12, m01, m02, m12); + Vec3d v1 = d0 * x[0] - d1 * x[1]; + Vec3d v2 = d0 * x[0] - d2 * x[2]; + // YY << v1, v2, v1.cross(v2); + Matx33d YY; + YY(0,0) = v1(0); YY(1,0) = v1(1); YY(2,0) = v1(2); + YY(0,1) = v2(0); YY(1,1) = v2(1); YY(2,1) = v2(2); + Vec3d v1_v2 = v1.cross(v2); + YY(0,2) = v1_v2(0); YY(1,2) = v1_v2(1); YY(2,2) = v1_v2(2); + + // output->emplace_back(R, d0 * x[0] - R * X[0]); + Matx33d R = (YY * XX); + Rs.push_back(Mat(R)); + Vec3d trans = (d0 * x[0] - R * X[0]); + ts.push_back(Mat(trans)); + ++n_sols; + } + } else { + double w0 = -p1 / p0; + double w1 = -p2 / p0; + double ca = 1.0 / (-a * w1 * w1 + 2 * a * m12 * w1 - a + 1); + double cb = 2 * (a * m12 * w0 - m01 - a * w0 * w1) * ca; + double cc = (1 - a * w0 * w0) * ca; + + double taus[2]; + if (!yaqding::root2real(cb, cc, taus[0], taus[1])) + continue; + + for (double tau : taus) { + if (tau <= 0) + continue; + + double d0 = std::sqrt(a01 / (tau * (tau - 2.0 * m01) + 1.0)); + double d1 = tau * d0; + double d2 = w0 * d0 + w1 * d1; + + if (d2 < 0) + continue; + + yaqding::refine_lambda(d0, d1, d2, a01, a02, a12, m01, m02, m12); + Vec3d v1 = d0 * x[0] - d1 * x[1]; + Vec3d v2 = d0 * x[0] - d2 * x[2]; + // YY << v1, v2, v1.cross(v2); + Matx33d YY; + YY(0,0) = v1(0); YY(1,0) = v1(1); YY(2,0) = v1(2); + YY(0,1) = v2(0); YY(1,1) = v2(1); YY(2,1) = v2(2); + Vec3d v1_v2 = v1.cross(v2); + YY(0,2) = v1_v2(0); YY(1,2) = v1_v2(1); YY(2,2) = v1_v2(2); + + // output->emplace_back(R, d0 * x[0] - R * X[0]); + Matx33d R = (YY * XX); + Rs.push_back(Mat(R)); + Vec3d trans = (d0 * x[0] - R * X[0]); + ts.push_back(Mat(trans)); + ++n_sols; + } + } + + if (n_sols > 0 && G) + break; + } + + return n_sols; +} diff --git a/modules/calib3d/src/p3p.h b/modules/calib3d/src/p3p.h index 93e867d479..3bd8cf77ef 100644 --- a/modules/calib3d/src/p3p.h +++ b/modules/calib3d/src/p3p.h @@ -1,71 +1,18 @@ #ifndef P3P_H #define P3P_H - #include "precomp.hpp" -class p3p -{ - public: - p3p(double fx, double fy, double cx, double cy); - p3p(cv::Mat cameraMatrix); +class p3p { +public: + p3p(); + int estimate(std::vector& Rs, std::vector& ts, const cv::Mat& opoints, const cv::Mat& ipoints); - bool solve(cv::Mat& R, cv::Mat& tvec, const cv::Mat& opoints, const cv::Mat& ipoints); - int solve(std::vector& Rs, std::vector& tvecs, const cv::Mat& opoints, const cv::Mat& ipoints); - int solve(double R[4][3][3], double t[4][3], - double mu0, double mv0, double X0, double Y0, double Z0, - double mu1, double mv1, double X1, double Y1, double Z1, - double mu2, double mv2, double X2, double Y2, double Z2, - double mu3, double mv3, double X3, double Y3, double Z3, - bool p4p); - bool solve(double R[3][3], double t[3], - double mu0, double mv0, double X0, double Y0, double Z0, - double mu1, double mv1, double X1, double Y1, double Z1, - double mu2, double mv2, double X2, double Y2, double Z2, - double mu3, double mv3, double X3, double Y3, double Z3); +private: + void calibrateAndNormalizePointsPnP(const cv::Mat& opoints, const cv::Mat& ipoints); - private: - template - void init_camera_parameters(const cv::Mat& cameraMatrix) - { - cx = cameraMatrix.at (0, 2); - cy = cameraMatrix.at (1, 2); - fx = cameraMatrix.at (0, 0); - fy = cameraMatrix.at (1, 1); - } - template - void extract_points(const cv::Mat& opoints, const cv::Mat& ipoints, std::vector& points) - { - points.clear(); - int npoints = std::max(opoints.checkVector(3, CV_32F), opoints.checkVector(3, CV_64F)); - points.resize(5*4); //resize vector to fit for p4p case - for(int i = 0; i < npoints; i++) - { - points[i*5] = ipoints.at(i).x*fx + cx; - points[i*5+1] = ipoints.at(i).y*fy + cy; - points[i*5+2] = opoints.at(i).x; - points[i*5+3] = opoints.at(i).y; - points[i*5+4] = opoints.at(i).z; - } - //Fill vectors with unused values for p3p case - for (int i = npoints; i < 4; i++) { - for (int j = 0; j < 5; j++) { - points[i * 5 + j] = 0; - } - } - } - void init_inverse_parameters(); - int solve_for_lengths(double lengths[4][3], double distances[3], double cosines[3]); - bool align(double M_start[3][3], - double X0, double Y0, double Z0, - double X1, double Y1, double Z1, - double X2, double Y2, double Z2, - double R[3][3], double T[3]); - - bool jacobi_4x4(double * A, double * D, double * U); - - double fx, fy, cx, cy; - double inv_fx, inv_fy, cx_fx, cy_fy; + std::array x_copy; + std::array X_copy; }; #endif // P3P_H diff --git a/modules/calib3d/src/solvepnp.cpp b/modules/calib3d/src/solvepnp.cpp index eefdc4116f..49f62fc331 100644 --- a/modules/calib3d/src/solvepnp.cpp +++ b/modules/calib3d/src/solvepnp.cpp @@ -452,8 +452,8 @@ int solveP3P( InputArray _opoints, InputArray _ipoints, int solutions = 0; if (flags == SOLVEPNP_P3P) { - p3p P3Psolver(cameraMatrix); - solutions = P3Psolver.solve(Rs, ts, opoints, undistortedPoints); + p3p P3Psolver; + solutions = P3Psolver.estimate(Rs, ts, opoints, undistortedPoints); } else if (flags == SOLVEPNP_AP3P) { diff --git a/modules/calib3d/src/usac.hpp b/modules/calib3d/src/usac.hpp index 33de40c46d..c68416dafe 100644 --- a/modules/calib3d/src/usac.hpp +++ b/modules/calib3d/src/usac.hpp @@ -658,8 +658,6 @@ namespace Math { Matx33d getSkewSymmetric(const Vec3d &v_); // eliminate matrix with m rows and n columns to be upper triangular. bool eliminateUpperTriangular (std::vector &a, int m, int n); - Matx33d rotVec2RotMat (const Vec3d &v); - Vec3d rotMat2RotVec (const Matx33d &R); } class SolverPoly: public Algorithm { diff --git a/modules/calib3d/src/usac/dls_solver.cpp b/modules/calib3d/src/usac/dls_solver.cpp index 2e866b9389..76e7e5d645 100644 --- a/modules/calib3d/src/usac/dls_solver.cpp +++ b/modules/calib3d/src/usac/dls_solver.cpp @@ -199,7 +199,7 @@ public: // and translation. const double qi = s1, qi2 = qi*qi, qj = s2, qj2 = qj*qj, qk = s3, qk2 = qk*qk; const double s = 1 / (1 + qi2 + qj2 + qk2); - const Matx33d rot_mat (1-2*s*(qj2+qk2), 2*s*(qi*qj+qk), 2*s*(qi*qk-qj), + Matx33d rot_mat (1-2*s*(qj2+qk2), 2*s*(qi*qj+qk), 2*s*(qi*qk-qj), 2*s*(qi*qj-qk), 1-2*s*(qi2+qk2), 2*s*(qj*qk+qi), 2*s*(qi*qk+qj), 2*s*(qj*qk-qi), 1-2*s*(qi2+qj2)); const Matx31d soln_translation = translation_factor * rot_mat.reshape<9,1>(); @@ -217,8 +217,14 @@ public: } if (all_points_in_front_of_camera) { + // https://github.com/opencv/opencv/blob/2ba688f23c4e20754f32179d9396ba9b54b3b064/modules/calib3d/src/usac/pnp_solver.cpp#L395 + // Use directly cv::Rodrigues + Matx31d rvec; + Rodrigues(rot_mat, rvec); + Rodrigues(rvec, rot_mat); + Mat model; - hconcat(Math::rotVec2RotMat(Math::rotMat2RotVec(rot_mat)), soln_translation, model); + hconcat(rot_mat, soln_translation, model); models_.emplace_back(K * model); } } diff --git a/modules/calib3d/src/usac/pnp_solver.cpp b/modules/calib3d/src/usac/pnp_solver.cpp index db04770908..24c06ac22b 100644 --- a/modules/calib3d/src/usac/pnp_solver.cpp +++ b/modules/calib3d/src/usac/pnp_solver.cpp @@ -392,7 +392,12 @@ public: zw[1] = Zw2(0); zw[4] = Zw2(1); zw[7] = Zw2(2); zw[2] = Z3crZ1w(0); zw[5] = Z3crZ1w(1); zw[8] = Z3crZ1w(2); - const Matx33d R = Math::rotVec2RotMat(Math::rotMat2RotVec(Z * Zw.inv())); + Matx33d R = Z * Zw.inv(); + // https://github.com/opencv/opencv/blob/2ba688f23c4e20754f32179d9396ba9b54b3b064/modules/calib3d/src/usac/pnp_solver.cpp#L395 + // Use directly cv::Rodrigues + Matx31d rvec; + Rodrigues(R, rvec); + Rodrigues(rvec, R); Matx33d KR = K * R; Matx34d P; hconcat(KR, -KR * (X1 - R.t() * nX1), P); diff --git a/modules/calib3d/src/usac/utils.cpp b/modules/calib3d/src/usac/utils.cpp index 8d95fb9c33..52bc456588 100644 --- a/modules/calib3d/src/usac/utils.cpp +++ b/modules/calib3d/src/usac/utils.cpp @@ -422,46 +422,6 @@ Matx33d Math::getSkewSymmetric(const Vec3d &v) { -v[1], v[0], 0}; } -Matx33d Math::rotVec2RotMat (const Vec3d &v) { - const double phi = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); - const double x = v[0] / phi, y = v[1] / phi, z = v[2] / phi; - const double a = sin(phi), b = cos(phi); - // R = I + sin(phi) * skew(v) + (1 - cos(phi) * skew(v)^2 - return {(b - 1)*y*y + (b - 1)*z*z + 1, -a*z - x*y*(b - 1), a*y - x*z*(b - 1), - a*z - x*y*(b - 1), (b - 1)*x*x + (b - 1)*z*z + 1, -a*x - y*z*(b - 1), - -a*y - x*z*(b - 1), a*x - y*z*(b - 1), (b - 1)*x*x + (b - 1)*y*y + 1}; -} - -Vec3d Math::rotMat2RotVec (const Matx33d &R) { - // https://math.stackexchange.com/questions/83874/efficient-and-accurate-numerical-implementation-of-the-inverse-rodrigues-rotatio?rq=1 - Vec3d rot_vec; - const double trace = R(0,0)+R(1,1)+R(2,2); - if (trace >= 3 - FLT_EPSILON) { - rot_vec = (0.5 * (trace-3)/12)*Vec3d(R(2,1)-R(1,2), - R(0,2)-R(2,0), - R(1,0)-R(0,1)); - } else if (3 - FLT_EPSILON > trace && trace > -1 + FLT_EPSILON) { - double theta = acos((trace - 1) / 2); - rot_vec = (theta / (2 * sin(theta))) * Vec3d(R(2,1)-R(1,2), - R(0,2)-R(2,0), - R(1,0)-R(0,1)); - } else { - int a; - if (R(0,0) > R(1,1)) - a = R(0,0) > R(2,2) ? 0 : 2; - else - a = R(1,1) > R(2,2) ? 1 : 2; - Vec3d v; - int b = (a + 1) % 3, c = (a + 2) % 3; - double s = sqrt(R(a,a) - R(b,b) - R(c,c) + 1); - v[a] = s / 2; - v[b] = (R(b,a) + R(a,b)) / (2 * s); - v[c] = (R(c,a) + R(a,c)) / (2 * s); - rot_vec = M_PI * v / norm(v); - } - return rot_vec; -} - /* * Eliminate matrix of m rows and n columns to be upper triangular. */ diff --git a/modules/calib3d/test/test_solvepnp_ransac.cpp b/modules/calib3d/test/test_solvepnp_ransac.cpp index b3a8c56e3f..5dc89305cc 100644 --- a/modules/calib3d/test/test_solvepnp_ransac.cpp +++ b/modules/calib3d/test/test_solvepnp_ransac.cpp @@ -470,7 +470,7 @@ public: { eps[SOLVEPNP_ITERATIVE] = 1.0e-6; eps[SOLVEPNP_EPNP] = 1.0e-6; - eps[SOLVEPNP_P3P] = 2.0e-4; + eps[SOLVEPNP_P3P] = 1.0e-4; eps[SOLVEPNP_AP3P] = 1.0e-4; eps[SOLVEPNP_DLS] = 1.0e-6; // DLS is remapped to EPnP, so we use the same threshold eps[SOLVEPNP_UPNP] = 1.0e-6; // UPnP is remapped to EPnP, so we use the same threshold @@ -613,7 +613,7 @@ class CV_solveP3P_Test : public CV_solvePnPRansac_Test public: CV_solveP3P_Test() { - eps[SOLVEPNP_P3P] = 2.0e-4; + eps[SOLVEPNP_P3P] = 1.0e-4; eps[SOLVEPNP_AP3P] = 1.0e-4; totalTestsCount = 1000; } @@ -1513,7 +1513,7 @@ TEST(Calib3d_SolvePnP, generic) for (size_t i = 0; i < rvecs_est.size() && !isTestSuccess; i++) { double rvecDiff = cvtest::norm(rvecs_est[i], rvec_ground_truth, NORM_L2); double tvecDiff = cvtest::norm(tvecs_est[i], tvec_ground_truth, NORM_L2); - const double threshold = method == SOLVEPNP_P3P ? 1e-2 : 1e-4; + const double threshold = 1e-4; isTestSuccess = rvecDiff < threshold && tvecDiff < threshold; } @@ -1581,7 +1581,7 @@ TEST(Calib3d_SolvePnP, generic) for (size_t i = 0; i < rvecs_est.size() && !isTestSuccess; i++) { double rvecDiff = cvtest::norm(rvecs_est[i], rvec_ground_truth, NORM_L2); double tvecDiff = cvtest::norm(tvecs_est[i], tvec_ground_truth, NORM_L2); - const double threshold = method == SOLVEPNP_P3P ? 1e-2 : 1e-4; + const double threshold = 1e-4; isTestSuccess = rvecDiff < threshold && tvecDiff < threshold; } From e45d07e95b1ae7a9e9b849f07b45fcd9c5165db6 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Thu, 16 Oct 2025 22:18:39 +0300 Subject: [PATCH 21/29] Refactor minEnclosingCircle --- modules/imgproc/src/shapedescr.cpp | 105 ++++++++++------------------- 1 file changed, 34 insertions(+), 71 deletions(-) diff --git a/modules/imgproc/src/shapedescr.cpp b/modules/imgproc/src/shapedescr.cpp index 75e8582521..8d78f00f26 100644 --- a/modules/imgproc/src/shapedescr.cpp +++ b/modules/imgproc/src/shapedescr.cpp @@ -98,13 +98,7 @@ static void findThirdPoint(const PT *pts, int i, int j, Point2f ¢er, float & ptsf[0] = (Point2f)pts[i]; ptsf[1] = (Point2f)pts[j]; ptsf[2] = (Point2f)pts[k]; - Point2f new_center; float new_radius = 0; - findCircle3pts(ptsf, new_center, new_radius); - if (new_radius > 0) - { - radius = new_radius; - center = new_center; - } + findCircle3pts(ptsf, center, radius); } } } @@ -129,13 +123,7 @@ void findSecondPoint(const PT *pts, int i, Point2f ¢er, float &radius) } else { - Point2f new_center; float new_radius = 0; - findThirdPoint(pts, i, j, new_center, new_radius); - if (new_radius > 0) - { - radius = new_radius; - center = new_center; - } + findThirdPoint(pts, i, j, center, radius); } } } @@ -161,13 +149,7 @@ static void findMinEnclosingCircle(const PT *pts, int count, Point2f ¢er, fl } else { - Point2f new_center; float new_radius = 0; - findSecondPoint(pts, i, new_center, new_radius); - if (new_radius > 0) - { - radius = new_radius; - center = new_center; - } + findSecondPoint(pts, i, center, radius); } } } @@ -193,61 +175,42 @@ void cv::minEnclosingCircle( InputArray _points, Point2f& _center, float& _radiu const Point* ptsi = points.ptr(); const Point2f* ptsf = points.ptr(); - switch (count) + if( count == 1 ) { - case 1: - { - _center = (is_float) ? ptsf[0] : Point2f((float)ptsi[0].x, (float)ptsi[0].y); - _radius = EPS; - break; - } - case 2: - { - Point2f p1 = (is_float) ? ptsf[0] : Point2f((float)ptsi[0].x, (float)ptsi[0].y); - Point2f p2 = (is_float) ? ptsf[1] : Point2f((float)ptsi[1].x, (float)ptsi[1].y); - _center.x = (p1.x + p2.x) / 2.0f; - _center.y = (p1.y + p2.y) / 2.0f; - _radius = (float)(norm(p1 - p2) / 2.0) + EPS; - break; - } - default: - { - Point2f center; - float radius = 0.f; - if (is_float) + _center = (is_float) ? ptsf[0] : Point2f((float)ptsi[0].x, (float)ptsi[0].y); + _radius = EPS; + return; + } + + if (is_float) + { + findMinEnclosingCircle(ptsf, count, _center, _radius); + #if 0 + for (int m = 0; m < count; ++m) { - findMinEnclosingCircle(ptsf, count, center, radius); - #if 0 - for (size_t m = 0; m < count; ++m) - { - float d = (float)norm(ptsf[m] - center); - if (d > radius) - { - printf("error!\n"); - } - } - #endif + float d = (float)norm(ptsf[m] - _center); + if (d > _radius) + { + printf("error!\n"); + } } - else + #endif + } + else + { + findMinEnclosingCircle(ptsi, count, _center, _radius); + #if 0 + for (int m = 0; m < count; ++m) { - findMinEnclosingCircle(ptsi, count, center, radius); - #if 0 - for (size_t m = 0; m < count; ++m) - { - double dx = ptsi[m].x - center.x; - double dy = ptsi[m].y - center.y; - double d = std::sqrt(dx * dx + dy * dy); - if (d > radius) - { - printf("error!\n"); - } - } - #endif + double dx = ptsi[m].x - _center.x; + double dy = ptsi[m].y - _center.y; + double d = std::sqrt(dx * dx + dy * dy); + if (d > _radius) + { + printf("error!\n"); + } } - _center = center; - _radius = radius; - break; - } + #endif } } From 98a12515fec3c79021c8a56acab6354e0764c43a Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Thu, 16 Oct 2025 22:58:55 +0300 Subject: [PATCH 22/29] Fix ml::KDTree::findNearest --- modules/ml/src/knearest.cpp | 17 +++++++++++++++-- modules/ml/test/test_knearest.cpp | 7 ++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/ml/src/knearest.cpp b/modules/ml/src/knearest.cpp index 3d8f9b5d2e..bd9137b24b 100644 --- a/modules/ml/src/knearest.cpp +++ b/modules/ml/src/knearest.cpp @@ -396,8 +396,21 @@ public: { Mat _res, _nr, _d; tr.findNearest(test_samples.row(i), k, Emax, _res, _nr, _d, noArray()); - res.push_back(_res.t()); - _results.assign(res); + if( _results.needed() ) + { + res.push_back(_res.t()); + _results.assign(res); + } + if( _neighborResponses.needed() ) + { + nr.push_back(_nr.t()); + _neighborResponses.assign(nr); + } + if( _dists.needed() ) + { + d.push_back(_d.t()); + _dists.assign(d); + } } return result; // currently always 0 diff --git a/modules/ml/test/test_knearest.cpp b/modules/ml/test/test_knearest.cpp index 80baed9626..56d3e94269 100644 --- a/modules/ml/test/test_knearest.cpp +++ b/modules/ml/test/test_knearest.cpp @@ -39,11 +39,16 @@ TEST(ML_KNearest, accuracy) { SCOPED_TRACE("KDTree"); Mat neighborIndexes; + Mat neighborResponses; + Mat dists; float err = 1000; Ptr knn = KNearest::create(); knn->setAlgorithmType(KNearest::KDTREE); knn->train(trainData, ml::ROW_SAMPLE, trainLabels); - knn->findNearest(testData, 4, neighborIndexes); + knn->findNearest(testData, 4, neighborIndexes, neighborResponses, dists); + EXPECT_EQ(neighborIndexes.size(), Size(4, pointsCount)); + EXPECT_EQ(neighborResponses.size(), Size(4, pointsCount * 2)); + EXPECT_EQ(dists.size(), Size(4, pointsCount)); Mat bestLabels; // The output of the KDTree are the neighbor indexes, not actual class labels // so we need to do some extra work to get actual predictions From f1a99760ad00c1670d24ea4ffa4a5a4b9417a7ca Mon Sep 17 00:00:00 2001 From: Dmitry Kurtaev Date: Fri, 17 Oct 2025 15:23:24 +0800 Subject: [PATCH 23/29] Merge pull request #27841 from dkurt:ffmpeg_camera Open camera device by index through FFmpeg #27841 ### Pull Request Readiness Checklist resolves https://github.com/opencv/opencv/issues/26812 Example of explicit backend option (similar to `-f v4l2` from command line) ``` export OPENCV_FFMPEG_CAPTURE_OPTIONS="f;v4l2" ``` see https://trac.ffmpeg.org/wiki/Capture/Webcam for available options 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 - [x] The PR is proposed to the proper branch - [x] 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 --- modules/videoio/cmake/detect_ffmpeg.cmake | 2 +- modules/videoio/src/cap_ffmpeg.cpp | 36 ++++++++-- modules/videoio/src/cap_ffmpeg_impl.hpp | 88 +++++++++++++++++++---- modules/videoio/src/cap_interface.hpp | 1 + modules/videoio/src/videoio_registry.cpp | 6 ++ modules/videoio/test/test_camera.cpp | 14 ++++ modules/videoio/test/test_v4l2.cpp | 14 ++++ 7 files changed, 139 insertions(+), 22 deletions(-) diff --git a/modules/videoio/cmake/detect_ffmpeg.cmake b/modules/videoio/cmake/detect_ffmpeg.cmake index 1d4faa8394..dd5eefaac9 100644 --- a/modules/videoio/cmake/detect_ffmpeg.cmake +++ b/modules/videoio/cmake/detect_ffmpeg.cmake @@ -1,5 +1,5 @@ # --- FFMPEG --- -OCV_OPTION(OPENCV_FFMPEG_ENABLE_LIBAVDEVICE "Include FFMPEG/libavdevice library support." OFF +OCV_OPTION(OPENCV_FFMPEG_ENABLE_LIBAVDEVICE "Include FFMPEG/libavdevice library support." ON VISIBLE_IF WITH_FFMPEG) if(NOT HAVE_FFMPEG AND OPENCV_FFMPEG_USE_FIND_PACKAGE) diff --git a/modules/videoio/src/cap_ffmpeg.cpp b/modules/videoio/src/cap_ffmpeg.cpp index cc62920c74..09ed1f43c2 100644 --- a/modules/videoio/src/cap_ffmpeg.cpp +++ b/modules/videoio/src/cap_ffmpeg.cpp @@ -74,6 +74,11 @@ public: { open(filename, params); } + CvCapture_FFMPEG_proxy(int index, const cv::VideoCaptureParameters& params) + : ffmpegCapture(NULL) + { + open(index, params); + } CvCapture_FFMPEG_proxy(const Ptr& stream, const cv::VideoCaptureParameters& params) : ffmpegCapture(NULL) { @@ -127,6 +132,13 @@ public: ffmpegCapture = cvCreateFileCaptureWithParams_FFMPEG(filename.c_str(), params); return ffmpegCapture != 0; } + bool open(int index, const cv::VideoCaptureParameters& params) + { + close(); + + ffmpegCapture = cvCreateFileCaptureWithParams_FFMPEG(index, params); + return ffmpegCapture != 0; + } bool open(const Ptr& stream, const cv::VideoCaptureParameters& params) { close(); @@ -161,6 +173,14 @@ cv::Ptr cvCreateFileCapture_FFMPEG_proxy(const std::string &f return cv::Ptr(); } +cv::Ptr cvCreateCameraCapture_FFMPEG_proxy(int index, const cv::VideoCaptureParameters& params) +{ + cv::Ptr capture = cv::makePtr(index, params); + if (capture && capture->isOpened()) + return capture; + return cv::Ptr(); +} + cv::Ptr cvCreateStreamCapture_FFMPEG_proxy(const Ptr& stream, const cv::VideoCaptureParameters& params) { cv::Ptr capture = std::make_shared(stream, params); @@ -272,13 +292,15 @@ CvResult CV_API_CALL cv_capture_open(const char* filename, int camera_index, CV_ if (!handle) return CV_ERROR_FAIL; *handle = NULL; - if (!filename) + if (!filename && camera_index < 0) return CV_ERROR_FAIL; - CV_UNUSED(camera_index); CvCapture_FFMPEG_proxy *cap = 0; try { - cap = new CvCapture_FFMPEG_proxy(String(filename), cv::VideoCaptureParameters()); + if (filename) + cap = new CvCapture_FFMPEG_proxy(String(filename), cv::VideoCaptureParameters()); + else + cap = new CvCapture_FFMPEG_proxy(camera_index, cv::VideoCaptureParameters()); if (cap->isOpened()) { *handle = (CvPluginCapture)cap; @@ -308,14 +330,16 @@ CvResult CV_API_CALL cv_capture_open_with_params( if (!handle) return CV_ERROR_FAIL; *handle = NULL; - if (!filename) + if (!filename && camera_index < 0) return CV_ERROR_FAIL; - CV_UNUSED(camera_index); CvCapture_FFMPEG_proxy *cap = 0; try { cv::VideoCaptureParameters parameters(params, n_params); - cap = new CvCapture_FFMPEG_proxy(String(filename), parameters); + if (filename) + cap = new CvCapture_FFMPEG_proxy(String(filename), parameters); + else + cap = new CvCapture_FFMPEG_proxy(camera_index, parameters); if (cap->isOpened()) { *handle = (CvPluginCapture)cap; diff --git a/modules/videoio/src/cap_ffmpeg_impl.hpp b/modules/videoio/src/cap_ffmpeg_impl.hpp index 6d7749d21c..7834c59111 100644 --- a/modules/videoio/src/cap_ffmpeg_impl.hpp +++ b/modules/videoio/src/cap_ffmpeg_impl.hpp @@ -526,7 +526,7 @@ inline static std::string _opencv_ffmpeg_get_error_string(int error_code) struct CvCapture_FFMPEG { - bool open(const char* filename, const Ptr& stream, const VideoCaptureParameters& params); + bool open(const char* filename, int index, const Ptr& stream, const VideoCaptureParameters& params); void close(); double getProperty(int) const; @@ -1043,7 +1043,7 @@ static bool isThreadSafe() { return threadSafe; } -bool CvCapture_FFMPEG::open(const char* _filename, const Ptr& stream, const VideoCaptureParameters& params) +bool CvCapture_FFMPEG::open(const char* _filename, int index, const Ptr& stream, const VideoCaptureParameters& params) { const bool threadSafe = isThreadSafe(); InternalFFMpegRegister::init(threadSafe); @@ -1145,16 +1145,6 @@ bool CvCapture_FFMPEG::open(const char* _filename, const Ptr& str } } -#if USE_AV_INTERRUPT_CALLBACK - /* interrupt callback */ - interrupt_metadata.timeout_after_ms = open_timeout; - get_monotonic_time(&interrupt_metadata.value); - - ic = avformat_alloc_context(); - ic->interrupt_callback.callback = _opencv_ffmpeg_interrupt_callback; - ic->interrupt_callback.opaque = &interrupt_metadata; -#endif - std::string options = utils::getConfigurationParameterString("OPENCV_FFMPEG_CAPTURE_OPTIONS"); if (options.empty()) { @@ -1180,7 +1170,53 @@ bool CvCapture_FFMPEG::open(const char* _filename, const Ptr& str input_format = av_find_input_format(entry->value); } - if (!_filename) + AVDeviceInfoList* device_list = nullptr; + if (index >= 0) + { +#ifdef HAVE_FFMPEG_LIBAVDEVICE + entry = av_dict_get(dict, "f", NULL, 0); + const char* backend = nullptr; + if (entry) + { + backend = entry->value; + } + else + { +#ifdef __linux__ + backend = "v4l2"; +#endif +#ifdef _WIN32 + backend = "dshow"; +#endif +#ifdef __APPLE__ + backend = "avfoundation"; +#endif + } + avdevice_list_input_sources(nullptr, backend, nullptr, &device_list); + if (!device_list) + { + CV_LOG_ONCE_WARNING(NULL, "VIDEOIO/FFMPEG: Failed list devices for backend " << backend); + return false; + } + CV_CheckLT(index, device_list->nb_devices, "VIDEOIO/FFMPEG: Camera index out of range"); + _filename = device_list->devices[index]->device_name; +#else + CV_LOG_ONCE_WARNING(NULL, "VIDEOIO/FFMPEG: OpenCV should be configured with libavdevice to open a camera device"); + return false; +#endif + } + +#if USE_AV_INTERRUPT_CALLBACK + /* interrupt callback */ + interrupt_metadata.timeout_after_ms = open_timeout; + get_monotonic_time(&interrupt_metadata.value); + + ic = avformat_alloc_context(); + ic->interrupt_callback.callback = _opencv_ffmpeg_interrupt_callback; + ic->interrupt_callback.opaque = &interrupt_metadata; +#endif + + if (stream) { size_t avio_ctx_buffer_size = 4096; uint8_t* avio_ctx_buffer = (uint8_t*)av_malloc(avio_ctx_buffer_size); @@ -1231,6 +1267,13 @@ bool CvCapture_FFMPEG::open(const char* _filename, const Ptr& str ic->pb = avio_context; } int err = avformat_open_input(&ic, _filename, input_format, &dict); + if (device_list) + { +#ifdef HAVE_FFMPEG_LIBAVDEVICE + avdevice_free_list_devices(&device_list); + device_list = nullptr; +#endif + } if (err < 0) { @@ -3447,7 +3490,22 @@ CvCapture_FFMPEG* cvCreateFileCaptureWithParams_FFMPEG(const char* filename, con if (!capture) return 0; capture->init(); - if (capture->open(filename, nullptr, params)) + if (capture->open(filename, -1, nullptr, params)) + return capture; + + capture->close(); + delete capture; + return 0; +} + +static +CvCapture_FFMPEG* cvCreateFileCaptureWithParams_FFMPEG(int index, const VideoCaptureParameters& params) +{ + CvCapture_FFMPEG* capture = new CvCapture_FFMPEG(); + if (!capture) + return 0; + capture->init(); + if (capture->open(nullptr, index, nullptr, params)) return capture; capture->close(); @@ -3462,7 +3520,7 @@ CvCapture_FFMPEG* cvCreateStreamCaptureWithParams_FFMPEG(const Ptrinit(); - if (capture->open(nullptr, stream, params)) + if (capture->open(nullptr, -1, stream, params)) return capture; capture->close(); diff --git a/modules/videoio/src/cap_interface.hpp b/modules/videoio/src/cap_interface.hpp index bb1e0b4ec9..ba50f6dcad 100644 --- a/modules/videoio/src/cap_interface.hpp +++ b/modules/videoio/src/cap_interface.hpp @@ -327,6 +327,7 @@ protected: //================================================================================================== Ptr cvCreateFileCapture_FFMPEG_proxy(const std::string &filename, const VideoCaptureParameters& params); +Ptr cvCreateCameraCapture_FFMPEG_proxy(int index, const VideoCaptureParameters& params); Ptr cvCreateStreamCapture_FFMPEG_proxy(const Ptr& stream, const VideoCaptureParameters& params); Ptr cvCreateVideoWriter_FFMPEG_proxy(const std::string& filename, int fourcc, double fps, const Size& frameSize, diff --git a/modules/videoio/src/videoio_registry.cpp b/modules/videoio/src/videoio_registry.cpp index a84258ad90..9c599d2c71 100644 --- a/modules/videoio/src/videoio_registry.cpp +++ b/modules/videoio/src/videoio_registry.cpp @@ -112,6 +112,12 @@ static const struct VideoBackendInfo builtin_backends[] = DECLARE_STATIC_BACKEND(CAP_V4L, "V4L_BSD", MODE_CAPTURE_ALL, create_V4L_capture_file, create_V4L_capture_cam, 0) #endif + // FFmpeg webcamera by underlying backend (DShow, V4L2, AVFoundation) +#ifdef HAVE_FFMPEG + DECLARE_STATIC_BACKEND(CAP_FFMPEG, "FFMPEG", MODE_CAPTURE_BY_INDEX, 0, cvCreateCameraCapture_FFMPEG_proxy, 0) +#elif defined(ENABLE_PLUGINS) || defined(HAVE_FFMPEG_WRAPPER) + DECLARE_DYNAMIC_BACKEND(CAP_FFMPEG, "FFMPEG", MODE_CAPTURE_BY_INDEX) +#endif // RGB-D universal #ifdef HAVE_OPENNI2 diff --git a/modules/videoio/test/test_camera.cpp b/modules/videoio/test/test_camera.cpp index f11fa3f251..30592b2932 100644 --- a/modules/videoio/test/test_camera.cpp +++ b/modules/videoio/test/test_camera.cpp @@ -327,4 +327,18 @@ TEST(DISABLED_videoio_camera, waitAny_V4L) } } +TEST(DISABLED_videoio_camera, ffmpeg_index) +{ + int idx = (int)utils::getConfigurationParameterSizeT("OPENCV_TEST_FFMPEG_DEVICE_IDX", (size_t)-1); + if (idx == -1) + { + throw SkipTestException("OPENCV_TEST_FFMPEG_DEVICE_IDX is not set"); + } + VideoCapture cap; + ASSERT_TRUE(cap.open(idx, CAP_FFMPEG)); + Mat frame; + ASSERT_TRUE(cap.read(frame)); + ASSERT_FALSE(frame.empty()); +} + }} // namespace diff --git a/modules/videoio/test/test_v4l2.cpp b/modules/videoio/test/test_v4l2.cpp index 6ea1f67e6d..d560e718bd 100644 --- a/modules/videoio/test/test_v4l2.cpp +++ b/modules/videoio/test/test_v4l2.cpp @@ -158,6 +158,20 @@ inline static std::string param_printer(const testing::TestParamInfo:: #endif // HAVE_CAMV4L2 From 75f738b3d15a2fae7c78dd208312337d7b4a47fe Mon Sep 17 00:00:00 2001 From: Kumataro Date: Sun, 19 Oct 2025 14:15:08 +0900 Subject: [PATCH 24/29] imgcodecs: Workaround for image flipping bug in older GDAL FITS drivers --- modules/imgcodecs/src/grfmt_gdal.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_gdal.cpp b/modules/imgcodecs/src/grfmt_gdal.cpp index ec7f70a925..8f81dd0b72 100644 --- a/modules/imgcodecs/src/grfmt_gdal.cpp +++ b/modules/imgcodecs/src/grfmt_gdal.cpp @@ -425,8 +425,18 @@ bool GdalDecoder::readData( Mat& img ){ // create a temporary scanline pointer to store data double* scanline = new double[nCols]; +#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,3,0) + // FITS drivers on version GDAL prior to v3.3.0 return vertically mirrored results. + // See https://github.com/OSGeo/gdal/pull/3520 + // See https://github.com/OSGeo/gdal/commit/ef0f86696d163e065943b27f50dcff77790a1311 + const bool isNeedVerticallyFlip = strncmp(m_dataset->GetDriverName(), "FITS", 4) == 0; +#else + const bool isNeedVerticallyFlip = false; +#endif + // iterate over each row and column - for( int y=0; yRasterIO( GF_Read, 0, y, nCols, 1, scanline, nCols, 1, GDT_Float64, 0, 0); @@ -438,10 +448,10 @@ bool GdalDecoder::readData( Mat& img ){ // set depending on image types // given boost, I would use enable_if to speed up. Avoid for now. if( hasColorTable == false ){ - write_pixel( scanline[x], gdalType, nChannels, img, y, x, color ); + write_pixel( scanline[x], gdalType, nChannels, img, yCv, x, color ); } else{ - write_ctable_pixel( scanline[x], gdalType, gdalColorTable, img, y, x, color ); + write_ctable_pixel( scanline[x], gdalType, gdalColorTable, img, yCv, x, color ); } } } From 455720aae8ef7539706a5e442bed4a75826d2825 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Sun, 19 Oct 2025 19:34:13 +0300 Subject: [PATCH 25/29] Support QR decomposition for stereoCalibrate --- modules/calib3d/src/calibration.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/calib3d/src/calibration.cpp b/modules/calib3d/src/calibration.cpp index d046a9b58e..09ecf0953c 100644 --- a/modules/calib3d/src/calibration.cpp +++ b/modules/calib3d/src/calibration.cpp @@ -755,6 +755,9 @@ static double stereoCalibrateImpl( if(flags & CALIB_USE_LU) { solver.solveMethod = DECOMP_LU; } + else if(flags & CALIB_USE_QR) { + solver.solveMethod = DECOMP_QR; + } if( recomputeIntrinsics ) { From 346308124ab0815ae9f5c3e1edc7003026e28014 Mon Sep 17 00:00:00 2001 From: Kumataro Date: Mon, 20 Oct 2025 18:37:04 +0900 Subject: [PATCH 26/29] imgcodecs: GDAL: show GDAL version at OpenCV configuration --- cmake/OpenCVFindLibsGrfmt.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index 8c7bc939b8..c3c358f6fc 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -385,6 +385,16 @@ if(WITH_GDAL) else() set(HAVE_GDAL YES) ocv_include_directories(${GDAL_INCLUDE_DIR}) + + # GDAL_VERSION requires CMake 3.14 or later, if not found, GDAL_RELEASE_NAME is used instead. + # See https://cmake.org/cmake/help/latest/module/FindGDAL.html + if(NOT GDAL_VERSION) + if(EXISTS "${GDAL_INCLUDE_DIR}/gdal_version.h") + file(STRINGS "${GDAL_INCLUDE_DIR}/gdal_version.h" gdal_version_str REGEX "^#[\t ]+define[\t ]+GDAL_RELEASE_NAME[\t ]*") + string(REGEX REPLACE "^#\[\t ]+define[\t ]+GDAL_RELEASE_NAME[\t ]+\"([^ \\n]*)\"" "\\1" GDAL_VERSION "${gdal_version_str}") + unset(gdal_version_str) + endif() + endif() endif() endif() From ff216e879656414cd9af7d722201d03e1012e102 Mon Sep 17 00:00:00 2001 From: cudawarped <12133430+cudawarped@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:58:39 +0300 Subject: [PATCH 27/29] [core][cuda] Move throw_no_cuda to it an independant stub so it is not included in the same file that requires cudart --- .../include/opencv2/core/private.cuda.hpp | 9 ++------ .../opencv2/core/private/cuda_stubs.hpp | 21 +++++++++++++++++++ modules/photo/CMakeLists.txt | 4 ++++ modules/photo/src/denoising.cuda.cpp | 4 +++- modules/stitching/CMakeLists.txt | 4 ++++ 5 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 modules/core/include/opencv2/core/private/cuda_stubs.hpp diff --git a/modules/core/include/opencv2/core/private.cuda.hpp b/modules/core/include/opencv2/core/private.cuda.hpp index 4250f61033..c087ee8028 100644 --- a/modules/core/include/opencv2/core/private.cuda.hpp +++ b/modules/core/include/opencv2/core/private.cuda.hpp @@ -54,6 +54,7 @@ #include "opencv2/core/base.hpp" #include "opencv2/core/cuda.hpp" +#include "opencv2/core/private/cuda_stubs.hpp" #ifdef HAVE_CUDA # include @@ -101,16 +102,10 @@ namespace cv { namespace cuda { CV_EXPORTS void syncOutput(const GpuMat& dst, OutputArray _dst, Stream& stream); }} -#ifndef HAVE_CUDA - -static inline CV_NORETURN void throw_no_cuda() { CV_Error(cv::Error::GpuNotSupported, "The library is compiled without CUDA support"); } - -#else // HAVE_CUDA +#ifdef HAVE_CUDA #define nppSafeSetStream(oldStream, newStream) { if(oldStream != newStream) { cudaStreamSynchronize(oldStream); nppSetStream(newStream); } } -static inline CV_NORETURN void throw_no_cuda() { CV_Error(cv::Error::StsNotImplemented, "The called functionality is disabled for current build or platform"); } - namespace cv { namespace cuda { static inline void checkNppError(int code, const char* file, const int line, const char* func) diff --git a/modules/core/include/opencv2/core/private/cuda_stubs.hpp b/modules/core/include/opencv2/core/private/cuda_stubs.hpp new file mode 100644 index 0000000000..478a9fdd20 --- /dev/null +++ b/modules/core/include/opencv2/core/private/cuda_stubs.hpp @@ -0,0 +1,21 @@ +// 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. + +#ifndef OPENCV_CORE_PRIVATE_CUDA_STUBS_HPP +#define OPENCV_CORE_PRIVATE_CUDA_STUBS_HPP + +#ifndef __OPENCV_BUILD +# error this is a private header which should not be used from outside of the OpenCV library +#endif + +#include "opencv2/core/cvdef.h" +#include "opencv2/core/base.hpp" + +#ifndef HAVE_CUDA +static inline CV_NORETURN void throw_no_cuda() { CV_Error(cv::Error::GpuNotSupported, "The library is compiled without CUDA support"); } +#else +static inline CV_NORETURN void throw_no_cuda() { CV_Error(cv::Error::StsNotImplemented, "The called functionality is disabled for current build or platform"); } +#endif + +#endif // OPENCV_CORE_PRIVATE_CUDA_STUBS_HPP diff --git a/modules/photo/CMakeLists.txt b/modules/photo/CMakeLists.txt index 34e3741601..f65cf9e678 100644 --- a/modules/photo/CMakeLists.txt +++ b/modules/photo/CMakeLists.txt @@ -5,3 +5,7 @@ if(HAVE_CUDA) endif() ocv_define_module(photo opencv_imgproc OPTIONAL opencv_cudaarithm opencv_cudaimgproc WRAP java objc python js) + +if(HAVE_CUDA AND ENABLE_CUDA_FIRST_CLASS_LANGUAGE AND HAVE_OPENCV_CUDAARITHM AND HAVE_OPENCV_CUDAIMGPROC) + ocv_target_link_libraries(${the_module} PUBLIC "CUDA::cudart${CUDA_LIB_EXT}") +endif() diff --git a/modules/photo/src/denoising.cuda.cpp b/modules/photo/src/denoising.cuda.cpp index 7ea37f6951..d9e265cda9 100644 --- a/modules/photo/src/denoising.cuda.cpp +++ b/modules/photo/src/denoising.cuda.cpp @@ -43,7 +43,6 @@ #include "precomp.hpp" #include "opencv2/photo/cuda.hpp" -#include "opencv2/core/private.cuda.hpp" #include "opencv2/opencv_modules.hpp" @@ -60,12 +59,15 @@ using namespace cv::cuda; #if !defined (HAVE_CUDA) || !defined(HAVE_OPENCV_CUDAARITHM) || !defined(HAVE_OPENCV_CUDAIMGPROC) +#include "opencv2/core/private/cuda_stubs.hpp" void cv::cuda::nonLocalMeans(InputArray, OutputArray, float, int, int, int, Stream&) { throw_no_cuda(); } void cv::cuda::fastNlMeansDenoising(InputArray, OutputArray, float, int, int, Stream&) { throw_no_cuda(); } void cv::cuda::fastNlMeansDenoisingColored(InputArray, OutputArray, float, float, int, int, Stream&) { throw_no_cuda(); } #else +#include "opencv2/core/private.cuda.hpp" + ////////////////////////////////////////////////////////////////////////////////// //// Non Local Means Denosing (brute force) diff --git a/modules/stitching/CMakeLists.txt b/modules/stitching/CMakeLists.txt index 44f35eb59b..e488369de8 100644 --- a/modules/stitching/CMakeLists.txt +++ b/modules/stitching/CMakeLists.txt @@ -11,3 +11,7 @@ endif() ocv_define_module(stitching opencv_imgproc opencv_features2d opencv_calib3d opencv_flann OPTIONAL opencv_cudaarithm opencv_cudawarping opencv_cudafeatures2d opencv_cudalegacy opencv_cudaimgproc ${STITCHING_CONTRIB_DEPS} WRAP python) + +if(HAVE_CUDA AND ENABLE_CUDA_FIRST_CLASS_LANGUAGE) + ocv_target_link_libraries(${the_module} PUBLIC "CUDA::cudart${CUDA_LIB_EXT}") +endif() From b65d66e797621e3340fdbff3056e3fd6b2483500 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Tue, 21 Oct 2025 04:56:22 +0300 Subject: [PATCH 28/29] Add comments for fisheye::undistortPoints --- modules/calib3d/src/fisheye.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/calib3d/src/fisheye.cpp b/modules/calib3d/src/fisheye.cpp index a74ac5cf7a..9a2e04a5eb 100644 --- a/modules/calib3d/src/fisheye.cpp +++ b/modules/calib3d/src/fisheye.cpp @@ -429,8 +429,15 @@ void cv::fisheye::undistortPoints( InputArray distorted, OutputArray undistorted for(size_t i = 0; i < n; i++ ) { Vec2d pi = sdepth == CV_32F ? (Vec2d)srcf[i] : srcd[i]; // image point + // u = fx * x' + cx (alpha = 0), v = fy * y' + cy => + // x' = (u - cx) / fx, y' = (v - cy) / fy Vec2d pw((pi[0] - c[0])/f[0], (pi[1] - c[1])/f[1]); // world point + // x' = (theta_d / r) * a, y' = (theta_d / r) * b => + // x'^2 + y'^2 = theta_d^2 * (a^2 + b^2) / r^2 => + // (r^2 = a^2 + b^2) + // x'^2 + y'^2 = theta_d^2 => + // theta_d = sqrt(x'^2 + y'^2) double theta_d = sqrt(pw[0]*pw[0] + pw[1]*pw[1]); // the current camera model is only valid up to 180 FOV @@ -449,9 +456,14 @@ void cv::fisheye::undistortPoints( InputArray distorted, OutputArray undistorted for (int j = 0; j < maxCount; j++) { + // theta_d = theta * (1 + k1 * theta^2 + k2 * theta^4 + k3 * theta^6 + k4 * theta^8) => + // f(theta) := theta * (1 + k1 * theta^2 + k2 * theta^4 + k3 * theta^6 + k4 * theta^8) - theta_d = 0 + // Newton's method: new_theta = theta - theta_fix, theta_fix := f(theta) / f'(theta) + // f'(theta) = (theta * (1 + k1 * theta^2 + k2 * theta^4 + k3 * theta^6 + k4 * theta^8) - theta_d)' = + // (theta + k1 * theta^3 + k2 * theta^5 + k3 * theta^7 + k4 * theta^9 - theta_d)' = + // 1 + 3 * k1 * theta^2 + 5 * k2 * theta^4 + 7 * k3 * theta^6 + 9 * k4 * theta^8 double theta2 = theta*theta, theta4 = theta2*theta2, theta6 = theta4*theta2, theta8 = theta6*theta2; double k0_theta2 = k[0] * theta2, k1_theta4 = k[1] * theta4, k2_theta6 = k[2] * theta6, k3_theta8 = k[3] * theta8; - /* new_theta = theta - theta_fix, theta_fix = f0(theta) / f0'(theta) */ double theta_fix = (theta * (1 + k0_theta2 + k1_theta4 + k2_theta6 + k3_theta8) - theta_d) / (1 + 3*k0_theta2 + 5*k1_theta4 + 7*k2_theta6 + 9*k3_theta8); theta = theta - theta_fix; @@ -463,6 +475,10 @@ void cv::fisheye::undistortPoints( InputArray distorted, OutputArray undistorted } } + // x' = (theta_d / r) * a, y' = (theta_d / r) * b => + // a = x' * r / theta_d, b = y' * r / theta_d => + // (theta = atan(r) => r = tan(theta), scale := r / theta_d = tan(theta) / theta_d) + // a = x' * scale, b = y' * scale scale = std::tan(theta) / theta_d; } else @@ -477,6 +493,7 @@ void cv::fisheye::undistortPoints( InputArray distorted, OutputArray undistorted if ((converged || !isEps) && !theta_flipped) { + // a = x' * scale, b = y' * scale Vec2d pu = pw * scale; //undistorted point Vec2d fi; From 47d71530a729eda8534920ab3c97ffcde2c3b992 Mon Sep 17 00:00:00 2001 From: Jay Pol <76426666+JayPol559@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:00:58 +0530 Subject: [PATCH 29/29] Merge pull request #27866 from JayPol559:4.x Fix HDR tutorial result mismatch by adding gamma note #27866 This PR fixes #22219 by clarifying the gamma correction value in the HDR tutorial. The function cv.createTonemap() has a default gamma value of 1.0. To match the tutorial example results, gamma should be explicitly set to 2.2. This note has been added to the Tonemap HDR image section of the tutorial. --- doc/py_tutorials/py_photo/py_hdr/py_hdr.markdown | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/py_tutorials/py_photo/py_hdr/py_hdr.markdown b/doc/py_tutorials/py_photo/py_hdr/py_hdr.markdown index 68788bde65..b06118b897 100644 --- a/doc/py_tutorials/py_photo/py_hdr/py_hdr.markdown +++ b/doc/py_tutorials/py_photo/py_hdr/py_hdr.markdown @@ -83,10 +83,15 @@ We map the 32-bit float HDR data into the range [0..1]. Actually, in some cases the values can be larger than 1 or lower the 0, so notice we will later have to clip the data in order to avoid overflow. +@note: The function `cv.createTonemap()` uses a default gamma value of 1.0. +Set it explicitly to 2.2 to match standard display brightness and ensure consistent tone mapping results. + + @code{.py} -# Tonemap HDR image +# Tonemap HDR images using gamma correction (set gamma=2.2 for standard display brightness) tonemap1 = cv.createTonemap(gamma=2.2) res_debevec = tonemap1.process(hdr_debevec.copy()) +res_robertson = tonemap1.process(hdr_robertson.copy()) @endcode ### 4. Merge exposures using Mertens fusion @@ -125,6 +130,8 @@ You can see the different results but consider that each algorithm have addition extra parameters that you should fit to get your desired outcome. Best practice is to try the different methods and see which one performs best for your scene. +The results below were generated with a gamma value of 2.2 during tonemapping. + ### Debevec: ![image](images/ldr_debevec.jpg)