From 42b5401708ba788fabc486ac109e08c62178d901 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 28 Jan 2026 14:56:30 -0500 Subject: [PATCH 1/6] Squashed 'vendor/apriltag/' changes from d4f8b0af..c5a4fc1c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c5a4fc1c Merge pull request #421 from sidd-27/opti-hashtable a00f7c3e fix warnings 16bfd23c adds testing 310dfb35 adds null checks to calloc and malloc 7d18fc56 refactor: implement pigeonhole based lookup table 31b29af3 Merge pull request #417 from chibai/master 7dc83848 add test_tag_pose_estimation for both c and py dc6316dc add python wrap for estimate_tag_pose 4cdd4fcb Merge pull request #416 from rzblue/image_u8_static 5108e616 make internal image_u8 functions static d63f8170 Merge pull request #414 from sidd-27/bug-capacity 05a40ba9 corrects capacity calculations 22b44c13 Merge pull request #405 from jvanvugt/joris/lfps-avoid-memcpy 2d0466d5 Merge pull request #404 from jvanvugt/joris/path-halving-union-find ad4a17fe Use local variables for accumulation 717dab55 Use path halving in unionfind to improve performance b1381d33 Merge pull request #403 from jvanvugt/joris/use-stackbuffer-for-ptsort f10450ed Avoid malloc overhead for small arrays in ptsort 3c97a99d Merge pull request #402 from VincentMarnier/master 24c8ecc0 chore: added comments to cross-compilation.yml 02e4db11 chore: added workflow to test cross compilation 6d36272e fix: windows crosscompilation d7aa51cb Merge pull request #400 from reprojection-calibration/readme-fix da104694 Remove local build hint 179b3ad9 Fix opencv readme section 94be7839 Merge pull request #399 from christian-rauch/rel_345 72accb4e release version 3.4.5 1e98edb6 Merge pull request #398 from christian-rauch/colcon_almalinux f525885e manually include 'pythread.h' in case this is not included by 'Python.h' c2397321 remove manual git installation a2d53aaf update 'actions/checkout' df24e0e2 add AlmaLinux to colcon CI a53b368f Merge pull request #396 from MqCreaple/pull-request 546680b9 add multithreading convolution for image_u8 59a2cf1d improve performance of image_u8_convolve_2D 3950db97 Merge pull request #393 from christian-rauch/rel_344 f2dc412d release version 3.4.4 41b39efa Merge pull request #390 from traversaro/patch-1 95dd4a1a When using CMake >= 3.24 use CMAKE_COMPILE_WARNING_AS_ERROR variable instead of setting `-Werror` directly c2d99936 Merge pull request #392 from christian-rauch/update_windows_runner b4330979 remove the colcon job on Windows due to issues with the GitHub actions: - https://github.com/ros-tooling/setup-ros/issues/839 - https://github.com/ros-tooling/action-ros-ci/issues/1000 68e98e28 update 'actions/setup-python' action 441967e6 update 'ros-tooling/action-ros-ci' action 8fd2152d replace deprecated 'windows-2019' runner with 'windows-latest' ce3ccd6b Merge pull request #386 from Jgunde/master 34b5b740 Lock before releasing GIL f11c949c Merge pull request #384 from Jgunde/patch-1 697ff80f Release GIL during detection 36763892 Merge pull request #377 from clysto/master 018a94a9 set linker flags only for target apriltag 41811f5e fix #352 fb2a4096 Merge pull request #371 from christian-rauch/release_343 8d6a4bc4 release version 3.4.3 5dc0286b Merge pull request #372 from christian-rauch/win32_lean_and_mean fff05796 compile with WIN32_LEAN_AND_MEAN on Windows 41f5bb0c Merge pull request #297 from mitjap/fix-lean-and-mean f765119f Merge pull request #368 from KySmith1/readme-coordinate-system 7f9467b7 Add clarity to tag coordinate system reference frame fc2a7b20 Merge pull request #364 from cottsay/cmake-config-dir c0d9ef15 Install CMake config to architecture-specific location 724a7d81 Merge pull request #362 from zhaoxi-scut/matd_memory_fix 0106e3e0 Close the zero variadic macro warning for Clang 6d0f9e0f Modify the memory allocation method of matd_t 4dfd338d Merge pull request #360 from cottsay/cottsay/top-level-tests aa503c59 Call enable_testing() in top-level project 1121feac Merge pull request #346 from NewThinker-Jeffrey/jeffrey/master 8bd4f76a Merge pull request #356 from Chris-F5/remove-crlf e285f15e Merge pull request #358 from Chris-F5/fix-readme 2e3fa040 Fix README C example f444e368 Replace non-ASCII characters 20a047ca Remove CRLF line ends 786ad11f Merge pull request #353 from christian-rauch/ci_test_python 5e69ee2f test importing the apriltag Python wrapper cb522aec Fix the inconsistent image coordinate conventions. 4b980f1a Use bilinear interpolation in refine_edges() 620b8423 Merge pull request #348 from christian-rauch/fix_test a626cced reference corner coordinates for detection without decimation 9671af8e fix the tag ID comparison and let it return the tag difference 3806edf3 Merge pull request #341 from s-trinh/add_missing_copyright_header 9dfec684 Add missing copyright header in tag36h10.c file 188c0e02 Merge pull request #339 from berteauxjb/fix/workerpool c16bba61 workerpool: Properly wait for initialization of worker threads aa5951aa workerpool: Add predicate to condition variable startcond 64654c00 Merge pull request #337 from s-trinh/fix_WIN32_macro 78111bb1 Use "_WIN32" macro everywhere for consistency. 6319d842 Merge pull request #336 from Suave101/patch-1 f053d25d Update README.md c4449c7b Merge pull request #332 from peci1/patch-1 11f30d37 apriltag_quad_thresh: Prevent using decimate for scale smaller than 1 21be60d7 Merge pull request #331 from LosWheatleynDew/master f00d0193 changed name apriltag_py_type.docstring 12cf56b9 Merge pull request #329 from christian-rauch/ci_bloom 5c14b199 increment patch version to 3.4.2 dc221322 initialise 'maxv' 8497f08c test blooming Debian packages abf7d58c Merge pull request #324 from christian-rauch/jazzy 8421879e increment the patch version cb822e0b add next ROS 2 release "jazzy" to CI 7dcb86de Merge pull request #327 from christian-rauch/ubuntu2404 ff5462e4 sort detections in test and compare coordinate double values within limits 97d85070 order reference detections and remove index 6f9531b8 test all Ubuntu LTS versions a93d6fc0 address "‘last_quadrant’ may be used uninitialized" error 16df4204 fix calloc parameter order aafb845c rename custom 'getline' to avoid symbol lookup conflicts 5476e5a9 remove examples gitignore 59cae917 Merge pull request #325 from christian-rauch/fix_homography_assert 3e866a43 fix compilation with WIN32_LEAN_AND_MEAN c8291621 skip homography when no row for swapping is found c687312e avoid division-by-zero and drawing line between identical coordinates 1110fd6e Merge pull request #323 from christian-rauch/reset_errno 17d1e865 reset errno at the beginning of Python function that check it in the end 77a769ac Merge pull request #316 from christian-rauch/rel_34 77928d8e bump minor version to prepare release e6cd3c7a Merge pull request #319 from EwingKang/fix_cmake_as_submodule 64e90400 Improve CMakeLists config path by using CMAKE_CURRENT_BINARY_DIR 51466ec5 Merge pull request #317 from christian-rauch/fix_len0 3a0a155d check for negative index 359a9840 check for 0 length aa9e66ce check determinant first f8ce1851 Merge pull request #314 from christian-rauch/test 44ce5f9e set C standard 9144bee4 add DLL to test folder for Windows CI tests 14c3a99b add tests d8dd3657 run tests from "test" directory ad010377 add example images with reference detections 69c21f0a Merge pull request #315 from christian-rauch/lang_cxx_optional ef12c1c9 remove the shared libs option from colcon CI and use the default ff7ecb49 require C++ language support only for the optional OpenCV demo 786ee019 rename pthreads_cross to a C source file 6a7b315e Merge pull request #313 from christian-rauch/rm_makefile 91aa0d11 remove old UNIX-specific Makefile 7a47a11c Merge pull request #311 from christian-rauch/test_shared_off 68ab7e39 remove gcc from macOS builds 590e0250 fix compilation under macOS with "Clang" instead of "AppleClang" caa865a3 set Python install directory via CMAKE_INSTALL_PREFIX e4b263b7 add python3-dev as build dependency 936a696b initialise last_theta 1ff4c8c6 test installation via CMake 95ae43a1 add generated docstring headers as dependency 880d9c71 set POSITION_INDEPENDENT_CODE for apriltag library 12220b33 default initialise PyMethodDef ff5710fd fill PyModuleDef 37b3dccd use Python3_add_library to define Python library with all required flags 1d7cb87d manipulate signal handling only on POSIX compatible systems 0fb0111a remove trailing whitespace 1c4f81d2 use vtk_encode_string to encode text as C array 3a8a3142 only generate header 4178ef0a vtkEncodeString.cmake 607299e8 use FindPython3 to find Python development files and NumPy f0a23f6f install Python and NumPy b77a5634 test building shared/static libraries on CI 5084e305 update checkout action 7960bf06 cleanup the CMake pipeline git-subtree-dir: vendor/apriltag git-subtree-split: c5a4fc1cc5c6edcb5929dfc3a79f167bf173f7a5 --- .github/workflows/bloom.yml | 53 ++ .github/workflows/cmake-multi-platform.yml | 63 ++- .github/workflows/cmake-ubuntu.yml | 62 +++ .github/workflows/colcon-workspace.yml | 84 +-- .github/workflows/cross-compilation.yml | 102 ++++ CMake/vtkEncodeString.cmake | 279 ++++++++++ CMakeLists.txt | 113 ++-- Makefile | 41 -- README.md | 29 +- apriltag.c | 317 ++++++----- apriltag_detect.docstring | 9 +- apriltag_estimate_tag_pose.docstring | 70 +++ apriltag_pose.h | 5 +- apriltag_py_type.docstring | 2 +- apriltag_pywrap.c | 220 +++++++- apriltag_quad_thresh.c | 87 +-- common/g2d.c | 4 +- common/image_u8.c | 15 +- common/image_u8_parallel.c | 165 ++++++ common/image_u8_parallel.h | 19 + common/matd.c | 13 +- common/matd.h | 3 +- .../{pthreads_cross.cpp => pthreads_cross.c} | 512 +++++++++--------- common/pthreads_cross.h | 164 +++--- common/time_util.h | 6 +- common/unionfind.h | 20 +- common/workerpool.c | 31 +- common/zmaxheap.c | 2 +- example/.gitignore | 1 - example/Makefile | 32 -- example/README | 1 - install.sh | 16 - package.xml | 3 +- python_build_flags.py | 34 -- tag36h10.c | 27 + test/CMakeLists.txt | 50 ++ test/data/33369213973_9d9bb4cc96_c.jpg | Bin 0 -> 131366 bytes test/data/33369213973_9d9bb4cc96_c.txt | 12 + test/data/34085369442_304b6bafd9_c.jpg | Bin 0 -> 120676 bytes test/data/34085369442_304b6bafd9_c.txt | 25 + test/data/34139872896_defdb2f8d9_c.jpg | Bin 0 -> 117625 bytes test/data/34139872896_defdb2f8d9_c.txt | 10 + test/data/README.md | 6 + test/getline.c | 55 ++ test/getline.h | 9 + test/test_detection.c | 170 ++++++ test/test_quick_decode.c | 126 +++++ test/test_tag_pose_estimation.c | 209 +++++++ test/test_tag_pose_estimation.py | 117 ++++ 49 files changed, 2546 insertions(+), 847 deletions(-) create mode 100644 .github/workflows/bloom.yml create mode 100644 .github/workflows/cmake-ubuntu.yml create mode 100644 .github/workflows/cross-compilation.yml create mode 100644 CMake/vtkEncodeString.cmake delete mode 100644 Makefile create mode 100644 apriltag_estimate_tag_pose.docstring create mode 100644 common/image_u8_parallel.c create mode 100644 common/image_u8_parallel.h rename common/{pthreads_cross.cpp => pthreads_cross.c} (95%) delete mode 100644 example/.gitignore delete mode 100644 example/Makefile delete mode 100644 example/README delete mode 100755 install.sh delete mode 100644 python_build_flags.py create mode 100644 test/CMakeLists.txt create mode 100644 test/data/33369213973_9d9bb4cc96_c.jpg create mode 100644 test/data/33369213973_9d9bb4cc96_c.txt create mode 100644 test/data/34085369442_304b6bafd9_c.jpg create mode 100644 test/data/34085369442_304b6bafd9_c.txt create mode 100644 test/data/34139872896_defdb2f8d9_c.jpg create mode 100644 test/data/34139872896_defdb2f8d9_c.txt create mode 100644 test/data/README.md create mode 100644 test/getline.c create mode 100644 test/getline.h create mode 100644 test/test_detection.c create mode 100644 test/test_quick_decode.c create mode 100644 test/test_tag_pose_estimation.c create mode 100644 test/test_tag_pose_estimation.py diff --git a/.github/workflows/bloom.yml b/.github/workflows/bloom.yml new file mode 100644 index 00000000..f74f0c0b --- /dev/null +++ b/.github/workflows/bloom.yml @@ -0,0 +1,53 @@ +name: bloom + +on: [push, pull_request] + +jobs: + build_linux: + name: "Ubuntu (${{ matrix.ros_distribution }})" + + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - docker_image: ubuntu:20.04 + ros_distribution: noetic + + - docker_image: ubuntu:22.04 + ros_distribution: humble + + - docker_image: ubuntu:24.04 + ros_distribution: jazzy + + container: + image: ${{ matrix.docker_image }} + + env: + DEBIAN_FRONTEND: noninteractive + + steps: + - name: install core dependencies + run: | + apt update + apt install -y --no-install-recommends git ca-certificates + + - uses: actions/checkout@v4 + + - uses: ros-tooling/setup-ros@v0.7 + + - name: install build tool dependencies + run: | + apt install -y --no-install-recommends devscripts equivs python3-bloom + + - name: bloom + run: | + rosdep update + bloom-generate rosdebian --ros-distro ${{ matrix.ros_distribution }} + mk-build-deps + apt install -y --no-install-recommends ./ros-${{ matrix.ros_distribution }}-apriltag-build-deps_*_all.deb + dpkg-buildpackage -b + + - name: install bloomed packages + run: | + apt install -y --no-install-recommends ../ros-${{ matrix.ros_distribution }}-apriltag_*.deb ../ros-${{ matrix.ros_distribution }}-apriltag-dbgsym_*.ddeb diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 5f735016..1a0f6aa6 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -1,6 +1,4 @@ -# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. -# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml -name: CMake on multiple platforms +name: CMake on: push: @@ -13,19 +11,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: - # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. - fail-fast: false - - # Set up a matrix to run the following 3 configurations: - # 1. - # 2. - # 3. - # - # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. matrix: os: [ubuntu-latest, windows-latest, macos-latest] build_type: [Release] c_compiler: [gcc, clang, cl] + shared_libs: ['ON', 'OFF'] include: - os: windows-latest c_compiler: cl @@ -45,20 +35,18 @@ jobs: - os: macos-latest c_compiler: clang cpp_compiler: clang++ - - os: macos-latest - c_compiler: gcc - cpp_compiler: g++ exclude: - os: ubuntu-latest c_compiler: cl - os: macos-latest c_compiler: cl + - os: macos-latest + c_compiler: gcc steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set reusable strings - # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. id: strings shell: bash run: | @@ -68,23 +56,46 @@ jobs: - uses: ilammy/msvc-dev-cmd@v1 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - run: pip install numpy + - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -G Ninja - -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -D CMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -D CMAKE_C_COMPILER=${{ matrix.c_compiler }} + -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} + -D BUILD_SHARED_LIBS=${{ matrix.shared_libs }} + -D BUILD_TESTING=ON -S ${{ github.workspace }} - name: Build - # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} + - name: Install (Windows) + if: matrix.os == 'windows-latest' + run: Start-Process -Verb RunAs -FilePath cmake "--build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} --target install" + + - name: Install (sudo) + if: matrix.os != 'windows-latest' + run: sudo cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} --target install + + - name: add DLL to test folder + if: matrix.os == 'windows-latest' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + cp *apriltag.dll test/ + - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} - # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest --build-config ${{ matrix.build_type }} + run: | + ctest --build-config ${{ matrix.build_type }} --no-tests=error --output-on-failure --verbose + + - name: Test Python Module Import + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + python3 -c "import apriltag; apriltag.apriltag(family='tag36h11')" diff --git a/.github/workflows/cmake-ubuntu.yml b/.github/workflows/cmake-ubuntu.yml new file mode 100644 index 00000000..15d8a545 --- /dev/null +++ b/.github/workflows/cmake-ubuntu.yml @@ -0,0 +1,62 @@ +name: CMake (Ubuntu) + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + version: ["20.04", "22.04", "24.04"] + c_compiler: [gcc, clang] + shared_libs: ['ON', 'OFF'] + # manual selection for the latest, non-default, compilers + include: + - version: "24.04" + c_compiler: gcc-14 + - version: "24.04" + c_compiler: clang-18 + + container: + image: ubuntu:${{ matrix.version }} + + env: + DEBIAN_FRONTEND: noninteractive + + steps: + - name: install dependencies + run: | + apt update + apt install -y --no-install-recommends cmake ninja-build ${{ matrix.c_compiler }} + apt install -y --no-install-recommends python3-dev python3-numpy + + - uses: actions/checkout@v4 + + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=$GITHUB_WORKSPACE/build" >> "$GITHUB_OUTPUT" + + - name: Configure CMake + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -G Ninja + -D CMAKE_C_COMPILER=${{ matrix.c_compiler }} + -D CMAKE_BUILD_TYPE=Release + -D BUILD_SHARED_LIBS=${{ matrix.shared_libs }} + -D BUILD_TESTING=ON + -S $GITHUB_WORKSPACE + + - name: Build & Install + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --target install + + - name: Test + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + ctest --no-tests=error --output-on-failure --verbose diff --git a/.github/workflows/colcon-workspace.yml b/.github/workflows/colcon-workspace.yml index ca37f4b3..a067d97d 100644 --- a/.github/workflows/colcon-workspace.yml +++ b/.github/workflows/colcon-workspace.yml @@ -5,14 +5,12 @@ on: [push, pull_request] jobs: # build on Ubuntu docker images build_linux: - name: "Ubuntu (${{ matrix.ros_distribution }}, shared: ${{ matrix.cmake_shared_libs }})" + name: "${{ matrix.docker_image }} (${{ matrix.ros_distribution }})" runs-on: ubuntu-latest strategy: matrix: - ros_distribution: [noetic, humble] - cmake_shared_libs: ['ON', 'OFF'] include: - docker_image: ubuntu:20.04 ros_distribution: noetic @@ -22,95 +20,56 @@ jobs: ros_distribution: humble ros_version: 2 - container: - image: ${{ matrix.docker_image }} - - steps: - - name: install core dependencies - run: | - apt update - apt install --no-install-recommends -y git ca-certificates - - - uses: actions/checkout@v4 - - - name: Setup ROS environment - uses: ros-tooling/setup-ros@v0.7 - - - name: ROS 1 CI Action - if: ${{ matrix.ros_version == 1 }} - uses: ros-tooling/action-ros-ci@v0.3 - with: - package-name: apriltag - target-ros1-distro: ${{ matrix.ros_distribution }} - extra-cmake-args: "-DBUILD_SHARED_LIBS=${{ matrix.cmake_shared_libs }}" - - - name: ROS 2 CI Action - if: ${{ matrix.ros_version == 2 }} - uses: ros-tooling/action-ros-ci@v0.3 - with: - package-name: apriltag - target-ros2-distro: ${{ matrix.ros_distribution }} - extra-cmake-args: "-DBUILD_SHARED_LIBS=${{ matrix.cmake_shared_libs }}" - - - # build on Windows native - build_windows: - name: "Windows (${{ matrix.ros_distribution }}, shared: ${{ matrix.cmake_shared_libs }})" - - runs-on: windows-2019 + - docker_image: ubuntu:24.04 + ros_distribution: jazzy + ros_version: 2 - strategy: - matrix: - ros_distribution: [noetic, humble] - cmake_shared_libs: ['OFF'] - include: - - ros_distribution: noetic - ros_version: 1 + - docker_image: almalinux:8 + ros_distribution: humble + ros_version: 2 - - ros_distribution: humble + - docker_image: almalinux:9 + ros_distribution: jazzy ros_version: 2 + container: + image: ${{ matrix.docker_image }} + steps: - - uses: actions/checkout@v4 - with: - submodules: recursive + - uses: actions/checkout@v5 - name: Setup ROS environment uses: ros-tooling/setup-ros@v0.7 - name: ROS 1 CI Action if: ${{ matrix.ros_version == 1 }} - uses: ros-tooling/action-ros-ci@v0.3 + uses: ros-tooling/action-ros-ci@v0.4 with: package-name: apriltag target-ros1-distro: ${{ matrix.ros_distribution }} - extra-cmake-args: "-DBUILD_SHARED_LIBS=${{ matrix.cmake_shared_libs }}" - name: ROS 2 CI Action if: ${{ matrix.ros_version == 2 }} - uses: ros-tooling/action-ros-ci@v0.3 + uses: ros-tooling/action-ros-ci@v0.4 with: package-name: apriltag target-ros2-distro: ${{ matrix.ros_distribution }} - extra-cmake-args: "-DBUILD_SHARED_LIBS=${{ matrix.cmake_shared_libs }}" + # build on macOS native build_macos: - name: "macOS (${{ matrix.ros_distribution }}, shared: ${{ matrix.cmake_shared_libs }})" + name: "macOS (${{ matrix.ros_distribution }})" runs-on: macos-latest strategy: matrix: - ros_distribution: [humble] - cmake_shared_libs: ['OFF'] + ros_distribution: [humble, jazzy] steps: - - uses: actions/checkout@v4 - with: - submodules: recursive + - uses: actions/checkout@v5 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.10" @@ -118,8 +77,7 @@ jobs: uses: ros-tooling/setup-ros@v0.7 - name: ROS 2 CI Action - uses: ros-tooling/action-ros-ci@v0.3 + uses: ros-tooling/action-ros-ci@v0.4 with: package-name: apriltag target-ros2-distro: ${{ matrix.ros_distribution }} - extra-cmake-args: "-DBUILD_SHARED_LIBS=${{ matrix.cmake_shared_libs }}" diff --git a/.github/workflows/cross-compilation.yml b/.github/workflows/cross-compilation.yml new file mode 100644 index 00000000..c9d08ac0 --- /dev/null +++ b/.github/workflows/cross-compilation.yml @@ -0,0 +1,102 @@ +name: Cross compilation + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + +jobs: + # Use mingw to cross compile apriltag and its tests on linux + build: + runs-on: ubuntu-latest + + strategy: + matrix: + shared_libs: ['ON','OFF'] + + steps: + # Install required packages to build for windows + - name: install dependencies + run: | + sudo apt update + sudo apt install -y --no-install-recommends cmake ninja-build gcc-mingw-w64-x86-64 + + # Get the sources + - uses: actions/checkout@v4 + + # Save the "build/" folder path + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=$GITHUB_WORKSPACE/build" >> "$GITHUB_OUTPUT" + + # Configure cmake + # CMAKE_SYSTEM_NAME=Windows is needed else it will produce libapriltag.so instead of .dll + - name: Configure CMake + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -G Ninja + -D CMAKE_SYSTEM_NAME=Windows + -D CMAKE_C_COMPILER=/usr/bin/x86_64-w64-mingw32-gcc + -D CMAKE_BUILD_TYPE=Release + -D BUILD_SHARED_LIBS=${{ matrix.shared_libs }} + -D BUILD_TESTING=ON + -S $GITHUB_WORKSPACE + + # Build apriltag + - name: Build + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} + + # In case of shared lib, copy the .dll file in the test directory + - name: add DLL to test folder + if: matrix.shared_libs == 'ON' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + cp *apriltag.dll test/ + + # Upload the "build/" directory as artifact + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: cross-compilation-build-sharedlib-${{ matrix.shared_libs }} + path: ${{ steps.strings.outputs.build-output-dir }} + + # On windows, retrieve the cross compiled build and run the tests + test: + needs: build + runs-on: windows-latest + + strategy: + matrix: + shared_libs: ['ON','OFF'] + + steps: + - uses: actions/checkout@v4 + + # Save the "build/" folder path + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=$GITHUB_WORKSPACE/build" >> "$GITHUB_OUTPUT" + + # Download the "build/" directory + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: cross-compilation-build-sharedlib-${{ matrix.shared_libs }} + path: ${{ steps.strings.outputs.build-output-dir }} + + # Change the tests pathes so they match the windows ones instead of the linux ones + - name: Change test pathes + run: | + (Get-Content ${{ steps.strings.outputs.build-output-dir }}/test/CTestTestfile.cmake) -replace '/home/runner/work/apriltag/apriltag', $PWD.Path.Replace('\', '/') | Set-Content ${{ steps.strings.outputs.build-output-dir }}/test/CTestTestfile.cmake + + # Run the tests + - name: Test + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + ctest --no-tests=error --output-on-failure --verbose diff --git a/CMake/vtkEncodeString.cmake b/CMake/vtkEncodeString.cmake new file mode 100644 index 00000000..25a332d2 --- /dev/null +++ b/CMake/vtkEncodeString.cmake @@ -0,0 +1,279 @@ +#[==[ + +Copyright (c) 1993-2015 Ken Martin, Will Schroeder, Bill Lorensen +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither name of Ken Martin, Will Schroeder, or Bill Lorensen nor the names + of any contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#]==] + +#[==[ +@file vtkEncodeString.cmake + +This module contains the @ref vtk_encode_string function which may be used to +turn a file into a C string. This is primarily used within a program so that +the content does not need to be retrieved from the filesystem at runtime, but +can still be developed as a standalone file. +#]==] + +set(_vtkEncodeString_script_file "${CMAKE_CURRENT_LIST_FILE}") + +#[==[ +@brief Encode a file as a C string at build time + +Adds a rule to turn a file into a C string. Note that any Unicode characters +will not be replaced with escaping, so it is recommended to avoid their usage +in the input. + +~~~ +vtk_encode_string( + INPUT + [NAME ] + [EXPORT_SYMBOL ] + [EXPORT_HEADER
] + [HEADER_OUTPUT ] + [SOURCE_OUTPUT ] + + [ABI_MANGLE_SYMBOL_BEGIN ] + [ABI_MANGLE_SYMBOL_END ] + [ABI_MANGLE_HEADER
] + + [BINARY] [NUL_TERMINATE]) +~~~ + +The only required variable is `INPUT`, however, it is likely that at least one +of `HEADER_OUTPUT` or `SOURCE_OUTPUT` will be required to add them to a +library. + + * `INPUT`: (Required) The path to the file to be embedded. If a relative path + is given, it will be interpreted as being relative to + `CMAKE_CURRENT_SOURCE_DIR`. + * `NAME`: This is the base name of the files that will be generated as well + as the variable name for the C string. It defaults to the basename of the + input without extensions. + * `EXPORT_SYMBOL`: The symbol to use for exporting the variable. By default, + it will not be exported. If set, `EXPORT_HEADER` must also be set. + * `EXPORT_HEADER`: The header to include for providing the given export + symbol. If set, `EXPORT_SYMBOL` should also be set. + * `HEADER_OUTPUT`: The variable to store the generated header path. + * `SOURCE_OUTPUT`: The variable to store the generated source path. + * `BINARY`: If given, the data will be written as an array of `unsigned char` + bytes. + * `NUL_TERMINATE`: If given, the binary data will be `NUL`-terminated. Only + makes sense with the `BINARY` flag. This is intended to be used if + embedding a file as a C string exceeds compiler limits on string literals + in various compilers. + * `ABI_MANGLE_SYMBOL_BEGIN`: Open a mangling namespace with the given symbol. + If given, `ABI_MANGLE_SYMBOL_END` and `ABI_MANGLE_HEADER` must also be set. + * `ABI_MANGLE_SYMBOL_END`: Close a mangling namespace with the given symbol. + If given, `ABI_MANGLE_SYMBOL_BEGIN` and `ABI_MANGLE_HEADER` must also be set. + * `ABI_MANGLE_HEADER`: The header which provides the ABI mangling symbols. + If given, `ABI_MANGLE_SYMBOL_BEGIN` and `ABI_MANGLE_SYMBOL_END` must also + be set. +#]==] +function (vtk_encode_string) + cmake_parse_arguments(PARSE_ARGV 0 _vtk_encode_string + "BINARY;NUL_TERMINATE" + "INPUT;NAME;EXPORT_SYMBOL;EXPORT_HEADER;HEADER_OUTPUT;SOURCE_OUTPUT;ABI_MANGLE_SYMBOL_BEGIN;ABI_MANGLE_SYMBOL_END;ABI_MANGLE_HEADER" + "") + + if (_vtk_encode_string_UNPARSED_ARGUMENTS) + message(FATAL_ERROR + "Unrecognized arguments to vtk_encode_string: " + "${_vtk_encode_string_UNPARSED_ARGUMENTS}") + endif () + + if (NOT DEFINED _vtk_encode_string_INPUT) + message(FATAL_ERROR + "Missing `INPUT` for vtk_encode_string.") + endif () + + if (NOT DEFINED _vtk_encode_string_NAME) + get_filename_component(_vtk_encode_string_NAME + "${_vtk_encode_string_INPUT}" NAME_WE) + endif () + + if (DEFINED _vtk_encode_string_EXPORT_SYMBOL AND + NOT DEFINED _vtk_encode_string_EXPORT_HEADER) + message(FATAL_ERROR + "Missing `EXPORT_HEADER` when using `EXPORT_SYMBOL`.") + endif () + + if (DEFINED _vtk_encode_string_EXPORT_HEADER AND + NOT DEFINED _vtk_encode_string_EXPORT_SYMBOL) + message(WARNING + "Missing `EXPORT_SYMBOL` when using `EXPORT_HEADER`.") + endif () + + if (DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_BEGIN AND + (NOT DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_END OR + NOT DEFINED _vtk_encode_string_ABI_MANGLE_HEADER)) + message(WARNING + "Missing `ABI_MANGLE_SYMBOL_END` or `ABI_MANGLE_HEADER` when using " + "`ABI_MANGLE_SYMBOL_BEGIN`.") + endif () + + if (DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_END AND + (NOT DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_BEGIN OR + NOT DEFINED _vtk_encode_string_ABI_MANGLE_HEADER)) + message(WARNING + "Missing `ABI_MANGLE_SYMBOL_BEGIN` or `ABI_MANGLE_HEADER` when using " + "`ABI_MANGLE_SYMBOL_END`.") + endif () + + if (DEFINED _vtk_encode_string_ABI_MANGLE_HEADER AND + (NOT DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_BEGIN OR + NOT DEFINED _vtk_encode_string_ABI_MANGLE_SYMBOL_END)) + message(WARNING + "Missing `ABI_MANGLE_SYMBOL_BEGIN` or `ABI_MANGLE_SYMBOL_END` when " + "using `ABI_MANGLE_HEADER`.") + endif () + + if (NOT _vtk_encode_string_BINARY AND _vtk_encode_string_NUL_TERMINATE) + message(FATAL_ERROR + "The `NUL_TERMINATE` flag only makes sense with the `BINARY` flag.") + endif () + + set(_vtk_encode_string_header + "${CMAKE_CURRENT_BINARY_DIR}/${_vtk_encode_string_NAME}.h") + set(_vtk_encode_string_source + "${CMAKE_CURRENT_BINARY_DIR}/${_vtk_encode_string_NAME}.cxx") + + if (IS_ABSOLUTE "${_vtk_encode_string_INPUT}") + set(_vtk_encode_string_input + "${_vtk_encode_string_INPUT}") + else () + set(_vtk_encode_string_input + "${CMAKE_CURRENT_SOURCE_DIR}/${_vtk_encode_string_INPUT}") + endif () + + set(_vtk_encode_string_depends_args) + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.27") + list(APPEND _vtk_encode_string_depends_args + DEPENDS_EXPLICIT_ONLY) + endif () + + add_custom_command( + OUTPUT ${_vtk_encode_string_header} + ${_vtk_encode_string_source} + DEPENDS "${_vtkEncodeString_script_file}" + "${_vtk_encode_string_input}" + COMMAND "${CMAKE_COMMAND}" + "-Dsource_dir=${CMAKE_CURRENT_SOURCE_DIR}" + "-Dbinary_dir=${CMAKE_CURRENT_BINARY_DIR}" + "-Dsource_file=${_vtk_encode_string_input}" + "-Doutput_name=${_vtk_encode_string_NAME}" + "-Dexport_symbol=${_vtk_encode_string_EXPORT_SYMBOL}" + "-Dexport_header=${_vtk_encode_string_EXPORT_HEADER}" + "-Dabi_mangle_symbol_begin=${_vtk_encode_string_ABI_MANGLE_SYMBOL_BEGIN}" + "-Dabi_mangle_symbol_end=${_vtk_encode_string_ABI_MANGLE_SYMBOL_END}" + "-Dabi_mangle_header=${_vtk_encode_string_ABI_MANGLE_HEADER}" + "-Dbinary=${_vtk_encode_string_BINARY}" + "-Dnul_terminate=${_vtk_encode_string_NUL_TERMINATE}" + "-D_vtk_encode_string_run=ON" + -P "${_vtkEncodeString_script_file}" + ${_vtk_encode_string_depends_args}) + + if (DEFINED _vtk_encode_string_SOURCE_OUTPUT) + set("${_vtk_encode_string_SOURCE_OUTPUT}" + "${_vtk_encode_string_source}" + PARENT_SCOPE) + endif () + + if (DEFINED _vtk_encode_string_HEADER_OUTPUT) + set("${_vtk_encode_string_HEADER_OUTPUT}" + "${_vtk_encode_string_header}" + PARENT_SCOPE) + endif () +endfunction () + +if (_vtk_encode_string_run AND CMAKE_SCRIPT_MODE_FILE) + set(output_source "${binary_dir}/${output_name}.h") + + set(license_topfile "// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen\n// SPDX-License-Identifier: BSD-3-Clause\n") + file(WRITE "${output_source}" ${license_topfile}) + + if (IS_ABSOLUTE "${source_file}") + set(source_file_full "${source_file}") + else () + set(source_file_full "${source_dir}/${source_file}") + endif () + set(hex_arg) + if (binary) + set(hex_arg HEX) + endif () + file(READ "${source_file_full}" original_content ${hex_arg}) + + if (binary) + if (nul_terminate) + string(APPEND original_content "00") + endif () + string(LENGTH "${original_content}" output_size) + math(EXPR output_size "${output_size} / 2") + + file(APPEND "${output_source}" + "#include \"${output_name}.h\"\n\n") + if (abi_mangle_symbol_begin) + file(APPEND "${output_source}" + "${abi_mangle_symbol_begin}\n\n") + endif () + file(APPEND "${output_source}" + "const unsigned char ${output_name}[${output_size}] = {\n") + string(REGEX REPLACE "\([0-9a-f][0-9a-f]\)" ",0x\\1" escaped_content "${original_content}") + # Hard line wrap the file. + string(REGEX REPLACE "\(..........................................................................,\)" "\\1\n" escaped_content "${escaped_content}") + # Remove the leading comma. + string(REGEX REPLACE "^," "" escaped_content "${escaped_content}") + file(APPEND "${output_source}" + "${escaped_content}\n") + file(APPEND "${output_source}" + "};\n") + if (abi_mangle_symbol_end) + file(APPEND "${output_source}" + "${abi_mangle_symbol_end}\n") + endif () + else () + # Escape literal backslashes. + string(REPLACE "\\" "\\\\" escaped_content "${original_content}") + # Escape literal double quotes. + string(REPLACE "\"" "\\\"" escaped_content "${escaped_content}") + # Turn newlines into newlines in the C string. + string(REPLACE "\n" "\\n\"\n\"" escaped_content "${escaped_content}") + + if (abi_mangle_symbol_begin) + file(APPEND "${output_source}" + "${abi_mangle_symbol_begin}\n\n") + endif () + file(APPEND "${output_source}" + "const char ${output_name}[] =\n") + file(APPEND "${output_source}" + "\"${escaped_content}\";\n") + if (abi_mangle_symbol_end) + file(APPEND "${output_source}" + "\n${abi_mangle_symbol_end}\n") + endif () + endif () +endif () diff --git a/CMakeLists.txt b/CMakeLists.txt index f8ff30c9..5b238324 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(apriltag VERSION 3.3.0 LANGUAGES C CXX) +project(apriltag VERSION 3.4.5 LANGUAGES C) if(POLICY CMP0077) cmake_policy(SET CMP0077 NEW) @@ -34,6 +34,10 @@ set(default_build_type "Release") SET(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +if(WIN32) + add_compile_definitions(WIN32_LEAN_AND_MEAN) +endif() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to '${default_build_type}' as none was specified.") set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) @@ -42,13 +46,21 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) endif() if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Werror) - # add_compile_options(-Wpedantic) + add_compile_options(-Wall -Wextra) + add_compile_options(-Wpedantic) + if(CMAKE_C_COMPILER_ID MATCHES "Clang") + add_compile_options( + -Wno-gnu-zero-variadic-macro-arguments + -Wno-strict-prototypes + -Wno-static-in-inline + ) + endif() add_compile_options(-Wno-shift-negative-value) -endif() - -if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_C_COMPILER_ID MATCHES "AppleClang" AND NOT CMAKE_C_SIMULATE_ID MATCHES "MSVC") - add_link_options("-Wl,-z,relro,-z,now,-z,defs") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24.0") + set(CMAKE_COMPILE_WARNING_AS_ERROR ON) + else() + add_compile_options(-Werror) + endif() endif() if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND CMAKE_C_SIMULATE_ID MATCHES "MSVC") @@ -65,6 +77,11 @@ set(APRILTAG_SRCS apriltag.c apriltag_pose.c apriltag_quad_thresh.c) # Library file(GLOB TAG_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tag*.c) add_library(${PROJECT_NAME} ${APRILTAG_SRCS} ${COMMON_SRC} ${TAG_FILES}) +set_property(TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) + +if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT APPLE AND NOT CMAKE_C_SIMULATE_ID MATCHES "MSVC") + target_link_options(${PROJECT_NAME} PRIVATE "-Wl,-z,relro,-z,now,-z,defs") +endif() if (MSVC) add_compile_definitions("_CRT_SECURE_NO_WARNINGS") @@ -79,6 +96,8 @@ endif() set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION 3 VERSION ${PROJECT_VERSION}) set_target_properties(${PROJECT_NAME} PROPERTIES DEBUG_POSTFIX "d") +set_property(TARGET ${PROJECT_NAME} PROPERTY C_STANDARD 99) + include(GNUInstallDirs) target_include_directories(${PROJECT_NAME} PUBLIC @@ -101,7 +120,7 @@ set(generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated") set(version_config "${generated_dir}/${PROJECT_NAME}ConfigVersion.cmake") set(project_config "${generated_dir}/${PROJECT_NAME}Config.cmake") set(targets_export_name "${PROJECT_NAME}Targets") -set(config_install_dir "share/${PROJECT_NAME}/cmake") +set(config_install_dir "${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}/cmake") # Include module with fuction 'write_basic_package_version_file' include(CMakePackageConfigHelpers) @@ -141,52 +160,48 @@ export(TARGETS apriltag # install pkgconfig file configure_file(${PROJECT_NAME}.pc.in ${PROJECT_NAME}.pc @ONLY) -install(FILES "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pc" +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") # Python wrapper -include(CMakeDependentOption) -cmake_dependent_option(BUILD_PYTHON_WRAPPER "Builds Python wrapper" ON BUILD_SHARED_LIBS OFF) - -if(BUILD_PYTHON_WRAPPER) - SET(Python_ADDITIONAL_VERSIONS 3) - find_package(PythonLibs) - execute_process(COMMAND which python3 OUTPUT_QUIET RESULT_VARIABLE Python3_NOT_FOUND) - execute_process(COMMAND python3 -c "import numpy" RESULT_VARIABLE Numpy_NOT_FOUND) -endif(BUILD_PYTHON_WRAPPER) - -if (NOT Python3_NOT_FOUND AND NOT Numpy_NOT_FOUND AND PYTHONLIBS_FOUND AND BUILD_PYTHON_WRAPPER) - # TODO deal with both python2/3 - execute_process(COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/python_build_flags.py OUTPUT_VARIABLE PY_OUT) - set(PY_VARS CFLAGS LDFLAGS LINKER EXT_SUFFIX) - cmake_parse_arguments(PY "" "${PY_VARS}" "" ${PY_OUT}) - separate_arguments(PY_CFLAGS) - list(REMOVE_ITEM PY_CFLAGS -flto) - separate_arguments(PY_LDFLAGS) - - foreach(X detect py_type) - add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/apriltag_${X}.docstring.h - COMMAND < ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_${X}.docstring sed 's/\"/\\\\\"/g\; s/^/\"/\; s/$$/\\\\n\"/\;' > apriltag_${X}.docstring.h - WORKING_DIRECTORY ${PROJECT_BINARY_DIR}) - endforeach() +option(BUILD_PYTHON_WRAPPER "Builds Python wrapper" ON) - add_custom_command(OUTPUT apriltag_pywrap.o - COMMAND ${CMAKE_C_COMPILER} ${PY_CFLAGS} -I${PROJECT_BINARY_DIR} -c -o apriltag_pywrap.o ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_pywrap.c - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_pywrap.c ${PROJECT_BINARY_DIR}/apriltag_detect.docstring.h ${PROJECT_BINARY_DIR}/apriltag_py_type.docstring.h) +find_package(Python3 QUIET COMPONENTS Development NumPy) - add_custom_target(apriltag${PY_EXT_SUFFIX} - ${PY_LINKER} ${PY_LDFLAGS} -Wl,-rpath,$ORIGIN apriltag_pywrap.o $ -o apriltag${PY_EXT_SUFFIX} - DEPENDS ${PROJECT_NAME} apriltag_pywrap.o - VERBATIM) +if(BUILD_PYTHON_WRAPPER AND Python3_Development_FOUND AND Python3_NumPy_FOUND) - add_custom_target(apriltag_python ALL - DEPENDS apriltag${PY_EXT_SUFFIX}) + include(CMake/vtkEncodeString.cmake) -execute_process(COMMAND python3 -m site --user-site OUTPUT_VARIABLE PY_DEST) -string(STRIP ${PY_DEST} PY_DEST) -install(FILES ${PROJECT_BINARY_DIR}/apriltag${PY_EXT_SUFFIX} DESTINATION ${PY_DEST}) -endif (NOT Python3_NOT_FOUND AND NOT Numpy_NOT_FOUND AND PYTHONLIBS_FOUND AND BUILD_PYTHON_WRAPPER) + foreach(X IN ITEMS detect py_type estimate_tag_pose) + vtk_encode_string( + INPUT ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_${X}.docstring + NAME apriltag_${X}_docstring + ) + endforeach() + add_custom_target(apriltag_py_docstrings DEPENDS + ${PROJECT_BINARY_DIR}/apriltag_detect_docstring.h + ${PROJECT_BINARY_DIR}/apriltag_py_type_docstring.h + ${PROJECT_BINARY_DIR}/apriltag_estimate_tag_pose_docstring.h + ) + + # set the SOABI manually since renaming the library via OUTPUT_NAME does not work on MSVC + set(apriltag_py_target "apriltag.${Python3_SOABI}") + Python3_add_library(${apriltag_py_target} MODULE ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_pywrap.c) + add_dependencies(${apriltag_py_target} apriltag_py_docstrings) + # avoid linking against Python3::Python to prevent segmentation faults in Conda environments + # https://github.com/AprilRobotics/apriltag/issues/352 + target_link_libraries(${apriltag_py_target} PRIVATE apriltag Python3::NumPy) + target_include_directories(${apriltag_py_target} PRIVATE ${PROJECT_BINARY_DIR}) + + set(PY_DEST ${CMAKE_INSTALL_PREFIX}/lib/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/) + install(TARGETS ${apriltag_py_target} LIBRARY DESTINATION ${PY_DEST}) +elseif(BUILD_PYTHON_WRAPPER) + message(WARNING + "Python bindings requested (BUILD_PYTHON_WRAPPER=ON) but Development and NumPy not found. " + "Python bindings will not be built. Set BUILD_PYTHON_WRAPPER=OFF to silent this warnings." + ) +endif() # Examples if (BUILD_EXAMPLES) @@ -198,6 +213,7 @@ if (BUILD_EXAMPLES) set(_OpenCV_REQUIRED_COMPONENTS core imgproc videoio highgui) find_package(OpenCV COMPONENTS ${_OpenCV_REQUIRED_COMPONENTS} QUIET CONFIG) if(OpenCV_FOUND) + enable_language(CXX) # NB: contrib required for TickMeter in OpenCV 2.4. This is only required for 16.04 backwards compatibility and can be removed in the future. # If we add it to the find_package initially, the demo won't build for newer OpenCV versions if(OpenCV_VERSION VERSION_LESS "3.0.0") @@ -216,3 +232,8 @@ if (BUILD_EXAMPLES) # install example programs install(TARGETS apriltag_demo RUNTIME DESTINATION bin) endif() + +if(BUILD_TESTING) + enable_testing() + add_subdirectory(test) +endif() diff --git a/Makefile b/Makefile deleted file mode 100644 index 32574ad3..00000000 --- a/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -PREFIX ?= /usr/local - -CC = gcc -AR = ar - -CFLAGS = -std=gnu99 -fPIC -Wall -Wno-unused-parameter -Wno-unused-function -CFLAGS += -I. -O3 -fno-strict-overflow - -#APRILTAG_SRCS := $(shell ls *.c common/*.c) -APRILTAG_SRCS := apriltag.c apriltag_pose.c apriltag_quad_thresh.c common/g2d.c common/getopt.c common/homography.c common/image_u8.c common/image_u8x3.c common/image_u8x4.c common/matd.c common/pam.c common/pjpeg.c common/pjpeg-idct.c common/pnm.c common/string_util.c common/svd22.c common/time_util.c common/unionfind.c common/workerpool.c common/zarray.c common/zhash.c common/zmaxheap.c tag16h5.c tag25h9.c tag36h11.c tagCircle21h7.c tagCircle49h12.c tagCustom48h12.c tagStandard41h12.c tagStandard52h13.c -APRILTAG_HEADERS := $(shell ls *.h common/*.h) -APRILTAG_OBJS := $(APRILTAG_SRCS:%.c=%.o) -TARGETS := libapriltag.a libapriltag.so - -.PHONY: all -all: $(TARGETS) - @$(MAKE) -C example all - -.PHONY: install -install: libapriltag.so - @chmod +x install.sh - @./install.sh $(PREFIX)/lib libapriltag.so - @./install.sh $(PREFIX)/include/apriltag $(APRILTAG_HEADERS) - @ldconfig - -libapriltag.a: $(APRILTAG_OBJS) - @echo " [$@]" - @$(AR) -cq $@ $(APRILTAG_OBJS) - -libapriltag.so: $(APRILTAG_OBJS) - @echo " [$@]" - @$(CC) -fPIC -shared -o $@ $^ - -%.o: %.c - @echo " $@" - @$(CC) -o $@ -c $< $(CFLAGS) - -.PHONY: clean -clean: - @rm -rf *.o common/*.o $(TARGETS) - @$(MAKE) -C example clean diff --git a/README.md b/README.md index 51f1a5eb..31f71353 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ Table of Contents ================= - [Papers](#papers) - [Install](#install) - - [cmake](#cmake) - - [make](#make) - [Usage](#usage) - [Choosing a Tag Family](#choosing-a-tag-family) - [Getting Started with the Detector](#getting-started-with-the-detector) @@ -44,8 +42,6 @@ Officially only Linux operating systems are supported, although users have had s The default installation will place headers in /usr/local/include and shared library in /usr/local/lib. It also installs a pkg-config script into /usr/local/lib/pkgconfig and will install a python wrapper if python3 is installed. -## cmake -If you have CMake installed, then do: ``` cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --target install @@ -65,16 +61,6 @@ to generate and compile via the ninja build script. It will be much faster than You can omit `--target install` if you only want to use this locally without installing. -## make -Otherwise, we have a handwritten makefile you can use (be warned it will do slightly different things): -``` -make -j -sudo make install -``` - -To install to a different directory than /usr/local: - - $ PREFIX=/some/path sudo make install Usage ===== @@ -103,11 +89,15 @@ If none of these fit your needs, generate your own custom tag family [here](http detections = detector.detect(image) -Alternately you can use the AprilTag python bindings created by [duckietown](https://github.com/duckietown/apriltags3-py). +Alternately you can use the AprilTag python bindings created by [duckietown](https://github.com/duckietown/lib-dt-apriltags). ### C - image_u8_t* im = image_u8_create_from_pnm("test.png"); + image_u8_t* im = image_u8_create_from_pnm("test.pnm"); + if (im == NULL) { + fprintf(stderr, "Failed to load pnm image.\n"); + exit(1); + } apriltag_detector_t *td = apriltag_detector_create(); apriltag_family_t *tf = tagStandard41h12_create(); apriltag_detector_add_family(td, tf); @@ -146,10 +136,9 @@ Note that this library has no external dependencies. Most applications will require, at minimum, a method for acquiring images. See example/opencv_demo.cc for an example of using AprilTag in C++ with OpenCV. -This example application can be built by executing the following: +After building the repository you can run the example opencv application with: - $ cd examples - $ make opencv_demo + $ ./build/opencv_demo Image data in a cv::Mat object can be passed to AprilTag without creating a deep copy. Simply create an image_u8_t header for the cv::Mat data buffer: @@ -202,7 +191,7 @@ Note: The tag size should not be measured from the outside of the tag. The tag s ![The tag size is the width of the edge between the white and black borders.](tag_size_48h12.png) ### Coordinate System -The coordinate system has the origin at the camera center. The z-axis points from the camera center out the camera lens. The x-axis is to the right in the image taken by the camera, and y is down. The tag's coordinate frame is centered at the center of the tag, with x-axis to the right, y-axis down, and z-axis into the tag. +The coordinate system has the origin at the camera center. The z-axis points from the camera center out the camera lens. The x-axis is to the right in the image taken by the camera, and y is down. The tag's coordinate frame is centered at the center of the tag. From the viewer's perspective, the x-axis is to the right, y-axis down, and z-axis is into the tag. Debugging ========= diff --git a/apriltag.c b/apriltag.c index ec4f0224..5e3be1d7 100644 --- a/apriltag.c +++ b/apriltag.c @@ -41,6 +41,7 @@ either expressed or implied, of the Regents of The University of Michigan. #include #include "common/image_u8.h" +#include "common/image_u8_parallel.h" #include "common/image_u8x3.h" #include "common/zarray.h" #include "common/matd.h" @@ -116,19 +117,13 @@ static double graymodel_interpolate(struct graymodel *gm, double x, double y) return gm->C[0]*x + gm->C[1]*y + gm->C[2]; } -struct quick_decode_entry +static inline int popcount64(uint64_t x) { - uint64_t rcode; // the queried code - uint16_t id; // the tag ID (a small integer) - uint8_t hamming; // how many errors corrected? - uint8_t rotation; // number of rotations [0, 3] -}; - -struct quick_decode -{ - int nentries; - struct quick_decode_entry *entries; -}; + x -= (x >> 1) & 0x5555555555555555ULL; + x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL); + x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fULL; + return (x * 0x0101010101010101ULL) >> 56; +} /** * Assuming we are drawing the image one quadrant at a time, what would the rotated image look like? @@ -168,18 +163,34 @@ static struct quad *quad_copy(struct quad *quad) return q; } -static void quick_decode_add(struct quick_decode *qd, uint64_t code, int id, int hamming) +struct quick_decode_result { - uint32_t bucket = code % qd->nentries; + uint64_t rcode; // the queried code + uint16_t id; // the tag ID (a small integer) + uint8_t hamming; // how many errors corrected? + uint8_t rotation; // number of rotations [0, 3] +}; - while (qd->entries[bucket].rcode != UINT64_MAX) { - bucket = (bucket + 1) % qd->nentries; - } +#define NUM_CHUNKS 4 - qd->entries[bucket].rcode = code; - qd->entries[bucket].id = id; - qd->entries[bucket].hamming = hamming; -} +struct quick_decode +{ + int nbits; + int chunk_size; + int capacity; + int chunk_mask; + int shifts[NUM_CHUNKS]; + + // chunk_offsets is a map from chunk value to a range of locations in chunk_ids. + // Together with chunk_ids, this allows a lookup of all codes matching a chunk value. + uint16_t* chunk_offsets[NUM_CHUNKS]; + + // chunk_ids is an array of indices into the codes table + uint16_t* chunk_ids[NUM_CHUNKS]; + + int maxhamming; + int ncodes; +}; static void quick_decode_uninit(apriltag_family_t *fam) { @@ -187,7 +198,10 @@ static void quick_decode_uninit(apriltag_family_t *fam) return; struct quick_decode *qd = (struct quick_decode*) fam->impl; - free(qd->entries); + for (int i = 0; i < NUM_CHUNKS; i++) { + free(qd->chunk_offsets[i]); + free(qd->chunk_ids[i]); + } free(qd); fam->impl = NULL; } @@ -197,126 +211,130 @@ static void quick_decode_init(apriltag_family_t *family, int maxhamming) assert(family->impl == NULL); assert(family->ncodes < 65536); - struct quick_decode *qd = calloc(1, sizeof(struct quick_decode)); - int capacity = family->ncodes; - - int nbits = family->nbits; - - if (maxhamming >= 1) - capacity += family->ncodes * nbits; - - if (maxhamming >= 2) - capacity += family->ncodes * nbits * (nbits-1); - - if (maxhamming >= 3) - capacity += family->ncodes * nbits * (nbits-1) * (nbits-2); - - qd->nentries = capacity * 3; - -// debug_print("capacity %d, size: %.0f kB\n", -// capacity, qd->nentries * sizeof(struct quick_decode_entry) / 1024.0); + if (maxhamming > 3) { + debug_print("\"maxhamming\" beyond 3 not supported\n"); + errno = EINVAL; + return; + } - qd->entries = calloc(qd->nentries, sizeof(struct quick_decode_entry)); - if (qd->entries == NULL) { - debug_print("Failed to allocate hamming decode table\n"); - // errno already set to ENOMEM (Error No MEMory) by calloc() failure + struct quick_decode *qd = calloc(1, sizeof(struct quick_decode)); + if (!qd) { + debug_print("Memory allocation failed\n"); return; } + family->impl = qd; - for (int i = 0; i < qd->nentries; i++) - qd->entries[i].rcode = UINT64_MAX; + qd->maxhamming = maxhamming; + qd->ncodes = family->ncodes; + qd->nbits = family->nbits; - errno = 0; + qd->chunk_size = (qd->nbits + (NUM_CHUNKS - 1)) / NUM_CHUNKS; + qd->capacity = 1 << qd->chunk_size; + qd->chunk_mask = qd->capacity - 1; - for (uint32_t i = 0; i < family->ncodes; i++) { - uint64_t code = family->codes[i]; + for (int i = 0; i < NUM_CHUNKS; i++) { + qd->shifts[i] = i * qd->chunk_size; + } - // add exact code (hamming = 0) - quick_decode_add(qd, code, i, 0); + for (int i = 0; i < NUM_CHUNKS; i++) { + qd->chunk_offsets[i] = calloc(qd->capacity + 1, sizeof(uint16_t)); + if (!qd->chunk_offsets[i]) { + debug_print("Memory allocation failed\n"); + goto fail; + } + qd->chunk_ids[i] = calloc(qd->ncodes, sizeof(uint16_t)); + if (!qd->chunk_ids[i]) { + debug_print("Memory allocation failed\n"); + goto fail; + } + } - if (maxhamming >= 1) { - // add hamming 1 - for (int j = 0; j < nbits; j++) - quick_decode_add(qd, code ^ (APRILTAG_U64_ONE << j), i, 1); + // Count frequencies + for (int i = 0; i < qd->ncodes; i++) { + uint64_t code = family->codes[i]; + for (int j = 0; j < NUM_CHUNKS; j++) { + int val = (code >> qd->shifts[j]) & qd->chunk_mask; + qd->chunk_offsets[j][val + 1]++; } + } - if (maxhamming >= 2) { - // add hamming 2 - for (int j = 0; j < nbits; j++) - for (int k = 0; k < j; k++) - quick_decode_add(qd, code ^ (APRILTAG_U64_ONE << j) ^ (APRILTAG_U64_ONE << k), i, 2); + // Prefix sum + for (int i = 0; i < NUM_CHUNKS; i++) { + for (int j = 0; j < qd->capacity; j++) { + qd->chunk_offsets[i][j + 1] += qd->chunk_offsets[i][j]; } + } - if (maxhamming >= 3) { - // add hamming 3 - for (int j = 0; j < nbits; j++) - for (int k = 0; k < j; k++) - for (int m = 0; m < k; m++) - quick_decode_add(qd, code ^ (APRILTAG_U64_ONE << j) ^ (APRILTAG_U64_ONE << k) ^ (APRILTAG_U64_ONE << m), i, 3); + // Populate ids + uint16_t *cursors[NUM_CHUNKS]; + memset(cursors, 0, sizeof(cursors)); + for (int i = 0; i < NUM_CHUNKS; i++) { + cursors[i] = malloc((qd->capacity + 1) * sizeof(uint16_t)); + if (cursors[i] == NULL) { + debug_print("Memory allocation failed\n"); + for (int j = 0; j < NUM_CHUNKS; j++) + free(cursors[j]); + goto fail; } + memcpy(cursors[i], qd->chunk_offsets[i], (qd->capacity + 1) * sizeof(uint16_t)); + } - if (maxhamming > 3) { - debug_print("\"maxhamming\" beyond 3 not supported\n"); - // set errno to Error INvalid VALue - errno = EINVAL; - return; + for (int i = 0; i < qd->ncodes; i++) { + uint64_t code = family->codes[i]; + for (int j = 0; j < NUM_CHUNKS; j++) { + int val = (code >> qd->shifts[j]) & qd->chunk_mask; + int write_pos = cursors[j][val]; + qd->chunk_ids[j][write_pos] = i; + cursors[j][val]++; } } - family->impl = qd; + for (int i = 0; i < NUM_CHUNKS; i++) { + free(cursors[i]); + } - #if 0 - int longest_run = 0; - int run = 0; - int run_sum = 0; - int run_count = 0; - - // This accounting code doesn't check the last possible run that - // occurs at the wrap-around. That's pretty insignificant. - for (int i = 0; i < qd->nentries; i++) { - if (qd->entries[i].rcode == UINT64_MAX) { - if (run > 0) { - run_sum += run; - run_count ++; - } - run = 0; - } else { - run ++; - longest_run = imax(longest_run, run); - } - } + return; - printf("quick decode: longest run: %d, average run %.3f\n", longest_run, 1.0 * run_sum / run_count); - #endif +fail: + quick_decode_uninit(family); } -// returns an entry with hamming set to 255 if no decode was found. +// returns a result with hamming set to 255 if no decode was found. static void quick_decode_codeword(apriltag_family_t *tf, uint64_t rcode, - struct quick_decode_entry *entry) + struct quick_decode_result *res) { struct quick_decode *qd = (struct quick_decode*) tf->impl; // qd might be null if detector_add_family_bits() failed for (int ridx = 0; qd != NULL && ridx < 4; ridx++) { - for (int bucket = rcode % qd->nentries; - qd->entries[bucket].rcode != UINT64_MAX; - bucket = (bucket + 1) % qd->nentries) { - - if (qd->entries[bucket].rcode == rcode) { - *entry = qd->entries[bucket]; - entry->rotation = ridx; - return; + for (int i = 0; i < NUM_CHUNKS; i++) { + int val = (rcode >> qd->shifts[i]) & qd->chunk_mask; + int start = qd->chunk_offsets[i][val]; + int end = qd->chunk_offsets[i][val + 1]; + + for (int j = start; j < end; j++) { + uint16_t id = qd->chunk_ids[i][j]; + uint64_t correct_code = tf->codes[id]; + int hamming = popcount64(correct_code ^ rcode); + + if (hamming <= qd->maxhamming) { + res->rcode = rcode; + res->id = id; + res->hamming = hamming; + res->rotation = ridx; + return; + } } } rcode = rotate90(rcode, tf->nbits); } - entry->rcode = 0; - entry->id = 65535; - entry->hamming = 255; - entry->rotation = 0; + res->rcode = 0; + res->id = 65535; + res->hamming = 255; + res->rotation = 0; } static inline int detection_compare_function(const void *_a, const void *_b) @@ -415,7 +433,7 @@ struct evaluate_quad_ret matd_t *H, *Hinv; int decode_status; - struct quick_decode_entry e; + struct quick_decode_result res; }; static matd_t* homography_compute2(double c[4][4]) { @@ -445,6 +463,10 @@ static matd_t* homography_compute2(double c[4][4]) { } } + if (max_val_idx < 0) { + return NULL; + } + if (max_val < epsilon) { debug_print("WRN: Matrix is singular.\n"); return NULL; @@ -562,7 +584,7 @@ static void sharpen(apriltag_detector_t* td, double* values, int size) { } // returns the decision margin. Return < 0 if the detection should be rejected. -static float quad_decode(apriltag_detector_t* td, apriltag_family_t *family, image_u8_t *im, struct quad *quad, struct quick_decode_entry *entry, image_u8_t *im_samples) +static float quad_decode(apriltag_detector_t* td, apriltag_family_t *family, image_u8_t *im, struct quad *quad, struct quick_decode_result *res, image_u8_t *im_samples) { // decode the tag binary contents by sampling the pixel // closest to the center of each bit cell. @@ -731,7 +753,7 @@ static float quad_decode(apriltag_detector_t* td, apriltag_family_t *family, ima } } - quick_decode_codeword(family, rcode, entry); + quick_decode_codeword(family, rcode, res); free(values); return fmin(white_score / white_score_count, black_score / black_score_count); } @@ -785,10 +807,19 @@ static void refine_edges(apriltag_detector_t *td, image_u8_t *im_orig, struct qu // search on another pixel in the first place. Likewise, // for very small tags, we don't want the range to be too // big. - double range = td->quad_decimate + 1; + + int range = td->quad_decimate + 1; + + // To reduce the overhead of bilinear interpolation, we can + // reduce the number of steps per unit. + int steps_per_unit = 4; + double step_length = 1.0 / steps_per_unit; + int max_steps = 2 * steps_per_unit * range + 1; + double delta = 0.5; // XXX tunable step size. - for (double n = -range; n <= range; n += 0.25) { + for (int step = 0; step < max_steps; ++step) { + double n = -range + step_length * step; // Because of the guaranteed winding order of the // points in the quad, we will start inside the white // portion of the quad and work our way outward. @@ -798,19 +829,36 @@ static void refine_edges(apriltag_detector_t *td, image_u8_t *im_orig, struct qu // gradient more precisely, but are more sensitive to // noise. double grange = 1; - int x1 = x0 + (n + grange)*nx; - int y1 = y0 + (n + grange)*ny; - if (x1 < 0 || x1 >= im_orig->width || y1 < 0 || y1 >= im_orig->height) - continue; - int x2 = x0 + (n - grange)*nx; - int y2 = y0 + (n - grange)*ny; - if (x2 < 0 || x2 >= im_orig->width || y2 < 0 || y2 >= im_orig->height) + double x1 = x0 + (n + grange)*nx - delta; + double y1 = y0 + (n + grange)*ny - delta; + double x1i_d, y1i_d, a1, b1; + a1 = modf(x1, &x1i_d); + b1 = modf(y1, &y1i_d); + int x1i = x1i_d, y1i = y1i_d; + + if (x1i < 0 || x1i + 1 >= im_orig->width || y1i < 0 || y1i + 1 >= im_orig->height) continue; - int g1 = im_orig->buf[y1*im_orig->stride + x1]; - int g2 = im_orig->buf[y2*im_orig->stride + x2]; + double x2 = x0 + (n - grange)*nx - delta; + double y2 = y0 + (n - grange)*ny - delta; + double x2i_d, y2i_d, a2, b2; + a2 = modf(x2, &x2i_d); + b2 = modf(y2, &y2i_d); + int x2i = x2i_d, y2i = y2i_d; + if (x2i < 0 || x2i + 1 >= im_orig->width || y2i < 0 || y2i + 1 >= im_orig->height) + continue; + + // interpolate + double g1 = (1 - a1) * (1 - b1) * im_orig->buf[y1i*im_orig->stride + x1i] + + a1 * (1 - b1) * im_orig->buf[y1i*im_orig->stride + x1i + 1] + + (1 - a1) * b1 * im_orig->buf[(y1i + 1)*im_orig->stride + x1i] + + a1 * b1 * im_orig->buf[(y1i + 1)*im_orig->stride + x1i + 1]; + double g2 = (1 - a2) * (1 - b2) * im_orig->buf[y2i*im_orig->stride + x2i] + + a2 * (1 - b2) * im_orig->buf[y2i*im_orig->stride + x2i + 1] + + (1 - a2) * b2 * im_orig->buf[(y2i + 1)*im_orig->stride + x2i] + + a2 * b2 * im_orig->buf[(y2i + 1)*im_orig->stride + x2i + 1]; if (g1 < g2) // reject points whose gradient is "backwards". They can only hurt us. continue; @@ -918,19 +966,19 @@ static void quad_decode_task(void *_u) // optimization process over with the original quad. struct quad *quad = quad_copy(quad_original); - struct quick_decode_entry entry; + struct quick_decode_result res; - float decision_margin = quad_decode(td, family, im, quad, &entry, task->im_samples); + float decision_margin = quad_decode(td, family, im, quad, &res, task->im_samples); - if (decision_margin >= 0 && entry.hamming < 255) { + if (decision_margin >= 0 && res.hamming < 255) { apriltag_detection_t *det = calloc(1, sizeof(apriltag_detection_t)); det->family = family; - det->id = entry.id; - det->hamming = entry.hamming; + det->id = res.id; + det->hamming = res.hamming; det->decision_margin = decision_margin; - double theta = entry.rotation * M_PI / 2.0; + double theta = res.rotation * M_PI / 2.0; double c = cos(theta), s = sin(theta); // Fix the rotation of our homography to properly orient the tag @@ -1046,11 +1094,11 @@ zarray_t *apriltag_detector_detect(apriltag_detector_t *td, image_u8_t *im_orig) if (td->quad_sigma > 0) { // Apply a blur - image_u8_gaussian_blur(quad_im, sigma, ksz); + image_u8_gaussian_blur_parallel(td->wp, quad_im, sigma, ksz); } else { // SHARPEN the image by subtracting the low frequency components. image_u8_t *orig = image_u8_copy(quad_im); - image_u8_gaussian_blur(quad_im, sigma, ksz); + image_u8_gaussian_blur_parallel(td->wp, quad_im, sigma, ksz); for (int y = 0; y < orig->height; y++) { for (int x = 0; x < orig->width; x++) { @@ -1086,13 +1134,8 @@ zarray_t *apriltag_detector_detect(apriltag_detector_t *td, image_u8_t *im_orig) zarray_get_volatile(quads, i, &q); for (int j = 0; j < 4; j++) { - if (td->quad_decimate == 1.5) { - q->p[j][0] *= td->quad_decimate; - q->p[j][1] *= td->quad_decimate; - } else { - q->p[j][0] = (q->p[j][0] - 0.5)*td->quad_decimate + 0.5; - q->p[j][1] = (q->p[j][1] - 0.5)*td->quad_decimate + 0.5; - } + q->p[j][0] *= td->quad_decimate; + q->p[j][1] *= td->quad_decimate; } } } diff --git a/apriltag_detect.docstring b/apriltag_detect.docstring index 0f9bca59..a70c8e33 100644 --- a/apriltag_detect.docstring +++ b/apriltag_detect.docstring @@ -42,7 +42,9 @@ a tuple containing the detections. Each detection is a dict with keys: - id: integer identifying each detected tag -- center: pixel coordinates of the center of each detection +- center: pixel coordinates of the center of each detection. NOTE: Please be + cautious regarding the image coordinate convention. Here, we define (0,0) as + the left-top corner (not the center point) of the left-top-most pixel. - lb-rb-rt-lt: pixel coordinates of the 4 corners of each detection. The order is left-bottom, right-bottom, right-top, left-top @@ -58,3 +60,8 @@ a tuple containing the detections. Each detection is a dict with keys: of detection accuracy only for very small tags-- not effective for larger tags (where we could have sampled anywhere within a bit cell and still gotten a good detection.) + +- homography: A 3x3 homography matrix that describes the projection from an + "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,-1)) to pixels + in the image. This matrix can be used to map points from the tag's coordinate + system to the image coordinate system, and is useful for pose estimation. diff --git a/apriltag_estimate_tag_pose.docstring b/apriltag_estimate_tag_pose.docstring new file mode 100644 index 00000000..8bc147b5 --- /dev/null +++ b/apriltag_estimate_tag_pose.docstring @@ -0,0 +1,70 @@ +estimate_tag_pose(detection, tagsize, fx, fy, cx, cy) -> dict + +SYNOPSIS + + import cv2 + import numpy as np + from apriltag import apriltag + + imagepath = '/tmp/tst.jpg' + image = cv2.imread(imagepath, cv2.IMREAD_GRAYSCALE) + detector = apriltag("tag36h11") + + detections = detector.detect(image) + if detections: + # Estimate pose for the first detected tag + # tagsize is the physical size of the tag in meters + # fx, fy are focal lengths in pixels + # cx, cy are principal point coordinates in pixels + pose = detector.estimate_tag_pose(detections[0], + tagsize=0.16, # 16cm tag + fx=600, fy=600, # focal lengths + cx=320, cy=240) # principal point + print("Rotation matrix R:\n", pose['R']) + print("Translation vector t:", pose['t']) + print("Reprojection error:", pose['error']) + +DESCRIPTION + +The estimate_tag_pose() method estimates the 6-DOF pose (position and orientation) +of a detected AprilTag in 3D space. This method requires the detection result from +the detect() method, the physical size of the tag, and camera intrinsic parameters. + +The pose estimation uses the homography matrix from the detection result to +compute the transformation from the tag's coordinate system to the camera's +coordinate system. + +ARGUMENTS + +- detection: A dictionary containing detection information returned by the + detect() method. This dictionary must include the 'homography' key with the + 3x3 homography matrix. + +- tagsize: The physical side length of the AprilTag in meters. This is the real- + world size of the tag, which is necessary for computing the scale of the pose. + +- fx: Focal length in the x direction in pixels. This is a camera intrinsic + parameter that describes how the camera projects 3D points to 2D image space. + +- fy: Focal length in the y direction in pixels. This is a camera intrinsic + parameter that describes how the camera projects 3D points to 2D image space. + +- cx: Principal point x coordinate in pixels. This is the x coordinate of the + optical center of the camera in the image. + +- cy: Principal point y coordinate in pixels. This is the y coordinate of the + optical center of the camera in the image. + +RETURNED VALUE + +Returns a dictionary containing: + +- 'R': 3x3 rotation matrix as a numpy array that represents the orientation + of the tag in the camera coordinate system. + +- 't': 3x1 translation vector as a numpy array (in meters) that represents the + position of the tag in the camera coordinate system. + +- 'error': The object-space error after the iteration process, representing + the sum of squared reprojection errors between observed and estimated points + in object space. A lower value indicates a better pose estimate. diff --git a/apriltag_pose.h b/apriltag_pose.h index 07ee37b2..8120502f 100644 --- a/apriltag_pose.h +++ b/apriltag_pose.h @@ -44,9 +44,9 @@ void estimate_pose_for_tag_homography( * used to find a potential second local minima and Orthogonal Iteration is * used to refine this second estimate. * - * [1]: E. Olson, “Apriltag: A robust and flexible visual fiducial system,” in + * [1]: E. Olson, "Apriltag: A robust and flexible visual fiducial system," in * 2011 IEEE International Conference on Robotics and Automation, - * May 2011, pp. 3400–3407. + * May 2011, pp. 3400-3407. * [2]: Lu, G. D. Hager and E. Mjolsness, "Fast and globally convergent pose * estimation from video images," in IEEE Transactions on Pattern Analysis * and Machine Intelligence, vol. 22, no. 6, pp. 610-622, June 2000. @@ -77,4 +77,3 @@ double estimate_tag_pose(apriltag_detection_info_t* info, apriltag_pose_t* pose) #ifdef __cplusplus } #endif - diff --git a/apriltag_py_type.docstring b/apriltag_py_type.docstring index 97612e32..4ae1ff01 100644 --- a/apriltag_py_type.docstring +++ b/apriltag_py_type.docstring @@ -54,7 +54,7 @@ The constructor takes a number of arguments: All the other arguments are optional: -- Nthreads: how many threads the detector should use. Default is 1 +- threads: how many threads the detector should use. Default is 1 - maxhamming: max number of corrected bits. Larger values guzzle RAM. Default is 1 diff --git a/apriltag_pywrap.c b/apriltag_pywrap.c index cb991bb4..975cea4e 100644 --- a/apriltag_pywrap.c +++ b/apriltag_pywrap.c @@ -2,11 +2,15 @@ #include #include +#ifndef Py_PYTHREAD_H +#include +#endif #include #include #include #include "apriltag.h" +#include "apriltag_pose.h" #include "tag36h10.h" #include "tag36h11.h" #include "tag25h9.h" @@ -68,6 +72,7 @@ typedef struct { apriltag_family_t* tf; apriltag_detector_t* td; + PyThread_type_lock det_lock; void (*destroy_func)(apriltag_family_t *tf); } apriltag_py_t; @@ -75,6 +80,8 @@ typedef struct { static PyObject * apriltag_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { + errno = 0; + bool success = false; apriltag_py_t* self = (apriltag_py_t*)type->tp_alloc(type, 0); @@ -83,6 +90,12 @@ apriltag_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) self->tf = NULL; self->td = NULL; + self->det_lock = PyThread_allocate_lock(); + if (self->det_lock == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Unable to allocate detection lock"); + goto done; + } + const char* family = NULL; int Nthreads = 1; int maxhamming = 1; @@ -171,6 +184,11 @@ apriltag_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) self->destroy_func(self->tf); self->tf = NULL; } + if(self->det_lock != NULL) + { + PyThread_free_lock(self->det_lock); + self->det_lock = NULL; + } Py_DECREF(self); } return NULL; @@ -193,6 +211,11 @@ static void apriltag_dealloc(apriltag_py_t* self) self->destroy_func(self->tf); self->tf = NULL; } + if(self->det_lock != NULL) + { + PyThread_free_lock(self->det_lock); + self->det_lock = NULL; + } Py_TYPE(self)->tp_free((PyObject*)self); } @@ -200,13 +223,18 @@ static void apriltag_dealloc(apriltag_py_t* self) static PyObject* apriltag_detect(apriltag_py_t* self, PyObject* args) { + errno = 0; + PyObject* result = NULL; PyArrayObject* xy_c = NULL; PyArrayObject* xy_lb_rb_rt_lt = NULL; + PyArrayObject* homography = NULL; PyArrayObject* image = NULL; PyObject* detections_tuple = NULL; +#ifdef _POSIX_C_SOURCE SET_SIGINT(); +#endif if(!PyArg_ParseTuple( args, "O&", PyArray_Converter, &image )) goto done; @@ -237,7 +265,13 @@ static PyObject* apriltag_detect(apriltag_py_t* self, .stride = strides[0], .buf = PyArray_DATA(image)}; - zarray_t* detections = apriltag_detector_detect(self->td, &im); + zarray_t *detections = NULL; // Declare detections variable outside the GIL macro block + Py_BEGIN_ALLOW_THREADS // Release the GIL to allow other Python threads to run + PyThread_acquire_lock(self->det_lock, 1); // Acquire the detection lock before running the detector (blocks until the lock is available) + detections = apriltag_detector_detect(self->td, &im); // Run detection + PyThread_release_lock(self->det_lock); // Release the detection lock + Py_END_ALLOW_THREADS // Acquire the GIL after releasing the detection lock + int N = zarray_size(detections); if (N == 0 && errno == EAGAIN){ @@ -251,7 +285,7 @@ static PyObject* apriltag_detect(apriltag_py_t* self, PyErr_Format(PyExc_RuntimeError, "Error creating output tuple of size %d", N); goto done; } - + for (int i=0; i < N; i++) { xy_c = (PyArrayObject*)PyArray_SimpleNew(1, ((npy_intp[]){2}), NPY_FLOAT64); @@ -279,13 +313,32 @@ static PyObject* apriltag_detect(apriltag_py_t* self, *(double*)PyArray_GETPTR2(xy_lb_rb_rt_lt, j, 1) = det->p[j][1]; } + // Add homography matrix (3x3) + homography = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3,3}), NPY_FLOAT64); + if(homography == NULL) + { + Py_DECREF(xy_c); + Py_DECREF(xy_lb_rb_rt_lt); + PyErr_SetString(PyExc_RuntimeError, "Could not allocate homography array"); + goto done; + } + + for(int j=0; j<3; j++) + { + for(int k=0; k<3; k++) + { + *(double*)PyArray_GETPTR2(homography, j, k) = MATD_EL(det->H, j, k); + } + } + PyTuple_SET_ITEM(detections_tuple, i, - Py_BuildValue("{s:i,s:f,s:i,s:N,s:N}", + Py_BuildValue("{s:i,s:f,s:i,s:N,s:N,s:N}", "hamming", det->hamming, "margin", det->decision_margin, "id", det->id, "center", xy_c, - "lb-rb-rt-lt", xy_lb_rb_rt_lt)); + "lb-rb-rt-lt", xy_lb_rb_rt_lt, + "homography", homography)); xy_c = NULL; xy_lb_rb_rt_lt = NULL; } @@ -300,21 +353,155 @@ static PyObject* apriltag_detect(apriltag_py_t* self, Py_XDECREF(image); Py_XDECREF(detections_tuple); +#ifdef _POSIX_C_SOURCE RESET_SIGINT(); +#endif + return result; +} + +static PyObject* apriltag_estimate_tag_pose(apriltag_py_t* self, + PyObject* args) +{ + PyObject* result = NULL; + PyObject* detection_dict = NULL; + PyArrayObject* R_array = NULL; + PyArrayObject* t_array = NULL; + matd_t* H_matrix = NULL; + double tagsize, fx, fy, cx, cy; + + if(!PyArg_ParseTuple(args, "Oddddd", + &detection_dict, + &tagsize, + &fx, &fy, &cx, &cy)) + return NULL; + + if(!PyDict_Check(detection_dict)) + { + PyErr_SetString(PyExc_TypeError, "First argument must be a detection dictionary"); + return NULL; + } + + // Extract detection information from the dictionary + PyObject* py_id = PyDict_GetItemString(detection_dict, "id"); + PyObject* py_hamming = PyDict_GetItemString(detection_dict, "hamming"); + PyObject* py_margin = PyDict_GetItemString(detection_dict, "margin"); + PyObject* py_center = PyDict_GetItemString(detection_dict, "center"); + PyObject* py_corners = PyDict_GetItemString(detection_dict, "lb-rb-rt-lt"); + PyObject* py_homography = PyDict_GetItemString(detection_dict, "homography"); + + if(!py_id || !py_hamming || !py_margin || !py_center || !py_corners || !py_homography) + { + PyErr_SetString(PyExc_ValueError, + "Detection dictionary is missing required fields. " + "Make sure you're using a detection from the updated detect() method that includes 'homography'."); + return NULL; + } + + // Create a temporary detection structure + apriltag_detection_t det; + det.family = self->tf; + det.id = PyLong_AsLong(py_id); + det.hamming = PyLong_AsLong(py_hamming); + det.decision_margin = PyFloat_AsDouble(py_margin); + + // Extract center + PyArrayObject* center_array = (PyArrayObject*)py_center; + det.c[0] = *(double*)PyArray_GETPTR1(center_array, 0); + det.c[1] = *(double*)PyArray_GETPTR1(center_array, 1); + + // Extract corners + PyArrayObject* corners_array = (PyArrayObject*)py_corners; + for(int i = 0; i < 4; i++) + { + det.p[i][0] = *(double*)PyArray_GETPTR2(corners_array, i, 0); + det.p[i][1] = *(double*)PyArray_GETPTR2(corners_array, i, 1); + } + + // Extract and copy homography matrix + PyArrayObject* homography_array = (PyArrayObject*)py_homography; + H_matrix = matd_create(3, 3); + if(!H_matrix) + { + PyErr_SetString(PyExc_RuntimeError, "Could not allocate homography matrix"); + return NULL; + } + + for(int i = 0; i < 3; i++) + { + for(int j = 0; j < 3; j++) + { + MATD_EL(H_matrix, i, j) = *(double*)PyArray_GETPTR2(homography_array, i, j); + } + } + det.H = H_matrix; + + // Setup detection info + apriltag_detection_info_t info; + info.det = &det; + info.tagsize = tagsize; + info.fx = fx; + info.fy = fy; + info.cx = cx; + info.cy = cy; + + // Estimate pose + apriltag_pose_t pose; + double error = estimate_tag_pose(&info, &pose); + + // Create numpy arrays for R and t + R_array = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3, 3}), NPY_FLOAT64); + t_array = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3, 1}), NPY_FLOAT64); + + if(!R_array || !t_array) + { + PyErr_SetString(PyExc_RuntimeError, "Could not allocate output arrays"); + goto cleanup; + } + + // Copy rotation matrix + for(int i = 0; i < 3; i++) + { + for(int j = 0; j < 3; j++) + { + *(double*)PyArray_GETPTR2(R_array, i, j) = MATD_EL(pose.R, i, j); + } + } + + // Copy translation vector + for(int i = 0; i < 3; i++) + { + *(double*)PyArray_GETPTR2(t_array, i, 0) = MATD_EL(pose.t, i, 0); + } + + result = Py_BuildValue("{s:N,s:N,s:d}", + "R", R_array, + "t", t_array, + "error", error); + R_array = NULL; + t_array = NULL; + +cleanup: + if(H_matrix) + matd_destroy(H_matrix); + if(pose.R) + matd_destroy(pose.R); + if(pose.t) + matd_destroy(pose.t); + Py_XDECREF(R_array); + Py_XDECREF(t_array); + return result; } -static const char apriltag_detect_docstring[] = -#include "apriltag_detect.docstring.h" - ; -static const char apriltag_type_docstring[] = -#include "apriltag_py_type.docstring.h" - ; +#include "apriltag_detect_docstring.h" +#include "apriltag_py_type_docstring.h" +#include "apriltag_estimate_tag_pose_docstring.h" static PyMethodDef apriltag_methods[] = { PYMETHODDEF_ENTRY(apriltag_, detect, METH_VARARGS), - {} + PYMETHODDEF_ENTRY(apriltag_, estimate_tag_pose, METH_VARARGS), + {NULL, NULL, 0, NULL} }; static PyTypeObject apriltagType = @@ -326,11 +513,11 @@ static PyTypeObject apriltagType = .tp_dealloc = (destructor)apriltag_dealloc, .tp_methods = apriltag_methods, .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = apriltag_type_docstring + .tp_doc = apriltag_py_type_docstring }; static PyMethodDef methods[] = - { {} + { {NULL, NULL, 0, NULL} }; @@ -358,7 +545,11 @@ static struct PyModuleDef module_def = "apriltag", "AprilTags visual fiducial system detector", -1, - methods + methods, + 0, + 0, + 0, + 0 }; PyMODINIT_FUNC PyInit_apriltag(void) @@ -378,4 +569,3 @@ PyMODINIT_FUNC PyInit_apriltag(void) } #endif - diff --git a/apriltag_quad_thresh.c b/apriltag_quad_thresh.c index 38d3e43d..cad10e56 100644 --- a/apriltag_quad_thresh.c +++ b/apriltag_quad_thresh.c @@ -266,8 +266,13 @@ void fit_line(struct line_fit_pt *lfps, int sz, int i0, int i1, double *lineparm } double length = sqrtf(M); - lineparm[2] = nx/length; - lineparm[3] = ny/length; + if (fabs(length) < 1e-12) { + lineparm[2] = lineparm[3] = 0; + } + else { + lineparm[2] = nx/length; + lineparm[3] = ny/length; + } } // sum of squared errors = @@ -615,42 +620,45 @@ int quad_segment_agg(zarray_t *cluster, struct line_fit_pt *lfps, int indices[4] */ struct line_fit_pt* compute_lfps(int sz, zarray_t* cluster, image_u8_t* im) { struct line_fit_pt *lfps = calloc(sz, sizeof(struct line_fit_pt)); + double sum_Mx = 0, sum_My = 0, sum_Mxx = 0, sum_Myy = 0, sum_Mxy = 0, sum_W = 0; for (int i = 0; i < sz; i++) { struct pt *p; zarray_get_volatile(cluster, i, &p); - if (i > 0) { - memcpy(&lfps[i], &lfps[i-1], sizeof(struct line_fit_pt)); - } - - { - // we now undo our fixed-point arithmetic. - double delta = 0.5; // adjust for pixel center bias - double x = p->x * .5 + delta; - double y = p->y * .5 + delta; - int ix = x, iy = y; - double W = 1; + // we now undo our fixed-point arithmetic. + double delta = 0.5; // adjust for pixel center bias + double x = p->x * .5 + delta; + double y = p->y * .5 + delta; + int ix = x, iy = y; + double W = 1; - if (ix > 0 && ix+1 < im->width && iy > 0 && iy+1 < im->height) { - int grad_x = im->buf[iy * im->stride + ix + 1] - - im->buf[iy * im->stride + ix - 1]; + if (ix > 0 && ix+1 < im->width && iy > 0 && iy+1 < im->height) { + int grad_x = im->buf[iy * im->stride + ix + 1] - + im->buf[iy * im->stride + ix - 1]; - int grad_y = im->buf[(iy+1) * im->stride + ix] - - im->buf[(iy-1) * im->stride + ix]; + int grad_y = im->buf[(iy+1) * im->stride + ix] - + im->buf[(iy-1) * im->stride + ix]; - // XXX Tunable. How to shape the gradient magnitude? - W = sqrt(grad_x*grad_x + grad_y*grad_y) + 1; - } - - double fx = x, fy = y; - lfps[i].Mx += W * fx; - lfps[i].My += W * fy; - lfps[i].Mxx += W * fx * fx; - lfps[i].Mxy += W * fx * fy; - lfps[i].Myy += W * fy * fy; - lfps[i].W += W; + // XXX Tunable. How to shape the gradient magnitude? + W = sqrt(grad_x*grad_x + grad_y*grad_y) + 1; } + + double fx = x, fy = y; + sum_Mx += W * fx; + sum_My += W * fy; + sum_Mxx += W * fx * fx; + sum_Mxy += W * fx * fy; + sum_Myy += W * fy * fy; + sum_W += W; + + // Store cumulative sums + lfps[i].Mx = sum_Mx; + lfps[i].My = sum_My; + lfps[i].Mxx = sum_Mxx; + lfps[i].Mxy = sum_Mxy; + lfps[i].Myy = sum_Myy; + lfps[i].W = sum_W; } return lfps; } @@ -709,8 +717,16 @@ static inline void ptsort(struct pt *pts, int sz) #undef MAYBE_SWAP // a merge sort with temp storage. - - struct pt *tmp = malloc(sizeof(struct pt) * sz); + // Use stack allocation for small arrays to avoid malloc overhead + #define STACK_BUFFER_SIZE 256 + struct pt stack_buffer[STACK_BUFFER_SIZE]; + struct pt *tmp; + const bool use_heap = sz > STACK_BUFFER_SIZE; + if (use_heap) { + tmp = malloc(sizeof(struct pt) * sz); + } else { + tmp = stack_buffer; + } memcpy(tmp, pts, sizeof(struct pt) * sz); @@ -744,7 +760,9 @@ static inline void ptsort(struct pt *pts, int sz) if (bpos < bsz) memcpy(&pts[outpos], &bs[bpos], (bsz-bpos)*sizeof(struct pt)); - free(tmp); + if (use_heap) { + free(tmp); + } #undef MERGE } @@ -899,11 +917,11 @@ int fit_quad( double det = A00 * A11 - A10 * A01; // inverse. - double W00 = A11 / det, W01 = -A01 / det; if (fabs(det) < 0.001) { res = 0; goto finish; } + double W00 = A11 / det, W01 = -A01 / det; // solve double L0 = W00*B0 + W01*B1; @@ -1805,7 +1823,8 @@ zarray_t* fit_quads(apriltag_detector_t *td, int w, int h, zarray_t* clusters, i normal_border |= !family->reversed_border; reversed_border |= family->reversed_border; } - min_tag_width /= td->quad_decimate; + if (td->quad_decimate > 1) + min_tag_width /= td->quad_decimate; if (min_tag_width < 3) { min_tag_width = 3; } diff --git a/common/g2d.c b/common/g2d.c index 91bbcabc..ebb85e55 100644 --- a/common/g2d.c +++ b/common/g2d.c @@ -120,7 +120,7 @@ int g2d_polygon_contains_point_ref(const zarray_t *poly, double q[2]) double acc_theta = 0; - double last_theta; + double last_theta = 0; for (int i = 0; i <= psz; i++) { double p[2]; @@ -321,7 +321,7 @@ int g2d_polygon_contains_point(const zarray_t *poly, double q[2]) int psz = zarray_size(poly); assert(psz > 0); - int last_quadrant; + int last_quadrant = 0; int quad_acc = 0; for (int i = 0; i <= psz; i++) { diff --git a/common/image_u8.c b/common/image_u8.c index 35458ddf..3064aff3 100644 --- a/common/image_u8.c +++ b/common/image_u8.c @@ -265,6 +265,9 @@ void image_u8_draw_annulus(image_u8_t *im, float x0, float y0, float r0, float r void image_u8_draw_line(image_u8_t *im, float x0, float y0, float x1, float y1, int v, int width) { double dist = sqrtf((y1-y0)*(y1-y0) + (x1-x0)*(x1-x0)); + if (dist == 0) { + return; + } double delta = 0.5 / dist; // terrible line drawing code @@ -318,29 +321,29 @@ void image_u8_convolve_2D(image_u8_t *im, const uint8_t *k, int ksz) { assert((ksz & 1) == 1); // ksz must be odd. + uint8_t *x = malloc(sizeof(uint8_t)*im->stride); for (int y = 0; y < im->height; y++) { - uint8_t *x = malloc(sizeof(uint8_t)*im->stride); memcpy(x, &im->buf[y*im->stride], im->stride); convolve(x, &im->buf[y*im->stride], im->width, k, ksz); - free(x); } + free(x); + uint8_t *xb = malloc(sizeof(uint8_t)*im->height); + uint8_t *yb = malloc(sizeof(uint8_t)*im->height); for (int x = 0; x < im->width; x++) { - uint8_t *xb = malloc(sizeof(uint8_t)*im->height); - uint8_t *yb = malloc(sizeof(uint8_t)*im->height); for (int y = 0; y < im->height; y++) xb[y] = im->buf[y*im->stride + x]; convolve(xb, yb, im->height, k, ksz); - free(xb); for (int y = 0; y < im->height; y++) im->buf[y*im->stride + x] = yb[y]; - free(yb); } + free(xb); + free(yb); } void image_u8_gaussian_blur(image_u8_t *im, double sigma, int ksz) diff --git a/common/image_u8_parallel.c b/common/image_u8_parallel.c new file mode 100644 index 00000000..8751c76e --- /dev/null +++ b/common/image_u8_parallel.c @@ -0,0 +1,165 @@ +/** + * @file image_u8_parallel.c + * @author MqCreaple (gmq14159@gmail.com) + * @brief Parallelized processing of various image_u8 related functions. + * @version 0.1 + * @date 2025-08-07 + * + * @copyright Copyright (c) 2025 + * + */ + +#include "common/image_u8_parallel.h" +#include "common/workerpool.h" +#include "common/math_util.h" + +static void convolve(const uint8_t *x, uint8_t *y, int sz, const uint8_t *k, int ksz) +{ + assert((ksz&1)==1); + + for (int i = 0; i < ksz/2 && i < sz; i++) + y[i] = x[i]; + + for (int i = 0; i < sz - ksz + 1; i++) { + uint32_t acc = 0; + + for (int j = 0; j < ksz; j++) + acc += k[j]*x[i+j]; + + y[ksz/2 + i] = acc >> 8; + } + + for (int i = sz - ksz/2; i < sz; i++) + y[i] = x[i]; +} + +struct image_u8_convolve_2D_task { + image_u8_t *im; + const uint8_t *k; + int ksz; + int idx_st; + int idx_ed; +}; + +static void _image_u8_convolve_2D_thread_1(void *p) { + struct image_u8_convolve_2D_task *params = (struct image_u8_convolve_2D_task*) p; + image_u8_t *im = params->im; + const uint8_t *k = params->k; + int ksz = params->ksz; + int y_st = params->idx_st; + int y_ed = params->idx_ed; + + assert((ksz & 1) == 1); // ksz must be odd. + + uint8_t *x = malloc(sizeof(uint8_t)*im->stride); + for (int y = y_st; y < y_ed; y++) { + memcpy(x, &im->buf[y*im->stride], im->stride); + convolve(x, &im->buf[y*im->stride], im->width, k, ksz); + } + free(x); +} + +static void _image_u8_convolve_2D_thread_2(void *p) { + struct image_u8_convolve_2D_task *params = (struct image_u8_convolve_2D_task*) p; + image_u8_t *im = params->im; + const uint8_t *k = params->k; + int ksz = params->ksz; + int x_st = params->idx_st; + int x_ed = params->idx_ed; + + uint8_t *xb = malloc(sizeof(uint8_t)*im->height); + uint8_t *yb = malloc(sizeof(uint8_t)*im->height); + for (int x = x_st; x < x_ed; x++) { + + for (int y = 0; y < im->height; y++) + xb[y] = im->buf[y*im->stride + x]; + + convolve(xb, yb, im->height, k, ksz); + + for (int y = 0; y < im->height; y++) + im->buf[y*im->stride + x] = yb[y]; + } + free(xb); + free(yb); +} + +void image_u8_convolve_2D_parallel(workerpool_t *wp, image_u8_t *im, const uint8_t *k, int ksz) { + if(im->width * im->height < 65536) { + // for small images, directly use single threaded convolution + image_u8_convolve_2D(im, k, ksz); + return; + } + int nthreads = workerpool_get_nthreads(wp); + + struct image_u8_convolve_2D_task *params = malloc(sizeof(struct image_u8_convolve_2D_task) * nthreads); + int y_inc = im->height / nthreads; + int y_remainder = im->height % nthreads; + int last_y = 0; + for(int idx = 0; idx < nthreads; idx++) { + params[idx].im = im; + params[idx].k = k; + params[idx].ksz = ksz; + params[idx].idx_st = last_y; + last_y += y_inc; + if(idx < y_remainder) { + last_y += 1; // distribute the remainders across the n threads + } + params[idx].idx_ed = last_y; + workerpool_add_task(wp, _image_u8_convolve_2D_thread_1, ¶ms[idx]); + } + workerpool_run(wp); + + int x_inc = im->width / nthreads; + int x_remainder = im->width % nthreads; + int last_x = 0; + for(int idx = 0; idx < nthreads; idx++) { + params[idx].im = im; + params[idx].k = k; + params[idx].ksz = ksz; + params[idx].idx_st = last_x; + last_x += x_inc; + if(idx < x_remainder) { + last_x += 1; // distribute the remainders across the n threads + } + params[idx].idx_ed = last_x; + workerpool_add_task(wp, _image_u8_convolve_2D_thread_2, ¶ms[idx]); + } + workerpool_run(wp); + + free(params); +} + +void image_u8_gaussian_blur_parallel(workerpool_t *wp, image_u8_t *im, double sigma, int ksz) { + if (sigma == 0) + return; + + assert((ksz & 1) == 1); // ksz must be odd. + + // build the kernel. + double *dk = malloc(sizeof(double)*ksz); + + // for kernel of length 5: + // dk[0] = f(-2), dk[1] = f(-1), dk[2] = f(0), dk[3] = f(1), dk[4] = f(2) + for (int i = 0; i < ksz; i++) { + int x = -ksz/2 + i; + double v = exp(-.5*sq(x / sigma)); + dk[i] = v; + } + + // normalize + double acc = 0; + for (int i = 0; i < ksz; i++) + acc += dk[i]; + + for (int i = 0; i < ksz; i++) + dk[i] /= acc; + + uint8_t *k = malloc(sizeof(uint8_t)*ksz); + for (int i = 0; i < ksz; i++) + k[i] = dk[i]*255; + + free(dk); + + image_u8_convolve_2D_parallel(wp, im, k, ksz); + free(k); +} diff --git a/common/image_u8_parallel.h b/common/image_u8_parallel.h new file mode 100644 index 00000000..bd60e475 --- /dev/null +++ b/common/image_u8_parallel.h @@ -0,0 +1,19 @@ +/** + * @file image_u8_parallel.h + * @author MqCreaple (gmq14159@gmail.com) + * @brief Parallelized processing of various image_u8 related functions. + * @version 0.1 + * @date 2025-08-07 + * + * @copyright Copyright (c) 2025 + * + */ +#pragma once + +#include "image_u8.h" +#include "workerpool.h" +#include "math_util.h" + +void image_u8_convolve_2D_parallel(workerpool_t *wp, image_u8_t *im, const uint8_t *k, int ksz); + +void image_u8_gaussian_blur_parallel(workerpool_t *wp, image_u8_t *im, double sigma, int ksz); diff --git a/common/matd.c b/common/matd.c index 176394ed..067e40ed 100644 --- a/common/matd.c +++ b/common/matd.c @@ -51,18 +51,20 @@ matd_t *matd_create(int rows, int cols) if (rows == 0 || cols == 0) return matd_create_scalar(0); - matd_t *m = calloc(1, sizeof(matd_t) + (rows*cols*sizeof(double))); + matd_t *m = calloc(1, sizeof(matd_t)); m->nrows = rows; m->ncols = cols; + m->data = calloc(rows * cols, sizeof(double)); return m; } matd_t *matd_create_scalar(TYPE v) { - matd_t *m = calloc(1, sizeof(matd_t) + sizeof(double)); + matd_t *m = calloc(1, sizeof(matd_t)); m->nrows = 0; m->ncols = 0; + m->data = calloc(1, sizeof(double)); m->data[0] = v; return m; @@ -220,7 +222,8 @@ void matd_destroy(matd_t *m) if (!m) return; - assert(m != NULL); + assert(m->data != NULL); + free(m->data); free(m); } @@ -402,7 +405,7 @@ double matd_det_general(const matd_t *a) // The determinant of a can be calculated as // epsilon*det(L)*det(U), // where epsilon is just the sign of the corresponding permutation - // (which is +1 for an even number of permutations and is −1 + // (which is +1 for an even number of permutations and is -1 // for an uneven number of permutations). double det = mlu->pivsign * detL * detU; @@ -1148,7 +1151,7 @@ static matd_svd_t matd_svd_tall(matd_t *A, int flags) assert(maxiters > 0); // reassure clang int iter; - double maxv; // maximum non-zero value being reduced this iteration + double maxv = 0; // maximum non-zero value being reduced this iteration double tol = 1E-10; diff --git a/common/matd.h b/common/matd.h index a293321d..6b7c5f63 100644 --- a/common/matd.h +++ b/common/matd.h @@ -45,8 +45,7 @@ extern "C" { typedef struct { unsigned int nrows, ncols; - double data[]; -// double *data; + double *data; } matd_t; #define MATD_ALLOC(name, nrows, ncols) double name ## _storage [nrows*ncols]; matd_t name = { .nrows = nrows, .ncols = ncols, .data = &name ## _storage }; diff --git a/common/pthreads_cross.cpp b/common/pthreads_cross.c similarity index 95% rename from common/pthreads_cross.cpp rename to common/pthreads_cross.c index 3403863f..04e556cf 100644 --- a/common/pthreads_cross.cpp +++ b/common/pthreads_cross.c @@ -1,256 +1,256 @@ -/** -Copyright John Schember - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ - -#include "common/pthreads_cross.h" - -#ifdef _WIN32 - -typedef struct { - SRWLOCK lock; - bool exclusive; -} pthread_rwlock_t; - -int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); -int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); -int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); - -int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) -{ - (void) attr; - - if (thread == NULL || start_routine == NULL) - return 1; - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-function-type" - *thread = (HANDLE) CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)start_routine, arg, 0, NULL); -#pragma GCC diagnostic pop - if (*thread == NULL) - return 1; - return 0; -} - -int pthread_join(pthread_t thread, void **value_ptr) -{ - (void)value_ptr; - WaitForSingleObject(thread, INFINITE); - CloseHandle(thread); - return 0; -} - -int pthread_detach(pthread_t thread) -{ - CloseHandle(thread); - return 0; -} - -int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) -{ - (void)attr; - - if (mutex == NULL) - return 1; - - InitializeCriticalSection(mutex); - return 0; -} - -int pthread_mutex_destroy(pthread_mutex_t *mutex) -{ - if (mutex == NULL) - return 1; - DeleteCriticalSection(mutex); - return 0; -} - -int pthread_mutex_lock(pthread_mutex_t *mutex) -{ - if (mutex == NULL) - return 1; - EnterCriticalSection(mutex); - return 0; -} - -int pthread_mutex_unlock(pthread_mutex_t *mutex) -{ - if (mutex == NULL) - return 1; - LeaveCriticalSection(mutex); - return 0; -} - -int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) -{ - (void)attr; - if (cond == NULL) - return 1; - InitializeConditionVariable(cond); - return 0; -} - -int pthread_cond_destroy(pthread_cond_t *cond) -{ - /* Windows does not have a destroy for conditionals */ - (void)cond; - return 0; -} - -int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) -{ - if (cond == NULL || mutex == NULL) - return 1; - return pthread_cond_timedwait(cond, mutex, NULL); -} - -int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, - const struct timespec *abstime) -{ - if (cond == NULL || mutex == NULL) - return 1; - if (!SleepConditionVariableCS(cond, mutex, timespec_to_ms(abstime))) - return 1; - return 0; -} - -int pthread_cond_signal(pthread_cond_t *cond) -{ - if (cond == NULL) - return 1; - WakeConditionVariable(cond); - return 0; -} - -int pthread_cond_broadcast(pthread_cond_t *cond) -{ - if (cond == NULL) - return 1; - WakeAllConditionVariable(cond); - return 0; -} - -int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr) -{ - (void)attr; - if (rwlock == NULL) - return 1; - InitializeSRWLock(&(rwlock->lock)); - rwlock->exclusive = false; - return 0; -} - -int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) -{ - (void)rwlock; - return 0; -} - -int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) -{ - if (rwlock == NULL) - return 1; - AcquireSRWLockShared(&(rwlock->lock)); - return 0; -} - -int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock) -{ - if (rwlock == NULL) - return 1; - return !TryAcquireSRWLockShared(&(rwlock->lock)); -} - -int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) -{ - if (rwlock == NULL) - return 1; - AcquireSRWLockExclusive(&(rwlock->lock)); - rwlock->exclusive = true; - return 0; -} - -int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock) -{ - BOOLEAN ret; - - if (rwlock == NULL) - return 1; - - ret = TryAcquireSRWLockExclusive(&(rwlock->lock)); - if (ret) - rwlock->exclusive = true; - return ret; -} - -int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) -{ - if (rwlock == NULL) - return 1; - - if (rwlock->exclusive) { - rwlock->exclusive = false; - ReleaseSRWLockExclusive(&(rwlock->lock)); - } else { - ReleaseSRWLockShared(&(rwlock->lock)); - } - return 0; -} - -int sched_yield() { - return (int)SwitchToThread(); -} - -void ms_to_timespec(struct timespec *ts, unsigned int ms) -{ - if (ts == NULL) - return; - ts->tv_sec = (ms / 1000) + time(NULL); - ts->tv_nsec = (ms % 1000) * 1000000; -} - -unsigned int timespec_to_ms(const struct timespec *abstime) -{ - if (abstime == NULL) - return INFINITE; - - return ((abstime->tv_sec - time(NULL)) * 1000) + (abstime->tv_nsec / 1000000); -} - -unsigned int pcthread_get_num_procs() -{ - SYSTEM_INFO sysinfo; - - GetSystemInfo(&sysinfo); - return sysinfo.dwNumberOfProcessors; -} - -#else - -#include -unsigned int pcthread_get_num_procs() -{ - return (unsigned int)sysconf(_SC_NPROCESSORS_ONLN); -} -#endif +/** +Copyright John Schember + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +#include "common/pthreads_cross.h" + +#ifdef _WIN32 + +typedef struct { + SRWLOCK lock; + bool exclusive; +} pthread_rwlock_t; + +int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); +int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); +int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); +int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); +int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); +int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); +int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); + +int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) +{ + (void) attr; + + if (thread == NULL || start_routine == NULL) + return 1; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" + *thread = (HANDLE) CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)start_routine, arg, 0, NULL); +#pragma GCC diagnostic pop + if (*thread == NULL) + return 1; + return 0; +} + +int pthread_join(pthread_t thread, void **value_ptr) +{ + (void)value_ptr; + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + return 0; +} + +int pthread_detach(pthread_t thread) +{ + CloseHandle(thread); + return 0; +} + +int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) +{ + (void)attr; + + if (mutex == NULL) + return 1; + + InitializeCriticalSection(mutex); + return 0; +} + +int pthread_mutex_destroy(pthread_mutex_t *mutex) +{ + if (mutex == NULL) + return 1; + DeleteCriticalSection(mutex); + return 0; +} + +int pthread_mutex_lock(pthread_mutex_t *mutex) +{ + if (mutex == NULL) + return 1; + EnterCriticalSection(mutex); + return 0; +} + +int pthread_mutex_unlock(pthread_mutex_t *mutex) +{ + if (mutex == NULL) + return 1; + LeaveCriticalSection(mutex); + return 0; +} + +int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) +{ + (void)attr; + if (cond == NULL) + return 1; + InitializeConditionVariable(cond); + return 0; +} + +int pthread_cond_destroy(pthread_cond_t *cond) +{ + /* Windows does not have a destroy for conditionals */ + (void)cond; + return 0; +} + +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) +{ + if (cond == NULL || mutex == NULL) + return 1; + return pthread_cond_timedwait(cond, mutex, NULL); +} + +int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, + const struct timespec *abstime) +{ + if (cond == NULL || mutex == NULL) + return 1; + if (!SleepConditionVariableCS(cond, mutex, timespec_to_ms(abstime))) + return 1; + return 0; +} + +int pthread_cond_signal(pthread_cond_t *cond) +{ + if (cond == NULL) + return 1; + WakeConditionVariable(cond); + return 0; +} + +int pthread_cond_broadcast(pthread_cond_t *cond) +{ + if (cond == NULL) + return 1; + WakeAllConditionVariable(cond); + return 0; +} + +int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr) +{ + (void)attr; + if (rwlock == NULL) + return 1; + InitializeSRWLock(&(rwlock->lock)); + rwlock->exclusive = false; + return 0; +} + +int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) +{ + (void)rwlock; + return 0; +} + +int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) +{ + if (rwlock == NULL) + return 1; + AcquireSRWLockShared(&(rwlock->lock)); + return 0; +} + +int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock) +{ + if (rwlock == NULL) + return 1; + return !TryAcquireSRWLockShared(&(rwlock->lock)); +} + +int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) +{ + if (rwlock == NULL) + return 1; + AcquireSRWLockExclusive(&(rwlock->lock)); + rwlock->exclusive = true; + return 0; +} + +int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock) +{ + BOOLEAN ret; + + if (rwlock == NULL) + return 1; + + ret = TryAcquireSRWLockExclusive(&(rwlock->lock)); + if (ret) + rwlock->exclusive = true; + return ret; +} + +int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) +{ + if (rwlock == NULL) + return 1; + + if (rwlock->exclusive) { + rwlock->exclusive = false; + ReleaseSRWLockExclusive(&(rwlock->lock)); + } else { + ReleaseSRWLockShared(&(rwlock->lock)); + } + return 0; +} + +int sched_yield() { + return (int)SwitchToThread(); +} + +void ms_to_timespec(struct timespec *ts, unsigned int ms) +{ + if (ts == NULL) + return; + ts->tv_sec = (ms / 1000) + time(NULL); + ts->tv_nsec = (ms % 1000) * 1000000; +} + +unsigned int timespec_to_ms(const struct timespec *abstime) +{ + if (abstime == NULL) + return INFINITE; + + return ((abstime->tv_sec - time(NULL)) * 1000) + (abstime->tv_nsec / 1000000); +} + +unsigned int pcthread_get_num_procs() +{ + SYSTEM_INFO sysinfo; + + GetSystemInfo(&sysinfo); + return sysinfo.dwNumberOfProcessors; +} + +#else + +#include +unsigned int pcthread_get_num_procs() +{ + return (unsigned int)sysconf(_SC_NPROCESSORS_ONLN); +} +#endif diff --git a/common/pthreads_cross.h b/common/pthreads_cross.h index 5970c679..fc40c9ba 100644 --- a/common/pthreads_cross.h +++ b/common/pthreads_cross.h @@ -1,82 +1,82 @@ -/** -Copyright John Schember - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#ifndef __CPTHREAD_H__ -#define __CPTHREAD_H__ - -#ifdef _WIN32 -#include -#include -#else -#include -#include -#endif -#include - -#ifdef _WIN32 - -typedef CRITICAL_SECTION pthread_mutex_t; -typedef void pthread_mutexattr_t; -typedef void pthread_attr_t; -typedef void pthread_condattr_t; -typedef void pthread_rwlockattr_t; -typedef HANDLE pthread_t; -typedef CONDITION_VARIABLE pthread_cond_t; - -#ifdef __cplusplus -extern "C" { -#endif -int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); -int pthread_join(pthread_t thread, void **value_ptr); -int pthread_detach(pthread_t); - -int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); -int pthread_mutex_destroy(pthread_mutex_t *mutex); -int pthread_mutex_lock(pthread_mutex_t *mutex); -int pthread_mutex_unlock(pthread_mutex_t *mutex); - -int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); -int pthread_cond_destroy(pthread_cond_t *cond); -int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); -int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); -int pthread_cond_signal(pthread_cond_t *cond); -int pthread_cond_broadcast(pthread_cond_t *cond); - -int sched_yield(void); -#ifdef __cplusplus -} -#endif -#endif - - -#ifdef __cplusplus -extern "C" { -#endif -unsigned int pcthread_get_num_procs(); - -void ms_to_timespec(struct timespec *ts, unsigned int ms); -unsigned int timespec_to_ms(const struct timespec *abstime); -#ifdef __cplusplus -} -#endif - -#endif /* __CPTHREAD_H__ */ +/** +Copyright John Schember + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef __CPTHREAD_H__ +#define __CPTHREAD_H__ + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif +#include + +#ifdef _WIN32 + +typedef CRITICAL_SECTION pthread_mutex_t; +typedef void pthread_mutexattr_t; +typedef void pthread_attr_t; +typedef void pthread_condattr_t; +typedef void pthread_rwlockattr_t; +typedef HANDLE pthread_t; +typedef CONDITION_VARIABLE pthread_cond_t; + +#ifdef __cplusplus +extern "C" { +#endif +int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); +int pthread_join(pthread_t thread, void **value_ptr); +int pthread_detach(pthread_t); + +int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); +int pthread_mutex_destroy(pthread_mutex_t *mutex); +int pthread_mutex_lock(pthread_mutex_t *mutex); +int pthread_mutex_unlock(pthread_mutex_t *mutex); + +int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); +int pthread_cond_destroy(pthread_cond_t *cond); +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); +int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); +int pthread_cond_signal(pthread_cond_t *cond); +int pthread_cond_broadcast(pthread_cond_t *cond); + +int sched_yield(void); +#ifdef __cplusplus +} +#endif +#endif + + +#ifdef __cplusplus +extern "C" { +#endif +unsigned int pcthread_get_num_procs(); + +void ms_to_timespec(struct timespec *ts, unsigned int ms); +unsigned int timespec_to_ms(const struct timespec *abstime); +#ifdef __cplusplus +} +#endif + +#endif /* __CPTHREAD_H__ */ diff --git a/common/time_util.h b/common/time_util.h index c1840495..a2ead4ca 100644 --- a/common/time_util.h +++ b/common/time_util.h @@ -32,7 +32,11 @@ either expressed or implied, of the Regents of The University of Michigan. #include #ifdef _WIN32 -#include +#if defined __has_include && __has_include ("winsock2.h") +#include +#else +#include +#endif typedef long long suseconds_t; #endif diff --git a/common/unionfind.h b/common/unionfind.h index fdfef9dc..b6caaa68 100644 --- a/common/unionfind.h +++ b/common/unionfind.h @@ -82,26 +82,20 @@ static inline uint32_t unionfind_get_representative(unionfind_t *uf, uint32_t id // version above. static inline uint32_t unionfind_get_representative(unionfind_t *uf, uint32_t id) { - uint32_t root = uf->parent[id]; // unititialized node, so set to self - if (root == 0xffffffff) { + if (uf->parent[id] == 0xffffffff) { uf->parent[id] = id; return id; } - // chase down the root - while (uf->parent[root] != root) { - root = uf->parent[root]; + // Path halving: make every node point to its grandparent (single pass) + // This is simpler and faster than full path compression while still effective + while (uf->parent[id] != id) { + uf->parent[id] = uf->parent[uf->parent[id]]; // Point to grandparent + id = uf->parent[id]; // Move to grandparent } - // go back and collapse the tree. - while (uf->parent[id] != root) { - uint32_t tmp = uf->parent[id]; - uf->parent[id] = root; - id = tmp; - } - - return root; + return id; } static inline uint32_t unionfind_get_set_size(unionfind_t *uf, uint32_t id) diff --git a/common/workerpool.c b/common/workerpool.c index 359abfef..6b73541c 100644 --- a/common/workerpool.c +++ b/common/workerpool.c @@ -30,6 +30,7 @@ either expressed or implied, of the Regents of The University of Michigan. #define __USE_GNU #include "common/pthreads_cross.h" #include +#include #include #include #ifdef _WIN32 @@ -51,6 +52,7 @@ struct workerpool { pthread_mutex_t mutex; pthread_cond_t startcond; // used to signal the availability of work + bool start_predicate; // predicate that prevents spurious wakeups on startcond pthread_cond_t endcond; // used to signal completion of all work int end_count; // how many threads are done? @@ -70,7 +72,7 @@ void *worker_thread(void *p) struct task *task; pthread_mutex_lock(&wp->mutex); - while (wp->taskspos == zarray_size(wp->tasks)) { + while (wp->taskspos == zarray_size(wp->tasks) || !wp->start_predicate) { wp->end_count++; pthread_cond_broadcast(&wp->endcond); pthread_cond_wait(&wp->startcond, &wp->mutex); @@ -98,6 +100,7 @@ workerpool_t *workerpool_create(int nthreads) workerpool_t *wp = calloc(1, sizeof(workerpool_t)); wp->nthreads = nthreads; wp->tasks = zarray_create(sizeof(struct task)); + wp->start_predicate = false; if (nthreads > 1) { wp->threads = calloc(wp->nthreads, sizeof(pthread_t)); @@ -114,6 +117,13 @@ workerpool_t *workerpool_create(int nthreads) return NULL; } } + + // Wait for the worker threads to be ready + pthread_mutex_lock(&wp->mutex); + while (wp->end_count < wp->nthreads) { + pthread_cond_wait(&wp->endcond, &wp->mutex); + } + pthread_mutex_unlock(&wp->mutex); } return wp; @@ -130,6 +140,7 @@ void workerpool_destroy(workerpool_t *wp) workerpool_add_task(wp, NULL, NULL); pthread_mutex_lock(&wp->mutex); + wp->start_predicate = true; pthread_cond_broadcast(&wp->startcond); pthread_mutex_unlock(&wp->mutex); @@ -157,7 +168,13 @@ void workerpool_add_task(workerpool_t *wp, void (*f)(void *p), void *p) t.f = f; t.p = p; - zarray_add(wp->tasks, &t); + if (wp->nthreads > 1) { + pthread_mutex_lock(&wp->mutex); + zarray_add(wp->tasks, &t); + pthread_mutex_unlock(&wp->mutex); + } else { + zarray_add(wp->tasks, &t); + } } void workerpool_run_single(workerpool_t *wp) @@ -175,9 +192,9 @@ void workerpool_run_single(workerpool_t *wp) void workerpool_run(workerpool_t *wp) { if (wp->nthreads > 1) { - wp->end_count = 0; - pthread_mutex_lock(&wp->mutex); + wp->end_count = 0; + wp->start_predicate = true; pthread_cond_broadcast(&wp->startcond); while (wp->end_count < wp->nthreads) { @@ -185,9 +202,9 @@ void workerpool_run(workerpool_t *wp) pthread_cond_wait(&wp->endcond, &wp->mutex); } - pthread_mutex_unlock(&wp->mutex); - wp->taskspos = 0; + wp->start_predicate = false; + pthread_mutex_unlock(&wp->mutex); zarray_clear(wp->tasks); @@ -198,7 +215,7 @@ void workerpool_run(workerpool_t *wp) int workerpool_get_nprocs() { -#ifdef WIN32 +#ifdef _WIN32 SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); return sysinfo.dwNumberOfProcessors; diff --git a/common/zmaxheap.c b/common/zmaxheap.c index 3ca30af7..75de9950 100644 --- a/common/zmaxheap.c +++ b/common/zmaxheap.c @@ -361,7 +361,7 @@ void zmaxheap_test() { int cap = 10000; int sz = 0; - int32_t *vals = calloc(sizeof(int32_t), cap); + int32_t *vals = calloc(cap, sizeof(int32_t)); zmaxheap_t *heap = zmaxheap_create(sizeof(int32_t)); diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index 1cef2e8f..00000000 --- a/example/.gitignore +++ /dev/null @@ -1 +0,0 @@ -apriltag_demo diff --git a/example/Makefile b/example/Makefile deleted file mode 100644 index 6027428e..00000000 --- a/example/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -CC = gcc -CXX = g++ - -CPPFLAGS = -I.. `pkg-config --cflags opencv4` -CFLAGS = -g -std=gnu99 -Wall -Wno-unused-parameter -Wno-unused-function -O3 -CXXFLAGS = -g -Wall -O3 -LDFLAGS = -lpthread -lm - -TARGETS := apriltag_demo opencv_demo - -.PHONY: all -all: apriltag_demo opencv_demo - -apriltag_demo: apriltag_demo.o ../libapriltag.a - @echo " [$@]" - @$(CC) -o $@ $^ $(LDFLAGS) - -opencv_demo: opencv_demo.o ../libapriltag.a - @echo " [$@]" - @$(CXX) -o $@ $^ $(LDFLAGS) `pkg-config --libs opencv4` - -%.o: %.c - @echo " $@" - @$(CC) -o $@ -c $< $(CFLAGS) $(CPPFLAGS) - -%.o: %.cc - @echo " $@" - @$(CXX) -o $@ -c $< $(CXXFLAGS) $(CPPFLAGS) - -.PHONY: clean -clean: - @rm -rf *.o $(TARGETS) diff --git a/example/README b/example/README deleted file mode 100644 index 5b3167df..00000000 --- a/example/README +++ /dev/null @@ -1 +0,0 @@ -These example programs are meant for distribution, and thus will not build in the april2 tree without modifications. diff --git a/install.sh b/install.sh deleted file mode 100755 index 04bb5b2f..00000000 --- a/install.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -e - -# Usage: install.sh TARGET [RELATIVE PATHS ...] -# -# e.g. ./install.sh /usr/local foo/file1 foo/file2 ... -# This creates the files /usr/local/foo/file1 and /usr/local/foo/file2 - -TARGETDIR=$1 -shift - -for src in "$@"; do - dest=$TARGETDIR/$src - mkdir -p $(dirname $dest) - cp $src $dest - echo $dest -done diff --git a/package.xml b/package.xml index f235a1af..8a6d2d76 100644 --- a/package.xml +++ b/package.xml @@ -2,7 +2,7 @@ apriltag - 3.3.0 + 3.4.5 AprilTag detector library Max Krogius @@ -16,6 +16,7 @@ Max Krogius cmake + python3-dev python3-numpy diff --git a/python_build_flags.py b/python_build_flags.py deleted file mode 100644 index f3f0e79a..00000000 --- a/python_build_flags.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import print_function -import sysconfig -import re -import numpy as np -conf = sysconfig.get_config_vars() - -print('CFLAGS', end=';') -c_flags = [] -# Grab compiler flags minus the compiler itself. -c_flags.extend(conf.get('CC', '').split()[2:]) -c_flags.extend(conf.get('CFLAGS', '').split()) -c_flags.extend(conf.get('CCSHARED', '').split()) -c_flags.append('-I{}'.format(conf.get('INCLUDEPY', ''))) -c_flags.append('-I{}'.format(np.get_include())) -c_flags.append('-Wno-strict-prototypes') -c_flags = [x for x in c_flags if not x.startswith('-O')] -print(' '.join(c_flags), end=';') - - -print('LINKER', end=';') -print(conf.get('BLDSHARED', '').split()[0], end=';') - -print('LDFLAGS', end=';') -print(' '.join(conf.get('BLDSHARED', '').split()[1:]) + ' ' + conf.get('BLDLIBRARY', '') + ' ' + conf.get('LDFLAGS', ''), end=';') - -print('EXT_SUFFIX', end=';') -ext_suffix = '.so' -if 'EXT_SUFFIX' in conf: - ext_suffix = conf['EXT_SUFFIX'] -elif 'MULTIARCH' in conf: - ext_suffix = '.' + conf['MULTIARCH'] + '.so' - -print(ext_suffix, end=';') - diff --git a/tag36h10.c b/tag36h10.c index 44a129e7..9c02c34c 100644 --- a/tag36h10.c +++ b/tag36h10.c @@ -1,3 +1,30 @@ +/* Copyright (C) 2013-2016, The Regents of The University of Michigan. +All rights reserved. +This software was developed in the APRIL Robotics Lab under the +direction of Edwin Olson, ebolson@umich.edu. This software may be +available under alternative licensing terms; contact the address above. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the Regents of The University of Michigan. +*/ + #include #include "tag36h10.h" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..094182c6 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,50 @@ +add_library(getline OBJECT getline.c) + +add_executable(test_detection test_detection.c) +target_link_libraries(test_detection ${PROJECT_NAME} getline) + +# Add the pose estimation test executable +add_executable(test_tag_pose_estimation test_tag_pose_estimation.c) +target_link_libraries(test_tag_pose_estimation ${PROJECT_NAME} getline) + +# test images with true detection +set(TEST_IMAGE_NAMES + "33369213973_9d9bb4cc96_c" + "34085369442_304b6bafd9_c" + "34139872896_defdb2f8d9_c" +) + +foreach(IMG IN LISTS TEST_IMAGE_NAMES) + add_test(NAME test_detection_${IMG} + COMMAND $ data/${IMG} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endforeach() + +# Add pose estimation tests for each image +foreach(IMG IN LISTS TEST_IMAGE_NAMES) + add_test(NAME test_tag_pose_estimation_${IMG} + COMMAND $ + data/${IMG}.jpg data/${IMG}.txt + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endforeach() + +# Quick decode test +file(GLOB COMMON_SRC "${CMAKE_SOURCE_DIR}/common/*.c") +file(GLOB TAG_FILES "${CMAKE_SOURCE_DIR}/tag*.c") + +add_executable(test_quick_decode test_quick_decode.c "${CMAKE_SOURCE_DIR}/apriltag_quad_thresh.c" ${COMMON_SRC} ${TAG_FILES}) +target_include_directories(test_quick_decode PRIVATE "${CMAKE_SOURCE_DIR}") + +if (UNIX) + target_link_libraries(test_quick_decode m) +endif() + +if(NOT MSVC) + find_package(Threads REQUIRED) + target_link_libraries(test_quick_decode Threads::Threads) +endif() + +add_test(NAME test_quick_decode COMMAND test_quick_decode) + diff --git a/test/data/33369213973_9d9bb4cc96_c.jpg b/test/data/33369213973_9d9bb4cc96_c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2526c1dbe3377fc534951376a932230cc747451c GIT binary patch literal 131366 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