diff --git a/.github/workflows/clean-gcloud-profiles.yml b/.github/workflows/clean-gcloud-profiles.yml index 5b5398156fd..2521b1de160 100644 --- a/.github/workflows/clean-gcloud-profiles.yml +++ b/.github/workflows/clean-gcloud-profiles.yml @@ -16,14 +16,11 @@ # happens, run this workflow manually to clean up the login profiles. name: Clean GCloud Profiles +permissions: + contents: read on: workflow_dispatch: - # push: - # branches: - # - main - # pull_request: - # types: [opened, reopened, synchronize] env: GCE_GPU_CI_SA: ${{ secrets.GCE_GPU_CI_SA }} @@ -36,12 +33,12 @@ jobs: fail-fast: false steps: - name: GCloud CLI auth - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: project_id: ${{ secrets.GCE_PROJECT }} credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: version: ${{ env.GCE_CLI_GHA_VERSION }} project_id: ${{ secrets.GCE_PROJECT }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 85368dd8a45..83527c68b56 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,4 +1,7 @@ name: Documentation +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -17,9 +20,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - GCE_CLI_GHA_VERSION: '416.0.0' # Fixed to avoid dependency on API changes - jobs: headless-docs: # Build headless and docs @@ -29,7 +29,7 @@ jobs: DEVELOPER_BUILD: ${{ github.event.inputs.developer_build || 'ON' }} steps: - name: Checkout Open3D source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | @@ -37,13 +37,13 @@ jobs: maximize_ubuntu_github_actions_build_space - name: Checkout Open3D-ML source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: isl-org/Open3D-ML path: ${{ env.OPEN3D_ML_ROOT }} - name: Setup cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: # Ref: https://github.com/apache/incubator-mxnet/pull/18459/files path: ~/.ccache @@ -56,9 +56,9 @@ jobs: restore-keys: | ${{ runner.os }}-ccache - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.11' - name: Install dependencies env: @@ -81,52 +81,49 @@ jobs: ccache -s - name: Upload docs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: open3d_docs path: docs/_out/html if-no-files-found: error - - name: GCloud CLI auth - if: ${{ github.ref == 'refs/heads/main' }} - uses: 'google-github-actions/auth@v1' - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} - - - name: Deploy docs + - name: Deploy docs if all artifacts available if: ${{ github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: ${{ github.token }} run: | - # Compress and upload the docs, only for main branch - docs_out_dir="docs/_out" # Docs in ${docs_out_dir}/html - tar_file="${{ github.sha }}_ready.tar.gz" + tar_file="open3d-${GITHUB_SHA}-docs.tar.gz" rm -rf ${tar_file} - tar -C ${docs_out_dir} -czvf ${tar_file} html - gsutil cp ${tar_file} gs://open3d-docs/${tar_file} - echo "Docs archive uploaded to:" - echo "https://storage.googleapis.com/open3d-docs/${tar_file}" + # Docs in docs/_out/html + tar -C docs/_out -cvzf ${tar_file} html - - name: Check wheels and ready documentation archive - if: ${{ github.ref == 'refs/heads/main' }} - run: | - if [ $(gsutil ls gs://open3d-docs/${{ github.sha }}_ready* | wc -l)\ - -eq 4 ]; then - echo "All wheels and docs available. Making docs ready." - # Remove all marker files: Note _ at end of pattern. - gsutil rm gs://open3d-docs/${{ github.sha }}_ready_* - # Rename docs archive: - gsutil mv gs://open3d-docs/${{ github.sha }}_ready.tar.gz \ - gs://open3d-docs/${{ github.sha }}.tar.gz - # Set holds on new artifacts, release on old - gsutil retention temp release gs://open3d-releases/* - gsutil retention temp set gs://open3d-releases/python-wheels/*${GITHUB_SHA:0:7}*.whl - gsutil retention temp set gs://open3d-releases/devel/*${GITHUB_SHA:0:7}* - else - echo "All wheels / docs not available yet." - fi + echo "Waiting for other release assets..." + this_sha=$(echo ${GITHUB_SHA} | cut -c 1-6) + n_this_sha_assets=$(gh release view main-devel --json assets --jq ".assets | map(select(.name | contains(\"${this_sha}\"))) | length") + # Total assets from each main branch commmit: + # Python wheels (4x4) + Viewer (3) + C++ libs (4+2+2) = 27, + while ((n_this_sha_assets < 27)); do + sleep 60 + echo -n "." + n_this_sha_assets=$(gh release view main-devel --json assets --jq ".assets | map(select(.name | contains(\"${this_sha}\"))) | length") + done + gh release upload main-devel ${tar_file} --clobber + gh release view main-devel + + echo "\nAll assets ready. Removing release assets except from last 3 commits: ${last_shas[@]}" + release_assets=($(gh release view main-devel --json assets --jq '.assets[] | .name')) + last_shas=($(git log --pretty=format:%h --abbrev-commit -n 3)) + for relass in "${release_assets[@]}"; do + found=false + for last_sha in "${last_shas[@]}"; do + if [[ $relass == *${last_sha}* ]]; then + found=true + fi + done + if [ $found == false ]; then + set -x + gh release delete-asset main-devel $relass + set +x + fi + done + gh release view main-devel diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 00bd9636130..362cc2327f0 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,7 @@ name: MacOS +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -22,14 +25,16 @@ env: # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources NPROC: 4 DEVELOPER_BUILD: ${{ github.event.inputs.developer_build || 'ON' }} - GCE_CLI_GHA_VERSION: '416.0.0' # Fixed to avoid dependency on API changes jobs: MacOS: - runs-on: macos-12 + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + # macos-12 is Intel runner, macos-14 is Apple Silicon + # https://github.com/actions/runner-images + os: [macos-12, macos-14] CONFIG: [ON, OFF] env: BUILD_SHARED_LIBS: ${{ matrix.CONFIG }} @@ -39,98 +44,161 @@ jobs: LOW_MEM_USAGE: ON steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: # Ref: https://github.com/apache/incubator-mxnet/pull/18459/files path: ~/.ccache # We include the commit sha in the cache key, as new cache entries are # only created if there is no existing entry for the key yet. - key: ${{ runner.os }}-ccache-${{ github.sha }} + key: ${{ runner.os }}-${{ runner.arch }}-ccache-${{ github.sha }} # Restore any ccache cache entry, if none for - # ${{ runner.os }}-ccache-${{ github.sha }} exists. + # ${{ runner.os }}-${{ runner.arch }}-ccache-${{ github.sha }} exists. # Common prefix will be used so that ccache can be used across commits. restore-keys: | - ${{ runner.os }}-ccache + ${{ runner.os }}-${{ runner.arch }}-ccache + - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: '3.11' + - name: Install dependencies run: | brew install ccache pkg-config - # Install libomp 11.1.0 from old brew bottle for catalina (10.15). - # Directly installing the Ruby formula will install for the current OS + + if [[ ${{ runner.arch}} == "ARM64" ]]; then + # Fix gfortran not found issue + ln -s $(which gfortran-13) /usr/local/bin/gfortran + + # Default macos-14 image Xcode (version 15.0.1) linker causes build issues. + # Newer Xcode versions work, but embree recommends Apple clang <= 14 on + # arm64 to avoid possible "EXEC_BAD_INSTRUCTION" runtime exceptions: + # https://github.com/embree/embree/releases/tag/v4.3.1 + sudo xcode-select -switch /Applications/Xcode_14.3.1.app + fi + + # Install libomp 11.1.0 from old brew bottle for x64 catalina (10.15) + # / arm64 big sur (11.0). Directly installing the Ruby formula will + # install for the current OS. # https://github.com/microsoft/LightGBM/issues/4229 - brew unlink libomp - curl -L -H "Authorization: Bearer QQ==" -o libomp-11.1.0.catalina.bottle.tar.gz \ - https://ghcr.io/v2/homebrew/core/libomp/blobs/sha256:45a5aa653bd45bd5ff5858580b1a4670c4b5a51ea29d68d45a53f72f56010e05 - brew install -f libomp-11.1.0.catalina.bottle.tar.gz + if [[ ${{ runner.arch}} == "X64" ]]; then + brew unlink libomp + # x64 catalina (10.15) bottle + export LIBOMP_BOTTLE_HASH=45a5aa653bd45bd5ff5858580b1a4670c4b5a51ea29d68d45a53f72f56010e05 + else # ARM64 + # arm64 big_sur (11.0) bottle + export LIBOMP_BOTTLE_HASH=f87f7841eb8b72650fa771af39642361aec371ea1a1f94f081ecc0e8168a0e75 + fi + curl -L -H "Authorization: Bearer QQ==" -o libomp-11.1.0.bottle.tar.gz \ + https://ghcr.io/v2/homebrew/core/libomp/blobs/sha256:$LIBOMP_BOTTLE_HASH + brew install -f libomp-11.1.0.bottle.tar.gz + ccache -M 2G # See .github/workflows/readme.md for ccache strategy. - name: Config and build run: | PATH=/usr/local/var/homebrew/linked/ccache/libexec:$PATH ccache -s ./util/run_ci.sh - DEVEL_PKG_NAME="$(basename package/open3d-devel-*.tar.xz)" + DEVEL_PKG_NAME="$(basename build/package/open3d-devel-*.tar.xz)" echo "DEVEL_PKG_NAME=$DEVEL_PKG_NAME" >> $GITHUB_ENV - name: Build Open3D viewer app if: ${{ env.BUILD_SHARED_LIBS == 'OFF' }} run: | + OPEN3D_VERSION_FULL="$(grep -F OPEN3D_VERSION_FULL build/CMakeCache.txt | cut -f2 -d'=')" PATH=/usr/local/var/homebrew/linked/ccache/libexec:$PATH pushd build make -j${NPROC} Open3DViewer pushd bin - zip -rv open3d-app-macosx-10_15.zip Open3D.app + zip -rv "open3d-${OPEN3D_VERSION_FULL}-app-macosx-10_15-${{ runner.arch }}.zip" Open3D.app ccache -s - name: Upload package if: ${{ env.BUILD_SHARED_LIBS == 'ON' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d-devel-macosx + name: ${{ env.DEVEL_PKG_NAME }} path: build/package/${{ env.DEVEL_PKG_NAME }} if-no-files-found: error - - name: GCloud CLI auth + - name: Update package devel release if: ${{ github.ref == 'refs/heads/main' && env.BUILD_SHARED_LIBS == 'ON' }} - uses: 'google-github-actions/auth@v1' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload main-devel build/package/${{ env.DEVEL_PKG_NAME }} --clobber + gh release view main-devel + + - name: Upload Open3D viewer app + uses: actions/upload-artifact@v4 + if: ${{ env.BUILD_SHARED_LIBS == 'OFF' }} with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' && env.BUILD_SHARED_LIBS == 'ON' }} - uses: google-github-actions/setup-gcloud@v1 + name: open3d-app-macosx-10_15-${{ runner.arch}} + path: build/bin/open3d-*-app-macosx-10_15-${{ runner.arch }}.zip + if-no-files-found: error + + fuse-viewer: + name: Fuse x64 and ARM64 viewer app + runs-on: [macos-12] + needs: [MacOS] + steps: + - name: Checkout source code # for gh release upload + uses: actions/checkout@v4 + - name: Download viewer apps + uses: actions/download-artifact@v4 with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} + pattern: open3d-app-macosx-10_15-* + merge-multiple: true - - name: Upload package to GCS bucket - if: ${{ github.ref == 'refs/heads/main' && env.BUILD_SHARED_LIBS == 'ON' }} + - name: Fuse x64 and arm64 viewer apps run: | - gsutil cp build/package/${{ env.DEVEL_PKG_NAME }} gs://open3d-releases/devel/ - echo "Download devel package at: https://storage.googleapis.com/open3d-releases/devel/${{ env.DEVEL_PKG_NAME }}" + unzip open3d-*-app-macosx-10_15-X64.zip -d x64 + unzip open3d-*-app-macosx-10_15-ARM64.zip -d arm64 + APP_NAME=$(ls open3d-*-app-macosx-10_15-X64.zip) + APP_NAME=${APP_NAME/-X64/-universal2} # includes version + for i in arm64/Open3D.app/Contents/MacOS/*; do + filepath=Open3D.app/Contents/MacOS/$(basename $i) + lipo -create arm64/${filepath} x64/${filepath} -output arm64/${filepath} + done + mv arm64/Open3D.app Open3D.app + zip -rv "${APP_NAME}" Open3D.app - name: Upload Open3D viewer app - uses: actions/upload-artifact@v3 - if: ${{ env.BUILD_SHARED_LIBS == 'OFF' }} + uses: actions/upload-artifact@v4 with: - name: open3d-app-macosx-10_15 - path: build/bin/open3d-app-macosx-10_15.zip + name: open3d-app-macosx-10_15-universal2 + path: open3d-*-app-macosx-10_15-universal2.zip if-no-files-found: error + - name: Update viewer devel release + if: ${{ github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload main-devel open3d-*-app-macosx-10_15-universal2.zip --clobber + gh release view main-devel + + build-wheel: name: Build wheel - runs-on: macos-12 + runs-on: ${{ matrix.os }} strategy: fail-fast: false # https://github.community/t/how-to-conditionally-include-exclude-items-in-matrix-eg-based-on-branch/16853/6 matrix: + # macos-12 is Intel runner, macos-14 is Apple Silicon + # https://github.com/actions/runner-images + os: [macos-12, macos-14] python_version: ['3.8', '3.9', '3.10', '3.11'] is_main: - ${{ github.ref == 'refs/heads/main' }} exclude: + # TODO: remove macos-14 excludes when https://github.com/actions/setup-python/issues/808 is fixed + - os: macos-14 + python_version: '3.8' + - os: macos-14 + python_version: '3.9' - is_main: false python_version: '3.8' - is_main: false @@ -143,30 +211,30 @@ jobs: OPEN3D_ML_ROOT: ${{ github.workspace }}/Open3D-ML steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout Open3D-ML source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: isl-org/Open3D-ML path: ${{ env.OPEN3D_ML_ROOT }} - name: Setup cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: # Ref: https://github.com/apache/incubator-mxnet/pull/18459/files path: ~/.ccache # We include the commit sha in the cache key, as new cache entries are # only created if there is no existing entry for the key yet. - key: ${{ runner.os }}-ccache-${{ github.sha }} + key: ${{ runner.os }}-${{ runner.arch }}-ccache-${{ github.sha }} # Restore any ccache cache entry, if none for - # ${{ runner.os }}-ccache-${{ github.sha }} exists. + # ${{ runner.os }}-${{ runner.arch }}-ccache-${{ github.sha }} exists. # Common prefix will be used so that ccache can be used across commits. restore-keys: | - ${{ runner.os }}-ccache + ${{ runner.os }}-${{ runner.arch }}-ccache - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} @@ -180,12 +248,26 @@ jobs: cmake --version source util/ci_utils.sh install_python_dependencies + + # Fix macos-14 arm64 runner image issues, see comments in MacOS job. + if [[ ${{ runner.arch}} == "ARM64" ]]; then + ln -s $(which gfortran-13) /usr/local/bin/gfortran + sudo xcode-select -switch /Applications/Xcode_14.3.1.app + fi + # Install libomp 11.1.0. See comment above. + if [[ ${{ runner.arch}} == "X64" ]]; then + brew unlink libomp + # x64 catalina (10.15) bottle + export LIBOMP_BOTTLE_HASH=45a5aa653bd45bd5ff5858580b1a4670c4b5a51ea29d68d45a53f72f56010e05 + else # ARM64 + # arm64 big_sur (11.0) bottle + export LIBOMP_BOTTLE_HASH=f87f7841eb8b72650fa771af39642361aec371ea1a1f94f081ecc0e8168a0e75 + fi + curl -L -H "Authorization: Bearer QQ==" -o libomp-11.1.0.bottle.tar.gz \ + https://ghcr.io/v2/homebrew/core/libomp/blobs/sha256:$LIBOMP_BOTTLE_HASH + brew install -f libomp-11.1.0.bottle.tar.gz brew install ccache - brew unlink libomp - curl -L -H "Authorization: Bearer QQ==" -o libomp-11.1.0.catalina.bottle.tar.gz \ - https://ghcr.io/v2/homebrew/core/libomp/blobs/sha256:45a5aa653bd45bd5ff5858580b1a4670c4b5a51ea29d68d45a53f72f56010e05 - brew install -f libomp-11.1.0.catalina.bottle.tar.gz ccache -M 2G # See .github/workflows/readme.md for ccache strategy. - name: Config and build wheel @@ -199,45 +281,93 @@ jobs: echo "PIP_PKG_NAME=$PIP_PKG_NAME" >> $GITHUB_ENV - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d_macosx_x86_64_wheels + name: ${{ env.PIP_PKG_NAME }} path: build/lib/python_package/pip_package/${{ env.PIP_PKG_NAME }} if-no-files-found: error - - name: GCloud CLI auth - if: ${{ github.ref == 'refs/heads/main' }} - uses: 'google-github-actions/auth@v1' + fuse-wheel: + name: Fuse universal2 wheel + runs-on: [macos-12] + needs: [build-wheel] + strategy: + fail-fast: false + # https://github.community/t/how-to-conditionally-include-exclude-items-in-matrix-eg-based-on-branch/16853/6 + matrix: + python_version: ['3.10', '3.11'] + is_main: + - ${{ github.ref == 'refs/heads/main' }} + exclude: + - is_main: false + python_version: '3.10' + steps: + - name: Checkout source code # for gh release upload + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 + python-version: ${{ matrix.python_version }} + + - name: Download X64 wheels + uses: actions/download-artifact@v4 with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} + pattern: open3d-*macosx*_x86_64.whl + path: x64_wheels + merge-multiple: true - - name: Upload wheel to GCS bucket - if: ${{ github.ref == 'refs/heads/main' }} + - name: Download ARM64 wheels + uses: actions/download-artifact@v4 + with: + pattern: open3d-*macosx*_arm64.whl + path: arm64_wheels + merge-multiple: true + + - name: Fuse x64 and ARM64 wheels env: python_version: ${{ matrix.python_version }} run: | PYTAG="-cp$(echo ${{ env.python_version }} | tr -d '.')" - gsutil cp build/lib/python_package/pip_package/${{ env.PIP_PKG_NAME }} gs://open3d-releases/python-wheels/ - echo "Download pip package at: https://storage.googleapis.com/open3d-releases/python-wheels/${{ env.PIP_PKG_NAME }}" + mkdir universal_wheels + pip install delocate + delocate-fuse -v x64_wheels/open3d-*${PYTAG}*.whl arm64_wheels/open3d-*${PYTAG}*.whl + # Normalize file name as delocate-fuse doesn't update it + OLD_WHL_NAME=$(basename x64_wheels/open3d-*${PYTAG}*.whl) + NEW_WHL_NAME=${OLD_WHL_NAME/x86_64/universal2} + mv x64_wheels/${OLD_WHL_NAME} universal_wheels/${NEW_WHL_NAME} + echo "PIP_PKG_NAME=$NEW_WHL_NAME" >> $GITHUB_ENV + + - name: Upload merged wheels + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PIP_PKG_NAME }} + path: universal_wheels/${{ env.PIP_PKG_NAME }} + if-no-files-found: error + + - name: Update devel release + if: ${{ github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload main-devel universal_wheels/${{ env.PIP_PKG_NAME }} --clobber + gh release view main-devel test-wheel: name: Test wheel - runs-on: macos-12 + runs-on: ${{ matrix.os }} needs: [build-wheel] strategy: fail-fast: false matrix: + os: [macos-12, macos-14] python_version: ['3.8', '3.9', '3.10', '3.11'] is_main: - ${{ github.ref == 'refs/heads/main' }} exclude: + - os: macos-14 + python_version: '3.8' + - os: macos-14 + python_version: '3.9' - is_main: false python_version: '3.8' - is_main: false @@ -249,23 +379,22 @@ jobs: OPEN3D_ML_ROOT: ${{ github.workspace }}/Open3D-ML steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout Open3D-ML source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: isl-org/Open3D-ML path: ${{ env.OPEN3D_ML_ROOT }} - name: Download wheels - uses: actions/download-artifact@v3 - # See https://github.com/dawidd6/action-download-artifact for more - # flexible artifact download options + uses: actions/download-artifact@v4 with: - name: open3d_macosx_x86_64_wheels + pattern: open3d-*macosx*.whl + merge-multiple: true - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} @@ -274,47 +403,10 @@ jobs: python -V source util/ci_utils.sh pi_tag=$(python -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')") - test_wheel open3d*-"$pi_tag"-*.whl + test_wheel open3d*-"$pi_tag"-*_$(uname -m).whl - - name: Run Python unit tests (benchmarks) + - name: Run Python unit tests run: | source util/ci_utils.sh echo "Running Open3D python tests..." run_python_tests - - ready-docs: - name: Ready docs archive - # no need to run on macOS - runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/main' }} - needs: [build-wheel, MacOS] - steps: - - name: GCloud CLI auth - uses: 'google-github-actions/auth@v1' - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} - - name: Check wheels and ready documentation archive - run: | - touch marker_file - gsutil cp marker_file gs://open3d-docs/${{ github.sha }}_ready_macos - if [ $(gsutil ls gs://open3d-docs/${{ github.sha }}_ready* | wc -l)\ - -eq 4 ]; then - echo "All wheels and docs available. Making docs ready." - # Remove all marker files: Note _ at end of pattern. - gsutil rm gs://open3d-docs/${{ github.sha }}_ready_* - # Rename docs archive: - gsutil mv gs://open3d-docs/${{ github.sha }}_ready.tar.gz \ - gs://open3d-docs/${{ github.sha }}.tar.gz - # Set holds on new artifacts, release on old - gsutil retention temp release gs://open3d-releases/* - gsutil retention temp set gs://open3d-releases/python-wheels/*${GITHUB_SHA:0:7}*.whl - gsutil retention temp set gs://open3d-releases/devel/*${GITHUB_SHA:0:7}* - else - echo "All wheels / docs not available yet. Docs not ready." - fi diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 744b997e2e7..3c27504fdd9 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -1,4 +1,7 @@ name: Style Check +permissions: + contents: read + actions: write on: workflow_dispatch: @@ -17,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.8' - name: Install dependencies diff --git a/.github/workflows/ubuntu-cuda.yml b/.github/workflows/ubuntu-cuda.yml index ce4528a83ca..9cf3cd3e749 100644 --- a/.github/workflows/ubuntu-cuda.yml +++ b/.github/workflows/ubuntu-cuda.yml @@ -1,4 +1,7 @@ name: Ubuntu CUDA +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -64,7 +67,7 @@ jobs: CCACHE_TAR_NAME : open3d-ci-${{ matrix.CI_CONFIG }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'false' - name: Package code @@ -74,12 +77,12 @@ jobs: tar -czvf Open3D.tar.gz Open3D ls -alh - name: GCloud CLI auth - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: project_id: ${{ secrets.GCE_PROJECT }} credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: version: ${{ env.GCE_CLI_GHA_VERSION }} project_id: ${{ secrets.GCE_PROJECT }} @@ -150,20 +153,19 @@ jobs: - name: Upload package if: ${{ env.BUILD_PACKAGE == 'true' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d-devel-linux-x86_64-cuda + name: open3d-devel-linux-x86_64-cuda-${{ matrix.CI_CONFIG }} path: open3d-devel-linux*.tar.xz if-no-files-found: error - - name: Upload package to GCS bucket + - name: Update devel release if: ${{ github.ref == 'refs/heads/main' && env.BUILD_PACKAGE == 'true' }} + env: + GH_TOKEN: ${{ github.token }} run: | - gcloud compute ssh "${INSTANCE_NAME}" \ - --zone="${GCE_ZONE}" \ - --command="ls -alh \ - && gsutil cp open3d-devel-linux-*.tar.xz \ - gs://open3d-releases/devel/" + gh release upload main-devel open3d-devel-linux-*.tar.xz --clobber + gh release view main-devel - name: VM run docker run: | diff --git a/.github/workflows/ubuntu-openblas.yml b/.github/workflows/ubuntu-openblas.yml index 233c32bd96f..acfe20e4ddf 100644 --- a/.github/workflows/ubuntu-openblas.yml +++ b/.github/workflows/ubuntu-openblas.yml @@ -1,4 +1,7 @@ name: Ubuntu OpenBLAS +permissions: + contents: read + actions: write on: workflow_dispatch: @@ -24,7 +27,7 @@ jobs: fail-fast: false steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | source util/ci_utils.sh @@ -66,7 +69,7 @@ jobs: GCE_INSTANCE_PREFIX: open3d-ci-openblas-arm64-py310-dev steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Package code run: | # GITHUB_WORKSPACE: /home/runner/work/Open3D/Open3D @@ -74,12 +77,12 @@ jobs: tar -czvf Open3D.tar.gz Open3D ls -alh - name: GCloud CLI auth - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: project_id: ${{ secrets.GCE_PROJECT }} credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: version: ${{ env.GCE_CLI_GHA_VERSION }} project_id: ${{ secrets.GCE_PROJECT }} diff --git a/.github/workflows/ubuntu-sycl.yml b/.github/workflows/ubuntu-sycl.yml index 1c1ac21697d..984d0fe9485 100644 --- a/.github/workflows/ubuntu-sycl.yml +++ b/.github/workflows/ubuntu-sycl.yml @@ -1,6 +1,10 @@ name: Ubuntu SYCL +permissions: + contents: read + actions: write on: + workflow_dispatch: push: branches: - main @@ -24,7 +28,7 @@ jobs: BUILD_SHARED_LIBS: [ON, OFF] steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | source util/ci_utils.sh @@ -38,6 +42,7 @@ jobs: fi - name: Docker test run: | + du -hs $PWD if [ "${{ matrix.BUILD_SHARED_LIBS }}" = "ON" ]; then docker/docker_test.sh sycl-shared else @@ -46,13 +51,13 @@ jobs: - name: GCloud CLI auth if: ${{ github.ref == 'refs/heads/main' }} - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: project_id: ${{ secrets.GCE_PROJECT }} credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - name: GCloud CLI setup if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: version: ${{ env.GCE_CLI_GHA_VERSION }} project_id: ${{ secrets.GCE_PROJECT }} diff --git a/.github/workflows/ubuntu-wheel.yml b/.github/workflows/ubuntu-wheel.yml index a798762c965..733c52218ee 100644 --- a/.github/workflows/ubuntu-wheel.yml +++ b/.github/workflows/ubuntu-wheel.yml @@ -1,4 +1,7 @@ name: Ubuntu Wheel +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -47,7 +50,7 @@ jobs: OPEN3D_CPU_RENDERING: true steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | source util/ci_utils.sh @@ -78,22 +81,22 @@ jobs: echo "PIP_PKG_NAME=$PIP_PKG_NAME" >> $GITHUB_ENV echo "PIP_CPU_PKG_NAME=$PIP_CPU_PKG_NAME" >> $GITHUB_ENV - name: Upload wheel to GitHub artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d_linux_x86_64_wheels + name: ${{ env.PIP_PKG_NAME }} path: | ${{ env.PIP_PKG_NAME }} ${{ env.PIP_CPU_PKG_NAME }} if-no-files-found: error - name: GCloud CLI auth if: ${{ github.ref == 'refs/heads/main' }} - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: project_id: ${{ secrets.GCE_PROJECT }} credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - name: GCloud CLI setup if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: version: ${{ env.GCE_CLI_GHA_VERSION }} project_id: ${{ secrets.GCE_PROJECT }} @@ -101,14 +104,14 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} run: | gsutil cp ${GITHUB_WORKSPACE}/${{ env.CCACHE_TAR_NAME }}.tar.gz gs://open3d-ci-cache/ - - name: Upload wheel to GCS + - name: Update devel release if: ${{ github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: ${{ github.token }} run: | - gsutil cp ${GITHUB_WORKSPACE}/${{ env.PIP_PKG_NAME }} \ - ${GITHUB_WORKSPACE}/${{ env.PIP_CPU_PKG_NAME }} gs://open3d-releases/python-wheels/ - echo "Download pip package at: - https://storage.googleapis.com/open3d-releases/python-wheels/${{ env.PIP_PKG_NAME }} - https://storage.googleapis.com/open3d-releases/python-wheels/${{ env.PIP_CPU_PKG_NAME }}" + gh release upload main-devel ${GITHUB_WORKSPACE}/${{ env.PIP_PKG_NAME }} \ + ${GITHUB_WORKSPACE}/${{ env.PIP_CPU_PKG_NAME }} --clobber + gh release view main-devel test-wheel-cpu: name: Test wheel CPU @@ -131,22 +134,23 @@ jobs: OPEN3D_ML_ROOT: ${{ github.workspace }}/Open3D-ML steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | source util/ci_utils.sh maximize_ubuntu_github_actions_build_space - name: Checkout Open3D-ML source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: isl-org/Open3D-ML path: ${{ env.OPEN3D_ML_ROOT }} - name: Download wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: open3d_linux_x86_64_wheels + pattern: open3d*-manylinux*.whl + merge-multiple: true - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Test Python package @@ -173,39 +177,3 @@ jobs: source util/ci_utils.sh echo "Running Open3D python tests (CPU wheel)..." run_python_tests - - ready-docs: - name: Ready docs archive - runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/main' }} - needs: [build-wheel] - steps: - - name: GCloud CLI auth - uses: 'google-github-actions/auth@v1' - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} - - name: Check wheels and ready documentation archive - run: | - touch marker_file - gsutil cp marker_file gs://open3d-docs/${{ github.sha }}_ready_ubuntu - if [ $(gsutil ls gs://open3d-docs/${{ github.sha }}_ready* | wc -l)\ - -eq 4 ]; then - echo "All wheels and docs available. Making docs ready." - # Remove all marker files: Note _ at end of pattern. - gsutil rm gs://open3d-docs/${{ github.sha }}_ready_* - # Rename docs archive: - gsutil mv gs://open3d-docs/${{ github.sha }}_ready.tar.gz \ - gs://open3d-docs/${{ github.sha }}.tar.gz - # Set holds on new artifacts, release on old - gsutil retention temp release gs://open3d-releases/* - gsutil retention temp set gs://open3d-releases/python-wheels/*${GITHUB_SHA:0:7}*.whl - gsutil retention temp set gs://open3d-releases/devel/*${GITHUB_SHA:0:7}* - else - echo "All wheels / docs not available yet." - fi diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1a69ec4da2d..08ef6721fea 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -1,4 +1,7 @@ name: Ubuntu +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -19,7 +22,6 @@ concurrency: env: NPROC: 2 - GCE_CLI_GHA_VERSION: "416.0.0" jobs: ubuntu: @@ -39,7 +41,7 @@ jobs: OPEN3D_CPU_RENDERING: true steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Maximize build space run: | source util/ci_utils.sh @@ -72,32 +74,26 @@ jobs: fi - name: Upload package to GitHub artifacts if: ${{ env.BUILD_SHARED_LIBS == 'ON' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d-devel-linux-x86_64 + name: open3d-devel-linux-x86_64-ML_${{ matrix.MLOPS }} path: open3d-devel-*.tar.xz if-no-files-found: error - name: Upload viewer to GitHub artifacts if: ${{ env.BUILD_SHARED_LIBS == 'OFF' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: open3d-viewer-Linux path: open3d-viewer-*-Linux.deb if-no-files-found: error - - name: GCloud CLI auth + - name: Update devel release if: ${{ github.ref == 'refs/heads/main' }} - uses: 'google-github-actions/auth@v1' - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} - - name: Upload package to GCS bucket - if: ${{ github.ref == 'refs/heads/main' && env.BUILD_SHARED_LIBS == 'ON' }} + env: + GH_TOKEN: ${{ github.token }} run: | - gsutil cp open3d-devel-*.tar.xz gs://open3d-releases/devel/ - echo "Download devel package at: https://storage.googleapis.com/open3d-releases/devel/${{ env.DEVEL_PKG_NAME }}" + if [ ${BUILD_SHARED_LIBS} == 'ON' ] ; then + gh release upload main-devel open3d-devel-*.tar.xz --clobber + else + gh release upload main-devel open3d-viewer-*-Linux.deb --clobber + fi + gh release view main-devel diff --git a/.github/workflows/vtk_packages.yml b/.github/workflows/vtk_packages.yml index 8ae7d812187..a134daeb71a 100644 --- a/.github/workflows/vtk_packages.yml +++ b/.github/workflows/vtk_packages.yml @@ -1,8 +1,8 @@ name: VTK Packages +permissions: + contents: write on: - # pull_request: - # branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-18.04 steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: CMake configure run: | mkdir build @@ -25,7 +25,7 @@ jobs: make -j$(nproc) cmake -E sha256sum vtk*.tar.gz > checksum_linux.txt - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: vtk_linux path: | @@ -53,7 +53,7 @@ jobs: - name: Disk space used run: Get-PSDrive - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Config # Move build directory to C: https://github.com/actions/virtual-environments/issues/1341 run: | @@ -75,7 +75,7 @@ jobs: ls cmake -E sha256sum (get-item vtk*.tar.gz).Name > checksum_win_${{matrix.configuration}}.txt - name: Upload package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: vtk_windows_${{matrix.configuration}} path: | @@ -90,7 +90,7 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: CMake configure run: | mkdir build @@ -102,7 +102,7 @@ jobs: make -j2 cmake -E sha256sum vtk*.tar.gz > checksum_macos.txt - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: vtk_macos path: | diff --git a/.github/workflows/webrtc.yml b/.github/workflows/webrtc.yml index 4602379d00f..91a9a5b122d 100644 --- a/.github/workflows/webrtc.yml +++ b/.github/workflows/webrtc.yml @@ -1,4 +1,7 @@ name: WebRTC +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -37,10 +40,10 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -61,9 +64,9 @@ jobs: build_webrtc - name: Upload WebRTC - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: webrtc_release + name: webrtc_release_${{ matrix.os }} path: | webrtc_*.tar.gz checksum_*.txt @@ -81,10 +84,10 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.8' @@ -167,9 +170,9 @@ jobs: cmake -E sha256sum webrtc_${env:WEBRTC_COMMIT_SHORT}_win.zip | Tee-Object -FilePath checksum_win.txt - name: Upload WebRTC - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: webrtc_release + name: webrtc_release_windows path: | ${{ env.WORK_DIR }}/webrtc_*.zip ${{ env.WORK_DIR }}/checksum_*.txt diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d687f6b7e33..c3c1d1e8395 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,4 +1,7 @@ name: Windows +permissions: + contents: write + actions: write on: workflow_dispatch: @@ -28,7 +31,6 @@ env: BUILD_DIR: "C:\\Open3D\\build" NPROC: 2 DEVELOPER_BUILD: ${{ github.event.inputs.developer_build || 'ON' }} - GCE_CLI_GHA_VERSION: '416.0.0' # Fixed to avoid dependency on API changes jobs: windows: @@ -54,7 +56,7 @@ jobs: run: Get-PSDrive - name: Setup Windows SDK - uses: GuillaumeFalourd/setup-windows10-sdk-action@v1.11 + uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 with: sdk-version: 19041 @@ -92,10 +94,10 @@ jobs: echo "$CUDA_PATH\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -148,12 +150,20 @@ jobs: - name: Upload Package if: ${{ matrix.BUILD_SHARED_LIBS == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d-devel-windows + name: ${{ env.DEVEL_PKG_NAME }} path: ${{ env.BUILD_DIR }}/package/${{ env.DEVEL_PKG_NAME }} if-no-files-found: error + - name: Update devel release with package + if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload main-devel ${{ env.BUILD_DIR }}/package/${{ env.DEVEL_PKG_NAME }} --clobber + gh release view main-devel + - name: Viewer App working-directory: ${{ env.BUILD_DIR }} if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} @@ -166,35 +176,22 @@ jobs: - name: Upload Viewer if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: open3d-app-windows-amd64 path: C:\Program Files\Open3D\bin\Open3D if-no-files-found: error - - name: GCloud CLI auth - if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' }} - uses: google-github-actions/auth@v1 - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' }} - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_DOCS_PROJECT }} - - - name: Upload package to GCS bucket - if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' }} + - name: Update devel release with viewer + if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + env: + GH_TOKEN: ${{ github.token }} run: | - gsutil cp ${{ env.BUILD_DIR }}/package/${{ env.DEVEL_PKG_NAME }} ` - gs://open3d-releases/devel/ - if ($LastExitCode -eq 0) { - echo "Download devel package at: https://storage.googleapis.com/open3d-releases/devel/${{ env.DEVEL_PKG_NAME }}" - } else { - throw "Devel package upload failed" - } + $Env:OPEN3D_VERSION_FULL = (Select-String -Path "C:/Open3D/build/CMakeCache.txt" -Pattern "OPEN3D_VERSION_FULL").Line.Split('=')[1] + Compress-Archive -Path "C:/Program Files/Open3D/bin/Open3D" -DestinationPath ` + "open3d-$Env:OPEN3D_VERSION_FULL-app-windows-amd64.zip" + gh release upload main-devel "open3d-$Env:OPEN3D_VERSION_FULL-app-windows-amd64.zip" --clobber + gh release view main-devel - name: Run C++ unit tests if: ${{ matrix.BUILD_CUDA_MODULE == 'OFF' }} @@ -255,15 +252,15 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Windows SDK - uses: GuillaumeFalourd/setup-windows10-sdk-action@v1.11 + uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 with: sdk-version: 19041 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} @@ -307,38 +304,19 @@ jobs: echo "PIP_PKG_NAME=$PIP_PKG_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: open3d_win_amd64_wheels + name: ${{ env.PIP_PKG_NAME }} path: ${{ env.BUILD_DIR }}/lib/python_package/pip_package/${{ env.PIP_PKG_NAME }} if-no-files-found: error - - name: GCloud CLI auth - if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/auth@v1 - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - if: ${{ github.ref == 'refs/heads/main' }} - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_DOCS_PROJECT }} - - - name: Upload wheel to GCS bucket + - name: Update devel release with wheel if: ${{ github.ref == 'refs/heads/main' }} env: - python_version: ${{ matrix.python_version }} + GH_TOKEN: ${{ github.token }} run: | - $ErrorActionPreference = 'Stop' - $PYTAG="-cp$(${{ env.python_version }} -replace '\.', '')" - gsutil cp ${{ env.BUILD_DIR }}/lib/python_package/pip_package/${{ env.PIP_PKG_NAME }} gs://open3d-releases/python-wheels/ - if ($LastExitCode -eq 0) { - echo "Download pip package at: https://storage.googleapis.com/open3d-releases/python-wheels/${{ env.PIP_PKG_NAME }}" - } else { - throw "Wheel upload failed" - } + gh release upload main-devel ${{ env.BUILD_DIR }}/lib/python_package/pip_package/${{ env.PIP_PKG_NAME }} --clobber + gh release view main-devel test-wheel: name: Test wheel @@ -360,17 +338,16 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download wheels - uses: actions/download-artifact@v3 - # See https://github.com/dawidd6/action-download-artifact for more - # flexible artifact download options + uses: actions/download-artifact@v4 with: - name: open3d_win_amd64_wheels + pattern: open3d-*win*.whl + merge-multiple: true - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} @@ -395,7 +372,6 @@ jobs: python -m pip install "$PIP_PKG_NAME" python -c "import open3d; print('Imported:', open3d)" python -c "import open3d; print('CUDA enabled: ', open3d.core.cuda.is_available())" - deactivate - name: Run Python unit tests @@ -407,42 +383,3 @@ jobs: echo "Testing ML and ML Ops disabled" python -m pytest python/test/ --ignore python/test/ml_ops/ deactivate - - ready-docs: - name: Ready docs archive - # no need to run on Windows - runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/main' }} - # temp workaround for Windows CUDA Debug CI out of space. Still update docs. - # needs: [build-wheel, windows] - needs: [build-wheel] - steps: - - name: GCloud CLI auth - uses: google-github-actions/auth@v1 - with: - project_id: ${{ secrets.GCE_PROJECT }} - credentials_json: '${{ secrets.GCE_SA_KEY_GPU_CI }}' - - name: GCloud CLI setup - uses: google-github-actions/setup-gcloud@v1 - with: - version: ${{ env.GCE_CLI_GHA_VERSION }} - project_id: ${{ secrets.GCE_PROJECT }} - - name: Check wheels and ready documentation archive - run: | - touch marker_file - gsutil cp marker_file gs://open3d-docs/${{ github.sha }}_ready_windows - if [ $(gsutil ls gs://open3d-docs/${{ github.sha }}_ready* | wc -l)\ - -eq 4 ]; then - echo "All wheels and docs available. Making docs ready." - # Remove all marker files: Note _ at end of pattern. - gsutil rm gs://open3d-docs/${{ github.sha }}_ready_* - # Rename docs archive: - gsutil mv gs://open3d-docs/${{ github.sha }}_ready.tar.gz \ - gs://open3d-docs/${{ github.sha }}.tar.gz - # Set holds on new artifacts, release on old - gsutil retention temp release gs://open3d-releases/* - gsutil retention temp set gs://open3d-releases/python-wheels/*${GITHUB_SHA:0:7}*.whl - gsutil retention temp set gs://open3d-releases/devel/*${GITHUB_SHA:0:7}* - else - echo "All wheels / docs not available yet." - fi diff --git a/3rdparty/civetweb/civetweb.cmake b/3rdparty/civetweb/civetweb.cmake index dc8d556ea64..5d4749fe4e7 100644 --- a/3rdparty/civetweb/civetweb.cmake +++ b/3rdparty/civetweb/civetweb.cmake @@ -3,8 +3,8 @@ include(ExternalProject) ExternalProject_Add( ext_civetweb PREFIX civetweb - URL https://github.com/civetweb/civetweb/archive/refs/tags/v1.15.tar.gz - URL_HASH SHA256=90a533422944ab327a4fbb9969f0845d0dba05354f9cacce3a5005fa59f593b9 + URL https://github.com/civetweb/civetweb/archive/refs/tags/v1.16.tar.gz + URL_HASH SHA256=f0e471c1bf4e7804a6cfb41ea9d13e7d623b2bcc7bc1e2a4dd54951a24d60285 DOWNLOAD_DIR "${OPEN3D_THIRD_PARTY_DOWNLOAD_DIR}/civetweb" UPDATE_COMMAND "" CMAKE_ARGS diff --git a/3rdparty/embree/embree.cmake b/3rdparty/embree/embree.cmake index 4e93eadf19e..35d27f11048 100644 --- a/3rdparty/embree/embree.cmake +++ b/3rdparty/embree/embree.cmake @@ -8,13 +8,11 @@ include(ExternalProject) # select ISAs if(APPLE) if(APPLE_AARCH64) - # Turn off ISA optimizations for Apple ARM64 for now. - set(ISA_ARGS -DEMBREE_ISA_AVX=OFF - -DEMBREE_ISA_AVX2=OFF - -DEMBREE_ISA_AVX512=OFF - -DEMBREE_ISA_SSE2=OFF - -DEMBREE_ISA_SSE42=OFF + set(ISA_ARGS -DEMBREE_ISA_NEON=OFF + -DEMBREE_ISA_NEON2X=ON ) + set(ISA_LIBS embree_avx2) + set(ISA_BUILD_BYPRODUCTS "/${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}embree_avx2${CMAKE_STATIC_LIBRARY_SUFFIX}") else() # With AppleClang we can select only 1 ISA. set(ISA_ARGS -DEMBREE_ISA_AVX=OFF @@ -69,8 +67,8 @@ endif() ExternalProject_Add( ext_embree PREFIX embree - URL https://github.com/embree/embree/archive/refs/tags/v3.13.3.tar.gz - URL_HASH SHA256=74ec785afb8f14d28ea5e0773544572c8df2e899caccdfc88509f1bfff58716f + URL https://github.com/embree/embree/archive/refs/tags/v4.3.1.tar.gz + URL_HASH SHA256=824edcbb7a8cd393c5bdb7a16738487b21ecc4e1d004ac9f761e934f97bb02a4 DOWNLOAD_DIR "${OPEN3D_THIRD_PARTY_DOWNLOAD_DIR}/embree" UPDATE_COMMAND "" CMAKE_ARGS @@ -88,7 +86,7 @@ ExternalProject_Add( -DEMBREE_TASKING_SYSTEM=INTERNAL ${WIN_CMAKE_ARGS} BUILD_BYPRODUCTS - /${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}embree3${CMAKE_STATIC_LIBRARY_SUFFIX} + /${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}embree4${CMAKE_STATIC_LIBRARY_SUFFIX} /${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}simd${CMAKE_STATIC_LIBRARY_SUFFIX} /${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}lexers${CMAKE_STATIC_LIBRARY_SUFFIX} /${Open3D_INSTALL_LIB_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}sys${CMAKE_STATIC_LIBRARY_SUFFIX} @@ -100,4 +98,4 @@ ExternalProject_Add( ExternalProject_Get_Property(ext_embree INSTALL_DIR) set(EMBREE_INCLUDE_DIRS ${INSTALL_DIR}/include/ ${INSTALL_DIR}/src/ext_embree/) # "/" is critical. set(EMBREE_LIB_DIR ${INSTALL_DIR}/${Open3D_INSTALL_LIB_DIR}) -set(EMBREE_LIBRARIES embree3 ${ISA_LIBS} simd lexers sys math tasking) +set(EMBREE_LIBRARIES embree4 ${ISA_LIBS} simd lexers sys math tasking) diff --git a/3rdparty/filament/filament_build.cmake b/3rdparty/filament/filament_build.cmake index cf2d650530e..432f55a28c0 100644 --- a/3rdparty/filament/filament_build.cmake +++ b/3rdparty/filament/filament_build.cmake @@ -72,5 +72,6 @@ ExternalProject_Add( -DFILAMENT_SUPPORTS_VULKAN=OFF -DFILAMENT_SKIP_SAMPLES=ON -DFILAMENT_OPENGL_HANDLE_ARENA_SIZE_IN_MB=20 # to support many small entities + -DSPIRV_WERROR=OFF BUILD_BYPRODUCTS ${lib_byproducts} ) diff --git a/3rdparty/filament/filament_download.cmake b/3rdparty/filament/filament_download.cmake index ef41dd17faf..f21cc91c31b 100644 --- a/3rdparty/filament/filament_download.cmake +++ b/3rdparty/filament/filament_download.cmake @@ -26,9 +26,17 @@ else() string(APPEND lib_dir /x86_64/md) endif() elseif(APPLE) - set(FILAMENT_URL https://github.com/google/filament/releases/download/v1.9.19/filament-v1.9.19-mac.tgz) - set(FILAMENT_SHA256 2765d0ce60647fc17d1880c4618cf7d6b5343d8be4dad87978c3917d9c723b4e) - string(APPEND lib_dir /x86_64) + if (APPLE_AARCH64) + set(FILAMENT_URL https://github.com/isl-org/open3d_downloads/releases/download/filament/filament-v1.9.19-macos_arm64.tgz) + set(FILAMENT_SHA256 3422bdff451d90144fbb69e625d8dcaeaf3222dc2c28879536067937955bc362) + string(APPEND lib_dir /arm64) + # Our arm64 builds use FILAMENT_SUPPORTS_VULKAN=OFF + list(REMOVE_ITEM filament_LIBRARIES bluevk) + else() + set(FILAMENT_URL https://github.com/google/filament/releases/download/v1.9.19/filament-v1.9.19-mac.tgz) + set(FILAMENT_SHA256 2765d0ce60647fc17d1880c4618cf7d6b5343d8be4dad87978c3917d9c723b4e) + string(APPEND lib_dir /x86_64) + endif() else() # Linux: Check glibc version and use open3d filament binary if new (Ubuntu 20.04 and similar) execute_process(COMMAND ldd --version OUTPUT_VARIABLE ldd_version) string(REGEX MATCH "([0-9]+\.)+[0-9]+" glibc_version ${ldd_version}) diff --git a/3rdparty/find_dependencies.cmake b/3rdparty/find_dependencies.cmake index 169bfbe1a54..d929b27728a 100644 --- a/3rdparty/find_dependencies.cmake +++ b/3rdparty/find_dependencies.cmake @@ -96,8 +96,6 @@ function(open3d_build_3rdparty_library name) if(arg_SOURCES) foreach(src IN LISTS arg_SOURCES) get_filename_component(abs_src "${src}" ABSOLUTE BASE_DIR "${arg_DIRECTORY}") - # Mark as generated to skip CMake's file existence checks - set_source_files_properties(${abs_src} PROPERTIES GENERATED TRUE) target_sources(${name} PRIVATE ${abs_src}) endforeach() foreach(incl IN LISTS include_dirs) @@ -1030,8 +1028,6 @@ if(NOT USE_SYSTEM_QHULLCPP) src/libqhull_r/rboxlib_r.c INCLUDE_DIRS src/ - DEPENDS - ext_qhull ) open3d_build_3rdparty_library(3rdparty_qhullcpp DIRECTORY ${QHULL_SOURCE_DIR} SOURCES @@ -1057,8 +1053,6 @@ if(NOT USE_SYSTEM_QHULLCPP) src/libqhullcpp/RoadLogEvent.cpp INCLUDE_DIRS src/ - DEPENDS - ext_qhull ) target_link_libraries(3rdparty_qhullcpp PRIVATE 3rdparty_qhull_r) list(APPEND Open3D_3RDPARTY_PRIVATE_TARGETS_FROM_CUSTOM Open3D::3rdparty_qhullcpp) @@ -1158,8 +1152,6 @@ if (BUILD_UNIT_TESTS) googletest/ googlemock/include/ googlemock/ - DEPENDS - ext_googletest ) endif() endif() @@ -1190,8 +1182,6 @@ if(BUILD_GUI) imgui_tables.cpp imgui_widgets.cpp imgui.cpp - DEPENDS - ext_imgui ) list(APPEND Open3D_3RDPARTY_PRIVATE_TARGETS_FROM_CUSTOM Open3D::3rdparty_imgui) else() diff --git a/3rdparty/googletest/googletest.cmake b/3rdparty/googletest/googletest.cmake index 706bf35204a..dd84c07c32c 100644 --- a/3rdparty/googletest/googletest.cmake +++ b/3rdparty/googletest/googletest.cmake @@ -1,6 +1,6 @@ -include(ExternalProject) +include(FetchContent) -ExternalProject_Add( +FetchContent_Declare( ext_googletest PREFIX googletest URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz @@ -12,5 +12,5 @@ ExternalProject_Add( INSTALL_COMMAND "" ) -ExternalProject_Get_Property(ext_googletest SOURCE_DIR) -set(GOOGLETEST_SOURCE_DIR ${SOURCE_DIR}) +FetchContent_Populate(ext_googletest) +FetchContent_GetProperties(ext_googletest SOURCE_DIR GOOGLETEST_SOURCE_DIR) diff --git a/3rdparty/imgui/imgui.cmake b/3rdparty/imgui/imgui.cmake index 752addd86a1..12ea3175736 100644 --- a/3rdparty/imgui/imgui.cmake +++ b/3rdparty/imgui/imgui.cmake @@ -1,6 +1,6 @@ -include(ExternalProject) +include(FetchContent) -ExternalProject_Add( +FetchContent_Declare( ext_imgui PREFIX imgui URL https://github.com/ocornut/imgui/archive/refs/tags/v1.88.tar.gz @@ -12,5 +12,5 @@ ExternalProject_Add( INSTALL_COMMAND "" ) -ExternalProject_Get_Property(ext_imgui SOURCE_DIR) -set(IMGUI_SOURCE_DIR ${SOURCE_DIR}) +FetchContent_Populate(ext_imgui) +FetchContent_GetProperties(ext_imgui SOURCE_DIR IMGUI_SOURCE_DIR) diff --git a/3rdparty/librealsense/fix-macos-arm64.patch b/3rdparty/librealsense/fix-macos-arm64.patch new file mode 100644 index 00000000000..0de73a574a1 --- /dev/null +++ b/3rdparty/librealsense/fix-macos-arm64.patch @@ -0,0 +1,22 @@ +From beb4c44debc8336de991c983274cad841eb5c323 Mon Sep 17 00:00:00 2001 +From: Pavol Rusnak +Date: Sun, 20 Jun 2021 12:26:58 +0200 +Subject: [PATCH] Fix build on macOS arm64 + +--- + src/proc/color-formats-converter.cpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/proc/color-formats-converter.cpp b/src/proc/color-formats-converter.cpp +index 564a23d9c4..6c6c8c97d8 100644 +--- a/src/proc/color-formats-converter.cpp ++++ b/src/proc/color-formats-converter.cpp +@@ -18,7 +18,7 @@ + #include // For SSSE3 intrinsics + #endif + +-#if defined (ANDROID) || (defined (__linux__) && !defined (__x86_64__)) ++#if defined (ANDROID) || (defined (__linux__) && !defined (__x86_64__)) || (defined (__APPLE__) && !defined (__x86_64__)) + + bool has_avx() { return false; } + diff --git a/3rdparty/librealsense/librealsense.cmake b/3rdparty/librealsense/librealsense.cmake index c6b4e358e32..e5caa700df7 100644 --- a/3rdparty/librealsense/librealsense.cmake +++ b/3rdparty/librealsense/librealsense.cmake @@ -17,6 +17,9 @@ ExternalProject_Add( COMMAND ${GIT_EXECUTABLE} init COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --ignore-whitespace ${CMAKE_CURRENT_LIST_DIR}/fix-cudacrt.patch + # Patch for macOS ARM64 support for versions < 2.50.0 + COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --ignore-whitespace + ${CMAKE_CURRENT_LIST_DIR}/fix-macos-arm64.patch CMAKE_ARGS -DCMAKE_INSTALL_PREFIX= -DBUILD_SHARED_LIBS=OFF diff --git a/3rdparty/qhull/qhull.cmake b/3rdparty/qhull/qhull.cmake index c20c86c471f..fe15e83ca6f 100644 --- a/3rdparty/qhull/qhull.cmake +++ b/3rdparty/qhull/qhull.cmake @@ -1,6 +1,6 @@ -include(ExternalProject) +include(FetchContent) -ExternalProject_Add( +FetchContent_Declare( ext_qhull PREFIX qhull # v8.0.0+ causes seg fault @@ -14,5 +14,5 @@ ExternalProject_Add( INSTALL_COMMAND "" ) -ExternalProject_Get_Property(ext_qhull SOURCE_DIR) -set(QHULL_SOURCE_DIR ${SOURCE_DIR}) +FetchContent_Populate(ext_qhull) +FetchContent_GetProperties(ext_qhull SOURCE_DIR QHULL_SOURCE_DIR) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24227ccae65..737669fbf2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Main + - Fix TriangleMesh::SamplePointsUniformly not sampling triangle meshes uniformly (PR #6653) - Fix tensor based TSDF integration example. - Use GLIBCXX_USE_CXX11_ABI=ON by default @@ -33,6 +34,10 @@ - Add Python pathlib support for file IO (PR #6619) - Fix log error message for `probability` argument validation in `PointCloud::SegmentPlane` (PR #6622) - `TriangleMesh`'s `+=` operator appends UVs regardless of the presence of existing features (PR #6728) +- Fix macOS arm64 builds, add CI runner for macOS arm64 (PR #6695) +- Fix KDTreeFlann possibly using a dangling pointer instead of internal storage and simplified its members (PR #6734) +- Fix RANSAC early stop if no inliers in a specific iteration (PR #6789) +- Fix segmentation fault (infinite recursion) of DetectPlanarPatches if multiple points have same coordinates (PR #6794) ## 0.13 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d03dad7dc6..6e5784bb4aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,11 +12,6 @@ cmake_minimum_required(VERSION 3.20) # CMake 3.20+ is required to: # - detect IntelLLVM compiler for SYCL -if (APPLE) -set (CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING - "Minimum OS X deployment version" FORCE) -endif() - # CMAKE_HOST_SYSTEM_PROCESSOR is only available after calling project(), # which depends on ${OPEN3D_VERSION}, which depends on ${DEVELOPER_BUILD}. if(UNIX AND NOT APPLE) @@ -35,6 +30,11 @@ if(APPLE) ) if(PROCESSOR_ARCH STREQUAL "arm64") set(APPLE_AARCH64 TRUE) + set (CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING + "Minimum OS X deployment version" FORCE) + else() + set (CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING + "Minimum OS X deployment version" FORCE) endif() endif() @@ -117,9 +117,12 @@ option(USE_SYSTEM_VTK "Use system pre-installed VTK" OFF option(USE_SYSTEM_ZEROMQ "Use system pre-installed ZeroMQ" OFF) if(LINUX_AARCH64 OR APPLE_AARCH64) option(BUILD_VTK_FROM_SOURCE "Build VTK from source" ON ) - option(BUILD_FILAMENT_FROM_SOURCE "Build filament from source" ON ) else() option(BUILD_VTK_FROM_SOURCE "Build VTK from source" OFF) +endif() +if(LINUX_AARCH64) + option(BUILD_FILAMENT_FROM_SOURCE "Build filament from source" ON ) +else() option(BUILD_FILAMENT_FROM_SOURCE "Build filament from source" OFF) endif() @@ -198,7 +201,7 @@ cmake_policy(GET CMP0072 CMP0072_VALUE) if ((LINUX_AARCH64 OR APPLE_AARCH64) AND BUILD_ISPC_MODULE) message(FATAL_ERROR "ISPC module is not yet supported on ARM Linux") endif() -if ((LINUX_AARCH64 OR APPLE_AARCH64) AND NOT BUILD_FILAMENT_FROM_SOURCE) +if (LINUX_AARCH64 AND NOT BUILD_FILAMENT_FROM_SOURCE) message(FATAL_ERROR "ARM CPU detected, you must set BUILD_FILAMENT_FROM_SOURCE=ON.") endif() if ((LINUX_AARCH64 OR APPLE_AARCH64) AND NOT USE_BLAS) @@ -254,7 +257,8 @@ string(CONCAT OPEN3D_VERSION ".${OPEN3D_VERSION_MINOR}" ".${OPEN3D_VERSION_PATCH}" ) -set(OPEN3D_VERSION_FULL "${OPEN3D_VERSION}${OPEN3D_VERSION_DEVHASH}") +set(OPEN3D_VERSION_FULL "${OPEN3D_VERSION}${OPEN3D_VERSION_DEVHASH}" CACHE + STRING "Open3D full version.") # Set additional info set(PROJECT_EMAIL "open3d@intel.com") set(PROJECT_DOCS "https://www.open3d.org/docs") diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..38d9c833993 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy +Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. + +## Reporting a Vulnerability +Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). \ No newline at end of file diff --git a/cmake/Open3DFetchISPCCompiler.cmake b/cmake/Open3DFetchISPCCompiler.cmake index 3f7940c648b..1753b52325e 100644 --- a/cmake/Open3DFetchISPCCompiler.cmake +++ b/cmake/Open3DFetchISPCCompiler.cmake @@ -50,7 +50,11 @@ function(open3d_fetch_ispc_compiler) ) FetchContent_MakeAvailable(ext_ispc) + if (WIN32) + set(CMAKE_ISPC_COMPILER "${ext_ispc_SOURCE_DIR}/bin/ispc.exe" PARENT_SCOPE) + else() # Linux + set(CMAKE_ISPC_COMPILER "${ext_ispc_SOURCE_DIR}/bin/ispc" PARENT_SCOPE) + endif() - set(CMAKE_ISPC_COMPILER "${ext_ispc_SOURCE_DIR}/bin/ispc" PARENT_SCOPE) endif() endfunction() diff --git a/cpp/apps/Open3DViewer/Debian/CMakeLists.in.txt b/cpp/apps/Open3DViewer/Debian/CMakeLists.in.txt index 9320f265d3e..b7bce43f4c6 100644 --- a/cpp/apps/Open3DViewer/Debian/CMakeLists.in.txt +++ b/cpp/apps/Open3DViewer/Debian/CMakeLists.in.txt @@ -32,7 +32,7 @@ set(CPACK_PACKAGE_NAME "open3d-viewer") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Open3D Viewer for 3D files") set(CPACK_PACKAGE_CONTACT "Open3D team <@PROJECT_EMAIL@>") set(CPACK_DEBIAN_PACKAGE_SECTION "Graphics") -set(CPACK_PACKAGE_VERSION "@OPEN3D_VERSION@") +set(CPACK_PACKAGE_VERSION "@OPEN3D_VERSION_FULL@") set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc++1, libgomp1, libpng16-16, libglfw3") set(CPACK_PACKAGE_HOMEPAGE_URL "@PROJECT_HOMEPAGE_URL@") diff --git a/cpp/open3d/core/CMakeLists.txt b/cpp/open3d/core/CMakeLists.txt index d42e645da39..7926d3fbcc6 100644 --- a/cpp/open3d/core/CMakeLists.txt +++ b/cpp/open3d/core/CMakeLists.txt @@ -56,6 +56,7 @@ target_sources(core PRIVATE kernel/ReductionCPU.cpp kernel/UnaryEW.cpp kernel/UnaryEWCPU.cpp + kernel/UnaryEWSYCL.cpp linalg/AddMM.cpp linalg/AddMMCPU.cpp linalg/Det.cpp diff --git a/cpp/open3d/core/Device.h b/cpp/open3d/core/Device.h index 2df1366c5b9..5b04875ed20 100644 --- a/cpp/open3d/core/Device.h +++ b/cpp/open3d/core/Device.h @@ -99,6 +99,10 @@ class IsDevice { inline bool IsCUDA() const { return GetDevice().GetType() == Device::DeviceType::CUDA; } + + inline bool IsSYCL() const { + return GetDevice().GetType() == Device::DeviceType::SYCL; + } }; } // namespace core diff --git a/cpp/open3d/core/Tensor.cpp b/cpp/open3d/core/Tensor.cpp index 7e1014bc800..8859be594e8 100644 --- a/cpp/open3d/core/Tensor.cpp +++ b/cpp/open3d/core/Tensor.cpp @@ -748,7 +748,7 @@ Tensor Tensor::Contiguous() const { std::string Tensor::ToString(bool with_suffix, const std::string& indent) const { std::ostringstream rc; - if (IsCUDA() || !IsContiguous()) { + if (IsCUDA() || IsSYCL() || !IsContiguous()) { Tensor host_contiguous_tensor = Contiguous().To(Device("CPU:0")); rc << host_contiguous_tensor.ToString(false, indent); } else { diff --git a/cpp/open3d/core/kernel/UnaryEW.cpp b/cpp/open3d/core/kernel/UnaryEW.cpp index d2cb7c89f4d..87b99a268aa 100644 --- a/cpp/open3d/core/kernel/UnaryEW.cpp +++ b/cpp/open3d/core/kernel/UnaryEW.cpp @@ -50,20 +50,28 @@ void Copy(const Tensor& src, Tensor& dst) { src.GetShape(), dst.GetShape()); } - // Disbatch to device + // Dispatch to device Device src_device = src.GetDevice(); Device dst_device = dst.GetDevice(); - if ((!src_device.IsCPU() && !src_device.IsCUDA()) || - (!dst_device.IsCPU() && !dst_device.IsCUDA())) { + if ((!src_device.IsCPU() && !src_device.IsCUDA() && !src_device.IsSYCL()) || + (!dst_device.IsCPU() && !dst_device.IsCUDA() && !dst_device.IsSYCL())) { utility::LogError("Copy: Unimplemented device"); } if (src_device.IsCPU() && dst_device.IsCPU()) { CopyCPU(src, dst); - } else { + } else if ((src_device.IsCPU() || src_device.IsCUDA()) && + (dst_device.IsCPU() || dst_device.IsCUDA())) { #ifdef BUILD_CUDA_MODULE CopyCUDA(src, dst); #else utility::LogError("Not compiled with CUDA, but CUDA device is used."); +#endif + } else if ((src_device.IsCPU() || src_device.IsSYCL()) && + (dst_device.IsCPU() || dst_device.IsSYCL())) { +#ifdef BUILD_SYCL_MODULE + CopySYCL(src, dst); +#else + utility::LogError("Not compiled with SYCL, but SYCL device is used."); #endif } } diff --git a/cpp/open3d/core/kernel/UnaryEW.h b/cpp/open3d/core/kernel/UnaryEW.h index c28c30bcc51..985eba0232a 100644 --- a/cpp/open3d/core/kernel/UnaryEW.h +++ b/cpp/open3d/core/kernel/UnaryEW.h @@ -49,6 +49,10 @@ void CopyCPU(const Tensor& src, Tensor& dst); void CopyCUDA(const Tensor& src, Tensor& dst); #endif +#ifdef BUILD_SYCL_MODULE +void CopySYCL(const Tensor& src, Tensor& dst); +#endif + } // namespace kernel } // namespace core } // namespace open3d diff --git a/cpp/open3d/core/kernel/UnaryEWSYCL.cpp b/cpp/open3d/core/kernel/UnaryEWSYCL.cpp new file mode 100644 index 00000000000..e32005c4b36 --- /dev/null +++ b/cpp/open3d/core/kernel/UnaryEWSYCL.cpp @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------- +// - Open3D: www.open3d.org - +// ---------------------------------------------------------------------------- +// Copyright (c) 2018-2023 www.open3d.org +// SPDX-License-Identifier: MIT +// ---------------------------------------------------------------------------- + +#include +#include + +#include "open3d/core/Dtype.h" +#include "open3d/core/MemoryManager.h" +#include "open3d/core/SizeVector.h" +#include "open3d/core/Tensor.h" +#include "open3d/core/kernel/UnaryEW.h" +#include "open3d/utility/Logging.h" + +namespace open3d { +namespace core { +namespace kernel { + +void CopySYCL(const Tensor& src, Tensor& dst) { + // It has been checked that + // - at least one of src or dst is SYCL device + SizeVector shape = src.GetShape(); + Dtype src_dtype = src.GetDtype(); + Dtype dst_dtype = dst.GetDtype(); + Device dst_device = dst.GetDevice(); + Device src_device = src.GetDevice(); + + if (src_dtype != dst_dtype) { + utility::LogError( + "CopySYCL: Dtype conversion from src to dst not implemented!"); + } + if ((dst_device.IsSYCL() && !dst.IsContiguous()) || + (src_device.IsSYCL() && !src.IsContiguous())) { + utility::LogError( + "CopySYCL: NonContiguous SYCL tensor Copy not implemented!"); + } + Tensor src_conti = src.Contiguous(); // No op if already contiguous + if (dst.IsContiguous() && src.GetShape() == dst.GetShape() && + src_dtype == dst_dtype) { + MemoryManager::Memcpy(dst.GetDataPtr(), dst_device, + src_conti.GetDataPtr(), src_conti.GetDevice(), + src_dtype.ByteSize() * shape.NumElements()); + } else { + dst.CopyFrom(src_conti.To(dst_device)); + } +} + +} // namespace kernel +} // namespace core +} // namespace open3d diff --git a/cpp/open3d/core/nns/NNSIndex.h b/cpp/open3d/core/nns/NNSIndex.h index 0e71a6d3a40..8eb8160da5d 100644 --- a/cpp/open3d/core/nns/NNSIndex.h +++ b/cpp/open3d/core/nns/NNSIndex.h @@ -51,7 +51,7 @@ class NNSIndex { /// \param knn Number of nearest neighbor to search. /// \return Pair of Tensors: (indices, distances): /// - indices: Tensor of shape {n, knn}, with dtype Int32. - /// - distainces: Tensor of shape {n, knn}, same dtype with dataset_points. + /// - distances: Tensor of shape {n, knn}, same dtype with dataset_points. virtual std::pair SearchKnn(const Tensor &query_points, int knn) const = 0; @@ -61,7 +61,7 @@ class NNSIndex { /// dtype with dataset_points. /// \param radii list of radius. Must be 1D, with shape {n, }. /// \return Tuple of Tensors: (indices, distances, num_neighbors): - /// - indicecs: Tensor of shape {total_num_neighbors,}, dtype Int32. + /// - indices: Tensor of shape {total_num_neighbors,}, dtype Int32. /// - distances: Tensor of shape {total_num_neighbors,}, same dtype with /// dataset_points. /// - num_neighbors: Tensor of shape {n,}, dtype Int32. @@ -76,7 +76,7 @@ class NNSIndex { /// dtype with dataset_points. /// \param radius Radius. /// \return Tuple of Tensors, (indices, distances, num_neighbors): - /// - indicecs: Tensor of shape {total_num_neighbors,}, dtype Int32. + /// - indices: Tensor of shape {total_num_neighbors,}, dtype Int32. /// - distances: Tensor of shape {total_num_neighbors,}, same dtype with /// dataset_points. /// - num_neighbors: Tensor of shape {n}, dtype Int32. diff --git a/cpp/open3d/core/nns/NanoFlannIndex.h b/cpp/open3d/core/nns/NanoFlannIndex.h index 32f0b48cedd..e092099d46e 100644 --- a/cpp/open3d/core/nns/NanoFlannIndex.h +++ b/cpp/open3d/core/nns/NanoFlannIndex.h @@ -54,7 +54,7 @@ class NanoFlannIndex : public NNSIndex { /// \param knn Number of nearest neighbor to search. /// \return Pair of Tensors: (indices, distances): /// - indices: Tensor of shape {n, knn}, with dtype Int32. - /// - distainces: Tensor of shape {n, knn}, same dtype with dataset_points. + /// - distances: Tensor of shape {n, knn}, same dtype with dataset_points. std::pair SearchKnn(const Tensor &query_points, int knn) const override; @@ -64,7 +64,7 @@ class NanoFlannIndex : public NNSIndex { /// dtype with dataset_points. /// \param radii list of radius. Must be 1D, with shape {n, }. /// \return Tuple of Tensors: (indices, distances, counts): - /// - indicecs: Tensor of shape {total_num_neighbors,}, dtype Int32. + /// - indices: Tensor of shape {total_num_neighbors,}, dtype Int32. /// - distances: Tensor of shape {total_num_neighbors,}, same dtype with /// dataset_points. /// - counts: Tensor of shape {n,}, dtype Int64. @@ -79,7 +79,7 @@ class NanoFlannIndex : public NNSIndex { /// dtype with dataset_points. /// \param radius Radius. /// \return Tuple of Tensors, (indices, distances, counts): - /// - indicecs: Tensor of shape {total_num_neighbors,}, dtype Int32. + /// - indices: Tensor of shape {total_num_neighbors,}, dtype Int32. /// - distances: Tensor of shape {total_num_neighbors,}, same dtype with /// dataset_points. /// - counts: Tensor of shape {n}, dtype Int64. diff --git a/cpp/open3d/core/nns/NearestNeighborSearch.h b/cpp/open3d/core/nns/NearestNeighborSearch.h index 32fa6b87785..7ce92811dd9 100644 --- a/cpp/open3d/core/nns/NearestNeighborSearch.h +++ b/cpp/open3d/core/nns/NearestNeighborSearch.h @@ -76,7 +76,7 @@ class NearestNeighborSearch { /// \param radius Radius. /// \param sort Sort the results by distance. Default is True. /// \return Tuple of Tensors, (indices, distances, num_neighbors): - /// - indicecs: Tensor of shape {total_number_of_neighbors,}, with dtype + /// - indices: Tensor of shape {total_number_of_neighbors,}, with dtype /// same as index_dtype_. /// - distances: Tensor of shape {total_number_of_neighbors,}, same dtype /// with query_points. The distances are squared L2 distances. @@ -91,7 +91,7 @@ class NearestNeighborSearch { /// \param radii Radii of query points. Each query point has one radius. /// Must be 1D, with shape {n,}. /// \return Tuple of Tensors, (indices,distances, num_neighbors): - /// - indicecs: Tensor of shape {total_number_of_neighbors,}, with dtype + /// - indices: Tensor of shape {total_number_of_neighbors,}, with dtype /// same as index_dtype_. /// - distances: Tensor of shape {total_number_of_neighbors,}, same dtype /// with query_points. The distances are squared L2 distances. @@ -108,7 +108,7 @@ class NearestNeighborSearch { /// \param max_knn Maximum number of neighbor to search per query. /// \return Tuple of Tensors, (indices, distances, counts): /// - indices: Tensor of shape {n, knn}, with dtype same as index_dtype_. - /// - distainces: Tensor of shape {n, knn}, with same dtype with + /// - distances: Tensor of shape {n, knn}, with same dtype with /// query_points. The distances are squared L2 distances. /// - counts: Counts of neighbour for each query points. [Tensor /// of shape {n}, with dtype same as index_dtype_]. diff --git a/cpp/open3d/geometry/KDTreeFlann.cpp b/cpp/open3d/geometry/KDTreeFlann.cpp index d93175a8d43..c81814a724d 100644 --- a/cpp/open3d/geometry/KDTreeFlann.cpp +++ b/cpp/open3d/geometry/KDTreeFlann.cpp @@ -97,8 +97,7 @@ int KDTreeFlann::SearchKNN(const T &query, // This is optimized code for heavily repeated search. // Other flann::Index::knnSearch() implementations lose performance due to // memory allocation/deallocation. - if (data_.empty() || dataset_size_ <= 0 || - size_t(query.rows()) != dimension_ || knn < 0) { + if (data_.size() == 0 || query.rows() != data_.rows() || knn < 0) { return -1; } indices.resize(knn); @@ -121,8 +120,7 @@ int KDTreeFlann::SearchRadius(const T &query, // Since max_nn is not given, we let flann to do its own memory management. // Other flann::Index::radiusSearch() implementations lose performance due // to memory management and CPU caching. - if (data_.empty() || dataset_size_ <= 0 || - size_t(query.rows()) != dimension_) { + if (data_.size() == 0 || query.rows() != data_.rows()) { return -1; } std::vector> indices_dists; @@ -148,8 +146,7 @@ int KDTreeFlann::SearchHybrid(const T &query, // It is also the recommended setting for search. // Other flann::Index::radiusSearch() implementations lose performance due // to memory allocation/deallocation. - if (data_.empty() || dataset_size_ <= 0 || - size_t(query.rows()) != dimension_ || max_nn < 0) { + if (data_.size() == 0 || query.rows() != data_.rows() || max_nn < 0) { return -1; } distance2.resize(max_nn); @@ -166,18 +163,12 @@ int KDTreeFlann::SearchHybrid(const T &query, } bool KDTreeFlann::SetRawData(const Eigen::Map &data) { - dimension_ = data.rows(); - dataset_size_ = data.cols(); - if (dimension_ == 0 || dataset_size_ == 0) { + if (data.size() == 0) { utility::LogWarning("[KDTreeFlann::SetRawData] Failed due to no data."); return false; } - data_.resize(dataset_size_ * dimension_); - memcpy(data_.data(), data.data(), - dataset_size_ * dimension_ * sizeof(double)); - data_interface_.reset(new Eigen::Map(data)); - nanoflann_index_.reset( - new KDTree_t(dimension_, std::cref(*data_interface_), 15)); + data_ = data; + nanoflann_index_ = std::make_unique(data_.rows(), data_, 15); nanoflann_index_->index_->buildIndex(); return true; } diff --git a/cpp/open3d/geometry/KDTreeFlann.h b/cpp/open3d/geometry/KDTreeFlann.h index 3d3ab2c5bea..cb41fccafb7 100644 --- a/cpp/open3d/geometry/KDTreeFlann.h +++ b/cpp/open3d/geometry/KDTreeFlann.h @@ -97,17 +97,13 @@ class KDTreeFlann { bool SetRawData(const Eigen::Map &data); protected: - using KDTree_t = nanoflann::KDTreeEigenMatrixAdaptor< - Eigen::Map, - -1, - nanoflann::metric_L2, - false>; + using KDTree_t = nanoflann::KDTreeEigenMatrixAdaptor; - std::vector data_; - std::unique_ptr> data_interface_; + Eigen::MatrixXd data_; std::unique_ptr nanoflann_index_; - size_t dimension_ = 0; - size_t dataset_size_ = 0; }; } // namespace geometry diff --git a/cpp/open3d/geometry/LineSetFactory.cpp b/cpp/open3d/geometry/LineSetFactory.cpp index 41e44d1db5f..3a66d86d300 100644 --- a/cpp/open3d/geometry/LineSetFactory.cpp +++ b/cpp/open3d/geometry/LineSetFactory.cpp @@ -170,6 +170,25 @@ std::shared_ptr LineSet::CreateCameraVisualization( lines->lines_.push_back({4, 1}); lines->PaintUniformColor({0.0f, 0.0f, 1.0f}); + // Add XYZ axes + lines->points_.push_back( + mult(m, Eigen::Vector3d{intrinsic(0, 0) * scale, 0.0, 0.0})); + lines->points_.push_back( + mult(m, Eigen::Vector3d{0.0, intrinsic(1, 1) * scale, 0.0})); + lines->points_.push_back( + mult(m, Eigen::Vector3d{intrinsic(0, 2) * scale, + intrinsic(1, 2) * scale, scale})); + + // Add lines for the axes + lines->lines_.push_back({0, 5}); // X axis (red) + lines->lines_.push_back({0, 6}); // Y axis (green) + lines->lines_.push_back({0, 7}); // Z axis (blue) + + // Set colors for the axes + lines->colors_.push_back({1.0f, 0.0f, 0.0f}); // Red + lines->colors_.push_back({0.0f, 1.0f, 0.0f}); // Green + lines->colors_.push_back({0.0f, 0.0f, 1.0f}); // Blue + return lines; } diff --git a/cpp/open3d/geometry/PointCloudPlanarPatchDetection.cpp b/cpp/open3d/geometry/PointCloudPlanarPatchDetection.cpp index b096ffc5655..b76bc9f3fc4 100644 --- a/cpp/open3d/geometry/PointCloudPlanarPatchDetection.cpp +++ b/cpp/open3d/geometry/PointCloudPlanarPatchDetection.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,8 @@ namespace geometry { namespace { +double tolerance = 1e-6; + /// \brief Planar patch container struct PlanarPatch { double GetSignedDistanceToPoint(const Eigen::Vector3d& point) const { @@ -55,12 +58,18 @@ class BoundaryVolumeHierarchy { /// \brief Constructor for the root node of the octree. /// - /// \param point_cloud is the associated set of points being partitioned - BoundaryVolumeHierarchy(const PointCloud* point_cloud, - const Eigen::Vector3d& min_bound, - const Eigen::Vector3d& max_bound, - size_t min_points = 1, - double min_size = 0.0) + /// \param point_cloud is the associated set of points being partitioned. + /// \param min_bound is the minimum coordinate of the bounding volume. + /// \param max_bound is the maximum coordinate of the bounding volume. + /// \param min_points is the threshold number of points in a node to stop + /// partitioning it further. \param min_size is the threshold size of a node + /// to stop partitioning it further. + BoundaryVolumeHierarchy( + const PointCloud* point_cloud, + const Eigen::Vector3d& min_bound, + const Eigen::Vector3d& max_bound, + size_t min_points = 1, + double min_size = std::numeric_limits::epsilon()) : point_cloud_(point_cloud), min_points_(min_points), min_size_(min_size), @@ -400,9 +409,13 @@ class PlaneDetector { (rect.bottom_left(1) + rect.top_right(1)) / 2. * rect.B.col(1); // Scale basis to fit points - const double width = (rect.top_right.x() - rect.bottom_left.x()); - const double height = (rect.top_right.y() - rect.bottom_left.y()); - const double depth = (rect.top_right.z() - rect.bottom_left.z()); + const double _mini = std::min(tolerance, max_point_dist_); + const double width = + std::max(rect.top_right.x() - rect.bottom_left.x(), _mini); + const double height = + std::max(rect.top_right.y() - rect.bottom_left.y(), _mini); + const double depth = + std::max(rect.top_right.z() - rect.bottom_left.z(), _mini); std::shared_ptr obox = std::make_shared(); @@ -590,6 +603,7 @@ class PlaneDetector { // Use lower bound of the spread around the median as an indication // of how similar the point normals associated with the patch are. GetMinMaxRScore(normal_similarities, min_normal_diff_, tmp, 3); + min_normal_diff_ = std::min(min_normal_diff_, 1.0 - tolerance); // Use upper bound of the spread around the median as an indication // of how close the points associated with the patch are to the patch. GetMinMaxRScore(point_distances, tmp, max_point_dist_, 3); @@ -937,7 +951,6 @@ void ExtractPatchesFromPlanes( Eigen::Vector3d(0.4660, 0.6740, 0.1880), Eigen::Vector3d(0.3010, 0.7450, 0.9330), Eigen::Vector3d(0.6350, 0.0780, 0.1840)}; - for (size_t i = 0; i < planes.size(); i++) { if (!planes[i]->IsFalsePositive()) { // create a patch by delimiting the plane using its perimeter points diff --git a/cpp/open3d/io/ImageIO.cpp b/cpp/open3d/io/ImageIO.cpp index a4126df14e6..c00361a449a 100644 --- a/cpp/open3d/io/ImageIO.cpp +++ b/cpp/open3d/io/ImageIO.cpp @@ -7,24 +7,25 @@ #include "open3d/io/ImageIO.h" +#include +#include #include #include "open3d/utility/FileSystem.h" #include "open3d/utility/Logging.h" namespace open3d { +namespace io { namespace { -using namespace io; -static const std::unordered_map< - std::string, - std::function> - file_extension_to_image_read_function{ - {"png", ReadImageFromPNG}, - {"jpg", ReadImageFromJPG}, - {"jpeg", ReadImageFromJPG}, - }; +using signature_decoder_t = + std::pair>; +static const std::array signature_decoder_list{ + {{"\x89\x50\x4e\x47\xd\xa\x1a\xa", ReadImageFromPNG}, + {"\xFF\xD8\xFF", ReadImageFromJPG}}}; +static constexpr uint8_t MAX_SIGNATURE_LEN = 8; static const std::unordered_map< std::string, @@ -34,11 +35,8 @@ static const std::unordered_map< {"jpg", WriteImageToJPG}, {"jpeg", WriteImageToJPG}, }; - } // unnamed namespace -namespace io { - std::shared_ptr CreateImageFromFile( const std::string &filename) { auto image = std::make_shared(); @@ -47,21 +45,27 @@ std::shared_ptr CreateImageFromFile( } bool ReadImage(const std::string &filename, geometry::Image &image) { - std::string filename_ext = - utility::filesystem::GetFileExtensionInLowerCase(filename); - if (filename_ext.empty()) { - utility::LogWarning( - "Read geometry::Image failed: missing file extension."); - return false; - } - auto map_itr = file_extension_to_image_read_function.find(filename_ext); - if (map_itr == file_extension_to_image_read_function.end()) { - utility::LogWarning( - "Read geometry::Image failed: file extension {} unknown", - filename_ext); - return false; + std::string signature_buffer(MAX_SIGNATURE_LEN, 0); + std::ifstream file(filename, std::ios::binary); + file.read(&signature_buffer[0], MAX_SIGNATURE_LEN); + std::string err_msg; + if (!file) { + err_msg = "Read geometry::Image failed for file {}. I/O error."; + } else { + file.close(); + for (const auto &signature_decoder : signature_decoder_list) { + if (signature_buffer.compare(0, signature_decoder.first.size(), + signature_decoder.first) == 0) { + return signature_decoder.second(filename, image); + } + } + err_msg = + "Read geometry::Image failed for file {}. Unknown file " + "signature, only PNG and JPG are supported."; } - return map_itr->second(filename, image); + image.Clear(); + utility::LogWarning(err_msg.c_str(), filename); + return false; } bool WriteImage(const std::string &filename, diff --git a/cpp/open3d/io/ImageIO.h b/cpp/open3d/io/ImageIO.h index 3c6b62be3d8..45b0a3f71ed 100644 --- a/cpp/open3d/io/ImageIO.h +++ b/cpp/open3d/io/ImageIO.h @@ -69,11 +69,18 @@ bool WriteImageToJPG(const std::string &filename, const geometry::Image &image, int quality = kOpen3DImageIODefaultQuality); -/// The general entrance for reading an Image from memory +/// Read a PNG image from memory. +/// \param image_data_ptr the pointer to image data in memory. +/// \param image_data_size the size of image data in memory. +/// \return return true if the read function is successful, false otherwise. bool ReadPNGFromMemory(const unsigned char *image_data_ptr, size_t image_data_size, geometry::Image &image); +/// Read a JPG image from memory. +/// \param image_data_ptr the pointer to image data in memory. +/// \param image_data_size the size of image data in memory. +/// \return return true if the read function is successful, false otherwise. bool ReadJPGFromMemory(const unsigned char *image_data_ptr, size_t image_data_size, geometry::Image &image); diff --git a/cpp/open3d/io/file_format/FileASSIMP.cpp b/cpp/open3d/io/file_format/FileASSIMP.cpp index e78d8be7718..92a2fda8077 100644 --- a/cpp/open3d/io/file_format/FileASSIMP.cpp +++ b/cpp/open3d/io/file_format/FileASSIMP.cpp @@ -422,6 +422,9 @@ bool ReadModelUsingAssimp(const std::string& filename, mat->Get(AI_MATKEY_CLEARCOAT_ROUGHNESS_FACTOR, o3d_mat.base_clearcoat_roughness); mat->Get(AI_MATKEY_ANISOTROPY, o3d_mat.base_anisotropy); + mat->Get(AI_MATKEY_COLOR_EMISSIVE, color); + o3d_mat.emissive_color = + Eigen::Vector4f(color.r, color.g, color.b, 1.f); aiString alpha_mode; mat->Get(AI_MATKEY_GLTF_ALPHAMODE, alpha_mode); std::string alpha_mode_str(alpha_mode.C_Str()); diff --git a/cpp/open3d/io/file_format/FileJPG.cpp b/cpp/open3d/io/file_format/FileJPG.cpp index 7de155575bf..cb9cf6760e9 100644 --- a/cpp/open3d/io/file_format/FileJPG.cpp +++ b/cpp/open3d/io/file_format/FileJPG.cpp @@ -17,6 +17,23 @@ namespace open3d { namespace io { +namespace { + +/// Convert libjpeg error messages to std::runtime_error. This prevents +/// libjpeg from exit() in case of errors. +void jpeg_error_throw(j_common_ptr p_cinfo) { + if (p_cinfo->is_decompressor) + jpeg_destroy_decompress( + reinterpret_cast(p_cinfo)); + else + jpeg_destroy_compress( + reinterpret_cast(p_cinfo)); + char buffer[JMSG_LENGTH_MAX]; + (*p_cinfo->err->format_message)(p_cinfo, buffer); + throw std::runtime_error(buffer); +} + +} // namespace bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { struct jpeg_decompress_struct cinfo; @@ -27,53 +44,64 @@ bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { if ((file_in = utility::filesystem::FOpen(filename, "rb")) == NULL) { utility::LogWarning("Read JPG failed: unable to open file: {}", filename); + image.Clear(); return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_stdio_src(&cinfo, file_in); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - int bytes_per_channel = 1; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return false; - } - jpeg_start_decompress(&cinfo); - image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, - bytes_per_channel); - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = image.data_.data(); - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - memcpy(pdata, buffer[0], row_stride); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, file_in); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + int bytes_per_channel = 1; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, + bytes_per_channel); + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = image.data_.data(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + memcpy(pdata, buffer[0], row_stride); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + return true; + } catch (const std::runtime_error &err) { + fclose(file_in); + image.Clear(); + utility::LogWarning("libjpeg error: {}", err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return true; } bool WriteImageToJPG(const std::string &filename, @@ -108,30 +136,37 @@ bool WriteImageToJPG(const std::string &filename, return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, file_out); - cinfo.image_width = image.width_; - cinfo.image_height = image.height_; - cinfo.input_components = image.num_of_channels_; - cinfo.in_color_space = - (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); - int row_stride = image.width_ * image.num_of_channels_; - const uint8_t *pdata = image.data_.data(); - std::vector buffer(row_stride); - while (cinfo.next_scanline < cinfo.image_height) { - memcpy(buffer.data(), pdata, row_stride); - row_pointer[0] = buffer.data(); - jpeg_write_scanlines(&cinfo, row_pointer, 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, file_out); + cinfo.image_width = image.width_; + cinfo.image_height = image.height_; + cinfo.input_components = image.num_of_channels_; + cinfo.in_color_space = + (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + int row_stride = image.width_ * image.num_of_channels_; + const uint8_t *pdata = image.data_.data(); + std::vector buffer(row_stride); + while (cinfo.next_scanline < cinfo.image_height) { + memcpy(buffer.data(), pdata, row_stride); + row_pointer[0] = buffer.data(); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + pdata += row_stride; + } + jpeg_finish_compress(&cinfo); + fclose(file_out); + jpeg_destroy_compress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + fclose(file_out); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_compress(&cinfo); - fclose(file_out); - jpeg_destroy_compress(&cinfo); - return true; } bool ReadJPGFromMemory(const unsigned char *image_data_ptr, @@ -141,48 +176,57 @@ bool ReadJPGFromMemory(const unsigned char *image_data_ptr, struct jpeg_error_mgr jerr; JSAMPARRAY buffer; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_mem_src(&cinfo, image_data_ptr, image_data_size); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - int bytes_per_channel = 1; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - return false; - } - jpeg_start_decompress(&cinfo); - image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, - bytes_per_channel); - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = image.data_.data(); - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - memcpy(pdata, buffer[0], row_stride); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, image_data_ptr, image_data_size); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + int bytes_per_channel = 1; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, + bytes_per_channel); + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = image.data_.data(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + memcpy(pdata, buffer[0], row_stride); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + image.Clear(); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - return true; } } // namespace io diff --git a/cpp/open3d/io/file_format/FilePNG.cpp b/cpp/open3d/io/file_format/FilePNG.cpp index 70832d8ee1e..55a7e410eda 100644 --- a/cpp/open3d/io/file_format/FilePNG.cpp +++ b/cpp/open3d/io/file_format/FilePNG.cpp @@ -106,6 +106,7 @@ bool ReadPNGFromMemory(const unsigned char *image_data_ptr, if (png_image_begin_read_from_memory(&pngimage, image_data_ptr, image_data_size) == 0) { utility::LogWarning("Read PNG failed: unable to parse header."); + image.Clear(); return false; } @@ -122,6 +123,7 @@ bool ReadPNGFromMemory(const unsigned char *image_data_ptr, if (png_image_finish_read(&pngimage, NULL, image.data_.data(), 0, NULL) == 0) { utility::LogWarning("PNG error: {}", pngimage.message); + image.Clear(); return false; } return true; diff --git a/cpp/open3d/pipelines/registration/Registration.cpp b/cpp/open3d/pipelines/registration/Registration.cpp index 8527d620bf0..6820d2f7c33 100644 --- a/cpp/open3d/pipelines/registration/Registration.cpp +++ b/cpp/open3d/pipelines/registration/Registration.cpp @@ -235,13 +235,15 @@ RegistrationResult RegistrationRANSACBasedOnCorrespondence( max_correspondence_distance, transformation); - // Update exit condition if necessary. - // If confidence is 1.0, then it is safely inf, we always - // consume all the iterations. + // Update exit condition if necessary double est_k_local_d = std::log(1.0 - criteria.confidence_) / std::log(1.0 - std::pow(corres_inlier_ratio, ransac_n)); + // This prevents having a negative number of iterations: + // est_k_local_d = -inf if corres_inlier_ratio = 0.0 + est_k_local_d = + est_k_local_d < 0 ? est_k_local : est_k_local_d; est_k_local = est_k_local_d < est_k_global ? static_cast(std::ceil(est_k_local_d)) diff --git a/cpp/open3d/t/geometry/LineSet.cpp b/cpp/open3d/t/geometry/LineSet.cpp index c2d95f0ced2..f525694ca45 100644 --- a/cpp/open3d/t/geometry/LineSet.cpp +++ b/cpp/open3d/t/geometry/LineSet.cpp @@ -9,6 +9,7 @@ #include +#include "open3d/core/Dtype.h" #include "open3d/core/EigenConverter.h" #include "open3d/core/ShapeUtil.h" #include "open3d/core/Tensor.h" @@ -211,6 +212,81 @@ OrientedBoundingBox LineSet::GetOrientedBoundingBox() const { return OrientedBoundingBox::CreateFromPoints(GetPointPositions()); } +LineSet &LineSet::PaintUniformColor(const core::Tensor &color) { + core::AssertTensorShape(color, {3}); + core::Tensor clipped_color = color.To(GetDevice()); + if (color.GetDtype() == core::Float32 || + color.GetDtype() == core::Float64) { + clipped_color = clipped_color.Clip(0.0f, 1.0f); + } + core::Tensor ls_colors = + core::Tensor::Empty({GetLineIndices().GetLength(), 3}, + clipped_color.GetDtype(), GetDevice()); + ls_colors.AsRvalue() = clipped_color; + SetLineColors(ls_colors); + + return *this; +} + +LineSet LineSet::CreateCameraVisualization(int view_width_px, + int view_height_px, + const core::Tensor &intrinsic_in, + const core::Tensor &extrinsic_in, + double scale, + const core::Tensor &color) { + core::AssertTensorShape(intrinsic_in, {3, 3}); + core::AssertTensorShape(extrinsic_in, {4, 4}); + core::Tensor intrinsic = intrinsic_in.To(core::Float32, "CPU:0"); + core::Tensor extrinsic = extrinsic_in.To(core::Float32, "CPU:0"); + + // Calculate points for camera visualization + float w(view_width_px), h(view_height_px), s(scale); + float fx = intrinsic[0][0].Item(), + fy = intrinsic[1][1].Item(), + cx = intrinsic[0][2].Item(), + cy = intrinsic[1][2].Item(); + core::Tensor points = core::Tensor::Init({{0.f, 0.f, 0.f}, // origin + {0.f, 0.f, s}, + {w * s, 0.f, s}, + {w * s, h * s, s}, + {0.f, h * s, s}, + // Add XYZ axes + {fx * s, 0.f, 0.f}, + {0.f, fy * s, 0.f}, + {cx * s, cy * s, s}}); + points = (intrinsic.Inverse().Matmul(points.T()) - + extrinsic.Slice(0, 0, 3).Slice(1, 3, 4)) + .T() + .Matmul(extrinsic.Slice(0, 0, 3).Slice(1, 0, 3)); + + // Add lines for camera frame and XYZ axes + core::Tensor lines = core::Tensor::Init({{0, 1}, + {0, 2}, + {0, 3}, + {0, 4}, + {1, 2}, + {2, 3}, + {3, 4}, + {4, 1}, + // Add XYZ axes + {0, 5}, + {0, 6}, + {0, 7}}); + + LineSet lineset(points, lines); + if (color.NumElements() == 3) { + lineset.PaintUniformColor(color); + } else { + lineset.PaintUniformColor(core::Tensor::Init({0.f, 0.f, 1.f})); + } + auto &lscolors = lineset.GetLineColors(); + lscolors[8] = core::Tensor::Init({1.f, 0.f, 0.f}); // Red + lscolors[9] = core::Tensor::Init({0.f, 1.f, 0.f}); // Green + lscolors[10] = core::Tensor::Init({0.f, 0.f, 1.f}); // Blue + + return lineset; +} + } // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/open3d/t/geometry/LineSet.h b/cpp/open3d/t/geometry/LineSet.h index 6c3ecb66da8..38fb7c61be2 100644 --- a/cpp/open3d/t/geometry/LineSet.h +++ b/cpp/open3d/t/geometry/LineSet.h @@ -337,6 +337,12 @@ class LineSet : public Geometry, public DrawableGeometry { /// \return Rotated line set. LineSet &Rotate(const core::Tensor &R, const core::Tensor ¢er); + /// \brief Assigns uniform color to all lines of the LineSet. + /// + /// \param color RGB color for the LineSet. {3,} shaped Tensor. + /// Floating color values are clipped between 0.0 and 1.0. + LineSet &PaintUniformColor(const core::Tensor &color); + /// \brief Returns the device attribute of this LineSet. core::Device GetDevice() const override { return device_; } @@ -385,6 +391,22 @@ class LineSet : public Geometry, public DrawableGeometry { double scale = 1.0, bool capping = true) const; + /// Factory function to create a LineSet from intrinsic and extrinsic + /// matrices. + /// + /// \param view_width_px The width of the view, in pixels. + /// \param view_height_px The height of the view, in pixels. + /// \param intrinsic The intrinsic matrix {3,3} shape. + /// \param extrinsic The extrinsic matrix {4,4} shape. + /// \param scale camera scale + /// \param color tensor with float32 dtype and shape {3}. Default is blue. + static LineSet CreateCameraVisualization(int view_width_px, + int view_height_px, + const core::Tensor &intrinsic, + const core::Tensor &extrinsic, + double scale, + const core::Tensor &color = {}); + protected: core::Device device_ = core::Device("CPU:0"); TensorMap point_attr_; diff --git a/cpp/open3d/t/geometry/RaycastingScene.cpp b/cpp/open3d/t/geometry/RaycastingScene.cpp index af7dd3f1480..14f9962c26c 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.cpp +++ b/cpp/open3d/t/geometry/RaycastingScene.cpp @@ -12,7 +12,7 @@ #include "open3d/t/geometry/RaycastingScene.h" // This header is in the embree src dir (embree/src/ext_embree/..). -#include +#include #include #include @@ -61,7 +61,7 @@ void AssertTensorDtypeLastDimDeviceMinNDim(const open3d::core::Tensor& tensor, } struct CountIntersectionsContext { - RTCIntersectContext context; + RTCRayQueryContext context; std::vector>* previous_geom_prim_ID_tfar; int* intersections; @@ -111,7 +111,7 @@ void CountIntersectionsFunc(const RTCFilterFunctionNArguments* args) { } struct ListIntersectionsContext { - RTCIntersectContext context; + RTCRayQueryContext context; std::vector>* previous_geom_prim_ID_tfar; unsigned int* ray_ids; @@ -360,9 +360,6 @@ struct RaycastingScene::Impl { const int nthreads) { CommitScene(); - struct RTCIntersectContext context; - rtcInitIntersectContext(&context); - auto LoopFn = [&](const tbb::blocked_range& range) { std::vector rayhits(range.size()); @@ -387,15 +384,14 @@ struct RaycastingScene::Impl { } else { rh.ray.tfar = std::numeric_limits::infinity(); } - rh.ray.mask = 0; + rh.ray.mask = -1; rh.ray.id = i - range.begin(); rh.ray.flags = 0; rh.hit.geomID = RTC_INVALID_GEOMETRY_ID; rh.hit.instID[0] = RTC_INVALID_GEOMETRY_ID; - } - rtcIntersect1M(scene_, &context, &rayhits[0], range.size(), - sizeof(RTCRayHit)); + rtcIntersect1(scene_, &rh); + } for (size_t i = range.begin(); i < range.end(); ++i) { RTCRayHit rh = rayhits[i - range.begin()]; @@ -446,8 +442,12 @@ struct RaycastingScene::Impl { const int nthreads) { CommitScene(); - struct RTCIntersectContext context; - rtcInitIntersectContext(&context); + struct RTCRayQueryContext context; + rtcInitRayQueryContext(&context); + + RTCOccludedArguments args; + rtcInitOccludedArguments(&args); + args.context = &context; auto LoopFn = [&](const tbb::blocked_range& range) { std::vector rayvec(range.size()); @@ -462,13 +462,12 @@ struct RaycastingScene::Impl { ray.dir_z = r[5]; ray.tnear = tnear; ray.tfar = tfar; - ray.mask = 0; + ray.mask = -1; ray.id = i - range.begin(); ray.flags = 0; - } - rtcOccluded1M(scene_, &context, &rayvec[0], range.size(), - sizeof(RTCRay)); + rtcOccluded1(scene_, &ray, &args); + } for (size_t i = range.begin(); i < range.end(); ++i) { RTCRay ray = rayvec[i - range.begin()]; @@ -508,11 +507,15 @@ struct RaycastingScene::Impl { 0.f)); CountIntersectionsContext context; - rtcInitIntersectContext(&context.context); - context.context.filter = CountIntersectionsFunc; + rtcInitRayQueryContext(&context.context); context.previous_geom_prim_ID_tfar = &previous_geom_prim_ID_tfar; context.intersections = intersections; + RTCIntersectArguments args; + rtcInitIntersectArguments(&args); + args.filter = CountIntersectionsFunc; + args.context = &context.context; + auto LoopFn = [&](const tbb::blocked_range& range) { std::vector rayhits(range.size()); @@ -527,14 +530,14 @@ struct RaycastingScene::Impl { rh->ray.dir_z = r[5]; rh->ray.tnear = 0; rh->ray.tfar = std::numeric_limits::infinity(); - rh->ray.mask = 0; + rh->ray.mask = -1; rh->ray.flags = 0; rh->ray.id = i; rh->hit.geomID = RTC_INVALID_GEOMETRY_ID; rh->hit.instID[0] = RTC_INVALID_GEOMETRY_ID; + + rtcIntersect1(scene_, rh, &args); } - rtcIntersect1M(scene_, &context.context, &rayhits[0], range.size(), - sizeof(RTCRayHit)); }; if (nthreads > 0) { @@ -579,8 +582,7 @@ struct RaycastingScene::Impl { 0.f)); ListIntersectionsContext context; - rtcInitIntersectContext(&context.context); - context.context.filter = ListIntersectionsFunc; + rtcInitRayQueryContext(&context.context); context.previous_geom_prim_ID_tfar = &previous_geom_prim_ID_tfar; context.ray_ids = ray_ids; context.geometry_ids = geometry_ids; @@ -590,6 +592,11 @@ struct RaycastingScene::Impl { context.cumsum = cumsum; context.track_intersections = track_intersections; + RTCIntersectArguments args; + rtcInitIntersectArguments(&args); + args.filter = ListIntersectionsFunc; + args.context = &context.context; + auto LoopFn = [&](const tbb::blocked_range& range) { std::vector rayhits(range.size()); @@ -604,14 +611,14 @@ struct RaycastingScene::Impl { rh->ray.dir_z = r[5]; rh->ray.tnear = 0; rh->ray.tfar = std::numeric_limits::infinity(); - rh->ray.mask = 0; + rh->ray.mask = -1; rh->ray.flags = 0; rh->ray.id = i; rh->hit.geomID = RTC_INVALID_GEOMETRY_ID; rh->hit.instID[0] = RTC_INVALID_GEOMETRY_ID; + + rtcIntersect1(scene_, rh, &args); } - rtcIntersect1M(scene_, &context.context, &rayhits[0], range.size(), - sizeof(RTCRayHit)); }; if (nthreads > 0) { @@ -695,9 +702,9 @@ RaycastingScene::RaycastingScene(int64_t nthreads) impl_->scene_ = rtcNewScene(impl_->device_); // set flag for better accuracy - rtcSetSceneFlags( - impl_->scene_, - RTC_SCENE_FLAG_ROBUST | RTC_SCENE_FLAG_CONTEXT_FILTER_FUNCTION); + rtcSetSceneFlags(impl_->scene_, + RTC_SCENE_FLAG_ROBUST | + RTC_SCENE_FLAG_FILTER_FUNCTION_IN_ARGUMENTS); impl_->devprop_join_commit = rtcGetDeviceProperty( impl_->device_, RTC_DEVICE_PROPERTY_JOIN_COMMIT_SUPPORTED); @@ -746,6 +753,7 @@ uint32_t RaycastingScene::AddTriangles(const core::Tensor& vertex_positions, memcpy(index_buffer, data.GetDataPtr(), sizeof(uint32_t) * 3 * num_triangles); } + rtcSetGeometryEnableFilterFunctionFromArguments(geom, true); rtcCommitGeometry(geom); uint32_t geom_id = rtcAttachGeometry(impl_->scene_, geom); diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index be5442b6428..43899aaf58f 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -377,6 +377,7 @@ geometry::TriangleMesh TriangleMesh::FromLegacy( tmat.SetAnisotropy(mat.baseAnisotropy); tmat.SetBaseClearcoat(mat.baseClearCoat); tmat.SetBaseClearcoatRoughness(mat.baseClearCoatRoughness); + // no emissive_color in legacy mesh material if (mat.albedo) tmat.SetAlbedoMap(Image::FromLegacy(*mat.albedo)); if (mat.normalMap) tmat.SetNormalMap(Image::FromLegacy(*mat.normalMap)); if (mat.roughness) @@ -453,10 +454,6 @@ open3d::geometry::TriangleMesh TriangleMesh::ToLegacy() const { legacy_mat.baseColor.f4[1] = tmat.GetBaseColor().y(); legacy_mat.baseColor.f4[2] = tmat.GetBaseColor().z(); legacy_mat.baseColor.f4[3] = tmat.GetBaseColor().w(); - utility::LogWarning("{},{},{},{}", legacy_mat.baseColor.f4[0], - legacy_mat.baseColor.f4[1], - legacy_mat.baseColor.f4[2], - legacy_mat.baseColor.f4[3]); } if (tmat.HasBaseRoughness()) { legacy_mat.baseRoughness = tmat.GetBaseRoughness(); @@ -523,6 +520,26 @@ open3d::geometry::TriangleMesh TriangleMesh::ToLegacy() const { return mesh_legacy; } +std::unordered_map +TriangleMesh::FromTriangleMeshModel( + const open3d::visualization::rendering::TriangleMeshModel &model, + core::Dtype float_dtype, + core::Dtype int_dtype, + const core::Device &device) { + std::unordered_map tmeshes; + for (const auto &mobj : model.meshes_) { + auto tmesh = TriangleMesh::FromLegacy(*mobj.mesh, float_dtype, + int_dtype, device); + // material textures will be on the CPU. GPU resident texture images is + // not yet supported. See comment in Material.cpp + tmesh.SetMaterial( + visualization::rendering::Material::FromMaterialRecord( + model.materials_[mobj.material_idx])); + tmeshes.emplace(mobj.mesh_name, tmesh); + } + return tmeshes; +} + TriangleMesh TriangleMesh::To(const core::Device &device, bool copy) const { if (!copy && GetDevice() == device) { return *this; @@ -1254,6 +1271,61 @@ TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const { return result; } +TriangleMesh TriangleMesh::RemoveUnreferencedVertices() { + if (!HasVertexPositions() || GetVertexPositions().GetLength() == 0) { + utility::LogWarning( + "[RemoveUnreferencedVertices] TriangleMesh has no vertices."); + return *this; + } + GetVertexAttr().AssertSizeSynchronized(); + + core::Dtype tri_dtype = HasTriangleIndices() + ? GetTriangleIndices().GetDtype() + : core::Int64; + + int64_t num_verts_old = GetVertexPositions().GetLength(); + // int mask for vertices as we need to remap indices. + core::Tensor vertex_mask = core::Tensor::Zeros({num_verts_old}, tri_dtype); + + if (!HasTriangleIndices() || GetTriangleIndices().GetLength() == 0) { + utility::LogWarning( + "[RemoveUnreferencedVertices] TriangleMesh has no triangles. " + "Removing all vertices."); + // in this case we need to empty vertices and their attributes + } else { + GetTriangleAttr().AssertSizeSynchronized(); + core::Tensor tris_cpu = + GetTriangleIndices().To(core::Device()).Contiguous(); + DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(tri_dtype, tris, [&]() { + scalar_tris_t *tris_ptr = tris_cpu.GetDataPtr(); + scalar_tris_t *vertex_mask_ptr = + vertex_mask.GetDataPtr(); + for (int i = 0; i < tris_cpu.GetLength(); i++) { + vertex_mask_ptr[tris_ptr[3 * i]] = 1; + vertex_mask_ptr[tris_ptr[3 * i + 1]] = 1; + vertex_mask_ptr[tris_ptr[3 * i + 2]] = 1; + } + + UpdateTriangleIndicesByVertexMask(tris_cpu, + vertex_mask); + }); + SetTriangleIndices(tris_cpu.To(GetDevice())); + } + + // send the vertex mask to original device and apply to + // vertices + vertex_mask = vertex_mask.To(GetDevice(), core::Bool); + for (auto item : GetVertexAttr()) { + SetVertexAttr(item.first, item.second.IndexGet({vertex_mask})); + } + + utility::LogDebug( + "[RemoveUnreferencedVertices] {:d} vertices have been removed.", + (int)(num_verts_old - GetVertexPositions().GetLength())); + + return *this; +} + } // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 7824f193b1c..fc592b0a9f9 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include "open3d/core/Tensor.h" #include "open3d/core/TensorCheck.h" @@ -16,6 +17,7 @@ #include "open3d/t/geometry/DrawableGeometry.h" #include "open3d/t/geometry/Geometry.h" #include "open3d/t/geometry/TensorMap.h" +#include "open3d/visualization/rendering/Model.h" namespace open3d { namespace t { @@ -701,7 +703,8 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// values, e.g. vertices, normals, colors. /// \param int_dtype Int32 or Int64, used to store index values, e.g. /// triangles. - /// \param device The device where the resulting TriangleMesh resides in. + /// \param device The device where the resulting TriangleMesh resides in + /// (default CPU:0). static geometry::TriangleMesh FromLegacy( const open3d::geometry::TriangleMesh &mesh_legacy, core::Dtype float_dtype = core::Float32, @@ -711,6 +714,28 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// Convert to a legacy Open3D TriangleMesh. open3d::geometry::TriangleMesh ToLegacy() const; + /// Convert a TriangleMeshModel (e.g. as read from a file with + /// open3d::io::ReadTriangleMeshModel) to an unordered map of mesh names to + /// TriangleMeshes. Only one material is supported per mesh. Materials + /// common to multiple meshes will be dupicated. Textures (as + /// t::geometry::Image) will use shared storage. + /// \param model TriangleMeshModel to convert. + /// \param float_dtype Float32 or Float64, used to store floating point + /// values, e.g. vertices, normals, colors. + /// \param int_dtype Int32 or Int64, used to store index values, e.g. + /// triangles. + /// \param device The device where the resulting TriangleMesh resides in + /// (default CPU:0). Material textures use CPU storage - GPU resident + /// texture images are not yet supported. + /// \return unordered map of constituent mesh names to TriangleMeshes, with + /// materials. + static std::unordered_map + FromTriangleMeshModel( + const open3d::visualization::rendering::TriangleMeshModel &model, + core::Dtype float_dtype = core::Float32, + core::Dtype int_dtype = core::Int64, + const core::Device &device = core::Device("CPU:0")); + /// Compute the convex hull of the triangle mesh using qhull. /// /// This runs on the CPU. @@ -945,6 +970,10 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// an empty mesh. TriangleMesh SelectByIndex(const core::Tensor &indices) const; + /// Removes unreferenced vertices from the mesh. + /// \return The reference to itself. + TriangleMesh RemoveUnreferencedVertices(); + protected: core::Device device_ = core::Device("CPU:0"); TensorMap vertex_attr_; diff --git a/cpp/open3d/t/io/ImageIO.cpp b/cpp/open3d/t/io/ImageIO.cpp index df1ca3625ce..d172ad16cb7 100644 --- a/cpp/open3d/t/io/ImageIO.cpp +++ b/cpp/open3d/t/io/ImageIO.cpp @@ -8,7 +8,9 @@ #include "open3d/t/io/ImageIO.h" #include +#include #include +#include #include #include @@ -23,14 +25,14 @@ namespace open3d { namespace t { namespace io { -static const std::unordered_map< - std::string, - std::function> - file_extension_to_image_read_function{ - {"png", ReadImageFromPNG}, - {"jpg", ReadImageFromJPG}, - {"jpeg", ReadImageFromJPG}, - }; +namespace { +using signature_decoder_t = + std::pair>; +static const std::array signature_decoder_list{ + {{"\x89\x50\x4e\x47\xd\xa\x1a\xa", ReadImageFromPNG}, + {"\xFF\xD8\xFF", ReadImageFromJPG}}}; +static constexpr uint8_t MAX_SIGNATURE_LEN = 8; static const std::unordered_map< std::string, @@ -40,6 +42,7 @@ static const std::unordered_map< {"jpg", WriteImageToJPG}, {"jpeg", WriteImageToJPG}, }; +} // namespace std::shared_ptr CreateImageFromFile( const std::string &filename) { @@ -49,21 +52,27 @@ std::shared_ptr CreateImageFromFile( } bool ReadImage(const std::string &filename, geometry::Image &image) { - std::string filename_ext = - utility::filesystem::GetFileExtensionInLowerCase(filename); - if (filename_ext.empty()) { - utility::LogWarning( - "Read geometry::Image failed: missing file extension."); - return false; - } - auto map_itr = file_extension_to_image_read_function.find(filename_ext); - if (map_itr == file_extension_to_image_read_function.end()) { - utility::LogWarning( - "Read geometry::Image failed: file extension {} unknown", - filename_ext); - return false; + std::string signature_buffer(MAX_SIGNATURE_LEN, 0); + std::ifstream file(filename, std::ios::binary); + file.read(&signature_buffer[0], MAX_SIGNATURE_LEN); + std::string err_msg; + if (!file) { + err_msg = "Read geometry::Image failed for file {}. I/O error."; + } else { + file.close(); + for (const auto &signature_decoder : signature_decoder_list) { + if (signature_buffer.compare(0, signature_decoder.first.size(), + signature_decoder.first) == 0) { + return signature_decoder.second(filename, image); + } + } + err_msg = + "Read geometry::Image failed for file {}. Unknown file " + "signature, only PNG and JPG are supported."; } - return map_itr->second(filename, image); + image.Clear(); + utility::LogWarning(err_msg.c_str(), filename); + return false; } bool WriteImage(const std::string &filename, @@ -99,7 +108,8 @@ DepthNoiseSimulator::DepthNoiseSimulator(const std::string &noise_model_path) { for (int i = 0; i < skip_first_n_lines; ++i) { if (!(line_buffer = file.ReadLine())) { utility::LogError( - "Read depth model failed: file {} is less than {} lines.", + "Read depth model failed: file {} is less than {} " + "lines.", noise_model_path, skip_first_n_lines); } } @@ -182,11 +192,11 @@ geometry::Image DepthNoiseSimulator::Simulate(const geometry::Image &im_src, geometry::kernel::TArrayIndexer dst_indexer(im_dst_tensor, 2); geometry::kernel::TArrayIndexer model_indexer(model_, 3); - // To match the original implementation, we try to keep the same variable - // names with reference to the original code. Compared to the original - // implementation, parallelization is done in im_dst_tensor per-pixel level, - // instead of per-image level. Check out the original code at: - // http://redwood-data.org/indoor/data/simdepth.py. + // To match the original implementation, we try to keep the same + // variable names with reference to the original code. Compared to the + // original implementation, parallelization is done in im_dst_tensor + // per-pixel level, instead of per-image level. Check out the original + // code at: http://redwood-data.org/indoor/data/simdepth.py. core::ParallelFor( core::Device("CPU:0"), width * height, [&] OPEN3D_DEVICE(int workload_idx) { diff --git a/cpp/open3d/t/io/file_format/FileASSIMP.cpp b/cpp/open3d/t/io/file_format/FileASSIMP.cpp index 5eae7a9ffdc..514d59fca1b 100644 --- a/cpp/open3d/t/io/file_format/FileASSIMP.cpp +++ b/cpp/open3d/t/io/file_format/FileASSIMP.cpp @@ -6,6 +6,7 @@ // ---------------------------------------------------------------------------- #include +#include #include #include @@ -357,12 +358,17 @@ bool WriteTriangleMeshUsingASSIMP(const std::string& filename, auto r = mesh.GetMaterial().GetBaseClearcoatRoughness(); ai_mat->AddProperty(&r, 1, AI_MATKEY_CLEARCOAT_ROUGHNESS_FACTOR); } + if (mesh.GetMaterial().HasEmissiveColor()) { + auto c = mesh.GetMaterial().GetEmissiveColor(); + auto ac = aiColor4D(c.x(), c.y(), c.z(), c.w()); + ai_mat->AddProperty(&ac, 1, AI_MATKEY_COLOR_EMISSIVE); + } // Count texture maps... // NOTE: GLTF2 expects a single combined roughness/metal map. If the // model has one we just export it, otherwise if both roughness and - // metal maps are avaialbe we combine them, otherwise if only one or the - // other is available we just export the one map. + // metal maps are available we combine them, otherwise if only one or + // the other is available we just export the one map. int n_textures = 0; if (mesh.GetMaterial().HasAlbedoMap()) ++n_textures; if (mesh.GetMaterial().HasNormalMap()) ++n_textures; diff --git a/cpp/open3d/t/io/file_format/FileJPG.cpp b/cpp/open3d/t/io/file_format/FileJPG.cpp index dc83fb3ad2e..e1830f5135f 100644 --- a/cpp/open3d/t/io/file_format/FileJPG.cpp +++ b/cpp/open3d/t/io/file_format/FileJPG.cpp @@ -19,6 +19,24 @@ namespace open3d { namespace t { namespace io { +namespace { + +/// Convert libjpeg error messages to std::runtime_error. This prevents +/// libjpeg from exit() in case of errors. +void jpeg_error_throw(j_common_ptr p_cinfo) { + if (p_cinfo->is_decompressor) + jpeg_destroy_decompress( + reinterpret_cast(p_cinfo)); + else + jpeg_destroy_compress( + reinterpret_cast(p_cinfo)); + char buffer[JMSG_LENGTH_MAX]; + (*p_cinfo->err->format_message)(p_cinfo, buffer); + throw std::runtime_error(buffer); +} + +} // namespace + bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; @@ -28,56 +46,67 @@ bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { if ((file_in = utility::filesystem::FOpen(filename, "rb")) == NULL) { utility::LogWarning("Read JPG failed: unable to open file: {}", filename); + image.Clear(); return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_stdio_src(&cinfo, file_in); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return false; - } - jpeg_start_decompress(&cinfo); - image.Clear(); - image.Reset(cinfo.output_height, cinfo.output_width, num_of_channels, - core::UInt8, image.GetDevice()); - - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = static_cast(image.GetDataPtr()); - - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - core::MemoryManager::MemcpyFromHost(pdata, image.GetDevice(), buffer[0], - row_stride * 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, file_in); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Clear(); + image.Reset(cinfo.output_height, cinfo.output_width, num_of_channels, + core::UInt8, image.GetDevice()); + + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = static_cast(image.GetDataPtr()); + + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + core::MemoryManager::MemcpyFromHost(pdata, image.GetDevice(), + buffer[0], row_stride * 1); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + return true; + } catch (const std::runtime_error &err) { + fclose(file_in); + image.Clear(); + utility::LogWarning("libjpeg error: {}", err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return true; } bool WriteImageToJPG(const std::string &filename, @@ -112,31 +141,38 @@ bool WriteImageToJPG(const std::string &filename, return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, file_out); - cinfo.image_width = image.GetCols(); - cinfo.image_height = image.GetRows(); - cinfo.input_components = image.GetChannels(); - cinfo.in_color_space = - (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); - int row_stride = image.GetCols() * image.GetChannels(); - const uint8_t *pdata = static_cast(image.GetDataPtr()); - std::vector buffer(row_stride); - while (cinfo.next_scanline < cinfo.image_height) { - core::MemoryManager::MemcpyToHost(buffer.data(), pdata, - image.GetDevice(), row_stride * 1); - row_pointer[0] = buffer.data(); - jpeg_write_scanlines(&cinfo, row_pointer, 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, file_out); + cinfo.image_width = image.GetCols(); + cinfo.image_height = image.GetRows(); + cinfo.input_components = image.GetChannels(); + cinfo.in_color_space = + (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + int row_stride = image.GetCols() * image.GetChannels(); + const uint8_t *pdata = static_cast(image.GetDataPtr()); + std::vector buffer(row_stride); + while (cinfo.next_scanline < cinfo.image_height) { + core::MemoryManager::MemcpyToHost( + buffer.data(), pdata, image.GetDevice(), row_stride * 1); + row_pointer[0] = buffer.data(); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + pdata += row_stride; + } + jpeg_finish_compress(&cinfo); + fclose(file_out); + jpeg_destroy_compress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + fclose(file_out); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_compress(&cinfo); - fclose(file_out); - jpeg_destroy_compress(&cinfo); - return true; } } // namespace io diff --git a/cpp/open3d/t/io/file_format/FilePNG.cpp b/cpp/open3d/t/io/file_format/FilePNG.cpp index 2497e2a71c8..8560bc8b431 100644 --- a/cpp/open3d/t/io/file_format/FilePNG.cpp +++ b/cpp/open3d/t/io/file_format/FilePNG.cpp @@ -41,6 +41,7 @@ bool ReadImageFromPNG(const std::string &filename, geometry::Image &image) { pngimage.version = PNG_IMAGE_VERSION; if (png_image_begin_read_from_file(&pngimage, filename.c_str()) == 0) { utility::LogWarning("Read PNG failed: unable to parse header."); + image.Clear(); return false; } @@ -64,6 +65,7 @@ bool ReadImageFromPNG(const std::string &filename, geometry::Image &image) { utility::LogWarning("Read PNG failed: unable to read file: {}", filename); utility::LogWarning("PNG error: {}", pngimage.message); + image.Clear(); return false; } return true; @@ -141,7 +143,7 @@ bool WriteImageToPNGInMemory(std::vector &buffer, buffer.resize(mem_bytes); if (png_image_write_to_memory(&pngimage, &buffer[0], &mem_bytes, 0, image.GetDataPtr(), 0, nullptr) == 0) { - utility::LogWarning("Unable to encode to encode to PNG in memory."); + utility::LogWarning("Unable to encode to PNG in memory."); return false; } return true; diff --git a/cpp/open3d/visualization/gui/PickPointsInteractor.cpp b/cpp/open3d/visualization/gui/PickPointsInteractor.cpp index 45ca7c44c1e..63ecfba7955 100644 --- a/cpp/open3d/visualization/gui/PickPointsInteractor.cpp +++ b/cpp/open3d/visualization/gui/PickPointsInteractor.cpp @@ -155,8 +155,6 @@ void PickPointsInteractor::SetPickableGeometry( // TriangleMesh so that occluded points are not selected. points_.clear(); for (auto &pg : geometry) { - lookup_->Add(pg.name, points_.size()); - auto cloud = dynamic_cast(pg.geometry); auto tcloud = dynamic_cast(pg.tgeometry); @@ -166,6 +164,12 @@ void PickPointsInteractor::SetPickableGeometry( auto lineset = dynamic_cast(pg.geometry); auto tlineset = dynamic_cast(pg.tgeometry); + + // only add geometry with pickable points + if (cloud || tcloud || mesh || tmesh || lineset || tlineset) { + lookup_->Add(pg.name, points_.size()); + } + if (cloud) { points_.insert(points_.end(), cloud->points_.begin(), cloud->points_.end()); @@ -268,35 +272,38 @@ void PickPointsInteractor::SetOnStartedPolygonPicking( } void PickPointsInteractor::Mouse(const MouseEvent &e) { - if (e.type == MouseEvent::BUTTON_UP) { - if (e.modifiers & int(KeyModifier::ALT)) { - if (pending_.empty() || pending_.back().keymods == 0) { - pending_.push({{gui::Point(e.x, e.y)}, int(KeyModifier::ALT)}); - if (on_ui_changed_) { - on_ui_changed_({}); - } - } else { - pending_.back().polygon.push_back(gui::Point(e.x, e.y)); - if (on_started_poly_pick_) { - on_started_poly_pick_(); - } - if (on_ui_changed_) { - std::vector lines; - auto &polygon = pending_.back().polygon; - for (size_t i = 1; i < polygon.size(); ++i) { - auto &p0 = polygon[i - 1]; - auto &p1 = polygon[i]; - lines.push_back({p0.x, p0.y}); - lines.push_back({p1.x, p1.y}); - } - lines.push_back({polygon.back().x, polygon.back().y}); - lines.push_back({polygon[0].x, polygon[0].y}); - on_ui_changed_(lines); - } + if (e.type != MouseEvent::BUTTON_UP) return; + + bool polygon_picking_requested = e.modifiers & int(KeyModifier::ALT); + if (!polygon_picking_requested) { + // standard point picking + pending_.push({{gui::Point(e.x, e.y)}, e.modifiers}); + DoPick(); + } else { + // special polygon picking mode + if (pending_.empty() || pending_.back().keymods == 0) { + pending_.push({{gui::Point(e.x, e.y)}, e.modifiers}); + if (on_ui_changed_) { + on_ui_changed_({}); } } else { - pending_.push({{gui::Point(e.x, e.y)}, 0}); - DoPick(); + pending_.back().polygon.push_back(gui::Point(e.x, e.y)); + if (on_started_poly_pick_) { + on_started_poly_pick_(); + } + if (on_ui_changed_) { + std::vector lines; + auto &polygon = pending_.back().polygon; + for (size_t i = 1; i < polygon.size(); ++i) { + auto &p0 = polygon[i - 1]; + auto &p1 = polygon[i]; + lines.push_back({p0.x, p0.y}); + lines.push_back({p1.x, p1.y}); + } + lines.push_back({polygon.back().x, polygon.back().y}); + lines.push_back({polygon[0].x, polygon[0].y}); + on_ui_changed_(lines); + } } } } diff --git a/cpp/open3d/visualization/rendering/Material.cpp b/cpp/open3d/visualization/rendering/Material.cpp index fad82623382..d264711f987 100644 --- a/cpp/open3d/visualization/rendering/Material.cpp +++ b/cpp/open3d/visualization/rendering/Material.cpp @@ -26,6 +26,7 @@ void Material::SetDefaultProperties() { SetTransmission(1.f); SetAbsorptionColor(Eigen::Vector4f(1.f, 1.f, 1.f, 1.f)); SetAbsorptionDistance(1.f); + SetEmissiveColor(Eigen::Vector4f(1.f, 1.f, 1.f, 1.f)); SetPointSize(3.f); SetLineWidth(1.f); } @@ -39,6 +40,24 @@ void Material::SetTextureMap(const std::string &key, texture_maps_[key] = image.To(core::Device("CPU:0"), true); } +std::string Material::ToString() const { + if (!IsValid()) { + return "Invalid Material\n"; + } + std::ostringstream os; + os << "Material " << material_name_ << '\n'; + for (const auto &kv : scalar_properties_) { + os << '\t' << kv.first << ": " << kv.second << '\n'; + } + for (const auto &kv : vector_properties_) { + os << '\t' << kv.first << ": " << kv.second.transpose() << '\n'; + } + for (const auto &kv : texture_maps_) { + os << '\t' << kv.first << ": " << kv.second.ToString() << '\n'; + } + return os.str(); +} + void Material::ToMaterialRecord(MaterialRecord &record) const { record.shader = GetMaterialName(); // Convert base material properties @@ -63,6 +82,9 @@ void Material::ToMaterialRecord(MaterialRecord &record) const { if (HasAnisotropy()) { record.base_anisotropy = GetAnisotropy(); } + if (HasEmissiveColor()) { + record.emissive_color = GetEmissiveColor(); + } if (HasThickness()) { record.thickness = GetThickness(); } @@ -124,6 +146,62 @@ void Material::ToMaterialRecord(MaterialRecord &record) const { } } +Material Material::FromMaterialRecord(const MaterialRecord &record) { + using t::geometry::Image; + Material tmat(record.shader); + // scalar and vector properties + tmat.SetBaseColor(record.base_color); + tmat.SetBaseMetallic(record.base_metallic); + tmat.SetBaseRoughness(record.base_roughness); + tmat.SetBaseReflectance(record.base_reflectance); + tmat.SetBaseClearcoat(record.base_clearcoat); + tmat.SetBaseClearcoatRoughness(record.base_clearcoat_roughness); + tmat.SetAnisotropy(record.base_anisotropy); + tmat.SetEmissiveColor(record.emissive_color); + // refractive materials + tmat.SetThickness(record.thickness); + tmat.SetTransmission(record.transmission); + tmat.SetAbsorptionDistance(record.absorption_distance); + // points and lines + tmat.SetPointSize(record.point_size); + tmat.SetLineWidth(record.line_width); + // maps + if (record.albedo_img) { + tmat.SetAlbedoMap(Image::FromLegacy(*record.albedo_img)); + } + if (record.normal_img) { + tmat.SetNormalMap(Image::FromLegacy(*record.normal_img)); + } + if (record.ao_img) { + tmat.SetAOMap(Image::FromLegacy(*record.ao_img)); + } + if (record.metallic_img) { + tmat.SetMetallicMap(Image::FromLegacy(*record.metallic_img)); + } + if (record.roughness_img) { + tmat.SetRoughnessMap(Image::FromLegacy(*record.roughness_img)); + } + if (record.reflectance_img) { + tmat.SetReflectanceMap(Image::FromLegacy(*record.reflectance_img)); + } + if (record.clearcoat_img) { + tmat.SetClearcoatMap(Image::FromLegacy(*record.clearcoat_img)); + } + if (record.clearcoat_roughness_img) { + tmat.SetClearcoatRoughnessMap( + Image::FromLegacy(*record.clearcoat_roughness_img)); + } + if (record.anisotropy_img) { + tmat.SetAnisotropyMap(Image::FromLegacy(*record.anisotropy_img)); + } + if (record.ao_rough_metal_img) { + tmat.SetAORoughnessMetalMap( + Image::FromLegacy(*record.ao_rough_metal_img)); + } + + return tmat; +} + } // namespace rendering } // namespace visualization } // namespace open3d diff --git a/cpp/open3d/visualization/rendering/Material.h b/cpp/open3d/visualization/rendering/Material.h index 7ee1396b281..27afcd8a69a 100644 --- a/cpp/open3d/visualization/rendering/Material.h +++ b/cpp/open3d/visualization/rendering/Material.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include "open3d/t/geometry/Image.h" @@ -34,6 +35,9 @@ class Material { Material(const Material &mat) = default; + /// Convert from MaterialRecord + static Material FromMaterialRecord(const MaterialRecord &mat); + Material &operator=(const Material &other) = default; /// Create an empty but valid material for the specified material name @@ -51,6 +55,9 @@ class Material { /// Get the name of the material. const std::string &GetMaterialName() const { return material_name_; } + /// String reprentation for printing. + std::string ToString() const; + /// Returns the texture map map const TextureMaps &GetTextureMaps() const { return texture_maps_; } @@ -249,6 +256,9 @@ class Material { float GetAbsorptionDistance() const { return GetScalarProperty("absorption_distance"); } + Eigen::Vector4f GetEmissiveColor() const { + return GetVectorProperty("emissive_color"); + } bool HasBaseColor() const { return HasVectorProperty("base_color"); } bool HasBaseMetallic() const { return HasScalarProperty("metallic"); } @@ -267,6 +277,9 @@ class Material { bool HasAbsorptionDistance() const { return HasScalarProperty("absorption_distance"); } + bool HasEmissiveColor() const { + return HasVectorProperty("emissive_color"); + } void SetBaseColor(const Eigen::Vector4f &value) { SetVectorProperty("base_color", value); @@ -295,6 +308,9 @@ class Material { void SetAbsorptionDistance(float value) { SetScalarProperty("absorption_distance", value); } + void SetEmissiveColor(const Eigen::Vector4f &value) { + SetVectorProperty("emissive_color", value); + } //////////////////////////////////////////////////////////////////////////// /// diff --git a/cpp/open3d/visualization/visualizer/O3DVisualizer.cpp b/cpp/open3d/visualization/visualizer/O3DVisualizer.cpp index 3ab06cec1b9..554d38b2466 100644 --- a/cpp/open3d/visualization/visualizer/O3DVisualizer.cpp +++ b/cpp/open3d/visualization/visualizer/O3DVisualizer.cpp @@ -384,7 +384,9 @@ struct O3DVisualizer::Impl { std::vector>> &indices, int keymods) { - if ((keymods & int(KeyModifier::SHIFT)) || + bool unselect_mode_requested = + keymods & int(KeyModifier::SHIFT); + if (unselect_mode_requested || polygon_selection_unselects_) { selections_->UnselectIndices(indices); } else { @@ -488,11 +490,13 @@ struct O3DVisualizer::Impl { }); #if __APPLE__ - const char *selection_help = - "Cmd-click to select a point\nCmd-ctrl-click to polygon select"; + const char *selection_help = R"(Cmd-click to select a point +Cmd-shift-click to deselect a point +Cmd-alt-click to polygon select)"; #else - const char *selection_help = - "Ctrl-click to select a point\nCmd-alt-click to polygon select"; + const char *selection_help = R"(Ctrl-click to select a point +Ctrl-shift-click to deselect a point +Ctrl-alt-click to polygon select)"; #endif // __APPLE__ h = new Horiz(); h->AddStretch(); @@ -1545,12 +1549,10 @@ struct O3DVisualizer::Impl { OverrideMaterial(o.name, o.material, ui_state_.scene_shader); } - auto bbox = scene_->GetScene()->GetBoundingBox(); - auto xdim = bbox.max_bound_.x() - bbox.min_bound_.x(); - auto ydim = bbox.max_bound_.y() - bbox.min_bound_.z(); - auto zdim = bbox.max_bound_.z() - bbox.min_bound_.y(); + auto bbox_extend = scene_->GetScene()->GetBoundingBox().GetExtent(); auto psize = double(std::max(5, px)) * 0.000666 * - std::max(xdim, std::max(ydim, zdim)); + std::max(bbox_extend.x(), + std::max(bbox_extend.y(), bbox_extend.z())); selections_->SetPointSize(psize); scene_->SetPickablePointSize(px); diff --git a/cpp/open3d/visualization/visualizer/ViewControl.cpp b/cpp/open3d/visualization/visualizer/ViewControl.cpp index a4dff55a64a..a4ba2aae314 100644 --- a/cpp/open3d/visualization/visualizer/ViewControl.cpp +++ b/cpp/open3d/visualization/visualizer/ViewControl.cpp @@ -182,8 +182,8 @@ bool ViewControl::ConvertFromPinholeCameraParameters( window_width_ != intrinsic.width_ || std::abs(intrinsic.intrinsic_matrix_(0, 2) - ((double)window_width_ / 2.0 - 0.5)) > threshold || - std::abs(intrinsic.intrinsic_matrix_(1, 2) = - ((double)window_height_ / 2.0 - 0.5)) > threshold)) { + std::abs(intrinsic.intrinsic_matrix_(1, 2) - + ((double)window_height_ / 2.0 - 0.5)) > threshold)) { utility::LogWarning( "[ViewControl] ConvertFromPinholeCameraParameters() failed " "because window height and width do not match."); diff --git a/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.cpp b/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.cpp index a4845166b8a..6fd167b73e5 100644 --- a/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.cpp +++ b/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.cpp @@ -26,6 +26,13 @@ void VisualizerWithKeyCallback::PrintVisualizerHelp() { utility::LogInfo( " The default functions of these keys will be overridden."); utility::LogInfo(""); + + std::string mouse_callbacks = (mouse_move_callback_ ? "MouseMove, " : ""); + mouse_callbacks += (mouse_scroll_callback_ ? "MouseScroll, " : ""); + mouse_callbacks += (mouse_button_callback_ ? "MouseButton, " : ""); + utility::LogInfo(" Custom mouse callbacks registered for: {}", + mouse_callbacks.substr(0, mouse_callbacks.size() - 2)); + utility::LogInfo(""); } void VisualizerWithKeyCallback::RegisterKeyCallback( @@ -38,6 +45,21 @@ void VisualizerWithKeyCallback::RegisterKeyActionCallback( key_action_to_callback_[key] = callback; } +void VisualizerWithKeyCallback::RegisterMouseMoveCallback( + std::function callback) { + mouse_move_callback_ = callback; +} + +void VisualizerWithKeyCallback::RegisterMouseScrollCallback( + std::function callback) { + mouse_scroll_callback_ = callback; +} + +void VisualizerWithKeyCallback::RegisterMouseButtonCallback( + std::function callback) { + mouse_button_callback_ = callback; +} + void VisualizerWithKeyCallback::KeyPressCallback( GLFWwindow *window, int key, int scancode, int action, int mods) { auto action_callback = key_action_to_callback_.find(key); @@ -63,6 +85,46 @@ void VisualizerWithKeyCallback::KeyPressCallback( } } +void VisualizerWithKeyCallback::MouseMoveCallback(GLFWwindow *window, + double x, + double y) { + if (mouse_move_callback_) { + if (mouse_move_callback_(this, x, y)) { + UpdateGeometry(); + } + UpdateRender(); + } else { + Visualizer::MouseMoveCallback(window, x, y); + } +} + +void VisualizerWithKeyCallback::MouseScrollCallback(GLFWwindow *window, + double x, + double y) { + if (mouse_scroll_callback_) { + if (mouse_scroll_callback_(this, x, y)) { + UpdateGeometry(); + } + UpdateRender(); + } else { + Visualizer::MouseScrollCallback(window, x, y); + } +} + +void VisualizerWithKeyCallback::MouseButtonCallback(GLFWwindow *window, + int button, + int action, + int mods) { + if (mouse_button_callback_) { + if (mouse_button_callback_(this, button, action, mods)) { + UpdateGeometry(); + } + UpdateRender(); + } else { + Visualizer::MouseButtonCallback(window, button, action, mods); + } +} + std::string VisualizerWithKeyCallback::PrintKeyToString(int key) { if (key == GLFW_KEY_SPACE) { // 32 return std::string("Space"); diff --git a/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.h b/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.h index 92274978d81..add4e265edc 100644 --- a/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.h +++ b/cpp/open3d/visualization/visualizer/VisualizerWithKeyCallback.h @@ -33,6 +33,7 @@ class VisualizerWithKeyCallback : public Visualizer { void PrintVisualizerHelp() override; void RegisterKeyCallback(int key, std::function callback); + /// Register callback function with access to GLFW key actions. /// /// \param key GLFW key value, see [GLFW key @@ -48,18 +49,61 @@ class VisualizerWithKeyCallback : public Visualizer { void RegisterKeyActionCallback( int key, std::function callback); + /// Register callback function with access to GLFW mouse actions. + /// + /// \param callback The callback function. The callback function takes + /// `Visualizer *`, and the x and y mouse position inside the window and + /// returns a boolean indicating if `UpdateGeometry()` needs to be run. See + /// [GLFW mouse + /// position](https://www.glfw.org/docs/latest/input_guide.html#input_mouse) + /// for more details. + void RegisterMouseMoveCallback( + std::function callback); + + /// Register callback function with access to GLFW mouse actions. + /// + /// \param callback The callback function. The callback function takes + /// `Visualizer *`, and the x and y scroll values and returns a boolean + /// indicating if `UpdateGeometry()` needs to be run. A normal mouse only + /// provides a y scroll value. See [GLFW mouse + /// scrolling](https://www.glfw.org/docs/latest/input_guide.html#scrolling) + /// for more details. + void RegisterMouseScrollCallback( + std::function callback); + + /// Register callback function with access to GLFW mouse actions. + /// + /// \param callback The callback function. The callback function takes + /// `Visualizer *`, `button`, `action` and `mods` as input and returns a + /// boolean indicating UpdateGeometry() needs to be run. The `action` can be + /// one of GLFW_RELEASE (0), GLFW_PRESS (1) or GLFW_REPEAT (2), see [GLFW + /// input interface](https://www.glfw.org/docs/latest/group__input.html). + /// The `mods` specifies the modifier key, see [GLFW modifier + /// key](https://www.glfw.org/docs/latest/group__mods.html). + void RegisterMouseButtonCallback( + std::function callback); + protected: void KeyPressCallback(GLFWwindow *window, int key, int scancode, int action, int mods) override; + void MouseMoveCallback(GLFWwindow *window, double x, double y) override; + void MouseScrollCallback(GLFWwindow *window, double x, double y) override; + void MouseButtonCallback(GLFWwindow *window, + int button, + int action, + int mods) override; std::string PrintKeyToString(int key); protected: std::map> key_to_callback_; std::map> key_action_to_callback_; + std::function mouse_move_callback_; + std::function mouse_scroll_callback_; + std::function mouse_button_callback_; }; } // namespace visualization diff --git a/cpp/pybind/pipelines/registration/registration.cpp b/cpp/pybind/pipelines/registration/registration.cpp index f5165537fea..cddcb7e7e80 100644 --- a/cpp/pybind/pipelines/registration/registration.cpp +++ b/cpp/pybind/pipelines/registration/registration.cpp @@ -351,7 +351,7 @@ Sets :math:`c = 1` if ``with_scaling`` is ``False``. "Check if two point clouds build the polygons with similar " "edge lengths. That is, checks if the lengths of any two " "arbitrary edges (line formed by two vertices) individually " - "drawn withinin source point cloud and within the target " + "drawn within the source point cloud and within the target " "point cloud with correspondences are similar. The only " "parameter similarity_threshold is a number between 0 " "(loose) and 1 (strict)"); diff --git a/cpp/pybind/t/geometry/lineset.cpp b/cpp/pybind/t/geometry/lineset.cpp index d9bf8adbe43..299deef36b0 100644 --- a/cpp/pybind/t/geometry/lineset.cpp +++ b/cpp/pybind/t/geometry/lineset.cpp @@ -299,6 +299,37 @@ transformation as :math:`P = R(P) + t`)"); mesh = lines.extrude_linear([0,1,0]) o3d.visualization.draw([{'name': 'L', 'geometry': mesh}]) +)"); + line_set.def("paint_uniform_color", &LineSet::PaintUniformColor, "color"_a, + "Assigns unifom color to all the lines of the LineSet. " + "Floating color values are clipped between 00 and 1.0. Input " + "`color` should be a (3,) shape tensor."); + line_set.def_static( + "create_camera_visualization", &LineSet::CreateCameraVisualization, + "view_width_px"_a, "view_height_px"_a, "intrinsic"_a, "extrinsic"_a, + "scale"_a = 1.f, "color"_a = core::Tensor({}, core::Float32), + R"(Factory function to create a LineSet from intrinsic and extrinsic +matrices. Camera reference frame is shown with XYZ axes in RGB. + +Args: + view_width_px (int): The width of the view, in pixels. + view_height_px (int): The height of the view, in pixels. + intrinsic (open3d.core.Tensor): The intrinsic matrix {3,3} shape. + extrinsic (open3d.core.Tensor): The extrinsic matrix {4,4} shape. + scale (float): camera scale + color (open3d.core.Tensor): color with float32 and shape {3}. Default is blue. + +Example: + + Draw a purple camera frame with XYZ axes in RGB. + + import open3d.core as o3c + from open3d.t.geometry import LineSet + from open3d.visualization import draw + K = o3c.Tensor([[512, 0, 512], [0, 512, 512], [0, 0, 1]], dtype=o3c.float32) + T = o3c.Tensor.eye(4, dtype=o3c.float32) + ls = LineSet.create_camera_visualization(1024, 1024, K, T, 1, [0.8, 0.2, 0.8]) + draw([ls]) )"); } diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 6285b00c6b3..96307fddc91 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -238,6 +238,28 @@ The attributes of the triangle mesh have different levels:: "vertex_dtype"_a = core::Float32, "triangle_dtype"_a = core::Int64, "device"_a = core::Device("CPU:0"), "Create a TriangleMesh from a legacy Open3D TriangleMesh."); + triangle_mesh.def_static( + "from_triangle_mesh_model", &TriangleMesh::FromTriangleMeshModel, + "model"_a, "vertex_dtype"_a = core::Float32, + "triangle_dtype"_a = core::Int64, + "device"_a = core::Device("CPU:0"), + R"(Convert a TriangleMeshModel (e.g. as read from a file with +`open3d.io.read_triangle_mesh_model()`) to a dictionary of mesh names to +triangle meshes with the specified vertex and triangle dtypes and moved to the +specified device. Only a single material per mesh is supported. Materials common +to multiple meshes will be duplicated. Textures (as t.geometry.Image) will use +shared storage on the CPU (GPU resident images for textures is not yet supported). + +Returns: + Dictionary of names to triangle meshes. + +Example: + flight_helmet = o3d.data.FlightHelmetModel() + model = o3d.io.read_triangle_model(flight_helmet.path) + mesh_dict = o3d.t.geometry.TriangleMesh.from_triangle_mesh_model(model) + o3d.visualization.draw(list({"name": name, "geometry": tmesh} for + (name, tmesh) in mesh_dict.items())) + )"); // conversion triangle_mesh.def("to_legacy", &TriangleMesh::ToLegacy, "Convert to a legacy Open3D TriangleMesh."); @@ -706,7 +728,7 @@ This function always uses the CPU device. Returns: This function creates a face attribute "texture_uvs" and returns a tuple - with (max stretch, num_charts, num_partitions) storing the + with (max stretch, num_charts, num_partitions) storing the actual amount of stretch, the number of created charts, and the number of parallel partitions created. @@ -883,7 +905,7 @@ This function always uses the CPU device. "max_faces"_a, R"(Partition the mesh by recursively doing PCA. -This function creates a new face attribute with the name "partition_ids" storing +This function creates a new face attribute with the name "partition_ids" storing the partition id for each face. Args: @@ -892,7 +914,7 @@ the partition id for each face. Example: - This code partitions a mesh such that each partition contains at most 20k + This code partitions a mesh such that each partition contains at most 20k faces:: import open3d as o3d @@ -911,15 +933,15 @@ the partition id for each face. R"(Returns a new mesh with the faces selected by a boolean mask. Args: - mask (open3d.core.Tensor): A boolean mask with the shape (N) with N as the + mask (open3d.core.Tensor): A boolean mask with the shape (N) with N as the number of faces in the mesh. - + Returns: A new mesh with the selected faces. If the original mesh is empty, return an empty mesh. Example: - This code partitions the mesh using PCA and then visualized the individual + This code partitions the mesh using PCA and then visualized the individual parts:: import open3d as o3d @@ -962,6 +984,10 @@ or has a negative value, it is ignored. box = o3d.t.geometry.TriangleMesh.create_box() top_face = box.select_by_index([2, 3, 6, 7]) )"); + + triangle_mesh.def("remove_unreferenced_vertices", + &TriangleMesh::RemoveUnreferencedVertices, + "Removes unreferenced vertices from the mesh in-place."); } } // namespace geometry diff --git a/cpp/pybind/visualization/rendering/material.cpp b/cpp/pybind/visualization/rendering/material.cpp index 82225c9c90a..3e109dcdbb3 100644 --- a/cpp/pybind/visualization/rendering/material.cpp +++ b/cpp/pybind/visualization/rendering/material.cpp @@ -12,6 +12,7 @@ #include "open3d/visualization/rendering/Material.h" +#include "open3d/visualization/rendering/MaterialRecord.h" #include "pybind/open3d_pybind.h" PYBIND11_MAKE_OPAQUE( @@ -41,6 +42,9 @@ void pybind_material(py::module& m) { mat.def(py::init<>()) .def(py::init(), "", "mat"_a) .def(py::init(), "", "material_name"_a) + .def(py::init(&Material::FromMaterialRecord), "material_record"_a, + "Convert from MaterialRecord.") + .def("__repr__", &Material::ToString) .def("set_default_properties", &Material::SetDefaultProperties, "Fills material with defaults for common PBR material " "properties used by Open3D") diff --git a/cpp/pybind/visualization/visualizer.cpp b/cpp/pybind/visualization/visualizer.cpp index 9370612d06c..990727dd45c 100644 --- a/cpp/pybind/visualization/visualizer.cpp +++ b/cpp/pybind/visualization/visualizer.cpp @@ -169,10 +169,48 @@ void pybind_visualizer(py::module &m) { .def("register_key_action_callback", &VisualizerWithKeyCallback::RegisterKeyActionCallback, "Function to register a callback function for a key action " - "event. The callback function takes Visualizer, action and " - "mods as input and returns a boolean indicating if " - "UpdateGeometry() needs to be run.", - "key"_a, "callback_func"_a); + "event. The callback function takes `Visualizer`, `action` " + "and `mods` as input and returns a boolean indicating if " + "`UpdateGeometry()` needs to be run. The `action` can be one " + "of `GLFW_RELEASE` (0), `GLFW_PRESS` (1) or `GLFW_REPEAT` " + "(2), see `GLFW input interface " + "`__. The " + "`mods` specifies the modifier key, see `GLFW modifier key " + "`__", + "key"_a, "callback_func"_a) + + .def("register_mouse_move_callback", + &VisualizerWithKeyCallback::RegisterMouseMoveCallback, + "Function to register a callback function for a mouse move " + "event. The callback function takes Visualizer, x and y mouse " + "position inside the window as input and returns a boolean " + "indicating if UpdateGeometry() needs to be run. `GLFW mouse " + "position `__ for more details.", + "callback_func"_a) + + .def("register_mouse_scroll_callback", + &VisualizerWithKeyCallback::RegisterMouseScrollCallback, + "Function to register a callback function for a mouse scroll " + "event. The callback function takes Visualizer, x and y mouse " + "scroll offset as input and returns a boolean " + "indicating if UpdateGeometry() needs to be run. `GLFW mouse " + "scrolling `__ for more details.", + "callback_func"_a) + + .def("register_mouse_button_callback", + &VisualizerWithKeyCallback::RegisterMouseButtonCallback, + "Function to register a callback function for a mouse button " + "event. The callback function takes `Visualizer`, `button`, " + "`action` and `mods` as input and returns a boolean " + "indicating `UpdateGeometry()` needs to be run. The `action` " + "can be one of GLFW_RELEASE (0), GLFW_PRESS (1) or " + "GLFW_REPEAT (2), see `GLFW input interface " + "`__. " + "The `mods` specifies the modifier key, see `GLFW modifier " + "key `__.", + "callback_func"_a); py::class_, std::shared_ptr> diff --git a/cpp/tests/core/Tensor.cpp b/cpp/tests/core/Tensor.cpp index ad377274169..e4a4b14cf9d 100644 --- a/cpp/tests/core/Tensor.cpp +++ b/cpp/tests/core/Tensor.cpp @@ -30,12 +30,24 @@ INSTANTIATE_TEST_SUITE_P(Tensor, TensorPermuteDevices, testing::ValuesIn(PermuteDevices::TestCases())); +class TensorPermuteDevicesWithSYCL : public PermuteDevices {}; +INSTANTIATE_TEST_SUITE_P( + Tensor, + TensorPermuteDevicesWithSYCL, + testing::ValuesIn(PermuteDevicesWithSYCL::TestCases())); + class TensorPermuteDevicePairs : public PermuteDevicePairs {}; INSTANTIATE_TEST_SUITE_P( Tensor, TensorPermuteDevicePairs, testing::ValuesIn(TensorPermuteDevicePairs::TestCases())); +class TensorPermuteDevicePairsWithSYCL : public PermuteDevicePairsWithSYCL {}; +INSTANTIATE_TEST_SUITE_P( + Tensor, + TensorPermuteDevicePairsWithSYCL, + testing::ValuesIn(TensorPermuteDevicePairsWithSYCL::TestCases())); + class TensorPermuteSizesDefaultStridesAndDevices : public testing::TestWithParam< std::tuple, @@ -54,7 +66,7 @@ static constexpr const T &AsConst(T &t) noexcept { return t; } -TEST_P(TensorPermuteDevices, Constructor) { +TEST_P(TensorPermuteDevicesWithSYCL, Constructor) { core::Device device = GetParam(); core::Dtype dtype = core::Float32; @@ -71,7 +83,7 @@ TEST_P(TensorPermuteDevices, Constructor) { EXPECT_ANY_THROW(core::Tensor({-1, -1}, dtype, device)); } -TEST_P(TensorPermuteDevices, ConstructorBool) { +TEST_P(TensorPermuteDevicesWithSYCL, ConstructorBool) { core::Device device = GetParam(); core::SizeVector shape{2, 3}; @@ -105,7 +117,7 @@ TEST_P(TensorPermuteDevices, WithInitValue) { EXPECT_EQ(t.ToFlatVector(), vals); } -TEST_P(TensorPermuteDevices, WithInitList) { +TEST_P(TensorPermuteDevicesWithSYCL, WithInitList) { core::Device device = GetParam(); core::Tensor t; @@ -187,7 +199,7 @@ TEST_P(TensorPermuteDevices, WithInitList) { std::exception); } -TEST_P(TensorPermuteDevices, WithInitValueBool) { +TEST_P(TensorPermuteDevicesWithSYCL, WithInitValueBool) { core::Device device = GetParam(); std::vector vals{true, false, true, true, false, false}; @@ -195,7 +207,7 @@ TEST_P(TensorPermuteDevices, WithInitValueBool) { EXPECT_EQ(t.ToFlatVector(), vals); } -TEST_P(TensorPermuteDevices, WithInitValueTypeMismatch) { +TEST_P(TensorPermuteDevicesWithSYCL, WithInitValueTypeMismatch) { core::Device device = GetParam(); std::vector vals{0, 1, 2, 3, 4, 5}; @@ -203,7 +215,7 @@ TEST_P(TensorPermuteDevices, WithInitValueTypeMismatch) { std::runtime_error); } -TEST_P(TensorPermuteDevices, WithInitValueSizeMismatch) { +TEST_P(TensorPermuteDevicesWithSYCL, WithInitValueSizeMismatch) { core::Device device = GetParam(); std::vector vals{0, 1, 2, 3, 4}; @@ -298,7 +310,7 @@ TEST_P(TensorPermuteDevicePairs, IndexSetFillFancy) { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0})); } -TEST_P(TensorPermuteDevicePairs, Copy) { +TEST_P(TensorPermuteDevicePairsWithSYCL, Copy) { core::Device dst_device; core::Device src_device; std::tie(dst_device, src_device) = GetParam(); @@ -317,7 +329,7 @@ TEST_P(TensorPermuteDevicePairs, Copy) { EXPECT_EQ(dst_t.ToFlatVector(), vals); } -TEST_P(TensorPermuteDevicePairs, CopyBool) { +TEST_P(TensorPermuteDevicePairsWithSYCL, CopyBool) { core::Device dst_device; core::Device src_device; std::tie(dst_device, src_device) = GetParam(); @@ -357,12 +369,15 @@ TEST_P(TensorPermuteDevicePairs, ToDevice) { core::Device src_device; std::tie(dst_device, src_device) = GetParam(); - core::Tensor src_t = core::Tensor::Init({0, 1, 2, 3}, src_device); + core::Tensor src_t = + core::Tensor::Init({0.f, 1.f, 2.f, 3.f}, src_device); core::Tensor dst_t = src_t.To(dst_device); EXPECT_TRUE(dst_t.To(src_device).AllClose(src_t)); EXPECT_ANY_THROW(src_t.To(core::Device("CPU:1"))); + EXPECT_ANY_THROW(src_t.To(core::Device("SYCL:100"))); + EXPECT_ANY_THROW(src_t.To(core::Device("CUDA:-1"))); EXPECT_ANY_THROW(src_t.To(core::Device("CUDA:100000"))); } @@ -529,7 +544,7 @@ TEST_P(TensorPermuteDevices, Flatten) { EXPECT_ANY_THROW(src_t.Flatten(2, 1)); } -TEST_P(TensorPermuteDevices, DefaultStrides) { +TEST_P(TensorPermuteDevicesWithSYCL, DefaultStrides) { core::Device device = GetParam(); core::Tensor t0({}, core::Float32, device); @@ -663,7 +678,7 @@ TEST_P(TensorPermuteDevices, ItemAssign) { EXPECT_EQ(t[1][2][3].Item(), 101); } -TEST_P(TensorPermuteDevices, ToString) { +TEST_P(TensorPermuteDevicesWithSYCL, ToString) { using ::testing::AnyOf; core::Device device = GetParam(); core::Tensor t; @@ -738,7 +753,7 @@ TEST_P(TensorPermuteDevices, ToString) { [True False False]])"); } -TEST_P(TensorPermuteDevicePairs, CopyContiguous) { +TEST_P(TensorPermuteDevicePairsWithSYCL, CopyContiguous) { core::Device dst_device; core::Device src_device; std::tie(dst_device, src_device) = GetParam(); diff --git a/cpp/tests/geometry/TriangleMesh.cpp b/cpp/tests/geometry/TriangleMesh.cpp index 559330bb2a0..ff6d75f6c8c 100644 --- a/cpp/tests/geometry/TriangleMesh.cpp +++ b/cpp/tests/geometry/TriangleMesh.cpp @@ -1843,33 +1843,121 @@ TEST(TriangleMesh, CreateFromPointCloudPoisson) { {0.742035, 0.885688, 0.458892}, {0.742035, 0.885688, 0.458892}, {0.383097, 0.761093, 0.173810}, {0.284898, 0.359292, 0.669062}}; mesh_gt.triangles_ = { - {1, 13, 0}, {0, 14, 2}, {13, 14, 0}, {1, 15, 13}, - {15, 16, 13}, {1, 3, 15}, {14, 13, 16}, {16, 17, 14}, - {2, 18, 4}, {14, 18, 2}, {4, 18, 5}, {18, 14, 17}, - {17, 19, 18}, {18, 19, 6}, {6, 5, 18}, {7, 16, 15}, - {7, 8, 16}, {8, 20, 16}, {21, 16, 20}, {17, 16, 21}, - {9, 20, 8}, {10, 20, 9}, {21, 20, 10}, {22, 17, 21}, - {19, 17, 22}, {19, 22, 11}, {11, 6, 19}, {22, 21, 10}, - {10, 12, 22}, {11, 22, 12}, {24, 0, 23}, {1, 0, 24}, - {0, 2, 25}, {25, 23, 0}, {3, 1, 24}, {24, 26, 3}, - {2, 4, 27}, {27, 25, 2}, {27, 5, 28}, {4, 5, 27}, - {28, 6, 29}, {5, 6, 28}, {8, 7, 30}, {30, 31, 8}, - {32, 8, 31}, {9, 8, 32}, {33, 9, 32}, {10, 9, 33}, - {6, 11, 34}, {34, 29, 6}, {12, 10, 33}, {33, 35, 12}, - {34, 12, 35}, {11, 12, 34}, {24, 23, 48}, {36, 23, 25}, - {36, 37, 23}, {37, 48, 23}, {38, 24, 48}, {38, 39, 24}, - {39, 26, 24}, {39, 49, 26}, {38, 48, 37}, {25, 27, 40}, - {40, 36, 25}, {27, 28, 41}, {41, 40, 27}, {28, 29, 42}, - {42, 41, 28}, {39, 30, 49}, {39, 31, 30}, {39, 50, 31}, - {39, 43, 50}, {44, 50, 43}, {32, 31, 50}, {44, 32, 50}, - {44, 45, 32}, {45, 33, 32}, {42, 34, 46}, {29, 34, 42}, - {47, 33, 45}, {35, 33, 47}, {34, 35, 47}, {47, 46, 34}, - {37, 36, 51}, {39, 38, 52}, {53, 52, 51}, {52, 37, 51}, - {52, 38, 37}, {36, 40, 54}, {54, 51, 36}, {54, 40, 41}, - {51, 54, 55}, {55, 53, 51}, {55, 41, 42}, {54, 41, 55}, - {43, 39, 52}, {56, 52, 53}, {56, 44, 52}, {44, 43, 52}, - {45, 44, 56}, {56, 55, 57}, {53, 55, 56}, {55, 42, 46}, - {46, 57, 55}, {45, 56, 57}, {57, 47, 45}, {57, 46, 47}}; + {1, 13, 0}, + {0, 14, 2}, + {13, 14, 0}, + {1, 15, 13}, + {15, 16, 13}, + {1, 3, 15}, + {14, 13, 16}, + {16, 17, 14}, + {2, 18, 4}, + {14, 18, 2}, + {4, 18, 5}, + {18, 14, 17}, + {17, 19, 18}, + {18, 19, 6}, + {6, 5, 18}, + {7, 16, 15}, + {7, 8, 16}, + {8, 20, 16}, + {21, 16, 20}, + {17, 16, 21}, + {9, 20, 8}, + {10, 20, 9}, + {21, 20, 10}, + {22, 17, 21}, + {19, 17, 22}, + {19, 22, 11}, + {11, 6, 19}, + {22, 21, 10}, + {10, 12, 22}, + {11, 22, 12}, + {24, 0, 23}, + {1, 0, 24}, + {0, 2, 25}, + {25, 23, 0}, + {3, 1, 24}, + {24, 26, 3}, + {2, 4, 27}, + {27, 25, 2}, + {27, 5, 28}, + {4, 5, 27}, + {28, 6, 29}, + {5, 6, 28}, + {8, 7, 30}, + {30, 31, 8}, + {32, 8, 31}, + {9, 8, 32}, + {33, 9, 32}, + {10, 9, 33}, + {6, 11, 34}, + {34, 29, 6}, + {12, 10, 33}, + {33, 35, 12}, + {34, 12, 35}, + {11, 12, 34}, + {24, 23, 48}, + {36, 23, 25}, + {36, 37, 23}, + {37, 48, 23}, + {38, 24, 48}, + {38, 39, 24}, + {39, 26, 24}, + {39, 49, 26}, + {38, 48, 37}, + {25, 27, 40}, + {40, 36, 25}, + {27, 28, 41}, + {41, 40, 27}, + {28, 29, 42}, + {42, 41, 28}, + {39, 30, 49}, + {39, 31, 30}, + {39, 50, 31}, + {39, 43, 50}, + {44, 50, 43}, + {32, 31, 50}, +#if defined(__APPLE__) && defined(__arm64__) + // Apple Silicon consistently triangulates the vertices differently + {44, 45, 50}, + {45, 32, 50}, +#else + {44, 32, 50}, + {44, 45, 32}, +#endif + {45, 33, 32}, + {42, 34, 46}, + {29, 34, 42}, + {47, 33, 45}, + {35, 33, 47}, + {34, 35, 47}, + {47, 46, 34}, + {37, 36, 51}, + {39, 38, 52}, + {53, 52, 51}, + {52, 37, 51}, + {52, 38, 37}, + {36, 40, 54}, + {54, 51, 36}, + {54, 40, 41}, + {51, 54, 55}, + {55, 53, 51}, + {55, 41, 42}, + {54, 41, 55}, + {43, 39, 52}, + {56, 52, 53}, + {56, 44, 52}, + {44, 43, 52}, + {45, 44, 56}, + {56, 55, 57}, + {53, 55, 56}, + {55, 42, 46}, + {46, 57, 55}, + {45, 56, 57}, + {57, 47, 45}, + {57, 46, 47} + }; std::vector densities_gt = { 0.39865168929100037, 0.32580316066741943, 0.39778709411621094, 0.2200755625963211, 0.428702175617218, 0.4288075268268585, @@ -2208,7 +2296,8 @@ TEST(TriangleMesh, CreateMeshCoordinateFrame) { indices.push_back(output_tm->triangles_[i](1, 0)); indices.push_back(output_tm->triangles_[i](2, 0)); } - unique(indices.begin(), indices.end()); + auto last = unique(indices.begin(), indices.end()); + indices.erase(last, indices.end()); sort(indices.begin(), indices.end()); auto output = output_tm->SelectByIndex(indices); diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index d1e2d819eac..aa0c75b8b7c 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -10,6 +10,7 @@ #include #include "core/CoreTest.h" +#include "open3d/core/Dtype.h" #include "open3d/core/EigenConverter.h" #include "open3d/core/TensorCheck.h" #include "tests/Tests.h" @@ -1212,5 +1213,135 @@ TEST_P(TriangleMeshPermuteDevices, SelectByIndex) { box_untouched.GetTriangleIndices())); } +TEST_P(TriangleMeshPermuteDevices, RemoveUnreferencedVertices) { + core::Device device = GetParam(); + t::geometry::TriangleMesh mesh_empty{device}; + + // check completely empty mesh + EXPECT_TRUE(mesh_empty.RemoveUnreferencedVertices().IsEmpty()); + + // check mesh w/o triangles + core::Tensor vertices_no_tris_orig = + core::Tensor::Ones({2, 3}, core::Float32, device); + mesh_empty.SetVertexPositions(vertices_no_tris_orig); + EXPECT_TRUE(mesh_empty.RemoveUnreferencedVertices().IsEmpty()); + + // Torus + core::Tensor verts = core::Tensor::Init( + { + {0, 0, 0}, /* 0 */ + {3.0, 0.0, 0.0}, + {1.5, 0.0, 0.866025}, + {1, 2, 3}, /* 3 */ + {1.5, 0.0, -0.866025}, + {1.5, 2.59808, 0.0}, + {0.75, 1.29904, 0.866025}, + {0.75, 1.29904, -0.866025}, + {-1.5, 2.59808, 0}, + {-0.75, 1.29904, 0.866025}, + {-0.75, 1.29904, -0.866025}, + {-3.0, 0.0, 0.0}, + {-1.5, 0.0, 0.866025}, + {-1.5, 0.0, -0.866025}, + {-1.5, -2.59808, 0.0}, + {-0.75, -1.29904, 0.866025}, + {-0.75, -1.29904, -0.866025}, + {4, 5, 6}, /* 17 */ + {1.5, -2.59808, 0.0}, + {0.75, -1.29904, 0.866025}, + {0.75, -1.29904, -0.866025}, + {7, 8, 9} /* 21 */ + }, + device); + + core::Tensor tris = core::Tensor::Init( + {{5, 6, 1}, {1, 6, 2}, {6, 7, 2}, {2, 7, 4}, + {7, 5, 4}, {4, 5, 1}, {8, 9, 5}, {5, 9, 6}, + {9, 10, 6}, {6, 10, 7}, {10, 8, 7}, {7, 8, 5}, + {11, 12, 8}, {8, 12, 9}, {12, 13, 9}, {9, 13, 10}, + {13, 11, 10}, {10, 11, 8}, {14, 15, 11}, {11, 15, 12}, + {15, 16, 12}, {12, 16, 13}, {16, 14, 13}, {13, 14, 11}, + {18, 19, 14}, {14, 19, 15}, {19, 20, 15}, {15, 20, 16}, + {20, 18, 16}, {16, 18, 14}, {1, 2, 18}, {18, 2, 19}, + {2, 4, 19}, {19, 4, 20}, {4, 1, 20}, {20, 1, 18}}, + device); + t::geometry::TriangleMesh torus{verts, tris}; + core::Tensor vertex_colors = core::Tensor::Init( + {{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}, {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, {4.0, 4.0, 4.0}, {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, {7.0, 7.0, 7.0}, {8.0, 8.0, 8.0}, + {9.0, 9.0, 9.0}, {10.0, 10.0, 10.0}, {11.0, 11.0, 11.0}, + {12.0, 12.0, 12.0}, {13.0, 13.0, 13.0}, {14.0, 14.0, 14.0}, + {15.0, 15.0, 15.0}, {16.0, 16.0, 16.0}, {17.0, 17.0, 17.0}, + {18.0, 18.0, 18.0}, {19.0, 19.0, 19.0}, {20.0, 20.0, 20.0}, + {21.0, 21.0, 21.0}}, + device); + core::Tensor vertex_labels = + core::Tensor::Init( + {{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}, {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, {4.0, 4.0, 4.0}, {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, {7.0, 7.0, 7.0}, {8.0, 8.0, 8.0}, + {9.0, 9.0, 9.0}, {10.0, 10.0, 10.0}, {11.0, 11.0, 11.0}, + {12.0, 12.0, 12.0}, {13.0, 13.0, 13.0}, {14.0, 14.0, 14.0}, + {15.0, 15.0, 15.0}, {16.0, 16.0, 16.0}, {17.0, 17.0, 17.0}, + {18.0, 18.0, 18.0}, {19.0, 19.0, 19.0}, {20.0, 20.0, 20.0}, + {21.0, 21.0, 21.0}}, + device) * + 10; + + core::Tensor triangle_labels = + core::Tensor::Init({{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, {7.0, 7.0, 7.0}, + {8.0, 8.0, 8.0}, {9.0, 9.0, 9.0}, + {10.0, 10.0, 10.0}, {11.0, 11.0, 11.0}, + {12.0, 12.0, 12.0}, {13.0, 13.0, 13.0}, + {14.0, 14.0, 14.0}, {15.0, 15.0, 15.0}, + {16.0, 16.0, 16.0}, {17.0, 17.0, 17.0}, + {18.0, 18.0, 18.0}, {19.0, 19.0, 19.0}, + {20.0, 20.0, 20.0}, {21.0, 21.0, 21.0}, + {22.0, 22.0, 22.0}, {23.0, 23.0, 23.0}, + {24.0, 24.0, 24.0}, {25.0, 25.0, 25.0}, + {26.0, 26.0, 26.0}, {27.0, 27.0, 27.0}, + {28.0, 28.0, 28.0}, {29.0, 29.0, 29.0}, + {30.0, 30.0, 30.0}, {31.0, 31.0, 31.0}, + {32.0, 32.0, 32.0}, {33.0, 33.0, 33.0}, + {34.0, 34.0, 34.0}, {35.0, 35.0, 35.0}}, + device) * + 100; + torus.SetVertexColors(vertex_colors); + torus.SetVertexAttr("labels", vertex_labels); + torus.ComputeVertexNormals(); + torus.ComputeTriangleNormals(); + + // set the expected value + core::Tensor verts_mask = core::Tensor::Init( + {0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0}, + device); + core::Tensor expected_verts = + torus.GetVertexPositions().IndexGet({verts_mask}); + core::Tensor expected_tris = + t::geometry::TriangleMesh::CreateTorus(2, 1, 6, 3, core::Float32, + core::Int32, device) + .GetTriangleIndices(); + core::Tensor expected_vert_normals = + torus.GetVertexNormals().IndexGet({verts_mask}); + core::Tensor expected_tri_normals = torus.GetTriangleNormals().Clone(); + core::Tensor expected_vert_labels = + torus.GetVertexAttr("labels").IndexGet({verts_mask}); + core::Tensor expected_vert_colors = + torus.GetVertexAttr("colors").IndexGet({verts_mask}); + + torus.RemoveUnreferencedVertices(); + + EXPECT_TRUE(torus.GetVertexPositions().AllClose(expected_verts)); + EXPECT_TRUE(torus.GetVertexNormals().AllClose(expected_vert_normals)); + EXPECT_TRUE(torus.GetVertexColors().AllClose(expected_vert_colors)); + EXPECT_TRUE(torus.GetVertexAttr("labels").AllClose(expected_vert_labels)); + EXPECT_TRUE(torus.GetTriangleIndices().AllClose(expected_tris)); + EXPECT_TRUE(torus.GetTriangleNormals().AllClose(expected_tri_normals)); +} + } // namespace tests } // namespace open3d diff --git a/docker/Dockerfile.ci b/docker/Dockerfile.ci index e446353744a..81379352f41 100644 --- a/docker/Dockerfile.ci +++ b/docker/Dockerfile.ci @@ -17,6 +17,7 @@ ARG BUILD_TENSORFLOW_OPS ARG BUILD_PYTORCH_OPS ARG PACKAGE ARG BUILD_SYCL_MODULE +ARG CI RUN if [ -z "${DEVELOPER_BUILD}" ]; then echo "Error: ARG DEVELOPER_BUILD not specified."; exit 1; fi \ && if [ -z "${CCACHE_TAR_NAME}" ]; then echo "Error: ARG CCACHE_TAR_NAME not specified."; exit 1; fi \ @@ -93,7 +94,7 @@ RUN conda --version \ # Activate open3d virtualenv # This works during docker build. It becomes the prefix of all RUN commands. # Ref: https://stackoverflow.com/a/60148365/1255535 -SHELL ["conda", "run", "-n", "open3d", "/bin/bash", "-c"] +SHELL ["conda", "run", "-n", "open3d", "/bin/bash", "-o", "pipefail", "-c"] # Dependencies: cmake ENV PATH=${HOME}/${CMAKE_VERSION}/bin:${PATH} @@ -210,7 +211,9 @@ RUN \ && make install-pip-package -j$(nproc) \ && make install -j$(nproc) \ && if [ "${PACKAGE}" = "ON" ]; then make package; fi \ - && if [ "${PACKAGE}" = "VIEWER" ]; then make package-Open3DViewer-deb; fi + && if [ "${PACKAGE}" = "VIEWER" ]; then make package-Open3DViewer-deb; fi \ + && if [ "${CI:-}a" != "a" ]; then rm -rf _deps assimp embree ippicv mkl mkl_install webrtc; fi +# If CI is not null or unset, remove all large build folders to save disk space # Compress ccache folder, move to / directory RUN ccache -s \ diff --git a/docker/Dockerfile.wheel b/docker/Dockerfile.wheel index 6a7d7080aed..a719262ece2 100644 --- a/docker/Dockerfile.wheel +++ b/docker/Dockerfile.wheel @@ -10,6 +10,7 @@ ARG CCACHE_VERSION ARG PYTHON_VERSION ARG BUILD_TENSORFLOW_OPS ARG BUILD_PYTORCH_OPS +ARG CI # Forward all ARG to ENV # ci_utils.sh requires these environment variables @@ -130,7 +131,9 @@ WORKDIR /root/Open3D RUN export NPROC=$(nproc) \ && export BUILD_SHARED_LIBS=OFF \ && source /root/Open3D/util/ci_utils.sh \ - && build_pip_package build_azure_kinect build_jupyter + && build_pip_package build_azure_kinect build_jupyter \ + && if [ ${CI:-}a != a ]; then cd /root/Open3D/build/ && ls | grep -Ev '^lib$' | xargs rm -rf ; fi + # remove build folder (except lib) to save CI space on Github # Compress ccache folder, move to / directory RUN ccache -s \ diff --git a/docker/docker_build.sh b/docker/docker_build.sh index 33ab6ad0115..f55276baed5 100755 --- a/docker/docker_build.sh +++ b/docker/docker_build.sh @@ -207,6 +207,7 @@ cuda_wheel_build() { --build-arg PYTHON_VERSION="${PYTHON_VERSION}" \ --build-arg BUILD_TENSORFLOW_OPS="${BUILD_TENSORFLOW_OPS}" \ --build-arg BUILD_PYTORCH_OPS="${BUILD_PYTORCH_OPS}" \ + --build-arg CI="${CI:-}" \ -t open3d-ci:wheel \ -f docker/Dockerfile.wheel . popd @@ -249,6 +250,7 @@ ci_build() { --build-arg BUILD_PYTORCH_OPS="${BUILD_PYTORCH_OPS}" \ --build-arg PACKAGE="${PACKAGE}" \ --build-arg BUILD_SYCL_MODULE="${BUILD_SYCL_MODULE}" \ + --build-arg CI="${CI:-}" \ -t "${DOCKER_TAG}" \ -f docker/Dockerfile.ci . popd diff --git a/docs/getting_started.in.rst b/docs/getting_started.in.rst index b9f390ea835..a2aa5a745ee 100644 --- a/docs/getting_started.in.rst +++ b/docs/getting_started.in.rst @@ -5,6 +5,21 @@ Getting started .. _install_open3d_python: +Viewer +====== + +Use the Open3D viewer application to visualize 3D data in various formats and +interact with it. You can download the latest stable release app from `Github +releases `__. The latest development +version (``HEAD`` of ``main`` branch) viewer app is provided here [#]_: + +* `Linux (Ubuntu 18.04+ or glibc 2.27+) `__ [#]_ +* `MacOSX v10.15+ (Intel or Apple Silicon) `__ +* `Windows 10+ (64-bit) `__ + +.. [#] Please use these links from the `latest version of this page `__ only. +.. [#] To check the `glibc` version on your system, run :code:`ldd --version`. + Python ====== @@ -70,28 +85,28 @@ version (``HEAD`` of ``main`` branch): :widths: auto * - Linux - - `Python 3.8 `__ - - `Python 3.9 `__ - - `Python 3.10 `__ - - `Python 3.11 `__ + - `Python 3.8 `__ + - `Python 3.9 `__ + - `Python 3.10 `__ + - `Python 3.11 `__ * - Linux (CPU) - - `Python 3.8 `__ - - `Python 3.9 `__ - - `Python 3.10 `__ - - `Python 3.11 `__ + - `Python 3.8 `__ + - `Python 3.9 `__ + - `Python 3.10 `__ + - `Python 3.11 `__ * - MacOS - - `Python 3.8 `__ - - `Python 3.9 `__ - - `Python 3.10 `__ - - `Python 3.11 `__ + - `Python 3.8 (x86_64) `__ + - `Python 3.9 (x86_64) `__ + - `Python 3.10 (x86_64+arm64) `__ + - `Python 3.11 (x86_64+arm64) `__ * - Windows - - `Python 3.8 `__ - - `Python 3.9 `__ - - `Python 3.10 `__ - - `Python 3.11 `__ + - `Python 3.8 `__ + - `Python 3.9 `__ + - `Python 3.10 `__ + - `Python 3.11 `__ Please use these links from the `latest version of this page `__ only. You can also @@ -162,23 +177,24 @@ available for the main supported platforms. Also, the latest development version .. hlist:: :columns: 2 - * `x86_64 (CXX11 ABI) `__ - * `x86_64 (CXX11 ABI) with CUDA 11.x `__ - * `x86_64 (pre CXX11 ABI) `__ - * `x86_64 (pre CXX11 ABI) with CUDA 11.x `__ + * `x86_64 (CXX11 ABI) `__ + * `x86_64 (CXX11 ABI) with CUDA 11.x `__ + * `x86_64 (pre CXX11 ABI) `__ + * `x86_64 (pre CXX11 ABI) with CUDA 11.x `__ :MacOSX v10.15+: .. hlist:: :columns: 2 - * `x86_64 `__ + * `x86_64 `__ + * `arm64 `__ :Windows 10+: .. hlist:: :columns: 2 - * `x86_64 Release `__ - * `x86_64 Debug `__ + * `x86_64 Release `__ + * `x86_64 Debug `__ .. [#] Please use these links from the `latest version of this page `__ only. diff --git a/docs/requirements.txt b/docs/requirements.txt index 299c15f5a84..a974fc66a1d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,6 @@ jinja2==3.1.3 m2r2==0.3.3.post2 matplotlib==3.7.3 nbsphinx==0.9.3 -sphinx==7.1.2 \ No newline at end of file +sphinx==7.1.2 +nbconvert==6.5.4 +lxml[html_clean]==5.2.1 diff --git a/examples/python/reconstruction_system/color_map_optimization_for_reconstruction_system.py b/examples/python/reconstruction_system/color_map_optimization_for_reconstruction_system.py index 173f6f52c22..088ec51818a 100644 --- a/examples/python/reconstruction_system/color_map_optimization_for_reconstruction_system.py +++ b/examples/python/reconstruction_system/color_map_optimization_for_reconstruction_system.py @@ -14,6 +14,7 @@ pyexample_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(pyexample_path) from open3d_example import * +from initialize_config import initialize_config def parse_keys(filename): diff --git a/examples/python/reconstruction_system/make_fragments.py b/examples/python/reconstruction_system/make_fragments.py index dc4e59d0780..5af9d960f4b 100644 --- a/examples/python/reconstruction_system/make_fragments.py +++ b/examples/python/reconstruction_system/make_fragments.py @@ -139,7 +139,11 @@ def make_pointcloud_for_fragment(path_dataset, color_files, depth_files, pcd.colors = mesh.vertex_colors pcd_name = join(path_dataset, config["template_fragment_pointcloud"] % fragment_id) - o3d.io.write_point_cloud(pcd_name, pcd, False, True) + o3d.io.write_point_cloud(pcd_name, + pcd, + format='auto', + write_ascii=False, + compressed=True) def process_single_fragment(fragment_id, color_files, depth_files, n_files, diff --git a/examples/python/visualization/customized_visualization_key_action.py b/examples/python/visualization/customized_visualization_key_action.py index 9f885615e4e..c68e2f1110f 100644 --- a/examples/python/visualization/customized_visualization_key_action.py +++ b/examples/python/visualization/customized_visualization_key_action.py @@ -41,11 +41,45 @@ def animation_callback(vis): vis.run() +def custom_mouse_action(pcd): + + vis = o3d.visualization.VisualizerWithKeyCallback() + buttons = ['left', 'right', 'middle'] + actions = ['up', 'down'] + mods_name = ['shift', 'ctrl', 'alt', 'cmd'] + + def on_key_action(vis, action, mods): + print("on_key_action", action, mods) + + vis.register_key_action_callback(ord("A"), on_key_action) + + def on_mouse_move(vis, x, y): + print(f"on_mouse_move({x:.2f}, {y:.2f})") + + def on_mouse_scroll(vis, x, y): + print(f"on_mouse_scroll({x:.2f}, {y:.2f})") + + def on_mouse_button(vis, button, action, mods): + pressed_mods = " ".join( + [mods_name[i] for i in range(4) if mods & (1 << i)]) + print(f"on_mouse_button: {buttons[button]}, {actions[action]}, " + + pressed_mods) + + vis.register_mouse_move_callback(on_mouse_move) + vis.register_mouse_scroll_callback(on_mouse_scroll) + vis.register_mouse_button_callback(on_mouse_button) + + vis.create_window() + vis.add_geometry(pcd) + vis.run() + + if __name__ == "__main__": ply_data = o3d.data.PLYPointCloud() pcd = o3d.io.read_point_cloud(ply_data.path) - print( - "Customized visualization with smooth key action (without keyboard repeat delay)" - ) + print("Customized visualization with smooth key action " + "(without keyboard repeat delay). Press the space-bar.") custom_key_action_without_kb_repeat_delay(pcd) + print("Customized visualization with mouse action.") + custom_mouse_action(pcd) diff --git a/examples/python/visualization/demo_scene.py b/examples/python/visualization/demo_scene.py index cfb5d3d2393..b0662feadc7 100644 --- a/examples/python/visualization/demo_scene.py +++ b/examples/python/visualization/demo_scene.py @@ -6,33 +6,15 @@ # ---------------------------------------------------------------------------- """Demo scene demonstrating models, built-in shapes, and materials""" -import math import numpy as np -import os import open3d as o3d import open3d.visualization as vis -def convert_material_record(mat_record): - mat = vis.Material('defaultLit') - # Convert scalar parameters - mat.vector_properties['base_color'] = mat_record.base_color - mat.scalar_properties['metallic'] = mat_record.base_metallic - mat.scalar_properties['roughness'] = mat_record.base_roughness - mat.scalar_properties['reflectance'] = mat_record.base_reflectance - mat.texture_maps['albedo'] = o3d.t.geometry.Image.from_legacy( - mat_record.albedo_img) - mat.texture_maps['normal'] = o3d.t.geometry.Image.from_legacy( - mat_record.normal_img) - mat.texture_maps['ao_rough_metal'] = o3d.t.geometry.Image.from_legacy( - mat_record.ao_rough_metal_img) - return mat - - def create_scene(): - ''' - Creates the geometry and materials for the demo scene and returns a dictionary suitable for draw call - ''' + """Creates the geometry and materials for the demo scene and returns a + dictionary suitable for draw call + """ # Create some shapes for our scene a_cube = o3d.geometry.TriangleMesh.create_box(2, 4, @@ -47,7 +29,7 @@ def create_scene(): resolution=40, create_uv_map=True) a_sphere.compute_vertex_normals() - rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi / 2, 0, 0)) + rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi / 2, 0, 0)) a_sphere.rotate(rotate_90) a_sphere.translate((5, 2.4, 0)) a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere) @@ -68,17 +50,13 @@ def create_scene(): # Load an OBJ model for our scene helmet_data = o3d.data.FlightHelmetModel() helmet = o3d.io.read_triangle_model(helmet_data.path) - helmet_parts = [] - for m in helmet.meshes: - # m.mesh.paint_uniform_color((1.0, 0.75, 0.3)) - m.mesh.scale(10.0, (0.0, 0.0, 0.0)) - helmet_parts.append(m) + helmet_parts = o3d.t.geometry.TriangleMesh.from_triangle_mesh_model(helmet) # Create a ground plane ground_plane = o3d.geometry.TriangleMesh.create_box( 50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True) ground_plane.compute_triangle_normals() - rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi, 0, 0)) + rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi, 0, 0)) ground_plane.rotate(rotate_180) ground_plane.translate((-25.0, -0.1, -25.0)) ground_plane.paint_uniform_color((1, 1, 1)) @@ -157,12 +135,11 @@ def create_scene(): "geometry": a_sphere }] # Load the helmet - for part in helmet_parts: - name = part.mesh_name - tgeom = o3d.t.geometry.TriangleMesh.from_legacy(part.mesh) - tgeom.material = convert_material_record( - helmet.materials[part.material_idx]) - geoms.append({"name": name, "geometry": tgeom}) + for name, tmesh in helmet_parts.items(): + geoms.append({ + "name": name, + "geometry": tmesh.scale(10.0, (0.0, 0.0, 0.0)) + }) return geoms diff --git a/python/setup.py b/python/setup.py index 029c5717180..e32f69ab615 100644 --- a/python/setup.py +++ b/python/setup.py @@ -70,6 +70,11 @@ def get_tag(self): libc.gnu_get_libc_version.restype = ctypes.c_char_p GLIBC_VER = libc.gnu_get_libc_version().decode("utf8").split(".") plat = f"manylinux_{GLIBC_VER[0]}_{GLIBC_VER[1]}{plat[5:]}" + elif plat[:6] == "macosx": + # If the Python interpreter is an universal2 app the resulting wheel is tagged as + # universal2 instead of the current architecture. This is a workaround to fix it. + plat = plat.replace("universal2", platform.machine()) + return python, abi, plat diff --git a/python/test/ml_ops/test_radius_search.py b/python/test/ml_ops/test_radius_search.py index 85b2af0ad0d..b799b2c7602 100644 --- a/python/test/ml_ops/test_radius_search.py +++ b/python/test/ml_ops/test_radius_search.py @@ -103,7 +103,7 @@ def test_radius_search(dtype, ml, num_points_queries, metric, if normalize_distances: gt_dist /= radii[i] - np.testing.assert_allclose(dist, gt_dist, rtol=1e-7, atol=1e-8) + np.testing.assert_allclose(dist, gt_dist, rtol=1e-7, atol=1e-7) @mltest.parametrize.ml_cpu_only @@ -236,4 +236,4 @@ def test_radius_search_batches(ml, batch_size): if normalize_distances: gt_dist /= radii[i] - np.testing.assert_allclose(dist, gt_dist, rtol=1e-7, atol=1e-8) + np.testing.assert_allclose(dist, gt_dist, rtol=1e-7, atol=1e-7) diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 4ca8d5e98ab..0eec5b3a4e5 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -660,3 +660,52 @@ def test_select_by_index_64(device): untouched_sphere.vertex.positions) assert sphere_custom.triangle.indices.allclose( untouched_sphere.triangle.indices) + + +def check_no_unreferenced_vertices(device, int_t, float_t): + sphere = o3d.t.geometry.TriangleMesh.create_sphere(1, 3, float_t, int_t, + device) + expected_sphere = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, float_t, int_t, device) + + sphere.remove_unreferenced_vertices() + + # nothing should be removed + assert sphere.vertex.positions.allclose(expected_sphere.vertex.positions) + assert sphere.triangle.indices.allclose(expected_sphere.triangle.indices) + + +def check_remove_unreferenced_vertices(device, int_t, float_t): + expected_mobius = o3d.t.geometry.TriangleMesh.create_mobius( + 10, 2, 1, 1, 1, 1, 1, float_t, int_t, device) + + verts = o3c.Tensor( + [[0.5, 0.0, 0.0], [1.5, 0.0, 0.0], [0.424307, 0.308277, -0.154508], + [1.19373, 0.867294, 0.154508], [0.184017, 0.566346, -0.293893], + [0.434017, 1.33577, 0.293893], [-0.218199, 0.671548, -0.404508], + [-0.399835, 1.23057, 0.404508], [-0.684017, 0.496967, -0.475528], + [-0.934017, 0.678603, 0.475528], [-1.0, 0.0, -0.5], [-1.0, 0.0, 0.5], + [-0.934017, -0.678603, -0.475528], [-0.684017, -0.496967, 0.475528], + [-0.399835, -1.23057, -0.404508], [-0.218199, -0.671548, 0.404508], + [0.434017, -1.33577, -0.293893], [0.184017, -0.566346, 0.293893], + [0, 0, 0], [1.19373, -0.867294, -0.154508], [1, 1, 1], + [0.424307, -0.308277, 0.154508]], float_t, device) + + tris = o3c.Tensor( + [[0, 3, 1], [0, 2, 3], [3, 2, 4], [3, 4, 5], [4, 7, 5], [4, 6, 7], + [7, 6, 8], [7, 8, 9], [8, 11, 9], [8, 10, 11], [11, 10, 12], + [11, 12, 13], [12, 15, 13], [12, 14, 15], [15, 14, 16], [15, 16, 17], + [16, 21, 17], [16, 19, 21], [19, 21, 1], [1, 21, 0]], int_t, device) + + mobius = o3d.t.geometry.TriangleMesh(verts, tris) + mobius.remove_unreferenced_vertices() + assert mobius.vertex.positions.allclose(expected_mobius.vertex.positions) + assert mobius.triangle.indices.allclose(expected_mobius.triangle.indices) + + +@pytest.mark.parametrize("device", list_devices()) +@pytest.mark.parametrize("int_t", (o3c.int32, o3c.int64)) +@pytest.mark.parametrize("float_t", (o3c.float32, o3c.float64)) +def test_remove_unreferenced_vertices(device, int_t, float_t): + check_no_unreferenced_vertices(device, int_t, float_t) + check_remove_unreferenced_vertices(device, int_t, float_t) diff --git a/util/ci_utils.sh b/util/ci_utils.sh index 38ff654880e..aa30d2c12ad 100644 --- a/util/ci_utils.sh +++ b/util/ci_utils.sh @@ -55,8 +55,14 @@ install_python_dependencies() { TF_ARCH_DISABLE_NAME=tensorflow-cpu TORCH_GLNX="torch==$TORCH_CUDA_GLNX_VER" else - TF_ARCH_NAME=tensorflow-cpu - TF_ARCH_DISABLE_NAME=tensorflow + # tensorflow-cpu wheels for macOS arm64 are not available + if [[ "$OSTYPE" == "darwin"* ]]; then + TF_ARCH_NAME=tensorflow + TF_ARCH_DISABLE_NAME=tensorflow + else + TF_ARCH_NAME=tensorflow-cpu + TF_ARCH_DISABLE_NAME=tensorflow + fi TORCH_GLNX="torch==$TORCH_CPU_GLNX_VER" fi