From c2b89072c4fc3a41e34a930d456b0205a04f1992 Mon Sep 17 00:00:00 2001 From: "Fedorov, Andrey" Date: Tue, 19 May 2026 09:33:41 -0700 Subject: [PATCH 01/15] rewoked warp perspective and aligned the whole file --- hal/ipp/include/ipp_hal_imgproc.hpp | 4 + hal/ipp/src/warp_ipp.cpp | 484 +++++++++++----------------- 2 files changed, 184 insertions(+), 304 deletions(-) diff --git a/hal/ipp/include/ipp_hal_imgproc.hpp b/hal/ipp/include/ipp_hal_imgproc.hpp index f3d5723bc1..4ab985c6e6 100644 --- a/hal/ipp/include/ipp_hal_imgproc.hpp +++ b/hal/ipp/include/ipp_hal_imgproc.hpp @@ -17,11 +17,15 @@ int ipp_hal_warpAffine(int src_type, const uchar *src_data, size_t src_step, int #define cv_hal_warpAffine ipp_hal_warpAffine #endif +#if IPP_VERSION_X100 >= 202600 + 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 // IPP_VERSION_X100 >= 202600 + 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, float* mapx, size_t mapx_step, float* mapy, size_t mapy_step, diff --git a/hal/ipp/src/warp_ipp.cpp b/hal/ipp/src/warp_ipp.cpp index fb059a82ea..7736b884fb 100644 --- a/hal/ipp/src/warp_ipp.cpp +++ b/hal/ipp/src/warp_ipp.cpp @@ -8,153 +8,13 @@ #include #include "precomp_ipp.hpp" +#include // Uncomment to enforce IPP calls for all supported by IPP configurations // #define IPP_CALLS_ENFORCED -#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) - { - ok = _ok; - - inter = _inter; - borderType = _borderType; - iwTransDirection = _iwTransDirection; - - for( int i = 0; i < 2; i++ ) - for( int j = 0; j < 3; j++ ) - coeffs[i][j] = _coeffs[i][j]; - - *ok = true; - } - ~ipp_warpAffineParallel() {} - - virtual void operator() (const cv::Range& range) const CV_OVERRIDE - { - //CV_INSTRUMENT_REGION_IPP(); - if(*ok == false) - return; - - try - { - ::ipp::IwiTile tile = ::ipp::IwiRoi(0, range.start, m_dst.m_size.width, range.end - range.start); - CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpAffine, m_src, m_dst, coeffs, iwTransDirection, inter, ::ipp::IwiWarpAffineParams(), borderType, tile); - } - catch(const ::ipp::IwException &) - { - *ok = false; - return; - } - CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); - } -private: - ::ipp::IwiImage &m_src; - ::ipp::IwiImage &m_dst; - - IppiInterpolationType inter; - double coeffs[2][3]; - ::ipp::IwiBorderType borderType; - IwTransDirection iwTransDirection; - - bool *ok; - const ipp_warpAffineParallel& operator= (const ipp_warpAffineParallel&); -}; - -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_HAL_CHECK_USE_IPP(); - //CV_INSTRUMENT_REGION_IPP(); - - 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 - {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {1, 0, 0}}, //16U - {{1, 0, 0}, {0, 0, 0}, {1, 0, 0}, {1, 0, 0}}, //16S - {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //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) - { - return CV_HAL_ERROR_NOT_IMPLEMENTED; - } - - // Acquire data and begin processing - try - { - ::ipp::IwiImage iwSrc; - iwSrc.Init({src_width, src_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), NULL, src_data, IwSize(src_step)); - ::ipp::IwiImage iwDst({dst_width, dst_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), NULL, dst_data, dst_step); - ::ipp::IwiBorderType ippBorder(ippiGetBorderType(borderType), {borderValue[0], borderValue[1], borderValue[2], borderValue[3]}); - IwTransDirection iwTransDirection = iwTransInverse; - - if((int)ippBorder == -1) - return CV_HAL_ERROR_NOT_IMPLEMENTED; - - double coeffs[2][3]; - for( int i = 0; i < 2; i++ ) - for( int j = 0; j < 3; j++ ) - coeffs[i][j] = M[i*3 + j]; - - 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 (threads > 1) - { - 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); - - if(!ok) - return CV_HAL_ERROR_NOT_IMPLEMENTED; - } - else - { - CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpAffine, iwSrc, iwDst, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpAffineParams(), ippBorder); - } - } - catch (const ::ipp::IwException &) - { - return CV_HAL_ERROR_NOT_IMPLEMENTED; - } - - return CV_HAL_ERROR_OK; -} - -#endif // HAVE_IPP_IW - -// End of Warp affine section - typedef IppStatus (CV_STDCALL* ippiSetFunc)(const void*, void *, int, IppiSize); template @@ -214,168 +74,143 @@ static bool IPPSet(const double value[4], void *dataPointer, int step, IppiSize return false; } -// Warp perspective section +#ifdef HAVE_IPP_IW -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*); +#include "iw++/iw.hpp" -class IPPWarpPerspectiveInvoker : - public cv::ParallelLoopBody +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]) { - // Mem object ot simplify IPP memory lifetime control - struct IPPWarpPerspectiveMem + //CV_INSTRUMENT_REGION_IPP(); + + 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 + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {1, 0, 0}}, //16U + {{1, 0, 0}, {0, 0, 0}, {1, 0, 0}, {1, 0, 0}}, //16S + {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, //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) { - 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, 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), initFunc(_initFunc), ok(_ok) - { - memcpy(this->borderValue, _borderValue, sizeof(this->borderValue)); - *ok = true; + return CV_HAL_ERROR_NOT_IMPLEMENTED; } - virtual void operator() (const cv::Range& range) const CV_OVERRIDE + // Acquire data and begin processing + double coeffs[2][3]; + for( int i = 0; i < 2; i++ ) + for( int j = 0; j < 3; j++ ) + coeffs[i][j] = M[i*3 + j]; + + try { - //CV_INSTRUMENT_REGION_IPP(); - if (*ok == false) - return; + std::atomic_bool ok{true}; + cv::Range cv_range(0, dst_height); + ::ipp::IwiImage iwSrc; + iwSrc.Init(IwiSize{src_width, src_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), IwiBorderSize(), src_data, IwSize(src_step)); + ::ipp::IwiImage iwDst(IwiSize{dst_width, dst_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), IwiBorderSize(), dst_data, IwSize(dst_step)); + ::ipp::IwiBorderType ippBorder(ippiGetBorderType(borderType), {borderValue, 4}); + // OpenCV inverts the affine matrix before calling the HAL (lines 2401-2411 of imgwarp.cpp), so the HAL receives the inverse transform. + IwTransDirection iwTransDirection = iwTransInverse; - IPPWarpPerspectiveMem mem; - - int specSize = 0, initSize = 0, bufSize = 0; - - IppiWarpDirection direction = ippWarpBackward; //fixed for IPP - const Ipp32u numChannels = CV_MAT_CN(src_type); - - IppiSize srcsize = {src.cols, src.rows}; - IppiSize dstsize = {dst.cols, dst.rows}; - IppiRect srcroi = {0, 0, src.cols, src.rows}; - - /* Spec and init buffer sizes */ - CV_IPP_SAFE_CALL(ippiWarpPerspectiveGetSize, ok, srcsize, srcroi, dstsize, ippiGetDataType(src_type), coeffs, inter, ippWarpBackward, ippiGetBorderType(borderType), &specSize, &initSize); - - mem.AllocateSpec(specSize); - - 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())) + if ((int)ippBorder == -1) { - *ok = false; - return; + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + // The lambda function is used to invoke IPP warping function in parallel for different image stripes. + // The function is exception safe and sets the 'ok' flag to false if any exception occurs during processing. + // The 'ok' flag is checked before and after parallel processing to determine if the operation was successful or + // if it should fall back to a non-IPP implementation. + auto IPPWarpAffineInvokerLambda = [&iwSrc, &iwDst, dst_width, ippInter, &coeffs, ippBorder, iwTransDirection, &ok](const cv::Range& range) + { + //CV_INSTRUMENT_REGION_IPP(); + if (!ok.load(std::memory_order_relaxed)) + { + return; + } + + try + { + ::ipp::IwiTile tile = ::ipp::IwiRoi(0, range.start, dst_width, range.end - range.start); + CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpAffine, iwSrc, iwDst, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpAffineParams(), ippBorder, tile); + } + catch (const ::ipp::IwException &) + { + ok.store(false, std::memory_order_relaxed); + return; + } + CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); + }; + + int min_payload = 1 << 16; // 64KB shall be minimal per thread to maximize scalability for warping functions + const int num_threads = ippiSuggestRowThreadsNum(iwDst, min_payload); + + if (num_threads > 1) + { + parallel_for_(cv_range, IPPWarpAffineInvokerLambda, num_threads); + } + else + { + CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpAffine, iwSrc, iwDst, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpAffineParams(), ippBorder); } - if (ippStsNoErr != CV_INSTRUMENT_FUN_IPP(func, src.ptr(), (int)src_step, pDst, (int)dst_step, dstRoiOffset, dstRoiSize, mem.pSpec, mem.pBuffer)) + if (!ok) { - *ok = false; - return; + return CV_HAL_ERROR_NOT_IMPLEMENTED; } - - CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); } -private: - int src_type; - cv::Mat &src; - size_t src_step; - cv::Mat &dst; - size_t dst_step; - IppiInterpolationType inter; - double (&coeffs)[3][3]; - int borderType; - double borderValue[4]; - ippiWarpPerspectiveFunc func; - ippiWarpPerspectiveInitFunc initFunc; - bool *ok; - const IPPWarpPerspectiveInvoker& operator= (const IPPWarpPerspectiveInvoker&); -}; + catch (const ::ipp::IwException &) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + + return CV_HAL_ERROR_OK; +} + +#if IPP_VERSION_X100 >= 202600 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]) { - CV_HAL_CHECK_USE_IPP(); //CV_INSTRUMENT_REGION_IPP(); + IppiInterpolationType ippInter = ippiGetInterpolation(interpolation); + if (src_height <= 1 || src_width <= 1) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } - ippiWarpPerspectiveFunc ippFunc = nullptr; - ippiWarpPerspectiveInitFunc ippInitFunc = nullptr; + int mode = + interpolation == cv::InterpolationFlags::INTER_NEAREST ? IPPI_INTER_NN : + interpolation == cv::InterpolationFlags::INTER_LINEAR ? IPPI_INTER_LINEAR : 0; - if (interpolation == cv::InterpolationFlags::INTER_NEAREST) - { - 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) - { - 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 + if (mode == 0) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } - if (ippFunc == nullptr) + // Unsupported source type + if (src_type != CV_8UC1 && src_type != CV_8UC3 && src_type != CV_8UC4 && + src_type != CV_16UC1 && src_type != CV_16UC3 && src_type != CV_16UC4 && + src_type != CV_16SC1 && src_type != CV_16SC3 && src_type != CV_16SC4 && + src_type != CV_32FC1 && src_type != CV_32FC3 && src_type != CV_32FC4) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } @@ -396,46 +231,90 @@ int ipp_hal_warpPerspective(int src_type, const uchar *src_data, size_t src_step {{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 - {{0, 0}, {0, 0}, {0, 0}, {1, 0}}, //32F + {{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) + if (impl[CV_TYPE(src_type)][CV_MAT_CN(src_type)-1][interpolation] == 0) { return CV_HAL_ERROR_NOT_IMPLEMENTED; } + + // Acquire data and begin processing 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 = 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); - - 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) + try { - CV_IMPL_ADD(CV_IMPL_IPP | CV_IMPL_MT); - return CV_HAL_ERROR_OK; + std::atomic_bool ok{true}; + cv::Range cv_range(0, dst_height); + ::ipp::IwiImage iwSrc; // src_data is const pointer. So, we need to call an init function + iwSrc.Init(IwiSize{src_width, src_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), IwiBorderSize(), src_data, IwSize(src_step)); + ::ipp::IwiImage iwDst(IwiSize{dst_width, dst_height}, ippiGetDataType(src_type), CV_MAT_CN(src_type), IwiBorderSize(), dst_data, IwSize(dst_step)); + ::ipp::IwiBorderType ippBorder(ippiGetBorderType(borderType), {borderValue, 4}); + IwTransDirection iwTransDirection = iwTransInverse; //fixed for IPP + if ((int)ippBorder == -1) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + + // The lambda function is used to invoke IPP warping function in parallel for different image stripes. + // The function is exception safe and sets the 'ok' flag to false if any exception occurs during processing. + // The 'ok' flag is checked before and after parallel processing to determine + // if the operation was successful or if it should fall back to a non-IPP implementation. + auto IPPWarpPerspectiveInvokerLambda = [&iwSrc, &iwDst, dst_width, ippInter, &coeffs, ippBorder, iwTransDirection, &ok](const cv::Range& range) + { + //CV_INSTRUMENT_REGION_IPP(); + if (!ok.load(std::memory_order_relaxed)) + { + return; + } + + try + { + ::ipp::IwiTile tile = ::ipp::IwiRoi(0, range.start, dst_width, range.end - range.start); + CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpPerspective, iwSrc, iwDst, ippRectInfinite, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpPerspectiveParams(), ippBorder, tile); + } + catch (const ::ipp::IwException &) + { + ok.store(false, std::memory_order_relaxed); + return; + } + CV_IMPL_ADD(CV_IMPL_IPP|CV_IMPL_MT); + }; + + int min_payload = 1 << 16; // 64KB shall be minimal per thread to maximize scalability for warping functions + const char type_size[CV_DEPTH_MAX] = {1,1,2,2,4,4,8}; + const int num_threads = ippiSuggestRowThreadsNum(dst_width, dst_height, type_size[CV_TYPE(src_type)]*CV_MAT_CN(src_type), min_payload); + + if (num_threads > 1) + { + parallel_for_(cv_range, IPPWarpPerspectiveInvokerLambda, num_threads); + } + else + { + CV_INSTRUMENT_FUN_IPP(::ipp::iwiWarpPerspective, iwSrc, iwDst, ippRectInfinite, coeffs, iwTransDirection, ippInter, ::ipp::IwiWarpPerspectiveParams(), ippBorder); + } + + if (!ok) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } + } + catch (const ::ipp::IwException &) + { + return CV_HAL_ERROR_NOT_IMPLEMENTED; } - return CV_HAL_ERROR_NOT_IMPLEMENTED; + return CV_HAL_ERROR_OK; } +#endif // IPP_VERSION_X100 >= 202600 -// End of Warp perspective section +#endif // HAVE_IPP_IW // 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, void *pDst, int dstStep, IppiSize dstRoiSize, int interpolation); @@ -445,20 +324,18 @@ class IPPRemapInvoker : public cv::ParallelLoopBody public: IPPRemapInvoker(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, float *_mapx, size_t _mapx_step, float *_mapy, - size_t _mapy_step, ippiRemap _ippFunc, int _ippInterpolation, int _borderType, const double _borderValue[4], bool *_ok) : - ParallelLoopBody(), + size_t _mapy_step, ippiRemap _ippFunc, int _ippInterpolation, int _borderType, const double _borderValue[4], std::atomic_bool *_ok) : src_type(_src_type), src(_src_data), src_step(_src_step), src_width(_src_width), src_height(_src_height), dst(_dst_data), dst_step(_dst_step), dst_width(_dst_width), mapx(_mapx), mapx_step(_mapx_step), mapy(_mapy), mapy_step(_mapy_step), ippFunc(_ippFunc), ippInterpolation(_ippInterpolation), borderType(_borderType), ok(_ok) { memcpy(this->borderValue, _borderValue, sizeof(this->borderValue)); - *ok = true; } virtual void operator()(const cv::Range &range) const { //CV_INSTRUMENT_REGION_IPP(); - if (*ok == false) + if(!ok->load(std::memory_order_relaxed)) return; IppiRect srcRoiRect = {0, 0, src_width, src_height}; @@ -469,7 +346,7 @@ public: if (borderType == cv::BORDER_CONSTANT && !IPPSet(borderValue, dst_roi_data, (int)dst_step, dstRoiSize, cn, depth)) { - *ok = false; + ok->store(false, std::memory_order_relaxed); return; } @@ -477,7 +354,7 @@ public: mapx, (int)mapx_step, mapy, (int)mapy_step, dst_roi_data, (int)dst_step, dstRoiSize, ippInterpolation)) { - *ok = false; + ok->store(false, std::memory_order_relaxed); return; } @@ -499,7 +376,7 @@ private: ippiRemap ippFunc; int ippInterpolation, borderType; double borderValue[4]; - bool *ok; + std::atomic_bool *ok; }; int ipp_hal_remap32f(int src_type, const uchar *src_data, size_t src_step, int src_width, int src_height, @@ -507,7 +384,6 @@ 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]) { - CV_HAL_CHECK_USE_IPP(); if (!((interpolation == cv::INTER_LINEAR || interpolation == cv::INTER_CUBIC || interpolation == cv::INTER_NEAREST) && (border_type == cv::BORDER_CONSTANT || border_type == cv::BORDER_TRANSPARENT))) { @@ -555,7 +431,7 @@ int ipp_hal_remap32f(int src_type, const uchar *src_data, size_t src_step, int s if (ippFunc) { - bool ok = true; + std::atomic_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); From 0abd5c86f78e3107e526ac1273db9088358bad55 Mon Sep 17 00:00:00 2001 From: s-trinh Date: Wed, 20 May 2026 14:05:51 +0200 Subject: [PATCH 02/15] Merge pull request #28824 from s-trinh:update_ArUco_doc Update ArUco doc #28824 See https://github.com/opencv/opencv/pull/28823 Update of the ArUco doc but targeted for OpenCV 4. ### 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. - [x] The feature is well documented and sample code can be built with the project CMake --- apps/interactive-calibration/main.cpp | 2 +- .../parametersController.cpp | 1 + .../DICT_ARUCO_MIP_36h12.json.gz | Bin 0 -> 3090 bytes apps/pattern-tools/test_charuco_board.py | 4 +- .../aruco_detection/aruco_detection.markdown | 2 +- doc/tutorials/tutorials.markdown | 2 +- ...rilTag_corners_comparison_opencv_april.png | Bin 0 -> 16612 bytes .../objdetect/doc/pics/AprilTag_family.png | Bin 0 -> 51952 bytes modules/objdetect/doc/pics/ArUco_family.png | Bin 0 -> 65978 bytes .../objdetect/include/opencv2/objdetect.hpp | 59 ++++++++++++++++++ samples/cpp/aruco_dict_utils.cpp | 8 ++- samples/cpp/calibration.cpp | 3 +- samples/cpp/stereo_calib.cpp | 3 +- .../camera_calibration/camera_calibration.cpp | 1 + .../objectDetection/calibrate_camera.cpp | 3 +- .../calibrate_camera_charuco.cpp | 3 +- .../objectDetection/create_board.cpp | 3 +- .../objectDetection/create_board_charuco.cpp | 3 +- .../objectDetection/create_diamond.cpp | 3 +- .../objectDetection/create_marker.cpp | 5 +- .../objectDetection/detect_board.cpp | 3 +- .../objectDetection/detect_board_charuco.cpp | 3 +- .../objectDetection/detect_diamonds.cpp | 3 +- .../objectDetection/detect_markers.cpp | 2 +- samples/python/aruco_detect_board_charuco.py | 3 +- samples/python/calibrate.py | 3 +- 26 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 apps/pattern-tools/DICT_ARUCO_MIP_36h12.json.gz create mode 100644 modules/objdetect/doc/pics/AprilTag_corners_comparison_opencv_april.png create mode 100644 modules/objdetect/doc/pics/AprilTag_family.png create mode 100644 modules/objdetect/doc/pics/ArUco_family.png diff --git a/apps/interactive-calibration/main.cpp b/apps/interactive-calibration/main.cpp index 889a536a69..ab0fa15ffd 100644 --- a/apps/interactive-calibration/main.cpp +++ b/apps/interactive-calibration/main.cpp @@ -52,7 +52,7 @@ const char* keys = "DICT_4X4_50, DICT_4X4_100, DICT_4X4_250, DICT_4X4_1000, DICT_5X5_50, DICT_5X5_100, DICT_5X5_250, " "DICT_5X5_1000, DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50, DICT_7X7_100, " "DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL, DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, " - "DICT_APRILTAG_36h10, DICT_APRILTAG_36h11 }" + "DICT_APRILTAG_36h10, DICT_APRILTAG_36h11, DICT_ARUCO_MIP_36h12 }" "{fad | None | name of file with ArUco dictionary}" "{of | cameraParameters.xml | Output file name}" "{ft | true | Auto tuning of calibration flags}" diff --git a/apps/interactive-calibration/parametersController.cpp b/apps/interactive-calibration/parametersController.cpp index 48639a2bdb..66fd769fbb 100644 --- a/apps/interactive-calibration/parametersController.cpp +++ b/apps/interactive-calibration/parametersController.cpp @@ -175,6 +175,7 @@ bool calib::parametersController::loadFromParser(cv::CommandLineParser &parser) else if (arucoDictName == "DICT_APRILTAG_25h9") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_25h9; } else if (arucoDictName == "DICT_APRILTAG_36h10") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_36h10; } else if (arucoDictName == "DICT_APRILTAG_36h11") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_36h11; } + else if (arucoDictName == "DICT_ARUCO_MIP_36h12") { mCapParams.charucoDictName = cv::aruco::DICT_ARUCO_MIP_36h12; } else { std::cout << "incorrect name of aruco dictionary \n"; return false; diff --git a/apps/pattern-tools/DICT_ARUCO_MIP_36h12.json.gz b/apps/pattern-tools/DICT_ARUCO_MIP_36h12.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..60734621e6b05d3022f512cef76d9dcd3a526d40 GIT binary patch literal 3090 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 literal 0 HcmV?d00001 diff --git a/apps/pattern-tools/test_charuco_board.py b/apps/pattern-tools/test_charuco_board.py index e92a38a216..6e52a1e785 100644 --- a/apps/pattern-tools/test_charuco_board.py +++ b/apps/pattern-tools/test_charuco_board.py @@ -20,10 +20,10 @@ class aruco_objdetect_test(NewOpenCVTests): square_size = 100 aruco_type = [cv.aruco.DICT_4X4_1000, cv.aruco.DICT_5X5_1000, cv.aruco.DICT_6X6_1000, cv.aruco.DICT_7X7_1000, cv.aruco.DICT_ARUCO_ORIGINAL, cv.aruco.DICT_APRILTAG_16h5, - cv.aruco.DICT_APRILTAG_25h9, cv.aruco.DICT_APRILTAG_36h10, cv.aruco.DICT_APRILTAG_36h11] + cv.aruco.DICT_APRILTAG_25h9, cv.aruco.DICT_APRILTAG_36h10, cv.aruco.DICT_APRILTAG_36h11, cv.aruco.DICT_ARUCO_MIP_36h12] aruco_type_str = ['DICT_4X4_1000','DICT_5X5_1000', 'DICT_6X6_1000', 'DICT_7X7_1000', 'DICT_ARUCO_ORIGINAL', 'DICT_APRILTAG_16h5', - 'DICT_APRILTAG_25h9', 'DICT_APRILTAG_36h10', 'DICT_APRILTAG_36h11'] + 'DICT_APRILTAG_25h9', 'DICT_APRILTAG_36h10', 'DICT_APRILTAG_36h11', 'DICT_ARUCO_MIP_36h12'] marker_size = 0.8*square_size board_width = cols*square_size board_height = rows*square_size diff --git a/doc/tutorials/objdetect/aruco_detection/aruco_detection.markdown b/doc/tutorials/objdetect/aruco_detection/aruco_detection.markdown index edd43866e7..dd4ae483df 100644 --- a/doc/tutorials/objdetect/aruco_detection/aruco_detection.markdown +++ b/doc/tutorials/objdetect/aruco_detection/aruco_detection.markdown @@ -144,7 +144,7 @@ Error correction techniques are employed when necessary. Consider the following image: -![Image with an assortment of markers](images/singlemarkerssource.jpg) +![Image with an assortment of markers](images/singlemarkerssource.jpg) { width=70% } And a printout of this image in a photo: diff --git a/doc/tutorials/tutorials.markdown b/doc/tutorials/tutorials.markdown index cd83c241af..d6e7cf2168 100644 --- a/doc/tutorials/tutorials.markdown +++ b/doc/tutorials/tutorials.markdown @@ -6,7 +6,7 @@ OpenCV Tutorials {#tutorial_root} - @subpage tutorial_table_of_content_imgproc - image processing functions - @subpage tutorial_table_of_content_app - application utils (GUI, image/video input/output) - @subpage tutorial_table_of_content_calib3d - extract 3D world information from 2D images -- @subpage tutorial_table_of_content_objdetect - INSERT OBJDETECT MODULE INFO +- @subpage tutorial_table_of_content_objdetect - detect ArUco markers and other calibration boards - @subpage tutorial_table_of_content_features2d - feature detectors, descriptors and matching framework - @subpage tutorial_table_of_content_dnn - infer neural networks using built-in _dnn_ module - @subpage tutorial_table_of_content_gapi - graph-based approach to computer vision algorithms building diff --git a/modules/objdetect/doc/pics/AprilTag_corners_comparison_opencv_april.png b/modules/objdetect/doc/pics/AprilTag_corners_comparison_opencv_april.png new file mode 100644 index 0000000000000000000000000000000000000000..3e794fe14859d2a1a36d70edc4544a48bb1007ae GIT binary patch literal 16612 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 H)%*SlN4_Z# literal 0 HcmV?d00001 diff --git a/modules/objdetect/doc/pics/AprilTag_family.png b/modules/objdetect/doc/pics/AprilTag_family.png new file mode 100644 index 0000000000000000000000000000000000000000..cb9127a85af9b0e97fac48799590622dc2750fcb GIT binary patch literal 51952 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 + + @warning In OpenCV, the order of the returned corners locations for the AprilTag family is not aligned with the ArUco one.\n + Note that this order is also different from the convention adopted by the official [AprilTag library](https://github.com/AprilRobotics/apriltag/). + ![](pics/AprilTag_corners_comparison_opencv_april.png) { width=80% } + +
+ + An overview of the supported ArUco markers family is visible in the following image: + ![](pics/ArUco_family.png) { width=80% } + +
+ + An overview of the supported AprilTag markers family is visible in the following image: + ![](pics/AprilTag_family.png) { width=80% } + + @note The generated images (in the above picture) using @ref aruco::generateImageMarker for the AprilTag markers have been + rotated by 180 degree in order to match the official AprilTag images. + When using the @ref aruco::generateImageMarker function, it will output by default a different image from the official AprilTag convention, + see the [AprilRobotics/apriltag-imgs](https://github.com/AprilRobotics/apriltag-imgs) repository. + This is the reason why you see a different corners order between ArUco and AprilTag in the above image. + +
+ + For the ArUco marker family, the recommended family is the DICT_ARUCO_MIP_36h12 one, [see](https://stackoverflow.com/a/51511558). + In general, a smaller marker family (e.g. `4x4` vs `6x6`) should give you a better detection rate with respect to the camera distance, + at the expense of having more probability to have issues with false detection or marker id decoding error. + The number of marker ids in a family is also something to take into account with respect to the application use case and the ability + to correct wrong bits during the marker id decoding process. + + You can download some pregenerated MIP_36h12 ArUco marker images from: + - https://sourceforge.net/projects/aruco/files/ + - or use the `samples/cpp/tutorial_code/objectDetection/create_marker.cpp` sample to generate the marker image for your + desired marker family (which uses the @ref aruco::generateImageMarker function) + + For the AprilTag family, you can find some pregenerated marker images in the + [AprilRobotics/apriltag-imgs](https://github.com/AprilRobotics/apriltag-imgs) repository. + + @note For accurate corners location extraction, a white border (to have a strong gradient between white and black transition) around the marker is important. + This is necessary to precisely extract the marker contour in difficult conditions such as bad illumination, confusing color background, etc. + +
+ + There are multiple parameters which can be tweaked to improve the marker detection rate or to be adapted to your use case (e.g. image resolution). + Please refer to the: + - @ref aruco::DetectorParameters + - "Detector Parameters" section in the @ref tutorial_aruco_detection tutorial or in the @ref tutorial_aruco_faq page + - [ArUco Library Documentation](https://drive.google.com/file/d/1OiavRVYVJ-WH88sQg1LUsh8CuJZUQyrX) for additional information from the ArUco library + + The corner refinement method can be changed according to the @ref aruco::CornerRefineMethod to improve the corners location accuracy + at the expense of more computation time. + +
+ + To estimate the marker pose with respect to the camera frame, we recommend you to look at the following sources of information: + - @ref tutorial_aruco_detection for a tutorial about ArUco markers detection + - @ref calib3d for some theoretical background about the pinhole camera model and the @ref calib3d_solvePnP page + - @ref solvePnP, @ref solvePnPGeneric, @ref solveP3P for the relevant pose estimation methods @} @} diff --git a/samples/cpp/aruco_dict_utils.cpp b/samples/cpp/aruco_dict_utils.cpp index 20fecd82e9..e9a40e8d50 100644 --- a/samples/cpp/aruco_dict_utils.cpp +++ b/samples/cpp/aruco_dict_utils.cpp @@ -260,7 +260,7 @@ const char* keys = "DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50," "DICT_7X7_100, DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL," "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10," - "DICT_APRILTAG_36h11}" + "DICT_APRILTAG_36h11, DICT_ARUCO_MIP_36h12}" "{nMarkers | | Number of markers in the dictionary }" "{markerSize | | Marker size }" "{cd | | Input file with custom dictionary }"; @@ -309,6 +309,7 @@ int main(int argc, char *argv[]) else if (arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } else if (arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } else if (arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } + else if (arucoDictName == "DICT_ARUCO_MIP_36h12") { arucoDict = cv::aruco::DICT_ARUCO_MIP_36h12; } else { cout << "incorrect name of aruco dictionary \n"; return 1; @@ -328,13 +329,14 @@ int main(int argc, char *argv[]) cerr << "Dictionary not specified" << endl; return 0; } + if (!outputFile.empty() && nMarkers > 0 && markerSize > 0) { FileStorage fs(outputFile, FileStorage::WRITE); if (checkFlippedMarkers) - dictionary = generateCustomAsymmetricDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); + dictionary = generateCustomAsymmetricDictionary(nMarkers, markerSize, dictionary, 0); else - dictionary = aruco::extendDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); + dictionary = aruco::extendDictionary(nMarkers, markerSize, dictionary, 0); dictionary.writeDictionary(fs); } diff --git a/samples/cpp/calibration.cpp b/samples/cpp/calibration.cpp index 99cb1a547b..3294a93d76 100644 --- a/samples/cpp/calibration.cpp +++ b/samples/cpp/calibration.cpp @@ -66,7 +66,7 @@ static void help(char** argv) "DICT_4X4_1000, DICT_5X5_50, DICT_5X5_100, DICT_5X5_250, DICT_5X5_1000, " "DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50, " "DICT_7X7_100, DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL, " - "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10, DICT_APRILTAG_36h11\n" + "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10, DICT_APRILTAG_36h11, DICT_ARUCO_MIP_36h12\n" " [-adf=] # Custom aruco dictionary file for ChArUco board\n" " [-o=] # the output filename for intrinsic [and extrinsic] parameters\n" " [-op] # write detected feature points\n" @@ -458,6 +458,7 @@ int main( int argc, char** argv ) else if (arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } else if (arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } else if (arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } + else if (arucoDictName == "DICT_ARUCO_MIP_36h12") { arucoDict = cv::aruco::DICT_ARUCO_MIP_36h12; } else { cout << "Incorrect Aruco dictionary name " << arucoDictName << std::endl; return 1; diff --git a/samples/cpp/stereo_calib.cpp b/samples/cpp/stereo_calib.cpp index c583c8fa09..e6ce6adb25 100644 --- a/samples/cpp/stereo_calib.cpp +++ b/samples/cpp/stereo_calib.cpp @@ -58,7 +58,7 @@ static int print_help(char** argv) << "DICT_4X4_1000, DICT_5X5_50, DICT_5X5_100, DICT_5X5_250, DICT_5X5_1000, " << "DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50, " << "DICT_7X7_100, DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL, " - << "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10, DICT_APRILTAG_36h11\n"; + << "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10, DICT_APRILTAG_36h11, DICT_ARUCO_MIP_36h12\n"; return 0; } @@ -435,6 +435,7 @@ int main(int argc, char** argv) else if (arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } else if (arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } else if (arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } + else if (arucoDictName == "DICT_ARUCO_MIP_36h12") { arucoDict = cv::aruco::DICT_ARUCO_MIP_36h12; } else { cout << "incorrect name of aruco dictionary \n"; return 1; diff --git a/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp b/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp index 98f1a10a0c..2c417caae8 100644 --- a/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp +++ b/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp @@ -339,6 +339,7 @@ int main(int argc, char* argv[]) else if (s.arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } else if (s.arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } else if (s.arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } + else if (s.arucoDictName == "DICT_ARUCO_MIP_36h12") { arucoDict = cv::aruco::DICT_ARUCO_MIP_36h12; } else { cout << "incorrect name of aruco dictionary \n"; return 1; diff --git a/samples/cpp/tutorial_code/objectDetection/calibrate_camera.cpp b/samples/cpp/tutorial_code/objectDetection/calibrate_camera.cpp index b415477019..5662a84124 100644 --- a/samples/cpp/tutorial_code/objectDetection/calibrate_camera.cpp +++ b/samples/cpp/tutorial_code/objectDetection/calibrate_camera.cpp @@ -25,7 +25,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{@outfile |cam.yml| Output file with calibrated camera parameters }" "{v | | Input from video file, if ommited, input comes from camera }" diff --git a/samples/cpp/tutorial_code/objectDetection/calibrate_camera_charuco.cpp b/samples/cpp/tutorial_code/objectDetection/calibrate_camera_charuco.cpp index 5bea807db9..dcb2c8aa21 100644 --- a/samples/cpp/tutorial_code/objectDetection/calibrate_camera_charuco.cpp +++ b/samples/cpp/tutorial_code/objectDetection/calibrate_camera_charuco.cpp @@ -23,7 +23,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{@outfile |cam.yml| Output file with calibrated camera parameters }" "{v | | Input from video file, if ommited, input comes from camera }" diff --git a/samples/cpp/tutorial_code/objectDetection/create_board.cpp b/samples/cpp/tutorial_code/objectDetection/create_board.cpp index b1864ffc19..4af6f18da4 100644 --- a/samples/cpp/tutorial_code/objectDetection/create_board.cpp +++ b/samples/cpp/tutorial_code/objectDetection/create_board.cpp @@ -16,7 +16,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{m | | Margins size (in pixels). Default is marker separation (-s) }" "{bb | 1 | Number of bits in marker borders }" diff --git a/samples/cpp/tutorial_code/objectDetection/create_board_charuco.cpp b/samples/cpp/tutorial_code/objectDetection/create_board_charuco.cpp index b76708817a..e07bd670ec 100644 --- a/samples/cpp/tutorial_code/objectDetection/create_board_charuco.cpp +++ b/samples/cpp/tutorial_code/objectDetection/create_board_charuco.cpp @@ -17,7 +17,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{m | | Margins size (in pixels). Default is (squareLength-markerLength) }" "{bb | 1 | Number of bits in marker borders }" diff --git a/samples/cpp/tutorial_code/objectDetection/create_diamond.cpp b/samples/cpp/tutorial_code/objectDetection/create_diamond.cpp index 0db00e7e02..07effaca57 100644 --- a/samples/cpp/tutorial_code/objectDetection/create_diamond.cpp +++ b/samples/cpp/tutorial_code/objectDetection/create_diamond.cpp @@ -17,7 +17,8 @@ const char* keys = "{d | 10 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{ids |0, 1, 2, 3 | Four ids for the ChArUco marker: id1,id2,id3,id4 }" "{m | 0 | Margins size (in pixels) }" "{bb | 1 | Number of bits in marker borders }" diff --git a/samples/cpp/tutorial_code/objectDetection/create_marker.cpp b/samples/cpp/tutorial_code/objectDetection/create_marker.cpp index 560e51c974..1ed78e4fe2 100644 --- a/samples/cpp/tutorial_code/objectDetection/create_marker.cpp +++ b/samples/cpp/tutorial_code/objectDetection/create_marker.cpp @@ -6,7 +6,7 @@ using namespace cv; namespace { -const char* about = "Create an ArUco marker image"; +const char* about = "Create an ArUco/AprilTag marker image"; //! [aruco_create_markers_keys] const char* keys = @@ -14,7 +14,8 @@ const char* keys = "{d | 0 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{id | 0 | Marker id in the dictionary }" "{ms | 200 | Marker size in pixels }" diff --git a/samples/cpp/tutorial_code/objectDetection/detect_board.cpp b/samples/cpp/tutorial_code/objectDetection/detect_board.cpp index a9c74f2fdc..c7ed7ca7c0 100644 --- a/samples/cpp/tutorial_code/objectDetection/detect_board.cpp +++ b/samples/cpp/tutorial_code/objectDetection/detect_board.cpp @@ -19,7 +19,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{c | | Output file with calibrated camera parameters }" "{v | | Input from video or image file, if omitted, input comes from camera }" diff --git a/samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp b/samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp index c02318d6eb..7c4b5a6f50 100644 --- a/samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp +++ b/samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp @@ -19,7 +19,8 @@ const char* keys = "{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{c | | Output file with calibrated camera parameters }" "{v | | Input from video or image file, if ommited, input comes from camera }" diff --git a/samples/cpp/tutorial_code/objectDetection/detect_diamonds.cpp b/samples/cpp/tutorial_code/objectDetection/detect_diamonds.cpp index f6a6236b2a..37097d87e9 100644 --- a/samples/cpp/tutorial_code/objectDetection/detect_diamonds.cpp +++ b/samples/cpp/tutorial_code/objectDetection/detect_diamonds.cpp @@ -16,7 +16,8 @@ const char* keys = "{d | 10 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2," "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," - "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}" + "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{c | | Output file with calibrated camera parameters }" "{as | | Automatic scale. The provided number is multiplied by the last" diff --git a/samples/cpp/tutorial_code/objectDetection/detect_markers.cpp b/samples/cpp/tutorial_code/objectDetection/detect_markers.cpp index f220b84565..06aa6221c4 100644 --- a/samples/cpp/tutorial_code/objectDetection/detect_markers.cpp +++ b/samples/cpp/tutorial_code/objectDetection/detect_markers.cpp @@ -15,7 +15,7 @@ const char* keys = "DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, " "DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12," "DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16," - "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20}" + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}" "{cd | | Input file with custom dictionary }" "{v | | Input from video or image file, if ommited, input comes from camera }" "{ci | 0 | Camera id if input doesnt come from video (-v) }" diff --git a/samples/python/aruco_detect_board_charuco.py b/samples/python/aruco_detect_board_charuco.py index ffb5ac677d..58ba5a899f 100644 --- a/samples/python/aruco_detect_board_charuco.py +++ b/samples/python/aruco_detect_board_charuco.py @@ -36,7 +36,8 @@ def main(): parser.add_argument("-d", help="dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2, DICT_4X4_1000=3," "DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, DICT_6X6_50=8," "DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12, DICT_7X7_100=13," - "DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}", + "DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL=16," + "DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36h12=21}", default="0", action="store", dest="d", type=int) parser.add_argument("-ci", help="Camera id if input doesnt come from video (-v)", default="0", action="store", dest="ci", type=int) diff --git a/samples/python/calibrate.py b/samples/python/calibrate.py index 33c87b6a52..19c0ab0d66 100755 --- a/samples/python/calibrate.py +++ b/samples/python/calibrate.py @@ -100,7 +100,8 @@ def main(): 'DICT_APRILTAG_16h5': cv.aruco.DICT_APRILTAG_16h5, 'DICT_APRILTAG_25h9': cv.aruco.DICT_APRILTAG_25h9, 'DICT_APRILTAG_36h10': cv.aruco.DICT_APRILTAG_36h10, - 'DICT_APRILTAG_36h11': cv.aruco.DICT_APRILTAG_36h11 + 'DICT_APRILTAG_36h11': cv.aruco.DICT_APRILTAG_36h11, + 'DICT_ARUCO_MIP_36h12': cv.aruco.DICT_ARUCO_MIP_36h12 } if (aruco_dict_name not in set(aruco_dicts.keys())): From 5d0dff1aac293532d5b58bf775f868a78dcc7c01 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Wed, 20 May 2026 12:48:50 -0500 Subject: [PATCH 03/15] dnn: register RandomNormalLike layer explicitly in init.cpp randomnormallike_layer.cpp registered itself via CV_DNN_REGISTER_LAYER_CLASS_STATIC at file scope. Nothing else in the translation unit was referenced from elsewhere, so when OpenCV is built statically (BUILD_SHARED_LIBS=OFF, as in the reporter's MSVC build) the linker drops the object file and the static-init registration never runs. The ONNX importer then sets layerParams.type = "RandomNormalLike" but getLayerInstance has no factory for that type, and parsing fails with "Can't create layer of type RandomNormalLike". Switch to the standard pattern used by every other layer in the module: declare RandomNormalLikeLayer in all_layers.hpp, expose a static create() factory from the .cpp, and register it explicitly from initializeLayerFactory in init.cpp. Because init.cpp is referenced by the DNN module init path, this pulls in the layer .cpp regardless of build mode and the registration always runs. The failure was incorrectly attributed to MSVC in the bug report. The bug is build-mode sensitive (static vs shared), not platform sensitive. Verified locally on linux/gcc-13 by building modules/dnn and opencv_test_dnn against the patched tree, then running opencv_test_dnn --gtest_filter='Test_ONNX_layers.RandomNormalLike_basic/0:Test_ONNX_layers.RandomNormalLike_complex/0' Both tests pass; without the patch the same binary reproduces the "Can't create layer of type RandomNormalLike" error from the report. --- modules/dnn/include/opencv2/dnn/all_layers.hpp | 6 ++++++ modules/dnn/src/init.cpp | 1 + modules/dnn/src/layers/randomnormallike_layer.cpp | 8 +++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/modules/dnn/include/opencv2/dnn/all_layers.hpp b/modules/dnn/include/opencv2/dnn/all_layers.hpp index d9d1833780..24e792cb11 100644 --- a/modules/dnn/include/opencv2/dnn/all_layers.hpp +++ b/modules/dnn/include/opencv2/dnn/all_layers.hpp @@ -1204,6 +1204,12 @@ CV__DNN_INLINE_NS_BEGIN static Ptr create(const LayerParams& params); }; + class CV_EXPORTS RandomNormalLikeLayer : public Layer + { + public: + static Ptr create(const LayerParams& params); + }; + //! @} //! @} CV__DNN_INLINE_NS_END diff --git a/modules/dnn/src/init.cpp b/modules/dnn/src/init.cpp index 61db2c1ba6..eea68d6af1 100644 --- a/modules/dnn/src/init.cpp +++ b/modules/dnn/src/init.cpp @@ -200,6 +200,7 @@ void initializeLayerFactory() CV_DNN_REGISTER_LAYER_CLASS(ScatterND, ScatterNDLayer); CV_DNN_REGISTER_LAYER_CLASS(Tile, TileLayer); CV_DNN_REGISTER_LAYER_CLASS(TopK, TopKLayer); + CV_DNN_REGISTER_LAYER_CLASS(RandomNormalLike, RandomNormalLikeLayer); CV_DNN_REGISTER_LAYER_CLASS(Quantize, QuantizeLayer); CV_DNN_REGISTER_LAYER_CLASS(Dequantize, DequantizeLayer); diff --git a/modules/dnn/src/layers/randomnormallike_layer.cpp b/modules/dnn/src/layers/randomnormallike_layer.cpp index 30713390c9..4bd6ca5728 100644 --- a/modules/dnn/src/layers/randomnormallike_layer.cpp +++ b/modules/dnn/src/layers/randomnormallike_layer.cpp @@ -6,12 +6,11 @@ #include "../precomp.hpp" #include "layers_common.hpp" -#include #include namespace cv { namespace dnn { -class RandomNormalLikeLayerImpl CV_FINAL : public Layer +class RandomNormalLikeLayerImpl CV_FINAL : public RandomNormalLikeLayer { public: RandomNormalLikeLayerImpl(const LayerParams& params) @@ -117,6 +116,9 @@ private: int depth; }; -CV_DNN_REGISTER_LAYER_CLASS_STATIC(RandomNormalLike, RandomNormalLikeLayerImpl); +Ptr RandomNormalLikeLayer::create(const LayerParams& params) +{ + return makePtr(params); +} }} // namespace cv::dnn From f2ecf968b871eb6d32ff13020d291a5a1e78aee3 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov <2536374+asmorkalov@users.noreply.github.com> Date: Wed, 20 May 2026 22:31:13 +0300 Subject: [PATCH 04/15] Merge pull request #28744 from asmorkalov:as/kleidicv_26.03 KleidiCV update to verison 26.03 #28744 KleidiCV release: https://gitlab.arm.com/kleidi/kleidicv/-/releases/26.03 Tuned DNN test threshold as resize linear produces slightly different result with the new KleidiCV version. ### 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 --- hal/kleidicv/kleidicv.cmake | 4 ++-- modules/dnn/misc/python/test/test_dnn.py | 2 +- modules/dnn/test/test_caffe_importer.cpp | 2 +- modules/dnn/test/test_common.impl.hpp | 2 ++ modules/dnn/test/test_darknet_importer.cpp | 14 ++------------ modules/dnn/test/test_int8_layers.cpp | 16 +++++++--------- modules/dnn/test/test_model.cpp | 2 +- modules/dnn/test/test_onnx_importer.cpp | 8 ++++---- modules/dnn/test/test_tf_importer.cpp | 8 ++------ modules/dnn/test/test_tflite_importer.cpp | 4 ++-- 10 files changed, 24 insertions(+), 38 deletions(-) diff --git a/hal/kleidicv/kleidicv.cmake b/hal/kleidicv/kleidicv.cmake index 4ea691f97a..61c398b3e2 100644 --- a/hal/kleidicv/kleidicv.cmake +++ b/hal/kleidicv/kleidicv.cmake @@ -1,8 +1,8 @@ function(download_kleidicv root_var) set(${root_var} "" PARENT_SCOPE) - ocv_update(KLEIDICV_SRC_COMMIT "0.7.0") - ocv_update(KLEIDICV_SRC_HASH "e8f94e427bd78a745afa5c8cd073b416") + ocv_update(KLEIDICV_SRC_COMMIT "26.03") + ocv_update(KLEIDICV_SRC_HASH "b85a745bfe0e87e67e30be9533eb6b24") set(THE_ROOT "${OpenCV_BINARY_DIR}/3rdparty/kleidicv") ocv_download(FILENAME "kleidicv-${KLEIDICV_SRC_COMMIT}.tar.gz" diff --git a/modules/dnn/misc/python/test/test_dnn.py b/modules/dnn/misc/python/test/test_dnn.py index 8d7eed52af..899264a140 100644 --- a/modules/dnn/misc/python/test/test_dnn.py +++ b/modules/dnn/misc/python/test/test_dnn.py @@ -231,7 +231,7 @@ class dnn_test(NewOpenCVTests): iouDiff = 0.05 confThreshold = 0.0001 nmsThreshold = 0 - scoreDiff = 1e-3 + scoreDiff = 1.1e-3 classIds, confidences, boxes = model.detect(frame, confThreshold, nmsThreshold) diff --git a/modules/dnn/test/test_caffe_importer.cpp b/modules/dnn/test/test_caffe_importer.cpp index 4db7796e58..82b0734b52 100644 --- a/modules/dnn/test/test_caffe_importer.cpp +++ b/modules/dnn/test/test_caffe_importer.cpp @@ -265,7 +265,7 @@ TEST(Reproducibility_FCN, Accuracy) int shape[] = {1, 21, 500, 500}; Mat ref(4, shape, CV_32FC1, refData.data); - normAssert(ref, out); + normAssert(ref, out, "", 0.013, 0.17); } TEST(Reproducibility_SSD, Accuracy) diff --git a/modules/dnn/test/test_common.impl.hpp b/modules/dnn/test/test_common.impl.hpp index 104ddec7ee..d8b12b3990 100644 --- a/modules/dnn/test/test_common.impl.hpp +++ b/modules/dnn/test/test_common.impl.hpp @@ -114,6 +114,8 @@ void normAssertDetections( const char *comment /*= ""*/, double confThreshold /*= 0.0*/, double scores_diff /*= 1e-5*/, double boxes_iou_diff /*= 1e-4*/) { + scores_diff = std::max(0.022, scores_diff); + boxes_iou_diff = std::max(0.019, boxes_iou_diff); ASSERT_FALSE(testClassIds.empty()) << "No detections"; std::vector matchedRefBoxes(refBoxes.size(), false); std::vector refBoxesIoUDiff(refBoxes.size(), 1.0); diff --git a/modules/dnn/test/test_darknet_importer.cpp b/modules/dnn/test/test_darknet_importer.cpp index ba2a7f14c6..9d2b77142f 100644 --- a/modules/dnn/test/test_darknet_importer.cpp +++ b/modules/dnn/test/test_darknet_importer.cpp @@ -383,18 +383,8 @@ TEST_P(Test_Darknet_nets, YoloVoc) 1, 6, 0.667770f, 0.446555f, 0.453578f, 0.499986f, 0.519167f, // a car 1, 6, 0.844947f, 0.637058f, 0.460398f, 0.828508f, 0.66427f); // a car - double nmsThreshold = (target == DNN_TARGET_MYRIAD || target == DNN_TARGET_CPU_FP16) ? 0.397 : 0.4; - double scoreDiff = 8e-5, iouDiff = 3e-4; - if (target == DNN_TARGET_OPENCL_FP16 || target == DNN_TARGET_MYRIAD || target == DNN_TARGET_CPU_FP16) - { - scoreDiff = 1e-2; - iouDiff = 0.018; - } - else if (target == DNN_TARGET_CUDA_FP16) - { - scoreDiff = 0.03; - iouDiff = 0.018; - } + double nmsThreshold = 0.397; + double scoreDiff = 0.03, iouDiff = 0.018; #if defined(INF_ENGINE_RELEASE) && INF_ENGINE_VER_MAJOR_EQ(2022010000) // accuracy if (backend == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH && target == DNN_TARGET_OPENCL_FP16) diff --git a/modules/dnn/test/test_int8_layers.cpp b/modules/dnn/test/test_int8_layers.cpp index 22553dc1a8..236966a0b4 100644 --- a/modules/dnn/test/test_int8_layers.cpp +++ b/modules/dnn/test/test_int8_layers.cpp @@ -971,7 +971,7 @@ TEST_P(Test_Int8_nets, MobileNet_v1_SSD) Mat blob = blobFromImage(inp, 1.0, Size(300, 300), Scalar(), true, false); Mat ref = blobFromNPY(_tf("tensorflow/ssd_mobilenet_v1_coco_2017_11_17.detection_out.npy")); - float confThreshold = 0.5, scoreDiff = 0.034, iouDiff = 0.13; + float confThreshold = 0.5, scoreDiff = 0.034, iouDiff = 0.14; testDetectionNet(net, blob, ref, confThreshold, scoreDiff, iouDiff); } @@ -989,7 +989,7 @@ TEST_P(Test_Int8_nets, MobileNet_v1_SSD_PPN) Mat blob = blobFromImage(inp, 1.0, Size(300, 300), Scalar(), true, false); Mat ref = blobFromNPY(_tf("tensorflow/ssd_mobilenet_v1_ppn_coco.detection_out.npy")); - float confThreshold = 0.51, scoreDiff = 0.05, iouDiff = 0.06; + float confThreshold = 0.51, scoreDiff = 0.05, iouDiff = 0.07; testDetectionNet(net, blob, ref, confThreshold, scoreDiff, iouDiff); } @@ -1279,7 +1279,7 @@ TEST_P(Test_Int8_nets, TinyYoloVoc) std::string config_file = "tiny-yolo-voc.cfg"; std::string weights_file = "tiny-yolo-voc.weights"; - double scoreDiff = 0.043, iouDiff = 0.12; + double scoreDiff = 0.043, iouDiff = 0.165; { SCOPED_TRACE("batch size 1"); testDarknetModel(config_file, weights_file, ref.rowRange(0, 2), scoreDiff, iouDiff); @@ -1332,7 +1332,7 @@ TEST_P(Test_Int8_nets, YOLOv3) std::string config_file = "yolov3.cfg"; std::string weights_file = "yolov3.weights"; - double scoreDiff = 0.08, iouDiff = 0.21, confThreshold = 0.25; + double scoreDiff = 0.08, iouDiff = 0.21, confThreshold = 0.28; { SCOPED_TRACE("batch size 1"); testDarknetModel(config_file, weights_file, ref.rowRange(0, N0), scoreDiff, iouDiff, confThreshold); @@ -1358,7 +1358,7 @@ TEST_P(Test_Int8_nets, YOLOv4) applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); const int N0 = 3; - const int N1 = 7; + const int N1 = 5; static const float ref_[/* (N0 + N1) * 7 */] = { 0, 16, 0.992194f, 0.172375f, 0.402458f, 0.403918f, 0.932801f, 0, 1, 0.988326f, 0.166708f, 0.228236f, 0.737208f, 0.735803f, @@ -1369,8 +1369,6 @@ TEST_P(Test_Int8_nets, YOLOv4) 1, 2, 0.98233f, 0.452007f, 0.462217f, 0.495612f, 0.521687f, 1, 9, 0.919195f, 0.374642f, 0.316524f, 0.398126f, 0.393714f, 1, 9, 0.856303f, 0.666842f, 0.372215f, 0.685539f, 0.44141f, -1, 9, 0.313516f, 0.656791f, 0.374734f, 0.671959f, 0.438371f, -1, 9, 0.256625f, 0.940232f, 0.326931f, 0.967586f, 0.374002f, }; Mat ref(N0 + N1, 7, CV_32FC1, (void*)ref_); @@ -1379,13 +1377,13 @@ TEST_P(Test_Int8_nets, YOLOv4) double scoreDiff = 0.15, iouDiff = 0.2; { SCOPED_TRACE("batch size 1"); - testDarknetModel(config_file, weights_file, ref.rowRange(0, N0), scoreDiff, iouDiff); + testDarknetModel(config_file, weights_file, ref.rowRange(0, N0), scoreDiff, iouDiff, 0.5); } { SCOPED_TRACE("batch size 2"); - testDarknetModel(config_file, weights_file, ref, scoreDiff, iouDiff); + testDarknetModel(config_file, weights_file, ref, scoreDiff, iouDiff, 0.5); } } diff --git a/modules/dnn/test/test_model.cpp b/modules/dnn/test/test_model.cpp index 902526930f..a85a2ad5cc 100644 --- a/modules/dnn/test/test_model.cpp +++ b/modules/dnn/test/test_model.cpp @@ -582,7 +582,7 @@ TEST_P(Test_Model, Keypoints_face) bool swapRB = false; // Ref. Range: [-1.1784188, 1.7758257] - float norm = 1e-4; + float norm = 2e-3; if (target == DNN_TARGET_OPENCL_FP16 || target == DNN_TARGET_CPU_FP16) norm = 5e-3; if (target == DNN_TARGET_MYRIAD) diff --git a/modules/dnn/test/test_onnx_importer.cpp b/modules/dnn/test/test_onnx_importer.cpp index cd1ae9d2fd..86363f37b3 100644 --- a/modules/dnn/test/test_onnx_importer.cpp +++ b/modules/dnn/test/test_onnx_importer.cpp @@ -2204,7 +2204,7 @@ TEST_P(Test_ONNX_nets, RAFT) // and output 12006 is calculated from 12007 so checking 12007 is sufficient. std::string ref_12700_path = _tf("data/output_optical_flow_estimation_raft_2023aug.npy"); auto ref0 = blobFromNPY(ref_12700_path); - normAssert(ref0, outs[0], "", 1e-5, 1.8e-4); + normAssert(ref0, outs[0], "", 1.5e-3, 3.2e-2); } TEST_P(Test_ONNX_nets, Squeezenet) @@ -3187,9 +3187,9 @@ TEST_P(Test_ONNX_nets, VitTrack) { auto ref_output2 = blobFromNPY(_tf("data/output_object_tracking_vittrack_2023sep_1.npy")); auto ref_output3 = blobFromNPY(_tf("data/output_object_tracking_vittrack_2023sep_2.npy")); - normAssert(ref_output1, outputs[0], "VitTrack output1"); - normAssert(ref_output2, outputs[1], "VitTrack output2"); - normAssert(ref_output3, outputs[2], "VitTrack output3"); + normAssert(ref_output1, outputs[0], "VitTrack output1", 3e-5, 3e-4); + normAssert(ref_output2, outputs[1], "VitTrack output2", 3e-5, 2e-4); + normAssert(ref_output3, outputs[2], "VitTrack output3", 3e-4, 9e-4); } TEST_P(Test_ONNX_layers, LayerNormNoFusion) { diff --git a/modules/dnn/test/test_tf_importer.cpp b/modules/dnn/test/test_tf_importer.cpp index 964fcbbbe8..6880747e6b 100644 --- a/modules/dnn/test/test_tf_importer.cpp +++ b/modules/dnn/test/test_tf_importer.cpp @@ -70,7 +70,7 @@ TEST(Test_TensorFlow, inception_accuracy) Mat ref = blobFromNPY(_tf("tf_inception_prob.npy")); - normAssert(ref, out); + normAssert(ref, out, "", 5e-5, 0.02); } static std::string path(const std::string& file) @@ -937,16 +937,12 @@ TEST_P(Test_TensorFlow_nets, MobileNet_SSD) net.setInput(inp); Mat out = net.forward(); - double scoreDiff = default_l1, iouDiff = default_lInf; + double scoreDiff = default_l1, iouDiff = 0.04; if (target == DNN_TARGET_OPENCL_FP16 || target == DNN_TARGET_MYRIAD || target == DNN_TARGET_CPU_FP16) { scoreDiff = 0.01; iouDiff = 0.1; } - else if (target == DNN_TARGET_CUDA_FP16) - { - iouDiff = 0.04; - } normAssertDetections(ref, out, "", 0.2, scoreDiff, iouDiff); #if defined(INF_ENGINE_RELEASE) && INF_ENGINE_RELEASE >= 2019010000 diff --git a/modules/dnn/test/test_tflite_importer.cpp b/modules/dnn/test/test_tflite_importer.cpp index f7689405dc..f92236d3a8 100644 --- a/modules/dnn/test/test_tflite_importer.cpp +++ b/modules/dnn/test/test_tflite_importer.cpp @@ -94,7 +94,7 @@ TEST_P(Test_TFLite, face_landmark) { if (backend == DNN_BACKEND_CUDA && target == DNN_TARGET_CUDA_FP16) applyTestTag(CV_TEST_TAG_DNN_SKIP_CUDA_FP16); - double l1 = 2.2e-5, lInf = 2e-4; + double l1 = 0.066, lInf = 0.21; if (target == DNN_TARGET_CPU_FP16 || target == DNN_TARGET_CUDA_FP16 || target == DNN_TARGET_OPENCL_FP16 || target == DNN_TARGET_MYRIAD || (backend == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH && target == DNN_TARGET_OPENCL)) { @@ -120,7 +120,7 @@ TEST_P(Test_TFLite, face_detection_short_range) // https://google.github.io/mediapipe/solutions/selfie_segmentation TEST_P(Test_TFLite, selfie_segmentation) { - double l1 = 0, lInf = 0; + double l1 = 0.002, lInf = 0.24; if (target == DNN_TARGET_CPU_FP16 || target == DNN_TARGET_CUDA_FP16 || target == DNN_TARGET_OPENCL_FP16 || target == DNN_TARGET_MYRIAD || (backend == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH && target == DNN_TARGET_OPENCL)) { From 2d7d9e5482bb7ae35726105561f400b7829d1b76 Mon Sep 17 00:00:00 2001 From: Vincent Rabaud Date: Wed, 20 May 2026 22:58:25 +0200 Subject: [PATCH 05/15] Get CV_DISABLE_UBSAN to compile with older compilers --- modules/core/include/opencv2/core/cvdef.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/core/include/opencv2/core/cvdef.h b/modules/core/include/opencv2/core/cvdef.h index 65973153c3..066ed66a15 100644 --- a/modules/core/include/opencv2/core/cvdef.h +++ b/modules/core/include/opencv2/core/cvdef.h @@ -695,9 +695,15 @@ __CV_ENUM_FLAGS_BITWISE_XOR_EQ (EnumType, EnumType) #endif #if defined(__clang__) || defined(__GNUC__) - #define CV_DISABLE_UBSAN __attribute__((no_sanitize("undefined"))) -#else - #define CV_DISABLE_UBSAN +# if defined(__has_attribute) +# if __has_attribute(no_sanitize) +# define CV_DISABLE_UBSAN __attribute__((no_sanitize("undefined"))) +# endif +# endif +#endif + +#ifndef CV_DISABLE_UBSAN +# define CV_DISABLE_UBSAN #endif /****************************************************************************************\ From 6f9d8127227ac7dade9a6dd9463f0242dd295d80 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Thu, 21 May 2026 11:10:31 +0300 Subject: [PATCH 06/15] Fixed build with old GCC. --- .../imgproc/test/test_connectedcomponents.cpp | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/imgproc/test/test_connectedcomponents.cpp b/modules/imgproc/test/test_connectedcomponents.cpp index 7d58f714e0..42ee000497 100644 --- a/modules/imgproc/test/test_connectedcomponents.cpp +++ b/modules/imgproc/test/test_connectedcomponents.cpp @@ -240,7 +240,7 @@ TEST(Imgproc_ConnectedComponents, missing_background_pixels) TEST(Imgproc_ConnectedComponents, spaghetti_bbdt_sauf_stats) { - cv::Mat1b img({16, 16}, { + cv::Mat1b img({16, 16}, { (unsigned char) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, @@ -361,8 +361,8 @@ TEST(Imgproc_ConnectedComponents, spaghetti_bbdt_sauf_stats) TEST(Imgproc_ConnectedComponents, chessboard_even) { - const auto size = {16, 16}; - cv::Mat1b input(size, { + auto size = {16, 16}; + cv::Mat1b input(size, { (unsigned char) 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, @@ -445,8 +445,8 @@ TEST(Imgproc_ConnectedComponents, chessboard_even) TEST(Imgproc_ConnectedComponents, chessboard_odd) { - const auto size = {15, 15}; - cv::Mat1b input(size, { + auto size = {15, 15}; + cv::Mat1b input(size, { (unsigned char) 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, @@ -526,8 +526,8 @@ TEST(Imgproc_ConnectedComponents, chessboard_odd) TEST(Imgproc_ConnectedComponents, maxlabels_8conn_even) { - const auto size = {16, 16}; - cv::Mat1b input(size, { + auto size = {16, 16}; + cv::Mat1b input(size, { (unsigned char) 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, @@ -607,8 +607,8 @@ TEST(Imgproc_ConnectedComponents, maxlabels_8conn_even) TEST(Imgproc_ConnectedComponents, maxlabels_8conn_odd) { - const auto size = {15, 15}; - cv::Mat1b input(size, { + auto size = {15, 15}; + cv::Mat1b input(size, { (unsigned char) 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, @@ -685,7 +685,7 @@ TEST(Imgproc_ConnectedComponents, maxlabels_8conn_odd) TEST(Imgproc_ConnectedComponents, single_row) { - const auto size = {1, 15}; + auto size = {1, 15}; cv::Mat1b input(size, {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); cv::Mat1i output_8c(size, {1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8}); cv::Mat1i output_4c(size, {1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8}); @@ -715,8 +715,8 @@ TEST(Imgproc_ConnectedComponents, single_row) TEST(Imgproc_ConnectedComponents, single_column) { - const auto size = {15, 1}; - cv::Mat1b input(size, {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); + auto size = {15, 1}; + cv::Mat1b input(size, {(unsigned char)1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); cv::Mat1i output_8c(size, {1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8}); cv::Mat1i output_4c(size, {1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8}); From 01b23a0de5ccc738dc84d5fd2544c3e6f1d6a014 Mon Sep 17 00:00:00 2001 From: kevinylin88 Date: Thu, 21 May 2026 19:29:55 +0800 Subject: [PATCH 07/15] Merge pull request #29080 from kevinylin88:project4_kevinlin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core(rvv): fix v_matmul/v_matmuladd scalable semantics and expand lane-group test coverage #29080 ### 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 ## Platform SpacemiT X60 (K1), 8-core RISC-V RVV 1.0, VLEN=256, 16GB RAM, OS: Bianbu Linux (kernel 6.6.63), GCC 13.2.0, Build: OpenCV 4.14.0-pre, Release, HAL: YES (RVV HAL 0.0.1) ## Motivation OpenCV's Universal Intrinsics `v_matmul` and `v_matmuladd` have a semantic bug in the RVV scalable backend (`modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp`). The current implementation uses `v_extract_n(v, 0/1/2/3)` with hardcoded indices, assuming the vector holds exactly 4 float lanes (128-bit fixed). On hardware with VLEN=256 (e.g. SpacemiT K1 / BPI-F3), `v_float32` with LMUL=2 holds 16 lanes. As a result, lanes 4–15 silently reuse the inputs from lanes 0–3, producing wrong results. OpenCV itself acknowledges this in `modules/core/src/matmul.simd.hpp`: // v_matmuladd for RVV is 128-bit only but not scalable, // this will fail the test Core_Transform.accuracy The RVV scalable `transform_32f` path has been disabled because of this bug. However, the existing `TheTest::test_matmul()` only checked the first 4-lane group (the outer loop was effectively hardcoded to `int i = 0`), so the bug was never caught by CI even on wide-vector backends. ## Modification **Test fix** (`modules/core/test/test_intrin_utils.hpp`): Expanded `test_matmul()` to iterate over all 4-lane groups: // Before (only checked lane group i=0) int i = 0; for (int j = i; j < i + 4; ++j) { ... } // After (checks all lane groups) for (int i = 0; i < VTraits::vlanes(); i += 4) { for (int j = i; j < i + 4; ++j) { ... } } **Kernel fix** (`modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp`): Rewrote `v_matmul` and `v_matmuladd` to process all 4-lane groups correctly. Each group of 4 lanes now independently computes the full matrix multiply using its own `v[i], v[i+1], v[i+2], v[i+3]` inputs. The `transform_32f` RVV path in `matmul.simd.hpp` remains disabled as the autovectorized path shows better performance on current hardware. ## Experiment 1: Bug reproduced on SpacemiT K1 (VLEN=256) ./opencv_test_core --gtest_filter="*intrin*" Result: `hal_intrin128.float32x4_BASELINE` FAILED with 24 failures, all from lane groups i=4, i=8, i=12 (lanes 4–15). Representative failures from `v_matmul` (line 1526): i=4 j=4: actual=158 expected=56 i=4 j=5: actual=166.39999 expected=59.200001 i=8 j=8: actual=314.39999 expected=68.800003 i=12 j=12: actual=512.40002 expected=81.599998 Representative failures from `v_matmuladd` (line 1540): i=4 j=4: actual=147.5 expected=51.5 i=8 j=8: actual=284.70001 expected=60.700001 i=12 j=12: actual=453.89999 expected=69.900002 Lane group i=0 (j=0..3) passed correctly — confirming the bug only affects lanes beyond the first 4, exactly as expected from the hardcoded `v_extract_n(v, 0/1/2/3)` implementation. ## Experiment 2: Both tests pass after fixing the kernel ./opencv_test_core --gtest_filter='hal_intrin128.float32x4_BASELINE' [ OK ] hal_intrin128.float32x4_BASELINE (1859 ms) [ PASSED ] 1 test. ./opencv_test_core --gtest_filter='Core_Transform.accuracy' [ OK ] Core_Transform.accuracy (819 ms) [ PASSED ] 1 test. ## Experiment 3: RVV transform path remains disabled (performance regression) After re-enabling the RVV scalable `transform_32f` path experimentally, benchmarks showed a significant regression vs the compiler-autovectorized scalar path (CV_32FC3): Size RVV path Scalar path Ratio 640x480 7.83 ms 1.48 ms 5.3x slower 1280x720 23.96 ms 5.17 ms 4.6x slower 1920x1080 53.76 ms 10.12 ms 5.3x slower The compiler-autovectorized path outperforms the hand-written RVV kernel for this workload, consistent with the original comment in `matmul.simd.hpp`. The `transform_32f` RVV path is therefore kept disabled in this PR. The kernel fix to `v_matmul`/`v_matmuladd` remains necessary for correctness on wide-vector hardware, and the expanded test ensures the bug cannot regress silently in future. --- .../opencv2/core/hal/intrin_rvv_scalable.hpp | 25 ++++++++++--------- modules/core/src/matmul.simd.hpp | 3 +-- modules/core/test/test_intrin_utils.hpp | 14 +++++------ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp b/modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp index 67c9d12741..ca82d140f8 100644 --- a/modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp +++ b/modules/core/include/opencv2/core/hal/intrin_rvv_scalable.hpp @@ -2145,28 +2145,29 @@ inline v_float64 v_dotprod_expand_fast(const v_int32& a, const v_int32& b, const { return v_add(v_dotprod_expand_fast(a, b) , c); } #endif -// TODO: only 128 bit now. inline v_float32 v_matmul(const v_float32& v, const v_float32& mat0, const v_float32& mat1, const v_float32& mat2, const v_float32& mat3) { - vfloat32m2_t res; - res = __riscv_vfmul_vf_f32m2(mat0, v_extract_n(v, 0), VTraits::vlanes()); - res = __riscv_vfmacc_vf_f32m2(res, v_extract_n(v, 1), mat1, VTraits::vlanes()); - res = __riscv_vfmacc_vf_f32m2(res, v_extract_n(v, 2), mat2, VTraits::vlanes()); - res = __riscv_vfmacc_vf_f32m2(res, v_extract_n(v, 3), mat3, VTraits::vlanes()); - return res; + const int vl = VTraits::vlanes(); + vuint32m2_t idx = __riscv_vand(__riscv_vid_v_u32m2(vl), 0xfffffffc, vl); + v_float32 v0 = __riscv_vrgather(v, idx, vl); + v_float32 v1 = __riscv_vrgather(v, __riscv_vadd(idx, 1, vl), vl); + v_float32 v2 = __riscv_vrgather(v, __riscv_vadd(idx, 2, vl), vl); + v_float32 v3 = __riscv_vrgather(v, __riscv_vadd(idx, 3, vl), vl); + return v_fma(v0, mat0, v_fma(v1, mat1, v_fma(v2, mat2, v_mul(v3, mat3)))); } -// TODO: only 128 bit now. inline v_float32 v_matmuladd(const v_float32& v, const v_float32& mat0, const v_float32& mat1, const v_float32& mat2, const v_float32& a) { - vfloat32m2_t res = __riscv_vfmul_vf_f32m2(mat0, v_extract_n(v,0), VTraits::vlanes()); - res = __riscv_vfmacc_vf_f32m2(res, v_extract_n(v,1), mat1, VTraits::vlanes()); - res = __riscv_vfmacc_vf_f32m2(res, v_extract_n(v,2), mat2, VTraits::vlanes()); - return __riscv_vfadd(res, a, VTraits::vlanes()); + const int vl = VTraits::vlanes(); + vuint32m2_t idx = __riscv_vand(__riscv_vid_v_u32m2(vl), 0xfffffffc, vl); + v_float32 v0 = __riscv_vrgather(v, idx, vl); + v_float32 v1 = __riscv_vrgather(v, __riscv_vadd(idx, 1, vl), vl); + v_float32 v2 = __riscv_vrgather(v, __riscv_vadd(idx, 2, vl), vl); + return v_fma(v0, mat0, v_fma(v1, mat1, v_fma(v2, mat2, a))); } inline void v_cleanup() {} diff --git a/modules/core/src/matmul.simd.hpp b/modules/core/src/matmul.simd.hpp index f089c90a01..5645617dd1 100644 --- a/modules/core/src/matmul.simd.hpp +++ b/modules/core/src/matmul.simd.hpp @@ -1596,8 +1596,7 @@ static void transform_32f( const float* src, float* dst, const float* m, int len, int scn, int dcn ) { // Disabled for RISC-V Vector (scalable), because of: -// 1. v_matmuladd for RVV is 128-bit only but not scalable, this will fail the test `Core_Transform.accuracy`. -// 2. Both gcc and clang can autovectorize this, with better performance than using Universal intrinsic. +// 1. Both gcc and clang can autovectorize this, with better performance than using Universal intrinsic. #if (CV_SIMD || CV_SIMD_SCALABLE) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(_M_ARM64EC) && !(CV_TRY_RVV && CV_RVV) int x = 0; if( scn == 3 && dcn == 3 ) diff --git a/modules/core/test/test_intrin_utils.hpp b/modules/core/test/test_intrin_utils.hpp index 8c2c69c79f..2a0ce37611 100644 --- a/modules/core/test/test_intrin_utils.hpp +++ b/modules/core/test/test_intrin_utils.hpp @@ -1519,9 +1519,8 @@ template struct TheTest R v = dataV, a = dataA, b = dataB, c = dataC, d = dataD; Data res = v_matmul(v, a, b, c, d); - // for (int i = 0; i < VTraits::vlanes(); i += 4) - // { - int i = 0; + for (int i = 0; i < VTraits::vlanes(); i += 4) + { for (int j = i; j < i + 4; ++j) { SCOPED_TRACE(cv::format("i=%d j=%d", i, j)); @@ -1531,12 +1530,11 @@ template struct TheTest + dataV[i + 3] * dataD[j]; EXPECT_COMPARE_EQ(val, res[j]); } - // } + } Data resAdd = v_matmuladd(v, a, b, c, d); - // for (int i = 0; i < VTraits::vlanes(); i += 4) - // { - i = 0; + for (int i = 0; i < VTraits::vlanes(); i += 4) + { for (int j = i; j < i + 4; ++j) { SCOPED_TRACE(cv::format("i=%d j=%d", i, j)); @@ -1546,7 +1544,7 @@ template struct TheTest + dataD[j]; EXPECT_COMPARE_EQ(val, resAdd[j]); } - // } + } return *this; } From 3157f3e3edfd22e96f6273c8bf383ddfea34dcc1 Mon Sep 17 00:00:00 2001 From: Adrian Kretz Date: Sat, 23 May 2026 10:17:16 +0200 Subject: [PATCH 08/15] Expand char to 32 bit of float --- modules/core/src/norm.simd.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/norm.simd.hpp b/modules/core/src/norm.simd.hpp index 4b76764b60..e5a2e657e7 100644 --- a/modules/core/src/norm.simd.hpp +++ b/modules/core/src/norm.simd.hpp @@ -1290,7 +1290,7 @@ struct MaskedNormInf_SIMD { v_float32 acc = vx_setzero_f32(); for (; i <= len - vstep; i += vstep) { - v_uint32 m = v_reinterpret_as_u32(vx_load_expand(mask + i)); + v_uint32 m = vx_load_expand_q(mask + i); v_uint32 cmp = v_gt(m, vx_setzero_u32()); v_float32 s = vx_load(src + i); s = v_abs(s); From 656f3395a705c7a93e216aeca828592091d1b764 Mon Sep 17 00:00:00 2001 From: Yixuan Tang <114117134+plain-noodle-expert@users.noreply.github.com> Date: Sat, 23 May 2026 07:58:03 -0400 Subject: [PATCH 09/15] Merge pull request #29113 from plain-noodle-expert:fix/mjpeg-put-bits-heap-overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit videoio(mjpeg): fix heap-buffer-overflow in put_bits off-by-one resize guard #29113 ### Summary Fix a heap-buffer-overflow (WRITE of size 4) in the built-in MJPEG encoder detected by AddressSanitizer during fuzzing. Fixes #29112 ### Root Cause `mjpeg_buffer::put_bits` in `modules/videoio/src/cap_mjpeg_encoder.cpp` guards buffer resize with: ```cpp if ((m_pos == (data.size() - 1) && len > bits_free) || m_pos == data.size()) resize(int(2 * data.size())); ``` When `len == bits_free` the guard is **false** (strict `>`), so no resize happens. The subsequent code then: 1. Subtracts `len` from `bits_free`, making it exactly 0. 2. Enters the `bits_free <= 0` branch and executes `++m_pos`. 3. Writes `data[m_pos]` — now equal to `data[data.size()]` — **out of bounds**. ### Fix Change `len > bits_free` to `len >= bits_free` so the buffer is grown whenever the current slot will be exactly or more than consumed. ```diff -if ((m_pos == (data.size() - 1) && len > bits_free) || m_pos == data.size()) +if ((m_pos == (data.size() - 1) && len >= bits_free) || m_pos == data.size()) ``` ### Verification Reproducer (1×1 grayscale frame, `CAP_OPENCV_MJPEG`): ```cpp uint8_t pixel = 0xff; cv::Mat frame(1, 1, CV_8UC1, &pixel); int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G'); cv::VideoWriter writer; writer.open("/tmp/poc.avi", cv::CAP_OPENCV_MJPEG, fourcc, 25.0, cv::Size(1,1), false); writer.write(frame); ``` Ran under `-fsanitize=address,undefined`; exits cleanly with no error after this fix. ### Regression Test `TEST(Videoio_MJPEG, put_bits_no_heap_overflow)` added to `modules/videoio/test/test_video_io.cpp` — opens a `CAP_OPENCV_MJPEG` VideoWriter for a 1×1 grayscale file and writes one frame; asserts `EXPECT_NO_THROW`. --- modules/videoio/src/cap_mjpeg_encoder.cpp | 2 +- modules/videoio/test/test_video_io.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/videoio/src/cap_mjpeg_encoder.cpp b/modules/videoio/src/cap_mjpeg_encoder.cpp index 2bb01f4a81..387f46bbf5 100644 --- a/modules/videoio/src/cap_mjpeg_encoder.cpp +++ b/modules/videoio/src/cap_mjpeg_encoder.cpp @@ -161,7 +161,7 @@ public: inline void put_bits(unsigned bits, int len) { CV_Assert(len >=0 && len < 32); - if((m_pos == (data.size() - 1) && len > bits_free) || m_pos == data.size()) + if((m_pos == (data.size() - 1) && len >= bits_free) || m_pos == data.size()) { resize(int(2*data.size())); } diff --git a/modules/videoio/test/test_video_io.cpp b/modules/videoio/test/test_video_io.cpp index 0461b30835..0b73aabbaf 100644 --- a/modules/videoio/test/test_video_io.cpp +++ b/modules/videoio/test/test_video_io.cpp @@ -1280,4 +1280,21 @@ VideoCaptureAPIs seekable_backeinds[] = {CAP_FFMPEG, CAP_MSMF, CAP_AVFOUNDATION} INSTANTIATE_TEST_CASE_P(videoio, PreciseSeekingTest, testing::ValuesIn(seekable_backeinds), safe_capture_name_printer); +// Regression test for heap-buffer-overflow in mjpeg_buffer::put_bits (GitHub issue #29112). +// When len == bits_free the old guard used strict '>' and skipped the resize, causing +// an out-of-bounds write after '++m_pos' advanced past data.size(). +TEST(Videoio_MJPEG, put_bits_no_heap_overflow) +{ + const std::string filename = cv::tempfile(".avi"); + cv::Mat frame(1, 1, CV_8UC1, cv::Scalar::all(255)); + int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G'); + { + cv::VideoWriter writer; + ASSERT_NO_THROW(writer.open(filename, CAP_OPENCV_MJPEG, fourcc, 25.0, cv::Size(1, 1), false)); + ASSERT_TRUE(writer.isOpened()); + EXPECT_NO_THROW(writer.write(frame)); + } + remove(filename.c_str()); +} + } // namespace From dd214962a5dd98658436b5d31160d1d250dfc9ad Mon Sep 17 00:00:00 2001 From: Adrian Kretz Date: Sun, 24 May 2026 09:53:48 +0200 Subject: [PATCH 10/15] Merge pull request #29109 from akretz:fix-issue-23644 Better Durand-Kerner Initialization #29109 While investigating issue #23644, I have found [this paper](https://link.springer.com/article/10.1007/BF01935059) which presents a good initialization for the Durand-Kerner algorithm. Basically the idea is to put the initial points equidistantly on a circle on the complex plane. The radius of the circle is computed as image Note that the $a_i$ coefficients in that paper are reversed compared to OpenCV. That's where the `(n - i)` in the code comes from. I have implemented just the mean of the $u_i$'s for the sake of simplicity. That's already enough to make the algorithm converge in all cases I have tested. I have used this to test for convergence for many polynomials of order 2 and 4 and coefficients of different magnitudes: ```cpp TEST(Core_SolvePoly, large_test) { cv::Mat_ coefs3(1,3); cv::Mat_ coefs5(1,5); cv::Mat r; double prec; for (int c0 = -20; c0 <= 20; c0++) { coefs3.at(0) = c0; for (int c1 = -20; c1 <= 20; c1++) { coefs3.at(1) = c1; for (int c2 = -20; c2 <= 20; c2++) { coefs3.at(2) = c2; prec = cv::solvePoly(coefs3, r); EXPECT_LE(prec, 1e-6); } } } for (int c0 = -10; c0 <= 10; c0++) { coefs5.at(0) = c0; for (int c1 = -10; c1 <= 10; c1++) { coefs5.at(1) = c1; for (int c2 = -10; c2 <= 10; c2++) { coefs5.at(2) = c2; for (int c3 = -10; c3 <= 10; c3++) { coefs5.at(3) = c3; for (int c4 = -10; c4 <= 10; c4++) { coefs5.at(4) = c4; prec = cv::solvePoly(coefs5, r); EXPECT_LE(prec, 1e-2); } } } } } for (int i = -10; i < 10; i++) { coefs3.at(0) = pow(2, i); for (int j = -10; j < 10; j++) { coefs3.at(1) = pow(2, j); for (int k = -10; k < 10; k++) { coefs3.at(2) = pow(2, k); prec = cv::solvePoly(coefs3, r); EXPECT_LE(prec, 1e-6); } } } } ``` This test passes, but I have not committed it because it runs for a couple of seconds. This fixes #23644 and replaces #29055. I have checked #29055 and it does not pass the test above. It seems to be optimized to the precise polynomial of #23644. ### 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/misc/java/test/CoreTest.java | 26 +++++-- modules/core/src/mathfuncs.cpp | 35 ++++++++- modules/core/test/test_math.cpp | 87 +++++++++++++++++++++++ 3 files changed, 140 insertions(+), 8 deletions(-) diff --git a/modules/core/misc/java/test/CoreTest.java b/modules/core/misc/java/test/CoreTest.java index 62d72b706f..8d6b731616 100644 --- a/modules/core/misc/java/test/CoreTest.java +++ b/modules/core/misc/java/test/CoreTest.java @@ -1820,12 +1820,18 @@ public class CoreTest extends OpenCVTestCase { assertGE(1e-6, Math.abs(Core.solvePoly(coeffs, roots))); - truth = new Mat(3, 1, CvType.CV_32FC2) { + List rootsReIm = new ArrayList(); + Core.split(roots, rootsReIm); + Core.sort(rootsReIm.get(0), dst, Core.SORT_EVERY_COLUMN); + + Mat truthRe = new Mat(3, 1, CvType.CV_32F) { { - put(0, 0, 1, 0, 2, 0, 3, 0); + put(0, 0, 1, 2, 3); } }; - assertMatEqual(truth, roots, EPS); + Mat truthIm = Mat.zeros(3, 1, CvType.CV_32F); + assertMatEqual(truthRe, dst, EPS); + assertMatEqual(truthIm, rootsReIm.get(1), EPS); } public void testSolvePolyMatMatInt() { @@ -1836,14 +1842,20 @@ public class CoreTest extends OpenCVTestCase { }; Mat roots = new Mat(); - assertEquals(10.198039027185569, Core.solvePoly(coeffs, roots, 1)); + assertGE(1e-6, Core.solvePoly(coeffs, roots, 10)); - truth = new Mat(3, 1, CvType.CV_32FC2) { + List rootsReIm = new ArrayList(); + Core.split(roots, rootsReIm); + Core.sort(rootsReIm.get(0), dst, Core.SORT_EVERY_COLUMN); + + Mat truthRe = new Mat(3, 1, CvType.CV_32F) { { - put(0, 0, 1, 0, -1, 2, -2, 12); + put(0, 0, 1, 2, 3); } }; - assertMatEqual(truth, roots, EPS); + Mat truthIm = Mat.zeros(3, 1, CvType.CV_32F); + assertMatEqual(truthRe, dst, EPS); + assertMatEqual(truthIm, rootsReIm.get(1), EPS); } public void testSort() { diff --git a/modules/core/src/mathfuncs.cpp b/modules/core/src/mathfuncs.cpp index abcfb54215..c3bd50e0d4 100644 --- a/modules/core/src/mathfuncs.cpp +++ b/modules/core/src/mathfuncs.cpp @@ -1748,7 +1748,40 @@ double cv::solvePoly( InputArray _coeffs0, OutputArray _roots0, int maxIters ) break; } - C p(1, 0), r(1, 1); + // Related issue: https://github.com/opencv/opencv/issues/23644, + // This the initialization scheme of "Initial approximations in Durand-Kerner's root finding method" by Guggenheimer. + // https://link.springer.com/article/10.1007/BF01935059 + // We put the initial points equidistantly on a circle on the complex plane. This code computes the circle radius as in the paper. + Mat absCoeffs(n + 1, 1, CV_64F); + for( i = 0; i <= n; i++ ) + absCoeffs.at(i) = abs(coeffs[i]); + + int nonZeroCoeffs = 0; + Mat u(n, 1, CV_64F, Scalar(0)), v(n, 1, CV_64F, Scalar(0)); + for( i = 0; i <= n; i++ ) + { + double coeff = absCoeffs.at(i); + if( coeff > DBL_EPSILON ) + { + if( i != n ) + u.at(i) = 2.0 * pow(coeff / absCoeffs.at(n), 1.0 / (n - i)); + if( i != 0 ) + v.at(i - 1) = 0.5 * pow(absCoeffs.at(0) / coeff, 1.0 / i); + nonZeroCoeffs++; + } + } + double scale = 1; + if( nonZeroCoeffs > 2 ) + { + Point maxU, minV; + minMaxLoc(u, nullptr, nullptr, nullptr, &maxU); + minMaxLoc(v, nullptr, nullptr, &minV); + u.at(maxU) = 0; + v.at(minV) = 0; + scale = (sum(u).val[0] + sum(v).val[0]) / (2 * nonZeroCoeffs - 2); + } + + C p(scale, 0), r(cos(CV_2PI / n), sin(CV_2PI / n)); for( i = 0; i < n; i++ ) { diff --git a/modules/core/test/test_math.cpp b/modules/core/test/test_math.cpp index 1c9aa81d32..70c244d351 100644 --- a/modules/core/test/test_math.cpp +++ b/modules/core/test/test_math.cpp @@ -2679,6 +2679,93 @@ TEST(Core_SolvePoly, regression_5599) } } +TEST(Core_SolvePoly, regression_23644) +{ + // x^2 - 2x - 3 = 0, roots: 3, -1 + cv::Mat coefs = (cv::Mat_(1,3) << -3, -2, 1 ); + cv::Mat r; + double prec; + prec = cv::solvePoly(coefs, r); + EXPECT_LE(prec, 1e-6); + EXPECT_EQ(2u, r.total()); + ASSERT_EQ(CV_32FC2, r.type()); + checkRoot(r, 3, 0); + checkRoot(r, -1, 0); +} + +TEST(Core_SolvePoly, degree_2_polynomials) +{ + cv::Mat_ coefs(1,3); + cv::Mat r; + double prec; + for (int c0 = -20; c0 <= 20; c0++) + { + coefs.at(0) = c0; + for (int c1 = -20; c1 <= 20; c1++) + { + coefs.at(1) = c1; + for (int c2 = -20; c2 <= 20; c2++) + { + coefs.at(2) = c2; + prec = cv::solvePoly(coefs, r); + EXPECT_LE(prec, 1e-6); + } + } + } +} + +TEST(Core_SolvePoly, degree_4_polynomials) +{ + applyTestTag(CV_TEST_TAG_VERYLONG); + + cv::Mat_ coefs(1,5); + cv::Mat r; + double prec; + for (int c0 = -10; c0 <= 10; c0++) + { + coefs.at(0) = c0; + for (int c1 = -10; c1 <= 10; c1++) + { + coefs.at(1) = c1; + for (int c2 = -10; c2 <= 10; c2++) + { + coefs.at(2) = c2; + for (int c3 = -10; c3 <= 10; c3++) + { + coefs.at(3) = c3; + for (int c4 = -10; c4 <= 10; c4++) + { + coefs.at(4) = c4; + prec = cv::solvePoly(coefs, r); + EXPECT_LE(prec, 1e-3); + } + } + } + } + } +} + +TEST(Core_SolvePoly, different_magnitudes_polynomials) +{ + cv::Mat_ coefs(1,3); + cv::Mat r; + double prec; + for (int i = -10; i < 10; i++) + { + coefs.at(0) = pow(2, i); + for (int j = -10; j < 10; j++) + { + coefs.at(1) = pow(2, j); + for (int k = -10; k < 10; k++) + { + coefs.at(2) = pow(2, k); + prec = cv::solvePoly(coefs, r); + EXPECT_LE(prec, 1e-6); + } + } + } +} + class Core_PhaseTest : public cvtest::BaseTest { int t; From ffd0cf49a71b9c2c65c371610d3e2f365db85d2d Mon Sep 17 00:00:00 2001 From: Jules <16029431+jules-ai@users.noreply.github.com> Date: Sun, 24 May 2026 16:41:17 +0800 Subject: [PATCH 11/15] Merge pull request #28667 from jules-ai:jules_fix_jpeg_sampling_factor Fix JPEG chroma configuration & Standardize component settings #28667 ### Problem Incorrectly treated Chroma as a single channel. In $YC_bC_r$, chroma must be set in both separate channels ($C_b$ and $C_r$). ### Fix - Explicit Setup: Configured all three components ($Y$, $C_b$, $C_r$) individually. - Standardization: Removed reliance on library defaults to prevent undefined behavior. ### 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 - [ ] 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/imgcodecs/src/grfmt_jpeg.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index 499036cedf..a2520179b8 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -825,10 +825,12 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) if( (channels > 1) && ( sampling_factor != 0 ) ) { - cinfo.comp_info[0].v_samp_factor = (sampling_factor >> 16 ) & 0xF; cinfo.comp_info[0].h_samp_factor = (sampling_factor >> 20 ) & 0xF; - cinfo.comp_info[1].v_samp_factor = 1; - cinfo.comp_info[1].h_samp_factor = 1; + cinfo.comp_info[0].v_samp_factor = (sampling_factor >> 16 ) & 0xF; + cinfo.comp_info[1].h_samp_factor = (sampling_factor >> 12 ) & 0xF; + cinfo.comp_info[1].v_samp_factor = (sampling_factor >> 8 ) & 0xF; + cinfo.comp_info[2].h_samp_factor = (sampling_factor >> 4 ) & 0xF; + cinfo.comp_info[2].v_samp_factor = (sampling_factor >> 0 ) & 0xF; } if (luma_quality >= 0 && chroma_quality >= 0) @@ -843,6 +845,8 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[1].h_samp_factor = 1; + cinfo.comp_info[2].v_samp_factor = 1; + cinfo.comp_info[2].h_samp_factor = 1; } jpeg_default_qtables( &cinfo, TRUE ); #else From ff540453d594f141edd95146d7a247d6f838239c Mon Sep 17 00:00:00 2001 From: yuomy <2837795781@qq.com> Date: Sat, 23 May 2026 22:47:07 +0800 Subject: [PATCH 12/15] gapi: optimize Fluid Select kernel using Universal Intrinsics - Replaced scalar fallback and legacy CV_SIMD128 with modern scalable CV_SIMD. - Added support for multi-channel (2/3/4) and 16-bit (ushort/short) configurations. - Utilized v_select for branchless execution and vx_load_expand for mixed-bitwidth mask handling. - Updated accuracy and perf tests. --- .../perf/cpu/gapi_core_perf_tests_fluid.cpp | 3 +- .../gapi/src/backends/fluid/gfluidcore.cpp | 88 +++------ .../fluid/gfluidcore_func.dispatch.cpp | 22 +++ .../src/backends/fluid/gfluidcore_func.hpp | 18 ++ .../backends/fluid/gfluidcore_func.simd.hpp | 169 ++++++++++++++++++ .../gapi/test/cpu/gapi_core_tests_fluid.cpp | 9 +- 6 files changed, 244 insertions(+), 65 deletions(-) diff --git a/modules/gapi/perf/cpu/gapi_core_perf_tests_fluid.cpp b/modules/gapi/perf/cpu/gapi_core_perf_tests_fluid.cpp index 2cadd01a0b..5e58c6ce0a 100644 --- a/modules/gapi/perf/cpu/gapi_core_perf_tests_fluid.cpp +++ b/modules/gapi/perf/cpu/gapi_core_perf_tests_fluid.cpp @@ -159,7 +159,8 @@ INSTANTIATE_TEST_CASE_P(BitwiseNotPerfTestFluid, BitwiseNotPerfTest, INSTANTIATE_TEST_CASE_P(SelectPerfTestFluid, SelectPerfTest, Combine(Values(AbsExact().to_compare_f()), Values(szSmall128, szVGA, sz720p, sz1080p), - Values(CV_8UC1, CV_8UC3, CV_16UC1, CV_16SC1), + Values(CV_8UC1, CV_8UC2, CV_8UC3, CV_8UC4, + CV_16UC1, CV_16UC3, CV_16SC1, CV_16SC3), Values(cv::compile_args(CORE_FLUID)))); INSTANTIATE_TEST_CASE_P(MinPerfTestFluid, MinPerfTest, diff --git a/modules/gapi/src/backends/fluid/gfluidcore.cpp b/modules/gapi/src/backends/fluid/gfluidcore.cpp index aeface5046..1f781d518a 100644 --- a/modules/gapi/src/backends/fluid/gfluidcore.cpp +++ b/modules/gapi/src/backends/fluid/gfluidcore.cpp @@ -2195,64 +2195,28 @@ GAPI_FLUID_KERNEL(GFluidInRange, cv::gapi::core::GInRange, false) // //---------------------- -// manually vectored function for important case if RGB/BGR image -static void run_select_row3(int width, uchar out[], uchar in1[], uchar in2[], uchar in3[]) +#if (CV_SIMD || CV_SIMD_SCALABLE) + +template +static CV_ALWAYS_INLINE +typename std::enable_if::value, int>::type +call_select_simd(const SRC in1[], const SRC in2[], const uchar in3[], + SRC out[], int width, int chan) { - int w = 0; // cycle index + return select_simd(in1, in2, in3, out, width, chan); +} -#if CV_SIMD128 - for (; w <= width-16; w+=16) - { - v_uint8x16 a1, b1, c1; - v_uint8x16 a2, b2, c2; - v_uint8x16 mask; - v_uint8x16 a, b, c; - v_load_deinterleave(&in1[3*w], a1, b1, c1); - v_load_deinterleave(&in2[3*w], a2, b2, c2); - - mask = v_load(&in3[w]); - mask = v_ne(mask, v_setzero_u8()); - - a = v_select(mask, a1, a2); - b = v_select(mask, b1, b2); - c = v_select(mask, c1, c2); - - v_store_interleave(&out[3*w], a, b, c); - } +template +static CV_ALWAYS_INLINE +typename std::enable_if::value, int>::type +call_select_simd(const SRC[], const SRC[], const uchar[], + SRC[], int, int) +{ + return 0; +} #endif - for (; w < width; w++) - { - out[3*w ] = in3[w]? in1[3*w ]: in2[3*w ]; - out[3*w + 1] = in3[w]? in1[3*w + 1]: in2[3*w + 1]; - out[3*w + 2] = in3[w]? in1[3*w + 2]: in2[3*w + 2]; - } -} - -// parameter chan is compile-time known constant, normally chan=1..4 -template -static void run_select_row(int width, DST out[], SRC1 in1[], SRC2 in2[], SRC3 in3[]) -{ - if (std::is_same::value && chan==3) - { - // manually vectored function for important case if RGB/BGR image - run_select_row3(width, (uchar*)out, (uchar*)in1, (uchar*)in2, (uchar*)in3); - return; - } - - // because `chan` is template parameter, its value is known at compilation time, - // so that modern compilers would efficiently vectorize this cycle if chan==1 - // (if chan>1, compilers may need help with de-interleaving of the channels) - for (int w=0; w < width; w++) - { - for (int c=0; c < chan; c++) - { - out[w*chan + c] = in3[w]? in1[w*chan + c]: in2[w*chan + c]; - } - } -} - template static void run_select(Buffer &dst, const View &src1, const View &src2, const View &src3) { @@ -2261,7 +2225,6 @@ static void run_select(Buffer &dst, const View &src1, const View &src2, const Vi static_assert(std::is_same::value, "wrong types"); auto *out = dst.OutLine(); - const auto *in1 = src1.InLine(0); const auto *in2 = src2.InLine(0); const auto *in3 = src3.InLine(0); @@ -2269,13 +2232,18 @@ static void run_select(Buffer &dst, const View &src1, const View &src2, const Vi int width = dst.length(); int chan = dst.meta().chan; - switch (chan) + int w = 0; + +#if (CV_SIMD || CV_SIMD_SCALABLE) + w = call_select_simd(in1, in2, in3, out, width, chan); +#endif + + for (; w < width; w++) { - case 1: run_select_row<1>(width, out, in1, in2, in3); break; - case 2: run_select_row<2>(width, out, in1, in2, in3); break; - case 3: run_select_row<3>(width, out, in1, in2, in3); break; - case 4: run_select_row<4>(width, out, in1, in2, in3); break; - default: CV_Error(cv::Error::StsBadArg, "unsupported number of channels"); + for (int c = 0; c < chan; c++) + { + out[w*chan + c] = in3[w] ? in1[w*chan + c] : in2[w*chan + c]; + } } } diff --git a/modules/gapi/src/backends/fluid/gfluidcore_func.dispatch.cpp b/modules/gapi/src/backends/fluid/gfluidcore_func.dispatch.cpp index 10e88ea44b..8a1e9e1d33 100644 --- a/modules/gapi/src/backends/fluid/gfluidcore_func.dispatch.cpp +++ b/modules/gapi/src/backends/fluid/gfluidcore_func.dispatch.cpp @@ -420,8 +420,30 @@ INRANGE_SIMD(short) #undef INRANGE_SIMD +//----------------------------------- +// +// Fluid kernels: Select +// +//----------------------------------- + +#define DISPATCH_SELECT_SIMD(SRC) \ +int select_simd(const SRC in1[], const SRC in2[], const uchar in3[], \ + SRC out[], const int length, const int chan) \ +{ \ + CV_CPU_DISPATCH(select_simd, (in1, in2, in3, out, length, chan), \ + CV_CPU_DISPATCH_MODES_ALL); \ +} + +DISPATCH_SELECT_SIMD(uchar) +DISPATCH_SELECT_SIMD(ushort) +DISPATCH_SELECT_SIMD(short) + +#undef DISPATCH_SELECT_SIMD + } // namespace fluid } // namespace gapi } // namespace cv + + #endif // CV_SIMD #endif // !defined(GAPI_STANDALONE) diff --git a/modules/gapi/src/backends/fluid/gfluidcore_func.hpp b/modules/gapi/src/backends/fluid/gfluidcore_func.hpp index 28bffc5bc5..305c1172d3 100644 --- a/modules/gapi/src/backends/fluid/gfluidcore_func.hpp +++ b/modules/gapi/src/backends/fluid/gfluidcore_func.hpp @@ -331,8 +331,26 @@ INRANGE_SIMD(short) #undef INRANGE_SIMD +//----------------------------------- +// +// Fluid kernels: Select +// +//----------------------------------- +#define SELECT_SIMD(SRC) \ +int select_simd(const SRC in1[], const SRC in2[], const uchar in3[], \ + SRC out[], const int length, const int chan); + +SELECT_SIMD(uchar) +SELECT_SIMD(ushort) +SELECT_SIMD(short) + +#undef SELECT_SIMD + } // namespace fluid } // namespace gapi } // namespace cv + + + #endif // !defined(GAPI_STANDALONE) diff --git a/modules/gapi/src/backends/fluid/gfluidcore_func.simd.hpp b/modules/gapi/src/backends/fluid/gfluidcore_func.simd.hpp index 10e332264e..d0c805cb95 100644 --- a/modules/gapi/src/backends/fluid/gfluidcore_func.simd.hpp +++ b/modules/gapi/src/backends/fluid/gfluidcore_func.simd.hpp @@ -347,6 +347,18 @@ int merge4_simd(const uchar in1[], const uchar in2[], const uchar in3[], const uchar in4[], uchar out[], const int width); + +#define DECLARE_SELECT_SIMD(SRC) \ +int select_simd(const SRC in1[], const SRC in2[], const uchar in3[], \ + SRC out[], const int length, const int chan); + +DECLARE_SELECT_SIMD(uchar) +DECLARE_SELECT_SIMD(ushort) +DECLARE_SELECT_SIMD(short) + +#undef DECLARE_SELECT_SIMD + + #ifndef CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY #define SRC_SHORT_OR_USHORT std::is_same::value || std::is_same::value @@ -3379,6 +3391,163 @@ INRANGE_SIMD(short) #undef INRANGE_SIMD +//----------------------------------- +// +// Fluid kernels: Select +// +//----------------------------------- + + + +CV_ALWAYS_INLINE int select_simd_impl(const uchar in1[], const uchar in2[], const uchar in3[], + uchar out[], const int length, const int chan) +{ + const int nlanes = VTraits::vlanes(); + if (length < nlanes) return 0; + + int x = 0; + for (;;) + { + for (; x <= length - nlanes; x += nlanes) + { + v_uint8 m = vx_load(&in3[x]); + m = v_ne(m, vx_setzero_u8()); + if (chan == 1) + { + v_uint8 a = vx_load(&in1[x]); + v_uint8 b = vx_load(&in2[x]); + v_store(&out[x], v_select(m, a, b)); + } + else if (chan == 2) + { + v_uint8 a0, a1, b0, b1; + v_load_deinterleave(&in1[2 * x], a0, a1); + v_load_deinterleave(&in2[2 * x], b0, b1); + v_store_interleave(&out[2 * x], v_select(m, a0, b0), + v_select(m, a1, b1)); + } + else if (chan == 3) + { + v_uint8 a0, a1, a2, b0, b1, b2; + v_load_deinterleave(&in1[3 * x], a0, a1, a2); + v_load_deinterleave(&in2[3 * x], b0, b1, b2); + v_store_interleave(&out[3 * x], v_select(m, a0, b0), + v_select(m, a1, b1), + v_select(m, a2, b2)); + } + else if (chan == 4) + { + v_uint8 a0, a1, a2, a3, b0, b1, b2, b3; + v_load_deinterleave(&in1[4 * x], a0, a1, a2, a3); + v_load_deinterleave(&in2[4 * x], b0, b1, b2, b3); + v_store_interleave(&out[4 * x], v_select(m, a0, b0), + v_select(m, a1, b1), + v_select(m, a2, b2), + v_select(m, a3, b3)); + } + } + + if (x < length) { x = length - nlanes; continue; } + break; + } + return x; +} + + +CV_ALWAYS_INLINE int select_simd_impl(const ushort in1[], const ushort in2[], const uchar in3[], + ushort out[], const int length, const int chan) +{ + const int nlanes = VTraits::vlanes(); + if (length < nlanes) return 0; + int x = 0; + for (;;) + { + for (; x <= length - nlanes; x += nlanes) + { + v_uint16 m = vx_load_expand(&in3[x]); + m = v_ne(m, vx_setzero_u16()); + + if (chan == 1) { + v_uint16 a = vx_load(&in1[x]); + v_uint16 b = vx_load(&in2[x]); + v_store(&out[x], v_select(m, a, b)); + } else if (chan == 2) { + v_uint16 a0, a1, b0, b1; + v_load_deinterleave(&in1[2 * x], a0, a1); + v_load_deinterleave(&in2[2 * x], b0, b1); + v_store_interleave(&out[2 * x], v_select(m, a0, b0), v_select(m, a1, b1)); + } else if (chan == 3) { + v_uint16 a0, a1, a2, b0, b1, b2; + v_load_deinterleave(&in1[3 * x], a0, a1, a2); + v_load_deinterleave(&in2[3 * x], b0, b1, b2); + v_store_interleave(&out[3 * x], v_select(m, a0, b0), v_select(m, a1, b1), v_select(m, a2, b2)); + } else if (chan == 4) { + v_uint16 a0, a1, a2, a3, b0, b1, b2, b3; + v_load_deinterleave(&in1[4 * x], a0, a1, a2, a3); + v_load_deinterleave(&in2[4 * x], b0, b1, b2, b3); + v_store_interleave(&out[4 * x], v_select(m, a0, b0), v_select(m, a1, b1), v_select(m, a2, b2), v_select(m, a3, b3)); + } + } + if (x < length) { x = length - nlanes; continue; } + break; + } + return x; +} + +CV_ALWAYS_INLINE int select_simd_impl(const short in1[], const short in2[], const uchar in3[], + short out[], const int length, const int chan) +{ + const int nlanes = VTraits::vlanes(); + if (length < nlanes) return 0; + int x = 0; + for (;;) + { + for (; x <= length - nlanes; x += nlanes) + { + v_int16 m = v_reinterpret_as_s16(vx_load_expand(&in3[x])); + m = v_ne(m, vx_setzero_s16()); + + if (chan == 1) { + v_int16 a = vx_load(&in1[x]); + v_int16 b = vx_load(&in2[x]); + v_store(&out[x], v_select(m, a, b)); + } else if (chan == 2) { + v_int16 a0, a1, b0, b1; + v_load_deinterleave(&in1[2 * x], a0, a1); + v_load_deinterleave(&in2[2 * x], b0, b1); + v_store_interleave(&out[2 * x], v_select(m, a0, b0), v_select(m, a1, b1)); + } else if (chan == 3) { + v_int16 a0, a1, a2, b0, b1, b2; + v_load_deinterleave(&in1[3 * x], a0, a1, a2); + v_load_deinterleave(&in2[3 * x], b0, b1, b2); + v_store_interleave(&out[3 * x], v_select(m, a0, b0), v_select(m, a1, b1), v_select(m, a2, b2)); + } else if (chan == 4) { + v_int16 a0, a1, a2, a3, b0, b1, b2, b3; + v_load_deinterleave(&in1[4 * x], a0, a1, a2, a3); + v_load_deinterleave(&in2[4 * x], b0, b1, b2, b3); + v_store_interleave(&out[4 * x], v_select(m, a0, b0), v_select(m, a1, b1), v_select(m, a2, b2), v_select(m, a3, b3)); + } + } + if (x < length) { x = length - nlanes; continue; } + break; + } + return x; +} + + +#define SELECT_SIMD(SRC) \ +int select_simd(const SRC in1[], const SRC in2[], const uchar in3[], \ + SRC out[], const int length, const int chan) \ +{ \ + return select_simd_impl(in1, in2, in3, out, length, chan); \ +} + +SELECT_SIMD(uchar) +SELECT_SIMD(ushort) +SELECT_SIMD(short) + +#undef SELECT_SIMD + #endif // CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY CV_CPU_OPTIMIZATION_NAMESPACE_END diff --git a/modules/gapi/test/cpu/gapi_core_tests_fluid.cpp b/modules/gapi/test/cpu/gapi_core_tests_fluid.cpp index b61ff95147..2164cdac46 100644 --- a/modules/gapi/test/cpu/gapi_core_tests_fluid.cpp +++ b/modules/gapi/test/cpu/gapi_core_tests_fluid.cpp @@ -240,10 +240,11 @@ INSTANTIATE_TEST_CASE_P(DISABLED_CropTestFluid, CropTest, Values(cv::Rect(10, 8, 20, 35), cv::Rect(4, 10, 37, 50)))); INSTANTIATE_TEST_CASE_P(SelectTestFluid, SelectTest, - Combine(Values(CV_8UC3, CV_8UC1, CV_16UC1, CV_16SC1), - ValuesIn(in_sizes), - Values(-1), - Values(CORE_FLUID))); + Combine(Values(CV_8UC1, CV_8UC2, CV_8UC3, CV_8UC4, + CV_16UC1, CV_16UC3, CV_16SC1, CV_16SC3), + ValuesIn(in_sizes), + Values(-1), + Values(CORE_FLUID))); INSTANTIATE_TEST_CASE_P(Polar2CartFluid, Polar2CartTest, Combine(Values(CV_32FC1), From 852c3d2fb947574655087a2514f80c66b4ae5954 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Sun, 24 May 2026 16:54:39 +0300 Subject: [PATCH 13/15] Replace codecs.open() with open() since codecs.open() deprecated in Python 3.14 --- apps/pattern-tools/svgfig.py | 6 +++--- modules/java/generator/gen_java.py | 3 +-- modules/objc/generator/gen_objc.py | 5 ++--- platforms/ios/build_framework.py | 10 +++++----- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/pattern-tools/svgfig.py b/apps/pattern-tools/svgfig.py index 73a91ce134..323ed420c0 100644 --- a/apps/pattern-tools/svgfig.py +++ b/apps/pattern-tools/svgfig.py @@ -489,9 +489,9 @@ class SVG: f.close() else: - f = codecs.open(fileName, "w", encoding=encoding) - f.write(self.standalone_xml(encoding=encoding)) - f.close() + with open(fileName, "w", encoding=encoding) as f: + f.write(self.standalone_xml(encoding=encoding)) + def inkview(self, fileName=None, encoding="utf-8"): """View in "inkview", assuming that program is available on your system. diff --git a/modules/java/generator/gen_java.py b/modules/java/generator/gen_java.py index 591eb38ea3..691e2eee00 100755 --- a/modules/java/generator/gen_java.py +++ b/modules/java/generator/gen_java.py @@ -3,7 +3,6 @@ import sys, re, os.path, errno, fnmatch import json import logging -import codecs from shutil import copyfile from pprint import pformat from string import Template @@ -589,7 +588,7 @@ class JavaWrapperGenerator(object): content = f.read() if content == buf: return - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(buf) updated_files += 1 diff --git a/modules/objc/generator/gen_objc.py b/modules/objc/generator/gen_objc.py index 10071267e6..5dfabbe65d 100755 --- a/modules/objc/generator/gen_objc.py +++ b/modules/objc/generator/gen_objc.py @@ -4,7 +4,6 @@ from __future__ import print_function, unicode_literals import sys, re, os.path, errno, fnmatch import json import logging -import codecs import io from shutil import copyfile from pprint import pformat @@ -902,7 +901,7 @@ class ObjectiveCWrapperGenerator(object): content = f.read() if content == buf: return - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(buf) updated_files += 1 @@ -1530,7 +1529,7 @@ typedef NS_ENUM(int, {1}) {{ body = file.read() body = body.replace("import OpenCV", "import " + framework_name) body = body.replace("#import ", "#import <" + framework_name + "/" + framework_name + ".h>") - with codecs.open(filepath, "w", "utf-8") as file: + with open(filepath, "w", encoding="utf-8") as file: file.write(body) diff --git a/platforms/ios/build_framework.py b/platforms/ios/build_framework.py index 5bf6764361..bca699bf0c 100755 --- a/platforms/ios/build_framework.py +++ b/platforms/ios/build_framework.py @@ -32,7 +32,7 @@ Adding --dynamic parameter will build {framework_name}.framework as App Store dy """ from __future__ import print_function, unicode_literals -import glob, os, os.path, shutil, string, sys, argparse, traceback, multiprocessing, codecs, io +import glob, os, os.path, shutil, string, sys, argparse, traceback, multiprocessing, io from subprocess import check_call, check_output, CalledProcessError if sys.version_info >= (3, 8): # Python 3.8+ @@ -190,7 +190,7 @@ class Builder: body = body[:insert_pos] + "import " + self.framework_name + "\n" + body[insert_pos:] else: body = "import " + self.framework_name + "\n\n" + body - with codecs.open(os.path.join(swift_sources_dir, file), "w", "utf-8") as file_out: + with open(os.path.join(swift_sources_dir, file), "w", encoding="utf-8") as file_out: file_out.write(body) def build(self, outdir): @@ -380,7 +380,7 @@ class Builder: framework = self.framework_name, hosting_base_path = self.hosting_base_path ) - with codecs.open(os.path.join(docs_dir, "HOWTO.md"), "w", "utf-8") as file: + with open(os.path.join(docs_dir, "HOWTO.md"), "w", encoding="utf-8") as file: file.write(howto) self.docs_built = True execute(["cmake", "-DBUILD_TYPE=%s" % self.getConfiguration(), "-DCMAKE_INSTALL_PREFIX=%s" % (builddir + "/install"), "-P", "cmake_install.cmake"], cwd = framework_build_dir) @@ -473,11 +473,11 @@ class Builder: for dirname, dirs, files in os.walk(os.path.join(dstdir, "Headers")): for filename in files: filepath = os.path.join(dirname, filename) - with codecs.open(filepath, "r", "utf-8") as file: + with open(filepath, "r", encoding="utf-8") as file: body = file.read() body = body.replace("include \"opencv2/", "include \"" + name + "/") body = body.replace("include Date: Mon, 25 May 2026 08:39:58 +0300 Subject: [PATCH 14/15] Merge pull request #29120 from asmorkalov:as/png_warning_check Libpng warning fix on Windows #29120 CI warning: ``` Warning: C:\GHA-OCV-3\_work\opencv\opencv\opencv\3rdparty\libpng\pngread.c(4114,21): warning C4146: unary minus operator applied to unsigned type, result still unsigned [C:\GHA-OCV-3\_work\opencv\opencv\build\3rdparty\libpng\libpng.vcxproj] Warning: C:\GHA-OCV-3\_work\opencv\opencv\opencv\3rdparty\libpng\pngwrite.c(2033,21): warning C4146: unary minus operator applied to unsigned type, result still unsigned [C:\GHA-OCV-3\_work\opencv\opencv\build\3rdparty\libpng\libpng.vcxproj] ``` ### 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 --- 3rdparty/libpng/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/3rdparty/libpng/CMakeLists.txt b/3rdparty/libpng/CMakeLists.txt index f6f39a17ed..728a40c305 100644 --- a/3rdparty/libpng/CMakeLists.txt +++ b/3rdparty/libpng/CMakeLists.txt @@ -278,6 +278,7 @@ endif(PNG_HARDWARE_OPTIMIZATIONS) if(MSVC) add_definitions(-D_CRT_SECURE_NO_DEPRECATE) + ocv_warnings_disable(CMAKE_C_FLAGS /wd4146) endif(MSVC) add_library(${PNG_LIBRARY} STATIC ${OPENCV_3RDPARTY_EXCLUDE_FROM_ALL} ${lib_srcs} ${lib_hdrs}) From 3e530e578428eb2e8e5649e6ed398b06e0d524bf Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Mon, 25 May 2026 08:51:12 +0300 Subject: [PATCH 15/15] solvePoly warning fix on Windows. --- modules/core/test/test_math.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/core/test/test_math.cpp b/modules/core/test/test_math.cpp index 70c244d351..7b7f1b4a3f 100644 --- a/modules/core/test/test_math.cpp +++ b/modules/core/test/test_math.cpp @@ -2698,13 +2698,13 @@ TEST(Core_SolvePoly, degree_2_polynomials) cv::Mat_ coefs(1,3); cv::Mat r; double prec; - for (int c0 = -20; c0 <= 20; c0++) + for (float c0 = -20; c0 <= 20; c0++) { coefs.at(0) = c0; - for (int c1 = -20; c1 <= 20; c1++) + for (float c1 = -20; c1 <= 20; c1++) { coefs.at(1) = c1; - for (int c2 = -20; c2 <= 20; c2++) + for (float c2 = -20; c2 <= 20; c2++) { coefs.at(2) = c2; prec = cv::solvePoly(coefs, r); @@ -2721,19 +2721,19 @@ TEST(Core_SolvePoly, degree_4_polynomials) cv::Mat_ coefs(1,5); cv::Mat r; double prec; - for (int c0 = -10; c0 <= 10; c0++) + for (float c0 = -10; c0 <= 10; c0++) { coefs.at(0) = c0; - for (int c1 = -10; c1 <= 10; c1++) + for (float c1 = -10; c1 <= 10; c1++) { coefs.at(1) = c1; - for (int c2 = -10; c2 <= 10; c2++) + for (float c2 = -10; c2 <= 10; c2++) { coefs.at(2) = c2; - for (int c3 = -10; c3 <= 10; c3++) + for (float c3 = -10; c3 <= 10; c3++) { coefs.at(3) = c3; - for (int c4 = -10; c4 <= 10; c4++) + for (float c4 = -10; c4 <= 10; c4++) { coefs.at(4) = c4; prec = cv::solvePoly(coefs, r); @@ -2750,15 +2750,15 @@ TEST(Core_SolvePoly, different_magnitudes_polynomials) cv::Mat_ coefs(1,3); cv::Mat r; double prec; - for (int i = -10; i < 10; i++) + for (float i = -10; i < 10; i++) { - coefs.at(0) = pow(2, i); - for (int j = -10; j < 10; j++) + coefs.at(0) = pow(2.f, i); + for (float j = -10; j < 10; j++) { - coefs.at(1) = pow(2, j); - for (int k = -10; k < 10; k++) + coefs.at(1) = pow(2.f, j); + for (float k = -10; k < 10; k++) { - coefs.at(2) = pow(2, k); + coefs.at(2) = pow(2.f, k); prec = cv::solvePoly(coefs, r); EXPECT_LE(prec, 1e-6); }