From c2a118549a2ce3040001ab98fd30e789fbd7f100 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 17 Jul 2026 16:38:16 +0200 Subject: [PATCH 1/3] recipe: opencv-contrib-python 5.0.0.93 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contrib twin of opencv-python 5.0.0.93 — same importable cv2, with the opencv_contrib modules folded in (aruco, face, tracking, ximgproc, wechat_qrcode, img_hash, xfeatures2d [non-free off], dnn_superres, ...). The contrib sdist ships the identical opencv/ source tree (it only ADDS opencv_contrib/ and points the extra-modules path at it), so the recipe is a byte-for-byte twin: meta.yaml and mobile.patch mirror opencv-python's, and the 14-file patch real-applies clean to the contrib sdist. gen2.py's generated- binding fix covers the contrib modules generically. One contrib-only iOS wrinkle: rgbd's hand-written pyopencv_linemod.hpp uses bare std::vector>, which hits the Apple Ptr clash. Gated with -DBUILD_opencv_rgbd=OFF on the iOS lane only (Android has no MacTypes.h and keeps rgbd); restoring rgbd on iOS is a 4-line cv::Ptr follow-up. Tests de-versioned per convention; test_contrib_module_works proves a contrib-only module (img_hash perceptual hash) actually loads and runs on device. --- recipes/opencv-contrib-python/meta.yaml | 77 +++ .../patches/mobile.patch | 574 ++++++++++++++++++ .../tests/test_opencv_contrib_python.py | 60 ++ 3 files changed, 711 insertions(+) create mode 100644 recipes/opencv-contrib-python/meta.yaml create mode 100644 recipes/opencv-contrib-python/patches/mobile.patch create mode 100644 recipes/opencv-contrib-python/tests/test_opencv_contrib_python.py diff --git a/recipes/opencv-contrib-python/meta.yaml b/recipes/opencv-contrib-python/meta.yaml new file mode 100644 index 00000000..a301c030 --- /dev/null +++ b/recipes/opencv-contrib-python/meta.yaml @@ -0,0 +1,77 @@ +package: + name: opencv-contrib-python + version: "5.0.0.93" + +build: + number: 1 + script_env: +# {% if sdk == 'android' %} + CMAKE_ARGS: >- + -DANDROID=ON + -DWITH_IPP=OFF + -DWITH_ITT=OFF + -DWITH_KLEIDICV=OFF + -DBUILD_ANDROID_PROJECTS=OFF + -DBUILD_ANDROID_EXAMPLES=OFF + -DBUILD_TESTS=OFF + -DBUILD_PERF_TESTS=OFF + -DENABLE_TESTING=OFF + -DBUILD_EXAMPLES=OFF + -DANDROID_ABI={ANDROID_ABI} + -DANDROID_STANDALONE_TOOLCHAIN={NDK_ROOT} + -DANDROID_NATIVE_API_LEVEL={ANDROID_API_LEVEL} + -DANDROID_ALLOW_UNDEFINED_VERSION_SCRIPT_SYMBOLS=1 + -DCMAKE_TOOLCHAIN_FILE={NDK_ROOT}/build/cmake/android.toolchain.cmake + -DCMAKE_SHARED_LINKER_FLAGS="-Wl,-z,max-page-size=16384" + -DCMAKE_MODULE_LINKER_FLAGS="-Wl,-z,max-page-size=16384" + -DOPENCV_FORCE_PYTHON_LIBS=ON + -DPYTHON3_INCLUDE_PATH={HOST_PYTHON_HOME}/include/python{py_version_short} + -DPYTHON3_LIBRARIES={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.so + -DPYTHON3_NUMPY_INCLUDE_DIRS={platlib}/numpy/_core/include +# {% else %} + CMAKE_ARGS: >- + -DAPPLE_FRAMEWORK=ON + -DCMAKE_SYSTEM_NAME=iOS + -DCMAKE_SYSTEM_PROCESSOR={{ 'x86_64' if arch == 'x86_64' else 'aarch64' }} + -DCMAKE_OSX_SYSROOT={{ sdk }} + -DCMAKE_OSX_DEPLOYMENT_TARGET={{ sdk_version }} + -DCMAKE_OSX_ARCHITECTURES={{ arch }} + -DWITH_IPP=OFF + -DWITH_ITT=OFF + -DWITH_KLEIDICV=OFF + -DBUILD_TESTS=OFF + -DBUILD_PERF_TESTS=OFF + -DENABLE_TESTING=OFF + -DBUILD_EXAMPLES=OFF + -DWITH_OPENCL=OFF + # contrib-only, iOS-only: the rgbd module ships a HAND-WRITTEN binding + # helper (pyopencv_linemod.hpp) with bare `std::vector>`, which + # clashes with Apple 's global `typedef char* Ptr;` under + # `using namespace cv` (the same MacTypes clash the mobile.patch fixes for + # core/dnn/videoio; gen2.py's generated bindings are fixed generically, but + # this hand-written helper isn't). Android has no MacTypes.h so it keeps + # rgbd. To restore rgbd on iOS instead, patch those 4 lines to `cv::Ptr<...>`. + -DBUILD_opencv_rgbd=OFF + -DOPENCV_FORCE_PYTHON_LIBS=ON + -DPYTHON3_INCLUDE_PATH={HOST_PYTHON_HOME}/include/python{py_version_short} + -DPYTHON3_LIBRARIES={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.dylib + -DPYTHON3_NUMPY_INCLUDE_DIRS={platlib}/numpy/_core/include +# {% endif %} + +requirements: + build: + # opencv-python's setup.py invokes its own dependency check (via scikit-build) which + # hard-requires cmake>=3.5 on PATH. macOS runners pre-install cmake but iOS lane's + # cross-venv strips it from the shimmed PATH, so add the pip-installable shim that + # ships a cmake binary into the build venv. + - cmake + host: + - numpy ^2.0.0 + +patches: + - mobile.patch + +# So the loader's extra-submodule scan can os.listdir() the package; see mobile.patch +# note 3 for the Flet 0.86 native-relocation loader fix this pairs with. +extract_packages: + - cv2 \ No newline at end of file diff --git a/recipes/opencv-contrib-python/patches/mobile.patch b/recipes/opencv-contrib-python/patches/mobile.patch new file mode 100644 index 00000000..5669b583 --- /dev/null +++ b/recipes/opencv-contrib-python/patches/mobile.patch @@ -0,0 +1,574 @@ +Make opencv-contrib-python 5.0 cross-compile AND load on Flet's mobile targets. + +This is the contrib twin of the opencv-python recipe. The contrib sdist ships +the identical `opencv/` source tree (it only ADDS `opencv_contrib/` and points +the build's extra-modules path at it), so this patch is byte-for-byte the +opencv-python 5.0 mobile.patch and touches only `opencv/` files — the 14 hunks +apply unchanged. The generated bindings for the contrib modules are handled by +the same gen2.py fix (note 1). One contrib-only iOS wrinkle is NOT patched here +but gated in meta.yaml instead: the rgbd module's hand-written +`pyopencv_linemod.hpp` uses bare `std::vector>` and would hit the Apple + `Ptr` clash, so the iOS lane sets `-DBUILD_opencv_rgbd=OFF` +(Android keeps rgbd; restoring it on iOS is a 4-line `cv::Ptr` follow-up). The +design notes below apply unchanged; on-device figures cited are from the +opencv-python validation this mirrors. + +1. Enable the cv2 Python bindings on Android/iOS (opencv disables the python + modules on ANDROID / APPLE_FRAMEWORK by default; keep only the WINRT guard), + and qualify OpenCV types with cv:: in the binding sources. The cv:: bit is + iOS/macOS-only: Apple's defines a global `typedef char* Ptr;` + that clashes with cv::Ptr under `using namespace cv;`. We fix the hand-written + binding helpers (pyopencv_*/shadow_umat.hpp), pycompat.hpp's CV_PY macros, and + gen2.py's generated code (prefix_ambiguous_ios, incl. Ptr< nested inside other + templates such as std::vector>). OpenCV 5 dropped the python2 module + (adjusted guard) and the old setup.py cv2.data haarcascades hunk (5.0 + restructured cv2.data packaging), so that hunk is gone. + +2. Skip the vendored MLAS (Microsoft Linear Algebra Subprograms, new in OpenCV 5) + on the iOS APPLE_FRAMEWORK build. MLAS builds as an OBJECT library whose + objects are meant to link straight into opencv_dnn, but they do NOT propagate + into the iOS .framework binary, so `import cv2` dies at dlopen with an + undefined `MlasGemmBatch` symbol. Gating add_subdirectory(3rdparty/mlas) on + NOT APPLE_FRAMEWORK leaves HAVE_MLAS=0 there, so the MlasGemmBatch call sites + compile out and opencv_dnn falls back to its built-in SGEMM. Android keeps + MLAS (it static-links fine; verified on-device). + +3. Teach the cv2 loader (modules/python/package/cv2/__init__.py) to boot under + Flet 0.86. OpenCV's stock bootstrap exec()s a config.py to discover a directory + of `.so` files, then pops the cv2 package and re-imports it as the top-level + module "cv2" so the native binary loads under that exact name (its compiled-in + bindings register submodules like `cv2.dnn` and assert each starts with the + parent name). Under Flet 0.86 Android neither half survives: `config.py` is + compiled to `config.pyc` (so `os.path.exists('config.py')` is False -> + "missing configuration file"), and the native `cv2.cv2` extension is relocated + to jniLibs (importable only as the submodule `cv2.cv2` via a `cv2/cv2.soref` + marker, never as top-level `cv2`). Loading it as `cv2.cv2` makes OpenCV abort + with "Submodule name should always start with a parent module name. Parent + name: cv2.cv2. Submodule name: cv2". iOS breaks the same way but relocates the + native into a `.framework` instead: serious_python leaves a `cv2/cv2.fwork` + marker whose text is the app-relative path to the framework binary, so the + stock loader recurses ("recursion is detected during loading of cv2 binary + extensions") on the pop-and-reimport. The fix inserts a fast-path right after + the recursion guard: resolve the relocated extension via `find_spec('cv2.cv2')` + (Android: the soref finder yields the real jniLibs `.so`; iOS: the FileFinder + yields the `.fwork` marker path -> read it and join with + `dirname(sys.executable)` to get the framework binary, exactly as CPython's + AppleFrameworkLoader does), then load it through a fresh + `ExtensionFileLoader("cv2", origin)` so its runtime `__name__` is the required + top-level "cv2", relink its symbols onto the package, and load the extra pure- + python submodules. On desktop / non-0.86 builds `find_spec('cv2.cv2')` returns + None and the stock loader runs unchanged. Pairs with `extract_packages: [cv2]` + in meta.yaml so the package ships as a real dir (the extra-submodule scan does + `os.listdir` on it). Verified on-device: Android arm64 4/4. iOS bundle->dylib + link + framework packaging validated locally; loader `.fwork` resolution + mirrors AppleFrameworkLoader (CI-confirmed alongside the coolprop iOS pass). + +diff --git a/opencv/CMakeLists.txt b/opencv/CMakeLists.txt +index d6812aa..7555914 100644 +--- a/opencv/CMakeLists.txt ++++ b/opencv/CMakeLists.txt +@@ -593,7 +593,7 @@ set(OPENCV_EXTRA_MODULES_PATH "" CACHE PATH "Where to look for additional OpenCV + ocv_cmake_hook(POST_CMAKE_BUILD_OPTIONS) + + # --- Python Support --- +-if(NOT IOS AND NOT XROS) ++if(NOT XROS) + include(cmake/OpenCVDetectPython.cmake) + include(cmake/OpenCVDetectDLPack.cmake) + endif() +diff --git a/opencv/cmake/OpenCVDetectPython.cmake b/opencv/cmake/OpenCVDetectPython.cmake +index 4e3cfa3..1ddf28d 100644 +--- a/opencv/cmake/OpenCVDetectPython.cmake ++++ b/opencv/cmake/OpenCVDetectPython.cmake +@@ -115,7 +115,6 @@ if(NOT ${found}) + if(_found) + set(_version_major_minor "${_version_major}.${_version_minor}") + +- if(NOT ANDROID AND NOT APPLE_FRAMEWORK) + ocv_check_environment_variables(${library_env} ${include_dir_env}) + if(NOT ${${library_env}} STREQUAL "") + set(PYTHON_LIBRARY "${${library_env}}") +@@ -153,8 +152,8 @@ if(NOT ${found}) + # Copy outputs + set(_libs_found ${PYTHONLIBS_FOUND}) + set(_libraries ${PYTHON_LIBRARIES}) +- set(_include_path ${PYTHON_INCLUDE_PATH}) +- set(_include_dirs ${PYTHON_INCLUDE_DIRS}) ++ set(_include_path ${PYTHON_INCLUDE_DIR}) ++ set(_include_dirs ${PYTHON_INCLUDE_DIR}) + set(_debug_libraries ${PYTHON_DEBUG_LIBRARIES}) + set(_libs_version_string ${PYTHONLIBS_VERSION_STRING}) + set(_debug_library ${PYTHON_DEBUG_LIBRARY}) +@@ -182,9 +181,8 @@ if(NOT ${found}) + unset(PYTHON_INCLUDE_DIR CACHE) + unset(PYTHON_INCLUDE_DIR2 CACHE) + endif() +- endif() + +- if(NOT ANDROID AND NOT IOS AND NOT XROS) ++ if(NOT XROS) + if(CMAKE_HOST_UNIX) + execute_process(COMMAND ${_executable} -c "from sysconfig import *; print(get_path('purelib'))" + RESULT_VARIABLE _cvpy_process +@@ -246,7 +244,7 @@ if(NOT ${found}) + OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + endif() +- endif(NOT ANDROID AND NOT IOS AND NOT XROS) ++ endif(NOT XROS) + endif() + + # Export return values +diff --git a/opencv/modules/core/misc/python/pyopencv_cuda.hpp b/opencv/modules/core/misc/python/pyopencv_cuda.hpp +index ab451b0..306e1fd 100644 +--- a/opencv/modules/core/misc/python/pyopencv_cuda.hpp ++++ b/opencv/modules/core/misc/python/pyopencv_cuda.hpp +@@ -38,7 +38,7 @@ CV_PY_FROM_CLASS(cuda::HostMem) + CV_PY_FROM_CLASS_PTR(cuda::GpuMat::Allocator) + + template<> +-bool fillDLPackTensor(const Ptr& src, DLManagedTensor* tensor, const DLDevice& device) ++bool fillDLPackTensor(const cv::Ptr& src, DLManagedTensor* tensor, const DLDevice& device) + { + if ((device.device_type != -1 && device.device_type != kDLCUDA) || device.device_id != 0) + { +@@ -60,7 +60,7 @@ bool fillDLPackTensor(const Ptr& src, DLManagedTensor* tensor, + } + + template<> +-bool fillDLPackTensor(const Ptr& src, DLManagedTensor* tensor, const DLDevice& device) ++bool fillDLPackTensor(const cv::Ptr& src, DLManagedTensor* tensor, const DLDevice& device) + { + if ((device.device_type != -1 && device.device_type != kDLCUDA) || device.device_id != 0) + { +@@ -151,20 +151,20 @@ bool parseDLPackTensor(DLManagedTensor* tensor, cv::cuda::GpuMatND& obj, bool co + } + + template<> +-int GetNumDims(const Ptr& src) { return 3; } ++int GetNumDims(const cv::Ptr& src) { return 3; } + + template<> +-int GetNumDims(const Ptr& src) { return src->dims; } ++int GetNumDims(const cv::Ptr& src) { return src->dims; } + + static PyObject* pyDLPackGpuMat(PyObject* self, PyObject* py_args, PyObject* kw) { +- Ptr * self1 = 0; ++ cv::Ptr * self1 = 0; + if (!pyopencv_cuda_GpuMat_getp(self, self1)) + return failmsgp("Incorrect type of self (must be 'cuda_GpuMat' or its derivative)"); + return to_dlpack(*(self1), self, py_args, kw); + } + + static PyObject* pyDLPackGpuMatND(PyObject* self, PyObject* py_args, PyObject* kw) { +- Ptr * self1 = 0; ++ cv::Ptr * self1 = 0; + if (!pyopencv_cuda_GpuMatND_getp(self, self1)) + return failmsgp("Incorrect type of self (must be 'cuda_GpuMatND' or its derivative)"); + return to_dlpack(*(self1), self, py_args, kw); +diff --git a/opencv/modules/core/misc/python/pyopencv_umat.hpp b/opencv/modules/core/misc/python/pyopencv_umat.hpp +index 2e91cd5..9905bd0 100644 +--- a/opencv/modules/core/misc/python/pyopencv_umat.hpp ++++ b/opencv/modules/core/misc/python/pyopencv_umat.hpp +@@ -7,7 +7,7 @@ typedef std::vector vector_Range; + CV_PY_TO_CLASS(UMat) + CV_PY_FROM_CLASS(UMat) + +-static bool cv_mappable_to(const Ptr& src, Ptr& dst) ++static bool cv_mappable_to(const cv::Ptr& src, cv::Ptr& dst) + { + //dst.reset(new UMat(src->getUMat(ACCESS_RW))); + dst.reset(new UMat()); +diff --git a/opencv/modules/core/misc/python/shadow_umat.hpp b/opencv/modules/core/misc/python/shadow_umat.hpp +index b194aac..6e5b48e 100644 +--- a/opencv/modules/core/misc/python/shadow_umat.hpp ++++ b/opencv/modules/core/misc/python/shadow_umat.hpp +@@ -18,7 +18,7 @@ public: + CV_WRAP UMat(Size size, int type, const Scalar& s, UMatUsageFlags usageFlags = USAGE_DEFAULT); + + //! Mat is mappable to UMat +- CV_WRAP_MAPPABLE(Ptr); ++ CV_WRAP_MAPPABLE(cv::Ptr); + + //! returns the OpenCL queue used by OpenCV UMat + CV_WRAP_PHANTOM(static void* queue()); +diff --git a/opencv/modules/dnn/CMakeLists.txt b/opencv/modules/dnn/CMakeLists.txt +index a765483..eb26ac6 100644 +--- a/opencv/modules/dnn/CMakeLists.txt ++++ b/opencv/modules/dnn/CMakeLists.txt +@@ -500,7 +500,7 @@ foreach(_v OPENCV_DNN_MLAS_ENABLED OPENCV_DNN_MLAS_SKIP_REASON + MLAS_HAS_ASM MLAS_HAS_POWER10 MLAS_HAS_RISCV64_RVV) + unset(${_v} CACHE) + endforeach() +-if(NOT EMSCRIPTEN) ++if(NOT EMSCRIPTEN AND NOT APPLE_FRAMEWORK) + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/mlas" "${CMAKE_BINARY_DIR}/3rdparty/mlas") + endif() + if(HAVE_MLAS) +diff --git a/opencv/modules/dnn/misc/python/pyopencv_dnn.hpp b/opencv/modules/dnn/misc/python/pyopencv_dnn.hpp +index 0cc2fe6..9b27f1c 100644 +--- a/opencv/modules/dnn/misc/python/pyopencv_dnn.hpp ++++ b/opencv/modules/dnn/misc/python/pyopencv_dnn.hpp +@@ -133,14 +133,14 @@ public: + } + } + +- static Ptr create(dnn::LayerParams ¶ms) ++ static cv::Ptr create(dnn::LayerParams ¶ms) + { + std::map >::iterator it = pyLayers.find(params.type); + if (it == pyLayers.end()) + CV_Error(Error::StsNotImplemented, "Layer with a type \"" + params.type + + "\" is not implemented"); + CV_Assert(!it->second.empty()); +- return Ptr(new pycvLayer(params, it->second.back())); ++ return cv::Ptr(new pycvLayer(params, it->second.back())); + } + + virtual void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, OutputArrayOfArrays) CV_OVERRIDE +diff --git a/opencv/modules/python/CMakeLists.txt b/opencv/modules/python/CMakeLists.txt +index da3d082..923578b 100644 +--- a/opencv/modules/python/CMakeLists.txt ++++ b/opencv/modules/python/CMakeLists.txt +@@ -3,7 +3,7 @@ + # ---------------------------------------------------------------------------- + if(DEFINED OPENCV_INITIAL_PASS) # OpenCV build + +-if(ANDROID OR APPLE_FRAMEWORK OR WINRT) ++if(WINRT) + ocv_module_disable_(python3) + return() + elseif(BUILD_opencv_world OR (WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug")) +diff --git a/opencv/modules/python/src2/cv2.cpp b/opencv/modules/python/src2/cv2.cpp +index 129b961..2653ffd 100644 +--- a/opencv/modules/python/src2/cv2.cpp ++++ b/opencv/modules/python/src2/cv2.cpp +@@ -23,16 +23,16 @@ typedef std::vector vector_int; + typedef std::vector vector_float; + typedef std::vector vector_double; + typedef std::vector vector_size_t; +-typedef std::vector vector_Point; ++typedef std::vector vector_Point; + typedef std::vector vector_Point2f; + typedef std::vector vector_Point3f; +-typedef std::vector vector_Size; ++typedef std::vector vector_Size; + typedef std::vector vector_Vec2f; + typedef std::vector vector_Vec3f; + typedef std::vector vector_Vec4f; + typedef std::vector vector_Vec6f; + typedef std::vector vector_Vec4i; +-typedef std::vector vector_Rect; ++typedef std::vector vector_Rect; + typedef std::vector vector_Rect2d; + typedef std::vector vector_RotatedRect; + typedef std::vector vector_KeyPoint; +@@ -51,7 +51,7 @@ typedef std::vector vector_MSTEdge; + #endif // HAVE_OPENCV_GEOMETRY + + typedef std::vector > vector_vector_char; +-typedef std::vector > vector_vector_Point; ++typedef std::vector > vector_vector_Point; + typedef std::vector > vector_vector_Point2f; + typedef std::vector > vector_vector_Point3f; + typedef std::vector > vector_vector_DMatch; +diff --git a/opencv/modules/python/src2/cv2_convert.cpp b/opencv/modules/python/src2/cv2_convert.cpp +index 01d48a3..ae5a264 100644 +--- a/opencv/modules/python/src2/cv2_convert.cpp ++++ b/opencv/modules/python/src2/cv2_convert.cpp +@@ -770,7 +770,7 @@ PyObject* pyopencv_from(const std::string& value) + // --- Size + + template<> +-bool pyopencv_to(PyObject* obj, Size& sz, const ArgInfo& info) ++bool pyopencv_to(PyObject* obj, cv::Size& sz, const ArgInfo& info) + { + RefWrapper values[] = {RefWrapper(sz.width), + RefWrapper(sz.height)}; +@@ -778,7 +778,7 @@ bool pyopencv_to(PyObject* obj, Size& sz, const ArgInfo& info) + } + + template<> +-PyObject* pyopencv_from(const Size& sz) ++PyObject* pyopencv_from(const cv::Size& sz) + { + return Py_BuildValue("(ii)", sz.width, sz.height); + } +@@ -800,7 +800,7 @@ PyObject* pyopencv_from(const Size_& sz) + // --- Rect + + template<> +-bool pyopencv_to(PyObject* obj, Rect& r, const ArgInfo& info) ++bool pyopencv_to(PyObject* obj, cv::Rect& r, const ArgInfo& info) + { + RefWrapper values[] = {RefWrapper(r.x), RefWrapper(r.y), + RefWrapper(r.width), +@@ -809,7 +809,7 @@ bool pyopencv_to(PyObject* obj, Rect& r, const ArgInfo& info) + } + + template<> +-PyObject* pyopencv_from(const Rect& r) ++PyObject* pyopencv_from(const cv::Rect& r) + { + return Py_BuildValue("(iiii)", r.x, r.y, r.width, r.height); + } +@@ -956,14 +956,14 @@ PyObject* pyopencv_from(const Range& r) + // --- Point + + template<> +-bool pyopencv_to(PyObject* obj, Point& p, const ArgInfo& info) ++bool pyopencv_to(PyObject* obj, cv::Point& p, const ArgInfo& info) + { + RefWrapper values[] = {RefWrapper(p.x), RefWrapper(p.y)}; + return parseSequence(obj, values, info); + } + + template<> +-PyObject* pyopencv_from(const Point& p) ++PyObject* pyopencv_from(const cv::Point& p) + { + return Py_BuildValue("(ii)", p.x, p.y); + } +diff --git a/opencv/modules/python/src2/gen2.py b/opencv/modules/python/src2/gen2.py +index c8e12c6..49106c0 100755 +--- a/opencv/modules/python/src2/gen2.py ++++ b/opencv/modules/python/src2/gen2.py +@@ -43,7 +43,7 @@ gen_template_check_self = Template(""" + return failmsgp("Incorrect type of self (must be '${name}' or its derivative)"); + ${pname} _self_ = ${cvt}(self1); + """) +-gen_template_call_constructor_prelude = Template("""new (&(self->v)) Ptr<$cname>(); // init Ptr with placement new ++gen_template_call_constructor_prelude = Template("""new (&(self->v)) cv::Ptr<$cname>(); // init Ptr with placement new + if(self) """) + + gen_template_call_constructor = Template("""self->v.reset(new ${cname}${py_args})""") +@@ -439,7 +439,7 @@ class ClassInfo(object): + return 'CVPY_TYPE({}, {}, {}, {}, {}, {}, "{}")\n'.format( + self.export_name, + self.class_id, +- self.cname if self.issimple else "Ptr<{}>".format(self.cname), ++ self.cname if self.issimple else "cv::Ptr<{}>".format(self.cname), + self.original_name if self.issimple else "Ptr", + baseptr, + constructor_name, +@@ -450,7 +450,7 @@ class ClassInfo(object): + + def handle_ptr(tp): + if tp.startswith('Ptr_'): +- tp = 'Ptr<' + "::".join(tp.split('_')[1:]) + '>' ++ tp = 'cv::Ptr<' + "::".join(tp.split('_')[1:]) + '>' + return tp + + +@@ -745,6 +745,19 @@ class FuncVariant(object): + self.args[argno].py_outputarg = True + self.py_outlist = outlist + ++def prefix_ambiguous_ios(val: str): ++ cnames = ["Rect", "Point", "Size"] ++ r = val ++ if val in cnames or val.startswith("Ptr<"): ++ r = f"cv::{r}" ++ for cn in cnames: ++ r = r.replace(f"<{cn}>", f"") ++ if r.startswith(f"{cn}("): ++ r = f"cv::{r}" ++ # also qualify any bare Ptr< nested inside other templates ++ # (e.g. std::vector>) which the leading check above misses ++ r = re.sub(r"(?".format(selfinfo.cname), +- pname=(selfinfo.cname + '*') if selfinfo.issimple else "Ptr<{}>".format(selfinfo.cname), ++ cname=selfinfo.cname if selfinfo.issimple else "cv::Ptr<{}>".format(selfinfo.cname), ++ pname=(selfinfo.cname + '*') if selfinfo.issimple else "cv::Ptr<{}>".format(selfinfo.cname), + cvt='' if selfinfo.issimple else '*' + ) + fullname = selfinfo.wname + "." + fullname +@@ -918,7 +931,7 @@ class FuncInfo(object): + else: + if tp in all_classes: + tp_classinfo = all_classes[tp] +- cname_of_value = tp_classinfo.cname if tp_classinfo.issimple else "Ptr<{}>".format(tp_classinfo.cname) ++ cname_of_value = tp_classinfo.cname if tp_classinfo.issimple else "cv::Ptr<{}>".format(tp_classinfo.cname) + arg_type_info = ArgTypeInfo(cname_of_value, FormatStrings.object, defval0, True) + assert not (a.is_smart_ptr and tp_classinfo.issimple), "Can't pass 'simple' type as Ptr<>" + if not a.is_smart_ptr and not tp_classinfo.issimple: +@@ -968,9 +981,9 @@ class FuncInfo(object): + if a.outputarg and not a.inputarg: + defval = "" + if defval: +- code_decl += " %s %s=%s;\n" % (arg_type_info.atype, a.name, defval) ++ code_decl += " %s %s=%s;\n" % (prefix_ambiguous_ios(arg_type_info.atype), a.name, prefix_ambiguous_ios(defval)) + else: +- code_decl += " %s %s;\n" % (arg_type_info.atype, a.name) ++ code_decl += " %s %s;\n" % (prefix_ambiguous_ios(arg_type_info.atype), a.name) + + if not code_args.endswith("("): + code_args += ", " +@@ -998,7 +1011,7 @@ class FuncInfo(object): + code_prelude = "" + code_fcall = "" + if v.rettype: +- code_decl += " " + v.rettype + " retval;\n" ++ code_decl += " " + prefix_ambiguous_ios(v.rettype) + " retval;\n" + code_fcall += "retval = " + if not v.isphantom and ismethod and not self.is_static: + code_fcall += "_self_->" + self.cname +@@ -1431,11 +1444,11 @@ class PythonWrapperGenerator(object): + self.code_types.write(gen_template_map_type_cvt.substitute(name=classinfo.name, cname=classinfo.cname)) + else: + mappable_code = "\n".join([ +- gen_template_mappable.substitute(cname=classinfo.cname, mappable=mappable) ++ gen_template_mappable.substitute(cname=classinfo.cname, mappable=prefix_ambiguous_ios(mappable)) + for mappable in classinfo.mappables]) + code = gen_template_type_decl.substitute( + name=classinfo.name, +- cname=classinfo.cname if classinfo.issimple else "Ptr<{}>".format(classinfo.cname), ++ cname=classinfo.cname if classinfo.issimple else "cv::Ptr<{}>".format(classinfo.cname), + mappable_code=mappable_code + ) + self.code_types.write(code) +diff --git a/opencv/modules/python/src2/pycompat.hpp b/opencv/modules/python/src2/pycompat.hpp +index c936f5e..b5a89c0 100644 +--- a/opencv/modules/python/src2/pycompat.hpp ++++ b/opencv/modules/python/src2/pycompat.hpp +@@ -147,7 +147,7 @@ bool pyopencv_to(PyObject* dst, TYPE& src, const ArgInfo& info) + { \ + if (!dst || dst == Py_None) \ + return true; \ +- Ptr ptr; \ ++ cv::Ptr ptr; \ + \ + if (!pyopencv_to(dst, ptr, info)) return false; \ + src = *ptr; \ +@@ -158,7 +158,7 @@ bool pyopencv_to(PyObject* dst, TYPE& src, const ArgInfo& info) + template<> \ + PyObject* pyopencv_from(const TYPE& src) \ + { \ +- Ptr ptr(new TYPE()); \ ++ cv::Ptr ptr(new TYPE()); \ + \ + *ptr = src; \ + return pyopencv_from(ptr); \ +@@ -170,7 +170,7 @@ bool pyopencv_to(PyObject* dst, TYPE*& src, const ArgInfo& info) + { \ + if (!dst || dst == Py_None) \ + return true; \ +- Ptr ptr; \ ++ cv::Ptr ptr; \ + \ + if (!pyopencv_to(dst, ptr, info)) return false; \ + src = ptr; \ +@@ -180,7 +180,7 @@ bool pyopencv_to(PyObject* dst, TYPE*& src, const ArgInfo& info) + #define CV_PY_FROM_CLASS_PTR(TYPE) \ + static PyObject* pyopencv_from(TYPE*& src) \ + { \ +- return pyopencv_from(Ptr(src)); \ ++ return pyopencv_from(cv::Ptr(src)); \ + } + + #define CV_PY_TO_ENUM(TYPE) \ +diff --git a/opencv/modules/videoio/misc/python/pyopencv_videoio.hpp b/opencv/modules/videoio/misc/python/pyopencv_videoio.hpp +index a4bc8e5..b86efd8 100644 +--- a/opencv/modules/videoio/misc/python/pyopencv_videoio.hpp ++++ b/opencv/modules/videoio/misc/python/pyopencv_videoio.hpp +@@ -23,7 +23,7 @@ bool pyopencv_to(PyObject *o, std::vector& apis, const Arg + + template<> bool pyopencv_to(PyObject* obj, cv::VideoCapture& stream, const ArgInfo& info) + { +- Ptr * obj_getp = nullptr; ++ cv::Ptr * obj_getp = nullptr; + if (!pyopencv_VideoCapture_getp(obj, obj_getp)) + return (failmsgp("Incorrect type of self (must be 'VideoCapture' or its derivative)") != nullptr); + +@@ -117,7 +117,7 @@ private: + }; + + template<> +-bool pyopencv_to(PyObject* obj, Ptr& p, const ArgInfo&) ++bool pyopencv_to(PyObject* obj, cv::Ptr& p, const ArgInfo&) + { + if (!obj) + return false; +@@ -147,7 +147,7 @@ static PyObject* pycvVideoEnter(PyObject* self, PyObject* /*args*/, PyObject* /* + } + + static PyObject* pycvVideoCaptureExit(PyObject* self, PyObject* /*args*/, PyObject* /*kw*/) { +- Ptr* obj_getp = nullptr; ++ cv::Ptr* obj_getp = nullptr; + if (pyopencv_VideoCapture_getp(self, obj_getp) && obj_getp && *obj_getp) { + (*obj_getp)->release(); + } +@@ -155,7 +155,7 @@ static PyObject* pycvVideoCaptureExit(PyObject* self, PyObject* /*args*/, PyObje + } + + static PyObject* pycvVideoWriterExit(PyObject* self, PyObject* /*args*/, PyObject* /*kw*/) { +- Ptr* obj_getp = nullptr; ++ cv::Ptr* obj_getp = nullptr; + if (pyopencv_VideoWriter_getp(self, obj_getp) && obj_getp && *obj_getp) { + (*obj_getp)->release(); + } + +diff --git a/opencv/modules/python/package/cv2/__init__.py b/opencv/modules/python/package/cv2/__init__.py +--- a/opencv/modules/python/package/cv2/__init__.py ++++ b/opencv/modules/python/package/cv2/__init__.py +@@ -75,6 +75,52 @@ def bootstrap(): + print(sys.path) + raise ImportError('ERROR: recursion is detected during loading of "cv2" binary extensions. Check OpenCV installation.') + sys.OpenCV_LOADER = True ++ # --- mobile-forge (Flet 0.86) fast-path ------------------------------- ++ # Under Flet 0.86 the native cv2 extension is relocated out of the wheel: ++ # Android -> jniLibs (a cv2/cv2.soref marker, origin is a real .so); iOS -> ++ # a framework (a cv2/cv2.fwork marker whose text is the app-relative path to ++ # the framework binary). config.py is also compiled to config.pyc, so the ++ # stock loader (which exec()s config.py and re-imports as top-level "cv2") ++ # recurses/fails. Resolve the relocated native and load it under the required ++ # top-level name "cv2" (OpenCV's compiled bindings hard-require that name). ++ try: ++ import importlib.util as _ilu ++ import importlib.machinery as _ilm ++ _sub_spec = _ilu.find_spec(__name__ + ".cv2") ++ except Exception: ++ _sub_spec = None ++ _origin = getattr(_sub_spec, "origin", None) if _sub_spec is not None else None ++ if _origin and _origin.endswith(".fwork"): ++ # iOS: the .fwork holds the app-relative framework-binary path; resolve it ++ # exactly as CPython's AppleFrameworkLoader does. ++ try: ++ with open(_origin, "rb") as _fwf: ++ _rel = _fwf.read().decode("utf-8").strip() ++ _origin = os.path.join(os.path.dirname(sys.executable), _rel) ++ except Exception: ++ _origin = None ++ if _origin: ++ _dbg = hasattr(sys, "OpenCV_LOADER_DEBUG") ++ _loader = _ilm.ExtensionFileLoader("cv2", _origin) ++ _spec = _ilu.spec_from_loader("cv2", _loader) ++ _native = _ilu.module_from_spec(_spec) ++ _loader.exec_module(_native) ++ _g = globals() ++ setattr(sys.modules[__name__], "_native", _native) ++ for _k, _v in _native.__dict__.items(): ++ if _k not in ("__file__", "__loader__", "__spec__", "__name__", "__package__") and _k not in _g: ++ _g[_k] = _v ++ try: ++ del sys.OpenCV_LOADER ++ except Exception: ++ pass ++ try: ++ for _s in __collect_extra_submodules(_dbg): ++ __load_extra_py_code_for_module("cv2", _s, _dbg) ++ except Exception: ++ pass ++ return ++ # --- end mobile-forge fast-path --------------------------------------- + + DEBUG = False + if hasattr(sys, 'OpenCV_LOADER_DEBUG'): diff --git a/recipes/opencv-contrib-python/tests/test_opencv_contrib_python.py b/recipes/opencv-contrib-python/tests/test_opencv_contrib_python.py new file mode 100644 index 00000000..62824cef --- /dev/null +++ b/recipes/opencv-contrib-python/tests/test_opencv_contrib_python.py @@ -0,0 +1,60 @@ +def test_resolves_contrib_pin(): + """The distribution must be installed under the name + `opencv-contrib-python` so pip pins like 'opencv-contrib-python>=4.9' + resolve to this wheel. importlib.metadata.version() raises + PackageNotFoundError if nothing is installed under that name, so a clean + lookup is the proof — the version value is irrelevant to the claim.""" + from importlib.metadata import version + + assert version("opencv-contrib-python") + + +def test_image_ops_roundtrip(): + """Real image pipeline through the native core (imgcodecs + imgproc): + encode a synthetic image to PNG, decode it back, convert to gray, and + run Canny edge detection — exercises the main-module C++ core.""" + import cv2 + import numpy as np + + img = np.zeros((16, 16, 3), dtype=np.uint8) + for y in range(16): + for x in range(16): + img[y, x] = (y * 16, x * 16, 128) + + ok, buf = cv2.imencode(".png", img) + assert ok and buf.nbytes > 0 + decoded = cv2.imdecode(buf, cv2.IMREAD_COLOR) + assert decoded is not None and decoded.shape == img.shape + + gray = cv2.cvtColor(decoded, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray, 50, 150) + assert edges.shape == (16, 16) + + +def test_dnn_module_present(): + """The dnn module (ONNX inference) rides along here too — it's the + latent inference runtime story, shared with the main opencv wheels.""" + import cv2 + + assert hasattr(cv2, "dnn") + assert callable(cv2.dnn.blobFromImage) + + +def test_contrib_module_works(): + """The reason this wheel exists over plain opencv-python: a contrib-only + module must actually load and run. img_hash is a stable, dependency-free + contrib module — a perceptual hash of identical images matches, and a + different image hashes differently.""" + import cv2 + import numpy as np + + a = np.zeros((32, 32, 3), dtype=np.uint8) + b = a.copy() + b[:16] = 255 # visibly different top half + + ha = cv2.img_hash.averageHash(a) + ha_again = cv2.img_hash.averageHash(a.copy()) + hb = cv2.img_hash.averageHash(b) + + assert np.array_equal(ha, ha_again) + assert not np.array_equal(ha, hb) From e57768bda90caeffc33afff78643ef2033415c26 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 17 Jul 2026 16:43:39 +0200 Subject: [PATCH 2/3] opencv-contrib-python: fix hunk-count in patch preamble [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cosmetic: '14 hunks' -> '14 files (38 hunks)'. Text-before-first-diff, no build impact — the running CI is unaffected. --- recipes/opencv-contrib-python/patches/mobile.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/opencv-contrib-python/patches/mobile.patch b/recipes/opencv-contrib-python/patches/mobile.patch index 5669b583..518cb64d 100644 --- a/recipes/opencv-contrib-python/patches/mobile.patch +++ b/recipes/opencv-contrib-python/patches/mobile.patch @@ -3,8 +3,8 @@ Make opencv-contrib-python 5.0 cross-compile AND load on Flet's mobile targets. This is the contrib twin of the opencv-python recipe. The contrib sdist ships the identical `opencv/` source tree (it only ADDS `opencv_contrib/` and points the build's extra-modules path at it), so this patch is byte-for-byte the -opencv-python 5.0 mobile.patch and touches only `opencv/` files — the 14 hunks -apply unchanged. The generated bindings for the contrib modules are handled by +opencv-python 5.0 mobile.patch and touches only `opencv/` files — all 14 files +(38 hunks) apply unchanged. The generated bindings for the contrib modules are handled by the same gen2.py fix (note 1). One contrib-only iOS wrinkle is NOT patched here but gated in meta.yaml instead: the rgbd module's hand-written `pyopencv_linemod.hpp` uses bare `std::vector>` and would hit the Apple From 8bf331db9918b82dcb9e1a2af659bfbbcbf70ceb Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 17 Jul 2026 17:46:49 +0200 Subject: [PATCH 3/3] =?UTF-8?q?opencv-contrib-python:=20fix=20iOS=20build?= =?UTF-8?q?=20=E2=80=94=20move=20rgbd=20comment=20out=20of=20the=20CMAKE?= =?UTF-8?q?=5FARGS=20scalar=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 3 iOS legs failed with 'ValueError: No closing quotation' (android green). Cause: the explanatory '#' comment I added lived INSIDE the iOS 'CMAKE_ARGS: >-' folded scalar. YAML only treats '#' as a comment OUTSIDE scalar values, so the prose (with apostrophes/backticks: MacTypes.h's, `char* Ptr;`) folded into the CMAKE_ARGS string and forge's shlex.split choked on the unbalanced quotes. Android has a separate scalar with no comment, hence green. Moved the rationale to a real YAML comment above the scalar. Verified: the rendered CMAKE_ARGS now shlex-splits cleanly for all three SDKs, no '#' in the value, rgbd gate present on iOS only. --- recipes/opencv-contrib-python/meta.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/recipes/opencv-contrib-python/meta.yaml b/recipes/opencv-contrib-python/meta.yaml index a301c030..940dcc67 100644 --- a/recipes/opencv-contrib-python/meta.yaml +++ b/recipes/opencv-contrib-python/meta.yaml @@ -29,6 +29,12 @@ build: -DPYTHON3_LIBRARIES={HOST_PYTHON_HOME}/lib/libpython{py_version_short}.so -DPYTHON3_NUMPY_INCLUDE_DIRS={platlib}/numpy/_core/include # {% else %} + # iOS only: BUILD_opencv_rgbd=OFF below gates out the rgbd module, whose + # hand-written pyopencv_linemod.hpp uses a bare Ptr<> that clashes with + # Apple MacTypes.h under `using namespace cv`. Android has no MacTypes.h so + # it keeps rgbd. Full rationale in patches/mobile.patch preamble. + # (This comment MUST stay OUTSIDE the >- scalar below — a `#` inside a YAML + # folded scalar is literal text, and its quotes break shlex-split of CMAKE_ARGS.) CMAKE_ARGS: >- -DAPPLE_FRAMEWORK=ON -DCMAKE_SYSTEM_NAME=iOS @@ -44,13 +50,6 @@ build: -DENABLE_TESTING=OFF -DBUILD_EXAMPLES=OFF -DWITH_OPENCL=OFF - # contrib-only, iOS-only: the rgbd module ships a HAND-WRITTEN binding - # helper (pyopencv_linemod.hpp) with bare `std::vector>`, which - # clashes with Apple 's global `typedef char* Ptr;` under - # `using namespace cv` (the same MacTypes clash the mobile.patch fixes for - # core/dnn/videoio; gen2.py's generated bindings are fixed generically, but - # this hand-written helper isn't). Android has no MacTypes.h so it keeps - # rgbd. To restore rgbd on iOS instead, patch those 4 lines to `cv::Ptr<...>`. -DBUILD_opencv_rgbd=OFF -DOPENCV_FORCE_PYTHON_LIBS=ON -DPYTHON3_INCLUDE_PATH={HOST_PYTHON_HOME}/include/python{py_version_short}