diff --git a/.circleci/config.yml b/.circleci/config.yml index 6650ee1ce..7f2be7538 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,78 +111,13 @@ jobs: - store_artifacts: path: /root/repo/packages/build-logs - build-packages-no-numpy-dependents: - <<: *defaults - resource_class: large - - steps: - - checkout - - - attach_workspace: - at: . - - - restore_cache: - keys: - - -pkg1-v20220303-{{ checksum "Makefile.envs" }} - - -pkg1-v20220303 - - - run: - name: build packages - no_output_timeout: 30m - command: | - source pyodide_env.sh - - # Set mtime for EM_CONFIG to avoid ccache cache misses - touch -m -d '1 Jan 2021 12:00' emsdk/emsdk/.emscripten - - ccache -z - PYODIDE_PACKAGES='*, no-numpy-dependents' make -C packages - ccache -s - environment: - PYODIDE_JOBS: 5 - - - run: - name: check-size - command: ls -lh dist/ - - - save_cache: - paths: - - /root/.ccache - key: -pkg1-v20220303-{{ checksum "Makefile.envs" }} - - - persist_to_workspace: - root: . - paths: - - ./packages - - ./dist - - - run: - name: Zip build directory - command: | - tar cjf pyodide-build.tar.gz dist - tar cjf build-logs.tar.gz packages/build-logs - - - persist_to_workspace: - root: . - paths: - - . - - - store_artifacts: - path: /root/repo/dist/ - - - store_artifacts: - path: /root/repo/pyodide-build.tar.gz - - - store_artifacts: - path: /root/repo/packages/build-logs - - - store_artifacts: - path: /root/repo/build-logs.tar.gz - build-packages: + parameters: + packages: + description: The packages to be built. + type: string <<: *defaults resource_class: large - steps: - checkout @@ -196,7 +131,7 @@ jobs: - run: name: build packages - no_output_timeout: 30m + no_output_timeout: 60m command: | source pyodide_env.sh @@ -204,7 +139,7 @@ jobs: touch -m -d '1 Jan 2021 12:00' emsdk/emsdk/.emscripten ccache -z - PYODIDE_PACKAGES='*' make -C packages + PYODIDE_PACKAGES='<< parameters.packages >>' make -C packages ccache -s environment: PYODIDE_JOBS: 5 @@ -221,24 +156,12 @@ jobs: - run: name: Zip build directory command: | - tar cjf pyodide-build.tar.gz dist + tar --exclude="node_modules" -cjf pyodide-build.tar.gz dist tar cjf build-logs.tar.gz packages/build-logs - - persist_to_workspace: - root: . - paths: - - ./packages/.artifacts - - ./dist - - - store_artifacts: - path: /root/repo/dist/ - - store_artifacts: path: /root/repo/pyodide-build.tar.gz - - store_artifacts: - path: /root/repo/packages/build-logs - - store_artifacts: path: /root/repo/build-logs.tar.gz @@ -421,19 +344,68 @@ workflows: tags: only: /.*/ - - build-packages-no-numpy-dependents: + - build-packages: + name: build-packages-no-numpy-dependents + packages: "*,no-numpy-dependents" requires: - build-core filters: tags: only: /.*/ + post-steps: + - persist_to_workspace: + root: . + paths: + - ./packages + - ./dist - build-packages: + name: build-packages-opencv-python + packages: opencv-python requires: - build-packages-no-numpy-dependents filters: tags: only: /.*/ + post-steps: + - persist_to_workspace: + root: . + paths: + - ./packages/opencv-python/build + - ./packages/opencv-python/dist + - ./packages/build-logs/opencv* + - ./dist/opencv* + + - build-packages: + name: build-packages-numpy-dependents + packages: "*,!opencv-python" + requires: + - build-packages-no-numpy-dependents + filters: + tags: + only: /.*/ + post-steps: + - persist_to_workspace: + root: . + paths: + - ./packages + - ./dist + + - build-packages: + name: build-packages + packages: "*" + requires: + - build-packages-numpy-dependents + - build-packages-opencv-python + filters: + tags: + only: /.*/ + post-steps: + - persist_to_workspace: + root: . + paths: + - ./packages/.artifacts + - ./dist - test-main: name: test-core-chrome diff --git a/conftest.py b/conftest.py index 28cd13850..684cc17ee 100644 --- a/conftest.py +++ b/conftest.py @@ -385,12 +385,16 @@ class NodeWrapper(SeleniumWrapper): browser = "node" def init_node(self): - self.p = pexpect.spawn( - f"node --expose-gc --experimental-wasm-bigint ./src/test-js/node_test_driver.js {self.base_url}", - timeout=60, - ) + self.p = pexpect.spawn("/bin/bash", timeout=60) self.p.setecho(False) self.p.delaybeforesend = None + # disable canonical input processing mode to allow sending longer lines + # See: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn.send + self.p.sendline("stty -icanon") + self.p.sendline( + f"node --expose-gc --experimental-wasm-bigint ./src/test-js/node_test_driver.js {self.base_url}", + ) + try: self.p.expect_exact("READY!!") except pexpect.exceptions.EOF: diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 4660502de..0f94b3327 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -26,6 +26,8 @@ substitutions: `pyodide.runPython(code, { globals : some_dict})`; {pr}`2391` +- New packages: opencv-python v4.5.5.64 {pr}`2305`, ffmpeg {pr}`2305`, libwebp {pr}`2305` + ## Version 0.20.0 [See the release notes for a summary.](https://blog.pyodide.org/posts/0.20-release/) diff --git a/packages/ffmpeg/meta.yaml b/packages/ffmpeg/meta.yaml new file mode 100644 index 000000000..6ebe4f23d --- /dev/null +++ b/packages/ffmpeg/meta.yaml @@ -0,0 +1,25 @@ +package: + name: ffmpeg + version: "4.4.1" + +source: + url: https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n4.4.1.tar.gz + sha256: 82b43cc67296bcd01a59ae6b327cdb50121d3a9e35f41a30de1edd71bb4a6666 + extract_dir: FFmpeg-n4.4.1 +build: + library: true + script: | + emconfigure ./configure \ + --extra-cflags="-fPIC" \ + --disable-x86asm \ + --disable-inline-asm \ + --disable-doc \ + --disable-stripping \ + --disable-programs \ + --disable-pthreads \ + --nm="$PYODIDE_ROOT/emsdk/emsdk/upstream/bin/llvm-nm -g" \ + --ar=emar --cc=emcc --cxx=em++ --objcc=emcc --dep-cc=emcc --ranlib=emranlib \ + --prefix=./lib + + emmake make -j${PYODIDE_JOBS:-3} + emmake make install diff --git a/packages/libwebp/meta.yaml b/packages/libwebp/meta.yaml new file mode 100644 index 000000000..9c52897dd --- /dev/null +++ b/packages/libwebp/meta.yaml @@ -0,0 +1,14 @@ +package: + name: libwebp + version: 1.2.2 + +source: + url: https://github.com/webmproject/libwebp/archive/refs/tags/v1.2.2.tar.gz + sha256: 51e9297aadb7d9eb99129fe0050f53a11fcce38a0848fb2b0389e385ad93695e + +build: + library: true + script: | + mkdir build && cd build && emcmake cmake -DCMAKE_C_FLAGS="-fPIC" -DCMAKE_INSTALL_PREFIX=./lib ../ + emmake make -j ${PYODIDE_JOBS:-3} + emmake make install diff --git a/packages/opencv-python/cmake/Config.cmake b/packages/opencv-python/cmake/Config.cmake new file mode 100644 index 000000000..97146e6e9 --- /dev/null +++ b/packages/opencv-python/cmake/Config.cmake @@ -0,0 +1,31 @@ +set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS TRUE) +set(OPENCV_PYTHON_SKIP_LINKER_EXCLUDE_LIBS TRUE) +set(CMAKE_SKIP_COMPATIBILITY_TESTS 1) +set(CMAKE_SIZEOF_CHAR 1) +set(CMAKE_SIZEOF_UNSIGNED_SHORT 2) +set(CMAKE_SIZEOF_SHORT 2) +set(CMAKE_SIZEOF_INT 4) +set(CMAKE_SIZEOF_UNSIGNED_LONG 4) +set(CMAKE_SIZEOF_UNSIGNED_INT 4) +set(CMAKE_SIZEOF_LONG 4) +set(CMAKE_SIZEOF_VOID_P 4) +set(CMAKE_SIZEOF_FLOAT 4) +set(CMAKE_SIZEOF_DOUBLE 8) +set(CMAKE_C_SIZEOF_DATA_PTR 4) +set(CMAKE_CXX_SIZEOF_DATA_PTR 4) +set(CMAKE_HAVE_LIMITS_H 1) +set(CMAKE_HAVE_UNISTD_H 1) +set(CMAKE_HAVE_PTHREAD_H 1) +set(CMAKE_HAVE_SYS_PRCTL_H 1) +set(CMAKE_WORDS_BIGENDIAN 0) +set(CMAKE_DL_LIBS) +set(CMAKE_RANLIB echo) + +# Force filesystem support, it is disabled in Emscripten platform (needs fix) +# https://github.com/opencv/opencv/blob/17234f82d025e3bbfbf611089637e5aa2038e7b8/modules/core/include/opencv2/core/utils/filesystem.private.hpp#L10 +add_definitions(-DOPENCV_HAVE_FILESYSTEM_SUPPORT=1) + +if ("${EMSCRIPTEN_ROOT_PATH}" STREQUAL "") + set(EMSCRIPTEN_ROOT_PATH "$ENV{EMSCRIPTEN}") +endif() +list(APPEND CMAKE_MODULE_PATH "${EMSCRIPTEN_ROOT_PATH}/cmake/Modules") diff --git a/packages/opencv-python/cmake/OpenCVFindLibsGrfmt.cmake b/packages/opencv-python/cmake/OpenCVFindLibsGrfmt.cmake new file mode 100644 index 000000000..f80e9605d --- /dev/null +++ b/packages/opencv-python/cmake/OpenCVFindLibsGrfmt.cmake @@ -0,0 +1,218 @@ +# ---------------------------------------------------------------------------- +# Detect 3rd-party image IO libraries +# ---------------------------------------------------------------------------- + +# We want to use emscripten-ported version of ZLIB, LIBJPEG, LIBPNG. +# However, OpenCV tries to find them in system paths. +# Let's deceive OpenCV and pretend we have them. + +set(HAVE_JPEG YES) +set(HAVE_PNG YES) +set(ZLIB_FOUND YES) + +# --- libtiff (optional, should be searched after zlib and libjpeg) --- +if(WITH_TIFF) + if(BUILD_TIFF) + ocv_clear_vars(TIFF_FOUND) + else() + ocv_clear_internal_cache_vars(TIFF_LIBRARY TIFF_INCLUDE_DIR) + include(FindTIFF) + if(TIFF_FOUND) + ocv_parse_header("${TIFF_INCLUDE_DIR}/tiff.h" TIFF_VERSION_LINES TIFF_VERSION_CLASSIC TIFF_VERSION_BIG TIFF_VERSION TIFF_BIGTIFF_VERSION) + endif() + endif() + + if(NOT TIFF_FOUND) + ocv_clear_vars(TIFF_LIBRARY TIFF_LIBRARIES TIFF_INCLUDE_DIR) + + set(TIFF_LIBRARY libtiff CACHE INTERNAL "") + set(TIFF_LIBRARIES ${TIFF_LIBRARY}) + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/libtiff") + set(TIFF_INCLUDE_DIR "${${TIFF_LIBRARY}_SOURCE_DIR}" "${${TIFF_LIBRARY}_BINARY_DIR}" CACHE INTERNAL "") + ocv_parse_header("${${TIFF_LIBRARY}_SOURCE_DIR}/tiff.h" TIFF_VERSION_LINES TIFF_VERSION_CLASSIC TIFF_VERSION_BIG TIFF_VERSION TIFF_BIGTIFF_VERSION) + endif() + + if(TIFF_VERSION_CLASSIC AND NOT TIFF_VERSION) + set(TIFF_VERSION ${TIFF_VERSION_CLASSIC}) + endif() + + if(TIFF_BIGTIFF_VERSION AND NOT TIFF_VERSION_BIG) + set(TIFF_VERSION_BIG ${TIFF_BIGTIFF_VERSION}) + endif() + + if(NOT TIFF_VERSION_STRING AND TIFF_INCLUDE_DIR) + list(GET TIFF_INCLUDE_DIR 0 _TIFF_INCLUDE_DIR) + if(EXISTS "${_TIFF_INCLUDE_DIR}/tiffvers.h") + file(STRINGS "${_TIFF_INCLUDE_DIR}/tiffvers.h" tiff_version_str REGEX "^#define[\t ]+TIFFLIB_VERSION_STR[\t ]+\"LIBTIFF, Version .*") + string(REGEX REPLACE "^#define[\t ]+TIFFLIB_VERSION_STR[\t ]+\"LIBTIFF, Version +([^ \\n]*).*" "\\1" TIFF_VERSION_STRING "${tiff_version_str}") + unset(tiff_version_str) + endif() + unset(_TIFF_INCLUDE_DIR) + endif() + + set(HAVE_TIFF YES) +endif() + +# --- libwebp (optional) --- + +if(WITH_WEBP) + if(BUILD_WEBP) + ocv_clear_vars(WEBP_FOUND WEBP_LIBRARY WEBP_LIBRARIES WEBP_INCLUDE_DIR) + else() + ocv_clear_internal_cache_vars(WEBP_LIBRARY WEBP_INCLUDE_DIR) + include(cmake/OpenCVFindWebP.cmake) + if(WEBP_FOUND) + set(HAVE_WEBP 1) + endif() + endif() +endif() + +# --- Add libwebp to 3rdparty/libwebp and compile it if not available --- +if(WITH_WEBP AND NOT WEBP_FOUND + AND (NOT ANDROID OR HAVE_CPUFEATURES) +) + ocv_clear_vars(WEBP_LIBRARY WEBP_INCLUDE_DIR) + set(WEBP_LIBRARY libwebp CACHE INTERNAL "") + set(WEBP_LIBRARIES ${WEBP_LIBRARY}) + + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/libwebp") + set(WEBP_INCLUDE_DIR "${${WEBP_LIBRARY}_SOURCE_DIR}/src" CACHE INTERNAL "") + set(HAVE_WEBP 1) +endif() + +if(NOT WEBP_VERSION AND WEBP_INCLUDE_DIR) + ocv_clear_vars(ENC_MAJ_VERSION ENC_MIN_VERSION ENC_REV_VERSION) + if(EXISTS "${WEBP_INCLUDE_DIR}/enc/vp8enci.h") + ocv_parse_header("${WEBP_INCLUDE_DIR}/enc/vp8enci.h" WEBP_VERSION_LINES ENC_MAJ_VERSION ENC_MIN_VERSION ENC_REV_VERSION) + set(WEBP_VERSION "${ENC_MAJ_VERSION}.${ENC_MIN_VERSION}.${ENC_REV_VERSION}") + elseif(EXISTS "${WEBP_INCLUDE_DIR}/webp/encode.h") + file(STRINGS "${WEBP_INCLUDE_DIR}/webp/encode.h" WEBP_ENCODER_ABI_VERSION REGEX "#define[ \t]+WEBP_ENCODER_ABI_VERSION[ \t]+([x0-9a-f]+)" ) + if(WEBP_ENCODER_ABI_VERSION MATCHES "#define[ \t]+WEBP_ENCODER_ABI_VERSION[ \t]+([x0-9a-f]+)") + set(WEBP_ENCODER_ABI_VERSION "${CMAKE_MATCH_1}") + set(WEBP_VERSION "encoder: ${WEBP_ENCODER_ABI_VERSION}") + else() + unset(WEBP_ENCODER_ABI_VERSION) + endif() + endif() +endif() + +# --- libopenjp2 (optional, check before libjasper) --- +if(WITH_OPENJPEG) + if(BUILD_OPENJPEG) + ocv_clear_vars(OpenJPEG_FOUND) + else() + find_package(OpenJPEG QUIET) + endif() + + if(NOT OpenJPEG_FOUND OR OPENJPEG_MAJOR_VERSION LESS 2) + ocv_clear_vars(OPENJPEG_MAJOR_VERSION OPENJPEG_MINOR_VERSION OPENJPEG_BUILD_VERSION OPENJPEG_LIBRARIES OPENJPEG_INCLUDE_DIRS) + message(STATUS "Could NOT find OpenJPEG (minimal suitable version: 2.0, " + "recommended version >= 2.3.1). OpenJPEG will be built from sources") + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/openjpeg") + if(OCV_CAN_BUILD_OPENJPEG) + set(HAVE_OPENJPEG YES) + message(STATUS "OpenJPEG libraries will be built from sources: ${OPENJPEG_LIBRARIES} " + "(version \"${OPENJPEG_VERSION}\")") + else() + set(HAVE_OPENJPEG NO) + message(STATUS "OpenJPEG libraries can't be built from sources. System requirements are not fulfilled.") + endif() + else() + set(HAVE_OPENJPEG YES) + message(STATUS "Found system OpenJPEG: ${OPENJPEG_LIBRARIES} " + "(found version \"${OPENJPEG_VERSION}\")") + endif() +endif() + +# --- libjasper (optional, should be searched after libjpeg) --- +if(WITH_JASPER AND NOT HAVE_OPENJPEG) + if(BUILD_JASPER) + ocv_clear_vars(JASPER_FOUND) + else() + include(FindJasper) + endif() + + if(NOT JASPER_FOUND) + ocv_clear_vars(JASPER_LIBRARY JASPER_LIBRARIES JASPER_INCLUDE_DIR) + + set(JASPER_LIBRARY libjasper CACHE INTERNAL "") + set(JASPER_LIBRARIES ${JASPER_LIBRARY}) + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/libjasper") + set(JASPER_INCLUDE_DIR "${${JASPER_LIBRARY}_SOURCE_DIR}" CACHE INTERNAL "") + endif() + + set(HAVE_JASPER YES) + + if(NOT JASPER_VERSION_STRING) + ocv_parse_header2(JASPER "${JASPER_INCLUDE_DIR}/jasper/jas_config.h" JAS_VERSION "") + endif() +endif() + +# --- OpenEXR (optional) --- +if(WITH_OPENEXR) + ocv_clear_vars(HAVE_OPENEXR) + if(NOT BUILD_OPENEXR) + ocv_clear_internal_cache_vars(OPENEXR_INCLUDE_PATHS OPENEXR_LIBRARIES OPENEXR_ILMIMF_LIBRARY OPENEXR_VERSION) + include("${OpenCV_SOURCE_DIR}/cmake/OpenCVFindOpenEXR.cmake") + endif() + + if(OPENEXR_FOUND) + set(HAVE_OPENEXR YES) + else() + ocv_clear_vars(OPENEXR_INCLUDE_PATHS OPENEXR_LIBRARIES OPENEXR_ILMIMF_LIBRARY OPENEXR_VERSION) + + set(OPENEXR_LIBRARIES IlmImf) + add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/openexr") + if(OPENEXR_VERSION) # check via TARGET doesn't work + set(BUILD_OPENEXR ON) + set(HAVE_OPENEXR YES) + set(BUILD_OPENEXR ON) + endif() + endif() +endif() + +# --- GDAL (optional) --- +if(WITH_GDAL) + find_package(GDAL QUIET) + + if(NOT GDAL_FOUND) + set(HAVE_GDAL NO) + ocv_clear_vars(GDAL_VERSION GDAL_LIBRARIES) + else() + set(HAVE_GDAL YES) + ocv_include_directories(${GDAL_INCLUDE_DIR}) + endif() +endif() + +if(WITH_GDCM) + find_package(GDCM QUIET) + if(NOT GDCM_FOUND) + set(HAVE_GDCM NO) + ocv_clear_vars(GDCM_VERSION GDCM_LIBRARIES) + else() + set(HAVE_GDCM YES) + # include(${GDCM_USE_FILE}) + set(GDCM_LIBRARIES gdcmMSFF) # GDCM does not set this variable for some reason + endif() +endif() + +if(WITH_IMGCODEC_HDR) + set(HAVE_IMGCODEC_HDR ON) +elseif(DEFINED WITH_IMGCODEC_HDR) + set(HAVE_IMGCODEC_HDR OFF) +endif() +if(WITH_IMGCODEC_SUNRASTER) + set(HAVE_IMGCODEC_SUNRASTER ON) +elseif(DEFINED WITH_IMGCODEC_SUNRASTER) + set(HAVE_IMGCODEC_SUNRASTER OFF) +endif() +if(WITH_IMGCODEC_PXM) + set(HAVE_IMGCODEC_PXM ON) +elseif(DEFINED WITH_IMGCODEC_PXM) + set(HAVE_IMGCODEC_PXM OFF) +endif() +if(WITH_IMGCODEC_PFM) + set(HAVE_IMGCODEC_PFM ON) +elseif(DEFINED WITH_IMGCODEC_PFM) + set(HAVE_IMGCODEC_PFM OFF) +endif() diff --git a/packages/opencv-python/cmake/build_args.sh b/packages/opencv-python/cmake/build_args.sh new file mode 100755 index 000000000..e77e220b8 --- /dev/null +++ b/packages/opencv-python/cmake/build_args.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +export CMAKE_ARGS=" \ +-DCMAKE_TOOLCHAIN_FILE=$PYODIDE_ROOT/packages/opencv-python/cmake/Config.cmake \ +-DPYTHON3_INCLUDE_PATH=$PYTHONINCLUDE \ +-DPYTHON3_LIBRARY=$PYTHONINCLUDE/../libpython$PYMAJOR.$PYMINOR.a \ +-DPYTHON3_VERSION_MAJOR=$PYMAJOR \ +-DPYTHON3_VERSION_MINOR=$PYMINOR \ +-DPYTHON3_NUMPY_INCLUDE_DIRS=$NUMPY_INCLUDE_DIR \ +\ +-DWITH_ADE=ON \ +-DWITH_JPEG=ON \ +-DWITH_PNG=ON \ +-DWITH_WEBP=ON \ +-DBUILD_WEBP=OFF \ +-DWEBP_INCLUDE_DIR=$LIBWEBP_ROOT/include \ +-DWEBP_LIBRARY=$LIBWEBP_ROOT/lib/libwebp.a \ +\ +-DBUILD_opencv_python3=ON \ +-DBUILD_opencv_world=ON \ +-DBUILD_opencv_imgcodecs=ON \ +-DBUILD_opencv_videoio=ON \ +-DBUILD_opencv_gapi=ON \ +-DBUILD_opencv_photo=ON \ +-DBUILD_opencv_stitching=ON \ +-DBUILD_opencv_highgui=ON \ +-DBUILD_opencv_features2d=ON \ +-DBUILD_opencv_flann=ON \ +-DBUILD_opencv_calib3d=ON \ +-DBUILD_opencv_dnn=ON \ +-DBUILD_opencv_ml=ON \ +-DBUILD_opencv_objdetect=ON \ +-DWITH_OPENCL=OFF \ +-DOPENCV_DNN_OPENCL=OFF \ +-DWITH_PROTOBUF=ON \ +-DWITH_FFMPEG=ON \ +\ +-DPYTHON3_EXECUTABLE=python \ +-DPYTHON3_LIMITED_API=ON \ +-DPYTHON_DEFAULT_EXECUTABLE=python \ +-DENABLE_PIC=FALSE \ +-DCMAKE_BUILD_TYPE=Release \ +-DCPU_BASELINE='' \ +-DCPU_DISPATCH='' \ +-DCV_TRACE=OFF \ +-DBUILD_SHARED_LIBS=OFF \ +-DWITH_1394=OFF \ +-DWITH_VTK=OFF \ +-DWITH_EIGEN=OFF \ +-DWITH_GSTREAMER=OFF \ +-DWITH_GTK=OFF \ +-DWITH_GTK_2_X=OFF \ +-DWITH_QT=OFF \ +-DWITH_IPP=OFF \ +-DWITH_JASPER=OFF \ +-DWITH_OPENJPEG=OFF \ +-DWITH_OPENEXR=OFF \ +-DWITH_OPENGL=OFF \ +-DWITH_OPENVX=OFF \ +-DWITH_OPENNI=OFF \ +-DWITH_OPENNI2=OFF \ +-DWITH_TBB=OFF \ +-DWITH_TIFF=OFF \ +-DWITH_V4L=OFF \ +-DWITH_OPENCL_SVM=OFF \ +-DWITH_OPENCLAMDFFT=OFF \ +-DWITH_OPENCLAMDBLAS=OFF \ +-DWITH_GPHOTO2=OFF \ +-DWITH_LAPACK=OFF \ +-DWITH_ITT=OFF \ +-DWITH_QUIRC=OFF \ +-DBUILD_ZLIB=OFF \ +-DBUILD_opencv_apps=OFF \ +-DBUILD_opencv_shape=OFF \ +-DBUILD_opencv_videostab=OFF \ +-DBUILD_opencv_superres=OFF \ +-DBUILD_opencv_java=OFF \ +-DBUILD_opencv_js=OFF \ +-DBUILD_opencv_python2=OFF \ +-DBUILD_EXAMPLES=OFF \ +-DBUILD_PACKAGE=OFF \ +-DBUILD_TESTS=OFF \ +-DBUILD_PERF_TESTS=OFF \ +-DBUILD_DOCS=OFF \ +-DWITH_PTHREADS_PF=OFF \ +-DCV_ENABLE_INTRINSICS=OFF \ +-DBUILD_WASM_INTRIN_TESTS=OFF \ +-DCMAKE_INSTALL_PREFIX=../cmake-install \ +-DCMAKE_VERBOSE_MAKEFILE=ON \ +" diff --git a/packages/opencv-python/cmake/detect_ffmpeg.cmake b/packages/opencv-python/cmake/detect_ffmpeg.cmake new file mode 100644 index 000000000..772ee62a0 --- /dev/null +++ b/packages/opencv-python/cmake/detect_ffmpeg.cmake @@ -0,0 +1,49 @@ +# --- FFMPEG --- + +set(HAVE_FFMPEG TRUE) +set(FFMPEG_FOUND TRUE) + +set(FFMPEG_ROOT_PATH "$ENV{FFMPEG_ROOT}") +set(FFMPEG_INCLUDE_DIRS "${FFMPEG_ROOT_PATH}/include") +set(FFMPEG_LIBRARIES + "${FFMPEG_ROOT_PATH}/lib/libavcodec.a" + "${FFMPEG_ROOT_PATH}/lib/libavformat.a" + "${FFMPEG_ROOT_PATH}/lib/libavutil.a" + "${FFMPEG_ROOT_PATH}/lib/libswscale.a" + "${FFMPEG_ROOT_PATH}/lib/libswresample.a" +) + +ocv_add_external_target(ffmpeg "${FFMPEG_INCLUDE_DIRS}" "${FFMPEG_LIBRARIES}" "HAVE_FFMPEG") + +set(__builtin_defines "") +set(__builtin_include_dirs "") +set(__builtin_libs "") +set(__plugin_defines "") +set(__plugin_include_dirs "") +set(__plugin_libs "") +if(HAVE_OPENCL) +set(__opencl_dirs "") +if(OPENCL_INCLUDE_DIRS) + set(__opencl_dirs "${OPENCL_INCLUDE_DIRS}") +elseif(OPENCL_INCLUDE_DIR) + set(__opencl_dirs "${OPENCL_INCLUDE_DIR}") +else() + set(__opencl_dirs "${OpenCV_SOURCE_DIR}/3rdparty/include/opencl/1.2") +endif() +# extra dependencies for buildin code (OpenCL dir is required for extensions like cl_d3d11.h) +# buildin HAVE_OPENCL is already defined through cvconfig.h +list(APPEND __builtin_include_dirs "${__opencl_dirs}") + +# extra dependencies for +list(APPEND __plugin_defines "HAVE_OPENCL") +list(APPEND __plugin_include_dirs "${__opencl_dirs}") +endif() + +# TODO: libva, d3d11 + +if(__builtin_include_dirs OR __builtin_include_defines OR __builtin_include_libs) +ocv_add_external_target(ffmpeg.builtin_deps "${__builtin_include_dirs}" "${__builtin_include_libs}" "${__builtin_defines}") +endif() +if(VIDEOIO_ENABLE_PLUGINS AND __plugin_include_dirs OR __plugin_include_defines OR __plugin_include_libs) +ocv_add_external_target(ffmpeg.plugin_deps "${__plugin_include_dirs}" "${__plugin_include_libs}" "${__plugin_defines}") +endif() diff --git a/packages/opencv-python/meta.yaml b/packages/opencv-python/meta.yaml new file mode 100644 index 000000000..d77617e84 --- /dev/null +++ b/packages/opencv-python/meta.yaml @@ -0,0 +1,65 @@ +package: + name: opencv-python + version: 4.5.5.64 +about: + home: https://github.com/skvark/opencv-python + PyPI: https://pypi.org/project/opencv-python + summary: Wrapper package for OpenCV python bindings. + license: MIT +source: + url: https://files.pythonhosted.org/packages/3c/61/ee4496192ed27f657532fdf0d814b05b9787e7fc5122ed3ca57282bae69c/opencv-python-4.5.5.64.tar.gz + sha256: f65de0446a330c3b773cd04ba10345d8ce1b15dcac3f49770204e37602d0b3f7 + extras: + - [cmake/OpenCVFindLibsGrfmt.cmake, opencv/cmake/OpenCVFindLibsGrfmt.cmake] + - [ + cmake/detect_ffmpeg.cmake, + opencv/modules/videoio/cmake/detect_ffmpeg.cmake, + ] + patches: + - patches/0001-Enable-file-system.patch + +requirements: + run: + - numpy + - ffmpeg + - libwebp +build: + cxxflags: | + -fPIC + -s USE_ZLIB=1 + -s USE_LIBJPEG=1 + -s USE_LIBPNG=1 + -s SIDE_MODULE=1 + ldflags: | + -ljpeg + -lz + -lpng + + # Note on CMAKE_ARGS: + # CMake args are adopted from OpenCV.js (https://github.com/opencv/opencv/blob/4.x/platforms/js/build_js.py) + # But we support more of modules than OpenCV.js. + # + # Note on CMAKE_TOOLCHAIN_FILE: + # We don't want to use toolchain file provided by Emscripten, + # because our build script hijack gcc, c++, ... and replace it with emcc, em++, ..., instead of calling them directly. + # + # List of OpenCV modules can be found at: https://docs.opencv.org/4.x/ + # Build configs can be found at: https://docs.opencv.org/4.x/db/d05/tutorial_config_reference.html + + script: | + pip install scikit-build + # TODO: remove this line after version update (https://github.com/opencv/opencv-python/issues/648) + sed -i "s/cmake_install_dir=cmake_install_reldir/_cmake_install_dir=cmake_install_reldir/" setup.py + + # export VERBOSE=1 + + export NUMPY_INCLUDE_DIR="$HOSTINSTALLDIR/lib/python$PYMAJOR.$PYMINOR/site-packages/numpy/core/include/" + export EMSCRIPTEN="$PYODIDE_ROOT/emsdk/emsdk/upstream/emscripten/" + export FFMPEG_ROOT="$PYODIDE_ROOT/packages/ffmpeg/build/ffmpeg-4.4.1/lib" + export LIBWEBP_ROOT="$PYODIDE_ROOT/packages/libwebp/build/libwebp-1.2.2/build/lib" + + source $PYODIDE_ROOT/packages/opencv-python/cmake/build_args.sh + +test: + imports: + - cv2 diff --git a/packages/opencv-python/patches/0001-Enable-file-system.patch b/packages/opencv-python/patches/0001-Enable-file-system.patch new file mode 100644 index 000000000..b9b2093b1 --- /dev/null +++ b/packages/opencv-python/patches/0001-Enable-file-system.patch @@ -0,0 +1,75 @@ +From c7e9f892204ce1e47774fe21790195e2cbd7f2b3 Mon Sep 17 00:00:00 2001 +From: ryanking13 +Date: Tue, 29 Mar 2022 01:58:40 +0000 +Subject: [PATCH] Enable file system + +--- + .../include/opencv2/core/utils/plugin_loader.private.hpp | 8 ++++---- + modules/core/src/utils/filesystem.cpp | 4 ++-- + 2 files changed, 6 insertions(+), 6 deletions(-) + +diff --git a/modules/core/include/opencv2/core/utils/plugin_loader.private.hpp b/modules/core/include/opencv2/core/utils/plugin_loader.private.hpp +index d6390fc74a..c089309443 100644 +--- a/opencv/modules/core/include/opencv2/core/utils/plugin_loader.private.hpp ++++ b/opencv/modules/core/include/opencv2/core/utils/plugin_loader.private.hpp +@@ -12,7 +12,7 @@ + + #if defined(_WIN32) + #include +-#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) ++#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) || defined(__EMSCRIPTEN__) + #include + #endif + +@@ -65,7 +65,7 @@ void* getSymbol_(LibHandle_t h, const char* symbolName) + { + #if defined(_WIN32) + return (void*)GetProcAddress(h, symbolName); +-#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) ++#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) || defined(__EMSCRIPTEN__) + return dlsym(h, symbolName); + #endif + } +@@ -79,7 +79,7 @@ LibHandle_t libraryLoad_(const FileSystemPath_t& filename) + # else + return LoadLibraryW(filename.c_str()); + #endif +-#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) ++#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) || defined(__EMSCRIPTEN__) + void* handle = dlopen(filename.c_str(), RTLD_NOW); + CV_LOG_IF_DEBUG(NULL, !handle, "dlopen() error: " << dlerror()); + return handle; +@@ -91,7 +91,7 @@ void libraryRelease_(LibHandle_t h) + { + #if defined(_WIN32) + FreeLibrary(h); +-#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) ++#elif defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__HAIKU__) || defined(__GLIBC__) || defined(__EMSCRIPTEN__) + dlclose(h); + #endif + } +diff --git a/modules/core/src/utils/filesystem.cpp b/modules/core/src/utils/filesystem.cpp +index 663ec311e4..3ef4408d2d 100644 +--- a/opencv/modules/core/src/utils/filesystem.cpp ++++ b/opencv/modules/core/src/utils/filesystem.cpp +@@ -34,7 +34,7 @@ + #include + #include + #include +-#elif defined __linux__ || defined __APPLE__ || defined __HAIKU__ || defined __FreeBSD__ ++#elif defined __linux__ || defined __APPLE__ || defined __HAIKU__ || defined __FreeBSD__ || defined __EMSCRIPTEN__ + #include + #include + #include +@@ -343,7 +343,7 @@ private: + Impl& operator=(const Impl&); // disabled + }; + +-#elif defined __linux__ || defined __APPLE__ || defined __HAIKU__ || defined __FreeBSD__ ++#elif defined __linux__ || defined __APPLE__ || defined __HAIKU__ || defined __FreeBSD__ || defined __EMSCRIPTEN__ + + struct FileLock::Impl + { +-- +2.35.1 + diff --git a/packages/opencv-python/reference-images/baboon.png b/packages/opencv-python/reference-images/baboon.png new file mode 100644 index 000000000..fb5d09e2c Binary files /dev/null and b/packages/opencv-python/reference-images/baboon.png differ diff --git a/packages/opencv-python/reference-images/baboon_canny.png b/packages/opencv-python/reference-images/baboon_canny.png new file mode 100644 index 000000000..70a97f598 Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_canny.png differ diff --git a/packages/opencv-python/reference-images/baboon_decolor_color_boost.png b/packages/opencv-python/reference-images/baboon_decolor_color_boost.png new file mode 100644 index 000000000..cefa9ccaa Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_decolor_color_boost.png differ diff --git a/packages/opencv-python/reference-images/baboon_decolor_grayscale.png b/packages/opencv-python/reference-images/baboon_decolor_grayscale.png new file mode 100644 index 000000000..44bd68da4 Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_decolor_grayscale.png differ diff --git a/packages/opencv-python/reference-images/baboon_kaze.png b/packages/opencv-python/reference-images/baboon_kaze.png new file mode 100644 index 000000000..ef185710a Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_kaze.png differ diff --git a/packages/opencv-python/reference-images/baboon_laplacian.png b/packages/opencv-python/reference-images/baboon_laplacian.png new file mode 100644 index 000000000..384b8cd32 Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_laplacian.png differ diff --git a/packages/opencv-python/reference-images/baboon_sobel.png b/packages/opencv-python/reference-images/baboon_sobel.png new file mode 100644 index 000000000..7d2ae610f Binary files /dev/null and b/packages/opencv-python/reference-images/baboon_sobel.png differ diff --git a/packages/opencv-python/reference-images/box.png b/packages/opencv-python/reference-images/box.png new file mode 100644 index 000000000..6f01082f7 Binary files /dev/null and b/packages/opencv-python/reference-images/box.png differ diff --git a/packages/opencv-python/reference-images/box_in_scene.png b/packages/opencv-python/reference-images/box_in_scene.png new file mode 100644 index 000000000..cff246a3f Binary files /dev/null and b/packages/opencv-python/reference-images/box_in_scene.png differ diff --git a/packages/opencv-python/reference-images/box_sift.png b/packages/opencv-python/reference-images/box_sift.png new file mode 100644 index 000000000..22ccc031f Binary files /dev/null and b/packages/opencv-python/reference-images/box_sift.png differ diff --git a/packages/opencv-python/reference-images/chessboard.png b/packages/opencv-python/reference-images/chessboard.png new file mode 100644 index 000000000..3a68ab484 Binary files /dev/null and b/packages/opencv-python/reference-images/chessboard.png differ diff --git a/packages/opencv-python/reference-images/chessboard_corners.png b/packages/opencv-python/reference-images/chessboard_corners.png new file mode 100644 index 000000000..81d6f9a68 Binary files /dev/null and b/packages/opencv-python/reference-images/chessboard_corners.png differ diff --git a/packages/opencv-python/reference-images/mnist.onnx b/packages/opencv-python/reference-images/mnist.onnx new file mode 100644 index 000000000..b0817f7a3 Binary files /dev/null and b/packages/opencv-python/reference-images/mnist.onnx differ diff --git a/packages/opencv-python/reference-images/mnist_2.png b/packages/opencv-python/reference-images/mnist_2.png new file mode 100644 index 000000000..e3b26cbda Binary files /dev/null and b/packages/opencv-python/reference-images/mnist_2.png differ diff --git a/packages/opencv-python/reference-images/monalisa.png b/packages/opencv-python/reference-images/monalisa.png new file mode 100644 index 000000000..0e276f833 Binary files /dev/null and b/packages/opencv-python/reference-images/monalisa.png differ diff --git a/packages/opencv-python/reference-images/monalisa_facedetect.png b/packages/opencv-python/reference-images/monalisa_facedetect.png new file mode 100644 index 000000000..3e149958d Binary files /dev/null and b/packages/opencv-python/reference-images/monalisa_facedetect.png differ diff --git a/packages/opencv-python/reference-images/mountain1.png b/packages/opencv-python/reference-images/mountain1.png new file mode 100644 index 000000000..96a89c8f9 Binary files /dev/null and b/packages/opencv-python/reference-images/mountain1.png differ diff --git a/packages/opencv-python/reference-images/mountain2.png b/packages/opencv-python/reference-images/mountain2.png new file mode 100644 index 000000000..c7e48cc7b Binary files /dev/null and b/packages/opencv-python/reference-images/mountain2.png differ diff --git a/packages/opencv-python/reference-images/pca.png b/packages/opencv-python/reference-images/pca.png new file mode 100644 index 000000000..b25660226 Binary files /dev/null and b/packages/opencv-python/reference-images/pca.png differ diff --git a/packages/opencv-python/reference-images/pca_result.png b/packages/opencv-python/reference-images/pca_result.png new file mode 100644 index 000000000..afa916af5 Binary files /dev/null and b/packages/opencv-python/reference-images/pca_result.png differ diff --git a/packages/opencv-python/reference-images/traffic.mp4 b/packages/opencv-python/reference-images/traffic.mp4 new file mode 100644 index 000000000..5968ccf11 Binary files /dev/null and b/packages/opencv-python/reference-images/traffic.mp4 differ diff --git a/packages/opencv-python/reference-images/traffic_optical_flow.png b/packages/opencv-python/reference-images/traffic_optical_flow.png new file mode 100644 index 000000000..4a99cbbf9 Binary files /dev/null and b/packages/opencv-python/reference-images/traffic_optical_flow.png differ diff --git a/packages/opencv-python/test_opencv_python.py b/packages/opencv-python/test_opencv_python.py new file mode 100644 index 000000000..7acf4d887 --- /dev/null +++ b/packages/opencv-python/test_opencv_python.py @@ -0,0 +1,551 @@ +import base64 +import pathlib + +from pyodide_build.testing import run_in_pyodide + +REFERENCE_IMAGES_PATH = pathlib.Path(__file__).parent / "reference-images" + + +def compare_with_reference_image(selenium, reference_image, var="img", grayscale=True): + reference_image_encoded = base64.b64encode(reference_image.read_bytes()) + grayscale = "cv.IMREAD_GRAYSCALE" if grayscale else "cv.IMREAD_COLOR" + match_ratio = selenium.run( + f""" + import base64 + import numpy as np + import cv2 as cv + DIFF_THRESHOLD = 2 + arr = np.frombuffer(base64.b64decode({reference_image_encoded!r}), np.uint8) + ref_data = cv.imdecode(arr, {grayscale}) + + pixels_match = np.count_nonzero(np.abs({var}.astype(np.int16) - ref_data.astype(np.int16)) <= DIFF_THRESHOLD) + pixels_total = ref_data.size + float(pixels_match / pixels_total) + """ + ) + + # Due to some randomness in the result, we allow a small difference + return match_ratio > 0.95 + + +def test_import(selenium): + selenium.set_script_timeout(60) + selenium.load_package("opencv-python") + selenium.run( + """ + import cv2 + cv2.__version__ + """ + ) + + +@run_in_pyodide(packages=["opencv-python", "numpy"]) +def test_image_extensions(): + import cv2 as cv + import numpy as np + + shape = (16, 16, 3) + img = np.zeros(shape, np.uint8) + + extensions = { + "bmp": b"BM6\x03", + "jpg": b"\xff\xd8\xff\xe0", + "jpeg": b"\xff\xd8\xff\xe0", + "png": b"\x89PNG", + "webp": b"RIFF", + } + + for ext, signature in extensions.items(): + result, buf = cv.imencode(f".{ext}", img) + assert result + assert bytes(buf[:4]) == signature + + +@run_in_pyodide(packages=["opencv-python", "numpy"]) +def test_io(): + import cv2 as cv + import numpy as np + + shape = (16, 16, 3) + img = np.zeros(shape, np.uint8) + + filename = "test.bmp" + cv.imwrite(filename, img) + img_ = cv.imread(filename) + assert img_.shape == img.shape + + +@run_in_pyodide(packages=["opencv-python", "numpy"]) +def test_drawing(): + import cv2 as cv + import numpy as np + + width = 100 + height = 100 + shape = (width, height, 3) + img = np.zeros(shape, np.uint8) + + cv.line(img, (0, 0), (width - 1, 0), (255, 0, 0), 5) + cv.line(img, (0, 0), (0, height - 1), (0, 0, 255), 5) + cv.rectangle(img, (0, 0), (width // 2, height // 2), (0, 255, 0), 2) + cv.circle(img, (0, 0), radius=width // 2, color=(255, 0, 0)) + cv.putText(img, "Hello Pyodide", (0, 0), cv.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) + + +@run_in_pyodide(packages=["opencv-python", "numpy"]) +def test_pixel_access(): + import cv2 as cv + import numpy as np + + shape = (16, 16, 3) + img = np.zeros(shape, np.uint8) + + img[5, 5] = [1, 2, 3] + assert list(img[5, 5]) == [1, 2, 3] + + b, g, r = cv.split(img) + img_ = cv.merge([b, g, r]) + assert (img == img_).all() + + +@run_in_pyodide(packages=["opencv-python", "numpy"]) +def test_image_processing(): + import cv2 as cv + import numpy as np + + # Masking + img = np.random.randint(0, 255, size=500) + lower = np.array([0]) + upper = np.array([200]) + mask = cv.inRange(img, lower, upper) + res = cv.bitwise_and(img, img, mask=mask) + assert not (res > 200).any() + + +def test_edge_detection(selenium): + original_img = base64.b64encode((REFERENCE_IMAGES_PATH / "baboon.png").read_bytes()) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) + sobel = cv.Sobel(gray, cv.CV_8U, 1, 0, 3) + laplacian = cv.Laplacian(gray, cv.CV_8U, ksize=3) + canny = cv.Canny(src, 100, 255) + None + """ + ) + + assert compare_with_reference_image( + selenium, REFERENCE_IMAGES_PATH / "baboon_sobel.png", "sobel" + ) + assert compare_with_reference_image( + selenium, REFERENCE_IMAGES_PATH / "baboon_laplacian.png", "laplacian" + ) + assert compare_with_reference_image( + selenium, REFERENCE_IMAGES_PATH / "baboon_canny.png", "canny" + ) + + +def test_photo_decolor(selenium): + original_img = base64.b64encode((REFERENCE_IMAGES_PATH / "baboon.png").read_bytes()) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + grayscale, color_boost = cv.decolor(src) + None + """ + ) + + assert compare_with_reference_image( + selenium, REFERENCE_IMAGES_PATH / "baboon_decolor_grayscale.png", "grayscale" + ) + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "baboon_decolor_color_boost.png", + "color_boost", + grayscale=False, + ) + + +def test_stitch(selenium): + original_img_left = base64.b64encode( + (REFERENCE_IMAGES_PATH / "mountain1.png").read_bytes() + ) + original_img_right = base64.b64encode( + (REFERENCE_IMAGES_PATH / "mountain2.png").read_bytes() + ) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + left = np.frombuffer(base64.b64decode({original_img_left!r}), np.uint8) + left = cv.imdecode(left, cv.IMREAD_COLOR) + right = np.frombuffer(base64.b64decode({original_img_right!r}), np.uint8) + right = cv.imdecode(right, cv.IMREAD_COLOR) + stitcher = cv.Stitcher.create(cv.Stitcher_PANORAMA) + status, panorama = stitcher.stitch([left, right]) + + # It seems that the result is not always the same due to the randomness, so check the status and size instead + assert status == cv.Stitcher_OK + assert panorama.shape[0] >= max(left.shape[0], right.shape[0]) + assert panorama.shape[1] >= max(left.shape[1], right.shape[1]) + """ + ) + + +def test_video_optical_flow(selenium): + original_img = base64.b64encode( + (REFERENCE_IMAGES_PATH / "traffic.mp4").read_bytes() + ) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + + src = base64.b64decode({original_img!r}) + + video_path = "video.mp4" + with open(video_path, "wb") as f: + f.write(src) + + cap = cv.VideoCapture(video_path) + assert cap.isOpened() + + # params for ShiTomasi corner detection + feature_params = dict( maxCorners = 100, + qualityLevel = 0.3, + minDistance = 7, + blockSize = 7 ) + # Parameters for lucas kanade optical flow + lk_params = dict( winSize = (15, 15), + maxLevel = 2, + criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03)) + + # Take first frame and find corners in it + ret, old_frame = cap.read() + assert ret + + old_gray = cv.cvtColor(old_frame, cv.COLOR_BGR2GRAY) + p0 = cv.goodFeaturesToTrack(old_gray, mask = None, **feature_params) + # Create a mask image for drawing purposes + mask = np.zeros_like(old_frame) + while(1): + ret, frame = cap.read() + if not ret: + break + frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) + # calculate optical flow + p1, st, err = cv.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) + # Select good points + if p1 is not None: + good_new = p1[st==1] + good_old = p0[st==1] + # draw the tracks + for i, (new, old) in enumerate(zip(good_new, good_old)): + a, b = new.ravel() + c, d = old.ravel() + mask = cv.line(mask, (int(a), int(b)), (int(c), int(d)), [0, 0, 255], 2) + frame = cv.circle(frame, (int(a), int(b)), 5, [255, 0, 0], -1) + img = cv.add(frame, mask) + # Now update the previous frame and previous points + old_gray = frame_gray.copy() + p0 = good_new.reshape(-1, 1, 2) + + optical_flow = img + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "traffic_optical_flow.png", + "optical_flow", + grayscale=False, + ) + + +def test_flann_sift(selenium): + original_img_src1 = base64.b64encode( + (REFERENCE_IMAGES_PATH / "box.png").read_bytes() + ) + original_img_src2 = base64.b64encode( + (REFERENCE_IMAGES_PATH / "box_in_scene.png").read_bytes() + ) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + src1 = np.frombuffer(base64.b64decode({original_img_src1!r}), np.uint8) + src1 = cv.imdecode(src1, cv.IMREAD_GRAYSCALE) + src2 = np.frombuffer(base64.b64decode({original_img_src2!r}), np.uint8) + src2 = cv.imdecode(src2, cv.IMREAD_GRAYSCALE) + + #-- Step 1: Detect the keypoints using SIFT Detector, compute the descriptors + detector = cv.SIFT_create() + keypoints1, descriptors1 = detector.detectAndCompute(src1, None) + keypoints2, descriptors2 = detector.detectAndCompute(src2, None) + + #-- Step 2: Matching descriptor vectors with a FLANN based matcher + matcher = cv.DescriptorMatcher_create(cv.DescriptorMatcher_FLANNBASED) + knn_matches = matcher.knnMatch(descriptors1, descriptors2, 2) + + #-- Filter matches using the Lowe's ratio test + ratio_thresh = 0.3 + good_matches = [] + for m,n in knn_matches: + if m.distance < ratio_thresh * n.distance: + good_matches.append(m) + + #-- Draw matches + matches = np.empty((max(src1.shape[0], src2.shape[0]), src1.shape[1]+src2.shape[1], 3), dtype=np.uint8) + cv.drawMatches(src1, keypoints1, src2, keypoints2, good_matches, matches, matchColor=[255, 0, 0], flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) + + sift_result = cv.cvtColor(matches, cv.COLOR_BGR2GRAY) + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "box_sift.png", + "sift_result", + grayscale=True, + ) + + +def test_dnn_mnist(selenium): + """ + Run tiny MNIST classification ONNX model + Training script: https://github.com/ryanking13/torch-opencv-mnist + """ + + original_img = base64.b64encode( + (REFERENCE_IMAGES_PATH / "mnist_2.png").read_bytes() + ) + tf_model = base64.b64encode((REFERENCE_IMAGES_PATH / "mnist.onnx").read_bytes()) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + + model_weights = base64.b64decode({tf_model!r}) + model_weights_path = './mnist.onnx' + with open(model_weights_path, 'wb') as f: + f.write(model_weights) + + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_GRAYSCALE) + + net = cv.dnn.readNet(model_weights_path) + blob = cv.dnn.blobFromImage(src, 1.0, (28, 28), (0, 0, 0), False, False) + + net.setInput(blob) + prob = net.forward() + assert "output_0" in net.getLayerNames() + assert np.argmax(prob) == 2 + """ + ) + + +def test_ml_pca(selenium): + original_img = base64.b64encode((REFERENCE_IMAGES_PATH / "pca.png").read_bytes()) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + from math import atan2, cos, sin, sqrt, pi + + def drawAxis(img, p_, q_, colour, scale): + p = list(p_) + q = list(q_) + + angle = atan2(p[1] - q[1], p[0] - q[0]) # angle in radians + hypotenuse = sqrt((p[1] - q[1]) * (p[1] - q[1]) + (p[0] - q[0]) * (p[0] - q[0])) + # Here we lengthen the arrow by a factor of scale + q[0] = p[0] - scale * hypotenuse * cos(angle) + q[1] = p[1] - scale * hypotenuse * sin(angle) + cv.line(img, (int(p[0]), int(p[1])), (int(q[0]), int(q[1])), colour, 1, cv.LINE_AA) + # create the arrow hooks + p[0] = q[0] + 9 * cos(angle + pi / 4) + p[1] = q[1] + 9 * sin(angle + pi / 4) + cv.line(img, (int(p[0]), int(p[1])), (int(q[0]), int(q[1])), colour, 1, cv.LINE_AA) + p[0] = q[0] + 9 * cos(angle - pi / 4) + p[1] = q[1] + 9 * sin(angle - pi / 4) + cv.line(img, (int(p[0]), int(p[1])), (int(q[0]), int(q[1])), colour, 1, cv.LINE_AA) + + def getOrientation(pts, img): + + sz = len(pts) + data_pts = np.empty((sz, 2), dtype=np.float64) + for i in range(data_pts.shape[0]): + data_pts[i,0] = pts[i,0,0] + data_pts[i,1] = pts[i,0,1] + # Perform PCA analysis + mean = np.empty((0)) + mean, eigenvectors, eigenvalues = cv.PCACompute2(data_pts, mean) + # Store the center of the object + cntr = (int(mean[0,0]), int(mean[0,1])) + + + cv.circle(img, cntr, 3, (255, 0, 255), 2) + p1 = (cntr[0] + 0.02 * eigenvectors[0,0] * eigenvalues[0,0], cntr[1] + 0.02 * eigenvectors[0,1] * eigenvalues[0,0]) + p2 = (cntr[0] - 0.02 * eigenvectors[1,0] * eigenvalues[1,0], cntr[1] - 0.02 * eigenvectors[1,1] * eigenvalues[1,0]) + drawAxis(img, cntr, p1, (0, 255, 0), 1) + drawAxis(img, cntr, p2, (255, 255, 0), 5) + angle = atan2(eigenvectors[0,1], eigenvectors[0,0]) # orientation in radians + + return angle + + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) + + # Convert image to binary + _, bw = cv.threshold(gray, 50, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) + contours, _ = cv.findContours(bw, cv.RETR_LIST, cv.CHAIN_APPROX_NONE) + for i, c in enumerate(contours): + # Calculate the area of each contour + area = cv.contourArea(c) + # Ignore contours that are too small or too large + if area < 1e2 or 1e5 < area: + continue + # Draw each contour only for visualisation purposes + cv.drawContours(src, contours, i, (0, 0, 255), 2) + # Find the orientation of each shape + getOrientation(c, src) + + pca_result = src + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "pca_result.png", + "pca_result", + grayscale=False, + ) + + +def test_objdetect_face(selenium): + original_img = base64.b64encode( + (REFERENCE_IMAGES_PATH / "monalisa.png").read_bytes() + ) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + from pathlib import Path + + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) + gray = cv.equalizeHist(gray) + + face_cascade = cv.CascadeClassifier() + eyes_cascade = cv.CascadeClassifier() + data_path = Path(cv.data.haarcascades) + face_cascade.load(str(data_path / "haarcascade_frontalface_alt.xml")) + eyes_cascade.load(str(data_path / "haarcascade_eye_tree_eyeglasses.xml")) + + faces = face_cascade.detectMultiScale(gray) + face_detected = src.copy() + for (x,y,w,h) in faces: + center = (x + w//2, y + h//2) + face_detected = cv.ellipse(face_detected, center, (w//2, h//2), 0, 0, 360, (255, 0, 255), 4) + faceROI = gray[y:y+h,x:x+w] + eyes = eyes_cascade.detectMultiScale(faceROI) + for (x2,y2,w2,h2) in eyes: + eye_center = (x + x2 + w2//2, y + y2 + h2//2) + radius = int(round((w2 + h2)*0.25)) + face_detected = cv.circle(face_detected, eye_center, radius, (255, 0, 0 ), 4) + + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "monalisa_facedetect.png", + "face_detected", + grayscale=False, + ) + + +def test_feature2d_kaze(selenium): + original_img = base64.b64encode((REFERENCE_IMAGES_PATH / "baboon.png").read_bytes()) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + + detector = cv.KAZE_create() + keypoints = detector.detect(src) + + kaze = cv.drawKeypoints(src, keypoints, None, color=(0, 0, 255), flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "baboon_kaze.png", + "kaze", + grayscale=False, + ) + + +def test_calib3d_chessboard(selenium): + original_img = base64.b64encode( + (REFERENCE_IMAGES_PATH / "chessboard.png").read_bytes() + ) + selenium.load_package("opencv-python") + selenium.run( + f""" + import base64 + import cv2 as cv + import numpy as np + src = np.frombuffer(base64.b64decode({original_img!r}), np.uint8) + src = cv.imdecode(src, cv.IMREAD_COLOR) + + criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001) + gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) + ret, corners = cv.findChessboardCorners(gray, (9, 6), None) + cv.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) + cv.drawChessboardCorners(gray, (9, 6), corners, ret) + chessboard_corners = gray + None + """ + ) + + assert compare_with_reference_image( + selenium, + REFERENCE_IMAGES_PATH / "chessboard_corners.png", + "chessboard_corners", + ) diff --git a/pyodide-build/pyodide_build/buildall.py b/pyodide-build/pyodide_build/buildall.py index 57752873f..3588e51b2 100755 --- a/pyodide-build/pyodide_build/buildall.py +++ b/pyodide-build/pyodide_build/buildall.py @@ -206,13 +206,18 @@ def generate_dependency_graph( if "*" in packages: packages.discard("*") packages.update( - str(x) for x in packages_dir.iterdir() if (x / "meta.yaml").is_file() + str(x.name) for x in packages_dir.iterdir() if (x / "meta.yaml").is_file() ) no_numpy_dependents = "no-numpy-dependents" in packages if no_numpy_dependents: packages.discard("no-numpy-dependents") + packages_exclude = list(filter(lambda pkg: pkg.startswith("!"), packages)) + for pkg_exclude in packages_exclude: + packages.discard(pkg_exclude) + packages.discard(pkg_exclude[1:]) + while packages: pkgname = packages.pop() diff --git a/pyodide-build/pyodide_build/pywasmcross.py b/pyodide-build/pyodide_build/pywasmcross.py index f3b88978b..06411c732 100755 --- a/pyodide-build/pyodide_build/pywasmcross.py +++ b/pyodide-build/pyodide_build/pywasmcross.py @@ -494,6 +494,7 @@ def handle_command_generate_args( if result: new_args.append(result) + return new_args @@ -527,6 +528,19 @@ def handle_command( if args.pkgname == "scipy": scipy_fixes(new_args) + # FIXME: For some unknown reason, + # opencv-python tries to link a same library (libopencv_world.a) multiple times, + # which leads to 'duplicated symbols' error. + if args.pkgname == "opencv-python": + duplicated_lib = "libopencv_world.a" + _new_args = [] + for arg in new_args: + if duplicated_lib in arg and arg in _new_args: + continue + _new_args.append(arg) + + new_args = _new_args + returncode = subprocess.run(new_args).returncode if returncode != 0: sys.exit(returncode)