pax_global_header00006660000000000000000000000064151736363450014527gustar00rootroot0000000000000052 comment=9c23f6942fe69962e06030905e77067c8673382f colmap-4.0.4/000077500000000000000000000000001517363634500130075ustar00rootroot00000000000000colmap-4.0.4/.clang-format000077500000000000000000000005661517363634500153740ustar00rootroot00000000000000BasedOnStyle: Google BinPackArguments: false BinPackParameters: false DerivePointerAlignment: false IncludeBlocks: Regroup IncludeCategories: - Regex: '^"colmap' Priority: 1 - Regex: '^"pycolmap' Priority: 2 - Regex: '^"thirdparty' Priority: 3 - Regex: '^<[[:alnum:]_]+>' Priority: 4 - Regex: '.*' Priority: 5 SortIncludes: true colmap-4.0.4/.clang-tidy000066400000000000000000000011511517363634500150410ustar00rootroot00000000000000Checks: > performance-*, concurrency-*, bugprone-*, -clang-analyzer-security.ArrayBound, -bugprone-easily-swappable-parameters, -bugprone-exception-escape, -bugprone-implicit-widening-of-multiplication-result, -bugprone-narrowing-conversions, -bugprone-reserved-identifier, -bugprone-unchecked-optional-access, -performance-enum-size, cppcoreguidelines-virtual-class-destructor, google-explicit-constructor, google-build-using-namespace, readability-avoid-const-params-in-decls, clang-analyzer-core*, clang-analyzer-cplusplus*, WarningsAsErrors: '*' FormatStyle: 'file' User: 'user' colmap-4.0.4/.dockerignore000066400000000000000000000000501517363634500154560ustar00rootroot00000000000000.git build* !build/_deps !build/.ccache colmap-4.0.4/.github/000077500000000000000000000000001517363634500143475ustar00rootroot00000000000000colmap-4.0.4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001517363634500165325ustar00rootroot00000000000000colmap-4.0.4/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011531517363634500212240ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. Input images and/or output reconstructions are usually helpful. **Environment:** - OS: [e.g. Windows 11] - COLMAP Version [e.g. 3.8 or git commit hash] - Capture Device [e.g. iPhone X] colmap-4.0.4/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000010601517363634500222540ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. colmap-4.0.4/.github/workflows/000077500000000000000000000000001517363634500164045ustar00rootroot00000000000000colmap-4.0.4/.github/workflows/build-docker.yml000066400000000000000000000065651517363634500215070ustar00rootroot00000000000000name: COLMAP (Docker) on: push: branches: - main - release/* pull_request: types: [ assigned, opened, synchronize, reopened ] release: types: [ published, edited ] env: IS_RELEASE: ${{ github.event_name == 'release' || startsWith(github.ref, 'refs/tags') }} FETCHCONTENT_CACHE_VERSION: 1 FETCHCONTENT_CACHE_DIR: build/_deps COMPILER_CACHE_VERSION: 1 CCACHE_DIR: build/.ccache jobs: build: name: ubuntu-24.04 runs-on: ubuntu-24.04 steps: - name: Free disk space uses: jlumbroso/free-disk-space@v1.3.1 with: tool-cache: false android: true dotnet: true haskell: true large-packages: true docker-images: true swap-storage: true - uses: actions/checkout@v4 - name: Restore FetchContent cache uses: actions/cache@v4 id: cache-fetchcontent with: key: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-docker-${{ hashFiles('cmake/FindDependencies.cmake', 'src/thirdparty/CMakeLists.txt') }} restore-keys: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-docker- path: ${{ env.FETCHCONTENT_CACHE_DIR }} - name: Restore compiler cache uses: actions/cache@v4 id: cache-ccache with: key: ccache-v${{ env.COMPILER_CACHE_VERSION }}-docker-${{ github.run_id }} restore-keys: ccache-v${{ env.COMPILER_CACHE_VERSION }}-docker- path: ${{ env.CCACHE_DIR }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub if: ${{ env.IS_RELEASE == 'true' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push run: | dockertag=$(date +%Y%m%d).${{ github.run_number }} if [[ "${{ env.IS_RELEASE }}" == "true" ]]; then cuda_archs="50;60;70;75;90" else cuda_archs="50" fi mkdir -p build/.ccache build/_deps docker_args=( --file docker/Dockerfile --build-arg CUDA_ARCHITECTURES="$cuda_archs" --build-arg FETCHCONTENT_FULLY_DISCONNECTED=${{ steps.cache-fetchcontent.outputs.cache-hit == 'true' && 'ON' || 'OFF' }} ) docker build . \ "${docker_args[@]}" \ --tag colmap/colmap:$dockertag # Extract updated build caches from the builder stage. docker build . \ "${docker_args[@]}" \ --target cache-export \ --output type=local,dest=build/cache-export rm -rf build/.ccache build/_deps mv build/cache-export/.ccache build/.ccache mv build/cache-export/_deps build/_deps rm -rf build/cache-export if [[ "${{ env.IS_RELEASE }}" == "true" ]]; then docker tag colmap/colmap:$dockertag colmap/colmap:latest docker push colmap/colmap:$dockertag docker push colmap/colmap:latest fi - name: Cleanup compiler cache run: | set -x sudo apt-get install -y ccache ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/.github/workflows/build-mac.yml000066400000000000000000000066771517363634500210040ustar00rootroot00000000000000name: COLMAP (Mac) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: push: branches: - main - release/* pull_request: types: [ assigned, opened, synchronize, reopened ] release: types: [ published, edited ] jobs: build: name: ${{ matrix.config.os }} ${{ matrix.config.arch }} ${{ matrix.config.cmakeBuildType }} runs-on: ${{ matrix.config.os }} strategy: matrix: config: [ { os: macos-15, arch: arm64, cmakeBuildType: Release, }, ] env: COMPILER_CACHE_VERSION: 1 COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache CCACHE_BASEDIR: ${{ github.workspace }} FETCHCONTENT_CACHE_VERSION: 1 FETCHCONTENT_CACHE_DIR: ${{ github.workspace }}/build/_deps GLOG_v: 2 GLOG_logtostderr: 1 steps: - uses: actions/checkout@v4 - name: Restore compiler cache uses: actions/cache@v4 id: cache-compiler with: key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }}-${{ github.run_id }}-${{ github.run_number }} restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }} path: ${{ env.COMPILER_CACHE_DIR }} - name: Restore FetchContent cache uses: actions/cache@v4 id: cache-fetchcontent with: key: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ hashFiles('cmake/FindDependencies.cmake', 'src/thirdparty/CMakeLists.txt') }} restore-keys: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}- path: ${{ env.FETCHCONTENT_CACHE_DIR }} - name: Setup Mac run: | # Fix `brew link` error. find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete brew update # Workaround for CI failures. brew install \ cmake \ ninja \ boost \ eigen \ openimageio \ curl \ metis \ glog \ googletest \ ceres-solver \ qt \ glew \ cgal \ sqlite3 \ ccache \ libomp brew link --force libomp - name: Configure and build run: | export PATH="/usr/local/opt/qt/bin:$PATH" cmake --version mkdir -p build cd build cmake .. \ -GNinja \ -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ -DTESTS_ENABLED=ON \ -DWERROR_ENABLED=ON \ -DQt6_DIR="$(brew --prefix qt)/lib/cmake/Qt6" \ -DFETCHCONTENT_FULLY_DISCONNECTED=${{ steps.cache-fetchcontent.outputs.cache-hit == 'true' && 'ON' || 'OFF' }} ninja - name: Run tests run: | cd build set +e ctest --output-on-failure - name: Cleanup compiler cache run: | set -x ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/.github/workflows/build-pycolmap.yml000066400000000000000000000333071517363634500220560ustar00rootroot00000000000000name: PyCOLMAP concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: workflow_dispatch: push: branches: - main - release/* pull_request: types: [ assigned, opened, synchronize, reopened ] release: types: [ published, edited ] jobs: build: name: ${{ matrix.config.os }} ${{ matrix.config.arch }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} runs-on: ${{ matrix.config.os }} strategy: matrix: config: [ {os: ubuntu-24.04, cudaEnabled: false}, {os: macos-14, arch: arm64, deploymentTarget: 14.0, cudaEnabled: false}, {os: windows-2025, cudaEnabled: false}, {os: ubuntu-24.04, cudaEnabled: true}, ] env: COMPILER_CACHE_VERSION: 1 COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache CCACHE_BASEDIR: ${{ github.workspace }} FETCHCONTENT_CACHE_VERSION: 1 FETCHCONTENT_CACHE_DIR: ${{ github.workspace }}/fetchcontent-cache MACOSX_DEPLOYMENT_TARGET: ${{ matrix.config.deploymentTarget }} # For faster builds in PRs, skip all but the oldest Python versions. PULL_REQUEST_CIBW_BUILD: cp311-{macosx,manylinux,win}* steps: - name: Free disk space if: runner.os == 'Linux' && matrix.config.cudaEnabled uses: jlumbroso/free-disk-space@v1.3.1 with: tool-cache: false android: true dotnet: true haskell: true large-packages: true docker-images: true swap-storage: true - uses: actions/checkout@v4 - name: Restore compiler cache uses: actions/cache@v4 id: cache-compiler with: key: pycolmap-v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ github.run_id }}-${{ github.run_number }} restore-keys: pycolmap-v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }} path: ${{ env.COMPILER_CACHE_DIR }} - name: Restore FetchContent cache uses: actions/cache@v4 id: cache-fetchcontent with: key: pycolmap-fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-cuda_${{ matrix.config.cudaEnabled }}-${{ hashFiles('cmake/FindDependencies.cmake', 'src/thirdparty/CMakeLists.txt') }} restore-keys: pycolmap-fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-cuda_${{ matrix.config.cudaEnabled }}- path: ${{ env.FETCHCONTENT_CACHE_DIR }} - name: Select Python if: github.event_name == 'pull_request' && runner.os != 'Windows' run: | echo "CIBW_BUILD=${PULL_REQUEST_CIBW_BUILD}" >> "$GITHUB_ENV" - name: Select Python if: ${{ github.event_name == 'pull_request' && runner.os == 'Windows' }} shell: pwsh run: | echo "CIBW_BUILD=${env:PULL_REQUEST_CIBW_BUILD}" >> "${env:GITHUB_ENV}" - name: Set env (macOS) if: runner.os == 'macOS' run: | VCPKG_TARGET_TRIPLET="arm64-osx-release" echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "$GITHUB_ENV" VCPKG_INSTALLATION_ROOT="/Users/runner/work/vcpkg" CMAKE_TOOLCHAIN_FILE="${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" CMAKE_OSX_ARCHITECTURES=${{ matrix.config.arch }} echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "$GITHUB_ENV" echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "$GITHUB_ENV" echo "CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" >> "$GITHUB_ENV" echo "ARCHFLAGS=-arch ${CMAKE_OSX_ARCHITECTURES}" >> "$GITHUB_ENV" # Fix: cibuildhweel cannot interpolate env variables. CONFIG_SETTINGS="cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_INSTALLED_DIR=${{ github.workspace }}/build/vcpkg_installed" CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" echo "CIBW_CONFIG_SETTINGS_MACOS=${CONFIG_SETTINGS}" >> "$GITHUB_ENV" echo "FETCHCONTENT_BASE_DIR=${FETCHCONTENT_CACHE_DIR}" >> "$GITHUB_ENV" if [ "${{ steps.cache-fetchcontent.outputs.cache-hit }}" = "true" ]; then echo "FETCHCONTENT_FULLY_DISCONNECTED=ON" >> "$GITHUB_ENV" else echo "FETCHCONTENT_FULLY_DISCONNECTED=OFF" >> "$GITHUB_ENV" fi # vcpkg binary caching # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository. VCPKG_BINARY_SOURCES="clear;x-azblob,https://colmap.blob.core.windows.net/github-actions-cache,sp=r&st=2024-12-10T17:29:32Z&se=2030-12-31T01:29:32Z&spr=https&sv=2022-11-02&sr=c&sig=bWydkilTMjRn3LHKTxLgdWrFpV4h%2Finzoe9QCOcPpYQ%3D,read" if [ -n "${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}" ]; then # The secrets are only accessible in runs triggered from within the target repository and not forks. VCPKG_BINARY_SOURCES="${VCPKG_BINARY_SOURCES};x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write" fi echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "$GITHUB_ENV" - name: Set env (Windows) if: runner.os == 'Windows' shell: pwsh run: | echo "${{ env.COMPILER_CACHE_DIR }}/bin" >> "${env:GITHUB_PATH}" $VCPKG_INSTALLATION_ROOT = "${{ github.workspace }}/vcpkg" echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "${env:GITHUB_ENV}" $CMAKE_TOOLCHAIN_FILE = "${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "${env:GITHUB_ENV}" $VCPKG_TARGET_TRIPLET = "x64-windows-release" echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "${env:GITHUB_ENV}" # Fix: cibuildhweel cannot interpolate env variables. $CMAKE_TOOLCHAIN_FILE = $CMAKE_TOOLCHAIN_FILE.replace('\', '/') $VCPKG_INSTALLED_DIR = "${{ github.workspace }}/build/vcpkg_installed" $VCPKG_INSTALLED_DIR = $VCPKG_INSTALLED_DIR.replace('\', '/') $CONFIG_SETTINGS = "cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" $CONFIG_SETTINGS = "${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" $CONFIG_SETTINGS = "${CONFIG_SETTINGS} cmake.define.VCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR}" echo "CIBW_CONFIG_SETTINGS_WINDOWS=${CONFIG_SETTINGS}" >> "${env:GITHUB_ENV}" $CIBW_REPAIR_WHEEL_COMMAND = "delvewheel repair -v --add-path ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin -w {dest_dir} {wheel}" echo "CIBW_REPAIR_WHEEL_COMMAND_WINDOWS=${CIBW_REPAIR_WHEEL_COMMAND}" >> "${env:GITHUB_ENV}" $FETCHCONTENT_BASE_DIR = "${env:FETCHCONTENT_CACHE_DIR}".replace('\', '/') echo "FETCHCONTENT_BASE_DIR=${FETCHCONTENT_BASE_DIR}" >> "${env:GITHUB_ENV}" if ("${{ steps.cache-fetchcontent.outputs.cache-hit }}" -eq "true") { echo "FETCHCONTENT_FULLY_DISCONNECTED=ON" >> "${env:GITHUB_ENV}" } else { echo "FETCHCONTENT_FULLY_DISCONNECTED=OFF" >> "${env:GITHUB_ENV}" } # vcpkg binary caching # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository. $VCPKG_BINARY_SOURCES = "clear;x-azblob,https://colmap.blob.core.windows.net/github-actions-cache,sp=r&st=2024-12-10T17:29:32Z&se=2030-12-31T01:29:32Z&spr=https&sv=2022-11-02&sr=c&sig=bWydkilTMjRn3LHKTxLgdWrFpV4h%2Finzoe9QCOcPpYQ%3D,read" if ("${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}") { # The secrets are only accessible in runs triggered from within the target repository and not forks. $VCPKG_BINARY_SOURCES += ";x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write" } echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "${env:GITHUB_ENV}" - name: Set env (Linux) if: runner.os == 'Linux' run: | VCPKG_TARGET_TRIPLET="x64-linux-release" echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" >> "$GITHUB_ENV" VCPKG_INSTALLATION_ROOT="${{ github.workspace }}/vcpkg" CMAKE_TOOLCHAIN_FILE="${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" echo "VCPKG_INSTALLATION_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "$GITHUB_ENV" echo "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" >> "$GITHUB_ENV" # Fix: cibuildhweel cannot interpolate env variables. CONFIG_SETTINGS="cmake.define.CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" CONFIG_SETTINGS="${CONFIG_SETTINGS} cmake.define.VCPKG_INSTALLED_DIR=/project/build/vcpkg_installed" echo "CIBW_CONFIG_SETTINGS_LINUX=${CONFIG_SETTINGS}" >> "$GITHUB_ENV" # Remap caching paths to the container CONTAINER_COMPILER_CACHE_DIR="/compiler-cache" CONTAINER_FETCHCONTENT_CACHE_DIR="/fetchcontent-cache" CIBW_CONTAINER_ENGINE="docker; create_args: -v ${COMPILER_CACHE_DIR}:${CONTAINER_COMPILER_CACHE_DIR} -v ${FETCHCONTENT_CACHE_DIR}:${CONTAINER_FETCHCONTENT_CACHE_DIR}" echo "CIBW_CONTAINER_ENGINE=${CIBW_CONTAINER_ENGINE}" >> "$GITHUB_ENV" echo "CONTAINER_COMPILER_CACHE_DIR=${CONTAINER_COMPILER_CACHE_DIR}" >> "$GITHUB_ENV" echo "CCACHE_DIR=${CONTAINER_COMPILER_CACHE_DIR}/ccache" >> "$GITHUB_ENV" echo "CCACHE_BASEDIR=/project" >> "$GITHUB_ENV" echo "FETCHCONTENT_BASE_DIR=${CONTAINER_FETCHCONTENT_CACHE_DIR}" >> "$GITHUB_ENV" if [ "${{ steps.cache-fetchcontent.outputs.cache-hit }}" = "true" ]; then echo "FETCHCONTENT_FULLY_DISCONNECTED=ON" >> "$GITHUB_ENV" else echo "FETCHCONTENT_FULLY_DISCONNECTED=OFF" >> "$GITHUB_ENV" fi # vcpkg binary caching # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository. VCPKG_BINARY_SOURCES="clear;x-azblob,https://colmap.blob.core.windows.net/github-actions-cache,sp=r&st=2024-12-10T17:29:32Z&se=2030-12-31T01:29:32Z&spr=https&sv=2022-11-02&sr=c&sig=bWydkilTMjRn3LHKTxLgdWrFpV4h%2Finzoe9QCOcPpYQ%3D,read" if [ -n "${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}" ]; then # The secrets are only accessible in runs triggered from within the target repository and not forks. VCPKG_BINARY_SOURCES="${VCPKG_BINARY_SOURCES};x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write" fi echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "$GITHUB_ENV" CIBW_ENVIRONMENT_PASS_LINUX="VCPKG_TARGET_TRIPLET VCPKG_INSTALLATION_ROOT CMAKE_TOOLCHAIN_FILE VCPKG_BINARY_SOURCES CONTAINER_COMPILER_CACHE_DIR CCACHE_DIR CCACHE_BASEDIR FETCHCONTENT_BASE_DIR FETCHCONTENT_FULLY_DISCONNECTED" echo "CIBW_ENVIRONMENT_PASS_LINUX=${CIBW_ENVIRONMENT_PASS_LINUX}" >> "$GITHUB_ENV" if [ "${{ matrix.config.cudaEnabled }}" = "true" ]; then CIBW_MANYLINUX_X86_64_IMAGE="pytorch/manylinux2_28-builder:cuda12.9" CIBW_REPAIR_WHEEL_COMMAND="auditwheel repair --exclude libcudart* --exclude libcurand* -w {dest_dir} {wheel}" echo "CIBW_REPAIR_WHEEL_COMMAND=${CIBW_REPAIR_WHEEL_COMMAND}" >> "$GITHUB_ENV" # Edit pyproject.toml to change package name to pycolmap-cuda12 and add CUDA Runtime native Libraries and CURAND native runtime libraries to dependencies pip install tomlkit python python/ci/update_pyproject_toml.py --name "pycolmap-cuda12" --add-deps "cuda-toolkit[cudart,curand]>=12,<13" else CIBW_MANYLINUX_X86_64_IMAGE="quay.io/pypa/manylinux_2_28_x86_64" fi echo "CIBW_MANYLINUX_X86_64_IMAGE=${CIBW_MANYLINUX_X86_64_IMAGE}" >> "$GITHUB_ENV" - name: Build wheels uses: pypa/cibuildwheel@v3.3.1 with: package-dir: ./ env: CIBW_ARCHS_MACOS: ${{ matrix.config.arch }} CIBW_ENVIRONMENT: > BUILD_CUDA_ENABLED=${{ matrix.config.cudaEnabled }} - name: Archive wheels uses: actions/upload-artifact@v4 with: name: pycolmap-${{ matrix.config.os }}-${{ matrix.config.arch }}${{ matrix.config.cudaEnabled && '-CUDA' || '' }} path: wheelhouse/pycolmap*.whl pypi-publish: name: Publish wheels to PyPI needs: build runs-on: ubuntu-latest environment: name: pypi # This URL is only for display in the GitHub UI url: https://pypi.org/p/pycolmap permissions: id-token: write # We publish the wheel to pypi when a new tag is pushed, # either by creating a new GitHub release or explicitly with `git tag` if: ${{ github.event_name == 'release' || startsWith(github.ref, 'refs/tags') }} steps: - name: Download wheels uses: actions/download-artifact@v4 with: path: ./artifacts/ - name: Move wheels run: mkdir ./wheelhouse && mv ./artifacts/**/*.whl ./wheelhouse/ - name: Publish package uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true packages-dir: ./wheelhouse/ colmap-4.0.4/.github/workflows/build-ubuntu.yml000066400000000000000000000300561517363634500215520ustar00rootroot00000000000000name: COLMAP (Ubuntu) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: push: branches: - main - release/* pull_request: types: [ assigned, opened, synchronize, reopened ] release: types: [ published, edited ] jobs: build: name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} ${{ matrix.config.asanEnabled && 'ASan' || '' }} ${{ matrix.config.coverageEnabled && 'Coverage' || '' }} runs-on: ${{ matrix.config.os }} strategy: matrix: config: [ { os: ubuntu-24.04, qtVersion: 6, cmakeBuildType: Release, asanEnabled: false, guiEnabled: true, cudaEnabled: false, e2eTests: false, checkCodeFormat: true, coverageEnabled: true, }, { os: ubuntu-22.04, qtVersion: 6, cmakeBuildType: Release, asanEnabled: false, guiEnabled: true, cudaEnabled: false, e2eTests: true, checkCodeFormat: true, coverageEnabled: false, }, { os: ubuntu-22.04, qtVersion: 5, cmakeBuildType: Release, asanEnabled: false, guiEnabled: false, cudaEnabled: true, e2eTests: false, checkCodeFormat: false, coverageEnabled: false, }, { os: ubuntu-24.04, qtVersion: 6, cmakeBuildType: Release, asanEnabled: true, guiEnabled: false, cudaEnabled: false, e2eTests: false, checkCodeFormat: false, coverageEnabled: false, }, { os: ubuntu-24.04, qtVersion: 6, cmakeBuildType: ClangTidy, asanEnabled: false, guiEnabled: false, cudaEnabled: false, e2eTests: false, checkCodeFormat: false, coverageEnabled: false, }, ] env: COMPILER_CACHE_VERSION: 1 COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache CCACHE_BASEDIR: ${{ github.workspace }} CTCACHE_DIR: ${{ github.workspace }}/compiler-cache/ctcache FETCHCONTENT_CACHE_VERSION: 1 FETCHCONTENT_CACHE_DIR: ${{ github.workspace }}/build/_deps GLOG_v: 2 GLOG_logtostderr: 1 steps: - uses: actions/checkout@v4 - name: Check code format if: matrix.config.checkCodeFormat run: | set +x -euo pipefail python -m pip install ruff==0.15.0 clang-format==21.1.8 ./scripts/format/c++.sh ./scripts/format/python.sh git diff --name-only git diff --exit-code || (echo "Code formatting failed" && exit 1) - name: Restore Compiler cache uses: actions/cache@v4 id: cache-compiler with: key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }} restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }} path: ${{ env.COMPILER_CACHE_DIR }} - name: Restore FetchContent cache uses: actions/cache@v4 id: cache-fetchcontent with: key: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-cuda_${{ matrix.config.cudaEnabled }}-${{ hashFiles('cmake/FindDependencies.cmake', 'src/thirdparty/CMakeLists.txt') }} restore-keys: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-cuda_${{ matrix.config.cudaEnabled }}- path: ${{ env.FETCHCONTENT_CACHE_DIR }} - name: Install compiler cache run: | mkdir -p "$CCACHE_DIR" "$CTCACHE_DIR" echo "$COMPILER_CACHE_DIR/bin" >> $GITHUB_PATH if [ -f "$COMPILER_CACHE_DIR/bin/ccache" ]; then exit 0 fi set -x wget https://github.com/ccache/ccache/releases/download/v4.12.2/ccache-4.12.2-linux-x86_64.tar.xz echo "630c34ec94d451b200f5b14a6a25580d6a45bc80c394b7e0b93e33556eee5d32 ccache-4.12.2-linux-x86_64.tar.xz" | sha256sum --check tar xfv ccache-4.12.2-linux-x86_64.tar.xz mkdir -p "$COMPILER_CACHE_DIR/bin" mv ./ccache-4.12.2-linux-x86_64/ccache "$COMPILER_CACHE_DIR/bin" ctcache_commit_id="66c3614175fc650591488519333c411b2eac15a3" wget https://github.com/matus-chochlik/ctcache/archive/${ctcache_commit_id}.zip echo "108b087f156a9fe7da0c796de1ef73f5855d2a33a27983769ea39061359a40fc ${ctcache_commit_id}.zip" | sha256sum --check unzip "${ctcache_commit_id}.zip" mv ctcache-${ctcache_commit_id}/clang-tidy* "$COMPILER_CACHE_DIR/bin" - name: Setup Ubuntu run: | if [ "${{ matrix.config.qtVersion }}" == "5" ]; then qt_packages="qtbase5-dev libqt5opengl5-dev libcgal-qt5-dev" elif [ "${{ matrix.config.qtVersion }}" == "6" ]; then qt_packages="qt6-base-dev libqt6opengl6-dev libqt6openglwidgets6" fi sudo apt-get update && sudo apt-get install -y \ build-essential \ cmake \ ninja-build \ libboost-program-options-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libceres-dev \ libopenimageio-dev \ openimageio-tools \ libsuitesparse-dev \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libgmock-dev \ libsqlite3-dev \ libglew-dev \ $qt_packages \ libcgal-dev \ libgl1-mesa-dri \ libunwind-dev \ libcurl4-openssl-dev \ libmkl-full-dev \ xvfb # Fix issue in Ubuntu's openimageio CMake config. # We don't depend on any of openimageio's OpenCV functionality, # but it still requires the OpenCV include directory to exist. sudo mkdir -p /usr/include/opencv4 if [ "${{ matrix.config.cudaEnabled }}" == "true" ]; then if [ "${{ matrix.config.os }}" == "ubuntu-22.04" ]; then sudo apt-get install -y \ nvidia-cuda-toolkit \ nvidia-cuda-toolkit-gcc \ gcc-10 g++-10 echo "CC=/usr/bin/gcc-10" >> $GITHUB_ENV echo "CXX=/usr/bin/g++-10" >> $GITHUB_ENV echo "CUDAHOSTCXX=/usr/bin/g++-10" >> $GITHUB_ENV fi fi if [ "${{ matrix.config.asanEnabled }}" == "true" ]; then sudo apt-get install -y clang-18 libomp-18-dev echo "CC=/usr/bin/clang-18" >> $GITHUB_ENV echo "CXX=/usr/bin/clang++-18" >> $GITHUB_ENV fi if [ "${{ matrix.config.cmakeBuildType }}" == "ClangTidy" ]; then sudo apt-get install -y clang-18 clang-tidy-18 libomp-18-dev echo "CC=/usr/bin/clang-18" >> $GITHUB_ENV echo "CXX=/usr/bin/clang++-18" >> $GITHUB_ENV fi if [ "${{ matrix.config.coverageEnabled }}" == "true" ]; then sudo apt-get install -y gcovr fi - name: Configure and build run: | set -x cmake --version mkdir -p build cd build cmake .. \ -GNinja \ -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ -DCMAKE_INSTALL_PREFIX=./install \ -DCMAKE_CUDA_ARCHITECTURES=50 \ -DTESTS_ENABLED=ON \ -DWERROR_ENABLED=ON \ -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} \ -DGUI_ENABLED=${{ matrix.config.guiEnabled }} \ -DASAN_ENABLED=${{ matrix.config.asanEnabled }} \ -DCOVERAGE_ENABLED=${{ matrix.config.coverageEnabled }} \ -DBLA_VENDOR=Intel10_64lp \ -DFETCHCONTENT_FULLY_DISCONNECTED=${{ steps.cache-fetchcontent.outputs.cache-hit == 'true' && 'ON' || 'OFF' }} ninja -k 10000 - name: Install and build sample if: ${{ matrix.config.cmakeBuildType != 'ClangTidy' && !matrix.config.asanEnabled && !matrix.config.coverageEnabled }} run: | set -x cd build ninja install cd ../doc/sample-project mkdir build cd build export colmap_DIR=${{ github.workspace }}/build/install/share/colmap cmake .. \ -GNinja \ -DCMAKE_CUDA_ARCHITECTURES=50 ninja ./hello_world --message "world" - name: Run tests if: ${{ matrix.config.cmakeBuildType != 'ClangTidy' }} run: | if [ "${{ matrix.config.cudaEnabled }}" == "true" ]; then ctestExclusions="(feature/sift_test)|(mvs/gpu_mat_test)" fi export DISPLAY=":99.0" export QT_QPA_PLATFORM="offscreen" Xvfb :99 & sleep 3 cd build ctest -E "$ctestExclusions" --output-on-failure - name: Run E2E tests if: matrix.config.e2eTests run: | export DISPLAY=":99.0" export QT_QPA_PLATFORM="offscreen" Xvfb :99 & sleep 3 sudo apt install 7zip mkdir eth3d_benchmark # Error thresholds in degrees and meters, # as the ETH3D groundtruth has metric scale. GLOG_v=1 python ./python/ci/test_regression_eth3d.py \ --workspace_path ./eth3d_benchmark \ --colmap_path ./build/src/colmap/exe/colmap \ --dataset_names boulders door \ --max_rotation_error 1.0 \ --max_proj_center_error 0.05 \ --use_cpu - name: Generate coverage report if: matrix.config.coverageEnabled run: | set -x cd build ../scripts/shell/generate_coverage_report.sh - name: Upload coverage HTML report uses: actions/upload-artifact@v4 if: matrix.config.coverageEnabled with: name: code-coverage path: build/coverage-html - name: Generate Github code coverage report if: matrix.config.coverageEnabled uses: irongut/CodeCoverageSummary@v1.3.0 with: filename: build/coverage-cobertura.xml badge: true fail_below_min: false format: markdown hide_branch_rate: false hide_complexity: true indicators: true output: both thresholds: '75 90' # TODO: Add code coverage comment to PR. Currently, this action reports # coverage for the entire repository, not just the changed files in the PR. # We could manually filter the coverage report to only include the changed # files in the PR, but this is non-trivial and may not be worth the effort. # - name: Add Github PR code coverage comment # uses: marocchino/sticky-pull-request-comment@v2 # if: ${{ matrix.config.coverageEnabled && github.event_name == 'pull_request' }} # with: # recreate: true # path: code-coverage-results.md - name: Cleanup compiler cache run: | set -x ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose echo "Size of ctcache before: $(du -sh $CTCACHE_DIR)" echo "Number of ctcache files before: $(find $CTCACHE_DIR | wc -l)" # Delete cache older than 10 days. find "$CTCACHE_DIR"/*/ -mtime +10 -print0 | xargs -0 rm -rf echo "Size of ctcache after: $(du -sh $CTCACHE_DIR)" echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)" colmap-4.0.4/.github/workflows/build-windows.yml000066400000000000000000000207521517363634500217240ustar00rootroot00000000000000name: COLMAP (Windows) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: push: branches: - main - release/* pull_request: types: [ assigned, opened, synchronize, reopened ] release: types: [ published, edited ] jobs: build: name: ${{ matrix.config.os }} ${{ matrix.config.cmakeBuildType }} ${{ matrix.config.cudaEnabled && 'CUDA' || '' }} runs-on: ${{ matrix.config.os }} strategy: matrix: config: [ { os: windows-2025, cmakeBuildType: Release, cudaEnabled: true, testsEnabled: true, exportPackage: true, }, { os: windows-2025, cmakeBuildType: Release, cudaEnabled: false, testsEnabled: true, exportPackage: true, }, ] env: COMPILER_CACHE_VERSION: 1 COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache CCACHE_BASEDIR: ${{ github.workspace }} FETCHCONTENT_CACHE_VERSION: 1 FETCHCONTENT_CACHE_DIR: ${{ github.workspace }}/build/_deps GLOG_v: 2 GLOG_logtostderr: 1 CUDA_MAJOR_VERSION: 12 CUDA_MINOR_VERSION: 9 CUDA_PATCH_VERSION: 1 steps: - uses: actions/checkout@v4 # We define the vcpkg binary sources using separate variables for read and # write operations: # * Read sources are defined as inline. These can be read by anyone and, # in particular, pull requests from forks. Unfortunately, we cannot # define these as action environment variables. See: # https://github.com/orgs/community/discussions/44322 # * Write sources are defined as action secret variables. These cannot be # read by pull requests from forks but only from pull requests from # within the target repository (i.e., created by a repository owner). # This protects us from malicious actors accessing our secrets and # gaining write access to our binary cache. For more information, see: # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ - name: Setup vcpkg binary cache shell: pwsh run: | # !!!PLEASE!!! be nice and don't use this cache for your own purposes. This is only meant for CI purposes in this repository. $VCPKG_BINARY_SOURCES = "clear;x-azblob,https://colmap.blob.core.windows.net/github-actions-cache,sp=r&st=2024-12-10T17:29:32Z&se=2030-12-31T01:29:32Z&spr=https&sv=2022-11-02&sr=c&sig=bWydkilTMjRn3LHKTxLgdWrFpV4h%2Finzoe9QCOcPpYQ%3D,read" if ("${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }}") { # The secrets are only accessible in runs triggered from within the target repository and not forks. $VCPKG_BINARY_SOURCES += ";x-azblob,${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_URL }},${{ secrets.VCPKG_BINARY_CACHE_AZBLOB_SAS }},write" } echo "VCPKG_BINARY_SOURCES=${VCPKG_BINARY_SOURCES}" >> "${env:GITHUB_ENV}" - name: Restore compiler cache uses: actions/cache@v4 id: cache-compiler with: key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }} restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }} path: ${{ env.COMPILER_CACHE_DIR }} - name: Restore FetchContent cache uses: actions/cache@v4 id: cache-fetchcontent with: key: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-cuda_${{ matrix.config.cudaEnabled }}-${{ hashFiles('cmake/FindDependencies.cmake', 'src/thirdparty/CMakeLists.txt') }} restore-keys: fetchcontent-v${{ env.FETCHCONTENT_CACHE_VERSION }}-${{ matrix.config.os }}-cuda_${{ matrix.config.cudaEnabled }}- path: ${{ env.FETCHCONTENT_CACHE_DIR }} - name: Install compile cache shell: pwsh run: | New-Item -ItemType Directory -Force -Path "${{ env.CCACHE_DIR }}" echo "${{ env.COMPILER_CACHE_DIR }}/bin" | Out-File -Encoding utf8 -Append -FilePath $env:GITHUB_PATH if (Test-Path -PathType Leaf "${{ env.COMPILER_CACHE_DIR }}/bin/ccache.exe") { exit } .github/workflows/install-ccache.ps1 -Destination "${{ env.COMPILER_CACHE_DIR }}/bin" - name: Install CUDA uses: Jimver/cuda-toolkit@v0.2.30 if: matrix.config.cudaEnabled id: cuda-toolkit with: cuda: '${{ env.CUDA_MAJOR_VERSION }}.${{ env.CUDA_MINOR_VERSION }}.${{ env.CUDA_PATCH_VERSION }}' sub-packages: '["nvcc", "nvtx", "cudart", "curand", "curand_dev", "nvrtc_dev"]' method: 'network' - name: Setup vcpkg shell: pwsh run: | ./scripts/shell/enter_vs_dev_shell.ps1 cd ${{ github.workspace }} git clone https://github.com/microsoft/vcpkg cd vcpkg ./bootstrap-vcpkg.bat - name: Install CMake and Ninja uses: lukka/get-cmake@latest with: cmakeVersion: "3.31.0" ninjaVersion: "1.12.1" - name: Configure and build shell: pwsh run: | ./scripts/shell/enter_vs_dev_shell.ps1 cd ${{ github.workspace }} ./vcpkg/vcpkg.exe integrate install mkdir -Force build cd build cmake .. ` -GNinja ` -DCMAKE_MAKE_PROGRAM=ninja ` -DCMAKE_BUILD_TYPE=Release ` -DTESTS_ENABLED=${{ matrix.config.testsEnabled }} ` -DGUI_ENABLED=ON ` -DCUDA_ENABLED=${{ matrix.config.cudaEnabled }} ` -DCMAKE_CUDA_ARCHITECTURES=all-major ` -DCUDAToolkit_ROOT="${{ steps.cuda-toolkit.outputs.CUDA_PATH }}" ` -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" ` -DVCPKG_TARGET_TRIPLET=x64-windows-release ` -DCMAKE_INSTALL_PREFIX=install ` -DFETCHCONTENT_FULLY_DISCONNECTED=${{ steps.cache-fetchcontent.outputs.cache-hit == 'true' && 'ON' || 'OFF' }} ninja - name: Run tests shell: pwsh run: | ./vcpkg/vcpkg.exe integrate install cd build $EXCLUDED_TESTS = "(feature/sift_test)|(util/opengl_utils_test)|(mvs/gpu_mat_test)" ctest -E ${EXCLUDED_TESTS} --output-on-failure - name: Export package if: matrix.config.exportPackage shell: pwsh run: | ./vcpkg/vcpkg.exe integrate install cd build ninja install ../vcpkg/vcpkg.exe install ` --triplet=x64-windows-release ` --x-feature=gui ` --x-feature=cgal ` $(if ($${{ matrix.config.testsEnabled }}) { echo "--x-feature=tests" }) ` $(if ($${{ matrix.config.cudaEnabled }}) { echo "--x-feature=cuda" }) ../vcpkg/vcpkg.exe export --raw --output-dir vcpkg_export --output colmap cp vcpkg_export/colmap/installed/x64-windows/bin/*.dll install/bin cp vcpkg_export/colmap/installed/x64-windows-release/bin/*.dll install/bin cp -r vcpkg_export/colmap/installed/x64-windows/Qt6/plugins install if ($${{ matrix.config.cudaEnabled }}) { cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/cudart64_*.dll" install/bin cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/curand64_*.dll" install/bin } Remove-Item -Recurse -Force -ErrorAction SilentlyContinue install/include,install/lib,install/share - name: Upload package uses: actions/upload-artifact@v4 if: ${{ matrix.config.exportPackage && matrix.config.cudaEnabled }} with: name: colmap-x64-windows-cuda path: build/install - name: Upload package uses: actions/upload-artifact@v4 if: ${{ matrix.config.exportPackage && !matrix.config.cudaEnabled }} with: name: colmap-x64-windows-nocuda path: build/install - name: Cleanup compiler cache shell: pwsh run: | ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/.github/workflows/install-ccache.ps1000066400000000000000000000023741517363634500217110ustar00rootroot00000000000000[CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Destination ) $version = "4.12.1" $folder="ccache-$version-windows-x86_64" $url = "https://github.com/ccache/ccache/releases/download/v$version/$folder.zip" $expectedSha256 = "98AEA520D66905B8BA7A8E648A4CC0CA941D5E119D441F1E879A4A9045BF18F6" $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $PSNativeCommandUseErrorActionPreference = $true try { New-Item -Path "$Destination" -ItemType Container -ErrorAction SilentlyContinue Write-Host "Download CCache" $zipFilePath = Join-Path "$env:TEMP" "$folder.zip" Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile "$zipFilePath" -MaximumRetryCount 3 $hash = Get-FileHash $zipFilePath -Algorithm "sha256" if ($hash.Hash -ne $expectedSha256) { throw "File $Path hash $hash.Hash did not match expected hash $expectedHash" } Write-Host "Unzip CCache" Expand-Archive -Path "$zipFilePath" -DestinationPath "$env:TEMP" Write-Host "Move CCache" Move-Item -Force "$env:TEMP/$folder/ccache.exe" "$Destination" Remove-Item "$zipFilePath" Remove-Item -Recurse "$env:TEMP/$folder" } catch { Write-Host "Installation failed with an error" $_.Exception | Format-List exit -1 } colmap-4.0.4/.gitignore000077500000000000000000000010771517363634500150070ustar00rootroot00000000000000# Custom files .python-version LocalConfig.cmake src/colmap/util/version.cc CMakeUserPresets.json .clangd compile_commands.json # Custom directories .idea/ .vscode .vs .DS_Store .cache build*/ install*/ data/ benchmark/reconstruction/data/ benchmark/reconstruction/runs/ doc/_build vcpkg_installed/ # Compiled Object files *.slo *.lo *.o *.obj *.pyc # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # backup files *~ *.orig colmap-4.0.4/.gitmodules000066400000000000000000000000001517363634500151520ustar00rootroot00000000000000colmap-4.0.4/AGENTS.md000066400000000000000000000222611517363634500143150ustar00rootroot00000000000000# AGENTS.md — COLMAP Guide ## Project Overview COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline that reconstructs 3D models from 2D image collections. Written in C++17 with optional CUDA support. Single binary (colmap) with many subcommands, a Qt GUI, and Python bindings (pycolmap). ## Directory Structure | Path | Description | |------|-------------| | CMakeLists.txt | Root build config | | cmake/CMakeHelper.cmake | COLMAP_ADD_LIBRARY / _EXECUTABLE / _TEST macros | | cmake/FindDependencies.cmake | All dependency discovery (Eigen, Ceres, CUDA, Qt, etc.) | | cmake/Find*.cmake | Custom find modules | | src/colmap/ | Primary C++ source (see Architecture below) | | src/pycolmap/ | pybind11 C++ bindings | | src/thirdparty/ | Bundled (VLFeat, SiftGPU, PoissonRecon, LSD) and fetched (PoseLib, faiss, ONNX Runtime) | | python/pycolmap/ | Python package (__init__.py, utilities) | | python/CMakeLists.txt | scikit-build-core build for pycolmap | | doc/ | Sphinx/RST documentation | | docker/ | Dockerfile + build/run scripts | | scripts/format/ | c++.sh (clang-format), python.sh (ruff) | | benchmark/ | Reconstruction + runtime benchmarks | | .github/workflows/ | CI: Ubuntu, macOS, Windows, Docker, pycolmap | | vcpkg.json | vcpkg manifest (Windows/macOS deps) | | pyproject.toml | Python build config (scikit-build-core, cibuildwheel) | | .clang-format | C++ formatting style | | ruff.toml | Python linting/formatting config | ## Module Dependency Layers (bottom → top) | Module | Description | |--------|-------------| | util/ | Threading, logging, caching, PLY I/O, CUDA/OpenGL helpers | | math/ | Random, polynomials, graph algorithms (cuts, union-find, spanning trees) | | geometry/ | Rigid3d, Sim3d, essential/homography matrices, triangulation, GPS | | sensor/ | Camera distortion models, Bitmap (image I/O), Rig, sensor specs DB | | feature/ | SIFT (CPU/GPU), ALIKED (ONNX), LightGlue, descriptor indexing (FAISS) | | optim/ | RANSAC, LO-RANSAC, SPRT, samplers, support measurers | | scene/ | Camera, Image, Frame, Point2D/3D, Track, Reconstruction, Database (SQLite), CorrespondenceGraph | | estimators/ | Bundle adjustment (Ceres), absolute/relative pose, two-view geometry, triangulation, alignment | | estimators/solvers/ | Minimal solvers: P3P, 5-pt essential, 7/8-pt fundamental, homography (via PoseLib) | | estimators/cost_functions/ | Ceres cost functors: reprojection, Sampson, alignment, pose prior | | sfm/ | IncrementalMapper, GlobalMapper, IncrementalTriangulator, ObservationManager | | mvs/ | PatchMatch stereo (CUDA), depth/normal maps, fusion, meshing | | image/ | Image undistortion, warping, line detection | | retrieval/ | Vocabulary tree (VisualIndex), inverted index, vote-and-verify | | controllers/ | AutomaticReconstruction, IncrementalPipeline, GlobalPipeline, HierarchicalPipeline, OptionManager | | exe/ | CLI command implementations (colmap.cc dispatcher + per-domain .cc files) | | ui/ | Qt GUI: MainWindow, ModelViewerWidget, OpenGL painters, config dialogs | ## Key Classes & Files | Class/File | Location | Purpose | |------------|----------|---------| | Reconstruction | scene/reconstruction.h | Top-level container: cameras, rigs, images, frames, points, tracks | | Camera | scene/camera.h | Intrinsics (focal, principal pt, distortion model) | | Rig | sensor/rig.h | Multi-sensor rig with sensor_from_rig transforms | | Image | scene/image.h | Exposure: name, Point2D observations, camera_id, frame_id | | Frame | scene/frame.h | Posed rig instantiation: rig_from_world + sensor data | | Point3D | scene/point3d.h | Triangulated 3D point: xyz, color, error, Track | | Track | scene/track.h | List of (image_id, point2D_idx) observations | | Database | scene/database.h | Abstract DB interface (SQLite impl in database_sqlite.h) | | DatabaseCache | scene/database_cache.h | In-memory cache + CorrespondenceGraph | | CorrespondenceGraph | scene/correspondence_graph.h | Feature-to-feature correspondences across images | | Rigid3d | geometry/rigid3.h | 6-DOF rigid transform (quaternion + translation) | | Sim3d | geometry/sim3.h | 7-DOF similarity transform (Rigid3d + scale) | | FeatureExtractor | feature/extractor.h | Abstract extractor (SIFT, ALIKED); factory Create() | | FeatureMatcher | feature/matcher.h | Abstract matcher; supports Match() and MatchGuided() | | FeatureKeypoint | feature/types.h | x, y + affine shape (a11, a12, a21, a22) | | BundleAdjuster | estimators/bundle_adjustment.h | Abstract BA; Ceres impl via CreateDefaultBundleAdjuster() | | BundleAdjustmentConfig | estimators/bundle_adjustment.h | What to optimize vs. hold constant | | EstimateAbsolutePose() | estimators/pose.h | P3P RANSAC from 2D-3D correspondences | | EstimateTwoViewGeometry() | estimators/two_view_geometry.h | Essential/fundamental/homography estimation | | EstimateTriangulation() | estimators/triangulation.h | Robust multi-view triangulation | | IncrementalMapper | sfm/incremental_mapper.h | Core incremental SfM engine | | GlobalMapper | sfm/global_mapper.h | Global SfM (rotation averaging + global positioning) | | IncrementalTriangulator | sfm/incremental_triangulator.h | Point creation, track merging/completion | | ObservationManager | sfm/observation_manager.h | Per-image visibility stats, filtering | | PatchMatch | mvs/patch_match.h | CPU wrapper for CUDA PatchMatch stereo | | PatchMatchController | mvs/patch_match.h | Orchestrates multi-GPU depth estimation | | StereoFusion | mvs/fusion.h | Fuses depth maps into 3D point cloud | | AutomaticReconstructionController | controllers/automatic_reconstruction.h | End-to-end pipeline (extract, match, SfM, MVS) | | IncrementalPipeline | controllers/incremental_pipeline.h | Manages incremental SfM loop + multi-model | | OptionManager | controllers/option_manager.h | Centralized CLI option parsing | | Camera models | sensor/models.h | SimplePinhole, Radial, OpenCV, Fisheye, etc. | | Bitmap | sensor/bitmap.h | Image I/O wrapper (OpenImageIO), EXIF extraction | CLI entry point: exe/colmap.cc (subcommand dispatcher). ## Build Instructions ```bash mkdir build && cd build cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../install ninja # No GUI, no CUDA (minimal) cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DGUI_ENABLED=OFF -DCUDA_ENABLED=OFF # With tests cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DTESTS_ENABLED=ON ``` ### Build pycolmap First build and install the C++ code: ```bash mkdir build && cd build cmake .. -GNinja -DCMAKE_INSTALL_PREFIX=../install ninja install ``` Then build pycolmap (from the repo root): ```bash colmap_DIR=./install ./python/incremental_build.sh # Fast incremental build colmap_DIR=./install ./python/build.sh # Clean build (slower) ``` If there is a local `.python-version` file, use pyenv/uv for Python commands, `pip install`, and building pycolmap. ## Testing Follow C++ and Python build instructions above. Run ctest from the build directory: ```bash cd build ctest --output-on-failure # All C++ tests ctest -R "util/cache_test" # Specific test ctest -E "(feature/sift_test)" # Exclude GPU tests ``` Run Python tests from the repo root: ```bash pytest # All Python tests (config in pyproject.toml) ``` - Test files (`*_test.cc`) across all modules, created via `COLMAP_ADD_TEST()` macro - Framework: GTest/GMock with custom main (`util/gtest_main.cc`) - Test utilities: `util/testing.h`, Eigen matchers: `util/eigen_matchers.h`, transform matchers: `geometry/rigid3_matchers.h`, `geometry/sim3_matchers.h` - CTest names: `module/test_name` (e.g., `estimators/alignment_test`) ## Code Style & Conventions ### Naming - Classes: `PascalCase` - Methods/functions: `PascalCase` (e.g., `FindNextImages()`) - Member variables: `snake_case_` (trailing underscore) - Local variables: `snake_case` - Constants/enums: `kPascalCase` or `UPPER_SNAKE_CASE` - Files: `snake_case.h` / `snake_case.cc` / `snake_case_test.cc` - Transforms: `target_from_source` (e.g., `cam_from_world`) - Coordinates: `x_in_y` (e.g., `point3D_in_world`) ### Index and Identifier Types (util/types.h) - Generic indexes: int, size_t - Special identifiers: camera_t, image_t, image_pair_t, frame_t, rig_t, point2D_t, point3D_t, sensor_t, data_t, pose_prior_t ### Formatting ```bash scripts/format/c++.sh scripts/format/python.sh ``` ## External Dependencies ### Core | Library | Role | |---------|------| | Eigen3 | Linear algebra, matrices, geometry | | Ceres Solver | Nonlinear optimization | | Boost | Graph algorithms, CLI options, etc. | | glog | Structured logging | | SQLite3 | Feature/match database | | OpenImageIO | Image I/O and processing | | CHOLMOD | Sparse Cholesky | | Metis | Graph partitioning | | PoseLib | Minimal pose solvers | | FAISS | Fast ANN for descriptor matching | ### Optional | Library | Role | Gate | |---------|------|------| | CUDA | GPU PatchMatch, SiftGPU, Ceres GPU BA | CUDA_ENABLED | | ONNX Runtime | ALIKED, LightGlue neural features | ONNX_ENABLED | | Qt5/6 | GUI | GUI_ENABLED | | OpenGL/GLEW | 3D visualization, SiftGPU | OPENGL_ENABLED | | CGAL | Delaunay meshing | CGAL_ENABLED | ### Bundled (src/thirdparty/) | Library | Role | |---------|------| | VLFeat | CPU SIFT | | SiftGPU | GPU SIFT | | PoissonRecon | Surface reconstruction | | LSD | Line detection | colmap-4.0.4/CHANGELOG.rst000066400000000000000000005165611517363634500150460ustar00rootroot00000000000000Changelog ========= ------------------------- COLMAP 4.0.4 (04/27/2026) ------------------------- Bug Fixes --------- * Log an error instead of crash on unknown EXIF orientation * Fix crash in AdjustGlobalBundle after aggressive frame filtering * Change std::filesystem::relative to lexically_relative for NormalizePath * Fix onnxruntime DLL copy error under Windows in Findonnxruntime.cmake ------------------------- COLMAP 4.0.3 (04/06/2026) ------------------------- Bug Fixes --------- * Fix various issues in incremental mapper's reg_stats bookkepping * Fix reading of dynamic matrices in SQLite3 database * Fix optional access in guided matching * Fix conditional Eigen alignment for 3.4.0 pre-release version * Fix ceres::GradientChecker constructor for older Ceres versions * Fix for pycolmap installation related to ONNX * Fix bug where num_reg_images was not cleared * Fix mask usage log never printing in feature writer thread * Fix undefined behavior in PoissonRecon * Fix locale-dependent float parsing/formatting * Fix empty PatchMatch results on Blackwell GPUs (sm_100+) * Fix pyceres .problem attribute on CeresBundleAdjuster returned by factory functions * Fix missing rotation averaging options to global mapper * Fix piping of bundle adjustment options in global mapper Improvements ------------ * Handle CHOLMOD includes not being in a subdirectory * Use non-deprecated SQLite3::SQLite3 CMake target * Reduce thread oversubscription in hierarchical mapper ------------------------- COLMAP 4.0.2 (03/18/2026) ------------------------- Bug Fixes --------- * Fix ALIKED keypoint score filtering * Fix ALIKED extraction with pycolmap * Fix missing reset of mesh simplication options ------------------------- COLMAP 4.0.1 (03/15/2026) ------------------------- Bug Fixes --------- * Add missing LightGlue matcher type to GUI * Fix checks in GpuMat constructor * Add destructor to ModelViewerWidget to call makeCurrent() ------------------------- COLMAP 4.0.0 (03/14/2026) ------------------------- New Features ------------ * Integrated GLOMAP global SfM pipeline into COLMAP as a first-class alternative to the incremental/hierarchical mappers, available via the ``global_mapper`` and ``automatic_reconstructor --mapper GLOBAL`` commands. Many fixes and improvements have been applied to the GLOMAP codebase as part of this migration. GLOMAP is maintained through the COLMAP repository going forward. The global pipeline uses view graph calibration to estimate intrinsics from two-view geometries, which may produce different camera parameters compared to the incremental pipeline's self-calibration. * Added ALIKED (N16Rot/N32) feature extraction through ONNX support. Support for brute-force and LightGlue matching as well as scalable matching with pre-trained vocabulary trees. * Added LightGlue ONNX feature matching for SIFT and ALIKED. * Added Python bindings for feature extractor and matcher. * Added support for reading image orientation from EXIF and auto-rotating images during feature extraction/matching for better robustness against rotational viewpoint changes. * Added structure-less image registration fallback using generalized relative pose estimator for registering images without 3-view overlap. * Added division camera models (SIMPLE_DIVISION, DIVISION). * Added fisheye camera model without distortion parameters (FISHEYE). * Improved Ceres bundle adjustment performance by ~10% through single pose parameter block. * Improved Ceres bundle adjustment performance by ~15% for SIMPLE_RADIAL camera model and trivial frames through analytical Jacobians. * Added abstract general bundle adjuster and solver summary in preparation for supporting different optimization backends. * Replaced FreeImage with OpenImageIO for ~2.5x faster image I/O, support for more image formats, and using an actively maintained dependency with security fixes, etc. * Added guided geometric verification using a known reconstruction. * Added view graph calibration as a new module. * Added model clustering to partition large reconstructions into smaller, manageable sub-models based on scene connectivity. * Added mesh simplification using Quadric Error Metric (QEM) decimation. * Added mesh texture mapping for producing textured meshes from calibrated images. * Added option to specify JPEG quality during image undistortion. * Added support for loading bitmaps with alpha channel. * Added option to control refinement of 3D points in bundle adjuster. * Added optional support for position prior in absolute pose refinement. * Added supernodal CHOLMOD L1 solver support. * Added multi-threading support for (LO)RANSAC loop. * Added support for custom SSL certificate locations through SSL_CERT_FILE and SSL_CERT_DIR environment variables. * Added support for static Windows builds with CUDA. * Exposed ``pycolmap.match_from_pairs`` for custom pair matching on GPU. * Added Python bindings for depth and normal maps. * Added Python type checking (mypy) for all code. * Improved incremental mapper and triangulator Python bindings. * Added support for visualizing (textured) meshes in the GUI. * Added drag-and-drop folder support for model import in the GUI. * Added drag-and-drop support for PLY import in the GUI. * Switched to native menu bar in GUI on macOS. * Added support for building shared libraries. * Improved patch match stereo performance by reading inputs in parallel. * Reduced model viewer memory usage in the GUI. * Added automatic fallback to BA CPU solver if GPU solver fails. * For other minor improvements, see full list of changes below. Bug Fixes --------- * Fixed guided matching for calibrated non-linear cameras. * Fixed database race conditions in mapping pipelines. * Fixed MSVC compatibility with ``isnan``. * Fixed undefined behavior in feature match swapping. * Fixed SVD computation in affine transform. * Fixed 2D association issue at ``AddPoint3D`` with ``point3D_id``. * Fixed several issues in database merging. * Fixed MVS workspace image downsizing. * Fixed pycolmap to respect ``fix_existing_frames`` option in the incremental pipeline. * Fixed transcription of image IDs in reconstruction from database. * Fixed ``BundleAdjustmentConfig::NumResiduals`` ignoring point statistics. * Fixed incorrect propagation of incremental options in automatic reconstructor. * Fixed triangulation angle computation in patch match sampling. * Fixed inverted units in ``ExifFocalLength`` metadata extraction. * Fixed slow performance of generalized absolute pose estimator verification. * Propagate exceptions from ``ThreadPool::Wait``. * Fixed empty patch match results on CUDA compute >= 100 (Blackwell GPUs). * Fixed ``filter_frames_`` usage in ``FindNextImages``. * Fixed ``model_aligner`` crash by using new pose prior API. * For other bug fixes, see full list of changes below. Breaking Changes ---------------- * Replaced FreeImage with OpenImageIO for image I/O. There may be slight differences in terms of supported image formats and loaded pixel values. * Dropped support for Python 3.9 due to EOL. * ``TwoViewGeometry`` fields ``cam2_from_cam1``, ``E``, ``F``, ``H`` are now ``std::optional``. * ``Rigid3d`` and ``Sim3d`` parameters stored as single vector. Use ``rotation()`` and ``translation()`` accessor methods instead of direct member access. * Bundle adjustment cost functors use single pose parameter blocks. * ``BundleAdjuster`` now returns ``std::shared_ptr`` instead of ``ceres::SolverSummary``. * Ceres-related CLI options are prefixed with ``BundleAdjustmentCeres.*``. * GPS/ENU coordinate conversion functionality cleaned up, breaking the Python interface. * Feature descriptors are now associated with a type that is propagated through the pipeline and storage. Fails matching if descriptor types are inconsistent. * Pose priors associated with generic sensor measurement data. * ``DatabaseCache`` initialization consolidated into a single options struct instead of scattered arguments. * Switched paths from ``std::string`` to ``std::filesystem::path`` throughout the C++ codebase. Python ``pathlib.Path`` objects are now automatically converted. No breaking impact on the Python interface. Full Change List (sorted temporally) ------------------------------------ * Fix MSVC compatibility with ``isnan`` by @nharbiso in https://github.com/colmap/colmap/pull/3720 * Include v3.13 to legacy dropdown. by @B1ueber2y in https://github.com/colmap/colmap/pull/3724 * (bugfix) initializing added_two_view_geometry_options_ to false by @AlePuglisi in https://github.com/colmap/colmap/pull/3727 * Update the tag of the doc submodule to the latest commit. by @B1ueber2y in https://github.com/colmap/colmap/pull/3729 * Fix duplicated parameters for pycolmap.extract_features and fix pycolmap README. by @B1ueber2y in https://github.com/colmap/colmap/pull/3728 * Document how to speed up bundle adjustment by @StonerLing in https://github.com/colmap/colmap/pull/3730 * Fix reconstruction benchmark by @ahojnnes in https://github.com/colmap/colmap/pull/3731 * Remove doc submodule for easier maintenance. by @B1ueber2y in https://github.com/colmap/colmap/pull/3732 * Fix undefined behavior in feature match swapping by @ahojnnes in https://github.com/colmap/colmap/pull/3733 * Fix SVD computation in affine transform by @ahojnnes in https://github.com/colmap/colmap/pull/3734 * Fix random seed stability tests for incremental mapper by @ahojnnes in https://github.com/colmap/colmap/pull/3735 * Structure-less image registration using generalized relative pose estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2829 * Add supernodal cholmod L1 solver support and more tests by @ahojnnes in https://github.com/colmap/colmap/pull/3737 * Add union-find implementation by @ahojnnes in https://github.com/colmap/colmap/pull/3738 * Add unit tests for base controller by @ahojnnes in https://github.com/colmap/colmap/pull/3739 * Add unit tests for PLY utils by @ahojnnes in https://github.com/colmap/colmap/pull/3742 * Inline SQLite3 utils in database implementation by @ahojnnes in https://github.com/colmap/colmap/pull/3741 * Allow reuse of cholesky decomposition in L1 solver by @ahojnnes in https://github.com/colmap/colmap/pull/3743 * Add unit tests for controller thread by @ahojnnes in https://github.com/colmap/colmap/pull/3740 * Add drag-and-drop folder support for model import in the GUI by @StonerLing in https://github.com/colmap/colmap/pull/3744 * Update to latest vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/3745 * Include instructions to update documentation after new releases by @chipironcin in https://github.com/colmap/colmap/pull/3716 * Remove unused CMake warning level variable by @ahojnnes in https://github.com/colmap/colmap/pull/3747 * Import of GLOMAP source code by @ahojnnes in https://github.com/colmap/colmap/pull/3748 * Correct the spelling of GeometricVerifier. by @B1ueber2y in https://github.com/colmap/colmap/pull/3749 * Update Windows ccache to 4.12.1 by @ahojnnes in https://github.com/colmap/colmap/pull/3750 * Remove custom glomap constants by @ahojnnes in https://github.com/colmap/colmap/pull/3751 * Remove unnecessary glomap Camera by @ahojnnes in https://github.com/colmap/colmap/pull/3753 * Misc code quality improvements for max spanning tree by @ahojnnes in https://github.com/colmap/colmap/pull/3755 * Configure clang-format for glomap code by @ahojnnes in https://github.com/colmap/colmap/pull/3756 * Support two view geometry with empty inlier matches in the database. by @B1ueber2y in https://github.com/colmap/colmap/pull/3754 * Modularize glomap libraries by @ahojnnes in https://github.com/colmap/colmap/pull/3757 * Use colmap reconstruction matchers in global mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/3758 * Fix WriteTwoViewGeometry when inlier matches are empty. by @B1ueber2y in https://github.com/colmap/colmap/pull/3761 * Add support for geometric verification given known reconstruction. by @B1ueber2y in https://github.com/colmap/colmap/pull/3752 * Enable glomap tests in CI by @ahojnnes in https://github.com/colmap/colmap/pull/3759 * Replace custom glomap Track with colmap Point3D by @ahojnnes in https://github.com/colmap/colmap/pull/3762 * Add missing option: FeatureMatching.skip_image_pairs_in_same_frame by @B1ueber2y in https://github.com/colmap/colmap/pull/3765 * Use fundamental/essential matrix utils from colmap by @ahojnnes in https://github.com/colmap/colmap/pull/3768 * Clean up redundant/unnecessary glomap rigid3d helper functions by @ahojnnes in https://github.com/colmap/colmap/pull/3764 * Add option to skip geometric verification at feature matching. by @B1ueber2y in https://github.com/colmap/colmap/pull/3766 * Fix missing colmap namespace in util/file.h macro. by @B1ueber2y in https://github.com/colmap/colmap/pull/3770 * Add missing header for the enum macros and remove colmap namespace. by @B1ueber2y in https://github.com/colmap/colmap/pull/3771 * Upgrade to latest vcpkg. by @B1ueber2y in https://github.com/colmap/colmap/pull/3774 * Add convenience method for filtering data by sensor type by @ahojnnes in https://github.com/colmap/colmap/pull/3776 * Cleanup redundant sqlite database merge implementation, consistent variable naming by @ahojnnes in https://github.com/colmap/colmap/pull/3778 * Add missing bindings for Bitmap and cleanup by @sarlinpe in https://github.com/colmap/colmap/pull/3780 * Set frame_id when reading database images by @ahojnnes in https://github.com/colmap/colmap/pull/3779 * Handle near zero matrices or identity transformations in gmock matchers by @ahojnnes in https://github.com/colmap/colmap/pull/3782 * Fix pycolmap to respect fix_existing_frames option by @S-o-T in https://github.com/colmap/colmap/pull/3781 * Add non-inserting FindIfExists to UnionFind by @B1ueber2y in https://github.com/colmap/colmap/pull/3783 * Use existing ceres function to stringify termination type by @ahojnnes in https://github.com/colmap/colmap/pull/3784 * Add convenience Python constructors for data_t/sensor_t by @ahojnnes in https://github.com/colmap/colmap/pull/3785 * Add utility function to compute Rigid3d origin by @ahojnnes in https://github.com/colmap/colmap/pull/3773 * Allow running glomap from colmap main executable by @ahojnnes in https://github.com/colmap/colmap/pull/3769 * Associate pose priors to generic sensor measurement data by @ahojnnes in https://github.com/colmap/colmap/pull/3777 * Enable compiler warnings as errors in GCC/CLang CI builds by @ahojnnes in https://github.com/colmap/colmap/pull/3788 * Remove debug log statement in db management widget by @ahojnnes in https://github.com/colmap/colmap/pull/3789 * Rename PosePrior::IsValid to HasPosition by @ahojnnes in https://github.com/colmap/colmap/pull/3786 * Add gravity to pose prior by @ahojnnes in https://github.com/colmap/colmap/pull/3787 * Disable editing of images in database management widget by @ahojnnes in https://github.com/colmap/colmap/pull/3792 * Fix and simplify database pose prior migration by @ahojnnes in https://github.com/colmap/colmap/pull/3791 * Synthesize gravities and move pose prior noise synthesis by @ahojnnes in https://github.com/colmap/colmap/pull/3795 * Replace FreeImage with OpenImageIO by @ahojnnes in https://github.com/colmap/colmap/pull/3459 * Add support on adding camera/image with trivial rig/frame to reconstruction. by @B1ueber2y in https://github.com/colmap/colmap/pull/3800 * Rename HasTrivialFrame to IsRefInFrame. by @B1ueber2y in https://github.com/colmap/colmap/pull/3801 * Ensure that the rig has only one sensor at AddImageWithTrivialFrame. by @B1ueber2y in https://github.com/colmap/colmap/pull/3802 * Fix meta data cloning when cloning bitmap by @ahojnnes in https://github.com/colmap/colmap/pull/3812 * Remove unused includes in MVS image by @ahojnnes in https://github.com/colmap/colmap/pull/3813 * Create global mapper pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/3808 * Fix stand-alone python visualizer. by @B1ueber2y in https://github.com/colmap/colmap/pull/3816 * Remove the deprecated read_write_model.py from colmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3817 * refactor: Replace OpenSqliteDatabase with Database::Open by @whuaegeanse in https://github.com/colmap/colmap/pull/3818 * Replace glomap gravity info with pose prior by @ahojnnes in https://github.com/colmap/colmap/pull/3794 * Relax CUDA requirements in Python wheels by @sarlinpe in https://github.com/colmap/colmap/pull/3799 * Fix copying behaviors of empty bitmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3823 * Don't build CUDA wheels for all Python versions in PRs by @sarlinpe in https://github.com/colmap/colmap/pull/3824 * Fix NumResiduals ignore point statistics and update test assertion by @whuaegeanse in https://github.com/colmap/colmap/pull/3820 * Add unit tests for MVS workspace by @ahojnnes in https://github.com/colmap/colmap/pull/3825 * Fix transcription of image IDs in reconstruction from database by @ahojnnes in https://github.com/colmap/colmap/pull/3826 * Towards removing custom glomap image class by @ahojnnes in https://github.com/colmap/colmap/pull/3805 * Add unit tests for MVS image by @ahojnnes in https://github.com/colmap/colmap/pull/3828 * Remove unused includes in MVS fusion by @ahojnnes in https://github.com/colmap/colmap/pull/3829 * Add simple integration test for MVS fusion by @ahojnnes in https://github.com/colmap/colmap/pull/3830 * Remove custom glomap image class by @ahojnnes in https://github.com/colmap/colmap/pull/3835 * Add integration tests for meshing, update poisson recon library to 18.75 by @ahojnnes in https://github.com/colmap/colmap/pull/3834 * Consolidate calculation of angle between vectors by @ahojnnes in https://github.com/colmap/colmap/pull/3836 * Clean up unused includes from geometry folder by @ahojnnes in https://github.com/colmap/colmap/pull/3837 * Clean up redundant glomap executable by @ahojnnes in https://github.com/colmap/colmap/pull/3838 * Update FAQ on dense stereo stage requirements by @The-Cyber-Captain in https://github.com/colmap/colmap/pull/3840 * Improve glomap cost function, add tests by @ahojnnes in https://github.com/colmap/colmap/pull/3839 * Refactor general cost function utils and add NormalPriorCost and NormalErrorCost. by @B1ueber2y in https://github.com/colmap/colmap/pull/3843 * Various improvements and fixes on glomap gravity impl. by @B1ueber2y in https://github.com/colmap/colmap/pull/3844 * Fix misleading logging for structure-less registration. by @B1ueber2y in https://github.com/colmap/colmap/pull/3848 * Remove glomap types_sfm.h and only include necessary headers. by @B1ueber2y in https://github.com/colmap/colmap/pull/3849 * Improvements and fixes on glomap two view geometry. by @B1ueber2y in https://github.com/colmap/colmap/pull/3847 * Move cluster id logic to top level and remove custom glomap Frame class. by @B1ueber2y in https://github.com/colmap/colmap/pull/3850 * Migrate glomap gravity utilities into colmap and add tests. by @B1ueber2y in https://github.com/colmap/colmap/pull/3845 * Move InlierThresholdOptions and remove unnecessary includes. by @B1ueber2y in https://github.com/colmap/colmap/pull/3851 * Minor: throw instead of LOG(ERROR) when SQLITE3_EXEC fails. by @B1ueber2y in https://github.com/colmap/colmap/pull/3855 * Add integration tests for image undistorters by @ahojnnes in https://github.com/colmap/colmap/pull/3856 * Pass database as pointer instead of reference in global mapper. Drop GlobalMapperResume. by @B1ueber2y in https://github.com/colmap/colmap/pull/3846 * Use colmap::Reconstruction instead of scattered variables in glomap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3853 * Use temporary storage for camera centers in global positioning. by @B1ueber2y in https://github.com/colmap/colmap/pull/3854 * Fix workspace image downsizing and improve tests by @ahojnnes in https://github.com/colmap/colmap/pull/3857 * Fix bitmap copy assignment test by @ahojnnes in https://github.com/colmap/colmap/pull/3859 * Add test for BitmapColor printing by @ahojnnes in https://github.com/colmap/colmap/pull/3860 * [Improve RA] Add PairConstraint, remove image_id_to_idx and improve naming/comments. by @B1ueber2y in https://github.com/colmap/colmap/pull/3858 * Add test for spatial verification in image retrieval, fix crash for invalid transformations by @ahojnnes in https://github.com/colmap/colmap/pull/3862 * Attempt to make visual index spatial verification tests deterministic by @ahojnnes in https://github.com/colmap/colmap/pull/3870 * Minor: rename glomap cost_function.h to cost_functions.h. by @B1ueber2y in https://github.com/colmap/colmap/pull/3872 * Fix 2D association issue at AddPoint3D with point3D_id. by @B1ueber2y in https://github.com/colmap/colmap/pull/3867 * [Improve RA] Abstract RotationAveragingProblem and RotationAveragingSolver. by @B1ueber2y in https://github.com/colmap/colmap/pull/3861 * Move relative pose filtering utilities into ViewGraph. by @B1ueber2y in https://github.com/colmap/colmap/pull/3864 * Fix database race conditions in mapping pipelines by @ahojnnes in https://github.com/colmap/colmap/pull/3865 * Improve stability of visual index spatial verification test by @ahojnnes in https://github.com/colmap/colmap/pull/3874 * Fix glomap track retriangulation refinement termination status by @ahojnnes in https://github.com/colmap/colmap/pull/3873 * Propagate exceptions from ThreadPool::Wait by @ahojnnes in https://github.com/colmap/colmap/pull/3869 * Catch AggregateException for ThreadPool. by @B1ueber2y in https://github.com/colmap/colmap/pull/3875 * Use DeRegisterFrame over ResetPose in view graph and track retriangulation. by @B1ueber2y in https://github.com/colmap/colmap/pull/3877 * Add Reconstruction::IsValid to check if a reconstruction object is not broken. by @B1ueber2y in https://github.com/colmap/colmap/pull/3878 * Improve the horrendous logic and naming in rotation initializer. by @B1ueber2y in https://github.com/colmap/colmap/pull/3876 * Use colmap ObservationManager for track filtering in glomap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3871 * Centralize random logic to use colmap SetPRNGSeed. Add deterministic test. by @B1ueber2y in https://github.com/colmap/colmap/pull/3879 * Abstract general minimum spanning tree algorithm in colmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3883 * Use logging over std::cout in glomap to avoid flooding logs in gtest. by @B1ueber2y in https://github.com/colmap/colmap/pull/3880 * Abstract base option manager and consolidate usage. by @B1ueber2y in https://github.com/colmap/colmap/pull/3881 * [Improve RA] Move stratified logic into estimators and only keep one option class. by @B1ueber2y in https://github.com/colmap/colmap/pull/3863 * ImagePair inherits colmap::TwoViewGeometry. Remove image id fields. by @B1ueber2y in https://github.com/colmap/colmap/pull/3882 * Further cleanup of std::cout in the codebase. by @B1ueber2y in https://github.com/colmap/colmap/pull/3884 * Rename SwapImagePair to ShouldSwapImagePair. by @B1ueber2y in https://github.com/colmap/colmap/pull/3887 * Move validity logic into ViewGraph and use filter_view in ValidImagePairs. by @B1ueber2y in https://github.com/colmap/colmap/pull/3885 * Directly use COLMAP bundle adjustment interface for glomap BA. by @B1ueber2y in https://github.com/colmap/colmap/pull/3868 * Add rotation averaging controller in colmap and consolidate glomap exe functions. by @B1ueber2y in https://github.com/colmap/colmap/pull/3888 * Remove legacy logging in global positioning. by @B1ueber2y in https://github.com/colmap/colmap/pull/3892 * Minor: improve the namings in global mapper options. by @B1ueber2y in https://github.com/colmap/colmap/pull/3891 * Add type annotations to python example scripts by @ahojnnes in https://github.com/colmap/colmap/pull/3889 * Remove OptimizationBase and use colmap::GetEffectiveNumThreads(). by @B1ueber2y in https://github.com/colmap/colmap/pull/3890 * Simplify the logic and naming of reconstruction pruning. by @B1ueber2y in https://github.com/colmap/colmap/pull/3886 * Fix wrongly migrated cuda option in glomap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3895 * Centralize poselib type conversion. by @B1ueber2y in https://github.com/colmap/colmap/pull/3896 * Drop weight support for different relative poses in rotation averaging. by @B1ueber2y in https://github.com/colmap/colmap/pull/3897 * Simplify colmap_io in glomap thanks to the unification. by @B1ueber2y in https://github.com/colmap/colmap/pull/3900 * Reduce code duplication in pycolmap MVS bindings by @sarlinpe in https://github.com/colmap/colmap/pull/3899 * Use colmap IterativeGlobalRefinement for retriangulation and refinement in glomap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3902 * Drop support for Python 3.9 due to EOL by @ahojnnes in https://github.com/colmap/colmap/pull/3910 * Add single-thread deterministic test for global mapper and rotation averaging controller. by @B1ueber2y in https://github.com/colmap/colmap/pull/3906 * Replace the usage of inliers in ImagePair with inlier_matches. by @B1ueber2y in https://github.com/colmap/colmap/pull/3909 * Update global mapper signature to align with colmap::IncrementalMapper. by @B1ueber2y in https://github.com/colmap/colmap/pull/3904 * Implement AddAndRegisterDefaultEnumOption and propagate to codebase. by @B1ueber2y in https://github.com/colmap/colmap/pull/3903 * Cleanup unused method declaration in GlobalMapper. by @B1ueber2y in https://github.com/colmap/colmap/pull/3912 * Refactor view graph calibration. by @B1ueber2y in https://github.com/colmap/colmap/pull/3911 * Delete legacy Python scripts by @ahojnnes in https://github.com/colmap/colmap/pull/3914 * Add support for computing recall scores by @ahojnnes in https://github.com/colmap/colmap/pull/3913 * Add Python bindings for depth and normal maps, retire legacy scripts by @ahojnnes in https://github.com/colmap/colmap/pull/3916 * Update and improve formatting of pycolmap readme by @ahojnnes in https://github.com/colmap/colmap/pull/3917 * Add Python type checking for all code by @ahojnnes in https://github.com/colmap/colmap/pull/3893 * Recompute F from E in the top level of CalibrateViewGraph. by @B1ueber2y in https://github.com/colmap/colmap/pull/3915 * Make DatabaseCache::Load/Create args optional except database. by @B1ueber2y in https://github.com/colmap/colmap/pull/3919 * Use DatabaseCache to load reconstruction in glomap and remove the custom loader. by @B1ueber2y in https://github.com/colmap/colmap/pull/3918 * Bugfix for the rotation averaging CLI. by @B1ueber2y in https://github.com/colmap/colmap/pull/3927 * Drop database member in SfM controllers for better modularity and safety. by @B1ueber2y in https://github.com/colmap/colmap/pull/3926 * Add tests for reconstruction benchmark utils by @ahojnnes in https://github.com/colmap/colmap/pull/3930 * Use new optional typing by @ahojnnes in https://github.com/colmap/colmap/pull/3931 * Switch to colmap backend for two view re-estimation after view graph calibration. by @B1ueber2y in https://github.com/colmap/colmap/pull/3923 * Fix sign of GPS longitude from EXIF by @ahojnnes in https://github.com/colmap/colmap/pull/3935 * Fix camera prior loading in benchmark code. by @B1ueber2y in https://github.com/colmap/colmap/pull/3933 * Remove Database::Clone method by @ahojnnes in https://github.com/colmap/colmap/pull/3929 * Cleanup unused glomap includes by @ahojnnes in https://github.com/colmap/colmap/pull/3937 * Improved code for view graph calibration cost functions by @ahojnnes in https://github.com/colmap/colmap/pull/3936 * Move and type annotate model visualization example by @ahojnnes in https://github.com/colmap/colmap/pull/3939 * Move end to end regression test script and annotate types by @ahojnnes in https://github.com/colmap/colmap/pull/3938 * Delete more legacy Python scripts by @ahojnnes in https://github.com/colmap/colmap/pull/3940 * Move and type annotate flickr downloader by @ahojnnes in https://github.com/colmap/colmap/pull/3941 * Add option to evaluate self-calibration in reconstruction benchmark by @ahojnnes in https://github.com/colmap/colmap/pull/3942 * Use std::optional for cam2_from_cam1 in TwoViewGeometry. by @B1ueber2y in https://github.com/colmap/colmap/pull/3932 * Use custom two view options for global pipeline in automatic reconstructor. by @B1ueber2y in https://github.com/colmap/colmap/pull/3943 * Add tests for estimating multiple two view geometries and minor perf optimization by @ahojnnes in https://github.com/colmap/colmap/pull/3947 * Add tests for uncovered edge cases in polynomial solvers by @ahojnnes in https://github.com/colmap/colmap/pull/3946 * Add unit tests for export to various external reconstruction formats by @ahojnnes in https://github.com/colmap/colmap/pull/3945 * Only create QApplication or OpenGL context if needed by @ahojnnes in https://github.com/colmap/colmap/pull/3944 * Simplify CSVToVector and add tests by @ahojnnes in https://github.com/colmap/colmap/pull/3953 * Migrate glomap exe into colmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3952 * Add unit tests for colmap version helpers by @ahojnnes in https://github.com/colmap/colmap/pull/3955 * Use make_unique instead of new by @ahojnnes in https://github.com/colmap/colmap/pull/3957 * Early exit pycolmap windows build on errors by @ahojnnes in https://github.com/colmap/colmap/pull/3958 * Make view graph calibration a stand-alone estimator module on colmap::Database and add CLI. by @B1ueber2y in https://github.com/colmap/colmap/pull/3951 * Replace ImagePair with RelativePoseData and rename ViewGraph to PoseGraph. by @B1ueber2y in https://github.com/colmap/colmap/pull/3954 * Make rotation averaging work with reconstruction with frames unregistered. by @B1ueber2y in https://github.com/colmap/colmap/pull/3949 * Replace 3-level option names with 2-level names for INI compatibility. by @B1ueber2y in https://github.com/colmap/colmap/pull/3962 * Add test for reading/writing options from ini by @ahojnnes in https://github.com/colmap/colmap/pull/3963 * Add relative poses to DatabaseCache and update the signature of global mapper. by @B1ueber2y in https://github.com/colmap/colmap/pull/3960 * Move module-specific functional methods outside PoseGraph. by @B1ueber2y in https://github.com/colmap/colmap/pull/3964 * Add tests for file utils, use enum utils by @ahojnnes in https://github.com/colmap/colmap/pull/3956 * Always try to decompose relative poses at the start of global pipeline. by @B1ueber2y in https://github.com/colmap/colmap/pull/3966 * Add unit tests for incremental triangulator by @ahojnnes in https://github.com/colmap/colmap/pull/3967 * Bugfix in pose decomposition interface. Update pipeline tests. by @B1ueber2y in https://github.com/colmap/colmap/pull/3969 * Switch paths from std::string to std::filesystem::path by @sarlinpe in https://github.com/colmap/colmap/pull/3901 * Add option to synthesize sparse view graph by @ahojnnes in https://github.com/colmap/colmap/pull/3968 * Optionally include runtime benchmarks in main project by @ahojnnes in https://github.com/colmap/colmap/pull/3970 * Refactor and improve track establishment. by @B1ueber2y in https://github.com/colmap/colmap/pull/3965 * Inline track establishment logic into GlobalMapper. by @B1ueber2y in https://github.com/colmap/colmap/pull/3972 * Minor: inline gravity cost functor into gravity refinement. by @B1ueber2y in https://github.com/colmap/colmap/pull/3973 * Store two view geometries in correspondence graph by @ahojnnes in https://github.com/colmap/colmap/pull/3975 * Support custom SSL certificate locations by @ahojnnes in https://github.com/colmap/colmap/pull/3976 * Fix database merging and improve tests by @ahojnnes in https://github.com/colmap/colmap/pull/3977 * Support loading bitmaps with alpha channel by @ahojnnes in https://github.com/colmap/colmap/pull/3978 * Reorder fields of core data structs for better packing by @ahojnnes in https://github.com/colmap/colmap/pull/3979 * Add tests for reading/writing bitmaps in popular image formats. by @ahojnnes in https://github.com/colmap/colmap/pull/3980 * Fix includes in feature index/matcher by @ahojnnes in https://github.com/colmap/colmap/pull/3982 * Do not store inlier matches in pose graph by @ahojnnes in https://github.com/colmap/colmap/pull/3981 * Setup priors when loading database, filter watermarks when creating database cache from another cache by @ahojnnes in https://github.com/colmap/colmap/pull/3983 * Fix guided matching for calibrated non-linear cameras by @ahojnnes in https://github.com/colmap/colmap/pull/3986 * Clear and log OpenImageIO errors by @ahojnnes in https://github.com/colmap/colmap/pull/3987 * Misc view graph calibration and global mapper improvements by @ahojnnes in https://github.com/colmap/colmap/pull/3985 * Do not crash but return error on unknown sensor_from_rig poses in incremental mapper by @ahojnnes in https://github.com/colmap/colmap/pull/3988 * Consistent and improved logging of headings by @ahojnnes in https://github.com/colmap/colmap/pull/3991 * Fix faiss linker warning under MSVC by @ahojnnes in https://github.com/colmap/colmap/pull/3994 * Make E F H optional in TwoViewGeometry struct. by @B1ueber2y in https://github.com/colmap/colmap/pull/3989 * Update Python incremental pipeline to match C++ by @ahojnnes in https://github.com/colmap/colmap/pull/3993 * Improve Reconstruction::NumRegImages from O(N) to O(1) by @ahojnnes in https://github.com/colmap/colmap/pull/3996 * Reduce incremental mapper's minimum size for small models by @ahojnnes in https://github.com/colmap/colmap/pull/3992 * Switch to Python 3.11 in the PR build. Disable mypy for Python 3.10. by @B1ueber2y in https://github.com/colmap/colmap/pull/3998 * Add missing Python bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3997 * Do not override test commands for Python 3.10 on windows. by @B1ueber2y in https://github.com/colmap/colmap/pull/4000 * Rename num_iterations_ba to ba_num_iterations for consistency by @ahojnnes in https://github.com/colmap/colmap/pull/4001 * Return std::optional from Bitmap::Exif* methods by @ahojnnes in https://github.com/colmap/colmap/pull/4002 * Use database in rotation averaging CLI. Add a python example for legacy format. by @B1ueber2y in https://github.com/colmap/colmap/pull/3990 * Cleanup some includes in sfm folder by @ahojnnes in https://github.com/colmap/colmap/pull/4005 * Switch paths from std::string to std::filesystem::path (continued) by @sarlinpe in https://github.com/colmap/colmap/pull/4004 * Remove dangling declaration of JoinPaths. by @B1ueber2y in https://github.com/colmap/colmap/pull/4009 * Make reconstruction clustering logic a stand-alone module by @lpanaf in https://github.com/colmap/colmap/pull/4008 * Switch to perform clustering on all registered frames by @lpanaf in https://github.com/colmap/colmap/pull/4007 * Add option to control refinement of points 3D in bundle adjuster by @ahojnnes in https://github.com/colmap/colmap/pull/4012 * Minor code improvement in triangulation estimator by @ahojnnes in https://github.com/colmap/colmap/pull/4015 * Add support for division camera models by @ahojnnes in https://github.com/colmap/colmap/pull/4013 * Add nominal tests on reconstruction accuracy for bundle adjustment. by @B1ueber2y in https://github.com/colmap/colmap/pull/4019 * Use colmap::BundleAdjustmentOptions in global pipeline and drop custom option class. by @B1ueber2y in https://github.com/colmap/colmap/pull/4016 * Add test for and fix bitmap jpeg quality setting by @ahojnnes in https://github.com/colmap/colmap/pull/4025 * Remove unimplemented GetFileSize function by @ahojnnes in https://github.com/colmap/colmap/pull/4026 * Drop experimental modes in global positioning. Add module signature and improve naming. by @B1ueber2y in https://github.com/colmap/colmap/pull/3920 * Add option to specify jpeg quality during undistortion by @ahojnnes in https://github.com/colmap/colmap/pull/4027 * Analytical Jacobians for reprojection error cost function with SimpleRadial cameras by @ahojnnes in https://github.com/colmap/colmap/pull/4017 * Fix backwards compatibility of reading database created before Jul 2023. by @B1ueber2y in https://github.com/colmap/colmap/pull/4030 * Bug fix for generalized absolute pose estimator. by @B1ueber2y in https://github.com/colmap/colmap/pull/4031 * Clear model outputs in all estimators and RANSACs by @ahojnnes in https://github.com/colmap/colmap/pull/4032 * Move undistortion controllers to controllers folder by @ahojnnes in https://github.com/colmap/colmap/pull/4033 * Remove the iterative logic for reconstruction clustering and use directly the strongly connected component by @lpanaf in https://github.com/colmap/colmap/pull/4035 * Add rig scale alignment support for the global pipeline. by @B1ueber2y in https://github.com/colmap/colmap/pull/4038 * Performance improvement on generalized absolute pose. by @B1ueber2y in https://github.com/colmap/colmap/pull/4037 * Bundle adjustment benchmark by @ahojnnes in https://github.com/colmap/colmap/pull/4022 * Add fisheye (equidistant) camera model by @lpanaf in https://github.com/colmap/colmap/pull/4039 * Store Rigid3/Sim3 params as single vector by @ahojnnes in https://github.com/colmap/colmap/pull/4041 * Move minimal solvers in estimators to sub-folder by @ahojnnes in https://github.com/colmap/colmap/pull/4047 * Move generalized absolute pose solver to sub-folder by @ahojnnes in https://github.com/colmap/colmap/pull/4048 * Use static Eigen array segment sizes in rotation averaging by @ahojnnes in https://github.com/colmap/colmap/pull/4049 * Single pose parameter block and angle axis for BA cost functions by @ahojnnes in https://github.com/colmap/colmap/pull/4029 * Automatically deep copy feature extraction/matching options by @ahojnnes in https://github.com/colmap/colmap/pull/4051 * Add test for rig verification and fixes by @ahojnnes in https://github.com/colmap/colmap/pull/4052 * Improve test coverage for image reader by @ahojnnes in https://github.com/colmap/colmap/pull/4053 * Abstract general bundle adjuster and solver summary. by @B1ueber2y in https://github.com/colmap/colmap/pull/4042 * Change test target names and file names by @ahojnnes in https://github.com/colmap/colmap/pull/4034 * Add nominal test and runtime benchmarking for global positioning. by @B1ueber2y in https://github.com/colmap/colmap/pull/4050 * Modularize cost functions by @ahojnnes in https://github.com/colmap/colmap/pull/4054 * Fix incorrect macOS install instruction by @sarlinpe in https://github.com/colmap/colmap/pull/4055 * Move glomap sources to colmap and replace glomap with colmap namespace by @ahojnnes in https://github.com/colmap/colmap/pull/4057 * Move motion averaging cost functions to sub-folder by @ahojnnes in https://github.com/colmap/colmap/pull/4058 * Move glomap Python bindings to colmap folder by @ahojnnes in https://github.com/colmap/colmap/pull/4059 * Add GLOMAP paper to list of citations by @ahojnnes in https://github.com/colmap/colmap/pull/4060 * Add test for reconstruction_clustering and sort cluster id by cluster size by @lpanaf in https://github.com/colmap/colmap/pull/4003 * Improve Fetzer cost implementation and tests by @ahojnnes in https://github.com/colmap/colmap/pull/4061 * Remove unused GetFileSize function by @ahojnnes in https://github.com/colmap/colmap/pull/4064 * Add function to read PLY mesh and add roundtrip tests by @ahojnnes in https://github.com/colmap/colmap/pull/4065 * Remove unused GLError function by @ahojnnes in https://github.com/colmap/colmap/pull/4066 * Modernize code using structured bindings by @ahojnnes in https://github.com/colmap/colmap/pull/4067 * Reuse sorting of initial images by @ahojnnes in https://github.com/colmap/colmap/pull/4069 * Clean up messy GPS conversion functionality by @ahojnnes in https://github.com/colmap/colmap/pull/4063 * Reuse code in camera by @ahojnnes in https://github.com/colmap/colmap/pull/4068 * Support static windows build with CUDA by @ahojnnes in https://github.com/colmap/colmap/pull/4072 * Use enum instead of #define in model viewer widget by @ahojnnes in https://github.com/colmap/colmap/pull/4073 * Extract some algorithmic logic from exe and add tests by @ahojnnes in https://github.com/colmap/colmap/pull/4074 * Cleanup outdated pycolmap deprecations by @ahojnnes in https://github.com/colmap/colmap/pull/4081 * Associate feature descriptors with type and propagate through pipeline and storage by @ahojnnes in https://github.com/colmap/colmap/pull/4080 * Fix pycolmap compiler warnings by @ahojnnes in https://github.com/colmap/colmap/pull/4082 * Update clang-format to 21.1.8 by @ahojnnes in https://github.com/colmap/colmap/pull/4085 * Update project dependencies by @ahojnnes in https://github.com/colmap/colmap/pull/4086 * Move instructions to build documentation to install.rst instead of root README by @ahojnnes in https://github.com/colmap/colmap/pull/4091 * Fix doc ordering for sphinx==0.9.1 by @sarlinpe in https://github.com/colmap/colmap/pull/4092 * Update to latest vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/4075 * Extract loading of random descriptors and sample randomly across images by @ahojnnes in https://github.com/colmap/colmap/pull/4096 * Fix duplicate word typos in observation manager and rig docstring. by @B1ueber2y in https://github.com/colmap/colmap/pull/4097 * Add LightGlue ONNX feature matching by @ahojnnes in https://github.com/colmap/colmap/pull/4098 * Improve incremental mapper/triangulator bindings by @ahojnnes in https://github.com/colmap/colmap/pull/4101 * Use native menu bar on Mac by @ahojnnes in https://github.com/colmap/colmap/pull/4102 * Avoid unnecessary copies by @ahojnnes in https://github.com/colmap/colmap/pull/4103 * Expose pycolmap.match_from_pairs to support custom pair matching on GPU by @samuelm2 in https://github.com/colmap/colmap/pull/4056 * Update onnxruntime FetchContent for ARM64 support by @johnnynunez in https://github.com/colmap/colmap/pull/4106 * Early exit incremental initialization if there are no points by @ahojnnes in https://github.com/colmap/colmap/pull/4108 * Fix incorrect propagation of incremental options in automatic reconstructor by @sarlinpe in https://github.com/colmap/colmap/pull/4111 * Fix triangulation angle computation in patch match sampling by @ahojnnes in https://github.com/colmap/colmap/pull/4110 * Correctly set the rpath in CMake by @sarlinpe in https://github.com/colmap/colmap/pull/4114 * Fix inverted units in ExifFocalLength metadata extraction by @MAX4ARCH in https://github.com/colmap/colmap/pull/4113 * Add vocab tree for ALIKED N16 rot feature by @ahojnnes in https://github.com/colmap/colmap/pull/4119 * Add track_length option in synthetic tooling and report ms/iter in BA benchmark. by @B1ueber2y in https://github.com/colmap/colmap/pull/4118 * Fix install destination for onnxruntime by @ahojnnes in https://github.com/colmap/colmap/pull/4120 * Read image orientation from EXIF and extract upright features by @sarlinpe in https://github.com/colmap/colmap/pull/4122 * Fix and improve documentation by @ahojnnes in https://github.com/colmap/colmap/pull/4123 * Optimize includes in MVS folder by @ahojnnes in https://github.com/colmap/colmap/pull/4125 * Follow-up fixes on EXIF auto-rotation by @sarlinpe in https://github.com/colmap/colmap/pull/4126 * Replace typedefs with using by @ahojnnes in https://github.com/colmap/colmap/pull/4128 * Add optional position prior support to RefineGeneralizedAbsolutePose by @ahojnnes in https://github.com/colmap/colmap/pull/4129 * Add Python bindings for feature extractor by @ahojnnes in https://github.com/colmap/colmap/pull/4130 * Add Python bindings for feature matcher by @ahojnnes in https://github.com/colmap/colmap/pull/4131 * Rotate LightGlue inputs based on gravity by @sarlinpe in https://github.com/colmap/colmap/pull/4132 * Fixing OpenGL profiles on Linux (Ubuntu 25.10) by @martin-pr in https://github.com/colmap/colmap/pull/4133 * Add pre-trained vocabulary tree for ALIKED N32 variant by @ahojnnes in https://github.com/colmap/colmap/pull/4134 * Untie circular dependency between feature and scene libraries by @ahojnnes in https://github.com/colmap/colmap/pull/4135 * Add tests for matcher cache by @ahojnnes in https://github.com/colmap/colmap/pull/4136 * Explicitly accept dragMoveEvent to fix broken drag-and-drop in Qt6 by @StonerLing in https://github.com/colmap/colmap/pull/4139 * Parse unregistered config options with warning instead of error by @StonerLing in https://github.com/colmap/colmap/pull/4140 * Revamp logging configuration and fix UI log sink formatting by @StonerLing in https://github.com/colmap/colmap/pull/4141 * Make view graph calibration a standalone module by @ahojnnes in https://github.com/colmap/colmap/pull/4143 * Refactor ONNX runtime configuration handling in CMake files by @whuaegeanse in https://github.com/colmap/colmap/pull/4144 * Relative pose decomposition operates in-memory on database cache by @ahojnnes in https://github.com/colmap/colmap/pull/4146 * Enforce compile-time type checking for option registration by @StonerLing in https://github.com/colmap/colmap/pull/4148 * Relax assertion for the global bundle adjustment by @lpanaf in https://github.com/colmap/colmap/pull/4149 * Expose more logging options in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/4150 * Load keypoints for all images to database cache for triangulation by @B1ueber2y in https://github.com/colmap/colmap/pull/4151 * Add separate trial counter for structure-less registration by @B1ueber2y in https://github.com/colmap/colmap/pull/4152 * Fix the usage of filter_frames_ in FindNextImages by @B1ueber2y in https://github.com/colmap/colmap/pull/4153 * Read inputs in parallel in patch match by @ahojnnes in https://github.com/colmap/colmap/pull/4154 * Improved caching for faster and more reliable CI by @ahojnnes in https://github.com/colmap/colmap/pull/4156 * Add retries for more reliable file download by @ahojnnes in https://github.com/colmap/colmap/pull/4157 * Add support for building shared libraries by @ahojnnes in https://github.com/colmap/colmap/pull/4158 * Fix missing colmap namespace in file macro by @B1ueber2y in https://github.com/colmap/colmap/pull/4159 * Fix file THROW_CHECK_* macros from double evaluation of path expressions by @ahojnnes in https://github.com/colmap/colmap/pull/4160 * Fix empty patch match results on CUDA compute >= 100 (Blackwell GPUs) by @ahojnnes in https://github.com/colmap/colmap/pull/4161 * Add AGENTS.md file by @ahojnnes in https://github.com/colmap/colmap/pull/4162 * Fix docker action caching by @ahojnnes in https://github.com/colmap/colmap/pull/4163 * Add compiler caching for CUDA code by @ahojnnes in https://github.com/colmap/colmap/pull/4164 * Improve ONNX Runtime error handling and CUDA compatibility warnings by @StonerLing in https://github.com/colmap/colmap/pull/4167 * Add structureless fallback options to reconstruction widget by @ahojnnes in https://github.com/colmap/colmap/pull/4168 * Add multi-threading support for (LO)RANSAC loop by @ahojnnes in https://github.com/colmap/colmap/pull/4169 * Add missing estimate sub-folder sources to cmake project by @ahojnnes in https://github.com/colmap/colmap/pull/4171 * Improve bundle adjustment test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4172 * Improve test coverage for coordinate frame estimators by @ahojnnes in https://github.com/colmap/colmap/pull/4173 * Improve test coverage for MVS consistency graph by @ahojnnes in https://github.com/colmap/colmap/pull/4174 * Add tests for SPRT sampler by @ahojnnes in https://github.com/colmap/colmap/pull/4175 * Improve reconstruction test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4176 * Add tests for MVS model by @ahojnnes in https://github.com/colmap/colmap/pull/4177 * Add tests for incremental mapper by @ahojnnes in https://github.com/colmap/colmap/pull/4178 * Add tests for feature matching utils by @ahojnnes in https://github.com/colmap/colmap/pull/4179 * Expand meshing test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4180 * Expand pose_graph_test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4181 * Expand extractor_test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4182 * Expand matcher_test coverage by @ahojnnes in https://github.com/colmap/colmap/pull/4183 * Add more log options in GUI by @StonerLing in https://github.com/colmap/colmap/pull/4186 * Add test for EstimateManhattanWorldFrame by @ahojnnes in https://github.com/colmap/colmap/pull/4187 * Fix ONNX model path handling for Windows by converting to UTF-8 by @whuaegeanse in https://github.com/colmap/colmap/pull/4188 * Add tests for rotation averaging edge cases by @ahojnnes in https://github.com/colmap/colmap/pull/4190 * Add mesh texture mapping by @ahojnnes in https://github.com/colmap/colmap/pull/4202 * Update install instructions for conda to mention Mamba by @ahojnnes in https://github.com/colmap/colmap/pull/4204 * Homogenize undistorter options by @ahojnnes in https://github.com/colmap/colmap/pull/4205 * Add support for importing surface mesh in viewer by @ahojnnes in https://github.com/colmap/colmap/pull/4207 * Save memory by storing colors as uint8 instead of float32 by @ahojnnes in https://github.com/colmap/colmap/pull/4210 * Configure Delaunay meshing num_threads in automatic reconstruction, more logging by @ahojnnes in https://github.com/colmap/colmap/pull/4211 * Log image reader non-success status as warning by @ahojnnes in https://github.com/colmap/colmap/pull/4212 * Support drag-and-drop for PLY point cloud / surface mesh in viewer by @ahojnnes in https://github.com/colmap/colmap/pull/4214 * Fix broken panorama_sfm.py by @sarlinpe in https://github.com/colmap/colmap/pull/4215 * Add implementation of mesh simplification by @ahojnnes in https://github.com/colmap/colmap/pull/4216 * Tests for filtering logic for incremental mapping by @winterclincke in https://github.com/colmap/colmap/pull/4219 * Deduplicate SIFT tests by @ahojnnes in https://github.com/colmap/colmap/pull/4220 * Fix ONNX installation on Linux for lib64 target by @ahojnnes in https://github.com/colmap/colmap/pull/4221 * Misc safety improvements by @ahojnnes in https://github.com/colmap/colmap/pull/4222 * Improved test coverage for bitmap by @ahojnnes in https://github.com/colmap/colmap/pull/4224 * Fix 180-degree flipped solutions in gravity-aligned rotation averaging by @ahojnnes in https://github.com/colmap/colmap/pull/4225 * Improve docs regarding view graph calibrator in global pipeline by @B1ueber2y in https://github.com/colmap/colmap/pull/4226 * Remove legacy cluster_ids in the global pipeline by @B1ueber2y in https://github.com/colmap/colmap/pull/4227 * Fallback to BA CPU solver if GPU failed by @ahojnnes in https://github.com/colmap/colmap/pull/4230 * Log out BA failures as errors by @ahojnnes in https://github.com/colmap/colmap/pull/4231 * Fix dangling pointers in OptionManager after ResetOptions by @B1ueber2y in https://github.com/colmap/colmap/pull/4232 * Fix model_aligner crash by using new pose prior API by @B1ueber2y in https://github.com/colmap/colmap/pull/4233 * Prevent nested threading for ONNX by @ahojnnes in https://github.com/colmap/colmap/pull/4235 * Scale max_image_size proportionally by feature extractor type in quality presets by @ahojnnes in https://github.com/colmap/colmap/pull/4236 * Rename reconstruction clusterer command to model clusterer by @ahojnnes in https://github.com/colmap/colmap/pull/4237 * Fix CPU parallelization for ALIKED by @ahojnnes in https://github.com/colmap/colmap/pull/4238 * feat: Added global mapping bindings by @TannerGilbert in https://github.com/colmap/colmap/pull/4228 -------------------------- COLMAP 3.13.0 (11/07/2025) -------------------------- New Features ------------ * Improved human-readable consistency checks when configuring a reconstruction with rigs, cameras, frames, and images. * Improved multi-GPU feature extraction & matching performance by avoiding global mutex. * Improved robustness of pose prior mapper against outlier priors. * Improved the performance of reconstruction initialization through parallelization. * Added CUDA-enabled pycolmap package for Linux and automatic publishing to PyPI. * Make the reconstruction process fully deterministic with a new random_seed parameter. * Added support for filtering stationary points in two-view geometry estimation. * Added option to perform geometric verification with rig constraints. * Added option to skip matching for image pairs within the same frame. * Added option to keep specific cameras or rigs constant during the reconstruction. * Added option to clean two-view geometries in database_cleaner. * Added option to specify timeout for incremental mapper. * Added an abstract database interface for easier integration with other database backends. * Added experimental support for feature sub-selection for global BA. * Added testing tools for synthetic reconstruction noise and image generation. * Added official support for Python 3.14. * Added support for Qt6 while still fully supporting Qt5. Bug Fixes --------- * Fixed various issues with new rig support. * Fixed rare deadlocks in feature matching. * Support for UTF-8 database paths in Windows. * Fixed bundle adjustment performance regression due to changed Gauge behavior. * Removed custom manifold from colmap to avoid issues with pyceres. * Fixed rare crash of GUI when clearing the model viewer. * Fixed focal length extraction from 35mm equivalent EXIF information. * Fixed coordinate bug in ComputeEqualPartsBboxes. Breaking Changes ---------------- * Dropped official support for Python 3.8. * Dropped official support for MacOS x86. * Upgraded to pybind11 3.X. * Removed deprecated rig bundle adjuster command in favor of rig configurator and default bundle adjuster supporting the same functionality. * Allow one-to-many matches in correspondence graph. Previously, only one-to-one matches were added to the correspondence graph and therefore other matches were not used during reconstruction. Full Change List (sorted temporally) ------------------------------------ * Autoselect compatible nvidia for docker by @MasahiroOgawa in https://github.com/colmap/colmap/pull/3454 * Add 3.12.1 changelog to main by @ahojnnes in https://github.com/colmap/colmap/pull/3461 * Add min_num_neighbors constraint for spatial matching by @StonerLing in https://github.com/colmap/colmap/pull/3463 * Refactor feature extraction/matching to support other features by @ahojnnes in https://github.com/colmap/colmap/pull/3465 * Define VisualIndex::Read as static in python bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3467 * Only find C/CXX OpenMP components by @ahojnnes in https://github.com/colmap/colmap/pull/3469 * Remove unnecessary GPU checks in pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/3472 * Fix a bug affecting feature matching on GPU by @sarlinpe in https://github.com/colmap/colmap/pull/3473 * Add command line option for `TwoViewGeometry.detect_watermark` by @gareth-cross in https://github.com/colmap/colmap/pull/3476 * Add and test GetRelativePath by @sarlinpe in https://github.com/colmap/colmap/pull/3475 * Update GetPathBaseName to rely on std::filesystem by @sarlinpe in https://github.com/colmap/colmap/pull/3481 * Fix potential deadlock in job queue by @huluoboge in https://github.com/colmap/colmap/pull/3480 * Update FindMetis.cmake to also link GK_LIBRARIES by @yeicor in https://github.com/colmap/colmap/pull/3470 * Remove check in database_cache.cc that breaks backwards compatibility with image_list_file_path by @pd-karoly-harsanyi in https://github.com/colmap/colmap/pull/3478 * Fix docker run script for GUI by @MasahiroOgawa in https://github.com/colmap/colmap/pull/3483 * Ensure UTF-8 encoding for database paths passed to SQLite open by @StonerLing in https://github.com/colmap/colmap/pull/3482 * Add changelog for 3.12.2 release by @ahojnnes in https://github.com/colmap/colmap/pull/3488 * Add changelog for 3.12.3 release by @ahojnnes in https://github.com/colmap/colmap/pull/3490 * Fix BundleAdjustmentConfig::SetConstantRigFromWorldPose by @whuaegeanse in https://github.com/colmap/colmap/pull/3501 * Add random_seed option to RANSACOptions for reproducible TwoViewGeometry estimation by @StonerLing in https://github.com/colmap/colmap/pull/3492 * Configure trivial rigs for unconfigured images by @ahojnnes in https://github.com/colmap/colmap/pull/3497 * Fix pycolmap test command by @ahojnnes in https://github.com/colmap/colmap/pull/3506 * Pano SFM example improvements and fixes by @ahojnnes in https://github.com/colmap/colmap/pull/3503 * Support Python 3.14 and retire 3.8 by @ahojnnes in https://github.com/colmap/colmap/pull/3518 * Upgrade ruff and automatically apply fixes by @ahojnnes in https://github.com/colmap/colmap/pull/3515 * Update point3D errors after bundle adjustment or SfM by @whuaegeanse in https://github.com/colmap/colmap/pull/3500 * Add option to filter stationary points in two-view geometry estimation by @ahojnnes in https://github.com/colmap/colmap/pull/3521 * Add unit test for watermark detection by @ahojnnes in https://github.com/colmap/colmap/pull/3524 * Fix comment for FilterStationaryMatches by @sarlinpe in https://github.com/colmap/colmap/pull/3525 * Ignore compile_commands.json in git by @ahojnnes in https://github.com/colmap/colmap/pull/3526 * Fix bundle adjustment performance regression due to changed Gauge by @ahojnnes in https://github.com/colmap/colmap/pull/3527 * Bind CUDA utils in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3532 * Add explicit tests for fixing gauge freedom and bugfix by @ahojnnes in https://github.com/colmap/colmap/pull/3533 * Fix format specifier overflow in WriteSnapshot() timestamp by @StonerLing in https://github.com/colmap/colmap/pull/3534 * Change input in cost function to log scale and remove custom manifold from colmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3538 * Throw exception when image.SetFramePtr() sets a frame without the image listed as its data. by @B1ueber2y in https://github.com/colmap/colmap/pull/3540 * Avoid frame having data from a sensor that does not exist in its associated rig. by @B1ueber2y in https://github.com/colmap/colmap/pull/3543 * Revert scale changes for metric reconstruction in rig sfm. by @B1ueber2y in https://github.com/colmap/colmap/pull/3530 * Fix comment for C++20 support. by @B1ueber2y in https://github.com/colmap/colmap/pull/3547 * Clean up unused includes in scene folder by @ahojnnes in https://github.com/colmap/colmap/pull/3548 * Add random_seed to IncrementalMapper to control all RANSAC seeds for improved SfM reproducibility by @StonerLing in https://github.com/colmap/colmap/pull/3544 * Throw exception at counting registered images when an image in the frame does not exist in the reconstruction. by @B1ueber2y in https://github.com/colmap/colmap/pull/3546 * Clean up unused includes in retrieval folder by @ahojnnes in https://github.com/colmap/colmap/pull/3549 * Clean up unused includes in optim folder by @ahojnnes in https://github.com/colmap/colmap/pull/3550 * Fix ModelViewerWidget::ClearReconstruction by @whuaegeanse in https://github.com/colmap/colmap/pull/3552 * Fix build with Boost 1.89.0 by @cho-m in https://github.com/colmap/colmap/pull/3553 * Clean up unused includes in util folder by @ahojnnes in https://github.com/colmap/colmap/pull/3551 * Retire Mac x86 support by @ahojnnes in https://github.com/colmap/colmap/pull/3555 * Update database bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3556 * Throw exception for Frame::SetRigId when rig pointer is available. by @B1ueber2y in https://github.com/colmap/colmap/pull/3558 * Fix rig configuration with differing IDs between database and reconstruction by @ahojnnes in https://github.com/colmap/colmap/pull/3557 * Removing the Global Lock in SiftGPUFeatureMatcher for CUDA backend by @yimingc in https://github.com/colmap/colmap/pull/3561 * Add option to skip matching for image pairs in same frame by @ahojnnes in https://github.com/colmap/colmap/pull/3563 * Add option to keep specific cameras constant by @ahojnnes in https://github.com/colmap/colmap/pull/3565 * Throw exception if any camera in the rig does not exist in the reconstruction. by @B1ueber2y in https://github.com/colmap/colmap/pull/3564 * Clean up unused includes in controllers folder by @ahojnnes in https://github.com/colmap/colmap/pull/3566 * Extract image pair conversion functions into type utils by @ahojnnes in https://github.com/colmap/colmap/pull/3568 * Clean up unused includes in estimators folder by @ahojnnes in https://github.com/colmap/colmap/pull/3567 * Expect error threshold for generalized relative pose in pixel space by @ahojnnes in https://github.com/colmap/colmap/pull/3571 * Add function to read number of matches by @ahojnnes in https://github.com/colmap/colmap/pull/3572 * Add a test to ensure THROW_CHECK conditions are evaluated exactly once by @ahojnnes in https://github.com/colmap/colmap/pull/3573 * Simplify and reuse feature matcher thread creation by @ahojnnes in https://github.com/colmap/colmap/pull/3575 * Increase exhaustive matching cache size by @ahojnnes in https://github.com/colmap/colmap/pull/3576 * Add option to clean two-view geometries by @ahojnnes in https://github.com/colmap/colmap/pull/3577 * Decouple feature matching and two-view geometry options by @ahojnnes in https://github.com/colmap/colmap/pull/3578 * Display CLI floating-point defaults with 3 significant digits by @StonerLing in https://github.com/colmap/colmap/pull/3579 * Make use_log_scale an option for Point3DAlignmentCost. by @B1ueber2y in https://github.com/colmap/colmap/pull/3574 * Cache max number of keypoints by @ahojnnes in https://github.com/colmap/colmap/pull/3583 * Add tool for geometric verification by @ahojnnes in https://github.com/colmap/colmap/pull/3581 * Minor perf and code quality improvements for visual inverted index by @ahojnnes in https://github.com/colmap/colmap/pull/3584 * Upgrade pybind11 to 3.0.0 by @ahojnnes in https://github.com/colmap/colmap/pull/3523 * Fix missing colmap namespace in sqlite3 macro. by @B1ueber2y in https://github.com/colmap/colmap/pull/3587 * Use pybind smart holder for all classes by @ahojnnes in https://github.com/colmap/colmap/pull/3542 * Add option to perform geometric verification with rig constraints by @ahojnnes in https://github.com/colmap/colmap/pull/3498 * Use Qt QSettings to remember last-used paths across file dialogs by @MotivaCG in https://github.com/colmap/colmap/pull/3585 * Fix MaybeLoadImages by @whuaegeanse in https://github.com/colmap/colmap/pull/3586 * Sync 3.12 changelog changes by @ahojnnes in https://github.com/colmap/colmap/pull/3591 * Document rig-related feature matching options and change rig verification defaults by @ahojnnes in https://github.com/colmap/colmap/pull/3592 * Abstract database interface and sqlite implementation by @ahojnnes in https://github.com/colmap/colmap/pull/3541 * Share feature extraction max_image_size option by @ahojnnes in https://github.com/colmap/colmap/pull/3600 * Support for reading RGB or grayscale images for feature extraction by @ahojnnes in https://github.com/colmap/colmap/pull/3603 * Support both Qt5 and Qt6 by @zhouzq-thu in https://github.com/colmap/colmap/pull/3597 * Add option to fix individual rigs by @ahojnnes in https://github.com/colmap/colmap/pull/3604 * Ensure download support on Ubuntu by installing libssl-dev for crypto by @ahojnnes in https://github.com/colmap/colmap/pull/3607 * Update pybind11 to 3.0.1 by @ahojnnes in https://github.com/colmap/colmap/pull/3609 * Fix focal length extraction from 35mm equivalent by @ahojnnes in https://github.com/colmap/colmap/pull/3617 * Update to Windows 2025 runners and pin pycolmap image versions by @ahojnnes in https://github.com/colmap/colmap/pull/3618 * Update to latest vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/3619 * Add bindings for mvs::model for covisibility support in pycolmap by @B1ueber2y in https://github.com/colmap/colmap/pull/3621 * Fix inconsistent pycolmap naming for RegisterFrame and DeRegisterFrame. by @B1ueber2y in https://github.com/colmap/colmap/pull/3623 * Add Conda package installation instructions by @Tobias-Fischer in https://github.com/colmap/colmap/pull/3624 * Call Retriangulate irrespective of the logging level by @sarlinpe in https://github.com/colmap/colmap/pull/3626 * Add missing rig and frame interfaces for pycolmap database. by @B1ueber2y in https://github.com/colmap/colmap/pull/3629 * Throw exception when the rig configurations of the database and existing reconstruction are inconsistent. by @B1ueber2y in https://github.com/colmap/colmap/pull/3628 * Improve the Gauge logic for fixing two views. by @B1ueber2y in https://github.com/colmap/colmap/pull/3627 * Add changelog for 3.12.6 release. by @B1ueber2y in https://github.com/colmap/colmap/pull/3632 * Ensure min/max_focal_length_ratio and max_extra_param are piped to triangulation by @ahojnnes in https://github.com/colmap/colmap/pull/3637 * Fix broken logic of removing cameras at Reconstruction::TearDown(). by @B1ueber2y in https://github.com/colmap/colmap/pull/3634 * Minor: resolve -Wsign-compare warning for min_num_inliers. by @B1ueber2y in https://github.com/colmap/colmap/pull/3639 * Remove redundant binding of feature module in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3640 * Remove plyfile python package copy due to GNU license by @ahojnnes in https://github.com/colmap/colmap/pull/3644 * rig_from_world and sensor_from_rig in pycolmap should return reference rather than copy. by @B1ueber2y in https://github.com/colmap/colmap/pull/3645 * Also replace mask file extension to .png instead of appending by @Dawars in https://github.com/colmap/colmap/pull/3611 * Fix crash when built without GPU support by @sarlinpe in https://github.com/colmap/colmap/pull/3649 * Replace custom filtering with image filter view by @ahojnnes in https://github.com/colmap/colmap/pull/3651 * Expose image_ids filter view in frame in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3652 * Simplify and fix updating cameras with priors in benchmarking by @ahojnnes in https://github.com/colmap/colmap/pull/3653 * Fix typo in patch match options documentation by @ahojnnes in https://github.com/colmap/colmap/pull/3655 * Consistently name local BA options and deduplicate local_ba_num_images by @ahojnnes in https://github.com/colmap/colmap/pull/3657 * Adjust reconstruction consistency checks by @ahojnnes in https://github.com/colmap/colmap/pull/3658 * Improve the doc for loop_detection_period. by @sarlinpe in https://github.com/colmap/colmap/pull/3661 * Expose comparison operators in Python bindings by @jhacsonmeza in https://github.com/colmap/colmap/pull/3663 * Feature sub-selection for global BA by @ahojnnes in https://github.com/colmap/colmap/pull/3650 * Compute alignment RANSAC max_error from RMS stddev and chi-square by @StonerLing in https://github.com/colmap/colmap/pull/3664 * Address clang-tidy 21 errors by @ahojnnes in https://github.com/colmap/colmap/pull/3668 * Extract synthetic reconstruction noise functionality into separate utility by @ahojnnes in https://github.com/colmap/colmap/pull/3670 * Fix copy assignment of image by @ahojnnes in https://github.com/colmap/colmap/pull/3671 * Add gmock matcher for checking approximate reconstruction equality by @ahojnnes in https://github.com/colmap/colmap/pull/3672 * Add gmock matcher for checking reconstruction equality by @ahojnnes in https://github.com/colmap/colmap/pull/3673 * Include cassert by @FlexW in https://github.com/colmap/colmap/pull/3674 * Parallelize incremental mapper initialization by @ahojnnes in https://github.com/colmap/colmap/pull/3675 * Add timeout option for incremental mapper by @ahojnnes in https://github.com/colmap/colmap/pull/3676 * Implement equals operator for FeatureKeypoint/Match by @ahojnnes in https://github.com/colmap/colmap/pull/3678 * Add method to update keypoints in database by @ahojnnes in https://github.com/colmap/colmap/pull/3679 * Fix noise synthesis to consistently update database keypoints by @ahojnnes in https://github.com/colmap/colmap/pull/3680 * Add missing binding for Reconstruction::DeleteAllPoints2DAndPoints3D. by @B1ueber2y in https://github.com/colmap/colmap/pull/3682 * Fix BA convergence setting bug during initialization by @ahojnnes in https://github.com/colmap/colmap/pull/3677 * Add notice of the version of panorama_sfm.py by @sarlinpe in https://github.com/colmap/colmap/pull/3685 * Cuda-Enabled Pip Package (Linux x86-64 only) by @Tobias314 in https://github.com/colmap/colmap/pull/3608 * Update to CUDA 12.9.1 in CI by @ahojnnes in https://github.com/colmap/colmap/pull/3610 * Add test for BA controller by @ahojnnes in https://github.com/colmap/colmap/pull/3683 * Add tests for option manager by @ahojnnes in https://github.com/colmap/colmap/pull/3684 * Add tests for feature matching controllers by @ahojnnes in https://github.com/colmap/colmap/pull/3686 * Remove unused variable in reconstruction test by @ahojnnes in https://github.com/colmap/colmap/pull/3691 * Update the logic of duplicate correspondence to support one-to-many matches. by @B1ueber2y in https://github.com/colmap/colmap/pull/3681 * Some misc code improvements for ObservationManager by @ahojnnes in https://github.com/colmap/colmap/pull/3689 * Update PyPI publishing by @sarlinpe in https://github.com/colmap/colmap/pull/3692 * Update deprecated Eigen JacobiSVD usage by @ahojnnes in https://github.com/colmap/colmap/pull/3690 * Add unit test for feature extraction controller by @ahojnnes in https://github.com/colmap/colmap/pull/3693 * Update to latest clang-format 20.1.5 by @ahojnnes in https://github.com/colmap/colmap/pull/3694 * Add function to synthesize images by @ahojnnes in https://github.com/colmap/colmap/pull/3696 * Remove deprecated rig bundle adjuster by @B1ueber2y in https://github.com/colmap/colmap/pull/3698 * Fix coordinate bug in ComputeEqualPartsBboxes by @ahojnnes in https://github.com/colmap/colmap/pull/3700 * Create unit test for automatic reconstruction controller by @ahojnnes in https://github.com/colmap/colmap/pull/3697 * Add unit test for HammingDistanceWeightFunctor by @ahojnnes in https://github.com/colmap/colmap/pull/3702 * Add unit test for option manager parsing and move sys exit to call sites by @ahojnnes in https://github.com/colmap/colmap/pull/3703 * Add unit test for feature importer controller by @ahojnnes in https://github.com/colmap/colmap/pull/3704 -------------------------- COLMAP 3.12.6 (09/17/2025) -------------------------- Improvements ------------ * Upgrade to pybind 3.0.1 and use smart holder for all classes. * Support both Qt5 and Qt6. * Ensure download support on Ubuntu by installing libssl-dev for crypto. * Add bindings for mvs::model for covisibility support in pycolmap. * Add missing rig and frame interfaces for pycolmap database. * Throw exception when the rig configurations of the database and existing reconstruction are inconsistent. * Improve the Gauge logic for fixing two views. Bug Fixes --------- * Fix focal length extraction from 35mm equivalent. * Fix inconsistent pycolmap naming for RegisterFrame and DeRegisterFrame. * Call Retriangulate irrespective of the logging level. * Fix bundle adjustment with constant rig from world pose. -------------------------- COLMAP 3.12.5 (08/22/2025) -------------------------- Improvements ------------ * Add various safety checks with more understandable error messages when adding misconfigured rigs/cameras/frames/images to the reconstruction. * Recover original metric reconstruction scale in case of configured rigs. Bug Fixes --------- * Fix incompatibilities due to redundant symbol definition in pycolmap/pyceres. * Fix error threshold for generalized relative pose to be in pixel space. * Fix rig configuration in case of inconsistent IDs in database/reconstruction. * Fix missing colmap namespace in sqlite3 macro. * Fix CMake configuration with Boost 1.89 or newer. * Fix viewer crash when clearing the reconstruction. -------------------------- COLMAP 3.12.4 (08/04/2025) -------------------------- Bug Fixes --------- * Fix global bundle adjustment performance regression due to changing gauge fixing mechanism by fixing points vs. cameras. -------------------------- COLMAP 3.12.3 (07/16/2025) -------------------------- Bug Fixes --------- * Set correct version number -------------------------- COLMAP 3.12.2 (07/16/2025) -------------------------- Bug Fixes --------- * Define VisualIndex::Read as static in python bindings * Only find C/CXX OpenMP components to support new CMake versions * Fix a bug affecting feature matching on GPU * Fix potential deadlock in job queue * Update FindMetis.cmake to also link GK_LIBRARIES * Fix backwards compatibility in mapper with custom image list * Fix docker run script for GUI -------------------------- COLMAP 3.12.1 (07/05/2025) -------------------------- Bug Fixes --------- * Fix Docker runtime libraries * Fix spatial matcher bug * Minor fixes for documentation -------------------------- COLMAP 3.12.0 (06/30/2025) -------------------------- New Features ------------ * Support for modeling sensor rigs (and thus multi-camera rigs and panoramas). For more details and usage examples, see: https://colmap.github.io/rigs.html. * Automatic download and caching of vocabulary trees and other resources. * Support for converting between LLA and UTM coordinates. * Improved minimal solvers for affine transform and generalized absolute/relative pose. * Improved absolute pose estimation by minimizing pixel error in image space. * Replaced FLANN with faiss for fast approximate nearest neighbor search for improved speed in CPU-based feature matching and vocabulary tree-based image retrieval. * Support for propagating relative pose covariance. * Support visualization of models with arbitrary origin and scale (e.g., in GPS space). * Reconstruction benchmark for ETH3D, IMC, BlendedMVS datasets. * Measure and report code test coverage in CI. Bug Fixes --------- * Fixed RANSAC stopping criterion, see https://arxiv.org/pdf/2503.07829. * Fixed and improved two-view pose and triangulation angle estimation. * Fix rare deadlock during vocab tree feature matching. * For other bug fixes, see full list of changes below. Breaking Changes ---------------- * Serialization of reconstruction and database contains a new abstraction: rigs and frames. The reconstruction output contains two new files `rigs.{bin,txt}` and `frames.{bin,txt}`. The database contains new tables: `rigs`, `rig_sensors`, `frames`, `frames_data`. Reading from existing reconstructions and databases (without rigs/frames) is fully backwards compatible and vice versa reading new reconstructions (with rigs/frames) using old code is fully forwards compatible. * Sensor poses (and thus image poses) are now composed as: `sensor_from_world = sensor_from_rig * rig_from_world`. Previously, `image.cam_from_world` returned a reference to the pose parameters. Now it returns a copy of the pose composition: `image.cam_from_world() = image.frame.rig.sensor_from_rig(image.camera.sensor_id) * image.frame.rig_from_world` with the underlying pose parameters stored in the rig and frame objects. * Default bundle adjuster supports sensor rigs and thus rig bundle adjuster is deprecated. * FLANN-based vocabulary trees are incompatible with faiss. New trees automatically downloaded, if no vocab_tree_path is provided, otherwise manual download and update required. * Removed official support for Ubuntu 20.04, MacOS 13, and Visual Studio 2019. Full Change List (sorted temporally) ------------------------------------ * Cancel previous Github action runs upon push by @ahojnnes in https://github.com/colmap/colmap/pull/2998 * Fix ccache installation in pycolmap windows CI by @ahojnnes in https://github.com/colmap/colmap/pull/2997 * Use Azure blob storage as vcpkg binary cache by @ahojnnes in https://github.com/colmap/colmap/pull/2999 * Add missing openmp flags in retrieval for flann parallelization by @ahojnnes in https://github.com/colmap/colmap/pull/3018 * Separate read and write SAS tokens for vcpkg binary cache by @ahojnnes in https://github.com/colmap/colmap/pull/3027 * Define vcpkg binary cache source inline by @ahojnnes in https://github.com/colmap/colmap/pull/3028 * Fix conditional vcpkg binary cache config in bash by @ahojnnes in https://github.com/colmap/colmap/pull/3031 * Avoid absolute path for the include directory installation by @jhacsonmeza in https://github.com/colmap/colmap/pull/3024 * Add conversion between LLA and UTM coords. by @StonerLing in https://github.com/colmap/colmap/pull/3030 * Improve interface for ReadWriteBinaryBlob and add tests by @ahojnnes in https://github.com/colmap/colmap/pull/3033 * Add support for downloading files by @ahojnnes in https://github.com/colmap/colmap/pull/3022 * Add function to compute sha256 digest by @ahojnnes in https://github.com/colmap/colmap/pull/3035 * Automatically download and cache vocabulary tree by @ahojnnes in https://github.com/colmap/colmap/pull/3036 * Set vcpkg default features and synchronize to latest vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/3038 * Avoid unnecessary copy of input elements in Percentile/Median by @ahojnnes in https://github.com/colmap/colmap/pull/3039 * Abstract algorithm class IncrementalMapperImpl by @B1ueber2y in https://github.com/colmap/colmap/pull/3040 * Perform linear interpolation in percentile computation by @ahojnnes in https://github.com/colmap/colmap/pull/3041 * Avoid dependent inputs in IncrementalMapperImpl by @B1ueber2y in https://github.com/colmap/colmap/pull/3043 * Reorder destructors for better safety in EndReconstruction by @B1ueber2y in https://github.com/colmap/colmap/pull/3046 * Improvements for reconstruction normalization / bbox / centroid by @ahojnnes in https://github.com/colmap/colmap/pull/3047 * Speedup affine transform minimal solver, create python bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3049 * Fix compilation with DOWNLOAD_ENABLED=OFF by @ahojnnes in https://github.com/colmap/colmap/pull/3053 * Consistent interface/tests for rigid3d/sim3d/affine2d, pycolmap bindings for rigid3d by @ahojnnes in https://github.com/colmap/colmap/pull/3051 * Improve logging for errors in masking during feature extraction by @Ambrosiussen in https://github.com/colmap/colmap/pull/3034 * Add copy constructor support for solver-related ceres bindings by @B1ueber2y in https://github.com/colmap/colmap/pull/3059 * Minor fix on using pycolmap bundle adjuster with pyceres by @B1ueber2y in https://github.com/colmap/colmap/pull/3060 * Re-enable interface support for covariance estimation from a Ceres::Problem instance by @B1ueber2y in https://github.com/colmap/colmap/pull/3061 * Only cancel CI runs in PRs and not in main/release branches by @ahojnnes in https://github.com/colmap/colmap/pull/3063 * Add binding support for invalid values in pycolmap id types by @B1ueber2y in https://github.com/colmap/colmap/pull/3072 * Fix custom quality level in ETH3D benchmark by @ahojnnes in https://github.com/colmap/colmap/pull/3076 * Set max_num_features automatically per quality level by @ahojnnes in https://github.com/colmap/colmap/pull/3077 * Make it possible to build the MVS doc even when CUDA is not installed by @sarlinpe in https://github.com/colmap/colmap/pull/3078 * Temporarily disable ccache in the pycolmap macOS CI by @sarlinpe in https://github.com/colmap/colmap/pull/3084 * Add option to specify image list in automatic reconstruction by @ahojnnes in https://github.com/colmap/colmap/pull/3074 * Only create OpenGL context in automatic reconstruction if necessary by @ahojnnes in https://github.com/colmap/colmap/pull/3075 * Remove unnecessary braces around initializer in pycolmap/covariance by @ahojnnes in https://github.com/colmap/colmap/pull/3080 * Remove temporary fixes for macOS CI by @sarlinpe in https://github.com/colmap/colmap/pull/2954 * Reconstruction benchmark by @ahojnnes in https://github.com/colmap/colmap/pull/2714 * Re-enable ccache in pycolmap Mac CI by @sarlinpe in https://github.com/colmap/colmap/pull/3085 * Fix transitive completion in incremental triangulator by @ahojnnes in https://github.com/colmap/colmap/pull/3094 * Fix image deletion, hide point viewer widget after deletion by @ahojnnes in https://github.com/colmap/colmap/pull/3098 * Fix download functionality under Windows by @ahojnnes in https://github.com/colmap/colmap/pull/3099 * Add back detailed logs for covariance estimation by @B1ueber2y in https://github.com/colmap/colmap/pull/3082 * Fix reprojection error in camera rig cost function by @binbin-xu in https://github.com/colmap/colmap/pull/3106 * Install missing libcurl4 runtime library in dockerfile by @ahojnnes in https://github.com/colmap/colmap/pull/3122 * Expose incremental mapper pose prior options in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3123 * Remove year from copyright by @ahojnnes in https://github.com/colmap/colmap/pull/3124 * Use poselib for generalized absolute pose minimal solver by @ahojnnes in https://github.com/colmap/colmap/pull/3125 * Add code coverage reporting by @ahojnnes in https://github.com/colmap/colmap/pull/3126 * Fix synthetic prior generation when stddev=0 by @ahojnnes in https://github.com/colmap/colmap/pull/3128 * Create temporary colmap test directy under system test directory by @ahojnnes in https://github.com/colmap/colmap/pull/3129 * Minor: pyceres is no longer a must for running pycolmap bundle adjuster by @B1ueber2y in https://github.com/colmap/colmap/pull/3130 * Fix cost functor convention for benchmarking by @B1ueber2y in https://github.com/colmap/colmap/pull/3131 * Support enum from string conversion by @ahojnnes in https://github.com/colmap/colmap/pull/3132 * More robustly handle degenerate triangulation angles by @ahojnnes in https://github.com/colmap/colmap/pull/3135 * Minor: add missing empty namespace in alignment testing script by @B1ueber2y in https://github.com/colmap/colmap/pull/3137 * Add frame impl for future rig support by @B1ueber2y in https://github.com/colmap/colmap/pull/2698 * Rename RigCalibration to RigCalib by @ahojnnes in https://github.com/colmap/colmap/pull/3142 * Fix and improve two-view pose and triangulation angle estimation by @ahojnnes in https://github.com/colmap/colmap/pull/3146 * Fix covariance propagation of pose inverse by @B1ueber2y in https://github.com/colmap/colmap/pull/3155 * [Spherical Camera Support] Change essential matrix estimation to use camera rays by @ahojnnes in https://github.com/colmap/colmap/pull/3159 * Improve incremental mapper initialization logic by @ahojnnes in https://github.com/colmap/colmap/pull/3161 * Improved RANSAC dependency injection by @ahojnnes in https://github.com/colmap/colmap/pull/3165 * Add docs on the left convention in COLMAP for covariance propagation. by @B1ueber2y in https://github.com/colmap/colmap/pull/3167 * Add docker instruction link to docs by @j3soon in https://github.com/colmap/colmap/pull/3169 * Compute absolute pose estimation error in image space by @ahojnnes in https://github.com/colmap/colmap/pull/3166 * Add support for propagating relative pose covariance. by @B1ueber2y in https://github.com/colmap/colmap/pull/3168 * Avoid using namespace in pycolmap headers by @ahojnnes in https://github.com/colmap/colmap/pull/3173 * Fix naming of cross covariance and add relative pose covariance interface by @B1ueber2y in https://github.com/colmap/colmap/pull/3170 * Camera models perform valid projection test by @ahojnnes in https://github.com/colmap/colmap/pull/3172 * Various improvements and extensions for pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3176 * Fix pycolmap ci build for pull requests by @B1ueber2y in https://github.com/colmap/colmap/pull/3178 * Change CamFromImg to return optional ray by @ahojnnes in https://github.com/colmap/colmap/pull/3180 * Triangulation operates on camera rays by @ahojnnes in https://github.com/colmap/colmap/pull/3184 * Python bindings for visual index by @ahojnnes in https://github.com/colmap/colmap/pull/3185 * Define bindings in the correct order by @sarlinpe in https://github.com/colmap/colmap/pull/3189 * Restore CamFromImg to return normalized camera coordinates instead of… by @ahojnnes in https://github.com/colmap/colmap/pull/3193 * Add Rig serialization support to reconstruction+database by @ahojnnes in https://github.com/colmap/colmap/pull/3143 * Pull changes from main branch by @ahojnnes in https://github.com/colmap/colmap/pull/3194 * Fix maybe-uninitialized warnings by @papjuli in https://github.com/colmap/colmap/pull/3199 * Fix compilation errors with PoissonRecon by @theartful in https://github.com/colmap/colmap/pull/3200 * Remove Ubuntu 20.04 from the CI by @sarlinpe in https://github.com/colmap/colmap/pull/3203 * Add support for frame serialization by @ahojnnes in https://github.com/colmap/colmap/pull/3202 * Handle non-trivial frames in bundle adjustment by @ahojnnes in https://github.com/colmap/colmap/pull/3214 * Update email address by @sarlinpe in https://github.com/colmap/colmap/pull/3223 * Change the root of the Python package by @sarlinpe in https://github.com/colmap/colmap/pull/3217 * Fix bug when toggling rendering by @ahojnnes in https://github.com/colmap/colmap/pull/3230 * Add convenience iterator for frame image ids by @ahojnnes in https://github.com/colmap/colmap/pull/3231 * Update feature/rig with main by @ahojnnes in https://github.com/colmap/colmap/pull/3241 * Update to latest vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/3243 * Update feature/rig branch with latest changes in main by @ahojnnes in https://github.com/colmap/colmap/pull/3244 * Fix incremental pycolmap build script by @ahojnnes in https://github.com/colmap/colmap/pull/3245 * Logically group image reader options by @ahojnnes in https://github.com/colmap/colmap/pull/3246 * Fix chained match synthesis by @ahojnnes in https://github.com/colmap/colmap/pull/3248 * Retire Reconstruction::IsImageRegistered in favor of existing Image::HasPose by @ahojnnes in https://github.com/colmap/colmap/pull/3247 * Fix two-view geometry pose estimation for homography by @ahojnnes in https://github.com/colmap/colmap/pull/3250 * Fix uninitialized variable warnings by @ahojnnes in https://github.com/colmap/colmap/pull/3254 * Include Boost headers on build by @jonahjnewton in https://github.com/colmap/colmap/pull/3257 * Pull latest changes from main to feature/rig by @ahojnnes in https://github.com/colmap/colmap/pull/3262 * Support rigs/frames in incremental mapper by @ahojnnes in https://github.com/colmap/colmap/pull/3238 * Rename FrameFromWorld to RigFromWorld pose by @ahojnnes in https://github.com/colmap/colmap/pull/3263 * Add pytest on the e2e python pipeline into CI. by @B1ueber2y in https://github.com/colmap/colmap/pull/3266 * Fix broken python interfaces by @B1ueber2y in https://github.com/colmap/colmap/pull/3267 * Use generalized absolute pose estimation for non-trivial frames by @ahojnnes in https://github.com/colmap/colmap/pull/3265 * Fix color extraction for rig frames by @ahojnnes in https://github.com/colmap/colmap/pull/3268 * Sequential matcher expands rig images by @ahojnnes in https://github.com/colmap/colmap/pull/3270 * Fix usage of deprecated pycolmap interfaces in pycolmap README. by @B1ueber2y in https://github.com/colmap/colmap/pull/3272 * Improved code/docs and tests for rig configuration by @ahojnnes in https://github.com/colmap/colmap/pull/3275 * Update vcpkg to pull in fixes for ceres by @ahojnnes in https://github.com/colmap/colmap/pull/3276 * Rig bundle adjuster uses default bundle adjustment routine by @ahojnnes in https://github.com/colmap/colmap/pull/3281 * Cleanup legacy camera rig code by @ahojnnes in https://github.com/colmap/colmap/pull/3283 * Store rig sensors and frame data in separate database tables by @ahojnnes in https://github.com/colmap/colmap/pull/3285 * Configure trivial rigs and frames during feature extraction by @ahojnnes in https://github.com/colmap/colmap/pull/3287 * [Bugfix] Center 2D points by principal point for absolute pose estimation with unknown focal length by @xjiangan in https://github.com/colmap/colmap/pull/3289 * Add bindings for rig configuration by @ahojnnes in https://github.com/colmap/colmap/pull/3291 * Documentation for rig support by @ahojnnes in https://github.com/colmap/colmap/pull/3290 * Fix documentation of rigs.txt by @sarlinpe in https://github.com/colmap/colmap/pull/3292 * Update feature/rig with latest changes in main by @ahojnnes in https://github.com/colmap/colmap/pull/3293 * Merge feature/rig branch into main by @ahojnnes in https://github.com/colmap/colmap/pull/3295 * improve clarity of the rig example by @B1ueber2y in https://github.com/colmap/colmap/pull/3297 * Bind missing SequentialMatchingOptions.loop_detection_period by @sarlinpe in https://github.com/colmap/colmap/pull/3299 * cleanup legacy comments for base controller. by @B1ueber2y in https://github.com/colmap/colmap/pull/3300 * Fix bug in grayscale Bitmap.to_array by @sarlinpe in https://github.com/colmap/colmap/pull/3301 * Handle errors in Bitmap.read by @sarlinpe in https://github.com/colmap/colmap/pull/3302 * Add an example script for SfM with 360 spherical images by @sarlinpe in https://github.com/colmap/colmap/pull/3304 * Recognize URIs for vocab_tree_path in GUI feature matching by @ahojnnes in https://github.com/colmap/colmap/pull/3305 * Deterministic behavior for Python pipeline tests by @ahojnnes in https://github.com/colmap/colmap/pull/3306 * Move colmap/ui/main_window.h include to implementation by @ahojnnes in https://github.com/colmap/colmap/pull/3307 * Add Python 3.13 to pycolmap build matrix by @ahojnnes in https://github.com/colmap/colmap/pull/3308 * Add missing SiftMatchingOptions::cpu_brute_force_matcher to pycolmap bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3309 * Augment pinhole renders with GPS EXIFs of the panos by @sarlinpe in https://github.com/colmap/colmap/pull/3310 * Add missing cpu_brute_force_matcher to option manager by @ahojnnes in https://github.com/colmap/colmap/pull/3315 * Bind GPSTransform and make GPSTransform::Ellipsoid an enum class by @sarlinpe in https://github.com/colmap/colmap/pull/3311 * Update pose prior bundle adjuster to handle rigs by @ahojnnes in https://github.com/colmap/colmap/pull/3312 * Add support for running pose prior mapper from GUI by @ahojnnes in https://github.com/colmap/colmap/pull/3313 * Enable different matcher types and default to sequential in pano example by @ahojnnes in https://github.com/colmap/colmap/pull/3314 * Modularize reconstruction I/O formats into different libraries by @ahojnnes in https://github.com/colmap/colmap/pull/3317 * Fall back to P3P solver for panoramic generalized absolute pose by @ahojnnes in https://github.com/colmap/colmap/pull/3318 * Fix FLANN-based CPU feature matcher crash in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/3320 * Update cibuildwheel to 2.23.2 by @ahojnnes in https://github.com/colmap/colmap/pull/3081 * Assume prior focal length for explicitly defined rig camera models by @ahojnnes in https://github.com/colmap/colmap/pull/3321 * Fix rig configuration with partial input reconstruction by @ahojnnes in https://github.com/colmap/colmap/pull/3322 * Use reference for image.camera and image.frame in pycolmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3323 * Use reference for frame.rig in pycolmap. by @B1ueber2y in https://github.com/colmap/colmap/pull/3324 * Cosmetic improvement on some geometry python bindings by @B1ueber2y in https://github.com/colmap/colmap/pull/3325 * Add unit test for EstimateAbsolutePose by @ahojnnes in https://github.com/colmap/colmap/pull/3327 * Add gmock matchers for rigid3 and sim3 by @ahojnnes in https://github.com/colmap/colmap/pull/3328 * Add unit tests for absolute pose refinement by @ahojnnes in https://github.com/colmap/colmap/pull/3330 * Cosmetic cleanup for absolute pose tests by @ahojnnes in https://github.com/colmap/colmap/pull/3333 * Add generalized relative pose estimation and pose binding cleanups by @ahojnnes in https://github.com/colmap/colmap/pull/3334 * Turn camera parameter access debug checks into throwing checks by @ahojnnes in https://github.com/colmap/colmap/pull/3337 * Handle panoramic rigs in generalized relative pose estimation by @ahojnnes in https://github.com/colmap/colmap/pull/3338 * Cosmetic variable name improvements to match conventions by @ahojnnes in https://github.com/colmap/colmap/pull/3341 * Add unit test for relative pose estimation by @ahojnnes in https://github.com/colmap/colmap/pull/3342 * Avoid nested parallelization for vocab tree pairing by @ahojnnes in https://github.com/colmap/colmap/pull/3343 * Fix rigid3/sim3 matchers for older eigen versions by @ahojnnes in https://github.com/colmap/colmap/pull/3344 * Deterministic homography test by @ahojnnes in https://github.com/colmap/colmap/pull/3346 * Add missing return statement in PyEstimateGeneralizedRelativePose by @ahojnnes in https://github.com/colmap/colmap/pull/3349 * Fix runtime error in panorama_sfm.py with sequential matching by @samuelm2 in https://github.com/colmap/colmap/pull/3351 * Fix race conditions in feature matcher cache by @ahojnnes in https://github.com/colmap/colmap/pull/3354 * Use shared lock in thread safe LRU cache by @ahojnnes in https://github.com/colmap/colmap/pull/3355 * Upgrade Jimver/cuda-toolkit GH actions task to 0.2.23 by @ahojnnes in https://github.com/colmap/colmap/pull/3358 * Upgrade to Ubuntu 24.04 / clang-18 in CI for ASan and ClangTidy builds by @ahojnnes in https://github.com/colmap/colmap/pull/3357 * Use add_compile_definitions instead of deprecated add_definitions by @ahojnnes in https://github.com/colmap/colmap/pull/3348 * Update Mac Github runners and fix pycolmap deployment targets by @ahojnnes in https://github.com/colmap/colmap/pull/3361 * Suppress CUDA warnings related constexpr host/device calls by @ahojnnes in https://github.com/colmap/colmap/pull/3362 * Update docker image to ubuntu 24.04 by @ahojnnes in https://github.com/colmap/colmap/pull/3363 * Fix benchmarking for rigs by @ahojnnes in https://github.com/colmap/colmap/pull/3364 * Add option to overwrite matches in benchmarking by @ahojnnes in https://github.com/colmap/colmap/pull/3365 * Replace flann with faiss by @ahojnnes in https://github.com/colmap/colmap/pull/3350 * Update docker with all major CUDA archs and updated boost version by @ahojnnes in https://github.com/colmap/colmap/pull/3369 * Retire remaining flann components and remove as dependency by @ahojnnes in https://github.com/colmap/colmap/pull/3370 * Update feature index to use float descriptors and distances by @ahojnnes in https://github.com/colmap/colmap/pull/3371 * Fix deadlock during feature matching by @ahojnnes in https://github.com/colmap/colmap/pull/3373 * Warn user when reading legacy flann index by @ahojnnes in https://github.com/colmap/colmap/pull/3372 * expose loading database into database cache from DatabaseCache::Create. by @B1ueber2y in https://github.com/colmap/colmap/pull/3375 * minor: rename DatabaseCache::LoadDatabase to Load by @B1ueber2y in https://github.com/colmap/colmap/pull/3376 * Fix typo by @B1ueber2y in https://github.com/colmap/colmap/pull/3377 * Unit tests for image reader, remove redundant definition of database by @ahojnnes in https://github.com/colmap/colmap/pull/3383 * Fix trailing comma-separation when printing list contents by @ahojnnes in https://github.com/colmap/colmap/pull/3388 * Add missing VocabTreeMatching.num_threads in option manager by @ahojnnes in https://github.com/colmap/colmap/pull/3389 * Use OpenBLAS OpenMP version under Ubuntu to fix slow faiss by @ahojnnes in https://github.com/colmap/colmap/pull/3390 * Speedup database reads of rigs/frames with single SQL outer join query by @ahojnnes in https://github.com/colmap/colmap/pull/3387 * Introduce context manager to reset sqlite3 statements by @ahojnnes in https://github.com/colmap/colmap/pull/3392 * Add missing use_gpu options in pycolmap SIFT bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3397 * Add FeatureMatch python bindings by @ahojnnes in https://github.com/colmap/colmap/pull/3398 * Add option to set log level in GUI by @ahojnnes in https://github.com/colmap/colmap/pull/3399 * Add docs to explain the concepts of rigs and frames. by @B1ueber2y in https://github.com/colmap/colmap/pull/3395 * Allow png mask without double extension by @MotivaCG in https://github.com/colmap/colmap/pull/3284 * Propagate macros to top-level CMakeLists.txt files by @jhacsonmeza in https://github.com/colmap/colmap/pull/3396 * Add a missing function implementation by @lpanaf in https://github.com/colmap/colmap/pull/3412 * Improved tests for reconstruction merging by @ahojnnes in https://github.com/colmap/colmap/pull/3413 * Use MKL as BLAS vendor for faiss by @ahojnnes in https://github.com/colmap/colmap/pull/3393 * Fix wrong doc for point covariance by @B1ueber2y in https://github.com/colmap/colmap/pull/3416 * Add legacy docs from 3.8 to 3.11. by @B1ueber2y in https://github.com/colmap/colmap/pull/3414 * Do not filter existing, fixed frames by @ahojnnes in https://github.com/colmap/colmap/pull/3403 * Tag commit id and date in the doc generation by @B1ueber2y in https://github.com/colmap/colmap/pull/3417 * Return bad initial pair when number of triangulation is less than abs_pose_min_num_inliers by @B1ueber2y in https://github.com/colmap/colmap/pull/3418 * Add option to build with thread sanitizer flags by @ahojnnes in https://github.com/colmap/colmap/pull/3420 * Add option to build with undefined behavior sanitizer flags by @ahojnnes in https://github.com/colmap/colmap/pull/3421 * Fix the RANSAC stopping criterion by @ahojnnes in https://github.com/colmap/colmap/pull/3425 * Replace incorrect call to nonZeros by @sarlinpe in https://github.com/colmap/colmap/pull/3426 * Add deprecation warning for rig_bundle_adjuster by @sarlinpe in https://github.com/colmap/colmap/pull/3427 * Fix incorrect include in euclidean_transform.h by @sarlinpe in https://github.com/colmap/colmap/pull/3428 * Add Frame::SetCamFromWorld in pycolmap and fix comment. by @B1ueber2y in https://github.com/colmap/colmap/pull/3429 * Estimate essential matrix using camera rays instead of points by @ahojnnes in https://github.com/colmap/colmap/pull/3423 * Fix FilterPoints3DWithSmallTriangulationAngle to return number of filtered observations by @whuaegeanse in https://github.com/colmap/colmap/pull/3424 * Update to latest vcpkg commit by @ahojnnes in https://github.com/colmap/colmap/pull/3430 * Initialize from non-trivial frame pairs using generalized relative pose by @ahojnnes in https://github.com/colmap/colmap/pull/3419 * Fix setup_ubuntu.sh for docker by @MasahiroOgawa in https://github.com/colmap/colmap/pull/3432 * Support visualization of models with arbitrary origin and scale by @ahojnnes in https://github.com/colmap/colmap/pull/3044 * Fix ReadPositionPriorData to return valid and numerically more stable Position prior data by @whuaegeanse https://github.com/colmap/colmap/pull/3438 -------------------------- COLMAP 3.11.1 (12/06/2024) -------------------------- Bug Fixes --------- * Fix typo in pycolmap function align_reconstruction_to_locations interface by @B1ueber2y in https://github.com/colmap/colmap/pull/2961 * Add back some ceres bindings to use pycolmap bundle adjustment without pyceres by @B1ueber2y in https://github.com/colmap/colmap/pull/2985 * Fix setting of RANSAC max error in pose prior BA alignment by @ahojnnes in https://github.com/colmap/colmap/pull/2993 -------------------------- COLMAP 3.11.0 (11/28/2024) -------------------------- New Features ------------ * New pose prior based incremental mapper that can leverage absolute pose priors from e.g. GPS measurements. * New bundle adjustment covariance estimation functionality. Significantly faster and more robust than Ceres. * API documentation with auto-generated stubs for pycolmap. * Use PoseLib's minimal solvers for faster performance and improved robustness. * Experimental support for CUDA-based bundle adjustment through Ceres (disabled by default). * Support for reading 16-bit PNG grayscale images. * New RAD_TAN_THIN_PRISM_FISHEYE camera model in support of Meta's Project Aria devices. * Replace numerical with analytical Jacobian in image undistortion for better convergence. * Many more performance optimizations and other improvements. See full list of changes below. Bug Fixes --------- * Fixed non-deterministic behavior of CUDA SIFT feature extractor. Broken since 3.10 release. * Fixed orientation detection of covariant/affine SIFT feature extractor. Broken since initial release. * Fixed point triangulator crashing due to bug in observation manager. Broken since 3.10 release. * Fixed sequential feature matcher overlap missing the farthest image. Broken since initial release. * Fixed rare deadlock during matching due to concurrent database access. Broken since 3.10 release. * Fixed little/big endian detection. Broken since 3.1 release. * For other bug fixes, see full list of changes below. Breaking Changes ---------------- * Dropped official support for Ubuntu 18.04, Visual Studio 2019. * Upgrade to C++17 standard in C++ and C++14 in CUDA source code. * New ``pose_priors`` table in database in support of pose prior based mapper. * PyCOLMAP API: * ``align_reconstrution_to_locations`` is renamed to ``align_reconstruction_to_locations`` (typo). * ``pycomap.cost_functions`` becomes a module and should be explicitly imported as ``import pycolmap.cost_functions``. * Replaced ``Image.registered`` by ``Image.{has_pose,reset_pose}``. * Replaced ``Image.{get_valid_point2D_ids,get_valid_points2D}`` by ``Image.{get_observation_point2D_idxs,get_observation_points2D}``. * Replaced ``Track.{append,remove}`` by ``Track.{add_element,delete_element}``. * ``AbsolutePoseErrorCost`` becomes ``AbsolutePosePriorCost``. * ``MetricRelativePoseErrorCost`` becomes ``RelativePosePriorCost``. * The signature of ``ReprojErrorCost`` and related cost functions was changed: arguments are reordered, the detection uncertainty is now a 2x2 covariance matrix. * ``BundleAdjuster`` becomes virtual and should be created with ``pycolmap.create_default_bundle_adjuster()``. * ``absolute_pose_estimation`` becomes ``estimate_and_refine_absolute_pose``. * ``pose_refinement`` becomes ``refine_absolute_pose``. * ``essential_matrix_estimation`` becomes ``estimate_essential_matrix``. * ``fundamental_matrix_estimation`` becomes ``estimate_fundamental_matrix``. * ``rig_absolute_pose_estimation`` becomes ``estimate_and_refine_generalized_absolute_pose``. * ``homography_matrix_estimation`` becomes ``estimate_homography_matrix``. * ``squared_sampson_error`` becomes ``compute_squared_sampson_error``. * ``homography_decomposition`` becomes ``pose_from_homography_matrix``. * ``Rigid3d.essential_matrix`` becomes ``pycolmap.essential_matrix_from_pose``. Full Change List (sorted temporally) ------------------------------------ * Updates for pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/2672 * Trigger CI on release/* branches by @ahojnnes in https://github.com/colmap/colmap/pull/2673 * Use consistent versioning scheme between C++/Python by @ahojnnes in https://github.com/colmap/colmap/pull/2674 * Add cost function for 3D alignment (with covariance) by @B1ueber2y in https://github.com/colmap/colmap/pull/2621 * Numpy 2 compatibility by @sarlinpe in https://github.com/colmap/colmap/pull/2682 * Add fix for specifying the correct pycolmap CMake python development … by @fulkast in https://github.com/colmap/colmap/pull/2683 * Remove non existant flags of model_aligner from docs by @TamirCohen in https://github.com/colmap/colmap/pull/2696 * Reset CMAKE_MODULE_PATH to previous value by @mvieth in https://github.com/colmap/colmap/pull/2699 * Robustify nchoosek against overflow by @ahojnnes in https://github.com/colmap/colmap/pull/2706 * Observation manager needs to check if image_id exists before query operations by @bo-rc in https://github.com/colmap/colmap/pull/2704 * Remove pose prior from database.py:add_image by @sarlinpe in https://github.com/colmap/colmap/pull/2707 * Fix: sequential matcher overlap number should be inclusive by @flm8620 in https://github.com/colmap/colmap/pull/2701 * Fix table mangled by clang-format by @sweber1 in https://github.com/colmap/colmap/pull/2710 * Write out options to ini in full precision, relax bundle adjuster convergence by @ahojnnes in https://github.com/colmap/colmap/pull/2713 * Tests for pairing library in feature matching by @ahojnnes in https://github.com/colmap/colmap/pull/2711 * Rename IncrementalMapperOptions to IncrementalPipelineOptions by @B1ueber2y in https://github.com/colmap/colmap/pull/2708 * Add support for CUDA sparse BA solver by @ahojnnes in https://github.com/colmap/colmap/pull/2717 * Rename HierarchicalMapperController to HierarchicalPipeline by @ahojnnes in https://github.com/colmap/colmap/pull/2718 * Make VisualIndex::Quantize const to improve readability by @IshitaTakeshi in https://github.com/colmap/colmap/pull/2723 * Fix CUDA_ENABLED macro in new bundle adjustment code by @drkoller in https://github.com/colmap/colmap/pull/2725 * Automatically generate stub files by @sarlinpe in https://github.com/colmap/colmap/pull/2721 * Add CUDA-based dense BA solver by @ahojnnes in https://github.com/colmap/colmap/pull/2732 * Improved and simplified caching in feature matching by @ahojnnes in https://github.com/colmap/colmap/pull/2731 * Fix colmap namespace in the macro support of logging. by @B1ueber2y in https://github.com/colmap/colmap/pull/2733 * Add callbacks by move by @ahojnnes in https://github.com/colmap/colmap/pull/2734 * Implement transitive matcher with pair generator + tests by @ahojnnes in https://github.com/colmap/colmap/pull/2735 * Provide reasonable defaults for some estimator options by @sarlinpe in https://github.com/colmap/colmap/pull/2745 * Fix mismatched Delaunay meshing options by @sarlinpe in https://github.com/colmap/colmap/pull/2748 * PyCOLMAP API documentation by @sarlinpe in https://github.com/colmap/colmap/pull/2749 * Improved pycolmap coverage and docs by @sarlinpe in https://github.com/colmap/colmap/pull/2752 * Follow-up fixes in pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/2755 * Report errors in import_images by @sarlinpe in https://github.com/colmap/colmap/pull/2750 * Further simplification of feature matcher code by @ahojnnes in https://github.com/colmap/colmap/pull/2744 * Add missing ClearModifiedPoints3D by @sarlinpe in https://github.com/colmap/colmap/pull/2761 * Store shared camera ptr for reconstruction images by @ahojnnes in https://github.com/colmap/colmap/pull/2762 * Avoid unnecessary copy of queue in IncrementalTriangulator::Complete() by @ahojnnes in https://github.com/colmap/colmap/pull/2764 * Branch prediction for THROW_CHECK_NOTNULL by @ahojnnes in https://github.com/colmap/colmap/pull/2765 * Use shared camera pointer in more places by @ahojnnes in https://github.com/colmap/colmap/pull/2763 * Support switching camera directly with camera pointer by @B1ueber2y in https://github.com/colmap/colmap/pull/2767 * Add test for MergeReconstructions by @B1ueber2y in https://github.com/colmap/colmap/pull/2766 * Fix little/big endian detection by @ahojnnes in https://github.com/colmap/colmap/pull/2768 * Fix options for CUDA sparse BA solver by @whuaegeanse in https://github.com/colmap/colmap/pull/2758 * Rename SupperMeasurer::Compare for improved readability by @ahojnnes in https://github.com/colmap/colmap/pull/2774 * Improvements for install docs by @ahojnnes in https://github.com/colmap/colmap/pull/2773 * fixed typo of align_reconstrution_to_locations to align_reconstructio… by @TamirCohen in https://github.com/colmap/colmap/pull/2776 * Fix missing camera ptr for Reconstruction.DeleteAllPoints2DAndPoints3D() by @B1ueber2y in https://github.com/colmap/colmap/pull/2779 * Rename remaining proj_matrix instances to cam_from_world by @ahojnnes in https://github.com/colmap/colmap/pull/2780 * Relative pose decomposition uses Rigid3d by @ahojnnes in https://github.com/colmap/colmap/pull/2781 * Minor renaming on pycolmap point2d and point3d filenames by @B1ueber2y in https://github.com/colmap/colmap/pull/2784 * Add validity check for pixel coordinate in the Fisheye camera. Fix tests. by @B1ueber2y in https://github.com/colmap/colmap/pull/2790 * Use branch prediction in PRNG functions by @ahojnnes in https://github.com/colmap/colmap/pull/2796 * Implementation of Aria Fisheye camera model by @nushakrishnan in https://github.com/colmap/colmap/pull/2786 * Upgrade to C++ 17 by @B1ueber2y in https://github.com/colmap/colmap/pull/2801 * Pose Prior based Incremental Mapper by @ferreram in https://github.com/colmap/colmap/pull/2660 * Expose UpdatePoint3DErrors to pycolmap by @theartful in https://github.com/colmap/colmap/pull/2805 * Switch to the Ruff Python formatter by @sarlinpe in https://github.com/colmap/colmap/pull/2803 * Add mixed Python-C++ PyCOLMAP package by @sarlinpe in https://github.com/colmap/colmap/pull/2747 * Enable Ruff linter for Python by @sarlinpe in https://github.com/colmap/colmap/pull/2806 * Use C++17 structured bindings in some places by @ahojnnes in https://github.com/colmap/colmap/pull/2808 * Add RAD_TAN_THIN_PRISM_FISHEYE to camera docs by @ahojnnes in https://github.com/colmap/colmap/pull/2810 * Customized cost functions should be functors instead by @B1ueber2y in https://github.com/colmap/colmap/pull/2811 * Install and use newer clang-format from pypi by @ahojnnes in https://github.com/colmap/colmap/pull/2812 * Return a reference in Reconstruction.image/camera/point3D by @sarlinpe in https://github.com/colmap/colmap/pull/2814 * Add test for PositionPriorErrorCostFunctor. by @ferreram in https://github.com/colmap/colmap/pull/2815 * Replace boost/filesystem with standard library by @ahojnnes in https://github.com/colmap/colmap/pull/2809 * Fix selection of BA solver type when there is no cuda by @ahojnnes in https://github.com/colmap/colmap/pull/2822 * More informative exception if invalid access of image/camera/point3D by @sarlinpe in https://github.com/colmap/colmap/pull/2825 * Use minimal solvers from poselib by @ahojnnes in https://github.com/colmap/colmap/pull/2288 * Disable -march=native flags in poselib by @ahojnnes in https://github.com/colmap/colmap/pull/2828 * Make ``Image::cam_from_world_`` optional by @sarlinpe in https://github.com/colmap/colmap/pull/2824 * Remove warning in configure step by @sarlinpe in https://github.com/colmap/colmap/pull/2830 * Fix coordinate notation in EstimateAbsolutePose by @ahojnnes in https://github.com/colmap/colmap/pull/2833 * Return success status in low-level triangulation functions by @ahojnnes in https://github.com/colmap/colmap/pull/2834 * Pin mypy version for tests by @ahojnnes in https://github.com/colmap/colmap/pull/2849 * Suppress CMP0167 warning for FindBoost under CMake 3.30 or newer by @ahojnnes in https://github.com/colmap/colmap/pull/2853 * Reconstruction reader/writer tests and scene class repr by @ahojnnes in https://github.com/colmap/colmap/pull/2842 * Select CUDA device when bundle adjustment uses GPU by @ahojnnes in https://github.com/colmap/colmap/pull/2846 * Fix copying behaviors of Reconstruction regarding camera pointers by @B1ueber2y in https://github.com/colmap/colmap/pull/2841 * Use the C++ string representation for Python dataclass objects by @sarlinpe in https://github.com/colmap/colmap/pull/2855 * Various improvements for pycolmap bindings by @ahojnnes in https://github.com/colmap/colmap/pull/2854 * Use analytical Jacobian in IterativeUndistortion. Add trust region by @B1ueber2y in https://github.com/colmap/colmap/pull/2857 * Improve the conditioning of covariance estimation by @B1ueber2y in https://github.com/colmap/colmap/pull/2860 * Avoid unnecessary copy of RANSAC inlier masks by @ahojnnes in https://github.com/colmap/colmap/pull/2863 * Various improvements for cost functors by @ahojnnes in https://github.com/colmap/colmap/pull/2867 * Rename ``*_mapper`` to ``*_pipeline`` files by @ahojnnes in https://github.com/colmap/colmap/pull/2870 * Update the manylinux CI to GCC 10 by @sarlinpe in https://github.com/colmap/colmap/pull/2873 * Fix rare deadlock during matching due to concurrent database access by @ahojnnes in https://github.com/colmap/colmap/pull/2876 * Add new and missing options to automatic reconstructor by @ahojnnes in https://github.com/colmap/colmap/pull/2877 * Shared auto diff cost function creation by @ahojnnes in https://github.com/colmap/colmap/pull/2878 * Enable model alignment to reference model by @ahojnnes in https://github.com/colmap/colmap/pull/2879 * Add covariance weighted cost functor by @ahojnnes in https://github.com/colmap/colmap/pull/2880 * Fix unused variable warnings under MSVC by @ahojnnes in https://github.com/colmap/colmap/pull/2884 * Skip all but latest Python version in PR builds by @ahojnnes in https://github.com/colmap/colmap/pull/2881 * [doc] Fix path to example in README.md by @kielnino in https://github.com/colmap/colmap/pull/2886 * Update Github actions versions by @ahojnnes in https://github.com/colmap/colmap/pull/2887 * [doc] Fix typo for gui menu item by @kielnino in https://github.com/colmap/colmap/pull/2885 * Fix input type for automatic stereo fusion on extreme quality setting by @ahojnnes in https://github.com/colmap/colmap/pull/2893 * Make target with all sources optional by @HernandoR in https://github.com/colmap/colmap/pull/2889 * Gracefully handle missing image pose in viewer by @ahojnnes in https://github.com/colmap/colmap/pull/2894 * Update to latest vcpkg release 2024.10.21 by @ahojnnes in https://github.com/colmap/colmap/pull/2908 * Fix conversion from CUDA texture references to objects in SIFT feature extraction by @ahojnnes in https://github.com/colmap/colmap/pull/2911 * Modernized bundle adjustment interface by @ahojnnes in https://github.com/colmap/colmap/pull/2896 * Add missing unit tests for reconstruction alignment functions by @ahojnnes in https://github.com/colmap/colmap/pull/2913 * Do not test EstimateManhattanWorldFrame if LSD is disabled by @sarlinpe in https://github.com/colmap/colmap/pull/2920 * Custom macro for enum to string support by @B1ueber2y in https://github.com/colmap/colmap/pull/2918 * Bind the estimation of Sim3d by @sarlinpe in https://github.com/colmap/colmap/pull/2903 * Initialize glog in custom gmock main function by @ahojnnes in https://github.com/colmap/colmap/pull/2916 * Update ccache for faster windows CI builds by @ahojnnes in https://github.com/colmap/colmap/pull/2922 * Fixes for Windows ARM64 support by @ahojnnes in https://github.com/colmap/colmap/pull/2921 * Move geometry implementation of ``__repr__``, ``__eq__`` overloads to C++ side by @ahojnnes in https://github.com/colmap/colmap/pull/2915 * Consistent interface and various improvements for pycolmap/estimators by @ahojnnes in https://github.com/colmap/colmap/pull/2923 * Exclude DetectLineSegments if LSD is disabled by @sarlinpe in https://github.com/colmap/colmap/pull/2927 * Enable reading 16bit/channel (png) images to grayscale by @Ediolot in https://github.com/colmap/colmap/pull/2924 * Cleanup of remaining pycolmap interfaces by @ahojnnes in https://github.com/colmap/colmap/pull/2925 * Fix affine SIFT feature orientation detection by @ahojnnes in https://github.com/colmap/colmap/pull/2929 * Improvements to deprecated pycolmap members by @sarlinpe in https://github.com/colmap/colmap/pull/2932 * Fix pkgconf installation in Mac CI by @ahojnnes in https://github.com/colmap/colmap/pull/2936 * Make sphinx show the pycolmap constructors by @sarlinpe in https://github.com/colmap/colmap/pull/2935 * Bind synthetic dataset functionality in pycolmap by @ahojnnes in https://github.com/colmap/colmap/pull/2938 * Cleaner import of C++ symbols by @sarlinpe in https://github.com/colmap/colmap/pull/2933 * Fix pycolmap breakage for Python 3.8 by @sarlinpe in https://github.com/colmap/colmap/pull/2941 * Remove legacy boost test macro by @ahojnnes in https://github.com/colmap/colmap/pull/2940 * Drop support for VS 2019 CI checks by @ahojnnes in https://github.com/colmap/colmap/pull/2943 * Fix CI cache thrashing by inconsistent vcpkg binary caching by @ahojnnes in https://github.com/colmap/colmap/pull/2942 * Introduce gmock Eigen matrix matchers by @ahojnnes in https://github.com/colmap/colmap/pull/2939 * Prevent double initialization of glog for <=0.5 by @sarlinpe in https://github.com/colmap/colmap/pull/2945 * Fixes and refactoring for bundle adjustment covariance estimation by @ahojnnes in https://github.com/colmap/colmap/pull/2788 * Fix duplicate library warnings in linking stage by @ahojnnes in https://github.com/colmap/colmap/pull/2871 * Add test for Python mapping pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/2946 * Add helper script for incremental pycolmap build by @ahojnnes in https://github.com/colmap/colmap/pull/2947 * Fix and consistently define Qt window flags by @ahojnnes in https://github.com/colmap/colmap/pull/2949 * Cross platform usage of monospace font by @ahojnnes in https://github.com/colmap/colmap/pull/2950 * Update to latest pybind11 version by @ahojnnes in https://github.com/colmap/colmap/pull/2952 * Update install instructions for Mac using homebrew by @ahojnnes in https://github.com/colmap/colmap/pull/2953 ------------------------ COLMAP 3.10 (07/23/2024) ------------------------ * Add missing "include " needed for unique_ptr by @Tobias-Fischer in https://github.com/colmap/colmap/pull/2338 * Support decoding multi-byte characters in Python script by @jot-jt in https://github.com/colmap/colmap/pull/2344 * Split Dockerfile in two stages: builder and runtime. by @pablospe in https://github.com/colmap/colmap/pull/2347 * Dockerfile improvements by @pablospe in https://github.com/colmap/colmap/pull/2356 * Update VCPKG commit in Windows CI by @sarlinpe in https://github.com/colmap/colmap/pull/2365 * Simplify the creation of reprojection error cost functions by @sarlinpe in https://github.com/colmap/colmap/pull/2364 * Migrate pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/2367 * Rename master -> main in pycolmap CI by @sarlinpe in https://github.com/colmap/colmap/pull/2370 * Bind SetPRNGSeed by @sarlinpe in https://github.com/colmap/colmap/pull/2369 * Encapsulate freeimage usage from pycolmap in colmap bitmap by @ahojnnes in https://github.com/colmap/colmap/pull/2372 * Re-generate version info on git changes by @ahojnnes in https://github.com/colmap/colmap/pull/2373 * Consolidate colmap/pycolmap readmes, updated acknowledgements, etc. by @ahojnnes in https://github.com/colmap/colmap/pull/2374 * Fix crashing pycolmap CI on Windows by @sarlinpe in https://github.com/colmap/colmap/pull/2383 * Add costs for pose graph optimization by @sarlinpe in https://github.com/colmap/colmap/pull/2378 * Switch to exception checks - v2 by @sarlinpe in https://github.com/colmap/colmap/pull/2376 * Cleanup checks in pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/2388 * Add RigReprojErrorConstantRigCostFunction by @sarlinpe in https://github.com/colmap/colmap/pull/2377 * Add cost functions to pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/2393 * Fix warning C4722 by @whuaegeanse in https://github.com/colmap/colmap/pull/2391 * Move reconstruction IO utils to a new file by @sarlinpe in https://github.com/colmap/colmap/pull/2399 * Acquire the GIL before returning None by @sarlinpe in https://github.com/colmap/colmap/pull/2400 * Disentangle the controller from threading and integrate the new logic into IncrementalMapperController by @B1ueber2y in https://github.com/colmap/colmap/pull/2392 * Simplify the low-level triangulation API by @sarlinpe in https://github.com/colmap/colmap/pull/2402 * Initialize glog in pycolmap only if not already done by @sarlinpe in https://github.com/colmap/colmap/pull/2405 * Adapt all the controllers to inherit from BaseController rather than Thread (except for feature extraction and matching) by @B1ueber2y in https://github.com/colmap/colmap/pull/2406 * Update path to models.h in database docs by @diffner in https://github.com/colmap/colmap/pull/2412 * Migrate Ubuntu CI pipelines from ADO to Github by @ahojnnes in https://github.com/colmap/colmap/pull/2411 * Build wheels for Python 3.12 by @sarlinpe in https://github.com/colmap/colmap/pull/2416 * Migrate MacOS CI pipeline from ADO to Github by @ahojnnes in https://github.com/colmap/colmap/pull/2418 * Improve bindings of Database by @sarlinpe in https://github.com/colmap/colmap/pull/2413 * Migrate Windows CI pipeline from ADO to Github by @ahojnnes in https://github.com/colmap/colmap/pull/2419 * Reduce logging during incremental mapping by @sarlinpe in https://github.com/colmap/colmap/pull/2420 * Migrate Docker CI from ADO to Github, remove ADO pipelines by @ahojnnes in https://github.com/colmap/colmap/pull/2422 * Simplify IncrementalMapperController by @sarlinpe in https://github.com/colmap/colmap/pull/2421 * Fix for glog 0.7.0 by @sarlinpe in https://github.com/colmap/colmap/pull/2428 * Fix typo by @whuaegeanse in https://github.com/colmap/colmap/pull/2430 * Fix RunMapper by @whuaegeanse in https://github.com/colmap/colmap/pull/2431 * Do triangulation in the IncrementalMapperController by @sarlinpe in https://github.com/colmap/colmap/pull/2429 * Only push a new Docker image on release by @sarlinpe in https://github.com/colmap/colmap/pull/2436 * model aligner with type "custom" does not update reconstruction by @lpanaf in https://github.com/colmap/colmap/pull/2433 * Define vcpkg manifest by @ahojnnes in https://github.com/colmap/colmap/pull/2426 * Fix ordering of keyword arguments in pycolmap.rig_absolute_pose_estimation by @sarlinpe in https://github.com/colmap/colmap/pull/2440 * Reduce the build time of pycolmap by @sarlinpe in https://github.com/colmap/colmap/pull/2443 * Improve bindings of CorrespondenceGraph by @sarlinpe in https://github.com/colmap/colmap/pull/2476 * Bind Reconstruction::{SetUp,ImagePairStats} by @sarlinpe in https://github.com/colmap/colmap/pull/2477 * Add bindings for substeps of incremental mapper with a python example by @B1ueber2y in https://github.com/colmap/colmap/pull/2478 * Debug crashing VCPKG-based CI builds by @sarlinpe in https://github.com/colmap/colmap/pull/2508 * Upgrade to pybind11 v2.12. Fix bind_map and reconstruction.points3D by @B1ueber2y in https://github.com/colmap/colmap/pull/2502 * Minor fix on logging for the pycolmap customized runner by @B1ueber2y in https://github.com/colmap/colmap/pull/2503 * Fix missing public link deps, break circular feature-scene dependency by @ahojnnes in https://github.com/colmap/colmap/pull/2497 * Avoid duplicate image allocation during undistortion by @fseegraeber in https://github.com/colmap/colmap/pull/2520 * Fix reconstruction.points3D by @B1ueber2y in https://github.com/colmap/colmap/pull/2523 * Fix 'std::out_of_range' error when using hierarchical_mapper by @GrayMask in https://github.com/colmap/colmap/pull/2526 * Fix binding for std::vector by @sarlinpe in https://github.com/colmap/colmap/pull/2533 * Include pybind eigen header by @tmnku in https://github.com/colmap/colmap/pull/2510 * Fix pycolmap python pipeline for multiple models by @B1ueber2y in https://github.com/colmap/colmap/pull/2531 * make two view geometry writable by @tmnku in https://github.com/colmap/colmap/pull/2540 * Customized python interface for bundle adjustment by @B1ueber2y in https://github.com/colmap/colmap/pull/2509 * Fix typos by @MaximSmolskiy in https://github.com/colmap/colmap/pull/2553 * Implicitly convert iterator to ListPoint2D by @sarlinpe in https://github.com/colmap/colmap/pull/2558 * Fix model_cropper not resetting image.num_points3D of cropped_rec by @ArneSchulzTUBS in https://github.com/colmap/colmap/pull/2557 * Split pair generation and matching by @sarlinpe in https://github.com/colmap/colmap/pull/2573 * Add ObservationManager by @sarlinpe in https://github.com/colmap/colmap/pull/2575 * Log info about created feature extractor/matcher types by @ahojnnes in https://github.com/colmap/colmap/pull/2579 * LSD: making the AGPL dependency optional by @zap150 in https://github.com/colmap/colmap/pull/2578 * Disable LSD when building pycolmap wheels by @sarlinpe in https://github.com/colmap/colmap/pull/2580 * Synthesize full two-view geometry and raw matches by @ahojnnes in https://github.com/colmap/colmap/pull/2595 * Support Adjoint matrix computation for Rigid3d by @B1ueber2y in https://github.com/colmap/colmap/pull/2598 * Fix cost functions for pose graph optimization by @B1ueber2y in https://github.com/colmap/colmap/pull/2601 * Fix python bundle adjustment example with pyceres by @B1ueber2y in https://github.com/colmap/colmap/pull/2606 * Faster homography estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2603 * Add function to find real cubic polynomial roots by @ahojnnes in https://github.com/colmap/colmap/pull/2609 * Align with the convention of ceres doc on SqrtInformation. by @B1ueber2y in https://github.com/colmap/colmap/pull/2611 * Faster 7-point fundamental matrix estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2612 * Faster 8-point fundamental matrix estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2613 * Covariance estimation for bundle adjustment with Schur elimination by @B1ueber2y in https://github.com/colmap/colmap/pull/2610 * Mac OS improvements by @BSVogler in https://github.com/colmap/colmap/pull/2622 * Update cibuildwheel to 2.19.2 by @ahojnnes in https://github.com/colmap/colmap/pull/2632 * Faster essential matrix estimators by @ahojnnes in https://github.com/colmap/colmap/pull/2618 * Remove CamFromWorldPrior and create LocationPrior by @sarlinpe in https://github.com/colmap/colmap/pull/2620 * Add option to disable uninstall target, restore CI pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/2634 * Faster covariance computation for small blocks by @B1ueber2y in https://github.com/colmap/colmap/pull/2633 * Fix optimal point algorithm by @morrishelle in https://github.com/colmap/colmap/pull/2640 * Add shell script helper for profiling by @ahojnnes in https://github.com/colmap/colmap/pull/2635 * Declare PosePrior::IsValid as const by @ahojnnes in https://github.com/colmap/colmap/pull/2653 * Add CI build for Windows CUDA by @ahojnnes in https://github.com/colmap/colmap/pull/2651 * Publish windows binaries from CI by @ahojnnes in https://github.com/colmap/colmap/pull/2663 ------------------------- COLMAP 3.9.1 (01/08/2024) ------------------------- * Version 3.9 changelog by @ahojnnes in https://github.com/colmap/colmap/pull/2325 * Fully encapsulate freeimage in bitmap library (#2332) by @ahojnnes in https://github.com/colmap/colmap/pull/2334 ----------------------- COLMAP 3.9 (01/06/2024) ----------------------- * clang format all code and require clang-format-14 by @ahojnnes in https://github.com/colmap/colmap/pull/1785 * Fix compilation for vcpkg windows build by @ahojnnes in https://github.com/colmap/colmap/pull/1791 * Increment version number to 3.9 by @ahojnnes in https://github.com/colmap/colmap/pull/1794 * Remove unnecessary /arch:sse2 flag for MSVC by @ahojnnes in https://github.com/colmap/colmap/pull/1798 * Updated faq.rst by @CGCooke in https://github.com/colmap/colmap/pull/1801 * Fixed mistake in code comment for OpenCV Fisheye camera by @CGCooke in https://github.com/colmap/colmap/pull/1802 * Replace deprecated cudaThreadSynchronize with cudaDeviceSynchronize by @ahojnnes in https://github.com/colmap/colmap/pull/1806 * Replace deprecated Cuda texture references with texture objects by @ahojnnes in https://github.com/colmap/colmap/pull/1809 * Remove unused SIFT GPU cuda texture reference by @ahojnnes in https://github.com/colmap/colmap/pull/1823 * Upgrade SiftGPU to use CUDA texture objects by @ahojnnes in https://github.com/colmap/colmap/pull/1838 * Remove PBA as bundle adjustment backend to support CUDA 12+ by @ahojnnes in https://github.com/colmap/colmap/pull/1840 * Replace deprecated CUDA sature function call by @ahojnnes in https://github.com/colmap/colmap/pull/1841 * Avoid unnecessary mallocs during sampling by @ahojnnes in https://github.com/colmap/colmap/pull/1842 * Cleaned up docker readme and scripts by @ahojnnes in https://github.com/colmap/colmap/pull/1852 * add "Shared intrinsics per sub-folder" checkbox to automatic reconstruction window by @kenshi84 in https://github.com/colmap/colmap/pull/1853 * Update vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/1925 * Log the name of the file that causes Mat::Read() to checkfail by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1923 * check Z_index correctly in ReadPly by @countywest in https://github.com/colmap/colmap/pull/1896 * Don't re-open files when reading and writing matrices by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1926 * Update vcpkg to latest commit by @ahojnnes in https://github.com/colmap/colmap/pull/1948 * Remove unnecessary custom Eigen aligned allocator macros by @ahojnnes in https://github.com/colmap/colmap/pull/1947 * Prefix internal sources/includes with colmap by @ahojnnes in https://github.com/colmap/colmap/pull/1949 * Simplify clang-format config and sort includes by @ahojnnes in https://github.com/colmap/colmap/pull/1950 * Handle possible overflow in median function by @ahojnnes in https://github.com/colmap/colmap/pull/1951 * Run ASan pipeline under Ubuntu 22.04 by @ahojnnes in https://github.com/colmap/colmap/pull/1952 * Fix Ceres version test by @drkoller in https://github.com/colmap/colmap/pull/1954 * Fix deprecation warning for Qt font metrics width by @ahojnnes in https://github.com/colmap/colmap/pull/1958 * Setup clang-tidy and enable perf warnings by @ahojnnes in https://github.com/colmap/colmap/pull/1959 * VCPKG binary caching for windows CI by @ahojnnes in https://github.com/colmap/colmap/pull/1957 * Cosmetics for VS dev shell script by @ahojnnes in https://github.com/colmap/colmap/pull/1965 * Enable clang-tidy concurrency checks by @ahojnnes in https://github.com/colmap/colmap/pull/1967 * [Bug] fix finding shared points3D in FindLocalBundle by @wesleyliwei in https://github.com/colmap/colmap/pull/1963 * Enable compiler caching in CI by @ahojnnes in https://github.com/colmap/colmap/pull/1972 * Set number of features for different quality levels by @ahojnnes in https://github.com/colmap/colmap/pull/1975 * Specify parameter name using inline comment by @ahojnnes in https://github.com/colmap/colmap/pull/1976 * Fix Windows CCache by @ahojnnes in https://github.com/colmap/colmap/pull/1977 * Add e2e tests in CI pipeline using ETH3D datasets by @ahojnnes in https://github.com/colmap/colmap/pull/1397 * [feature] print verbose information for model analyzer by @wesleyliwei in https://github.com/colmap/colmap/pull/1978 * Add a missing include to compile with gcc13 by @EstebanDugueperoux2 in https://github.com/colmap/colmap/pull/1984 * Speed up snapshot construct in RigBundleAdjuster by @wesleyliwei in https://github.com/colmap/colmap/pull/1988 * Update outdated docker cuda image tag by @ahojnnes in https://github.com/colmap/colmap/pull/1992 * Add boulders ETH3D dataset to CI E2E tests by @ahojnnes in https://github.com/colmap/colmap/pull/1991 * Update executable paths in documentation by @ahojnnes in https://github.com/colmap/colmap/pull/1993 * Avoid unnecessary copy in ExtractTopScaleFeatures by @ahojnnes in https://github.com/colmap/colmap/pull/1994 * Move related code under new image library folder by @ahojnnes in https://github.com/colmap/colmap/pull/1995 * Move related code under new camera folder by @ahojnnes in https://github.com/colmap/colmap/pull/1996 * Added a virtual destructor to Sampler by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/2000 * Add a few more clang-tidy checks by @ahojnnes in https://github.com/colmap/colmap/pull/2001 * Move related code to new geometry module by @ahojnnes in https://github.com/colmap/colmap/pull/2006 * Use #pragma once as include guard by @ahojnnes in https://github.com/colmap/colmap/pull/2007 * Add bugprone-* clang-tidy checks by @ahojnnes in https://github.com/colmap/colmap/pull/2010 * Avoid const params in declarations by @ahojnnes in https://github.com/colmap/colmap/pull/2011 * Set and require C++14 by @ahojnnes in https://github.com/colmap/colmap/pull/2012 * Cleanup math functions that are now part of eigen/stdlib by @ahojnnes in https://github.com/colmap/colmap/pull/2013 * Add clang-analyzer checks by @ahojnnes in https://github.com/colmap/colmap/pull/2014 * Replace CMake provided find_package scripts and modern CMake targets by @ahojnnes in https://github.com/colmap/colmap/pull/2016 * Switch from Boost unit tests to Gtest by @ahojnnes in https://github.com/colmap/colmap/pull/2017 * Fix ccache restore keys in pipeline caching by @ahojnnes in https://github.com/colmap/colmap/pull/2018 * Add missing cacheHitVar to fix ccache by @ahojnnes in https://github.com/colmap/colmap/pull/2020 * Add missing Boost::graph import by @sarlinpe in https://github.com/colmap/colmap/pull/2021 * Compressed/flattened correspondence graph for faster triangulation / less memory by @ahojnnes in https://github.com/colmap/colmap/pull/2019 * Fix window ccache key by @ahojnnes in https://github.com/colmap/colmap/pull/2024 * Consistently use shared_ptr for shared pointers for SFM objects by @ahojnnes in https://github.com/colmap/colmap/pull/2023 * Remove check on Qt version by @sarlinpe in https://github.com/colmap/colmap/pull/2022 * Synthetics for E2E incremental mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2025 * New math module by @ahojnnes in https://github.com/colmap/colmap/pull/2028 * Simplify similarity transform and more tests by @ahojnnes in https://github.com/colmap/colmap/pull/2030 * Extract reconstruction alignment functions into new file by @ahojnnes in https://github.com/colmap/colmap/pull/2032 * Add E2E hierarchical mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2033 * Rename SimilarityTransform3 to Sim3d by @ahojnnes in https://github.com/colmap/colmap/pull/2034 * Add Rigid3d transform class by @ahojnnes in https://github.com/colmap/colmap/pull/2035 * Consolidate and simplify Rigid3d and Sim3d by @ahojnnes in https://github.com/colmap/colmap/pull/2037 * Some small improvements/cleanup for rigid3d/sim3d usage by @ahojnnes in https://github.com/colmap/colmap/pull/2041 * CamFromWorld replaces qvec/tvec by @ahojnnes in https://github.com/colmap/colmap/pull/2039 * Retry download of ETH3D datasets by @ahojnnes in https://github.com/colmap/colmap/pull/2043 * WorldToImage becomes CamToImg by @ahojnnes in https://github.com/colmap/colmap/pull/2044 * Camera models operate on camera rays by @ahojnnes in https://github.com/colmap/colmap/pull/2045 * Ignore directory .vs by @whuaegeanse in https://github.com/colmap/colmap/pull/2046 * Use the reference of Rigid3d to reduce memory consumption by @whuaegeanse in https://github.com/colmap/colmap/pull/2047 * Inline point to image projection by @ahojnnes in https://github.com/colmap/colmap/pull/2050 * Point2D becomes simpler pure data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2051 * Use Eigen math for estimator utils by @ahojnnes in https://github.com/colmap/colmap/pull/2052 * Move cost functions under geometry module and rename by @ahojnnes in https://github.com/colmap/colmap/pull/2053 * Bundle adjuster is an estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2054 * Remaining base targets move to new scene module by @ahojnnes in https://github.com/colmap/colmap/pull/2055 * Vote and verify improvements/speedup by @ahojnnes in https://github.com/colmap/colmap/pull/2056 * Generate version info in .cc file to reduce number of recompilations by @ahojnnes in https://github.com/colmap/colmap/pull/2057 * Option manager moves to controllers to disentangle circular deps by @ahojnnes in https://github.com/colmap/colmap/pull/2058 * Granular CMake modules and build targets by @ahojnnes in https://github.com/colmap/colmap/pull/2059 * Fix docker build by @ahojnnes in https://github.com/colmap/colmap/pull/2069 * Remove warnings about duplicated marco NOMINMAX by @whuaegeanse in https://github.com/colmap/colmap/pull/2067 * lib folder becomes thirdparty folder by @ahojnnes in https://github.com/colmap/colmap/pull/2068 * Remove unnecessary checks in image pair conversion by @ahojnnes in https://github.com/colmap/colmap/pull/2074 * Replace flaky ETH3D terrace with courtyard dataset by @ahojnnes in https://github.com/colmap/colmap/pull/2075 * Synthesize chained match graph for more mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2076 * Introduce abstract feature extractor by @ahojnnes in https://github.com/colmap/colmap/pull/2077 * Avoid unnecessary data copies in feature conversion utils by @ahojnnes in https://github.com/colmap/colmap/pull/2078 * Abstract feature matcher by @ahojnnes in https://github.com/colmap/colmap/pull/2082 * Encapsulate feature matching controller/worker implementations by @ahojnnes in https://github.com/colmap/colmap/pull/2085 * Some cosmetics for util/feature types by @ahojnnes in https://github.com/colmap/colmap/pull/2084 * Use std:: when cmath included by @whuaegeanse in https://github.com/colmap/colmap/pull/2081 * Encapsulate feature extraction controller/worker implementations by @ahojnnes in https://github.com/colmap/colmap/pull/2086 * Reenable VS2022 CI pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1689 * Consistent transform convention for CenterAndNormalizeImagePoints by @ahojnnes in https://github.com/colmap/colmap/pull/2092 * Retire Mac 11 CI build by @ahojnnes in https://github.com/colmap/colmap/pull/2094 * Add ReprojErrorConstantPoint3DCostFunction to speed up the RefineAbsolutePose function by @whuaegeanse in https://github.com/colmap/colmap/pull/2089 * Numeric differentiation of camera model using partial piv LU by @ahojnnes in https://github.com/colmap/colmap/pull/2100 * cmake: add testing.cc to colmap_util only if TESTS_ENABLED=ON by @NeroBurner in https://github.com/colmap/colmap/pull/2102 * Set CUDA_STANDARD to 14 by @ahojnnes in https://github.com/colmap/colmap/pull/2108 * Transform back to existing images positions after mapper processing if set fixed by @ferreram in https://github.com/colmap/colmap/pull/2095 * Update documentation with new branch policy by @ahojnnes in https://github.com/colmap/colmap/pull/2110 * Update CMake find dependencies for vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/2116 * Decouple SIFT match from two view geometry options by @ahojnnes in https://github.com/colmap/colmap/pull/2118 * Fix docker build by @vnmsklnk in https://github.com/colmap/colmap/pull/2122 * Trigger build pipeline on main branch by @ahojnnes in https://github.com/colmap/colmap/pull/2123 * Update Linux install documentation with new branch policy by @joshuaoreilly in https://github.com/colmap/colmap/pull/2126 * Fix link in camera model documentation by @CFretter in https://github.com/colmap/colmap/pull/2152 * [Bugfix] Fix GUI_ENABLED=OFF and skip SiftGPU if no GUI and no CUDA by @sarlinpe in https://github.com/colmap/colmap/pull/2151 * [Bugfix] Properly handle CGAL_ENABLED by @sarlinpe in https://github.com/colmap/colmap/pull/2149 * Refinement of intrinsics in the point_triangulator by @tsattler in https://github.com/colmap/colmap/pull/2144 * Bugfix in handling COLMAP_GPU_ENABLED by @sarlinpe in https://github.com/colmap/colmap/pull/2163 * Expose exe as libs by @sarlinpe in https://github.com/colmap/colmap/pull/2165 * Add Sim3d::FromMatrix by @sarlinpe in https://github.com/colmap/colmap/pull/2147 * Check code format in CI by @ahojnnes in https://github.com/colmap/colmap/pull/2171 * Clean up dependencies by @sarlinpe in https://github.com/colmap/colmap/pull/2173 * Move tests into anonymous namespaces by @ahojnnes in https://github.com/colmap/colmap/pull/2175 * Fix glew/qopengl conflict warning by @ahojnnes in https://github.com/colmap/colmap/pull/2176 * Update documentation with new link to GitHub discussions by @ahojnnes in https://github.com/colmap/colmap/pull/2177 * Restore GLEW include by @sarlinpe in https://github.com/colmap/colmap/pull/2178 * Align reconstructions via shared 3D points by @sarlinpe in https://github.com/colmap/colmap/pull/2169 * Add clang-tidy-cachein CI by @ahojnnes in https://github.com/colmap/colmap/pull/2182 * Disable GUI build in one CI config by @ahojnnes in https://github.com/colmap/colmap/pull/2181 * Show verbose ccache stats by @ahojnnes in https://github.com/colmap/colmap/pull/2183 * Add EstimateGeneralizedAbsolutePose by @sarlinpe in https://github.com/colmap/colmap/pull/2174 * Fix bug in ReconstructionManagerWidget::Update by @whuaegeanse in https://github.com/colmap/colmap/pull/2186 * Fix missing retrieval dependency by @ahojnnes in https://github.com/colmap/colmap/pull/2189 * Removing clustering_options and mapper_options in Hierarchical Mapper Controller by @Serenitysmk in https://github.com/colmap/colmap/pull/2193 * Publish docker image to docker hub by @ahojnnes in https://github.com/colmap/colmap/pull/2195 * Fix Cuda architecture in docker build by @ahojnnes in https://github.com/colmap/colmap/pull/2196 * Fix all-major cuda arch missing in CMake < 3.23 by @ahojnnes in https://github.com/colmap/colmap/pull/2197 * Update triangulation.cc by @RayShark0605 in https://github.com/colmap/colmap/pull/2205 * Update author and acknowledgements by @ahojnnes in https://github.com/colmap/colmap/pull/2207 * Code formatting for Python by @ahojnnes in https://github.com/colmap/colmap/pull/2208 * Retire outdated build script by @ahojnnes in https://github.com/colmap/colmap/pull/2217 * Remove mention of deprecated build script by @sarlinpe in https://github.com/colmap/colmap/pull/2220 * Improve word spelling by @zchrissirhcz in https://github.com/colmap/colmap/pull/2235 * Stack allocate camera param idx arrays by @ahojnnes in https://github.com/colmap/colmap/pull/2234 * fix: typo in colmap/src/colmap/ui/project_widget.cc by @varundhand in https://github.com/colmap/colmap/pull/2241 * Update reconstruction.cc by @RayShark0605 in https://github.com/colmap/colmap/pull/2238 * Update to Docker CUDA 12.2.2 by @ahojnnes in https://github.com/colmap/colmap/pull/2244 * Stop setting C++ standard flags manually by @AdrianBunk in https://github.com/colmap/colmap/pull/2251 * Setting clear_points to true per default in point_triangulator by @tsattler in https://github.com/colmap/colmap/pull/2252 * Update cameras.rst to fix link to code by @tsattler in https://github.com/colmap/colmap/pull/2246 * Fix matching of imported features without descriptors by @ahojnnes in https://github.com/colmap/colmap/pull/2269 * Consistent versioning between documentation and code by @ahojnnes in https://github.com/colmap/colmap/pull/2275 * Reduce mallocs for RANSAC estimator models by @ahojnnes in https://github.com/colmap/colmap/pull/2283 * Migrate to glog logging by @ahojnnes in https://github.com/colmap/colmap/pull/2172 * Turn Point3D into simple data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2285 * Camera becomes simple data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2286 * Recover custom Eigen std::vector allocator for Eigen <3.4 support by @ahojnnes in https://github.com/colmap/colmap/pull/2293 * Replace result_of with invoke_result_t by @sarlinpe in https://github.com/colmap/colmap/pull/2300 * Allow getters FocalLength{X,Y} for isotropic models by @sarlinpe in https://github.com/colmap/colmap/pull/2301 * Add missing Boost targets and cleanup includes by @sarlinpe in https://github.com/colmap/colmap/pull/2304 * Expose IncrementalMapperOptions::{mapper,triangulation} by @sarlinpe in https://github.com/colmap/colmap/pull/2308 * Update install instructions for Mac by @Dawars in https://github.com/colmap/colmap/pull/2310 * Remove unused ceres reference in doc by @ahojnnes in https://github.com/colmap/colmap/pull/2315 * Fix typo by @whuaegeanse in https://github.com/colmap/colmap/pull/2317 * Stable version 3.9 release by @ahojnnes in https://github.com/colmap/colmap/pull/2319 ----------------------- COLMAP 3.8 (01/31/2023) ----------------------- * Updating geo-registration doc. by @ferreram in https://github.com/colmap/colmap/pull/1410 * Adding user-specified option for reconstructing purely planar scene. … by @ferreram in https://github.com/colmap/colmap/pull/1408 * Only apply sqlite vacuum command when elements are deleted from the database. by @ferreram in https://github.com/colmap/colmap/pull/1414 * Replace Graclus with Metis dependency by @ahojnnes in https://github.com/colmap/colmap/pull/1422 * Update ceres download URL in build script by @whuaegeanse in https://github.com/colmap/colmap/pull/1430 * Fix type errors when building colmap with build.py in windows by @whuaegeanse in https://github.com/colmap/colmap/pull/1440 * Fix bug in the computation of the statistics Global/Local BA by @whuaegeanse in https://github.com/colmap/colmap/pull/1449 * Add RefineGeneralizedAbsolutePose and covariance estimation by @Skydes in https://github.com/colmap/colmap/pull/1464 * Update docker image definition by @ahojnnes in https://github.com/colmap/colmap/pull/1478 * Upgrade deprecated ceres parameterizations to manifolds by @ahojnnes in https://github.com/colmap/colmap/pull/1477 * Use masks for stereo fusion on automatic reconstruction by @ibrarmalik in https://github.com/colmap/colmap/pull/1488 * fix random seed set failed from external interface by @WZG3661 in https://github.com/colmap/colmap/pull/1498 * Replace deprecated Eigen nonZeros() call for most recent Eigen versions. by @nackjaylor in https://github.com/colmap/colmap/pull/1494 * Fix ceres-solver folder name by @f-fl0 in https://github.com/colmap/colmap/pull/1501 * Improved convergence criterion for XYZ to ELL conversion by @ahojnnes in https://github.com/colmap/colmap/pull/1505 * Fix bug in the function SetPtr of Bitmap by @whuaegeanse in https://github.com/colmap/colmap/pull/1525 * Avoid the calling of copy constructor/assignment by @whuaegeanse in https://github.com/colmap/colmap/pull/1524 * Avoid calling copy constructors of FeatureKeypoints and FeatureDescriptors by @whuaegeanse in https://github.com/colmap/colmap/pull/1540 * Initialize freeimage if statically linked by @ahojnnes in https://github.com/colmap/colmap/pull/1549 * Avoid hard crash if Jacobian matrix is rank deficient by @mihaidusmanu in https://github.com/colmap/colmap/pull/1557 * visualize_model.py: added FULL_OPENCV model by @soeroesg in https://github.com/colmap/colmap/pull/1552 * Update vcpkg version to fix CI pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1568 * Replace deprecated Mac OS 10.15 with Mac OS 12 build in CI by @ahojnnes in https://github.com/colmap/colmap/pull/1569 * Fix inconsistent between the actual executed image reader option and the saved project.ini file by @XuChengHUST in https://github.com/colmap/colmap/pull/1564 * checkout the expected version of ceres solver by @scott-vsi in https://github.com/colmap/colmap/pull/1576 * use default qt5 brew install directory #1573 by @catapulta in https://github.com/colmap/colmap/pull/1574 * Fix image undistortion with nested image folders by @ahojnnes in https://github.com/colmap/colmap/pull/1606 * Fix source file permissions by @ahojnnes in https://github.com/colmap/colmap/pull/1607 * Fixed the collection of arguments in colmap.bat by @tdegraaff in https://github.com/colmap/colmap/pull/1121 * Add OpenMP to COLMAP_EXTERNAL_LIBRARIES if enabled by @logchan in https://github.com/colmap/colmap/pull/1632 * Fix output tile reconstructions are the same as the input reconstruction in `RunModelSplitter` (#1513) by @Serenitysmk in https://github.com/colmap/colmap/pull/1531 * add `libmetis-dev` to solve `METIS_INCLUDE_DIRS`. by @FavorMylikes in https://github.com/colmap/colmap/pull/1672 * Update install.rst by @tomer-grin in https://github.com/colmap/colmap/pull/1671 * Update freeimage links. by @Yulv-git in https://github.com/colmap/colmap/pull/1675 * fix small typo by @skal65535 in https://github.com/colmap/colmap/pull/1668 * Update build.py with new glew link by @aghand0ur in https://github.com/colmap/colmap/pull/1658 * Add use_cache in fusion options GUI by @hrflr in https://github.com/colmap/colmap/pull/1655 * Add CI pipeline for Ubuntu 22.04 by @ahojnnes in https://github.com/colmap/colmap/pull/1688 * Avoid unnecessary copies of data by @ahojnnes in https://github.com/colmap/colmap/pull/1691 * Reduce memory allocations in correspondence graph search by @ahojnnes in https://github.com/colmap/colmap/pull/1692 * Use FindCUDAToolkit when available. by @hanseuljun in https://github.com/colmap/colmap/pull/1693 * Fixed a crash due to inconsistent undistortion by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1698 * Add CUDA Ubuntu 22.04 CI build by @ahojnnes in https://github.com/colmap/colmap/pull/1705 * Delete the redundancy install of libmetis-dev by @thomas-graphopti in https://github.com/colmap/colmap/pull/1721 * Fix broken loading of image masks on macOS by @buesma in https://github.com/colmap/colmap/pull/1639 * Update install instructions with latest hints and known issues by @ahojnnes in https://github.com/colmap/colmap/pull/1736 * Modernize smart pointer initialization, fix alloc/dealloc mismatch by @ahojnnes in https://github.com/colmap/colmap/pull/1737 * Fix typo in cli.rst by @ojhernandez in https://github.com/colmap/colmap/pull/1747 * Fix inconsistent image resizing between CPU/GPU implementations of SIFT by @Yzhbuaa in https://github.com/colmap/colmap/pull/1642 * Reduce number of SIFT test features to make tests run under WSL by @ahojnnes in https://github.com/colmap/colmap/pull/1748 * Tag documentation version with dev by @ahojnnes in https://github.com/colmap/colmap/pull/1749 * Update copyright to 2023 by @ahojnnes in https://github.com/colmap/colmap/pull/1750 * Fix max image dimension for positive first_octave by @ahojnnes in https://github.com/colmap/colmap/pull/1751 * Fix SIFT GPU match creation by @ahojnnes in https://github.com/colmap/colmap/pull/1757 * Fix SIFT tests for OpenGL by @ahojnnes in https://github.com/colmap/colmap/pull/1762 * Suppress CUDA stack size warning for ptxas by @ahojnnes in https://github.com/colmap/colmap/pull/1770 * Simplify CUDA CMake configuration by @ahojnnes in https://github.com/colmap/colmap/pull/1776 * Fixes for CUDA compilation by @ahojnnes in https://github.com/colmap/colmap/pull/1777 * Improvements to dockerfile and build pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1778 * Explicitly require CMAKE_CUDA_ARCHITECTURES to be defined by @ahojnnes in https://github.com/colmap/colmap/pull/1781 * Depend on system installed FLANN by @ahojnnes in https://github.com/colmap/colmap/pull/1782 * Option to store relative pose between two cameras in database by @yanxke in https://github.com/colmap/colmap/pull/1774 * Depend on system installed SQLite3 by @ahojnnes in https://github.com/colmap/colmap/pull/1783 ----------------------- COLMAP 3.7 (01/26/2022) ----------------------- * Allow to save fused point cloud in colmap format when using command line by @boitumeloruf in https://github.com/colmap/colmap/pull/799 * Fix typos in image.h by @Pascal-So in https://github.com/colmap/colmap/pull/936 * Fix for EPnP estimator by @vlarsson in https://github.com/colmap/colmap/pull/943 * Visualize models using Python in Open3D by @ahojnnes in https://github.com/colmap/colmap/pull/948 * Update tutorial.rst by @ignacio-rocco in https://github.com/colmap/colmap/pull/953 * 8 point algorithm internal contraint fix by @mihaidusmanu in https://github.com/colmap/colmap/pull/982 * Python script for writing depth/normal arrays by @SBCV in https://github.com/colmap/colmap/pull/957 * BuildImageModel: use std::vector instead of numbered arguments by @Pascal-So in https://github.com/colmap/colmap/pull/949 * Fix bugs of sift feature matching by @whuaegeanse in https://github.com/colmap/colmap/pull/985 * script for modifying fused results by @SBCV in https://github.com/colmap/colmap/pull/984 * fix camera model query by @Pascal-So in https://github.com/colmap/colmap/pull/997 * fixed small bug in visualize_model.py by @sniklaus in https://github.com/colmap/colmap/pull/1007 * Update .travis.yml by @srinivas32 in https://github.com/colmap/colmap/pull/989 * Ensure DecomposeHomographyMatrix() always returns rotations by @daithimaco in https://github.com/colmap/colmap/pull/1040 * Remove deprecated qt foreach by @UncleGene in https://github.com/colmap/colmap/pull/1039 * Fix AMD/Windows GUI visualization bug by @drkoller in https://github.com/colmap/colmap/pull/1079 * include colmap_cuda in COLMAP_LIBRARIES when compiled with cuda by @ClementPinard in https://github.com/colmap/colmap/pull/1084 * Fix runtime crash when sparsesuite is missing from ceres by @anmatako in https://github.com/colmap/colmap/pull/1115 * Store relative poses in two_view_geometry table by @Ahmed-Salama in https://github.com/colmap/colmap/pull/1103 * search src images for patch_match from all set, not only referenced subset by @DaniilSNikulin in https://github.com/colmap/colmap/pull/1038 * Replace Travis CI with Azure Pipelines for Linux/Mac builds by @ahojnnes in https://github.com/colmap/colmap/pull/1119 * Allow ReadPly to handle double precision files by @anmatako in https://github.com/colmap/colmap/pull/1131 * Update GPSTransform calculations to improve accuracy by @anmatako in https://github.com/colmap/colmap/pull/1132 * Add scale template flag in SimilarityTransform3::Estimate by @anmatako in https://github.com/colmap/colmap/pull/1133 * Add CopyFile utility that can copy or hard/soft-link files by @anmatako in https://github.com/colmap/colmap/pull/1134 * Expose BA options in IncrementalMapper by @anmatako in https://github.com/colmap/colmap/pull/1139 * Allow configurable paths for mvs::Model by @anmatako in https://github.com/colmap/colmap/pull/1141 * Change ReconstructionMaanger to write larger recons first by @anmatako in https://github.com/colmap/colmap/pull/1137 * Setup Azure pipelines for Windows build by @ahojnnes in https://github.com/colmap/colmap/pull/1150 * Add fixed extrinsics in rig config by @anmatako in https://github.com/colmap/colmap/pull/1144 * Allow custom config and missing dependencies for patch-match by @anmatako in https://github.com/colmap/colmap/pull/1142 * Update print statements for Python 3 compatibility by @UncleGene in https://github.com/colmap/colmap/pull/1126 * Allow cleanup of SQLite tables using new database_cleaner command by @anmatako in https://github.com/colmap/colmap/pull/1136 * Extend SceneClustering to support non-hierarchical (flat) clusters by @anmatako in https://github.com/colmap/colmap/pull/1140 * Support more formats in model_converter by @anmatako in https://github.com/colmap/colmap/pull/1147 * Fix Mac 10.15 build due to changed Qt5 path by @ahojnnes in https://github.com/colmap/colmap/pull/1157 * Fix bug in ReadCameraRigConfig when reading extrinsics by @anmatako in https://github.com/colmap/colmap/pull/1158 * Add utility to compare poses between two sparse models by @ahojnnes in https://github.com/colmap/colmap/pull/1159 * Modularize executable main functions into separate sources by @ahojnnes in https://github.com/colmap/colmap/pull/1160 * Fix unnecessary copies in for range loops by @ahojnnes in https://github.com/colmap/colmap/pull/1162 * Add script to clang-format all source code by @ahojnnes in https://github.com/colmap/colmap/pull/1163 * Add back new options and formats for model_converter by @anmatako in https://github.com/colmap/colmap/pull/1164 * ImageReder new option and bug fix in GPS priors by @anmatako in https://github.com/colmap/colmap/pull/1146 * Parallelize stereo fusion; needs pre-loading of entire workspace by @anmatako in https://github.com/colmap/colmap/pull/1148 * Refactoring and new functionality in Reconstruction class by @anmatako in https://github.com/colmap/colmap/pull/1169 * Add new functionality in image_undistorter by @anmatako in https://github.com/colmap/colmap/pull/1168 * Add new CMake option to disable GUI by @anmatako in https://github.com/colmap/colmap/pull/1165 * Fix the memory leak caused by not releasing the memory of the PRNG at the end of the thread by @whuaegeanse in https://github.com/colmap/colmap/pull/1170 * Fix fusion segfault bug by @anmatako in https://github.com/colmap/colmap/pull/1176 * Update SiftGPU to use floorf for floats by @anmatako in https://github.com/colmap/colmap/pull/1182 * fix typo in extraction.cc by @iuk in https://github.com/colmap/colmap/pull/1191 * Improvements to NVM, Cam, Recon3D, and Bundler exporters by @drkoller in https://github.com/colmap/colmap/pull/1187 * Update model_aligner functionality by @anmatako in https://github.com/colmap/colmap/pull/1177 * Add new model_cropper and model_splitter commands by @anmatako in https://github.com/colmap/colmap/pull/1179 * use type point2D_t instead of image_t by @iuk in https://github.com/colmap/colmap/pull/1199 * Fix radial distortion in Cam format exporter by @drkoller in https://github.com/colmap/colmap/pull/1196 * Add new model_transformer command by @anmatako in https://github.com/colmap/colmap/pull/1178 * Fix error of using urllib to download eigen from gitlab by @whuaegeanse in https://github.com/colmap/colmap/pull/1194 * Multi-line string fix in Python model script by @mihaidusmanu in https://github.com/colmap/colmap/pull/1217 * added visibility_sigma to CLI input options for delaunay_mesher. by @Matstah in https://github.com/colmap/colmap/pull/1236 * Backwards compatibility of model_aligner by @tsattler in https://github.com/colmap/colmap/pull/1240 * [update undistortion] update dumped commands by @hiakru in https://github.com/colmap/colmap/pull/1276 * Compute reprojection error in generalized absolute solver by @Skydes in https://github.com/colmap/colmap/pull/1257 * Modifying scripts/python/flickr_downloader.py to create files with correct extensions by @snavely in https://github.com/colmap/colmap/pull/1275 * revise Dockerfile and readme. by @MasahiroOgawa in https://github.com/colmap/colmap/pull/1281 * Update to latest vcpkg version by @ahojnnes in https://github.com/colmap/colmap/pull/1319 * Fix compiler warnings reported by GCC by @ahojnnes in https://github.com/colmap/colmap/pull/1317 * Auto-rotate JPEG images based on EXIF orientation by @ahojnnes in https://github.com/colmap/colmap/pull/1318 * Upgrade vcpkg to fix CI build issues by @ahojnnes in https://github.com/colmap/colmap/pull/1331 * Added descriptor normalization argument to feature_extractor. by @mihaidusmanu in https://github.com/colmap/colmap/pull/1332 * Fix memory leak in the function of StringAppendV by @whuaegeanse in https://github.com/colmap/colmap/pull/1337 * Add CUDA_SAFE_CALL to cudaGetDeviceCount. by @chpatrick in https://github.com/colmap/colmap/pull/1334 * Add missing include in case CUDA/GUI is not available by @ahojnnes in https://github.com/colmap/colmap/pull/1329 * Fix wrong WGS84 model and test cases in GPSTransform by @Freeverc in https://github.com/colmap/colmap/pull/1333 * Fixes bug in sprt.cc: num_inliers was not set. by @rmbrualla in https://github.com/colmap/colmap/pull/1360 * Prevent a divide by zero corner case. by @rmbrualla in https://github.com/colmap/colmap/pull/1361 * Adds missing header. by @rmbrualla in https://github.com/colmap/colmap/pull/1362 * Require Qt in COLMAPConfig only if GUI is enabled by @Skydes in https://github.com/colmap/colmap/pull/1365 * Keep precision in the process of storing in text. by @whuaegeanse in https://github.com/colmap/colmap/pull/1363 * Expose exe internals by @Skydes in https://github.com/colmap/colmap/pull/1366 * Fix inliers matches extraction in EstimateUncalibrated function. by @ferreram in https://github.com/colmap/colmap/pull/1369 * Expose exe internals - fix by @Skydes in https://github.com/colmap/colmap/pull/1368 * Remove deprecated Mac OSX 10.14 image in ADO pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1383 * Add Mac OSX 11 ADO pipeline job by @ahojnnes in https://github.com/colmap/colmap/pull/1384 * Fix warnings for latest compiler/libraries by @ahojnnes in https://github.com/colmap/colmap/pull/1382 * Fix clang compiler warnings by @ahojnnes in https://github.com/colmap/colmap/pull/1387 * Add Address Sanitizer options and fix reported issues by @ahojnnes in https://github.com/colmap/colmap/pull/1390 * User/joschonb/asan cleanup by @ahojnnes in https://github.com/colmap/colmap/pull/1391 * Add ADO pipeline for Visual Studio 2022 by @ahojnnes in https://github.com/colmap/colmap/pull/1392 * Add ccache option by @ahojnnes in https://github.com/colmap/colmap/pull/1395 * Update ModelAligner to handle GPS and custom coords. and more by @ferreram in https://github.com/colmap/colmap/pull/1371 ----------------------- COLMAP 3.6 (07/24/2020) ----------------------- * Improved robustness and faster incremental reconstruction process * Add ``image_deleter`` command to remove images from sparse model * Add ``image_filter`` command to filter bad registrations from sparse model * Add ``point_filtering`` command to filter sparse model point clouds * Add ``database_merger`` command to merge two databases, which is useful to parallelize matching across different machines * Add ``image_undistorter_standalone`` to enable undistorting images without a pre-existing full sparse model * Improved undistortion for fisheye cameras and FOV camera model * Support for masking input images in feature extraction stage * Improved HiDPI support in GUI for high-resolution monitors * Import sparse model when launching GUI from CLI * Faster CPU-based matching using approximate NN search * Support for bundle adjustment with fixed extrinsics * Support for fixing existing images when continuing reconstruction * Camera model colors in viewer can be customized * Support for latest GPU architectures in CUDA build * Support for writing sparse models in Python scripts * Scripts for building and running COLMAP in Docker * Many more bug fixes and improvements to code and documentation ----------------------- COLMAP 3.5 (08/22/2018) ----------------------- * COLMAP is now released under the BSD license instead of the GPL * COLMAP is now installed as a library, whose headers can be included and libraries linked against from other C/C++ code * Add hierarchical mapper for parallelized reconstruction or large scenes * Add sparse and dense Delaunay meshing algorithms, which reconstruct a watertight surface using a graph cut on the Delaunay triangulation of the reconstructed sparse or dense point cloud * Improved robustness when merging different models * Improved pre-trained vocabulary trees available for download * Add COLMAP as a software entry under Linux desktop systems * Add support to compile COLMAP on ARM platforms * Add example Python script to read/write COLMAP database * Add region of interest (ROI) cropping in image undistortion * Several import bug fixes for spatial verification in image retrieval * Add more extensive continuous integration across more compilation scenarios * Many more bug fixes and improvements to code and documentation ----------------------- COLMAP 3.4 (01/29/2018) ----------------------- * Unified command-line interface: The functionality of previous executables have been merged into the ``src/exe/colmap.cc`` executable. The GUI can now be started using the command ``colmap gui`` and other commands are available as ``colmap [command]``. For example, the feature extractor is now available as ``colmap feature_extractor [args]`` while all command-line arguments stay the same as before. This should result in much faster project compile times and smaller disk space usage of the program. More details about the new interface are documented at https://colmap.github.io/cli.html * More complete depth and normal maps with larger patch sizes * Faster dense stereo computation by skipping rows/columns in patch match, improved random sampling in patch match, and faster bilateral NCC * Better high DPI screen support for the graphical user interface * Improved model viewer under Windows, which now requires Qt 5.4 * Save computed two-view geometries in database * Images (keypoint/matches visualization, depth and normal maps) can now be saved from the graphical user interface * Support for PMVS format without sparse bundler file * Faster covariant feature detection * Many more bug fixes and improvements ----------------------- COLMAP 3.3 (11/21/2017) ----------------------- * Add DSP (Domain Size Pooling) SIFT implementation. DSP-SIFT outperforms standard SIFT in most cases, as shown in "Comparative Evaluation of Hand-Crafted and Learned Local Features", Schoenberger et al., CVPR 2017 * Improved parameters dense reconstruction of smaller models * Improved compile times due to various code optimizations * Add option to specify camera model in automatic reconstruction * Add new model orientation alignment based on upright image assumption * Improved numerical stability for generalized absolute pose solver * Support for image range specification in PMVS dense reconstruction format * Support for older Python versions in automatic build script * Fix OpenCV Fisheye camera model to exactly match OpenCV specifications --------------------- COLMAP 3.2 (9/2/2017) --------------------- * Fully automatic cross-platform build script (Windows, Mac, Linux) * Add multi-GPU feature extraction if multiple CUDA devices are available * Configurable dimension and data type for vocabulary tree implementation * Add new sequential matching mode for image sequences with high frame-rate * Add generalized relative pose solver for multi-camera systems * Add sparse least absolute deviation solver * Add CPU/GPU options to automatic reconstruction tool * Add continuous integration system under Windows, Mac, Linux through Github * Many more bug fixes and improvements ---------------------- COLMAP 3.1 (6/15/2017) ---------------------- * Add fast spatial verification to image retrieval module * Add binary file format for sparse models by default. Old text format still fully compatible and possible conversion in GUI and CLI * Add cross-platform little endian binary file reading and writing * Faster and less memory hungry stereo fusion by computing consistency on demand and possible limitation of image size in fusion * Simpler geometric stereo processing interface. Now geometric stereo output can be computed using a single pass * Faster and multi-architecture CUDA compilation * Add medium quality option in automatic reconstructor * Many more bug fixes and improvements ---------------------- COLMAP 3.0 (5/22/2017) ---------------------- * Add automatic end-to-end reconstruction tool that automatically performs sparse and dense reconstruction on a given set of images * Add multi-GPU dense stereo if multiple CUDA devices are available * Add multi-GPU feature matching if multiple CUDA devices are available * Add Manhattan-world / gravity alignment using line detection * Add CUDA-based feature extraction useful for usage on clusters * Add CPU-based feature matching for machines without GPU * Add new THIN_PRISM_FISHEYE camera model with tangential/radial correction * Add binary to triangulate existing/empty sparse reconstruction * Add binary to print summary statistics about sparse reconstruction * Add transitive feature matching to transitively complete match graph * Improved scalability of dense reconstruction by using caching * More stable GPU-based feature matching with informative warnings * Faster vocabulary tree matching using dynamic scheduling in FLANN * Faster spatial feature matching using linear index instead of kd-tree * More stable camera undistortion using numerical Newton iteration * Improved option parsing with some backwards incompatible option renaming * Faster compile times by optimizing includes and CUDA flags * More stable view selection for small baseline scenario in dense reconstruction * Many more bug fixes and improvements ---------------------- COLMAP 2.1 (12/7/2016) ---------------------- * Support to only index and match specific images in vocabulary tree matching * Support to perform image retrieval using vocabulary tree * Several bug fixes and improvements for multi-view stereo module * Improved Structure-from-Motion initialization strategy * Support to only reconstruct the scene using specific images in the database * Add support to merge two models using overlapping registered images * Add support to geo-register/align models using known camera locations * Support to only extract specific images in feature extraction module * Support for snapshot model export during reconstruction * Skip already undistorted images if they exist in output directory * Support to limit the number of features in image retrieval for improved speed * Miscellaneous bug fixes and improvements --------------------- COLMAP 2.0 (9/8/2016) --------------------- * Implementation of dense reconstruction pipeline * Improved feature matching performance * New bundle adjuster for rigidly mounted multi-camera systems * New generalized absolute pose solver for multi-camera systems * New executable to extract colors from all images * Boost can now be linked in shared and static mode * Various bug fixes and performance improvements ---------------------- COLMAP 1.1 (5/19/2016) ---------------------- * Implementation of state-of-the-art image retrieval system using Hamming embedding for vocabulary tree matching. This should lead to much improved matching results as compared to the previous implementation. * Guided matching as an optional functionality. * New demo datasets for download. * Automatically switch to PBA if supported by the project. * Implementation of EPNP solver for local pose optimization in RANSAC. * Add option to extract upright SIFT features. * Saving JPEGs in superb quality by default in export. * Add option to clear matches and inlier matches in the project. * New fisheye camera models, including the FOV camera model used by Google Project Tango (Thomas Schoeps). * Extended documentation based on user feedback. * Fixed typo in documentation (Thomas Schoeps). --------------------- COLMAP 1.0 (4/4/2016) --------------------- * Initial release of COLMAP. colmap-4.0.4/CMakeLists.txt000066400000000000000000000421011517363634500155450ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. cmake_minimum_required(VERSION 3.12) ################################################################################ # Options ################################################################################ option(SIMD_ENABLED "Whether to enable SIMD optimizations" ON) option(OPENMP_ENABLED "Whether to enable OpenMP parallelization" ON) option(IPO_ENABLED "Whether to enable interprocedural optimization" ON) option(CUDA_ENABLED "Whether to enable CUDA, if available" ON) option(ONNX_ENABLED "Whether to enable ONNX, if available" ON) option(GUI_ENABLED "Whether to enable the graphical UI" ON) option(OPENGL_ENABLED "Whether to enable OpenGL, if available" ON) option(TESTS_ENABLED "Whether to build test binaries" OFF) option(COVERAGE_ENABLED "Whether to enable code coverage" OFF) option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF) option(TSAN_ENABLED "Whether to enable ThreadSanitizer flags" OFF) option(UBSAN_ENABLED "Whether to enable UndefinedBehaviorSanitizer flags" OFF) option(PROFILING_ENABLED "Whether to enable google-perftools linker flags" OFF) option(WERROR_ENABLED "Whether to treat compiler warnings as errors" OFF) option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON) option(CGAL_ENABLED "Whether to enable the CGAL library" ON) option(LSD_ENABLED "Whether to enable the LSD library" ON) option(DOWNLOAD_ENABLED "Whether to enable (automatic) download of resources (requires Curl/OpenSSL)" ON) option(UNINSTALL_ENABLED "Whether to create a target to 'uninstall' colmap" ON) option(BENCHMARK_ENABLED "Whether to enable runtime benchmarking support" OFF) option(FETCH_POSELIB "Whether to consume PoseLib using FetchContent or find_package" ON) option(FETCH_FAISS "Whether to consume faiss using FetchContent or find_package" ON) option(FETCH_ONNX "Whether to consume ONNX using FetchContent or find_package" ON) option(BUILD_SHARED_LIBS "Whether to build shared libraries (faster linktime, slower runtime)" OFF) option(ALL_SOURCE_TARGET "Whether to create a target for all source files (for Visual Studio / XCode development)" OFF) # Disables default features, as we specify each required feature manually below. list(APPEND VCPKG_MANIFEST_FEATURES "core") # Propagate options to vcpkg manifest. if(TESTS_ENABLED) list(APPEND VCPKG_MANIFEST_FEATURES "tests") endif() if(CUDA_ENABLED) list(APPEND VCPKG_MANIFEST_FEATURES "cuda") endif() if(ONNX_ENABLED AND NOT FETCH_ONNX) list(APPEND VCPKG_MANIFEST_FEATURES "onnx") endif() if(GUI_ENABLED) list(APPEND VCPKG_MANIFEST_FEATURES "gui") endif() if(CGAL_ENABLED) list(APPEND VCPKG_MANIFEST_FEATURES "cgal") endif() if(DOWNLOAD_ENABLED) list(APPEND VCPKG_MANIFEST_FEATURES "download") endif() project(COLMAP LANGUAGES C CXX) set(COLMAP_VERSION "4.0.4") set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CUDA_STANDARD 17) set(CMAKE_CUDA_STANDARD_REQUIRED ON) set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON) ################################################################################ # Include CMake dependencies ################################################################################ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(CheckCXXCompilerFlag) include(GNUInstallDirs) # Include helper macros and commands, and allow the included file to override # the CMake policies in this file include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeHelper.cmake NO_POLICY_SCOPE) # Build position-independent code, so that shared libraries can link against # COLMAP's static libraries. set(CMAKE_POSITION_INDEPENDENT_CODE ON) ################################################################################ # Dependency configuration ################################################################################ set(COLMAP_FIND_QUIETLY FALSE) include(cmake/FindDependencies.cmake) ################################################################################ # Compiler specific configuration ################################################################################ if(CMAKE_BUILD_TYPE) message(STATUS "Build type specified as ${CMAKE_BUILD_TYPE}") else() message(STATUS "Build type not specified, using Release") set(CMAKE_BUILD_TYPE Release) set(IS_DEBUG OFF) endif() if("${CMAKE_BUILD_TYPE}" STREQUAL "ClangTidy") find_program(CLANG_TIDY_EXE NAMES clang-tidy) if(NOT CLANG_TIDY_EXE) message(FATAL_ERROR "Could not find the clang-tidy executable, please set CLANG_TIDY_EXE") endif() else() unset(CLANG_TIDY_EXE) endif() if(IS_MSVC) # Some fixes for the Glog library. add_compile_definitions(GLOG_USE_GLOG_EXPORT) add_compile_definitions(GLOG_NO_ABBREVIATED_SEVERITIES) add_compile_definitions(GL_GLEXT_PROTOTYPES) add_compile_definitions(NOMINMAX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc") # Disable warning: 'initializing': conversion from 'X' to 'Y', possible loss of data set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244 /wd4267 /wd4305") # Enable object level parallel builds in Visual Studio. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /bigobj") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") endif() endif() if(IS_GNU) if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) message(FATAL_ERROR "GCC version 4.8 or older not supported") endif() endif() if(IS_MACOS) # Mitigate CMake limitation, see: https://discourse.cmake.org/t/avoid-duplicate-linking-to-avoid-xcode-15-warnings/9084/10 add_link_options(LINKER:-no_warn_duplicate_libraries) endif() # Correctly set RPATH to look up dependencies. if(NOT IS_MSVC) if(IS_MACOS) set(RPATH_BASE "@executable_path") else() set(RPATH_BASE "$ORIGIN") endif() file(RELATIVE_PATH INSTALL_LIB_DIR "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") list(APPEND CMAKE_INSTALL_RPATH "${RPATH_BASE}/${INSTALL_LIB_DIR}") endif() if(IS_DEBUG) add_compile_definitions(EIGEN_INITIALIZE_MATRICES_BY_NAN) endif() if(SIMD_ENABLED) message(STATUS "Enabling SIMD support") else() message(STATUS "Disabling SIMD support") endif() if(IPO_ENABLED AND NOT IS_DEBUG AND NOT IS_GNU) message(STATUS "Enabling interprocedural optimization") set_property(DIRECTORY PROPERTY INTERPROCEDURAL_OPTIMIZATION 1) else() message(STATUS "Disabling interprocedural optimization") endif() if(ASAN_ENABLED) message(STATUS "Enabling ASan support") if(IS_CLANG OR IS_GNU) add_compile_options(-fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope) add_link_options(-fsanitize=address) else() message(FATAL_ERROR "Unsupported compiler for ASan mode") endif() endif() if(TSAN_ENABLED) message(STATUS "Enabling TSan support") if(IS_CLANG OR IS_GNU) add_compile_options(-fsanitize=thread) add_link_options(-fsanitize=thread) else() message(FATAL_ERROR "Unsupported compiler for TSan mode") endif() endif() if(UBSAN_ENABLED) message(STATUS "Enabling UBsan support") if(IS_CLANG OR IS_GNU) add_compile_options(-fsanitize=undefined) add_link_options(-fsanitize=undefined) else() message(FATAL_ERROR "Unsupported compiler for UBsan mode") endif() endif() if(CCACHE_ENABLED) find_program(CCACHE ccache) if(CCACHE) message(STATUS "Enabling ccache support") set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) if(CUDA_ENABLED) set(CMAKE_CUDA_COMPILER_LAUNCHER ${CCACHE}) endif() else() message(STATUS "Disabling ccache support") endif() else() message(STATUS "Disabling ccache support") endif() if(PROFILING_ENABLED) message(STATUS "Enabling profiling support") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -lprofiler -ltcmalloc") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -lprofiler -ltcmalloc") else() message(STATUS "Disabling profiling support") endif() if(TESTS_ENABLED) message(STATUS "Enabling tests") enable_testing() include(CTest) else() message(STATUS "Disabling tests") endif() if(COVERAGE_ENABLED) message(STATUS "Enabling coverage support") else() message(STATUS "Disabling coverage support") endif() ################################################################################ # Add sources ################################################################################ # Generate source file with version definitions. include(GenerateVersionDefinitions) include_directories(src) link_directories(${COLMAP_LINK_DIRS}) add_subdirectory(src/thirdparty) add_subdirectory(src/colmap) if(BENCHMARK_ENABLED) add_subdirectory(benchmark/runtime) endif() ################################################################################ # Generate source groups for Visual Studio, XCode, etc. ################################################################################ COLMAP_ADD_SOURCE_DIR(src/colmap/controllers CONTROLLERS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/estimators ESTIMATORS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/estimators/cost_functions ESTIMATORS_COST_FUNCTIONS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/estimators/solvers ESTIMATORS_SOLVERS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/exe EXE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/feature FEATURE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/geometry GEOMETRY_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/image IMAGE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/math MATH_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/mvs MVS_SRCS *.h *.cc *.cu) COLMAP_ADD_SOURCE_DIR(src/colmap/optim OPTIM_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/retrieval RETRIEVAL_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/scene SCENE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/sensor SENSOR_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/sfm SFM_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/tools TOOLS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/ui UI_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/util UTIL_SRCS *.h *.cc) if(LSD_ENABLED) COLMAP_ADD_SOURCE_DIR(src/thirdparty/LSD THIRDPARTY_LSD_SRCS *.h *.c) endif() COLMAP_ADD_SOURCE_DIR(src/thirdparty/PoissonRecon THIRDPARTY_POISSON_RECON_SRCS *.h *.cpp *.inl) COLMAP_ADD_SOURCE_DIR(src/thirdparty/SiftGPU THIRDPARTY_SIFT_GPU_SRCS *.h *.cpp *.cu) COLMAP_ADD_SOURCE_DIR(src/thirdparty/VLFeat THIRDPARTY_VLFEAT_SRCS *.h *.c *.tc) # Add all of the source files to a regular library target, as using a custom # target does not allow us to set its C++ include directories (and thus # intellisense can't find any of the included files). if(ALL_SOURCE_TARGET) set(ALL_SRCS ${CONTROLLERS_SRCS} ${ESTIMATORS_SRCS} ${ESTIMATORS_COST_FUNCTIONS_SRCS} ${ESTIMATORS_SOLVERS_SRCS} ${EXE_SRCS} ${FEATURE_SRCS} ${GEOMETRY_SRCS} ${IMAGE_SRCS} ${MATH_SRCS} ${MVS_SRCS} ${OPTIM_SRCS} ${RETRIEVAL_SRCS} ${SCENE_SRCS} ${SENSOR_SRCS} ${SFM_SRCS} ${TOOLS_SRCS} ${UI_SRCS} ${UTIL_SRCS} ${THIRDPARTY_POISSON_RECON_SRCS} ${THIRDPARTY_SIFT_GPU_SRCS} ${THIRDPARTY_VLFEAT_SRCS} ) if(LSD_ENABLED) list(APPEND ALL_SRCS ${THIRDPARTY_LSD_SRCS} ) endif() add_library( ${COLMAP_SRC_ROOT_FOLDER} ${ALL_SRCS} ) # Prevent the library from being compiled automatically. set_target_properties( ${COLMAP_SRC_ROOT_FOLDER} PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1) endif() ################################################################################ # Install and uninstall scripts ################################################################################ # Install batch scripts under Windows. if(IS_MSVC) install(FILES "scripts/shell/COLMAP.bat" "scripts/shell/RUN_TESTS.bat" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION "/") endif() # Install application meny entry under Linux/Unix. if(UNIX AND NOT APPLE) install(FILES "doc/COLMAP.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") endif() # Configure the uninstallation script. if(UNINSTALL_ENABLED) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeUninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/CMakeUninstall.cmake" IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/CMakeUninstall.cmake) set_target_properties(uninstall PROPERTIES FOLDER ${CMAKE_TARGETS_ROOT_FOLDER}) endif() set(COLMAP_EXPORT_LIBS # Internal. colmap_controllers colmap_estimators colmap_estimators_cost_functions colmap_estimators_solvers colmap_exe colmap_feature_types colmap_feature colmap_geometry colmap_image colmap_math colmap_mvs colmap_optim colmap_retrieval colmap_scene colmap_scene_types colmap_sensor colmap_sfm colmap_util # Third-party. colmap_poisson_recon colmap_vlfeat ) if(LSD_ENABLED) list(APPEND COLMAP_EXPORT_LIBS # Third-party. colmap_lsd ) endif() if(GUI_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_ui ) endif() if(CUDA_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_util_cuda colmap_mvs_cuda ) endif() if(GPU_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_sift_gpu ) endif() if(FETCH_POSELIB) list(APPEND COLMAP_EXPORT_LIBS PoseLib) endif() if(FETCH_FAISS) list(APPEND COLMAP_EXPORT_LIBS faiss) endif() # Add unified interface library target to export. add_library(colmap INTERFACE) target_link_libraries(colmap INTERFACE ${COLMAP_EXPORT_LIBS}) target_include_directories( colmap INTERFACE $ $) install( TARGETS colmap ${COLMAP_EXPORT_LIBS} EXPORT colmap-targets LIBRARY DESTINATION thirdparty/) # Generate config and version. include(CMakePackageConfigHelpers) set(PACKAGE_CONFIG_FILE "${CMAKE_CURRENT_BINARY_DIR}/colmap-config.cmake") set(INSTALL_CONFIG_DIR "${CMAKE_INSTALL_DATAROOTDIR}/colmap") configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/colmap-config.cmake.in ${PACKAGE_CONFIG_FILE} INSTALL_DESTINATION ${INSTALL_CONFIG_DIR}) install(FILES ${PACKAGE_CONFIG_FILE} DESTINATION ${INSTALL_CONFIG_DIR}) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/colmap-config-version.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/colmap-config-version.cmake" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/colmap-config-version.cmake" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/colmap") # Install targets. install( EXPORT colmap-targets FILE colmap-targets.cmake NAMESPACE colmap:: DESTINATION ${INSTALL_CONFIG_DIR}) # Install header files. install( DIRECTORY src/colmap DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") install( DIRECTORY src/thirdparty DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/colmap FILES_MATCHING REGEX ".*[.]h|.*[.]hpp|.*[.]inl") # Install find_package scripts for dependencies. install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/cmake DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/colmap FILES_MATCHING PATTERN "Find*.cmake") colmap-4.0.4/CONTRIBUTING.md000066400000000000000000000010721517363634500152400ustar00rootroot00000000000000Contributing ------------ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. Please, adhere to the Google coding style guide: https://google.github.io/styleguide/cppguide.html by using the provided ".clang-format" file. Document code, functions, methods, classes, etc. Make sure to add unit tests for all newly added code and make sure that algorithmic "improvements" generalize and actually improve the results of the pipeline on a variety of datasets. colmap-4.0.4/COPYING.txt000066400000000000000000000035651517363634500146710ustar00rootroot00000000000000The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. Copyright (c), ETH Zurich and UNC Chapel Hill. 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 the name of ETH Zurich and UNC Chapel Hill nor the names of its 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 COPYRIGHT HOLDERS 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. colmap-4.0.4/README.md000066400000000000000000000146531517363634500142770ustar00rootroot00000000000000COLMAP ====== About ----- COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. The software is licensed under the new BSD license. The latest source code is available at https://github.com/colmap/colmap. COLMAP builds on top of existing works and when using specific algorithms within COLMAP, please also cite the original authors, as specified in the source code, and consider citing relevant third-party dependencies (most notably ceres-solver, poselib, sift-gpu, vlfeat). Download -------- * Binaries for **Windows** and other resources can be downloaded from https://github.com/colmap/colmap/releases. * Binaries for **Linux/Unix/BSD** are available at https://repology.org/metapackage/colmap/versions. * Pre-built **Docker** images are available at https://hub.docker.com/r/colmap/colmap. * Conda packages are available at https://anaconda.org/conda-forge/colmap and can be installed with `conda install colmap` * **Python bindings** are available at https://pypi.org/project/pycolmap. CUDA-enabled wheels are available at https://pypi.org/project/pycolmap-cuda12. * To **build from source**, please see https://colmap.github.io/install.html. Getting Started --------------- 1. Download pre-built binaries or build from source. 2. Download one of the provided [sample datasets](https://demuc.de/colmap/datasets/) or use your own images. 3. Use the **automatic reconstruction** to easily build models with a single click or command. Documentation ------------- The documentation is available [here](https://colmap.github.io/). To build and update the documentation at the documentation website, follow [these steps](https://colmap.github.io/install.html#documentation). Support ------- Please, use [GitHub Discussions](https://github.com/colmap/colmap/discussions) for questions and the [GitHub issue tracker](https://github.com/colmap/colmap) for bug reports, feature requests/additions, etc. Acknowledgments --------------- COLMAP was originally written by [Johannes Schönberger](https://demuc.de/) with funding provided by his PhD advisors Jan-Michael Frahm and Marc Pollefeys. The team of core project maintainers currently includes [Johannes Schönberger](https://github.com/ahojnnes), [Paul-Edouard Sarlin](https://github.com/sarlinpe), [Shaohui Liu](https://github.com/B1ueber2y), and [Linfei Pan](https://lpanaf.github.io/). The Python bindings in PyCOLMAP were originally added by [Mihai Dusmanu](https://github.com/mihaidusmanu), [Philipp Lindenberger](https://github.com/Phil26AT), and [Paul-Edouard Sarlin](https://github.com/sarlinpe). The project has also benefitted from countless community contributions, including bug fixes, improvements, new features, third-party tooling, and community support (special credits to [Torsten Sattler](https://tsattler.github.io)). Citation -------- If you use this project for your research, please cite: @inproceedings{schoenberger2016sfm, author={Sch\"{o}nberger, Johannes Lutz and Frahm, Jan-Michael}, title={Structure-from-Motion Revisited}, booktitle={Conference on Computer Vision and Pattern Recognition (CVPR)}, year={2016}, } @inproceedings{schoenberger2016mvs, author={Sch\"{o}nberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael}, title={Pixelwise View Selection for Unstructured Multi-View Stereo}, booktitle={European Conference on Computer Vision (ECCV)}, year={2016}, } If you use the global SfM pipeline (GLOMAP), please cite: @inproceedings{pan2024glomap, author={Pan, Linfei and Barath, Daniel and Pollefeys, Marc and Sch\"{o}nberger, Johannes Lutz}, title={{Global Structure-from-Motion Revisited}}, booktitle={European Conference on Computer Vision (ECCV)}, year={2024}, } If you use the image retrieval / vocabulary tree engine, please cite: @inproceedings{schoenberger2016vote, author={Sch\"{o}nberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc}, title={A Vote-and-Verify Strategy for Fast Spatial Verification in Image Retrieval}, booktitle={Asian Conference on Computer Vision (ACCV)}, year={2016}, } Contribution ------------ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. License ------- The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its thirdparty dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. Copyright (c), ETH Zurich and UNC Chapel Hill. 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 the name of ETH Zurich and UNC Chapel Hill nor the names of its 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 COPYRIGHT HOLDERS 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. colmap-4.0.4/benchmark/000077500000000000000000000000001517363634500147415ustar00rootroot00000000000000colmap-4.0.4/benchmark/reconstruction/000077500000000000000000000000001517363634500200225ustar00rootroot00000000000000colmap-4.0.4/benchmark/reconstruction/compare.py000066400000000000000000000050201517363634500220170ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import argparse import pickle from pathlib import Path from evaluation.utils import create_result_table, diff_metrics import pycolmap def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--report_a_path", type=Path, required=True) parser.add_argument("--report_b_path", type=Path, required=True) return parser.parse_args() def main() -> None: args = parse_args() with open(args.report_a_path, "rb") as report_file: metrics_a = pickle.load(report_file) with open(args.report_b_path, "rb") as report_file: metrics_b = pickle.load(report_file) metrics_diff = diff_metrics(metrics_a, metrics_b) pycolmap.logging.info("Results A:\n" + create_result_table(metrics_a)) pycolmap.logging.info("Results B:\n" + create_result_table(metrics_b)) pycolmap.logging.info( "Results A - B:\n" + create_result_table(metrics_diff) ) if __name__ == "__main__": main() colmap-4.0.4/benchmark/reconstruction/download.py000066400000000000000000000132531517363634500222070ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import argparse import shutil import subprocess import zipfile from pathlib import Path import py7zr import requests import pycolmap def download_file(url: str, target_folder: Path) -> str: filename = url.split("/")[-1] with requests.get(url, stream=True) as req: req.raise_for_status() with open(target_folder / filename, "wb") as f: for chunk in req.iter_content(chunk_size=8192): f.write(chunk) return filename def download_eth3d(data_path: Path) -> None: for filename, category in [ ("multi_view_training_dslr_undistorted.7z", "dslr"), ("multi_view_test_dslr_undistorted.7z", "dslr"), ("multi_view_training_rig_undistorted.7z", "rig"), ("multi_view_test_rig_undistorted.7z", "rig"), ]: target_folder = data_path / category target_folder.mkdir(parents=True, exist_ok=True) pycolmap.logging.info( f"Downloading ETH3D category={category}, filename={filename}" ) download_file("https://www.eth3d.net/data/" + filename, target_folder) pycolmap.logging.info( f"Extracting ETH3D category={category}, filename={filename}" ) with py7zr.SevenZipFile(target_folder / filename, mode="r") as archive: archive.extractall(path=target_folder) def download_imc2023(data_path: Path) -> None: data_path.mkdir(parents=True, exist_ok=True) pycolmap.logging.info("Downloading IMC2023") subprocess.check_call( [ "kaggle", "competitions", "download", "-c", "image-matching-challenge-2023", "-p", str(data_path), ], ) pycolmap.logging.info("Extracting IMC2023") with zipfile.ZipFile( data_path / "image-matching-challenge-2023.zip", mode="r" ) as archive: archive.extractall(path=data_path) def download_imc2024(data_path: Path) -> None: data_path.mkdir(parents=True, exist_ok=True) pycolmap.logging.info("Downloading IMC2024") subprocess.check_call( [ "kaggle", "competitions", "download", "-c", "image-matching-challenge-2024", "-p", str(data_path), ], ) pycolmap.logging.info("Extracting IMC2024") with zipfile.ZipFile( data_path / "image-matching-challenge-2024.zip", mode="r" ) as archive: archive.extractall(path=data_path) # Move all scenes to the "all" category sub-folder. category_path = data_path / "train/all" category_path.mkdir(parents=True, exist_ok=True) for scene in (data_path / "train").iterdir(): if str(scene).endswith("/all"): continue shutil.move(scene, data_path / category_path) # TODO: BlendedMVS+ and BlendedMVS++. def download_blended_mvs(data_path: Path) -> None: target_folder = data_path / "BlendedMVS" target_folder.mkdir(parents=True, exist_ok=True) pycolmap.logging.info("Downloading BlendedMVS") for filename in [ "BlendedMVS.zip", ] + [f"BlendedMVS.z{i:02d}" for i in range(1, 16)]: download_file( "https://github.com/YoYo000/BlendedMVS/releases/download/v1.0.0/" + filename, target_folder, ) pycolmap.logging.info("Extracting BlendedMVS") with zipfile.ZipFile(target_folder / "BlendedMVS.zip", mode="r") as archive: archive.extractall(path=data_path) DOWNLOADERS = { "eth3d": download_eth3d, "imc2023": download_imc2023, "imc2024": download_imc2024, "blended-mvs": download_blended_mvs, } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "--data_path", type=Path, default=Path(__file__).parent / "data" ) parser.add_argument( "--datasets", nargs="+", default=DOWNLOADERS.keys(), choices=DOWNLOADERS.keys(), ) return parser.parse_args() def main() -> None: args = parse_args() for dataset in args.datasets: DOWNLOADERS[dataset](args.data_path / dataset) if __name__ == "__main__": main() colmap-4.0.4/benchmark/reconstruction/evaluate.py000066400000000000000000000064231517363634500222070ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import pickle from evaluation.blended_mvs import DatasetBlendedMVS from evaluation.eth3d import DatasetETH3D from evaluation.imc import DatasetIMC2023, DatasetIMC2024 from evaluation.utils import ( Dataset, create_result_table, parse_args, process_scenes, ) import pycolmap def main() -> None: args = parse_args() datasets: dict[str, type[Dataset]] = { "eth3d": DatasetETH3D, "blended-mvs": DatasetBlendedMVS, "imc2023": DatasetIMC2023, "imc2024": DatasetIMC2024, } metrics = {} for dataset_name in args.datasets: if dataset_name not in datasets: pycolmap.logging.error(f"Unknown dataset: {dataset_name}") return pycolmap.logging.info(f"Evaluating dataset: {dataset_name}") dataset = datasets[dataset_name]( data_path=args.data_path, categories=args.categories, scenes=args.scenes, run_path=args.run_path, run_name=args.run_name, ) scene_infos = dataset.list_scenes() if not scene_infos: pycolmap.logging.warning("No scenes found") return metrics[dataset_name] = process_scenes( args=args, scene_infos=scene_infos, prepare_scene=dataset.prepare_scene, position_accuracy_gt=dataset.position_accuracy_gt, ) pycolmap.logging.info("Results:\n" + create_result_table(metrics)) report_path = args.run_path / args.run_name / (args.report_name + ".pkl") pycolmap.logging.info(f"Saving report to: {report_path}") with open(report_path, "wb") as report_file: pickle.dump(metrics, report_file) if __name__ == "__main__": main() colmap-4.0.4/benchmark/reconstruction/evaluation/000077500000000000000000000000001517363634500221715ustar00rootroot00000000000000colmap-4.0.4/benchmark/reconstruction/evaluation/__init__.py000066400000000000000000000000001517363634500242700ustar00rootroot00000000000000colmap-4.0.4/benchmark/reconstruction/evaluation/blended_mvs.py000066400000000000000000000131461517363634500250320ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import numpy as np from PIL import Image import pycolmap from .utils import Dataset, SceneInfo class DatasetBlendedMVS(Dataset): @property def position_accuracy_gt(self): return 0.001 def list_scenes(self): scene_infos = [] for category_path in (self.data_path / "blended-mvs").iterdir(): if not category_path.is_dir() or ( self.categories and category_path.name not in self.categories ): continue category = category_path.name for scene_path in sorted(category_path.iterdir()): if not scene_path.is_dir(): continue scene = scene_path.name if self.scenes and scene not in self.scenes: continue workspace_path = ( self.run_path / self.run_name / "blended-mvs" / category / scene ) image_path = scene_path / "blended_images" image_list_path = scene_path / "images.txt" with open(image_list_path, "w") as fid: for filepath in sorted(image_path.iterdir()): image_name = str(filepath.name) if ( image_name.endswith(".jpg") and "masked" not in image_name ): fid.write(image_name + "\n") sparse_gt_path = scene_path / "sparse_gt" colmap_extra_args = ["--image_list_path", image_list_path] scene_info = SceneInfo( dataset="blended-mvs", category=category, scene=scene, workspace_path=workspace_path, image_path=image_path, sparse_gt_path=sparse_gt_path, has_camera_priors=True, colmap_extra_args=colmap_extra_args, ) scene_infos.append(scene_info) return scene_infos def prepare_scene(self, scene_info): if scene_info.sparse_gt_path.exists(): return scene_path = scene_info.image_path.parent sparse_gt = pycolmap.Reconstruction() for i, filepath in enumerate(sorted((scene_path / "cams").iterdir())): filename = str(filepath.name) if not filename.endswith("_cam.txt"): continue image_name = filename[:-8] + ".jpg" width, height = Image.open( scene_path / "blended_images" / image_name ).size[:2] with open(filepath, encoding="ascii") as fid: lines = list(map(lambda b: b.strip(), fid.readlines())) extrinsic = np.fromstring( " ".join(lines[1:4]), count=12, sep=" ", ).reshape(3, 4) intrinsic = np.fromstring( " ".join(lines[7:10]), count=9, sep=" ", ).reshape(3, 3) camera = pycolmap.Camera( camera_id=i, model=pycolmap.CameraModelId.PINHOLE, width=width, height=height, params=intrinsic[(0, 1, 0, 1), (0, 1, 2, 2)], ) rig = pycolmap.Rig(rig_id=i) rig.add_ref_sensor(camera.sensor_id) image = pycolmap.Image( image_id=i, camera_id=i, name=image_name, cam_from_world=pycolmap.Rigid3d(extrinsic), ) frame = pycolmap.Frame(frame_id=i) frame.add_data_id(image.data_id) sparse_gt.add_camera(camera) sparse_gt.add_rig(rig) sparse_gt.add_image(image) sparse_gt.add_frame(frame) scene_info.sparse_gt_path.mkdir(exist_ok=True) sparse_gt.write(scene_info.sparse_gt_path) colmap-4.0.4/benchmark/reconstruction/evaluation/eth3d.py000066400000000000000000000066501517363634500235610ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. from .utils import Dataset, SceneInfo class DatasetETH3D(Dataset): @property def position_accuracy_gt(self): return 0.001 def list_scenes(self): scene_infos = [] for category_path in (self.data_path / "eth3d").iterdir(): if not category_path.is_dir() or ( self.categories and category_path.name not in self.categories ): continue category = category_path.name for scene_path in sorted(category_path.iterdir()): if not scene_path.is_dir(): continue scene = scene_path.name if self.scenes and scene not in self.scenes: continue workspace_path = ( self.run_path / self.run_name / "eth3d" / category / scene ) image_path = scene_path / "images" sparse_gt_path = list( scene_path.glob("*_calibration_undistorted") )[0] colmap_extra_args = [] if category == "dslr": colmap_extra_args.extend(["--data_type", "individual"]) elif category == "rig": colmap_extra_args.extend(["--data_type", "video"]) scene_info = SceneInfo( dataset="eth3d", category=category, scene=scene, workspace_path=workspace_path, image_path=image_path, sparse_gt_path=sparse_gt_path, has_camera_priors=True, colmap_extra_args=colmap_extra_args, ) scene_infos.append(scene_info) return scene_infos def prepare_scene(self, scene_info): # Nothing to prepare for ETH3D. pass colmap-4.0.4/benchmark/reconstruction/evaluation/imc.py000066400000000000000000000136701517363634500233220ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. from pathlib import Path import pycolmap from .utils import Dataset, SceneInfo class _DatasetIMC(Dataset): @property def position_accuracy_gt(self): return 0.02 def list_scenes(self): folder_name = f"imc{self.year}" scene_infos = [] for category_path in Path( self.data_path / f"{folder_name}/train" ).iterdir(): if not category_path.is_dir() or ( self.categories and category_path.name not in self.categories ): continue category = category_path.name for scene_path in category_path.iterdir(): if not scene_path.is_dir(): continue scene = scene_path.name if self.scenes and scene not in self.scenes: continue sfm_path = scene_path / "sfm" if not sfm_path.exists(): pycolmap.logging.warning( f"Skipping dataset=IMC{self.year}, " f"category={category}, scene={scene}, " "because the GT reconstruction is missing" ) continue workspace_path = ( self.run_path / self.run_name / folder_name / category / scene ) image_path = scene_path / "images" sparse_gt_path = scene_path / "sparse_gt" scene_info = SceneInfo( dataset=f"IMC{self.year}", category=category, scene=scene, workspace_path=workspace_path, image_path=image_path, sparse_gt_path=sparse_gt_path, has_camera_priors=False, colmap_extra_args=None, ) scene_infos.append(scene_info) return scene_infos def prepare_scene(self, scene_info): if scene_info.sparse_gt_path.exists(): return scene_path = scene_info.image_path.parent sfm_path = scene_path / "sfm" sparse_sfm = pycolmap.Reconstruction(sfm_path) sparse_gt = pycolmap.Reconstruction() train_image_ids = set() train_image_names = set( image.name for image in scene_info.image_path.iterdir() ) for image in sparse_sfm.images.values(): if image.name in train_image_names: train_image_ids.add(image.image_id) for camera in sparse_sfm.cameras.values(): sparse_gt.add_camera(camera) for rig in sparse_sfm.rigs.values(): sparse_gt.add_rig(rig) for frame in sparse_sfm.frames.values(): has_train_image = False for data_id in frame.image_ids: if data_id.id in train_image_ids: has_train_image = True break if has_train_image: frame.reset_rig_ptr() sparse_gt.add_frame(frame) for image in sparse_sfm.images.values(): if image.image_id not in train_image_ids: continue if image.camera_id not in sparse_gt.cameras: sparse_gt.add_camera(image.camera) image.reset_camera_ptr() image.reset_frame_ptr() sparse_gt.add_image(image) scene_info.sparse_gt_path.mkdir(exist_ok=True) sparse_gt.write(scene_info.sparse_gt_path) class DatasetIMC2023(_DatasetIMC): def __init__( self, data_path: Path, categories: list[str], scenes: list[Path], run_path: Path, run_name: str, ): super().__init__( data_path=data_path, categories=categories, scenes=scenes, run_path=run_path, run_name=run_name, ) self.year = 2023 class DatasetIMC2024(_DatasetIMC): def __init__( self, data_path: Path, categories: list[str], scenes: list[Path], run_path: Path, run_name: str, ): super().__init__( data_path=data_path, categories=categories, scenes=scenes, run_path=run_path, run_name=run_name, ) self.year = 2024 colmap-4.0.4/benchmark/reconstruction/evaluation/utils.py000066400000000000000000001000511517363634500237000ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import argparse import collections import copy import dataclasses import datetime import functools import multiprocessing import shutil import subprocess from abc import ABC, abstractmethod from collections.abc import Callable from pathlib import Path import numpy as np import numpy.typing as npt import pycolmap @dataclasses.dataclass(kw_only=True) class SceneInfo: # Dataset name. dataset: str # Category name. category: str # Scene name. scene: str # Path to the workspace directory in the run directory. workspace_path: Path # Path to the input images. image_path: Path # Path to the ground-truth sparse reconstruction. sparse_gt_path: Path # Whether the dataset has camera priors. has_camera_priors: bool # Additional arguments for the COLMAP reconstruction command. colmap_extra_args: list[str] @dataclasses.dataclass(kw_only=True) class SceneResult: # Scene information for which the result was computed. scene_info: SceneInfo # Flat list of errors. errors: npt.NDArray[np.floating] # Number of images in the scene. num_images: int # Number of registered images in the scene (over all components). num_reg_images: int # Number of components in the scene. num_components: int # Number of images in the largest component. largest_component: int @dataclasses.dataclass(kw_only=True) class Metrics: # Recall at specified error thresholds. recalls: npt.NDArray[np.floating] # Area under the curve (AUC) scores at specified error thresholds. aucs: npt.NDArray[np.floating] error_thresholds: npt.NDArray[np.floating] error_type: str # Number of images in the scene. num_images: int # Number of registered images in the scene (over all components). num_reg_images: int # Number of components in the scene. num_components: int # Number of images in the largest component. largest_component: int MetricsByScene = dict[str, Metrics] MetricsByCatByScene = dict[str, MetricsByScene] MetricsByDatasetByCatByScene = dict[str, MetricsByCatByScene] class Dataset(ABC): def __init__( self, data_path: Path, categories: list[str], scenes: list[Path], run_path: Path, run_name: str, ): self.data_path = data_path self.categories = categories self.scenes = scenes self.run_path = run_path self.run_name = run_name @property @abstractmethod def position_accuracy_gt(self) -> float: """Ground-truth position accuracy in meters.""" pass @abstractmethod def list_scenes(self) -> list[SceneInfo]: """List all scenes to evaluate.""" pass @abstractmethod def prepare_scene(self, scene_info: SceneInfo) -> None: """Prepare the scene for reconstruction.""" pass def parse_args() -> argparse.Namespace: datetime_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") parser = argparse.ArgumentParser() parser.add_argument( "--data_path", default=Path(__file__).parent.parent / "data", type=Path ) parser.add_argument( "--datasets", nargs="+", default=["eth3d", "blended-mvs", "imc2023", "imc2024"], ) parser.add_argument( "--categories", nargs="+", default=[], help="Categories to evaluate, if empty all categories are evaluated.", ) parser.add_argument( "--scenes", nargs="+", default=[], help="Scenes to evaluate, if empty all scenes are evaluated.", ) parser.add_argument( "--run_path", default=Path(__file__).parent.parent / "runs", type=Path ) parser.add_argument("--run_name", default=datetime_str) parser.add_argument("--report_name", default=f"report-{datetime_str}") parser.add_argument( "--overwrite_database", default=False, action="store_true" ) parser.add_argument( "--overwrite_matches", default=False, action="store_true" ) parser.add_argument( "--overwrite_reconstruction", default=False, action="store_true" ) parser.add_argument( "--overwrite_alignment", default=False, action="store_true" ) parser.add_argument("--colmap_path", required=True) parser.add_argument("--use_gpu", default=True, action="store_true") parser.add_argument("--use_cpu", dest="use_gpu", action="store_false") parser.add_argument( "--parallelism", type=int, default=multiprocessing.cpu_count(), help="Number of processes for parallel reconstruction.", ) parser.add_argument( "--feature", default="sift", choices=["sift", "aliked"], ) parser.add_argument( "--mapper", default="incremental", choices=["incremental", "hierarchical", "global"], ) parser.add_argument( "--quality", default="high", choices=["low", "medium", "high"] ) parser.add_argument( "--uncalibrated", default=False, action="store_true", help="Whether to evaluate the setting of uncalibrated input cameras, " "even if normal setting for the dataset contains calibrated inputs. " "This is useful for evaluating the performance of self-calibration.", ) parser.add_argument( "--error_type", default="relative_auc", choices=[ "relative_auc", "absolute_auc", "relative_recall", "absolute_recall", ], help="Whether to evaluate relative pairwise pose errors in angular " "distance or absolute pose errors through GT alignment.", ) parser.add_argument( "--rel_error_thresholds", type=float, nargs="+", default=[0.5, 1, 5, 10], help="Evaluation thresholds in degrees.", ) parser.add_argument( "--abs_error_thresholds", type=float, nargs="+", default=[0.02, 0.05, 0.2, 0.5], help="Evaluation thresholds in meters.", ) args = parser.parse_args() args.colmap_path = Path(args.colmap_path).resolve() if args.overwrite_database: pycolmap.logging.info( "Overwriting database also overwrites reconstruction" ) args.overwrite_reconstruction = True if args.overwrite_matches: pycolmap.logging.info( "Overwriting matches also overwrites reconstruction" ) args.overwrite_reconstruction = True if args.overwrite_reconstruction: pycolmap.logging.info( "Overwriting reconstruction also overwrites alignment" ) args.overwrite_alignment = True return args def set_camera_priors( database_path: Path, camera_priors_sparse_gt: pycolmap.Reconstruction ) -> None: pycolmap.logging.info("Setting prior cameras from GT") with pycolmap.Database.open(str(database_path)) as database: images_gt_by_name = {} for image_gt in camera_priors_sparse_gt.images.values(): images_gt_by_name[image_gt.name] = image_gt updated_camera_ids = set() for image in database.read_all_images(): if image.name not in images_gt_by_name: pycolmap.logging.warning( f"Not setting prior camera for image {image.name}, " "because it does not exist in GT" ) continue image_gt = images_gt_by_name[image.name] if image.camera_id in updated_camera_ids: continue camera_gt = camera_priors_sparse_gt.cameras[image_gt.camera_id] camera_gt.camera_id = image.camera_id camera_gt.has_prior_focal_length = True database.update_camera(camera_gt) updated_camera_ids.add(image.camera_id) def colmap_reconstruction( args: argparse.Namespace, workspace_path: Path, image_path: Path, camera_priors_sparse_gt: pycolmap.Reconstruction | None = None, colmap_extra_args: list | None = None, num_threads: int = 1, ) -> None: workspace_path.mkdir(parents=True, exist_ok=True) database_path = workspace_path / "database.db" if args.overwrite_database and database_path.exists(): database_path.unlink() sparse_path = workspace_path / "sparse" if args.overwrite_reconstruction and sparse_path.exists(): shutil.rmtree(sparse_path) if sparse_path.exists(): pycolmap.logging.info("Skipping reconstruction, as it already exists") return if args.overwrite_matches: subprocess.check_call( [ args.colmap_path, "database_cleaner", "--database_path", database_path, "--type", "matches", ], cwd=workspace_path, ) # TODO: Expose automatic reconstruction through pycolmap bindings instead # of using the command line interface. One blocker for this is that we # currently do not produce CUDA enabled pycolmap packages. colmap_args = [ args.colmap_path, "automatic_reconstructor", "--image_path", image_path, "--workspace_path", workspace_path, "--use_gpu", "1" if args.use_gpu else "0", "--num_threads", str(num_threads), "--feature", args.feature, "--mapper", args.mapper, "--quality", args.quality, ] subprocess.check_call( colmap_args + (colmap_extra_args or []) + [ "--extraction", "1", "--matching", "0", "--sparse", "0", "--dense", "0", ], cwd=workspace_path, ) if camera_priors_sparse_gt is not None: set_camera_priors(database_path, camera_priors_sparse_gt) subprocess.check_call( colmap_args + (colmap_extra_args or []) + [ "--extraction", "0", "--matching", "1", "--sparse", "0", "--dense", "0", ], cwd=workspace_path, ) # Decouple matching from sparse reconstruction, because matching will # initialize an OpenGL context and Mac on Apple silicon tends to assign GUI # applications to the low efficiency cores but we want to use the # performance cores. subprocess.check_call( colmap_args + (colmap_extra_args or []) + [ "--extraction", "0", "--matching", "0", "--sparse", "1", "--dense", "0", ], cwd=workspace_path, ) def colmap_alignment( args: argparse.Namespace, sparse_path: Path, sparse_gt_path: Path, sparse_aligned_path: Path, max_ref_model_error: float, ) -> None: if args.overwrite_alignment and sparse_aligned_path.exists(): shutil.rmtree(sparse_aligned_path) if sparse_aligned_path.exists(): pycolmap.logging.info("Skipping alignment, as it already exists") return if sparse_path.exists(): sparse_aligned_path.mkdir(parents=True, exist_ok=True) subprocess.call( [ args.colmap_path, "model_aligner", "--input_path", sparse_path, "--ref_model_path", sparse_gt_path, "--output_path", sparse_aligned_path, "--alignment_max_error", str(max_ref_model_error), ] ) def process_scene( args: argparse.Namespace, scene_info: SceneInfo, prepare_scene: Callable[[SceneInfo], None], position_accuracy_gt: float, num_threads: int, ) -> SceneResult: pycolmap.logging.info( f"Processing dataset={scene_info.dataset}, " f"category={scene_info.category}, " f"scene={scene_info.scene}" ) prepare_scene(scene_info) sparse_gt = pycolmap.Reconstruction(str(scene_info.sparse_gt_path)) colmap_reconstruction( args=args, workspace_path=scene_info.workspace_path, image_path=scene_info.image_path, camera_priors_sparse_gt=( sparse_gt if not args.uncalibrated and scene_info.has_camera_priors else None ), num_threads=num_threads, colmap_extra_args=scene_info.colmap_extra_args, ) # Merge all sub-models into a single reconstruction. Each sub-model will be # "randomly" aligned to the other sub-models. We then compute the overall # error over the merged reconstruction. With this simple appraoch, there is # a small chance that the randomly aligned images in one sub-model are # correctly aligned with other sub-models and the error is therefore # underestimated. However, this is very unlikely to happen. sparse_merged = pycolmap.Reconstruction() num_components = 0 largest_component = 0 for sparse_path in (scene_info.workspace_path / "sparse").iterdir(): if not sparse_path.is_dir(): continue num_components += 1 sparse = None if args.error_type.startswith("relative"): sparse = pycolmap.Reconstruction(str(sparse_path)) elif args.error_type.startswith("absolute"): sparse_aligned_path = scene_info.workspace_path / "sparse_aligned" colmap_alignment( args=args, sparse_path=sparse_path, sparse_gt_path=scene_info.sparse_gt_path, sparse_aligned_path=sparse_aligned_path, max_ref_model_error=position_accuracy_gt, ) if (sparse_aligned_path / "images.bin").exists(): sparse = pycolmap.Reconstruction(str(sparse_aligned_path)) else: raise ValueError(f"Invalid error type: {args.error_type}") if sparse is not None: largest_component = max(largest_component, sparse.num_images()) for image in sparse.images.values(): if image.image_id in sparse_merged.images: continue if image.camera_id not in sparse_merged.cameras: sparse_merged.add_camera(image.camera) if image.frame_id not in sparse_merged.frames: if image.frame.rig_id not in sparse_merged.rigs: sparse_merged.add_rig(image.frame.rig) image.frame.reset_rig_ptr() sparse_merged.add_frame(image.frame) image.reset_camera_ptr() image.reset_frame_ptr() sparse_merged.add_image(image) if args.error_type.startswith("relative"): dts, dRs = compute_rel_errors( sparse_gt=sparse_gt, sparse=sparse_merged, min_proj_center_dist=position_accuracy_gt, ) errors = np.maximum(dts, dRs) elif args.error_type.startswith("absolute"): dts, dRs = compute_abs_errors( sparse_gt=sparse_gt, sparse=sparse_merged, ) errors = dts else: raise ValueError(f"Invalid error type: {args.error_type}") return SceneResult( scene_info=scene_info, errors=errors, num_images=sparse_gt.num_images(), num_reg_images=sparse_merged.num_images(), num_components=num_components, largest_component=largest_component, ) def process_scenes( args: argparse.Namespace, scene_infos: list[SceneInfo], prepare_scene: Callable[[SceneInfo], None], position_accuracy_gt: float, ) -> MetricsByCatByScene: error_thresholds = get_error_thresholds(args) num_threads = min( args.parallelism, 2 * max(1, int(args.parallelism / len(scene_infos))) ) with multiprocessing.Pool(processes=args.parallelism) as p: results = p.map( functools.partial( process_scene, args, prepare_scene=prepare_scene, position_accuracy_gt=position_accuracy_gt, num_threads=num_threads, ), scene_infos, ) metrics: MetricsByCatByScene = collections.defaultdict(dict) errors_by_category: dict[str, list] = collections.defaultdict(list) total_num_images = 0 total_num_reg_images = 0 total_num_components = 0 total_largest_components = 0 num_scenes = len(results) for result in results: errors_by_category[result.scene_info.category].extend(result.errors) total_num_images += result.num_images total_num_reg_images += result.num_reg_images total_num_components += result.num_components total_largest_components += result.largest_component metrics[result.scene_info.category][result.scene_info.scene] = Metrics( aucs=compute_auc( result.errors, error_thresholds, min_error=position_accuracy_gt, ), recalls=compute_recall(result.errors, error_thresholds), error_thresholds=error_thresholds, error_type=args.error_type, num_images=result.num_images, num_reg_images=result.num_reg_images, num_components=result.num_components, largest_component=result.largest_component, ) for category, errors in errors_by_category.items(): errors_array = np.array(errors) metrics[category]["__all__"] = Metrics( aucs=compute_auc( errors_array, error_thresholds, min_error=position_accuracy_gt, ), recalls=compute_recall(errors_array, error_thresholds), error_thresholds=error_thresholds, error_type=args.error_type, num_images=total_num_images, num_reg_images=total_num_reg_images, num_components=total_num_components, largest_component=total_largest_components, ) aucs, recalls = compute_avg_metrics(metrics[category]) metrics[category]["__avg__"] = Metrics( aucs=aucs, recalls=recalls, error_thresholds=error_thresholds, error_type=args.error_type, num_images=int(round(total_num_images / num_scenes)), num_reg_images=int(round(total_num_reg_images / num_scenes)), num_components=int(round(total_num_components / num_scenes)), largest_component=int(round(total_largest_components / num_scenes)), ) return metrics def normalize_vec( vec: npt.NDArray[np.floating], eps: float = 1e-10 ) -> npt.NDArray[np.floating]: return vec / max(eps, float(np.linalg.norm(vec))) def vec_angular_dist_deg( vec1: npt.NDArray[np.floating], vec2: npt.NDArray[np.floating] ) -> float: cos_dist = np.clip(np.dot(normalize_vec(vec1), normalize_vec(vec2)), -1, 1) return np.rad2deg(np.acos(cos_dist)) def get_error_thresholds(args: argparse.Namespace) -> npt.NDArray[np.floating]: if args.error_type.startswith("relative"): return np.array(args.rel_error_thresholds) elif args.error_type.startswith("absolute"): return np.array(args.abs_error_thresholds) else: raise ValueError(f"Invalid error type: {args.error_type}") def get_scores(error_type: str, metrics: Metrics) -> npt.NDArray[np.floating]: if error_type.endswith("auc"): return metrics.aucs elif error_type.endswith("recall"): return metrics.recalls else: raise ValueError(f"Invalid error type: {error_type}") def compute_rel_errors( sparse_gt: pycolmap.Reconstruction, sparse: pycolmap.Reconstruction, min_proj_center_dist: float, ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]: """Computes angular relative pose errors across all image pairs. Notice that this approach leads to a super-linear decrease in the AUC scores when multiple images fail to register. Consider that we have N images in total in a dataset and M images are registered in the evaluated reconstruction. In this case, we can compute "finite" errors for (N-M)^2 pairs while the dataset has a total of N^2 pairs. In case of many unregistered images, the AUC score will drop much more than the (intuitively) expected (N-M) / N ratio. One could appropriately normalize by computing a single score per image through a suitable normalization of all pairwise errors per image. However, this becomes difficult when multiple sub-components are incorrectly stitched together in the same reconstruction (e.g., in the case of symmetry issues). """ if sparse is None: pycolmap.logging.error("Reconstruction failed") return len(sparse_gt.images) * [np.inf], len(sparse_gt.images) * [180] images = {} for image in sparse.images.values(): images[image.name] = image dts = [] dRs = [] for this_image_gt in sparse_gt.images.values(): if this_image_gt.name not in images: for _ in range(sparse_gt.num_images() - 1): dts.append(np.inf) dRs.append(180) continue this_image = images[this_image_gt.name] for other_image_gt in sparse_gt.images.values(): if this_image_gt.image_id == other_image_gt.image_id: continue if other_image_gt.name not in images: dts.append(np.inf) dRs.append(180) continue other_image = images[other_image_gt.name] other_from_this = ( other_image.cam_from_world() * this_image.cam_from_world().inverse() ) other_from_this_gt = ( other_image_gt.cam_from_world() * this_image_gt.cam_from_world().inverse() ) estimated_from_gt = other_from_this.inverse() * other_from_this_gt if ( np.linalg.norm(other_from_this_gt.translation) < min_proj_center_dist ): # If the cameras almost coincide, then the angular direction # distance is unstable, because a small position change can # cause a large rotational error. In this case, we only measure # rotational relative pose error. dt = 0.0 else: dt = vec_angular_dist_deg( other_from_this.translation, other_from_this_gt.translation ) dR = np.rad2deg(estimated_from_gt.rotation.angle()) dts.append(dt) dRs.append(dR) return np.array(dts), np.array(dRs) def compute_abs_errors( sparse_gt: pycolmap.Reconstruction, sparse: pycolmap.Reconstruction ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]: """Computes rotational and translational absolute pose errors. Assumes that the input reconstructions are aligned in the same coordinate system. Computes one error per ground-truth image. """ dts = np.full(len(sparse_gt.images), fill_value=np.inf, dtype=np.float64) dRs = np.full(len(sparse_gt.images), fill_value=180, dtype=np.float64) if sparse is None: pycolmap.logging.error("Reconstruction or alignment failed") return dts, dRs images = {} for image in sparse.images.values(): images[image.name] = image dts = np.full(len(sparse_gt.images), fill_value=np.inf, dtype=np.float64) dRs = np.full(len(sparse_gt.images), fill_value=180, dtype=np.float64) for i, image_gt in enumerate(sparse_gt.images.values()): if image_gt.name not in images: continue image = images[image_gt.name] estimated_from_gt = ( image.cam_from_world() * image_gt.cam_from_world().inverse() ) dts[i] = np.linalg.norm(estimated_from_gt.translation) dRs[i] = np.rad2deg(estimated_from_gt.rotation.angle()) return dts, dRs def compute_auc( errors: npt.NDArray[np.floating], thresholds: npt.NDArray[np.floating], min_error: float = 0, ) -> npt.NDArray[np.floating]: num_elems = len(errors) if len(errors) == 0: raise ValueError("No errors to evaluate") errors = np.sort(errors) recalls = (np.arange(num_elems) + 1) / num_elems if min_error > 0: min_index = np.searchsorted(errors, min_error, side="right") min_recall = min_index / num_elems recalls = np.r_[min_recall, min_recall, recalls[min_index:]] errors = np.r_[0, min_error, errors[min_index:]] else: recalls = np.r_[0, recalls] errors = np.r_[0, errors] aucs = np.zeros(len(thresholds), dtype=np.float64) for i, t in enumerate(thresholds): last_index = np.searchsorted(errors, t, side="right") r = np.r_[recalls[:last_index], recalls[last_index - 1]] e = np.r_[errors[:last_index], t] auc = np.trapezoid(r, x=e) / t aucs[i] = auc * 100 return aucs def compute_recall( errors: npt.NDArray[np.floating], thresholds: npt.NDArray[np.floating], min_error: float = 0, ) -> npt.NDArray[np.floating]: num_elems = len(errors) if num_elems == 0: raise ValueError("No errors to evaluate") recalls = np.zeros(len(thresholds), dtype=np.float64) for i, t in enumerate(thresholds): recalls[i] = 100 * np.sum(errors <= t) / num_elems return recalls def compute_avg_metrics( scene_metrics: MetricsByScene, ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]: auc_sum = None recall_sum = None num_scenes = 0 for scene, metrics in scene_metrics.items(): if scene.startswith("__") and scene.endswith("__"): continue num_scenes += 1 if auc_sum is None: auc_sum = copy.copy(metrics.aucs) if recall_sum is None: recall_sum = copy.copy(metrics.recalls) else: for i in range(len(auc_sum)): auc_sum[i] += metrics.aucs[i] for i in range(len(recall_sum)): recall_sum[i] += metrics.recalls[i] return np.array(auc_sum) / num_scenes, np.array(recall_sum) / num_scenes def diff_metrics( metrics_a: MetricsByDatasetByCatByScene, metrics_b: MetricsByDatasetByCatByScene, ): """Computes difference between two sets of metrics. Raises exception if the metrics are inconsistent. """ metrics_diff = copy.deepcopy(metrics_a) for dataset, category_metrics_a in metrics_a.items(): if dataset not in metrics_b: raise ValueError(f"Dataset {dataset} not found in metrics_b") category_metrics_b = metrics_b[dataset] for category, scene_metrics_a in category_metrics_a.items(): if category not in category_metrics_b: raise ValueError(f"Category {category} not found in metrics_b") scene_metrics_b = category_metrics_b[category] for scene, metrics_a_item in scene_metrics_a.items(): if scene not in scene_metrics_b: raise ValueError(f"Scene {scene} not found in metrics_b") metrics_b_item = scene_metrics_b[scene] if ( metrics_a_item.error_type != metrics_b_item.error_type or not np.all( metrics_a_item.error_thresholds == metrics_b_item.error_thresholds ) ): raise ValueError("Inconsistent error thresholds or types") metrics_diff[dataset][category][scene] = Metrics( aucs=metrics_a_item.aucs - metrics_b_item.aucs, recalls=metrics_a_item.recalls - metrics_b_item.recalls, error_thresholds=metrics_a_item.error_thresholds, error_type=metrics_a_item.error_type, num_images=metrics_a_item.num_images - metrics_b_item.num_images, num_reg_images=metrics_a_item.num_reg_images - metrics_b_item.num_reg_images, num_components=metrics_a_item.num_components - metrics_b_item.num_components, largest_component=metrics_a_item.largest_component - metrics_b_item.largest_component, ) return metrics_diff def create_result_table( dataset_metrics: MetricsByDatasetByCatByScene, ) -> str: first_metrics = next( iter(next(iter(next(iter(dataset_metrics.values())).values())).values()) ) is_auc = first_metrics.error_type.endswith("auc") is_relative = first_metrics.error_type.startswith("relative") score_type = "AUC" if is_auc else "Recall" score_unit = "deg" if is_relative else "cm" label = f"{score_type} @ X {score_unit} (%)" if is_relative: thresholds = first_metrics.error_thresholds else: thresholds = 100 * first_metrics.error_thresholds # cm column = "scenes" size_scenes = max( len(column) + 2, max( len(s) for d in dataset_metrics.values() for c in d.values() for s in c ), ) size_aucs = max(len(label) + 2, len(thresholds) * 7 - 1) size_imgs = 12 size_comps = 12 size_sep = size_scenes + size_aucs + size_imgs + size_comps + 3 header = ( f"{column:=^{size_scenes}} {label:=^{size_aucs}} " f"{'images':=^{size_imgs}} {'components':=^{size_comps}}" ) header += "\n" + " " * (size_scenes + 1) header += " ".join(f"{str(t).rstrip('.'):^6}" for t in thresholds) header += " reg all num largest" text = [header] for dataset, category_metrics in dataset_metrics.items(): for category, scene_metrics in category_metrics.items(): text.append(f"\n{dataset + '=' + category:=^{size_sep}}") for scene, metrics in scene_metrics.items(): scores = get_scores(first_metrics.error_type, metrics) assert len(scores) == len(thresholds) row = "" if scene == "__avg__": scene = "average" row += "-" * size_sep + "\n" if scene == "__all__": scene = "overall" row += "-" * size_sep + "\n" row += f"{scene:<{size_scenes}} " row += " ".join(f"{score:>6.2f}" for score in scores) row += f" {metrics.num_reg_images:6d}" row += f"{metrics.num_images:6d}" row += f" {metrics.num_components:4d}" row += f"{metrics.largest_component:8d}" text.append(row) return "\n".join(text) colmap-4.0.4/benchmark/reconstruction/evaluation/utils_test.py000066400000000000000000000423731517363634500247530ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import numpy as np import pytest import pycolmap from .utils import ( Metrics, compute_abs_errors, compute_auc, compute_avg_metrics, compute_recall, compute_rel_errors, diff_metrics, get_scores, normalize_vec, vec_angular_dist_deg, ) class TestNormalizeVec: def test_unit_vector(self): vec = np.array([1.0, 0.0, 0.0]) normalized = normalize_vec(vec) np.testing.assert_allclose(normalized, vec) np.testing.assert_almost_equal(np.linalg.norm(normalized), 1.0) def test_non_unit_vector(self): vec = np.array([3.0, 4.0, 0.0]) normalized = normalize_vec(vec) expected = np.array([0.6, 0.8, 0.0]) np.testing.assert_allclose(normalized, expected) np.testing.assert_almost_equal(np.linalg.norm(normalized), 1.0) def test_zero_vector(self): vec = np.array([0.0, 0.0, 0.0]) normalized = normalize_vec(vec) assert np.linalg.norm(normalized) < 1e-8 class TestVecAngularDistDeg: def test_identical_vectors(self): vec1 = np.array([1.0, 0.0, 0.0]) vec2 = np.array([1.0, 0.0, 0.0]) dist = vec_angular_dist_deg(vec1, vec2) np.testing.assert_almost_equal(dist, 0.0) def test_opposite_vectors(self): vec1 = np.array([1.0, 0.0, 0.0]) vec2 = np.array([-1.0, 0.0, 0.0]) dist = vec_angular_dist_deg(vec1, vec2) np.testing.assert_almost_equal(dist, 180.0) def test_perpendicular_vectors(self): vec1 = np.array([1.0, 0.0, 0.0]) vec2 = np.array([0.0, 1.0, 0.0]) dist = vec_angular_dist_deg(vec1, vec2) np.testing.assert_almost_equal(dist, 90.0) def test_45_degree_vectors(self): vec1 = np.array([1.0, 0.0, 0.0]) vec2 = np.array([1.0, 1.0, 0.0]) dist = vec_angular_dist_deg(vec1, vec2) np.testing.assert_almost_equal(dist, 45.0) def test_non_unit_vectors(self): vec1 = np.array([2.0, 0.0, 0.0]) vec2 = np.array([0.0, 3.0, 0.0]) dist = vec_angular_dist_deg(vec1, vec2) # Should normalize internally np.testing.assert_almost_equal(dist, 90.0) def test_clipping_behavior(self): # Test that dot product is clipped to [-1, 1] to avoid numerical issues vec1 = np.array([1.0, 1e-10, 1e-10]) vec2 = np.array([1.0, -1e-10, -1e-10]) dist = vec_angular_dist_deg(vec1, vec2) # Should not raise error even with potential numerical issues assert 0 <= dist <= 180 class TestComputeAuc: def test_simple_uniform_errors(self): errors = np.array([0.1, 0.2, 0.3, 0.4, 0.5]) thresholds = np.array([0.25, 0.5, 1.0]) aucs = compute_auc(errors, thresholds) np.testing.assert_almost_equal(aucs[0], 24.0, decimal=5) np.testing.assert_almost_equal(aucs[1], 50.0, decimal=5) np.testing.assert_almost_equal(aucs[2], 75.0, decimal=5) def test_all_errors_zero(self): errors = np.array([0.0, 0.0, 0.0]) thresholds = np.array([0.5, 1.0]) aucs = compute_auc(errors, thresholds) np.testing.assert_array_almost_equal(aucs, [100.0, 100.0]) def test_empty_errors(self): errors = np.array([]) thresholds = np.array([0.5, 1.0]) with pytest.raises(ValueError, match="No errors to evaluate"): compute_auc(errors, thresholds) def test_all_errors_above_threshold(self): errors = np.array([10.0, 20.0, 30.0]) thresholds = np.array([5.0]) aucs = compute_auc(errors, thresholds) np.testing.assert_almost_equal(aucs[0], 0.0) def test_all_errors_below_threshold(self): errors = np.array([0.1, 0.2, 0.3]) thresholds = np.array([1.0]) aucs = compute_auc(errors, thresholds) np.testing.assert_almost_equal(aucs[0], 85.0, decimal=5) def test_inf_errors(self): errors = np.array([0.1, 0.2, np.inf, np.inf]) thresholds = np.array([0.5, 1.0]) aucs = compute_auc(errors, thresholds) assert np.all(aucs >= 0) assert np.all(aucs <= 100) def test_single_error(self): errors = np.array([0.5]) thresholds = np.array([0.3, 1.0]) aucs = compute_auc(errors, thresholds) assert len(aucs) == 2 np.testing.assert_almost_equal(aucs[0], 0.0) np.testing.assert_almost_equal(aucs[1], 75.0) class TestComputeRecall: def test_basic_recall(self): errors = np.array([0.1, 0.2, 0.3, 0.4, 0.5]) thresholds = np.array([0.05, 0.25, 0.5, 1.0]) recalls = compute_recall(errors, thresholds) assert len(recalls) == 4 assert recalls[3] >= recalls[2] >= recalls[1] >= recalls[0] assert np.all(recalls >= 0) assert np.all(recalls <= 100) def test_empty_errors(self): errors = np.array([]) thresholds = np.array([0.5, 1.0]) with pytest.raises(ValueError, match="No errors to evaluate"): compute_recall(errors, thresholds) def test_all_errors_above_threshold(self): errors = np.array([10.0, 20.0, 30.0]) thresholds = np.array([5.0]) recalls = compute_recall(errors, thresholds) np.testing.assert_almost_equal(recalls[0], 0.0) def test_all_errors_below_threshold(self): errors = np.array([0.1, 0.2, 0.3]) thresholds = np.array([1.0]) recalls = compute_recall(errors, thresholds) np.testing.assert_almost_equal(recalls[0], 100.0) def test_exact_threshold(self): errors = np.array([0.1, 0.5, 0.9]) thresholds = np.array([0.5]) recalls = compute_recall(errors, thresholds) np.testing.assert_almost_equal(recalls[0], 200.0 / 3.0) def test_multiple_thresholds(self): errors = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) thresholds = np.array([2.0, 3.0, 4.0]) recalls = compute_recall(errors, thresholds) np.testing.assert_almost_equal(recalls[0], 40.0) np.testing.assert_almost_equal(recalls[1], 60.0) np.testing.assert_almost_equal(recalls[2], 80.0) class TestComputeAvgMetrics: def test_single_scene(self): scene_metrics = { "scene1": Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ) } aucs, recalls = compute_avg_metrics(scene_metrics) np.testing.assert_array_equal(aucs, [10.0, 20.0, 30.0]) np.testing.assert_array_equal(recalls, [15.0, 25.0, 35.0]) def test_multiple_scenes(self): scene_metrics = { "scene1": Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ), "scene2": Metrics( aucs=np.array([20.0, 30.0, 40.0]), recalls=np.array([25.0, 35.0, 45.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ), } aucs, recalls = compute_avg_metrics(scene_metrics) np.testing.assert_array_equal(aucs, [15.0, 25.0, 35.0]) np.testing.assert_array_equal(recalls, [20.0, 30.0, 40.0]) def test_skip_special_scenes(self): scene_metrics = { "scene1": Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ), "__avg__": Metrics( aucs=np.array([50.0, 60.0, 70.0]), recalls=np.array([55.0, 65.0, 75.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ), "__all__": Metrics( aucs=np.array([80.0, 90.0, 100.0]), recalls=np.array([85.0, 95.0, 105.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ), } aucs, recalls = compute_avg_metrics(scene_metrics) # Should only average scene1, not __avg__ or __all__ np.testing.assert_array_equal(aucs, [10.0, 20.0, 30.0]) np.testing.assert_array_equal(recalls, [15.0, 25.0, 35.0]) class TestGetScores: def test_get_auc_scores(self): metrics = Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ) scores = get_scores("relative_auc", metrics) np.testing.assert_array_equal(scores, metrics.aucs) def test_get_recall_scores(self): metrics = Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_recall", num_images=100, num_reg_images=90, num_components=1, largest_component=90, ) scores = get_scores("relative_recall", metrics) np.testing.assert_array_equal(scores, metrics.recalls) class TestDiffMetrics: def test_nominal(self): metrics_a = { "dataset1": { "category1": { "scene1": Metrics( aucs=np.array([20.0, 30.0, 40.0]), recalls=np.array([25.0, 35.0, 45.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=90, num_components=2, largest_component=80, ) } } } metrics_b = { "dataset1": { "category1": { "scene1": Metrics( aucs=np.array([10.0, 20.0, 30.0]), recalls=np.array([15.0, 25.0, 35.0]), error_thresholds=np.array([0.5, 1.0, 2.0]), error_type="relative_auc", num_images=100, num_reg_images=85, num_components=1, largest_component=85, ) } } } diff = diff_metrics(metrics_a, metrics_b) scene_diff = diff["dataset1"]["category1"]["scene1"] np.testing.assert_array_equal(scene_diff.aucs, [10.0, 10.0, 10.0]) np.testing.assert_array_equal(scene_diff.recalls, [10.0, 10.0, 10.0]) assert scene_diff.num_reg_images == 5 assert scene_diff.num_components == 1 def create_test_reconstruction(): pycolmap.set_random_seed(0) synthetic_dataset_options = pycolmap.SyntheticDatasetOptions() synthetic_dataset_options.num_cameras_per_rig = 1 synthetic_dataset_options.num_frames_per_rig = 5 synthetic_dataset_options.num_points3D = 0 return pycolmap.synthesize_dataset(synthetic_dataset_options) class TestComputeAbsErrors: def test_identical_reconstruction(self): reconstruction = create_test_reconstruction() dts, dRs = compute_abs_errors( sparse_gt=reconstruction, sparse=reconstruction ) assert len(dts) == reconstruction.num_images() assert len(dRs) == reconstruction.num_images() np.testing.assert_allclose(dts, 0.0, atol=1e-10) np.testing.assert_allclose(dRs, 0.0, atol=1e-10) def test_transformed_reconstruction(self): gt_reconstruction = create_test_reconstruction() reconstruction = create_test_reconstruction() translation = np.array([1, 2, 3]) for frame in reconstruction.frames.values(): world_from_rig = frame.rig_from_world.inverse() world_from_rig.rotation = ( world_from_rig.rotation * pycolmap.Rotation3d([0, 1, 0, 0]) ) world_from_rig.translation += translation frame.rig_from_world = world_from_rig.inverse() dts, dRs = compute_abs_errors( sparse_gt=gt_reconstruction, sparse=reconstruction ) assert len(dts) == reconstruction.num_images() assert len(dRs) == reconstruction.num_images() np.testing.assert_allclose(dts, np.linalg.norm(translation), atol=1e-10) np.testing.assert_allclose(dRs, 180.0, atol=1e-10) class TestComputeRelErrors: def test_identical_reconstruction(self): reconstruction = create_test_reconstruction() dts, dRs = compute_rel_errors( sparse_gt=reconstruction, sparse=reconstruction, min_proj_center_dist=0.01, ) num_images = reconstruction.num_images() expected_num_errors = num_images * (num_images - 1) assert len(dts) == expected_num_errors assert len(dRs) == expected_num_errors np.testing.assert_allclose(dts, 0.0, atol=1e-5) np.testing.assert_allclose(dRs, 0.0, atol=1e-5) def test_transformed_reconstruction(self): gt_reconstruction = create_test_reconstruction() reconstruction = create_test_reconstruction() reconstruction.transform( pycolmap.Sim3d( 1.0, pycolmap.Rotation3d(np.array([0, 1, 0, 0])), np.array([1, 2, 3]), ) ) dts, dRs = compute_rel_errors( sparse_gt=gt_reconstruction, sparse=reconstruction, min_proj_center_dist=0.01, ) num_images = reconstruction.num_images() expected_num_errors = num_images * (num_images - 1) assert len(dts) == expected_num_errors assert len(dRs) == expected_num_errors np.testing.assert_allclose(dts, 0.0, atol=1e-5) np.testing.assert_allclose(dRs, 0.0, atol=1e-5) def test_different_reconstructions(self): gt_reconstruction = create_test_reconstruction() reconstruction = create_test_reconstruction() for image in reconstruction.images.values(): image.frame.rig_from_world.rotation = ( pycolmap.Rotation3d(np.array([0, 1, 0, 0])) * image.frame.rig_from_world.rotation ) image.frame.rig_from_world.translation += np.array([1, 2, 3]) dts, dRs = compute_rel_errors( sparse_gt=gt_reconstruction, sparse=reconstruction, min_proj_center_dist=0.01, ) num_images = reconstruction.num_images() expected_num_errors = num_images * (num_images - 1) assert len(dts) == expected_num_errors assert len(dRs) == expected_num_errors assert np.all(dts > 0.1) assert np.all(dRs > 0.1) colmap-4.0.4/benchmark/reconstruction/requirements.txt000066400000000000000000000000451517363634500233050ustar00rootroot00000000000000pillow numpy requests pycolmap py7zr colmap-4.0.4/benchmark/runtime/000077500000000000000000000000001517363634500164245ustar00rootroot00000000000000colmap-4.0.4/benchmark/runtime/CMakeLists.txt000066400000000000000000000010411517363634500211600ustar00rootroot00000000000000find_package(benchmark REQUIRED) add_executable(benchmark_cost_functions cost_functions.cc) target_link_libraries(benchmark_cost_functions PRIVATE colmap_estimators benchmark::benchmark) add_executable(benchmark_bundle_adjustment bundle_adjustment.cc) target_link_libraries(benchmark_bundle_adjustment PRIVATE colmap_estimators colmap_scene benchmark::benchmark) add_executable(benchmark_global_positioning global_positioning.cc) target_link_libraries(benchmark_global_positioning PRIVATE colmap_estimators colmap_scene benchmark::benchmark) colmap-4.0.4/benchmark/runtime/README.md000066400000000000000000000012261517363634500177040ustar00rootroot00000000000000# Benchmarking ## Installation 1. Install [google/benchmark](https://github.com/google/benchmark). For example, using homebrew on a Mac: `brew install google-benchmark`. 2. Build and run the benchmarking executables: ```bash cmake .. -DBENCHMARK_ENABLED=ON ninja benchmark/runtime/benchmark_cost_functions ``` To reduce the variance, consider setting up your system appropriately [following these instructions](https://github.com/google/benchmark/blob/main/docs/reducing_variance.md). ## Running the benchmarks Cost functions: ```bash ./benchmark/runtime/benchmark_cost_functions --benchmark_display_aggregates_only=true --benchmark_repetitions=50 ``` colmap-4.0.4/benchmark/runtime/bundle_adjustment.cc000066400000000000000000000147571517363634500224600ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/scene/reconstruction.h" #include "colmap/scene/synthetic.h" #include using namespace colmap; void GenerateArguments(benchmark::Benchmark* b) { for (const int track_length : {5, 20, 100}) { for (const int num_rigs : {1, 5}) { for (const int num_cameras_per_rig : {1, 3}) { for (const int num_frames_per_rig : {10, 50}) { const int num_images = num_rigs * num_cameras_per_rig * num_frames_per_rig; if (track_length > num_images) { continue; } for (const int num_points3D : {1000, 10000}) { b->Args({track_length, num_rigs, num_cameras_per_rig, num_frames_per_rig, num_points3D}); } } } } } } inline const ceres::Solver::Summary& GetCeresSummary( const BundleAdjustmentSummary* summary) { return dynamic_cast(summary) ->ceres_summary; } class BM_BundleAdjustment : public benchmark::Fixture { public: void SetUp(::benchmark::State& state) { SetPRNGSeed(42); SyntheticDatasetOptions dataset_options; dataset_options.track_length = state.range(0); dataset_options.num_rigs = state.range(1); dataset_options.num_cameras_per_rig = state.range(2); dataset_options.num_frames_per_rig = state.range(3); dataset_options.num_points3D = state.range(4); reconstruction_ = std::make_unique(); SynthesizeDataset(dataset_options, reconstruction_.get()); SyntheticNoiseOptions noise_options; noise_options.point2D_stddev = 1.0; noise_options.point3D_stddev = 0.05; noise_options.rig_from_world_translation_stddev = 0.01; noise_options.rig_from_world_rotation_stddev = 1.0; SynthesizeNoise(noise_options, reconstruction_.get()); for (const image_t image_id : reconstruction_->RegImageIds()) { config_.AddImage(image_id); } config_.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); // Set up BA options. options_.print_summary = false; if (options_.ceres) { options_.ceres->solver_options.max_num_iterations = 50; } } void TearDown(::benchmark::State& state) { reconstruction_.reset(); options_ = BundleAdjustmentOptions(); config_ = BundleAdjustmentConfig(); } protected: std::unique_ptr reconstruction_; BundleAdjustmentConfig config_; BundleAdjustmentOptions options_; }; BENCHMARK_DEFINE_F(BM_BundleAdjustment, Solve)(benchmark::State& state) { int num_iterations = 0; double total_solve_time_s = 0; for (auto _ : state) { state.PauseTiming(); // Copy the reconstruction for each iteration since BA modifies it. Reconstruction reconstruction_copy = *reconstruction_; state.ResumeTiming(); auto bundle_adjuster = CreateDefaultBundleAdjuster(options_, config_, reconstruction_copy); const auto summary = bundle_adjuster->Solve(); // Stop timing and check if BA converged. state.PauseTiming(); const auto& ceres_summary = GetCeresSummary(summary.get()); num_iterations += ceres_summary.num_successful_steps + ceres_summary.num_unsuccessful_steps; total_solve_time_s += ceres_summary.total_time_in_seconds; if (summary->termination_type == BundleAdjustmentTerminationType::NO_CONVERGENCE) { state.SkipWithError("Bundle adjustment did not converge"); } const int ceres_iterations = ceres_summary.num_successful_steps + ceres_summary.num_unsuccessful_steps; if (ceres_iterations > 0) { state.SetIterationTime(ceres_summary.total_time_in_seconds / ceres_iterations); } state.ResumeTiming(); } state.PauseTiming(); // Report custom counters. state.counters["track_len"] = reconstruction_->ComputeMeanTrackLength(); state.counters["imgs"] = reconstruction_->NumRegImages(); state.counters["rigs"] = reconstruction_->NumRigs(); state.counters["cams"] = reconstruction_->NumCameras(); state.counters["frms"] = reconstruction_->NumRegFrames(); state.counters["pnts"] = reconstruction_->NumPoints3D(); state.counters["avg_itrs"] = std::round(num_iterations * 10.0 / state.iterations()) / 10.0; state.ResumeTiming(); } // Time column reports time per solver iteration (not per benchmark iteration). BENCHMARK_REGISTER_F(BM_BundleAdjustment, Solve) ->Apply(GenerateArguments) ->Unit(benchmark::kMillisecond) ->UseManualTime(); int main(int argc, char** argv) { benchmark::Initialize(&argc, argv); if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; std::cerr << "\033[1mNote: Time column reports time (ms) per solver " "iteration.\033[0m" << std::endl; benchmark::RunSpecifiedBenchmarks(); benchmark::Shutdown(); return 0; } colmap-4.0.4/benchmark/runtime/cost_functions.cc000066400000000000000000000070571517363634500220040ustar00rootroot00000000000000#include "colmap/estimators/cost_functions/reprojection_error.h" #include "colmap/geometry/rigid3.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include #include using namespace colmap; using camera_model = SimpleRadialCameraModel; struct ReprojErrorData { Rigid3d cam_from_world; Eigen::Vector3d point3D; Eigen::Vector2d point2D; std::vector camera_params; }; static ReprojErrorData CreateReprojErrorData() { ReprojErrorData data{ Rigid3d(Eigen::Quaterniond(0.9, 0.1, 0.1, 0.1), Eigen::Vector3d::Zero()), Eigen::Vector3d(1, 2, 10), Eigen::Vector2d(0.1, 0.2), {1, 0, 0, 0.1}, }; CHECK_EQ(data.camera_params.size(), camera_model::num_params); return std::move(data); } class BM_ReprojErrorCostFunction : public benchmark::Fixture { public: void SetUp(::benchmark::State& state) { cost_function.reset( ReprojErrorCostFunctor::Create(data.point2D)); } ReprojErrorData data = CreateReprojErrorData(); const double* parameters[4] = {data.cam_from_world.rotation().coeffs().data(), data.cam_from_world.translation().data(), data.point3D.data(), data.camera_params.data()}; double residuals[2]; double jacobian_q[2 * 4]; double jacobian_t[2 * 3]; double jacobian_p[2 * 3]; double jacobian_params[2 * camera_model::num_params]; double* jacobians[4] = {jacobian_q, jacobian_t, jacobian_p, jacobian_params}; std::unique_ptr cost_function; }; BENCHMARK_F(BM_ReprojErrorCostFunction, Run)(benchmark::State& state) { for (auto _ : state) { cost_function->Evaluate(parameters, residuals, jacobians); } } class BM_ReprojErrorConstantPoseCostFunction : public benchmark::Fixture { public: void SetUp(::benchmark::State& state) { cost_function.reset( ReprojErrorConstantPoseCostFunctor::Create( data.point2D, data.cam_from_world)); } ReprojErrorData data = CreateReprojErrorData(); const double* parameters[2] = {data.point3D.data(), data.camera_params.data()}; double residuals[2]; double jacobian_p[2 * 3]; double jacobian_params[2 * camera_model::num_params]; double* jacobians[2] = {jacobian_p, jacobian_params}; std::unique_ptr cost_function; }; BENCHMARK_F(BM_ReprojErrorConstantPoseCostFunction, Run) (benchmark::State& state) { for (auto _ : state) { cost_function->Evaluate(parameters, residuals, jacobians); } } class BM_ReprojErrorConstantPoint3DCostFunction : public benchmark::Fixture { public: void SetUp(::benchmark::State& state) { cost_function.reset( ReprojErrorConstantPoint3DCostFunctor::Create( data.point2D, data.point3D)); } ReprojErrorData data = CreateReprojErrorData(); const double* parameters[3] = {data.cam_from_world.rotation().coeffs().data(), data.cam_from_world.translation().data(), data.camera_params.data()}; double residuals[2]; double jacobian_q[2 * 4]; double jacobian_t[2 * 3]; double jacobian_params[2 * camera_model::num_params]; double* jacobians[3] = {jacobian_q, jacobian_t, jacobian_params}; std::unique_ptr cost_function; }; BENCHMARK_F(BM_ReprojErrorConstantPoint3DCostFunction, Run) (benchmark::State& state) { for (auto _ : state) { cost_function->Evaluate(parameters, residuals, jacobians); } } BENCHMARK_MAIN(); colmap-4.0.4/benchmark/runtime/global_positioning.cc000066400000000000000000000150411517363634500226160ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/global_positioning.h" #include "colmap/math/random.h" #include "colmap/scene/database_cache.h" #include "colmap/scene/database_sqlite.h" #include "colmap/scene/pose_graph.h" #include "colmap/scene/reconstruction.h" #include "colmap/scene/synthetic.h" #include #include #include #include using namespace colmap; struct CachedData { std::array dataset_args; Reconstruction reconstruction; colmap::PoseGraph pose_graph; }; static void BM_GlobalPositioning(benchmark::State& state) { FLAGS_minloglevel = 2; // Suppress INFO and WARNING logs. const std::array dataset_args = {state.range(0), state.range(1), state.range(2), state.range(3), state.range(4)}; const bool use_parameter_block_ordering = state.range(5); // Cache dataset to avoid resynthesizing for different ordering options. static std::unique_ptr cached; if (!cached || cached->dataset_args != dataset_args) { SetPRNGSeed(42); SyntheticDatasetOptions dataset_options; dataset_options.num_rigs = dataset_args[0]; dataset_options.num_cameras_per_rig = dataset_args[1]; dataset_options.num_frames_per_rig = dataset_args[2]; dataset_options.num_points3D = dataset_args[3]; // Compute sparsity from target number of neighbors per image. // sparsity ≈ 1 - num_neighbors / (num_images - 1) const int num_neighbors = dataset_args[4]; const int num_images = dataset_options.num_rigs * dataset_options.num_cameras_per_rig * dataset_options.num_frames_per_rig; dataset_options.match_sparsity = std::max( 0.0, 1.0 - static_cast(num_neighbors) / (num_images - 1)); dataset_options.match_config = SyntheticDatasetOptions::MatchConfig::SPARSE; dataset_options.two_view_geometry_has_relative_pose = true; cached = std::make_unique(); cached->dataset_args = dataset_args; auto database = Database::Open(kInMemorySqliteDatabasePath); Reconstruction gt_reconstruction; SynthesizeDataset(dataset_options, >_reconstruction, database.get()); DatabaseCache database_cache; DatabaseCache::Options cache_options; database_cache.Load(*database, cache_options); database.reset(); // Close database connection. cached->pose_graph.Load(*database_cache.CorrespondenceGraph()); cached->reconstruction = gt_reconstruction; for (const auto& [frame_id, _] : cached->reconstruction.Frames()) { Frame& frame = cached->reconstruction.Frame(frame_id); frame.SetRigFromWorld( Rigid3d(frame.RigFromWorld().rotation(), Eigen::Vector3d::Zero())); } } const Reconstruction& reconstruction = cached->reconstruction; const colmap::PoseGraph& pose_graph = cached->pose_graph; const int num_neighbors = dataset_args[4]; colmap::GlobalPositionerOptions base_options; base_options.use_gpu = false; base_options.random_seed = 42; base_options.solver_options.max_num_iterations = 50; base_options.solver_options.minimizer_progress_to_stdout = false; for (auto _ : state) { state.PauseTiming(); Reconstruction reconstruction_copy = reconstruction; colmap::GlobalPositionerOptions options = base_options; options.use_parameter_block_ordering = use_parameter_block_ordering; state.ResumeTiming(); colmap::GlobalPositioner positioner(options); positioner.Solve(pose_graph, reconstruction_copy); } state.counters["ord"] = use_parameter_block_ordering; state.counters["imgs"] = reconstruction.NumRegImages(); state.counters["rigs"] = reconstruction.NumRigs(); state.counters["cams"] = reconstruction.NumCameras(); state.counters["frms"] = reconstruction.NumRegFrames(); state.counters["pnts"] = reconstruction.NumPoints3D(); state.counters["nbrs"] = num_neighbors; } static void GenerateArguments(benchmark::Benchmark* b) { // Args: {num_rigs, num_cameras_per_rig, num_frames_per_rig, num_points3D, // num_neighbors, use_parameter_block_ordering} for (const int num_rigs : {1, 5}) { for (const int num_cameras_per_rig : {1, 3}) { for (const int num_frames_per_rig : {10, 50}) { for (const int num_points3D : {1000, 10000}) { for (const int num_neighbors : {10, 20}) { for (const bool use_parameter_block_ordering : {true, false}) { b->Args({num_rigs, num_cameras_per_rig, num_frames_per_rig, num_points3D, num_neighbors, use_parameter_block_ordering}); } } } } } } } BENCHMARK(BM_GlobalPositioning) ->Apply(GenerateArguments) ->Unit(benchmark::kMillisecond) ->UseRealTime(); BENCHMARK_MAIN(); colmap-4.0.4/cmake/000077500000000000000000000000001517363634500140675ustar00rootroot00000000000000colmap-4.0.4/cmake/CMakeHelper.cmake000066400000000000000000000160601517363634500172140ustar00rootroot00000000000000if(POLICY CMP0043) cmake_policy(SET CMP0043 NEW) endif() if(POLICY CMP0054) cmake_policy(SET CMP0054 NEW) endif() # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24") cmake_policy(SET CMP0135 NEW) endif() if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.30") cmake_policy(SET CMP0167 NEW) endif() # Determine project compiler. if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set(IS_MSVC TRUE) endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(IS_GNU TRUE) endif() if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") set(IS_CLANG TRUE) endif() # Determine project architecture. if(CMAKE_SYSTEM_PROCESSOR MATCHES "[ix].?86|amd64|AMD64") set(IS_X86 TRUE) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") set(IS_ARM64 TRUE) endif() # Determine project operating system. string(REGEX MATCH "Linux" IS_LINUX ${CMAKE_SYSTEM_NAME}) string(REGEX MATCH "DragonFly|BSD" IS_BSD ${CMAKE_SYSTEM_NAME}) string(REGEX MATCH "SunOS" IS_SOLARIS ${CMAKE_SYSTEM_NAME}) if(WIN32) set(IS_WINDOWS TRUE BOOL INTERNAL) endif() if(APPLE) set(IS_MACOS TRUE BOOL INTERNAL) endif() string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug" OR CMAKE_BUILD_TYPE_LOWER STREQUAL "relwithdebinfo") set(IS_DEBUG TRUE) endif() # Enable solution folders. set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_TARGETS_ROOT_FOLDER "cmake") set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER ${CMAKE_TARGETS_ROOT_FOLDER}) set(COLMAP_TARGETS_ROOT_FOLDER "colmap_targets") set(COLMAP_SRC_ROOT_FOLDER "colmap_sources") # This macro will search for source files in a given directory, will add them # to a source group (folder within a project), and will then return paths to # each of the found files. The usage of the macro is as follows: # COLMAP_ADD_SOURCE_DIR( # # # ) macro(COLMAP_ADD_SOURCE_DIR SRC_DIR SRC_VAR) # Create the list of expressions to be used in the search. set(GLOB_EXPRESSIONS "") foreach(ARG ${ARGN}) list(APPEND GLOB_EXPRESSIONS ${SRC_DIR}/${ARG}) endforeach() # Perform the search for the source files. file(GLOB ${SRC_VAR} RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${GLOB_EXPRESSIONS}) # Create the source group. string(REPLACE "/" "\\" GROUP_NAME ${SRC_DIR}) source_group(${GROUP_NAME} FILES ${${SRC_VAR}}) # Clean-up. unset(GLOB_EXPRESSIONS) unset(ARG) unset(GROUP_NAME) endmacro(COLMAP_ADD_SOURCE_DIR) # Replacement for the normal add_library() command. The syntax remains the same # in that the first argument is the target name, and the following arguments # are the source files to use when building the target. # Supports TYPE argument: STATIC (default) or INTERFACE (header-only libraries). # For INTERFACE libraries, use INTERFACE_LINK_LIBS instead of PRIVATE/PUBLIC_LINK_LIBS. macro(COLMAP_ADD_LIBRARY) set(options) set(oneValueArgs TYPE) set(multiValueArgs NAME SRCS PRIVATE_LINK_LIBS PUBLIC_LINK_LIBS INTERFACE_LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_LIBRARY "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(COLMAP_ADD_LIBRARY_TYPE STREQUAL "INTERFACE") # Header-only library add_library(${COLMAP_ADD_LIBRARY_NAME} INTERFACE) set_target_properties(${COLMAP_ADD_LIBRARY_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) target_link_libraries(${COLMAP_ADD_LIBRARY_NAME} INTERFACE ${COLMAP_ADD_LIBRARY_INTERFACE_LINK_LIBS}) target_compile_definitions(${COLMAP_ADD_LIBRARY_NAME} INTERFACE ${COLMAP_COMPILE_DEFINITIONS}) else() # Regular library (TYPE can be STATIC to override BUILD_SHARED_LIBS). add_library(${COLMAP_ADD_LIBRARY_NAME} ${COLMAP_ADD_LIBRARY_TYPE} ${COLMAP_ADD_LIBRARY_SRCS}) set_target_properties(${COLMAP_ADD_LIBRARY_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_LIBRARY_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() target_link_libraries(${COLMAP_ADD_LIBRARY_NAME} PRIVATE ${COLMAP_ADD_LIBRARY_PRIVATE_LINK_LIBS} PUBLIC ${COLMAP_ADD_LIBRARY_PUBLIC_LINK_LIBS}) target_compile_definitions(${COLMAP_ADD_LIBRARY_NAME} PUBLIC ${COLMAP_COMPILE_DEFINITIONS}) endif() endmacro(COLMAP_ADD_LIBRARY) # Replacement for the normal add_executable() command. The syntax remains the # same in that the first argument is the target name, and the following # arguments are the source files to use when building the target. macro(COLMAP_ADD_EXECUTABLE) set(options) set(oneValueArgs) set(multiValueArgs NAME SRCS LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_EXECUTABLE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) add_executable(${COLMAP_ADD_EXECUTABLE_NAME} ${COLMAP_ADD_EXECUTABLE_SRCS}) set_target_properties(${COLMAP_ADD_EXECUTABLE_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) target_link_libraries(${COLMAP_ADD_EXECUTABLE_NAME} ${COLMAP_ADD_EXECUTABLE_LINK_LIBS}) if(VCPKG_BUILD) install(TARGETS ${COLMAP_ADD_EXECUTABLE_NAME} DESTINATION tools/) else() install(TARGETS ${COLMAP_ADD_EXECUTABLE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_EXECUTABLE_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() target_compile_definitions(${COLMAP_ADD_EXECUTABLE_NAME} PRIVATE ${COLMAP_COMPILE_DEFINITIONS}) endmacro(COLMAP_ADD_EXECUTABLE) # Wrapper for test executables. macro(COLMAP_ADD_TEST) set(options) set(oneValueArgs) set(multiValueArgs NAME SRCS LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(TESTS_ENABLED) # ${ARGN} will store the list of link libraries. set(COLMAP_ADD_TEST_TARGET "colmap_${FOLDER_NAME}_${COLMAP_ADD_TEST_NAME}") add_executable(${COLMAP_ADD_TEST_TARGET} ${COLMAP_ADD_TEST_SRCS}) set_target_properties(${COLMAP_ADD_TEST_TARGET} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME} OUTPUT_NAME "${COLMAP_ADD_TEST_NAME}") if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_TEST_TARGET} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() target_link_libraries(${COLMAP_ADD_TEST_TARGET} ${COLMAP_ADD_TEST_LINK_LIBS} colmap_gtest_main) add_test(NAME "${FOLDER_NAME}/${COLMAP_ADD_TEST_NAME}" COMMAND $) if(IS_MSVC) install(TARGETS ${COLMAP_ADD_TEST_TARGET} DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() target_compile_definitions(${COLMAP_ADD_TEST_TARGET} PRIVATE ${COLMAP_COMPILE_DEFINITIONS}) endif() endmacro(COLMAP_ADD_TEST) colmap-4.0.4/cmake/CMakeUninstall.cmake.in000066400000000000000000000015331517363634500203520ustar00rootroot00000000000000if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") endif() file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") exec_program("@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" OUTPUT_VARIABLE rm_out RETURN_VALUE rm_retval) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") endif() else() message(STATUS "File $ENV{DESTDIR}${file} does not exist.") endif() endforeach() colmap-4.0.4/cmake/FindCHOLMOD.cmake000066400000000000000000000110521517363634500167560ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for CHOLMOD library. # # The following variables are set by this module: # # CHOLMOD_FOUND: TRUE if CHOLMOD is found. # CHOLMOD::CHOLMOD: Imported target to link against. # # The following variables control the behavior of this module: # # CHOLMOD_INCLUDE_DIR_HINTS: List of additional directories in which to # search for CHOLMOD includes. # CHOLMOD_LIBRARY_DIR_HINTS: List of additional directories in which to # search for CHOLMOD libraries. set(CHOLMOD_INCLUDE_DIR_HINTS "" CACHE PATH "CHOLMOD include directory") set(CHOLMOD_LIBRARY_DIR_HINTS "" CACHE PATH "CHOLMOD library directory") unset(CHOLMOD_FOUND) unset(CHOLMOD_INCLUDE_DIRS) unset(CHOLMOD_LIBRARIES) find_package(CHOLMOD CONFIG QUIET) if(TARGET CHOLMOD::CHOLMOD) set(CHOLMOD_FOUND TRUE) message(STATUS "Found CHOLMOD") message(STATUS " Target : CHOLMOD::CHOLMOD") else() list(APPEND CHOLMOD_INCLUDE_SEARCH_PATHS ${CHOLMOD_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /sw/include /opt/include /opt/local/include) # Some distros don't package suitesparse under a /suitesparse subdirectory (e.g. NixOS). # Search for both layouts separately so that the suitesparse/ subdirectory # layout is reliably preferred across all search paths. A single find_path # call with both names would iterate search paths first and names second, # which could pick up a bare cholmod.h from an earlier path over # suitesparse/cholmod.h from a later one. find_path(CHOLMOD_INCLUDE_DIRS NAMES suitesparse/cholmod.h PATHS ${CHOLMOD_INCLUDE_SEARCH_PATHS}) if(NOT CHOLMOD_INCLUDE_DIRS) unset(CHOLMOD_INCLUDE_DIRS CACHE) find_path(CHOLMOD_INCLUDE_DIRS NAMES cholmod.h PATHS ${CHOLMOD_INCLUDE_SEARCH_PATHS}) endif() find_library(CHOLMOD_LIBRARIES NAMES cholmod PATHS ${CHOLMOD_LIBRARY_DIR_HINTS} /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib /sw/lib /opt/lib /opt/local/lib) if(CHOLMOD_INCLUDE_DIRS AND CHOLMOD_LIBRARIES) set(CHOLMOD_FOUND TRUE) message(STATUS "Found CHOLMOD") message(STATUS " Includes : ${CHOLMOD_INCLUDE_DIRS}") message(STATUS " Libraries : ${CHOLMOD_LIBRARIES}") else() set(CHOLMOD_FOUND FALSE) endif() if(EXISTS "${CHOLMOD_INCLUDE_DIRS}/suitesparse/cholmod.h") set(CHOLMOD_INTERFACE_INCLUDE_DIRS "${CHOLMOD_INCLUDE_DIRS}/suitesparse") else() set(CHOLMOD_INTERFACE_INCLUDE_DIRS "${CHOLMOD_INCLUDE_DIRS}") endif() add_library(CHOLMOD::CHOLMOD INTERFACE IMPORTED) target_include_directories( CHOLMOD::CHOLMOD INTERFACE ${CHOLMOD_INTERFACE_INCLUDE_DIRS}) target_link_libraries( CHOLMOD::CHOLMOD INTERFACE ${CHOLMOD_LIBRARIES}) endif() if(NOT CHOLMOD_FOUND AND CHOLMOD_FIND_REQUIRED) message(FATAL_ERROR "Could not find CHOLMOD") endif() colmap-4.0.4/cmake/FindCryptoPP.cmake000066400000000000000000000065501517363634500174200ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for CryptoPP library. # # The following variables are set by this module: # # CryptoPP_FOUND: TRUE if CryptoPP is found. # cryptopp: Imported target to link against. # # The following variables control the behavior of this module: # # CryptoPP_INCLUDE_DIR_HINTS: List of additional directories in which to # search for CryptoPP includes. # CryptoPP_LIBRARY_DIR_HINTS: List of additional directories in which to # search for CryptoPP libraries. set(CryptoPP_INCLUDE_DIR_HINTS "" CACHE PATH "CryptoPP include directory") set(CryptoPP_LIBRARY_DIR_HINTS "" CACHE PATH "CryptoPP library directory") unset(CryptoPP_FOUND) unset(CryptoPP_INCLUDE_DIRS) unset(CryptoPP_LIBRARIES) list(APPEND CryptoPP_CHECK_INCLUDE_DIRS ${CryptoPP_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND CryptoPP_CHECK_LIBRARY_DIRS ${CryptoPP_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /opt/lib /opt/local/lib ) find_path(CryptoPP_INCLUDE_DIRS NAMES cryptopp/cryptlib.h PATHS ${CryptoPP_CHECK_INCLUDE_DIRS}) find_library(CryptoPP_LIBRARIES NAMES cryptopp PATHS ${CryptoPP_CHECK_LIBRARY_DIRS}) if(CryptoPP_INCLUDE_DIRS AND CryptoPP_LIBRARIES) set(CryptoPP_FOUND TRUE) endif() if(CryptoPP_FOUND) message(STATUS "Found CryptoPP") message(STATUS " Includes : ${CryptoPP_INCLUDE_DIRS}") message(STATUS " Libraries : ${CryptoPP_LIBRARIES}") else() if(CryptoPP_FIND_REQUIRED) message(FATAL_ERROR "Could not find CryptoPP") endif() endif() add_library(cryptopp INTERFACE IMPORTED) target_include_directories( cryptopp INTERFACE ${CryptoPP_INCLUDE_DIRS}) target_link_libraries( cryptopp INTERFACE ${CryptoPP_LIBRARIES}) colmap-4.0.4/cmake/FindDependencies.cmake000066400000000000000000000472361517363634500202740ustar00rootroot00000000000000if(COLMAP_FIND_QUIETLY) set(COLMAP_FIND_TYPE QUIET) else() set(COLMAP_FIND_TYPE REQUIRED) endif() # Track all the compile definitions set(COLMAP_COMPILE_DEFINITIONS) if(LSD_ENABLED) list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_LSD_ENABLED) message(STATUS "Enabling LSD support") else() message(STATUS "Disabling LSD support") endif() find_package(OpenMP REQUIRED COMPONENTS C CXX) find_package(Boost ${COLMAP_FIND_TYPE} COMPONENTS graph program_options OPTIONAL_COMPONENTS system) find_package(Eigen3 ${COLMAP_FIND_TYPE}) find_package(OpenImageIO ${COLMAP_FIND_TYPE}) find_package(Metis ${COLMAP_FIND_TYPE}) find_package(Glog ${COLMAP_FIND_TYPE}) if(DEFINED glog_VERSION_MAJOR) # Older versions of glog don't export version variables. list(APPEND COLMAP_COMPILE_DEFINITIONS GLOG_VERSION_MAJOR=${glog_VERSION_MAJOR}) list(APPEND COLMAP_COMPILE_DEFINITIONS GLOG_VERSION_MINOR=${glog_VERSION_MINOR}) endif() find_package(SQLite3 ${COLMAP_FIND_TYPE}) # Older CMake versions define SQLite::SQLite3 instead of SQLite3::SQLite3. if(NOT TARGET SQLite3::SQLite3 AND TARGET SQLite::SQLite3) add_library(SQLite3::SQLite3 ALIAS SQLite::SQLite3) endif() set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL ${COLMAP_FIND_TYPE}) find_package(Glew ${COLMAP_FIND_TYPE}) find_package(Git) find_package(CHOLMOD REQUIRED) find_package(Ceres ${COLMAP_FIND_TYPE}) if(NOT TARGET Ceres::ceres) # Older Ceres versions don't come with an imported interface target. add_library(Ceres::ceres INTERFACE IMPORTED) target_include_directories( Ceres::ceres INTERFACE ${CERES_INCLUDE_DIRS}) target_link_libraries( Ceres::ceres INTERFACE ${CERES_LIBRARIES}) endif() if(TESTS_ENABLED) find_package(GTest ${COLMAP_FIND_TYPE}) endif() if(CGAL_ENABLED) set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE TRUE) # We do not use CGAL data. This prevents an unnecessary warning by CMake. set(CGAL_DATA_DIR "unused") find_package(CGAL ${COLMAP_FIND_TYPE}) endif() if(CGAL_FOUND) list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_CGAL_ENABLED) list(APPEND CGAL_LIBRARY ${CGAL_LIBRARIES}) message(STATUS "Found CGAL") message(STATUS " Includes : ${CGAL_INCLUDE_DIRS}") message(STATUS " Libraries : ${CGAL_LIBRARY}") if(NOT TARGET CGAL) # Older CGAL versions don't come with an imported interface target. add_library(CGAL INTERFACE IMPORTED) target_include_directories( CGAL INTERFACE ${CGAL_INCLUDE_DIRS} ${GMP_INCLUDE_DIR}) target_link_libraries( CGAL INTERFACE ${CGAL_LIBRARY} ${GMP_LIBRARIES}) endif() list(APPEND COLMAP_LINK_DIRS ${CGAL_LIBRARIES_DIR}) else() if(CGAL_ENABLED) set(CGAL_ENABLED OFF) message(STATUS "Disabling CGAL support (not found)") else() message(STATUS "Disabling CGAL support") endif() endif() if(DOWNLOAD_ENABLED) # The OpenSSL package in vcpkg seems broken under Windows and leads to # missing certificate verification when connecting to SSL servers. We # therefore use curl[sspi] (i.e., native Windows SSL/TLS) under Windows # and curl[openssl] otherwise. find_package(CURL QUIET) set(CRYPTO_FOUND FALSE) if(IS_MSVC AND IS_ARM64) # OpenSSL crashes for ARM64 under Windows. We therefore fall back to # CryptoPP as an alternative to OpenSSL for SHA256 computation. find_package(CryptoPP QUIET) if(CryptoPP_FOUND) set(CRYPTO_FOUND TRUE) else() message(STATUS "CryptoPP not found") endif() else() find_package(OpenSSL QUIET COMPONENTS Crypto) if(OpenSSL_FOUND) set(CRYPTO_FOUND TRUE) else() message(STATUS "OpenSSL::Crypto not found") endif() endif() if(CURL_FOUND AND CRYPTO_FOUND) message(STATUS "Enabling download support") list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_DOWNLOAD_ENABLED) else() set(DOWNLOAD_ENABLED OFF) message(STATUS "Disabling download support (Curl/Crypto not found)") endif() else() message(STATUS "Disabling download support") endif() if(NOT FETCH_POSELIB) find_package(PoseLib ${COLMAP_FIND_TYPE}) endif() if(NOT FETCH_FAISS) find_package(faiss ${COLMAP_FIND_TYPE}) endif() set(COLMAP_LINK_DIRS ${Boost_LIBRARY_DIRS}) set(CUDA_MIN_VERSION "7.0") if(CUDA_ENABLED) if(CMAKE_VERSION VERSION_LESS 3.17) find_package(CUDA QUIET) if(CUDA_FOUND) message(STATUS "Found CUDA version ${CUDA_VERSION} installed in " "${CUDA_TOOLKIT_ROOT_DIR} via legacy CMake (<3.17) module. " "Using the legacy CMake module means that any installation of " "COLMAP will require that the CUDA libraries are " "available under LD_LIBRARY_PATH.") message(STATUS "Found CUDA ") message(STATUS " Includes : ${CUDA_INCLUDE_DIRS}") message(STATUS " Libraries : ${CUDA_LIBRARIES}") enable_language(CUDA) macro(declare_imported_cuda_target module) add_library(CUDA::${module} INTERFACE IMPORTED) target_include_directories( CUDA::${module} INTERFACE ${CUDA_INCLUDE_DIRS}) target_link_libraries( CUDA::${module} INTERFACE ${CUDA_${module}_LIBRARY} ${ARGN}) endmacro() declare_imported_cuda_target(cudart ${CUDA_LIBRARIES}) declare_imported_cuda_target(curand ${CUDA_LIBRARIES}) set(CUDAToolkit_VERSION "${CUDA_VERSION_STRING}") set(CUDAToolkit_BIN_DIR "${CUDA_TOOLKIT_ROOT_DIR}/bin") else() message(STATUS "Disabling CUDA support (not found)") endif() else() find_package(CUDAToolkit QUIET) if(CUDAToolkit_FOUND) set(CUDA_FOUND ON) enable_language(CUDA) else() message(STATUS "Disabling CUDA support (not found)") endif() endif() else() message(STATUS "Disabling CUDA support") endif() if(CUDA_ENABLED AND CUDA_FOUND) if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) set(CMAKE_CUDA_ARCHITECTURES "native") endif() list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_CUDA_ENABLED) # Do not show warnings if the architectures are deprecated. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Wno-deprecated-gpu-targets") # Suppress warnings related to Eigen: # Calling a constexpr __host__ function from a __host__ __device__ function is not allowed. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr") # Explicitly set PIC flags for CUDA targets. if(NOT IS_MSVC) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --compiler-options -fPIC") endif() # Handle MSVC runtime library for CUDA to support static CRT linking. # CMake's default CUDA flags use /MD (dynamic), but if the user is building # with static CRT (/MT), we need to override the CUDA flags to match. if(IS_MSVC) # Detect the runtime library from CMAKE_MSVC_RUNTIME_LIBRARY or CXX flags set(_COLMAP_USE_STATIC_RUNTIME OFF) if(DEFINED CMAKE_MSVC_RUNTIME_LIBRARY) if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "MultiThreaded" AND NOT CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "DLL") set(_COLMAP_USE_STATIC_RUNTIME ON) endif() elseif(CMAKE_CXX_FLAGS_DEBUG MATCHES "/MTd" OR CMAKE_CXX_FLAGS_RELEASE MATCHES "/MT[^d]" OR CMAKE_CXX_FLAGS MATCHES "/MT") set(_COLMAP_USE_STATIC_RUNTIME ON) endif() if(_COLMAP_USE_STATIC_RUNTIME) message(STATUS "CUDA: Using static MSVC runtime library (/MT)") # Replace /MD with /MT in CUDA flags for each build type foreach(_BUILD_TYPE DEBUG RELEASE RELWITHDEBINFO MINSIZEREL) if(DEFINED CMAKE_CUDA_FLAGS_${_BUILD_TYPE}) string(REPLACE "-MDd" "-MTd" CMAKE_CUDA_FLAGS_${_BUILD_TYPE} "${CMAKE_CUDA_FLAGS_${_BUILD_TYPE}}") string(REPLACE "-MD" "-MT" CMAKE_CUDA_FLAGS_${_BUILD_TYPE} "${CMAKE_CUDA_FLAGS_${_BUILD_TYPE}}") string(REPLACE "/MDd" "/MTd" CMAKE_CUDA_FLAGS_${_BUILD_TYPE} "${CMAKE_CUDA_FLAGS_${_BUILD_TYPE}}") string(REPLACE "/MD" "/MT" CMAKE_CUDA_FLAGS_${_BUILD_TYPE} "${CMAKE_CUDA_FLAGS_${_BUILD_TYPE}}") endif() endforeach() endif() unset(_COLMAP_USE_STATIC_RUNTIME) endif() message(STATUS "Enabling CUDA support (version: ${CUDAToolkit_VERSION}, " "archs: ${CMAKE_CUDA_ARCHITECTURES})") else() set(CUDA_ENABLED OFF) endif() if(ONNX_ENABLED) if(FETCH_ONNX) include(FetchContent) message(STATUS "Configuring onnxruntime...") set(ONNX_VERSION "1.24.1") # ONNX Runtime >= 1.22 GPU binaries are built with CUDA >= 12 if(ONNX_VERSION VERSION_GREATER_EQUAL "1.22" AND CUDA_ENABLED AND CUDA_FOUND AND CUDAToolkit_VERSION VERSION_LESS "12.0") message(WARNING "ONNX Runtime ${ONNX_VERSION} GPU binary is built with CUDA >= 12, " "but CUDA ${CUDAToolkit_VERSION} was detected. The ONNX Runtime CUDA " "execution provider may fail at runtime, CPU execution will continue to work. " "Consider upgrading CUDA to >= 12 or using a source-built onnxruntime.") endif() if(IS_MACOS) if(CMAKE_OSX_ARCHITECTURES) set(_COLMAP_MACOS_ARCH ${CMAKE_OSX_ARCHITECTURES}) else() set(_COLMAP_MACOS_ARCH ${CMAKE_SYSTEM_PROCESSOR}) endif() if(_COLMAP_MACOS_ARCH STREQUAL "x86_64") message(FATAL_ERROR "x86_64 is not supported for onnxruntime") else() FetchContent_Declare(onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-osx-arm64-${ONNX_VERSION}.tgz URL_HASH SHA256=c2969315cd9ce0f5fa04f6b53ff72cb92f87f7dcf38e88cacfa40c8f983fbba9 ${_fetch_content_declare_args} ) endif() elseif(IS_LINUX) if(IS_ARM64) FetchContent_Declare(onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-aarch64-${ONNX_VERSION}.tgz URL_HASH SHA256=0f56edd68f7602df790b68b874a46b115add037e88385c6c842bb763b39b9f89 ${_fetch_content_declare_args} ) else() if(CUDA_ENABLED) FetchContent_Declare(onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-x64-gpu-${ONNX_VERSION}.tgz URL_HASH SHA256=1c468821456b7863640555e31ee5b71e56bb959874b9db0dbf79503997993673 ${_fetch_content_declare_args} ) else() FetchContent_Declare(onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-x64-${ONNX_VERSION}.tgz URL_HASH SHA256=9142552248b735920f9390027e4512a2cacf8946a1ffcbe9071a5c210531026f ${_fetch_content_declare_args} ) endif() endif() elseif(IS_WINDOWS) FetchContent_Declare(onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-win-x64-gpu-${ONNX_VERSION}.zip URL_HASH SHA256=176af2aade9eb9e429cd2a738aa5d71a1f20ec7123e4b99a382ad62b9db970fb ${_fetch_content_declare_args} ) endif() FetchContent_MakeAvailable(onnxruntime) set(ONNX_INCLUDE_DIR ${onnxruntime_BINARY_DIR}/include/onnxruntime) if(NOT EXISTS ${ONNX_INCLUDE_DIR}) file(MAKE_DIRECTORY ${ONNX_INCLUDE_DIR}) file(COPY ${onnxruntime_SOURCE_DIR}/include/ DESTINATION ${ONNX_INCLUDE_DIR}/) endif() set(onnxruntime_LIB_DIR ${onnxruntime_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) if(NOT EXISTS ${onnxruntime_LIB_DIR}) file(MAKE_DIRECTORY ${onnxruntime_LIB_DIR}) file(COPY ${onnxruntime_SOURCE_DIR}/lib/ DESTINATION ${onnxruntime_LIB_DIR}) file(REMOVE_RECURSE ${onnxruntime_LIB_DIR}/cmake) file(REMOVE_RECURSE ${onnxruntime_LIB_DIR}/pkgconfig) endif() if(NOT IS_WINDOWS) set(ONNX_DATA_DIR ${onnxruntime_BINARY_DIR}/share/onnxruntime) if(NOT EXISTS ${ONNX_DATA_DIR}) file(MAKE_DIRECTORY ${ONNX_DATA_DIR}) file(COPY ${onnxruntime_SOURCE_DIR}/lib/cmake/onnxruntime/ DESTINATION ${ONNX_DATA_DIR}/cmake/) file(REMOVE_RECURSE ${onnxruntime_SOURCE_DIR}/lib/cmake) # The downloaded cmake configs may reference lib64/ (e.g. on Linux x64), # but the actual install directory depends on CMAKE_INSTALL_LIBDIR # (lib/ or lib64/ depending on the distro). Patch the configs to match. if(IS_LINUX AND NOT IS_ARM64) file(GLOB _onnx_cmake_configs "${ONNX_DATA_DIR}/cmake/*.cmake") foreach(_config_file ${_onnx_cmake_configs}) file(READ "${_config_file}" _config_content) string(REPLACE "/lib64/" "/${CMAKE_INSTALL_LIBDIR}/" _config_content "${_config_content}") file(WRITE "${_config_file}" "${_config_content}") endforeach() endif() endif() set(onnxruntime_CONFIG_DIR_HINTS ${ONNX_DATA_DIR}/cmake CACHE PATH "ONNX Runtime config directory hints") endif() set(onnxruntime_INCLUDE_DIR_HINTS ${onnxruntime_BINARY_DIR}/include CACHE PATH "ONNX Runtime include directory hints") set(onnxruntime_LIBRARY_DIR_HINTS ${onnxruntime_BINARY_DIR}/lib CACHE PATH "ONNX Runtime library directory hints") find_package(onnxruntime ${COLMAP_FIND_TYPE}) install(DIRECTORY "${onnxruntime_BINARY_DIR}/include/" TYPE INCLUDE) if(IS_WINDOWS) # On Windows, selectively install Libs to lib/. Always install core Libs. # For not supporting TensorRT/ROCM/etc. as a runtime, so not installing it intentionally. install(FILES "${onnxruntime_LIB_DIR}/onnxruntime.lib" "${onnxruntime_LIB_DIR}/onnxruntime_providers_shared.lib" TYPE LIB) # Only install CUDA provider Lib if CUDA is enabled. if(CUDA_ENABLED) install(FILES "${onnxruntime_LIB_DIR}/onnxruntime_providers_cuda.lib" TYPE LIB) endif() # On Windows, selectively install DLLs to bin/. Always install core DLLs. # For not supporting TensorRT/ROCM/etc. as a runtime, so not installing it intentionally. install(FILES "${onnxruntime_LIB_DIR}/onnxruntime.dll" "${onnxruntime_LIB_DIR}/onnxruntime_providers_shared.dll" TYPE BIN) # Only install CUDA provider DLL if CUDA is enabled. if(CUDA_ENABLED) install(FILES "${onnxruntime_LIB_DIR}/onnxruntime_providers_cuda.dll" TYPE BIN) endif() else() # On Linux/macOS, selectively install library files. Always install core libraries. # Not supporting TensorRT/ROCM/etc. as a runtime, so not installing them. if(IS_MACOS) file(GLOB onnxruntime_CORE_LIBS "${onnxruntime_LIB_DIR}/libonnxruntime.dylib" "${onnxruntime_LIB_DIR}/libonnxruntime.*.dylib" "${onnxruntime_LIB_DIR}/libonnxruntime_providers_shared.dylib") install(FILES ${onnxruntime_CORE_LIBS} TYPE LIB) else() file(GLOB onnxruntime_CORE_LIBS "${onnxruntime_LIB_DIR}/libonnxruntime.so*" "${onnxruntime_LIB_DIR}/libonnxruntime_providers_shared.so*") install(FILES ${onnxruntime_CORE_LIBS} TYPE LIB) # Only install CUDA provider if CUDA is enabled. if(CUDA_ENABLED) file(GLOB onnxruntime_CUDA_LIBS "${onnxruntime_LIB_DIR}/libonnxruntime_providers_cuda.so*") install(FILES ${onnxruntime_CUDA_LIBS} TYPE LIB) endif() endif() endif() if(EXISTS "${onnxruntime_BINARY_DIR}/share") install(DIRECTORY "${onnxruntime_BINARY_DIR}/share/" TYPE DATA) endif() message(STATUS "Configuring onnxruntime... done") else() find_package(onnxruntime ${COLMAP_FIND_TYPE}) if(NOT onnxruntime_FOUND) message(STATUS "Disabling ONNX support (not found)") set(ONNX_ENABLED OFF) endif() endif() else() message(STATUS "Disabling ONNX support") endif() if(TARGET onnxruntime::onnxruntime) list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_ONNX_ENABLED) message(STATUS "Enabling ONNX support") endif() if(GUI_ENABLED) find_package(QT NAMES Qt5 Qt6 REQUIRED) set(COLMAP_QT_COMPONENTS Core OpenGL Widgets) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) list(APPEND COLMAP_QT_COMPONENTS OpenGLWidgets) endif() find_package(Qt${QT_VERSION_MAJOR} ${COLMAP_FIND_TYPE} ${COLMAP_QT_COMPONENTS}) message(STATUS "Found Qt") message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}Core_DIR}") message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}OpenGL_DIR}") message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}Widgets_DIR}") if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}OpenGLWidgets_DIR}") endif() if(Qt5_FOUND) # Qt5 was built with -reduce-relocations. if(Qt5_POSITION_INDEPENDENT_CODE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Workaround for Qt5 CMake config bug under Ubuntu 20.04: https://gitlab.kitware.com/cmake/cmake/-/issues/16915 if(TARGET Qt5::Core) get_property(core_options TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS) string(REPLACE "-fPIC" "" new_qt5_core_options "${core_options}") set_property(TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS ${new_qt5_core_options}) set_property(TARGET Qt5::Core PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE "ON") if(NOT IS_MSVC) set(CMAKE_CXX_COMPILE_OPTIONS_PIE "-fPIC") endif() endif() endif() endif() if(QT_FOUND) # Enable automatic compilation of Qt resource files. set(CMAKE_AUTORCC ON) endif() endif() if(GUI_ENABLED AND Qt${QT_VERSION_MAJOR}_FOUND) list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_GUI_ENABLED) message(STATUS "Enabling GUI support") else() set(GUI_ENABLED OFF) message(STATUS "Disabling GUI support") endif() if(OPENGL_ENABLED) if(NOT GUI_ENABLED) message(STATUS "Disabling GUI also disables OpenGL") set(OPENGL_ENABLED OFF) else() message(STATUS "Enabling OpenGL support") endif() else() message(STATUS "Disabling OpenGL support") endif() set(GPU_ENABLED OFF) if(OPENGL_ENABLED OR CUDA_ENABLED) list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_GPU_ENABLED) message(STATUS "Enabling GPU support (OpenGL: ${OPENGL_ENABLED}, CUDA: ${CUDA_ENABLED})") set(GPU_ENABLED ON) endif() colmap-4.0.4/cmake/FindGlew.cmake000066400000000000000000000067621517363634500166030ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for Glew library. # # The following variables are set by this module: # # GLEW_FOUND: TRUE if Glew is found. # GLEW::GLEW: Imported target to link against. # # The following variables control the behavior of this module: # # GLEW_INCLUDE_DIR_HINTS: List of additional directories in which to # search for Glew includes. # GLEW_LIBRARY_DIR_HINTS: List of additional directories in which to # search for Glew libraries. set(GLEW_INCLUDE_DIR_HINTS "" CACHE PATH "Glew include directory") set(GLEW_LIBRARY_DIR_HINTS "" CACHE PATH "Glew library directory") unset(GLEW_FOUND) unset(GLEW_INCLUDE_DIRS) unset(GLEW_LIBRARIES) find_package(Glew CONFIG QUIET) if(TARGET GLEW::GLEW) set(GLEW_FOUND TRUE) message(STATUS "Found Glew") message(STATUS " Target : GLEW::GLEW") else() find_path(GLEW_INCLUDE_DIRS NAMES GL/glew.h PATHS ${GLEW_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /sw/include /opt/include /opt/local/include) find_library(GLEW_LIBRARIES NAMES GLEW Glew glew glew32 PATHS ${GLEW_LIBRARY_DIR_HINTS} /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib /sw/lib /opt/lib /opt/local/lib) if(GLEW_INCLUDE_DIRS AND GLEW_LIBRARIES) set(GLEW_FOUND TRUE) message(STATUS "Found Glew") message(STATUS " Includes : ${GLEW_INCLUDE_DIRS}") message(STATUS " Libraries : ${GLEW_LIBRARIES}") else() set(GLEW_FOUND FALSE) endif() add_library(GLEW::GLEW INTERFACE IMPORTED) target_include_directories( GLEW::GLEW INTERFACE ${GLEW_INCLUDE_DIRS}) target_link_libraries( GLEW::GLEW INTERFACE ${GLEW_LIBRARIES}) endif() if(NOT GLEW_FOUND AND GLEW_FIND_REQUIRED) message(FATAL_ERROR "Could not find Glew") endif() colmap-4.0.4/cmake/FindGlog.cmake000066400000000000000000000101261517363634500165620ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for Glog library. # # The following variables are set by this module: # # GLOG_FOUND: TRUE if Glog is found. # glog::glog: Imported target to link against. # # The following variables control the behavior of this module: # # GLOG_INCLUDE_DIR_HINTS: List of additional directories in which to # search for Glog includes. # GLOG_LIBRARY_DIR_HINTS: List of additional directories in which to # search for Glog libraries. set(GLOG_INCLUDE_DIR_HINTS "" CACHE PATH "Glog include directory") set(GLOG_LIBRARY_DIR_HINTS "" CACHE PATH "Glog library directory") unset(GLOG_FOUND) find_package(glog CONFIG QUIET) if(TARGET glog::glog) set(GLOG_FOUND TRUE) message(STATUS "Found Glog") message(STATUS " Target : glog::glog") else() # Older versions of glog don't come with a find_package config. # Fall back to custom logic to find the library and remap to imported target. include(FindPackageHandleStandardArgs) list(APPEND GLOG_CHECK_INCLUDE_DIRS /usr/local/include /usr/local/homebrew/include /opt/local/var/macports/software /opt/local/include /usr/include) list(APPEND GLOG_CHECK_PATH_SUFFIXES glog/include glog/Include Glog/include Glog/Include src/windows) list(APPEND GLOG_CHECK_LIBRARY_DIRS /usr/local/lib /usr/local/homebrew/lib /opt/local/lib /usr/lib) list(APPEND GLOG_CHECK_LIBRARY_SUFFIXES glog/lib glog/Lib Glog/lib Glog/Lib x64/Release) find_path(GLOG_INCLUDE_DIRS NAMES glog/logging.h PATHS ${GLOG_INCLUDE_DIR_HINTS} ${GLOG_CHECK_INCLUDE_DIRS} PATH_SUFFIXES ${GLOG_CHECK_PATH_SUFFIXES}) find_library(GLOG_LIBRARIES NAMES glog libglog PATHS ${GLOG_LIBRARY_DIR_HINTS} ${GLOG_CHECK_LIBRARY_DIRS} PATH_SUFFIXES ${GLOG_CHECK_LIBRARY_SUFFIXES}) if(GLOG_INCLUDE_DIRS AND GLOG_LIBRARIES) set(GLOG_FOUND TRUE) message(STATUS "Found Glog") message(STATUS " Includes : ${GLOG_INCLUDE_DIRS}") message(STATUS " Libraries : ${GLOG_LIBRARIES}") endif() add_library(glog::glog INTERFACE IMPORTED) target_include_directories(glog::glog INTERFACE ${GLOG_INCLUDE_DIRS}) target_link_libraries(glog::glog INTERFACE ${GLOG_LIBRARIES}) endif() if(NOT GLOG_FOUND AND GLOG_FIND_REQUIRED) message(FATAL_ERROR "Could not find Glog") endif() colmap-4.0.4/cmake/FindMetis.cmake000066400000000000000000000073071517363634500167620ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for Metis library. # # The following variables are set by this module: # # METIS_FOUND: TRUE if Metis is found. # metis: Imported target to link against. # # The following variables control the behavior of this module: # # METIS_INCLUDE_DIR_HINTS: List of additional directories in which to # search for Metis includes. # METIS_LIBRARY_DIR_HINTS: List of additional directories in which to # search for Metis libraries. set(METIS_INCLUDE_DIR_HINTS "" CACHE PATH "Metis include directory") set(METIS_LIBRARY_DIR_HINTS "" CACHE PATH "Metis library directory") unset(METIS_FOUND) find_package(metis CONFIG QUIET) if(TARGET metis) set(METIS_FOUND TRUE) message(STATUS "Found Metis") message(STATUS " Target : metis") else() list(APPEND METIS_CHECK_INCLUDE_DIRS ${METIS_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND METIS_CHECK_LIBRARY_DIRS ${METIS_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /opt/lib /opt/local/lib ) find_path(METIS_INCLUDE_DIRS NAMES metis.h PATHS ${METIS_CHECK_INCLUDE_DIRS}) find_library(METIS_LIBRARIES NAMES metis PATHS ${METIS_CHECK_LIBRARY_DIRS}) find_library(GK_LIBRARIES NAMES GKlib PATHS ${METIS_CHECK_LIBRARY_DIRS}) if(GK_LIBRARIES) set(METIS_LIBRARIES ${METIS_LIBRARIES} ${GK_LIBRARIES}) message(STATUS "Found GKlib") endif() if(METIS_INCLUDE_DIRS AND METIS_LIBRARIES) set(METIS_FOUND TRUE) message(STATUS "Found Metis") message(STATUS " Includes : ${METIS_INCLUDE_DIRS}") message(STATUS " Libraries : ${METIS_LIBRARIES}") endif() add_library(metis INTERFACE IMPORTED) target_include_directories( metis INTERFACE ${METIS_INCLUDE_DIRS}) target_link_libraries( metis INTERFACE ${METIS_LIBRARIES}) endif() if(NOT METIS_FOUND AND METIS_FIND_REQUIRED) message(FATAL_ERROR "Could not find Metis") endif() colmap-4.0.4/cmake/Findonnxruntime.cmake000066400000000000000000000133421517363634500202630ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package module for onnxruntime library. # # The following variables are set by this module: # # onnxruntime_FOUND: TRUE if onnxruntime is found. # onnxruntime::onnxruntime: Imported target to link against. # # The following variables control the behavior of this module: # # onnxruntime_CONFIG_DIR_HINTS: List of additional directories in which to # search for onnxruntime CMake configs. # onnxruntime_INCLUDE_DIR_HINTS: List of additional directories in which to # search for onnxruntime includes. # onnxruntime_LIBRARY_DIR_HINTS: List of additional directories in which to # search for onnxruntime libraries. set(onnxruntime_CONFIG_DIR_HINTS "" CACHE PATH "onnxruntime config directory") set(onnxruntime_INCLUDE_DIR_HINTS "" CACHE PATH "onnxruntime include directory") set(onnxruntime_LIBRARY_DIR_HINTS "" CACHE PATH "onnxruntime library directory") unset(onnxruntime_FOUND) unset(onnxruntime_INCLUDE_DIRS) unset(onnxruntime_LIBRARIES) find_package(onnxruntime CONFIG QUIET PATHS ${onnxruntime_CONFIG_DIR_HINTS}) if(TARGET onnxruntime::onnxruntime) set(onnxruntime_FOUND TRUE) message(STATUS "Found onnxruntime") message(STATUS " Target : onnxruntime::onnxruntime") else() find_path(onnxruntime_INCLUDE_DIRS NAMES onnxruntime/onnxruntime_cxx_api.h PATHS ${onnxruntime_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /sw/include /opt/include /opt/local/include) list(APPEND onnxruntime_LIBRARY_DIR_HINTS /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib /sw/lib /opt/lib /opt/local/lib) find_library(onnxruntime_LIBRARIES NAMES onnxruntime libonnxruntime PATHS ${onnxruntime_LIBRARY_DIR_HINTS}) find_library(onnxruntime_PROVIDERS_SHARED_LIBRARY NAMES onnxruntime_providers_shared libonnxruntime_providers_shared PATHS ${onnxruntime_LIBRARY_DIR_HINTS}) if(CUDA_ENABLED) find_library(onnxruntime_PROVIDERS_CUDA_LIBRARY NAMES onnxruntime_providers_cuda libonnxruntime_providers_cuda PATHS ${onnxruntime_LIBRARY_DIR_HINTS}) endif() if(onnxruntime_INCLUDE_DIRS AND onnxruntime_LIBRARIES) if(onnxruntime_PROVIDERS_SHARED_LIBRARY) list(APPEND onnxruntime_LIBRARIES ${onnxruntime_PROVIDERS_SHARED_LIBRARY}) endif() if(onnxruntime_PROVIDERS_CUDA_LIBRARY) list(APPEND onnxruntime_LIBRARIES ${onnxruntime_PROVIDERS_CUDA_LIBRARY}) endif() set(onnxruntime_FOUND TRUE) message(STATUS "Found onnxruntime") message(STATUS " Includes : ${onnxruntime_INCLUDE_DIRS}") message(STATUS " Libraries : ${onnxruntime_LIBRARIES}") else() set(onnxruntime_FOUND FALSE) endif() add_library(onnxruntime::onnxruntime INTERFACE IMPORTED) target_include_directories( onnxruntime::onnxruntime INTERFACE ${onnxruntime_INCLUDE_DIRS}/onnxruntime) target_link_libraries( onnxruntime::onnxruntime INTERFACE ${onnxruntime_LIBRARIES}) # This is a hack to make sure that the onnxruntime dll is copied to the output directory, # since vcpkg's custom add_library/add_executable macros copy any dependencies from vcpkg's # installed directory to the output directory. # See: https://github.com/microsoft/vcpkg/blob/fb7ba3b89b0d8e3e56b0508a144fe85015edfab6/scripts/buildsystems/vcpkg.cmake#L607 if(IS_WINDOWS AND VCPKG_INSTALLED_DIR) foreach(_lib ${onnxruntime_LIBRARIES}) get_filename_component(_lib_dir "${_lib}" DIRECTORY) get_filename_component(_lib_name "${_lib}" NAME_WE) set(_dlls "${_lib_dir}/${_lib_name}.dll" "${_lib_dir}/../bin/${_lib_name}.dll") foreach(_dll ${_dlls}) if( EXISTS "${_dll}" ) file(COPY "${_dll}" DESTINATION "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin") endif() endforeach() endforeach() endif() endif() if(NOT onnxruntime_FOUND AND onnxruntime_FIND_REQUIRED) message(FATAL_ERROR "Could not find onnxruntime") endif() colmap-4.0.4/cmake/GenerateVersionDefinitions.cmake000066400000000000000000000057341517363634500223760ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. if (DEFINED GIT_COMMIT_ID OR DEFINED GIT_COMMIT_DATE) message(STATUS "Using custom-defined GIT_COMMIT_ID (${GIT_COMMIT_ID}) " "and GIT_COMMIT_DATE (${GIT_COMMIT_DATE})") elseif(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_ID ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%ad --date=short WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_DATE ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) # Re-generate version.cc if the git index changes. set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/.git/index" ) else() set(GIT_COMMIT_ID "Unknown") set(GIT_COMMIT_DATE "Unknown") endif() # Parse COLMAP_VERSION to extract MAJOR, MINOR, PATCH components. string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" _version_match "${COLMAP_VERSION}") set(COLMAP_VERSION_MAJOR "${CMAKE_MATCH_1}") set(COLMAP_VERSION_MINOR "${CMAKE_MATCH_2}") set(COLMAP_VERSION_PATCH "${CMAKE_MATCH_3}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/colmap/util/version.cc.in" "${CMAKE_CURRENT_SOURCE_DIR}/src/colmap/util/version.cc") colmap-4.0.4/cmake/colmap-config-version.cmake.in000066400000000000000000000034141517363634500217010ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. set(PACKAGE_VERSION "@COLMAP_VERSION@") if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() colmap-4.0.4/cmake/colmap-config.cmake.in000066400000000000000000000071351517363634500202220ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Find package config for COLMAP library. # # The following variables are set by this config: # # COLMAP_FOUND: TRUE if COLMAP is found. # COLMAP_VERSION: COLMAP version. # # The colmap::colmap imported interface target is defined. @PACKAGE_INIT@ set(COLMAP_FOUND FALSE) # Set hints for finding dependency packages. set(METIS_INCLUDE_DIR_HINTS @METIS_INCLUDE_DIR_HINTS@) set(METIS_LIBRARY_DIR_HINTS @METIS_LIBRARY_DIR_HINTS@) set(GLEW_INCLUDE_DIR_HINTS @GLEW_INCLUDE_DIR_HINTS@) set(GLEW_LIBRARY_DIR_HINTS @GLEW_LIBRARY_DIR_HINTS@) set(GLOG_INCLUDE_DIR_HINTS @GLOG_INCLUDE_DIR_HINTS@) set(GLOG_LIBRARY_DIR_HINTS @GLOG_LIBRARY_DIR_HINTS@) set(CryptoPP_INCLUDE_DIR_HINTS @CryptoPP_INCLUDE_DIR_HINTS@) set(CryptoPP_LIBRARY_DIR_HINTS @CryptoPP_LIBRARY_DIR_HINTS@) # Find dependency packages. set(TEMP_CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) set(CMAKE_MODULE_PATH ${PACKAGE_PREFIX_DIR}/share/colmap/cmake) # Set the exported variables. set(COLMAP_FOUND TRUE) set(COLMAP_VERSION @COLMAP_VERSION@) set(OPENMP_ENABLED @OPENMP_ENABLED@) set(CUDA_ENABLED @CUDA_ENABLED@) set(CUDA_MIN_VERSION @CUDA_MIN_VERSION@) set(ONNX_ENABLED @ONNX_ENABLED@) set(DOWNLOAD_ENABLED @DOWNLOAD_ENABLED@) set(GUI_ENABLED @GUI_ENABLED@) set(CGAL_ENABLED @CGAL_ENABLED@) set(LSD_ENABLED @LSD_ENABLED@) set(FETCH_POSELIB @FETCH_POSELIB@) set(FETCH_FAISS @FETCH_FAISS@) set(FETCH_ONNX FALSE) if(@FETCH_ONNX@) if(ONNX_ENABLED AND EXISTS ${PACKAGE_PREFIX_DIR}/share/onnxruntime/cmake) set(onnxruntime_DIR ${PACKAGE_PREFIX_DIR}/share/onnxruntime/cmake) endif() set(onnxruntime_INCLUDE_DIR_HINTS ${PACKAGE_PREFIX_DIR}/include CACHE PATH "ONNX Runtime include directory hints") set(onnxruntime_LIBRARY_DIR_HINTS ${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@ CACHE PATH "ONNX Runtime library directory hints") endif() include(${PACKAGE_PREFIX_DIR}/share/colmap/colmap-targets.cmake) include(${PACKAGE_PREFIX_DIR}/share/colmap/cmake/FindDependencies.cmake) check_required_components(colmap) # Reset to previous value set(CMAKE_MODULE_PATH ${TEMP_CMAKE_MODULE_PATH}) colmap-4.0.4/doc/000077500000000000000000000000001517363634500135545ustar00rootroot00000000000000colmap-4.0.4/doc/COLMAP.desktop000066400000000000000000000003601517363634500161210ustar00rootroot00000000000000[Desktop Entry] Name=COLMAP Comment=Structure-from-Motion and Multi-View Stereo Exec=colmap gui Icon=colmap Terminal=false Categories=Graphics;3DGraphics; Keywords=3d;reconstruction;structure-from-motion;multi-view-stereo; Type=Application colmap-4.0.4/doc/Makefile000077500000000000000000000151521517363634500152230ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/COLMAP.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/COLMAP.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/COLMAP" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/COLMAP" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." colmap-4.0.4/doc/_static/000077500000000000000000000000001517363634500152025ustar00rootroot00000000000000colmap-4.0.4/doc/_static/custom.css000066400000000000000000000007551517363634500172350ustar00rootroot00000000000000/* Restore version display removed in sphinx-rtd-theme 3.x. */ .wy-side-nav-search > div.version { margin-top: -.4045em; margin-bottom: .809em; font-weight: normal; color: hsla(0, 0%, 100%, .3); } /* Fixes https://github.com/readthedocs/sphinx_rtd_theme/issues/1301#issuecomment-1876120817 */ .py.property { display: block !important; } /* Better display of multi-line signatures. */ dt.sig > dl > dd { margin-bottom: 0px; } dt.sig > dl { margin-bottom: 0px; } colmap-4.0.4/doc/_templates/000077500000000000000000000000001517363634500157115ustar00rootroot00000000000000colmap-4.0.4/doc/_templates/layout.html000066400000000000000000000011451517363634500201150ustar00rootroot00000000000000{% extends "!layout.html" %} {% block sidebartitle %} {%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %} {%- set _root_doc = root_doc|default(master_doc) %} {% if not theme_logo_only %}{{ project }}{% endif %} {%- if logo or logo_url %} {%- endif %} {%- if version %}
{{ version }}
{%- endif %} {%- include "searchbox.html" %} {% endblock %} colmap-4.0.4/doc/bibliography.rst000077500000000000000000000042011517363634500167610ustar00rootroot00000000000000Bibliography ============ .. [schoenberger_thesis] Johannes L. Schönberger. "Robust Methods for Accurate and Efficient 3D Modeling from Unstructured Imagery." ETH Zürich, 2018. .. [furukawa10] Furukawa, Yasutaka, and Jean Ponce. "Accurate, dense, and robust multiview stereopsis." Transactions on Pattern Analysis and Machine Intelligence, 2010. .. [garland1997] Garland, Michael, and Heckbert, Paul S. "Surface simplification using quadric error metrics." Proceedings of SIGGRAPH, 1997. .. [hofer16] Hofer, M., Maurer, M., and Bischof, H. Efficient 3D Scene Abstraction Using Line Segments, Computer Vision and Image Understanding, 2016. .. [jancosek11] Jancosek, Michal, and Tomás Pajdla. "Multi-view reconstruction preserving weakly-supported surfaces." Conference on Computer Vision and Pattern Recognition, 2011. .. [kazhdan2013] Kazhdan, Michael and Hoppe, Hugues "Screened poisson surface reconstruction." ACM Transactions on Graphics (TOG), 2013. .. [schoenberger16sfm] Schönberger, Johannes Lutz and Frahm, Jan-Michael. "Structure-from-Motion Revisited." Conference on Computer Vision and Pattern Recognition, 2016. .. [schoenberger16mvs] Schönberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael. "Pixelwise View Selection for Unstructured Multi-View Stereo." European Conference on Computer Vision, 2016. .. [schoenberger16vote] Schönberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc "A Vote­-and­-Verify Strategy for Fast Spatial Verification in Image Retrieval." Asian Conference on Computer Vision, 2016. .. [lowe04] Lowe, David G. "Distinctive image features from scale-invariant keypoints". International journal of computer vision 60.2 (2004): 91-110. .. [waechter2014] Waechter, Michael and Moehrle, Nils and Goesele, Michael. "Let there be color! Large-scale texturing of 3D reconstructions." European Conference on Computer Vision, 2014. .. [wu13] Wu, Changchang. "Towards linear-time incremental structure from motion." International Conference 3D Vision, 2013. colmap-4.0.4/doc/cameras.rst000066400000000000000000000064761517363634500157360ustar00rootroot00000000000000Camera Models ============= COLMAP implements different camera models of varying complexity. If no intrinsic parameters are known a priori, it is generally best to use the simplest camera model that is complex enough to model the distortion effects: - ``SIMPLE_PINHOLE``, ``PINHOLE``: Use these camera models, if your images are undistorted a priori. These use one and two focal length parameters, respectively. Note that even in the case of undistorted images, COLMAP could try to improve the intrinsics with a more complex camera model. - ``SIMPLE_RADIAL``, ``RADIAL``: This should be the camera model of choice, if the intrinsics are unknown and every image has a different camera calibration, e.g., in the case of Internet photos. Both models are simplified versions of the ``OPENCV`` model only modeling radial distortion effects with one and two parameters, respectively. - ``OPENCV``, ``FULL_OPENCV``: Use these camera models, if you know the calibration parameters a priori. You can also try to let COLMAP estimate the parameters, if you share the intrinsics for multiple images. Note that the automatic estimation of parameters will most likely fail, if every image has a separate set of intrinsic parameters. - ``SIMPLE_RADIAL_FISHEYE``, ``RADIAL_FISHEYE``, ``OPENCV_FISHEYE``, ``FOV``, ``THIN_PRISM_FISHEYE``, ``RAD_TAN_THIN_PRISM_FISHEYE``: Use these camera models for fisheye lenses and note that all other models are not really capable of modeling the distortion effects of fisheye lenses. The ``FOV`` model is used by Google Project Tango (make sure to not initialize ``omega`` to zero). - ``SIMPLE_FISHEYE``, ``FISHEYE``: Use these camera models for fisheye lenses with equidistant projection where distortion can be ignored or has been pre-corrected. These models use the equidistant projection (theta = atan(r)) without any distortion parameters. ``SIMPLE_FISHEYE`` has a single focal length (f), while ``FISHEYE`` has two (fx, fy). - ``SIMPLE_DIVISION``, ``DIVISION``: Use these camera models, if you know the calibration parameters a priori. Similar to ``SIMPLE_RADIAL`` and ``RADIAL`` models, they can model simple radial distortion effects. The two models have first-order local equivalence for small distortions. You can inspect the estimated intrinsic parameters by double-clicking specific images in the model viewer or by exporting the model and opening the ``cameras.txt`` file. To achieve optimal reconstruction results, you might have to try different camera models for your problem. Generally, when the reconstruction fails and the estimated focal length values / distortion coefficients are grossly wrong, it is a sign of using a too complex camera model. Contrary, if COLMAP uses many iterative local and global bundle adjustments, it is a sign of using a too simple camera model that is not able to fully model the distortion effects. You can also share intrinsics between multiple images to obtain more reliable results (see :ref:`Share intrinsic camera parameters `) or you can fix the intrinsic parameters during the reconstruction (see :ref:`Fix intrinsic camera parameters `). Please, refer to the camera models header file for information on the parameters of the different camera models: https://github.com/colmap/colmap/blob/main/src/colmap/sensor/models.h colmap-4.0.4/doc/changelog.rst000066400000000000000000000000561517363634500162360ustar00rootroot00000000000000.. _changelog: .. include:: ../CHANGELOG.rst colmap-4.0.4/doc/cli.rst000066400000000000000000000443741517363634500150710ustar00rootroot00000000000000.. _cli: Command-line Interface ====================== The command-line interface provides access to all of COLMAP's functionality for automated scripting. Each core functionality is implemented as a command to the ``colmap`` executable. Run ``colmap -h`` to list the available commands (or ``COLMAP.bat -h`` under Windows). Note that if you run COLMAP from the CMake build folder, the executable is located at ``./src/colmap/exe/colmap``. To start the graphical user interface, run ``colmap gui``. Example ------- Assuming you stored the images of your project in the following structure:: /path/to/project/... +── images │   +── image1.jpg │   +── image2.jpg │   +── ... │   +── imageN.jpg The command for the automatic reconstruction tool would be:: # The project folder must contain a folder "images" with all the images. $ DATASET_PATH=/path/to/project $ colmap automatic_reconstructor \ --workspace_path $DATASET_PATH \ --image_path $DATASET_PATH/images Note that any command lists all available options using the ``-h,--help`` command-line argument. In case you need more control over the individual parameters of the reconstruction process, you can execute the following sequence of commands as an alternative to the automatic reconstruction command:: # The project folder must contain a folder "images" with all the images. $ DATASET_PATH=/path/to/dataset $ colmap feature_extractor \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images $ colmap exhaustive_matcher \ --database_path $DATASET_PATH/database.db $ mkdir -p $DATASET_PATH/sparse $ colmap mapper \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images \ --output_path $DATASET_PATH/sparse $ mkdir -p $DATASET_PATH/dense $ colmap image_undistorter \ --image_path $DATASET_PATH/images \ --input_path $DATASET_PATH/sparse/0 \ --output_path $DATASET_PATH/dense \ --output_type COLMAP \ --max_image_size 2000 $ colmap patch_match_stereo \ --workspace_path $DATASET_PATH/dense \ --workspace_format COLMAP \ --PatchMatchStereo.geom_consistency true $ colmap stereo_fusion \ --workspace_path $DATASET_PATH/dense \ --workspace_format COLMAP \ --input_type geometric \ --output_path $DATASET_PATH/dense/fused.ply $ colmap poisson_mesher \ --input_path $DATASET_PATH/dense/fused.ply \ --output_path $DATASET_PATH/dense/meshed-poisson.ply $ colmap delaunay_mesher \ --input_path $DATASET_PATH/dense \ --output_path $DATASET_PATH/dense/meshed-delaunay.ply # Optionally simplify a dense mesh to reduce its size. $ colmap mesh_simplifier \ --input_path $DATASET_PATH/dense/meshed-poisson.ply \ --output_path $DATASET_PATH/dense/meshed-poisson-simplified.ply \ --MeshSimplification.target_face_ratio 0.25 # Optionally texture a mesh using the undistorted images. $ colmap mesh_texturer \ --workspace_path $DATASET_PATH/dense \ --input_path $DATASET_PATH/dense/meshed-poisson.ply \ --output_path $DATASET_PATH/dense/textured To use the global SfM pipeline instead of the incremental mapper, replace the ``mapper`` step with ``global_mapper``. The global mapper depends on good focal length priors, so if reliable intrinsics are not available (e.g., from EXIF or lab calibration), you should run ``view_graph_calibrator`` first. This step is optional but recommended to improve the quality of global SfM, as was always the default in `GLOMAP `_. Note that ``view_graph_calibrator`` modifies camera intrinsics and two-view geometries in the database in-place, so it is recommended to work on a copy of the database:: $ colmap feature_extractor \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images $ colmap exhaustive_matcher \ --database_path $DATASET_PATH/database.db # Optional but often needed: calibrate intrinsics from the view graph. # This modifies the database in-place, so work on a copy. $ cp $DATASET_PATH/database.db $DATASET_PATH/database_global.db $ colmap view_graph_calibrator \ --database_path $DATASET_PATH/database_global.db $ mkdir -p $DATASET_PATH/sparse $ colmap global_mapper \ --database_path $DATASET_PATH/database_global.db \ --image_path $DATASET_PATH/images \ --output_path $DATASET_PATH/sparse If you want to run COLMAP on a computer without an attached display (e.g., cluster or cloud service), COLMAP automatically switches to use CUDA if supported by your system. If no CUDA enabled device is available, you can manually select to use CPU-based feature extraction and matching by setting the ``--FeatureExtraction.use_gpu 0`` and ``--FeatureMatching.use_gpu 0`` options. Help ---- The available commands can be listed using the command:: $ colmap help Usage: colmap [command] [options] Documentation: https://colmap.github.io/ Example usage: colmap help [ -h, --help ] colmap gui colmap gui -h [ --help ] colmap automatic_reconstructor -h [ --help ] colmap automatic_reconstructor --image_path IMAGES --workspace_path WORKSPACE colmap feature_extractor --image_path IMAGES --database_path DATABASE colmap exhaustive_matcher --database_path DATABASE colmap mapper --image_path IMAGES --database_path DATABASE --output_path MODEL ... Available commands: help gui automatic_reconstructor bundle_adjuster color_extractor database_cleaner database_creator database_merger delaunay_mesher exhaustive_matcher feature_extractor feature_importer geometric_verifier global_mapper guided_geometric_verifier hierarchical_mapper image_deleter image_filterer image_rectifier image_registrator image_undistorter image_undistorter_standalone mapper matches_importer mesh_simplifier mesh_texturer model_aligner model_analyzer model_clusterer model_comparer model_converter model_cropper model_merger model_orientation_aligner model_splitter model_transformer patch_match_stereo point_filtering point_triangulator pose_prior_mapper poisson_mesher project_generator rig_configurator rotation_averager sequential_matcher spatial_matcher stereo_fusion transitive_matcher view_graph_calibrator vocab_tree_builder vocab_tree_matcher vocab_tree_retriever And each command has a ``-h,--help`` command-line argument to show the usage and the available options, e.g.:: $ colmap feature_extractor -h Options can either be specified via command-line or by defining them in a .ini project file passed to ``--project_path``. -h [ --help ] --default_random_seed arg (=0) --log_target arg (=stderr_and_file) {stderr, stdout, file, stderr_and_file} --log_path arg --log_level arg (=0) --log_severity arg (=0) 0:INFO, 1:WARNING, 2:ERROR, 3:FATAL --log_color arg (=1) --project_path arg --database_path arg --image_path arg --camera_mode arg (=-1) --image_list_path arg --descriptor_normalization arg (=l1_root) {'l1_root', 'l2'} --ImageReader.mask_path arg --ImageReader.camera_model arg (=SIMPLE_RADIAL) --ImageReader.single_camera arg (=0) --ImageReader.single_camera_per_folder arg (=0) --ImageReader.single_camera_per_image arg (=0) --ImageReader.existing_camera_id arg (=-1) --ImageReader.camera_params arg --ImageReader.default_focal_length_factor arg (=1.2) --ImageReader.camera_mask_path arg --FeatureExtraction.type arg (=SIFT) --FeatureExtraction.max_image_size arg (=3200) --FeatureExtraction.num_threads arg (=-1) --FeatureExtraction.use_gpu arg (=1) --FeatureExtraction.gpu_index arg (=-1) --SiftExtraction.max_num_features arg (=8192) --SiftExtraction.first_octave arg (=-1) --SiftExtraction.num_octaves arg (=4) --SiftExtraction.octave_resolution arg (=3) --SiftExtraction.peak_threshold arg (=0.0066666666666666671) --SiftExtraction.edge_threshold arg (=10) --SiftExtraction.estimate_affine_shape arg (=0) --SiftExtraction.max_num_orientations arg (=2) --SiftExtraction.upright arg (=0) --SiftExtraction.domain_size_pooling arg (=0) --SiftExtraction.dsp_min_scale arg (=0.16666666666666666) --SiftExtraction.dsp_max_scale arg (=3) --SiftExtraction.dsp_num_scales arg (=10) The available options can either be provided directly from the command-line or through a ``.ini`` file provided to ``--project_path``. Commands -------- The following list briefly documents the functionality of each command, that is available as ``colmap [command]``: - ``gui``: The graphical user interface, see :ref:`Graphical User Interface ` for more information. - ``automatic_reconstructor``: Automatically reconstruct sparse and dense model for a set of input images. Key options include ``--quality`` (LOW, MEDIUM, HIGH, EXTREME), ``--data_type`` (INDIVIDUAL, VIDEO, INTERNET) to tune settings for different capture scenarios, ``--feature`` (SIFT, ALIKED) to select the feature extraction algorithm, ``--mapper`` (INCREMENTAL, HIERARCHICAL, GLOBAL) to choose the SfM pipeline, and ``--mesher`` (POISSON, DELAUNAY) to select the surface reconstruction method. - ``project_generator``: Generate project files at different quality settings. - ``feature_extractor``, ``feature_importer``: Perform feature extraction or import features for a set of images. - ``exhaustive_matcher``, ``vocab_tree_matcher``, ``sequential_matcher``, ``spatial_matcher``, ``transitive_matcher``, ``matches_importer``: Perform feature matching after performing feature extraction. - ``geometric_verifier``: Run standalone geometric verification on existing feature matches in the database. This estimates two-view geometries (fundamental/essential matrices, homographies) for matched image pairs. - ``guided_geometric_verifier``: Run geometric verification guided by an existing sparse reconstruction. Uses the known relative camera poses to improve match verification results. - ``mapper``: Sparse 3D reconstruction / mapping of the dataset using SfM after performing feature extraction and matching. - ``global_mapper``: Sparse 3D reconstruction using the global SfM pipeline. Unlike the incremental ``mapper``, the global approach solves for all camera poses simultaneously using rotation averaging and global positioning. This can be faster for large datasets but may be less robust to outliers. The global mapper depends on reasonably good focal length priors to perform well. Run ``view_graph_calibrator`` before ``global_mapper`` to calibrate camera intrinsics and estimate relative poses from the view graph, or provide camera calibrations manually. - ``pose_prior_mapper``: Sparse 3D reconstruction / mapping using pose priors. - ``hierarchical_mapper``: Sparse 3D reconstruction / mapping of the dataset using hierarchical SfM after performing feature extraction and matching. This parallelizes the reconstruction process by partitioning the scene into overlapping submodels and then reconstructing each submodel independently. Finally, the overlapping submodels are merged into a single reconstruction. It is recommended to run a few rounds of point triangulation and bundle adjustment after this step. - ``image_undistorter``: Undistort images and/or export them for MVS or to external dense reconstruction software, such as CMVS/PMVS. - ``image_rectifier``: Stereo rectify cameras and undistort images for stereo disparity estimation. - ``image_filterer``: Filter images from a sparse reconstruction. - ``image_deleter``: Delete specific images from a sparse reconstruction. - ``patch_match_stereo``: Dense 3D reconstruction / mapping using MVS after running the ``image_undistorter`` to initialize the workspace. - ``stereo_fusion``: Fusion of ``patch_match_stereo`` results into to a colored point cloud. - ``poisson_mesher``: Meshing of the fused point cloud using Poisson surface reconstruction. - ``delaunay_mesher``: Meshing of the reconstructed sparse or dense point cloud using a graph cut on the Delaunay triangulation and visibility voting. - ``mesh_simplifier``: Simplify a triangle mesh (PLY format) using Quadric Error Metric (QEM) decimation. This reduces the number of faces in a mesh while preserving its overall shape and appearance. Key options include ``--MeshSimplification.target_face_ratio`` to control the fraction of faces to retain (default 0.1), ``--MeshSimplification.max_error`` to set a maximum quadric error threshold (0 = disabled), and ``--MeshSimplification.boundary_weight`` to control boundary edge preservation (default 1000). Supports multi-threaded initialization via ``--MeshSimplification.num_threads``. - ``mesh_texturer``: Produce a texture atlas and UV coordinates for a triangle mesh using calibrated multi-view images. - ``image_registrator``: Register new images in the database against an existing model, e.g., when extracting features and matching newly added images in a database after running ``mapper``. Note that no bundle adjustment or triangulation is performed. - ``point_triangulator``: Triangulate all observations of registered images in an existing model using the feature matches in a database. - ``point_filtering``: Filter sparse points in model by enforcing criteria, such as minimum track length, maximum reprojection error, etc. - ``bundle_adjuster``: Run global bundle adjustment on a reconstructed scene, e.g., when a refinement of the intrinsics is needed or after running the ``image_registrator``. - ``database_cleaner``: Clean specific or all database tables. - ``database_creator``: Create an empty COLMAP SQLite database with the necessary database schema information. - ``database_merger``: Merge two databases into a new database. Note that the cameras will not be merged and that the unique camera and image identifiers might change during the merging process. - ``model_analyzer``: Print statistics about reconstructions. - ``model_clusterer``: Split a reconstruction into smaller sub-model clusters. Useful for managing and processing large-scale reconstructions. - ``model_aligner``: Align/geo-register model to coordinate system of given camera centers. - ``model_orientation_aligner``: Align the coordinate axis of a model using a Manhattan world assumption. - ``model_comparer``: Compare statistics of two reconstructions. - ``model_converter``: Convert the COLMAP export format to another format, such as PLY or NVM. - ``model_cropper``: Crop model to specific bounding box described in GPS or model coordinate system. - ``model_merger``: Attempt to merge two disconnected reconstructions, if they have common registered images. - ``model_splitter``: Divide model in rectangular sub-models specified from file containing bounding box coordinates, or max extent of sub-model, or number of subdivisions in each dimension. - ``model_transformer``: Transform coordinate frame of a model. - ``color_extractor``: Extract mean colors for all 3D points of a model. - ``rig_configurator``: Configure rigs and frames after feature extraction. - ``vocab_tree_builder``: Create a vocabulary tree from a database with extracted images. This is an offline procedure and can be run once, while the same vocabulary tree can be reused for other datasets. Note that, as a rule of thumb, you should use at least 10-100 times more features than visual words. Pre-trained trees can be downloaded from https://demuc.de/colmap/. This is useful if you want to build a custom tree with a different trade-off in terms of precision/recall vs. speed. - ``vocab_tree_retriever``: Perform vocabulary tree based image retrieval. - ``rotation_averager``: Run standalone rotation averaging on the view graph. Estimates global camera rotations from pairwise relative rotations. - ``view_graph_calibrator``: Calibrate camera intrinsics using the view graph. Estimates focal lengths and other intrinsic parameters from pairwise geometric relations. Should be run before ``global_mapper``, if no good prior camera intrinsics are known, since the global mapper depends on reasonably good focal length priors to perform well. Visualization ------------- If you want to quickly visualize the outputs of the sparse or dense reconstruction pipelines, COLMAP offers you the following possibilities: - The sparse point cloud obtained with the ``mapper`` can be visualized via the COLMAP GUI by importing the model files: choose ``File > Import Model`` and select the folder containing the sparse model files (``cameras.txt``, ``images.txt``, ``points3D.txt``, etc.). - The dense point cloud obtained with the ``stereo_fusion`` can be visualized via the COLMAP GUI by importing ``fused.ply``: choose ``File > Import Model from...`` and then select the file ``fused.ply``. - The dense mesh model ``meshed-*.ply`` obtained with the ``poisson_mesher`` or the ``delaunay_mesher`` can currently not be visualized with COLMAP, instead you can use an external viewer, such as Meshlab. Use the ``mesh_simplifier`` command to reduce the mesh size for faster visualization or downstream processing. Use the ``mesh_texturer`` command to produce a textured mesh with a texture atlas that can be visualized in Meshlab or other 3D viewers. colmap-4.0.4/doc/colmap.1000066400000000000000000000021021517363634500151040ustar00rootroot00000000000000.TH colmap 1 "January 4 2018" .SH NAME colmap \- Structure-from-Motion and Multi-View Stereo .SH SYNOPSIS .B colmap .RI "[command] [options]" .SH DESCRIPTION This manual page documents briefly the .B colmap command. .PP COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. .SH OPTIONS This program offers a graphical and command-line interface. Each command offers summary of all available options. .TP .B help [ \-h, \-\-help ] Show summary of all options. .TP .B gui Start graphical interface. .TP .B gui \-h [ \-\-help ] Show summary of graphical interface options. .TP .B feature_extractor \-h [ \-\-help ] Show summary of feature extractor options. .TP .B feature_extractor \-\-image_path IMAGES \-\-database_path DATABASE Extract features for images in the given folder and store them in the database. .br .TP .B ... .br .SH ONLINE DOCUMENTATION The program is documented at https://colmap.github.io/ colmap-4.0.4/doc/concepts.rst000077500000000000000000000050421517363634500161300ustar00rootroot00000000000000.. _concepts: Key Concepts ============ Starting from COLMAP 3.12, the concepts of rigs and frames have been introduced to enable a principled modeling of multi-sensor platforms as well as 360° panorama images. These concepts provide a structured framework to organize sensors and their measurements, enabling more flexible calibration and fusion of diverse data types (e.g., see :ref:`rig-support`). These additions are backward-compatible and do not affect the traditional, default usage of COLMAP for single-camera capture setups. .. _sensors: Sensors and Measurements ------------------------ A **sensor** is a device that captures data about the environment, producing measurements at specific timestamps. The most common sensor type is the camera, which captures images as its measurements. Other examples include IMUs (Inertial Measurement Units), which record acceleration and angular velocity, and GNSS receivers, which provide absolute position data. Currently, COLMAP supports only cameras and their image measurements, though the sensor concept is designed to extend to other types such as IMUs and GNSS for future support of multi-modal data fusion. .. _rigs: Rigs ---- A **rig** models a platform composed of multiple sensors with fixed relative poses, enabling synchronized and consistent multi-sensor data collection. Examples include stereo camera setups, headworn AR/VR devices, and autonomous driving sensor suites. It can also be virtual — for example, a rig modeling multiple virtual cameras arranged to capture overlapping views used to create seamless 360° panoramic images. In COLMAP, each sensor must be uniquely associated with exactly one rig. Each rig has a single reference sensor that defines its origin. For example, in a stereo camera rig, one camera is designated as the reference sensor with an identity ``sensor_from_rig`` pose, while the second camera’s pose is defined relative to this reference. In a single-camera setup, the camera itself serves as the sole reference sensor for its rig. .. _frames: Frames ------ A **frame** represents a rig captured at a single timestamp, containing measurements from one or more sensors within that rig. For example, if a rig consists of three sensors, a frame may include measurements from all three sensors, or only a subset, depending on availability. This concept allows association of multi-sensor data at specific points in time. For instance, in a stereo camera rig recording video, each frame corresponds to a set of two images—one from each camera—captured at the same moment. colmap-4.0.4/doc/conf.py000077500000000000000000000255771517363634500150760ustar00rootroot00000000000000# COLMAP documentation build configuration file, created by # sphinx-quickstart on Wed Jan 28 09:31:25 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import re import subprocess from sphinx.ext import autodoc def get_git_revision(): try: commit_id = ( subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) .decode() .strip() ) commit_date = ( subprocess.check_output( ["git", "log", "-1", "--format=%cd", "--date=short"] ) .decode() .strip() ) return f"{commit_id} ({commit_date})" except Exception: return "Unknown" # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.mathjax", "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "COLMAP" copyright = "2025, Johannes L. Schoenberger" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short MAJOR.MINOR.PATCH version. version = "4.0.4" + " | " + get_git_revision() # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_css_files = ["custom.css"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "COLMAPdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'a4paper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "COLMAP.tex", "COLMAP Documentation", "Johannes L. Schoenberger", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. latex_show_pagerefs = True # If true, show URL addresses after external links. latex_show_urls = "footnote" # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. latex_domain_indices = False # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "colmap", "COLMAP Documentation", ["Johannes L. Schoenberger"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "COLMAP", "COLMAP Documentation", "Johannes L. Schoenberger", "COLMAP", "Structure-from-Motion and Multi-View Stereo.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # Configure how Python API docs are displayed. autoclass_content = "both" autodoc_member_order = "bysource" autodoc_typehints = "both" python_maximum_signature_line_length = 120 autodoc_use_legacy_class_based = True def sort_members( self, documenters: list[tuple[autodoc.Documenter, bool]], order: str ) -> list[tuple[autodoc.Documenter, bool]]: """Order the members by their definition order.""" class_names = list(self.object.__dict__) def keyfunc(entry: tuple[autodoc.Documenter, bool]) -> int: name = entry[0].name.split("::")[1].split(".")[1] if name in class_names: return class_names.index(name) else: return len(class_names) documenters.sort(key=keyfunc) return documenters # autodoc_member_order=bysource does not work for C++-defined classes since they # cannot be introspected and do not have an __all__ list. Instead, # we extract the definition order from object.__dict__. autodoc.ClassDocumenter.sort_members = sort_members def process_doc(app, what, name, obj, options, lines): if not lines: return has_overload = lines[0] == "Overloaded function." for i in range(len(lines)): lines[i] = lines[i].replace("pycolmap._core", "pycolmap") if has_overload and re.search(r"^\d+\. ", lines[i]): index, signature = lines[i].split(". ", 1) signature = "``" + signature.replace("->", "→") + "``" lines[i] = ". ".join([index, signature]) def process_sig(app, what, name, obj, options, signature, return_annotation): if signature is None: return None, return_annotation signature = signature.replace("pycolmap._core", "pycolmap") if isinstance(return_annotation, str): return_annotation = return_annotation.replace( "pycolmap._core", "pycolmap" ) return signature, return_annotation def setup(app): # Remap types from the C++ module pycolmap._core to the Python namespace. app.connect("autodoc-process-docstring", process_doc) app.connect("autodoc-process-signature", process_sig) colmap-4.0.4/doc/contribution.rst000066400000000000000000000016731517363634500170340ustar00rootroot00000000000000Contribution ============ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. Please, adhere to the Google coding style guide:: https://google.github.io/styleguide/cppguide.html by using the provided ".clang-format" file. Document functions, methods, classes, etc. with inline documentation strings describing the API, using the following format:: // Short description. // // Longer description with a few sentences and multiple lines. // // @param parameter1 Description for parameter 1. // @param parameter2 Description for parameter 2. // // @return Description of optional return value. Add unit tests for all newly added code and make sure that algorithmic "improvements" generalize and actually improve the results of the pipeline on a variety of datasets. colmap-4.0.4/doc/database.rst000077500000000000000000000130411517363634500160540ustar00rootroot00000000000000.. _database-format: Database Format =============== COLMAP stores all extracted information in a single SQLite database file. The database can be accessed with the database management toolkit in the COLMAP GUI, the provided C++ database API (see ``src/colmap/scene/database.h``), or using Python with pycolmap. The database contains the following tables: - rigs - cameras - frames - images - keypoints - descriptors - matches - two_view_geometries To initialize an empty SQLite database file with the required schema, you can either create a new project in the GUI or run the ``colmap database_creator`` command. Rigs and Sensors ---------------- The relation between rigs and sensors (cameras, etc.) is 1-to-N with one sensor being chosen as the reference sensor to define the origin of the rig. Each sensor must only be part of one rig. Rigs and Frames --------------- The relation between rigs and frames is 1-to-N, where a frame defines a specific instance of the rig with all or a subset of sensors exposed at the same time. Cameras and Images ------------------ The relation between cameras and images is 1-to-N. This has important implications for Structure-from-Motion, since one camera shares the same intrinsic parameters (focal length, principal point, distortion, etc.), while every image has separate extrinsic parameters (orientation and location). The intrinsic parameters of cameras are stored as contiguous binary blobs in ``float64``, ordered as specified in ``src/colmap/sensor/models.h``. COLMAP only uses cameras that are referenced by images, all other cameras are ignored. The ``name`` column in the images table is the unique relative path in the image folder. As such, the database file and image folder can be moved to different locations, as long as the relative folder structure is preserved. When manually inserting images and cameras into the database, make sure that all identifiers are positive and non-zero, i.e. ``image_id > 0`` and ``camera_id > 0``. Keypoints and Descriptors ------------------------- The detected keypoints are stored as row-major ``float32`` binary blobs, where the first two columns are the X and Y locations in the image, respectively. COLMAP uses the convention that the upper left image corner has coordinate ``(0, 0)`` and the center of the upper left most pixel has coordinate ``(0.5, 0.5)``. If the keypoints have 4 columns, then the feature geometry is a similarity and the third column is the scale and the fourth column the orientation of the feature (according to SIFT conventions). If the keypoints have 6 columns, then the feature geometry is an affinity and the last 4 columns encode its affine shape (see ``src/colmap/feature/types.h`` for details). The extracted descriptors are stored as row-major binary blobs, where each row describes the feature appearance of the corresponding entry in the keypoints table. The data type and dimensionality depend on the feature extractor: - **SIFT**: ``uint8`` descriptors with 128 dimensions (128 bytes per feature). - **ALIKED**: ``float32`` descriptors with 128 dimensions (512 bytes per feature). The ``cols`` column in the descriptors table specifies the number of bytes per descriptor row. For ``uint8`` descriptors, this equals the descriptor dimension. For ``float32`` descriptors, this equals ``4 * dimension``. In both tables, the ``rows`` table specifies the number of detected features per image, while ``rows=0`` means that an image has no features. For feature matching and geometric verification, every image must have a corresponding keypoints and descriptors entry. Note that only vocabulary tree matching with fast spatial verification requires meaningful values for the local feature geometry, i.e., only X and Y must be provided and the other keypoint columns can be set to zero. The rest of the reconstruction pipeline only uses the keypoint locations. Matches and two-view geometries ------------------------------- Feature matching stores its output in the ``matches`` table and geometric verification in the ``two_view_geometries`` table. COLMAP only uses the data in ``two_view_geometries`` for reconstruction. Every entry in the two tables stores the feature matches between two unique images, where the ``pair_id`` is the row-major, linear index in the upper-triangular match matrix, generated as follows:: def image_ids_to_pair_id(image_id1, image_id2): if image_id1 > image_id2: return 2147483647 * image_id2 + image_id1 else: return 2147483647 * image_id1 + image_id2 and image identifiers can be uniquely determined from the ``pair_id`` as:: def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 The ``pair_id`` enables efficient database queries, as the matches tables may contain several hundred millions of entries. This scheme limits the maximum number of images in a database to 2147483647 (maximum value of signed 32-bit integers), i.e. ``image_id`` must be smaller than 2147483647. The binary blobs in the matches tables are row-major ``uint32`` matrices, where the left column are zero-based indices into the features of ``image_id1`` and the second column into the features of ``image_id2``. The column ``cols`` must be 2 and the ``rows`` column specifies the number of feature matches. The F, E, H blobs in the ``two_view_geometries`` table are stored as 3x3 matrices in row-major ``float64`` format. The meaning of the ``config`` values are documented in the ``src/colmap/estimators/two_view_geometry.h`` source file. colmap-4.0.4/doc/datasets.rst000066400000000000000000000021461517363634500161210ustar00rootroot00000000000000.. _datasets: Datasets ======== A number of different datasets are available for download at: https://demuc.de/colmap/datasets/ - **Gerrard Hall**: 100 high-resolution images of the "Gerrard" hall at UNC Chapel Hill, which is the building right next to the "South" building. The images are taken with the same camera but different focus using a wide-angle lens. - **Graham Hall**: 1273 high-resolution images of the interior and exterior of "Graham" memorial hall at UNC Chapel Hill. The images are taken with the same camera but different focus using a wide-angle lens. - **Person Hall**: 330 high-resolution images of the "Person" hall at UNC Chapel Hill. The images are taken with the same camera using a wide-angle lens. - **South Building**: 128 images of the "South" building at UNC Chapel Hill. The images are taken with the same camera, kindly provided by Christopher Zach. A number of sample reconstructions produced by COLMAP can be viewed here: **Sparse reconstructions**: - https://youtu.be/PmXqdfBQxfQ - https://youtu.be/DIv1aGKqSIk **Dense reconstructions**: - https://youtu.be/11awtGWSqQU colmap-4.0.4/doc/faq.rst000066400000000000000000001100301517363634500150500ustar00rootroot00000000000000Frequently Asked Questions ========================== Adjusting the options for different reconstruction scenarios and output quality ------------------------------------------------------------------------------- COLMAP provides many options that can be tuned for different reconstruction scenarios and to trade off accuracy and completeness versus efficiency. The default options are set for medium to high quality reconstruction of unstructured input data. There are several presets for different scenarios and quality levels, which can be set in the GUI as ``Extras > Set options for ...``. To use these presets from the command-line, you can save the current set of options as ``File > Save project`` after choosing the presets. The resulting project file can be opened with a text editor to view the different options. Alternatively, you can generate the project file also from the command-line by running ``colmap project_generator``. Extending COLMAP ---------------- If you need to simply analyze the produced sparse or dense reconstructions from COLMAP, you can load the sparse models using pycolmap in Python or the scripts in ``scripts/matlab`` for Matlab. If you want to write a C/C++ executable that builds on top of COLMAP, there are two possible approaches. First, the COLMAP headers and library are installed to the ``CMAKE_INSTALL_PREFIX`` by default. Compiling against COLMAP as a library is described :ref:`here `. Alternatively, you can start from the ``src/colmap/tools/example.cc`` code template and implement the desired functionality directly as a new binary within COLMAP. Choosing between SIFT and ALIKED features ----------------------------------------- COLMAP supports two feature extraction algorithms: SIFT (default) and ALIKED (requires ONNX support). Here are some guidelines for choosing between them: - **SIFT** is the most widely tested and robust choice. It works well for scenarios with moderate to high view overlap, sufficient scene texture, and captured under similar illumination conditiions. It supports both GPU and CPU extraction. - **ALIKED** is a learned feature extractor that can produce more repeatable features in some cases, particularly for scenes with limited view overlap, little scene texture, and drastic illumination changes. It requires ONNX Runtime at build time (``-DONNX_ENABLED=ON``). Both feature types support brute-force matching as well as LightGlue neural network-based matching. LightGlue typically produces higher inlier ratios, especially for image pairs with large viewpoint or illumination changes, but requires ONNX support. See :ref:`Feature Extraction and Matching ` for details on available options. Do not mix different feature types (e.g., SIFT and ALIKED) in the same database, as the descriptors are incompatible. .. _faq-choosing-camera-model: Choosing the right camera model ------------------------------- COLMAP supports many camera models with varying numbers of parameters (see :doc:`cameras` for the full list). Choosing the right model depends on your lens type and reconstruction requirements: - **SIMPLE_RADIAL** (default): A good starting point for most standard cameras. Models a single focal length, principal point, and one radial distortion parameter. - **PINHOLE**: Use if your images have negligible lens distortion (e.g., already undistorted images or high-quality industrial lenses). - **OPENCV**: A good choice for wider-angle lenses with moderate distortion. Models 2 focal lengths, principal point, and 4 distortion parameters (2 radial + 2 tangential). - **SIMPLE_RADIAL_FISHEYE** or **OPENCV_FISHEYE**: Use for fisheye lenses with a field of view significantly larger than 120 degrees. - **FULL_OPENCV**: Use only when you have many images sharing intrinsics and need to model complex distortion patterns. With 12 parameters, this model requires a large number of observations to converge reliably. As a rule of thumb, use the simplest model that adequately describes your lens. Overly complex models with many parameters can lead to degenerate or overfitted calibration, especially when few images share intrinsics. If in doubt, start with ``SIMPLE_RADIAL`` and inspect the reprojection errors in the model statistics. Choosing between incremental, global, and hierarchical SfM ---------------------------------------------------------- COLMAP offers three SfM pipelines: - **Incremental mapper** (``mapper``, default): Reconstructs the scene by incrementally adding one image at a time. This is the most robust and well-tested pipeline, but can become slow for large image collections. - **Global mapper** (``global_mapper``): Solves for all camera poses simultaneously using rotation averaging and global positioning. This can be faster for large datasets with good matching graphs, but may be less robust to outliers in the matching. The global mapper depends on good focal length priors. If reliable intrinsics are not available, run ``view_graph_calibrator`` before ``global_mapper`` to estimate them from the view graph (optional but recommended to improve the quality of global SfM). Note that ``view_graph_calibrator`` modifies the database in-place, so it is recommended to work on a copy. - **Hierarchical mapper** (``hierarchical_mapper``): Partitions the scene into overlapping sub-models and reconstructs each independently, then merges them. This is useful for very large-scale datasets where the incremental approach becomes too slow but is usually less robust than the other two pipelines. All three can also be selected via the ``automatic_reconstructor`` using ``--mapper INCREMENTAL``, ``--mapper GLOBAL``, or ``--mapper HIERARCHICAL``. Reconstruction with pose priors (GPS) ------------------------------------- If your images have GPS information in their EXIF metadata, COLMAP automatically extracts and stores it as pose priors in the database during feature extraction. These priors can then be used during reconstruction with the ``pose_prior_mapper``:: colmap feature_extractor \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images colmap exhaustive_matcher \ --database_path $PROJECT_PATH/database.db colmap pose_prior_mapper \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --output_path $PROJECT_PATH/sparse The ``pose_prior_mapper`` is essentially the incremental mapper with prior position constraints enabled. You can control the prior covariance (uncertainty) using ``--prior_position_std_x``, ``--prior_position_std_y``, and ``--prior_position_std_z`` (default: 1.0 meter each), or override all priors' covariance with ``--overwrite_priors_covariance``. For geo-registration of an already reconstructed model (without using priors during mapping), see the `Geo-registration`_ section. .. _faq-share-intrinsics: Share intrinsics ---------------- COLMAP supports shared intrinsics for arbitrary groups of images and camera models. Images share the same intrinsics, if they refer to the same camera, as specified by the ``camera_id`` property in the database. You can add new cameras and set shared intrinsics in the database management tool. Please, refer to :ref:`Database Management ` for more information. .. _faq-fix-intrinsics: Fix intrinsics -------------- By default, COLMAP tries to refine the intrinsic camera parameters (except principal point) automatically during the reconstruction. Usually, if there are enough images in the dataset and you share the intrinsics between multiple images, the estimated intrinsic camera parameters in SfM should be better than parameters manually obtained with a calibration pattern. However, sometimes COLMAP's self-calibration routine might converge in degenerate parameters, especially in case of the more complex camera models with many distortion parameters. If you know the calibration parameters a priori, you can fix different parameter groups during the reconstruction. Choose ``Reconstruction > Reconstruction options > Bundle Adj. > refine_*`` and check which parameter group to refine or to keep constant. Even if you keep the parameters constant during the reconstruction, you can refine the parameters in a final global bundle adjustment by setting ``Reconstruction > Bundle adj. options > refine_*`` and then running ``Reconstruction > Bundle adjustment``. Principal point refinement -------------------------- By default, COLMAP keeps the principal point constant during the reconstruction, as principal point estimation is an ill-posed problem in general. Once all images are reconstructed, the problem is most often constrained enough that you can try to refine the principal point in global bundle adjustment, especially when sharing intrinsic parameters between multiple images. Please, refer to :ref:`Fix intrinsics ` for more information. Increase number of matches / sparse 3D points --------------------------------------------- To increase the number of matches, you should use the more discriminative DSP-SIFT features instead of plain SIFT and also estimate the affine feature shape using the options: ``--SiftExtraction.estimate_affine_shape=true`` and ``--SiftExtraction.domain_size_pooling=true``. In addition, you should enable guided feature matching using: ``--FeatureMatching.guided_matching=true``. By default, COLMAP ignores two-view feature tracks in triangulation, resulting in fewer 3D points than possible. Triangulation of two-view tracks can in rare cases improve the stability of sparse image collections by providing additional constraints in bundle adjustment. To also triangulate two-view tracks, unselect the option ``Reconstruction > Reconstruction options > Triangulation > ignore_two_view_tracks``. If your images are taken from far distance with respect to the scene, you can try to reduce the minimum triangulation angle. Reconstruct sparse/dense model from known camera poses ------------------------------------------------------ If the camera poses are known and you want to reconstruct a sparse or dense model of the scene, you must first manually construct a sparse model by creating a ``cameras.txt``, ``points3D.txt``, and ``images.txt`` under a new folder:: +── path/to/manually/created/sparse/model │   +── cameras.txt │   +── images.txt │   +── points3D.txt The ``points3D.txt`` file should be empty while every other line in the ``images.txt`` should also be empty, since the sparse features are computed, as described below. You can refer to :ref:`this article ` for more information about the structure of a sparse model. Example of images.txt:: 1 0.695104 0.718385 -0.024566 0.012285 -0.046895 0.005253 -0.199664 1 image0001.png # Make sure every other line is left empty 2 0.696445 0.717090 -0.023185 0.014441 -0.041213 0.001928 -0.134851 2 image0002.png 3 0.697457 0.715925 -0.025383 0.018967 -0.054056 0.008579 -0.378221 1 image0003.png 4 0.698777 0.714625 -0.023996 0.021129 -0.048184 0.004529 -0.313427 2 image0004.png Each image above must have the same ``image_id`` (first column) as in the database (next step). This database can be inspected either in the GUI (under ``Database management > Processing``), or, one can create a reconstruction with colmap and later export it as text in order to see the images.txt file it creates. To reconstruct a sparse map, you first have to recompute features from the images of the known camera poses as follows:: colmap feature_extractor \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images If your known camera intrinsics have large distortion coefficients, you should now manually copy the parameters from your ``cameras.txt`` to the database, such that the matcher can leverage the intrinsics. Modifying the database is possible in many ways, but an easy option is to use pycolmap's database API. Otherwise, you can skip this step and simply continue as follows:: colmap exhaustive_matcher \ # or alternatively any other matcher --database_path $PROJECT_PATH/database.db colmap point_triangulator \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images --input_path path/to/manually/created/sparse/model \ --output_path path/to/triangulated/sparse/model Note that the sparse reconstruction step is not necessary in order to compute a dense model from known camera poses. Assuming you computed a sparse model from the known camera poses, you can compute a dense model as follows:: colmap image_undistorter \ --image_path $PROJECT_PATH/images \ --input_path path/to/triangulated/sparse/model \ --output_path path/to/dense/workspace colmap patch_match_stereo \ --workspace_path path/to/dense/workspace colmap stereo_fusion \ --workspace_path path/to/dense/workspace \ --output_path path/to/dense/workspace/fused.ply Alternatively, you can also produce a dense model without a sparse model as:: colmap image_undistorter \ --image_path $PROJECT_PATH/images \ --input_path path/to/manually/created/sparse/model \ --output_path path/to/dense/workspace Since the sparse point cloud is used to automatically select neighboring images during the dense stereo stage, you have to manually specify the source images, as described :ref:`here `. The dense stereo stage now also requires a manual specification of the depth range. Finally, in this case, fusion will fail to successfully match points if min_num_pixels is left at the default (greater than 1). So also set that parameter, as below:: colmap patch_match_stereo \ --workspace_path path/to/dense/workspace \ --PatchMatchStereo.depth_min $MIN_DEPTH \ --PatchMatchStereo.depth_max $MAX_DEPTH colmap stereo_fusion \ --workspace_path path/to/dense/workspace \ --StereoFusion.min_num_pixels 1 \ --output_path path/to/dense/workspace/fused.ply .. _faq-merge-models: Merge disconnected models ------------------------- Sometimes COLMAP fails to reconstruct all images into the same model and hence produces multiple sub-models. If those sub-models have common registered images, they can be merged into a single model as post-processing step:: colmap model_merger \ --input_path1 /path/to/sub-model1 \ --input_path2 /path/to/sub-model2 \ --output_path /path/to/merged-model To improve the quality of the alignment between the two sub-models, it is recommended to run another global bundle adjustment after the merge:: colmap bundle_adjuster \ --input_path /path/to/merged-model \ --output_path /path/to/refined-merged-model Geo-registration ---------------- Geo-registration of models is possible by providing the 3D locations for the camera centers of a subset or all registered images. The 3D similarity transformation between the reconstructed model and the target coordinate frame of the geo-registration is determined from these correspondences. The geo-registered 3D coordinates can either be extracted from the database (tvec_prior field) or from a user specified text file. For text-files, the geo-registered 3D coordinates of the camera centers for images must be specified with the following format:: image_name1.jpg X1 Y1 Z1 image_name2.jpg X2 Y2 Z2 image_name3.jpg X3 Y3 Z3 ... The coordinates can be either GPS-based (lat/lon/alt) or cartesian-based (x/y/z). In case of GPS coordinates, a conversion will be performed to turn those into cartesian coordinates. The conversion can be done from GPS to ECEF (Earth-Centered-Earth-Fixed) or to ENU (East-North-Up) coordinates. If ENU coordinates are used, the first image GPS coordinates will define the origin of the ENU frame. It is also possible to use ECEF coordinates for alignment and then rotate the aligned reconstruction into the ENU plane. Note that at least 3 images must be specified to estimate a 3D similarity transformation. Then, the model can be geo-registered using:: colmap model_aligner \ --input_path /path/to/model \ --output_path /path/to/geo-registered-model \ --ref_images_path /path/to/text-file (or --database_path /path/to/database.db) \ --ref_is_gps 1 \ --alignment_type ecef \ --alignment_max_error 3.0 (where 3.0 is the error threshold to be used in RANSAC) A 3D similarity transformation will be estimated with a RANSAC estimator to be robust to potential outliers in the data. It is required to provide the error threshold to be used in the RANSAC estimator. Manhattan world alignment ------------------------- COLMAP has functionality to align the coordinate axes of a reconstruction using a Manhattan world assumption, i.e. COLMAP can automatically determine the gravity axis and the major horizontal axis of the Manhattan world through vanishing point detection in the images. Please, refer to the ``model_orientation_aligner`` for more details. Mask image regions ------------------ COLMAP supports masking of keypoints during feature extraction two different ways: 1. Passing ``mask_path`` to a folder with image masks. For a given image, the corresponding mask must have the same sub-path below this root as the image has below ``image_path``. The filename must be equal, aside from the added extension ``.png``. For example, for an image ``image_path/abc/012.jpg``, the mask would be ``mask_path/abc/012.jpg.png``. 2. Passing ``camera_mask_path`` to a single mask image. This single mask is applied to all images. In both cases no features will be extracted in regions, where the mask image is black (pixel intensity value 0 in grayscale). Image orientation and EXIF -------------------------- COLMAP automatically reads the EXIF orientation tag from images during feature extraction. The orientation is converted to a gravity direction vector in sensor coordinates, which is stored as part of the pose prior in the database. This gravity information is used during feature extraction and matching to improve robustness against image rotation. This is crucial for feature extractors/matchers with limited orientation invariance, such as ALIKED and LightGlue. Register/localize new images into an existing reconstruction ------------------------------------------------------------ If you have an existing reconstruction of images and want to register/localize new images within this reconstruction, you can follow these steps:: colmap feature_extractor \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --image_list_path /path/to/image-list.txt colmap vocab_tree_matcher \ --database_path $PROJECT_PATH/database.db \ --VocabTreeMatching.match_list_path /path/to/image-list.txt colmap image_registrator \ --database_path $PROJECT_PATH/database.db \ --input_path /path/to/existing-model \ --output_path /path/to/model-with-new-images colmap bundle_adjuster \ --input_path /path/to/model-with-new-images \ --output_path /path/to/model-with-new-images Note that this first extracts features for the new images, then matches them to the existing images in the database, and finally registers them into the model. The image list text file contains a list of images to extract and match, specified as one image file name per line. The bundle adjustment is optional. If you need a more accurate image registration with triangulation, then you should restart or continue the reconstruction process rather than just registering the images to the model. Instead of running the ``image_registrator``, you should run the ``mapper`` to continue the reconstruction process from the existing model:: colmap mapper \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --input_path /path/to/existing-model \ --output_path /path/to/model-with-new-images Or, alternatively, you can start the reconstruction from scratch:: colmap mapper \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --output_path /path/to/model-with-new-images Note that dense reconstruction must be re-run from scratch after running the ``mapper`` or the ``bundle_adjuster``, as the coordinate frame of the model can change during these steps. Available functionality without GPU/CUDA ---------------------------------------- If you do not have a CUDA-enabled GPU but some other GPU, you can use all COLMAP functionality except the dense reconstruction part. However, you can use external dense reconstruction software as an alternative, as described in the :ref:`Tutorial `. If you have a GPU with low compute power or you want to execute COLMAP on a machine without an attached display and without CUDA support, you can run all steps on the CPU by specifying the appropriate options (e.g., ``--FeatureExtraction.use_gpu=false`` for the feature extraction step). But note that this might result in a significant slow-down of the reconstruction pipeline. Please, also note that feature extraction on the CPU can consume excessive RAM for large images in the default settings, which might require manually reducing the maximum image size using ``--FeatureExtraction.max_image_size`` and/or setting ``--SiftExtraction.first_octave 0`` or by manually limiting the number of threads using ``--FeatureExtraction.num_threads``. Multi-GPU support in feature extraction/matching ------------------------------------------------ You can run feature extraction/matching on multiple GPUs by specifying multiple indices for CUDA-enabled GPUs, e.g., ``--FeatureExtraction.gpu_index=0,1,2,3`` and ``--FeatureMatching.gpu_index=0,1,2,3`` runs the feature extraction/matching on 4 GPUs in parallel. Note that you can only run one thread per GPU and this typically also gives the best performance. By default, COLMAP runs one feature extraction/matching thread per CUDA-enabled GPU and this usually gives the best performance as compared to running multiple threads on the same GPU. Feature matching fails due to illegal memory access --------------------------------------------------- If you encounter the following error message:: MultiplyDescriptor: an illegal memory access was encountered or the following: ERROR: Feature matching failed. This probably caused by insufficient GPU memory. Consider reducing the maximum number of features. during feature matching, your GPU runs out of memory. Try decreasing the option ``--FeatureMatching.max_num_matches`` until the error disappears. Note that this might lead to inferior feature matching results, since the lower-scale input features will be clamped in order to fit them into GPU memory. Alternatively, you could change to CPU-based feature matching, but this can become very slow, or better you buy a GPU with more memory. The maximum required GPU memory can be approximately estimated using the following formula: ``4 * num_matches * num_matches + 4 * num_matches * 256`` for SIFT. For example, if you set ``--FeatureMatching.max_num_matches 10000``, the maximum required GPU memory will be around 400MB, which are only allocated if one of your images actually has that many features. Speedup bundle adjustment ------------------------- The following describes practical ways to reduce bundle adjustment runtime. - **Reduce the problem size** Limit the number of correspondences so that BA solves a smaller problem: - Reduce features by decreasing ``--SiftExtraction.max_image_size`` and/or ``--SiftExtraction.max_num_features``. - Reduce matching pairs (and avoid ``exhaustive_matcher`` when possible) by decreasing ``--SequentialMatching.overlap``, ``--SpatialMatching.max_num_neighbors``, or ``--VocabTreeMatching.num_images``. - Reduce matches by decreasing ``--FeatureMatching.max_num_matches``. - Enable experimental landmark pruning to drop redundant 3D points using ``--Mapper.ba_global_ignore_redundant_points3D 1``. - **Utilize GPU acceleration** Enable GPU-based Ceres solvers for bundle adjustment by setting ``--Mapper.ba_use_gpu 1`` for the ``mapper`` and ``--BundleAdjustmentCeres.use_gpu 1`` for the standalone ``bundle_adjuster``. Several parameters control when and which GPU solver is used: - The GPU solver is activated only when the number of images exceeds ``--BundleAdjustmentCeres.min_num_images_gpu_solver``. - Select between the direct dense, direct sparse, and iterative sparse GPU solvers using ``--BundleAdjustmentCeres.max_num_images_direct_dense_gpu_solver`` and ``--BundleAdjustmentCeres.max_num_images_direct_sparse_gpu_solver`` .. Attention:: COLMAP's official CUDA-enabled binaries are not distributed with ceres[cuda] until Ceres 2.3 is officially released. To use the GPU solvers you must compile Ceres with the CUDA/cuDSS support and link that build to COLMAP. **Note:** Low GPU utilization for the Schur-based sparse solver (cuDSS) can occur when the Schur-complement matrix becomes less sparse (i.e., exhibits more fill-in). Typical causes include: - High image covisibility - Shared camera intrinsics. - **Additional practical tips** - Improve initial conditions by tuning observation-filtering parameters so BA receives more inliers and fewer outliers, or by supplying accurate priors (e.g., intrinsics, poses). - Fix or restrict refinement of parameters when possible (e.g., hold intrinsics fixed if they are known) to reduce the number of optimized variables. - Reduce LM iterations or relax convergence tolerances to trade a small amount of accuracy for runtime: ``--Mapper.ba_global_max_num_iterations``, ``--Mapper.ba_global_function_tolerance``. - Reduce the frequency of expensive global BA passes with mapper options: ``--Mapper.ba_global_frames_freq``, ``--Mapper.ba_global_points_freq``, ``--Mapper.ba_global_frames_ratio`` and ``--Mapper.ba_global_points_ratio``. Trading off completeness and accuracy in dense reconstruction ------------------------------------------------------------- If the dense point cloud contains too many outliers and too much noise, try to increase the value of option ``--StereoFusion.min_num_pixels``. If the reconstructed dense surface mesh model using Poisson reconstruction contains no surface or there are too many outlier surfaces, you should reduce the value of option ``--PoissonMeshing.trim`` to decrease the surface area and vice versa to increase it. Also consider to try the reduce the outliers or increase the completeness in the fusion stage, as described above. If the reconstructed dense surface mesh model using Delaunay reconstruction contains too noisy or incomplete surfaces, you should increase the ``--DelaunayMeshing.quality_regularization`` parameter to obtain a smoother surface. If the resolution of the mesh is too coarse, you should reduce the ``--DelaunayMeshing.max_proj_dist`` option to a lower value. Improving dense reconstruction results for weakly textured surfaces ------------------------------------------------------------------- For scenes with weakly textured surfaces it can help to have a high resolution of the input images (``--PatchMatchStereo.max_image_size``) and a large patch window radius (``--PatchMatchStereo.window_radius``). You may also want to reduce the filtering threshold for the photometric consistency cost (``--PatchMatchStereo.filter_min_ncc``). Surface mesh reconstruction --------------------------- COLMAP supports two types of surface reconstruction algorithms. Poisson surface reconstruction [kazhdan2013]_ and graph-cut based surface extraction from a Delaunay triangulation. Poisson surface reconstruction typically requires an almost outlier-free input point cloud and it often produces bad surfaces in the presence of outliers or large holes in the input data. The Delaunay triangulation based meshing algorithm is more robust to outliers and in general more scalable to large datasets than the Poisson algorithm, but it usually produces less smooth surfaces. Furthermore, the Delaunay based meshing can be applied to sparse and dense reconstruction results. To increase the smoothness of the surface as a post-processing step, you could use Laplacian smoothing, as e.g. implemented in Meshlab. Note that the two algorithms can also be combined by first running the Delaunay meshing to robustly filter outliers from the sparse or dense point cloud and then, in the second step, performing Poisson surface reconstruction to obtain a smooth surface. After meshing, the ``mesh_texturer`` command can be used to produce a textured mesh with a texture atlas [waechter2014]_. This assigns each mesh face to the best-view camera image based on projected area and viewing angle, and bakes the texture into an atlas with per-face UV coordinates. The command requires the undistorted workspace produced by ``image_undistorter`` as input. Speedup dense reconstruction ---------------------------- The dense reconstruction can be speeded up in multiple ways: - Put more GPUs in your system as the dense reconstruction can make use of multiple GPUs during the stereo reconstruction step. Put more RAM into your system and increase the ``--PatchMatchStereo.cache_size``, ``--StereoFusion.cache_size`` to the largest possible value in order to speed up the dense fusion step. - Do not perform geometric dense stereo reconstruction ``--PatchMatchStereo.geom_consistency false``. Make sure to also enable ``--PatchMatchStereo.filter true`` in this case. - Reduce the ``--PatchMatchStereo.max_image_size``, ``--StereoFusion.max_image_size`` values to perform dense reconstruction on a maximum image resolution. - Reduce the number of source images per reference image to be considered, as described :ref:`here `. - Increase the patch windows step ``--PatchMatchStereo.window_step`` to 2. - Reduce the patch window radius ``--PatchMatchStereo.window_radius``. - Reduce the number of patch match iterations ``--PatchMatchStereo.num_iterations``. - Reduce the number of sampled views ``--PatchMatchStereo.num_samples``. - To speedup the dense stereo and fusion step for very large reconstructions, you can use CMVS to partition your scene into multiple clusters and to prune redundant images, as described :ref:`here `. Note that apart from upgrading your hardware, the proposed changes might degrade the quality of the dense reconstruction results. When canceling the stereo reconstruction process and restarting it later, the previous progress is not lost and any already processed views will be skipped. .. _faq-dense-memory: Reduce memory usage during dense reconstruction ----------------------------------------------- If you run out of GPU memory during patch match stereo, you can either reduce the maximum image size by setting the option ``--PatchMatchStereo.max_image_size`` or reduce the number of source images in the ``stereo/patch-match.cfg`` file from e.g. ``__auto__, 30`` to ``__auto__, 10``. Note that enabling the ``geom_consistency`` option increases the required GPU memory. If you run out of CPU memory during stereo or fusion, you can reduce the ``--PatchMatchStereo.cache_size`` or ``--StereoFusion.cache_size`` specified in gigabytes or you can reduce ``--PatchMatchStereo.max_image_size`` or ``--StereoFusion.max_image_size``. Note that a too low value might lead to very slow processing and heavy load on the hard disk. For large-scale reconstructions of several thousands of images, you should consider splitting your sparse reconstruction into more manageable clusters of images using e.g. CMVS [furukawa10]_. In addition, CMVS allows to prune redundant images observing the same scene elements. Note that, for this use case, COLMAP's dense reconstruction pipeline also supports the PMVS/CMVS folder structure when executed from the command-line. Please, refer to the workspace folder for example shell scripts. Note that the example shell scripts for PMVS/CMVS are only generated, if the output type is set to PMVS. Since CMVS produces highly overlapping clusters, it is recommended to increase the default value of 100 images per cluster to as high as possible according to your available system resources and speed requirements. To change the number of images using CMVS, you must modify the shell scripts accordingly. For example, ``cmvs pmvs/ 500`` to limit each cluster to 500 images. If you want to use CMVS to prune redundant images but not to cluster the scene, you can simply set this number to a very large value. .. _faq-dense-manual-source: Manual specification of source images during dense reconstruction ----------------------------------------------------------------- You can change the number of source images in the ``stereo/patch-match.cfg`` file from e.g. ``__auto__, 30`` to ``__auto__, 10``. This selects the images with the most visual overlap automatically as source images. You can also use all other images as source images, by specifying ``__all__``. Alternatively, you can manually specify images with their name, for example:: image1.jpg image2.jpg, image3.jpg image2.jpg image1.jpg, image3.jpg image3.jpg image1.jpg, image2.jpg Here, ``image2.jpg`` and ``image3.jpg`` are used as source images for ``image1.jpg``, etc. Multi-GPU support in dense reconstruction ----------------------------------------- You can run dense reconstruction on multiple GPUs by specifying multiple indices for CUDA-enabled GPUs, e.g., ``--PatchMatchStereo.gpu_index=0,1,2,3`` runs the dense reconstruction on 4 GPUs in parallel. You can also run multiple dense reconstruction threads on the same GPU by specifying the same GPU index twice, e.g., ``--PatchMatchStereo.gpu_index=0,0,1,1,2,3``. By default, COLMAP runs one dense reconstruction thread per CUDA-enabled GPU. .. _faq-dense-timeout: Fix GPU freezes and timeouts during dense reconstruction -------------------------------------------------------- The stereo reconstruction pipeline runs on the GPU using CUDA and puts the GPU under heavy load. You might experience a display freeze or even a program crash during the reconstruction. As a solution to this problem, you could use a secondary GPU in your system, that is not connected to your display by setting the GPU indices explicitly (usually index 0 corresponds to the card that the display is attached to). Alternatively, you can increase the GPU timeouts of your system, as detailed in the following. By default, the Windows operating system detects response problems from the GPU, and recovers to a functional desktop by resetting the card and aborting the stereo reconstruction process. The solution is to increase the so-called "Timeout Detection & Recovery" (TDR) delay to a larger value. Please, refer to the `NVIDIA Nsight documentation `_ or to the `Microsoft documentation `_ on how to increase the delay time under Windows. You can increase the delay using the following Windows Registry entries:: [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers] "TdrLevel"=dword:00000001 "TdrDelay"=dword:00000120 To set the registry entries, execute the following commands using administrator privileges (e.g., in ``cmd.exe`` or ``powershell.exe``):: reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers /v TdrLevel /t REG_DWORD /d 00000001 reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers /v TdrDelay /t REG_DWORD /d 00000120 and restart your machine afterwards to make the changes effective. The X window system under Linux/Unix has a similar feature and detects response problems of the GPU. The easiest solution to avoid timeout problems under the X window system is to shut it down and run the stereo reconstruction from the command-line. Under Ubuntu, you could first stop X using:: sudo service lightdm stop And then run the dense reconstruction code from the command-line:: colmap patch_match_stereo ... Finally, you can restart your desktop environment with the following command:: sudo service lightdm start If the dense reconstruction still crashes after these changes, the reason is probably insufficient GPU memory, as discussed in a separate item in this list. colmap-4.0.4/doc/features.rst000066400000000000000000000077461517363634500161420ustar00rootroot00000000000000.. _features: Feature Extraction and Matching =============================== COLMAP supports multiple feature extraction and matching algorithms. This page describes how to switch between them using the command-line interface or the graphical user interface. Feature Extractor Types ----------------------- The following feature extractor types are available: - ``SIFT``: Scale-Invariant Feature Transform (default). The classic and most widely tested feature extractor. Produces 128-dimensional uint8 descriptors. - ``ALIKED``: A Lighter Keypoint and Descriptor Extractor. A learned feature extractor that produces floating-point descriptors. Requires ONNX support to be enabled at build time (``-DONNX_ENABLED=ON``). To select a feature extractor type via the command-line:: $ colmap feature_extractor \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images \ --FeatureExtraction.type ALIKED_N16ROT \ --AlikedExtraction.max_num_features 2048 For SIFT (the default), you can omit the type or explicitly set it:: $ colmap feature_extractor \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images \ --FeatureExtraction.type SIFT \ --SiftExtraction.max_num_features 8192 In the GUI, open ``Processing > Feature extraction`` and select the desired tab (SIFT, ALIKED, etc.) before clicking Extract. Feature Matcher Types --------------------- The following feature matcher types are available: - ``SIFT_BRUTEFORCE``: Brute-force matching optimized for SIFT descriptors (default). Uses L2 distance with ratio test. - ``ALIKED_BRUTEFORCE``: Brute-force matching for ALIKED descriptors. Uses cosine similarity. Requires ONNX support to be enabled at build time. - ``SIFT_LIGHTGLUE``: Neural network-based matching using the LightGlue model for SIFT descriptors. This typically produces more matches and higher inlier ratios than brute-force matching, especially for challenging image pairs with large viewpoint or illumination changes. Requires ONNX support to be enabled at build time. - ``ALIKED_LIGHTGLUE``: Neural network-based matching using the LightGlue model for ALIKED descriptors. Requires ONNX support to be enabled at build time. To select a feature matcher type via the command-line:: $ colmap exhaustive_matcher \ --database_path $DATASET_PATH/database.db \ --FeatureMatching.type ALIKED_BRUTEFORCE \ --AlikedMatching.min_cossim 0.85 For SIFT matching (the default):: $ colmap exhaustive_matcher \ --database_path $DATASET_PATH/database.db \ --FeatureMatching.type SIFT_BRUTEFORCE \ --SiftMatching.max_ratio 0.8 In the GUI, open ``Processing > Feature matching``, select any matching tab (Exhaustive, Sequential, etc.), and choose the matcher type from the "Type" dropdown in the shared options section. Compatible Extractor and Matcher Types -------------------------------------- The feature extractor and matcher types should be compatible: - Use ``SIFT`` extraction with ``SIFT_BRUTEFORCE`` or ``SIFT_LIGHTGLUE`` matching. - Use ``ALIKED_*`` extraction with ``ALIKED_BRUTEFORCE`` or ``ALIKED_LIGHTGLUE`` matching. Mixing incompatible types (e.g., SIFT features with ALIKED matcher) will result in a runtime error. Do not mix different feature extractor types (e.g., SIFT and ALIKED) in the same database. ALIKED Model Variants --------------------- ALIKED requires an ONNX model file. Several model variants are available with different trade-offs between speed and accuracy: - ``aliked-n16rot``: Faster and trained for some viewpoint invariance. 128-dim descriptors. - ``aliked-n32``: More expensive but not explicitly trained for viewpoint invariance, 128-dim descriptors. Specify the model path using ``--AlikedExtraction.*_model_path``. If the path is a URL, COLMAP will automatically download and cache the model. You can download different ALIKED models from the release page at https://github.com/colmap/colmap/releases/ colmap-4.0.4/doc/format.rst000066400000000000000000000271321517363634500156030ustar00rootroot00000000000000.. _output-format: Output Format ============= ================== Binary File Format ================== Note that all binary data is stored using little endian byte ordering. All x86 processors are little endian and thus no special care has to be taken when reading COLMAP binary data on most platforms. The data can be most conveniently parsed using the C++ reconstruction API under ``src/colmap/scene/reconstruction_io.h`` or using the Python API provided by pycolmap. ======================= Indices and Identifiers ======================= Any variable name ending with ``*_idx`` should be considered as an ordered, contiguous zero-based index. In general, any variable name ending with ``*_id`` should be considered as an unordered, non-contiguous identifier. For example, the unique identifiers of cameras (``CAMERA_ID``), images (``IMAGE_ID``), and 3D points (``POINT3D_ID``) are unordered and are most likely not contiguous. This also means that the maximum ``POINT3D_ID`` does not necessarily correspond to the number 3D points, since some ``POINT3D_ID``'s are missing due to filtering during the reconstruction, etc. ===================== Sparse Reconstruction ===================== By default, COLMAP uses a binary file format (machine-readable, fast) for storing sparse models. In addition, COLMAP provides the option to store the sparse models as text files (human-readable, slow). In both cases, the information is split into multiples files for the information about ``rigs``, ``cameras``, ``frames``, ``images``, and ``points``. Any directory containing these files constitutes a sparse model. The binary files have the file extension ``.bin`` and the text files the file extension ``.txt``. Note that when loading a model from a directory which contains both binary and text files, COLMAP prefers the binary format. Note that older versions of COLMAP had no rig support and thus the ``rigs`` and ``frames`` files may be missing. The reconstruction I/O routines in COLMAP are fully backwards compatible in that models without these files can be read and trivial rigs and frames will be automatically initialized. Furthermore, newer output reconstructions' ``cameras`` and ``images`` files are fully compatible with old outputs. To export the currently selected model in the GUI, choose ``File > Export model``. To export all reconstructed models in the current dataset, choose ``File > Export all``. The selected folder then contains the model files, and for convenience, the current project configuration for importing the model to COLMAP. To import the exported models, e.g., for visualization or to resume the reconstruction, choose ``File > Import model`` and select the folder containing the ``rigs``, ``cameras``, ``frames``, ``images``, and ``points3D`` files. To convert between the binary and text format in the GUI, you can load the model using ``File > Import model`` and then export the model in the desired output format using ``File > Export model`` (binary) or ``File > Export model as text`` (text). In addition, you can export sparse models to other formats, such as VisualSfM's NVM, Bundler files, PLY, VRML, etc., using ``File > Export as...``. To convert between various formats from the CLI, use the ``model_converter`` executable. There are two source files to conveniently read the sparse reconstructions using Python (pycolmap) and Matlab (``scripts/matlab/read_model.m`` supporting text). ----------- Text Format ----------- COLMAP exports the following text files for every reconstructed model: ``rigs.txt``, ``cameras.txt``, ``frames.txt``, ``images.txt``, and ``points3D.txt``. Comments start with a leading "#" character and are ignored. The first comment lines briefly describe the format of the text files, as described in more detailed on this page. rigs.txt ----------- This file contains the configured rigs and sensors, e.g.:: # Rig calib list with one line of data per calib: # RIG_ID, NUM_SENSORS, REF_SENSOR_TYPE, REF_SENSOR_ID, SENSORS[] as (SENSOR_TYPE, SENSOR_ID, HAS_POSE, [QW, QX, QY, QZ, TX, TY, TZ]) # Number of rigs: 1 1 2 CAMERA 1 CAMERA 2 1 -0.9999701516465348 -0.0011120266840749639 -0.0075347911527510894 0.0012985125893421306 -0.19316906391350164 0.00085222218993398979 0.0070758955539026785 2 1 CAMERA 3 Here, the dataset contains two rigs: the first rig has two cameras and the second one has 1 camera. cameras.txt ----------- This file contains the intrinsic parameters of all reconstructed cameras in the dataset using one line per camera, e.g.:: # Camera list with one line of data per camera: # CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] # Number of cameras: 3 1 SIMPLE_PINHOLE 3072 2304 2559.81 1536 1152 2 PINHOLE 3072 2304 2560.56 2560.56 1536 1152 3 SIMPLE_RADIAL 3072 2304 2559.69 1536 1152 -0.0218531 Here, the dataset contains 3 cameras based on different distortion models with the same sensor dimensions (width: 3072, height: 2304). The length of parameters is variable and depends on the camera model. For the first camera, there are 3 parameters with a single focal length of 2559.81 pixels and a principal point at pixel location ``(1536, 1152)``. The intrinsic parameters of a camera can be shared by multiple images, which refer to cameras using the unique identifier ``CAMERA_ID``. frames.txt ---------- This file contains the frames, where a frame defines a specific instance of a rig with all or a subset of sensors exposed at the same time, e.g.:: # Frame list with one line of data per frame: # FRAME_ID, RIG_ID, RIG_FROM_WORLD[QW, QX, QY, QZ, TX, TY, TZ], NUM_DATA_IDS, DATA_IDS[] as (SENSOR_TYPE, SENSOR_ID, DATA_ID) # Number of frames: 151 1 1 0.99801363919752195 0.040985139360073107 0.041890917712361225 -0.023111584553400576 -5.2666546897987896 -0.17120007823690631 0.12300519697527648 2 CAMERA 1 1 CAMERA 2 2 2 2 0.99816472047267968 0.037605501383281774 0.043101511724657163 -0.019881568259519072 -5.1956060695789192 -0.20794508616745555 0.14967533910764824 1 CAMERA 3 3 Here, the dataset contains two frames, where frame 1 is an instance of rig 1 and frame 2 an instance of rig 2. images.txt ---------- This file contains the pose and keypoints of all reconstructed images in the dataset using two lines per image, e.g.:: # Image list with two lines of data per image: # IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME # POINTS2D[] as (X, Y, POINT3D_ID) # Number of images: 2, mean observations per image: 2 1 0.851773 0.0165051 0.503764 -0.142941 -0.737434 1.02973 3.74354 1 P1180141.JPG 2362.39 248.498 58396 1784.7 268.254 59027 1784.7 268.254 -1 2 0.851773 0.0165051 0.503764 -0.142941 -0.737434 1.02973 3.74354 1 P1180142.JPG 1190.83 663.957 23056 1258.77 640.354 59070 Here, the first two lines define the information of the first image, and so on. The reconstructed pose of an image is specified as the projection from world to the camera coordinate system of an image using a quaternion ``(QW, QX, QY, QZ)`` and a translation vector ``(TX, TY, TZ)``. The quaternion is defined using the Hamilton convention, which is, for example, also used by the Eigen library. The coordinates of the projection/camera center are given by ``-R^t * T``, where ``R^t`` is the inverse/transpose of the 3x3 rotation matrix composed from the quaternion and ``T`` is the translation vector. The local camera coordinate system of an image is defined in a way that the X axis points to the right, the Y axis to the bottom, and the Z axis to the front as seen from the image. Both images in the example above use the same camera model and share intrinsics (``CAMERA_ID = 1``). The image name is relative to the selected base image folder of the project. The first image has 3 keypoints and the second image has 2 keypoints, while the location of the keypoints is specified in pixel coordinates. Both images observe 2 3D points and note that the last keypoint of the first image does not observe a 3D point in the reconstruction as the 3D point identifier is -1. points3D.txt ------------ This file contains the information of all reconstructed 3D points in the dataset using one line per point, e.g.:: # 3D point list with one line of data per point: # POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) # Number of points: 3, mean track length: 3.3334 63390 1.67241 0.292931 0.609726 115 121 122 1.33927 16 6542 15 7345 6 6714 14 7227 63376 2.01848 0.108877 -0.0260841 102 209 250 1.73449 16 6519 15 7322 14 7212 8 3991 63371 1.71102 0.28566 0.53475 245 251 249 0.612829 118 4140 117 4473 Here, there are three reconstructed 3D points, where ``POINT2D_IDX`` defines the zero-based index of the keypoint in the ``images.txt`` file. The error is given in pixels of reprojection error and is only updated after global bundle adjustment. ==================== Dense Reconstruction ==================== COLMAP uses the following workspace folder structure:: +── images │   +── image1.jpg │   +── image2.jpg │   +── ... +── sparse │   +── cameras.txt │   +── images.txt │   +── points3D.txt +── stereo │   +── consistency_graphs │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── depth_maps │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── normal_maps │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── patch-match.cfg │   +── fusion.cfg +── fused.ply +── meshed-poisson.ply +── meshed-delaunay.ply +── textured │ +── mesh.ply │ +── texture.png +── run-colmap-geometric.sh +── run-colmap-photometric.sh Here, the ``images`` folder contains the undistorted images, the ``sparse`` folder contains the sparse reconstruction with undistorted cameras, the ``stereo`` folder contains the stereo reconstruction results, ``fused.ply``, ``meshed-poisson.ply``, and ``meshed-delaunay.ply`` are the results of the fusion and meshing procedure, the ``textured`` folder contains the textured mesh (``mesh.ply`` with per-face UV coordinates and ``texture.png`` with the texture atlas) produced by ``mesh_texturer``, and ``run-colmap-geometric.sh`` and ``run-colmap-photometric.sh`` contain example command-line usage to perform the dense reconstruction. --------------------- Depth and Normal Maps --------------------- The depth maps are stored as mixed text and binary files. The text header defines the dimensions of the image in the format ``width&height&channels&`` followed by row-major ``float32`` binary data. For depth maps ``channels=1`` and for normal maps ``channels=3``. The depth and normal maps can be conveniently read with Python using pycolmap and with Matlab using the functions in ``scripts/matlab/read_depth_map.m`` and ``scripts/matlab/read_normal_map.m``. ------------------ Consistency Graphs ------------------ The consistency graph defines, for all pixels in an image, the source images a pixel is consistent with. The graph is stored as a mixed text and binary file, while the text part is equivalent to the depth and normal maps and the binary part is a continuous list of ``int32`` values in the format ``...``. Here, ``(row, col)`` defines the location of the pixel in the image followed by a list of ``N`` image indices. The indices are specified w.r.t. the ordering in the ``images.txt`` file. colmap-4.0.4/doc/gui.rst000066400000000000000000000060131517363634500150720ustar00rootroot00000000000000.. _gui: Graphical User Interface ======================== The graphical user interface of COLMAP provides access to most of the available functionality and visualizes the reconstruction process in "real-time". To start the GUI, you can run the pre-built packages (Windows: ``COLMAP.bat``, Mac: ``COLMAP.app``), execute ``colmap gui`` if you installed COLMAP or execute ``./src/colmap/exe/colmap gui`` from the CMake build folder. The GUI application requires an attached display with at least OpenGL 3.2 support. Registered images are visualized in red and reconstructed points in their average point color extracted from the images. The viewer can also visualize dense point clouds produced from Multi-View Stereo. Model Viewer Controls --------------------- - **Rotate model**: Left-click and drag. - **Shift model**: Right-click or -click (-click) and drag. - **Zoom model**: Scroll. - **Change point size**: -scroll (-scroll). - **Change camera size**: -scroll. - **Adjust clipping plane**: -scroll. - **Select point**: Double-left-click point (change point size if too small). The green lines visualize the projections into the images that see the point. The opening window shows the projected locations of the point in all images. - **Select camera**: Double-left-click camera (change camera size if too small). The purple lines visualize images that see at least one common point with the selected image. The opening window shows a few statistics of the image. - **Reset view**: To reset all viewing settings, choose ``Render > Reset view``. Render Options -------------- The model viewer allows you to render the model with different settings, projections, colormaps, etc. Please, choose ``Render > Render options``. Create Screenshots ------------------ To create screenshots of the current viewpoint (without coordinate axes), choose ``Extras > Grab image`` and save the image in the format of your choice. Create Screencast ----------------- To create a video screen capture of the reconstructed model, choose ``Extras > Grab movie``. This dialog allows you to set individual control viewpoints by choosing ``Add``. COLMAP generates a fixed number of frames per second between each control viewpoint by smoothly interpolating the linear trajectory, and to interpolate the configured point and the camera sizes at the time of clicking ``Add``. To change the number of frames between two viewpoints or to reorder individual viewpoints, modify the time of the viewpoint by double-clicking the respective cell in the table. Note that the video capture requires to set the perspective projection model in the render options. You can review the trajectory in the viewer, which is rendered in light blue. Choose ``Assemble movie``, if you are done creating the trajectory. The output directory then contains the individual frames of the video capture, which can be assembled to a movie using `FFMPEG `_ with the following command:: ffmpeg -i frame%06d.png -r 30 -vf scale=1680:1050 movie.mp4 colmap-4.0.4/doc/images/000077500000000000000000000000001517363634500150215ustar00rootroot00000000000000colmap-4.0.4/doc/images/dense.png000066400000000000000000034071551517363634500166440ustar00rootroot00000000000000PNG  IHDR#jgAMA a cHRMz&u0`:pQ< pHYs""*YiTXtXML:com.adobe.xmp 1 L'Y@IDATxWu7wP@PPDQjShG/zxx?Ŀ}vնF7Ւ%Cxr9'?ɖ"KUB9;| Z\|@@ \ rhIhD@ @@ @@ eE`>["ˋsѪe'ߵ+HmmcVﵬ<cآƻkhkW+wʒ;xRӮ%b#i7oȲٜE UY̌6gs&i"}hdsZ,Ocx>(nbӹј%i΋plƓ?΍s̊u]΍Yl8hcKsK&hb]ڽdt.-HY۷rbW-OXg0Ngӱe Br mY/ZqKS֘.it:J1o ιwhycGm4Y2pl#pXzƴo 9rf6m>y}3ʌG! ҳD2iiʎ،r)g22 6aVkϐ: 4Ĺ)+ 69}Glk1+ smll X?sۮEvֆ,dlڣ{-OΎ]\٠7Gڪl4؈6E_ԯ\>Msk5Z<>٩ Cmsyjժ5ջw`е/13+S|^bNN,qҬcO*S ;1e&|&ݵ3|b0Z4f7Fq|2漈S>;sdjz}'3vU+.s@X5A4ƌ֑&F>J9|ډXٲno`J6؈s}>1,k;1ssO쯾c;<9L:ae;ni2,_(xZRL3d#sZK^:kl8,<㜛X$7fpgN4?ދӎ(mךԏy\iXjIouZm2Sk"ΚOShԣ@EFsY$i<\gk.@w¯@@ @@ )" ;M_x@@ |"t[%E6ӵٮZpr@)R[ׂC^VK;+Hr 15dmnHk׃T-V!P5*63 =YR4 A q_"WQ!x,IQ(,e2lC>C2$FG"\?"7EF!'>j,jh|HY3!ҙ=rCʗWv_1uX']^eO 9Ne9?ҟYQDDJ=z qGm ,օj\i)qS"Ez'!rA0&cr6:gJ;z.d1 o\VCC{;[bH%Wq9;ER{-CA HEfJKQľ_C12+vẕ'`ץ QJ{ꖭol{mnv8?0:nF 15}<4jˎ%W 9QùC>D#"\!x"sRs4{?sD{8s^։YE  ÈB*U tՎR1!Рp NSIfk8Lsʔ?I)4s)qg2Omk{H߽aZٺ(xY.!48pn.sg?vymKm2xj9r|P3/@"HqкP{SC9H) )D4zKf؄r}cɔN52$&"g\ cC"g՟`G47y1"U@@  nr`Gs1U;?9BJ]NaA=8LD3G)NCD&yL[Tn`a$ռƒpP(nc0frJNrޘz4RRy8F|kmqYaڕB@8{៷P|y;:FlU=Dߒs rj'=@"iPP+džYQ\;Kk}*`15]xo[UiY[D` m':26/-@DuQ'ŵSǩ 'zC{ - 9bͳY祮 Kz!սBA)D2?Q9ppPDpwޗ39yo#7Y8݇! @@ <d額caGGWg;"qE>BHˆQ7;dl֪3ځlBAIaS8kRy",!jbqv յEaaPt_ GꇜҩmVRhu:8iy9> }Z`EȽ}tu>HpU_B2 Գdkqo>.TDlr~+4"d#q`eDI8C9o7-u==+yBM;OB8\rU4uJ]HV{ۀx߾rŎAC>W57f39Ka5\LtxWpL4Wi<\eƆe˱DĬb"#w{Ǝ@ʡC 9/U{["D|y<ؤsfΉ9N"ITϵsg͌*nAw4z["UBG!nͷ>7= 0DO$&SN3xznKY׸YT_ҁKuPTi͛#R!4'ܣi(uz9szD+҄ھX(Uo9ׁ@whï@@ @@ &[< 0d$&+ѫazOH}29!;/Z/\u_|^3ǻ2,Wz@@ <Zʴ{PZ:ۍв6DY |"{CHkϞ Lvp樐(v~Qظ!&rHVr% 2Wnn%*yﲛږBK"Dl)~K{rC*SgP`+nGe/Eߕ$]• =kVO}:Q*ϙ*w(Ҁ)&-" h^#֍By"=abJdaVI`F4?> G֡-b=Mn T|M+ i޻:gGu/e"Ck{헌ٟMr\PQ4v L; `c D`l`sZ ȁFu 3}#ksUZJ)E п0_a@@  9CNBsqKWT u]Φ_%d!HUCIPBDoC%+Qn˂x)C h=CU+*8')x!'z &G{FN:vε []28^tYN4mKh.+z&2?|'S 'Ehbmx9T :> "%A{rFмcs fyڬk]69% ~D+A&7R@@ @@ [" Ń,271Be ٌ>>'DF]w;g*A. u~aeO6z`:\2z]5PĹ4GB6w|Ӎ ,d)liމh3 y^~@@yC`/J*2 2\SD!eu!=:9xl0.<l r< #wV?"~ RŹ]($ud(zB]%96ZVxw){gF#B&0Ӝ)9NygRyW 9S}"ef> Zw"MH$חs$m[1\v|zFFαy? }̊?Cu_=> >9F4k}3?Fu EA{g~g/w'㓎g}oSosgI(!E^i?R[>I{ͬzi=TۉV_X2rd៤:>1u8iH>g\tz5-x2mEۅZuacՕ G! c"x(9 |}pW9{E<4]Ը)2*tUDh19©ˎL$2zK>}H N!TEV4F%-D:8y Qxꓵ5Ǯ@Fh)"C弇 ERYeꑿbyIى@? fldb]Wq~&Q%/MS Un0 ՝q&tU*+PMC"O."TJa HAu ܁ Tv!QCD B/Q*RFsp cd-H2iV@#TcoصW^G6u^cݳMB5/RtTRܕ\a8LM~VRqU-m^4Ug2x0,+~Ϛ PÇ)-_65/NW 58fn/,Qqzf He}Wogp+UˮWd8 X(謕]fWf>XWQpep(2w6WvFtd9̨.ؿʮ@޻g†"ذQs}mK ݽqo8e!!rt#;e+YF&_z08u(9DYK?z͝kW_@:3=5e]+c0p˄@1U `Y!Ç8#0^1 CNoIamh|!GU: zB&c]]{aWvQoz2N1E7..)K/\ju}d}k4Wɯ` 5>YNWluz5h 'v@.~RDt9 M &w@ x.\?hF@@ VX XZ cm?챥crg35꧘\>Wi[y۶BNTPlzE 'x>[.-e)B} =u*6%!!R%nCPeOsU'47 _*UwH ݝ[Aؠvq0]B$y֎p_*Cg~xbޠDI rg}|x A*U-οPWC,*aU*%m%ԦC@'j^Vuť>^JP‰wQ_k$s"-/T-@Ŭ|r(\)mw꼞toBC=KD&Q 5j<@yYr$'! *(tzpun>uyO gK#e:\9q) +sʮnLPnYRNޯA& c)B0 ;9= *Z+붺7XkpquOVskKeRyt.e0xH5/Qf<"D6FV:C+W ˽JꋣsKr>ҷַ+҇C08`Kv+ vpx*[^]C$J*(_ho;,[:g!ʅY{9 C^`@|{BܿcUwC+xa- J8sŗ_~{ J\ Ϟ[;ށ$׶{e6w_@|n]rˑDys镻7ligux燶3}slejkV?ﶱ{5-|ǧUJp4^4DG`S2% azzƲ) C{]kB@my61P2*xawΎ? pywiej@@ ߀nKU H}BèCqAWLjl v' 23BH7ȥ$PCI2^ƤHh飇=ǔ uF.A> ^:kX*[f0Ӿ!XhXC^5G &IC*|]!@@ |q`V=l.F8ŊVex T95s|  '[RRcYpVaUN'b\ ,̩P^uۨP!&!*vնc+:tqpfϦ1[ڶ ۽qRaڢ~R*ooRv:!7~EI 8Dw Bkpqʩ]ݽmCbCO[R#2߼IN6|ᕫ,Y{NHv>F8a3Ex(g'mˢ\nB[}dDŽ>'NwZoޣjbϏ0Ur0*9̓s!1;Ng2 `6IcT>v@AҔ!M УM+:}0Yfxl[WTz5c(3)r==?<<{{5 aAg˶ i6qY9qtȪ8gܽm_![>GzpCBCܦ F7LJ'v}iƣ{kk[6d1(ԑ]h} Ƕcز8* /qb>*gZ0=:dpuQbZ pxFԴ!!ND< UTjq?ʾI(1!s9'z9[ #)ͭ1,dn",R)9LPpHANG8o@w{msg[͍! l{NF`oS+_?yC{w?It|[C,lWZjm0ޝx'Ƶ.c >,'Q>" )u'&\h{7yGȐS룾3&:F//\C@ @@ "6{*DcC6Fڈhb.-O$F6vksbՙY\-z(K Z$(v"l)\4FػT 'd{wzA^U>%dH6N ]*W((n4/qɎE?= <@@ |xz/$D){R> ?M?7=QAOO|j|(J;zsoUnY#6dkq=Ӈ}SH縭FAAzi rEHOZǐtjDx*rw!yIz${kC-olc3{ǯvB'rCB% $@u 0̫yBG6^aM6Wʋ-8[sNRTO:kʉ@fK SD@ +`_60"}ɧ#{6xUژ#ʡ-[q0Ah* uQ-B`=6CDvNJPJw1N]ZF~qQS"$@w D55^߃o]r?E-$Jc]1蓬>Z1o `xɀCިwQ_=d}2@5&w 'yc $ĶhsɽX?`_*; rӓЎŰPo3PߗQ:*gGZr8Q'R1׸&V߼ϯRW@ <Ј@@ gSٍEWv,I&%c 𤝝}n*`?>'ƔO˖nR3Ξ?/ !(匏gq-l燇V-GZVޮ\'\jxv" v߲ pM%Cϲ'?R/j @@ y#9ST1Dģ#kۮJu] Cxl%K){!Q{CV**D,"ĜIj"OuyLKϾDL JpK!8_BR@ETBo':ix2ϙBo_q V'ܳJļ6=tx<5uhpJek_Bi;#Uk0I {Jh \TR@{;T:nCxL ҽcs6vw֭ÚJgхo"b@Da\1a! (_'|f<~ҴrEX[_;#W֦(icht/G=]+C2FzKW1减i!c?_&Z #BzX=VZŹ8()%>j;|0KӶ%H(5:>'3r<)8|/ޖF~6]|<޷S d 9@.远Q(ufuBk`Ii"y@:(M{Ox>6"V8vU۾qC0(6!#:l!p'*q迲˸@U؋7(A1Z͜)IΟ%sFM7# S><@@ )DܽȦ}htB=@=pcפ1MT7Zs~Hq Eu"_ NiH 9ĥULGZFh]mx@r.5wD3$Y ]9u~Em22Q\G (U 蟋LCM ?-0 !ܞ%ɇE_FU|hnٛ7-Ʊ?>':ژcB>#om~I{oQG[!=rSkǧ'r1:$NKЖ(S;Fz(RUŔ'[ğ"omAS)!SSW*cx(*u a )iDFАZۜ z'*Wx.ss:90Ard'Ve_B|rUu4Czė ݫ(֖!QSrDT]D[krzWE1@gʱ]C6 O/i wWi{QrZAy􅥚;Dq1 (1{"eՎ(X.1`Sx%~>G'95#3!8cDX["y&Gܘ2DS $=QZ Ey?KSPcr-(?zs01@s"Qxx=+9ϼp;Lוm)j\SQDBCj'!5+_6s1"2ﰎ66^@ A瘏Z* =DO`1|s"<] Ƙ%q"ȧκ@P_@@@`|%" ~v#+ JX!/V!,,Z»kNDT/e꼶nbP`>W3yG#^RhgD/wѻO#pa7˾Y7ȆV:WB!sC"nqȆ}G"܏}XO@@ Y!wDb xh#|RyZ"DLy"dCO""r'_ 5IgJ| QÁ5qJM BKUne% V|Qr=qIu+WHB/rxg*W($$%Դ@IDAT ڜsF̐CN$9Ex+0bFRX)CxmE'E$fΕm'8j/QiC7^T}|BFαSDJqcǔ`B=k阇Weϋ i>B&U_!s:)G9N'ESE-Z㨐 s>k$ѿ9Qm,JDcUIp3g 2u(/PAwf¯@@ | /MݐG@ 2ƱܰHw1" oل#̡޶5Gs6 x}bc]ޗGzflLĹޒ}g?^:_|rc)W>c?oAgbH+zʉ>g%ݶ{?[B!ذ9-Ez({8C66pO>Hp~d:uF7@@ L_LɠTn r.2ef-aqԆʅ: =cs_g@@!{`G=JԮ"~E)td-Ya'D` EZ@ D`JUIzSsYuL0"e^tsD]BeK-~0VmSZRR*NJ(LA)qu@d|\[C> HeE!ה#Y}+TrzKE?iGO*gǵ"S")IPt8?2zj|&Ji2w2Tf}|J;"=9H,v VAr1YI}oǂRs*GXG_?kT=0ٽ#7j(ZbI:k;װPW=c{<9sŒePԧŗ~[zջ8Pf^^"D:f#:sQa D\iV\؝;wl͝5B_$-fs/oڃ{vyZmU3V$}#m0#ڄ I穞P}>]z-9Lp2֘Ρ#,sT^Y" |k]{x_VAsO6t݄%X+S{DQ? SH*C>K>I@x]s8Y(Ig k~ԇ?g^Wp _ty@@ dMbKPCPDșaC&ZŠ~4)H_/W($ڜ=y':s VV1(X 6S=T率&zM<9Rp6oT tխ(L6#ڬRɋ2yCsY쳉VE7)f JБ4B@@#`m ?kP"=@GECwv՛7w!Ͼƀn>=sER`V$` C =Fy 1>_ݺw亹錔qXϮNU OvL2\qRJ9.UH`DE">OnȮBUD3#PDcEq&[ickJ. 3!S-հSV$l&ẗȳI -k1:=\eLdݲCrӷ"+E[&N(QJ빯i(BVzCNRjP:G6̀:P}֌:̯DCT"imggjKK(y۶QJSl ?|w8llۍk o/<]=}kllKV^~/}=yIt#oP1PK*r9oȆN75@s)إo3)A5L׊%ezCS܆ӚT@e-z\wx[eqA~:?|g9Ux{8UrkQ! |6!gPc` KsDng'!['9u26loѓ>W1=V+le9,+vm] "/B}xLn/ݾ Q_?;!B# @@ >7e:i./rl`ʺbG]6"lÛ͝~:q‡MU9/㟷1*Dlsʗɫ_ 9EvOD{b7lLA##V"i>Cv+:2)TEԍd *mhGnsX~žicX2UgDT|DT:s7S~J/"?N"<Jf$,HV9"ujl*]++lE/M[DKO1nڼSFϹeSa>@ ҫwŻ4=Kr.e߷wՊm. ^{K:8A֛Q (}7HiF4Hneliދ@~/< ˘sMY+S/7JTjCٷ^w^E߳{NZɖ/S{[+ps³=@^P8 ϯ44{i2Q斝-lZ7^kns7"̿vfr*%19Z3ZZ"['L=vctڕC'$5/nay b"h̘}^uLp('PhwUjc6)OP2y(C14(30rT(S=#"H1f^A6B;8|BY6L'_V`p?آR'ZB9%/jF{=n4{D@@ XWB[XIrx4O'y߉HG@ )(>4*T+vQu߇(ױBz=:&7pE++6+F #*}֫ǺclĪ~ݰ"5sߝ kCd_}? Y*ҸIxt]VaU/=4:8yycVsC&4;<=l*h[}ܤW}CcpLc3Oo~ž؝׈fF{,g-K̚۰/!!2!D6"22x}'1E!nXaഹ$8L6?'2bCƶvУb8vK^&&."87U'NM$MqMD@21N\!lߢDfPsKV!1d2f-ާ="\O )T 8?"3ժf/\# Oݷ^1d%!3,UFVGmthMtW|Ws ר׭bCq=Lr2U_ SPȗ{h\zZC+U>"˕D"5e.[F"ȤX"8ݗ Dr.HQb"m]>0ꐜDԍ E>10Ԧ kg!R(g+D!.Ԧr^WVn!͛lv.MO3uڸZWzNF"eXDJe ^oH Vy{D։ V[tn7穖F8 Pm1j3yq!鿜Er k9N6"w)˯P>dɩ$mdBGm/ޡ+77$c'V`})[owUW8W].R{6I4<t\j+х7}Jbϊ4!/`))nBkHg`ܖ,@Uﶡ#υgz}]?u{|s[Y.NOtJ7wn76}}[1Cv^ڠo/:ˡQk>,xT5wcQоWt ).*kBe0cyucZơ8<!y-SAnr3gs(K| \9mFB*v0y0@ad1:rCUT6g2y bm kڤ`sy}Pa0g|Aj mnȠUy.{2f6L02Z|!1tgwˠ_x(lhS_֚pr o'ѩ_דz'<@@ $+FH{{:ZG{UO=DT<Ц1[l~⡝O=vuŎ [WnbI8\# (n"?n"s+=1N¬=x(*FݸC@Ql(\5F QRCm!ؕ"W֝Q}!"{1y*}}OzZʺWOr^rKĢއӷ(m}vA#-\NZsm(Ɖ F9]a-;te1 j0c1לJe* W 7蝢i~cT~:wMLH꧷p\[$&ssWv]=;.xCCivPٹbU EZqX*sV\Z*xTGV} PpvqavWnж~dbͪ+CH3f/rVl6UT7gM<ٝ<ݲX-\ yeOp]`.5t>]w4&)/ڟ񿰛d5YKWw[ok[wnaϭūCXY!=s\v{Vm2~8s+zEb, )>M9W ɈݚiZrg!ZoR_r8\]4/|M{ih_Hi8BHv=a柊\ӼTrHuGNs)ԵW E}+ (qdGեzBXc|UrQza# 'œy0`?`6Iߑ眅ʀ)U>ވ(/^:;\iʧqA+$ĉa)WO[ ǀ2#fѧCSSeДe C+g?h{c7wqW5nΒso (WN9G0Y p|dP]\p @}OoR^[g ڹ '{w-@nr  TRz d@駵._*+L9 <=euͣ؃3~s'Y{ @8vi1,fJr[n$b- 1!CnLm\-HV(- 3Xl*1.=+&VM-_6ǐ%\I!&',%xL0UMd9\[IVFf5ՅLu db +tph b//sJrQd۴WfӋEg^C,_@,qH~I]1]i j*_@ּ(muj2IP6R|aa"%/Ϧ~?D:gtVm@J.-}qx@y-,,23Z||g糿*I',=Q&.),Z>O } Q Ȝ^y!G~IzƕtVWϕ7>~6?fͭW!'X> PmsN80׭2ʵ[[q @1=b^m䴎M#HǣZϼ<9G,0L(K(8߉-g {|B[ǝ?es̜P.#桻iC'wCNlL8mH9x`hsThme^X|or'i3y0m;G<%s|*}z%›5iA4)d9A7EoUYX/S 3A6ީ!Fd[ X{Pkܗ7'̶4[!mK|dЯxYf˲,:J %:>Q! ڬ~] '?ź] {$6MX޿f7Qaa PZK{+3 /)b;-!nJkE朱oknI29$-s>Fڌ1 t2v4x%q1qLrEl9N0tCR]k$SofFk'fsZe$1qLEe'8_.&_[ @nr |,%d~F k X.]GqPQYhPhg=T+&ʷb#ְ*;>gkRlXy YqO\KBF +밎u%͠X7+,!Z|S:ZZ,f`% r% l@>qAAo\+Yv4U#33zu>Z5>9$A=66/ΦNLw܍URE gm @ w u5ItaRf{8->yTTZ ;m% e p<`V1Q&л 6t:E$ŇP(gx޾w/^}d{Mg'PwV:9\s[N͸W i 9m(6AO"AfD{`{O|+$}h1un 6 ›Y5A45(G6Z Wcm0<&FDQW>EclC  =BiN|x7 P10!n$Lϒ} ~8d^nyߣ⃶1Dr $9lb <&  z{Iz*1 Ac'7CN^EK1:&lB~^" <3y7[h|Տ;yA 8}?$%}OkS^+-7JhJ5Gh >oYFh'knZ*uDP NM->}iB^`_QvuQԒmSLN Ema#w&F3+\tɞV*KƹR=OAee(yym3v?mc rFKE3@7Hn ,'-q/q}8spVU:b:Ȱ)x"XOOfX am@P.wZɹpy dcj: }8Ԃcv$>:؅FT8B0t/pg˽瓒wbe~/rkn/r ~gS_jkt0>>ڢߝtDvgӵ "Y\!{՟ulijvsNl7s:̷_:m~pH`P\bڵlUrL:]La6$xD\%˼B&ʛd+2gۄ65#>Yϟ@Bz*Fh9:_%Qe YΨl I5!O b i K`Ų|hWPjoo/?8kt9UjWċ7T=>!ͬVxIR?A~B։Ȅ*{~qb^T7 'j X[?Bؕ=9gds<(22n>tbs--?S2`ޏoD ɹϸ \#]OOBZɡ 1Gɠ0~?! ƚvS57XAR] \јۂ7o |1U/,kĥ).b3];}B&HKm`8zx׶#{m>xkd MLϦ52j8޾}?}5{A iT?:<<8'*ܾ}/p%׆1?9l2!vHdv-|Oi/>^9˟;pXU;g̲^Jจ9&F7`b<2^$!/i+K%*TDA "0v܋6mKU.qGUQ4QmJoEb@{?3CySCC%;%Zf:ۈb{ h,s&9RoU/PyS(c[w_{8#mi~Y-v f4xm56b$t+o$H0S,xkH_"h].&;j3svX]3qTh ~<|YBԬcm3, y"<6ȏ-EYe}f.ͧ|xaꙂh"lUkږbDV%1k&D##b:"j5 WߝlzU׀bYsCilsK1xOX$FtmA2g؋1>7n~F&t[@^Й-h>4G SI&hOhsv "qJ7=TZ,7LV d%Ova |9Je J'-%ӃBzϝ3)@]B6PC=4al5Xz9X#{(pYgnհϓݴ~ }/ȧ6^kvvvhKki3 ksӶ*OP{N;?-@n//Ikf-[ @n+PK=Yd 1 $o%\dJe:6md d}OGM"w Nܽ.>9-8y$%CWFu~F!xdҮ*u.8z}׌[(<[Y1I&dNg1e"$u_ThRG 4|^ pgt~d@6i [\9ե^ wlTx_lAd&m&{ {\(!m*8SV i+gY PHG^NLrnlc|{,t`?P6ݝ}\/!_'wy:5wQ3a:)Ldє1J8+2aMRϵ]N؂<or -[;i QyՅٹt:Q:V } SgCumdx)w9or <lƨa-'W{ےvb&G5X #l'DKSJ8;s<>Sn1ׄc.qoh,؟>gm N=hXM0JE"~9F >3%b 15U:%S9u=~u0 .Hh eͼm4رn><[pzo쳒}`+3`MIbqs$gg|>#ll{^_y g"Ûsg*xWfx&J|x;23]HNkkirn&Uwn6U 1h`s{;>x9ݴ09* b doaCZe1xoPf+g$x}m(Qd/(@,Wz*:0sxV*2YMҹ|:josedt$q?oPz>t;H{{$2P:lmJ Ot^%;$"br[ep'*{B]υei?̸.hA|IpvY{ 9Mnެ/b{zyx=ϱ/_5}:(H[D^%>o{d/#IAIv2̛dL 8o!tPRSzWz|;d2 mSv7C<FKۦ5̓ZSMS8,VVQcSX%H $6+Dm{gMjHA# ponV DyO{)-1q+n2x5d͹Yb kvtamجdߗ6[~IXwwKNwXk(ؠr>?է 6V`YŬ>35/70fW.0؋{m8 fyؐHzJ{fj#۔R5 {ݶx)t; >]z۳`14y?גp 5U@8B^t&'Vd6K;̱f\<࠰mʉnڤ |6^u?.'D3grv8q_d1˖\ZnYq<ܲ6 ]C: M>lNNկ~LdR'OCgۻ~q>mpW>x=a9<[cog1 9:?\}f yhM%@*ǜS_rrr߽{77G}={P܎Ą>j̡shm@=C`"CU`p{hc6{}k6vRf_ hD{vR %s\^LoryJ;q'W1"gtwQ#ԁ09c45``|lk}&_ĞoV$/O sioIe{,,v6yvτcK̏%Bay̔h66蹏ľYA~ goAf,iq9N\KL0 68pژ%>&j;>58g|BIgN:`Vӭqð ,HO-QLJ^"Nv8<$fs2}4Px xr1̰?!ArUB,hk_L3Π+; W9*>HtsV];9$#H_♝?nʜ۶kCkc7wo st|nZ&}+pf ^Lk`>o9 {nl?l3ҹ622%dxQm0l7k,r\s޿Q;Gfq+#Q6#E_K zQ%">̼*1m0;)4CO·%v*g.N cꁶ7 N&{;x_yq޾8#KyLZ!1(9|8;4w-[ Kb[ @nu-f0 V0I눘O+6 Jc Lok fB:^p4f6+~ u3W։bZoisDYnQ6tґA3vA YLGQt 7V"dj?.L!#ff.Rt ¼NtQs9j^BrAJ >J&Vtc*)Ax~n:=z8]Z\H/edG M?G:~rzr)HK >؏~_t 4nܸfd__^#~>n]#`Dt%uE}"k8._YL[SZP_i48x5FluZHGvp]~Etl\ Ņ)@V*? ái`6HwȂW%3]&/sMQJ{ G $vb6K\l6 N~bv ḱCO-Tq)! ffN<(wLUfoq/-[ @nWU:tڵ4*k{nGZkC ?UH7:zk23,]Tx|7K-u,qs|iC_$%sKӋq#J1D3dA-krR'[*b)}m`4Te% kN6VB/碄us&Mmoᮇ]ƃ!!%ZFLۓc'Q* ~SX|4%C-"|hlnAҖt ?OHC.=t=dJBb /9 Y Hg{cO ־ 868WSV7AQ. 4H`lbWۃ49dS*DI@~0TW|?m͝׮>o"8qdj#;~af&'| 9y b: /Iˇ0!o8[s Ɀ ^:nYD;9)IVB!='CCr] ^im^ -6UK8?Ivݥ2w m}U8I0V*ɑtKȀ>]_qKHݯs 1xg>Ƹ/j}>{2 JFw CKto5Ri'8"eې8L#< 9 gmU0It_ϸY lT8m.!^oBY" SxLg"hΛ+Q 2 6:_)t% ͨuDH*6Q\<ڭh:p9ܻ2ʮI+.>5Wo9 \R?c@@H Qʦ׫izz"Wá3` ܠ)v:$afۆ!/9gK6ցJ'itl$"N?=)-B߹G=v#Vħd_H u\"Pν;ick12!?hp-^'x3~]ix`(=y4FmGG8ƬZc!xftt^3bkk3.-Ef[8zq$㥳lhhX\\}MpصNح c2^{b<#g$QA7QdNh90܏ّr -[%@I5i|_.?I?~7]X(׿H0˕HМ}?d=͛- V"C:0\@r|u =6Yqp,pOٰ>>e0g@{k!#n`exifutH#~9=77v?$pr96#Ĉtk I$s#R8:DAYܬrۦC1}Uj/^Z!3~n> !u Zy,^Cd?x0t&lUбs!GSÞp, iM@zcM-y2kH0 შ'?W {E2@0^Z^YJi vkKc? C-;tb6  6p;e?jyv$߸7dl ݸ:}Gwːc`o$'ƆC!.kUap<1¹y/+UG>T%-%F G%؟x/Ey ?Wu (xo2ڨ(K{G[)^\26 pRTgN|%|>eW=Wc&k:>ksKŸcGڌ}[ B 1X'SB_+ePA_>o1Ir -[ @nr hG"j9󆔖_eΨ>r015ՂjfLp;oo{-p,x~ANlf%!iw g53Bi@wd"UFz qTJYolIn `gOgg7I+Ƞ_*G['A<{=o@+J>~'Y8KN‰a"A}8b #84gfpޭ|>` 2:8u;ntbgf[:h{ [="9 KݧLPG^$>@qD]pnꯛ,6Ci^6=M9kkB>}rƍd#3ixq:W㇏3iii9_qcdtH[uޱ>vAp:tuRY>A]㎰sT άcRp(jL'eO0ǵ(#*aLǶѠ㣧nM_0#yA'^ 7c)?m߶u~}w,)nϸC]_ew:JUvccR*$m5bD] Lz`caxbxYo'wįdR %1WxT`1Y h cWCw\N6TY5.O)A[%*(:`_3ו77[mPrdm^8ߩn7m3@UeE#TXG@5dϟI]GHX 1he'O8ۤҗc1$i37{JUƔ%-'bȌ]=};/ku,"m}46w"-:ԋj@K7Zi|X[LGk@Md}fl^q{7 }!ÇbѶ-dm،Z{;Þލ ~p(~~6Gy@k6f8%I^Äzp,]%b!{7s^{^1$3| .6O#P{JaV8;~F~}JՅ4O]B+ B{VS2'G``-ăfh P߾Z:A ;(½xҝ2(VwGlV*1%H 0&(n7eǁ}]P'_2@.UI0?sruMlт>!;ۜ;s( A@\{9cyưJiὴJ}qxIqQ;.R?׷_U"ܧ` |cڤOkR#Ld*cpEHkSrJL9\] 8GJӼ-NfqeRn $7nJmgHnok$l3keM 0 Tq]jY>  Gx!MdH3d9}mj"]Hn1~YjML4UKb{'ej`F2^C=lY ]ksYӾvSx}Cy#`">cO lyc_Ȝ^ ?9;ψ11r%gP44cX9eMOӓSiv zqj8~82GƆ)7G\o lN/1~*Yow`Dro (BK/A(* 6T~$d7m{ j^ho >ƎPV;DAl)g}$ׂ~APp3zoyx]h,- ,wS9n EP1yfl ݼ9E)jZz*a\KoLDr>o-@?׏4YCh!F2M8 CRοyT-[ @nr x,p5$Zb pй"z %IНD"Wڀ0ж]q/f?c1GAG+hԴC`4`ㄈ_?#hJ*b >/qXg:]nvq-. cֈY.9\rF$KZjcgm/B1lé "76:@#uǩ=>6=xfPo` 5wOAIlf#CQHtt`}mYsSN^2g~6;]{p@z?d:$+݃Pn5s>@V8@ .lb'!&5j~p2t1A=Q@=AA3T}!!ϵW3@[cY{H8K+sKBی~,;;O?㫴0Υp{ 7®nt t8HG3h)'OpX,>5'XKS{-ᕀAvBc;gaE6@zN)rd_M}zKt(Ƀ{i>3>kߌq$ͼC7pɇI ZaOFn]zt 9 捎 ?Qdz{39;}c7_9kusk#>kq@e!FaxuGo޲糀~GŜԱ;G}ј>xq=Ζy_'&Ryo'u]MXf> rt܀k@'$d߾Y%ou 7 dGYJ귣.seA,0kxv'8Gr5Yw XrK֜az_BV ~ =׀\bˬ"ckb4S]mBB'V9!+TBB1MeǓF8R"M, .~kk?cFv9:\_:jAwhٷY dH- ^82 %*lZkG%OO ŻS2o%6#Ex lenlG6qFi*x- jo%8. 8=!KS݀>Tc+N;NO!lQz335O $b!PzQH:0cYNm) Y.>F|4ķ;b~ So0r~8=4;'dz'ޣ1ϳfkkqa0S2̝7_L|p7- u!-G@\q~%k\? !ɠ&$֟F-]@` ,Opz?v' ȣ a#E߅lt[Ck[{!ȋ5Z@R>#<>Q9ssn;'NSL~x{s}@ 4??;gR*dsĽKR$ 83}50hKW2TAV[id)J.4i }HYdJz`K3'r -[ @n^Z RGpwBJ,r%u~12%3#Vnͭ42:A4+ =og?| ΢?ۤ#Zf03.P(6/|ʪ)M/>اEVDUâ BA+ I&A?E=%iek@UfY/Isg/dL$uCm8;P"Go$Zܼq>=]`,,mQA[zLx+v#/C␌ &3MyS.-k"WTh@F_֓nF"mG~i 1*d5je}t3m[?QgNԋ!`ͱYF=#8W P m2KZb\'49tJöz&ʷ0{]r܉aq8%SnuYY~Od?Zq1|pS<{MӼTfh.l+C]@YuW{G?In]M߾}b}tP5]t2!g udSq,8ôF\4"t|:19ϳ?;$\{7v}fӹtFfRmn2IwS5GwzG;Ix&}Sm{qVȯ[yzMb{ R _* OB01x ~|=}^ȭ6c8q46D6|~jF -'.q܊i|fEx0~Vl?I[Ry&ÿm(RӦDR Uaʙ`q讆AAxλn"7xn+1%, PVARw9o,l$ Q ͈#JmXSt%!U%I3vqyz #ˇGFrٵ2 O^6 ֖mr~84<ŵjhu,qt bL|}W ms*pQV:u촃q_ 8/;kU:h۬x&8|i V $^1j@QzJ^> N)յ p@|6F% Rc;`}N%#w;Oĵ@DϰS9ΐ>§cPk܏ǀ 9xnN\S=v 2kc^11621nb#TM5ĩ [vg)Uq.&糵%vmo\~|v>XׯͦO8@lKY֍{^E Dc<- =`NG%ψl`+͠`==6 P`\+  /m,Qs[T٭K0 f;7> tlor -[ @n/bYbٚKG­]$m*O.nFu:1h?V[]_M[釯o%9# Mfq{s 840ڶhO=.%gn٭HfN~LwEQ!-4XJ v]>i.kڧv k38 =iJoAbh~ǏRN'қ]KW./upG h%!S6#닋AnY~qa4O}7Y0cEw'OFҁ;K9V"_'$λ}Hcll R#1G넙.1od6 );|{H,29{l*7g=`T':tzʱ1]A?״.^oFV- k;f0ҪA `? pPb>O!}N,>?qqgHodL]!l,OR{ ެKC&}`h8=@?cԭAZ!akO*uq /1d/-0<V=.]ZL|I|iHw%@ɽw+W/,m52ݧ҃ jB`&(6 Ameqk`tJj3ZUKsP=Xjy(dy_0"S~jÑ 4* :,#}: [t"fgg7i/b?:$ے?oIu`s7m*^Ϸߤ>%=>((  kejBC~rry}wy@'O5%&SuHs?1 {(2C\ybޯnon 30v 9+dkoMc `QfߑE[uHuw WN[YKߵ@}G` {YDB1s]Tvm~|iwvތ&xEJS5Զ̸ ZKۑ>wY~ T4H %J{G&:VS3g? ZLs8^8=&:te|w6AA>|N_N:1YRO2/dn}E_|ƹX~o.Ifw*W3siآH`KVq9M B@IDATYi; n+n증13+/DlkWQ2˞ƎQ˞@-ƍ*b>љ`P`e 1˧-0Ա2r1 Spxmhv*gT^G>Y0Ft ۴~t+~FVxm|πgm]KGbxVx }^DBcfV817m:J[*OL@7Ȩ]> Kd1+v?$7thiXk:6O򙫲jv>;(W ߏj.xguG!z#n|`/#p}q$B٢$ٟ-ܱ^=p%}{-o.s郃t9jڇA<1Z 5r -[ @nr tTx5 4p ^LY }*h.:i\ 8 5C9ߦne$~? *Xˍ8˽:W>k<hx+[6CA;Ay Cv d;e֑2_PN !%Kǀ$V b>!9GJxK$XO>J 2|VlpMXHduS[Ea6`[8s%1Rd!_亂oi1d\_@sWY5]gYӕpflomZ"f4_^DSf\g1+369H9cX'ݶH&` P)?K9l} ^Сr G$8 `V:όqc@c o9?S>MM1LKҕKڕK?$m'MC?y"IxB:L,̟Q8Oӛ3T ҏj|PHOR}C2%#RusR]1ajsAu+k|BA5ZWf֌i7_@Q5[f ?NLO v萙AGa IP!!穵Թ5b:n>+|$s+ͳcy+mgJ>A +ȽsCdj_RgGߞB_ӱ<Ξ8xz2'5  [/Zv(slBe8 zpzd!Rي~7TOKd*n}P횟 jÔop,*W`0^qŖU\%,I?hwe>w,v #Zf~  Rcleop.syvF#c3XY`jO:J 4>%UC97`@{-xl=3,TVymVR| Ei.-[yA9ra֒\ӻt9:!}3r *v t4r -[ @n^. m0TX]~6VVKEj,Jr^:l,YnyN6!X`e ɜ_?OCzkLhw$?[1H@+;R^)3·slS`>e$ڼ~rZ-`FwaX0df}zQwf$$Ynf@Z<DtW#,#(m`:8$RሏfI["NF{Mvۦ>Nvrˀ[W 8f ' ]IK+= g!i$_zmU:DUdF|Wۈi:!,GTWBm$֭B0ҢQڃJבө#ifN]䗤HQkJܛMu(EgmH™]'%m:u ѯ%ޭ5yVy|1x)tnfC ]%|D7߇Wv|DoooS|Wҭ7CvX/}~n.}iZ\XH.~Wsz[pܼkʕPS[$0{ާwp @37|L<] C(2a`ysyZ$ جpL' ΰe::tsbj9[s\Ηc'dY~&h\>?}֢nLjI8u$G|Vo>̮xv9.`:~Sj;u/h_'k?I3;fyh1gVCKjW B_mHb($]R$CwYYFs(0Lns?83SnJ7cf.++gH YR 0#o7-}{t%{?Y³jޑ6_c;o^)QۯCh ,i7?}pIgdOݺ{Ag{;im=llhuz/N*(yZ^JLF)w[SEMO9+|WTb\]=p:e;>[caCՄa9h'5!H\@V< +X/w}Kpp:Gly [l`6T j %}KMRF-Q,/OAH-4;f},YW&Li,8,w:zؿھ>$_BV5 vm5{r6q 巕gηU={J*MU3}|txy0X{ɇi[X{mJ}6R+_߼MN3#_WJ- ifz:JTgV rka`:N4W+wֶ ;W;Rdžߣ߰gW~g%}`HXW "i䞫ȸIr6~\rq<Ɔ*/Niv :%JfT1׀)bLonz bheNxN ~nmnƻ`[fNSi4QNv|&ngqjm$x1ykӨ& vKW2k@%?A~v}Eg\tyNשX_UE eUId \Ɛ$ӒC}31}[@!*8>$; ߇4 $0낔iõ} KTs!} ORI hM+[<jΕvQ68=݈pA2]8VWA@ʵ!@q{Ur{(@=PT'끁 S˵;y0տ'{&p.BSEƨ/w8~GkttiuX&&'ҿݟQ:MI=ȃHe]Άzsq^9]q3:ql/:L{:S|o%l*U.:ZRMY۞;]&q08$ >&k[&H.j:bYK{-j%̥ptttt‡%PMF  .==Z"`_ 54H-j>{8`*~ ;2}t[{zcI'!!ɠm"kAY3=lE[1 <` ΰp0u\wdɬVc)5%ZWLyS/P1 H9W'Z"Nt0ut ]PFF` 2R6-s4 @0Ap? f0Ot$/>;mĖ=o6(έpj@)AZ96|8γJJ%{B,{k ZQTwO4c ⏌!8%b1]Cv?3c=cK/by[ ~{a9Xe ?\&⫓v ?۟"gV,F|}ٿ7EWpf<fW xUAk[tA">=3"$:nr%%G6Cp9oJ(NMmݬdAyCd.!} 6@U*{zvgFjxQ~ r~(6L[E.wAnh`R^S+{sda1ײylI|߼HO>0 NMAd @gy}7g>3#3Ӎ zAZb\l`dZKd8OfHV<{ 6 NYkaWbYs2R/ > $j}>0Fش]l%Ek=&wVh (p8;c}irM±3X| M^A@өE*%%{ gjdzfkckrm5tT/G!]kq5SbYfWC*P_6L_19gmohhocMgLϙOv熌`^ %wM5Í `Ź~)n8׭ۈ?PAq*Hdb7n(18?r\ Qdy;.! J:0T0h6s&p;s_}Q[a\9J<¸L /p`f%i Ihi,Pԣ{(<#P[RnA=PonOƤLCc?z^o?${`~ nldptttSfJ@M# [k}uZ:Rt68G' ;TmȄc:`.rql,&* N(-`LN {\2q qk]mllvj~;{= 'lbm 'S*`X[ksHd/Z Ot,oƹL]^GHf9Av a 6Oڎ&;lxDRcʿ;7[>Ln1'C GE;'%g_'\vEKqƝ˺f5!iJs`pPzAp> %s~_NϨ_HƁȸo:ەmn׭ی~k;C/(K<4Wr^`Mdb3s7; ͞!e)}W7=ǹ{&r u2GQ.hE@ib|RDwZ[2:2b=]\QgQlU@wRd tIŋ4h>J[>x&cJ433lkzyo|?ݘHȨW޺_|@"xZ^\०!ז4g\ͦgL`}dFKyXDBdjk yLpy`m$ŜV -2+0. ˙PCzX#=\? n 2 f,} Xr-C 2 Wg|$llYUnܞAN) 2p簩sF:3XsRrk±q qk7סF>#C.dcKh@uZ\W"p^!,2#i/WLKèvl٥;&G{)+u3=6v͛7a,Du2U3CϺCw+BbB | 6Xi}yN ׵ c3\*DDUš_-ɾ6d|ۛF~ HWw-@}%d 3VwS3kb)_C̬s*ꈿ?q:YЯVvb{{8=%bxqQEl%>Ԉ}]Jl qSb'2w8Roj2iR~](8E:y<5#Mtݓ u}{ثy?ρU2{G~*7 V ڍuKڣ`>4@29{Y)Jο(@#=qP~{(/z@'K$L5?}VmikNH8чi~n>ܹEf'qn]XpZKntxt /΃/ o?/ux'g0O[~Nal=cS2uRtܰѩ AFYG5ἊN*PK'tq8|K'"@Yzo8k:X|P2]GUoպr2P)G𫻦=JnVsmp(-ȖC/wINTQ;a0ۺ80_Afk?WP,x \x;=@"YČLZ"+++Q'Ѯ3`dlcWUXp:6+qe(D6["5 (OVnȲIc)暎 X٧d>Wlmz}eB=`Ӈ+.lI\XԹh{uz:Vz ceYlAgu'~.KApyE<Ӄ RG - 5dR-2Y/IɈ8;H=ȱ[\9X%Z4(!)Lo fq.KQ:~_P+fߜgc#d %ܸ9Y y =}!OXqF|faaP8xu!OW/_fu$GSd wFPE180n|OGDNtyJ{Yo޼I?&n+oboH~8mCoXfT!S z B6H(vx`D=>T)r jF!ܬ-q̇sMdf;HJ9c}r/)H1+U`YUG@ub0ǽǽS <׭^tϲB#\ z [`jd;9G0$ 劽0;(KKivvi[/! 0ɴ:n B)/N8 o{J'fu>PNOsiȫ'j5NhP*Wvf 1 $W42QR҆YX1cI?Sc"~7v7 a Lئs_UC]8$?Ht9-3N[ o !nj&lW(Hʹu&1؉˘SȫV7[@6xf\qdF}ȝ*aS0 pGJ6-ys.Qy& /gI6Jp>H1.%i{G5AȀ``6lv@llHfڟ:C`5Ry!02%3o&`z1IM ԾG/O .SϺt8[cbQ{wQ<[$Ej5}'p*īY#x^h+PD|U$_s+O3kdɵ4vg(؞CLP54,bCf3K*5Y妍1bo}J Ǽ|@+BLm.ggJ$O'ߘD\K#E\L'IkVzu:ZNh Aq_?~ӗ9%ҡ һ.::uws2CǸi_/jjG;]O] ā ƪxR=kcpȹ"VpAFu+LKߟ7jk;+"kGU~Vx>ϝNU$sOcWq/_>+WXY>CIa#e= 9BU-$!6gAW~z> GWoV_џnQ[f)-Z{UÇ!<˳=9wD&[g6ͱ,\CR~W}JvV1$`IcMY>GC ItCE% u\{V+X\1O dN-ܟ(U[ҹ+Z|u6q|vgq00 "}-K N1`&L*kK *-&%l6"uG+^7c[f Rcv Y֖\ 1sh̖qdjOcXhYj G,6t56{ɠ ?!`aVll'''*W8w-Jm,!ar|"#px3UP3䫭>1:XTxH_.o%9^ ,h'`u#.hNVGH8f7|6[Zft8f>#D CJ^ՂJrZOkLy-C^XizA)+<@èg#H8Ɯ[Q@ !gJm 7i|r@v'C+Yi||,g^M.䌡k$[7ǟ|X-s5j  otūT}@ u2 Vhtk %w]آ9{kqi~>Z*F'm@Y_+xD,a16HcM^\:%Iz2GϸKkLnӬ{>뫂r'PJ5g.x8nbsH& gׯo8zE>,Mj9mYu[_j^mt`tzgT{wLs}ڟڔMd_=i>˳G@d dF j ~5^5D8_ߍp$TKJdG* hh=5 eL6>eAPU`bEnVU3LXU^b_)5^5j.o4uư~mj`= QJFeT%1T <L؎gI+KMURUL`aQ9/$CwAC{(Ն튏=Fm6q rs J;ůrs|߱˰2yڔ|-_@QCN{DJOkz۟M?mqvB>دTr?dT[Ϳ ^z.4:Бn=WNNP/ϪO; }3bVW^CQn*E|chh Iguhwr8'ԱH @^+MW\{ta2\+ۡDXf7|FHSF=vm_#6+òT-]Brp|.yJҹ佶~!˧EP2j.% >{y-$ڳ. sKN`ԙpցփ۔.O&pkf[إY;i.ZbIt%`_s8 SX9f,5N"V}]^c6s"X CD 12Ĥg4L17/f;?en&~5$߸RW;U!=Pm1fXxb:~\бRB/>OoߩE]W(valn#S8EO' YqH{Uߐ$pB^sCl/熀q13\˯r{(@=Pr|gz@]'#f~f$_] Gogh3>XyI@dutk%m&U2p4:n/0[2<3`.YY>kFl>z+q*pk͂.88UP}Oriia9d\8ʊYo\KGEIt :0f\ ʞ{n0SJi1"Ζ]#hi{A?`<-Yƹ6\ k1TQ5ZuWYS ylAq8^@8; Zp\_ |)홛8~5Ծ^g+pS?ʷE6{%*T0ڹHo_t!KՂv>58K o"l@ !@]<2]g0yti6 ` }8CapzSw 7 ue;FL5,{h3fA1tCI w~lۛz@im4 0֨_fqtȣKhh$hp݀4Mzo% JW6]E뻎@!kk۞ R /@MdoB8(7sݻiph}3hf޼~ms!]'\¿|>|'?~,tL۟~(OG,!2@x%mOM?<-,.qdwwғOm2{!sJd1aРIUeA& ΣQ "m8Z DSEc ϔIb]mjb^4إqoK iZUK./0~M^h 1$kUByd19&XOf__#CBalT]Cku]YCA 53:)aPer"ƪG#/ ~w+*$دx77ˆ.SoI')CR DEumką!^QriA `̕e=zO=PH*=Prþ4s-P:!CtA8fbI5FzZ[Y j >2RFhmp*zu )6n*x[$J@Vc#T>EA;N_~E1QPlλcfi+ 3*a/)#z\`eaSbj\ffM3m{ mkݛ o:կS A> 6:f>88DMt0m@IDATG8CYuqkC\b7"쳒 4|U s16S!kee9dXʮI@$cvyoO`y72xsHo0%:MHw_J޺e__]|&#}h }g+`?.>r+o12_~ @_i Ejz)4K$ 3?yF֥vtkJΝi}nanG#Mߺ>GLMΑ_Nc[y?sc/R$7.IwclpPIADI:l  9w z1{=1/̋ 1٦b */5%M:7ݟ\2þ`;\~~*C҉۰ 2'ԩ<Ci$'̱Z]Z=e=ɩ{]zFW]:~/u!]F S*SW\d#( /(G0Asm$7Ș?d$@ϲL(.i:ޘWƄ&RO'όKl&WEh~^ʭTSH{#0rjxo[s'l:8-Sڤq(qU}6Knf %.Tq;?3e{e~ +k ;>w"{p U2jX =J}55d!X@kG>y0w HLewI̦[_fsI9RϮ_ FwiHLfW{M*1^vsn~)B^P&7hl+B}FJ}y|v&X_jo ~|ޑVҧ~EaOU ;^hU11k 50nsP36\4zm}Pv`wF+P"U89M1pvvhc*hT%Q5ic$ND}}oT?Ԑ.DvOXCmDD 3$mM \Wڇ͸x Wu_;?3)Kf_(@=Pr{{8lٸvt`pܾYE,+MlJ2Q%Em8YrR:L;:05u mVV=4cRuڃ]av`d38l|݇a~vac͹jD$* seK$Zڥ0e8f>}T VgYޯ~AJbARg8En;@'S6Nt 3W9XئV]& 7Rwiig[; v;6[ȳ(im rCdoLOh ֎Rm4gND;i]v16A܆^G0=5ta]X4|!TNV/rs]I0Q6̽ie0^~)mp#r|Vr<xw$6w\i.Vbe;!\iC"[q[A_ηKo@(eX!Χޡ‚dfb.$QxY@ c_ we{ %S@mH@0;ùiF׳) C pNzpgil &:@xH#N?I|L+k#R?zqg>v1~+DLǍ@؞ ѱAMUF! p>` C_X1DwKY賀ssd? 16L}v3/6@ \zޝ4qH&77wc5:|Ȓ1$=|aq/PɉʷzN9/;[woSE*B)g(LMCrW/ q {QxFnjR-N_ϦfjK6O`Ȁd>>C?rUF o)xg'Xq<_kG}|q?%oAӭÒXfUfY>K9"_/plPIY3eo 2;\s"Rc玱ռ'2sST)~g .b{t mEj3.`Mrʉ,.}( OJ<Օ}ه9)7ărPWd~[Or>ʤ4o*}U_G7*O-\Л~H$\mמT͵jB@/ 0I޻`肏uMر %$H ϯ~_о 1aμ2۟f3fucяlH 0#9O_"&B[Ÿ;q?t@-sX@׾Uޞ 7sHh_yf xx.dw`}>#k R54z)-.m[SAw;n;?zn 1nĘ)gģ2s~/BVх!. !ڬ|!r.ԙ1!KuA$lA/evV/!s>uC]e;)IVnz[6K㓓FG&,&Ex,7C 9!SJH+gv*Yꀩ##Y8]ԧ^l!"|@pTWq.=@mHnـIdYWA=@rpJu.YeN3elumL~| 6S*H0)"N'vQޝɉ3L^1W9e|k6K`C`J1N(٥?B4}uikLL~6CqgD6$WWVbun0LNuOLLO>u>KK ;x':ȸϟ}zg/w0pinvDsk&}nFj/B;<$KCG=~hznݙdZ1B˗KSUzw}'=˴~`%$Ѡ}ჟ ȐE&2OMUѱdO;P(8#;Ӆ  A `k| @-TνFt$c\D( W]f04gO͛(~4[c}mdG fvz)n!+_;1 ں xK2oEzn$e{ͪ`5>[";9 \#23!|UU8fd2=4 k5 $~6f<#l{ʯr{ ̀yu}G̨do5$v'G]{<VB9xҹgNwߤ7-NICŌU~}{ 7CvAGnvMZ`we(DdڀTsW[_۵mTZSy gxF bj/ism!obS/<8-yn&1U2x x<_@hoXgԍ穾,5GvX:45vA:B%C~XNlƐW~)ղ!~-]Sa_:R\w`OG:) o P &l&lm#mxT4)`;*=6îr*i 6l>"܌j@قzfx,5?k'*C. g8WM#vAd:Cka8i}/D8Jg>ĽnBAu>~!}}M>LF} [>7l[mlY0:UTYxx  uR [+[ӍS>]n ceT:qo* Bz 0ޓaUU1 KѮCK3^QSK9aY[fR!YzId/z=mg{\Ok7#TC0}=˩%k>Q|;%K#A8{ttLsg5u}=~95w#@lkt<7qaĸwcl}:O_n4:<@GkZ]ۉ1߆c@ >B}_ُ!Cw+]ʌ<. }b+) 6BIQՕC(ňQ\?ֵoiAu >"%Y~i}VߏeU!e9oX{|O*9˃BD-\uȺ\c8ŋWec8R^ = YE5oCC脽i=Pr{>ˎwa$m(@ 6YPL6l5F1`/,ID&sO{>Ys8(Zf|+Utu2KYH!HVְWz]vAU K?ue%cDMK8]:ehRY:42uRq3hY t ,< @*( ,0.p{cWjNAAkn"DGlo*s1-ڼ =AV8j骯?qkj{;~?@~l1`]dn{K] )XhPEoS584RgKkwR73 wr(F?]?0@f3w|6 ҚOVf2t !߈nfXJn*XU2ynk!a`_ |&d dH|hír~Yo['M+eWHȔӇJM<^,ٰ81o!f7$p#kEGN{fe3öQӜsd0|17C%v@5z5úvu~*;ʼg_MdilUYp\atޛo? FKE4$\ywҕ?P7>D }|L=yMَR d/1۹vS2s1x^QU{CA'mHU:JSf~ P`u͇C_~\n65#s=a+.SiK ^KbOM7oͤI(_Od%`u|.Yތ~Ƹ庙<H:L5D B/mpfbcʯr*{ѓһD:F; U7Q`)]4`RdkYc)=fIyB$3Z(~ /s"U;ؐϟ=FY`l=J(E@\hkk#ij(rt؍Dоϡ l@RxuHsA> KSmt!=JH `}!MC^ĆkVM&^KZx+[".Tdg 6.]1>ΐNrϾ5fM!OLՇ!LjRQg&6m?^k2WYyġ+16L$:S5_F~c~5E}3U|N!Cmnc0B9n\(w(4-2{ i9b=V^̥ڣI3!*{mI-`II$T6kQMW%>m5"Üdi`z$pp b1FqXrF?q\`Ud xU6~Cd#ؾюD}aN3x=קk5e|A0M"SŗĀ#39L;SU{g9X(R{뮅{Ur{ߍMєr+=Pr|c=]zɔUι޼y dɶuE`3خ;d{s~ (gK52)u5} b Ѻ2 upq+ T8IY@T;dd TVƫ@10R|/J"-aauFf #y 2ciҙ :mAY+;'S&Ϋ`SA=c)Sf``a tsZER:c4|=&CKm&^D`w=5WV5&Ggn%Pdvqgrihp9zۃ`dTH[C P\d8X/z}l)35oNOEmia`$aFrE|`Z@ nCWgQ7H 8;o9asYk jq0-8l褞KбsJvLL[{3!X1屮q/rD}_` Afq ,tu\#Ω{ ȇ4s: D9AzuJ`mwE4g?kS .nHWr I]U pu%xDMkBLեm;@,Dv?sœ¾100$(e\f`|'0:Y]]^bL!@Ejsz^؞ݫ7 N?{g-@1?zrO ䷦q2>I>ȚejA31dnzSCI2z2}?`g}{i9 x$8*]lŘ<"}5s 81{ɬpjX(Q:~VtD>,%XC fg%7$) [c!=2$e/x=1fPhlhF:l4}[Tc֒"`I]c\[ _{f(gy{;(TݫZJdI.=pI!6A BbyYޞv};ݿ}7i ՑK( "q R~{W:mJl[҇O0Ndb=n8}]8Kyqku , ;0~--F}=ClTOYEHNbAvr|FMUl"UNcj tm= ! V<^1?%"U +eث%bkXPH'w@Vq>hinU G 31c5rV diF&!3ʬ4mC#V1Jw/Ϗ eQ-/k(렢OR@a+6z]53'[O pxrãߕ׷'1$rG ݫ.̉~q+Yr=l q_cܾ1 Ar-@j\%vK@b+lQo os#63%u;ZFXCi~n.G}/X ĵ#X{dzY"s UcXh  rd<..=o d û SINfެ.QĀ)b װm6C{p}:>$Ʉ%=#!xa0Ilf- +챂=pqk$r̵+ NHN9TR,GWmR cǻ=d8ϾIU!}O[((t 0jI gC-Ey^^fPXp \_U3!ǔB\l:Y68Y3.K>II^ygc s&Cr9PO"S,+~ga zMD'P ,(mÆk%g:'& Z> '*vləчihgwsdv25R݈;>y Ng[ 6 6QFM(ǔKC7~3u1VLRg3W3/?Q_nϴCފuZ&V1b5;l#(Uc#񠕸q!:o1C恪)>WTf|O[>uP͒#N\/=kܳς*|+yA a LdD>~nm/6LeIj{}ixdl89=L5GiLj$> |yNm$zY43߹YܷtZ:q.aUUjƷPR\ܯ }|Y>d *+IӃcJb~؉$eH?kE@rn ZYaNpCI>!SZ˘xø#L\MsG'6j3{D$t|~\FKCTևX(@=Pr{ j e3wDHWSI TFK~pxnhh-oxl3Tc=2Xd E/ l e% luAf-PFJ6ue *pxø綔t:$KT53uLwwMע60E]C@y+vtl]iem@s@ f*YOMfcj>i4?8 g-%VV>J[wM33_.Q3]NK'UAKW7^~W_4%{EoEBCt+ҟ~̽z#0{ G$R,l_s]OO^IBc2Yy6#;:M@XӡfFx- aX zxLp(VGm1dA:m_N|`͍q}8m8x+-9K#OԠ$Ltt^E-c 0fz]qD3q2;B싮*krGuU:%|32|Z4==Y #fg`5\{&D 9\͝4{յ(ߠW:^B闿U ߾MX*'lnnzuW߯ĜyMF׿PgNMmuY xK L1?Ov=r뛷K鳟<} 0[p33KD6}NFea̋O^"kx*Ϳ|izn~;o?sj&ӟI2`f@`\߽}N'kk9n<"Qc';2x"aEc<;3@=FX{.Tmbln3`&P"]6S̫MUsUp'GƉk{AO9;={ @1y0Zo'{M6lY= (&VΤ gJuEqQ=ys}Dٛ)^AC!6Cg=F4_`G??S=T%@66j݇ ǰS ?JT5}=ό7K rk X!j,LFa[3$ٻw(ؒسȹ qGΦ~ȁC|>(aח0AilPGA55xƬwoMԪ"(/_ɐX!oxQw@>9]1eNx|8Z # h?=uς_ NśT|>vJ\FЉYEH3O*E0<צb<+H;̌vJ1h{n;3)u<::.7(Ze:~wC`m3[k̍{;$ۙ9H+RG ľmT7\̥lLZ ԚƷ ߭eiGm"|aVaK]3Cre;Uisƺ{`uI」n֯ L h^6q]~eԧ6`C-NnԌ 0[=W[R"v ,!V[lDUrTԏYHOOԥE=/rXbC2CwZ̦RMf*Vˌi|OҀ!Ӵle۩mrBA?@g!r/Kcޯ,mU!rGMz3}y rNPπ8Oޟ %=~= ޼}Ov4%5/s\$ DH(3Y9sxCy6nPVD 1J||5Na16H$dl#4a}Ɣd,:b } ze$d|96M <_؏5 w['mks(֢\qLxm܄4|zWG8>wϻN:Sj@=F4za{ qOP3ѩy]:NVnB)E݆saݧ.K`"3;Tqm}iq;pB4.dtI t8~JiP `=Dvhގlce+ש\g#f5O,lg g`uAq+7q ?Y v60sEy{:?i1i>zɽf+cf}vS3)lրR 2:]<+S%t]I㖧-*Ŧ-=J p:u mcAE/fW]L kp 6f@zӾ[޻#PY€'z[fM@::>gkwb{7k X%Lo3ջ oԁIƁyau=1=ÓeCa.o|GF 11SRR B/qwk2>$;{sO Eދ \zn)OȪB,]jq!I&yJ}vGjV6Ȁ/"[~:s32/Oۻ!]_&X^M15Dfx/U WfoFܿ_v8Gy;{3 4,ޓ?Ak%v"}d&7.$_-2ݭ/713n"5ӗKč@xPQ`5op{~[tFJW. 1㣫륉eh#` ̝<5x8VjG2? 2DDPTyS--d`YfR~$H)xrsI'% ]8*80A1*ƫ@@`ADeOhO,dG؜_EwW"lrHMP氥=%!V_U,wO3|5aB`wimwf* t3&[iD47՝ޞ*I/hf& }ʸߎo5Y:z a%ӑ6q^bokOFlktb[ [:D T>pdKyپ׈s}]W q~'#ne jؖbn ]><{bսۋxg_ a U9!8s7lG1kL a?֞%! SGd3Ɛl࠘0aI6|3sh % ,/T QE{3|_u]^Jm  pTM1\ ӁG=F]s_3alx5zh@=0 ƫIsY1~K nū/I[ڽĐ| ێFt`sڐ-m6@Nm:Ge$Q-d*.He/S7 yF⠕̎yM3IdaϾ\"'R:G/_,-Q*<0Xtlf :w 7 0i5@,f Ykvf:[Q`Q`Y~nMs2' Z͙\ Ȭg .EA^lؼ=d%Gv)x8Yz__wL^|Ceßq$p6aid;!F9fз&keJ }ȼtVnqw%|5]KFYdCL6% 9C1?trunu h|QAݧ#y 8xeY^SG[13!:nv1g ?AX`M=)i3>onSC+HIgqTg,Bve+!CZɧbSGwHsu:frf,r.SR_9;>> `5!Y0 hU \WRBE_cmA`Ȍ6spxl PLpÓSbqCH5/o2Aaڽ,m0s1Fv$_@7طri5M.2OC.=ng/G_mAZ知Y-36X_Ga-}U%21f{D:HCXY]皅 _z>-l_}y޼yr_/{iP6z]e p5˽'T_dOGfhl<8ۗ2I`O|-}2{: Թ#[S.=Q.g% [cݢ[d~ƸJZ9 A rӆ62(c#k ڐ,An|OhnqM)-lQ=JH3X~͚!(l ˍFp.h'7܀q!X E|Yo@)uAs ߥoߥ֎z Tl#CHOHD8; l=,9~J}#3A-_sv?8aV5+jf=FVXkN[Ϭ?wrO`Jn@2 %Om`=b p=K&=/V~]2sH_]~p3X78-C°ADa=C̸%aY=7skΖi6C쁙8Yrff`_Vu qptW`14M]1,|ş)Ϳ|Jp])qov-eA[\Cw$h"QWkbO?i@6Yl3<뀩6RYhg` s %/',IQli 5|~|8}wѬE]{@fV@k> # !~5>3皐~g@?êi#w-ް4nt .)HY:QdOچOBᦂz=\!ds@bJ?Q0F}+GEG@V`\\5ɠRа99,hf{vH l_bF|N17|TU!πJ SϪ$AumПk}<ӿ W[@ejUJzA<mH2bO$s[T`؂)%ܶV|G8sV]=39m`>B$bJ6 [ǹ3!QCh @^"aJV8-+ 7"żm eHc| e@ddlͬ>2Ffз37!x7.k<>syn]K#a>:d҆lݟ%9UzfwxE!%b(x=* v >)~ IuUl$sO/&[*o|_t|鋵 0o-4k 5ϢܘW;Dy У[i@=Fz@C?uün`i5^iX8@ޡ/IZNsԳ}T6y)]LJ872[,jޭȊ圞QpbY twt\@?dTp$7Y\"Zn^.ഄA{g" #~gswvvLd(-(?BswfN(ò?<8Id 8 L=$ݭ8?;q0)ێ6\ ''GWɯǓ֚W&3R`' g7[L_I GiW fH Yw_3ExI::>2G4 rڎWpl=' cffNrm:Ifn=/#͆QPKțEQ"̊!k^'v-ʦ+Q^Zȑ&7$m_p?4r瓵%pWݯ ˘6?2ɹD3;udo 8@~=ÕL&V>!c!}OVB%Qk3 t?ej֘QCI/.il@ odc[2f7sT7d26ftQmXwTw `β-16Ef>1_Tt ױbӻe=4sr[{ -q(906Y;ާY|AcvaT ~_9?3{,Q|=a> Z38T9=gI p1ꬒ^5<~z?[Et9h`S4ڡt[.yHi]k͛ RBoOaWh:%yP{8_%tNVa ?Jmdwwv(Lƛ*5.j&հyJ^`G(L^߰ePY;6k^sqp$~96Yζ5$!g=+]Lxs B;M>o-mRd 5ˇ89F =l1Wl{? I|mreP;/wJ c9q}w2=y⟦yaNf>c}{njF=ApY@Sb`lcNX$]qSl딬-/d$L2/vYU % WksqJ0*CsbwVͨUYZ9fQku-Pڐl¶crL[TgyU(d9|AKI1cYk>JgTz} E[c[bN´5BrO1K{+.bE[rY̖rDܻ"kFwHWG `w&)Ў}ߏtY\qGث)jN<;31x6J}gI.}¸CXm͙nm#As/K->>5YuiT4>4 "Hd2k^ߙ-y#1cch??k3mG>L3gŵAO7^h@=Fz@ClAkj 茴PNjO4^eV@|QOh8`"j#8'g_y Q}}BF۬wʑi}֋u{Z"{z#OkWlfN,qE1َ@ߔ=?`||Hɺ4@ !8@X;Wq0k 64Bݓp> U^* o"s 8?8I_H HAQu<&{vkff㻀 ss Hbۛ/=(l@]'` ١f;Jʐk_^H  Y_@PތR맯ujPd!.v7JgguH *ɮ*o7c]Њ{e0=V"lgIRA`Ax@s2SW*f$ O#%BՂ^Xw:igɥu͝>ei8kL8$D)vs}^ p 0#0qtt,֨IF;Y`NQ ,?+yiUw9oS_ٵ\k!d ѯ&FPp<+3-[ ~w~x(ON[6kg1y ,w#wY+'(E-p@yhd$z% 3Aأij|B<O5ڳ07;a`/T"* FKD\ߠ[ PorU@4ϒlGjn5xZf|3n)֗"Gf~KX~L4Oޯ鐀7c|a?IDZݹcFDR} oyi27o7 P睽j}oQTNwÛk[Q2mY>!lM NBÖֿP;PFml<䕹=iWo m}>?>gY؊UlKxsnw9l4;1=ގ}M6ЮMM=d=p2}ӌ]+z,Bz$^sle@݇8F9{٢c{,\Dg Ifm( dPja^>C&SWUN=T:uœ=@f[6G"q3b͑ڌ< /Ƿc/l;L}bWY \_ P.Rzl!`k6#c{[ ;`38}AfA<@2-g}{,5cp(=+ -R?7:e)c JyYEs5fA.6@@ć;A< 1muxAoI=؀fan>,dѱ?et?>~]c\<l`ka%KX缧B^"H uۙ7'M$kނJʃ 9,R 8=|f/k$w=([[o$S0d|ė+U>pmm$?!6@Ņ{;s#kyˌ@q[=g$1(;-`xX*:Ȁ8=1fuFms̖7@g `[ xZ7wRwԿ1>uT'@A)hiK9Gߧ׽}1Vf&q@|cz1 v(g>Ϻ_DY4?aM4{kܲD( (PL OSy݂,:7lw07s<n3 \@w1o )LŅ yqo ~_βidVjݠJoByP @w;woZE2Y@y!O*UX>'x# ;I 7_BPRֽ r H'S'8{sXbobL/ 3\G璓Fxf$+eҗ}@מ#"g>uݛrMuYqnDǽ=/u?={*t ͼ J|<{Ȗfδ2}3ȹ;㏵1?-͢z b Ax(nv\bm2*+Pe^\a {缗,pm׊Dȵ8k3i;~|M\ H8Ύ&sB{ {;߭M.9ƹj̥bm~'jcxyF=AgG7`1p]%"_t܊\35F4z?n=wDN#Q1a֘Gg6>#pܘǽ8ֽ]մ'˶bc\%y Ä b#b]0LJnQn5"A8wt&qe_& ,"KT=3$qqH*XzT5G CbbG'K-E04OU@P)|vV̔ӁCr.J ALs / Yŋ=Ȗ.ٙ707{hlw7Y;ۻ^ +=733PhkW=޳RH6C~`?mnoo0y R5QpNDse5\{ UfP@ ^{)<\[ ~׏(A(mc xx@9ך֨?`6G=H`feP=".87 %S< ,rSC<@YC?NӬwoAH3s3dʕ dϒQoRa? #ϽWSǧo.+MTPo `s7! -w mWF\  !,~2ŋf}}|!o:R/ΰIg?6v_YOd#M/. ~V'!ҿ|^ddt,d(.*[_jg 6 0QQ8>a^FP^i}%'%*Tܐ!Pݑ9rA05㹂uj'38PLAZ;JoA1FtfUƫ{c ,?+ݴU:A}ag$oCE;^?U뙣5sKP]CF5ǻd\W6S!O.Rϭ*gutcxzz߃`E>*{ÿ-{ [TTmhFӾR)ͬ"vg-e߶m!ѿͬ%V/3Pߋ`^D٤ؼ% {Y{4yc uW9cF0xMWxA ֜ WgglAE,Ɣ ^b3Ϋ҅|s;.j5ڮGg0>vSE=RzBu~J&Ozpo )Ka"@>lf)gg~}|NE_?9d̯x+;&~?S $>?P1=5ԍ-f;=,'f!hUOz'%3<-ޑ7 +Ts8l 7dz{}'/D?~ƾ>qVڧCBq36$g$zph,I:VT^򄞟ރrHPȲv_b }88.$֢kCNJ+9*.SYU+8LV@? mSeϲ<{ܘK6^h@=FDzCŬ@ KnMF{)mؚfӯ⟦W"`_"O~vN)m2V`${cElXeL[3Wrɢ'آptOP0ͫul3hT@6.f|;T1OVw V'K .묍^Yjel%5M SWgΌ 6>,@-mU/ !_|SΚjq0^o3ȉCwe`6,l3$ָΥA& o T:w!f-Б}H|*A#u׍㹆B8+x}j&}3@NMwk+]n@HVݺ+{ ]Jid7 O r"HHK2{+ izゕd÷GY'Ȝa@="  vl#slvnϲ P+|jj:g*!8bw2wenvy9ں6\֫g?Yzɫ >$9\;i11ymu38vG  |Air^8D5 4O`Noޑr]KSdʿ~*H6;[x}D?XtqѕynqyCQ-o{&c$'/  `KʩB1QG.Jn@ .$t(G%C2ц)N )oEBb-~S%8:kLBAÝdv뤖C9(̠J`FŻ7[~F?56k~& P\t= (NVI=G@f@F|#,j,83$=^_cʏBj3Гg[!UٿRc]yb#}EXl7؃fa ( We$B_d8YUvn+ rN*ag`/sǗi.K()En>03ll*׹fH%Z HFE5:*EDJ<|Uzk͘ v{![S[kolz{'',5njzLVw}KN/@50(ǃt? _>L#CiLB_i`$Ѻ Ax=Mk NƃUάmJqB [yS}i$y'h=6hi/K*\^EUk*vv ֜Nbw12UviܽƆ]%Hlk@Ż 0"h[ڬ^uYl4^{. *Tk{>T!G\fP#CBV`^| 16->P~z{=bc|2;=-9|2\_ Z֨W)IM_5+%%AWq)i ;~6{$nkK^|F :#|w):,s:)IGX d~j ;v2=@T+ Ǿ4žyw bZ<_.'#1YL_TW^ǿ%s  fFy -Ayc}|f;xgۏĤDM<(?ΟTxtsՁo!ir ౵WUJuuYl<$rG0Sв@IDAT  U Koۑşϰ6trhm]E|x_Y%sVRS~nVUΗ;&f, :9<װf{bx.db_f;N/[[ܛ/Qw!ZŽB\ W 1㻮/?9b=K"m-&ĩ{Φ K YVrkEή VAO9B)FfO-. Y0Po, Op]i`/dggfpZCiBHe4@z贶*fkA|: pu'fpnjY])TJVsZ` ދ€yf !'o >||?X2sܭ[_xL Tf2Œs$3Mm:UvT%Dmzޚ_fS1;C?n ^ +vNС !$yө7;dBvW t :_Tvԃdv0oz)Jt0n0 EzԌI6@i{^.:^瘌pN֣ .>i(*־6%H.pM qĹ0ے.x!ofR;'=n h:Gz((>u.7SdB6㿳WVWK\wfsa_C},cE Gd OgO-H,fzi`{> k[@LRVߥfG֔Λ&6}Ja%2_F~}ڂ2H69 !m( ě5}y\^cVR'kb@Zlٛ jϤ+̥ۺ!h_|1O_7.q߃巀dFbGF_ncc:dc2GQ{%ϛWU 2 wk+˧%{1>\HnB0@7|p]2^ّmثb"053cuuyRf8V`ium`0^5y q5(G%HĴ;ϨgI,~gvI7θ[ °ۑ{xGRP.dJud;{7ҳc m-Y@{и1T&dgVD_WBvx=r# ) \3sVž'm=4(~܃w9h}kG~ x j0(d"3ݏ}_MaMzVo;ܳKtArLo?asϞ0f:7Ӟ}Hfv{/-s#~s3[#!lK??gPȰOys8~ħ~xraQvgc&gH.T{[{m/(\-$ÑRQx8l p~qP!oro*3z6{$=pI-pܸ%b ).:,bLBEKtRrJN̵c5AA[]C&MaTU3x~^9==?JTD3~!.[E)&xUoSK,6ƫh@~`n=w@ 6Y|}'=w8,a|ct3$h/,Ҽ2[ 403H-ōesQn]uZqXu4[k:fNYtng޿qDu `Xw=>p>csƷ:rdJTn`)oV"qrz:0߾`e@J Ϛ-s Tf~/}@C W/`2*yw/qR9g*e}~,iԏN#V Y Gt>ϫk @AuCF,O9RZYy.%'X9YS뗟+g8Y%nY'H C;C32o^?:Vf0tS½Gi;c14}jeep3LYJ?Lv+X1Ü xx}$KD67-"ϜW:lyc_:d,7^EQh-u|W=ȴ1$NY>`ųGp,\A< k01#PkQ213ZAS~˾"C~楄%,D9Rf][\pY7l(^WC{;\\c [ q3ggW_}$"=' ]]Xd+xov0ReY{]PZsӱɸw=Oeuꬿz@۷TxO붭ym9U@ׯ2@gb/5޽w]S|}}ZfCB, kڒ s9}ELƳ"oOafv6fkc-Ƙӎ6i{uu YSAx?DR^Fci8^qX}zv"֚{! _ѯ'ז 2^24;̣l{BJ" b#[6(au-C=;vm2R{{NlVW׃tr^B!.lj%‹teN~*l`5[ tp l%1B.J3k+# Hʳw";304q;g[<3.3h ח.g$'KuBtQnH(k~_ %2wwWHOaC<'^lނ/IsülM*([A9HܳVw#?{iv/mS=Խm.FRp!7Tn ^-wKGڌ:UH*o1mFkI6.lo5W!eK ZwVQ(Ŷxf_3ZF:>܄P  ##9Woy fI%|y ¾ {cm6 9r%61۟2U9-A Cf!l|=Ą{|H?Q,8/`9?~RLW/ufGG1h}wGΞa 3 gn:lHoAIT~6ekȄ?1GȄl2Y&؃rF|bNW[e-dvzsM <~1vdds[Z)YP8gG }ɌXV<>e$ g%r)C̻V0ʢΔV&ZZ*O6:8<0[To.>> B!)M9;gm2X KyG1F L!a X1$k? ,Y_EmYg0j3};Yy4!|*bY: 'llo8Z 1=/7esNש2~_0y( U2d]D3mo(2 @}Z==ݏ}~H#ڼ#|9uÞfB;o|&Hxf4wOcC5|Rv+C<ߖZ.mkL+i-j>gxS!b5$?fk[{8yji\[g^xA'雯[_͉VwCf<hîY1|rI]ω8 fYq;YIt~5 2!cFG 9E,Sl7}زANd{:>Y_p]dG^lg[Dʘ9}: pf p Vm<Ȑ֩TZk}{ )UԺ66jf6l}gy0جsN֠9yo`Lh+]ԗ$9Gfy2}ݪ 8> ѺWu8w7Wc믾J+K_ k+f,'dv/7oYb ӹ2f~/QN?Uo$ٙX 7e[/=\fuWNj4L+k!n}tޯEEw"02}|ź5>5%_XdMf6#}mԉ}ǜ+ ?PMI3+As Tɨ]7Q:wzߤ&2}k3:S'iͿH=:MdP>?ςUԹZO3@C2Rj~Dʘ,@Zn$wO\_L)0rIJ4hIThf#ۚyrtʏ~B$k ;fL8o? @ܸDk?켲 r5,#;;bkErWZ^AKEʽR >Mf>Ym#n+[( <tObDO_l㕾Y-f57\@gO;$ df9W޻!}nx+dKoc?8G=}c:~O.P)]j68X Nv݃#S:|IeH2}գr ѷ:x97M.EA\e-CQpw$n,g{g{bE3 *<מ۞*H=e#P/0yK0D&%s[H}l7}4.+2^@U^I#;fq#'#q \%@[ Pa}2t^ f*~o@7J ׺xscm4 ֣GM kiU~LԳu x㈑GU$HqEt(:Zn!#0fk^҄`3?~k]KSd&0M 0x/HC/YamT0̙^^2 "6㛄nFܿCA;f tp:llX]CЕy1g.zN+K%\0^dY3D7:6ta? 4FZ^c;9FB`DJ|M<0L 0 Ape`avt:[쳧pˈ)a \Ì?Ǒ =Hptqo uZl&[X?i`]H>R a `f^6n]Ldu11`-FLF2 q֪s6[#gk'e~?^OK;Dsδд>6(Ye 󂨨p:H6{PB6zxq:=xHD.f;sΎ1xk~e+QwV~ak4=(;!Tƅ+RsiZORct¹KA@䴿(7#y|}R#2Ϭ%:a5G&rgWOYrX_-ңg$N3~KXUYRz'ǟw51v8ꕗqz9'N[ 65̴'?e½ދ:j6Eqtj 7 о77=ca]9HeD`6D١>,Jp曞 0TVAXA](}`(gfȵx7*HF XF}HgdZ #}?N+qZdC.̓c&cKׂo>'}9Fwӕ1uJEAFZ(E c 9֩o7}5eǏW8 ˲S}&OQ9Q>[f(Ya&96'|]Ό  ɞu/.k$ΔȘxⰮlt \ΐO"\MҶNBYl8`tN tQʐ_YY˅ 8X,xڦ~wx(Fm"a [gvfgů%O?l9,vm}:MUU5zQG/Iӄ-"t!h_@_ٲޫGDk@~o0 ^BժŢŌ!Ǣ\ Y|u1&B?7ik0(EؚRSHCP6chVǛKߔL*7\bδ+gVPO1Lz^GAZmi(w7*:=ԭ69gY&gYN᙮"+CP0TxրZ2OwPn0"v S~zE*"QQ`5P+o h5qk syiZiMSyϠ8lj6? RKWSV]c@' oԬQzO¡^mPP 8$K)Qs?FֈzZׁ)NUPP5.3I6&0@1zH#X?j(虜/m4&Q_Z O] ^,z_]Q #(*]2`4'z70 `h"#xi@R7׍Q k&34p~T.$k-d~o>,8|F:264d~,SuZNVLĮ t2xRJu]vXD]v93[`}u`i$ ,YPd% KKiaJMufG[͛\ݹ<ǽ+ /{.P*tr6ey쎥ۤN?WpHs֧5ƍV~'"}^LOpbOo&}|*K/q_z)+x=M͗n? G]al7c9C}o=x[Ȍp ӥv2{_<:1~&e:۫[ic2 8I`PcUF#  8AޭFG=1|ѣih|8!ihtv*6:{d}|O? D`g/t;Թ%t8+X'W "o<_#~j*ԓ\)S -"-6a7Ms`eMٷ0_S<5jqϔQΫs.s?Pv%Kp J8f6}b@ԫw8<XM/W$3~#(nw>1| `86SѝvTdϏ\gϰ~2[5`<:Yh r!W5 I Ha \KL./џ<|!@!]umҡҽ铻Dt!"uL #ЀOXHDiAmYKɴ}4bp'Gd(k {Ѐ~gv<2HFQs`>ٶH' 2Qzٛ;H=r wYmAARQyK kflZpqVkqOɜ#̷6u͚(#lq~UxдȜ5G:Zfih?Qse:>YJ=/l.dZ%" *^6Iko'/JHv 7c7ѳ,ETWl޳dV)b.s2-!W8s#s5@tFvQO@].}Qܫi.s&Z5K ?Ë1u\A"@gŁ!|wY MJDjW)]`jD3 u!0Q%#\Ged>o,`uCTrLn{82Q"J`Ѭ]ܧSy[l`FZF> [L: g:5myYcY^`v0ykR L.wt؈lJ[yWOY|gxfC֓W u}YL}J3s?m;7c ̂pYpPgpa|[D`]鴽Sϑ:N֕ot2@C6Dtz&1zl 1&5(}9}b>8{de&`t8(1㞛vp u/Z3XqNA/ssMfG_E5 % ]{frώ MpmPAZӞ3v3HA|C7H'QG%_b IéU?^OV_+>;_ WdCj=_TU~ƙw(||K7Q֞Љs z[mptRhG^ G-2gԇ7z> ?RQSnIL:oQ"0! &&^eHM 0`+2>/ NQ*Q[TobV:0r\Ћ*"YnUbh{ m zCͻ(]x D4ңO=@)Y[!jpKo}k߱PX. 2X#)G2 Kt>)0]pf:*<퍷^Iq/=xL-Iqn''j`g?Vqc!Ԛl|eJs} scē'Qr?ɻiuiԍ'?r2G|wi6ۡ4߹{7|4 aL̽5mǡ^G OF ?>=Fjb(<0x'#{Pfc~CbM=f<B ѵ4Uǀ·Deuñe#MR|?g J<]QyLf}} p;0~ADV?|OV7#g}(gU$8:<~Hqm _%-byHQ+;)=&=$i5ꓤ#*mg:l3@f510CZ\.6xҩ];-E Pv)+%18%nCTG.ƞ{X%)hDZT!tmҷc!mx8G(`vIU a t]$hx:WI{66|VB6wRB f,edrO hDjK|H;z++,_^Le|Rz7! Nfȼek}1=ĹZQ2k ީ_gۉ6CQw~Z~1:hϔ.RyY19bp3<>xgW>Vj~RrcMNI}&ԇ0W4yo(Ml!} /ʖZ2[V`?sRZUUF)QݱnCg d)5qgS+V #=8_D(##Uro()urAKee.=b|qD @Z;o7ƠȚ){{fzm`~,oPGrq `YV҂: uV~j99v @gmoD y"'zrC3uhyy{6]4:X.-!i&H}:;8}FVĸtV.w CἤƵ<|gg=~֗|R3΂0(t uq0ne1m~mDasqv>[(kXS^WtOU}Y k9{zkŠt3޽aAeּ?{{&*t=/tqN8op++`\>rjFײsv=Ӳ#9dӫ +O[RifәPKcs5@c9{ ¢D{UJ:hXdqZ/͝=b 8FPA{zQP@×`Ƨ8elq|zJIޫGDVfŨ d!=a_Wk0# ^H U0x E0ߕ@IDATyڍXɧNU1pzWOQnTGsϱB&ѦB:zwܩtU|rtƹ1҆8qIXmo6g{PT>T;<I**Ce(6'PTUvyǣC*$PqO?QhSغo*=*!HX9#QUM4I=3zTF#`| '+aʷwx6Jv( _}AM/0ye릁tlV TNvFӽ/>H #ͪJ#&PgajΥGҵ۷ӝ?]˗𻼼jqo m.d0o{#H7 l$Q9OCmbDP6g/-C?ίƺ4=xo޸Z9n\=B2iIf=AJ5_Fl [޽{ԫ'H/Rc7ER)@%iw~͕>#7^ƫ'O#uUXiJm 4VtH~ij1JX+eWp0,׳ _9Ž/L:,|t&7K_|EmoS} cߋn:Nӗ{o-?Mo.wx2;_>I}#{5>4x7ݽ|ҕowǏM?Ʃ΃黿mx=}BƼ@a>GqJ#xiYw/>^}Dko>$M(!ed>D{D`>7Lw</'G#$B_'~ WL#(?`mk,cР%.22 F;׌IaqHX]!O$.6"0rihlSRM47ux:!sno:3BDC3FEv$Rgq+EF80*l`_;ӸfG[%FXq㑦j\:>ime*Y';8b| ]W5 g`fjo>6,N`Whfpֶ]1~rdGAȆQH.D'/_% >5Ro[oQz12{wȭy:gfK̳J)B o]My9Qӏ{N 9уt ɬ+qF[K+ts)]{-)}^EN&dMca:F33˄TJ:ۈE:^U DH!/чYmC3 BWF96Rh>#VW{ @k x(e@zZ&ƵkȀ 2^އ|G$~T~oIrhQKuQ0:Ũ^GVU=- ܝHC /!M NTdGr#1ʿkY4͛[}m /f<1r(H+w{ o\y%]|]T#u!j YǔˋdvXNS|GіiW.s 'D +_[,8#`\P)~}bOk?I72 !^z)䉋. _#¢eoTmy=r*:63l"eՎ. Vx.30)>5;/r AvCջ3Om uu蓮q"Y)-?Q' hQ5xF?N<<FRZ brv]y@Ͳazyy5UnAykKr}g嘔qK|-Cg{c̶3cV7=={_ʂdC^֑%Q%g%FmƄK+KG ªgY2e Tg(`NC5k^!߬sD >qok9 ʇ !1J~f,W;tlq_Z#upsp8'\Z!yUF䎿yrw5U% ڬwyMOc.]3tic ,g+5=gM׵a?L%XoՎv)VɊN҂.+Bd> AGA%kXS3рQ~B& >Juݫ9a_> "@gݳxi'#l|yj zt`mSĺaیhͦ\5m_ ]ڵ)"^Y)$1^XO)~j+c./ҖvH:(6-5 L@zmEmp u߱lOGIe3hK?] 5i 'QG2|uRXWD}  _4:Pz# BsH;kjb;ƾ AH 4P @ ? UAxE?Ko]Z|/-Z3C<{|}ҏ6J @M¡eQv-n5VXe8m\%YQ1 k!KUh{pûkY@0׽0ҦJmJ g#$:8caD47{ 03?5<5h8]oeb_ys\y嫰x Tl<-B!R(PשaQ06A/IC#L g_dƇ6i4HPw>&Bv^sli2i)&:㤯̇+2GpkQhkpRh3^/u0Nj_T5'}*l9F)=/R΅W"iGqҒo{} \DjE GdWxv !_?V̶ X'Om~j&)åIfd6Ƈ1R<M q AMa7ij5tZ`Qye-se]*O+BM9ШB451@C~&?eN}=eO'}.cF1j( ]_\ t ϗOQv3.FS׹?}Yȳ#ǽx?Q^\g[܈Gן[&󯽚#5A_U uPe-A͸~ pS1@/~Nv?t )Gh\m_~B(@Q]d+&_ǘzsյEikӵW_0X ڞLCsb\ C{&&eJVH~e08U&b ;Szz{ 80 a|i2,h0铗wVǏ>%cbȊcu" dj%OxTGh*]ga&|u<"ޠ/xfj^5w(Cd:G^BNҩBvI23 4էp{!#O0BX7aɧ!< `VL1〠ǜ߂:V/;"S:|5̡2?Rg;5Jփ!ځ'sB<}9=\y>AQhQ^|Jt!Ne6b= \צ9}1O=L,P跦0G"M,\g)<|WPVyf/u_߼mqdC,g?F'/P6$ugڒOPnsRLFdh/&ki=pBI42w-xZ*"_<㻋icdp}.F160L:\O'ցcu.:#e#xDࣘkZ!b,'=y%,H^ԕ40zrrDʣmAr۷) AfCa]h?t~#[olo{K/o4\>B֧{Z!9?B`4ʛF%L^ lC@uJMW82(İvFb//C"_P5hr4g^ZF|+!!878D]I҄lE"wSztfx$`:1v1#Mz{ɗ񘘃Q-y_U&>'9'nhUOt7͸ k: 6|f0eK?19ש~o?{ꭖ5R\ l#ףu8uKR]g诌szw~&LR^5NG: k+~mxv2CP+se]>Ct. {hQ+ 2甠%9"22^%x8a( 2ço⯢926 Aӳ3<se q SC<#gzeAJ'QO[NG1xW欗dc5лO-OVW|ZF] ^NIQDnpaϛ}&g=Pe&ѤM޴ tm?[`c^= (УPЗXAD%A뽞ҭG$u)|qD.y)КJ!O:<>kE:G ʋJ/y=Uzͷ?$ HlLBzb!88g:Bq~[YodHhQ⌣U#Q|FFeB_[}(g< Kȗ&\70pJQ'96MJ`q\5B ^r= t4Pb.Хqx@YW\F7DRF0ڧbQ )4XJ½SSDQLqܠ3/5 dPӺwvbRK˜ό26U~{ɩ쓵7_ sίhTW<&zV)AA4z΁)xx?"5yF. ԟSSeΙ_OfXҷZwvA7gXix3>rv5׼g3(ٓ uyCf4:1GM"S}4HQw8 a<ч4"5#Vù s0Z0J nh#O]4#WF F#c?OSόvrN ]Sbǹ4r/ҠdQ⼥ g xmeMX4hq$oB'E>O=`[+yhs] M}=w)Wko:N1:i5LTA ֳ0o*߈Pd,*43#d5oߺu^eN1]g_~(Sr{.n5^z-휯;?2l_ ~gqG?;[x3ÔG{[CO_ ]@ @)Rh07]@! 0^8 gF0眏5:~ mJ#wx4Ly+8g p]Q{A>0h$s#;-9W:xgun<òF,CkFN O/2am@~p l48*q=0FL[0q}4}YX*iWo)q3YQ 5C^fkytirhNh< ˈ}k1K.|A:\^l}X>tN7eTGF4gAgzzt?,>HWV4K o&:i‡aڣlХ_Kz<^ ='+s~_!DQ3Ed.,Ϝ/{;8&7Y8ܢm#KDU2Y~8TйLk.4k@tM}wUSd_)L?NnvN^kҏpFfpD02odȪb>thf̀k\5zR [aO"zN\;*Y*'jP3hZxi9@=ls'q%C :Y ]֖$h:rvlǹ/<\qh28b,EFWbFu0V֢9H{Ʋ;ۛaAN&1ByBx(s8gxKml8XR}O y}Nt63ͅP,o2zQMYgY|c_d_B` w*lb4 fE=( , N6֨TSe3G)N8x74)PJM%h+ om% 4 6')lXzU/:=5(ئ}zr(Q(ק6m{>úD՞c HiY;aDq(e°ՐKPVIRHGB)2U&gBhP"cAӰfѺm̺8G[*z \$ ›r9R[Sڻu,%BT18\O_U ٜ 7רf@TOt6BY凇)q}7Jm.Oqʏ>*?{YN$3m׽kA.(Cq\tRP/=ڥ-j5΁#JNgevX+)ٹޚy- Ύ*JJOEѬlOhNF9tX6oSS\YDvF1HBV빋0Rh!9d7F?%L`>t1Z<)\3 g>0iB#@(m#t+R4i3uJs (bmtݦ u g  3q[P7_``p 5q}ӎYzR0$.scU up-[·Tg!iJ\cX5p@Q_%'][[:>=s32/SHR{T%XF4y>F/a `v-VΓ\.6Kv'yugʓ!dϵ':d^q-YPޤߗ_##gZAK}OFMFC^sK @nw!x\#R#+>F ɮ{\s}O0ms7CQرkd˜@1 w9g5gG8uW֗Gq?ŠMdZ_}cI1AW) u)GL,# >Ai:lёGg:Ҟ717qL.PH|q`,-3'A`enJsW8k8i#d>rبwrsZjx٥fF>Rd7-짽 0hLz6]URSZc``T\ ("3GГ[ `T$yr3i5E6;74Ȝ#JE aь 32p6)1AmR>GqX~'ι.5֪?.'߹P~a&q_jrJX|@*ä/Uqȏԗ_Ms҆t'7Dy0Pdc2aei'dW_ׇzN4+:MX ^u.4-usdMdF֌+2@?2! ynw!52170˂Gr:F\Or>w@YxJY~K:mw0 :+cq'"?sz$gd3}{ dFRG>Wtǻ/wpj֤{=AR3Xnpd%] }#YɆpf:<ߐW+6Yn[/̬ܭmBg\oNقY9Xm ]/]uR*;iddn+B3R3c'|2 K mw8'YM #)f:T|qSưf̷59K44:](uF2 H}uCS,62:k;ts32JDUhQUcuآdЖ3K=%38x&O3: m"fxӌhKk33cBaM'ENy]Ci69gpF;ytNԏG܊ 2L{3!H&BN >~n?f(svه%(<ѡp\;FWWtb, ez3>Zε@cǦ.|~ gMi4#gg:|Xsod hr| jc upzx~z! =LhޫGzx^ Ȭ4)0Yrz,߻RCBQ7Q@Y#Fb/""@ )PbR裏s5)xc~&K)-z1ŮnPfLߧiM1LAY%?9\#M5L6M) Z2QlT,ҶhBAy#4.@|2K,U0f%;T?C e>B|AA?u\iR!yn&J3tb rq1e^(߇q%Φ d `_4&]㚘g}Te<7sn ŹwKf.[:O m򝣢:y;|ڍ;.t9:tҀ%noD@k'FKQzQHF-ʹN['B|>;?+I\o3h@,izB˄-Rȏ|>>` NYķs@tЏ2gB@C8-- {/xN;# ײKaSým.ԇC4UOࠡe6t3p =ͳ/J1Rڱ9ʬ :HO&uӏ#'` 4:H+;16S OCZi~&qS^ȡev.gd9 bYVգ@= |I @zD,/ }i)u* [@u ~QO}KK! )^/Rg,oK^J @F[oQ&1^mY*Ʀ!0=pGVkXc e% B_-$3!dP`ܵ>f?`acq1Ǝ7JTbDYQU@R`%;C m h +[q9FgAE_# o6^?q*U9=TLޞL[in|)+|w6*U!!xa"@c^_˼b7L/igDcVΨaY皅tp| _1!BnDϕGH.-/!a4F; a\Q" }Cs#?%>_|i>" :;,Zp+t9>5E?i-lfN49)=?aʀ~FW:AY}ƘgBsmR#G9y>o&}>dkdJh0rnh=-0b{;԰adc38#Yшnh<؞QUz><',F\:KCAι*P&=QD lm$00uMi F*ZnrϾzr% `0r^c bݣ3tp:F:'wlZMI haBF+Fc>_~⺏LN];1Y*pjdgIHj{sm)4ggag_K{K>.>}@FEcF1bQf:[3kbf/9fv}!''/XNyncl҉ OcޫG+ח`~:yDZt Mo;^vt|JX 048x(iC X?c|H87S8牸7U˺ `'KwU+ !87A$0wRP-hDt]_DEy"y0uIM6"@x~ <:gM:ΌϦo٣>dd2 j{WW-(ji|.\zj@BRp1Bebl=Xզ&e|)L^>bw8qΏ7Fyuԫѭ\ mmxnACԚ:A"gv+Ȍ1'dAd3RqCnSHޥ ;x'MviGq0eڽ1^7ʶ9@9 epL,.p N"^GbPPuO8R+_꜒E|q[;%Oy{zdT^!4 2Dl3rKfA CfكflH&NSFӞ7ȘN"c71CCU:s@QC%pnʕ:ƾ9#ȉ:6}d.7"< ~ҙs OR[=`)?32LR L]7`]]s)]u^Z6h)&Tc2%/=#npv tờ˟ ?!&/)Șsg$% "XgvP0~C@2IlETC5s2N&)v+y֥q?T\Z¹4Fy+n_+m%N2kֲb- I]:lY]Jqi5paAcd"}Crp}ꭥ[iR3ռסu* fbQ>yS/Όva]i|g(G+|tc(*vF)?Hg zqvI`ޫlJ ^|Me4Y^lZC(AL4diya@h[pc1Yߦ "q5[C1C@ָLX<i2 pvvON9>I]m#`Ήs'_FnM:a;O_H & A>lrJE@IDAT97sy3sJ;+8hn-[8~}*;"{| 'sơl g m\F]sDqN#Q4 W.hjR. :㳼а`;?/^x}9kz1FqAgAa=j:A}-V#C%oZ#h\&F_VaΉ-I @mZ+"Ix{Bi ]gsq19 ȽS:,#3dgwA,9Ƥ<)*}''qP^ossww-wډc7ku16H;s):kiqO99TN"qM9;ԑ @Q^}f\܏OO@ͨ( p0BH"<R`3|'1cfAm7_~b#c1Seߐ${KF:^= |p,P:Kgi46s9obiǙiC 3ҌNմvܷA X-R_F|<;GC kCݤFGDul;EQ :Btr\'b7pڨ"ˌ Hyϙu}9}:#ib : [)kw&)Ms=>D5N$;cӌ«'¹Z$c7S2Ys$k@RGGt,Q>lR6…l5#_H׌.ƐOɦ]wS=`})3iaPiѵ4pFR_9 d4+X-Y-XȠu/KvQ$X-)ϱ[ȏ܋΂!f>:$K< -}-yqFH?:ьòMFM<0׈@/*ːáۍgז өCEZxuo  hf;`9#xkQ@ :g>-ahյD,i|:vU %P-l5SFwik'!wM7wfe5tb&t`#/edj{X^k nF3|ˋ[|ޯq;oϧ[W,E.s8 FB>Pԩ^'ar2`: gk ީ\#F^Ӷ/exitF[Ag8˰@ug J)1N}lu6gNN֝ʣqsik)|\'dʥNpD u|Q9XZl}A8yB< ;k8$v҃tQ)m}ϻyq<d>YEB >OcJlZs?cˌ<>Cx0t 4jIzi`1FZm"j=7bHOltqU4D!W~\7J(y^ͺ|>0vC8*HrvJđ8ig}KЕczz$hovޜg>#h{,1Ft3h#ss訣F,W ~\s1xb>Hf 0|YG,2 pcᕟ (7OA'k={|ɟ]vuP4B =H}/W.cuymsM}h h߱ؗK kE_?kTq8p+v3/U(e47 4Ȼx>0g8HEqCD[% 3<[hpIΉ5sBɳxO4h :V+, r FGXWyC?ҋy e#3քl!GZF#;":g|'t6(~yF3:k6]]WCi]{vc 5{оl |I##^Q'8̐J}cQ>FE]̉T6{ >oA{9RJ|f\뉮dA~{(۠M@lP)2gzeҴY<oY+^`tNrݑId+ CiH0RuˈSz%y5hDJ`I(寿y^-gn_y39n콬6X 8.%ܛ˔p ,@qh넳'mTWҥx FfxQ?}?`ΥoF}% ҧE!jWlXtlI 6{mwٹH]*х~9y"JqxdD n*|6`9{dL#%j_q+f4=I~^>Ld/]2>f1`:j9 2,I?E~-ΡYӌyiEcM$Y{eef{hl)ġF"Gٳ8Fa}r8b8q8PgqÐI#")A$n^յdVJR v^|߻{g?8 @_c21~x@x5Ԑi_483Ai '$\Ir,QH{S5 5kr&Mϕg(cbFyZJ|_iO6qoy)_扯`87YLo"F:޽'IM$'|&lm[,؎P*ًJc)rc] F.^Tc*PXm$q9JC'Ԁ?pY ix%H ̍eU6Xg[`յyM@EeYa7QOagZ䙇R8@K@Р:$x|1NDpc0g26l#%= gZ2J]2!gD*/."G[z̫G3oȊSj0X4r6u0K +4c#'Y2J:nZcGRSRNBcMV |m# H:}:K8clVc'tV# 薙0{tB:I|+ L8-a0XЕ5BkkΈA"'T:K-nT)=>1|x='J#QʨSa_J;;(9Nr8 Udʜ=Si#>+D.Nwd`mSIA_0bg 8aiAcL]ɱ7@q_G dc7m Ag C: ieFwRu;]&૲o2p{%-]{r-Fp[4:TAa螙̬ 0!j_rgFr{BF!YF^zԘ"Q*^kZ_Jfzt @mT}qyÖ[w~.ݸ4E|@*7o atf;k(_Q'/ G-74VY${ #J_d~k #PlGY*zUzrg4u1UA# BFE/RBL~]^zۚ>4 )&"=UJqA7|%R2`*$'>S7b_!x292z \,/*T'iN NGigTgV*,njɱ @bCXOL9ᅀ$amD7t0u* y',j1Ƶl|B@5z! IeD;51Ol yUgkt|||߶#߲r@pw #rǺgAr~P믾 `=7QPB EiИoqy7)O8xmϳQ@8DnDeʺ9uspp흣ks^FYgTUlPnpiX3Tr3縄Q>nF/Xh7+sXik{^VQpA#cx5`X6GkìmW1_X3Gג&|4< ̎|e;k?*h5טSbc*}AWcY4;;nݸl:(0rRTd `?)&jlbzz셅ŕ I~1*F<UlW:2(j<}  ?wzoyd?C1^rIW$'ʳHeP㌈؈f#8cQPb41:twc9He^K/Qa(:chYLHPY.l5jSZN ܔ4U9ZӁ XKgysSd<:N?v-JI;qCN$YY ѡBˆ)^6oo},c5 5k}8,KAU'疴[<:#4{M"+9s]AGOF!|tEbnz;4{nz$Dg3F*TUtcb,3< 8(.'%:Yz$t1NJgX% Gn|]8ʁ:[ lr&GXtdND,a^Xw)Y5>(F|3K--.əǀ>A3ϱv8t8vDtN?Z|2nʲ:zuvR| #d gϮfvdBs8xxegա="gd9x" ⎂[6%\cuG|& F0MHpsMvч{8X#7(rlS}70SV.2ԁg{1xw5٧)4(`' " }aԩ/@ڡ~GB9g:ۘ}Ub#7J<ۢ %:{!#9K;9j//}+-47q?D62IfV]b\aO.*㙍E}}u5M=yPNf?Wq ?nxpO\;(ƞc.N[8)FPyED!>p]?.vKΎq1Vߍ^ydKg9&"fxZ~q±ñLQ<ݽRxHm8P#$TI6lZR/QLEccc#1FNux4}#sP@L[~0uϏ9Źxɟ^`!Іc(^@Q٘Gݖv2zn Aps.}զSu4W"ͬ?~ ,ϡ%=({icc&'}(C`g H* M%U޽y-]&6Ã_y hI^K}߭y1"] DPns*R%Xĺ1AM o~M߼~>X;wի1(Ra< :p/":x_èk ;H:N ٴ07ΝOvU0Nc:y~Xfy-Y,WdM@Ŭ#M8cb4n, ᖴףq|f[(?D#rC}? [ƨJm3*TIǻϞ9!}(5w!1Jcݧ@o9+v (ޘC4G$~z6|cvu[_gk?>P",'KO"bT9]h]T9HuGyN9Kaki~tl6J[Y1'l!yY` 8?vsVI/oV2q䧂Vʏ5bc~*u Q /GtO~${:i|MM=8|YԔ劔60)}+O9"А݆BYþ |~10iY,ޕsDg<ZK^NuCr xgqD4R!1vhu( GmV%hQ5@1SCr8:fe_.̯MDgvZ9LMQ%] v0tjtn|/sNC:҇cx\>̳ifGoetE>[-ynoPZ]QP_VXǬ7;;d`{@S]ǔBȢ.CZSfpR|+qZ|CoAX3 Lƻ^D?aO_A4D?r',DfCYH BDXXCz0;N)P604|;AQ_=iH4V2KGZzQ( :Sgu<_\gzTsV,,m%J3\8 nQpۡq;7fkc-MU$r:3&+05pT 5#z:+i׈HgZG)ygȻ0%+ jzD3RE iT i@S9{bzӟI᏾G7P}b o}][Z &9X 'yk.\>|rJ*Yj]\ڈKbB P)S!d e;^y뜲QJ!= tUs Xd\^#;+GXF?RCާ)y3Co*W!:#YVZh?6ͅ-e}*go쓧BxxSAr@89o획lPI{gѶ 7S hcᝨ]|p\rn>d* \sqqgcJA>"߮E<8? gX;g 7D?#ѣdMO*Y(EԎS-{uT-LcOg!8^S<1C+EπD;ci]B-m^ ! Jw*Ǩ ^7"@ٗ2Pm':֩'rl(5vWP(KP[kQF\t+igxY |@:u4u'TB J$}K)=F$Ъu`1=Gl g39_q282R35x/;n]M#VZH:]ͻswS3z:BD4bic^f4Αm d]eI USf GrDw߻Zc8a P\k``_&*&4vNbS,!_D8/p!m}apq Ȝ Dgǹ*3FIk+ uf(Wvp_І]kGwZEܶư%JJ yp뼢'Ӧ'0LNc;G#= s|K}|\Ms!˜YF3Q XTtH^q4ۖ҆ 0+/0HM0x׵Sc##,0VK(}itSe{)6Np4l|H|t83}%{TznN=L{\6{V/)}6DAfv1Ā,?c{'OBA=ICcWO}HK!1☣t0ꒂc*[(/IaDݡX'5ޝPvaLH{?g~3=Zvz;4+xXZ$?@d T(=KW/WFaiZet~_ڛwoӗ.?AUpTP Iz d^hz?#X"5H͌qL~ {V1nm, }iiWW0m%}UTb6F5{~_hii~H^l>nnPAض!Ky欯a`..@R@2A N'#AI<ט4\6ㅧ3\y{ߟ?^ %o?ksT/FEt-5m.M?o,z"/Ǒ((`]ZC=Y[\wO\&`z8⮓_[8f +~Ct2ΈKtq<*q[{G=K>`l|SfZɥ6> eM tʜy1IsHϺo,pC㟼@8km#K{w?%49AAօag|F79>@zu)OeJ Ƴ& ENO#w,k?-'͒JסWW:2-"{ ɛP%lVSdp ـ)0ⶉA•gRg Ȟ6ux5UbP ޝ9.s C;O؎NĈ1W}jCc;On< 8˴I?"l2򽑅Zlg'ݣdޕMZYoCkdARgSݙQT7wȳH)1~@]K$O[]GЃg  Ǜq1JHF:'km35 ,ү(D<,e f{22{}EYYkF9'(W8LW,\$MJ:JuKe]ls~k4f=[uso\{S+2!agΛJ6A04}3)Tz,;:`* y 0lnX#R,mx6s8X?zTǙ6&x7#0 c} 5J5ng9KVl}Nci{}5`)YAPAZ7zs$+ž .43B$h8le* e@0Y\*״};:(e 1Qp6}Hk*:0F`i>8%߶yt=DcedM)pP^2~7E!C~ 4UZ"ezyǣ09B0qU@yoA5~nGݟO1J482^ ~8YM0ڮuT1w ˯RnF8Y/W0 ?Hw4~#(j]6џ0](q@0qWxEV5H|;!( x{Q A*.0VlMVڅ@3<6isz3FPL.^WL3zFɻhIFXBZIa4y˓&G'޺ӱZøZj|R A25X9 ptĴCPmIшkc"A4@w6L>yŅt5x:xpM3ZmڽCYR_浀xmi1tF|2l mSD`ކV-n!)닋%699D5Jc>#ø:LJ~7ckzVbȲM3AZ9QyK-@IDATI i~6gZ>u5CFJedg}^C,֡_P|hm{pRD1VSiAȅ8 M9sucOw1v2h5>ALGxKyuOwU }# r/g >bذVbINGT0, U Ct<d (K-sY6=9<8n);9:S ŜtG>#Vaz7!:lu',wukqJ);Vy@G`༳| LYouV)۲E{<:kg~4Z3Z:q;NkD2K7sqq:2*?uj`":ZeB"H 0s-g_:w)ou@PR7m/q,D[[r&Tv;ͱiC[ 8or`3>RYԆS΂]3Lݾ^Ù ]~B^Kߦ4 u?@<'Vbxy|_W7:ݸ|u=Kdsd XtTos\߿cS| ޓhԎ9ݘ(;(䞔9`ٞBCUbVO[3@>ƝN9*_'2n_Y9w:?+13C\iWK|o~)<牼Аq#0F b61(`/?_[jq-kQ_ =MIͯr.O`0P\mpN*\1{j֎T`mC9a81?6=믽Wi:'DEa굫DlF -NVRgI 1޴B3_GdV֥, 4غ R* v.uiSa~%B,䟀wgm' @BhHG˴PXM n_cB.PWI›)XF*wܧs ʈ^@zt7#uYuoXER( 41&9`Z#e OY_R%FDRa->#CÅxȫ1+Y+@!ew/Ԩ9.gN-\s8!;wۇ]*|bxh@\m>_d{g|-51sPX| )w.y?d~\?&Y E_}ts_;=vURcc UrTQ,.WezFɨ# KPG8>sx^H #P4tC4 _N!,YUX:Q(+["u {~Ы @KQzC=|W-]x>ɳ8_H @5%L{Q VHވf>j}ސ+4pۿb7K8VF1bc>wfya׽P#=,sctg\=dBu Ҍ$U rIE>Wz8[qmathc{QƲO9E`M4Kb/S]cyȰo#s3 '_'>_GCHghx%/{8)B:H`'SD> @^:uj"m*aC0P=(sct3k;}H}/&0mfw[7o6/ug pz,nڅyJ4e83 WQb\dH:v(SDv8(%la>csR0F@am7Q,Z?vWTh =Ed 8H]Zԩa+]޽#>txÃAi;Awݛf0"sbw=MK  l83=25V+Hd030UwįcXq7:@2l[Hy`pnmiku ] dWMȴ?::(*ƣEW3>UP6 lj[]Ts#CP&ZGGƍ{RFlBɄЋP_O˗ t O#  D 4ܼy3}s8Gy0ng(¬>՟j79"mFeѸ"F{QΞbnzK=n_xD?q3?8KrL^Zc/ SݧCeyt_>*Ky^CdmwcgbwU xmAuitz(d'\=J ـ.YP64I. b & j"N9ԝ⃂\D" _߫44c"rE譇v8%P>+T +(W@i(32mL7sA.C_UF$w |0]v.m:ug!`3NmJty>jkV#4)R*e#ӫBiss (Q Tkx*Xo CwYqU̱]8 ~ndRnj(5Jv-(;y< @ME印q_d)dޱo&ѕ7 ᕭOP((b$Ռ4LF2ys)sIJo( Ƭ蜮bsH×tTt;=4R$TDjf9ya&IB apƩf Y!̧Xpj&+C3a<0uQt5Dy;ij>S;gㅑ*Fs6o(QIo~iA.N^M5##p;;i(v>ac:Ե<-Ի]*raEzgZnϟ;g8MR!TbuK ._Wu~> r>=job4", =uTGB5 c+bN!: 7=R %ٯ0Vqq꫕Tb~zȻ7?Dkkxd_;yGmg|JyveFs4A&N5V=e2=S 쮟4Ft-9N0-p¶Mf#a{Xow.jȕ#AdHe\v\з~)'m*mMdɩ1 ߕt%q;y:;B )B,ݣsbij/ͨaf$8P-B.Ajq <$iP_#jD'c2$mU&AMd-";ɒ\wPj3^rQ'uvN̝ut9f}hbd֙W!*8_4fWx#C#qFi,M/̋d#@0ݩqdV20MJƐ士8?֦SgęAPTdt:X#d8)pS-޹>TYxaDQ0櫫+YRӕER!~# l6eCŇ#kx* Uyb=v ewa8LO/d>0 cu/YQ|p|cU4Xy#c+qqhx(mmw!aΩ;ͻ٩t㍗9¯}9MN2nx{UDr̥H$C[bL1lD?Ll؈"HsV6"ϫtC+p01vY: nKY!<*e%qXYgJ~r/[^jA)  f6`SA#^{݃^?܃WFǧaF:_"C_ɳgHOVQ5~z!x̲Quh~_;yEF⿂BZ3_BWgFU遾`2"YGF0zΜbN?٦%SɀXub&j\C0jY]:Fi'_sz!]OrjwaǪxm2EUsZF[E|fJnlҀqD ƪ&<5)kPnm0L5e!QuÀG6עBVԸG(hWEʖ5.w>5׿(), 2Fkx{`ڱR'TVX553 2^:ťd Rwg/"%LYw:HD4Q saW ,B(><ؠT(:RQA}w֝h8]J;2U!SKnY3@~Af]ӎZ%JZߩ" kW+ҹZBI3&gCWy>{ulwE:=cW`T>9zD{SO04ZNA,#ˌ>oV}D0xNPuuQG WY2ɥg8)XuF?];Jmj"b LۄtV~=nIuX$>Qk8FT2u|7Bf>O`4vTzY hT (2B]ZLy*O@ͬP}+v蘖c3NB5 #?ƥz%Z]k#J=nGW7"݁qh`OQwL(ڱP0f-lP]2k#:k: pI/^"o hwl\k_KMD RkwEw_z*QھmEV!L1LMx/F78=<^H"NB;]ܓԹ5JC:i'? )WXsActn"M~2".k@{4T( OPx>o 7;Omad6rRsF̕::z/7thOSO>pja3W R:7B{l}Dmm2{!HcFtJki=;"dݥߙ"_ñWu[ vę#)r  @:k=3X|}8 Pʁ]GAڏ; )KoquN70HANNT}{U;*q~۷n7".$ p@mgQZ'X.Ix~>FeJBbQaD`,ݟ10aSY%s'MMHu"8P_* \Pv6N"' Ṕ$ C@)pb=PN*kA\\yzO9C<{5r^dDFg&0dwY'ш85T<ī0GeY$iqf8qٶX[UZysk5224nBg~%}7~*YfX=.0ցx >ρ@`GUtwt7!v. @sԩ;őJcg")#*wF>y"8}ZQpeϿO5Ji]+KizQe=/9Ԍ3C Bٕi7yQJWtH%L\)'kpӫP4z1^[ K>FnVͥ5JD)ރs(EӈQ]YA{o% N{}%*h{D7̈ӋCeuhR"DIʃh(WYy3G2ܝ0p~FpbdH+O@/㞊|~{GuqאީL0"T#KAcfYtȴ/nh:ƫ C|N;Y~%xriB \+8Sy择2$O! h"˟>^QVK^xty᷹r/5Ԕft[(A+Ȯ6 ݧ}æCqjl-G>;K60ξo x\}~@,ìY'Ϻ_{DǾm-ձ"x&2Z;ugSwur$Y2{_YŒJV=X1SϽ`܇.am30fBOķ>2#+]x k?orߋ MsێaD>{Jy}f0E ol^4Npj3tuζ5Fy9_h4u,EЉ~A^3ⴅNsҲxEQ.O2M 37K0*ЙmD{o+kT|*"cFl% SN%30e6 ) 6v(G|gu&ѺR;1kF~d&T,` )n q)}TT:vnl(~ <?!}=c Nx7@M:Hp$ K4BA" @4bgӧ1e  ߍVۓb!5ldﳥ?  q`~$΅aW}` >(haYY:Z{RD3~܃deL s"# 9>Ѧ.svt>S[ N%۳W0xGHQmo s|އ^n/o>.Lu:UHq f8g/M8=šN|f~>zk{ o[zՆ!r3MOd h}x)] __p_I!ԓO!ĭ#Ю@FY<#.P1P bYge; {02Ƴ?{yОff B4VNж+l~$M\y l :`xz!""ZRS'p/g*E( R{HHQ Q[>kqj QXl MTqFjgdХګ 0Pz!GFAi_%G(lud0Jݘhv#4;7*l*D\[ֿ>3ezC`Нe|RָHsw{] M\}ea{ 7t.x l(P^b*ƧLwM vFȫH;7oJgfP5fVLZ>vVQssD/C1bj4ޝ!׿n rb흛򣴂CC1fr:G* t`G6Fo^x^6_*&#])ߙ!&c2cW0n+?Ir @rJ1$t>|'>8bs*t2C^Ǭgz7 /-sV:ax*]{{a;"h(~hxG)/j䴬uQh`eI;h)hsp7x ;m>`%#`=gtG6SATV)ج4pzeɐ6s4!l)K3fѓ>y|12!ÿ+[{ڈa֦KauuS@眐|e1Шm-hi#u,[:XƹЉ>M'NJ:+ch4\BîNt8#lr'}e0;[*!fY1wUS[àSȌDSG2qX{{^gN#Gs {znauwM%؝*"m!?o ֶQ0ܨQހ(` y/+iDwh 9{Bxʂ ="::JRYs,ֽwޏNy 5ه1+K}&C-ƾZ#~K0zr%2ޭH4~)md⢏)֘d+|!iտ3)?%x F8k#MFLT>_zH?X>%RmcNV٥J2[lL0j҉9|wJ@Mg(WE6Sk%>p.[wl+g1s @eKKze=eGbnw4Ƴh^fR^Pzc> <}Чa'E1 t<0v=;wh f =ёn;2rfe}Nkc \"k$Y䷴C{~qvyC |Eо%4{ٯe^"sp:˭NHo~ڨtn#M˱#cN#z7 )Tƣ~/? 'mU~2dCAѽL%OW[>8ϻ"ؗM:x辱ܳe n8 b gG![SL@IDATyχ7- [S)fd}CGLXq/xd7&:gpzGT1J e@r{Cݏ cf,/ǥ8 4l̸qo|FF]3N_} K%cWf~DXCT6~6W5yw q ~2re!88'BgVVnGKqK~N&UO/p!DgU32N)oPֻoptuqDe K[CV3~tL>8: ZDžLn *qAӵk. pM+iٗv8uvDWYwz0R!x֜uX"Hd⦦]!B C68y4fVݐ~k'o n姂m0z^CQRqo #,"؄A”%u*Q, JV&m 7`⹯FVugo X:5_,>ؖg ~w {< q`E4oɦr@≑O:T4iR +{ F6h Ҩr%ҧ.A ru<_C5J˶;ʙx8!7ݤq/mvƫF;VVg"4wҤ_ =X3Mu*1ƒR62|S]3zKuorE/ ^8 3^ *4:8&Tp5ģ(AѻiS@1.2O p>#G45JO_⟑SU-מPվ;x`@3Tq{ƏBAhԣccl]-K'9[Z76sf4A޺VNi?P^O~G9ɓٳ 'k3ҔMIx;\cOXwҟA y+Ɨe"oP=!8qKOh7Cc1jZ^ nz`lK./1O?„i;/2 t`ؼ@Po?097*4L}Օ% aymB퀀/Nk@ñ:1 k>\a|1ҚG;˾it*̑i0Adlccd f4.߸{7y.˻Ƶ7g޸GzE֢M3ӧ̍+{c'7=7>cƫuHO{~߭3&+ٗ#RQNʽt,Ers:{iU5x}+6mF[Mў~[vWqN\z-#YVM8[5ln`'AvGQgUn‘Sۣ])`58K ]TX'۝g)c} :vJ=pac 770~p0u n8XuʋֹNmE$Fcc )EHTq$Sُ4D{+߅c^O_= meƷ?{~N:Jc`dℕ7j RxN9o _e% &ҧ$t}jk.{E\ 5@WuBu)y-ʖn*.V{m++)Al҇?+kWYΑoݥ[G'7;vw`@Ltb/Y@.9ͣs6p ^GCPQ?Q@G0ǯ^z|STkoj2XiQ]ת+ LD}{ƌfu/&- g۷*g zH@|Viw/;-ԧoA km*+%M>ڙc\WV=ʑdrfXW`t_]Y/6pg<[JXRGȱ_`*P* 0^|)LׂS; r+5z.ó:3~.c*M"-(M- trv::<  NFDu{]8y3i׽ڊ3&YZap:Ɵ#`Nڱ:ZB\<8ZC+wKP/AGMun u2N8q;{^}˔<[ĝL' ,Ch4={X}('#ʽl 7/ j4c9N|U CD`uA0`fsˌhqzT*+%>51BlFy)r]W!tsΌ#w6M 2rLHG62Lf9-mGty/7_Pr[5Vѿ*7O M1<]"')qG'?ApHfV4θV%L#$ݼ6!𞄀„gyBNy&#͂G@Hc#ipAD!j $5_;ٛMi)^hXKhJŠBɮGx {ivW;ZGCEV0u:/d gLqWprzΝ>D{~׃A.ε3:5wogy2]T4#4X ">O~ߔm2Fi0p/Y+_g2tgC?OP9pV4c,*:-}z r"?c&ޢ+G=:yN_e/Uޖ[Y~MᦒLkJg{\{F<#H YUTT4w:(Z*P"9:5\80WVQQs8wFYwϯpUT3lkUhTBqޣR&YDQUo"T"PR!Mpθ!D8Cy!$ =R23YLVث00 ot&2`RxLË|煙 L1S!\]&بQ25|[Pwxvr)mXp^*gΞN_w.Α}$^2YsPR;SFi#WNӵ*{+'\Ov%p f8㶃ulitsN'F:C6-iG+~5pj°d1coJ#8A-;3gg&`C* 19d:oӶqqdqw`aN4f҂cV 1cM gy8N bo|o43uJpJP}cҞ4Ye^}uaV4+&[xwxj ֈ&/|.ky_^ AW({߇#Kuٝ*9>p>P^|0Y8߭^F#Y྇?^~0Cữ/_u׎;`P^;so-ʂ5Sw׫yc~m^嵷70]+ese]DYIyjwm2~+{# 22G[-pxNa+(38NzG:kdspi{$r$uxya27dB39Q6]V3S XTZi4䦼+n#f`'(7rΰuZ͚zՔZs\?qu֎I2N[eH:唛#f<Ȏ2;kh"y}EĻ{4{SEmUʠPOĖ.9VVNE7! ZQO&uNF4yQ\{ySޡڌ9 xEၬOt$KSZCWߟ[)W {e.{odF-@bs;᯾C_> Ώ%sʧ'@(e-TDPK|s:܆?2JC>C@2?BU]gԎ_Y71ʕV~dE/loX -sQ` h!zA&Sh;$GXDصs\ p>K3o dd[S3 ~nK+e9~/A㬧XL?D8JUhU?M()b4IPzSS ^S&G[YeZSIT䂲I} }wpɠhҝ6Z*Z8h3S #U3"[*IeK;McxLwdL䑞kLh7eݱ'|&|=p V0(!ѫGjRK2۟o*oCB%9mۺ 3b gyvyZJ?geێv tކ z ]$dǿX92O:4=\҈)SH:UWK wLj5 AIF UA"D*R"ŨWGeϮPA46cMc^r Oób$i!3DAWD;|돖/韕)v AN3ޅc-ѶwGy_kJ{t-#ET_+Ll<}.])?OllfS*5;wyδ`(g51Pm6 wFApY S 8&8hVeY&WH U\ faBq ]Go)3 :5ni7A#yUUKz, ,oVǎU$#O>2DB(ËPj,Qs6hԾx9dnﯖS'O{V2CVxV%hg(Fs]ƒ}Oz0A#`1uB3ʕ4CZ/lYsnT2Eҿ0ĞU{Q J$#<k^sqı'^ l:u&+ۇʒiڷg*ڇ*7 J%™T z-OO4(awڶ&"Wk47ۖq2hJ`=TlW>Ewi$1H'y{(ߏRW!w Q~)}@=Qiy:ds4D?ZԤ":煉<X.kqרbPmH &Vjh0u=#f> Gq SHYrT%N%6F5E(߳q3f!\uZJTSdM:[)uܽaV|^kv.L̘NڳwiihjW5ƐrwUqIյѬLT:_Kq=uM_;sǘiॼffzᷲdbF̳-3/PWzIŪ~#YG/iw䯋57Ͳ^d&_mc^t<ާ3.mhS8;ɪs S#} ^} HeMtm:1M]/}dlgGp׌ñKnymBG/[[%dePQPMhb,54STޕKB}E*NnΉnqna ⅈJ?Ч4&_R~0l=X`͞A&Z _>ZGi_޺y p#x׌lcn|2kxoky}ƮZq%þ\<9FåurAƙ'LV1&dk>iG!bl*>OڣM#6x36fO ֶƙϽtFY@hFʃȀک[| ~.n>ųX'1bmClWƼ :U<]gzgQqяN^^ӽQqjtV6ϕf8m ,)ίc} 56@[Gz˲{2c}.rl8&ssM-z_e?tv੓QD^ԡQI[9Vn8tiw gqL<w}8O99XQ*yMp&,:҅7dB*5 $6 fZQ< ?lADoyZΌb34k N6c\-##aL hy3貝?s%HRi97o ,SX^,8=Nm~"O΀mۉdBVYfB%w*#;~ȠssoݻrDG#ꞦC>Fި--s}?7SOrr!蠥 Q>™W(g *}uĨD!3}okDyg˗eJUR4 5r阑tTԹ)V]C- -N3D ctH.k8(1(YʷF7-nd1D*Ve`Zչtc 2p"yPmcP-?eu@ZWƧ^pq$J CF%Y0!p"ܔP2s(ubܱPIUPS,8unK2e 3} "-g8G˱;x7Ne= .DywkȨJ,!0uKҦ.ià>l? !u.][xhFܗƨC_i2|\FS?+H[ff 4h,.t- N-Gأ_oϥ4g?ylg} ڳ{G$`g~7bw?EJf/X\fPfݼw  3@Sͮ_*M'ANqq+-l+8Jء/u;452|DU^ )|T"I ,Dڵ,SӗV}lZu( M9H wǨ^t乧KqCxrKg[d/+p>/elxkD!Kwl>sK NȏNl:yM [8Cp*7ބ]?}}6r2:;;Ku.jWbyn"2_'<%1Hݏ#tg6WnjApzj*GK٦+E*nWWE^%*fu66E_.*QV=Wo%OYu(,m̎{L@A_IHKm祐Th@jGqV9vQ;`2G ` žJk馒.BB}2YH8Z NN`};P[cU'Wٯ P&*dbsK`30]QZ9^-v{NmeruDM(k6hSXoP07[Zփ@M|mB@qKX{X+vB}ut݇瀥_X$[9v*8uTO7Nw-k Ec߰L9seę2\a.ipda-ԍt>}of.{t2Z!@D^)],  Ii0@͐s8[ "Q1*;?JC7 Ł;WP ȰTΰ_{Ⱥ`kʟk@Kl60Y~CkjOΚ3/~u=AtޞeX ՇlcK({FV'aFO',KL:%hk @u#`"8M+{6||rwe,:-ymBiV6͜})T $ U4'cy",hNz&i= YFJ@zekMVC,_ǎr"gØ]q4*;AJwΌgaěC>L7Mad#ݠ7ܨ#Cz}oWMZnܶz,BI;4熯㨖L pBOH{RUS8VgV~T<{tLidxS$ܢיJ~r iF4ƿ -s{:#/X 0Sϸ8M%bJgT"(|=KxmPxt_1t ~1lgLZE3]{r!+*a0$R98/D~x-:{T-ooP;u*D2KQ|}v'3uj -qXkmqOX-DuTsҩ/&ͽ#*#Gc ̈9%heyVp2&3=g}ۯSӸnY9MY/xRW}2G.a8{;TRѭ lPf]0?˃lb21VNGk":h\b53t 3J_ڴ f;qi>u~b4~1Nd+cv1N& љX2f/eeu`s)d;p!ޫKQ)<[u^ОރL%fUϬ\2ǎT~rmryil e]9qNp|ޠbΖn& Cdr3aVZ[m u3`Y΀KѠ99uy_9{@5 ݃Df6qP<7g=ov8,3V]92MLi*eXS9p'#PW/OMGa }w k7rfe|5O#౥uבp~KNgUs?t\_O.0Ypi?{}#-r/JC=Y&%wK WڈQֆY'<\vܿN5bRVKZGy i8{|[]bk'CtlTR~g*ש@}D=k#3oi6pPuD6T'r/2B"|'iSq }oʜkv·gi&LW.Jð!ЀcKV$N(( %09Fe$ɺ) iJwhx@@}n89qIo z,F^+K0N.޻9y~YY{v`OedBt -6zDjB+KZdwřf;OEv ŭ Y2j?T쿥toBρor[Ye5mߩpLrjSf\~它^q*)WxM[**ba.r#Hp$pUϟ8%(A(0="7+ܚy=ӡ8EA_9a@X}wz{ɨgf>2thf,#l*XUA'xl/_dܰFXq}WKî4ZhP-0b職tf\9d~<{!<`k+ϫJGF@A>-v`(>]1P^bV# FiI5d xeg,]94kLuO4IW~BWG6~4ƟZ B:`1q: ՙOcauǑg|;__|pt/ve WO :CCAI#(]R?5L#=[mMyt~,T)@߭LU{bGT)_y^p 9=Yx/hm#h>yCϗ{g} }8 pސUdzX67O[256uty5xWp$/5U_+ʍ$lVM&{&|fP*|`}Nn#G67|q^J*cP&֗p))Y*]InItm9VIR.I6V׆SQ݃GY8ͬב̱ZܫWpi mphtdLe[hiIb"Uԙa羫-~yN\CRgf[*JqXPΡ꿞MO-RpkX1Fsf$[y;_uqC:km`TC}x0Xm72?U/rLuvX22eM ɘŅʰDԴNoM2zWeX Vcifl6Wc3u^V\Hcڇ/_Y&iE'}ЁNmѫMBgLF"_nAIڵ:h +kY˚k@{ҫǥӯvAV;]<u;Vnw %kCr- \0[V"s#kD x9jmt@f;DiU >Y̫/F J9DQT\$pǀMm=tuifr 8S=0ҿ|G(ӇȼaAޫ"K>s,|s0#$[6ȻtbԆv~ˍ]ؒg](_6y ^Kj %8҅m)軇IcQ:3Ͻڡ/9#>ҵMUloi{ *8.{rx{Xj puRt s^y {ʞݻ`2F}9,@i(}^KSdx|Oplt6Źjtt9srȟ(kUr!A79xx)0x VX×cR ְ=qJTKGux^GhH7%`Ȳ1`@rj-QzbFx m!n`z?W3 Yu{A>p\=,&h(J]w6gȀQִf$Lp}7ZoQw2r :R,Qe3\у9:33( o! Z$]W^𣶼O#2'̏٣\dP666PvSYrp'^tfмmfv3YVe|iL&x5Y0pq'򙁋-i3PW$ 1/)@j!zpٴ{r|~v~سSYnx ӡf2:ƍq{xmwov .q˟MRe׻j?k p: 0Jc;}|Gqfd:ˆǩ89zKWdyd rOkҌ'N/?x~S8h@{ˍ}g4i6**N,Qc}{]6o'ǥřCʦVݲomtE`/e_9T]ZZ h}ЀЈغУg/zU . fǪ egc:t6*ߋU']CޣmEYݹ,/,ү9 mXنU1!]CtRz+cwnާ~ͣNh:3w7c_pAKڧ%esЮ"l{+znՏ{I/jil\:-;Q<͜j3fy=LM", n:(9"17e(aeҀ'W-O2bKgN M*0)l@m|\f 6Nʣk[$~=\;aM< ,øL цv93ݝTI8JbaaH LRvT7!aq]Qc~QZB@.^,$㚣=)6TyֱHGL$|iTPļWIrn~`ƌyɨ% Qn5:ߚ39Tp-K1  tr`rFݗxet25&, \ҪQ@Hn4GqXd7)ДE!T@l0]!i7D,Ќ~ɂa>9'C|f,A!L `7M =Ȭ9z©<|虲[{G>QEA扎;vXygZ)| 2ӱ@ԕrV RkJ+__,)H_2)%zԩҭyG;&zk?/g{k\ OMUKC/N5ixXÒtsȑrTAA$tF*)*(5*: m9BLJF:ٿfIGd?BmI+*} L pA; V*"|A|KT<H8ÏR{ї [6u}|ޅx$2^FC~8Sxl.8=ZP=Nv7a8t~27wO~:??E'j6I<*_Q9To/|~ДAҺ%@O$QOyrsVPXmIq>3(*:t`cvڀJToH0LloUv)f+׹cͶTX%6?*&>~T4g*F}M1cMAc/ ;rb*c0qюBƹ:nEx*sfpG)fJdcp7u0 3Y{pT[pu25-xw]b dhf&}m?xǓ ~Wo ߡ4$ͺGXmkmE=Cy{գ!3 7HcZqh5hl#HuG${i8̇Wy2WÏgƯسw_~ko߽Gu_}+l}@# nV?Uδ+7ѽ!*kwRչcpsA~7r%؜jIr%1r_: .`t2B>;/z5~)4p3o6>/k?\ƱHS^y+ދm˽ޔo5)W"x nZѯgq>œf2cWN$f|V^&|نi˞AqI xvpvppxr$C:С$&G]ϔ]fe [8Qﬤ~/CsB\}c E0F앪[T:uF&q- lO8W[,7=M%O6|:{8=A^/X#u6ʯ/Ž KHg+)P`"ps K5iʑK8fyD6t䞞^ƸP.b{[7̠vQ0Vt_҆qok*-)C&o\_m"y|ue3bcc#,ѿ-00TeȔyJvNM'j;+" uJ+7{T4%$,Z>C4(K8`c降 k3N Mx#ߣ)kOϚ}.9Ԟaoj#`MS||׎rlKDbz5 A`Կu$ŷT-w&*gqr2 >ǓDt%Ǫ1m׌Zm 6EEG?nL(4V &! F73F wlg:Mv$)wQ]X%ZNҐ64[ux{Z!{o*?F:I_"C9'S[_^uczƚ$1YU VX'lA*G+8vu^|Aڨ7kUqړDc~󄋁(VNbˣ!ab&`o#];U[?:\ʝͬ-~Or!mvUkjV,beD2]D,ڶs/ )LC!v\ٌ˗^:RpBQu9`1&dt(!Fy$J6ΒBD)0-;FdhVXшꔓ4V_MWQ0‘?r`rg RC?P3 p- m^x! /F:Qr.ecWbxk648: nqg4+d+t@_Dy?Yߣc_ )- _}2K{Yƨ[گ2?||*,sN0rO?ܿ-C[#5p(9W0;, u<TPO8im Wi2(8fw i&}-eYz.WiB!p4{Gh$Y/>T:Tz8r/|Ӕ-ǎ-/d2M#36qz=W>]%l e;Jq/`Bg^i9cL;Y#䤍9ʰQ>|_|Q>}æM8ֶr "߇؃˩cǩC2⼀@GCӄ/kz3gƅ?qi .ً?;3M 6$'XHX)И bJ+BNfd"I=#mؒvTЗlksyY_qqx5^xF= 4x.Rgp`ؤ!sXXS-c?*Ei`.dW/JVՐ&"$#tׄo@$S"9q &N\eQ^e~]E<<'cUZƥ aCM+t4,u _iC~3C*_2Ag?,h/|s?|~NẾ?yp{y e嵍"=ym2(WGWuڏ6<8locyZk4-v\?6iƽLǔmG& Bb9e!w|CkH[e֭V/7s;LYvq{^@|}0ڞdSz-:{ƭe ;U(*4^v':*kMtə׻{oNl3eێ`+d_Xi~ׅÍ.fZ~cSn[wTFMKuN!-:%]VMH`Y?uc{p2*kCȤ'v]t\OԅXCVf"K'=L+de{0  V֧Q5I^nx䕬E N%/b-:Б秐;zIB& s~©sqE6RU&YC_] ]=&gp{zg{7q+4,33_K0w# \{t$d9MTRٮ)Oj L/ΚB݌$?6gc=nz5lHa* .K0ɂBP!q9v3ʝya2v7|9ZXXL_䫺ccg]'eO̱,{3#88GDMCfuxlGW;LFڨJ#u>,,![xS- Z2yzdNfxLB zN':`b<:1\ٍۜ-R4l|tJTZS>WuXӮNuqbPhmz:H[dUxj&HzI3ap+? .-p;Y'á t4;,s9MԪyfW9lݮ1?)Ú{D0=8\yW.~xxnp@vc`xn2 o6153/aL}^$xcLx،tۦ4'MFgŖL߱V Zn Y, ,3va Ieo`scw Ra WAI񂌋,LZ (ؖ)b*)Dg"H Bӎ 7yDcJ fi)gtd[Ll a!S}Ү1C#slo 00 ;\d/n <+SW y~cd_'tR\Ҥ_260 dRʢ8q?~:Pd*F)nf)j^2bu JO/P,S\Inمʋ(49knN=x^ (S{/v9_!O|w̭Ys!4^a@r {t;C:hНİ2㾥i^ 3k8Tyl:<xӈC ,F}!Bx>!ܢ= 8dFX5i3g-X+2Ҭ 悻:k &Wv#Suə2IM:*]DUa}葟+{_Jo?Op|y,|tϾEJOvZv#EJ oN6'0s%,M._ K$ }+5J?sæې}OmcfzlݱwTuqr?9 2$w'\|v7"[aRǐA2z뾭%LJ80S81dOkG0:& :?±(e<{rJ?{KdRcyXҗsccw\̚ XK@@:׍x>bmGs\Li+䢏5a>r]@aUq@*΋A "RB,X qLX|LIܒxo%϶%f:1@e[ ͱ-KjLe dT^8~xܳe:v3@Nq*=Ϳ+WN|7߃z$43 Lz'2?vZ.eܳ==vcYoNי+g,ǜ1v G0wX]T8ۚg&c00#` } ( gN#|}׭A΁u>/>Y1JΏ{8Eg^>T^~8 ޽|Kq$ڜ_ " /?K0֞P}:ufo-49t(7A5ĀYX^ X29=CyY={{t̃$NTx:+-M NuyմLBt^:p%~z9,ՑZeT{$*vA|PMo/.Z"|ػD\KBUԟ*2#1(YVsy ?Ĕ)wޫ=LbCt F|,OoWd~B.fF^;^:L AIm>L@ ' :?.m!V( >&(-7C^GNjnUSZ-PA?*H6 x`qfcg9sr=7籺A]."1h٩:;O[9]Չd7($rF40ٞe0bO~^,e]IrlU]9[U{^G&GWjVս}sgN|Sk#gfTqOgM0Ư!"+L30Lm۰~V#*a l'Ly(a`F짯K;23i U H޷UoO> 8~ge@@eT!2qkٳTrN.MR~:c!mnlW+1 ˟ҭ$r%ʤt cʷ.Z$8gS;s?>;ӟWbfV+}O!tL [;>_a?e"'z0Jq_T=*j;k#X46es[Ҋ|e XH!j*d tnO3*+mãqRE5 x[Nq 6z2d¯NcieE\y%/ߋEH`v_:@AaE-|LuFo$NB8smp1f= OdC:%j&~Rafy_ǐk ]íI UP Bc}QVJ^iM]5M[裴uT&{ D}P4 %3 Tת,a@B+: pPryA@ag4S=3T!7N[يˬkϪ#ٿ55whG.U$ IY*48N>w[^QM/|ϱ/9fοPD|;7 6[:oj7a=c~2dxe[e7u`mOF%tCo*t k;7w ?kl>  oGWx;YyY#їSXe+ޭ 9 ؀dvQ+ǩq&>#m7Ihä xgM8: edMqӳ nSǂ8/,6G{y3cY$q{d X ,V{M1ītɮ"5Ë^,n8ŷ[xOy7|uE4 {]z 'V"2O\}2ƇMN3"Ƒyp1kK slAG[Ҏ ^{@flUn:t{%lG_ 3VuU(_ E6(I |QGRV[[8 QYZE b3ף*`ǧl 2RX40.@lxVVɏ|Z*/D9O C&jHd]jlkL6@d֠HM7H±?Fu/SJp-r;z֠SWYiW+[pwV*pTAץݴB'4C Й뀵Gfl}v- B*fNv)P/?|Vn8@r=ϗפ\'[$"uvsi/atvqM%IѬ\ǬK >F;Z*OR3/tW8be^97ck'Ff,Q6dnl[jܠ]>1 @c![B~E7r\kY={4c]͵Lcgyrˤlg [ '8Bћ&5StRI@P0v*]ԂM'[q7!8_kQ3B>s {yf-TLi >v;&u#hF_M(_L`7lg{kHc-T?UM0r?u&}*[m&`} !#W,7]O:M|0,ZAc$0gDLz=C+MrL@-ڑҷ4+ >.lbm+ׁ{-FvCIz J 8S^vkCP$LQVp~%!e,:K8VVȀ8O>!o!{a|aM}(B"H#DfPV87. "gQ6>riJb4D2[xbR.ց: K u3G|ʔQVG܅Җw|E y|P|tG!~c-Sq:Ah:u,4^ü YQ:C8G+/tɠ|Qę+2+]L2aEOW^MeRA}7҅ǯPXr=]>qVw\g=}%|t5YuI 6Wo{2΀cFVO>9|.|-|\K&yN#D0+C(6 l ~֢4y̵U/L :3d!~y6kd&cdtf؏{_'˞A>*Wa nxZ]=OڅϦ/|,;ZAWq;72a|$]?Kپʫ2-7^'45ti0xV'|hz h ޙHYq|Tumkiriҳ~C+s< Kkx!Zה7H 3iWC*-;3r87LN# zyT3r5x%l[?s%@a@IDAT*ҩϦ3,UQ찋50C~[$%܍d'٧_#IKc洛79Α4˩$]x |H+bQtS3rR`ěߠ"t/ӗӊRe$罽O(>lMy4plx%-YP{ykMҘs=OI6}< 7򹣝47nZ1rICW0 A0{?ͽTw6Xۼs>GA5-̌7jwSsO=.| ̠^N]zNKMCi/8mӲ-}{aHK'.=)1L&tTŏrts0/&M#QrΤ2@}[iw5dPc#ϤӗKeۜoK3$LxsmI&W}9p>,> A勩Ae_԰fjF?[8TK+_FF寧(7 X򭴼8 8e ζCt 5z|eyi?s ,?3ms{#(V1 (nI$RZ&ySm7%h(Q 7W(![NScwC'no?OwQ& \G@ $wсobjmfcl碂x)I}7_3J#$yj0U7u-tK=R=XYUI}S^on}CO8Fua;FU* IV:7!g7"Πj;CTVGSדkp+*#;bYźCp@6 8үڤ 5oo ;JA>J~ ձ_1>4qp@E5AL꠾|qjdנhV؂`Aε+ 6#~jo?%ѥ ` ι&+ĮB4s;b|2V𻶡A}k~Cߡ02eM"/ɴ>쪠._:%;S0):8O) n<6vn$,o1Iki/,-Q4G5}iiܩIrӽ5pR`u]Bq]}^dvvj'KQ[(!~HpݗΪ:4|-SHɂNazF 7k]t- ;33Y`qQ_4M a/mjh̄?4&wvK%:j7mn '|?Q=m;{Rv ϵ -uBzv,dFZMb#gkYG:I]d%QF 7>0I4p7H$wv? ΐ'X:R sƧJ}HdclтâyZ  {"cAy.{n[5y*E1rt?u53)a'yqv %瓭wdp+rd" &#4C [g0MKfeS*i:J F<35lvA֎'˗`0CQ5Dۯ %ך^ˬ_Aq2OO߿mHgtBUBDVSy |P%,G܍HlC(xn֦0Cg*-Q1uXٶKƛ٢*dr! ZTޕ+d0q2ѬA8֚I$pI$b^/=!kYY% T O S?1?Qf ;*s( WH%۞轏6NhN~*~:klYf "&Y)|Nh8J^zpBf'p҅b :; "~ j/Jk??ҳ=Gg;_忉'#Zr,`efhs_CPFFE%t>Eļ'~qBb(a _x\'Z{[P 31 ..H4iHgAv1APxs ͅy~P[/^86ib|,`,4+?f Ȓ܎ii11;g>gwOpG=t"˯Ml097aQx>Ac&T rrpHeA+H9!M<|,mh@\ ]evP {zm*t'_zbC}2c1[PU 93Rrv[j.c:vt>$qI< sfT=42J"z3hn~:Z{o"{ޛ?Q86ş5*'nv7pS!\_V-,.Nln,RxzéM:9o8 Ckt)#[H‘YRK—%J$4V xXUcPhq2ܺT"\0z:&7{~kUUޫ`\(i+)-(5Io5Z4@Ow^F \>}>U^{*UxIsk=]~Rd6s n."p'XTC9 СxlO5ᕍWZ޹Okth)Aưy "#{ \d;՛G_"$Y$o;l 84 j Kk33x8\g(@ Q9ZN+ $<`ی!+RW[=4A&tc#ʹ^*Бs\k~_AP׀ZK5$cۀ#Eew[LX:*/ /N05n^f׵) &+kj[oOJ|tOСRNX%ol^dBm}YoIMؓz&Y›s8Kit-A3su4vVWzNE~sY_/ LN8?~MO QI׷]ANЮvl& @2یV˿v.ԮބgU JHM}.OKu\+(9\5Y ֦q&=/dv(֗`w,^X&讓 7pD{~2s6)^@ǽ ~`/ t(0нr>p<ag󺻺 1KBi8\T܌otu,ud P%QeD0>nalF/!w @fڹnq&+!<4Vɠw=YVKͣ#6!@V8LX?J8oN LmgX/[z|2VC I/*֪U*N2s#[aЫ NΘw*>tj2Be)^G|أ#|HP( X23gfH`Lr㺎`=_:U@URh?w6`;ȘP8 ƔIq>7  exTF7b0j( -bJ4XF^'V"m!-Yʯ2^gN.\,*Gu͵JҾ{ ,#e]g CY0ۋ5\g!qqo\@)YFԯ„/ln6pKJF Zp}@R|d8nFa{gҧ>q~T@[k@RCJYvY5.кF xՔE:|w2ٍI]xX#Hߕi*[q@'8#~}} GRE벵*5梇\sWn"KpQW/\^4ZIAf$KL ' vRgwfn_KvY ]C`Kr;|$s/3=NYU*Om 辔lf' _>o5d o@ Y\wCVhotV(w*h%IO\z:]Si9:o G;i%h "IЊs8s =P &p d[ zVE ,Ms bq{4mt! if|HKih؅xfG͹h"o:aٚs'c,ݳԅ28= ҄{OP?wJB?WB6gNOЍ:N3! %ΉW#8Ԕ<]x+"t{.p]{6!og\Kq$ܟ-lYRst>?`ୂSW˰0QzXnP!ȡR5>q@{ԳϦ~s t$T-..Cš#j!x.]y*`S{Y_[ʏGgp["U?IY/jԚh^ERzn|_A?lsN@lAڸtoG8m}me(R_o;ɴ g-pγ -$6y% +˛8c|D T$\k@9wŵkSV@og?*Q]wgyˏ[gb0ڏë6\A:Ri,`t1իTg^(2 _RC{7v 0 ֭ci7u4tE'@!5WWH׾AL <gu1olʐE| _'nD@WƵז{S½J˩bk+=ZA}&pYZ:*~9ReOJ?_3"[TCϰx'-~-[! -~ QZ}[i /ujt-x8 svnTVow SOMžL,ߡ}D "iqAktn;6'{oxHە_4@5TP[FΔ6q=ߌ@3^sq]$6ҵFOp7sf3`6MVQ7v 4FUzww5h?~xp l\cnp[Jc ܱem-  >Wm|1:NXg-OpQ-$8wCMSX~oųUpŪ>\LI1ȣNVSSiiy`eФڃ>gu0S Rt|a-yc4!&1/ a-EhuAbmw sj ]3kwuZ&Hڠ ҙ]>F $f$z l8SWթc)BOzbŠnv#2T{䌵[b̫i{4bⷺ, d^Xm'np&x/Dcc m}Yf"iEu`"sD$NH4}Ҷ/mn݉ )`߭gVް-4[ P=]§u?-dS>8"~5lykV 3-Xccvguӱ]Q' [`$/j'}KDr=:Ci'LRrUh2%VS W!Wܻv؆] b"2Hvo8* \ ?Q5o;vڅO)"S$mTakOh_J&0KG*$)3V;uϗ/ٵ+ߖ熯 >u:]*=)7)yd'ba צ?W믾dw}\UȖc(8Dĉ~(vU,LAF>W(4®30\Cz*dx ɇK)")AA htt !2&Wb^#aVɚ)䢲*̀flJٌ%  2}Dpn@haHLFvkpp EF*16(%%K߻{;ݻNZ!sA8>U ΂p ~P|tGM!}i8cB_?KHy啟f 7[ Ơ^00` ar"sי%d%9ӒkeRҸ1msmϺJ k׮GϹ}cT~Ao_;R! KR56҇]%w*L=ƒv8#9#%c bmCuIbV jr\ _ϸ&h㛟;U5(TK&GyK>S_aD5K-e_G@@!Z4 N/1C 5lPiQ |Fjdc 7p8u@_/c"c%t =G{ts}7֬j`VagϞK;2z Z12c`)h]7F )#wubw7]xKl0 ֥~o%aOteo7b- :/R#=ϋkyIYU V$`4L?s ܻG >WV"?9&O`fq3~j"ڠ q pc}p9 ,>'  ']oנqA>~5Aw}v8Yt.h$Cs'yڌ g!m0Ԭ\5`+\p2LMEA-#9gNgFΥǮ>H~TDA~^,5 m{$y3pH8JgҀNMԞsVS WḰ%C^u\?>8{*C 驏Ϧy*uxQ%ZΉcMFXZ lg[ r u|Y4#MQʡ1q׹nV[HPVyd r[N1AyGI2L6Tº 㝓~ҫa=OV_#ZGm8E PM vЩêG=͗xx\v:Uْ2GVa2q5C76V&HGnJUIc:XL4x~P'q_P\ '%ҖxhusU*f- .//UFnz/ݿ{7u8Rõ@ S7 U&+@K8xw Cl~#O?y `UMSi-ëT#gN:> Npir@@&m5p޹GbG:L5gY*.8?[,ͧ~ BU5>EhZB^cF:;< w# PU# SY}鉧? e3Fu%T O^}:]y9a^zx6a; VK7Shn%9A>37:H`[DA4= s =Tok: {I>=$OMkq&(*3S8o>>J-G۴őNz#s9˜7! 2Kz $x/t4\SN&_/aҴU5O^A7B*+[M7(xL20te^|eUcݭiArHj:mXW'^60m%|Tv&Hc.t1Zuß'ތs73'YwMjj0m~M8ܡҵLn8lO+ed28-ڱv0}tCvCَRY0đ$ȧ N :B"efz{Va{5`I!-,QJ.|`ùtx^`!6[b@{`{pOR9詽Tr޹ /?\Sgkw%n56r|kuUc$xV7i#q m-4` \X*_WCTnf=̾X/;ct_~{C}M+ YyAή/:f :4y-Ehurn&,pvoE&gC4.]8ѣ!b2|&,H3m4r:L p6IWZ_'ɞgWV2ƚ$t7J3w6 72pU8lD7|TgmV|șxfŢq9WÙOwS7LElcMh6i0m1}!>JNqUV٦Q]c|6X2gmr{6B :=JRPI|ʗxs_BW\t8Ȏt]6ueSvf4A`ρh$Dj|Pڧ-Sa7;qXʬMd /\@3]tVt$*lF)ʥ险o}?m,ZǑq}:uq=*3yޣ#}@@LSeUδ3c$(cNZY.@`z^{⯿Ã$` `&Daߢy8ڏJ2z: Idfn>z'^VfiJ:}GG0!F#MW/C*}d-V0}??"uZ#|2]KskQuxbİ2QjqDYs Yus4\!})(N"r$/_5}_,sD"AѼ3b 2 r^^ZΗ54{2aTGID,F1`$2N=y_0=b~ =)?L B)A!pӀXw-'@J.t7W1LDd=8A '҃vȨVT9pl4dix\,ȊUd}TO5wy`oθB}%#EߪAaeѱ-2!Vd0|Wvil 'O#'")%بA; _^c`=uy&&(YU8" #3}݊Q5z| 9_ k+Ðb>3{!w>:1hiӣZu [5C.-3: g">>3Kx^|>3ܧlQU= l UXddoٱ‹s C2YŶyfXz~:ɱ\e:>2Q\V;B޵gO?Iz{igyQ!SF\!)@^O8i8„hMYF&7d_vkņ<8!վdCFQ'd]/hCLu`YE)gBfحޜg%PӞ$$L,oU𬹊~e ]ڴ=E]W`gwT2X@:NT;▯ch^~,L]St{k1;jӪhu az/N΍473_ VևYD%NO5hI M~"YcY {kƿњDS#1DBȢm[-ξ}DXq6o˹M,YU_tpMM{0;& wsHKwQ ?Av/ՑW-N*gJ_}b*\x]NuXϥaP-"QE?܄<%P*_X`/%M{ٿA޹N? ۠S k]_bG[C8XUlu ]@k/x{%i9TSvzL*t-_5YVQQgE҅.,}ltڤG..vHz:[{U&=$ nYF +M.c9{>J%1 }"*7-*l `S[D!T;J0S9\d[//o'w2R;gq07@83u;;XUCq`X#ptQ6Ry $f8v n쥶n Yx9in{mpgBz<1YG49?ޢtGv NOO-G[/ T$*o7#Q7E֓taT\vmP[ @e:W' vf1(,hQL 5yMg{JI/]#&935!N޳9VX~P]0=govrX5Nh]p'%,[']y_+ߑ-x&)dyzq ՜H!*68mfz#W KX"|Apa㜇kxF1(|v8=|8a?:6ݾ>*Xcl mDmUXTK$ۜB(f* Yn :U>)~m .)LI"ck_H?`7M8E@9.Is=}&;NH:vF$ЈLT3Q_ܘy ^fkHXi̓Q$&˗.e(:7HYWBwاc\І\V .G_+]qy$7E1Mܕ &ݝJw)R=  37[n:6yWf[q4Jt>hLk t 1ZpNLObQQe /jHAdbf0L߽H$*ػUE# Vhʛl^@riB*aZTT,sdB"0^ 6TH}S0FRS |pA'Cdluga4cL) :~9YRSg4u}j̍{ n@,ȟ/ /~d#QQT2AJyNV)Vg0Qb\.1w2z6CfyiNk0  ,3%B2N]|(|w3ai\&Ye]JYѱOuLr~QhZ#FfM;8T֠ `Dƫ$Mħ2[Y=Iy,{<~! }y*M3-zPP~x}<7jJoϫ>޻~ }dA-qNSX}1tR , k=3S g av, x&3I`'Nno;z #@:mW\hL5-qmoL6`bPDW-8HUz״aYk2mmVWyei/=n0U! e@!,!ǀN!?} D+ 30/U'FAw&:p=26~hjt64T$٧.3XkN}ЛxϋZaBmv9T Z],lf0} fijE"|w؍+##\J8zkٿVGO|u"ofޯ~3k$9VpT$щxIméޜ\I{G^өCtvp$N+w4w_a,16h@ ]T<4`#u=g\@IDAT58ziR6@99::ϮV/lҸCU׌J`LQ+GtD|9Lx+UL$5y˒W6z[B<FۗpGsJ :Ƭ`c3=!\%\OGǵhAk-Z l6-Gnx?&PWb}o_/^ Bgحw&q]7E'bEI\hrugoiϼC5%pv N.????W<nz_xџ#WA`';4Ќ|jkH?贙U7 =V]tWD@* }ܭQՊXA !^p Uwhe̵Vw1 ^Woy h+mwM#?"jk: mᘩWN>'ZSȔ-]?N28ЃU`L%c l믾M{X͵5E__IXUV*zmBiG|07qCt.tUTuvemSjཝ2=7֗D"x@0|~YŚqolGL'ە{RcY[L ng);JS|hSioe:Q}ǻ*3cE>(-Ƞ;Yj[:hg`˓a[l{Jr$ %,h'a*1]`h^>_u޾z#& ֩aK{܀ek uzt٩EYCb;M8vuٛ#Gg} Mo[/v?:U9`)Hd7^+ߩe?eU0 QVܥW;(i ,OAg"PoW+x "QI<&;@* b^@['lx64B#77wxE< G8TJW5golDb;|v#:"{>U5P3DzD <@G,Y'qfgc@G$u m5$?;{Δ(:y] R ;|Jţ2&y%@oVu頒vV7B%E?jtW8n\(A~5NO&ٹ*jtྸFbJS7\=.og",^O$dkт A@delaCkhơ- 9#?,oÒrEeu:.Otm枫;˛Ѓ>gV8Ų!ރa8“*]NS1/B$omSlPwᵵ}|ǀfa^,Ó$4?~Օ2$!vU8bSSs%*۝x&pnӎe0kB:Q9`f2.?t `B2nawƊ;1xs~+ t_4O\ /rDjp3l>Xٮ07S窌0Y8Q,Θ&Onq]ur;Y%9$]Ցmd*>:<(2H7`S 'I">u4> >^U$8E\v,b;cޏf!loG;y&v:+SLV:z2((]|O1~bejmJJ Y" HR^m+ Wy)bH_!9=V%L"cG/;ߓvfqHGIڶWL# yc̅K+u3F5&&F P97zOşh(mZIc͂Bdh01@ OTi#_:`2:0eմ:]@鬑Pd}<*2quyZ:8!f)ճ@ uVJ>>Ĵ;}jхXcGw?2CCiAovvȠIq3ɐx2bIiCQOGi dЎZ&4 I P`3\'שFF8]$y2_ᅬ8J:t'L8-%ZQew]S~MMP \5CpB!}[AwCY}@N|w"|jt+ӧO?'_$^޾~?JϾt6HsvdwkUcpp2`7څ;CP,\FߥEesC`9{t9ƘZN\I[WT&u~ݯz F8^|6ԗ׃2MsSCm٣|0T~'g"H8iS}|}DՀkaϧ?r8v쑕%ZR_~ Š r8'jQ8+|c|nf|~:#Lwܡ/W&pTsepQo+ :In#yC), h o~cKWO73Zށ:ś, ڇ7AMFS1Nt 2SFF[V2rZg"H 'x *:] oӭQ_aÊ38#^e mT(ҏ x'!+tDUr: \TYw]{T/ڝ{"&[Y}M}5t8@AKfAG|.7[s33ug8%h%n ry8̴eYL.|';xv> <޹F҇'hwT@[|#XӱRљ5xgg\/D5\ãނϻ3[֓}.x|#髮6Tc{1mqnrŭ.-T4_.kITb#1Tۙ٨NsG5k:VFdVD[׶ bﰯXKQFBCteڄ^WZI*oϢ=tWmgfgڎTAxn5:U/VE_FbCu4Z?\b0:sFU0&x6`TC┉q֮5;,ktc8FS轅x:6:UO)kN[3:0/wcT5Ϥս=pDmQqdl췼!ԯH5.v8>^HUB >Dv6yƸvݖKVJ[gZy.>cs@Yh{>Dxr$H3 nV2N}lVGq} ?FXGPXZ#1v Vm 6FvBȋ|[gj}|8+>`i)d ְ OPY=])SS}nfC̱@12k@g,2@>}m \7mG 1$&t? 8F䈉uQHPok^thMj _ G#&Yi.<;qXT#]\΄ڴJ2`Y2S B܄,+ӃX[.vRp_rt |P>߅ f<Ϊ԰zwwMhV@;+t.-ܡ)o{ody}}{ݷ}Y03ID+(Ɋ$+$v*T?S)H~\J%.T\%ʶ04E2$A v̾uOyϴ) fs-^-A>z=*Y^V$"20&"͖~-? ҵ(LS&&(J]@'=-$/Y"2 o0FR^14sG7兾6=#@ids &qyEse"¯cA9< 5JR5+0Y'"!}v鴱AzW$(a;q{l{qKPc0tuT]X 'PE8oZS%: Ư{&  QBmd.5q<縉r4ԉaL .>L ~M,so44;?FYy}]UfZլFJQs}-!}/`إT4p.Bi@/*/*PA] bPr|RJfﮃG!!Tl :pwuiX@#|lDFnݸA3+n ~wYtszs#|R.̿D> -(}p'Fo77ʞpep :263HHpG/;P/^fC.-YS9P';_(| 1H>sِ1E".JMDY>xjj*"+0>6g0O `dV&HCsө%*G@r-q7w5츟Q Bf*2*o:AG*d3 sU| 8ssw _^]b0F3@'CY($=cآ6C7?=1J8u!~3:O̔38x}<380n~)Ek@1Us *5piV.]&dѱr*=SKY`){cTA)Bzӏ'?gO r+L{Ò#/3r *lq쑼^Zm³7{̯(x4fzxa&[Ekd.`Ͻ J|{NY+*x! ]GJ!,j8<.L"X i39ܹHEip.ju0s!cMudmFs 29|,0naݾ?;!3 b'9;Z V@V |0e|c㭔Sm(7p>C~霎?D:68LVj;iH}ҳ_r=!2|X#W?ߤQV^ԁ:>3lϟ={;cEq\091/4NB|u>sʁՙ&m7kkЏ Ё-JVS%x=|U87c{ BZ_!B滸^8[5G Nn5ךhJV|7C|4w[wț_0 E$|AzpHS S0cl"KdN@u cY:+*Zx* d+CjFuzyfh:(͹2U7+leLYyytB 8C-/ϓ8wK98q qʑORR_Bi:ov@j~GFX 8`vQX%qwx@cwXQ2 '9ӊŒfdqkS"5;E2}&Zg|eqVRbYZ{$"7:8E2s 8('a=ྊ\N8sh҂MIj<sÜJ K3ڔ+ S@;D 9"08I&xPVV{E΋r,X,q:`BNcNڈKx/Lf{Rji8}iaeVq#;I G>S.| *֑qz570< ^VWf3ﴧm+u8:nH<,27#| Oe#S5 };|^ԙYztdRjnMs!uwxnQA&ǎMY]>l%L Lg#pU1u6eQncl2 ^dZ9 ˄nG~r 'dZ*4Z) 1^(w'!t"+Τ2^z7+-tޮi*?) cV8GOgΞ>V<&N {T/4Ҏi!L͵`zÞ{q:e owEn .A|I6z J)pܢ o y=b[scHUUjZ)/sRv}. |6f; 2"~(iL-N}& | VvGV-)Uaа"`_1fathpkYj5 `D6*lt!\MֵyCڤ-@W珁a᳁.UA?4xOEk%XGЕU8Qd΅;i95z<ĹX`3< 4=.A VYe}=;@5 |^X,9.Cv# mγ2^9m` s .v+iTTgr7*ŽH&PMs:[>]S-X/J$c7 A"""&A.fdB2 '!o0޴C^!2 5;;8a8i/] W׬0@ 5`Z&Nx∀UB:"8~Jf j$m^VJ= jC8T9$T Ew638|=@h߇W^N]ݼ lDΛ$w;uBK-  o`fy*p\ rS-F+sk0K2WŁ`F,iS$A"Y#$s PMs0ej C;mBсC*˃$l>\0q1%Z[4=1Ef /SdhF`!@$guv)UaD 5srfwB|VW0LQQwkXShPс0?K\Ώ l8FU`?I!< :`iX‘|/rpQh\:%^w^,fF^yԏV9¯B0/we9?ӱn)O\WgY.9|°d> c<1FqQh/4A|6w't+cRi*I?xwb,)Gv5rYO{)ۚ4ƙ= tiV6SYZkV V٘;+Zfg:Cu&"# 'W0XX68X};,?;#`'dl^G Fw';_b:ɕMȨ]E=񤍶BRWq/P71fG:Ǽ4N${'CxfٮpC`4e}=8/ϰ$ZH6ʟsq _?9Qܼ;,AGuliA~"n<5UILs: cDeH~,GWe?:%+o^]_ a\AT/?|ggnoI㊷9$) `sx4,;W-e]gT贰J5-~&ǹDi}/ çFobdEAۈad43dcO4?& X>!qFgLLg盝?%^#x:gh?|E+sqv] k@!f0EiztIZ\)' n$4[ʉ~'_SIxg,p ߕn>c1d;p{pxtH\@8ߣ]0"e)eG % tFh_0K=*#m :mN{NS1{,=p{ }q|IN>B2B.`q: K9o8:M_pz${ns/~y{<48vw=|ksqekҥŧ 6Nuk~X1k.eslg6[B#nz8 xCmOa?:#9u.\ge1?oF>bF\ށ<{awi3Wa/gkr7qIX?B`ߘb6;g>9mrYkq2fjk2 Tޱ _gfNzǎ9v;fmV udgzly#m| wnp;΃3?󍟏ps,w5{fg"!󬙽t3 9BZ52FȘ~xU&$2CwaA*u,KY0Lr`UĀ2:Ϸ8<@m<P@w9NNW-:)NBYŶHw[al?k>` ɞg}L2PXѸAe*L;صеIV6{ũ:Axhh2x!C3qIJ3؀Z@J/ԩ̦!itmC tD8:d)wFp:rh0M2]n* jgݮfᬓ6R1Dg+t-SާoYXBCu7-VrYt~NTa9K[) $uDVYfOfh/RD'aZR5]\J_H8!ԠCe7{y7GYʽ+tg묟%q fnΐD[V2NUy3SO]HnI_|2e[I|%+H ;wU䉦;sJp|xf=f({ V[$}&*soYi7Xa·f X_I3hoE>#rpr'.j9#=ԇЩS!g/؅P0|{;#@0A7ֶHR%*Ù1  ~fiP!*J!| 9JZ5E]%Dt_a T¿2A0f1q3cf{׽x$փ|0ɟ'M -QfqlX i<:Ai%S2HD~'1O{ e0pJ{D[FbH@=Y.$BYY GJ@3O҄FC1*S\ykJ8JV©O3, Є]yJ#f0yTĥ9KWvV)k?AKF7֨&ggܺ}P ,Z`VeHYtH=0+S QUDž pG,P2 ЅK^JVykFzs=duVj@ WJi"v 7ȽεiL po+8~ T-C2RѠ#fKBpFLrֺL QPҰ.MSaf` `FT$k)doȸ`N8# Mj\ eAel%L!@4  Ot( }og["\7ԉf7O~za/ί믿f<`9`8HO 5(ЦC<`x{ }e3#:8s>a0(̸(mmSS+]Quš?kLL }B7Ө\rw2-0{`:9v 0`h͓_>&fn~j0f#Yk:YAe9Ng'mqKX/AԲ1&2mAC^ LyA?B.Cgϸ@aC)5f | r_B㤆__̪X4 ʡcK8KVᾚʵiCN13CN{e3|o<8Oaf}V=Xes p1f6K=`a#'>ܒvSpa1p^f vcKzmmmo=/ޘUGiE>^a5@#Ș2{_{-My~ /O[WgHS頋zjʔjo%iq 2tЅAԬ݋YKBp|߭p,8%EUerxe#+#ȿf1%l_mfPGUx<'][]&_f!3cX^s:U3>%r=A^ӀՀQP&1Y+{ww&v>Dq;6U? ;Q"qXGX}ȕy[Kuʋ]T:9pQ~.Б8o8Hvp\֨S,L-·}y09y$N3` ;aO^|9\ '~. $FneEM~aa6Zl^=r`5*י8WXr^,p38C#?y^=EyNs1;e{?Oy.GT>#Ў͍=QN9|>ָ! ,'<͍1|#.d;g:-0GˠQev8wg>\WVle[J9y, ctӂL L,F&+G'Cӿ"{7 }E BK7{8xl ֠WOF&} ]%`NҎ NhQԡA\sQɿ9XUȞbٔȖOTi2x-Dl`ЁsA&υL>NeT[l||e%p{ZiG#Ǿ],aꗶB-֠C9p7a-8[IV"hoP@W] p!t>tmU`2Ւl*IT22{_4,-2Gn#'ORe D.9^_op$KҮ 1@!D:Cc~VȬzbb{Y07ѫ?ua[+0JD38bl Ph F6v Й.p J  Dٲ 7boe_4NH}͍LI(ɳJ~w "%!X#:1\%.~+F k;>;޿v@Xc p2Q#"n!(4$b) Kr PWSULK `  YdBg% Q35I*E:`YA̩gt[:TI3i{4LS >&n‹y;юa$8.T/m0Y ,/8Bi k8,4l0gn+oU&rRzӈ #+^H*ہ#$NP21~'=ē`UIiIiNeATCe u#xZ &ydEW_M/ЖAEqѕSO`ʣ$ g}dlQ Zr,m&6a9PdciȆA@'xMYǛ#g$h4UWEΥ|e d J^kw2Z6yBqīQ]4Q?(;(SWdoiU0J%~ƀ亽4J>S0uiJ۩Fe(澱VA[CY־\Hb1j+e&4+ Ӣkֶ0*e3OՀ渏NXAw܂^E{#LZV%7 i >ӷru/<_뿚>M=v~=MxʕQaGܐn.h`wMnBFWլ#OYw4$q-?:g,Ey ˠRnTA 9;yX9:폋HyqH@IDATwCw,U1<>jdjziyY0#G~De"۟q38}Kf\|eZg7iF hKlQwIUw__sꭙaL@ UVq̝459.صڻOޢe*,yaxVX7ΨdY`01^J(nT@s,XckgD~X!?'>Af\{T 7flP%uCW[>(?r__5Ρm4G+u5SV逗t;WyaK]q Yf^| MIR`<1ypɴ./1Ѻ |c3Ɩrr| xB6=r2٫ \X%~wU~ y0_0Rm"e,ErM8yP$!8eE85>+zŞUuf9hy*Mj[D$ )cE`p#,,xS'30<`%eߩϫbg$?lE쁓U^JWC%9>_ u['CyEY!;s=.^ND7I3M]m"py79=6rq['O Gtyf 4>:O__;maT:}DziS[rD?Ѐ9ZP<tD4RcpR|8(H&oqs g" tf'YMj7snO=-ɧ[F .N1`m|H7.v59>}7|pvf{/뫱>6pqs XpEZB64Y"YRFiB=١T3P{[M2JZ^@ϡ9iJKzft %t0#^2ǃN$dVI46 1E ⱺ4@Y1CU ̼(=6!>vA*3/Vwqs0H[O[7Sڬ.ooA f/:?;;r"kivsTvn -)ZB47TA V{*NZ6kWӕdmߕ?>y?tܩ'ùge2}A8ҷH& ;&In# ڬNH/]6+-c@g+~D/a&. io{=q%;Y+ l _Kp=ITϑY39W1\I8QPM\x@c}ww(7/ں(4ؿ}qS@QM}ε \vdsuYuz##ʓ ȃ `.TuO2@i86wq 艾<2U+*+%Jh aзQZc)§hIyh _ J;|/Go*"RC! O-ukv6?9AfM^B(@&"z`R/{DPe~ȁ@!Hb `o,8FBgOU!Tk]Lad}?@5_B%Kye'ϥz<36٩ؗpZ#K/\l'}#ܻ>;{q!z ,B6Kɒ-AYG>0   DrהCTj66*2 B̌,dX>h8L9FSaDj55>>M@eY>e!9{3Ƞͥb0h44H;@5_zO?R+ߴ@d("\Ef]aZr4}$(wFVTZB^`K ԈWS:D?ݛ:)~A+91<m’J@B VOfl2 )kD^q!AyW#u>硱F\E zO~%jOOo0uC1SQ5Dj9D r|.W@7hjtt4=`>_};dKd1 wU/2.89p|$=O?W<&Goss!)?KD9sν46z c0=+ |»Y$ZlO?LX7ȵfyl w m33Dic m 'Ә2->JSo#2*{i*,y쯎-Kw,33ggKv3Csr(wR [8̨ނh -1Ht7r ӈ6g95i`0$2uIrk׹:mi 2G(v|3:߮9ivmMdVOœ=̔3'a$Cy C.d`k,,QdU4 _^4O[}O8})5u]=ōkiexS'M2{Ho;zmOԣj@),P® Bs(' 5Rx6j2Ӿ@P%[A5_-aLv×pq{8 +,4}NFöÕ"{ L,> rsg}يWI8WYi Ќ`׌'4SFNwwTGĸ޺3"/BO}޳^ٙ9 D iL u1$0|u6tF4SS&W!*#wl70&sYkd1--)^!tvCg B(h8؃ aQbdK44(@×jPGuZTI 1;]7BN@>G)dJ't[ɚ~aՃ?'LǏ; gF)~kuQ%!;8c <=s[})S!wJ y0tWWv'iT )7ceGMs``85o5w8ܪLc 8''=n4| 6t#֝&gbxsh_}j=@xM2چ!tIәn[>eI]=m1*e>[öq\J3 D ս.r-S!L1M:fIGVcQ#m*[٬Щ?[ѩv/SA:ښ <_oA6""?֡|S=cA4cnS.Fd ,AizAtp708ao&(0fAdEyW>u\> ĻČtG4֖thUAZp4F8jW]G~4TGZEzIbر0ʿv:8c}ښNb&HhGJ~ HR8t2TB^+/ xT3.]x|ZH)k|r@CȽ8KΟQϼəu kk]#m7ס iӾR -.k_Aڦ}gq _#yp{Qݍ9Zp[+8F] a8|sr6ũN u e`=F/(˜9Z|WBU*P{^ƶ{SM>~k{T#cOS8y裡f-3Q/q b"zTqH\[ÞU 9-f)g<83 \߳.,\:aYe  6\g:#Y gf}# YE-=PLoDˏIE86甗MRn-mdžScww$ ,EdT$ɩ$ģ=s22U,9u2I};B8 i}#ܚK/t%ƨc{א5*SЦH);0ЗT#{[ ֱoy{`O(P^ɧ+̜[ĠJ)J I_}!Z%f&5SrcC̡@C _{E"v1aRCo<4M#""e)G?umyrJIK;YUOa;ͲnTU&"Rk 0ыfUa[OghHQ޸)cNՈV KL~ +(u4Oׯ1'b]Z'YryeKl+v b~-ʞ;WHq^M,^YiK蟝%qt9,^ Pq˂9 bD=!. v/3C{ewQrZo|T=0^75JAg|cVJwէn 8|qnRCQB!]_ʘoeQ  .x@qhYdt m73K9J|97X^mQ?Gh@=wUU(ݾA Cy -R/kD;OcX^p%hOl?Zezg9#OV̙|g,ˌ9ra+9EK`B`8 :.8M[L bf*:ᱱ E1+aIa%>ʰN`3vRNJ >`C* e7ƙ.M\.LLu1Gl;0 ope4̸01>Ϝ(c* ##E<-̅Uo^-{!vC{ ibu-a7 `)IS{+ҿ\4d|D>N5[V aџo'φÁdZX&q3+;O]wv ǡS~-K0/ 7vпgVQ|b aqYivvZ_lH FTbg, TV c1M!ʹZ4L{@m nm-3^s="3<>< Z;uJ㵶cXٙ awAg/F(uįXk4*.k8>۬θ Cko<1yy9օk7qܶ@TVA;YAjJ#dV"}S1B 2vȑahz xeE}=?u2汈ʟq-!+ԑ*]S(ukV,Vż!{I65L̓˜4<&Cgr sohZHegMM5A;{zޅZo^bJ>@{w8MKN[YX{A]BKjrǠI{U # .ȜZ)ݳCz>gWF|9E: f9Z[eGy6)URy;6&3]z5s/ >,ږpT+6@V)V=X&E]Yݠ97N_-xǴ6^JL4P-ZH* TW64Md$hp lG5 amUjud9SW5ȧ56gQ*PR Zrx@!sPxRq2elfXy k᝕ȘdV:1:! 6ك=E/ |p)#K͸/4X\WD'^ˁ[I̞cWO 4!+m $``!7o,mDdF"Ebs,D/8hp<x^x㬬 \C PWoVt>jO2W-`ouS+rp܏ zՓ)8sbK4:d}O>:>6Up{e{ x{1:6&@M*{Bڨ8F`VʍWfb ]Cԋ-^^k )x6Ј&| }&BKԧ3q,~)eZ؍וΝ?>ԓi?۠ս4^I] :}ÌuV$ z%IŹT&/˗ng74QLz#ǑDƧk;o٨/g_Og>u'GRoJq=NܑHBj#Y=K-_z57._Xу2p|(LC Q=!o3 WAk6.;r>Jեts_1L2*K+FH97[=a,@0 etTj\T@F:e?KQ p=JjWq J{+Oӷ%Sa^ J2 [ԏ#p!;va;|u'H{93Z{^ SC7\4P[ÉEq;gKaKZEBEBY>8[@c1yY0IF? Q mʵʧV%1 "@͐b?tPFh {sl㈘swu[ᡑLtK,ņÑȜ X~W~[8}(#LFewwo*/?d~=?yB^>dOy v 7ʎ(7$y]dAt;٩/]B(_ߠ9*U1⿒%jÌF0av~8"- |LtE21bo!c 28ut4u!ǰ"UGFvK# c2?$zyu+ilV%Ș%BFE8ƘFRF ط0 u,k~z!]rg5ZAC"[[g 2=C9i^1wq8 L630IxQ@F]qEp%^WgHx49b&%H1f;jTF԰as±n._1(~/>c&|RvUd.BZ[0Maa^bWu z|8v)ǩ.JU|ilay()G% n߿GSyE:5 `<> juC'^C7􉠙I/2OLлrU8-OF0c0jF֩NǠL{" 35)ll"]<}s Qou'G?]%\2e sC#Y5e=61}Pt<8s3a̽K0n с=5P331As:ݘnp:oa 6Rg}1oƺu{4VyߥTa aV~ʘ]LNL_8o\<Φeqֲ T"X_esɞV[na f22rgl@BQKkG"7^fͿ U{𓎘zΒ;n˯_HkKcZgi|=մ U^.v1Wvgځn3ڲFo@6LgGfW|Wert4dYΕ)o>m[ Xeqv! ]CF_c/#e6+OnRBFҝ.r8=>A&2?rd8bqPo@:7lTeβc,w&)}N 6x:6@+V0GWF #ѝ P1@%o{Llpr;UdqzA"iM85}BPgؓRZ&; B4Cܪ'Ms`I pi0&uW[ kMMO!]x+TVYJ5 5;1he ݟ>O}7{{8"[K[}׶k啅t2<:h*d~*NFe$1WDqS ]T;,P b p/dNk;`k渑;8G4P*I>,P/2iGQ>c+D= ƕi [QUw)GR򬊇[u }ؕt;"؁*h %|y~prz?Ob:qYSVfN?лǘ7<ȑ^ܻ=9 G"@*O"x$ 2 \wN,igV(BgDʱV g>g©¾4ܦ HRNQmI_yYkAUTKDɖqJ8q lV)a=29!O\ :$>!\bs=hZvg3H ξ6d,;p;IB:;3\MGy}\-ñVLPd7RK?>Q 8F@'~RΝR/?aC Wwjc!)Ƽ_ǀ;Hvdq^*K&]BVcH#yx펪)_#[r>r39rй#(L'î@x7߹]*WfSK.\M=f[XwCeg@lű A߬#pAB8mdG*07kIR [pWr<'zOXb}⬟|#pLObZ%N˿ L 8QAdAXB>ck!jS2 wrReR?34AcǪNq |'/?k$Gy<]Cx2<1p?Ǡ'BiVEM8?NyB*VR=_> ±Hu9ᦱ㬉LJdbd4K1d3Ӑo仍%NNYr '8ZnA?\%?Lg<Z6)|l瘆Vf~#Rk60$бPakˁӈ/w= T7T85j(k=r^X9ggCXQB⨤GrO,%JBRt8#F n8JA{Q6Z!:#4% :APxPK{ B'j#bx03+las-x/AüNZdPm9YFNq=28@ (" (ۡ-ߚ ̳ek=z8約c2iVojMGhk4!C6sdq 7 쬭؝:C5UTa/@Ü82;,Q ӭ[\38ŧeKf,sv:\ܤ< Q5^nd8[6\RJkA_Y±kDJ[f*u?|y=$<<8>'--&f\VVɂG|eFsab"\iY\]c{@-[3f'Sϙ; H0A2:t hJCyrGZm+ p+! R?- kgʪ9珎MuW8NEZ1o5{K׋ W^GNISD |A l83GkR!PP{ ڃp&d`аHyȇW:Y,2{n?@W"꜓`8n ӱebYz) ky2Y1-+7 VNT?\ ( ]đЗ2z˹X!7\x0gu }5ap:?we D'|]la33XB46(U3v:IY j)BA6v@څ0ٺA0p{3`d<+H_ ֹ =d ci:d*V7R̄233SZF e:mÖzn@G:mfhII?TkkNYX%! a-KӟJ«>?dkiOSYM=wny#u5«8*geAЗM| JXk5mfW<$V4tw 7//~3PGzQn&:^uI}σK__… 8ռ&Ge hA:k 'y`ޞe^:ZO'`! Htjsω?sK[ڠ8ˑ+I"Bt / 6c2.0pm"iu^+2ÙуjرCj ] a.>T@IDAT*dd1:l+l:G9}(mk~̡%O)Y]|4,O(GX9^d>~W,@ ݂cnĹBopA*+:#~ͭ~"BOoUvKWFy$Ql# `KPFpy>]cA|y/{lVu<]C6RBa/ DA^wD" sD)l J,=Gq#}%:}^Jw 4Rx fq*}] KpF~ -w%|sAbFl`_eߌ b lF ~[9FYR􃏤jtF)N78J._Ht2VMɧ29oBǟuߏ>i=V?Qߜ8;J$N.ʷ !P9Ad |tS1ub2H4TSgۃx*.4<{ O{ow{W`hJfX㱝?2JL28d&(HF5rl5Z D! p 7yA@4^򰻿>S.BdTqi(7rTG@B}-ZVH123AY]8Az ` eBV„Pt]Oݩ7ag x;nhe>'q~K[ *CxB]`'f ?=Tٕ,QE3lb0d> r-(g: -.WJ3*~I! ? @s%y^>K`BnQ>3\EXg}2Y8d6tn,Ћ# ϰ;Ur?oĥl$(X6s*O[?7wM_m!i!1$/b] CA@#.%[Z>tp]*eQF_;ƿ'ƵŁ47c[T-nAjhӠ, l;4V\V㐽225N#Wurz;Ky_v<35"e1mQ ˌ ϋf@8]OS=taj"\3RЩۭay?'aĩd`B#j\atލu8WOX>JL`5!幧6kϬday/|+11L trž_=li k[Pզphjj:hB㰖|ڵɞ*}hpA7BkLEUx{:7yX h O!\fVX.( +,`/#c\JV "_W. a]P@_n[8t;Ǻ`/4]^5x6yT|8TM~^N\9Tį aڻ,U>U:fp#TPLmC/k^T?6)H+"3 }Р?e|<*TV T, p {TX=Ѓ&"bUvd:O=Z!?(]^>Z(Dže[8o9m@+ʶz!BQm^HilVmK[Si23ܥjH)B(Ri+qVz*ӭm[FD#Jw[a|DZEG[礎P/#T +8;zYY#DڒW5gmA90(H(En+F3 Er1p@=&c e:,r » F2d|t;u PM*GC26<}LPv8]C$tX24P笼 ."zvomTg%>ip-9t;{Yt־:Et\JeVϹ@ *H)=3G.nrfU۽ehcp /|S/[~߀Q=SyZ2ǝ3vw? +PF{epqlR€busL+$Y<6ܧ~=GA_ 2ԠYg>%!,X  0Gc0Aq렫R62h gLV?εJpX ˜!҈86&6ˉE/rcuvDUd&ի_쯾] z:G{u.N |F5NT^//O?HzT u5k#7NEMO!A&е #_4< Zeb+n6  @iM}T6 }&hdoJ{K/]HO>sJO|齏L}K {{.9=~OzԂNSրdq}=[$&tX L[7~7zz:8chWoGPgSErH>T'ryoAh{:34q?xV+T([S_HԷ,n; 6=8ܠ(~>Y:M{ i.s܄/my?VAg+MB9 /F#P1Wr ZIY%  xD%NpVI0_A/r+dضky>$Eiz`yqqZbE[BD*wF=B  v2%#ڊ)9N"ʯD*Kf,Qp騬];Q F Fsa_ gn-#BN`X# |9>K)"?ʹCd0D`"g0S&o0'?chh ,p!-D&05)njLKFӨ[{2Ɔ>:jd+Q.y(aT1yZ^YD`lb/ rsfc<3|W彯Lqhu׻nok۠L6%Ȗ)$Oc/*92^>rop@te/۹u*N:#kXwQSJ\){{nMo]`EqCD`( c;̆4M녤JcMa*:X,U>Pא4>]̝A2#ԯ3 9=CL2Z"S\ʲK8j냿Z˹{UL V|,g';M";2R(k(6yñs[&#cԱ$e&Z>#oEYA&1fkxWQ '8{탖vVnȲ'qJr|A_]%ةGSֵ@AuliWu8-c]9 {>~7 >z(h΋H7.^JS#dhAg&g-S/pEiRegO/Stݍ!|N ?2ْa#F2 A, J|W[|Nf`Ek>UPl aPSK HA}W7m={Ov;qb_K/H}psWЈ25WR FQz&G z/Q lMXf}_ kc_ȒflP[PX0q4J'Ml88ا]J1ho< /q̀WG쳰瞕@P&g#+r1 E)py pLzwug.n&60bֹP&}q^wˆ#B8;Ͻ0:*p&^o'_OCs;}Y)1΀Osq G4rkSCJtzC*oh'ʨ!}Ҝļ]AZ?u+Fࣿ/w υY4RQJE2 kjI*g%Ql$qS B'ns|\zKF| (Xϥ McwM]dcq`md2mVI8r5~|Xoe5lBrYn" GY\c c2b ,;/l'b@9dRS"IaDV2ϠDxuq.fr8hSVجEzA8ԏx2gc[$s:F`͔UU[5h\hNEYRױG%l‰g2:~=Cٯ:f̬{W#svޞ<:խ&s(IϟVcD:6~*++|/S6$-/ǩ%W&-{Ƈf `#\9`LX8xr̳5 އ|dg^DV#h'ma wK yGiJM bʙ1FyFTrRw~Qgȴ}^=f+=7 ;&!/D];߻t ^bcBvett6azrh[M%GQޱ0AckVju(C9„#un=6dN:-B}&wz;rDIF[srJ{{ ,.('YSq,\"EEGYHNNkoq=~u*w(;vK XΪfA+yrm)xCyj5|PaחrcZV1V&dHd0>n~-hv)m [{W)ejKgBWrh&F{O*r6I2šK D]5KV]$p|͢41ZzR+mk6;R#` : F΀_])YVP6W,f>A8 |>}l_HM'Rw{ @_?z;_'iӚi>7цxokzOAuzi`Lp#VRSJ$},Mq Gf-a;JSߊr)CFt/Fv/¶r-Φ>p<-g~R|ASTߙ>&g3ϪӇ>wNg F߁զGW}L_ft 0 ЄQ*{po錠 /qalEJ}ipvASV|`](" -OH*CRNUh2l!CQāmq<=3#Ʉ](Qoµ>BY6i!?R&+(R6 ҂.긗fd;f䔰mAVXAe/v#ΖyIe_ ДuYs3RBٟ"XUE#eHJBQwleBUP ` @ZBψL'%2he#t~I8X@5Dh4K}D=aL)VWɜ D((0 d 1[}3>k En |dJXP fU !Æ[c!#tn_BaI]g/ s D@ G$z2UE"¼#P!dy*?<'HׯJy א;u1y3D&5įlLq1} FVVQ1+\A@ Ӟk/}賻x^Cq7Je*-uX(FS8K.Q1JkD5B5-! ` / 3^S#B3[]rnaeYf JFUzH+.3@w;[t陯}u' 5BF/Ew kE\C0{*Az\q. %vY&Ce@7s+'}>QLA;@eV"a:v_ :{=PɤcC\gٗ?VoxO05%k1 TlBШCeހU .uxZ-rَ㣿o3746a|\Bg\sqeZeٌHe sQQ],,o;::~3 <ʮaXce2\͌`V򫃱DٖhҒ2بUFQ~磠4i$ V|(}S9Exx^ >%j͐b.m1G>&\(d+p oFo]cK\gܯ9N$ {/:{urf3W2P$+& yE%*&`9o3uK\BV J8v }>E66Jנ(" үr!MЏriF=u# .:Qx(KO>-"_J]80p@XgO7=hxϰˌz|[: X2N*R4|F/y{!#"u30Xϩw2m#)b)_I/_JOx(6F4mJ8LEc]:eCC@>ɴ- ]FHH:~QULQ>v΁I7]X2Ѕc2ba[K:5^DRE8#y:RI aAG( 튻O5$C 3:S̺*XmUB:6WigNY:#CUWa_Dv[yYȁKט$vEJP1gM<_u- ttح*⨥}2KMذ* 2gZ~QKog4"ݒmvlOP>ʏty&^#[߅kePe,NNxh uBc?Gܶα2^& >hw4TY~*lX|A2K%VPEԳFK Y`<^Y;lζئ)" 8U8 F ,$`G#CkH|VoktUlP/l's3S)&62Vo><g2ܣ`S+a@9<yo.5NriLs~c֦{úűWSZ$vo^t 2%)_^E0jlr`j況VÜsowVAď1GD% {JF+\Z񰀾λRp@RFa=+BlX6ȬxP2(3w~jVN (D>!dseGp|yt9HGWfwFdZH3XG.XGh_.˞c{>T+'l K$m4jip{{y,XQ|=@R ڹ8!we觴^D-Џ89d{Vf!Θ&NӑN ɔVxn.堸='>>5E%Ac>9_FE2ֱB` _ZƦ"֖y8yI8hmVҎr=`!.!o r!i*UVX(~1"{XY#8M`n/cʰ]{z ][~,u44MRvP-.OV-ZhIA[2ӗi@{!Z5#w7U`tby.5er|r)Um{;D%O᯿q%U5s ۦC0*<}~2MӖ#7?IW^>~we')=N WY%x}?羑)C?ۋi"FOˍ~Zn 8 ^k8ՖO%@U# pg-_k "mİL5qut}( o&;P_dS!ExWW#40Of>˺ 50H&=H~؅>9@Gӎ+rAUW^#|]+qo[|i@VwiXDEںvAh6i}lr Ce;dKhj>CNN3Y. OY ^_gA®B=!&`\gFD2ʮ4X r!0&2d CQAEEYA rP:@V6 _e 0C=/A{O',:$,r7@ ʢM"B ;d4Lh(݋s81bIȋYÃ1Hr0DՒkԤ˨-*FAҗTоv}8=r k6?ul:hsL_?x(2/{""zZ9j tf11/p1IIW@m!"m~IVdՉTBa-"#UguI}WØ}ƕH0P;VHcDR@: l%z2g eMgM"e&Df/]Dsme?X{e8ʓj\&!T1yщ9,GDVIB|j;XĻ"^oH̗rkFhk'Id0n 42#}3  K\ǺD. =:A(o\\{_,Ci<9xnay?g߄?}k'yltQ㙓 &*6dDd/?Jb$SGDj4Naڲ*:q<̟z82<?c{8¯PD\~&G n@h߲t/s*k`SVNp,S%n#wb*5t $NP8yE F+ xчt #\do:yyd  w^餲e/_@8'wzF7F# 3@Po<]O6o.|>λ]:ҷ^:\OIQ1R:Fo fGiI]$Up 9>tUq4 . _A'(OMU+%^~Ÿk7*kWOC/8p)>{mf Wm/Eq?rxNmjκH{(YƈW^p;x啴85Bhp1pѬ;N[>KߺrbCG?ƞ i;o Y@ ]P >tDШW!0=‘pF*C}> P5* \| MW1C.n#d.c9kedk\`| _AßL&(4htNԉmwA`/'0 c1־5TϷ[t 3Tw exao^4әo8kWUV1Jdxk3k6N$je:8c.aǨWGfkή[Ö1:{2auvO晸FXY(ss-=^"hjzx?: ^:1ZᣩQ,PYȬY_l>P .I,FEZaz].yn`ﭷoȲ+sonn$Yk!PZ@"P-+UU-B?p {4n+S1\;MF`[m nLQ3t(Sar] cŎ 'JtN׵Y b#Ka6=m mR구ه23yHStU 8 0#yVrfYiT&uD".rl-tͺf!$_su2v '%{K17vIe>h%d-]SѵZ V~L¤Ɉ]Exn!foZ@ZZvHwwY ?b?q925o+҇=n1ձ%Jζ0>$_zU`ӟ$e8ItY+ W']F8c0WzE95#F^{9O[;8aXKl2@q##df(E82%ؠe.\C6͋uMˋٸH2Y.m>sT-]ֵ /os9Fm$0SR=iru[ tMnq1پp38}N'M?DҖbդ:Zh`"!Y #)g;fes}T:;!#ŵ0Ӎb\pP}W<8dpP΄y@9Yi =~'i6/>ep:wQ RU^QIDgЩz3>E/Jo8|J))Ky{^&8`}y>G e&Cӑ'S1xw}h0h^a!J5f[:_K4o?LeY6nԊN gӻ;!?ے~ ɗ.wiL9et!iU8#t PYmkSWS%8epRc/89V?̹d/,CS RHA<( CzRJ$~L=Tidڻͦ'Τ_F?~_Q)uH=p KicS~YfS `6q॑XU;[S\)C |/a\j2McC{+Q5$M#HıMfo \Er.D?-q/-M&eńی//CsHGl3B :#s"/[TE֋sVFԑů\fbyRΊӄ-઴<2*sMVflU+ 22.tph-{(t*ʥ sMQ>Q[&fhDzqi& 0'rzmnmd;5Pp".Sƒ!i>ZT˭WFRQ]&d.Dq )Dw}U2fXc;==,%Nj$̿BYa2Ef+V`Ѓmr4V@ MP1=gSN{aA/2j'ђn'A)uy15hcX$bV> w4W^cy}G%eT$smd:Uu_3`M2ʔ uuu9[xŠB.DSۊ4Opdx=~4sF<w^}:`mYM,sDVWTcQMޥ,Ĭ8@O<3tu+WHXZ8IJ8m? –YQ?ud G d[n5@XQ``|SZA{W2Ƹ W>QN^z̖>څ;+.SH4?8庱!ix4DmuHt7 L5A`ϭ~x2QTngAƞ1Py@ uzkHnKQ|#|=:1ᚨag&с.@ I\[ȉ¤ Ѧ:M/„:7iEߠ̽=*`G׎}f}ly0o~˭``t_0.53* ԨZUSlNjI1t숷L2 G&.Q<+ϥ;svOϚ__Gᶷ84m:f N%K`h(ISM0T./]&V[bGiE4u,-*#TPp^3S#㶷d>:]FfmByuDJwVu?w>>oL_{ DS+}%}I>~xK־BЦ)>Kc[#k8.ڏnC[+ eO>`cy>Ck% -,!aF8ξgVV)2$AЦEP?20Ω-D>)b4ܷ[{c+3;WCѡݰ @ݰȡ@Jڤ%iS+<@Ϝ{+UcJ|f8mqU^Z]M`/&`n% d!AtBVZѣxMm#E;o*/ZȬ&ʧeiY{=2Fy r*ptP4q^[{S%RV1蔞Ig5D: 7*8RViʼnRQC2KF(㲜 T4*2E~'!ߦdBk?Qfτ_8Yuwxmߢp8*hCYCmkz bӂ1]Kr9VC8Y} C3>(*ҮIaZ^y O1J"_tK[HyRP>w}3 P~k%gt.4'G9<BIA*9edMZ>"˷ V16>MatB9p`iaFw TA8-ܣ\gumF "U\K̩uNv֨5 sU+9k7kƝoDitFv8LN>fpP<Ø^EaN,}a38 I VZg8a{}ԁ8tZ&`c1g 8tNwgVPB9C9O/\0t!^ SXxEdG=|fU`wz^4ۿ2r+  + YBVYݼjKRc~l* ֫.̥+G g1kvѡpmu?cF0&a5ggtPC;3;f_ev`j? |a?- kff*_3:':,1Q#`頎@}蠼dtbf^s/kԱ~hܟz BǗ&S"vDyoq9r?f ZU<]I_FҁtM*$B@o=#"59 A38&SL>j!NG8/}&pٽ^m+SUU#TaMtOyK7pOM$48 tr@Gv-A;7)tOWiY)Hklg)ԉ{z*rdj-կ̧#T"X`s%=|y8Ltփ3mDT>OAۘ>_N|J?G҃ӳitj9=gO;?6* L9WX_.V6a3t␃Jjm@džg0kV6H vv5aBlgvdmu:.x)U*$}83і:&oђ1mGE]}`8yk(`f0H/H똏:𦎻N@&dO✰H%*-J D&3Ġd±NA:Зv4Bt]ұ1-rHigUahK2p)w^XNE1)ފ@<y0 D5. y#qT)U0Bi~vRƤٌ+ 70M5,cq,d7Y"mȉi'<*NQ[k'$%jyahnL}y"qåY8;&(AC'j-"iU@nEñhh%’ *(kw=ц [&*a`(,HNǸ(CqQ 2k }-ǟ:-a?)G ?ܐ[{cxg}YOlU8m_;^ܳۼg߽UԈg)t068}vDV<}vؤX[C9KmʍNAa@8^5IʁJ(7S'mK#k;cZ(H|,a{ YbɈHkUV|}$5 An3/YNWvǺ ~ؙ}%׃O?Oh}-!8K=8^{=oo@7'B/-64zk%ej@ _亮|_:zX^~_ KD(1nJUtJmjv8D!:gW e2[YEU>RL5â 썤G\7i\| LF$pV?:9T@?g(ڒJ(9%BɅ~̣Ҭfd? zPq.;IO# 7BoЮ>DI63;pfh=Yȕ[Te;>`2C85gAkl)t㐓9FiG[ Q EG(eOtHOFdYbʖ^W2sdNkdu2tEْ' ҭk=?? w$^J5л]o}j0z9 hY _xE,OF ǎH5|Z5ju9ݚ)A{/MDraZL;Ց6ҧnJӳg4vG> pʞGamAA6~:t+K"t a "zCgn$n46(E =C * #1WT= 8s`\zPzϦ_~ r7yTN 3۞ߖqx-RIՑ%3/45 KOa@^0;G#`w5ռ`<4A~ڡIy54A4]Y~2[h`{:WX랫 "w Fa\Nq¯R#.&(3JmЂiW itIW^& x>-&D_ysݑ[J&i]^`UCv72{eK,m1Kט]$"t.;vTW9HOTȫl2[&jc2| CPY@@Ut.nb>rFYU 5 AӮ83$a;UL94J5FZn[sFfX=<}#/@&iG/Mg fZdisA`?w!뜧%Y$5Hަ09ʬSRZOPHǑ{(7}.-({ί~i,S%pdV`.dZS9 2uX*I[]+U?wNs3Wikr4t2:7щ9,@aE}G1 >Ifij5 fP&Zݯʓ  ý8tց3ivzeNF\Л<"f"s9XB7OgHTVTŏKQ` YD9w'-qnq>$@b$pYJV3mCaсD%7i{جЕa ]}* Y;7Hm\2x h"D ;\ASh(Bнo+&'b&m )NUUr߰3w¿pFh.L߄1t`"׎N+`.dCAd+~n`&- |l(oKT>˾j5 KHa gf硵dW0Ou"q:\ɵS ߖ 'L^&m*|Ce5: sT⌵>þ׊ںCҸNCa|Vd->.= ĕyJ?ܕn>s1=OP4;Oyh]l @{ URǜ.nG0V.̰)p }A)%}癣gv6[y Ϟ]}eg:{M=Z"O,@k,Jw^n#}n*+ oH C ـ5p~gq(L`hKk@tJ{WRڱnׇgh! :] >8u2g?я~I5~tu,_A,EZ ץI2 䝼g,aP>v+H |j::sLh7 L)4%uU۬gJȪ^kWkB=&3*e"l%wpN{xo߱F[+@*D5>7#QN~1%h*~Pۊ˳PKmU&[YE“UN34>ʡ5awL`ÊGTR10{uگR Nsqc$a| osg»aLI֊"j "k4LDlluAYJ_zUq{M7n^݈09[ӓ}(G8T*_P]N8<"њqDD_)"MD81f͔ #cpQhن H\ODRhxaeS z J@%Q$Zy}o$nfhЉC@g2e^GhZ"O5h 0ijZ7m!(!dS`2.K\ys7FAhqq(a֍vX es^4Xڷ{-&d~Ͻן`f's_]&h{=︎{\ޛ핟{,"{画7lWx5W[IN߾r= ¶ph#G"[,O<0i*LpCSQF*iMDN^N=FzH)Zx*kB**2tc[|NX2u%h#gT8ď"b 53K-"[Z ek"u0lg8W,?J?X:}ug r?sc\sn[a?ysS8x?|}>Aaq6MɣΥ\kǸ_9F\hN N5"Fm伙Qao 8SqdPMı;ߛ>K$B.;Od8XA xK^*9^opa^#p^8)Hˋ q[ZGP(޻*2 ݼBRyBIVR9 >zzz0Ԅ_ȑ;ݽ1sZN[ODl,YJ)tA߳^c.ןG!=;qVt=ؤaWQmi1(Q2xO%e [ 3z8 죫23k%VA9=wu)zdk|!Uc MtD W⯰󽣿mS `)v0TRAG^e2ʉGSwz`Z,:o4T^\8^DMή9FD/b9p_9&o4^j!vs>c0*Yg,8%JC&\U}:mOxp;qXDF |7 BYy 2K%pVp ^Xj߿#EڻGݗFpϠ 2(Knam#YKIX%wqQw>=>~`Τ}߻J"4s:' g 2ŊT 1db 0?@^'҉S⠠`Q/r Y"#Y5ٗu8͕s:.aK,;7I@9^EitE'![s~p4@Xͣú^¦kTp+4?ot4"7O0Uu0x!4U$gm_"7gF|ڧґG8lGtiqee˂TĻG9& azq)e?97hjF9uG$zޢ4~cY`8cH[:V T>udE^\fYϙ1_yi5RtxḎ`u=gj{T Vדyv94`(JW ko޿`˵^yWZ+, `rNp:?7Mb05I'~ox /iL` ac@VyK]!xE$%t91a]Q}*^_܇7āxM#eRu@M=XG0a1}{vG]ZMODQ~WLted':lAc΋ۥT3IG v}*+E(ݫX":o' =ZF*ľʬ=4"f9}(ޏeo4%0X**eܣp0˂l=:ZxU,MOIO>^{}JW/ҥkcs έt9W7dmJ_ S$7RO2Df>t%:vUGA[ryqJG;e|5 iUy˹.{m@հMvchj -!hѮV{v7*cyIR&ٍM>_UBKrvf[ȻvV(f~ڇZM`:)pV9^-o_=p?)7yF\wԖxcLڪ?۶隔H;UO! DIn([kCMœGjSnŗ4DWQhB10(W^E 8sG0Ph y,]a,Lʷ1.miZjRf*`g$M4\SdԙnkŲ+ai W3=` d|4& ѹbyFzPvm+$:kO6O@Cvo*[&ab@.$ ǎt +z`DKygɆK?DP3@/SƀoJ.<'D ?*uW,vC0N< R 掮J`CXJQ~8h8zHfP9h-ز3XH4Y ً8BF/P^d5{҂5ZTVʀ}C%d0-EXFp[* z۬Nb :l#;f1Ft5,⹟tThL~FڐO4lp`ш= \\a+c{/kuυ +rVvfSfZgeJ'\%0}V%ЕTU]M$5F^[ | 5Q28CfQc[}dVEۮݠcTYSEKIo. }2x_3ae΄#HZnyMRO3̃-mAٝ ri~zx#^3c)_`Nk8/MZӟ2]BNG/,y 3g}݇AzN7"S96/-9L8bx{O-Bi~LuY-ݸf+!pPjtq픛GOy ifȐΑed=Vct6`/`뽗y͐};&&&R`Bw󆞵k;Jfl!8~[qNҬ:Px˵|뚻<^tLk"fvn)e՗a E.PAeji WY9ڮp\svUY@n5Nڱǘb~! w"{f޽G Nwh #05eF8Tvi+f<ugi߽}}ݮp귡 "qxXYmMFˆ =+l j@X'ֆvѩ6Ӧ=\ _Bkqay7k:uϜ1MnFgzDǑ-IRfVL)s)+[ޟ={& t$*9y|b*~^yŠa@ c`i#>.lUncƵ7EbC}B I NGto&v*jOlΎ&&hWb-᎘]s;TQ9ߑ^^b$#i1mklm5+zyGA#HykWjy&)O+ w*7M|c _{$&}m;5tr 9dy1&8uZs`wBkK=>w|){j\׵qxҺE{֭u-d͙Y͹<陵9aitgfMDw_'I$Ϝ. uh?PJ"7#x!BkL#6Mh 뛚yv4ܒ|,zL:6~?L?1<BC~AsPq BHn86?(ڱq'utw?'f&'bE/㕝y+PVqiX8'y{rl"}_L_7SPP>@?aЍ+/qzn;HW7?osa1r3U7;}dN3X}IN}%'oͶ̫JN?>+>)W֏9Wϔ7b=coҏPZ} )-3A ȳAh(gRR ֠xRxOCޝttb r71!qBqe6 +%-dr݌2WֽF]=JҨ82YZ>ὲLֳ}2tB$QFk{R2p1 * ̒Zj#69$h;Tlۺ п;vl=I9zI b;<U@շՋ^WelڙAQ"6; dwxϪvG6Re)=JؐP=$hQ=d=M,uOvTRI`ez[$.6 GY]p^gZ|VLymqbb' m-LG[kqHc|'@IDAT.-4I\KzڣD`ℑe} t- g7#\1;];dYZaR]\`&X5f5"q=nM<>BoGT}`]A>I4{t oCFcZ*)lK 9 .RsMrn4M.NwxX4Ȼ,_&y.xAmb> %4Yk酇L*NsIZg/̔6ԅil&m 6m ': ~޸1A r~jggM^Fqo.`{@eȸ_=2]Y^9LoBK Kw҅CRһ}1ثZ/M{*}᳏o{Tj;?lI>t2C'O?dV6 {He|*x ;m{㎙`Ź]FL4L:H =DbE#U%bny:#\L|4jLd]g<&Qѩtbl7[}GcʰON&aag9ff)BGv/+Zl^2$ߺfs<ڨ)gm;v@:pvrr0g,9ǹ V7.f`i<ɷ~Sa̝Źt'a:>5bt֕tᱧW Υ!\ß(_>Zmhm H5_]6TTг_b^U )f, Lc0qKW> =Dݯ|`pU+!P@O&)laEH0pC6*`KDRJl@#]@lfn>DHa2-m9 Va < Ct< ZvY2'1\tl0v2'k u Xe X#W{:bٸU*%.n6QPcMUy=1fCUf2F 9& pLe(Wqaj" f|(̢3$3ƘaPA0 =.c;BFpE+NƼX[F$kkƜI]<&m5PLwv䩇BV|#.VoV"w=CpJv6@k&ݦ @^AP8\FTD镐w õ~-њtl-ۂ( rȾVmH*81 P!p|N@{8JK;躧@* ,·^Mo&Ldtu03qYNtd} Ŧ_^WzUnY@Rp>+5/kJ ^E,0=c?_B) <0SW_ 0M ;KLp^qz[#}u}S#c΃ QuЩJRZLq|833 (B+kk'q8R2dv܎/ ̗$'2`ǘ3CWoE+~6M |Zsy/^TA?A\#vC{vs3@3?B28M}6"u>7CL=&3qN4<>`{#c*V=3B >7"(h+oG R%_]ZoGE KKs1ɂ>l5dΏvhR<@ (fiuT!2qE=k?ē*|W~k.l:/muUked4gse=Žu͕ߙʛfsG- )%xG"1O`Odci߅ =(Y溓~7.*db$Cj堲V&C pqlCZPr@ۄژ]~9d#hlj{L~i`B0 y6I*k񙓧O"[ ldOGVҝDvI?,:@k%C_XY^þA=Kǭ|;e5f?kcXYL.m ؙ0 @#YɄEimb̀'ɓ]iWߺiV_5WzuoڕؕcpP;_\K]"Ρf'ಬmO}gzҟob{JVh0[iaSbLaL*]V%l(4j pw $O_H= q=}tj,յ23ՙ5F{-9~vTrf[xdTX\b}iO{G<ʸo>DU ul%e@HV#83i[{ڪ'OS>qcv:1ah+EqYT: vs|.| f/4~GHh>l0 %c`Ղ&% סh[-+B#Yucla6dgJ/j.cˬbwP5wPLv갣La),ULmvq }*ly9%ЇFRH{.yX EZ52O"֤t#:>.~1znmm86ϩGE8{܋-mO Aj 4u '[Z#x٧MP-|6{;DӖn!!yr*CHs£j7+@Gi2;iFl1Y-cP`64BvUl#> o{ kۮFw&d[\vwq -Qѻgi[=8{/- $h5mWa#_O W OU|uM~_K*EPN*i1w $GmE. ATMi@ƎA,3lFiCi^Q>;:VA#buh 1m[n=̳S| F ENݡWS>lx8Eq|ݗU-P5 1ehc lNىsC$19JGqɐEq5`LԷuow'AU9E5&0mH)oh+̪hzBHwꢳA;7黎ih\#I5 4,?wkRM9$ ]xG|6 g6P4kpi~ITR偊 c}v30 HBj؝jn) q0ooxAZ0Z/Z:z 3,[oj;x ?k d!ؚ>2}Mrǵ} ?N˃fA:l&,~`a0S2K7FC=DeL}=4K>L$z ;ޥ[zz&K'mZOH~#>}{Se=8 g?z`;/y1*υ}!Qki~ d7:2J[A]}~K"=HKts,YuZH3ky]WK83nVg,Lt i-Lb:l4'DY*džipQilLVP? }7؃M֣Z@BQsfe?}sϤo] Zn~8=̅ }"_ڵ`Z[sd:.[^eoZp݌-Uar~@g&O3fB@@64LX>/SNk;hX G숿LS\ vvTmoosM_o?rn})|FLm}T (9d}:nm,J.|1h=ȲwYĺŷ]hhS2X_?#XA.tS .&qL`W\L]vЧ& Xc!.63@Ȍj~(Z*۴xxG,`Ʀ&Z ԲBaPAHŊ^$9/r:sbk˂bul44yO3<0³ڻixPO.AGڢDxdL۽D-0VY{ ( {#bf #M-*_T!*M>ڪls%#uM2Q e52w c؇N>.<$#k%`ɼ\)cUuwe8 S8Vv]HZ,]J< 79 ؼfE 6,=3Mb/A鐍 r!5.Yl(e UiZRB L13ol>C%d^aj`.!$Ӷ$D :IXq]Oؗ }g \iG0ipY+LzѷBpum{K#%6PWa2* Ys"hcDr5luPaDuE^U`"[~GMÄ@,j(&<B5vߙop5^Ro[yFhk<$8~| E&R|.j+o^M +ǦB?L>/!:3f.ς `W|d,:I<HVRpq=mq"ptAgW7-iŽ׈$֐GA2}\`6rN$Q Lα 2:w,ԥӨ{S P90#Q0>Fi8GT]Lf[-2 kg_x"Hf :қLp[XK#c$Q\0JWxH) :=ۗK#vCDn.IdtQHwpd ˇ́ޑ==Jt0Gv~|m8YYDsfaV\*/ +Py-sFK5te!Z+8|5sB"㼐 B2 j`.ZTEgPөy=ٖ$ٱA ͑]stў ˁ e>ݾv38Tp39y!ۿ" T Md( t3[~ZMIbTjʛ<=JX-2~T$u6khW[VȀq։俉Qj':Ds#`T>6.Rڄ3S[P& X}Vhly9ΛVv{muu/ Zka2#]#)"4z6Q྾OGzGOT 5s{釯DZX1fMPArǢC,FV{[MF$e}~}M0H:aT1v{ɳ{t]RGGU?ReAPKü_K_|F.TAxg%i 4@h\Zp\\%jSi;ZlھgoU&ܗ\k" YmD=w X`|dDdGmO6kS_OWPOk!0 vb|ͽYs r(sLV|"ޝ;0k?͸ұ-Tz_~t'g}ƌDnXD).{=7b26}c:Jj[f'{47,nc>HH0_}X'tW\5tOOw?]q*bUdſ^^[&~ېV湪~| @E$B6V(7HT@yI0vi*4gi oHG򞝛'!R VzG_U{y>Vd όI& @ܸ}/X;AzmU՗CvіNF) {We-6C>)OWY ioj`ݷ x9W"KWIBm0^Țg]ձ՗==&X<)cke$TUКU=?%@?7O,״Z-5L3<%BP |Nj0@gR1B2%ջ$1'&Xw|4|Wdi*f'Vʢ \U^؉ PY@&6Z8la8jǕIن(03]*%.$0V~@B5؛1yMvBLI}J_-UOjGH}~F0lp=;5 ^^aTu*X ж7CNc3qb]5!t!W74 Ѵ Rn~.߼  rl),_*[S/PE:3 teQ c\Ц*_ZI2xKG.쟫n? ɉQƺhr&e8`z (~b\>G>~ݑ4EW+Ty!sDхPط rbe{GtlV̉uV6".~|?;$*UagU *=mtyĽӶ>o@uy*H"DnINtMFAștFk|yp1dAjO?vD?yqˑ2%-{:<9 ό`k#`umƱAqK 輄"6^/z~(GLlm.Re.y+qToѐKpvj+j'c=1xw4m}F鏾մ,F~+iyݷ-xvM ?Ebt^~J vڶ]Ǟy@ aM謢%Q%٪w%bD0< 'Kk#I m׏Z'Gv23 & li`>\cqiH&i8\e2x@%h6 ,F ソi(!gPD 94R>1}MjeB$qD@Ұ8fΜ;]}_03f㏖|7 b-R}Qu{oHO=8qtΞ kvp]Es7?|Z7_3qPrhcpX4PD{OC0=q/q *`V1F8DX-^I^-a[dg[ъMCzkWfZXl6Sn"mIGU9 ֿ?_yhO)IAJwfʗ=,5|H}%dF@~7ۺjV\E,  }0U`odO -@(2ʠ[A_Uy6UZF!cM&N  zegмFmVP\3+WҬaka]X>RV4a=ƖLщz92ci}0oIX6X[S:+(~?rD}G"Kv[[wA[U=}Yo0yȧ:rQ- N#Z?rbg w0ݽs%ݬ6fi @3Fumר796Hh7X.L$$ݹG=0S1Ypۻ78cw9;Nr+s89>嬈: lӣV_.`y5 ]#L{eaD%IƜcv|Yn",մ] -8miLCe^[:>4^\𠱹%YU#Y0?hۺG={}]/|mrqĦyi'+8weTDӞ,l+O,ĵ/y7ߦE-Q Şk/G<2[Z9N:_ot_*Y+Uڰ;&L@%D5]wy:/^H^^}Ml l=LlRnڵD~WǏ !;[ozw꯱q锄v!2zЁdl%] 84Q!(;Z,ڮT}Uh v i`i@۸4GNLN J3Wt bF"(ܥ#A@NK9jNvz~czkJj Z`m J 91/hBBf}Ǭ^&"ƺ煼WPBBi'0w)b&六{|d\K3CdZƬ~5YϙKy]@辎Q۶gv~5?{1?|9l;J1B#_ԭWL<|Gɽ+K oc.E4?"bbsR1H[/s.m5 ]Ŷ♧n5ɂ A0M\_$ѽ䶠9-x?u6 $ish>7K#`ID$H0+7Ã6wzKt?d@A=E!;[N}a#>ݮm̅pNw>!f6APćyTz Id+#AI_NQǯgz49{>0'~6=zQ= ^TN2OlAr׸,V9&0l(慎>̇3Gn`w}w N9`jݎd#<*Cلu]HЗ41:M-C>F)CޣX{RW`U?XJ+TJ{ܔ؈PnULYcvh$AR^0 *õ+`^1j_!҅|^ S%w9%|VF!Na/>1IMʔM4ēsD\-AG\г묝j0v N~Z5/6PU^QŴg[򮒄< x:xې״ ~Cc]ԏewU@tRu6rUJ}% H §_@IDAT&772\FlX,(Ct:LNL\'`͞/|H@<ù.R1|_^QWjѵ:&Z5fo-"+(b&VA&Dp5c"oSTuyĎ+;5qN@ψ-Jl#uArq`;q @.|h'iP[4` [1) BR+w\KtY\\ͅfE4bB]n97G̘$b_g9ZWL vA$_q%CS¸QLaXn8>5ui1d Q1uMf =\L+x(NWȽ8 wKtX͜HNYV ^jGt5ք[3TO0|y*y?WNɇA؞;5>;3okzhQ@k-^P86vsR$qʼnz%i+sFF*b-h x؉ @J=*:#@7u6tp瞦ߟ$]uYڻC(*h*:X>3edF)i9yrT%eiӎET#$AsQvs|2c{]vVJZ C'7É1AvQ1rNu>9h;XgRstI= ާj ,|W#!Կ4jC\P|H,*sjD٬o9I>A ;F2wYXMhCYD87D6E.)ŃkX$aMC( T:g]ѯk-"Cwgl^1YЅfC2T,%933*.ΉqbKwuwCr,P8@Yκ B4#0J\؃%b)vijJ_yif",9Ws(bfSً!|p/!noA Ĭ5BȊR0r@J 9\^Sbˌ]U;Ƃjm,82T FBAYѦ!c++ ,r2K{KCư#$ΩiqV_E\6!)USǘAFn}Fƶ-DSr^۶|$ry^\~!V`{R614D5(#8 8fc0~Y:ȓJDcŴn&S cJN'0ӛozc4f(+H4N$J4 U#wUf%k`-@$duηNDiJb&JWa[k1@EpG=˜1{ls* 0@#Py5# C[F =Q5"1Osۉ:_ݱEW9LQ*4|ͬksr| )QDq'+_*ċ0c_m8&.c+.0. !swԣSy^>m0 "ċ9clC.F;Gc#Ά2⬆3K]H9[}co2Jv%SM+u !g&Q٫(`#HlBD jm"DЙ61ǠK##jɞk0_朵lDHF6ie~4[X`TJVN.Bc+XGƞ/fd`,uK\Lz&I&{dNa8hzUJ_wދLx;_#ť H(cf+  ,Z,9(BȔ.t*7WCWB%FpN<r_ն{=:pȿGdJVu~L,)ޣ 8Lא5woߎ*w-3d왼9x E *I"pg@]-8AM7[yMJe c>x]`(x ^*F 8{ƞ:vCtgp?|츽tƇ\gȬZ sXoetfO=DyY3{D*D߻q=M._#V-̄~MЄx#ls}0voXw*Routiⅳ'kLʛh.uTOn߼n^BDV!&(-4z; 6p5H D< Q;jq*Vw-T•/ETo޼]N't bU^VJoV6biC)4lo0>pNƒζW7:b_l52Zny"#)n>udp=/o@QֳE t$γ*IȺ3KNgҩ~*H؉TF,,n-9;/jĠ~ F}CzGliWۑK;@@=iGwy,x6yh;oUb'R7~nҫ^ׅ}3Z{h)ٽ g Xۗb|T2Y^WV*N×_ZG~Hdu?_ڗc_ |[\h=Sk Tn:)=jeKdЩ$S?r 8/#N]U'/ Y/Szo T&|?n& } ]9_wJ-;J7xB[ȸ=Lb)42"!mK#؇.pl}uP|.XF۞ :2T<2/)?~'?G~{L,蘶Cw"H'Gc00η{!;fIR~t;?ÆJ?eL@I[e-WL7=Ɵ .{$C՜aZaaKMLV ϾW768cM; .8hG׀ O = JBfa1 MLc1I¾Ad1i~+Q(<#\m^`ElEU1KR`+Wv} ^ڼ&kmE$zdG)`5jA:0F֘J~]589o{yze_b^Ӏk+$ 6Qآ_q&juLp=ND|-E+ʕf0aR m; i,:؈Y8vgq M/O1tuɨoj4l6Nql\_UsfK_ Zf[XKq6_ md&3֕"YqbQؐxT/,GRʱ>N*7IPl[y* yLi -[}n *{j+ gYwUʋ<%i"cbR>=RTu=ud!tl=έVY"*'Oq D5J#7{L >ΗSFyB69W*ibY`1btb] ]B!U>]6' ep&uc]]XdQPD_D%0PdPV*w٘H>30Sĵo߸o_N=XӞ^!c$)h>Iuئm $`~;'2Gwt|x;WX}=WT1[54pyYy]L^W#xO[fIQA؄ClOFځOJ fB#Z Drrqmfv5n(@_kفY\/;1 (~SءaGЃi?9*\/i͵2(~MOm{j\wq8ͬNV-\Jeь];E lܗF~)qˑ&ͷ;2{.cn!`$g?]G d ىNMOn$X(WTBZ~oL!%q,M!s?yr)qOv]Oq8tRY`/k|w;rH@؊?o}S&!%ZTT!v* lnE CP&y5lq6ea[H!ЃUCUuu],1>L=vC?P2Aj'/y}#{{kneUXD -?E򼐌amh6R,v 8NRI'2]$I8͵.m FKYZaY (Iݴ)cByh,&Z9o"60-? '}jU 5^T$7 BcB@qf5f9,:, b:N <~ϚuNDuqf@;Щ_(`[Ȉ -705hZg->ݻw믾9u8/cmm,c쾶24X_C1Qia -5*z $QGG9t枸.-h+RtTfFuF:xrpv~O Fӡh>4#!B2^Asi;4ˠc񡣣1 3<\˱`A\Ccpdv~ EAT^hGca||kqwxxfa 'vFu.bGhչD=fǦS!/[9kՠB%mFdl$;V[XjPV 0 dc|s>?k>@c _0%ni;eI;CŮ@} e@2}!zKSVܘC0wt6E\4BqiL3.h0RW#ͺU:.D%}3zS4v&d"HY<1)EQ71&?97}ru:u:J0)%;wIeG79@m07!@g$׵k/(4ʠj kX0tIQO ƚn-׫$C{J%[% (/p @-3u(D+|H Y`Kl;|AЋ{%i-a%Ѳ[@"$ܤZWs xK90ĞbL%}ۚWCO_EWH^GD77 <{8=;]ΏAL9f 97ύr&owTԦ9^ :8ީQa۩ԀͰǺIvҴ2C>0_}+p&ҷmTo2,<=2:r@cӤNXC2 "~g|lG/-ߟ3~sfXao%wߋ%)l y9%8@NR@,96bɱ,ɲm%ކp\Rr`[~v3u_BIxT7 MeO/&%ޜbaÉӱTK@*= 'z/g(Apºl^M+0j;$S<z<[뎯]hTu?{S e:p⒧x =4\+81޴W%9mZB`57%\gKY`oifIYLf=/:eK_#it܃ǿVW3@9LIWM h$hcwp_^k7u]kǏ $Lv!Wd܇3N`1~UZE렡V~%Gm{ԯߝ`zbјn@9Zr4& [wrܳ%f`.gq$X7xΪm^/ v$U\ݤRႽ^4M 8x$_˿nIHmY'/HM _gO:8Uјm|+:p@4_Jw˹PY=uU&tU ^b7eckr! N|LɊGE><㳵[o-m0ho$D8! Ow|{TaTxnyov%ԙw_tetVJI\ejzؖh{U-]+<WƇLҷ{τ7ƾ-0yNowv=슷s?,%kq:3޶AZI??p@ue2Cn{Hk^m50TLǡ{ tak:]>`oH1V|&I06}hk7lIwJTe-d5_u04V Y~vb6>4U%:ܣlITz[F=:hSf]cLv,;o$!@czS"J-o#tZW ;=ZQR*O>C#5hGroC kFV{K ulJ8mwp>]us:u,/Udl/:]UPcOܵ<&7_3gz|lwN%S:$Yw3[>[6mfmGkU' DDϓpΞ-#\S8o]X?H._S;KI8k~ıh?TK6>OBnjn<}>"hTZ]vرs6MGW$lcȿd۳,hnxf\39/oC|0n~?vud\H>t#Й{Ki%d>tZ2Jڍfס<6>*wdWD 7nHϑZRFu/+RU|cG׺Eċ%`4ľpw~-Ӄv̀핿3}ҭu_- ~ࠑf4L|xnY2 &{ͮ뉉jh""H2}+'|W :'BdL;C~m:ۧFKM8VBk`-`qfxOiɅS~y*ǶnRKo dٞ$w~!XǏn7bQq: TSb7jn\1>cyfr,yY ǰ!Z%óKWeSǧԾ`A͏>dś^&8KЙ?~Uꠌس󳍘[w;ՒgCX3ёw+qF虝B˜g/p7Xϕ IݹξXd\V{_~?{P-ϝx'?^]2^ז=dK[s=]<1'[Nzt@ i%1|tnzXUW/-tbw8C9,p(aI9)BZѾ_2pskSbs[v[q_s9{)RU /-cWN1+#nNFlU[_23Bq</<|#!i>|S< ʷDNF{e/z=.KƵ`ș2{ ?Z0TeA.]# K s`[:7oNGO FK f"z x?2]TOKoL/ |fniVIڼ<+k(*YWwq8(Ti[vg ‘=}MwMه)9ڮonS8GG3JRlǽ1rp`#@F%='/JA5A !^YJ콖we(I#!xG8.Ayf= ֛CG7e%v~b 88bD]D`r bSn-[_E+9{=8)V=K94\9yfĺec[P+H*Bi0ozqQ|db DR]cG׽)5j+SWg9Q1ȓJ \1 1{SkxJ dS: @Ӟa7WcOa}Xz!+VzǴj17w^ߛ8>hPf 3t .rC( e-0z|;/BQ^k_o e0g8!oxf3Ct~F F)pk>o*j$Pm"dd\Z= <@1Z'>~?%1d֋>n|]Hj#;e Z=(eo?G;8{tR]v<88օ*]T>J’e7&!4-3X=)['VeAg:|w$S|N1Ux {Ɏ㻝>5 !2rdfsdj=\/^]i?e)%x0 25Ҵ='`*TC,¸Zb v j竟sUՁ3g4 ,8W:y/1GV;nSo֕D@Hgץ˕"׾ k|I+]-cje?e{(sėE6k'th?ϛMiLS9t#쫵eط/=^OC X^f?O}p{7:tgQӌ7}t3WJFj.յVcM-F_)g=3@wU;@Q{7k"xRo yhs g@$L$d938\ML͆)v*XzH\>9lo! |sKbˢs"1 U]#aA7@Jϥ9>t/E_G"AK]I{1uTsad]lTYt.. 6U˕,>ΆO2=pl=k_sн]xV֯∘V|% 'DBSn-Dz3Gp*9So%_RN#TꫀK6tomϞM޷GˎW FG!_2:q-̾VsUO_mk gޣ|V_ͰޞK^z F ;w peUM^F<]w0 3~8 ".^NNC/dB9!jrgl<3gbk]Ugֹ0t>k=M|􍯿(q@L(ǎ4 u!x *-پϳuBtT g dW66>ZKxw;]ɮ*KerAAhל~D?3Ɔ3dLAN:4g?`]rdoi۹r`{:ч}=$Jckŋ7:5z /WZmlG9񉗜3'p Up/ٻ\zk8 F2zx[l| e /wNNBȮx35-9͚Wbt~&hC035[tx"WrKzFT=pZm}4xt0y07/]CA=f+I% D#}\'"I+MdL\|?ϑ;ׅwtٺ޽< G2&U>"7&SllV3/O"~|md/g.wk%Aqmdۭ; $U fiMlH_t5lmj5]AZ\Qh71]hk~}x'?t9\s5;h [gG:^%Dubh(]ָ乊;~8uk%g~P)J~InH%<́ Z7xbvF{\Zb% M E cXY+ɝ*=s*wHO*n왪N7~$ 䍶ѕ1O*ӓ@F hTpS,l4VAlXrD@P6D,-%Eة\:i=޿wwe͇&>yHl^3~{;n>?fs I`2/^#/T\/ ˒!TN=yr!(]Qc׾l/rzpz|+*ۛ\t{IhAt.>|\Ǧo;%0{Kg¹ &mwu!h2=mSHf_ӵ7o5JN]ڣDvx H8\Ϟ=3MA۶KlB)q]~Me??«*i)^^>0۝  Jw\j/ Ӓ5Rt(Zw~1~jZrXR o 6w,\g^xřt$NC79M8G3n_ 7 z_GMcI}t:qU7*xry^ELDѢ`p7::X7wu%l0].)pk ZV>FzXthz3o/]_2ȾPR g-e ?~m_U6۬`9WC=ur-z}%ݯG*;.~ |ߣ*;YEݭrׯkNL{~w{sqzJ.Eo5CRWxm"ԩ7z&e-dvVMؐ伭O]4<@KG̉::5W]6.eFUE{nξБaS{Ek?#/wOξeדQ|zPpd=\6gO[hrCj6'NO72uJjq 874HKvgo. s1oa&y" ~_)kk_gDe3CiÊU]l%Ʊ?g:*Y'<)Z(2n}@n&ΙWl"'T!(,ŦIU+קW? Ҧ)SeF2~8XimBU\QQ>w#6Clm1XAwo?3sOK}{9+JY;sl_NׯN=W)S"=Qۉx)KPT eY#'xUZ>δ-#S "S8(wrLj8Ҿ=CC;%@CB'|#iPfu[+̯{~{2VѾJ)|ha G9dΫht?*>654Qߴ̅K;.Lw0U`{,pR)p={C\wl/!mKUZ%B_7̿a(DGh]9{ڠؿgr 8({QjPX^+4 ֕Q!g> Y3~ܰI[{5O`` ';fՑf>|dy:λ[Z7W}jMEFKKGɎe0?^OHo<<^e]ܳ7eM)ƻ $T\_xt>7#Ι7[G־ʈVtE)Hf J 2~y٨ ͻKh0yrh>z|72[עaǁΙOߟOWpolmG7VBIzA/yGu$86YEZƜ=v|vV?+/ Q$r+HF 93r=eG{fꢻ)9)_ )׋C-Rh4gxO|)7r^k=e_9KVrƎd|~1~! ="=R%=g2VrCczŎ[De_3OI0t~zo>̕`)#9GG&tِ ),hޘ(t'U }3}9=m*2XJݭcN1`4 [JT7cԺG %!&ַ'x$^1TSϐ`6=ƗޒT5YF =Ӿu㉜>M!WHl^D~y~no1>K?Ƒ̡1LHZ±$,auz }u&ޯzO?s:q{{@j2)p1E~^mtfBJM}$x*xF@IDATn%Ǟ d%wZPep^ ~ f94\lTF0u_?rd[?ϠqƽA89v8I"M' S=O0Noˁ]Å▒Fw_u=]L5>ȑTλ ֺ?>/1ǛENkKz*ى6O%n!=ozr)󬗟9-LY>_NGugl}Vŋӭ4kHC?mg;/;G w-}遪^.*w8gs9fe٣مm g3{:F)+1"Й&o֤.x\gtJ(:p핓_BǼHRy J虽9!9T ģj诲i^t%ƍӵFPofÁ_f߼8-5XW* Χs?}$:Jn7#:Ba77b(P>~1ȧpʽK&9>ޙtWţHA<s4Φ^~b vl*A2U <]Kx?<ݿv!-sq׫3$#;Է?eP}|CC]_>q.O9uO57{yqW"%|qbl`KI$ݩ;O$ޚN`y/%D*F~kxU|!%Q4x5{]YMA䖒hxtlƶ3jpL2Bׯŝ=t |>;ϕ4*ˬ4R/~rgD'%{}ex$5K@ƙVIJwomٝq zኻJt9>?~^ -v`/wmQpęI2{<-?~ :-OKVh[쥫wmD _={wJ:f5C6]]iTڼs$IYtdb,) ?t((ٷዹߨƃo(T=|6ڠߍGs#I/'ܨҭx{{=j/LZ,7ө#(0k \׾`{ T3J~{*->%E5*t]Ю_ wU~B;$ . w| -Y~ D"2NwPkNyR±|22p,t[5G{`8#$)1|nع'r:oIV}1?) tl;4&Ut'ϟ>ayW?Ua k+q8.Kr{Q7Ӆ+kcζ![ndb12G z$Xֲ,45Y<| :mDol|  l( `.CK;PQh96\lF`tv) dGBtݻgF b}\@*{fgS2g3ؕs`DZ@I%=dNLe*ý>ȕS?3⸥WAH֡>1Ow̴1 , Ώkd\ EH;ߺ_0^U "@ wpp9k! C/(q. [kA͘pDV3=ՌAMM#pv jAS^6kaI!&p#qJ ӓǏ0^H3=fΓ]}s r~ZerP=e|̀|U%t2Z3Ik;9 ~)xϒ$I H۷ytHStd;d%wtx T¥.-9\bEj eAƉǚNe52|>"Zs Pê-?˘t?:6r؊0D%ڼ<<UϬJUQ92;< RH\/KW h 6ݐet [.fI r>S]ըJVad@npќ` #95LrF {g|M'=}.@}A#v#+F@C4yp(yrp FPu0J߻}y]c_}4dfwF'ӅC)0ZŪ!H\1;Z^xgwϜ (|W~eYIT]Օgs6ëupPs}vi"T$3rK kؙdAwV^ek$sig4 /tF{osZzha{GI8K`-`C/pb<1,U,lëq y9aQΰ{HrI$ee؂p/ɀn&IYkIF:#W>\W5_m*YLm-lvliu.{n3o ײwlulbf?*6LjW?.m:طI۶h4^}tA=*@*4UڊerWdwSt>y`+Ι=9&80Z΂2sx}n|>xq漴6ݜ'ydp J_>@o2G<;N:w6ҧ='y hV#W{|Xy̔Ļvcm#Y%'[~Qss2Q>6lQ'ϷR:Pb##(qw\V8\rD:qlV}x]pOf$9S66ɺU:~O =pbȽ}'[1k7}]&FDYV. TIs$rс75`?]|ӇEՓ[{}@_g>`563} 33;)Lg{GͫGBD=r쓕+=Ewn|0x`@NdĠ?|wa{ztL貪*N9rӺi}vq:d:\3g>.8Qc~:'i g GnZ}^l8D}I#; V;aaol 4CTsACmV7|339?I*HIgIxm^uDs3g?k{jHYO|tTqѱQk=&; }}($f_0L׮]n>NU`K'К}v> hwk[Vﺈ_/N'f7'__\SGU~x1tl9={YJhsj‡̙kBJ{iIPȄό_zrAI^ħcxs> %Y2\8|zRx[0s%V@8qϼLK:HnѮ\,|`o7{G?8F{T'/\ ^zZsﮥק$ɐ#0|Q!zx%~W%2د`ĉFfo/7 |3\o\4ݾ+Pϝ]L UAӕgɠ$h?jקC{* 3 ȉС܇"UD]>n+ _|$!B}If '7eO4.,NDw|~ϼk>%9*avK#A<[gҟ_k4CIxx}HӐs%. uM/W/`t"MgPC+$k;D LV~ٳWuF9a9^"H@+}ĉwrD?=;qb%;vrȅ&]?۽OՍЙVAxሩz8p(w3mZނ. -u8;WQ|zU7"w_vA?-!fڨ^>X 5Zf pf#U,YtQ~iiN8߼^k݃|cN޾6m>ⳑp3ta ӛ컇S'prFӑ%R疲L7YyrݺO_uj#t;#%oI4(=ZUɧ' ~~$!_'[Jӎft5{NoYbwvB&|j#qG-Ngte[W"T_W\'A?b>փ5:-LėM- hjt-0O#qfZ ΑH Y`W}EI);_Asp5F7ccDbQk0f{|!*v&{S)pp#^WَF`2'9Q,e {X3ΗDoC 8ķ'!8و9iރZb=@SFyTЪA/@pc5` jلeҴ >HI߁iCp]G9B[C$:ziRѪp cBfg96iT5c2/rIY*)Okͷi"Z6@ֹmZJ՗WjGBI9i0`LXXThM wm.1͠]8=!NT vn<3yrs"8  L4` c06hfat3n=u_f,S֝{GpHI 9_Жԫ@x<3GNyP5Mj g050d9BmU l>ߟ‡A c1`G4uwUv&cj@ .=l3:vsd?LlRQ7c^kF[)q]PFeO&((&dN ?m˃ͪl-.@+e290iL/(oKtK6ih xnX2X@D%wY39~ t<ЬirT< x2`X2  ϗm(H sȕa8mK6{\H䝝4N|Lk_h {T{ݖ"^RN'&%Bfr]h,Q={gD}v$ȉxĐ:Y5?':0|e#>`۾eQ>y]0j(Bq?< dM4g/f_d]Xd-c?s c{/'/+ͺTiwOz`Q%w=w ,s\p сTn|*{Wk94&9s0`ǩcG7dlnKgX{S2JƚJSNUD2h)Pa&or [%2VI"4V%l<\G84H1B  -Y=8{Ar=C3XԎ9k9Q  &U 9TvoG GEnIs?wWs0#Jhgkw/IKkSŏY4l~j[1 zd0vp AWhC)[h8oO޸ߛWSm>Uv}9)[-Rx0lUUG*ͅY-k0 ;.zLw*|ګdHJ U|oFM%i3pVYkzâEc L2oWr(ϏXسxLOպmI=}pЁ3sNZX38*"2ڳy;sUj!y_tV`hg-S a+ _p߿oy n r{mIUCj-H=%Q!>[g^d(؜_z8 <8mtfc:K:g2h&N@Q@ i5͋UӞ4}U :lN?2l]z_co3m>[U&dkoe=/:.S6;/.'z~9ϥ;*iJMz"G 7: YInu /R9yU>E% n $·:Y|ŅO?^uv;}s}ݩ[G>`(:[^^IdZv2zD m%z{ۜ .*ѕ `g?}$oUy#il-J0ŕhѫH2(˂={f&+SȮX7h/z՜m/~>Ƀvg/2^fAVNNs#3:&1~4Ϻ '^8N13VL>i-0.+xW|` +~A~nλ[vdAP`^_HKLT?sjG-~ݗdGAA@lFL1\x;νcQAbt&cF~/ r&gߒ/M ,JT .{#I+_sswhfq]ҩj OZ*5ւu~x]F+?Gp#y%[cit7d˗6<}?a0rCg!.gmb4㼺ƵkchJ@ I/H&g;s[@Wo菎MW D%sOKǝ5ׂw{/\t;A!XݕYϝʷ]PR.ؔ -*a㱍*"t̫QUUAYsMƧ9'IcS#O.Wx_Nww]W kބ 6^CNpS ;gy?K_08n_/=HvṋEg:Lqgu]);YAoB#;Ys Ft:c4>ݪjO 7gR Fx9oY*Hiew4~ޓǵ/8J/W/,ZKQb> fS%@{7bs9xzmJ ݔu"yEרO_3V@KXM" m6,%u^42Op"*UER7F">%1ĞĀnޘz@K"kWh2/M_DF~M<Ʒ?2i`g0V{>ﶇ];Z3uP7ﳥzy:}0|rxGb%-Td\}x/^trӑǞ G U?@˗d Dכ_|sڞdn8\w9]AL *1=V7kq\__Y OC.n1Rea:R"ɑT$I]yQ'f{6޷?7)Wxn/ QGlŚmx!|0T2Z84J3>@St(H3 "NY;#@+p/Fsd^;00f bhè`mʄ&0a 9g|)90J(S8r ˮu,3 _)g̴=aܶQ4̃FpS*sL;03(CF9[kRqY 8yPoӦ 3??/;ZjgUX;z/ \%uSQϣѐ[_y7Ve͎|fkΏ9,s)[e(7 BıgՑ ?l)\;&ZF },0Ĭ*s;Z >YTFzJ޳YͲT[ r$6  ){jm{8) `h89-( oNO[(@]:;f&2 8 dZc36 aR;R3k&|h.tO]U0݋b@+~%;j87sP9<8A Cu 3E1kj3˼L||6>ޒR7r~aOH/ߚ??}qZ ֺwikZx4~ ,j`Iļ7uXKx;J̈0gN~[g ocJ6a!)}p>ڹIO7#M (-e/\vF9BU(Su8Ϥl)1`0KhRPA9d5S=oK+'? UȰVt=R_1|_c4/e C8XwaRI# kס^0'pDJxc$#{PNGJf(uB^eZ<mHhe%~0[{hs1IdĠWsV;Ҝ I$0D3aM~d6C ohTɅ3)8E`O-c lۮZv$]Yƍsu 95>P)p\ =O%3OE2L8O#kѽW`on#2U~g)f ^ݳ`tOYClhq}O'|NA ~/UHm h^Uvw0WlTsd}hyv]UG C_s8 wGch*i蒍{<9qn~PbA>x֪j [s;- 5f ~P+ɛ^!=yө셹eƵ8؍䅀0tڞ/<8d=WX ǹw#`p2}I#s<݆FbVq$3fӡcM ^J+Wh֫ g>o>y.pVŲ$kM\}chp2G7o~Pl$zUd?"b7^ڕ+Zʮ=q2%wM=u?}g싋_LbZz*F`ZEdﰅǀiT V:fTիKcT:|3יn@VO~ 3ndkLk!J#O,y{-w悲/> W5\ڷ?~`oeьO[䇩 k87_'M;gݹ_Wp|tJX>Wd|RWp>}1tFyQ%-I fHޅ \tCJjb簵k>;%%Cb|N] ~,m}3v6:mˊZ ]l|=^Kx̎3SwR%P\C7xj9-3iFӕ/“l]G ɠ77ArTZĖRSΜHbnLK&֔q'}kG0Y-$ŁkvmB#O}\(zu-xN|8gB~mɞyI1H {%ˇjv\wMGVpk{FYg2}ڠ =޴q:v'GI] a7r>_xG=NP2+0'Wg* KXP HSaUq͵~6wUHĝNYL| l:nk)wzYGѺ]++W7--%_vO~1}G8l9~l-]t<0짿wz`>k݇?D׿c _CS3ZsC֛twdO+t?64rղ7$[/DSGOݙ~1mm ۻG Bd9 k%D{SLuZ],.NA8N/=_!W_3ڮ(+xG7*EFb=ME\䚎ůÅg%K^  >ᆰWgy+:-]c}BK89ݒm y[kE6ϣO{ZW %BW]Lvz?uOz9@N*pڵ~~22|vJɋuHҧ^AxOqEf[pzileI:<2;~2?w赒 ?y8:,Shm_DnܦkO /7|&bd/E'wKN|dm禋7nL¡{Ō.4X~4*F[‹*ܲz5Rpoߺק'J0v߸:hqێ ީ۫GEzˮ]1f>vs[K֗.zNOaID4nbzt Bsγ O:ׯ'~Ca{z[bRE|xMzQL7]|ż '|5 >:x3:;,Ƴ/|;D'3zetFؖoEqxl<#ߙV|o.<ޕDZňN*nAta: \1/ncqMWIpˍ sW+ߵ3Y^ ESV !0:I|[F^C@ t~Nn7Sx3CJM!ev) oU! Se/\ܼ֡f=;fFLmfUV}I>7v.pS O9ra#FӘ6cBȰO1r?]BB?5^{s 2X{Z`'LYh]gP; Z匠 @ʠeNeoB|F ;2 gղNdhYAqHxYS88p6={ 1Sl[j0PU̞,JU x>r]0vZml2u_ܛڌTW e9~ u S_o9 ze0ZђCyì"ƍZonCq 8 )›cL`?}twd01Cۇ+g!ϩ}GI 7đuyFBiW[ooOg,կAj-RùD0cqW;8; Nm]/|);f) 9oSqR3xB]wςw.@IDATCLem=9 8p)x{l=kQùYK:[ke2P( ]ܝܹ?f10Yvb:D0Li}Wf$øï(az(+)G;ף ӵ֩3c9=L xa(H*\5/N"%'1@B3fk_oQo~ڎ5A$kao])UʤzZ9rlfۚAq?yEfFu3Q p̅#nt6-~rJM6~Cik;] }Z]g3h 9$z8hFlKIn=i$ }mP/C?[.-xpX0kp~x=Wgw&T?O!CzPu7%p$_6mC9>I9t\z+\P5[E5S'ncCq(LP] @ ow~ɋ@㜰^2BV3VPнe?~4nK^p*Mә#qs`oPfznW?mΜ9=޻)aW}8SGU.3wSFU_9sbӍ52ȡqI9ݏX8R0^A;}Yc$a$A|+ϒ=u|$6/@Qqg`dD'焖FX0?sR@ddc&ݓPO>|: g~i6~p0{<9g' ߝ,ʹiG~=xݞd an@ɠ=1!A5j fKG58SF d^=&Ylnm^IEq}2 xNXvFU=Xha g+Y h `wN7ڝax42L~J/::s-]dBsd0Tbtʨo{UΥTPh@4Se(]cVv#kt(3;ukn*+^C_z[|Qm|(3tdqZlQ0w+ZYڻJ$r=KG9*=vi 3ڙѧF?J,uUd,Ҿv<_\ĴD.I cW97)#6mk%eJ3?m-5TW /b_8BcK;֍G]=}{YGVA*ϏL ,qt. >lד7f́; ~(!%tk!Y.;^]57?QYA|=; Br$gp-g/.5оƏutb? R)?b宿ZnUl`\?w<{OEI19Ҙ#U?T{^oI`]pfGu;ǟMϟN)ֳF:سUSwe>swƫ%H{Ndd̙{G ܇%[;l, J@@S= ۱o>GFw1$ZsiϒNU5#}^6zeo3mi&iє Ѧ=`ꜣ6g&lf1+hT4e={3Vt#tl莳. [jތ3J&u3vP†4/y,vA1F*aKnwz ֫9K3W4{E5ym##ϋ},8Yu*C~a E3/uF_ɮ,MN {vuLs8 NbB 2`phfztuWH 2o q`ŘSs{>Yg zt Lu}A"]2v`bȠt~VY>|舯.6>~64ͮq4'OpvlnebE3|Q::*t_P@}tgntLEDϳ|Oln*e m| :Q!yboљγ\.K׾()%aS'O N0ݞ=|q{&1%+ S*8R@1YZQ#)}(:<3>΢58c)LQv[W<Ӊ3uQH3X~;֢˟aqLʿ?JZ#L߳G׍֙xVc6K>D_hp ƗY~HX䫣O2}W 71NN?bˑ>GfCu}{J,O/pt~w'8SGBɰkM,1qsEe47z&/%⭥ǭ!qu|`}J-?@חbǀ_^;;k9KHȗjaTX{] Y6H^\6PwV0x EC{ts6R]=$Sf؂=Qg' ,JlG[}ϲ{^~/L_O?  /F?;Ӻn-]hMvYbxsѱ:rx֝#f$5{Sɾq䢒+zIOd._xVbm|ZY<4ߜ{.I#tk` Hfbx[ZWjl{QV77ɮ$ -,d4}ޯȵ>¼X 6O4^1mbSn4{W mzF>9 ohP!G5w\ P :\ynN@ 7m15̕@4_͞d΃ >WКJOr1FϊXp8誘=j1һZӎ,pA"+o ]ggq*(Ka T/KS&_۬Bl8 F&)yH& jArƆ ѱEiR3 VNm``ˈcoP*@~+Sš?0r[xlAtY>A=30wڙ "d@vkpl }~1F$=édNtsv~ؑy]oqHl"g\E@u L@J}wU[h") UF].sڅi!#A _rF{7\mO zvk[ު_&70\/#rT4x+AcC{xdԆ$mq=[Ftn)?m6dn.@z3iFFwdçϟ`ғΒn,PB`ޖYPR@gƛZ~Xoʵl`zP65eaNJAhx`e(ϩv9'7v Hgh/AEˏ X@8ħkA %XZFׇMzݸ%Wx_W.Y+-;kZ%l39"'%AZZ z5(K(Grk1l yxyh(]=xwғ" k=# t16IJkɾi$>}sn.N8>R-#a䗗<{'7\n]CUg2/5vo4vmk2@gT `$d2-:a`sњ;}6xpδ@uF3b2f[?8Qu :VUtkn۱ƇG4In2Sٰ .66=0~Mh A3وKF^&R `JY d]*2l|0PEk#8-M"ݣ2{wng _zok FGG_N M`ܞ<} jh3ـC+e?H-+a`ϟI^n޾7'ΌXG, :rd]tP&_y7/1 Z>؝j6cӝ}O뙫%DRvqW ;P;WUl-nxn2>KF0Qdv9NrPܯ߇{l,1Catu֠- ;Uu/U·M&nGZFO2'$GfSqFOxÞȣA3wKr4Tk}~ `R֔&C&=Sgƈq]v?'1fGa QUszP9^tws6?D9뭱nl!z8]¡f@z(wߞս{%jn~TIuͫ@U;E^}ް'Z"#̙ј="o?P)˾f[;nm1:]pa|O`g C 3RE: tC%ɡnM?|q{|vd (%$=v'c,Σv>d 3-5;xײˎ*t|yl[a:1% Eo.+aM;HFFmotեdm!:) a rXn>t7zd|*#`^%C_$+%.˻29#4WrqB:^ _rL.QaQh MƐCےlT:d/&Es?6ы8R iu$"Qոg/WëI7sݯ3]љ D~hma-&@mM/"cA+ܙ76ɐ$ ty/yž2p&S7g=_77*P1ocCt ~iS4p]Faa{Q*xrajη M;v?EW ۪+ɫk%῍A*^8 φ>?Nk \? 39s!h3 QuJRғY;諷L[?xݳ1t%A>K^`xsv`k%3g3ۯ${AO hDs',xMG\jزO-бۦZar^Ijo Rv]瞋o,?4\h%Xo$2 P5ڧtb717 }H%VoSGB<)P _wOgʻuwx}H:'@/d_?||rW(eV5ߒV=+7o,8wx.g:JstMhʰk6ٱ!~,1qaA*wɓl 6ӠG?(rwrK؝76*N i_>{'{_"[-h[v,a5Br :':x zQæB3t. qǐ=}oȟ잯+Z9Kw_ů ޚ.\::Z^ll>}ΎS7tښoIͿ" m_xdpc$vƋ.pl\Uě;1Lfѳ|b m oU~%6GO|w\I>|s=Қ=XZ"${h.&AY:qnO_d%VIDz|I0oSmhphIhu&,9KL;2hmH ;[03^T4Qd5T¾2\pwX}S8wNSKu.Q]2Bncd`H|¦z E87uc{\gdI:EK=4C]q!lGfᗺKHch]Sl{t#{Ae Id6"3C?>/B1)8b}%JHKgg·oFzImxmG4Efl Kt Tzނ[D-norŋ4ʋNϞ~w əxd>q;]\XF;>ԿvV{K bg)UUo/isvrE֡C?Uu޻dgsLl+򶤊{k_R~TJvNqBgUZ75UE:՛%lH`G(쀃Բl{nnor_wEtv;?Vf;DCcI^?:E[ctaG#d":Kߌ{ϾptˎUX6L7>vonm%c7F|y]>VG$%I̒CwFޱK%coyG d z!+l?>Eg/ _/.FdU/7"ud)o%miNG8ֱ<z]$&q~>JUhF;d1"a]q d7Zs,k޻ѣ:kei \=2>KAV#SўA!_ypr0zt86BtgxY|Ə%$wŢ%3&VQxlbUU9֞0rRn.|]2,?x6_u!;4?|a7n*\O?Ao~:\p…sU-Wzo@ӕg ҟ\{'٣̏ဿ>_bӉ|^zJۥ}f]5dUbF%r\u?GIC++Qw ˩yU֣cK}o.:Ȟ*}*w'TN8pl+tiGWWuo qIsVp1\kܬ/>ݐA\6䵀0~zc lj#ѭ{ fK]ؽ$J{gs\Z4D{|\`?l~¾X[ВGV,>xvHxj>+#]e`<^8qdO`?Lw`ZAP ]vF|Bnj4ncZ0j/pgܞg\>֠qhh]s3[WCf`R~ON W^{Hlgۦq 0AwyqPXUsF /41g!D/nN'/{a=Xf!oaF>X7~ Y]{L7ߞW-IMȥ6f>-|{L񗿞Zf7UIz}nlի'Cɲi ^[ ݜn;+8m%ջdCH ; Kx.5OGi*VͶ;SHcޒ6'5K۞ף Ӿ{_24nG jl ;JdZ+qE5@={DA{@p pKU|Z{UTS$xP)~}m̴7M8nl'1 r|"1p5dain"Y㢗1L_c_<ϧI2րΎPٍd#}xVwsx0}Άw̌oTOf7 3+!K}5gA΋Xg~$vVɊ W+Fְc^] ["(g7;3^X˹GN]`o]Qbb0;݊N77G6 H%$Z$OZ#˧Ս؛Jօ1;[JIJ\Stko}w>J%"/C|el|ƕ#l tj9*wB t [ߖd}C9ȣb$qbVxj9]dm[IXe>':'>Qɛ-h Z)7t w=\%hj8Z`KbȌ()`+y>x%8:lTAJA,5ܽx*̖lX8[fh m+f WZ [,Ntʷ4'sew_сW,-Y Ewoݎ{[͎0nhu|_38ڈ >"!et_n^dǘmft9>pӗv_*:M寇PGO>iw%\/YD4K ks"{|W/M )EIK+|%jnXszHLSˆ,ac,e.H|#sn?ޔq!ŘnW յ8]Pb!N0!PkTr/B 4lV¢12vP6QMLPn]>#. Ʀ}UNjI/ 84)Dah 1/&7 52G&VDh Tl%PƦ,  KBڍ"dl@`[#f=r@?iQBQ1iSRﲎLeڽVl`PBXc@f轋@yeVd}3?n7loE3 ƽ5~4ư6˭ h?} ޭ9 }Ξ:2eNxs2g](=W=ƹ=pfsoMQh1D" Nw2H +@>`r E H`Q&ret H PpƷ`2?=ܗ6?h!4OJ+7nO@pl8B >?jp52=)>~TpZ`%XgN-4~Qڟs[fZ[@[+>2㇁x|GC#؅@>mhHw̫ք0? efS|"ږxU ƞ};ڔHRfd 掆]`186y:ƾY_/y>C}gFvȒ U(مGw|P2p6V@1由#=x4|!m*I(OrhoAF!ښ:Fp|l$C$~F _/I6Q'²G;Ѕ*2HB Q24s^m-Ku PaMak}̸`!Oy|C9A(Ij!"Kp]z @+ #봵"G]RV;G[=>v Zb.r^f#GnȨmr8-^ANG'kMazT{&WbNxqddhrո%IY_簥@ߌ3x7wrύѷ@8qυ8W3&ŀ-mՐ9_xoh9sZ'zvR_Q}80'#w iWw8;CHhPb;;ΜŐ]HI\0p2?%0/]d*vˢ)vDq|#SF4`M]8lj[!kToOOv7h3<#qCɩZp-{`bE is-wew-X. 1b gG/Tݹsjfvƃi-Oȥoc|UHhkroz[ у4dⳏ_С?/|C Ϸ^_Tuf޵"20ǗF힣~M;cpF?QݕOΤU;<>=^e}R^+קO/_ywܨ^\K}M{}nAk7U@ǵN|tؓWCV8y"ǴNe9q4Ph8'yE#ov>> ȝ9y|ɏ2Gz}@j ɩc^. ټ3{v| yI$IP~g sG`9k%t( oFCo~YB6 >{o=g,n[ TClybߡ@|4^_Z&FQ-j+@gxuq_1w M\5KJfҩt1A#u 5q}>k={.:ǹC6yyؿD}6Ԟ'vr}}-C[tr%}\/AO?/#q[Qȣ_w9w>VRop$|x* nIz䉓 A5ӧL7oޚف"/ۑs7,G6O<~lTx=nǷ'mN?:S`ysgΝIV;w;yd(dAOyk:{<#xe||cp͝b G;TQٹs]JɃ%,/Y`p^?=>MOU}%ĠU(]qk/K!zHu%k]Mv/}Bg:D/)><ǾSiސ.^UhCˎ < l)@Y'Z͖hYy ~M6w3g!N9ArU?vM*A􁏵^.Z`??->Ćd?s] |7ovYabn*Zۣc&dUśc={{E1}:G~|Y`^|o tWNjv$ )ZYWX-[еY a+nh :p/#يy{o?,Бu.x*KƇ6$،Wͨ$o GMϤ[]+IÊ1Kϊlkwe6]g=a+>k_lu;~B؀$/G1 R Z)Ѻ7߄7EVq6R Mu.f;l؛snjRI{zoɷCoLF(crϝ]6o6+6Ck]G[m.^7 &l;u^y+Ȍ,$ȓGñAh#u ؟eW.~+nί!{@IDAT5+x֨ />}{M-O`-65׮OgϜv-`g Dg^.G)ZB/{`a=+%k8dw#ںe7UѢ58؛Պ?:h5&{^HVתUR+^uawO鰹O̭6dlŻ: MI|Uuni (ͺ$hp6mxM6hDaW;6{=Yc״d~d_أ]I:hUL:Ϡݗ1[m~kUZ:2Z=7cbF+rIFOۍ(z~ɜe:2+~\+iBYYo-U v779O#??F8a-{[Y"tQbt(xc Nܜ;1;wF$ǑW" [6-%@JgTaH h@8/lV(/\)(i9LXF͔QVSʼʨ= `ۄƈ~dHk=w@B-:;3o>~rt{gq6xk=qy@q UگhAƀmLI=pxf4DM)[WT; R!v opcF]JK4=~DéLhMymR( A8 #  ܜC17ABp'`7QA&lsߋgY֔,'e毥Q+AosapR3gUr>P2P83)Sf8=57q-Rbp(ƨ?b¼~q̳Kc.c2UhEXTL="Kcy0F:Zd[ -9RY1%  Go#<zgYT)ֆL`1zP/d0 @YH EY,{%{3g&ȴΏ7w{ ` l-e5c/G9-mZ Zhf nUN“ʶdgZ)O9x Yj=c[" s_|Z]d'`,|Vr^fTpY YFF$O23̀*Z -dc5-9phν2禸?:} ِ%tO; jM}'Cb\YW LB٧x4䪵N@w[kkHo-)LIvT%!y@W$9,MsIGU'G>٫|_@#Sw" ϸu.X}QH,+<~ ϫ$jsf V[d:n'w&|!Kf \ =c(Dg `PDE5 [3^蝑@!o}૾~J4ۣ ^ ENw=LQ5DG&͆<@7Fr1E/KV1.L6IszWK0GF$`U YpZǏ=mnLИǚ5"_r/BIM(zdh<{7e-xwG2ۯ[7&;uD:RO~:rs%ŮX '- ζ]cFF-}ѺDTjYž5x^`}#@ dKl;Ydzyp&,9in\4CoJ7t05:V{DX0cv]U2Vi P|蔢1D1TCn(+=)(d$8#=/ 0"Pɷ3x((#8eO:zllݪDzW{$WU/ P|>X`⃑叧UTSǖCϹdO>Umoه?\{yܹZs.t = H[\c~&N?\) 9wh._ͰZl* g6__ j_8wvؘtsO/?s]T'?|!$wJ-хsӥ%.~gyx3N/Զ7/H}$ZId~HvڪH~ܸy{"@dS}m-pf=,WJIQ fw>dzggmWd RGvQQ>.P') u^ :Dv5w/r}<?7Olʮ~7+K2)>1~"i|]6S5~/B sӻه/-63;j[Eɖnb7?n(gIx i)MΟRKƾO<;/^Rr$ T ظ[}aI1ڃɇǏv&oGpYaVVF󁶌Jw}V{ОD?. خ 5N?Q׊ Wf$PI9P믿mOJSWdٿ~1}wى#)E~2̏>8ɪϝ?whIKO:6 I:?z. ~OIL!#.WT>uV%T!KrRO/'ss%CGOZnz#N矬WGUG\ @;Oxr;ʢ1-9\pێep^ek.5_X݂e_3fE4;ylxg˭轀YI>Y:$ϺuI4Jl,(ǰj㷡ޠ}jq+'Ot-kX_W?+uUaCS4Ƶ-_|๎Hϛ‡)+=L.Dwa Hi|dDxQ|Tzsэ|u%SUZ1aVPw[<ܭ%>"v> hEd`Bf^1٫h; }"%iTwƍ3bOv%֢rmuRqSԟj"|ǸC҇s6 þKxo7銇sjrs$w؞JoV hhucFD X Տ7agt]7<]t%;|G 1U7fχݺuQgӽ֘hD?^tRI 4dZ? а(ٶ&ە0<&# _ؑ^?Yb"^TG =yVa滐%@yViϖMIpWm'N~~F/?w"Odͽ#%ӟ_L^KJGp@R6?}sK0:v">Uto7oլjtf# ?k[玙Z9ڔ 녎U:?I>N;ݨ?rǗC6gf_ ВJyBz|:>پ?{`&(}a˴<lWU`~󿸒Z#xi4LEn \n v5Vޖ?dI~i 2u=aݼ]I=$#x}4~|@~sVpiX*B[[|W0Ѿ. hJbH'nq5-wT+Uų _ݵsKi߮bg $: 7 ;G6Mo|3-]MWXy+BZa^㜳؜XuÍ~qپ%:֑uzGŠӱKz8a]ǧsr=Z~Jߎ֦:wwoLOk;0[z >ty߳ktхuM_oGUY7ĞLghn;QWpqu{nUd>!ɝi/a;| F^ ymZ>UDmE=t̵Q@}c?Ϝ%u%qǽ/6߹;>Um{nΛVSf? +S|s싟`fڝ5}-Q]!jQPbـa6U7%?E !녛lN)km2ucXB ˒ޚ_07V1kquf-᱐bsI`]ŮxvP9,xGkE+Ab8 117#-`ldA;ܼ;}KON' 4}/Oi`*[R#6fMۛG@iK!Do1 6cSu5ΐ]B14f@um ׏ NraH31'%> d*im>!+vkQZ;KNڙsxTԯRci1c sڕ"Y-8`r zV`mKk# Eژ)g%q)+{&X102$Na6F1b~ZG# Pr} 38˱ϲ J"g{^p0b̑ J32jUgF7=GLk8&:QNqGX췠u8=baL-Md˴(}o8:l Pk,?:$ <,ZFy gt&fj !Nusо͟GGZh%0H/F`߇ќGo\W j5hTexK42yȄ>aݬ;mъ3D)ߌKʣGH+?2_[ͭ=Pp,V_Rt~tݺfvecؚoI( }46z cBz6 }Lx6٦Rbʺ> c`hs>2U5sB:60MU5\k7砷7ƀGsp½?h^w5Mk$mk|ҹTt1w/|LD:baA/{]g_|Fx^1dbb_{]dk3пi ѡX8]H2|E>-^3ِaڏ:yK3cOV[Jn ]Fpr3Bk }9иN-ȐUUq$(ghv椝bس';W~G̺ڴpn+`_A+A9My*?x.U=x J}&[* &O7$<ǀ1Mk;KLdh:jȲxM_=tk$ɇYgku Y˂*W9َd==UVp ku 0,x9ツ Q{K7:K`Օ8WIHi*ڨmT[٦8B9nGIJ*5$>%Xqs2jOC+`aa 9%@p6~`%i(??%ڋ' vVv@9{[m<1-1E;n*'X.@ T{,ڰ-?Z43'I%!c2]Aې}Ȯ#W0Tap{g9=#St::>b[hiïX{.]7t^ w| $ v"?:tH l]=A. ٙ37xf7^{{';]18_֧{qiwh'ӿj/5p6|<O&Zq~*0grg裵rkyFY'K,HcNA`+,= &.ܸqkM;^I@oViZLA͛sgwW=x9cyfsx7}OKɐN\:]sI}Qہ:۫0?1GK'u歂{Fʳ!y… cLD}v9@o7v2TK[;UJ,?8ư=?{NLmqG<||TY`iχ=TdV@VZ^t|GbgG$_LudagKFng/K<"ozmUl5_^TF& Ӂ׫?xE^U,/Y;yf?. XZb-?#rM>vXu' ֕D~|&ѐt S>,K2vסOߎduCҏ2vߨjGBJSi ֬exM1 =653rSwTU|G_Y"dh nmns/~60ϣ1;6#ڳXA!_-T/U}Gs޺m:>LM@oW`:}\O."ypwPX"!DsѢ؛fOR|ͧ* lQ]Gr>r-v\ˮcZ@*$KEI~} 4&bgS5ւ[jKsv aQ$}෣tϜqKkTId^qϲ{FR~sVמ2V}=,仌k>Z[|[Da+8Թ}F;qKHܷph髎7Y XtBI%̒=vd{/ uAUsU5[cGaԝ|gI[Fg).=&mtoUs!⺾3y7א!K2SdnvEcÇ/ue[`bgvZO92M{P^_޶oz^b.g$?UT2GaM2q୭Bցe?t`IMOVdbG9u ~j:oO{`vLѓ>RYR'uQ}S]뵭Dxd_vZ:9Jwm p rGߺYsHse$mp x"UA_>A8w'}zJˆ%?ę[{_ nkt"/cSSr8lD~9yγ;\=nj/^"F̕Y.8H47FӠTv֖KwH8J?EM;amr3p8ٲGKv$nYKiC=CG@qamxJ6ׄ%<jA`u[k@bb.FE㼡ͽ_ CY{-̡CG15+F{3_HM_]{P Bc/Ml%Ĉd1-1zg{Qkղ%w,qSUo( oޟ漩&"ۭQ.CgGWXFK_̘aqp~lvːP u6-%$)]9C`TcăpTb~Ƭ*mlz(x&\AO Hmr3ѼFbׯ=3E{))hdk9 0N hnnJn89V Rø1p}=H'>c W7aR^ڛˀ@*}U{}Peb(4'o<{9@m8$c. g>O՞7fǠ4 c8` %'#y2ţo*Y9Ief]EΨ1qSx>2-%,d^W.^vW~ϓh58!>do;byuK3/_W γΪ?r M[ ޽2!=o1hͭcB=7i= C7ER2?x+ )xR2)a=SEx.8G6܃V08%g}LԤ!}|S} hV>(VJݬ PI9F9: +d[ڝKtΔ}l30i -GrgJd4~^mbS6޹ Esq@ p.K?wW} INԞ@vxM  =&#']FgΔ{9c9g?k$2jOsn|T=d% ?p`wF2uClid !i /xB-]* `g@xwA/a\#]>ltz:<Xi3l#l2h?y*b8tS~wNIdKD|ڽ{eS>׆Tc&]2]zOsue7o ݰH^djSg>"Ƚa&'ܘ35m@tth^cs/<RƉκˠqs8').6>lo.n2c'Sn@<ۃ.93\I GH\s7Po݊VFUR/{ZMn],_OԵ"<17Zi mѪ/L~]A;][{-7{*h\|+Wrt:Yr:va|E밭1y׮?2ƍۓ'ӥCKoG#\|qzzH?|g5wN?: $Mm80.~k Nxa=+4u1ƴ{s%h~¹tZ~^[\zu?9!K%N'w9 Bkxߟ/PAvnE`ԃZZv7ٴ7wzrOZ,ξhM8Ir螜"|R8|Ż 'c[Aװg@VPW ?y㦮2zG%,`d #F6)7\\kr4j:}%Y1aGdոɲ&9 . -׳2Vn߽uF6+g?~~^rmѵ.= dc5gW&֪0ZKhnJ{T ܺ}`okSLڱf3,ޓ.Ml?YgObOo^/q0;䳏?ws6vײe#?z= s킎%~}Gӎ?(6Jh|>qjw2 F_*ym$D{w|˾ j˃,pqà;t)͟!3gބ~U{tc>Sh;Z3`,d}L F} ~ H=Ll.!2O$dg1^io 1Bm:79#'; X#ĩ6ZBW(IhmȩQ3 ̉A~ +rt&>'|,wx}Y@RĖ? ߌoEZݷ'ҭߝ~4ۙ.X#ag "aGݛW`Z ;.kLh:7F҃J7;z΍u7x|E W%nX#\kO[ZAW[W_U ѩ*s딓/~/ӟ/k{uAǗ.k>|~܉^{l b۳G++ H\J><-A!ڑ@JuBg]tû{$9uIͱj=:?nSPwe^ųoSU7uy_RLG>Q.-6GEɞfnsYx&!af?dWv޷]T:늉w¹СHl!AW @Şscm$ZkM[۷OJTRž>75;Y9뺈 ylj $ IO Y,!|  \2³oclJP/Ɨ{:ji4Бtmhu/O>|7yu]d9orNfc;wb c]{J{ZPAIo^:K5}c%c8O{%t h׭}gO_MG# *}Z,unM.x[ }ntA]3,N3`GTb1Gm-qc<71ËGW&=W|lv͏adM:U7:=lNG &v%co6Y day1%7Ӄ}8EEd+^FKFwx_U)߾q-l:w|vh]vs;%YZ3@6"矞Yr|ɣl/] ^"uSX-.sǺHjMhW%ڃ%R`س*S*F~:rlF/ *TޙC༟ɩa Zl~LQ57k VͦHEo'8[V*U Y]㠪|K[Rdlhy>Uu1*T eض''1FA*UR,KɹB Y1 ȧjS,a`6J2lTIieFQ|u)aXX[9RZQRy{k/~̾㵫ŻX { #27.[&а8H2ebf{;+~ 28`pɜ*kRuKjɆ%K{_À!-{avCMYLc FAsZi-VE2sy5v\Ào},u%W3\*!Ӷ-]TS ݴҒ!Iذ.ƍZ5LPd~^e*y$3Z uáaBѸ1 =)v\eS$;۫vz tJy #z}-ͥ Rsawf-m<9=wF6 >Dcgg# $wuc,ֽ=x=<@4CK ̼}XƈG[֌*ͯl[ޯMKˉO.l]<===ehykzH|O#[c??D{h]XbM4eLnG?2F t3VBx&o!+f@!o TMP{[g( C=R۬ :sLΊikNnoNF&Jsx+)Dtł^_16/{>:2b3#C-Ѐ޵{7i4w1azs{W7~ mf H,TGqDn..d8SG~@z"&ͅ-=Q@eFZxXƶa1=3f8:=~`P.y(!ၜX^ |Ę9 *5O-w4jX&[epwy2 DzHZɱΓܞB=EQ)Np]A[sBƦ,qfz3 fkǰI Ky(h&7Bu sn۝]5Voѫrsr4|űfOrz0Tdl2e#k?Ue|(K f8R =rk$䄟:uv pdߨ8ڈtl$<^}:O?:GQ-F7.:xP3Gr|Y%V HkS'OS`UV<@(:=xlK"Tp8>$ڇpz J*ao\)8#qߙD o%ǜ͐^K]V7jdhU(9D'3a>wO2Law-0vUnZOT`-+ YsLxޓ} A3(-UbL܋-)Q} 'ڶ Iݑ>WS/Cz_Fֺ{̳2lfAgQٳ;ux7^/pIA*.U8DcZSe8+.ۖ#bq.b{_]%+9P%ͽH^2${`݋?t~(`{l $|{Os׻X芾ٿI6-Yp69~Vim[AM=ɵ-=I@IDATvKrw&#FrABFmjZִ7%Ʈzit!_$p]'b%S6/bjK͸ͷo\%<й٣/N9}{=ڑw7jU=X~tAowOa0Nx "ouO ڡvݕ_>RZ.`;pj-vDCvJ]01t˕g/~ra]K6럜9qAJ>@mсP%a3xNz/ˮ1xa=>  $iȼd>% cF9|&[kG1[>@;C/Cml]O7[;?}0AX=h$d*d(<}-P 6E X\ ?β7R|◱}7F4o"( nG+]ۡ}̑֞P8aߞmFAJc[)6'laӌaϭ{<*\FMvC.4Q 'qv_>ŎlR>){;WƾdA9B́ (X9{Mo-$Pʈk=l{qi}9ƾct`G 8$XwgS 4!ƴ O:am<q4K_#!` |N1G;kO+"t5_@Ǚw7Ty u*OiosdIyϹ)S"9/EqF%mmvBG|WK|/*?ΗЍWt2z|ܬ__'7U'ccz}3vtk)LIr6lĢ$Wzdώ1i1Yi&aױ -z-H^ 5 ;5{D@1?[y:W,Ry_C*upl;d|sYz[: Cmg@=WҿsKkޣ KtΓ~-ѿQLg$p[Z͎adU3yd[OO3u#?l:sR ݽic!"{lϦƊc)7iv⋒^澟Nwgo+j(yXɩ`rQ,>SjwX5*)u, YvWB%ɤ%ݠ1{&w3kaH7%Z$zΠ'wfub7~zqW}&;v̎tH zHZ'SR1Kǜ%Ov xe?m;[a7KedKt6߄I'qӗwӣq]Ɲ zFxT~{~DwHflYǢIVzUܡ9$%>xOg`Բ}( AYج"8ګmNMm{8]@`ڣWXSyJo/>] mPjt@W0218j3dV1|UhEP~ n\g0%@k @NcYQ9%-Ue)ec,?N` Uy-c8 ?oO̬>.X\E"$[z^xW?9>Gkc=VPh¡ʠ\36G,ֻIRףW#H;x(!bb`̨59z>i3 @2-d`<[.aV-)CeR^:e/.}ܷ3T@ʆ(ks:1CŲv)1Ҟ13^`nRֺf#y"8h0%!SP;#C+G/نAx1!U{CFwѴg&t8y2pH=d 'sȁ)@#Ae,jײ|jyk<tOGV[=-:/8s3, 0g)n_\6gYr=E. *v2vM*2s쪌ڣUȥ*(Xt&^y^FL Xhl'rb *@lvݤV8kϹVP Bsytb8ろz&zg @3Mّ)ݤm*"ɽ-Uvb8}W+"m`!RaϮA&RiFΚV>oljJ3I;|O2e/oZ -.j74s8})fb{c[́4m~ ̳v$>udhL[;8w 'L~3\c.Kbkzv.p^yq%ݔlr.(/3W@!\:D;ΖL ,0m{JqzKY@+ l]4hlRAK9ֽEod֜ ?k`oG^dX: 0Z4ֻVYr8}_ ln*GC.QGǕ/`/ׯL/i@Zk/5chs@ @rmDIpO{T#.XjaLDo5}PPu$`qcWn}5ΟڿLO߱“ۛvxPm),9wFqW֒@ ɓGI sgTEuz<\{ <gG`tƶ١uRyHViV#(&O>to1dsv9,uNUþYyNH+K%UWG?~|d ;W9 ;%*񹹀T&׿OK4xB>AKH ] 8l1Iv,{!HInt]pvPA-í+: KV$iSNRUG=.9ØUK2 ,/Ié1mI8n5v[-݂tċy𼄗-@cl[;t]ŃT*I(]kIš_=n2UKO!t*` aT%efQ}z?2H G:yJl./ z0GL։ Lf3ha]ePzY>V u4BO7bl`7m~gzM7,rfgszB͵4!v, Ml_6a${[61BrVkE7Qh5CVZ mop f iʞkT]+`1qx8 Wkhd" KR~H>`f1ٽc{23".,#۪dPzp{֍KF#|i3e۠#> :/m rO;f8^l 2|G]H'|U4^0J}*>NO#o*ۇtvjgXBemTG6K|؟s}<ۭgBKtd؉UX{4@~PwHjϝ7k§0љu9T"-G-NQkDeO<= ~s ||vvlĖ- ؖeɀM#?`*6A W\Sh$+9@|>k1.U ݋@>5"¶f'+'iaL[7𿾻reߏOF#m1o6^gw^0|w1_hyz_^kbƇATn?[dv$辪a@ϻ#_eˮ7Ç(q4\zQ>-uQ~>f2 cwŷ_O_3L~{w yqǟM7nߙ|w|IϿKrC?/V񜝂%pσh8 .nPUtgIg}鈒׸|I_ut%y;߱Vc@2QX{kUoT*V{}!Zv-#gA ßGI#GAc`'{0.?0|'ҭY4oN{@ X'[6tDFML" 5nQ!kthdBLNvZɍO?KZh<[}꽒l6J~%5/vs6l?´no6HMcҬ;i Ǽ^C4.G?}5F_N{f5srdcWPyK&HV#Lۚ H5Ԟz>ޔiAO=mhr$]:~F ̫uqGt?VTg>J^,ID{muܬp"l3'G 8'7dY<.іP]l\2?֦U|nݼ5Q1ܜuMp%ё7إ}3wӧE랊`̻Z'\q?:FN`hB`#k;톾QJi>|[7$f$h㑎Kḑfxq|h]@i͌VGbTv8Os_k- $\.+i=8@l4hc#Ej~r/N/3Jm=<Qwnd=:VB_]7p>#@= I ,$ed+E?eG)Qbah 9?8R"(`58 ? P=VA8}`8|Anfpv-v7kA a 9tt,;4J@0|ͻ&FXG1C3+DD[Q> -.HeC͝#Gxo##n6J j-mpf`?u@Ĉ@ܧ !Q l6gNE6} =UMsٷe>bjKMIQ.-<811U=KۧdT#҆g\T-Uư-ʄ{z9`ݞ=c-4N`ԕ -׻1vcv0v`,xxMf(zec(ڽw 'khc3n_QY o_ɾR9Q;Q2(͂e2\S:Woޙ5=[,{2#|}<^ v{h xbw@%ahN@'OJhOy?xHu*Շm %AHPyrߗInoG.$0tAMpZ8t@4:`a;ÑVgH8HojŅ8\!ctkQz@hjcCw}Ю ]H6KJ88tDrg{*^eezk'U?_4߮"Z4 PyY?q`?L7ro^P~}$訊\b:SrбK3O k/<=!)/1 _Ĺs૳g/}S)/~qd׉*,k&@{rw4u70ĉus@]e>I!Ts>>Ν=Z xi٣ɓb7o (*"Z{޽G`ƍ;O `Oq>҇9Kg߁Р_8[K@xdf s?pLbsV.ܑٖ{ek=x`y~/PڽعǭF~f>}{gOB=U IAѣ'#൴|~v-fܝ.4,Z @Z/-q}Bx&f5 tkH?أw^6GkDd9~v-9;xAvg/Ǘs9ٝn`l3kNUlX @nNve^$>n X]kiݛ!ir}\G6.dz$ѝ*G Xu_WI= ,#s7-HZ@4nONj@`/[SS |7{`~*=CN&i'vĖ"cTaGJ8 `i_Jmv P tMb9ד[p2Tt% j wqd4eZѹkkUѼLscHm-#bKBՖH;'\喝lUaCixpIW5t|ά^bT׻GtZ .ޗR?_ ɞgKwvǍw>A+kAG<^:h 8: FP,mEfFw_,ZAedL#r`N]$%?ܷ:ucJ:>:hÎ omJB~$Xg_ TtR{{ ٺ 6Γ' X%6{wg@Z5p {hc>AhN/z Gh^ܞL+V3?+;jj+l׎ [/Ϟ??< |\ZuV.U׿~x;6E k_E#AUPçѧ kN㷿d7k yBVē #J'_~Y/Ӄル߶%#DZ.9ޭ;;xٛ5i}b^Nb Xօ\OxZ?h~`b0z/UIed7#z`G۾7Zh' F (_s/ GgQ]3GY}tJ8P+5IFz̏ VGgkXO+aG;DpV 2-b _7nI%Ȫdf]zpaYn꾰-k(BA~Kz^!&׻tl^JC/JkK7ۨS D O. lIT5-ivo,m=bZ}o$ȜIkw/_9&l$O4XC]6WjnHulX>zЋYjW0< C$ٗ0QO_؀A} LtZؖd L ,HNhGp{xi-"Z _(:swDҙCv_ [޿u%%l:A3+LIhBRw肞GPP0ƕeCVZW֕nl=mZdnr{Vz:162 lb]pxe[zv,)yOܝ*ϒ)֭3svT`lUkF{<=&k!~?ؾ~<|dj),4v]QzE|1+;46 :?fs双1UUm< ><ý|;xK6&Cfw̅lr~zk:9q=W;J3y|kLi:># T^Q* G6:q]Dƪc$fEK{%\m⍇t<|rq4~BpӟإOΏʇԟ}vivuGۂ3=9al-6?:O ЅNSg+דۋ;|Ҫd_ roM0l:n \j#qtLzxD T? {w߮ciϹ091uQANZ<*9ud1~:wD*G%P#_&߷<1_fz>8]ˆh'C5Ϲ> ضzxwtDg%on.bGFEKg']yZ}_NT H3#.z˺5)d)}-|e7h &l]2⇲=Р) ՍѹIH.AmՊ&4.KbB ),F39F 8O8eDX;/|-0PߦH{ص@m YkG2hcckYp3Cc|%a0>@ac~:-$9*"Unuj2|w EY2@' (JT6#s)FG8v̲m9_̨ADя=%j 0.̩%%/4^mAG]ځOB|fldj{h{ɘ)@A-d=ȀѸm#P llmuSb |ΰТUg̜ޚP@1KRhF1Z@.b L&zycKs|E)ٽTelO6~XWAA/N@T[&wj[󌅅& %b&LW?#*$7;d, ՇSG g4(jk}sV3$T(kQ &@@d3-3CGT jG"Kt< a99<g!~(X;$p,Vbȁt Dٛ7U|2Bb"]eH[#G;ВI^ص-|5sg:" C=wz?+[4%+<(DW41RTtp X8R"vƞes٭e\*+>fۣC)N`ɪ؛d,92%k;K:5/ Ȯnٽ9<סH[ssPP{G;/Ds*~S@Pi&.Pm'M!g83nOF˒(v_m] | 92 3p '$_S=uT5` UŚ C>Gk׶<"[-[GmvgꎜkUlyYwdQə/ꭿ?~bf6ʑ*ު |_5ꌼG7kZ5@e݂gwݹ}sz=A灦:<^p@+8ڟ=_Uq@?sM|O>4ҋn޾B['ahYp Ξeg :SN6|1Qh=~jdNw~[g/*y[rU*3 M]׫7'M' ]Sp0ޭtluw8JP iO:?O\q}6?@vHyŮ6肙Nr9~Qx DH3=kZc~&[_v%̋z#:{ޘ2$i8'Zgv/G'ue ׹i71~Hw~q[rd;Pd6=ӽdݶ|U7ϒl *sc@-ka.o>GX֗4 ?с`ŇR,D dM>- SgI1K8(!C@+K 1'-W%3%a)- g K!ăa0 㑧 ;Lx@ Z!V/[Y~qKi=wo/wƻ q<ə9ϯ: ;WW@soCkԖ޷Y%7VE'7̀Uү_^|gu. `D'M:@Z]z6Oɸm=ݿ[w<΂tx \ k9ZaSߥ_ F\ɖi{iQJb:Y =5Ev%}Q=i o.M=~9|LuZl" ^aWεxu`Tu0#p/ c'OO'ΜkNmUl7^5$4&/GtLOI7K@ﻓ pUϐpÉd, ]q tA%x6pְdN h ,S"$;kΔ@Ҍ6E=qGӒ-v=mc^mhcЁBG렭K=/z'7=\@oiNO Nj.]%QH,aD GүaG(W-!~ 2 = `m C sTh=g0'ϟ7_@:nwe?ӱ: g'fd%-GxcKfجߎ sVGaN-Oi}^k͏,A [0G2 f6G)6o/wm9"J'c.c A;kg=m2:d'ײۢ_>d G~D%G_n2kN? B2ztJV*cqw@{سdf:3GBкkU&+o4i-L?m>au:;l1: cpdkY^c?G[˭Pݝml0/Aw3ڂC#{nO<(Rn{ulhZOQnG皓Dpfs)9" _`kyV6/u>W_ =o[z.u=?E 6bNVpڠ7Y Vd-qd[cX]lcǖsU&ASO{`~Xِѿ. MwȾKfm;-;th*l,f[?BhK l||Zy_W=O _CQTLWE&{vpڊ y_=ݼd7ݵ7/a*GJx3OgREъđN;=}Q+ũn?\3S7gXCg8vY@I-ƾ}ujM,=ϗKkuHgn,{Js:?_L幃[/^EWWn?oU8tӟb%?ث0Ltz0G%Pk bFuCe |OĺFr|N4ߘ$7$ +$}ۘto%ݽ{{4w|1:$ d -\Y؜.ys&m~Q %v6~'kśفi=S`GX)ueT#"‹lo䂤΄<0*s|b=k@g52#.=Hk?B:l8c۶L JG@[2==ɈAȌpE+e4Fo=BbР|fP1E%-5sЦ a<5 i-5B>Hl 7t#=i.2  a0:fM-[p$ Ay@aS*s0*B6}YcyRd5%U"øs V%!D$0Y`c7:\aQ1he_Z]<ԺnFg>XT}p¬ۄwծ|:7xqi!mFÚђ5&?قp:&ۆǨ2M PO󬥡dD 0v' 1t%Oxyk<'>ٌƥ!%&(3>s1@}"@1ʂWy@j9ד7Wl27L}f:CC6d:j$#LLsaGmDݻښ_jU٭z^V NyN9Gֽx:Y6sd^_ _ uoS7<5mS2Ğ*>忚9=xS2ɩGtܩh=&ٹ=h-TtrI< 7|[^[gÃ*Kod߹u@wm|R@bG\:Zt{3 qJ"9|J`4x(T Yׯ`>DzpڋIU=fٻnj6́H7g8~s nܼ3W`TvۑT%P1?ZٯWo: ; QpUTѤ0x{=sU4K*v@o7ktφ޿yFa;ë'Nl[F:[b4BNloZjtVqϞj/Ν{%X<.]LCL%~gCFҥ +_i/_.7V&j["ݹ#`;9g\spT~ϓᄏRζ:1'˵:Y=U3]/TcOd󰽖TqLQBÅs;G';<.~]j&GAS4v"GJ%l,D}v a'vrL߼/1\Wb)g+7T4:Y~aծw;]&gyUU;VO:}#9#`X{k =OkC 3]sgC]/Y8gkе@(t8>fw设gdޔșJ>ɐwɞauõlѨ`۫8h xY.BkG髗\{әv}Cͷ_} ArJ/J:ufw`Qyw)FE{vF)}dY||5`xu;M 0-pb6>>"zmϮ+2 ˪ 2BvJՕ$q=&lnudY}0hVDE u1`1بD{:9;ڽ֪XP^P̘T%~t|]#Y#ے:7uT{Ai5ws ›x㯱E EǢ{-%m^OFU [X<>Y[Sӧ?EܬC ryn/`{|n~9\$gGlNHel~O/ HhCĩ~vNf4WuW=}gIA|l,ٳaQ>ç nQk{U#ب\$ >UY(KoGK>nPR/|֧=w E sFrXv(d=$5{o!%9t'uġG62;C_5&[qҳ|%<ĿcVt+|?~WXwZp%)!(}#5g${! $ykUIc\ְeo ɀ_)uE V̺&y|aXAt3}U[ 0k ط#W$ ==ř>K!56%TmY&D[J{>]Bv~KDRkMt@`&ښvh gjo8(*`#u.> ,XQpo(g*$u fTB[^MlB8j ^]݂ у}7m#ө=u7z_G %njͭ%9+>6_ Zl#8Y`? ; nܜW/O0 $3%)'P (sãdetIģ@8@`g(M+H6 0S}ܙnj Lxg&´]?&ӳfZ֡w&n٤RM^n7?KGlr/xC\V]bA;:a&=q&7geC:[I#$%Fa|<9J^8y|Ϥ.t==)Axs4] )ߴb6  u|8"3'&쵚%Kt6cFNNVNgf#I-paI62nC~ڡ!yKs)L 槰\]]k~#Q_̆9Aغȉm|Ĉ>)=Nw-Kߞ\/ԝ+ǽ.7tTtoTqU~|s\tgQy4M׾]TZ4zzsjUOIRq|W*vh®L'ow~>hOޭ@:V=Á n;=}P"(;u"Id?tɞ$WhH@ОɄUa"p#`} 1 #7ړ+A -r2E%6[S2٥ V-ES2]m,%Bi0P?YƋXK2bf,Q1Zd05+~\Nu~OIim6hd>f oAۚtn7dy$t撜mC)}"jFxr﹞#[ R6_RyBHb?T'#5 y1֖N0{3LY\#Q3(?S:c`$uv͑ 'hXǜ^fWCJ^Ho8Ɇ s<9F;/} -1^In~Y#D_@G`YxA[7d{=h[|Ɲ_+pA%sS'kӷOV<.ITt}N=w):źCnu[}v./վg|~֭dWׯ~_t{]tdʃ Ɍ&nFSkɛʷ}nNg]J.U<\ָg}ҥh[pǀHZ`γ::>Dz~\rjW;{FKpxg_`M (/ڣU:=_~O'/n{.UKf:V, 38!8MF8DB>\vټ'(%QIvz>?Td`#KˣPsU,Wu(dnȁrs/=CV$wM' ڥ`*?=s-ޔ&t: 5>]+@oW^U|KI(ۓUJz a1`Qzq:W/ xp'R-PYrAroG&Cfkh~%^fȡ=ߘ6ɡ^|IoQGJ| :;) 6ͿgdXj$Kwgy7Uh)y/H'f} aEۗ]e% Dΐ7=ḗ܅3CozdJ !yr?Vuvzxt3otɁlŎzHo.DNj _8U '8 ߩŀ%~?}/ կ'Æo~?}zĽxht\ը?}mIE{/|i݃ZpgDo+%=P_/: KJ|Z3?׾l8&0ds{kB3fwG ̈=mf{v$M7тw|YZWPQ7>E=pd"E~tdnd[k2V*HGdzw]񆍃2jJ67$Y>.|ɿtpa1oAdKKZdK8J@g6k7H^R8:t%Ǿ P%PR6ߝO{FMu?mBgzb^+YϽE6pߑhuGa3wTqߎlv؟X+ɇCLkى }Tk4cg9^ NG=Jpzo)gJw6 fϹynIW ɒKϢ;?q|-aY~)O-$wkobnMTə  UN8f`pt TɩO?s\6{d~9N'S/aG/6/^ozs]|xMLrZ6OUKJ\̾DgKUPEȅZj{Q{PcZF찑HV'aӯ' Y>p+<ԭ v#2sɶl)sA7܋&:f$B :Esr Qn2_gk뫽ƻCO?&s7|q헿ɦ{+Ta^G̾|i됐=q=9%t@TO IK:B_{M$+dtZMfZ,Jn셾%5C|O 0(,,ZOFC_4u_o=DjZd 1!tv(qeN2" 3 tXn2zK}pH:jPOثƭ o/(yٛo:kao:QI;VTo1!ٞFo%8cfB ۜqLZ?=y-܊ @lxxN cd㺛+sDtw>~9e¾x2tr ӜnSD>j6C'*sz#޽`Ɔ RrjAgJ6P7>9=ixXҵ[8`zlgK:YKoZ׍@uvA J >~PEdv@yݹ}y Vlo @>y7;NKػs֐}ϝ+WoT'#s.\hm)`: \YɈ/~ ؘ>xl7\/O޿ZO.k <I/^ѷqJ@p$/], H7}p+~3tܻ *l/-A]𱯭ݮwpzܴm}~G@nkr2[WЋݡ]3}b돬xi[(;L`RC3zݛ5G}=;wULaHT#Tо +BЕB $aZ␜kž'DbI @{*[nȋt |gSъg0<&ƖVCQ~+be}@F7n~::: g_Ա5hOS(?;ԟ_Vh%w6TC#]}Zi/2=MqqI/n摳&HD^I&ɸ_asfHhA U:h_b$R7԰ISj^4*ȆMrF: ~[/^" W6kbvB0I]1AⅠGij8@{h255V76փw/~m2qe>XmOIyƺ^P9{Ɛ|D%T=&XYCh5*g RU:F$'  oAE@ ]!&K/EOy%׾?"iBAV zth(?#(C?;; V̀tj9N&k5 :{jD82dT j)S@3|[.1 G{psnAi&w刴f nd[s:ZIb |m/ =om✲ |yMk`FZc5f[y4hE MM&6]V l,?9=4\infu|ek2" Nb&[1}۲4ŭ#O2vqa3A QlYr۠.wvX#aM T7]~*'y_5iN3h>zCd3"Gc/_ ޵O 6Or*n^֟Wֹ~KƓ"H|q2@ғ؇ߕ vґ.&[G1 93LSEm}mY+Б"ykũkwF,vjI߱K=LP|\pŇO%V@B8SW95AuLhIc3 ^?^EE[̳wx?=GI]} W[y|P8Cٛ7؆C@Aulݑ֐<8[qۡ!?O[:CtgO>Uvk$MMlA3g=x8=ldJj9~ZgmfL~5k}}3aWt]$ nVg؎v`sԐu$aDudjJݝm ڊsFQWH 99ytUb6tfa}E ýtM ױ =٣s#/SGdl4K3vikN%`d)xCmw{'ձCUJn:x|l&xul3mq[FvgVoMk3h^ k_/+eowhŠ<~b%pG$ngBG)S k$$櫝~LN:.I}oގ8RӪ8q mSx5 _@{PK fmI(LTQg2Z~ZX^ l^;$[z}"&ZoH1#B\إMf;?DP aZ3LsR?&u$ >J:{vB_ dY]M&>&y K+AJupGY8X֗_٩t5#I>Q-.ўgq/9]&('1 CYnIl$^z3' ŧ?O@Ṃ +[L^FƙcID[>Xx</lq G ܫkl 1y՘IWg?twNz:g k2:>ga9GZk7(W8bGPgщG]?'P,th᪠=Փ3 s=n YvU(O/%-nΡ~l#ߪ\㽘c7D 55O<8秊1- Tރ86 <gwcm:?1qdss<@)}\Ш̈WzijġӔOKmGѷ2w=ҴUzW}-;N1fyIg@'Fѫ(ӎ+W^B[ am1OB&3΄3q A}5xu i:Q8 U2mU<rDg>b)AF8:7h |aGe"R@(PNX=q:#sbO )g[õF3k9hB AH9_ƀu%sT1`^@E`YւJ%@'B^HڸfKjLG1[^0|cjՊN} K31m3-0jct=f?LAu*rhs*QK+75"(k t̮(~bVaP|h)ޣ ]#öyڄ];┟,=R*5ZUҷ!>&{)/Zi诡ޘH@ &ֆ RưBƘХq[Xu^r+EЈs #VT04vB/̅G%N[5kg<톄*[ŧ7O^_~繅LQ]u 3t|s0`!`e`Xt> Ւlgqv Yӈ3q.p=M}pL87?= r{~f A甏Ґߜo|nDy㙸wj0l!KY+ݨd_㓿|7=6AQѿ!OO} m10GT}C Zf:B>p-{kd2lц<u0e*5mn=7E+*V 'Xؖg}W_;tJFùCPŠ`H"{L[nWȪ NomTMpJlInVUx܀6V%[g+o1V&N(+i:gT ,xˀ:mo V1knnP5ϺͷG` :;*9?|r*c6~ȶ-VK:Zyvmff✎}f`-gY"ɯ8<&Ee+w .Nr ;0xUUv6ukL?ZG^ܞe3WeMLfP3R^X\}I& !%3TM)ӹ=l9ݰ$E4f%kPӪ!"cO+ m A*~?N$x?UA:+|;)nC(oa:[!Ճp]ݦ}m?K[N{?HgU6]_&ikxkۮ8.Dٗ HEG"zEgk@ҖWc\|E%ߦE~Fnnm?/.R J$ O{;8oVz.rnCіڙG|mzv输R Yܥ!- hs\#e'V <|GҋO'QwDi >A')A /"|߃>?#9`%2 voC&./Eg>`]޳^LDY_ 3a5"EW7bk8z;@&Db:ȖНQdii jހ>=|͞׮$-p +V8L9D+LUʹjжk߻V+ Yn ɷMH B]U%FpyZ@iHt >Uڣ'#M;ߑӋ^*!ٖ*HYЉ9-$Ϯ+h>:ia ~NM2C~1 9х dx; >n~5G hqxWqYܪ.03.&A1<AXE`씉Q*CЗ??4aÎ4bgV T 39Z_AumP[,}|v޽$Iup|0#'/]<^q-$I+&<drʢ>Jk1 K!]{HzҸ6;aΑ Xs8p g }wIW )9sR?y‡<-YG7=>>9SX̣%k7'e|{o:& ,F Y.(d|˹\Uӵ @IDATyNhj7Pa;Z{v 잚^/ dV?gLITvmB\8 .c^_ /u˟lOZ4Z/1L ]_¿=ln3kj^q-*٬5αq&<\*^N{ձ;X9?1"c-cbT ҧ{u+Ha\Y+-Ń8'ņQcLpXI{oN-o?:5')DEuuUC6XNI1r;/EBvҨ2NR[ jnBϸv-nsخKasU)AVsٰ^w<[m!oKwxdr4[,oߒÙ]o`??mEs,i #l\4UKO[֮ TdTS|Z'ྕ6Q$kf|ay[ure4g̓Ci/Cнz&U40RL[GpVߒ 9}B܂;KtM'ěđ%xA (꿛Sx1 I j0sJdKSD کJ&BfAs_5 ΰkC({]F2.ѳ9Zو :]d10ܐ4J` Xɧ>S`uqZkh^RC4-&_!MO 髙T5L*rFE;}u]`rE|(qiɮմslRwe-&wK{\_Ft/櫀\J^a?76&^ >je`> 0p#),({ μh`dFyXQH<6sGM}] gO2DgQ`Z5r5u" %]Q~Fv B 683 L /1fU fF# ʴΔ: 4,TBTS0\6c8p 2+_X|TrL2{@;8d?b.u,ML!r }S ls ؜U `ppdկ@@(pU5m;3HgM:"dg]7w2`5MdmwK'2TZGUY80*Fri@"oaAc^rtHDwv0 R:yÙXae% J]\ag?;utۚ/*!:us%|15J 2,?9; 8ó[< # Y8M74ȣdX py NgŦy;SQyrXs,b?F@ #{VU9|Z|.a/IhՀ08k fW4t xzƳ`{!G8yF d,͐o6ahh{\wZm o4̨w>z L8uΘc12S@wQ8P!>^VӖ G}ײ-x\Iz0 49J_|'-Rm3 1 HnH}}g!Myҙ]t&)u0Mw*(6pӍB,?4>=;d|ɍe3fa"?7XG;sixE1Ƈ ZmRx>&я-2*Or0cg  'r [JAwS lV@zu$349c @~v7|dcېxuSU|ls8[ 3ԖnLA? "LQ#Y?`Y0=|*r}>3tjr~MbйF>T>Pfٷ>iow^Qvrx\G~X9|t]}Zݤ~>.>ggi}=8R WȂmeDݣZJxQ2pL NaVÊZK=B &P#Q.ACfB؝ "Q}3tNE#cU-\4ۮǺ :Uq+ݪ8-Kc*{NNٴf:W ιᙝcn0NMm5s %,YJ| hNgi]`$mme Tm뾈-,KVZx{JM2߬`F * 6i{]kNJa'G8\ґ N)1 4א3V+gv N Gɠwf8I_;~ʃٙ:KKK8'T ъ,e+TRr=pu8Qd51,@JsVH>dO666[Z$Xηzw$U |W+.tAer NAGBpF"`o7O"t@t[3jʣ݀bW mwd/u4f6pg[QGD󯟿D,61< Z( &ʂ k3+Gd[#wʊ,osH/@O_ ?H- NMގC~aC2;lw9h۱at p}8FW#1׵zLrӶP1ȹ`"e`Xpwd 9 3Xx}51 eNɀ'\Ƞ @]a(D^yڇl+GUhx=za#!Oʹ ׯބr>- hnۉg ku+KL~}1=J>_^^JP:=)bo_Wا9M\CedC@>oo3}IM>'Hn`<`a$d"?~DJ 610Ȇ,`sS#h`YAY@U麌#$9|kLFBgj[m!Cqv|pL6hUjw]̤#\?:H?$ +m頽 ۲xrNGk̀p" |2VjQ= :{^PHD5]mm6ZȨj`$M*ס>Ŷ؃yMd 9Fi[\L.3i^='B4a% ׭SI_SWA[c}H5m$Rؠs|u"A=񸼢zg{"k:SN}XMZ5َ/to< z+m@m{/ߋ*Tng=,V2<ww{ 3oiIa'_#q`ҬxIVjlŦD{qu1p?q]Yqth= hxz@AY~7%p:ΐwkG8v$'>hB;=^'|ސ/UΙqӹdxMKthxyL]$7YpGYh'mW3v&ͭџ :W 3o+\%,7)Y/1p^bN(f,~}IR-J &]8vd%ΤϫMN+V{N҂؃NH|}|-9jdX5w,TYi#X L<sE/Ld4

\< 'Smv*/|@֋!Tw4IN >P~S[.G\ BK33Ė[Ott-\L9vQ7!L,V&M,?g]^n1Q--DVF%S?ɛ?Юĉ#:tDU9Usm`*`GgAO| 3Ҧ&Xd8oJ$3RdBB/0(hq r )qGCAS!csu52oXBƇCPEg,JH6 X>ϭúx7GCfuּ$HkSjJ4B(9 Lgq>a UW)F`}z6à0 װĪzًtj h 2 A< G-'xP 2dBt?c&Qz |i0*8)f{ѾeF3U:mfxheg% c׈RHDu6] v3s^ BMNj%0SOa2.{8rn[>ֶ 6Ѝ@fل *ysf8D90ھ[Z%BMQjuRQS8<WQ) /ίS>y4PuFkKfH,.jzM*wt..5p~0TB7di }4} kȖX!eVA9/YhvFpraSnEϫs=ukLpL,바?0Xk.S-b09S&`b>ܟ6{PUCy( *{@wj ƾ3*QypLx_'>}շ!G_GdgZ{ G&z|Sg&'OOѪ9!\]*0̶򻀎:_"!) H~rs+kS7y~Anܣ5K|8ңA'^}LvPs=?kA <]&wzB6yY03(IyΌ3zO#`j7Ayd'{/v{c =O #pL86 Z"j\e%AJyo#x;LpDV4;%b{}7-^ۯ~"5vp+ hۛ"8*[io{'|MV3!7}{X}2p55KP6͹{ֻ͊5f}n??d}$PFzAS홅E蔖켮aBghϮK­mk{3-r3:7}>5>"a'5:xC-Vc`ݽE"'\1 DP 8k0jx} "'yVT9EM y$Mvh_n &59 W l$}쫕|ArMtc WxKE|`m]$m ݈}ȂeLqVZBٖ'W"ԉ%8_TgnT-,6@k.i7Z޺DyH8& V:Nq <6{<7/a+~zG扠Cz}J$,hX1>r嚶]yVWhǶÉǩb*0G@p3hXՉ{tlBB:Gӑ$֤cW 1m L Ϡάcǡ 5_{Gg6e6؋:y״ x C3Gm,'3PYD$n8(5d< J7}&hkh߿rۅ{ݕ{.Vk0ܒZzlwEZn3-rhl0}IFYt%J$͒21L' >"gHنސMjooo] ygGхE 2"dtv)ҚR`jxl$WY ߖ`72zqn \6=g,mکr&dZX߂s=;7l^;Np '3+O Bc\٬'lډşa* pNCz V0]S绽Iw*W1f3%X0Al 8$]3}-.Cpau H 1`7.SX-l!gؘ`bg(>uH& )9oy)]+p wx bY -e6,%oj_)[|.}7y39M[$-v:W&}iIV;gBWTʌ 9:vߩ y9^(F9y0֯eo>/U *:C&īcoy/ TϬ$$_XxC{Y*;;q:Hz3wnKW~! Xb0ʹKً'%QA?FzɰHlC[3MWFldɶA.3~*bЃ>i1}Ga<8sekmK%!v9 ѲIǠZÂkq\r/J8I~`ˠwg؎Gi ;෉=|҂ncXyydI9b\}SWvvuN1xzLyEn$Vv$ӖvHwm⃘h Ơ]R4H)'|jL|Y'41ɨ1}}:gz݄1S[ tú9?~WGƉ oO&g?wHW &6707[/#o!LH!aDԤ.o#v/S,}^\|`aM^S"-WP !w'<'rMg}B_bVz¨%b/MSʛ+Ji;PhjjIvfۉ#^N7~'O-Rd1-sN2y#N8#s/D ,`2wYV+gza҃?kY,a?͜/`aU=(&k({.gYm $h :2 HĘYAj1vӂzBc=FTÐ% Y]da *50L@8CCs !,ySy '(74F-QW EpjF%1TCPVFBz4P@4YV>>\^XB?7Z[l[Љـ4em̊vTfWeWkdD!7iEwZpt6NiV^H>:| h  XLf%AFI68 ^hAt^DƳp5܌˾wAr~@;ʈ<eNhB+]\ %#=ˮO:P+ٛ&xOVv[zA5l*fw?ۛq?ev+-zɈF~xk[,HY(9w0 /ʖ\.sDV`rglu7A7-Q[:btӴ%R)#of 한UAȆl`97M~Npg޵۲+F(W[FLŠ}&`6#VI5VbH{ \1sӁC=<#'@ed]Zt/qzohs97uhoǜy{8WYԪz6'+w[ȣ )-ٕȽR"9*xN g3_92PdH4AVY)&y]_hx'yxN ϼy,O2Mq$ǠYc 쥞=3߳, G$]>?'@Zڊr \nHT ly`j,[L $Cy S$#qBp`ߊG-Z/Q]N p,iݎG:T 9=)4dÌ֖2ɶۛځTKTVȀ 9#{J+B&Km-zS8'^G{'mVS^6,*l,CôvoZ;3$Tprc/ 4daG"2A1/3v?sf `SJ+K򮾈 (΍5hq~8=5c`JL,$&$;8L>Ty&ImQ@73UڑAVK]fg YgKse|bL&(׫:{<Ԡgaa(-fZ-*Nܡ7F MC}yq,}hkE\41528$*9: lybi&-w@g|li~pyѬo~7yГ=Aΐi[-pT*Bu{&TjAa{1 L:V ~OlC"&:Ի.y0ř8k7.d  uZ@ؾ\q3y/vB˙qj ^`<.4#̓k;BMqwBIƣ%͛PACڲ8.N.0a2ER<鴵:y#sa{lL:Yx 0fM""}ѱy:Rݢ3bgMcs?2Qj#GC8F`^/"vGTSùh_]% '&h:z+Aȑ"|rݮ }1>ݷr!oLx%m^}vwb^&cqCc.F]hjckJ>;FYn|khbgR6:)dziS'I_0.e+M0Da$[$eAgh>Շ^~ŐU(ow,UwlK: 1fD9(TGlQ&]d~@b{뗸A$D\@}}tc08|a9pxH0~D#H̭+M(-)p㴁-Z4Z _p;^8FR~w*wt6>h שj[fnX1$eISm/i`wq]V䂣ۡJ̗RmetPEjimq3@-L.Qyhe%xNakSsq&kkO_1JɘH2Ihz{"~zzD:bP`g]&UE1 3W69 &^zu7}hNcwܚ\3P1za|& ?%A9w|/K+~)D~2_253NSA#Y\G;l"c|uS7rpYظ֑~H .]l p,#gKܤ9;"@glҮd/*"9WF_Vi[hI%߆?Y%Yc3=@֐4SM3 "bRe(ح/Y7d2;JZ ŘN\js#r2 3>w7Av &!۟K0Ѭra* &p i:$g;࡫l e-xfS" ١*1@F3a 7uxOL7µ4 C,>d Ҹб %ZmT #kB 2 5ޒCfv7R1J'S@@Y">"P|w=C(6ժŅ`> y彶}Α<ʰ& R‘&0S`ՂA *9 ):OU ;|Ϊn^A'!X[( IC:Pue%oA0((l lfΜb@ #Kʄ { se *=+4Ⱦ òWffl /.讋5Y% 5yA A%WIf x,hM{-6K@xI'Q) >#ߙ'笞 ܳq"a.f *e=G/nN2 w =D_̞٬OxT:p]ҔAԡJ91ۤQ]tez8p'Hę>P\\+A횹dЌ:Yوpk(>ҩ)iL=~=>Gy.|#+6k³]\$ޔkp\$^g "rs\{e2,v=>t0?S9JVeMM"2*C`[~yζU'dL5amMH-fLX5@C{T^v0HaR9Dy 4L7K-ux\m8CaHtSߞ"ӡ8?4eq `X}sk$&ęYY^7fw^o\;r/+0ӝƊtşK4+0td~x 2A߿)[f]d((zLS:/v7K66<,77xß8.a̲ak9**57SR'e>g7`@'쫶#:p~qw쬕cc<^Ɠ_[O T1- 8I`: Cc=N&"jBpynљ,KCLHE: 0VV͔XlZ>td{uH]g;:|`hA~ٝ&pƬ@ mH/\-L?AktArpuY2^P [uRq. Ö(-ao9N#+mBҲ6kdrHMu on3_{_Qq9MP:! \"]V;{N pzjf+ V@uQ?<8}+  ?ChY6vзtku;6;س=*۱E:/yvE1Ak[Xs@{NJ͍U}F&+gཻ2I'ya坝uy2YX[~D9ۯ}{дAQ:~%gBtT:#[,XsrT#HO[Yf,b'Yrm2UCHPu6c0|b6uf2k 1tb#I~l` V+ -ZrV"SvmPZ|VlÇ*F pMK4Porsf*N]jQxNpA tQP8wDK$G kˮ:~_-#V"As& ab y܎1*K4Kż7:*a0>9\IWtN\@&jBaT+QՇ@9p ]Fke&exC$8΄l$ n.xF;ۍLUɨqos$tޏzGgSWDkv*QL3TlFOlo`MDc$6V?Kjohh58va+9VąkGoR"W#GQ>w;tJG qcF?s4tkV7I=.l8ЖNϱw_19!vWʝa1cܓ aiўvhg5 R?;ǎoؿ"]F>  \DΖ%ɝ(KEYµ釿j{1|k]ҝ翨pe8S;?H6@?NDuwhN޾z޲7[% ҃:qG{;Ou: FUcU;b7zW"gxrV|Q\XG|B)[x^YBv>{Xo'kҡOEG_*4Cb*`&H㎞~ M'1&l{E$Ƌ"mgexG6q h?mmqLWhsW^ yI`r`[~/ƆIc{`$^-|8}iuY JfljDî 9VgzGuxM?};~Winf'r;e0.f8{Y`/T:@*hoFxc{QQԡ1q`ؔ\ VI lͺdNɽ5$Kj .#Y0W jVEkrfiJ~f!>-y\U痟Wdh@*@JR55)@kq/ T*+UN\@3I**'=<>, *#dv1jg-]-OوsMR]|1`x†@]:D4|$DTL.itsMRe&*vxa/6NV@eO'V(CB|#MB6ҭg֐ qo&U-΢GP\3o^=T/gF:{ʀy;ucmb1xx<ơ2Z'Hڶfp~i>"GF< *d4cuY5; kM|"9pJ7V,<,mML#TmoA3Y oȸQT᭾~ >/Ǘ:;5tr]|(ꠗϿ :(]׷D!գ1Ϋ5ȰL(c#!2PW>t>֙yc+1Co;5$([u e O^L ^滭ؕu4dydr|0h1+ ytRb |*K$4q ^Ƒ:&1$} JB\g#VUs%>Ý̔OydL(d=wm ı;0e2߬Dߑn;~~ Ru=]Z!a2kE& l&?a^#bRCWv'reV㳑|ʞkgdD[AA!G"KZ}xsCwyݠ,`Y̴]DYi"Poc-GD2|N ]a_^0@Skt3xJPDP}z>r<:|[[Ks|53&iM.0aE2\yHha4(kI+V3b˸ |%:T ۂ3Bg6Սy^?5pQi^+hL> r/c\Md7 {_*k?-m \a >3 o9 ]5i]*xrJZnadKZH__|Ml-f$~0*6usČ7(i=ߠ[52!y! NaAY:##K89}dN=;L{+f$Mv-T6{Gs {׾% d>MFm'`0̧}5י~o SL+W0-g>jP<=NQc?4|}7߉3[{WNU+hzK{uzac4ڗS?*!&V5{2]}3_T霤 wPuYnF;/, >v-W966rVrwmo]/:8=Ɖ+WauiM᏶Pg:K.Jv`I~5BK3p~Zz)EeFOm(o3Dhyvr3gSRl68c.MahfVUkܫ-J،gmO˫mMBfW;u`Z7k7˭,3]h:~V-yn\OsJ _|ѶrOtٝ>l?Pt><+xU޻lvlo 7O> :0>zڃdGLbpKI<4f8=/*|ߖc˾tç6:r98`CgxasRkjϻ⬺wtC%,dꍂ{{w\b|-YϢl8d3r!޹?]ARdRk[pQ:.\{^w!o x˞G=`x7?6&8-[#㏘wtp5НW^#au6^Oޢ3;%O8 r -;u@Xv[]۳Jn}^7ĝKlsOxom_nsI/)`_jlտ_t,d,vNP;aǰm$o^!-{[O2ʷwϜ9^s $jѫnPE?˟WL*0[yk9 \ϯSAra$\OiMϢ;] V.$I{nܗ/q/:Z˧p8{1~SZҚ[>ugv hٛC+Y/w0g!2NQ~6Dີr [ _|;%;I5XQ^+/\#2 = OxI*GZ|{.:Em>}7fG}{bWٺZl_.o~scváT}#FMO/k/q\r"Kn6>p2g*ff窟ݟ/:NV;~xױKw\>\ G=Tl"]}_ Qvid7뾻w>5WuN'凿֯8ڱdpt+mh2{66CǗٽײ]Ts,KCp/}nVv#z}hl&ӵ=3ْc':CIhťrq+1c'W]ɖ:rPwJjhog2;PrjwIZ=|={K&\Y6Q|Ul𖍎W; Cե<~Rj],TZGaޚثs!]Ykg^gClL~U(zNM;vIu~۾CҝhzYZA]}y+_]^7)q˒rt"uoXێhQ.R 2r@u|5Qٳnt"#ug OLw Rv hpP8\g`aMd. |A)[V9 r̙x-DVL-bHK2Z՝eʴj`1F_$8tJ!YEs%e\[ m1xשB;%~ QL"n<_Em[A: (pƍk+A0ʩyo7u:?6ZR6չT['RY '$\ l+եv'[ n{ ?bP06s_v4鍪%pDgᩚY ۚH/ &GсT|idC%i3@|p -.hDBRgoӪNљ᩽p4/?_KM);J;U M 6G]JJ3ʼn#z{IX_9r22 ~eN>. wN45.G?!_f'= s*[7xlG8jq0ڑ3 Yn=^O_~y& ̚t*iM@/4Gy?tฟ᪎;KDܟ=dlq`LArÎ]LPu|%ž85ӻې^͇<#z{3rژp@F_i@vm_;\}ptb>j;|O9RjDn yVɛt_I<*pNcj/K$qDXצu{ncKBq4=bV oR5۩s%ڱSdstS2j7<9mu`p>۞RrxmQcpyMг@[;p:,ȶ^ʷs/:VV A:D;K۲u6L1%wlc%k>Tsr0X]-D: '~r$k--~h$ث]Ul>kwC+2ڵetθ_qg>xwԪ6{Z#/{t'⸵>ƾ%y&a{%!faGoܛ7mO:f.g S}:̫~В7w(3{?T"hQ0~󋁿;Y7;N;aRV24 *c&adEՅS,;05yɒ}0YwFJc﨑8˾8[zctڍx.d|ܴmMQUT;1ԭH@33>짛W.KufnzMXBsD#bx1>{4:^lo GvJcpUآ:O2^ʊ8&±f*$|n˟l9/'.f-66dI'[=$u:{X̽W+ּ:d[s^㻇k!8!ɞc_ Q%$ SwdلH/P),>ӱTK6#pcS 4"8{JؠOKF]*o/~9)&.wWa勗zcot_Pݾu :Q,ЍgO~c@_qIH3_:–Kk#.7N/˯}_~?rSvo|k Ξ=Sgŋ;:v)ۏt9RU7i'^Yξ?Zw~eGk'|pNY'ϒ*xVݻS6GN?MXakqh_zU=$o/ҧ`-f$;zoɖ"b,VzgGbQ{*L8nDw}xUpk}~Jƻ2`tb@ 3@3BZ6̝;*.\)8F'18K198?B=HR*N@>w^eC@9`/m01@la՛9%ME35>x%:?*R KWoNlۄuALe2ݫ˜7)x 1po֌;DL$ >E0rMr 2,g#]cz^zkU-3xOf 1 ZmoseAHơK P 5 G{>LC׮Deق[bMza9+Qr? ؒ-8&e ɸJ Nc+S&jvfu&TFXeppn]Փ2&Ku=טּk9oôEmÞm 29;(/ku@6k&>֖) %pz=| (<΀h10rs9 }-]MoJUS^v̺;&ee7͡=푖SMiȶoд|"KUev/j㱫Қ#wV;6XT,oYm(cKh8cW0fO~BR2Qx8ԀZWTVb>7QF@ic\RWx]т3ιV=QKk N Uia7% kt8Cia5m;B?Z8,|9__ϕl+OI|鼦2SERshϾrN 1<ߒ.r26SFùOf`F_?*px,~P,?JS9,<ke()O6p: H^'}T(#ȩ'U1bႠfsfH8V/AW[/.j%#>$h5y#ihMn7MkIk1It }X.y-y GS1N.*9yq"ZxOU" H(^"nogRFNՉ4{c%# lZ`Ì]UssKj]x %0 I-‰%3IǕL&%]ׇgɵ.Dgr Z=3~s_VVrs EӏI ^wz:IdBMq5yį,0<>g/g{/@e.hhtر}%lH?V8`w3Z|e5< G{~[ANSNUypӐN<5FЋ؂w.|[7f3N svՍtisفދN7&$9M8 =&=-]Q/,S|.Vs9z *m<&OdVsy{um}!FšANfb y84/kµ=;u{-/gh͋ojo{->*|q:0%L4w NC %%?KWj}ڣ?lꏞ=:uРn䜧uzTZ=6/)OPl Oji0ݲuS2vaFM*3I"xAK[ipj?mMeC1 ~ Yblg6 Ѿć2x"–16 ;t|h88g~g8Zf#9_7oש' t%y+ҍ1s{;Xɼ_gqvKhk3Kng}z\L?*資\~J=o|ǎAP) }3@?ժ=YG5p ^}>Õq3ҪlHmOuj+%AEAUG?'.9@tB2BۃǵG[sg8f{lϚy3n2r  'Nht@0{M5G'Ӿ;pw]0xۀ=S+W8*{x<_w4 ɞuuiLd"mm81_okK+_7Y-c N{\fg=^2 {NǾ9@ K~(q x(_kI{w*yQ8씇e_}PoW7N<~w~䫎Xxqrۢu+E(dV᾽I,:Ucy#ocM],= .Xlg[oTU$vzprKnOw,_{r7拼S;] _/O|?^uu[idF/J`Rwˑ./{PQٽ?戴m%8(@~u1+_ S_uU65z(|Ï7"x&1tO&_ts@;ɷ5.6b/‘b5&FkrȽ{j۾5aw/7=ȃpG%.4oWE/[v$)c ۤ`ɣtJ-SC!b\i9|_|#>HG6{áؒAm:}ޱ!z_}팇s4eL~ (SC=<%^Zus/ӊ f̞)q"vN?XG

U al?oQ1K<Մ_"&cFSskYg,SRi1XaY]cDm2Tӯwp1c.Jyt#[E$}G8ܼywY<9яW^_gO*NI2;< m#z. 0X.ھ{ӫ_ ph> 9p{[2- 4{giaivwG8&Fwlr(*\1"Zb1 >#t@ 3uqT+=YBd 6\p8T|}JPpP@cQr*.#MfJ&%BٻE8 W[7x~A ֠n?v6-eka6#M@)AyE]!+孄w =,g9ЊϊV8n*EKtuȼD`T$ ౷29 `,|hd`45 JS9hes50&UF]a -WiKe&?zghc3z;V4rBX#huq'vSagRA7͠s%eg*tV'+4 1gȾ7J-<&JxUܧ' B#w\}JzDi'H;~aQ8ӽ;XvȘ /ϩ^LN[X'MaJB%+MZ\Ų'yW_{)KKxr * JעG*EfH"n\0'.5a3- :ꨅV4ڡب D[hנA CaZnd^yrͳbtXs!g.&mAjle~ΘiCI'׌KTb(#{[*l&hL9(-<٩R_],,1- (XQc ʓ ZZ4lN#>,Y*xq,:Ȩ̫븧_KJqWY<Q" 5멧(GQI%U܉yGƟhkܢu7k8/ꃏVwEg&: hX,W[ Qz)@"vpalCBKAe;6nuyЧn$^u2j4}y̧7q5R%qXPt v7 mPIVf%`wtzkL[fFd3̜ `T89ϰ56 |~Y?;^ 8_,0 娾S__)W ܘÀ9u Kk#snd, ޺q0ۗ&$g޴SHH anM^; P*TF'> cvz1ӄe F=gWH~lpKׯ_lC>s.$* #q+^ x K8[JRWw'(߰I$~QE6%$Gd*GzZ{$N$&{Jv.[3|h+\zo|-Ѵp`Btxm{Y}i7k@bW vȪŮ*F` tO%Ͽp߾)-9> AMk+x/bL?o#坤rl3bld̷>0 w̯q)2O #+;33>sO2&|cߌ'|+Qg6 89cgpVPntzV) |}H27<9U\_,09(G{NT<G DyuY> +ͩDϵ@zE횪x?~\}8$.^x|OR~oG}rcyD$><_ -)⫌keU)Zy ;[|v{$ٞ]D1 g=^2p fquY̚6K>| Տ@v)Ӕ͏x79fC|R2= eCSKM.@4Ç~Yc$p--_]$hZˉ;4-\CGғT$>.\|liŞfGP(+k4 E;Wf3P'/v^]o?J!gS{נIdʣF nQy_nG [-r-_5)Nu|'~&vWmN٠\6e [MA~z |W*3gڒaI; M+9s#81FC%6ՠ*Y =3aT ﴋfaO,SֶWwQ>gBױ(d2 _[1[%Qer$I}-~뉾:f*zwt&4ZNeEa1] Hdc/4>?gb4 v?cP珮zyV| w:t ]0n#_50mb>&< {j#@ V6cx^.CC.8|EBynٙ1;G">)9ϔvw^NA12xm/ Jy3eu5ף)bg0qW1rmu=I ~l<]ƌk ܻ{" T#< }Ӛ)z2ܫH-ZzcTk B{G7&2_]e*n݊qj45J+{ݡrgf83ތ.ϥS%J<(2i kgpP!A>ӉSV2`CD0iն:l˕*i`duI½= g%DV RZnx5WP2]`'c81z[:Īo`HB|Z>jptE *e\Y ]{c1Z֦/ L'A@%ږpH0>6nW8-Z32*P'!B=gi*.&&<җNɑw$fe( V@_Κ%SSY3*zϽ7vc:~:ֿIUEPTK?}o4+陗>GӏERwɯ Stk} Dybl8 _$/~=z[źVe+.uJx.8G1c7 H5 mTi4غǻVn޸#\0|CweLNOUaH'\fս~K?Y7ce=WkAth$=qQ{Rc{Gk p_缌^%u,ڻt44<<@[n):gQef& 0~s/F;;ֽ;8 7#i׶9Ys 4?6 ûP[@IDAT)U$͢9kDA KϽrNm)f''ҩco/~wA`@k{:h_ݣo2xwl% +I-u1iiD+*eMgy{)> ND[5gPB H.a<ٶPFU5 .B ّ yƳ&Jߵ_CXԣ͠\V9 R1΄O[e{=ե徾4xH8xkZ OVaȩ8Xe :vԓq?H^;wy(ΪϨ G$YAb3ÝiaW:աF$NgU> ces*RkH%un#[)2 =7Ƒ;,s+?m-4-0v;S~'E5gC= evtcI|#p,}ϷD=9&8 :MЩ|zD埶J$56Wq؞ʱ%' XX fv;9-lgkmzK.LǮABpÄyPѶ'*ЃV(e7%hY&bi`| k ԅMƴS17[~W|tn:|0|yD283ìpĂKIjjMKcep c|2[Ymǥ+. rf}I/Z֒4ppp`:eԡй 5Cڿv@WT0 c Rš%qOQΕbV̀V(σw;8'`"nǃt x/[d^3ǞIsߞD":gzER=g:ع{k8fH`#Yn! @YG LLAXKMbM!EK؛oW뵬1Ч*<>kl l 6Tp"pudWtP/芞* xɃZ>~=pF3G,ԱYi^`D>Ay4$g#[~Lw|釙h¹!%VXڂo@g/[zeWd0NłXrB'۔a0 QQ *W Vr"`/v4uu唶<]X 兛xH 7\XH]#`6;*ٳ ?۬B{ Lv6 iG9Ky|.>gȺv1VWQHϒm9Giڒ;>fb~LeARg,,"P|*O)OV^he8XS$X@mU;I!C+oWʗ(?]| q]|j5Ld'N󝣢[: qAsyN?] r^^k:b$gfԻeuR/3 g577{ -TG`oߵC} iؽcK18/ٟ"v$gL C $46=w=ϕE046vnpkܓ]_W3Ϝ@=|TaBw$@QݮIwOG7 _?wQ3↸ ψ:yp<ػ8 gTY"Hp+maΡCknOc#[ N)`-t૯5]FbC(@OuCY>XRt ~xna|Xf ޸~5yi|5pއVeCq15K)$S/g/bISiڗgeL'="K|=S4)ЊA+k7nkƯ=u'$Zs'm2kQ% wq^ b/4Y ٕZ/|˳Eh3b4n#[lܣ-gĪۮܫ?=9pЛc!&;@{̤wegZ[zyFF2~^#iGa ;x6}: YX M]r `ظSĆN} {yX +е>:})g@EM^=a`EEv)ETgS Y?9hny Sdའ(;$+ &LJm4 *;+ V DjЁ' QpŤB 0C4  $xIJ.ϼ*.%JV("As70F寁]Ƅbpekf?? jp`Dh re sHU!o1`/(| [Md\砫 BxT.edgGw:M ^۱{94 /VtpRB*ݻk OJArc7$<:3ʕ<ߒ=nF/ͤ'lΟ>J|7OoVO_+eh{lA_ F잠ugւ)_NO>ۡ,~& ~t8<<8}@^/9=PEרf|' ?zޅRY`U'QAqՠw+u4]O=Ba/$xM)ٙk,e8Z+ H/Cރ_n*U&TZhA>};j%xw @Z+=S$w<}C csX:3qVe0 1*$?yPz卯D`߉wEI-NTU hw(C*N]=y(/eL"I$׌ &a|jc!dE{I@*-@oy(U9] ᨂ3G>=K$,VZlPZӈ\@!qNE߁9dJ 471>(0>`@aN] +T7Aj7`'aAe|i[pىe"A3}hB l ;r{]"YIe/zw&}:^p.*&ѱSՑάL1lj`ߣkL:{3@⁼H=qVT)g.y?b Y4D׊ٛMu/*OKT"Vۏ-Eqfe #Ačj^ EUƣ>=c\ЀR Ɵ/hg"4b+׍FEAm7P{N򸱉jP"u4h6uI:l1F ƀF֍e~14ݾG@ c8)aLN 7=bʧtP/,)Lj'`5Bod+$TWL5rj6DEN>[ZZɽd }V!C\w™SO"0ƆGXGvLL&9g{{+xg'Oq\_ <[C7Mt pUYm@gL@4eSʩ'O~ d8&^ӀZ'Hvg@co|`2:@V~Kҷcn ;Ogϔ)mΧz*:RUCK|_%v 9؊NFR4}+MЎ\7'3 pR;Rنl&ܔAЋ>p.<:]搞VEr?D\]7Y920jUw{:sػd&zJ[dY)QLuƿenU>ʒ>tḒKҮɁ8M }D0iFmB;aAQa<@N|Xʤ]MY<%8glnk繭4F;9$vSw:D@ٚxGOW40Wxf~!{}ܚFF ޣ.{=_YLoN 4[_Ntǥ#Had yIagWd-\Q~߲WIw85|㰗&L.gh*)fȻ \왟Ϡ|$>(Vg~,F`!$K ǎ_y{x̽JSR U_N썁v7#nwmcu_39'mSF%ocր:@̞A4};ZuHw]ěC pfS؄HvѿZOjC"}&wnߌs_Υg-&>;kAsdזeAzC ;L3Rb uyePwɮ/@vH7YIb $t8|3u~Pg.v41ѽ[A`y*Ga82wxnj&D<<ᄁkˮ1ϴb3гs 'n1 _.+febX €b8f2+.TwOVX|yv?G `1 `uM0ޱQ: Ja({4fxJpsos҅0~A6Fɳ@\50.ـkϱ]hд4j2%'o~i{Дşl(.+&Lo؇q s[|-\<[XvRnMRH<~XnqEPVLd~myʠRr5~c"~0k7Mm`~mn1\Cã$;FRd6*vĭ!P-͆Yم}i~H+S<$E*a 0U4o'/ "ژn30__W Ky("D$"9iq}v]d6>Zi@2g[y"=Wl|tKc Liq,XL`?\͹vY3+$%لw*_ nFz5aqcy / :u4mU8kNY*JX9睱7<5B8khrrU:M)(V춦@HK TYd16~UT.ώ ';(Kq#R?~m/K m2Q2i`kL۾Bz0@*IU!5`O2ȫLFAZe > *2#Γ,"8z 8A)X;{9`H@ʸ[ev~f sw~N X-ifbb4N4r]WDЇ=*@4M<& | Ũtrc$jB>Dt{wH@XgP z@u%JO'Μ !Fׅ_IW.}Pfs< A#ǩ|9Vr. }Cg`<ձh{5ufZ)K'HEE]tPJs*(HU( G`+{3*2V7?UgLh5Y΁ Tt6USɉ1ln٤^bFpΖPUjULP!ݡ 24@Si/d=x^Z8KUSBN*u"` ʥ-$tj(O8U¤Gb(*GUjVqpiΣ\FPwٲ*>x yKyt,!񦽓Ly2f;IUs5I\K:tܠ.z)6C=5m1[VTW2Tg|Ic $Xϙ pL$ku?AN>w%uƱL:ȵ F* pݥ-) -c%9 I6#y{7)k?t|J-\TПgƇ9<3E\R{9V:4|}zKInTyqޏ+xO\\g~&0 |gG ΞCy#ҠAcuQJ3 {Mdݾ>g;!le sr~־c< \dtL3[ŵeK>2Tg]H߅Y&焣@;WG^~d&26cfwN!3?~ksSe:31*;70##`k Հ4bIJY]h!gAj;gkLpT/z\#s Qۺ$z>G}u]~E-v5̅Z5j.|6VIԟy%Y1xcxC{sG{<SgCY_!CX?p6QMX9|i/Ȩ+  XӒ'|0tB&\5k輮-*sN#BAk0!y>[]]QlRs8012ihTGb)cF⁺4U`*npJ bMGYNσy|>}֒]e ivV/бɮ*)ymݘzx3MV4Įյ|9_%&#].WϑO,Qm|ѝhЫך(,/sҪv>TF?]R?I҆wۥLvxep"X?jZb ؕT^c6⛝"t?m"CB /'(j|k1HA+=d]N=G;#YMspᰑf788|!;f;h,WZЮ ҇ՠ^{j2ֳ \MUX책mqmydIWv39 +#{|Ne5YY[H?5>5 Rg-/bʒYiԪyI"Z^97lk{2ݹkLk Ԫ?.r``Og,aso\FWd>^JfC92x_TuHPd?ܵ():W~D/M1uhv׶,cFB16+}Z\&9#s޷+&g}.]:?x`fm<IuCKೝM0ͽ#KUUFvuY&xBɞ[1BI؀^ l21|s;F<BWL(< Eݠ7}v_d9hwo GEڥuʉ0"plRp` a YTNUG2zZ2BG}*+d(|e"~`@1€:nU %b4 fʴ ÖuVzp20<f!.aŌ/|'mH hnmGoʑ+ NL=~uswGOeM `jkn%eu03zo-@ 6,0`y͂RP>*lMVWJ *iP*KH&a-R`HҭSճ bP]T,}0%c2H_^g਀a6(3o_"84ޢG{*뎘?xX OAS[n_#y#58&'I4;z2*&b嘂AsLۆs( TRUɤcMOO%xWDW)e|:?V8*~nV3_ LMq4;1ZM* |;-R$lSyP%J CL/??8_x+'`(M Hu\0z^ gsԵu*& /" 2xi*#3r4VbK:գlb8^GT `:uCS!pz>4!ys}]t9J]|ߠc39SGڑ/N5Hۿ+i#G{3Sgd[ZN0S'mG ݥbV:(dG+LpD'w<d/XtwSWOAtʶё23{]]*:8$qQxb!V^nl]A~}R;ᘲK.{0Hm/~~D;(N3 t>؂oR1>z)` {9n$#4S&3I⻕dk6ẈQ(zH':]S-i}iAoeo sttQy0ٮNϲK )֦3@j֝] _*}id2i3Qds%$}U\.+B&iJ(Yfm(@bkQ53~%pX5e=//ۗ5ʹØK|a~߯x,/i 9DŽ4z!@}~5XZÑx(IpԫGtjHKқH!1dԖT e>M/9 \~Aŗ:Jtn /e ]|;PLܹcr[a]bkAKf3<6eп/ p*x0 Zϱ.|m1WGRgEqŤ';e*\::& VVbf $OdUO8xpSgQ 85'D f 8u m  Xyӻ vlla.KI`z9܏|$=dr)t֤ C̡lCY GXN97x,Q^PCx []];ݯ{ 0kgbQmmUڟ:@(rkNg/jflkU-36<}n6<jMdgTZN_.^;PC:!5(Jd@4ܵcIUWrm~A$KWGǣ#c!/t <%:3J{t&8  5ReU~$µwE[o!/KpA-[Ѩp_ffF3⿷7ոeeuT>&{ 8qB[a>-LyMfd?N>St֗bSM-.iw"A5t}GH8`onfVqyv+S1kb(Х*uh.Ot4bn59m8#Gm!Q8Te=90=$ℬ'6ڍBi^^-IO!gthTgi;uT3^&Ot.zctrҵT$-κ*ڀ' #(C̺4ݑ:?X Eɔ҃WG!!H<1q) 8u*Luv!?~A"O[{М3MMs/A` _Y\Vpġg)3S\fLiޥyʱ湚jlh R `n߲σm L&h2%:+P[0-ma3~ =x֨s/i'h z$\L"'<ρ[&/#w}$희*z32rq "@^j/tS7L![݋|RVpVs>CsAL-+ύ44|BF&ן$x5+3W`Z3›߭ypb[jYWCo ]#g`JW?Y<0cd;LMO2>]·'۞2&2QָnÆg;00Sʎ1xgAcD<sԶH £B>Gg& <_>e=̣6_Rvq&c x.p{$ź x={yy$C"ʠ"s 7ߡ?VB 3W,9s3H9//l$S8el1` T: sNe ?ұտMzqBSyDq~;Տ?F&G\S}~ic L&X+-iÊg݇lE&utvE1H{W7~34'/5*~ N{O=9I4 %3 HK|$3+>@{fW̵%UoIk$׈-L?KW+[WIXX_\06q;y:6_x%l@ǜ-Mg땣k{l#nne}ږāE&-xVOrAɺ | 7 <]_YtSttfNY]ׯ[y^qX35eN&҂q5:a]Vmyd8 _2(;әyx o} &#ȃ(%on,AVrlv19v $#зjuu}X{wtR2_4צ/6EV.Ʒ->&׶z+(j2G7G/D^ar_@2vj]0ijR6^c+6U%~Co`na=~+-\Ƌ2ؔ > _;>O6j-V+"yy]Fq_aؽ262L=:wƨA._ДzQlmkkEgu:@ٓZ-4mIL:A8@8*@o7lCp8|_Np~|2-7. Y HhzvN+^}Wf=NUR:MZ 6m]V= 00-Ye*:tZy5Ž8aJz[A=m`-V.Q@IDATTX=cfHL SM`e]&Tz?|Uu=w0mmpd2T7LsVsd힄6w%jҚ}jrdFֹ]`H+| c ~` g(ϔbA^xM@컦6?Df?$S֗tD*f(=~TbE2Ӫ`5$.6~5)P;7C =N?s9^z (6ڥ|6޴;wB:~IJ0CGKr:ăS;-{~#Ю]N˦y*hNฬ6}/HM?Bga) f4h$M q;xdTB!%oX#huq:=΃T48gsnLWu9}2?r{./~mR'C:UHLrG |}v@8J ze㴴'+pwl} vW5uJB*YƍJa$@C9VjLm2Izݠy: ;F`@;wS-&lZ]4xBD<{1eTab7D@U5pR7~>|txā3ϑPRc8Gb<7kGZ{gXpVGA>h2?$M4^|Ͻ9o.xBd'KҍmT!{̱ /DBMgCZDa%O1l<ƖҹW(>;qL50"_;v @܋|1s׫1 mЭrz 0& :_ 2' iQ%^q1? hO߶2F|v4agW-_L3QKŴA #u^f Cc仨'2'_:M]yc9Ep\": s]+ 9ףaF5H{-aiv.YL&ǨJLov4΀Ԙ)ɘ5h6Y?! +fX )/ om6(}X!^ &i_XIS@w7t"]+ud.c5nr:2[~\TGц'9A\}=ڙk#9| rd|D!O|s DDɧu{C9U` O'ok?x?tZ :#dpV1:>vnsޠC?s*M[0v{q7:0N[ǁg-|S._Cr_=_*@_g4I&}tc&LZm+M[{gVtxa/-k 1I U:+)m.Axy6)_tZH נo6V t^bKuxk:W&IdyW*/.{S{~c <9Pi( O /U@Yq8P{CcX3dS۷ :K׳4PڭZR[O.:wE~AƖ7 ?rY;6:Ař];Йյܙ lm`lsXOGQ۸otd$OIK {I^:>|:r x6e8#3|s,K2Yⳝ3kӱMgŠUAà|;CeC1zdl3^G?Iܐwy]_"GrAYHa\iĹtfITȍ 8 r`p0ॆg+3̻#$Q6)3})`]SM<}Εwnxzz\ݵMa.9PM4HkP0tA`!@]Ga"POs]ͪ{R<kxV)|5Tp ~tX)cL&s?v)ݳ[֮ۮ7S&xϪɼ]ez¤Y4d9L>$'N]Q|{%}^{R}gL7qa ĸ2/z9+dTR{3"_˥ȣd͛7??o\W՘~;(@ssMX,9Bv>,AؓX"\ q= w-;Ҍ2[l`=&Κ;7(5շF`;-tJ|k/8iTTw9#u&J9/m5dgڵ3"s]ΓVW^?${͍kxSC:pO< up: =CtߓZG'u+=:obA%Z !~cCOgDžBc{t!Tu[=E,p-p͊*?˕o)tOVl>j.XPT ŗɈiOۭUդ mrC}H&)gOAN}ȡܼ| (P>;r\VzUz,x5$C0(QB /dYgQatyP4[ϳf;|ަbCM=`i9#=Ұ+]ozV-vWhCGͥ Cf"Q =bigs!Z֔t}8! Cv5v/GȒ((^Huũ.@g#Q92Z`V 3ZCCMvV\\T~K&u/ `$ h;S0#vn\CRF7ocvE~K)&2") [w{Z ~$j|=즰)W{L m q_&HBx'4i_ w >1he?ǔ$kOb[>`ij^+,^q%gFz[V=_Cr|k s;\xEv4v Ûi?aN]]\`3a8!M `aҽd2@5x]Usw;YHX2鞃]cH&lHՍġ莊,-6;t(kogniy8u~gZg_!EP23pe 1àV`D%ne,FgVX9HK @#1f= $l:>yx3 2# *ԖgC&> t"LSdu8i]:&azgTTiTKXğ5pц(2RJI)5>tBc['込c , fTj7!h[S36B,3~ߡ&$?`H8%1*|yt3(n"ȭ&dnqv/<ݷʉs쏹{gBZU<1_ALpWP$2u6 Gυ^ L F'l=PKE /W!`VrMkO$4As/c<G(r?8GAGs CJBGo ]q=P_g.^cay>:2HN8BkIP}zK'ZwNȐؚkѭ32"uj fQNtUu߱P?61X//z %/1Zk=H8586y-2͚Ķ.Ӻ @ zz_wG齟Y*:0p/;w3;?i']mId$+Kڥ-&F>o3V!<YGBJ܋3`.Ѫ`$ؙ:;3= hC1_|=zgKiKmv]6l@=KZUSpT:4[dZgmo8J1zHqT?n}~3}=No#GTf/: ~0棇qܬ$!q “v%+4".gGKҗ,e8HGJsS5||ܗ|07O#_ 퍫Cs3r >k*ȒmUpZT(g_wJ9~koF]3n2jZ-eAB^MhտXE;tZ'4rob [9Pљv*(ѸmʅPgRefqJ 8'$u>VJh$qlq*h8VUa>[hq`w1Ghi/^EqʋsJ[҈F:$W>y/>Je]җ[,WĦzHiZt`n #4V%p้0:T@\7,KWq0+s+]҂!r Щ\ر4I%\l"}A6LdyV;qtkk\z,P% f Cbߕ0X&m$ƀOyOFIE}7h}6Ax`* ,cs4{Xt-TCts|y&zS69/H&cmxgsLaCѹ#)s_= uKv+n5]GBxIXz$7X\#5n;glq:Vɮ3K%*d mnz^ Mϩ'B<#4ѣk]=~::PTx'lk\G-v)D/&ӯaG-q`=yx8N}&وOl ?эWš]H٫^ӻ} ڽ©Ϡ`Idj Elzq씣#-bżg/zh3 }qS MހqPe.HЭK.g6E埰i얢2O0psue~J3\0!C6P)y4'WG&<H:VY>7']G>$`jp*E*FA5.< |lƆ-*hsEWq0s"ú z&pwqTWvúK/Deﵻ_5 v:][Kݥ]&䑫t ?֥Vjr!3W^X^>.̔zנ3~ y$n#FGgS؏V:OX-jy|:1k^&c8wJf:B*[{3y]M̺P/ʻ 00gdu(4oWPeRϘpf2 巁$Phc%tNob/~ '}>gqf-ǣ&Xq}Zാ&|Z[˥S 6jM8nghc,QΛ`f?wyv[~eӅ$-u*LRIZBI$`@Sb5 Y|!PE3ؚMt1deRajߍ lB߷PR>+ѧ38d/K~AYMݖ?QMSLVz*|)P?4fg<k6eՎ/'f(пt$ndbz| 4@Hc&>zV'L](M$Mvƭ#-:tR|qs<}@]>T^}-|dC^yU]}s6(nE4פ| #6~iY~Ey n{tyf5>o_JoeDFv(灏y؟RY^^ǡ:[݊gJnb+ؿ$R N,SPXH EdtƗ K k E2yypoG2qerzvXYJ<;{TOSˑaCe [MHA/+{/ XG$#5Gdpٚݛٽ&iƍ|b^Lܥzswt~!7QL°|=pwq1r *J.Cy۾c/I 1oMVd>'cwG(4ˠW!cH~*FC~pj6y\LFn+k~~H;6IN*=9{꣘Ï>N6^;-XCQz ᐝf9\cd~>_hZ"x.0oI>td|ԥgT !h<տuxwI% vGw~ 8ɟaMо}! N-nSy߻rU!:NteߣXE_a8PUm]xUsyy״$i'2@ AY&\un2ma^;O}5yNT&hbdwWy%:Τ/2P;br!Z;ǘ=-8k:ҳ?bJ&'X/PŸkpG| guqQyb 4_ȫYCC/ &loq!n` p /gU]w ]J].l\%V:%u*筢O"z8#>E:mqr4y{R $fݼ/&xa$AHۚmL`o$}wC2F'1.abb`P>lB<]fcG;6t"\3o;qqΪ 鎁\le"NC5|A6_:;M>bp U,5tuMs^rG6o3jˍrߖh Y^lY4QQun)ջVtАTVTeZS+C49t˵HSHeW ҌBRF{ku<.%[.n2ޮU*NuK&G=9c:u~`l8m |LùaRK3!Xm9'~c[t Y N*Dt_=R/B"uF-\Aw_F$W nmTD* M9pi.S5?@vYjշv`3IG\w?IWߑMV]`= NT' Nq6/V{vE& 'EvAI=<.x"gNvVY3dT^챾Y˜7 Py(Hd<+%N),ugI!cMϩiG|91Sd,hbEy8X!SG2EpH!G:"lb/)f|sb, 5<@. ]%d20OS]"9fG]6lm'm]tF| 94w )-s3_Gj jMRPl%:f86vF=_MJU({O8Oqu E2GϣO_2ey#&w=^=bǤgm90+,b7ϲ ލ_ q!X8,3ga1_W|EMT[_տt5AhWosLϿoQ"+4{ (7%!wyr̝[N/}n¸;Z\po,Gw\C 6@k^M7b>}m7sr*&ye){NW(LJGFiolH@)찓@s`/@1Oyq@[ |B SL XܫNY kH?2D؈j֦6W (lӂT^7 }\w5>6~ [v(mu/w؍pXJ=ָEƣTO@4=zlhcǎIG^/# {m_g[o]nv)xFܯAؿ[r)ԧgP')C7;~{`dY&}v^lYjx0YF[$,!3#Dy<jESf(b3FCh0D'%HoGN(4 BX\a(CRrr|~0dyLQTPwZ͟WwcgהU?_|0NR΀Zea> }8^1~6t8,sq? LLiv̪!!b7 HHx92bp=2{heNwx+nN8 qLyQ2@S(p2aE4-$63PpFx0`A۠τ6Y" Bi6f[Zڢx[5~ig\YUEk``! ʥ(yi4hKػ /eǕz8S){;prC>#$jt~-"bֹ FVr"#W׮daINY3QCikc}{Lt9}~ZWw-@2v ʭ5_ku!>=L8X\qTjbh"%V609}!Ӧ{%<+ZHT ?UƴͰ0. }d,93xI QѶz&q~oy ^UΣ.ԝ#YL`gL2/3%ΆnL:hgh{m yZzi, m#LL`4)Lg΅ oÃj&(^ϻ=xm`AEz@nG86RG'6!zNƣUʑ=ҕaDdBrLwm8~: '0\vMx< ILv1|֬USQB5׮\ Y%ϲ}xqK ApJ5fc2LRu8{Sc56Qet| 3$K8.՝=;~:YwwHgS;af8T6y3'C^ ]#p#A'} teKesL#NH48!&13k6`y:I *LA>zB=-j<ʽ7Ʃ ^Zm&tI[fzU;b]6WHntºQ+:I{kt-oiX3TgvBeIp3 Rc1EZb"lCX+;z4h}®cKo7 q` Vv@%Ųy{v?)'̠9B: 23r ]̦nX:`'[pڎX(OLa8 ރ{൉0ACG@$AJf_eGۀk3fXhC>*~ץEՕKH:!zILkעffڗ2I[h҉/9!J;'ys< pnn7qn .>[0 g[G!gX1~"ɚ ګP/rұ0Eb1C^.N~ʼA(:ĜѝCyZ qꞸҠ< s᧺ b-"| f߬Nm~֧QqXEt"aXghË3?du **1`c_Z+'L <'guDaidqn4%0T>(_1L{HK 5l[KTW=Ϊ?0`j%9gq]2I# ! ߹6qW!}n@<7A1(R_5W~i$hm+/h [:xG1{5K3~c~wC뱫msތ9OT&y0oS@>p_^!1Ŭ╎/}Z!/oWβUʂ)Sa.\;B>SFcazZ_XFU@#ɿ{=ܵ;Ojwßz܂.'=%JՅAow(R 7I0YΙvUh΍g\I1֘w7k4iiטx>T&X~Lڽ?56CGgj3Tϓ?u-#qfÆ\{|1G=$;:wN(AE!_-ZvT78,^8:>8ؕ=A+u|OWoO"_yjKtSv ]GB(DDUuS~YQ;y^p;~jz飒yG]\L*SlM$׻L!NQuxU'gDHJ82if?޽ /2yed?Of^TWp/ *\^|9nGeϞ9zzGZ:kG^)+=3y-{ґ#I/}dzrT)H]=@o[wM>pr@8c9AG~,ݑɷ'.5'|:]|>,?0JpbK5Tj7`.Og'':]qVR'q0@NWRwO_H FeÿvФ[b ;Ψ: z7xpORIrڑW-g~ר X2O(}w/ 0`KzRuH/?^M=8q42O{>|1kQJ\tbwhjY1აma@>9:1u>͝2xK4PaZ9 QTv:plߓ3t+S_E ƇT!4  _j:t# sf߹>^-\|gH X [{]y57 W&ݹۑ9 wDv7}UN'|*h&|qrԣght4MJ0 xŌ-Z5DYX||PҁkpҶ-||8?Yxrzk \cBfk_dN!8i$h]v4@m!&M.gc-H䳏a8Y̎v'V݁#@.N#[Gyܮ^3g'"rm*4\*1:k;7AQ!{8<6gʾD:Mi8~X<0 갵ւppAc`?oVN2YV^[ëQ]6Q.[ׇl+Y׮\wjF4ЭAe,[*p6$Uu#nԿ˱';hSC9t[`\[ յ:hN$]' ]}償CLt_"l+=glj R9"w`MBǏGux}\M'SJGnNX;yl71il B{ie Ya*h/~<*=t3@07)N%vW>[q{v~)Sc$F_8{NeE ɾ)͚N!$XUAā#79NRCP IC/G"V̍XN]S+Wuc1sԼ ~މZSЙ+ԩ}t軌Y]hySfdSct7A"/Aŕi2^'H{e p3GْM~[4`{Ykش*VBbW/Pҷ h"38HN +f *ێw'b?\y׫mVϊU>S9z&CĐf?Bsw+MxgGY[&jCoTUCzt-07t CFIdܞ*KP.ﳳxT:4t9 ȹ[tx>}9H?2A&K_#Yr_0!`ol5 gCWt._pO3sv_{Yg^:,&Qo5(G=ܙ3t H8`&Jʃ.:0uply#sJӌB vi"]ږ.-bfGy(𫟍1FAcc''6C9ԳvצU`(M"l( =^%} oo)]4XfWy>#n*p؛m'UxoOoO{ル xyٿ+IU7_()Ŧ^oGRmr[f ~@nWRFTy0CDX}_/[4c&KqML OqI'ccrE\.}]!١,B>aZ\JG`2wkF$x#mQS d4b"q=Rx x$v2O?|>!ɲ2R{T&RY3e-ik!"-^1SW5R&K <2ktzac倁2NeiTi SXiLZmK+Q$ȴ_Sɾ6 mf<Q⠳T%fF19q=hƆQAV( PWVJ¼7]6k~D!95ZG]z쐑W@mPK19ぃmnT /njq %[VRP!P#s#/I_?4nt3s߽xDqOw$I˒Q^*wpA߽9ܹs8xNJ\=2ޞ>2nL̜]_hmM57>~%< =8JRiϱDU{~cd؞go,J6 gPQ蘫!k͵򤫫'DE Q^f@,A!p..] 6l(xUw4H]F*ȏ50>!ugOfX0Ikg>z.}D.|| gUrŃHr&{à X}Ài>]$:rswy̽ʍww~4@!r?bJ 8ZB. K{ٹ& !x8 q* |S8D܊7m. [1">,*uАoc:M9>Lw[ۑ$0{F# mk6D`@awꐱ>ߗGuYN-|wo{ QZ ؙ& 4k_txbF$8nGCҽ"CG{RARXC Q1 % }![e.w1'D%r3b'yL֭_+x2g, [j3 m$y11h+&ҹ"Up:(tRA)˞vav9m۹ZlLy襰$P'z c`xesR-.`0k7e=9Kn:*ՍEeN8ӉU.uis( rҡaHz$zmhnugqw=|>D8sa#\YWg߼=Z8F # qZ!/_Ĥ?H]:YӠ?j*C  YuS+Yd֥"QYv.1R~/QY :pld&n2 .,s0LƸVLRF@YG@w=axCh6\DBw2 \smK[lgbUcUcy03XUҡ$2~H1A&t$,:IUr%EG?8tAS/Hk =x9 1yy#+Lݽq3i/N{D]N~j N&<2CA!3*cyR9t8ƣ;ZyQg *2&@ȗ_ e$^#BE,Hv܂J-Bco^ pq^ VBnr ]lO ,L3Fo%o3G^)Lg>&/ 5Fs?P`KzҡJp9{lRZ0@UfG&e.aI3_> 4jpt(ҥh2a|A` R\$O^;)C K]?G> 0u|v몧K,JVIbg{ΥZ!Y綈;#8~>n|g$|Q~-6τCIV]_u ^~q9EҚ:|;tNZ='Wфo%>x`lҜTVWu_dyQFYX&Mŋmh,ű=(1OkzTt}H";~"$eG-OX.Z/QRYr{㼄x%c etnv-252-kxq95ߡofQ~1aW/W0_pu]@vKhaGuqTy7,S5k 7e]LVh?s"J&zze{klۉ71 +i:sd);)NV~-#<:XjB% 93c";;S VW~WXIɢ($few+cnltbmn!v|H*ndvMW=Z_HZ]H7F((o* ͯhE?GQјTx+A0w:Rᥕ4vr*'z쒰5-Nޒ$r/ll BC5-M:p =Ґ]lޜ>KϤ%)UIVK[a.ٝjNulvK- )@%#*ޚg`ckW11`Xd|^#ŋЉp+{7}Bk[xسU v30{_|lqȐ_<#&;wyhGު_Ȗ!qNQ2L:G]ROڑK؃W|7 ^/$@8fz;}e]pg%l"7D^yn[i>= [1=Gs{j}=8[Q,zIgxo r-cvD"yC0{n burVbCCSTݺ~ 6R6s 8E !>2@eB1a#CgiefUWbg#sA"REEgV=4ͬsںuNMW݀X{|V +OFTjVH ųnRQvQh.Cvv@lQ 2O~΄CYE| 8+ ~)w?w O* |_Nޱw8[lT{ҒW>֑lջB1Ǡ{6hKcdžh3~__I/=B`rBz;)} I=y?s}Ts}3˷4W0 _H4}%귨rMx5:ח^=Q"s2J ?/RF@ (` s:.?=` }w+燀un~~/ rRn4D SyR!ugys ( -+5%e< &}+uuE`yk uђSGv[[;G<6i3M,j}RJΡx⒏Bmi_^lA `'pbqdt8=RBVR[E0Ci(qz^hNYy8?d@znyhGK]Ze KLj"'h?{)?O "U) zK[F4~g4 ᷿g9@}I xE*qP Y/A&Q=F$.?="*Wv4:[ v :e|fή4wTG㨎a1dU/W_טzSG|B喕Q:P0~6u,rzGxkzuYqFcccMN%P%P ,צxp~8LuDt޼4(7Zlv]eq:5t2 5p$?gPai\K|n!+T9]5]oy_ɞ _d!kǐ" ^^ynӫXXin%';0pN%.aPdӅ> yC67ߞ;}dGI:`֦ G#O~m;G$Yں>.^мD5J : HhvVu/e*E'AKs[zpkr\9ܔ|qam}b5Y :V);+ udQ~\Z9oV~+#?@ ?E7 ѫ vN.sL:D9Nu_b+ #Luq(nHf˂Xlg…P+p2;wtAa4 /u.iԐY`]^ͻyo4U2u]m3TvmDbzlO8-0v9hnWc xcB|ׇMJ#KᨹM;vZ#ɸ$b$<o v-L71Gm؁viYEb]Ji".|6t z?L>tso=Ej?[!^B:_2aPZ1r~.E9`] c଼P}Yi/csC43Ym@@[6|kCZxcrD̑o@. /.2Ͽ? 9Q[ s'就.CL:W 2#S>GxY&ph(O٘N2!+yN swfn/h,[A/ ɇ :ѯ_p-pmlC(oy}p KzX%Z ~G}J>ė0:5K9iuU~뺅[)2f GL_Pf[&{ ,1$e a Y"}>8Lze:Ϲ`N L n;gU^d'k5 ?%ެՔ!>cl߭a[)đѣ īӐ0z^[7|7!ueqwy;8o7P&s%xz `1N X~1750ҽ=;qdzqe;šQ&yHwN&עmt2x'(榌d7v*+ek Z;|혠d!^с>>&S\cg :/q#y#0cK$ڼ'xufsSp)'wG7ϮU!1uа-W6(OmC׬%va28pɺ|Zm]!C5yy܃>Xްb\c*3^;rf+ϣnN~VT6/@'_st e\Hj;{:7k84tUtZy誧Gkiss$ ٓcWҶz^E_-8J{:|ɤ+6~-+U&-S^v-Քy1{"Yt[]#}X]&>=~,`E.}*ǗCUl[qzp/jI\ƶW16QGR7fBg"Ϙ f.hnikMO!;{#lk4؝N"lgE䓥#6ڄ|y~+dĦ,ju `Q<Ӟ[c>SLNQ| HN t( xxmqCy$.:13`e1[ϢJyPG7 k{؟dKzN#)7kf^Q.pX^bR2&MѦFg!-o{,G&im?] ?qIVŃ ڹɤg(.tLO;wnd'CΔ󅞡SC Ts3ID$~f3`* X u Ju0P%ХcA:ZԚUpހ Hc({Uߴ;8kՋ =Er q'%r*J 10.@«c#ce٬p #Z{B-DJh3hu̬xk"!E tt{vbHRu0L25{>8HN>+rb?+ŋ3iS3-%ԩC_sWDWu+i{o?] v;(`Еx7 g?cЮAtege*>s'Oo\u3 ;w&9Yq0caY_r:z_EpKy'uG{ yD?9}{a??l|q=( MhL9.ɟsacwb:I|9,o3cg}*ށ=I:5īrc;V{f?ۖ^1Yۖҕ^*GB mmhϕafٞ=V\Y[U+@y5<3A@EFoYcSsб] JY5&Duc:Ojs[l#PPlpu_1o6D͞H3ζ% pX/Qc+he)b;2T 8V^.^4@@舑oUrqVOE΅ZdtWéiƽfZ$଴Vid#8ęF茵B4(#} '?}w~+pnJyiT#]",1p:˨S5%P]τU߶Q 6:pã vە^yʹ "gbdFcW wf0v$K'FTl>h NpkPSlyV߄e3_usuY9S@,e9 I/Kg$Aslgy uT"]J8Zl q'_0GK 7n#)$'n%|'-d-| eplգpL6Ι2ည1%wpp`ET2Ȁ 8O]h- ̬.,Ćb:J8~h$7EbNjTuG=`{}֮? Q|ʤ&hDL_پCgҵӕ+$?HrL;|&ԁ=H•:t } 5vu:V$O3ƞ[&- *MࣲNς$> I)H[~vH, WP"3X䁋[*ЏrD9@xs&@C35YɖY0NYUA]c] H0^+tu&HPǮ+'Ae)#f֕I:cX(Kn~~Hzҗ~+vΧo84 otWޭ"/y}PW^a.o1s^ܘ#8snc/ hbռKD̤뵝p]}wVGzV:WT8 Y.c{z[ouO䍧~_V XDr8&^`YPRyGۉ_ ߺ'fĿl noOJ}r"=qxp&1{b}ΘF)71Uvk^Ls80qQS&]Ү8/;y]zB*Z6|{i?)X[ki^#eU )pNt~0?m9[ooPY PI$4}/+[7l&1c ,ty옭+s#I4G/ZOB5/&֣^n؞ʞedjRTi|t`ht|8ˮGy 9<[b‚6wVU~i+t`,7'*pe..#@֭حNYO#cRS]:G*V*&5C;zzV1J3p-ŖAwW7 +*M"C;eȃD {^-}K/(з@w=vVw`I]%u$gWGQNiᶼ$T! DDˀ&*l @phZ.`t2PiA<uRg+blRcL(ڐ JJhMJҁFlH1mh ((|o5OpTg-N8$l5=eM ZN#:ԇz[~¯֛Z t !89tg|2x?pb/3OT`l~[/={}{ErQG0+tFa O/k7Pӆe &V>M%պn$NM/կq,#ÜJzA/q>7o3 c`9mqq_wt\h*ǎ~狿GGOOwo:gX5REWWO(2p`]yOA;|GSo-(붦֮[GL=v쭭qυthT~xKb~ F=cMQ1]L> ^"S8P$wI3{o iUeG]ٟ~sE w?~9% FP><=wx챿8[Gr12?Sw>^:93 ngj[ţ7ʒ 2x=FX&ܷ])*ZڐC(BZP,r:6!oNTa[ emhj|nIŲl8xm uU>s2پژ 1uZ.R.ߴZ皩 z23U.y?u@cDNjUsƤguـ6V%mT t5Iv\e:8V۷S ^0['/EGp+|t:HQΖaTr4]ݞWF9yNت&D(xm̼vjP m5o!KT\GE9V5[LZ5!&RQ"&h4ʌ3Оsձ5fG-&(&fhQY~9}Ɖw4o57D@IDATg7ikkc <R =- 1Y1 rBBx3?HEѣo7yb) o$`_Y9B1#Yg_*t6$u^ vPV(K"kޛ~K_$N|"hM|L<ܓwkꌀ|uFx Sl(6+Ij y`qEd\6g%CwE4gXg acG~h'4&TXo\붆~K7 ձΜU {+u[JpXMl`u<BãO?Sou,8cm3=/}M+,)!4]]$ʶ 9+fљB<X)~IWE,31*0 " ۉ]xn%-k&:gp43G87W]ɽo('H$NƊ2Qq@^j'%"|NUv5GY"_\ /ز2]NS7pG64KT?슡oiEH,\Sg G"j9S^yNRqP&=]ǞiQڱ&3_EIH>mL z]h^ܯݾHEK25i.8ˀɐN\Y-ԡiD@Sc8C}_syl;Qp @x&`ƱWY'ۃz鳟ԯc{mAJPqn_ǍBÛ=|,ǹiHQ.~Gg{\=2 {c֢tu0J2,#ע깁$nmCe %kgFbx`RZ8gLT gvuy5> 8Hv,l##i ~([CJ^:= } X[,Y_c$vd& GgˤseNv܊I" [8gTzoqWfQe A5\` W^{dy&}sn3F$AHs%r ;)WWrZo׻5]{4#is$b&(0D$Nht9y=3RI.f5+p$9۠3p#Ʀou,ǩD=- 96Ҳs{֚6}<~cOw}w:M0Ġ //9O񙕹8+ \co_ĝU78gdzCiyC)AcZG/)w (\AL9Mt gS_|fMF L&-Ŗ[Al!0l~wozf+l҉ |/sbڳ/?z._J}mZ?,t'ρR njTroTh3p,bִMBWG.Tor[pZoyjY126H_b}[[̰ y}v^`SXnYAFhmxЗ ,~B~0/a/v"fQ/GG:\UdR>N<1-J0022H~Ί"ʪ'pzb82 V`WilD3gzs@{%LfV|^6+ Uޓh6j`cX쮦Sa?qi:A͠Wb0P ml-@0E^uY7:4=}LGxOkǎ~ַ~z_?fV>]wyFh }3 xcitмs70JwPUݵ;_ YizG?Ga;{:u=Ddk_Dޅ!&=c7o[Wb0*7|8g0Α O|嗞5۱W_{)F!s ͵,;2w=tu$'CЉhW+/<+'PLC? \xW->OzL4:G}CI*?oQ~* K9MTn: ?@\miJ `*&Hh(WtE*UMf2:m\SWO Hr2l, rL9qSBުTէb[%]CF4t$snT]Etj~'k7ua❴.bKIosxޡ#eeD g#S5THfvN]hY,yS=83xf^ǧ*&<[~9 -V~1og(vPiH7jGs'iIKbYjH׌grȀ'wו0j(f5;:tDEEVjDFjg킓};޼^[~\ :q= k[ Uz$ʣlD-bzٽ.ӍCaPXC |-IS& NQʅhsDI+$@Up&A&>Q# FYqDeXA7SE<څ.X|R:ڹDngd-Dк7}Ya014kݪy4)h>VÆqO_92" 0tӀV"FB/`x6u9E`~ ޥM,t 3z̒{8W })7?/ C;4{5w7ూk5Z,˝K8LYz8իfI:=0N bq-?#aG}/]|]49Kev'p? 1 JZ͗k7" l"@w< g Mg!ɟrMj-k7KR[w5ݘ5vm0 Kì_љM,㱬 ]vҮ7 Q < `hH?B1*&--cZ&/?g*Y@._8zLA{`8lu}mM!؊ߗzx Y.YtvAaM|M xvv:&oDiQ<3>܏yy?;y4^}XzҵWo.WLGݑ/p~T@cqׅ6YGC[gde0e^⹳Y(8-"ٸB ;l-SѰN.. ]TJw5kKd4U.^cOC<+iAk!l:AsNMiE  &| ɴ=lNW2t l[{U9AyWN DzI6Tۉ"bJ!~q X9Nd* {.K"Yd6Al,5gk__Md1u3 uo,K6'h(s36 }89c(PE l`"Y rs龴jgؽJaɵ5g~|e6mzrr8[: 36νS?sI?>^ݖM&~Ɋ412&ϓp1Zhs\ഃw"`S[oIz_QQ>ւ>^ʺ8 pD^@CrD u@啭| <|?yBtzco,aCL &Grx|G> ![W3q#w[GNZ;A}ńw}Ů7%d5A)#X ߗ{cO\_?_pd:]&%Xuh [cًgrW9WمbkLdE^2mE,1pg2LW)k[؉[>IB  ĿSʪrΙ *IMSV¬(AId.2-Af[0 ZR H*|d7C@M{5T*V. C0TZx"ɕe?L PFG9;t^N @5*:3kljIE^gjߵ/ #N({%l?]vF)?C@0L)(*"n(8wGŴJ e2d{v*C:̀SjsQ^KlXU9.MC2w=Tuvtt>—>P~qBxTFPnӧNG]ݡ|\u{2 mG$ٳ/=ǂc=f8} 檿ʬ?(#S?+ M=`y#pEco}kw^QhT&(HYgIk4QZ )ĪlEf񑣷p䶥|!8x EQ\2J|oܫխ?Reᄂ LuvШUZZvF2~Gvfk(:^8MNʑ-Ŵ+5.2fh 8c:ۄRFIJd=Q}G*_-;: $N}狩c2<]Gpܻ #fQ 1";-_ ʿ9r2 [P2K 뎁"pmb|$X營#gc`KTY 9rFr7o% ~3Mƈt@qlWc]qp{{i,st9Btas՗ed/@?"|0Ǩ@Y#M舑o(^zm+h͎beAOl8tH`@VԦ!gLx+ѥYsK[!lXoNFnʳ~v"WXM/Z玜Mc+[^S9C&,ϰ*70kkpE0#Hm\ǼW ęLq-T7u-Y$#UqS?~.lv *Zmpu8#L4&?|㋳Q;n w YF+Mt9D[Ċ2 *̒8[F|8쿊$Z種2K_vYs$Ae3蟜v|W8}? 9ulY"̂%:*Lоze5$Zci8nI!* t6ipRV613VgjrԄI8jaDmm>gqҫ-i{I8^JUQa\lo$<8#YmM^&g46cS-R, p20=Wq1>ߣ |wsyZPn:ٜ@8ܽns+xSJc;jE'?X=H&ᡳϼT;PSE=g;=93m=IѦg3|=Yy]; )k"Z`oUez8wwuL%4ĩ̉jY\#gjJU8[ ^Ź ,>d=iBWOO`'^LkQ /g3dV }T:muGpX(? *tfk{v1?U٩,uLɐS#A= 5d/_2V@H+,4G]R٦HsWW7,pYB߮I\ԶpmAjussc7:WLXEޣb_|fbPCExK&BCkq +uB&c8[N@k%)_@D/h؇#9R]~gG{mR|dՠSƕy)Ӄ(a#X `~~G\:x%н:}LKKk ԩX+V ۤk3ﺢx/zqoG,O9Xѭ>&t3X""B|>%;8YE824]&s=g˂5o*y|%pfRTю> cF2p?qE,]yiض+W1>Q7/,.ڣ&|kԳGUMnS^ʻ=?TU֋Ɩq2ytyN4-׾FRIvR(!9ݵh&f$e~{6uæCf&)Y66Fl^i ^0.:mTJv8WIZ:ڛSO|bV`rBaA8_Yk T2ꬡ5fF 37)JPQ}`ooCˋө( (WkI"sֻy'ܖNńeшJ@j T ӝ.?GRzjm ?uQ;vtou#[G'ijn #SmNeY+uUNMV@уǛl/i[a[޻#+JDx(rwni+G.|ְ Z|`gHCF|e2\d#+40.2$%7놰Yĕȿw:<"*g؏{N(:ʂmoL)-Y:A[Z~n |0^.B\Ί'M_ ^FG//4Tt3f`heM#QƤJkpT ^dcTI= [X:-*/.:r Y{ܔ$4:3;[ "%;FctV2*q ~ X] tCl[]焐fUT=ؓm#>Z.J(=@.CG S'iRZ 4KM0b&{GN݄9s:uwS1LyHCV/75Ӗ ,|Թ fOk~+lͬCїRX>>:|σ6F7H6B5(`Xiy/:>-7k8EF]@da A<$TtM8u;?ZՙpXws5wOFvVvS4*QJ {  ,_xw| B əb"p LdY߅Ϥ=T M[ӉaҚY̨62MGkw0:tX3*+UN&;FPQ9{-6,taK4̩‚#M2'-Ҏ rvoudy5۰#xAi^RI*O40C4 2ǶFƢN a<>AfA<[:7ziRهI눶ҬAσðYa0@ua4W[ڛS 뵂[gp+6:ǺI$4'tv\ G1tyBuc/ﱡ=ouwĩ=Y:] 9o]^zص #/ wp 9eGKA%vXx/~ 2_I4(1lE֮#5i-;&QxVV2XVϕJYqY ]~l+,m= atѶBFU<VGq&t8 OXUuY^n}{n/}D`* K?=&9v.-P9Gǖ̽OX7)#RW7Ia .Rs :A:. Np!Hx6F7PcA8c CON.J סMH J3$KzbE$ ڭwOß~WLd)ـ> AZ,qFGWF)^=;{GRHU'R.oUfk[:#bgV*ڞt&oPf?>30\ G0(G- SW(3@}'K(k]%e1A;zWna/rҧ:ywKUlfF;Mӿ&./mFߞGW+ߢk>=83N0)[u/%`a!f,]uל pf^= `!R+L0c'` LGO1n ,6)`ԃ}#;r dkt& @D>(]3/f߼0<6$;Z;~AimqFϜz'u0*#i"VZSGY3 (/Pv[: ^OǶ˳wEHr_Aa/A}8AIu1Թ\I<)j W_HGs<; 6~]$L= 74<)'IuD|_\ s%zN&y\#z:oʳR P ^,]=uoe8RyY> +r-bd~vd_ڻ1 Lp&!' ?G.in|nEi`쮵yi@J:+ [q3<>i/]O?[t\B=xg9;K/Ĉ;>^"A:O[4:>ࡄq2:~~1d[w0*zm-6FqAO>nQuixۂN\Agޞѧ}-v%ב_LΛd^v`LkiŪyt0'V;H#s>zƀP\{]";Rސs$G|6o,M"ÃDokd9*Xxy]g~g0U| H]]/P!v Wv} *Ɨ^[[zz/?;kVa8NhɱK?JbFʑ?ii~ ~x{Jn X%߇ӟul"Iu"8ZT_Kol^V^ty*QZq W0.w>UN^vשю!6*QR򣍆Ъ'h aC:V Uaf[+[vd!}W} ƟU4qva?L`rhhS>O?q2nesy_]U@T/KV~粊N(o+S͠J?ORq*!s"mu z38uئxU98}o ]yzd,'f0Le%3eM#zGSQWR: 1peMqwU~Zo} 逯*>I;'/ɹԼZNg}+'\K*!0sG<*°5ߊ̠hasn!*Amt_$S9hqS9XJ0]U56-$h3@eS(fAFsF,8zuP wq< |]AV/#5 0$ }7.Nn5h0dsѡ1>&r6~^ 2JL#s?UNg:攟:|y+|ocd7ԇp<,Y6 8Qϼ62J6h:;{WYk /~g}7ZC nŠ.Xd5ksZ>{(pSϪbv:"5Fs j0=_7q=\3y{ט] N'eϭ\:b\E9+8ᰎq/qE#_N>95*4F}?Q,\0S XՉ\6K8$pw-%׹K4{q/sfzU^OvjD }sssqa:m*Q{QdjuEytՈFM8VWDH хSJp=zAӮ8:` B~cnw :</:G;"EoŅ1B#z v)rGE̲&ʲՑ` x<{@qC &.'vҧAm/:cښuF.A/unqY;ҳۗkq|yboӋZ`~FcI7Œo%FsHT qurSŔi7o*ܭƨhPPI[[ޒBp >qmHFᳰY8;Dy~ޥ{_, +*ػn`˩mK~fvɡ{|K=ɱ>W}Ps?}gkp KHO$G{^Q,lGc͍sUBtlre4ת@@j)|ch\8PSk0VtYD]A2}AzL5T#b޴ &[*L1S@K2sE,KٳQPJb 3>5bS>.8 ~y.39{y/jV#&H?rSζZʣLE@IDAT`8FOu5FW)|J]leUߥ+{>vGby{K5~t,/з6\~2?W.Jg e-su^h|5Y|)1$ȁf3D{Lޔ˪ 'ku ]QK8ȅv!jJa`lkR]nƔA:a `7*.3KU'*q3R ;#gKp{ JÍ|(}EF=A<&ag[EsHG*6ҍxu=s&?Dj耐3#I&l rK1F7Z+C} =]0{a#>uYM=JtYx Z7D@@3ܧޥ*ȭ(q ;kUQ~myRG{GzVBgnF¯~bA_]-G{ϧ=t=BYS@w{δ}[mzcҍ@q6w~p*ѸpWǯCp@UN?〧D}=̔C?O)0榟=s:vUxJn:v1We-Zӛtw7H0iß::T_A${Hw&ir:J4hC =h$T|*6XY]]SB_uԇ=g9+fWPSrXz:7 l$rvȘ&AƗg\%O[F$ t# xjVbkȠZ"SNDɑ!2p;d0 ҍAckw^/MGQi?8?!a&TqQQ|$G|eq*[u2Y15oSiw>F"t .́F(:G^`FMY諬I^ɖp"URV&x:-tu 4)$DyfTjKr?l)pCSe"G< UҠm q xFToAg$ǐ0gqG2UX2AHEN?H☵YmD|raH#95~Qx:6Qf,`t*]TF#w# ߹;_l)< MgbCB]&x./:t#6\g0lo9ф'm'_]Wqe\i`M=Xn2083G o5čvt㾫X&^qE[ 9Dd: A$T *.*v [.ywk9O)]8DC;SKc}Ts.?|(K478\Q2IF ^"# Z+&HLi% 31/ $ BҶŗ4'*e&ZIvKy5Nl |kS!+OOr^ybw>? "S"Q*?pG%{gЫ*-M-8盙}HMs,1.O4+C:ܕMh~d:pz[0WkF=yًk~gUMۓzG:6k8)̚(v:TmJee3v_<v"/y|;!gؓ:wtb L{et e{mg) %d_?aC8.$p d7uC_n`;yx ԅaX?kҨ⊟O%tt$O~$=G;"hٗAS =]@0yQI\g:m} X {32> ήj`OmB_'k{<[֫N`7L`@s.LJil7seYt_\õ!xϭ^&"~\4ΐ 1QІd n.;BM*ю5ACiq#T+ϣvO;on݃l+ 8_+^_bR<>x>U[vfgӽc HnAKM.Z"JMYϮ.'^M؆@vHHZƨt\^զE:+붧9dϫ=琥i;]+ Գ1i? t[^I@wq*!7m%9g_鐅Ovd +k!TQnU>^QMGC\}ϥiꨄfsF8'k4*&sQ( @Syl⏱۝4f$:D:ʣÃD\:+%U_(|=Fmo&iK}Δ6BU_zV0Yܷxt C擌,`H(|#$xc錛;NLj'#+c iB0a(gsz*:t8 )8pardب9LdBs3T7EkL!ԓ Fk KkꚙՊ1LAxn7c$| b)hUٶ6 ;©n"Y.% KCAe!M(m{rizw?DEhį#'P Yx?O{ f{ώх@ pa[sn1ÇoGq,hqi7پ+?2DN{r#|> ARɰOO3mߥ{lI=̱I_滏={>2FߖK_LğwQ}/IzGU~;(`ydxM/\T;Gw7eCl,F[yaq+Q/|wv&F;o-LMox(Vt:rwJWܑs wCOOg{Sߓce>o-/喲NYn{߈f>Ym04ew`MD|5gqTԄPaT}Ť5I蹜:n3\u빓&[ֵ93 R ,i./g[W9Z霶z*9~*:KT)ʕU9Gk&On2EGOH(Q^ZIWn.C7Xyy pD}OBIU9OIr"}[YDGWʶݥʻx84G+MqMAg(VJʭipެkĩ`$׾TPMPu*]>:z)g . i8`8o]<1Lֳ=4X6 qCS0*tX`VTӘ:t鴵UutVjY罭RY3MzYIT`sƚ2uS~s4F q+ [l]w2+a4䳼NnF@E|S⺵q- dypKdžϏ*1YGp> Vp,e_; de8x==0.e;;0=CP5{U|WV׻ kX\_hW`KҠ9_kgpWc?0\92m ufqƃAmqXlQ3^"7IdV\ G #yg!Á  : ʒ` Z7 EqS7K2^g&/PVDԌg /W3qn\wL\i} چ]q< q_篩ww3 Ws^eL8gu8O$|CC,gL Ζnml\xZ-,r9{&&IqX:>te~b`NxepBY#~M ݘt+[5=q \.\f6<|m0g+|tW9GrH߳{7- !0~!,VaW8AYP *d87hɼk< ~%er ᴌzTU0Ҫh f%VΈDS.V2p?{#0 *" :v˕9O`/[:t£t"CqWj#% 7ƹx@)-6HÆs׬=IP[vA6O$|aN2XeEyd{#@1Ѯh;v^b{OGh `G"eu` x sϮ!H)Y~zS&㠗:|֐'"!!|\ݸ{-OQg2tUoX sç0d/Uax \)Օc"P &pp[ѵ{_t̒Lh,H}#=>ڭ}}$+瞑ؑ[$ܱx v?#sgz o zq%~.].V"f#8nɵt{=lhl}j !뭂u&x_8+ ,R)aor~k,r.s$p3&^3]d W" V}a忭@UT'|v[.o>[n+V B.'ŮTK% \qdp[{c$h |ʑZ^STDL!44iWl3d" `ىyrz{Үv7t5sU)IӜ&/~T(rR]#5zItu-GA)I&\_glQYĞXߨ!Ka":EƼu}W ^C!nI3lKA7,hpMXFu'61I9kw7z8c6eg4z\NZ.o>Me&gF9D:s F}ǽ9LSUUJIys>s3PWEyې>}:bI~Ƀ%e2|ו,PKN@A}AQwy4mŌ7Jv&x_" c=NuAevvB|tY\# 甝q&S((Kԋ7Ky3?q`˸hd1,~ơ=V ~B':UU>L(X,3+v@o CʌLUfSXIiAv& dFm-͊r&:+5;?0 B,!mN>8N7ƖV~9CPeZ[]O(YuQ}BqPlF{N}1k[#;'A ?'"}S!,rN0[{3u"` K9=xm^m|1i2snDe3z=d&j֕<Ϝ-dho8H[qD?ģCqi0n0VCt[9SCI8i Vؘ1lfsV$ HN0v0Tpxyߗ^5a86jj5#gih3H _f:<33g {71?|G[-?fI;]s=b4 Gܷ0 T[~gmmT7 :0rsd%GL x(OP&2dg,\/ Nlo6bxOꝷNy ~=pV8Z ˌ6B&_? :t6ʵ̪[%Ȟ+qS 8I |ND\c7I{D nq? !`=+u>Lx |/';s^O=>N\+< s&=8I`oV([\4=e }&bIvT+Yjn|9F*Q:rrפx0RCǘAyiM!c5y҄:rQqv|Q [Nq)N: dkϣѾ ep6 avE!R& xdAۨz/sx/-pp#HĭXUc[T/:)6h׽˟كA9gG}hK<+r&:r* Ϣ*wtkji39ΣW&x q$$mO؂?K<cN4|("ghmՐ3 bNZwI*2=*!ӊ=?z;tH <1aOWgK&u4%G>+w(wmx277= V7KpY-d-(֘Mj*Vφ䟈ʡ%x+o_"hXC"6>#0t[i=iҰ:5O>l+XkP(LRϑ-uikgځ IS7DIb\=w1~}㩷RQKgOMg;/O]֦CZ^}7tX{?νm%qiMYL u ?8sݾ3 ZjL`SOӒOib__-eFλo֏_ tpE4@:aQƺ7}y6$qfS\GyB9L$rv-qˉ*wu t.{(γY^ B/`,S٘N=+C!Ks8RQ69~M m e/(y#_s4jΝ]dg}X21@uP2+–8KEd)A)AqPfm a? !G5xnR4hA)!2NkQ̲m F=VԶ3D et5m=p_"}m~w}LCoQB ZyOK/{} X+D}9 'fsgQ{"(v޷okNÀ6%r+:;?^xᇴٙu/|D(smV1}˟9UQdjki R!ɤ|T?}(/aG]+3Q& Fw BtE[e*X*X=A/|~'{pմ|C}=VYTk]|B85ҿHbv 0c4cΫI"h^\s8} I/*N^C Q 8=r_ }*նâOr}o!@@5H 9JEU%9/%UZ7a%$$U$DW583lg0wPBEB}a(0t8zǞxYҧζy xT8iO>:U|\o)T4 8Dϒ9f0&DϾ^ Ze(rc c9aʬE:6M,K 1䩄Ϫ.F5UJB 2B1pNk'$;$-u[\"N-HCVt0͗zժ>s +K6t#1&x4r* -mmN YD," i }=LdfFUnj0M<65DX {݄ɾFgљU@UGz%4}iA|x&U.I*K vڢ#AS_.Ȋ"=V#`WEJrAb\Gu?*K?ZE.LkEI&V۽pPgN-?4LfAm[~'=bQi RY\"`y{tk|ױ &hlh@v"oK*u"X4UA%:OE]AJ mu1\6ya8:KYZ\'AQn@ƭ- :UϪCxzdp ?eYzΣV@ 'Thѡ|hI qV QpTz{M|.Rx?5J,`M I ~B OĴ#Vl(g+1AЋ\oi0/ cWQZ!sc:HLz3Ƴt"0菙ޢBВNq$ loa) Me0tƲ8%pf ~mOGλ_ Xh[{0Q:p$t 4a(ZsU"~}0Vd\l3>zq1Vn΢7߸pw:qt0׉G\|_7* P:$ҁrxO#x+xr:0AN"6g VJ |4/nru*\1Ћ܊JŰ T)&X62S| PlNєeMdWȷX0# {>$Cw?_w>]f|IlK#x6[{0!Ym(SLgq%ҥtLF/g?KH'6{AnͣSEȵQ[R⼣:@sCtoKDcI߮_Yw0N"CGu!v8U+4zٯٙi :"3q;1ƌ9s)ˋf9t1A.VY$.g{`2q >I`Ftk0zT$u0,^ؕ>hN;UQ_E[\i~[BJ\tQy\Vs#H=HtOA)8^F>5OwUݨwй]!mJtNy1* eרrצs(K Dݤ }/p"\о/}W EE[PNkk;6 R_$!9H{kDZ OЮD6`/1ivNu4`L*-DƿD <68~u:+#H3`y l̝4>> oa;樑iJXy2>p+_e .DlNwr|Q\ z c&VSuRq^zajoHGSaabף\."v eU]AnUezZWLF$VܽClr^ErOmYjkN'P_Ϣ#Iv 6 YZ(rC+<$'9bT*M[yyt5 t2-m]yEZS-U˜ύ%br#m-l9`ǣ-a@$O+"-+k_aKMRɿG*iSyT|س9e:Gh)m^]  t둁U!o8c KD95¯iŔũ*aO{mWMȳېG 5ݢ#Je}1g/5j/GbfVo,΁O]rR3YmM0-:g f<zb0lBitDg:KB'Z@sctkyPQء@/M lR&)h4;z8ۦXWd`z_MU$2?s-iQQ Fq[A@#dATTQ!Pko|o[8$C)ICGiAj{"&;0pv* {ɔ,;iڵ)ʭv2h85P{CqWB@?CZI:V $Q1t*'M8M-EEA6Z/Ra+C?w\~JU*͑RKڕ/K\#EZձ:5~=,39y-azPu1ݎf摩|`s҈F]E өCXNJIsL:S"QeAqZ U|*R.Pʬr]"F0WBwz/A|=!(=>1t0Z-ߡccV lc`; TȢחi7<Z}(1Dn-1L pzF&1!cR[٧⾅\4m{-[w9Fr/sԻ2:GБ+[%SopB { sn"2+ gպYug) yU:̂9e?*a(}Ĉ7!55ƋNs:L1xnl8pQag' H4futsVuh{֖ #eaaG\!R>; sWWC,1wZבZafND&yY+!le+ s_Mߣ?bϫo܋pl`1uσZEGQ-=pPKW r|*>K{<.U0wiߦ-IR^cc1l&8@ň4itOܛhAkKxlm2 B#=fnaDq0)m$ghҦAWx}5 UE^]k]±s@D ( ^֖ !ςzS0Ay$xLo`]‘G>ey3 :|@X({A)r$6Ĵ:U;s do|KO~#su"O[&Ih{H$آgx%{慓sQas uoG贄g1?c)L>jlfc%>>+IO GNc 2'к:AAϰU obZc2b$_n^BBɍM9KIǮU<#qGli"K Cގ 1̡y#d%*5]{qT6p\ЉIx8\i>|F=JmXoI]ix!%}nϼ[ 6\>]v.ѵGͬ*<22j}oaƌj;NY:q U9?_ФT[O76lڍv1`77xK':1`i#D[,u6^r[왛$sBXO A匃N-c uܳ peAFL ݦgЋzc(vݍi+{4h|&˨^A7s)w@C$2g\Z?& =`r7j,c,J 63c{ԑ2$7i)z&t++6CL55H-ـ۟d ?dr l=m"v/TnKİh͍LDlG~C汉.XCh?GV;pS\ekBӑ44=4PT%YviJy|+3^~˞'ϱImWu=fU[XMQyk&I{inbtZW'ROvtI 6W.tBG2\NA6sEi~٣dG1&7+ r]CYTp0=Dll%ZOoqRu]cÆٲ 7\HQ/3h <]Nu s&lե]o)_ [_&W=b0+ӫH: KEbѽ`v~9m,)L}M7U3tU,kQE@*Ytt݂#QgМ<#:@3â#}[5%!^,ϖLރ?iBUO$mրF9/{l#(^nqLWθ<KO, $_L5}]GG!IUv0=fAS^j_'?3yO;7mcY^*/Wȧ]< {MZ'L-%UQgnb5x1{-Z*Rf˺w]}S[i |^"8GOy3ӡ䕖D5u4qSl@a&](rC7埨vP^/8qIܑ9U2+޾Kଗxv1ڶߝ^?S2&;;DƠnۀT0TA^!D+Z3Sh4"2AŠBCdateKsQSSVFp^*#hY~`IJM͠U'=[N%-HqNF.${$+?|3Ц6C|qlnstJCevs3ћu ;G輜uvmqp>eoZc:lfK!JUŋ0'O_:Z <\B p|}K:yc%0wEZ O߯qM[7Upfԉgø9W׹Vi ,FפQCIG3i8AC9я(+(WO4iqD8Wxsa}>=R%O{WӗIl%:smii I ` mp,TA3G2|51+8PP;p0򉶿(^;7jqfxkO_N+x@ 49EЛ4Im!Q3ץSi09fUZLgخ͠'+ ([k_D:VݐKMy |Yv$)/>)Tg/ Qc/c5 b`S^3Pce!!}J{דޗ] $4M">װ h2<ps߂ɍ|?&thԊb9[5sVuu5ur7l:F'{D0WMz FS\f _a4רJI5Ž#m81F=yci ':9aF̲4XE+u( S6M<1|y0 7٪]9sg.8 /&TTa F׆;:;V\*XY\EXC kWU.L T]TgZ5X8`eWyvS/ֺ̜ ]ӱq:u7`҅U+RZ 6vya"i̽ =w)WysRH:S 5:fR?0P&E~`~lVcl<>[:Jl.-r<;;[8<սt:iM 7mXnpV"8An>xN\Cq&Y$CG+&)ro1й \VڋvNV(PRE<\"P)~)mґ2Z}$%Z׿{a+0?Xulwҕ<î:7 {`~.Һߏ ϔĶ4_@9G<#,TYoʛ%"ռ2}}/ؼs0IϽ~A%{wsu dIPw\s WL3&291 \A࣬_ǵA\ 4$>+D#Y)?q(&h}+G;7n0*!\ۄ \=uf ܓ}ȟ6so㔯fiWo.哰v/25P~0{="Exx>qIx [ATڝ'ө3q۟uקNZh^"`jΐ @#-WW$s/e Bĺ?7BBS3٭Xs4%C>e"z{zy/[8iYf0H'ur'.3|Edy(L_d,n.u7^9΀?sl`r:fS_]ޠsor?ۿo3ܧr$n +iSZ,n38kN?tzuH:5TY%D(6fkM}3`%T9:p.{u<׬ƢDv*vS۽Z]jԘsEDdN95DI-iQo4@>"+2tF:OE4ӹMB~(qȤ(qD'>jy&{TEV=O\;ʝs0GAҦcyؚj耪n-/ĭ|ooL78Vѫz;O4NaCOu-/+oq*gۙv5Fxv<}D^d~=KI<`_s RA]ڦ$y{l v߮[$Y,3Ai[Mm ,s78~T+Ud5] -ö#-;08rWgS"ԑu,wiЫ9"89N^ܥEQ/Au X^~(֫$t|P>v5uR±bG--Ko]N1c#+~t u)Q)mL+MN=c#=M Աn4AiG K ű_*ײVg o3Ƀ_~6|0lnYW3zn>m%D{{7Kkn= EKtOz\?rD!ㅽh.ea7^-v7/L1"Sts,'m\P"m𩫻A ,!bW}ó,)+Ol(t+07 6rPN/.$~2)2Q4,lK[^<<:A@0Uxfp|W}E;.~gtl۾PR;/kP Ǿ_<(ƀ*I_Ku'?ʗ pvss{R%/}"X>K弮AtDkSaO_IO)6Q ?65gYÎiż:g* B rg06;Bx] lKFlf>2z"xZÊT&Θ]:~^z9]czh:1Vc^|u1i#k ƸgGktjO#-tH[ᡌp0C]^љxU@ ilt"mA/QaӚ*qҬ]qM-οq.}3^mSk8Ŵ%MΜ77m,s}U(ЇmlZhMVCT}\^`HɾIW$  fW.0ujj<)_`0ު(Q 6FPD?^e^VE $)m"{]C9 s8`f8L@X!^'cmu~d ѡQ3Vz0$6nm9ͤx{µ[q2e ͻuV]Va0y~i p.<\5}eo_MY,-[%aYƽrxp~|=UQv!h`Ye|)aC凉LҧȐ) |/?\ob?uԣSDIp"k;)_"'r6xK qX u95R`rRF\|^3 X8?rpC@G .aD=zX%y4 d)u"a}X<Ĵ(> nԕc y@+CM)!t"_x TT#w:T %uc\1l:MzsKU,'qL@W^Ѳny0p G>[iZ-atG9 ]yYdG;6r=^!ᮣW[¿4 e;?ȍq_Gt8q[cOIe-;gPͶ9839 '湎q\;C 8GNom.;@¯r}ިn nen#9N&|BOM;r ?s68U.!HW:9lH/fuRv|3[ǀ:2-(4[Gp9:׻<>uXfI$19!t ߑGw8tL^>x /lm &gg=ҝG S G).%t;No%z@%wqi,Ȑ",~qcIp:k[,x&зOI{H]C\k#^\??DF ~>K͎m/}<$p:c㫗6"q~&J|/V [Λn#}P{I1ա4XPR !Ҳl0lnTx/rܘ 3m&V|/%>{ ;.:W,{Υ<`_h c%'?ER3<ǫHU'd]ΫV@u>"ՎSJaMI[mb8R-E$?ջ |Ϲ#$odB$h!/{̴{R&Y+KgdHEeTe_OEqqEk5<ĘW Efui| s3Ξ6c.] ha]Kv{[izY"opv1:Eگcdn;kiv#NJ}kF'AE|$ڕmz܀z:#VP~~v63؎v@B^سueD^OGN74QхB,=[`BC0{m JYBjռg/-CkAk;D8$EF v.fR(+XNaҹ#18(UTo>6lj蹜OXD5p.m3,޶/D7pƑ<:3i8pDuG:YtCMKq[V rsπo:;ȟAQ''x2E8ǽyzq$i9xggpтŵww 6YD(;l-dx'ʕ!IJg6Ӆ$M/z!pCʇ檠)k7P9T }-zHtcԯ.\r8ٔԐ"8s*®wm݄W u${i;//[c-/FpD Ǽ=^Ya§ KTG3Zox&.A5:t8'/6tcg|Yep AVy. eVP5*u4dz =%] 8+b8cK F>GWPn$3C@; n:}%j?STuvxn~"uyWcz H`bO?g2 g\jc6o?G gy4-h@!yp|Ssg8o0zH8tN%<<x^Bᳲ駟LЇ#oh3;~أI}1,]=iV?gƙi:F,{}jͷJ+u;9=48t9?z2=~S:GPGIw.]kU8?~c=YTGc.^<*MNӧ3Quތ/}/i֟>, K}kw?>ǻN~~| K|Ɍ7T.}w˧> I^k@rPrH:җB٨."%:MJ2[~=ZOS&󨠴DZaj&Ξ0F1,\k"(! \gd=3@ǝr$? LQt*x*ck ,6Ҫ \z5p+| ,X luoCSMOV5Ha\6I3*f4Tq4Seͫ0ʤꀇx JgqL;V8M`9ȾDgM-^͠#_a6 v`v([ΞB|GKYo8!.ޔuz244^@HΤ9;'m}'t xo;øvB1ޚ訯LY+zPCb;WpuAqE^!Jn>O8Agy'ARtO\ݸ[z~1ួvq<;ѩd#N} W8=;g<;E}  wy31$ӗm]IH-uw3@A z3IWp^gqaY|`$]EfթP79[9&~\Z܂&0tCNxom{O}+Z*v:F9oY{NC{ѽ3N1g\C&p&7уM.F_}] sjs.Z>f𿔣}67CZ{^z&z4!鼊mVL@v߷jFg#w9g~mAл-d]f691!IŽDG,_gV`KCfV~n v* oqՒ1Cɴ+NϼlSSkwEXPS)wSu -&KuTx2s鍡tumZP%?v du6-Ryf:uMgE_ V'4ZE&_2E ZbW ;_פJh /I;$LL!c!lѫvӉ>j"~m}-؞YpZ/L˻. /JŴ|FZV6AEFө5q]sLK샅HKbvz"\?tHءꐇēRh~5]&SUisꫧ.Uyh!6Ic&GҒLDŐYByko'}`QmG"+&i3ZGlcGnC5ɿ2:#? >N %|7ng{umܫ~$7g&=>GTNb=h_;n@¿*Jp' b‹ϥfhu-u_an_3?&8f_ù}SzgȀmM?҃T+oCQͯ݊PoY"8:5}VW,P.:`?Bhm$"m3C%=4;>ދCFFDZGϨ3;٥k NPlJz ^5ԇtk|iYͬ&NV춹OO c[G˯tk;Xl Fz**hÅr:?>ۢZB=9ii3W=E*' ζ0&47qsA0;* \+cߥg̗9xjWYcLz2pcSҐV&x0U'X?r(9Fv?yΝYŶ4K{MGf <蹒$mXC!z~bLZ$:E܀y7iI k~5ΤVΏ3846:Jw դ =1keAakGRe^APT00iyGND+GE$j36utdW^8c=cy4 RB<e$"46VB:nWzeha:ӲrU' X^炰Gܿ [a?5|9L>%!iBu~G=!H\O2G/nnQEWqWM o݅}PrDT @bvUAp<3FH{H;O+`pJ[P ؑsƀgVF2: vv" <`)ߊ2;WuA/C3(A1.Ѱ,򬻻i ;toArq~)!OX,8MSf-Idu i m_>D2YRlO47J1SQZS1+kU˅ t#4xnyzNS&n(yQ|&$FaܜM?wӝwݕG'G1Kd?v2 O/cLn^!Q„m!OR^i< [rkǘZpdp3 rsz٧ӧ!O g*WOR}{4|_J\A$kTŅ{ j05I)QȿC^W9u{]{{;vAjhO৹=p[V{,Qo$` WÇ5xr+]t,0qòXʓxu .:;]ǐ[$z֑ԦI?d|t5/OCҌ,r=+Nw1G!W@ 73 "a'E;7)g&v{mj2کnbbpıT+OˎK.NH Y̯Ϧ#m=n{^lB0o$C:+ T`KC 9cU:;輔&aXnF}Q6oyL1\W?o|GRzPヷ9 & @6n$kCLs<''co,' _u 2NV! 7i%W2*fׯO1-5ZZ>POi t<֨$+a\ǍAgQm8TfHF:S`hhz Pr/ LymLx7>~W,|2^Gպ*8U(ǨwHwc' `M=E޷:EV]'V?>f|5I0@+]н=}R?m>°A2N݁筪;?|w(]{)2_awZUĄ}}y8 #G=uw+ǒM$t.|ơ}h5*v)sUTLzo ?%R>x>\KVю%h_b>}󭑼b%o&|UТʡ;NJFYnȼSe*3ChegIdZ(Z,nH8V2unl,E9 G+$ᣁ_:l]UrFҠc l @6dg+(lШEy6l駞o#agي?CUUm+*7D x8BOVyTwV2›ɬ$<r ;?r'4/ cJ:#KCuz 70ڄ0ϱk6PN7. l4Ԃ 20*iɈxx &&Q]↰6u6x7eПѠt8p/oQm9(+ rZ-4~#?ta;?`Jn{5A9kdT7RQ. ?: qWYuj+USy!pR !&4:F$Y:?22r™f IaK;"t$ ֭,~ K[8Nd뱪R|aJxu|ᒿIOy<8zpetu@MP/CS8\"W|W;aGrBFz H- 쭷G{%̡Kؿ ,g6yy:CMcDÀmΈ_iC(UVdxwyTQm}ChKO= ǑmTb64()$reE%& #{ jRL^8{fiyuW xOfzz[l3ɐcu&V m_F0[H2Nhpuui+Zۼ`a?jOJ#Js'Ͻ>xڢZx:0n^\_P6ɬe*:1I'9I}&[Q ?!hQnvR'#,o/0Q53}T;mh-)>a~H'8 0EC@2l5F^ͥ`tl!쨘f@8Y ߩ@W ,vgIF^DߪNؓE$D&AJRUߚfW6Wwͦc"-Oto"AEڴX#@YeNn3&%N5pȺQ R6P ~AmNJw}t9]!RXΑ% ų 9mrksXLN䶾y=rwTk\&D{2xt͇HLiR|x  G$ sJPW1$BLT@לKZ@7i &֠ULbt3b:oܣ9dT8bAtiT:&Bk݂iO!||lq33Dr`>8a.w3c$,jx:mPd2kң|N1YtY,dEX[XnGTy_†+t~)o 3&a8 I# s)++ vP(CjƠ社-@Q@;'`}_['_!H72kϔ!OMt OKǃﱫoPw:us(5E: 䠤86)ÀUNxF/0>)Dnl3慗^v0ffq8y:*ƚ>jCQ%9dNx?EϓO8n.1c ֹo(rv@ܻ}(#" yWdɪ8z&i\ڕ`"f G0U}-ZC{MT8jI:=S֓yAC剡$,PX?K(7 ~3utlPAO0>[(n^!!r:Hbi]x1 u9 l:w]uwu t[b D"A:UK Xmhh3djE$$CTtIgXqn*~*mV#HF/$c}ϋԡ䴲IL=ЧAD LW:+LvSNYWq #Ǫ(|d;*UVnh˧e~a- ^]aI$|:1㻶eILyD<ʰ"ֱ 2rLҜv^f7lUlɏeٵ$ #[EKE/NOĴw+`qFVXQcQ1f2" V^Ft$IU3iuqX!APsu<,!!`5v:lS<',xӡCP)+΅vWVzLpV*8HXZJXBDPQtԭ+4<=5\i?|?5u@Gpǖ+\wqyDv0yB\# ZSapPNj[Ka de Y.93TH$?xԿ r ^ꬱUi#C<#Kož74"2ϲÞps=?ɫ˘hk@`pdZR (I^48f";<`46Adl#!o#T?2CmƘ?~\ۊZz%T]سc.NGP&$^CŦ|8@x|]\"@V Mc:I=ӄ+}za 9옑#Luz|@z G^UTR1 goPy))מx#uCwѸrtcYZ?ww#H;uvwIPE}͆Ɣ!=a~Bg=n Ǵ50$ҹ뽑|$bT19N& Jsr: xĆtҜN#66UsU|nX3{~r?6i;0+/"#TyW>{Cv0 O)M!yVJ1fRqf0>?N3Sj?PSu=ur`.kANwvw7uzW|.^x}a.l`9Z/)+·bE W)fЗl9LXC糳SgĤnJp8nN~QEx>+( \bJE/ 1RG`L\y-4QV_ҋs#XcD/^yXcwOG)K>tqQ3L+<&TQE z6] #d8oQ:աs/c}l"ӿ-$3&6${1v156:^yyH?z`P0x[MK,Ǘ! =@~c|yjgv'?2|Xtb.0hKՒdk8fڙ[ΤOCgp}n<,!/~Lx>ƹ3ӑǂyUIPԩڤUtDgJv;ߘ>دn򛉻Y?_ؿL,&PW_.d(b(7Gp<O:m,GS~9JG&ba?Ek*%ąa8F .<@}M(׺B z#UZQu(4;p2B<> yצJe GRi6AFm(׿o+J<7 )V &gOxf#w٦Xsg@fhn:8oG9mCeV--֕9\kf/ʶg׮"Y^KeWC{nq5ϮA7n}bgtcWeիsϩ.g~#|T<ӹ^S\ /7m4ߝ)[@(mleJՅ;O`k\=7/_7RKMAL8[YipĬ ``R yi|]L]`۬bzd73kIo!CyYU ȑD919VثI*Q툞EK$B!;I 2: O{J#|nV8-Mıl&cZRc>k9G?[娅ߵ7ҽd+(c@)I+6*[ qt*5 C̏\0Gnݫ"^.Zm--m4偷LqAW0ќJPĥ^f&?ۤ\x!Sx| K }\V83;?10=5:Uix hLq8L"۶eTФcH` m?9:Ag Da-^y6' F"QgW;=5d DGAa Na᳼j¸4JI4dlΟ -9+Xg|]f Y)3}֣:&ttd~7xf gJfI #Xx65t2u_1\(1ضo/|?vlK/ ס%z-"iR t`Pww.^A y]g`F(:VAw.'mG9c-+lˬvItk-aAC%ֆ2pk4YdeFUp\gU_U! oВ]يKj[ghTrJ[% XA6V: "}:Ufyhp `뜥nu& lBb h̪WZ 'q]І*U+fB{@.*s0Ҙlzv>?ޣŝ{unQC[ ~70O+*)pqU鹵tzPwh'^E8w=;KxV.ڔ%LVlwk̂ev0[[:X`=yX >f]wxsʖ@ъ&::zqjUYs`&C]21 MHiep?;:@;fP[V;[ֽUu`u~|O^Gt\3B'.~s ,h~d'|*& p$K&Q0)cyv<\q~eЛO8҃&^|P.ԱokJVڭ7ߒx5*gHt/mWc_-puZ?y':<[  9sROww*:?g w-% [mgq)|$塗=δr Y&Q^ah w[tOTZOE_-pHU.n@>];CTR1Av7Wvw~[$vH:9ZhL/EL 4?я"cqL2I\("qI+FVVI ^#**A $[ν3|˄g>tDAgޓGCB^AwF?/_^={UHgHY(8 @ =I]},!`/qMN[)EWVeI¥9.?")MԔ:8kKG$h1wSG}WR1Ns^̦ C-[[ Go:qStrrtel#cX?=uNUvLS_$ו*W@߯vc$|ٳЖF9~J*O(܀mjZ6ҹԾu[to[W]Lx!@6OL*OP'_}ċܪ,i$Yiٲ;A,ϲ29KмG=u7 Cj~k&F͂i" ̀t- UcQA sF1BʹÖ'cA*]DogG*:x _; Xהv_^cJlR^"t}bhm-DD.[1 9Bc$Ѧ&s# {G TY|H;JҥγlDLvIG7`"!b}^_Ŷ%%CT`O KHpTk3\.SoLP@- >~e [K=MR}h LUAA=t#H?7>L j$ms#k8zH2V+DPS/v p ۟׼LW<Z|8Ih輐\=x$POw#7{셶J4I7aJ%72>i'pG}.a$؋+kJc1 ,𮦅ŇC!x uUHYRxeI=2":Di \'ɼ5pPۼŽ M<$|F%̗\zœr˄B(Yxt=OڤBvymJG !CΪ7c u-[@֏%Mveodƍ6GöҖ;ct@rrL9۵{eݑ_:ytz\<5͜;tT8xs-UKa>deY>2`jhEHrGְ c_~ !2tdķkqCF#cWzTYCkD\:~h:~L$BzϢ>;> ϤQMjݗP:Q}:ʋs{8Mg8x?Yõ>Fddڶ.*·ޞP$UmW}a7rG/nƿBfk9JnLjVS'80H~/]8>xJs&l߾-ǢoEAt+ ǔ\S<7U<;W%6Q hjj"19޹k7^e.pZsۺsR7&S栉nE\1KGJ T$br43@8[V:q9n0V}ige #r {?t_=?5x4ԧ3kpn{ⷉReeSm?kyʪ%<<\븗Cl rN` 7ôE#VaO@[o$;w4-?A`o@ k~?$%tbb݅$p:*FZhF ly]$:t^0OloxiU[$/y9UV t&<E?`b/uo0&Uю5sp[KH v~\넟DsL*#sTKxݲ>{Z:nM" (M3D RNp\ ~ )֩]zW[XAC;ڄ*;U0U[樐U{5) X7t^X0e14UY*t{ _`\RG/ ć77@\zVCou ?q Y{{;e|1ށ\3]'kd04@Z*sT#xNe=/W-J R\.D'uI>C:䧲NQ!z$+0X9K&d⋕W8y;v4Fgg`]ŝN]Sgdnmnv+Os~0ue[m$igCKcB)V-^wLJ0Ad_:r ai2_ik ӏ|Bə" ;X\ޠ.gٮD7d7;H7r8zɞ贬[ A|LGτ!٫Cd&':Ne ӥ >k@3:5Ad+t?4O >賎$τ̨4ſx|]*jn izy{3q>o.݈!ik7`c`s!L {v 4ouiM$/V|ŽoK}9.pa`eRfg._Z -).5~EП AWe6ז((13u{aX.ddޭ&E UDAA[* ,50< Y'!9y#k ೸$ZA+v*[ CX32ƳsQ a\Fe8Z`ngR!򨪯DQu^Mcg6Jy |h}3| L-CѦ$=O. ϼHtIAAbX]pf|YiVh" n-$'.Ձ*:_I]B`PQ[5Tߗ+h{n{1[l~GG3`,^*.⵩*OwKSVX ͡$?@D> wT 3IlW7YE9 2=V́_C ݠ#\V{+2ɃݛBSmL<3,LŗeyHEN1dϳɵ®y%tcW~")" LD>^t!?}M|@]#ʯ=F]ژO)HR$$', mQNcV,w9)VWBo#(vvCPD*S H]}HDW OaqmgY:COw_Yɻwٴҧ> Fׯ_/Ď-PI$JvN`yVX| K͍Jȉsa`&:'෢I* 53.`8R~7٭ e%UF;wyLtEf׎i}hJڟ2`8ub:덓q& }'MMCVqK=`A@i,}? ǟps-Χ@ڜoٲ5p@rЅ &2YfռN+O6o}<0宴sn ☇BF?v'PO@4СCQ5 s1tLH=`ٍ7ߚ:v( lAQ,CaFOZ[8|=nn!AJxҽ*}@!o#9Ud#lDӟ\#.mv4"qnq|Z=9Uy·N)_xU ]jL$TϹ. \ _6(NPH\u&.%^Hgqϐ0D []vrHl :t xPA6]#7k^cy5C{s[~Px{u{Y&mX9ev9|Kctǻ@>H-g ^N )1]wWR{{+r4"uOXsS@>a@Xvt t>C t.ˋ133Q-${+)#;B'{H c]ĕ@30ˋ^-ZTO7Pwh.xl%BzX{V֡G }u>zX9r$1B1 4nduQmfs3| sMR6kk 08i"C硞']~F:`U0Gܴƨ,'|>kb<'^ԓؚsK *v\%b{.G=Lvn嘃ɪx|^}${9L-&0 {P"OWR QQPҿ~W. ve05覝/zeI-/ioۨ$_S}?u)DϐoiIS%%qq*:>_ZVY У v9F3$إN'-R7]&x s':4 U3WjbHYyP:tx:07>YG;z-s==,&PI+m8yXs ~!n~ҋ7ЩV9eKj /y%[eV:NK<A>QoBV$s^fH[eW~ֱW6* GR ٱklބA8J&ii 6lm(i%$C݀3sUxFꆟ7_g[1w/l.~® =SgzTs`hopϡQa4 ]0Hig{gϝa%_='ɇjy[F%kkޱ}Kxx%䀎u;eNL},ϊ+Pꎎ%Nk|Z^a! &A%VZHROiKCr81X*#od|\ .+^//C{>rSBwЍAp@{u Gk & ga1h:=} XoL3$2Hg9cbI08VpS \o{Q*5l pi_8ī'sNtt9Bךة ?TPCV ?{'P‹/<˚q=C8/Mi{)>Nж 3I)ء{p|Vw$`5цT s?VmJܓw&M2Z[u~fRbuO8dT]xo9_t0`c`q ,^ďZ*v}GyhwVd3nDYSg $`FTܦxPr63*?a6xT*E$PuVy`k" MQ(j/*Ex:3Ԡ{ (4f! S/׹PykBv Hy<%tsZм-K9 C>F?=uUtJkğ6 d&4 Gllވ,&a/7uPi#tpA|1ANַg#6Ͽ=R̤# &QJV83&!(Sxz4:ZU ua&Tu@E;iWUk+6`k7k*,8KvD6fgADc 8I@3;o8+E|-鞘)Kr'B+#UFznTyI&:+mUvW! kDUP"[S WZ;6r\ĺʋIlv[7Cunjs U_rgU6FGFhX%=}=#RǓA2UA/|)}_O/:>}e3>`W>fqz8,g@!S 8uJ8zoK_,>h]MǎMwƫc}=s}UB0Jg~,m^?v8`t[W{9:U>aݶm'pH`$ʳu}Ͻlw wG~㟑p3L6*0Z`P OMF% I*8J1H Ʋ __%2\`m!c<݊\k>8FtE<049lKљa6:F  :&gL;\0P3g 1g{ĭПէ: f]2Yy,~Kl:4\)0R8\]/<˥ГNdprpʢpx dP5.LR^pYQ&K|^X{FأF52ճ}k:t0Qc4tz6τu`lޱA LޮBHkf'I +,ˋ &e>`>E.s ЩaNqBFظ>[| xYQl[+}i#ΗQ:NMg Yv+ +}LTQ >̾G&ѱ'ұLs$._ ? | #ج`m@ܦk }r@D %*mm&ul-8 ȉk= sLk&^<tdB8b+}8ց'S5E)uzzIΞ f@mV:TO*v 02@޴ms3b~:J P"Bg?#A`HV{ЊAj1*23QqN*+üͫJ4+s.]JHt*AQztw҂gҹ3g-:o3Z'ì3uBU֙hkf{v-YA1^JEwkwN zB@CZ4VDr!.:Ģ„X#tp[5^KL'g `cwoz0}}敎iiԟp~ܤm7|J>l+HSۺbNcel:¤GwYz-vpTu._5V+g)2 Q-i ֊έʷdVUwuv$e/ ?("ID=_2%lޮSϕV&I<_ngM3gP`K9B`nл<%HjMrF 2M,2"8z1D+ .gr,Xp:S'k.bmj@mo6KZŞ W#91:?7H%TTԧi9,65l8 198&#32W4|dkĞA5`O7Fv% SusG"vΠHfCT~|]mܣᦜ.OҚ:7DвmcEjڜsϛ <=^=r9v''7<@9G2kM$Us3 t L{"v{j'`痊cMX+}TeOӦcS~nN:RA3oڏUOѴo?G~qv:+s؊N5yX&‡&>^hʢusqz9~5F9Z/,єG}  ڧkҎ6fl4NPhvMX.0^pSYNNc =\>EGo)}O(\'=1~>c6|Ϥly2D8@2|CV2n[|my*§kґ eTf&QT(0BgYл%/&dy~!u$IW Hz frE<W i+CFq)Gf*NE&6F2b SXncM6TŽhaJ@؜+2qLHnBs*g&p@f۩0 `?"UQRE gpxd YA:Qr[M;]L~Nïs{8Ҙ3w g`>B 2pFbEMG+>|:t8?ʗӃ~;G(!>H P?Mkڝ,1G>WC0 {=s- t~@|_A4nN:o1=G~Gݻ@1~ȸ][jz߽TZjHH__, ";Ph}\?G;-{{w05!x|҃}/a)܄*B;ޓ?Ln?{W6 k (y뭷wODrs{s=jX+\~! hGaimϞ} ;cbιsBYn$aklCеEi;4p&4T]v5 3gfIJoLG\: pWKڝ F285X^}S@%12rݮX~MMdXOPyׯ?rs-qE]u ,^*Ug8mW!Qb`bHekKpZVtO35!(*u鴰JȀjbf PC];(:z25[_}57e!tLjHjjYM#B׸9᡼C7Ңua),T^2̈dj)[c1 ]Jy{+gOryܫAs{5=: K+k#4\ %+lѪQ¯B=crcDVJS:/-V0{T(pgaS0uiQ.א|y}V+-%Ηm{(nxxh5Rɥ/ee?loXe{A6@O`KA#={F-8I&AbE+:[..PyHeWҡ|RLaZK<$milwtTunNഴcc|o?pEӽ=S7~z7ɞ]`Ñ'%EY{CG"8'mD=H.]Sܚlp2J.H߄vcI=~<1-*y!\u'8x <'8+~٧<Wx7JniH&>G^\LY\hj(c Jh{[=yxKl[%_>""Q}b|2_GϔftlҩI03}U:+ $آfXiz^C~9~,VY#( t,SAw\ױLb}&pq htsj5`Pm`NPlV$eW<v7Ҧir%8q %$M۰( ?˚t.QHT٠l@2`>7_ G1Y(&/.O4(@◼kdx)'RZA 0Xn{ Ug! @G b bne(Nx7| Fpw[Лĝ !jk瞧#tfc'VIe)<]}vC=E]|RW1H_oʹ/SA}-PoX#(* t 3 -0;s0(CZX^Y \թ _ćoєd'iΠW>oTw}rwȢM֍ `7:eR!fWC=uLā-۶L6Lhih26('quҘIS2 k7P`;m.?bcS#Iv`ϣ wV=_N߷B[$Z@{5saD-B9RL&IiL]FV,wYkOy WԣE]]WZ쾩g akW:s/sV7zk{z&Iyig^Sv Xj5p 與Rz5see&P-I7l0dx.7Ϣ' @&Յ>70EqP=$A~a@_^c>h"qSouy>B;;DyE5U܃:}]8oKK WϘ-_5+!nus}d][\_]}L*4Dw NJ% a,!VOdn{h|& hRȺV|_"yA[C\[[%?;/Nr, S$9y܈I&2!tdA}f #G d+Dz#ظʇgϕ!Z;@,'9j71un$G<>s$DʧJw=ڑNҙm3;GmyF1s9RL71t5Lc7r>(G֚dGY:y`c|Df/vGb/fH.`hCV}]4g]M!ws"!utoҶ\*2*r8"8NUPmcg\4nmeMr sig!oy x9VBm o_KL7<]Y$< 8i{6!(sˤ2^RFh3A&=@W0g/dځg zo/1Y|.>??CNqy5|VUaa?"!{g+)gˑ&+#2K{%N};7UmUNGR`[F2y'S|GG6UD]+ܒ en"YB%,4Sp`s Z T _QҠ̭#eK2S`GGGf*M=J-!>cs-$^~ q+e^{_bbL!t13Ǖ2sd64f57Vɯ_ 0?oEH%4^&#[1Q/3[8qTzCm9Dsak֨|˖?cʬܙS8(7œM:G?$ 466+WƷ{hsy gmgW)1MmQzsWSlBX>ƛvygBq޾톨(B覛n;]f'~o-WݹM!KqĽ€BQ5fg-EsbW_ػ.;gqՓV7`,)mҩ#;# kLQ2nVw~>PBy:%[94fz@ Sidx0{D۶J?Jdj߶;EK]tXTٔ^: M۝ۙ=&{r^, z圆z I ԤёPMpG G0x`Uz:2=t~kex::T{:04tقqte'ar ΅2:mhy=z-^_8%fCϧ3x(TQ(BCv:= F$r7*dL A)l:#`^:lvO`x c)]̙9HbH5t1_Sey!3бΠƖX s]&-px-z o:tzLȨQdU5'vYc/L@7xΑtGC/30< >>›po GH@h8׮=Ec6"a;v*rۅ:^2E|fr{cINn9zg@~f:7chHzidw >/ q$mO>%pFnf?:m;x#^]A|7p6Jc}{>po$ZzE{cy WZZ_n8mBo TwO)0[X^sr6ƞyn4!CGg)" 5* dϏa8 O-%YHtgu`s\O>(^J:WKy$˫kYKZ8jԤB 32IC謴ǵӎuH /uddhgidgN$=3AN_ˏtʘ$|_v^3\!_WҘ]溚vk cſ7Q|gWo!zZGG6ww`|w.[&d רb 2s|ʯku?p/s]_ϼkn| A*8ST z8M;A $`0ǐbZ,%)h$8/s~|I_&YM(t>Ь2-Cx9;: @⠶Jv0m^&Ԣ6I^:}Ot 忎]yl*y @^\Tשp̬R1ؤFNtbQ^[wf H{og# E!Tpk3 b$QaSyNJMkeB<o)NAn fڭ4*YzJ>1X/n{,P|Gu[W,uNļ|fG]Lk^XɊ f#v:V)˥3[@WH%aApS:[$*+7A]3Gz|_fbגʹמR_d, ZTxfxad x64s0oynq!gX]AY8cd>T9Bz65mED_ T gd&Gؤ+8vct'Hv|Х$j#ֆ$G@Vj.j. ҳz4{a{mUЍA: q/aԅ]OK]/\jtSZˬŵA-W%$ItauF1M7%ػJ>]H'ҾыtHç:IȦ|l ?b0$xEK&{_J-YFz۴5zqrN4O`:*'>0pg{=/bol[D9pehNٞz'ҩ#$̣TMWTpGU=Csh|'eQ.S>=ٟ6?iԁc2|`A<q &_Lf4M-4e;(_>̂0qv TD6n*:uSu$v@*#2'Ӕ‘mM_@6A+-鹄AX*[']$U џoz]TQ]G s{[i4 +GGV%mjD2c;Gy`g! {}ӕ?}Qل>Ӫ楱\̚)Vpj+`+ hӊ$Љ5hYta` XƶUK5hN|̡41~8'i(/Omb:@?>3<]8N|# Gd֫asc}aE\UwE8^fv4C}V{QG3q9wp k\|Y4Ō&xH"ILMMM|y1>MSd3o48A\:z]o?giOݷ?⍎sgOz1 !<̱ラ41Aѽ/55="C. *2:*h@J13(7gA'n۞.?@e镗±4^-i'&J?}{鱇3 īR)ɣx~ykV5O`I j҃.B}){5ZWq+z ^ Ɩ^M!jkrWl=DrI*>4c`З^ɋcQ>jmm˶|6G^}%?_c MTQ=%`KkT@@'<"ekq]5w(U * apJA5X9s4 l铲-6=ÇLLݠ ?hX2u, ϶:7tFҮZ7>s:msk *~|_w-t@IDATU^b]~^HE-" qyky(U EV&QWH;c B ϡIf;ɐtё}Vl]=qq)';C#M"6EgDHD' u{0୺ luYqo.L9ҳcFGq8WnAZQҤRv!p e/Pr/|޳eLɏ)ɘh#m%:Jǐ/?uP}gceetE9-&dHo?Bk>uR^\ugzIA4QYDgƹI烃!C՗lp"gQBWP_}sT? }9Gp<*sAc$rcxa~$40~Wemsݚy9>CGgǒ~K^(߱:?D8NDA"G 5x#<||*IҹLWgSWRSY/3m;#nDK}d3Μ:Ns^}u~.u[d>M8~ ?d>Ê>>#5ܵ|̏, |sSA(vz6֣$x9<9g}׋[v*W-SQKO"13f/F9^%A_.u,nnp-^ Fɩ~=(8-cI=9w|FZ.> w(BVdg8/SwX@AFVR"?(gy0Ǫ_'y|#Qׇk3$|CNzտ?;py dzٽM+" isc ζBeZJF3ag׮on(a|===i&m@G:Rߌ }vo~%ح V xsdFtuʷ#0C1Cc*c=@/VZ@t}+# <3h)L lpP_;4OgO3Y 9h*d3IٳK 4F~GdCM&ۑݔ=r|#}eiqWҗ۵gMPR\L* ț4lHW,_0z֘0Sf l(OGD/ɒՃ}ᕅ+V(7w (`cj[ZVڋL>ƔҘנڥa)x͑p0;?A8m[_ WLTv+cl2]; &qc>+)%slCNбЌ=̉/PD PfI*tTdhN^n^d1 [6mq%Sa*S ˗.".`ϏZ)*/qCAzr|&γ8\* =?ع'TVd唡7G?E @hxW*gP]<[7[2e*])Rko'0~sϤHD <ڵkw%}or-AwU`vK?Rjz҇Rm'17o F{5 42όjtbS(>K8a7-{D0yh,ZQ[t#Stޚϗ8sp6[J@jMY[}uy ,pL9^{(m@wǏ/?xo.e$}I:tk<9VRٚe 3 Y~>-h( B z0ОNeԿF Z|W!Ns/Z-*GFXCW(.uaFҏ{#lx:J 96eYbN:.V7[~AgYWm53W!lU' 7]1xۀky>q:Olԣ!9(fG9 ?crHy=3;auB{KlOV%Ѫb2g/[F$uצ&IŪCo5wYvpÑ{:t"ͺ?|A'~8}xq{쑇"1ڎH>qow/~tt(0_yOt0ck`!zyg2Z_}۲Lxiq_i|"$ ?#PT*܄Ƶ|QjurY] !dt:2~ܹpE;0ٗofI əNlɶlٖdْcI"邢Z4(?@hPhSIڤI,cǒ(k_IqIq}߷>{ٴƖ+ g{==~;7wHϺw!S@JFxk̛p dLafkE+Y:-%2sܳbCǹf;!:Pq`0O۶eSNcy-/lde‘DGXǡz0t0& {~L|=wgғsˏKJ{N/ 砖mo4Ƞʼu D_^)C}] tcZ a)ؠTOio]# {_y k ֬ ҥi?B7*E+Ӿ'>G0OHk r,S%h.CBH n>1\tVWcsRZ/S)W=cu*+dwsƷձőHO ބoE:PpR{o^$ ,;1D[[+dK\]HR]q|gZ+3/_KM`i V(WD&C^K: 2ƿ;G l m%nk3߹@1rD8z>q`/ aQyI"҈[^ ?1{cB8פQFM(@1pޢNv& X3"c{.2 ?wsC;/ 'l!׭}!ҔAM{X3]ԽBܽ; pH7vP^*ݻ£=|@C$~ Gn/d&**i֭<1oњt{=/yzhŪW817"6-64E"#;yۡL\F!|& e\(W$:*9ɪ7~VY) 3=3}K$lsi2W2{С{Kp4; MMy5IvٴW؁XW35QH`F { ]i%6MKØdه" TRg_/%.֒W4H9nj}EFz6SRzQm+ b} 8]@/|XIƒ<#h߭T_zGASw+-Hvsst[A yI :_jk&v_/#0Q%V9gڵJ{$? 8F#_X'7&< z۪T+KX|$~䬾@2۟gSI1umQ"#-Uf%B W1 U] ^(lZ2"ULBGsc0 lxzC@JpV GPJ'd'$2{zpp_**]kEgo·1#/S2L>BPG[90}}sw.P38u,v>XTm~gt?K~ 1(o})=paT\/sDb|`?KO|mοͯ:YfT>s[ilʶȊ|gC ѳۿ!H2/^w GwΥ6#qys_ ښct*Γ(ө_zOSv9?8Zrʋt[𣏡6$NPΪctĵ;Otvޝtj6mck1QoFI ]XAypfjli$ Ӊ/.Zhuq`:tOJ%/r zpzPg=Po~3x8a`:KqyxQ{Ǐs|wK ݟ]SV^%gw`(n\l{muyʵpF VHD N஭笚=ITZU4=WH(&+vm' {FQCUpt**:o+ .@v⭌diem} bF:$AL42(JdoA6χnhf3IϞ9U% 0S$!S3FUC<ܕt&jh g PL#TjԗVO= j1Í L8l j̮s[`ٕK1p>w$*Pm֠`A:1Kv7\3H!YL,c&0Sjxa6i%2ot#c`At4CkoC91sg@4~5aS Vgh~fob]Hׯ\$C^:(!ZTrxqh+P:YV Ƹ40iu gj7alyLU#pцt'S{u|3 %[½9E _ل5̺ۜ=T@n+Ox4o&/~7бF;:6- }gb|XA-#FXbNti{o2G"eY7x(RƼk%-?c= G1Vqa|#JLE䍶Q? >HU:t\/kg.?xEn~H`V=1Zfw`'=,j*2q.<&g<)oz]]ǀI X?Mmhu7qv. gڤ&ݠer}$ Cwo/P9MY߃]z]~;+7uX%>) KsFO> l}F t +r[ŕV!)q67vt#2sw=6?#n?@߁++RQOR'1Æ[h2goZ9)|:)`t|F\JK }51`.<tNkhB4`a|Yq[~>Iʎ<.|w>z-ymCO+Atqf#W#2rO7n`d\W(!MQ.w49t} +[x#m)IA68'qQ9?jY~Omnr"H͘;rey7~6v)ՓL)uuq_š b톩 28W 6p'iXE?GubO̱\-,/,Nx9rRqM& _mW~g`_pl Ah=:D N~XUVar6upXe2yɓ{Y>lrxYv:P2`ʜpu q ɛq*DC70+]${د l+|.>e^L0ߌ\ٗC= {om ,b›v8U v@u4.v ʙW._NpM¬"0Ğ,oYv7[K҃:fˤ0 R+M{ 'ISO]JF d"5dPܟ+Cˋ|^$oOY#6^D"Fm +Q=iS[>^zzmI0;2O.*gװ9:'h ~^S]@)tQ޵m$! WTXen376qwh @`#mm{ McԛW#_9ND1vR\Ga.Z,KA,^# ^۵9B0IvHcz ʣgLq\}K|uOߥѮ>cOSމ+|{ vޫ}gRv96z*<׮yet[G7[Rmf>3cbf3҂RP N |7&HүCGm{wyl#LctA{8^kh$ٮD铯ZdĔ>VveW0 ܆^Df:s)3}By W! %0))"_ޗWKV& T_'`(2yYZp&*]]=}q.0dlˬb/Eo 83d~ -c+>\+wy1>v3 Z M>APTZ'8C|.#S=^JK_r:BM[Dϝ;~~+hu*ܛ0 _~%$$ȥ"7~p"7Ns=mao v``%ɓ8 >ztPƬ=vtױ@۝Ea.>t0`bO=EDF8Ѿ3#CNwݓoz>ioc#>gK^L֓ғg"Ѥb~1J/u<=98lk|W`S8xTJQaS++?FaZ]Qs)ﶥQ}~(g*tڸ:):5TS}y0'kw{3(VTVq:YꋜI;+}TGosCZЂz c9}ֱVS9xQKO)gPejb0p\> (0J@!:')/a,aV6{ldz ]dT7]g5ϸy5̌6 n$AK^ikkQ{3j0:#%1aG%e#﵎"'FgݿU%1& im,L?lX HҀt]ÂiahDZƺ]PVYkTvZ7N\3g roMH'LnagݶƾVֶ3TFǰ]V{qc4^; i?Gkh#̩'fkPVN$2 r}ʺr0gb@ :{Ƀ~ =:-XG6{@,Rϩ5d5L4_7A8:S;/y }^d}bo5;[qR{wO@ }| Ju Tn[ש]zGL&5D9) ̊@hϱ&?W).Ύ4"wB~U쑕>`@??=7Yyʓuh`W~iܻ f|a'E+C+} Z/+`_Xo0?^<'hHlո :Q:'3锪Ş%Pysz0u^NC2 N>cB~V[!߫$DsqIsEoVl}=8tH[t۾{MlʺK;9~GyؠAgfj5K"sXȃ^&0VMgv vQn[O[ф5ەzLn"u'_4ɮ/QyDzTFޞNơ22р{DV=?<7QCZ =) 0 ?`[( 'KЎ^8YcHI>]y"{cqD${Nlגɺs&dX >0= wC'4[K(k P@hI']WbSw)\O]]D.Nc Q2wub}2{1)ӓt`ϬPV}WK hA&K ћ~7~=Hs'y&2" (gvȦoq*^"sp~Y{<ysC>G ܔ'K2BQ ,r, eYνʆн?Kʈ ZW).AL@R3an翞][VL!D2"XZŭ-~V>& { [e *  , ԋB ✾i[[+hD` si8frYjH^D{SQ}$ɱǍ7N{ȣ<*3RYt_ZI1VM;/ Kwetg2/ŵkK&yAQyGC|4􊫗/WHlEV^B/iNׯ]EOZM} VުeYKOS3MqC$1?iHzY&τeWI tۖW\XC#\f=5M;,b9Uڻ/"~VKxt#Yltv\!xk(=Pz䑔zve'-=wس1T Ѽc|f aciw7 Mn,.a_V질,DwΟ\-ػt%uoL⯝:]9|A:Lj؋.w6RSf\`0IG*T455G*ŧSϭ & c/Jլo6: nyJJ'>juzgө8| }&Bo߾Q;9z_'(-'`)p]wu7>kO}E>!\>'S]ƹN!w%_Uza0+lAؙ)xAo*Q&u \ydݑYI@٢ǟ{&_7PY&]a7{1c=BRxz㜷Ƞv(2f3y"=?!j{΢ldQG lC:;'T8ѽV\gR*/}؛Yj z+uU)_>,~T:OǗ|V֡ v:<qNmXko~EQsr)nytdtlR(M|'b}:~1Wa{͜W:Bɺ)%/ED*t21y30&?bz*-.]8*5pv/1iHHYmph+ejMJVIX>D=28Scv˶c6}E0Zr  yy{ ՚յxh+Je]ƾQW@: Xm <6bϷw>hKCқ*N jN+lnNc!I#a$rdzh/0:vxwN槣`ccH\ލ ":*]d2y]dey]sglX lCr2M{3ߩg avmB*&\8yxA9w/ ! ‘(RO/>tBH13 qtHr .uF@"AՁpt4ju6f wWD+g|ݹo0y*/[{o|>,g6U h^Ҋ$k +*:0]7 +sy V m#1}H s.;j8N DI̽${D%-*kKIpA';Q ˯ j[WlOuK 7gØ:!7׹:9:F]I:rY YǼt\Wg:jǼ{P+d>p,u0Oy-2ؾ}72Ё-L1ho"|11UUP5g)jcuR瞗QG 8)WݛO} 6MxPcQEfYzlAx_8QwV(դMvoIRǖ^cᄐ[U??JLu/r|  TЀ=lb"p+KV) t0o C;c#$+]fy0\^/gR~iD9jNLpI*,2RՀԈgU{ys{c]|,/K*0H ]X', _Md f˙wwOg"VaPSMx.Bs T-[: QU1S}^ĺɹY>2y.}Ron$ .RfZG(Om[[.Qg s9ys]&INYZV`|9ѧ Bʯ8AY>~ ?eV\ yQVQ+g z1XTב\90o?ҹ7FfetkRU:>U$`PA12rSڵ_`mAy+kQɄ#?G`P_*2s?'Y(*}/SRx7yxs}V:VGCLhϊkqC{zG?V~"ĩ7h2ݬVTeOq]$ʏ?~"ɽw.?[gIֲY͋>鱩@Tw{>2s&cxpjN& ޞ]wCڶGƀe9x:GII&o-kAdᑾ^HЗGK4#Lhbف[Gn*cIۡÕP׶gsJ6{l >˷u}ꬑ<΂u6B>m1޽iws̅~dŸ]Ӎح&ha򖶚Etr.@IDATյ i`=[܎ >i>d>W@bAHz18'm߳M ssī8ijl&3V]Tݰz%y*C4?I8ho:7p^&l9DlJtM]ldkSKd9P nb37!L,\_N夈;p>x2uT"I&T8"la@}=GȡC<_LS+Iօ9ή=8drYJr}] v:3y~nŇ~fٖW03(闘4Ogൡ`FeBˀ{mI`(v3ZK}}TzfG;7~N_Zg1gݬ-ZfڅV<@v@GF8s0d {w 1g~0>1`b4#v: .G0mkQ{/ߌ R <@SJ:`8yHHh댿t-۞=> ;̟x<>૿֑>/2FK|PC{τRfK-Cm ^8>ϧ=1Մ?[M2YBcqs,UR!(H?; JH⇎Bu4b`wvv` y}a@3uP9gs]?Q>+Fҟgh4 Py/yP?YV kՑ0WH2諂̊WncMzdpod\! MTO!vZ/5ZO, : f*o{t 4DnMѕ\:_XI3Zc!FN9J  O- c̪ҀXpTި,x Ku5'23*:㎶_%ܳ9 O}yP:[ ( t ??xp8 e Ff±P&`cJ\-֪ r1l 2 w Qs:gµUXo6x9 ";g5-R64s"L ka;iT{9 r 1_f͘dkKTRalt]gAh{l cVp -:Ê_l}eFϚJv+*tRZjfddI>D=rKZ&ƭ^ou5q>;58۷T`{ ɟg[/+L <gpa"{" m{hy%&xy3OY!jX*֭ẓna>ja>GMҼN>2om-~2zs=%yR!dGp` >#ză-7R\Vb;o/ 욤V :k~G{A-Vϒ恗<ٱ-;lLu,kPtYg?V}|0+.(g|VWP01dO b-טJ&Óۘӱc]dl<,Ѕd \A;whL q΁p dÄ}^HTEtbU:[DlI>+t(\χ;t*O\,1\خ&8JbO2<(c/|gfJktN@@5Lt\=^gUR;%gkHc[ybRK sd%3ܘv'$"ǭpZđFp0T>6#ؑF|Lbzz"g Č*WJ 8:,]!#oЩYkPGb;8ccR.d.>$ߢcgN<V< DZb]aG3'8>s._ IX+dh+]i:t}c#GOhޤK.q--?>V~(LVy~!ORIO&6]Rމ3ʾ͡^x|% {9A iwm}=݃q:LlpVLLNAhws~88M#*Ѝ !-ҭ޼ձ 7/ p0V~<慾#uF+;vr.]յFe7HDnz)xܪU\\"P{g# b_XR,pϕFreқ2GTh֠p]<*pR+'Xe֪#؛*\@*#%uע s.d0匼MLQq{Gؒ_; `z 4P[H8Qi MY@>UY=\]F9/Keb6u4Eiϥa*w5E ܗYNU %85ۻgwG iq4!ְS;4 n'KW&( pGUvv26w졲fG'@!%oP7ab&^^<y_KAhPe Cqr&ʃ%)eү,# IfJU\kHM')9;;vUVmH N![M7W樷"'|@PAy0s]ͥIY|ʊrd庶iELh,~6},$tmd5j=Ռn1&c TYIhu{sx+Unn2?A-8~`pg)1T:*tr4~ytτ4AyJk(ieĥn# Αm0YCKEA#l7.׮]M/M CPA]|h)o*NV&B{/Z/ 4s!՝zc:xe:+T N- ?2\CYdDV߳wo$;>W)Þk \_|W#,8Q^&y3\ f'fvYN+{34rGvpsOt>|, 65mH|s@mmmZEфIPTWpow~x?B@R^`pvbpNK hsJ3i0_Y<껜%ýG{9>N~2;4`J`/EFPaܢqy{\)ө!K@%Yd Ƙ4lA݃C#$)xxdqRӮj:ڌW/s!:Dޘ`7Wi8~ <3*ǷoqULwꈫ&xRJe(R٨: jtu,FĚ`ܤ 癣ZI/\6A%Y:I:#߱wo/ݍN}6:{6ڿiwoo$nw[\;/**oe8mTg|_6΅ .?rvXmXtZo_q lGz%V\oEz4ͳ^aiF+FS.3ЏM2}9  ._*c8% ]]oݿG|W_C &u*pA*w&;tuJKi}Wr$CgU.{r 1A Pn}9anX/JXՇwq_>gR~V;0xnGZ$gOury@`oW:Mٱd4:3Ab2/L<I&ӗED:r=lGx;d.c* qpN'赏cQmep]x)w*8{mb Vq:40zYMN2.g_vQ\MKg|lzV Αgz.I?';07і 4iRǠ[ER=+E,2~  ʔdžܠ9>4Itƻ;h+kgл#tggv۝B]Q~־l65ds)u2 g7Q%*ޙ `&0Il6:ǘ*/mbܜ"Џg$ ctJǹ64 Xk%GP~"꫐MkM:'aV"l _8~+K&laۚ(  K44LVC˧@̕0wnn+qF{<8 ^[FRR#mvvχ fz|L:s޶"NBI²ɲeMs#TnS9I :͡H$޽{;M^":&m. /-CvCZ'7R4}js;հo$aMӢ@" ',1FKJ5.0詴mo/rk^%Q{cgvN)mSWLfxΞKǎ4tdT.GoY݆V mdu=Ӷv~by"d2 0b#MAϞ9]mK"8o~}$IMڠ G] dz~@09S,BOD'G2X8 ߕ,W/mVm9u 1WҊ$B {R0@{S]o=J_H҆|^|7@>-L#g2gO|!pt|:EA[H `I'(ܶـLU*㓣iUzn\2s$h.yqC"laEUG8:sH{84K]Jyj_P6dƫShRbzM5T#>tc>2K=l~| ܷM}}TtF W/("X7mM!;#JFOv^!CsVR ǚ+ ELtW_eٷBξ}5nSy 0ЁOp@FA{{HRH]G0VNqꐀCH~q&*N-fk&'gO3X_}8k^;V ĐQY\CDgli}~׹q%Rm5aIoycdssT<*bvRX+o`TN" ^Ñx-lRS 瑼Lnut^ijggAs:C1,̢ցn[`ۚӁl]f[N ʩS5סaզN&+YZ o~m;:&_AA^-7"MaQ8*0^p3[z'4a7˾5rk''ǠHuS8^{XN7^{sbN@\0qKNxh G30O45!Xqq6]ү{A d\ݚu|1?9a&]isa1TC#/ DE9':ppܟ8+d:f1ՁtI//ΐk=}Y؎5 [$۽$e?<^[vpa<7 5/*\UZ1eO9jeu ;+ 81x,m;s둻ο^5IKVbijto\ j;YuQd$ϚkfN҈{'0@6LJ6vVL܍_Bn|#~lQ|HYzY&ykxv49(n+'+m 3a >F޹UyGzMH!Y2w`rP> "0LJosCjUc m{yVͶ^ Eoϑ/%;' ځ Xߍml+eۿ: HX#~ ao]bk ԡz^L_嚶0AW_.QmPнXAҊzuGtnK*QdrW+غʎࡌk6M^):MҎ|M/M Bc~Ѝ8"sMW٥e[v8Ar|o+k ̫ `ʸ1;ixae]gq{}2 琷٠ "uϞY X2+{7 l1uB]څ 㭯Y) ??e:"u*B&n>baA-8CmE*<8OpW-}]ۀWN&hSIR Byb#[vc1p|u ˴[/wQ}$[XmX ,䭍 8 Z $Q}PI#N&[ҋQnԓP`yq+tHanE"oWt& K_־Eޫdx~<"@nigې #M)0a<@u{-ݯ j,soBx4 36ʅU>D2a53ׁo9G3Wė];ڰ  n[fCB9G p⏽9[kPyl>K]bRQN% 7ZZry2@Hcହ y`$r?>]8z`]Q ɭFW3qg]QU3&0'?BǾ)"hhVIia[p&xo%W†SN$VnC;A kGcn.WIcQe|f/6uU[e½Q}p`Λ'QSV_/=qq9@!웑t<:a*^Kk *8]M@ԉGs|&ƉÕe`B:(*Z`n6~]C v=<Ǝ3HggԺfNa[KqէIhio^;ߑp֢VB'F' !}07[bC|F>zyЮc!GmKc[lhbzbs;TGw=CAӑmevj45,tkӅ=E %yx,W# /c/:Xg/Y>&|US$c3SuEU ؘltr*,UOW;(Oɟ;W7_ y7ށ ]X"OPW6=;mبâMṬ|p(L1L`w]F^ڙB\k A2*RX7:2B:G]r >B>Kʹi9G؝vuh/$^MK#`Ņ`62`>Vf Q>Hr[^JZf'ɟj-9GX EטI%xx*`UnX L7eyYt/gyz6kV|2W]l"N}F wκR1rW!9uAM8F ?47SopܮihBo|0 F3<}dz2P"zѬS'u_~VRJR +;b1Ҝp^V<*$:WiUʥw2hiᅀ}hd c&&b<+$߹t1 8Ȧ ?Q:QE ¼C J㯧'_2~;ʾ޶T>s"S$ ܵhMSO?>JTx5]Dђ fܶʎ[ce^8Me8J4˯nk: ߥ!BG=/@?ÎQ mr1ѩc\Sޫc Вc`€Tu9:ZZC˖: TBww."){<6³0qa$6l:46^#VP"CwXcC(:4= (XـAFA\VX-A K;4 L)M\REgb +Tu8cj@C ʹ 3=YdUti͌cYeLΎ^q -g_s`*ydm81 8|q]6 X]0^ ^8'W8|[9>EF/X(& %#rSyTc;KP|EU"g;\(G#HZg: M-$`O2o&F5 g:u؊':sul 2NZ1&`𷊂$LשQcBNϙ` xdLG ̫A"n`$UD:g|#li$֑ˁrchhs֟.X1ksL/ʎuzŶF|su&Cᄔmp3\ YTHJM:Qfg-^Բ@:3 ?&.ymo ti\" hlˋ//87O^!П_|T0 l(z9Ztƨ>ɢM.+E k Cԓ@fm.ڶp2EqKDZx8١#G38"%, p>n)gc{hb&I18,,=Z'{&7&(xҽI/u^Q 2WťsC^dU3]|̇x(ADŽk7aDNtF]ޡLH=W֙@@~ji}iUyУ>Lⓕҁ"x Nsp:W \׹+G[^Ar݈`A`r8wAޮ,@L@';)mg pb.BG{:7n B>pai_=!-l{Ϟ4n;#\1=&/k$\7N`τs 4Q=km! `t2ϑvX^r/ӱǖVP'xW{Caձ&t"~y.ECT;&X.n9yR0;؈HhT"=3/ eN8y ޫY<S~UַQ̈8> 6Y;N/H?:#8 mWy\@{˖E‚m _Уlߜ\ow"G\tD~>feج3p"ֽ['d%]vL4?y:4:sL/R%zW D0v5 ?"HλM`ob "5fCT.nWh=5E$n!G5!4뀷z2pFH?|Vw9#KkSܱݿ4N5\Nlq|Kи m{coN6C&7c,So*L Lzil"v8oV^!>ۥ)u Q["GJcL^"ބ[oV ӽ;RkD S>j(n,ZAf}5oP>M9$t$i~Aw釒f< :-d c~TGзMw`b牗ROߡ^pBFɹ9B.g;a l pkc3 ewo;J5LR=-P;Jc]$[\='K]G]ݡƈHHəYL&&MV&)&w3N<͵SN57m)0YyV<7 M1gmMa~TIˤ w7:\!:ހ"pة;i9;As v@%z;D}@f}sazl.i@,$eUѦHYfYVKE[,T4eJR(BHQ$% ("l Sttxsn~Fr$0}{ .U,_&}yoUXN??.uFP*ڞێMG͗JW)iɔ|O@ӏ؟%qQ= Ϥ`|ܹg;>s1JnEVJ捳l}[#~}8x=pUA!R?4iihG^H:u֭0e4]x.{xdBq1C>pO49,t?FÑǏߕ~_B!Α7)|ܙcLS}K>LZ2/_[t]AzJJ/j"e;t u|{ez>Gͭ/e|;6˗}lM_ew+:,,рB|9 UrWs7SM-9j~~@.LD"u}845|4`w. Ho[ *V;uM*db=FevdNf2 >5jucp:>@*dAiC0Ϭt5{:m):{2:PAme6g1q<}\9[ vwZy|DLW'<@p8S #2` /{EʢEaRX5 8g{̗8LbCh9},X*d*quu73 pzt/- ! :#1W&8B @>^{p 01yޛ%bEEfPQ rZ^Ӑ2!*n:e$H_1f*^~_{HBo>h%k`$O_/598{\ev= ٳg,k)83tx fs25,M,P}L8^i2OEb#,/#/roYᛁ͂{GwuC "+Q:e,~tJ:¸ET5#נZ[6mTPyGPU -|-wp\su>UxW?A[8D0d.0\yV ڡ@:77\{;ҍZr`` _E $zOB#nQKа9n[(̿.lj |RO" V j[ܶ;>f$qIZ_H[lgoئjU{ITT? (5Lr-H\x%S$LtNׄ.JݘIiWY|?pU798xfU'; Lr8myVII?c1Yg$Uu8cDѫ؝l 9^ ǫ-٘!awDR &LpaLk~RIbesܣv&южV^to0ݵ5 @~6Ю\ f$A|YH><RU&1칼/ɓβd%ϡkXcw0[k,+ρ8FE eu s%o&>V ]DpUd / ױ<⠯;욧e(~"^ڐ]Ä#_oL2k *_J/)_ִ 20le:=XE1m3'`FTF-۩A=XLWyɤm:AƆ<$6|q4,n\CǮ^ŮpwG%`WXk ׂs,õk/2=Mhh ѯ2HnirIv+$Ege*~EKLHc$ѭyva=MoU;ĸh@*~ |R@)?=}G;4;7pvǭwll[#]txxܪ+dxwJA_UPgo37'9JO>1g-GN1TW ,ƹGs;:Q _x5*#ya0\z=Oy}!& ~3ОsY3+li@Ť?9Im_ }IVk׮"#Q@Wy$:|ܝ N{ϾhՍCgsx9Kmu>UĹ\ATQ"ęT?*O:x-F5mnnKmuA>3l,AV`@DSmLѐ5rSd<{]aa LptTLR4yW:˜q.hJD|+X^'UH5Ayf:Ð70x숀 A0쳶_bBiKW {EQ$1qpk!Dȗ dX*`&nbN f:N2}yUz̑^8-oyDأUZC˥3䋤 9{I|vQkW ^Wtu pt$d Ⱦ˃s^~Ws|_@ @Rci~~O1Z |ώGѡV̶Kop~~k*_[upZm3V~msҝ5cY¹xDρӪ2'3!BN:؛n"w}㩭#=2AEdȿ_WP$ҕ}Եygk$.o T2uj PPzc.q;ϑ+#YWv!ޠ݄ސ `UqQ1g~ᇣ:YNܣ։{x&2(:,Ƕ>'^z 8C0Po~t~%Y$|+J#\"aPvw&X y1lM<^:o`,?ґ]+_KSo__T   s/y,۝ Y|9Q%P FmYA;Hf À 6:Np}j/]E~3].y&-풢 vO⯪'[ jmjtE ip MN6rBˠ3U!d]=^kC4/\K T.Tq/vU/X_NWH[]M[cnm kʯR6*[+Lr{0;wJ٭=[#lnjnCo@<|$M.TC3aǶs~eJ\uZ<Ӓ_6d[M;j hS]gU+^Ku`{_)ĵ2Py3'm c%u nQA8M&7Hg%8F-um< ~LnS \9_E EbG=#GI"pCtҏuEWUV4?xb5ṕQ0'4soZ>:N1tAz&gX/ lnG y/숢>Ӈ-T-C+<\,A3K%oleG*O;z/Wv ]ض)/22yπ#BL^Ta)]6hkHsSyܝzx[`291ASk%8Ow-g7ٵ#;Rz :#]A*trMj{W7Zylg*(0pZWA*s)H[lj7=iqj{sD{.t,C5`voê2qԏ+mKm 2vڢ*G ;9lmB:\;"Qܚ] RU^}:L3٢.&vryt.v8%"$a9[^(dbB8i{ؕ@=ζ{$-g|Q&>#JܽZgWIvo&dT-drؒs=:A>[ʪzN<,# [qjD'E3RO8g$L3X{g {̮{WzQ~S3(@; O^g*`Ɍ![[>!@0Ä}LCPGA!+\st5Ѹ1`|b>_!Q@^{-*l=Y_{qFޗ7H?F Tp2Sf;oK v:9[_V;Ⱦ89H5+T1*MV |=yhQMO}D ʊRw7Uܻ9*l HBY}=+SYeBdr"Tl*'@^+~)YM&P`395PiڞQL!?tpV+ϟ Q,f`}m<|QcUg I"E; ;[G8j I*dUg.z$BB+u`fgVQ`ch\A8ۖyf;;F뫹{ԙB|G9.Pl^g {YDgY" ne2s,ӎU^'s4%& L KJ Tڐ5`V?plsSwtoK_N[ٗ.lSc+S})τ1OUzlM2/51@gHym !uYi:ӌGzK;D_$c\( 9tPoKs8 | :"iZgkND^H2 l9Pf B~62q rGTKm{ # gQ]i* /xygЅUg"4<`ڂ1v:S p'wNRT'[Yn.(Ci;RNnvv:<^^4ζfMʩF}?B\>Y%86$E 8=x.<8]G[U"h[iϲ ;&,rΙNt9c(G8A+{m҇&a25:" OpLRp {sXX76l<&=ؗV[޲]U_>Nw?=aeO|$8h$#'^~8$&h=޿rBz 1گr:O"#twI#GAyMjcPh|*aײ6Y4i- ::Sdѿ>[`Ց:1BW{sHњuޕׯ3?O?!2IkVl#sLBq_:IZ&- -qJAV$DARZLNT^s0egL g1&etxFi Ah͎P4eZwv N`3AO!墕 }lrAI .HIZn8] 01CaNu{dw "*)I<f58 _8{ߛR'|+Ҹb dǾ;rM5N7NG ` SG7?#/x:8|)i@Wi u<'OEKG~O6tЅ,5xooHΝ9ttv`0ki@{RmY lWrR[YPޢjѤ+{*z ]@cIj"0zR1VEyh`iN0В9K/ (\N$1'yO|:ma^Op|*C;ְI|~1a.#IŽzb Wa8MMt.b-yMe =G6JPڪ}` ^Cڥ9u GG=F&" 3t:BU! VVoZ-,F 'eW؄8١UP{;j/;ΨgV Q]w"9†5uw Lf`3YuڥEk"D@dvޢv˴__UU 6+0<#ѤvW0?Џ$ kj؍AnN_CqSV8w,,b!7渴pMGG;إ{L R BꏴsVO' J!_AfR Ek5jO%'o3fw ee) UO+KD5g,0ed; .\{\Ϝh#0@`LE Y#kMQٴXiZɠͣ_[sEB {[刵͂ HƧtov5kV:8r|mr J_ՇP8?AWj*MxΉ+4_$1aAEGVV_ѝ"ހu&sAIJ_F^q+ iAvip3`Mi1xVsC!zO|<7ѮCCǎ]<&=eI8keXN { ˠ-015tߑRS'if 3g nWjv'`."ߖ"ԙkL4㛉y6ȟ~n2`Ks|l^dv/T4iZq%!dm*~])_x k$&}ϥگ#y\kjhͪUAeG1z|^NA4^wbI&/)L PMou=ZXdۆ}}0NBv7;tʒd|%L {V+m)ʞX1za! 订3~G*ޥS"]~iUnjM%GDN `<%7q7X hʿJ)82bγwX&6Q1qcW죓,XEW6ڵ7H&P_)lRRmq<t55Upō !"z+`e`JgSCG5U&9C~vv>tX22S7TAڗ`kj͠{fM~T~S- (+j`lXK=:8OH:][__~]:J;u1;v@i֣vŷ XY2h$Hr-5V!2: I_=B\Wl"Ѳ @cdv3YO@qWvNl37!yV _p(->йt 29iJy=[w[aϱ\L |w?~m}6Q'2<ǽ+*ӯBӜ_ͽps?z :_O[nO +/_l._y BX$Y#"\罰EJ8מsq0N}Ns 83KqZb߄|&!yn1#&=4bb3lw-3򤰠; ㍱ W279zʀ'COz䁡CS'e2y^4q+\cc<<6ਕ2*t<J[  Gb\g:<{l!"?8?JPdPR10"]зGhs#A߫0cvݾ՟ν*} Dq ϡsZzL$jghrvpۦ=l6wBպZT?)ӾN&FU/F @}C/d1g;s4nO|+O8f@=I'5 ЁCq&|9|Rv@/atA`ϹCaERFDN]kcku")D Y%Y"L%7hE[VZN5fMWƄlC:z:Gmmu5\1Btix~ͤBh|y&xV~QɛU3L\2# xTG7ZZy]vw1iDj"džy_y)+L*-Z>szS1t0Bڇ (Jq%R7b:>Hs_=l$,;Vb/UUM5V8wԳwqɚ+ASwO7?پSg qq,|n60п-LRM-kF¿9SLp0(|"L)]PĘg&S_]VAO'} 宫ȵʘS1a}&lJI(\= h@]ϣfUVQ銊$ZRM!FӷiAiwr3 Tsd|S!zLDvF)bY'8*N$ }}{8{.卬MSI yV͡H@Eo~VQ9niODoNTR^NDZt#Ak/rdQ Oy{)GKkSA׎aN.^!I̼m_UNmKu4ϖ}~i>J2ͭ.QVp.4e ^6G q.!̊Y[1._2ģM#GQy-qC-׭^Avn' T{(l@f;f嬯ߔk/hP,N&j#jöx(v S9eÇu =Wҕ7}k96];C19x;tP/P}zOP1ҵy+aBhp?0ƖHdTv\c2fCinFZ_vJyפH9*·<>cݔz?˿nֿ}(\nyNԣ=ƵY`^-<c5ɀPwo_TlF_={211aL!QQpރǽ6.7iw4Bl(c|uñҹ׮qFe7z}99~Jk@Ά?x{~gQ $| >k_j'Ae ׽he{T@՗{;`4 l!rw b[!$.`$Y e%o0۠眏&jZ48uno:swq}±Ԗp1|P7WF6Qnvs5x"Q 7hhEgdіh-'Z_7>#=7r5HdZMN]=83 h`e2UY>g00n5U8OCS \I?:q(ӗ9t,8(v,Q;">O'Z`+t3Lmiiƶ:VNtrm V/_Yb50]BGc 8Κ4tmW=L0j|ܶaf4n<"P#0j{mΌR/YQoŔ`*]T:VEq`% dS:EZ]'`Ch"-h&n˜V)mUYEv9>{soq_l߅755[HGμ`p+|m`Uw!'Y讣fV ɥoDpaUe&Yao3Ixe~~pqD{gUtOVHvǎ Oޖ#@}kKN@ s2@)h]Zl(h̨r-KqxpLyC_Nr~:875]kyi`C"π˿Go_l/cyL: b`>;Lё8VC ;}_H3t~LBYIy[,@^BΑͮo`My'O\ |#c1\?F"dqVq B~y ^S'"3y/*\;EN%ṯ)c/B\smQqph@V;`=NKt|N,ϼ~:hRpq̒X`2OR]\O u{3T] 5e[ͬm."57 q|y7(գVOcP j"o$?w p _+أL,WVHϮ9:}\Kթ{M/|胞ށt™Gs!"Ζ&B#"1`ӽp⮻Cԓrc T δi<ñTU7vh=zEIϒiy*U (ӨZ]7X7`#:Q~Q~ gGz$#JL *ZV!ptM[VaZS:d"!e,}>Fe1uGi3 {u^d;n$Qc s(gG[Ţz{(Cv O;As %;'?V8'JWiq-i5@_i2g5Y$|t7kj iă32R'xPY?MUVIrǔ\:IFw1F"XIlDltu(]wQ +̤ {xBu+ijH9D^-eϚT&/ O I x&5`G]X1$,DE3[\5\mH|G+^2+؝3墲X=[r+>ݟV.km@ggKKomRnBbtZ. ֻF/Wؙ$>;^(IcRF^eϑSY JSB[rlTuA%\zi 2y*mOfk*j7fs[NϽzǥfּ(ߩ%tcD.>K`|&bd`(W%&TP=:.8WЏX:$ؽk|{$A)MZ ],FȌt*mjUCnF\z"iMNbC0jRpmDP8 ?T0VǞнlm{s3A7kLP];;9Cg2e5ڝ\L`1QvD/I6 n ʹ n2h&aaֶ K|o'F+?f1A D+a;$ i߳j~RM4𸫝} W#a>򽩩H42p)= ~6q /ׯf>sס.*);+tSņRsi =SGglf4̳Nϫ/#E|4KTWGz4z`N,ClcL|[o{\4xnR4cg>{m6.lht!2W{cbj{p+bi1'hױ]C_&PXYN +ӱy@IDAT&诙xb`fav:{ͣXJSHX߽ 97 :q^C`L)mvv;n17m8_OrAL/E$>?[+IGk&mkw}>X.}vY=-'u=kJS7H3;p?aK]gXy~%NhkL"vߩIm!r$ %$evC4r:G 5I{HT$݇O!",c,W_<`["[-BKW_Lu|dkW3#H7N#{*kfrAU7 9c}~.]Ԓ!Mё3O4YoYv-U_`/{:|80Xvv*gmjmePfUk|O@Rt+46c{`Q^b_t9 ўen{5egV_f~kxs8d# /l/C.4NZgYῐ={BṊd?ɒJKp?e goWmYaJls4uE𛬯YPi2F$э@yx+{"!L^{ kF$d"*k\f1N]Ry;)z'wzs?Z%9N]N_85,0bV8 ʩ|4|gҐ_g?XR׭έ۟g'p{mmT}^[$W#Ø%Vz|+z^Kܣ xiixE`\x51Ϫ%myY]TaX(g | X1:ȶs #)76 S1Ahp -ktt#ZH F5=. F5<G[/mEA6vid}tη^u7Ff'pCb+Zz0sSׯS[aYWG- ҡA@3VƆȤU9mm3[*< H@utzp J2~/F0YcX/A:|(٭ǂCUg A*I+ZWRuwvjU =#QMSKtnk:hI#\ GGb?嫵u &/O*W?8uL:Q,o2[%E笄9A5:Cj3 LO"8 jT!ceX-tt¶~~Gʠk)NHRW!l8&tj6Yc=mu-xUK%6V? `ْmbmX?~ 3R ZFFN௠`SWՑᱍT. E%4.bkOTh3LbV$f )gUArutk]`=tzwg>vwK7@y:&8Ko%AN 8ӗ~q\u1Wu]Sj!੺:t鍯/CN"`G:NWjp6 F`ٺ^kCÂւ< Q}=nv@ّ:O%>V .Z]gs@"=*(].q(_|^ H~=tc#N;ȫI`8+x Ń YQ(bNAf&! s^ @_<r!w6%@{Pr wt>:t$~-̠[mcL!? |;+:yN:;K/Co֠ZHb֣p_hs]ѪkȂq{LJ(H1s.W9h/XO[qk ,2#KLvilqv=h Vf^xA@v=nW=:mdt,xz AsR8gnRdA~7mx ,Cڹ_WFv%{X%N>>0Vԯ`gq&h1i'{)C;3B"IT]e5k@w h0܇EAӮ,5('V or]:+'`0׸n,苺&0u;qysll+:T8Dv4b 3QBK嬁AmMd i5QHԘg8^fgl$4b;R94[ yC[cȴgv6<] o:&hE(GSA&Ľ2edlK;E%$4_$d%44!qae.اtj㘀iC'vw &3L*U6ɰI e+|/U3/a| $x {'ejjLR`= YA9>c&-ο NڣT8Ǯ4NL}<"-3&hhIW&iWqC9¹K ~#aà:2ɻkEqhNx,e~vO3$4 C2rymr#m+;̩d #̡ȳ2bRbC(7M ^e-LQY[-vP"H=F}y+"ލ,*7Po8:;0#|$ gRU2ra eد`…G*YYr d^& ]6ذm~go^ ?)Kf'E'߲˖2VtdILyEe?Qwuuw(F;})dln|9Og亸菸_ADTP4ۍ[qvoTr%fpO8.a >/ODƷAr1[MAhwY4@c?\I18Cc#i97| rPV^@k6AR5<'pjii1Q m+h dv\>j%Nq.NCCt&=ߛ:zz"u~24rds b` zYotI* syMBcJqA΃摨wK+AeN[ vz_&yrEzyvX{u|$ke`&6Fzm^&E=̟1hEb߉ޑ/} }|:3% 66N+6`Z/ImgzF0gX1\KԎ/ޢ#Gk=ףj=dZ02|Znz:1P}:d [m.?dTuMuDy/m^]0`42quc/qWZ;Р{΀mT:! .ōn_o-EpG/8\_eF!m+/]"/$hsIgx-:"5lC[H߬VgK j/dgCڑA%55.?H{+\a}I;t8GSRfMMM$ߡTg5E2WIS$1nt(߳»ڤ>i?Ž`JcW ϖVjH #`'@͸zWqc9~h2833ͭ:Ck_L{ =.='ѫLGzT dQǪ 0iKZ 7Y` ]ɅD.*FAjz;g'z o>`%j%vrȀ $Rf?`u;tigo_})YܟO?Gx>ws HVK熡<1góǖH8>_)EڹV/"L61̎Gyva1_ml[R_x7#?~="(]^|kt08|$5vZ_kiCӾ2!ܔȨgv$g¯i(sXgv+;mk{{yӂP>@ ʦl?-VmK]RK'esLܛb'BWg8;ZaqĐ7a;0/\5"7<︃}3%](M7W_LN}wUciU9Oٻrd[%b+_- JE&^q9gX vYľpN{ OQͧ8KRsֳMnPؚ$ZZ:Ob7/\♼mV ;WGJ9+A9wR \a,u; tMAّEFmM|]x{~җm'MH-u~&]k*]bdشR&L_̽^DWGUx$ xM[ģ|=ORW`.zt0gva_sMGYE1I``2e"}vbYȣ+ }yYrȐEKH:DwґAluaR0rLONdz ^39`K婉Av 3+KW.t=M9?N+-&9qmĮEgeĎuì6 ծE}$ "Abe43@Ur^b=&ds7 .R$`osoro 3*Xsοos=k0ƦgwUveZ@#i,3 v9hXyKQhƖ493{Z,P|[u2)2#ҡ\SYdB 0}}A1PfC;o*F7 TN/o2f͌ 0m"2bq J5ort3o=t8̈׹68c|ӡãm`<ڷ@ 4u(eY|n5%$`Ln ,+˪2lh:cF]F`m;F Y9tFVۊ*e6oE#sϪFr!Ws5j ֖GNx HjM_j8-Hא\[쿙p @oL^9w#l4+5#c`ý 7 xnh!jp:2?q9wճ._Yjds}{WGjjSqd=kubiRTFǵ[fh7⻖Y[gck`ՖTXaVik% Lz>x8$N\ آ :+é%0nƹƵ_91J =- yq@ MӒN0E9*xA+!շ/N X[q 5*$s58__ʍ},E%@)$wzdf3 H 2l|1+Sg[GTy<mk8|)R1IV@}G=XyQDЦ꺽LߨL~. 4p˘3O@%*.I7$h+/!x68s T &meJ}$5utԽQi;h:DyNô >ұ+Z[cHr/@(䟁aVԳJbv贂$`/h/,}mpfv}W{(* ^*cO)/BA'5/JS&G ;æPiɏF1ƱTuϱccTaɃ&. )hZq#+xOV%G{xﮮ.:wAxE8LT  8q"heK^.7nL8uLӗ~4_NGddѶa4+;)o* ,/Eq#]`퐫++$&TiCs+/mO-`[yS>KϿfz'xQ̋9f`$,҃7>v4͟k陯SwFrٖ\vRVo b)q 1jpN;ǎclْ"[Hlmy^8WZ{id7耗^Ѓ]g*}iOsx"h:s *Zi,t n@m:d 9 -6֥<.;WG.,48@bJEppNò$QkEԹ p~;BҲg05[9+g=VF$Wh: }鮭tΦrgcc8u*{^f=l-v-ef2H|@s3PUpo^Rf""ѱv2ˡ87Y Ú 8D~ Vn1q˷ nSqn#SȦ__!22n VZTkS(zu*UV2WJ+S=wtCh TfѹUih^9I<_[N 4FpB%:"z$32<-m6io~JV!v=rf{Y5pw  \2W tw9|Y-#؁,͈ieϋڑNy.gWGeKh*,{8`ۼ:n<ͭ(;d~̭tkV#j(t12ax ak{;W5Gz'PSG41ЧgnEC.w>u*.GN`$z @8ڻ(:2ԇA@N1G3@7-9r/0 0XxQ{v5dYo",:rP2l}߲|,gQ^4--ehk&>/ ֹ0p:pg_05t|odPΡl|}|'s{&ZAڎ?7bгFi@Z\e+p|fgƽa34\54D&(lתr~y(|t= eΒӂ~3E[_0 Ƹ (쇤)8C|/[uy(b/5}>}8YiL҉Or:?"]w]gø|o? w8]C$@=h:$)K<yȬM.9M _qO{gfeJ)~gvt~YqŲ|cg^9~L_/=+S!㼁d(Ϣ_+{^Lpz eo{gwˠM`f8]|`_OzwL/( ][Dz8>?rFi#G\@<J9tʂX)\:Anu lZ{:1 f-Oi@{ci\}@vw#GOw|y TNT2ZL mr8e9 82 t|:cl?b?uR@1췞02ЋcIcw+[H[8t Z+N4Fuu6`ulm(x|f5gXc5h}Rw3rf2 S{HhU6RW%wQ^w,eϥ}s}sl׿NmNvZ3njEKz#m#o v;稒?a"从7ie1rzj"ǵ7Xr4 ՒF`[8ȝR@UzSnj]a%W1Al%7s2K #}Ϣ#g:7!3@j>iJ%jpǝ۷a"޶ SKCӔ75hӑ 0ILS(:Υt_l,W-m_O*zz' xWJLy{??W_G.:NnϏ}uj+qY#bt٨^k_h'AYEϩNz]z:6-_k?`.=L_? CO3nK=bꫴ z4x4oIi{[E 1 Z'Nm9)LxʠKp,UǽGh腬kho]N^n@A^:r*Y%خegk|ydg%ܯY ~@WЕ]ܽp/GKҮb%H%Ŵ(ЀouGY̡d*a7  ;xLBv*sܻ3I_h'rLA6 ȿ3Y˵W.gҏUXtfœ86Tޮ:3'ufȖz'P_wSz8+:L#ϘǶӱ9r$?oϦ/3w(XؠqwJ,A&㹻j#v$4/"fr26Bv#Y`V3)+@T~*]̐:H/WM9TǷ{>Y[{éh~:n_L[iVy()[U C74hyM[ɧ7   m = Igut'[h,2cdp t_u6x3' d֑_*S%[ 62F-ȍԄ<68@&m8`NZyZGTv {"S/^6աlaBMt#xɴ'SZ\}A)hdIn<1(۴wx`6$3xI2'/QVQea|x~3%8A+дzCIA&0>t\=l@x\xHmk$jꚍ S-2Ye^yx kSHɐ=zAhs6uuu7lu}Mۙ:IZ)v$ *w+W VyfP}b8PY x =:fLPʪ%Q4d-+YPQpK􃏧2%d]t%c(g=Dp#JvzSalZҲ(Q@Cq_aث]A%w@"@{:܎193 #_lOrkd-j%uH3jTd ]`h0]@e ?`42n { $nV<~*7ep 83TB[8V7{LNvp._1Z4t12Fqr a!(9|gϫ-Pl.M3]tFJ%``b י\ )VADiNP: g44̶β`2)%{<d^sJzנx69+?) k fl ﲮ?Ү Pp}4nKj xMֺ FLj;`Ҡ8qts]-[%Sӈr 3pm >f,N )ٓNBa@ң !FK#qv4uR'K. ~JsLT>\KT1uGjf :$޾8.?` p$ISsE,]:pWctFNϋgV@B pf)FKQKCz%=JY 2)9[ܪ%yhϒ5,>wK}slIs"l|Aҝw :uvuU'4E pnlmM'%8I6*@IDATt~+/x^%}5d@#CNF[Sgil*~i2**RN[?WPųZ+oiN0sۯK{>{u޻SZ1Ua#_!#i`=f+3=:ͦ9p5 l; x3pL4s>|y ښLwo{->]cuNጅst=]8{}bDfIt-[xӂֳʐ?>E۹b6b5sۻsړ{۱^I0;qGXAo:QfgNՎeڛ7i JZMiryE*ZugqNyCO:,|[g4`r*8kٻ) R(f_Yw[ B'_$}ĴpeT2"`L<Q`f4 eg)F|ꠖL Xdt%3y-oSf3Q֚}W^X}l۽ӿO҇?Y=w _gYu9yM#?PNF*:~ufWs+(T(t ;cO><۩uJ/7{P2H>f<3]@y'P̝ot-ǘ+V_)oF_wf9V[Qv)fՋdЩu{!(1<~ǰ=xz$=ᄾ}KPehit }^GU-9]"iCҲ1!ξ:,:UGB癢=:MN//˿p28~R#môj /Q~Σk(Sow>:4x>i$ [ lϥ>C3ʳ=A:wΤ7F?J,b3I-pB{1V%Ӏ*d?Ɩvff͔7h#\~1 [`&O<x &]FFpߛI~_CŒ9Ζgyf`=:mx/ `/g1j}֒Vz {"H3PݞF8?%ix8ZmδF'3Yk19jV慎[Y}3 &Ŗxv凡鳗V MנJq}W;JK&Y".2 ex |^l%5S~)ntkit#yߡ@GAp%vsV{V)tA?FXY$e L@Nm.rGshx8d2vt\^HvA6=35R!A1b%ƩoFЙ:vJ}=$ݐa$tk`I_o̻30F[(gLؿW`W x0$wR8mgoߝȮ4T~IR̲Ejs + IJҰƤRF8` (*g>QbsTt64jGENH @#vY|Y#[YYM(ѥʟDxWs(pqYE?C:\FQZxFv=Y4 [$ 2"ri]J/7e}gc}Y#O>gхk?zz?EѮK'{"=»9pVgm3G 8ӫ(s?~oWxR37yJ:r>;Z20Ÿ[˜d qٴp|#5~z Z BAr&_y+trR%aPߖ*(|A&&荌_vX3?6=pc_@"9v Ѳ1V 8Yqg]Z b&n 30^G\[V^G:fgVfV:{UF (,鋼13іЍ,ů ضg9`ʖZ=>䂥2ܟ;Ĺ2ɊA^P׉kd% 7f;ޥ;un3}} ܾN<{gi+.qysE**$Hu*$?51ڠ8`9p`Xy[MV+dz<0 G'K:y pK4QE+d=,PA)NxƖjgp:cܝQbvu!d1<ދc%"e"bgSYbm&C)liofY2nM%z|i8d4V:SWvʟ~'@__oRqH/ϫR͏:su._Β)9~yV[r2ttBGqu."`#eoe)#`*zry= úo|6Z#D.PV௜]`Nj,(ɊySpBWr JocӴɤ쏞*ΊFҴY0tJxvM4pwWd73lu};]y'=#w':=H=x %boa$L2y]|KC[],|]mPMjphm*mY(rE>[ bRƘOeJ٧AxkyiRJO=\Cͼ:烳^xoٕofz_K_J{"󑫞3Onk![XL{kv>L8zCu mWi^O~*Y5w0yEw iJ1hm@oNsOڃ[7lʄ`kkG`A^k۩Щ4pU:wVѡQi.7bٙ Zy,Qum{ñ(#tL:UxW @;1sF=3d acm_,Bس5Zmԩ87l d 꼙G[;>xv6Q:`o{vVNS1Dx}~bf;>iU RCG&Wt;!9:VߪL{LH0$-m|e']zbz{GѥR\@;̳Wm^CE,0eGeevq-]Ke T9 +W4ЗbT(Ou耹Q~}}nE\usLBPu]O-5!;eXrx[9 F[I}mX)fo(8?sv!4e_*ATBb V6vtvTLPaЧd ,! Uj FSϑ /*;x}*,/H%ЇhsjB?UҾXAЖ_ZQ%mA Q؀>W`}o|Bmsz:!f}=wB?w] ;sd*0ɒ2m;Qg@8]˽A2 =jqvVܿWǹAÜ=cgI~Bl3~H#P]1ƼʘH܏ Jo-F@_fgJ-jF}5Tcd̹>Ia$-v-kYfdĻ >Yu5ÞHMT TU(:h bHs:/f/}Ϳto[w/~?^('O}07pr54_ ufl`GEȝ藅:54pXL۷IJ77_L}g}ݘK0SZ0M}(_Ae/ rρVMFjZkH;A oܸ>s`d7@kRK'xN_Qz껯A"wCts6zjtΏWY +_P!m_H>U=Fҗt?w~;}Fz/uo:2v_YFG0ss.9d{l*ΕgE?Fg P`F,MΙ% }[NN<=HqP 7 H{:gNpc_cN{W+8u G Hyۡ4D A7t '\p%,23w' $Oqo3Y:`! Z&RPP׬3/a_<2\|zife+. 3,JV? .ĩdY+uL '圔/8gY-Q d[ xUU0Νiܱ6 [D˗" op?[h̵7^'h0uCWDyeT~H 3smb`_I<|$q^/U% >waܟ=k/p&H?wbk3巴Axs/}^o/Xԕ\i.rty臎^8s/oA6u#x:Ne ᳂)^37 +ҡnPҙnton-Olen0.%P{^Q-PgϳT;mO^:uiaDu%_Ę[1g v, c"ӔkimmM_)AL g8YX؁rd et:ۤ?=6xP2@o() i_teg] gzǝϐ[ڹ9e/ăd'NOWgŲ{dMg|Lr>סz._rfjtGexb{=H7?]@C.N}}n_\3uOR޴Gr8yN8!lm}Vpt,$`4`Lຫ/.ׁ0< f '~__Tb0E.Q?̸?O"?gH.l 3Th5Nux4e21Sgdϼ:m# t+G큩NgHGgWjIlyWW7H]3^SUz#ϥ'+>u$x6(并׼%;Uޘ[Yi=l DntV<7M: hKcɓSB!ePo7@{ȑRa |󟧚ljkg-CvAqX9gat3[oV "45Q9rp©ghxizLA"ý9{<+! #Xs /ۿ 6tm̝8ژ[͈-.r$ӇӪJ{sǨ<Uk t$)Bϩ.BY}s>Jk'?|wٻaڵЅ qj}3BVVS'pw} z`or 2lcV9S"VLm)1dSߩ^gr& {T,YG}v jϺrQoA?]GN:l=cg1^Z[a[Xtؕ@—:K>C{nk*dAY<KZzA_O ܜg .[(wXչ4`3|c0b;ҭԎR<@BS}0@GX08oS]{Gg-]z=ufoEEBwdaHC]Mn.L )!uur8K-mMPUaaltyPd90Q}W* 'gvڝd?(JLt>@`N~}A? $ ( -4ʙtDJw+!^R<{r]?uMg /tÞ &ӫ.GEEuWSɯ œZ]x.WV+x~z(?WgVȱ/=? 3Q{ k{ gCG޵%зlTG:39荒Ac5B;pcqKY;@ BPc 7bjSx֜|}`ʗG+g6Ir<A.Y W;'b#*`y DXж*6(:0Ifa **xb櫩s>*H<62ר?2}3D_++\$4?_? HըA:Tsq9!(D扙= V N34&Ψ'H1r&a4WAT)5K0Ouzj<>9ĩ]"Ӽ @(ƃ(E遇#qg^`Ncm\4G6FNcd`H j&(((N/ĩS@/3@ @xL2gt`:O{ uvLK ,̑c#S&KS >.َR;W<^ܟ f1{ $hAO;ۗd]1/i/;1` ˗qp٭8`rn [d>Šg .ͭ նF> s9fWUi'8oYZy`!joU`,h)_4C8BBضi`5J|7)ENƲ>r(Qd1't5Z h[ڬ2cxgov-K櫆.'AbƋ,d|.b8(("*k+njk}cБQ&Hm?*ߩS& Og Җ~~>0wRd4b/Wt;OAl(=y1:v]aLoZr=/|Qgk l1+ϞtBy8扶Ͼ7=38_K`O'zy+utvA;<=9@s#=p.:>K|J &v!4k'P~ݘ Cd>9ұe$2t ̰>dnQ_ _ ~U8Od 07)53a|ѭ Ҽ] 2uS}^]<SFX'^N^ޫNlL gϵtZc!ussPGX25ES;R 2෍lJ:_VGplxG8nTdukXq;e?XTϔgZ}BY%QR.{LVGwfy{'rEP篭ZgAVJ0 (Vn]=ȶ+y[~_Kۀ~0 2/`.YDKm *f= cg{ZC)!@cP0˳pұavkkfDYm>t1OphTy>Ⱦ/+_ʠ`'|&&Y jv>i~s2dX%u4Rt^NDg [>YG.!X;VЎR2@y:H: &|޼>.%zb*x:gZ>dx1U7؛5 Nm *lᨶ͵VwI%8~kW/U!>.8"tgjkmft] ku3L,6 F8lJK6dz01x5diˀI{]$١^tGe̍S]]yt7_jTўՕo, 3lhnjMζPTUQ~z=>:1>,TA6^=Ey~-ik Phc#ǂ_yjwD=z7POՙJص5$sO`2!  ^5285Rk|qi'{'뼛G/+Oڊfpp4k-  R2:8z/&*887L.K"m_oXikЪtT/vɹ5*^R}{_`*>c\`HgYK L+-=^Rq6UVZn` qbk ^_! CͭѩoC<^]Ҙrgzom^硌#f=RB;4M;[ݴ A55Y%X꛻{qI:q%PQ.8co`K?Q9_x@w,3Wщ0Yѽ*|-e_. .3*[#E2Mjmkp |x 8}\ K.m+l8ii\leFr=>~g䰆?pנ73@cKY_ͺ̲J54+PG]N3Bu* :r4]{0M6PnpkttGJb[7ThӉ ϦF9ܛMU"5F4khpKJԩiy%1 ,'?}Z҅7l_f*1]zzÌupZF} О8,a(ANNe-pqC>fπA( aɿu#1FM s Pm x-7q=%UI:h"ۊb>f BF h N]iIg :rnFqL!RTЏe@0q-sZH䀦 4~ С![pa,fFj=tЬIȘ*;D %2/t&44q_Y߂m:<4by8紻m1C+1t9MqZ (!SOlO8_ Yʏԟ3=K1ATq5c<:Ix57 [; Z(n7˗F7o+L/} K:[qܹ6 #XcF<3%Yv=:__?[׾?_}<~'X%8S؟MwlN:yYgN40cB8NMM@'d.2Bg{&=S]8GuMl/in~lx^I9Fg+Tp.d Qݥ+63Sa+3g*3~`hsy s [# !7xS刴-8 ns{tzPk% +cL$֌2O((ik~A/- BfU %0He @AV =uo8]az)u0@e@mάٰ'Nu@؛Vҡ><޷K_D!z2@hsf#.,cڬ-xAeٟ8O~>>n=BB?0Sa506ղ΂=_3ଡ଼2Hsej"8E8 F\qw"ʯLsaKT+iÚ9=vff@쵲( uWy̹e sOY!Ә\6[ Eܗ9P0.3b m[|s>֫TK 큆gʨh #~x:gQ- :㭞_u!*isB>)Z\ 3Rw 7vXrNKMD$m .[AE9nCy5CiRDT2\q湀\g8 bѺ Vu gl}Oq2U֤ *>ZyK.8 + >^f&R]|>ǖ`r^bF6--_%d Z9:8i-O(mJWAŀmej}Z]''=2AiwikRZit G]my>lR^wY\w~-{&NBir1דWP V6/ |B^6k_nc{ OFL'҉,y*PmS_zk0dhhN'L KTryiT&%p?KYv\'jsFm8y;cwq$=Dz=͆ hm;Y=yfQ^{p51oime2#^gNu0q<3R' @IDATSUulmwYG:l4*P4ͶґmyeM4ME|. ўth6@P6 bn ,*'{ɽ7K  d_Im0F|d;sϳ|QFkitiHN\~-JGi YmT38qk؇۵t>,BhX[ȬU;AT+k8uzo:8pr=]M@fϵ@u@BV =ɧL"_Z@:8 ~XQ,&N:YWGX#=Y ]|pm&ycϷ̥%cqcpk̩~;# -t1NωU9arq'{/"Œ4Xz:8}KgF[$g_r ^Kdki[lOǹ'xd4ͮJ#Q΄Nd@tw^Sw~ȹ8G:؆B:4gn3Of.+#d51 T3!W79k}l6~ұf,DSCA5fc;^[YFGntH 8.i<¿s#?PX(â2kl;{.ϻA}>MmlM=wҺUz)_QYlv8B@]*X}fz~˾ ǧQ_]PWVA@??HQ0&3 Qupzf}f'o+ߩ W,tʿ]G[4[--ȺbJ<&f9>|E;!`] =vs&M/Etyh2vGCFխQ~d@ץs.e] \t=9R`1RR=tR :Х4goڠit˅i)1L+hgo?WgC{*W|erTOsXkXBecDtcaN')UH) u,.ϲ^:k]}}| ǽ6cOǞ|&[TO/gR3YʀН`e }Gטf̓5-u Qyܣ[#P{?8Ubot+ocom?JA;wwp"c5Bϐ1bmۻ%*L m9YȶD˩GL2L׸=5@l# %}:$d‡ ։ =+|G!2`pY6adaCϷ?r=j7=],[\uH!얲4O!0uICy88ܶUEg 9|ga  U$9ø:r8e08lߡ sei^b[#.il-,,vڥuAԽ#ІV\ 4?<ɹ3:=[U=ˌ}u zf[9aAs-j-s 5Q +){eT=_v $EvT2']%yu thYVJ44M+m6X'97u)+*XmGƱ|az_v>k|d -4LR]:K zpי"Zg jd7xM(N%-Wl___iD*8~h04:g@Fڏ/*ҟE G.-{Ʀᖕ0E=PdpgzeUV K Ƅ"R2h613# -;sdq;6\sPM-m2R78tf ~=˂VIPW3fq^ PY@fYZth?u=FԿ+M|6z!~t GA}=a4CC?Le5L][3kcpW1s'O`L,b#??@C#6rf(؟ڲl)`J1J4{%4?OXs^d>ٿQG%k(Skue "VC@R c è'\̶l ƤZ%֘}bMu+(̆ :tܳn^Cƚa-(35# Aр|!P{ CDJw`Mkr~Z֕S~%#}3I-1^z6γ!  xe{44o p{d=Gw|Ҁ52 j/Tq[0u߯ H@]9оh-ν)xq )d}|to;A>C4psc )fy !]\"pN6nf\/ +A΄ q=`λgY{T`,(ggeu&&<9i⽖;|XАRafRi%V1A !^K-`%ұ)$6,K@Дel/JiTʸJY=9uǪ9:*B'i-aГs`q,y45a:o؟um! H@Jz{ҙg*m%!SjT6t{u xvx@r(,y+%1_xf:2G7HV.N5ES!t ȇОMV. r* 6]d K[]܎ v3!!f(2E4 ~ɼQGE.tMVQ3<;=?g\[zJ>?ﱑ=Ik~&8|c{%Y74tԛ3ƒ1=S ^j-j?Gi_#M KD-,+`,qmuF`9K; h)8ixXЗ (|$Ul3P^"3s\~WhR"Ry[\;|&:ٝ?Dz[ĕBte%Ǣ^N 0@vbۇun-sH;D'ݫd!C_:9(C+-OF-NrP&bj&uF'K^ f~:"! Uzڣ? _{AOj4 y_+z0@7vvd_evw&=3DZ2s ^4?AGk//ZtxP__xȤCeTe$ЋD;xQN.`P@'kZx")ilQ~~Eb/5 ڒ}v;"u^Y/0-);ʃ<:tOBH/_8uds;d ϓY'RWG{8,D+W8{{qZý=K#!xu\EǙN:tt8xBl,ꍺ >~"Bdkْ;@E&:A: e*|rFӆ!X`ə$6N6 VM،(֫7<1z!~'b'X '}+T]ig9/nԬ=l`?׌+Όwc#1@b++h]o-F47Ž4*A;GSHVWK{t9J厲U;4 6wZCGȴrVPzV_UpIp_-yrI0K*fz ocǑ3e`F0bWM|6gr6/ʁ_z=Ëm*Zeͤa%/+$̐٫ ')xb5d PWpk~1YACH@˟}# yY[hqzϮep u2/`M j ::+ydG/*:d+,1P~DVB ֨,џc4[CaiT_A4/{M־k^]ޛHJ)R6eIq02c  0A;ccKD˒EIŝ약Wum]յv{~RDF}z}s9YnUZiYLA'7o9 36tU-tp= @{> Q2YAISO;jBw9m>򡯨+wC{LI<ӽV.v(ܯ"p(8 ,t+~/vftt6~ϖ?7ũhTFiϐ/AGՐVZmܥs@/yۊ65y:$<' Lt%#QًuЅ-kb_c#~z;h<ǣZ7ϸF/9 6LLODsyH'E"+s(1@y )4 4 tlac/rp (LO5|OAƹbSo񰢎Ocl- .|#XOVn*y_x60֠&"m5M@m Yse@qUFӀJ719jr* [:V tv&qox 0^z+k/co<i|0ڬW^~nO>dso*mpr rEuXVa9v7oq3SgL 7deu1!%0 ~jE?{h7]^a;_#6V˚a0a|Vݻ.K,LYi7'\J^zws7?uX gϖ7L:Miъ&< `3 p+#8KE2C7<ϊ=t=ɤV|xfVQ9̶R$ehDQkKǦ& (EpR9)!&@)']*$;A3 :p@ŵj9HB`nͭ}sW!w}^/fm&~ ȇﱋ,X`ɳ*o~AA/ m~E+W P{OuO- %6A^7b c7E H 5ytȀO/ H6~0>6[~ҁLs9]й)^@X,FoX%d`b||%(H<|T:=>'C vN @AZ$2?o ʏ:`\ȣt5Pkғ?*e`")V {np Rp = ICGe}2ݧ%_w6d/eq'M<S5u̞sLx9zW 40 VyC+=; 935ʜr=({MF?sX^צ̳S4}vՃy;7W>^jئ~ܚ t r |?jOciroGg&r>?`pSYFTiۉv$,$29Jn 5Ri>>H_J`G ۠aA&+YbŞṺcs9k8y+/< Y㧨 L#ϧf?Ξ=k&MgН7Rlp݀[2SOeXq2A`6 ߩN+GuW *G!q0$T]k6~/=GR@rkHF[US}]0!HB{82ɀctUEgW}_Tl{؃lOV/~aQ{kLh717_KG@GqtOA$pE 86WX6X ApXI[s]އ܄DEY6Czn/<;>z> h3?ɾr}}y e݄eaG Af#ï@-CgjX+求mH~EP0ƀ fAE'VWCT_.됼oWW{b:{|*6Q]t4-W7^k1|t M,G|mDX4hh}9: ~ x&2Lf<#a耣_j0}kdl<ӱx<&O>]kX?XF y$̪g|]?w~w>GԕTm֖546׎2p Ok9xFU=d1ڍO+ ̚(/Vop6?s=?ldS[mBC3Ǖıjt ?/]?E[@LѡVmg|שWF;_Is0 -؃PwVy)OV_;F'QsFFv2@ؿ \ߠ{r"V0,Wښ0ЉG?-řDrIV{s@; ;:c+R']`tWsKA??E'A[$Xtevj*O*#cߵb3HTpKll(Qe+$d҄>mC7ADUy( 3$m z%7 ֡wІ€ 8SviNf%*<ߔ:y G .^O/ 2j8- :r( 6J+5lk /cb<3lwQۖҋ Yߣik_.G.R/.+)jҘ;p0x!`4ŷ|:>ݟʁ2F~7%N{vK 'ٷ,IF9%Q&H|A7iN].sݔ; G:"z9G~kM` ,22ָ.ްAb/0ʠ)>+8Kc#vrL]AL u3)㌧S O<SP˽ʻ+/wB Z F]n`FBSM E'/`d HQO)?,\(eUN4 mC.fœAi9`ƝHo@)dH` !#n[ve |x1Z[i0@s8ܛ{gc\Krs<?A>K%j[slgN0-S_,hq{3`|d4|"Ix:* 4*}iEEww_:t"@\iJձ:;p3X𣏠!RjfV_2a+m  VjT¼-:f 9ڰ[NUw ~ B*q u)G'Hm ,q0 r X <Gv&6L_?#nH= 4uw2'ʿ SDկʎϕ* V-ĸm3-&aܧ(48@PfO2Y'g,(m9mU.(c*9)+kIO-b1տ*-5oq)ۃʋ^:A GP **:VIs( 6!L1C*Dm!-8Ť*lZS huڼ|뭛cQY #kܵ3hk˳]$i ZICXmu ljъT-S 9'1fpI'1\"qy`naW޼4dUxq YS^Ʀ I\*Z:l Vx^' o0Uk-**tW7y?V?3ʫ`@}|cLB`dkՏ2͵=ձNm`A\?'cc;oFCT 1#ǎ 8@9TC}W^mO)'8'ys "^ ʒ`bȩ Z >99 VI6 IF gD?\>R..0 lنPW&(]Ck gPY:X6NU 6g*OL [z s|Ib@2ԑ?)f.QM;!M9Gn‹NAK/4eF_9^@$@_4h.r¤F-;;i/AFwCđIѷ9fd:}Fvqk y$ѽ6qKj{ [j?#wcEA}ر$B._~-' gl#ȳ ڐ}ȏUnwG>hoϤlQg9 Es+RCSYtXgkdH$P3qP=ͥ^H(hlH<]8w&q~ӿ߉.,Ȏ]cuv-MvCXr g E?g6#ZqQ;xgc}a iw{5 Ὠr&Q?/}C*J=]Vjߎ7 ?ywݳK=V89%pm7}b?hDp@߽}y %y)?$;c nT]y75`TXd4Eǎޚ&)!x yv&SM1-bG,w~H G398\Plk~N0O~ߣSIR&OP43Fdc}ŋrOR j5Ih@6tqٸvʮnՑ&fiոѱK/%+Ὦ_jGYS[HŦB@"l2w/Zӽ?ǒi1(_a ^+3p|jZNwrSkUm&MYRDkx Iώg>aؿ''qn'M3|,~t~5^0"'LMOI~˄cilk *}3]L`+LV9J=eq{ ** b7>O|﮹:d|5|3;LLbG9.tI5{>Nt\ُ Ug/6=XL>+-ė8^\?M KN_G ݠ d/P%^ؕd"eȞM-ށFTq:ذ{лkG.Yi7 \dYJ}G)yo9#kcKPGt`e _?7=D"];xe8ؼ\Hkl7#m9WNK"ޣ>2X yǽ~Vn`X{3^_𦶅S<1e Cl.jeQ`Gv3A]_ZM|u~$Y/gtrwLgQ8lxϹ"/|x|%j9~dmy{ST척yochk^ؾ?wh^QMDž t &< 1@y )<Ѝ+/ @كq\+f_:+O6tr.3 `u#{2ӹ1P *$P:]65F#^tW(* jdrS;*: ^ N \yL|+ hjm8u<Ѡ@{7+ب춲I0]QIy/X{ 㤵)F)ZFg H?'0.+G㹞gaڻt杷q NK)U8JG:p=WcV-,q.<wnzӊ]^!~LX%UiZ)bE&k/sBP{\reťPkf΂:f : L>& DP ^*dm88*xx/A"6VRi]sҧoNfg[gGOQbA )y :o:vg8ci' 9ɲ6pL;)83r SMZYU:Opv V: 7s0FF%Ǡ4 x ^"8s_yMqNέp[mZ;W|wRE|TA7۠c7 Z3M\yÊawj;idSK ({+/ Yxy EkJ8ߎ /E@`8g@ >dJ1C`K \i}c1h$m҈.t!~{@@&ͪOlX<ϵɜ.$\wm>nan:+Aa]r puX!-.g;dlmptXla9|VEF"u<Uy5z8OãФPr"pCE:eJn'{ͧϳ}{&Z27`E\qU   gRGX|bѪzZ"2\M+u&@uWCA^iPH7voJ,ܛ!!mKԤ!W/A_e@fɂ=vtyk93I 0ucS4/eф cwh%)/C׮I2NŦ24|f TJ]AH>C5WufC2AsT'p/ldy$ IȻL辇>-Չu$OL҆1HKb@Id5N FʊDQϠa+/<;ž^\h%DYݗHRLE5 4^#Z[N?vX$HJ<7a9q2@*`@@֏{,d *ncf!v>bܻdWt&lD{; jkA)[(nY9.?Ю`eu)$eE{'x@-c;DB6=&[V#?x[ߧ#Kս?݃IcoA,|?ÑL>ɧIS5{{gc+\ bݼr+@.LrP B\"l63GmpeD }`;&OppyE2d?oD)؂٫c/L=0LK[K$)IS/rdGdY=k؁k9~Mxm#=_L/F8~⥋16;h&٧UN Coe59 5*%crLBcmWX-sfr%zuuQnuGsA']]t9 8݁.fek-Mw& XEa_'_*'~^uo=~qWn_ZHEgi@or@cS;v~9פު_񣵑D'3x#ݗMt!CvQ`#b/u$]bvj`e߄x/sO@ꨚ2\=%O7eGY >e0{\{:<| |5Ϲ66]v18:ݡeH5yR_'Zcsi,WHra1t *tHeS{Žh46:Xo1ҋqHэP ɤ y_{gtF`K$*4FTos.BX$1Mi ew?~mVYwF*vť52& U< ﮯF:dQ: ~B"lU9Wө%mO{ta)*[­4ORJR M82!g" ~ܶ\o2c݈5W/@wi/gX`b·vv_p2Rw>u\{e^>#jc;? gDfb&o g(&wfz#}se1Y#괇c |n$m3%kWB2i |w^ v,PյD_QEڷkc`0dqǽe:612k{t@39i2PZA/1 iiD=z}z~<}Wy )@npɲAJ ^{&ødȭ>ggߛOB̤0t3jK8i$s9Nd  s~dUvhYŞkAe`!u[#s)s-Z gA]=>gxnzN݌7;5z ij<tva@IDAT/cݟ~W-M\jJl?\~ DGF- BL;G ǵٙB@`"[i_s2/I*jqDV'U+bM$'s,8 uMUwes':%\'Hk@@Ac>Zg{/`0ׇ<QPf7`]3g.EdYα#݅8򉀠ޞK 0-#%V z \k$/D%6cc 'c DUgM'SHXo8Yv2 :H}| FpN'ʎ`]"V,q+]jD}YV3$J̱}tL(>3ؙLxK2bcs]7 ÚPxsT &p[ H\yW ,-7vZ9if]q*UKE`s4wR4c$ʞϧ.'<_&PvG9 s#[\PRߪ ؘ SIѪAk; 䩻;`? ܯۻ{Amx>μ΂b@*xQe;_A'=#A5m6nQΠU2:}t6l 8: +/ؼ}"ez >zYg4%k?jٕH<Ɏy6轞Ak !"=τmf;iMPYKжDvgV>ۣgcQ׹qӊk"8x KO:\0׊fu[ص%M]p.k#%8.t1R-1X}sXn`J}O۩#X.}KNce"IۀVسWJfc#lVWުr-gZ;4r[VߥRM݋'5R6w ]moRwo)$iX6vrt@ 1NֽEpbcAuxu~e[[cQd1CӡST W/?>L[[\5Ifʂ{C߻V$no][y\IŴf@t>=Ο;/#=yVF_`D)$!DsHʄԝڵe2zIr߁gY:!m|GF%Vv-]nOO~FqzgC8n6Z/w5뤲 A唥=.4YY$5hRÃ|:faDwh%666#t"ХxUpJ֟:|-zz]&| ̤ע#IAs9F'/ά-3ʊGi@{tFX&&18AQ}eUw#X%̭_V-뚗m|6;ګi ^b&Ё ՟޷kt1A ?{cWG Ao幘gy4Tǭ(&~wq}/!_ &՚tYE ~ki$ب\cpLZ5Vhj$Kf}Z CѬLO1D';[ Oݚ@Z1 FF"XimB6t(:tacIwJ^p5<×WL̷]}L/ժ }%ϋ+Ķͥ$$GtY^equ {F _7(ԁլìuMJ]ASu8YIq2{iAH"w.$!;aX# To_23|F:!7Mz6xd$exHҪ~:Қ10CqiFt; #Hn;Y(ff۾TU,{oຉNآr5е,`6"A/#[]ܔm@6t.I&{*zme}'#tz; L^0IYĎڀcSp"իH9~c#P"Ks--5U嵂7SBX;,@n0gon8-:qz<{^_hWd&.c?Mt*g0vq4hv2Oeezp@9gG1*W_ǜW|^nl=}Yvtۇ{Ռ1|Wy )@fj?9-FYU j"#^h+O6:7A U1iƅASJ`Ͽ ]~Ƚ5^1+;A#f Op=Ɏaΰ@ΥNcۘړp3m/gt; Ch uR~R`\ܳnߟ}I-*:48bު]e+[@E[zނ6?X }8s{*+ äv * `Ÿ b[sɻ#اN2޿@ޣ##%wgGզ΢x֨jbw= Z^ #1\{1btՑ4;_ضc:Ǭ.nbNTuB ,t'HB9mya7u?PԶ[%" ?8VdN` ιKE9S~55@U8Lۮu#79WdU wg.WZ k(+er4'}>lw0)+OM ]*GcΠUN:K+ST@Rmu_'ҁCo?C`Z@3P?&<;̀u_}:~yW`oUUKT'`z 9DYe~B>M=7WRZZym9O6Vlnu.'i߶܌d[eȏ:: Y~KU'`nH{흩vCB]xb M]yeq!W%8p#v $ΘAom)&qGA9[Ț#0V^,Bb ,bXdc5uF%I&~vQ]5&z<7@e "F^l ZֶvV86+ y*dgVtt^mHpQfm{`}Im$di>x-:=<A^$j:6֮nւ6Фo'F}ŗoB[H :H ! Vaė}}$'{;4_b'@QkrA;4&b&`wy<:_&$a@#;e#wkUTymyZy62_!do^ڵ ,x믽 mT!%\m]F]]B{/0#h fMq IU)ql!#،i%8Y_ [,F'|&Pu{g*vEk=~]xLE2 {tVݧ==J\Af|z6&1Hzt\&,//\ԭOYzDP]?Cr«{)?r$X9rZO#Yt1h;@IT fR 2OS3tLNg^/Mw:m/cBzcs#^Jl<9v3Vؼ]PM\Kc7߽@Uw:/']|ӝ:;+O/}i%1uoܟ\h>_7qϰE-g7DMS:%-1 3W$s?>K%e?^"5rrck.l}~ԽܜkɜXOMb1#.Z``r3aa'w:Cdzi %긙;2 vMZ c QkYr۳)CV[CRV.8—{;MwIr)$)c!-snq3J2.{mB%렮Q^Hڵ+IX4 6ϭA, WW6"x^PHlb{dV`YsG'vm:&wqL A~0k |=YL?\-`Z>z6SLFu F{\|w yr +bY%[K).v&{Cǿ8~/RXM[]C}/6v|{2?o㯷4޴8@&$T,|q!-=VD#|Q6M5x/Usݢ܃cfL+ҵ=&*9d+O<SS %lj 7hf*ڣ6(u56r~hy `@`d@V k,@T6ёt)J+rUhvz^tvt3'--)=/`5j{J9ul/cnuF&c,*:̌ _h2Uݶif }u4䧣UtiK7Db:}= 4R[)[o*vZkLaKM2=q+/SE|Ng[6l:t0L7A6qYpPiYMP)^g 0R `Ffs^cѳEO8,fZEl9,]3UH>@Ktx+F`0%[vt20`{Lᚱ`=@izںMG~:*qXuH*.\ߜ FD>K t HjjiCUV{zOy2mh.pb\ifVsTWY2BaR 4lM7Fo3m1lR  4 `ʨ?̐>ޠ{ $6/1*[̄I=!@P' /p˝4h yoí)_[%_ԇ_LNU|S, ﹯ed[ùV|K៥j աӴ/xX蔊JƁ 6V z ۚvE=/!gA5yҵVKiY{h[wl'nեduҖ ~=mo[0tg׫ssU~3$ewftc\ Dq#q$4uvb`CIVw~N&eaKs_%y$UOT!!+Ttsb4]p&MυXu3יȊж K[4Zk1AOe5Vّ Glm/uc;GAأy ^Jų]=~]au$/Vk+dAKJ3muH%PǦnW]݈E:6׮|NdZy{ROWs1WN<. Uk2wį|=zdĈWi2#5֤N}7ZJABLsVL3m(=*^ \L?L6h dzv ㏟5A q?Htt_ӌ_/$D13g+JxG(`GkȟhkQEPtYd(d@B~Uٔ^2@%syٵ~*͎:?e멧~dGgٻaq~g5IǡHAw5W3d7k29K̝ tzN1DvTR/8km=ߤ4y*{K:6/rlAk GK_FC9dTiPn>/ ,YG[ؠ$t-l$p8br_REBx˵֨/~!HS{=@{;fvz'rg†soRWI;P.$vY฾-$ q[tB"Y@6H)3g=KY1eڽy2t") Ԓ`B`S}k;=WLs~(#{_@Ym={ V2sA!rU-T7~}/w9B )MaTGL9>Gx FZ^yJv&HT{b?NwǙ`/dp?vx1%ɋ94g.eu_q:pS#IUH rVJphQ؃:X#{nrMkaOs[Y/ ׾e_7)KϿ-I&aJ~<.&E򹹫s1eլiQkܺ]O GǦc_ Xh^f"@$m[ؗvT_Q8y&!lؽdy((ԏd% QS~yݽ@^g*P/D|YeD7숇$|CדyTJq=a$;ouzaPӟX- t<}H_n~m8n$yFfcLx)gO_@0WmLB:찗^|&`=F%{{St$ F<ك%A&|R,Nw? HGDŽ0Ni꾻g=]TDbv^%K&= <ƼMrQg[‡c, LؕV|"ή.'Ѯz+$&xĵ!pqe%$ c q6k L<`_ *f*`mY;'@OXmE1e%2:7[e⡾|1&C| h,[b2|:^[)*UMp?dx<_[S:8(aYA7҅qloJx(_I̢ >'6Ax_~1)D _ .1tS O<S'4pMMqdlS͞333ƢWSo]^`wuV44?^Dlfa"QK[a~H[?k)˸OKq`B%9~xy[D|+!‟ v~*#>#UWΜ6c7p|^| Ë8i"gylc;C8=}}]xX} 2ٖζǏ̲VV=/b` P`t.Ǒߴ)Ь@'^BmFӂIM+:謭mR ȞL/%q7oլ `@0kX~7qM1+/V넕7Gg0TYg2s ̐' ' gRRZHy@*mp f(n< wʜǒx: I*Vk'@£֙w3 \7)xh7[[O2X 4#Јg7p~c5k1, Vz4 ^؁uVYfm*̕Q۹Ud]Jau_h<}3{lȣz5'l:vU ٩]0AQLO<κs>'M|*Mutf-_pg 2jծeM_X.ʏ#g`"4I/ 8:&T xS~' Clp^9 ^eMq.b~Cw v?M.-ʀgW_ҁTbo=)HOȕxd#'Sowgʬ0õ50ParG zԈm[SKSe}!*lWU4?,7 d1? \Zo `Retw@T+ޤ{^*]~& nZTm-^Nn@_`+v;p L3{B0C^ $ > < 4ٹ=+g/?+ Sͱ663~O3C?I֬E]8FĶPW[+=uD^8s d hl@i;uo$lB_+H΄{f5pyL8,V/d4yb q"ǭK#z-p ,o0Q`T}8m keŶ+Jm$8Ҥ}V6DupXSN]*"=܇v3ёT:^JZ{/ekll,@L 7:;{M&W'?OIOs-nBk$ sɭ>zࡒlWgW'v+Tœkpw.b,X_ Zg\7n4BP!X19/Kgy+ݜHK_HM1;UӖ9bcC3[ TJ 7Η& b*k&&#l`DD&z}$3(W_x3Ux{2c :dӹ~4C"[& x:IۂsDf;#BjؾV>5zWFbst] ̣k:|$xLZy]ùgxL^u, C<3hO 3_|G E~2ڑe{ޘLTֲK *|^]ʅ~rw`{NT0Ef<\ecM?:OPons/'!r rxyv.`z=mOG{3=@\jX\( II4,Z^d (O*,wu>ң XZIff/s;Vzv;DL[H o{i%IPL־t^kYk ;~J2}}gon y'a8( @N?Jȍ].l6W Ϫo$`aJ9Zbda|EϽ$qcVxn1zD؄ 騮TwM8g-ÙֲI )M!_y ))ѩQ7G;S@MZ?Y5K4=9A 7v?iS@Ѱ՘ Ww5P8X{Wq<^V%jEE8Yq^::\L^ZBH`YyU_:]]igj|c[~;tj}$qaTK/iH;lTB/ kt` ,`) ]&V9gO,< /A> HUXڐoJ8+u^|ؿ/@L:g]!K0m+W:::*V},b-BQdrcC<+@Z:Q_So𕆶\4(c@u@@4k`2]I<-oNS W/'Xy$kiN;=%)+Ƃ/ 6:e>X,H[A_!;z`!Р Ydxf7X4/sl,sP gm g%*=wl)mz 0bfD+mxs&GGqkߊ뀁_ {V~*r>nl@ (crW| O"1G1ܖ.\;¹3gMMw Kh/[WJhPwNྠe5ri=t XyȾLUAE@#xeTL}*k<{`1-mf <"Lra;*a *ydu`(/:0p9:)pnh`= `@ۀCEmϳݬt #pecM6c<:.QʹsCaz>1x5ԇZ: d l keAD+e' &yT4YYm_y^ZSIr|(73_ڂSYl񉘏2)K r/Z} ZȈ"@^m+JIBh)$#'#h!/X`h+ن65ͭӰ]*㈁-ùN3La2L u;[jR*wJev%TrPnKo>K:;; ,1HJӇ7r9W~gGVKwGIi/\4(>OÀ{vq+=q_=w΀ތ4Jk,/ IYdC5-+]1M6TV LMcMEGPj˯:IȨ|>A\"vjmW,Ї9W"Q@4Hh$|F˂!TqF0n$S`/ʂ& ,rc&v e.RaW2)J>\όػY;((V+}& qFV` +-- so k$@ǨDǙ$)`ȂSS7V*_1kd}Ȝ|]WojYu{ 6reS!2l!CزA2/A{&yȅ{#`.p|>݀W nK_㕗|◉~&u_|-8q,a@T%7wVUW gz4 Dd-SLz2RG hyx-|(*UQV/,<NKK|V$X[ۗN=p$7vɵw0X;)*{ǔ] 2=7?EP?`q1(\ K!z7>:- U#T ik 3Zutc}Q/a02(VSDÕKBVZ }h/1ʏ'&bt[_l7]ev~28e oknn5P{_&#̓8RKPo3VROS\ tH~NSHa||Mi&Y'΂򬌏]?Fi/u6YWog/[&L,dm}2Mg8[Zlڹ!=L9˄ɡsSϑbKK+ti}3?Yۮ<&4 g I&#L/\,A^ /Foh.͝ ʞ Lq(s/g/oK}}?$dI!D|^{G <& ȑ)3Ig,s|ͦ }<.oj6tdN&LZnH=Ag2qVϰ1Tx >cLZ3 yCι؉ ?=ģ-xxby ="mՇ|$ ~ \kkľ!kv݁"ßF)>M\`Υ ,?zP88q-Mh׷͝yĎ`|4poнƦ?&?ijiinGw؏/VLbfRY!jpzJN[IUnLΕqT Fg ;ȗr+ݼv d n ldY9)_”SGS 8i0YjX &h5C#'ƈMg9 { f7aK_5彨nQ5 {(OUl<:B:>'A<ﴎ @r.J{bU5.d\QG2G 4k5 &uD~3@IDATv چ,'S5瞫9A:If ovA |YCѪUL|ϗti&8cuĹc[Erchۆr9^%@`N@u1/ُDOܮfWhgVSC>j5 ]/dǍG 4 x܀ATA'<sU#1Sȫs @1&|3v8|XA*VA:Al3e.nۏLHh7::t=lvqMq_dk$l"Oѻ8>cֽ8Kj:T˴L?[R[پ cbrNu[%}$o|s7*r]hiEBugJ[*csF 5>|`Ah `ڛ 4n!i9ʽ2@>9@fK,FϮn6SK^<݁6Y;g*b̃&Yur'ڵyW3CFSrۼ+=o@--\1pJòTt> @i۽ fWVIbC yt>ws ij6Cez3f"泳lPOOO L1g~=VMT/[,"Nq OAC%A$8hZ5R""REs{/猖kXX`r̼s*T?Q1`g > q,3 RP¡= 2Ond@F/}tBxOg sTa#yAKNe>dM̦|:r}}㽼R[d# 0=q&756Y{C%YtH/Ҁd.Y0tG02I0Ya+8f 6Б'^D&{%YjoHYOkNҋrZr69[ҕn#Wב!cxE/ޓv6%W 2sqϔQ3uw'V.IӭJby[jc~C:Bo f=ut6&g?{/Z7cd--h(ahp36UT̂ pN7؛ڻ^%:%,յ7K&xQ̕8֜|˹^*h Vqf#tww3<Ǟc#b K c ~TFYIF: JknZ1K1~źKֶ\Og @35=5ܼlR -^]~J@g\r =V@arm# r͂YF΅Q+WzT~q x{߽~@Vŀ^Uy[kB ][>z0A)QFuٵ2Y:=eATpk &&f}hHd|ڊ[/K#3YϱCGW/c]'{hO dJSjpN_zI4ބ\1hTT~X23ubPJ ѥٵg33iXoaU33\o l h qYyvH|09eG޻w(m:FSc[KT^݄On|y&J=3d0=?9z@|FRΩ0no_\= `Иm2|%}5!C&7wyjDg9QҍdEƶfҞ 2xvKc+ߗk;OU =#|[0y,%Hhmo%ƱIf$L7B{lUsϧ^cLjjךe p`jZZ~b>ǜ-uѴ@ \[XXaYol"i | \\DT9} N0H8,TɧO혤)+d%M3?/gjSk)ǑHT҇/ >D4נ/+p~po{e 5s824,r/dg /X7\%TWx Ѣpypf0?5dӟhsŖ}˵HB(o;6+z` KΛB-P_d>Kq.C ή&zQڲd-`ѡRדqg#@#j};Z8Z+Ѭ J3-7W)W[[ݾ(:?ᣎ~+pljp1ty-)Ǹo}/u׼sBᥢ?qSp_~FC˲]RzO^Lıs(fPnni0LǵgːYnjK#K T#IDaI @i)i eھ?L(Ͽvl=,g>ϳkHL.=B8DG`#(kGd7˰LeWHYhF%8v?QF(́ 028(4ߎ y200ð ##ĩӑWcrf_#}2_w'?)>_K1KY28(BZmN'˫߿q:Nܺ{i j`I/C  qXr1d6D 88ua ?0ܰI#V|0bX}54k:W2(h1ƕ1ttTҒ&؋M){;6n8tlX Q :aQj\Ǿ8.\x#1ݸ~]+@{^{=2^ØׯF@ r~/ ՝[H{.h<:(lĵFoa,4ԣ#8p@kQR=0Ӫ>FU`\{uFRϱ7]-=EM^mPR%j  é95E0wj3]]c4Ïsʹ`x2gA.;&x /{N2Foo m^\4g^ݮB'%.Y +A:mYUlmE a|*(RglA s?ȥj*ks%;M.؄qpn"q*:J >& tH/c{-?ea=flSx`e8\3:i czWR->Q},tg>^KOJg:le`Oi^x.p -,xUUy:9hlę6{̣gܽ7g(Khdϐ qmw8 g@0 _~"x`)ڪ)C^|V`rkVy?\/E+ |t7WRKkcgzA^Ǚ @"u4:ߦ_y Ul3 ѫ?))SU ޭYk{7 Ν=Pi"G3 M?b?{?Ν@iΫ_&\鮅Ooٹ45OI5yWCu k"; R&u|( 溴8!Xj%`9A456) k?G$@̗_x%+1}w౻rCS['U'gK@E_BԛM bfraM|r3~ޜN/]n"gz:v,Y~XE3t:) a@]7L07O)5t6H3-aW?}/puF4էٟ? 2ՙ_ыʶNgұcDZA$Gݣz Nu2^<pb<۪(] |""09w_\&OCDegLiuB. pQk[T];xo4{wcC䰎E[lK b"m{79ijVۂV<mȰtIlN)؜JO;y5@3RƷ]YZ߲Eu H8 r@\̠4DP U5 7"#̕uZ8u:o|,ga#c YU)Ml3Vn%/CkjrmWߋ M+nyj |~uV}?SNMaq %8~8O' suF^ "53`,2eΠ6e뵭 0 nU)XUO.0ЌpyknZ#mYGPM)Ύ 1΂&IB`%5+ yJ%+ծr@+,ɳqAo=< &.~}k T4@q--k`RHU9;^S4c3@ZgOJ RiU/2Ọ s &fm@1[ yTFP"||kJ#ʪ e0Lz'1@pK+s*#8 wBv:Q*jQalj:Jܿum-t,e@ԥGŐE t%:kC.t37{,]z݈_'}֖а/)ndk$ C<@@}KSdpp@hBь:M%ێ`Ę%p4l&h:'w{'y' _ .!Tc>>%i}n·}GE9cBTvĠC\/0^Tu,*%="J_LAz |af3-#qTp6 0 r cgSnfN2퇬(x'k}cPyWWqZMצY@I.{*xԞt$oڀ- fA[5Lm&0(p V] 4D8N4)0cZjvbgxSd Ҕ4KOGEG:}~멵˲[̑g5hh{<ǚS >VғO?K뎐_ ) ius S2$]f2&F h90.teEۙ.|)?3=\-I YcYG`A1ڊ fH%3Mi8=kWe7ϚskϬDy_yCq&| Ǥ<o/~ZF h'2*үYPS8?{Ԁ\҄L,#ʠ5|cC3Ͳ :Yuϳ-LW2AsIy[\;jG9g(3</yUڛK=z}c ǧ]N#ϒ.Ypi,0M-Cg|y2k23| dA՜džp 2@Ք:1S{ :e=Cxvg]9ͺؗnzh5_^yoЙ_W3:Yȣ/sw]@ &5Xm XwָF&"J2͎w_/v,32y0*uX[i uVWf{k/VMQ'~׮cd2wp*񽹶8;KP_!y+~E`s-ib8dtt`G_XB*(ij[輶=}Vy~s/=]|^xI?я"{==RGK=@u'p 1^2Z|/π½Zx8T_RQ)8fLm2̀W7y1dNzWs=FGtdb?qЯKe$6lG^ 9nmd{o|@*??~eu8,,@#bFv@Ӹ: -{yo UWOI~H5t)C':#nhJg:'O³;o l[sg˩`T\LkU|K&dž8ngw7i$!ϴ-;Pz?&ѐXFVhSmʉHHÓi[~f~ʛ=CTg/w ̲/E?t1=bo~+uJzc+W)]o9yK˸O`eѭ qv/)TCW9-BF`1/T$SEVco~*͞4ykk"N[oཇ 9u%C򇽽c\%ϋA6;yukeo{okT5mޜ2[Pـ&0iRʶʫ6, n`d4ØXVξygڏfzX3(- Rguo;[y}g# v12CнԾ ި?B+[Ww(܌K~g`es֩WKI $}dgB2!C).ip-_x]櫛9ʽ!YP݌9`~+BTb0>5=$҈MVLk[yXb:슚hnnm/gxsV{y4j1ʪ[;K x4-['L:ÕO?M 0dE0+Eˁx)SlWJF}X{n!aس mx5J:GI ҃o}ޣQ:: k Ry{9X2bJ1fu@ge.8viX=սg 89.EL3~H<εH9b`.<N̸|ZyA7LC->,δ\K[Jǜ#x SiCIw UV[PDfbi;-R9a}$^D 9mX]Y{셾YkϲVr2VU?݃gN'ͳȅXVh>rhV+ ^% $r?}e2OXŰ(SsK%TRy>1qL3+ݦAR(aoC3>9>HUw:U\T Ut=?2oټ^.yqKZ{~'0|8{) ܽr'87qTn֌kfwBjdkOak?IW(aϫi:$z}c "ZFG'q /w~77ETٞ,Pe 1jCg}c5MJ͘7 H?=[O`䧞n z˳.k?OC]z]Xtq]Jh3(,#`9-OKH|JSiy`YH89q@92ЕGq ӓ#d繻 EJuϧ4OPtGSGwO\xIYiDToZems&*\ #3ye UAs˔NkjNl<eedQ˷~y!2Cǔ~ -gYݗ:qa RBF:<3:*j)54H'AW Ze]uI!Oچt8-2yZ?v=MXZɆg$-/ң dm6 &.^br,:FƣQ܎Z&뜣\ 8z)wp{\2~g p0!gnR*ӉЖOo@+ x~)yg~ɓ*-SSHvh*ØՉ`zEu@-rqv:g!?S@'*t>[F5Y}}>kYV"uֹ @^yՔH23ޗ}4=&zvw:cm GA1[jXJ5y5|ƚ@TPɼ?1qSUne*y,=%6Tr[s-9S_oE;wsf3u2:{"xFޙjNoTs7Ktt W=2[JDKdhzn7K ޾q-_*1cf >m%7qLSThRlb4.eͬ G>5Vq~WS}Sϥ`,i{XBw)-7pIpX[^Oϟgy 5XR*`H81"xSrfS~[pklju-r_z@N)s/~$[$i3y)mmXd>7ϕd/tA^ \&axYNSy%,:"n HNw7?Uz3grb,i/Aq8#A[rΐ) xr*89H%P+땮M>rm㷺KȋkM>}xE:y 3ޛ'&O/]NǼ>vv(񘱷H{3AՀP:VVҘ$ "T4T!=Չ}5-#^s)c"Ҏq؂ ?A?p4hJL8-nVr&dɥkAyh6<:g꘬aJ'SW<@/1z,U k Y"6k9u'?}UA~c"h_R .]8{<JV,'4v G2^yg-YڱdB%C:OfS6>rK3\ {(RA˛ 賐YƄͳ=G٢mNf2OƠCZl3W[‡C}2I9S^gt c0 *!y`Z[G 4qJ^;N)zk)=+)3_])mӢbn)vxgs8L<_B]!2 Ҋbz9) dgk?2+pA݌NF T=wg;R@e9^t; CJ&9I3sK&l-TZ'T`;71S5W[>LU p̗<^1s/_ҷɟHy,cT@-yt%y T P`嘌׺3 K}^584@esZt 2DV:wGyY :VAw,HU 4.\@6S[.Txmn: hb{rce}hTRM`Wg:M|᫰RO<2P{Y3?PmlGe>>#+RUd[5suȝV822}N}uZvx-v-8Q.ʓd#/_l,̠ki'tϏz[סa@O|G-ґ7wQqwQ~IUWx2+?5TQq3C-:> ᥽BA/:\#0[W*xjS5kVeo 2-ػn08Yh5]G+p*h:,Qfvp/OId,d7a\-AN*k:c5^t:^A/]9_M#Utjrߺ@e4G}ݷ){u"@*p`JۇR JrtK{Y uyqid5 0!TELѱ-q)79F\85q5vNJDOrNvS=P1x@V}q B0QS#~22o9,W@o:hUGR񁥣pPHI9A6R@o!uJb7˖{/7ʴ>zy3YCifl /MikUCD0L4!q~XԸO} #ql~%0w3uW䤞'JoؚROF(_e:= Ɗ >N?4SOzd@980/R =oֶvh^̯JG~.?B4|ܝ }5鳃sI}Kc1/a=ӑZ!a$ ꢧ.y8!X{ef~=3o}U.^z(3gRf;c+83^ 843a^e]s7#@B'jy1q`T|jhK3ЃƤ=4FW:Je*8< 0`V5+h篎`WB_?,0c\?zJyRWLޮ?31q{dsuaϞmdЁilPە8\ P]@@1tot7>"W)v:(jҀ:`{3/pP=u\fyw˪BN.뾜{XWG_fH6PĬvA*A'u=tz޲}ܒ;/{A3>!͖^z1rufd_v= @_هDV ?;n ̶ s/V|Cak.l7oXP KXv!# iBt2,uN ZBZȦ -.ٵ 8ml0e2Ƚw9l1a3Ys-Ɉҁ+ }8sҝ<74e lmZ mEU7rlbi咼K5@+=Q}' 009K aFsGoġ%'@[]%ؐDY3 wų6/^m7*-ljy.w],>m1NA%^l*"` =Ax):7],+"}4cy,PK>2cα|>2vk幅Ix/!ˬ}X+]5#8(f.{ XF1=2(U 8@ 8=c;e'E2=lsW! 6Y#EuV]HPVyA=873#aS>wm 8qMKS=繌5W\Zu+j)ݛ8#(q{>s8HC)=G)*w.ce"Axk>ϱe:!aMGeƱ˽PNz▨>p_ZAqP 1ŮsG+pG+ptfEysܹ@)WAwA"QΰP$股Y^ά17I8DTn-#Pg_W K<U2j (- pA!q҈hՀFfU\~iPRms;L7{;sj3-SE;fTPɬP~ًCQ~άq{[\cе lNmYVqc(%@gb[NTDeoUj}Q>s^'^O>L8ѫ{3 ͱW;80Z&P8kK1 j@UCccI@o0pVoQ7`dh;2ۉB}D & jZ 3##3:Cp@-i0N͐VJ7`_SRX]orddhoN SDWӅ+Iol Gqv߼gLPXCH=!kYy3N¡3C[m{c3?u:JWq>R.pjާ8-UXB%\#lre4&p40EGgxiok{țCq}v#kp?sDIM–ӭ!yؒm:sKq".R꿜 2 KV}55 K˷͑ 3/htx=]KhQܝeK(c~l._:,Ga\>ɥ<LJ?X~m1m8L6 k{ GSa} p˺:8sy^tU{qR>scOmkYMȗ.:]q葁]8<5E:@4a?^=03C]gad$s\f~N,@J{nF:L V "8-oSX=spmm͇Fn,0Wmk:>2&߃'x77ޡ~ejO/-856LIG"y"hikBn P0,1f UL'=_f6/5q&kCR:j)nKT Nti@%`[[ovjj2=30룔`v?/W峭Ds{w7[s6ӡ^;zO p9u>i@G.K4#`\/7kV"[A9r G,3՟zz{9~yu/3YMŒ.ӖV7\e8Ygc84 ڻߕ:ǕOϖ3f53^"NۛSk{{u\ ?^'? esp:^\0w] eusqrfmXIE 6ޖM̦g?{c}fBBN)_Μ=079Kt.R9Yr߶ <$L"*1qś/KY0 {FFn),%f)oANiXi߃,2E1SRf&{]Ynry(bJW1_ 7~8cНHгdꄂfqVwu>3>Co[FQLdS)3)o?ǮCP |!3@h I 7˚c {WVY_2kg9Pb NtR. h`: <^m_^u5`Ksz  p^;Dp9j<0q-AF8VJ `I6u0`?Oum@=)r3~;4cm@ 2iJ۹Q2 `Y`MtiK(ڔִAwY=UBCluv60q=ڭ!{x>ڞswuJ?{NMr2t-b_ {H,R͖織 @ؕ7S铑Ih$G^h?N]XF;:C6( 6Tk?dF<i\T5Ԗ-!`nAonPsF+Ǘ#;zhDuY[0uun66ײ}7.2BvW>m+r7ɽ=g1g/=\`?_E%qB[D@p@}i; #u{`I3I @BO-  x+=KRkCej@3 ^Į/Q}G 6A.3S(al&0,*4ޢnh.GYSx&"4fs֖W &JŠ\>O+0x8ʁ iMl} ڰgAX/,UZس6CsE\2cԖXkW+u:yʒ:,GY>]0R=%ztcILX1P Fe/|*f!_r7 0_*Ap[~޳kθp)[7fy~j)5mBlwmSIL}E&,|g?rrQY}Ogmaie#DȠs=qb#8]yfA K' WqZɭ4FО~Pn~?R҆,r085ct iP,sCB4p@&lL?q屧<Ϲ(xsp/HaD̷Qz J ~xE`sŝ-Fohzqlݛtq.]Jʴyf&(kϢ߸7LҋJk s8Cop**8sOG?Z~QsG+pG+pp\dYDuVXpBGγc'3g:$K:Z+ܖ-`.4nJ`:=ZO``#uHALSgL88xC>\N8aXU4uB˿j Ŗ2 1'{(5ױ_|Xde8R߻oaګ4sx';,QROO9=qxR^[[nu 8GFFR=.<8 8VҕgӇccTU[y5O0qcQfRi矽YNJ0cif?ڻ{q"QOsɶ/,=˫pH|YUB2"1|z`hϓUUOu E=t/%1{&Q4д3DΜ"D4=X{&G*0䠁A -D!hlK|AdЬ^L^ CWR3T .%,P u N@anǰ,lH7k{_ pEGd`=ʚNA#^{'~h>d%o_]o_5F;p}oCh'ϥ[aþ淪cpvgQ:%tT(Ct|B]e1gjXtl$&  &hA"e~x1'tl:7>^: utΌ4YNpsyo)g ~碣^kgMȺ)YyeI;mlx$u_oR*:|V;l+yٴs_|@rK"q}_tr.*_X %*%g?M׮N"w(ŮSҗ_t>ÿ PpfsoN諈`2>~:wl!(-IU#uVPv57_·sss} Q ύq>>/>G} 0/*)5(}љ]3]No4sDx!ͪ g2#(Qk1S>cc+Qvt):sp  @[R6A5℞M#&/fI薄O?As)֠&\c|xCWgѹ؆.`_3:#np^,-|{s Xdn}tpd zV1c3zCf &׳7<8A +?\ &p9xxs*JDb],JUL%6ꈮKU,ih GzOνXڪFc)cl1:mF\K[;YdRC~G1пvidgMwJf;̚ɝ 2%X$O>#] ZJ5<29ȂS3-^uʆDH㹬k6CPql3:.ssћ pDұV#1@+"t{\ueӱl왠_."q @e[8p 8ANAHWt_ُyT3yPpd37(3;s xy|:ny-xʫS[I xz^tsFG | QxX16v분mZLVT6yCރ4Tn뽢\54^/҈zmGr8+]#)HY~y1&m]f=K[5<mVfOQf q/={U6<3#qy|ki 1vl[nH^W='eRJ[ {{ssxNLxblڄ6p?92C^/_\&~%˽x#dIҨA߼ qꎂ҇qJnq^Yp5f:8_ VP.:m_Az@ܟ-q5_`fЌ~:A͍TiȫJXǘZpY?Yъw>ꑞ;y6᜛l{m=oP۠o/Vr2hC=0 k.Bkc_ AZfH;'FL-XgA)+ $]r1kskJP "{9Gʖ+:s^ 8) ';[HYրmavwhY:eڥ#؋mF0tp4]/ D k={qhЙ4s(7֨]{=Z8Z8ZO $/\-c99~(**uFs3^uV@NeX~LeXNo{#7;hBS96L@YETZcIτ? ?a/X3fV ~{~>~Q+DdOftu!ʫs,~On&%38o45kDb4gy^FI#^ bmm2'2,%#xU,Cҹ `0prutG4}nYý]0_RNr\o0z{ץqx0v,:wy=O?,rYZu\TZ9(㚯-TMtTK^ "cEd9Xw=ػZ[üo$ ޓg?A[{;A%kdx~,R>J@)}pi=}?d5Et%|w_Iqð87j֒Mh8&)18!A+!t{tǻC,o>x O]H>ɶ+1kt@> ̀W{гrװ\@TG"5ǧ|*ӌ#})Ӊ# >Kʳ.:HcsW*)ؚrKCW6g_3/櫡4yWgPw^|ln%+sKGZ4BI#Cq&:{mZ9sW_I([YY;J{@Y]Mm$ 8)&.v`m 7Ϫjqw畁տ:dJg%[ T›\g̜/U\y~6p.tqt>D!-+?E53ulF_j502X_1T7=NDn ?yr:ݻu+=t!{W?"otr!zTWzWY뱠/ 0mki$HL Yi0P쫭i\م텾`!Hl,NCienY:I3GY3,Pޯ:)!5޴85B;K |^B_8<-m uysV/Z`A<{I4i36}gLcGYȒ8wΣ#V@Rooc0*FZ`@VpҬ3@!dki䃶*_k3ж)䪼Ǡ 3ҥAlH>V.MͦU~3z=orP Ht'gY/&uG;e jV)G=W b`+ x\ (J]+xX NC堗p~ p]o4 ;u?"Oym w|E0,kרHK\G,Ae"q qRBrA[2##\?. g Ty,VX#3C>C0ㅏr,y/[} bl:x3϶1pۿJnskˉhU~ ns_" ,ogu<,C`ewH@X{5ϙ4rہsBQ`_@;|'&ʵҊ6|=ڮg`$gML~Cu1<ҋqbyG%! e>:>}`! vGnqNg9`&~>de0#X_ 36kI:a ]6QyW2Ȳ./#p1gǤ-!/Jp^&fߘkgZ=lzM97s~ -^d~V_B>/̧)>= d$ΓrߣhVhVhV *]?gJUYg|;c8lG kZL%xWY68D_{{Y (՞OPR )gaY9#"Tqx }=UX34~Y>*^ǘ e!ˏ>|>2-w΍t5gR(9i 2Zq ܿ,b NI.h JTC, 0P 0zmwfۀ:0B*CH v -O.>+=q1љW뽋ҵE[,L5k39^[# xav8dO @lz0oo`4=r=?u7XNKN#' тRdn: ūdfcഫ,X)L. |M~Kw=}-'Qf4h^Da KwkgL+ Z5}t$i괲\U l mŁb{n̜?襧zBYd)$dIoϤ=Wy3:,`h 4=Y&QǔevWS'ڟ@u@V`Ԕmq~f}zg9 &{a\L7ߜrg>AdON;o ;pwޗSJ?dCP0nk.5Uയ[/}k|vUu}85870,mtYpd}8 KK5$)VFydgkvnbnm8p*6ptrSC}q^HϫjɆ!a.?Z:$ XKB!St<˱G LK:kfQ81+i@{]eQi~>U5N+gz$?&jd/̥QUym\[xg\g[D/Wdncв& {SVBU|zZ]x"նւ CLj+Yp3:;YgI3B3X×/d|댲]EI9Y&իT԰$*]#MP*]FȮ YL&BfGvxϱ3) f/tv ʐ1 KYcSuC{C4r @O ccUưVr+S]Ua>Y lYZHu,{RAF[!΍N2ԟ(hQ8&bFklX]?p&5BƖv}CGVSEF|z_wWdttusN[.mmG|GOV8 uZwt=:2Tu=~w}yHC9]q8&[/dC 0ŀ@2[5ZԳkǽ̮*?n ӟ\&4`] AKuevS1PJ;udVךշ߫os4__,,=!)IibA=^e3xEƿk띭o'lf r0,+yss,byu*^A.&i"gW=GHo=^Jd_[:'b_ֆ SpxGh.ֽ{~)sRڕ=33Ɍ.m <tˑ+='?* pSuR@V_wV. ;w58;,<<:ƹ2%hC/zZe!rtA`9tbկeta,)#+zß2z @zw'2' zWӕQ 0^s') _IO<:2! ؓ4<25sz b%ۛ=JXL${kUP 4p^:afmrl(;>u'=s'>_W= 6ғǨŹW.]IS~9t_>ڸߔGe0 4Y]7L11:7grX$%c|M x]d; nm8{'ݚr!l e6A{kΪ9J+hg/nnBoeTE}? A?j́"R; }!HOVPJ|=tNKߝ tj/Si~:I>u$ghؒxjFNα ma&gtbl4 | (6A}4PD I}.c}o:z5vFo 6FV"SBs'ۅ0Y}>.{gT!yΜ@>c]00MO^eJ? g<g.wAwnKFǦU2[I;I?zUL,SlN %#i.K764ϕg3 Qg888z)kgt8axwϔv<4sރuFNg U_V\Fg*UtЛqrn}Am5RD/:5tyr05]t x1ƇY*yk\F]1`tkP1yWȧ:0;k%")}DX3HûhP=f{#RVYq:D45]#YG&h#x+ml50*ye #`gxl:LO=֛KBPz2{ KiHR<;sTW| g ?8 &:\Cx!uF'!d4=DηM2Y ƭwoBਙ?:XZ)R\"0܊hޣ;:ERn0 Aǭe̊M޷WfA`ch}<ۼoA Y46H'?; o8c> ^iFfHTdsiwކھzjoA:N1nn5q&r9\CGX3ʂΔeJ{y:!Ý5I92ʾ i}hPZ/Zp4`fNpr3ynU䂎Oעcm"cr("XSS8Ӊ><xs0"vzSg7/*x9$@p8zBa]m={2 h KB d9EgG (4z5[LDevC?KgAOV,>}F/`YF RfT{{ b 02K8 ơ4WTzn}z.?.Gp(7ٌL^H75$&Ҹ+K2&)(zDZ_Z\#2=q駞I˟`-0\|H熇K_Zuc'Υ"Sz[Ɣa m cd󀦭dK۫=D6mozůr>p|Oߌ a<7>l>&sX]N/~G>30$ 8! g5]K:T>':fd`a~WV#V<: bBvB_ߞŏ'K"t_'new2uU@ 733S[,(J~ Uvtvm+ 6&R3sl y) 2_Hz8>\Φvy _z<}7nf-@IDAThq*c0|2u6P[= fM셁Ad[ֲnVtZgfr/[_y_cۑTi_B -h>qwduu| b[ѿ`*@ nV؏X6I5˵R! P@.zqa͏}} +vvIrH=sf^2 8%wmN*|8_`zTF&wӍ1 AV<.18*OOsN@rm3řPo6hЀ䋗 }-UykO_?Oχx?\]uƿOw!33 ]E7=08BC41禌)tWߨ@+ 3kg*X!3Ф;OV><>|gr3K >?g(J;FFv'ggkipVEstMȌXllz矴<͊lTu)/3ր N&2nSЌ TгXQ>&qϨuϱXd:l{ʜ6Y=*Pwufg4C‚Uj,<fBaxƒxƲ`aߪ쨝k&ug2Y9檫ϪM 6|C7@4;N=N+CMj5Cg3aǙms?rkVϳO+ANߓ,(o};y* ?G;tVW'ҮFπ.sq8fF_mbkVݣ~RDcrnEU {܀.NI$:N5}# g y<!;~"L}a)_4Rm:@g-^g8(o@S^E*ϏqDY/UNLJQQ' g?% cԌphڱfik$li084!-lcjb'>PaCY CJs8tR:#¸yx(F YS'eU{ 5٧1 xe_t!~aȖpiXOhVNd9vvLsȀ̒.K:4noPjNpVmn?c['(\'c ȝ}J_βOwWs<: Γ.O܋ru={8Y ӷ44^`Ɩy^Z Q/X #PK;=kp?z7>@Yz!@1{TZqrdMT+'``5\"}:!b3)avFuV$Q*ʔJm{򰙺aoc"H,+`y 9Y^'}%dMlc4(*+]5RNL8C ܤ}>B}a1q@%JųXWW2,, {p) lNK[6d_4cZ|+l(]GՃ/8vבs#$&:gkC#df>9&8ejW oՂUbU-d.؉`U0uRc?lEV npI%σ5uJa]8؂/hޘm{C#iF:N= -l6d ^Y\kn۞n[9! @FGL7 qYpg%ZNC2)V1g,)J+4,Paѱst ;b3̄4t5/9jr2ZghVX"h *gid+ȭ@UB*M8)G8Ua:Y|Œ}W_k.۟pX,wVVSPꠏEtev߽due~V"zg>;E%tWZQ~C?Ʈ< HAdU3Y Z:SZGL&!zBT6Dڻ#q :vKA-='sG ~o!Of!id|=_ã#VoYkiw 3;'Vi싀řQ{*3k3(t #sq_q=+i;L##xF 7Sr QUX#qd%2x1g`I32xI+KGfmnc{pgOڵG`:U$fՌzhmP!:_g:ƃ~Î8s>]FTw/%(L9&ym:|؜f'`>I/1A/8<S{;}cO'?6cnp4r1$[9m؏J[׽=;.s6(]Kxn!< v+~=ݾu+cHe?B6o([]% } j5<,=9 2t}]rϴ# U/70à :ǰ><ğ33/Euqm/SeZmԇ)QmJ~_6'z'^b!1&0{ӠrLylKƞ?:y1V+ř~j^2YWEp{j ;CKI̙ 30,Ѯ0`M եSagEi".͕5r{Я UllaiCW>B^٧vaGuh!@>i䱍=lU Wb (h {]y2rMUږul< B`NcjGs3lD6{M6?o B)<_MV3S*mT HW{6YFߊ 7kS\Y=͖9zgvvcOh #xh*N>BH zvh5Ocmg `*+q˵fV< Q>Ԗ4~Ϝc[Y Tё7kK+__]ŀ؇Ẋ}7wKl@nn1 hF~.(캟n!}9WʓU%͂:2]]RG% 涍^#=byL?H32 2n!KW*֣ kYwQq;%u [z/']|Fԃ\0Z)-].ӾaHShoR2sr}˳jT8ViiKam P߲f琥b4N(=RC= ݓ5oJk(f"v~ A@2d[gref<ٿk 5|vs#$786^[} C '6%UJ7ݻf:zҷCRdS:6PqizD:F g f6qt# JX\=7hL=][Ǚz8Ssdϰ3 jrs dvb]^%~:uS.|otƺ7c-Lj!pLpdiq'Z/%]]T, ;XcYgΔQ{8'4=(8pi^Ĩy 991ʏ P۳v&`|0̎=3l_pO1(1ӌ}(_3 TOߛ:2(H{@.8*d?w9*-@{( oF)- /3?-WBPCilD0-:&tznR@UwCj2}q4d =Ͷ?N9,`BaAzaAۀr -זTs XwuT:z 31L( cѡ! J{{4:FfsPNЁ;u<4X楬dR#2r8[= 8l9w{2өP͔</9?e:']t^̀ux\5h̀ &G2]wPֲphxs*}ۑwt 6`KJf+y?>Kg `Tnv< gP?cSg|9;QɵEck򎿣>3##ߝ Bg)gVpޝJ >c4׉/ nd$U{_{3QfGoQWv@0YVYo 箞N`UDrvda={sM8_elHlgJ0@G/y4{n>Y<K+o,Ym0XT? qjɏdɫ7ښQtrcOƜQCҩ{|rr2_>ǑAl˽sO(HИ}%Z4YȳVp HX[=.Rٍ#+98G/rkKyXB\xcy2ݛ 0 n*V)B_#V0;M hnV,z8Ng܊)L *`tXoB 4BJpxH^٧‚g;00&I+џR҅Y]%{ٱ/Yͨ < -|r{5ٹ\u#d1 V'<6+\0I 5ޑ_0{NP|_MUty5:6t~߱ J RPeBnFsx?cu)*E.h ܊ 0e:;;Dؐ.HQZWm"ןU1S)Gfjr9s-ww8 F/\B>Χwnfuu3U`kbErSЕ9?[k!?xFs~/ڗ?Akȝj3<"xA A.tAgc+}lLP'Uė2A4#"\!}6CVN |ty-s(dLxb  n[5 `ۏz6SPi3+x6ijMSwk}|l33nmxZ_뭾YF qRzG rq{?$@Q|\VM}%M؀Vyz03(iy!F P?*~kR 1q?b+gL'9L,~} LtTW_"uP.#l3LA.9izNi*C#}$ﹿ;}bٳ-:XCm7O`(ˍۅpqǚ q}կ#{U=1v~B<)sYB]MlL~iofvy؞["Bg}cG']RO#w~s9~'y_:HlM+k<=)TGk`i3W׎=AׄeJ'՝7o>j+)ǝT3 (pF3 Qn Li|LQ*-:3rsg//_f60ݦyԢAӆ!*pFlkp(]R}% * WyJԳ-^J 0:{/a@@쀎%, 5vY@Sc}#ԕG 7 F KX>ЊXYAWMr(, 9/OQD04@5`u [ _L$+/\` (;"Ace/4шe,f_؛ 9h y(p3s:c՘07;Z1mI rEcOg{/e>7@M'Ya2}^k!@u9:DJ/UuXN[KROzoǑY bP(ܔȔ 4?=U;dTc |~48QYzNG{x@x@9o{fkhAlN$Lv>8 Ij|{CGoq.uc-Z;f)ۖ硶h/] XD;{4I:q07L|xWm/Ve2m)ϕX*,i \VS%r:xhjP^AK긳CFɬ.^GV (10Esг fnN:ye&ndw5mrLȌHKnV1|k_NWS3*(.Q~y`z1Uwo燑uhØ=8&MѯaC >yJ>i93A6s M3J$gw4ci)*XYt|s{K/eg`g朆 Z2k,d`-&Q,3X~u^=nO˰z}:=oi͔=fxW" }Q|* i%MP,TGurzO[<2%X\B+{Rq eG kXuc!}AbjggciR跇4i2i| '=)c2r\P{|@aZ|hhW6ov:RJc/AthC_jz7SP.2~FS bSh>`XQG0|=uGi.~x0逇=y /~O7rAfs"&R׷v82YEEҽeUvZ7LSsk>v?3?z^ght sOsV0U@7 ʐa3d<`@wfKe*2a"@U$BgDe+,ADJ_?S`nr)I}yrǑ,U  EkfW P)cNRm Yd =3AUhME~}O{ `q|c . sa>fv3n앹1'MCk8Dfm)O3}'jVd*ǃ|#cr5Qh1_Lρ,89s yN, ߰ڕ{֪KW& iWʯ3)ϩ\H >So I%mnS#2y o ;{۩ j%$}K+xZ:מ,V#C/b<}ieA loa(׎H/y\~`m]v}j ^t;nh0h6tǦg]9o2x(-Ͷv9Sp ?ז<5sM^$C!92cWa(/ϵonA3']{'wg>@ܛA?] 9wrq` `+'Tvןõ1F/kK\g p g.+丵9[^~:H`RQFxc|MiýU"#Z!@?DyuF3 QgQ)衿0?C<84=˺{p Q;0!ta6 |^g3 $))Y*Mo_Jo7MJR (Z+8Y7nfu׉AZ$3K#j]':5T~}uhw,Q^g#(Ne g'Me6XD ߋ,{oDu6uJil,N5]^N/pbӺ Di;1ǯ?EX$7){oИ9 A>^ iay.[d+P~y]uk)%1tlvpHr3-Q~ r2|fU-!r O]%w+`e p~Ul5nJ@Fxf~`Vgw|b,>84O'O1tב)5xFg%1,swQnJ^b~ Tu,a2g.sk8q&>f౼@-bt7smnKřͼnKlpe27 lNDGI^`hZmdsjNuҺt,YP=E7 8&pUwstXN+O :NJ)u n3gr A~.:Y!(&Oj%>Y n'mNpƟL0fj"F؎EY|Չfd{L`F}TYvnjc\pG0Ȩ!_ @O0Ălf_\6D;ء! [{F`UA2eSM-ō)Jz†*GTz\y2cr׹yo>  Oή_9)"?3N/ Kl2SEflVA7@N}$`JCQW>(<ҕK3EKoyB]Sb nCGa 3~6VW~5zt f,եa]=g 1)WZ!G GGe-.隔<;qV3#T]:Eэ?k?{P}n2;n\O(]eqkl"P6/Iy`juP2vh >`ٳKPy;ӡΗBrjy2j r/͗|<7= |Et\{Y§ 4B;0DYw\o׾%-;/Β" f:\f"g瞙{ؠ\X^_1kW_IޥRk}>qNI~~cixxVV@W?6@TyZ%y d;(\u¾iYu#SY &v=ߙ;ފ{_@ÏF1P*7AnR.$okf졛7h5t؏]o}{W0@LטAծ=Y9 ofCkl"= 4Oi?n˩w=26/68ƫ_TNuxf175fnԀMUTSzK 9Ni԰~nF,h 1ߴr>yXUl<?t,k@Zy4:GVXCKע{#2Ľ?_s=+ >ЏN(򖽩LuNU您s#Kp(ڡ`x{z+SZ,o#ggOPy'g>?d$GZ~ =l.R*M҇ @GGT1uYu1: ؄!T&y恵 HZeD9X|—Z%P̛u_N%6 G̬imsi@vurjx_Kw$Gy~2-ęvv@]ױ5!'''z{7piՇ= Cg5BKzO[ H3Jd.k\=={3#g+W\^'{ysp8u6Ƴܙ~M#I^$1ayI9Vl ߈<ÏxP-D'< tZiY|s`2e7"Iw~'%>>>"P>4:Ṫ5&xM@>>a(pF3 `𐜜=po|]$D~{)!~ШC5<2 fY a܆3M43Q+zV GY= L u( *:K*o*:F6@=ZYT}a[~[B@S%U hsLO#cW)xT;ǵnJ'!s!L%&,tx/S"f7@wuvg`衷2Cp8nS\pS!0!`b6@ǗQDhN pfbkD ɖÙUk'yTngvp #ЂRb,ea1&X+kk +(6qAG_QR5:9w F4 QK}X-0b~oyqT jYe@J'  ONw'>uMneA+4jgg_2 (٨U\c]Kű8QpW݈UPlQ~9fppի+ao)}'T=/ZSp-zkPvtdS|cD.]`if4Aql75/_+= Fc(0,c_];G]3G ,aVqU7Y[b[вh-,ϙEy}1bV7HyYefQSi1C%ɕ G8,\ƩsU10~q0Oj <'Io.!7)#c\' 884p6T;Ȅ7̻Dr=zۀ̮v A,]g 'd WWkQ ;26tW n|M6k 3=h8} NP~fQ[&ࠂcoNg[ h&/YL)9x 9wX]C8Rlٙ)x }|_gEGQ0s/뤬yL*\YL[+* #s(|o3 x?2h}yCk+m- /1@ QԔE sS&2[cDG]miDv zmdӗי%gUS\#}Bnrvv~6am]F<(;׽e&{:Vj4/| /\.: {ȽyJ[1?އ2T |:7O AxL&`F}%fG)ղj{h3PFr_Խ ZXk:ϔHd.!gc绞7:/0rؗo~3RvM-=8De4~:GA.z;tm)\#.a葜1}]OHc4yHCJ U7s7k3|U3bl ? dLSA6@rGGU+Ge!m8ݟuofn` ru_f]y:i67>k]@,]Rf%; +xvy>qXn̽ekE_~fZ[?p<}Ҟ9*&_5_Xdmq,JpgKy+{n ODS:{s p gud Um: [҅cMgh!Nۀ~GdN-R]@IDAT} `=Cg2x9^b 1#\}$(-^eܻ˽)HSZolU4 ΗpC#{n6y涽>EE*3Ky>2ϿOB3 wZo( /=E>«nP 4e 4,^- bPK _7#N1u }dF?TpĝT!h=8m-mQ:ϭ`&cA oHccRQ=`+{~Lf>ng(FF y^d*ef?z@S>ײnOu!c._}$ ^f>뿑FF) V7pJ+i- - /ƾʅB97c;26(t@9_/Pdfkz/f@ ]Ǭ'A}Pp+7ΝA~2k&2sEWl$lae/Xw~`U dzTT@浱G%;Vhc+7غӮeQe0百~'0;5YP.xɎ-^~rz߳ڔmrv1~jgeh.4Ҁ5sU "ۗ FoHGiKSmn( Ak{j]UI5җH }EJc|O(C>xOi{KFSw'?iG0e {o@A>vD"{=S3؁+ Z}>_~rnQUڱµ3p]k/O9իx`|+`:Hs 1Pu੠ߋа'4cheDdw ߮/$?I^b* `\$^{R@n-G%q'GXe o͜:Hh .UFe gw{uF3 QG~-㧃Ũm[ 3wȩX$tl?CϮ;z(|* .F8ӳRVR}47Q=OɾǨm2zV~NHC R%BـCcU<T|~qj\9mԸ7-#I)D3-Gc=#ᰟ#ȌK/mmf6(׮©3 DJl+=8T#s˞JWñhgy=A薾pʚ 3F'i*qevtY"ǀ:eر Y:ȥU[Y920%JR8405k8HU,a}t{ f5J7 ׏uf_ l5Kk02ri  Gxd`{K|RN3|#,[(r&F2m|Y t?lf,Wb>3Y9D9[Bc̈tՌ_mf |h}Uc^z9u5䞥[Qľ~PvenλfgHgu:th42 t1 E 2 jOriñ @ )4-A:+qT(QX1SGSBޒ܉2a8-S>OFr5a= *7qv{G,6O{b -MLc-a]g7X8sj½]5Lpf} ]Gx s]ngr R)^jSW@vJDV^<+H9!l8 *X>նl4WCY'γc|Aǩ~sd#r;Zr|Lv:|Wսs?ˑn֞ra9˔'0ÑBABs\ [zxřqg- Qw|7oe_:Wt]}zA7րw[33iwmm-Γ-}d!(Z{lc \?%{"c_ !f1V{XrcG:%m|ؒFV.+-j4;5Ε$>PNB[xW|1POKܪ'xY~ v(}Bw[`< 5j=gU 񾤃a|+ˀ%Y:,I7a $X+~*{=["XW@p~Y\ݔ6{Eq V3w+Os?_s{OG*U2۩K 35DZ4 8-ӹi\;ΓJVgc,;5y7ƭ^h,;ekynP 0[M9*\ 0ެǟxoЬ >}~<m@`_O3tZ&2~;W^6e;x]B=|im.~h%剥G .lrmUg" 4+5=wO-×\ w->~V Y\6B1d")KQŊ<8[6: ls3ז|q "S {8Fycu&*{6w:iw;!4w-olJ3V/9&HL2 UN繇I?AtKt]#>Z'Kت EJ*KvZHQ!;*gA4V}NOLհl+3qut\`jj,QEKa{8ݼ*4^JhP, vwq.ii*SxL ,[ vclj^zkd ~Y5.\vOܲ/cn 0 #=ɾY_83*c@(ش #6Og} qDv-չoɞW@ѵuTU=H!!`ȾzwCd7~6cr 7Ndm^=肬z 6(TsՀ->Gw'v3ѾK@bЉ[>RtY!$BVsY&z-"Z1X #2!¿ޤPW!6 ^|ncx˳@oQ|Z h <{CYpE|~?SY{ZA>޷uhz౼HP3 h5G绶Rqͽ1ifP 7kkgӺnُ5*v>6řhpD<ɟ^$ʒd`<.(SVɂ$O:JT rx?\ãat:&y1 ]y u QL~52ܳYmr&K]孾;hH9Tb4SNBO1~'<߳q" j|pIGJ q?s u䀶t7 T\T+W5Zluh'Y[DGC GkjՂ>{;Ng8L &Y:L<=,gY+V j$3<Dp!tj8?& {JK'/GD#SM>֐PaV($[i&)}ʛ}?5aLNKnj0z;Ph{0  "pXrsQKz(yM9ӡp@ =! P5:3,93*8Z}+QzY,Ȍ+hm}lo‧9`^˔oiKK5>q~qzZ Ȥb~#(%7 .-@Sg7*V)0PԨ8T,@F'!fU\[2{M0 <ר|Z67LHjF)'y?J_-D CD+KFʲi2T}ZȞqh%o+x?!cYV%c>2)##Ӫ j!ht8%jZD8t`cz/L}x}؅iHMp>Yw+3hu*< h{It&A:3q+~%wVg^&FU qs}%.1&>Ƣ8di: QǷhc`:@ٯ_ЮR#k[(Ͼ+a3n#i kȚi{ n _ 4|lȖDl`϶t)[K2͒[F!ƠHT>`_5 js}YP}L x(=@ {oX`V'h -Zaq=zE-RB(M2+now3ݩ:n3wqx FRV~PNbbpGbzymsE-TtGNi9wt08>:i?,kok{BF:x#٠L ȴ gF.WO>pc>%p*0;vI i 2Ʈˏ ۸Ng:<+t"帋^ɿ"ZCF t#3yH'q\+ TRF_LJfF:}yFXsSt{:c3 WuΒ8l̵ۅ7*QCf:)~陜QΜ9֮T`9JV-tGA.cuDRo/rВ,"ezp|\<[+hłֽ,3:lѵt4Ş} Q@434-~f7h!,a`D}& nQ&9*k'>/=׬*Չ\;Cg\63\uP= [{^|5#=qNKS_mP:@h< 4a^gf(XG,{h6kn~&5kw MGxɝvrXbh{DۏVKٵC ,3@ @%e qs` ^*rRYe/!2+͐TYKikbBwwWR ?5ɚoݴ,lZݠ% <.H)R@z NZYd Lۃ cuY3V{:>[+-deAzySl>=9?8<{n\g86og,%ΏS@pb x('upMF?<{p:wڨ= <g>dtJ  /ˡݹ}5TA4 C霃ו!I[Vm2`q;mb܇{[>i9/̜ߩYf ƤmtDF~Snj sۊ>L}f529LebY 5etzgwiC {wDLrRܒS\oi{Ȳ%D&r N6[$EHKF!r8ŽpO vP<&{u+v $>U%P*os]Eqx&%ֹ=QDǀ?*N4h_cp)C.NY⩷Ђ~vVQp4pO[0n:.4#]G>>R6#OyF*֠sYӵ@~߇NkFp;C}<^1U'R ʖkTDשuk$5U p&qj2k -;R|L*GA x& -- +FŅiigNc3w (fxpZ*rBYKeC XOG7  +")PoJ^d 9,F |zOʮ-9sf!uޭh w@3yDȏN@"F8ꩬi6cOhp;n"?߻Y!(#}akJvgs)q=\[!̟ޯzzq($w+drxEoyÜ @*@0XQP1@]s"=b\FP#'Mf1|,ãvUZ zDdJO&<xs*(|>XU# j3HxO58ø BsZb>~b0w߫!{ma=(azsNU0}–)C̫('QxQaN!h=cGR6ÿΥ#p#s>cB<:J=]{Eԃ#ooܕKwQ5(#`QOm2 F ha}TϝFNއ~7DHu}wd :/0(aH:2NdsxdjDr׊ c\A7EdC ݗFMio< h 2#x_Վ=(1 ,7y ^CɢKit :WGH#G/oxF\+c|܄nzw*\r7u@ѱ 8=+Xm;Vo^zrwE5̲ƍz7q@t=? 8aC;RgdP^CJ\"$Oz' :?MTc>(*}M :Y_qo#]]'*'7*mV*x-|{;;>/XO_|Le^|`F#|[1>OcIq"F|򎞜J12 {Dҡf5F>m;T\۷UfF6a'9? Om+h𽷳k_V#/ϝxuWY{w;ƪm_/BX pu]P^#mmgNchw=g_4>&h18=wb3z;W!O<{*٫Qo³|25*/ݾq3[\5-]n\>bx׵i2*'Y5 f~0jNӷzc#Kg5Mi:;78CPIً;>3䖑0sj"Ћyӏ>@T&Y@@ ƾKL+3j X͡8igu-FP^~㘻>#­׳an:u2'"2 }> 1gxl3lSD cr-gnaI KW;3' w;⠁2D8וWXd~,rr2{3u0JIB"=0pc=;g(Q/@qFg %@b;xK(T:0|YP~Aߥn;U `8i*ʗ/"C0{YhᚢKu]vl T=̤!A]P.:P7ܥ0yX)eib_kD ^e܏RFYjȼ V5e:w`e9w/.q_Y'ǟSUxD`>9OG|8&?>'0P~K$/tR (( PpqwJl:7Bѳ\%7i +} 0G!@Z-GTNB58 |'(eܘ* ^x b0AυQ7y XI\:lb|.)Dt"qP4tyw֢II@3{t7=tsc!}s-dhC^ZxB1-,!R6 ū0ukd(QIm 6A߮0HSţ4SO$ PCĚui6lQpP UХxPլZFGI޷n!65I.a1iG oo$;&=, Nzo5)MmA0Q4l-֏gJIӧ_TN}G: `o4f.Tmy^qݥ^zE:W**X<5#z{~Py@#[ZzN9bLO޺%塺k@{}$O7;5sn 4(Wyj;5({uy|e<Lo#x@yS \s1 T>HkrwSgnjQHtxo|TTG}GG9'hݦ͌Ii5 `^4}N(}P87TK<\pqpDZVҵEzCtdƆaݤp2lCCiɈfG幋NEv[U<PzHu-p1^`]UAZsQb*YS=o AIǑF&w#CyA~t? 2y)GAee6=f @W DҍTar'6]Ob: k$ M#nZh8C(dn~+>T޹0,။):? 7'M&]MdFhV:r f][4ǹIKZJ 000t:xi8(m_Wx+HEp2LIrr @0:`ax d_拁JJ% "ӹGtxR=]?z۶ʶ޽[VKx= ]95 @*< 5V zI& O;b:ǽd {W4.92%6`y<k*j%PkH 4J,!׀{!o|D3Skm/?h$+_ [`}+z&lwShuǦfU=N[ys@/1*}隹^IyELX_c5hcAM K쟥'sf?֥ LxW @?:B8/9cXWDžgǖW ~s^_g ,iDݦ;BQpXFW45#"W}3M"?Vi1%7D/'mCyM} Fhֹq>-B^8j}5}wLhj|$(Kyzo݈}\vFq((PPS@"J zaxm~@nc:(oU`2ش޼Ym,=y*Y!,={U; ;7/pC*6XT[CS$S0N!DbqQ7vP\ Oe*i6 + ~N~)0 ۫^El7Pl { qS`Ɠg/gɭ'PAH$!Z`Pu(qi@ :LM!!U DT9{ ! /;+x)׋P%?Df'!6*lVD9u!O]|3MP,vFf&ʆ"0 J:y ~`-ݦwc%]OLs7#ٖ"' Ãz@NfŬU:ۮӭ^x *A-n-xjKh!fw_'&%aBpRlCنя09H '@'|vT(i#\| T:d?yOzfl\gz+,ש2 e:i-l.͇fer~n`^SsSY u"wkVv՟F\TԘxqqJi˄a{-[I(VYEi>=th7ѬaG 9]cڣ!LE`(Tv_Ht(WBc#v lﱞ |<сII)VmrQzoCWQZ>Dc. =«`ޜUyEndx @{'үT~9ŻR{nzL%VOѩ?c=^=t_l냯xtXFōeƜiCp~>yX?TeQ?Exrxt}`?(#V0~aĹG*b9EM*٨[:2ICж!@C|{u9z&Az FD*`}^?^XקǑWexΠ:szqqE*x<O[tEo9Pvz SO|;b`\ESs:BR{S6hz:c妡!MX{i.Ea r}3u9} {Qkz*7ͳ}=f6=rZWS-B/(p2LaD(p# #%9U=&x=#ӱz07 !a7{ͤwD4D @4mSDz3*Pgi^`u ;?CMZ$=W1bL2ƜJ>f4~lwbl4ǦMD?ӿoNуm*7~9v#}Y# aǖϲ p7Fms-`^cu?}ucoGkD];Eh{\~UƓ|D_zyg?pnHGZ.F!}491U?u?UAsy\O:@#>JŚ}DJS@IDATqR9C9ތ6X9j |ܧN~WS<C3;;Љ&AtDPFx'VBRt횮q뢆(;y藷~'h+~yIi4x&[4Fxbh$BJa $0$wASG@rRN0 hm 3`{00޾]w%?1?}0ƄmS wҿAh8e,<温AwH 'caU ##h3ftdo5q[mid^^#th? ~=*-Ap9z]7OL81cp a~u4-Iq'L5.>K i-)Z m>kAI-OMc-S^^{Ngw'"tF>XB (w nV6% 3H /׸Rppu1L9lD1ͮwcDfѶ|?c~R񌆕0BDqVi  OvV*k)B}j0 )׹TW:8Ƨ|3>c]wzg} !h]pZQ71`ֽk'm f RsK+#9IWu.eˬς66X((PP@AP;??~G^D7\BJlhY$@pW޵Kx.&f,Ǽ8UPY(@fYƗCYBu]dG+^Cd2ܽF}o{ZBK!"ʑI6I( z)9׹szV:7^ PٜM*[T\vc+XV;5Zz]!1C+65]n}-S`& ^W̰[cxm#`X$Dz_ФkvxotC X!S᭏˾_D|7  aJ#,@vhr0C*mB)*SoLe;ݿ ѓ–:FoGB<$N7f pjP,{Cc{s-(pOA?No)v 9GϾ$=(FQ;χW)w]*\'=ƅu˂9|;r6/s8T uO:G_'*##/1j0x =å'9xna9 Jq1nx :{M޳\x?=ȕ&> oґd gAo֟9ϜcA(n vpc} `M&cu0DRc>sr=J\| G~煭-z?;:}Q';}~2_~Y\(ک,i* s~_1"U޽=SRuF:ڂ5hGY@SᏇt#zʛ+?1-;_{>3{lIG͸s< Uka}8{?6Ócx~y7?bd}mIi!?Cql&# D04r-"78c:?ru`14>ǐg]=ek8,U=L W/ZGܔS?+.򋳷DCטtE.2$=kU@h7k"_PhxV[%~Gmjq,ϸ?H `ݤ'Z sC;x8ﻗwaB/P/iDEjg/2癃\+o8r>R?f#\;\s`,z|nF9& wݺq-{9gIc_ ^ y`0 :W\d0WRWj[do3o`aAR*=©BdB_Ϯ~}`$=[s (࿉|!5 ѻFr9 "?3z;ng{c-ܷs`} >A61f>d#Cو}$^ҧNbLEc i`4J ( G#+%['"@YTrn I7E3|7`cDv7{v?)idPU=XSf6 c6%wAO[sst ނĞˁ?Sqy/Տ:7ެҖ| 0.O f^Xcu [ȞSCn³PZǷ =ig1w۽|nHuGy؍ס͂ FBe3,c6 C̐ni=?6q-xt}4\c[cA )m /qe: !(ZZQ+$瀆CI@C`zUsów`|`8x'Mh Mm /v<{ aA{3GS)vCGD ؾBV BCK+뀹_8pZ2tY7IH}?/vCoxަ?\W, bG_^~Cw25BWDQ̩Ƈ- B\&2Qvϥ.F#Gmiwb46uܑu<=~sNE1O x?3X]ybDsY.lzoMM#(&_<QPR@Si;op\{U^n td{;@{A!ʹ)k>>g!p<3=w>IA*hYPi y*s|FAwj *Bͱg$R0%)*>} ~7H9y( q($toQư4T8,dŻ1!)ӆ0RwZzLyxlalfD 1wJ~Cژx]A\{~M!u CݥXU= P_{kQG_v Ig/ ְx$/|k.@:R˧)ِ!B/3mڵŻ OPD5UIO=#tyI e#ûWɋGF= [)o.!^(pN `L$o Uhw+?2Kzh - 'zYfLMG<<+G^3׏<wsۋ)ȿI9ϽFOU?yky%9zj>N5N{t~>Cmy=qg{yOs7U|˵Cن27xTK 7 >TD!X(p 2*nzNlc$9$؃Kп+ wmYZ0p=JW q/}{?d~{otqE79ayכ'x.^i{ɩ&/če74Y7yq-~)aIEL:;{ ,p{e-x{Nr~{}GKHeRg_ˍrz*SO?l~vQV*5/qcikɻ>SZ5KGe`ž Z5B0wzV??6&}z;5tZ#/WDo &ݼ{)Fj( wt>;3gGѶ9 a~xsA9>{NShK1y_{(eDEw-? 6)2D9xo^itHuW::mqIT#Hj KR 'Fh?~b,iц89NnD6>r "ia$ Á/X z/I|+O4<1iC)t?@tY?Cs`3}y2hLm}>Fl%k%G0֙S!/d64~qL ?:ķvX.nSDJ^S{b(;7DČjvTqoTA1ݦiSkk/e[Ռ@Ew )64pgPYzn!Ouf`9$waÈ5pr4Xm<`]ClU|~JtȽW,зlg:2 F 73D0`Mbb#1f]4#aDϽ5ua/͉c8FFMn1Z:4C|֐ Vzk Acͱ?uLISK8Ffhl TV)k:bhPe5~?|BW i-Ycg$ C+SFЀ_l c**ϔ'j otwփϬ GwO; f6iACm 4Ȅk^Xu푞D@]l~5e#O8hsLtof2vb>2 S^mM'|B|d$sv 80Q)h*l:cs{:"pcRĎc EF!T]+<d¶w [ahU6>r|֤Sha$ S ڏ(׋]-+(PPE&ֲ|m6/=H %g y6Y \1A_)a}#YTiwܴ+-@@J$-OI BK A94sST9T7fêpU0 ss=+dhJ7*AŽl#/-v<) Oޟ+A|f0*,л J$]3=CC`Q sof*F gr/n: 4-C9ij 6I! &") ww>l~O{9_NQw9H|Ee|ܪ2x[=X]= b^f)c{b?xk4Fxx6Gjջ[o Vb~zZZ0ƈ &KAWފ]º:wbH2Z~%럑F#Dw!%ߺuz/\^ק^C~}D 0@ 4Cۧ9_h[ܷ^>Uw]Ǖ-i|&<T=m# Wο~{礹d!Hu4 t;lPjzQ:ܓמO=s@py @_{7<\ީ.=wCwia>4ʴmMƍ!)*FB0|GzDo0g!h/{*ɕqQ?޾O{rA̯,~3"b(gBsG8>sшn3"F_`;bPc< \λM)ؿOh}+%Y=$ %/crܹ?Nxj|16q5lm\ޚk'qV JRc.B7"2:]i#AXFxHI6?&cJ6. MJ- JܿTupfT;~E }:tiO1?xdowFP'drydQ m  O } ΢ooau1%+#DU9,#;<bQ7T>/ AvT^9L{ @}y_~ eVp~|崝6DROWo!t1qhVzw{1%jܢk +u>4EoX`CͬL w;e=l`\H\96DZG^R ݹq-[XɦSgGؗ;;%u)L8 ((Odn*\hTݹYlJ_z=6B{0툰6ȃ=5`8l=ޭK|nyyj_P"A:T@Pcagc@CaԿgO|uBJhfuoZ9=쓠]+5M"My(G~^I~\g{s*psvz_|Oޖ*_˿_G!*iԹlxPl^e1GXhzv:|x{voڏږy:x)מVvOi4_o|>?sy,Mľۏ{-_]Ժ#kܧ~ύEX8JGe=czRlS"{}ʛ?Wܥטd㔙&"p"c#1ˆBQکD1wBiM'PWs&_ ێ8-e"_ ?,y|Ηc]ݗ3zя;ݸYvcӛF?"4D7|mSY\^ƫ^ %Z F+zW< }t~u<;[ / J64~bb$fz|@ijւFF)+\8j :iVYL]ʨzE8o͆ݞ S󎷈޷ m p}xN kF&tȉ6^僀KcE1 WQ9ӫDڨGJcC᩾J/ \;3\x r =DیP#m.t$ziQ bPz;zЭE>uHCc\[z @aa;uR׫`!=%7 VJ7-% 6@ap .ڄsԏE/dt.7v7h`"ܽ ;Xg#p!}>h15q?m otF 5(S݌1#izxuBT1țQϥ1Ģqv9f c=𭻀;Ms4`hcH[ӯhd1o=;U0! A,J*NBD[?#i4Fʎs"w-]QP@An V*@M +2C!N[X&V@rlo]ś'Q@A.}#/eyrG\ABӿ*(^_=G^v׺wpq2z?wo~ιl}wVB>>+L-zʏd@`ˏAދݏUOrV6@©ʕzӧg,pQejaw6?rP>'N{=xsNà7 6f¾:k*EPEMXSH\RA{x[0T7Dtz7\CbCk/'WTP@AR *U :&:; [K4eMgK`ZEA i`54>zԇ{j/D2 Ci g j(7)L@s{XG]nZ-)Z/ ze?//{Woً//BCl}>FarӞn|Ry]%C4ٸ5I 5g/SwDcTOs`D0:r3g9 jҺg4>+2|o]BsLNw0a -j l;9O2pyja~gMeimu-V7W?Rv"u~ /K }T fҷY]Pyb"eRӐ])LA2x &upĶx7kҺ>D9@=e[޺#r,Ϻ.n ܏sM{Y0֨}Ye6V%-hm)=7wG=pN>C[,BX7A*7^7ycFz<@uc={cCpƖC% /g@H^XN.x}Xύ;:s`c뛄׫ @ ZR;Xɶ\/z(B[B&z[Vø]s]E{Cc` 8A6Ԕ!uAEH`|T<54p@ :Z_| y'/8v[vrMIA Ox٨wt+؍ ^tl<hvF݆nz;4&WJ+BOGZh[X?ƛݛu@>O b: 2#4u=w.! M *DQM]4>0BF {j#M@A <+\)n. )JF |Ճ7By.i2䭞RY_]WP{?7ni=bCzXar|qNet;_@O||Ƨ<.*=`,NhϤ>@=3Ba|PH~y٧dr :?//,&R=ўz+7#)|B'}̋O}eq@A*.#9ϨD=Y8|\֛-JQ P\#vk!@͆0&Kr:Ң,C];7;Ox3Uw545 Q 5:鑾I*!r[5?8V @aC m?lS?n>o(]uicT5{ls\׀[&F< LNqbuH/<ہ>ȯ[׈04s# O:0<(kԧm@b wa R-b@8]$h˔Gao ] , 7 8x82z} Dz3'bߍW;uAspXJ\ۣ;MRgYe>4t{o.n}szֈ]_k}̴Q#)jh~6}LHK.<[xe@A[n,&,/0dM8a"?5BWȿ((PP(g"N&iuyz2gyǯI*ݟܓ<5Z<^P@AAQ% HIhTLZ#*^=搬g7>|(UU`j,"m>E2v'g/~yF Kȡw`ً|QVA_O~>;cΈ/9_wi'.pyҹ2U:\ƉLR7!D s<>܍uܥcMy^6{duk-eo(%RJT-,.aEojA6^6( a &% d?7 36 {?y\G=di%Oev޷_x w£:YXpg{ Tp0RP`7L#Cs0/.z^pAW_c7a>te`@!}F?ݜ΂-0[*Ui%Û`ӡM>~ӔDPYѽD^l FEo*amjwNNy%@lڭ@~h]m ({!<덬l7ӎC O;X9/mt44D3N2T,׶2ۀ-CS#K ꏁby../[pC,3IARhg8.s,G~h'?9pQɭ>7AJ7+%(-?MEu On%.}M12[G@^" # i R^qEXǤ9ch}kohQ#4YLk((PP@A ((PP?^TqrT> t5wrѿ&gPføYK9EA}̆xrvcJ`#{W1^ xLʌzTU~=_|q_Ƨx7󚩜IJ'GG^kѷY#9k0;w&;26Vs/z#]I6>1wt$/x5[$A42LuAnTAn=#x@VD>ߦ/,)\1F= m.@/e>e WOWhS@EcV!}-7?}gPxъ-Y}Q_ئxLyzHEz|wwBsG@N+! pQ'f*X3wsaģ­SSr kuAQ 6yw+^7BD^Qu;FxƈZFY8nF 7z_ JQA{vv]ݓƾ^.1_#y{!~6Ysnmc-XH'.?/&ZU AkpacY /q5|061X_~wo/øc{=}5ih F(a=HEQ u*H!]r)!5X?,kt?Fz=֍a6xo#AJ~֝ԣ^CgBpywh hhO-RF`Kk-ܿ^An4Fsƃ-YN_}Ѓ+_ ((|pesiomx^{`"6]B{qG*i*l\WcA 4Y̺, ((PP@A |LRu " ; @rlЩ NL׆h ϲU?ٿOhl~~>*: y1@QLAPE識楟vNʁ˗c^ W^m6[̇ˀ!Vw7fF㩩t#{Cϯ~ݸ9e.0}a4y_i Oe@JoK{#ЬM.u@ Ŗ׍.lpyK{xo B yπ}x`M/xfs^FplVhYR7AHzMf(՛[ gm;ynڮ7o|0^rL |>va` 69gw tRnӞ}g7ӫ@ @0hO `[x \kڤNS0Sz;s[o(z=-rY眬  SuhF]C;ó;C1re7؏bKz[\3©wxUI 2cJVa'oonqN:-TE@ ;s{y i$Op 0GqJž$z*NXo/s^5-# 3/}B< ynixL-P,$6-._#{!@-uC'6p(D)`Zu7dLjV .[ztN8Je?b9TŰ0 aЫ8-ck>|x] jiALs(#z})Z{{o5WvUN@{=hj6Jl0zgys%ܐzK+$8]foWMSZ h锣\vNVaHp֓wm ;oٶ t=]5zUxy\Ms)-dY1 &s穓 ԦE O_'D){'RtXp|)A@?tl?s\^͜t!Wo܈1'n/G7A BbwOc}ɓ{_[;\35  3&tX>;%l91٤.1n,>|@ƪ~KY9Q wӠ'Jq((PP@AVTBeh-&N]mS Ys`*Xf%@A ((PP@AM  2Ի0*2Q!o=ˑ5= d^m.VJxֶtײ;on /z ]_CNGA <'\̻l܌Ŝ.nL6⺶\:Lql劒oZy((PP@A ((PP ( Uw%=.6/P1-0|G>9K}Jt()oܸɋ.g#]Y\ijq @5oq, bxDHޗ܂X0 !uK[lrf[> %X\N 4lN6=s@|ϵs--qZke'OWgwg+L*֬zmLo l Ry{ i>QGnsnTLXpXiT ߼8VYϥE`{Nϔa(#fY @.\mmX W+(kD2[7;Dizװ}z9j`F6B~gQ{:";5TxX\v`;"N7^ˀ^c` iN ^~gzvЫGn5G&J[|~TNzrOp|E^;&#A}nu M]<Kqq4.kmh}@}hPЗ+zXwz[k6Ə~FG1# /^#L>x ~rX/kTWjZ Q}@mW}ACs}O2y.95@HeOg)C`hC @"{^-ڨW;%mʋcbT~ :(wS=w7(4@.z[7߹ՂgĄ xYÈug ~D{㤅9 )#` b4Bf iÏU1W|^[!:ߔ .CЌO_RAǺ2x1X[v3j֖ Mp@Okl|2vR?DިIENDB`colmap-4.0.4/doc/images/incremental-sfm.png000066400000000000000000004031261517363634500206210ustar00rootroot00000000000000PNG  IHDRQiCCPICC Profile(c``RH,(a``+) rwRR` ɠ\\0|/” RR N.(*a``K @ [$)^bdo!`5 g ͗f3K@l 蘒򽆡&~ JR+J@s~AeQfzF#0Rþ9ɥEePc JJNЪ pHYsgR@IDATx] `E&!! n,x-("  ,(" "9}{3=$W0WU     y!r^%     ( p$%'  p#0,.d 'NA@@/Ԇ!&ݻwcɦ`9h׮Lrϱ|տ.X ?g%Wi:kڂV.  6WZVe2u" Ɋ    @E@[wsA@A@A@A@>|\_`v؁;w^\Ouvx~#@=E9/`=m۷mi/h'A@{ũ/QdZ$S@رcXdMD[mrrq pы## A@A@A@AСC-i5E@^X%uA@A@A@A@K!.b    \XhKꂀ    B]")A@A@A@A@vaA@A@A@A@.hD*R!    pa/     \"vTCA@A@A@A"`K8s6oގ|=QO4j ]1QNF$*(meI5%)8|, %DMZ"j\>jԙ! WWIݺy",4 A~5Ʌ^ ,%8r8թ GIVAS4/ q <@fQ4;LJ,A|(IށyLŮZbw|cxnxQ>}qDbŷ3bu:q,x(Q:@_WPrӸ5:ۋ9.kDrw)#p71 =1K{˖ CD9e(g7xbڦjՙ]??tMd4YѠI8|*%F$*j/al5r"K8M΃[ C-]g-F-P4Gֿ?C&NBs\/x99 oE`wVOM!m>^/z ܴ;:6HUUg?q]X; ADPΑ/w\\~BNj]T+>"XKhڟ[Im\5&[uRY4yi)h&%G_LVE,n"!Tl VѳjO>2tHHHwd+6EAvRN3!"Ce_^K%KqŐjbUՍAـY "V7X bd z4vlAx$ V9(I˱N׻YdHV |Vz#dI>׳Gƿi!C`v&t6tu=\.wzSQA@|PB@OAVObjdB!̿3sV^ڏ"74ʡ=ӥƠ.q"5έ iav' p^Pw'=B>~yE4<.dO?H#567>ԧsg'%!%W4CoS_D"(#XF~ٙW?*"[i.3ΰ:j\.C ˏ 2pS9Rpd:߰ Fa>-$⠷ M_gmzֆ4 C?/#[wqh6]#'fuF7&L]3+II\n0o F|diE(b^0A<9s^[\.'CN&azu"EYy!|ug4,?Ǐlgrᷴ5ySrsklK0 W,Ɓ0j +/eCng*w`r^0oOQ."ar8y49Gl}ߔ)Yy;mFeʮ:FQ+reֻ;v}^ZHC50Zrj\|G?}jjѯl9G3@9m^1bS.ʉ@;>7:՟Gax~CkdY pӽA9G`OQiLzݣ2zi=HTѫp#ޘrw+\7ݎ t!#)  f5͚{h #ѳ  nOz3zi*Gu=;dV#_-=ob쿏A8n10QsOP:8_ԕ3CӠQM}%'%w~9mD`du)usz}3qÌ/1Z\w=޳⺮nywQ4FߑԓX2|جY1uxj7b[}D: w4[iz?>㵍/)0ZWq}]źҡtr!\x@VG>c010' dDZw[} D.3rq{74iJ3wA -ZTI>0IIgЇ##_}z9&lNegjF ,$ '+'Bi&FrZig裃&Gf>Ȼ*!ӌ<gRLݛc k V=L@bيMN}!h@  qν)n5v}xdhDO-X>=1M6zXL*VxphR 9"4iBsA[ Ńu^P(rLH4cV Q5b䒑s\;-{uo_+m{՜%v"h}5c"ZycC pFyv*ϵ[|#asi̐v֋^FL[|K9D[r"@I1I"ʻ|ޘ:sk OV|{ /}uy 0l^~hcJMvo #B".r+l.k=Inm9F̃WDDP}qىF9C=;j.G;isKO7ByqmgL/eLq' >& |E>iZ<"520;X#HLۿyCNP/1rW/ʺC]W]ҸgXbӯẛpeybwᖑuU ^6KN.IJvI鲤ȞYۺu)}9.i1ee̥,?YʽG4Op+]{U,XWCóC{8ll`6^_[]0!Yw6<7 Hmu6l[_OʷgA]DyI=~1k$*ͨгK,ڨs"<_ 1iƴ34GqIl$}'-^]Z31_7 izzo4(Zumuqh&秪zKl)@$+[}_B(m Uf8Pf"%c2,]#H.Y`Lj+6Q>{nf"R(\,!>XlIpvO -ol^ĸQ)Iz+(}qF, 6M흯پmAK+NqYH14UfS£!ȕqxsʺw DL@R1-hk,{ ﲥ Sҡi~j'`C2gfa'cLJ^FN;Teg} oz;Kq3EQT`saRUG(֤N|j'D#(H v=?8AW3[0e*éų%ک^ө#*:v+۷C+V8͵&x0qV鏸Ȯ`nP1cx[܎ :yzՏ{Fjo,n|9o ɩ𻩾AES]tUuAA61|ůuLT#>4C >e'<u!e,&,=B٪jߚxaY-7 HIQ]m\11ja1EFY|kB׾i> 1>#a[x#CwRJwcc34@֏#GC0nNۃPfc^EoI?F.O$ǃ}v~6+2(=x8Z4lȆЗW<;ꯊdK.x|J6:+7plee"RKqN+2f뗟>1A=az3V(2後'M j3Ge،uS${D]=[hO/DoSl,G[׶T;Ρt2Z_˨70= }7o^wN#O;j{xO!=l|utum0pj{EWɦtыH^#p>{HPΘNΣQ's.NV*T@m#SFF$+i{x9ʄMp(d2ws0\Sډ'FL@Y#qc>ێ ‡Ȏ9eAqDy/rͦCOaM* I<Ŀ!;^,[o Hi4v۪-z3wk$+nW{F=}Q1fH6 䓏! Z1C#w?>tg? ?Xݭ?=22}FHvsLbB51}|H6αGp{zC\+ 6d}mhY/z8<1F.Yր> {1 |@cQEH.Syxp#ל'ٯ5_f^OЈxc!ErI?r銇 p)"|dgg;RaͼV Ch^1H67w .X/ĐDKY_k_SNE܏`‰?~}9∔d#Ky$;c ,Xïun[ҡSM4o V.]%wi.6 {nͯ1u\SR$߫)X8! h颏OҤ}GMkZjw=9t.>;&cW!DxfgKJK9yE ľf61{}mtco-o eZ v'FGn!X-hn5=qb w=^ zj`Z dϙMfcwr>8v(Iп#: b?ީ_ w9-I1_d<Xkx)bA OCFG]`='WF^јgQg=ﺖZ1Jı}:yLk[r,(JX]RD ZIvFhZIrn)OۛɎhtyfΔV4AMNi)AYU7܅+췉f\o 4aiڬ$'X,﫿Qܼk0Vw&t$yy9iwM tl=2tڎ[Qq3/{7vEF45v?mwx0g|-@ Z$;ɧӰw <:W2R1z5?YmW-UVYhiA=8>-^ڄ'8΋ɿÀ>mSFWuLQӣ9 --T|OMnH~ Fz=+,4%pD_a\8îU IS_fIz~Q]ϰr ϰwD={;Lyg:,(qĹ_s\An9gRq:9ZYUֽOJ@ IXdX>[.[K{+B5}++A5?nUnheU,CYRZ4e"#H6 t=ґH6++u=;cӇ&,8ݯiROBIJ2Mv &q"ڐBy_G]fh]ɂI-MtO*vI^><ѴmKфE!GZd"Ho^4׎hAO7mBDq3}Q|qCSpݶ[~@[A;oCc|nImI9`|ͪ=K}@甦:tS˄jw?kd?M ӮjəzUVPP6izY͂ZּL*9 G̵ CHw=~h[?'eX 7G10F;O}EG/w֔1)ozSrXz5u2SA@8O\?ˈڲ?]5yډ|U=є0Me Y3_nEJ1`݆cd˝r,W[鴭et:1=:q 6_n=>^s3%[ykiaHzӴA|.6MH:3f/D n޶;&^{4w> 7nڵҼrdwf-R4s]W. \c;64wނwQ^A !b&ueY`AB Ne#22px7ؓnT$%bJ-I1vQ}B 09ߘ9Z5FeC`\}Um{j8v&R]<2}o0eÏ7AYfvw̕+VPŧFAS:ۈWC󥱊8A@rY%@Coyy妛}* 0 "v*4:>BegZl>S^@(h"};j?[XnРQcᙃs!|Lic:&Y-L\K)YFdT(q<_|MVw6cöpe`W| qOjjT*I&ueM$@E 'fNۺbZI rNbױv7,[~gD]#U^t"Îv",vl dnum4xO9g.B=c0rHgr)7TՍzPtۏ6q샦&NGT֦ML=ϵ(f|*Ӥΰ$zWPk5UQ:9vvYb,(B̬Ip?슬?~>:M&<9wZdQQCxNh {c:GՇ-[9iF dO؈F09K6) BU.iN−|185VhmbH:,Ņ͇LxMNʆk@K؆~cP`7+Ym2*{޳4vl{ed:sˢM3(o6JK6uH7B"Fnxk쁃$k§Ys!4l>:P[9k`4nԊ~4J>[iV17&+gIi抳38*tt]5Uΰ$i.9h!!E:yvıcp($2ڄ(*$^A#56ÒοcwƟFGFp b.mRd2ux4J|j ޺IJ&YLZ?]I*9VowD/@QҔH)ܶ' Ih-{cWI[4-v(@~ ]S8q.5=-.dv# r^wc;<3 g-B lZUE|Z,m;܈i$U (f^[+:\s-tE[/i0 dOi,y!=Z F/)2ݩ w6wzomeiITVM]i*io 6dM(,͛5/:]Q3ny<ΐ8 ξw&~ bh2ߺnci=j\_M>c 9GpLMru.x:$x\=}cH71}tH{JFuv,KqoDx Ń@$OT;c&ߴ"->]=6COÞWcKRXmY[G;BR5aq}#.oZQSM\݌/n8E;?5MV{a*{a$Gߜr]wPbz&N<9ϰf@wϰ9&m%n؉Š +\ L̈́-b? gl%"̟6g1mFKLtaX]?{UPn-{Wa[L[f*?14eu ݫ&<@rFwbx؏_g1LpK24F~Pnw!x}Qgл|mkH8MNz>|;z6d?Eo9)h!-5>HE1$ۿ߭^RHnl:6WWa\.wENp2W=z'*M:òkPwt\3DAsǝw@r25ln 4&hK1zgXf2jT /n30m:d0k |򫦿@^G]"ґB =k/?|(rKJgNA|.C&onhn^"֫Ǚ}k0nzȯM ]&2j73@BT.[=U#mjHp6zXt_vTN-Ov.=s;LWD$N|ܴ^o}3 y|Gx֍';O33םqM&e&Hdlk%s?oAsbEkZ_m`)?z>ق3VA^v|/ZWusC{C΍=MR?| mYb 5*5]@TrۗN{#M(HWoV${t/A OC?=DUp$ݻTq.=ިKW QGF H6|OH˫ xPv*eotd ףK;c5Uں=k1F/~֚kxq8c1~X#꫿zz1Lz }}э>)GFr_MqڋcM{,KA2E:܀ݛf[w}l(+1Çǰ fߢҧcƍظqEK{z&ovlY]v wF{M0fHx*Z4lz6S3jZ޸FM О:mb66s,ɝ9T$Wq K>2]o-yi +ٚVDcۇ ȣcXhعg7[b$b{G{Tzu7AhA7P|95~vM x~Bwd;Uď{CdX|RaR1Ye0lhID,=wF3DSS35%KrK\WQzmuMCŸ4l ֞>7־тy$VYƶqyxë|*{jXiX`ĤJѣv|¿#-ŢRӺhj4m|<9j)Bj4s5e?j#~?cڪ۩|݇?ϚD_zv) 3>nIʑ߼8F^AG\G+(\njZSA@pmGgo$3䗱,4Y)-]j\gŹi'gKo+7`#[I%FI}[-mLV}|83g`a8߳L; UDzLϜIS|xE=<^-8X1m Ϗˏ}v w^AV1y |2V1ZoD830lmj PdG!H}z Q +?#O{wi=0tnG9Fz_.MV܆_{v^MRy{1NWPu|+P915V^sRϗQueI py"` PYIZ=qC}嵄!<#̝<* .MdU TFL3QSP~_W}(8 @pXhsl Zlirۻ֎Iń4kMFzZ(M׿;[]N=U_ǼηswW 3Mg ;H>cS2YoBO~-дF6b٧2/M)c7ѫaIe#"#tТtuLzmA$#w)LG/R4m!S~nE@DyueNQ1Shk%ae&قge{jG[Ljt\9 dƣ"eao TW z<"L6sMQq]9  7ZBsƑl(~jՇ:^,z[a&:t7VQS|~"/ĔϨht6e1g\;>3aH0z 5,()YDrȠI-]]G6QWj kIYY|sFؖ0lލ&_+=p\u6ʗX֡X>h7.B1bjtɈ5lM9ClL hb%)>F7I5Krn>2rFݻw 7On1~;Xm"шUSIP~xoS"z#^۶ŒLSd!7 -U_eދ;cS1d$+V9+m43MǶ "Opt6#дU~(_uQ} ê~zlڵÔ)S單nϏViԪ‚L'"=!E(qSy83qP(^q1A: mNe658X|O7?F44iZ83)= (&Ѳ1Х,ϤӾ~-Kh,V DFU *ПP3q"V+›qN\V*#8AeZ j.r$!1U#;|Bl8)_5/.ӓ(Omn7!MVT/<:7|?SaЫ>>I?1]_3-|Zo6U(A|@>zg!%ׂ   EeC5u??uhߡ]VϱB F yp+A @49i~Z\J.9A@A@A@t. /ax}owZ%:vUO&IP87x̘[qs%A@A@A@A\VDCqPA@A@A@A@AFHZA@A@A@A@ABhA@A@A@A@jm5KA@A@A@A@ BXSA@A@A@A@B /A@A@A@A@*DVvMLLė_~YaY=DDDTOXA@A@A@A@.:jh;y$-Zt>eSNBr"55Y(-+eN w7U'/%%%زWqeV*'NQezvF _l;Eh׶n+|/D@A@(2ZPRRJ@^^.`bI?7V6X^T (\YV檄, Dba@Ie]GK)X<}(Oʋd4u*Q#ysl'.΋rN QaЕɲNyghr nX={CQqQ-h PY%J/ڵ.ݵG>Bҩ52@z!%%Z)%"v)%+q QK_vߊUbCwV!A@ArC@1D z @mnNH rZaQY1(28R&ڈ0SDE8.Lщ+,,o!9*ť,X4PRıX ua26"HŒ$ >1*θlSL:g} 5E ic+ !~t OO*q1oꛍ?o?g˂, X .vhrVDw0A@`0Q~:9 Ϗ*d'̖l$(|댺yx! gWDVw;*qJI .^![GD@A@` *~ gLX\FɸO˕*ƀ0DN=৸% bͥD)IE?.@cc5M 5PvKtfJ(>3itTDnYFrjzf(N)G'(-Ҷ59ÂMMYqة(p|)Psdcۍ()Hs }q5иI}:*:F98AԪl"l:zijuQ_SJ#Oe+svl޲3H0;W+^UKC n>hH rսኯpm+(fI7 R"c]@q?t,1friˋ?Ĝ 54*`2D$馑ItDjz'tL:R 21G?ʌ'ڭ͘l+A91!cKJQ$M?Mw")?Eq#-l?&4Y=.fܔ9Y1fґ.tBscaĝ?&[ΟlFuQa A@hg%ߣ|=زmq[~e[sVvqXr#\0ծ;o<-_\ WeU<5.yZ;$ka"K[{L#"-kL©2J{a2X?ƃEDTO +#]eLT)8Ci>V4u8FX#-ZNcBNcMM5e= 7Whtk:J}QolFe0[FX kq P{] g ?9^XxwQtQgK6#L Pw/_GD75yȉ  p!Vl%%Ŵh>rOa.hULO-:bpILR[DC?E)- s'y&h!M+o[Zy#)6J#ʍ-rLU5K1Si3٥c(#AcZYkd ?E)ʧ*{J?bf]Y+iG#(,*Txlr|=l89qqe/nhA@8+^2kđ  P`%fİTQoooFm(**˕ԲPl}&b31$R>d-ƻra7M'Z7KpnV J պj..ǥ%Zm.lƖn4Ǔ3&lq23OR*W*T3"ȟu_&r4vw3erstҍ'*MlUDsN&w,4&N/{m shTdCS1܈!$?Pg2R]m\~m'M 3(vrFҴJWZi/CU@^ sS/X7\QQZv9tƒv!55ՇcmDNYtM dbʉSjHzlb VKq7flB!ZRc>c?NGWfSA@A@.y r3P h>G#NA@0lyI I?Jh*ۇJi0JJkGWfZwe"B(2K'PƛpD\ m&X.H6KIYUwOwnL (2ޘ@A82\#r5-\7x{!Qh2d;q*i)IhB&e;D Gd2!Gt\(5q7 wEЕF l+GUHFi2IB;YVBm%ͅ64`5&FfK7[1!GBT6]\ɏ3HFnLĿ*"0VZLVB?K֑6@ţ9ڇ76Qyz4jcƐ'aO I0MxKVOɤd4 \ؿ ذ4ybcm64Z!BϥM0=CEcb%┘VGV6.H(x6y+/HB" a-@_ ‚BZ2wbюdF=|I4Jz\hMbb;S鼌 @@s?"232(V+ y`M10IҔu<m)njjpZ5Gq(iL)'e;%%4)hݲ];/huEr}:m)Ւ=!z!50γ<{F3[N_}$qI#4d)mhKKK)ҋlۅB²-RGq._ۺoi>?$[Ƕ$=|u;$I`&v9{DBUM5uY3㿂s&S9U#]ol 7Q op۷(m_yzqI |*7֢|\- M\4x.Za F@#/5Wo3.|+X\- Kj0@ \1DueXRקʪJ9ƠLu".ǭ޴0~˪4d>PTq|9zu[BҩȱSYmu &Yv-ȴjELb?iI ',r4Tblh oqB3dR X&SGFj*ʥ'LB! c ,N(IA楸ΝX֥|ͪ| Te3 @QGsQjԨ#FeH5.*g6cl޸]Am]drOC\fKTE&S4p)*XBz59bFݖL*u.95۲VZE@dؗi@:F<g?{' c6pb @ډ"b| m,+'ă:PI3 \K:]F@#XFT_#wyp[Fv9 V|%IgafEjHpdT ֋u&Z8v D]xlڰ 7J|rRZzĄ"-|m'Nɏ|[%~xL(BDH~;IWjfKd`9S c/u>̅pD2hF9Yʫj6R.M SOz'd6ɎZtex'Rf:y_yX^~K&_ԭ_*j9#yfX"~Mvog"$ãh0lLߝ$4oerM77WKZi c|NS/{6îZw͛Ϋjŋ{C J^ N~g֤F@#LE w~BnS[ʂIPР!́@v'c9rG'QJ,̺E~ZR \W8pS|T٠sct,$cc8Q4٤ $d$[!IPK!b)Y,<'LJ}(Y}@V,62s25I&x4 ʆ@Ё6j)E 'Fx b5$'rT1,3Hwļ%9xoseJz*K,fW\'k02*ȉ@ߴ$qi2i>A$&goBTU-?(cv_#I9:ԙˑ[8VFsXI f4Fխđ8-Ͻ !U~lo*N^{q/J`rj;3mz5ӑ<.D) ǚ6q6P &{ln+Ȱk\s٨klZzq'S#3C<_{X}ܬwE[dK]w) h2l<r̖cvun`^=9Y^jLF} ȷ?af_˖m7_wL5ʕ/OWN[wG۩쨬|kU54lxxpH{ nw|x'L#R[[ր|_ \QwG۠y@NLN˜ۑH(RB>;pllw/y|p.Kk4t46"`A 9Ȣ^p=n[l$qϧ8*bP#JChŏoλ(He16oPP8P+e48WWK2(nNSkq +8[0!# QIk+8 "骄ήC)aTso-A*QfR2d18cTo6[n08탪ME)!ɓAYPhFTkd8`Fͳ'ʧᓲaf+[6]1K1{[S Hc-Ok)yQEvpCx Y}TQCׁ:( [V;%ĹK%WOH7I#Ko'2e"0 #ʶ}PvA1%U5˩_ )6p,ԽEZ+Wx^l|\Qi|9z.p6GPxֶ-krl|L>-r֛fK}^Za>8|?gik|MCVZ᫇_rej[}@yyli ¹yѰ]E9*dTQg !pG쯿0k+;&csfx6-_2^)=HxH>*FTGZ!c| ^|( O$<HT:qɵPՌ[fd}r:攫h6WkgF6$b2P^9̥$,21I`OrJ/6_ywØcd&6eLo_{񩥱:_Q㟏oȈA wV̲mKY߶Ir{I,\0).^՗7|Sԝ+!_=Um<,2W _D*YV_h6x]~s9z׷Q/ɶY#|YZ-υ|N|m}4aށ&@ `k̺ hJҎoU} $8 @WŹ`a1aQJu6>`fՄ("@+9&a:V/$.&22\Oz]Y:#[KuWɁai\f1yc:ҽOv^AiQCZ;[!rfQO^yV@Ncn3q;3myv0\sYe$s*Y4BG^z8;:6sG66oʮ[vYr '0;R^4[*lxWO7'*<'h^ Yzgݚ{o}a<[8$o_O[)Cw7/dTx٠p0I6W7 Pia8r*Ǟ4^W{j\V~oƮ{i=<1p#$[8h$.!<ܮҊr Sw^`a1m$?%JaI A rl%+-)D4SS7$**0z H:{BdQ$`F!1lVGlORp~'+ LQ# >ز@eɠb N^R65@gaL9Ab2p"XR|]+3Á1nŶ20QX9ƒpAոk~ [)N :kW&g$͎/k,VS.]Cc$X^dO݃>dR`m.$ {6GLl7>#ymlgY>:i:߆gO|zJwݤZn9Mb\WJDu}EX\q"96ddS'Gs+u" DZ<#yydע3|RX^=8UN Iۚ~!i7.%#B%=J*Φ\"tzam7ӃI) 5{n)+?=ϗ%Jd\^, \Rez>qT@EV/+KN>sėȁo NuK=z*My!_;Æ[ ߑ$yr؋b-Ur}.w/lcZ)_^Y~'MGNa[6lؤ([$+.)IN ěYvj1B r@RffY$L81U[MO&.V(2H Q3ONj3N2`G+b† `( XA'l"G*6m'}U;6- ?m ՒuEm$J' ҳʮ-˄hknj{@~\c4m_("M6s#__)5e֛&o.XBD[TN()#26ǎU\/\3|csLɕd?(v6Aƃh*ų_.f.ٲ룲6>Itwo*Z3tEŋ7S? H8y)O9Nn'KܛO\;Y^ av#S\O~ &Ey<0="U{~?#(4Vy|;?X~\Œ7sMwFlKfbVf]',8@PQT?׈I<|InrĘ0cRҼ›򑑻su%LqŸo/ReuT\߸WֵS׬n~Hty _ɇSl+! _H_0 `~(}d2ND&`Ul4#F2WtH&ijj)ވq,ZuD1Cf?2;C0 pÝw2=$>T\p-J7c X gv%H7+8R!& >uPQyڄ53`H6[Tdr-/rNG':$مO 5i$cYj Ono[P//FFI6Hth%3!( =>G2 Ĝʜ (qAY@MF&HzD-ѕ G &sK% ,5v$28|إYl:e$ަ2:"ǐف}\Q*#?K"i, *43'wʂ2#0ac2p|ƙ F'hj3BoL,ʗZfA7}IlHvL.?=*4KeEd|D-Gcr sXPR]ou~jI(峭:3OXH]6r%Q\7#}7XY9 [ʓlkG6}Qq;Gqe3|nvj) K.3s~umN(̈́ovߚD5NtLJ'GU{:!\za$۞wwʽts\pl?@]Myaܮy/F4T,n/}#v00gGC;07:̈́"ب`%`i\g}e~J8euޙ "=WhX^E/2)9~F\~K3Mr4MQT|%D4lqVM YHNQ+l⣲b#M3e6kZ܎ wI} CN<`JY?Ζd4BUd&,kpQK+$җ04HJWvyKDeC/لFoQ0I'\X0dMJ)vŠC_(NOf{USYеis{.q;rOV!]\( W߾k|쭆P7hag"q߸&?#ϞNF@#P*4V*$u=Fd!immUf݈ںz2vG $Zq6,SJJY dOJ< ReZEyRuc8 J(@2B#ԖˉU=W+Vɀ@eAdKt3 =Z#H+H3+iI C0 _m~PAx0d 8ivTrY$᧭LQan$')G8nmPYAeЎPp V 6+ ?N(P) Du*Bhr?l/DF9+ӯx[dcȄ8pp~bB-rC[QIi[.47kMDZ@C2MDh| G)-ߣH6GLfꗵu[$%%#|N䇯H6l "7>"?n?T%|L!gk$|nrj$$7v3]kߡH66?jm.}?&pKJ?y~iӽҘ&b:"@ߝtD Ok A`ڷ^4݉g-{gH;<!mmjZo^!5E|mߴypj:zvRfr[?p4A/ JRyְ>۰~q(Ӧ~P rs5wZ{|&zn*͎s"Tv_tⱨ*h4s@`Ǖ#6":F!`*и'}_A"P6;8x,{R;]xCJƲf(_!HS4 Bgt>Z%UZQ* %<=@IDAT(ᗺ ;F`ة|K\FMeK`p(4}9x  H|)E6SQ>Aa'5*u[ L$ed+F @:`b0jq(ʁ IGevTdoƹ63LF dӌ3YN3#ڼJ3 TSʬʽ8| ԏsh Jֺ~g8| V bKzGhWܘk{[}p PwUM@II )D.eŎneH>{qSVqj}"g_ew=WSj\|Gj*lUS~QE@ѧEnG2 y;8uN7M ;yW[~!]U?*Z3{N^ D"jt.W֫&ſ4S^f[(J%Rce1qR'F #!i2(U%kk/?헶?-;3߻5~_{z^(dHEIL<kA;qf<Òܵ=(%[7C7}}>eR\zzehdZe:ܱfDD\YY8Q9i0P6|S7Ӆ,F`VFٱb$̌*Hݤ`EaΎ"0=)ʓ*ex ` NeLIaZX7Tiv샦}rE=]4l(0&MR9!ј)sB]^:I!)O\w= i's).=g/ۈMr jN9\9CTN|\ %xdEUEN(sfA êq'GjV7S&ɖcq6iiDnys՗Skߒ@ 7+2]?o8"˞?~Bظ;\m:F^q`6+Ã"lsGFbJO|] T@7^'>:J RAycG߳+ȶVm9 ;Iy^w3S|%+<#;w3O|f p oca ن/$l5b}<{SvTe00!' LQ~v~eXTE3yeoI}Gw3C &x10}|u-<7.lGD1 *(|`2نa e,]6[ᕱ=F#1(J0"J,T"XD4hJFGU`7"k@Y"b $؍Ϻ GDvEIf~@>adR5Cߒ9t?ȡ[ 6aX<$U-TR  6Ef9$"pbJIF? Aj@?9AsAf%41@ %ބ:`Bw wTkпJӔM_LReI1j)=D 80j)lm%l\6c62xDr&, E谄;0.RE !uzQlu_B[D -k+om|jxErF@#'%I P( ڕOuPߝ^eȹ,3S%/[ \MFI V'K{{ KY~"#GKV]6T-TR3G@T%itPNKKKl\F ,ʏ-eJY@DU6\,PZ]Vjw9afźUBc}^7ʢYp F /mފ\$j pTGAJKB1DBy3i8yf2G'` /βDNKK8Fl,NDȀ?:tI(<ei1f2/і1MozE,m oH嫓F@#D0`&6//Dr ?:-cCߡW4i@ElHr )}2 CNB9BdQ!4rZ+w<K(*9~[i2+FuŸx`0$?E!LJAzl3O'vk)*)F*sPE൮MVhYr GVHz:1I&@ 0SE6T񡃜:i4C?:- ȧ$hR7\+?:i4cOSF@#gpɂ?'Ȁd >هGF:l޲.(2H"̚f WA4ʊ٪h:Süv!I2OLe1 O$( p@eU+fe<k.7F7lX?BK,)FkvE%&<E! d ,3ILQKV;S& LRlNjR8,&2Q9gQ'Aj9'i&`* &CCgHI PQYF Y>f!@}v;nK`|`'o:>ctA)l/ Vj჊AJ@uUCru˔}zC#X:.LxᱹmKxu)k[ ʆS Fpe(M|h$G5@i Cg,<:CPP,oWPV1a*+r$H{KHJu6RDɅQFYt _5ɯ†F`plĈ q3"[らOF|J(2֋q'M>s}6m+BeDvbAGaN+4}Nl8^ $hcAE.&$4Ɇ;@ԩ:sةqE]i| # r}f؎!Y H5)!Rua8D+@ٖ6Ed}T6k6  *6ƣk ΜPerTmFQ,x 6M7:ݳ*w ڢa!V^_n,Q]rB a'CzE#h. * +aM`$ ͭT|~?BpGI5cNQPoRB@y&k"8܍A:6˃O XF:|EkdE}x@%Aby!X:KJq9iI 8YNO: άTy}JL 2 6["3ASI !38T@a͆dL9y"ɨZcyф/C Aϔ6*H@:FR&_FH(l#3¡_q#RE=:-ܒ'zɎkXxF@#pIkL7\=%$~ |Gol[e@lbLz^9e{R7^Fj4ŃMg=SNAqt[/]0Y(2<<yhf26zGȑW|gygyvojVxfq&n\hclH?=ϫTȱTKـͤ7G~_ΫmT9HKH4Lž2%ZCp3T~Q" @sR*Hŗl26IJ, Ƈ&*H-*K{v'6RI0$Ԕ܎DJd q jzYVP9V4!MB/-(%$t:?޼fyJwoX`\vByQ^?uiF#DD?ENўoi_oϊ/)إW爀{o7_/iٲWՒN%$>aq;[ ٜ.{۲Á~~mX_Kk4E@@eddH\! t鑾!9|رCd#r&d#F$LRC*.{y&12A ȿXvN ~/= ,8 b%!s#hfpIDyṘg.Jm8QF)2+5s\ҊiKFP}ĕ~ڠV 0* (6O@ $Z3BÌU&FE<4CA}t17TuUg'}2 3Hʱ.|d90D"a9bC'26y!`GW8r9'eWZх5R#gFw A_`.kv)RR/˴w?t E[.c6}95ծ*5-U]FƉA<SYx1wRKk4EB Er_i5UU|ɉ'F75. B{c~wOˆf$|> osLG@VH1sR,'H ߵ!b<'"*+C 44>4܀8+SI ^$:(bDLp!@70CM0in)p6hDee`\N8)N~ppB /e(uuLPd$i&I:;qbYf~XOqX']^LHxc6?Tl~Ȧ2" qb(@3 Ru(l_G`E}/ O![5h4 |24i 9%krw!]|Cuu'qDZs#}]ٻAeHe\b'V<18QLJ 4.@+$z&"3POpRqdu }ؗB~bTۡ?>:7M+G+=H^v 4 Ulvymw)x),qЊҠKe^kwn\Án&::}k ז 2Yn ?=>iiZ!&ܸfE9uI#h憀I*Nx5>e$ b'0PQ0CFDFWVcRYFN*;\dbr)ǍnCٌS px$\Sf9'Lg^Sr,L jo!Bx ٺ;$b>;PŤZyCs<$To[U DHI>ϒ ՌE"|# EOFE0X@!E2Ɏ ?gd"I*e$#Crl x!d  OQ~p"8Nq.=>x#C *p4)+/S*suI8eȧTn@:QxpRc4U3ʩJ)]oe9qH08T%jBW)/Ͳm,QS1~I v{Z b7zJe;$}sSkC,? _5/޶WnZU7uh4<2m"rܘ%hi 6| oU^ERP520Ї` Y۶(]fPd@dD2<뫩Q$Hґsf]jQC~،fxߊO$Ҹ%6’ )Q-* Ϝq46(;>C GbμO<$&#^~\DBmךIȷaj\LX}]R:REU9J)cAΥ$/ 𧤿 @N aieCQjd#E s2 \P'i VRB͸Hpjh$UgQHS4>>R d"^QiI[FsN}YU'_!? Z}E To4'Eyׯ`IwjrHG)i&С0MM9'E#hW uh4r@`5ۗ04%F"~O a`DexxPi]ܚS[*jm H,؂V ''%-LJ9FHJ1e3Aۆ=h$/tB2`Ψj#4d2#38QLxyU h#y Q:/eI_lX$I|AQAY"uD:#肝3/`CQ:MCiG*Џ1HMcID+Jnj,oy,$ٸP7D\g8ueKcuLj QUE 뷁ʂihlVnts-iVӋlܴД:_gb8VR2> w!'4X(Nt&l @*/^6BI" @qb3g+V6v(TP4jȁ6p륉i"gBp8@GGjacNȫ?? ~gA8I5Hq|c+IGth4  :i4F`9 ƚ͚hˣW.wb$TT!R Yg>e'7HCCճ?I"*bPXy $O* ?^RG CpZiqiU&Tx>}|:L$ r&zy)"OAu$ `)8eJ\(XQ:Kg'dttT-&Mm-2)-i6KW]i^ HPCv6(7KJѹ0@'M>X-RU8u<D Li8f\4p2FK#OR`& 'qIUrD%ŝvgb'VY] AeaZjaCU9M`pmƐĿ CLT4F)C'F@#h4<?6PJ3dpecH֐uwaG$ژzzz`-r1xjA1 +F5Hc@%9?<s-bN%'Bc$ڊd+޻gF@#XNވp\|F@#h4rE i|< $xZ[(CxO@ZB..A2h`tXjs GN;vI)-^Ri#Jd2_"H)2 c$xemc[V$WcW&AM#L@aT 8uT2b gn#Fq|`j_cH̑$c?Zfs pE4SF(ik睼9^ַXSI6uK_F@#XB?fͷ^j4F@#,/` O)F8܏im A)UJr* ʪ*fDWʲD"8 {p?S^ "dzs5!}Z0`YH20ӕk@8gϹKB555ɯ_~4F@#07pQ}z]#h4F`!`'AC8+ ׎mġ'9pټ?BV"WC9*ΜP0&۷m1D(}♟h׭C0Bf<2a^~RlaGe?4ub 7R9SL"\lHh!Qa.fF&5@X4쨩jl/S@bY}Ƃ2:>~Q|TA-strQ%9ï>uRdLxp^RTG%9/weAe4)i> <[uHY}t-%n^#h4D@)pǹ`^h4F@#XrHK1dɞTAG)]vJ8FW^`e8x8K[7N}[ʼ jm \lzUw¿='(lQQ*$̊-hdZV2uuZ`zKEMHI WNndxX`R Bh`50դ_7q4ȧ8H,U:`^i&Wgs_%VsX@3ZgLBa`nDO DT* ʾ EnUUWK+Rݏ&1vr|=3gN)k$ZZ*men0/m<^v0tph@1VI08.8}PZJB}s'N,-.}K_Z:=h4Kh'o3GƔs|>CIntd?MK@"wa5?W.&аBh _ Md?Ł^gqQA0GlFIC E4Q\Zn˭p7 P?@}.Ԣ0zF#ʬj2KnPx X0/%鄾x<|Kg*O$=ycܦ57=,"TIʑL@IH:@1hB!U|T% 18u60 qhAlS"wpA<&B$h<~G\eeG` 8Tl$l-TA)qc !W< B-ɯ>I_~y}. +}^ht2CO>9(؉o@k _ni>Neߋ /hJ#]}7ɖMm%\rתl4)EzWxP8EjbekrYXm˂ Oit R$,ȣ<>qUV\(  BkQ>2 vd)[$h ֕`7b07U*1O9ެ)L#)Rue@}z 2ĉm2Of:?1c?`b\ƀUcoajc`.J.#:fшm xB ԶjM'aqp#ٶSI6sm_E%r9dOgA%mdZ+ΨK,H6`i"OfQ,"mV0T 1OsZNAkG-G&\O Qqd>p$q ¾Ujg FzOu#ر㢶{1Lȳ/os|73I3 !*J3gG]uj%$sm'yc ';$;,4X$*7 Q(A7@enY>z7|`މg4H&|| \*ߊV9I&ˈ~ٚ໭\GayF|?ý,M!SɆ`4OQH)kV.5JH[22o"џаtvH6]gK$a>yf8_T)m؞hҚVf'qU0-.HؘT5g32,)FvlcFj:do 2Z,DՂ|@ǁl6"ڦs&p?4II2a34Y&ˇʼnW. g^Li c8A H:+=+2(d,b)3裏͛/DzQCbʊA\0Hs!#ߔw>.XR=~ Їr7 t/O4 = ˧l+v$>'ǎ+EueIj \V]PHq;.y%1DT#CPU[ 8\ՊaL*hX_jxWa&U&VKGIB洺`2I_oA4SLǔ[#9$]#C#$P)/T`.;HyU2!$LN SYA) %vHv.crq.7c$Bb _v*##*qv""]FGFU^0X>~[x<^%vz 7H/~7协~1Sڊ@,%NmL}I aDސbqδ$ D's sǝ73-guXMC2N)4:JMXLE 3OKy&KZ68 +FDa}6H9 44f/L7XgU*5V.eeUIzXCuc uf BEV12헰k.NdZض&`qh+Ad?( :P8 aXk_6!Op@$1A#IE$TDryk3egnf>|)TPdpZ &)Llf)}}PAI'H8{ϜnٶmR!NBSu$wBo`h*qm3}_kY:{ aMXdDqe88:"2(*ʪh@ a_BBKzߗVJ4I'4P:U;Ww*:TrMq9JBy(4ˁ/d&x޶Tl#t^Y1^mٗ@ڭǭΙ4ƒj% LrTBot9vK@O kDu)&*1 ʩSz2JgMtb7ٜR&dHӆա\ S_''b') %T摡aU[d!ӡ5Vqvg:]u=]yomSI뭷9kq.4*X^pe ;y8 (\ |=%5ă4@IDATP`:ޗ+c$?s~D _bͮ~V5Ԓplzz< d$X jdGlBIJ"a`D2wJ-w#dhLW}JGE`H?oLD]S%XT侪]]f\;sH:qr;FaThπ˷ond 3rPo>B:%]CCۿ5f+Nc?d)IKtmśxb x A LN8SN T睇WWC,h^ȉh|WaNI,40ٟ3o_9s'_m|;~>KDK$9>q?4Hl4l3[UU{.F2km:@a8Cv5׼VM8ѧpy,bsg[GQo~xEP=6 Z3 To %K@jG?qĺl]_){띈IDeG ML;1\Ynd7轶W gWo=~=o]oɾN-hA:ykj\D^N@(Wʰ[Z0lAҸGGr-)5-?r>m_F3 $M \Μ*CAt<23ԭNx(@nlJGGމ?wFf-_Am%N@yNZwk EZrSE_)r+[ njq][{R)ٯܮ]qrëDVXag> ^í+rQ/q -0) %g̰j IN2$I"wѸͳF5n[@׍=n#e%%ŋ;Wc}\IJA ֞HBrA+ K`^Μ 5W6XIi 4 _@FݸGVI\n"mH=<(櫇~.\ Щ~crCZ8еk~wBm}w({l~/7}CcOusu[/q -,H@HttyS_`d8< 8f,?ʦV}h/?-(3; !M9qf0PN^ ¼}ff_hdƜ;{.[`EkhwiJy(Zfvd#l`(ă,Ve]- E]ŬȺjy(-hndF[?72Snsm`jvB@YT^JP Mtt0edCP}(F[]]%N˓lrol|{k]Ғ2 h(lU;^M/<~gA~c8ĻlοlQXֆqKU7'*Ͼ_J 0 X 0K ־\L , ;X's WAmP/bEk\z@nv<&EM,d@A )oadb};zV=ZܫK0aq1" ͛\D6sLKҙ1 l (H#vϬaN v'+jNNAp5yQl ٯiQb袋i 'nVk[37ю/-^[k]hӵknn׏7h{9BD}&(p6 O NJ*kh~^HQI#VHED9r1(޲?dLFKO d̥$)Qk%!bЍ{% `)b@aBc-;8`YSɅTJ{H8o~[fvq}W?uDfdn%A${p`Qm2NqBAh@I Q˥p ù>;g.@0^~y#|۹Ѻz:]_|H(>+DK D99i.mye |˴m;wr)K[X>ۉ Sq WFPCiKXxDQjW,;=%pOEMb뭧bm2OOL)hs~*VTbĂlZ8>dy>.|0 ,h}Ou @1TBcgQQCUy;VoT+\J/b⹐Zp$0M'F|Q@Կ9-9fs!{ #V\n32Vo\^^?ʆ3fLMLcqtRKC#nb,05>%-)5f>j1RKQ2w7q -0?%a*ʿl˶Vnz?~6|+ce9O-4\_1房[UY`3zUMa*œmʞ`vj9/+vzCc'[UW-J>+ nid:gR77eˎ]ol iJa 9s2rc{;}n#@2? {+BP3!I$g!2(KR81_0G`xsCr@qRzOA}C@CfɲSO] m&˟2JyَJJJ:Nx:Ӱzxڿ@caa> ˨#ߖ,]bXuj@;?S\}{:Čb: on,Vj>{&!M*= <&Aj]ƾR Go?\ay4p'7 tZ$dAHͷg= gt&^\ީax:mmh*6NE|%S!q c8A Snu N$1B[49z`iZt^ts[i on[ckp$rl~)N?<]wuSXkx3Zk.w'ugzA!ۤ*@q L`z{Vۣȃ% $3 󳬧6t.۶ۈ-ItRۼy{*WVFW^!95V ^!ՐUI.X+(C6H2U ",TG#rFDA#s@֜UeUUbΊy *4>%]]5M,|IUб~ AѦhI)( Q%ks.^rmzM3L K'qBA^%nMfg4&Z$6klh@tہɽWj_[ n)G[tiԽpʫBAbcK^Pu(yfU37$YAؾe;`ʦM.n[ <s헇TrXKjS@$C(R}CoCG(u22ӭu! N1+.Jvtry N>bA1Tt.z>2 +/˰bŵw>#W)mbqd@A7dgjs5.7$?郆Z6BFStabv/Ds=ȧȿrڷ gi)<`>2ЁRWG/ͲA-"C?Nn_9 ͺqGZj9 ' t:ģGPI¡yE~x"*J`wA{_[ n׳ȏ^4ĮGzI*}vCV_!n8&j`޽{G( 'xcU>-4ƒ顇z~QD CBU^imϒQt%Asc/m ( Օs'ۜg@=J<\3ՃZؖ#==ӹI=t+^[ckf27Ö4R@Rr Ceoi0nvہ`CVR\% >"WvfIҀQ`;Ý4G/$k&N R)&DEjTjIɰ W-e=U4mmm#h;9HP:vݤ\þ:ĸUi!Ms#CmA^0%$`*qqn߱U[*}sb,I0dS_R2bg =`'{ľWߌ,h d{*A͞;%b}Rl,([OuJA0f&%680'4[N0d'vY| PPH \4r$$![Xč؎=i#th_'Y.m ),tD|2< d!^QNOiF wgwP.C'+uŖx@ QZzjhrQ6+'n; -zpꁛ wIΟ?ߞy.kڅ^8+-@GK /9v=7og_v45kּj-@XP6Mj}L@X@a-<2.I555{_bUTTLT.LT 'J>JK ixPy-&n1?\.u&X$qje~C&hp Tiw-T+u[ /$& DdrKu}. :ɖZH\z++"4UK.@uww R)aVv.@d8򙳀HRD ǢduܪŖ HZ.YWj<[q 0-S$ (EŨljlL̙(WF- .1R|w "{xgΘG{brU] Ń_ˌ}ì[j)HҀ^ͽ^ 4[SWdi=j0!H~@JrZZ`r!B$T;/t>:5icuP-l',ʰ@v V|9GEyxcA|~M4BG.%%=Y;b# lVmmXGFK!Semq3llWN2hZm&K p!'ʹ 5V[[2f (+t4Q Ec (1BRx[ n}k]7`^DKq -p@%f;e[`*, xEEDP+;%*hm!k&;Q_6 ^2Rbxn-mNqDL'X`U7EHq%8չsnүp Kv "T@!֭(v%R).2BR<m*S8%`p^2ӖírڐDl8%_ )01늋KPR-4Su(\$ i$) OnrP2LqRl%!u#dfJ݋'N M6൓UN@/ӹJWzYeMOzv5A;Giv( kQ,f,Cp)J)_'kj߷ŝ&wP}{JmZ"Hxk #, :a!NNj$];wۼӖ.^Hܚy2m9S[l4䑩(ۺ@݃j9$_N>}> 8$uqUJ7e B@̀fI'&AZ I; OHgYimQJΊ Wc3s-ŏT?Lb ҆6{V%{@TũNe.YYΧw|ө-@etmjTmr=V:Bz 7x|Ӽ?u]=MY\V(De ms책k޺Μd;}84Qղ+,g?RQlɜkWۉE3zS.[ms2OoΛfHhفk-8>wb/Ϫhw[ۧY-v*, PͦM\uOvgLEszmwH0Xl2mqb{Y8I!JhrĢXlEe˜*TT+nȅ+A"xC 3TXKT(ƤRFO9kGUe⢙˥\4+fVt҉ Rs s (2fShKmߏB8%p.:PDKMC f@SfzE ZJQ%]50po  ᐟXsy9ԙMl&OB(!N]+7"[j>iApUZCijs*W^^> ȸlk%&.e.zGK.ay6p2öW ŋܹԞzh>roA]!arvAPΉ ~k'iJ Mz֡z ?Թ-ZSSgmrTTh#]ԁ45)˶Vؖt&   I|*5_@GHLXv9K!Ö`s* ,mi9'.zodLCzHLmްV`dHM"iB.Z{g)j;>|K%\G; n(# f=֬Y3bٶN$q˛ ;km{۶t;Zit~=co VYF{𩻭SwǃӬn˛XQ >&]\4SR%EPrEoNp]iP>|-aof-9H2Ot7? kvhkN,Ůzr[p ]Jb @`ל[F~C!=xꙗm9Ly>&FS|3vI#E9KV jWÎ9i731Pmم/O+_1{Wl7|-z.oxж'vٕj$yn/ntF@C5$L4mWy-A7 c\f͚懓m!E//6>jU#2:&T6`d~\$n$/D`3[qqGHX0H`| 2a*\R.eeY>J.^W2rT>D8N~ i䊙NdtWWގ$r¡,qr+MN@F<9Dcbc:kbNs墚RS ,݃",[m~a^vR2oIԳkbu8ݍj :mm+*f+)).N5v)2j)5ݦ;^.KE{e_J/$Bes#fvlnX&0K$JCڗ\sKU窷[F0O*;?\WIA_8>ԎJڰk[t-\xlv jxwmMh c`&f3dV|Xc>,~mjʼnHZm2Ȝ3ܲfH2qV's !2Z_)$KP쵞ȁ" ct2 )fiC2:md5G<  ?IJN.\M,`P!BF{?N%FP Kf%L7Vy'\64R*7ڦukƽ_m<޶mB[mV<,7|;}b18WagqP-mܑ?N-Nv~MG{/hCz0M@DQEqQ.u&M=j͐I-ģ7S!܉!3 EtxH vQIa5V_Zéia|Ĩ6JH7ٽ5v1PfoAuI6hn/ 3GOP{=Nk!\˂ŶtNH̷wü\k(T{I+F(9pѣxoFr_;Pф9a|އ<[Gܯl?Ń. h;;)O?ă>h^{T5msP؈2D\~L싊ZZI<00,VࡧdSHib ׼Jgg?F"1)KK$9EZj86)(Y$ڻeK5qĩG!q*!+.fƅrT~,ʍUPfX2y'҃xFSI<ELIT IHR,4xbe쬝rJ%(xWTTH{ϊqלDrCu׃zncTOCQ\AT7ZZ kv]33+v<innmx\p@X?*t!{DjC:J@eMaXevrh΋K&̙Rܹc h' +9.hmq%d؇t2[@FBKh!E>~\> bWgmv"hcum߲PYO6uuKs-39 *`aHS' .Xi!mL'*W;`KpQ .lDm͵\gy4hl‰C%G 鶟A\I1c*^b .`ڵ5ޠ)fP gyO  WVD&ʇўU~µ߯;mm&߿- vC~ؾoY GZ\U:cyz<67?n+m_b_e"ϷU'msGw0v[ZWncP&7h>! w ckts<`;ƪhj~nl![wwߏ$l"ї_9߃ׯ굳W#z5OXz꿶sq^ѠX[O' -G/1zSQ]m}.X6SQl۞={J*)2Lv? N/ÚBxƵ[nQ.\oZ-c8ThAa)4ٖ9=.E0tN /Xn-dlt.mm'S(FDRoO"IfT%eLⵈaM57! {l@0·(=E'>P4D@Lbw Y__'ˁl~ އ Pص7Tn}+D@/\ }e՘[I |xr IQo ͅpiW1ovra V H$od2;zŚwJn=8^I#՟~_FHRMdwHD#!n;!@ݡڠ)+:0N' p.XAzB] z9`'g%e6QCcի%\t=̳da 2dƔ⺅P۠ۛWD`A2{;j7撤 ;_6΋t_d $vp2**)@'A:XwKx /Vxp*VvOn`zw.:փ64d, $y?t; 00J.Y4ȑ^eݺuvǻөDz-L^:n鶢"_a~eon_ \K 6Sʝo?;rQyK>uR+GoC}[oSV/J2Ѻ}Գ%'.\@unՏ^'z}.8@}[P2\-@\fr|{N m4Fچub\piv NtPk3;VYlmMd ;(`Ûv{dYV(0bg{'[F\Kʹwu*ٕ'G_mS 9s\ >3meIp'"*]:uB[P0v>~]V5'[E]'swe{og\j޳gէTݩGz[ ofYb$u=jf_A}ӗ-؃wm?wWd,ln?/m-wSaUO<=:T[=r2UCMRg#-VMrɄ YC݋"0A .u9wрS/IkE׍,C<S_'ʵC!RMwWڌRB! 2I|7JJ;-U. !=#P̅8lBrkM2X7 lCn+G@@]TFwq^޸E[lڷ2]Irug{j]S Tp#,JʣNr FV=`3z, gP 1PBe)‘vI&r8S{ xxcc"2`[QcW"W l[wTuM7Mf=d{DženoBv={Vrrk&Vr+ͷo1>wL"%!V>"u,?=xSоx}FW ܆~; a7=)U笴~3b9]Ąa0ox/xd]q&'+`WsUD終ӪVzl=m 3;Ŏj6WߝDdkmO/ħfndՊ,p zʩ9`g^4XW, u]z۵# Ga̵ irn{*b #_8?_Ώ_q,k+oKNZfK{"r˯@{f+'/þсmS bZnRe3b˭qE <>!d=?w9>{fw0`6@LDq)}qɖ'%̹Pv.2ۉ+#{qon:mm_>쁯_m%}{8)ˊ]!*Wk֏ݥ"ߥ\ei> c=cO]}][9gص˹O<%ShŮa5ȜUlg>icvJX[m|;휋GhK}7 yАm#GEc/zjo){E[csA<3hG=6*p2  ig}ήMC^㎦f ! KE%1SCW)04P M\W\$SDa(5.k䥥M!l\ۈwV R0O>|UG0iؔkz(+e1%)f'&\;~nġ qe:̾33%#]SGW'j:}N}Yʄ q,>!Bņ-$7IB'1r,x;QRZ P?ԁpW⸕!8z&'sŃlztr'ܘbY eU'ػ-A[ rf3<~w ȏ>y}U g#>݊y= ܛ#1@&˝s]xb6V3_w{[~{_ U؎[r莫o6~ .`{Lf6QOwΝɻv:{Oo/}1`wk Of({P!pe͞eeYnmkU=}WmGilTsW;ewnxZ dC9y5rc+4G];B̓[#=AP#hgm-?nFMOIԍK/EPB_l ~b[-;uU`dK\]ao6ن;]Tae2Hwx,f> ٺ[lEaikV/v3k=m+:і]5o#sGdG-I_"Ńlgszl) lϰ?~?–:|3Hf]Umֲ Gs~>1eeY SyzYuoNsGZ /=]|ɀuuvJ:*~uM` BGV~lt{O{&5W 9{5?Pw>_=qͮsNmr~WoJ٢k6m`LCl%;}ukxۏAu5wƀo"a}e]{mWgLlXlum "n=VgWxa;CN TmK]~yI/d |hb-^jۧ?&uXj׷LjݣDl* ${[6{T<32IE(m29 a55֨NevʕD7(:-kJR|}P@Rw1x4^@*nB*<ܤ PܵCV\<é0̡E\n;[NC+si?ނǕUvBo f)9sj6(0ہl!Jy7y͕RJwA SB^h@L@)68o*L\iJjQHAF\WB!DM )O@~=Ė˨>{bZBt:Z_M*sz&[F ٙ@IDAT`P$pYifX! '`rAZKCz۸?i.ȡ9T';M3N`W h^:X Xyl%iJR.IH?+gխ]TP (0j@Vz Hp|cnI*.~ E\0m , C*JR -z2ĞW"]T?ho;^Ò7^Cyq][7j-w; hw'~(dj\oOrVzMv٦ E)t6kNˮlm-UVTy-W,.9/q,To/ǒfl 昑7|ۭƸ)Yo,vԓ͘ DەlvozmK]gE1ѫ0[i]P.?_u?Tu(t,eHG﹏<s1N7+>LL|> oQZnߺQ2q)Ό6?Frg9 {wwd QzJ܉͞O쯣s w> G2Qm½vwMdY[#hPӾ[w:7O,=cMW'=Tg:ʷ.TyǼc]c^7o~"Tu[o΍v&1yΊӻ3Nݡ-0 BVyb}*OU+}}%\}_}$ ]h}o87n\=ށ޼S#;`w1?Y|ݜuP`eȶ@,[@IMBW"‚ (VuMk;Ks! q0'Zm&*;Tfa`Q8L{b}T@K'A 19҈k%c X\G{sXrdjDnBTa,"%5Rg ΥoL+%Plmmwϑ sؔxL@Q*:Sۼ?q&[ۼ:rRyCAH^')uXA;z( CڀbC#7oAF2w #$#IRp5so0lH#x hCjƥ \XdLumͧ>PČ> D+I;N,&RAM;zZ][:P@*' lKP|%$<+  Œ@g&:t.ms˗/?!kN\sDF<&kZZu&d[žhE6/:~ٺI `*lyY[_c~n? ,IXR=~^rOk6|~,+P˟_2Nܔ ߶UԮC9+luh魾Ǿ#:(S;]9ir^k֩8(ŷ_[ΟQ>F[mWo).~muIdr߽? 4i {UxgLk Fzmm@#V[KAeA{!`}V\CBt}M :[ow<_ ܑ&R^P9Awu^316߷mԅ~4$ھf} wB]uj(d6:~iQq6cJgqUi}EN(׾sx|qgCO4Җ:'e{O]{Al2ldw; V^;Riݯ$,ƍiF1__3;bc|̓l:{ݸ0_)치V}9ٴ6u vhN>Ξ}asyz}VّW"9I$+L:BЮG!(Ga{bU쳟7}V҄uwkIg柇˯QSxU[&wjH+ h )HF(@I.grljn`*eTـ#bu2봲2b1䢊{?3fg:I"aa6 Ul-"v6S0̢4a{|!CIJ0zL4>TVLls\=;bxC0pZV ڍ0P0ϩ NlenuڗTz )V[[nG_1 %ЪvDژ%,PRH 9aZ2HzkoSO=fΜ Ms.?ʤ*YE\FR,޻G+lmO0QE`MMק4-9jO'ӎmG?LfC.BTK@{qzZ.>r [hO#a.~:U)U=Oaqd!0=@(_D:H?iH!! _zq.>)ծH02j;QE} $(gOOJ)Pm guÅYS\-t?Q|,H[N~\{ŬmH:޴B6}n'7'~vUۯwGx #Mwv5OGE~ķo>zhr83F?$~Si5QqE꺚WրPeO+y}[zz0z۲XE(fumfA_kg3+Ća?0XdH[ \m]w7(=w11uKll_ꗧ))f-[Z+]Ubj %LUϾ؞zh5gqd?l%IJӖЯ=o݋`/u[VL!鮶?~2VWDo 峢I7z|5/.igivuo/Y3)[FL?ůgjkkG!ъS&`ܜr WH)vPZ Z)SV::{l@ 8Dk){"jb`Q^BU%)*<`}<^n'rs.⼷ي':ՖD?}6qv.9@&dv`YdMֺl*i@&+[jFدJaUI JsW b1FHD .^\#m-i1xrht!%C>bwvǶe%F+A|'VA=@ܐxW=JKKkccZ[` "Te{gC#Śl(me:lZ i^6:me*޶seavvp0m^'ϵڶN9냝\FrE}>9 GZ[/i5YZ쉍DİN. of=nu(C'^Hz{*H=zv91ѕN&oc-N 9 c`mwF$ʮDn3ۤ8B1Eq \nœ`V=-vB6ovSOYw~^}?=5Z[~{28J_yxU^eUUUs/bC?UK󦪺cWV΢f MFhQFas2M6 "/g,ɋLMM~n+*/$+sL~\jsc X;PQXKYVpV>rn}isxuVa.J{;?ztzm owf\,CFᠷ0A\J:S㼉`?cHWƟ\Drve-`{[~h$R6yXy鉣}g ftr͋Nkˎag]̧ɿZZ1΁:cu<`YyK_L͙]fs=g}RLV$$@󄙢Fz (8_4/E3h5{ZJD h@Aҹs!NZhRz> $(I@71Hl0">Y C{D@p,@R$-T iUwseQ9vRf!8H %:e G庻"5 \VUC)9JȨ oKƐN&r ,sNUtŶn>3P)^U&lzV$@Y.±(F]Ft#Z?A|}.f\ ,G-{a t)XmN6\k(+]iV]2 r)ɥ^S ! $V:%)]TSC.-#%hjpII#tTsQ6 :@Wϟo 2w|뎸WmGl *Нi.^x}yM_ +NLGk+G(S18N&}2󦻇Z3{t8N3Ĭx{,DJri=H"})~VN\~=Sn{OԀLTT,byeѢEVIHxG0J xB:ڣm6o&Csf`%靓_#q˾sYFgѸVk&Ӌ1DEw>Vg'upxr5r&g_D±;^`Q_6;o78un'eN똕xBN|:~;v s˭:GWS[-LIҀJOGtP ^Kj@9G_\#pd%bNߑʹ @g;)_p"W̓j<>-ȆɳV pHee· DgӉNG84 .v"YD)x?,!"zeMsAt#?NNHOHpfL&CaeR e *(+Dfqfق9KGG]R0>kk\'tvꢑ$LI[00W2bUQ瓚-^ތxޞjYD.9\Nѧ 6f\o'F!P6{tezׄ. pvy}ϿXz1R4xEӬSГ-WBd_w,Ǡ wfj.O|Nu.9vCYvQ论IV⁹5F({蚨-׋ʐ|/j{c$82As]~œ# (Lek㐦+{sw؎Z KEvN϶F h%מjGX.Z$RٓOlsZ Sbxyq_v1ϗΊ럶}]yAAGƼ9HuBg3 8[m7 xzZpǟm tܔ%np>aG}‡v=j_̜ ׼Q>n@<%]2p+`O~a ~p'\ӳpt<۪7W؅(ڶY\e ׻7pEEEv'A OMm̀  En3gʹݻvXKcU.ɢYP0˹.!Xj#Vn@=RI:Jd1[Q@ u*$H0SP̵tm">ETv?5РvD2(…m,hmd晓1R)Yo@.bib%@RHƐr]Fҋ { >̕}+mJ{e&{ߘ9sVlaᓒ$':|>H$D(SAh[p9(pX/VJf 7oݨ$ѱ(\WKKC,⦅ݙk%Ye[ 6c v!BJ !R.@ Z>m1jV/٩Ύ4╵kf<=s~R P3<)ht+{e ] ii;eTnZNMǕنue?yQo Sy p SXDG EAr7ǀ[;9l0 MTN |DIǤ!R=T@P`o &+IN8U384PF&aPg EtmA3 rX\DܮX;U*/˧?3>/A^QP$%U\;Ǝ~R' NXK){xxhT#Hdklݵ- ;6 v+?ozXnG![S5Gf#)e؍Moz-{#ov-f W݆f uԟ-1?wԖ5q>SI:irq3ӽBQgu \{?_s E(ahOhs |Su걣š{J7-+szA6M?m5sw?#A^6{ f|SQ/RO expc驕O?y%6Ԧ;Gc ]W\/G/˂tt7N] ~ b;=zJ?chTGd>87;ki3op7?#Ígߏ'"|\^<@ n{FY܁)@ڜsC9 Q-E ƉqzPˬ`SOvPMf`OΛ MVWYR<}rئjIj _R8'Ǚ$/] Bȴ p)+,EyV(z=mTBDBLWf&&&`7JݿKJ) z{mL'3q"*H.erq7R>bb&mxl=_>,w{GP{Q^?l{rf?/l&"]򦨏'XvbCdkz#4fzjbTv} d>G_$mrjƀhY|1E0e 07!Nrkk2AJ;dAf"a$ G!A'Gm4ZV`{7Wu7\e\,ʷ (Lo)r 68CDVGkG;$PʰG8Dr$'> ;@d֭s}.ƭf1q?]?K/-yѶgw/$δuT6Vdfq/dG_awf׽(^v;f|\?߲) ,Ͷ#mm ȗ`#?ekn&RzEq6Qc+믿YE,t?Ӧk׼voMT/km_||/?a/L^R-^JsO}kݟ|VP~Լ"]/vb#&mt[}kvgW nxf3Bn>}Hu>f:svg vIv;l?}հ Z]}3k%FתW>;?9N헽K!>2gWHqW>&8*O-f>{ǶƜc{-14هn}h#LWgd}?&>l.v7; l`鿷? d:o{!B3v'?lnB 6^͓.c#dО=kIe >{O>k}˜Ӫ׍/cM/{l_N+7nZo~H%N\_v~sr3KկCq~s>8S۾gپޡsX>_.e3Bu[)8E>]jMi{t񫮺ʚ?/R\@2J1ƦV.}OGi8ΐM@\jD+DӋ"./ M !P 2+hZ)4QbTX.n:(!UdB (9a:~ݵ|a,M/VG7>q>gTQ aDc?h]'ÿ}DY+{/}|Ԑ*ӔnxmeޞgdҏvEs& VR '(ZEd=lELU2:()Flr#-Y@FmB2=?;Q@dىwRlhxĩFITRe0?DTxRcI)(j]#(86J,Ƌ?؊TfJIw0𰨨 xMߍZMF3 C0b@Ba(`R^:A:JlI+ ~֏Ăs4R?Y(JU?jEDU`B VS[DGg1TMjnn[o}o;O)F"UѺ5k6ڵQUݹӶe{&<"޽{l߾dFp? 5vbk`f}e|C&A.IIdK=+j8 1e0ɉlV/~Sk b Pfl{4òO~Tn(#.3=:YpNDZvMmdv~_pN{[qgn1i;O/|~Xo P aձ}OB龜nimHm}[ZW+]i%D`~žxqt,J~Vv*m":mbvko|X~f/}}v̋ځ66|/w] Ǟ'w;_}ݺc7]v߿ էjc[:w/p)gr;?|ߧg?ve*L56ǣc=1V'X?FW߱?omjry@\gV>Cֱyg)ENyUK.ES"7@XFɚu 4Rc-=dQӍDBjzM8wPʩ+PO->WMi"|D=cP *OMMS Le`+i:U3 K\3E(Xls(9d]5z+`U}W?Zq>Ja`srN &JQa3IZ,z@62x#>.g;xBS$W/9|dAXYf^ h%TmA\flݶv*kx}#Zm[p|k_C ^kɕt 'ny b=?2{uv cN\E+(nٯ5vG/FCZlW*#*ݭsۭ j7S]\9&8 B-EP^62s,\RA]~$jݢH5؎#] P5ڞ5H.ɬ_D([nyڭp߾}GS.25Qټ@7_AWF>kg3o=׿c^uvUەvn>V϶mpGvF=x'Z9F62#=FѥSDm_VW-ӑ?=Yf{'̣>~*|>DXZ\bK~;SޛK.---gz%?fd OK.JΌ M 'J5jNL]wc^!r::ҕJOv\o9l~ǵW^kr>p_>P9ؙ 545j;μIsw`pu&MZ:w;|NO(&Lc.Y`5f˧_pR;1n%tp<١6 j+hMΨAj5 "3¬b*ZgO'I򃡳54_`; iꎝ; %\8?$w 2'k[.n~?kX3YNbG3|}/x v+`̀6 RiC-pHwۖ|${hazfv۠V~d%GֆEowzGMMo\p*=~.՗Q~V}Aɴ{ُ };Gn_n7#ORڮީ%dG{==~ձ+Ƶl`HMM``3ܭ/^Kڿs n+^nkөs!tۨ@4mqdH-rφjHRS&kh#&(F\*~cuvtzmr-_ YNJR 8 (m[PYV,oakrp Y[Mg Hw8%PTL&"R(s H:6 n;*TN*%iS|$>9T3(͂[ur` RMĩ8J &cɉ);źrRU)F@Y!F 8LFUbE} 宪r^%%&!SKkAGwfāgJ4oƑ92 wߑ|n\˩!?VgEymx&Q؏@_f14UKN_&\_;)QcSpѭȿ-*h TOsjʭUT|.6?0M0+j& 45ϡ:StZa8q$y_ =_slWkX8h $a,\f r. z1ע!wO(1vPd5IݸD7tSy޺CiT9pB|rRO20"y3P)+i^C.h|(RtM?ν,L ڪܪoOء Xmr`#2x=8O8y8}?s%ÿ"^nnLa]wsZٮ.}Z2u>J/u->klchV^z"E`)D }+aZjYeeM^>ni&B&&qԌSiQ?M{uMOM5*_T;l@IDATX\=#@qK.eem$i@TJ*cukjRqmڵD'Aq,:11];Ov*msZ[K+}s~QY|dFTKT^QaB@IJݥ巠*\Z 4t$UivKPKY:g",( f-Ny-R JF_UxpkԾ,/w (+PHUٷX YΚ5s|~?*9q1ֱW_}4^mK73)x+_xljZ2s1嬎7j )NsEmIqUPָs "PIjB];Vf)I8H_:a&u7ndSd!cBv|NU=![E *m{_NCF*ovz]?D"2IL2Yl.r|.>C\],"t}c\xʭCb(n^HqbC K%/ar{͋#p}큫Ͽzu4^#kvORK/ G@mk四ネ w}<o/ynozeByhHؿ}/cq/rd{~]VeἄRic|K HVWYk P ̌^YQD0`K)ʁC2T,+$S]@v΢(s( <Rq/084} jȚ=X5~ɧrPH*71jek`03>?蠦 ":`Kr*I}(ʁv\W KR]=ʰIT>J9*p.˚m B#$RQC-6#CՁC17,+@Z(p2TitEx?J)QJW峍d#Vb#À> .6;HulW-#ڵkMo[ BwpM956NXTv)n:iYf n1YQ3TR",IGw cZb!U+"/XESZB .ݽ;fK{;mCYe;(k[Șt':( f"e`3!k~UMU4I 1M0\P~.b7($Gey  8=G <Dr)u%>vx]w)=ر4R\PW~RfD"0SD%A xRHb6>4V )E[1J69PA΢"U((k#8e^W|AXZFyz <91:IJ^ hUqpf\q@jQq!ͱTX'nG <(z?"so\8X[-> +_lEE@YM7K9Iey"pN]̯+|AJ)Ea갩>>[[JrɮU]]܋aP9a4ɺ+-\1LqI"$UUZ{a4kT2Jkrɞ QF`(H?j6rOؚ8F <¡Q%*+mӦkq2 qW0Hy.Xj:cOwj< .B 4|A1΅ 9FP# '4J$mى*xDԸiby1ڢ6~QLRIuvP;x6_{=9Q@1ydƒч*r b( 9je!8BC0,(SA+e VWw.|Q_))a,1 2E3@J)MXDu!HC |Cy H墆3Tf*{o F^d- 1&MzxX"k8xgϻ_+B_.VnOo:02 zZvتUXS(EGܷ֐2/XF$L jDʆI_R*Φ`}dΕZ|A{s?XG%-RA_XX`S #FiwrRr64brWGtEUW /m퇁f~Y.۟,J/>-_MH0oE[XQH?G_7!AEA0pr\h7f/aQ)lhuەM倱Bz82`Pz$ jx0/  GXqf&$*/^b4 cACt!RZ[\4)*ŝop5=,"OG:H0R*u\F5t[uNz{)[b9u4.#;~43Ng4q2ișL¢6Q]|1ڄ;RL  G9oC"ӖQ0LTޭ/fJQMCCu;2pGQP!p[*ܗE(B) d €7B FT0-H!.7Y.ik$r}p\qW?E{# Fj+}iN`0)ƥtA\P6Ex={EҎv5=b}ϖbhݚGAV5M׼x8ɴ*nH=ZM7>˸D67=쁶%x.]it}]jsj q\JTS:e œ .;t[$SJ3$ TVܞ$&QUІ"FNcXD&V:n(dܱ0F+@\@AEgg`TPʶZШ+,*jUV&#A8=4.3srR~$0iU*QI?0Jr(3oS:ǪJ2x(a^GgƇcgMe5|f~M:p/ B &6RN]97?a)uET >268RJ[`4e)e#vaS[1A`rʘ!ЁVH ֛#dM˚P:eeN$5JgnDXwW8ιNW]5kQʕ+*TT2QIG",AC)ܤ"T+-6ZcA}pّw;i@wuMi 1HTO?.3hdfj$c[ cIQ3$ lS~16WdJ?uE28 E藜(0eȡbITN m>+Z4ʳ9D?t4DM}jRh/}KݠZ }9>HYTTdZw| 5/^.4]ۺ/?ȯx+nj$_&{-\p1g/z(Gi)o;_u]Uei*r=#0?7N1Rkr;,ѴvƸ! |E0QMÃ{Ł*?.@cb1E b1VICq&$ؤj%%؀Pbq'NU ,=ZZ2 l7a-wr Cr=j(5r+L.fP 'K $e)MLR)uE&uҔŧL/A@0T8OT̹A2\ &X*U|>'Y?1!lDBus%&%5Mu(dPģ)ZH+2,D< AWTcSANiyԊH .TJmXF(UIY^CK| mڴ^[3ΣZT -1~̨4rs6T2§gQA=lNqnb`8vL'!*'Wy-.B_U9'@EPYMrki[{=9+\@%L)v-#^5Qt,USo(GP:r5sV$$A!" 4ԤJj˵fȤO)AbymW\k.]SJ ck^\TLp]}#YbyjThXm̶n!2=vbpAׄmںI2U7 og@r}(mۼyL5yAk@tᆁZJ=bSuORMSw,L5ܺP Rwp,A -Sb'q Tm2P7s9\ ](0;r 6 ETK]~ QNEs552>N5׊G=GQ,2NPq@TfL(GxRۘ#Ql%\59S͸"RIΝX!tv~՜h+"nڗT\*\JVLq4q.P2wPٗ xs+EV@"D2f"Λ9c,R9z!+dP@@U.g5Z}Q[Ue%2&7SOAv,_T0z1ꪉ qdzm;v5_4QM4X'P'Ct< kK a4(8i h]m[dΪϦ\ȦAr) ůO0 YJ k^\ Zaw ů~}gݹsX}y}# IR/~s؞K7|&.Xt3M9ʏTG)Av1zW.u99Crt& 'Rj*nݺ zH)FD:uw ~,Ч 0@#&l߁fjXT[ͨƁ)fe6p5aR_yTž0hbj;+$׬A Qu,r*?H|JTSs4F?\OUW%C(ÏȌ mBF2W-daT|#15 AʣEy8xSyJ{+Q`S).8ql Vܞڵ˩a`!?JI3 yH'O4BL34N(Ҵ~.t[eI TӶ N/?@]ʑbhzTj,+''ŕ9u! 7@~şUy+l㯻pF1V"__X{"E326K^B״KvmETcO#S,2׼x8ST}]a[2v}cfHߙMUdX*ܒ/K4 ?7]ORX=H.,Q)Vbʰ\et'k\='e}>bYTT5T㝍B!Q $pGzbCC-e>+HՓ29ŶN<$BM`<'COj !@Q.9 C 2LW۪V !4 X(͉ ˗r܂{ 0(dPY]:ԩp Y_2=8p "L\n m6g}Mr #Ã7*\YSS3P<Ǫ:"e0"~NCy۳w379NN:J0^(@لFjbFHqI&S"H[VP)jV&5T=T^ϙztmQA{ĺA&ʫPR3QI\ɍ,'g7vDWXCpm{r;.md\$hC }1Q)#Me2ÎRWMrS.w_˩RA:& ') ZʻjImtƠ8M䕼qNǫp9݆h'] bnfU) /^tn}KMdyzRz'?h]uyEIip9P]kӓ6G-X_2̇iŚ(3tI4&!L@\1x2 ToJm .,.I b }=NEN)(DSuEYHv 4bFI'CU%0R Tbp+%!S^*IA$А+6SM Aiv+Q Q['aU(L(1NjR=oJ N%ݩSelڢ6I,R8 <%t%UMnk ;|Ӣ^XOG7=H38b;4uT4|lraQ<\^lqJxvl<-%Mm]tN nm6qޟ$[nkjlQ]^"Eb@?o[w[B /ǞiwtRs/|_iI!ťklU[kKEA/|_7 rK5/^E@ne(mʕf^bT{Ip+,dKp?1:Z;O:"E)꟟@(PP7jNbG0'4R8lcfzf' $RyZºSr7U*̛Lv hI/b%eK'NR[vQ($(ĉw0|e㲙 S x F]ݶR qPJ C:uqt¤ێ22TQbًlRfqFtB@-%t ʈ`vCeAŠuw:V43=EwDkCkpr\s "($ĕ*v#Vf/N冋Oh98˞ZLmQA[5+ŘvziPRFf[4)}q" Qq–-k?sϰر@2BqEfes<\;2mq'{E/^x"E/^<} *Y ((&,F m'4IZc 3z 8EۊUVYriEayE۾={]0Ȥ~_ ^hd10%PZZW9)R"W@w7"Mir&8F y l7xe6: zI &e[Q=9@GS6>UᄁG(!3QRFE8Ɓ^p^WG_n:>SvRǮ QA SNDB<Ϸku[ WE#l*+pry-ȕr)X nNAs>0O :bƱP+6?[X]a;qIs1+^lCc= |!Jc3h@ CRKID*](/0k׼x"E/^x"Es@Nj9<_!O5*!_ʋQpr&f45+{Hƕ5Wmr`(U[# ܲ;\ꤡ6CFDC36C^:gRR%TVR[C̳k7 :LʁAs@! ߍT)2/0p)i/tDt6.EٮjI(`+kc?VZpì33 <`)Z>};,ojpnRe;;`Q km]A,ܤm9D< FKgi (2964;2z@"j蓊z*1ku=t%1r A;˔!_(]K))ʂНR76:'?66A?}r*ٲ)TAugڢ6g \:<IE`Jkʋ!+LB}AU OMyhhfv2)wHDƨK϶~,kALۮ]X`ՉIR!7W Y b!uZZs"KN~܊8(9tR@׼x"E/^x"EsTj o|@$SZi#vo+W$.hՔ X pGfGgK0).lHge0@8lRSgm2*TQ x +qjnHU7Z9JÇls9cvP1V(3I}6J}dXN]Mu5.똦}@B*kmmpQS)}q |~zkVgYvNSD;|K,R00(U#8`J6*ن O"ӵSu5CCjRxQmx[G+M12+++Y2vY]uTKe(ffP=)JPIgrb*XCE:#Ϥ^~(s#'G(2˩jG3-d GHm7wzDS͝!$[O{MI $tiy^Oi\X,>ڏiWQ1 TMwN۪ۧ abKP*OmA2ne޳3^z:U^fE/^x"E/^mةJRڸO"vP)xZwz+BLjXՔYSV J\z&JDAțGA&T pDY0\dӈgqb'vJ'm^LV f^eS(PT2 T,>؉j LAEcB NJ ׭`͎KY1XRqmjj\1) pHMMS Gԙc )MS9_ :K:@Vq%R*q^O104_#ҥ̩Z昲i[E#  oy/^x"E/^x"E9D@d >i*$M*IZ%p%%eQ`i%b}]fZzzn*P!bcN{rVakP]6|dbbU%Ek>:%Yte"< +l4Tl{W7PO &Bzd lhR+`z]m ̑: ,Zj1KQ*;H0uEH%fc0)*.QFTed:!'h%! 1JgIGkk+`Y7 =e;_bt21  a>Qi -) *>?&2EA1(ZWit7S9-R)/L?N܌EgQ@d'o3^x"E/^x"E3F@fj=%\e5L"F{>E1pͨU( oW_}c& dm-<[m}S R]oE9cQL0YCLP1VALQ9W.+!utȚZAj,Z9Y935h)XOW7})WjAK|L>[˱m? Q,o@#㘡O%ԕknnV "+uX %T ^ 2B_ssxsTî .TsX&X__@@OQ0e$ 69˯>T^nEji5 I1P3M?U?q=(mk^x"E/^x"Eg+i&أ^S!yf>K`rR2p\boQ`;v%GToaT_5V._굏NeUL*))Q)PPMYeuUmfFxnSTnb J3j5)A46\AU_BS9n6sIZ>ɂ>Sr7Ue_T4,rЩ*:dA+6ZK[ |N(aziuf`@iijJ'c@A .[H=WVZaފ j!,qBC>bTw_RI&@ U=2e7WM=sj'o!=eQ@6M"E/^x"E/^twu:" RjSbĢ'Y- XLeX:#@bqN0EV['(0' VV:*>*7]4Umn_2@WA=6 9PV@kIe4^680l=]vu7mhqʚXy¨ʁm(dIRFβ*6Dzllm< FW^ dkVRP|K@jϝ;w:_Xz~l뵌&,y+*ڕrR2L-xeQcB),^,VKQ7z",vN`Kؿ'{lι@۹[ڋ/^x"E/^xXhiiqEZiRM)]P6)xjuFmVouՓc VWLJ3ʄ@EtRd%6bPN<@b=VV52ZbMk@ W8HPyAzBS՜2-kfv9}fK,H?mb|ĥJjH_E)f\@H A.K+/P\eR]ne/ҭc{ _":#J/M+;))([=rLV|ҰJA,3os61*RJͣ]iPSMLC}F9\Y'Rx#p3xr󙛠bmQ@{^ۺuM?<#g/^x"E/^x"twu9v. : 4Fp [o#uuw/~q/`1?X a$ $ LPh<Rیzg@,ΑD׼L*qT>&21eU,Mi@Шbd# j gQ0>HJPsEYVґQxPE6AIOh0 xQM-JGX`_ADi g4iZKK hg8id8hWZpqrsgpaYk2sc)L%&*՛wY 4M̳٪ܲuμx"E/^x"E/=iRI_JG6d6mz`ܺ5lYS=+CہEeuFZ(5i@Sc{U@I͌r{tS?)T,4(P xmЕ0+CVN '1eqc͐F9kdpWIh:;;iS+!rQNu KsI,6 նͮ m p0I-;G90=Z*5tʩ_pJd7tK)94=KM*<)% ռs89N+ ӹ6d<.ƏilZWγgje3ϴ3w果o~_YUu?'m-S_XL}N |}P85Bȡ``3<7Es 1UA]GEjOql"` 1} /~"E/]:.}e/^"E@)-Ծ!uVj&ctȑ瞧^"EX JlYu @IDAT>3~unp%wR`)SHibq@PyQnp|_cՀY[ 9N()5S)rՔ)@i)ΟTqR%b#S& @ _RvC-P8NjfytU|z͘6Tә?tZQ<]ͅIh{tdٖ-[jeGoB4Ob 9t.*9 I&棿-irsq%>joZLj=m||' XK/N+N%6u+avn+B;2ojv\ j*3:ˀ`кE%]cmE ']m uÅ_EҊ(Z_hWndMK^o}{'{?zθ7Ӌs=E(xʗ¯]8&~?pJ{+ou7 Ѕ.SNm5%%%qZ_n3i <' {B TQGb TR QΛR 4 9^8{RWL;U m|8mzQm6IB5aLV(pM9hi^'dJ:i Xjyx@`-P8)bnի }zf@]- r4~tM]]CmGˀ6Xֲ&KeߗN8শ+e+ }:Pno}8Bynvozzw:}w[C=*utvڇ8u_$=E`iD@@+U{֌kIy>㎩xYyеD{F Y@~lFoM怨>7?P:&'hnDBBƉ \VUW,<4%<}hH?W#b0;R$94 5>Lj9oOtrw Cm"AIW !@}k`#I)'jRFڔ<'G3r:#Uӂ|?Te TJ2<_TxI2̏}85DKUKтHKXպSh@VyzŪjMD $р*G@iS0dM{BEOɗԓGO<)'AO΅'NP |v-Th=Z_Mzڊ(F)LN&;: W꩹h`^8kҗү$Ěl۪x*"HhD\Ft ]Np}A&LmB gmqn7ymeszx<S¨mpiUsG #(4h4y5@6w)FIee~ 8PUYtT+*,,ƿ{~ȹ8\Y>ȣ5@6/ h#08P?P^C}sDn-*"mhsI~/sM_5MT#4qpX:%BD֭͂# !*#|*II x%x i1q c1 kb5'H n ^WP*Rzb5*VÐ1km dQlOH>H_DKnK; `i͆RO+GWz{&"'۳\/R)QJ\X@(KIr0HksTkhCUő~)HfJh%yҵdn hb8N|usЧEd?ɨ NDڙfNtƻ/N{G86: 96)J U垞V&ppXF&_A .t2".pD 7R)jxrgٲ?^ 4:(/"3r?}uoh:@4E@ xt kS$O'A#I&A-vDmS[em۶e  r%."uMS\!80ʦ~yHBCnn6AHi(:,XAoZ6:q 1[~2. = ]r&d2 ri7X}hA9%ҽh Eִq48`p4p@_J kP=>3=-f&O?٤,,*ǓmϨgpB‷$E-V 8o,q\J# h侩}_YQԓښF ʥ$@Ks1488HQM7kG;^TDoP\,gQw f **vUTC:/z,! BU_EMEDD(S1ʹ6K0MU[l vQ bN0 (y.4 }^JjQ9Un-4Q{ -VVsk[ͩ{&ʶ,AiVyhg$y;-&hUX0КwAd[gw::PI(*ԃn9Kqx>ʫJa2T\R˃'j&vnM7q vAi66lQG6dsÈ88޿,Sw6sn۱AObR`D h('E)'vKJs-RBnY>k|G18`pQ}/O~ }%Fhȷ{Ď(jwVzYSXHqO;W$]T5Z&Hi;K=)wE%D!yA \/LRB)'3`z'(%˵L=7.HUS Ba.FEI)bS6On,rCKsĒFE4TnJ]J\52 p7sr81n#08,c-}b(w\>i6h28prqUomҾ\K|wkZb\Ҟ-/B; *9Ui.( >Z Ȫ F'lK$Jԋix T@effdHT_ȧwJRʭ}tRÁ qb׏K #:dZ xt-_ʱ4-_x-)PYz7lت)h΅C$h|$h#/4d*N4"U-?OfVR`J?#h3jXmD|,p+MYd|!JC|< o[YVXy{=W}PQU_ ",صi-!XL%ۖi*Lvw8&c:1hC  łf 08`p h>Dz5 Q&ג'w.m]2h bL56P %HiiR*XFqЦMA7(UGg%%%(,(^칻 [(eWHډc}iQi/ZZcZ9籖Iɹ>ގr-9:DLN,IJAg')yDB`7/TQ%FtMP_( FbA" &V!x%R/Jc""lHf<,sS'eD+ʩ X|\A4esކ豭R'bk;7dNvo7dg$^~qpxelEcHT4+]vsg_ 08`p 08`p\` HNQELLETiiGp|aa!rrr'"*uıq  BؘI6%T@Bҍpjk\[:ڊvN13,w73.rDBrR J ~6SZL3N`3U VI)2 Z()(=۱ a~t\%5/,g,CL18k<EDW@6H̙NI;+]ۚ(mC 81ȸf1ȑ݊vkJƁQ'z]ԥ}j,~DŌ 08`p 08pq@rrr,##Cq t|DϠ AlEEE)?x$_&*i*(,TDM@: ŧ<ԆBpSʊY}łH"IuK"PMdlٺGJ2 z BE7DshO@6w7֣BTgrBЫk$KMKoDtnmBA1QZ)()@ud $"T4Y8hOEL+d/ꭢ:M ]tz@2Dơ0P|C!5F븆&h|0878`p 08`p2D*M/%V:)'?<+͵`uT5iK.dH '`R 1J@ɳI7 @@:C!ɦIWKWKnE6b/jբ7lJqQPvǾ=(*Ɂ76Pt'ܨ?\UJee9VmB@ <U#=e F}Ѿ Ete$FUQϢZ*QD;pb%͍n&J˙*nfk&VR&nn ԙ`GmRt}}:2 >D[!3/GA-"!'*YSQ RO΍pp ~ø⡈ ͇\ $[߾rzu5{Z7]Jj]Ec^ `§nL=rJ˕#$J'Kãd/>_. 9{yTYɃm- hY@`etWM [.$=U- -ᵏxQ8/\ VʹhNW-AZ7#7yzdLƕcg@^/6w^KƂr^ [tϾ9 <#\j֩7Νβ§~\Ԣ= g O3QýS,@T=E2͓CBB`L6*l[VH\@EJx Wii 2=8TIm<d>$]OpH5dE6=_OGT @$|1wy @Hұ{%"-ʲ<384 HD1 ]׊w4w /_Jq"NT@6x.J 7QGu%n PNUn)fFɛ.tZjE])#W w/7 k6`bwuH/׵~` ߒ0:n၇€V~t /?\.?#lRuk.Z|IOEW>i}@[T3H7KQYSk\640fPO ͆bmRxLpw){j.-S ϨzsVc 6qO^1yLT3jV!oeS003YGpe/fӱ{ Lhf W tlvIQr}0(woi4Ka옑zj}o9 >pēЃׄ53VXXw@{ƠQq NڀWG}ULN/ 3a@֞HZ%n7>srN:)UDRM(;mI&1%RnV* %Tr.i'V^@9=Ȧ}Fzm/ gMǪKRm^U#@E `oh]B:($h}9GCwz @d]tS9y(,Ce`ir\B 8l?|:_MnyhGDxFn,J:Bͫyrf ljGЭkwNdoNZOP>Ǐі8mPUJdsIƣ`IA#@ƉtoQL&g^Ёl8g,[ '$ [g(Cz7v ~#f`Dd4tZuSoG5f)?2pRˑmxDGtv͊7kkޚH}ץ%y$B΃Br<̑ݲǹO#u|{*ΦieR;nuRʚƀ3zR%g" .톶3iܨ79ā<9-۶=g2[I=n-.@fV[0AG^~Vhw*Vf$TRaGJtIndNی7)rӕ6q.>T=hi8{$^5>&58P#itpFIPc)Hy'[@IMM gn-*Egf+ZEO 2PQҚ.j;{HIK-2xX|!UPthtPlT,b -9 s0BUxE]qqpnrv"ŧbܔqp"r>0!70k]ph!vLָ\`,tK'`#ޯni](cJwѮ7c]NLF.-5ؑ&M#&ԍ[]WwMw%ᖾg\Ɵ>D]v)>J,}n-5;r7w^(,Al;/mw3iby}i$Tp>W͜p$>yPSJ+D|hå9#[bc Jɐki6PŲL0upK!7}J{ŽStyZP"$?¨v9s>f;NU~eC{6βZf 4i:gZ}ik jǜHkVT;q'[ξkʵm;uG7eBnn @T YhN 6uV){{//E}t~э$>=;RVKq,y.~]CX٘utU%%8,iRrQ=Gsa]pd*DhQ9BUu W9S/ÇQnO}^{-,Nna9_.ſ0z@4<˳ywa͍h͊>Q Ǐ?nxtŶ. Gn>PpٌI0}(.oHRofAHۑGi.CǍ!]n~O/!q݄]unmRg[hT<.f%s<=P:dۃNi?I4݅jƗmjy1XAm!ﶚ@[~;sNyGcd'6%Y0K1 Xh!&`|7.»۸!H;j$2G޲:eO.(6.jE5+JFP#vcƟ֬e>=V5.͢Ye}P;NC618|l Mm4$@.fmp کu&L_5Z6vP-Dښ88ѳ`T'o3$u{"̛*Lo\^'Io@qG'T[U&o_%`ڧ}~V݆mWZ q`˯x?[{!}q̡ ᑠ ~a MIqю}.`k4}ٺʸ1O 9zkzG̽P^|`T|-H6uOUOrKinjk/_c0fpO`S@7dEa8YcW0yMࠀ*`Hvo+>o=V-_7KR"WsH1z:}b&[.u[س+tem5# 1R UBi:D-W2Ϛ-KSmnayƍBRfFmԙuVKߍϾ6BtJ%K 7qaV ~m ÚKj؏ '78H/S]32+аhN08^k|O sL ›+*qm(eru.A1aѿ6|50G鳹q!8mf59K5 #$3=yt,BMLēM?[؟@ktf8\~˽վ= ILn3WNӘ3Eհsg 0 z s?hs& G=+1;ӮF 䝏j8`h=m[-\5BҌGƼ% h4+W>SH){D`9EQ֬YmeJ%]|l3"X{ZQ/Gqڳ`̼jn-žتs9qN(n-8&-@Xeb.O֯  Y0M T#K;vT Y<\*B9#iq(op.)fޅ/+R Ee+K_ 5;q`AHA]}psq!*Qw:iiRE^JɋJ{@qY|耡J|8 !4ҍ6m6>6qX :#} I_̐t [1l޼ ~0A9n<4"`ud+o80 s<#vJ>T`cݿE6x0ۍN.\r1VX0 Rq8t _M} hL-Q2OFdj鉣{p5d+8`?b l A 0 z v)OuIbHF cagf] a!'VF#IGajErڶa52`$s LJ1̝cf,ݔXJ k?=Jr dfb/ˑ\>Ǡkmp9X^JSc'o ԇr$#;}łXOߣiNwqtCۖt\#q٭sSngP榍*^/5w*n̸̬x|ׇ@,Fy+ ;lNZCef2Nď[2G)vs|ꎶy/c^ws#zDx2^;@ۯ|/8A,lKGDsVkZ:ؠ=sj)n@6k1مb_6sk̹+};mtkDM\d'`b,;A|:q]1qHGW^{vm 6'_'^=gnz;tTDڕwh$&̘K۩LY6,[6 2<56̝RJ:8}xoIL mm }يN`SB*ºF 13{ `ߐae FW<^x[gn߀[KJf1EGvKP/,mF>^!tc!KVah׶ |?>?O|LA79ׇ~uVagZ--sոfi (ҙ\TA=m)]\MY ؗ Sl\;.]Gd]ߞk7Pcn+ٗ/(tCi/)FsexSp?Gȭ! u͟~8FW=GT⡃^īW a܃ҙJF͚c٬ٯwp_>$ K8\fy Xq7g]vjc_+z/ԤYwPrMxeen3]˹ JMaF$KKӫCBߣ o2k2!$hzAl>*޽ \Ҧ̩WL rAWˣjBmu?U(ie/Fcy:Tbொ:6npJ" ǘhP+ڷ~ wcrucޡ`͙дXvuP^{_5}+_p S"Lu#ߏ`_o@.K^Sg'=={/>>%llN?S4Ɵ㫞?R\={vLL̛%5Ʊ,e5C%_Z FQr{*&Ĩ7JlW_jzb 4Y9쟵V|} k_Lf8kZS],ú0߄oV&b֭1N7Ă;'Kz5mna܇Dum X^ʓ:a) S,ݼ@H9C>Cxj|LŔ^qНٝꋘ쏒e<8oj-umbz{,rFEl>sh{Xݪѱsח2rU,Ad=`!ҖG ިWZhFur7wl+$fQM;4yRL 9QϤUϝd"%2Xn8:Uɟ:@6ɵ忇AqI7:tҝjisͺ5:ovl5t'!k+H֐y$a5--ҸkZ\.rSS<.+ѴZUZ4v]r0dNj UkkiHiU R$*( F(s%{q9zHk?R@' nŠN.>W_W7ŗ5 ʑ]VS-UMX g:?,›vH1%f /ȵdX,ۆ%|4i[+P_ \M:*T1ljի>hVE{<4gY#Cd3l\Q"Պy;;@6IfkCErg 4gs3]Z#ÝropM|,lUt:PFUT3UW+*@\UaKZHT67%Y(_NI2 .*KaQ_~qj1(F B5QCDwۚauPz-G63zR%)OHTC4F)6d"~&^KY.ƣ2dShύII8X#ŃG+oZsRm4R[6WCc3, qjm67ҿp48@|}_+g|)iwhPvB1\ƂC{ @+сa6#`MC!L6NOaA! ^+z#U灶IMؤPK?Kkwڀ@IDAT z5Gsx[ګ6Zuұ?Mb.J:ȾԤ4LZnOd_ ꫱`CKݦ'=jITlE+sݟrzQB3oJ//e6zW_x.Du-'Wkm|Xa^!{sʯfո=ďvi)%èKvBv$^9JMgzQB)(qR[*Th/zJu^X(oOy/6?J9MCg9xOK6p$,YEe["#gp"zqy@u4yEx|^?LU5EXɳV.:uGkMAsa/:QF L{UuuO3qw@6 Fb}-x|nı]Ip4w"~~?8H7?"? >dcMx?cS>I;Z_=[5~Iv`u٬xwڴYPb";/Wͼ|v]נg>OKF 4c,Ie^^(Ə3 k[]oǼh&T[(AbpUʷ0-AwdUVW6tȝŷ'kV4"Ƶ~y<dVTmNo7V\t,)6\fTP W <к WU3V> \'}Zq=3|F7ǚ w:u覼SDjq#G s@GO @_4zRN~3Kuw۸K&aܨKrA@\+n0EER ). _pEW0&FyZx~zJ[#jSAMJ (Q#G;{96[(Z5 7Xm.SV+ tvggԣu_ YT+.4H6V?]2nt@MM#// cjYK }ZP(@YX*Q3Ղ/7kj09 k ?|ͩѮ#%f]^zl&aJAam0NDGkH5m`c׬StMQ G Φˣ b4,$IHyO?Eԩ04MX1sy qڵ%c'UPҁAa iǨzNR;Ώ@ .f#VͅVf6?uM,ۉ|EZL-QT3 6OKKӧ4xRuTѤ-*eNܔX72LUVR@MLĉQ휬L۽ Qgcy7:L(hZHWQjLҾ)ؙHɘm\WCW}R%pP٭q51p^ %Yyd$,z ܲꢎ7n^/D̛S v,(xY;G#%bڈSvׂw1 чxI!DR( #su9;Kyb7&ݱj8lfDWٶu޶=V_lX-`H Wt`d[eG;{DPDڄ|J։lK؆]I 3ˋd7BC[NhkA:Lp ,oj.mSLG-{qg`ht;f]/Ҵk4\0lzD҇_ b-w ~bC5mI P/^( iPžj?ez֍xީf.OMgm6Mxt5yhD!ּ#:ޗt"`i;W 'Ć:Z]VE@֫+F5!)|cLʽ{}5g~G>~QPx4HvF.:ޟ9$܉>¥c1v@O{ ԯuǸ #uE|y*kg^Sz>FW/"pI vRǕCYo6pn&5k glm/h3(=G3?tTG\/I̓@S#Z=3}MUs:A_ %Bu>8S/+Bizs.U9N Deн8O=_am\+9}ܷ3yZdS#[ӧ`[N |? ֌;#ZV }w5;5_PNh"xaЩNclٲAΤg9X6Ool|TRȃ!ExA3)+d].ZJ4FTW7#eh,^9w() A7&Ms9l%tZĽe;lEKFL_Wߦaq/wdڻm ZSL8j5!qݯ9O9i7C٤տ68\kεIj6⒛`ЕOgl Xw|B'~9 }FS>rI1LDG%Dƭ7.1pAl.7qL9s7BG*ё]q#QTn^GjÑ:lyy1%VĶS7Iw OG;R=mkeyAl|Rsu@Kǜ.$4Z#rӓx]z҂=ٱjۦؾx8 ;ա:qGZ\$nz &)EN4΍5K̥ƧK8̌ x1N5#+ԙڠ[*EƏ0bG t0#-Mv^ ƷOq?MB0r֨s o(HLWhB'Iko)9미ӝ?{>&e8(YڳE/*@qiJQ!ȱjee2Q+(4^݊KZA@܉J`hYI9yL?E#KtB(efжZ JX+өAUP q"+Y!a$]9Oϋj7XVF.x{(qG| XF`IB:e e(=Ac"G/R9YӝD8keYr2Ӝ:MvٲGWhMn? (Qʱ\t۩b!\1#yr'nTBy"l &|MGqV&%FR?fj0&PQdZ; xc^>Oߎz]%&v~$&x!!̇POg߿7ImP•o>橒dF%T57_R'?Įa4!]uͼׄ<חÃwLJ'4o 4ƹ]{HG"/鷀xbJV('Z:;ASK1γxlt%ئUoNγa(WLע]%:+cɵƲz0_GE-9h;'t  .HItyECcuQr=f7?žC-tGȢ@]Q#1zvlI Ry߅+n>KC`,ޭ8YiҜե$c̳تD_Ƕ|[>ຉacb{eoŸyԘ7Pt|7moϼSrFx Ҟ/N`mHO<&.kX]ζq=vS&-݇`vCFHf*d^Ai :Q3"bb'ש޻“xf۸ilVg6?ۧ^)CWcpϨ0m1$|_|M%?D;|6.W ;'%c6h z!aʯq[+|pE!g'X!Meoތl ~e_]I;eԆq(hKp%;:bBjFݓc<EfOz%UM*Ħc%wBrE{i H&N8q8R1 h!E!mݻwEϋzSΔkGϏVS*"'^O=Yh6*i,hG"p2@Jѱ/|*.mbI~RŮ~iq Rw=V{ю ?kl Ͷ ?=MINѡd+M=T-y)ܽ:kG`'V/:lPrT=C.~vk>51+^wJ􌛣0q>s]@[x ^NW?Gsr`:}ERBF*'wu$sQ$L[ي3S.YbG 9HG=ҜkQJIW%w H#9:pX$!$ ;@"lFDҁ=Puq=ZhO;s 5@s"qPDݡž{9K̫Nچ?KvHX\Jc^9m{/x/K>T!e.$.T݅YVيұG:ҕ r-&|-vz @{ʇ{-)o71]m:3Hk'>>Hx:@]UHTI`WyY38;9P^z20s||b_8pel׬ƶlu"X)~]G#.o-ęd+؍_ I.,O+٤f zDkغxm}ql䆧B0xXC{] 7kVsE(ӁlG|i (lS+ϸn1J2J;HS" :eblPWЁlekJTmOU\`Ӟ#/(sCM>ZFѣ1e d5 ˇU*(c%iiH]H\\9r$n<Ө"ֶS'n܃ƻ̛&{%MBB͛㩧rqضmfLbg\_Yj$BDrfR~tz3īh9U5E~ GEH"l꟥S$e= xqԗ(e8GHF)A ֖/_Ilg$]qkѽ ?`iqЙQ}aŗ c0)Rx/eZWSL\ٔVaQq+?+>öPUOϊ-IJ DV~GOJݻoJR_vNO~jTw hvIF*ڗb2Tg#)q vg60슑Fʗ%6Ŷz m~(ᇂeazZm,9 B՚|+Sa VfpͪCt`iKuY޳=g JB(?_xYokMhֵhb-0 }J1_r6;vBi5 0{}5뺜9T6dhS8uԪ-Z/=J^~yW27x=mn>fNsȍ4yH4"GEӊŒ|>]y~{f=Z`DG JڸѱB3Mk9*\65,,(,Ƅܴ uѐN?Mp 7#=3;SM_N"QbU@8W ͋L~|ym8#joԮHIuƼ::mBdm{c9g32h B>=&o ~w,}bJnWGu"pxpb!i.,޼{}k p>ݸv Nj ŒYц_$7ͯKVbUjk!{ $&Sԏ8ti OjHeebբ]Hk̮;Ag.sK}m?*Rjx\kg}UG;y?W᫯:%Ν5NF5-hIc4]I;oG@9s=y2)4dsHٲEV\9kq1cc@@ cFG'Q2yuN_%ysYi^..-c?mT -%d=*kV9c>PVe;qbzrݴ*z#w I' ~~>JBH5A*wJy h9U8=h4%Ъ $cY֋ŒK)JСr`ر}#|<{[+y+)W/w:P$}eVBq(?JNAH4m}ؖ@VnVh >(=u{z2V4ZM|? 7`n~y5 &Ge!-S;qwFJf]п4Y@GmǤOB׬rvXQg~.qaVMyc3nT3l$oj[ޙ@GYk}LBBd_dQѺ墧[sO=Gj[{ZSkmmKrRE!2v&H$L`3'7H}eѫ^K_M6쒗~zSߎ(OC dJޗv:ή3SbT/wחWsYc~-R23Ǥv ~ּ}O}e}X)t&1 ~l V40D /!+d:v@PK@a hҥkhɯ8qwCńO~!Y^}fEV];YR .ܾmŸ`(4,ÄDevj(ɤbI Ǣpǹ_,qŞZeqF;4*h# `660PL_5?OJJdI>pm)fOuI"EX#(-0^  |ijbm:{ Vu4lm@42|ts%PJ8#cq4Ah+yϼZyrfY&VIJFPqIOW (C>K=v[CJ 7vv5s%~[)8Dv3;vl*?i?58c?z`KVC-cbY X/':"1bB1K 7[UT$-iM@eKAmqǹMi)pPL*A&YɩU-pY0C:*6*k:+DiY"-iO1#OڒRXBm 77۞"xvV>r4%L9 7ҸZP[Ii ? :PJ@ ( $Srz=tT!q}k

'/etp ב|OcXՀzoㅗ_ur"4iP- xԟ5085 X$X50KKEV2M7r|/d 8 <sũR7,4U߫T][J{j[UA¬;ᏥаkOk]Hy2Vj%Acę|굔L-nR: #[A-3'ý0Bt ElĐ#H2˜`_ FWΠz%3TdeR^ltJ?yt>w)aЇ7mۈCS׼Lt3l5GjL ^7ʾ|ʕ'#ϚASO emطkwl@;_CڍgR+5e rྃ`l䶼+1.m &Ji!&h±.}Kƚef4[k7 ͻtWCz DžXxpWߘ^x5X!'-=YX0j'fނ1CC{GPukjfAُYx[ۢZ`um@V~TpL-*"_7\}[bjJw鉖`Mu#a6/GeuVҙRv^V&܌*QA%h ~vN^dΆ(yY7C!: 0r@X`US4,'%[nHgl1_Mġ֡cvJLd_qyz8{͐`hl4hB .c9l<7oOg<^k ŋB`'{ h\Ukk/*mэ*SBqАQ)%ذu+ D6gl<H2cqg:xҁSXߋZpq $Ox:hC~YH0OuYrd.Ų)Cu4}!&( [ 2V{dh #ݴ͌2L.אVCm簁/umpQs"S 壒dRr7G :)cf.#NJW/Q`%}5#7xi;N$s)XF8lU^/i$P(FebR>m\~`GeƃS?WUMq&`GϚzT k~ Zv׿m 4wR!PxU71Xo# N#*7TNk:m87kh-gѶC{řRA]>U߽b>.&ɀ\$Tq\_M&ō)KJr1c3X9y7z]Fg e(}}D Fs[\B!MiLJLCBbvRv;ƌ*u ńԔR lGi_v 8F;}n:}_ѫ_&NFU?tҐ4H=g-ի Ymcp@Ryر ;V$m[| F~5\Z; _/ƞ-+bIz*H==Jѩ[_v̰ ®SorOXSsRW8W/c]iB6Cc$$8.4hPBj#4=`r[E%r?\N%h0Ɍ޼gӘP!Jr?b6Fnv6Ysھmg4طzO3}kݹ?tyE~Ύ.,ۗޱW 3t::aIFeCsrߵP'm z{kSkC{\T(dq-FUFUoêJH`*)y& WߠPԀ_5@K1BV5fK)Ov( E1H>yUWh=vbm9ˣ.`& F1X([USc{4GM0G%njnl*pr8R5+7 dcP0ap~)%>,թr8qɆҽ?J  {79iշ1 fA${C dR29/3'0#) 286z zϦT}>ןr:CG*kxؘ֮_N+/ ؗu@k&Fط7Ɛ omת[{# *+t yiOk13ڴmk=eZ T< aԉVʖhKIk5}?n}&\S K&i Y-m[TV'$b!s]\g@D֐//ֵK 6i~TP% ./k_Ć ഢy/Aǩ|%8 =Vu׌t c0̳3) F1i=~g8y2X>=UBSm 'auC[CI$uA׶wYcy5%_VۡFb$yΞոqNtW:tćE. G7_hs$?d݊ͽvD2Lgrlo: Vq PrE5Xid8NIb,!\@>/~I.Z|1OlzM,iCi'}HS\=8~SmBoϭBUebo+s/;-֌Cxg1XpN:"8(PRGT0m8:~oމ>m,Pyiq9.G~YS,W.nCO=U;xnZ!P[θݺuWwt|^3#鿽" E; " ?ԹޘfKg-Zo硨3OB2[4|"JT< mhlںnB xZ׮}0n_g|D]wV pw7aܽu(u pAJ-aCѫk [0OmPT\R\&* ÐV&4Ę!RhxR9 g,Ry/ϒ,T@Z2 *V)OsUZ*{Mr_ ϣ^|Uڑ+&Os@rtt–k¸oUyxbTD.'z'L z}Şݿbبi8! M#E#EAk`1o5h*&\+W_fz.%r,LKJq1!;׺9[WvI]bLZqL27.bp3Z@XMlj4o像pDp].c0:1>K3'p̘Io] ްu-/Xao댤܇KgSA3=؏'|lcK[$npsƸRVd!\ ӳkQR-mutj;*H)+sh8ɱc;vA`P'k>y!}Ï󑝕};f~3h) EPV)DWK˔8ڱDJ2QYOډufv͠ƒ@lhhYWC|܎]#1U5 Rdoo:'kFVy梀SdԴCs2,>_J 2v24$H=zt";?zH}ܱLֳuoP-*s8|o߮c >~Z!~zzIV%9L77A Ooum۔ 4wS^O2y'a`ڸ^)Z2 2ˆ~U)>zXbXO5zlm~QU~ge6*89Y~ $ɳ֌g.y*YʸyP 1L[|:{v|Z&ge=E+e˘!#UMRSج[g!<蝹+ R[H: ޞ=d<䫺zhJa^D[UTd8)Nv= rrf>pw:\{ Ʈ6i}sl#…M[@O% mRclamqZo<} See>W[{% cXմq:<=wW@T;'8ފlӻp=^yGǦMq`.UEmnzR~˵&OzR̂SG 04L,F;i۾mE~a5m^xMYI/cS'{D]W98ЁX wR'crZWX5&HOT @*u>0=>pcL3g]sdY㪬A7uM"~^/dК/UɿHeda\_MոO-ާl*ZZ 8ʋc01\(Iƀa+2Q;"lWL`NnJ娃x2A9#XY%J֖4mSH#+pᙩ Qvѣ)RGKL㲺CFvNSd%aѢx, ys?G+j(kH3A^gmPZPKC#U88.\LB>|(Q8}$qK'v*rW]s)W@.{aʂKuL*bJYʂ4xyyƶdˮ]Ѡ:4ƪF3 O"sՆ;aZh?X 30čl˯W>.؀m00qO&|۫Ȩ0 WSo |{ؔ\6w t `x^}61͆fhHE3Λ *+aorUmAMm52w2grKa-\|\i0>d+5!DUf;W+^r/06^aC1u,HBOD{ւj乻3f-J9R!LPdJjq򳠗ކuZ_4N5_bt$ב@}T{,Z1tJ)Cͭg'a#\gP´)׫U#5/B;-٪d*CJumGn_>uj 7+W-9*, .$W75ȶ! [w[:OI_Sa Ѳ'[OQ-7Pf==2tBgH p/8! au.ׂq,%ߵlǤ)q-/ bwlal߯> sp:] k^XRVYd ˷9<r?' gO—QH8 x$'͸"1hgwxS cgj!Y>i㏦ .G6O1.= 9nXY;7,O%]X[oc&+ј/n w{ݴ~=y$?8zY[N;>d֌nmO=Udlj;G߿Ý 7SV9izOF4`npl(|~P(..(d.Zo&(9xx^|3m+`esdA'W3>\VWQyPތ it%TӑI=|ւ㜯~F. ګ:,NKP`CLLZmut  >J;9ïE"1&qFlY(jc/7`^|ac&Q<+~ 9b\:@!~NyĦ}?>(aZQYÈo'"I:ʅ{@%g)iqSdٵL_CF/d:Q⾐comNKVÖIC)~:,CC'9a*XץpE{N(FYC{y@FE!@O?[ w+a'z<җVYoЇ44o鋣{0Gǫ0G6Eޜ5o:($c2rۜ!*Ԥ|c܋TGkWOĘckV)G{l5(L2-:> (s#df>^:㶜+`(5x+9[Pʓyz=<2Vo܆?(4/1wg8`Һz螷r-kj2Y]MZ࿮P%Z~R*:]+\.c5(/J<)wgBe(k@k-o,, )8q2WFt9|շ 5AG5+hg"1J {sܨDIq|Q[ƫ~m8L2u8V;{x)[;N %-?{>"s-߶^/̓{+{/Ibwni~l'/JQ/}6pA.4᪡)B:'?0q[** &Uz^K]u\[VTdѾ׵׹[Wnǽ6]-hjhjhjr 0,20}܋ =4c¬~x [X~x)+L)#Y1E7]32r0flRPf2%]+!FGWOwŘ]:w|Jy݃ϿJcOS)9[+r6XFkTq;! `giG~ ^|)Jp~(ͺǧ<{v`xb+1 S}ո}!!/IY ! d.aR .r^3$˒߷{`_U꿑O K`萤UXp쐧VyJW{ɣS3ڦ/$uXgNFY)L%kSBW*Z$:;*r]-\}q vɅ-׬P)Auq!FV5'XCV]5 [,h3XxϿ x9ZFPf ke)LjS8SR1f평lQʓzIޯ:+n=G1QN/'`M;1 U-N%dЖe6]I3x{hk߻ZNCʽ(y_ro |Ǐ`,^|_4Z9HsUx˩f ob/sqG(d}j3HNOsQx}< Ye ~Ы-U)9s'k&=5ɩWўJ /_[Ǚ)w(.]J˯߲mU8ed^-,G>L~IUd*YB5|(1EZv!OGǞ= 15,ϕ#dz%t/u@&L֞BykƸ *]5Vg;Ďsl<6d1ƷQ(QXpcXǡ8YÂ>g*[ܛ|];A8v/?:ACVQ#SWM'Oܯ:_0QVֆ +P]F)YY~6ξ"t c∶fB؃hFE A @u11p4 Vq AP:,C5: /ϝŌiP񧟢{n˗NFĉHtъͻ%3]ɿ"Iw3ݍydx>eNYt2Ñ} (eXtmsCVn>>LR޲X]u7nGq}IS+ **ϞE{O$:Vcu>~|"1Nz`B2zGbt j~F2oN2~Z+[ΣSI ᪟>Qh림sG%T\ \ftNS[$"I=;p\bb biTڸ/bbcuS X:sF9N\WKgb.ͧ_A1oWذq3w 0j9:uꤾU{|DCƍ- d #pt,ŵbRQ%ßO]S_GK֯DMTTx+Ptnތ%_jLQ׌r_R-r?v]GWV`ĹH Ol?R6M-M-شb.(VYFvD8 BmBy q4#&Jq 2 Yq^?YQ&믾qOg8{}lI¹K3=c.㚉|A+IL< Ջ J:zLQZS|oY5dhJol ̚v035?gI z _C~~!(TmX42lWw/oJG8cm2'z|LVUXe{vwfm!cmI-7';@⍛7koZzw-Mr<)LcfQt%Kʇ,kHa6_R2+#46c }><-PRY77x:uEWUIޞ/NVjsUJLZգWYe+({";~":t >[Qkׯw0T]eZ+ 1!j[(DÁτ,6F"sC='W *tVQQ4_*1d0f}g9=XXA2߮Ǥ:iƤ3W;"xKRى~~{+L!!Te۲M U)1rhe.[|%NZI9SHxTqŊhtE1g^رe+يp5BǐP*)ؼ2t9Rzpf?PUY?G/զ RO>yUtњ,f{zA"&Ͼ6d,\X_*MA}٦'~^G 7Upst/.vc;7z\%<ѢN>EKaiP̋tEUQ6q`~giv>%pprgLD\s):Y?MsG>:9)2,`Uu VjB?q?[VA,5z9P]saoڒ/\go* RoF232p ڭǯ׮;z1Q%TʞϪ'$m˱af8<2/}hOՍ'd^ذ0|ﷵJ+Ɇ!o ؿ*M"od$}*cUYHK5^ g*Jc#XQ3q+L!a3i ޾|Un ~덱lo~-LSM .?>**?ižTM-~ FT^=5z+O)240kKKfەlټޱ`!Y iӦqDk m1bH>p7n~h^N=:%"뗵($/ͫnޫWL%_Tj:DZ? +H\ctkGgxdH4e+}UW2(럛l儿?O6&&u}0X[xܗ :5Ln鎛ZZZ_ Țg:68N1QLK2H:x>CqaD(L}">VXJءC;4d(q=cǟ#i,+P).cƇ+jjAƭ„L&C;dHE-6)$s[F؋ 9XVm%4޺UUxEY {Zk@v^|1yj(ɣ1ٚEF/?R3^$(7醎u}Z%S=O}'{"ɢYDؽ.͐F2, 8>Hx (ؾ DIivsբjB+>C?Fak{SX~j,O@VIiɵua j:c k3.p rޒt*v/?G“*sO7xhjR$gJVP6P[ːzM&xA]D/[ 8w>/>idBƋUޥLVi$ǤԤS? H])ߥO?ߢ3+yĩ~ѿ_\&f8{ V5?G\&;uǰqJL6N- F0% ΘOJ 1OOJgv{yQ1g)fo&w//!5R9yVgcٔ3o DoQ3(2/=;"m3#P t: ]Vxc@H g1p ޕ${WM%~;B+輻GЭxy|.XޚZcG#39ι"!OPg|9} A9*S7 +Щ{t  SzHUY">^֧Y_́2Z`V?}xƷԜIqIxء]8 Gp$*F}͓JV3̺ǟ^,<||=zgXbOQxgqz@LV/o~B¹xxJ޸pݷ'͂3ǞK_E{FgrRm3οaजnffKAvP@Fn*1Vv5/7;пwfafL4)髝$_HHe~t:24У2%N?.dqzd6iFd@BZ˜`ay{4#0=d]ŝ:-)dS͂gavX%Wgٝ WIj?#r?l'NFa޼Oh}c2hQ7դ^nʑ{P:ƫO3'sS 4oV:MƚU+Qp/Xvr )OM1Ao|[G[^:{b^Ωo;شZ࿢ 1R$M3Tɦhjhj8|Ev8TGTGC72cS2 \ĩʾ`zJ:~:.!/*s`mR }eNz5| L9_e'cŋ/f6#>ѳsKap[8d)eu۷F|ƂFo2X|r!eG!7/vvY}v=r-F3 >2Ye_b1l]oQ0\łkDP@XyP F=qԁ 9 "^6]yZ9>P\Rg*hm0JnƵ3\iTJiKrJB54TrLmĨ"Iwz>TRRyV!((q?E_J pQM D=w /HGvlډ! CĞepiƷFWo?tҳ?,_E(>KM`'`Koy+Huťs&|>>z ^Tg3۬5M`j|oaεxp-~T;Xehvԛ),G! 9WG+YW%^@ǶA G}))}y?=F8J[[bؘ=U.21zbYGUtؐ8~~aW:|aORR_|`lw08 QvW6YJ)9>mhh%Ő)+|߅=My`~U_;Ѭ1pi<&۩Hmۆc] Bs-u11FM}~e ڲcͣ}p8nW3Npx4W;֡ψ ()-Ws]5f=LƝx ^{~ t ܽx]0kJ~r '㼃”W_`s\6Ʋc5d Ke/S[O 8ʴ"گk^lXj' [z_ ןOϠCG fLрyn'>|:PܟBvǏG*[ym_ehߛ~4 k ݨԔ-zR|9w.:oG|t6B$`X{l7_|IGFR*DF S]1a<7E;RÌmccyZn,u#s$+{a ,Vë&u_~j~VeErsMZ-o V; U>DM5kjhjhjH8}9I'@f>Y)QBdZk3v15҃f#뵬xDK1Xr 2?Oa%Zzaء5`| 89Ko.m(HYUI QTRd7wtn.rAXJv Kʱ7qJ- ofQ1?suBr% =eZPQ;idt֮)%)ILx tńDJJ42")+e r}|^ Uu7QdhC&j ~pQ8b/Ō;>rJVFGi[[OϸPW&do)M#UzMul7o~AN {dj UU5t%ٰvg"8d X7 iȥH/JƅcnuUM6|Ȳv%]I)SNV8GgG'y%Xr|+Th F*Woٍ1 Z^@$+`ɳ0Pi}jҒ{<s 0"ui !Y>\l{5uwG1h޳ca O*IoGʄΞŖ;PjJWA1Jes {-yvl4Gab "OH"]~^v_W_VDEt.?oSJК( lBj}_uB!,{4xp ) >lm/7|/;T[bX_?>{7AŻkU]3fΘ!+'bnM{ĕ!d+^XreN'66U#C *$eB RA.ItW}離}mOuK{EsUN^=nu [3&hy7[P-nLK< ]K``F^%Va_"yj 2vtW7ۖ6H9gbRB2rM |eXܙSH x5o#OvKAdC7̐2gto!ˢ\و?$vg;AAa3>f̜ ???|2"7b/o 1撺JU$sC{vĆ`y\r[T%:(kT}QbIJI%Be{lo--{w^E hktٺ]?bVO579^ޒ[zEb,=YBr;yBbw,*Fa=sVb3~ md{"91Aw2d,,%+32Mv̓QMpp(m/(F m1sghg­ V Q,Q@Shz_IM|{MtSPrb^Т<;E|M61gzyCȤVғg?&fRE< } )H%-ﭵ4ttïAb|9@uK=*G7MMGn-xI ,л콘/JH8hĔi0,:r/jK `on A\V<%6NZ6~muGQy ^,VydNୗ;9 N~AAI7eͫC拏jrys\7wXJw>eo?w.lOXZ!ٙ5\&u0"[y о8u 6>EE`_bSc>k Nv2lĵe\ @7i?əu]+{:0`faq-ٕb!HIJsMlz 5o?5s/8=1anjfMaEܙ_SO=v>~}hT 'M{8T7b:TQʜuCݥ,|ބ/s>vSؿe_eMNZ։@?2c =%v̝/JWP}6q T/vًw7?3! hu){H `U](6g"75GueydBy5e;}u ~d3XFدWh4P@VWM`K*5'.$ %q`Q:x'b 2+Ǟ{a) iO[^A׳5}ט_oAoW=]hic0l^X026f;5S7k/DÛ9l)Ǥ#e [[5kV:ǜݮM+a䫰Aua}9_yx/T?Դ߲wa>z{<(7FA„ &&KqY|g sg(~,U`6*J) 11R~WUIYZ/"*XIj-SHjj9-\! # 6f3O!:: >V,./۹S:TlRWOۈecO::ĸSQw]1Wx\~-nY hJ$:%!$Lȁh7l]+GJwubƠȝKS:wYΌyM3v{WD9.=@'Et(OfN`X@̞<yYhԲBCs3e[%FURR٘ >/R:)nWYb - 5៚&ؚi3aL4G >'*z+,.Οݫ*|+}mdCy@N/#^DiHUׄ0AtQOWqq#(A0o"뱥q n?5ҫo a`ei!pq^_Q+Y8u. XXbQRŋ6fRņW /W4SϿߧTAl=J:JMV:LEJa"Se+ֿ+s;Xr}]LVi+|jii>rry{cOY[{f[0WJtpdYx_uׯ}1;$ZX/,)l5g{; sf +})+nQO_~qS 6'9!#ai- 5>FXzeu($w,B%b!,\nXEIՅ~Od&Oя_)(7>`PN<Gl!Z_͢NSGzՊaʟk}3"lXf47p-hng]$DFp{9&Ca?b;qTX#1\Qё?G.ä/2w&e̽3rajs[?02FDLIA4hz0`MAZy:FegKNr[y "}@ 2K֭~[\ܯ(fЎzwW'7an4P۬M6v>>sŽRW}r8z]~=,1u\ߠSA`0` *E̙ml9M v,4rtt.hn_#SqaJjSҷl%;BCAḙ j)>ۘoDw@q;ߘ0mZز2}U`PI!qb^i+K甂DDn ;;G `$[6_|h˖ؿ ab`,I\KJRvOr%x](s9EHI㦃<7=Sу8PW28)oh`g+nrsXC"+;i3:Jy50虋i Qha{߱N M~YH$i☫ ry[W>_.{!0d폷;]s[JүlZ]v8BЮg E  L~:R ]m =Gbx)+BW61׆V}gb/b\F5]hEO@vnr ~>SW<պP߳n**"jCv*gSJP-nV`d2VB1d=Fd'Gs柒Mi`NsS-K Sh^0+) OEhs5 4TH@nI+V̴pE:p!SO߄ZPիĉ"K-jb57 ܇tzqZɆh:8ѾN^o&d>mf0//Z { XWh$1_dDImm&Z2״5NBهÆH (*J0d`堝⛣2)afb{QPX=>љƶuf=]lm4'<NΕgn4̓{U!Sd3kj]tI-{2 {~݀ttEDmzI!rz&[ݠH7W1)khO7OĊ4ĤPMT0zQO^C!FoHC V\ L=)gXOF  DBi=XW$[ėtO2ji,$1Mi Țe/<$G -}#>OX%EFV^#oz}=(_P>c"ĽP!>l'[P|+"{ucר ,@S1NX2;` %ä SNqW3BǎǗ˾&0?:ϝ=G) _&{CLr>x{bR L" xf";3f2v;vlU%R՗15%YM[Vz/ۊk^1[,^$K`^ #}aHֲ㡭7!sk}0ςCa_ʼn ({xȩ%;:XeF#SYFh/ f `7GFHy\C~mG7/̞+E󾱿(mۯ`=y) PP-ś/TQg"1M+>C[u&<@h+.l_{c6-1)|.ƜL,}z30%q7~-G eŷl AahdN5K'Oy1ޝ֖̃;]zD wl|i&?#;W}Ii[ƍ > *o/@Wn}l/6=IX$C{ś iJL`E{[Xμh 46֣$uZ1TpurD;[W}eP J&Hka ;?o UI2VUŠk[j dNqѷ Ursx/멺 R(oݏvUU.Ϯ'ES*Yy]dzp#XLR2q|" }9n}.n+:1 xe[`'y1CJQcR%ϡXyUv%t;LD>`.T@Kz}GG 'HYvc=Zrx 0ЪN2HgssBBJ.VK^m {4'S¿?%fW\гU0lXRW_c؈R4:wmC)lLRw(%bq_8q\b9; 6zn\j2vcZZS~NtffKXy_xL7ɬTfψ~BɘMDmMn>+wߵh2Qr˯cZm~ [=O|jWU,M,V#z-̖}f u棭@+EɘlE`p !D`"iSh5yHͫm /0i*?zì|Ӯ|=}2GT萭i";B `whO⮔k&rA+C.8!dKY3oe3y᱉hKX?`D!CP*Lfv>ήΐ[M\U9Yf9x4)bl +R[dfZ/@RwF:D.(kއ-'pbu4*RO[oGaA!,_}FXt.!%Ф#ؘAMm)W8(;76K~IVhO#l[|Ž?}~Y_|p֎—fl#' @&ɳJkK2į⍏cة!C('oƑ=0`Ynu5*ߦ@%1̭`CUҡuX PEϼ"M.&}0ȡA. 8|a&2BnI9.+XrQY;^c'a7?]_7'Uؓpۄig혧@.Δ^eT BԬ BV]<0%2|F q|ũ "& 01zGg_9ym JrawXu~Z^w*92Ȧ:߲e nJsj_,@IDAT媵HMJfM6vg0**VS塕 g|>ؾSg΂-02)϶_~"`Y 32h`n/g zE*r\6:pDae!8ǘ@8Mv U)@˅s0NKH8,$UPLb H@@hol[L0lu;'#\Xlr,lA֦6TO6 *H!֚F4N=F]p@k(eU-:vcA58_X[ZZpת^']sZy=&ġn^snנE@sTWs Q>zH@kϥz^Z}c{rcB+S/YS^ -0nڭ#]J6ѬKLSeY c"P`1~a<{x57V6JJ6"P5_(;qi2xa$5(srz!1 ]*PX TUo+J}=X.f_LhABjS-GZ`ŷ?Œ,M1Z`Hxyymٍw7lތ6̫LxhOsX&6 [ޯWۓDRn\QSqa7G\s+BYU]KIKU-)WL&)=F1hSSԖM%y(/$-umېD~J 0>#x'g3F߂},B:z(4U*RP0ʊJ1sd4VCAYપ&T+J7 i`k(^rk,0Ӎ!<6lG!܅L֙5qa+Hc",֞qt<[)l߹+*Aiq!sO!wJ--%U:?!I4 qMҚu#Ž*J f÷O0YdZK:@YɛbP{=%(MUK\Gnڑ%%#R8=*Ig ;:IX'(?H*S8[ xvӚZ_|V!Rڽ.ƶwkawKq+lް v?K s!5 6oksz<7wp72ѶG_p׃|hyaf._ACI?Z>vhf-Ŀ>xK}X^qt˒TT$r)oOr}慽Oߣ{P%:G3k|QOIv& s'Fi坛aHJ3>*) e,,Ms$3gq*̽w'WO݇|iiU RӲx-O@h :~︧ӱOoGؤp4ꁍ?/|)\U^JE M-m{ͭ1 SjF&* ưc }3+[qq1ގRT 667;|qI6as-Fg㾇F.;L1f$eLl*~\5Ӑ \XY / }54X )#H`V(Ce;Qz ŹX7|7(J%&ӳ1r4)K_Nxu_g9)^}9}/oT/[qu(\}d'o+ x}}Q}u嗟c9ر+e6,Qcvh"Xϔ smSdSEap8ߏI`"i8GMYnŽ*҉h&8S_)9lqwft|^A\߭Qŋ S' `տ$?{ FcF*'s gr xI676֢cmYIg_}1.}(˖-ӧ1 ,$)1hUV[Czc5f6w ސ(I9CzF|;}PH} ?`#qX! jU1Δ@1֔Vqlye)}P]߅EFB۪2_ L>PY>S"ٛ ,3`G*)4.VRC-pSAֿiLn (Q!(ʗa?H{S-P:mMyɩ;w@2 _.M##CYSq tS|L,{y[b:8S 8[lFFLk>c/A9Uelؿ^&K ;ȟ +m| ~2kr^'S e7<:* 9 Sk[`+۲LTV89 uTLocI3ϽHPO_zVjfdfKJ~^ٞ)̹^Y¤; T%*2p %9 EfYo/7e+7KN X_1/[j2cV%l $V/]s};ڽV$n=8\2TŘL8=c,'V&ή#E(匲;A0|P5l1&jE&@O .mB'R.!(؇FR11Ԁ62` Êͻ7- `Tc|÷JKY6>y!8p>_/3Q[Z+T[aA?Rz}$G.E /O 0d2A!mgH!.AJXU}苮 F8Jpn"6-9˨h٧n-TSOuW"3GZ; &|.9 mXYY`έ-x_ױ41٪qߺbVFԐɚ$n]L$WIW Ъju}bRT̙2@X>MV+zZAACjýBKVk-$*(y5ڑSTfi 1u^5/wHJS@Fz|sc*"b= u5Ujsp4E َFv(H<3±V~w͗Ņ,BJ2۠%p?( } [ϟ΂2cna1ֺlGfFҲ9(Cgp~3)x~֢}]$mEYSZmGP~71Zd 3U,0A#Nec܈RuQ8FǔFBPUm/T< .W-AjûпdlBcNϼERG\b`cދ 1БVts͖}l@;ٽ}܉gW'+xʤqD*&kegڤI(a!ֺ_yN+fZq}i3OZ0XO%*37ND>Go!㻒!r7bg"FL7zM?._r>ۈ| 7InGWb;~5h(4uR7N٥ nGkWb,q߄RWnt Aϧ>N 28fYs#U.-FAr*0`Gg7/,Eʜ<FZhjdv(,.C-e|{`NGƪo#W}o[JE%Y_tD2/e|/Zt!v״ p ceG2AsR/Zd3Ĝ}8{7:`deņd.F'w1}-<썋dE;bNt[ꪘfZ!d8VQ)#9B(VEDD^YEIvX \t%L y:[_3㒎S/*pqW:,6JmN&- I;A B̙}֧D]} .uqLjjrql G;k\Ldʬ+V1Ӊ}AUz\!ԖQN6{U)eLZom⏔ oia5 cBmS:ut Qj o!]DA8ԗ6_"zܪf}L̈́^r(A=1nu)Np>ܡJYkKΜ< LGms2+[44[L6pq4G\r.>=&8ΜOF1n`(2JQZ7e;A}<_tтu8bl-'"(?%G&w3cU. `>מ B);sgil*@y09 fՀ8yJ9^qЂ2vn@L>T`;*L(t/Ζ }o߈VQ Lbs)w0:𫥩s'Ub>5;I+u}}Ɍj+N&uwf9!Qބ;`OoL? !a`ԋ[}D)U|[1﹆&(E|ycӨ2PqI;>cTPx$ge!bEYF9f0^NcS2n `88K)(!RG9LshMf2CiO<~$-382'_|})L0zf߾2Ul7sO\v$[hA A@]8XaĐR ߶mF(s!gEqaPG*VS^xd1=TP_IUbê\*;+6A)_d"CF_<5)RC./eNN7q| dz ~R_'+H)ǽ$('Qѩq([4+*%@?ǓVqF_<D_-~֧o&X0MPH O0m JƝ1-AV\ `uN?'Ώ=8؟Bp ^ez2[k :ϲs֘QӱOaK;39yt= kpΔ]* ›ZWih5^WS€~^ވG|f`nI&d=}~LQ|84׶q Rj? q pR<ۄ1<\%U,K׭OX֋cz·vHҼri C"J Wڅ(*֫**]zW[@ S[@<^[<`azgP?[9W,dȀ4CL'L 2M1| 75 % b޹ s,ANU3jۘ{:G^8wAʷgK׭)?+L;RE (L)|jHw()-'U4W#5dlX!!<"HYM dFœz :pUkΔQI Л ["dZ葝Q&3G9PʾآĻ!=}?Y>`z)ąhhh;w{US$|4)q t^ <߭=SK=.ڭ<f2hor1̈QQCvM*3)g{E;s[{+n;NJEFsNą3arp#rJ)coUhs+Pl3ˇm›G7Vyz3jXK#)%9W(MkI2yd0/_oף^8DL33(8r#(#V:R~@Fwߏ'I 4-_RyS%M2CMHKK'w^O*Va $ՊӍ03:3 t 5v` T˯-Ł1`[4(} bXwR?)鵘`o g|IUp$5 桰{g 8B/̯ȭ$[T B^$Ig+ ,2ȎqprCȀҷ\ ؿX||&vMU\θe\]ں:i#*s^" Cx?5Nkw`aڇ)||T5a;\>V2HWHL+!up褁&Nka`}RV> pHr UTw͍,P/ɿWA~.s 3gֵ(xӲBL>2lWX {ߐ1rюog>JNsKұgدFYq^.QُOwTp]ծ`sUUPZ'svsgtfTCC%smK` YWo9ic)%ν JdnexOIWYmXg\B{n `nwo!/OBƔ q@N.\TZ!?B_/K pjQƾ[<b5 >nl]}.+{ /$ЙD3a=mGMY1N9^>8z,)䲿st%IFȱcBmmŐ:TX!0+^*.X^G]`A]N^{1gc%֘scؑAIek~P 1߅E{Kf]?_aҷ܂̍sF&^9Vba BJ@1G޵#c˟?>RYo /6XBN}&<yc9}f<*Ȏ *ep`zU{&n6 `T4 JIw76u m&RiőGY@3TRe Y7=\/XEw2c舑R:u{lmz6eepRYk^[ѯw;XU8֑M4GFT')Xx%7P>Xj~XU3Q."bR-pPWnd茛!% oB)&pR7y[#Va DG9 A*U)Z>>ndN"E(94a$QT x{1~8^% v/ٱ{w cg9턓Qgb\e8yAtr1? Aᛕ)Ni:Ğ?Ipdiѡg.z VaDu6g4.r2\+(bCȱ/H&:hv*\aS&8I 7鬅2q4k2Df&4H71RgΝp lg=BpA_f5NfXɐz7G0g£/c]rX8\}$bOΞ-\5?}02G i166{ヌ?@F.&L,toP>dS܁`"€RV۶߶Kc:d'? aNp=䅷a=2iٓp2PVR8: ﵰ E?n#/@8DD:BVdhU2dEБur+Ec踩rJL|Tbyՠ n5CKacnG] `$ #{A ', 9SF;6 FXzpԈ#.!.UeArSpbng}f6Ml̈r.07~M-jHg2Qw3嗕LOT]V| +vYw=1L>d|ϩnPVdx"sKXrj,݇+f-d'A% o#wͧt"Y&`g|ꦈ&}-r9JsӡІP_ 7܍,sd2/c3Peo@,ƪ|V$$BTr;yܨx'zjd"Pc1N~ ~X؇##]v|X@S4Rl+_ UpiXyy82@ Vז#3/6n˗#Y~=fΙ cJ[1J2*vُ)b|=Q'0e\ru,?M`Bd]#A00w!VK2蹢 \tfsӃҹUA/. o(+Uc_DEAva#Q?lCRA3رcRN[xiu2sFJ4A}acaQ6`;1H"Cs"qĺrrwXww!>2C7}ԇ^RI74#M\=8`\ޙU_cE+ɍYL)HbNJrMYeXa@h?滽&J+l Mf@ay=2\*=\[6m`܆vJ`C$AnT_⺭d(%qy}p#SL cZp kA ?[^8g 8p |0rDs<6dVttT6Gm4Ν܇ 4=,ۼi=( .=cxJ<֐[r"c&U=ǞyH1npHXF&!+ ]x]Q..$ږ-u}W}u7ԠJw@ ! ąp=7WF̜{̙Z˖%8ȝ;v:BWk(/*)9~&\vRҺSEݦVސN (҆2w.)NX\k ,Uw% "j+.nEƙLa}+MUdbÖ m4#&hIؼb-;r]@6'2\eI楜'- 56@dYux^m𞩓f;LgSZs/SUW@J$sFA5 TXin&v]UW6+V^=I@ VFZ֩^wXY1z21/mWB};`N6܀ ms=3\jEn]/moW`m! n *ytŸI9hĉ>? x͗FXؙc=O0) EpTwǘ[+7*={HΥ"%-]ʯg;@ZSrRz&[y?5xrNaƜ0Rhl>ՒI@W*C]H>a#lτ}~l#̙6OP:7:`˯ /ډxtDIy1?{cִ xg׈?˻V}r~ޣ )ExA }ARlJvnX3 2399 e3jN=T1aJ|[2( ZGz3AEf"Rncb a!^>n:.ڳWodQ+}r1qR̿"ʜ !@cIpy}ˢe9Yd7D-L'&Ţ2Y #ځoH__Fܙ(8{bptK ξJ=cԘqpՐi4 ^g&k/wQ:@}Gi#,4ظi}M/N[gWe 0nGOD!n~B9̴S[_~ odH2r2eɬ򆹵I &E_",d[ѝv L`-)72L0ӳ}M_d!,_  ʲ=$U٫W2)QMU37'l?{1k:}v`pߑ-%їR;{+ p1h5gу1oL)Xƪ %epf9P-An9ki1x,܋jKǎӲoþmk1[@Vŗwc{Ǯd~/[ΐ@2U.VD*wPnv)8!uWrYv" mGJ7ڬp0y3Ȫ2nk1SǶl؄ *3.)[֕}HF&@nHVD :L'dRD?6ur)dWס{FFX 5CCL4$#۠bJ־%`H0?z73j=OÃ6 !!n4meMA&1T(# 5dZhZ9~jxVV"qϝ8J{#πVUO<m߲_Ok&ь=mP>yU ww.aZ /dyݿ IJ +5cN2i&zA2I u,ﵯer8654KW9T|j91u pߌ$|dbߚr5T.7 4cd΢>3oX@ߒ{KJ 2)bY7΃AxDC)ǝyQO}z{)nZ?(w<} gl,nFwT9%ț$*VDFpPHvE@-jZEElwx'L7O:㓇o`]e։,WmZivҟ Ǥ<ɤ/ҥr0je%SN;T*DǩXWZ~A 6=~6җAvFuc'Kb$($" |lK]E"\,B/(q~g>?Jx-[69b^Dmړb"L+,RU>q낭%^z"} Odܘ*E%kEdEз-{Z`A nu۱{ =!vqm ś7nLEa=|D05+ An,qt >[?)f@l;"c{LF 7s;c۸1 0g6QM)Α9I:x1nÀIz0`{Lz(O}UYݍR^{5 Z `"c.mlN|-m1>0ol SS'ޞl6mCi`;bؼn9zt J^Ȉ `]86ʑtD9ރ;6 ̟s zm ΘQSu# Ⱦdcw"CϽݻ/<8kݙ5t|e;.%Ks[lp69C2=C q/mؚGEں(݇~k^ qIH}.0pRAP'U}seXǜ=d#@h\Pks6G ?W^D8|H#)*0w62/%zl-m zRhd1=uD¤fzt6${ϖ~܅C#%)0UGOCAN*B{ ͿghLi2_VZ-;];#cOg"% cnA(eZ6_'X'yg17 [J`?8~-څ$H2]>]q! YkaFqkd"l!@O9 Po%=ۚ pC?e5?<}&:w&!L*L^i!Cݕ MK~E͜1 L2gbW%?ÔN:bE)ϫcܺ~+^S".*!wRG/&xxŽI]Lͩ|.! 09eJ=^3(S>Z3Q v(; >8-\pܓ \.psU&XZr=Em[*=EXe\תoou*8X(/cYIu-M3-|GȎ[Tر|\<ҋvwNzC 7e3l0N8j&f%&\_>A녍mbnLebܗ_o}wgG-8R݉V!cz&0<]V]GƜ(~lnܨ'~F@wL.unUю >k*12W^`r|-} [@II'60$Ҫ~з-7k0?-"L_+g&!?));A&n1Rs=gG Ep$c2SFPXN9'Rء3 DBaAC$3 <AAN/e Ss[ʽbk9%s1x6(*L 1 ES3w&?K+?cD qs̝;2U=.#6mތ:^x*x6 eRRs`2621cݘ;NĞQ}%)=o AzeƔP@?##E6o/L&O V=L;2kCq Z>q%w#<0rX vbO] !Ed El ,p6);#Нg҂ :vxw;dn^jkutr~)_sPXk3gcH8C֒wAzFcE-U\Gdva6 gڲNSt?d[7̬ .6 G 5;G,X!@d;;xE?Af?&Vp!6f|eG2RmϿ;wOi‚622}Fw5g0̢Y+;h:/5/_ [!$YfV%x$_¦u된ƍ{y "{D 3ϣ!5"eq!7'FʹT;w& (QZNc̨ a`3 X忞Cf%j)ẛI4x!A~2/#bh8V%(!ѨєME6W**SwUCZyuV̸aFKU2֗"ѫ;ޗkN{|w c<>6 5kC;$30}r*W,vdZpcxo wk TuaR2{q.TDrlͅoRC hc/.\d&[h&zQvmdE^|a̿Ɗx'ӑSäg+dЮ8S?: ln"_TT{`J z|5?R߳s+NHLP1CSS,]чl gږQFa Ҏv-#"q?~ LPRZ@FG'"+=QLnI8eQ^?wmd"g~{ԱЋff2-D;_e:t̖c0#= gXMO=ࠣcb,CeLK_zW(fSZu`LrXwnYE&eݏb`*v-M G%nfWT?r rkҙ <oR7Eiy9(Z6LF+xͻ\h@ BXeŋ*uxܷTzmwu"ŠVk{K{EU]U֑>xctmk;?۽0&gRYb~>>ރQ2/cu^z]+4LnFpZ>:Ч-6oلg$IV~ЧZQшvFȽTMl?{4b!*;|l/]SEExL ej=&Y `,jf[ۑO[@~]Rw-j,{з-p9azG4w|5xao=2*Ά K-Ė,U$؍ӧI_ވ[A)Yؔ$0w8s=>,C Ȧj8JgRYe7E2Nġ3'E8n4`JŜNDl)<_EeϿw%RuEy6]9_O!}gض~g墫O0)38}x; AFrn{J?&@u;']'l+od`܄`K@N]~yo&{RkaPAU-2N!wW{DzVL" ;WX3)@vz10YZۄWlqsnYܾO6,g*b ]'hSZWc-@sA7SK2s~{+ǘ($C[xX*w$CSP&aŵpro A?+(,;%_yn/6 rӐ2cpҕ֚RD_SgW+mєz5&KJJ'eruPyHv'QF7a2Yz`/3qxG2$Κ"z=SCC3}8CP)So#1,'S+QQ:[-g6ڶ䶽1a5Ƨ%qtprVG}9zL_W>@]?0ӗ}Yid/#7>B1cqR[n) s"Ԃ MQѲ^ IPWeM2-M hYCUс{Ȫ'hzA+IHWXu*6:xj4e ; ~뒸_U2,MЙC9A42l,ʙ|ڹa}kgӅغs#_y2 Ƿwj5-,5].ʡ(EXk;Yn3ig0܇n)}GU{{?c6*]n̙0!E[QdDRXW~VVYOI6#/è#?BxPYt #SyQWe`炀naʻ2W^P-W ?+(hGӾ 0)҆ziY*7w97LdBR&RO@AEߋ]aEؕrxvܹ ѧSɔ, *X񾑄/!5VN:cAҵ%an`=hjkd)[Z'_ wSVĈA *C1lXܾmtKH3=\ØQXoukUV;|p.r9v>kTE_}U2q:ycTJA[n}Cډ7aFFRbΧ&äYwbhL>_c׊ґ#LɁgxǵJX2i`$kL3᡽dӖRHՏWxZ9vd֬Xp55aT?%R.ړm(PZUM Br^ac;QUN}.fxT`z>zSF9#53S$;”rϫ~\CXWĝGˊ֯R# )z<&r7=< O'xv JEyʤꆮ Z"Ig"+ȶjbRA);)b=N*޼v'b`˜%fL [wƬYspwο^Ow^~ZTBy |Bu"2[Ņׯ/ 6:?`v_|f&G!%1ϻ4z$Ėەkcw,GGwFDJQ7<==pmS( ѧ/ 0xe}O0xM}sv@^ WWq6EvA1 -4w3 Ng/Sy_;W&t2܅u/:O&2IPIQAIDZ䙠~3\ 8gi-Hԭ22]5#ސdrǫ ^;nqB@Uiy]mkԃJ溦}vw0< \adh7K"3UeG >d }>}vހ<Ʃ3m{NdM#>ث{Y"$SÒ+WdwsuP5,kYi˦&HJ@$ T-t& ;IoD{%;+t}p@ZD[~ `ݾc-rؖ mulJ9|0| O|{K#tp8"o?\B0f\Ӄ5l,jꨘ TtJK)\ "S'C=IňQ95;h PP k܄>* JR&Kd#\Đ30Kj9L$1Z!}.8&'}#5>H'g#wϼ ;x2R]y~hTqL i f@%+'(zy8kQꉨA۶\YSQأG'aŒKlYu[f^\:bdZlغ?DKD-?[ Xy P^1o n-vCƦfxÏ0̨Uleb *&ϸw^oY 9.ʗ"}c/rЂ\& `ZV w d#[׮"I#rOR{0sK*?‚lyٛT)/_ *MsO)ÏO={c'/0ifOeXw FJ0K_ʔ߂7!ȿ\ `խKalU?mbUe(\Du+6XoNbD7"m/<$Ru]Gx3\}RE5<ڱ R#HV}e-2ɒ޸؟} [@dK*E]S_*(^ [TVrRLWi\} [@\ UXX_})Ի@dF5;ʜ\|on|K-YE ԗw0%-+ol1vDt!#n䠲ZAtRN:ć_OcHɳdԌRdi= 7LW9axe+VS꾖\/d֪ O(F}ʺ|WH4b`7K22M,|JZׯlQmCOO8{GiY4q%S|+ꋸ曑v\̆fLnA&%Z ڼfĴXu\yVXTB/ S"Oを@ lrLܯ+}d&Ȉs'=C)t E ~"":*o.n{2ܼя"Wz^d$ca6q4 Qb} E+~H9 ^x:sC{}XI&XeXg\YׄO<#Bɧ6)dFxٽA2] \=^uO? d$@CP=b(][m W~FgToj1ND+H>{:KڠNQǷ*k~K@J*?KM`!a3GG&E+0{k wxZ%8Y嗽k⨱63eB2 Lş"޴u6@W?p3fp@ LtaսRȤpJC/8W=BagWr|]$C2%,3Sѣ"mkBF6RX{MOބ>ڀ$,]3fÞQ$%|EjkkOѾUST p{n߭Q 끬trBTJҗÚǐ3|v/F0}F wܱ+KJD'^(aRF SOgHf4x-1UeڈVǧ~ټa1 eĬ%I{zz],8WQ -+gw wJws/jcG,6!v֝GՇ_^R"@t[KKclo{I2\+ gk,hh^=`_tYbޟee@ic~Ͼ]egqK8iYSHe}XD53 N5)R*Ș TpQ*zdRZR9񫱱:Wwm𼉀|/yM `2LHbL[L8zVNE궢sXPg[l+&UkqUPeQ|l/^6N9/U3ᅫAc^kIg/1cM&2^V\ZO;lyxI@z鋥ظpLDu|.Iev0Ta62&Վq9ڢ:!"ߛ$OM0Ŷeʎ&ce)WԷߦ _SI)Y&.tH_.ԠIeqPo8/?} [@*"Iv/%Yd@Jujo+4l)xU_}]jDsm?yYH۱[cеp$%CuQ.L=y,bs3$:RR LHg7<P7*8 j0@ѽDZJ$eeR ȺSGY)',a=CWػ"4&MT̿m.WS@ȎaY^!>5ylaR6 }㉍>7ވ5W\);)%gMڏyCFGYֺywdhD䓰mj(;K>E Z[v2]wލ GѲo GFREEquuA w̞+`,6Mz޾/(;y; ̢dt8GXO y+ aM~:'09'd]k$Φh@ˁV(OKMチG#F>v] P]:J:DM$&̿x6L= -r\IcN|sկ=xj^-u\ `dcGLnYw} [5dca 6~d9+fP{kTQE;t$>{ RBF*|12o>!/|`)5tƮc)(MT2FJv>>d!^zQؒY&,jJu牟.C)#{zv587J aXX `1H8jy%gXOֵC 766k6LLAdf)+^RǠ!X O,*@L .M``}mG(R_.=ze[{'cQ`T]IK{TVF].uV/j% *`5#;5[VzR͛" 9h2s MSo}uD yёz!Ov@H!֥Gp9]pmMYFP%+]Uγb^ZR&ںڑaq ܛC4mVYTT~GNH\R>%qc+L:pY5ӧ?Hy=Ɠmx[o&8C LħJvRjK~ ;~L9j7ʧł׶2}bac&("O#-n+o'W`%U8-1eJo\#B([?zHZ;k4q 1i8&3x^/JjS~@U-֘ 'g_-洄T?#+'aΎJhXAJ+++R;0oҍԽL"ń}zd&娷wF0'(`qSWODڟ~@UpՀc3sVMA׊~O>D WhAB9CpU$aa~uڛ',E[ϘWY߇Yvשo} 3v̽drl8t-!w*~;|k|HLa8R0Fq5DŇ`Z $9 /ev8Z*+K OXXt?Hg"KpZ.>ܴGʾ^j mLPZR Kk6+uj ~ᘟdspyrϒȑoչ*˦LY|PWr8ć/mAaƀE2djm1l۾w{쉮lFJ*%A:RRwRQ\o܈^}MXuSOoyR3"̤l1~Pٟ~,qa^kyL⋺kYȯ]^GYGwyοLD7 l߰IńၔW'i5) "ȔIJM͂lHWOfHzU{_:E݉nS3Ε?R)ש.*;q(A__M/ƛϞ`U"GƵ(8~,{Z9.֍`[ `Ⱦ %SYp, M52ӾrknMN:F?6# ==ppoMu;R,9A'ʩ{( 3Q {?وux|.k)7wr|ꋴzx?TUz|Qͬ)9Rԩ*=g>y da˚jW9S)=peZ凅Sh=~,hБ2rz6gÝ]SU%2Kp+Dq*Kעx =SS&K7EN֓ ֬ D=P>\.[=ǟ8zүވ,YC7]?/>τ3K4V&$x܇-`tAVK&DgVُ <ȃdAyl_"XUXvA>Ǐ ֯ڟ` [ۍLooKX.Nr)vʷixXR)JMH©9qlxpLvrǗ< 5",`QkP؁~Ɲغg;ns~-9vb'މs|Ħ^ AM{DPh[GyFQ, oq*ͭwN{e9 t-}<{3:~l{f$ɑ* Kmk0a񰼫}>9񋔴+ܜVXEaɩ7rr} [Ѓ:We{ae5#A? HFJ/oY{OSQ5v|X9()z%Z*A* GKA{¾[# Hx$rT46Xg^~7̞C Ί@6g7N+K2'kKxt)w/e|`G. I'_n_FU+}5;'Kd5b(yh }G;a@'W$즇h1zf?c+sCY9g1lTu҄񺋕gb#)?lQG(gg^LN,[|_sSR`2i|T>|R N}@=-l8S s3 0S ӻ?'MduJX$`S >55;b/P/dff`KKFfgu1/ed^kȺ%L7K32́qs`9}E2i7/LI+;zt9rD`Uxt`>s}0ش}+.g _}Ha>#GVKuj̳7dQu#h[VQ{̷"K"Y ^{ :A+I_%`EВ qױ0kHtظ~ ɸ$t aZl,cfF:&DU>|mv+%;oqv5qO@JJ]gfv;L&ogw'+6.$DČ7Љyyg1ʒ;VپcC)= xc3QLоriUuIOA2 H+݆dj-7bPG! |.%8>~ h_+vs#Qt&" iAzy&uny{FwZ'hFL1FJon†!heJ]/Jʈ3[xxD nJ|Ji?k};Z]}ү Ө1qʎLp&0_ 0 cŲ~B^i{7_g&erCQ\Vg)/ cFnt)E C=+&O8YL0=6,$9lLwG?K[L8ٳm⫅>݂ae)'vrd ^oZRW"1z7>m J$gv3&gbDkg/t#K$#td?{\aynGϤ4) VX Μ"g̤@@O\JgDB^;,N *󶄄Tz`u#p Ego2x2:F]ֲOʋ5q!M&6SaƤ޴pRV]fuY2[Sـ[¡Hx{S1߯ہ~E~Fl/>rRA޽{R]5*X!Ep:ՊU1~@r%,&k1 LD$Qk-eTg"OgFv6=0׋iѓ.J1XWlXZzj߫;~lKh;]3Ld>Ғ&WGSWWdd$ыg5y2iZίJӳZ[z A%+X%Gee?-oZ`d?+2kOdVbu"#`c3^0C`U?sZ IHXM #6P|@IDATWߢ;h@h~2[#ғZ֓ F(dk"Nz.faǖ h]¿ŭ߭؃qQkHD: A>e3gar(i 瀋:=WR l^m胓M4K >&:*;;5A9aC~˰s%&rM[8Bi3Uw='Xy%ٴUQu2OU g]Y5p&t&ѽVj̒r L4{^EuWMsH$$tBUR]+CłHJN%^I_g./}ܹ3gfΔs^{mg;z ;=HcD!+LTkLI.R S¤3yJ>6"il=|&eG?1U[RSN Xv41*eگ/Ԡ^ sCO2%T-73Ub\8ӖBaQ܎Cw^$kמf~/;;5 <*1k5Њ)\\.hڻWde󟂾y wO4~ 4uzg1B=sY ^';GJϸcҿ^=){VՕP6 XWq&vbnH:yH'KD6˫%_}sbF@4 SZ:0Н`~[52X,%8w H,2YA5%ځs٫c?w2G0y֛o }zv ѵ9bLGD姟] 'i; {RZX2֯=(o`?jg)a@+QT2RKsp֞N^:(Z?:t* Zz}w7p߬Lmt)9>CQWJtDg |) k V 5`ѯѳ6yA >0Fo?ظ8DiT#J:92uy1ѩdNG~@:wR*߬X̜l/Sa_~q6^Z%#0u ޻t!v^nd;Rygh‡1gq>||z(D;K# PPQܒZ }ڀt IS@iA!Gl(~K'(շp,Lg&}[`hf}lr4 ?\ڧ>%IϪl ]B\s@vaY1&FU0ZJm~ O/f& fhu[kp@ gDZ3Hkq9 laAUl.d0%e0fR5VW>&1Ly~sq4.Mn[ӈb[< [ځE94!#O`ʔ{ }1Ϥ))b+e9qv8$icdgMq ߵ'LAi ,uP޻ϯAU[ >nȉĀєZOr؄idCߜx'fQ->l{ue*8>־"A.3g.o>'tk{aGпʙKGA\J]ģ>~~{(KeAv*P,0m"Jpg?'mCat1';%]a\ʏ`c.%njc:->.~^s")d6tO3I:y`X9 `Eua}b31-VلYؑr!Y}nw܆_}  i.%[B?36~5T;mMG7? 妢\Gt L%)g^eA3#HHUK9 Lg< MЛBè7~ƶ'`vIc\{dnj+\VW| #/53H mS SuV+KDeD72=;Da6^/;-+g2];hL4ja3k. [2K54+hj(5H@%Ia@W) @`HKB!g~.]8}EOR>'K膌cXf7 2x픲)f"!9u('kTv(.}3{ΜU;)|RL9cƎ\po,Vxw. sF:udTHls){ĜPD(Y[A<[e em͂BuD#9#qiջ;O#\҇2`M?']ݍh: M6WBna!~-P\X=lS^zuWS,Nj| nFo=:ZO-JpW ԯ^Vl[}gs횛"v?ܢ* pz ƒ'7$# #&NǕLJ CLZ2护c#B8:3?%W1Ͱ[,/ 3 0Om7c-G#>$Nw+E}+`z*R?-)W}MFmT%rXj6AeyvdwI-i3pcţԾS!&;$E~NBtxvoo#AĐ曯OG~0¾sh)E߽Nd S;K; [s#4Rb^pTw>sS=mHN~5UT؏sT 8}8 RoU%)Æ2M]9xy#(>y8GV:wn 5:{FkB@>Sj\([|dۂ%lw˘'TBV)az5K9xd>dG٥ңoǍaҥ;ekMRIdIb#x'FpLרakXt&Um/` |t4̮7NZrPʰrEKliu<+Uu$ޝ-;oZZ[)=}{2'Ѯ im[HpUXU)(,ŲO>Jh8΁j9 9;;۫(bY/|a䇙ÆV ?z O"[rRREUW7wբ:EߤaNx2kSQ1N`' #/*[/DO닏(G ?jxi0gϠʊ0q|c!L=>kGL9GMOrl?Zz{u =j@ =,nDcfeWS׀25qk֮Ʃ"Yܷ?tEE_,)Gcݏ3~J2=%W)bYjg9h'1gwmJ].0w.ndm<7ȂLĹD%C~` +m۶2[ /c3hpl#sXB޾W۹u=.\ND}rUT!sSdM כ:x665U>oy3@ǽMd8OI,}90s+)9nofs_X.m3IɅ6]fOdk5Ow`Jby(>DBk"HGbRPE0hEaCC-peRZ-{ ~`Ȑaͭv0?.K.)W5C,s L{5 Ŋ/d|HǧX-|laz)Z(ewn'+K_ӯ&&>rFeѳh.a8#X8xÜZK4anЄ f_t (OF%ޣ&pZD2Jaࠡظ;tCdc>4Ey/LKat<ϟmdv V50y*2P6ծcޡ +68|7>Dlݹ OŌ{fH%Y1 ˬDid[Ǔcb fX~Z3+]Hڷ?v(&θ^t+z]0JpH0Qbx+AVr|ŧHKuɓ.:>a7GcHrJ .yp5T0`קe\_yxswW’ ~X@&X/deFYJ#9A9&FV =yVX.HN|SG7NPn5eP= ikn47VJ2!f{^!mX }+Y,)dLe9[wK?bb#Nc!OߩrJrOIRȋ&2Hq\TȱH̴]cP~XwuJ3SЍٸ45$kI9{ ?c'$ 8Dx!#soVTPUQ׀5u⹨Y]vxuÐaC\],MbС؍LD1ع7nYSZdfx2)‚Zq|- R'kʘaS÷,%3R")cA%e)O',>ejq/sZiiiȫkjX֯N+{}&P+sLr{Sx Ll޲ d:Qݲ0}8h)B,s%gejOg!3Iv뾹 `?mJ*gՙ@U/*4lL)}FC MF⥳ho,&xֵ@:c*R{m#ʜDLx7_DC4-2߬=hZ KKcimmC' ^gk`!5VƸ0yIBvn6& s 5ܦy#\%b;s=;8#-1!=|)>"h<aCG251ed?[".DHEӱj5jqxN|姨ihA4B РĠN & 19{Nxۚ1r 8-w BvRʖ@)-FJ9G}c9`=N˘Gg!egݻxWֶv_WwS+q(s$d1E?!Zp%GN_@\+_ .%XcjLV++#S$_c@<" ٘>}6 |ZiŕdF]nÔQ5_<ugFY" Kݭac&`ƃK4 X8؉N|"&xqǞGIOK";-R گ (ݼVi3MOyu Z?M؉Rǖ 0կ7(PUOK` h"cQ3W@PMqCR #0=8eaڽw|!3/ &s@/8=}w筇 `c0YK~RM=p]!~TSwy =UO6U@+baZ| ZcfwӵD*H LXKU(f\[Q9:~9OD3(@D9۳[Pmȣ76.Cs\tX4 iJ;p9< PӀLvsSk2kJl~sϣ -K+K  saE4̚~?ێ%.g.f`PBJe$wlQ@q Cٙ 1fDJ libAfi0Yvg3oX=0!L^.>z!*'h%Љ]B jH*.ېLI?Z1f>$´w|:e G[‮53Fwћ0YQ4tz;)7GPB^Bq >^b~/tRFEA,a cy#{ aDF=d@{G"uquCV:ESѳwd$b!d(yRQ3f>..S9VD|PdUb|"#}In[cPU #6u6\\"lA6+eU썠04A %VsvA;{1Yuu*Ρo>NGFU+K%BL5g\\o+^p pjIVhVbYd/䫽"oYLpC^8SQX0ɠI/OOaۖ(#hL,VoCIiPA8w(Ν GCWA~.^c!<(a(O{xxxu=z0WlO,\;~6z Ev6YB-]FFZ+>s0a VEAo/CP; a^2qߨI^D(Af FV̀oP_^M *_w .UϿ6^zm<қ&|+q1"ub6r PZ.~U ai~ IR\\y=?թ寝nw ѩZ5M%3\1v˜,ޔk[dW[Dg@>9'44?깗Гjʯ8ܜr!\Z*NeeͲz3)][ۄ##*rE`1s[1f|{(\J'qRNόFqBU6GfOOėx{0>] ۲"EE˓;ytk? YyGT@[W#VPA~jߨo,w +&f'9nKx{;7zWә?/ sz+;;8PEu;')7}."&~+٪JV_P~ gۤ8.9;*&mR_Nb~ 7# f K2xޟJanB<4Ac'Cm_vr]]wD#dek%juk(elkyzu o׀YIϺ+)־F?y\ZNe%ljSEb/S.k=C6f39ڷNhbBc: (A۟ }_K); VdZ&ǟG!UT ^]q^pȾP=LVHLّ̈́J?Q_|-b¹˸ᄍG&LnŘyOz%^]Qy5fWW_~ #K+H`IO'@zH#Ӳ S":Mбpj qR* #JR7%y+T^mz 'mA^^  QATއL]~|xt$+n =}> v/ٽ6̽OyT+g ~.%88w gc^z<+n@X?PL٩aCc{QV^"FN,sEK-{7;o@n6CH(|Fܵaބ#cp4V! ajǮ:bG0عIђ8 Bä Ž8+KR2K&(?}?IǐUڿx64o[nhh/k38 )}`vץKܝ~x>Ξ"W..=+".Td?Ģf]ːK>kQR$NœCiH(s8\玼\tvǥmp\mCH4}AekMo7$ )z2qFj:MƌCvp({IJF A|AMM9Q4|=d<Ҡ Q1dD=O}ѓu[vūj2taMNcc3Fb.X'm0vfB^^|E8tH>X@ޯ|ldOE@ df_mӂ؀>z9vdM{Ȟxds R0Yg}r]IHI@5`.(n×asƓЪA8< شqΔ~? @l"[;b tbAXG@ՒSF`x_C|9AXm;Kʀܷ[NQ]mc4=@:`.X7D92 1L:!Z5?RpmXJZ-6e<20k ػ,Aȋ6>=@YU۲JB0̰_y16EnV&¾ƫh@V 9182CGSCԜ2A5b SO D8 ]b] 1BtI={6Գ&}\δ0Jfa$wqROK+VH f܃#sg.\j(] J6teZJ]!7#.!'u?!.1 EU8rꍊ OO' 3E8|yOL9yr62o嗻 >& =Ocr!6dlz28Ql{0+Ϸ V0Z.'">% QgA| Qvj x K;NF?w J/ Ք+uܫ8>Gfs9J*LyRG3vQ ыb!/LԵCK<:Q^Sե7xj|~0}x7m=i.Ɍ!84L ~4pRր)\驓G#_r픞Li_z{)羔I_Bb^G &EnMJ?z:u2w>.MG41+4pe}>my|,/L||ن3'xʏwqCؘI1n 1詭V&ZV;rD֮tvݖ < 1']w.i͎=^SNvn{>35e%L|pE^` s*د1Fmm+j+%=zceX_7Bb0oZ6tYccш M2\&C3DŽH-9eܙ=+'/6}UY釖mxF}a/J6g_S)Cؖ}ߴPրd^T)gk@n]?z$Y(4aʆU8j9n̙[9D"5Gz{u k@ԀowşAMU3/Lj(Ō*Õ,vgL#2St;+%t$NmFE/hڭȳ>i.VyZ/< M+l!HA&Lɜ41GJ^) nc01w?l @V)M Z%ǝ]EALd4um`܋~x7ޮ oN8)\`/'m(YI aOz7y9"Fȣsf[=j[#=] !Pz!<{WDˮ-rtV"I1 GO[e2Vڅ#G"E%%d0IL7 &'Ģ' /&LgV'LF57bh?~c&NBzN!sj梌8|0'|VX.2{ڴi,: #vcAq(%G@1Rx +PVQBdj: "Cv/e˖aܸqҭQۤ6Z)^䤖V@8ּ4ߎiB*χA=|}&9 dO=(j 4'\6&J${ΖkQ|:uhЇoW1kkD SA @HOC;Y}}QmFy&>\Y ѣ{mÏ?|!0 Q!_02k"6&c Y_JZn ZVv &סĄ3B eJ FQ|8s(5VZUdkSU[ׁ ArL̐?Ka=3VڀtLQF"cdg>w M[Vz8sN^ [Sٳ#6[{v deY0ep5hbyzDBn3j{Y7;Ĝ3T:vE^|uE8{T<:IM,eؾ{/ҽg{f{CFgoq wusX5doũ?GMĈ#B J܁ R+2Ȟc U~ SCZz/q:;Ľ"N_| u`OWoYcpswt}/DG]B+[α>;F3n* ~ S_ա$oNJPcGyݼ,ݺwZkz+ǵ-i0)gp`mkKs3AlR:Wc06C!|c| *VOUW>0Eu3$ݖ )GmP׀jNVqcawk+NGv{?6nDˀh1QjmoA6)8T]mji)aϨ]ZXO Nwʹ$s; qퟧzMu ]k@ ]c5 -1;  (K|D#)ٻK ^_%_P JuXtv^-)ž]{ FEd [Iet۴l=SyY?}.{n1Aj 6RY]oʮ NAF !mu ?S&DRrp֟ T@alA(` \| Ѝg.i6z)|M?(sbdy| dXu(/c 1{r;At:V~{Yi]2tF.zm]8##V@d9@-<?ımW(> }37Ե#ƒ͹vFdV^J,6o}[{sMzd ݩ,z>A֭\l3MMAtE 0u8#/ d> Yz l%XYq,2kA7&5)6}zug WM/i؂LuM iq> ?^ S{f[НCzݵ@{ֱ+`YŅxwdzh}\ɑb^*/%I8do1S:zRZ,)R2FnV7t|Ղn1_/A,zd,,cl<')O7r`C!!lL6蓏;U{̙2'M:g<m|iV䣥ucPrf h5.Cz~3rO`ġZLfM@VP0m4U=>y I hZ\]̱aU2p0@R9b)~(,bnZ/xztEzy^{p/X{baB ``lK]M16d SM)G37 `UXUz|4<˸GJ~m_~I%-8q3s绪 |Q `JMN`\؍dk%#?I9LKCjs(#dJqNq l~ ,C1Ve²۝Ԣ,k*`rWuUr \m&l"ۘ31<5wK_hYY٩zFg\+Max dKy(egw?@01V9s!=~XHS'F yYyIcb#b#bPע#[\c'_v-rz|[8VtO`}ؼ͍V;Uۮl)rӝ>4+L*Ni%p8͝Ŝ?;bӯHG6> cǏGUJFV uo692OP9SA.hQߘX=_yWejֲz;u k࿭z.*yEG{[8#nۃ@`9 H,z-4PQ達s+PQ(J5/$3r_Tprr1@XO9<{1ߘ+~D2<'fΗUAՙhE9R >ZmPk@ NK5pG5 FJvzu k@]ӧOLeļr̛?r0h-O k(%ɗz`7G4?c!={`-kG؉7~ez:Z3GmPSW.]pB 8Bܚhܴ*7LX,v| _G-`:<n1f2>Yۻٳj@^@={̝;3wCowy| AxwΊƩrډ{30m9V'P 15I.o:b0e{QpϾu"ck`E7HZ#*045ڻ8^Ȍ;Gʒr `.;cU#J)pґ!;}e(wUŸJOLf!I7˗*;hkNExLoܴi;V6:O'*xrϑu+11 I֯3D!KA ԛ` kH"3--~(Эf boQȀ#zwQOE^I!b٭ Ч8%S&,l/8(SfCJq8٘ҋT%J3=utl2~Zwܾx (,`N& uQF: pl&L3!):ꔨ An00` ƿxI{ gFsI0 ES7``KJbz__>WqsC/eE`O| *+OGY'~%;Mq+.qOܼM{o`Ii߭Rhm.AܸT>DL [2Q] f`XQU^` ,C}>ؘVO/HtݼNnnh66J}AZJU_X߯T+u_ܶ8B1A~}k?6mľxcV3%w(P4>VVг2]bW `ڊ[=P^!`U.kD{Ea=m"=8_0N !+׮HnkF^>dzzqs`^^ &AkO?r\!\ kQ|vy-GO૗_Ƌ[QA~ИtlF|q,{E:O:(**CVL<&[ e+p1l8,Ȣ՟2^ `kPxj,8N4MiR&[6Y376VVFYR зwg yZ,_0|aB AYV7$E;*Ny޼Hv%0=;Sv͈6Aksu S6eOYA9AE--L!sU2x8!ɒs6x;c<'bEdŎDɷVZ9 6zwr)Z(t43 oL9j:kP9+@!¢GG2F5v($hkcס-m- t89X” cgŽ&,Udn^ڼD)מ@ItWFFy:b)~+}R#=% ԅϽd\sKo"p҇IE3y`7K̴D`1c~>]XMUŘ_-֤j8rgB f"դDm{{Rցg&E SJLw̛!UK_WX;29ypp af5BqK+Q^Y!L3 1(jOMVÕLGo#lH3%@Uzc@*'*m*Gba⇊qcG*֫Z*vU{[@YO0Mo~}cP7b*P_l&攱j+w=)h 0 ^Xl'jwȕ}hjlyhFzv`O9F߽j硢Zjhi91p #q!-f{G4 >gOI071DCp ^yZ[6`i 9 ښ[ΚjXlNRjН,LcqqkA.K_c'O"I^H]nfP'k&(*:2 Úps#^j& aƣ+ F5"V Ȥ{ٗBP?,J |G9Kq[\`*/X۶F^,>ͺUQ0qc>do0Yx@$Vь9d ?3)' Zkӑ!8}*%P׀bbG=6Ràgb"iFc$2=qPDfh L; xr vT+Nfo#'uч/}aFl+M%s35+'[ȼ|*,!__?u0.A[$롧F38'Ҁ_Q.kV,STQZ!@XHHsJJKeAojr=Nݹ{L>IN?fW54t@L8R2ߗm"ӎk7Y8cdm~XwCFk)%ޭ-B BNHA=M@Y^_\׊nc=LɔSQۻ•}2*}d:ww)>^6+▾&䐁MxA+ ɍ@(R̢MN{b4,B1n4C+c/E7g*ЪcEiK*}-*mMچrk#dh{bWv엒hZ:{)v-^M;j&\ 6v҃}V~PJys$rq)OE?_O<̻￷{N6)b(6ֿ ^wnI7W{.6,u+-k`b)"sc޼κ;d&͋7WWYT jƮ(—FUn\ ׶5Zj{s#4w1M[_ EJ8.^'pSꫯ A܉cصvlIdW fd63h*@ٳIրĴJ^Ԛw~!6dw kjPnTV,=Ĭ`;gO챟@%vgyyZ[>~ڛR׊urv !s߽h#X/qrD|DlltWuBıӵQPJD]Q!Æ#|X ǎGU%-)9"Cċn](I:r} rœLB>},oQc 'u6n};0v̄s{.LJ;x'(H1 Ĉpad/'st;Fdg”)S挣n!מdK#v-(Bo 3aTLZ[)6'/ z\D[[@(cgebǫPL?^wR0Rz&煍}2߳XX!e\%]MT {yց̎lؾ\br6X87D}Kbi㣰7%ir$;Mi/ѷm3)/DZb^ggamqVۤ'sse@(nٲmDҀƊ7|'y V(k,;).?s8%k1uN+c+Y9Tw\2cS5veu"QF! wm7 /&Lή>uEeXS4>e:O>X˃ KyصfϞ 6=KItO'^1E[cDЅɳnBN^K J24,mpD3J ύ u#V3`2Ce~yV؊.X1P77߼>CQG =5VQ;Ȭ~vWLy7|#x~i)~=Жu `% h,@?oJ+… Y8w&l߮4յQܞN=6A UQ]nعxR MMLY_ =&Ei.–竢(;b&{T. )-Wucg[3G>}Q5ńt`4!5  %!$i:@ iZ u'S>퓏E n%+֗+7J 5i GKg7eɔLTq(?^[iGb?_B@PJS1,_Wd}% 3xonNL_#?+5yGTJmAgmLC YfboY*TyVA3At%RV6U͚Mo/H+-?plxdxD z[Jrqqx)iToe Xp {`b5Ҵ ~GGM㚋X EǞvOSqkw3TEMchX4gD<|HmîCG1yQQU@o):AV3~4ңv}|֏OJAiE%}]I7Jgz1^QЪ#a?XMjc*rt~+ƛb%P7-zbh>yc/͉/ߺXQIPcA t<j7jjU Z࿪to6ܶN{Nne},f>Yc%6L -Ю·)%aGUN'&,ss)9{kkf懏7?Oѓu©3py2[s5L5l߾zԉ? c[lXaڴو`S`GLMe]dV#Osh10Cie ):`#79l,XOC=23sq Zuz1{)v"r6^`{ⰰ 8hFyfMvrԱ1O ud45Z1YZ$P"UH;:c!h @ViKLg^6): +6`FhjiEss3lXȥoe44gOBYI.dˉׯ|:Ȍ Q_ f`0enVs`OvW9^R.K=Xr217^㉃x H]:8A0Вi5\1@I(<{.bVA!%>;Kdy8 F֍8}^He5lǩ|i<<*I,YΠs9Aŀ\*"-VT1(D n>peYي߈rš,g6640(B޺Re`OEwqrÁ&szZ۾gE8VL QQbRq%5Rk@ZV.""SGd={Kֈ؉LEk[`nզ$p.l\8vF!B&C_HKMFq7JLú "=a2A`E>?NTv#d>d;#Јh~Q6-8gb&ghlTQG~ݵ dN!8t1TQv▛f!7' H#PQ(&gMYjqn==)lZMu!Fo@0Q/%oZY~/UYx{ _bF]j䔳JvVٵ{'vفIT|1CҐQjjJ3!N=mU2z{(㜅t73bzQW3Qm_3߱zq^WWf8XV~LMXb1%kT`U(n׍7$ve&6̈]s7|.}i" Tj+QF _hswdƌf+ӓ?v> C*N1a QhW WmxML1חV1=lF1Ғ$ţc QFL$%,&K<CzXxS7J 1`۞h!v3f~Vt}>(o}g*1/HU!%=Yblٶ&:a$B5~U3A&2+w[U|I 9%'1e@]K!*`ֱ͍fqe+W--MX챎+d~E Xܻ=LJd6ʥRsQYlhD 40SF"CW%OLAĸ9x%Y C$~m-/u*^2J+*JQi{-wC˒@> JaLԀ:Y#)~=0ºMX.77Qf^ŖGՁK~׫."fHD}Q3  4RNiLViLD"r[,S=1gzT-g dZZD!0 >vJPnxȴ]Q\Pyq ~e7|@O҄ A-udQv"lAZ٧XZ̘4_ uxWXe쬍0CGK-AAƤq)ysgbŰ$p6mrjt?5#oB = \V1 f˖eOYA]J~F2QiDFf*Zq>^~TPZpZLMQ()Yn Dܾd3ReUhee0"f&pds;2,!'EAtZXZB FkU%+De3訝} L ĄĐ;_@l5K|h'[76z3x K/^MD%cmA!䰽#Jx-00'9F'\ `ɤ'%ÚP#V7'Mj r+![`Ǽ H?ɶ`;Mǥz1nÂ^cS[[g?|dK=ww4Ì@:sQIaqXP:wӣ>8NU9e} ygyVx0_o;2͝v|lstʿQzAԖ!$jϖQWWS1M~ Qoh?e#xmXTsi EQd>ⷊҮӗyZq*!$@M_EvrY`cX F=_7r;c0vΒFZ|󰞕uSbko_d&ӏv3}oiik+39kPc\MeHǝ6ƍ /BA~xڴ)F7ANfGI,ʰs0#K>cTtǷΎXt(9Ozu0`a)-=sCqӘ\Azd.`Bvv#ٰLWjJQ{o~aۻ%yzxMM%3;c?`cOb.Y!qc@,2Wb)5$E2Aڶ~Xػę0|$]O>d2ō4 Ж wuSfLfG-=ں^d01d}@ljSR&DqǦ IghΠ\\>N98~{Q>4t1,znZ Y+7QjG`z51qH:z =~bd@k' )Yn HJ_OOq}(Ldƹ AaΑ cJJ*މnih p<2j=L?w.3$?|RzMm yP2d-hl۾3OQSg$f/.%RU#`xH?,+&Ys-:ܓOBW gⅹPG2?O_pOvMU[xO>I V$` 3gc(v_gO&GyN0XXqX+Gn;Gy^bYV VUw_ f=ME̗(WX|n=rGΕ XYΖRĭW.`Gل$*~*ovZ쮮YKy/IA@V0/7 F<Ø<$L%ɮz@IDAT >Iiqr{iâfZM*Ȉer1A<,ƚ5*uysge1I" "@Ɩ!DF,UD*HWckJz滲>R}3/ uwKl_=T-?*vƷJ&Ʒj -3+@fq/A(Ϝƿw-ϧXϤੵ=0a[W 331* [@1VO@44x\x>]!GJ@P66v*u& #pOӌFWW?3My0㢇H|.eji)dcueUq j3L۱Ch딓Ujot\%7 qX]{G/Muy62"t ژM+PAsKSwv ܯw0<ܟˡ2EGC!Ɲ~O4{Z@Љ S; H ""oA֪XFf4 1v0{} B(y:%x`f> )rnuԖW.bF̀'3гfQk2p'[oj־> h]GB'ĊWH`JFLJ!v .lsmdb) zTrX[ɩXoK ζ>47աm&8Hm/YA[u6FNyF{ܛ `…pWAێc̰en2+ iA]ͼ)a[sZ 3g30Nf[>^,-lMWHGGAdjS V[39$? 7؎>$`0A0&|,;XJ6 [8,mGȩfRfXIH<*1f;k.KbZ!o&+_y1i߲UejF֤yDRPjj]f ^ uzZƭBJf,CgURLGHd[hNUPJ+-ǐ~~_z~R|PP)T~NVR=ǟgV2n}>]l/TN:DJ@-DPZū03҇*[ d[ Fii1R7+"sG0y%fM]U O]睨#`yr1[-~*<-q'%ULfVh!&hK;)c7 ͝hm[Vgg-²e1q<%0kLMpLbEE #)oҭ {{ @C4z3?'؇.WWh44(%26AYSaSt 2 a2ϾԴlzU3(m(( F`N'P~WN󔗤Hwgx0 6̛w+);lmara"t03*+V&,Ruh:ںٷ(/-Jݶ5U"vlJ$@x g NA?||e#؆-|6.瓓ɓ'L?.䜋?w7\ ZPB]}<,8@! NnQJpU 8u l2rZIK s\}Qֹ-$z`XGmΜ&1 3_5h74~^ޮapK [7@?O  qTb*h_^<߈Όu~"IuzS9 jzlkL胚FIt>bO_Z\zci8x<˖/%;܈ʋd_Nq`}𡥒4ރ5A4kؙr>aM_2O69ŕ8NI`=" = } &Y@YalΘ1j6bA˞`H>el<֖V-n$m:m |<^LhmES> tQN]MuuK>%jIN2~<KsNC`ŠަIs>T1y n_Fb2( :AqXW0aQz51AS<8q Ef|k!!%&נu'j" 0]*~Q3fv./l3jTgC@[G=3C/jo6}687r~uޖwce rsve͋gj'h-=)Pʊ)3}]=,\|'qt,h;SShhYQ>ʮژFW&͘a8|2k!&ܻ0rpԶ.*lT]=/^VQH9and@\k?UU-/ɺLSwV)D0=Km j3%>!QCF(OEHI"Ab$r{w.% 6׾+j0Q3ĶE b b,'Iew` #*DlV&<"xn-YnGXja *7|2_1/2DQܐP_=r~~׽{i 'qgϞD櫘0R`"*~#xp).d$(몃b"Cf"RGTߘˆ#)J Z;"ADw)S&2hM9Ax1`Ƚa`G=ܴ UTE(& Z;pX餾uucbm 9Zow@@֣ ޞ>tv#=qS7 O<%_`Ri蓝9"<?p.t 1VFObseV68@|gJ^YHS/rL$) _`O 7U}/_OXCpĒ )X~،̌;MɔbI= &&d6ADO̧@fj">c|s2ed&L1P*rI)1k`L97/SQUi*6$&3gk#8=iEOD م~]Z(ʼnNނY-Y{A5Y|xl<|w.YH̵I0b`^ U|hÞaFOF dTF$h]?yb U j`XX,U^M!Hik{%ӊytSX+FӉA]^06nC#9 _\ɔh"jzUԵ" M_($>͛b ȢZIa1~9DFF|09 ,f$^Pz\+09't T,|u…3p"7V!dḺ{.X.2Fp1dt5 $Csp/%i$"eIسbiAyQɨO=Vz ..8yٹd6̽u ܁ (8ELVw5i#<-!3wCrFe33D!?lI#4D]3>1fT,YV;Fc=C55xҷq(a[oJ)Yj2d0$g`j/‡|;41,v_q}8$rIGBj:67^Qp|3I8ww.d?ى> &պXB"`i瀠XʚcJًR\Ro(=eTyQuGrD%PږSP褯ΥbȦ}5llO\1:Nԑ[ArM& t5M^$Przw?DlEeS7<۪FeFז½}wMB&/7uQULpԄi h4+~ahF3GD:RKO`q㗑LZ%Ey{q|q7r!ںF7u~~&oH۳ 1eEp"OuAC;A2ṽ7Ϙ"IecŨ*/.7rıhJ_*OQYEQV䚙x xѻBPt@L;beY?{,k{ރ 8}p7N˧uÙL{QF )'.S*(tpH~ ^b>W_z)~uWXŏ=ap mvWW p_ϾZFk*VLg(*"P*YT(G\ky|U Z?Uu7׍/*$(Iԟ;n˫ d_<$ƐHc}8OfWUEUӿ[ @ֿ+Ut4/Q~O[T-z@WK },l1M!~KȄ L믕jzeY0pOۺw*ȡw=-(K< sɯm}D.gN|Q `o@5%%!48+JH.c 8QzT&~@^9dl°t0N\5BldoŬ Tc2~3ƍ?_+f27mk!K`W~Xp"tp}ԧ" 8|&F'Ԇ6F o0V d&Rw}~c+ ψ7 ]g?jp^ 6Ȑu.Ayǝ46)l.8o0~p0s a1cmnJGlhöʗ"Z᳚BoC 1~>OЈކ'QVDv[[*-?SZ% dCBZbBzc TO ?Jjl֗TNn|@ZlOdZqVoZ'‚M[DwB5|#ZDp/ǰ(|sJ7˜ ?7ghv#cO*\؂.?X:GF3<Dd41=[ dePRy^ d,!(p.-_ CV6aɔ7wA#e%84N|s_{dz2hhZSzߍ'}پ\SWKG31 auFx;➕S.fe?>LIL5}(Og3{eL_uL7l}¥]mMhl큞JeYie_5Х|XYO7)9x0 ^Q_F|1ns7ϓF+tyJrnN?Ucyx,(3s780d5z =O; etB\ua#hdUAa,蠇w(Tj4f#}@={_\(ov TSj^R& SB7#bFHO)#ރ{LF܉8P6֕>6fdb,Y~9RAu'_wXk j;QB6ϥ|fΜyEoȼB>Wo֎2!edON^&GP DOVwmց] VxPU Z࿳}=;+vZ)3Xp^(}5ptqk`5Ul-"t(Lm.b3*Xb`؊{ݣ_ XzK_܀ꍪB-YBZUm>yD |)X"*7!q΍ٲjPZ #:2tp]bˌUc`[YY.&'ï ygKcbn!UC+/l??dرkzxwkoVfN$#FxbE/["(oS/1/~H/f՗Â,bJ4j2q. X^0svEUY7z#SXAv)̭,@ZƅL45$0IRƲ7eD %gf1)i="YWŠK=Dc0Q~r*^U;rh .$'#FN!,};gbp4Aх3naz"Ix0G6 zy$֧ؑ%/ǎÅo{s3Q%o0> ٌddqKC7kԹPM@+35 r0z=]Ȫ< ‹ ܍%ClVQ()4d(ldOeXV ?.vރ%[E y+rb=a"KV$ŶDX3. űןDW:=~*%۫$|#wq#d~4[Zxշqp(Wc]C-%7ѧU[Lwr`d'(~!dKѝ,~?m0 }X`1lvP%کsur!h.[ zA[L_ Xv-0DcNĢ%˰pus',Sx套  gn}c,xw.Շ X&zd{a=5-`8JuLv:’J3XGLEzٝoc&pײBМ2n00QU-&r8*g`Ҙɸ뾻I+vy!z!C&Yח}h0v=RFhmxs%I_?q@<ML}S52SOP#1~8P:l߳S?Jpە2jMʠi9E쐒^J/nSgYCQ'}7|I&-d׺ZyԀKV$rqo>&Snl4Ƞ֑ج2&W lNGGc!:m_ FwU\ks@BEZZ@K5j*uJR $$ĉ?gæ -}-7w췻gf]ᄚ>.LFA W3㊆s' GM4OVSV0gr c, M,4* :f'x[+2q[#L%: 6(E|ZޅDZ+5O *Ry- 0rurD}Y! 5)A:ºrvARr֠PHfW* \W< ==]넔%,^t;b<4,,,.JLZ^2θXeUbιE֎023 .d2ѡtPK?]BF)I(\>p3[!(-)AeJQVW?OQ>LE+/Ŷ͟s r~*rBz"Mu0y%=}KACGBX5p7L}Q!?Ĝ93..?fW!'IP&;M$JNU L=?#'1V -k61=Wx&&%jNTYŒ2&Ommc»sPځ8|W_|X*mƁlAA!~m:m6em@6#ReK`ɤJ{F=~/Nk%KlԉꕧDRكLEwX7 ~'Oaa#V'SY,́[D&R"?Ue6Wn-U (ۂ2Z_ }˳\em 3tnf8E?k_ ;:s$^OW6@6۫,>gx!D$&ݠM.!;yd} uo{=BL5=h0<<0$4ٶb#P&]0ȡ0l 256;Pd{Mc^~<KF7?c [Yj*WO>v6'IV3oK\SĘbNH }}㲵" SUz35Z,ߜ1a1ČڊFO@Vyqt^?}("*?ow'Z:T|nwYNN{dRL=zӚFr̛oyJuϹ^A"("'D@H q_o*F$DRݕ6T-QE P,E&鎅p' Ĵ]L濶_y3oXE}*>"w>5~3~'ÝCA =^9; \;wY]":D1464&G;#ҏwJɀlE>|L ܱ p deOA֎ 4bbbFӀrJ;ҋP]nka"Il^%`w("EM[Pz0!goҵ5:XiZyWrҗ1QM--Р4r _LHmhkFJQ5eDQ̚)SL9ke MxeJ[PTBְ4g!Ç*\Ǐ9of[ I s܂PL֭!;=G0({d hW iE#G'wP #+3$g^t)<F Mχ>;_iL_H|*=UM橒RYoSi0n @[2E23Rl4Y[힦Nd`9ּM;}b("L9~{/O& U$Hׯ 2{ x KQJ=aGevay1׭x`ex2r7w:;R674c5GaDzss!m}L'~MCGcgqH39rv=sLHΜ9Gh322$3,(k4Ѐ.%RCLG2e:-))DԕhdSً c~%傸D/xYIelǶD.V덺+H?T(bkkFPǰjd[[%ѣG]$@Ԉ=#.{ vnp2na(M!Fݑd^D/`G'1zow=,p5`b^cFR ^lR2}KD&%p]9riVo®`壏"_nfPH'!&A,K[f;wL0Op'Nu`"1Y':?={.{JJFMdϊ0s=G(&zDbNczeU`a/u@!LM`a\ReܡMTq5|#*Jx?4BP/w%G 'z!EPWUetbCȣu#-#h<\Ou9h_J%ɲGvAAF^gB u@/gwÆZd7³ >~"X˯EUz 0u22Pw:ѣ[.Am}|'p%ZPX%!d! ը$?yK)@F**NOAql 5UJ3R|D7>tBg U̴5s׌]N+u;YI <8J݀._NTVQ1#%EM %5Q׀{{!=xΘQfEKX:ᄁas9y=|O]EM)U[YGxD$&̜Kw7 8HR\W|ff|{vSD@EȢAl*!+%+ZkU($Rs/HRTpt<ܣӟhN:v?  o~.B ݧG/9zf'?(38bԵ kpnKLFgL){}|[- J۽{~)Y]˂2Cvnvur`azzJdՖW[ &% ^Xs*g*/pT0p&8Z_z!j#6&ɩ5v ^zv?{v1(ymMЧD=t sUI Y/FXF&fODRX[bsއQW7{ ӫϐTHjGbdyT4h_q2A2A| d0M8117ЕY-XpBi[,cʒ `P$1+vz7wT:Y*B:w&u'7]s} NRqJΛ} t5`Es]0!Y'K+W4giO#sp ?,z |Ι8r&p~w qQUe'o&KdN4c,tD_+ҵk:k  K۟30ȺuGsN)+[*iXZvQ:XIz}F+!ܹ~?t *h֫x̺gz_zlEDk465W9ߍ$z-hh"bFݕKoDq9Qa^O\ ڦ6̴:YQavJaƌ1ޥP&T2aPG RUdam(1 gJJ߻x1_H)hQ߃gz~EI  x6rY68}>4tM)TfYLYb{àbޕ+Ae|B81$6*TJصk']VEAajjޢr1,zwf9wA\@spa|{>\ֆ۶o/K#F˗baHPO=4RAB5h)5`,HXء _OGVի(FLJ2`s<3 y󟀾>~s$ Ւ~'#>NjУF6O)ޔ6!*;D3!`!ן~B`چ k9Vy]-m]%X%b)իK5 ew!C/(h`v$ّy\h"}&ljv}B~fFLDFXam2۾ΥrE0foU Kyεn5/!\ư]ٱb-T2\)r᧛4jį5ё6nIn[W&XJshVOIϿ!,LIk&rՋN%u&SX=UXد"G_E.CGJeU *+K  W%xVE[X-hrc UbdY/"sײay[,Vʓ;cvPD`Vq`M"(:uA;n qWHX drI41m켌 [hb e@SCN&Z+sL[0b}wWGщ/-1C 4dO47 `EF^F?J8Ir2ՁoMA $CY*0+?eZY,X?덈MKCuqY6 xhhSxEJ !TTҳT)<r u-LpVK%1w{/)P[jM2%VQqՐZ1"0׸,xI2客~~&wY@ƭGwL#Kl&ңME聩M_N[G<+HD/CIσG&%U٭mEd^`\TJ0(37,c-$*1h~Z17$vŸ`];T|?AF%e/_BDt80 ->5lLvc&eE7bC'p^dd3>$CΓw?R淔'OGZJ =>U{zO&(f͛UWl޸As!˫ʼn7@pԾ򍿂ԇP<Уd`wsZORbzc∿){M<3h':~}=r s*0ݷ>42t72}}F@GOm&) T-5uXlik4#Fqט ' `ϟ{_ "A?kd>Fה.!1%1R kdX k|YO5CvZl~ b(>_%2jW^e<<ԪU|z5y)mD Z[Ua-MQńbz?sb2r߹ŵPҶ$kU )̃4TS׈fJd;o:.N Ex"@Ь(+}@9B(k>LdR`g2N1Ϊ$ql[x4vmrL1'Sݷ `)|hVU ]5sC+`NmZheȺIFJ2i-YwSքKN~N;OW_'-ʓO>!XWDE/_祏"QD&*ߛyl[kpjz"龷v{vN: DƮS.2^?//穯X;`=}.~sKё^ss)k4yaY&-xA|kܩï?N}Q;@M  wZ;3`o*XGEF%}|U1ЧJ߆'cb#XN\D{tŸ"(@ֿYj#QT]EgFGI`*HU%Ě@S}p>|*,~%IVuoa⴩ZOnA>N9IB/R>Y0_K]9FJA–-p^ z% ` ;o幢$;'VQaL%Y_|AVy݆y3 "ĕVз:Z8xcaMո,9A5Jg /&:R UJ(!(stF4bZql#G|)ѨOFr4ؑ[Wm dK#RdՓPC9*\݌[>ĹNIvY:>Բ:6mޅ!ACSTzXΉ,+C |p˰N54;SȾ=aP%(VT"]vxK:={>ۺϑA]3| '_yYZLG#{GٙݓPfP2B |47 cSby(\?|K؆]?Yߋ.PVk9̙]!9uGwK?#($~#"Mo]w{tm@+/ YWaՔANQSWC_*t40z(XLPFcq_š`jOg'gzE@V掄|^PϿXǴA `FlܲWiKj#PGޛR٩YL &*#mk9HUR<>x/tn'eNԽ4Z~%+[m-΁QKL{1jD8&6?/WbfKRadXѧ(#\SsAI+ZsK<QRDC G}`iHC2,n;v.̮\AvzR ЌF p7Wط }Ma5r3'}î`4uH =e[Lnem%1֚ыu ;YJLwA#. ןYb8V=RLPQQzL F(:Lk_)S$@S =9T!Jrb rWU)˟XE f3)Z.l`\OE%zy7ʥBZW  SJ ElkN)`<< ] qƍ ˝+"< 2}yjѦx{icJ2~ŹAf6Ȫ=$ʞ}{O5^S7%EӥfJ `nm^w./]W--ɡm@jRrBlAÛ;eU#d7ƣƋukK/ߋX?|-&u[qFj'&7'ރAN ?' Xb\= o|`U㳨"#Pnܻ_|h%3U E;f'';u$K*~=",JJp.XҫPZq`#qOۯOZp`@/<ɢgqJgk+`'W"/U5homLNE>bn=]z7>a#FJv pSi6ЃKH'3Fـl4m-*A̬l݃2&XN֑e-XeVeF^#]>vڷ,fp3A =Dgw.ܶ&/)iK)Q٬ SCzf\J.5arEzGme`ehRA`ˆ Ì0RvH #hN, mG5ٜ4Ad+1JY{Gx)I%J˪ bR fJ^: 5ͼz‚?G+Zz檫Èrr\zE9Y ;h*q~\ٙ^(MKDnz";Ӥ$֑W -w^3k`s?zX94D &+gº`8u2v9iN1?) ( ӭ fx?R2kHF#m {% d[#e`~!G"0fHTVA]S|).5("ΈV>Qs[/ĐmGaɰ0FnId 5gϡ3Ylϟ4P5Q$G?-]#ƑJTSXmvz|)ͫͰn69j(d t7E$eˊΦDo&xmU~s!uŃ˧)aDְ(\@N޻@*M &w-` (Eӏ3 0Lt<||稣? m[bYeU7nw#h@%D6T '#׹2..ז9Es 1iǭ ;a]k$X$) #ggoPZTΦtإ(!߀\9Fq?D-xŚXEIS/WV塇V"!>LЃiyՔ!KXŚLdʁZP_UYNtLIðd7fMkUNe]jm>#gK V^Um(8)9\IhZn&?!WqjJ+Myy-z@_[H1a1\j>Tz¯ia6 1=z|h8O){[*P_2̜>+ôakGZ KߋVJ{w݅,fcYmmU>9~*9hTz1=*싄'%=={Z@r2+Ķ/չPLZ,ѿ?ʦ Su1\UYŰ7r 0VXWoPQA)aLP`*,G&svʝ҂[X{PD@Etڔ1#G]oso[1*ދED|YN+"ode!wXQuEPDEWW@6找d ӤweZm vKb w22(0& 1m i`+Y~u|czbR[&J]iROZ7@l`Ea.K.3PU _v檷6dàDyy讚\U^WqFs7Heꉕdg2^8g/z}$Utq틋0"9BO=rB)?~l27]Ov5!B9D6U.j={ z(M%|UB̤"z6ֳwzN!%cddzXJp&`B 9]HjS69Pb^*՛Vq,CO|dFHJލsEoK@ٛoЁdw/ÀC*):(]J]q: UU"a8v| ]B]h0[ Pcǿ5j 6*KZLXH5{u=ʒ`SIk,xV`۷lDSO4TBR;zBtY4 )|q9TJYY+a'T5’Iɔ(e7;N@XA<(/P\,,o¥ 4,Fd577xUU0yF k1d5X[Gẇ'l љe4{bq/ӏ[r||p 3\f'ghlBOZmX!&fQj$ZWeل' FhGWz榦e%!2H a3Ydۑ%f*A:nѓۗ ܎i^i_Jd=wJ\ "]3K+SޫK/|N:K̵Ux~s>!h`T/3`pnf&̚E+ƥ$>B]lalw %P~\񓍘lQ4#ZoR/mUu?~;o482ĔZcbyܖv乌1R^V!6}7_xK LĘgG>X1nX&Уr@~J]guھN jb.3cmNi[ -pv޻ X_ks_u *LDMc42!>"gךC9 wnP]ho0]{چװX5(I2<6y+U1O^{j 'X}M5AHB DICix;v}.|ҧВƝG.ѧ3PF(j"芳'3ibb/}c@[oիwG(acv iǂԱsa('_"^dDҗ6"=1lpAěـ/:^|f%%6ǠE pn4AQI[bl":&? #BO3θcPL`u%ܔ\G cT\x9Z!>$ewdCUع@ l DI%vi\#X-J\0kdFp|SraVU28#E )[ٹXa=s֓lhAۡS8JEM-u%KQGg?Y]7{@۬4Mc8O/GGEJæ 0ҝqcǾh,L l%Es}{7V݇#&AiY)a$8L!p. g~Kp${|#'3{aÆWPT #c޽zEEE%R:Ԗ@uUpv--W" MEZF. άJS-|GU&oHb<4:(yMģ(? Ͼ&gc/eW~]C.[:JC'woEr -Ɨl?~k=,^{a &ςZ7>hldF ںLP0DjV[Y'/t(ʸH|Y?~#CIȾd̥Q3iw6p"OMB 9w֦%;TTҋ,۷3d.ɓ"OBv&x*d^}u ppYθJfU@GU1IhljĹv #Gd06d:ӷԉr|J1зCRJֽlް˰xC|!vT+ɸ$3YEjՇ33d.YPaaHp+Yn 0`'1h!+S2oht;5ˉAID o\HW O$$-ZVo0aNEH|!)ZOo+hXwnӇ8n ߅+;3$m%'wgA = ̦F= $]-'AVSC#) 44X1"X6Fl";үi!!)*(.F e{w^ %s9P2kaNQ۽yDGEeU$/[}SKxƠrԔ'gtk63G !k TAu2 uTkx|UA N2}@=A:,ʐťc1,?~N;\ow=KQTnx /o~; lLgFM *(_oxK~d${ֿc+]fc]u#-y_ P/ѓO2PU[kFVrO;s-">+="A]g3ã'eWTl #؊lmCX1%%`kE2Q՝,_[dN`ֿ }֭p.lU%Kշާ[] mz\W52Dut0m,>Rz u<m{ \fGcjj&4J,&>_.\%erK%me&-Hkk6lD sUNUǙ\$"676g $;Ax^ ?wc'9['1byQv݅L3*[[{~oE^Nb[kʚW`XF.m0􂉚Y(Nv|JܛO0$; a>s08::¬ ZOûLX!$memL_PUs>}&Ttd,4b["YH(qQ?q,dط`:,s':S 2 ~w4ҵ#K̋=j5wgغ"w&`I}Ilwf*"pg#ZEՄ. N_ۺxiWjW -}>յo*/+~;tf$27No!ܾ Pk +[EPDoF^)_JYB84iOv:%>*TufyzFG/jհ+vΞLH[,[y?|=wei2duԱH#V ~zޑ^^ċӰ[%P1M2(\,$Yiq-Ces'' WLɌ\v; w>pz8_q$NЖࡥ+ec0Ūʎos2ļ8 OGeToRZ{'O@)=HGˌW(8~883Q*>BjQrc-}SW-թd1h|`C0[3zw 4Wf5PGRfiJ4y 2LP{mGL %m)! LuiY߅iSfSqtt2ln^1dDŕ¸W"2~=]! =:5f<پEDbr&.DƠGOw*2J^J LM&ڻ7V>ZެCϞtl±S{(/|6?|6ew"4+ ڙO#kw~K Pӆ .`_qmF&fCM( GJהKQCxץJʡQ#,Ʀr2eK*J(gMK-W̤ Sl9{$\VQ;kWeAa'/&L#FÚ?M1yH㨫Ĺ31a??;."Sl35^c/ tG#WuPW'PBM%<@b>3d< $ !!3(,,f"J_ns9XpB6*!@ߴ䭿>SFNĉ`I@5.>خ'bb-FY^x\e{mdl68{&-9kB!b^pwrRiִ⾗ 1u6AL^2rJͲb]9)C!:7E!%M b;Y~E=*9wrm?BJ~oc\Z<*ktLlkGĻJg*Y:Քڕ>%0ה Xx~6dwdU^'yb"P+AEPD_1_9oo62g3؀aTYsq/>C(Y*{ ly`'}WD^VثdiTooM7lj$=N ݧO ®;@ (L9$dy\H)ؠ{ݏ4: X3YDPE*ecm8rzڡ/ gq%C+:.^u5 k?1:^x{Y~&$8p">!˞o[O*ycޒz^H4=*3gfHK,zIyi¾o7QV8JS 0f(PS=()yJ .:гřKwM +MBrrܔM+ BԲv2[TM}"#P34[h<y]6:/si~=K?8m݊lxz߁Qu9)glӧB(%<q;s_CF"XIhR5)5ރ O_0եDh0CPU6S/GiXsG [2ӹ'x匞< r^K^?`7*v.Lt;҅Ǣ̂"=Xd_f⻝c젾$CRG&"Z*ҵ "5:\(6r,jHxٰk&"## 7y$T ؎9[Zy%7}1W^D+lx&d6媇!c ewT``%D6 E%xݷ`j p篾:.7b/a0nX ?ﮃW9٦a|C7#]:nWABŵoPPƅXЛ2}$k ƶs,¬b*{o?ͺ9Q8^Ov 2cFVoאcOy3]eJb)wF 8=]}u&ڃ΀P* ]]̛HE~rg6xN<Nt-#PրZ`l{iDű+"wH ,b(O `@;]wacj 65n" Uq ("""OX(HJg ?+/G ='NV*1PZZZ'gP!& 8-!;sI.S7)'m<ɬvtV/_ZJye2ŧY+ӗLBQ^F:P)7oF6A˛ƙG$Ջ7!)Xo~\ߪ z3YUy40cG M lDM*AP>}& Iw9JO֐͆r=3KEv>\(QGʞmRzZ엷&ف;h ӯTX\`VvxyTG6S"fU0_32Z5@pS/,xv ~UUNd)CI9!# 9.Iɉ#(2ǂ$nU3w4QvѸpt0{^?0v  !R&3}WA':@fnCfNv0ٮBZHߛ.KM P1]#T %5:4c?2DW=vcϠ!3*ꐷua,G-6 [6H8IIl3 \, DyU5J6c 0 뻍@vېDyF~d$# <]EOCso -.D ~/GŜ +צaV,716%ؕBFL4O\J 6d! ̈́#e|_>`Gכdkz=˺9;Hͭ)l +~%zHr'O!.VPRBt>y+\׈ѓ#'!}#qהq2c5Lp" 5sfL#v ,mƺ``*,P:5%'yWT`ZeI]I1}\#h[ RU k{uDO&488_lGOlZXI H!Iqd•ZQZ%o&%kt%ʝ .&Xsvhe0֗,ܠm;w]RrJT4wgC /*ާg`ɉs ?nAVk̍G}>41p@sZn4GÛd< H9(2ƍs\MUeIZǟl'٘ ^;e:ɴHƴI?k6}Ǯ.qs:L>Spk5gs.+ TY#Eȶ1AnI$[v};HE{+[D{c,nqr!{X%.Go@k[;4Sm`OA仐GHWcX<92a-־ T{ WٲS}וO# @O &# <#'"POSʟUud]CWpʋN>4#nY"KJ נ,#Iu@c|9 Yc[ke2E,++~mHA3}z-6 wjXIuKLGZj&)=@ '\-@'٦H hMy=5LժZEXXL_.綎vhI,Jjf\8}abvvcGgx}}Ic;":{JfΥrА5 P#8C7 |Y[=ҫjC#e:7Ç/!l$lI@Q_Mu6(jJF A&3 "H]iӦÀ2з񅽕9}Ip1^@[eBg5Lyy C!~БnǁЦ:2ѵM-4!tE!.< ǐSk_kQE/>%sCm)iEN~VVNq}#HbO؞c$F!":Eٰ6*/GnVjj‹o޻gd:Q֔޾:(m XۑEK7=PQ݀"2}9{HXzNXłȈr(~ dϡ[$<Y ֬xJKNsٌ~C=`AL}]]l߷$ы֊^—x׏k:d_p#x0m{GgL2:7߄1&da2n[Xwo]T&yUP$; uyX%Ubc!VO!ȊgzGdRG8(F FIm3~N-`C&@IDAT֛T~<8և|vϜ6 s*z!Xh{ Ʀ攰+Y ,$r11q(f;wޝɉ.LD&s V`RZK"6(oPBCM%,Ar>|ޫamnF(қԦ6cԑQ?uwu?½ z)L^l0VYLV%4 }VQUvv$WXkX^ u C7u@`}F_Mp klCuB]eipsEي%`fNh2i3s|23AGwȡh۫+JŔBYYYq31w}{ʍl{1QŠDZzӱh_D`_]q?~GZ.wnVL 0zX)OCv?5`pV#ܼbBVZg$t"~X,~OT>-wT;gUvrU K /%}{xBwG?y);?t}%0s_<E@CGWe/AC)'%&N9Xspn~פSyYؽu>%j7ݏҚENș/mE>a3p#eF7)(*QҨFSPD\KeU1noSUB_}2Jzz x>8g)oΎS-^siQ. 6d*h‘jE?ڊ|2\jŽd2nje24q^۰}'8[K6A*gϝCVv%v:u'c4~W+SN,: 0qw YV!{ ЈeQF,,52ظxaPUϞ&KOd )5ǡlMEuxMaef An=u)oyE 7CnP&#~LfVScǾ+ Ά<ԩ(\cX"wU+V\z/=!g+//Q\dŒ3'Rj3PN!W-u [ qCl\"NT" K^+}ď6"1/s{i4K^SsjwwqhGum=vEwxb F"AVU}公?s FDlJ"n0ւ`.=)dCBCC#0t =>\;{'2-d ?EХ?Kuk=}[8lr+qpvc[ Q`I9dZ\K^AuUgP{jo,8]0%=#;//0L\zv ?,Ϲ5doÁ#gm㛱/%Z7khBZ YQ8 dP<蚯WcvQĎi^ > }5\8>!(Ql&TTp7= \:?w`Xu~bYsLPV*@+X\S}_qGJ3v Ͼ ylJJkȆ'sC]q%16UPfn/@ۑC_LH Y|R{pP0*rϬXŲ|YvR} |Q;0$~crMUl !@q5Vζjءx0/2QTTq6)ARr"s[ŔCmnE2-ӓ@ ;^xGj?%R1/dr %"ul7=vSqJi6#*ՃY{9h%~;0ɄUrV| {gcCC]Osji.AcL( 7YgS]=Su>#<E\:Zɞ3f=iؐRoJ--$OYZCj䨋1޼m=}{})*0lT(oA&=+sh a{wl%`G#1pT8L=!KFʅ (cqL8J.>`\MOfVOػJV%@Y!{QvW$j ŬE9s,`۬Hyg}_ ?YJx"$F/mtx6߽OU cIm;ivJ6[ H\>up֬Y$^&6~@,z-x%#õ;f^äS1kh|=tPWWSRºLhAtG"39j #}0fÆ~S mKKoPT~u,? ݣb}^ WeE7܄|7Hy:2 Dy4]*֛k)…&Š IU8}4UWGfM;Juڍ^{ YeM4Nq&M͵2ydMRPP@}ȏl\Uejk3AՊR&4dz )-OoϳOb3eG9edoc"T;,$"'9QHG^~ b(%bm.NgҒc)-}q_Ï=FO7zZe؍0jwsTut)3L2ӏ?7zN$,~)>d9ˋAלR? *̊mo݁:bp_(S.Q JCSj0tΔ V2;'hkL,4SӡD'rf"@(uŎwEMYl)1->ƴG3{MYZSh4K|v`\!q.6dB!CQ3~iiDt$,-+~㕟Wɒ2*TI?D4%[PnTW⢮j70DuNbx 7 "^Gc0z$!4RCU;cTPC$4_"SёZ3LJx.m`Z)mΣo.t,`K9]w3MmO_Rz`K[v/ :NNБc(jxki85M5sp߭5vVM]9 qQ=2Sc W& 81ʐcmu|pd?f(r3p1۴ꪪlڱϡY_[6/.<M ut7^ " 4 [VEQ`H$_bRCG܊ c 7^&cٮz"Y-?Wub6h0mMk~\G|x,zIG.!69(m&zxAAʷ6^YBY\Y`;Р `mZO0sǶxEb_*? /=}d4/'kz)$aAa%lxDzqy?s_z`k O'\]3vU dr1,zC U\eߋ|^ =_<# m,Sc kIAv;dصuycʍUbpO2?X!~dA ]<oCcSQ?j*޾^*tc(ŻD*[D"S4 F#UDmoEeE9ʫo{=p!/.@Q{vMiLC'΄2fM ?H6]3ZjT;`ZJpj\ z?qrȎ*(F]-eo):mƜ/S_Ss S-{L8߁ȴ\ԕJLچf5IEx`J&O/~k md%'STV"3ᨧiI~:\. [Nt_w7":N)9oeF;ѿ a,/\ 4zR>x lE(.d`y /Od`$+4552:x_4"BR[Н+a1h sc=Lw_DCdJi܅/% \#*& jcʬq$/*5C` k HܔB+) 3# u3#T7y+ ZR5tڊ7.†;M.58HI{2|<(kNob9+j"|k7Mԧ2$'^ =n RPCph& FUe~Ӹ CYh!HSᑈLʁ&O +ȧTx\c&uaLl2%0 ME[ʕ^SلҬ68N b]`QVR42@~3H|6+ n[Ο%`R|zZ׷Z;kJҖں|2!S/'eGEOI#Zvs:&mO f[gW5`M GqkaD6(;t5`o"m'#SIͻSo mEƵ \`wX6O-NCULFӡ@ɳ 6ArzLHHKNF}-xY$Ɯ?r&_GYTH8"Кkr]ΞF)Z9nb{V'l~<#\8eC{7('ELپu;j zN5&jxx [ 0vP& YN՚'ѩHcJSTb0Ǹ(4nd-$RN~~=ޭFl}Q{#Ϡz0%aJޢD`iBnL0Cs5BAUmd! uEm训"McKS^|ظc/:0Rf&ذe?2#OζoG|GD% +3s,-wI]yyPۄⲒc2^r)Wݔ%Qd,A`YY3qt{mr4;%o'I8t)F5V_:OV~1س{;`BAbVοg]CؓI$U -[1@R-,] wOã-mx(!wK%_I>PRTX̾nBT]9"|Mwy R%U2=ƛ&M ͭ<# ?&56$ͬ!ƿVz%d棝,°$U+X>bY s.g NϻV$bVU0p8ٰh*᝟1r +pb V"_-65k2n*vM:w|*w"mꔳ`=* -%9E.p5j6~#''J$'("Hhxivih^?{Z$J'&&"2QщdvR֐5&IכCR}TNF :|{-X;c%X?"|)`PX8,3K>lex1$,ɸ+&KPՄ \k 5Tں!]OI!NQBMC?Q疢xxle|W0~J|ryhocWW3ΜW=8s~ziK=SfSuq(E]@/9)Iekq49(Y /_2kh}<*JZ{km}j (f *@uk2 URtZ: 3wo(Qf5;;hm_ [Xݺ& t%UJӃ>$y.(jZ][VtrsȚ."PDC-L3PR$ Ѕ3TTƖGQD'`g|$akmի##F$UXS)mQH11W^(rT&(_\\+W ȷeZ:NUSccEQn:.R2ׂrؠPe#=9 eu)?nM4Kҫ ZU UVE+Ԗ/EPӄn((KgCK F/S7s[JX"ۍٓHۙYqx]"GהIoX_LuHO$V[,X..JAD\XqgNB>JbⰞĕ_VxWPDYW-kaKaN aް";O$2U2SdbCV9c}_?| s3QDcfJ1yhi#ا6;7l3IIJ*ӕ(`"kq.֕f4Rzl_:2OK=.HH~D9c NA9wli=F#DbAv.hM' ͠_xkc5 0{Ђ3iR~GU SQXccc X+e{p`GŋaMXi7P^M?zNvp%6!hI&>(Rl읡HYF&0\J΅6cDus%ݶU|M{`F~5SP}Cf[svcyN1 =A-$ؚnX!>ПI IXiӎzl/T L*u_Vǣ 7A)h%{۔n+=*6Աe'  tw?^s-x͓־>x|&?|C}gFroﰒm 6vD;3U^JUygDJ?9da2d'{m-)E"pjv6mxO[?de `j"#'" YD# <#W"p3 yI0$#i N2=Lnf"=LBYfHFwAV~zN=dyVf_WY9Vya'{&}VwZ xx"YJ;ԙw!%)6 aljwp߬%kFx!=ZNڊlI,.”Ȃ3"+R&˦@D~z^L.쭌`O&Udjjh“@YgȳЊxƏJRRU;;;0dマnA8 2 ^r?V6dbQp|\nXg``7=6+G|xFf\fK%&VE- & 'lni´Ak$>+?vdZghI`oI/QE=u/g`M5eMdl݆RT㳱S#A_}Ĉ̵`hS)/u龲@VaZuMf\!Pd[*HWs#== 4^ؔ?< GĹQS fCEx%JfbԔ x m ]CI6XM20\cx_ޒGFMDstİa4a<<Ȟ/*PX\;[#ɯ#rC fONF9mZ,;Ts % )[ E&&k*f@uk*O=QGf!P}0u*eu.OŒBF\.>YƆʎ ]]dԍD =R(KKiE_&ҴCU\Ԃ1'0SN8`m'<5mHʁD+Owf(mHQCdɄ?Ͼ@6k9RҎK؝u #BF L&ݏ,~#.>>ñg?qNҫ@t~1%Ҟ~b/Rb[ 6*ע3 KCmXo-$//[skːL0? ۄXX@ޫK߅c'ϝ`L7 }Z<7VT|IAq d|8*_t#YڔF?!Y U$V/W {Zw.KJ;tahvaB.T:P\Q,a0P0 $'Í /!+8455̔ /*/oaLF,q2{-A0eEDļnd UN W$E!o/J}fm _sSO0c.G=pU$!5+QϹ1(ޅ 7!^LƙKѸ{ӫE™cDZI CBՔ^(/=fd~ǎ_?H |՗j"ɤwq 9 mLH A; G=.梁^>z֭!]UgлZh? r=ZW/L60‰Pj6ax<J6#P4B U\h'rv]GvE_z)}'/s#0etw-~B].6:9IAȼak"Na__ y~'rw$_,<##xUډVOG_^}i3e:MosWΪ woAslnyWWNU+xl=.zl(H@0M4Q.4x^[_V>TbZf5~v^y]>ʙݴU24 )WZRN6_< NE Je0u.YJ s ;ojc@S)x@`&DX #' "tVGQq1t#Sp#~Sѯ53<̳2d e3> IE1Nurq+&kU+ޒFHT#coTe#\CQ_ЗTG_,C_ACGR#.pKb89:FUQ3nXB@g{`d,K G9Ӕ;!RO#yRP-0 8@v(X]u#*`(7Ԕ.VB'T1Y #>@;tuZmǏWwj, z<Ef 3|UT"k5T({;Sg=,#C&2{q ;?pvAci:^u <xӓՖ-U RZRFq̥SHLI#MV+8|:?AIGK :;X8‹ dV#@uMS>cGo_3 uwm9*PH^+G_+Ȁk6ytG}} A}GP ;#6W?#JeJ TtNJ '=z"x.J"7+X`XJj%C}{|$ᬙ $8 !CG7t2.FR$Pg:]); I[?\>F,DnǩMG)YʔTҏvxO($#0t8"xEw]z_ YE%aR2E` yg`=cʕ|[@ ވMxK7I\9CWr2 5eEϥjCSiń'qZ^- RBٚ dMJ)" 0 FӇZL6%-PYQ&[U=+ҧZLI з CF`-/CΕ 8 Wbv4㷶Co6zQOSH}Vb_VMb2M0ib=S3k G &܎2.Np2פrҠNprA\5U陮(e"CF LzLJJ.@iYx0k7/5;: FjlwU!n @ۋ@oo7|ImTm%.]GpSXU_"%q<awݚoqA̟$^^JO-!6I 1xbg_|SGp*] 5upeUn,˖ _t?#)%q~6fW( d" 凬vt(RfrG^ 2KzM߶(iC{ѽ} i vumDFew  $j2!ݘN:|s2AlXx'HُBOzҷQdS@JthR~v,&Y\B2Z@OԔxsz^,bd5QJv@F|W2襸{d?ssq5|:ee†;91Esk'=#\{;hhP꒠[JJ2A<h>Z-X%eR dPׁ)Y"]"+LpSJ:u3VVSƚLW0Õ QZۆSE;Х SD9zx_%dt$4E_oo<)@`Z޶o؆adc_C ?DN`I\WgQI%GUuCX٠8/ f˳l6zب1SZ:zLưXN5W>vb1Iu4HfՔr#.mu'_bɚj"eE115-~AbJ_}D3q⭷^ƼD#|o(2HQ 5וLΰb.F%mpQa @?WԵk}ömd߇zRJ"th!޼yXzⵗ_BeV(uM0BLX{-_yY:wO =l,٪h1xp oooż8V`ekTVx iL_#ÞCxa|)Jz0[k[~ذJ{4[>}gF 癞 S*~' i$wZy =wqa&+dϋ|Q$(U&9z˷]97Q yG@ߎWi⿃7r\Ԯf/dudԕA *j4FfYRgYJ԰X!08ᇰa&l`H@̒j_#;Ы2|\+时 Yy>H&ӴW3^ 6Uȑ8~0zh2BM9^kۼ /$cmj4&j U*F[ȴTԯYŅ[-jj`K.\}1an:AE_MS4Y?y&}G?@7em pw= {-(JFsY^K6!ɬxZD NC#2t͊O)ElDɢ{,ʖ&PMϢd3Z;VPYSJkt!%Sh"iHLN: F[sGLb2zAK p=yWȦFNV"m!ek[뛑]D+2 D萉\!3 I~8J p22BW2-*m2r SZB]>k %5+OW3сpp-NVХ7O("z ѣo } `={U*eg@@5@/͌a?p1\IcW0.^ >^<*3pC@`BHH _<7fV wz&(6c?.Veȿ1z|d@^d2߻=;漧vf"o@Z|{O<`RJ%(޷kYYR"ͫݩd-B:36LΉK,@.#!]E6u3c=wҋPRB^*%Sɞ/yݯ(L.0g={vÚҿ/pH46u`cPHg6n$ 8+AjSy)+lp#z)2u~7geۿ[(.*`g U gqPumWGVG |,1ТAm2`?Eq+)G_ic03.3Cc/O?B NcPQM҆LDFF^F ]}l\v]2s`1!KQU=۾ AzZ* a,Y~d oloڸIACo*,VYS-A{= +3&cքUku5iήpl,L"}$)s+Ѻ3v`*Ub>@H }[ʗ# <pUi{olX tz a#dS?< ?>ʯBy#Cʢ;TE 5!JYl)o(Z]W4Xw؄];7q7q_?v$iMgf8|0Aذi# 82\1#bٲ=fs'-۶w!~eiz0UĐwGZztȋvXwx/)8%I\˞}qll C[I׷g^/2ľ<}PI/cGUy%ٶ9k6eȶnAK5U8K ɚCUYgMU73)M!奬gc'N`[DGŬ YFz:$`|"]K;}OP3CJ*Xq1zP @0j3ZEfnO.ٰi(iPB*3k ̜2AҢL Sڎ> p I`޵kvއӯ0iu>ZGu1Y{Aֵ 7--TIx| nvؾ(+4Fh(SAm8)2]0 ˦Tυ٣ [VA>٢CO>>PPR ꜩ23k*.i ISR؄0`#x"N2QȂG%mmPSr96{qzVȚVZr2>[Ka(+KltτVߐ/\}#rON[KE[)U%k;u7ҝ # <#l߿aP6r@;6#|_G@Y=X>SLI*՟YD硃~Z :93INg;MCjz￷ `zѻݏWa}D';bO-[SQvsnA$PF~)Uiˎ.C3Datq>=Q>xs7uv@_2v#1 >szOv휘I @FV2LOƛ! ,ʯu6fpϖ/kI:c{aĹ-MkKKtUlC ]X|LJgW7A#F`Рo}2IDV';3=D=6j411j.?#F_~tmYnmy)XDĶ!j ^HDIHfjg~9<4u@YffL!GfC_0;{y;y .?bB:dg!4x8.\8 'WP*0-Z\MCQ5!3uSFZ&xk/#-1y<,aH'1-"7Z-2eRmh3` <ݝPF߫'+RrC{Zq[WJZ0I5-H<< fcAhU[ϿY7{73PzV\&3Kz|8qvn[Ο#I GrJ.\+>@V=2n'aѩ潌KK^AxTD\ CC̜0` }4]@{!jE&G~4[&TY*b,1O Wzjެ}ɊlU1Cs'_X6Is=VL?wȣ Y¦(kEVnuzKN& 7v1ل:x7@db^y-kdz%nZ5=W&,Ɩ͛y2X##)~ .~8M4'(..h-|Y:gdey){mozQ/Ix|!_?l%L&T01%#9.8g+._#f}mU8tP7삻d*gS 1yPH_a/p4Ġ_HKdO&FP:cϕڎTBrǗ,|aE`:jhO̕aCfLe_/cl[ dˆ^[*&ܫ,&AZUG}m}-T?{EG?:g߻}N9${r{r# G#Z\xL'\K9YU:cψrE2UNꑙl9CrbGq55'[OL A1L̍al(E F-3iMJIQ;se1D֕c 4uS7#= 1Rd(ǎ5pdXӫ0+r`)nhQ>ҶDeY1z %?o%3(inq E_ER};S+Hm\̈i_i <>* XbjZғn0:1ϧ370Ȃۧ'@FPTlRr|c Zݰ&.5BQ_Έ')Gf>,3)Y!''XdbIO1=`CE-"#: %J&ڪ4LkEI[|\72A1Pl%U.,UJ9vݤt(!; 7o$pп>{p&Sṵ)FN!pZ!TL`kcm̝7s澃Ƿcw_1^/_.t1_'[k:.]BkJWɫLBS|:ܔLwSsؒdt#[6}G)de wPEH j/oYiCV7jZ$4U8}r\bڒmZgeW`pH0} ua`n޶3(-ĝ+غGQ0id`p>d$vu& ^ ]MQTR/@yUn`ݮb1`̘ R܄#1zǎE@qHw(:|$Zʏ<Vzt_y嶭d4ec+BLȉ%p-ꋶ^KE[BD]C!UzƠg˦ه*_ p.#<ޙ=@ R^sšך-fgy `QGH 7n<9=&b!-!~UR+'ACmkr~{@)#XE KVQ~{r#w@&>Y9IjWz2&7m+D@Y gI9Xx A-%% uϰq `+K$&Rr6iS4  5*vۄ|PZYXu kg1i`;,a~{{X5 gB{WX(.}&-98Jݻ'[IS _C3;$fZO{ַSf;:F.z\i CM us@ˣv ~XT8p%-!(@VQ*J%t`Prw dǘC[ 89Q*U=(ߡr۰n#V}Tpذ?zcND2eJ #@ 'ʡp:u\jiX9oncS7X6cR*5|c}r.\LlX%>'iPQn}W{TXG%M_ f0Vť[PҊ (mjCSu j)CKj*j%c@=\ьՠ!c1$R$Q ?bɫ:u!yD:]*d8>!Phi@Ђ{}&7J`ݕ[pt.&P9'+ҤdV)VQRn`aZ|#Xw ti&։Mgd9AGۍ[R\\;5d-G$U0T  fVǻm!gu҄IҢxd;N 3d־l2zC֎  FRUIuKQW_b°QHLCi-D2AaM\igC64uQZVs 1.LB fcx9SώWxlz_6??q9^^^Od ؁mGO )圚|twCYM)EdEaa1)1AX6-Ǐ N h7y!\Jv 8.ʭ7|S72 jq;d܎]ڷA{;#9|Ɣ00+fYjn1r2{~DD#61=a)}W(IeS^`ڪo:cjxuOg.SYqZH`8.̤ع{&JIȋaCǠoDe7pu۵;>&> ۸rgxZ 9Pg$dR^A,(ϣ3vB{Sc |< K*XI5S豚Cy󻸕QB6f8y*R*gW7wx8MFLJ4Ukq'%acPW oZ=KÇ֎Yq<`Q3U|%e ?P|JSVr+FU#^hQXEX  bb6 gs# G@?][JWsN1cr#O β|r$CC[,7l&f+Z| ֖k*KFdaբT,U#C!:(sŜ EWbpI|z6 r@\ }z[/ѡ];̛?L-[ `.ɉ`kHn-݆F{MyPc8jCJjȬӈؤl݀:wVH] 2ޓ!a+'šmAc[2o_y3QvϿ6V=*k[hjbJ:^3OfmYLxʫԦ-<:yKwDYY)prՌ* X[}WYDotpH,2,uvټ،7efu?%4 i2"E100 Iۅ$ɻRwq~isqEh3jV 50> {Dk6#cvH#IXiiP6,k߇mh&%cxb0:wǐ!c/`m?~*^gCAh>ub0Y.#{{?Cd]'q7PO0 փ lw,~5o#~X) *u7sUv*A"AZMV]eW(zQ ">nH#3ZIM{й9jB?O%,TTexqp\q'۷8MMaFuryt!hN,+?y/<*}c=|R~G%dނͭTT UkGG>}}(g"PISinaE^OΈ޸2|$,܉@DX99XAE[~܎Z#^A'MKv._gΜDk-f~Md@%dӬT(!Go\$u?<^9 :̭rN NLs=Xd|-^񱗨

x%yd>Z8<2=zG.R3Ġw~}T`};wRU0.--- zI0@!?3oc%@u!я.3;d%Ɔ#$/߃ %u{?ڼ}v%e:i8dNlO"#tm/>`'T piw4PhH0Ntx8tYp [-m5ć|EoNYY40@,K4zVVFA^k} >ߺm3HOF &3:Su+CzJq+ IuޗD{++}$Nܹs$Y>ઢ њ,0;UPoa,S#Mzѻ OFRb"\ɚ6&λoKY{lCaA>ZZHe(i!Ȏh6'ӺIfߴgEzPhT !d{z`0A0z^n}qm۶.=v !ܠwGϏ|D@U ooa ${5 AXyL}q?= [6FZf4Y۵iOK3O=8sއIr= mY~~ ABEKKϭ=LPWĒIbnFmYacDָ ͙XP-1<[SHNbRvxi{a A9s5ID ^-Kph(aIy046ٳdۄ=E)mju_`-/G"hi#|z8OۆC[~ Ÿ #<G2nP 䬫FEm&"I)Ϭ 'sm*d5M6i&=/ fDan"OcgMʸUdS;&|VB 0k!8꺼 U"sE''l;,1p LZP*Bg`Cr\ #}q}S/aρ訫"L2 )ELЩAS9=[eZSyjK5LܪeG~/.-{DGVW&Y=|t0ߺ?r;ͭ(Ba:)G@9rEuyZR1UrVAֿF3*/[ F6g_D 5&9H,JruD({5)e5b:Y: 02o.S˿T`\ԸY[-$ϯ9ѴޝdJ(&\j55_ ss32b򛕔3t2:LNITFT"f(Bdߵiqsms:8:Xrdz刦՟x^l5&Q0DRWOfQ5^d2rim]A}DF#Ț@GAu)Քش~BbHdj@Ԙ Ci8>ZZzv&N` @! HD]4>u>p:z8u0>_D&Z=zaμzs`T?[ʣJkzxҗ嗣.!42.E. V5v5މ^d'ܩp6j**OocUAz3n߳(ICm)kdjkHҰu`\5[=]]gD\W޻ xҭ0d<3e2, $vRB==Yɶ=+WD]uh i~*=;Y9L%{T'<"70x$[_ᓏH&Uժ^CXb… p^G02,9={SG/e0z1jgNËȸˠI ݏ1lㄤ0ѷ¦[0p=& &%SjU0om8kL@ڝ#$*ʔgK&)AjZY'КR8K)ho\Dݠ\+{+:y^)?~64ݍ^=Ê%'+M+#ce3>({ {-|{HՄjAm+ FkNH'C/FVtzcK^;= HڰuSxWܯ@W&%j\$F]Fzv% Jˡfz:AVy%H j%3#X \HLW;L+ )1rS6BΜACVӾy֭kWhSg_(Mo$L1jȣ Y]$`/ag7)ܽ{o\t@6%fzჇ0m Y9  1?:*غ};ƏyMVq*d6SG2ia#VQQE =?  FWY2 Opr9ro"ou~%?JRUl#Pޔ?9{F@Yxʭ# GYϝ!ȚKEf/rZF@[[KBSS5',TTBik@_7P|L?3}hKVžu\Е5-?rtp8v1ِMU*-ѯwO` `g6duՓ畸E9wamo+WoP4E%hb"anf K['2|ʵ-GmT$^c{蓉[C*/^GK`pjX`OHܺC'lT1d1ݳ#,o8mt uPYJେT- ̾V4t e?^:oANm2_Ҫ:W_-ǨQc$pu*F6Yg__ ⎝{|..IW Oqr e@M,e8e_Bg:hӾ#%d˱wFy:ғb)3+S^F2FF/Šgsp&cLxaV@%c!ڑ;sluj\|>WNeT1H!%] :fg`ɍ!QMF ՟Xx!jXΤH^dnV)AJtw҂5ܯ6lJYPW_!@9؇$X`ŗש'?ItJ)AOߵ߬#a:XtϦ%@SWSзG_\ԔdH +t[b[ƹdR 31a\ ?d|D ]ӧOKұZNo<٠k%]  Yx#5vk1F` 8l[=C~Lћ}PYo!׉w'߼qQUPq |2dyvE8K/ܻEX姸 E%{絳sk$IC1:ΣW3)%Ю9NF ALMJϓr7ueem$ܾ]Xꯗ0y  i[!k,VQCOZ|֭PP 'N"XjD/tIJ-OFrL@qh#k^_lG:fFՋad KU$%{!r4:vۙEsAjtM[w_z1 OOO^C%ssSJֻd ]w5(Qܿ[|aazyn*L=Sv51IDnm`P1C9R\PI|DAYfdBą H.+ԧ7v $m @QUU &QRn]Ct=WˤŶlCQ`Uԗ?4@׀,*x[т@_mzfy9r+ki"7;E@xo54Xlg^9r Oҟ{XE 5X2*׫Y* 5Օ45O;RZLalkr?Z V*7B+-_P/i04N&D-OibYqZ%'ͱeسwl͌$Prq-Q&--3H#}MVQ~DG2Vu~2&Kà 5*&QĠ]َ +sovOV"TPK~6T co&TU֝ {2BIbGb(7~sh. ς| e@uWIբopaLxޚ WbHڿ8īDM_hS:7;wopCn=N[qz۳t#O&;35⬗Δ lF,4u v;ܦ4aǯkz ZndՖ47ؤcM@BJ smi^HGٽR](M'0GV3΄=*aL|,qS'I0}Œ3Ɍw}yZR=aC`έ;^uX9AKG_bЏQ$"d#LV`m 6 Û2:xzJռ\_"G)uPVlJ.=y#o^/_}W/2Q!f4~ܺyYYKG/Q+"h(܊dë!?BDC)݉IT(.44XrhkI"HNG$ ޱÀ1N%sdݻsڅSK,r-&%hA[C7o#٩ҳLxn۲<\COJ .Th3/HUPFi^Oڷ'kS-؃nxZPޔB=&0i- JBa-现 }H`,x~bʜ8G^}GdRz"qShXoXS;&,~mU{|"e8Sai2B|rTr}i~NAm_dvUJ_E ȈmUq}~ }5V ܸ- k&3d.| dgW^%vy_1H,`񯏝9r~kDn \oL߷0koݏ1ߘBXxE?=2O# G@?Un߼JY惹 ZkVRZh,I+_ ffƿjU0%gᕕ` =G薥AS|t|JvnM_,E%s.0@Xv'#[5gb2"M QD5dš{\!XH+6RI77'o¿U')W 6y\'g'8XA0j΍L#mu5ٹ_F[[s8R% Kdcoi}~.^y/G0Xbp6]rrX$Zd>RJ\m"*+++S1+pl:JzQ =&_u`7'Ι`Sgz [bﶯAb*BלL7z>3PTZ1L,l8&3z5*YE Eoٺu{E5?eEUo#͂O?Vo: Ȇ54wu~N\HK$hdػ6xvK!y>8w }QX~y4&;ɦ3>5_bİ1!?f$Ib7 C{P_;ʈvpqѣGwJ:$ kdnɓNѰ08k-.\JfNٻw'[ Y.p7v<(l|Jn}!N5<-UD^!MÅKqM$g$8w?U/z ?Ыw)M7ܸy. h.,z&C9=7l{NZ!,PYY58w4G\]6`DooMgȾԿJ=0RwNMՁEU_#ƌ]FIWSJNĠ!C0\ D"A:%e&\1O8i 8wĵ,}+1c5Q8|q.AYμo3!{?+3 ]05J^=@M,jz0;|PQ#ہ n|~%w]&wZ[JS*&| 5+mD5QTXao)I^V|=ץ\sTQ26l '{;ĆA V*صL.,+!Ż`Cph=xLF7.kQ5+GG ^[z>8?OMnH ɰkF=VTCOSd& eI0Y9r#?@Urr(!dwo#utiP Op-|R1' ~9!U=9LO*/Y(?Y `6M@}mzrAֿYI9r_%6l2]2kvTq8#|H?mpʪI|LdH݃bV*U}vJQvDPsp؀`Ian U @W-jOҤyi `L-$5::r8B!nP~xWRj!#flk0kv f4}c P@)l~ @|r{~ӣ֯ZƶHJMQWW[T`S $jz59e+m[;(6V#]zCGu[ZX B-+70Xy@,šUk$OۉiNf#77gϝƲ% )[#!j} wb%ZBKKЮ}7i)⧀Oٺm{D?v˝1ؼe5aN C۸컶jjfkOZݹe&L*%FṊq 0/.*5ƦDp}挗x~EДGZ2II/}L{jq]ߜ3q,lW?GlH\^d O)q@5EJ S}J=Xs䕔ҋS fPq =N6{>|~TEWÁMe_~=R62[R6;-5%pUkXh0*knnfP"ɸr5(ALHzHߥew(O] U^c ƕF}5S%䇾#;@IDAT,ѩ'KRV4#Ֆҥ2U}ؽ?y)ЭG~QYT桓+doQKkpʷ@%שTTW2e{K%`ZK UdJ0<ژ`GZ偙ؐ_T4*@YuEV-JpXX=L^Ptv5|w@= /tlUk'_l<+!FРJc1`S3Xo(+Sm?Ըp*vev9v6=uw)SpN&[Ùd>LOp|d* v]Y ++U9{ek<b;]3jXfS˭F0} :62dyTuߢkH ~.NN=t 4|^BzD>~ *U`}D Er# GSgCq\(z]S& S)0|*qa)ܽ/"l(āwŖehBDqlr}v~r%9P>>9r#=m98k5IB\ )p5S6):l R/!\!{їbq?e}70i=9.]1rp〻(¡gh 6hée^*"|? 3 !(.m+Džsp2UT }ew$CFbWN?// pQ.eWHvasyܻi89܏LܯIJe_!2,tdIil` O_"XqQ# ۹#-*\]z耉$b|TSƣ_xƄ<#$f>"3'J߫ ],{ ?dҼbJ> 'PmX>}Ty\-`g*M Vv]}I_4v!GNms{mܺmc{{5TF>t1υ\=G 2IA3=A.dЉp"t#ڊWC: ?CB֌?l$`% s87/7qY LW¾'CHzxy{GG^A%V݀Wm$dÞgL4qNB!qwnu} ":Us\)/_CEKdO?A^׹3~ܸdeKqo~Ԧ⏡my 9ژ(xP\Q.Zp6XU̷ꆼa5{6,y̚{duWBT ^JJOLvlrƀ{6@!bbU( 2G7RZrrP USN+1GէPfxY.0'z٦-hW$3Gd =yBL8tx.Ο7Gv; Mz Y.رuDƯ*F D&۵A~v'JdSS ӛ,%0\LZ1to׆$f7ܹ?ko3[i9%[iSeJy6Z~p"\^Z|,X%> DYD>C**^^&#*ց|nZuo%O݂+<&.P\B ,YD R9I{GUB)'_صf m>yG^q< .mZX5.9r# GoIiW3\Um=in1AZbT U=܄;=6F?ZbiH|c IdC}c]طk|J7^n޺IpVR})J4 ̇mrdz*9rB(@o.azM#~V}?}v!i9֦^OTb,LFjL'-Gw(U5j]埠 .}t 5ȴ#3NJ2%EԮ 2ŗ GQЦvc.G.}-,]IqԦG\Xd$ڡR:֜E9U4KjDߦ猨/ DҎdeio }1T\(`{LҴy+zUS܏>YK??0~|Mh >*SXYD6%;xPٜg<\PMQ_q:{(jJv*Z`Ҍٍ݉t9< s]<`Z BP6&:1ط;T>a߫~5􌊊"+R~px6hPsFνj,b%=Hhܵ&Ĥҙ{Hl`&)/5$ߗa$%+[l޴ƏkB OܜސLxHIE*NHC,#ׂވۋ{;{4g+s!ؽ{ƺ'~-j*ᕹoݵ3Jk`g9JwR)|cbҤR[ h!*9u?Nd` jpX(Sag^|VcCӥQ#a eX-,>](lCƲ/_Nvi-}O!'GNrm~3b1ж[ͷAXc3S3}qQ6 F n$ϏEzv)H]V :x _.R2cL_f&S,b4+oMFްiU/oxM 6 >V\(4pA\ XM!}#π@3ۻ38tx?O{^V+)I^V|. c1=y'{=MfSԩÈ錜<Zelݬ2 9PQӆ =`u=̝RυEGIԻxiL? (Lq* Iy /nZ2\L/CF~ :#7 w+`)pF|{uheUؚ2QȀ1A` \~iэ! ?PGeGxuɵ9t:cOPMUzg }kI~o:Ty_;97޿3`)|Ⱦ.?mU? MzI))hwa]5cfʵ+̘rlL6b-\$v0;,"b)Lh(J)ďT3'{9{=+3'ҩ4k)MxO04n8'W4~L;OH>%u* ѿKr#rGJlw3E~ n|qv;L43R6!**4r`,1lX=y~.ֻmGw`ڳoVb\A_,9r(d]+'Y(e6 찋dd̓ ZYSVlǎrJp*"5-˿]w/ȷb'ʀ^b_\*vA( ~ *;F 0YH&k'䒅p5>)}~Iݷc)A?[9zzɥ(`]x7$7$詳Ԧa'EUSϷ}XҔNhi *֋OsLO+ Ы!O/zNb*l ֊ٰёM7k6oF63ffsZ\v^1( ? vx WKR5D +̃X_Fزi=U;PRB>A%S=edVG.QƑ%{U^Id GbKsP[YDY G64uM. ]~!ظ#z92sq'=VfXf]#̱plӮ}T)'v–/@7Yw9D:z>s׋&(=*Xzyn! ,4O=` ƌ K ko?R9-o;Ey__o^âf{2gPWطq~8WU#N*}?#6 :yza J [5Ve 8Af e#m׎Hs(/oΟ kbf !xntEahlLTgg$vXƥXSJze_>zr>j*001Euh1STAnvFJj ;JD"&4*w;B0|@CK\XZ01#ڐ(_Ic`Ȟ,([cee>]F֠>['NĈ߽'(ڲ_8K9@|oXmԖe z&@O) l#ઘnЏfj&({ ߯1'զ!o}&<%uQ^K==7'{Zgј8~L!hK>e!c&!;)@(i9g ?AFJjKU͸雷c$`-]30e<d<^ev?WG:ޝK+**RczA ;yh>}D{jWO.]9̸/oo~4Y UѸ@+C{َ_JZ*zpW.Ā@c0dﵺKќjrصrң) A~&}͝Oq.ظa3ϡj"?j| l߸/KlO,-q_RV`G@V[<lYOÕ"8󚱖>wC{w#P LPze! Ū?+3}b0eK :ڳ reqM} Z|`6ՉC=|Re45 //pd6ٗIr]{wkß̿d  =Tǎ 庺rJTo&At|cL/j&R &!3w^+( ;n.Xr>zv+1xvlJww\T܄Imo2ܷ)"XqKi+X|5{ ][H2.FP oTPҗ7fP69uQ$cVݻGVp-L5V((,DyI4 ֟:]&I$54uZu*ojq 2ak#XZ|#4Mc x_ALTӅ"±Q_ro l|&;;Iӧ~az[ܼ *+̅ ]XnI%&&Ҵ߉)L0B'b͟'Հ xo11# ]P~<`d ՚縈LOǶm6~\wW&iDof~Ø@;xKFƮ= ª`>w8A:x::"/%c[!U&8<$wxhois#%&՛ cNv:E4:۹uEq}C{/bA{$hbM4$&1&$[,Qk%AA  |ͺHF{ ;;ٙw9?Gq9EtG+E3+/'1t#)>U=!7mކ  ʶȞf%9EH.[+Mt3!CLxo Kl.2EbN)*U$jQ6"GVe_6 d@gJ@ݳٖt+X9Rl܊ݗsS"3 OO{%wnVFbÃ`N௙AO\ܬ ʫDOi6S A X}f͌;ύ,*%Sj,~skHLMr)rik jJd&ęPOooS.5c$xz5]DĤLBJ96_C?kϝnz5=;aJXYd:Kj*V`)bmчɢz@ VsLoՉv؅ I#{Yʹ0F;۸CpSYEdݺ zu"0>q;)&M#<x!AQa Q7YPuUKNDȱ>2dL/]N p{~\c*= XXh= c"R=^qS?e3 (2Ȁ" d?"<5Ox-kOhxyhs'޽wYF󂍋rxӓ@)\ue2k j!.ށr}")Sr FsIc€Vp gD94DbJed$`?1O$J~~>jI~[^:!rId At,",1ބ! ( 580Uy;~  ,"97jC(,̭fd@ƒ>Wp;2oL= N3)s|x,ȤO˶5v"@ֹ G6Oѷ%89-S[A.]p#TK0J4qrR ֖pt/aaht{W:y?A;# {>| oۛRd\y~Rc`h@ߗ|%u5qgGOxO>;FEXrt(}0Ыc{I˥?|O ~5[Գ4D 3A.  1 ?G%=7 | Pуҭ"5"9;ĩShgϞGn]mf-8NŐwǡ(+ hhkp1VCQ~:l{mբHrzl} ~;$̙ K4nOVl0ϓhJb6 (é5 NN><^mz o8uo"c6 B @/fa%hoFHz$~lѼ C/vMiݺ5X.HLr4a[y&ЂS WTV.YNO3Ķ O_S{h%u4av7i:Ӷ f͒qt M( ȿmHE^[]ёi '_F峔lS}`lZ&;%ٛX2o#J X܇U…4]9D~aQ~_$6RI;q Lz.ρ.\(3ڶFs~-4Җ; V(IDԴ0iX((.f ` KFb[O\?Eˋ,yN҄+V!tm\nWE>ev~ QGwaB-'6OQ>]j\޶ Aٰ5W56?澾Hydg 4C_e~^"Rb9p1ie,Aia96ۅ gczb~Ԉ>_\Շ;uui֊~ua΍XPn&칳,Ԥ[>?%6S  []^O*:Ud@E@U-Ԩm']Utq]ZZ6QM zwKLAQ~f6i̽)RhyOv-wӿ3,(Q\s˜+5!/TD( פ4(Po%2 NBQL"تqF_Bl]֖QYadYl=;; 4@v\:y 'TT쏍PQR5 8S⼧iS;`Zq4mJ!v`?}{8#Syz,ԣ?$C},ݠߜ5eR,U:zK+K2|#hJCc&:0AZB9死A28D0Ď2z]5f"J ҪHTQ#= ŏdJuhAz9wXw] +"#jkNjVIYg)CidG p5#`֌ٔt,,") d%% s/j^yoc߼00% (^G(_*#J x[8AM)h2p8s)IUVl;* :rh 2)yZ[XgaնNGېAԶ^,@f ;c{$/צ6<%%={RʡF@5sD́KKR~=IJƩ Ζ>뺦'X pR|dAQr8?Q2'#WnD|<'~GaG{6˯|-Ѧ?+Իxo?[-pEl碣cc4'sNh ;ְsy&k(٪Ukcď`V`OŋSo[)5:بFdQbJX >7 fPӇcάn- GgRZdp{{{{i:qbQ4#aN f.EwTm`yCT,عknĤPֶbSyތF6Al4(g %~G|ZdjJ r*7.\ Ĝ#0gh‚sJ:M}[ӷTU5;c'}Z3d| >}6u@ME2t.X>mڳmk<;ݷŖdYz`6m k?_ ȱd(G)o4Hc}0w)*$߽6*  FF)7oL@ڍ8_PBd7 KU w?JԙI;:<&;n`qVA(햆_+ A#+yѺk-]_=3ظs-TӍ=)2Ȫ2rU ;ymBF ;#bhd ?O!kյ$A7cT9pLGݣ *_)Og'GK]mi24Y/-1315E QC櫭il c,}aW_~1(3&!ƦfUWTP? lӦmv\}ht> mJЪx9*:KKK5qm">0"J.%WH_F4Cl%(uukF  (Vuw&te9 PJ:%%^hcD@R7D*2 Gbb"m}W;2*o— HeUAhh5LNɋdu'1 ;pts UHQSvyFF6Hxiyܼ='V@x<qPtJ]L~N?L //222解] !q+[ I%2i-_ɽr-΃%o }avVt.R͛˻|d%FȖIq|`oXXbҢ*wʂf!44#$7fiݏ@ oHWX1}ǔZ!rsobm|;{|4CUop{tlۙO::u6(ǿʕg|}#Uْ9ؾ{;0o<)֡ "Ḥxs6\IhײVΆ#s $(aFjJHF]=[du0Z zp76Wv lm#ϧWH6,C^!b1VT @MO+M6;;fLVyچl`oWٚF<&"j?d6jmz$ 繜7#xiOGGYJyr; Bcynvh#ֱ`eS?-3'4 D'e6J޸lBzEbz.\ݽϬdwJhgQKy鉨sJzMC];NKoţx9Lf=<yd3"8 bJs:P/;@Ym(y{x@VѦW*V6^^TJPv?]z~X:=v:&n^(ϫ?$4TjM`'֬M5#L^0a'UĂKbHIJnK j G^ zfR湍Osf}Nu-pj}o=Y`#=S4i{؄fd}2 gU'-ـs~W `;viTh%}]rފ"JIb~XTo9Dy]|6{vA]G[QISrWb jٝ,P3 AMn?8R<↦氠oK^<\qJ JofLM[A@~ÆHhs,L&F umc`(%>z:PbIt4rRٕP3nȂ D_BHiY`K`۽f~e"|6lՓ4[a-O6o\lߺrmc-^ޢi&!F,?VzwtvnbnSq.Z穌{&L2f%@aM-cԯo 3* #.G/CvP[)WݺC}(+vępxV=O2,B1|xg)gA֊3 #+2Lj:0b"^|~]._ֿ)Ȁ" JC=]ԗ) 0V,oS1l8Y{%dsrG*,FBbn$E%ťhbhĢwE<{FMVWoS~qһ iF[t]8h^]8_M4L x|֔dOLŇ<)O#=Ue) Mu3` ?-ҏo_R,C*X&V:UxLVVNxVU2؂B&? `]b HTJ-Lz{O6Us~PCRҗ(U5VM*Ξ9W(G#L,$0&X#zl;LlCʜOWX'є H/?7C&Up>(!qf* bqO9ݒRdӘ\fXrO7s7O9C@Uvx*VdaAeU,Zi4xT.d.BUVW1}׹GZDEX'YkCC,a#ύ'B\1hCV ))޵c{J2F9d%Q ;n+U9OOOέXr)GX+ғprZSlxRb<"îS:m4l `w?bMWˠZ{ضs/-&!<hYY@倢ROQJxnuAܹSt%_bQQJJkQM TRB?3YY?m {c0.9I7&M|@lMm۷ eK!܌>u63pțkŁAR_ߌKW.+rVZ*WGVb[oxvcG.\оs׬ɯBN\>}91XXN}++fn/wzV!!k!3 B+2tz;r"ƾ;a˟"!׮b/b ESEx $AL[VC#%{HZ`:!I"#q\}86  :jՅ!ztv}轰b2gY1ʓ4yۇ==._ IaE(2_ʀ+U2 NBQ 2X2J)Z@IDATm};!H{(SЄjϼۅUd O>~-(f3JӊI62Y/\έEDfƢc#(”ΚPKԀ=(IY#}9R=%3xp2'g#! crai[@SR6d&w յt'B0;'pG|qa;>1'E]ۈ0g|dFOZAִZ%&ôWrޫJڞRfW[V=xo$yy5kjW+) JsV9<>~)&),2j'^1a GUھe tth-y@W8v$~^NM@C&pÎ56=zڈzZPբbpj2 +45楑f7"(3Æ|q>YZW4<qދ7Gصpb9s?FIa61qT܈/µ hHa7fg>ۧ+zxK>+U0Xt My.dl^hck_iV-[jjS{'yXNAII1<%TX U$2kk6~Rs1qD+N}7UKbkƘE)%"ܺs7غ7$܊NrKnuyr| zݠM Sc'țWK66rz-+ˑy&oD)kdJzcs>Ve͚4@T\PBlݣV=0¼R\\=j?uqdh^1EC׉j3`ElE %s4)zPd@Z3p/Ã{rA&ނzjmIfy˔p `һ{{lc-yCD]-)߁*d}$~Zx&n^*x )KV/d_qA>uy?[1! Ÿb\&ޮzw7xQ~3oⅾ (@֗n^T~.ێ](Ud@ Iq;:d|P``emhd%O}69ML;߅9Y~oQ gK $[L]ۈv)@̄;[wp>"R Gl0#5w{gac&J d;r ҋ,B^Nn%ߔ@ֲr%.pCڊ wԈ҆7&;+V(I# x`RZx<6$-̙ ;|6 ,A#SSj"1:>_OyFy|8}z UKEћסtq+$H+br{|G:8h4d|WÇ+2xxgL,g`8D! ]p!93u5@xx8%u3"99媪p!\č30| o[8ohu`מXe%XVH{a`ŨO6??Sױsݾ݄z \D_d  H/IkSc>|-(? J>6vq-Zw:Yaaذu% Ơކ?==ݐ3dv.f8G.وu$yW ǽ sk ʔ:lۦҚ4o9lzI[ﺹ`=gn<#./vȺﯭbJO 8g!MA[K^ı ߏ5s@HxPTO۞~RS_fgf<[7Gk>T9fUuS.\L+ָXлs[srw^Okk+Kқ؈^DݡD;)W)uWWVR#E9]@hd2蟬/F+" V9yEرs?膾=K,ߓ0OgrUjrw_SBeKǵ| iW"}|3몕Yեshp3[*Ţ>C6# _Kɮg_+^˫ S3jW/@=Pƺon~pڭB_U,EgD\PV86qlNsNn>R9lSFLz{.7$VZiZ" } :=VÁYN Jp.,(kV΀R!Qң`ID=S>Tѣg/z¹O)mZCr |޻U)vªcw 쪺*Щ[0$ӧ'Ss+\xk.W?CUY'n2Ps.ҪۈMǦͫG6Nͪ<BFT]x*1R]ǍK E+o_!no6@y}h Ok}FLʻ~)gNC'J/h>~*""L&X>2r<A}Xa@7,#JW/"lTetڸ!Yy%P_`_lGU ĸb|>Si, :|yqaucS lY瓧ҥk^WӽJ Ռnqa-壄?=AOkiݻο9 MZ(u= ~ )R[?/٧ı ^$@M;V>!!^#PX=voYȗ^^O_`mcMպɈ=w^'=O=7K9ovzR޽WVGϞcm ּǥsPLs8[=cJ07nkx2^D 7ѡCGAUC66Tk0@DmC禒wA%`܈25Q__Gj:eBQÂoz?+d43w.Ο[K@262 :i]-#-37sQrx*mOz6D39|{]4'L1!VLO'W4/S(͇>WR ӠLA, 98f V/[cG!*9Eז<_nq.6֟~j~@8A6у;*)fxu"PdeE]fwCTt,REeN{nܸ*xR۹/TΒ= ?'|D`if[Ч);ϳM~%1{LuH֯5YԐ]C:^YMHbݶMņ]K1"+s vk1֊ (_\n=+2xl9K|  t1+ECX\RZ=d ccȧd(oI=gkB|h L uդoely2zPW^:+5]pL4IOµ,Ӄ[]$G?ЌLJEN!Q01ŝB2HcaRYlgi+.WYэxҕp%o{ 4+dV!i+b -zԙ~Y3=^LtLuPfy=p.+ѹ{*iQm k<լES๼SkzrzXW7ϣ,[04榦V0aNt_|ɥn΄񰤏02`SyhBᾃT 8o7Zn_eURJ*TcKpisR7f"=XIBxdڷ UOšM\ftN^wBBL5Tpr6%%uWS5!N )yiE6%lD2zvP=F^gzQ:!qgEuXa^.Iu/]CslGж]7Jv@VUd O~p 㽘RyEmU=>?"Z1QgDI s{+cA8!o+^Zл&l,Cc dx؍\zI32eߧ%ʝ&ZVfEIz Kus޽ bcЉRBڬ+i 7`:d$}]xaÆMKOV,;{Ȍ{<V. 5^=.OSwFK_b'AmNѱ oE5kdNw*̎.NNCztM̽>S:whH{u'$ ٟ^,*d:{#1wLQ} d[ܑXfU".&M}.[SNIÖ_g}vF,\d 5-5}("E^عK‹"5Ǥ/fy (͚{}wn)3aq)#,Vn",(/&Vi Ide9wܪEKmd YsOPMThdKc*ozCO[R,_5O3OJ[s-M5>^vն?.-XE^VTl"Pdg@+/8)0uHOJ{nض*/VdCGjNlx2 $}!Y`7ȪřEgXѿ" (2Sg`cMj`"GZC`}a'Ou ߞ5',a C](1r~ :2d قfm*}IMz\ 8>-]潨 mZ@=Zߒ@IS~Jx9Ծǹ-_;ai=1yg>].^#;]׊{~u J H?7JPފq OBf^JM7A4v>I9ڄ9tIVboo< _TUgcPhҾ=ths_x=#SQ&}s$i^T2vyK %, ȦsU$ƦGR2RN "FhEpuJԛV)G z6%8[KF_#-%V+(ALn+>dGҿ(z|~g1jdrMxIu $ܽ{ښ-d oao֞sxc@ Əz{7#ZVd⸱R? }tdD$Giw FX@r'f-:BRc6bx.gz U%?`\be̞1 Kyc߳31ZVcu'NC.Suc_KΟPWBn c_GIr 6lG2%ջuB "x/#&LPY-]0lp07l)Ю};DE";\Vߍ4<]ܦ *Pˑ1ش~->5>ѯWZ;XMܥ81e%;wz<۱Vb4sj]mAGE̚a\- - w#:͕' !Q. @zLi GG??.")5vB8{ W*7߾q|WU水4O /W37ƏDln4XPF]3zyfEX> UBO@Sq!k՚7ƍIv0ZxQ;τKMؙhTb^ݺ.GφE5>W"pz>Ag,PSQqIog԰: z*̀T(ج{*U{GGEgt v1R9ٛh#.5k뙥0ђU1X' JH.Ο=kVN:)W~m,-@V'#A<ݒR<>HHLFaQ>s蝶FZ߄5?~ ܡc-P'72 }Ⱦ5ʳ:[`KE KOMy}`Щ.-欈}Ԝ!tf.5 d ZkBFL=>zZtGiJ}_ũS%aia:`u]<~Tj̳U֔:%%}]Ȏ@RRzUڋ'i&I3el RUb!2AZįa'gI)k>H.xàG0d+K{pPaưD' 0;VZFqedϙCބ|oZѪ}@OKCVT&Mf5.J9hB&]ރ=1&MCSSΰ z*ZLbefa#pvrDLtT 5'ޝ!PI01a5#bСaMgǯZ*_DIʾ@u>*||Cyy,dGZfdbJin G¬JVSgIEG|vQAN)zrԼ8A#sO0sظn#ychب!<=ԏ*!!bNEn:{/؟#Gܺk7h$ٵv:vV 眄DRWnM]>q-_ \<(k}lO&v`3ޔZ :wc#)Blwi 򳡭GRS|XO8 3?NF3dT(Cc ؟Y"aOɜk՝.8ga}:r^d^L6ߵr~lR'FFx-b-nh`Pj瞃ص BPAN|:u  ZCR]gAKF8y$E66Vi]5cs1S3pf-ĨA=%뤑b'ÒI 33.VˑE7> !/&GNOSnT?ƅd$L吚9a.ʝoݽ(_9 йcʋOF0]=],W؊Pd@hdd7nͩ{wT7i8?FE0x]X[6>&4rCmM2sZ/uj)+|,oP]-d`)_ =bׅ^Wxu (@;8nEPd΀9_lQZ+*^k)@IYΤ+j,>k-] p;.wC#t >[`| ; tnEWu}#Mܚ 9G$E^0ԟ=%MΞ&[c"{y*EB\!xJm/̆F9\y uCKJ|$ygCAԿ!:E=} -d^hpF o1gW̅R<f-;Cn^r9K 7}o-b{Nz,((x~B-d7_1Y_}uʪxs[3U¦g )>k 7եM֯__ZYINp-!@X/8kGATU8KҀwqOe[ZYƵغU*bH!XDe@2;ӧNb*{1v:ujMVzQEѪ]'I9N_Vӿ J)W +m9r } 4ؾm *oyjoת%x{yc4v3wG^=q;;Vشy;f},_n ;,ưk㧐&HMĎnSz5µ>{ۊcɓ[ptypf(c:8APKG) {=ލFMO1)XbL4{]AzW HR{G 7owggfg۷owΜs 0uTܼxwF 1c4^xiL]&=NM[/ 1Ћb11'jkRH>ӤXlhAO`vt\œE||غ/=qB@~y+͟ ҒV+ױwٙ|yh~ܻw$0KtS6Ab}IH60o0tUp-\4ѷ@8;ׂk[8H Z7[/Dӱ;pkZ_ [2ua8W.]q8{|d%cT2$e5ըvFB_.k 0sJ$8/k|Ѣ8ƾ Ǥ}VodM'VoXMr;>˜QA_=mOǸL'kjl;>[Zp \$KP5n)S jKWh* ޽m";t!tMLjL_1~͈D # gⰍ?vnXn?oܐ`ŏCrf.y?S_&\> wW_MWO|=o">xLqž+]p߰dEtdKq+_9X]Ū*3@uVD;)5P5J\7z3 p5~HY# e)竏IQáA2ܒ$PK,tvhlanXh ל絆8U{#6E>Uua B߻ٳ!(2 {cjPh"<}F e^nn{JaWY;={1zg6oxpx8I 'G$b 1vT~4`s ZSQjffI7wYo&kvz2?jێcީ/ dgYqQ8|20b)yaY$:2=C'kv(|8:xd&ATʋ EcݳHO[O`tw2'U+Hf |ڤ Űֿ:W/`?9IPM61kMe!¤.?:D2 `0b:X&ٱcd⃁X7/B{e-k"  un">$,Ɛ|T7#{}3AU3jg.yV8z#hJl^vۼr96)枥e~W^f&څQcIU < wL9t$&d /|i#~8{ j5?bub1S$ I1g3]؉#O"I2JZZZ\[#sXpKzU!98퓈vj-&n$H=?3I)S&ת&\صߗn#Ic%; `eՒ7hf܀UFCixO_ Y}*&nuXRm7oن6lى.e*_t*hW=f%8@5$%R\cBDEqd\8֥;jc+s$T`b̈_لWd_<%AR+d0_7UsL$NJI!߲ У tR{[g#eZ"|FxZtsuwg1f(bxϞ;6m݀żSPH&X$zڔa;j9cDA"VEs%yC+nY|,Ԭ$ك#ժA0P$b$e%Mݨ1| |xxg}&unhPv@o= -ߧV֦(VsD=NҊDOIMÕ{h׿ G^=(v\}W,6];SQDҲK^FWEary${x.4';5Uh`MIzgO<Т<23R J*L5^Q V=bZPj{)bEģXxE@=f峍'\ `|ke&Gmac br;|D$@YV2"Ȉ|殭z=< X ڴ7!{_qw.&fWE1mow"eWl;qIe.] u8 #YѬ5y }6Ҫr9Fѭ&MY]<+6mIJN}j?Fl]q|n'ٙHgZЋ$]L'c+%r:2(M.1\Hբ|2"mHB./XvX1Hy ضe]Inf*;c>rqAG[^K)kH cEoJr>#B5*dosÆbJۏgAOF=|$uMkŢKx4qH+M1q=O1DFMCm5\Ŭ|xM'1+ee/{_xNtre>Cq2r/ܵЅ)0 ,Ue.BCj*CʕXl[5mH.@Bst'<3g"0M.?r"\p͉_:uLL=kS-<&?ᶋ+zUhn.ӹ&,& !LcT=\> K^l3ywlܼ)<Ι #8/F; ;3t`u耿;̣' miZ<U_ Vؽ4˽ hgLbamsTdxT.G2B 3cf4MQp={d׿xK-[.÷=Vj 5TdYLAd|rKq8l=n; bGMLH26:+nMb^h)P-"IY eD1^5*1)uU"@qi$>i{tu0_ڐ@.KV_TCzmXӰfUU#ZHտnAN$@' eAL‚"wo_Z;#;~ ́>MksZb@,M:b4Sq9H槡,FB+t|2AJJ<^b?`F,X$gV^*? }''9ioĐw de֬k+# 9h i!~;w׾=;C௲7u~alP<@C{@xkCo-<-ZZBFF\'6lِ=kBrGTz2m@IDATefggDXD@'A?`kU0WiJߕ.l«0\3<GCC#WR YQ0sD$ِmmȷVw)a5NMCSyL|vGBrާIGѱ+lK*H(@FVQT@ޕPlJ^$2)L"|FCNN1fFEb_f2()u]af1X^$[KYnTX,[iTRM F r󐣥WH'̴%I @Fca2!)\aҬL=䧝9 }H浬([y4L]637 nx*}rE\\ 5*#C"Ze!E~UԆ޾BS$Sc:?)haGҙMc OПXLzW=~/Е$9~IU҅Xc 0qRmy_},F#@0!C[GO vFӓ␑ \Qd5"eԨХ:yxr0n<nOt7Et}YXrRPTADD$ b$QAk]U?FgpujA2MH>۶ Jt/Tiv.Z]ꅳغu bpq um_PڐTYtL,{4Qj7}t$bАh$ SNd?$} tp(f~~~$i[3S3s ? ʒ4Y$gmL2 :`׮УX)(L CRAKwcװUp2 QhMbrJCUh˶3f*sRB>k[2? 滵xt YYg֐A8ޖk!sĞ}WyGRܽ '`~4ɖmweOS'vٕwQ,t0)'& _z-|GSm ORml&MTT™s7ؽ1f(sMo/`].-W0ֳv")10/TqŊZiG-2v G`YMGN$H<Ťvq\ r҈K,.62SsiQsGvH:\hfZû!yhkl2-Xh@]U$/&b/^8 ZRGx#KģR `~Q_}%\+pyݻv_.o?Ac]_Լ)yn ;¼~!lI:W:PezVt9W. ts4%x(А@QUv7;g\Je78ȸJ/pyqS@JO5o{}U2KB$%k7RZf *xk`Y}c==3Y?o?$oiBju2==䑚/ mG@r BKKjjJ VMu5hPm#9Vs,kq_Jľ郻pr $`dQ DƼ"dL3/bU; Fj3t<)ARIlvЬ2dk7CňXǘExڵӅN;Ct0  Ğ[ѥ[OY t }oYT1UXc Z+ݮR9H"K$ V)H򹪪ugkoXQ[Y>@=wIm0kbEVO+|&('^xnVź#$M2ݻw %AXު+B,kuarM:vvv|mmA$Uֿty1W_?> )`]D<1cQ,?¾Q2OhFAv[Ќn{TFVFwQobeC]Q\V--:&DE)^uƒBVΞĤW8o'? ö/ ?@IA>14;™'&bc⡤ ދqL""CS/W@[^9W ӵcŝN40CGb܅0'F1eߥw~sm_a캊cVّZ:ujyk[{;q1 I2ԙT-[)b|aƦj7X_6.ӢUYF$g(:g YDWWBtj߄'b@0 R֪#d,R2I(q~xw0[pR-ZC;x= WKySzk8c)-- ĤRP,>r*:r +W?t}BpU H VV>62H.Q2d,䒼c`l-i0]Y GHbաk7̘7h8@Wh& /y4%D 0RɐTS%#1sɴ(D2T=ĵ34u#5.[CN=JL-YCcHP엊1/85/&QgXz[Y⏪}0ؓf4U ѓaVa$<#s^g-(;[ն߂@%F#&X+el2f41 4 2ߔ˹lCI}199`+ܺnVO3{gAAYm H*HS9MYZ‹&7aʤ9HH|B<3Wߍ442I]&.ֺ: ` F]!-ՔYY>XVՉZ6YGvgD=@4Bѭ,̄*4lG'vL6bw7͡*?a*K4.#M3x={Wy>D0p"#8(HA1rKW:= v@X+*OC:&Q زw94mL*.HƗ)[LEכ&l[ݡJ8U:m'UM6"ms6X>ox)I tap!.2ն[-:*;۷W} k+bцa}.¨C:$;G}~~ ];VVnر$ۙ1Oщb%W4ǩ\cRirƄ7ؐ);*NzSP!VȉsSQ`e4 5ߣHDs\{Nz6$` )oi0kCkn}aE%9?K(.BqQߧdU`dj bƷ|_')JNN*֊Uu.BEl!4IEp<jUab{ENeFC`2Fu$ڵ&h EroVVW~Olm$"J`NLU3Cǽ[nPGXD$N:4acS@8_35@u0w1~EU(dj. g7t^ڕhҘcgRRD(: U\HKzY%u>Omu*aZU3*EjqVv`?Q|ӹcg$#7/ڡ:u۞DZӧJSH*m_{ٮ&ν>m`Pux_={}i͐!'`q=Squ$͜]0!~*#*4>c>XPYLDhd:\m}*ܙE쫜W)pypzP#/{dȀ9H ~}\]ܒeKB۳`ʕ'yӛpS(,j3sR26kgPP*>VXӧp8i4lȭy < ]4ƬsI>t4n:;#3'Ot] y"Îc`JޟwY?> zl,:lP8?|jbwc(Ǒc[0$Ձ$KYb~V=[g 0F23` iťfb݆-ڬmowg4Ql 0#&7?ڍDjd)63\V6rCn(n6_m$f4$eZaE3)"<lނ7I[Sצ)!=cqa,L&",&ς#H7kDT,\Ya%tj~~u!4OtSxƥ%FIڽWq6L̟=kv֜8ue1g͙ϖ/ir&<~ &!,#hYZxcpXIR2-@nA!d$ O乺 vc` ֪Xc IiA]EDF텻eV-)urx|0` > ͛5?˯Vӳ9 1! \HUM<=]2k$H 69Ym}-S±z+\?K 3$, ?2d;v˾zn_ӧpn4Dί[1i=:d}Ky={@Es=(f}56m r(=ْO?g ?g1V)cd fR^b  RGT1|< }vj;}o/ :d h3_e`@9{x1GϚA2DjZS҈H>cb@Tl c4a/ivaq}Veant꼏CJՙM70c{KlgU\=I/ C zFP+{% pi >X YkCcҽ¥5~"Zw}wܣ%YHKOF!ErroM1N5k=\"@V0,%l{<b^'kqƖk+_aɗ`\?Z˵q}',VrEJJ MeQЄ$_~ 5ͪ׶N87ܩS݆(BV7,f (TsEr-ʇ6M] -ѿ@04~Pl3dR[ˈ '&֓cZAv֘9{ob̞99!be '9@R{7߆qǶ6|<Cn"@_d`Pߚzs6:ε[4;{aja^Ff͸ݱB£q1\}O=@[68Pj|l˱XYU +o``+.7}?xY-,xNr-Ld%Q SqB3]$HI@In:^FZ3(7K76mށ{wo'QW=uNq Xtj%22d1v HSi L,-б"$% +## <9swoS'saJ3{wn$}e-!Hg9x '%9 )#oHI I/(05/=LOۓdYHoݹz  _͎UT%F Z6ᤒ5f!/nm d)K QD1:-:Y6MtuŽ߷$$I'I5Z?|XwCzZ^FMsaOU-lQ:޲'Ʃ okQQ/0y]&lK7SKw\o]m%Řq% dX:tFXbo W77 <XSl|=Ѻ6maKE<޻$We Ԅ&q{@>Ex={@X]o߃EhB9!ٺ-%g#M'gDF ;_}"=wC:ȃhBdءrJqHE]P$Fp?'aj Y5c\]aҡL$VcŚj\&մBߪ>w$R\A*Xkm_K d=v|ibΏh<q 8{fΜA$tFWeu{&0:$gbđѱijn;y-_?81bwZ|F<}GcY oSfa%$cAIkID=A@0[yoCR҅<G#X٢Z4liY},<:&8C-1{\,*gN%(ƎG71f hb 9 _bڸ1~^2g5&o]Fl }#i醻5ړwaaG{1'\%5 d,kAדkjk`@1t7oZnߥGaddCJ]t9uIz ii!ho`'A!@1aŧ"Φhе?M9~cP!(J+˗l*f2mELyX;G.\/BfܦtfDz-NЪW|%u,]3hyf;Аk}TUٽTb;XO:G`ȊU8 @VYtzwX{u'h׃py&J#6kS?u$nr-7:N]aeґb\4H[nɩ\OF'ortCWhaca mFdP_D{4ٲYPX0xU5|4+-PwY݉ry{>n\laߓ^$@VMeWDq>H^̃gc[]r\.LJj7B^3 ݮ:驉DkO[5'=i5bbBb9AdssR`lC~]Vl ի*>b|X Zo"#?U_İ\7/jPNEq[).cx)V?D#(ⴲ>%'c 唠I['Ig e|юR꿫V&+KLdZM-m^ҋG`ni$Sنd"_:yTb~A97j""2Ő9YuYil$ᤣ˥u_hS|j hu=~}߻LΕp2VMD2h$]z}jc\}V/E2ڣEljQ TFFSTew\} Wn#%/~= L}:ZӃ$+L 9`K1%Ri F: }1?ϓ0_+>+$Ab.5zFv%6&od&b@@ٷ1JJбk@zݿoE7TenJC#mD,] 6jbŗ^_j}3$P_bIbۯ `[BfГV+ɮ9 ;X_+5/fX;`eݜ:e:K1vlP;ckY>xxbxp"B r-8V!d$ñ[?,#JXXؖmOmi!Cf暘0i WNqf::ڕ[WX&q~]YPVl : յe˷4a&c`jWy~Mu+ӧi͙ =<([6c4ZPBRxUU$ɴTfǃ'v2`KN3pk e@tA \у{k&acLU㒰NuKt1*Wc^pEE7g*`t c$)^JSbaq܄m2*Xٶ` V/]6leUmzFZz ^W&iJlN9yʹOޝu5,b|6jh °ۍAŃ<Uo Vkۀչ@{}2˞w*0Y]y\Lt9 re45@SSnbe!<*ۇn@E-JxDqwuttp%.mrk*saRtl---b.C^É{Æ /5c>j?5$]wC+&X]"W(WA]v@1`HFYns߷͈n#:~bkWGC̝3n>~Ėh3F OcФ*U-7} DGRF9φ) zr?j&P33FIYnB1Re dեeAV z(b嗼x=az+\ )˳0TŅ xϗ7OJIEGځΞA8ZGvPIVXLL:г2.=I*h=~߅sR~R 1KzF/zd2.x"~ڵk@!y#s!^eaM$)f?:3's۶?&1W9ižRiC[-?_0,@'b|fie aE ;z\ӽ!\oɣP]t47$uibM:NR2vhg wæ?cg#08?.o[/[F#(n˝ÀV&M W:`yŅhJhOӺK?Vl]uqx={qzaT_@V_iu~\hB,p ׷N'| L6N?I)`Ӝ(BC7׌6ӳ1 0BA-QPC[8C"&?nSűCnÏ߯+5eg6kJ,6&z^nq~>b_7'm~ & WlΤS2:~\wK[.ς_ß" ڠ={c6oR7S,~J~}W| hJ֝[W!xLSZ`qHH@'R/99 ČC ſAO&[Y*<`ef<_Rz[sM28 8X-'E䁅$V a,WE\1)1Z)Pl~(`d'CezY1}VT/O`Ъԭ'N9M]LgAB;_,2ۗE nÒ}V~DzEz?F$\֔dyzʕ `NZӒZ*i-l6$'SCXWceg]⬐&^Cܽ OI<E3dęaGkhZFMeuw2 ;ở~ *_|9ijeJ3 }r,VV̙aQ*KH|7mǤɓfuLTI̪ؑ&GffLZwzΞ:.W!ÑH3\cV$cӷС%MQ`%hKqjMiE{ 4:bm`A }I="l%RY+{D[#1k~_={ RV!5dzUիX]OҾѭENǽѺUK&'CkvÕ5 mHc-dH)^%AGCEt1qX88WrDš ;;@W+u!f# ._ ZkŇc0k ! w\`a^L}PW+Ş=\z4K0TDĂzhVW 5)iE_!Yb õڷ7ϳ` ;3,~ùc]w}stgQOnt|OCƥϡԦ-MtdKftAHLex4 4Fx={RuK2+}#3b[6,* ?HK7G[|I@nΎyAضv ҳrɶ~/#^ڵ7/ |OjrFTl,99ӿ֗FM ܊O}7Iӵ* {6x-TT9!P`@R'wVdz "e>_;a$c:[ k|<|̬L,7P;B&7wzlsHJf:>@W3nMu n؍;Ù4ùbZY'7(5MM"ߗq8SxqhӺlaٸ=l.^xx*]paɏ####P;}bY N,MnbGW5/3z? +]{Z\+܊={wjB.X Ҁ+BL TxΛs?7s,㨛GNvM GQ 8:wGm6_dɔ1u)sWԨ(Nhq(Pn}grZ+ca_%YȲ::VyF8es]u -IX78X_ٹ[([$m@XSS?b|yrU{}6;!^$< ҕ%ǔ5@=;)Fs],,,Rib\HQE +;Gаa#DGU@z=jU`a-˸R#cc۷-{,":uA}QS%%ey 4De1fMFRQ`عm3UQa?emoPRT J"YGfDtsz.O˲h^j 7%b=p)fcrl.英r Μ8=IZ7"\H.|qP_Sdb њڴB{ cM?+;b s9jLɝ13,_.$~R62K)?d2ɪ4i. Ʌ}Mٹx:J`Ϲk5Gjz:,nHǻCzFe@Ou-&012(Orr_#"*g.Aγ].3[ 'qIH!Ek}}^m[`L #D:wE&z8p:뀘8ls}#y:PpOR95x0&OGY56I f %0%42JT /s&gW/Q0pXbe֫[W-X& -G hʽ7*V2!7 h)35W낥ޞkbM$j8n-b"a%k>POpw O"p<]1kRaE\Dܺ%֡ fG|ѳx/rAJv֮ ޼ŻEo ":]m2vQ#[aF+`Umo t?XV K% beIQڟ'E!#q]CILC%:E[ ]tEKE6o~<<@TϷبRnڈ6nJիYWCԢʇ<#Gr3N.8Xh\ԡ!,X2uUx9phE=}Cz`ǜ3K|ޢO80$~*He߾ë/L⢩+:e.p_S,W&̺lV./Э/CѴ!tYmj7KBa֜GFviO Vrc jZʼȒϚUvxfr i#óMĄ>kGYV&bie]Hs -DݍdȊ{w օJ=Mm&t1FJ up\"3K-[`ApvñcG`5UL DCOWvh@۸9cHa Vnx"M y¿$==D -YEJVҞ$pss!׵ϰ&xkqS\⇽'/*7wr; _YdJn{. .8j=`$~c#v,NTTT*UeL5yعs?[)_|CyclH>rn@BB8Wp89r( s3?NEMm(?`v߆dž&h)jW3'K$SX4+80ѦCp'!tt 7uQUY`G&> \]p'V=C=sb\Ab]"i J0$ bwgwtnw"xԴu`FVpd9HɃGi># s7: w?q DVuWƽ;' u%X\ۻuFW -FXK \bm߾[b)S'bYb,Ŝ3U朑N={q뱭`C.^-I’g\q],܄;hc+:X҉\jba?VlƨH=&O1Rݱ`ؐ۵w*zh3/s>t; "g7o|3~őf}/ *V7sVg!&C1׳k_O֠w/`gc%m؂:Zs0Ǝm.4KNNXj-ƌO!Wg9hJ&/#YJ6)VU_t) Ӧ-kh'[:~<FA=޷a!2M {OW05h{GG Y?mJqwd*{>U9E#4U}DQ +FZKu;MytlakVc  pS/B7/wo":;.^ )pm&:Ϥ={GE\ C@=GGG"-Vcن-puwInmqhRx8VxrC^jHҊ.NpMk+:P ퟕ[f}<Z&2<*=7ln^ 2_% 7 1b}3<0ڽ vvQ`Y&>k++0?z\*\WAF ReDo~,"s nIGHz  B0ohK>Y$~d8l|-b 3rDp/.K)RW\#X;Äxp~([2oo& J4a$2o/ҚV;B؂.ٯߠ^ .#",;)qeRSAha}V{y@}^{ ȝ!(8{+f(sX =Ua_vj{%Kb#`$kxH8&ޙSF # 1$*,{.̉ޣڏS\ `DkKCؑ|-<<<<)c̰QH&9p9,Lp?:@ȈV+&@BWV,[pUYcV8 #Yl؏&!I"Ku%u9`|[DQ'\+0_ 2\CSls g Y`cH!G#F_!}-;|&S.\]"CWIq23+͇ICm*].6>/ $'Wq˹3GKOxx)_U3?*@uD`?ptu kz\T&+[[0rEV23ĵ %`xtc -tStmWd!lєh6U1tED ֋WVC,D;w ,1IM厲d=OK {bVZ҄Yn G*PVFfVqs_/=fΜJnM.0"W/ pJIDǩUTփb$:phK9Q9Hɉwq\zzx3wW0n WT 59(.|bP|ޥA]C:D񉏐&@6o%w䞲oGTȝ7ɗ=zCWWF8ü(3a,?|;V.D^Y݇5992j4cɫ@]x*v&ޅkW $%%gsps 7&HMʔt0g.ɂ擋p=ɋ}:F~Y-) zZS߻.zE1}::KBd4ۑhX`,{S Wn_b\q v.h(ft=uxsQ1HM{h )gH_ q=~}͐;; ˖P2(4'^ܲkX>####P\&e+>ֻh;P-;"ԉtMVܾUKӭOqX)m^}qt^]?DpfEVabbACQ)˞3΂,hR=d ٳ0f:~֎b]:آLDnӥWvūWx"jjg[BƹʹjcweL==$8+x5k(g.(-@qk/% aT%z9GïGGGL0II2 ^ƌ`:\ߵ=kfsF$/@+zL]UU Y٧oOڌ0mz é󗡦xFx]W jq՛&-=/hlU0Ҝ,TemH*>ЀEW䇀@?&,Eh DmX&Hn7l(ׯe7`Kx7)g7 )p& om=ÿb$z;l:4AW~E%M"4F'{Im?de#,,\u 6X 5v~we=&"?K0)1q#Y˻:Am`(1̎KѺ]7"_%ɢeˠwWg8>y̛a;w& `pn [Rʻ}MIA^LC0#ߘGGGG )B;{ 箅^nުâ.KT. b2Wډ2Zҟu_hG"%ֳl@D$l00{zE$F`7ߔܹ kު^fQ[O}=R0; U"V2rOO##Pn)|G#Wʮɪ GFŃ.̂,k<'Q0I6i `?sZfujA8q2 "3m`UE_KB|l)py<+nX*ݲ Aek -&L5Y73qtr)Brzh+WV5hF{,=<9C$rnha 1]&HzDBOɘدcSoSN_|% naإ"+kˬXra hL%ڱ/WwGlLr7|.U Vk["+6++SPT5"+۾zG'or`em"~A:eRb[G{`-!GGG#G,1ٰa )ɗ@n0@#:6/%SĄc <$Ri?7x"544?yHk&2iz&450tH2 " ƏH dzH}BZtD@<&y6 W:aј?,OE!W!y5|+ܜEK\##P$kˏ##PHL|PV["CzuXDnrʼnVraRu>Ec4q%0l F!EUuix c,2FqI.)1X;faZVܘ`⪊'!PqI+:U\zZEFWUˇM6>R,tATes%O,`[Uo8#Z'=X42咝Kkt*,s+PlEPڽCUY9?[}'Yz #3U[[ F W;8Yؾ _ [I컖7||*;LNs;Ek ͹={sWE&88]] ,Gnc垎u,\4_ٌIS{V`*"oH`ةeŝ|L0+fჸ⚉=S,Պ)"wBٶ0\b53<<<<EX -* (ڻG[wr.77E}u(w7L2PnYE@:r*K56pU<'J0\,z(Ch޾]Z5<)45(p3):7d$x a!g<$ė|!,X:$XޓXz]yxxʁ.]xxj`ݹc~iV*7^ue$+zU4;p%d۱nKE]!F}eS{ PWS;zA@0T60Y/_CSY\knV/[fK,G@,6UqY|z뱣,UF&+ٔU d+6P"tW$,F4wZJhԁ *OS'p[KփXbyS'Lp yzؔaZjGnQTn#Eڴ űxMqʒ>GuE=.^X~+TUp̚#GD~vK~0\tQ)eCqܑ c]pXӫ)c9XLkKlM)xECⵢc43g|O Ƅ>>F.<1i S팕 3h]z޼ W|3e&Wm6D`$)8 cGCr6 'K!?Ȟ&##ZY XFaD\[! *ua<<<< Y:X<4n ]#{Q}Th?L@b8r }1gڹ%o9tqXSɊ[N\E/ZsTP"xq.~fΥς*N9fM╚L{Ln؇1>:>##3/_AmD 2LjT De#Ѫ nA1!>diU<穉P3+i/)^ǪRԂAQI,UX6Aer\i3 *[,Dn]M'@4+ `۞u ^ OҢф̗'0ROU4hԠ;▤7pdwGƯ)oA ׂ3/,_ Thiadh[/΂M⮙GIۿSZɴ)QV;k1nU"ltsg97v0[ش˪)WMU6~^eGb &Łd$;!==5d564KNШc#kARӇ܂&odˎ-0d`Қ@ "*^=N#:L7G}-ŗ<>2lV._^ڴi~-ōպ2>tڵk ڀZpRp2dg? qE@k7Wʪ{FL5Ap\" ♲_)Tg#)9eV/iعl{=?Tx:r򈈎GJj& u؈[>L6܋SC _Mp3Ӂ$N"AA])=͓ät?"P-h![NNT(E6-8nֈk./Jru,STݧh'|[UYbg #toVu3H9nl.J[#ۡ-JT%kaɏ6ɳ}q8M*vBZPP N{Z&L7`d]!>x_TPlbÒܶ #> z`XvLNr #Y!M2(<"cogߑ'y9I?C#}ԟ0n,RLR?mlVlX40n[A%\akLߎE KT_GqsX}5ߏmRh2d⮳_/ 6mm7vu) a&f&W8xndzm}7x͆RFEF;5V._ &ݻزq -,_JQ7n@-D !lk?7{|\DM`e[a͐Wì¶0 oM7Chŵ'ӕkVam%*}×T: _ fBKW xxxxʄ8ɺv^K)h3QVP({{Kgh,)vE@P V~E2ڴH7ϔ;1z" ,Y5Hx5M^n{P_rmdKD>]ZsC6zhApy8T8N%w[P*,{9@%#[V2pBo"'GK\AU5@MG`Pnzظuj[KZe#IP^.d hkbc)i#C=0K۸ʪc&7//YaQxE|LcVYY֯Ņs'0z(1ĪMkq@?LU5LufM5rYk` %99._ꭇ ;((I&`\6lE0,T(6,3p>m6~x_T҃re0 Vf:'GY~SպDUѢdp6%1YYOԥ¡(g}QOvk,JC]S(OVں Ê+^]v:"QPy7Yk7{ΚǐlɥCɲZaÆ ORQMp$ VG.RA^R ?#Cq\B=Mqn4D=-UYݣ[Dy>)?5jڐW=+g;5)@@=/]#uhG^Q׻UGV5.E}o4@SK XC"{EFZ#08F>cF Ŕ\ާX(OR9x3g!$nHaXzqJf3s~xy }~ʤe~w"9|#фgl΋x5.cس{76lt45lخ"ۀ_iH|>###!o~]~_}K\"H늹e]yhzF2:L1z L3`}"{\Fm[`\ۊlnFċtfϝG.u@T/tdo/`eY *}YZӮCKΣK0k=o(TkrX<w^vT7x&qthQ*Z{xIv*GG$kE@ˊvԳ8oц| X B8F]f}ٔ6N:]֠{^\F~,L͇+*|3 ~;n訩b%O7{;de+KBYu CoG>*,³;yU*r;{hhe`uq(>c525>>gϟЁ82Vx6=_Q} dFd=V#`ily5:]3+5f*$Zy"++#gm`K1 Iê:xbS*ZU$X:e="FX{v庻lFgJVĪa}=$`'EؙnËW! thnM1w\㩑uڶi.;HƟ vG~[[mFJբ֢Te4,3S`T0uҬ1v ɽ DwbBiɴq&|GnUunh5h`HE? Ef^ Mx dNxÓF6Rhaf|kp\~ sѯG栴ud=5Uӣ{z+3 iPlyt|4kEDz,2Rsq.]s }`uX#B࿁&uΘ/M&ȓ|EnU }1W+ݡ;;*pbx ǩ(m>/ڹBP.maԤprs+iyǤ&%]_`Νk+KӳպN|4L7hnf+F[S@'"Q>.#Bʕ66IDATB'w#PTnveF"U&~@¹\ɓ'04K_X(u,źY *fZV+V#f(®:Òq#Ye zTXQ/0{ L3+$?swlL~gb֯Ֆ#K01Ǣj$ tM0rhx'OpCo䰘Y35v/U~ܹ#VNبL'HKz';mi믬=qUYUqN; ĺoZ%qΟ|\& BVc6r/-[`o}54quu%RU@¢YC : GF%^*$WΟEpP@:ۈ\zB51&oS1f_d,. xkǷϿO26n\ 2b0X= vl8m;zQ1w7֯[+w(U YFESև(ԄD####_ҳ_PE0aD֞em.s; ?TYr5ٓyX~hzVB)eg_o|=yl^#2MbSr{+SC0_Q.[ͤ(*XgRwO"X!69v|%@c7FlLߎ\ j~?)"xcEȭʘ1S^ A #5)t+ 5Sk`Ū%عm?nEVT">LW Jޢ`9?R=g"`Sox[>####P6~f>]b (<ʶz۞y8ih@!Nod\jxs#aLVvaH=>GwPyHzr;8v j:Dq˵{k~}&k'|jUg^]wQȆ+J4|0Yrfrt0zrDZ V 2`=tL?/`ݲsD|Up- "凐۷>;1u9n dm ^o^Hdz^qk˗XsF}7 7n\Ƣ%6}* (2~L ׈=%} bqazr}^枥W@@/@ ? "6~aS"gYz xbĽ(mh4}P?q-訩pW>UiѢX `DDäT=9u`.YWUat8P77Qs4h q\ӻȐ(=wKNn;|,\lV{lCJ+-<%X٘W^w:L}%XņkK*ɋXX.ia1h(/ ];ċ'J"+lF^: ).XbbV7Y߲T^0/E# ޒU:.|i9`VFL@#Pع$VZ\L |fzZR{b݌Ȣ٤vŅfqWתfrþp R2Nn>vцנZoqaR1u' r Oww̘>Ek.~#KzVO<4AbuA!px"eZCnf ̚:TL`?:W>=j0L4%eZGum,=YiOɁT%-k\]bO"dQD[ϐ; _C3\ylj z !lR+֬c¤ɵz> #KOڜ8lĢnw Yw⢖GCpv4qذn֯߈UAKS u7-[xQZ:gt~噊ìT1_qlWcYwx/M1~hQgϸF=>V>###PEy(_0_HępкԙZXcaټd ĔqJ]+kP%k]H\L>9o9%}Ue3epsvZ_ $rKjbڨإQV=㴧\zE26LH _##P^xK"@@<>VP!U{*GړXesE~ +ħ;j<|>y yJ )@=ȹqu<#W+ VVnem9IA#3avbk/Iۿ tѢ]{Z*N"loI2` 5N]:OFSPl&ݲc܈{ ̉_8X>x(2CBtml rS(/{y죤ղ o;ha CJUA!%Xw?#1&#### F(J<jksAH%+B9L̒%qakn~%r>/5yp)+ӗ!66sq˸ uUf3ܽ' GS'%ʅ',題f<! WU؅cqr Y'Buvhܸ Yڱ -qxRoj:qǑjK ľ[&*ВMWuMg8`Ԑ]j {uR=MKnK@J׈Jle *$^'.":>\vʈ[qlKV00kfg!=h^iM@lz<1s[+[ޞqXoL=icLb(*{׵{YKwok؂bT$g@KE*7726 3< Ρ7)?$ABBF'1󰑣8׿b'}}#xzxJߋ36//`si}l\]XQ1шVDw΄EDC w"0ֶ}쬬0wve=aW'8;qc>N,cb1]Kj#+deUh2r",X)/l}v֔l$*mٍP;L%y Ѩ$ĨN *JXT%+ʕ+`Ѵ1?ga{wa#W#P!xw`bv*@1C&i v7bX/$H*XR!Ў+qcˆVYIf.X%NEzEݮةTFK&+\28ŭֹE7{>86\+"~+ #9>zu˗#ONC-JLӧX=U94mnV$O$󇞁1t+?$92VYDV8<<18rlɻEhެ,׭ǃ{wП6tmȥ4 0/С]"Շē,L0ETwNBn/O3eyQ]y2d Ͽbm,p8e;Wzt?~V5G޺9k?J~>uw., mqH<@!"s^@_=V@F 80z?4Ք*؃*D eʷC 9ݼn=9֍ !ٺKv@ɨqjI+IKŨa?5=Ujܚ#0A ѽG'Z5/S4L04񞷄5kkY4.4ŢEmj ɚֽ03gQ=ʄlaҭ|NNID@t-%+WHV#=ŲҾ;]BK[[[ 4-i *kfs X"52[,s+;/6Z07|xOm5֪rUe  WzkNLnbAN{iB~F ٜgKKEI!Q2AMSjf6ۄĄGMNjA|*.c:Aj VF6=?|n ڍk1qo%;H_LIcC,&Sԙu֌#5kŞ}"-#BrP۠I(]_ !Q+zSL;  w/Ӧ_aq1hg6gQ|b+横'S"Z-ajB Z&`1v31HMSOI{QV/( ;bǷ;qֈ𦨣oRvOŖGe##lŪ9E>ЄpRT4S8?_oCCж7,g` XE H_IWSs\zzV#Gkst,,<#Pg*d9Ũz(¼Wzug dثuO?%/sU7spE U')bL; M@/{׭?LMU]C 9= _ $_^Y '')4),sb)8w9WnCxu'ďÃyv_',Xي\0#:v6b!o٩xɓjL put Xۺ wn4E׬يUcagDI7㚍]! #<n)`xC1t( ߾Z^v6Ç[>WahԵ'BODTF*Eb{{"kez\"Wh3,Db.M4ڵ U]t9uUL.ABAʺ4L"X@.]_ Î;S蒘Oeߛܣ ^.=7$R)YaA#Iw蹬~oZ[]0,l*.1~h]Zn5'j* O$bxơ$kk#1#H,IOr S#n! vOj(Nz0GkH,zeT>Rz^q[ַU۩VFý/^}m!{J.-O|c;oJ~;#|8l L-`J'V_'rKŰяBXš,dAZ$rSFS#;Dp"{A_Õ%p !έs"q] Ї=#Yz!rUDfBB;;,Ktї6Um >0x`leEdyjSa+Vu"Uek>9Yx8¬GÁV_0"tR+._-[oǶTfF`=MqlvKؼy&<|"1-%ўno %g/#ߍΆCt^t2ظdK==ͫ8QG`ҍǞIukL8]G7zei\;=&Z[񥒙Ѵ<#0@XzDza  Rg݊ƒ{Aj3+(A"6t10ҋ+^Dw S|K,i3U.|Y{wa?$2 *sM;E+ڼm|; *SbӑغE| 4b Uќdd1Nb $քÿ7%&MD0@oj |X' !axLd}  P?}0 V|lzmѪJ@ԗ甲rUMkߣH H$'; {VT .!>Ri͂9O>!ꭅ[S`F}">k&XQ%5C0kTxq'T{KJMH.sfʈd.IݭೃqC#caD<---p@^^D Eמw~e;%+WTWb9Ӳ%kˣ3#8R''>]׮!,1hn\2qx,1E !]Xi#Β5GL!qD.ml; }TVAaUMkj]'RO'@O[Y1(arz&f<2#tn!' 4" xs7!WO݌zp/ȵSʱtD~1ewڸ{Lȥ9Y w9|NKޝeziim5Ǩ"Js$ŠUH ŐWXyx71}-Ȣ\)r_;lHX*#jɏNح2+VUmT rVRW)v|UXs5 (ECp>qg=4Y:O !il=)+g/_WMņ錯<|\Z8yŭ¬Yp) >lyYa(~rmHuX*S ?)OmݠΖb$_F>Y( 6a{Z<˩3ۭI1&Y;ҫka W`eedT`=E1% ;*'%%7Zhkڰ[#oV)(`ɢ̛=޾B:RPU2$"?,%&e dlſ0q܃HLLMb̧/; 0MƫYnfG(Y=$X<\UFAIYT$dlHjGRRI2ןL*0n홫gVA:-#sΩ 鿶 YgΜf{HҒR`rq֖n9Pf\! #3>!Q=|-.DVQOZYx&C]\Ǻ/^IBgF`:"7hQ=:\ɓ'O0B꒑{}Tv߰n5b nz}{δ?32eEGt"y27OSHϝmW7xGHNLƚMk80#z< ~^wԓt( b>[n͊ tzWٿ.ս7瓹rQÂ}=wVT!ZY4Ygc2_GD5) ڙf #$bV8eW e-xif)9>8=yg052f$:ls+_C+F͟YSKŸ}GQǣ>L(mrܹ?Ǯߚg|dgˮ?'gڱ#4,QWLP2QG}}63B}뚐|_\'>1k7UЍM%XVE"XEbmEIJŦou{*oo|9B(kn/jwߑVJU/b D{CŘ=wtϿF@sRS!\!XEU$l$&?yLΓ=Fnխu?ʿVIŹ%%~V&j"jqy-VE F@d^J69jseD_S V1G``>pȲЊ`*k7)^kVaW;g,lCZjQGL*IVp!#0Yz7vf0bHw0DjI i{ Y̝k@Ŧvk_EY; 5~r*쯹ؗ5_ymtqĄIi~\",X|>۲ڒuG!R@p`u=gszRYWcǒOjViO^)AV]w43v}&XλTo8عWޜiÝxΞOO.}i!~ܵ]jfonwU[ǟҶ[l5ֺ%*Ff "E}ϮD PA"',EsEO m[ ,- nyyF`F@)z:l-s!#+MJ5֏1hH$ ;3^Gj`Un0iDns F"wh we u߃?^y9yҫ X=KJLW9~<^^Rx;c|̘cAR]d6N[a#sjM̚4|ćWc蠈겦d}1||t:N)AʻEɼ,z2\v mRORv6k18t 21˜`ei!%emeVVؾyOA~X[ I֍=oOȒ4͗6`0qP4ϟNj.P}={"^kcӪ453jh 1fG՟wVA$cC" K*ߤGwWƠpܺX"Z7 w Gi"NA_tN}5ە&Ru(Sgٵ F`:yan]T U.v9>ٶ ځ*j7: PuvRx+9 YN\/`FhjګgVckZuV\GX.uиY=Mzsb`gDzjT]΋Z|0ݡ#R`>.\LCWR AfF:,%& IH|,յD +֥-EZzT'XJ*b:rf9d.d/ID=6FJuuN!cj&n%+9 1DbaF`E@imA$erɥlbR"<=ʚq"{n\i&1ztt)Bŧ ̙%n"֌#0@dUߵc4BVRXx׮]E^>w[Lx%v3nuv-t̏`gk N~}r} >nF'[!‚Esѷo_|f zPow| R}brݥ  ^uk`na!%1)]t0'a#Gh4v9juc͚F (LַزsJ/b`rs9jF:F7ah`)LA]-zwӦO?Yۑ:qip =_^{bU1#0 !p\busPB>Ų2)kO-\mQ\xgKwE+8{6|:™u¶]r:ՙ,^0!2Ifqdtv zGqrPȍ{5P9F@gȤxMgKuFgVTsjhd#Cjf4QQ[呤dعvU&?{N.p$Op EݔHX.Zӟx|}.\l[ ~Zm;l:|p$=DJsSGyfQ*IV|J묤hXٻJi/?!r*g snN귿gj&$$"88HeChrU&ʬKTM{A—VV6,%mr{F`F)TqAFfETٝTl޺Cxrt2ٯ_g]>`d?F`j `M(ޖt<ȪK뾃{Qz,ڿK’ҋ4]YX`%oёkCL-, N+LxeiQ\]]^5 ||ae8Uף"0 !?߽~&fpWz\ -._'0s_`ep{Ytp7on߮Ľ{wLrI$ H #ڟ>c)ޕ(ޫC:8*,XmgѰڛkF`F-`U=]P_7~d̝5~%=?/عݕ8A=x\0t"4C~LOIENDB`colmap-4.0.4/doc/index.rst000077500000000000000000000103071517363634500154210ustar00rootroot00000000000000COLMAP ====== .. figure:: images/sparse.png :alt: Sparse reconstruction of central Rome. :figclass: align-center Sparse model of central Rome using 21K photos produced by COLMAP's SfM pipeline. .. figure:: images/dense.png :alt: Dense reconstruction of landmarks. :figclass: align-center Dense models of several landmarks produced by COLMAP's MVS pipeline. About ----- COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. The software is licensed under the new BSD license. The latest source code is available at `GitHub `_. COLMAP builds on top of existing works and when using specific algorithms within COLMAP, please also cite the original authors, as specified in the source code. Download -------- Executables and other resources can be downloaded from https://demuc.de/colmap/. Getting Started --------------- 1. Download the `pre-built binaries `_ or build the library manually from `source `_ (see :ref:`Installation `). 2. Download one of the provided datasets (see :ref:`Datasets `) or use your own images. 3. Use the **automatic reconstruction** to easily build models with a single click (see :ref:`Quickstart `). Support ------- Please, use `GitHub Discussions `_ for questions and the `GitHub issue tracker `_ for bug reports, feature requests/additions, etc. Citation -------- If you use this project for your research, please cite:: @inproceedings{schoenberger2016sfm, author={Sch\"{o}nberger, Johannes Lutz and Frahm, Jan-Michael}, title={Structure-from-Motion Revisited}, booktitle={Conference on Computer Vision and Pattern Recognition (CVPR)}, year={2016}, } @inproceedings{schoenberger2016mvs, author={Sch\"{o}nberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael}, title={Pixelwise View Selection for Unstructured Multi-View Stereo}, booktitle={European Conference on Computer Vision (ECCV)}, year={2016}, } If you use the global SfM pipeline (GLOMAP), please cite:: @inproceedings{pan2024glomap, author={Pan, Linfei and Barath, Daniel and Pollefeys, Marc and Sch\"{o}nberger, Johannes Lutz}, title={{Global Structure-from-Motion Revisited}}, booktitle={European Conference on Computer Vision (ECCV)}, year={2024}, } If you use the image retrieval / vocabulary tree engine, please cite:: @inproceedings{schoenberger2016vote, author={Sch\"{o}nberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc}, title={A Vote-and-Verify Strategy for Fast Spatial Verification in Image Retrieval}, booktitle={Asian Conference on Computer Vision (ACCV)}, year={2016}, } Acknowledgments --------------- COLMAP was originally written by `Johannes Schönberger `__ with funding provided by his PhD advisors Jan-Michael Frahm and Marc Pollefeys. The team of core project maintainers currently includes `Johannes Schönberger `__, `Paul-Edouard Sarlin `_, and `Shaohui Liu `_. The Python bindings in PyCOLMAP were originally added by `Mihai Dusmanu `_, `Philipp Lindenberger `_, and `Paul-Edouard Sarlin `_. The project has also benefitted from countless community contributions, including bug fixes, improvements, new features, third-party tooling, and community support (special credits to `Torsten Sattler `_). .. toctree:: :hidden: :maxdepth: 2 install tutorial concepts features database cameras rigs format datasets gui cli pycolmap/index faq changelog contribution license bibliography legacy colmap-4.0.4/doc/install.rst000077500000000000000000000317311517363634500157640ustar00rootroot00000000000000.. _installation: Installation ============ You can either download one of the pre-built binaries or build the source code manually. Pre-built binaries and other resources can be downloaded from https://demuc.de/colmap/. An overview of system packages for Linux/Unix/BSD distributions are available at https://repology.org/metapackage/colmap/versions. Note that the COLMAP packages in the default repositories for Linux/Unix/BSD do not come with CUDA support, which requires a manual build from source, as explained further below. For Mac users, `Homebrew `__ provides a formula for COLMAP with pre-compiled binaries or the option to build from source. After installing homebrew, installing COLMAP is as easy as running ``brew install colmap``. COLMAP can be used as an independent application through the command-line or graphical user interface. Alternatively, COLMAP is also built as a reusable library, i.e., you can include and link COLMAP against your own C++ source code, as described further below. Furthermore, you can use most of COLMAP's functionality with :ref:`PyCOLMAP ` in Python. ------------------ Pre-built Binaries ------------------ Windows ------- For convenience, the pre-built binaries for Windows contain both the graphical and command-line interface executables. To start the COLMAP GUI, you can simply double-click the ``COLMAP.bat`` batch script or alternatively run it from the Windows command shell or Powershell. The command-line interface is also accessible through this batch script, which automatically sets the necessary library paths. To list the available COLMAP commands, run ``COLMAP.bat -h`` in the command shell ``cmd.exe`` or in Powershell. The first time you run COLMAP, Windows defender may prompt you with a security warning, because the binaries are not officially signed. The provided COLMAP binaries are automatically built from GitHub Actions CI machines. If you do not trust them, you can build from source as described below. Docker ------ COLMAP provides a pre-built Docker image with CUDA support. For detailed instructions on how to build and run COLMAP using Docker, please refer to the `Docker documentation `__. ----------------- Build from Source ----------------- COLMAP builds on all major platforms (Linux, Mac, Windows) with little effort. First, checkout the latest source code:: git clone https://github.com/colmap/colmap Under Linux and Mac, it is generally recommended to follow the installation instructions below, which use the respective system package managers to install the required dependencies. Alternatively, the instructions for VCPKG can be used to compile the required dependencies from scratch on more exotic systems with limited system packages. The VCPKG approach is also the method of choice under Windows, compute clusters, or if you do not have root access under Linux or Mac. Debian/Ubuntu ------------- *Recommended dependencies:* CUDA (at least version 11.X) Dependencies from the default Ubuntu repositories:: sudo apt-get install \ git \ cmake \ ninja-build \ build-essential \ libboost-program-options-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libopenimageio-dev \ openimageio-tools \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libgmock-dev \ libsqlite3-dev \ libglew-dev \ qt6-base-dev \ libqt6opengl6-dev \ libqt6openglwidgets6 \ libcgal-dev \ libceres-dev \ libsuitesparse-dev \ libcurl4-openssl-dev \ libssl-dev \ libmkl-full-dev # Fix issue in Ubuntu's openimageio CMake config. # We don't depend on any of openimageio's OpenCV functionality, # but it still requires the OpenCV include directory to exist. sudo mkdir -p /usr/include/opencv4 Alternatively, you can also build against Qt 5 instead of Qt 6 using:: qtbase5-dev libqt5opengl5-dev To compile with **CUDA support**, also install Ubuntu's default CUDA package:: sudo apt-get install -y \ nvidia-cuda-toolkit \ nvidia-cuda-toolkit-gcc Or, manually install the latest CUDA from NVIDIA's homepage. During CMake configuration, specify ``-DCMAKE_CUDA_ARCHITECTURES=native``, if you want to run COLMAP only on your current machine (default), "all"/"all-major" to be able to distribute to other machines, or a specific CUDA architecture like "75", etc. Configure and compile COLMAP:: git clone https://github.com/colmap/colmap.git cd colmap mkdir build cd build cmake .. -GNinja -DBLA_VENDOR=Intel10_64lp ninja sudo ninja install Run COLMAP:: colmap -h colmap gui Under **Ubuntu 22.04**, there is a problem when compiling with Ubuntu's default CUDA package and GCC, and you must compile against GCC 10:: sudo apt-get install gcc-10 g++-10 export CC=/usr/bin/gcc-10 export CXX=/usr/bin/g++-10 export CUDAHOSTCXX=/usr/bin/g++-10 # ... and then run CMake against COLMAP's sources. Notice that the ``BLA_VENDOR=Intel10_64lp`` option tells CMake to find Intel's MKL implementation of BLAS. If you decide to compile against OpenBLAS instead of MKL, you must install and select the OpenMP version under Debian/Ubuntu because of `this issue `__. Mac --- Dependencies from `Homebrew `__:: brew install \ cmake \ ninja \ boost \ eigen \ openimageio \ curl \ libomp \ metis \ glog \ googletest \ ceres-solver \ suitesparse \ qt \ glew \ cgal \ sqlite3 brew link --force libomp Configure and compile COLMAP:: git clone https://github.com/colmap/colmap.git cd colmap mkdir build cd build cmake .. -GNinja ninja sudo ninja install If you have Qt 5 installed on your system as well, you might have to temporarily link your Qt 5 installation while configuring CMake:: brew unlink qt && brew link --force qt cmake ... Run COLMAP:: colmap -h colmap gui Windows ------- *Recommended dependencies:* CUDA (at least version 11.X), Visual Studio 2019 or newer On Windows, the recommended way is to build COLMAP using VCPKG:: git clone https://github.com/microsoft/vcpkg cd vcpkg .\bootstrap-vcpkg.bat .\vcpkg install colmap[cuda,tests]:x64-windows To compile CUDA for multiple compute architectures, please use:: .\vcpkg install colmap[cuda-redist]:x64-windows Please refer to the next section for more details. VCPKG ----- COLMAP ships as part of the VCPKG distribution. This enables to conveniently build COLMAP and all of its dependencies from scratch under different platforms. Note that VCPKG requires you to install CUDA manually in the standard way on your platform. To compile COLMAP using VCPKG, you run:: git clone https://github.com/microsoft/vcpkg cd vcpkg ./bootstrap-vcpkg.sh ./vcpkg install colmap:x64-linux VCPKG ships with support for various other platforms (e.g., x64-osx, x64-windows, etc.). To compile with CUDA support and to build all tests:: ./vcpkg install colmap[cuda,tests]:x64-linux The above commands will build the latest release version of COLMAP. To compile the latest commit in the dev branch, you can use the following options:: ./vcpkg install colmap:x64-linux --head To modify the source code, you can further add ``--editable --no-downloads``. Or, if you want to build from another folder and use the dependencies from vcpkg, first run ``./vcpkg integrate install`` (under Windows use pwsh and ``./scripts/shell/enter_vs_dev_shell.ps1``) and then configure COLMAP as:: cd path/to/colmap mkdir build cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release cmake --build . --config release --target colmap --parallel 24 Anaconda/Mamba -------------- Install miniconda and run the following commands. You can replace ``conda`` with ``mamba`` for faster package installation:: conda create -n colmap python=3.12 conda config --add channels conda-forge conda config --set channel_priority strict conda install \ cmake \ ninja \ boost \ ccache \ eigen \ openimageio \ curl \ metis \ glog \ gtest \ ceres-solver \ suitesparse \ qt \ glew \ sqlite \ cgal-cpp \ mesa-libgl-devel-cos7-x86_64 \ cuda-compiler==12.6.2 \ cuda-cudart-dev \ cuda-nvrtc-dev \ libcurand-dev git clone https://github.com/colmap/colmap.git cd colmap mkdir build cd build cmake .. -GNinja ninja .. _installation-library: ------- Library ------- If you want to include and link COLMAP against your own library, the easiest way is to use CMake as a build configuration tool. After configuring the COLMAP build and running ``ninja/make install``, COLMAP automatically installs all headers to ``${CMAKE_INSTALL_PREFIX}/include/colmap``, all libraries to ``${CMAKE_INSTALL_PREFIX}/lib/colmap``, and the CMake configuration to ``${CMAKE_INSTALL_PREFIX}/share/colmap``. For example, compiling your own source code against COLMAP is as simple as using the following ``CMakeLists.txt``:: cmake_minimum_required(VERSION 3.10) project(SampleProject) find_package(colmap REQUIRED) # or to require a specific version: find_package(colmap 3.4 REQUIRED) add_executable(hello_world hello_world.cc) target_link_libraries(hello_world colmap::colmap) with the source code ``hello_world.cc``:: #include #include #include #include int main(int argc, char** argv) { colmap::InitializeGlog(argv); std::string message; colmap::OptionManager options; options.AddRequiredOption("message", &message); if (!options.Parse(argc, argv)) { return EXIT_FAILURE; } std::cout << colmap::StringPrintf("Hello %s!\n", message.c_str()); return EXIT_SUCCESS; } Then compile and run your code as:: mkdir build cd build export colmap_DIR=${CMAKE_INSTALL_PREFIX}/share/colmap cmake .. -GNinja ninja ./hello_world --message "world" The sources of this example are stored under ``doc/sample-project``. ---------------- Shared Libraries ---------------- By default, COLMAP builds static libraries. To build shared/dynamic libraries instead, enable the ``BUILD_SHARED_LIBS`` option:: cmake .. -GNinja -DBUILD_SHARED_LIBS=ON Trade-offs compared to static libraries: - **Faster incremental linking**: Only the changed shared library needs to be re-linked during development, rather than all executables. - **Reduced disk usage**: Multiple executables share the same library files on disk and in memory. - **No cross-library optimization**: The compiler cannot inline or apply link-time optimization (LTO/IPO) across shared library boundaries, which reduces runtime performance. - **Symbol resolution overhead**: The dynamic linker resolves symbols at load time, adding minor startup cost and indirect call overhead. For development workflows, shared libraries can significantly speed up edit-compile-test cycles. For production or benchmarking, static libraries are recommended. ---------------- AddressSanitizer ---------------- If you want to build COLMAP with address sanitizer flags enabled, you need to use a recent compiler with ASan support. For example, you can manually install a recent clang version on your Ubuntu machine and invoke CMake as follows:: CC=/usr/bin/clang CXX=/usr/bin/clang++ cmake .. \ -DASAN_ENABLED=ON \ -DTESTS_ENABLED=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo Note that it is generally useful to combine ASan with debug symbols to get meaningful traces for reported issues. ------------- Documentation ------------- 1. Install latest pycolmap for up-to-date pycolmap API documentation. 2. Build the documentation:: cd path/to/colmap/doc pip install -r requirements.txt make html open _build/html/index.html # preview results Alternatively, you can build the documentation as PDF, EPUB, etc.:: make latexpdf open _build/pdf/COLMAP.pdf 3. Clone the website repository `colmap/colmap.github.io `__. 4. Copy the contents of the generated files at ``_build/html`` to the cloned repository root. 5. Create a pull request to the `colmap/colmap.github.io `__ repository with the updated files. 6. (Optional, if main release) Copy the previous release as legacy to the "legacy" folder, under a folder with the release number `see here `__. colmap-4.0.4/doc/legacy.rst000066400000000000000000000007671517363634500155640ustar00rootroot00000000000000Legacy Documentations ===================== .. toctree:: :maxdepth: 1 v3.13 (2025-11-07) v3.12 (2025-06-30) v3.11 (2024-11-28) v3.10 (2024-07-23) v3.9 (2024-01-06) v3.8 (2023-01-31) colmap-4.0.4/doc/license.rst000066400000000000000000000036471517363634500157420ustar00rootroot00000000000000License ======= The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its thirdparty dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. .. code-block:: text Copyright (c), ETH Zurich and UNC Chapel Hill. 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 the name of ETH Zurich and UNC Chapel Hill nor the names of its 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 COPYRIGHT HOLDERS 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. colmap-4.0.4/doc/make.bat000077500000000000000000000144731517363634500151750ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\COLMAP.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\COLMAP.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end colmap-4.0.4/doc/pycolmap/000077500000000000000000000000001517363634500154005ustar00rootroot00000000000000colmap-4.0.4/doc/pycolmap/cost_functions.rst000066400000000000000000000002051517363634500211670ustar00rootroot00000000000000.. _pycolmap/cost_functions: Cost Functions ============== .. automodule:: pycolmap.cost_functions :members: :undoc-members: colmap-4.0.4/doc/pycolmap/index.rst000066400000000000000000000023051517363634500172410ustar00rootroot00000000000000.. _pycolmap/index: PyCOLMAP ======== PyCOLMAP exposes to Python most capabilities of COLMAP. Installation ------------ Pre-built wheels for Linux, macOS, and Windows can be installed using pip:: pip install pycolmap The wheels are automatically built and pushed to `PyPI `_ at each release. To benefit from GPU acceleration, wheels built for CUDA 12 (only for Linux - for now) are available under the `package pycolmap-cuda12 `_. To build PyCOLMAP from source, follow these steps: 1. Install COLMAP from source following :ref:`installation`. 2. Build PyCOLMAP: * On Linux and macOS:: python -m pip install . * On Windows, after installing COLMAP via VCPKG, run in powershell:: python -m pip install . ` --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` --cmake.define.VCPKG_TARGET_TRIPLET="x64-windows" Some features, such as cost functions, require that `PyCeres `_ is installed in the same manner as PyCOLMAP, so either from PyPI or from source. API ----- .. toctree:: :maxdepth: 2 pycolmap cost_functions colmap-4.0.4/doc/pycolmap/pycolmap.rst000066400000000000000000000001511517363634500177530ustar00rootroot00000000000000.. _pycolmap/pycolmap: pycolmap ============ .. automodule:: pycolmap :members: :undoc-members: colmap-4.0.4/doc/requirements.txt000066400000000000000000000000741517363634500170410ustar00rootroot00000000000000sphinx==9.1.0 sphinx-toolbox==4.1.2 sphinx-rtd-theme==3.1.0 colmap-4.0.4/doc/rigs.rst000066400000000000000000000221361517363634500152560ustar00rootroot00000000000000.. _rig-support: Rig Support =========== COLMAP has native support for modeling sensor rigs during the reconstruction process. The sensors in a rig are assumed to have fixed relative poses between each other with one reference sensor defining the origin of the rig. A frame defines a specific instance of the rig with all or a subset of sensors exposed at the same time. For example, in a stereo camera rig, one camera would be defined as the reference sensor and have an identity ``sensor_from_rig`` pose, whereas the second camera would be posed relative to the reference camera. Each frame would then usually be composed of two images as the measurements of both of the cameras at the same time. Workflow -------- By default, when running the standard reconstruction pipeline, each camera is modeled with a separate rig and thus each frame contains only a single image. To model rigs, the recommended workflow is to organize images by rigs and cameras in a folder structure as follows (ensure that images corresponding to the same frame have identical filenames across all folders):: rig1/ camera1/ image0001.jpg image0002.jpg ... camera2/ image0001.jpg # same frame as camera1/image0001.jpg image0002.jpg # same frame as camera1/image0002.jpg ... ... rig2/ camera1/ ... ... ... As a next step, we would first extract features using:: colmap feature_extractor \ --image_path $DATASET_PATH/images \ --database_path $DATASET_PATH/database.db \ --ImageReader.single_camera_per_folder 1 By default, the resulting database now contains a separate rig for each camera and a separate frame for each image. As such, we must adjust the relationships in the database with the desired rig configuration. This is done using:: colmap rig_configurator \ --database_path $DATASET_PATH/database.db \ --rig_config_path $DATASET_PATH/rig_config.json where the ``rig_config.json`` could look as follows, if the relative sensor poses in the rig are known a priori:: [ { "cameras": [ { "image_prefix": "rig1/camera1/", "ref_sensor": true }, { "image_prefix": "rig1/camera2/", "cam_from_rig_rotation": [ 0.7071067811865475, 0.0, 0.7071067811865476, 0.0 ], "cam_from_rig_translation": [ 0, 0, 0 ] } ] }, { "cameras": [ { "image_prefix": "rig2/camera1/", "ref_sensor": true }, ... ] }, ... ] Notice that this modifies the rig and frame configuration in the database, which contains the full specification of rigs that we later feed as an input to downstream processing steps. With known calibrated camera parameters, each camera can optionally also have specified ``camera_model_name`` and ``camera_params`` fields. For more fine-grain configuration of rigs and frames, the most convenient option is to manually configure the database using pycolmap by either using the ``apply_rig_config`` function or by individually adding the desired rig and frame objects to the reconstruction for the most flexibility. Next, we run standard feature matching. Note that it is important to configure the rigs before sequential feature matching, as images in consecutive frames will be automatically matched against each other. Finally, we can reconstruct the scene using the standard ``mapper`` command with the option of keeping the relative poses in the rig fixed using ``--Mapper.ba_refine_sensor_from_rig 0``. Unknown rig sensor poses ------------------------ If the relative poses of sensors in the rig are not known a priori and we only know that a specific set of sensors are rigidly mounted and exposed at the same time, one can attempt the following two-step reconstruction approach. Before starting, ensure to organize your images as detailed above and perform feature extraction with the ``--ImageReader.single_camera_per_folder 1`` option. Next, reconstruct the scene without rig constraints by modeling each camera as its own rig (the default behavior of COLMAP without further configuration). Note that this can be a partial reconstruction from a subset of the full set of input images. The only requirement is that each camera must have at least one registered image in the same frame with a registered image of the reference camera. If the reconstruction was successful and the relative poses between registered images look roughly correct, we can proceed with the next step. The ``rig_configurator`` can also work without ``cam_from_rig_*`` transformations. By providing an existing (partial) reconstruction of the scene, it can compute the average relative rig sensor poses from all registered images:: colmap rig_configurator \ --database_path $DATASET_PATH/database.db \ --input_path $DATASET_PATH/sparse-model-without-rigs-and-frames \ --rig_config_path $DATASET_PATH/rig_config.json \ [ --output_path $DATASET_PATH/sparse-model-with-rigs-and-frames ] The provided ``rig_config.json`` must simply omit the respective ``cam_from_rig_rotation`` and ``cam_from_rig_translation`` fields. Now, we can either run rig bundle adjustment on the (optional) output reconstruction with configured rigs and frames:: colmap bundle_adjuster \ --input_path $DATASET_PATH/sparse-model-with-rigs-and-frames \ --output_path $DATASET_PATH/bundled-sparse-model-with-rigs-and-frames or alternatively start the reconstruction process from scratch with rig constraints, which may lead to more accurate reconstruction results:: colmap mapper --image_path $DATASET_PATH/images \ --database_path $DATASET_PATH/database.db \ --output_path $DATASET_PATH/sparse-model-with-rigs-and-frames Example ------- The following example shows an end-to-end example for how to reconstruct one of the ETH3D rig datasets using COLMAP's rig support:: wget https://www.eth3d.net/data/terrains_rig_undistorted.7z 7zz x terrains_rig_undistorted.7z colmap feature_extractor \ --database_path terrains/database.db \ --image_path terrains/images \ --ImageReader.single_camera_per_folder 1 The ETH3D dataset conveniently comes with a groundtruth COLMAP reconstruction that we use to configure the sensor rig poses as well as camera models using:: colmap rig_configurator \ --database_path terrains/database.db \ --rig_config_path terrains/rig_config.json \ --input_path terrains/rig_calibration_undistorted with the ``rig_config.json``:: [ { "cameras": [ { "image_prefix": "images_rig_cam4_undistorted/", "ref_sensor": true }, { "image_prefix": "images_rig_cam5_undistorted/" }, { "image_prefix": "images_rig_cam6_undistorted/" }, { "image_prefix": "images_rig_cam7_undistorted/" } ] } ] Notice that we do not specify the sensor poses, because we used an existing reconstruction (in this case, the groundtruth but it can also be a reconstruction without rig constraints, as explained in the previous section) to automatically infer the average rig extrinsics and camera parameters. Next, we sequentially match the frames, since they were captured as a video:: colmap sequential_matcher --database_path terrains/database.db Depending on the accuracy of the provided sensor_from_rig poses, you can optionally enable the option `--FeatureMatching.rig_verification 1` or, if you know that the sensors within the same frame do not have visual overlap, you can enable the option `--FeatureMatching.skip_image_pairs_in_same_frame 1`. Finally, we reconstruct the scene using the mapper while keeping the groundtruth sensor rig poses and camera parameters fixed:: mkdir -p terrains/sparse colmap mapper \ --database_path terrains/database.db \ --Mapper.ba_refine_sensor_from_rig 0 \ --Mapper.ba_refine_focal_length 0 \ --Mapper.ba_refine_extra_params 0 \ --image_path terrains/images \ --output_path terrains/sparse Reconstruction from 360° spherical images ----------------------------------------- COLMAP can handle collections of 360° panoramas by rendering virtual pinhole images (similar to a cubemap) and treating them as a camera rig. Since the rig extrinsics and camera intrinsics are known, the reconstruction process is more robust. We provide an example Python script to reconstruct a 360° collection:: python python/examples/panorama_sfm.py \ --input_image_path image_directory \ --output_path output_directory Make sure to use the version of the script that corresponds to the version of COLMAP that you are using, as the script at HEAD is not guaranteed to be compatible. colmap-4.0.4/doc/sample-project/000077500000000000000000000000001517363634500165015ustar00rootroot00000000000000colmap-4.0.4/doc/sample-project/CMakeLists.txt000066400000000000000000000004001517363634500212330ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.10) project(SampleProject) find_package(colmap REQUIRED) # or to require a specific version: find_package(colmap 3.4 REQUIRED) add_executable(hello_world hello_world.cc) target_link_libraries(hello_world colmap::colmap) colmap-4.0.4/doc/sample-project/hello_world.cc000066400000000000000000000007061517363634500213250ustar00rootroot00000000000000#include #include #include #include int main(int argc, char** argv) { colmap::InitializeGlog(argv); std::string message; colmap::OptionManager options; options.AddRequiredOption("message", &message); if (!options.Parse(argc, argv)) { return EXIT_FAILURE; } std::cout << colmap::StringPrintf("Hello %s!\n", message.c_str()); return EXIT_SUCCESS; } colmap-4.0.4/doc/tutorial.rst000077500000000000000000000665701517363634500161720ustar00rootroot00000000000000.. _tutorial: Tutorial ======== This tutorial covers the topic of image-based 3D reconstruction by demonstrating the individual processing steps in COLMAP. If you are interested in a more general and mathematical introduction to the topic of image-based 3D reconstruction, please also refer to the `CVPR 2017 Tutorial on Large-scale 3D Modeling from Crowdsourced Data `_ and [schoenberger_thesis]_. Image-based 3D reconstruction from images traditionally first recovers a sparse representation of the scene and the camera poses of the input images using Structure-from-Motion. This output then serves as the input to Multi-View Stereo to recover a dense representation of the scene. .. _quick-start: Quickstart ---------- First, start the graphical user interface of COLMAP, as described :ref:`here `. COLMAP provides an automatic reconstruction tool that simply takes a folder of input images and produces a sparse and dense reconstruction in a workspace folder. Click ``Reconstruction > Automatic Reconstruction`` in the GUI and specify the relevant options. The output is written to the workspace folder. For example, if your images are located in ``path/to/project/images``, you could select ``path/to/project`` as a workspace folder and after running the automatic reconstruction tool, the folder would look similar to this:: +── images │   +── image1.jpg │   +── image2.jpg │   +── ... +── sparse │   +── 0 │   │ +── rigs.bin │   │ +── cameras.bin │   │ +── frames.bin │   │ +── images.bin │   │ +── points3D.bin │ +── ... +── dense │   +── 0 │   │ +── images │   │ +── sparse │   │ +── stereo │   │ +── fused.ply │   │ +── meshed-poisson.ply │   │ +── meshed-delaunay.ply │ +── ... +── database.db Here, the ``path/to/project/sparse`` contains the sparse models for all reconstructed components, while ``path/to/project/dense`` contains their corresponding dense models. The dense point cloud ``fused.ply`` can be imported in COLMAP using ``File > Import model from ...``, while the dense mesh must be visualized with an external viewer such as Meshlab. The following sections give general recommendations and describe the reconstruction process in more detail, if you need more control over the reconstruction process/parameters or if you are interested in the underlying technology in COLMAP. Structure-from-Motion --------------------- .. figure:: images/incremental-sfm.png :alt: Incremental Structure-from-Motion pipeline :figclass: align-center COLMAP's incremental Structure-from-Motion pipeline. Structure-from-Motion (SfM) is the process of reconstructing 3D structure from its projections into a series of images. The input is a set of overlapping images of the same object, taken from different viewpoints. The output is a 3-D reconstruction of the object, and the reconstructed intrinsic and extrinsic camera parameters of all images. Typically, Structure-from-Motion systems divide this process into three stages: 1) Feature detection and extraction 2) Feature matching and geometric verification 3) Structure and motion reconstruction COLMAP reflects these stages in different modules, that can be combined depending on the application. More information on Structure-from-Motion in general and the algorithms in COLMAP can be found in [schoenberger16sfm]_ and [schoenberger16mvs]_. If you have control over the picture capture process, please follow these guidelines for optimal reconstruction results: - Capture images with **good texture**. Avoid completely texture-less images (e.g., a white wall or empty desk). If the scene does not contain enough texture itself, you could place additional background objects, such as posters, etc. - Capture images at **similar illumination** conditions. Avoid high dynamic range scenes (e.g., pictures against the sun with shadows or pictures through doors/windows). Avoid specularities on shiny surfaces. - Capture images with **high visual overlap**. Make sure that each object is seen in at least 3 images -- the more images the better. - Capture images from **different viewpoints**. Do not take images from the same location by only rotating the camera, e.g., make a few steps after each shot. At the same time, try to have enough images from a relatively similar viewpoint. Note that more images is not necessarily better and might lead to a slow reconstruction process. If you use a video as input, consider down-sampling the frame rate. Multi-View Stereo ----------------- Multi-View Stereo (MVS) takes the output of SfM to compute depth and/or normal information for every pixel in an image. Fusion of the depth and normal maps of multiple images in 3D then produces a dense point cloud of the scene. Using the depth and normal information of the fused point cloud, algorithms such as the (screened) Poisson surface reconstruction [kazhdan2013]_ can then recover the 3D surface geometry of the scene. The resulting meshes can optionally be simplified using Quadric Error Metric (QEM) decimation [garland1997]_ to reduce their complexity while preserving shape and appearance. Additionally, the meshes can be textured using multi-view texture mapping [waechter2014]_, which assigns each face to the best-view camera image and produces a texture atlas with UV coordinates. More information on Multi-View Stereo in general and the algorithms in COLMAP can be found in [schoenberger16mvs]_. Preface ------- COLMAP requires only few steps to do a standard reconstruction for a general user. For more experienced users, the program exposes many different parameters, only some of which are intuitive to a beginner. The program should usually work without the need to modify any parameters. The defaults are chosen as a trade- off between reconstruction robustness/quality and speed. You can set "optimal" options for different reconstruction scenarios by choosing ``Extras > Set options for ... data``. If in doubt what settings to choose, stick to the defaults. The source code contains more documentation about all parameters. COLMAP is research software and in rare cases it may exit ungracefully if some constraints are not fulfilled. In this case, the program prints a traceback to stdout. To see this traceback or more debug information, it is recommended to run the executables (including the GUI) from the command-line, where you can define various levels of logging verbosity. Terminology ----------- The term **camera** is associated with the physical object of a camera using the same zoom-factor and lens. A camera defines the intrinsic projection model in COLMAP. A single camera can take multiple images with the same resolution, intrinsic parameters, and distortion characteristics. The term **image** is associated with a bitmap file, e.g., a JPEG or PNG file on disk. COLMAP detects **keypoints** in each image whose appearance is described by numerical **descriptors**. Pure appearance-based correspondences between keypoints/descriptors are defined by **matches**, while **inlier matches** are geometrically verified and used for the reconstruction procedure. Data Structure -------------- COLMAP assumes that all input images are in one input directory with potentially nested sub-directories. It recursively considers all images stored in this directory, and it supports various different image formats by OpenImageIO. Other files are automatically ignored. If high performance is a requirement, then you should separate any files that are not images. Images are identified uniquely by their relative file path. For later processing, such as image undistortion or dense reconstruction, the relative folder structure should be preserved. COLMAP does not modify the input images or directory and all extracted data is stored in a single, self-contained SQLite database file (see :doc:`database`). The first step is to start the graphical user interface of COLMAP by running the pre-built binaries (Windows: ``COLMAP.bat``, Mac: ``COLMAP.app``) or by executing ``./src/colmap/exe/colmap gui`` from the CMake build folder. Next, create a new project by choosing ``File > New project``. In this dialog, you must select where to store the database and the folder that contains the input images. For convenience, you can save the entire project settings to a configuration file by choosing ``File > Save project``. The project configuration stores the absolute path information of the database and image folder in addition to any other parameter settings. If you decide to move the database or image folder, you must change the paths accordingly by creating a new project. Alternatively, the resulting ``.ini`` configuration file can be directly modified in a text editor of your choice. To reopen an existing project, you can simply open the configuration file by choosing ``File > Open project`` and all parameter settings should be recovered. Note that all COLMAP executables can be started from the command-line by either specifying individual settings as command-line arguments or by providing the path to the project configuration file (see :ref:`Interface `). An example folder structure could look like this:: /path/to/project/... +── images │   +── image1.jpg │   +── image2.jpg │   +── ... │   +── imageN.jpg +── database.db +── project.ini In this example, you would select ``/path/to/project/images`` as the image folder path, ``/path/to/project/database.db`` as the database file path, and save the project configuration to ``/path/to/project/project.ini``. Feature Detection and Extraction -------------------------------- In the first step, feature detection/extraction finds sparse feature points in the image and describes their appearance using a numerical descriptor. COLMAP imports images and performs feature detection/extraction in one step in order to only load images from disk once. Next, choose ``Processing > Extract features``. In this dialog, you must first decide on the employed intrinsic camera model. You can either automatically extract focal length information from the embedded EXIF information or manually specify intrinsic parameters, e.g., as obtained in a lab calibration. If an image has partial EXIF information, COLMAP tries to find the missing camera specifications in a large database of camera models automatically. If all your images were captured by the same physical camera with identical zoom factor, it is recommended to share intrinsics between all images. Note that the program will exit ungracefully if the same camera model is shared among all images but not all images have the same size or EXIF focal length. If you have several groups of images that share the same intrinsic camera parameters, you can easily modify the camera models at a later point as well (see :ref:`Database Management `). If in doubt what to choose in this step, simply stick to the default parameters. You can either detect and extract new features from the images or import existing features from text files. By default, COLMAP extracts SIFT [lowe04]_ features either on the GPU or the CPU. The GPU version requires an attached display, while the CPU version is recommended for use on a server. In general, the GPU version is favorable as it has a customized feature detection mode that often produces higher quality features in the case of high contrast images. COLMAP also supports ALIKED feature extraction, a learned feature extractor using ONNX models, which can be selected via the ``--FeatureExtraction.type`` option (see :ref:`Feature Extraction and Matching ` for details). If you import existing features, every image must have a text file next to it (e.g., ``/path/to/image1.jpg`` and ``/path/to/image1.jpg.txt``) in the following format:: NUM_FEATURES 128 X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_128 ... X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_128 where ``X, Y, SCALE, ORIENTATION`` are floating point numbers and ``D_1...D_128`` values in the range ``0...255``. The file should have ``NUM_FEATURES`` lines with one line per feature. For example, if an image has 4 features, then the text file should look something like this:: 4 128 1.2 2.3 0.1 0.3 1 2 3 4 ... 21 2.2 3.3 1.1 0.3 3 2 3 2 ... 32 0.2 1.3 1.1 0.3 3 2 3 2 ... 2 1.2 2.3 1.1 0.3 3 2 3 2 ... 3 Note that by convention the upper left corner of an image has coordinate ``(0, 0)`` and the center of the upper left most pixel has coordinate ``(0.5, 0.5)``. If you must import features for large image collections, it is much more efficient to directly access the database with your favorite scripting language (see :ref:`Database Format `). If you are done setting all options, choose ``Extract`` and wait for the extraction to finish or cancel. If you cancel during the extraction process, the next time you start extracting images for the same project, COLMAP automatically continues where it left off. This also allows you to add images to an existing project/reconstruction. In this case, be sure to verify the camera parameters when using shared intrinsics. All extracted data will be stored in the database file and can be reviewed/managed in the database management tool (see :ref:`Database Management `) or, for experts, directly modified using SQLite (see :ref:`Database Format `). Feature Matching and Geometric Verification ------------------------------------------- In the second step, feature matching and geometric verification finds correspondences between the feature points in different images. Please, choose ``Processing > Feature matching`` and select one of the provided matching modes, that are intended for different input scenarios: - **Exhaustive Matching**: If the number of images in your dataset is relatively low (up to several hundreds), this matching mode should be fast enough and leads to the best reconstruction results. Here, every image is matched against every other image, while the block size determines how many images are loaded from disk into memory at the same time. - **Sequential Matching**: This mode is useful if the images are acquired in sequential order, e.g., by a video camera. In this case, consecutive frames have visual overlap and there is no need to match all image pairs exhaustively. Instead, consecutively captured images are matched against each other. This matching mode has built-in loop detection based on a vocabulary tree, where every N-th image (``loop_detection_period``) is matched against its visually most similar images (``loop_detection_num_images``). Note that image file names must be ordered sequentially (e.g., ``image0001.jpg``, ``image0002.jpg``, etc.). The order in the database is not relevant, since the images are explicitly ordered according to their file names. Note that loop detection requires a pre-trained vocabulary tree. A default tree will be automatically downloaded and cached. More trees are available and can be downloaded from https://demuc.de/colmap/. In case rigs and frames are configured appropriately in the database, sequential matching will automatically match all images in consecutive frames against each other. - **Vocabulary Tree Matching**: In this matching mode [schoenberger16vote]_, every image is matched against its visual nearest neighbors using a vocabulary tree with spatial re-ranking. This is the recommended matching mode for large image collections (several thousands). This requires a pre-trained vocabulary tree, that can be downloaded from https://demuc.de/colmap/. - **Spatial Matching**: This matching mode matches every image against its spatial nearest neighbors. Spatial locations can be manually set in the database management. By default, COLMAP also extracts GPS information from EXIF and uses it for spatial nearest neighbor search. If accurate prior location information is available, this is the recommended matching mode. - **Transitive Matching**: This matching mode uses the transitive relations of already existing feature matches to produce a more complete matching graph. If an image A matches to an image B and B matches to C, then this matcher attempts to match A to C directly. - **Custom Matching**: This mode allows to specify individual image pairs for matching or to import individual feature matches. To specify image pairs, you have to provide a text file with one image pair per line:: image1.jpg image2.jpg image1.jpg image3.jpg ... where ``image1.jpg`` is the relative path in the image folder. You have two options to import individual feature matches. Either raw feature matches, which are not geometrically verified or already geometrically verified feature matches. In both cases, the expected format is:: image1.jpg image2.jpg 0 1 1 2 3 4 image1.jpg image3.jpg 0 1 1 2 3 4 4 5 ... where ``image1.jpg`` is the relative path in the image folder and the pairs of numbers are zero-based feature indices in the respective images. If you must import many matches for large image collections, it is more efficient to directly access the database with a scripting language of your choice. If you are done setting all options, choose ``Match`` and wait for the matching to finish or cancel in between. Note that this step can take a significant amount of time depending on the number of images, the number of features per image, and the chosen matching mode. Expected times for exhaustive matching are from a few minutes for tens of images to a few hours for hundreds of images to days or weeks for thousands of images. If you cancel the matching process or import new images after matching, COLMAP only matches image pairs that have not been matched previously. The overhead of skipping already matched image pairs is low. This also enables to match additional images imported after an initial matching and it enables to combine different matching modes for the same dataset. All extracted data will be stored in the database file and can be reviewed/managed in the database management tool (see :ref:`Database Management `) or, for experts, directly modified using SQLite (see :ref:`Database Format `). Note that SIFT feature matching can use a GPU for acceleration, and the display performance of your computer might degrade significantly during the matching process. If your system has multiple CUDA-enabled GPUs, you can select specific GPUs with the ``gpu_index`` option. Feature matching can also be performed on the CPU by setting ``--FeatureMatching.use_gpu 0``, although this will be significantly slower for large datasets. Sparse Reconstruction --------------------- After producing the scene graph in the previous two steps, you can start the incremental reconstruction process by choosing ``Reconstruction > Start``. COLMAP first loads all extracted data from the database into memory and seeds the reconstruction from an initial image pair. Then, the scene is incrementally extended by registering new images and triangulating new points. The results are visualized in "real-time" during this reconstruction process. Refer to the :ref:`Graphical User Interface ` section for more details about the available controls. COLMAP attempts to reconstruct multiple models if not all images are registered into the same model. The different models can be selected from the drop-down menu in the toolbar. If the different models have common registered images, you can use the ``model_merger`` executable to merge them into a single reconstruction (see :ref:`FAQ ` for details). Ideally, the reconstruction works fine and all images are registered. If this is not the case, it is recommended to: - Perform additional matching. For best results, use exhaustive matching, enable guided matching, increase the number of nearest neighbors in vocabulary tree matching, or increase the overlap in sequential matching, etc. - Manually choose an initial image pair, if COLMAP fails to initialize. Choose ``Reconstruction > Reconstruction options > Init`` and set images from the database management tool that have enough matches from different viewpoints. Importing and Exporting ----------------------- COLMAP provides several export options for further processing. For full flexibility, it is recommended to export the reconstruction in COLMAP's data format by choosing ``File > Export`` to export the currently viewed model or ``File > Export all`` to export all reconstructed models. The model is exported in the selected folder using separate text files for the reconstructed cameras, images, and points. When exporting in COLMAP's data format, you can re- import the reconstruction for later visualization, image undistortion, or to continue an existing reconstruction from where it left off (e.g., after importing and matching new images). To import a model, choose ``File > Import`` and select the export folder path. Alternatively, you can also export the model in various other formats, such as Bundler, VisualSfM [#f1]_, PLY, or VRML by choosing ``File > Export as...``. COLMAP can visualize plain PLY point cloud files with RGB information by choosing ``File > Import From...``. Further information about the format of the exported models can be found :ref:`here `. .. _dense-reconstruction: Dense Reconstruction -------------------- After reconstructing a sparse representation of the scene and the camera poses of the input images, MVS can now recover denser scene geometry. COLMAP has an integrated dense reconstruction pipeline to produce depth and normal maps for all registered images, to fuse the depth and normal maps into a dense point cloud with normal information, and to finally estimate a dense surface from the fused point cloud using Poisson [kazhdan2013]_ or Delaunay reconstruction. Optionally, the resulting meshes can be simplified using the ``mesh_simplifier`` command to reduce the number of faces while preserving the overall shape. The meshes can also be textured using the ``mesh_texturer`` command, which produces a texture atlas and per-face UV coordinates from the undistorted images. To get started, import your sparse 3D model into COLMAP (or select the reconstructed model after finishing the previous sparse reconstruction steps). Then, choose ``Reconstruction > Multi-view stereo`` and select an empty or existing workspace folder, which is used for the output and of all dense reconstruction results. The first step is to ``undistort`` the images, second to compute the depth and normal maps using ``stereo``, third to ``fuse`` the depth and normals maps to a point cloud, followed by a final, optional point cloud ``meshing`` step. During the stereo reconstruction process, the display might freeze due to heavy compute load and, if your GPU does not have enough memory, the reconstruction process might ungracefully crash. Please, refer to the FAQ (:ref:`freeze ` and :ref:`memory `) for information on how to avoid these problems. Note that the reconstructed normals of the point cloud cannot be directly visualized in COLMAP, but e.g. in Meshlab by enabling ``Render > Show Normal/Curvature``. Similarly, the reconstructed dense surface mesh model must be visualized with external software. In addition to the internal dense reconstruction functionality, COLMAP exports to several other dense reconstruction libraries, such as CMVS/PMVS [furukawa10]_ or CMP-MVS [jancosek11]_. Please choose ``Extras > Undistort images`` and select the appropriate format. The output folders contain the reconstruction and the undistorted images. In addition, the folders contain sample shell scripts to perform the dense reconstruction. To run PMVS2, execute the following commands: ./path/to/pmvs2 /path/to/undistortion/folder/pmvs/ option-all where ``/path/to/undistortion/folder`` is the folder selected in the undistortion dialog. Make sure not to forget the trailing slash in ``/path/to/undistortion/folder/pmvs/`` in the above command-line arguments. For large datasets, you probably want to first run CMVS to cluster the scene into more manageable parts and then run COLMAP or PMVS2. Please, refer to the sample shell scripts in the undistortion output folder on how to run CMVS in combination with COLMAP or PMVS2. Moreover, there are a number of external libraries that support COLMAP's output: - `CMVS/PMVS `_ [furukawa10]_ - `CMP-MVS `_ [jancosek11]_ - `Line3D++ `_ [hofer16]_. .. _database-management: Database Management ------------------- You can review and manage the imported cameras, images, and feature matches in the database management tool. Choose ``Processing > Manage database``. In the opening dialog, you can see the list of imported images and cameras. You can view the features and matches for each image by clicking ``Show image`` and ``Overlapping images``. Individual entries in the database tables can be modified by double clicking specific cells. Note that any changes to the database are only effective after clicking ``Save``. To share intrinsic camera parameters between arbitrary groups of images, select a single or multiple images, choose ``Set camera`` and set the ``camera_id``, which corresponds to the unique ``camera_id`` column in the cameras table. You can also add new cameras with specific parameters. By setting the ``prior_focal_length`` flag to 0 or 1, you can give a hint whether the reconstruction algorithm should trust the focal length value. In case of a prior lab calibration, you want to set this value to 1. Without prior knowledge about the focal length, it is recommended to set this value to ``1.25 * max(width_in_px, height_in_px)``. The database management tool has only limited functionality and, for full control over the data, you must directly modify the SQLite database (see :ref:`Database Format `). By accessing the database directly, you can use COLMAP only for feature extraction and matching or you can import your own features and matches to only use COLMAP's incremental reconstruction algorithm. .. _interface: Graphical and Command-line Interface ------------------------------------ Most of COLMAP's features are accessible from both the graphical and the command-line interface, which are both embedded in the same executable. You can provide the options directly as command-line arguments or you can provide a ``.ini`` project configuration file containing the options using the ``--project_path path/to/project.ini`` argument. To start the GUI application, please execute ``colmap gui`` or directly specify a project configuration as ``colmap gui --project_path path/to/project.ini`` to avoid tedious selection in the GUI. To list the different commands available from the command-line, execute ``colmap help``. For example, to run feature extraction from the command-line, you must execute ``colmap feature_extractor``. The :ref:`graphical user interface ` and :ref:`command-line Interface ` sections provide more details about the available commands. .. rubric:: Footnotes .. [#f1] VisualSfM's [wu13]_ projection model applies the distortion to the measurements and COLMAP to the projection, hence the exported NVM file is not fully compatible with VisualSfM. colmap-4.0.4/docker/000077500000000000000000000000001517363634500142565ustar00rootroot00000000000000colmap-4.0.4/docker/Dockerfile000066400000000000000000000065651517363634500162640ustar00rootroot00000000000000# syntax=docker/dockerfile:1 ARG UBUNTU_VERSION=24.04 ARG NVIDIA_CUDA_VERSION=12.9.1 # # Docker builder stage. # FROM nvidia/cuda:${NVIDIA_CUDA_VERSION}-devel-ubuntu${UBUNTU_VERSION} AS builder ARG CUDA_ARCHITECTURES=all-major ENV QT_XCB_GL_INTEGRATION=xcb_egl ARG FETCHCONTENT_FULLY_DISCONNECTED=OFF ENV CCACHE_DIR=/colmap/build/.ccache ENV CCACHE_BASEDIR=/colmap # Prevent stop building ubuntu at time zone selection. ENV DEBIAN_FRONTEND=noninteractive # Prepare and empty machine for building. RUN apt-get update && \ apt-get install -y \ ccache \ cmake \ ninja-build \ build-essential \ libboost-program-options-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libopenimageio-dev \ openimageio-tools \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libgmock-dev \ libsqlite3-dev \ libglew-dev \ qt6-base-dev \ libqt6opengl6-dev \ libqt6openglwidgets6 \ libcgal-dev \ libceres-dev \ libcurl4-openssl-dev \ libssl-dev \ libmkl-full-dev # Fix issue in Ubuntu's openimageio CMake config. # We don't depend on any of openimageio's OpenCV functionality, # but it still requires the OpenCV include directory to exist. RUN mkdir -p /usr/include/opencv4 # Copy source into the image. COPY . /colmap # Build and install COLMAP. RUN cd /colmap && \ mkdir -p build/.ccache && \ cd build && \ cmake .. \ -GNinja \ -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHITECTURES} \ -DCMAKE_INSTALL_PREFIX=/colmap-install \ -DFETCHCONTENT_FULLY_DISCONNECTED=${FETCHCONTENT_FULLY_DISCONNECTED} \ -DBLA_VENDOR=Intel10_64lp && \ ninja install # # Stage to export build caches for CI round-tripping via actions/cache. # FROM scratch AS cache-export COPY --from=builder /colmap/build/.ccache/ /.ccache/ COPY --from=builder /colmap/build/_deps/ /_deps/ # # Docker runtime stage. # FROM nvidia/cuda:${NVIDIA_CUDA_VERSION}-runtime-ubuntu${UBUNTU_VERSION} AS runtime # Minimal dependencies to run COLMAP binary compiled in the builder stage. # Note: this reduces the size of the final image considerably, since all the # build dependencies are not needed. RUN apt-get update && \ apt-get install -y --no-install-recommends --no-install-suggests \ libboost-program-options1.83.0 \ libc6 \ libomp5 \ libopengl0 \ libmetis5 \ libceres4t64 \ libopenimageio2.4t64 \ libgcc-s1 \ libgl1 \ libglew2.2 \ libgoogle-glog0v6t64 \ libqt6core6 \ libqt6gui6 \ libqt6widgets6 \ libqt6openglwidgets6 \ libcurl4 \ libssl3t64 \ libmkl-locale \ libmkl-intel-lp64 \ libmkl-intel-thread \ libmkl-core && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Copy all files from /colmap-install/ in the builder stage to /usr/local/ in # the runtime stage. This simulates installing COLMAP in the default location # (/usr/local/), which simplifies environment variables. It also allows the user # of this Docker image to use it as a base image for compiling against COLMAP as # a library. For instance, CMake will be able to find COLMAP easily with the # command: find_package(COLMAP REQUIRED). COPY --from=builder /colmap-install/ /usr/local/ colmap-4.0.4/docker/README.md000066400000000000000000000026571517363634500155470ustar00rootroot00000000000000# How to build COLMAP using Docker ## Requirements - Host machine with at least one NVIDIA GPU/CUDA support and installed drivers (to support dense reconstruction). - Docker (for CUDA support 19.03+). ## Quick Start 1. Check that Docker >=19.03 installed on your host machine: ```bash docker --version ``` 2. Setup the NVIDIA driver and nvidia-toolkit on your host machine: For Ubuntu host machines: `./setup-ubuntu.sh` For CentOS host machines: `./setup-centos.sh` 3. Run the *run* script, using the *full local path* to your preferred local working directory (a folder with your input files/images, etc.): ```bash ./run.sh /path/where/your/working/folder/is ``` This will put you in a directory (inside the Docker container) mounted to the local path you specified. Now you can run COLMAP binaries on your own inputs like this: ```bash colmap automatic_reconstructor --image_path ./images --workspace_path . ``` Alternatively, you can run the *run-gui* script, which will start the graphical user interface of COLMAP: ```bash ./run-gui.sh /path/where/your/working/folder/is ``` ## Build from Scratch After completing steps 1-2, you can build the Docker image from scratch using the **Dockerfile**. First, update the CUDA and Ubuntu versions in Dockerfile lines 1-2 to match your system, then: ```bash ./build.sh ./run.sh /path/where/your/working/folder/is ``` colmap-4.0.4/docker/build.sh000077500000000000000000000006331517363634500157160ustar00rootroot00000000000000#!/bin/bash # Build COLMAP Docker image from the repository root. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" docker build "$REPO_ROOT" \ -f "$REPO_ROOT/docker/Dockerfile" \ -t colmap:latest # In some cases, you may have to explicitly specify the compute architecture: # docker build . -f docker/Dockerfile -t colmap:latest --build-arg CUDA_ARCHITECTURES=75 colmap-4.0.4/docker/run-gui.sh000077500000000000000000000055011517363634500162040ustar00rootroot00000000000000#!/bin/bash # # A robust script to run the COLMAP GUI inside a Docker container, # with automatic GPU detection and flexible argument passing. # --- Initial Checks --- # Exit immediately if a command exits with a non-zero status. set -e # --- Cleanup Function and Trap --- # This function will be called automatically when the script exits # to ensure that X server permissions are always restored. function cleanup { echo "Revoking X-server access..." # The '|| true' prevents the script from failing if xhost has issues. xhost -local:root > /dev/null || true } trap cleanup EXIT # Check if any argument is provided. if [ $# -eq 0 ]; then echo "Usage: $0 " echo "Example: $0 ../dataset/" exit 1 fi # --- Docker Image Selection --- # Check if local colmap:latest image exists (in case you ran build.sh), otherwise use official image if docker image inspect colmap:latest >/dev/null 2>&1; then echo "Using local COLMAP Docker image..." COLMAP_IMAGE="colmap:latest" else echo "Local COLMAP image not found, pulling official image..." docker pull colmap/colmap:latest COLMAP_IMAGE="colmap/colmap:latest" fi # Get absolute path HOST_DIR=$(realpath "$1") if [ ! -d "$HOST_DIR" ]; then echo "Error: Directory '$HOST_DIR' does not exist." exit 1 fi echo "Running COLMAP container with directory: $HOST_DIR" # --- Build Docker Arguments --- # Start with the base arguments for GUI forwarding. DOCKER_ARGS=( -it --rm --net=host -e DISPLAY -v "${HOST_DIR}:/working" -w /working ) # --- GPU Detection and Configuration --- echo "Testing for GPU acceleration..." # A successful `nvidia-smi` call is the most reliable test. if docker run --rm --runtime=nvidia "${COLMAP_IMAGE}" nvidia-smi >/dev/null 2>&1; then echo "✅ GPU detected. Using --runtime=nvidia and mapping all graphics devices." # Use the nvidia runtime AND also pass through the host's render devices. # This can solve driver conflicts on hybrid graphics systems. DOCKER_ARGS+=( --runtime=nvidia -e NVIDIA_DRIVER_CAPABILITIES=all ) if [ -d /dev/dri ]; then DOCKER_ARGS+=( --device=/dev/dri ) fi else echo "⚠️ GPU not detected. Falling back to CPU rendering." # For CPU mode, we give the container access to the host's render devices. if [ -d /dev/dri ]; then DOCKER_ARGS+=( --device=/dev/dri ) fi fi # --- X11 Forwarding Security --- # Grant permissions just before running the container. xhost +local:root > /dev/null # --- Execute the Container --- # Pass all arguments after the directory path ("${@:2}") to the colmap gui command. echo "Launching GUI..." # "${@:2}" accepts extra arguments for the COLMAP GUI. docker run "${DOCKER_ARGS[@]}" "${COLMAP_IMAGE}" colmap gui "${@:2}" # The `trap` will automatically call the `cleanup` function here, colmap-4.0.4/docker/run.sh000077500000000000000000000026521517363634500154260ustar00rootroot00000000000000#!/bin/bash # Check if any argument is provided. if [ $# -eq 0 ]; then echo "Usage: $0 " echo "Example: $0 ../dataset/" exit 1 fi # Check if local colmap:latest image exists (in case you ran build.sh), otherwise use official image if docker image inspect colmap:latest >/dev/null 2>&1; then echo "Using local COLMAP Docker image..." COLMAP_IMAGE="colmap:latest" else echo "Local COLMAP image not found, pulling official image..." docker pull colmap/colmap:latest COLMAP_IMAGE="colmap/colmap:latest" fi # Get absolute path HOST_DIR=$(realpath "$1") if [ ! -d "$HOST_DIR" ]; then echo "Error: Directory '$HOST_DIR' does not exist." exit 1 fi echo "Running COLMAP container with directory: $HOST_DIR" # --- Build Docker Arguments --- # Start with the base arguments. DOCKER_ARGS=( -it --rm -v "${HOST_DIR}:/working" -w /working ) # --- GPU Detection and Configuration --- echo "Testing for GPU acceleration..." # A successful `nvidia-smi` call is the most reliable test. if docker run --rm --runtime=nvidia "${COLMAP_IMAGE}" nvidia-smi >/dev/null 2>&1; then echo "✅ GPU detected. Using --runtime=nvidia." DOCKER_ARGS+=( --runtime=nvidia ) else echo "⚠️ GPU not detected. Using CPU mode." fi # --- Execute the Container --- # Always start an interactive bash shell. echo "Starting interactive bash shell..." docker run "${DOCKER_ARGS[@]}" "${COLMAP_IMAGE}" bashcolmap-4.0.4/docker/setup-centos.sh000077500000000000000000000006261517363634500172520ustar00rootroot00000000000000# Add the package repositories distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.repo | sudo tee /etc/yum.repos.d/nvidia-docker.repo # Install nvidia-container-toolkit sudo yum install -y nvidia-container-toolkit sudo systemctl restart docker # Check that it worked! docker run --gpus all nvidia/cuda:10.2-base nvidia-smi colmap-4.0.4/docker/setup-ubuntu.sh000077500000000000000000000102701517363634500172750ustar00rootroot00000000000000#!/bin/bash echo "🚀 Starting intelligent NVIDIA Docker setup..." echo "📦 Updating NVIDIA driver to latest..." sudo apt update sudo ubuntu-drivers autoinstall echo "🔄 Rebooting required after driver update. Run this script again after reboot." # Check if reboot is needed if [ -f /var/run/reboot-required ]; then echo "⚠️ System reboot required. Please reboot and run this script again." echo "After reboot, run: sudo reboot && ./setup-ubuntu.sh" exit 0 fi echo "🔍 Detecting NVIDIA driver version..." DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits | head -1) echo "Found NVIDIA driver: $DRIVER_VERSION" echo "🔍 Determine compatible CUDA version based on driver..." get_compatible_cuda_version() { local driver_ver=$1 # Extract major version (e.g., 560 from 560.35.03) local major_ver=$(echo $driver_ver | cut -d'.' -f1) # NVIDIA Driver-CUDA Compatibility Matrix if [ $major_ver -ge 565 ]; then echo "12.9" # Driver 565+ supports CUDA 12.9 elif [ $major_ver -ge 560 ]; then echo "12.6" # Driver 560+ supports CUDA 12.6 (your current case) elif [ $major_ver -ge 555 ]; then echo "12.5" # Driver 555+ supports CUDA 12.5 elif [ $major_ver -ge 550 ]; then echo "12.4" # Driver 550+ supports CUDA 12.4 elif [ $major_ver -ge 535 ]; then echo "12.2" # Driver 535+ supports CUDA 12.2 elif [ $major_ver -ge 525 ]; then echo "12.0" # Driver 525+ supports CUDA 12.0 else echo "11.8" # Fallback to CUDA 11.8 fi } COMPATIBLE_CUDA=$(get_compatible_cuda_version $DRIVER_VERSION) echo "✅ Compatible CUDA version: $COMPATIBLE_CUDA" echo "📦 Installing latest nvidia-container-toolkit..." curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit echo "📦 Configure Docker" sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker echo "🔍 Finding latest patch version for CUDA $COMPATIBLE_CUDA..." AVAILABLE_VERSIONS=$(curl -s "https://registry.hub.docker.com/v2/repositories/nvidia/cuda/tags/?page_size=100" | jq -r '.results[].name' | grep -E "^${COMPATIBLE_CUDA}\.[0-9]+-base-ubuntu24\.04$" | head -1) if [ -n "$AVAILABLE_VERSIONS" ]; then # Extract full version (e.g., "12.9.1" from "12.9.1-base-ubuntu24.04") FULL_CUDA_VERSION=$(echo "$AVAILABLE_VERSIONS" | cut -d'-' -f1) echo "✅ Found CUDA version: $FULL_CUDA_VERSION" else echo "❌ No CUDA $COMPATIBLE_CUDA images found for Ubuntu 24.04, trying Ubuntu 22.04..." AVAILABLE_VERSIONS=$(curl -s "https://registry.hub.docker.com/v2/repositories/nvidia/cuda/tags/?page_size=100" | jq -r '.results[].name' | grep -E "^${COMPATIBLE_CUDA}\.[0-9]+-base-ubuntu22\.04$" | head -1) if [ -n "$AVAILABLE_VERSIONS" ]; then FULL_CUDA_VERSION=$(echo "$AVAILABLE_VERSIONS" | cut -d'-' -f1) UBUNTU_VERSION="22.04" echo "✅ Found CUDA version: $FULL_CUDA_VERSION for Ubuntu 22.04" else echo "❌ No compatible CUDA images found" exit 1 fi fi echo "🧪 Testing with automatically detected compatible CUDA version: $COMPATIBLE_CUDA..." if docker run --rm --runtime=nvidia nvidia/cuda:${FULL_CUDA_VERSION}-base-ubuntu${UBUNTU_VERSION:-24.04} nvidia-smi; then echo "✅ GPU support working with CUDA $FULL_CUDA_VERSION!" # Update Dockerfile with compatible version if [ -f "Dockerfile" ]; then sed -i "s/ARG NVIDIA_CUDA_VERSION=.*/ARG NVIDIA_CUDA_VERSION=${FULL_CUDA_VERSION}/" Dockerfile if [ "${UBUNTU_VERSION:-24.04}" = "22.04" ]; then sed -i "s/ARG UBUNTU_VERSION=.*/ARG UBUNTU_VERSION=22.04/" Dockerfile fi echo "✅ Updated Dockerfile to use CUDA $FULL_CUDA_VERSION" fi else echo "❌ GPU test failed with CUDA $FULL_CUDA_VERSION" ficolmap-4.0.4/pyproject.toml000066400000000000000000000053141517363634500157260ustar00rootroot00000000000000[build-system] requires = [ "scikit-build-core>=0.3.3", "pybind11==3.0.1", "pybind11_stubgen @ git+https://github.com/sarlinpe/pybind11-stubgen@sarlinpe/fix-2025-08-20", "numpy", "ruff==0.15.0", "clang-format==21.1.8", ] build-backend = "scikit_build_core.build" [project] name = "pycolmap" # WARNING: This version must follow the MAJOR.MINOR.PATCH format. If only # MAJOR.MINOR is used, cibuildwheel will add a .dev0 patch version, which # results in releasing a pre-release version on PyPI. version = "4.0.4" description = "COLMAP bindings" readme = "python/README.md" authors = [ { name = "Johannes Schönberger", email = "jsch@demuc.de" }, { name = "Mihai Dusmanu", email = "mihai.dusmanu@gmail.com" }, { name = "Paul-Edouard Sarlin", email = "paul.edouard.sarlin@gmail.com" }, { name = "Shaohui Liu", email = "b1ueber2y@gmail.com" }, { name = "Philipp Lindenberger", email = "plindenbe@ethz.ch" }, ] license = {text = "BSD-3-Clause"} urls = {Repository = "https://github.com/colmap/colmap"} requires-python = ">=3.10" dependencies = ["numpy"] classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", ] [tool.scikit-build] wheel.expand-macos-universal-tags = true cmake.source-dir = "python/" wheel.packages = ["python/pycolmap"] [tool.cibuildwheel] build = "cp3{10,11,12,13,14}-{macosx,manylinux,win}*" archs = ["auto64"] test-requires = "pytest>=8.0 mypy==1.19.1 enlighten==1.14.1" test-command = """\ python -c "import pycolmap; print(pycolmap.__version__)" && \ cd {project} && \ mypy --package pycolmap && \ mypy --install-types --non-interactive python benchmark && \ pytest\ """ [tool.cibuildwheel.linux] before-all = "{project}/python/ci/install-colmap-almalinux.sh" [tool.cibuildwheel.macos] before-all = "{project}/python/ci/install-colmap-macos.sh" [tool.cibuildwheel.windows] before-all = "pwsh -File {project}/python/ci/install-colmap-windows.ps1" before-build = "pip install delvewheel" test-command = "pwsh -File {project}/python/ci/test-colmap-windows.ps1" # Skip mypy on source files for Python 3.10. There's a bug in numpy's type # annotations that was fixed in version 2.3.0, but numpy stopped shipping # wheels for Python 3.10 in 2.3.0. This causes mypy to fail. [[tool.cibuildwheel.overrides]] select = "cp310-{macosx,manylinux}*" test-command = """\ python -c "import pycolmap; print(pycolmap.__version__)" && \ cd {project} && \ mypy --package pycolmap && \ pytest\ """ [tool.pytest.ini_options] minversion = "8.0" addopts = ["-ra", "-q"] testpaths = [ "python", "benchmark", ] norecursedirs = [ "build", "python/build", ] [tool.mypy] implicit_optional = true ignore_missing_imports = true colmap-4.0.4/python/000077500000000000000000000000001517363634500143305ustar00rootroot00000000000000colmap-4.0.4/python/.gitignore000066400000000000000000000000341517363634500163150ustar00rootroot00000000000000*.egg-info/ build/ example/ colmap-4.0.4/python/CMakeLists.txt000066400000000000000000000110451517363634500170710ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.10) project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION}) option(GENERATE_STUBS "Whether to generate stubs" ON) option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CUDA_STANDARD 14) set(CMAKE_CUDA_STANDARD_REQUIRED ON) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # Some fixes for the Glog library. add_compile_definitions(GLOG_NO_ABBREVIATED_SEVERITIES) add_compile_definitions(GL_GLEXT_PROTOTYPES) add_compile_definitions(NOMINMAX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc") # Enable object level parallel builds in Visual Studio. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") endif() find_package(colmap REQUIRED) if(CCACHE_ENABLED) find_program(CCACHE ccache) if(CCACHE) message(STATUS "Enabling ccache support") set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) if(CUDA_ENABLED) set(CMAKE_CUDA_COMPILER_LAUNCHER ${CCACHE}) endif() else() message(STATUS "Disabling ccache support") endif() else() message(STATUS "Disabling ccache support") endif() # If colmap was built with ONNX support, we need to set the rpath so the # Python module can find the ONNX Runtime library at runtime. set(ONNX_RPATH "") if(DEFINED onnxruntime_LIBRARY_DIR_HINTS AND EXISTS "${onnxruntime_LIBRARY_DIR_HINTS}") message(STATUS "ONNX Runtime library directory: ${onnxruntime_LIBRARY_DIR_HINTS}") set(ONNX_RPATH "${onnxruntime_LIBRARY_DIR_HINTS}") elseif(TARGET onnxruntime::onnxruntime) # Derive the library directory from the imported target. get_target_property(_ort_type onnxruntime::onnxruntime TYPE) if(_ort_type STREQUAL "INTERFACE_LIBRARY") # Find module creates an INTERFACE target with link libraries. get_target_property(_ort_libs onnxruntime::onnxruntime INTERFACE_LINK_LIBRARIES) list(GET _ort_libs 0 _ort_location) else() get_target_property(_ort_location onnxruntime::onnxruntime IMPORTED_LOCATION) if(NOT _ort_location) get_target_property(_ort_location onnxruntime::onnxruntime IMPORTED_LOCATION_RELEASE) endif() endif() if(_ort_location) get_filename_component(ONNX_RPATH "${_ort_location}" DIRECTORY) message(STATUS "ONNX Runtime library directory (from target): ${ONNX_RPATH}") endif() endif() if (CMAKE_VERSION VERSION_LESS 3.18) set(DEV_MODULE Development) else() set(DEV_MODULE Development.Module) endif() find_package(Python REQUIRED COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) find_package(pybind11 2.13.0 REQUIRED) file(GLOB_RECURSE SOURCE_FILES "${PROJECT_SOURCE_DIR}/../src/pycolmap/*.cc") pybind11_add_module(_core ${SOURCE_FILES}) target_include_directories(_core PRIVATE ${PROJECT_SOURCE_DIR}/../src/) target_link_libraries(_core PRIVATE colmap::colmap glog::glog Ceres::ceres) target_compile_definitions(_core PRIVATE VERSION_INFO="${PROJECT_VERSION}") # Set rpath to find ONNX Runtime library at runtime. if(ONNX_RPATH) if(APPLE) set_target_properties(_core PROPERTIES BUILD_RPATH "${ONNX_RPATH}" INSTALL_RPATH "${ONNX_RPATH}" ) elseif(UNIX) set_target_properties(_core PROPERTIES BUILD_RPATH "${ONNX_RPATH}" INSTALL_RPATH "$ORIGIN/../lib:${ONNX_RPATH}" ) endif() endif() install(TARGETS _core LIBRARY DESTINATION pycolmap) if(GENERATE_STUBS AND UNIX) message(STATUS "Enabling stubs generation") set(STUBGEN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/_core") # Set library path for stub generation to find ONNX Runtime. if(APPLE AND ONNX_RPATH) set(STUBGEN_LIB_PATH_VAR "DYLD_LIBRARY_PATH=${ONNX_RPATH}:$ENV{DYLD_LIBRARY_PATH}") elseif(ONNX_RPATH) set(STUBGEN_LIB_PATH_VAR "LD_LIBRARY_PATH=${ONNX_RPATH}:$ENV{LD_LIBRARY_PATH}") else() set(STUBGEN_LIB_PATH_VAR "") endif() add_custom_command( TARGET _core POST_BUILD COMMAND "${CMAKE_COMMAND}" -E env "PYTHONPATH=$:$ENV{PYTHONPATH}" ${STUBGEN_LIB_PATH_VAR} bash ${PROJECT_SOURCE_DIR}/generate_stubs.sh "${Python_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating pybind11 stubs" VERBATIM ) install(DIRECTORY ${STUBGEN_OUTPUT_DIR} DESTINATION pycolmap) endif() colmap-4.0.4/python/README.md000066400000000000000000000255631517363634500156220ustar00rootroot00000000000000# Python Bindings for COLMAP PyCOLMAP exposes to Python most capabilities of the [COLMAP](https://colmap.github.io/) Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline. ## Installation Pre-built wheels for Linux, macOS, and Windows can be installed using pip: ```bash pip install pycolmap ``` The wheels are automatically built and pushed to [PyPI](https://pypi.org/project/pycolmap/) at each release. To benefit from GPU acceleration, wheels built for CUDA 12 (only for Linux - for now) are available under the [package `pycolmap-cuda12`](https://pypi.org/project/pycolmap-cuda12/).

[Building PyCOLMAP from source - click to expand] 1. Install COLMAP from source following [the official guide](https://colmap.github.io/install.html). 2. Build PyCOLMAP: - On Linux and macOS: ```bash python -m pip install . ``` - On Windows, after installing COLMAP [via VCPKG](https://colmap.github.io/install.html#id3), run in powershell: ```powershell python -m pip install . ` --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` --cmake.define.VCPKG_TARGET_TRIPLET="x64-windows" ```
## Reconstruction Pipeline PyCOLMAP provides bindings for multiple steps of the standard reconstruction pipeline: - Extracting and matching SIFT features - Importing an image folder into a COLMAP database - Inferring the camera parameters from the EXIF metadata of an image file - Running two-view geometric verification of matches on a COLMAP database - Triangulating points into an existing COLMAP model - Running incremental reconstruction from a COLMAP database - Dense reconstruction with multi-view stereo ### Sparse & Dense Reconstruction Sparse & Dense reconstruction from a folder of images can be performed with: ```python output_path: pathlib.Path image_dir: pathlib.Path output_path.mkdir() mvs_path = output_path / "mvs" database_path = output_path / "database.db" pycolmap.extract_features(database_path, image_dir) pycolmap.match_exhaustive(database_path) maps = pycolmap.incremental_mapping(database_path, image_dir, output_path) maps[0].write(output_path) # Dense reconstruction pycolmap.undistort_images(mvs_path, output_path, image_dir) pycolmap.patch_match_stereo(mvs_path) # requires compilation with CUDA pycolmap.stereo_fusion(mvs_path / "dense.ply", mvs_path) ``` PyCOLMAP can leverage the GPU for feature extraction, matching, and multi-view stereo if COLMAP was compiled with CUDA support. Similarly, PyCOLMAP can run Delaunay Triangulation if COLMAP was compiled with CGAL support. This requires to build the package from source and is not available with the PyPI wheels. ### Configuration Options All of the above steps are easily configurable with python dicts which are recursively merged into their respective defaults, for example: ```python pycolmap.extract_features( database_path, image_dir, extraction_options={"sift": {"max_num_features": 512}} ) # Equivalent to: ops = pycolmap.FeatureExtractionOptions() ops.sift.max_num_features = 512 pycolmap.extract_features(database_path, image_dir, extraction_options=ops) ``` To list available options and their default parameters: ```python help(pycolmap.SiftExtractionOptions) ``` For another example of usage, see [`example.py`](./examples/example.py) or [`hloc/reconstruction.py`](https://github.com/cvg/Hierarchical-Localization/blob/master/hloc/reconstruction.py). ## Reconstruction Object We can load and manipulate an existing COLMAP 3D reconstruction: ```python import pycolmap reconstruction = pycolmap.Reconstruction("path/to/reconstruction/dir") print(reconstruction.summary()) for image_id, image in reconstruction.images.items(): print(image_id, image) for point3D_id, point3D in reconstruction.points3D.items(): print(point3D_id, point3D) for camera_id, camera in reconstruction.cameras.items(): print(camera_id, camera) reconstruction.write("path/to/reconstruction/dir/") ``` ### Common Operations The object API mirrors the COLMAP C++ library. The bindings support many operations, for example: **Projecting a 3D point into an image** with arbitrary camera model: ```python uv = camera.img_from_cam(image.cam_from_world * point3D.xyz) ``` **Aligning two 3D reconstructions** by their camera poses: ```python rec2_from_rec1 = pycolmap.align_reconstructions_via_reprojections( reconstruction1, reconstruction2 ) reconstruction1.transform(rec2_from_rec1) print(rec2_from_rec1.scale, rec2_from_rec1.rotation, rec2_from_rec1.translation) ``` **Exporting reconstructions** to text, PLY, or other formats: ```python reconstruction.write_text("path/to/new/reconstruction/dir/") # text format reconstruction.export_PLY("rec.ply") # PLY format ``` ## Estimators We provide robust RANSAC-based estimators for: - Absolute camera pose (single-camera and multi-camera-rig) - Essential matrix - Fundamental matrix - Homography - Two-view relative pose for calibrated cameras All RANSAC and estimation parameters are exposed as objects that behave similarly as Python dataclasses. The RANSAC options are described in [`colmap/optim/ransac.h`](https://github.com/colmap/colmap/blob/main/src/colmap/optim/ransac.h#L43-L72) and their default values are: ```python ransac_options = pycolmap.RANSACOptions( max_error=4.0, # For example the reprojection error in pixels min_inlier_ratio=0.01, confidence=0.9999, min_num_trials=1000, max_num_trials=100000, ) ``` ### Absolute Pose Estimation To estimate the absolute pose of a query camera given 2D-3D correspondences: ```python # Parameters: # - points2D: Nx2 array; pixel coordinates # - points3D: Nx3 array; world coordinates # - camera: pycolmap.Camera # Optional parameters: # - estimation_options: dict or pycolmap.AbsolutePoseEstimationOptions # - refinement_options: dict or pycolmap.AbsolutePoseRefinementOptions answer = pycolmap.estimate_and_refine_absolute_pose(points2D, points3D, camera) # Returns: dictionary of estimation outputs or None if failure ``` 2D and 3D points are passed as Numpy arrays or lists. The options are defined in [`estimators/absolute_pose.cc`](./pycolmap/estimators/absolute_pose.h#L100-L122) and can be passed as regular (nested) Python dictionaries: ```python pycolmap.estimate_and_refine_absolute_pose( points2D, points3D, camera, estimation_options=dict(ransac=dict(max_error=12.0)), refinement_options=dict(refine_focal_length=True), ) ``` ### Absolute Pose Refinement ```python # Parameters: # - cam_from_world: pycolmap.Rigid3d, initial pose # - points2D: Nx2 array; pixel coordinates # - points3D: Nx3 array; world coordinates # - inlier_mask: array of N bool; inlier_mask[i] is true if correspondence i is an inlier # - camera: pycolmap.Camera # Optional parameters: # - refinement_options: dict or pycolmap.AbsolutePoseRefinementOptions answer = pycolmap.refine_absolute_pose( cam_from_world, points2D, points3D, inlier_mask, camera ) # Returns: dictionary of refinement outputs or None if failure ``` ### Essential Matrix Estimation ```python # Parameters: # - points1: Nx2 array; 2D pixel coordinates in image 1 # - points2: Nx2 array; 2D pixel coordinates in image 2 # - camera1: pycolmap.Camera of image 1 # - camera2: pycolmap.Camera of image 2 # Optional parameters: # - options: dict or pycolmap.RANSACOptions (default inlier threshold is 4px) answer = pycolmap.estimate_essential_matrix(points1, points2, camera1, camera2) # Returns: dictionary of estimation outputs or None if failure ``` ### Fundamental Matrix Estimation ```python answer = pycolmap.estimate_fundamental_matrix( points1, points2, [options], # optional dict or pycolmap.RANSACOptions ) ``` ### Homography Estimation ```python answer = pycolmap.estimate_homography_matrix( points1, points2, [options], # optional dict or pycolmap.RANSACOptions ) ``` ### Two-View Geometry Estimation COLMAP can also estimate a relative pose between two calibrated cameras by estimating both E and H and accounting for the degeneracies of each model. ```python # Parameters: # - camera1: pycolmap.Camera of image 1 # - points1: Nx2 array; 2D pixel coordinates in image 1 # - camera2: pycolmap.Camera of image 2 # - points2: Nx2 array; 2D pixel coordinates in image 2 # Optional parameters: # - matches: Nx2 integer array; correspondences across images # - options: dict or pycolmap.TwoViewGeometryOptions answer = pycolmap.estimate_calibrated_two_view_geometry( camera1, points1, camera2, points2 ) # Returns: pycolmap.TwoViewGeometry ``` The `TwoViewGeometryOptions` control how each model is selected. The output structure contains the geometric model, inlier matches, the relative pose (if `options.compute_relative_pose=True`), and the type of camera configuration, which is an instance of the enum `pycolmap.TwoViewGeometryConfiguration`. ### Camera Argument Some estimators expect a COLMAP camera object, which can be created as follows: ```python camera = pycolmap.Camera( model=camera_model_name_or_id, width=width, height=height, params=params, ) ``` The different camera models and their extra parameters are defined in [`colmap/src/colmap/sensor/models.h`](https://github.com/colmap/colmap/blob/main/src/colmap/sensor/models.h). For example for a pinhole camera: ```python camera = pycolmap.Camera( model='SIMPLE_PINHOLE', width=width, height=height, params=[focal_length, cx, cy], ) ``` Alternatively, we can also pass a camera dictionary: ```python camera_dict = { 'model': COLMAP_CAMERA_MODEL_NAME_OR_ID, 'width': IMAGE_WIDTH, 'height': IMAGE_HEIGHT, 'params': EXTRA_CAMERA_PARAMETERS_LIST } ``` ## SIFT Feature Extraction ```python import numpy as np import pycolmap from PIL import Image, ImageOps # Input should be grayscale image with range [0, 1]. img = Image.open('image.jpg').convert('RGB') img = ImageOps.grayscale(img) img = np.array(img).astype(np.float) / 255. # Optional parameters: # - options: dict or pycolmap.SiftExtractionOptions # - device: default pycolmap.Device.auto uses the GPU if available sift = pycolmap.Sift() # Parameters: # - image: HxW float array keypoints, descriptors = sift.extract(img) # Returns: # - keypoints: Nx4 array; format: x (j), y (i), scale, orientation # - descriptors: Nx128 array; L2-normalized descriptors ``` ## Bitmap PyCOLMAP provides bindings for the `Bitmap` class to work with images and convert them to/from NumPy arrays: ```python import numpy as np import pycolmap # Read a bitmap from file bitmap = pycolmap.Bitmap.read("image.jpg", as_rgb=True) print(f"Size: {bitmap.width}x{bitmap.height}, Channels: {bitmap.channels}") # Convert to NumPy array array = bitmap.to_array() # Shape: (H, W, 3) for RGB or (H, W) for grayscale # Create bitmap from NumPy array array = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) bitmap = pycolmap.Bitmap.from_array(array) # Write bitmap to file bitmap.write("output.jpg") # Rescale bitmap bitmap.rescale(new_width=320, new_height=240) ``` colmap-4.0.4/python/build.sh000077500000000000000000000015231517363634500157670ustar00rootroot00000000000000#!/bin/bash # Invoke from anywhere to perform an incremental build of pycolmap bindings. # Make sure to install the requirements from pyproject.toml. If colmap is not # installed globally but in a custom directory, you should set the colmap_DIR # environment variable, e.g.: # # colmap_DIR=/path/to/cmake/install/prefix pycolmap/incremental_build.sh set -e script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) pip install \ -ve \ "$script_dir/.." # Symlink the compiled _core extension into the source tree so that the # editable install (which adds python/ to sys.path) can find it. site_pkg=$(python -c "import sysconfig; print(sysconfig.get_path('purelib'))") core_so=$(ls -t "$site_pkg/pycolmap"/_core*.so 2>/dev/null | head -1) if [ -n "$core_so" ]; then ln -sf "$core_so" "$script_dir/pycolmap/" fi colmap-4.0.4/python/ci/000077500000000000000000000000001517363634500147235ustar00rootroot00000000000000colmap-4.0.4/python/ci/install-colmap-almalinux.sh000077500000000000000000000050361517363634500221750ustar00rootroot00000000000000#!/bin/bash set -e -x uname -a CURRDIR=$(pwd) export PATH="/usr/bin" # Install config manager and EPEL release yum install -y dnf-plugins-core epel-release # Enable the PowerTools repository (required for ninja-build) yum config-manager --set-enabled powertools # Install toolchain under AlmaLinux 8, # see https://almalinux.pkgs.org/8/almalinux-appstream-x86_64/ yum install -y \ gcc-toolset-12-gcc \ gcc-toolset-12-gcc-c++ \ gcc-toolset-12-gcc-gfortran \ kernel-headers \ perl-IPC-Cmd \ scl-utils \ git \ cmake3 \ ninja-build \ curl \ zip \ unzip \ tar \ perl \ libXmu-devel \ libXi-devel \ mesa-libGL-devel \ mesa-libGLU-devel source scl_source enable gcc-toolset-12 CUDA_HOME="/usr/local/cuda" if [ ! -d "${CUDA_HOME}" ] && [ -d "${CUDA_HOME}-12.9" ]; then ln -s "${CUDA_HOME}-12.9" "${CUDA_HOME}" fi if [ -d "${CUDA_HOME}" ]; then export PATH="${CUDA_HOME}/bin:${PATH}" if [ ! -f "/usr/local/bin/nvcc" ] && [ -f "${CUDA_HOME}/bin/nvcc" ]; then ln -s "${CUDA_HOME}/bin/nvcc" /usr/local/bin/nvcc fi echo "${CUDA_HOME}/lib64" > /etc/ld.so.conf.d/cuda.conf fi # ccache shipped by CentOS is too old so we download and cache it. COMPILER_TOOLS_DIR="${CONTAINER_COMPILER_CACHE_DIR}/bin" mkdir -p ${COMPILER_TOOLS_DIR} if [ ! -f "${COMPILER_TOOLS_DIR}/ccache" ]; then FILE="ccache-4.10.1-linux-x86_64" curl -sSLO https://github.com/ccache/ccache/releases/download/v4.10.1/${FILE}.tar.xz tar -xf ${FILE}.tar.xz cp ${FILE}/ccache ${COMPILER_TOOLS_DIR} fi export PATH="${COMPILER_TOOLS_DIR}:${PATH}" ln -sf ${COMPILER_TOOLS_DIR}/ccache /usr/local/bin/ccache # Setup vcpkg git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} cd ${VCPKG_INSTALLATION_ROOT} ./bootstrap-vcpkg.sh ./vcpkg integrate install # Build COLMAP cd ${CURRDIR} mkdir build && cd build cmake3 .. -GNinja \ -DCUDA_ENABLED="${BUILD_CUDA_ENABLED}" \ -DCMAKE_CUDA_ARCHITECTURES="all-major" \ -DONNX_ENABLED=OFF \ -DGUI_ENABLED=OFF \ -DCGAL_ENABLED=OFF \ -DLSD_ENABLED=OFF \ -DCCACHE_ENABLED=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja \ -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" \ -DVCPKG_TARGET_TRIPLET="${VCPKG_TARGET_TRIPLET}" \ -DCMAKE_EXE_LINKER_FLAGS_INIT="-ldl" \ -DFETCHCONTENT_BASE_DIR="${FETCHCONTENT_BASE_DIR}" \ -DFETCHCONTENT_FULLY_DISCONNECTED="${FETCHCONTENT_FULLY_DISCONNECTED}" ninja install ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/python/ci/install-colmap-macos.sh000077500000000000000000000026711517363634500213070ustar00rootroot00000000000000#!/bin/bash set -x -e CURRDIR=$(pwd) # Fix `brew link` error. find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete brew uninstall cmake # Workaround for CI failures. brew install git cmake ninja gfortran ccache libomp brew link --force libomp sudo xcode-select --reset # When building lapack-reference, vcpkg/cmake looks for gfortran. ln -sf $(which gfortran-14) "$(dirname $(which gfortran-14))/gfortran" # Setup vcpkg git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} cd ${VCPKG_INSTALLATION_ROOT} ./bootstrap-vcpkg.sh ./vcpkg integrate install # Build COLMAP cd ${CURRDIR} "$(brew --prefix cmake)/bin/cmake" \ -S . -B build/ \ -GNinja \ -DCUDA_ENABLED=OFF \ -DONNX_ENABLED=OFF \ -DGUI_ENABLED=OFF \ -DCGAL_ENABLED=OFF \ -DLSD_ENABLED=OFF \ -DCCACHE_ENABLED=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_MAKE_PROGRAM="$(brew --prefix ninja)/bin/ninja" \ -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" \ -DVCPKG_TARGET_TRIPLET="${VCPKG_TARGET_TRIPLET}" \ -DCMAKE_OSX_ARCHITECTURES="${CMAKE_OSX_ARCHITECTURES}" \ -DFETCHCONTENT_BASE_DIR="${FETCHCONTENT_BASE_DIR}" \ -DFETCHCONTENT_FULLY_DISCONNECTED="${FETCHCONTENT_FULLY_DISCONNECTED}" \ `if [[ ${CIBW_ARCHS_MACOS} == "arm64" ]]; then echo "-DSIMD_ENABLED=OFF"; fi` sudo cmake --build build/ --target install ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/python/ci/install-colmap-windows.ps1000077500000000000000000000025041517363634500217630ustar00rootroot00000000000000$ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $PSNativeCommandUseErrorActionPreference = $true $CURRDIR = $PWD $COMPILER_TOOLS_DIR = "${env:COMPILER_CACHE_DIR}/bin" New-Item -ItemType Directory -Force -Path ${COMPILER_TOOLS_DIR} $env:Path = "${COMPILER_TOOLS_DIR};" + $env:Path If (!(Test-Path -path "${COMPILER_TOOLS_DIR}/ccache.exe" -PathType Leaf)) { .github/workflows/install-ccache.ps1 -Destination "${COMPILER_TOOLS_DIR}" } # Setup vcpkg cd ${CURRDIR} git clone https://github.com/microsoft/vcpkg ${env:VCPKG_INSTALLATION_ROOT} cd ${env:VCPKG_INSTALLATION_ROOT} ./bootstrap-vcpkg.bat cd ${CURRDIR} & "./scripts/shell/enter_vs_dev_shell.ps1" & "${env:VCPKG_ROOT}/vcpkg.exe" integrate install # Build COLMAP mkdir build cd build cmake .. ` -GNinja ` -DCMAKE_MAKE_PROGRAM=ninja ` -DCUDA_ENABLED="OFF" ` -DONNX_ENABLED="OFF" ` -DGUI_ENABLED="OFF" ` -DCGAL_ENABLED="OFF" ` -DLSD_ENABLED="OFF" ` -DCMAKE_BUILD_TYPE="Release" ` -DCMAKE_TOOLCHAIN_FILE="${env:CMAKE_TOOLCHAIN_FILE}" ` -DVCPKG_TARGET_TRIPLET="${env:VCPKG_TARGET_TRIPLET}" ` -DFETCHCONTENT_BASE_DIR="${env:FETCHCONTENT_BASE_DIR}" ` -DFETCHCONTENT_FULLY_DISCONNECTED="${env:FETCHCONTENT_FULLY_DISCONNECTED}" ninja install ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose colmap-4.0.4/python/ci/test-colmap-windows.ps1000066400000000000000000000002531517363634500212700ustar00rootroot00000000000000& "$PSScriptRoot/../../scripts/shell/enter_vs_dev_shell.ps1" & "${env:VCPKG_ROOT}/vcpkg.exe" integrate install & python -c "import pycolmap; print(pycolmap.__version__)" colmap-4.0.4/python/ci/test_regression_eth3d.py000066400000000000000000000140721517363634500216070ustar00rootroot00000000000000""" Runs the COLMAP automatic reconstruction pipeline on the ETH3D dataset and asserts that the reconstructed model is close to the ground truth. This script is intended to be run as a CI test. It is not intended to be run manually. Instead use benchmark/reconstruction/evaluate.py. """ import argparse import logging import os import subprocess import sys import urllib.request def download_file(url: str, file_path: str, max_retries: int = 3) -> None: if os.path.exists(file_path): return logging.info(f"Downloading {url} to {file_path}") for retry in range(max_retries): try: urllib.request.urlretrieve(url, file_path) return except Exception as exc: logging.error( f"Failed to download {url} (trial={retry + 1}) " f"to {file_path} due to {exc}" ) def check_small_errors_or_exit( dataset_name: str, max_rotation_error: float, max_proj_center_error: float, expected_num_images: float, errors_csv_path: str, ) -> None: logging.info(f"Evaluating errors for {dataset_name}") error = False with open(errors_csv_path) as fid: num_images = 0 for line in fid: line = line.strip() if len(line) == 0 or line.startswith("#"): continue rotation_error, proj_center_error = map(float, line.split(",")) num_images += 1 if rotation_error > max_rotation_error: logging.info( "Exceeded rotation error threshold:", rotation_error ) error = True if proj_center_error > max_proj_center_error: logging.info( "Exceeded projection center error threshold:", proj_center_error, ) error = True if num_images != expected_num_images: logging.error("Unexpected number of images:", num_images) error = True if error: sys.exit(1) def process_dataset(args: argparse.Namespace, dataset_name: str) -> None: logging.info("Processing dataset:", dataset_name) workspace_path = os.path.join( os.path.realpath(args.workspace_path), dataset_name ) os.makedirs(workspace_path, exist_ok=True) dataset_archive_path = os.path.join(workspace_path, f"{dataset_name}.7z") download_file( f"https://www.eth3d.net/data/{dataset_name}_dslr_undistorted.7z", dataset_archive_path, ) subprocess.check_call( ["7zz", "x", "-y", f"{dataset_name}.7z"], cwd=workspace_path ) # Find undistorted parameters of first camera and # initialize all images with it. This is an approximation # because not all datasets have only a single camera. # However, it is a good enough initialization. with open( os.path.join( workspace_path, f"{dataset_name}/dslr_calibration_undistorted/cameras.txt", ), ) as fid: for line in fid: if not line.startswith("#"): first_camera_data = line.split() camera_model = first_camera_data[1] assert camera_model == "PINHOLE" camera_params = first_camera_data[4:] assert len(camera_params) == 4 break # Count the number of expected images in the GT. expected_num_images = 0 with open( os.path.join( workspace_path, f"{dataset_name}/dslr_calibration_undistorted/images.txt", ), ) as fid: for line in fid: if not line.startswith("#") and line.strip(): expected_num_images += 1 # Each image uses two consecutive lines. assert expected_num_images % 2 == 0 expected_num_images //= 2 # Run automatic reconstruction pipeline. subprocess.check_call( [ os.path.realpath(args.colmap_path), "automatic_reconstructor", "--image_path", f"{dataset_name}/images/", "--workspace_path", workspace_path, "--use_gpu", "1" if args.use_gpu else "0", "--num_threads", str(args.num_threads), "--quality", args.quality, "--camera_model", "PINHOLE", "--camera_params", ",".join(camera_params), ], cwd=workspace_path, ) # Compare reconstructed model to GT model. subprocess.check_call( [ os.path.realpath(args.colmap_path), "model_comparer", "--input_path1", "sparse/0", "--input_path2", f"{dataset_name}/dslr_calibration_undistorted/", "--output_path", ".", "--alignment_error", "proj_center", "--max_proj_center_error", str(args.max_proj_center_error), ], cwd=workspace_path, ) # Ensure discrepancy between reconstructed model and GT is small. check_small_errors_or_exit( dataset_name, args.max_rotation_error, args.max_proj_center_error, expected_num_images, os.path.join(workspace_path, "errors.csv"), ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--dataset_names", nargs="+", required=True) parser.add_argument("--workspace_path", required=True) parser.add_argument("--colmap_path", required=True) parser.add_argument("--use_gpu", default=True, action="store_true") parser.add_argument("--use_cpu", dest="use_gpu", action="store_false") parser.add_argument("--num_threads", type=int, default=-1) parser.add_argument("--quality", default="medium") parser.add_argument("--max_rotation_error", type=float, default=1.0) parser.add_argument("--max_proj_center_error", type=float, default=0.1) return parser.parse_args() def main() -> None: args = parse_args() for dataset_name in args.dataset_names: process_dataset(args, dataset_name) if __name__ == "__main__": main() colmap-4.0.4/python/ci/update_pyproject_toml.py000066400000000000000000000020651517363634500217140ustar00rootroot00000000000000import argparse from pathlib import Path import tomlkit # Set up command-line argument parser parser = argparse.ArgumentParser( description="Modify pyproject.toml for a custom build." ) parser.add_argument( "--name", required=True, help="The new package name for the wheel." ) parser.add_argument( "--add-deps", nargs="+", default=[], help="Space-separated list of Python dependencies to add.", ) args = parser.parse_args() # Modify the pyproject.toml file pyproject_path = Path("pyproject.toml") if not pyproject_path.exists(): raise FileNotFoundError(pyproject_path) with open(pyproject_path, encoding="utf-8") as f: config = tomlkit.load(f) config["project"]["name"] = args.name if args.add_deps: if "dependencies" not in config["project"]: config["project"]["dependencies"] = [] existing_deps = config["project"]["dependencies"] for req in args.add_deps: if req not in existing_deps: existing_deps.append(req) with open(pyproject_path, "w", encoding="utf-8") as f: tomlkit.dump(config, f) colmap-4.0.4/python/examples/000077500000000000000000000000001517363634500161465ustar00rootroot00000000000000colmap-4.0.4/python/examples/convert_legacy_rotation_averaging_format.py000066400000000000000000000211271517363634500270410ustar00rootroot00000000000000""" Convert legacy rotation averaging file formats to a COLMAP database. This script provides a migration path from deprecated file-based input to database-based input for the rotation averaging CLI. Legacy file formats: - Relative poses: IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ - Gravity priors: IMAGE_NAME GX GY GZ (optional) After conversion, use the database with: colmap rotation_averager --database_path database.db --output_path output/ Usage: python legacy_rotation_averaging_io.py \ --relpose_path relative_poses.txt \ --database_path database.db \ [--gravity_path gravity_priors.txt] """ import argparse from dataclasses import dataclass from pathlib import Path import numpy as np from numpy.typing import NDArray import pycolmap from pycolmap import logging @dataclass class RelativePose: """Relative pose between two images.""" image_name1: str image_name2: str cam2_from_cam1: pycolmap.Rigid3d @dataclass class GravityPrior: """Gravity prior for an image.""" image_name: str gravity: NDArray[np.float64] def read_relative_poses(file_path: Path | str) -> list[RelativePose]: """Read relative poses from a file. Format: IMAGE_NAME_1 IMAGE_NAME_2 QW QX QY QZ TX TY TZ Args: file_path: Path to the relative poses file. Returns: List of RelativePose objects. """ file_path = Path(file_path) relative_poses = [] with open(file_path) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) < 9: continue name1, name2 = parts[0], parts[1] qw, qx, qy, qz = map(float, parts[2:6]) tx, ty, tz = map(float, parts[6:9]) # pycolmap.Rotation3d takes quaternion in xyzw format rotation = pycolmap.Rotation3d(np.array([qx, qy, qz, qw])) translation = np.array([tx, ty, tz]) cam2_from_cam1 = pycolmap.Rigid3d(rotation, translation) relative_poses.append( RelativePose( image_name1=name1, image_name2=name2, cam2_from_cam1=cam2_from_cam1, ) ) return relative_poses def read_gravity_priors(file_path: Path | str) -> list[GravityPrior]: """Read gravity priors from a file. Format: IMAGE_NAME GX GY GZ Args: file_path: Path to the gravity priors file. Returns: List of GravityPrior objects. """ file_path = Path(file_path) gravity_priors = [] with open(file_path) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) < 4: continue name = parts[0] gx, gy, gz = map(float, parts[1:4]) gravity_priors.append( GravityPrior( image_name=name, gravity=np.array([gx, gy, gz], dtype=np.float64), ) ) return gravity_priors def get_image_names_from_relative_poses( relative_poses: list[RelativePose], ) -> dict[str, int]: """Extract unique image names from relative poses and assign IDs. Args: relative_poses: List of RelativePose objects. Returns: Dictionary mapping image names to IDs. """ image_names: dict[str, int] = {} next_id = 1 # COLMAP uses 1-based IDs for pose in relative_poses: for name in [pose.image_name1, pose.image_name2]: if name not in image_names: image_names[name] = next_id next_id += 1 return image_names def create_database_from_relative_poses( database_path: Path, relative_poses: list[RelativePose], gravity_priors: list[GravityPrior] | None = None, ) -> dict[str, int]: """Create a COLMAP database from relative poses and gravity priors. Args: database_path: Path to the output database. relative_poses: List of relative poses between image pairs. gravity_priors: Optional list of gravity priors for images. Returns: Dictionary mapping image names to their database image IDs. """ image_name_to_id = get_image_names_from_relative_poses(relative_poses) if database_path.exists(): database_path.unlink() # Use Reconstruction to create cameras, rigs, frames, and images # with the trivial rig/frame helper methods reconstruction = pycolmap.Reconstruction() for image_name, image_id in image_name_to_id.items(): camera_id = image_id # Create camera with trivial rig (rig_id = camera_id) camera = pycolmap.Camera.create( camera_id=camera_id, model=pycolmap.CameraModelId.SIMPLE_PINHOLE, focal_length=1.0, width=1, height=1, ) reconstruction.add_camera_with_trivial_rig(camera) # Create image with trivial frame (frame_id = image_id) image = pycolmap.Image() image.image_id = image_id image.name = image_name image.camera_id = camera_id reconstruction.add_image_with_trivial_frame(image) # Write to database with pycolmap.Database.open(database_path) as db: for camera in reconstruction.cameras.values(): db.write_camera(camera, use_camera_id=True) for rig in reconstruction.rigs.values(): db.write_rig(rig, use_rig_id=True) for frame in reconstruction.frames.values(): db.write_frame(frame, use_frame_id=True) for image in reconstruction.images.values(): db.write_image(image, use_image_id=True) # Write two-view geometries with relative poses for rel_pose in relative_poses: id1 = image_name_to_id[rel_pose.image_name1] id2 = image_name_to_id[rel_pose.image_name2] two_view_geom = pycolmap.TwoViewGeometry() two_view_geom.config = ( pycolmap.TwoViewGeometryConfiguration.CALIBRATED ) two_view_geom.cam2_from_cam1 = rel_pose.cam2_from_cam1 db.write_two_view_geometry(id1, id2, two_view_geom) # Write gravity priors if provided if gravity_priors: gravity_by_name = { gp.image_name: gp.gravity for gp in gravity_priors } for image in reconstruction.images.values(): if image.name in gravity_by_name: pose_prior = pycolmap.PosePrior() pose_prior.pose_prior_id = image.image_id pose_prior.corr_data_id = image.data_id pose_prior.gravity = gravity_by_name[image.name] db.write_pose_prior(pose_prior, use_pose_prior_id=True) return image_name_to_id def main(): parser = argparse.ArgumentParser( description="Convert rotation averaging files to database format" ) parser.add_argument( "--relpose_path", type=Path, required=True, help="Path to relative poses file", ) parser.add_argument( "--database_path", type=Path, required=True, help="Path for output database", ) parser.add_argument( "--gravity_path", type=Path, default=None, help="Optional path to gravity priors file", ) args = parser.parse_args() if not args.relpose_path.exists(): logging.error(f"Relative poses file not found: {args.relpose_path}") return 1 logging.info(f"Reading relative poses from {args.relpose_path}") relative_poses = read_relative_poses(args.relpose_path) logging.info(f"Loaded {len(relative_poses)} relative poses") gravity_priors = None if args.gravity_path is not None and args.gravity_path.exists(): logging.info(f"Reading gravity priors from {args.gravity_path}") gravity_priors = read_gravity_priors(args.gravity_path) logging.info(f"Loaded {len(gravity_priors)} gravity priors") logging.info(f"Creating database at {args.database_path}") image_name_to_id = create_database_from_relative_poses( args.database_path, relative_poses, gravity_priors ) logging.info(f"Database created with {len(image_name_to_id)} images") logging.info( f"Conversion complete. Use the database with:\n" f" colmap rotation_averager --database_path {args.database_path} " f"--output_path " ) return 0 if __name__ == "__main__": exit(main()) colmap-4.0.4/python/examples/custom_bundle_adjustment.py000066400000000000000000000303731517363634500236270ustar00rootroot00000000000000""" Python reimplementation of the bundle adjustment for the incremental mapper of C++ with equivalent logic. As a result, one can add customized residuals on top of the exposed ceres problem from conventional bundle adjustment. """ import collections import copy import pycolmap from pycolmap import logging def solve_bundle_adjustment( reconstruction: pycolmap.Reconstruction, ba_options: pycolmap.BundleAdjustmentOptions, ba_config: pycolmap.BundleAdjustmentConfig, ) -> pycolmap.BundleAdjustmentSummary: bundle_adjuster = pycolmap.create_default_bundle_adjuster( ba_options, ba_config, reconstruction ) summary = bundle_adjuster.solve() # Alternatively, you can customize the existing Ceres problem or options as: # import pyceres # The minimal bindings in pycolmap aren't sufficient. # bundle_adjuster = pycolmap.create_default_ceres_bundle_adjuster( # ba_options, ba_config, reconstruction # ) # solver_options = ba_options.ceres.create_solver_options( # ba_config, bundle_adjuster.problem # ) # summary = pyceres.SolverSummary() # pyceres.solve(solver_options, bundle_adjuster.problem, summary) return summary def adjust_global_bundle( mapper: pycolmap.IncrementalMapper, mapper_options: pycolmap.IncrementalMapperOptions, ba_options: pycolmap.BundleAdjustmentOptions, ) -> bool: """Equivalent to mapper.adjust_global_bundle(...)""" reconstruction = mapper.reconstruction assert reconstruction is not None reg_frame_ids = reconstruction.reg_frame_ids() if len(reg_frame_ids) < 2: logging.fatal("At least two images must be registered for global BA") custom_ba_options = copy.deepcopy(ba_options) # Use stricter convergence criteria for first registered images if len(reg_frame_ids) < 10: # kMinNumRegImagesForFastBA = 10 custom_ba_options.ceres.solver_options.function_tolerance /= 10 custom_ba_options.ceres.solver_options.gradient_tolerance /= 10 custom_ba_options.ceres.solver_options.parameter_tolerance /= 10 custom_ba_options.ceres.solver_options.max_num_iterations *= 2 custom_ba_options.ceres.solver_options.max_linear_solver_iterations = ( 200 ) # Avoid degeneracies in bundle adjustment mapper.observation_manager.filter_observations_with_negative_depth() # Configure bundle adjustment ba_config = pycolmap.BundleAdjustmentConfig() for frame_id in reg_frame_ids: frame = reconstruction.frame(frame_id) for data_id in frame.data_ids: if data_id.sensor_id.type != pycolmap.SensorType.CAMERA: continue ba_config.add_image(data_id.id) # Fix the existing images, if option specified if mapper_options.fix_existing_frames: for frame_id in reg_frame_ids: if frame_id in mapper.existing_frame_ids: ba_config.set_constant_rig_from_world_pose(frame_id) for rig_id in mapper_options.constant_rigs: for sensor_id in reconstruction.rig(rig_id).non_ref_sensors: ba_config.set_constant_sensor_from_rig_pose(sensor_id) for camera_id in mapper_options.constant_cameras: ba_config.set_constant_cam_intrinsics(camera_id) # TODO: Add python support for prior positions # Fixing the gauge with two cameras leads to a more stable optimization # with fewer steps as compared to fixing three points. ba_config.fix_gauge(pycolmap.BundleAdjustmentGauge.TWO_CAMS_FROM_WORLD) # Run bundle adjustment summary = solve_bundle_adjustment( reconstruction, custom_ba_options, ba_config ) logging.info("Global Bundle Adjustment") logging.info(summary.brief_report()) return summary.is_solution_usable() def iterative_global_refinement( mapper: pycolmap.IncrementalMapper, max_num_refinements: int, max_refinement_change: float, mapper_options: pycolmap.IncrementalMapperOptions, ba_options: pycolmap.BundleAdjustmentOptions, tri_options: pycolmap.IncrementalTriangulatorOptions, normalize_reconstruction: bool = True, ) -> bool: """Equivalent to mapper.iterative_global_refinement(...)""" reconstruction = mapper.reconstruction mapper.complete_and_merge_tracks(tri_options) num_retriangulated_observations = mapper.retriangulate(tri_options) logging.verbose( 1, f"=> Retriangulated observations: {num_retriangulated_observations}" ) for _ in range(max_num_refinements): num_observations = reconstruction.compute_num_observations() # mapper.adjust_global_bundle(mapper_options, ba_options) if not adjust_global_bundle(mapper, mapper_options, ba_options): return False if normalize_reconstruction: reconstruction.normalize() num_changed_observations = mapper.complete_and_merge_tracks(tri_options) num_changed_observations += mapper.filter_points(mapper_options) changed = ( num_changed_observations / num_observations if num_observations > 0 else 0 ) logging.verbose(1, f"=> Changed observations: {changed:.6f}") if changed < max_refinement_change: break return True def adjust_local_bundle( mapper: pycolmap.IncrementalMapper, mapper_options: pycolmap.IncrementalMapperOptions, ba_options: pycolmap.BundleAdjustmentOptions, tri_options: pycolmap.IncrementalTriangulatorOptions, image_id: int, point3D_ids: set[int], ) -> pycolmap.LocalBundleAdjustmentReport: """Equivalent to mapper.adjust_local_bundle(...)""" reconstruction = mapper.reconstruction assert reconstruction is not None report = pycolmap.LocalBundleAdjustmentReport() # Find images that have most 3D points with given image in common local_bundle = mapper.find_local_bundle(mapper_options, image_id) image_ids = set() # Do the bundle adjustment only if there is any connected images if local_bundle: ba_config = pycolmap.BundleAdjustmentConfig() ba_config.fix_gauge(pycolmap.BundleAdjustmentGauge.THREE_POINTS) # Insert the images of all local frames. image = reconstruction.image(image_id) frame_ids = {image.frame_id} assert image.frame is not None for data_id in image.frame.image_ids: ba_config.add_image(data_id.id) for local_image_id in local_bundle: local_image = reconstruction.image(local_image_id) frame_ids.add(local_image.frame_id) assert local_image.frame is not None for data_id in local_image.frame.image_ids: ba_config.add_image(data_id.id) # Fix the existing images, if options specified if mapper_options.fix_existing_frames: for frame_id in frame_ids: if frame_id in mapper.existing_frame_ids: ba_config.set_constant_rig_from_world_pose(frame_id) # Fix rig poses, if not all frames within the local bundle. num_frames_per_rig: dict[int, int] = collections.defaultdict(int) for frame_id in frame_ids: frame = reconstruction.frame(frame_id) num_frames_per_rig[frame.rig_id] += 1 for rig_id, num_frames_local in num_frames_per_rig.items(): if ( rig_id in mapper_options.constant_rigs or num_frames_local < mapper.num_reg_frames_per_rig[rig_id] ): for sensor_id in reconstruction.rig(rig_id).non_ref_sensors: ba_config.set_constant_sensor_from_rig_pose(sensor_id) # Fix camera intrinsics, if not all images within local bundle. num_images_per_camera: dict[int, int] = collections.defaultdict(int) for image_id in ba_config.images: image = reconstruction.images[image_id] num_images_per_camera[image.camera_id] += 1 for camera_id, num_images_local in num_images_per_camera.items(): if ( camera_id in mapper_options.constant_cameras or num_images_local < mapper.num_reg_images_per_camera[camera_id] ): ba_config.set_constant_cam_intrinsics(camera_id) # Make sure, we refine all new and short-track 3D points, no matter if # they are fully contained in the local image set or not. Do not include # long track 3D points as they are usually already very stable and # adding to them to bundle adjustment and track merging/completion would # slow down the local bundle adjustment significantly. variable_point3D_ids = set() for point3D_id in list(point3D_ids): point3D = reconstruction.point3D(point3D_id) kMaxTrackLength = 15 if ( point3D.error == -1.0 ) or point3D.track.length() <= kMaxTrackLength: ba_config.add_variable_point(point3D_id) variable_point3D_ids.add(point3D_id) # Adjust the local bundle summary = solve_bundle_adjustment( mapper.reconstruction, ba_options, ba_config ) logging.info("Local Bundle Adjustment") logging.info(summary.brief_report()) image_ids = ba_config.images report.num_adjusted_observations = int(summary.num_residuals / 2) # Merge refined tracks with other existing points report.num_merged_observations = mapper.triangulator.merge_tracks( tri_options, variable_point3D_ids ) # Complete tracks that may have failed to triangulate before refinement # of camera pose and calibration in bundle adjustment. This may avoid # that some points are filtered and helps for subsequent image # registrations. report.num_completed_observations = mapper.triangulator.complete_tracks( tri_options, variable_point3D_ids ) report.num_completed_observations += mapper.triangulator.complete_image( tri_options, image_id ) report.num_filtered_observations = ( mapper.observation_manager.filter_points3D_in_images( mapper_options.filter_max_reproj_error, mapper_options.filter_min_tri_angle, image_ids, ) ) report.num_filtered_observations += ( mapper.observation_manager.filter_points3D( mapper_options.filter_max_reproj_error, mapper_options.filter_min_tri_angle, point3D_ids, ) ) return report def iterative_local_refinement( mapper: pycolmap.IncrementalMapper, max_num_refinements: int, max_refinement_change: float, mapper_options: pycolmap.IncrementalMapperOptions, ba_options: pycolmap.BundleAdjustmentOptions, tri_options: pycolmap.IncrementalTriangulatorOptions, image_id: int, ) -> None: """Equivalent to mapper.iterative_local_refinement(...)""" custom_ba_options = copy.deepcopy(ba_options) for _ in range(max_num_refinements): # report = mapper.adjust_local_bundle( # mapper_options, # custom_ba_options, # tri_options, # image_id, # mapper.get_modified_points3D(), # ) report = adjust_local_bundle( mapper, mapper_options, custom_ba_options, tri_options, image_id, mapper.get_modified_points3D(), ) logging.verbose( 1, f"=> Merged observations: {report.num_merged_observations}" ) logging.verbose( 1, f"=> Completed observations: {report.num_completed_observations}" ) logging.verbose( 1, f"=> Filtered observations: {report.num_filtered_observations}" ) changed = 0.0 if report.num_adjusted_observations > 0: changed = ( report.num_merged_observations + report.num_completed_observations + report.num_filtered_observations ) / report.num_adjusted_observations logging.verbose(1, f"=> Changed observations: {changed:.6f}") if changed < max_refinement_change: break # Only use robust cost function for first iteration custom_ba_options.ceres.loss_function_type = ( pycolmap.LossFunctionType.TRIVIAL ) mapper.clear_modified_points3D() colmap-4.0.4/python/examples/custom_incremental_pipeline.py000066400000000000000000000472531517363634500243130ustar00rootroot00000000000000""" Python reimplementation of the C++ incremental mapper with equivalent logic. """ import argparse import time from pathlib import Path import custom_bundle_adjustment import enlighten import pycolmap from pycolmap import ( IncrementalMapper, IncrementalMapperOptions, IncrementalPipeline, IncrementalPipelineCallback, IncrementalPipelineOptions, IncrementalPipelineStatus, Reconstruction, ReconstructionManager, logging, ) def write_snapshot(reconstruction: Reconstruction, snapshot_path: Path) -> None: logging.info("Creating snapshot") timestamp = time.time() * 1000 path = snapshot_path / f"{timestamp:010d}" path.mkdir(exist_ok=True, parents=True) logging.verbose(1, f"=> Writing to {path}") reconstruction.write(path) def has_unknown_sensor_from_rig( reconstruction: Reconstruction, ) -> bool: parameterized_rig_ids = set() for image in reconstruction.images.values(): parameterized_rig_ids.add(image.frame.rig_id) for rig_id in parameterized_rig_ids: rig = reconstruction.rig(rig_id) for sensor_id, sensor_from_rig in rig.non_ref_sensors.items(): if ( sensor_id.type == pycolmap.SensorType.CAMERA and sensor_from_rig is None ): return True return False def iterative_global_refinement( options: IncrementalPipelineOptions, mapper_options: IncrementalMapperOptions, mapper: IncrementalMapper, ) -> None: logging.info("Retriangulation and Global bundle adjustment") # The following is equivalent to mapper.iterative_global_refinement(...) custom_bundle_adjustment.iterative_global_refinement( mapper, options.ba_global_max_refinements, options.ba_global_max_refinement_change, mapper_options, options.get_global_bundle_adjustment(), options.get_triangulation(), ) mapper.filter_frames(mapper_options) def initialize_reconstruction( controller: IncrementalPipeline, mapper: IncrementalMapper, mapper_options: IncrementalMapperOptions, reconstruction: Reconstruction, ) -> IncrementalPipelineStatus: """Equivalent to IncrementalPipeline.initialize_reconstruction(...)""" options = controller.options init_pair = (options.init_image_id1, options.init_image_id2) # Try to find good initial pair if not options.is_initial_pair_provided(): logging.info("Finding good initial image pair") ret = mapper.find_initial_image_pair(mapper_options, *init_pair) if ret is None: logging.info("No good initial image pair found.") return IncrementalPipelineStatus.NO_INITIAL_PAIR init_pair, init_cam2_from_cam1 = ret else: if not all(reconstruction.exists_image(i) for i in init_pair): logging.info(f"=> Initial image pair {init_pair} does not exist.") return IncrementalPipelineStatus.NO_INITIAL_PAIR maybe_init_cam2_from_cam1 = mapper.estimate_initial_two_view_geometry( mapper_options, *init_pair ) if maybe_init_cam2_from_cam1 is None: logging.info("Provided pair is unsuitable for initialization") return IncrementalPipelineStatus.BAD_INITIAL_PAIR init_cam2_from_cam1 = maybe_init_cam2_from_cam1 logging.info( f"Registering initial image pair #{init_pair[0]} and #{init_pair[1]}" ) mapper.register_initial_image_pair( mapper_options, *init_pair, init_cam2_from_cam1 ) tri_options = options.get_triangulation() tri_options.min_angle = mapper_options.init_min_tri_angle for image_id in init_pair: image = reconstruction.images[image_id] assert image.frame is not None for data_id in image.frame.image_ids: mapper.triangulate_image(tri_options, data_id.id) logging.info("Global bundle adjustment") # The following is equivalent to: mapper.adjust_global_bundle(...) custom_bundle_adjustment.adjust_global_bundle( mapper, mapper_options, options.get_global_bundle_adjustment() ) reconstruction.normalize() mapper.filter_points(mapper_options) mapper.filter_frames(mapper_options) # Initial image pair failed to register if ( reconstruction.num_reg_frames() == 0 or reconstruction.num_points3D() == 0 ): return IncrementalPipelineStatus.BAD_INITIAL_PAIR if options.extract_colors: reconstruction.extract_colors_for_all_images(options.image_path) return IncrementalPipelineStatus.SUCCESS def reconstruct_sub_model( controller: IncrementalPipeline, mapper: IncrementalMapper, mapper_options: IncrementalMapperOptions, reconstruction: Reconstruction, ) -> IncrementalPipelineStatus: """Equivalent to IncrementalPipeline.reconstruct_sub_model(...)""" mapper.begin_reconstruction(reconstruction) if has_unknown_sensor_from_rig(reconstruction): return IncrementalPipelineStatus.UNKNOWN_SENSOR_FROM_RIG if reconstruction.num_reg_frames() == 0: init_status = initialize_reconstruction( controller, mapper, mapper_options, reconstruction ) if init_status != IncrementalPipelineStatus.SUCCESS: return init_status controller.callback( IncrementalPipelineCallback.INITIAL_IMAGE_PAIR_REG_CALLBACK ) options = controller.options structure_less_flags = [] if options.structure_less_registration_only: structure_less_flags = [True] else: if options.structure_less_registration_fallback: structure_less_flags = [False, True] else: structure_less_flags = [False] snapshot_prev_num_reg_frames = reconstruction.num_reg_frames() ba_prev_num_reg_frames = reconstruction.num_reg_frames() ba_prev_num_points = reconstruction.num_points3D() reg_next_success, prev_reg_next_success = True, True while True: if not (reg_next_success or prev_reg_next_success): break if controller.check_reached_max_runtime(): break prev_reg_next_success = reg_next_success reg_next_success = False next_image_id = None for structure_less in structure_less_flags: next_images = mapper.find_next_images( mapper_options, structure_less=structure_less ) for reg_trial, next_image_id in enumerate(next_images): logging.info( f"Registering image #{next_image_id} " f"(num_reg_frames={reconstruction.num_reg_frames()})" ) if structure_less: logging.info( "Registering image with structure-less fallback" ) num_vis = ( mapper.observation_manager.num_visible_correspondences( next_image_id ) ) num_corrs = mapper.observation_manager.num_correspondences( next_image_id ) logging.info( f"=> Image sees {num_vis} / {num_corrs} correspondences" ) reg_next_success = ( mapper.register_next_structure_less_image( mapper_options, next_image_id ) ) else: num_vis = mapper.observation_manager.num_visible_points3D( next_image_id ) num_obs = mapper.observation_manager.num_observations( next_image_id ) logging.info(f"=> Image sees {num_vis} / {num_obs} points") reg_next_success = mapper.register_next_image( mapper_options, next_image_id ) if reg_next_success: break else: logging.info("=> Could not register, trying another image.") # If initial pair fails to continue for some time, # abort and try different initial pair. kMinNumInitialRegTrials = 30 if ( reg_trial >= kMinNumInitialRegTrials and reconstruction.num_reg_images() < options.min_model_size ): break if reg_next_success: break if reg_next_success and next_image_id is not None: image = reconstruction.images[next_image_id] assert image.frame is not None for data_id in image.frame.image_ids: mapper.triangulate_image( options.get_triangulation(), data_id.id ) # This is equivalent to mapper.iterative_local_refinement(...) custom_bundle_adjustment.iterative_local_refinement( mapper, options.ba_local_max_refinements, options.ba_local_max_refinement_change, mapper_options, options.get_local_bundle_adjustment(), options.get_triangulation(), next_image_id, ) if controller.check_run_global_refinement( reconstruction, ba_prev_num_reg_frames, ba_prev_num_points ): iterative_global_refinement(options, mapper_options, mapper) ba_prev_num_points = reconstruction.num_points3D() ba_prev_num_reg_frames = reconstruction.num_reg_frames() if options.extract_colors: for data_id in image.frame.image_ids: if not reconstruction.extract_colors_for_image( data_id.id, options.image_path ): logging.warning( f"Could not read image " f"{reconstruction.images[data_id.id].name} " f"at path {options.image_path}" ) if ( options.snapshot_frames_freq > 0 and reconstruction.num_reg_frames() >= options.snapshot_frames_freq + snapshot_prev_num_reg_frames ): snapshot_prev_num_reg_frames = reconstruction.num_reg_frames() write_snapshot(reconstruction, Path(options.snapshot_path)) controller.callback( IncrementalPipelineCallback.NEXT_IMAGE_REG_CALLBACK ) if mapper.num_shared_reg_images() >= int(options.max_model_overlap): break if (not reg_next_success) and prev_reg_next_success: iterative_global_refinement(options, mapper_options, mapper) if controller.check_reached_max_runtime(): return pycolmap.IncrementalPipelineStatus.INTERRUPTED # Only run final global BA, if last incremental BA was not global if ( reconstruction.num_reg_frames() > 0 and reconstruction.num_reg_frames() != ba_prev_num_reg_frames and reconstruction.num_points3D() != ba_prev_num_points ): iterative_global_refinement(options, mapper_options, mapper) return IncrementalPipelineStatus.SUCCESS def reconstruct( controller: IncrementalPipeline, mapper: IncrementalMapper, mapper_options: IncrementalMapperOptions, continue_reconstruction: bool, ) -> IncrementalPipelineStatus: """Equivalent to IncrementalPipeline.reconstruct(...)""" options = controller.options database_cache = controller.database_cache reconstruction_manager = controller.reconstruction_manager for num_trials in range(options.init_num_trials): if controller.check_reached_max_runtime(): break if not continue_reconstruction or num_trials > 0: reconstruction_idx = reconstruction_manager.add() else: reconstruction_idx = 0 reconstruction = reconstruction_manager.get(reconstruction_idx) status = reconstruct_sub_model( controller, mapper, mapper_options, reconstruction ) if status == IncrementalPipelineStatus.INTERRUPTED: reconstruction.update_point_3d_errors() logging.info("Keeping reconstruction due to interrupt") mapper.end_reconstruction(False) pycolmap.align_reconstruction_to_orig_rig_scales( database_cache.rigs, reconstruction ) elif status == IncrementalPipelineStatus.UNKNOWN_SENSOR_FROM_RIG: logging.error( "Discarding reconstruction due to unknown sensor_from_rig " "poses. Either explicitly define the poses by configuring the " "rigs or first run reconstruction without configured rigs and " "then derive the poses from the initial reconstruction for a " "subsequent reconstruction with rig constraints. See " "documentation for detailed instructions." ) mapper.end_reconstruction(True) reconstruction_manager.delete(reconstruction_idx) return IncrementalPipelineStatus.STOP elif status == IncrementalPipelineStatus.BAD_INITIAL_PAIR: logging.info("Disacarding reconstruction due to bad initial pair") mapper.end_reconstruction(True) reconstruction_manager.delete(reconstruction_idx) elif status == IncrementalPipelineStatus.NO_INITIAL_PAIR: logging.info("Disacarding reconstruction due to no initial pair") mapper.end_reconstruction(True) reconstruction_manager.delete(reconstruction_idx) return IncrementalPipelineStatus.CONTINUE elif status == IncrementalPipelineStatus.SUCCESS: num_reg_images = reconstruction.num_reg_images() total_num_reg_images = mapper.num_total_reg_images() if ( options.multiple_models and reconstruction_manager.size() > 1 and num_reg_images < options.min_model_size ) or num_reg_images == 0: logging.info( "Discarding reconstruction due to insufficient size" ) mapper.end_reconstruction(True) reconstruction_manager.delete(reconstruction_idx) else: reconstruction.update_point_3d_errors() logging.info("Keeping successful reconstruction") mapper.end_reconstruction(False) pycolmap.align_reconstruction_to_orig_rig_scales( database_cache.rigs, reconstruction ) controller.callback( IncrementalPipelineCallback.LAST_IMAGE_REG_CALLBACK ) if ( not options.multiple_models or reconstruction_manager.size() >= options.max_num_models or total_num_reg_images >= database_cache.num_images() - 1 ): return IncrementalPipelineStatus.STOP else: logging.fatal(f"Unknown reconstruction status: {status}") return IncrementalPipelineStatus.CONTINUE def main_incremental_mapper(controller: IncrementalPipeline) -> None: """Equivalent to IncrementalPipeline.run()""" timer = pycolmap.Timer() timer.start() database_cache = controller.database_cache if database_cache.num_images() == 0: logging.warning("No images with matches found in the database") return if ( controller.options.use_prior_position and database_cache.num_pose_priors() == 0 ): logging.warning("No pose priors") return reconstruction_manager = controller.reconstruction_manager continue_reconstruction = reconstruction_manager.size() > 0 if reconstruction_manager.size() > 1: logging.fatal( "Can only resume from a single reconstruction, " "but multiple are given" ) num_images = database_cache.num_images() mapper = IncrementalMapper(database_cache) mapper_options = controller.options.get_mapper() if ( reconstruct(controller, mapper, mapper_options, continue_reconstruction) == IncrementalPipelineStatus.STOP ): return def should_stop(): return ( mapper.num_total_reg_images() == num_images or controller.check_reached_max_runtime() ) for _ in range(2): # number of relaxations if should_stop(): break logging.info("=> Relaxing the initialization constraints") mapper_options.init_min_num_inliers = int( mapper_options.init_min_num_inliers / 2 ) mapper.reset_initialization_stats() if ( reconstruct( controller, mapper, mapper_options, continue_reconstruction=False, ) == IncrementalPipelineStatus.STOP ): return if should_stop(): break logging.info("=> Relaxing the initialization constraints") mapper_options.init_min_tri_angle /= 2 mapper.reset_initialization_stats() if ( reconstruct( controller, mapper, mapper_options, continue_reconstruction=False, ) == IncrementalPipelineStatus.STOP ): return timer.print_minutes() def main( database_path: Path, image_path: Path, output_path: Path, options: IncrementalPipelineOptions | None = None, input_path: Path | None = None, ) -> dict[int, Reconstruction]: if options is None: options = IncrementalPipelineOptions() options.image_path = image_path if not database_path.exists(): logging.fatal(f"Database path does not exist: {database_path}") if not image_path.exists(): logging.fatal(f"Image path does not exist: {image_path}") output_path.mkdir(exist_ok=True, parents=True) reconstruction_manager = ReconstructionManager() if input_path: reconstruction_manager.read(input_path) with pycolmap.Database.open(database_path) as database: mapper = IncrementalPipeline(options, database, reconstruction_manager) num_images = database.num_images() with enlighten.Manager() as manager: with manager.counter( total=num_images, desc="Images registered:" ) as pbar: pbar.update(0, force=True) mapper.add_callback( IncrementalPipelineCallback.INITIAL_IMAGE_PAIR_REG_CALLBACK, lambda: pbar.update(2), ) mapper.add_callback( IncrementalPipelineCallback.NEXT_IMAGE_REG_CALLBACK, lambda: pbar.update(1), ) main_incremental_mapper(mapper) # write and output reconstruction_manager.write(output_path) reconstructions = {} for i in range(reconstruction_manager.size()): reconstructions[i] = reconstruction_manager.get(i) return reconstructions def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--database_path", required=True) parser.add_argument("--image_path", required=True) parser.add_argument("--input_path", default=None) parser.add_argument("--output_path", required=True) return parser.parse_args() if __name__ == "__main__": args = parse_args() main( database_path=Path(args.database_path), image_path=Path(args.image_path), input_path=Path(args.input_path) if args.input_path else None, output_path=Path(args.output_path), ) colmap-4.0.4/python/examples/custom_incremental_pipeline_test.py000066400000000000000000000146651517363634500253530ustar00rootroot00000000000000# Equivalent tests to src/colmap/controllers/incremental_pipeline_test.cc from pathlib import Path import custom_incremental_pipeline import pycolmap def expect_equal_reconstructions( gt: pycolmap.Reconstruction, computed: pycolmap.Reconstruction, max_rotation_error_deg: float, max_proj_center_error: float, num_obs_tolerance: float, ) -> None: assert computed.num_cameras() == gt.num_cameras() assert computed.num_images() == gt.num_images() assert computed.num_reg_images() == gt.num_reg_images() assert ( computed.compute_num_observations() >= (1 - num_obs_tolerance) * gt.compute_num_observations() ) result = pycolmap.compare_reconstructions( computed, gt, alignment_error="proj_center", max_proj_center_error=max_proj_center_error, ) assert result is not None for error in result["errors"]: assert error.rotation_error_deg < max_rotation_error_deg assert error.proj_center_error < max_proj_center_error def create_test_options() -> pycolmap.IncrementalPipelineOptions: options = pycolmap.IncrementalPipelineOptions() # Use single thread for deterministic behavior. options.num_threads = 1 return options def test_without_noise(tmp_path: Path) -> None: pycolmap.set_random_seed(0) database_path = tmp_path / "database.db" image_path = tmp_path / "images" image_path.mkdir() output_path = tmp_path / "sparse" output_path.mkdir() with pycolmap.Database.open(database_path) as database: synthetic_dataset_options = pycolmap.SyntheticDatasetOptions() synthetic_dataset_options.num_cameras_per_rig = 2 synthetic_dataset_options.num_frames_per_rig = 7 synthetic_dataset_options.num_points3D = 50 gt_reconstruction = pycolmap.synthesize_dataset( synthetic_dataset_options, database ) custom_incremental_pipeline.main( database_path=database_path, image_path=image_path, output_path=output_path, options=create_test_options(), ) expect_equal_reconstructions( gt_reconstruction, pycolmap.Reconstruction(output_path / "0"), max_rotation_error_deg=1e-2, max_proj_center_error=1e-4, num_obs_tolerance=0, ) def test_with_noise(tmp_path: Path) -> None: pycolmap.set_random_seed(0) database_path = tmp_path / "database.db" image_path = tmp_path / "images" image_path.mkdir() output_path = tmp_path / "sparse" output_path.mkdir() with pycolmap.Database.open(database_path) as database: synthetic_dataset_options = pycolmap.SyntheticDatasetOptions() synthetic_dataset_options.num_cameras_per_rig = 2 synthetic_dataset_options.num_frames_per_rig = 7 synthetic_dataset_options.num_points3D = 100 gt_reconstruction = pycolmap.synthesize_dataset( synthetic_dataset_options, database ) synthetic_noise_options = pycolmap.SyntheticNoiseOptions() synthetic_noise_options.point2D_stddev = 0.5 pycolmap.synthesize_noise(synthetic_noise_options, gt_reconstruction) custom_incremental_pipeline.main( database_path=database_path, image_path=image_path, output_path=output_path, options=create_test_options(), ) expect_equal_reconstructions( gt_reconstruction, pycolmap.Reconstruction(output_path / "0"), max_rotation_error_deg=1e-1, max_proj_center_error=1e-1, num_obs_tolerance=0.02, ) def test_multi_reconstruction(tmp_path: Path) -> None: pycolmap.set_random_seed(0) database_path = tmp_path / "database.db" image_path = tmp_path / "images" image_path.mkdir() output_path = tmp_path / "sparse" output_path.mkdir() with pycolmap.Database.open(database_path) as database: synthetic_dataset_options = pycolmap.SyntheticDatasetOptions() synthetic_dataset_options.num_cameras_per_rig = 1 synthetic_dataset_options.num_frames_per_rig = 5 synthetic_dataset_options.num_points3D = 50 gt_reconstruction1 = pycolmap.synthesize_dataset( synthetic_dataset_options, database ) synthetic_dataset_options.num_frames_per_rig = 4 gt_reconstruction2 = pycolmap.synthesize_dataset( synthetic_dataset_options, database ) options = create_test_options() options.min_model_size = 4 custom_incremental_pipeline.main( database_path=database_path, image_path=image_path, output_path=output_path, options=options, ) assert len(list(output_path.iterdir())) == 2 reconstruction1 = pycolmap.Reconstruction(output_path / "0") reconstruction2 = pycolmap.Reconstruction(output_path / "1") if reconstruction1 == gt_reconstruction2.num_reg_images(): reconstruction1, reconstruction2 = reconstruction2, reconstruction1 expect_equal_reconstructions( gt_reconstruction1, reconstruction1, max_rotation_error_deg=1e-2, max_proj_center_error=1e-4, num_obs_tolerance=0, ) expect_equal_reconstructions( gt_reconstruction2, reconstruction2, max_rotation_error_deg=1e-2, max_proj_center_error=1e-4, num_obs_tolerance=0, ) def test_chained_matches(tmp_path: Path) -> None: pycolmap.set_random_seed(0) database_path = tmp_path / "database.db" image_path = tmp_path / "images" image_path.mkdir() output_path = tmp_path / "sparse" output_path.mkdir() with pycolmap.Database.open(database_path) as database: synthetic_dataset_options = pycolmap.SyntheticDatasetOptions() synthetic_dataset_options.num_cameras_per_rig = 1 synthetic_dataset_options.num_frames_per_rig = 4 synthetic_dataset_options.num_points3D = 100 synthetic_dataset_options.match_config = ( pycolmap.SyntheticDatasetMatchConfig.CHAINED ) gt_reconstruction = pycolmap.synthesize_dataset( synthetic_dataset_options, database ) custom_incremental_pipeline.main( database_path=database_path, image_path=image_path, output_path=output_path, options=create_test_options(), ) expect_equal_reconstructions( gt_reconstruction, pycolmap.Reconstruction(output_path / "0"), max_rotation_error_deg=1e-2, max_proj_center_error=1e-4, num_obs_tolerance=0, ) colmap-4.0.4/python/examples/example.py000066400000000000000000000045711517363634500201620ustar00rootroot00000000000000""" An example for running incremental SfM on images with the pycolmap interface. """ import shutil import urllib.request import zipfile from pathlib import Path import enlighten import pycolmap from pycolmap import logging def incremental_mapping_with_pbar( database_path: Path, image_path: Path, sfm_path: Path ) -> dict[int, pycolmap.Reconstruction]: with pycolmap.Database.open(database_path) as database: num_images = database.num_images() with enlighten.Manager() as manager: with manager.counter( total=num_images, desc="Images registered:" ) as pbar: pbar.update(0, force=True) reconstructions = pycolmap.incremental_mapping( database_path, image_path, sfm_path, initial_image_pair_callback=lambda: pbar.update(2), next_image_callback=lambda: pbar.update(1), ) return reconstructions def run() -> None: output_path = Path("example/") image_path = output_path / "Fountain/images" database_path = output_path / "database.db" sfm_path = output_path / "sfm" output_path.mkdir(exist_ok=True) # The log filename is postfixed with the execution timestamp. logging.set_log_destination(logging.INFO, output_path / "INFO.log.") data_url = "https://cvg-data.inf.ethz.ch/local-feature-evaluation-schoenberger2017/Strecha-Fountain.zip" if not image_path.exists(): logging.info("Downloading the data.") zip_path = output_path / "data.zip" urllib.request.urlretrieve(data_url, zip_path) with zipfile.ZipFile(zip_path, "r") as fid: fid.extractall(output_path) logging.info(f"Data extracted to {output_path}.") if database_path.exists(): database_path.unlink() pycolmap.set_random_seed(0) pycolmap.extract_features(database_path, image_path) pycolmap.match_exhaustive(database_path) if sfm_path.exists(): shutil.rmtree(sfm_path) sfm_path.mkdir(exist_ok=True) recs = incremental_mapping_with_pbar(database_path, image_path, sfm_path) # alternatively, use: # import custom_incremental_pipeline # recs = custom_incremental_pipeline.main( # database_path, image_path, sfm_path # ) for idx, rec in recs.items(): logging.info(f"#{idx} {rec.summary()}") if __name__ == "__main__": run() colmap-4.0.4/python/examples/panorama_sfm.py000066400000000000000000000324151517363634500211700ustar00rootroot00000000000000""" An example for running incremental SfM on 360 spherical panorama images. """ import argparse import os from collections.abc import Sequence from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from pathlib import Path from threading import Lock from typing import Literal, TypeVar, cast import cv2 import numpy as np import numpy.typing as npt import PIL.ExifTags import PIL.Image from scipy.spatial.transform import Rotation from tqdm import tqdm import pycolmap from pycolmap import logging N = TypeVar("N", bound=int) NDArrayNx2 = np.ndarray[tuple[N, Literal[2]], np.dtype[np.float64]] NDArray3x1 = np.ndarray[tuple[Literal[3], Literal[1]], np.dtype[np.float64]] @dataclass class PanoRenderOptions: num_steps_yaw: int pitches_deg: Sequence[float] hfov_deg: float vfov_deg: float PANO_RENDER_OPTIONS: dict[str, PanoRenderOptions] = { "overlapping": PanoRenderOptions( num_steps_yaw=4, pitches_deg=(-35.0, 0.0, 35.0), hfov_deg=90.0, vfov_deg=90.0, ), # Cubemap without top and bottom images. "non-overlapping": PanoRenderOptions( num_steps_yaw=4, pitches_deg=(0.0,), hfov_deg=90.0, vfov_deg=90.0, ), } def create_virtual_camera( pano_width: int, pano_height: int, hfov_deg: float, vfov_deg: float, ) -> pycolmap.Camera: """Create a virtual perspective camera.""" image_width = int(pano_width * hfov_deg / 360) image_height = int(pano_height * vfov_deg / 180) focal = image_width / (2 * np.tan(np.deg2rad(hfov_deg) / 2)) return pycolmap.Camera.create_from_model_id( camera_id=0, model=pycolmap.CameraModelId.SIMPLE_PINHOLE, focal_length=focal, width=image_width, height=image_height, ) def get_virtual_camera_rays( camera: pycolmap.Camera, ) -> npt.NDArray[np.floating]: size = (camera.width, camera.height) x, y = np.indices(size).astype(np.float32) xy: NDArrayNx2 = np.column_stack([x.ravel(), y.ravel()]) # The center of the upper left most pixel has coordinate (0.5, 0.5) xy += 0.5 xy_norm: NDArrayNx2 = camera.cam_from_img(image_points=xy) rays = np.concatenate([xy_norm, np.ones_like(xy_norm[:, :1])], -1) rays /= np.linalg.norm(rays, axis=-1, keepdims=True) return rays def spherical_img_from_cam( image_size: tuple[int, int], rays_in_cam: npt.NDArray[np.floating] ) -> npt.NDArray[np.floating]: """Project rays into a 360 panorama (spherical) image.""" if image_size[0] != image_size[1] * 2: raise ValueError("Only 360° panoramas are supported.") if rays_in_cam.ndim != 2 or rays_in_cam.shape[1] != 3: raise ValueError(f"{rays_in_cam.shape=} but expected (N,3).") r = rays_in_cam.T yaw = np.arctan2(r[0], r[2]) pitch = -np.arctan2(r[1], np.linalg.norm(r[[0, 2]], axis=0)) u = (1 + yaw / np.pi) / 2 v = (1 - pitch * 2 / np.pi) / 2 return np.stack([u, v], -1) * image_size def get_virtual_rotations( num_steps_yaw: int, pitches_deg: Sequence[float] ) -> Sequence[npt.NDArray[np.floating]]: """Get the relative rotations of the virtual cameras w.r.t. the panorama.""" # Assuming that the panos are approximately upright. cams_from_pano_r = [] yaws = np.linspace(0, 360, num_steps_yaw, endpoint=False) for pitch_deg in pitches_deg: yaw_offset = (360 / num_steps_yaw / 2) if pitch_deg > 0 else 0 for yaw_deg in yaws + yaw_offset: cam_from_pano_r = Rotation.from_euler( "XY", [-pitch_deg, -yaw_deg], degrees=True ).as_matrix() cams_from_pano_r.append(cam_from_pano_r) return cams_from_pano_r def create_pano_rig_config( cams_from_pano_rotation: Sequence[npt.NDArray[np.floating]], ref_idx: int = 0, ) -> pycolmap.RigConfig: """Create a RigConfig for the given virtual rotations.""" rig_cameras = [] zero_translation = cast(NDArray3x1, np.zeros((3, 1), dtype=np.float64)) for idx, cam_from_pano_rotation in enumerate(cams_from_pano_rotation): if idx == ref_idx: cam_from_rig = None else: cam_from_ref_rotation = ( cam_from_pano_rotation @ cams_from_pano_rotation[ref_idx].T ) cam_from_rig = pycolmap.Rigid3d( pycolmap.Rotation3d(cam_from_ref_rotation), zero_translation, ) rig_cameras.append( pycolmap.RigConfigCamera( ref_sensor=idx == ref_idx, image_prefix=f"pano_camera{idx}/", cam_from_rig=cam_from_rig, ) ) return pycolmap.RigConfig(cameras=rig_cameras) class PanoProcessor: def __init__( self, pano_image_dir: Path, output_image_dir: Path, mask_dir: Path, render_options: PanoRenderOptions, ): self.render_options = render_options self.pano_image_dir = pano_image_dir self.output_image_dir = output_image_dir self.mask_dir = mask_dir self.cams_from_pano_rotation = get_virtual_rotations( num_steps_yaw=render_options.num_steps_yaw, pitches_deg=render_options.pitches_deg, ) self.rig_config = create_pano_rig_config(self.cams_from_pano_rotation) # We assign each pano pixel to the virtual camera # with the closest camera center. self.cam_centers_in_pano = np.einsum( "nij,i->nj", self.cams_from_pano_rotation, [0, 0, 1] ) self._lock = Lock() # These are initialized on the first pano image # to avoid recomputing the rays for each pano image. self._camera: pycolmap.Camera | None = None self._pano_size: tuple[int, int] | None = None self._rays_in_cam: npt.NDArray[np.floating] | None = None def process(self, pano_name: str) -> None: pano_path = self.pano_image_dir / pano_name try: pano_pil_image = PIL.Image.open(pano_path) except PIL.Image.UnidentifiedImageError: logging.info(f"Skipping file {pano_path} as it cannot be read.") return pano_exif = pano_pil_image.getexif() pano_image = np.asarray(pano_pil_image) gpsonly_exif = PIL.Image.Exif() gpsonly_exif[PIL.ExifTags.IFD.GPSInfo] = pano_exif.get_ifd( PIL.ExifTags.IFD.GPSInfo ) pano_height, pano_width, *_ = pano_image.shape if pano_width != pano_height * 2: raise ValueError("Only 360° panoramas are supported.") with self._lock: if self._camera is None: # First image, precompute rays once. self._camera = create_virtual_camera( pano_width=pano_width, pano_height=pano_height, hfov_deg=self.render_options.hfov_deg, vfov_deg=self.render_options.vfov_deg, ) for rig_camera in self.rig_config.cameras: rig_camera.camera = self._camera self._pano_size = (pano_width, pano_height) self._rays_in_cam = get_virtual_camera_rays(self._camera) else: # Later images, verify consistent panoramas. if (pano_width, pano_height) != self._pano_size: raise ValueError( "Panoramas of different sizes are not supported." ) for cam_idx, cam_from_pano_r in enumerate(self.cams_from_pano_rotation): assert self._rays_in_cam is not None rays_in_pano = self._rays_in_cam @ cam_from_pano_r xy_in_pano = spherical_img_from_cam(self._pano_size, rays_in_pano) xy_in_pano = xy_in_pano.reshape( self._camera.width, self._camera.height, 2 ).astype(np.float32) xy_in_pano -= 0.5 # COLMAP to OpenCV pixel origin. x_coords, y_coords = np.moveaxis(xy_in_pano, [0, 1, 2], [2, 1, 0]) image = cv2.remap( pano_image, x_coords, y_coords, cv2.INTER_LINEAR, borderMode=cv2.BORDER_WRAP, ) # We define a mask such that each pixel of the panorama has its # features extracted only in a single virtual camera. closest_camera = np.argmax( rays_in_pano @ self.cam_centers_in_pano.T, -1 ) mask = ( ((closest_camera == cam_idx) * 255) .astype(np.uint8) .reshape(self._camera.width, self._camera.height) .transpose() ) image_name = ( self.rig_config.cameras[cam_idx].image_prefix + pano_name ) mask_name = f"{image_name}.png" image_path = self.output_image_dir / image_name image_path.parent.mkdir(exist_ok=True, parents=True) PIL.Image.fromarray(image).save(image_path, exif=gpsonly_exif) mask_path = self.mask_dir / mask_name mask_path.parent.mkdir(exist_ok=True, parents=True) if not pycolmap.Bitmap.from_array(mask).write(mask_path): raise RuntimeError(f"Cannot write {mask_path}") def render_perspective_images( pano_image_names: Sequence[str], pano_image_dir: Path, output_image_dir: Path, mask_dir: Path, render_options: PanoRenderOptions, ) -> pycolmap.RigConfig: processor = PanoProcessor( pano_image_dir, output_image_dir, mask_dir, render_options ) num_panos = len(pano_image_names) max_workers = min(32, (os.cpu_count() or 2) - 1) with tqdm(total=num_panos) as pbar: with ThreadPoolExecutor(max_workers=max_workers) as thread_pool: futures = [ thread_pool.submit(processor.process, pano_name) for pano_name in pano_image_names ] for future in as_completed(futures): future.result() pbar.update(1) return processor.rig_config def run(args: argparse.Namespace) -> None: pycolmap.set_random_seed(0) # Define the paths. image_dir = args.output_path / "images" mask_dir = args.output_path / "masks" image_dir.mkdir(exist_ok=True, parents=True) mask_dir.mkdir(exist_ok=True, parents=True) database_path = args.output_path / "database.db" if database_path.exists(): database_path.unlink() rec_path = args.output_path / "sparse" rec_path.mkdir(exist_ok=True, parents=True) # Search for input images. pano_image_dir = args.input_image_path pano_image_names = sorted( p.relative_to(pano_image_dir).as_posix() for p in pano_image_dir.rglob("*") if not p.is_dir() ) logging.info(f"Found {len(pano_image_names)} images in {pano_image_dir}.") rig_config = render_perspective_images( pano_image_names, pano_image_dir, image_dir, mask_dir, PANO_RENDER_OPTIONS[args.pano_render_type], ) pycolmap.extract_features( database_path, image_dir, reader_options=pycolmap.ImageReaderOptions(mask_path=mask_dir), camera_mode=pycolmap.CameraMode.PER_FOLDER, ) with pycolmap.Database.open(database_path) as db: pycolmap.apply_rig_config([rig_config], db) matching_options = pycolmap.FeatureMatchingOptions() # We have perfect sensor_from_rig poses (except for potential stitching # artifacts by the spherical image provider), so we can perform geometric # verification using rig constraints. matching_options.rig_verification = True # The images within a frame do not have overlap due to the provided masks. matching_options.skip_image_pairs_in_same_frame = True if args.matcher == "sequential": pycolmap.match_sequential( database_path, pairing_options=pycolmap.SequentialPairingOptions( loop_detection=True ), matching_options=matching_options, ) elif args.matcher == "exhaustive": pycolmap.match_exhaustive( database_path, matching_options=matching_options ) elif args.matcher == "vocabtree": pycolmap.match_vocabtree( database_path, matching_options=matching_options ) elif args.matcher == "spatial": pycolmap.match_spatial(database_path, matching_options=matching_options) else: logging.fatal(f"Unknown matcher: {args.matcher}") opts = pycolmap.IncrementalPipelineOptions( ba_refine_sensor_from_rig=False, ba_refine_focal_length=False, ba_refine_principal_point=False, ba_refine_extra_params=False, ) recs = pycolmap.incremental_mapping( database_path, image_dir, rec_path, opts ) for idx, rec in recs.items(): logging.info(f"#{idx} {rec.summary()}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--input_image_path", type=Path, required=True) parser.add_argument("--output_path", type=Path, required=True) parser.add_argument( "--matcher", default="sequential", choices=["sequential", "exhaustive", "vocabtree", "spatial"], ) parser.add_argument( "--pano_render_type", default="overlapping", choices=list(PANO_RENDER_OPTIONS.keys()), ) run(parser.parse_args()) colmap-4.0.4/python/examples/visualize_model.py000077500000000000000000000166521517363634500217300ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import argparse import numpy as np import numpy.typing as npt import open3d import pycolmap class Model: def __init__(self) -> None: self.reconstruction: pycolmap.Reconstruction self.visualizer: open3d.visualization.Visualizer def read_model(self, path: str) -> None: self.reconstruction = pycolmap.Reconstruction(path) def add_points( self, min_track_len: int = 3, remove_statistical_outlier: bool = True ) -> None: pcd = open3d.geometry.PointCloud() xyz = [] rgb = [] for point in self.reconstruction.points3D.values(): if point.track.length() < min_track_len: continue xyz.append(point.xyz) rgb.append(point.color / 255) pcd.points = open3d.utility.Vector3dVector(xyz) pcd.colors = open3d.utility.Vector3dVector(rgb) # remove obvious outliers if remove_statistical_outlier: [pcd, _] = pcd.remove_statistical_outlier( nb_neighbors=20, std_ratio=2.0 ) # open3d.visualization.draw_geometries([pcd]) self.visualizer.add_geometry(pcd) self.visualizer.poll_events() self.visualizer.update_renderer() def add_cameras(self, scale: float = 1) -> None: frustums = [] for img in self.reconstruction.images.values(): # extrinsics world_from_cam = img.cam_from_world().inverse() R = world_from_cam.rotation.matrix() t = world_from_cam.translation # intrinsics cam = img.camera if cam.model in ( pycolmap.CameraModelId.SIMPLE_PINHOLE, pycolmap.CameraModelId.SIMPLE_RADIAL, pycolmap.CameraModelId.RADIAL, ): fx = fy = cam.params[0] cx = cam.params[1] cy = cam.params[2] elif cam.model in ( pycolmap.CameraModelId.PINHOLE, pycolmap.CameraModelId.OPENCV, pycolmap.CameraModelId.OPENCV_FISHEYE, pycolmap.CameraModelId.FULL_OPENCV, ): fx = cam.params[0] fy = cam.params[1] cx = cam.params[2] cy = cam.params[3] else: raise Exception("Camera model not supported") # intrinsics K = np.identity(3) K[0, 0] = fx K[1, 1] = fy K[0, 2] = cx K[1, 2] = cy # create axis, plane and pyramid geometries that will be drawn cam_model = draw_camera(K, R, t, cam.width, cam.height, scale) frustums.extend(cam_model) # add geometries to visualizer for i in frustums: self.visualizer.add_geometry(i) def create_window(self) -> None: self.visualizer = open3d.visualization.Visualizer() self.visualizer.create_window() def show(self) -> None: self.visualizer.poll_events() self.visualizer.update_renderer() self.visualizer.run() self.visualizer.destroy_window() def draw_camera( K: npt.NDArray[np.float64], R: npt.NDArray[np.float64], t: npt.NDArray[np.float64], w: int, h: int, scale: float = 1, color: list[float] | None = None, ) -> list[open3d.geometry.Geometry]: """Create axis, plane and pyramed geometries in Open3D format. :param K: calibration matrix (camera intrinsics) :param R: rotation matrix :param t: translation :param w: image width :param h: image height :param scale: camera model scale :param color: color of the image plane and pyramid lines :return: camera model geometries (axis, plane and pyramid) """ if color is None: color = [0.8, 0.2, 0.8] # intrinsics K = K.copy() / scale Kinv = np.linalg.inv(K) # 4x4 transformation T = np.column_stack((R, t)) T = np.vstack((T, (0, 0, 0, 1))) # axis axis = open3d.geometry.TriangleMesh.create_coordinate_frame( size=0.5 * scale ) axis.transform(T) # points in pixel points_pixel = [ [0, 0, 0], [0, 0, 1], [w, 0, 1], [0, h, 1], [w, h, 1], ] # pixel to camera coordinate system points = [Kinv @ p for p in points_pixel] # image plane width = abs(points[1][0]) + abs(points[3][0]) height = abs(points[1][1]) + abs(points[3][1]) plane = open3d.geometry.TriangleMesh.create_box(width, height, depth=1e-6) plane.paint_uniform_color(color) plane.translate([points[1][0], points[1][1], scale]) plane.transform(T) # pyramid points_in_world = [(R @ p + t) for p in points] lines = [ [0, 1], [0, 2], [0, 3], [0, 4], ] colors = [color for i in range(len(lines))] line_set = open3d.geometry.LineSet( points=open3d.utility.Vector3dVector(points_in_world), lines=open3d.utility.Vector2iVector(lines), ) line_set.colors = open3d.utility.Vector3dVector(colors) # return as list in Open3D format return [axis, plane, line_set] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Visualize COLMAP binary and text models" ) parser.add_argument( "--input_model", required=True, help="path to input model folder" ) args = parser.parse_args() return args def main() -> None: args = parse_args() # read COLMAP model model = Model() model.read_model(args.input_model) print("num_cameras:", model.reconstruction.num_cameras()) print("num_images:", model.reconstruction.num_images()) print("num_points3D:", model.reconstruction.num_points3D()) # display using Open3D visualization tools model.create_window() model.add_points() model.add_cameras(scale=0.25) model.show() if __name__ == "__main__": main() colmap-4.0.4/python/generate_stubs.sh000077500000000000000000000022711517363634500177030ustar00rootroot00000000000000#!/bin/bash set -e PYTHON_EXEC=$1 OUTPUT=$2 PACKAGE_NAME="_core" echo "Building stubs with $PYTHON_EXEC to $OUTPUT" $PYTHON_EXEC -m pybind11_stubgen $PACKAGE_NAME -o $OUTPUT \ --numpy-array-use-type-var \ --enum-class-locations=.+:$PACKAGE_NAME \ --ignore-invalid-expressions "ceres::*" \ --print-invalid-expressions-as-is \ --print-safe-value-reprs "[a-zA-Z]+Options\(\)" FILES=$(find $OUTPUT/$PACKAGE_NAME/ -name '*.pyi' -type f) perl -i -pe's/\b_core\b/pycolmap/g' $FILES perl -i -pe's/: ceres::([a-zA-Z]|::)+//g' $FILES perl -i -pe's/ -> ceres::([a-zA-Z]|::)+:$/:/g' $FILES # pybind issue, will not be fixed: https://github.com/pybind/pybind11/pull/2277 perl -i -pe's/(?<=\b__(eq|ne)__\(self, )arg0: [a-zA-Z0-9_]+\)/other: object)/g' $FILES # mypy bug: https://github.com/python/mypy/issues/4266 perl -i -pe's/(__hash__:? .*= None)$/\1 # type: ignore/g' $FILES # pybind issue: dictionary keys should not be cast to the more generic types. perl -i -pe's/Mapping\[typing.Supports(Int|Float)/Mapping\[\L\1/g' $FILES COLMAP_DIR=$(dirname $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )) ruff format --config ${COLMAP_DIR}/ruff.toml ${FILES} colmap-4.0.4/python/incremental_build.sh000077500000000000000000000016241517363634500203520ustar00rootroot00000000000000#!/bin/bash # Invoke from anywhere to perform an incremental build of pycolmap bindings. # Make sure to install the requirements from pyproject.toml. If colmap is not # installed globally but in a custom directory, you should set the colmap_DIR # environment variable, e.g.: # # colmap_DIR=/path/to/cmake/install/prefix pycolmap/incremental_build.sh set -e script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) pip install \ --no-build-isolation \ -Cbuild-dir="$script_dir/build" \ -ve \ "$script_dir/.." # Symlink the compiled _core extension into the source tree so that the # editable install (which adds python/ to sys.path) can find it. site_pkg=$(python -c "import sysconfig; print(sysconfig.get_path('purelib'))") core_so=$(ls -t "$site_pkg/pycolmap"/_core*.so 2>/dev/null | head -1) if [ -n "$core_so" ]; then ln -sf "$core_so" "$script_dir/pycolmap/" fi colmap-4.0.4/python/pycolmap/000077500000000000000000000000001517363634500161545ustar00rootroot00000000000000colmap-4.0.4/python/pycolmap/__init__.py000066400000000000000000000042151517363634500202670ustar00rootroot00000000000000import contextlib import ctypes import importlib import platform import textwrap from pathlib import Path from typing import TYPE_CHECKING from .utils import import_module_symbols def _preload_cuda_lib(module_name: str, lib_name: str): """Preload a single library.""" try: module = importlib.import_module(module_name) except ImportError: return else: # TODO: update the logic to handle CUDA 13, # using as reference https://github.com/pytorch/pytorch/pull/163661. # Resolve the library directory robustly if module_file_path := getattr(module, "__file__", None): lib_dir = Path(module_file_path).parent elif paths := getattr(module, "__path__", None): # Implicit namespace packages have __path__ but no __file__ lib_dir = Path(list(paths)[0]) else: return # Find the first file matching the pattern if lib_path := next((lib_dir / "lib").glob(lib_name), None): with contextlib.suppress(OSError): ctypes.CDLL(str(lib_path)) def _preload_cuda_deps(): """Preloads CUDA dependencies from pip packages on Linux.""" if platform.system() != "Linux": return cuda_libs = { "nvidia.cuda_runtime": "libcudart.so.*[0-9]", "nvidia.curand": "libcurand.so.*[0-9]", } for module_name, lib_glob in cuda_libs.items(): _preload_cuda_lib(module_name, lib_glob) _preload_cuda_deps() try: from . import _core except ImportError as e: raise RuntimeError( textwrap.dedent(""" Cannot import the C++ backend pycolmap._core. Make sure that you successfully install the package with $ python -m pip install pycolmap/ """) ) from e # Type checkers cannot deal with dynamic manipulation of globals. # Instead, we use the same workaround as PyTorch. if TYPE_CHECKING: from ._core import * # noqa F403 __all__ = import_module_symbols( globals(), _core, exclude={"cost_functions", "pyceres"} ) __all__.extend(["__version__", "__ceres_version__"]) __version__ = _core.__version__ __ceres_version__ = _core.__ceres_version__ colmap-4.0.4/python/pycolmap/cost_functions/000077500000000000000000000000001517363634500212145ustar00rootroot00000000000000colmap-4.0.4/python/pycolmap/cost_functions/__init__.py000066400000000000000000000003611517363634500233250ustar00rootroot00000000000000from typing import TYPE_CHECKING from .. import _core from ..utils import import_module_symbols if TYPE_CHECKING: from .._core.cost_functions import * # noqa __all__ = import_module_symbols(globals(), _core.cost_functions) del _core colmap-4.0.4/python/pycolmap/py.typed000066400000000000000000000000001517363634500176410ustar00rootroot00000000000000colmap-4.0.4/python/pycolmap/pyceres/000077500000000000000000000000001517363634500176265ustar00rootroot00000000000000colmap-4.0.4/python/pycolmap/pyceres/__init__.py000066400000000000000000000003431517363634500217370ustar00rootroot00000000000000from typing import TYPE_CHECKING from .. import _core from ..utils import import_module_symbols if TYPE_CHECKING: from .._core.pyceres import * # noqa __all__ = import_module_symbols(globals(), _core.pyceres) del _core colmap-4.0.4/python/pycolmap/utils.py000066400000000000000000000007701517363634500176720ustar00rootroot00000000000000from collections.abc import MutableSequence from types import ModuleType from typing import Any def import_module_symbols( dst_vars: dict[str, Any], src_module: ModuleType, exclude: set[str] | None = None, ) -> MutableSequence[str]: symbols = {} for n, s in vars(src_module).items(): if n.startswith("_"): continue if exclude is not None and n in exclude: continue symbols[n] = s dst_vars.update(symbols) return list(symbols) colmap-4.0.4/python/util/000077500000000000000000000000001517363634500153055ustar00rootroot00000000000000colmap-4.0.4/python/util/flickr_downloader.py000077500000000000000000000152151517363634500213560ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. import argparse import datetime import multiprocessing import os import time import urllib.error import urllib.request import xml.etree.ElementTree as ElementTree from typing import Final import urllib2 import urlparse PER_PAGE: Final[int] = 500 SORT: Final[str] = "date-posted-desc" URL: Final[str] = ( "https://api.flickr.com/services/rest/?method=flickr.photos.search&" "api_key=%s&text=%s&sort=%s&per_page=%d&page=%d&min_upload_date=%s&" "max_upload_date=%s&format=rest&extras=url_o,url_l,url_c,url_z,url_n" ) MAX_PAGE_REQUESTS: Final[int] = 5 MAX_PAGE_TIMEOUT: Final[int] = 20 MAX_IMAGE_REQUESTS: Final[int] = 3 TIME_SKIP: Final[int] = 24 * 60 * 60 MAX_DATE: Final[float] = time.time() MIN_DATE: Final[float] = MAX_DATE - TIME_SKIP def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--search_text", required=True) parser.add_argument("--api_key", required=True) parser.add_argument("--image_path", required=True) parser.add_argument("--num_procs", type=int, default=10) parser.add_argument("--max_days_without_image", type=int, default=365) args = parser.parse_args() return args def compose_url( page: int, api_key: str, text: str, min_date: float, max_date: float ) -> str: return URL % ( api_key, text, SORT, PER_PAGE, page, str(min_date), str(max_date), ) def parse_page( page: int, api_key: str, text: str, min_date: float, max_date: float ) -> tuple[dict[str, str], tuple[dict[str, str], ...]]: f: urllib2.urlopen | None = None for _ in range(MAX_PAGE_REQUESTS): try: f = urllib2.urlopen( compose_url(page, api_key, text, min_date, max_date), timeout=MAX_PAGE_TIMEOUT, ) except TimeoutError: continue else: break if f is None: return { "pages": "0", "total": "0", "page": "0", "perpage": "0", }, tuple() response = f.read() root = ElementTree.fromstring(response) if root.attrib["stat"] != "ok": raise OSError metadata = root.find("photos") assert metadata is not None photos = tuple(photo.attrib for photo in root.iter("photo")) return metadata.attrib, photos class PhotoDownloader: def __init__(self, image_path: str) -> None: self.image_path: str = image_path def __call__(self, photo: dict[str, str]) -> None: # Find the URL corresponding to the highest image resolution. We will # need this URL here to determine the image extension (typically .jpg, # but could be .png, .gif, etc). url: str | None = None for url_suffix in ("o", "l", "k", "h", "b", "c", "z"): url_attr = f"url_{url_suffix}" if photo.get(url_attr) is not None: url = photo.get(url_attr) break if url is not None: # Note that the following statement may fail in Python 3. urlparse # may need to be replaced with urllib.parse. url_filename = urlparse.urlparse(url).path image_ext = os.path.splitext(url_filename)[1] image_name = f"{photo['id']}_{photo['secret']}{image_ext}" path = os.path.join(self.image_path, image_name) if not os.path.exists(path): print(url) for _ in range(MAX_IMAGE_REQUESTS): try: urllib.request.urlretrieve(url, path) except urllib.error.ContentTooShortError: continue else: break def main() -> None: args = parse_args() downloader = PhotoDownloader(args.image_path) pool = multiprocessing.Pool(processes=args.num_procs) num_pages = float("inf") page = 0 min_date = MIN_DATE max_date = MAX_DATE days_in_row = 0 search_text = args.search_text.replace(" ", "-") while num_pages > page: page += 1 metadata, photos = parse_page( page, args.api_key, search_text, min_date, max_date ) num_pages = int(metadata["pages"]) print(78 * "=") print("Page:\t\t", page, "of", num_pages) print("Min-Date:\t", datetime.datetime.fromtimestamp(min_date)) print("Max-Date:\t", datetime.datetime.fromtimestamp(max_date)) print("Num-Photos:\t", len(photos)) print(78 * "=") try: pool.map_async(downloader, photos).get(1e10) except KeyboardInterrupt: pool.close() pool.join() break if page >= num_pages: max_date -= TIME_SKIP min_date -= TIME_SKIP page = 0 if num_pages == 0: days_in_row = days_in_row + 1 num_pages = float("inf") print(" No images in", days_in_row, "days in a row") if days_in_row == args.max_days_without_image: break else: days_in_row = 0 if __name__ == "__main__": main() colmap-4.0.4/ruff.toml000066400000000000000000000004441517363634500146500ustar00rootroot00000000000000line-length = 80 [lint] select = [ # pycodestyle "E", # Pyflakes "F", # pyupgrade "UP", # flake8-bugbear "B", # flake8-simplify "SIM", # isort "I", ] ignore = ["SIM117"] [lint.per-file-ignores] "scripts/python/*.py" = ["E", "SIM", "UP", "B"] colmap-4.0.4/scripts/000077500000000000000000000000001517363634500144765ustar00rootroot00000000000000colmap-4.0.4/scripts/format/000077500000000000000000000000001517363634500157665ustar00rootroot00000000000000colmap-4.0.4/scripts/format/c++.sh000077500000000000000000000017001517363634500166730ustar00rootroot00000000000000#!/usr/bin/env bash # This script applies clang-format to the whole repository. # Check version version_string=$(clang-format --version | sed -E 's/^.*(\d+\.\d+\.\d+-.*).*$/\1/') expected_version_string='21.1.8' if [[ "$version_string" =~ "$expected_version_string" ]]; then echo "clang-format version '$version_string' matches '$expected_version_string'" else echo "clang-format version '$version_string' doesn't match '$expected_version_string'" exit 1 fi # Get all C++ files checked into the repo, excluding submodules root_folder=$(git rev-parse --show-toplevel) extensions_regex="\(\.cc\|\.h\|\.hpp\|\.cpp\|\.cu\)" all_files=$( \ git ls-tree --full-tree -r --name-only HEAD . \ | grep "\(^src/\(colmap\|glomap\|pycolmap\).*$extensions_regex$\)\|\(benchmark/.*$extensions_regex$\)" \ | sed "s~^~$root_folder/~") num_files=$(echo $all_files | wc -w) echo "Formatting ${num_files} files" echo "$all_files" | xargs clang-format -i colmap-4.0.4/scripts/format/python.sh000077500000000000000000000016241517363634500176510ustar00rootroot00000000000000#!/usr/bin/env bash # This script runs the ruff Python formatter on the whole repository. # Check version version_string=$(ruff --version | sed -E 's/^.*(\d+\.\d+-.*).*$/\1/') expected_version_string='0.15.0' if [[ "$version_string" =~ "$expected_version_string" ]]; then echo "ruff version '$version_string' matches '$expected_version_string'" else echo "ruff version '$version_string' doesn't match '$expected_version_string'" exit 1 fi # Get all C++ files checked into the repo, excluding submodules root_folder=$(git rev-parse --show-toplevel) all_files=$( \ git ls-tree --full-tree -r --name-only HEAD . \ | grep "^.*\(\.py\)$" \ | sed "s~^~$root_folder/~") num_files=$(echo $all_files | wc -w) echo "Formatting ${num_files} files" # shellcheck disable=SC2086 ruff format --config ${root_folder}/ruff.toml ${all_files} ruff check --config ${root_folder}/ruff.toml ${all_files} --fix colmap-4.0.4/scripts/matlab/000077500000000000000000000000001517363634500157365ustar00rootroot00000000000000colmap-4.0.4/scripts/matlab/cmap2rgb.m000066400000000000000000000036321517363634500176150ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function rgb = cmap2rgb(image, cmap, varargin) if length(varargin) == 1 clim = varargin{1}; image(image <= clim(1)) = clim(1); image(image > clim(2)) = clim(2); end image_min = min(image(:)); image_max = max(image(:)); image = (image - image_min) / (image_max - image_min) * size(cmap, 1); rgb = ind2rgb(uint32(image), cmap); end colmap-4.0.4/scripts/matlab/plot_model.m000077500000000000000000000045711517363634500202640ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function plot_model(cameras, images, points) % Visualize COLMAP model. keys = images.keys; camera_centers = zeros(images.length, 3); view_dirs = zeros(3 * images.length, 3); for i = 1:images.length image_id = keys{i}; image = images(image_id); camera_centers(i,:) = -image.R' * image.t; view_dirs(3 * i - 2,:) = camera_centers(i,:); view_dirs(3 * i - 1,:) = camera_centers(i,:)' + image.R' * [0; 0; 0.3]; view_dirs(3 * i,:) = nan; end keys = points.keys; xyz = zeros(points.length, 3); for i = 1:points.length point_id = keys{i}; point = points(point_id); xyz(i,:) = point.xyz; end hold on; plot3(camera_centers(:,1), camera_centers(:,2), camera_centers(:,3), 'xr'); plot3(view_dirs(:,1), view_dirs(:,2), view_dirs(:,3), '-b'); plot3(xyz(:,1), xyz(:,2), xyz(:,3), '.k'); hold off; end colmap-4.0.4/scripts/matlab/quat2rotmat.m000077500000000000000000000041601517363634500204030ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function rotmat = quat2rotmat(qvec) rotmat = [1 - 2 * qvec(3).^2 - 2 * qvec(4).^2, ... 2 * qvec(2) * qvec(3) - 2 * qvec(1) * qvec(4), ... 2 * qvec(4) * qvec(2) + 2 * qvec(1) * qvec(3); ... 2 * qvec(2) * qvec(3) + 2 * qvec(1) * qvec(4), ... 1 - 2 * qvec(2).^2 - 2 * qvec(4).^2, ... 2 * qvec(3) * qvec(4) - 2 * qvec(1) * qvec(2); ... 2 * qvec(4) * qvec(2) - 2 * qvec(1) * qvec(3), ... 2 * qvec(3) * qvec(4) + 2 * qvec(1) * qvec(2), ... 1 - 2 * qvec(2).^2 - 2 * qvec(3).^2]; end colmap-4.0.4/scripts/matlab/read_array.m000066400000000000000000000037711517363634500202350ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function array = read_array(path, varargin) if length(varargin) == 1 dtype = varargin{1}; else dtype = 'single'; end fid = fopen(path); line = fscanf(fid, '%d&%d&%d&', [1, 3]); width = line(1); height = line(2); channels = line(3); num = width * height * channels; array = fread(fid, num, dtype); array = reshape(array, [width, height, channels]); array = permute(array, [2 1 3]); array = cast(array, dtype); fclose(fid); end colmap-4.0.4/scripts/matlab/read_depth_map.m000066400000000000000000000035031517363634500210510ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function [depth_map, depth_map_rgb] = read_depth_map(path) depth_map = read_array(path); depth_range = prctile(depth_map(depth_map(:) > 0), [2, 98]); depth_map(depth_map<=0) = nan; depth_map_rgb = cmap2rgb(depth_map, [0 0 0; jet(2^15)], depth_range); end colmap-4.0.4/scripts/matlab/read_model.m000077500000000000000000000111541517363634500202140ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function [cameras, images, points3D] = read_model(path) % Read COLMAP model from folder, which contains a % cameras.txt, images.txt, and points3D.txt. if numel(path) > 0 && path(end) ~= '/' path = [path '/']; end cameras = read_cameras([path 'cameras.txt']); images = read_images([path 'images.txt']); points3D = read_points3D([path 'points3D.txt']); end function cameras = read_cameras(path) cameras = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) elems = strsplit(tline); if numel(elems) < 4 || strcmp(elems(1), '#') tline = fgets(fid); continue end if mod(cameras.Count, 10) == 0 fprintf('Reading camera %d\n', cameras.length); end camera = struct; camera.camera_id = str2num(elems{1}); camera.model = elems{2}; camera.width = str2num(elems{3}); camera.height = str2num(elems{4}); camera.params = zeros(numel(elems) - 5, 1); for i = 5:numel(elems) - 1 camera.params(i - 4) = str2double(elems{i}); end cameras(camera.camera_id) = camera; tline = fgets(fid); end fclose(fid); end function images = read_images(path) images = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) elems = strsplit(tline); if numel(elems) < 4 || strcmp(elems(1), '#') tline = fgets(fid); continue end if mod(images.Count, 10) == 0 fprintf('Reading image %d\n', images.length); end image = struct; image.image_id = str2num(elems{1}); qw = str2double(elems{2}); qx = str2double(elems{3}); qy = str2double(elems{4}); qz = str2double(elems{5}); image.R = quat2rotmat([qw, qx, qy, qz]); tx = str2double(elems{6}); ty = str2double(elems{7}); tz = str2double(elems{8}); image.t = [tx; ty; tz]; image.camera_id = str2num(elems{9}); image.name = elems{10}; tline = fgets(fid); elems = sscanf(tline, '%f'); elems = reshape(elems, [3, numel(elems) / 3]); image.xys = elems(1:2,:)'; image.point3D_ids = elems(3,:)'; images(image.image_id) = image; tline = fgets(fid); end fclose(fid); end function points3D = read_points3D(path) points3D = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) if numel(tline) == 0 || strcmp(tline(1), '#') tline = fgets(fid); continue; end elems = sscanf(tline, '%f'); if numel(elems) == 0 tline = fgets(fid); continue; end if mod(points3D.Count, 1000) == 0 fprintf('Reading point %d\n', points3D.length); end point = struct; point.point3D_id = int64(elems(1)); point.xyz = elems(2:4); point.rgb = uint8(elems(5:7)); point.error = elems(8); point.track = int64(elems(9:end)); point.track = reshape(point.track, [2, numel(point.track) / 2])'; point.track(:,2) = point.track(:,2) + 1; points3D(point.point3D_id) = point; tline = fgets(fid); end fclose(fid); end colmap-4.0.4/scripts/matlab/read_normal_map.m000066400000000000000000000040761517363634500212430ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function [normal_map, normal_map_rgb] = read_normal_map(path, varargin) normal_map = read_array(path); normal_map_rgb = -[normal_map(:,:,1) normal_map(:,:,2) ... 2 * normal_map(:,:,3) + 1]; normal_map_rgb = reshape(normal_map_rgb, ... size(normal_map,1), size(normal_map,2), 3); normal_map_rgb = uint8(((normal_map_rgb + 1) ./ 2) .* 255); if length(varargin) == 1 depth_map = varargin{1}; normal_map_rgb(repmat(isnan(depth_map), [1, 1, 3])) = 0; end end colmap-4.0.4/scripts/matlab/read_ply.m000077500000000000000000000044161517363634500177230ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function [xyz, normals, rgb] = read_ply(path) % Read point cloud from PLY text file. file = fopen(path, 'r'); type = fscanf(file, '%s', 1); format = fscanf(file, '%s', 3); data = fscanf(file, '%s', 2); num_points = fscanf(file, '%d', 1); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 1); points_data = textscan(file, '%f %f %f %f %f %f %f %f %f', num_points); xyz = [points_data{1}, points_data{2}, points_data{3}]; rgb = [points_data{7}, points_data{8}, points_data{9}]; normals = [points_data{4}, points_data{5}, points_data{6}]; end colmap-4.0.4/scripts/matlab/write_array.m000066400000000000000000000034331517363634500204470ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function write_array(path, array) fid = fopen(path, 'w'); fprintf(fid, '%d&%d&%d&', size(array, 2), size(array, 1), size(array, 3)); array = permute(array, [2 1 3]); fwrite(fid, array, class(array)); fclose(fid); end colmap-4.0.4/scripts/matlab/write_ply.m000077500000000000000000000046411517363634500201420ustar00rootroot00000000000000% Copyright (c), ETH Zurich and UNC Chapel Hill. % 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 the name of ETH Zurich and UNC Chapel Hill nor the names of % its 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 COPYRIGHT HOLDERS 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. function write_ply(path, xyz, normals, rgb) % Write point cloud to PLY text file. file = fopen(path, 'W'); fprintf(file,'ply\n'); fprintf(file,'format ascii 1.0\n'); fprintf(file,'element vertex %d\n',size(xyz,1)); fprintf(file,'property float x\n'); fprintf(file,'property float y\n'); fprintf(file,'property float z\n'); fprintf(file,'property float nx\n'); fprintf(file,'property float ny\n'); fprintf(file,'property float nz\n'); fprintf(file,'property uchar diffuse_red\n'); fprintf(file,'property uchar diffuse_green\n'); fprintf(file,'property uchar diffuse_blue\n'); fprintf(file,'end_header\n'); for i = 1:size(xyz, 1) fprintf(file, '%f %f %f %f %f %f %d %d %d\n', ... xyz(i,1), xyz(i,2), xyz(i,3), ... normals(i,1), normals(i,2), normals(i,3), ... uint8(rgb(i,1)), uint8(rgb(i,2)), uint8(rgb(i,3))); end fclose(file); end colmap-4.0.4/scripts/shell/000077500000000000000000000000001517363634500156055ustar00rootroot00000000000000colmap-4.0.4/scripts/shell/build_mac_app.sh000077500000000000000000000066201517363634500207270ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # This script creates a deployable package of COLMAP for Mac OS X. BASE_PATH=$(dirname $1) echo "Creating bundle directory" mkdir -p "$BASE_PATH/COLMAP.app/Contents/MacOS" echo "Copying binary" cp "$BASE_PATH/colmap" "$BASE_PATH/COLMAP.app/Contents/MacOS/colmap" echo "Writing Info.plist" cat <"$BASE_PATH/COLMAP.app/Contents/Info.plist" CFBundlePackageType APPL CFBundleExecutable colmap CFBundleIdentifier COLMAP CFBundleName COLMAP CFBundleDisplayName COLMAP NSHighResolutionCapable NSAppSleepDisabled EOM install_name_tool -change @rpath/libtbb.dylib /usr/local/lib/libtbb.dylib $BASE_PATH/COLMAP.app/Contents/MacOS/COLMAP install_name_tool -change @rpath/libtbbmalloc.dylib /usr/local/lib/libtbbmalloc.dylib $BASE_PATH/COLMAP.app/Contents/MacOS/COLMAP echo "Linking dynamic libraries" if [ -d "$(brew --prefix)/opt/qt6" ]; then $(brew --prefix)/opt/qt6/bin/macdeployqt "$BASE_PATH/COLMAP.app" else $(brew --prefix)/opt/qt5/bin/macdeployqt "$BASE_PATH/COLMAP.app" fi echo "Wrapping binary" cat <"$BASE_PATH/COLMAP.app/Contents/MacOS/colmap_gui.sh" #!/bin/bash script_path="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" \$script_path/colmap gui EOM chmod +x $BASE_PATH/COLMAP.app/Contents/MacOS/colmap_gui.sh sed -i '' 's#colmap#colmap_gui.sh#g' $BASE_PATH/COLMAP.app/Contents/Info.plist echo "Compressing application" cd "$BASE_PATH" zip -r "COLMAP-mac.zip" "COLMAP.app" colmap-4.0.4/scripts/shell/colmap.bat000077500000000000000000000035261517363634500175610ustar00rootroot00000000000000@echo off rem Copyright (c), ETH Zurich and UNC Chapel Hill. rem All rights reserved. rem rem Redistribution and use in source and binary forms, with or without rem modification, are permitted provided that the following conditions are met: rem rem * Redistributions of source code must retain the above copyright rem notice, this list of conditions and the following disclaimer. rem rem * Redistributions in binary form must reproduce the above copyright rem notice, this list of conditions and the following disclaimer in the rem documentation and/or other materials provided with the distribution. rem rem * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of rem its contributors may be used to endorse or promote products derived rem from this software without specific prior written permission. rem rem THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" rem AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE rem IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE rem ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE rem LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR rem CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF rem SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS rem INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN rem CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) rem ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE rem POSSIBILITY OF SUCH DAMAGE. set SCRIPT_PATH=%~dp0 set PATH=%SCRIPT_PATH%\bin;%PATH% set QT_PLUGIN_PATH=%SCRIPT_PATH%\plugins;%QT_PLUGIN_PATH% set ARGUMENTS=%* if "%ARGUMENTS%"=="" set ARGUMENTS=gui "%SCRIPT_PATH%\bin\colmap" %ARGUMENTS% colmap-4.0.4/scripts/shell/enter_vs_dev_shell.ps1000066400000000000000000000015031517363634500221030ustar00rootroot00000000000000if (!$env:VisualStudioDevShell) { $vswhere = "${Env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" if (!(Test-Path $vswhere)) { throw "Failed to find vswhere.exe" } & $vswhere -latest -format json $vsInstance = & $vswhere -latest -format json | ConvertFrom-Json if ($LASTEXITCODE) { throw "vswhere.exe returned exit code $LASTEXITCODE" } Import-Module "$($vsInstance.installationPath)/Common7/Tools/Microsoft.VisualStudio.DevShell.dll" $prevCwd = Get-Location try { Enter-VsDevShell $vsInstance.instanceId -DevCmdArguments "-no_logo -host_arch=amd64 -arch=amd64" } catch { Write-Host $_ Write-Error "Failed to enter Visual Studio Dev Shell" exit 1 } Set-Location $prevCwd $env:VisualStudioDevShell = $true } colmap-4.0.4/scripts/shell/generate_coverage_report.sh000077500000000000000000000047741517363634500232200ustar00rootroot00000000000000#!/bin/bash # Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Script to generate a coverage report for the COLMAP code base. Must be # executed from the build directory. The generated HTML report will be available # in the coverage/ directory. # The script assumes that the codebase has been compiled with CMake options: # cmake ... # -DTESTS_ENABLED=ON \ # -DCOVERAGE_ENABLED=ON \ # -DCMAKE_BUILD_TYPE=RelWithDebInfo|Debug # ninja # ctest -j$(nproc) colmap_root_dir=$(git rev-parse --show-toplevel) if [ ! -f "CMakeCache.txt" ]; then echo "Please run this script from the build directory." exit 1 fi rm -rf coverage-html mkdir -p coverage-html gcovr \ --root "$colmap_root_dir" \ --exclude "$colmap_root_dir/src/thirdparty/*" \ --exclude "$(pwd)/_deps/*" \ --exclude "$(pwd)/src/colmap/ui/colmap_ui_autogen/*" \ --cobertura coverage-cobertura.xml \ --cobertura-pretty \ --html-nested coverage-html/index.html \ --html-theme github.blue colmap-4.0.4/scripts/shell/images_to_video.sh000077500000000000000000000033111517363634500212770ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Command to produce video from images produced by COLMAP movie grabber tool. ffmpeg -i frame%06d.png -r 30 -vf scale=1680:1050 out.mp4 colmap-4.0.4/scripts/shell/profile_binary.sh000077500000000000000000000047751517363634500211650ustar00rootroot00000000000000#!/bin/bash # Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. # Script to perform profiling on a command. For example: # ./profile_binary.sh ./src/colmap/exe/colmap automatic_reconstructor ... perf_bin=$(find /usr/lib/linux-tools -name perf | head -1) if [[ -z $perf_bin ]]; then echo "Error: Perf tool not found. Under ubuntu, install as:" echo " sudo apt-get install linux-tools-generic" exit 1 fi "$perf_bin" record -e cycles:u -g "$@" binary_filename=$(basename -- "${binary_path}") profile_path="$binary_filename.perf.data" mv perf.data "$profile_path" echo "#####################################################################" echo "###### Profiling finished. Inspect results using the commands: ######" echo "#####################################################################" echo "If the perf output contains unknown list items, recompile with RelWithDebInfo" echo "$perf_bin report -i $profile_path" echo "$perf_bin report --stdio -g graph,0.5,caller -i $profile_path" colmap-4.0.4/scripts/shell/restore_git_submodules.sh000077500000000000000000000035211517363634500227350ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. set -e git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | while read path_key path do url_key=$(echo $path_key | sed 's/\.path/.url/') url=$(git config -f .gitmodules --get "$url_key") git submodule add -f $url $path done colmap-4.0.4/scripts/shell/run_tests.bat000077500000000000000000000035511517363634500203320ustar00rootroot00000000000000@echo off rem Copyright (c), ETH Zurich and UNC Chapel Hill. rem All rights reserved. rem rem Redistribution and use in source and binary forms, with or without rem modification, are permitted provided that the following conditions are met: rem rem * Redistributions of source code must retain the above copyright rem notice, this list of conditions and the following disclaimer. rem rem * Redistributions in binary form must reproduce the above copyright rem notice, this list of conditions and the following disclaimer in the rem documentation and/or other materials provided with the distribution. rem rem * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of rem its contributors may be used to endorse or promote products derived rem from this software without specific prior written permission. rem rem THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" rem AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE rem IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE rem ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE rem LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR rem CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF rem SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS rem INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN rem CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) rem ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE rem POSSIBILITY OF SUCH DAMAGE. set SCRIPT_PATH=%~dp0 set PATH=%SCRIPT_PATH%\bin;%PATH% set QT_PLUGIN_PATH=%SCRIPT_PATH%\plugins;%QT_PLUGIN_PATH% @echo on for %%i in (%SCRIPT_PATH%\bin\*_test.exe) do ( %%i if %errorlevel% neq 0 goto end ) :end pause colmap-4.0.4/src/000077500000000000000000000000001517363634500135765ustar00rootroot00000000000000colmap-4.0.4/src/colmap/000077500000000000000000000000001517363634500150515ustar00rootroot00000000000000colmap-4.0.4/src/colmap/CMakeLists.txt000066400000000000000000000063341517363634500176170ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. if(IS_MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W3") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3") if(WERROR_ENABLED) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Werror") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Werror") endif() # Avoid pulling in too many header files through add_compile_definitions(WIN32_LEAN_AND_MEAN) elseif(IS_GNU OR IS_CLANG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") if(WERROR_ENABLED) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") endif() endif() if(CUDA_ENABLED) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --use_fast_math") # Use a separate stream per thread to allow for concurrent kernel execution # between multiple threads on the same device. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --default-stream per-thread") # Suppress warnings: # ptxas warning : Stack size for entry function X cannot be statically determined. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Xptxas=-suppress-stack-size-warning") endif() if(COVERAGE_ENABLED) add_compile_options(-coverage -fprofile-update=atomic) add_link_options(-coverage) endif() add_subdirectory(controllers) add_subdirectory(estimators) add_subdirectory(exe) add_subdirectory(feature) add_subdirectory(geometry) add_subdirectory(image) add_subdirectory(math) add_subdirectory(mvs) add_subdirectory(optim) add_subdirectory(retrieval) add_subdirectory(scene) add_subdirectory(sensor) add_subdirectory(sfm) add_subdirectory(tools) add_subdirectory(util) if (GUI_ENABLED) add_subdirectory(ui) endif() colmap-4.0.4/src/colmap/controllers/000077500000000000000000000000001517363634500174175ustar00rootroot00000000000000colmap-4.0.4/src/colmap/controllers/CMakeLists.txt000066400000000000000000000114471517363634500221660ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. set(FOLDER_NAME "controllers") COLMAP_ADD_LIBRARY( NAME colmap_controllers SRCS automatic_reconstruction.h automatic_reconstruction.cc base_option_manager.h base_option_manager.cc bundle_adjustment.h bundle_adjustment.cc hierarchical_pipeline.h hierarchical_pipeline.cc feature_extraction.h feature_extraction.cc feature_matching.h feature_matching.cc feature_matching_utils.h feature_matching_utils.cc matcher_cache.h matcher_cache.cc pairing.h pairing.cc global_pipeline.h global_pipeline.cc image_reader.h image_reader.cc incremental_pipeline.h incremental_pipeline.cc option_manager.h option_manager.cc reconstruction_clustering.h reconstruction_clustering.cc rotation_averaging.h rotation_averaging.cc undistorters.h undistorters.cc PUBLIC_LINK_LIBS colmap_estimators colmap_feature colmap_retrieval colmap_geometry colmap_scene colmap_util Eigen3::Eigen Boost::program_options PRIVATE_LINK_LIBS colmap_image colmap_math colmap_mvs colmap_sfm Ceres::ceres Boost::boost faiss ) if(CUDA_ENABLED) target_link_libraries(colmap_controllers PRIVATE colmap_util_cuda colmap_mvs_cuda ) endif() COLMAP_ADD_TEST( NAME automatic_reconstruction_test SRCS automatic_reconstruction_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME bundle_adjustment_test SRCS bundle_adjustment_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME hierarchical_pipeline_test SRCS hierarchical_pipeline_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME incremental_pipeline_test SRCS incremental_pipeline_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME image_reader_test SRCS image_reader_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME feature_extraction_test SRCS feature_extraction_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME feature_matching_test SRCS feature_matching_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME feature_matching_utils_test SRCS feature_matching_utils_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME global_pipeline_test SRCS global_pipeline_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME base_option_manager_test SRCS base_option_manager_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME option_manager_test SRCS option_manager_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME rotation_averaging_test SRCS rotation_averaging_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME reconstruction_clustering_test SRCS reconstruction_clustering_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME undistorters_test SRCS undistorters_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME matcher_cache_test SRCS matcher_cache_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME pairing_test SRCS pairing_test.cc LINK_LIBS colmap_controllers ) colmap-4.0.4/src/colmap/controllers/automatic_reconstruction.cc000066400000000000000000000423241517363634500250620ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/automatic_reconstruction.h" #include "colmap/controllers/feature_extraction.h" #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/global_pipeline.h" #include "colmap/controllers/hierarchical_pipeline.h" #include "colmap/controllers/incremental_pipeline.h" #include "colmap/controllers/option_manager.h" #include "colmap/controllers/undistorters.h" #include "colmap/estimators/view_graph_calibration.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match.h" #include "colmap/retrieval/resources.h" #include "colmap/scene/database.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" namespace colmap { AutomaticReconstructionController::AutomaticReconstructionController( const Options& options, std::shared_ptr reconstruction_manager) : options_(options), reconstruction_manager_(std::move(reconstruction_manager)), active_thread_(nullptr) { THROW_CHECK_DIR_EXISTS(options_.workspace_path); THROW_CHECK_DIR_EXISTS(options_.image_path); THROW_CHECK_NOTNULL(reconstruction_manager_); option_manager_.AddAllOptions(); *option_manager_.image_path = options_.image_path; option_manager_.image_reader->image_names = options_.image_names; option_manager_.mapper->image_names = {options_.image_names.begin(), options_.image_names.end()}; *option_manager_.database_path = options_.workspace_path / "database.db"; if (options_.data_type == DataType::VIDEO) { option_manager_.ModifyForVideoData(); } else if (options_.data_type == DataType::INDIVIDUAL) { option_manager_.ModifyForIndividualData(); } else if (options_.data_type == DataType::INTERNET) { option_manager_.ModifyForInternetData(); } else { LOG(FATAL_THROW) << "Data type not supported"; } THROW_CHECK(ExistsCameraModelWithName(options_.camera_model)); // Set feature type first so quality modifiers can query EffMaxImageSize(). if (options_.feature == Feature::SIFT) { option_manager_.feature_extraction->type = FeatureExtractorType::SIFT; option_manager_.feature_matching->type = FeatureMatcherType::SIFT_BRUTEFORCE; } else if (options_.feature == Feature::ALIKED) { option_manager_.feature_extraction->type = FeatureExtractorType::ALIKED_N16ROT; option_manager_.feature_matching->type = FeatureMatcherType::ALIKED_BRUTEFORCE; } // Apply quality preset (scales max_image_size relative to extractor default). if (options_.quality == Quality::LOW) { option_manager_.ModifyForLowQuality(); } else if (options_.quality == Quality::MEDIUM) { option_manager_.ModifyForMediumQuality(); } else if (options_.quality == Quality::HIGH) { option_manager_.ModifyForHighQuality(); } else if (options_.quality == Quality::EXTREME) { option_manager_.ModifyForExtremeQuality(); } // Feature-specific overrides that must come after quality. if (options_.feature == Feature::ALIKED) { // Guided matching is not supported for ALIKED. option_manager_.feature_matching->guided_matching = false; } option_manager_.feature_extraction->num_threads = options_.num_threads; option_manager_.feature_matching->num_threads = options_.num_threads; option_manager_.sequential_pairing->num_threads = options_.num_threads; option_manager_.vocab_tree_pairing->num_threads = options_.num_threads; option_manager_.mapper->num_threads = options_.num_threads; option_manager_.patch_match_stereo->num_threads = options_.num_threads; option_manager_.poisson_meshing->num_threads = options_.num_threads; option_manager_.delaunay_meshing->num_threads = options_.num_threads; option_manager_.vocab_tree_pairing->vocab_tree_path = GetVocabTreeUriForFeatureType(option_manager_.feature_extraction->type); option_manager_.sequential_pairing->vocab_tree_path = GetVocabTreeUriForFeatureType(option_manager_.feature_extraction->type); option_manager_.sequential_pairing->loop_detection = true; // Apply mapper-appropriate two-view geometry defaults. // Global uses stricter thresholds; Incremental/Hierarchical use standard. TwoViewGeometryOptions& two_view_geometry_options = *option_manager_.two_view_geometry; two_view_geometry_options.ransac_options.random_seed = options_.random_seed; if (options_.mapper == Mapper::GLOBAL) { two_view_geometry_options.ransac_options.max_error = 1.0; two_view_geometry_options.min_num_inliers = 30; two_view_geometry_options.min_inlier_ratio = 0.25; // Disable guided matching for global mapper to avoid regression issues. // Currently the guided matching leads to significantly worse results of the // global pipeline. // TODO: Write to database matches instead of inlier matches in guided // matching and figure out a good min_num_inliers and min_inlier_ratio // threshold for it. option_manager_.feature_matching->guided_matching = false; } option_manager_.mapper->random_seed = options_.random_seed; if (!options_.mask_path.empty()) { option_manager_.stereo_fusion->mask_path = options_.mask_path; } option_manager_.feature_extraction->use_gpu = options_.use_gpu; option_manager_.feature_matching->use_gpu = options_.use_gpu; option_manager_.mapper->ba_use_gpu = options_.use_gpu; if (option_manager_.bundle_adjustment->ceres) { option_manager_.bundle_adjustment->ceres->use_gpu = options_.use_gpu; } option_manager_.feature_extraction->gpu_index = options_.gpu_index; option_manager_.feature_matching->gpu_index = options_.gpu_index; option_manager_.patch_match_stereo->gpu_index = options_.gpu_index; option_manager_.mapper->ba_gpu_index = options_.gpu_index; if (option_manager_.bundle_adjustment->ceres) { option_manager_.bundle_adjustment->ceres->gpu_index = options_.gpu_index; } } bool AutomaticReconstructionController::RequiresOpenGL() const { return (options_.extraction && option_manager_.feature_extraction->RequiresOpenGL()) || (options_.matching && option_manager_.feature_matching->RequiresOpenGL()); } void AutomaticReconstructionController::Setup() { if (options_.extraction) { ImageReaderOptions& reader_options = *option_manager_.image_reader; reader_options.mask_path = options_.mask_path; reader_options.single_camera = options_.single_camera; reader_options.single_camera_per_folder = options_.single_camera_per_folder; reader_options.camera_model = options_.camera_model; reader_options.camera_params = options_.camera_params; reader_options.image_path = *option_manager_.image_path; reader_options.as_rgb = option_manager_.feature_extraction->RequiresRGB(); feature_extractor_ = CreateFeatureExtractorController(*option_manager_.database_path, reader_options, *option_manager_.feature_extraction); } if (options_.matching) { exhaustive_matcher_ = CreateExhaustiveFeatureMatcher(*option_manager_.exhaustive_pairing, *option_manager_.feature_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); sequential_matcher_ = CreateSequentialFeatureMatcher(*option_manager_.sequential_pairing, *option_manager_.feature_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); if (!options_.vocab_tree_path.empty()) { vocab_tree_matcher_ = CreateVocabTreeFeatureMatcher(*option_manager_.vocab_tree_pairing, *option_manager_.feature_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); } } } void AutomaticReconstructionController::Stop() { if (active_thread_ != nullptr) { active_thread_->Stop(); } Thread::Stop(); } void AutomaticReconstructionController::Run() { if (IsStopped()) { return; } if (options_.extraction) { RunFeatureExtraction(); } if (IsStopped()) { return; } if (options_.matching) { RunFeatureMatching(); } if (IsStopped()) { return; } if (options_.sparse) { RunSparseMapper(); } if (IsStopped()) { return; } if (options_.dense) { RunDenseMapper(); } } void AutomaticReconstructionController::RunFeatureExtraction() { LOG_HEADING1("Feature extraction"); THROW_CHECK_NOTNULL(feature_extractor_); active_thread_ = feature_extractor_.get(); feature_extractor_->Start(); feature_extractor_->Wait(); feature_extractor_.reset(); active_thread_ = nullptr; } void AutomaticReconstructionController::RunFeatureMatching() { LOG_HEADING1("Feature matching"); Thread* matcher = nullptr; if (options_.data_type == DataType::VIDEO) { matcher = sequential_matcher_.get(); } else if (options_.data_type == DataType::INDIVIDUAL || options_.data_type == DataType::INTERNET) { auto database = Database::Open(*option_manager_.database_path); const size_t num_images = database->NumImages(); if (options_.vocab_tree_path.empty() || num_images < 200) { matcher = exhaustive_matcher_.get(); } else { matcher = vocab_tree_matcher_.get(); } } THROW_CHECK_NOTNULL(matcher); active_thread_ = matcher; matcher->Start(); matcher->Wait(); exhaustive_matcher_.reset(); sequential_matcher_.reset(); vocab_tree_matcher_.reset(); active_thread_ = nullptr; } void AutomaticReconstructionController::RunSparseMapper() { LOG_HEADING1("Sparse reconstruction"); const auto sparse_path = options_.workspace_path / "sparse"; if (ExistsDir(sparse_path)) { auto dir_list = GetDirList(sparse_path); std::sort(dir_list.begin(), dir_list.end()); if (dir_list.size() > 0) { LOG(INFO) << "Skipping sparse reconstruction because it is already computed"; for (const auto& dir : dir_list) { reconstruction_manager_->Read(dir); } return; } } std::unique_ptr mapper; auto database = Database::Open(*option_manager_.database_path); switch (options_.mapper) { case Mapper::INCREMENTAL: { auto options = std::make_shared(*option_manager_.mapper); options->image_path = *option_manager_.image_path; mapper = std::make_unique( options, std::move(database), reconstruction_manager_); break; } case Mapper::HIERARCHICAL: { HierarchicalPipeline::Options options; options.image_path = *option_manager_.image_path; options.incremental_options = *option_manager_.mapper; mapper = std::make_unique( options, std::move(database), reconstruction_manager_); break; } case Mapper::GLOBAL: { ViewGraphCalibrationOptions vgc_options; vgc_options.random_seed = options_.random_seed; vgc_options.solver_options.num_threads = options_.num_threads; CalibrateViewGraph(vgc_options, database.get()); GlobalPipelineOptions global_options; global_options.image_path = *option_manager_.image_path; global_options.num_threads = options_.num_threads; global_options.random_seed = options_.random_seed; mapper = std::make_unique(std::move(global_options), std::move(database), reconstruction_manager_); break; } default: LOG(FATAL_THROW) << "Mapper not supported"; } mapper->SetCheckIfStoppedFunc([&]() { return IsStopped(); }); mapper->Run(); CreateDirIfNotExists(sparse_path); reconstruction_manager_->Write(sparse_path); option_manager_.Write(sparse_path / "project.ini"); } void AutomaticReconstructionController::RunDenseMapper() { LOG_HEADING1("Dense reconstruction"); CreateDirIfNotExists(options_.workspace_path / "dense"); for (size_t i = 0; i < reconstruction_manager_->Size(); ++i) { if (IsStopped()) { return; } const auto dense_path = options_.workspace_path / "dense" / std::to_string(i); const auto fused_path = dense_path / "fused.ply"; std::filesystem::path meshing_path; if (options_.mesher == Mesher::POISSON) { meshing_path = dense_path / "meshed-poisson.ply"; } else if (options_.mesher == Mesher::DELAUNAY) { meshing_path = dense_path / "meshed-delaunay.ply"; } if (ExistsFile(fused_path) && ExistsFile(meshing_path)) { LOG(INFO) << "Skipping dense reconstruction for model " << i << " as it already exists."; continue; } // Image undistortion. if (!ExistsDir(dense_path)) { CreateDirIfNotExists(dense_path); UndistortCameraOptions undistortion_options; undistortion_options.max_image_size = option_manager_.patch_match_stereo->max_image_size; COLMAPUndistorter::Options undistorter_options; undistorter_options.num_threads = options_.num_threads; COLMAPUndistorter undistorter(std::move(undistorter_options), undistortion_options, *reconstruction_manager_->Get(i), *option_manager_.image_path, dense_path); undistorter.SetCheckIfStoppedFunc([&]() { return IsStopped(); }); undistorter.Run(); } if (IsStopped()) { return; } // Patch match stereo. #if defined(COLMAP_CUDA_ENABLED) { mvs::PatchMatchController patch_match_controller( *option_manager_.patch_match_stereo, dense_path, "COLMAP", ""); patch_match_controller.SetCheckIfStoppedFunc( [&]() { return IsStopped(); }); patch_match_controller.Run(); } #else // COLMAP_CUDA_ENABLED LOG(WARNING) << "Skipping patch match stereo because CUDA is not available"; return; #endif // COLMAP_CUDA_ENABLED if (IsStopped()) { return; } // Stereo fusion. if (!ExistsFile(fused_path)) { auto fusion_options = *option_manager_.stereo_fusion; const int num_reg_images = reconstruction_manager_->Get(i)->NumRegImages(); fusion_options.min_num_pixels = std::min(num_reg_images + 1, fusion_options.min_num_pixels); mvs::StereoFusion fuser( fusion_options, dense_path, "COLMAP", "", option_manager_.patch_match_stereo->geom_consistency ? "geometric" : "photometric"); fuser.SetCheckIfStoppedFunc([&]() { return IsStopped(); }); fuser.Run(); LOG(INFO) << "Writing output: " << fused_path; WriteBinaryPlyPoints(fused_path, fuser.GetFusedPoints()); mvs::WritePointsVisibility(AddFileExtension(fused_path, ".vis"), fuser.GetFusedPointsVisibility()); } if (IsStopped()) { return; } // Surface meshing. if (!ExistsFile(meshing_path)) { if (options_.mesher == Mesher::POISSON) { mvs::PoissonMeshing( *option_manager_.poisson_meshing, fused_path, meshing_path); } else if (options_.mesher == Mesher::DELAUNAY) { #if defined(COLMAP_CGAL_ENABLED) mvs::DenseDelaunayMeshing( *option_manager_.delaunay_meshing, dense_path, meshing_path); #else // COLMAP_CGAL_ENABLED LOG(WARNING) << "Skipping Delaunay meshing because CGAL is not available"; return; #endif // COLMAP_CGAL_ENABLED } } } } } // namespace colmap colmap-4.0.4/src/colmap/controllers/automatic_reconstruction.h000066400000000000000000000125621517363634500247250ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/util/enum_utils.h" #include "colmap/util/threading.h" #include #include #include namespace colmap { class AutomaticReconstructionController : public Thread { public: MAKE_ENUM_CLASS(DataType, 0, INDIVIDUAL, VIDEO, INTERNET); MAKE_ENUM_CLASS(Quality, 0, LOW, MEDIUM, HIGH, EXTREME); MAKE_ENUM_CLASS(Feature, 0, SIFT, ALIKED); MAKE_ENUM_CLASS(Mapper, 0, INCREMENTAL, HIERARCHICAL, GLOBAL); MAKE_ENUM_CLASS(Mesher, 0, POISSON, DELAUNAY); struct Options { // The path to the workspace folder in which all results are stored. std::filesystem::path workspace_path; // The path to the image folder which are used as input. std::filesystem::path image_path; // Optional list of image names to reconstruct. The list must contain the // relative path of the images with respect to the image_path. std::vector image_names; // The path to the mask folder which are used as input. std::filesystem::path mask_path; // The path to the vocabulary tree for feature matching. std::filesystem::path vocab_tree_path; // The type of input data used to choose optimal mapper settings. DataType data_type = DataType::INDIVIDUAL; // Whether to perform low- or high-quality reconstruction. Quality quality = Quality::HIGH; // Whether to use shared intrinsics or not. bool single_camera = false; // Whether to use shared intrinsics or not for all images in the same // sub-folder. bool single_camera_per_folder = false; // Which camera model to use for images. std::string camera_model = "SIMPLE_RADIAL"; // Initial camera params for all images. std::string camera_params; // Whether to perform feature extraction. bool extraction = true; // Whether to perform feature matching. bool matching = true; // Whether to perform sparse mapping. bool sparse = true; // Whether to perform dense mapping. #if defined(COLMAP_CUDA_ENABLED) bool dense = true; #else bool dense = false; #endif // The feature extraction/matching algorithm to be used. Feature feature = Feature::SIFT; // The mapping algorithm to be used. Mapper mapper = Mapper::INCREMENTAL; // The meshing algorithm to be used. Mesher mesher = Mesher::POISSON; // The number of threads to use in all stages. int num_threads = -1; // The random seed to use in all stages. int random_seed = -1; // Whether to use the GPU in feature extraction, feature matching, and // bundle adjustment. bool use_gpu = true; // Index of the GPU used for GPU stages. For multi-GPU computation in // feature extraction/matching, you should separate multiple GPU indices by // comma, e.g., "0,1,2,3". For single-GPU stages only the first GPU will be // used. By default, all available GPUs will be used in all stages. std::string gpu_index = "-1"; }; AutomaticReconstructionController( const Options& options, std::shared_ptr reconstruction_manager); // Whether any of the selected reconstruction stages requires OpenGL. bool RequiresOpenGL() const; void Setup(); void Stop() override; private: void Run() override; void RunFeatureExtraction(); void RunFeatureMatching(); void RunSparseMapper(); void RunDenseMapper(); const Options options_; OptionManager option_manager_; std::shared_ptr reconstruction_manager_; Thread* active_thread_; std::unique_ptr feature_extractor_; std::unique_ptr exhaustive_matcher_; std::unique_ptr sequential_matcher_; std::unique_ptr vocab_tree_matcher_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/automatic_reconstruction_test.cc000066400000000000000000000106501517363634500261160ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/automatic_reconstruction.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include "colmap/util/file.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { class ParameterizedAutomaticReconstructionTests : public ::testing::TestWithParam< AutomaticReconstructionController::Mapper> {}; TEST_P(ParameterizedAutomaticReconstructionTests, Nominal) { SetPRNGSeed(1); const auto test_dir = CreateTestDir(); const auto workspace_path = test_dir / "workspace"; const auto image_path = test_dir / "images"; CreateDirIfNotExists(workspace_path); CreateDirIfNotExists(image_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 5; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 200; synthetic_dataset_options.num_points2D_without_point3D = 10; SynthesizeDataset(synthetic_dataset_options, >_reconstruction); SynthesizeImages(SyntheticImageOptions(), gt_reconstruction, image_path); AutomaticReconstructionController::Options options; options.workspace_path = workspace_path; options.image_path = image_path; options.data_type = AutomaticReconstructionController::DataType::INDIVIDUAL; options.quality = AutomaticReconstructionController::Quality::LOW; options.single_camera = false; options.dense = false; // Disable dense reconstruction to avoid GPU options.use_gpu = false; options.random_seed = 1; options.mapper = GetParam(); auto reconstruction_manager = std::make_shared(); AutomaticReconstructionController controller(options, reconstruction_manager); controller.Setup(); controller.Start(); controller.Wait(); EXPECT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(*reconstruction_manager->Get(0), ReconstructionNear(gt_reconstruction, /*max_rotation_error_deg=*/0.5, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.9, /*align=*/true)); } // TODO: Add GLOBAL mapper test. Currently excluded because the test produces // fewer observations than expected. The global pipeline is tested separately // in global_pipeline_test.cc. INSTANTIATE_TEST_SUITE_P( AutomaticReconstructionTests, ParameterizedAutomaticReconstructionTests, ::testing::Values(AutomaticReconstructionController::Mapper::INCREMENTAL, AutomaticReconstructionController::Mapper::HIERARCHICAL)); } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/base_option_manager.cc000066400000000000000000000261201517363634500237230ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/base_option_manager.h" #include "colmap/math/random.h" #include "colmap/util/file.h" #include "colmap/util/string.h" #include #include namespace config = boost::program_options; namespace colmap { BaseOptionManager::BaseOptionManager(bool add_project_options) { project_path = std::make_shared(); database_path = std::make_shared(); image_path = std::make_shared(); ResetImpl(/*reset_logging=*/true); desc_->add_options()("help,h", ""); if (add_project_options) { desc_->add_options()("project_path", config::value()); } AddRandomOptions(); AddLogOptions(); } void BaseOptionManager::AddRandomOptions() { if (added_random_options_) { return; } added_random_options_ = true; AddDefaultOption("default_random_seed", &kDefaultPRNGSeed); } void BaseOptionManager::AddLogOptions() { if (added_log_options_) { return; } added_log_options_ = true; AddDefaultOption( "log_target", &log_target_, "{stderr, stdout, file, stderr_and_file}"); // Directory for log files. If empty, glog uses $GOOGLE_LOG_DIR, /tmp, or // %TEMP%. AddDefaultOption("log_path", &FLAGS_log_dir); AddDefaultOption("log_level", &FLAGS_v); AddDefaultOption("log_severity", &FLAGS_minloglevel, "0:INFO, 1:WARNING, 2:ERROR, 3:FATAL"); AddDefaultOption("log_color", &FLAGS_colorlogtostderr); } void BaseOptionManager::AddDatabaseOptions() { if (added_database_options_) { return; } added_database_options_ = true; AddRequiredOption("database_path", database_path.get()); } void BaseOptionManager::AddImageOptions() { if (added_image_options_) { return; } added_image_options_ = true; AddRequiredOption("image_path", image_path.get()); } void BaseOptionManager::Reset(bool reset_logging) { ResetImpl(reset_logging); } void BaseOptionManager::ResetOptions(const bool reset_paths) { auto saved_project_path = std::move(*project_path); auto saved_database_path = std::move(*database_path); auto saved_image_path = std::move(*image_path); // Re-register all options to update raw pointers, since subclass // ResetOptions() may reallocate internal sub-objects. Reset(/*reset_logging=*/false); AddAllOptions(); if (!reset_paths) { *project_path = std::move(saved_project_path); *database_path = std::move(saved_database_path); *image_path = std::move(saved_image_path); } } void BaseOptionManager::ResetImpl(bool reset_logging) { if (reset_logging) { log_target_ = "stderr_and_file"; FLAGS_log_dir = ""; FLAGS_v = 0; FLAGS_minloglevel = 0; FLAGS_colorlogtostderr = true; ApplyLogFlags(); } const bool kResetPaths = true; ResetOptionsImpl(kResetPaths); desc_ = std::make_shared(); options_bool_.clear(); options_int_.clear(); options_double_.clear(); options_string_.clear(); options_path_.clear(); added_random_options_ = false; added_log_options_ = false; added_database_options_ = false; added_image_options_ = false; } void BaseOptionManager::ResetOptionsImpl(const bool reset_paths) { if (reset_paths) { *project_path = ""; *database_path = ""; *image_path = ""; } } bool BaseOptionManager::Check() { bool success = true; if (added_database_options_) { const auto database_parent_path = GetParentDir(*database_path); success = success && CHECK_OPTION_IMPL(!ExistsDir(*database_path)) && CHECK_OPTION_IMPL(database_parent_path.empty() || ExistsDir(database_parent_path)); } if (added_image_options_) { success = success && CHECK_OPTION_IMPL(ExistsDir(*image_path)); } return success; } void BaseOptionManager::PostParse() { // Default implementation does nothing. Subclasses can override. } void BaseOptionManager::ApplyEnumConversions() { for (const auto& info : enum_options_) { info->apply(); } } void BaseOptionManager::ApplyLogFlags() { FLAGS_logtostderr = false; #if defined(GLOG_VERSION_MAJOR) && \ (GLOG_VERSION_MAJOR > 0 || GLOG_VERSION_MINOR >= 6) FLAGS_logtostdout = false; #endif FLAGS_alsologtostderr = false; if (log_target_ == "stderr") { FLAGS_logtostderr = true; } else if (log_target_ == "stdout") { #if defined(GLOG_VERSION_MAJOR) && \ (GLOG_VERSION_MAJOR > 0 || GLOG_VERSION_MINOR >= 6) FLAGS_logtostdout = true; #else LOG(WARNING) << "log_target=stdout requires glog >= 0.6. " "Falling back to stderr."; FLAGS_logtostderr = true; #endif } else if (log_target_ == "file") { } else if (log_target_ == "stderr_and_file") { FLAGS_alsologtostderr = true; } else { LOG(ERROR) << "Invalid log_target: " << log_target_ << ". Falling back to stderr_and_file."; FLAGS_alsologtostderr = true; } #if defined(GLOG_VERSION_MAJOR) && \ (GLOG_VERSION_MAJOR > 0 || GLOG_VERSION_MINOR >= 6) FLAGS_colorlogtostdout = FLAGS_colorlogtostderr; #endif if (!FLAGS_log_dir.empty() && (log_target_ == "file" || log_target_ == "stderr_and_file")) { CreateDirIfNotExists(FLAGS_log_dir); } } void BaseOptionManager::PrintHelp() const { LOG(INFO) << "Options can either be specified via command-line or by " "defining them in a .ini project file.\n" << *desc_; } void BaseOptionManager::AddAllOptions() { AddRandomOptions(); AddLogOptions(); AddDatabaseOptions(); AddImageOptions(); } bool BaseOptionManager::Parse(const int argc, char** argv) { config::variables_map vmap; try { config::store(config::parse_command_line(argc, argv, *desc_), vmap); if (vmap.count("help")) { PrintHelp(); // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_SUCCESS); } if (vmap.count("project_path")) { *project_path = vmap["project_path"].as(); if (!Read(*project_path)) { return false; } } else { vmap.notify(); } ApplyEnumConversions(); ApplyLogFlags(); PostParse(); } catch (std::exception& exc) { LOG(ERROR) << "Failed to parse options - " << exc.what() << "."; return false; } catch (...) { LOG(ERROR) << "Failed to parse options for unknown reason."; return false; } if (!Check()) { LOG(ERROR) << "Invalid options provided."; return false; } return true; } bool BaseOptionManager::Read(const std::filesystem::path& path, bool allow_unregistered) { config::variables_map vmap; if (!ExistsFile(path)) { LOG(ERROR) << "Configuration file does not exist."; return false; } try { std::ifstream file(path); THROW_CHECK_FILE_OPEN(file, path); const config::parsed_options parsed_options = config::parse_config_file(file, *desc_, allow_unregistered); config::store(parsed_options, vmap); if (allow_unregistered) { for (const auto& option : parsed_options.options) { if (option.unregistered) { LOG(WARNING) << "Unrecognized option key: " << option.string_key; } } } vmap.notify(); ApplyEnumConversions(); } catch (std::exception& e) { LOG(ERROR) << "Failed to parse options " << e.what() << "."; return false; } catch (...) { LOG(ERROR) << "Failed to parse options for unknown reason."; return false; } return true; } bool BaseOptionManager::ReRead(const std::filesystem::path& path, bool reset_logging, bool allow_unregistered) { Reset(reset_logging); AddAllOptions(); return Read(path, allow_unregistered); } void BaseOptionManager::Write(const std::filesystem::path& path) const { boost::property_tree::ptree pt; // First, put all options without a section and then those with a section. // This is necessary as otherwise older Boost versions will write the // options without a section in between other sections and therefore // the errors will be assigned to the wrong section if read later. for (const auto& [key, value] : options_bool_) { if (!StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_int_) { if (!StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_double_) { if (!StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_string_) { if (!StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_path_) { if (!StringContains(key, ".")) { pt.put(key, value->string()); } } for (const auto& [key, value] : options_bool_) { if (StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_int_) { if (StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_double_) { if (StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_string_) { if (StringContains(key, ".")) { pt.put(key, *value); } } for (const auto& [key, value] : options_path_) { if (StringContains(key, ".")) { pt.put(key, value->string()); } } std::ofstream file(path); THROW_CHECK_FILE_OPEN(file, path); // Ensure that we don't lose any precision by storing in text. file.precision(17); boost::property_tree::write_ini(file, pt); file.close(); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/base_option_manager.h000066400000000000000000000236551517363634500235770ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/util/logging.h" #include "colmap/util/types.h" #include #include #include #include #include #include #include namespace colmap { // Base class for option managers providing core infrastructure for // command-line parsing, configuration file I/O, and option registration. class BaseOptionManager { public: NON_COPYABLE(BaseOptionManager) explicit BaseOptionManager(bool add_project_options = true); BaseOptionManager(BaseOptionManager&&) = default; BaseOptionManager& operator=(BaseOptionManager&&) = default; virtual ~BaseOptionManager() = default; void AddRandomOptions(); void AddLogOptions(); void AddDatabaseOptions(); void AddImageOptions(); template void AddRequiredOption(const std::string& name, T* option, const std::string& help_text = ""); template void AddDefaultOption(const std::string& name, T* option, const std::string& help_text = ""); // Register an enum option with automatic string-to-enum conversion. // Uses the ToString/FromString functions generated by MAKE_ENUM_CLASS. // The conversion is applied automatically after parsing. // // Example: // MAKE_ENUM_CLASS(MyEnum, 0, VALUE_A, VALUE_B); // AddDefaultEnumOption("my_option", // &options.my_enum, // MyEnumToString, // MyEnumFromString); template void AddDefaultEnumOption(const std::string& name, EnumT* option, std::string_view (*to_string_fn)(EnumT), EnumT (*from_string_fn)(std::string_view), const std::string& help_text = ""); // Reset all internal state. If reset_logging is true, restore glog defaults. // Higher-level applications may override the logging configuration. virtual void Reset(bool reset_logging = true); virtual void ResetOptions(bool reset_paths); virtual bool Check(); bool Parse(int argc, char** argv); virtual bool Read(const std::filesystem::path& path, bool allow_unregistered = true); bool ReRead(const std::filesystem::path& path, bool reset_logging = true, bool allow_unregistered = true); void Write(const std::filesystem::path& path) const; std::shared_ptr project_path; std::shared_ptr database_path; std::shared_ptr image_path; protected: template void RegisterOption(const std::string& name, const T* option); // Hook for subclasses to perform post-parse processing. // Called after successful parsing but before Check(). virtual void PostParse(); // Hook for subclasses to print custom help message. virtual void PrintHelp() const; // Hook for subclasses to add all their options. Called by ReRead(). // Base implementation adds common options (random, log, database, image). // Subclasses should call BaseOptionManager::AddAllOptions() first. virtual void AddAllOptions(); std::shared_ptr desc_; // Log destination choice: {stderr, stdout, file, stderr_and_file}. std::string log_target_ = "stderr_and_file"; std::vector> options_bool_; std::vector> options_int_; std::vector> options_double_; std::vector> options_string_; std::vector> options_path_; // Storage for enum options: string value and conversion callback. // Uses unique_ptr for pointer stability when the vector grows. struct EnumOptionInfo { std::string value; // String value for parsing std::function apply; // Callback to apply string->enum conversion }; std::vector> enum_options_; bool added_random_options_ = false; bool added_log_options_ = false; bool added_database_options_ = false; bool added_image_options_ = false; private: // Non-virtual implementations called from constructor and virtual methods. // These avoid the clang-tidy warning about virtual calls during construction. void ResetImpl(bool reset_logging); void ResetOptionsImpl(bool reset_paths); // Apply string->enum conversions for all registered enum options. void ApplyEnumConversions(); // Map simplified log output options to glog flags. void ApplyLogFlags(); }; template void BaseOptionManager::AddRequiredOption(const std::string& name, T* option, const std::string& help_text) { if constexpr (std::is_same::value) { // Boost program options does not support std::filesystem::path by default. // We treat it as a string and manualy convert it using a notifier. desc_->add_options()( name.c_str(), boost::program_options::value()->required()->notifier( [option](const std::string& val) { *option = val; }), help_text.c_str()); } else { desc_->add_options()(name.c_str(), boost::program_options::value(option)->required(), help_text.c_str()); } RegisterOption(name, option); } template void BaseOptionManager::AddDefaultOption(const std::string& name, T* option, const std::string& help_text) { if constexpr (std::is_floating_point::value) { desc_->add_options()( name.c_str(), boost::program_options::value(option)->default_value( *option, StringPrintf("%.3g", *option)), help_text.c_str()); } else if constexpr (std::is_same::value) { // Boost program options does not support std::filesystem::path by default. // We treat it as a string and manualy convert it using a notifier. desc_->add_options()( name.c_str(), boost::program_options::value() ->default_value(option->string()) ->notifier([option](const std::string& val) { *option = val; }), help_text.c_str()); } else { desc_->add_options()( name.c_str(), boost::program_options::value(option)->default_value(*option), help_text.c_str()); } RegisterOption(name, option); } template void BaseOptionManager::RegisterOption(const std::string& name, const T* option) { if constexpr (std::is_same::value) { options_bool_.emplace_back(name, reinterpret_cast(option)); } else if constexpr (std::is_same::value) { options_int_.emplace_back(name, reinterpret_cast(option)); } else if constexpr (std::is_same::value) { options_double_.emplace_back(name, reinterpret_cast(option)); } else if constexpr (std::is_same::value) { options_string_.emplace_back(name, reinterpret_cast(option)); } else if constexpr (std::is_same::value) { options_path_.emplace_back( name, reinterpret_cast(option)); } else { static_assert(always_false::value, "Unsupported option type"); } } template void BaseOptionManager::AddDefaultEnumOption( const std::string& name, EnumT* option, std::string_view (*to_string_fn)(EnumT), EnumT (*from_string_fn)(std::string_view), const std::string& help_text) { // Create storage for this enum option (unique_ptr for pointer stability) auto info = std::make_unique(); info->value = std::string(to_string_fn(*option)); EnumOptionInfo* info_ptr = info.get(); info->apply = [info_ptr, option, from_string_fn]() { *option = from_string_fn(info_ptr->value); }; // Register as a string option pointing to our storage AddDefaultOption(name, &info->value, help_text); enum_options_.push_back(std::move(info)); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/base_option_manager_test.cc000066400000000000000000000423051517363634500247650ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/base_option_manager.h" #include "colmap/util/enum_utils.h" #include "colmap/util/file.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { // Test enum for AddDefaultEnumOption tests MAKE_ENUM_CLASS(TestEnumType, 0, VALUE_A, VALUE_B, VALUE_C); TEST(BaseOptionManager, Reset) { BaseOptionManager options; *options.database_path = "/test/path"; *options.image_path = "/test/images"; options.AddDatabaseOptions(); options.AddImageOptions(); EXPECT_EQ(*options.database_path, "/test/path"); EXPECT_EQ(*options.image_path, "/test/images"); options.Reset(); EXPECT_TRUE(options.database_path->empty()); EXPECT_TRUE(options.image_path->empty()); } TEST(BaseOptionManager, ResetOptions) { BaseOptionManager options; *options.database_path = "/test/path"; *options.image_path = "/test/images"; options.ResetOptions(/*reset_paths=*/true); EXPECT_TRUE(options.database_path->empty()); EXPECT_TRUE(options.image_path->empty()); *options.database_path = "/test/path"; *options.image_path = "/test/images"; options.ResetOptions(/*reset_paths=*/false); EXPECT_EQ(*options.database_path, "/test/path"); EXPECT_EQ(*options.image_path, "/test/images"); } TEST(BaseOptionManager, AddOptionsIdempotent) { BaseOptionManager options; // Adding options multiple times should not cause issues options.AddLogOptions(); options.AddLogOptions(); options.AddRandomOptions(); options.AddRandomOptions(); options.AddDatabaseOptions(); options.AddDatabaseOptions(); options.AddImageOptions(); options.AddImageOptions(); // If idempotency is not maintained, the above would cause errors SUCCEED(); } TEST(BaseOptionManager, WriteAndRead) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; // Create necessary directories CreateDirIfNotExists(test_dir / "images"); bool bool_option_write = true; int int_option_write = 42; double double_option_write = 3.14; std::string string_option_write = "foobar"; std::string section_option_write = "section"; TestEnumType enum_option_write = TestEnumType::VALUE_B; // Create and configure a BaseOptionManager BaseOptionManager options_write; options_write.AddDatabaseOptions(); options_write.AddImageOptions(); options_write.AddDefaultOption("bool_option", &bool_option_write); options_write.AddDefaultOption("int_option", &int_option_write); options_write.AddDefaultOption("double_option", &double_option_write); options_write.AddDefaultOption("string_option", &string_option_write); options_write.AddDefaultOption("Section.option", §ion_option_write); options_write.AddDefaultEnumOption("enum_option", &enum_option_write, TestEnumTypeToString, TestEnumTypeFromString); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; // Write to file options_write.Write(config_path); EXPECT_TRUE(ExistsFile(config_path)); bool bool_option_read = false; int int_option_read = -1; double double_option_read = 0; std::string string_option_read; std::string section_option_read; TestEnumType enum_option_read = TestEnumType::VALUE_A; // Read from file BaseOptionManager options_read; options_read.AddDatabaseOptions(); options_read.AddImageOptions(); options_read.AddDefaultOption("bool_option", &bool_option_read); options_read.AddDefaultOption("int_option", &int_option_read); options_read.AddDefaultOption("double_option", &double_option_read); options_read.AddDefaultOption("string_option", &string_option_read); options_read.AddDefaultOption("Section.option", §ion_option_read); options_read.AddDefaultEnumOption("enum_option", &enum_option_read, TestEnumTypeToString, TestEnumTypeFromString); EXPECT_TRUE(options_read.Read(config_path)); // Verify that values were read correctly EXPECT_EQ(*options_read.database_path, *options_write.database_path); EXPECT_EQ(*options_read.image_path, *options_write.image_path); EXPECT_EQ(bool_option_read, bool_option_write); EXPECT_EQ(int_option_read, int_option_write); EXPECT_EQ(double_option_read, double_option_write); EXPECT_EQ(string_option_read, string_option_write); EXPECT_EQ(section_option_read, section_option_write); EXPECT_EQ(enum_option_read, enum_option_write); } TEST(BaseOptionManager, ReadWithUnregisteredOptions) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; CreateDirIfNotExists(test_dir / "images"); std::ofstream file(config_path); file << "database_path=" << (test_dir / "database.db").string() << "\n"; file << "image_path=" << (test_dir / "images").string() << "\n"; file << "unknown_option=foobar\n"; file.close(); BaseOptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); EXPECT_TRUE(options.Read(config_path, /*allow_unregistered=*/true)); EXPECT_FALSE(options.Read(config_path, /*allow_unregistered=*/false)); EXPECT_EQ(*options.database_path, test_dir / "database.db"); EXPECT_EQ(*options.image_path, test_dir / "images"); } TEST(BaseOptionManager, ReRead) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; // Create necessary directories CreateDirIfNotExists(test_dir / "images"); // Create and write initial config BaseOptionManager options_write; options_write.AddDatabaseOptions(); options_write.AddImageOptions(); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; options_write.Write(config_path); // Read with ReRead BaseOptionManager options_read; EXPECT_TRUE(options_read.ReRead(config_path)); // Verify values EXPECT_EQ(*options_read.database_path, *options_write.database_path); EXPECT_EQ(*options_read.image_path, *options_write.image_path); } TEST(BaseOptionManager, ReadNonExistentFile) { BaseOptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); EXPECT_FALSE(options.Read("/path/that/does/not/exist.ini")); } TEST(BaseOptionManager, Check) { const auto test_dir = CreateTestDir(); BaseOptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); // Should fail with non-existent paths *options.database_path = test_dir / "database.db"; *options.image_path = "/path/that/does/not/exist"; EXPECT_FALSE(options.Check()); // Should succeed with valid paths CreateDirIfNotExists(test_dir / "images"); *options.image_path = test_dir / "images"; EXPECT_TRUE(options.Check()); } TEST(BaseOptionManager, CheckDatabaseParentDir) { const auto test_dir = CreateTestDir(); BaseOptionManager options; options.AddDatabaseOptions(); // Should succeed when database parent dir exists *options.database_path = test_dir / "database.db"; EXPECT_TRUE(options.Check()); // Should fail when database path is a directory CreateDirIfNotExists(test_dir / "bad_database"); *options.database_path = test_dir / "bad_database"; EXPECT_FALSE(options.Check()); } TEST(BaseOptionManager, ParseWithOptions) { const auto test_dir = CreateTestDir(); CreateDirIfNotExists(test_dir / "images"); BaseOptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); const auto database_path = test_dir / "database.db"; const auto image_path = test_dir / "images"; // Create argv with additional options const std::vector args = { "colmap", "--database_path", database_path.string(), "--image_path", image_path.string(), }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Verify parsed values EXPECT_EQ(*options.database_path, database_path); EXPECT_EQ(*options.image_path, image_path); } TEST(BaseOptionManager, ParseWithProjectPath) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; CreateDirIfNotExists(test_dir / "images"); // Create and write a config file BaseOptionManager options_write; options_write.AddDatabaseOptions(); options_write.AddImageOptions(); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; options_write.Write(config_path); // Parse using project_path BaseOptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); const std::vector args = { "colmap", "--project_path", config_path.string(), }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Verify values were loaded from config file EXPECT_EQ(*options.database_path, *options_write.database_path); EXPECT_EQ(*options.image_path, *options_write.image_path); } TEST(BaseOptionManager, ParseEmptyArguments) { BaseOptionManager options; const std::vector args = {"colmap"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } // Should succeed with no required options EXPECT_TRUE(options.Parse(argv.size(), argv.data())); } TEST(BaseOptionManager, ParseUnknownArgumentsFails) { const auto test_dir = CreateTestDir(); BaseOptionManager options; options.AddDatabaseOptions(); const auto database_path = test_dir / "database.db"; // Create argv with an unknown option const std::vector args = { "colmap", "--database_path", database_path.string(), "--unknown_option", "value", }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } // Should return false when encountering unknown option EXPECT_FALSE(options.Parse(argv.size(), argv.data())); } // Helper class to test enum options through BaseOptionManager class TestEnumOptionManager : public BaseOptionManager { public: TestEnumOptionManager() : BaseOptionManager(/*add_project_options=*/false) { AddDefaultEnumOption("test_enum", &test_enum_value, TestEnumTypeToString, TestEnumTypeFromString); } TestEnumType test_enum_value = TestEnumType::VALUE_A; }; TEST(BaseOptionManager, EnumOptionDefaultValue) { TestEnumOptionManager options; // Default value should be VALUE_A EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_A); // Parse with no enum option specified const std::vector args = {"test"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Should still be default value EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_A); } TEST(BaseOptionManager, EnumOptionParseFromCommandLine) { TestEnumOptionManager options; // Parse with enum option set to VALUE_B const std::vector args = {"test", "--test_enum", "VALUE_B"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Should be VALUE_B after parsing EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_B); } TEST(BaseOptionManager, EnumOptionParseFromCommandLineValueC) { TestEnumOptionManager options; // Parse with enum option set to VALUE_C const std::vector args = {"test", "--test_enum", "VALUE_C"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Should be VALUE_C after parsing EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_C); } TEST(BaseOptionManager, EnumOptionInvalidValue) { TestEnumOptionManager options; // Parse with invalid enum value const std::vector args = { "test", "--test_enum", "INVALID_VALUE"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } // Should fail due to invalid enum value EXPECT_FALSE(options.Parse(argv.size(), argv.data())); } // Helper class to test enum options with non-default initial value class TestEnumOptionManagerWithValueB : public BaseOptionManager { public: TestEnumOptionManagerWithValueB() : BaseOptionManager(/*add_project_options=*/false) { AddDefaultEnumOption("test_enum", &test_enum_value, TestEnumTypeToString, TestEnumTypeFromString); } TestEnumType test_enum_value = TestEnumType::VALUE_B; }; TEST(BaseOptionManager, EnumOptionNonDefaultInitialValue) { TestEnumOptionManagerWithValueB options; // Default value should be VALUE_B (non-default) EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_B); // Parse with no enum option specified const std::vector args = {"test"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Should still be VALUE_B (the initial value) EXPECT_EQ(options.test_enum_value, TestEnumType::VALUE_B); } TEST(BaseOptionManager, LogOptions) { BaseOptionManager options; options.AddLogOptions(); auto VerifyLogState = [&](const std::string& output, bool expect_stderr, bool expect_stdout, bool expect_stderr_and_file) { const std::vector args = {"colmap", "--log_target", output}; std::vector argv; argv.reserve(args.size()); for (const auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); EXPECT_EQ(FLAGS_logtostderr, expect_stderr); #if defined(GLOG_VERSION_MAJOR) && \ (GLOG_VERSION_MAJOR > 0 || GLOG_VERSION_MINOR >= 6) EXPECT_EQ(FLAGS_logtostdout, expect_stdout); #endif EXPECT_EQ(FLAGS_alsologtostderr, expect_stderr_and_file); }; VerifyLogState("stderr", /*expect_stderr=*/true, /*expect_stdout=*/false, /*expect_and_file=*/false); #if defined(GLOG_VERSION_MAJOR) && \ (GLOG_VERSION_MAJOR > 0 || GLOG_VERSION_MINOR >= 6) VerifyLogState("stdout", /*expect_stderr=*/false, /*expect_stdout=*/true, /*expect_and_file=*/false); #else // glog < 0.6 does not support FLAGS_logtostdout, falls back to stderr. VerifyLogState("stdout", /*expect_stderr=*/true, /*expect_stdout=*/false, /*expect_and_file=*/false); #endif VerifyLogState("file", /*expect_stderr=*/false, /*expect_stdout=*/false, /*expect_and_file=*/false); VerifyLogState("stderr_and_file", /*expect_stderr=*/false, /*expect_stdout=*/false, /*expect_and_file=*/true); VerifyLogState("invalid", /*expect_stderr=*/false, /*expect_stdout=*/false, /*expect_and_file=*/true); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/bundle_adjustment.cc000066400000000000000000000100511517363634500234320ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/bundle_adjustment.h" #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/sfm/observation_manager.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" namespace colmap { namespace { // Callback functor called after each bundle adjustment iteration. class BundleAdjustmentIterationCallback : public ceres::IterationCallback { public: explicit BundleAdjustmentIterationCallback(BaseController* controller) : controller_(controller) {} virtual ceres::CallbackReturnType operator()( const ceres::IterationSummary& summary) { THROW_CHECK_NOTNULL(controller_); if (controller_->CheckIfStopped()) { return ceres::SOLVER_TERMINATE_SUCCESSFULLY; } else { return ceres::SOLVER_CONTINUE; } } private: BaseController* controller_; }; } // namespace BundleAdjustmentController::BundleAdjustmentController( const OptionManager& options, std::shared_ptr reconstruction) : options_(options), reconstruction_(std::move(reconstruction)) {} void BundleAdjustmentController::Run() { THROW_CHECK_NOTNULL(reconstruction_); LOG_HEADING1("Global bundle adjustment"); Timer run_timer; run_timer.Start(); if (reconstruction_->NumRegFrames() == 0) { LOG(ERROR) << "Need at least one registered frame."; return; } // Avoid degeneracies in bundle adjustment. ObservationManager(*reconstruction_).FilterObservationsWithNegativeDepth(); BundleAdjustmentOptions ba_options = *options_.bundle_adjustment; BundleAdjustmentIterationCallback iteration_callback(this); if (ba_options.ceres) { ba_options.ceres->solver_options.callbacks.push_back(&iteration_callback); } // Configure bundle adjustment. BundleAdjustmentConfig ba_config; for (const image_t image_id : reconstruction_->RegImageIds()) { ba_config.AddImage(image_id); } // Fixing the gauge with two cameras leads to a more stable optimization // with fewer steps as compared to fixing three points. // TODO(jsch): Investigate whether it is safe to not fix the gauge at all, // as initial experiments show that it is even faster. ba_config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); // Run bundle adjustment. std::unique_ptr bundle_adjuster = CreateDefaultBundleAdjuster(ba_options, ba_config, *reconstruction_); bundle_adjuster->Solve(); reconstruction_->UpdatePoint3DErrors(); run_timer.PrintMinutes(); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/bundle_adjustment.h000066400000000000000000000042241517363634500233010ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/base_controller.h" namespace colmap { // Class that controls the global bundle adjustment procedure. class BundleAdjustmentController : public BaseController { public: BundleAdjustmentController(const OptionManager& options, std::shared_ptr reconstruction); void Run(); private: const OptionManager& options_; std::shared_ptr reconstruction_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/bundle_adjustment_test.cc000066400000000000000000000066501517363634500245030ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/bundle_adjustment.h" #include "colmap/controllers/option_manager.h" #include "colmap/math/random.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include namespace colmap { namespace { TEST(BundleAdjustmentController, EmptyReconstruction) { SetPRNGSeed(1); auto reconstruction = std::make_shared(); OptionManager options; BundleAdjustmentController controller(options, reconstruction); EXPECT_NO_THROW(controller.Run()); EXPECT_EQ(reconstruction->NumRegImages(), 0); EXPECT_EQ(reconstruction->NumPoints3D(), 0); } TEST(BundleAdjustmentController, Reconstruction) { SetPRNGSeed(1); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 2; synthetic_options.num_frames_per_rig = 3; synthetic_options.num_points3D = 100; SynthesizeDataset(synthetic_options, >_reconstruction); auto reconstruction = std::make_shared(gt_reconstruction); SyntheticNoiseOptions noise_options; noise_options.point2D_stddev = 0.1; noise_options.point3D_stddev = 0.1; noise_options.rig_from_world_rotation_stddev = 0.1; noise_options.rig_from_world_translation_stddev = 0.1; SynthesizeNoise(noise_options, reconstruction.get()); OptionManager options; BundleAdjustmentController controller(options, reconstruction); controller.Run(); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.0)); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_extraction.cc000066400000000000000000000615201517363634500236250ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_extraction.h" #include "colmap/feature/sift.h" #include "colmap/scene/database.h" #include "colmap/util/cuda.h" #include "colmap/util/file.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" #include "colmap/util/timer.h" #include namespace colmap { namespace { void ScaleKeypoints(int bitmap_width, int bitmap_height, size_t camera_width, size_t camera_height, FeatureKeypoints* keypoints) { if (static_cast(bitmap_width) != camera_width || static_cast(bitmap_height) != camera_height) { const float scale_x = static_cast(camera_width) / bitmap_width; const float scale_y = static_cast(camera_height) / bitmap_height; for (auto& keypoint : *keypoints) { keypoint.Rescale(scale_x, scale_y); } } } void MaskFeatures(const Bitmap& mask, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) { size_t out_index = 0; BitmapColor color; for (size_t i = 0; i < keypoints->size(); ++i) { if (!mask.GetPixel(static_cast(keypoints->at(i).x), static_cast(keypoints->at(i).y), &color) || color.r == 0) { // Delete this keypoint by not copying it to the output. } else { // Retain this keypoint by copying it to the output index (in case this // index differs from its current position). if (out_index != i) { keypoints->at(out_index) = keypoints->at(i); for (int col = 0; col < descriptors->data.cols(); ++col) { descriptors->data(out_index, col) = descriptors->data(i, col); } } out_index += 1; } } keypoints->resize(out_index); descriptors->data.conservativeResize(out_index, descriptors->data.cols()); } struct ImageData { ImageReader::Status status = ImageReader::Status::FAILURE; Rig rig; Camera camera; Image image; PosePrior pose_prior; std::unique_ptr bitmap; std::unique_ptr mask; FeatureKeypoints keypoints; FeatureDescriptors descriptors; }; class ImageResizerThread : public Thread { public: ImageResizerThread(int max_image_size, JobQueue* input_queue, JobQueue* output_queue) : max_image_size_(max_image_size), input_queue_(input_queue), output_queue_(output_queue) { THROW_CHECK_GT(max_image_size_, 0); } private: void Run() override { while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); if (image_data.status == ImageReader::Status::SUCCESS) { if (static_cast(image_data.bitmap->Width()) > max_image_size_ || static_cast(image_data.bitmap->Height()) > max_image_size_) { // Fit the down-sampled version exactly into the max dimensions. const double scale = static_cast(max_image_size_) / std::max(image_data.bitmap->Width(), image_data.bitmap->Height()); const int new_width = static_cast(image_data.bitmap->Width() * scale); const int new_height = static_cast(image_data.bitmap->Height() * scale); image_data.bitmap->Rescale(new_width, new_height); } } output_queue_->Push(std::move(image_data)); } else { break; } } } const int max_image_size_; JobQueue* input_queue_; JobQueue* output_queue_; }; class FeatureExtractorThread : public Thread { public: FeatureExtractorThread(const FeatureExtractionOptions& extraction_options, const std::shared_ptr& camera_mask, JobQueue* input_queue, JobQueue* output_queue) : extraction_options_(extraction_options), camera_mask_(camera_mask), input_queue_(input_queue), output_queue_(output_queue) { THROW_CHECK(extraction_options_.Check()); if (extraction_options_.RequiresOpenGL()) { opengl_context_ = std::make_unique(); } } private: void Run() override { if (opengl_context_ != nullptr) { THROW_CHECK(opengl_context_->MakeCurrent()); } std::unique_ptr extractor = FeatureExtractor::Create(extraction_options_); if (extractor == nullptr) { LOG(ERROR) << "Failed to create feature extractor."; SignalInvalidSetup(); return; } SignalValidSetup(); while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); if (image_data.status == ImageReader::Status::SUCCESS) { const int orig_width = image_data.bitmap->Width(); const int orig_height = image_data.bitmap->Height(); const int rot90 = image_data.pose_prior.HasGravity() ? ComputeRot90FromGravity(image_data.pose_prior.gravity) : 0; if (rot90 > 0) { image_data.bitmap->Rot90(rot90); } if (extractor->Extract(*image_data.bitmap, &image_data.keypoints, &image_data.descriptors)) { if (rot90 > 0) { const int w = image_data.bitmap->Width(); const int h = image_data.bitmap->Height(); for (auto& kp : image_data.keypoints) { kp.Rot90(4 - rot90, w, h); } } ScaleKeypoints(orig_width, orig_height, image_data.camera.width, image_data.camera.height, &image_data.keypoints); if (camera_mask_) { MaskFeatures(*camera_mask_, &image_data.keypoints, &image_data.descriptors); } if (image_data.mask) { MaskFeatures(*image_data.mask, &image_data.keypoints, &image_data.descriptors); } } else { image_data.status = ImageReader::Status::FAILURE; } } // Release the memory, since it is not used afterwards. // Warning: Do not reset the pointer, as we use it later // to check if a mask exists for logging purposes. *image_data.bitmap = Bitmap(); if (image_data.mask) { *image_data.mask = Bitmap(); } output_queue_->Push(std::move(image_data)); } else { break; } } } const FeatureExtractionOptions extraction_options_; std::shared_ptr camera_mask_; std::unique_ptr opengl_context_; JobQueue* input_queue_; JobQueue* output_queue_; }; class FeatureWriterThread : public Thread { public: FeatureWriterThread(FeatureExtractorType extractor_type, size_t num_images, Database* database, JobQueue* input_queue) : extractor_type_str_(FeatureExtractorTypeToString(extractor_type)), num_images_(num_images), database_(database), input_queue_(input_queue) {} private: void Run() override { size_t image_index = 0; while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); image_index += 1; LOG(INFO) << StringPrintf( "Processed file [%d/%d]", image_index, num_images_); LOG(INFO) << StringPrintf(" Name: %s", image_data.image.Name().c_str()); if (image_data.status != ImageReader::Status::SUCCESS) { LOG(WARNING) << image_data.image.Name() << " " << ImageReader::StatusToString(image_data.status); continue; } LOG(INFO) << StringPrintf(" Dimensions: %d x %d", image_data.camera.width, image_data.camera.height); LOG(INFO) << StringPrintf(" Camera: #%d - %s", image_data.camera.camera_id, image_data.camera.ModelName().c_str()); LOG(INFO) << StringPrintf( " Focal Length: %.2fpx%s", image_data.camera.MeanFocalLength(), image_data.camera.has_prior_focal_length ? " (Prior)" : ""); LOG(INFO) << " Features: " << image_data.keypoints.size() << " (" << extractor_type_str_ << ")"; if (image_data.mask) { LOG(INFO) << " Mask: Yes"; } DatabaseTransaction database_transaction(database_); if (image_data.image.ImageId() == kInvalidImageId) { image_data.image.SetImageId(database_->WriteImage(image_data.image)); if (image_data.pose_prior.HasPosition() || image_data.pose_prior.HasGravity()) { if (image_data.pose_prior.HasPosition()) { LOG(INFO) << StringPrintf( " GPS: LAT=%.3f, LON=%.3f, ALT=%.3f", image_data.pose_prior.position.x(), image_data.pose_prior.position.y(), image_data.pose_prior.position.z()); } if (image_data.pose_prior.HasGravity()) { LOG(INFO) << StringPrintf( " Gravity: X=%.3f, Y=%.3f, Z=%.3f", image_data.pose_prior.gravity.x(), image_data.pose_prior.gravity.y(), image_data.pose_prior.gravity.z()); } image_data.pose_prior.corr_data_id = image_data.image.DataId(); image_data.pose_prior.pose_prior_id = database_->WritePosePrior(image_data.pose_prior); } Frame frame; frame.SetRigId(image_data.rig.RigId()); frame.AddDataId(image_data.image.DataId()); database_->WriteFrame(frame); } if (!database_->ExistsKeypoints(image_data.image.ImageId())) { database_->WriteKeypoints(image_data.image.ImageId(), image_data.keypoints); } if (!database_->ExistsDescriptors(image_data.image.ImageId())) { database_->WriteDescriptors(image_data.image.ImageId(), image_data.descriptors); } } else { break; } } } const std::string extractor_type_str_; const size_t num_images_; Database* database_; JobQueue* input_queue_; }; // Feature extraction class to extract features for all images in a directory. class FeatureExtractorController : public Thread { public: FeatureExtractorController(const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const FeatureExtractionOptions& extraction_options) : reader_options_(reader_options), extraction_options_(extraction_options), database_(Database::Open(database_path)), image_reader_(reader_options_, database_.get()) { THROW_CHECK(reader_options_.Check()); THROW_CHECK(extraction_options_.Check()); std::shared_ptr camera_mask; if (!reader_options_.camera_mask_path.empty()) { if (ExistsFile(reader_options_.camera_mask_path)) { camera_mask = std::make_shared(); if (!camera_mask->Read(reader_options_.camera_mask_path, /*as_rgb=*/false)) { LOG(ERROR) << "Failed to read invalid mask file at: " << reader_options_.camera_mask_path << ". No mask is going to be used."; camera_mask.reset(); } } else { LOG(ERROR) << "Mask at " << reader_options_.camera_mask_path << " does not exist."; } } const int num_threads = GetEffectiveNumThreads(extraction_options_.num_threads); THROW_CHECK_GT(num_threads, 0); // Make sure that we only have limited number of objects in the queue to // avoid excess in memory usage since images and features take lots of // memory. constexpr int kQueueSize = 1; resizer_queue_ = std::make_unique>(kQueueSize); extractor_queue_ = std::make_unique>(kQueueSize); writer_queue_ = std::make_unique>(kQueueSize); const int max_image_size = extraction_options_.EffMaxImageSize(); for (int i = 0; i < num_threads; ++i) { resizers_.emplace_back(std::make_unique( max_image_size, resizer_queue_.get(), extractor_queue_.get())); } // Determine if GPU extraction should be used. SIFT GPU extraction is not // supported with domain_size_pooling or estimate_affine_shape, which // require CPU-based covariant SIFT. auto worker_extraction_options = extraction_options_; if (extraction_options_.type == FeatureExtractorType::SIFT && (extraction_options_.sift->domain_size_pooling || extraction_options_.sift->estimate_affine_shape)) { worker_extraction_options.use_gpu = false; } if (worker_extraction_options.use_gpu) { std::vector gpu_indices = CSVToVector(extraction_options_.gpu_index); THROW_CHECK_GT(gpu_indices.size(), 0); #if defined(COLMAP_CUDA_ENABLED) if (gpu_indices.size() == 1 && gpu_indices[0] == -1) { const int num_cuda_devices = GetNumCudaDevices(); THROW_CHECK_GT(num_cuda_devices, 0); gpu_indices.resize(num_cuda_devices); std::iota(gpu_indices.begin(), gpu_indices.end(), 0); } #endif // COLMAP_CUDA_ENABLED // Prevent nested threading, as we multi-thread at the controller level. worker_extraction_options.num_threads = std::max(num_threads / static_cast(gpu_indices.size()), 1); for (const int gpu_index : gpu_indices) { worker_extraction_options.gpu_index = std::to_string(gpu_index); extractors_.emplace_back( std::make_unique(worker_extraction_options, camera_mask, extractor_queue_.get(), writer_queue_.get())); } } else { const static FeatureExtractionOptions kDefaultExtractionOptions; if (extraction_options_.num_threads == -1 && extraction_options_.type == FeatureExtractorType::SIFT && extraction_options_.max_image_size == kDefaultExtractionOptions.max_image_size && extraction_options_.sift->first_octave == kDefaultExtractionOptions.sift->first_octave) { LOG(WARNING) << "Your current options use the maximum number of " "threads on the machine to extract features. Extracting SIFT " "features on the CPU can consume a lot of RAM per thread for " "large images. Consider reducing the maximum image size and/or " "the first octave or manually limit the number of extraction " "threads. Ignore this warning, if your machine has sufficient " "memory for the current settings."; } int num_extractors = 0; switch (extraction_options_.type) { case FeatureExtractorType::SIFT: // Prevent nested threading, as we multi-thread at the controller // level as SIFT extraction doesn't require much RAM per extractor. num_extractors = num_threads; worker_extraction_options.num_threads = 1; break; case FeatureExtractorType::ALIKED_N16ROT: case FeatureExtractorType::ALIKED_N32: // Use a single extractor with parallelization per image because // ALIKED requires a lot of RAM per extractor and would otherwise OOM. num_extractors = 1; worker_extraction_options.num_threads = num_threads; break; default: LOG(FATAL_THROW) << "Unknown feature extractor type: " << FeatureExtractorTypeToString( extraction_options_.type); } THROW_CHECK_GT(num_extractors, 0); for (int i = 0; i < num_extractors; ++i) { extractors_.emplace_back( std::make_unique(worker_extraction_options, camera_mask, extractor_queue_.get(), writer_queue_.get())); } } writer_ = std::make_unique(extraction_options_.type, image_reader_.NumImages(), database_.get(), writer_queue_.get()); } private: void Run() override { LOG_HEADING1("Feature extraction"); Timer run_timer; run_timer.Start(); for (auto& resizer : resizers_) { resizer->Start(); } for (auto& extractor : extractors_) { extractor->Start(); } writer_->Start(); for (auto& extractor : extractors_) { if (!extractor->CheckValidSetup()) { return; } } while (image_reader_.NextIndex() < image_reader_.NumImages()) { if (IsStopped()) { resizer_queue_->Stop(); extractor_queue_->Stop(); resizer_queue_->Clear(); extractor_queue_->Clear(); break; } ImageData image_data; image_data.bitmap = std::make_unique(); Bitmap mask; image_data.status = image_reader_.Next(&image_data.rig, &image_data.camera, &image_data.image, &image_data.pose_prior, image_data.bitmap.get(), &mask); if (!mask.IsEmpty()) { image_data.mask = std::make_unique(std::move(mask)); } if (image_data.status != ImageReader::Status::SUCCESS) { // Release the memory, since it is not used afterwards. *image_data.bitmap = Bitmap(); if (image_data.mask) { *image_data.mask = Bitmap(); } } THROW_CHECK(resizer_queue_->Push(std::move(image_data))); } resizer_queue_->Wait(); resizer_queue_->Stop(); for (auto& resizer : resizers_) { resizer->Wait(); } extractor_queue_->Wait(); extractor_queue_->Stop(); for (auto& extractor : extractors_) { extractor->Wait(); } writer_queue_->Wait(); writer_queue_->Stop(); writer_->Wait(); run_timer.PrintMinutes(); } const ImageReaderOptions reader_options_; const FeatureExtractionOptions extraction_options_; std::shared_ptr database_; ImageReader image_reader_; std::vector> resizers_; std::vector> extractors_; std::unique_ptr writer_; std::unique_ptr> resizer_queue_; std::unique_ptr> extractor_queue_; std::unique_ptr> writer_queue_; }; // Import features from text files. Each image must have a corresponding text // file with the same name and an additional ".txt" suffix. // Currently hard-coded to support SIFT features. class FeatureImporterController : public Thread { public: FeatureImporterController(const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const std::filesystem::path& import_path) : database_path_(database_path), reader_options_(reader_options), import_path_(import_path) {} private: void Run() override { LOG_HEADING1("Feature import"); Timer run_timer; run_timer.Start(); if (!ExistsDir(import_path_)) { LOG(ERROR) << "Import directory does not exist."; return; } auto database = Database::Open(database_path_); ImageReader image_reader(reader_options_, database.get()); while (image_reader.NextIndex() < image_reader.NumImages()) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf("Processing file [%d/%d]", image_reader.NextIndex() + 1, image_reader.NumImages()); // Load image data and possibly save camera to database. Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; if (image_reader.Next( &rig, &camera, &image, &pose_prior, &bitmap, nullptr) != ImageReader::Status::SUCCESS) { continue; } const auto path = import_path_ / (image.Name() + ".txt"); if (ExistsFile(path)) { FeatureKeypoints keypoints; FeatureDescriptors descriptors; LoadSiftFeaturesFromTextFile(path, &keypoints, &descriptors); LOG(INFO) << "Features: " << keypoints.size() << "(Imported SIFT)"; DatabaseTransaction database_transaction(database.get()); if (image.ImageId() == kInvalidImageId) { image.SetImageId(database->WriteImage(image)); if (pose_prior.HasPosition() || pose_prior.HasGravity()) { pose_prior.corr_data_id = image.DataId(); pose_prior.pose_prior_id = database->WritePosePrior(pose_prior); } Frame frame; frame.SetRigId(rig.RigId()); frame.AddDataId(image.DataId()); database->WriteFrame(frame); } if (!database->ExistsKeypoints(image.ImageId())) { database->WriteKeypoints(image.ImageId(), keypoints); } if (!database->ExistsDescriptors(image.ImageId())) { database->WriteDescriptors(image.ImageId(), descriptors); } } else { LOG(INFO) << "SKIP: No features found at " << path; } } run_timer.PrintMinutes(); } const std::filesystem::path database_path_; const ImageReaderOptions reader_options_; const std::filesystem::path import_path_; }; } // namespace std::unique_ptr CreateFeatureExtractorController( const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const FeatureExtractionOptions& extraction_options) { return std::make_unique( database_path, reader_options, extraction_options); } std::unique_ptr CreateFeatureImporterController( const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const std::filesystem::path& import_path) { return std::make_unique( database_path, reader_options, import_path); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_extraction.h000066400000000000000000000046241517363634500234710ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/image_reader.h" #include "colmap/feature/extractor.h" #include "colmap/util/threading.h" #include namespace colmap { // Reads images from a folder, extracts features, and writes them to database. std::unique_ptr CreateFeatureExtractorController( const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const FeatureExtractionOptions& extraction_options); // Import features from text files. Each image must have a corresponding text // file with the same name and an additional ".txt" suffix. std::unique_ptr CreateFeatureImporterController( const std::filesystem::path& database_path, const ImageReaderOptions& reader_options, const std::filesystem::path& import_path); } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_extraction_test.cc000066400000000000000000000230441517363634500246630ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_extraction.h" #include "colmap/scene/database.h" #include "colmap/util/file.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { Bitmap CreateTestBitmap() { Bitmap bitmap(100, 100, /*as_rgb=*/false); bitmap.Fill(BitmapColor(0)); for (int y = 30; y < 70; ++y) { for (int x = 30; x < 70; ++x) { bitmap.SetPixel(x, y, BitmapColor(255)); } } return bitmap; } TEST(CreateFeatureExtractorController, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto image_path = test_dir / "images"; CreateDirIfNotExists(image_path); // Create test images const int kNumImages = 2; const Bitmap test_bitmap = CreateTestBitmap(); for (int i = 0; i < kNumImages; ++i) { test_bitmap.Write(image_path / (std::to_string(i) + ".png")); } // Set up options ImageReaderOptions reader_options; reader_options.image_path = image_path; FeatureExtractionOptions extraction_options; extraction_options.use_gpu = false; extraction_options.num_threads = kNumImages; // Create and run the controller auto controller = CreateFeatureExtractorController( database_path, reader_options, extraction_options); ASSERT_NE(controller, nullptr); controller->Start(); controller->Wait(); // Verify results in database auto database = Database::Open(database_path); const std::vector images = database->ReadAllImages(); EXPECT_EQ(images.size(), kNumImages); for (const auto& image : images) { EXPECT_TRUE(database->ExistsKeypoints(image.ImageId())); EXPECT_TRUE(database->ExistsDescriptors(image.ImageId())); const FeatureKeypoints keypoints = database->ReadKeypoints(image.ImageId()); const FeatureDescriptors descriptors = database->ReadDescriptors(image.ImageId()); // Check that features were extracted EXPECT_GT(keypoints.size(), 0); EXPECT_EQ(keypoints.size(), descriptors.data.rows()); EXPECT_EQ(descriptors.type, FeatureExtractorType::SIFT); EXPECT_EQ(descriptors.data.cols(), 128); } } TEST(CreateFeatureExtractorController, WithCameraMask) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto image_path = test_dir / "images"; const auto mask_path = test_dir / "mask.png"; CreateDirIfNotExists(image_path); // Create test image with features const Bitmap test_bitmap = CreateTestBitmap(); test_bitmap.Write(image_path / "test.png"); // Create a mask that only allows the center region (white = keep, black = // mask) The test bitmap has a white square from (30,30) to (70,70) We'll // create a mask that only keeps a smaller region Bitmap mask_bitmap(100, 100, /*as_rgb=*/false); mask_bitmap.Fill(BitmapColor(0)); // Start with all black (masked) // Only keep center region (40,40) to (60,60) for (int y = 40; y < 60; ++y) { for (int x = 40; x < 60; ++x) { mask_bitmap.SetPixel(x, y, BitmapColor(255)); // White = keep } } mask_bitmap.Write(mask_path); // Extract features without mask first to get baseline ImageReaderOptions reader_options_no_mask; reader_options_no_mask.image_path = image_path; FeatureExtractionOptions extraction_options; extraction_options.use_gpu = false; extraction_options.num_threads = 1; auto controller = CreateFeatureExtractorController( database_path, reader_options_no_mask, extraction_options); ASSERT_NE(controller, nullptr); controller->Start(); controller->Wait(); auto database = Database::Open(database_path); std::vector images = database->ReadAllImages(); ASSERT_EQ(images.size(), 1); const size_t num_features_no_mask = database->ReadKeypoints(images[0].ImageId()).size(); EXPECT_GT(num_features_no_mask, 0); // Now extract with mask const auto database_path_masked = test_dir / "database_masked.db"; ImageReaderOptions reader_options_masked; reader_options_masked.image_path = image_path; reader_options_masked.camera_mask_path = mask_path; controller = CreateFeatureExtractorController( database_path_masked, reader_options_masked, extraction_options); ASSERT_NE(controller, nullptr); controller->Start(); controller->Wait(); auto database_masked = Database::Open(database_path_masked); images = database_masked->ReadAllImages(); ASSERT_EQ(images.size(), 1); const FeatureKeypoints keypoints_masked = database_masked->ReadKeypoints(images[0].ImageId()); const FeatureDescriptors descriptors_masked = database_masked->ReadDescriptors(images[0].ImageId()); const size_t num_features_masked = keypoints_masked.size(); // With mask, should have fewer features EXPECT_LT(num_features_masked, num_features_no_mask); EXPECT_GT(num_features_masked, 0); // But should still have some features // All remaining keypoints should be within the unmasked region (40-60, 40-60) for (const auto& kp : keypoints_masked) { EXPECT_GE(kp.x, 40.0f); EXPECT_LT(kp.x, 60.0f); EXPECT_GE(kp.y, 40.0f); EXPECT_LT(kp.y, 60.0f); } // Descriptors should match keypoints count EXPECT_EQ(descriptors_masked.data.rows(), keypoints_masked.size()); EXPECT_EQ(descriptors_masked.type, FeatureExtractorType::SIFT); EXPECT_EQ(descriptors_masked.data.cols(), 128); } TEST(CreateFeatureImporterController, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto image_path = test_dir / "images"; const auto import_path = test_dir / "features"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(import_path); const int kNumImages = 2; const int kNumFeatures = 3; // Create test images const Bitmap test_bitmap = CreateTestBitmap(); for (int i = 0; i < kNumImages; ++i) { test_bitmap.Write(image_path / (std::to_string(i) + ".png")); } // Create feature text files for each image for (int i = 0; i < kNumImages; ++i) { const auto feature_file = import_path / (std::to_string(i) + ".png.txt"); std::ofstream file(feature_file); ASSERT_TRUE(file.is_open()); // Write header: num_features dimension const int kDimension = 128; file << kNumFeatures << " " << kDimension << "\n"; // Write features: x y scale orientation descriptor[0..127] for (int j = 0; j < kNumFeatures; ++j) { // Keypoint data file << (10.0f + j * 5.0f) << " " // x << (20.0f + j * 5.0f) << " " // y << (1.5f + j * 0.1f) << " " // scale << (0.5f + j * 0.2f); // orientation // Descriptor data (128 values) for (int k = 0; k < kDimension; ++k) { file << " " << ((j * kDimension + k) % 256); } file << "\n"; } } // Set up options ImageReaderOptions reader_options; reader_options.image_path = image_path; // Create and run the controller auto controller = CreateFeatureImporterController( database_path, reader_options, import_path); ASSERT_NE(controller, nullptr); controller->Start(); controller->Wait(); // Verify results in database auto database = Database::Open(database_path); const std::vector images = database->ReadAllImages(); EXPECT_EQ(images.size(), kNumImages); for (const auto& image : images) { EXPECT_TRUE(database->ExistsKeypoints(image.ImageId())); EXPECT_TRUE(database->ExistsDescriptors(image.ImageId())); const FeatureKeypoints keypoints = database->ReadKeypoints(image.ImageId()); const FeatureDescriptors descriptors = database->ReadDescriptors(image.ImageId()); // Check that features were imported correctly EXPECT_EQ(keypoints.size(), kNumFeatures); EXPECT_EQ(descriptors.type, FeatureExtractorType::SIFT); EXPECT_EQ(descriptors.data.rows(), kNumFeatures); EXPECT_EQ(descriptors.data.cols(), 128); // Verify some keypoint values EXPECT_FLOAT_EQ(keypoints[0].x, 10.0f); EXPECT_FLOAT_EQ(keypoints[0].y, 20.0f); } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching.cc000066400000000000000000000503461517363634500232430ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/feature_matching_utils.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/matcher.h" #include "colmap/feature/utils.h" #include "colmap/scene/database.h" #include "colmap/util/file.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" #include namespace colmap { namespace { void RigVerification(const std::shared_ptr& database, const std::shared_ptr& cache, const TwoViewGeometryOptions& geometry_options, const int num_threads) { std::unordered_map rigs; for (auto& rig : database->ReadAllRigs()) { rigs[rig.RigId()] = std::move(rig); } std::unordered_map image_to_frame_ids; for (const auto& frame : database->ReadAllFrames()) { for (const data_t& data_id : frame.ImageIds()) { image_to_frame_ids[data_id.id] = frame.FrameId(); } } struct FramePairStats { int num_image_pairs = 0; int num_matches = 0; }; std::map, FramePairStats> frame_pair_stats; for (const auto& [image_pair_id, pair_num_matches] : database->ReadNumMatches()) { if (pair_num_matches == 0) { continue; } const auto [image_id1, image_id2] = PairIdToImagePair(image_pair_id); frame_t frame_id1 = image_to_frame_ids.at(image_id1); frame_t frame_id2 = image_to_frame_ids.at(image_id2); if (frame_id1 > frame_id2) { std::swap(frame_id1, frame_id2); } auto& stats = frame_pair_stats[{frame_id1, frame_id2}]; stats.num_image_pairs += 1; stats.num_matches += pair_num_matches; } ThreadPool thread_pool(num_threads); for (const auto& [frame_pair, stats] : frame_pair_stats) { // If the frame pair has only matches between one pair of images, then // there is no need to run rig verification, as there are no rig // constraints. if (stats.num_image_pairs <= 1 || stats.num_matches < geometry_options.min_num_inliers) { continue; } thread_pool.AddTask([&cache, &rigs, geometry_options, frame_id1 = frame_pair.first, frame_id2 = frame_pair.second]() { const Frame& frame1 = cache->GetFrame(frame_id1); const Frame& frame2 = cache->GetFrame(frame_id2); const Rig& rig1 = rigs.at(frame1.RigId()); const Rig& rig2 = rigs.at(frame2.RigId()); std::unordered_map images; images.reserve(frame1.NumDataIds() + frame2.NumDataIds()); std::unordered_map cameras; cameras.reserve(images.size()); auto add_images_and_cameras = [&cache, &images, &cameras]( const Frame& frame) { for (const data_t& data_id : frame.ImageIds()) { Image& image = images[data_id.id]; image = cache->GetImage(data_id.id); image.SetPoints2D( FeatureKeypointsToPointsVector(*cache->GetKeypoints(data_id.id))); cameras[image.CameraId()] = cache->GetCamera(image.CameraId()); } }; add_images_and_cameras(frame1); add_images_and_cameras(frame2); std::vector, FeatureMatches>> matches; matches.reserve(frame1.NumDataIds() * frame2.NumDataIds()); for (const data_t& data_id1 : frame1.ImageIds()) { const image_t image_id1 = data_id1.id; for (const data_t& data_id2 : frame2.ImageIds()) { const image_t image_id2 = data_id2.id; // If verifying within the same frame, then skip redundant image // pairs, whereas different frames are guaranteed to have different // image pairs. Note that verifying within the same frame can be // useful when the images have some overlap but the matches between // image pairs are not enough alone but accumulating them over the // whole frame can lead to a successful verification. if ((frame_id1 == frame_id2 && image_id1 <= image_id2) || !cache->ExistsMatches(image_id1, image_id2)) { continue; } matches.emplace_back(std::make_pair(image_id1, image_id2), cache->GetMatches(image_id1, image_id2)); } } for (const auto& [image_pair, two_view_geometry] : EstimateRigTwoViewGeometries( rig1, rig2, images, cameras, matches, geometry_options)) { const auto& [image_id1, image_id2] = image_pair; cache->DeleteTwoViewGeometry(image_id1, image_id2); cache->WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); } }); } thread_pool.Wait(); } class FeatureMatcherThread : public Thread { public: template static std::unique_ptr Create( const typename PairGeneratorType::PairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { auto database = Database::Open(database_path); auto cache = std::make_shared( pairing_options.CacheSize(), database); return std::make_unique( matching_options, geometry_options, database, cache, [pairing_options, cache]() { return std::make_unique(pairing_options, cache); }); } using PairGeneratorFactory = std::function()>; FeatureMatcherThread(const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr database, std::shared_ptr cache, PairGeneratorFactory pair_generator_factory) : matching_options_(matching_options), geometry_options_(geometry_options), database_(std::move(database)), cache_(std::move(cache)), pair_generator_factory_(std::move(pair_generator_factory)), matcher_(matching_options, geometry_options, cache_) { THROW_CHECK(matching_options.Check()); THROW_CHECK(geometry_options.Check()); } private: void Run() override { LOG_HEADING1("Feature matching & geometric verification"); Timer run_timer; run_timer.Start(); if (!matcher_.Setup()) { return; } std::unique_ptr pair_generator = THROW_CHECK_NOTNULL(pair_generator_factory_()); while (!pair_generator->HasFinished()) { if (IsStopped()) { run_timer.PrintMinutes(); return; } Timer timer; timer.Start(); const std::vector> image_pairs = pair_generator->Next(); matcher_.Match(image_pairs); LOG(INFO) << StringPrintf("in %.3fs", timer.ElapsedSeconds()); } run_timer.PrintMinutes(); // Notice that we run rig verification after feature matching, because // feature matching operates on pairs of images instead of pairs of frames. // Rig verification operates on pairs of frames and we require all image // pairs between two frames to be matched before running rig verification. if (!matching_options_.skip_geometric_verification && matching_options_.rig_verification) { run_timer.Restart(); LOG_HEADING1("Rig verification"); RigVerification( database_, cache_, geometry_options_, matching_options_.num_threads); run_timer.PrintMinutes(); } } const FeatureMatchingOptions matching_options_; const TwoViewGeometryOptions geometry_options_; const std::shared_ptr database_; const std::shared_ptr cache_; const PairGeneratorFactory pair_generator_factory_; FeatureMatcherController matcher_; }; class GeometricVerifierThread : public Thread { public: template static std::unique_ptr Create( const GeometricVerifierOptions& verifier_options, const typename PairGeneratorType::PairingOptions& pairing_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { auto database = Database::Open(database_path); auto cache = std::make_shared( pairing_options.CacheSize(), database); return std::make_unique( verifier_options, geometry_options, database, cache, [pairing_options, cache]() { return std::make_unique(pairing_options, cache); }); } using PairGeneratorFactory = std::function()>; GeometricVerifierThread(const GeometricVerifierOptions& verifier_options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr database, std::shared_ptr cache, PairGeneratorFactory pair_generator_factory) : geometry_options_(geometry_options), database_(std::move(database)), cache_(std::move(cache)), pair_generator_factory_(std::move(pair_generator_factory)), verifier_(verifier_options, geometry_options, cache_) { THROW_CHECK(geometry_options.Check()); } private: void Run() override { LOG_HEADING1("Geometric verification"); Timer run_timer; run_timer.Start(); if (!verifier_.Setup()) { return; } std::unique_ptr pair_generator = THROW_CHECK_NOTNULL(pair_generator_factory_()); while (!pair_generator->HasFinished()) { if (IsStopped()) { run_timer.PrintMinutes(); return; } Timer timer; timer.Start(); const std::vector> image_pairs = pair_generator->Next(); verifier_.Verify(image_pairs); LOG(INFO) << StringPrintf("in %.3fs", timer.ElapsedSeconds()); } if (verifier_.Options().rig_verification) { run_timer.Restart(); LOG_HEADING1("Rig verification"); RigVerification(database_, cache_, geometry_options_, verifier_.Options().num_threads); run_timer.PrintMinutes(); } run_timer.PrintMinutes(); } const TwoViewGeometryOptions geometry_options_; const std::shared_ptr database_; const std::shared_ptr cache_; const PairGeneratorFactory pair_generator_factory_; GeometricVerifierController verifier_; }; } // namespace std::unique_ptr CreateExhaustiveFeatureMatcher( const ExhaustivePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateVocabTreeFeatureMatcher( const VocabTreePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateSequentialFeatureMatcher( const SequentialPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateSpatialFeatureMatcher( const SpatialPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateTransitiveFeatureMatcher( const TransitivePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateImagePairsFeatureMatcher( const ImportedPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return FeatureMatcherThread::Create( pairing_options, matching_options, geometry_options, database_path); } namespace { class FeaturePairsFeatureMatcher : public Thread { public: FeaturePairsFeatureMatcher(const FeaturePairsMatchingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) : options_(pairing_options), matching_options_(matching_options), geometry_options_(geometry_options), database_(Database::Open(database_path)), cache_(std::make_shared(/*cache_size=*/100, database_)) { THROW_CHECK(pairing_options.Check()); THROW_CHECK(matching_options.Check()); THROW_CHECK(geometry_options.Check()); } private: void Run() override { LOG_HEADING1("Importing matches"); Timer run_timer; run_timer.Start(); std::unordered_map image_name_to_image; image_name_to_image.reserve(cache_->GetImageIds().size()); for (const auto image_id : cache_->GetImageIds()) { const auto& image = cache_->GetImage(image_id); image_name_to_image.emplace(image.Name(), &image); } std::ifstream file(options_.match_list_path); THROW_CHECK_FILE_OPEN(file, options_.match_list_path); std::string line; while (std::getline(file, line)) { if (IsStopped()) { run_timer.PrintMinutes(); return; } StringTrim(&line); if (line.empty()) { continue; } std::istringstream line_stream(line); std::string image_name1, image_name2; try { line_stream >> image_name1 >> image_name2; } catch (...) { LOG(ERROR) << "Could not read image pair."; break; } LOG(INFO) << StringPrintf( "%s - %s", image_name1.c_str(), image_name2.c_str()); if (image_name_to_image.count(image_name1) == 0) { LOG(INFO) << StringPrintf("SKIP: Image %s not found in database.", image_name1.c_str()); break; } if (image_name_to_image.count(image_name2) == 0) { LOG(INFO) << StringPrintf("SKIP: Image %s not found in database.", image_name2.c_str()); break; } const Image& image1 = *image_name_to_image[image_name1]; const Image& image2 = *image_name_to_image[image_name2]; bool skip_pair = false; if (database_->ExistsTwoViewGeometry(image1.ImageId(), image2.ImageId())) { LOG(INFO) << "SKIP: Matches for image pair already exist in database."; skip_pair = true; } FeatureMatches matches; while (std::getline(file, line)) { StringTrim(&line); if (line.empty()) { break; } std::istringstream line_stream(line); FeatureMatch match; try { line_stream >> match.point2D_idx1 >> match.point2D_idx2; } catch (...) { LOG(ERROR) << "Cannot read feature matches."; break; } matches.push_back(match); } if (skip_pair) { continue; } const Camera& camera1 = cache_->GetCamera(image1.CameraId()); const Camera& camera2 = cache_->GetCamera(image2.CameraId()); TwoViewGeometry two_view_geometry; if (options_.verify_matches) { database_->WriteMatches(image1.ImageId(), image2.ImageId(), matches); const std::shared_ptr keypoints1 = cache_->GetKeypoints(image1.ImageId()); const std::shared_ptr keypoints2 = cache_->GetKeypoints(image2.ImageId()); two_view_geometry = EstimateTwoViewGeometry(camera1, FeatureKeypointsToPointsVector(*keypoints1), camera2, FeatureKeypointsToPointsVector(*keypoints2), std::move(matches), geometry_options_); } else { if (camera1.has_prior_focal_length && camera2.has_prior_focal_length) { two_view_geometry.config = TwoViewGeometry::CALIBRATED; } else { two_view_geometry.config = TwoViewGeometry::UNCALIBRATED; } two_view_geometry.inlier_matches = std::move(matches); } database_->WriteTwoViewGeometry( image1.ImageId(), image2.ImageId(), two_view_geometry); } run_timer.PrintMinutes(); } const FeaturePairsMatchingOptions options_; const FeatureMatchingOptions matching_options_; const TwoViewGeometryOptions geometry_options_; const std::shared_ptr database_; const std::shared_ptr cache_; }; } // namespace std::unique_ptr CreateFeaturePairsFeatureMatcher( const FeaturePairsMatchingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return std::make_unique( pairing_options, matching_options, geometry_options, database_path); } std::unique_ptr CreateGeometricVerifier( const GeometricVerifierOptions& verifier_options, const ExistingMatchedPairingOptions& pairing_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path) { return GeometricVerifierThread::Create( verifier_options, pairing_options, geometry_options, database_path); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching.h000066400000000000000000000170011517363634500230740ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/pairing.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/matcher.h" #include "colmap/util/threading.h" #include #include namespace colmap { // Exhaustively match images by processing each block in the exhaustive match // matrix in one batch: // // +----+----+-----------------> images[i] // |#000|0000| // |1#00|1000| <- Above the main diagonal, the block diagonal is not matched // |11#0|1100| ^ // |111#|1110| | // +----+----+ | // |1000|#000|\ | // |1100|1#00| \ One block | // |1110|11#0| / of image pairs | // |1111|111#|/ | // +----+----+ | // | ^ | // | | | // | Below the main diagonal, the block diagonal is matched <--------------+ // | // v // images[i] // // Pairs will only be matched if 1, to avoid duplicate pairs. Pairs with # // are on the main diagonal and denote pairs of the same image. std::unique_ptr CreateExhaustiveFeatureMatcher( const ExhaustivePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Match each image against its nearest neighbors using a vocabulary tree. std::unique_ptr CreateVocabTreeFeatureMatcher( const VocabTreePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Sequentially match images within neighborhood: // // +-------------------------------+-----------------------> images[i] // ^ | ^ // | Current image[i] | // | | | // +----------+-----------+ // | // Match image_i against // // image_[i - o, i + o] with o = [1 .. overlap] // image_[i - 2^o, i + 2^o] (for quadratic overlap) // // Sequential order is determined based on the image names in ascending order. // // Invoke loop detection if `(i mod loop_detection_period) == 0`, retrieve // most similar `loop_detection_num_images` images from vocabulary tree, // and perform matching and verification. std::unique_ptr CreateSequentialFeatureMatcher( const SequentialPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Match images against spatial nearest neighbors using prior location // information, e.g. provided manually or extracted from EXIF. std::unique_ptr CreateSpatialFeatureMatcher( const SpatialPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Match transitive image pairs in a database with existing feature matches. // This matcher transitively closes loops/triplets. For example, if image pairs // A-B and B-C match but A-C has not been matched, then this matcher attempts to // match A-C. This procedure is performed for multiple iterations. std::unique_ptr CreateTransitiveFeatureMatcher( const TransitivePairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Match images manually specified in a list of image pairs. // // Read matches file with the following format: // // image_name1 image_name2 // image_name1 image_name3 // image_name2 image_name3 // ... // std::unique_ptr CreateImagePairsFeatureMatcher( const ImportedPairingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Import feature matches from a text file. // // Read matches file with the following format: // // image_name1 image_name2 // 0 1 // 1 2 // 2 3 // // image_name1 image_name3 // 0 1 // 1 2 // 2 3 // ... // std::unique_ptr CreateFeaturePairsFeatureMatcher( const FeaturePairsMatchingOptions& pairing_options, const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); // Options for CreateGeometricVerifier struct GeometricVerifierOptions { // Number of threads for geometric verification. int num_threads = -1; // Whether to perform rig verification at the end. Unnecessary when we have // existing relative poses for each matched pair. bool rig_verification = false; // Whether to use the existing relative pose stored in TwoViewGeometry in the // database. If no TwoViewGeometry is found we will fall back to geometric // verification with RANSAC. bool use_existing_relative_pose = false; }; // Perform geometric verification of existing matched image pairs. std::unique_ptr CreateGeometricVerifier( const GeometricVerifierOptions& verifier_options, const ExistingMatchedPairingOptions& pairing_options, const TwoViewGeometryOptions& geometry_options, const std::filesystem::path& database_path); } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching_test.cc000066400000000000000000000440101517363634500242710ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_matching.h" #include "colmap/feature/types.h" #include "colmap/retrieval/visual_index.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { void CreateTestDatabase(int num_images, Database& database) { Reconstruction unused_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = num_images; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 20; synthetic_dataset_options.num_points2D_without_point3D = 3; synthetic_dataset_options.prior_position = true; SynthesizeDataset( synthetic_dataset_options, &unused_reconstruction, &database); } std::unique_ptr CreateSyntheticVisualIndex() { auto visual_index = retrieval::VisualIndex::Create(); retrieval::VisualIndex::BuildOptions build_options; build_options.num_visual_words = 5; visual_index->Build( build_options, FeatureDescriptorsFloat(FeatureExtractorType::SIFT, FeatureDescriptorsFloatData::Random(50, 128))); return visual_index; } TEST(CreateExhaustiveFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); ExhaustivePairingOptions pairing_options; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateExhaustiveFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); EXPECT_EQ(database->ReadAllMatches().size(), 6); EXPECT_EQ(database->ReadTwoViewGeometries().size(), 6); } TEST(CreateVocabTreeFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto vocab_tree_path = test_dir / "vocab_tree.bin"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); // Create vocab tree CreateSyntheticVisualIndex()->Write(vocab_tree_path); VocabTreePairingOptions pairing_options; pairing_options.vocab_tree_path = vocab_tree_path; pairing_options.num_images = 2; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateVocabTreeFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); // Each image should match with num_images others, // while some of the pairs may be redundant. EXPECT_GE(database->ReadAllMatches().size(), 4); EXPECT_GE(database->ReadTwoViewGeometries().size(), 4); } TEST(CreateSequentialFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/5, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); SequentialPairingOptions pairing_options; pairing_options.overlap = 2; pairing_options.quadratic_overlap = false; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateSequentialFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); // With 5 images and overlap=2: // (0,1), (0,2), (1,2), (1,3), (2,3), (2,4), (3,4) EXPECT_EQ(database->ReadAllMatches().size(), 7); EXPECT_EQ(database->ReadTwoViewGeometries().size(), 7); } TEST(CreateSpatialFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); SpatialPairingOptions pairing_options; pairing_options.max_num_neighbors = 2; pairing_options.max_distance = 1e6; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateSpatialFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); EXPECT_GT(database->ReadAllMatches().size(), 0); EXPECT_GT(database->ReadTwoViewGeometries().size(), 0); } TEST(CreateTransitiveFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); const std::vector images = database->ReadAllImages(); ASSERT_GE(images.size(), 3); // Create initial matches: 1-2 and 2-3 TwoViewGeometry two_view_geometry; two_view_geometry.config = TwoViewGeometry::CALIBRATED; two_view_geometry.inlier_matches = FeatureMatches(10); database->WriteTwoViewGeometry( images[0].ImageId(), images[1].ImageId(), two_view_geometry); database->WriteTwoViewGeometry( images[1].ImageId(), images[2].ImageId(), two_view_geometry); TransitivePairingOptions pairing_options; pairing_options.batch_size = 100; pairing_options.num_iterations = 1; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateTransitiveFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); // Should create transitive match 1-3 const size_t final_matches = database->ReadTwoViewGeometries().size(); EXPECT_GE(final_matches, 2); // At least the original 2 matches } TEST(CreateImagePairsFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto match_list_path = test_dir / "match_list.txt"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); const std::vector images = database->ReadAllImages(); ASSERT_GE(images.size(), 3); // Create match list file with specific image pairs std::ofstream file(match_list_path); file << images[0].Name() << " " << images[1].Name() << "\n"; file << images[1].Name() << " " << images[2].Name() << "\n"; file << images[2].Name() << " " << images[3].Name() << "\n"; file.close(); ImportedPairingOptions pairing_options; pairing_options.match_list_path = match_list_path; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto matcher = CreateImagePairsFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); EXPECT_EQ(database->ReadAllMatches().size(), 3); EXPECT_EQ(database->ReadTwoViewGeometries().size(), 3); } TEST(CreateFeaturePairsFeatureMatcher, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; const auto match_list_path = test_dir / "feature_match_list.txt"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/3, *database); database->ClearMatches(); database->ClearTwoViewGeometries(); const std::vector images = database->ReadAllImages(); ASSERT_GE(images.size(), 2); // Create feature match list file with many matches for better verification std::ofstream file(match_list_path); file << images[0].Name() << " " << images[1].Name() << "\n"; for (int i = 0; i < 15; ++i) { file << i << " " << i << "\n"; } file << "\n"; // Empty line separates pairs file << images[1].Name() << " " << images[2].Name() << "\n"; for (int i = 0; i < 15; ++i) { file << i << " " << i << "\n"; } file << "\n"; file.close(); FeaturePairsMatchingOptions pairing_options; pairing_options.match_list_path = match_list_path; pairing_options.verify_matches = true; FeatureMatchingOptions matching_options; matching_options.use_gpu = false; matching_options.num_threads = 1; TwoViewGeometryOptions geometry_options; geometry_options.min_num_inliers = 5; // Lower threshold for testing auto matcher = CreateFeaturePairsFeatureMatcher( pairing_options, matching_options, geometry_options, database_path); ASSERT_NE(matcher, nullptr); matcher->Start(); matcher->Wait(); // Should have imported and verified the matches EXPECT_GE(database->ReadTwoViewGeometries().size(), 2); } TEST(CreateGeometricVerifier, Nominal) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); CreateTestDatabase(/*num_images=*/4, *database); database->ClearTwoViewGeometries(); ExistingMatchedPairingOptions pairing_options; GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; auto verifier = CreateGeometricVerifier( verifier_options, pairing_options, geometry_options, database_path); ASSERT_NE(verifier, nullptr); verifier->Start(); verifier->Wait(); EXPECT_GE(database->ReadAllMatches().size(), 3); EXPECT_GE(database->ReadTwoViewGeometries().size(), 3); } void ExpectRigVerificationResults(const Database& database, int num_expected_matches, int num_expected_calibrated, int num_expected_calibrated_rig) { // Verify that two-view geometries were created. int num_calibrated = 0; int num_calibrated_rig = 0; int num_others = 0; for (const auto& [pair_id, two_view_geometry] : database.ReadTwoViewGeometries()) { EXPECT_EQ(two_view_geometry.inlier_matches.size(), num_expected_matches); switch (two_view_geometry.config) { case TwoViewGeometry::CALIBRATED: ++num_calibrated; break; case TwoViewGeometry::CALIBRATED_RIG: ++num_calibrated_rig; break; default: ++num_others; } } // Two calibrated pairs between images in the same frames. EXPECT_EQ(num_calibrated, num_expected_calibrated); // Four calibrated pairs between images in different frames. EXPECT_EQ(num_calibrated_rig, num_expected_calibrated_rig); EXPECT_EQ(num_others, 0); } TEST(CreateGeometricVerifier, RigVerificationWithNonTrivialFrames) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 2; synthetic_dataset_options.num_points3D = 25; synthetic_dataset_options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; synthetic_dataset_options.camera_has_prior_focal_length = true; SynthesizeDataset(synthetic_dataset_options, &reconstruction, database.get()); ExistingMatchedPairingOptions pairing_options; GeometricVerifierOptions verifier_options; verifier_options.num_threads = -1; verifier_options.rig_verification = true; TwoViewGeometryOptions geometry_options; geometry_options.min_num_inliers = 5; auto verifier = CreateGeometricVerifier( verifier_options, pairing_options, geometry_options, database_path); ASSERT_NE(verifier, nullptr); verifier->Start(); verifier->Wait(); // All pairs should be overwritten with calibrated rig pairs. ExpectRigVerificationResults(*database, synthetic_dataset_options.num_points3D, /*num_expected_calibrated=*/0, /*num_expected_calibrated_rig=*/15); } TEST(CreateGeometricVerifier, RigVerificationWithTrivialFrames) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 2; synthetic_dataset_options.num_points3D = 25; synthetic_dataset_options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; synthetic_dataset_options.camera_has_prior_focal_length = true; SynthesizeDataset(synthetic_dataset_options, &reconstruction, database.get()); ExistingMatchedPairingOptions pairing_options; GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; verifier_options.rig_verification = true; TwoViewGeometryOptions geometry_options; geometry_options.min_num_inliers = 5; auto verifier = CreateGeometricVerifier( verifier_options, pairing_options, geometry_options, database_path); ASSERT_NE(verifier, nullptr); verifier->Start(); verifier->Wait(); // Trivial frames should be skipped and unmodified. ExpectRigVerificationResults(*database, synthetic_dataset_options.num_points3D, /*num_expected_calibrated=*/1, /*num_expected_calibrated_rig=*/0); } TEST(CreateGeometricVerifier, Guided) { const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.inlier_match_ratio = 0.6; synthetic_dataset_options.two_view_geometry_has_relative_pose = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); // Clear all inlier matches. cam2_from_cam1 is already gt from the synthesized // database. std::vector> gt_two_view_geometries = database->ReadTwoViewGeometries(); for (const auto& [pair_id, _] : gt_two_view_geometries) { const auto [image_id1, image_id2] = PairIdToImagePair(pair_id); database->DeleteInlierMatches(image_id1, image_id2); } ExistingMatchedPairingOptions pairing_options; GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; verifier_options.use_existing_relative_pose = true; TwoViewGeometryOptions geometry_options; auto verifier = CreateGeometricVerifier( verifier_options, pairing_options, geometry_options, database_path); ASSERT_NE(verifier, nullptr); verifier->Start(); verifier->Wait(); // Check validity after guided geometric verification. std::vector> two_view_geometries = database->ReadTwoViewGeometries(); EXPECT_GE(two_view_geometries.size(), gt_two_view_geometries.size()); for (size_t i = 0; i < two_view_geometries.size(); ++i) { EXPECT_EQ(two_view_geometries[i].first, gt_two_view_geometries[i].first); EXPECT_EQ(two_view_geometries[i].second.cam2_from_cam1, gt_two_view_geometries[i].second.cam2_from_cam1); EXPECT_TRUE(gt_two_view_geometries[i].second.E.value().isApprox( two_view_geometries[i].second.E.value())); // Should at least have all the original inliers. Some generated outliers // can be accidentally inliers as well. EXPECT_GE(two_view_geometries[i].second.inlier_matches.size(), gt_two_view_geometries[i].second.inlier_matches.size()); } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching_utils.cc000066400000000000000000000536041517363634500244630ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_matching_utils.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/sift.h" #include "colmap/feature/utils.h" #include "colmap/util/cuda.h" #include "colmap/util/misc.h" #if defined(COLMAP_CUDA_ENABLED) #include #endif #include namespace colmap { FeatureMatcherWorker::FeatureMatcherWorker( const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::shared_ptr& cache, JobQueue* input_queue, JobQueue* output_queue) : matching_options_(matching_options), geometry_options_(geometry_options), cache_(cache), input_queue_(input_queue), output_queue_(output_queue) { THROW_CHECK(matching_options_.Check()); if (matching_options_.RequiresOpenGL()) { opengl_context_ = std::make_unique(); } } void FeatureMatcherWorker::Run() { if (opengl_context_ != nullptr) { THROW_CHECK(opengl_context_->MakeCurrent()); } #if defined(COLMAP_CUDA_ENABLED) if (matching_options_.use_gpu) { // Initialize CUDA device for this worker thread const std::vector gpu_indices = CSVToVector(matching_options_.gpu_index); THROW_CHECK_EQ(gpu_indices.size(), 1) << "Each matching worker can only use one GPU"; const int gpu_index = gpu_indices[0]; if (gpu_index >= 0) { SetBestCudaDevice(gpu_index); LOG(INFO) << "Bind FeatureMatcherWorker to GPU device " << gpu_index; } } #endif if (matching_options_.type == FeatureMatcherType::SIFT_BRUTEFORCE) { // TODO(jsch): This is a bit ugly, but currently cannot think of a better // way to inject the shared descriptor index cache. THROW_CHECK_NOTNULL(matching_options_.sift)->cpu_descriptor_index_cache = &cache_->GetFeatureDescriptorIndexCache(); THROW_CHECK_NOTNULL(matching_options_.sift->cpu_descriptor_index_cache); } // Minimize the amount of allocated GPU memory by computing the maximum number // of descriptors for any image over the whole database. matching_options_.max_num_matches = std::min( matching_options_.max_num_matches, cache_->MaxNumKeypoints()); std::unique_ptr matcher = FeatureMatcher::Create(matching_options_); if (matcher == nullptr) { LOG(ERROR) << "Failed to create feature matcher."; SignalInvalidSetup(); return; } SignalValidSetup(); while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& data = input_job.Data(); if (!cache_->ExistsDescriptors(data.image_id1) || !cache_->ExistsDescriptors(data.image_id2)) { THROW_CHECK(output_queue_->Push(std::move(data))); continue; } const auto& camera1 = cache_->GetCamera(cache_->GetImage(data.image_id1).CameraId()); const auto& camera2 = cache_->GetCamera(cache_->GetImage(data.image_id2).CameraId()); if (matching_options_.guided_matching) { matcher->MatchGuided( geometry_options_.ransac_options.max_error, { data.image_id1, &camera1, cache_->GetKeypoints(data.image_id1), cache_->GetDescriptors(data.image_id1), cache_->FindImagePosePriorOrNull(data.image_id1), }, { data.image_id2, &camera2, cache_->GetKeypoints(data.image_id2), cache_->GetDescriptors(data.image_id2), cache_->FindImagePosePriorOrNull(data.image_id2), }, &data.two_view_geometry); } else { matcher->Match( { data.image_id1, &camera1, cache_->GetKeypoints(data.image_id1), cache_->GetDescriptors(data.image_id1), cache_->FindImagePosePriorOrNull(data.image_id1), }, { data.image_id2, &camera2, cache_->GetKeypoints(data.image_id2), cache_->GetDescriptors(data.image_id2), cache_->FindImagePosePriorOrNull(data.image_id2), }, &data.matches); } THROW_CHECK(output_queue_->Push(std::move(data))); } } } namespace { class VerifierWorker : public Thread { public: using Input = FeatureMatcherData; using Output = FeatureMatcherData; VerifierWorker(const TwoViewGeometryOptions& options, std::shared_ptr cache, JobQueue* input_queue, JobQueue* output_queue, const bool use_existing_relative_pose = false) : options_(options), cache_(std::move(cache)), use_existing_relative_pose_(use_existing_relative_pose), input_queue_(input_queue), output_queue_(output_queue) { THROW_CHECK(options_.Check()); } protected: void Run() override { while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& data = input_job.Data(); if (data.matches.size() < static_cast(options_.min_num_inliers)) { THROW_CHECK(output_queue_->Push(std::move(data))); continue; } const auto& camera1 = cache_->GetCamera(cache_->GetImage(data.image_id1).CameraId()); const auto& camera2 = cache_->GetCamera(cache_->GetImage(data.image_id2).CameraId()); const auto keypoints1 = cache_->GetKeypoints(data.image_id1); const auto keypoints2 = cache_->GetKeypoints(data.image_id2); const std::vector points1 = FeatureKeypointsToPointsVector(*keypoints1); const std::vector points2 = FeatureKeypointsToPointsVector(*keypoints2); if (use_existing_relative_pose_ && data.two_view_geometry.cam2_from_cam1.has_value()) { data.two_view_geometry = TwoViewGeometryFromKnownRelativePose( camera1, points1, camera2, points2, *data.two_view_geometry.cam2_from_cam1, data.matches, options_.min_num_inliers, options_.ransac_options.max_error); } else { data.two_view_geometry = EstimateTwoViewGeometry( camera1, points1, camera2, points2, data.matches, options_); } THROW_CHECK(output_queue_->Push(std::move(data))); } } } private: const TwoViewGeometryOptions options_; std::shared_ptr cache_; const bool use_existing_relative_pose_; JobQueue* input_queue_; JobQueue* output_queue_; }; } // namespace FeatureMatcherController::FeatureMatcherController( const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr cache) : matching_options_(matching_options), geometry_options_(geometry_options), cache_(std::move(cache)), is_setup_(false) { THROW_CHECK(matching_options_.Check()); THROW_CHECK(geometry_options_.Check()); THROW_CHECK_EQ(geometry_options_.ransac_options.num_threads, 1) << "Parallel RANSAC is not supported inside multi-threaded matching"; const int num_threads = GetEffectiveNumThreads(matching_options_.num_threads); THROW_CHECK_GT(num_threads, 0); std::vector gpu_indices = CSVToVector(matching_options_.gpu_index); THROW_CHECK_GT(gpu_indices.size(), 0); #if defined(COLMAP_CUDA_ENABLED) if (matching_options_.use_gpu && gpu_indices.size() == 1 && gpu_indices[0] == -1) { const int num_cuda_devices = GetNumCudaDevices(); THROW_CHECK_GT(num_cuda_devices, 0); gpu_indices.resize(num_cuda_devices); std::iota(gpu_indices.begin(), gpu_indices.end(), 0); } #endif // COLMAP_CUDA_ENABLED // If skip_geometric_verification, match directly to output_queue_. const bool skip_geometric_verification = matching_options_.skip_geometric_verification && !matching_options_.guided_matching; JobQueue* matcher_output_queue = skip_geometric_verification ? &output_queue_ : &verifier_queue_; if (matching_options_.use_gpu) { auto worker_matching_options = matching_options_; // The first matching is always without guided matching. worker_matching_options.guided_matching = false; matchers_.reserve(gpu_indices.size()); for (const auto& gpu_index : gpu_indices) { worker_matching_options.gpu_index = std::to_string(gpu_index); matchers_.emplace_back( std::make_unique(worker_matching_options, geometry_options_, cache_, &matcher_queue_, matcher_output_queue)); } } else { auto worker_matching_options = matching_options_; // Prevent nested threading. worker_matching_options.num_threads = 1; // The first matching is always without guided matching. worker_matching_options.guided_matching = false; matchers_.reserve(num_threads); for (int i = 0; i < num_threads; ++i) { matchers_.emplace_back( std::make_unique(worker_matching_options, geometry_options_, cache_, &matcher_queue_, matcher_output_queue)); } } verifiers_.reserve(num_threads); if (matching_options_.guided_matching) { // Redirect the verification output to final round of guided matching. for (int i = 0; i < num_threads; ++i) { verifiers_.emplace_back(std::make_unique( geometry_options_, cache_, &verifier_queue_, &guided_matcher_queue_)); } if (matching_options_.use_gpu) { auto worker_matching_options = matching_options_; guided_matchers_.reserve(gpu_indices.size()); for (const auto& gpu_index : gpu_indices) { worker_matching_options.gpu_index = std::to_string(gpu_index); guided_matchers_.emplace_back( std::make_unique(worker_matching_options, geometry_options_, cache_, &guided_matcher_queue_, &output_queue_)); } } else { auto worker_matching_options = matching_options_; // Prevent nested threading. worker_matching_options.num_threads = 1; guided_matchers_.reserve(num_threads); for (int i = 0; i < num_threads; ++i) { guided_matchers_.emplace_back( std::make_unique(worker_matching_options, geometry_options_, cache_, &guided_matcher_queue_, &output_queue_)); } } } else if (!matching_options.skip_geometric_verification) { for (int i = 0; i < num_threads; ++i) { verifiers_.emplace_back(std::make_unique( geometry_options_, cache_, &verifier_queue_, &output_queue_)); } } } FeatureMatcherController::~FeatureMatcherController() { matcher_queue_.Wait(); verifier_queue_.Wait(); guided_matcher_queue_.Wait(); output_queue_.Wait(); for (auto& matcher : matchers_) { matcher->Stop(); } for (auto& verifier : verifiers_) { verifier->Stop(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->Stop(); } matcher_queue_.Stop(); verifier_queue_.Stop(); guided_matcher_queue_.Stop(); output_queue_.Stop(); for (auto& matcher : matchers_) { matcher->Wait(); } for (auto& verifier : verifiers_) { verifier->Wait(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->Wait(); } } bool FeatureMatcherController::Setup() { for (auto& matcher : matchers_) { matcher->Start(); } for (auto& verifier : verifiers_) { verifier->Start(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->Start(); } for (auto& matcher : matchers_) { if (!matcher->CheckValidSetup()) { return false; } } for (auto& guided_matcher : guided_matchers_) { if (!guided_matcher->CheckValidSetup()) { return false; } } is_setup_ = true; return true; } void FeatureMatcherController::Match( const std::vector>& image_pairs) { THROW_CHECK_NOTNULL(cache_); THROW_CHECK(is_setup_); if (image_pairs.empty()) { return; } ////////////////////////////////////////////////////////////////////////////// // Match the image pairs ////////////////////////////////////////////////////////////////////////////// std::unordered_set image_pair_ids; image_pair_ids.reserve(image_pairs.size()); size_t num_outputs = 0; for (const auto& [image_id1, image_id2] : image_pairs) { // Avoid self-matches. if (image_id1 == image_id2) { continue; } // Avoid duplicate image pairs. const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); if (!image_pair_ids.insert(pair_id).second) { continue; } // Avoid self-matches within a frame. if (matching_options_.skip_image_pairs_in_same_frame) { const Image& image1 = cache_->GetImage(image_id1); const Image& image2 = cache_->GetImage(image_id2); if (image1.HasFrameId() && image2.HasFrameId() && image1.FrameId() == image2.FrameId()) { continue; } } const bool exists_matches = cache_->ExistsMatches(image_id1, image_id2); const bool exists_two_view_geometry = cache_->ExistsTwoViewGeometry(image_id1, image_id2); if (exists_matches && exists_two_view_geometry) { continue; } num_outputs += 1; // If only one of the matches or inlier matches exist, we recompute them // from scratch and delete the existing results. This must be done before // pushing the jobs to the queue, otherwise database constraints might fail // when writing an existing result into the database. if (exists_two_view_geometry) { cache_->DeleteTwoViewGeometry(image_id1, image_id2); } FeatureMatcherData data; data.image_id1 = image_id1; data.image_id2 = image_id2; if (exists_matches) { data.matches = cache_->GetMatches(image_id1, image_id2); cache_->DeleteMatches(image_id1, image_id2); THROW_CHECK(verifier_queue_.Push(std::move(data))); } else { THROW_CHECK(matcher_queue_.Push(std::move(data))); } } ////////////////////////////////////////////////////////////////////////////// // Write results to database ////////////////////////////////////////////////////////////////////////////// for (size_t i = 0; i < num_outputs; ++i) { auto output_job = output_queue_.Pop(); THROW_CHECK(output_job.IsValid()); auto& output = output_job.Data(); if (output.matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.matches = {}; } if (output.two_view_geometry.inlier_matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.two_view_geometry = TwoViewGeometry(); } cache_->WriteMatches(output.image_id1, output.image_id2, output.matches); cache_->WriteTwoViewGeometry( output.image_id1, output.image_id2, output.two_view_geometry); } THROW_CHECK_EQ(output_queue_.Size(), 0); } GeometricVerifierController::GeometricVerifierController( const GeometricVerifierOptions& options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr cache) : geometry_options_(geometry_options), cache_(std::move(cache)), options_(options), is_setup_(false) { THROW_CHECK(geometry_options_.Check()); const int num_threads = GetEffectiveNumThreads(options_.num_threads); // Run geometric verification for (int i = 0; i < num_threads; ++i) { verifiers_.emplace_back( std::make_unique(geometry_options_, cache_, &verifier_queue_, &output_queue_, options_.use_existing_relative_pose)); } } GeometricVerifierController::~GeometricVerifierController() { verifier_queue_.Wait(); output_queue_.Wait(); for (auto& verifier : verifiers_) { verifier->Stop(); } verifier_queue_.Stop(); output_queue_.Stop(); for (auto& verifier : verifiers_) { verifier->Wait(); } } const GeometricVerifierOptions& GeometricVerifierController::Options() const { return options_; } GeometricVerifierOptions& GeometricVerifierController::Options() { return options_; } bool GeometricVerifierController::Setup() { for (auto& verifier : verifiers_) { verifier->Start(); } is_setup_ = true; return true; } void GeometricVerifierController::Verify( const std::vector>& image_pairs) { THROW_CHECK_NOTNULL(cache_); THROW_CHECK(is_setup_); if (image_pairs.empty()) { return; } ////////////////////////////////////////////////////////////////////////////// // Verify the matches from the image pairs ////////////////////////////////////////////////////////////////////////////// std::unordered_set image_pair_ids; image_pair_ids.reserve(image_pairs.size()); size_t num_outputs = 0; for (const auto& [image_id1, image_id2] : image_pairs) { // Avoid self-matches. if (image_id1 == image_id2) { continue; } // Avoid duplicate image pairs. const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); if (!image_pair_ids.insert(pair_id).second) { continue; } const bool exists_matches = cache_->ExistsMatches(image_id1, image_id2); const bool exists_inlier_matches = cache_->ExistsInlierMatches(image_id1, image_id2); if (exists_matches && exists_inlier_matches) { continue; } num_outputs += 1; // If only one of the matches or inlier matches exist, we recompute them // from scratch and delete the existing results. This must be done before // pushing the jobs to the queue, otherwise database constraints might fail // when writing an existing result into the database. if (exists_inlier_matches) { cache_->DeleteTwoViewGeometry(image_id1, image_id2); } FeatureMatcherData data; data.image_id1 = image_id1; data.image_id2 = image_id2; if (exists_matches) { data.matches = cache_->GetMatches(image_id1, image_id2); // There exists a two view geometry without inlier matches. if (cache_->ExistsTwoViewGeometry(image_id1, image_id2)) { data.two_view_geometry = cache_->GetTwoViewGeometry(image_id1, image_id2); } THROW_CHECK(verifier_queue_.Push(std::move(data))); } } ////////////////////////////////////////////////////////////////////////////// // Write results to database ////////////////////////////////////////////////////////////////////////////// for (size_t i = 0; i < num_outputs; ++i) { auto output_job = output_queue_.Pop(); THROW_CHECK(output_job.IsValid()); auto& output = output_job.Data(); if (output.matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.matches = {}; } if (output.two_view_geometry.inlier_matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.two_view_geometry = TwoViewGeometry(); } if (cache_->ExistsTwoViewGeometry(output.image_id1, output.image_id2)) { cache_->DeleteTwoViewGeometry(output.image_id1, output.image_id2); } cache_->WriteTwoViewGeometry( output.image_id1, output.image_id2, output.two_view_geometry); } THROW_CHECK_EQ(output_queue_.Size(), 0); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching_utils.h000066400000000000000000000120701517363634500243150ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/feature_matching.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/matcher.h" #include "colmap/util/opengl_utils.h" #include "colmap/util/threading.h" #include #include namespace colmap { struct FeatureMatcherData { image_t image_id1 = kInvalidImageId; image_t image_id2 = kInvalidImageId; FeatureMatches matches; TwoViewGeometry two_view_geometry; }; class FeatureMatcherWorker : public Thread { public: using Input = FeatureMatcherData; using Output = FeatureMatcherData; FeatureMatcherWorker(const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::shared_ptr& cache, JobQueue* input_queue, JobQueue* output_queue); private: void Run() override; FeatureMatchingOptions matching_options_; TwoViewGeometryOptions geometry_options_; std::shared_ptr cache_; JobQueue* input_queue_; JobQueue* output_queue_; std::unique_ptr opengl_context_; }; // Multi-threaded and multi-GPU SIFT feature matcher, which writes the computed // results to the database and skips already matched image pairs. To improve // performance of the matching by taking advantage of caching and database // transactions, pass multiple images to the `Match` function. Note that the // database should be in an active transaction while calling `Match`. class FeatureMatcherController { public: FeatureMatcherController(const FeatureMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr cache); ~FeatureMatcherController(); // Setup the matchers and return if successful. bool Setup(); // Match one batch of multiple image pairs. void Match(const std::vector>& image_pairs); private: FeatureMatchingOptions matching_options_; TwoViewGeometryOptions geometry_options_; std::shared_ptr cache_; bool is_setup_; std::vector> matchers_; std::vector> guided_matchers_; std::vector> verifiers_; JobQueue matcher_queue_; JobQueue verifier_queue_; JobQueue guided_matcher_queue_; JobQueue output_queue_; }; class GeometricVerifierController { public: GeometricVerifierController(const GeometricVerifierOptions& verifier_options, const TwoViewGeometryOptions& geometry_options, std::shared_ptr cache); const GeometricVerifierOptions& Options() const; GeometricVerifierOptions& Options(); ~GeometricVerifierController(); // Setup the verifiers and return if successful. bool Setup(); // Verify one batch of multiple image pairs. void Verify(const std::vector>& image_pairs); private: TwoViewGeometryOptions geometry_options_; std::shared_ptr cache_; GeometricVerifierOptions options_; bool is_setup_; std::vector> verifiers_; JobQueue verifier_queue_; JobQueue output_queue_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/feature_matching_utils_test.cc000066400000000000000000000273731517363634500255260ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/feature_matching_utils.h" #include "colmap/controllers/matcher_cache.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { struct TestData { std::filesystem::path test_dir; std::shared_ptr database; std::shared_ptr cache; std::vector image_ids; }; TestData CreateTestData(int num_images) { TestData data; data.test_dir = CreateTestDir(); const auto database_path = data.test_dir / "database.db"; data.database = Database::Open(database_path); Reconstruction reconstruction; SyntheticDatasetOptions options; options.num_rigs = num_images; options.num_cameras_per_rig = 1; options.num_frames_per_rig = 1; options.num_points3D = 20; options.num_points2D_without_point3D = 3; SynthesizeDataset(options, &reconstruction, data.database.get()); data.cache = std::make_shared(100, data.database); data.image_ids = data.cache->GetImageIds(); return data; } FeatureMatchingOptions DefaultMatchingOptions() { FeatureMatchingOptions options; options.use_gpu = false; options.num_threads = 1; return options; } std::vector> AllPairs( const std::vector& image_ids) { std::vector> pairs; for (size_t i = 0; i < image_ids.size(); ++i) { for (size_t j = i + 1; j < image_ids.size(); ++j) { pairs.emplace_back(image_ids[i], image_ids[j]); } } return pairs; } // Match pairs without geometric verification, then clear TVGs. // Leaves matches in the database ready for a GeometricVerifierController. void MatchPairsWithoutVerification( TestData& data, const std::vector>& pairs) { data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); matching_options.skip_geometric_verification = true; TwoViewGeometryOptions geometry_options; FeatureMatcherController matcher( matching_options, geometry_options, data.cache); ASSERT_TRUE(matcher.Setup()); matcher.Match(pairs); data.database->ClearTwoViewGeometries(); } TEST(FeatureMatcherController, MatchEmptyPairs) { auto data = CreateTestData(3); data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); // Matching empty pairs should return without error controller.Match({}); EXPECT_EQ(data.database->ReadAllMatches().size(), 0); } TEST(FeatureMatcherController, MatchSkipsSelfMatches) { auto data = CreateTestData(3); data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); // Self-match pairs should be skipped std::vector> pairs; pairs.reserve(data.image_ids.size()); for (const auto id : data.image_ids) { pairs.emplace_back(id, id); } controller.Match(pairs); EXPECT_EQ(data.database->ReadAllMatches().size(), 0); } TEST(FeatureMatcherController, MatchSkipsDuplicatePairs) { auto data = CreateTestData(3); data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); ASSERT_GE(data.image_ids.size(), 2); const image_t id1 = data.image_ids[0]; const image_t id2 = data.image_ids[1]; // Submit same pair multiple times — should only process once controller.Match({{id1, id2}, {id1, id2}, {id1, id2}}); const auto matches = data.database->ReadAllMatches(); EXPECT_EQ(matches.size(), 1); } TEST(FeatureMatcherController, MatchSkipsExistingResults) { auto data = CreateTestData(3); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); ASSERT_GE(data.image_ids.size(), 2); const image_t id1 = data.image_ids[0]; const image_t id2 = data.image_ids[1]; // Clear and match once data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); controller.Match({{id1, id2}}); const auto matches_before = data.database->ReadAllMatches(); const auto tvg_before = data.database->ReadTwoViewGeometries(); EXPECT_EQ(matches_before.size(), 1); EXPECT_EQ(tvg_before.size(), 1); // Match same pair again — should skip since both matches and TVG exist controller.Match({{id1, id2}}); const auto matches_after = data.database->ReadAllMatches(); const auto tvg_after = data.database->ReadTwoViewGeometries(); EXPECT_EQ(matches_after.size(), matches_before.size()); EXPECT_EQ(tvg_after.size(), tvg_before.size()); // Match with reversed pair — should also be skipped controller.Match({{id2, id1}}); const auto matches_reversed = data.database->ReadAllMatches(); const auto tvg_reversed = data.database->ReadTwoViewGeometries(); EXPECT_EQ(matches_reversed.size(), matches_before.size()); EXPECT_EQ(tvg_reversed.size(), tvg_before.size()); } TEST(FeatureMatcherController, MatchMultiplePairs) { auto data = CreateTestData(4); data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); // Match all pairs const auto pairs = AllPairs(data.image_ids); controller.Match(pairs); // 4 choose 2 = 6 pairs EXPECT_EQ(data.database->ReadAllMatches().size(), 6); EXPECT_EQ(data.database->ReadTwoViewGeometries().size(), 6); } TEST(FeatureMatcherController, MatchSkipGeometricVerification) { auto data = CreateTestData(3); data.database->ClearMatches(); data.database->ClearTwoViewGeometries(); FeatureMatchingOptions matching_options = DefaultMatchingOptions(); matching_options.skip_geometric_verification = true; TwoViewGeometryOptions geometry_options; FeatureMatcherController controller( matching_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); ASSERT_GE(data.image_ids.size(), 2); controller.Match({{data.image_ids[0], data.image_ids[1]}}); // Matches should be written even without geometric verification EXPECT_EQ(data.database->ReadAllMatches().size(), 1); // Verify geometric verification was skipped: TVG should have UNDEFINED config const auto tvg = data.database->ReadTwoViewGeometry(data.image_ids[0], data.image_ids[1]); EXPECT_EQ(tvg.config, TwoViewGeometry::UNDEFINED); EXPECT_TRUE(tvg.inlier_matches.empty()); } TEST(GeometricVerifierController, OptionsAccessor) { auto data = CreateTestData(3); GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; GeometricVerifierController controller( verifier_options, geometry_options, data.cache); EXPECT_EQ(controller.Options().num_threads, 1); controller.Options().num_threads = 2; EXPECT_EQ(controller.Options().num_threads, 2); } TEST(GeometricVerifierController, VerifyEmptyPairs) { auto data = CreateTestData(3); data.database->ClearTwoViewGeometries(); GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; GeometricVerifierController controller( verifier_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); // Verifying empty pairs should return without error controller.Verify({}); EXPECT_EQ(data.database->ReadTwoViewGeometries().size(), 0); } TEST(GeometricVerifierController, VerifySkipsSelfMatches) { auto data = CreateTestData(3); data.database->ClearTwoViewGeometries(); GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; GeometricVerifierController controller( verifier_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); std::vector> pairs; pairs.reserve(data.image_ids.size()); for (const auto id : data.image_ids) { pairs.emplace_back(id, id); } controller.Verify(pairs); EXPECT_EQ(data.database->ReadTwoViewGeometries().size(), 0); } TEST(GeometricVerifierController, VerifySkipsDuplicatePairs) { auto data = CreateTestData(3); ASSERT_GE(data.image_ids.size(), 2); MatchPairsWithoutVerification(data, {{data.image_ids[0], data.image_ids[1]}}); GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; GeometricVerifierController controller( verifier_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); const image_t id1 = data.image_ids[0]; const image_t id2 = data.image_ids[1]; // Submit same pair multiple times — should only process once controller.Verify({{id1, id2}, {id1, id2}, {id1, id2}}); const auto tvgs = data.database->ReadTwoViewGeometries(); EXPECT_EQ(tvgs.size(), 1); } TEST(GeometricVerifierController, VerifyWithExistingMatches) { auto data = CreateTestData(4); const auto pairs = AllPairs(data.image_ids); MatchPairsWithoutVerification(data, pairs); GeometricVerifierOptions verifier_options; verifier_options.num_threads = 1; TwoViewGeometryOptions geometry_options; GeometricVerifierController controller( verifier_options, geometry_options, data.cache); ASSERT_TRUE(controller.Setup()); controller.Verify(pairs); // All 6 pairs should now have TVGs EXPECT_EQ(data.database->ReadTwoViewGeometries().size(), 6); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/global_pipeline.cc000066400000000000000000000124761517363634500230650ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/global_pipeline.h" #include "colmap/estimators/alignment.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/scene/database_cache.h" #include "colmap/sfm/global_mapper.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" namespace colmap { namespace { constexpr double kMinPriorFocalLengthRatio = 0.5; bool HasInsufficientPriorFocalLengths(const DatabaseCache& database_cache) { const auto& cameras = database_cache.Cameras(); if (cameras.empty()) { return false; } const size_t num_with_prior = std::count_if(cameras.begin(), cameras.end(), [](const auto& camera) { return camera.second.has_prior_focal_length; }); return num_with_prior < kMinPriorFocalLengthRatio * cameras.size(); } void WarnInsufficientPriorFocalLengths() { LOG(WARNING) << "Less than " << kMinPriorFocalLengthRatio * 100 << "% of cameras have prior focal lengths. The " "global mapper depends on reasonably good focal length " "priors to perform well. Consider running " "'colmap view_graph_calibrator' before 'colmap " "global_mapper' or providing camera calibrations " "manually."; } } // namespace GlobalPipeline::GlobalPipeline( GlobalPipelineOptions options, std::shared_ptr database, std::shared_ptr reconstruction_manager) : options_(std::move(options)), reconstruction_manager_( std::move(THROW_CHECK_NOTNULL(reconstruction_manager))) { THROW_CHECK_NOTNULL(database); // Create database cache with relative poses for pose graph. DatabaseCache::Options database_cache_options; database_cache_options.min_num_matches = options_.min_num_matches; database_cache_options.ignore_watermarks = options_.ignore_watermarks; database_cache_options.image_names = {options_.image_names.begin(), options_.image_names.end()}; database_cache_ = DatabaseCache::Create(*database, database_cache_options); if (options_.decompose_relative_pose) { MaybeDecomposeRelativePoses(database_cache_.get()); } } void GlobalPipeline::Run() { const bool has_insufficient_prior_focal_lengths = HasInsufficientPriorFocalLengths(*database_cache_); if (has_insufficient_prior_focal_lengths) { WarnInsufficientPriorFocalLengths(); } auto reconstruction = std::make_shared(); // Prepare mapper options with top-level options. GlobalMapperOptions mapper_options = options_.mapper; mapper_options.image_path = options_.image_path; mapper_options.num_threads = options_.num_threads; mapper_options.random_seed = options_.random_seed; GlobalMapper global_mapper(database_cache_); global_mapper.BeginReconstruction(reconstruction); Timer run_timer; run_timer.Start(); global_mapper.Solve(mapper_options); LOG(INFO) << "Reconstruction done in " << run_timer.ElapsedSeconds() << " seconds"; // Align reconstruction to the original metric scales in rig extrinsics. AlignReconstructionToOrigRigScales(database_cache_->Rigs(), reconstruction.get()); // Output the reconstruction. Reconstruction& output_reconstruction = *reconstruction_manager_->Get(reconstruction_manager_->Add()); output_reconstruction = *reconstruction; if (!options_.image_path.empty()) { LOG(INFO) << "Extracting colors ..."; output_reconstruction.ExtractColorsForAllImages(options_.image_path); } if (has_insufficient_prior_focal_lengths) { // Intentionally logging this warning before and after the reconstruction // to make sure it is not missed. WarnInsufficientPriorFocalLengths(); } } } // namespace colmap colmap-4.0.4/src/colmap/controllers/global_pipeline.h000066400000000000000000000060001517363634500227110ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/scene/reconstruction_manager.h" #include "colmap/sfm/global_mapper.h" #include "colmap/util/base_controller.h" #include #include #include namespace colmap { struct GlobalPipelineOptions { // The minimum number of matches for inlier matches to be considered. int min_num_matches = 15; // Whether to ignore the inlier matches of watermark image pairs. bool ignore_watermarks = false; // Names of images to reconstruct. If empty, all images are used. std::vector image_names; // The image path at which to find the images to extract point colors. std::filesystem::path image_path; // Number of threads for parallel processing. int num_threads = -1; // Random seed for reproducibility. int random_seed = -1; // Whether to decompose relative poses from two-view geometries. bool decompose_relative_pose = true; // Options for the global mapper. GlobalMapperOptions mapper; }; class GlobalPipeline : public BaseController { public: GlobalPipeline(GlobalPipelineOptions options, std::shared_ptr database, std::shared_ptr reconstruction_manager); void Run() override; private: const GlobalPipelineOptions options_; std::shared_ptr database_cache_; std::shared_ptr reconstruction_manager_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/global_pipeline_test.cc000066400000000000000000000216161517363634500241200ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/global_pipeline.h" #include "colmap/estimators/view_graph_calibration.h" #include "colmap/math/random.h" #include "colmap/scene/database.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { // TODO(jsch): Create parameterized tests for the different mapper // implementations (incremental, hierarchical, global) TEST(GlobalPipeline, Nominal) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); GlobalPipelineOptions options; ViewGraphCalibrationOptions vgc_options; CalibrateViewGraph(vgc_options, database.get()); GlobalPipeline mapper(std::move(options), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(GlobalPipeline, SfMWithRandomSeedStability) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 4; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto run_mapper = [&](int num_threads, int random_seed) { GlobalPipelineOptions options; options.num_threads = num_threads; options.random_seed = random_seed; ViewGraphCalibrationOptions vgc_options; vgc_options.random_seed = random_seed; vgc_options.solver_options.num_threads = num_threads; CalibrateViewGraph(vgc_options, database.get()); auto reconstruction_manager = std::make_shared(); GlobalPipeline mapper(std::move(options), database, reconstruction_manager); mapper.Run(); EXPECT_EQ(reconstruction_manager->Size(), 1); return reconstruction_manager; }; constexpr int kRandomSeed = 42; // Single-threaded execution. { auto reconstruction_manager0 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); auto reconstruction_manager1 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); EXPECT_THAT(*reconstruction_manager0->Get(0), ReconstructionEq(*reconstruction_manager1->Get(0))); } // Multi-threaded execution. { auto reconstruction_manager0 = run_mapper(/*num_threads=*/3, /*random_seed=*/kRandomSeed); auto reconstruction_manager1 = run_mapper(/*num_threads=*/3, /*random_seed=*/kRandomSeed); // Same seed should produce similar results, up to floating-point variations // in optimization. EXPECT_THAT(*reconstruction_manager0->Get(0), ReconstructionNear(*reconstruction_manager1->Get(0), /*max_rotation_error_deg=*/1e-9, /*max_proj_center_error=*/1e-9, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.01, /*align=*/false)); } } TEST(GlobalPipeline, WithExistingRelativePoses) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.two_view_geometry_has_relative_pose = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); GlobalPipelineOptions options; ViewGraphCalibrationOptions vgc_options; CalibrateViewGraph(vgc_options, database.get()); GlobalPipeline mapper(std::move(options), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } // To test relative pose re-estimation from view graph calibration. TEST(GlobalPipeline, WithNoisyExistingRelativePoses) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.two_view_geometry_has_relative_pose = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); // Replace relative poses with completely random values. for (auto& [pair_id, two_view_geometry] : database->ReadTwoViewGeometries()) { if (!two_view_geometry.cam2_from_cam1.has_value()) { continue; } two_view_geometry.cam2_from_cam1->rotation() = Eigen::Quaterniond::UnitRandom(); two_view_geometry.cam2_from_cam1->translation() = Eigen::Vector3d::Random().normalized(); const auto [image_id1, image_id2] = PairIdToImagePair(pair_id); database->UpdateTwoViewGeometry(image_id1, image_id2, two_view_geometry); } auto reconstruction_manager = std::make_shared(); GlobalPipelineOptions options; ViewGraphCalibrationOptions vgc_options; CalibrateViewGraph(vgc_options, database.get()); GlobalPipeline mapper(std::move(options), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); // Expect slightly worse accuracy due to noisy input poses. EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/hierarchical_pipeline.cc000066400000000000000000000263601517363634500242400ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/hierarchical_pipeline.h" #include "colmap/scene/database.h" #include "colmap/scene/scene_clustering.h" #include "colmap/sfm/observation_manager.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" #include "colmap/util/timer.h" namespace colmap { namespace { void MergeClusters(const SceneClustering::Cluster& cluster, std::unordered_map>* reconstruction_managers) { // Extract all reconstructions from all child clusters. std::vector> reconstructions; for (const auto& child_cluster : cluster.child_clusters) { if (!child_cluster.child_clusters.empty()) { MergeClusters(child_cluster, reconstruction_managers); } auto& reconstruction_manager = reconstruction_managers->at(&child_cluster); for (size_t i = 0; i < reconstruction_manager->Size(); ++i) { reconstructions.push_back(reconstruction_manager->Get(i)); } } // Try to merge all child cluster reconstruction. while (reconstructions.size() > 1) { bool merge_success = false; for (size_t i = 0; i < reconstructions.size(); ++i) { const int num_reg_images_i = reconstructions[i]->NumRegImages(); for (size_t j = 0; j < i; ++j) { const double kMaxReprojError = 8.0; const int num_reg_images_j = reconstructions[j]->NumRegImages(); if (MergeAndFilterReconstructions( kMaxReprojError, *reconstructions[j], *reconstructions[i])) { LOG(INFO) << StringPrintf( "=> Merged clusters with %d and %d images into %d images", num_reg_images_i, num_reg_images_j, reconstructions[i]->NumRegImages()); reconstructions.erase(reconstructions.begin() + j); merge_success = true; break; } } if (merge_success) { break; } } if (!merge_success) { break; } } // Insert a new reconstruction manager for merged cluster. auto& reconstruction_manager = (*reconstruction_managers)[&cluster]; reconstruction_manager = std::make_shared(); for (const auto& reconstruction : reconstructions) { reconstruction_manager->Get(reconstruction_manager->Add()) = reconstruction; } // Delete all merged child cluster reconstruction managers. for (const auto& child_cluster : cluster.child_clusters) { reconstruction_managers->erase(&child_cluster); } } } // namespace bool HierarchicalPipeline::Options::Check() const { CHECK_OPTION_GT(init_num_trials, -1); CHECK_OPTION_GE(num_threads, -1); CHECK_OPTION_GE(num_workers, -1); clustering_options.Check(); THROW_CHECK_EQ(clustering_options.branching, 2); incremental_options.Check(); return true; } HierarchicalPipeline::HierarchicalPipeline( const Options& options, std::shared_ptr database, std::shared_ptr reconstruction_manager) : options_(options), reconstruction_manager_( std::move(THROW_CHECK_NOTNULL(reconstruction_manager))) { THROW_CHECK(options_.Check()); THROW_CHECK_NOTNULL(database); LOG(INFO) << "Loading database"; Timer timer; timer.Start(); DatabaseCache::Options database_cache_options; database_cache_options.min_num_matches = static_cast(options_.incremental_options.min_num_matches); database_cache_options.ignore_watermarks = options_.incremental_options.ignore_watermarks; database_cache_ = DatabaseCache::Create(*database, database_cache_options); timer.PrintMinutes(); if (options_.incremental_options.ba_refine_sensor_from_rig) { LOG(WARNING) << "The hierarchical reconstruction pipeline currently does not work " "robustly when refining the rig extrinsics, because overlapping " "frames in different child clusters are optimized independently and " "can thus diverge significantly. The merging of clusters oftentimes " "fails in these cases."; } } void HierarchicalPipeline::Run() { LOG_HEADING1("Partitioning scene"); Timer run_timer; run_timer.Start(); ////////////////////////////////////////////////////////////////////////////// // Cluster scene graph ////////////////////////////////////////////////////////////////////////////// std::unordered_map image_id_to_name; image_id_to_name.reserve(database_cache_->NumImages()); for (const auto& [image_id, image] : database_cache_->Images()) { image_id_to_name.emplace(image_id, image.Name()); } SceneClustering scene_clustering = SceneClustering::Create(options_.clustering_options, *database_cache_); auto leaf_clusters = scene_clustering.GetLeafClusters(); size_t total_num_images = 0; for (size_t i = 0; i < leaf_clusters.size(); ++i) { total_num_images += leaf_clusters[i]->image_ids.size(); LOG(INFO) << StringPrintf(" Cluster %d with %d images", i + 1, leaf_clusters[i]->image_ids.size()); } LOG(INFO) << StringPrintf("Clusters have %d images", total_num_images); ////////////////////////////////////////////////////////////////////////////// // Reconstruct clusters ////////////////////////////////////////////////////////////////////////////// LOG_HEADING1("Reconstructing clusters"); // Determine the number of workers and threads per worker. The total thread // budget is divided across workers to avoid oversubscription. if (options_.incremental_options.num_threads > 0) { LOG(WARNING) << "Mapper.num_threads is ignored in hierarchical mapping. Use " "num_threads to control the total thread budget instead."; } const int num_total_threads = GetEffectiveNumThreads(options_.num_threads); const int kDefaultNumWorkers = 8; const int num_eff_workers = std::max( 1, std::min(static_cast(leaf_clusters.size()), std::min(options_.num_workers < 1 ? kDefaultNumWorkers : options_.num_workers, num_total_threads))); const int num_threads_per_worker = std::max(1, num_total_threads / num_eff_workers); // Function to reconstruct one cluster using incremental mapping. auto ReconstructCluster = [this, &image_id_to_name, num_threads_per_worker]( const SceneClustering::Cluster& cluster, std::shared_ptr reconstruction_manager) { if (cluster.image_ids.empty()) { return; } auto incremental_options = std::make_shared( options_.incremental_options); incremental_options->image_path = options_.image_path; incremental_options->max_model_overlap = 3; incremental_options->init_num_trials = options_.init_num_trials; incremental_options->num_threads = num_threads_per_worker; std::unordered_set cluster_image_names; cluster_image_names.reserve(cluster.image_ids.size()); for (const image_t image_id : cluster.image_ids) { cluster_image_names.insert(image_id_to_name.at(image_id)); } // Create a filtered database cache for this cluster. DatabaseCache::Options cluster_cache_options; cluster_cache_options.min_num_matches = static_cast(options_.incremental_options.min_num_matches); cluster_cache_options.image_names = cluster_image_names; auto cluster_database_cache = DatabaseCache::CreateFromCache( *database_cache_, cluster_cache_options); IncrementalPipeline mapper(std::move(incremental_options), std::move(cluster_database_cache), std::move(reconstruction_manager)); mapper.Run(); }; // Start reconstructing the bigger clusters first for better resource usage. // NOLINTNEXTLINE(bugprone-nondeterministic-pointer-iteration-order) std::sort(leaf_clusters.begin(), leaf_clusters.end(), [](const SceneClustering::Cluster* cluster1, const SceneClustering::Cluster* cluster2) { return cluster1->image_ids.size() > cluster2->image_ids.size(); }); // Start the reconstruction workers. Use a separate reconstruction manager per // thread to avoid race conditions. std::unordered_map> reconstruction_managers; reconstruction_managers.reserve(leaf_clusters.size()); ThreadPool thread_pool(num_eff_workers); for (const auto& cluster : leaf_clusters) { reconstruction_managers[cluster] = std::make_shared(); thread_pool.AddTask( ReconstructCluster, *cluster, reconstruction_managers[cluster]); } thread_pool.Wait(); ////////////////////////////////////////////////////////////////////////////// // Merge clusters ////////////////////////////////////////////////////////////////////////////// if (leaf_clusters.size() > 1) { LOG_HEADING1("Merging clusters"); MergeClusters(*scene_clustering.GetRootCluster(), &reconstruction_managers); } THROW_CHECK_EQ(reconstruction_managers.size(), 1); THROW_CHECK_GT( reconstruction_managers.begin()->second->Get(0)->NumRegImages(), 0); *reconstruction_manager_ = *reconstruction_managers.begin()->second; for (size_t i = 0; i < reconstruction_manager_->Size(); ++i) { auto reconstruction = reconstruction_manager_->Get(i); reconstruction->UpdatePoint3DErrors(); } run_timer.PrintMinutes(); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/hierarchical_pipeline.h000066400000000000000000000066431517363634500241040ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/incremental_pipeline.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/scene/scene_clustering.h" #include "colmap/util/base_controller.h" #include #include namespace colmap { // Hierarchical mapping first hierarchically partitions the scene into multiple // overlapping clusters, then reconstructs them separately using incremental // mapping, and finally merges them all into a globally consistent // reconstruction. This is especially useful for larger-scale scenes, since // incremental mapping becomes slow with an increasing number of images. class HierarchicalPipeline : public BaseController { public: struct Options { // The image path at which to find the images to extract point colors. // If not specified, all point colors will be black. std::filesystem::path image_path; // The maximum number of trials to initialize a cluster. int init_num_trials = 10; // The total number of threads for the hierarchical pipeline. This budget // is divided across workers to avoid thread oversubscription. int num_threads = -1; // The number of workers used to reconstruct clusters in parallel. int num_workers = -1; // Options for clustering the scene graph. SceneClustering::Options clustering_options; // Options used to reconstruction each cluster individually. IncrementalPipelineOptions incremental_options; bool Check() const; }; HierarchicalPipeline( const Options& options, std::shared_ptr database, std::shared_ptr reconstruction_manager); void Run() override; private: const Options options_; std::shared_ptr database_cache_; std::shared_ptr reconstruction_manager_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/hierarchical_pipeline_test.cc000066400000000000000000000231521517363634500252730ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/hierarchical_pipeline.h" #include "colmap/estimators/alignment.h" #include "colmap/scene/database.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { void ExpectEqualReconstructions(const Reconstruction& gt, const Reconstruction& computed, const double max_rotation_error_deg, const double max_proj_center_error, const double num_obs_tolerance) { EXPECT_EQ(computed.NumCameras(), gt.NumCameras()); EXPECT_EQ(computed.NumImages(), gt.NumImages()); EXPECT_EQ(computed.NumRegImages(), gt.NumRegImages()); EXPECT_GE(computed.ComputeNumObservations(), (1 - num_obs_tolerance) * gt.ComputeNumObservations()); Sim3d gt_from_computed; ASSERT_TRUE(AlignReconstructionsViaProjCenters(computed, gt, /*max_proj_center_error=*/0.1, >_from_computed)); const std::vector errors = ComputeImageAlignmentError(computed, gt, gt_from_computed); EXPECT_EQ(errors.size(), gt.NumImages()); for (const auto& error : errors) { EXPECT_LT(error.rotation_error_deg, max_rotation_error_deg); EXPECT_LT(error.proj_center_error, max_proj_center_error); } } TEST(HierarchicalPipeline, WithoutNoise) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 20; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); HierarchicalPipeline::Options mapper_options; mapper_options.clustering_options.leaf_max_num_images = 5; mapper_options.clustering_options.image_overlap = 3; HierarchicalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } TEST(HierarchicalPipeline, WithoutNoiseAndNonTrivialFrames) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0.05; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); HierarchicalPipeline::Options mapper_options; mapper_options.clustering_options.leaf_max_num_images = 10; mapper_options.clustering_options.image_overlap = 3; // Note that the hierarchical mapper does not work well when the // sensor_from_rig poses are inconsistently refined in different clusters, // because then the merging does not work well. mapper_options.incremental_options.ba_refine_sensor_from_rig = false; HierarchicalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-3, /*num_obs_tolerance=*/0); } TEST(HierarchicalPipeline, WithoutNoiseAndPanoramicNonTrivialFrames) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); HierarchicalPipeline::Options mapper_options; mapper_options.clustering_options.leaf_max_num_images = 10; mapper_options.clustering_options.image_overlap = 3; // Note that the hierarchical mapper does not work well when the // sensor_from_rig poses are inconsistently refined in different clusters, // because then the merging does not work well. mapper_options.incremental_options.ba_refine_sensor_from_rig = false; HierarchicalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-3, /*num_obs_tolerance=*/0); } TEST(HierarchicalPipeline, MultiReconstruction) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction1; Reconstruction gt_reconstruction2; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset( synthetic_dataset_options, >_reconstruction1, database.get()); synthetic_dataset_options.num_frames_per_rig = 4; SynthesizeDataset( synthetic_dataset_options, >_reconstruction2, database.get()); auto reconstruction_manager = std::make_shared(); HierarchicalPipeline::Options mapper_options; mapper_options.clustering_options.leaf_max_num_images = 5; mapper_options.clustering_options.image_overlap = 3; HierarchicalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 2); Reconstruction* computed_reconstruction1 = nullptr; Reconstruction* computed_reconstruction2 = nullptr; if (reconstruction_manager->Get(0)->NumRegImages() == 5) { computed_reconstruction1 = reconstruction_manager->Get(0).get(); computed_reconstruction2 = reconstruction_manager->Get(1).get(); } else { computed_reconstruction1 = reconstruction_manager->Get(1).get(); computed_reconstruction2 = reconstruction_manager->Get(0).get(); } ExpectEqualReconstructions(gt_reconstruction1, *computed_reconstruction1, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); ExpectEqualReconstructions(gt_reconstruction2, *computed_reconstruction2, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/image_reader.cc000066400000000000000000000351721517363634500223420ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/image_reader.h" #include "colmap/sensor/models.h" #include "colmap/util/file.h" #include "colmap/util/misc.h" namespace colmap { bool ImageReaderOptions::Check() const { CHECK_OPTION_GT(default_focal_length_factor, 0.0); CHECK_OPTION(ExistsCameraModelWithName(camera_model)); const CameraModelId model_id = CameraModelNameToId(camera_model); if (!camera_params.empty()) { CHECK_OPTION( CameraModelVerifyParams(model_id, CSVToVector(camera_params))); } return true; } ImageReader::ImageReader(const ImageReaderOptions& options, Database* database) : options_(options), database_(database), image_index_(0) { THROW_CHECK(options_.Check()); // Get a list of all files in the image path, sorted by image name. if (options_.image_names.empty()) { auto image_paths = GetRecursiveFileList(options_.image_path); std::sort(image_paths.begin(), image_paths.end()); options_.image_names.reserve(image_paths.size()); for (const auto& image_path : image_paths) { options_.image_names.push_back( GetNormalizedRelativePath(image_path, options_.image_path)); } } else { if (!std::is_sorted(options_.image_names.begin(), options_.image_names.end())) { std::sort(options_.image_names.begin(), options_.image_names.end()); } } if (static_cast(options_.existing_camera_id) != kInvalidCameraId) { THROW_CHECK(database->ExistsCamera(options_.existing_camera_id)); prev_camera_ = database->ReadCamera(options_.existing_camera_id); if (std::optional rig = database->ReadRigWithSensor(prev_camera_.SensorId()); rig.has_value()) { prev_rig_ = std::move(*rig); } else { // For backwards compatibility with old databases without rigs. prev_rig_.AddRefSensor(prev_camera_.SensorId()); prev_rig_.SetRigId(database_->WriteRig(prev_rig_)); } } else { // Set the manually specified camera parameters. prev_camera_.camera_id = kInvalidCameraId; THROW_CHECK(ExistsCameraModelWithName(options_.camera_model)); prev_camera_.model_id = CameraModelNameToId(options_.camera_model); prev_camera_.params.resize(CameraModelNumParams(prev_camera_.model_id), 0.); if (!options_.camera_params.empty()) { THROW_CHECK(prev_camera_.SetParamsFromString(options_.camera_params)); prev_camera_.has_prior_focal_length = true; } } } ImageReader::Status ImageReader::Next(Rig* rig, Camera* camera, Image* image, PosePrior* pose_prior, Bitmap* bitmap, Bitmap* mask) { THROW_CHECK_NOTNULL(camera); THROW_CHECK_NOTNULL(image); THROW_CHECK_NOTNULL(bitmap); image_index_ += 1; THROW_CHECK_LE(image_index_, options_.image_names.size()); const std::string image_name = options_.image_names.at(image_index_ - 1); const std::filesystem::path image_path = options_.image_path / image_name; DatabaseTransaction database_transaction(database_); ////////////////////////////////////////////////////////////////////////////// // Set the image name. ////////////////////////////////////////////////////////////////////////////// image->SetName(image_name); const std::string image_folder = GetParentDir(image->Name()).string(); ////////////////////////////////////////////////////////////////////////////// // Check if image already read. ////////////////////////////////////////////////////////////////////////////// const bool exists_image = database_->ExistsImageWithName(image->Name()); if (exists_image) { *image = database_->ReadImageWithName(image->Name()).value(); const bool exists_keypoints = database_->ExistsKeypoints(image->ImageId()); const bool exists_descriptors = database_->ExistsDescriptors(image->ImageId()); if (exists_keypoints && exists_descriptors) { return Status::IMAGE_EXISTS; } } ////////////////////////////////////////////////////////////////////////////// // Read image. ////////////////////////////////////////////////////////////////////////////// if (!bitmap->Read(image_path, /*as_rgb=*/options_.as_rgb)) { return Status::BITMAP_ERROR; } ////////////////////////////////////////////////////////////////////////////// // Read mask. ////////////////////////////////////////////////////////////////////////////// if (mask && !options_.mask_path.empty()) { auto mask_path = options_.mask_path / (image->Name() + ".png"); if (!ExistsFile(mask_path)) { bool exists_mask = false; // Try replacing extension with .png const std::string& base_name = image->Name(); const size_t last_dot = base_name.find_last_of('.'); if (last_dot != std::string::npos) { auto alt_mask_path = options_.mask_path / (base_name.substr(0, last_dot) + ".png"); if (ExistsFile(alt_mask_path)) { mask_path = std::move(alt_mask_path); exists_mask = true; } } if (!exists_mask) { LOG(ERROR) << "Mask at " << mask_path << " does not exist."; return Status::MASK_ERROR; } } if (!mask->Read(mask_path, false)) { LOG(ERROR) << "Failed to read invalid mask file at: " << mask_path; return Status::MASK_ERROR; } } ////////////////////////////////////////////////////////////////////////////// // Check for well-formed data. ////////////////////////////////////////////////////////////////////////////// if (exists_image) { Camera current_camera = database_->ReadCamera(image->CameraId()); if (options_.single_camera && prev_camera_.camera_id != kInvalidCameraId && (current_camera.width != prev_camera_.width || current_camera.height != prev_camera_.height)) { return Status::CAMERA_SINGLE_DIM_ERROR; } if (static_cast(bitmap->Width()) != current_camera.width || static_cast(bitmap->Height()) != current_camera.height) { return Status::CAMERA_EXIST_DIM_ERROR; } prev_camera_ = std::move(current_camera); if (std::optional rig = database_->ReadRigWithSensor(prev_camera_.SensorId()); rig.has_value()) { prev_rig_ = std::move(rig.value()); } else { // For backwards compatibility with old databases, we create a rig. prev_rig_ = Rig(); prev_rig_.AddRefSensor(prev_camera_.SensorId()); prev_rig_.SetRigId(database_->WriteRig(prev_rig_)); } } else { ////////////////////////////////////////////////////////////////////////////// // Check image dimensions. ////////////////////////////////////////////////////////////////////////////// if (prev_camera_.camera_id != kInvalidCameraId && ((options_.single_camera && !options_.single_camera_per_folder) || (options_.single_camera_per_folder && image_folder == prev_image_folder_)) && (prev_camera_.width != static_cast(bitmap->Width()) || prev_camera_.height != static_cast(bitmap->Height()))) { return Status::CAMERA_SINGLE_DIM_ERROR; } ////////////////////////////////////////////////////////////////////////////// // Read camera model and check for consistency if it exists ////////////////////////////////////////////////////////////////////////////// const std::optional camera_model = bitmap->ExifCameraModel(); if (camera_model.has_value() && camera_model_to_id_.count(*camera_model) > 0) { Camera camera = database_->ReadCamera(camera_model_to_id_.at(*camera_model)); if (camera.width != static_cast(bitmap->Width()) || camera.height != static_cast(bitmap->Height())) { return Status::CAMERA_EXIST_DIM_ERROR; } prev_camera_ = std::move(camera); if (std::optional rig = database_->ReadRigWithSensor(prev_camera_.SensorId()); rig.has_value()) { prev_rig_ = std::move(rig.value()); } else { // For backwards compatibility with old databases, we create a rig. prev_rig_ = Rig(); prev_rig_.AddRefSensor(prev_camera_.SensorId()); prev_rig_.SetRigId(database_->WriteRig(prev_rig_)); } } ////////////////////////////////////////////////////////////////////////////// // Extract camera model and focal length ////////////////////////////////////////////////////////////////////////////// if (prev_camera_.camera_id == kInvalidCameraId || options_.single_camera_per_image || (!options_.single_camera && !options_.single_camera_per_folder && static_cast(options_.existing_camera_id) == kInvalidCameraId && (!camera_model.has_value() || camera_model_to_id_.count(camera_model.value()) == 0)) || (options_.single_camera_per_folder && image_folders_.count(image_folder) == 0)) { if (options_.camera_params.empty()) { // Extract focal length. const std::optional maybe_focal_length = bitmap->ExifFocalLength(); const double focal_length = maybe_focal_length.value_or( options_.default_focal_length_factor * std::max(bitmap->Width(), bitmap->Height())); prev_camera_ = Camera::CreateFromModelId(prev_camera_.camera_id, prev_camera_.model_id, focal_length, bitmap->Width(), bitmap->Height()); prev_camera_.has_prior_focal_length = maybe_focal_length.has_value(); } prev_camera_.width = static_cast(bitmap->Width()); prev_camera_.height = static_cast(bitmap->Height()); if (!prev_camera_.VerifyParams()) { return Status::CAMERA_PARAM_ERROR; } prev_camera_.camera_id = database_->WriteCamera(prev_camera_); // By default we create a separate rig per camera. Grouping of different // cameras into the same rig is expected to be done with the // "rig_configurator" after feature extraction. if (!database_->ReadRigWithSensor(prev_camera_.SensorId()).has_value()) { prev_rig_ = Rig(); prev_rig_.AddRefSensor(prev_camera_.SensorId()); prev_rig_.SetRigId(database_->WriteRig(prev_rig_)); } if (camera_model.has_value()) { camera_model_to_id_[*camera_model] = prev_camera_.camera_id; } } image->SetCameraId(prev_camera_.camera_id); ////////////////////////////////////////////////////////////////////////////// // Extract GPS data. ////////////////////////////////////////////////////////////////////////////// const std::optional latitude = bitmap->ExifLatitude(); const std::optional longitude = bitmap->ExifLongitude(); const std::optional altitude = bitmap->ExifAltitude(); if (latitude.has_value() && longitude.has_value() && altitude.has_value()) { pose_prior->position = Eigen::Vector3d(*latitude, *longitude, *altitude); pose_prior->coordinate_system = PosePrior::CoordinateSystem::WGS84; } ////////////////////////////////////////////////////////////////////////////// // Extract Gravity from Orientation. ////////////////////////////////////////////////////////////////////////////// const std::optional orientation = bitmap->ExifOrientation(); if (orientation.has_value()) { const auto gravity = GravityFromExifOrientation(orientation.value()); if (gravity.has_value()) { pose_prior->gravity = gravity.value(); } } } *camera = prev_camera_; *rig = prev_rig_; image_folders_.insert(image_folder); prev_image_folder_ = image_folder; return Status::SUCCESS; } size_t ImageReader::NextIndex() const { return image_index_; } size_t ImageReader::NumImages() const { return options_.image_names.size(); } std::string ImageReader::StatusToString(const ImageReader::Status status) { switch (status) { case ImageReader::Status::SUCCESS: return "SUCCESS"; case ImageReader::Status::FAILURE: return "FAILURE: Failed to process the image."; case ImageReader::Status::IMAGE_EXISTS: return "IMAGE_EXISTS: Features for image were already extracted."; case ImageReader::Status::BITMAP_ERROR: return "BITMAP_ERROR: Failed to read the image file format."; case ImageReader::Status::MASK_ERROR: return "MASK_ERROR: Failed to read the mask file."; case ImageReader::Status::CAMERA_SINGLE_DIM_ERROR: return "CAMERA_SINGLE_DIM_ERROR: Single camera specified, but images " "have different dimensions."; case ImageReader::Status::CAMERA_EXIST_DIM_ERROR: return "CAMERA_EXIST_DIM_ERROR: Image previously processed, but current " "image has different dimensions."; case ImageReader::Status::CAMERA_PARAM_ERROR: return "CAMERA_PARAM_ERROR: Camera has invalid parameters."; default: return "Unknown"; } } } // namespace colmap colmap-4.0.4/src/colmap/controllers/image_reader.h000066400000000000000000000120741517363634500222000ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/scene/database.h" #include "colmap/sensor/bitmap.h" #include #include #include #include namespace colmap { struct ImageReaderOptions { // Root path to folder which contains the images. std::filesystem::path image_path; // Optional root path to folder which contains image masks. For a given image, // the corresponding mask must have the same sub-path below this root as the // image has below image_path. The filename must be equal, aside from the // added extension .png. For example, for an image image_path/abc/012.jpg, the // mask would be mask_path/abc/012.jpg.png. No features will be extracted in // regions where the mask image is black (pixel intensity value 0 in // grayscale). std::filesystem::path mask_path; // Optional path to an image file specifying a mask for all images. No // features will be extracted in regions where the mask is black (pixel // intensity value 0 in grayscale). std::filesystem::path camera_mask_path; // Optional list of images to read. The list must contain the relative path // of the images with respect to the image_path. std::vector image_names; // Name of the camera model. std::string camera_model = "SIMPLE_RADIAL"; // Manual specification of camera parameters. If empty, camera parameters // will be extracted from EXIF, i.e. principal point and focal length. std::string camera_params; // Whether to use the same camera for all images. bool single_camera = false; // Whether to use the same camera for all images in the same sub-folder. bool single_camera_per_folder = false; // Whether to use a different camera for each image. bool single_camera_per_image = false; // Whether to explicitly use an existing camera for all images. Note that in // this case the specified camera model and parameters are ignored. int existing_camera_id = kInvalidCameraId; // If camera parameters are not specified manually and the image does not // have focal length EXIF information, the focal length is set to the // value `default_focal_length_factor * max(width, height)`. double default_focal_length_factor = 1.2; // Whether to read images as grayscale or RGB. bool as_rgb = false; bool Check() const; }; // Recursively iterate over the images in a directory. Skips an image if it // already exists in the database. Extracts the camera intrinsics from EXIF and // writes the camera information to the database. class ImageReader { public: enum class Status { FAILURE, SUCCESS, IMAGE_EXISTS, BITMAP_ERROR, MASK_ERROR, CAMERA_SINGLE_DIM_ERROR, CAMERA_EXIST_DIM_ERROR, CAMERA_PARAM_ERROR }; ImageReader(const ImageReaderOptions& options, Database* database); Status Next(Rig* rig, Camera* camera, Image* image, PosePrior* pose_prior, Bitmap* bitmap, Bitmap* mask); size_t NextIndex() const; size_t NumImages() const; static std::string StatusToString(Status status); private: // Image reader options. ImageReaderOptions options_; Database* database_; // Index of previously processed image. size_t image_index_; // Previously processed rig/camera. Rig prev_rig_; Camera prev_camera_; std::unordered_map camera_model_to_id_; // Names of image sub-folders. std::string prev_image_folder_; std::unordered_set image_folders_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/image_reader_test.cc000066400000000000000000000463501517363634500234010ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/image_reader.h" #include "colmap/scene/database_sqlite.h" #include "colmap/sensor/models.h" #include "colmap/util/file.h" #include "colmap/util/testing.h" #include #include #include namespace colmap { namespace { Bitmap CreateTestBitmap(bool as_rgb) { Bitmap bitmap(1, 3, as_rgb); bitmap.SetPixel(0, 0, BitmapColor(1)); bitmap.SetPixel(1, 0, BitmapColor(2)); bitmap.SetPixel(2, 0, BitmapColor(3)); return bitmap; } class ParameterizedImageReaderTests : public ::testing::TestWithParam> {}; TEST_P(ParameterizedImageReaderTests, Nominal) { const auto [kNumImages, kWithMasks, kWithExistingImages, kAsRGB, kExtension] = GetParam(); auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.as_rgb = kAsRGB; CreateDirIfNotExists(options.image_path); if (kWithMasks) { options.mask_path = test_dir / "masks"; CreateDirIfNotExists(options.mask_path); } const Bitmap test_bitmap = CreateTestBitmap(kAsRGB); for (int i = 0; i < kNumImages; ++i) { const std::string stem = std::to_string(i); const std::string image_name = stem + kExtension; test_bitmap.Write(options.image_path / image_name); if (kWithMasks) { if (i == 0) { // append .png to image_name test_bitmap.Write(options.mask_path / (image_name + ".png")); } else { // replace mask extension by .png test_bitmap.Write(options.mask_path / (stem + ".png")); } } if (kWithExistingImages) { Image image; image.SetName(image_name); image.SetCameraId(database->WriteCamera( Camera::CreateFromModelName(i + 1, options.camera_model, /*focal_length=*/1, test_bitmap.Width(), test_bitmap.Height()))); image.SetImageId(database->WriteImage(image)); database->WriteKeypoints(image.ImageId(), FeatureKeypoints()); database->WriteDescriptors(image.ImageId(), FeatureDescriptors()); Rig rig; rig.AddRefSensor(sensor_t(SensorType::CAMERA, image.CameraId())); database->WriteRig(rig); } } ImageReader image_reader(options, database.get()); EXPECT_EQ(image_reader.NumImages(), kNumImages); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; for (int i = 0; i < kNumImages; ++i) { EXPECT_EQ(image_reader.NextIndex(), i); const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); if (kWithExistingImages) { EXPECT_EQ(status, ImageReader::Status::IMAGE_EXISTS); continue; } ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(rig.RigId(), i + 1); EXPECT_EQ(camera.camera_id, i + 1); EXPECT_EQ(camera.ModelName(), options.camera_model); EXPECT_EQ(camera.width, test_bitmap.Width()); EXPECT_EQ(camera.height, test_bitmap.Height()); EXPECT_EQ(image.Name(), std::to_string(i) + kExtension); EXPECT_EQ(bitmap.IsRGB(), kAsRGB); EXPECT_EQ(bitmap.RowMajorData(), test_bitmap.RowMajorData()); if (kWithExistingImages) { EXPECT_EQ(database->NumRigs(), kNumImages); EXPECT_EQ(database->NumCameras(), kNumImages); } else { EXPECT_EQ(database->NumRigs(), i + 1); EXPECT_EQ(database->NumCameras(), i + 1); } } EXPECT_THROW( image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask), std::invalid_argument); EXPECT_EQ(database->NumRigs(), kNumImages); EXPECT_EQ(database->NumCameras(), kNumImages); } INSTANTIATE_TEST_SUITE_P( ImageReaderTests, ParameterizedImageReaderTests, ::testing::Values(std::make_tuple(/*num_images=*/0, /*with_masks=*/false, /*with_existing_images=*/true, /*as_rgb=*/true, /*extension=*/".png"), std::make_tuple(/*num_images=*/5, /*with_masks=*/false, /*with_existing_images=*/false, /*as_rgb=*/true, /*extension=*/".png"), std::make_tuple(/*num_images=*/5, /*with_masks=*/true, /*with_existing_images=*/false, /*as_rgb=*/true, /*extension=*/".png"), std::make_tuple(/*num_images=*/5, /*with_masks=*/true, /*with_existing_images=*/false, /*as_rgb=*/true, /*extension=*/".bmp"), std::make_tuple(/*num_images=*/5, /*with_masks=*/true, /*with_existing_images=*/false, /*as_rgb=*/false, /*extension=*/".png"), std::make_tuple(/*num_images=*/5, /*with_masks=*/false, /*with_existing_images=*/true, /*as_rgb=*/true, /*extension=*/".png"))); TEST(ImageReaderTest, SingleCamera) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.single_camera = true; CreateDirIfNotExists(options.image_path); // Create 3 test images with same dimensions Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "0.png"); test_bitmap.Write(options.image_path / "1.png"); test_bitmap.Write(options.image_path / "2.png"); ImageReader image_reader(options, database.get()); EXPECT_EQ(image_reader.NumImages(), 3); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; for (int i = 0; i < 3; ++i) { const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); } EXPECT_EQ(database->NumRigs(), 1); EXPECT_EQ(database->NumCameras(), 1); } TEST(ImageReaderTest, SingleCameraDimensionError) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.single_camera = true; CreateDirIfNotExists(options.image_path); // Create images with different dimensions Bitmap bitmap1(10, 20, true); Bitmap bitmap2(30, 40, true); bitmap1.Write(options.image_path / "0.png"); bitmap2.Write(options.image_path / "1.png"); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; // First image succeeds auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); // Second image fails due to dimension mismatch status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); EXPECT_EQ(status, ImageReader::Status::CAMERA_SINGLE_DIM_ERROR); } TEST(ImageReaderTest, SingleCameraPerFolder) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.single_camera_per_folder = true; CreateDirIfNotExists(options.image_path); CreateDirIfNotExists(options.image_path / "folder1"); CreateDirIfNotExists(options.image_path / "folder2"); // Create 2 images in each folder Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "folder1" / "0.png"); test_bitmap.Write(options.image_path / "folder1" / "1.png"); test_bitmap.Write(options.image_path / "folder2" / "0.png"); test_bitmap.Write(options.image_path / "folder2" / "1.png"); ImageReader image_reader(options, database.get()); EXPECT_EQ(image_reader.NumImages(), 4); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; std::unordered_map folder_cameras; for (int i = 0; i < 4; ++i) { const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); const std::string folder = GetParentDir(image.Name()).string(); if (folder_cameras.count(folder) == 0) { folder_cameras[folder] = camera.camera_id; } else { EXPECT_EQ(camera.camera_id, folder_cameras[folder]); } } // Should have 2 cameras (one per folder) EXPECT_EQ(database->NumCameras(), 2); } TEST(ImageReaderTest, SingleCameraPerImage) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.single_camera_per_image = true; CreateDirIfNotExists(options.image_path); // Create 3 images with same dimensions Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "0.png"); test_bitmap.Write(options.image_path / "1.png"); test_bitmap.Write(options.image_path / "2.png"); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; for (int i = 0; i < 3; ++i) { const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(camera.camera_id, i + 1); // Each image gets its own camera } // Should have 3 cameras (one per image) EXPECT_EQ(database->NumCameras(), 3); } TEST(ImageReaderTest, ExistingCameraId) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); // Create an existing camera in the database Camera existing_camera; existing_camera.model_id = CameraModelNameToId("SIMPLE_RADIAL"); existing_camera.width = 10; existing_camera.height = 20; existing_camera.params = {1.0, 5.0, 10.0, 0.0}; existing_camera.camera_id = database->WriteCamera(existing_camera); Rig existing_rig; existing_rig.AddRefSensor(existing_camera.SensorId()); database->WriteRig(existing_rig); ImageReaderOptions options; options.image_path = test_dir / "images"; options.existing_camera_id = existing_camera.camera_id; CreateDirIfNotExists(options.image_path); Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "0.png"); test_bitmap.Write(options.image_path / "1.png"); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; for (int i = 0; i < 2; ++i) { const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(camera.camera_id, existing_camera.camera_id); EXPECT_EQ(camera.params, existing_camera.params); } // No new cameras created EXPECT_EQ(database->NumCameras(), 1); } TEST(ImageReaderTest, ManualCameraParams) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.camera_model = "PINHOLE"; options.camera_params = "500.0, 500.0, 320.0, 240.0"; CreateDirIfNotExists(options.image_path); Bitmap test_bitmap(640, 480, true); test_bitmap.Write(options.image_path / "test.png"); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(camera.model_id, PinholeCameraModel::model_id); EXPECT_EQ(camera.params[0], 500.0); EXPECT_EQ(camera.params[1], 500.0); EXPECT_EQ(camera.params[2], 320.0); EXPECT_EQ(camera.params[3], 240.0); EXPECT_TRUE(camera.has_prior_focal_length); } TEST(ImageReaderTest, ExplicitImageNames) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; CreateDirIfNotExists(options.image_path); // Create 5 images Bitmap test_bitmap(10, 20, true); for (int i = 0; i < 5; ++i) { test_bitmap.Write(options.image_path / (std::to_string(i) + ".png")); } // Only select a subset of images options.image_names = {"1.png", "3.png"}; ImageReader image_reader(options, database.get()); EXPECT_EQ(image_reader.NumImages(), 2); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(image.Name(), "1.png"); status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); ASSERT_EQ(status, ImageReader::Status::SUCCESS); EXPECT_EQ(image.Name(), "3.png"); } TEST(ImageReaderTest, BitmapError) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; CreateDirIfNotExists(options.image_path); // Create a file that is not a valid image std::ofstream file(options.image_path / "invalid.png"); file << "not an image"; file.close(); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); EXPECT_EQ(status, ImageReader::Status::BITMAP_ERROR); } TEST(ImageReaderTest, MaskErrorMissing) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.mask_path = test_dir / "masks"; CreateDirIfNotExists(options.image_path); CreateDirIfNotExists(options.mask_path); Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "test.png"); // Don't create mask file ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); EXPECT_EQ(status, ImageReader::Status::MASK_ERROR); } TEST(ImageReaderTest, MaskErrorInvalid) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; options.mask_path = test_dir / "masks"; CreateDirIfNotExists(options.image_path); CreateDirIfNotExists(options.mask_path); Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "test.png"); // Create invalid mask file std::ofstream file(options.mask_path / "test.png.png"); file << "not an image"; file.close(); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); EXPECT_EQ(status, ImageReader::Status::MASK_ERROR); } TEST(ImageReaderTest, ImageExistsWithKeypoints) { auto database = Database::Open(kInMemorySqliteDatabasePath); const auto test_dir = CreateTestDir(); ImageReaderOptions options; options.image_path = test_dir / "images"; CreateDirIfNotExists(options.image_path); Bitmap test_bitmap(10, 20, true); test_bitmap.Write(options.image_path / "test.png"); // Add existing image with keypoints and descriptors Camera existing_camera; existing_camera.model_id = CameraModelNameToId("SIMPLE_RADIAL"); existing_camera.width = 10; existing_camera.height = 20; existing_camera.params = {1.0, 5.0, 10.0, 0.0}; existing_camera.camera_id = database->WriteCamera(existing_camera); Rig existing_rig; existing_rig.AddRefSensor(existing_camera.SensorId()); database->WriteRig(existing_rig); Image existing_image; existing_image.SetName("test.png"); existing_image.SetCameraId(existing_camera.camera_id); existing_image.SetImageId(database->WriteImage(existing_image)); database->WriteKeypoints(existing_image.ImageId(), FeatureKeypoints()); database->WriteDescriptors(existing_image.ImageId(), FeatureDescriptors()); ImageReader image_reader(options, database.get()); Rig rig; Camera camera; Image image; PosePrior pose_prior; Bitmap bitmap; Bitmap mask; const auto status = image_reader.Next(&rig, &camera, &image, &pose_prior, &bitmap, &mask); EXPECT_EQ(status, ImageReader::Status::IMAGE_EXISTS); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/incremental_pipeline.cc000066400000000000000000000761621517363634500241300ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/incremental_pipeline.h" #include "colmap/estimators/alignment.h" #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/scene/database.h" #include "colmap/util/file.h" #include "colmap/util/timer.h" namespace colmap { namespace { void CustomizeIncrementalPipelineOptions(const DatabaseCache& database_cache, IncrementalPipelineOptions& options) { // If the total number of images is small then do not enforce the // minimum model size so that we can reconstruct small image // collections, i.e., if the model is at least half of the total number // of images, we always keep it. options.min_model_size = std::min(0.5 * database_cache.NumImages(), options.min_model_size); } DatabaseCache::Options CreateDatabaseCacheOptions( const IncrementalPipelineOptions& options, const ReconstructionManager& reconstruction_manager) { DatabaseCache::Options database_cache_options; database_cache_options.min_num_matches = static_cast(options.min_num_matches); database_cache_options.ignore_watermarks = options.ignore_watermarks; database_cache_options.image_names = {options.image_names.begin(), options.image_names.end()}; // Make sure images of the given reconstruction are also included when // manually specifying images for the reconstruction procedure. if (reconstruction_manager.Size() == 1 && !options.image_names.empty()) { const auto& reconstruction = reconstruction_manager.Get(0); for (const image_t image_id : reconstruction->RegImageIds()) { const auto& image = reconstruction->Image(image_id); database_cache_options.image_names.insert(image.Name()); } } database_cache_options.load_all_images = options.load_all_images; database_cache_options.convert_pose_priors_to_enu = options.use_prior_position; return database_cache_options; } void IterativeGlobalRefinement(const IncrementalPipelineOptions& options, const IncrementalMapper::Options& mapper_options, IncrementalMapper& mapper) { LOG(INFO) << "Retriangulation and Global bundle adjustment"; mapper.IterativeGlobalRefinement(options.ba_global_max_refinements, options.ba_global_max_refinement_change, mapper_options, options.GlobalBundleAdjustment(), options.Triangulation()); mapper.FilterFrames(mapper_options); } void ExtractColors(const std::filesystem::path& image_path, const image_t image_id, Reconstruction& reconstruction) { if (!reconstruction.ExtractColorsForImage(image_id, image_path)) { LOG(WARNING) << "Could not read image " << reconstruction.Image(image_id).Name() << " at path " << image_path << "."; } } void WriteSnapshot(const Reconstruction& reconstruction, const std::filesystem::path& snapshot_path) { LOG(INFO) << "Creating snapshot"; // Get the current timestamp in milliseconds. const size_t timestamp = std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()) .count(); // Write reconstruction to unique path with current timestamp. const auto path = snapshot_path / StringPrintf("%010zu", timestamp); CreateDirIfNotExists(path); VLOG(1) << "=> Writing to " << path; reconstruction.Write(path); } bool HasUnknownSensorFromRig(const Reconstruction& reconstruction) { std::unordered_set parameterized_rigs; for (const auto& [_, image] : reconstruction.Images()) { parameterized_rigs.insert(image.FramePtr()->RigPtr()); } for (const Rig* rig : parameterized_rigs) { for (const auto& [sensor_id, sensor_from_rig] : rig->NonRefSensors()) { if (sensor_id.type == SensorType::CAMERA && !sensor_from_rig.has_value()) { return true; } } } return false; } } // namespace IncrementalMapper::Options IncrementalPipelineOptions::Mapper() const { IncrementalMapper::Options options = mapper; options.abs_pose_refine_focal_length = ba_refine_focal_length; options.abs_pose_refine_extra_params = ba_refine_extra_params; options.min_focal_length_ratio = min_focal_length_ratio; options.max_focal_length_ratio = max_focal_length_ratio; options.max_extra_param = max_extra_param; options.num_threads = num_threads; options.fix_existing_frames = fix_existing_frames; options.constant_rigs = constant_rigs; options.constant_cameras = constant_cameras; options.use_prior_position = use_prior_position; options.use_robust_loss_on_prior_position = use_robust_loss_on_prior_position; options.prior_position_loss_scale = prior_position_loss_scale; options.random_seed = random_seed; return options; } IncrementalTriangulator::Options IncrementalPipelineOptions::Triangulation() const { IncrementalTriangulator::Options options = triangulation; options.min_focal_length_ratio = min_focal_length_ratio; options.max_focal_length_ratio = max_focal_length_ratio; options.max_extra_param = max_extra_param; options.random_seed = random_seed; return options; } BundleAdjustmentOptions IncrementalPipelineOptions::LocalBundleAdjustment() const { BundleAdjustmentOptions options; options.print_summary = false; options.refine_focal_length = ba_refine_focal_length; options.refine_principal_point = ba_refine_principal_point; options.refine_extra_params = ba_refine_extra_params; options.refine_sensor_from_rig = ba_refine_sensor_from_rig; if (options.ceres) { options.ceres->solver_options.function_tolerance = ba_local_function_tolerance; options.ceres->solver_options.gradient_tolerance = 10.0; options.ceres->solver_options.parameter_tolerance = 0.0; options.ceres->solver_options.max_num_iterations = ba_local_max_num_iterations; options.ceres->solver_options.max_linear_solver_iterations = 100; options.ceres->solver_options.logging_type = ceres::LoggingType::SILENT; options.ceres->solver_options.num_threads = num_threads; #if CERES_VERSION_MAJOR < 2 options.ceres->solver_options.num_linear_solver_threads = num_threads; #endif // CERES_VERSION_MAJOR options.ceres->min_num_residuals_for_cpu_multi_threading = ba_min_num_residuals_for_cpu_multi_threading; options.ceres->loss_function_scale = 1.0; options.ceres->loss_function_type = CeresBundleAdjustmentOptions::LossFunctionType::SOFT_L1; options.ceres->use_gpu = ba_use_gpu; options.ceres->gpu_index = ba_gpu_index; } return options; } BundleAdjustmentOptions IncrementalPipelineOptions::GlobalBundleAdjustment() const { BundleAdjustmentOptions options; options.print_summary = false; options.refine_focal_length = ba_refine_focal_length; options.refine_principal_point = ba_refine_principal_point; options.refine_extra_params = ba_refine_extra_params; options.refine_sensor_from_rig = ba_refine_sensor_from_rig; if (options.ceres) { options.ceres->solver_options.function_tolerance = ba_global_function_tolerance; options.ceres->solver_options.gradient_tolerance = 1.0; options.ceres->solver_options.parameter_tolerance = 0.0; options.ceres->solver_options.max_num_iterations = ba_global_max_num_iterations; options.ceres->solver_options.max_linear_solver_iterations = 100; options.ceres->solver_options.logging_type = ceres::LoggingType::SILENT; if (VLOG_IS_ON(2)) { options.ceres->solver_options.minimizer_progress_to_stdout = true; options.ceres->solver_options.logging_type = ceres::LoggingType::PER_MINIMIZER_ITERATION; } options.ceres->solver_options.num_threads = num_threads; #if CERES_VERSION_MAJOR < 2 options.ceres->solver_options.num_linear_solver_threads = num_threads; #endif // CERES_VERSION_MAJOR options.ceres->min_num_residuals_for_cpu_multi_threading = ba_min_num_residuals_for_cpu_multi_threading; options.ceres->loss_function_type = CeresBundleAdjustmentOptions::LossFunctionType::TRIVIAL; options.ceres->use_gpu = ba_use_gpu; options.ceres->gpu_index = ba_gpu_index; } return options; } bool IncrementalPipelineOptions::Check() const { CHECK_OPTION_GT(min_num_matches, 0); CHECK_OPTION_GT(max_num_models, 0); CHECK_OPTION_GT(max_model_overlap, 0); CHECK_OPTION_GE(min_model_size, 0); CHECK_OPTION_GT(init_num_trials, 0); CHECK_OPTION_GT(min_focal_length_ratio, 0); CHECK_OPTION_GT(max_focal_length_ratio, 0); CHECK_OPTION_GE(max_extra_param, 0); CHECK_OPTION_GE(ba_local_max_num_iterations, 0); CHECK_OPTION_GT(ba_global_frames_ratio, 1.0); CHECK_OPTION_GT(ba_global_points_ratio, 1.0); CHECK_OPTION_GT(ba_global_frames_freq, 0); CHECK_OPTION_GT(ba_global_points_freq, 0); CHECK_OPTION_GT(ba_global_max_num_iterations, 0); CHECK_OPTION_GT(ba_local_max_refinements, 0); CHECK_OPTION_GE(ba_local_max_refinement_change, 0); CHECK_OPTION_GE(ba_global_max_refinements, 0); CHECK_OPTION_GE(ba_global_max_refinement_change, 0); CHECK_OPTION_GE(snapshot_frames_freq, 0); CHECK_OPTION_GT(prior_position_loss_scale, 0.); CHECK_OPTION_GE(num_threads, -1); CHECK_OPTION_GE(random_seed, -1); CHECK_OPTION(Mapper().Check()); CHECK_OPTION(Triangulation().Check()); return true; } IncrementalPipeline::IncrementalPipeline( std::shared_ptr options, std::shared_ptr database, std::shared_ptr reconstruction_manager) : options_(std::move(THROW_CHECK_NOTNULL(options))), reconstruction_manager_( THROW_CHECK_NOTNULL(std::move(reconstruction_manager))), total_run_timer_(std::make_shared()) { THROW_CHECK(options_->Check()); THROW_CHECK_NOTNULL(database); LOG(INFO) << "Loading database"; Timer timer; timer.Start(); database_cache_ = DatabaseCache::Create( *database, CreateDatabaseCacheOptions(*options_, *reconstruction_manager_)); timer.PrintMinutes(); CustomizeIncrementalPipelineOptions(*database_cache_, *options_); RegisterCallbacks(); } IncrementalPipeline::IncrementalPipeline( std::shared_ptr options, std::shared_ptr database_cache, std::shared_ptr reconstruction_manager) : options_(std::move(THROW_CHECK_NOTNULL(options))), reconstruction_manager_( THROW_CHECK_NOTNULL(std::move(reconstruction_manager))), total_run_timer_(std::make_shared()) { THROW_CHECK(options_->Check()); THROW_CHECK_NOTNULL(database_cache); database_cache_ = DatabaseCache::CreateFromCache( *database_cache, CreateDatabaseCacheOptions(*options_, *reconstruction_manager_)); CustomizeIncrementalPipelineOptions(*database_cache_, *options_); RegisterCallbacks(); } void IncrementalPipeline::Run() { total_run_timer_->Start(); if (database_cache_->NumImages() == 0) { LOG(WARNING) << "No images with matches"; return; } if (options_->use_prior_position && database_cache_->NumPosePriors() == 0) { LOG(WARNING) << "No pose priors"; return; } // Is there a sub-reconstruction before we start the reconstruction? I.e. the // user has imported an existing reconstruction. const bool continue_reconstruction = reconstruction_manager_->Size() > 0; THROW_CHECK_LE(reconstruction_manager_->Size(), 1) << "Can only continue from a single reconstruction, " "but multiple are given."; const size_t num_images = database_cache_->NumImages(); IncrementalMapper::Options mapper_options = options_->Mapper(); IncrementalMapper mapper(database_cache_); if (Reconstruct(mapper, mapper_options, /*continue_reconstruction=*/continue_reconstruction) == Status::STOP) { total_run_timer_->PrintMinutes(); return; } auto ShouldStop = [this, &mapper, &num_images]() { return mapper.NumTotalRegImages() == num_images || CheckIfStopped() || CheckReachedMaxRuntime(); }; const size_t kNumInitRelaxations = 2; for (size_t i = 0; i < kNumInitRelaxations; ++i) { if (ShouldStop()) { break; } LOG(INFO) << "=> Relaxing the initialization constraints."; mapper_options.init_min_num_inliers /= 2; mapper.ResetInitializationStats(); if (Reconstruct(mapper, mapper_options, /*continue_reconstruction=*/false) == Status::STOP) { break; } if (ShouldStop()) { break; } LOG(INFO) << "=> Relaxing the initialization constraints."; mapper_options.init_min_tri_angle /= 2; mapper.ResetInitializationStats(); if (Reconstruct(mapper, mapper_options, /*continue_reconstruction=*/false) == Status::STOP) { break; } } total_run_timer_->PrintMinutes(); } IncrementalPipeline::Status IncrementalPipeline::InitializeReconstruction( IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, Reconstruction& reconstruction) { image_t image_id1 = static_cast(options_->init_image_id1); image_t image_id2 = static_cast(options_->init_image_id2); // Try to find good initial pair. Rigid3d cam2_from_cam1; if (!options_->IsInitialPairProvided()) { LOG(INFO) << "Finding good initial image pair"; const bool find_init_success = mapper.FindInitialImagePair( mapper_options, image_id1, image_id2, cam2_from_cam1); if (!find_init_success) { LOG(INFO) << "=> No good initial image pair found."; return Status::NO_INITIAL_PAIR; } } else { if (!reconstruction.ExistsImage(image_id1) || !reconstruction.ExistsImage(image_id2)) { LOG(INFO) << StringPrintf( "=> Initial image pair #%d and #%d does not exist.", image_id1, image_id2); return Status::NO_INITIAL_PAIR; } const bool provided_init_success = mapper.EstimateInitialTwoViewGeometry( mapper_options, image_id1, image_id2, cam2_from_cam1); if (!provided_init_success) { LOG(INFO) << "=> Provided pair is unsuitable for initialization."; return Status::BAD_INITIAL_PAIR; } } LOG(INFO) << StringPrintf( "Registering initial image pair #%d and #%d", image_id1, image_id2); mapper.RegisterInitialImagePair( mapper_options, image_id1, image_id2, cam2_from_cam1); IncrementalTriangulator::Options tri_options = options_->Triangulation(); tri_options.min_angle = mapper_options.init_min_tri_angle; for (const image_t image_id : {image_id1, image_id2}) { const Image& image = reconstruction.Image(image_id); for (const data_t& data_id : image.FramePtr()->ImageIds()) { mapper.TriangulateImage(tri_options, data_id.id); } } if (reconstruction.NumPoints3D() == 0) { return Status::BAD_INITIAL_PAIR; } LOG(INFO) << "Global bundle adjustment"; mapper.AdjustGlobalBundle(mapper_options, options_->GlobalBundleAdjustment()); reconstruction.Normalize(); mapper.FilterPoints(mapper_options); mapper.FilterFrames(mapper_options); // Initial image pair failed to register. if (reconstruction.NumRegFrames() == 0 || reconstruction.NumPoints3D() == 0) { return Status::BAD_INITIAL_PAIR; } // Number of triangulated points not enough for registering future images. if (static_cast(reconstruction.NumPoints3D()) < mapper_options.abs_pose_min_num_inliers) { return Status::BAD_INITIAL_PAIR; } if (options_->extract_colors) { for (const image_t image_id : {image_id1, image_id2}) { const Image& image = reconstruction.Image(image_id); for (const data_t& data_id : image.FramePtr()->ImageIds()) { ExtractColors(options_->image_path, data_id.id, reconstruction); } } } return Status::SUCCESS; } bool IncrementalPipeline::CheckRunGlobalRefinement( const Reconstruction& reconstruction, const size_t ba_prev_num_reg_frames, const size_t ba_prev_num_points) { return reconstruction.NumRegFrames() >= options_->ba_global_frames_ratio * ba_prev_num_reg_frames || reconstruction.NumRegFrames() >= options_->ba_global_frames_freq + ba_prev_num_reg_frames || reconstruction.NumPoints3D() >= options_->ba_global_points_ratio * ba_prev_num_points || reconstruction.NumPoints3D() >= options_->ba_global_points_freq + ba_prev_num_points; } IncrementalPipeline::Status IncrementalPipeline::ReconstructSubModel( IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, const std::shared_ptr& reconstruction) { mapper.BeginReconstruction(reconstruction); if (HasUnknownSensorFromRig(*reconstruction)) { return Status::UNKNOWN_SENSOR_FROM_RIG; } //////////////////////////////////////////////////////////////////////////// // Register initial pair //////////////////////////////////////////////////////////////////////////// if (reconstruction->NumRegFrames() == 0) { const Status init_status = IncrementalPipeline::InitializeReconstruction( mapper, mapper_options, *reconstruction); if (init_status != Status::SUCCESS) { return init_status; } } Callback(INITIAL_IMAGE_PAIR_REG_CALLBACK); //////////////////////////////////////////////////////////////////////////// // Incremental mapping //////////////////////////////////////////////////////////////////////////// size_t snapshot_prev_num_reg_frames = reconstruction->NumRegFrames(); size_t ba_prev_num_reg_frames = reconstruction->NumRegFrames(); size_t ba_prev_num_points = reconstruction->NumPoints3D(); std::vector structure_less_flags; if (options_->structure_less_registration_only) { structure_less_flags = {true}; } else { if (options_->structure_less_registration_fallback) { structure_less_flags = {false, true}; } else { structure_less_flags = {false}; } } bool reg_next_success = true; bool prev_reg_next_success = true; do { if (CheckIfStopped() || CheckReachedMaxRuntime()) { break; } prev_reg_next_success = reg_next_success; reg_next_success = false; image_t next_image_id = kInvalidImageId; // Try to register next image. Always prefer structure-based registration // first, and if that fails, try (less reliable) structure-less // registration. for (const bool structure_less : structure_less_flags) { const std::vector next_images = mapper.FindNextImages( mapper_options, /*structure_less=*/structure_less); for (size_t reg_trial = 0; reg_trial < next_images.size(); ++reg_trial) { next_image_id = next_images[reg_trial]; LOG(INFO) << StringPrintf("Registering image #%d (num_reg_frames=%d)", next_image_id, reconstruction->NumRegFrames()); LOG(INFO) << StringPrintf( "=> Image sees %d / %d points", mapper.ObservationManager().NumVisiblePoints3D(next_image_id), mapper.ObservationManager().NumObservations(next_image_id)); if (structure_less) { LOG(INFO) << StringPrintf( "Registering image with structure-less fallback"); LOG(INFO) << StringPrintf( "=> Image sees %d / %d correspondences", mapper.ObservationManager().NumVisibleCorrespondences( next_image_id), mapper.ObservationManager().NumCorrespondences(next_image_id)); reg_next_success = mapper.RegisterNextStructureLessImage( mapper_options, next_image_id); } else { reg_next_success = mapper.RegisterNextImage(mapper_options, next_image_id); } if (reg_next_success) { break; } else { LOG(INFO) << "=> Could not register, trying another image."; // If initial model fails to continue for some time, // abort and try different initial pair. const size_t kMinNumInitialRegTrials = 30; if (reg_trial >= kMinNumInitialRegTrials && reconstruction->NumRegImages() < static_cast(options_->min_model_size)) { break; } } } if (reg_next_success) { break; } } if (reg_next_success) { const Image& image = reconstruction->Image(next_image_id); for (const data_t& data_id : image.FramePtr()->ImageIds()) { mapper.TriangulateImage(options_->Triangulation(), data_id.id); } mapper.IterativeLocalRefinement(options_->ba_local_max_refinements, options_->ba_local_max_refinement_change, mapper_options, options_->LocalBundleAdjustment(), options_->Triangulation(), next_image_id); if (CheckRunGlobalRefinement( *reconstruction, ba_prev_num_reg_frames, ba_prev_num_points)) { IterativeGlobalRefinement(*options_, mapper_options, mapper); ba_prev_num_points = reconstruction->NumPoints3D(); ba_prev_num_reg_frames = reconstruction->NumRegFrames(); } if (options_->extract_colors) { for (const data_t& data_id : image.FramePtr()->ImageIds()) { ExtractColors(options_->image_path, data_id.id, *reconstruction); } } if (options_->snapshot_frames_freq > 0 && reconstruction->NumRegFrames() >= options_->snapshot_frames_freq + snapshot_prev_num_reg_frames) { snapshot_prev_num_reg_frames = reconstruction->NumRegFrames(); WriteSnapshot(*reconstruction, options_->snapshot_path); } Callback(NEXT_IMAGE_REG_CALLBACK); } const size_t max_model_overlap = static_cast(options_->max_model_overlap); if (mapper.NumSharedRegImages() >= max_model_overlap) { break; } // If no image could be registered, try a single final global iterative // bundle adjustment and try again to register one image. If this fails // once, then exit the incremental mapping. if (!reg_next_success && prev_reg_next_success) { IterativeGlobalRefinement(*options_, mapper_options, mapper); } } while (reg_next_success || prev_reg_next_success); if (CheckIfStopped() || CheckReachedMaxRuntime()) { return Status::INTERRUPTED; } // Only run final global BA, if last incremental BA was not global. if (reconstruction->NumRegFrames() > 0 && reconstruction->NumRegFrames() != ba_prev_num_reg_frames && reconstruction->NumPoints3D() != ba_prev_num_points) { IterativeGlobalRefinement(*options_, mapper_options, mapper); } return Status::SUCCESS; } IncrementalPipeline::Status IncrementalPipeline::Reconstruct( IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, bool continue_reconstruction) { for (int num_trials = 0; num_trials < options_->init_num_trials; ++num_trials) { if (CheckIfStopped() || CheckReachedMaxRuntime()) { return Status::STOP; } const size_t reconstruction_idx = (!continue_reconstruction || num_trials > 0) ? reconstruction_manager_->Add() : 0; std::shared_ptr reconstruction = reconstruction_manager_->Get(reconstruction_idx); const Status status = ReconstructSubModel(mapper, mapper_options, reconstruction); switch (status) { case Status::INTERRUPTED: { reconstruction->UpdatePoint3DErrors(); LOG(INFO) << "Keeping reconstruction due to interrupt"; mapper.EndReconstruction(/*discard=*/false); AlignReconstructionToOrigRigScales(database_cache_->Rigs(), reconstruction.get()); return Status::STOP; } case Status::UNKNOWN_SENSOR_FROM_RIG: { LOG(ERROR) << "Discarding reconstruction due to unknown sensor_from_rig " "poses. Either explicitly define the poses by configuring the " "rigs or first run reconstruction without configured rigs and " "then derive the poses from the initial reconstruction for a " "subsequent reconstruction with rig constraints. See " "documentation for detailed instructions."; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); // If the reconstruction was discarded due to an unknown sensor from // rig, we can stop the outer trial loop, because all trials will fail. return Status::STOP; } case Status::BAD_INITIAL_PAIR: { LOG(INFO) << "Discarding reconstruction due to bad initial pair"; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); // If an initial pair was found but it was bad, we discard and attempt // to initialize from any of the remaining pairs in the next trials. break; } case Status::NO_INITIAL_PAIR: { LOG(INFO) << "Discarding reconstruction due to no initial pair"; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); // If no pair could be found, we can exit the trial loop, because // the next trials in this loop will not find anything unless the // initialization thresholds are relaxed. However, by relaxing the // constraints in the outer loop we can succeed. return Status::CONTINUE; } case Status::SUCCESS: { // Remember the total number of registered images before potentially // discarding it below due to small size, so we can exit out of the main // loop, if all images were registered. const size_t num_reg_images = reconstruction->NumRegImages(); const size_t total_num_reg_images = mapper.NumTotalRegImages(); // Always keep the first reconstruction, independent of size. if ((options_->multiple_models && reconstruction_manager_->Size() > 1 && num_reg_images < static_cast(options_->min_model_size)) || num_reg_images == 0) { LOG(WARNING) << "Discarding reconstruction due to insufficient size"; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); } else { reconstruction->UpdatePoint3DErrors(); LOG(INFO) << "Keeping successful reconstruction"; mapper.EndReconstruction(/*discard=*/false); AlignReconstructionToOrigRigScales(database_cache_->Rigs(), reconstruction.get()); } Callback(LAST_IMAGE_REG_CALLBACK); // Check if we should or can reconstruct another sub-model. if (!options_->multiple_models || reconstruction_manager_->Size() >= static_cast(options_->max_num_models) || total_num_reg_images >= database_cache_->NumImages() - 1) { return Status::STOP; } // In case the reconstruction was successful and there are remaining // images, we try to reconstruct another sub-model in the next trial. break; } default: LOG(FATAL_THROW) << "Unknown reconstruction status."; } } return Status::CONTINUE; } void IncrementalPipeline::TriangulateReconstruction( const std::shared_ptr& reconstruction) { THROW_CHECK_GT(database_cache_->NumImages(), 0) << "No images with matches found in the database"; IncrementalMapper mapper(database_cache_); mapper.BeginReconstruction(reconstruction); LOG(INFO) << "Iterative triangulation"; size_t image_idx = 0; for (const image_t image_id : reconstruction->RegImageIds()) { const auto& image = reconstruction->Image(image_id); LOG(INFO) << StringPrintf( "Triangulating image #%d (%d)", image_id, image_idx++); const size_t num_existing_points3D = image.NumPoints3D(); LOG(INFO) << "=> Image sees " << num_existing_points3D << " / " << mapper.ObservationManager().NumObservations(image_id) << " points"; mapper.TriangulateImage(options_->Triangulation(), image_id); VLOG(1) << "=> Triangulated " << (image.NumPoints3D() - num_existing_points3D) << " points"; } LOG(INFO) << "Retriangulation and Global bundle adjustment"; mapper.IterativeGlobalRefinement(options_->ba_global_max_refinements, options_->ba_global_max_refinement_change, options_->Mapper(), options_->GlobalBundleAdjustment(), options_->Triangulation(), /*normalize_reconstruction=*/false); mapper.EndReconstruction(/*discard=*/false); reconstruction->UpdatePoint3DErrors(); LOG(INFO) << "Extracting colors"; reconstruction->ExtractColorsForAllImages(options_->image_path); } void IncrementalPipeline::RegisterCallbacks() { RegisterCallback(INITIAL_IMAGE_PAIR_REG_CALLBACK); RegisterCallback(NEXT_IMAGE_REG_CALLBACK); RegisterCallback(LAST_IMAGE_REG_CALLBACK); } bool IncrementalPipeline::CheckReachedMaxRuntime() const { if (options_->max_runtime_seconds > 0 && total_run_timer_->ElapsedSeconds() > options_->max_runtime_seconds) { LOG(INFO) << "Reached maximum runtime of " << options_->max_runtime_seconds << " seconds."; return true; } return false; } } // namespace colmap colmap-4.0.4/src/colmap/controllers/incremental_pipeline.h000066400000000000000000000240511517363634500237600ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/scene/reconstruction_manager.h" #include "colmap/sfm/incremental_mapper.h" #include "colmap/util/base_controller.h" #include #include #include #include #include namespace colmap { class Timer; // NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) struct IncrementalPipelineOptions { // The minimum number of matches for inlier matches to be considered. int min_num_matches = 15; // Whether to ignore the inlier matches of watermark image pairs. bool ignore_watermarks = false; // Whether to reconstruct multiple sub-models. bool multiple_models = true; // The number of sub-models to reconstruct. int max_num_models = 50; // The maximum number of overlapping images between sub-models. If the // current sub-models shares more than this number of images with another // model, then the reconstruction is stopped. int max_model_overlap = 20; // The minimum number of registered images of a sub-model, otherwise the // sub-model is discarded. Note that the first sub-model is always kept // independent of size. If the model contains at least half of the total // number of images, we also always keep it. int min_model_size = 10; // The image identifiers used to initialize the reconstruction. Note that // only one or both image identifiers can be specified. In the former case, // the second image is automatically determined. int init_image_id1 = -1; int init_image_id2 = -1; // The number of trials to initialize the reconstruction. int init_num_trials = 200; // Enable fallback to structure-less image registration using 2D-2D // correspondences, if structured-based registration fails using 2D-3D // correspondences. bool structure_less_registration_fallback = true; // Only use structure-less and skip structure-based image registration. bool structure_less_registration_only = false; // Whether to extract colors for reconstructed points. bool extract_colors = true; // The number of threads to use during reconstruction. int num_threads = -1; // PRNG seed for all stochastic methods during reconstruction. int random_seed = -1; // Thresholds for filtering images with degenerate intrinsics. double min_focal_length_ratio = 0.1; double max_focal_length_ratio = 10.0; double max_extra_param = 1.0; // Which camera parameters to optimize during the reconstruction. bool ba_refine_focal_length = true; bool ba_refine_principal_point = false; bool ba_refine_extra_params = true; // Whether to optimize rig poses during the reconstruction. bool ba_refine_sensor_from_rig = true; // The minimum number of residuals per bundle adjustment problem to // enable multi-threading solving of the problems. int ba_min_num_residuals_for_cpu_multi_threading = 50000; // Ceres solver function tolerance for local bundle adjustment double ba_local_function_tolerance = 0.0; // The maximum number of local bundle adjustment iterations. int ba_local_max_num_iterations = 25; // The growth rates after which to perform global bundle adjustment. double ba_global_frames_ratio = 1.1; double ba_global_points_ratio = 1.1; int ba_global_frames_freq = 500; int ba_global_points_freq = 250000; // Ceres solver function tolerance for global bundle adjustment double ba_global_function_tolerance = 0.0; // The maximum number of global bundle adjustment iterations. int ba_global_max_num_iterations = 50; // The thresholds for iterative bundle adjustment refinements. int ba_local_max_refinements = 2; double ba_local_max_refinement_change = 0.001; int ba_global_max_refinements = 5; double ba_global_max_refinement_change = 0.0005; // Whether to use Ceres' CUDA sparse linear algebra library, if available. bool ba_use_gpu = false; std::string ba_gpu_index = "-1"; // Whether to use priors on the camera positions. bool use_prior_position = false; // Whether to use a robust loss on prior camera positions. bool use_robust_loss_on_prior_position = false; // Threshold on the residual for the robust position prior loss // (chi2 for 3DOF at 95% = 7.815). double prior_position_loss_scale = 7.815; // Path to a folder with reconstruction snapshots during incremental // reconstruction. Snapshots will be saved according to the specified // frequency of registered images. std::filesystem::path snapshot_path; int snapshot_frames_freq = 0; // The image path at which to find the images to extract point colors. // If not specified, all point colors will be black. std::filesystem::path image_path; // Optional list of image names to reconstruct. If no images are specified, // all images will be reconstructed by default. std::vector image_names; // Whether to load all images from the database, including those without // correspondences. Only useful for triangulation where all images are // already registered and should retain their keypoints. Should not be // enabled for incremental SfM. bool load_all_images = false; // If reconstruction is provided as input, fix the existing frame poses. bool fix_existing_frames = false; // List of rigs for which to fix the sensor_from_rig transformation, // independent of ba_refine_sensor_from_rig. std::unordered_set constant_rigs; // List of cameras for which to fix the camera parameters independent // of refine_focal_length, refine_principal_point, and refine_extra_params. std::unordered_set constant_cameras; // Maximum runtime in seconds for the reconstruction process. // If set to a non-positive value, the process will run until completion. int max_runtime_seconds = -1; IncrementalMapper::Options mapper; IncrementalTriangulator::Options triangulation; IncrementalMapper::Options Mapper() const; IncrementalTriangulator::Options Triangulation() const; BundleAdjustmentOptions LocalBundleAdjustment() const; BundleAdjustmentOptions GlobalBundleAdjustment() const; inline bool IsInitialPairProvided() const { return init_image_id1 != -1 && init_image_id2 != -1; } bool Check() const; }; // Class that controls the incremental mapping procedure by iteratively // initializing reconstructions from the same scene graph. class IncrementalPipeline : public BaseController { public: enum CallbackType { INITIAL_IMAGE_PAIR_REG_CALLBACK, NEXT_IMAGE_REG_CALLBACK, LAST_IMAGE_REG_CALLBACK, }; enum class Status { SUCCESS, INTERRUPTED, CONTINUE, STOP, NO_INITIAL_PAIR, BAD_INITIAL_PAIR, UNKNOWN_SENSOR_FROM_RIG, }; IncrementalPipeline( std::shared_ptr options, std::shared_ptr database, std::shared_ptr reconstruction_manager); IncrementalPipeline( std::shared_ptr options, std::shared_ptr database_cache, std::shared_ptr reconstruction_manager); void Run() override; // Getter functions for python pipelines. std::shared_ptr Options() const { return options_; } const std::shared_ptr& ReconstructionManager() const { return reconstruction_manager_; } const std::shared_ptr& DatabaseCache() const { return database_cache_; } Status Reconstruct(IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, bool continue_reconstruction); Status ReconstructSubModel( IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, const std::shared_ptr& reconstruction); Status InitializeReconstruction( IncrementalMapper& mapper, const IncrementalMapper::Options& mapper_options, Reconstruction& reconstruction); void TriangulateReconstruction( const std::shared_ptr& reconstruction); bool CheckRunGlobalRefinement(const Reconstruction& reconstruction, size_t ba_prev_num_reg_images, size_t ba_prev_num_points); bool CheckReachedMaxRuntime() const; private: void RegisterCallbacks(); const std::shared_ptr options_; std::shared_ptr reconstruction_manager_; std::shared_ptr database_cache_; std::shared_ptr total_run_timer_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/incremental_pipeline_test.cc000066400000000000000000000710211517363634500251540ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/incremental_pipeline.h" #include "colmap/geometry/rigid3_matchers.h" #include "colmap/scene/database.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { TEST(IncrementalPipeline, WithoutNoise) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(std::make_shared(), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, WithoutNoiseAndWithNonTrivialFrames) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0.05; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); for (const bool refine_sensor_from_rig : {true, false}) { auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); options->ba_refine_sensor_from_rig = refine_sensor_from_rig; IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear( *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-3, /*max_scale_error=*/refine_sensor_from_rig ? 1e-2 : 1e-4)); } } TEST(IncrementalPipeline, UnknownSensorFromRigExitsGracefully) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0.05; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); // Set one of the sensor from rig to unknown. auto rig = database->ReadAllRigs()[0]; rig.NonRefSensors().begin()->second.reset(); database->UpdateRig(rig); auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 0); } TEST(IncrementalPipeline, WithNonTrivialFramesAndConstantRigsAndCameras) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0.05; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); constexpr int kConstantRigId = 1; constexpr int kConstantCameraId = 1; auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); options->constant_rigs.insert(kConstantRigId); options->constant_cameras.insert(kConstantCameraId); IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); auto& reconstruction = *reconstruction_manager->Get(0); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-3)); for (const auto& [sensor_id, sensor_from_rig] : reconstruction.Rig(kConstantRigId).NonRefSensors()) { EXPECT_THAT( sensor_from_rig.value(), Rigid3dNear( gt_reconstruction.Rig(kConstantRigId).SensorFromRig(sensor_id), /*rtol=*/1e-5, /*ttol=*/1e-5)); } EXPECT_EQ(reconstruction.Camera(kConstantCameraId).params, gt_reconstruction.Camera(kConstantCameraId).params); } TEST(IncrementalPipeline, WithoutNoiseAndWithPanoramicNonTrivialFrames) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.sensor_from_rig_translation_stddev = 0; synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); for (const bool refine_sensor_from_rig : {true, false}) { auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); options->ba_refine_sensor_from_rig = refine_sensor_from_rig; IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-3)); } } TEST(IncrementalPipeline, WithPriorFocalLength) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(std::make_shared(), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, WithNoise) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(std::make_shared(), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(IncrementalPipeline, IgnoreRedundantPoints3D) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); options->mapper.ba_global_ignore_redundant_points3D = true; options->mapper.ba_global_ignore_redundant_points3D_min_coverage_gain = 0.5; IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, StructureLessRegistrationOnly) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); options->structure_less_registration_only = true; IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-3, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, MultiReconstruction) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction1; Reconstruction gt_reconstruction2; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset( synthetic_dataset_options, >_reconstruction1, database.get()); synthetic_dataset_options.num_frames_per_rig = 4; SynthesizeDataset( synthetic_dataset_options, >_reconstruction2, database.get()); auto reconstruction_manager = std::make_shared(); auto mapper_options = std::make_shared(); mapper_options->min_model_size = 4; IncrementalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 2); Reconstruction* computed_reconstruction1 = nullptr; Reconstruction* computed_reconstruction2 = nullptr; if (reconstruction_manager->Get(0)->NumRegImages() == 5) { computed_reconstruction1 = reconstruction_manager->Get(0).get(); computed_reconstruction2 = reconstruction_manager->Get(1).get(); } else { computed_reconstruction1 = reconstruction_manager->Get(1).get(); computed_reconstruction2 = reconstruction_manager->Get(0).get(); } EXPECT_THAT(gt_reconstruction1, ReconstructionNear(*computed_reconstruction1, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); EXPECT_THAT(gt_reconstruction2, ReconstructionNear(*computed_reconstruction2, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, FixExistingFrames) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.camera_has_prior_focal_length = false; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); auto options = std::make_shared(); for (const bool fix_existing_frames : {false, true}) { if (fix_existing_frames) { ASSERT_EQ(reconstruction_manager->Size(), 1); Reconstruction& reconstruction = *reconstruction_manager->Get(0); // De-register a frame that expect to be re-registered in the second run. reconstruction.DeRegisterFrame(1); // Clear all the observations of one image but keep it registered. We do // not expect fixed images to be filtered (due to insufficient // observations). Image& image2 = reconstruction.Image(2); for (point2D_t point2D_idx = 0; point2D_idx < image2.NumPoints2D(); ++point2D_idx) { if (image2.Point2D(point2D_idx).HasPoint3D()) { reconstruction.DeleteObservation(image2.ImageId(), point2D_idx); } } } options->fix_existing_frames = fix_existing_frames; IncrementalPipeline mapper(options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } } TEST(IncrementalPipeline, ChainedMatches) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.match_config = SyntheticDatasetOptions::MatchConfig::CHAINED; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 4; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(std::make_shared(), database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4)); } TEST(IncrementalPipeline, PriorBasedSfMWithoutNoise) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.prior_position = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.prior_position_stddev = 0.0; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); std::shared_ptr mapper_options = std::make_shared(); mapper_options->use_prior_position = true; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); // No noise on prior so do not align gt & computed (expected to be aligned // from PositionPriorBundleAdjustment) EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02, /*align=*/false)); } TEST(IncrementalPipeline, PriorBasedSfMWithoutNoiseAndWithNonTrivialFrames) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.camera_has_prior_focal_length = false; synthetic_dataset_options.prior_position = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); std::shared_ptr mapper_options = std::make_shared(); mapper_options->use_prior_position = true; mapper_options->use_robust_loss_on_prior_position = true; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(IncrementalPipeline, PriorBasedSfMWithNoise) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.prior_position = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.prior_position_stddev = 1.5; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); std::shared_ptr mapper_options = std::make_shared(); mapper_options->use_prior_position = true; mapper_options->use_robust_loss_on_prior_position = true; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(IncrementalPipeline, GPSPriorBasedSfMWithNoise) { const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.prior_position = true; synthetic_dataset_options.prior_position_coordinate_system = PosePrior::CoordinateSystem::WGS84; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.prior_position_stddev = 1.5; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); std::shared_ptr mapper_options = std::make_shared(); mapper_options->use_prior_position = true; mapper_options->use_robust_loss_on_prior_position = true; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper(mapper_options, database, reconstruction_manager); mapper.Run(); ASSERT_EQ(reconstruction_manager->Size(), 1); EXPECT_THAT(gt_reconstruction, ReconstructionNear(*reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(IncrementalPipeline, SfMWithRandomSeedStability) { SetPRNGSeed(42); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 3; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.prior_position = false; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.1; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto run_mapper = [&](int num_threads, int random_seed) { auto mapper_options = std::make_shared(); mapper_options->use_prior_position = false; mapper_options->num_threads = num_threads; mapper_options->random_seed = random_seed; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper( mapper_options, database, reconstruction_manager); mapper.Run(); EXPECT_EQ(reconstruction_manager->Size(), 1); return reconstruction_manager; }; constexpr int kRandomSeed = 42; auto reconstruction_manager0 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); auto reconstruction_manager1 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); EXPECT_THAT(*reconstruction_manager0->Get(0), ReconstructionEq(*reconstruction_manager1->Get(0))); } TEST(IncrementalPipeline, PriorBasedSfMWithRandomSeedStability) { SetPRNGSeed(42); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.prior_position = true; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.1; synthetic_noise_options.prior_position_stddev = 0.1; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto run_mapper = [&](int num_threads, int random_seed) { auto mapper_options = std::make_shared(); mapper_options->use_prior_position = true; mapper_options->num_threads = num_threads; mapper_options->random_seed = random_seed; auto reconstruction_manager = std::make_shared(); IncrementalPipeline mapper( mapper_options, database, reconstruction_manager); mapper.Run(); EXPECT_EQ(reconstruction_manager->Size(), 1); return reconstruction_manager; }; constexpr int kRandomSeed = 42; auto reconstruction_manager0 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); auto reconstruction_manager1 = run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); EXPECT_THAT(*reconstruction_manager0->Get(0), ReconstructionEq(*reconstruction_manager1->Get(0))); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/matcher_cache.cc000066400000000000000000000260761517363634500225070ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/matcher_cache.h" namespace colmap { FeatureMatcherCache::FeatureMatcherCache( const size_t cache_size, const std::shared_ptr& database) : cache_size_(cache_size), database_(THROW_CHECK_NOTNULL(database)), descriptor_index_cache_(cache_size_, [this](const image_t image_id) { auto descriptors = GetDescriptors(image_id); auto index = FeatureDescriptorIndex::Create(); index->Build(descriptors->ToFloat()); return index; }) { keypoints_cache_ = std::make_unique>( cache_size_, [this](const image_t image_id) { std::lock_guard lock(database_mutex_); return std::make_shared( database_->ReadKeypoints(image_id)); }); descriptors_cache_ = std::make_unique>( cache_size_, [this](const image_t image_id) { std::lock_guard lock(database_mutex_); return std::make_shared( database_->ReadDescriptors(image_id)); }); keypoints_exists_cache_ = std::make_unique>( cache_size_, [this](const image_t image_id) { std::lock_guard lock(database_mutex_); return std::make_shared(database_->ExistsKeypoints(image_id)); }); descriptors_exists_cache_ = std::make_unique>( cache_size_, [this](const image_t image_id) { std::lock_guard lock(database_mutex_); return std::make_shared( database_->ExistsDescriptors(image_id)); }); } void FeatureMatcherCache::AccessDatabase( const std::function& func) { std::lock_guard lock(database_mutex_); func(*database_); } const Camera& FeatureMatcherCache::GetCamera(const camera_t camera_id) { MaybeLoadCameras(); return cameras_cache_->at(camera_id); } const Frame& FeatureMatcherCache::GetFrame(const frame_t frame_id) { MaybeLoadFrames(); return frames_cache_->at(frame_id); } const Image& FeatureMatcherCache::GetImage(const image_t image_id) { MaybeLoadImages(); return images_cache_->at(image_id); } const PosePrior* FeatureMatcherCache::FindImagePosePriorOrNull( const image_t image_id) { MaybeLoadPosePriors(); const auto it = pose_priors_cache_->find(image_id); if (it != pose_priors_cache_->end()) { return &it->second; } return nullptr; } std::shared_ptr FeatureMatcherCache::GetKeypoints( const image_t image_id) { return keypoints_cache_->Get(image_id); } std::shared_ptr FeatureMatcherCache::GetDescriptors( const image_t image_id) { return descriptors_cache_->Get(image_id); } FeatureMatches FeatureMatcherCache::GetMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ReadMatches(image_id1, image_id2); } TwoViewGeometry FeatureMatcherCache::GetTwoViewGeometry( const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ReadTwoViewGeometry(image_id1, image_id2); } std::vector FeatureMatcherCache::GetFrameIds() { MaybeLoadFrames(); std::vector frame_ids; frame_ids.reserve(frames_cache_->size()); for (const auto& frame : *frames_cache_) { frame_ids.push_back(frame.first); } // Sort the frames for deterministic behavior. Note that the frames_cache_ is // an unordered_map, which does not guarantee a deterministic order across // different standard library implementations. std::sort(frame_ids.begin(), frame_ids.end()); return frame_ids; } std::vector FeatureMatcherCache::GetImageIds() { MaybeLoadImages(); std::vector image_ids; image_ids.reserve(images_cache_->size()); for (const auto& image : *images_cache_) { image_ids.push_back(image.first); } // Sort the images for deterministic behavior. Note that the images_cache_ is // an unordered_map, which does not guarantee a deterministic order across // different standard library implementations. std::sort(image_ids.begin(), image_ids.end()); return image_ids; } ThreadSafeLRUCache& FeatureMatcherCache::GetFeatureDescriptorIndexCache() { return descriptor_index_cache_; } bool FeatureMatcherCache::ExistsKeypoints(const image_t image_id) { return *keypoints_exists_cache_->Get(image_id); } bool FeatureMatcherCache::ExistsDescriptors(const image_t image_id) { return *descriptors_exists_cache_->Get(image_id); } bool FeatureMatcherCache::ExistsMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ExistsMatches(image_id1, image_id2); } bool FeatureMatcherCache::ExistsTwoViewGeometry(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ExistsTwoViewGeometry(image_id1, image_id2); } bool FeatureMatcherCache::ExistsInlierMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); if (!database_->ExistsTwoViewGeometry(image_id1, image_id2)) { return false; } auto two_view_geometry = database_->ReadTwoViewGeometry(image_id1, image_id2); return !two_view_geometry.inlier_matches.empty(); } void FeatureMatcherCache::UpdateTwoViewGeometry( const image_t image_id1, const image_t image_id2, const TwoViewGeometry& two_view_geometry) { std::lock_guard lock(database_mutex_); database_->UpdateTwoViewGeometry(image_id1, image_id2, two_view_geometry); } void FeatureMatcherCache::WriteMatches(const image_t image_id1, const image_t image_id2, const FeatureMatches& matches) { std::lock_guard lock(database_mutex_); database_->WriteMatches(image_id1, image_id2, matches); } void FeatureMatcherCache::WriteTwoViewGeometry( const image_t image_id1, const image_t image_id2, const TwoViewGeometry& two_view_geometry) { std::lock_guard lock(database_mutex_); database_->WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); } void FeatureMatcherCache::DeleteMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); database_->DeleteMatches(image_id1, image_id2); } void FeatureMatcherCache::DeleteTwoViewGeometry(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); database_->DeleteTwoViewGeometry(image_id1, image_id2); } void FeatureMatcherCache::DeleteInlierMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); database_->DeleteInlierMatches(image_id1, image_id2); } size_t FeatureMatcherCache::MaxNumKeypoints() { std::lock_guard lock(database_mutex_); if (!max_num_keypoints_) { max_num_keypoints_ = database_->MaxNumKeypoints(); } return *max_num_keypoints_; } void FeatureMatcherCache::MaybeLoadCameras() { std::lock_guard lock(database_mutex_); if (cameras_cache_) { return; } std::vector cameras = database_->ReadAllCameras(); cameras_cache_ = std::make_unique>(); cameras_cache_->reserve(cameras.size()); for (Camera& camera : cameras) { cameras_cache_->emplace(camera.camera_id, std::move(camera)); } } void FeatureMatcherCache::MaybeLoadFrames() { std::lock_guard lock(database_mutex_); if (frames_cache_) { return; } std::vector frames = database_->ReadAllFrames(); frames_cache_ = std::make_unique>(); frames_cache_->reserve(frames.size()); for (Frame& frame : frames) { frames_cache_->emplace(frame.FrameId(), std::move(frame)); } } void FeatureMatcherCache::MaybeLoadImages() { MaybeLoadFrames(); std::lock_guard lock(database_mutex_); if (images_cache_) { return; } std::vector images = database_->ReadAllImages(); images_cache_ = std::make_unique>(); images_cache_->reserve(images.size()); for (Image& image : images) { images_cache_->emplace(image.ImageId(), std::move(image)); } } void FeatureMatcherCache::MaybeLoadPosePriors() { MaybeLoadImages(); std::lock_guard lock(database_mutex_); if (pose_priors_cache_) { return; } std::vector pose_priors = database_->ReadAllPosePriors(); pose_priors_cache_ = std::make_unique>(); pose_priors_cache_->reserve(pose_priors.size()); for (PosePrior& pose_prior : pose_priors) { if (pose_prior.corr_data_id.sensor_id.type == SensorType::CAMERA) { const image_t image_id = pose_prior.corr_data_id.id; THROW_CHECK( pose_priors_cache_->emplace(image_id, std::move(pose_prior)).second) << "Duplicate pose prior for image " << image_id; } } } } // namespace colmap colmap-4.0.4/src/colmap/controllers/matcher_cache.h000066400000000000000000000117421517363634500223430ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/feature/index.h" #include "colmap/feature/types.h" #include "colmap/scene/camera.h" #include "colmap/scene/database.h" #include "colmap/scene/image.h" #include "colmap/scene/two_view_geometry.h" #include "colmap/util/cache.h" #include "colmap/util/types.h" #include #include #include #include namespace colmap { // Cache for feature matching to minimize database access during matching. class FeatureMatcherCache { public: FeatureMatcherCache(size_t cache_size, const std::shared_ptr& database); // Executes a function that accesses the database. This function is thread // safe and ensures that only one function can access the database at a time. void AccessDatabase(const std::function& func); const Camera& GetCamera(camera_t camera_id); const Frame& GetFrame(frame_t frame_id); const Image& GetImage(image_t image_id); const PosePrior* FindImagePosePriorOrNull(image_t image_id); std::shared_ptr GetKeypoints(image_t image_id); std::shared_ptr GetDescriptors(image_t image_id); FeatureMatches GetMatches(image_t image_id1, image_t image_id2); TwoViewGeometry GetTwoViewGeometry(image_t image_id1, image_t image_id2); std::vector GetFrameIds(); std::vector GetImageIds(); ThreadSafeLRUCache& GetFeatureDescriptorIndexCache(); bool ExistsKeypoints(image_t image_id); bool ExistsDescriptors(image_t image_id); bool ExistsMatches(image_t image_id1, image_t image_id2); bool ExistsTwoViewGeometry(image_t image_id1, image_t image_id2); bool ExistsInlierMatches(image_t image_id1, image_t image_id2); void UpdateTwoViewGeometry(image_t image_id1, image_t image_id2, const TwoViewGeometry& two_view_geometry); void WriteMatches(image_t image_id1, image_t image_id2, const FeatureMatches& matches); void WriteTwoViewGeometry(image_t image_id1, image_t image_id2, const TwoViewGeometry& two_view_geometry); void DeleteMatches(image_t image_id1, image_t image_id2); void DeleteTwoViewGeometry(image_t image_id1, image_t image_id2); void DeleteInlierMatches(image_t image_id1, image_t image_id2); size_t MaxNumKeypoints(); private: void MaybeLoadCameras(); void MaybeLoadFrames(); void MaybeLoadImages(); void MaybeLoadPosePriors(); const size_t cache_size_; const std::shared_ptr database_; std::mutex database_mutex_; std::unique_ptr> cameras_cache_; std::unique_ptr> frames_cache_; std::unique_ptr> images_cache_; std::unique_ptr> pose_priors_cache_; std::unique_ptr> keypoints_cache_; std::unique_ptr> descriptors_cache_; std::unique_ptr> keypoints_exists_cache_; std::unique_ptr> descriptors_exists_cache_; ThreadSafeLRUCache descriptor_index_cache_; std::optional max_num_keypoints_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/matcher_cache_test.cc000066400000000000000000000304061517363634500235360ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/matcher_cache.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { struct TestData { std::shared_ptr database; Reconstruction reconstruction; }; TestData CreateTestData(int num_images, bool with_priors = false, int num_cameras_per_rig = 1) { TestData data; const auto test_dir = CreateTestDir(); const auto database_path = test_dir / "database.db"; data.database = Database::Open(database_path); SyntheticDatasetOptions options; options.num_rigs = num_images / num_cameras_per_rig; options.num_cameras_per_rig = num_cameras_per_rig; options.num_frames_per_rig = 1; options.num_points3D = 20; options.num_points2D_without_point3D = 3; options.prior_position = with_priors; SynthesizeDataset(options, &data.reconstruction, data.database.get()); return data; } TEST(FeatureMatcherCache, GetCamera) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector cameras = data.database->ReadAllCameras(); ASSERT_FALSE(cameras.empty()); for (const Camera& camera : cameras) { EXPECT_EQ(cache.GetCamera(camera.camera_id), camera); } } TEST(FeatureMatcherCache, GetFrame) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector frames = data.database->ReadAllFrames(); ASSERT_FALSE(frames.empty()); for (const Frame& frame : frames) { EXPECT_EQ(cache.GetFrame(frame.FrameId()), frame); } } TEST(FeatureMatcherCache, GetImage) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_FALSE(images.empty()); for (const Image& image : images) { EXPECT_EQ(cache.GetImage(image.ImageId()), image); } } TEST(FeatureMatcherCache, GetImageIds) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); std::vector expected_ids; expected_ids.reserve(images.size()); for (const Image& image : images) { expected_ids.push_back(image.ImageId()); } EXPECT_THAT(cache.GetImageIds(), ::testing::UnorderedElementsAreArray(expected_ids)); } TEST(FeatureMatcherCache, GetFrameIds) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector frames = data.database->ReadAllFrames(); std::vector expected_ids; expected_ids.reserve(frames.size()); for (const Frame& frame : frames) { expected_ids.push_back(frame.FrameId()); } EXPECT_THAT(cache.GetFrameIds(), ::testing::UnorderedElementsAreArray(expected_ids)); } TEST(FeatureMatcherCache, FindImagePosePriorOrNullWithPriors) { auto data = CreateTestData(4, /*with_priors=*/true); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_FALSE(images.empty()); for (const Image& image : images) { const PosePrior* prior = cache.FindImagePosePriorOrNull(image.ImageId()); ASSERT_NE(prior, nullptr); EXPECT_TRUE(prior->HasPosition()); } } TEST(FeatureMatcherCache, FindImagePosePriorOrNullWithoutPriors) { auto data = CreateTestData(4, /*with_priors=*/false); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_FALSE(images.empty()); for (const Image& image : images) { const PosePrior* prior = cache.FindImagePosePriorOrNull(image.ImageId()); EXPECT_EQ(prior, nullptr); } } TEST(FeatureMatcherCache, Features) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_FALSE(images.empty()); for (const Image& image : images) { EXPECT_TRUE(cache.ExistsKeypoints(image.ImageId())); EXPECT_TRUE(cache.ExistsDescriptors(image.ImageId())); EXPECT_EQ(*cache.GetKeypoints(image.ImageId()), data.database->ReadKeypoints(image.ImageId())); auto cached_descriptors = cache.GetDescriptors(image.ImageId()); FeatureDescriptors db_descriptors = data.database->ReadDescriptors(image.ImageId()); EXPECT_EQ(cached_descriptors->type, db_descriptors.type); EXPECT_EQ(cached_descriptors->data, db_descriptors.data); } } TEST(FeatureMatcherCache, Matches) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_matches = data.database->ReadAllMatches(); ASSERT_FALSE(all_matches.empty()); for (const auto& [pair_id, matches] : all_matches) { const auto [image_id1, image_id2] = PairIdToImagePair(pair_id); EXPECT_TRUE(cache.ExistsMatches(image_id1, image_id2)); EXPECT_EQ(cache.GetMatches(image_id1, image_id2), matches); } } TEST(FeatureMatcherCache, TwoViewGeometry) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_tvg = data.database->ReadTwoViewGeometries(); ASSERT_FALSE(all_tvg.empty()); for (const auto& [pair_id, tvg] : all_tvg) { const auto [image_id1, image_id2] = PairIdToImagePair(pair_id); EXPECT_TRUE(cache.ExistsTwoViewGeometry(image_id1, image_id2)); EXPECT_EQ(cache.ExistsInlierMatches(image_id1, image_id2), !tvg.inlier_matches.empty()); EXPECT_EQ(cache.GetTwoViewGeometry(image_id1, image_id2).inlier_matches, tvg.inlier_matches); } } TEST(FeatureMatcherCache, WriteAndGetMatches) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_GE(images.size(), 2); const image_t id1 = images[0].ImageId(); const image_t id2 = images[1].ImageId(); // Delete existing matches first. cache.DeleteMatches(id1, id2); EXPECT_FALSE(cache.ExistsMatches(id1, id2)); // Write new matches. FeatureMatches matches(5); for (size_t i = 0; i < matches.size(); ++i) { matches[i].point2D_idx1 = i; matches[i].point2D_idx2 = i; } cache.WriteMatches(id1, id2, matches); EXPECT_TRUE(cache.ExistsMatches(id1, id2)); FeatureMatches read_matches = cache.GetMatches(id1, id2); EXPECT_EQ(read_matches.size(), 5); } TEST(FeatureMatcherCache, WriteAndGetTwoViewGeometry) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const std::vector images = data.database->ReadAllImages(); ASSERT_GE(images.size(), 2); const image_t id1 = images[0].ImageId(); const image_t id2 = images[1].ImageId(); // Delete existing two-view geometry first. cache.DeleteTwoViewGeometry(id1, id2); EXPECT_FALSE(cache.ExistsTwoViewGeometry(id1, id2)); // Write new two-view geometry. TwoViewGeometry tvg; tvg.config = TwoViewGeometry::CALIBRATED; tvg.inlier_matches.resize(10); cache.WriteTwoViewGeometry(id1, id2, tvg); EXPECT_TRUE(cache.ExistsTwoViewGeometry(id1, id2)); TwoViewGeometry read_tvg = cache.GetTwoViewGeometry(id1, id2); EXPECT_EQ(read_tvg.config, TwoViewGeometry::CALIBRATED); EXPECT_EQ(read_tvg.inlier_matches.size(), 10); } TEST(FeatureMatcherCache, UpdateTwoViewGeometry) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_tvg = data.database->ReadTwoViewGeometries(); ASSERT_FALSE(all_tvg.empty()); const auto& [pair_id, original_tvg] = all_tvg.front(); const auto [id1, id2] = PairIdToImagePair(pair_id); TwoViewGeometry updated_tvg; updated_tvg.config = TwoViewGeometry::UNCALIBRATED; updated_tvg.inlier_matches.resize(7); cache.UpdateTwoViewGeometry(id1, id2, updated_tvg); TwoViewGeometry read_tvg = cache.GetTwoViewGeometry(id1, id2); EXPECT_EQ(read_tvg.config, TwoViewGeometry::UNCALIBRATED); EXPECT_EQ(read_tvg.inlier_matches.size(), 7); } TEST(FeatureMatcherCache, DeleteMatches) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_matches = data.database->ReadAllMatches(); ASSERT_FALSE(all_matches.empty()); const auto& [pair_id, _] = all_matches.front(); const auto [id1, id2] = PairIdToImagePair(pair_id); EXPECT_TRUE(cache.ExistsMatches(id1, id2)); cache.DeleteMatches(id1, id2); EXPECT_FALSE(cache.ExistsMatches(id1, id2)); } TEST(FeatureMatcherCache, DeleteTwoViewGeometry) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_tvg = data.database->ReadTwoViewGeometries(); ASSERT_FALSE(all_tvg.empty()); const auto& [pair_id, _] = all_tvg.front(); const auto [id1, id2] = PairIdToImagePair(pair_id); EXPECT_TRUE(cache.ExistsTwoViewGeometry(id1, id2)); cache.DeleteTwoViewGeometry(id1, id2); EXPECT_FALSE(cache.ExistsTwoViewGeometry(id1, id2)); } TEST(FeatureMatcherCache, DeleteInlierMatches) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const auto all_tvg = data.database->ReadTwoViewGeometries(); ASSERT_FALSE(all_tvg.empty()); // Find a pair with inlier matches. image_t image_id1 = 0, image_id2 = 0; bool found = false; for (const auto& [pair_id, tvg] : all_tvg) { if (!tvg.inlier_matches.empty()) { std::tie(image_id1, image_id2) = PairIdToImagePair(pair_id); found = true; break; } } ASSERT_TRUE(found); EXPECT_TRUE(cache.ExistsInlierMatches(image_id1, image_id2)); cache.DeleteInlierMatches(image_id1, image_id2); EXPECT_FALSE(cache.ExistsInlierMatches(image_id1, image_id2)); // The two-view geometry entry should still exist. EXPECT_TRUE(cache.ExistsTwoViewGeometry(image_id1, image_id2)); } TEST(FeatureMatcherCache, MaxNumKeypoints) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); const size_t max_num_keypoints = cache.MaxNumKeypoints(); EXPECT_GT(max_num_keypoints, 0); // Calling again should return the cached value. EXPECT_EQ(cache.MaxNumKeypoints(), max_num_keypoints); } TEST(FeatureMatcherCache, AccessDatabase) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); size_t num_images = 0; cache.AccessDatabase([&num_images](Database& database) { num_images = database.ReadAllImages().size(); }); EXPECT_EQ(num_images, 4); } TEST(FeatureMatcherCache, GetFeatureDescriptorIndexCache) { auto data = CreateTestData(4); FeatureMatcherCache cache(5, data.database); auto& index_cache = cache.GetFeatureDescriptorIndexCache(); const std::vector images = data.database->ReadAllImages(); ASSERT_FALSE(images.empty()); // Access descriptor index for the first image to trigger build. auto index = index_cache.Get(images[0].ImageId()); ASSERT_NE(index, nullptr); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/option_manager.cc000066400000000000000000001450731517363634500227420ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/option_manager.h" #include "colmap/controllers/global_pipeline.h" #include "colmap/controllers/image_reader.h" #include "colmap/controllers/incremental_pipeline.h" #include "colmap/controllers/pairing.h" #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/estimators/global_positioning.h" #include "colmap/estimators/gravity_refinement.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/aliked.h" #include "colmap/feature/sift.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/mesh_simplification.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match_options.h" #include "colmap/mvs/texture_mapping.h" #include "colmap/scene/reconstruction_clustering.h" #include "colmap/ui/render_options.h" #include "colmap/util/file.h" #include "colmap/util/version.h" namespace config = boost::program_options; namespace colmap { OptionManager::OptionManager(bool add_project_options) : BaseOptionManager(add_project_options) { image_reader = std::make_shared(); feature_extraction = std::make_shared(); feature_matching = std::make_shared(); two_view_geometry = std::make_shared(); exhaustive_pairing = std::make_shared(); sequential_pairing = std::make_shared(); vocab_tree_pairing = std::make_shared(); spatial_pairing = std::make_shared(); transitive_pairing = std::make_shared(); imported_pairing = std::make_shared(); bundle_adjustment = std::make_shared(); mapper = std::make_shared(); global_mapper = std::make_shared(); gravity_refiner = std::make_shared(); reconstruction_clusterer = std::make_shared(); patch_match_stereo = std::make_shared(); stereo_fusion = std::make_shared(); poisson_meshing = std::make_shared(); delaunay_meshing = std::make_shared(); mesh_texture_mapping = std::make_shared(); mesh_simplification = std::make_shared(); render = std::make_shared(); } void OptionManager::ModifyForIndividualData() { mapper->min_focal_length_ratio = 0.1; mapper->max_focal_length_ratio = 10; mapper->max_extra_param = std::numeric_limits::max(); } void OptionManager::ModifyForVideoData() { const bool kResetPaths = false; ResetOptions(kResetPaths); mapper->mapper.init_min_tri_angle /= 2; mapper->ba_global_frames_ratio = 1.4; mapper->ba_global_points_ratio = 1.4; mapper->min_focal_length_ratio = 0.1; mapper->max_focal_length_ratio = 10; mapper->max_extra_param = std::numeric_limits::max(); stereo_fusion->min_num_pixels = 15; } void OptionManager::ModifyForInternetData() { stereo_fusion->min_num_pixels = 10; } void OptionManager::ModifyForLowQuality() { feature_extraction->max_image_size = static_cast(0.3125 * feature_extraction->EffMaxImageSize()); feature_extraction->sift->max_num_features = 2048; sequential_pairing->loop_detection_num_images /= 2; vocab_tree_pairing->max_num_features = 256; vocab_tree_pairing->num_images /= 2; mapper->ba_local_max_num_iterations /= 2; mapper->ba_global_max_num_iterations /= 2; mapper->ba_global_frames_ratio *= 1.2; mapper->ba_global_points_ratio *= 1.2; mapper->ba_global_max_refinements = 2; patch_match_stereo->max_image_size = 1000; patch_match_stereo->window_radius = 4; patch_match_stereo->window_step = 2; patch_match_stereo->num_samples /= 2; patch_match_stereo->num_iterations = 3; patch_match_stereo->geom_consistency = false; stereo_fusion->check_num_images /= 2; stereo_fusion->max_image_size = 1000; } void OptionManager::ModifyForMediumQuality() { feature_extraction->max_image_size = static_cast(0.5 * feature_extraction->EffMaxImageSize()); feature_extraction->sift->max_num_features = 4096; sequential_pairing->loop_detection_num_images /= 1.5; vocab_tree_pairing->max_num_features = 1024; vocab_tree_pairing->num_images /= 1.5; mapper->ba_local_max_num_iterations /= 1.5; mapper->ba_global_max_num_iterations /= 1.5; mapper->ba_global_frames_ratio *= 1.1; mapper->ba_global_points_ratio *= 1.1; mapper->ba_global_max_refinements = 2; patch_match_stereo->max_image_size = 1600; patch_match_stereo->window_radius = 4; patch_match_stereo->window_step = 2; patch_match_stereo->num_samples /= 1.5; patch_match_stereo->num_iterations = 5; patch_match_stereo->geom_consistency = false; stereo_fusion->check_num_images /= 1.5; stereo_fusion->max_image_size = 1600; } void OptionManager::ModifyForHighQuality() { feature_extraction->sift->estimate_affine_shape = true; feature_extraction->max_image_size = static_cast(0.75 * feature_extraction->EffMaxImageSize()); feature_extraction->sift->max_num_features = 8192; feature_matching->guided_matching = true; vocab_tree_pairing->max_num_features = 4096; mapper->ba_local_max_num_iterations = 30; mapper->ba_local_max_refinements = 3; mapper->ba_global_max_num_iterations = 75; patch_match_stereo->max_image_size = 2400; stereo_fusion->max_image_size = 2400; } void OptionManager::ModifyForExtremeQuality() { // Most of the options are set to extreme quality by default. feature_extraction->sift->estimate_affine_shape = true; feature_extraction->sift->domain_size_pooling = true; feature_matching->guided_matching = true; mapper->ba_local_max_num_iterations = 40; mapper->ba_local_max_refinements = 3; mapper->ba_global_max_num_iterations = 100; } void OptionManager::AddAllOptions() { BaseOptionManager::AddAllOptions(); AddFeatureExtractionOptions(); AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddExhaustivePairingOptions(); AddSequentialPairingOptions(); AddVocabTreePairingOptions(); AddSpatialPairingOptions(); AddTransitivePairingOptions(); AddImportedPairingOptions(); AddBundleAdjustmentOptions(); AddMapperOptions(); AddPatchMatchStereoOptions(); AddStereoFusionOptions(); AddPoissonMeshingOptions(); AddDelaunayMeshingOptions(); AddMeshTextureMappingOptions(); AddMeshSimplificationOptions(); AddRenderOptions(); } void OptionManager::AddFeatureExtractionOptions() { if (added_feature_extraction_options_) { return; } added_feature_extraction_options_ = true; AddDefaultOption("ImageReader.mask_path", &image_reader->mask_path); AddDefaultOption("ImageReader.camera_model", &image_reader->camera_model); AddDefaultOption("ImageReader.single_camera", &image_reader->single_camera); AddDefaultOption("ImageReader.single_camera_per_folder", &image_reader->single_camera_per_folder); AddDefaultOption("ImageReader.single_camera_per_image", &image_reader->single_camera_per_image); AddDefaultOption("ImageReader.existing_camera_id", &image_reader->existing_camera_id); AddDefaultOption("ImageReader.camera_params", &image_reader->camera_params); AddDefaultOption("ImageReader.default_focal_length_factor", &image_reader->default_focal_length_factor); AddDefaultOption("ImageReader.camera_mask_path", &image_reader->camera_mask_path); AddDefaultEnumOption("FeatureExtraction.type", &feature_extraction->type, FeatureExtractorTypeToString, FeatureExtractorTypeFromString); AddDefaultOption("FeatureExtraction.num_threads", &feature_extraction->num_threads); AddDefaultOption("FeatureExtraction.use_gpu", &feature_extraction->use_gpu); AddDefaultOption("FeatureExtraction.gpu_index", &feature_extraction->gpu_index); AddDefaultOption("FeatureExtraction.max_image_size", &feature_extraction->max_image_size); AddDefaultOption("SiftExtraction.max_num_features", &feature_extraction->sift->max_num_features); AddDefaultOption("SiftExtraction.first_octave", &feature_extraction->sift->first_octave); AddDefaultOption("SiftExtraction.num_octaves", &feature_extraction->sift->num_octaves); AddDefaultOption("SiftExtraction.octave_resolution", &feature_extraction->sift->octave_resolution); AddDefaultOption("SiftExtraction.peak_threshold", &feature_extraction->sift->peak_threshold); AddDefaultOption("SiftExtraction.edge_threshold", &feature_extraction->sift->edge_threshold); AddDefaultOption("SiftExtraction.estimate_affine_shape", &feature_extraction->sift->estimate_affine_shape); AddDefaultOption("SiftExtraction.max_num_orientations", &feature_extraction->sift->max_num_orientations); AddDefaultOption("SiftExtraction.upright", &feature_extraction->sift->upright); AddDefaultOption("SiftExtraction.domain_size_pooling", &feature_extraction->sift->domain_size_pooling); AddDefaultOption("SiftExtraction.dsp_min_scale", &feature_extraction->sift->dsp_min_scale); AddDefaultOption("SiftExtraction.dsp_max_scale", &feature_extraction->sift->dsp_max_scale); AddDefaultOption("SiftExtraction.dsp_num_scales", &feature_extraction->sift->dsp_num_scales); AddDefaultOption("AlikedExtraction.max_num_features", &feature_extraction->aliked->max_num_features); AddDefaultOption("AlikedExtraction.min_score", &feature_extraction->aliked->min_score); AddDefaultOption("AlikedExtraction.n16rot_model_path", &feature_extraction->aliked->n16rot_model_path); AddDefaultOption("AlikedExtraction.n32_model_path", &feature_extraction->aliked->n32_model_path); } void OptionManager::AddFeatureMatchingOptions() { if (added_feature_matching_options_) { return; } added_feature_matching_options_ = true; AddDefaultEnumOption("FeatureMatching.type", &feature_matching->type, FeatureMatcherTypeToString, FeatureMatcherTypeFromString); AddDefaultOption("FeatureMatching.num_threads", &feature_matching->num_threads); AddDefaultOption("FeatureMatching.use_gpu", &feature_matching->use_gpu); AddDefaultOption("FeatureMatching.gpu_index", &feature_matching->gpu_index); AddDefaultOption("FeatureMatching.guided_matching", &feature_matching->guided_matching); AddDefaultOption("FeatureMatching.skip_geometric_verification", &feature_matching->skip_geometric_verification); AddDefaultOption("FeatureMatching.rig_verification", &feature_matching->rig_verification); AddDefaultOption("FeatureMatching.skip_image_pairs_in_same_frame", &feature_matching->skip_image_pairs_in_same_frame); AddDefaultOption("FeatureMatching.max_num_matches", &feature_matching->max_num_matches); AddDefaultOption("SiftMatching.max_ratio", &feature_matching->sift->max_ratio); AddDefaultOption("SiftMatching.max_distance", &feature_matching->sift->max_distance); AddDefaultOption("SiftMatching.cross_check", &feature_matching->sift->cross_check); AddDefaultOption("SiftMatching.cpu_brute_force_matcher", &feature_matching->sift->cpu_brute_force_matcher); AddDefaultOption("SiftMatching.lightglue_min_score", &feature_matching->sift->lightglue.min_score); AddDefaultOption("SiftMatching.lightglue_model_path", &feature_matching->sift->lightglue.model_path); AddDefaultOption("AlikedMatching.brute_force_min_cossim", &feature_matching->aliked->brute_force.min_cossim); AddDefaultOption("AlikedMatching.brute_force_max_ratio", &feature_matching->aliked->brute_force.max_ratio); AddDefaultOption("AlikedMatching.brute_force_cross_check", &feature_matching->aliked->brute_force.cross_check); AddDefaultOption("AlikedMatching.bruteforce_model_path", &feature_matching->aliked->brute_force.model_path); AddDefaultOption("AlikedMatching.lightglue_min_score", &feature_matching->aliked->lightglue.min_score); AddDefaultOption("AlikedMatching.lightglue_model_path", &feature_matching->aliked->lightglue.model_path); } void OptionManager::AddTwoViewGeometryOptions() { if (added_two_view_geometry_options_) { return; } added_two_view_geometry_options_ = true; AddDefaultOption("TwoViewGeometry.min_num_inliers", &two_view_geometry->min_num_inliers); AddDefaultOption("TwoViewGeometry.multiple_models", &two_view_geometry->multiple_models); AddDefaultOption("TwoViewGeometry.compute_relative_pose", &two_view_geometry->compute_relative_pose); AddDefaultOption("TwoViewGeometry.detect_watermark", &two_view_geometry->detect_watermark); AddDefaultOption("TwoViewGeometry.multiple_ignore_watermark", &two_view_geometry->multiple_ignore_watermark); AddDefaultOption("TwoViewGeometry.watermark_detection_max_error", &two_view_geometry->watermark_detection_max_error); AddDefaultOption("TwoViewGeometry.filter_stationary_matches", &two_view_geometry->filter_stationary_matches); AddDefaultOption("TwoViewGeometry.stationary_matches_max_error", &two_view_geometry->stationary_matches_max_error); AddDefaultOption("TwoViewGeometry.max_error", &two_view_geometry->ransac_options.max_error); AddDefaultOption("TwoViewGeometry.confidence", &two_view_geometry->ransac_options.confidence); AddDefaultOption("TwoViewGeometry.max_num_trials", &two_view_geometry->ransac_options.max_num_trials); AddDefaultOption("TwoViewGeometry.min_inlier_ratio", &two_view_geometry->ransac_options.min_inlier_ratio); AddDefaultOption("TwoViewGeometry.random_seed", &two_view_geometry->ransac_options.random_seed); } void OptionManager::AddExhaustivePairingOptions() { if (added_exhaustive_pairing_options_) { return; } added_exhaustive_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("ExhaustiveMatching.block_size", &exhaustive_pairing->block_size); } void OptionManager::AddSequentialPairingOptions() { if (added_sequential_pairing_options_) { return; } added_sequential_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("SequentialMatching.overlap", &sequential_pairing->overlap); AddDefaultOption("SequentialMatching.quadratic_overlap", &sequential_pairing->quadratic_overlap); AddDefaultOption("SequentialMatching.expand_rig_images", &sequential_pairing->expand_rig_images); AddDefaultOption("SequentialMatching.loop_detection", &sequential_pairing->loop_detection); AddDefaultOption("SequentialMatching.loop_detection_period", &sequential_pairing->loop_detection_period); AddDefaultOption("SequentialMatching.loop_detection_num_images", &sequential_pairing->loop_detection_num_images); AddDefaultOption("SequentialMatching.loop_detection_num_nearest_neighbors", &sequential_pairing->loop_detection_num_nearest_neighbors); AddDefaultOption("SequentialMatching.loop_detection_num_checks", &sequential_pairing->loop_detection_num_checks); AddDefaultOption( "SequentialMatching.loop_detection_num_images_after_verification", &sequential_pairing->loop_detection_num_images_after_verification); AddDefaultOption("SequentialMatching.loop_detection_max_num_features", &sequential_pairing->loop_detection_max_num_features); AddDefaultOption("SequentialMatching.vocab_tree_path", &sequential_pairing->vocab_tree_path); AddDefaultOption("SequentialMatching.num_threads", &sequential_pairing->num_threads); } void OptionManager::AddVocabTreePairingOptions() { if (added_vocab_tree_pairing_options_) { return; } added_vocab_tree_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("VocabTreeMatching.num_images", &vocab_tree_pairing->num_images); AddDefaultOption("VocabTreeMatching.num_nearest_neighbors", &vocab_tree_pairing->num_nearest_neighbors); AddDefaultOption("VocabTreeMatching.num_checks", &vocab_tree_pairing->num_checks); AddDefaultOption("VocabTreeMatching.num_images_after_verification", &vocab_tree_pairing->num_images_after_verification); AddDefaultOption("VocabTreeMatching.max_num_features", &vocab_tree_pairing->max_num_features); AddDefaultOption("VocabTreeMatching.vocab_tree_path", &vocab_tree_pairing->vocab_tree_path); AddDefaultOption("VocabTreeMatching.match_list_path", &vocab_tree_pairing->match_list_path); AddDefaultOption("VocabTreeMatching.num_threads", &vocab_tree_pairing->num_threads); } void OptionManager::AddSpatialPairingOptions() { if (added_spatial_pairing_options_) { return; } added_spatial_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("SpatialMatching.ignore_z", &spatial_pairing->ignore_z); AddDefaultOption("SpatialMatching.max_num_neighbors", &spatial_pairing->max_num_neighbors); AddDefaultOption("SpatialMatching.min_num_neighbors", &spatial_pairing->min_num_neighbors); AddDefaultOption("SpatialMatching.max_distance", &spatial_pairing->max_distance); } void OptionManager::AddTransitivePairingOptions() { if (added_transitive_pairing_options_) { return; } added_transitive_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("TransitiveMatching.batch_size", &transitive_pairing->batch_size); AddDefaultOption("TransitiveMatching.num_iterations", &transitive_pairing->num_iterations); } void OptionManager::AddImportedPairingOptions() { if (added_image_pairs_pairing_options_) { return; } added_image_pairs_pairing_options_ = true; AddFeatureMatchingOptions(); AddTwoViewGeometryOptions(); AddDefaultOption("ImagePairsMatching.block_size", &imported_pairing->block_size); } void OptionManager::AddBundleAdjustmentOptions() { if (added_ba_options_) { return; } added_ba_options_ = true; // Solver-agnostic options AddDefaultOption("BundleAdjustment.refine_focal_length", &bundle_adjustment->refine_focal_length); AddDefaultOption("BundleAdjustment.refine_principal_point", &bundle_adjustment->refine_principal_point); AddDefaultOption("BundleAdjustment.refine_extra_params", &bundle_adjustment->refine_extra_params); AddDefaultOption("BundleAdjustment.refine_rig_from_world", &bundle_adjustment->refine_rig_from_world); AddDefaultOption("BundleAdjustment.refine_sensor_from_rig", &bundle_adjustment->refine_sensor_from_rig); AddDefaultOption("BundleAdjustment.refine_points3D", &bundle_adjustment->refine_points3D); AddDefaultOption("BundleAdjustment.constant_rig_from_world_rotation", &bundle_adjustment->constant_rig_from_world_rotation); AddDefaultOption("BundleAdjustment.min_track_length", &bundle_adjustment->min_track_length); // Ceres-specific options AddDefaultOption( "BundleAdjustmentCeres.max_num_iterations", &bundle_adjustment->ceres->solver_options.max_num_iterations); AddDefaultOption( "BundleAdjustmentCeres.max_linear_solver_iterations", &bundle_adjustment->ceres->solver_options.max_linear_solver_iterations); AddDefaultOption( "BundleAdjustmentCeres.function_tolerance", &bundle_adjustment->ceres->solver_options.function_tolerance); AddDefaultOption( "BundleAdjustmentCeres.gradient_tolerance", &bundle_adjustment->ceres->solver_options.gradient_tolerance); AddDefaultOption( "BundleAdjustmentCeres.parameter_tolerance", &bundle_adjustment->ceres->solver_options.parameter_tolerance); AddDefaultOption("BundleAdjustmentCeres.use_gpu", &bundle_adjustment->ceres->use_gpu); AddDefaultOption("BundleAdjustmentCeres.gpu_index", &bundle_adjustment->ceres->gpu_index); AddDefaultOption("BundleAdjustmentCeres.min_num_images_gpu_solver", &bundle_adjustment->ceres->min_num_images_gpu_solver); AddDefaultOption( "BundleAdjustmentCeres.min_num_residuals_for_cpu_multi_threading", &bundle_adjustment->ceres->min_num_residuals_for_cpu_multi_threading); AddDefaultOption( "BundleAdjustmentCeres.max_num_images_direct_dense_cpu_solver", &bundle_adjustment->ceres->max_num_images_direct_dense_cpu_solver); AddDefaultOption( "BundleAdjustmentCeres.max_num_images_direct_sparse_cpu_solver", &bundle_adjustment->ceres->max_num_images_direct_sparse_cpu_solver); AddDefaultOption( "BundleAdjustmentCeres.max_num_images_direct_dense_gpu_solver", &bundle_adjustment->ceres->max_num_images_direct_dense_gpu_solver); AddDefaultOption( "BundleAdjustmentCeres.max_num_images_direct_sparse_gpu_solver", &bundle_adjustment->ceres->max_num_images_direct_sparse_gpu_solver); } void OptionManager::AddMapperOptions() { if (added_mapper_options_) { return; } added_mapper_options_ = true; AddDefaultOption("Mapper.min_num_matches", &mapper->min_num_matches); AddDefaultOption("Mapper.ignore_watermarks", &mapper->ignore_watermarks); AddDefaultOption("Mapper.multiple_models", &mapper->multiple_models); AddDefaultOption("Mapper.max_num_models", &mapper->max_num_models); AddDefaultOption("Mapper.max_model_overlap", &mapper->max_model_overlap); AddDefaultOption("Mapper.min_model_size", &mapper->min_model_size); AddDefaultOption("Mapper.init_image_id1", &mapper->init_image_id1); AddDefaultOption("Mapper.init_image_id2", &mapper->init_image_id2); AddDefaultOption("Mapper.init_num_trials", &mapper->init_num_trials); AddDefaultOption("Mapper.structure_less_registration_fallback", &mapper->structure_less_registration_fallback); AddDefaultOption("Mapper.structure_less_registration_only", &mapper->structure_less_registration_only); AddDefaultOption("Mapper.extract_colors", &mapper->extract_colors); AddDefaultOption("Mapper.num_threads", &mapper->num_threads); AddDefaultOption("Mapper.random_seed", &mapper->random_seed); AddDefaultOption("Mapper.min_focal_length_ratio", &mapper->min_focal_length_ratio); AddDefaultOption("Mapper.max_focal_length_ratio", &mapper->max_focal_length_ratio); AddDefaultOption("Mapper.max_extra_param", &mapper->max_extra_param); AddDefaultOption("Mapper.ba_refine_focal_length", &mapper->ba_refine_focal_length); AddDefaultOption("Mapper.ba_refine_principal_point", &mapper->ba_refine_principal_point); AddDefaultOption("Mapper.ba_refine_extra_params", &mapper->ba_refine_extra_params); AddDefaultOption("Mapper.ba_refine_sensor_from_rig", &mapper->ba_refine_sensor_from_rig); AddDefaultOption("Mapper.ba_local_function_tolerance", &mapper->ba_local_function_tolerance); AddDefaultOption("Mapper.ba_local_max_num_iterations", &mapper->ba_local_max_num_iterations); AddDefaultOption("Mapper.ba_global_frames_ratio", &mapper->ba_global_frames_ratio); AddDefaultOption("Mapper.ba_global_points_ratio", &mapper->ba_global_points_ratio); AddDefaultOption("Mapper.ba_global_frames_freq", &mapper->ba_global_frames_freq); AddDefaultOption("Mapper.ba_global_points_freq", &mapper->ba_global_points_freq); AddDefaultOption("Mapper.ba_global_function_tolerance", &mapper->ba_global_function_tolerance); AddDefaultOption("Mapper.ba_global_max_num_iterations", &mapper->ba_global_max_num_iterations); AddDefaultOption("Mapper.ba_global_max_refinements", &mapper->ba_global_max_refinements); AddDefaultOption("Mapper.ba_global_max_refinement_change", &mapper->ba_global_max_refinement_change); AddDefaultOption("Mapper.ba_local_max_refinements", &mapper->ba_local_max_refinements); AddDefaultOption("Mapper.ba_local_max_refinement_change", &mapper->ba_local_max_refinement_change); AddDefaultOption("Mapper.ba_use_gpu", &mapper->ba_use_gpu); AddDefaultOption("Mapper.ba_gpu_index", &mapper->ba_gpu_index); AddDefaultOption("Mapper.ba_min_num_residuals_for_cpu_multi_threading", &mapper->ba_min_num_residuals_for_cpu_multi_threading); AddDefaultOption("Mapper.snapshot_path", &mapper->snapshot_path); AddDefaultOption("Mapper.snapshot_frames_freq", &mapper->snapshot_frames_freq); AddDefaultOption("Mapper.fix_existing_frames", &mapper->fix_existing_frames); // IncrementalMapper. AddDefaultOption("Mapper.init_min_num_inliers", &mapper->mapper.init_min_num_inliers); AddDefaultOption("Mapper.init_max_error", &mapper->mapper.init_max_error); AddDefaultOption("Mapper.init_max_forward_motion", &mapper->mapper.init_max_forward_motion); AddDefaultOption("Mapper.init_min_tri_angle", &mapper->mapper.init_min_tri_angle); AddDefaultOption("Mapper.init_max_reg_trials", &mapper->mapper.init_max_reg_trials); AddDefaultOption("Mapper.abs_pose_max_error", &mapper->mapper.abs_pose_max_error); AddDefaultOption("Mapper.abs_pose_min_num_inliers", &mapper->mapper.abs_pose_min_num_inliers); AddDefaultOption("Mapper.abs_pose_min_inlier_ratio", &mapper->mapper.abs_pose_min_inlier_ratio); AddDefaultOption("Mapper.filter_max_reproj_error", &mapper->mapper.filter_max_reproj_error); AddDefaultOption("Mapper.filter_min_tri_angle", &mapper->mapper.filter_min_tri_angle); AddDefaultOption("Mapper.max_reg_trials", &mapper->mapper.max_reg_trials); AddDefaultOption("Mapper.ba_local_num_images", &mapper->mapper.ba_local_num_images); AddDefaultOption("Mapper.ba_local_min_tri_angle", &mapper->mapper.ba_local_min_tri_angle); AddDefaultOption("Mapper.ba_global_ignore_redundant_points3D", &mapper->mapper.ba_global_ignore_redundant_points3D); AddDefaultOption( "Mapper.ba_global_ignore_redundant_points3D_min_coverage_gain", &mapper->mapper.ba_global_ignore_redundant_points3D_min_coverage_gain); AddDefaultOption("Mapper.image_list_path", &mapper_image_list_path_); AddDefaultOption("Mapper.constant_rig_list_path", &mapper_constant_rig_list_path_); AddDefaultOption("Mapper.constant_camera_list_path", &mapper_constant_camera_list_path_); AddDefaultOption("Mapper.max_runtime_seconds", &mapper->max_runtime_seconds); // IncrementalTriangulator. AddDefaultOption("Mapper.tri_max_transitivity", &mapper->triangulation.max_transitivity); AddDefaultOption("Mapper.tri_create_max_angle_error", &mapper->triangulation.create_max_angle_error); AddDefaultOption("Mapper.tri_continue_max_angle_error", &mapper->triangulation.continue_max_angle_error); AddDefaultOption("Mapper.tri_merge_max_reproj_error", &mapper->triangulation.merge_max_reproj_error); AddDefaultOption("Mapper.tri_complete_max_reproj_error", &mapper->triangulation.complete_max_reproj_error); AddDefaultOption("Mapper.tri_complete_max_transitivity", &mapper->triangulation.complete_max_transitivity); AddDefaultOption("Mapper.tri_re_max_angle_error", &mapper->triangulation.re_max_angle_error); AddDefaultOption("Mapper.tri_re_min_ratio", &mapper->triangulation.re_min_ratio); AddDefaultOption("Mapper.tri_re_max_trials", &mapper->triangulation.re_max_trials); AddDefaultOption("Mapper.tri_min_angle", &mapper->triangulation.min_angle); AddDefaultOption("Mapper.tri_ignore_two_view_tracks", &mapper->triangulation.ignore_two_view_tracks); } void OptionManager::AddGlobalMapperOptions() { if (added_global_mapper_options_) { return; } added_global_mapper_options_ = true; // Global mapper options. AddDefaultOption("GlobalMapper.image_list_path", &global_mapper_image_list_path_); AddDefaultOption("GlobalMapper.min_num_matches", &global_mapper->min_num_matches); AddDefaultOption("GlobalMapper.ignore_watermarks", &global_mapper->ignore_watermarks); AddDefaultOption("GlobalMapper.num_threads", &global_mapper->num_threads); AddDefaultOption("GlobalMapper.random_seed", &global_mapper->random_seed); AddDefaultOption("GlobalMapper.decompose_relative_pose", &global_mapper->decompose_relative_pose); AddDefaultOption("GlobalMapper.ba_num_iterations", &global_mapper->mapper.ba_num_iterations); AddDefaultOption("GlobalMapper.skip_rotation_averaging", &global_mapper->mapper.skip_rotation_averaging); AddDefaultOption("GlobalMapper.skip_track_establishment", &global_mapper->mapper.skip_track_establishment); AddDefaultOption("GlobalMapper.skip_global_positioning", &global_mapper->mapper.skip_global_positioning); AddDefaultOption("GlobalMapper.skip_bundle_adjustment", &global_mapper->mapper.skip_bundle_adjustment); AddDefaultOption("GlobalMapper.skip_retriangulation", &global_mapper->mapper.skip_retriangulation); // Track establishment options. AddDefaultOption( "GlobalMapper.track_intra_image_consistency_threshold", &global_mapper->mapper.track_intra_image_consistency_threshold); AddDefaultOption("GlobalMapper.track_required_tracks_per_view", &global_mapper->mapper.track_required_tracks_per_view); AddDefaultOption("GlobalMapper.track_min_num_views_per_track", &global_mapper->mapper.track_min_num_views_per_track); // Global positioning options. AddDefaultOption("GlobalMapper.gp_use_gpu", &global_mapper->mapper.global_positioning.use_gpu); AddDefaultOption("GlobalMapper.gp_gpu_index", &global_mapper->mapper.global_positioning.gpu_index); AddDefaultOption( "GlobalMapper.gp_optimize_positions", &global_mapper->mapper.global_positioning.optimize_positions); AddDefaultOption("GlobalMapper.gp_optimize_points", &global_mapper->mapper.global_positioning.optimize_points); AddDefaultOption("GlobalMapper.gp_optimize_scales", &global_mapper->mapper.global_positioning.optimize_scales); AddDefaultOption( "GlobalMapper.gp_loss_function_scale", &global_mapper->mapper.global_positioning.loss_function_scale); AddDefaultOption("GlobalMapper.gp_max_num_iterations", &global_mapper->mapper.global_positioning.solver_options .max_num_iterations); // Bundle adjustment options (solver-agnostic). AddDefaultOption( "GlobalMapper.ba_refine_focal_length", &global_mapper->mapper.bundle_adjustment.refine_focal_length); AddDefaultOption( "GlobalMapper.ba_refine_principal_point", &global_mapper->mapper.bundle_adjustment.refine_principal_point); AddDefaultOption( "GlobalMapper.ba_refine_extra_params", &global_mapper->mapper.bundle_adjustment.refine_extra_params); AddDefaultOption( "GlobalMapper.ba_refine_sensor_from_rig", &global_mapper->mapper.bundle_adjustment.refine_sensor_from_rig); AddDefaultOption( "GlobalMapper.ba_refine_rig_from_world", &global_mapper->mapper.bundle_adjustment.refine_rig_from_world); AddDefaultOption("GlobalMapper.ba_refine_points3D", &global_mapper->mapper.bundle_adjustment.refine_points3D); AddDefaultOption("GlobalMapper.ba_min_track_length", &global_mapper->mapper.bundle_adjustment.min_track_length); // Bundle adjustment options (Ceres-specific). AddDefaultOption("GlobalMapper.ba_ceres_use_gpu", &global_mapper->mapper.bundle_adjustment.ceres->use_gpu); AddDefaultOption("GlobalMapper.ba_ceres_gpu_index", &global_mapper->mapper.bundle_adjustment.ceres->gpu_index); AddDefaultOption( "GlobalMapper.ba_ceres_loss_function_scale", &global_mapper->mapper.bundle_adjustment.ceres->loss_function_scale); AddDefaultOption("GlobalMapper.ba_ceres_max_num_iterations", &global_mapper->mapper.bundle_adjustment.ceres ->solver_options.max_num_iterations); AddDefaultOption("GlobalMapper.ba_skip_fixed_rotation_stage", &global_mapper->mapper.ba_skip_fixed_rotation_stage); AddDefaultOption("GlobalMapper.ba_skip_joint_optimization_stage", &global_mapper->mapper.ba_skip_joint_optimization_stage); // Retriangulation options. AddDefaultOption( "GlobalMapper.tri_complete_max_reproj_error", &global_mapper->mapper.retriangulation.complete_max_reproj_error); AddDefaultOption( "GlobalMapper.tri_merge_max_reproj_error", &global_mapper->mapper.retriangulation.merge_max_reproj_error); AddDefaultOption("GlobalMapper.tri_min_angle", &global_mapper->mapper.retriangulation.min_angle); // Rotation averaging options. AddDefaultOption("GlobalMapper.ra_use_gravity", &global_mapper->mapper.rotation_averaging.use_gravity); AddDefaultOption("GlobalMapper.ra_use_stratified", &global_mapper->mapper.rotation_averaging.use_stratified); AddDefaultOption( "GlobalMapper.ra_max_rotation_error_deg", &global_mapper->mapper.rotation_averaging.max_rotation_error_deg); // Threshold options. AddDefaultOption("GlobalMapper.max_angular_reproj_error_deg", &global_mapper->mapper.max_angular_reproj_error_deg); AddDefaultOption("GlobalMapper.max_normalized_reproj_error", &global_mapper->mapper.max_normalized_reproj_error); AddDefaultOption("GlobalMapper.min_tri_angle_deg", &global_mapper->mapper.min_tri_angle_deg); } void OptionManager::AddGravityRefinerOptions() { if (added_gravity_refiner_options_) { return; } added_gravity_refiner_options_ = true; AddDefaultOption("GravityRefiner.max_outlier_ratio", &gravity_refiner->max_outlier_ratio); AddDefaultOption("GravityRefiner.max_gravity_error", &gravity_refiner->max_gravity_error); AddDefaultOption("GravityRefiner.min_num_neighbors", &gravity_refiner->min_num_neighbors); } void OptionManager::AddReconstructionClustererOptions() { if (added_reconstruction_clusterer_options_) { return; } added_reconstruction_clusterer_options_ = true; AddDefaultOption("ReconstructionClusterer.min_covisibility_count", &reconstruction_clusterer->min_covisibility_count); AddDefaultOption("ReconstructionClusterer.min_edge_weight_threshold", &reconstruction_clusterer->min_edge_weight_threshold); AddDefaultOption("ReconstructionClusterer.min_num_reg_frames", &reconstruction_clusterer->min_num_reg_frames); } void OptionManager::AddPatchMatchStereoOptions() { if (added_patch_match_stereo_options_) { return; } added_patch_match_stereo_options_ = true; AddDefaultOption("PatchMatchStereo.max_image_size", &patch_match_stereo->max_image_size); AddDefaultOption("PatchMatchStereo.gpu_index", &patch_match_stereo->gpu_index); AddDefaultOption("PatchMatchStereo.depth_min", &patch_match_stereo->depth_min); AddDefaultOption("PatchMatchStereo.depth_max", &patch_match_stereo->depth_max); AddDefaultOption("PatchMatchStereo.window_radius", &patch_match_stereo->window_radius); AddDefaultOption("PatchMatchStereo.window_step", &patch_match_stereo->window_step); AddDefaultOption("PatchMatchStereo.sigma_spatial", &patch_match_stereo->sigma_spatial); AddDefaultOption("PatchMatchStereo.sigma_color", &patch_match_stereo->sigma_color); AddDefaultOption("PatchMatchStereo.num_samples", &patch_match_stereo->num_samples); AddDefaultOption("PatchMatchStereo.ncc_sigma", &patch_match_stereo->ncc_sigma); AddDefaultOption("PatchMatchStereo.min_triangulation_angle", &patch_match_stereo->min_triangulation_angle); AddDefaultOption("PatchMatchStereo.incident_angle_sigma", &patch_match_stereo->incident_angle_sigma); AddDefaultOption("PatchMatchStereo.num_iterations", &patch_match_stereo->num_iterations); AddDefaultOption("PatchMatchStereo.geom_consistency", &patch_match_stereo->geom_consistency); AddDefaultOption("PatchMatchStereo.geom_consistency_regularizer", &patch_match_stereo->geom_consistency_regularizer); AddDefaultOption("PatchMatchStereo.geom_consistency_max_cost", &patch_match_stereo->geom_consistency_max_cost); AddDefaultOption("PatchMatchStereo.filter", &patch_match_stereo->filter); AddDefaultOption("PatchMatchStereo.filter_min_ncc", &patch_match_stereo->filter_min_ncc); AddDefaultOption("PatchMatchStereo.filter_min_triangulation_angle", &patch_match_stereo->filter_min_triangulation_angle); AddDefaultOption("PatchMatchStereo.filter_min_num_consistent", &patch_match_stereo->filter_min_num_consistent); AddDefaultOption("PatchMatchStereo.filter_geom_consistency_max_cost", &patch_match_stereo->filter_geom_consistency_max_cost); AddDefaultOption("PatchMatchStereo.cache_size", &patch_match_stereo->cache_size); AddDefaultOption("PatchMatchStereo.allow_missing_files", &patch_match_stereo->allow_missing_files); AddDefaultOption("PatchMatchStereo.write_consistency_graph", &patch_match_stereo->write_consistency_graph); AddDefaultOption("PatchMatchStereo.num_threads", &patch_match_stereo->num_threads); } void OptionManager::AddStereoFusionOptions() { if (added_stereo_fusion_options_) { return; } added_stereo_fusion_options_ = true; AddDefaultOption("StereoFusion.mask_path", &stereo_fusion->mask_path); AddDefaultOption("StereoFusion.num_threads", &stereo_fusion->num_threads); AddDefaultOption("StereoFusion.max_image_size", &stereo_fusion->max_image_size); AddDefaultOption("StereoFusion.min_num_pixels", &stereo_fusion->min_num_pixels); AddDefaultOption("StereoFusion.max_num_pixels", &stereo_fusion->max_num_pixels); AddDefaultOption("StereoFusion.max_traversal_depth", &stereo_fusion->max_traversal_depth); AddDefaultOption("StereoFusion.max_reproj_error", &stereo_fusion->max_reproj_error); AddDefaultOption("StereoFusion.max_depth_error", &stereo_fusion->max_depth_error); AddDefaultOption("StereoFusion.max_normal_error", &stereo_fusion->max_normal_error); AddDefaultOption("StereoFusion.check_num_images", &stereo_fusion->check_num_images); AddDefaultOption("StereoFusion.cache_size", &stereo_fusion->cache_size); AddDefaultOption("StereoFusion.use_cache", &stereo_fusion->use_cache); } void OptionManager::AddPoissonMeshingOptions() { if (added_poisson_meshing_options_) { return; } added_poisson_meshing_options_ = true; AddDefaultOption("PoissonMeshing.point_weight", &poisson_meshing->point_weight); AddDefaultOption("PoissonMeshing.depth", &poisson_meshing->depth); AddDefaultOption("PoissonMeshing.color", &poisson_meshing->color); AddDefaultOption("PoissonMeshing.trim", &poisson_meshing->trim); AddDefaultOption("PoissonMeshing.num_threads", &poisson_meshing->num_threads); } void OptionManager::AddDelaunayMeshingOptions() { if (added_delaunay_meshing_options_) { return; } added_delaunay_meshing_options_ = true; AddDefaultOption("DelaunayMeshing.max_proj_dist", &delaunay_meshing->max_proj_dist); AddDefaultOption("DelaunayMeshing.max_depth_dist", &delaunay_meshing->max_depth_dist); AddDefaultOption("DelaunayMeshing.visibility_sigma", &delaunay_meshing->visibility_sigma); AddDefaultOption("DelaunayMeshing.distance_sigma_factor", &delaunay_meshing->distance_sigma_factor); AddDefaultOption("DelaunayMeshing.quality_regularization", &delaunay_meshing->quality_regularization); AddDefaultOption("DelaunayMeshing.max_side_length_factor", &delaunay_meshing->max_side_length_factor); AddDefaultOption("DelaunayMeshing.max_side_length_percentile", &delaunay_meshing->max_side_length_percentile); AddDefaultOption("DelaunayMeshing.num_threads", &delaunay_meshing->num_threads); } void OptionManager::AddMeshTextureMappingOptions() { if (added_mesh_texture_mapping_options_) { return; } added_mesh_texture_mapping_options_ = true; AddDefaultOption("MeshTextureMapping.min_cos_normal_angle", &mesh_texture_mapping->min_cos_normal_angle); AddDefaultOption("MeshTextureMapping.min_visible_vertices", &mesh_texture_mapping->min_visible_vertices); AddDefaultOption("MeshTextureMapping.view_selection_smoothing_iterations", &mesh_texture_mapping->view_selection_smoothing_iterations); AddDefaultOption("MeshTextureMapping.atlas_patch_padding", &mesh_texture_mapping->atlas_patch_padding); AddDefaultOption("MeshTextureMapping.inpaint_radius", &mesh_texture_mapping->inpaint_radius); AddDefaultOption("MeshTextureMapping.apply_color_correction", &mesh_texture_mapping->apply_color_correction); AddDefaultOption("MeshTextureMapping.color_correction_regularization", &mesh_texture_mapping->color_correction_regularization); AddDefaultOption("MeshTextureMapping.num_threads", &mesh_texture_mapping->num_threads); AddDefaultOption("MeshTextureMapping.texture_scale_factor", &mesh_texture_mapping->texture_scale_factor); } void OptionManager::AddMeshSimplificationOptions() { if (added_mesh_simplification_options_) { return; } added_mesh_simplification_options_ = true; AddDefaultOption("MeshSimplification.target_face_ratio", &mesh_simplification->target_face_ratio); AddDefaultOption("MeshSimplification.max_error", &mesh_simplification->max_error); AddDefaultOption("MeshSimplification.boundary_weight", &mesh_simplification->boundary_weight); AddDefaultOption("MeshSimplification.interpolate_colors", &mesh_simplification->interpolate_colors); AddDefaultOption("MeshSimplification.num_threads", &mesh_simplification->num_threads); } void OptionManager::AddRenderOptions() { if (added_render_options_) { return; } added_render_options_ = true; AddDefaultOption("Render.min_track_len", &render->min_track_len); AddDefaultOption("Render.max_error", &render->max_error); AddDefaultOption("Render.refresh_rate", &render->refresh_rate); AddDefaultOption("Render.adapt_refresh_rate", &render->adapt_refresh_rate); AddDefaultOption("Render.image_connections", &render->image_connections); AddDefaultOption("Render.projection_type", &render->projection_type); } void OptionManager::Reset(bool reset_logging) { BaseOptionManager::Reset(reset_logging); added_feature_extraction_options_ = false; added_feature_matching_options_ = false; added_two_view_geometry_options_ = false; added_exhaustive_pairing_options_ = false; added_sequential_pairing_options_ = false; added_vocab_tree_pairing_options_ = false; added_spatial_pairing_options_ = false; added_transitive_pairing_options_ = false; added_image_pairs_pairing_options_ = false; added_ba_options_ = false; added_mapper_options_ = false; added_global_mapper_options_ = false; added_gravity_refiner_options_ = false; added_reconstruction_clusterer_options_ = false; added_patch_match_stereo_options_ = false; added_stereo_fusion_options_ = false; added_poisson_meshing_options_ = false; added_delaunay_meshing_options_ = false; added_mesh_texture_mapping_options_ = false; added_render_options_ = false; } void OptionManager::ResetOptions(const bool reset_paths) { *image_reader = ImageReaderOptions(); *feature_extraction = FeatureExtractionOptions(); *feature_matching = FeatureMatchingOptions(); *exhaustive_pairing = ExhaustivePairingOptions(); *sequential_pairing = SequentialPairingOptions(); *vocab_tree_pairing = VocabTreePairingOptions(); *spatial_pairing = SpatialPairingOptions(); *transitive_pairing = TransitivePairingOptions(); *imported_pairing = ImportedPairingOptions(); *bundle_adjustment = BundleAdjustmentOptions(); *mapper = IncrementalPipelineOptions(); *global_mapper = GlobalPipelineOptions(); *gravity_refiner = GravityRefinerOptions(); *reconstruction_clusterer = ReconstructionClusteringOptions(); *patch_match_stereo = mvs::PatchMatchOptions(); *stereo_fusion = mvs::StereoFusionOptions(); *poisson_meshing = mvs::PoissonMeshingOptions(); *delaunay_meshing = mvs::DelaunayMeshingOptions(); *mesh_texture_mapping = mvs::MeshTextureMappingOptions(); *mesh_simplification = mvs::MeshSimplificationOptions(); *render = RenderOptions(); BaseOptionManager::ResetOptions(reset_paths); } bool OptionManager::Check() { if (!BaseOptionManager::Check()) { return false; } bool success = true; if (image_reader) success = success && image_reader->Check(); if (feature_extraction) success = success && feature_extraction->Check(); if (feature_matching) success = success && feature_matching->Check(); if (two_view_geometry) success = success && two_view_geometry->Check(); if (exhaustive_pairing) success = success && exhaustive_pairing->Check(); if (sequential_pairing) success = success && sequential_pairing->Check(); if (vocab_tree_pairing) success = success && vocab_tree_pairing->Check(); if (spatial_pairing) success = success && spatial_pairing->Check(); if (transitive_pairing) success = success && transitive_pairing->Check(); if (imported_pairing) success = success && imported_pairing->Check(); if (bundle_adjustment) success = success && bundle_adjustment->Check(); if (mapper) success = success && mapper->Check(); if (patch_match_stereo) success = success && patch_match_stereo->Check(); if (stereo_fusion) success = success && stereo_fusion->Check(); if (poisson_meshing) success = success && poisson_meshing->Check(); if (delaunay_meshing) success = success && delaunay_meshing->Check(); if (mesh_texture_mapping) success = success && mesh_texture_mapping->Check(); #if defined(COLMAP_GUI_ENABLED) if (render) success = success && render->Check(); #endif return success; } bool OptionManager::Read(const std::filesystem::path& path, bool allow_unregistered) { if (!BaseOptionManager::Read(path, allow_unregistered)) { return false; } return Check(); } void OptionManager::PostParse() { if (!mapper_image_list_path_.empty()) { mapper->image_names = ReadTextFileLines(mapper_image_list_path_); } if (!global_mapper_image_list_path_.empty()) { global_mapper->image_names = ReadTextFileLines(global_mapper_image_list_path_); } if (!mapper_constant_rig_list_path_.empty()) { for (const std::string& line : ReadTextFileLines(mapper_constant_rig_list_path_)) { mapper->constant_rigs.insert(std::stoi(line)); } } if (!mapper_constant_camera_list_path_.empty()) { for (const std::string& line : ReadTextFileLines(mapper_constant_camera_list_path_)) { mapper->constant_cameras.insert(std::stoi(line)); } } } void OptionManager::PrintHelp() const { LOG(INFO) << StringPrintf( "%s (%s)", GetVersionInfo().c_str(), GetBuildInfo().c_str()); LOG(INFO) << "Options can either be specified via command-line or by " "defining them in a .ini project file passed to " "`--project_path`.\n" << *desc_; } } // namespace colmap colmap-4.0.4/src/colmap/controllers/option_manager.h000066400000000000000000000153361517363634500226020ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/base_option_manager.h" #include namespace colmap { struct ImageReaderOptions; struct FeatureExtractionOptions; struct FeatureMatchingOptions; struct SiftMatchingOptions; struct TwoViewGeometryOptions; struct ExhaustivePairingOptions; struct SequentialPairingOptions; struct VocabTreePairingOptions; struct SpatialPairingOptions; struct TransitivePairingOptions; struct ImportedPairingOptions; struct ExistingMatchedPairingOptions; struct BundleAdjustmentOptions; struct IncrementalPipelineOptions; struct GlobalPipelineOptions; struct RenderOptions; struct ReconstructionClusteringOptions; namespace mvs { struct PatchMatchOptions; struct StereoFusionOptions; struct PoissonMeshingOptions; struct DelaunayMeshingOptions; struct MeshTextureMappingOptions; struct MeshSimplificationOptions; } // namespace mvs struct GravityRefinerOptions; } // namespace colmap namespace colmap { class OptionManager : public BaseOptionManager { public: explicit OptionManager(bool add_project_options = true); // Create "optimal" set of options for different reconstruction scenarios. void ModifyForIndividualData(); void ModifyForVideoData(); void ModifyForInternetData(); // Create "optimal" set of options for different quality settings. // Note that the existing options are modified, so if your parameters are // already low quality, they will be further degraded. void ModifyForLowQuality(); void ModifyForMediumQuality(); void ModifyForHighQuality(); void ModifyForExtremeQuality(); void AddAllOptions() override; void AddFeatureExtractionOptions(); void AddFeatureMatchingOptions(); void AddTwoViewGeometryOptions(); void AddExhaustivePairingOptions(); void AddSequentialPairingOptions(); void AddVocabTreePairingOptions(); void AddSpatialPairingOptions(); void AddTransitivePairingOptions(); void AddImportedPairingOptions(); void AddBundleAdjustmentOptions(); void AddMapperOptions(); void AddGlobalMapperOptions(); void AddGravityRefinerOptions(); void AddReconstructionClustererOptions(); void AddPatchMatchStereoOptions(); void AddStereoFusionOptions(); void AddPoissonMeshingOptions(); void AddDelaunayMeshingOptions(); void AddMeshTextureMappingOptions(); void AddMeshSimplificationOptions(); void AddRenderOptions(); void Reset(bool reset_logging = true) override; void ResetOptions(bool reset_paths) override; bool Check() override; bool Read(const std::filesystem::path& path, bool allow_unregistered = true) override; std::shared_ptr image_reader; std::shared_ptr feature_extraction; std::shared_ptr feature_matching; std::shared_ptr two_view_geometry; std::shared_ptr exhaustive_pairing; std::shared_ptr sequential_pairing; std::shared_ptr vocab_tree_pairing; std::shared_ptr spatial_pairing; std::shared_ptr transitive_pairing; std::shared_ptr imported_pairing; std::shared_ptr bundle_adjustment; std::shared_ptr mapper; std::shared_ptr global_mapper; std::shared_ptr reconstruction_clusterer; std::shared_ptr gravity_refiner; std::shared_ptr patch_match_stereo; std::shared_ptr stereo_fusion; std::shared_ptr poisson_meshing; std::shared_ptr delaunay_meshing; std::shared_ptr mesh_texture_mapping; std::shared_ptr mesh_simplification; std::shared_ptr render; protected: void PostParse() override; void PrintHelp() const override; std::filesystem::path mapper_image_list_path_; std::filesystem::path mapper_constant_rig_list_path_; std::filesystem::path mapper_constant_camera_list_path_; std::filesystem::path global_mapper_image_list_path_; bool added_feature_extraction_options_ = false; bool added_feature_matching_options_ = false; bool added_two_view_geometry_options_ = false; bool added_exhaustive_pairing_options_ = false; bool added_sequential_pairing_options_ = false; bool added_vocab_tree_pairing_options_ = false; bool added_spatial_pairing_options_ = false; bool added_transitive_pairing_options_ = false; bool added_image_pairs_pairing_options_ = false; bool added_ba_options_ = false; bool added_mapper_options_ = false; bool added_global_mapper_options_ = false; bool added_gravity_refiner_options_ = false; bool added_reconstruction_clusterer_options_ = false; bool added_patch_match_stereo_options_ = false; bool added_stereo_fusion_options_ = false; bool added_poisson_meshing_options_ = false; bool added_delaunay_meshing_options_ = false; bool added_mesh_texture_mapping_options_ = false; bool added_mesh_simplification_options_ = false; bool added_render_options_ = false; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/option_manager_test.cc000066400000000000000000000275731517363634500240050ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/option_manager.h" #include "colmap/controllers/image_reader.h" #include "colmap/controllers/incremental_pipeline.h" #include "colmap/feature/sift.h" #include "colmap/mvs/patch_match_options.h" #include "colmap/util/file.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { TEST(OptionManager, Reset) { OptionManager options; *options.database_path = "/test/path"; *options.image_path = "/test/images"; options.AddDatabaseOptions(); options.AddImageOptions(); EXPECT_EQ(*options.database_path, "/test/path"); EXPECT_EQ(*options.image_path, "/test/images"); options.Reset(); EXPECT_EQ(*options.database_path, ""); EXPECT_EQ(*options.image_path, ""); } TEST(OptionManager, ResetOptions) { OptionManager options; *options.database_path = "/test/path"; *options.image_path = "/test/images"; const int original_num_threads = options.feature_extraction->num_threads; options.feature_extraction->num_threads = original_num_threads + 42; options.ResetOptions(/*reset_paths=*/true); EXPECT_EQ(*options.database_path, ""); EXPECT_EQ(*options.image_path, ""); EXPECT_EQ(options.feature_extraction->num_threads, original_num_threads); *options.database_path = "/test/path"; *options.image_path = "/test/images"; options.feature_extraction->num_threads = original_num_threads + 42; options.ResetOptions(/*reset_paths=*/false); EXPECT_EQ(*options.database_path, "/test/path"); EXPECT_EQ(*options.image_path, "/test/images"); EXPECT_EQ(options.feature_extraction->num_threads, original_num_threads); } TEST(OptionManager, AddOptionsIdempotent) { OptionManager options; // Adding options multiple times should not cause issues options.AddLogOptions(); options.AddLogOptions(); options.AddRandomOptions(); options.AddRandomOptions(); options.AddFeatureExtractionOptions(); options.AddFeatureExtractionOptions(); options.AddFeatureMatchingOptions(); options.AddFeatureMatchingOptions(); options.AddMapperOptions(); options.AddMapperOptions(); // If idempotency is not maintained, the above would cause errors SUCCEED(); } TEST(OptionManager, AddAllOptions) { OptionManager options; options.AddAllOptions(); // Verify that at least some key options are initialized EXPECT_NE(options.image_reader, nullptr); EXPECT_NE(options.feature_extraction, nullptr); EXPECT_NE(options.feature_matching, nullptr); EXPECT_NE(options.bundle_adjustment, nullptr); EXPECT_NE(options.mapper, nullptr); EXPECT_NE(options.patch_match_stereo, nullptr); } TEST(OptionManager, WriteAndRead) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; // Create necessary directories CreateDirIfNotExists(test_dir / "images"); // Create and configure an OptionManager OptionManager options_write; options_write.AddDatabaseOptions(); options_write.AddImageOptions(); options_write.AddFeatureExtractionOptions(); options_write.AddMapperOptions(); options_write.AddGlobalMapperOptions(); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; options_write.feature_extraction->max_image_size = 2048; options_write.feature_extraction->sift->max_num_features = 4096; options_write.mapper->min_num_matches = 20; // Write to file options_write.Write(config_path); EXPECT_TRUE(ExistsFile(config_path)); // Read from file OptionManager options_read; options_read.AddDatabaseOptions(); options_read.AddImageOptions(); options_read.AddFeatureExtractionOptions(); options_read.AddMapperOptions(); options_read.AddGlobalMapperOptions(); EXPECT_TRUE(options_read.Read(config_path)); // Verify that values were read correctly EXPECT_EQ(*options_read.database_path, *options_write.database_path); EXPECT_EQ(*options_read.image_path, *options_write.image_path); EXPECT_EQ(options_read.feature_extraction->max_image_size, options_write.feature_extraction->max_image_size); EXPECT_EQ(options_read.feature_extraction->sift->max_num_features, options_write.feature_extraction->sift->max_num_features); EXPECT_EQ(options_read.mapper->min_num_matches, options_write.mapper->min_num_matches); } TEST(OptionManager, ReRead) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; // Create necessary directories CreateDirIfNotExists(test_dir / "images"); // Create and write initial config OptionManager options_write; options_write.AddAllOptions(); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; options_write.feature_extraction->max_image_size = 2048; options_write.Write(config_path); // Read with ReRead OptionManager options_read; EXPECT_TRUE(options_read.ReRead(config_path)); // Verify values EXPECT_EQ(*options_read.database_path, *options_write.database_path); EXPECT_EQ(*options_read.image_path, *options_write.image_path); EXPECT_EQ(options_read.feature_extraction->max_image_size, 2048); } TEST(OptionManager, ReadNonExistentFile) { OptionManager options; options.AddAllOptions(); EXPECT_FALSE(options.Read("/path/that/does/not/exist.ini")); } TEST(OptionManager, Check) { const auto test_dir = CreateTestDir(); OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); // Should fail with non-existent paths *options.database_path = test_dir / "database.db"; *options.image_path = "/path/that/does/not/exist"; EXPECT_FALSE(options.Check()); // Should succeed with valid paths CreateDirIfNotExists(test_dir / "images"); *options.image_path = test_dir / "images"; EXPECT_TRUE(options.Check()); } TEST(OptionManager, CheckDatabaseParentDir) { const auto test_dir = CreateTestDir(); OptionManager options; options.AddDatabaseOptions(); // Should succeed when database parent dir exists *options.database_path = test_dir / "database.db"; EXPECT_TRUE(options.Check()); // Should fail when database path is a directory CreateDirIfNotExists(test_dir / "bad_database"); *options.database_path = test_dir / "bad_database"; EXPECT_FALSE(options.Check()); } TEST(OptionManager, ParseWithOptions) { const auto test_dir = CreateTestDir(); CreateDirIfNotExists(test_dir / "images"); OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddFeatureExtractionOptions(); const auto database_path = test_dir / "database.db"; const auto image_path = test_dir / "images"; // Create argv with additional options const std::vector args = { "colmap", "--database_path", database_path.string(), "--image_path", image_path.string(), "--FeatureExtraction.max_image_size", "1024", "--SiftExtraction.max_num_features", "2048", }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Verify parsed values EXPECT_EQ(*options.database_path, database_path); EXPECT_EQ(*options.image_path, image_path); EXPECT_EQ(options.feature_extraction->max_image_size, 1024); EXPECT_EQ(options.feature_extraction->sift->max_num_features, 2048); } TEST(OptionManager, ParseWithProjectPath) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; CreateDirIfNotExists(test_dir / "images"); // Create and write a config file OptionManager options_write; options_write.AddDatabaseOptions(); options_write.AddImageOptions(); options_write.AddFeatureExtractionOptions(); *options_write.database_path = test_dir / "database.db"; *options_write.image_path = test_dir / "images"; options_write.feature_extraction->max_image_size = 3000; options_write.Write(config_path); // Parse using project_path OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddFeatureExtractionOptions(); const std::vector args = { "colmap", "--project_path", config_path.string(), }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } EXPECT_TRUE(options.Parse(argv.size(), argv.data())); // Verify values were loaded from config file EXPECT_EQ(*options.database_path, *options_write.database_path); EXPECT_EQ(*options.image_path, *options_write.image_path); EXPECT_EQ(options.feature_extraction->max_image_size, 3000); } TEST(OptionManager, ParseEmptyArguments) { OptionManager options; const std::vector args = {"colmap"}; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } // Should succeed with no required options EXPECT_TRUE(options.Parse(argv.size(), argv.data())); } TEST(OptionManager, ParseUnknownArgumentsFails) { const auto test_dir = CreateTestDir(); OptionManager options; options.AddDatabaseOptions(); const auto database_path = test_dir / "database.db"; // Create argv with an unknown option const std::vector args = { "colmap", "--database_path", database_path.string(), "--unknown_option", "value", }; std::vector argv; argv.reserve(args.size()); for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } // Should return false when encountering unknown option EXPECT_FALSE(options.Parse(argv.size(), argv.data())); } TEST(OptionManager, WriteAfterResetOptions) { const auto test_dir = CreateTestDir(); const auto config_path = test_dir / "config.ini"; OptionManager options; options.AddAllOptions(); *options.database_path = test_dir / "database.db"; CreateDirIfNotExists(test_dir / "images"); *options.image_path = test_dir / "images"; // ResetOptions reassigns option structs, which reallocates sub-objects // (e.g., feature_matching->sift, bundle_adjustment->ceres). This must not // invalidate the raw pointers registered by AddAllOptions, otherwise // Write() will dereference dangling pointers. options.ResetOptions(/*reset_paths=*/false); EXPECT_NO_THROW(options.Write(config_path)); EXPECT_TRUE(ExistsFile(config_path)); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/pairing.cc000066400000000000000000000753301517363634500213670ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/pairing.h" #include "colmap/feature/utils.h" #include "colmap/geometry/gps.h" #include "colmap/retrieval/resources.h" #include "colmap/util/file.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" #include #include #include #include #include #include namespace colmap { namespace { std::vector> ReadImagePairsText( const std::filesystem::path& path, const std::unordered_map& image_name_to_image_id) { std::ifstream file(path); THROW_CHECK_FILE_OPEN(file, path); std::string line; std::vector> image_pairs; std::unordered_set image_pairs_set; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } std::stringstream line_stream(line); std::string image_name1; std::string image_name2; std::getline(line_stream, image_name1, ' '); StringTrim(&image_name1); std::getline(line_stream, image_name2, ' '); StringTrim(&image_name2); if (image_name_to_image_id.count(image_name1) == 0) { LOG(ERROR) << "Image " << image_name1 << " does not exist."; continue; } if (image_name_to_image_id.count(image_name2) == 0) { LOG(ERROR) << "Image " << image_name2 << " does not exist."; continue; } const image_t image_id1 = image_name_to_image_id.at(image_name1); const image_t image_id2 = image_name_to_image_id.at(image_name2); const image_pair_t image_pair = ImagePairToPairId(image_id1, image_id2); const bool image_pair_exists = image_pairs_set.insert(image_pair).second; if (image_pair_exists) { image_pairs.emplace_back(image_id1, image_id2); } } return image_pairs; } } // namespace bool ExistingMatchedPairingOptions::Check() const { CHECK_OPTION_GT(batch_size, 1); return true; } bool ExhaustivePairingOptions::Check() const { CHECK_OPTION_GT(block_size, 1); return true; } bool VocabTreePairingOptions::Check() const { CHECK_OPTION_GT(num_images, 0); CHECK_OPTION_GT(num_nearest_neighbors, 0); CHECK_OPTION_GT(num_checks, 0); return true; } bool SequentialPairingOptions::Check() const { CHECK_OPTION_GT(overlap, 0); CHECK_OPTION_GT(loop_detection_period, 0); CHECK_OPTION_GT(loop_detection_num_images, 0); CHECK_OPTION_GT(loop_detection_num_nearest_neighbors, 0); CHECK_OPTION_GT(loop_detection_num_checks, 0); return true; } VocabTreePairingOptions SequentialPairingOptions::VocabTreeOptions() const { VocabTreePairingOptions options; options.num_images = loop_detection_num_images; options.num_nearest_neighbors = loop_detection_num_nearest_neighbors; options.num_checks = loop_detection_num_checks; options.num_images_after_verification = loop_detection_num_images_after_verification; options.max_num_features = loop_detection_max_num_features; options.vocab_tree_path = vocab_tree_path; options.num_threads = num_threads; return options; } bool SpatialPairingOptions::Check() const { CHECK_OPTION_GE(max_distance, 0.0); CHECK_OPTION_GT(max_num_neighbors, 0); CHECK_OPTION_LE(min_num_neighbors, max_num_neighbors); CHECK_OPTION_GE(min_num_neighbors, 0); CHECK_OPTION(max_distance > 0.0 || min_num_neighbors > 0); return true; } bool TransitivePairingOptions::Check() const { CHECK_OPTION_GT(batch_size, 0); CHECK_OPTION_GT(num_iterations, 0); return true; } bool ImportedPairingOptions::Check() const { CHECK_OPTION_GT(block_size, 0); return true; } bool FeaturePairsMatchingOptions::Check() const { return true; } std::vector> PairGenerator::AllPairs() { std::vector> image_pairs; while (!this->HasFinished()) { std::vector> image_pairs_block = this->Next(); image_pairs.insert(image_pairs.end(), std::make_move_iterator(image_pairs_block.begin()), std::make_move_iterator(image_pairs_block.end())); } return image_pairs; } ExhaustivePairGenerator::ExhaustivePairGenerator( const ExhaustivePairingOptions& options, const std::shared_ptr& cache) : options_(options), image_ids_(THROW_CHECK_NOTNULL(cache)->GetImageIds()), block_size_(static_cast(options_.block_size)), num_blocks_(static_cast( std::ceil(static_cast(image_ids_.size()) / block_size_))) { THROW_CHECK(options.Check()); LOG(INFO) << "Generating exhaustive image pairs..."; const size_t num_pairs_per_block = block_size_ * (block_size_ - 1) / 2; image_pairs_.reserve(num_pairs_per_block); } ExhaustivePairGenerator::ExhaustivePairGenerator( const ExhaustivePairingOptions& options, const std::shared_ptr& database) : ExhaustivePairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void ExhaustivePairGenerator::Reset() { start_idx1_ = 0; start_idx2_ = 0; } bool ExhaustivePairGenerator::HasFinished() const { return start_idx1_ >= image_ids_.size(); } std::vector> ExhaustivePairGenerator::Next() { image_pairs_.clear(); if (HasFinished()) { return image_pairs_; } const size_t end_idx1 = std::min(image_ids_.size(), start_idx1_ + block_size_) - 1; const size_t end_idx2 = std::min(image_ids_.size(), start_idx2_ + block_size_) - 1; LOG(INFO) << StringPrintf("Processing block [%d/%d, %d/%d]", start_idx1_ / block_size_ + 1, num_blocks_, start_idx2_ / block_size_ + 1, num_blocks_); for (size_t idx1 = start_idx1_; idx1 <= end_idx1; ++idx1) { for (size_t idx2 = start_idx2_; idx2 <= end_idx2; ++idx2) { const size_t block_id1 = idx1 % block_size_; const size_t block_id2 = idx2 % block_size_; if ((idx1 > idx2 && block_id1 <= block_id2) || (idx1 < idx2 && block_id1 < block_id2)) { // Avoid duplicate pairs image_pairs_.emplace_back(image_ids_[idx1], image_ids_[idx2]); } } } start_idx2_ += block_size_; if (start_idx2_ >= image_ids_.size()) { start_idx2_ = 0; start_idx1_ += block_size_; } return image_pairs_; } VocabTreePairGenerator::VocabTreePairGenerator( const VocabTreePairingOptions& options, const std::shared_ptr& cache, const std::vector& query_image_ids) : options_(options), cache_(THROW_CHECK_NOTNULL(cache)), thread_pool_(options_.num_threads), queue_(options_.num_threads) { THROW_CHECK(options.Check()); LOG(INFO) << "Generating image pairs with vocabulary tree..."; const std::vector all_image_ids = cache_->GetImageIds(); if (query_image_ids.size() > 0) { query_image_ids_ = query_image_ids; } else if (options_.match_list_path == "") { query_image_ids_ = cache_->GetImageIds(); } else { // Map image names to image identifiers. std::unordered_map image_name_to_image_id; image_name_to_image_id.reserve(all_image_ids.size()); for (const auto image_id : all_image_ids) { const auto& image = cache_->GetImage(image_id); image_name_to_image_id.emplace(image.Name(), image_id); } // Read the match list path. std::ifstream file(options_.match_list_path); THROW_CHECK_FILE_OPEN(file, options_.match_list_path); std::string line; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } if (image_name_to_image_id.count(line) == 0) { LOG(ERROR) << "Image " << line << " does not exist."; } else { query_image_ids_.push_back(image_name_to_image_id.at(line)); } } } IndexImages(all_image_ids); // Since we parallelize over the query images, there is no need to parallelize // the nearest neighbor search over the query descriptors. query_options_.num_threads = 1; query_options_.max_num_images = options_.num_images; query_options_.num_neighbors = options_.num_nearest_neighbors; query_options_.num_checks = options_.num_checks; query_options_.num_images_after_verification = options_.num_images_after_verification; } VocabTreePairGenerator::VocabTreePairGenerator( const VocabTreePairingOptions& options, const std::shared_ptr& database, const std::vector& query_image_ids) : VocabTreePairGenerator( options, std::make_shared(options.CacheSize(), THROW_CHECK_NOTNULL(database)), query_image_ids) {} void VocabTreePairGenerator::Reset() { query_idx_ = 0; result_idx_ = 0; } bool VocabTreePairGenerator::HasFinished() const { return result_idx_ >= query_image_ids_.size(); } std::vector> VocabTreePairGenerator::Next() { image_pairs_.clear(); if (HasFinished()) { return {}; } if (query_idx_ == 0) { // Initially, make all retrieval threads busy and continue with the // matching. const size_t init_num_tasks = std::min(query_image_ids_.size(), 2 * thread_pool_.NumThreads()); for (; query_idx_ < init_num_tasks; ++query_idx_) { thread_pool_.AddTask( &VocabTreePairGenerator::Query, this, query_image_ids_[query_idx_]); } } LOG(INFO) << StringPrintf( "Processing image [%d/%d]", result_idx_ + 1, query_image_ids_.size()); // Push the next image to the retrieval queue. if (query_idx_ < query_image_ids_.size()) { thread_pool_.AddTask( &VocabTreePairGenerator::Query, this, query_image_ids_[query_idx_++]); } // Pop the next results from the retrieval queue. auto retrieval = queue_.Pop(); THROW_CHECK(retrieval.IsValid()); const auto& image_id = retrieval.Data().image_id; const auto& image_scores = retrieval.Data().image_scores; // Compose the image pairs from the scores. image_pairs_.reserve(image_scores.size()); for (const auto& image_score : image_scores) { image_pairs_.emplace_back(image_id, image_score.image_id); } ++result_idx_; return image_pairs_; } void VocabTreePairGenerator::IndexImages( const std::vector& image_ids) { retrieval::VisualIndex::IndexOptions index_options; // We only assign each feature to a single visual word in the indexing phase. // During the query phase, we check for overlap in possibly multiple nearest // neighbor visual words. We could do it symmetrically but experiments showed // only marginal improvements that do not justify the memory/compute increase. index_options.num_neighbors = 1; index_options.num_checks = options_.num_checks; index_options.num_threads = options_.num_threads; for (size_t i = 0; i < image_ids.size(); ++i) { Timer timer; timer.Start(); LOG(INFO) << StringPrintf( "Indexing image [%d/%d]", i + 1, image_ids.size()); auto keypoints = *cache_->GetKeypoints(image_ids[i]); auto descriptors = *cache_->GetDescriptors(image_ids[i]); if (visual_index_ == nullptr) { visual_index_ = retrieval::VisualIndex::Read( options_.vocab_tree_path.empty() ? GetVocabTreeUriForFeatureType(descriptors.type) : options_.vocab_tree_path); } if (options_.max_num_features > 0 && descriptors.data.rows() > options_.max_num_features) { ExtractTopScaleFeatures( &keypoints, &descriptors, options_.max_num_features); } visual_index_->Add( index_options, image_ids[i], keypoints, descriptors.ToFloat()); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); } // Compute the TF-IDF weights, etc. visual_index_->Prepare(); } void VocabTreePairGenerator::Query(const image_t image_id) { auto keypoints = *cache_->GetKeypoints(image_id); auto descriptors = *cache_->GetDescriptors(image_id); if (options_.max_num_features > 0 && descriptors.data.rows() > options_.max_num_features) { ExtractTopScaleFeatures( &keypoints, &descriptors, options_.max_num_features); } Retrieval retrieval; retrieval.image_id = image_id; visual_index_->Query(query_options_, keypoints, descriptors.ToFloat(), &retrieval.image_scores); THROW_CHECK(queue_.Push(std::move(retrieval))); } SequentialPairGenerator::SequentialPairGenerator( const SequentialPairingOptions& options, const std::shared_ptr& cache) : options_(options), cache_(THROW_CHECK_NOTNULL(cache)) { THROW_CHECK(options.Check()); LOG(INFO) << "Generating sequential image pairs..."; image_ids_ = GetOrderedImageIds(); image_pairs_.reserve(options_.overlap); if (options_.loop_detection) { std::vector query_image_ids; for (size_t i = 0; i < image_ids_.size(); i += options_.loop_detection_period) { query_image_ids.push_back(image_ids_[i]); } vocab_tree_pair_generator_ = std::make_unique( options_.VocabTreeOptions(), cache_, query_image_ids); } if (options_.expand_rig_images) { const std::vector frame_ids = cache_->GetFrameIds(); frame_to_image_ids_.reserve(frame_ids.size()); image_to_frame_ids_.reserve(image_ids_.size()); for (const frame_t frame_id : frame_ids) { const Frame& frame = cache_->GetFrame(frame_id); auto& frame_image_ids = frame_to_image_ids_[frame_id]; for (const data_t& data_id : frame.ImageIds()) { frame_image_ids.push_back(data_id.id); image_to_frame_ids_[data_id.id] = frame_id; } } } } SequentialPairGenerator::SequentialPairGenerator( const SequentialPairingOptions& options, const std::shared_ptr& database) : SequentialPairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void SequentialPairGenerator::Reset() { image_idx_ = 0; if (vocab_tree_pair_generator_) { vocab_tree_pair_generator_->Reset(); } } bool SequentialPairGenerator::HasFinished() const { return image_idx_ >= image_ids_.size() && (vocab_tree_pair_generator_ ? vocab_tree_pair_generator_->HasFinished() : true); } std::vector> SequentialPairGenerator::Next() { image_pairs_.clear(); if (image_idx_ >= image_ids_.size()) { if (vocab_tree_pair_generator_) { return vocab_tree_pair_generator_->Next(); } return image_pairs_; } LOG(INFO) << StringPrintf( "Processing image [%d/%d]", image_idx_ + 1, image_ids_.size()); const auto image_id1 = image_ids_.at(image_idx_); // If image is part of a rig, then pair the other images in the same frame. if (options_.expand_rig_images) { if (const auto frame_id1_it = image_to_frame_ids_.find(image_id1); frame_id1_it != image_to_frame_ids_.end()) { for (const image_t frame_image_id2 : frame_to_image_ids_.at(frame_id1_it->second)) { if (image_id1 != frame_image_id2) { image_pairs_.emplace_back(image_id1, frame_image_id2); } } } } auto MaybeExpandRigImages = [this](image_t image_id1, image_t image_id2) { if (!options_.expand_rig_images) { return; } const auto frame_id2_it = image_to_frame_ids_.find(image_id2); if (frame_id2_it != image_to_frame_ids_.end()) { // Pair with all images in second frame. for (const image_t frame_image_id2 : frame_to_image_ids_.at(frame_id2_it->second)) { if (image_id1 != frame_image_id2 && image_id2 != frame_image_id2) { image_pairs_.emplace_back(image_id1, frame_image_id2); } } } }; for (int i = 0; i < options_.overlap; ++i) { if (options_.quadratic_overlap) { const size_t image_idx_2_quadratic = image_idx_ + (1ull << i); if (image_idx_2_quadratic < image_ids_.size()) { const image_t image_id2 = image_ids_.at(image_idx_2_quadratic); image_pairs_.emplace_back(image_id1, image_id2); MaybeExpandRigImages(image_id1, image_id2); } else { break; } } else { const size_t image_idx_2 = image_idx_ + i + 1; if (image_idx_2 < image_ids_.size()) { const image_t image_id2 = image_ids_.at(image_idx_2); image_pairs_.emplace_back(image_id1, image_id2); MaybeExpandRigImages(image_id1, image_id2); } else { break; } } } ++image_idx_; return image_pairs_; } std::vector SequentialPairGenerator::GetOrderedImageIds() const { const std::vector image_ids = cache_->GetImageIds(); std::vector ordered_images; ordered_images.reserve(image_ids.size()); for (const auto image_id : image_ids) { ordered_images.push_back(cache_->GetImage(image_id)); } std::sort(ordered_images.begin(), ordered_images.end(), [](const Image& image1, const Image& image2) { return image1.Name() < image2.Name(); }); std::vector ordered_image_ids; ordered_image_ids.reserve(image_ids.size()); for (const auto& image : ordered_images) { ordered_image_ids.push_back(image.ImageId()); } return ordered_image_ids; } SpatialPairGenerator::SpatialPairGenerator( const SpatialPairingOptions& options, const std::shared_ptr& cache) : options_(options), image_ids_(THROW_CHECK_NOTNULL(cache)->GetImageIds()) { LOG(INFO) << "Generating spatial image pairs..."; THROW_CHECK(options.Check()); Timer timer; timer.Start(); LOG(INFO) << "Indexing images..."; Eigen::RowMajorMatrixXf position_matrix = ReadPositionPriorData(*cache); const int num_positions = position_idxs_.size(); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); if (num_positions == 0) { LOG(INFO) << "=> No images with location data."; return; } if (num_positions <= options_.min_num_neighbors) { LOG(WARNING) << StringPrintf( "min_num_neighbors (%d) exceeds number of images with location data " "(%zu), this may limit the number of matched pairs.", options_.min_num_neighbors, num_positions); } timer.Restart(); LOG(INFO) << "Building search index..."; faiss::IndexFlatL2 search_index(/*d=*/3); search_index.add(position_matrix.rows(), position_matrix.data()); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); timer.Restart(); LOG(INFO) << "Searching for nearest neighbors..."; knn_ = std::min(options_.max_num_neighbors + 1, num_positions); image_pairs_.reserve(knn_); index_matrix_.resize(num_positions, knn_); distance_squared_matrix_.resize(num_positions, knn_); omp_set_num_threads(GetEffectiveNumThreads(options_.num_threads)); search_index.search(position_matrix.rows(), position_matrix.data(), knn_, distance_squared_matrix_.data(), index_matrix_.data()); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); } SpatialPairGenerator::SpatialPairGenerator( const SpatialPairingOptions& options, const std::shared_ptr& database) : SpatialPairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void SpatialPairGenerator::Reset() { current_idx_ = 0; } bool SpatialPairGenerator::HasFinished() const { return current_idx_ >= position_idxs_.size(); } std::vector> SpatialPairGenerator::Next() { image_pairs_.clear(); if (HasFinished()) { return image_pairs_; } LOG(INFO) << StringPrintf( "Processing image [%d/%d]", current_idx_ + 1, position_idxs_.size()); const float max_distance_squared = static_cast(options_.max_distance * options_.max_distance); for (int j = 0; j < knn_; ++j) { // Check if query equals result. if (index_matrix_(current_idx_, j) == static_cast(current_idx_)) { continue; } // Since the nearest neighbors are sorted by distance, we can break // once the distance is too large and enough neighbors are collected. if (distance_squared_matrix_(current_idx_, j) > max_distance_squared && j > options_.min_num_neighbors) { break; } const image_t image_id = image_ids_.at(position_idxs_[current_idx_]); const size_t nn_idx = position_idxs_.at(index_matrix_(current_idx_, j)); const image_t nn_image_id = image_ids_.at(nn_idx); image_pairs_.emplace_back(image_id, nn_image_id); } ++current_idx_; return image_pairs_; } Eigen::RowMajorMatrixXf SpatialPairGenerator::ReadPositionPriorData( FeatureMatcherCache& cache) { GPSTransform gps_transform; std::vector ells(1); Eigen::RowMajorMatrixXd position_matrix(image_ids_.size(), 3); position_idxs_.clear(); position_idxs_.reserve(image_ids_.size()); for (size_t i = 0; i < image_ids_.size(); ++i) { const PosePrior* pose_prior = cache.FindImagePosePriorOrNull(image_ids_[i]); if (pose_prior == nullptr) { continue; } if ((!options_.ignore_z && !pose_prior->HasPosition()) || (options_.ignore_z && !pose_prior->position.head<2>().allFinite())) { continue; } const size_t position_idx = position_idxs_.size(); position_idxs_.push_back(i); switch (pose_prior->coordinate_system) { case PosePrior::CoordinateSystem::WGS84: { ells[0](0) = pose_prior->position(0); ells[0](1) = pose_prior->position(1); ells[0](2) = options_.ignore_z ? 0 : pose_prior->position(2); const std::vector xyzs = gps_transform.EllipsoidToECEF(ells); position_matrix(position_idx, 0) = xyzs[0](0); position_matrix(position_idx, 1) = xyzs[0](1); position_matrix(position_idx, 2) = xyzs[0](2); } break; case PosePrior::CoordinateSystem::UNDEFINED: default: LOG(WARNING) << "Unknown coordinate system for image " << image_ids_[i] << ", assuming cartesian."; case PosePrior::CoordinateSystem::CARTESIAN: position_matrix(position_idx, 0) = pose_prior->position(0); position_matrix(position_idx, 1) = pose_prior->position(1); position_matrix(position_idx, 2) = options_.ignore_z ? 0 : pose_prior->position(2); } } // Subtract the mean coordinate before casting to float for better numerical // precision when dealing with large coordinates (e.g. GPS). For even better // precision, we could also rescale the coordinates. position_matrix.rowwise() -= position_matrix.colwise().mean(); return position_matrix.topRows(position_idxs_.size()).cast(); } TransitivePairGenerator::TransitivePairGenerator( const TransitivePairingOptions& options, const std::shared_ptr& cache) : options_(options), cache_(cache) { THROW_CHECK(options.Check()); } TransitivePairGenerator::TransitivePairGenerator( const TransitivePairingOptions& options, const std::shared_ptr& database) : TransitivePairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void TransitivePairGenerator::Reset() { current_iteration_ = 0; current_batch_idx_ = 0; image_pairs_.clear(); image_pair_ids_.clear(); } bool TransitivePairGenerator::HasFinished() const { return current_iteration_ >= options_.num_iterations && image_pairs_.empty(); } std::vector> TransitivePairGenerator::Next() { if (!image_pairs_.empty()) { current_batch_idx_++; std::vector> batch; while (!image_pairs_.empty() && static_cast(batch.size()) < options_.batch_size) { batch.push_back(image_pairs_.back()); image_pairs_.pop_back(); } LOG(INFO) << StringPrintf( "Processing batch [%d/%d]", current_batch_idx_, current_num_batches_); return batch; } if (current_iteration_ >= options_.num_iterations) { return {}; } current_batch_idx_ = 0; current_num_batches_ = 0; current_iteration_++; LOG(INFO) << StringPrintf( "Iteration [%d/%d]", current_iteration_, options_.num_iterations); std::vector> existing_pair_ids_and_num_inliers; cache_->AccessDatabase( [&existing_pair_ids_and_num_inliers](Database& database) { existing_pair_ids_and_num_inliers = database.ReadTwoViewGeometryNumInliers(); }); std::map> adjacency; for (const auto& [pair_id, _] : existing_pair_ids_and_num_inliers) { const auto [image_id1, image_id2] = PairIdToImagePair(pair_id); adjacency[image_id1].push_back(image_id2); adjacency[image_id2].push_back(image_id1); image_pair_ids_.insert(pair_id); } for (const auto& image : adjacency) { const auto image_id1 = image.first; for (const auto& image_id2 : image.second) { const auto it = adjacency.find(image_id2); if (it == adjacency.end()) { continue; } for (const auto& image_id3 : it->second) { if (image_id1 == image_id3) { continue; } const auto image_pair_id = ImagePairToPairId(image_id1, image_id3); if (image_pair_ids_.count(image_pair_id) != 0) { continue; } image_pairs_.emplace_back(std::minmax(image_id1, image_id3)); image_pair_ids_.insert(image_pair_id); } } } current_num_batches_ = std::ceil(static_cast(image_pairs_.size()) / options_.batch_size); return Next(); } ImportedPairGenerator::ImportedPairGenerator( const ImportedPairingOptions& options, const std::shared_ptr& cache) : options_(options) { THROW_CHECK(options.Check()); LOG(INFO) << "Importing image pairs..."; const std::vector image_ids = cache->GetImageIds(); std::unordered_map image_name_to_image_id; image_name_to_image_id.reserve(image_ids.size()); for (const auto image_id : image_ids) { const auto& image = cache->GetImage(image_id); image_name_to_image_id.emplace(image.Name(), image_id); } image_pairs_ = ReadImagePairsText(options_.match_list_path, image_name_to_image_id); block_image_pairs_.reserve(options_.block_size); } ImportedPairGenerator::ImportedPairGenerator( const ImportedPairingOptions& options, const std::shared_ptr& database) : ImportedPairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void ImportedPairGenerator::Reset() { pair_idx_ = 0; } bool ImportedPairGenerator::HasFinished() const { return pair_idx_ >= image_pairs_.size(); } std::vector> ImportedPairGenerator::Next() { block_image_pairs_.clear(); if (HasFinished()) { return block_image_pairs_; } LOG(INFO) << StringPrintf("Processing block [%d/%d]", pair_idx_ / options_.block_size + 1, image_pairs_.size() / options_.block_size + 1); const size_t block_end = std::min(pair_idx_ + options_.block_size, image_pairs_.size()); for (size_t j = pair_idx_; j < block_end; ++j) { block_image_pairs_.push_back(image_pairs_[j]); } pair_idx_ += options_.block_size; return block_image_pairs_; } ExistingMatchedPairGenerator::ExistingMatchedPairGenerator( const ExistingMatchedPairingOptions& options, const std::shared_ptr& cache) : options_(options) { THROW_CHECK(options.Check()); LOG(INFO) << "Generating existing image pairs..."; cache->AccessDatabase([this](Database& database) { auto num_matches = database.ReadNumMatches(); image_pairs_.reserve(num_matches.size()); for (const auto& [pair_id, _] : num_matches) { image_pairs_.emplace_back(PairIdToImagePair(pair_id)); } }); num_batches_ = std::ceil(static_cast(image_pairs_.size()) / options_.batch_size); } ExistingMatchedPairGenerator::ExistingMatchedPairGenerator( const ExistingMatchedPairingOptions& options, const std::shared_ptr& database) : ExistingMatchedPairGenerator( options, std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database))) {} void ExistingMatchedPairGenerator::Reset() { start_idx_ = 0; } bool ExistingMatchedPairGenerator::HasFinished() const { return start_idx_ >= image_pairs_.size(); } std::vector> ExistingMatchedPairGenerator::Next() { if (HasFinished()) { return {}; } const size_t end_idx = std::min(start_idx_ + options_.batch_size, image_pairs_.size()); std::vector> batch; batch.reserve(end_idx - start_idx_); for (size_t idx = start_idx_; idx < end_idx; ++idx) { batch.emplace_back(image_pairs_[idx]); } LOG(INFO) << StringPrintf("Processing batch [%d/%d]", start_idx_ / options_.batch_size + 1, num_batches_); start_idx_ = end_idx; return batch; } } // namespace colmap colmap-4.0.4/src/colmap/controllers/pairing.h000066400000000000000000000336571517363634500212370ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/controllers/matcher_cache.h" #include "colmap/retrieval/visual_index.h" #include "colmap/scene/database.h" #include "colmap/util/threading.h" #include "colmap/util/types.h" #include #include namespace colmap { struct ExhaustivePairingOptions { // Block size, i.e. number of images to simultaneously load into memory. int block_size = 50; bool Check() const; // Each block matches two sets of images with size block_size. To hold all // images in the block, the cache thus needs to hold 2 * block_size. inline size_t CacheSize() const { return 2 * block_size; } }; struct VocabTreePairingOptions { // Number of images to retrieve for each query image. int num_images = 100; // Number of nearest neighbors to retrieve per query feature. int num_nearest_neighbors = 5; // Number of nearest-neighbor checks to use in retrieval. int num_checks = 64; // How many images to return after spatial verification. Set to 0 to turn off // spatial verification. int num_images_after_verification = 0; // The maximum number of features to use for indexing an image. If an // image has more features, only the largest-scale features will be indexed. int max_num_features = -1; // Path to the vocabulary tree. std::filesystem::path vocab_tree_path; // Optional path to file with specific image names to match. std::filesystem::path match_list_path = ""; // Number of threads for indexing and retrieval. int num_threads = -1; bool Check() const; inline size_t CacheSize() const { return 5 * num_images; } }; struct SequentialPairingOptions { // Number of overlapping image pairs. int overlap = 10; // Whether to match images against their quadratic neighbors. bool quadratic_overlap = true; // Whether to match an image against all images within the same rig frame // and all images in neighboring rig frames. Note that this assumes that // images are appropriate named according to the following scheme: // // rig1/ // camera1/ // image0001.jpg // image0002.jpg // image0003.jpg // ... // camera2/ // image0001.jpg // image0002.jpg // image0003.jpg // ... // camera3/ // image0001.jpg // image0002.jpg // image0003.jpg // ... // ... // // where, for overlap=1, rig1/camera1/image0001.jpg will be matched against: // // rig1/camera2/image0001.jpg # same frame // rig1/camera3/image0001.jpg # same frame // rig1/camera1/image0002.jpg # neighboring frame // rig1/camera2/image0002.jpg # neighboring frame // rig1/camera3/image0002.jpg # neighboring frame // // If no rigs/frames are configured in the database, this option is ignored. bool expand_rig_images = true; // Whether to enable vocabulary tree based loop detection. bool loop_detection = false; // The frequency at which loop detection is triggered, in number of images. int loop_detection_period = 10; // The number of images to retrieve in loop detection. This number should // be significantly larger than the sequential matching overlap. int loop_detection_num_images = 50; // Number of nearest neighbors to retrieve per query feature. int loop_detection_num_nearest_neighbors = 1; // Number of nearest-neighbor checks to use in retrieval. int loop_detection_num_checks = 64; // How many images to return after spatial verification. Set to 0 to turn off // spatial verification. int loop_detection_num_images_after_verification = 0; // The maximum number of features to use for indexing an image. If an // image has more features, only the largest-scale features will be indexed. int loop_detection_max_num_features = -1; // Number of threads for loop detection indexing and retrieval. int num_threads = -1; // Path to the vocabulary tree. std::filesystem::path vocab_tree_path; bool Check() const; VocabTreePairingOptions VocabTreeOptions() const; inline size_t CacheSize() const { return std::max(5 * loop_detection_num_images, 5 * overlap); } }; struct SpatialPairingOptions { // Whether to ignore the Z-component of the location prior. bool ignore_z = true; // The maximum number of nearest neighbors to match. int max_num_neighbors = 50; // The minimum number of nearest neighbors to match. Neighbors include those // within max_distance or to satisfy min_num_neighbors. int min_num_neighbors = 0; // The maximum distance between the query and nearest neighbor. For GPS // coordinates the unit is Euclidean distance in meters. double max_distance = 100; // Number of threads for indexing and retrieval. int num_threads = -1; bool Check() const; inline size_t CacheSize() const { return 5 * max_num_neighbors; } }; struct TransitivePairingOptions { // The maximum number of image pairs to process in one batch. int batch_size = 1000; // The number of transitive closure iterations. int num_iterations = 3; bool Check() const; inline size_t CacheSize() const { return 2 * batch_size; } }; struct ImportedPairingOptions { // Number of image pairs to match in one batch. int block_size = 1225; // Path to the file with the matches. std::filesystem::path match_list_path = ""; bool Check() const; inline size_t CacheSize() const { return block_size; } }; struct FeaturePairsMatchingOptions { // Whether to geometrically verify the given matches. bool verify_matches = true; // Path to the file with the matches. std::filesystem::path match_list_path = ""; bool Check() const; }; struct ExistingMatchedPairingOptions { // The number of image pairs to match in one batch. int batch_size = 1000; bool Check() const; inline size_t CacheSize() const { return std::max(10, static_cast(2 * std::sqrt(batch_size))); } }; class PairGenerator { public: virtual ~PairGenerator() = default; virtual void Reset() = 0; virtual bool HasFinished() const = 0; virtual std::vector> Next() = 0; std::vector> AllPairs(); }; class ExhaustivePairGenerator : public PairGenerator { public: using PairingOptions = ExhaustivePairingOptions; ExhaustivePairGenerator(const ExhaustivePairingOptions& options, const std::shared_ptr& cache); ExhaustivePairGenerator(const ExhaustivePairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: const ExhaustivePairingOptions options_; const std::vector image_ids_; const size_t block_size_; const size_t num_blocks_; size_t start_idx1_ = 0; size_t start_idx2_ = 0; std::vector> image_pairs_; }; class VocabTreePairGenerator : public PairGenerator { public: using PairingOptions = VocabTreePairingOptions; VocabTreePairGenerator(const VocabTreePairingOptions& options, const std::shared_ptr& cache, const std::vector& query_image_ids = {}); VocabTreePairGenerator(const VocabTreePairingOptions& options, const std::shared_ptr& database, const std::vector& query_image_ids = {}); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: void IndexImages(const std::vector& image_ids); struct Retrieval { image_t image_id = kInvalidImageId; std::vector image_scores; }; void Query(image_t image_id); const VocabTreePairingOptions options_; const std::shared_ptr cache_; ThreadPool thread_pool_; JobQueue queue_; std::unique_ptr visual_index_; retrieval::VisualIndex::QueryOptions query_options_; std::vector query_image_ids_; std::vector> image_pairs_; size_t query_idx_ = 0; size_t result_idx_ = 0; }; class SequentialPairGenerator : public PairGenerator { public: using PairingOptions = SequentialPairingOptions; SequentialPairGenerator(const SequentialPairingOptions& options, const std::shared_ptr& cache); SequentialPairGenerator(const SequentialPairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: std::vector GetOrderedImageIds() const; const SequentialPairingOptions options_; const std::shared_ptr cache_; std::vector image_ids_; // Optional mapping from frames to images and vice versa. std::unordered_map> frame_to_image_ids_; std::unordered_map image_to_frame_ids_; std::unique_ptr vocab_tree_pair_generator_; std::vector> image_pairs_; size_t image_idx_ = 0; }; class SpatialPairGenerator : public PairGenerator { public: using PairingOptions = SpatialPairingOptions; SpatialPairGenerator(const SpatialPairingOptions& options, const std::shared_ptr& cache); SpatialPairGenerator(const SpatialPairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; Eigen::RowMajorMatrixXf ReadPositionPriorData(FeatureMatcherCache& cache); private: const SpatialPairingOptions options_; std::vector> image_pairs_; Eigen::Matrix index_matrix_; Eigen::RowMajorMatrixXf distance_squared_matrix_; std::vector image_ids_; std::vector position_idxs_; size_t current_idx_ = 0; int knn_ = 0; }; class TransitivePairGenerator : public PairGenerator { public: using PairingOptions = TransitivePairingOptions; TransitivePairGenerator(const TransitivePairingOptions& options, const std::shared_ptr& cache); TransitivePairGenerator(const TransitivePairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: const TransitivePairingOptions options_; const std::shared_ptr cache_; int current_iteration_ = 0; int current_batch_idx_ = 0; int current_num_batches_ = 0; std::vector> image_pairs_; std::unordered_set image_pair_ids_; }; class ImportedPairGenerator : public PairGenerator { public: using PairingOptions = ImportedPairingOptions; ImportedPairGenerator(const ImportedPairingOptions& options, const std::shared_ptr& cache); ImportedPairGenerator(const ImportedPairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: const ImportedPairingOptions options_; std::vector> image_pairs_; std::vector> block_image_pairs_; size_t pair_idx_ = 0; }; class ExistingMatchedPairGenerator : public PairGenerator { public: using PairingOptions = ExistingMatchedPairingOptions; ExistingMatchedPairGenerator( const ExistingMatchedPairingOptions& options, const std::shared_ptr& cache); ExistingMatchedPairGenerator(const ExistingMatchedPairingOptions& options, const std::shared_ptr& database); void Reset() override; bool HasFinished() const override; std::vector> Next() override; private: const ExistingMatchedPairingOptions options_; std::vector> image_pairs_; size_t start_idx_ = 0; size_t num_batches_ = 0; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/pairing_test.cc000066400000000000000000000770111517363634500224240ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/pairing.h" #include "colmap/feature/types.h" #include "colmap/retrieval/visual_index.h" #include "colmap/scene/database_sqlite.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include #include #include namespace colmap { namespace { void CreateSyntheticDatabase(int num_images, Database& database) { Reconstruction unused_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = num_images; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; SynthesizeDataset( synthetic_dataset_options, &unused_reconstruction, &database); } TEST(ExhaustivePairGenerator, Nominal) { constexpr int kNumImages = 34; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); ExhaustivePairingOptions options; options.block_size = 10; ExhaustivePairGenerator generator(options, database); const int num_expected_blocks = std::ceil(static_cast(kNumImages) / options.block_size) * std::ceil(static_cast(kNumImages) / options.block_size); std::set> pairs; for (int i = 0; i < num_expected_blocks; ++i) { for (const auto& pair : generator.Next()) { pairs.insert(pair); } } EXPECT_EQ(pairs.size(), kNumImages * (kNumImages - 1) / 2); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } std::unique_ptr CreateSyntheticVisualIndex() { auto visual_index = retrieval::VisualIndex::Create(); retrieval::VisualIndex::BuildOptions build_options; build_options.num_visual_words = 5; // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) visual_index->Build( build_options, FeatureDescriptorsFloat(FeatureExtractorType::SIFT, FeatureDescriptorsFloatData::Random(50, 128))); return visual_index; } TEST(VocabTreePairGenerator, Nominal) { constexpr int kNumImages = 5; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); VocabTreePairingOptions options; options.vocab_tree_path = CreateTestDir() / "vocab_tree.txt"; // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) CreateSyntheticVisualIndex()->Write(options.vocab_tree_path); { options.num_images = 3; VocabTreePairGenerator generator(options, database); for (int i = 0; i < kNumImages; ++i) { const auto pairs = generator.Next(); EXPECT_EQ(pairs.size(), options.num_images); EXPECT_EQ( (std::set>(pairs.begin(), pairs.end()) .size()), pairs.size()); } EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } { options.num_images = 100; VocabTreePairGenerator generator(options, database); for (int i = 0; i < kNumImages; ++i) { const auto pairs = generator.Next(); EXPECT_EQ(pairs.size(), kNumImages); } EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } } TEST(SequentialPairGenerator, Linear) { constexpr int kNumImages = 5; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); SequentialPairingOptions options; options.overlap = 3; options.quadratic_overlap = false; SequentialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()), std::make_pair(images[0].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[2].ImageId()), std::make_pair(images[1].ImageId(), images[3].ImageId()), std::make_pair(images[1].ImageId(), images[4].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[3].ImageId()), std::make_pair(images[2].ImageId(), images[4].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[4].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } TEST(SequentialPairGenerator, LinearRig) { auto database = Database::Open(kInMemorySqliteDatabasePath); Reconstruction unused_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 3; SynthesizeDataset( synthetic_dataset_options, &unused_reconstruction, database.get()); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), synthetic_dataset_options.num_cameras_per_rig * synthetic_dataset_options.num_frames_per_rig); SequentialPairingOptions options; options.overlap = 1; options.quadratic_overlap = false; SequentialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()), std::make_pair(images[0].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[3].ImageId()), std::make_pair(images[2].ImageId(), images[4].ImageId()), std::make_pair(images[2].ImageId(), images[5].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[4].ImageId(), images[5].ImageId()), std::make_pair(images[4].ImageId(), images[1].ImageId()), std::make_pair(images[4].ImageId(), images[0].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()), std::make_pair(images[1].ImageId(), images[3].ImageId()), std::make_pair(images[1].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[2].ImageId()), std::make_pair(images[3].ImageId(), images[5].ImageId()), std::make_pair(images[3].ImageId(), images[4].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[5].ImageId(), images[4].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } TEST(SequentialPairGenerator, Quadratic) { constexpr int kNumImages = 5; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); SequentialPairingOptions options; options.overlap = 3; options.quadratic_overlap = true; SequentialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()), std::make_pair(images[0].ImageId(), images[4].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[2].ImageId()), std::make_pair(images[1].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[3].ImageId()), std::make_pair(images[2].ImageId(), images[4].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[4].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } TEST(SpatialPairGenerator, Nominal) { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(1, 2, 3); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; { SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } { options.ignore_z = true; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } { options.ignore_z = false; options.max_distance = 5; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } { options.max_num_neighbors = 2; options.max_distance = 1000; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()), std::make_pair(images[1].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()), std::make_pair(images[2].ImageId(), images[0].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } } TEST(SpatialPairGenerator, LargeCoordinates) { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(1, 2, 3) + Eigen::Vector3d::Constant(1e16); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4) + Eigen::Vector3d::Constant(1e16); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12) + Eigen::Vector3d::Constant(1e16); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } TEST(SpatialPairGenerator, MinNumNeighborsControlsMatchingDistance) { constexpr int kNumImages = 4; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const auto images = database->ReadAllImages(); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(1, 1, 2); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(1, 2, 3); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior3); PosePrior pose_prior4; pose_prior4.corr_data_id = images[3].DataId(); pose_prior4.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior4); SpatialPairingOptions options; options.ignore_z = false; options.max_num_neighbors = kNumImages; options.max_distance = 0.0; { options.min_num_neighbors = 0; EXPECT_FALSE(options.Check()); } { options.min_num_neighbors = 1; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[2].ImageId()))); EXPECT_TRUE(generator.Next().empty()); } { options.min_num_neighbors = 2; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()), std::make_pair(images[1].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()), std::make_pair(images[2].ImageId(), images[0].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[2].ImageId()), std::make_pair(images[3].ImageId(), images[1].ImageId()))); EXPECT_TRUE(generator.Next().empty()); } { options.min_num_neighbors = 3; SpatialPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()), std::make_pair(images[0].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[1].ImageId(), images[0].ImageId()), std::make_pair(images[1].ImageId(), images[2].ImageId()), std::make_pair(images[1].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[1].ImageId()), std::make_pair(images[2].ImageId(), images[0].ImageId()), std::make_pair(images[2].ImageId(), images[3].ImageId()))); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[3].ImageId(), images[2].ImageId()), std::make_pair(images[3].ImageId(), images[1].ImageId()), std::make_pair(images[3].ImageId(), images[0].ImageId()))); EXPECT_TRUE(generator.Next().empty()); } } TEST(SpatialPairGenerator, ReadPositionPriorData) { { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(1, 2, 3); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 3); } { // Test that the position prior data is read correctly when some images // don't have a pose prior. constexpr int kNumImages = 4; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); database->ClearPosePriors(); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(1, 2, 3); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior4; pose_prior4.corr_data_id = images[3].DataId(); pose_prior4.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior4); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 3); } { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(0, 0, std::numeric_limits::quiet_NaN()); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 2); } { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(0, 0, std::numeric_limits::quiet_NaN()); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = true; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 3); } { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = false; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 2); } { constexpr int kNumImages = 3; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); PosePrior pose_prior1; pose_prior1.corr_data_id = images[0].DataId(); pose_prior1.position = Eigen::Vector3d(std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); database->WritePosePrior(pose_prior1); PosePrior pose_prior2; pose_prior2.corr_data_id = images[1].DataId(); pose_prior2.position = Eigen::Vector3d(2, 3, 4); database->WritePosePrior(pose_prior2); PosePrior pose_prior3; pose_prior3.corr_data_id = images[2].DataId(); pose_prior3.position = Eigen::Vector3d(2, 4, 12); database->WritePosePrior(pose_prior3); SpatialPairingOptions options; options.max_num_neighbors = 1; options.max_distance = 1000; options.ignore_z = true; auto cache = std::make_shared( options.CacheSize(), THROW_CHECK_NOTNULL(database)); SpatialPairGenerator generator(options, cache); Eigen::RowMajorMatrixXf position_matrix = generator.ReadPositionPriorData(*cache); EXPECT_EQ(position_matrix.rows(), 2); } } TEST(TransitivePairGenerator, Nominal) { constexpr int kNumImages = 5; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); TwoViewGeometry two_view_geometry; two_view_geometry.inlier_matches.resize(10); database->ClearTwoViewGeometries(); database->WriteTwoViewGeometry( images[0].ImageId(), images[1].ImageId(), two_view_geometry); database->WriteTwoViewGeometry( images[0].ImageId(), images[2].ImageId(), two_view_geometry); database->WriteTwoViewGeometry( images[1].ImageId(), images[3].ImageId(), two_view_geometry); TransitivePairingOptions options; TransitivePairGenerator generator(options, database); const auto pairs1 = generator.Next(); EXPECT_THAT(pairs1, testing::UnorderedElementsAre( std::make_pair(images[1].ImageId(), images[2].ImageId()), std::make_pair(images[0].ImageId(), images[3].ImageId()))); for (const auto& pair : pairs1) { database->WriteTwoViewGeometry(pair.first, pair.second, two_view_geometry); } EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[3].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } TEST(ImportedPairGenerator, Nominal) { constexpr int kNumImages = 10; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); ImportedPairingOptions options; options.match_list_path = CreateTestDir() / "pairs.txt"; { std::ofstream match_list_file(options.match_list_path); match_list_file.close(); ImportedPairGenerator generator(options, database); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } { std::ofstream match_list_file(options.match_list_path); match_list_file << images[2].Name() << " " << images[4].Name() << '\n'; match_list_file << images[1].Name() << " " << images[3].Name() << '\n'; match_list_file << images[2].Name() << " " << images[9].Name() << '\n'; match_list_file.close(); ImportedPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::ElementsAre( std::make_pair(images[2].ImageId(), images[4].ImageId()), std::make_pair(images[1].ImageId(), images[3].ImageId()), std::make_pair(images[2].ImageId(), images[9].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } } TEST(ExistingMatchedPairGenerator, Nominal) { constexpr int kNumImages = 5; auto database = Database::Open(kInMemorySqliteDatabasePath); CreateSyntheticDatabase(kNumImages, *database); const std::vector images = database->ReadAllImages(); CHECK_EQ(images.size(), kNumImages); database->ClearMatches(); database->WriteMatches( images[0].ImageId(), images[1].ImageId(), FeatureMatches(1)); database->WriteMatches( images[0].ImageId(), images[2].ImageId(), FeatureMatches(2)); database->WriteMatches( images[1].ImageId(), images[3].ImageId(), FeatureMatches(3)); database->WriteMatches( images[2].ImageId(), images[3].ImageId(), FeatureMatches(0)); ExistingMatchedPairingOptions options; options.batch_size = 2; ExistingMatchedPairGenerator generator(options, database); EXPECT_THAT(generator.Next(), testing::UnorderedElementsAre( std::make_pair(images[0].ImageId(), images[1].ImageId()), std::make_pair(images[0].ImageId(), images[2].ImageId()))); EXPECT_THAT(generator.Next(), testing::UnorderedElementsAre( std::make_pair(images[1].ImageId(), images[3].ImageId()))); EXPECT_TRUE(generator.Next().empty()); EXPECT_TRUE(generator.HasFinished()); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/reconstruction_clustering.cc000066400000000000000000000135411517363634500252520ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/reconstruction_clustering.h" #include "colmap/scene/reconstruction_clustering.h" #include "colmap/util/file.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" namespace colmap { namespace { // Extract a subset of the reconstruction for a specific cluster. // Returns a new Reconstruction containing only frames/images/points from the // specified cluster. std::shared_ptr SubReconstructionByClusterId( const Reconstruction& reconstruction, const std::unordered_map& cluster_ids, int cluster_id) { // Helper to get cluster id for a frame auto get_cluster_id = [&cluster_ids](frame_t frame_id) -> int { auto it = cluster_ids.find(frame_id); return it != cluster_ids.end() ? it->second : -1; }; // Make a copy of the reconstruction auto filtered = std::make_shared(reconstruction); // Collect frames to deregister (those not in this cluster) std::vector frames_to_deregister; for (const auto& [frame_id, frame] : filtered->Frames()) { if (!frame.HasPose() || get_cluster_id(frame_id) != cluster_id) { frames_to_deregister.push_back(frame_id); } } // Deregister frames not in this cluster // This also removes point observations from those frames' images for (frame_t frame_id : frames_to_deregister) { if (filtered->Frame(frame_id).HasPose()) { filtered->DeRegisterFrame(frame_id); } } filtered->UpdatePoint3DErrors(); return filtered; } } // namespace ReconstructionClustererController::ReconstructionClustererController( const ReconstructionClusteringOptions& options, std::shared_ptr reconstruction, std::shared_ptr reconstruction_manager) : options_(options), reconstruction_(std::move(reconstruction)), reconstruction_manager_(std::move(reconstruction_manager)) {} void ReconstructionClustererController::Run() { THROW_CHECK_NOTNULL(reconstruction_); THROW_CHECK_NOTNULL(reconstruction_manager_); LOG_HEADING1("Pruning weakly connected frames"); Timer timer; timer.Start(); std::unordered_map cluster_ids = ClusterReconstructionFrames(options_, *reconstruction_); LOG(INFO) << "Pruning done in " << timer.ElapsedSeconds() << " seconds"; LOG(INFO) << "Number of frames after pruning: " << reconstruction_->NumRegFrames(); // Find max cluster id int max_cluster_id = -1; for (const auto& [frame_id, cluster_id] : cluster_ids) { if (cluster_id > max_cluster_id) { max_cluster_id = cluster_id; } } // Clear any existing reconstructions reconstruction_manager_->Clear(); // If no clusters (or single cluster), add the single reconstruction // Note that cluster_id start from 0, so max_cluster_id of -1 means no // clusters if (max_cluster_id < 0) { if (reconstruction_->NumRegFrames() >= static_cast(options_.min_num_reg_frames)) { size_t idx = reconstruction_manager_->Add(); *reconstruction_manager_->Get(idx) = *reconstruction_; } else { LOG(WARNING) << "Reconstruction has only " << reconstruction_->NumRegFrames() << " registered frames, below minimum threshold of " << options_.min_num_reg_frames; } } else { // For invalid frames, clusters ids are -1 and are skipped automatically // Split by cluster and add multiple reconstructions for (int comp = 0; comp <= max_cluster_id; comp++) { std::shared_ptr cluster_reconstruction = SubReconstructionByClusterId(*reconstruction_, cluster_ids, comp); THROW_CHECK_GE( cluster_reconstruction->NumRegFrames(), static_cast( options_.min_num_reg_frames)); // Should always be true const size_t num_reg_frames = cluster_reconstruction->NumRegFrames(); size_t idx = reconstruction_manager_->Add(); reconstruction_manager_->Get(idx) = std::move(cluster_reconstruction); LOG(INFO) << "Added cluster " << comp << " with " << num_reg_frames << " registered frames"; } LOG(INFO) << "Created " << reconstruction_manager_->Size() << " cluster reconstructions"; } } } // namespace colmap colmap-4.0.4/src/colmap/controllers/reconstruction_clustering.h000066400000000000000000000052721517363634500251160ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/scene/reconstruction.h" #include "colmap/scene/reconstruction_clustering.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/util/base_controller.h" #include namespace colmap { // Controller that clusters frames from a reconstruction // and splits it into multiple reconstructions based on clustering. // Note: this module is experimental and should be verified carefully // before use in production pipelines. class ReconstructionClustererController : public BaseController { public: ReconstructionClustererController( const ReconstructionClusteringOptions& options, std::shared_ptr reconstruction, std::shared_ptr reconstruction_manager); // Runs the pruning and clustering algorithm. // Results are stored in the reconstruction manager passed to the constructor. void Run() override; private: const ReconstructionClusteringOptions options_; std::shared_ptr reconstruction_; std::shared_ptr reconstruction_manager_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/reconstruction_clustering_test.cc000066400000000000000000000270251517363634500263130ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/reconstruction_clustering.h" #include "colmap/math/random.h" #include "colmap/scene/synthetic.h" #include namespace colmap { namespace { // Creates a reconstruction with two weakly connected clusters. // The reconstruction is synthesized with `num_frames` frames, then split into // two clusters by removing cross-cluster observations, keeping only // `num_weak_links` 3D points that connect both clusters. void CreateTwoWeaklyConnectedClusters(Reconstruction* reconstruction, int num_frames, int num_points3D, int num_weak_links) { SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = num_frames; synthetic_options.num_points3D = num_points3D; synthetic_options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; SynthesizeDataset(synthetic_options, reconstruction); // Collect all frame IDs and split them into two clusters std::vector all_frames; for (const auto& [frame_id, frame] : reconstruction->Frames()) { if (frame.HasPose()) { all_frames.push_back(frame_id); } } std::sort(all_frames.begin(), all_frames.end()); const size_t half = all_frames.size() / 2; std::unordered_set cluster1_frames(all_frames.begin(), all_frames.begin() + half); std::unordered_set cluster2_frames(all_frames.begin() + half, all_frames.end()); // For each 3D point, randomly assign it to one cluster and remove all // observations from the other cluster. Keep a few points as weak links. std::vector points_to_delete; int weak_link_count = 0; for (auto& [point3D_id, point3D] : reconstruction->Points3D()) { std::vector> cluster1_obs; std::vector> cluster2_obs; for (const auto& elem : point3D.track.Elements()) { const frame_t frame_id = reconstruction->Image(elem.image_id).FrameId(); if (cluster1_frames.count(frame_id)) { cluster1_obs.emplace_back(elem.image_id, elem.point2D_idx); } else if (cluster2_frames.count(frame_id)) { cluster2_obs.emplace_back(elem.image_id, elem.point2D_idx); } } // If the point has observations in both clusters if (!cluster1_obs.empty() && !cluster2_obs.empty()) { // Keep a few points as weak links (with observations in both clusters) if (weak_link_count < num_weak_links) { weak_link_count++; continue; } // Randomly assign this point to one cluster bool assign_to_cluster1 = (RandomUniformInteger(0, 1) == 0); const auto& obs_to_remove = assign_to_cluster1 ? cluster2_obs : cluster1_obs; for (const auto& [image_id, point2D_idx] : obs_to_remove) { reconstruction->DeleteObservation(image_id, point2D_idx); } // If the track is now too short, mark for deletion if (point3D.track.Length() < 2) { points_to_delete.push_back(point3D_id); } } } // Delete points with insufficient observations for (point3D_t point3D_id : points_to_delete) { if (reconstruction->ExistsPoint3D(point3D_id)) { reconstruction->DeletePoint3D(point3D_id); } } } TEST(ReconstructionClustererController, EmptyReconstruction) { SetPRNGSeed(1); auto reconstruction = std::make_shared(); auto reconstruction_manager = std::make_shared(); ReconstructionClusteringOptions options; ReconstructionClustererController controller( options, reconstruction, reconstruction_manager); EXPECT_NO_THROW(controller.Run()); // Empty reconstruction should result in no output reconstructions EXPECT_EQ(reconstruction_manager->Size(), 0); } TEST(ReconstructionClustererController, SingleCluster) { SetPRNGSeed(1); auto reconstruction = std::make_shared(); // Create a synthetic dataset with well-connected frames SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 5; synthetic_options.num_points3D = 200; // More points for better covisibility synthetic_options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; SynthesizeDataset(synthetic_options, reconstruction.get()); EXPECT_EQ(reconstruction->NumRegFrames(), 5); auto reconstruction_manager = std::make_shared(); // Use relaxed clustering options to ensure all frames stay connected ReconstructionClusteringOptions options; ReconstructionClustererController controller( options, reconstruction, reconstruction_manager); // Controller should run without crashing EXPECT_NO_THROW(controller.Run()); EXPECT_EQ(reconstruction_manager->Size(), 1); } TEST(ReconstructionClustererController, SingleClusterWithOutlierFrames) { SetPRNGSeed(42); auto reconstruction = std::make_shared(); // Create a well-connected reconstruction with more frames SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 10; synthetic_options.num_points3D = 500; synthetic_options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; SynthesizeDataset(synthetic_options, reconstruction.get()); const size_t total_frames = reconstruction->NumRegFrames(); EXPECT_EQ(total_frames, 10); // Select the last 3 frames as outliers and remove all their 3D point // observations, making them isolated from the main cluster constexpr int kNumOutliers = 3; std::vector all_frame_ids(reconstruction->RegFrameIds().begin(), reconstruction->RegFrameIds().end()); std::sort(all_frame_ids.begin(), all_frame_ids.end()); std::unordered_set outlier_frame_ids; for (size_t i = total_frames - kNumOutliers; i < total_frames; i++) { outlier_frame_ids.insert(all_frame_ids[i]); } // Remove all 3D point observations from outlier frames for (const frame_t outlier_frame_id : outlier_frame_ids) { const Frame& frame = reconstruction->Frame(outlier_frame_id); for (const data_t& data_id : frame.ImageIds()) { const image_t image_id = data_id.id; Image& image = reconstruction->Image(image_id); const auto num_points2D = image.NumPoints2D(); for (point2D_t point2D_idx = 0; point2D_idx < num_points2D; ++point2D_idx) { if (image.Point2D(point2D_idx).HasPoint3D()) { reconstruction->DeleteObservation(image_id, point2D_idx); } } } } const size_t main_cluster_frames = total_frames - kNumOutliers; auto reconstruction_manager = std::make_shared(); ReconstructionClusteringOptions options; options.min_num_reg_frames = 3; ReconstructionClustererController controller( options, reconstruction, reconstruction_manager); controller.Run(); // Should produce exactly one cluster containing only the main frames // The outlier frames should be filtered out (cluster_id = -1) because // they have no covisibility edges with any other frames EXPECT_EQ(reconstruction_manager->Size(), 1) << "Expected single cluster after filtering outliers"; // The single cluster should contain only the main cluster frames EXPECT_EQ(reconstruction_manager->Get(0)->NumRegFrames(), main_cluster_frames) << "Outlier frames should not be included in the output reconstruction"; } TEST(ReconstructionClustererController, MinNumRegFramesFilter) { SetPRNGSeed(1); auto reconstruction = std::make_shared(); // Create a small synthetic dataset SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 2; synthetic_options.num_points3D = 20; SynthesizeDataset(synthetic_options, reconstruction.get()); auto reconstruction_manager = std::make_shared(); // Set min_num_reg_frames higher than the number of frames ReconstructionClusteringOptions options; options.min_num_reg_frames = 5; ReconstructionClustererController controller( options, reconstruction, reconstruction_manager); controller.Run(); // Should be filtered out due to min_num_reg_frames threshold EXPECT_EQ(reconstruction_manager->Size(), 0); } TEST(ReconstructionClustererController, TwoWeaklyConnectedClusters) { SetPRNGSeed(42); auto reconstruction = std::make_shared(); // Create a reconstruction with two clusters connected by only 10 weak links constexpr int kNumFrames = 10; constexpr int kNumPoints3D = 500; constexpr int kNumWeakLinks = 10; CreateTwoWeaklyConnectedClusters( reconstruction.get(), kNumFrames, kNumPoints3D, kNumWeakLinks); EXPECT_EQ(reconstruction->NumRegFrames(), kNumFrames); auto reconstruction_manager = std::make_shared(); // Use default clustering options - the algorithm should detect the weak // connection and split into two clusters ReconstructionClusteringOptions options; options.min_num_reg_frames = 3; ReconstructionClustererController controller( options, reconstruction, reconstruction_manager); controller.Run(); // The algorithm should produce two separate reconstructions EXPECT_EQ(reconstruction_manager->Size(), 2) << "Expected two clusters from weakly connected reconstruction"; // Check that each cluster has exactly half of the frames const size_t expected_frames_per_cluster = kNumFrames / 2; for (size_t i = 0; i < reconstruction_manager->Size(); i++) { EXPECT_EQ(reconstruction_manager->Get(i)->NumRegFrames(), expected_frames_per_cluster); } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/rotation_averaging.cc000066400000000000000000000126371517363634500236210ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/rotation_averaging.h" #include "colmap/estimators/gravity_refinement.h" #include "colmap/estimators/rotation_averaging.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/geometry/pose.h" #include "colmap/scene/pose_graph.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "colmap/util/timer.h" #include namespace colmap { RotationAveragingPipeline::RotationAveragingPipeline( const RotationAveragingPipelineOptions& options, std::shared_ptr database, std::shared_ptr reconstruction) : options_(options), reconstruction_(std::move(THROW_CHECK_NOTNULL(reconstruction))) { THROW_CHECK_NOTNULL(database); LOG(INFO) << "Loading database"; DatabaseCache::Options database_cache_options; database_cache_options.min_num_matches = options_.min_num_matches; database_cache_options.ignore_watermarks = options_.ignore_watermarks; database_cache_options.image_names = {options_.image_names.begin(), options_.image_names.end()}; database_cache_ = DatabaseCache::Create(*database, database_cache_options); if (options_.decompose_relative_pose) { MaybeDecomposeRelativePoses(database_cache_.get()); } } void RotationAveragingPipeline::Run() { // Propagate options to component options. RotationAveragingPipelineOptions options = options_; options.rotation_estimation.random_seed = options.random_seed; options.gravity_refiner.solver_options.num_threads = options.num_threads; Timer run_timer; run_timer.Start(); // Load reconstruction and pose graph from database cache. reconstruction_->Load(*database_cache_); PoseGraph pose_graph; pose_graph.Load(*database_cache_->CorrespondenceGraph()); if (pose_graph.Empty()) { LOG(ERROR) << "Cannot continue without image pairs"; return; } // Get a mutable copy of pose priors. std::vector pose_priors = database_cache_->PosePriors(); // Initialize frame rotations from gravity priors. const Eigen::Vector3d kUnknownTranslation = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); for (const auto& pose_prior : pose_priors) { if (!pose_prior.HasGravity()) { continue; } const auto& image = reconstruction_->Image(pose_prior.pose_prior_id); if (!image.IsRefInFrame()) { continue; } reconstruction_->Frame(image.FrameId()) .SetRigFromWorld(Rigid3d( Eigen::Quaterniond(GravityAlignedRotation(pose_prior.gravity)), kUnknownTranslation)); } // Optionally refine gravity priors (only if gravity priors exist). if (options.refine_gravity && !pose_priors.empty()) { // Compute largest connected component and invalidate pairs before gravity // refinement. const std::unordered_set active_frame_ids = pose_graph.ComputeLargestConnectedFrameComponent( *reconstruction_, /*filter_unregistered=*/false); std::unordered_set active_image_ids; for (const auto& [image_id, image] : reconstruction_->Images()) { if (active_frame_ids.count(image.FrameId())) { active_image_ids.insert(image_id); } } pose_graph.InvalidatePairsOutsideActiveImageIds(active_image_ids); LOG_HEADING1("Running gravity refinement"); RunGravityRefinement( options.gravity_refiner, pose_graph, *reconstruction_, pose_priors); } LOG_HEADING1("Running rotation averaging"); if (!RunRotationAveraging(options.rotation_estimation, pose_graph, *reconstruction_, pose_priors)) { LOG(ERROR) << "Failed to solve rotation averaging"; return; } LOG(INFO) << "Rotation averaging done in " << run_timer.ElapsedSeconds() << "s"; } } // namespace colmap colmap-4.0.4/src/colmap/controllers/rotation_averaging.h000066400000000000000000000065611517363634500234620ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/estimators/gravity_refinement.h" #include "colmap/estimators/rotation_averaging.h" #include "colmap/scene/database_cache.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/base_controller.h" #include #include namespace colmap { struct RotationAveragingPipelineOptions { // The minimum number of matches for inlier matches to be considered. int min_num_matches = 0; // Whether to ignore the inlier matches of watermark image pairs. bool ignore_watermarks = false; // Names of images to reconstruct. If empty, all images are used. std::vector image_names; // Number of threads. int num_threads = -1; // PRNG seed for all stochastic methods during reconstruction. // If -1 (default), the seed is derived from the current time // (non-deterministic). If >= 0, the pipeline is deterministic with the given // seed. int random_seed = -1; // Whether to decompose relative poses from two-view geometries. bool decompose_relative_pose = true; // Whether to refine gravity priors before rotation averaging. bool refine_gravity = false; // Options for gravity refinement. GravityRefinerOptions gravity_refiner; // Options for rotation averaging. RotationEstimatorOptions rotation_estimation; }; class RotationAveragingPipeline : public BaseController { public: RotationAveragingPipeline(const RotationAveragingPipelineOptions& options, std::shared_ptr database, std::shared_ptr reconstruction); void Run() override; private: const RotationAveragingPipelineOptions options_; std::shared_ptr database_cache_; std::shared_ptr reconstruction_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/rotation_averaging_test.cc000066400000000000000000000163021517363634500246510ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/rotation_averaging.h" #include "colmap/math/random.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { void ExpectEqualRotations(const Reconstruction& gt, const Reconstruction& computed, const double max_rotation_error_deg) { const double max_rotation_error_rad = DegToRad(max_rotation_error_deg); const std::vector reg_image_ids = gt.RegImageIds(); for (size_t i = 0; i < reg_image_ids.size(); i++) { const image_t image_id1 = reg_image_ids[i]; for (size_t j = 0; j < i; j++) { const image_t image_id2 = reg_image_ids[j]; const Eigen::Quaterniond cam2_from_cam1 = computed.Image(image_id2).CamFromWorld().rotation() * computed.Image(image_id1).CamFromWorld().rotation().inverse(); const Eigen::Quaterniond cam2_from_cam1_gt = gt.Image(image_id2).CamFromWorld().rotation() * gt.Image(image_id1).CamFromWorld().rotation().inverse(); EXPECT_LT(cam2_from_cam1.angularDistance(cam2_from_cam1_gt), max_rotation_error_rad); } } } TEST(RotationAveragingPipeline, WithoutNoise) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); auto reconstruction = std::make_shared(); RotationAveragingPipelineOptions options; RotationAveragingPipeline controller(options, database, reconstruction); controller.Run(); ExpectEqualRotations(gt_reconstruction, *reconstruction, /*max_rotation_error_deg=*/1e-2); } TEST(RotationAveragingPipeline, WithNoiseAndOutliers) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.inlier_match_ratio = 0.6; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto reconstruction = std::make_shared(); RotationAveragingPipelineOptions options; RotationAveragingPipeline controller(options, database, reconstruction); controller.Run(); ExpectEqualRotations(gt_reconstruction, *reconstruction, /*max_rotation_error_deg=*/3); } void ExpectExactEqualRotations(const Reconstruction& reconstruction1, const Reconstruction& reconstruction2) { const std::vector reg_image_ids = reconstruction1.RegImageIds(); ASSERT_EQ(reg_image_ids.size(), reconstruction2.RegImageIds().size()); for (const image_t image_id : reg_image_ids) { EXPECT_EQ( reconstruction1.Image(image_id).CamFromWorld().rotation().coeffs(), reconstruction2.Image(image_id).CamFromWorld().rotation().coeffs()); } } TEST(RotationAveragingPipeline, WithRandomSeedStability) { SetPRNGSeed(1); const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; SynthesizeNoise(synthetic_noise_options, >_reconstruction, database.get()); auto run_controller = [&](int num_threads, int random_seed) { auto reconstruction = std::make_shared(); RotationAveragingPipelineOptions options; options.num_threads = num_threads; options.random_seed = random_seed; RotationAveragingPipeline controller(options, database, reconstruction); controller.Run(); return reconstruction; }; constexpr int kRandomSeed = 42; // Single-threaded execution. { auto reconstruction0 = run_controller(/*num_threads=*/1, /*random_seed=*/kRandomSeed); auto reconstruction1 = run_controller(/*num_threads=*/1, /*random_seed=*/kRandomSeed); ExpectExactEqualRotations(*reconstruction0, *reconstruction1); } // Multi-threaded execution. { auto reconstruction0 = run_controller(/*num_threads=*/3, /*random_seed=*/kRandomSeed); auto reconstruction1 = run_controller(/*num_threads=*/3, /*random_seed=*/kRandomSeed); // Same seed should produce similar results, up to floating-point variations // in optimization. ExpectEqualRotations(*reconstruction0, *reconstruction1, /*max_rotation_error_deg=*/1e-10); } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/controllers/undistorters.cc000066400000000000000000000703711517363634500225030ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/undistorters.h" #include "colmap/scene/reconstruction_io.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" #include #include #include namespace colmap { namespace { void MaybeSetJpegQuality(const std::filesystem::path& path, Bitmap& bitmap, int jpeg_quality) { if ((HasFileExtension(path, ".jpg") || HasFileExtension(path, ".jpeg")) && jpeg_quality > 0) { bitmap.SetMetaData("Compression", "jpeg:" + std::to_string(jpeg_quality)); } } template void WriteMatrix(const Eigen::MatrixBase& matrix, std::ofstream* file) { using index_t = typename Eigen::MatrixBase::Index; for (index_t r = 0; r < matrix.rows(); ++r) { for (index_t c = 0; c < matrix.cols() - 1; ++c) { *file << matrix(r, c) << " "; } *file << matrix(r, matrix.cols() - 1) << '\n'; } } // Write projection matrix P = K * [R t] to file and prepend given header. void WriteProjectionMatrix(const std::filesystem::path& path, const Camera& camera, const Image& image, const std::string& header) { THROW_CHECK(camera.model_id == PinholeCameraModel::model_id); std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file.imbue(std::locale::classic()); Eigen::Matrix3d calib_matrix = Eigen::Matrix3d::Identity(); calib_matrix(0, 0) = camera.FocalLengthX(); calib_matrix(1, 1) = camera.FocalLengthY(); calib_matrix(0, 2) = camera.PrincipalPointX(); calib_matrix(1, 2) = camera.PrincipalPointY(); const Eigen::Matrix3x4d img_from_world = calib_matrix * image.CamFromWorld().ToMatrix(); if (!header.empty()) { file << header << '\n'; } WriteMatrix(img_from_world, &file); } void WriteCOLMAPCommands(const bool geometric, const std::filesystem::path& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& output_prefix, const std::string& indent, std::ofstream* file) { if (geometric) { *file << indent << "$COLMAP_EXE_PATH/colmap patch_match_stereo \\\n"; *file << indent << " --workspace_path " << workspace_path << " \\\n"; *file << indent << " --workspace_format " << workspace_format << " \\\n"; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\\n"; } *file << indent << " --PatchMatchStereo.max_image_size 2000 \\\n"; *file << indent << " --PatchMatchStereo.geom_consistency true\n"; } else { *file << indent << "$COLMAP_EXE_PATH/colmap patch_match_stereo \\\n"; *file << indent << " --workspace_path " << workspace_path << " \\\n"; *file << indent << " --workspace_format " << workspace_format << " \\\n"; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\\n"; } *file << indent << " --PatchMatchStereo.max_image_size 2000 \\\n"; *file << indent << " --PatchMatchStereo.geom_consistency false\n"; } *file << indent << "$COLMAP_EXE_PATH/colmap stereo_fusion \\\n"; *file << indent << " --workspace_path " << workspace_path << " \\\n"; *file << indent << " --workspace_format " << workspace_format << " \\\n"; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\\n"; } if (geometric) { *file << indent << " --input_type geometric \\\n"; } else { *file << indent << " --input_type photometric \\\n"; } *file << indent << " --output_path " << workspace_path / (output_prefix + "fused.ply") << " \\\n"; *file << indent << "$COLMAP_EXE_PATH/colmap poisson_mesher \\\n"; *file << indent << " --input_path " << workspace_path / (output_prefix + "fused.ply") << " \\\n"; *file << indent << " --output_path " << workspace_path / (output_prefix + "meshed-poisson.ply") << " \\\n"; *file << indent << "$COLMAP_EXE_PATH/colmap delaunay_mesher \\\n"; *file << indent << " --input_path " << workspace_path / output_prefix << " \\\n"; *file << indent << " --input_type dense \\\n"; *file << indent << " --output_path " << workspace_path / (output_prefix + "meshed-delaunay.ply") << " \\\n"; } } // namespace COLMAPUndistorter::COLMAPUndistorter( Options options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path) : options_(std::move(options)), camera_options_(camera_options), reconstruction_(reconstruction), image_path_(image_path), output_path_(output_path) { THROW_CHECK_GE(options_.num_patch_match_src_images, 1); THROW_CHECK_GE(options_.jpeg_quality, -1); THROW_CHECK_LE(options_.jpeg_quality, 100); } void COLMAPUndistorter::Run() { LOG_HEADING1("Image undistortion"); Timer run_timer; run_timer.Start(); CreateDirIfNotExists(output_path_ / "images"); CreateDirIfNotExists(output_path_ / "sparse"); CreateDirIfNotExists(output_path_ / "stereo"); CreateDirIfNotExists(output_path_ / "stereo" / "depth_maps"); CreateDirIfNotExists(output_path_ / "stereo" / "normal_maps"); CreateDirIfNotExists(output_path_ / "stereo" / "consistency_graphs"); reconstruction_.CreateImageDirs(output_path_ / "images"); reconstruction_.CreateImageDirs(output_path_ / "stereo" / "depth_maps"); reconstruction_.CreateImageDirs(output_path_ / "stereo" / "normal_maps"); reconstruction_.CreateImageDirs(output_path_ / "stereo" / "consistency_graphs"); const std::vector image_ids = options_.image_ids.empty() ? reconstruction_.RegImageIds() : options_.image_ids; const size_t num_images = image_ids.size(); ThreadPool thread_pool(options_.num_threads); std::vector> futures; futures.reserve(num_images); for (const image_t image_id : image_ids) { futures.push_back( thread_pool.AddTask(&COLMAPUndistorter::Undistort, this, image_id)); } // Only use the image names for the successfully undistorted images // when writing the MVS config files std::vector image_names; image_names.reserve(num_images); for (size_t i = 0; i < futures.size(); ++i) { if (CheckIfStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); if (futures[i].get()) { image_names.push_back(reconstruction_.Image(image_ids[i]).Name()); } } LOG(INFO) << "Writing reconstruction..."; Reconstruction undistorted_reconstruction = reconstruction_; UndistortReconstruction(camera_options_, &undistorted_reconstruction); undistorted_reconstruction.Write(output_path_ / "sparse"); LOG(INFO) << "Writing configuration..."; WritePatchMatchConfig(image_names); WriteFusionConfig(image_names); LOG(INFO) << "Writing scripts..."; WriteScript(/*geometric=*/false); WriteScript(/*geometric=*/true); run_timer.PrintMinutes(); } bool COLMAPUndistorter::Undistort(const image_t image_id) const { const Image& image = reconstruction_.Image(image_id); const Camera& camera = *image.CameraPtr(); const auto input_image_path = image_path_ / image.Name(); const auto output_image_path = output_path_ / "images" / image.Name(); // Check if the image is already undistorted and copy from source if no // scaling is needed if (camera.IsUndistorted() && camera_options_.max_image_size < 0 && ExistsFile(input_image_path)) { LOG(INFO) << "Copying already distorted image to location: " << output_image_path; FileCopy(input_image_path, output_image_path, options_.copy_type); return true; } Bitmap distorted_bitmap; if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path: " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(camera_options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); MaybeSetJpegQuality( output_image_path, undistorted_bitmap, options_.jpeg_quality); return undistorted_bitmap.Write(output_image_path); } void COLMAPUndistorter::WritePatchMatchConfig( const std::vector& image_names) const { const auto path = output_path_ / "stereo" / "patch-match.cfg"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); for (const auto& image_name : image_names) { file << image_name << '\n'; file << "__auto__, " << options_.num_patch_match_src_images << '\n'; } } void COLMAPUndistorter::WriteFusionConfig( const std::vector& image_names) const { const auto path = output_path_ / "stereo" / "fusion.cfg"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); for (const auto& image_name : image_names) { file << image_name << '\n'; } } void COLMAPUndistorter::WriteScript(const bool geometric) const { const auto path = output_path_ / (geometric ? "run-colmap-geometric.sh" : "run-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# You must set $COLMAP_EXE_PATH to \n" << "# the directory containing the COLMAP executables.\n"; WriteCOLMAPCommands(geometric, ".", "COLMAP", "option-all", "", "", &file); } PMVSUndistorter::PMVSUndistorter(const Options& options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path) : options_(options), camera_options_(camera_options), reconstruction_(reconstruction), image_path_(image_path), output_path_(output_path) { THROW_CHECK_GE(options_.jpeg_quality, -1); THROW_CHECK_LE(options_.jpeg_quality, 100); } void PMVSUndistorter::Run() { LOG_HEADING1("Image undistortion (CMVS/PMVS)"); Timer run_timer; run_timer.Start(); CreateDirIfNotExists(output_path_ / "pmvs"); CreateDirIfNotExists(output_path_ / "pmvs" / "txt"); CreateDirIfNotExists(output_path_ / "pmvs" / "visualize"); CreateDirIfNotExists(output_path_ / "pmvs" / "models"); ThreadPool thread_pool(options_.num_threads); std::vector> futures; futures.reserve(reconstruction_.NumRegImages()); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { futures.push_back( thread_pool.AddTask(&PMVSUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (CheckIfStopped()) { thread_pool.Stop(); LOG(WARNING) << "Stopped the undistortion process. Image point " "locations and camera parameters for not yet processed " "images in the Bundler output file is probably wrong."; break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } LOG(INFO) << "Writing bundle file..."; Reconstruction undistorted_reconstruction = reconstruction_; UndistortReconstruction(camera_options_, &undistorted_reconstruction); const auto bundle_path = output_path_ / "pmvs" / "bundle.rd.out"; ExportBundler(undistorted_reconstruction, bundle_path, AddFileExtension(bundle_path, ".list.txt")); LOG(INFO) << "Writing visibility file..."; WriteVisibilityData(); LOG(INFO) << "Writing option file..."; WriteOptionFile(); LOG(INFO) << "Writing scripts..."; WritePMVSScript(); WriteCMVSPMVSScript(); WriteCOLMAPScript(false); WriteCOLMAPScript(true); WriteCMVSCOLMAPScript(false); WriteCMVSCOLMAPScript(true); run_timer.PrintMinutes(); } bool PMVSUndistorter::Undistort(const size_t reg_image_idx) const { const auto output_image_path = output_path_ / StringPrintf("pmvs/visualize/%08d.jpg", reg_image_idx); const auto proj_matrix_path = output_path_ / StringPrintf("pmvs/txt/%08d.txt", reg_image_idx); const image_t image_id = *std::next(reconstruction_.RegImageIds().begin(), reg_image_idx); const Image& image = reconstruction_.Image(image_id); const Camera& camera = *image.CameraPtr(); Bitmap distorted_bitmap; const auto input_image_path = image_path_ / image.Name(); if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(camera_options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); WriteProjectionMatrix(proj_matrix_path, undistorted_camera, image, "CONTOUR"); MaybeSetJpegQuality( output_image_path, undistorted_bitmap, options_.jpeg_quality); return undistorted_bitmap.Write(output_image_path); } void PMVSUndistorter::WriteVisibilityData() const { const auto path = output_path_ / "pmvs" / "vis.dat"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "VISDATA\n"; file << reconstruction_.NumRegImages() << '\n'; size_t image_idx = 0; for (const image_t image_id : reconstruction_.RegImageIds()) { const Image& image = reconstruction_.Image(image_id); std::unordered_set visible_image_ids; for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const Point2D& point2D = image.Point2D(point2D_idx); if (point2D.HasPoint3D()) { const Point3D& point3D = reconstruction_.Point3D(point2D.point3D_id); for (const TrackElement& track_el : point3D.track.Elements()) { if (track_el.image_id != image_id) { visible_image_ids.insert(track_el.image_id); } } } } std::vector sorted_visible_image_ids(visible_image_ids.begin(), visible_image_ids.end()); std::sort(sorted_visible_image_ids.begin(), sorted_visible_image_ids.end()); file << image_idx++ << " " << visible_image_ids.size(); for (const image_t visible_image_id : sorted_visible_image_ids) { file << " " << visible_image_id; } file << '\n'; } } void PMVSUndistorter::WritePMVSScript() const { const auto path = output_path_ / "run-pmvs.sh"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# You must set $PMVS_EXE_PATH to \n" << "# the directory containing the CMVS-PMVS executables.\n"; file << "$PMVS_EXE_PATH/pmvs2 pmvs/ option-all\n"; } void PMVSUndistorter::WriteCMVSPMVSScript() const { const auto path = output_path_ / "run-cmvs-pmvs.sh"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# You must set $PMVS_EXE_PATH to \n" << "# the directory containing the CMVS-PMVS executables.\n"; file << "$PMVS_EXE_PATH/cmvs pmvs/\n"; file << "$PMVS_EXE_PATH/genOption pmvs/\n"; file << "find pmvs/ -iname \"option-*\" | sort | while read file_name\n"; file << "do\n"; file << " option_name=$(basename \"$file_name\")\n"; file << " if [ \"$option_name\" = \"option-all\" ]; then\n"; file << " continue\n"; file << " fi\n"; file << " $PMVS_EXE_PATH/pmvs2 pmvs/ $option_name\n"; file << "done\n"; } void PMVSUndistorter::WriteCOLMAPScript(const bool geometric) const { const auto path = output_path_ / (geometric ? "run-colmap-geometric.sh" : "run-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# You must set $COLMAP_EXE_PATH to \n" << "# the directory containing the COLMAP executables.\n"; WriteCOLMAPCommands( geometric, "pmvs", "PMVS", "option-all", "option-all-", "", &file); } void PMVSUndistorter::WriteCMVSCOLMAPScript(const bool geometric) const { const auto path = output_path_ / (geometric ? "run-cmvs-colmap-geometric.sh" : "run-cmvs-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# You must set $PMVS_EXE_PATH to \n" << "# the directory containing the CMVS-PMVS executables\n"; file << "# and you must set $COLMAP_EXE_PATH to \n" << "# the directory containing the COLMAP executables.\n"; file << "$PMVS_EXE_PATH/cmvs pmvs/\n"; file << "$PMVS_EXE_PATH/genOption pmvs/\n"; file << "find pmvs/ -iname \"option-*\" | sort | while read file_name\n"; file << "do\n"; file << " workspace_path=$(dirname \"$file_name\")\n"; file << " option_name=$(basename \"$file_name\")\n"; file << " if [ \"$option_name\" = \"option-all\" ]; then\n"; file << " continue\n"; file << " fi\n"; file << " rm -rf \"$workspace_path/stereo\"\n"; WriteCOLMAPCommands(geometric, "pmvs", "PMVS", "$option_name", "$option_name-", " ", &file); file << "done\n"; } void PMVSUndistorter::WriteOptionFile() const { const auto path = output_path_ / "pmvs" / "option-all"; std::ofstream file(path, std::ios::trunc); THROW_CHECK_FILE_OPEN(file, path); file << "# Generated by COLMAP - all images, no clustering.\n"; file << "level 1\n"; file << "csize 2\n"; file << "threshold 0.7\n"; file << "wsize 7\n"; file << "minImageNum 3\n"; file << "CPU " << std::thread::hardware_concurrency() << '\n'; file << "setEdge 0\n"; file << "useBound 0\n"; file << "useVisData 1\n"; file << "sequence -1\n"; file << "maxAngle 10\n"; file << "quad 2.0\n"; file << "timages " << reconstruction_.NumRegImages(); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { file << " " << i; } file << '\n'; file << "oimages 0\n"; } CMPMVSUndistorter::CMPMVSUndistorter( const Options& options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path) : options_(options), camera_options_(camera_options), image_path_(image_path), output_path_(output_path), reconstruction_(reconstruction) { THROW_CHECK_GE(options_.jpeg_quality, -1); THROW_CHECK_LE(options_.jpeg_quality, 100); } void CMPMVSUndistorter::Run() { LOG_HEADING1("Image undistortion (CMP-MVS)"); Timer run_timer; run_timer.Start(); ThreadPool thread_pool(options_.num_threads); std::vector> futures; futures.reserve(reconstruction_.NumRegImages()); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { futures.push_back( thread_pool.AddTask(&CMPMVSUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (CheckIfStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } run_timer.PrintMinutes(); } bool CMPMVSUndistorter::Undistort(const size_t reg_image_idx) const { const auto output_image_path = output_path_ / StringPrintf("%05d.jpg", reg_image_idx + 1); const auto proj_matrix_path = output_path_ / StringPrintf("%05d_P.txt", reg_image_idx + 1); const image_t image_id = *std::next(reconstruction_.RegImageIds().begin(), reg_image_idx); const Image& image = reconstruction_.Image(image_id); const Camera& camera = *image.CameraPtr(); Bitmap distorted_bitmap; const auto input_image_path = image_path_ / image.Name(); if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(camera_options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); WriteProjectionMatrix(proj_matrix_path, undistorted_camera, image, "CONTOUR"); MaybeSetJpegQuality( output_image_path, undistorted_bitmap, options_.jpeg_quality); return undistorted_bitmap.Write(output_image_path); } StandaloneImageUndistorter::StandaloneImageUndistorter( Options options, const UndistortCameraOptions& camera_options, const std::filesystem::path& image_path, const std::filesystem::path& output_path) : options_(std::move(options)), camera_options_(camera_options), image_path_(image_path), output_path_(output_path) { THROW_CHECK_GE(options_.jpeg_quality, -1); THROW_CHECK_LE(options_.jpeg_quality, 100); } void StandaloneImageUndistorter::Run() { LOG_HEADING1("Image undistortion"); Timer run_timer; run_timer.Start(); CreateDirIfNotExists(output_path_); ThreadPool thread_pool(options_.num_threads); std::vector> futures; const size_t num_images = options_.image_names_and_cameras.size(); futures.reserve(num_images); for (size_t i = 0; i < num_images; ++i) { futures.push_back( thread_pool.AddTask(&StandaloneImageUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (CheckIfStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } run_timer.PrintMinutes(); } bool StandaloneImageUndistorter::Undistort(const size_t image_idx) const { const auto& [image_name, camera] = options_.image_names_and_cameras[image_idx]; const auto output_image_path = output_path_ / image_name; const auto input_image_path = image_path_ / image_name; // Check if the image is already undistorted and copy from source if no // scaling is needed if (camera.IsUndistorted() && camera_options_.max_image_size < 0 && ExistsFile(input_image_path)) { FileCopy(input_image_path, output_image_path, options_.copy_type); return true; } Bitmap distorted_bitmap; if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(camera_options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); MaybeSetJpegQuality( output_image_path, undistorted_bitmap, options_.jpeg_quality); return undistorted_bitmap.Write(output_image_path); } StereoImageRectifier::StereoImageRectifier( Options options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path) : options_(std::move(options)), camera_options_(camera_options), reconstruction_(reconstruction), image_path_(image_path), output_path_(output_path) { THROW_CHECK_GE(options_.jpeg_quality, -1); THROW_CHECK_LE(options_.jpeg_quality, 100); } void StereoImageRectifier::Run() { LOG_HEADING1("Stereo rectification"); Timer run_timer; run_timer.Start(); ThreadPool thread_pool(options_.num_threads); std::vector> futures; futures.reserve(options_.stereo_pairs.size()); for (const auto& stereo_pair : options_.stereo_pairs) { futures.push_back(thread_pool.AddTask(&StereoImageRectifier::Rectify, this, stereo_pair.first, stereo_pair.second)); } for (size_t i = 0; i < futures.size(); ++i) { if (CheckIfStopped()) { break; } LOG(INFO) << StringPrintf( "Rectifying image pair [%d/%d]", i + 1, futures.size()); futures[i].get(); } run_timer.PrintMinutes(); } void StereoImageRectifier::Rectify(const image_t image_id1, const image_t image_id2) const { const Image& image1 = reconstruction_.Image(image_id1); const Image& image2 = reconstruction_.Image(image_id2); const Camera& camera1 = reconstruction_.Camera(image1.CameraId()); const Camera& camera2 = reconstruction_.Camera(image2.CameraId()); const std::string image_name1 = StringReplace(image1.Name(), "/", "-"); const std::string image_name2 = StringReplace(image2.Name(), "/", "-"); const std::string stereo_pair_name = StringPrintf("%s-%s", image_name1.c_str(), image_name2.c_str()); CreateDirIfNotExists(output_path_ / stereo_pair_name); const auto output_image_path1 = output_path_ / stereo_pair_name / image_name1; const auto output_image_path2 = output_path_ / stereo_pair_name / image_name2; Bitmap distorted_bitmap1; const auto input_image1_path = image_path_ / image1.Name(); if (!distorted_bitmap1.Read(input_image1_path)) { LOG(ERROR) << "Cannot read image at path " << input_image1_path; return; } Bitmap distorted_bitmap2; const auto input_image2_path = image_path_ / image2.Name(); if (!distorted_bitmap2.Read(input_image2_path)) { LOG(ERROR) << "Cannot read image at path " << input_image2_path; return; } const Rigid3d cam2_from_cam1 = image2.CamFromWorld() * Inverse(image1.CamFromWorld()); Bitmap undistorted_bitmap1; Bitmap undistorted_bitmap2; Camera undistorted_camera; Eigen::Matrix4d Q; RectifyAndUndistortStereoImages(camera_options_, distorted_bitmap1, distorted_bitmap2, camera1, camera2, cam2_from_cam1, &undistorted_bitmap1, &undistorted_bitmap2, &undistorted_camera, &Q); MaybeSetJpegQuality( output_image_path1, undistorted_bitmap1, options_.jpeg_quality); MaybeSetJpegQuality( output_image_path2, undistorted_bitmap2, options_.jpeg_quality); undistorted_bitmap1.Write(output_image_path1); undistorted_bitmap2.Write(output_image_path2); const auto Q_path = output_path_ / stereo_pair_name / "Q.txt"; std::ofstream Q_file(Q_path, std::ios::trunc); THROW_CHECK_FILE_OPEN(Q_file, Q_path); Q_file.imbue(std::locale::classic()); WriteMatrix(Q, &Q_file); } } // namespace colmap colmap-4.0.4/src/colmap/controllers/undistorters.h000066400000000000000000000203001517363634500223300ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/image/undistortion.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/base_controller.h" #include "colmap/util/file.h" namespace colmap { // Undistort images and export undistorted cameras, as required by the // mvs::PatchMatchController class. class COLMAPUndistorter : public BaseController { public: struct Options { // The copy type to use when copying already undistorted images to the // output directory. This can be used to speed up the undistortion process // when a majority of the images are already undistorted and choosing // COPY_HARD_LINK or COPY_SYMLINK to avoid duplicating the images. FileCopyType copy_type = FileCopyType::COPY; // How many images to use as patch match source images when generating the // patch match config file. int num_patch_match_src_images = 20; // List of images to undistort. If empty, all images are undistorted. std::vector image_ids; // JPEG quality setting in the range [0, 100]. A value of -1 uses the // default (quality 100). Lower values produce smaller file sizes. int jpeg_quality = -1; // Number of threads to use for undistortion. A value of -1 uses all // available CPU cores. int num_threads = -1; }; COLMAPUndistorter(Options options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path); void Run(); private: bool Undistort(image_t image_id) const; void WritePatchMatchConfig(const std::vector& image_names) const; void WriteFusionConfig(const std::vector& image_names) const; void WriteScript(bool geometric) const; const Options options_; const UndistortCameraOptions camera_options_; const Reconstruction& reconstruction_; const std::filesystem::path image_path_; const std::filesystem::path output_path_; }; // Undistort images and prepare data for CMVS/PMVS. class PMVSUndistorter : public BaseController { public: struct Options { // JPEG quality setting in the range [0, 100]. A value of -1 uses the // default (quality 100). Lower values produce smaller file sizes. int jpeg_quality = -1; // Number of threads to use for undistortion. A value of -1 uses all // available CPU cores. int num_threads = -1; }; PMVSUndistorter(const Options& options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path); void Run(); private: bool Undistort(size_t reg_image_idx) const; void WriteVisibilityData() const; void WriteOptionFile() const; void WritePMVSScript() const; void WriteCMVSPMVSScript() const; void WriteCOLMAPScript(bool geometric) const; void WriteCMVSCOLMAPScript(bool geometric) const; const Options options_; const UndistortCameraOptions camera_options_; const Reconstruction& reconstruction_; const std::filesystem::path image_path_; const std::filesystem::path output_path_; }; // Undistort images and prepare data for CMP-MVS. class CMPMVSUndistorter : public BaseController { public: struct Options { // JPEG quality setting in the range [0, 100]. A value of -1 uses the // default (quality 100). Lower values produce smaller file sizes. int jpeg_quality = -1; // Number of threads to use for undistortion. A value of -1 uses all // available CPU cores. int num_threads = -1; }; CMPMVSUndistorter(const Options& options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path); void Run(); private: bool Undistort(size_t reg_image_idx) const; const Options options_; const UndistortCameraOptions camera_options_; const std::filesystem::path image_path_; const std::filesystem::path output_path_; const Reconstruction& reconstruction_; }; // Undistort images and export undistorted cameras without the need for a // reconstruction. Instead, the image names and camera model information are // read from a text file. class StandaloneImageUndistorter : public BaseController { public: struct Options { // The images and cameras to undistort. std::vector> image_names_and_cameras; // The copy type to use when copying already undistorted images to the // output directory. FileCopyType copy_type = FileCopyType::COPY; // JPEG quality setting in the range [0, 100]. A value of -1 uses the // default (quality 100). Lower values produce smaller file sizes. int jpeg_quality = -1; // Number of threads to use for undistortion. A value of -1 uses all // available CPU cores. int num_threads = -1; }; StandaloneImageUndistorter(Options options, const UndistortCameraOptions& camera_options, const std::filesystem::path& image_path, const std::filesystem::path& output_path); void Run(); private: bool Undistort(size_t image_idx) const; const Options options_; const UndistortCameraOptions camera_options_; const std::filesystem::path image_path_; const std::filesystem::path output_path_; }; // Rectify stereo image pairs. class StereoImageRectifier : public BaseController { public: struct Options { // The stereo image pairs to rectify. std::vector> stereo_pairs; // JPEG quality setting in the range [0, 100]. A value of -1 uses the // default (quality 100). Lower values produce smaller file sizes. int jpeg_quality = -1; // Number of threads to use for undistortion. A value of -1 uses all // available CPU cores. int num_threads = -1; }; StereoImageRectifier(Options options, const UndistortCameraOptions& camera_options, const Reconstruction& reconstruction, const std::filesystem::path& image_path, const std::filesystem::path& output_path); void Run(); private: void Rectify(image_t image_id1, image_t image_id2) const; const Options options_; const UndistortCameraOptions camera_options_; const Reconstruction& reconstruction_; const std::filesystem::path image_path_; const std::filesystem::path output_path_; }; } // namespace colmap colmap-4.0.4/src/colmap/controllers/undistorters_test.cc000066400000000000000000000275141517363634500235430ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/controllers/undistorters.h" #include "colmap/scene/synthetic.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/file.h" #include "colmap/util/string.h" #include "colmap/util/testing.h" #include #include #include namespace colmap { namespace { Reconstruction CreateSyntheticReconstructionWithBitmaps( const std::filesystem::path& image_path, int num_images = 2, int image_width = 100, int image_height = 100, const std::string& image_extension = ".png") { SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = num_images; synthetic_dataset_options.camera_width = image_width; synthetic_dataset_options.camera_height = image_height; synthetic_dataset_options.image_extension = image_extension; Reconstruction reconstruction; SynthesizeDataset(synthetic_dataset_options, &reconstruction); // Create dummy images. for (const auto& [image_id, image] : reconstruction.Images()) { Bitmap bitmap(image_width, image_height, true); bitmap.Fill(BitmapColor(128, 128, 128)); bitmap.Write(image_path / image.Name()); } return reconstruction; } TEST(COLMAPUndistorter, Integration) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. const Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path); // Run COLMAP undistorter. COLMAPUndistorter undistorter(COLMAPUndistorter::Options(), UndistortCameraOptions(), reconstruction, image_path, output_path); undistorter.Run(); // Verify output directories were created. EXPECT_TRUE(ExistsDir(output_path / "images")); EXPECT_TRUE(ExistsDir(output_path / "sparse")); EXPECT_TRUE(ExistsDir(output_path / "stereo")); // Verify undistorted images were written. for (const auto& [image_id, image] : reconstruction.Images()) { EXPECT_TRUE(ExistsFile(output_path / "images" / image.Name())); } // Expect dense reconstruction files to be written. EXPECT_TRUE(ExistsFile(output_path / "stereo/patch-match.cfg")); EXPECT_TRUE(ExistsFile(output_path / "stereo/fusion.cfg")); } TEST(COLMAPUndistorter, SpecificImages) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path, /*num_images=*/2, /*image_width=*/100, /*image_height=*/100, /*image_extension=*/".jpg"); const Image& image = reconstruction.Image(reconstruction.RegImageIds()[0]); // Run COLMAP undistorter. COLMAPUndistorter::Options options; options.image_ids = {image.ImageId()}; COLMAPUndistorter undistorter(options, UndistortCameraOptions(), reconstruction, image_path, output_path); undistorter.Run(); // Verify that only the specified image was written. EXPECT_THAT( GetRecursiveFileList(output_path / "images"), testing::UnorderedElementsAre(output_path / "images" / image.Name())); } TEST(COLMAPUndistorter, JpegQuality) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path, /*num_images=*/1, /*image_width=*/100, /*image_height=*/100, /*image_extension=*/".jpg"); // Run COLMAP undistorter. COLMAPUndistorter::Options options; options.jpeg_quality = 50; COLMAPUndistorter undistorter(options, UndistortCameraOptions(), reconstruction, image_path, output_path); undistorter.Run(); // Verify undistorted images were written. for (const auto& [image_id, image] : reconstruction.Images()) { EXPECT_TRUE(ExistsFile(output_path / "images" / image.Name())); } } TEST(PMVSUndistorter, Integration) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "pmvs_output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. const Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path); // Run PMVS undistorter. PMVSUndistorter undistorter(PMVSUndistorter::Options(), UndistortCameraOptions(), reconstruction, image_path, output_path); undistorter.Run(); // Verify PMVS output structure was created (under pmvs/ subdirectory). EXPECT_TRUE(ExistsDir(output_path / "pmvs")); EXPECT_TRUE(ExistsDir(output_path / "pmvs" / "models")); EXPECT_TRUE(ExistsDir(output_path / "pmvs" / "txt")); EXPECT_TRUE(ExistsDir(output_path / "pmvs" / "visualize")); // Verify undistorted images were written with numbered names. // PMVS writes images as 00000000.jpg, 00000001.jpg, etc. const size_t num_images = reconstruction.NumRegImages(); for (size_t i = 0; i < num_images; ++i) { const std::string image_name = StringPrintf("%08zu.jpg", i); EXPECT_TRUE(ExistsFile(output_path / "pmvs" / "visualize" / image_name)); } } TEST(CMPMVSUndistorter, Integration) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "cmpmvs_output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. const Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path); // Run CMP-MVS undistorter. CMPMVSUndistorter undistorter(CMPMVSUndistorter::Options(), UndistortCameraOptions(), reconstruction, image_path, output_path); undistorter.Run(); // Verify CMP-MVS output structure was created. EXPECT_TRUE(ExistsDir(output_path)); // Verify undistorted images were written with sequential numbering. // CMP-MVS writes images as 00001.jpg, 00002.jpg, etc. const size_t num_images = reconstruction.NumRegImages(); for (size_t i = 1; i <= num_images; ++i) { const std::string image_name = StringPrintf("%05zu.jpg", i); EXPECT_TRUE(ExistsFile(output_path / image_name)); } } TEST(StandaloneImageUndistorter, Integration) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "pure_output"; CreateDirIfNotExists(image_path); // Create synthetic reconstruction with dummy images. const Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path); StandaloneImageUndistorter::Options options; for (const auto& [_, image] : reconstruction.Images()) { options.image_names_and_cameras.emplace_back(image.Name(), *image.CameraPtr()); } // Run standalone image undistorter. StandaloneImageUndistorter undistorter( options, UndistortCameraOptions(), image_path, output_path); undistorter.Run(); // Verify output directory was created. EXPECT_TRUE(ExistsDir(output_path)); // Verify undistorted images were written. for (const auto& [image_name, camera] : options.image_names_and_cameras) { EXPECT_TRUE(ExistsFile(output_path / image_name)); } } TEST(StereoImageRectifier, Integration) { const auto temp_dir = CreateTestDir(); const auto image_path = temp_dir / "input_images"; const auto output_path = temp_dir / "stereo_output"; CreateDirIfNotExists(image_path); CreateDirIfNotExists(output_path); // Create synthetic reconstruction with dummy images. const Reconstruction reconstruction = CreateSyntheticReconstructionWithBitmaps(image_path); // Create stereo pair from first two images. StereoImageRectifier::Options options; const std::vector image_ids = reconstruction.RegImageIds(); ASSERT_GE(image_ids.size(), 2); options.stereo_pairs.emplace_back(image_ids[0], image_ids[1]); // Run stereo image rectifier. StereoImageRectifier rectifier(options, UndistortCameraOptions(), reconstruction, image_path, output_path); rectifier.Run(); // Verify output directory was created. EXPECT_TRUE(ExistsDir(output_path)); // Verify rectified images were written. // StereoImageRectifier creates a subdirectory for each stereo pair. const auto& image1 = reconstruction.Image(options.stereo_pairs[0].first); const auto& image2 = reconstruction.Image(options.stereo_pairs[0].second); const std::string stereo_pair_name = StringPrintf("%s-%s", image1.Name().c_str(), image2.Name().c_str()); EXPECT_TRUE(ExistsDir(output_path / stereo_pair_name)); EXPECT_TRUE(ExistsFile(output_path / stereo_pair_name / image1.Name())); EXPECT_TRUE(ExistsFile(output_path / stereo_pair_name / image2.Name())); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/000077500000000000000000000000001517363634500172435ustar00rootroot00000000000000colmap-4.0.4/src/colmap/estimators/CMakeLists.txt000066400000000000000000000102071517363634500220030ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. set(FOLDER_NAME "estimators") add_subdirectory(cost_functions) add_subdirectory(solvers) COLMAP_ADD_LIBRARY( NAME colmap_estimators SRCS alignment.h alignment.cc bundle_adjustment.h bundle_adjustment.cc bundle_adjustment_ceres.h bundle_adjustment_ceres.cc coordinate_frame.h coordinate_frame.cc covariance.h covariance.cc generalized_pose.h generalized_pose.cc global_positioning.h global_positioning.cc gravity_refinement.h gravity_refinement.cc pose.h pose.cc rotation_averaging.h rotation_averaging.cc rotation_averaging_impl.h rotation_averaging_impl.cc triangulation.h triangulation.cc two_view_geometry.h two_view_geometry.cc view_graph_calibration.h view_graph_calibration.cc PUBLIC_LINK_LIBS colmap_util colmap_math colmap_feature_types colmap_geometry colmap_sensor colmap_image colmap_scene colmap_optim colmap_estimators_cost_functions colmap_estimators_solvers Eigen3::Eigen Ceres::ceres PoseLib::PoseLib ) if(CUDA_ENABLED) target_link_libraries(colmap_estimators PUBLIC colmap_util_cuda) endif() COLMAP_ADD_TEST( NAME alignment_test SRCS alignment_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME bundle_adjustment_test SRCS bundle_adjustment_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME bundle_adjustment_ceres_test SRCS bundle_adjustment_ceres_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME coordinate_frame_test SRCS coordinate_frame_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME covariance_test SRCS covariance_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME generalized_pose_test SRCS generalized_pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME pose_test SRCS pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME two_view_geometry_test SRCS two_view_geometry_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME view_graph_calibration_test SRCS view_graph_calibration_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME rotation_averaging_test SRCS rotation_averaging_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME gravity_refinement_test SRCS gravity_refinement_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME global_positioning_test SRCS global_positioning_test.cc LINK_LIBS colmap_estimators ) colmap-4.0.4/src/colmap/estimators/alignment.cc000066400000000000000000000554131517363634500215400ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/alignment.h" #include "colmap/estimators/solvers/similarity_transform.h" #include "colmap/geometry/pose.h" #include "colmap/math/math.h" #include "colmap/optim/loransac.h" #include "colmap/scene/projection.h" #include "colmap/util/logging.h" #include namespace colmap { namespace { struct ReconstructionAlignmentEstimator { static const int kMinNumSamples = 3; using X_t = const Image*; using Y_t = const Image*; using M_t = Sim3d; ReconstructionAlignmentEstimator(double max_reproj_error, const Reconstruction* src_reconstruction, const Reconstruction* tgt_reconstruction) : max_squared_reproj_error_(max_reproj_error * max_reproj_error), src_reconstruction_(src_reconstruction), tgt_reconstruction_(tgt_reconstruction) { THROW_CHECK_GE(max_reproj_error, 0); THROW_CHECK_NOTNULL(src_reconstruction_); THROW_CHECK_NOTNULL(tgt_reconstruction_); } // Estimate 3D similarity transform from corresponding projection centers. void Estimate(const std::vector& src_images, const std::vector& tgt_images, std::vector* models) const { THROW_CHECK_GE(src_images.size(), 3); THROW_CHECK_GE(tgt_images.size(), 3); THROW_CHECK_EQ(src_images.size(), tgt_images.size()); THROW_CHECK(models != nullptr); models->clear(); std::vector proj_centers1(src_images.size()); std::vector proj_centers2(tgt_images.size()); for (size_t i = 0; i < src_images.size(); ++i) { THROW_CHECK_EQ(src_images[i]->ImageId(), tgt_images[i]->ImageId()); proj_centers1[i] = src_images[i]->ProjectionCenter(); proj_centers2[i] = tgt_images[i]->ProjectionCenter(); } Sim3d tgt_from_src; if (!EstimateSim3d(proj_centers1, proj_centers2, tgt_from_src)) { return; } models->resize(1); (*models)[0] = tgt_from_src; } // For each image, determine the ratio of 3D points that correctly project // from one image to the other image and vice versa for the given // tgt_from_src. The residual is then defined as 1 minus this ratio, i.e., an // error threshold of 0.3 means that 70% of the points for that image must // reproject within the given maximum reprojection error threshold. void Residuals(const std::vector& src_images, const std::vector& tgt_images, const M_t& tgt_from_src, std::vector* residuals) const { THROW_CHECK_EQ(src_images.size(), tgt_images.size()); THROW_CHECK_NOTNULL(src_reconstruction_); THROW_CHECK_NOTNULL(tgt_reconstruction_); const Sim3d src_from_tgt = Inverse(tgt_from_src); residuals->resize(src_images.size()); for (size_t i = 0; i < src_images.size(); ++i) { const Image& src_image = *src_images[i]; const Image& tgt_image = *tgt_images[i]; THROW_CHECK_EQ(src_image.ImageId(), tgt_image.ImageId()); const Camera& src_camera = *src_image.CameraPtr(); const Camera& tgt_camera = *tgt_image.CameraPtr(); const Eigen::Matrix3x4d src_cam_from_world = src_image.CamFromWorld().ToMatrix(); const Eigen::Matrix3x4d tgt_cam_from_world = tgt_image.CamFromWorld().ToMatrix(); THROW_CHECK_EQ(src_image.NumPoints2D(), tgt_image.NumPoints2D()); size_t num_inliers = 0; size_t num_common_points = 0; for (point2D_t point2D_idx = 0; point2D_idx < src_image.NumPoints2D(); ++point2D_idx) { // Check if both images have a 3D point. const auto& src_point2D = src_image.Point2D(point2D_idx); if (!src_point2D.HasPoint3D()) { continue; } const auto& tgt_point2D = tgt_image.Point2D(point2D_idx); if (!tgt_point2D.HasPoint3D()) { continue; } num_common_points += 1; const Eigen::Vector3d src_point_in_tgt = tgt_from_src * src_reconstruction_->Point3D(src_point2D.point3D_id).xyz; if (CalculateSquaredReprojectionError(tgt_point2D.xy, src_point_in_tgt, tgt_cam_from_world, tgt_camera) > max_squared_reproj_error_) { continue; } const Eigen::Vector3d tgt_point_in_src = src_from_tgt * tgt_reconstruction_->Point3D(tgt_point2D.point3D_id).xyz; if (CalculateSquaredReprojectionError(src_point2D.xy, tgt_point_in_src, src_cam_from_world, src_camera) > max_squared_reproj_error_) { continue; } num_inliers += 1; } if (num_common_points == 0) { (*residuals)[i] = 1.0; } else { const double negative_inlier_ratio = 1.0 - static_cast(num_inliers) / static_cast(num_common_points); (*residuals)[i] = negative_inlier_ratio * negative_inlier_ratio; } } } private: double max_squared_reproj_error_; const Reconstruction* src_reconstruction_; const Reconstruction* tgt_reconstruction_; }; } // namespace bool AlignReconstructionToLocations( const Reconstruction& src_reconstruction, const std::vector& tgt_image_names, const std::vector& tgt_image_locations, const int min_common_images, const RANSACOptions& ransac_options, Sim3d* tgt_from_src) { THROW_CHECK_GE(min_common_images, 3); THROW_CHECK_EQ(tgt_image_names.size(), tgt_image_locations.size()); // Find out which images are contained in the reconstruction and get the // positions of their camera centers. std::unordered_set common_image_ids; std::vector src; std::vector dst; for (size_t i = 0; i < tgt_image_names.size(); ++i) { const class Image* src_image = src_reconstruction.FindImageWithName(tgt_image_names[i]); if (src_image == nullptr) { continue; } if (!src_image->HasPose()) { continue; } // Ignore duplicate images. if (!common_image_ids.insert(src_image->ImageId()).second) { continue; } src.push_back(src_image->ProjectionCenter()); dst.push_back(tgt_image_locations[i]); } // Only compute the alignment if there are enough correspondences. if (common_image_ids.size() < static_cast(min_common_images)) { return false; } Sim3d tgt_from_src_; const auto report = EstimateSim3dRobust(src, dst, ransac_options, tgt_from_src_); if (report.support.num_inliers < static_cast(min_common_images)) { return false; } if (tgt_from_src != nullptr) { *tgt_from_src = tgt_from_src_; } return true; } bool AlignReconstructionToPosePriors( const Reconstruction& src_reconstruction, const std::vector& tgt_pose_priors, const RANSACOptions& ransac_options, Sim3d* tgt_from_src) { std::vector src; std::vector tgt; src.reserve(tgt_pose_priors.size()); tgt.reserve(tgt_pose_priors.size()); std::unordered_map tgt_image_to_pose_prior; for (const auto& pose_prior : tgt_pose_priors) { if (pose_prior.corr_data_id.sensor_id.type == SensorType::CAMERA && pose_prior.HasPosition()) { THROW_CHECK(tgt_image_to_pose_prior .emplace(pose_prior.corr_data_id.id, pose_prior) .second) << "Duplicate pose prior for image " << pose_prior.corr_data_id.id; } } for (const image_t image_id : src_reconstruction.RegImageIds()) { const auto pose_prior_it = tgt_image_to_pose_prior.find(image_id); if (pose_prior_it != tgt_image_to_pose_prior.end()) { const auto& image = src_reconstruction.Image(image_id); src.push_back(image.ProjectionCenter()); tgt.push_back(pose_prior_it->second.position); } } if (src.size() < 3) { LOG(WARNING) << "Not enough valid pose priors for alignment"; return false; } if (ransac_options.max_error > 0) { return EstimateSim3dRobust(src, tgt, ransac_options, *tgt_from_src).success; } return EstimateSim3d(src, tgt, *tgt_from_src); } bool AlignReconstructionsViaReprojections( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const double min_inlier_observations, const double max_reproj_error, Sim3d* tgt_from_src) { THROW_CHECK_GE(min_inlier_observations, 0.0); THROW_CHECK_LE(min_inlier_observations, 1.0); RANSACOptions ransac_options; ransac_options.max_error = 1.0 - min_inlier_observations; ransac_options.min_inlier_ratio = 0.2; LORANSAC ransac(ransac_options, ReconstructionAlignmentEstimator( max_reproj_error, &src_reconstruction, &tgt_reconstruction), ReconstructionAlignmentEstimator( max_reproj_error, &src_reconstruction, &tgt_reconstruction)); const std::vector> common_image_ids = src_reconstruction.FindCommonRegImageIds(tgt_reconstruction); if (common_image_ids.size() < 3) { return false; } std::vector src_images(common_image_ids.size()); std::vector tgt_images(common_image_ids.size()); for (size_t i = 0; i < common_image_ids.size(); ++i) { src_images[i] = &src_reconstruction.Image(common_image_ids[i].first); tgt_images[i] = &tgt_reconstruction.Image(common_image_ids[i].second); } const auto report = ransac.Estimate(src_images, tgt_images); if (report.success) { *tgt_from_src = report.model; } return report.success; } bool AlignReconstructionsViaProjCenters( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const double max_proj_center_error, Sim3d* tgt_from_src) { THROW_CHECK_GT(max_proj_center_error, 0); std::vector ref_image_names; std::vector ref_proj_centers; for (const auto& image : tgt_reconstruction.Images()) { if (image.second.HasPose()) { ref_image_names.push_back(image.second.Name()); ref_proj_centers.push_back(image.second.ProjectionCenter()); } } Sim3d tform; RANSACOptions ransac_options; ransac_options.max_error = max_proj_center_error; return AlignReconstructionToLocations(src_reconstruction, ref_image_names, ref_proj_centers, /*min_common_images=*/3, ransac_options, tgt_from_src); } std::vector ComputeImageAlignmentError( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const Sim3d& tgt_from_src) { const std::vector> common_image_ids = src_reconstruction.FindCommonRegImageIds(tgt_reconstruction); const int num_common_images = common_image_ids.size(); std::vector errors; errors.reserve(num_common_images); for (const auto& image_ids : common_image_ids) { const auto& src_image = src_reconstruction.Image(image_ids.first); const Rigid3d tgt_world_from_src_cam = Inverse(TransformCameraWorld(tgt_from_src, src_image.CamFromWorld())); const Rigid3d tgt_world_from_tgt_cam = Inverse(tgt_reconstruction.Image(image_ids.second).CamFromWorld()); ImageAlignmentError error; error.image_name = src_image.Name(); error.rotation_error_deg = RadToDeg(tgt_world_from_src_cam.rotation().angularDistance( tgt_world_from_tgt_cam.rotation())); error.proj_center_error = (tgt_world_from_src_cam.translation() - tgt_world_from_tgt_cam.translation()) .norm(); errors.push_back(error); } return errors; } bool AlignReconstructionsViaPoints(const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const size_t min_common_observations, const double max_error, const double min_inlier_ratio, Sim3d* tgt_from_src) { THROW_CHECK_GT(min_common_observations, 0); THROW_CHECK_GT(max_error, 0.0); THROW_CHECK_GE(min_inlier_ratio, 0.0); THROW_CHECK_LE(min_inlier_ratio, 1.0); std::vector src_xyz; std::vector tgt_xyz; std::unordered_map counts; // Associate 3D points using point2D_idx for (const auto& src_point3D : src_reconstruction.Points3D()) { counts.clear(); // Count how often a 3D point in tgt is associated to this 3D point. for (const auto& track_el : src_point3D.second.track.Elements()) { const Image& tgt_image = tgt_reconstruction.Image(track_el.image_id); if (!tgt_image.HasPose()) { continue; } const Point2D& tgt_point2D = tgt_image.Point2D(track_el.point2D_idx); if (tgt_point2D.HasPoint3D()) { if (counts.find(tgt_point2D.point3D_id) != counts.end()) { counts[tgt_point2D.point3D_id]++; } else { counts[tgt_point2D.point3D_id] = 0; } } } if (counts.empty()) { continue; } // The 3D point in tgt who is associated the most is selected auto best_point3D = std::max_element(counts.begin(), counts.end(), [](const std::pair& p1, const std::pair& p2) { return p1.second < p2.second; }); if (best_point3D->second >= min_common_observations) { src_xyz.push_back(src_point3D.second.xyz); tgt_xyz.push_back(tgt_reconstruction.Point3D(best_point3D->first).xyz); } } THROW_CHECK_EQ(src_xyz.size(), tgt_xyz.size()); LOG(INFO) << "Found " << src_xyz.size() << " / " << src_reconstruction.NumPoints3D() << " valid correspondences."; RANSACOptions ransac_options; ransac_options.max_error = max_error; ransac_options.min_inlier_ratio = min_inlier_ratio; const auto report = EstimateSim3dRobust(src_xyz, tgt_xyz, ransac_options, *tgt_from_src); return report.success; } namespace { void CopyRegisteredImage(image_t image_id, const Sim3d& tgt_from_src, const Reconstruction& src_reconstruction, Reconstruction& tgt_reconstruction) { const Image& src_image = src_reconstruction.Image(image_id); if (!tgt_reconstruction.ExistsCamera(src_image.CameraId())) { tgt_reconstruction.AddCamera( src_reconstruction.Camera(src_image.CameraId())); } if (!tgt_reconstruction.ExistsRig(src_image.FramePtr()->RigId())) { tgt_reconstruction.AddRig( src_reconstruction.Rig(src_image.FramePtr()->RigId())); } if (!tgt_reconstruction.ExistsFrame(src_image.FrameId())) { Frame tgt_frame = src_reconstruction.Frame(src_image.FrameId()); tgt_frame.ResetRigPtr(); tgt_reconstruction.AddFrame(std::move(tgt_frame)); const Rigid3d cam_from_tgt_world = TransformCameraWorld(tgt_from_src, src_image.CamFromWorld()); tgt_reconstruction.Frame(src_image.FrameId()) .SetCamFromWorld(src_image.CameraId(), cam_from_tgt_world); } Image tgt_image = src_image; tgt_image.ResetCameraPtr(); tgt_image.ResetFramePtr(); tgt_reconstruction.AddImage(std::move(tgt_image)); } } // namespace bool MergeReconstructions(const double max_reproj_error, const Reconstruction& src_reconstruction, Reconstruction& tgt_reconstruction) { Sim3d tgt_from_src; if (!AlignReconstructionsViaReprojections(src_reconstruction, tgt_reconstruction, /*min_inlier_observations=*/0.3, max_reproj_error, &tgt_from_src)) { return false; } // Find common and missing images in the two reconstructions. std::unordered_set common_image_ids; common_image_ids.reserve(src_reconstruction.NumRegImages()); std::unordered_set missing_image_ids; missing_image_ids.reserve(src_reconstruction.NumRegImages()); for (const image_t image_id : src_reconstruction.RegImageIds()) { if (tgt_reconstruction.ExistsImage(image_id)) { common_image_ids.insert(image_id); } else { missing_image_ids.insert(image_id); } } // Register the missing images in this src_reconstruction. for (const auto image_id : missing_image_ids) { CopyRegisteredImage( image_id, tgt_from_src, src_reconstruction, tgt_reconstruction); } // Merge the two point clouds using the following two rules: // - copy points to this src_reconstruction with non-conflicting tracks, // i.e. points that do not have an already triangulated observation // in this src_reconstruction. // - merge tracks that are unambiguous, i.e. only merge points in the two // reconstructions if they have a one-to-one mapping. // Note that in both cases no cheirality or reprojection test is performed. for (const auto& [_, point3D] : src_reconstruction.Points3D()) { Track new_track; Track old_track; std::unordered_set old_point3D_ids; for (const auto& track_el : point3D.track.Elements()) { if (common_image_ids.count(track_el.image_id) > 0) { const auto& point2D = tgt_reconstruction.Image(track_el.image_id) .Point2D(track_el.point2D_idx); if (point2D.HasPoint3D()) { old_track.AddElement(track_el); old_point3D_ids.insert(point2D.point3D_id); } else { new_track.AddElement(track_el); } } else if (missing_image_ids.count(track_el.image_id) > 0) { tgt_reconstruction.Image(track_el.image_id) .ResetPoint3DForPoint2D(track_el.point2D_idx); new_track.AddElement(track_el); } } const bool create_new_point = new_track.Length() >= 2; const bool merge_new_and_old_point = (new_track.Length() + old_track.Length()) >= 2 && old_point3D_ids.size() == 1; if (create_new_point || merge_new_and_old_point) { const Eigen::Vector3d xyz = tgt_from_src * point3D.xyz; const auto point3D_id = tgt_reconstruction.AddPoint3D(xyz, new_track, point3D.color); if (old_point3D_ids.size() == 1) { tgt_reconstruction.MergePoints3D(point3D_id, *old_point3D_ids.begin()); } } } return true; } bool AlignReconstructionToOrigRigScales( const std::unordered_map& orig_rigs, Reconstruction* reconstruction) { double scale_sum = 0; int scale_count = 0; for (const auto& [rig_id, orig_rig] : orig_rigs) { double scale_sum_rig = 0; int scale_count_rig = 0; for (auto& [sensor_id, sensor_from_orig_rig] : orig_rig.NonRefSensors()) { if (!sensor_from_orig_rig.has_value()) { continue; } // Here we do not include rigs that are panoramic. double sensor_from_orig_rig_norm = sensor_from_orig_rig->translation().norm(); if (sensor_from_orig_rig_norm < 1e-6) { continue; } THROW_CHECK(reconstruction->Rig(rig_id).HasSensorFromRig(sensor_id)); double scale = reconstruction->Rig(rig_id) .SensorFromRig(sensor_id) .translation() .norm() / sensor_from_orig_rig_norm; scale_sum_rig += scale; ++scale_count_rig; } if (scale_count_rig > 0) { scale_sum += scale_sum_rig / scale_count_rig; ++scale_count; } } if (scale_count == 0) { return false; } Sim3d new_from_old_world; new_from_old_world.scale() = scale_count / scale_sum; reconstruction->Transform(new_from_old_world); return true; } AlignmentErrorSummary AlignmentErrorSummary::Compute( const std::vector& errors) { AlignmentErrorSummary summary; if (errors.empty()) { return summary; } std::vector rotation_errors_deg; rotation_errors_deg.reserve(errors.size()); std::vector proj_center_errors; proj_center_errors.reserve(errors.size()); for (const auto& error : errors) { rotation_errors_deg.push_back(error.rotation_error_deg); proj_center_errors.push_back(error.proj_center_error); } auto ComputeStatistics = [](std::vector& values) { Statistics stats; if (values.empty()) { return stats; } stats.min = Percentile(values, 0); stats.max = Percentile(values, 100); stats.mean = Mean(values); stats.median = Median(values); stats.p90 = Percentile(values, 90); stats.p99 = Percentile(values, 99); return stats; }; summary.rotation_errors_deg = ComputeStatistics(rotation_errors_deg); summary.proj_center_errors = ComputeStatistics(proj_center_errors); return summary; } } // namespace colmap colmap-4.0.4/src/colmap/estimators/alignment.h000066400000000000000000000126551517363634500214030ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/geometry/sim3.h" #include "colmap/optim/ransac.h" #include "colmap/scene/reconstruction.h" #include namespace colmap { // Robustly align reconstruction to given image locations (projection centers). bool AlignReconstructionToLocations( const Reconstruction& src_reconstruction, const std::vector& tgt_image_names, const std::vector& tgt_image_locations, int min_common_images, const RANSACOptions& ransac_options, Sim3d* tgt_from_src); // Robustly align reconstruction to given pose priors. bool AlignReconstructionToPosePriors( const Reconstruction& src_reconstruction, const std::vector& tgt_pose_priors, const RANSACOptions& ransac_options, Sim3d* tgt_from_src); // Robustly compute alignment between reconstructions by finding images that // are registered in both reconstructions. The alignment is then estimated // robustly inside RANSAC from corresponding projection centers. An alignment // is verified by reprojecting common 3D point observations. // The min_inlier_observations threshold determines how many observations // in a common image must reproject within the given threshold. bool AlignReconstructionsViaReprojections( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, double min_inlier_observations, double max_reproj_error, Sim3d* tgt_from_src); // Robustly compute alignment between reconstructions by finding images that // are registered in both reconstructions. The alignment is then estimated // robustly inside RANSAC from corresponding projection centers and by // minimizing the Euclidean distance between them in world space. bool AlignReconstructionsViaProjCenters( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, double max_proj_center_error, Sim3d* tgt_from_src); // Robustly compute the alignment between reconstructions that share the // same 2D points. It is estimated by minimizing the 3D distance between // corresponding 3D points. bool AlignReconstructionsViaPoints(const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, size_t min_common_observations, double max_error, double min_inlier_ratio, Sim3d* tgt_from_src); // Compute image alignment errors in the target coordinate frame. struct ImageAlignmentError { std::string image_name; double rotation_error_deg = -1; double proj_center_error = -1; }; std::vector ComputeImageAlignmentError( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const Sim3d& tgt_from_src); // Summary of alignment errors for image poses. struct AlignmentErrorSummary { struct Statistics { double min = 0; double max = 0; double mean = 0; double median = 0; double p90 = 0; double p99 = 0; }; Statistics rotation_errors_deg; Statistics proj_center_errors; static AlignmentErrorSummary Compute( const std::vector& errors); }; // Aligns the source to the target reconstruction and merges cameras, images, // points3D into the target using the alignment. Returns false on failure. bool MergeReconstructions(double max_reproj_error, const Reconstruction& src_reconstruction, Reconstruction& tgt_reconstruction); // Align reconstruction to the original metric scales in rig extrinsics. Returns // false if there is no available non-panoramic rig in the alignment process. bool AlignReconstructionToOrigRigScales( const std::unordered_map& orig_rigs, Reconstruction* reconstruction); } // namespace colmap colmap-4.0.4/src/colmap/estimators/alignment_test.cc000066400000000000000000000267211517363634500225770ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/alignment.h" #include "colmap/geometry/rigid3_matchers.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/scene/reconstruction.h" #include "colmap/scene/synthetic.h" #include namespace colmap { namespace { Sim3d TestSim3d() { return Sim3d(RandomUniformReal(0.5, 2), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); } void ExpectEqualSim3d(const Sim3d& gt_tgt_from_src, const Sim3d& tgt_from_src) { EXPECT_NEAR(gt_tgt_from_src.scale(), tgt_from_src.scale(), 1e-6); EXPECT_LT(gt_tgt_from_src.rotation().angularDistance(tgt_from_src.rotation()), 1e-6); EXPECT_LT((gt_tgt_from_src.translation() - tgt_from_src.translation()).norm(), 1e-6); } Reconstruction GenerateReconstructionForAlignment() { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset(synthetic_dataset_options, &reconstruction); return reconstruction; } TEST(Alignment, AlignReconstructionToLocations) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); std::vector tgt_image_names; std::vector tgt_image_locations; for (const auto& [_, image] : tgt_reconstruction.Images()) { tgt_image_names.push_back(image.Name()); tgt_image_locations.push_back(image.ProjectionCenter()); } RANSACOptions ransac_options; ransac_options.max_error = 1e-2; Sim3d tgt_from_src; ASSERT_FALSE(AlignReconstructionToLocations( src_reconstruction, tgt_image_names, tgt_image_locations, /*min_common_images=*/tgt_image_names.size() + 1, ransac_options, &tgt_from_src)); ASSERT_TRUE(AlignReconstructionToLocations(src_reconstruction, tgt_image_names, tgt_image_locations, /*min_common_images=*/3, ransac_options, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionToPosePriors) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); std::vector tgt_pose_priors; for (const auto& [image_id, image] : tgt_reconstruction.Images()) { PosePrior& pose_prior = tgt_pose_priors.emplace_back(); pose_prior.pose_prior_id = tgt_pose_priors.size(); pose_prior.corr_data_id = image.DataId(); pose_prior.coordinate_system = PosePrior::CoordinateSystem::CARTESIAN; pose_prior.position = image.ProjectionCenter(); pose_prior.position_covariance = 1e-2 * Eigen::Matrix3d::Identity(); } RANSACOptions ransac_options; ransac_options.max_error = 1e-2; Sim3d tgt_from_src; ASSERT_TRUE(AlignReconstructionToPosePriors( src_reconstruction, tgt_pose_priors, ransac_options, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionsViaReprojections) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; ASSERT_TRUE( AlignReconstructionsViaReprojections(src_reconstruction, tgt_reconstruction, /*min_inlier_observations=*/0.9, /*max_reproj_error=*/2, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionsViaProjCenters) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; ASSERT_TRUE(AlignReconstructionsViaProjCenters(src_reconstruction, tgt_reconstruction, /*max_proj_center_error=*/0.1, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionsViaPoints) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; ASSERT_TRUE(AlignReconstructionsViaPoints(src_reconstruction, tgt_reconstruction, /*min_common_observations=*/3, /*max_error=*/0.01, /*min_inlier_ratio=*/0.9, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, MergeReconstructions) { // Synthesize a reconstruction which has at least two cameras Reconstruction src_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 3; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset(synthetic_dataset_options, &src_reconstruction); Reconstruction orig_reconstruction = src_reconstruction; Reconstruction tgt_reconstruction = src_reconstruction; auto remove_rig_frames = [](Reconstruction& reconstruction, rig_t rig_id) { const std::vector frame_ids = reconstruction.RegFrameIds(); for (const auto& frame_id : frame_ids) { if (reconstruction.Frame(frame_id).RigId() == rig_id) { reconstruction.DeRegisterFrame(frame_id); } } }; remove_rig_frames(src_reconstruction, 1); remove_rig_frames(tgt_reconstruction, 2); // Remove all unregistered rigs/cameras/frames/images. src_reconstruction.TearDown(); tgt_reconstruction.TearDown(); EXPECT_EQ(src_reconstruction.NumRigs(), 2); EXPECT_EQ(src_reconstruction.NumCameras(), 2); EXPECT_EQ(src_reconstruction.NumFrames(), 20); EXPECT_EQ(src_reconstruction.NumRegFrames(), 20); EXPECT_EQ(src_reconstruction.NumImages(), 20); EXPECT_EQ(tgt_reconstruction.NumRigs(), 2); EXPECT_EQ(tgt_reconstruction.NumCameras(), 2); EXPECT_EQ(tgt_reconstruction.NumFrames(), 20); EXPECT_EQ(tgt_reconstruction.NumRegFrames(), 20); EXPECT_EQ(tgt_reconstruction.NumImages(), 20); // Merge reconstructions. ASSERT_TRUE(MergeReconstructions( /*max_reproj_error=*/1e-4, src_reconstruction, tgt_reconstruction)); EXPECT_EQ(tgt_reconstruction.NumRigs(), 3); EXPECT_EQ(tgt_reconstruction.NumCameras(), 3); EXPECT_EQ(tgt_reconstruction.NumFrames(), 30); EXPECT_EQ(tgt_reconstruction.NumRegFrames(), 30); EXPECT_EQ(tgt_reconstruction.NumImages(), 30); EXPECT_EQ(tgt_reconstruction.NumPoints3D(), 50); EXPECT_EQ(tgt_reconstruction.ComputeNumObservations(), orig_reconstruction.ComputeNumObservations()); } TEST(Alignment, AlignReconstructionToOrigRigScales) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 4; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 50; SynthesizeDataset(synthetic_dataset_options, &reconstruction); std::unordered_map orig_rigs = reconstruction.Rigs(); reconstruction.Transform(TestSim3d()); AlignReconstructionToOrigRigScales(orig_rigs, &reconstruction); for (const auto& [rig_id, orig_rig] : orig_rigs) { for (const auto& [sensor_id, sensor_from_orig_rig] : orig_rig.NonRefSensors()) { if (!sensor_from_orig_rig.has_value()) { continue; } EXPECT_THAT( reconstruction.Rig(rig_id).SensorFromRig(sensor_id), Rigid3dNear( sensor_from_orig_rig.value(), /*rtol=*/1e-6, /*ttol=*/1e-6)); } } } TEST(AlignmentErrorSummary, Empty) { std::vector errors; AlignmentErrorSummary summary = AlignmentErrorSummary::Compute(errors); EXPECT_EQ(summary.rotation_errors_deg.min, 0); EXPECT_EQ(summary.proj_center_errors.min, 0); } TEST(AlignmentErrorSummary, MultipleErrors) { std::vector errors(5); for (size_t i = 0; i < errors.size(); ++i) { errors[i].rotation_error_deg = static_cast(i + 1); errors[i].proj_center_error = static_cast(i + 1) * 0.1; } const AlignmentErrorSummary summary = AlignmentErrorSummary::Compute(errors); EXPECT_NEAR(summary.rotation_errors_deg.min, 1.0, 1e-10); EXPECT_NEAR(summary.rotation_errors_deg.max, 5.0, 1e-10); EXPECT_NEAR(summary.rotation_errors_deg.mean, 3.0, 1e-10); EXPECT_NEAR(summary.rotation_errors_deg.median, 3.0, 1e-10); EXPECT_NEAR(summary.rotation_errors_deg.p90, 4.6, 1e-10); EXPECT_NEAR(summary.rotation_errors_deg.p99, 4.96, 1e-10); EXPECT_NEAR(summary.proj_center_errors.min, 0.1, 1e-10); EXPECT_NEAR(summary.proj_center_errors.max, 0.5, 1e-10); EXPECT_NEAR(summary.proj_center_errors.mean, 0.3, 1e-10); EXPECT_NEAR(summary.proj_center_errors.p90, 0.46, 1e-10); EXPECT_NEAR(summary.proj_center_errors.p99, 0.496, 1e-10); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment.cc000066400000000000000000000305561517363634500232720ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/bundle_adjustment_ceres.h" namespace colmap { bool BundleAdjustmentSummary::IsSolutionUsable() const { return termination_type == BundleAdjustmentTerminationType::CONVERGENCE || termination_type == BundleAdjustmentTerminationType::NO_CONVERGENCE || termination_type == BundleAdjustmentTerminationType::USER_SUCCESS; } std::string BundleAdjustmentSummary::BriefReport() const { return "Bundle adjustment report: termination=" + std::string( BundleAdjustmentTerminationTypeToString(termination_type)) + ", num_residuals=" + std::to_string(num_residuals); } //////////////////////////////////////////////////////////////////////////////// // BundleAdjustmentConfig //////////////////////////////////////////////////////////////////////////////// void BundleAdjustmentConfig::FixGauge(BundleAdjustmentGauge gauge) { fixed_gauge_ = gauge; } BundleAdjustmentGauge BundleAdjustmentConfig::FixedGauge() const { return fixed_gauge_; } size_t BundleAdjustmentConfig::NumImages() const { return image_ids_.size(); } size_t BundleAdjustmentConfig::NumPoints() const { return variable_point3D_ids_.size() + constant_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumConstantCamIntrinsics() const { return constant_cam_intrinsics_.size(); } size_t BundleAdjustmentConfig::NumConstantSensorFromRigPoses() const { return constant_sensor_from_rig_poses_.size(); } size_t BundleAdjustmentConfig::NumConstantRigFromWorldPoses() const { return constant_rig_from_world_poses_.size(); } size_t BundleAdjustmentConfig::NumVariablePoints() const { return variable_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumConstantPoints() const { return constant_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumResiduals( const Reconstruction& reconstruction) const { // Count the number of observations for all added images. size_t num_observations = 0; for (const image_t image_id : image_ids_) { const auto& image = reconstruction.Image(image_id); for (const auto& point2D : image.Points2D()) { if (point2D.HasPoint3D() && !IsIgnoredPoint(point2D.point3D_id)) { ++num_observations; } } } // Count the number of observations for all added 3D points that are not // already added as part of the images above. auto NumObservationsForPoint = [this, &reconstruction](const point3D_t point3D_id) { size_t num_observations_for_point = 0; const auto& point3D = reconstruction.Point3D(point3D_id); for (const auto& track_el : point3D.track.Elements()) { if (image_ids_.count(track_el.image_id) == 0) { ++num_observations_for_point; } } return num_observations_for_point; }; for (const auto point3D_id : variable_point3D_ids_) { num_observations += NumObservationsForPoint(point3D_id); } for (const auto point3D_id : constant_point3D_ids_) { num_observations += NumObservationsForPoint(point3D_id); } CHECK_GE(num_observations, 0); return 2 * num_observations; } void BundleAdjustmentConfig::AddImage(const image_t image_id) { image_ids_.insert(image_id); } bool BundleAdjustmentConfig::HasImage(const image_t image_id) const { return image_ids_.find(image_id) != image_ids_.end(); } void BundleAdjustmentConfig::RemoveImage(const image_t image_id) { image_ids_.erase(image_id); } void BundleAdjustmentConfig::SetConstantCamIntrinsics( const camera_t camera_id) { constant_cam_intrinsics_.insert(camera_id); } void BundleAdjustmentConfig::SetVariableCamIntrinsics( const camera_t camera_id) { constant_cam_intrinsics_.erase(camera_id); } bool BundleAdjustmentConfig::HasConstantCamIntrinsics( const camera_t camera_id) const { return constant_cam_intrinsics_.find(camera_id) != constant_cam_intrinsics_.end(); } void BundleAdjustmentConfig::SetConstantSensorFromRigPose( const sensor_t sensor_id) { constant_sensor_from_rig_poses_.insert(sensor_id); } void BundleAdjustmentConfig::SetVariableSensorFromRigPose( const sensor_t sensor_id) { constant_sensor_from_rig_poses_.erase(sensor_id); } bool BundleAdjustmentConfig::HasConstantSensorFromRigPose( const sensor_t sensor_id) const { return constant_sensor_from_rig_poses_.find(sensor_id) != constant_sensor_from_rig_poses_.end(); } void BundleAdjustmentConfig::SetConstantRigFromWorldPose( const frame_t frame_id) { constant_rig_from_world_poses_.insert(frame_id); } void BundleAdjustmentConfig::SetVariableRigFromWorldPose( const frame_t frame_id) { constant_rig_from_world_poses_.erase(frame_id); } bool BundleAdjustmentConfig::HasConstantRigFromWorldPose( const frame_t frame_id) const { return constant_rig_from_world_poses_.find(frame_id) != constant_rig_from_world_poses_.end(); } const std::unordered_set& BundleAdjustmentConfig::Images() const { return image_ids_; } const std::unordered_set& BundleAdjustmentConfig::VariablePoints() const { return variable_point3D_ids_; } const std::unordered_set& BundleAdjustmentConfig::ConstantPoints() const { return constant_point3D_ids_; } const std::unordered_set& BundleAdjustmentConfig::ConstantCamIntrinsics() const { return constant_cam_intrinsics_; } const std::unordered_set& BundleAdjustmentConfig::ConstantSensorFromRigPoses() const { return constant_sensor_from_rig_poses_; } const std::unordered_set& BundleAdjustmentConfig::ConstantRigFromWorldPoses() const { return constant_rig_from_world_poses_; } void BundleAdjustmentConfig::AddVariablePoint(const point3D_t point3D_id) { THROW_CHECK(!HasConstantPoint(point3D_id)); variable_point3D_ids_.insert(point3D_id); } void BundleAdjustmentConfig::AddConstantPoint(const point3D_t point3D_id) { THROW_CHECK(!HasVariablePoint(point3D_id)); constant_point3D_ids_.insert(point3D_id); } void BundleAdjustmentConfig::IgnorePoint(const point3D_t point3D_id) { CHECK(!HasVariablePoint(point3D_id)); CHECK(!HasConstantPoint(point3D_id)); ignored_point3D_ids_.insert(point3D_id); } bool BundleAdjustmentConfig::HasPoint(const point3D_t point3D_id) const { return HasVariablePoint(point3D_id) || HasConstantPoint(point3D_id); } bool BundleAdjustmentConfig::HasVariablePoint( const point3D_t point3D_id) const { return variable_point3D_ids_.count(point3D_id); } bool BundleAdjustmentConfig::HasConstantPoint( const point3D_t point3D_id) const { return constant_point3D_ids_.count(point3D_id); } bool BundleAdjustmentConfig::IsIgnoredPoint(const point3D_t point3D_id) const { return ignored_point3D_ids_.count(point3D_id); } void BundleAdjustmentConfig::RemoveVariablePoint(const point3D_t point3D_id) { variable_point3D_ids_.erase(point3D_id); } void BundleAdjustmentConfig::RemoveConstantPoint(const point3D_t point3D_id) { constant_point3D_ids_.erase(point3D_id); } //////////////////////////////////////////////////////////////////////////////// // BundleAdjuster //////////////////////////////////////////////////////////////////////////////// BundleAdjuster::BundleAdjuster(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config) : options_(options), config_(config) { THROW_CHECK(options_.Check()); } const BundleAdjustmentOptions& BundleAdjuster::Options() const { return options_; } const BundleAdjustmentConfig& BundleAdjuster::Config() const { return config_; } //////////////////////////////////////////////////////////////////////////////// // BundleAdjustmentOptions //////////////////////////////////////////////////////////////////////////////// BundleAdjustmentBackendOptions::BundleAdjustmentBackendOptions() : ceres(std::make_shared()) {} BundleAdjustmentBackendOptions::BundleAdjustmentBackendOptions( const BundleAdjustmentBackendOptions& other) { if (other.ceres) { ceres = std::make_shared(*other.ceres); } } BundleAdjustmentBackendOptions& BundleAdjustmentBackendOptions::operator=( const BundleAdjustmentBackendOptions& other) { if (this == &other) { return *this; } if (other.ceres) { ceres = std::make_shared(*other.ceres); } else { ceres.reset(); } return *this; } bool BundleAdjustmentOptions::Check() const { return THROW_CHECK_NOTNULL(ceres)->Check(); } std::unique_ptr CreateDefaultBundleAdjuster( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, Reconstruction& reconstruction) { switch (options.backend) { case BundleAdjustmentBackend::CERES: return CreateDefaultCeresBundleAdjuster(options, config, reconstruction); } LOG(FATAL_THROW) << "Unknown bundle adjustment backend: " << static_cast(options.backend); return nullptr; } //////////////////////////////////////////////////////////////////////////////// // PosePriorBundleAdjustmentOptions //////////////////////////////////////////////////////////////////////////////// PosePriorBundleAdjustmentBackendOptions:: PosePriorBundleAdjustmentBackendOptions() : ceres(std::make_shared()) {} PosePriorBundleAdjustmentBackendOptions:: PosePriorBundleAdjustmentBackendOptions( const PosePriorBundleAdjustmentBackendOptions& other) { if (other.ceres) { ceres = std::make_shared(*other.ceres); } } PosePriorBundleAdjustmentBackendOptions& PosePriorBundleAdjustmentBackendOptions::operator=( const PosePriorBundleAdjustmentBackendOptions& other) { if (this == &other) { return *this; } if (other.ceres) { ceres = std::make_shared(*other.ceres); } else { ceres.reset(); } return *this; } bool PosePriorBundleAdjustmentOptions::Check() const { CHECK_OPTION_GT(prior_position_fallback_stddev, 0); return THROW_CHECK_NOTNULL(ceres)->Check(); } std::unique_ptr CreatePosePriorBundleAdjuster( const BundleAdjustmentOptions& options, const PosePriorBundleAdjustmentOptions& prior_options, const BundleAdjustmentConfig& config, std::vector pose_priors, Reconstruction& reconstruction) { switch (options.backend) { case BundleAdjustmentBackend::CERES: return CreatePosePriorCeresBundleAdjuster(options, prior_options, config, std::move(pose_priors), reconstruction); } LOG(FATAL_THROW) << "Unknown bundle adjustment backend: " << static_cast(options.backend); return nullptr; } } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment.h000066400000000000000000000245521517363634500231330ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/optim/ransac.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/enum_utils.h" #include #include #include namespace colmap { struct CeresBundleAdjustmentOptions; struct CeresPosePriorBundleAdjustmentOptions; MAKE_ENUM_CLASS_OVERLOAD_STREAM( BundleAdjustmentGauge, -1, UNSPECIFIED, TWO_CAMS_FROM_WORLD, THREE_POINTS); // Termination type for bundle adjustment, independent of solver backend. MAKE_ENUM_CLASS_OVERLOAD_STREAM(BundleAdjustmentTerminationType, 0, CONVERGENCE, NO_CONVERGENCE, FAILURE, USER_SUCCESS, USER_FAILURE); // Backend for bundle adjustment solver. MAKE_ENUM_CLASS_OVERLOAD_STREAM(BundleAdjustmentBackend, 0, CERES); // Summary of bundle adjustment results, independent of solver backend. struct BundleAdjustmentSummary { BundleAdjustmentTerminationType termination_type = BundleAdjustmentTerminationType::FAILURE; // Number of residuals connected to at least one variable parameter block. // Excludes residuals where all connected parameters are constant. int num_residuals = 0; bool IsSolutionUsable() const; virtual std::string BriefReport() const; virtual ~BundleAdjustmentSummary() = default; }; // Configuration container to setup bundle adjustment problems. class BundleAdjustmentConfig { public: BundleAdjustmentConfig() = default; void FixGauge(BundleAdjustmentGauge gauge); BundleAdjustmentGauge FixedGauge() const; size_t NumImages() const; size_t NumPoints() const; size_t NumVariablePoints() const; size_t NumConstantPoints() const; size_t NumConstantCamIntrinsics() const; size_t NumConstantSensorFromRigPoses() const; size_t NumConstantRigFromWorldPoses() const; // Determine the number of residuals for the given reconstruction. The number // of residuals equals the number of observations times two. size_t NumResiduals(const Reconstruction& reconstruction) const; // Add / remove images from the configuration. void AddImage(image_t image_id); bool HasImage(image_t image_id) const; void RemoveImage(image_t image_id); // Set cameras of added images as constant or variable. By default all // cameras of added images are variable. Note that the corresponding images // have to be added prior to calling these methods. void SetConstantCamIntrinsics(camera_t camera_id); void SetVariableCamIntrinsics(camera_t camera_id); bool HasConstantCamIntrinsics(camera_t camera_id) const; // Set the pose of added images as constant. The pose is defined as the // rotational and translational part of the projection matrix. void SetConstantSensorFromRigPose(sensor_t sensor_id); void SetVariableSensorFromRigPose(sensor_t sensor_id); bool HasConstantSensorFromRigPose(sensor_t sensor_id) const; // Set the rig from world pose as constant. void SetConstantRigFromWorldPose(frame_t frame_id); void SetVariableRigFromWorldPose(frame_t frame_id); bool HasConstantRigFromWorldPose(frame_t frame_id) const; // Add / remove points from the configuration. Note that points can either // be variable or constant but not both at the same time. void AddVariablePoint(point3D_t point3D_id); void AddConstantPoint(point3D_t point3D_id); void IgnorePoint(point3D_t point3D_id); bool HasPoint(point3D_t point3D_id) const; bool HasVariablePoint(point3D_t point3D_id) const; bool HasConstantPoint(point3D_t point3D_id) const; bool IsIgnoredPoint(point3D_t point3D_id) const; void RemoveVariablePoint(point3D_t point3D_id); void RemoveConstantPoint(point3D_t point3D_id); // Access configuration data. const std::unordered_set& Images() const; const std::unordered_set& VariablePoints() const; const std::unordered_set& ConstantPoints() const; const std::unordered_set& ConstantCamIntrinsics() const; const std::unordered_set& ConstantSensorFromRigPoses() const; const std::unordered_set& ConstantRigFromWorldPoses() const; private: BundleAdjustmentGauge fixed_gauge_ = BundleAdjustmentGauge::UNSPECIFIED; std::unordered_set constant_cam_intrinsics_; std::unordered_set image_ids_; std::unordered_set variable_point3D_ids_; std::unordered_set constant_point3D_ids_; std::unordered_set ignored_point3D_ids_; std::unordered_set constant_sensor_from_rig_poses_; std::unordered_set constant_rig_from_world_poses_; }; struct BundleAdjustmentBackendOptions { // Ceres-specific options (only used when backend == CERES). std::shared_ptr ceres; BundleAdjustmentBackendOptions(); BundleAdjustmentBackendOptions(const BundleAdjustmentBackendOptions& other); BundleAdjustmentBackendOptions& operator=( const BundleAdjustmentBackendOptions& other); BundleAdjustmentBackendOptions(BundleAdjustmentBackendOptions&& other) = default; BundleAdjustmentBackendOptions& operator=( BundleAdjustmentBackendOptions&& other) = default; }; // Solver-agnostic bundle adjustment options. struct BundleAdjustmentOptions : public BundleAdjustmentBackendOptions { // Whether to refine the focal length parameter group. bool refine_focal_length = true; // Whether to refine the principal point parameter group. bool refine_principal_point = false; // Whether to refine the extra parameter group. bool refine_extra_params = true; // Whether to refine the extrinsic parameter group. bool refine_sensor_from_rig = true; bool refine_rig_from_world = true; // Whether to refine the 3D point positions. When false, all 3D points are // treated as constant, enabling refinement of only camera intrinsics and // poses. This is useful when 3D points come from a reference model and // should not be modified. bool refine_points3D = true; // Minimum track length for a 3D point to be included in bundle adjustment. // Points with fewer observations are ignored. int min_track_length = 0; // Whether to keep the rotation component of rig_from_world constant. // Only takes effect when refine_rig_from_world is true. // When true, only translation is refined. bool constant_rig_from_world_rotation = false; // Whether to print a final summary. bool print_summary = true; // Solver backend to use for bundle adjustment. BundleAdjustmentBackend backend = BundleAdjustmentBackend::CERES; bool Check() const; }; // Abstract base class for bundle adjustment, independent of solver backend. class BundleAdjuster { public: BundleAdjuster(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config); virtual ~BundleAdjuster() = default; virtual std::shared_ptr Solve() = 0; const BundleAdjustmentOptions& Options() const; const BundleAdjustmentConfig& Config() const; protected: BundleAdjustmentOptions options_; BundleAdjustmentConfig config_; }; // Factory function to create bundle adjusters. // Currently uses Ceres as the backend, but can be extended to support // other backends (e.g., Caspar) in the future. std::unique_ptr CreateDefaultBundleAdjuster( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, Reconstruction& reconstruction); struct PosePriorBundleAdjustmentBackendOptions { // Ceres-specific options (only used when backend == CERES). std::shared_ptr ceres; PosePriorBundleAdjustmentBackendOptions(); PosePriorBundleAdjustmentBackendOptions( const PosePriorBundleAdjustmentBackendOptions& other); PosePriorBundleAdjustmentBackendOptions& operator=( const PosePriorBundleAdjustmentBackendOptions& other); PosePriorBundleAdjustmentBackendOptions( PosePriorBundleAdjustmentBackendOptions&& other) = default; PosePriorBundleAdjustmentBackendOptions& operator=( PosePriorBundleAdjustmentBackendOptions&& other) = default; }; // Solver-agnostic pose prior bundle adjustment options. struct PosePriorBundleAdjustmentOptions : public PosePriorBundleAdjustmentBackendOptions { // Fallback if no prior position covariance is provided. double prior_position_fallback_stddev = 1.0; // Sim3 alignment options. RANSACOptions alignment_ransac_options; bool Check() const; }; // Factory function to create pose prior bundle adjusters. std::unique_ptr CreatePosePriorBundleAdjuster( const BundleAdjustmentOptions& options, const PosePriorBundleAdjustmentOptions& prior_options, const BundleAdjustmentConfig& config, std::vector pose_priors, Reconstruction& reconstruction); } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment_ceres.cc000066400000000000000000001273371517363634500244570ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/estimators/alignment.h" #include "colmap/estimators/cost_functions/manifold.h" #include "colmap/estimators/cost_functions/pose_prior.h" #include "colmap/estimators/cost_functions/reprojection_error.h" #include "colmap/estimators/cost_functions/utils.h" #include "colmap/util/cuda.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" #include namespace colmap { namespace { BundleAdjustmentTerminationType CeresTerminationTypeToTerminationType( ceres::TerminationType ceres_type) { switch (ceres_type) { case ceres::CONVERGENCE: return BundleAdjustmentTerminationType::CONVERGENCE; case ceres::NO_CONVERGENCE: return BundleAdjustmentTerminationType::NO_CONVERGENCE; case ceres::FAILURE: return BundleAdjustmentTerminationType::FAILURE; case ceres::USER_SUCCESS: return BundleAdjustmentTerminationType::USER_SUCCESS; case ceres::USER_FAILURE: return BundleAdjustmentTerminationType::USER_FAILURE; } LOG(FATAL_THROW) << "Unknown Ceres termination type: " << ceres_type; return BundleAdjustmentTerminationType::FAILURE; } std::unique_ptr CreateLossFunction( CeresBundleAdjustmentOptions::LossFunctionType loss_function_type, double loss_function_scale) { switch (loss_function_type) { case CeresBundleAdjustmentOptions::LossFunctionType::TRIVIAL: return std::make_unique(); case CeresBundleAdjustmentOptions::LossFunctionType::SOFT_L1: return std::make_unique(loss_function_scale); case CeresBundleAdjustmentOptions::LossFunctionType::CAUCHY: return std::make_unique(loss_function_scale); case CeresBundleAdjustmentOptions::LossFunctionType::HUBER: return std::make_unique(loss_function_scale); } return nullptr; } } // namespace std::shared_ptr CeresBundleAdjustmentSummary::Create(ceres::Solver::Summary ceres_summary) { auto summary = std::make_shared(); summary->termination_type = CeresTerminationTypeToTerminationType(ceres_summary.termination_type); summary->num_residuals = ceres_summary.num_residuals_reduced; summary->ceres_summary = std::move(ceres_summary); return summary; } std::string CeresBundleAdjustmentSummary::BriefReport() const { return ceres_summary.BriefReport(); } //////////////////////////////////////////////////////////////////////////////// // CeresBundleAdjustmentOptions //////////////////////////////////////////////////////////////////////////////// CeresBundleAdjustmentOptions::CeresBundleAdjustmentOptions() { solver_options.function_tolerance = 0.0; solver_options.gradient_tolerance = 1e-4; solver_options.parameter_tolerance = 0.0; solver_options.logging_type = ceres::LoggingType::SILENT; solver_options.max_num_iterations = 100; solver_options.max_linear_solver_iterations = 200; solver_options.max_num_consecutive_invalid_steps = 10; solver_options.max_consecutive_nonmonotonic_steps = 10; solver_options.num_threads = -1; #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = -1; #endif // CERES_VERSION_MAJOR } std::unique_ptr CeresBundleAdjustmentOptions::CreateLossFunction() const { return colmap::CreateLossFunction(loss_function_type, loss_function_scale); } ceres::Solver::Options CeresBundleAdjustmentOptions::CreateSolverOptions( const BundleAdjustmentConfig& config, const ceres::Problem& problem) const { ceres::Solver::Options custom_solver_options = solver_options; if (VLOG_IS_ON(2)) { custom_solver_options.minimizer_progress_to_stdout = true; custom_solver_options.logging_type = ceres::LoggingType::PER_MINIMIZER_ITERATION; } const int num_images = config.NumImages(); const bool has_sparse = custom_solver_options.sparse_linear_algebra_library_type != ceres::NO_SPARSE; int max_num_images_direct_dense_solver = max_num_images_direct_dense_cpu_solver; int max_num_images_direct_sparse_solver = max_num_images_direct_sparse_cpu_solver; #ifdef COLMAP_CUDA_ENABLED bool cuda_solver_enabled = false; #if (CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 2)) && \ !defined(CERES_NO_CUDA) if (use_gpu && num_images >= min_num_images_gpu_solver) { cuda_solver_enabled = true; custom_solver_options.dense_linear_algebra_library_type = ceres::CUDA; max_num_images_direct_dense_solver = max_num_images_direct_dense_gpu_solver; } #else if (use_gpu) { LOG_FIRST_N(WARNING, 1) << "Requested to use GPU for bundle adjustment, but Ceres was " "compiled without CUDA support. Falling back to CPU-based dense " "solvers."; } #endif #if (CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 3)) && \ !defined(CERES_NO_CUDSS) if (use_gpu && num_images >= min_num_images_gpu_solver) { cuda_solver_enabled = true; custom_solver_options.sparse_linear_algebra_library_type = ceres::CUDA_SPARSE; max_num_images_direct_sparse_solver = max_num_images_direct_sparse_gpu_solver; } #else if (use_gpu) { LOG_FIRST_N(WARNING, 1) << "Requested to use GPU for bundle adjustment, but Ceres was " "compiled without cuDSS support. Falling back to CPU-based sparse " "solvers."; } #endif if (cuda_solver_enabled) { const std::vector gpu_indices = CSVToVector(gpu_index); THROW_CHECK_GT(gpu_indices.size(), 0); SetBestCudaDevice(gpu_indices[0]); } #else if (use_gpu) { LOG_FIRST_N(WARNING, 1) << "Requested to use GPU for bundle adjustment, but COLMAP was " "compiled without CUDA support. Falling back to CPU-based " "solvers."; } #endif // COLMAP_CUDA_ENABLED // Auto-select solver type based on problem size, unless disabled. if (auto_select_solver_type) { if (num_images <= max_num_images_direct_dense_solver) { custom_solver_options.linear_solver_type = ceres::DENSE_SCHUR; } else if (has_sparse && num_images <= max_num_images_direct_sparse_solver) { custom_solver_options.linear_solver_type = ceres::SPARSE_SCHUR; } else { // Indirect sparse (preconditioned CG) solver. custom_solver_options.linear_solver_type = ceres::ITERATIVE_SCHUR; custom_solver_options.preconditioner_type = ceres::SCHUR_JACOBI; } } if (problem.NumResiduals() < min_num_residuals_for_cpu_multi_threading) { custom_solver_options.num_threads = 1; #if CERES_VERSION_MAJOR < 2 custom_solver_options.num_linear_solver_threads = 1; #endif // CERES_VERSION_MAJOR } else { custom_solver_options.num_threads = GetEffectiveNumThreads(custom_solver_options.num_threads); #if CERES_VERSION_MAJOR < 2 custom_solver_options.num_linear_solver_threads = GetEffectiveNumThreads(custom_solver_options.num_linear_solver_threads); #endif // CERES_VERSION_MAJOR } std::string solver_error; THROW_CHECK(custom_solver_options.IsValid(&solver_error)) << solver_error; return custom_solver_options; } bool CeresBundleAdjustmentOptions::Check() const { CHECK_OPTION_GE(loss_function_scale, 0); CHECK_OPTION_LT(max_num_images_direct_dense_cpu_solver, max_num_images_direct_sparse_cpu_solver); CHECK_OPTION_LT(max_num_images_direct_dense_gpu_solver, max_num_images_direct_sparse_gpu_solver); return true; } bool CeresPosePriorBundleAdjustmentOptions::Check() const { CHECK_OPTION_GT(prior_position_loss_scale, 0); return true; } namespace { struct FixedGaugeWithThreePoints { // The number of fixed points for the Gauge. Eigen::Index num_fixed_points = 0; // The coordinates of the fixed points as columns. Eigen::Matrix3d fixed_points = Eigen::Matrix3d::Zero(); bool MaybeAddFixedPoint(const Eigen::Vector3d& point) { if (num_fixed_points >= 3) { return false; } fixed_points.col(num_fixed_points) = point; if (fixed_points.colPivHouseholderQr().rank() > num_fixed_points) { ++num_fixed_points; return true; } else { fixed_points.col(num_fixed_points).setZero(); return false; } } }; void FixGaugeWithThreePoints( const std::unordered_map& point3D_num_observations, Reconstruction& reconstruction, ceres::Problem& problem) { FixedGaugeWithThreePoints fixed_gauge; // First check if we already fixed enough points in the problem. for (const auto& [point3D_id, num_observations] : point3D_num_observations) { const Point3D& point3D = reconstruction.Point3D(point3D_id); if (problem.IsParameterBlockConstant(point3D.xyz.data()) && fixed_gauge.MaybeAddFixedPoint(point3D.xyz) && fixed_gauge.num_fixed_points >= 3) { return; } } // Otherwise, fix sufficient points in the problem. for (const auto& [point3D_id, num_observations] : point3D_num_observations) { Point3D& point3D = reconstruction.Point3D(point3D_id); if (!problem.IsParameterBlockConstant(point3D.xyz.data()) && fixed_gauge.MaybeAddFixedPoint(point3D.xyz)) { problem.SetParameterBlockConstant(point3D.xyz.data()); if (fixed_gauge.num_fixed_points >= 3) { return; } } } LOG(WARNING) << "Failed to fix Gauge due to insufficient number of fixed points: " << fixed_gauge.num_fixed_points; } // Note that the following implementation does not handle all degenerate edge // cases well, e.g., where the selected two cameras are not well constrained // with respect to each other with shared observations. Furthermore, the // implementation could be more sophisticated for multi-camera rigs by selecting // camera pairs within a rig, etc. void FixGaugeWithTwoCamsFromWorld( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, const std::set& image_ids, const std::unordered_map& point3D_num_observations, Reconstruction& reconstruction, ceres::Problem& problem) { // No need to fix the Gauge if all frames are constant. if (!options.refine_rig_from_world) { return; } Image* image1 = nullptr; Image* image2 = nullptr; // Check if a sensor is either a reference sensor, or a non-reference sensor // with sensor_from_rig fixed. auto IsParameterizedConstSensor = [&problem, &config, &options](const Image& image) { const sensor_t sensor_id = image.CameraPtr()->SensorId(); if (image.FramePtr()->RigPtr()->IsRefSensor(sensor_id)) { return true; } const Rigid3d& sensor_from_rig = image.FramePtr()->RigPtr()->SensorFromRig(sensor_id); if (problem.HasParameterBlock(sensor_from_rig.params.data()) && problem.IsParameterBlockConstant(sensor_from_rig.params.data())) { return true; } // Cover corner case when ReprojErrorConstantPoseCostFunctor is used if (config.HasConstantSensorFromRigPose(sensor_id) || !options.refine_sensor_from_rig) { return true; } return false; }; // First, search through the already fixed cameras in the problem. for (const image_t image_id : image_ids) { Image& image = reconstruction.Image(image_id); if (config.HasConstantRigFromWorldPose(image.FrameId()) && IsParameterizedConstSensor(image)) { if (image1 == nullptr) { image1 = ℑ } else if (image1 != nullptr && image1->FrameId() != image.FrameId()) { // No need to fix the Gauge if two frames are already fixed. return; } } } // Otherwise, search through the variable cameras in the problem. int frame2_from_world_fixed_dim = 0; for (const image_t image_id : image_ids) { Image& image = reconstruction.Image(image_id); const Rigid3d& rig_from_world = image.FramePtr()->RigFromWorld(); if (image1 == nullptr && IsParameterizedConstSensor(image)) { image1 = ℑ } else if (image1 != nullptr && image1->FrameId() != image.FrameId() && IsParameterizedConstSensor(image) && problem.HasParameterBlock(rig_from_world.params.data())) { // Check if one of the baseline dimensions is large enough and // choose it as the fixed coordinate. If there is no such pair of // frames, then the scale is not constrained well. const Eigen::Vector3d baseline = (image1->FramePtr()->RigFromWorld() * Inverse(image.FramePtr()->RigFromWorld())) .translation(); Eigen::Index max_coeff_idx = 0; if (baseline.cwiseAbs().maxCoeff(&max_coeff_idx) > 1e-9) { image2 = ℑ frame2_from_world_fixed_dim = max_coeff_idx; break; } } } // TODO(jsch): Notice that we could alternatively fall back to fixing the // Gauge between two cameras in the same frame or in different frames. Since // there are many different combinations to iterate through, we instead fall // back to fixing the Gauge with three points for simplicity. Furthermore, // once we support IMUs or other sensors, we should fix the Gauge differently. if (image1 == nullptr || image2 == nullptr) { LOG(WARNING) << "Failed to fix Gauge with two cameras. " "Falling back to fixing Gauge with three points."; FixGaugeWithThreePoints(point3D_num_observations, reconstruction, problem); return; } if (!config.HasConstantRigFromWorldPose(image1->FrameId())) { const Rigid3d& frame1_from_world = image1->FramePtr()->RigFromWorld(); problem.SetParameterBlockConstant(frame1_from_world.params.data()); } if (!config.HasConstantRigFromWorldPose(image2->FrameId())) { Rigid3d& frame2_from_world = image2->FramePtr()->RigFromWorld(); if (options.constant_rig_from_world_rotation) { SetManifold(&problem, frame2_from_world.params.data(), CreateSubsetManifold( 7, {0, 1, 2, 3, 4 + frame2_from_world_fixed_dim})); } else { SetManifold(&problem, frame2_from_world.params.data(), CreateProductManifold( CreateEigenQuaternionManifold(), CreateSubsetManifold(3, {frame2_from_world_fixed_dim}))); } } } void ParameterizeCameras(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, const std::set& camera_ids, Reconstruction& reconstruction, ceres::Problem& problem) { const bool constant_camera = !options.refine_focal_length && !options.refine_principal_point && !options.refine_extra_params; for (const camera_t camera_id : camera_ids) { Camera& camera = reconstruction.Camera(camera_id); if (constant_camera || config.HasConstantCamIntrinsics(camera_id)) { problem.SetParameterBlockConstant(camera.params.data()); } else { std::vector const_camera_params; const_camera_params.reserve(camera.params.size()); if (!options.refine_focal_length) { const span params_idxs = camera.FocalLengthIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (!options.refine_principal_point) { const span params_idxs = camera.PrincipalPointIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (!options.refine_extra_params) { const span params_idxs = camera.ExtraParamsIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (!const_camera_params.empty()) { SetManifold( &problem, camera.params.data(), CreateSubsetManifold(camera.params.size(), const_camera_params)); } } } } void ParameterizeRigsAndFrames(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, const std::set& image_ids, Reconstruction& reconstruction, ceres::Problem& problem) { std::unordered_set parameterized_rig_ids; std::unordered_set parameterized_sensor_ids; std::unordered_set parameterized_frame_ids; for (const image_t image_id : image_ids) { Image& image = reconstruction.Image(image_id); parameterized_rig_ids.insert(image.FramePtr()->RigId()); // Parameterize sensor_from_rig. const sensor_t sensor_id = image.CameraPtr()->SensorId(); const bool not_parameterized_before = parameterized_sensor_ids.insert(sensor_id).second; if (not_parameterized_before && !image.IsRefInFrame()) { Rigid3d& sensor_from_rig = image.FramePtr()->RigPtr()->SensorFromRig(sensor_id); // CostFunction assumes unit quaternions. sensor_from_rig.rotation().normalize(); if (problem.HasParameterBlock(sensor_from_rig.params.data())) { SetManifold(&problem, sensor_from_rig.params.data(), CreateProductManifold(CreateEigenQuaternionManifold(), CreateEuclideanManifold<3>())); if (!options.refine_sensor_from_rig || config.HasConstantSensorFromRigPose(sensor_id)) { problem.SetParameterBlockConstant(sensor_from_rig.params.data()); } } } // Parameterize rig_from_world. if (parameterized_frame_ids.insert(image.FrameId()).second) { Rigid3d& rig_from_world = image.FramePtr()->RigFromWorld(); // CostFunction assumes unit quaternions. rig_from_world.rotation().normalize(); if (problem.HasParameterBlock(rig_from_world.params.data())) { if (!options.refine_rig_from_world || config.HasConstantRigFromWorldPose(image.FrameId())) { problem.SetParameterBlockConstant(rig_from_world.params.data()); } else if (options.constant_rig_from_world_rotation) { SetManifold(&problem, rig_from_world.params.data(), CreateSubsetManifold(7, {0, 1, 2, 3})); } else { SetManifold(&problem, rig_from_world.params.data(), CreateProductManifold(CreateEigenQuaternionManifold(), CreateEuclideanManifold<3>())); } } } } // Set the rig poses as constant, if the reference sensor is not part of the // problem. Otherwise, the relative pose between the sensors is not well // constrained. Notice that this does not handle degenerate configurations and // assumes the observations in the problem constrain the relative poses // sufficiently. for (const rig_t rig_id : parameterized_rig_ids) { Rig& rig = reconstruction.Rig(rig_id); if (parameterized_sensor_ids.count(rig.RefSensorId()) != 0) { continue; } for (auto& [sensor_id, sensor_from_rig] : rig.NonRefSensors()) { THROW_CHECK(sensor_from_rig.has_value()); if (problem.HasParameterBlock(sensor_from_rig->params.data())) { problem.SetParameterBlockConstant(sensor_from_rig->params.data()); } } } } void ParameterizePoints( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, const std::unordered_map& point3D_num_observations, Reconstruction& reconstruction, ceres::Problem& problem) { for (const auto& [point3D_id, num_observations] : point3D_num_observations) { Point3D& point3D = reconstruction.Point3D(point3D_id); if (!options.refine_points3D || point3D.track.Length() > num_observations) { problem.SetParameterBlockConstant(point3D.xyz.data()); } } for (const point3D_t point3D_id : config.ConstantPoints()) { Point3D& point3D = reconstruction.Point3D(point3D_id); problem.SetParameterBlockConstant(point3D.xyz.data()); } } std::shared_ptr CreateSummaryAndLogFailure( ceres::Solver::Summary ceres_summary, const std::string& context) { auto summary = CeresBundleAdjustmentSummary::Create(std::move(ceres_summary)); if (!summary->IsSolutionUsable()) { LOG(ERROR) << context << " failed: " << summary->ceres_summary.message; } return summary; } ceres::Solver::Summary SolveWithGpuFallback( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, ceres::Problem* problem) { const ceres::Solver::Options solver_options = options.ceres->CreateSolverOptions(config, *problem); ceres::Solver::Summary ceres_summary; ceres::Solve(solver_options, problem, &ceres_summary); if (ceres_summary.termination_type == ceres::FAILURE && options.ceres->use_gpu) { const std::string& msg = ceres_summary.message; if (msg.find("CUDA initialization failed") != std::string::npos || msg.find("non-numeric") != std::string::npos || msg.find("Unable to create Jacobian") != std::string::npos) { LOG(WARNING) << "GPU bundle adjustment failed (" << msg << "), retrying with CPU."; auto cpu_options = std::make_shared(*options.ceres); cpu_options->use_gpu = false; const ceres::Solver::Options cpu_solver_options = cpu_options->CreateSolverOptions(config, *problem); ceres::Solve(cpu_solver_options, problem, &ceres_summary); } } return ceres_summary; } class DefaultBundleAdjuster : public CeresBundleAdjuster { public: DefaultBundleAdjuster(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, Reconstruction& reconstruction) : CeresBundleAdjuster(options, config), loss_function_(options_.ceres->CreateLossFunction()) { ceres::Problem::Options problem_options; problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; problem_ = std::make_shared(problem_options); // Verify that reconstruction is internally consistent. THROW_CHECK(reconstruction.IsValid()); // Set up problem. // Warning: AddPointsToProblem assumes that AddImageToProblem is called // first. Do not change order of instructions! for (const image_t image_id : config_.Images()) { AddImageToProblem(image_id, reconstruction); } for (const auto point3D_id : config_.VariablePoints()) { AddPointToProblem(point3D_id, reconstruction); } for (const auto point3D_id : config_.ConstantPoints()) { AddPointToProblem(point3D_id, reconstruction); } ParameterizeCameras(options_, config_, parameterized_camera_ids_, reconstruction, *problem_); ParameterizeRigsAndFrames( options_, config_, parameterized_image_ids_, reconstruction, *problem_); ParameterizePoints(options_, config_, point3D_num_observations_, reconstruction, *problem_); switch (config_.FixedGauge()) { case BundleAdjustmentGauge::UNSPECIFIED: break; case BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD: FixGaugeWithTwoCamsFromWorld(options_, config_, parameterized_image_ids_, point3D_num_observations_, reconstruction, *problem_); break; case BundleAdjustmentGauge::THREE_POINTS: FixGaugeWithThreePoints( point3D_num_observations_, reconstruction, *problem_); break; default: LOG(FATAL_THROW) << "Unknown BundleAdjustmentGauge"; } } std::shared_ptr Solve() override { if (problem_->NumResiduals() == 0) { return std::make_shared(); } ceres::Solver::Summary ceres_summary = SolveWithGpuFallback(options_, config_, problem_.get()); if (options_.print_summary || VLOG_IS_ON(1)) { PrintSolverSummary(ceres_summary, "Bundle adjustment report"); } return CreateSummaryAndLogFailure(std::move(ceres_summary), "Bundle adjustment"); } std::shared_ptr& Problem() override { return problem_; } const std::set& ParameterizedImageIds() const { return parameterized_image_ids_; } void AddImageToProblem(const image_t image_id, Reconstruction& reconstruction) { Image& image = reconstruction.Image(image_id); if (image.IsRefInFrame()) { AddImageWithTrivialFrame(image, reconstruction); } else { AddImageWithNonTrivialFrame(image, reconstruction); } } void AddImageWithTrivialFrame(Image& image, Reconstruction& reconstruction) { Camera& camera = *image.CameraPtr(); const bool constant_cam_from_world = !options_.refine_rig_from_world || config_.HasConstantRigFromWorldPose(image.FrameId()); THROW_CHECK(image.IsRefInFrame()); Rigid3d& rig_from_world = image.FramePtr()->RigFromWorld(); // Add residuals to bundle adjustment problem. size_t num_observations = 0; for (const Point2D& point2D : image.Points2D()) { if (!point2D.HasPoint3D() || config_.IsIgnoredPoint(point2D.point3D_id)) { continue; } Point3D& point3D = reconstruction.Point3D(point2D.point3D_id); THROW_CHECK_GT(point3D.track.Length(), 1); // Skip points with track length below minimum. if (options_.min_track_length > 0 && static_cast(point3D.track.Length()) < options_.min_track_length) { continue; } num_observations += 1; point3D_num_observations_[point2D.point3D_id] += 1; if (constant_cam_from_world) { problem_->AddResidualBlock( CreateCameraCostFunction( camera.model_id, point2D.xy, rig_from_world), loss_function_.get(), point3D.xyz.data(), camera.params.data()); } else { problem_->AddResidualBlock( CreateCameraCostFunction(camera.model_id, point2D.xy), loss_function_.get(), point3D.xyz.data(), rig_from_world.params.data(), camera.params.data()); } } if (num_observations > 0) { parameterized_camera_ids_.insert(image.CameraId()); parameterized_image_ids_.insert(image.ImageId()); } } void AddImageWithNonTrivialFrame(Image& image, Reconstruction& reconstruction) { Camera& camera = *image.CameraPtr(); const sensor_t sensor_id = camera.SensorId(); const bool constant_sensor_from_rig = !options_.refine_sensor_from_rig || config_.HasConstantSensorFromRigPose(sensor_id); const bool constant_rig_from_world = !options_.refine_rig_from_world || config_.HasConstantRigFromWorldPose(image.FrameId()); THROW_CHECK(!image.IsRefInFrame()); Rigid3d& sensor_from_rig = image.FramePtr()->RigPtr()->SensorFromRig(sensor_id); Rigid3d& rig_from_world = image.FramePtr()->RigFromWorld(); const std::optional cam_from_world = (constant_sensor_from_rig && constant_rig_from_world) ? std::make_optional(sensor_from_rig * rig_from_world) : std::nullopt; // Add residuals to bundle adjustment problem. size_t num_observations = 0; for (const Point2D& point2D : image.Points2D()) { if (!point2D.HasPoint3D() || config_.IsIgnoredPoint(point2D.point3D_id)) { continue; } Point3D& point3D = reconstruction.Point3D(point2D.point3D_id); THROW_CHECK_GT(point3D.track.Length(), 1); // Skip points with track length below minimum. if (options_.min_track_length > 0 && static_cast(point3D.track.Length()) < options_.min_track_length) { continue; } num_observations += 1; point3D_num_observations_[point2D.point3D_id] += 1; // The !constant_sensor_from_rig && constant_rig_from_world is // rare enough that we do not have a specialized cost function for it. if (constant_sensor_from_rig && constant_rig_from_world) { problem_->AddResidualBlock( CreateCameraCostFunction( camera.model_id, point2D.xy, cam_from_world.value()), loss_function_.get(), point3D.xyz.data(), camera.params.data()); } else if (!constant_rig_from_world && constant_sensor_from_rig) { problem_->AddResidualBlock( CreateCameraCostFunction( camera.model_id, point2D.xy, sensor_from_rig), loss_function_.get(), point3D.xyz.data(), rig_from_world.params.data(), camera.params.data()); } else { problem_->AddResidualBlock( CreateCameraCostFunction(camera.model_id, point2D.xy), loss_function_.get(), point3D.xyz.data(), sensor_from_rig.params.data(), rig_from_world.params.data(), camera.params.data()); } } if (num_observations > 0) { parameterized_camera_ids_.insert(image.CameraId()); parameterized_image_ids_.insert(image.ImageId()); } } void AddPointToProblem(const point3D_t point3D_id, Reconstruction& reconstruction) { THROW_CHECK(!config_.IsIgnoredPoint(point3D_id)); Point3D& point3D = reconstruction.Point3D(point3D_id); // Skip points with track length below minimum. if (options_.min_track_length > 0 && static_cast(point3D.track.Length()) < options_.min_track_length) { return; } size_t& num_observations = point3D_num_observations_[point3D_id]; // Is 3D point already fully contained in the problem? I.e. its entire // track is contained in `variable_image_ids`, `constant_image_ids`, // `constant_x_image_ids`. if (num_observations == point3D.track.Length()) { return; } for (const auto& track_el : point3D.track.Elements()) { // Skip observations that were already added in `FillImages`. if (config_.HasImage(track_el.image_id)) { continue; } num_observations += 1; Image& image = reconstruction.Image(track_el.image_id); Camera& camera = *image.CameraPtr(); const Point2D& point2D = image.Point2D(track_el.point2D_idx); if (image.IsRefInFrame()) { Rigid3d& cam_from_world = image.FramePtr()->RigFromWorld(); problem_->AddResidualBlock( CreateCameraCostFunction( camera.model_id, point2D.xy, cam_from_world), loss_function_.get(), point3D.xyz.data(), camera.params.data()); } else { Rigid3d& cam_from_rig = image.FramePtr()->RigPtr()->SensorFromRig( image.CameraPtr()->SensorId()); Rigid3d& rig_from_world = image.FramePtr()->RigFromWorld(); problem_->AddResidualBlock( CreateCameraCostFunction( camera.model_id, point2D.xy, cam_from_rig * rig_from_world), loss_function_.get(), point3D.xyz.data(), camera.params.data()); } // Do not optimize intrinsics if th corresponding images // were not included explicitly in the config. if (parameterized_camera_ids_.insert(image.CameraId()).second) { config_.SetConstantCamIntrinsics(image.CameraId()); } } } private: std::shared_ptr problem_; std::unique_ptr loss_function_; std::set parameterized_camera_ids_; std::set parameterized_image_ids_; std::unordered_map point3D_num_observations_; }; class PosePriorBundleAdjuster : public CeresBundleAdjuster { public: PosePriorBundleAdjuster(const BundleAdjustmentOptions& options, const PosePriorBundleAdjustmentOptions& prior_options, const BundleAdjustmentConfig& config, std::vector pose_priors, Reconstruction& reconstruction) : CeresBundleAdjuster(options, config), prior_options_(prior_options), pose_priors_(std::move(pose_priors)), reconstruction_(reconstruction) { THROW_CHECK(prior_options_.Check()); // Filter irrelevant pose priors. pose_priors_.erase( std::remove_if(pose_priors_.begin(), pose_priors_.end(), [this](const auto& pose_prior) { return !pose_prior.HasPosition() || pose_prior.corr_data_id.sensor_id.type != SensorType::CAMERA || !config_.HasImage(pose_prior.corr_data_id.id); }), pose_priors_.end()); const bool use_prior_position = AlignReconstruction(); // Fix 7-DOFs of BA problem if not enough valid pose priors. if (use_prior_position) { // Normalize the reconstruction to avoid any numerical instability but // do not transform priors as they will be transformed when added to // ceres::Problem. normalized_from_metric_ = reconstruction_.Normalize(/*fixed_scale=*/true); } else { config_.FixGauge(BundleAdjustmentGauge::THREE_POINTS); } // WARNING: Do not move this above the reconstruction normalization. default_bundle_adjuster_ = std::make_unique( options_, config_, reconstruction); if (use_prior_position) { prior_loss_function_ = CreateLossFunction( prior_options_.ceres->prior_position_loss_function_type, prior_options_.ceres->prior_position_loss_scale); // Only consider parameterized images for pose priors. Notice that some // images may be configured to be included in the BA problem but have no // reprojection constraints, etc. const std::set& parameterized_image_ids = default_bundle_adjuster_->ParameterizedImageIds(); for (const auto& pose_prior : pose_priors_) { if (parameterized_image_ids.count(pose_prior.corr_data_id.id) > 0) { AddImagePosePriorToProblem( pose_prior.corr_data_id.id, pose_prior, reconstruction); } } } } std::shared_ptr Solve() override { std::shared_ptr problem = default_bundle_adjuster_->Problem(); if (problem->NumResiduals() == 0) { return std::make_shared(); } ceres::Solver::Summary ceres_summary = SolveWithGpuFallback(options_, config_, problem.get()); reconstruction_.Transform(Inverse(normalized_from_metric_)); if (options_.print_summary || VLOG_IS_ON(1)) { PrintSolverSummary(ceres_summary, "Pose Prior Bundle adjustment report"); } return CreateSummaryAndLogFailure(std::move(ceres_summary), "Pose prior bundle adjustment"); } std::shared_ptr& Problem() override { return default_bundle_adjuster_->Problem(); } void AddImagePosePriorToProblem(image_t image_id, const PosePrior& pose_prior, Reconstruction& reconstruction) { Image& image = reconstruction.Image(image_id); const bool constant_sensor_from_rig = !options_.refine_sensor_from_rig || config_.HasConstantSensorFromRigPose(image.CameraPtr()->SensorId()); const bool constant_rig_from_world = !options_.refine_rig_from_world || config_.HasConstantRigFromWorldPose(image.FrameId()); if (constant_sensor_from_rig && constant_rig_from_world) { return; } ceres::Problem& problem = *default_bundle_adjuster_->Problem(); Frame& frame = *image.FramePtr(); Rigid3d& rig_from_world = frame.RigFromWorld(); const Eigen::Vector3d normalized_position = normalized_from_metric_ * pose_prior.position; const Eigen::Matrix3d normalized_from_metric_scaled_rotation = normalized_from_metric_.scale() * normalized_from_metric_.rotation().toRotationMatrix(); const Eigen::Matrix3d position_cov = pose_prior.HasPositionCov() ? pose_prior.position_covariance : (prior_options_.prior_position_fallback_stddev * prior_options_.prior_position_fallback_stddev * Eigen::Matrix3d::Identity()); const Eigen::Matrix3d normalized_position_cov = normalized_from_metric_scaled_rotation * position_cov * normalized_from_metric_scaled_rotation.transpose(); if (image.IsRefInFrame()) { problem.AddResidualBlock( CovarianceWeightedCostFunctor:: Create(normalized_position_cov, normalized_position), prior_loss_function_.get(), rig_from_world.params.data()); } else { Rigid3d& cam_from_rig = frame.RigPtr()->SensorFromRig(image.CameraPtr()->SensorId()); problem.AddResidualBlock( CovarianceWeightedCostFunctor< AbsoluteRigPosePositionPriorCostFunctor>:: Create(normalized_position_cov, normalized_position), prior_loss_function_.get(), cam_from_rig.params.data(), rig_from_world.params.data()); } } bool AlignReconstruction() { RANSACOptions ransac_options = prior_options_.alignment_ransac_options; if (ransac_options.max_error <= 0) { std::vector rms_vars; rms_vars.reserve(pose_priors_.size()); for (const auto& pose_prior : pose_priors_) { const double trace = pose_prior.position_covariance.trace(); if (trace <= 0.0) { continue; } rms_vars.push_back(trace / 3.0); } if (rms_vars.empty()) { LOG(WARNING) << "No pose priors with valid covariance found."; rms_vars.push_back(prior_options_.prior_position_fallback_stddev * prior_options_.prior_position_fallback_stddev); } // Set max error using the median RMS variance of valid pose priors. // Scaled by sqrt(chi-square 95% quantile, 3 DOF) to approximate a 95% // confidence radius. ransac_options.max_error = std::sqrt(kChiSquare95ThreeDof * Median(rms_vars)); } VLOG(2) << "Robustly aligning reconstruction with max_error=" << ransac_options.max_error; Sim3d metric_from_orig; if (!AlignReconstructionToPosePriors( reconstruction_, pose_priors_, ransac_options, &metric_from_orig)) { LOG(WARNING) << "Alignment w.r.t. prior positions failed"; return false; } reconstruction_.Transform(metric_from_orig); // Compute alignment error w.r.t. prior positions. if (VLOG_IS_ON(2)) { std::vector verr2_wrt_prior; verr2_wrt_prior.reserve(config_.NumImages()); for (const auto& pose_prior : pose_priors_) { const auto& image = reconstruction_.Image(pose_prior.corr_data_id.id); verr2_wrt_prior.push_back( (image.ProjectionCenter() - pose_prior.position).squaredNorm()); } VLOG(2) << "Alignment error w.r.t. prior positions:\n" << " - rmse: " << std::sqrt(Mean(verr2_wrt_prior)) << '\n' << " - median: " << std::sqrt(Median(verr2_wrt_prior)) << '\n'; } return true; } private: PosePriorBundleAdjustmentOptions prior_options_; std::vector pose_priors_; Reconstruction& reconstruction_; std::unique_ptr default_bundle_adjuster_; std::unique_ptr prior_loss_function_; Sim3d normalized_from_metric_; }; } // namespace std::unique_ptr CreateDefaultCeresBundleAdjuster( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, Reconstruction& reconstruction) { return std::make_unique( options, config, reconstruction); } std::unique_ptr CreatePosePriorCeresBundleAdjuster( const BundleAdjustmentOptions& options, const PosePriorBundleAdjustmentOptions& prior_options, const BundleAdjustmentConfig& config, std::vector pose_priors, Reconstruction& reconstruction) { return std::make_unique( options, prior_options, config, std::move(pose_priors), reconstruction); } void PrintSolverSummary(const ceres::Solver::Summary& summary, const std::string& header) { if (VLOG_IS_ON(3)) { LOG(INFO) << summary.FullReport(); } std::ostringstream log; log << header << '\n'; log << std::right << std::setw(16) << "Residuals : "; log << std::left << summary.num_residuals_reduced << '\n'; log << std::right << std::setw(16) << "Parameters : "; log << std::left << summary.num_effective_parameters_reduced << '\n'; log << std::right << std::setw(16) << "Iterations : "; log << std::left << summary.num_successful_steps + summary.num_unsuccessful_steps << '\n'; log << std::right << std::setw(16) << "Time : "; log << std::left << summary.total_time_in_seconds << " [s]\n"; log << std::right << std::setw(16) << "Initial cost : "; log << std::right << std::setprecision(6) << std::sqrt(summary.initial_cost / summary.num_residuals_reduced) << " [px]\n"; log << std::right << std::setw(16) << "Final cost : "; log << std::right << std::setprecision(6) << std::sqrt(summary.final_cost / summary.num_residuals_reduced) << " [px]\n"; log << std::right << std::setw(16) << "Termination : "; log << std::right << ceres::TerminationTypeToString(summary.termination_type) << "\n\n"; LOG(INFO) << log.str(); } } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment_ceres.h000066400000000000000000000130161517363634500243050ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/estimators/bundle_adjustment.h" #include "colmap/math/math.h" #include "colmap/optim/ransac.h" #include namespace colmap { // Ceres-specific bundle adjustment options. struct CeresBundleAdjustmentOptions { // Loss function types: Trivial (non-robust) and robust loss functions. enum class LossFunctionType { TRIVIAL, SOFT_L1, CAUCHY, HUBER }; LossFunctionType loss_function_type = LossFunctionType::TRIVIAL; // Scaling factor determines residual at which robustification takes place. double loss_function_scale = 1.0; // Whether to use Ceres' CUDA linear algebra library, if available. bool use_gpu = false; std::string gpu_index = "-1"; // Ceres-Solver options. ceres::Solver::Options solver_options; // Heuristic threshold to switch from CPU to GPU based solvers. // Typically, the GPU is faster for large problems but the overhead of // transferring memory from the CPU to the GPU leads to better CPU performance // for small problems. This depends on the specific problem and hardware. int min_num_images_gpu_solver = 50; // Heuristic threshold on the minimum number of residuals to enable // multi-threading. Note that single-threaded is typically better for small // bundle adjustment problems due to the overhead of threading. int min_num_residuals_for_cpu_multi_threading = 50000; // Heuristic thresholds to switch between direct, sparse, and iterative // solvers. These thresholds may not be optimal for all types of problems. int max_num_images_direct_dense_cpu_solver = 50; int max_num_images_direct_sparse_cpu_solver = 1000; int max_num_images_direct_dense_gpu_solver = 200; int max_num_images_direct_sparse_gpu_solver = 4000; // Whether to automatically select solver type based on problem size. // When false, uses the linear_solver_type and preconditioner_type // from solver_options directly. bool auto_select_solver_type = true; CeresBundleAdjustmentOptions(); // Create loss function for given options. std::unique_ptr CreateLossFunction() const; // Create options tailored for given bundle adjustment config and problem. ceres::Solver::Options CreateSolverOptions( const BundleAdjustmentConfig& config, const ceres::Problem& problem) const; bool Check() const; }; // Ceres-specific bundle adjustment summary with access to full solver details. struct CeresBundleAdjustmentSummary : public BundleAdjustmentSummary { ceres::Solver::Summary ceres_summary; std::string BriefReport() const override; static std::shared_ptr Create( ceres::Solver::Summary ceres_summary); }; // Ceres-specific pose prior bundle adjustment options. struct CeresPosePriorBundleAdjustmentOptions { // Loss function for prior position loss. CeresBundleAdjustmentOptions::LossFunctionType prior_position_loss_function_type = CeresBundleAdjustmentOptions::LossFunctionType::TRIVIAL; // Threshold on the residual for the robust loss. double prior_position_loss_scale = std::sqrt(kChiSquare95ThreeDof); bool Check() const; }; // Ceres-specific bundle adjuster with access to the underlying problem. class CeresBundleAdjuster : public BundleAdjuster { public: using BundleAdjuster::BundleAdjuster; virtual std::shared_ptr& Problem() = 0; }; std::unique_ptr CreateDefaultCeresBundleAdjuster( const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config, Reconstruction& reconstruction); std::unique_ptr CreatePosePriorCeresBundleAdjuster( const BundleAdjustmentOptions& options, const PosePriorBundleAdjustmentOptions& prior_options, const BundleAdjustmentConfig& config, std::vector pose_priors, Reconstruction& reconstruction); void PrintSolverSummary(const ceres::Solver::Summary& summary, const std::string& header); } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment_ceres_test.cc000066400000000000000000001733661517363634500255210ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/geometry/rigid3_matchers.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include "colmap/sensor/models.h" #include "colmap/util/testing.h" #include // Due to pose normalization operations, constant variables may not be perfectly // fixed during bundle adjustment. constexpr double kConstantPoseVarEps = 1e-9; #define CheckVariableCamera(camera, orig_camera) \ { \ const size_t focal_length_idx = \ SimpleRadialCameraModel::focal_length_idxs[0]; \ const size_t extra_param_idx = \ SimpleRadialCameraModel::extra_params_idxs[0]; \ EXPECT_NE((camera).params[focal_length_idx], \ (orig_camera).params[focal_length_idx]); \ EXPECT_NE((camera).params[extra_param_idx], \ (orig_camera).params[extra_param_idx]); \ } #define CheckConstantCamera(camera, orig_camera) \ { \ const size_t focal_length_idx = \ SimpleRadialCameraModel::focal_length_idxs[0]; \ const size_t extra_param_idx = \ SimpleRadialCameraModel::extra_params_idxs[0]; \ EXPECT_EQ((camera).params[focal_length_idx], \ (orig_camera).params[focal_length_idx]); \ EXPECT_EQ((camera).params[extra_param_idx], \ (orig_camera).params[extra_param_idx]); \ } #define CheckVariableCamFromWorld(image, orig_image) \ { \ EXPECT_THAT((image).CamFromWorld(), \ testing::Not(Rigid3dEq((orig_image).CamFromWorld()))); \ } #define CheckConstantCamFromWorld(image, orig_image) \ { \ EXPECT_THAT((image).CamFromWorld(), \ Rigid3dNear((orig_image).CamFromWorld(), \ kConstantPoseVarEps, \ kConstantPoseVarEps)); \ } #define CheckConstantCamFromWorldTranslationCoord(image, orig_image) \ { \ size_t num_constant_coords = 0; \ for (int i = 0; i < 3; ++i) { \ if (std::abs((image).CamFromWorld().translation()(i) - \ (orig_image).CamFromWorld().translation()(i)) < \ kConstantPoseVarEps) { \ ++num_constant_coords; \ } \ } \ EXPECT_EQ(num_constant_coords, 1); \ } #define CheckVariablePoint(point, orig_point) \ { \ EXPECT_NE((point).xyz, (orig_point).xyz); \ } #define CheckConstantPoint(point, orig_point) \ { \ EXPECT_EQ((point).xyz, (orig_point).xyz); \ } namespace colmap { namespace { // Helper to get Problem from BundleAdjuster (requires casting to Ceres impl) inline ceres::Problem& GetCeresProblem(BundleAdjuster& bundle_adjuster) { auto* ceres_ba = dynamic_cast(&bundle_adjuster); CHECK_NOTNULL(ceres_ba); return *ceres_ba->Problem(); } // Helper to get ceres::Solver::Summary from base summary inline const ceres::Solver::Summary& GetCeresSummary( const BundleAdjustmentSummary* summary) { auto* ceres_summary = dynamic_cast(summary); CHECK_NOTNULL(ceres_summary); return ceres_summary->ceres_summary; } TEST(DefaultBundleAdjuster, Nominal) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 200; SynthesizeDataset(synthetic_dataset_options, >_reconstruction); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.point3D_stddev = 0.1; synthetic_noise_options.rig_from_world_rotation_stddev = 0.5; synthetic_noise_options.rig_from_world_translation_stddev = 0.1; SynthesizeNoise(synthetic_noise_options, &reconstruction); BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.0)); } TEST(DefaultBundleAdjuster, NominalMultiCameraRig) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 200; SynthesizeDataset(synthetic_dataset_options, >_reconstruction); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.point3D_stddev = 0.1; synthetic_noise_options.rig_from_world_rotation_stddev = 0.5; synthetic_noise_options.rig_from_world_translation_stddev = 0.1; SynthesizeNoise(synthetic_noise_options, &reconstruction); BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.0)); } TEST(DefaultBundleAdjuster, TwoView) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 309); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorldTranslationCoord(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, TwoViewRig) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 2; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 4 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 800); // 97 x 3 point parameters (3 fixed for gauge) // + 2 x 6 rig_from_world parameters // + 1 x 6 sensor_from_rig parameters // + 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 313); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckVariableCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); size_t num_variable_points = 0; for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D != orig_reconstruction.Point3D(point3D_id)) { ++num_variable_points; } } EXPECT_EQ(num_variable_points, 97); } TEST(DefaultBundleAdjuster, ManyViewRig) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 30 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 6000); // 97 x 3 point parameters (3 fixed for gauge) // + 10 x 6 rig_from_world parameters // + 4 x 6 sensor_from_rig parameters // + 6 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 387); for (const auto& [camera_id, camera] : reconstruction.Cameras()) { CheckVariableCamera(camera, orig_reconstruction.Camera(camera_id)); } for (const image_t image_id : reconstruction.RegImageIds()) { CheckVariableCamFromWorld(reconstruction.Image(image_id), orig_reconstruction.Image(image_id)); } size_t num_variable_points = 0; for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D != orig_reconstruction.Point3D(point3D_id)) { ++num_variable_points; } } EXPECT_EQ(num_variable_points, 97); } TEST(DefaultBundleAdjuster, ManyViewRigConstantSensorFromRig) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.SetConstantSensorFromRigPose(reconstruction.Camera(2).SensorId()); config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 30 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 6000); // 97 x 3 point parameters (3 fixed for gauge) // + 10 x 6 rig_from_world parameters // + 3 x 6 sensor_from_rig parameters // + 6 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 381); for (const auto& [camera_id, camera] : reconstruction.Cameras()) { CheckVariableCamera(camera, orig_reconstruction.Camera(camera_id)); } for (const image_t image_id : reconstruction.RegImageIds()) { CheckVariableCamFromWorld(reconstruction.Image(image_id), orig_reconstruction.Image(image_id)); } size_t num_variable_points = 0; for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D != orig_reconstruction.Point3D(point3D_id)) { ++num_variable_points; } } EXPECT_EQ(num_variable_points, 97); } TEST(DefaultBundleAdjuster, ManyViewRigConstantRigFromWorld) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 3; synthetic_dataset_options.num_frames_per_rig = 5; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } const frame_t constant_frame_id = 1; config.SetConstantRigFromWorldPose(constant_frame_id); config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 30 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 6000); // 97 x 3 point parameters (3 fixed for gauge) // + 9 x 6 rig_from_world parameters // + 4 x 6 sensor_from_rig parameters // + 6 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 381); for (const auto& [camera_id, camera] : reconstruction.Cameras()) { CheckVariableCamera(camera, orig_reconstruction.Camera(camera_id)); } for (const image_t image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); if (image.FrameId() == constant_frame_id && image.FramePtr()->RigPtr()->IsRefSensor( image.CameraPtr()->SensorId())) { CheckConstantCamFromWorld(image, orig_reconstruction.Image(image_id)); } else { CheckVariableCamFromWorld(image, orig_reconstruction.Image(image_id)); } } size_t num_variable_points = 0; for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D != orig_reconstruction.Point3D(point3D_id)) { ++num_variable_points; } } EXPECT_EQ(num_variable_points, 97); } TEST(DefaultBundleAdjuster, ConstantRigFromWorldRotation) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 3; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.AddImage(3); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; options.constant_rig_from_world_rotation = true; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 600); // 100 x 3 point parameters // + 2 translation parameters (second image, one coord fixed for gauge) // + 3 translation parameters (third image) // + 3 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 311); // Check rotations are constant for all images for (const image_t image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); const auto& orig_image = orig_reconstruction.Image(image_id); // Rotation should be nearly unchanged (use angular distance) EXPECT_LE(image.CamFromWorld().rotation().angularDistance( orig_image.CamFromWorld().rotation()), kConstantPoseVarEps); } // Check translations are variable (except for gauge-fixed parts) // At least one image should have changed translation bool has_variable_translation = false; for (const image_t image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); const auto& orig_image = orig_reconstruction.Image(image_id); if ((image.CamFromWorld().translation() - orig_image.CamFromWorld().translation()) .norm() > kConstantPoseVarEps) { has_variable_translation = true; break; } } EXPECT_TRUE(has_variable_translation); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, TwoViewConstantCamera) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.SetConstantRigFromWorldPose(1); config.SetConstantRigFromWorldPose(2); config.SetConstantCamIntrinsics(1); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 100 x 3 point parameters // + 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 302); CheckConstantCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, PartiallyContainedTracks) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 3; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.num_points2D_without_point3D = 0; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto variable_point3D_id = reconstruction.Image(3).Point2D(0).point3D_id; reconstruction.DeleteObservation(3, 0); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.SetConstantRigFromWorldPose(1); config.SetConstantRigFromWorldPose(2); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 1 x 3 point parameters // 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 7); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); CheckConstantCamera(reconstruction.Camera(3), orig_reconstruction.Camera(3)); CheckConstantCamFromWorld(reconstruction.Image(3), orig_reconstruction.Image(3)); for (const auto& point3D : reconstruction.Points3D()) { if (point3D.first == variable_point3D_id) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } else { CheckConstantPoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } } TEST(DefaultBundleAdjuster, PartiallyContainedTracksForceToOptimizePoint) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 3; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.num_points2D_without_point3D = 0; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const point3D_t variable_point3D_id = reconstruction.Image(3).Point2D(0).point3D_id; const point3D_t add_variable_point3D_id = reconstruction.Image(3).Point2D(1).point3D_id; const point3D_t add_constant_point3D_id = reconstruction.Image(3).Point2D(2).point3D_id; reconstruction.DeleteObservation(3, 0); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.SetConstantRigFromWorldPose(1); config.SetConstantRigFromWorldPose(2); config.AddVariablePoint(add_variable_point3D_id); config.AddConstantPoint(add_constant_point3D_id); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image // + 2 residuals in 3rd image for added variable 3D point // (added constant point does not add residuals since the image/camera // is also constant). EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 402); // 2 x 3 point parameters // 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 10); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); CheckConstantCamera(reconstruction.Camera(3), orig_reconstruction.Camera(3)); CheckConstantCamFromWorld(reconstruction.Image(3), orig_reconstruction.Image(3)); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D_id == variable_point3D_id || point3D_id == add_variable_point3D_id) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } else { CheckConstantPoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } } TEST(DefaultBundleAdjuster, ConstantPoints) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; const point3D_t constant_point3D_id1 = 1; const point3D_t constant_point3D_id2 = 2; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.SetConstantRigFromWorldPose(1); config.SetConstantRigFromWorldPose(2); config.AddConstantPoint(constant_point3D_id1); config.AddConstantPoint(constant_point3D_id2); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 98 x 3 point parameters // + 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 298); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { if (point3D_id == constant_point3D_id1 || point3D_id == constant_point3D_id2) { CheckConstantPoint(point3D, orig_reconstruction.Point3D(point3D_id)); } else { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } } TEST(DefaultBundleAdjuster, VariableImage) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 3; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.AddImage(3); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 600); // 100 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 6 rig_from_world parameters (pose of third image) // + 3 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 317); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantCamFromWorldTranslationCoord(reconstruction.Image(2), orig_reconstruction.Image(2)); CheckVariableCamera(reconstruction.Camera(3), orig_reconstruction.Camera(3)); CheckVariableCamFromWorld(reconstruction.Image(3), orig_reconstruction.Image(3)); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, ConstantFocalLength) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; options.refine_focal_length = false; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 307); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCamFromWorldTranslationCoord(reconstruction.Image(2), orig_reconstruction.Image(2)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(1); const auto& orig_camera0 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera0.params[focal_length_idx] == orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[extra_param_idx] != orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(2); const auto& orig_camera1 = orig_reconstruction.Camera(2); EXPECT_TRUE(camera1.params[focal_length_idx] == orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[extra_param_idx] != orig_camera1.params[extra_param_idx]); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, VariablePrincipalPoint) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; options.refine_principal_point = true; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 8 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 313); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCamFromWorldTranslationCoord(reconstruction.Image(2), orig_reconstruction.Image(2)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t principal_point_idx_x = SimpleRadialCameraModel::principal_point_idxs[0]; const size_t principal_point_idx_y = SimpleRadialCameraModel::principal_point_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(1); const auto& orig_camera0 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera0.params[focal_length_idx] != orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[principal_point_idx_x] != orig_camera0.params[principal_point_idx_x]); EXPECT_TRUE(camera0.params[principal_point_idx_y] != orig_camera0.params[principal_point_idx_y]); EXPECT_TRUE(camera0.params[extra_param_idx] != orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(2); const auto& orig_camera1 = orig_reconstruction.Camera(2); EXPECT_TRUE(camera1.params[focal_length_idx] != orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[principal_point_idx_x] != orig_camera1.params[principal_point_idx_x]); EXPECT_TRUE(camera1.params[principal_point_idx_y] != orig_camera1.params[principal_point_idx_y]); EXPECT_TRUE(camera1.params[extra_param_idx] != orig_camera1.params[extra_param_idx]); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, ConstantExtraParam) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; options.refine_extra_params = false; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 307); CheckConstantCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCamFromWorldTranslationCoord(reconstruction.Image(2), orig_reconstruction.Image(2)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(1); const auto& orig_camera0 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera0.params[focal_length_idx] != orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[extra_param_idx] == orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(2); const auto& orig_camera1 = orig_reconstruction.Camera(2); EXPECT_TRUE(camera1.params[focal_length_idx] != orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[extra_param_idx] == orig_camera1.params[extra_param_idx]); for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckVariablePoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, ConstantPoints3D) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 20; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); BundleAdjustmentOptions options; options.refine_points3D = false; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 20 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 80); // 0 point parameters (all constant due to refine_points3D=false) // + 2 x 6 rig_from_world parameters // + 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 16); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableCamFromWorld(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckVariableCamFromWorld(reconstruction.Image(2), orig_reconstruction.Image(2)); // All 3D points should remain constant. for (const auto& [point3D_id, point3D] : reconstruction.Points3D()) { CheckConstantPoint(point3D, orig_reconstruction.Point3D(point3D_id)); } } TEST(DefaultBundleAdjuster, FixGaugeWithThreePoints) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); auto ExpectValidSolve = [&config, &reconstruction]( const int num_effective_parameters_reduced) { const auto summary1 = CreateDefaultCeresBundleAdjuster( BundleAdjustmentOptions(), config, reconstruction) ->Solve(); ASSERT_NE(summary1->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(GetCeresSummary(summary1.get()).num_effective_parameters_reduced, num_effective_parameters_reduced); }; ExpectValidSolve(316); config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); ExpectValidSolve(307); config.AddConstantPoint(1); ExpectValidSolve(307); config.AddConstantPoint(2); config.AddConstantPoint(3); ExpectValidSolve(307); config.AddConstantPoint(4); ExpectValidSolve(304); } TEST(DefaultBundleAdjuster, FixGaugeWithTwoCamsFromWorld) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentOptions options; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.AddImage(3); config.AddImage(4); auto ExpectValidSolve = [&options, &config, &reconstruction]( const int num_effective_parameters_reduced) { const auto summary1 = CreateDefaultCeresBundleAdjuster(options, config, reconstruction) ->Solve(); ASSERT_NE(summary1->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(GetCeresSummary(summary1.get()).num_effective_parameters_reduced, num_effective_parameters_reduced); }; options.refine_rig_from_world = false; ExpectValidSolve(320); options.refine_rig_from_world = true; ExpectValidSolve(332); options.refine_rig_from_world = false; config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); ExpectValidSolve(320); options.refine_rig_from_world = true; ExpectValidSolve(325); config.SetConstantRigFromWorldPose(1); ExpectValidSolve(325); config.SetConstantRigFromWorldPose(2); ExpectValidSolve(320); } TEST(DefaultBundleAdjuster, FixGaugeWithTwoCamsFromWorldFixSensorFromRig) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentOptions options; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.AddImage(3); config.AddImage(4); auto ExpectValidSolve = [&options, &config, &reconstruction]( const int num_effective_parameters_reduced) { const auto summary1 = CreateDefaultCeresBundleAdjuster(options, config, reconstruction) ->Solve(); ASSERT_NE(summary1->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(GetCeresSummary(summary1.get()).num_effective_parameters_reduced, num_effective_parameters_reduced); }; options.refine_rig_from_world = false; options.refine_sensor_from_rig = false; ExpectValidSolve(308); options.refine_rig_from_world = true; ExpectValidSolve(320); options.refine_rig_from_world = false; config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); ExpectValidSolve(308); options.refine_rig_from_world = true; ExpectValidSolve(313); config.SetConstantRigFromWorldPose(1); ExpectValidSolve(313); config.SetConstantRigFromWorldPose(2); ExpectValidSolve(308); } TEST(DefaultBundleAdjuster, FixGaugeWithTwoCamsFromWorldNoReferenceSensor) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; // Delete observations from the two reference images. THROW_CHECK(reconstruction.Image(1).IsRefInFrame()); THROW_CHECK(reconstruction.Image(3).IsRefInFrame()); for (point2D_t i = 0; i < reconstruction.Image(1).NumPoints2D(); ++i) { if (reconstruction.Image(1).Point2D(i).HasPoint3D()) { reconstruction.DeleteObservation(1, i); } } for (point2D_t i = 0; i < reconstruction.Image(3).NumPoints2D(); ++i) { if (reconstruction.Image(3).Point2D(i).HasPoint3D()) { reconstruction.DeleteObservation(3, i); } } // Only add two non-reference images. BundleAdjustmentOptions options; BundleAdjustmentConfig config; config.AddImage(2); config.AddImage(4); auto ExpectValidSolve = [&options, &config, &reconstruction]( const int num_effective_parameters_reduced) { const auto summary1 = CreateDefaultCeresBundleAdjuster(options, config, reconstruction) ->Solve(); THROW_CHECK_NE(summary1->termination_type, BundleAdjustmentTerminationType::FAILURE); THROW_CHECK_EQ( GetCeresSummary(summary1.get()).num_effective_parameters_reduced, num_effective_parameters_reduced); }; // refine_sensor_from_rig should have no effect when there are no reference // sensors options.refine_rig_from_world = true; options.refine_sensor_from_rig = true; ExpectValidSolve(316); options.refine_rig_from_world = false; ExpectValidSolve(304); options.refine_rig_from_world = true; ExpectValidSolve(316); options.refine_rig_from_world = false; config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); ExpectValidSolve(304); options.refine_sensor_from_rig = false; ExpectValidSolve(304); options.refine_rig_from_world = true; ExpectValidSolve(309); config.SetConstantRigFromWorldPose(1); ExpectValidSolve(309); options.refine_rig_from_world = false; ExpectValidSolve(304); config.SetConstantRigFromWorldPose(2); options.refine_rig_from_world = true; ExpectValidSolve(304); options.refine_rig_from_world = false; ExpectValidSolve(304); } TEST(DefaultBundleAdjuster, FixGaugeWithTwoCamsFromWorldFallback) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 2; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentOptions options; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); // The current implementation needs two reference cameras in different frames // to fix the gauge. If there are none, it falls back to fixing the gauge with // three points. config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); const auto summary = CreateDefaultCeresBundleAdjuster(options, config, reconstruction) ->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters, 316); EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 307); } TEST(DefaultBundleAdjuster, IgnorePoint) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 2; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 1; SynthesizeNoise(synthetic_noise_options, &reconstruction); const Reconstruction orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(1); config.AddImage(2); config.IgnorePoint(42); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; std::unique_ptr bundle_adjuster = CreateDefaultCeresBundleAdjuster(options, config, reconstruction); const auto summary = bundle_adjuster->Solve(); ASSERT_NE(summary->termination_type, BundleAdjustmentTerminationType::FAILURE); EXPECT_EQ(config.NumResiduals(reconstruction), GetCeresProblem(*bundle_adjuster).NumResiduals()); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(GetCeresSummary(summary.get()).num_residuals_reduced, 396); // 99 x 3 point parameters // + 5 rig_from_world parameters (pose of second image) // + 2 x 2 camera parameters EXPECT_EQ(GetCeresSummary(summary.get()).num_effective_parameters_reduced, 306); } TEST(PosePriorBundleAdjuster, AlignmentRobustToOutliers) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 7; synthetic_options.num_points3D = 50; synthetic_options.prior_position = true; const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); SynthesizeDataset(synthetic_options, >_reconstruction, database.get()); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point3D_stddev = 0.2; synthetic_noise_options.rig_from_world_rotation_stddev = 1.0; synthetic_noise_options.rig_from_world_translation_stddev = 0.2; synthetic_noise_options.prior_position_stddev = 0.05; SynthesizeNoise(synthetic_noise_options, &reconstruction); std::vector pose_priors = database->ReadAllPosePriors(); // Add 2 outlier priors with very large covariance pose_priors.at(0).position += Eigen::Vector3d::Constant(10); pose_priors.at(0).position_covariance = Eigen::Matrix3d::Identity() * 1e6; pose_priors.at(1).position += Eigen::Vector3d::Constant(1); pose_priors.at(1).position_covariance = Eigen::Matrix3d::Identity() * 1e2; PosePriorBundleAdjustmentOptions prior_ba_options; prior_ba_options.alignment_ransac_options.random_seed = 0; prior_ba_options.alignment_ransac_options.max_error = 0.0; BundleAdjustmentOptions ba_options; BundleAdjustmentConfig ba_config; for (const frame_t frame_id : reconstruction.RegFrameIds()) { const Frame& frame = reconstruction.Frame(frame_id); for (const data_t& data_id : frame.ImageIds()) { ba_config.AddImage(data_id.id); } } auto adjuster = CreatePosePriorBundleAdjuster( ba_options, prior_ba_options, ba_config, pose_priors, reconstruction); auto summary = adjuster->Solve(); ASSERT_TRUE(summary->IsSolutionUsable()); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(PosePriorBundleAdjuster, MissingPositionCov) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 7; synthetic_options.num_points3D = 100; synthetic_options.prior_position = true; const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); SynthesizeDataset(synthetic_options, >_reconstruction, database.get()); Reconstruction reconstruction = gt_reconstruction; std::vector pose_priors = database->ReadAllPosePriors(); for (PosePrior& pose_prior : pose_priors) { EXPECT_FALSE(pose_prior.HasPositionCov()); } PosePriorBundleAdjustmentOptions prior_ba_options; prior_ba_options.alignment_ransac_options.random_seed = 0; prior_ba_options.ceres->prior_position_loss_function_type = CeresBundleAdjustmentOptions::LossFunctionType::CAUCHY; BundleAdjustmentOptions ba_options; BundleAdjustmentConfig ba_config; for (const frame_t frame_id : reconstruction.RegFrameIds()) { const Frame& frame = reconstruction.Frame(frame_id); for (const data_t& data_id : frame.ImageIds()) { ba_config.AddImage(data_id.id); } } auto adjuster = CreatePosePriorBundleAdjuster( ba_options, prior_ba_options, ba_config, pose_priors, reconstruction); auto summary = adjuster->Solve(); ASSERT_TRUE(summary->IsSolutionUsable()); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } TEST(PosePriorBundleAdjuster, OptimizationRobustToOutliers) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_options; synthetic_options.num_rigs = 1; synthetic_options.num_cameras_per_rig = 1; synthetic_options.num_frames_per_rig = 7; synthetic_options.num_points3D = 100; synthetic_options.prior_position = true; const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); SynthesizeDataset(synthetic_options, >_reconstruction, database.get()); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point3D_stddev = 0.2; synthetic_noise_options.rig_from_world_rotation_stddev = 1.0; synthetic_noise_options.rig_from_world_translation_stddev = 0.2; synthetic_noise_options.prior_position_stddev = 0.05; SynthesizeNoise(synthetic_noise_options, &reconstruction); std::vector pose_priors = database->ReadAllPosePriors(); // Add 2 confident but wrong priors. pose_priors[0].position_covariance = Eigen::Matrix3d::Identity() * 0.01; pose_priors[0].position += Eigen::Vector3d::Constant(10); pose_priors[1].position_covariance = Eigen::Matrix3d::Identity() * 1.01; pose_priors[1].position += Eigen::Vector3d::Constant(10); PosePriorBundleAdjustmentOptions prior_ba_options; prior_ba_options.alignment_ransac_options.random_seed = 0; prior_ba_options.ceres->prior_position_loss_function_type = CeresBundleAdjustmentOptions::LossFunctionType::CAUCHY; BundleAdjustmentOptions ba_options; BundleAdjustmentConfig ba_config; for (const frame_t frame_id : reconstruction.RegFrameIds()) { const Frame& frame = reconstruction.Frame(frame_id); for (const data_t& data_id : frame.ImageIds()) { ba_config.AddImage(data_id.id); } } auto adjuster = CreatePosePriorBundleAdjuster( ba_options, prior_ba_options, ba_config, pose_priors, reconstruction); auto summary = adjuster->Solve(); ASSERT_TRUE(summary->IsSolutionUsable()); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/bundle_adjustment_test.cc000066400000000000000000000364021517363634500243250ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/bundle_adjustment_ceres.h" #include "colmap/scene/database.h" #include "colmap/scene/reconstruction_matchers.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { TEST(BundleAdjustmentOptions, Copy) { BundleAdjustmentOptions options; options.refine_focal_length = false; options.refine_principal_point = true; options.min_track_length = 5; options.ceres->solver_options.max_num_iterations = 42; BundleAdjustmentOptions copy = options; // Verify fields are copied EXPECT_EQ(copy.refine_focal_length, false); EXPECT_EQ(copy.refine_principal_point, true); EXPECT_EQ(copy.min_track_length, 5); EXPECT_EQ(copy.ceres->solver_options.max_num_iterations, 42); // Verify deep copy of shared_ptr (different pointer instances) EXPECT_NE(options.ceres.get(), copy.ceres.get()); } TEST(PosePriorBundleAdjustmentOptions, Copy) { PosePriorBundleAdjustmentOptions options; options.prior_position_fallback_stddev = 2.5; options.alignment_ransac_options.max_error = 1.0; options.ceres->prior_position_loss_scale = 0.42; PosePriorBundleAdjustmentOptions copy = options; // Verify fields are copied EXPECT_EQ(copy.prior_position_fallback_stddev, 2.5); EXPECT_EQ(copy.alignment_ransac_options.max_error, 1.0); EXPECT_EQ(copy.ceres->prior_position_loss_scale, 0.42); // Verify deep copy of shared_ptr (different pointer instances) EXPECT_NE(options.ceres.get(), copy.ceres.get()); } TEST(BundleAdjustmentSummary, IsSolutionUsable) { BundleAdjustmentSummary summary; summary.termination_type = BundleAdjustmentTerminationType::CONVERGENCE; EXPECT_TRUE(summary.IsSolutionUsable()); summary.termination_type = BundleAdjustmentTerminationType::NO_CONVERGENCE; EXPECT_TRUE(summary.IsSolutionUsable()); summary.termination_type = BundleAdjustmentTerminationType::USER_SUCCESS; EXPECT_TRUE(summary.IsSolutionUsable()); summary.termination_type = BundleAdjustmentTerminationType::FAILURE; EXPECT_FALSE(summary.IsSolutionUsable()); summary.termination_type = BundleAdjustmentTerminationType::USER_FAILURE; EXPECT_FALSE(summary.IsSolutionUsable()); } TEST(BundleAdjustmentConfig, NumResiduals) { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 4; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 1; synthetic_dataset_options.num_points3D = 100; SynthesizeDataset(synthetic_dataset_options, &reconstruction); const std::vector image_ids = reconstruction.RegImageIds(); CHECK_EQ(image_ids.size(), 4); BundleAdjustmentConfig config; config.AddImage(image_ids[0]); config.AddImage(image_ids[1]); EXPECT_EQ(config.NumResiduals(reconstruction), 400); config.AddVariablePoint(1); EXPECT_EQ(config.NumResiduals(reconstruction), 404); config.AddConstantPoint(2); EXPECT_EQ(config.NumResiduals(reconstruction), 408); config.AddImage(image_ids[2]); EXPECT_EQ(config.NumResiduals(reconstruction), 604); config.AddImage(image_ids[3]); EXPECT_EQ(config.NumResiduals(reconstruction), 800); config.IgnorePoint(3); EXPECT_EQ(config.NumResiduals(reconstruction), 792); } TEST(BundleAdjustmentConfig, AddRemoveImage) { BundleAdjustmentConfig config; EXPECT_EQ(config.NumImages(), 0); config.AddImage(1); config.AddImage(2); config.AddImage(3); EXPECT_EQ(config.NumImages(), 3); EXPECT_TRUE(config.HasImage(1)); EXPECT_TRUE(config.HasImage(2)); EXPECT_TRUE(config.HasImage(3)); EXPECT_FALSE(config.HasImage(4)); config.RemoveImage(2); EXPECT_EQ(config.NumImages(), 2); EXPECT_TRUE(config.HasImage(1)); EXPECT_FALSE(config.HasImage(2)); EXPECT_TRUE(config.HasImage(3)); // Removing non-existent image is a no-op config.RemoveImage(99); EXPECT_EQ(config.NumImages(), 2); } TEST(BundleAdjustmentConfig, ConstantVariableCamIntrinsics) { BundleAdjustmentConfig config; EXPECT_EQ(config.NumConstantCamIntrinsics(), 0); config.SetConstantCamIntrinsics(1); config.SetConstantCamIntrinsics(2); EXPECT_EQ(config.NumConstantCamIntrinsics(), 2); EXPECT_TRUE(config.HasConstantCamIntrinsics(1)); EXPECT_TRUE(config.HasConstantCamIntrinsics(2)); EXPECT_FALSE(config.HasConstantCamIntrinsics(3)); config.SetVariableCamIntrinsics(1); EXPECT_EQ(config.NumConstantCamIntrinsics(), 1); EXPECT_FALSE(config.HasConstantCamIntrinsics(1)); EXPECT_TRUE(config.HasConstantCamIntrinsics(2)); const auto& constant_cams = config.ConstantCamIntrinsics(); EXPECT_EQ(constant_cams.size(), 1); EXPECT_EQ(constant_cams.count(2), 1); } TEST(BundleAdjustmentConfig, ConstantVariableSensorFromRigPose) { BundleAdjustmentConfig config; EXPECT_EQ(config.NumConstantSensorFromRigPoses(), 0); sensor_t sensor1(SensorType::CAMERA, 1); sensor_t sensor2(SensorType::CAMERA, 2); config.SetConstantSensorFromRigPose(sensor1); config.SetConstantSensorFromRigPose(sensor2); EXPECT_EQ(config.NumConstantSensorFromRigPoses(), 2); EXPECT_TRUE(config.HasConstantSensorFromRigPose(sensor1)); EXPECT_TRUE(config.HasConstantSensorFromRigPose(sensor2)); config.SetVariableSensorFromRigPose(sensor1); EXPECT_EQ(config.NumConstantSensorFromRigPoses(), 1); EXPECT_FALSE(config.HasConstantSensorFromRigPose(sensor1)); EXPECT_TRUE(config.HasConstantSensorFromRigPose(sensor2)); const auto& constant_poses = config.ConstantSensorFromRigPoses(); EXPECT_EQ(constant_poses.size(), 1); EXPECT_EQ(constant_poses.count(sensor2), 1); } TEST(BundleAdjustmentConfig, ConstantVariableRigFromWorldPose) { BundleAdjustmentConfig config; EXPECT_EQ(config.NumConstantRigFromWorldPoses(), 0); config.SetConstantRigFromWorldPose(1); config.SetConstantRigFromWorldPose(2); EXPECT_EQ(config.NumConstantRigFromWorldPoses(), 2); EXPECT_TRUE(config.HasConstantRigFromWorldPose(1)); EXPECT_TRUE(config.HasConstantRigFromWorldPose(2)); config.SetVariableRigFromWorldPose(1); EXPECT_EQ(config.NumConstantRigFromWorldPoses(), 1); EXPECT_FALSE(config.HasConstantRigFromWorldPose(1)); EXPECT_TRUE(config.HasConstantRigFromWorldPose(2)); const auto& constant_rig_poses = config.ConstantRigFromWorldPoses(); EXPECT_EQ(constant_rig_poses.size(), 1); EXPECT_EQ(constant_rig_poses.count(2), 1); } TEST(BundleAdjustmentConfig, ConstantVariablePoints) { BundleAdjustmentConfig config; EXPECT_EQ(config.NumPoints(), 0); EXPECT_EQ(config.NumVariablePoints(), 0); EXPECT_EQ(config.NumConstantPoints(), 0); config.AddVariablePoint(1); config.AddVariablePoint(2); EXPECT_EQ(config.NumPoints(), 2); EXPECT_EQ(config.NumVariablePoints(), 2); EXPECT_EQ(config.NumConstantPoints(), 0); EXPECT_TRUE(config.HasPoint(1)); EXPECT_TRUE(config.HasVariablePoint(1)); EXPECT_FALSE(config.HasConstantPoint(1)); config.AddConstantPoint(3); EXPECT_EQ(config.NumPoints(), 3); EXPECT_EQ(config.NumVariablePoints(), 2); EXPECT_EQ(config.NumConstantPoints(), 1); EXPECT_TRUE(config.HasPoint(3)); EXPECT_FALSE(config.HasVariablePoint(3)); EXPECT_TRUE(config.HasConstantPoint(3)); config.RemoveVariablePoint(1); EXPECT_EQ(config.NumVariablePoints(), 1); EXPECT_FALSE(config.HasPoint(1)); config.RemoveConstantPoint(3); EXPECT_EQ(config.NumConstantPoints(), 0); EXPECT_FALSE(config.HasPoint(3)); const auto& var_points = config.VariablePoints(); EXPECT_EQ(var_points.size(), 1); EXPECT_EQ(var_points.count(2), 1); const auto& const_points = config.ConstantPoints(); EXPECT_TRUE(const_points.empty()); } TEST(BundleAdjustmentConfig, IgnoredPoints) { BundleAdjustmentConfig config; EXPECT_FALSE(config.IsIgnoredPoint(1)); config.IgnorePoint(1); EXPECT_TRUE(config.IsIgnoredPoint(1)); EXPECT_FALSE(config.IsIgnoredPoint(2)); } TEST(BundleAdjustmentConfig, FixGauge) { BundleAdjustmentConfig config; EXPECT_EQ(config.FixedGauge(), BundleAdjustmentGauge::UNSPECIFIED); config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); EXPECT_EQ(config.FixedGauge(), BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); config.FixGauge(BundleAdjustmentGauge::THREE_POINTS); EXPECT_EQ(config.FixedGauge(), BundleAdjustmentGauge::THREE_POINTS); } TEST(BundleAdjustmentConfig, Images) { BundleAdjustmentConfig config; config.AddImage(5); config.AddImage(10); const auto& images = config.Images(); EXPECT_EQ(images.size(), 2); EXPECT_EQ(images.count(5), 1); EXPECT_EQ(images.count(10), 1); } TEST(BundleAdjustmentSummary, BriefReport) { BundleAdjustmentSummary summary; summary.termination_type = BundleAdjustmentTerminationType::CONVERGENCE; summary.num_residuals = 42; const std::string report = summary.BriefReport(); EXPECT_NE(report.find("CONVERGENCE"), std::string::npos); EXPECT_NE(report.find("42"), std::string::npos); } // Parameterized test for generic BundleAdjuster interface across backends. class BundleAdjusterBackendTest : public ::testing::TestWithParam {}; TEST_P(BundleAdjusterBackendTest, Nominal) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 10; synthetic_dataset_options.num_points3D = 200; SynthesizeDataset(synthetic_dataset_options, >_reconstruction); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.point3D_stddev = 0.1; synthetic_noise_options.rig_from_world_rotation_stddev = 0.5; synthetic_noise_options.rig_from_world_translation_stddev = 0.1; SynthesizeNoise(synthetic_noise_options, &reconstruction); BundleAdjustmentConfig config; for (const image_t image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } config.FixGauge(BundleAdjustmentGauge::TWO_CAMS_FROM_WORLD); BundleAdjustmentOptions options; options.backend = GetParam(); std::unique_ptr bundle_adjuster = CreateDefaultBundleAdjuster(options, config, reconstruction); // Test abstract interface accessors EXPECT_EQ(bundle_adjuster->Options().backend, GetParam()); EXPECT_EQ(bundle_adjuster->Config().NumImages(), 10); // Solve and verify through abstract interface const auto summary = bundle_adjuster->Solve(); EXPECT_TRUE(summary->IsSolutionUsable()); EXPECT_GT(summary->num_residuals, 0); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.0)); } INSTANTIATE_TEST_SUITE_P(BundleAdjusterBackends, BundleAdjusterBackendTest, ::testing::Values(BundleAdjustmentBackend::CERES)); // Parameterized test for generic PosePriorBundleAdjuster interface across // backends. class PosePriorBundleAdjusterBackendTest : public ::testing::TestWithParam {}; TEST_P(PosePriorBundleAdjusterBackendTest, Nominal) { SetPRNGSeed(0); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_rigs = 1; synthetic_dataset_options.num_cameras_per_rig = 1; synthetic_dataset_options.num_frames_per_rig = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.prior_position = true; const auto database_path = CreateTestDir() / "database.db"; auto database = Database::Open(database_path); SynthesizeDataset( synthetic_dataset_options, >_reconstruction, database.get()); Reconstruction reconstruction = gt_reconstruction; SyntheticNoiseOptions synthetic_noise_options; synthetic_noise_options.point2D_stddev = 0.5; synthetic_noise_options.point3D_stddev = 0.1; synthetic_noise_options.rig_from_world_rotation_stddev = 0.5; synthetic_noise_options.rig_from_world_translation_stddev = 0.1; synthetic_noise_options.prior_position_stddev = 0.05; SynthesizeNoise(synthetic_noise_options, &reconstruction); std::vector pose_priors = database->ReadAllPosePriors(); BundleAdjustmentConfig config; for (const frame_t frame_id : reconstruction.RegFrameIds()) { const Frame& frame = reconstruction.Frame(frame_id); for (const data_t& data_id : frame.ImageIds()) { config.AddImage(data_id.id); } } BundleAdjustmentOptions options; options.backend = GetParam(); PosePriorBundleAdjustmentOptions prior_options; prior_options.alignment_ransac_options.random_seed = 0; std::unique_ptr bundle_adjuster = CreatePosePriorBundleAdjuster( options, prior_options, config, pose_priors, reconstruction); // Test abstract interface accessors EXPECT_EQ(bundle_adjuster->Options().backend, GetParam()); EXPECT_EQ(bundle_adjuster->Config().NumImages(), 7); // Solve and verify through abstract interface const auto summary = bundle_adjuster->Solve(); EXPECT_TRUE(summary->IsSolutionUsable()); EXPECT_GT(summary->num_residuals, 0); EXPECT_THAT(gt_reconstruction, ReconstructionNear(reconstruction, /*max_rotation_error_deg=*/0.1, /*max_proj_center_error=*/0.1, /*max_scale_error=*/std::nullopt, /*num_obs_tolerance=*/0.02)); } INSTANTIATE_TEST_SUITE_P(PosePriorBundleAdjusterBackends, PosePriorBundleAdjusterBackendTest, ::testing::Values(BundleAdjustmentBackend::CERES)); } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/coordinate_frame.cc000066400000000000000000000344531517363634500230640ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/coordinate_frame.h" #include "colmap/geometry/gps.h" #include "colmap/geometry/pose.h" #include "colmap/image/line.h" #include "colmap/image/undistortion.h" #include "colmap/math/math.h" #include "colmap/optim/ransac.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" namespace colmap { namespace { Eigen::Vector3d FindBestConsensusAxis(const std::vector& axes, const double max_distance) { if (axes.empty()) { return Eigen::Vector3d::Zero(); } std::vector inlier_idxs; inlier_idxs.reserve(axes.size()); std::vector best_inlier_idxs; best_inlier_idxs.reserve(axes.size()); double best_inlier_distance_sum = std::numeric_limits::max(); for (size_t i = 0; i < axes.size(); ++i) { const Eigen::Vector3d& ref_axis = axes[i]; double inlier_distance_sum = 0; inlier_idxs.clear(); for (size_t j = 0; j < axes.size(); ++j) { if (i == j) { inlier_idxs.push_back(j); } else { const double distance = 1 - ref_axis.dot(axes[j]); if (distance <= max_distance) { inlier_distance_sum += distance; inlier_idxs.push_back(j); } } } if (inlier_idxs.size() > best_inlier_idxs.size() || (inlier_idxs.size() == best_inlier_idxs.size() && inlier_distance_sum < best_inlier_distance_sum)) { best_inlier_distance_sum = inlier_distance_sum; best_inlier_idxs = inlier_idxs; } } if (best_inlier_idxs.empty()) { return Eigen::Vector3d::Zero(); } Eigen::Vector3d best_axis(0, 0, 0); for (const auto idx : best_inlier_idxs) { best_axis += axes[idx]; } best_axis /= best_inlier_idxs.size(); return best_axis; } } // namespace Eigen::Vector3d EstimateGravityVectorFromImageOrientation( const Reconstruction& reconstruction, const double max_axis_distance) { std::vector downward_axes; downward_axes.reserve(reconstruction.NumRegImages()); for (const auto image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); downward_axes.push_back( image.CamFromWorld().rotation().toRotationMatrix().row(1)); } return FindBestConsensusAxis(downward_axes, max_axis_distance); } #ifdef COLMAP_LSD_ENABLED struct VanishingPointEstimator { // The line segments. using X_t = LineSegment; // The line representation of the segments. using Y_t = Eigen::Vector3d; // The vanishing point. using M_t = Eigen::Vector3d; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 2; // Estimate the vanishing point from at least two line segments. static void Estimate(const std::vector& line_segments, const std::vector& lines, std::vector* models) { THROW_CHECK_EQ(line_segments.size(), 2); THROW_CHECK_EQ(lines.size(), 2); THROW_CHECK(models != nullptr); models->resize(1); (*models)[0] = lines[0].cross(lines[1]); } // Calculate the squared distance of each line segment's end point to the line // connecting the vanishing point and the midpoint of the line segment. static void Residuals(const std::vector& line_segments, const std::vector& lines, const M_t& vanishing_point, std::vector* residuals) { residuals->resize(line_segments.size()); // Check if vanishing point is at infinity. if (vanishing_point[2] == 0) { std::fill(residuals->begin(), residuals->end(), std::numeric_limits::max()); return; } for (size_t i = 0; i < lines.size(); ++i) { const Eigen::Vector3d midpoint = (0.5 * (line_segments[i].start + line_segments[i].end)).homogeneous(); const Eigen::Vector3d connecting_line = midpoint.cross(vanishing_point); const double signed_distance = connecting_line.dot(line_segments[i].end.homogeneous()) / connecting_line.head<2>().norm(); (*residuals)[i] = signed_distance * signed_distance; } } }; Eigen::Matrix3d EstimateManhattanWorldFrame( const ManhattanWorldFrameEstimationOptions& options, const Reconstruction& reconstruction, const std::filesystem::path& image_path) { std::vector rightward_axes; std::vector downward_axes; size_t image_idx = 0; for (const image_t image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); const auto& camera = *image.CameraPtr(); LOG_HEADING1(StringPrintf("Processing image %s (%d / %d)", image.Name().c_str(), ++image_idx, reconstruction.NumRegImages())); LOG(INFO) << "Reading image..."; Bitmap bitmap; THROW_CHECK(bitmap.Read(image_path / image.Name())); LOG(INFO) << "Undistorting image..."; UndistortCameraOptions undistortion_options; undistortion_options.max_image_size = options.max_image_size; Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(undistortion_options, bitmap, camera, &undistorted_bitmap, &undistorted_camera); LOG(INFO) << "Detecting lines..."; const std::vector line_segments = DetectLineSegments(undistorted_bitmap, options.min_line_length); const std::vector line_orientations = ClassifyLineSegmentOrientations(line_segments, options.line_orientation_tolerance); LOG(INFO) << StringPrintf(" %d", line_segments.size()); std::vector horizontal_line_segments; std::vector vertical_line_segments; std::vector horizontal_lines; std::vector vertical_lines; for (size_t i = 0; i < line_segments.size(); ++i) { const auto& line_segment = line_segments[i]; const Eigen::Vector3d line_segment_start = line_segment.start.homogeneous(); const Eigen::Vector3d line_segment_end = line_segment.end.homogeneous(); const Eigen::Vector3d line = line_segment_start.cross(line_segment_end); if (line_orientations[i] == LineSegmentOrientation::HORIZONTAL) { horizontal_line_segments.push_back(line_segment); horizontal_lines.push_back(line); } else if (line_orientations[i] == LineSegmentOrientation::VERTICAL) { vertical_line_segments.push_back(line_segment); vertical_lines.push_back(line); } } LOG(INFO) << StringPrintf(" (%d horizontal, %d vertical)", horizontal_lines.size(), vertical_lines.size()); LOG(INFO) << "Estimating vanishing points..."; RANSACOptions ransac_options; ransac_options.max_error = options.max_line_vp_distance; RANSAC ransac(ransac_options); const auto horizontal_report = ransac.Estimate(horizontal_line_segments, horizontal_lines); const auto vertical_report = ransac.Estimate(vertical_line_segments, vertical_lines); LOG(INFO) << StringPrintf(" (%d horizontal inliers, %d vertical inliers)", horizontal_report.support.num_inliers, vertical_report.support.num_inliers); LOG(INFO) << "Composing coordinate axes..."; const Eigen::Matrix3d inv_calib_matrix = undistorted_camera.CalibrationMatrix().inverse(); const Eigen::Quaterniond world_from_cam_rotation = image.CamFromWorld().rotation().inverse(); if (horizontal_report.success) { Eigen::Vector3d horizontal_axis_in_world = world_from_cam_rotation * (inv_calib_matrix * horizontal_report.model).normalized(); // Make sure all axes point into the same direction. if (rightward_axes.size() > 0 && rightward_axes[0].dot(horizontal_axis_in_world) < 0) { horizontal_axis_in_world = -horizontal_axis_in_world; } rightward_axes.push_back(horizontal_axis_in_world); LOG(INFO) << "Horizontal: " << horizontal_axis_in_world.transpose(); } if (vertical_report.success) { const Eigen::Vector3d vertical_axis_in_cam = (inv_calib_matrix * vertical_report.model).normalized(); Eigen::Vector3d vertical_axis_in_world = (world_from_cam_rotation * vertical_axis_in_cam).normalized(); // Make sure axis points downwards in the image, assuming that the image // was taken in upright orientation. if (vertical_axis_in_world.dot(Eigen::Vector3d(0, 1, 0)) < 0) { vertical_axis_in_world = -vertical_axis_in_world; } downward_axes.push_back(vertical_axis_in_world); LOG(INFO) << "Vertical: " << vertical_axis_in_world.transpose(); } } LOG_HEADING1("Computing coordinate frame"); Eigen::Matrix3d frame = Eigen::Matrix3d::Zero(); if (rightward_axes.size() > 0) { frame.col(0) = FindBestConsensusAxis(rightward_axes, options.max_axis_distance); } LOG(INFO) << "Found rightward axis: " << frame.col(0).transpose(); if (downward_axes.size() > 0) { frame.col(1) = FindBestConsensusAxis(downward_axes, options.max_axis_distance); } LOG(INFO) << "Found downward axis: " << frame.col(1).transpose(); if (rightward_axes.size() > 0 && downward_axes.size() > 0) { frame.col(2) = frame.col(0).cross(frame.col(1)); Eigen::JacobiSVD svd( frame, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix3d orthonormal_frame = svd.matrixU() * Eigen::Matrix3d::Identity() * svd.matrixV().transpose(); frame = orthonormal_frame; } LOG(INFO) << "Found orthonormal frame:\n" << frame; return frame; } #endif void AlignToPrincipalPlane(Reconstruction* reconstruction, Sim3d* aligned_from_original) { THROW_CHECK_GT(reconstruction->NumRegFrames(), 0); // Perform SVD on the 3D points to estimate the ground plane basis const Eigen::Vector3d centroid = reconstruction->ComputeCentroid(0.0, 1.0); Eigen::MatrixXd normalized_points3D(3, reconstruction->NumPoints3D()); int pidx = 0; for (const auto& point : reconstruction->Points3D()) { normalized_points3D.col(pidx++) = point.second.xyz - centroid; } #if EIGEN_VERSION_AT_LEAST(5, 0, 0) const Eigen::Matrix3d basis = normalized_points3D.jacobiSvd() .matrixU(); #else const Eigen::Matrix3d basis = normalized_points3D.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV) .matrixU(); #endif Eigen::Matrix3d rot_mat; rot_mat << basis.col(0), basis.col(1), basis.col(0).cross(basis.col(1)); rot_mat.transposeInPlace(); *aligned_from_original = Sim3d(1.0, Eigen::Quaterniond(rot_mat), -rot_mat * centroid); // If camera plane ends up below ground then flip basis vectors. const Frame& frame0 = reconstruction->Frame(reconstruction->RegFrameIds().front()); const auto frame0_image_ids = frame0.ImageIds(); THROW_CHECK(frame0_image_ids.begin() != frame0_image_ids.end()); const Rigid3d cam0_from_aligned_world = TransformCameraWorld( *aligned_from_original, reconstruction->Image(frame0_image_ids.begin()->id).CamFromWorld()); if (Inverse(cam0_from_aligned_world).translation().z() < 0.0) { rot_mat << basis.col(0), -basis.col(1), basis.col(0).cross(-basis.col(1)); rot_mat.transposeInPlace(); *aligned_from_original = Sim3d(1.0, Eigen::Quaterniond(rot_mat), -rot_mat * centroid); } reconstruction->Transform(*aligned_from_original); } void AlignToENUPlane(Reconstruction* reconstruction, Sim3d* aligned_from_original, bool unscaled) { const Eigen::Vector3d centroid = reconstruction->ComputeCentroid(0.0, 1.0); GPSTransform gps_tform; const Eigen::Vector3d ell_centroid = gps_tform.ECEFToEllipsoid({centroid}).at(0); // Create rotation matrix from ECEF to ENU coordinates const double sin_lat = std::sin(DegToRad(ell_centroid(0))); const double sin_lon = std::sin(DegToRad(ell_centroid(1))); const double cos_lat = std::cos(DegToRad(ell_centroid(0))); const double cos_lon = std::cos(DegToRad(ell_centroid(1))); // Create ECEF to ENU rotation matrix Eigen::Matrix3d rot_mat; rot_mat << -sin_lon, cos_lon, 0, -cos_lon * sin_lat, -sin_lon * sin_lat, cos_lat, cos_lon * cos_lat, sin_lon * cos_lat, sin_lat; const double scale = unscaled ? 1.0 / aligned_from_original->scale() : 1.0; *aligned_from_original = Sim3d(scale, Eigen::Quaterniond(rot_mat), -scale * rot_mat * centroid); reconstruction->Transform(*aligned_from_original); } } // namespace colmap colmap-4.0.4/src/colmap/estimators/coordinate_frame.h000066400000000000000000000100531517363634500227140ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/scene/reconstruction.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { struct ManhattanWorldFrameEstimationOptions { // The maximum image size for line detection. int max_image_size = 1024; // The minimum length of line segments in pixels. double min_line_length = 3; // The tolerance for classifying lines into horizontal/vertical. double line_orientation_tolerance = 0.2; // The maximum distance in pixels between lines and the vanishing points. double max_line_vp_distance = 0.5; // The maximum cosine distance between estimated axes to be inliers. double max_axis_distance = 0.05; }; // Estimate gravity vector by assuming gravity-aligned image orientation, i.e. // the majority of images is assumed to have the gravity vector aligned with an // upright image plane. Eigen::Vector3d EstimateGravityVectorFromImageOrientation( const Reconstruction& reconstruction, double max_axis_distance = 0.05); // Estimate the coordinate frame of the reconstruction assuming a Manhattan // world by finding the major vanishing points in each image. This function // assumes that the majority of images is taken in upright direction, i.e. // people are standing upright in the image. The orthonormal axes of the // estimated coordinate frame will be given in the columns of the returned // matrix. If one axis could not be determined, the respective column will be // zero. The axes are specified in the world coordinate system in the order // rightward, downward, forward. #ifdef COLMAP_LSD_ENABLED Eigen::Matrix3d EstimateManhattanWorldFrame( const ManhattanWorldFrameEstimationOptions& options, const Reconstruction& reconstruction, const std::filesystem::path& image_path); #endif // Aligns the reconstruction to the plane defined by running PCA on the 3D // points. The model centroid is at the origin of the new coordinate system // and the X axis is the first principal component with the Y axis being the // second principal component void AlignToPrincipalPlane(Reconstruction* recon, Sim3d* tform); // Aligns the reconstruction to the local ENU plane orientation. Rotates the // reconstruction such that the x-y plane aligns with the ENU tangent plane at // the point cloud centroid and translates the origin to the centroid. // If unscaled == true, then the original scale of the model remains unchanged. void AlignToENUPlane(Reconstruction* recon, Sim3d* tform, bool unscaled); } // namespace colmap colmap-4.0.4/src/colmap/estimators/coordinate_frame_test.cc000066400000000000000000000420611517363634500241150ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/coordinate_frame.h" #include "colmap/geometry/gps.h" #include "colmap/math/math.h" #include "colmap/math/random.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/eigen_matchers.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { TEST(EstimateGravityVectorFromImageOrientation, Nominal) { // Create a reconstruction with multiple upright images. // Upright images have gravity aligned with the camera Y axis, so // row(1) of the rotation matrix should point downward (along +Y world). Reconstruction reconstruction; Camera camera = Camera::CreateFromModelId(1, CameraModelId::kSimplePinhole, 1, 1, 1); reconstruction.AddCameraWithTrivialRig(camera); // Add 5 registered images with random rotation around +Y (upright, gravity // ~ +Y) for (int i = 0; i < 5; ++i) { Image image; image.SetImageId(i); image.SetCameraId(camera.camera_id); const Rigid3d cam_from_world( Eigen::Quaterniond( Eigen::AngleAxisd(RandomUniformReal(-EIGEN_PI, EIGEN_PI), Eigen::Vector3d::UnitY())), Eigen::Vector3d(i, 0, 0)); reconstruction.AddImageWithTrivialFrame(std::move(image), cam_from_world); } // 1 outlier image rotated 90 degrees around Z { Image image; image.SetImageId(6); image.SetCameraId(camera.camera_id); const Rigid3d cam_from_world(Eigen::Quaterniond(Eigen::AngleAxisd( EIGEN_PI / 2, Eigen::Vector3d::UnitZ())), Eigen::Vector3d(6, 0, 0)); reconstruction.AddImageWithTrivialFrame(std::move(image), cam_from_world); } const Eigen::Vector3d gravity = EstimateGravityVectorFromImageOrientation(reconstruction); // Consensus should find gravity ~ (0, 1, 0) despite the outlier EXPECT_NEAR(gravity.norm(), 1.0, 1e-6); EXPECT_NEAR(std::abs(gravity.dot(Eigen::Vector3d(0, 1, 0))), 1.0, 1e-6); } TEST(EstimateGravityVectorFromImageOrientation, Empty) { Reconstruction reconstruction; EXPECT_EQ(EstimateGravityVectorFromImageOrientation(reconstruction), Eigen::Vector3d::Zero()); } #ifdef COLMAP_LSD_ENABLED constexpr int kWidth = 512; constexpr int kHeight = 512; constexpr double kFocal = 400.0; struct Line3D { Eigen::Vector3d beg; Eigen::Vector3d end; }; struct ManhattanScene { Eigen::Matrix3d manhattan_from_world; std::vector vertical_lines; std::vector horizontal_lines; }; ManhattanScene CreateManhattanScene() { // Rotate the Manhattan frame so the solution is not trivially axis-aligned. const Eigen::Matrix3d manhattan_from_world = (Eigen::AngleAxisd(DegToRad(25.0), Eigen::Vector3d::UnitZ()) * Eigen::AngleAxisd(DegToRad(11.0), Eigen::Vector3d::UnitX())) .toRotationMatrix(); // Define axis-aligned 3D lines, then rotate them into the tilted frame. // Vertical lines are parallel to Y; horizontal lines to X. std::vector vertical_lines = { {{-1.5, -2, 5}, {-1.5, 2, 5}}, {{-0.5, -2, 5}, {-0.5, 2, 5}}, {{0.5, -2, 5}, {0.5, 2, 5}}, {{1.5, -2, 5}, {1.5, 2, 5}}, {{-1, -2, 7}, {-1, 2, 7}}, {{0, -2, 7}, {0, 2, 7}}, {{1, -2, 7}, {1, 2, 7}}, }; std::vector horizontal_lines = { {{-2, -1.5, 5}, {2, -1.5, 5}}, {{-2, -0.5, 5}, {2, -0.5, 5}}, {{-2, 0.5, 5}, {2, 0.5, 5}}, {{-2, 1.5, 5}, {2, 1.5, 5}}, {{-2, -1, 7}, {2, -1, 7}}, {{-2, 0, 7}, {2, 0, 7}}, {{-2, 1, 7}, {2, 1, 7}}, }; for (auto& l : vertical_lines) { l.beg = manhattan_from_world * l.beg; l.end = manhattan_from_world * l.end; } for (auto& l : horizontal_lines) { l.beg = manhattan_from_world * l.beg; l.end = manhattan_from_world * l.end; } return {manhattan_from_world, std::move(vertical_lines), std::move(horizontal_lines)}; } void DrawLine(Bitmap& bitmap, const Eigen::Vector2d& a, const Eigen::Vector2d& b, int radius) { const int steps = std::max(1, static_cast(std::ceil((b - a).norm() * 2))); for (int s = 0; s <= steps; ++s) { const double t = static_cast(s) / steps; const double x = a.x() + t * (b.x() - a.x()); const double y = a.y() + t * (b.y() - a.y()); for (int dy = -radius; dy <= radius; ++dy) { for (int dx = -radius; dx <= radius; ++dx) { if (dx * dx + dy * dy > radius * radius) continue; const int px = static_cast(std::round(x + dx)); const int py = static_cast(std::round(y + dy)); if (px >= 0 && px < kWidth && py >= 0 && py < kHeight) { bitmap.SetPixel(px, py, BitmapColor(255)); } } } } } void RenderLineImages(Reconstruction& reconstruction, const ManhattanScene& scene, const std::filesystem::path& test_dir) { // 3 cameras with combined pitch (around X) and yaw (around Y) so that both // the horizontal and vertical vanishing points are at finite image locations. const std::array pitch_deg = {10.0, 10.0, 8.0}; const std::array yaw_deg = {15.0, -15.0, 5.0}; const Camera camera = Camera::CreateFromModelId( 1, CameraModelId::kSimplePinhole, kFocal, kWidth, kHeight); reconstruction.AddCameraWithTrivialRig(camera); for (size_t cam_idx = 0; cam_idx < pitch_deg.size(); ++cam_idx) { const Eigen::Quaterniond cam_from_world_rot( Eigen::AngleAxisd(DegToRad(pitch_deg[cam_idx]), Eigen::Vector3d::UnitX()) * Eigen::AngleAxisd(DegToRad(yaw_deg[cam_idx]), Eigen::Vector3d::UnitY())); const Rigid3d cam_from_world(cam_from_world_rot, Eigen::Vector3d::Zero()); Image image; image.SetImageId(cam_idx); image.SetCameraId(camera.camera_id); image.SetName("image" + std::to_string(cam_idx) + ".png"); reconstruction.AddImageWithTrivialFrame(std::move(image), cam_from_world); const auto& reg_image = reconstruction.Image(cam_idx); // Project 3D lines and draw thick white stripes on a gray background. Bitmap bitmap(kWidth, kHeight, /*as_rgb=*/true); bitmap.Fill(BitmapColor(128)); constexpr int kRadius = 3; for (const auto& line : scene.vertical_lines) { const auto p1 = reg_image.ProjectPoint(line.beg); const auto p2 = reg_image.ProjectPoint(line.end); if (p1 && p2) DrawLine(bitmap, *p1, *p2, kRadius); } for (const auto& line : scene.horizontal_lines) { const auto p1 = reg_image.ProjectPoint(line.beg); const auto p2 = reg_image.ProjectPoint(line.end); if (p1 && p2) DrawLine(bitmap, *p1, *p2, kRadius); } ASSERT_TRUE(bitmap.Write(test_dir / reg_image.Name())); } } TEST(EstimateManhattanWorldFrame, Synthetic) { SetPRNGSeed(0); const auto scene = CreateManhattanScene(); const auto test_dir = CreateTestDir(); Reconstruction reconstruction; ASSERT_NO_FATAL_FAILURE(RenderLineImages(reconstruction, scene, test_dir)); // Run Manhattan world frame estimation. ManhattanWorldFrameEstimationOptions options; const Eigen::Matrix3d frame = EstimateManhattanWorldFrame(options, reconstruction, test_dir); // The estimated frame should recover the tilted Manhattan axes. const Eigen::Vector3d expected_rightward = scene.manhattan_from_world.col(0); const Eigen::Vector3d expected_downward = scene.manhattan_from_world.col(1); const Eigen::Vector3d expected_forward = scene.manhattan_from_world.col(2); // Rightward direction (col 0) must align with the rotated X axis. // The sign of the rightward and forward axes is ambiguous, so check absolute // dot product. EXPECT_LT(std::abs(std::abs(frame.col(0).dot(expected_rightward)) - 1), 1e-3); // Gravity direction (col 1) must align with the rotated Y axis. // The sign is deterministic (flipped to match +Y in the implementation). EXPECT_LT(std::abs(frame.col(1).dot(expected_downward) - 1), 1e-3); // Forward direction (col 2) must align with the rotated Z axis. EXPECT_LT(std::abs(std::abs(frame.col(2).dot(expected_forward)) - 1), 1e-3); // Verify orthonormality. EXPECT_NEAR(frame.col(0).norm(), 1.0, 1e-6); EXPECT_NEAR(frame.col(1).norm(), 1.0, 1e-6); EXPECT_NEAR(frame.col(2).norm(), 1.0, 1e-6); EXPECT_NEAR(std::abs(frame.col(0).dot(frame.col(1))), 0.0, 1e-6); EXPECT_NEAR(std::abs(frame.col(0).dot(frame.col(2))), 0.0, 1e-6); EXPECT_NEAR(std::abs(frame.col(1).dot(frame.col(2))), 0.0, 1e-6); } TEST(EstimateManhattanWorldFrame, Empty) { Reconstruction reconstruction; std::filesystem::path image_path; EXPECT_EQ( EstimateManhattanWorldFrame( ManhattanWorldFrameEstimationOptions(), reconstruction, image_path), Eigen::Matrix3d::Zero()); } #endif TEST(AlignToPrincipalPlane, Nominal) { // Start with reconstruction containing points on the Y-Z plane and cameras // "above" the plane on the positive X axis. After alignment the points should // be on the X-Y plane and the cameras "above" the plane on the positive Z // axis. Sim3d tform; Reconstruction reconstruction; Camera camera = Camera::CreateFromModelId(1, CameraModelId::kSimplePinhole, 1, 1, 1); reconstruction.AddCamera(camera); Rig rig; rig.SetRigId(1); rig.AddRefSensor(sensor_t(SensorType::CAMERA, 1)); reconstruction.AddRig(rig); Frame frame; frame.SetFrameId(1); frame.SetRigId(rig.RigId()); frame.AddDataId(data_t(camera.SensorId(), 1)); frame.SetRigFromWorld( Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(-1, 0, 0))); reconstruction.AddFrame(frame); // Setup image with projection center at (1, 0, 0) Image image; image.SetCameraId(camera.camera_id); image.SetImageId(1); image.SetFrameId(1); reconstruction.AddImage(image); // Setup 4 points on the Y-Z plane const point3D_t p1 = reconstruction.AddPoint3D(Eigen::Vector3d(0, -1, 0), Track()); const point3D_t p2 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 1, 0), Track()); const point3D_t p3 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, -1), Track()); const point3D_t p4 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 1), Track()); AlignToPrincipalPlane(&reconstruction, &tform); // Note that the final X and Y axes may be inverted after alignment, so we // need to account for both cases when checking for correctness const bool inverted = tform.rotation().y() < 0; // Verify that points lie on the correct locations of the X-Y plane EXPECT_LE((reconstruction.Point3D(p1).xyz - Eigen::Vector3d(inverted ? 1 : -1, 0, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p2).xyz - Eigen::Vector3d(inverted ? -1 : 1, 0, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p3).xyz - Eigen::Vector3d(0, inverted ? 1 : -1, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p4).xyz - Eigen::Vector3d(0, inverted ? -1 : 1, 0)) .norm(), 1e-6); // Verify that projection center is at (0, 0, 1) EXPECT_LE( (reconstruction.Image(1).ProjectionCenter() - Eigen::Vector3d(0, 0, 1)) .norm(), 1e-6); // Verify that transform matrix does shuffling of axes Eigen::Matrix3x4d expected; if (inverted) { expected << 0, -1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0; } else { expected << 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0; } EXPECT_THAT(tform.ToMatrix(), EigenMatrixNear(expected, 1e-6)); } TEST(AlignToENUPlane, Scaled) { // Create reconstruction with 4 points with known LLA coordinates. After the // ENU transform all 4 points should land approximately on the X-Y plane. GPSTransform gps; auto points = gps.EllipsoidToECEF({Eigen::Vector3d(50, 10.1, 100), Eigen::Vector3d(50.1, 10, 100), Eigen::Vector3d(50.1, 10.1, 100), Eigen::Vector3d(50, 10, 100)}); Sim3d tform; Reconstruction reconstruction; std::vector point_ids; for (size_t i = 0; i < points.size(); ++i) { point_ids.push_back(reconstruction.AddPoint3D(points[i], Track())); LOG(INFO) << points[i].transpose(); } AlignToENUPlane(&reconstruction, &tform, false); // Verify final locations of points EXPECT_THAT(reconstruction.Point3D(point_ids[0]).xyz, EigenMatrixNear(Eigen::Vector3d(3584.8433196335045, -5561.5866894473402, -0.0020947810262441635), 1e-6)); EXPECT_THAT(reconstruction.Point3D(point_ids[1]).xyz, EigenMatrixNear(Eigen::Vector3d(-3577.4020366631503, 5561.5866894469982, 0.0020947791635990143), 1e-6)); EXPECT_THAT(reconstruction.Point3D(point_ids[2]).xyz, EigenMatrixNear(Eigen::Vector3d(3577.4020366640707, 5561.5866894467654, 0.0020947791635990143), 1e-6)); EXPECT_THAT(reconstruction.Point3D(point_ids[3]).xyz, EigenMatrixNear(Eigen::Vector3d(-3584.8433196330498, -5561.586689447573, -0.0020947810262441635), 1e-6)); // Verify that straight line distance between points is preserved for (size_t i = 1; i < points.size(); ++i) { const double dist_orig = (points[i] - points[i - 1]).norm(); const double dist_tform = (reconstruction.Point3D(point_ids[i]).xyz - reconstruction.Point3D(point_ids[i - 1]).xyz) .norm(); EXPECT_LE(std::abs(dist_orig - dist_tform), 1e-6); } } TEST(AlignToENUPlane, Unscaled) { // Test unscaled variant: starting from a model with non-unit scale, the // alignment should undo the scale so that original distances are preserved. GPSTransform gps; auto points = gps.EllipsoidToECEF({Eigen::Vector3d(50, 10.1, 100), Eigen::Vector3d(50.1, 10, 100), Eigen::Vector3d(50.1, 10.1, 100), Eigen::Vector3d(50, 10, 100)}); Reconstruction reconstruction; std::vector point3D_ids; point3D_ids.reserve(points.size()); for (size_t i = 0; i < points.size(); ++i) { point3D_ids.push_back(reconstruction.AddPoint3D(points[i], Track())); } // Apply a non-unit scale to simulate a scaled model const double model_scale = 2.0; Sim3d pre_scale( model_scale, Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); reconstruction.Transform(pre_scale); // Align with unscaled=true, passing the pre_scale as the current transform Sim3d tform = pre_scale; AlignToENUPlane(&reconstruction, &tform, /*unscaled=*/true); // The applied transform should have inverse scale to undo pre_scale EXPECT_NEAR(tform.scale(), 1.0 / model_scale, 1e-6); // Original (unscaled) distances between ECEF points should be preserved for (size_t i = 1; i < points.size(); ++i) { const double dist_orig = (points[i] - points[i - 1]).norm(); const double dist_tform = (reconstruction.Point3D(point3D_ids[i]).xyz - reconstruction.Point3D(point3D_ids[i - 1]).xyz) .norm(); EXPECT_NEAR(dist_orig, dist_tform, 1e-4); } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/000077500000000000000000000000001517363634500223035ustar00rootroot00000000000000colmap-4.0.4/src/colmap/estimators/cost_functions/CMakeLists.txt000066400000000000000000000055061517363634500250510ustar00rootroot00000000000000# Copyright (c), ETH Zurich and UNC Chapel Hill. # 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 the name of ETH Zurich and UNC Chapel Hill nor the names of # its 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 COPYRIGHT HOLDERS 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. set(FOLDER_NAME "estimators_cost_functions") COLMAP_ADD_LIBRARY( NAME colmap_estimators_cost_functions TYPE INTERFACE SRCS alignment.h calibration.h manifold.h motion_averaging.h pose_prior.h reprojection_error.h sampson_error.h utils.h INTERFACE_LINK_LIBS colmap_geometry colmap_sensor Eigen3::Eigen Ceres::ceres ) COLMAP_ADD_TEST( NAME alignment_test SRCS alignment_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME calibration_test SRCS calibration_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME utils_test SRCS utils_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME reprojection_error_test SRCS reprojection_error_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME sampson_error_test SRCS sampson_error_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME pose_prior_test SRCS pose_prior_test.cc LINK_LIBS colmap_estimators_cost_functions ) COLMAP_ADD_TEST( NAME motion_averaging_test SRCS motion_averaging_test.cc LINK_LIBS colmap_estimators_cost_functions ) colmap-4.0.4/src/colmap/estimators/cost_functions/alignment.h000066400000000000000000000057241517363634500244420ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/estimators/cost_functions/utils.h" #include #include namespace colmap { // Cost function for aligning one 3D point with a reference 3D point with // covariance. The Residual is computed in frame b. Coordinate transformation // convention is equivalent to Sim3d. struct Point3DAlignmentCostFunctor : public AutoDiffCostFunctor { public: explicit Point3DAlignmentCostFunctor(const Eigen::Vector3d& point_in_b_prior, bool use_log_scale = true) : point_in_b_prior_(point_in_b_prior), use_log_scale_(use_log_scale) {} template bool operator()(const T* const point_in_a, const T* const b_from_a, T* residuals_ptr) const { // Select whether to exponentiate const T b_from_a_scale = use_log_scale_ ? ceres::exp(b_from_a[7]) : b_from_a[7]; const Eigen::Matrix point_in_b = EigenQuaternionMap(b_from_a) * EigenVector3Map(point_in_a) * b_from_a_scale + EigenVector3Map(b_from_a + 4); Eigen::Map> residuals(residuals_ptr); residuals = point_in_b - point_in_b_prior_.cast(); return true; } private: const Eigen::Vector3d point_in_b_prior_; const bool use_log_scale_; }; } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/alignment_test.cc000066400000000000000000000074111517363634500256320ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/cost_functions/alignment.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/util/eigen_matchers.h" #include namespace colmap { namespace { TEST(Point3DAlignmentCostFunctor, UseLogScale) { Sim3d b_from_a = Sim3d(RandomUniformReal(0.1, 10), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Eigen::Vector3d point_in_b_prior(1., 2., 3.); const Eigen::Vector3d point_in_a(3., 2., 1.); const Eigen::Vector3d point_in_b = b_from_a * point_in_a; std::unique_ptr cost_function( Point3DAlignmentCostFunctor::Create(point_in_b_prior, /*use_log_scale=*/true)); b_from_a.scale() = std::log(b_from_a.scale()); const double* parameters_log_scale[2] = {point_in_a.data(), b_from_a.params.data()}; Eigen::Vector3d residuals; EXPECT_TRUE( cost_function->Evaluate(parameters_log_scale, residuals.data(), nullptr)); const Eigen::Vector3d error = point_in_b - point_in_b_prior; EXPECT_THAT(residuals, EigenMatrixNear(error, 1e-6)); } TEST(Point3DAlignmentCostFunctor, DoNotUseLogScale) { const Sim3d b_from_a = Sim3d(RandomUniformReal(0.1, 10), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Eigen::Vector3d point_in_b_prior(1., 2., 3.); const Eigen::Vector3d point_in_a(3., 2., 1.); const Eigen::Vector3d point_in_b = b_from_a * point_in_a; std::unique_ptr cost_function( Point3DAlignmentCostFunctor::Create(point_in_b_prior, /*use_log_scale=*/false)); const double* parameters_log_scale[2] = {point_in_a.data(), b_from_a.params.data()}; Eigen::Vector3d residuals; EXPECT_TRUE( cost_function->Evaluate(parameters_log_scale, residuals.data(), nullptr)); const Eigen::Vector3d error = point_in_b - point_in_b_prior; EXPECT_THAT(residuals, EigenMatrixNear(error, 1e-6)); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/calibration.h000066400000000000000000000210701517363634500247430ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include #include #include namespace colmap { // Compute polynomial coefficients from cross-products of SVD-derived vectors // for the Fetzer focal length estimation method. The coefficients encode the // relationship between the two focal lengths derived from the fundamental // matrix constraint. // See: "Stable Intrinsic Auto-Calibration from Fundamental Matrices of Devices // with Uncorrelated Camera Parameters", Fetzer et al., WACV 2020. inline Eigen::Vector4d ComputeFetzerPolynomialCoefficients( const Eigen::Vector3d& ai, const Eigen::Vector3d& bi, const Eigen::Vector3d& aj, const Eigen::Vector3d& bj, const int u, const int v) { return {ai(u) * aj(v) - ai(v) * aj(u), ai(u) * bj(v) - ai(v) * bj(u), bi(u) * aj(v) - bi(v) * aj(u), bi(u) * bj(v) - bi(v) * bj(u)}; } // Decompose the fundamental matrix (adjusted by principal points) via SVD and // compute the polynomial coefficients for the Fetzer focal length method. // Returns three coefficient vectors used to estimate the two focal lengths. inline std::array DecomposeFundamentalMatrixForFetzer( const Eigen::Matrix3d& i1_F_i0, const Eigen::Vector2d& principal_point0, const Eigen::Vector2d& principal_point1) { Eigen::Matrix3d K0 = Eigen::Matrix3d::Identity(3, 3); K0(0, 2) = principal_point0(0); K0(1, 2) = principal_point0(1); Eigen::Matrix3d K1 = Eigen::Matrix3d::Identity(3, 3); K1(0, 2) = principal_point1(0); K1(1, 2) = principal_point1(1); // Factoring out the principal points before the SVD appears to be numerically // more stable than the method described in the paper. const Eigen::Matrix3d i1_G_i0 = K1.transpose() * i1_F_i0 * K0; const Eigen::JacobiSVD svd( i1_G_i0, Eigen::ComputeFullU | Eigen::ComputeFullV); const Eigen::Vector3d& s = svd.singularValues(); const Eigen::Vector3d v0 = svd.matrixV().col(0); const Eigen::Vector3d v1 = svd.matrixV().col(1); const Eigen::Vector3d u0 = svd.matrixU().col(0); const Eigen::Vector3d u1 = svd.matrixU().col(1); // Equation 11. Notice there is a sign error in the paper. // Equation 8 shows the sign in aj(1) and bj(1) correctly. const Eigen::Vector3d ai(s(0) * s(0) * (v0(0) * v0(0) + v0(1) * v0(1)), s(0) * s(1) * (v0(0) * v1(0) + v0(1) * v1(1)), s(1) * s(1) * (v1(0) * v1(0) + v1(1) * v1(1))); const Eigen::Vector3d aj(u1(0) * u1(0) + u1(1) * u1(1), -(u0(0) * u1(0) + u0(1) * u1(1)), u0(0) * u0(0) + u0(1) * u0(1)); const Eigen::Vector3d bi(s(0) * s(0) * v0(2) * v0(2), s(0) * s(1) * v0(2) * v1(2), s(1) * s(1) * v1(2) * v1(2)); const Eigen::Vector3d bj(u1(2) * u1(2), -(u0(2) * u1(2)), u0(2) * u0(2)); // Equation 12. // Experiments showed that the d02 term is not useful. // The d10, d21m d20 are redundant to d01, d12, d02. const Eigen::Vector4d d01 = ComputeFetzerPolynomialCoefficients(ai, bi, aj, bj, 1, 0); const Eigen::Vector4d d12 = ComputeFetzerPolynomialCoefficients(ai, bi, aj, bj, 2, 1); return {d01, d12}; } template inline T ComputeFetzerResidual1(const Eigen::Vector& d, const T& fi_sq, const T& fj_sq) { // Equation 13. T denom = fj_sq * d(0) + d(1); denom = denom == T(0) ? T(1e-6) : denom; const T K1 = -(fj_sq * d(2) + d(3)) / denom; return (fi_sq - K1) / fi_sq; } template inline T ComputeFetzerResidual2(const Eigen::Vector& d, const T& fi_sq, const T& fj_sq) { // Equation 14. T denom = fi_sq * d(0) + d(2); denom = denom == T(0) ? T(1e-6) : denom; const T K2 = -(fi_sq * d(1) + d(3)) / denom; return (fj_sq - K2) / fj_sq; } // Cost functor for estimating focal lengths from the fundamental matrix using // the Fetzer method. Used when two images have different cameras (different // focal lengths). The residual measures the relative error between the // estimated and expected focal lengths based on the fundamental matrix // constraint. class FetzerFocalLengthCostFunctor { public: FetzerFocalLengthCostFunctor(const Eigen::Matrix3d& j_F_i, const Eigen::Vector2d& principal_point_i, const Eigen::Vector2d& principal_point_j) : coeffs_(DecomposeFundamentalMatrixForFetzer( j_F_i, principal_point_i, principal_point_j)) {} static ceres::CostFunction* Create(const Eigen::Matrix3d& j_F_i, const Eigen::Vector2d& principal_point_i, const Eigen::Vector2d& principal_point_j) { return new ceres:: AutoDiffCostFunction( new FetzerFocalLengthCostFunctor( j_F_i, principal_point_i, principal_point_j)); } template bool operator()(const T* const focal_length_i, const T* const focal_length_j, T* residuals) const { const T fi_sq = focal_length_i[0] * focal_length_i[0]; const T fj_sq = focal_length_j[0] * focal_length_j[0]; const Eigen::Vector d01 = coeffs_[0].cast(); residuals[0] = ComputeFetzerResidual1(d01, fi_sq, fj_sq); const Eigen::Vector d12 = coeffs_[1].cast(); residuals[1] = ComputeFetzerResidual2(d12, fi_sq, fj_sq); return true; } private: const std::array coeffs_; }; // Cost functor for estimating focal length from the fundamental matrix using // the Fetzer method. Used when two images share the same camera (same focal // length). The residual measures the relative error between the estimated and // expected focal length based on the fundamental matrix constraint. class FetzerFocalLengthSameCameraCostFunctor { public: FetzerFocalLengthSameCameraCostFunctor(const Eigen::Matrix3d& j_F_i, const Eigen::Vector2d& principal_point) : coeffs_(DecomposeFundamentalMatrixForFetzer( j_F_i, principal_point, principal_point)) {} static ceres::CostFunction* Create(const Eigen::Matrix3d& j_F_i, const Eigen::Vector2d& principal_point) { return new ceres:: AutoDiffCostFunction( new FetzerFocalLengthSameCameraCostFunctor(j_F_i, principal_point)); } template bool operator()(const T* const focal_length, T* residuals) const { const T f_sq = focal_length[0] * focal_length[0]; const Eigen::Vector d01 = coeffs_[0].cast(); residuals[0] = ComputeFetzerResidual1(d01, f_sq, f_sq); const Eigen::Vector d12 = coeffs_[1].cast(); residuals[1] = ComputeFetzerResidual2(d12, f_sq, f_sq); return true; } private: const std::array coeffs_; }; } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/calibration_test.cc000066400000000000000000000124031517363634500261400ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/cost_functions/calibration.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/rigid3.h" #include namespace colmap { namespace { TEST(FetzerFocalLengthCostFunctor, ConvexCostLandscape) { constexpr int kNumTrials = 10; for (int i = 0; i < kNumTrials; ++i) { const double focal_length1 = 128; const double focal_length2 = 256; const Eigen::Vector2d pp1(320, 240); const Eigen::Vector2d pp2(480, 320); const Rigid3d cam2_from_cam1(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); Eigen::Matrix3d K1; K1 << focal_length1, 0, pp1(0), 0, focal_length1, pp1(1), 0, 0, 1; Eigen::Matrix3d K2; K2 << focal_length2, 0, pp2(0), 0, focal_length2, pp2(1), 0, 0, 1; const Eigen::Matrix3d F = FundamentalFromEssentialMatrix( K2, EssentialMatrixFromPose(cam2_from_cam1), K1); FetzerFocalLengthCostFunctor cost_functor(F, pp1, pp2); Eigen::VectorXd optimal_residual(2); EXPECT_TRUE( cost_functor(&focal_length1, &focal_length2, optimal_residual.data())); EXPECT_LT(optimal_residual.norm(), 1e-8); double previous_cost = -1e-9; double modified_focal_length1 = focal_length1; double modified_focal_length2 = focal_length2; for (int j = 0; j < 10; ++j) { Eigen::VectorXd residual(2); EXPECT_TRUE(cost_functor( &modified_focal_length1, &modified_focal_length2, residual.data())); const double cost = residual.norm(); EXPECT_GT(cost, previous_cost); previous_cost = cost; modified_focal_length1 *= 1.05; modified_focal_length2 *= 1.05; } previous_cost = -1e-9; modified_focal_length1 = focal_length1; modified_focal_length2 = focal_length2; for (int j = 0; j < 10; ++j) { Eigen::VectorXd residual(2); EXPECT_TRUE(cost_functor( &modified_focal_length1, &modified_focal_length2, residual.data())); const double cost = residual.norm(); EXPECT_GT(cost, previous_cost); previous_cost = cost; modified_focal_length1 *= 0.95; modified_focal_length2 *= 0.95; } } } TEST(FetzerFocalLengthSameCameraCostFunctor, ConvexCostLandscape) { constexpr int kNumTrials = 10; for (int i = 0; i < kNumTrials; ++i) { const double focal_length = 128; const Eigen::Vector2d pp(320, 240); const Rigid3d cam2_from_cam1(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); Eigen::Matrix3d K; K << focal_length, 0, pp(0), 0, focal_length, pp(1), 0, 0, 1; const Eigen::Matrix3d F = FundamentalFromEssentialMatrix( K, EssentialMatrixFromPose(cam2_from_cam1), K); FetzerFocalLengthSameCameraCostFunctor cost_functor(F, pp); Eigen::VectorXd optimal_residual(2); EXPECT_TRUE(cost_functor(&focal_length, optimal_residual.data())); EXPECT_LT(optimal_residual.norm(), 1e-8); double previous_cost = -1e-9; double modified_focal_length = focal_length; for (int j = 0; j < 10; ++j) { Eigen::VectorXd residual(2); EXPECT_TRUE(cost_functor(&modified_focal_length, residual.data())); const double cost = residual.norm(); EXPECT_GT(cost, previous_cost); previous_cost = cost; modified_focal_length *= 1.05; } previous_cost = -1e-9; modified_focal_length = focal_length; for (int j = 0; j < 10; ++j) { Eigen::VectorXd residual(2); EXPECT_TRUE(cost_functor(&modified_focal_length, residual.data())); const double cost = residual.norm(); EXPECT_GT(cost, previous_cost); previous_cost = cost; modified_focal_length *= 0.95; } } } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/manifold.h000066400000000000000000000117261517363634500242540ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include #include namespace colmap { #if CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 1) inline void SetManifold(ceres::Problem* problem, double* params, ceres::Manifold* manifold) { problem->SetManifold(params, manifold); } inline void SetManifold(ceres::Problem* problem, double* params, std::unique_ptr manifold) { problem->SetManifold(params, manifold.release()); } template inline std::unique_ptr CreateEuclideanManifold() { return std::make_unique>(); } inline std::unique_ptr CreateEigenQuaternionManifold() { return std::make_unique(); } inline std::unique_ptr CreateSubsetManifold( int size, const std::vector& constant_params) { return std::make_unique(size, constant_params); } template inline std::unique_ptr CreateSphereManifold() { return std::make_unique>(); } template inline std::unique_ptr CreateProductManifold( Args&&... manifolds) { // Note: Does not support make_unique due to template constructor. return std::unique_ptr( new ceres::ProductManifold(std::forward(manifolds)...)); } inline int ParameterBlockTangentSize(const ceres::Problem& problem, const double* param) { return problem.ParameterBlockTangentSize(param); } #else // CERES_VERSION_MAJOR < 2.1.0 inline void SetManifold(ceres::Problem* problem, double* params, ceres::LocalParameterization* parameterization) { problem->SetParameterization(params, parameterization); } inline void SetManifold( ceres::Problem* problem, double* params, std::unique_ptr parameterization) { problem->SetParameterization(params, parameterization.release()); } template inline std::unique_ptr CreateEuclideanManifold() { return std::make_unique(size); } inline std::unique_ptr CreateEigenQuaternionManifold() { return std::make_unique(); } inline std::unique_ptr CreateSubsetManifold( int size, const std::vector& constant_params) { return std::make_unique(size, constant_params); } template inline std::unique_ptr CreateSphereManifold() { return std::make_unique(size); } template inline std::unique_ptr CreateProductManifold( Args&&... parameterizations) { // Note: Does not support make_unique due to template constructor. return std::unique_ptr( new ceres::ProductParameterization(parameterizations.release()...)); } inline int ParameterBlockTangentSize(const ceres::Problem& problem, const double* param) { return problem.ParameterBlockLocalSize(param); } #endif } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/motion_averaging.h000066400000000000000000000115741517363634500260140ustar00rootroot00000000000000 #pragma once #include #include #include namespace colmap { // Computes the error between a translation direction and the direction formed // from two positions such that: t_ij - scale * (p_j - p_i) is minimized. // The positions can either be two camera centers or one camera center and one // 3D point. // Reference: Zhuang et al., "Baseline Desensitizing In Translation Averaging", // CVPR 2018. struct BATAPairwiseDirectionCostFunctor { explicit BATAPairwiseDirectionCostFunctor( const Eigen::Vector3d& pos2_from_pos1_dir) : pos2_from_pos1_dir_(pos2_from_pos1_dir) {} template bool operator()(const T* pos1, const T* pos2, const T* scale, T* residuals) const { Eigen::Map> residuals_vec(residuals); residuals_vec = pos2_from_pos1_dir_.cast() - scale[0] * (Eigen::Map>(pos2) - Eigen::Map>(pos1)); return true; } static ceres::CostFunction* Create( const Eigen::Vector3d& pos2_from_pos1_dir) { return ( new ceres:: AutoDiffCostFunction( new BATAPairwiseDirectionCostFunctor(pos2_from_pos1_dir))); } const Eigen::Vector3d pos2_from_pos1_dir_; }; // Computes the error between a translation direction and the direction formed // from a camera (c) and 3D point (p) with constant rig extrinsics, such that: // t_ij - scale * (p - c + t_rig) is minimized. struct RigBATAPairwiseDirectionConstantRigCostFunctor { RigBATAPairwiseDirectionConstantRigCostFunctor( const Eigen::Vector3d& cam_from_point3D_dir, const Eigen::Vector3d& cam_from_rig_translation) : cam_from_point3D_dir_(cam_from_point3D_dir), cam_from_rig_translation_(cam_from_rig_translation) {} template bool operator()(const T* point3D, const T* rig_in_world, const T* scale, T* residuals) const { Eigen::Map> residuals_vec(residuals); residuals_vec = cam_from_point3D_dir_.cast() - scale[0] * (Eigen::Map>(point3D) - Eigen::Map>(rig_in_world) + cam_from_rig_translation_.cast()); return true; } static ceres::CostFunction* Create( const Eigen::Vector3d& cam_from_point3D_dir, const Eigen::Vector3d& cam_from_rig_translation) { return (new ceres::AutoDiffCostFunction< RigBATAPairwiseDirectionConstantRigCostFunctor, 3, 3, 3, 1>(new RigBATAPairwiseDirectionConstantRigCostFunctor( cam_from_point3D_dir, cam_from_rig_translation))); } const Eigen::Vector3d cam_from_point3D_dir_; const Eigen::Vector3d cam_from_rig_translation_; }; // Computes the error between a translation direction and the direction formed // from a camera (c) and 3D point (p) with variable rig extrinsics, such that: // t_ij - scale * (p - c + t_rig) is minimized. struct RigBATAPairwiseDirectionCostFunctor { RigBATAPairwiseDirectionCostFunctor( const Eigen::Vector3d& cam_from_point3D_dir, const Eigen::Quaterniond& rig_from_world_rot) : cam_from_point3D_dir_(cam_from_point3D_dir), world_from_rig_rot_(rig_from_world_rot.inverse()) {} template bool operator()(const T* point3D, const T* rig_in_world, const T* cam_in_rig, const T* scale, T* residuals) const { const Eigen::Matrix cam_from_rig_translation = world_from_rig_rot_.cast() * Eigen::Map>(cam_in_rig); Eigen::Map> residuals_vec(residuals); residuals_vec = cam_from_point3D_dir_.cast() - scale[0] * (Eigen::Map>(point3D) - Eigen::Map>(rig_in_world) - cam_from_rig_translation); return true; } static ceres::CostFunction* Create( const Eigen::Vector3d& cam_from_point3D_dir, const Eigen::Quaterniond& rig_from_world_rot) { return (new ceres::AutoDiffCostFunction( new RigBATAPairwiseDirectionCostFunctor(cam_from_point3D_dir, rig_from_world_rot))); } const Eigen::Vector3d cam_from_point3D_dir_; const Eigen::Quaterniond world_from_rig_rot_; }; } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/motion_averaging_test.cc000066400000000000000000000172541517363634500272120ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/cost_functions/motion_averaging.h" #include "colmap/util/eigen_matchers.h" #include namespace colmap { namespace { TEST(BATAPairwiseDirectionCostFunctor, ZeroResidual) { const Eigen::Vector3d pos1(1, 2, 3); const Eigen::Vector3d pos2(2, 3, 4); const double scale = 1.0; const Eigen::Vector3d direction = pos2 - pos1; BATAPairwiseDirectionCostFunctor cost_functor(direction); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor(pos1.data(), pos2.data(), &scale, residuals.data())); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-10)); } TEST(BATAPairwiseDirectionCostFunctor, NonZeroResidual) { const Eigen::Vector3d pos1(1, 2, 3); const Eigen::Vector3d pos2(4, 5, 6); const double scale = 2.0; const Eigen::Vector3d direction(1, 1, 1); BATAPairwiseDirectionCostFunctor cost_functor(direction); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor(pos1.data(), pos2.data(), &scale, residuals.data())); const Eigen::Vector3d expected_residuals = direction - scale * (pos2 - pos1); EXPECT_THAT(residuals, EigenMatrixNear(expected_residuals, 1e-10)); } TEST(BATAPairwiseDirectionCostFunctor, DifferentScale) { const Eigen::Vector3d pos1(1, 2, 3); const Eigen::Vector3d pos2(2, 4, 6); const double scale = 0.5; const Eigen::Vector3d direction = scale * (pos2 - pos1); BATAPairwiseDirectionCostFunctor cost_functor(direction); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor(pos1.data(), pos2.data(), &scale, residuals.data())); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-10)); } TEST(BATAPairwiseDirectionCostFunctor, Create) { const Eigen::Vector3d direction(1, 0, 0); std::unique_ptr cost_function( BATAPairwiseDirectionCostFunctor::Create(direction)); ASSERT_NE(cost_function, nullptr); } TEST(RigBATAPairwiseDirectionConstantRigCostFunctor, ZeroResidual) { const Eigen::Vector3d point3D(1, 2, 3); const Eigen::Vector3d rig_in_world(3, 2, 1); const double scale = 1.5; const Eigen::Vector3d cam_from_rig_dir(0.25, 0.5, 0.75); const Eigen::Vector3d cam_from_point3D_dir = scale * (point3D - rig_in_world + cam_from_rig_dir); RigBATAPairwiseDirectionConstantRigCostFunctor cost_functor( cam_from_point3D_dir, cam_from_rig_dir); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor( point3D.data(), rig_in_world.data(), &scale, residuals.data())); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-10)); } TEST(RigBATAPairwiseDirectionConstantRigCostFunctor, NonZeroResidual) { const Eigen::Vector3d point3D(3, 4, 5); const Eigen::Vector3d rig_in_world(1, 2, 3); const double scale = 2.0; const Eigen::Vector3d cam_from_rig_dir(0.1, 0.2, 0.3); const Eigen::Vector3d cam_from_point3D_dir(1, 1, 1); RigBATAPairwiseDirectionConstantRigCostFunctor cost_functor( cam_from_point3D_dir, cam_from_rig_dir); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor( point3D.data(), rig_in_world.data(), &scale, residuals.data())); const Eigen::Vector3d expected_residuals = cam_from_point3D_dir - scale * (point3D - rig_in_world + cam_from_rig_dir); EXPECT_THAT(residuals, EigenMatrixNear(expected_residuals, 1e-10)); } TEST(RigBATAPairwiseDirectionConstantRigCostFunctor, Create) { const Eigen::Vector3d cam_from_point3D_dir(1, 0, 0); const Eigen::Vector3d cam_from_rig_dir(0, 1, 0); std::unique_ptr cost_function( RigBATAPairwiseDirectionConstantRigCostFunctor::Create( cam_from_point3D_dir, cam_from_rig_dir)); ASSERT_NE(cost_function, nullptr); } TEST(RigBATAPairwiseDirectionCostFunctor, ZeroResidual) { const Eigen::Vector3d point3D(5, 5, 5); const Eigen::Vector3d rig_in_world(1, 1, 1); const Eigen::Vector3d cam_in_rig(0.5, 0.5, 0.5); const double scale = 1.0; const Eigen::Quaterniond rig_from_world_rot = Eigen::Quaterniond::Identity(); const Eigen::Vector3d cam_from_rig_dir = rig_from_world_rot.inverse() * cam_in_rig; const Eigen::Vector3d cam_from_point3D_dir = scale * (point3D - rig_in_world - cam_from_rig_dir); RigBATAPairwiseDirectionCostFunctor cost_functor(cam_from_point3D_dir, rig_from_world_rot); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor(point3D.data(), rig_in_world.data(), cam_in_rig.data(), &scale, residuals.data())); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-10)); } TEST(RigBATAPairwiseDirectionCostFunctor, NonZeroResidual) { const Eigen::Vector3d point3D(3, 4, 5); const Eigen::Vector3d rig_in_world(1, 2, 3); const Eigen::Vector3d cam_in_rig(0.2, 0.3, 0.4); const double scale = 2.0; const Eigen::Quaterniond rig_from_world_rot = Eigen::Quaterniond(0.707, 0.707, 0, 0).normalized(); const Eigen::Vector3d cam_from_point3D_dir(1, 1, 1); RigBATAPairwiseDirectionCostFunctor cost_functor(cam_from_point3D_dir, rig_from_world_rot); Eigen::Vector3d residuals; EXPECT_TRUE(cost_functor(point3D.data(), rig_in_world.data(), cam_in_rig.data(), &scale, residuals.data())); const Eigen::Vector3d cam_from_rig_dir = rig_from_world_rot.toRotationMatrix().transpose() * cam_in_rig; const Eigen::Vector3d expected_residuals = cam_from_point3D_dir - scale * (point3D - rig_in_world - cam_from_rig_dir); EXPECT_THAT(residuals, EigenMatrixNear(expected_residuals, 1e-10)); } TEST(RigBATAPairwiseDirectionCostFunctor, Create) { const Eigen::Vector3d cam_from_point3D_dir(1, 0, 0); const Eigen::Quaterniond rig_from_world_rot = Eigen::Quaterniond::Identity(); std::unique_ptr cost_function( RigBATAPairwiseDirectionCostFunctor::Create(cam_from_point3D_dir, rig_from_world_rot)); ASSERT_NE(cost_function, nullptr); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/pose_prior.h000066400000000000000000000165741517363634500246520ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/estimators/cost_functions/utils.h" #include "colmap/geometry/rigid3.h" #include #include #include namespace colmap { template inline void EigenQuaternionToAngleAxis(const T* eigen_quaternion, T* angle_axis) { const T quaternion[4] = {eigen_quaternion[3], eigen_quaternion[0], eigen_quaternion[1], eigen_quaternion[2]}; ceres::QuaternionToAngleAxis(quaternion, angle_axis); } // 6-DoF error on the absolute sensor pose. The residual is the log of the error // pose, splitting SE(3) into SO(3) x R^3. The residual is computed in the // sensor frame. Its first and last three components correspond to the rotation // and translation errors, respectively. struct AbsolutePosePriorCostFunctor : public AutoDiffCostFunctor { public: explicit AbsolutePosePriorCostFunctor(const Rigid3d& sensor_from_world_prior) : world_from_sensor_prior_(Inverse(sensor_from_world_prior)) {} template bool operator()(const T* const sensor_from_world, T* residuals_ptr) const { const Eigen::Quaternion param_from_prior_rotation = EigenQuaternionMap(sensor_from_world) * world_from_sensor_prior_.rotation().cast(); EigenQuaternionToAngleAxis(param_from_prior_rotation.coeffs().data(), residuals_ptr); Eigen::Map> param_from_prior_translation( residuals_ptr + 3); param_from_prior_translation = EigenVector3Map(sensor_from_world + 4) + EigenQuaternionMap(sensor_from_world) * world_from_sensor_prior_.translation().cast(); return true; } private: const Rigid3d world_from_sensor_prior_; }; // 3-DoF error on the sensor position in the world coordinate frame. struct AbsolutePosePositionPriorCostFunctor : public AutoDiffCostFunctor { public: explicit AbsolutePosePositionPriorCostFunctor( const Eigen::Vector3d& position_in_world_prior) : position_in_world_prior_(position_in_world_prior) {} template bool operator()(const T* const sensor_from_world, T* residuals_ptr) const { Eigen::Map> residuals(residuals_ptr); residuals = position_in_world_prior_.cast() + EigenQuaternionMap(sensor_from_world).inverse() * EigenVector3Map(sensor_from_world + 4); return true; } private: const Eigen::Vector3d position_in_world_prior_; }; // 3-DoF error on the rig sensor position in the world coordinate frame. struct AbsoluteRigPosePositionPriorCostFunctor : public AutoDiffCostFunctor { public: explicit AbsoluteRigPosePositionPriorCostFunctor( const Eigen::Vector3d& position_in_world_prior) : position_in_world_prior_(position_in_world_prior) {} template bool operator()(const T* const sensor_from_rig, const T* const rig_from_world, T* residuals_ptr) const { const Eigen::Quaternion sensor_from_world_rotation = EigenQuaternionMap(sensor_from_rig) * EigenQuaternionMap(rig_from_world); const Eigen::Matrix sensor_from_world_translation = EigenVector3Map(sensor_from_rig + 4) + EigenQuaternionMap(sensor_from_rig) * EigenVector3Map(rig_from_world + 4); Eigen::Map> residuals(residuals_ptr); residuals = position_in_world_prior_.cast() + sensor_from_world_rotation.inverse() * sensor_from_world_translation; return true; } private: const Eigen::Vector3d position_in_world_prior_; }; // 6-DoF error between two absolute camera poses based on a prior on their // relative pose, with identical scale for the translation. The residual is // computed in the frame of camera i. Its first and last three components // correspond to the rotation and translation errors, respectively. // // Derivation: // i_T_w = ΔT_i·i_T_j·j_T_w // where ΔT_i = exp(η_i) is the resjdual in SE(3) and η_i in tangent space. // Thus η_i = log(i_T_w·j_T_w⁻¹·j_T_i) // Rotation term: ΔR = log(i_R_w·j_R_w⁻¹·j_R_i) // Translation term: Δt = i_t_w + i_R_w·j_R_w⁻¹·(j_t_i -j_t_w) struct RelativePosePriorCostFunctor : public AutoDiffCostFunctor { public: explicit RelativePosePriorCostFunctor(const Rigid3d& i_from_j_prior) : j_from_i_prior_(Inverse(i_from_j_prior)) {} template bool operator()(const T* const i_from_world, const T* const j_from_world, T* residuals_ptr) const { const Eigen::Quaternion i_from_j_rotation = EigenQuaternionMap(i_from_world) * EigenQuaternionMap(j_from_world).inverse(); const Eigen::Quaternion param_from_prior_rotation = i_from_j_rotation * j_from_i_prior_.rotation().template cast(); EigenQuaternionToAngleAxis(param_from_prior_rotation.coeffs().data(), residuals_ptr); const Eigen::Matrix j_from_i_prior_translation = j_from_i_prior_.translation().cast() - EigenVector3Map(j_from_world + 4); Eigen::Map> param_from_prior_translation( residuals_ptr + 3); param_from_prior_translation = EigenVector3Map(i_from_world + 4) + i_from_j_rotation * j_from_i_prior_translation; return true; } private: const Rigid3d j_from_i_prior_; }; } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/pose_prior_test.cc000066400000000000000000000221441517363634500260350ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #include "colmap/estimators/cost_functions/pose_prior.h" #include "colmap/geometry/rigid3.h" #include "colmap/math/math.h" #include "colmap/util/eigen_matchers.h" #include namespace colmap { namespace { TEST(AbsolutePosePositionPriorCostFunctor, Nominal) { std::unique_ptr cost_function( AbsolutePosePositionPriorCostFunctor::Create(Eigen::Vector3d::Zero())); Rigid3d sensor_from_world = Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); Eigen::Vector3d residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); const double* parameters[1] = {sensor_from_world.params.data()}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-6)); sensor_from_world = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Eigen::Vector3d position_in_world = Inverse(sensor_from_world).translation(); residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(-position_in_world), 1e-6)); cost_function.reset( AbsolutePosePositionPriorCostFunctor::Create(position_in_world)); residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-6)); } TEST(AbsoluteRigPosePositionPriorCostFunctor, Nominal) { std::unique_ptr cost_function( AbsoluteRigPosePositionPriorCostFunctor::Create(Eigen::Vector3d::Zero())); Rigid3d sensor_from_rig(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); Rigid3d rig_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); Eigen::Vector3d residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); const double* parameters[2] = {sensor_from_rig.params.data(), rig_from_world.params.data()}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-6)); sensor_from_rig = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); rig_from_world = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Rigid3d sensor_from_world = sensor_from_rig * rig_from_world; const Eigen::Vector3d position_in_world = Inverse(sensor_from_world).translation(); residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(-position_in_world), 1e-6)); cost_function.reset( AbsoluteRigPosePositionPriorCostFunctor::Create(position_in_world)); residuals = Eigen::Vector3d::Constant(std::numeric_limits::quiet_NaN()); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals.data(), nullptr)); EXPECT_THAT(residuals, EigenMatrixNear(Eigen::Vector3d(0, 0, 0), 1e-6)); } TEST(AbsolutePosePriorCostFunctor, Nominal) { const Rigid3d cam_from_world_prior; std::unique_ptr cost_function( AbsolutePosePriorCostFunctor::Create(cam_from_world_prior)); double cam_from_world[7] = {0, 0, 0, 1, 0, 0, 0}; double residuals[6]; const double* parameters[1] = {cam_from_world}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); EXPECT_EQ(residuals[2], 0); EXPECT_EQ(residuals[3], 0); EXPECT_EQ(residuals[4], 0); EXPECT_EQ(residuals[5], 0); cam_from_world[4] = 1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); EXPECT_EQ(residuals[2], 0); EXPECT_EQ(residuals[3], 1); EXPECT_EQ(residuals[4], 0); EXPECT_EQ(residuals[5], 0); // Rotation by 90 degrees around the Y axis. Eigen::Matrix3d rotation_matrix; rotation_matrix << 0, 0, 1, 0, 1, 0, -1, 0, 0; Eigen::Map(static_cast(cam_from_world)) = rotation_matrix; cam_from_world[5] = 2; cam_from_world[6] = 3; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_NEAR(residuals[0], 0, 1e-6); EXPECT_NEAR(residuals[1], DegToRad(90.0), 1e-6); EXPECT_NEAR(residuals[2], 0, 1e-6); EXPECT_NEAR(residuals[3], 1, 1e-6); EXPECT_NEAR(residuals[4], 2, 1e-6); EXPECT_NEAR(residuals[5], 3, 1e-6); } TEST(RelativePosePriorCostFunctor, Nominal) { Rigid3d i_from_j_prior(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, -1)); std::unique_ptr cost_function( RelativePosePriorCostFunctor::Create(i_from_j_prior)); double i_from_world[7] = {0, 0, 0, 1, 0, 0, 0}; double j_from_world[7] = {0, 0, 0, 1, 0, 0, 1}; double residuals[6]; const double* parameters[2] = {i_from_world, j_from_world}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); EXPECT_EQ(residuals[2], 0); EXPECT_EQ(residuals[3], 0); EXPECT_EQ(residuals[4], 0); EXPECT_EQ(residuals[5], 0); i_from_world[6] = 4; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); EXPECT_EQ(residuals[2], 0); EXPECT_EQ(residuals[3], 0); EXPECT_EQ(residuals[4], 0); EXPECT_EQ(residuals[5], 4); j_from_world[4] = 2; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); EXPECT_EQ(residuals[2], 0); EXPECT_EQ(residuals[3], -2); EXPECT_EQ(residuals[4], 0); EXPECT_EQ(residuals[5], 4); // Rotation by 90 degrees around the Y axis. Eigen::Matrix3d rotation_matrix; rotation_matrix << 0, 0, 1, 0, 1, 0, -1, 0, 0; Eigen::Map(static_cast(j_from_world)) = rotation_matrix; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_NEAR(residuals[0], 0, 1e-6); EXPECT_NEAR(residuals[1], DegToRad(-90.0), 1e-6); EXPECT_NEAR(residuals[2], 0, 1e-6); EXPECT_NEAR(residuals[3], 0, 1e-6); EXPECT_NEAR(residuals[4], 0, 1e-6); EXPECT_NEAR(residuals[5], 2, 1e-6); } TEST(CovarianceWeightedCostFunctor, AbsolutePosePositionPriorCostFunctor) { const Rigid3d cam_from_world(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Rigid3d world_from_cam = Inverse(cam_from_world); double residuals[3]; const double* parameters[1] = {cam_from_world.params.data()}; std::unique_ptr cost_function( CovarianceWeightedCostFunctor:: Create(2 * Eigen::Matrix3d::Identity(), Eigen::Vector3d::Zero())); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_NEAR(residuals[0], -0.5 * std::sqrt(2) * world_from_cam.translation()[0], 1e-6); EXPECT_NEAR(residuals[1], -0.5 * std::sqrt(2) * world_from_cam.translation()[1], 1e-6); EXPECT_NEAR(residuals[2], -0.5 * std::sqrt(2) * world_from_cam.translation()[2], 1e-6); } } // namespace } // namespace colmap colmap-4.0.4/src/colmap/estimators/cost_functions/reprojection_error.h000066400000000000000000000347141517363634500264010ustar00rootroot00000000000000// Copyright (c), ETH Zurich and UNC Chapel Hill. // 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 the name of ETH Zurich and UNC Chapel Hill nor the names of // its 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 COPYRIGHT HOLDERS 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. #pragma once #include "colmap/estimators/cost_functions/utils.h" #include "colmap/geometry/rigid3.h" #include "colmap/sensor/models.h" #include #include #include namespace colmap { // Rotates the point and computes the Jacobian of R(q) * p with respect to Eigen // quaternions. J_out is a 3x4 matrix in row-major order. inline Eigen::Vector3d QuaternionRotatePointWithJac(const double* q, const double* pt, double* J_out) { const double qx = q[0], qy = q[1], qz = q[2], qw = q[3]; const double px = pt[0], py = pt[1], pz = pt[2]; // Common sub-expressions. const double qx_py = qx * py; const double qx_pz = qx * pz; const double qy_px = qy * px; const double qy_pz = qy * pz; const double qz_px = qz * px; const double qz_py = qz * py; // R(q) * p using the formula: p' = p + 2*w*(v x p) + 2*(v x (v x p)), // where v = (qx, qy, qz) is the imaginary part and w = qw is the scalar. // First compute v x p. const double v_x_p0 = qy_pz - qz_py; const double v_x_p1 = qz_px - qx_pz; const double v_x_p2 = qx_py - qy_px; // Then compute v x (v x p). const double v_x_v_x_p0 = qy * v_x_p2 - qz * v_x_p1; const double v_x_v_x_p1 = qz * v_x_p0 - qx * v_x_p2; const double v_x_v_x_p2 = qx * v_x_p1 - qy * v_x_p0; // p' = p + 2*w*(v x p) + 2*(v x (v x p)). Eigen::Vector3d pt_out(px + 2.0 * (qw * v_x_p0 + v_x_v_x_p0), py + 2.0 * (qw * v_x_p1 + v_x_v_x_p1), pz + 2.0 * (qw * v_x_p2 + v_x_v_x_p2)); if (J_out) { // Jacobian d(R*p) / dq for Eigen quaternions (x, y, z, w). // Must use the ORIGINAL point (px, py, pz), not the rotated point. // Common sub-expressions. const double qx_px = qx * px; const double qx_pz = qx * pz; const double qy_px = qy * px; const double qy_py = qy * py; const double qz_pz = qz * pz; const double qw_px = qw * px; const double qw_py = qw * py; const double qw_pz = qw * pz; // d(R*p)_x / d(x,y,z,w) J_out[0] = 2.0 * (qy_py + qz_pz); J_out[1] = 2.0 * (-2.0 * qy_px + qx_py + qw_pz); J_out[2] = 2.0 * (-2.0 * qz_px - qw_py + qx_pz); J_out[3] = 2.0 * (-qz_py + qy_pz); // d(R*p)_y / d(x,y,z,w) J_out[4] = 2.0 * (qy_px - 2.0 * qx_py - qw_pz); J_out[5] = 2.0 * (qx_px + qz_pz); J_out[6] = 2.0 * (qw_px - 2.0 * qz_py + qy_pz); J_out[7] = 2.0 * (qz_px - qx_pz); // d(R*p)_z / d(x,y,z,w) J_out[8] = 2.0 * (qz_px + qw_py - 2.0 * qx_pz); J_out[9] = 2.0 * (-qw_px + qz_py - 2.0 * qy_pz); J_out[10] = 2.0 * (qx_px + qy_py); J_out[11] = 2.0 * (-qy_px + qx_py); } return pt_out; } // Full reprojection error cost function with analytical Jacobians. // Requires camera model to implement ImgFromCamWithJac(). template class AnalyticalReprojErrorCostFunction : public ceres::SizedCostFunction<2, 3, 7, CameraModel::num_params> { public: explicit AnalyticalReprojErrorCostFunction(const Eigen::Vector2d& point2D) : point2D_(point2D) {} bool Evaluate(double const* const* parameters, double* residuals, double** jacobians) const override { const double* point3D_in_world = parameters[0]; const double* cam_from_world = parameters[1]; const double* camera_params = parameters[2]; double* J_point = jacobians ? jacobians[0] : nullptr; double* J_pose = jacobians ? jacobians[1] : nullptr; double* J_params = jacobians ? jacobians[2] : nullptr; Eigen::Map residuals_vec(residuals); Eigen::Map> J_point_mat( J_point); Eigen::Map> J_pose_mat(J_pose); Eigen::Map< Eigen::Matrix> J_params_mat(J_params); Eigen::Matrix J_Rp_quat_mat; Eigen::Matrix J_uvw_mat; const Eigen::Vector3d point3D_in_cam = QuaternionRotatePointWithJac(cam_from_world, point3D_in_world, J_pose ? J_Rp_quat_mat.data() : nullptr) + Eigen::Map(cam_from_world + 4); if (!CameraModel::ImgFromCamWithJac( camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1], J_params, (J_point || J_pose) ? J_uvw_mat.data() : nullptr)) { residuals_vec.setZero(); if (J_pose) { J_pose_mat.setZero(); } if (J_point) { J_point_mat.setZero(); } if (J_params) { J_params_mat.setZero(); } return true; } residuals_vec -= point2D_; if (J_point) { J_point_mat = J_uvw_mat * EigenQuaternionMap(cam_from_world).toRotationMatrix(); } if (J_pose) { J_pose_mat.leftCols<4>() = J_uvw_mat * J_Rp_quat_mat; J_pose_mat.rightCols<3>() = J_uvw_mat; } return true; } private: const Eigen::Vector2d point2D_; }; // Standard bundle adjustment cost function for variable // camera pose, calibration, and point parameters. template class ReprojErrorCostFunctor : public AutoDiffCostFunctor, 2, 3, 7, CameraModel::num_params> { public: explicit ReprojErrorCostFunctor(const Eigen::Vector2d& point2D) : point2D_(point2D) {} template bool operator()(const T* const point3D_in_world, const T* const cam_from_world, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_cam = EigenQuaternionMap(cam_from_world) * EigenVector3Map(point3D_in_world) + EigenVector3Map(cam_from_world + 4); Eigen::Map> residuals_vec(residuals); if (CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1])) { residuals_vec -= point2D_.cast(); } else { residuals_vec.setZero(); } return true; } private: const Eigen::Vector2d point2D_; }; // Bundle adjustment cost function for variable // camera calibration and point parameters, and fixed camera pose. template class ReprojErrorConstantPoseCostFunctor : public AutoDiffCostFunctor< ReprojErrorConstantPoseCostFunctor, 2, 3, CameraModel::num_params> { public: ReprojErrorConstantPoseCostFunctor(const Eigen::Vector2d& point2D, const Rigid3d& cam_from_world) : cam_from_world_(cam_from_world), reproj_cost_(point2D) {} template bool operator()(const T* const point3D_in_world, const T* const camera_params, T* residuals) const { const Eigen::Matrix cam_from_world = cam_from_world_.params.cast(); return reproj_cost_( point3D_in_world, cam_from_world.data(), camera_params, residuals); } private: const Rigid3d cam_from_world_; const ReprojErrorCostFunctor reproj_cost_; }; // Bundle adjustment cost function for variable // camera pose and calibration parameters, and fixed point. template class ReprojErrorConstantPoint3DCostFunctor : public AutoDiffCostFunctor< ReprojErrorConstantPoint3DCostFunctor, 2, 7, CameraModel::num_params> { public: ReprojErrorConstantPoint3DCostFunctor(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D_in_world) : point3D_in_world_(point3D_in_world), reproj_cost_(point2D) {} template bool operator()(const T* const cam_from_world, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_world = point3D_in_world_.cast(); return reproj_cost_( point3D_in_world.data(), cam_from_world, camera_params, residuals); } private: const Eigen::Vector3d point3D_in_world_; const ReprojErrorCostFunctor reproj_cost_; }; // Rig bundle adjustment cost function for variable camera pose and calibration // and point parameters. Different from the standard bundle adjustment function, // this cost function is suitable for camera rigs with consistent relative poses // of the cameras within the rig. The cost function first projects points into // the local system of the camera rig and then into the local system of the // camera within the rig. template class RigReprojErrorCostFunctor : public AutoDiffCostFunctor, 2, 3, 7, 7, CameraModel::num_params> { public: explicit RigReprojErrorCostFunctor(const Eigen::Vector2d& point2D) : point2D_(point2D) {} template bool operator()(const T* const point3D_in_world, const T* const cam_from_rig, const T* const rig_from_world, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_cam = EigenQuaternionMap(cam_from_rig) * (EigenQuaternionMap(rig_from_world) * EigenVector3Map(point3D_in_world) + EigenVector3Map(rig_from_world + 4)) + EigenVector3Map(cam_from_rig + 4); Eigen::Map> residuals_vec(residuals); if (CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1])) { residuals_vec -= point2D_.cast(); } else { residuals_vec.setZero(); } return true; } private: const Eigen::Vector2d point2D_; }; // Rig bundle adjustment cost function for variable camera pose and camera // calibration and point parameters but fixed rig extrinsic poses. template class RigReprojErrorConstantRigCostFunctor : public AutoDiffCostFunctor< RigReprojErrorConstantRigCostFunctor, 2, 3, 7, CameraModel::num_params> { public: RigReprojErrorConstantRigCostFunctor(const Eigen::Vector2d& point2D, const Rigid3d& cam_from_rig) : cam_from_rig_(cam_from_rig), reproj_cost_(point2D) {} template bool operator()(const T* const point3D_in_world, const T* const rig_from_world, const T* const camera_params, T* residuals) const { const Eigen::Matrix cam_from_rig = cam_from_rig_.params.cast(); return reproj_cost_(point3D_in_world, cam_from_rig.data(), rig_from_world, camera_params, residuals); } private: const Rigid3d cam_from_rig_; const RigReprojErrorCostFunctor reproj_cost_; }; template

B[F7C(@<g  7 l$(BPKATt4 "UX2C.a槜>8 X \κ8\r,Z* _Cl`-9(/clˎ4ʛK>kPJ̙SD%HeK*y?Lλ24;͂V3.ϗK`? 96<";=ǎ_zJ~g`)FE߱nZ@Z:e/?^(+,4λl w`ݲe2l$ОlԜ92=}* l @/=B[sa+bD,c/ p1;.d)kP~l1dEѯN8>OZy\dBJHhαı(LApm1_3x =0hi[/[7Ȇ:mR^6BƎBٹ{cR_vR5~*z%L(D nʮ%^{zǕPJ@ (%p\{e=( 0`G?Tj<@:Ll()X%<`;0(Gtlz )@U!5J%6 q.H׬' ?_Xɠfk\vEuI `:, #*?F:ʉ\n:]jٰl_h&lntv?h~lF\B ]nxB`GA"_y$>JRi%c8=17\ (pW3GכBX&0SeߞҶ]"xҤ pyqGn.*'&&OҲQIv5m7m zpi ˫^Gksz#|"t(%$>># eqV|xC ( {^_Ϩ{@;mcJXBڟ}jY2͘OGt?5}#esܹY;vΝ ܉W^)[kj6o6&F-=FΘ!+K/5mq%׈,\YGy%6%i(Gh -b&:gTtEFQBye;G9*AC' >6b!PdcI ti*6 4;D5M ot~5V.$z-c-\ה )pД~:vqlc|(g=I&kzQ,'vG?(v\(Ӧ͔m ҾI<.P 4C7l\u(%04 >z^ݟG@.-QG~ezD%04ģ] %Źy 7/Gv% ^?חHf*1et /H[czMBkI\  \<u|KFqNH2nbX,S}<\\.o2T:X:=[Quoqo'> VҿMY׽7ˁZDk}ȑ{ 8zro-KyD&wq&~V[cvgKNAh@g՟9'OEl^-G?[HBaKVX)$e 5Yfu IJF`Ŷ;%{FdR7_)BcbhU60g .w6"V}jsBXcgGGD"T0M0~p6S3AsA@Vׁ}-(MtAD` va$"!(a D"թpmq@3*{v# kؾ様:;ө8}LYw@ _7uBL 7k׸!p9s幅':?4ػE;U?ȝWIWBMOKɯWMGZhhm߰Q9V]Unx+'(,.6%}Tҥ2M?k۱7ٲG1H"u5̉pQYBa=a qQ<`m̓S_U~)=CmcYV~WyNXFkbB\`0ށ͈[SI,r<B%RTj& ~k6(;X36P3Ғ֔ADs׃MdPbI/QtFKBǺykGiC8Ǭ #C!d! 7|j>F|6 KEyTl&(p-Pjs3 t ژ*۱/?]* Πlvy mEl\ro#ҶZӧnzt59B 5mxnZQU(9;}^M;rzG%z'ѣuDB68aґ}]A}:#rm>얳xaa \S.lB*Gic0wd6mݏ+)nY/x”֎ӱhmyz(.然 pLNU֮{ߔe:\ne?)L;*{VpH.:\15”%Wh0 wY%xPHeMҨ2bǤSh7T;qfD)+Cv5@Gt8n㎃X)))<_T)cFK</2ab!]~|n4ŠIϜ4avYއob(Snm ;ρ%'f:ٓtuxM֑{nR^6nb6 5=ߓNI]ѱOCd @Λs1~]h> _v,h :w~C (n^|u_K^OvC)8ko5"[Ŝ9tG9/)U&l P~5>~_6?*0q %l4^R3a t6=֭\{y&UGSDQ t,\1JAicI5_LA@cO5qL)( MS?!3}t@hgCYf 4=ݮ,!FDIX!90! *uIŘr"rCK` jxB> eZ[> PcN:1W{|?F4!{3f?=v]`[SWam][%r kZGn`[}'t-2{PJwe%r]r虑#GʯyX?kez ]O%"PQQ!?yGIh5M.H/Jqeeg]\Y,2bKEE9%'d3 9T.\(cȔy Mm.0kk? miIƨN\1hO)Qr8\Xt6ԓSP`, X)As"0STc5G5!@ZpqpAhb)zXVs`=w~_W]ζdkZG9_w}Uzcs]L +_r̿\xKM<翘R6 ѡ Yt/I<u[0q G#KCb)ci 3,}rtg[PA~u}L]YD^}_w9*1q#q4pK H&]x!2bbV:SLԶYUz8 v"BS9HFˍepQ̈Q!t?At=c(QW'¹(R9!^9\G&wgh{;Z0 X.ʴ% KY)s"dBט)?H}pű=pQ wY˂sTXBm:C q.L ud1k60Gy<Ztdߘ;弳>///~^֭_S]mY)A"c|lZ.:HaZ:9dsV$5w~UP* |R1jLXُxHp7ɍ7\/k֮-["lx.[ .Dʱ2}'.? p/m}YCڵK(PX9VE6vbK@g_}u-[fyC qyԨs>tˑ6Ks}>O$p !.XsXi be0l?P`UfWOjӉu0 ͠TTY2sROR8c(W 6C3.jpA ̜cN4sc8߰N&FAlPtݱ4RS ]\:9$pQ !;Ka8u(ӌV#f@:P"`).nzWh23\xӡM3>S=;% &G c}yI;6;Dﲖޓ|&O,#!Y3;w,GAJ 匯}-OML(=Һ}!m^[ŁpCbU\צ\/Y6YuJaI>݂P_6ZPJGKI1 E\(/miVO[k,لX E8t1Z,"]L7|o<K2*x !-I,;t@ -fViÄ$`9XK^mX|JI, e+Scv(u׮ƕzL3~싂Q>,/]r:]2e#FEgVJ@ (%PJ@ (AF|UU{dx?g6ɑX!vh^uOe[oI WFeSh=Ǥck6֗RP5kH$ҩؗp{55IҺmXZ D4%KK M>*{4T^sM_ mtsY\eKAڊRr@X ,-2MSJ Xg(+wFôgf\jmtŃ! Jٶ/pk9ֺ} p%q1B,(Z./`t0bsZO*pPb(K)cyP]fo=2#MFלB'q?(jR͎@VϹ)%}jsPPL':qqx)&!0!退g5W%PJ@ (%PJoZׯ}k֘zV8 C[sI-LbK)Rm%Gn+r8 tpdm\XN^F k>'h&:No Ck.q-%8q7 H^kG JRx)h[qIMYk~?k}PJ@ (%PJ@ (%'Ґ9K 8Y_!}_h?s+#Re^y%]Zxb6G "[}ݟU? }PJ@ (%PJ@ (%)QW\!7K>DHsid'Z_{<O?ǯ('QwJ ˳PnK"--{X9s|G~Ux*#+B#<2TWשPJ@ (%PJ@ Yd~ Ƣ@u]w;WqW\j|;曻IAc_J|Rdőߵɜ@fūϯ{(%PJ@ (%PJ@ d/CiAoEO }c֏~$_s`Oy>i7ߔаp\T֓~3U]PJ@ (%PJ@ (%pp6!EW9G\)%H,:d~a-O?-vτ@m,woEf>\gtd mᨳ(%PJ@ (%PJ˥ o =NA9'pmw#3!ĭFH‹sHѬYoHm}GeAUh;_]=7%PJ@ (%PJ `XLŅ@VTkWi{MҶa\jLG :O@3PJ@ (%PJ@ (%pTm6v/Y"@@@<#Qʺf%uxPJ@ (%PJ@ (@{Eٵt\e56_B=%PJ@ (%PJ@ @g }tPh`IPJ@ (%PJ@ (%7h;^A:`0(:PJk<&kSJ@ (%r@ݖ9"]8|v/R(t(߾E~eSF (%@`˶FL:PJ@ (%>/BW@@KGQPJ@ (%PJ@ (%P*πPJ@ (%PJ@ (%K #=O 5 Izu(%PJ@ (%JTJ5 m*%PJ@ (%PJ@ (%p`PJ@ (%PJ@ (%@ otPJ@ (%PJ@ (%8@?CIENDB`colmap-4.0.4/doc/images/sparse.png000077500000000000000000040755471517363634500170550ustar00rootroot00000000000000PNG  IHDRY5C$sRGB pHYs  YiTXtXML:com.adobe.xmp 1 L'Y@IDATx|\ll6HiQ*Z.p{{.ťTh)uwMٸsβF6&y̙3lJKD /st=sA~} ḼhUk藟+kky#w+TUU&tm(MAȞϰ[> b,}ɀȉspuvC_M^47:B#v/^FеKx/UUUpv&o۵\ʟjhv˕%0 dfLd]]P- DH -Pe27Te :,oXNMlj{ NCھ:U"@ E@DV!4"@M|.N&@-u0p/^u\!3%?wpuV 'EbE]:"+=u'}=^hA*27*ym?nZ j*izLuA{cs}C2 #v30 D"@&l~";V"kCM>?@J!"@ D`d yN & Ue2JK%Co $vN}s ~C\=q ex(5ոv ~ep0==|]~fZ#o;8h#h?+)9#SVv돻~'RŹ p|9v6=k> SԺSceD ,:j"@PN(']Y31>X18qVE&;D"@F,04 D<Cln:1UX>GC'@503pg,V#`"-oVeAۨmLݛ FMeoD@ca*L=[N`G8[ػx")z` `BLl~y/Z;v3͏?Bv/'-0[);O->w YB7"2r+V4̛uX?{XuB s{xL8,-'x&ˆ D`\vNu ujpSZ4Z@@V˒"@P&.Xͅ"0 KXM#PZڰp]!5!}xPª < 3!@lsf•"Rjb,Zx+lp"빠kȋNogώx`#=k.ï} lݦ iv,zYԨ=St?ڊbԲ0Qq8a,[43D|UMKg-BSm)=b '۽{t?)soj^K='mƠBLC"@ D`t ^=ttgI"@ %@K"@&ƆZ@]  H׶W&4`a〫BosW˯Sa}K =ow)L[;{Lٗ0mrdfł}#3Gv߮]c:*&ry<&#X9B#3=sx?zBaªWS588 _/vV{N`rl$\VTXVRix{e+͏"@:JQ_t C D O3$ DqO*jPWBBl8+kMO {.l_u/zzc L|2#P܆|7S[S^@8?ZJP~#@#00ABz6jذ~Spq,?]# nuv,fV3ҡhifa_/5ܩ#H$k#ı]_u(Xv5,Xn^0 OAYw'gW^s0Jan&P]^WD!-!֛9#R1 ;pkХKȈ ëwMr #VrϝfG D7:͗"0kW?hp*hvg@Md[ӲsQQY  N(qYs;PTP ohhiW_~}!|!y{*(4+T[ T,+ݏU9f8ft oLvݡ[줶$/ EV*:"0Bjkk! G:8{ ȍ*Iniб#^_dhID"@'@ 'PUVaC]IM tSoYqLζhilA|dx7pbZ"`Ru}xeHMn[`LLOz6imT40%RMBgV.qk8u/?i4\TQ.S" 펏zGS`}ꙗ勳 H% ,g*Z?:+FRW_'zm4+UuLI`M4 DIJ@m&iDIE 8t:Zwm"  ՟N`}tmx7dUˣ]((.SPY^ 63PO 'aώ 00…בw~-L7{:JsR0sў27fVPԁ z 4Ń뤤OΞ#qμlNoEn(aaIa"kpV?ŴY{%˔ D`|,˒@VwBȘe7bSPЏ1 K+7(D yNH D(@Nr, L``J^NJ;,^9ٙxS9TVW"*2,HPUW -gwO4U@K Bkd&c㦇 ~m> $P[%$'N޷$5ͅPAMwpԔ̸i&#o|wNh}b4wYX^fypIq&!.D`B =-_z{B38W_x?utgG)#IAĖ85M85̄{웚) DnHd톃N s3m> D^T|̥{e:h|ԡjUW\߁KoAoB|yOq|"RjDDC3>}&^?r[UhcqYspB@ǐ O0VUzz|^huiX횢RDzN!||<5RI.Tl3~77V-t@aO>@d(@ou3lҿ@Bk7$t455B]]j#X>|d?a,=2⠢maRXWq>l(d"@nI5 DOL\՚ kΰϝCzF&gӬ+n3ric]dd0o~̅ acBE@]EEYe~zy!]R KPBۏduYV7DŽtz?LD1F˦4+Lؿc#V֕壅髩38O̫ZYRIZ8 klcJZ:z0YAMmbK:=S:N٤t\q `"pPfQt:C[y41 UUU>2Ln ZU"H7T}Sf[$ikhd~ 5έe:"@!@VB }(//l"]!cO`Ro+߷%l౧ř"3dg!!&_9 )q2<:X.D {qyc~\3–Xcl(=ҵ[ K{\uB'D$h "@@fR< md:HP7BUKqp"k{Y ]6 dR/vMTAy*鍡i"@F@L*(qIDqyhD"0QT&D"󉲤qNd0m_a3t@ء3ϙ8]:v4eb r됓OOhI s/ܺrhj}r%r_"+'?-PhC7 GKl$>> .^ǐmtX u!PqE|ʴX66`V 6(.*t4s%i ![;R[Y8^Κ>щ @ue9l0Xb U"@@lv {L6l.t(% &E{1m\͜33V.:z78?LKKWf| qQX_p~e^EyiLڲa(r@3ݝSU(jVea &{=f%CGf!c7#'&' \!m.?yCW= 5h1r.$uoDSwϻ |tBpB•YHTA D"@ yNJ"D @d5XXvBu2#c{A]Q *Qyn%Z-(h{ :wb}Iۏ>R;Ȳ-`uY1MK C5tɣcLZ^I=f~G{W4TL@? khأ"/= :won$po*lhb {1 L DjK9uKVԀ"@jJ @LBYl+}Wȋ6 핹n^sa gQ 7ZL|3k.m?mՅ;:yg L<.]$/W̋ci-b--)7/;GU @Mq68֮)&6;wtYsy+^PDFF{3sR~rLW}2^zm> aX)"0OEO\E~nZ"N ؘ"@ DHd5#jA$$ceJD`PGkGsO ǟظae-PmGNz"Ԅƨkl!8R=X__BaeTr,\?2+ʍ>~?]|]Ld^y1 1WAC/̦ ^|~~n:شqZk07/?lދt=^+YhhH:y7̘9D }Li|VqB4p D"hOVG4C"@@mU!ln;Vti8=L'ơ^P;gJRpYR#/{N:zT "6>Ά0-NDF|7?KUTcUpUq0'(f_z܊;zEhoiElT`{ >sVAl;qm,Nk_X.]VdyHzMxg0cHS'rhk<3Y\P.¼tp"못=ҜD@ _$pz@K5\7X΋&D"@z '!DLz%/򄖎K"\/3B3B=W5qM+ }-T <.O8:< ʄhƩ_Eq1ʋ%YYBSӨORǏWQvogUC;y\)]&>N`͎?>/ Êkbl<}m7qҰ^wzkKuCE@CCYGg`1FAOLDl trd~M1xwp|^"'$Xp'$\=Uxd"@&65 '-4 /V"._D- %VW[5&>y96YKw\ [p!1ܾi1 yd'~%.yߩ,DbzAڷIL223ygy[$HKw}BYz8*KQA~ SC`JJ.sny*J`=e<p*DQM /K-T\(?L::l)=ǘ?o51F_G ˔ D y#D"XG-!]:UI$ȍ[ײu{5 Wo^ʊz~(j_;#THr9ҵwŽݖfoᏃq600ۄmS`}MPkjyb/S"+!o!琝*xM!>./uV).;6 v}&b8qa-$/1󼝜y %Ym- w'#݁=Run58#i3٬iD;JႻs3"@D!P[3 _ԉ4Z 7w OGL"@d!PSSc\U5A E!ge.z-k\xX`|4TW{ߙDPCSC$qwT]7hU7^Of7_7z7OE)έY_v}mPU\Hl۾Yh`_\1|cD#/zzrkxe7DOf Y͏NhTpK)xdW\GϹBTXU҉xv~41"0܃8z7RԆ"0> %DQI<>WA&DV-!R^pK[5D@fI DqH .1ť0 yZZex{y,?P& |wU!b'42E:F-m IZ<ZQS^ <3dH`{Λ?c{Ww}w@V!-< * h7ևGb" l2pA]eUƒ5=,V6;hd"8:N5,f#*&B] S6UkCi&lll~:046Ayy /jj/} "nKYWOG7-,m'n~+׭OUWW"@d)r^ڂgO||YA -&"k1\251gΜ֯tA[ZY^9¹lN=8Ds]ǻcO=![g.өb ܀#$Ζig^LdeѦߪ}EjZKg(D`LOa{-.ײs_才}&nY"  XCs^Ö 6e&g8>C ֮r{xХBTWYNLL*-}{ d)P[ڀ>JXN\咡qwI/ی۹Rd wT1[vj?B4VCS GIj24Q]SAomFFZ<\8ID3 \=*>OXkbTUSæk'5D89o&IIR"S3'n݄B|G  t71a]|`膦6ADT:%,vϷ181b;ԋ1:SH_ f i TϢ7/lΉ5f!ra9c22k]5ΎV6qA <Y@@ዏUvt?aȳ8;䁨`=? [Cޟ4%MP14ٸަL"koT"@x*#3 I`{,X ukw^HscS3e`?anOSfRSpitTY=Zja]p!>zu6KC%,g/EAEU T;|ǃ:8:`V@ճ;W{mmPS+] -2r8y2`=hm,YdRV n1Fpf6m~Ggjj‹ʥ O=#7ޱFVR}8cw-OjSקts0vC:tՠ!**63f_{f'c <մyY;KE `Ű)<݇!b^\hgJA@`bM`ͪ/UvQSKܵD P78$?N糶owk"&`YT kdYك%z0 )"0 i:/-mhE^4<3}%~ !ڍ0TA?1I"0Q#"@  Һ{gFF lZXXhDئ*v3FT|4ݻGѨhkB[s54F.LP#t@:T;]в Xv; gC u-0f;P7/{x̅ox-5HAy fa׭_{Pvk0eN\+ǩjZSUڀLI=|x " FzXam Chhgk0\^8 6*b)MVUmT[m=#>'3.s`ϩ\*$3v 2 "2b"Qw17ElM -]35g^B>wmϕKr4=끝?")6 n[ѯ* \*,(wl,[<Z&^pdd7板U+Uё"@FpVbmkpx]Q' qaqnurQU]V: D`@*Zz~7.(2uz$vAe"@ $S!߇w_=Ko1[Ov},~3-MhlQPgto569iJôQh'E :uUphe ?+"cX̨ X3&h裠O@mE!~t]*b']up񇵃+Q[YIJ%LbFT!'BCl1XϝG)L@yqJ %SVÄX{O>jK-B۔:"aQ&Uĉ7u%ġ74ʉ%?`>|3'yquִ݃: K+&Zo2*緑y kqzKU`xM{˔_ٖ%6oFXXR6,[sn&^zK{qm4'"0pM`c{U9uS<=_l.*5e}M[5DIM`c +Ryl74=#/N Mŋz}8"p3,pYYUǤ $ɯ߹n=_~2Ց/v^dV8yہrs̼=o""'a&.l,ƣ 3Ev:'Z)"0'xL`>P9 p\_3 rΚfEb/h$0/֞/DK \D i*SY6*5ő'"0y <Ð h哉@aJ SSk}/acG`^غ{pe Bcw>Ubia+ s6|p|s"2:s̑ sˣP:ʷ-'>Gz~ʋklJ8y_nj+a FjV.Zs n~b\t6"%Wڹ3p>,=XػwVY]>|s ϣfhj`qIw8O҃BWBx/V*R@: Iv vwлS箆!-tŶ^0,[v̤\3kz:B|~~>7k_z$k/DT5V\l,!ޣk[w>X{b#"tKD'%`lɼ޹L"@N?_ft `dMv|/]l޹PXS.}P[3*Kss-s!ُuH 5F}]Kota/Da#$D`زe /N^FnLT}QAWĝ6|`me_sƺ@(lش#w|c'bu[mFPG=-Nܞe%vYY7&$,=n36CijݽRY v?v%&٥7Dzkpy(e⪡Ȏm  ~ j#:"}~z4*+ S\WBrV!|v<{0*Z+W+ s+QXcs.9WmeYg/`@X.n>w^Bdp\=ghB0-Gv7o[&rōR=VPV|߾z02/`bҺk$g 39fVҗ5C D"PZ^y3jxY}c"S X߹[355M~[H"@&. u}~hD"0Lwo؀W}BRJj̚%Xʠ^`MdMMdJ2!†ƦZXX[+'rǁz43tX1Bn = ʧtV>OJCMy1ٹ:ξ~cR\$``䎊Z8zOE+sjd+W}=,ZB EsS M=N`lEFF"`,PSIV,rsnhhׅ#ħ8#aE0049Zf]ss3l]f! PWJz5e}7ߛ&z;6r\A!܆,3!xƂ0 LMDYqQ7=?ènc@(21=wBL(DQ~&WrG\d7B]ML""@P(g[Ɩ._3+@ $O ͙J`-qyO??1ŢҘ΀CAv0"@GTc`=wo&;ww"ڷ <_?c#%9?uu'v}KL@h4䯁Po8 OBWSO{w|_ڜpĦV'- |~L@AB? x'wocx9Ⱦ VT?W('֔Cϴ61X+k-Yx*5?.!7ї i34uybHɋ d墭 \ ;uڌ@GTBl)X|[S%wg #PSRGDC~k8 ۋP j;;1ϻv|H9g^7E 2#:D602A۳u?r`kga{v9y9g8~>7?B#cö#='D"@ D"@;fuU;‰=bJ:F'ð}Hapڈhi)pC 2ﰵN{+.^:`depK̓ {P Wgascud],++fפ"L!33%uLJ7ZKH8w,m`i]|U 5I!7`#>?o~ӑ{I^`:/B]cR`ge VTT'`cˇVyXXNꁚyM 5IAp`jp5IJˠV-mZL`)(glY!Oiϡ -+)};GPKEe,L&D,..\ 5f^*x9V.GQ%fW 51 ᵘ:ou&\ĩb }RM"S&=UkXxrwm-XxRHgߙ%%/res[Gy{jxpcZ1/~KKȝS"D"@ D"@G=#|&SQc?o&w*i'X8mJo4>Ԑ"0V~7$Ƅ"&:yYܹwQ&7' gYj*P[U ;YdCGWy5+XR鿋YXo* ^S뷟oD;֯?{@KN?~9ĬE="1Db{QT 5c6~׃w1VqN*l߷thhB``6&]=ńcE&Gt砱B#BM{U{HJpez3_N\ݒrwQ8lp||"!% p2 qS[v߅<[{hC8t:1d$9&vx/#9 !,P %q.yxÞ+KE 0w/#/;wo[U>ɏ7?^<<\uC)DE"&*nykhဘkǽ/imo+OÇ0a`oPxQ"@O? "Ѝ@ow; \n*JD"@ @EkuT ^ǽwNh' Q μ:=|cVsم0#Ѭ ?// w3N=5 ]d@IDATq|'+QR׊Yxի33-=&*l{6"@%VůLEM]&-" 'hWW4md(p^n10de%p)2vEFXS P9OyGo","1QK_n݉,,fo1!Z55zkEyhµs;yMXWm]&_NR_3 ƍ ze~6T~Zd샱x`x]Ώ25S1W~>ZYuta"A5lyiXo,ĝi&^8i\85Dߟę;7s NHE1C]}; ]9VK] ׮^AuuTSgamܵp/<>V΃uo AFlٖuAfVPW[0 xKACW =6= |  31{9VAsl>NO^99/y,WgHǴ{زu&r핵%}vo?'4tNٟgfh[ W/IJU69—gߦ7Esr2Js4%ci &`>*a3XDn&`Bw 4H'uCjA! si>"ShFK RQ_[[מ[ 5%&p!,vv 5/ QY;!ڌK}7;{NDL@W) \VJ'~gaaWc-L`2ܧ@OvsƼ[- pϟ!1 3 ,iVF& wgr֊bO$1Gu= 3n?ʻue_Myso'|~]][Gǧ=b{&Da? \-k/s/k )<w8r8^?q΀/^nXc]+l5S L8ŋfc<\0wv-'Q^+6`6=.# =4*I؞aaAX~K0s6?_|9m#Dd"+45 $r{t =p;쥇$vrWbX ;7 ƛd޺;v"**K:c7>lek /6l@_lGWg>o~ekgxi#ŧ :J{É7I<M"@>.#sʾJ$N g6Cuh㾵 _ SW@58t_Z/|Sf-FIE,\u6<Eb[YA]:\|ѫv8 S\]u!tin__7ݼnSj8/!kI-wVHr 6xyzs:TTV͏_(˓ B?!b̌ X^oBB\40^ Fԍk^{k[ ; B}| Mz%+VAlnv(`(2Q5Q.Z {wPY^+J^_m߆O;&`uU9UR! %[sк:#pPa}u -&Զ3PtDiq1.]m,e1A˫Zl%z9,Z ihgqyXD8,P>!b/$7L3p4c#z>~ wb3rahc8t.{}_sSsq~Y+֮AЬ2#oơ=7/~u浞^$Vnb,\>;[+E@u,߳؛0b_UXv޾X,֙LBC`rvqez^>WںBhI#qILwls"T]2 D( kw*A|msU nM(N`=wx{wr 06lU4&yR 5mfDM e"~/0}Y9z2qUӘQY{yr+,.Md9{I+@]y& +`bFÊ;ֆ#fjaӞ^~ /zxX:]Ķ 3o==,G3R]scKa^y{T5:S[o!g}{X}ތgK*k0pR+-^evomF"9&yNCcm6}L,`l>UO<*|}n\=ZǨKs '>\xU?.cŎuGUl?nzW $7AE)ݧ{CłDEy4@ i$!!el)^MS{ww9y}>9OBMw/o[ZHW{ $'g )Jr6ELV=Y +?-WܪcB{}:Ѧ6(<uOL q9DLd1$ŜiRőB WN1rUo6~yrTp@16ơk gHe䳏TTciBBB;67Cn[+s? XMH>{.~3C!̈lf V*g*KL51 Q605/4D;Ap8LmV6W W( & G`-Nh#'`LXڻ`$]NDIq1m8-P8<9P"6! z^XmՄӭzQqa9ՉL;yssچxr3Iq"v9ٙE.$Ģt5}7ril"~ykj=;w!묌geÐ"]43G~"zz% vn'"E4M÷HEBf3N&ʋ~ ,(N.\܆NyenECMԵnI}wT[V"-*+A&5&ɥxqgFQWU\,{erUzqdfeb֬Bѯ{‡\ֵ~kurՙ`#+ȳwƺ0=7soT\7<gkGHDsݫꆷ&v1u$8`8#X]"b͞=?ϱ3Uz!RN>.$C%VD~-"#Xk)wTRxP^VTDօjhin@-Db5 ̲$YY:55 1 /ŲT$^8yH""qxBcYoBdK&PT -.*2`F֒Z*wz*,}+nkVRm"$V@KOLjJ"t,Zz0u; +DeBB<)榆-ڄQy }񐿠L3+(>lFϽyEz$kjV@HkDӄ6roT"S?*+Mj[XAO Iu 5U()ļ ؈76~"s=}hwWio(}O=#?@SGK71,/ ;ih鄊yeE%Y ?' BwԤt(7{ځo݊!3'եsd?}NFvF2q!KU0YDzٴ %Y$DO_GUXBs8Gc53z#PG)q0ycP7] d9m""1My2 ֮k6=/Ea^<ǯG|-G#,,k)ۈ7Ҥ&mJFv%c`exsuwop`ݶm3-[GUNvǚ,1Xs%J ֊4ִdu:DXydAMq4ui#͓G~Ù3D.ꢩaXu>]M5{d ɬ;TlxXtuDyS`b޻efĹsɁezo@Y =N^.Fd^^ |U͑lrO͞9 cxi8M:(hFxZBVUp75ZxƥPǿކzptƹ7Cw͸݌܆+p xw;8ug}U_Xt=AxZ!!"xVbscM?l\Frj1Mf]F5aO8}w_u7ގrE]դCvfzݳE ]"l"O>~ L*{LyydUj!ce-GUSAEK 2byR}ۻ[aw{RxQ(4uҰs+qœw܀5ol$~ bU[~S[#+Jq=rPPZ _/dȒٙt7F0 p&'/:g<~jJ"a;cKQֿ 73hKV^`31suXڛBY25rZ[ϰ%IQ^Q˶,y9"RJd3e`ݲu6?h0~05,ip8M~.L.I<~uew(`}U5ZL. eYWQYV )ĕ>+*: <0߯~88h",YK?Z_ݩ=*4(EB5h.#Y;1cUtHbw)G#0$&&ظ6 CoAuQĞt#5f}p%BSЃɪcL5?+Z<"}{W޽(.ʁl)|C5+4VWɉ37Y ]5ɑ!%Ȋ U*R"]ӧC-H4Tߏ6ASIțY݆kɽ5)ֺË,A6u u1X*F&32%B.W4F͑v[KeXVF*ړ$_gͽhDq^&J7U``.^VTQ5=|fZZňe7ZJq[^{"+; Df&:Ydj+D6;FT E;A062FYA>۰ wyoW3MtU(NlAbѪkhJj*fڌ~K,XXjSW}@"p|uyRnoE]ܹ My55Bb.spFSϮ%1K5X‰u@djb>21,^# ܼ| Wa%eKe}BF/Y>__vNҢQOk. IR%CzXyGߋJƦ!+pnmDNFjJD Ǿ"$XpzAසGV.Cq1:zPSds@xBc('q=NecOˆTĎL>/ht{CTuujϘ۞?O%XT8G[on:ģ7ƂVN?r٭a= 쯨̀1%`0dٺF$yJ//"\1mj ")";!b)25J<-JN4ɽpl|^ VURZؠ(=܍@ȎU3#ϡegeKճKeO0ͻo}u>+sd['^P^Jr%҃%=XXc#*8)nЪBMd$'bڍ0 V-he}4 M\ad)&YJޔʚV5@+$U˯.F^vY߆h(-MCK]%dLq q+O%2_LS&f8{* sEzKniDnu02Ё)h#W;w@6†K~O*dJJʚPR!s\7߀G3ƺUUą#ʋEuc?skxB /7 UU~]e>(ǦQP,1֣:DyU0ŗ M1(/VqQϯu%ӮVWR.qEl@Aq1|<&$̆#P5wj(J3OXWRg֨kRɌz-xG^Ui/)X*dV(+PgY>1$x8ɆNTzhl t98Nӧ -xMn\&Ь/"7mP5EBb"t.d:ғb%7 S"Uf̞VA>o'G1FnBeLo?n?c/@K_L֗ >/dĄQ _0k2()Mr8-;ň Ɗ{6]Uɤbq~ܼp$kLu)ޮ޵$hUUp[s5`Pw]#de.OLLſM5PRUGUy>g*ps r!")jZdXBHdž74PN!f́%ŝU_#?5JpߞS,)Iƴ7Թxܴl%45PTP&z;OIokq3 !yōZ@.i9*2 ~`}AZ;[\{\튓&U 괵Ԣ&jߔz6VCQY]w1 %G#0Zq39 S}~7tNmz=FQ^b"áJIp gCOBnf&T L8{!SÔ,~-h ||'\N^@Ld$+pFL4{ Ykp&du'q?y3p8G`,!g\4[9-Um ?]aU]uoJeA, &!D Tܒx1G#0q` />0!y}n2d\*+VYHkZcu.eQ@0y8o>aO_٘Dj0v|@S5qU>^xrꅆ]Mn'R65 :ʬX,`%bfܬLXْAيuPllma e}s<w1*PV|jXnl* Wrs[n[dx?o(.*52ek'r]+V_bZBxix6={ YY+ /c[_̙AYl7[pܦEjN%`osz0*}JQ\ jػ qV[?>+RN'4/]‡`7n{O7mg0rAT6B/ͻpFF%@~ymArb <ٳ>r#P"XQ/T>S ^)D-ɓϿe%u(qŃރc,,e' pOfKQl]-m a5aύQRSQ &J@@0Pz^x UHa&V`)#%Wם'zf4\r)={>[Yi7x6ٰ/\X(π5ZT#n+*6EuoDF x̸UPs۝wwJʪJL@nm}L_aF?0C5%}+2G1sU +p8G#H*yMA*4D۸^4흧;y3htR}0%F֒"kUdHC|Vk;>+kA~y1nLq؟g"93;WxNogtf]WNW`廭x/ڳY~3r+oIeڪUSTPQׁ1QA͏nGrά(Y#C0}&J`<75)9#Z+l1O<[P ~njn38:Xdy =CVVFJb"dsQH87q'‰FaQa;dUIW{,X9_x=@ ,MVa*K̋DD X>dJݎ<&k7HxG#p:ؿ{xxvdt9HK! ĻLPxKn.E8EUe9+o_vVNZH_{<9ChAt{{]Y8xeA fo=D؏a+K '{bɚdF@ L$XYY P[G$$y:`ʾIM+ Uz..c.'N|.#ްnhrez52n[Tz wz^[&WZeM&d p8@AAA`/Qccp_pq÷sti'X% 05p3p+a Z(:!Xٙ\cv5=)!h̽$} 7ļyTe?mǾ]p 5.r!ֺ,jIX9Z:́3ݢt̹G0MEo,َb~`މ1X Xssue8[N__6[OW^$-HVW/9d;1Gkhj,xd%_thL$"hYP VqKhjj I&p8A PRPde {̂u",1\m|ա'А.@7Q'Lfg}S.]]n\Wߎܜv+;RAQY99򊖞Uykkshʏ@ht:~'țѪ^~ZA3x:{FX0y3 z$X=9@`׮HIϔ-N 9[d5WROKq|a?Z:FPUׂȢ,_Hލqx' Bc9+ɓʍ;kZ-ԋ;;NG-/쎀&нS#W~S`=k3Iٽo.bxb6T+j?k*JRŌ,Xi񄉝zk#YsUkJIq.GZIYHUX"%XYV5׃%y$:BF6c)Y`scfE0GGuCax }ϼ#YlYu*4)Qq!sSOAh/E}EP!'G\KInE2` G# pq4E<^9ͺ.OO*,|(Lyq@IDAT8XaܪNOo"KwzhkOO8_zG{;-4U$()L"`*4c>y/X~TCaq|2qvcl&WUa<iByCɋQERU) ~8/bc";m86ݕZYWUko"%״pI)&qSp[4D&MQ0q髪P^ZX@%1]y L.$X**ʈgGgIqs,H$}Ӫkr=.qiWLb2n o(s&,YuIhϘ;!bMOr!PU] FPha֌]o2p|AYn:/G]=w#y<G` d ^pFJǟ{yt찘dݹc <}(FxonBEy>ųx*--Dmm9k28z5G阫{6o3YKBiWw#X{ %_-`*5UXij}5W^xwqKim3M6TFϺu=?_Bgڄw }UV\,^55qoV=U&INnڄ7{gC jM W鷨Q.bꔎBEG%~6=z3/Tsp&&qdibe :j ŋApF ċBߎȊ;b57DTQ|μ} j4[ 1( u%UlOϊG#0w#{^*X0LOGt-ֽ9FY( }gw,.X(<#$FϏ bіj!0 ++sDtj»vT崱 Tj"py5W/cL}iI!CC90qBym%˪$5{iTV6R"=-U5`m+q*j\Cˡ~o&-zdo'_h-9xjmMH师Qh.E%J[IT3`e;VJBp;'qL*gI0'*RxT֦a쁫dą#g:`el?u}i8:7&5TW>C =3aa2O`!}}<.rVRՀ>ر?u-Tar58yz 5PSt/>e>RjPI EZn~8,Z쪄zw+'alԵhTu 2HNKT7wһVK HLL* V?6O:D2q]k_="/̔$(is w_IܾgΆh`ؾg4 p\K" 3;\17@y\c=)~zX4DfBd1ŵx(4U@v@0{x Iށ&DŽY%w۴h6豜pzCqqVq8À@#=5_Ƙ5 *G 焞L6F^u+c$kqnܦV33MWA3;c:_$#;3 I?:KIJ;88CYVkIIqvr8B-`颅RD+'Y6#X{}|{8)sq2Uede0psz GvCf(΅Ο ÙW'6OO?>.RnNJ[V~Ale^n'Wê{E046CSbxݯThkizSPRp5h.׀uՆ!Y;Vpj`UG#00u{w?(&~W/#5%\4ۏ+b)Ul] ,U݉ 7gE76o |-[qR`mgx9[ۍo}-̠8|G\m[w`=mNJIt\nyG#L<q8D<Udu2x7ʈ6:BO V~Ԃ1TWFCy&LZocL{M- 9H8}PE suM`pm?aN66=mz,̐|::t|oowԨGc`s+RHc;E!3V61ۖYё6QNJ .5UP1{#Ǟqbmayw!gdy+Npd4"N)/إ%ńS1\&%)_KoX˥eFuG 1<  O6 ,eVX`2)*fοo r>R05AP7qAVz ,3Ko\;_FFjj0{ܵy,~@uUD"t#p=d!G#0O` mE+ ҡ3Ɋν('`nݽܱ\t@v03Zl?F2QS\GM(;U"]AYp0.n£|/"ڈ|t)!n>ӰEo$&`o.I X& I/BuM9s 6qFA^N!5M]hji#J &&zZ@L@]M54t[elyŮu?G-fkV6j(h iLMfTHGx ϟ?s8C@CS~ՠBi sXgǰ404wkbbn#ϵv7]glmЕWz"Yma dVl*ja!~liUDPσb KEn 3KhҚ+ :hܴr'wh G#L,E&' 24 >G`"nÔ)2j{*K``՝ۿeK-4[:֙yǾ]:v\dG%k^< x" # մtfwf 5"hjJ`ek3f;VG͜9_}+o)4PR,ɏ^s(@MU#2:KЦ\[$[S^ZtZMT KssI PZ6Z ʚhk yHʂ~EsD. ƁgDn:*/F`QĄ@CUmD;>/'ʐx"YwY曯QT\ww13tFDA`]p*bk$RЂ =5vUokkBϞ@m}EG&֨EcC=T4ĔZSĆ.z('p8B`dV'f|68Elu`͋G xsc>= bevK'gW:8a%ydh`~%SY+q8 4NP7sGEM , T%4{fY]==.n_-83Ү\&W9J#ZeInz*XMy?*~Iŵ+yaNPIqy. }=DFn|bff7o ,X}gH֯nE_~Nc… KqϨ+EScuTtF֢-xaW& $x7'G#ȏ@U%T^0Yr)d&׽ubݛâ)}pU2Ȉa?/ߩ:.l!Xu*x{J1q_B󈌺&zOZ̜\|G"X7Hngt:5Η#0`@7e v>[O 1(KV溂[Gq8Å`2c}vK* FVdq׳ūTqt)6RVUEȁ #3/o' 52X7ٹX;`:%RI%ݰ\Ђ\rb6gtX3qA--WX飦TmзSR7G>_H&,%+w)mQ]W.nMiY=Κ\Y)J:K߻spp\4sMuTҟS!: QW1oAO4|iKqݲ[5wûW,j&F"k!,dSP}jX IN%5t="TSզTu.DÇ&hQsgOE N ,9cRT*הydQr~1k2G*3ߩڻ n.{XR6h'Q0$ħBNB4%W[ߏb*OJseIt e&KU-DlIYisP4PRRԈ< (-{qb^ J[[W1sz1NJ=W%*~|榖nF+ߕ$4qkrV !g zo_46` X}\OD\Bpe=sbk[w`ʴk) #+_~AE[Fii=Q<]N 'ړGjla7z̅uɫ !sl׽fz~KG|| m$#+# #`LKeWw-|2fCPƖ~ƾ=1k/$Á[AI@su"|e焱NkWcKO h" *6xWZ=@r:zpqhM1w⹔ΐMrLlhoYK3:&/uxٚuuMr5> #CGT X;odKX|xIM!^+o6+dualツVGޭq;j*L5Sse&rЦm9f }s8BGSϾf"HLml_X"Zh+,u#⏣>VE9mVVעf۬ɬCOBaz#dLxCw!6>qzl0'oG>U}Upglx G#w#,,nQ>4zQs6|^/kO5[>ۄi=o}e}e2+@{p!,?Y&zX|u*}LvwqW` b^%u!̉,*1r7 oCYw/e7{G'hkjCM7|ePi6a*vV߮RDkTDRe?u^# \Plh8v~ eWaIqw5Dp9.ZثY/' nn.W]Ym JڼDǢR<ف%1PԅĮ_b .Lb.G#p8qÏx䱇qw>MG~/hUwJo0\v;2ru8ֆB+ iB\+@VND1zմiBeYjĠp7 ֽ1nV6QFjkμG`!Ī[/54UϤ G#p8C@G*rI\NI(>2 `ͯ(И/l ʥt&XY+FD&0,%%¬LeJfb)Xld\(I"W ^# ;;k \@:74FSSs`jf*Aͧ@sKt[P_S&aLPY002C]<I[Zڹ?ڌ MI$l5CW:(,ɊTb\M?9UҬףRb|x{+Yu;ܑkGO2CT`{nXf8Hr>s LL(kxEeky4e'5,F> pG]U5*[ʒL4S@uL ,,))tȶ<ꠇRdŞS5:[ZX4S}=#1pvHgr9ZJh:\BEQ̦O%aa!ojvhѣѱ+`$.c_2N~HK U$YMe .p EU!I G#4y:%& &z;WM- 0J 0œ͜b[>B%`Y[YI z(4`ۖ M "6X?eSkeZ82"wVE*Z{]x/ #dL!NquL}|0Ƀ{ {+O#MJHV.#/wBGGR-Fv{Rg#?a7G!>m gC Y|;By\LJGEG^n.,%={riPY[ 'gOD ؋p"eEy00$MJ7?cw.wl_@ RԵPR\Np[m>@ue#70駝_~',3Y`eRQ궵5DF>}E|/BkvqCm5/DG (j yFFF-k5p٩У ?/Xzxbkh% xbF~1ΑES'M]jj21mZp!!!XzuGvmPGI/8u͍5Ґ(x|l+ foqdpm@K :6:űK G ai۶@pP =zmgk ;Ϊnǂ q۠k`*NC@xe9XtJU55iCΘ+>yFu)|/)\[( X-I}xTg;d"w7x H)*ۖ.vn]~Bmۭ"HMd?ߝN <7ΕOν3=AVw,92LL ]=0Q<&EjjꂗDiߎ n|F۷Di05&碁[ 9K,(3_M<\ops;Y%w! gLGe~2s1mF0,lQWXOށ L: :*8wg[,bkm]v9,()-F_֮dP*)LDA~6VCֹy˃c7jGЌpr+tt QVVoFz `)`eן9~ =V޳S."PQcTC>w5gw'h5jGKH GΝr5  XIj2Y45B惻& lMY/NW?t(;5?={`y@ Ԃ@@ lm`*[(Iks:B &#_ʒRК~1~+$_9 R*+-w=߼JX{p׍OEAR-h>SJ̬Frmu#yϗz[u  : Z5xp yY4NF@֌{!T)ܿd%(Zǁ߮Y/ջBwz@+X_}}t^E x6l؀BL 򆝵=T(pO2@,V ;_9|k")6:׬?aTHIǹn45!l5aca'ϣ QYA83R[h(&%MQK Jˠ!O̶J$+7 5E8 yɸw+zopB5ba|mV]ƥ톼eMx3 $,6~|YQ. A4Eqf,MIP҇¡ڻ]ˊrq* 5CO0$Jr<\`> -(+.-MI,"-+)x&,,,->mQt_,Kς <b:L}ZmRf~X4/- U%:s~pH.h?5Eca%HB qԵ$=+CHhZ 6ZA7iO455jd6= Z׍p&vtCZK:L!H{k={Dak}U9iz@ RwwW Rl~Ǟx/# 'N]ew\Ƥʥρa>4һכ8جqQ9U`a6! Hѝ1w&! d7|l$i>Ij7 %;(U(`zw1֥Za~y#R30*m54#_W*8qD$[} 9p6z RD~&C_؈#'3kbB?FPN<|'{X6FQU%ʚNyq(Ix-MzKbL{S 2R{VKy@`B}mCw{.R/ b)Ĉ~M ce\9Vsal ^fP_LOGHcn>`1z[Wa~Ν!0jDz]=N a]C3LnYʬ_4R 7"%^yz=f?cw|i+AffdϷi02!`]Ul}?𛵘l8}:IvTêUQ_$7 `UMO#=%s*g$}'9о$e97j]G߃zwstww932 |Z(LnSMcI$4Ķ9|x=pG{@D`q@xD#ñf]褱cu ޾uNm Ѐ v%0vbc ͊' E"BVUb h$aAAla*x `e˵L.oNpǻw2 mEò '͛ b,`G9Аm[\wdK3:%ba=LN1eikhWdw~=0q<5FC3X}۾0K|Ӈ@ma:DzFDyqT$>}h+Pt+WcAKw'{1sk ÿ_D [*KTW7=Ca=TfpVKVv#ˠeJhr뚰1oNyQRe+Gv D"!kL =Dr9ȕɱYJФr\O2E2U t Tvd;M0qGDj6>+X:C^SlQ~OGvϚaCyTGco"]?Wd$km}hZAjm8+] ЦABx9nS|(N }շ`*D^< +{iX[ 6 1t7}>^x+`CТm9Kh' .' TR Wm8H?g.DV":{7İ '{Î킌ڸ`(=}$8l\}l'/_m_m|k['|(FvZ4w)eLm].Ƀt$&+ˇ\Yy:Gj_9@] asއFLA~n<NI"Iiwo_/6cr 6 UMJ% 5RH39:)1 Ot5#)6Iq wt;ܱc;˳ r ^v+k/Gعcq9K)i&W[F[m9bRdp M/&B5h{ N.(+- 'ҭ ;g;?|],]_.䩗Gb֬y:d"'TSW a{bb jESm%K(h'deuV!n meFUxMq=CY˖uy``g+$Ęe K(ʸr^ )H{>GD[g@ZIXbN񟆤S[Hfvb*XRrR+[{6])L(04UMF 9AsO$G˗ՂPnjrjWPQV],,;(g+/1GgOj+ut_8@fjc PC@ 4{\)W:tήWwr eгseEwфhfdA@v5 =RM*&F ykр1[yA:.KC ` 9a>SGW{ TƘ.@g셦H DWA2c }l`[gQ`+޺{pjV>Y7N=0%pxux?Gy(=p *5 a&5(9qT:X޽*MUR+c*ٷ{2_N/1wֺ ފjZj\i\?qH44g  *c B wv jJWp#GXtYÖ-[8uܹCm$o~Dzv~t*`x 5- /\sJ<:in%PE%P.SNƹqKl通 ,Yr:ox` `QQRMXb 'eV$1 a0nވ hCKU$ut! *5!< Rp=xn100,A/s/嘬xij,*3#߿TԉfACaZ+Qi_5HGQ68z>T<'v Ʈʨ@% OլAw{ȒPDy?ʋ_Ѐ*Tv3ZPՍbTQ'[j9 %()/~;3Y0$.C=(H;_dJ7"YK Hi8}`44)oOA\6#J,Ng'ӧQXc "q100sAyA]&ԀW =`U٠+Îm۱{w(ɸDbڊbhk^~_6p K5 :=JSYTv1036X v(*,& %P% (1/89a@ʻ8UVj:76cAIb)KGNIj8&64if=_ȷ@֛`m-Ek]u k%U}Vv c+]MS_Qc `U_teRM?kpuqR;i=աo@;i7h:`7o-@m}]WNhh{*{y +-]&MODmu tئovc= *'aVgj-='Fjzq((GII JilFj)1E"~)z^75 \җ*ߞ.fd@UUe> `^=0½<J' ߠ갡 P Hj\V )^^z\;u $3%X3pIG+?cӫ#`u91oCB Iˑؽ'kW==}b/# x8q$k+@6?TenRb ;Kߐs pp6a]8e3=,f`B -f5SEjf!FK)o$N C.› l|fb}'T;؜X˅hVSAgm=ϝC~2 gCIT ?'S$$UK mfHkAtVP!Ё'#R`MyV[QZӎKgoB:@"#@Z}{4ukņF5a;ݦ3NEɑ0555#0IL߄D󖮆3SŇoaѲjLWLwGqJ$^~QQ a9VB+m fUR#Y'<6ϡ4/A~d]>l0gcc]PNI89@?2pfyho%_(Cϟ®_~Bb _>{vvYkH('Fuy WKbWD_C7Z+Hstk76Fk+W//wb[אǛҎ$M=pCnP+fx=< ?][VMdPkFt2σY'؃7聘H*|El4}\W{Tn@IDATjjf.O h'h|?JizMi.XS X)*QTbT k1+-&&`Tp*Y,-pU/Rbxe8v/ADnZw") HUIʊ=[bϸKYU^Bb9$ Sf&xghg6aǹkڛ(o|ICp1őxC@:Dhښ(Iπ5IWT"#=rIi<ΜfbkAaIeH˯DI*8XL#|p=DptM$^W)159E5 J`߉vY svmSH^ k#ockW|ݗhE+ `cڍ+ #b #[M=LJi>~$!<^6K `Uֿ|?/ArT vYzTc$;[we%rW^U3g-itT!*yo|CMQ@V.[䑃XKtC7I^[8<qv+Oګ⸜f'XF}Ck}1 pð"񰃿ÂC9Pzm4ǩ2AW{"w/.{!mLP\L/ĪUt\JsV4` @j*0ݖ+},X *aL7 ekh[n3̳ <\>7!"֭7xȡphohnM]OLv]^/={=LJ}&&ܢtK0{eʧ!Tqc׻wCJR:+X=7KjP,Bt\ V9'cޮ^&˄[9bH[EZ#ܞAӯJ oiNJ&Cbn d :(ǫ/-ԹيHȉÙ7?o$+ &*̛j6 ' M2zM!ܫew#I]p6WX Ye-1KmHNɇrʔdN]*dQQUC& hC'~.hd9p8}*DgNG CRrG_epBYi!4UI6QߣN+X Y;#pϽAS[ g+C%ޗ{|8ҍx@cL KKL/5ϽBU'l>k<.{m3;o6" byu!!- er9Ak# {C\{i ~XqIУ~ihPзwߩtHΞMXeUP /K0 ]Zf4]Kmu@gZ()*9ExSXմ=\A7YӜ9yi 늩hb*A)_TEt5,MNy1Roa N;·cHJWCqYo]TN۾ S:zLؘHJm4`e7!@`1 mVƁfXQn#=A&gQ>{>Hx;Z\ /,ǯ$onsh4qi!3%ul|VZI^d Z`goFp V @ͧ85MBrT+KZJuuZb&D\Vvhon{Z/vG"dfÍX|}|tb`gLFl=KȅC}W5:gN]=F4I4Y54K>^򕳰vAAY%d*16 b X܅9s=' 3j.MԔTTQ1پ}? 3.Dա l ,S@JUo7`A߿gNPfgB"1\953-91j(N=k{> nx^ߌ^>b/Z&\+$AzɼݟL,e,VyqȚ}lO?`000 ^S|<:-#!6+̸TR 9v,kui1hyET)!fFc0_Ra뱘<\Ǯ(]aFjq,}hz{m?Qbђ:z7皮NA갠;CJQ^_:diO5kZ%\~ɮmbM\wKٴG\V>Jeu03z:\%4Fc- w>LaVv =p=)>YG1|Uy. O an$9ǭYaY0s@j*Cd!xOrn \e˱C8ul/N@'yVUM6`U89\pgUWOs 'Pf(ӣm K+[Y *e+3{gQ2k-=ظSNE+b~΄,(킵t|0j#]kv6ž;(TrwZW=?!O$_Fx#`&S3I=U?P6ZF-xOC$D쵶 jGjۻX]Je#h< X1A$p5Ui>R x$"6Rm&^u֡ [ Y}#)NHJJsboJdXBKOj!Oe;@ lWNFKAM--(--A zMiZZ2ZaKd `eNaױq Ϗ1bYY` T1u|1_6Q_LRmmk h҇%l @A}'ΝGXy  duvFъHs#Xd)]SmD:#|sO jp^Ԗ(X{ Qӆ-ă9g xlj"p sg xGG\9=z$U0û.GnJ<̉ѫ'S|\Yby\T={`~K8' Ց7 `eNhp)6V1 _/թX h ޤ puWQS]By+Xg"9,\)ZÍĒlnjEܶSq'SSNՔ+ G/Ul{،> <ȟ!gꚊ D&^sZHVj `jMg;|+w"od޵ }wڎ4J=Jl\> 8" kޚ{sUڿÆ`~SQZ!;q&)+!%Z1ef&EYA&y!xc(9;~uBʷw/յ_Fmpp@WH]l9 0s wvunUY9 (|K~S{98yp7ܼ{ XO- S * `1Mp6@aq1"ȈAny=C;הίR]sb.mKqpu&C+׭w`ie Z[9ɉ؏) @dluЌS@HTKA hsm-[ZQ,:؄46$3N,U p9'Uxo!hTL1K-$7 z~6ddb'RJAmtBrA1S\5e+1?-%bDtT(W3j* 1^qX`ݢ R1d9P9TeU$;n8 %19Y¼=veQv=fkK(!yk g^-wI,D a$ߛ P#kDuiH2 @0+nXEf-| j<_A|{c +Digs%z4q'r2+MHhX#hɉs VM`-IL+Mt5CWU6z utӏCi_X[hIB ػ}@ ypn-FZLX]FH4 ގ'Jh^-M[Wxh($"h@ԍI8&>$Mԅj u@t.Ą !ǎ\$)=eXr= \?q|[QVOҹa$[ZZ{կ/ec_95pKW>W%Ij1l톆{SyyH/E{S=ISbp)w '->#TJ.zyro^\WSc~+`h7u3,_;#"/3jh#QVDWdXȝ&T:b|r`U7ZUy`sO")3NiDp:4*%lro y,<:г^,.hՒJ[ |^di=zXk{%)VV2xH>S3hU\Mʂb16Sv[؄ T壩bP#ZGwr ԡD9iv-_koR\B㩄Ig*MMKXg`eׅG\ds{E%T3z+OeDy=]ȋUkLͭPЊHDJ{vcƜcfuQC~I|(,twv"! "sqȥ]գW+5ƅ"X9ylrϺ7YYQm{z:/@*cY9Iؿᙇfo)nN9Wtw0O섋#\0M>4.4okiwJԋ1Гި".b䪢bUo%1=N=SUXrW0N CJϭڪ0CSc3j!rVwI(m3Bm8$aC]OYl `5^$&S8N4cHVe=wBWk< Ϥ`1g΅ `Bd< pX:C#:b[s7C2vqp l Ϊ<!JwM?bLư(cbqf XSԒ )o9~+kG{=Y{z#/Wy=Ipr3X\7:meT˪LkZZ)cYBFʣ(! 1hoc[^L6X hvYM韩 ɀ^xeN;p96@h#f.DbYΡ5+)$|oq 33vз {w|5a+:1Gp2a|$f/YsdexMXt#LMɀ,JKI߶#!/υX,yNCNv.*̞7/.TdΉ9 :kC7&` )1D*cXe obUBXdlb%zy˛Z,6I/UVW#+- GK-_gJ5;D<6u#be #+ .+ C|1r*p8iг6$zcfh$VN^;#: yi2%0BGU@@׻~ 0h!J4Hjx\+nqi "CT)m&[[M(  0n/" $6Ν'cwJ}vqRS)3ѨBu̝3``(Ō=ʖ8N\Uz0' ތk7f+[z3X{_\/ YϴrG䁇MA8z|ԆQ Xn\sa~GrR>Ӡ|o Ky=٢MV뀦rr•14i[(R5#G)W9챆zdL {8 |>W>+|K̹1&a .p'x޻k7 YyE*:ںhoof`=w ܊%Cp=|Y\h!"#<<iE 5vfp;x#6%ǃϽ\K̵!''?Xt5%< |ݧ!;zx:421! `}mqWAA.ڢTAǘףvmzAUefA[~ <6Wf.ߌPtP` 4˯{{'(85%:IH  BhJh,劲wX+)NSm% Ukx"oD{:Got53<:е_ 3Yrwί/*oze·w~7g9vjs겟(j`@GֺN@NT>/[Tσ}={fx1Y8x!݆D8vyQTռ:\VZ߶fbآA~C)g gu;sz1ݞVB\^^$/K,99"@|2p`sXXY\y:(LBZl "I6(diOcisuNhi+o68cVT$#/_yM90oJ?MA/@E!7񯃋+Abd Q[Kړ4%bϞw[w[@ 㸴;\{kr;N*+ybf/ʽԜ> nj S=`u<9и`vȻk =* _U=\ #=$PN৶=s&.*,UamЎA/6DUVU2Bۊag#NFBWGfK/Hl(&J(ܭrb@<'9Q uRV;u~A>魣gHW;ZP%n\ yY4Q!%,!^|O < \.mF]}N Ea!zG#RBOlPU OvԔd|\gU[WLK8ZKTΚ{"`c*f^|:g%#]!}fwX `(+'oKJ5))|_Hڴ~ o>sN1b|i Jc*;{`r{*W=odo dM"Պ"R7<Ko5)UÐTix}=`NWkJ|֧u|.BfB80g=(HICasJ)TO've;^bۮ`ssLB1̬>û/t>T6 eY4+"s9 eLסb= ,d;3 XmA7a+`fR7zةB 6\0H[szpbt_=A.w=p# @ I?zx`fzXJȋS [Gw0u(c,֧~zX1? aee5T1~yv<4$` @jaUO5a/pzQʖQ.S he Vr=K x [k򡍈<˸B7f„̘usۺ4k:+,]`A90-B_~Уla&GtZX[XȯlĒ+QNҪ’ǟݻ(("^Osb>0tXEpGh_2ƷИt!w݋#o`\Q u:SY?{W_ű~Oݕ$hbJiKz_jTJK BpC ;mww98n 2khU,`=K8훡qrZPВXUf?uI,MmMhʃ>nyFS ϿR*/"&],"/Z U~U15ǰX|iq툌ݿcyEr#ܧ.udJ [b\1 =2.*RVR7}>/W]]2c_*`#ظqnCK6N7g-f֯d%x:VTV˼ $!()kX Ң4V `5vkkjC& Xxv;;ȞkQ,*'ɬ>yLg{&-Av$+m;ovd]Y(oeee07;~>+ 茚hNC ࣕBZ@6v hd ;liqyj9ڧY;[`- H+-U`XFYaabaibu0;p23,]VݔQ_;'MĖl534+PutuGhiN^YxL8Ʈw͒齲$AWCj*J1c 2GM6~YΗUOAd[N<,~M+g `nfҥIյ1z]0 {.X$`f`~ KU +8s&u=]vľ'ׄ|Շ*rݛDyRnbyX1vb_AV`%*ۖƒ$INn(>?zh)˃Zί>2p 1WqԗXnvR-xè PH@[K٥8}6|Al?\f ωdysD,X1@6u?Mpw\z)硤Ny%%x{ǟa0Im~$%ܢ#OJΪ( 6)% Y[KK~>NPk hԻNP[*gm# 6AZL_nd {L9T[&詷 !1攐ln̦#LHL>|]bϱX{n²[o1)N9ݫ5@AE J*n;?݄d,!hYj11t \鞍O7RmgoV^j̋:-.G`مXe(/9Bke{ܺ zfs1f#NJcurnv`#4cV֘1g-`[rgdI6e4ϼly>TׇǤ@53F?wt'jmv[?ԃC"Tx%@7ƻQXV#@+;P?1V+0qȋ>g݂E8hۏ=0qFCE!( (0_'b*c]߼x/[Цa$zu$ h5UmQP^OZ8de4Z8Ш+F=C{PMQ)qps"+WNF 4z&$ $9]V'%>)(+aG:\eN~"8\TLyh T%8Fں ks^-ptCMi:UdrGߴt9xDhL>Nl2]72XE}Z7Wq"5$(%c,7YnK؊~TisV,=!~[&z5T]LVqžhcV V$?Pq GW*gNm=A֡XM^Fnl- ~v.WbuP؃x=ءHLƴX47@lo*K1Kܐ+\P2v$ehg xĤbO1,GO0)rC'*ssycqb͚'Ц WQVk$ME?~)|ug`磼Nhxhl((@```u/X¶X;1:36NfIݺzfI9%XJ1L76qJcc'EJuF< UX,vh(k.XƆF4@;Fl f@@IDAT틌Xr`v0NϫٶeutȂX u֪NUd0$mR^-ܐp|/<l0N^;,aK:q@hL2y h[jJ6טqHNM@ S M6ŸNSNXmmڰhuE0/?(#QPI՞44u`h$سYx/ Gߵ5U WKSvE]fbJiMRze"nJ7v~i_3ۆu'Ө"3Ĉ1c%2eLK ̂F7nGv6b,S!UgX|.Kݼa_K[**7\(b_JOrB语r 4 j.Or ]oft2C# gOY]&;] vm&4@c_6o2"̚}!AO- nQ:'d&rXZ_͇. ;3AAA/ ÿ|U.K'fá!t66 5`onOo=ejۈ%sw+smWSioɓ%3!S[#z!nkT2d*}r -pI,PUQ7xjId-%!Bšš_Eo(L&}u0Ymo\Ub-.JCXV;tL6CQIQ- ej0& *6S %nϼE a˖%w´ QX\ ?ƴ32=Nݻ :Ty␯PYu)5~PqǪdg$_TM,,Lɞ: ğ&OgYF`ZDHdbYAPD"9&\_L!4!2* ^=JϦ;s/$20f=b@.q4ڊzʲ*R>""+ac 1\=(eIVR!aM&,œz`~sS=%~TwYRBwЪX̛)~ΰLtiA&&.ʭy%8@qP1"5q) [afj.>*CAFUQ jK>c6c4)1yb]rj `HXC7:VMYXeF@H'ŌÈ16k;1H.:f,ԉoa3(Ǎ7ߌ¬T8hG*s)},j,ͬSkjAqVHEJ_N9k܇ּ$ʱ*vQ6ׁ" 9ܫ `ϿȯA8]`RTG7w-pXeC71LX&HJLW -"YX^o|9saō+em9 LК톋+Q 5-?oD HZ2ٝ'aWDT-YskG01Fm6TcLEŢ<^pg ߱2YXFzH/b:L6Mg'XG:յcV j`I.[D%0hPS.F/2jYS 5Uz&Cw٪C;Mkƺ,r={;u˷/.)|2raӏ,k.+F̱#Kʏ- Xj̤w5&ZXQ"RNzꙣ`#3L9r1mej˥Ә:"Џ1jAaA+ދ3m]3Y8m0a"#0TO*zR_u {,,,ׇUnKl:-!1m~w 1in_ܻz1v8u-|@tT 7\8U%0UN=SL:_>{A`:^0ecmI9ڪRs'L(7 ޓf99uH1bl`G'g\7o~g3*IMSWZl%cOIuUejbwZ`XyL 7-A4Ai_EQREZ cE2-+nZy3(TW vۘNK&&|֑Y_Đ$a(.73Y/X`cgǎ[%.l5 o/ ?Dei҄ p2TD{m < 뒴5U;Jėfmyo|eټW{0 }KCui*45)[#DÕ>y=)YwAmϘRD=bO};#JoʶgUWv~Z<2;g1?L0*&XB@3)a{lrvt2c@5כk($+ vU 0zÝM>$5/3/ 5:P)PP8Gi k 457CGS|ɱ6?$4!36 041\\#zhljh(}%\,*JcNt  b I_!Wۗ8_*'#N|] h%Y9h:RE2|BfPIn.6 30w,4_w믇1JeHMɰX<2aZ K/@NV,ZJ2N!mjw1՜=:^2)oԚkmh[Dll;eի`:e >'`8_~y~-KMIAM%EV87s8j=8}܅LJ@gԠ =!!dېѮ `1#EHŦ[=X{C y}{̙8t83CQY- ]P-;2԰):4^fJwNAuF,ȚKnB=rOq +zo`s\TS֦za0m&)[yq,F \fE{`v773%':'JM)%3w*%*EyYjAzesl{mXx!޷}CC{;4$WAd=~x7Cims&˂XɉàGL}|Z\;EUw-0b,ĸ]0yC: (n{{1cþ'\zM>u>ʺH)Uϼ+@K=wgxmr*x ؉3i+16n*/܄h}G*Լ`/eF{$Z; _~Ĕ3nes^!e_9W?[}c`|` n}nFg_|?SLfۘtǹF'TtrBI@ z%}sqx Ux6G%ǥQՆ9d|1c:B} q~ݧCw *?VԹNSf ʋ ``b!+lP:g$TIn. `0 [~FaH.yG۩ƙD%sLKܵ~֒ˤ4X7qDT/{5 )1CtX0ՌSOS&ڵӮ'pG7sv?R70R3E K8̻-θp/>XY[ub[OG'UӎLёf.X\J4&K]0=CZtX(,βaZ>O , s'ٝ2LKgAvӳ7m4t(WH'J:IP|}=߾탳HǢ#qDzqͫ2Ⱥ`vD0X]|&I(5k4@%ţ[ ӗvV) ۪l]G!++_- Ęv!6\U{alӿgj5%jy-%J1P;O|Rb[o^y{N>cH#>X`]һXr áC"حq>dI83h+Y1\@@504_}G[S8#)1X SS4Ty9o?:77 ,̅JC)uЧ?&j;i5XUb1 _p|}{GlDfr4ȊH7 ҘQǶn[ SayV9 \qD/Gb\ŦGv`W ރӌ;(d7 S'!""NXJZ${>9 bPωG}.%M:lcv":6͡5t/Him ޔ*AcH=CvHo}#w޴^vz{5UACba::?U0iDZ 4UUhh̆JƆAbp3i?';!*;=yPZIplo]b)CRSP\oWoTUddɁaYxx#ccGdx$<8xt;~=s708`֫E(ñ48QvN]h!*5=` &{O4/sğٖИ X]&7=HwRCaq/U8.ݲQNkdC<3yk(ǐ7\_/ݺAbTmh1Ӱ]sJ]4Кa.`ޔ[wl.򩓩&}?ʪ꫼W PdXڞv퐳 Ωo"X1#JSWf߂/,]al <F$'dű]?~-Y_YQT^+$Ğ)I R Zvn*|H럚/ ,ʎ6Xy#4ƭ2dmEFa yɴkb̤GY]'L1kTD({ \=XF ;l?t6?c.&O<vdRB:5BSմK%!G73Ε.KHӍ){[X<.cF{I~ ^C3a$cghObEX4n3Wv6M@b5K*=I{tCUImǎc΍tH ^ނŋϙ L*CXsXqaۡٶuV]U{l蚠\--PrOWX`p---tO6Ey_B kWޅ 6"t Cy[`灖|a'Gock ҂,D kg5?d#S7ߦsqhjk‚|::]J7Q>eE8%eUuSgdŖsS1`@*Lf/QTR`Rc&TojtO?.=|py1w魜 0BZ缹1h(r#4Nl{\ rMY !zPUYLמDu#x veYj89R|5APimרjI:Bk{ |T0$!=0rsP}fD;C`6.Vnk`,o '+#pr3>7U85=DD!>6)LGlN"=72ܸ}[-#Ϡ~}۝oc9qo@sWs,23;@ LpW5 #3FΛ"YE:S'(CORcQP\Ƹ>H?|XXcћr2'{!:>nE hٸ('MȫNstO=l7d$'ȶ` q@VY%aA8%Fz>p\2i\fԩH7/DY~"Ф +s6~gkN'p"0t0iRv jʅ9ָ{Mcm9k:"mS,+GOj+Xٻb]JZ:SQwJEE6&&QB{MQ׬'+=ury*!ᖇk; aukEldF`'kK[BF.LXǔcjI(`}q ~T *-ذ;~hSjy31)p !)b"p}mBf=9Vi.5wuλ ,^^0WV$X*jW~"y>{y!~+BNiTP3V[iq}.^bl,Z]]߾w ұݻ!0`#'yM:~#7Čr _$Q>|ʭ[rK]E'zIglW453󷥄rtHQy+dL>sYW-pQ(/-T ey8NO7|.bI9 6CZ;#t"ݸ tBWUSr%7K5U#Pw賓,!bm= *$J+INuײg;k+ -a;BiT.2N2?#7WuHD:W2el{tV:NniL=_\?"]oұ B)N,+e 5Fo9W'>D :"I)wcXM:$U3[Y}\zʪdڟs}wy:J7q6~f5w/ߢ{`%B_$K>oV_)mw7oFu+zӿAց>SO<6$_CI.j1KrRyr -XB(jhBWӈ?G&J.22sjn]3X_C[d0g+UGoq.Э_Y#O^eߎȚ͘Hĭ*r5nppr@mm+ʊ*qPՃ3N5kfM?߉02ATX'MGn,N;BU$˳v셽#wA [B05H![*yo<NDD'/c־@Vq4ZK:IHH >7g<_+rLN/7$pzyذ TSadcs{>xtU T1~xi{Wpo~mg-ZTn9F٘ ,j@3܇>gZQ0>2 Qah(HE~A qg7(Q0Fpd<QnkRzPW@)>ZO(1[) c2Ң\yٔ^uv*_F[m1 :5RK.ﶎ^GlǞCۄrMv&-~|\PWQ̸,Xzbx&ģqir;ް J*Yj)E{]ubuȉ(CƳuN~ݸg9!^r#u#,8XjZWk[c";)gyυ_C0N|g=)k3Qغy^~s7?HV;W%iYTТi_,Z2w; yY8ܽ}%Q^>z2 !@AQCZ!]w2[ =9(߯u.?H@rJrou7z'@' <8:tT}%tzt P]>$Wu[Z@Z'yŸp<ca'v:Tvff2Z[k 4a,H/*1S/ kFHe2;[;>0eCI{Ƽ[4>)K:L3зva<=l#YOU#0U\u7Z^Dl4TÚa,5б6ʊp& *(&Cک4ᕪ?pƸ``D$W#<"E98߇twÍ7,C=|<گ:, +uLM+J ,L ێIKGyr \+h.KC}A470n o'--CJIqq73[O?ӧLCzq  : wO|ר˰bL|RIG#kꘫvM-ph Gw(Ø٥h&ftT`,Bjr<4PQ-RTT(+cVxxcQUU}N IaMƤ,.ʥi,Y1LbD+J%5-) OR*ҿsiIngbu勆xu(ll+/cV!Kmu|yPXA8s̬4ܶr1e@mN3.`1qޢ eׅ5m;<8AXv G.vl `lFϴL: Ye{'moߡYRQd˳cчdҡ)7رc|VuqĬ7EF3GbU=D]H!x;ʤv[2HIuyhD ǷXPdAZ9c'֒{g!IO;@CO^* \ʔ VxLju2 vu)-E{n;XJ`}DZPY pVJJge3J99RUs0~J@_U_Lv޿*>r/Gs sJ_π[.JsgmIz/-"UT5zV b==st353j8Isc;_k] [D͑2[@AW` cɒaky]P|z$~nq:\_1^̫peN;tH&9}K{uX@%௽u: /Z@ΩuE;ͥq"6%hiڃ{|o86Kvu^ev6 lPd &QūOQ&|n6k*B*$i@u4t?5 a`M}M(\shux-C4KPVU )9i Mՠ͈HGZ,ԊP8g|t1&p5A~vT| Ǯk lVݶӞ!gO++ a M^:$ܸr)~,[BΊY@\"o?L~᪒֑Y/7ZW*N(̓'/^0zs&kuPh1wBEIl[ *]^Z&-.RRcNE9=R3p@^N.n[u'&O9Hdi;g7qmdX>-KXyoeKutW^V{QE'w;5ιͲ`gg׵X爓'o0lj,F1EM,g$<+ | yUƵ]ݫJA[4 `u9ѯmgfzJp=7/25L8YyCÎMt*٨ 7nx)P]n)060"h<7@{=(#HX"LP\[}Uƥ„1RQG)0UJMDw hYC lLgJӑ{b抻3(lRRtЭH5λeFZxlXY%Jn݇t3 k(qPmP~M9N$# EDGyKf}t 2ULyIS1F>N݂ܔX5Tc̄9Xl W1ǥB7!9 hL6VJ)uk5fz?CFy **,Vz|Nt, U6}&`k7`Ϻm#5cS/~qGw+g!+u;&90&\0߯V,9!-i̓"U$OMMG y[@n"HZҺ(UM OO"ZLTFBqܼ6XƱ; $;3 z+bn*yB 9K`px_~ ,= 5`=9/冢%c7ʄР@N-W\+WE\qoíGAZX׎8iĄ1qP}p:P˄!F;T(NP>NӱF=Y$#h2,g$P T"+k&`+HPGNp`#41nsqVS,gm D' -5ep4oFtxlձ( B] LFϟʒt-C<-kS6X!6-t.ihAMa +j=16%UT"~uה,'#&:za{ȁ?ѦY\W[\Z@6b]a:6|_0=Ycz@bʮV,P/^*]hހGݺG=RaQ!OƑÇڊ6ELFɠ};61i/~y )o,1r2=v4Ȏsth!K>F,qv`xXc_z%%q.չ052**1065RNu (AKG MM0l(3VL[R"(VCu>vfԜa[3عxLc& &`|ƻL̠o[Ѐ/n2Usot:y|*/+7g:2&< ܁.'wĒ[Tʪ*< XD: |y[V'Ȫ>ٺZ04FY_+K?Nc>e6@{ƈ"(75'|"mhk84 xKK׼x7HF^U'.&BxG"*0R;2kPKGSXYq"~Ká,y}f2ȏh{^aP u]B * [َFrGaqj֢LJz)t )~١Ĉ8u,X6#TXh9]Gz|[z*;xnVJLqlߺ Nf|^ZӋ U" k㜪"a*;ML8t9_*̀ p&N?_>:<>Μy\bIve}x'QPP^~ >oj o6]01ǩ19:.ey-B*:#빯U"}'8i3mUŹI s#UH:IO[7|/: L]ASٗDXE*ʝpKHIaCWU^!inFAMqػyW<(?V~ZE?@u#bn \B^q?\~=CCTUW ,. { Px^XЮp3k=){h "BB.-vd ӡ°¤ItoO 'DALz6FOǰ$c2)To//$!>1qk1WVeKmֻ D@QFYfkk'j`k؂+' 4T˲0r@4ҫS-dqP-O$]Fb(0ʎ%9d||scPk-,<1t49d 5PRN&ӒgrY7Ug0E:W񫷐z {-ƪQԋ@IDAT1B_L9cEFbSoĭ ξ{[ӛvefa7_ˏly ,^G`b `Jض ]cJkgz|: "lX^cY?zGrvtdݾo%D]*ni-\)mz~p]CXL`nsNDɖ^{g\ůw L)k5䢆:t~<5ʰgw<- r \. ғwA3k*/kS  $ld>q51@F&:wM5[,zSv$u=&|-XDw!¦}2iL0טы,CTW8/rŒZFKs*ŵ jʈ F}k;CqC5cܭ'[STJtUlMGd8W#`hjFA^6dޠ'^}z#ArD8FsC(>x= Cv8QiDĆ\`9;XXhJRFT%#-i(Aˊ p2K4$`ËfqtqFfz l\ZƒpsBi|_Td,3PuI9?0etsuy>ǰN |T7Ct&9F+ωFz= 071>,̀3;*sN'f5Cξ}mo1CqPA}K`T`Q{&h,o2UERKY5sZ'3O#fml9֎j$W΢~(l< df碶F窧%f#! :uwoeD>zǗ/_NJ~W޽ku(MNz1y{P34妠筺2ULEUrVwcE67~D@6"1oRMe*c RHe$'pwѨ}ַ"v!35<Ï`߽zJX^V|BH9Ic=NL_29t<7)t_zVNIvr(^+~Ƽ$2~*I#KJ^~۰|ēxpzUS^K.xy\rɦxz0mt vUV}405dL'ό VWrv{Pwo~m,7ghADɡ]&O.x;x',zY-@VUE! ?4_ߵ5tQJ7cGT$L m=F*mE\P#U*Y:s\kٱY:C v0VBz |YUFJZ;X{B3Q Ȣ~F\͆j+J*ѕu Sd'CXJ> RՔL$Gqz`u[,ݲ !MY=R8n"Ȣ! s0@[ⲓћy9MEw{+zQWI[tĒ>:QzKĄ%p'Q1Pl"ʡP%7b]`~Ԍ MJ2dg@fL-?" rY (LԀ) egS i$^y]!_KCƿ.-)6tKz@q†a#@VkssY*]agL7mDOk3sBјl|Hݻ7!"pJ[ϻ|nn~j4 8x tiQ!p5g*Zi/{t#5U8F4wt#lf NTY 9\G޾6#DFv2U71+5#+w>ɡb?r3Rc-FpJm u%EX!%3 ٘ة&dNhlPS}B5 If3̤Zܾ<򤴭9A م8^~̍ɳyz@btd6ˊiYÉVVrD2ACO PXQ'NA>N(NC!lKGXPm`f~V55f~4$"z?5s'kyH# ~ @I?^SІ*~f8XDڡO?^d$%OIF$Z؄[~ak*MF*ESM=~"xd"c⤾QcGIQjiKƬ# E+˽W`/e+VCsKG1]T1/Z_ =prxQ9n1?֞V#. PsMAeqd~9Z:Md!}9P U+`-*gBtb#kT {cZbL+ݬlʪjͣbII ]=mR2ޯ,Mru<Tq +V▕ieӒp0l?*ZyNZvOoKk}_BNV:z9Aj؆˯>x_"9/-)GJ"eh'$dԖ3zs~^9maQ AR-u8:X2LX:cY5<0;D{6#P͈/%Vy((aa54HxxJJf O+~Ե! h置TҩOOLP֬pK›XhMhc/;q^+EfKs-23eo+7TV_EV/ *S f(-0mr9.KL3 ְ)n[ 7B̅myiG7EJz*7c\ln!s̘ s#Mh N< @hs;{>!T#7)}B^EoR*^U m\eo}{2 4xrDEF#+RԂ^F&rE;#Gpal߽u5x̂>L,m?Q1hĝʊ2GH.s(Dk*i5~`:&,>ܾ?iRU%'0;N/3$X(w`NN 52VbijBlb,6l`@kgG70VL(/*@dlcấ+hlFڬhPQ8AR7/%:+hXI6OsrĉteM3'X &&A%}>JP>';B+ÃJ,*K$y_/FVRS1HW4LlQ -*`8mK;Tu`I@؎*_c~TɸEJa+IVhojODqv_hCkQ "\Tcܧ/XϞ9b9{NMk SG.\:T', U֩cfz;M:x||E^WG9`@sH~ T&:F5H9Fu{4ԑX̜w|z9 3m-[7+Ϲ0u3*+W0avZIPjJTds;Z*AG[3??6WҼ$|˻oEdYCjwaoU =0&≧+&&CIKFlKLi-roO|5'L Q85wȴeTE@9pRIVxF  끫`osLsXuBN큋>tƒt;I^Xw 363v7**9GulR1o%`{O<+ VQZTIs 326WV&Iֵr`UʾȽsa ilk2cmrG%;{ۢ4UD+ >{ 5A|A> #ڳ#"jZut궘dm]V)n/fFn0@kި2!>_=JRK~W(()ACU#8N,]L5j/zXKgCӡcDhc , ̹`cf. u͚B?Z{#j_/}{ЧnSvقL .>f`f˗@g ftkkk13]*+C-rʚsh C1*p Ʊ@!O>Њv3"4]2;ԑlD`_{?d"ʫX5pÊթgS1?W56(˪P]KXiIUg(u/NE[M)'u8[*aL[WWggxVeSQw>W|XKX+G5ةp8ZWD+T*]I)kUT 9]0fm>AnXp dso0޵oxadCIMK+H!<+akއkp[y]&I[G`LNUvq;&:R6Uysf闇 uDdSliHUKagiը&@ZWY gOM?Vh(a ^*KUbh9QRź%Ŝāhe Gj~z4:1en|HJZ 8}ո8 "@[Ff2 ?2ZѤAN㣢TUTUT!9pte6Р6l0RFC}+lMh_L&:xh(DnEipOen-"hqJ dؔnMaG8㒱H4jI.(=mn*Ǭv8kRlMK%3+'Гv&PaeQjhIPIz?*mdNV ?'.Uک$ntҢ|c/DE YT l^lt0޻ S'tuu`c \fTTV@9-+nͷI]0ոz9z|$z"ZwY k~K/<#NZ+qz3`qIXb 7a͏[‡*[FF;p?_?|;+č7.;W$a5P srQDhpl+v@vXT W"0ً@4 ꍷo| }yݿƢ~$鑘GU!`jTH&_t7 u p6D"W{HQReA"Vї`MMMֵw=PXm^OGm0%ӛ@{v`iX\I"- U ]_Bmk!~3w0:` '凖*ڀAJM{٨o lplu ]z?V+ݢ itW:z%@N_-[H'[ؐ\mLa<sYRGI…Iet=^|@_WXOأ\(.mB Sκ=mpCi G_AM/F6@ $:~N{)aaHf] /^,t5J&zZlz5 "߁y7AZ dDQ:Zʤ^ohhjZÅQ/\XYHӂH>od Bg $՚8CUP:uSihtIeufs=FG*ʘZLւ-0wth(bakvP1`3hk/yl=Q3m545NtG X82Ebv@ie Jh/HA[@&h+O?5YػV:g=N_:MHWu".|?76Q'PجkT>qoS{.vx\HYG۰_C?KȋhJdы n#?dCij/Ԍw㏏U^ Jdn4bw\-Ul VLwȲY\J."#YaQ!bhIPGVf'm#pk9@ń_G[չ 7gTxPthLV 7d9g4z5@Z%&c޴`qrH  Q5MswEn~,]ߒnuh'aimp3~FEU ZPe}~Ͻ bN+ǟÄ֒bOS-=hw9cY[˷WxbON{.݋i K ϵ `9z(B)H4L9I+$޻=q2N C0Xh1}*ޥhD7Fڛ0wbWM~1Gbvp& 2{Iys0^{p]3+5iEmMm ]:`k9}pnvZjjMEg51/0-TCFT5!"&5jy)d?j{ cx$/ŠJ QۖBuWTŷ? Cy-AdYqd\vPQVL/#pfL{EXBw'C< {$+4z*{k =B6h;\T.*5ֆAN 0 -MhPo8ب@fdG p7ѦT_*0a!QЈ.I+4)!Z:S0wIe(Y|ʍ(Uե<U BQZ2w߉s<0︬;4<errHN# 4z1s}Qbll1!, fp7tV- _UˁR:lY&g,JGYy N`e mb<䦿K k43ZϷ7D (~'iO6ȢF6ZxLQ>َ(wdxO+>xz!lg.L^Mzn!G'^3ut ~5!!׶sÏbn pu`XF>x1zۘ߬Ee1I~21>z*u>%bqA;T\RaȃjMtB q%6y**mk3]i qBzY7>>ۙU=^]X'|]=qpNM0ūO>qZ%ǽ~#|4u P]M† 7jj*8䱓9ӡrꢛճ#2:%Z)1A Bѯ΃R/TaW7O Z-Yу"UU5gֵLso + swAM=ho(܀/i/g:K9F &}-e (P-kAi-B3ʭ̯-`D\ս)?$Ȑw=J0fJ'9f1 L|=uጧ K{$"맗硼"_GӀu¡z˥MU`f=S1a*{a8XGhQ{7 u󠋫y9uއ7K0}Rd+1w 9nR\EDI< 쿟|mROqI܁P`AAI̽ѯICFMy14[@5jIRلt+rŃm.f_=:POPŸ??wtDl!KkCOG"恪Ja^ \,ٕ#k9 ZY1>,BJd~SI> OQ}Y{y,A `)bwM |)sk><* ܽTQhi= g}[% ύ?Kc%#TPy~^&_B[G؅,*0K5w"5Yv g@OWعhoMGWG'&M$;NeOfU\M[Fk~ 4۴-ApiӦ-Lh:8XOIHĴi3}P҇͝2+2hl*27-OBo`6i1s%BLS)I"yGִD:AXua@9mr4$nfqyNؿ e´I׋ 5hQdc@vc~Xx -LΚ\+~9jċo]#3B>e- nNTDBS -0ڲ<7Mʦ(WGI~!Y9 _֠4}blNp5m]*mIPVi)nrFD~b‡VʓW.CEZ +̞:5>kG[#zWofG]17ELX6} <'Onhc{6ZΜ;CO^ w/ocq]7^T3gٗ_R\ ~?XH3#7+C0$z:|;n!UlHPB,*je"A?&ٽg; {c̅3ZsdXEN5R`0Ǽ?tf8|_ tXrp? KX;#*VWzu*Z\nϭ 0gov܈nADtN7X|ח__>XyA_)’]_ܳnې<ν]̎1)T=aX(N7ppP>r&s21c P'RIV-ZݶIq1n"U5΃8dFW۹g klD8CXr-z?y +Bqye>հ/> +F]bGDw8#,ĉk1}*f.mm S?ė߬-<~MX2h<[Zc\O1# %y8u]rY .. Fas+==<RpIio3P[H:ɡ3E=\26hXnld)L ?r+&D)q *S͚};$p-Caz@ YiXH_?:ZlytZ*Bg߸ƴ>m6ູAH*FzNJ[KE5eHL@Z㱇c_v+h Lo7RaB%F!:>aQAGS'˛\*-LT6U! P5s7"S~v;-sj;<Φ𵷂e~YV_*MtWրept(nq1'0u-\ YE+AT$&"_bfs+np4O*~9_,/G(㲨uMUii4ZrA6 ¬ ìffX-b%Hf8J@:.{ɉ!diA'g_bbP1X*ly^&ֆxQxv;˰ 1޶t7Tq9{\1u8+1"-^'?uqAKXt6$Nwc}(Fc[9ٙ'H)0MUzI0^~EX2BA{6؛C.T6S)[ԖJ\*XNtomkdgd>"!G?tk(KBkeIJTfc…P|3B'Ҋ(h^c+Kј6hTTG]oWP^(#0`*qMׅJxbEI1P,L2nx.ΰqq#:ƹ'cz~zKRl SmZZ:2}/ߤiU׎_cGPθ6dע.j C%q ~1 T+Ɍ Eb;6#.-եV+vG7U6~N\ `XM>߾r=0&ʽ{`L-rب!K39?ľAzQJ֙AmW'sCE*+zxOdeJ+62k w&Èu_nXL、9*}}(bCI.5@oՠj?\LfB وYq'A%}[6㹧GTttc[x.hDhG!d\ʞ1RE\F֜*=|/|bHI^[]qEK=km] *𢅫1I0^EDj2b)ya-p6@]^ӥFt+= !dPV@MPd`wCy1?a͖E id_"8W^^ŋ`d 2˒]/:~䕷?=OȄǶTUk'];ۈCby^&m TeMV! tv; {Qߗ Zm6E`Οׄiny VD8?c%k-iڻ|,Yv]^s='RDt-ϝ\-,gtЅ(%9w3֡l]=5t`X|xx{#!6"8yaڂ眼;wD(HDO32RZSQYf#H25ZZV Zc+2f h[JtpֹXr:YܬM$tgW;50yUP\lٲr"ڂE-NYi%K1sluJ>ڠYKq0# yQS0A|ZU`ض>X ^v*p@LY3z;yPVD}2t!9#akS:U90<*  ;Kv8~yXEZ+Nt30bJmp<\]FGRrY˛h%#Z`X,h ٲYngƲ)cSGZ(L0gb3 qw1!ێ:in}m mVSFv ZY1!g`N<$@IDATRY'8݂ >j OG-kY1C'q>݇k-ޏv"oe!w>qnY36],&ӗse"%Ajj* ; FKL]EǤ޽u3^xiĦ$獛**5O.;QR\Ki/JO+\3*D{%3HC÷mwsVhh2䁝 ;ƺ\=8弻),E;3 )Q10L $⎽AkM6^&RcQz=pupF4 }hdd7w Ek!eݚ v4Wӓ6ϢA=梉&7_{ov0$#Մ_WO,Z١ga%@N򇥿}Jp$/6,mVqߓ֢`= ]t-R9!$?5T᝗'3~ FB߈tPс!^poٵexO3Ïk머3ed1(dd5s3 ϨX>[90{# 0 20w|_ص{/44aԀ4:^C{7A,0xO?Cs,i5ke;fMSTDVW]MaNG~#wȜhv@3xUl߾Ҫ&@MKVƻe-u8OrYv P$~8(l}SF^fꡮ F^#A]y Wހ/ 72fvUm8G=ZQA6-ONeNMMexɺQOn so/~ U5t4?\1ΙzOma[f}6tqidei3AU=JsW#n43 I6ƵhUqrN)<erVgFIXdԀˎtˋb=@!%i1c{ח8華5Zyq'Y,'hO? ܊oOצ ƃ SW'#ؗ&r"r<,[&F.%w̄u2S7q(^9xSp@_qvs6m3UZ* Y(IGze=Rdn5sLRU_{e (伳1AF0uA;I@4ˍ 7NÝdmx_ρ]0l,tPOu?ȆQRE>P㓏u۰ ײx vFG4!<>:B_#@NԢ)I05`k9P,hJ(.o@vEe=֥%;2tnÙNQc&`t u=RR7#$>G8u2sKҭi 4Y۳( -(&QZGSf:h'<׍uI uuXlF#ߓ*j5im]ֲ4dX6-0@%ToUِi2ՌLcl!SCEC3Up.#VsoUhFJil?r9~qq!}`B"&Sٍ}[‚ʆS(I 22XѦaZLAGSomHkxLOFA&NpT4h8riQPR/܋߮rRbG{.9@/2_6 ]S9ڶ R OyZٺcn =>ߟ|6!T1BP\&2tlՓE\S6;'] h+42;gam^zZԤ, ZMA9)i` ʘ*T†ġ]\v;`*pM˙#e͚,֦(4sPf*NG?²7``3 j?kԓSvcQE>>6=UФ߀mܗ&rөnvafb43S%[q0qlT`A*VQu&7p@o\,khBIE.ˈ PAjv!zz ʙBLK}T`taT1Y }Mx򥷥W3-6Cse92p~3\To@VqZqy!, !GFE-8, k@̘95>XE]߮ pN}8͐VþD\B"c"`zk2/U{Zul/? &#uHnC9;F{g;eN*L0GaD?/~ON1h[ѪC|<+|G[j :~u#^WV񤫧8:.ϝz%Besh/ QStX: *ښZК3ѱUbml9*z8Ʉ30h=0so=0XZF{<- 5^/EUZDCeL '$2%! @O=:?VK/ٛ ݏ8!ԆǡH<*KDg BVo/*O!vp%iYo^m*LZ}Rq͝sPmBGov*2@SW /)Ascy.Uf4(ܼlYgoʤx].Ǵn$UO=OX~Ϝ=-͍&Z c=Uu"I5p6V{z=M R{x+s9 _A4fvEZ=aejKgCŬ#`\x-#!VFc6=mtq^^GKݒ\MEU0bT* zRAENl;~h뀦0uu Rjj>ㅘGNE1s96(q;R8@;Z`[xD F*ڷ<407UKK5]QpRD'thnne%$Ihύ1>PMt~؋trA?qьkoŴ6'dc sB1ݿꙿE513RxVAElj8/-$1fT #d 8hgM@6B9o'2u4Rlo6ެeɌQDPT&B"nwC Pn*6 :lϮ Nzq=={;Cȝ;ű-I"fg,.Ŕa+SïU rѾ4,2P[8gG[J>V+궏&Ңڈpq+Ue+AsQe1N pu`.`=N+C9*6 9@#{~HSɰ$iŨKyV![&"zJHԐ@VNϦ-aTCq4emL_߀oljQHLL~5w9&FZz<՜v;bP7ٝi3ќ3gnM ̤4ž}{n&  mˏx[ȑjض)٥Xs uwKPPL(1˘as2QQZf0Vࣨ >i'=x[^; 6vH< )݉EFڝ*ZcO?"-A3'xgܹ:of]VĴl<+qMҕUx.;҂%0s:NZ1;o!9 i3S#, Ԏ ,T倵K; jj u \}#sQ8[k l #]Y#?n, /PX,*n G~[_>osٺL1hTvHJB|AR:f F*t ]_|>vI"A<ܼj923%~Uuty.ʁ#'򦩥uZF~nAVo%>"Xv~V7pڈ<[ ,޼qvhV!mKv:)C*} ZׄEK A>d (oc` ~>PƎ8.q"@+'!-t=Z|(\ [c PTӆg_}84Ĉ:V>21DIezJo|6uwoBѥCw݅]eq_P(ԝI=D0mڦ Lf2r3W^kZtC L UT Ξ^T&2y,u02E:xNr ;9 l=Wbs/"=3|'A ^QbKX/̄|O?GZ.8b{O35%\S<yE@cLCs-0oOr{Jr$տUxd?6v܇@MJl .0-%^(5kh1T`Z_5rBՉ[P}s4<6=u{cNs3^Z `lFRc圅&ZT(׃sQ_ 5C' WygZX8h#PARϩp @ky<7U5vC}S\+lͽ"PQs֡'i4@d(q_%fxx>Mx?"6TA$ C_7&*щ6sXXih)Fۅ=1\_?yIx-D+ŭX\JV>Z|>cH8"io$UlIj,,ζܒ/_6{C*=$mPXX|/J:sv"=gW0{ t15M}ظWV"7Jizkq7iDVepK˫p<;^q4֔[O=x73bAjl CΑ>kin q[@dL>F'K+f +D *cbbNKxͫE\{kQBi?1UYK#1ɵkq*ٴ񃷱qhn*r,hc$ Tj(oˇ7mZ|B҉DcR  ف^LmPi_mB 65I[z ֹ{%{S}d1Ȓ8O;CdM+ܶV8;ЫcRN2oKP1,< Aѳ9 Z+h-#SI\ <:hSko32ѡr:1"1LV5 0Q@!/kD-jH5h ngn8X㹷,uYta+) ձ t+/ȃ2=hD>@-\@#A.bUBJr͈&ہ^ B:Tv.FAA)reUHdIjO\$ڸ$ n({PtDVB ?e|wu(v9#Y*h.-C1a|e5|O]CvкZf;s33GĞ[]G{*V@|z/2f&(kGw֎μ365ezbiFz@a{m[6 L, M*@ *}`nnKY&TegeViKaJYe ΀>KlݹE,wq 5qLT 6d{*G^2<.f('%P+j{Q*jTcç/0t݌@ECKQG GmnnB[=VFC̊8=ۚ+NƗB } iy32- ͉ fBZ=JAf& ض+˩z~o'-B) j !8by)lO?'x־1wCta}?b27Cqv ,M`>??(:FpcFf t+֘xA .vcr̔xUvfdf~YU|֚@rNg@.lۓG6fx,޺l9m6~Ch-d ">9/npe- w1'4 X*PEX#3;ܵkĂt50k -1`g ڵ,녓O0,^!ǀV8^"Hr;͘a', KW?POcR5S?3D>:^+˪cm-Ύ#N3T@78 DO'<** CDZ~]+Ӯ\KTt[Aw?3Z,nMAA&ou 7܈ȉ3k 0D^ۨbNmpwtd Y74=ѥme&l|-z˺y-nd^T\nXc?"w’Fgxn7㹮Ve@VChRf '2Y? ym>aM7sy++ Qs*wFv|b"\]Ƙ);QIbQe)"'{L Uh<y1GQPA pD ?TZ(Qʘ&ꨐqRryjgl>}gk0 wwArkiI>dM>>& k N MMZRG>Dk/U }%\S Ě  ݷ߉߷Yi-vt<ΜN#5k=ise~^y6k`Q=+_NG%l-} ص>z \5 P!ȡljKW9ucU"%NO?-+ÚmM6l5 絜W/_qaZj#}ֹfO_ݣ^ynZFK &^:F ~k4$5p3"+.U1G'BVڲş@7cr*ɺ]T([!/̓K8JKK "衶J*DSP G$ [C ?m̸ rLD]?[PD$ݎgQ0Ft:ZY %W=(7KDD2IN1-QPLn_ sHΎ. 6ۇ>>taA-8pu"#RN)I` -KUw^ jf~-cQ]-'5%R;:8mK~*5,HVaaW@Ou9!TBom' ݫV!؍R#Ҏ9R Ft޼w K$7?42B ԰ fzPmlh5lj;pۂM֩g㠖N*u<3`m M#3PDptG ʲnVnSi:$৘߼YK%9J}@buf{ l]/Bk 47`I v"l1b@@W_}w d 9QHYO&Wy)n-<<<`lG^m=u5Qn;DlUT:d@VpmTj2zWKU %U`k`N*PGr["#h|c*nd*Ir:P-Uᩉ17qoԍaGh d瑇YT HMC8f&CN\xVT` \׏E"GK8q~m4q JϢzY:e4,n}Wotf"n~¸G6;6G`U}DŞ@`8PVQ^Nf&Y{z Orn `=/xG&PIylj&Ӑ$8^|ol@BEr$"~cx^ iW,4` <=q4g`gu⹜ӝTQZY-Ite5òǝTL'# 3Sfyrzh3tCe'e\%'x hoFF-,CƉuu'R=J$Aݜ>ygUw+.$(_oחdU8S~і˄!b##KiSH (z_辝@H$޵Ob854ڟOO Iv`Y *STiIq;H35F5y1PކC"hŝaKw"IMC{QIU:=;A!$T[j s3:|?$kQ4|󸓊5H1,q'ћ+(L=gvs~NhT+1SP&bo{yl|sB/jp0 M)شq3Iy/]T_GMȖb)\TД&Gt1oT ͺ Ș9=jq(! Nt4IE:AVorFbAka*)bsz5<§CS`J~2hC֓ΖCZ$Adh ~ϛKel~ אrZ+M%Sգ VK5Kc7Y_wpxrT5+ru:HOJKTQFQ1qhoYӳFk}UȥRT[%  0ua 鎨%]_ZZ:O6pG|:ki9Yɜ־ADB@;K swb$+X xAA+~ DP&oXQo2A8sL3y"PQMWr}l%eIv22:T@_הnʐv6yTr.jhz53eHWvσ[W:Z\ fEE 5`FG2CG9QAtS-VE%zB>ϻN8}Q{kAoZy5h#7@T 30']ukN4#Z^K}jRUTC+}&Jґ>mQ1G˵nZj "3oO ޶3qPV u/U{Gwns甓UKRS*[1P۸z,*u:sc7oڄ;W߅'d^bcr.[6nUtrCCQSΆ;oưVeE'LM G{\>Z{/O?wA!V3L.sm48IG֎&xy .Vo-V $"GPS`a }Dq<Ϝ?TW Lg)S|!7Q*lip}e7bf$sx Q}T/9Jm\1}&BVr4k-?w2>ܟ[%#{f4l^.ո!TLH`H~31)u@+t!//AT嬎;h\@sdXZ[CfcJIM=q-F=KkpQlqpt8'}D<&MʉbmO.ФWǜ+ yU1񪃼f` w^|뎍?2GSO WΟvQwCv@zYL:sp'9]Շۙ[!ن}\vV)Ra gU\Z]uE`V!&EUuI-7/YժTx+8ULE mjZ:3hmaml mԳ(n5umwu;IgaIV1 IstD[hw/^eȤa}sʲG/xjT !gkk{T+9$//f9U$N)[Re+#$(lDU>}6Rqk}τ>,bE+fxq8},*Ui'HTqY8";5cxRgRHX۾$`M]gĤaƁ|{‡tDZhP*o @fCbђhyKW@7.`mD/m_kS0jDh9HXs 9<:)yҵYE5c>sXlo:hU>V~r`%R _ h+"ҏ1Cf`VdM`cecv@A׀ty}E&嬏weXT$I R"R)®T*\7aݸqtߛ:MNUu]#'Y/}?LjT=?HkU2MC=jǺzCàFAdRjj.9V ؃{ f2kp8*k*1-2c;|aBXUk(Xc95PRZn TR\Q9稦n-Dw]s7Y/bց..<.qw8Oщ@ 1ﵞO ]Lti5]ku.0g+%݌E[u*˨(W&tRO'OC&@ܶi}IERy7B"Y g0E0=r6 =y)#XU4אts!tlREϡdf Aj8f38&o$0*.6 oTz{K:~TaLfH(9-C#YFbV,=P +KF|pl 3'ٻlTHyjhZKwJjMy݇EIE jx0˖--p3z^9wt3Zs0f"cZ/1 y{""tԨWWGFFYL@X4ns/'ڐO)U /_#;ˤ*aa8}UR>z[]6t`TDT YVP鄞6ƳK 1hdYc`\ߠcvF&A =[g eUXSR251m hhaRikkN\L\Ag7'X|%~z8 'Cӳpttę18y0.Y}[hjJkRCKaomq=y%twUuQIOMKڛGpcw #Da2ٞy{Iss xC`DK8q]Evؼ[dpv%HkFq &/V)9YhjE9 x7XXj.9X6ucEm_\]R>a}G6O=~vm6($nnAeܜQ|4SǼ OTc"Y'6[‚VFp:<8p%n6\eZGtGjd2ڇ35ε:Ɖ =pr0mW3Camej=iβRtuW?}2Rc r? &,Of8#T )YɓQ*HTsGUa*|P\F`ǡV0<|Ώfص}3:p nfh-=ettT:ej8IA?}F`fّ]2',N,27\In*:cvþBb 0򃡃\LaalvɩOAFʹ٤Y3gaJ%?K6v<_%oቲ2 z++8Ȩ14 dD3F -%8 viPo_"k€kijd:Ĺ[#{Xs y7nFVWR6U{~``!WxEIZ=o1)z>;y =SSUUItd!U\7uuo5]%} LI&@mlgz*{(Pp@T  tm8/~HX2FN *]ԟ/_薓xch}V:ĝ}0J: 1#Mo$ٌ2@Ϗۇ9e"E_0gNN[b'lp; ~)͠5UU8ڶ;Bj#))n_Rl〮4ۨa8tտUz/:Xwt&ઃT7* 8G洅 ZT4"9wv'AB*gIa*Tw*M:!7ɲ3a RFd23W/9q0΁׿qrX[|$T`r[w޹V6ݿp|F%[=^l=CeLa< |ڢewqN֜X~ o;}|Ỵq޲'N@ c5!bhr^cƎlS3si+Ma;n@}yVY25CMztj5)Kds6\XlԕE`@ .]p_Ȣ}kdݨoFK\W v#3aV9pq ;9&QII '%f6.t[oCJ&_*=>)VG`t7_Re*08I᭜R2*+]hax|]{EPYUHw;ұNo '}u~qCVM;X4͝+SaN&,_&GN&3o#-xy,|Xz>} Ҳ qaӧ†Yūv9=w˲GFzG.UTBۚzͶRٯI p60~ZkGcw;9вmP#? {T,NrsC铭-2V֢Yѵ: [7|O?IY2c#mr]QSPݣI;A*9Ar2g;V#8ɼSd׊H{s>A=u&[6z<~|%-V..y8]*r,_C1ϽCeFU+_ǜq*֗^|i(0w.>RUe_,F>/B՞DRN\{4 Ӷ\/΄ e,(YΙ/̇u )^;TddǺbD '^2DMu iEe4a@W+M*Xւ63;ϟO&GKY[y=rY̝*TX k +`΢眴Jy½֓TNsO^K ܵMfS+z%OSGKg} Z$94nlIr6 0irAhl9d.E4IϨO>D̹K s%_| :w~,۱JW vIj̄C+X$9}ŵ:jX{nh\w {%uRN4 <8I#̵ԠAe`Vs7_ZͲrwNqc,DGU.XN(e\JWo;ƸZa4xOFO%= uXib+d5+)ȭ邕#:C3Kj\tkNgM7mH\?[q ,T:[R+u fZ~u[^Z8}^N$v7wOQ`=?OL C6Z{iNLsFkХWn]>TN*Ѳ̌A Rz"~3Hε#<N)Iɉ$GsYL-lO Wg`Aח?`uvH!B\ k 8(/;~8 FX "ی2.lDR &fzj|Oku71vP﨩\|f-Dqn64uHy SG&wd3z .ZZ7pr@x"&N u.}T:#m@L-χkA䤝B %2ءf&\3l}B߉_w+ۿcgZL? 1X#KqrT:O ]WR}#s=Olg8O^.lo$z;UGloypC^JNZ>iR*w;+*/C[Rw* | mX^ڦ>4-/H["d*u{Pe/v lC䄩(?LH=6mc2D3Ed[)$že,ˡF柢̰ms'x""$ė{)N&$!x$l?m2/ 9(c1Y2R]l^8`ޢ8qx?&k"_m ~#446PG%E\O5w0~VS»T??uچg$uPRKdMsZ]f~Xin$ė7m]m ur`d|JI"Šv&-%>Fzlv6b`AKɋWPI mus:~R)ȫ0Tc*:tfsiBئ26Am($[߄ ;:`S] )`K_AݽÙtf:SIJ*lиY wf<' 2 yeŒ2 Jf:3mϠZH@=HLM@&̵l`V2*F]hVI+$~~0KՑjg^wdг`.N br/嬇U72nl f. *HMM8ڪT4}C11i0VG?Mːx&T9c9gփO([<%njeB*'%U-Fە{@ZY#ZEm}E+<>(!kdZ>?{`Xe Y]xwr[nkVv~zVTJl7 %pbllz`-++Ğ? T?їaH @i;cf9ћK.;h˖"A2o_;m6s`@"+\iLڎvՉrs tk>c63P %Xt˽,\_IyI8p޽?!.7wxҪcԙںTlȚ~ZzRH r2c PL5}-O fn1`p,hK%ݽd;(^1^{+zzWhoYM.YĦ50'`5 9+VcIQkNOO -LK1tdRVW;$K.CJ)Zj ɃHk6=T`¸ flGﮓ@V-wh?mn Nmǎ%?EA!M9[K#%}狨B=H!Bf`cDkצ.̋4"'P= }2=k(M5YiO$c*X 0ԗg0؊ɳV9%50vL%ԣ&XuVg9k2Z.WH,ڏ!Aki/bm1b%2V*++9ƙ`lKnŧw \ڻmG7EY ?~.Z3.^4f3ڐ#^0pdOayv<51vР6'4&GAT:1dAX%q47OofA$XZ'nn9p*DL"LDuۏ_"L uS0=2=P :0{1\lGA~) ` d%CBx8q7e;k?lxOpQ]fؘ8fH^f j r_;8%6jfTPr q+= ?yVTpp5.9 $ R=[#|?|+i#/f6-i"Y(d m=fZ2z:8хoAZ[fdr}JkiEA}E<31yL JKʑ(ey",d%nK F27} pI8 U'wN\=$Ŝ<__I/+@_r6oM5 / XūFv"\:Ntym͔P 51 <}/LzN`~l#e X@}hӆ AlFBt`kiqx ~ L!ԝ dkBfʶ`|M!;|(_Y rq\:`\]nl}dUUbE+ zhlz6ȳ-*,){_|?'ޯ: ^18y'[%Vwk!.W_|wXtCxQUM2&݁r(x1(lK/]H83O{ #]`U.gU+ژgZ2tIzrDn]pbݣ^0Y7<1S瑧i`M:mG]Gv1I*ՋVyDTmH\L&E*w`)W%w%Y>u`ϋ[rESIu*E!/ISej2l"gUi,G<c.qfKC[qm8-lĆ-{`m(*2w @"-)&Z:e2C#.?ߔ:ATRA,mŮAzX 2ӱpio؎ӧ"9?lMnAxXVemJD~.nFǸ6 CֹƺTd nXA$'{1` nV8oY Rb"@GqGuYy9b\YƩD 'L1z-:4!+FXT&>*qj^4crIa"#=yǝȏV醢0 }t:oATD2t B{qoYu{{hD]\TFеJs8NTE2hBBa ME(/6 f,:FTZ!'1 cgՙC Ʊ>:NZ;QaBX|ms3 1hVS;^]q'"W&h^U9ڴPDi 8owf]dZ.؂Xc[gDag _$+Fg:5thI2 "o7Ŗҹ1w&0(NiMl q*Tc[Z@cveC>Me9-+=h"Yτ ;oـhN*J : k:dز~,Q8y[7RdVYZM̦Yb%¯0!zDSdW!82핛7'[hosDЄ {rT5Tx-]Inc+ࣺ򶟸L]!C;m)uvn[@Rܝ$$D|ϹaB v7εssq0Y\lĮ% @]'A?MHܩD_iOG\^1I)"l%h ESHd'&-ݰwt㽅7K)oPh,<0yB1PҀ0o4SLט{}Q@Roo2Z^]h>T2ۀl桭 ;~Uru|P^ `bMd\%5-=9/h&M' ]H*K(gz5Ja$ۈv0Zqޢ0\*-,@м|HtvFt},`u5gXD2x[AʪFK|K(*wns_ZI댶#pXž\-V?|/6<*"ކE  0{&Μx_ ]iH߷~\IJNZ-;r}AcL *> :Uz4QZ?Mp(b"Fi V 3]OB*l j hTo0|%}. QXGjbx)ڶLOԴ*/,/W#btS}xP,+_ށP[ZaA,2hM|N$ W/{cwBbByT)0HBK#8r29RW,w=,TfݮrT3ReE2j[u%5ح ά8 up [3ZxY Zq U1:M PRYA)KW/9RZa3 ̪ 08W:ꬅ\7WoDޱ Xb-ESpL&լXԎ䑈I#mZkXQb *[ UTVB;Uݰ2VxF<`]XA qV§.F3j4a+n~`.MtUJfizuX4kKoC5߉>3EkLpX C,iNqJi#㡲f޼p@q?VL6uQ ̜|{Wa-bأښsf(+(D-mn$j>e7C.5X+aww1VUz Y)c7aU+=.?0wCS[22i+mq]}ߟ҂k;|K; #4=G@Xk2GX{㔞|)iq,(L,kR:00f$,CYIi`x\hxVSM;#VmכMQ0DBr.&]<4Ү`mO=4;w;fCRͻ`ذi|m}C'o%8FRd@`hTrHų bZMMIjhj͎HBycж5-&[KKz߈ݐ8 <} \EX6:؁Z{`dy&RyP(-j<ުhBl>yx/ R=xor4A 1e\dY5(nì鳠@&߻R٫c :m-rcR1GjF3=Tecm Efy3+b4fOetnaPՒ!U)!R!}sp93V˜֡2[n.3SFj,37J(H+ׂv*jO8HKf/ K`IBmK|Zў@*ƐTy6TQ8AB?B=Ht{g͓,c|=lKTCXzz&{Cbax-M*;j0 ӧGJf 3iiuKg>&8ҼWlS>^nn y9޹ ~y5qDWt YXsvBdq0Aj)^yd FwtGch B}i>b׾p8P1Y2dYӮBEY74'zi#T楝9VA=8e|LV:ر|b-]g7uS+g޲VVVs?Jv5dٽG.@m`+F8^O34~!'$b-`US3VeCG>e. UΑ4ƥ @~XTCb-:d/pf+^smmd .Gj꘣ĂCssYcho gVq` E^"Gxaftk{F%O d~>3fqtz`_$ spb& c%gH 5<$3ﵛ}wkT(튬#7ksX[SpM8h(a*Q^S\XRvP^ Z{ߙ8^L{n?9+/>MuB>Od:Txձ~Uq*Ө$3;@٘%L:c̽ _BOPKh=?1w .$9jItsn[_"ՠrHzzF^_|S:?e {Q`e##x€cXA6k*!BW_‘,ǫ0*D{u)4GJҢC& lXXdB2U\J<--A _S?qp 'vXwp /ZI0no!RH-Pf*PE.T3O Wk"OT(?EPn 1NyXhd]bZ  XڴJi+'R X'ƍ4}GHbB*T '`Ⱦ尺r|FHJǴU0'?iT[ƟZj2$B3Ks:oG,$e쉍 g1=]-4һW n^YMQHqx"KA-CCWyBnx #ۙv֍_4V`eJ}]Œ`AVw@]D2sOMSñg Vՠ :Qك.7"%\q)-R2ZC2'߽ *MP\JuyZ%&c܏"Z!}}Gq@FL0Kf!FZ׶xMB/?%m" ?| pՏ0-w(h`Ӕ)6C'͈6:uRip<FT..nm5fP^>S9XСUtrƹ$}ӲXdSMjjHB.Skqغupq|ˆ2Y6nlJW= r\; ')))Ȃk XOփ8M1 giށ%eD9~NX g! h\XZBKVFBh}lmg\h+4ԑTlӾtе@1W 馗˞Fs7wd޼.$6KMua`5sK$K\Ⱥ`_] ɭw1 7%8zFU4ŀ֊X2);I#3WS!ո;/Yw%֦F|T#VR6fwN5)_ O݇B*<&JdQhZ$N.EBj|\AUZ2O V̆m)ai:id 8w3DU=sW|xQo@oun$zC@IDATBg@U#I,⡭Lb$^vI۱m]:q&׽[9cx*y#G"DS%P4G-^ S !a0e&8THްd^k\{b`l$9Ql}=HMJ܀FA֑ScVbw#~b2':B)1C[1}mMI~iU'PH'?`;wy떿8{p+Y+Ph#b*<z6;h`<a77Oi1~JǓhD |[:袗f7G ?m(p(MʊN!M겿pQ*1*}<T@߯wꉥlvrS}=[ {bW,;]7*>ڣ/[L}ƃVOU8tvUEM ZI̎:|:誳rJS' =F!q.t%Z€tYj y !Þ@*4&`Y8u22bNqyy-8J^wg+ԜAfQ=?h{[,6TAjqdHт]`M¦ ۏBl%գ1t/+Ha(#VVdڢk`1MUG ]ю:..~E ~:5DW2H5*D[sZ>SCLN6P! , \Dg*QDux7[(Vvws'fPefϦdt2ӕqvfQuݒxai)TcXĪ'\*2K":*o؀_[f bA;|SJptqZ(th/ҨN%Iv$nK*[-a)?5dmcg*E䠁hǔ 풡RyS1ޝvh k0TGGFޕb8s.HLH'z{ƫ Qo50u0-YMuQYL"me9;UF;kڄ?= ˀ`D++.!c&Ѕ[T]o+q_\@S[R|M}A@MXofcQRX(,zĵ{d#:?U$槪֭+7EŰtzVYO;0)+7WXvh1`*/a>B?-BX ^Z%e\|#PV$hMQ>'*Nja=쩆ttfx{n#˜Kpr qd颔5|X{Z촠Ů>V4x ć'x0!l6; q(`h0? (#3M}zyO30YlphO*փQE__rr3yeϸ)U>*.DjKfOC<-C᩿J\¾CXu8>U'-^ǟyUԵhEk_ &Y0꫟ J꒍|NuDE$!#HM $FԜc}d:\_%M|qX$%;qԙf "x g޲ %՘9V&mϊJB*8h=Qqu%|H5QQ(,*D8u!H 0 xgF7ci5, -olO@ -Nvr4rXxD-e#4qEglϬ_bUd)MIՅy(eT,ԡ>82ՑKK״3XXfkc¬0vø ١6Bin6٧@msQAPœ6Ҹ,g>õ Hxo۶M(,, ş*XBTpW߶* hlkaK2 xsH@9( sDSfA9N-XL{i h=/箐 VR Z Ih|k,A !9%v|֣vupU3jC3ϣο|Jغ9֢ vpq@Vi°lpŰ9s 8x@Q*%ammՅTP%nb7t nWF;+K2m;kQVQPEL5ڻ%F:Fe ҳp>1uH$_*&0%!2TD8me@:HίGynQD{G x1F(Ju{Ȳm,xf^s2yO/D 9!1!ޞdz3`OAIFI kIujEh'HHHSsF!OR*F\IhkPM=V Pln]Y= So߱hq`.FYrU_]`?R^l Z3 y#ӔJ =ox{#ۿҼ7QBӂŋp X~e;֟dŕ:](Xo`8ߑTHߑ[YIX<d=փ-bU?H'V$DɣL3 D$tK{N@TtW19eUm*UQD;Jfk.qbՒ ?'תg0rrQ^>nX׉?uUq*g)'m *rІEKa@aW;+Y/ JTir*o$P9p}-$mx9v's_$ ktI'߾<6 +׽mUu/^J(s??-Xt`E`Xlz#M[mڳ`]w\5wAaa]+ԞY[1ے@6օK2#,x9Z׮لXmym#M:"0*NVb]bvP4*ahc e@ZsTj7D8/M/%ݳ2*@tCҁVW٥KEnc\Pxv9$z7U되,P għ$KWEˠ'Uuu\9~-]-EFa5B]Gg c*S{YSUgBztu ݸ95 h}kB:ASsI<wd'%5Xgԧp==Iڀ$Iq/LfL$%e3 y9glS/u]7²6gŸE]+/Z`;ka"ĘK@̡~s3ӵQKj- IPl%/ g.&U=ЪO$2 d-D,Ӷ:J"gSU¹͘%=> 8x Kp@Aj8Z^5=m;ť2Y7EMXxHhR!iGko Lكq>ލHQfjJaem뭐twSq)F(Zf rsC$4]t*Ƭ3l93g&J/WjD+{tV -g464ד)狜[vmڀ]7 t^-H6%v wЁ~} .㖀E)g^G`ojnQ`V7 dݪVWSҢlXٺH Y_!\,;#F:md<JECԙo%ʴ<ݵm6FXz2њ|Zb"77z2@Tn۸k"*U@ 2jjk zuhSbN; ;8 Qd?Oi P@Z**>[ I7el@qmm-;Ҳ&I 랝[NA`##DJCAȊƇ}w&L~ X;K5k>ƌY3x{9Ǒ{ѧwW=^76D fig{cxAZ'q@'C#g4vlټ!J1(UK+rѩ!=NyM*vLHF *x7t; \4;C -C_} ,]{9OBP8ڹ`&;v#@I^=,C_ &h[ g\6ݘp;xfQLDU-,\OCT iGsd/xQw1@$Zc^5fyi1gգ.m#I]8} hFSRlm0#û<'Ed1_CgC@p+ X|mb0 F6Zs:] UE{GB%thdZ6?U+5v^'ئOFhNV2$5@c.\/ k ITϘ5 |p~-x^ mjX9x{~)n#x,CeתP瀿߃JY6z3>vZf`hH R6 }D[t(G6akD=H>}'לpBMܚż3,MN$XXΆFAVZ]{TjwG8ԢOTV27nE `agZGLd3@WRʨ6pQ`NU26fޱ.l\ IsԓOڋom@+bt6I1,gL7M7]=.Xѕ^Y~ճ=~'UXjZ~㥗_3Xؓ02N=`~x% u =YYIxvN`dVCZg 뙄kH:nf2WACM v޼ġ!#'߅H0iƜE;X?Jtp'sN9pM Ool$)P``߸F ăG_P1ҟa\ h+gZYE+DrJpðҐfʴ68)&uo42ĻgC񁟨jCZV>R?0F#Gw›6 MddT&Vm+gT3gSbU՞CN5!?BAWq?g.>oyK `^M袺U]]݌"knW~} )2t3&*l@m3!5=%ts\٪|-J$n1+4U6ubʄt)s=Շػw;d9'A*6 F3-۰7*2u06ƒ@iJy<&ZЬ=腊::IB|0.z8JA2_!Tv2z^ ndЪ1),iB@fEGW/ǵ5A?4̞*B}m=x$ I?o A~T*ؠ1̷bͦ[ iA[ڼĜL`XIel3eVK 4p'O3"9.@C9xk ]MRG{;*X26Tg-*|`onuɆo>0ʩQP. u|em ]I.-ŹtqO7K5zODGa^e+,9({ڬRu[L UpC㯭^=Z[\t$]-0S9^ÊH ףT;ϻBɡ(++ "e ?'mus(C7 ڥT~^;.=T,[ uzyÈumQ۾ޖ$az Gt9{K@V =\Q%$Bm?XX |"H.-_:6߭Xv4胯OR;trkmғ* =v\ 80jż\-l"VGjJS*c65-Vg_ 3T 4c\`γ; *:k`NjK>J01ċG.viRUZiѴ_,~E8B5xPI(_yF Wa}ydK\b4h%@!+ ߈W[Z)$=p=#yUE|;f*e>vREҼl,_ZjPԉCѧ믿@9M"_DGum}{,Y~&koY4Ф N3Dse-aٜxޘ*d;hwj S1alXhx24ca"hjY"fFvg^rh %vMȝ5ᇈEW5|k/{0n*oJhcS&`L@0Њx$z\` :taꍳwaR&;e7'᳗4\iMɧ_;og%G'N{Pe5t BwAQ4}M!k*1'ndީ> FTvKI 焢Uy4dV@G 5JO ȓc(: ]*0oyC+ZS[Y1c'΃7N%sn(mn\~TO9[ZKG/+9%בܢZ' JO>DJ656Ď-?3ͼyfcAVѶtKcSxvoD`X2z _ɇ¬Qveע m7USFm{au4](UITWgc(Ck"V?lz|C"tr+W H$LI x8qUCrDpf,+Y<mۀ Wd[Wz9BUwb}uiٸ`;_VCP;d!ʞEĹسm ɜMZZaN{Y{6mwPE:jpAکcy͜0R^Ӄs*$ @Vd5{d6Va'l'+UeyTD2ds]MJ Np9]ztzx/^UYJ [ 5Jw+>'ELT}N7A>7Av3 b*HYEw#~W}?[C'_: i!~¿b ĦPͨ@ЁOn$CJWwOޚ2~6ﱁz<*X(XEdJV# V30Xgb&*GV7;["sƔ)pdƎ>Ufz? &tߡ}w ivP6.R$ݺwϝŗo$Lm4˾1OŌ}ub Fq2:fNPF2%`m*A[/QC|F?i2Rb$U#~X2Xr$i;6DW݇^iށx )VP ^tuXV]ߪ ss}kש(Hő`Uo+SbY$\[ŭXFܵ$ln^Z8}tvb~F3Ol` {8cIgY1N6TTݬ3Mcsc)t*o釣^/Ւ 63#95s03jք$Ecql7GbjzQ WF9ޮVtAܷftʦԥ` rղVh$.UJRuc p"hD.t1*璡ψ%Cm*J%7cjNJj[Cmr +hi@Zf,LR&k5 ~ NmneC,$SS]s$v8Uld:;;"$8WѡExm1ָJ2?a;FO^y5>ѕK_Jͩ$$geRY((w|%|i͐ٺq\Ig*TՕ?/ h;`ñ.!9ѝt}{qKYcz9h:k^mTkgMWsk,l[\o'{2CZ rmhUfwc~:Ԩr6ő=`Ipvr1('X> ਲ਼SD/wiHuFv#~C1uK['$ϼr][(#;>T;8z[/;:!cL.W$m_(QM\nd6] o?}HW*ZsAu< ,W՗17`C G7qHa)KD;t>TPZNXGTf%t"^< Ga%E#t ;5΢úb\X(,Y8'7{&Ρ*{du梼_|#f!? Cm*YqHGY(Z2?2b`z\\ X1P(vލg}flU?$3-)ڼV]/ eؑ{y*g-`ȭf͛wf)`hqGɎ+Ad9zcpvĵl<~O=$XS |!)H9M+dM2%]:ڴCg1TFۣ = mEFhbJ`,}ȰNG jT)3$S\@1ӒFǘzKSPJVj n{|r2hSOjT.jFFsO{9j]6u+#}/-JJ˩q0%1=OIB0Uá !,9=t;igc%%3}zs:$;OqMH pDb.C7[|s٭Z^pw"UryX%dTz? ktC3 C%t Kg :M+.:+"P 1^̽WQB{[bN„}LOi) M6 Ȁ#Ц7*x+@Đ@l/d5NG- 2Ԁe&F!Зzzʼ3K`AWs3HB*@7?Ef0Y]9H?Ks{:c&L b|e̬ *2^TRm뫴fױjJSL hD!GZvR q &A>&"1)emk%@ORҞ=;>ynvNvhP`KF˸o7l`L#4T hNPYcH,[gG[ 3q{fZ:&NZo.&iBn}BS)8Xj#T"Uz(lԞ5}񯯡C}Pץwb1^JD\n#*!٫O"1Yħ؈}ƄX"GXt$^elܶl.EI>یԂ%^ښE0h%:=6HRٖ$0!m8ҁJ ""ЯMQDXL9͈#655>}Pw8^d-gc-*~x;Wsilz+k)0U7 x啕yNw.|Lvn䟷C[*_\I7?fŽO8⑰kZ-YGb/s `3^㏤B#0 mgb+ӡ󲠯g@uLkm;׭ą_kV}`ϫg͇$2(d"&> % I!ʂy(%H۔NiÉ#̑ kږ 4ij{xJB^ Wk$%T[>3$Ul_6.k^y 튒w_~H tӄ=U*pZSSݴW_OzOs 8{U?GlFT2䳯#+qHOOq^^ .V\$eP Vj D>Y&C3b66 fED޲ r4eGNӲY%}=$N]w9~whbR~_P`Tq ؁n/]ަ*yA;mM dfmn7:$aG^ ΞO;DK GvN^FeY0j9цw<9P`` CzՃZc+/BBӆHzDGfіj/oNjCPV \*$ PK#מGyC7f?*0}IXpN:@"@ Kf Iqe[=ߔ"1m|z75AJ\u<3I2,<9-R9H0hJ[ٔ%(g?üe嚩>Swߎ* #VCnRhs9=0rR0qKVURT$(lqְ84bO9T71 aUig O?% m–7p@Aϳ ϾqrT5jV dyÛ&sGX7~1 NTDLR*kJU-꘵SH;_e="hB2Bfr>\ -=_|)pĉSb~˴iqm~}epp:uXT"dCnLnoGuf*ala=zuЎYԃ1iAeH}i ta3H@Y7ⁿю,<|G̾=Y GL/{opؾ/}%|1FuG@iq Pa]-ObɐQmEU4IJ$=„C鐃wYOfrvBp>.syi.PvG8JSK;aJK,a@An;H30 SǑJ'm$lAʢ.c^)T T֫ EۺyeMٍIq\ C;R`mrliÖ4 bb-EJDPIoo_n+j}l71Pxlj f U~c'<{9+LG-&YW^J!zZ$u33;<;|(gqEStSVa२kw _a8=tRS >p,J|JѧL~N2 uko37MQ}ZP9Xɱ>-h;QW_I桛N[==|ETR}1[/rUif (gq7>OxH*r)thU3yM."I^ :^B.#pN< t 82z* mazql!_F`-g%{ 7OdǞx~5ÞU~~ITΖUIЩ6mFWu"c8m-,QGqD4).ƒ;nCmY,_N$7kcb#~#tβtG`,W F]*4`8R@o<Gʻq'%e_IfX+fT6ٕ\= o{3F9\Z"6W`c CMxi?%Kަ;j;``h Pz"W=5Z<Rṕw7Ng}O ݵŴnF O KmVm `[1kކy.m[)Fښ@5AueʍxN0w} c/$@.v<-~Y!8yv}\瀉x m"rKd~L~}#\cN'CM&BQQVG%  Q8yW=ݑQFG+aц̼3rZA^**Ic}m \?Px&,:5mU*U5bAu hVAХ6fHl!ml[|m쐛]Q dӎȉ`g٩nNAfyLsjuk{W/̥kʹhI:&0;6dm'Y+p-|SagGX2Dž҆c@5*v]f04 daeZ~^hߟժ kg?] X<"<:4FGd(8,qL5~*2KO"VJ.7Zpƙ OkUآ@J0a~ue5-tcg ?y3j^w^GA#S*4P؈;Ym%g/~W4‹EO'i=~9& ckShdjN+s(PSe.DÊjB*~U%zS>$ڠh h=CX{M 'NsP K)eKg$%Yw{iP^ `3R]kH kNj,-*cS+U! n1Q0+pb:o݂ U%fT额甔LfR%j&9)ఒ|hUO:B3Sa(. Ku:p$uv:iDÏ0;ﻏf3v65HYWѤ ,VC:̨ mtRB4Z|QI*3ӢHWs*pߊ;Vtd7В)PBj)P^|xxy( ջ%]IKruOU=>z}<p[ [k!bQ|`I\ &go{:@RBO U!%)*buY}[?omp 巄)8 k=2e ̩HkĪExd NIҔY p G*2D2 R㼝B$nMƔ5P `e#]51jk a˖M;w> J_Z6T3'pk6TyC=}gȲ,o`%zy4sBisR_SAc¨2>e.)w0#.gODLb-d0.Z,ˈAf45;6і8 .]x5_+U*U'f/$Vx+L[YXxGGڰNEu[zO18) P߱NND5֕ro3)r49:&t$gh!!#vCFXkhN+ >X;y` 3B \υE͍c=q1Kbt4]|s7+e1^YN*48}VWSB]icL:ł e>-9S2Yv C;homfaf8M2 #qrg-8 HwkbNDC ݬZ=LȒUzl s'VXSM,^R~ sGqAl{dSO|S{p-W噩l,\#kEIZweU22e;5":E0 F'hnpqEc7g{-AOغoзy=<~V_J+|5 wIUˌF$dTӖ՞3e,xkNP@+Ֆͅs̝ԫj̤+󐙗kӗ?H1Z41]GbWȨطKe:hr-MD07kߎ`J5?IohCPqJ A9'9޷e6Rb_%COC|n}6,Hz(J ;-dH:~nTo{|J[N3PK洎v'T; A\qּ޳0^q+-BK_ogBwߕ2B']rsnXf^tsM\S|ŧ|tL>_`Һ#H udzϿ8d qx'c֗uJǻH\Pןq4(WJ_7? 9W#nc'B){7o1NK'JڱvڎyT7鐘4qBĥQeM;P O$G!XZy Q;׭+BW<ܸ(R;U_Y4gEjj]A!cM|-C4*ӆ#Z׳Z#)X PQ+u 7-֩X g8 o [0"snA -"x0q\ܯ50>/ vw6&]IYCާb}޶cD:tHF57v/O^TLfa.N:tGGxx@e~NZ LhZ050SX6uyX;Aewz4HRU%OE|"eQ"DSFw/g'h<@]q%[5wML",k&dWTM C{v6^sh.)Ɣ9 I0'~/:,TAMۑPzZpעшϬf6nBfJ,]/R!1ݕ`q/x'?3CfHEWX[~-Jn%T$#s:O\DYejsd_@_?IuƂ)>)GGzLj&~T=&>.ׇZm*:|*t02D-JzL,LksT> n}XNhPij*K[pqhA)jbt9*Qك chv3 >4i%жW0!2!vT7@L)Ucl 06jtEu)NyE5:,mtih3ZhZz?rG`9mI.,@_]%AVtMK"ϩӴf9>^ٌ!9l6BV:̶G/ d$+ƫ/9ޣ{dEDVW%ŕ DFsycǍyzbO`>`_Jo\3KS߄8Ga>H#??=k;?ȺA1hvs7"K }ߡ޾~CU~U?{gL[{D`J#YT 8##=#Ԗ3'oT[1crZQƆN+;wx3O5_0_ӧLDya6:%A[7⨨x$_ ΐ-ذyr9!76*& ^?&iQiMRڅ;ox嗑f$7J;֛ )mm-;$@S:i$şC^q"Hس\VF[w7a֧:X9j> w`sC7Rɞ={Z}Mp7 ڸ|ZP c*â_FusV& NL CmU6!5%{ٷ.Q\g$ǓE;+'*AU0*p,lGaji~=^ywaXy]^VR9D?2R%QT]MPVQG@ 挥NP`f. Kh-H:"5-GIكeTQ,BX}$upp3}+Afb˼J*38$s;4u*!1T󹏆w}EOwԬ\$sSiGe㒰 5CjU-v^ iKlF<.0CEvTC6C֖vYDFiq:{Lafyݺy 0'XGBY,2QSZ,Њr+XTU†]g\ M*qFB\WFEwО[q"Yrsͮ#fQE%;W ຫ$Hſ3f]/h@'Ywt\QՂX*#ᢝӧ=-:T(+`O&Y'9K>oђw/Y\bpnEse~`96G7Ǒ=;i[%+VPTwDR9_RR& 3'(`.Ζ#.88c|k5\}e;7ngB2w* kv1}ɐY:\qsxɧۤe׾:}:?*|$Zb㤬d혟kf 2صo7#)F :\X< 2dz7b:l?[\lw߃>wHt523Z3_"ܑ|Sz<>ih8`( x Z5ļ(s3KFpj8nDю.+؀'ȓG ֖u/0EMaHKFmS'|gu٭Xzm̬O=l=<= Z(bni!1cהO+c(H~Uf TY #-"`=Z0%560RZjkSfXJ~ X #Tφ)c4ݐFYws=^?>-;l*͇Ut$nZ1F@}Ņ$hk~`T^߿mIWMRBu%B*@PqQ*-Lr)jߛNQ+iECyCƖzCzؾg˷yWZ׺xBo՟B#)޶3ebu0mv/h>APF^e :Z 82krN1>r.mfIjv5%SU:T9մ-9PCչ+TIkNUGNE"+HsJ59?6>JHpќ}'eAdPgT]>=&#!`g#^|` G3*8qrҹ"Hɮ@tF] `ANښ)C1E>Y+qxhR1w8T6J쯵ɯ0sgR%oiH,a;c( Y }~A"6QQ~5l5f05}];/Uz6HdeGfvP,{[fY4ŗd+~д@aV2rًZ#S ޱ K%3'xQ!F<S&# F/-KOFJ{ JYٲ:*X1\GHM*jcUyHTwJǐb)3[`}_+$ձOJdgϟ șM59{fAkm0]Ti,Cm^"T R|F5Y1ۗ.dYC*[7g~䬩aJA$LmOWj$wgw+JIPB!WORj& @7D V2@?Yv"%>rxB\,.Nl_s"<:&OG$b#^.eVp,xeƇ!z+@f6* d*n0j/ۍz8[1C[ g23uYp ]WtC4<=rly}0\6#_PINFmZ P$oF,@Y xXi-vIp6N!YڴvVJmq!0.Dʳ pgn'F2y<(lS'DdR:Ue̜m2TiCg%X}M%Y^fmjG E1ɨ%(8lKž}xRQ /M&+]<Ǝq6bsFsEP3ىߓ,mh7ܺ;i5wJ0j,} 'MsgoŌ"ڐS!Zb'& }*,#`9V!^sfnQ>YYTW$V,[fAvg<2V> >]8Fl/۶m֭W{erM[p=w=(ޘLaMG{b* 0ocX=zs.D'bvڀERXSBݚt DNgNmOL@t pFb}brЮhg~-c8x=$wkb..Or#.qP Pb0 ԥk싒Vf*mQW\iב!dACԶ⽋0UW @X|Çؿb_wE8 i5 %jdݾo/ctꢒ &x[swآ ʮ<,ISfx.\>ao**$0н-yљʐh"Ԏ3sdTus_-[1o88{QAD,1Ag-[S *FaZں}U0Z2l|0]zHIFrDr.(/\2U\JBV5Iqn vd"O&YS 6$?/m\7qfv1tuB1c1o-aܠʞPh/YW7j"~Tv~fOuNQBZ(j~Gy؏C7،9 aX;TЁ 8s>2Y񻨭y$h1-eXىB9~eΰpSP}FYA^R"ɧ`vE-??$BV1bIT`֭HnÏ#=c>qAPj'Ɇ6S$GVDBB-Ӻ-2"I\.1dq oam쿇vl` #Hx:ul@ `M f_-%7ᄍF8YYێN5uŋZ,Rm׳]UlL<X[$.E?t ɨsHYY>\ŷ_oj4cPI ˦ yȉ9LJ“گ-TCi!VNv:B2!kF hOol/}M0;;p PN}}j O|bpz);~oB'^tM'uʼnc/DY %u̙3l_r?eDz9]UQMK` "xsLjr2ˡ ƝĜEФ"h(\M w>O##5nE;ZFY NTaL؜^_͈'H%&cad :\*6A}2\gG\pfXHMp:ⓆY[D Z\ ^X2,,;&FF蝏֢I %D6, ׼m8h47%qA~$HC.wRM0QP! XDd;h vO;gp!.̰WfYv㤿v洪ٌܜ Pe'h*^,XDl*Ai42#i܊Flͧµ,ίWF\4 UY*r&1E '=3CWO^{]x8ٴ?zX ھOG -ʛBw1Oy~[7GZRT($P},8&2 B[f5*KKnm}e6PF64W>0 9ye/ٶljP cɎvg ȒiQ؇iMq::eX1DaPbk 7WC3cvs z+V'xj 9D**[3~Wx̘:k UZF37&Ui!Ӥ+3TEXFy3u 3hpǯ66F!!A;DhQatiBHPX[b$gyXrp!;(pWg3)#PFE)~Q\ >!/F1)^[4GDŽ"JxJO`rߐJl*.eK h*C YXKJ[amIկK6G/~q;n޺^NpA}$@>)UMt;PơqgUZhۇfm= u%<ua ŌκfXY2.Jө`IWeSޣRcUH6D'ގ$Ӳ_bIU!rX6SMX_$<3Z3@WEk cfب +c}֠R\tX FxrИi).[!&4LBQ|JUKв 6NtlS6j~+ɬX㩓p%x%07<-2`@ HdT45A8-u;Zgj7zdi,w"* ;v Z^*kV0;s]e=zZS6S WQ b6.+x%MQJٱ!dV@9s4PEo=mJYȢL'CSOサ =_U3lS~џ"7S ߯ ~؀:` Am^riYI^~ݱTY5`Y:8Bਜ਼vW[M˝ ֫AKCm]SڪGiA _TYSEawv$ E\`Yb|&'`FzhY,F]y&W"xqYZ*?_.'Fh,^4&OX? ݾnY.-sedXSnBۛ؄ hNJV+-Gf|tL2b&Hko|_~eKRuz =phy$:)PJ_'-w_'>NϿ,ٗd*DZ ".9^|GfXKU&Uk]i2qԖ076o^@v'N;ɬ*wͰ56@! *1Z񻵀*uVV#̹YxӾB/ٺbd|d Tc,&`&7ANIϟ5LRxAjU_Y} ߵ#+5l+ot26cW_Wal^_ -,,XW;;`Wy@0Ӗo/:*Յ)Zg29X͗8O@`9.;ѓ0E:YS>ԩST/;M̄C= IMuB`-wLڃ7Rg3G6~C~y"}ff8J(V^[bE~c-~K| O^U઒SwShJ<3X Dz3?2{ +ZŐ2p1:xPzyꆾ+KOfsY~ I 7`mA1$ѹc-}z2z'e ぷ: nKR6=,...י*tv2˹CJF?|yAp\~I'\Z~*ϊl 7_*V` iaG0wf#=*Tb!ݐI}dsYbH\k *X Qo)4lT_({۩xFLe)ʋW!p׺!P]b ~]͞yY&_a*h*۶c#nu(nv:1Kf^uj-0Ö5UIѺm>e4IQY1JP3 YR͒t[rlj;p>!b[SqΥw,]e&#dVa1$ף8wp*6W:9 6 &y; UOdN5Hs5ߠ#$JX2TWK&Ḣ B [mp>: GOXW0Pn&iv^} NêϾAφɎ@•:}dI!e\P[^M66dwH@ik2/*Qy׸!탷)Wbf`8Ӂ ]TiצTlh<3s:O$4m uj'>{$&„2&<Z]r†YW#b$+1eWe$ޛ*Zd*k!Y TaKH"niJP,>إ*"GkAxP X*@9"xI]n@29?l* J89̍ӱilE'("b xOM{y2rkIy3Λ(ϋ@z@Ӻ.+QG_t4076H+Liq9~{*"6σdPs `a O0~5b;/iy -*lLJ2b eZpD2Mγyx \ҵ3)_:o,7E-Ro:sraIasȖ++@bZ1V`6Bh,M)˄ _OndYΖg#dҺ8tՔ0eD-&J'?HJ8[W<} 7' E( JP5gjCOIli;Gf*:8=oNiAs,uɐ?سC[WA:{"/^x禵/G~M_Qg. ^6Sdo!]|%W%&[V#|85J/YVtmLm~0(g?^6*֋oEmM 6Ӓ*fa9.*EmriM*3"|'`=c)|Uw?vH+3 \H'{8D[w:Mz͢CX5*3”i!;i قU_|;هFc:4CĉØ:g>'a<110#jهTd, {|6} CQG*IR",Yub= gw6oA0c\]=pp6NXO_#(b܌J8‘62QAcl ah"K0%4y*Ű"PZ ɖ{9Q`g0Zo!.Ćsxrqi5O(ǿjeWRF/٪(KnTY !9RbN@;FKe9<@uj7?cN>d%ѭd?[x='3؆igIusMLPb lD;T޽_}).b)j~3zTZX>/5ŇXkֺͺxR$6@*b; Z`Ҧf&& (B)2}Ȩ҂ yJCjh㉌ʇ)Ia/eщot 61;Vg0i(gKYEfr,v06"FJm6!+Ri^AN&0˖1K0*Ԃ p"©:V)CM-߷2VVU3p80%i0R%*Zh|Uy1"g]zy;iuJ$䢉`6u#XNY{7$%P n|4:l ⺙Z Rb /DM'6Ѿ~ oډkv*F=H͢zJN)]|~BB}XX(a꘳ۡe^Ws6M7CM7,%*.A5z㏘?}IQbT\PVD2O mZ:1x_eF\o5j XgǟĦCa&]g<"Ubx;!#ڨI1,Cqx?.ͭ0WXo >g]1a!,?;X1ZڑI樅ei\5^'xStԈ$lt"N7[7HdQq_w:Qʬ ; }z[o/Ǧ)UIO'h7ukxas6=55,bT`dn ww q =v'ۿ_B33=iRDHwlDAA&IY\g_Ǎ(2Ӑ6RХJv5xh#߬3F(g>1~,g^ƴAD\O}-ZJ*l{zzAgP\3/XKAJ:>y"%œ6jl5*lW9%6XTM3> ru0V e ueZ7ޅ\BElR<1 u+s,uX`KFAy[;.Oxtr2w`Sw{vHK\6^ɢfSEJh%lLU5a3Rc)Ƿ%f[~Hc^vZRT*t`d]ܗ: NISpx~yyxQYVfH8u zʁeFqVn=EK~JjsyԱIɍ#mΆT:`-(,wc(7;Z6s`6A\\4xq H35Ο}pUuq3QD ]J*PWet3"(_û"O4Or;IF3@;IB5YPRj~$ mo Օ 7=G{wԦZU}x$O݋[ŠWH$,XbCKײR3Vq[PZe`7{ؼc7i X9IYaUȜ~68I_: R%bơY -CKĭbܝ>&Pf ;\HŌiKf(/(@Gw5<M$ gCwb׏+ŜYϗλQ^+T|TPMJO* Vԃⶕ "槮zCTY/j'ߪ?nWXLM/zx;Ķm`x-q[1X (h|mXߐރX^$-P2s58+jf~S<~5Vݽ3'+(wWkn(c߽ }*'u?,q`~ؚ̙Hy}˂_`0\}s6:irڠ&CM*cM)tILJ.aT b)(0,G|.L\|PCl ˺W cMi鄝횑ۤW iJ$Jfܢ^xI}O3*㣒(jȧb1 yyRV%dshL+cb*9LlTbeŌcE¾Z+բ^NLv IY06䶪`~P ɸbu&lIpqAX/ysF|Q9v2dQgQG2X6B̂-JsIu\XSPSXL#pBBDHµF .h CmMhkm3ERd̒>hjSRGO>N}.10d F,_{vFhxEUWSc8:Xc}.Fӈں S (WN%<'Lup4 x<gwf%D'T~O2 C+<i[b:Z`מ=jKC/6l ~퓞8 d'_s(6uzXffs sڱ(kBd PK zTJ-8+L]b{_ߐ PN 񜈷>f`ōUad*փ"<<`L"ucU3Z(0uh.eB nh%ލ^@+zIRY>6ETY(Zf^I1ez޴G|.*n(3ZLx;ϢPVo*?6CI-=RS=-n/-eT`o˦:XVuj[ۧB+SQ*6TW";K(B& XU%9f EdA{~d-hnl)ZDLVT /CP#';'c3lkL:w[׎!Tw ;Ty}ĈD-Gֱzysc&Zހp/ß.nX\4Qu]g|}`c(-)GHx^{U,Ai5'NƳYW:h=LFB-TX=*u319B&IzbJ0ƼiQA"T/Ze4(qKxzVI0{)W5 8h ,^L(6eU2Gw;ZT"[u獵S5ed9s\PʪK4~>NU4'?b^9Nx/bGf-'T_q[ۻʊ Wd}cse;7Gr[y O™_{XЙ(˜h|7 f՗K{S\ӒR梜1˗,ӱo~ܱtT>9AAS[KEՓLu|,^v0D3] Y繹@ed=ckGw@75Fus)( lւR:~"2u=&N@fτ:-l%ũG@b1xlbx$q>XNUf+t,]΀ڙCq@sʔ}U&LJ@?[N"'kc+I.,@ ?Ug@Av&ȉ ju u#b2Ϥ燬օ@!vT~F%m_O?ZImյM Gd }n KXW=7tlvJ .,$#o<^kfA+{*\|}(l#"A hCd.@8U_~O`jk`N&[W"|z \8yEȨ)$WQ5\no~E[ZO3U?0V1c i]'vh̑_ձ2W J˫q&Gn%Cu JTTg<','Q:ƜL=K@m|dyڬmQdv֤ ?wڸ˖.=vGgC i`bWX3*l|AKdtykEߥwm,#VN2+9/ZyT-s#x@554g|R5#;bHl sؓL LK*!|$!yz,gR? qY6 'Mٲ*t)P|^Yi̡M|QsZTiгUh@0рO mNY+ٳYC5hga(H4a*ݞ9<6Ba:Nzn^ P ݇~}-P#3bkEֆiEu qòېDASc'hMS:O!*v3p=%6.NTabލK1u,2aY0j 勍;v4/woEiA|lk?s/K9PAZTrQ81ߧd{0?K7T=<`*֏nTFzX|͈qC;G%7ފ*6!:WЂeg^mCzu0cM,r.E%tNun7aժU$ogL[;P:61}Pq:xM{`cffk #?VO$0( })xϢTY%hj2LJ1onk! .-UM9UZy;2?l'|9 ٴBBrD $#Xl  e7ރBhGwBj"EО7r&=|f-Dk:x899Vv!W2f',"OX%|b' = *,=*/wsOcE}WeU,7AEJ!!*iw[\~ _Q4*=(Xeŵ ] U~C&OS=sx+NfK=l _$/iu#ky\d%7]ص;kGs@Vy܏J+XŲ̺pƐ/eՌ1QQpGjjvُn | :sUa~lwj*OiO=]={`ufΡv|VuJLI"u'%]G:}[Bde,zͬfDjV^_\6hO $5(C74<:`{Uymģ&P)Tl*?&x+!Բ#kɱ႕_;6zNl2 NEaFDu3fG6V?b Yay1UԜU$ t9(Eit wIgWoeeCH$멽#IQQVC^k:vIÃzFBImq<ٷk-Q}\7WA\&UjW`aG6 6z;T>ɇ%זki)uKꡣUMCsk4dV&m0%:Y#&Y4֬b5 If8ξ{Gݽؼi cDfcTF%iЁ\&`y b$UDԓdHVF=E'l # 4kc&wV^3N]uG`mT9tAu5E-ڪ}x)$ >ΧGoݱʘ;iNPt&E0!Ѯ]|fĖ3gMw1ɎjBoh)ɗjTpE[AQ2=A~%Π]aib0oFsmBnvg/cjZC\-Me_H2ANKk/0Ӈ`\j3TR\ n4ꃆt0ֈvFڪhlC|̊c~'c9CŨkBZg;`j!@P+c궰FD$ر(7][$b=J8.`TVva5+i)LjZ+응ž=n)gO(&[u%"A[} ]~灆l4nc.dԕ=W\<|٫w[Tk듈)q =ն=+-)X/PX'*ZKs}k~CXT^v뤼5*McU =T)WKȞ-38w|GWX#+N\߅,AϻR\6heOYشc>ň8hO?d!8~${7_ػ^t cu:i%!h2 u9Y$~(fCKz4PExLnoPJjhVb&>NRk.Cq"st$f{|Xul{ki=96:?`@_OmĺoZm=H1Xv)۵8\˗ʬj$a!(K9"E,PQlX+d79jdu52g-oOCz% &R68v>v~@+ӘЩʿ^r?<'9q3{ ܠ+X(DM 5 iYFUlB XR[zZ)@tTcd)@4"h+5fΚ* ,`ie2hE;͌u}?s 3Y?,!C`I 9`ZΤeF=՗MrfO_ > :t3OE~my;);Ů+i)p9pB|ᱴeޕ 4Ɏ:l̩FI[ߞyLT=mḼ8Tnm1EkgGcQrYIDle|)Tfdo/¶_b0鴃6mdQW";Tb 1 ӇvG "z@o\~'9/<@ M;Qϋh-5>]W뺍d n% MHN>ɘ:|Z*$o 7\#Q?@VLCM<|^S({ u66o>=d *ߩO' ]~k6?|]LwZ*A{Z+rO 0$~~oA M CX2j$3]l~fˋ-vkG K. M3GX/YT0^j5:2RZ.4FfoȈp:sP+'ּ rrq]j.q}#wt獜+ k Xm@ O}0W FjCVKy)|6GwW<ضֳV"kjK{[H4@af6ڊ dDcs sO}Ib<aR]^dUM$<|p#]Bb:He̾F!|+SU=!)҆";X+֛c־>\T ():he]@brMwF@d Z65ucp@&)Ta?_+ t.- EQ;:;Q8$[;!9 5MTXAkR/m7;=|=M&ްu^H=[NທQCLq}STk6!42Kv' 9€٤g_U mc 8әˤ JBL zV5qt*XP*IVc_sϡxT7#"T,^FZ<ށzO h ^֤&I=" Q[w:y #҄/º=I>Lz⊕+D:]%LD_[d!=~|fW(elBP~ n <}'eܒ:RoNMǞOwM1amn'N.W +gYv;sn/Ԡ߶!TOqץ6<-U)]LHѫt mW>(S+E]CgQQW\)M 8I&z d[,/ыNF ܄~%ZUk< j(_#ȚvF?-B%_F>&LDܑ:}dpmCpGu#@4#+ TDəmBs`WHRqaBR %Fx-4Jًhs">6g93,O%⓷ޤ~1]ii\k[ ZJaABG'U̩M/QfUM .y?pgQ_QQ #V=m:`GFm|MI oWA lp5f<i|ӓ+= j>{O.-'SY4C;D/Rf"-㴼U!y&7[H26fiCA(y-Yܩ`QF;h_qP=s ۱/ɇ=mnRs"tb N̬ /peի% d3͗ C%u %@]]QH*\D,dx7mxᠴʽLbo_f:f 7&$L ]hf\mQ e߇Tq,(2YU" 2'l.I˅sn͜{'OHp`C(XXL:*/)/z*lrm Zr2g¨~5V :\-uy-TQan-=F%/ 1{?fמq\8w_mڄ(FIlFEwXNjq$j|˨DR!*2"vJ:{8jl*\7?izTEE fv0vVy4sk`V*?QL3Es~rQThen-b׳AOC4T @Hhn PB)Ee+$PT޶+2+ϙlRzL+k91 ^1*E 蕞|?*\pU<]B+J\$KulZ+ :L`]2o&z]ZSPDs{4i#w)JaT-x77Ξ%mTK Npi#'8"q%6-Ʈ$pH*mA  q ]Ҭm膧)˨'`ՏejP]E#fT`DB4{FvNH&`gj$P;\_@Xgpe'8{{nBQV*`h3i5D[jpWXmmpzY {@k +oO2r%ՌI.(gQCBҽq9J)9;:fַXAW[Z;  5+[)jB1o!Nk"A#q(huhQ]A-YҒY8j ّ(UuupsǪ5_"͒.8MJ7tWUuHfNY.?>$&KsV- KFFu'S wSZ>VZ0!֙#k>5\ZS-KkQN9L\#8P٧39E5mPSa^G:b ۰ց9[9Z@=K \B:,>#[Dc1-|0eY3g}|Az=8o &탉ax'Om=UćG:zձq\UUtTŤpoܚuUult]iY>O9vtjx/!K,Gw$'sZ)Ij&_SM쳙>=b]Q:M1\"HUqE/q}ڂ!ԸOi:iA꾭Pcڸ{ZT)&Y\R&=L~JqKvERr2-0;;U^Q8*Kp w)!GA}T YcZald]f6L$U0^~O=N%#bCk+yPqQu3??NR|ޫ9"Z1#,ۘ04;ӗ̪rpqŖj1~uv6JhD1 LYY۞MjN\~ idl1-t<"dz,>ᱚtrD-^2Ƴ< 𗿢9n\yi;m q![_~eG~ \Na"&5qE'lI;ɬZ;K]h`FFXH8"'ͺ$* Cߟ~z*iGfPEv@ cVI'9,:XqD[~RFҕO_4Wa%ZZp&(v5Gvf:ޫ$jC])eDvM ~U أTttZfUht%EA֨YÚyl8?"=3Al@9 7ah i12pUHӱ)z.|{6q.:Fř |iĢTIWg/dq?q\ `m}eIȤڻ[vsiCoSG`nSut" u,Xi[R'an@Z^6̰͑I#v>)@ݩ=d螥Uo' # ɳ瑕l&myE+(Y+䡝ؼTvwFMʲSt_]6c0UŹtܙm^y+d6S3W.EC̾n2?q׽c(šd߫I].-Ρl"#8ӥ}aHuS71%rSC_higPxj@n5☙֨Gs_~`?}>Jt`Խ""I؉^pk\TF̱ǓO=A{l߷5$;c$>G U%Y k2knJ`jp@^zyT`Y[BFkg#ߟ'5ŵX[iyF|~T"(nW7~+\dpnUۯF{yw$4^Kw֯*U|dc\*V(URzFR%.ϞXz=K^wmH#fMqM=vݷx;y*Jǟ $sg5yv(ƆdX^~r;SMScShKGn *jW<*x9LhC8AOo_YUch^Ev/a[UYi dٷ .~YE%~8/aUo GcoF%8ڋay:nZb0l2NEuUХ*BL8f⼨1Z(XeU5hp'x|dU,kn}Qu WC~df 뛑xFw?(pS.;һbҍ$ tpt.UAf#@IDATޡ᠁P kmC#M kǑYkM# x~C<f2ft`5h?K{^ف u04:g NF}TZ+0]z֖U#Iͱ-tX_0vfH4 \nt88YYI{. pe κʆ_7_?smbjH.Ds[UtI򀫷d]V L}O? ftNpcHH'ؘDnc`p ړh<жvѶʜVGd!8\*|W׍ J{iK+Iw8JX+0F o7/d3Mmwc)jZT.l!N'9 qyf&NO? _WQC+BJqtB,U]):2lŶa㊬ٻL]t1N ؉'䁳Sq20 wc8gR5loWRti&X }>-a=Ok?XϞ }F1 gknck|uPIx/G{FRPspQ Va;%8+F)\:7\_Zc[h<ՍwEf^W1RNs7|úBݺm93 :23@,2ex3*=m"bt6hsoX2|DM Rt[krxS\GyY%HM<-OG Jsx?s<8AieՋ_/:k-I|PZφqR~$!ts&v͚58tM}.'mֆl/ oO;F֛ 238Њ2wԀ{/!<,jK94c.}1L5b aӦؿ} ~63l,a'x߯gTXزN蜅5c3I CZ+fTىa=>ʕ+Zw3B rP9}A\* uaAfvv0eUJW!ĉȟ׻ہ^cWHI΢GHN6M{/,;G,JFXsY_gb|G>PI^Je@ 975":dٳ~f#1@y Xp"< 1`:ې{d$ @<ǿE71+`L,L-BI5T"t*,-{ {HFbTp ~ ]+ `fh ^,1-2n0s ,zX2':P^V"y]'kߛh,9S o<s@IYLrfʎh# ،9)ǘZYuP!@Y*X^3gP&hE J~FdSu17ߐx*m3H24Ї |~_]?c \<6q*du63 W uD u*yA[tKIἊ ᣯ &m9_gVaִXig8*Zk3\lX&8pdP}{v0GLPpo2R1SajZa]zaa7n@m~5vf&WgsaI"4(ʳzh `iA${O $/ke-ؽu;(Sk#UIy%wN5[~~>>9afFvmw%| JIp=nA)8z; ^҆E-RƪjI631d-m9*[KۄVFߴ gŠ4U܌9eNRiG'[G|0{Ge^m6{GBK;V=IwhZ“a#'n[ ~垀={=N`ZtZb6!Ÿ:kJ~5,t6Um*?n!!?:[67RF ϻiż^4)]<8oʟ٨UـfΊ1'_Ep LjqdjiGl vum+%㓨mG"  (mBfiYo >ޚHM͇1k|r L,6t2@I?'_mTwd֣-B1'7ѴC^ LK姑.&M'綠qKy^&S5);dwE7nX %&Y#@wz qiѫ"h5`+\\ bX?06Fq%Aaע)`jj.]"Ȫobl x[mAUc hN:T!kk" fհi6W>I,w{هobނq)8t|C(X#uAm!+XYYxXT{qÆC`il~i<EEE@c50='?$;DiNAetg]Ze+s 1uu<#n1ѧ:3WHˍGn:^0SiVMڊUa1/hvw!C>-0&{_`ֻ1amɛ0PU_a3*s !wzttTr{U^F[@9t-_#*vД9 zh/P^MUX ؜"ij>o361.#BZr.![ e䤧yyoeƻ/ A 3p:o?y*VWE ŦሗjWeӎO%tΕօ˹\;ۚH ~=7ͣr>u+UZ"ԢWX ]SJd`bT8P Pru1V$pbV|J7rJZG\C+iwXClSc(妸_|/{-ou C\XlWDQ;OOuEesǺ-[ĹJGUxǏ#*vo-)#ކV ػưXϴ̩47ӄY?i,O樞}O: w7⎛oğp7׫{~vGJ[55־<>|7d×hd ϴ h1ԁJZ|5*G2ukc-t4%mFz@_#ibeH'M(,dHpHOF~v.Z uחnKECITD)PIUݬy^ӂĄT\tIbV҃y} ]٫g=C[ؼLZ|kKH4k 7) %p_c.[{k\^0"*pvcj}M!-^ի.>tUpKRG5mMPYQFa5L%fBUTr )~Fё i| T ǧ$q_01v{hKؕT5%ۀ}8Z suvB[1HŌ a˼Fx>mAva :yJKp(2} ơX1rHZudif2BSE$n]47ifj;s}, D[C 3={4 ~y)@GKg{͞fV* ]Ն Zʳ.r@o.] vi,0]WK=KCr2nb#Pƾ$ikKSdgSF4QLJ@@j .{*Tbb&d/iR GOs9X>ǎW* @&~voۀxH -/+(3{~GYŞ!*VŲvUOSiT]t3PlfS9b 75v<I[H{]>}1}!QoV#I}8p騠7:ھ퓷Zm~X{>TtƤYCvdL_bZ|# lOY|\~<ҙ6C6?\6 X6i{]`?yl/NLA@KrBETNs"Ut,& _oƼ%^%b!ǚ?/X" qM7sc7?&*`zPqC#٩qtwqk-hi` 2xG:,iFP/2R>ܨT Qb3.Ajږ_1ց>1mHm$4#1`hj ڽ;pdgu!ra4Xd5F[VJ"9@O;-čYo^*9'C=uU(JFK;`xW@ o< O7r6WH;Dkd]ҕ+|'+Է(An۩Rg܆.8T6jLZ2.?Nӏ/a仇bd-MX #,`VXn/et73usҳF̃,j*u*AoQ1.jK;,ݼPE U?(/Xt,\z؀3f~=mfIeBwqǔ<STr3ǙẒe=,&#O _|ɜշطM7?|WiƱ=yUY= -s^,{ `ŗ6*o 厢?IC-`/<m^,ʼnf=tlV-;zгS\])sմHO%A V߱w.un a]> ~6v:dŤ,4yu]d,Vd@q# ֑W}e_IpHVloǖ/֊]$Egp:#X6<Of D؄njn*93hwEQ<8sW-'Ų^Q%M֮|ҳD&=/.=8ܽ$wVsWM[g"8ԖWq;Zg8s8^bhQ~Y3H]taPGɵU/V7sP1XgZVVڬ5𐍯XץA<.GzNQ ]S5i+:l{!])LG A͜= su!{dٚcNs|xYJ`Ѯ-XJi=UUΊ6l[1ٳEa'l@ĸbC >zի^孿cɵ.G#hlkkBĘ\{fCQ ca,却dI >>m@r E,DJ—֧i9RF`T `c`p˄Mjo- C˔49-Rn* SG6^[z7 rKʻLhkfA}2%faJ^z̡ov`$bU НIς j,2u2uE,xިjQهtt (z*TTHꅲjj Ю$mlK`i;Qx6y7R<$A0?0LZYW iJ,3]}H0O؃QP8?A@8#Gツ=d1_ZaM޸s-,BY47-PnS`WT1jR L 1%)@*V&8Kwʵ6a1ފ$ww :sv]Cv<mUa2Zg]0j%Su{;u m$Qr*o_'elJ⏋'|Grc}=Ӽ`gV"C7LCca<^nd`fX݃_*gBEˡ ?`9Fdx&3#jm3 Fia͘HM 9WfіAҍ|LZyfL]׮G ,$[i^,o.8\}mtd0oW6l~tcMHQ=/XI_B@d1}Jz6=#hH`EfCA??v̻wA.ɿ؀+Xs PS@[箢Ɩ6O=dQ`ABW٫LN+;$T1Tԙ% gc͖qYDP?*7,{[LN߫^Q.ewJx7Z>WuȾ)V>*`\%HYֿﺕ.F-vA ]8e:Ҳ4mW'Ú-$_ۀTְo{ony&^$DB.uU]3 "E I~vpja6tHR:2Zݴg)4,Rai@;V h7@MPKCM&(Hؽsdi'<)Jzb}#ۆzO^  ۛ9٣zT7AOe-%"*ʴsSozsN =i*( ׵v]uok/HHH's{8+o~{ܹ3̝;>99 mzk('M% D'tX1Ij,F^&$T+}=BArpchoBnӘkisNzj B@1 Pτ9Gc)g8&aꎃ#VHwcY$j(.0נ1:;&g݇soNg5[ي_oc4kAnQ] .큺>zT^wUm",:LB{U% E ʯw4QZΙFQ @Cs'&M  LH$ KL)Ӿں25UǚY(*Ȁ Vְj6f0}REpsU._~z4aaªSXE,~ꥭdOAK^7aADKhm#+f;UpL"$*Cn"e+v; t Qku-M\MkbJxKk\ϵ˙/gd?sfLIx$>A݀)<4Y TOY.bh ۯ\)X 4㽷9 =j)VE97L2uM Ǫǟem)맇>./$ P24ԗ0a{mxstF!'yQ0:jiQcvx zLuD~\1%l 24%Rw 99AC?9 E@* s!$䐆I|!Vs[]zf|24ț)"4d%VqP/sM}O>儆7^n3uVyZ 4܃FdEG<$OSS W]]q`\2mW'}$V p&[- /oe఼ޱ`GXjMٮo AAq#*P"> 6FdRǑbIJ釉vV9: u֌A\#(~w2 Gz,ί1*fe]?Y(˟n \)S^ct Zbgu庫ñ 4^YNVRn&9ILxtPj޼0E~w!}ܼG[Fx`5ߡ_Ÿw*ƚ!.ht+ ]jܠIg6Go&&Y]huV\T"&D&N?_'y՜?#4(xEKȏ?SV=t'QA0D )id}zϤܦ%m*k)lC8E~f"BcRb JJ (܂nоUJE_B8󇿈O8O'a=VJsz:b2lw5,zTZ;5`6y&tm?a=1^MώN8RJy1$;'Ht\2EI MWj*DpcuC!V>=Vy+/uLb o^@*l UT]Hˌ-cvc_|) ŏfs#0it%Gy]7~p♜_C<8-\&\S9>Ɇ/$/epL m m9eP@Sg: ҧ=$ 1nW,'䆇dg2$SW#>nͬ{E_Jb$/x*NT :ay;ujwg"8ԒkGe(ίƱSTtQ*X] G/G5SI& CyfI$>F Uզa3ՈL(`ZD L< Q1'hA:WaӮosWF9յM503]늊YMLYaڦXtl6PfĿuu*ެ^4F ??oV7ҧD-p%g(!E 80r `k}4&ɐE7MCIlǁQTQŠ^:y ;c<++P* kIU2FJ1c.8Μ(ժq}̭$bJC{& v`\q/YlOZ` h3m*a#'Zt!ɂUu=bnԼB%B yB9 ,ĊF}S'H-mGYENGeu&Tl/AGUcVpv11'`"pd1nr(-1#QH\xUTnǞ1$HrƠET+ͯCX5IRkꬡ- ӻO塸voG>U("nUU=N~0f壒BN vkA5ύ_K/4kms 0G1m*@-((D[q"AVԷBgMxOR]<*|Ei9 X77Z2DNf&* S=m6P5G S`xw?ooSQn/]%V藐V/{ee8"Л#)sn f2W3dUhsK}t@ v7֘z&NVjqe1vY3F(;{%8_/^}Lψ:&(@/Ev#N]nJP] M)ԯaؚ jࠏ, Z~?-=G1֕ҔXxT4/-Ifxi:)^+UQ*VN /¬IFઘD$/gCFdop fe+*8J#%޲ CU>>Ǜ鱗3 }| icB G(1Htp2~QTEΆ/9`m?I pO\ŠՕ"`YnCz U+ksIޙ?wP~dXk+᳏0}3__% 0?￧_=*u4z^|%b&C[Y3s;1! D~TKO9V/)6x_Ĥepe&z51qrVϡO NI Ճ27AOP_[\ UL5 ,"] %I. 3,RRfoz #7>DŽ:PmjYg"dwuUÎ=I߂v. 2p6ueB)U\^Ӈvr5ٗ[(BzU+^z5~/a7aːA%"LwTL &p\{Yu}ԏ+/`Xbސ2RBɛ8Z+񞟑v|OTiY۹^fx#4t2 .<}Fdw{څk?AFJ2s7_cJ٠YqFЏU [㵽}=9-$$_cCuNS Eyʤ^ZXűG~za}(~8sZcSE?ݻs3cYX]fͯ`/VTFU0?cbr4La8N  iҟ*NSN1eߏȣ֏c288IJ$묏_Hg-sg> bPXK1.I Z,3V\thO(oZ-{x-I`ySmXL43i6oفSY`5WH> TT#ܐp(Muh+AqQ=C I1IY$pzCe즨fgՐs%f A"Sa# E+QKv=YŪӹ5s ۚ񫅅 9\,F^'@L([K֪aÎGuQ1耤bfd.YEWcf VSzל$P '@CX*HzC"u۷m;P8UP2_hK( lT3!lM@e[ukF6@ݩDښ$f@JwOS\*q}3MG,]AyV[J-٨MIb I;ru6A#uv#FEu$ Ik`tR>Ҕus8IUFT%pW1X8}"2ڑ4`-s͜fcoG }'ћ=82ba{/"1vzI-clPJfwOe 6i3&566,K)̍id fM X %shRR^| хVLRYZC<êE,?u:1ur s"Ep=ƚE/ +qjuȬEoS!tM߭Đs|P6뭼XuYo4ʴ4U8 tA\#ǫp%K"~m*Ո*:EIZ{yCQ:O60bGҋ8i*&m`; dݾIJJ ~!5{L oӰHwM6((Ϸg&z`X*Gk,d:{)U1_i.9b{~) kmmjk**d%fbpwdt`D|,u@IDAT:$ӫtd+5AA3c9אM {sd4*#,$ _,Z<ǎ<\ȐCY|, b =֨ÑH&H1sr7<]t6H!sG"2@"7a;Yε H*VtX&u;_A77 `ˊωs†^[Q"Ҙ+b21~/5DOt.M /Mv&>;woǛ}B\5pڡmMiF ="CUH_G̘*0!p+m1ϰ ; B|YfrKX⇸"1%PZD]{$Fž΀k k hw%CULYZ_/+]p<9[nǺOסV 6ƴ&99ZWUUU0M%g_y?¯)Nr,\JXOa[ tGǥ❏c뾇{Zw  WzՎLJm|@Xz߯2OEa: Lݡc"2Y9dd$AZ wkoO?J?\*)0lL&YЕOzϏBEi=i$J}C>M{zӓfɅ{VD5v^F AHewY]*fI#GhӵcgbGcc(.ɅAG߀C>J^hwl 54lY/crFr"%~zOTYr+ի  Tw 9 /ʂ ,9\ä.dկ`l*NkSR_W~H ya 短o߽'LXJֳ;ގ[Nsf]G.c_DBfK[(9HZQHkց-19=>?ի>r;GgPKpoo',s v0+7 \YjFV>U*W+1?>m'*ˤd?1dmRES#!{|)ms(.5Ul5TR6`W)xF]-h+~| vf"hg Z>' i9`ڜxGDkb?Ԅ,L!!#9X4;o[=@b TE#4˔õ"flk7x'jd5R D[C -(KгA_$S\C*0eNݖѣB7hGZ=cK(㵖d}m NDݓ)c>x1誯b %LYXJ`1sYػ` Z7 *F3 +N/TuAܹ. @xg5ЁVBVFΘ=fZH)wh3x!s b9ĜlT㶕#m&Z^T)!A<;ə(&lmU+&$F޲Si_;SmP6ga^:Ռ*3YYMV%֎$ҿ1uAVMWЊ }U$I̜2(U w'3v 8h<.{gH=fy v㇨T6lˊSmͱh#RIU<:j-)z'z;Ĺ'ы-A[u1fMj@q +-FT$_±7k6;OMDaN(M\?EH3|+cK]bq \NC-&*]oA:r 7bqx=# Z2H:V3 \@2W~3h3RI)ٛxNyUXx D'$7{GiZbJJLLQ Sb*m4:}ZER4'4ZfN< WmR@!"67'5lVy=ll2x # decoDJJ >_!iM)ڲ\>Oj)N ;퐍^AiRbV*ۨE[2rD2J2R+9U% LԔƔ^n.|ҾߴSyjdD29zF~lm ӴiC ׈C($7Kn[1>pv̓;J s06tIC'xVP 9(g/t66DĬYȯGM J=Нyor=,'y^X837 .$~d#+zqih,Ʉ3j) uEXp"W5k{4LūoK/p7EjF:Jxӵp@Ǫz&6GW*# )Eۺ;*=dG@OQ`,=gƩ#д΄ 6scPA r{K+-+_ #V ? pu8z D221CI ^^# =Gzd欒֜z[ӛU VңwmbRFEeGy%9 oW `,O:C[ Yoj<_sveJsW]'AA!38{|hb!Mj/6mIHln{;PaTUݽAz$1kEQy2sW%bhUl';$ * u,U' WͽE~@>Gǫro|UwK(+Q51m9'1Aʀ)_чIRޗXǣpwp9SQ.9;Zht7WG#9 8TD^C§bvh&/̤TWT&=HN`kI.%n z>x[{\tȣSJE'to:e,G_oOg? c+_g,?DNJ ۚs|iܵ<`maD`Uɀ8.?pR򔞡)bʺMAyc'JXHb-Anz{D[8",8@+ݔ hV4jQ޹~M=AxM+Z`J+͹L,)QDb$$!<nn_~lJVӉD$ɷ}ܦG/=ku4jSyxY(mf*`ؓ_ q)]RWRpre)7FK vo-kO/kģhya#b~X/doX~Y56h ł7@E#JPModҶ$CxjQ-iJ&ռ8Yxd&LjF1j{ĶkHrґD&,]h\YH!\Oֱf̛Prc.5%\Xkԃ-5CQe-0 J kUa) Blr)n[I+,^4o[+^n[@Id)|$Iߏ䘃r*@#?8/oҋȑϤg>{'= {ܖFYںXQjե lkeU^'`}j@euq1xz:(T HʧTx| ͐ -CQ}< Wslj-)9Yy>Mu<_?nJG;Z_}]0? Jz$+mwTew}_ƺ8z˜VxK,I'{u$̘-BÆv+? 0t},<S9 3'90bbcz^?&s^D:`'b 3kK8ذ~Y8g'L Vَ _8/71|}w9~c}:xY.h~,<)*zjbA%NL0}ܧ/}/~OJ$ U>fOJT O>!H&رcBоڸSMk>l𴳹rx)Iޖ-XyXB^~&F}w?R `2h9J1rVob._y.^z".F}".B)i|6߻r>ת-_[Ux Eo/m^.._zQVqE''QÂ.cWD}\t΄Z8u)-sn^q? Z EĜ$G'y>w 3Uc*ڄ` S&1q9-qʰ%sA ib7s9w6V`G$ QNT \><1b X?mš;"H%ZZ2}[YA> NHJMV?+h&$jP#y[Si gWw0"F4aK|(YkIG|8Ց5g-=:F20@sfF:Rp,>Mʍamr:%,&N&<:PϠ5 'Fc̝܋8dW T-X]r`a9R5a-:[WsgU9OZ@T;Kh$tΒ۠.AՏC)J(¸݀NTp=?ϿV˘ 5]0wCyº=ш&/޼ MɓQ[\ 7_/VHff&@ 7hS1>3kCq&(wL8&p%O{fm,q9AuW!%eRhA5Lp;-Š&h+ՁĹiﷆӠ:fџwDbs; /J4Z˞fD0 \9UKJj+B{?g#H5qmv}M7Ʋ:*45J,mMC[?d`zy_/<_zkF:4mZ閵hz&?#;4>xg|-ܵ9v—A!ej 3ȢtּP J5T>y}8S? J^`ͧѵ)+YF7$hdugU ~@ $WS<0(dGӓeļsk"%vPnm !#V'Q%"Hq9b*ǻ+\ԉH|ף_X+Lz#zrkiiCBZ[UNxS6DägMuJ .k?˼i1ob `(n]C5{#>JQ`UedZX9~?xjiÚQSKPՈu}Atwh>v(f_%ÒS6Ǜ';Q-M2ǵfъ@*~Mf쒘x-&ݰsx< }?c;vb]|N֨PAY^ _;t梛d ocV$%9ARJ;ݼJ:4l}yX KyxVx.ЙF(zo:Ix=:Cј7klErM:$1Y>1.mnA5! ?y?zL9i5d2n}YWgd% {὏?6Ib@&šh֢=|(Ť9xo-X9akFB#}"^M["5 P^E-g̈́}3-8 )0"7F1 :̈́F4Qvʤ(CnA֪rާo?}x|e7+7OPKL5 TWp8e 8>UH-t Jd)#ɱKpxVN; S:oohߌ 6+ b2zi F%87eIoqz:%qkv1ckъYY$ٳi9QY~5Dd[$݋T66%,t3p(*KHUmmj2Ҳ%UV$j7z]b 33-GTA`%>+Ɵ*Q$kR9T}$޳61r?GǻM60#m,rS6GGDR1,a±-CU!r򋐲w; N?<ܐAYICraG4ƬEk)'ࡣxW`G`jtZ;0iTz{v0z.~hׅ>ɴ5m匎M]1؅ 5XaiFޘS hjc @7!~@ PLSv 35 Tӧ=Q h;SXFhjюA c۴ T?Rh$} `/1.UaldNUS$$pME-*jyoV' F[26{TƤM0Ez6}a#ngg-ߵ^Vj26pYR2N1nD1=jCi{+ "19<7CA1V[RX-xӪ_?؋ j j{dB2Jf1F׷q$ o_b0/U-HE+54uiPU=>K[VT1k-N:ӿT ~N|!c8\Q 󯵱 ۷PLEuy7q#@MajU5}yDD=R:ZSQcgv"ժ6&ܲ>jj!#7:1*{}xCIEa#bTQAޚR3nS1S>]TE$``O0njn L H&t?ڨ5.|DW-瘢*լn}'Ѭ"Xu^\H٧P(ZەrY ^2IEޔO6ykki7S;^%rz}G?wg~߿+Y6,/@H |CLD(fu3qȀjB:9KvB'XKc-UE78+,ċW#g%԰ceg`߲S8[\4{yhsKm<<|Y:\G2en5b[m}Ę\̋rD }4ZLM䞓NPJ0-=R’vc5:d_<sQ@_#C0}ѫ_oZLI23zhFxM!j@sph+JWOPlΞ25_01@*ɓBpObʿп5v @کè,DeCy6DZzt[go(!4o0rBxL_gkeZxrR814Nl#̅=>鋖l>G1SeM&$=,H9V $w_׊{aԴ?kա3 | ;)G6yPڸB)hQQ) 8 <60$[<,Pˁ{bqXMLn&9`"SW1؊u Y 'PZش&';(Ä$#Vr \ qCj NoR&di<7TWI;r9Gϼͫ7- w@׎L|NZdd̆ +Zx.ywPw¸gУY^uv7IA-6m'[o&<QJoZ4WV df R*RVclm<i)I:]zdˍUΊ&~~ntvgCv^x`E#i%I(qmci]EOG7O&(9 koC JpT~+z<ו $~C)t(l.^~ bcbVk2?<6z*Fc_վg;^X'SJ9b%\eUad`Gݮ6Zсm'xb ]Z$8-i|$ƭ X%u-[[/$UZ"_s~q Un8 )/'?uJ4_;__~.X\^-<1|}V3z5s9.-8 wVMꢩׄ猊qW%Y5`TMTԠwKIhnƴ֯7AA( Ϲ t?N%!68 Jc{f:zg oJa\'&![%,]LP\KZm*'Qj 8v=yTd`{&_; .NHb7aVcuw@yv@ ,*BJ%;©2Vpjb;H8J$B"1Ay1Afфt>F"OFK:9>e Ful= Cz2s8 7y- J$'~/6(f_ 4:`ŪnM%<yTRjAηjRx3;yjSI$`E!&@X4 Rbm"O๙|2N va Oc B0auC(eN Ƕ66CJ>ٟ>^#(m c9ϫlVIQ2ҦlM+"t(^-EnBUxYPƸY$eU?+n{~/T [(w#R-H; `M:䊳.)(*҇ۢe4cbMjxWP\3%z/YSբq uY!jAM&A&Hì;?M&=gj ׂ;OšrO aa%yl_^?wfdτRboPQZJMHk[CxsUQ V&;&jnjbU+bbʔ 赟R] \tP'߆+zp74cXyc8;؜vXՏ_| CF}^ <u I&Q*G9DTdwL+9@hʛ;qv[3YA],Jcvb֌Yg9GCOG ,=_s5;/oY6*MȂ 53/Kr}6 ttΗܵuwK exI˕<(sןwA6Ҙ6m5%bp]QZ`;Ff NDpsPUrP(nתIkРZmJmGj"P6s͙ Tl.COKU`~3"yy3oտ?Ww1Vql$쯐S5f"_^X OA4D:(*ˀr=cpV\ xO?ԺJQW;/x>u ;6QX^pD7ݛ qmLX ^CeM :kѯA9Hmm (mgI_Μ(JFS^8dSTyRrw`M>gJ*W RǷ~c,YGӌIVa5k0Gɦ0 ^ {c0I G&0Q<3VtlN %W i+H<ŝwA}~<'@iTC@$IzXA rm,H4ߧwy'I֔qkW%X#SQK ̉%|(X;|Mm?x܇PQKtk1*z1w?@UW-N SU|Ey]'\ݜYTq< *Be.Fƚhդݏ9D ke;NfV2^SFo Am*"U7L= wm'.--QދrŊDz=k)Mf̌3ByB>+ڠeoō!?j3}2q= gzWk1R] W[J:7,Wf̋PrAti3'ƞ _Lf 7eGyJrǵ }2tƍl#%'d VrAj%'0u \hvR)BϘ73kr̝:uye%,iyitVb:9<!>4,)>mή:v V`JmĞؓYl`d)HL{zId;c<UQ)7-H=%I[=d0 Ei*TګQ_!IŖbɏgxb`OC1c\{ʈ~\ċM7 =7~gW4Y G_c^sm` >`FVG?J ERIY]8e*,K#>7J0:#M*; 2{mGu$~&ڪlb~eFY1"fcߜyBJ5"=|@L= c3 ^Ɠ>Ck4^XDVQQIF8<|5}Ph)ZhcaOOmr&Vc ;4YsACgl V,#7+^3g俐TUI^!D KQeBiQYQi-aD+|\O3g BN>n۲aGVPбӋHZ,) ozh$QU ,c&֯]x}=~QhL\/U8&1!^>xCl%L]2]|e~7^I'AB/RD@bvv==ޝ]v *]z5${{މBHB@Pfgvvfvv}O[J(55Y5PJ>:3kv}ߢsfb"O*NDRP(IdlXq9޲gX$|zPj/<='A\EFPTedM':v` ZFT#$=":m6 2X1KUrm:pZ@IDAT bnZZ}=Q@o^^iBOݐǬ直YS'#3=䅌<+~x7y"OŐPw:2E02%.6{ۑC5o *fg S JB5{t4ҽ<$+k13" QTbD^H57ۊy!Ԝ"u ] }cDHWK+?C]^y3NO;p:$A"z[ZDVrdj*9SIl74=t!\V̈(3d&v M2fcH3hVi9'‚P aޠ H1:$ : ޝ8s]ظDdT 8xo1iHrdCZ~>-KyT͍ 018%:#͟zb `9|c"F4:k`Ut)M*!o=Դ ^ Lq-P)E1E  ɩ0w6O߻=\V@C2IkA=FzPaA@b;jz:ac >PiF$g =G=m x![8:vdҍt{aJTu$Uzer]yC$Db]bFX<Evvg_ʞտSjAo_6tL;"O q$kp; Ep)*,`A5oōk@.;P;|xhlMfh yz$I *T+OX;fJ4(K \yE%^Ϭ'S=@ n㍌xAMM7E P{fH+<aT9Zv# 9 uxC lkW.etxMX5z6_x Ęa'@J>25tٗ}Ks)Km$53<ҒD?`\TI|B4%pU pu,U,Cx:f`݀,'Zi*lh_sFy9!ME`xE!<=sYX]v2q/PBE&B+Wkr͏?fĊ;j FViӽ EHK-pr$2 g47V,zaOg7( H3[Uz@~:Ωwݶ \Qv3.m06ns).YEmߟVE!*tQ4'`j ӳ H omPU+{TՔ9"U@W3${~QO#N1k0ye:4_1 VrhgyVUjDUHqwkԌPTgIA? j fZ 9L=Օ6 <,W={)lFyu_+1!%U0P5D+'=~$gI TgbSH;Nc_@+\4кRAE<^d6ʊ,dT66Z5.R^Gzi)\sRGU ;톆4=^HKjӆ\D'e՚ i;0q# Y[ oK[d׵<(De*)VZ.ƽ#\oV܆jsz hѾ}WPz>svm8WU'Qy<^ZpF}O^.*==Q%0=srF] `'eւIfhB(<+PƌT:j 餬 4W#˘}czq:!.}X0#.CD4GG7OG:գ8GN!K>!pA2k5u&lwo~h[6I/_6}|Rs}tyځ\EP Zu ٴ糒>\PnEuC5l!)Ts`eGsK"ɧ².2%];,د`5|jp;3֗ [c kn7o eAr6;/㑇q",6Vd|`u-6d7[~8wqVT LdO`Wjy0T1nbb~?PS`s6ny;6RbD==~Y4"9fj0b>礱FZ%%`0**"c9Z[Z$W,ˈW%h\qN6ޭ[x_}  ;01h&݊}w~[>| 6Nշaxe/ЪְU4} ]  x?ƌ6<&SHHj-,$v,G{jT-''>-T6t8 @?}-6nۆbi08:?F%plIxQSۆ+cuֳ0AwodL[{oR!,MNjeX_ֶN_#)XsshmT+MCZr,qa(*>Ԃ[nt=Xd  U5ѳU}vʊC ,(r ,Nn,nG#@.A<2̘Mic ֎+QĔ>fPovO0̃1꩐=t~U ٭R֭B&I?'KgLGMt;Yac's;+={A *qS#pZ!6Q}.YlCœ47'%()HE6t,bO'mE1iMՐ?+4mA#;Fdm$RO9c9RBN{wmDF8wy.ZcyOIrb"Y+nᳮ\Ua@I\l(cZB C+A4i,4C"$gMM*Z[1 2xn=‘I*mk@p͆_n4$p16xs 6L$j2DdHN^@kWe޿K)07D$aM 5Gt7gK6}*GF`Hߍ?ف ~A^h;!t#J4cedc*Pilb OF|z1 3} =T(#""mJjZR'-8V+'\\Eע<|R_љmccGq:. z 2oJjMt$U߾fv.ttxg":ᦢ:Ն$gzACnq}c$,c]5:i,tf'bckcn'H&"rrR؅l5v8K (tRlC^ЩIdvR1k#h"GV>ߨpmGeg94 ,w\֡MB-' 4V؉I96 mD5ػ}72i1״U=/@R4w1-Y!0Q'ϱ#s4Ì;>JWNŁ̟m (>Mjw/`df_DZ)2ȢnyZmT Khp8RlYM{F4 /6zgͰkZfy.uڨQҢ'_4uN+IY˜7X/H;Z @pr+;7z |BYްK\qjsD9!!a|sʸR7oFXm`s@mђL}TZR1 Y`{=}Ap+y0'/=~l "i)٭5ML678}d _XQt3E.PNY5Q +z2〱n؉_ZE;62K,'E#+5s00fp2AYfZ3#]ӦK;;e}w=Y=W$?[릏߆>YV%:d_i AOvէ62%2h,IVhNI o@ N ;~ oSx_xL;sMn"CcuhUG"=ZIEz 8XaN$zjw#*Xiw!X:Ud Mښk' Dr;30<$\{LZ6P-t"hxUʷ?hgNRw|$ֈe_ʑ93i ض|cT(^JK$t^_<ՍEtmy/SdgeE,'`:TYhP `9*-b%շp0 KX>t!Z)gmYg-Z!b$Մ$% 7=<nfK kpTPƼ(|soi_>Jt R*;#.,̌[rID1`A}D8k8|%oiT[d?~*bⵝO{vC[K6*HĘPL+J$_ ua_]ih[d1IctsSs"{ނW$9裏 ^+ BJHC?S|t桳o%PUUˁG(?7C0=|˝Dܑ .XP(-l#Q9YJ5\%]xv5YY)=%F&p vְDRQTRZTQiYe'@[HU1H(hš \i!O"-#wEhFE؛U g%ec:slLdZ$BL!b]-K^ q,d`df2*EJ"\IYR Aum shF ԒJEeuk6Ņg:<5V@ kTez*Ʈ~:1#d?#X:ۨbMsI A=*OERJzZ^2:AuQ-Yzp6튕q&%QxzT]׌ʺ#y$8|8i.3ڜO%ai.p%hhC`;cocl2DmE\GXƇc^x`*Nv懟Nѐ›j*/;qtAܭqdf- 2--fԦA.b>2B߉%򊩂#ZZ<:k2}3|b27I[C =*dT+f vsoE`@@N1%> Y,TB|^QVkvtO>y pr &rGh\Z.N5s-͑iN`dQI{yg_$n++`KPW ^ @"g2a**\XaA;d [#5 *hVر1"՚;~>BS2t)ÙivHy m`jBb\^yض{x/a}5 m$S:%w>=H3ie{/*؁x#1%.fNjoIU5 zT%6qޚ 2q0Zf4wMc].'Get=om;ΆP U E+kN ~RrR^|q5]}o"ܕmT {mm ,'Ȥ[\DZs ٍ,,B:w^{N*Y<5-ERV݌M-نJZh?~wc˗ʴCaTZ]_($6 wr9l^88!i30' a7aQ=)a<F{:QdɖC=^fL2k!ê,bҵ,(&w<CXfz!'X_I<13g/ i:i xR#Hf:$~(z T33]4ji|SIM$X %m ͧ)mJB yb7GgX؏9Ȫ:)]cnJ 9/-9s; rȨ8HS!,&y)nɓhlR ߌZ+* ֺ06C| GgZ*Q_SS {̧k˔hjڝ0W6mv$+Yԇ$jb]˖ćT*E߿˓ipM$D!a|((Tj>uQ~ g[du * +' VZ0ZY:0KC1 v-*WP_h8u`TSeHB=T0OOtAge:4dhheٵ駡Ѓ"@avNT%7@M?Sbΐmg{c0VX:X"=jȰnO6Zk &! fXH2ŌI.tu( =uhے<\źEN7d"*Z:! k2uTZ9HQl& EutqŶ71VmhhP[YkelǸa1҆/g[h;L7#7(dĺsUH;~@ʜmVw0O-&/XZŭM~}+k es({?kN[7^~9Ϗ'xwH==΄쀷o06} dM Md h $D nnŎoc\oBdt,>rz "z/j9}6l| TcR!ۅ^ 6`ᩍWϜD[TroD5B$̕oK,n>*`nO!NJX8 ٘5k Aׯ;ibg:MOdu+"*!XQ~+WH! !JNOV57,Pf^T_BécDnq3xg8i1e(EVh꒬܇r\x Yl{XhOǍH禛J1V#S6sϣ@~׏3OJRѦU|’ՀN pjJT gZJe#;CB"hlYg5iko!#))"l5Oއ=8=DgKu#M)!g6Ṇ ٖN\o&I$NBvq=1 0S8tu]]QX-a{a \KaAЯyBmT2֜.IBXc݁i >5z0uUAX1b?@I?j/xxfR ؗzYeTW]AmX#2dpAu{3iP#u^rT7ٹJƮ'0w4 V&jCn{r]$'&_92s [A7k hha6.Aq)FDng9S6DSu4Kl Hhhh~iC'f"v̛B BcH5aq'\yTuxgfJWc_kaߧ4)bEs=6}g#u#cB wk o<i̕Jb"h!Pۮs>AP/2$P pm)Ec|&X jɧ=ċ6pprrcȠE5^$ Җ>ڸ2Jwp3m-TݖBۓEDd.gbotC֎:X>xY6@m 0̵6Çzz,VLVo݂عy琓y"TaDfZ GxdoGPͼq8ݏaq4 Ȃߦ7NP&vصg/󗭗@K C?ǂ--*}{y`*;&F= IuE:&Fd!2p7qgaS>%+82w)3xC5BqS%?-O,d`kLpRs|Ư*cD9=sVɓ}:Z;X)V^**)V+gv憵S̠rO`_Y(,mm̍`f~A|%nWj5-:){ e0uvl߆7yiغZs%Uּ>XWu5ϫG_yUDvحlJ4>oŠ[F&;xWÃvUխ{enqd;m8y|86Ws3 n@L Ʊ*<^:@F豃11-mÛcǎ#٨̧7q`T+QV;.[8y"v% zO+(ZL5 A1:I/ACM%\ZEͫ ADBP((xv#Z6t(XXfj<_I|~5 EE?|ZO9hOUea>2RA%7%lѫBA=|Jft_GY׽f_,ז][ (aQǺur;9 պc8u-(on#> f$٘٬_ʻȠw`FX\}~~jέxEuPДs))!رz9]:ȂVfO 3 Zp!{ޡjV^eeHȤק_DFNaɍk0;(`T5Ld~DFc*-";hg7@3v+/< xu8mk_#Zb슚B4In*dwߩJ'mTY /':toE?J%ֿxE\rUX{z%PEWئ6/T6'^#gL!ջFi+V`/$t]_&j "Cu(V;T1+Ų`:(blg4l$D8W4&>Bc¡_Ft1b倪mebX8n?&>}@©Kyϵ2 **ՖQ Y $ >l歂9tLm5b]yit#G]:;:&켂-}\w_C)DLb3e[` 9JӞf̒BKIA} ӆk]It"ܲ"H2=XF9گ!ʛ'Her=36SRRi(-MI%)zCW"e-EejfZtML IsPϱ#g ♣ZVWgsew46({|˺|?b'pn6(j㹬ߊbQԄfsL%@oCGc;Ke-ŕ߮}Z˶s.Y"s KǑB*k<5 wo .'꭭$1$VL-u~ )gdDS_3TpUݻ >> 9mȋ< y~ F-XԘAaJn'ݎZGbK1]H+BcI׍VާRN^gOddV h[Vg[=j{dX6f1&I?qGen4P*@ 񴭮)PMJK=y4A_m{>O[]y#ѣN3 { Vf Ni*́1`rU|=o.5gc襬 *8g'v88w`nF\'KP뱋`m8*V̸֥PyCXPVPunG/< otT4dsP|aŌ M*cs* dMe7\EA زOg`ML|/C ⸉] W0Id_k61<}>x; nط5_ |'r",2N#t"2#S 3 ETX-\$$w[$t)tS H ̅ד숏ywfîAU[(uՍY&r#$|xzeX6=ϗ5u>3GqNNmw.]1AEk'ZHbAwJ |}"6=]pcZ|9:RVՀ#61U|;tpE:ssATu.T $$$O{GW,Y vMKZcs{Ŭ.+b ͧBq\hsFO<(qPMAc:9vp&ȕbl2rj ԡٹ'Ed Wu<]63{uX4]f}\rUlq;S"]y$ȳ V8quO#D-RӳQW\iTL-AIa)J:Z/k7 5)~B sGf{6*rq*[J%i2#Zq$1 nyS-Z** Y`'3MJdz>rka'9u0.F'#p>3ꣴ.v o6nWA2#ғQ!#5hRe'TiѫehT9ݜ U~M[!JoK5W6HYUMbu=;ͼZ}: )63֚͝}*ȣb?_Kqݽ{faI4Nz0EmXĸuH6WK]:n#_ Nz@Lps3YYhccn$)II>)`ͷvy*UXxr@?m%zh&2i}y T*` ڲ>x uT8:ïcgdn^MXŶpu8*^BY(XjZ=TǏB\?"dD$䙞7EnA Z  ɦ*Ǐ!ǡg/xhA[%@e%oT@v hzRޖ~>^yŻ()*ez,=mG؀~˗-Syݲ{ZJ,[1|/#@4ݽs+sVPQYŽL ]miTfefC6He̥h10N7AP"`J1Um-Msi6uڬs6y&k$cv1I ?}ȜĽ~7w[0}jj+$+VE))SD/T~L 袍{7W_Ef{#zVR%#T'bƲiC[eόc3{I^!9TƇ@-̲6~$pu8zhGo Rְ0lx7n6i`]<|3 zNǩhɜoÊuw);(o׬ffG ʑZ.bÏ1"ZE]ľגY!M$Cv++-A@V>$bZhjNW$ZAҠbs&ퟹ"'9ng.7TK$ΚP!z31(AЇݩ̿J߿(,ژ^]cpL0f?QÙӧe?}ӧLv iY<0SW֖0u* b{8=ԳmO Dт`UlmNw~)3{U~'LZm>gmk. K!aA1f j"$wDX/_1P%}1sq?2WDjkx[yB&c5lX%KY󳑦U.`yU`zZR\ԌDh3'6ndHN+ QYFP9y$tkM*^q5q]z+)J]P4s$q c$q:.*OymA+UB3iA3Qh㯯o]]s2֑XX땓{*Ijk~2Kj0s=ؗ|v~֣GwbUU蕟At/I1fp6 2U:,z8N<BܞϘKGD-XIUTRfLՍ}$%k!ak'nNtL /CZcт4@Wg("ZPs[/imePAwEUax~e̞(m w*u{or+0k:PGn_mjMR[] ':Hx+2pچ.P)ʆ䥰?lRVaN9?}]muK^3ĮıA(G:&@+q]*.`<2%Ȏ)EoLע_| :bBmivki#vBY4YKabCPkGwu# %GWJ[J?CM4YEu(*߲?GK/>C.d%P ڲg} gRSe8/j` غiJݞVC]Bdm♧\?%LU k{eܩp'ƢWb6q9x>?^{2Bm KKKſʜpf,_v%q{\׮ gIEe;yVֶ8th;<#78$!)v&%)(,(:; R'^nj83XWѲ3f.oa#I0g1cxPl!7I 3LC_hmv▛Ӛr.sȴ ܘ9,*[RĬN7>I9IokvҲCL:LOFeu 4;щ’ lfT̏]͓<{J71x|ֵeG?UTVw9D[ky Ħ W쇮܂I l3}k.}Nqf hU9 V0^VLcRGض}ѕ'd]qΏT<T GP]]|e=/BH#B*Xpյkok^@AEQ@!  {;qb ~yy͛ͼy3߽sTS~('_ÊޝєsԌiy q÷^ VGc; ;b+eL"Zgu ͔_MF?g/P70 ]59UM`ey#eY*^r^ ԆjbP9%XTjd߼e#w`R tXdRa)殬;IRX2ͱ\ؚF!Qޥ7mpAzN}G+S~yqm9NXNY~Q{n^)aQ>܈7};w lSV:JZ |H'w/l0''Gz7;@ Ds(U:#d ƱHKr&~*V,wXH7_#hNwz"݊Ⱦvm&k2[F6{#?{DzްsoӚ*K3Z\ &ipwVT_odߧ3), %yϤ bؔq\;& B4cǢ$ ;dJSAVddWB(nͻg~2e^3ƎND2wpud/Pf}򩇐[Q\,mX 7p7`dfS: 4 {DX|M?ofq1BxTۍ^C &wq{ĸI겗7-[y^Tp-nա<W7_aW9d4l=-?jLp{9ʤ2NV'[|0߾KXؗRg իS)VTdw~t腏0R@Q_Rf/S1, Xj/V?UɞsiqUKN6Kɹ#$MWd[vTbIU7S{Kc"8n{'-^x9Sw. pםw*on-6> v0pFLƨ1Kű SʷP!kX9͛.|; ojZia^\<+dkZqs[nFGT2 XcT62JV hgNtH;v,Cimeh@Fnvn {GcL #󵔠eنl\Xib N;GMƒu*֣"" uiz=Td,1 rx|y舤X1}D:/`.bPTZ^@v;.?S0v(]u9! J^{8ɛ|D!p&d@7l^f(ϯi)#AthСd!HRPQ2Jc%Ezv5S]KZ0zHe]ՉseA8P8S2ݬ` 6C) +6{ql˕h8/%tXpʒe_\򀽋i_dm.A.'ңˍޣ7^x- 8x(i c?iLV Dz˱;uDDe?̬Xy=YvX{2%hu0nK׹2Mq!ҞsN?Om &uu!6gHf:,j+}LYsf) ]k|;:ceP _q^o6Sz8kӿ0`-$n}}- l۶5d zPSJ^B@4l%qm+z@Vg21Vm7LdՄ>sMU%1RVhl,pՒ{XI X"VBlLgKN=MLqR'ЎJeB>o:9C"H_ [$7?mل@/AՋ!jo MhKh~k ݱ~=ْ1a^=&R8y3\ f݉'?{V0;JiLXxѣsnW442_x]rp(55CR>{8.O}5oooL4FG,q84^.kN)(eu~5UQ)t`"7~@r##0l5bҢPzlSLaam+w$_?.J9 FeRUm<MEL\x-9f? \1c#P[/z"'f硆?&Yf]dzq>ﱸ!1z?ke>sNVƏ}_|[8*+; *ޭo2L)7L 0XO299|vJYqB0Q{o`l׎/$gU ?\7uҀ ̀3@3Xֽ 2kZpQEUyt@]d~.<{TvÖ4Ljk@A2~#+,vͯ-GrquK0BG*f m0ߝ|yU&~8Rc#khl[n^eX͌N;__W&v4';&{z2qvw C˯0>w" c׫`e,cv:[ØV:;ť8Z]"ZdϯǼ3ǣg$SA8fzd~T@ WtbπeS> }eoet#,{K%V~91k,)MJ/kQHSz߮+Dz?oVesB=ހ:]e$6}Y+ j,~+"`mmGZwo*;AdmGY^~Ǝ VDJ;1ERLRkE̍]AgMd2:VKO_A7ޏޫ8˳k'|:LP[MܝG5]N2 ;q<^ʦLuh"FWSC):aHWG6 I "9y=#h2;v$a׳TqGj"6lY Nyeap sq|q../^*Q-· 8vG叏Xij[[tZC=VyQdn2Tl?a)M hsqS Λv`vk ԏZz[k6! RMQ -{gu=yeMoSE9 SqE+ë\G-Yzzo^MQvn1UT+g=vT.Ep!C]9jQ^yqؚaɬ|; "y0՜ CÆc(ѕj)AbIRBm `??;*ō)lD'wjw /e#!. )Xh*t*X,ʺNJyD,@֮E d]ZG cN@/v7ј.y1wP6%2~dY٠T)ӻSt!>8ZV0e=QQtS$lI!Df5EnhL9%}VD]XeGMXۅʦ:9h|x OHP CZ%v NGɌ za儧OT4S'5 3(caSS俲Sڝs%~UރD+IN͚]ܻ}_Wd '(l(8wn"2VY}o\:s(j یSfPc($‡JOxŋDRmgM;}8ȲG:i.=i(ʬ!5 I*[!7ZG3@5onYt%Ojڙmj)`ei k5VcM&CH>ŻitDjȦ9`)dx&8?&1"; <ưR}͵ 1qq5W#c̓{y.v5ֳc>\js# l>&$ЎM}Dw>|- sK`e]`HZG2'XePW[QM )7vY(m,9\ĥYRy,&yt'L&ҤZR-ͧPVv&Jz'Y!+[^%#q(eJpO_}ѓ59U㽡*# LFSZ@&~9gp"s3!VdaЗPFNLV6s ڸy)ECckңw`ߚO1dlTϪ\a=ӦOD}HjG1R+(Ubliqij"Ys@D1 [bmhGfZ*Ҫº3)1{5~#p`魪o;~2bﺝ?|iLt[8mABA-m )0 wOʓ5ä$ W,xzfϚ#ʡ%Fӻ{N/`u>T2a a3wnBe[10wu6ـ)UT@0¢rlz_%DEUBzhMX'X>ptv.aoѦqFd!Ehzhۋ:eniSrq7ʣUT|iPE+#' mia_UO>튱˴ဧ#V򱰨Jզf`W7] ǩ(>D4!knؙ飚d&*qT뵢JZSǴFfU袽yJ; LQ*0CghݴL"C]+ǽ[/|nFy Rhiv0a1}k}Mƌ]FiLhcZ+g﵈)Mg,IӲeX̢Ip+$X(̹:yRwƌq s'QG#^T0W~6,B46en)^x#;5Lv&Tjq%uT3.Y:dF?RELwٚQ!).|iω*rcvG IRȩh1}of|XR] Y%Uu䘫8s2=J$m#+7g.? &m ظv-ꫲK"fUԒӪk+&$s_+h2fHG` 0U5 \_dա_5/ 8pvt$ʥ1LvHV&FL+CzA^y-o?C6'>w|bggDS64bϊq&m Aa)}"=8H7E;!:x#ԱRHYEIjq5qZ -pM3(iVVR40ApE6p 0DA']IGcZɬƎ 9`d=HiǏ+Sի^+k(*y<1@ڬ 9016$] V禥 "6IG AJGSY~.pX!}R#)1|Y̜5Qe̮%:7+^ρi9>-Zt-=~E\HuXX%pQ%g$kMH)="oI %XsԈ)z>_HXSܹsZ)%sr\^La _]]BV* CB9hu`R+'TJpk̀tڪ*i~w~oE_yt?)}ym>`*zlLPQ:$i ^dm;5j!J0V+ퟺ/:q<~; 'swdAڕʹ!燀QfuȜLn=Kk쿞ҥ5+3S|O FM9V[N/}\c::tǏ57ٻ+Z[|wU{Nu VIPT.6*%3`bjJCn!Q&0̈́ 3VNd>`}g^.qz:k^i~ 3&3/XwʸĠt1ј>} cA635Ě/Bxn\X.*l(""dô\ ؅< GLFZi0Fpؾ? #"(t2{ڊ7z/;BC5?[؉u},j\Y+.y#;0t^MPAVyVVJdUMS]bͺ/xV 雜X/~3[ܾ88 ;UÕ>S# i5"kvod绛ExG#":W磏:vګ.̸~g;5KygI0x&ڨs8V:0ja~ մK\ڌ JET&)Blƌ$U܎6ܣj#?\ ǢEGC%Z16gL_:lPc-8R 6_!:mY&}\=#di꯺jny"b {x?:D̳:1݊ys:~ÐLō<* a>;+5A#'(+I!ǷcKȾvWNk324$<(Ew0&51 Mr2*GCR>xj=hCu#=C(F'3+z"=0=XfӐ'bw4tK62e^Mfʼn0r'q<M5HI u0ɀ)jAsdHH4a.ŠGoo<:ixNUv/Ŋwi jYWcBpY .K\97^ǏK)-wUԤw&< ”GXL>DV`M C'W99؟'_~xC^c3e>}Vly1Ge:ع'n~ p/Hn]QÂMoӡ|ƑÇ@ܜl{{뙻ӆ1}"xPv]0o5=Ъ1y|08P^N P MV/$':+&M?/j4''c`7x·'8 aŕ#|1@507}[sg^w HuKkz(iI-Lo:j+JD̎m[ D0\"/r&֔jʣzJR|M5"/#m^HN3g·g &VLݎA?Sc3zz{YY)Y;fU_umߴ7Lqܾurs^c (Ji<XI'Ajw]OZ%*j }z|,7Z:F(4U:K]^~R-wE~d#*Z`zVi? Ѓޡ# [}b=e,9:ӻ$RQSO{JM;)-˪\r\v~# V9>3Xof~sCsc=2hUhv w>"X1>*Jf?r>ݬ\RDaGFah4uO cw"P-cGn #`Pt\MͱM,>"g-D޽ 382m6}p0@\Y@ʉF#Q6d+x}V=rNJRNVd2zDy_X1_'J ;EZT?s߰ppaK5# &0: ?,Vw ψ8> 'Æ_W2Zz%VK?ߊrP0 +qsa0_}=FI!6|'Ūϰ`e'L;7L o/y!^0]L>Lb&b4Xe>)L2ӧLW};o3I, C,̬#V,W 8 356lD谐^/YLIYtwݸ\uRʼn4m?f dF9C :[ׅ ѕ 7uKduwS@ZB*U}iI,2݌Cxn z*UƦ2>̵4EI[\_v4G=}U>{㭷0^~.w̧mu4&X[@IDAT˽|j,W.$v>PY{W LcRu3}}]./ӥPWUtbwZT`,s7GYa~NN3,$Vp6>$Eʢd2;N.,Ԫ#9vgڴ +3^  c MTk5UvLpp?v{:ȫhނ,|+|n[ΧC`OB~{ql 3stÍE]6-{TLYBRGS6^dQz06*d`_0 egfDu.)RWb"l LM?P9^ƥ cYERF$4SfἻ5G@7*z%rWoG$|@ᣟ~3.1[\^ Vd;ߟNVU)1]ta:(\R%0s&tn<62Μ<Ʃd$ E%/.{D%G(4<.}sb䨑ȤhuVV MC9X\X1#{s0c, PYg2x10@VQ E^mE| ray?F'b`eh#y n/QhCӌ9A.0vJ b$}&\tlQ 4F;9?Ǫ af7<\SP׺JtP>/eY\v gRmeaP Ӧhڷ֭50 _>{G"@&Ge-(8~GC0uXDs_v$!GUG2ifwiL kȊ򻐒MSu5=A '[lI=֯eR7lpeZ6}]>rZ̗~/ol^Kx?xUj(o,O| ֎Hb?pf. >Lԏ7AMbQvX},X.`ߡXLp}#%0iEjdəR.^fw؀)ǽnb֬0%KCt>__.rgUSf@E,20c"\ |pJ@> G 8ǙIˊ2ƿbg _@kLgaAl \ݲ vv62uY,I zȨS"C::|"}+}g$x&Y,?}Hk pjsJħwg>wtP|a;PҨ;@( $K .$`,}V$#+jbwv@Og2k^@gAOeӋ5.c# Rj@)q] J2qhG`8࿭Y,$܌RL7eם5*xZ.w j+ub2b5w(@ [-AޗAMfoB%1:y=u),'^HyI;qu ;9~`ݟ^7f#>UF8]kpVFz*; H9+pˏq>h'O^)im12[p7‹2(Y3b~J;WwHxiDzIb  {#~:Ƴ8J%'J nZ6QsN% `|ŒDݺpd=]qϒ=odG_Ճ :ѽPrm8|z-K`']H)tOo_ `z19{6M֓No9VsJ `}dܶ0A)ce}^;4XcȱJ{Q8Xa@judzMkȘѦ8ExL>#^/Yv^*:BUK3+&O,3'*AeʛbȪ%&IscQd~T؟T88wӅס_@T_S-\i2td2̸BBq&md't>/!m=ibRURgoL]/K)^Yhl~)ci^i'SF QD2Yu6`W(!UQe_XH1kVElsOwdOOxd<\Iͼ!X4^|R*RRafM&zh2!#I>E9YKKcM7c愡?{nܲ&BaX;_vb1L{e6rԷ#^5Y-[SZQB[Wˍka5AC)Rf7PF.sR70AY9l@RJu8tl@CF`+9t^xI>p}X!h@bNQI–1e-iC0,w*1oZ|>Ϛ2䓢ߖժcahlF`crpB 6ӌ|]Vt/M.$ !|Q"m *:ˆ)jDMwpruPoh\{[bgJA]Ɛ㚾M~W^W:5zbZ|nFTnn}YLe[+#|y;jz& )f͍ǶY'G>5I->6L:?$92l`9YdzڢEm` Êim7~t :udȩxEٓ Hl)=m;J8L[+Oj˲gs:mYW2R΋LP./NP2)ӃYa#Ue( 8][ݳm+,4znjGBWs)FE5~)S %OOJGS]Ro;/M<00\ 3f`M<.vxZhLɍ GGK/_}hOϝCxŧ .b]ʧ1cg\UV~NnsެYefbة~ e.A JTbI#r4jՖI}2 Y)&kouۆ/s)2)&}-,?Xj 'to܎d;`_R<ҳqd=8 ^o@AZye= Ձ_sMW&4`MHSݏ'y p$#PL<ة0f՘Oߡ&BN<25PΆM]25L# L/]DЕpt }75I5ee ؋%&1&sj"-liiٳ°#1ΚEO@ֶ6tvuA iKXx0Dӣ{5"zZOLG5?Ŵd3e(\ut۞X^sux)wu2{qn)No[A| ^~fϜE P #묇Y[/!*,6o1[!]dli F]%j2ZX4lFll;nk ڔܦ-YTm~ܵwA-1j\}񽖞);-?rLHEkWacƊZ^wãϼDV#FHJM7{3Z)Ek,:gX|2免1cR })Akny\w<h?W~'Z`,]:|,k hjljOq:0|(3(LdLQ9TCl/mSD1#Ǐ,)ǘƆ;:Ӆ 6U'|A6:Zp<5o 5$@Зv|zմL ˘ǝ![Aտғ>M 7,Tt:[jEc(lSt6"Fִ"+}gho"J~Eߥ6jNMZZt{ Zҋ7ehLҀ%X6)z\E[ΚѬg8̶N4vy u~߈ te~벶Ww}oCqa>f] Fs%0tBiQcZf*ȴMEM@+9H>Ivk mK h埍 Ϗ X0k^fQt(oj_MW)Af0I}dElVYgk9Ɨ8kx)~]Kv˹lR JgߖAF3]υxogcͫPH VB袩YWb539B zqsl Xۡk1rlҽ*AUcx 2P(A|븐:v A0:Hy;L.!"::ŸUz'jFC[&W󘤳(e?f (ۛ́Jq5ѸឧX?b %c1 z;Rf3Oce^{)L6 lW mAQۓKެ?A؛AIP@ԆwK75?|VY Kť%M܉75f?9`-N?V=9H-Z5̌X5jr6oڂo0 kUyS}+y 42`;Zpbژz_T&$c/pp2U@LՕxOe9s桔 6?]@ϕU-7#eBf-_K-NXeZ9}GBfF6n^5:[94\L@~( *{n,/ZXF3b%gЈ h$MAֳ=r^~7}%ivrΦiiOgmdJBɉ+U463Qgj@OH63z>&@ {)2 ܬ! EYA*8NӒDYRu3D /e>: Pr9Lާ00''p?UʏoxNjN jibZ7,=@sn1jla_q؁=3 K/UNg :`GܞQCĘ ʹon۳^Ѥe&JM qS.Gf&)D0J(Y扇)(ULxgewXԳo7|$%sYD.YQk  `;"JYR8U|w++PUll *@`ݦ>+qч1oij*xң_S@aHx2u|?z]dq2)50PNwБk?[7ԬuCcS5}^oVQ+5Q):XBU1$o!lT˔XFNWWS_QmxBACޫMUdW쓨S#Eg,1_7JXby?f@K<8ViqAdX+_}+fgޘ#u?* ;_Fpd}7^OX(㭷G 9f,WӉk ?/l%Zw۶hv$h5U@1ЮMk-Dw!ِD'XKWv$P>Ð~8t)++2z>ԛ@Újɉ˙y@G*s?&$k̬,K*QmjA1:0&Hʼnv*BgNxZ@l*2PE <Xpt'BX,UWWa;yUcځlM[q ڔyK+HCh+SV-r>T#7%;SȐy4Sjl߲R1,cFK#m[ۀb{*Eyᛕ,c]k_DsUǿ`^SNȡ]D=9Lm޼,& BGxzDmv_d xņv-Q S .=MېVPIhCv1c֝b02YL>LhWsJ(ⱳy^[U kc eMkhCc#8ⰀSr d1I3Vӎ9 pO]tԣ\*S1p\g' $"L {kj@ƑCE]s;~ڴ qc٥G{*M &>O>d`ό.~fTic/1Fn`EHn@řh#j>4wiS *u*r 7Jy{]v 9('*xjiC_\`|:O] WYE9P;ݶw[3ʷ'eh*LY ŬGc}\ JVUX @O{ J V=(?*t2޹|-9CZ`  æR^f 9,{a ZxQ˲g=ap@$V c{0lן/@ȲF>[nk `g}g?8 #,&WQ/sYE"Fd95*ըGŪ.]{0iJj'L;~8DUIa1 ~6F;Ya9[+1ωߛ n4R.מfCy]ɠײA502& t\uM [T=t6d„sY V?:Y0TV=7hi^|7ӮSlc'<&b_l+QuUx_jJ+97f?mF:c|j¦2?ڮ,CgZ_tU}n]*L Ovk'V6N'N|x =~i#5N=F=*!8xZѭm`@BZ,`!SCIj]ʥ=5¨Z8">K+gmq{+jٔscD?q 4nlzc.X EEߺ3d? -Oۨ^aê$2xč/ GEe9q rWwڵ[],Xs|fgƐ5beDޟWFlD]l[#ZoDl#B`sodfE7nɞ3#ѳ "~"~ -nȥ4]9WKYeQp)tH{w(}= x=ܞ Ț|01P:_/eװ辞)qXQu>~F5@`X9* RyH0:$dM=V}_ohXlbD['dun+͛3JSr&`<@E)KKK# b3ԮUVi_>쓐~7w)9&-5Ǘ"9+6=x-,9]ٗo~ǹ2 Jl 9X=q0XOս>+>PQɒy;2;u#_±obxh>tZo Eէ9N5<6$>Tzdž*UM^KC !2XN*Se=省.Kϼ K(l0|ri<FXeT7aJʱZ KtA_ Z-eUxX_vh) %a>0@bc =[oClqtVc5]ux䃇\(36L](i O' -S4Av ܇Qv,ȡYaIuR1g|2, W]$ [Å =5խ'WovqC,GkE-(bu.-Ÿۨ0*X]Sy ջnYʹ#cYGÊz~Wۣ_&-O w6D}jI}c`?TIշ"x:Z \l5UaQs8*g):2>k:BiL@ bR2Je7X9{2/ibs;~Z)f/Zl~̯,*G>J2B[;;1 ;^TK{93#7ǰ/8kMeo v2W˜G#6v/"|@7rpmt]`j?|# A8agʕuض/\eRVy p_Ap}: B9j D`C5`i#B=xn@\ Z+57Gc^e. DfƸ9ٻ?4zUZAZhH(+D~y}뉑<(JVTU"]hzvh%H/vQ"]NŘѨo19ͫ9}! ,cW0[bҥMÈ Όnrk<.&wnss]i-Y+1D/%釐H†?{E~ RIH Лt lػkYW]EEJSI/!@ Bzキy'LLBwg}ߔ;%3s}{ΡKF_ܽx!}` SgɽoД9-\G'T"{j۹fS u57YIqQfaH~6ꒇB`xdUK˙W۫uC_y`bZT3v}~w#=-]jyvߌ) }w 2Spx8 i=K>LVʒV0d8J6,d9?f&ö .RLL0o<6 }6`sYV̲dkJpL*M3дٶQ4gP3thSGάzi piqגh|LN3 tpXe_67̹ 9Ap_7Gf"L"\(Caȡ|ָ00d~gF%QQV䟨CdRB(k\rLD^f{56M96 .]?K:ܴmE@ҥicp  V%d ;y͵(Uh&׵u3sjt5d+CP^yE{@"A>>ؾBT03iSC0速*D c ÕO> L,ҡßUvl, 3N SzJB 3s ~ A=^|.Ƹ0 o]tJA3>WN#aPUv@a_PȾk빧$$."(/w~QfmZj-x#MSdnnf=":: aw۳+ŝCP؄LLdp0#/.x`n4;a_CbT -J߰! e148:FШж㻜/IgqP$B-D3(8bfWb:0R@֫Q*3O@jTԑz zY` I8OG9JT>wʄ6U~CLʨ@8X4Qַ=X?Y]ިf8vAE2Qm'VHL?Y_}}}#=DŽr5E11cBյ(Q mlz]E@`&m o޶ 0K 0XUtrzZbQŠun+W hiCgoCL|\r8y`3s~pQSП9Zh?OX= o'nI }BH%s:)X%~!j;˜l/U\:yNl%)r$*e"UGyp"઀*.f9MMmW_4Rɧ0X)xU64T5sO_!q2-V ak~_1cQPDJUeV8ux2At&󱐲*oOˣܲfDUe5ʳqKp 0> uXu"}F@?7#8{z2YQ1L;16$.7zcvHiZY ''gEȈHȃ^Ϝ]-(4!@̸):4׳ǣƼ[Yf~ P(:;+?g-0!U-+tN9Z]$8u$41Ä$G?ym c`?jN\؍!Ah(-]gf@ WG3IdZM) p űx d76BBrrqfRT?F6rȄ542fh5p;^" 7;,S'`hx-hyPs īumKL#XjFH+A*V6"QQIPCV}a A}3J8BAjla{t:5cܼv.u9t*H9`)Ӧhi(Üde[^  E X "91E,gYY~Qu6I?]J6SO<>nw3IVzA]h| QW_ #&@ɤs0g03F`rc= >Ad9a)zU8סN$ Ich['^'?O% B]8 xr0ʛ 4PX9E̺*jCيsk,_J5v1`X 6O?bNLW+Hc6k%v`}[q+D!!>Np}?kV;;4#߱G1؊0|e$fUf29 iɃ0 zu8$ssc *$!TҌ8=YӦSg!3qTPTM#H]+iSœ 6_EW^w?3q564d($Y"KP:ݽVo2_n:$G*}f&1ͿlP\:Q0[YdN?bv@. n!X:Rf*i2$b`87Ԣd0xy*z%bWR~[C3&lЗZXh7vsI߷gN1]PK4zY'i#u0,PSIUuZ2^ 'MMMe|>UVI]n Q?!H.kSXFs~oK 4|7ԖeJj3:/pxJep>}^=6wRGx!dh/nϪZ4Ut؏qe{gUV2܏D Ai}z_`m#̎JƤI7ȬcdV0&dбxQB1HBiqkϩEלI(N8f0O:^Op{)ad=sy#o]a ٷnfu/#n7x㯕 UБ΂# Qr#Qx\ `M|VU0P=J:e%7yG*6, ]Gsw|F.\gΜfv u:0E ~&ȚIVZȅVח磶 #k?V'V" GzA+,KM8rV/9.K%#^m_<\0I.3gNǬ\Qi^?|-7a2Q FZL,I`8v(3x}TP9Cv%’;{sL9j,uxD@,bRU^T PUD:WbaSZ=X"jaxJ+H:}&٣!|&ۖ-O߁AZ3$c$*jر}7"s(O i ֛y~4vE :- 3yp?liKGIޏ$jBOZC vRJۿ W}QYY =7F"#6Gq*>E(T û(5 Dg6Mkj3DS$dihxR(+%^L8n3Exf/O(e~ڜE8d Alۼ 5BU -pq>&amU_d\)TJ s`&y,X!ʋr } &J*̙Ol3%jgAnbPOlgXdj)6^NTQkPBCbqeR g\ן2ںzp `>Cw٪]wV8d~SW9#Ow5cvڵk̘!C{ qoY '_2E64cϫ93[?'NL\*0gysTٜЈ]B;qSɊt:[tV$J=*@IDATف%H:{F=GN,CFǜٖI<4F}>7j EW՛c)Qp`RJ  ^F 4Sco<nj˕X|F#{Y2H']Hu[Z%&y<:g_erMDZ87_} 3o @Oxvmc^sIәb *,-gdRX#O>@M'o][xE"{b??JTy:L W nZ6m SYɪr2!(2TH?LP{r֋Km8X:ߚ|5u/X!۳'ْLDI{۶w,CkǨn_Q1cIU%ZVa YErH@֮KWmD</>y0y:U*ZZyibdxls>,\:r1~b + _ŒQc7h9MA{[nmc dOH:vS1<>C&ݩ'r?X؈LQz8;eIcDq|Z74w9cdgyy9a-uf&-Y0m݉)odb0~; iGU> Ϣvs`9`$\d7J97=ٶڷR'O^C)}:Rb~/wL `"WX?$]q9|9DY)|oՓJƵ?Q0_ZL&͢ԣȘ~\vT~9NRXt"-m7ۋWƩVפ#{ ~ێ)d\T"# 3bv춅+LV+; c%?_sKm[&u섖KCǢ1oR{/vUϥ=W_VNҦ026O1Өwu)-q)SbUP]]W]j| q' m}?ŞG`9l1/nY7;5c?A Qq"0srENRWB/ˆ# +PL76l&c0Sz2l c^9KYKDž21Lx&FN@/+oc PZKM${Q20+mÆ|^X̜1|P_€pD].}0mDe;Kt±dd'2Ky}̈́H_G|=a"%0aMV~%Ra ɴ}[?YKll?n<_^ #SHd^uw7Ķay >"# ʩdhğzc:>fJB>nhi{+1"z?{^ՠK[+΂B7LJkoltHH'ZJ L6m@/_ $5-QQZ[+1cHmAaur)/csPP[{AԴ3>*:5\O9$Cޫ}2;Zb퓀\Z_d EaA>N`q!$(+rf„Rh6>,@v34`n)ţ?!SQ~qѧяrkn_4E~c7e-`GNn(ypdhضĊLeRu KuHtV9. I{ց2>_)ыU-o5! ?Jve) f p`J}1At/J  Zfv`ڴ”r)}Xe ZDa fy/dE3X[yܹa%x_ Fn^lfCԀ}mU6ڡo[/Fފ,Xðy&diŇ߳A샠siZzxwaLO2NSrZOV鄳b,0>Crz.)Qq K e9ܾYp HHNJD }pDٻg7E[p 6c&8nZ.`mNs#6h=LOfL 0ZI:i&Ϣbǎ ZvmS 'op¼q|/W_CzJ*|: ̮㭋,U*jM5_z6Cf` z?}1.q/^8ZG3Q͂QEʟ0W?f7&qGO!8'O¾߁V!%TF0!#wy;ӋًM,Rڦ;#HL9CVq$LJ3tUyZN]0so 2<`5I`C{QY.lxJK/Sm}==g`dpVѥΒD5SR"k+W`]`fE6T0 _߮wܟ>}qj/=HAy q=W)їuW◍k`R? έgF\f8 7=qS *w᥊ SkeĐI{7 V!LRr9ގuX_dHMXe3EYSA֘X1i5E Je$A10sa[s̹Wpo?a;,2ᴵ0cBd\7f)d:'Oic:Y UkOXVh+cGe%{Njd?GV0Rk`l004?\TZGO?d\*LqwYYٙ3Sa\ cr & V3~OLh >dG&q%&C9g䔉E7?^z6e f^cRRT-@Tb4Nme~ﭓ,<ش]^[IYJngMD%$zIUJe;Emwzޒ,/ :TE%{+P;JW skT ʺ?ɧݺq vLchH1A`DI3r])@Ӗ덴b]n>)wN&nV m&'U>AQq$2z8YI ypv@2AC-&hj=&5.CXu#lIh!0[5 @7L M$nM c 3b8*90SeT [!p8uMJԉGqÍ PavkUMwF𜅌AhQ+՚pj&f@us,\o=a{1u)hK 8H%(ޣNCbJumF8q +oT +'k\tH0!-y2 !TZ~`9pɓa-;]X\@.@&>; ?OqJxΉv/nK`53G $ں [MFSJ[ɧaM1g0ӂ4cdbI~;CQ0kZ&{E.}31j8yJz>]#" =/8>؞cp6#a 5%$>%H1Y)glE|^A<8ާ-Ʉ7(<LG=gwG8+J[p<$Jz99܄{HF$g$A_i4ϱP]h|"P|hb}*g3 ^EV/ڱL`(crVNz}I ,+c'&jMOlU۔G=94ZBbSc]fM7SV?IߔBZ"ߌ1GD[Rv䦜T3hJt3Jb~}`N:{}@L8 ~݋S}{Űgʉ1o`FI-tkرҿĠGW(<< ή{rRGo cғ\j]Kf$ZA TDG7bO>KXsӭ *u|?o[옑ˀ6~.O`ࠑJQ^|4JIMI_OT(Yx1@V/=R 6X-"?, 2kZEkS"[O: 12Kqm Oa꒡I'69ppcPO NZ!&> aa(qU4DIu;"M ,A/3Av skՄmove嬬NY%QqfܸIl9@gS34^zlDu1Y }tGP ri>́]9JeEe)ܞqIIlEeޭߚ+pYR?KrU Tv/&%żO?M~b;ȈՅ3Ϳ cv1s0VNf1N- #ى;> }oSPY J䦤!(lX^oYLL d*r5;b+O` 7v8UXm Ab"vHl0ɏaß&;eߝJ̒º[=r/$zuPVԤk>,6}J&NLtfN3Eӝϔآ;3{ vU5̘аoQK{^}}B:pg m2͘ zQIea谑dg ţ}: (%{L6dgV_Rvvv_m $ ym_KĆ{qM7a^:=r,>yoLZ1'k\bmj+0VLru3(WFPeYOظ]5u҅+";`HC#;؉}ꆝV/ ֋ê/^r Eje݃W8+ݼn"A-$T1XuOk/7)vIB^5X\<2Ӛc G`yRБaRM EJZ*)Y6X]kOj+X#־I2:}&&.m c+﷠̩ǜW&=;f'ze @c1>y4RXdڭ#'3<` ̙1߯Fa:;9p?**7xo4iV2wx*r~8 cY/ؠ/.cPQpUI^#h`MqJ-HBf$cq$̜"TvSǍBDIiʳd˄3pcBGV[IR/mi!n˾$_T&(o2_Cs?~[nDyuѴ܍/j}L!\L6i]jvf9`ȔVuZxhT }PG_(cFi?3?/C';#dtJ`\B]:xddee(Qؾs75 33 K;;z僭 /S zZ9^`ÐNtJlmHjz*Q^,nЙwX,ԅ+GNe~|v^wd~ }7_}= s´ֺb 'ɪ2]35*ݹHs4߿|pFF& (紊r:x(POټ(ɤM}7d*ҁoQ.nWϋdf@xݪyNkVOxJ檲ZHvΰf]Eb٣`lܲ~uL7R/{6C̖怊^GmNY GkÞYLz.Qը,b`6jIQr dw4I7_ ^}EȀYU0 3Lc`„TDފcɥ¶;ۊD* +t\@6U2B.KI ޏL`@zsAAAe6$,4zA,njpC{+n.FYr,9/Kaar=*R&]]} zXՓl\Mڙig *+z`:{Vu?3G}2[C;:r<(Y7׃ 404( 2(bFV. `AǮ iIĊ٘ z{PKaOfϫ_}Ә>k.z7+p]uzOjr>/yBM/GБW1ň_2udef(tm\.*wgLI>%'!= Ȼ5|1w, RDw\~d.2KsڶWLʈg`?֡ǯ˿A6:h4w۵V|@cdwXP7;vG_>3~MV.y>4QKyPhe;ݹ:k kR?~ώcO/>GH6H1fH.k,S4KñX|7q×aAnW/m|@b#>X* +)DXv=ܛw"e?G_LWل4.+f̟J@~ xJ9J+ Jv|ɲReJ5SLֹX `?YKG>F2 ԪW|J }W\T/TVR G(?s@t[Ӷa.n68/I#'LLB&=uc_`Ɣ:YFi-/0c9iu5a(OA6u[[oXekR q%K2[ASSsM~X S3ӏ wO8lzE 0C82wf| V3 +[9.f\`"qH@Gx0Ffq&@(TiC؈ j7el *mM籁\ _=~کX T%F)۪jeZkwV BF+k{:_vJ[9e =!`MUu uj,%zT无O?fz7z3 IAψ6~קּ>ǝGǣNHR*; ;NA1c8_"y"!@f􂿛3 f%XVYY owk09ɦé@ŧh3+biXW4jD"hA&n%jElgɃ V M`zܰ 蚻a] `o+ 29vs, ~,dWS4lAhHCѻ{MśV≿dFphlxnڍ8N>L\0DI98Ir0;@lٯ-zz-GDQ^bHd R TC%öY˶tl& ZPڕ#LYS/XӮTd/KyM261Gc%V-ܙ~ v3j ~L{ 3-Mk&41gMfL֮h3U5gjUzOFad򲰇AHsWAĒ^' r3MG 6\?5Pe]̴"CWJiQ>B` .GzzjW%1$)\TAD R@i--mdz4CKuWSRfLU(2?d B:78J )@fc '1śxmw$>ryy dǤbQЪ/B իE cu͡ou+W"((fr0AY^v<,U^4;5$.8ZǖTV^^AgY}+ʔ@MpFpG #c*2GA*OŦNz9(ϱ%W ;ov&Ai~FO?9m.)6ryQZZ{\w&@syA3 ǕC#>?*{f?UJ΍)7T|bO%K)xSx P0s ;L=2k-¼Keo1*31[HوǶ"STU] JL2sfhk>!xe~Jg4:5%9,2)1܀}|`rZNP9.yKu)}lTM;WGoCt` $Bkt3Q{v.WC$ȻQkquڮygNSA\Dָm{:O%T0&[8MGߛ m@YKÁŊi6e`OFܥpoSY9B ?ԳWVJ#;8~<f.V× z1qQTK!XW~<Q{ab_*70X75 '-3U-**%Yi8/r ObT&ϙ SLC'5c06x E!juݧ#%2OIec# LXi/+qksX;*(I`rr'*b*>zn",clmj!Fi^E4cOҗ5ptOJ ;#jN;{,_i8x,Ǝ7z YH Ol(BLZ}X k&f4]FA QS:-Í''x)3y@kX9{ѮK(iDjr***+PS sS/*dsK], `KE#< Ou#zq4jL -58س'%}@`jqc !&q(T#ؐ4f"*z8wXt?A=1Zwb|fK5H-FtÎLL-c)#c2Vʘ֔ç ja2!a_*>6L^@nBfAa=Лʚ٨v"šhu0r[nw/EN)>/g[J+Jz9tI+LW6cULv+B@:S"y!es/CE Sg>q&j/%jKdOW3杛 Gnꏸ'OlnħCxaQY[1vUnida\ECCSI,s($1qxhTxB#%W5L3l׊ZMRZ+ N> qPOƚyL0潕DO{G{"loo㦠4hq@,'-%@'TT\m 0p-*o9f:ǭ.$.ϒĭn]x x O`PILO%۶ЁL%6A>p<O/8@Æ H)V=mC:lqsQveUGuX7c˪'>+rnd*5iUk& iتD@yGZ]mi[|ۡnٯEw- [_WY ΒόpPgfҌР=zO#)Uk7P<krB>}a8)QNRmf(@E! 12&LgwOL aA/[`;`0h/=X")2`H-rz+VRuy˂V9+ʺIy*L6@^ 2Rqde{>߲)0kr +Q$jiN]zd8Juu03ia<;f. cc<0=ؘ|F&fC7WfeL6Bb^?Z)ַo_{O3Н'wFѓ9ƬdКvcNJh"ퟃa[ >rW/c84 0oʎNFY1B9Hy&=&壎i^a ;{"0*t,)b̌-;~Ffg/o >*Ϗ"ʁd/vij-l,uyanW%Aͦl$ϪS9) pz LUٖ+)'1 zJGf+y /}D Tg"08bNvP]:Y@6Jr:}Sa4p]m8$冯_ o/@HҦ?U\}rCF2%~ D^[ĆU)rl`G02Ҿ sL#+7CJ9>lEB̓Amۉhb|\]Qf?a1>(2wZS}.dҾɺd0̝ҫ_Q@Ѱ|k^SY&aQ} ڗڶ[j(kA[ёrUq(K= kd6GZUhFx>c8D2lb M;Q 57eFL?_Tt'b"-8z(+i|1=*gXȁ--!ۮi@h@۲grW:ȩ)GtxOfL,1 ^2JX]>:-l VPW{ 7`7Q noj`2&9v7m 5Lt=[}Z0|LFTd htD/c#̚6 pw(*{L`;vMN^a5Of몕 kl &tdT!|J?S۪u\*z.lfLd&v$Č,wB,ǫy:p5$)( ȑXKPcZ(^d/^6dڬG:^{ "Qg 4-}q}i{ -xe84mNA~͌M@_9$b@[w s-) lKA)=CؠMvb-,_wx F!vdsjߌ(C\_ W#r=&Mƕmݨu7o??GOس Xx";~eanQ1u4e ȘCWK9G6=./ޮ:]!ޖM&#Üx}Liq&l#3rl`I >HOˀ&Mփ C `9{w `liV"V1VbO>9n[o ßk+ؙsbA9drctu G4qQ,tU7^vBV*fEGUIhP=u=wPrU뀁YUvͤ#3rtZɪn/NY:Z~w>^u+@JJV$/|W-{UUٔ+eޝOa?zXp~ֱT:4taNV8&U~C@FY.)}|Y5@Xd9"'!@IDAT0!S*vۊ "`޻ƒخ4V(kJpf\&`(Co/oM<_`U>f2>;%cu2 ǧvoޠē#|z@oW~gT@Ͻo6ߙӟ*VjƠ3Uس}+ۯ jg`nan8@'(jR >s0Hi6B jJ v}ՏSe 9asãy>?du5U>\ `c5)(<r2L W/ VX?*4{ޒP4)6@et~DZcAEEP" RIBH{7 3{w߹w9gB/Pʆ?7m:d{(iLG+&t8`U#׾OTf>_{U7^Y`\YNmkx9IXe~> 9_V *'CIrԯvtK30Jcc,ZsD|˔&GN*Z#|x#O'P`мZ`=tz3M/4J_B| \N8F#ia I {+vتmƍobuW&VvARĩ_O;HYWm0YTQY;5P.zl,F c|q`Ƴv=v(^~e,[5ʹ}D{}v܃[`$JTC U{Rʵ$g44curd8Z2t\!/F%B lv (h0A } g)䓼jK I$L=~'c7[44"*1Ɔ@mi8*KGzj*p4W6oN:3Ss^V<,(|%zB|j&'bI67ŹL8YBL˨bT%6> /8jp1 po0O= RY %A܍ [r2w Nd$ן$o$Óx//t X4jȈ245FDÄh,~12Ya4O7d!Y2MI9 œAUR4D=z(ύ9s ː >dKΓ<Pn*<"+5AQ/JdH+BCp̙7^O֬' N>Ʃ~9{?xLEO,ƳO?>DNϜ={ q~pl_+Yx"cր"2ɻtLi_^+[d.}.ରC ϒv/>!h S0u"ɡ`ZAv)wSU߄3~1{ 3q5?/nɳ]d Idh -KRVz+bH'Vﶢ9ot1:VӿA YHIPƷ*TGC4U|W :=M!40LiMŧZԓT 1}D鷴ϹeEmlgUWK]}w y/m7&^~Tи׳v `R̜W>u*쮳\0'Yb~r1{߅GԸoݤ8u%}{H_n_K7?_x>uʐ]P?|u[Vc2˘\F S)3"6#Q^B5߽81TV)+Yμ&>˯w(>zxOF!a8y81c:ngW= H ?+,<ٛc`bfVO[;{Nݷ?GT%F b!#u -;\VFVHfȁkLP &I5ΜȤ0}&'1K`T1*[ S槣=G Ѹuˆ3]Gw- 077Aph(Ahgaݷ8g>Mu= GWdR"Q3"Q#Fs@hP/&:Be$yCmZuUl+/eb䨴'}e"yfg#1;SFUxIhOJd2vc@icAGJutv  2d"領3 %=Ww专SGȉ׀ۻ(=3ut_E&>X:;b8eWS8 y$R >ȆsJgieFNd7`p+{-."8?DX/=;z(cdo0¼*0WWS7o[cA~DVVЁ2-0:) 8 n:96/o/s&y3aLgM{0`k/z> :.j{jމ,<ׁvs#3R~ ~X΋Xm 9W<[&8gD˧gO`U70<غ5j4N|g+­,9IW,1 UJXx.VtzSK]dvFwMݫa^ `ڶ|Ft;ua~ßϕ-ˣǿo\I6d*l;خBJ z||(M`vk] ${D.XWSAb36EZF"##tb}=ꪛ} JB>QV=cl[z;VigV~1eطehX11#&sg"&FΧOSޢؔYs.q^w%^;+A'yIUVڹVb1h᧞}~2~%'`׵YUM6)`P「:!v- :/zra'E` 9ER] @W]=欏ʭ˄i%Wm᡻_@v8vK:9_W jwaVjmV:R-҂D\J-@oF0Ԙv.z$YGƟ>swRaWR?l :o;VUYۄ?G3gASM)< ɗ{b/&7^ ? d EϺjb}} hᤃ~SU0m7}|X=q&8wL[C&rrh¼1MLV KӣsHRʁsVѓ%6P jރ1ؗ~\`,9aTv GC};tAF% p2,=qH I?)̕ݛ[_u%  w}HrF%(blNq88DjkyOٙpfx[žsEjMF}K=)Q'p} pe?~KncR ;`0 pޯ(i6̡֨1#7)f dcI0>UD+ YW+\`AmL`L歮>R">Tl N?U|՝nak~SWG:I6i_2]m2Z+hUj:YeБZ Zq=tdp΅QR[>nƻomw#ұ9f l:1`΋(87d6Fa;0;8s| hbf(|$fa$ ~!^f.21#k yT^"Vۑ(/n3mAS/8^z<}>Ǩ|&!ȗ+4vYGo&C?O?F2Gh`ʙG1Vl|S> k>O2})sKzTV5cƌ٘5]8j^֎ۆ%b<,PxR~c *u2g;nvő:755 REɋmKg& J *6fp*`fD~ (A̹F _æ*=y򳏙*$0W1 b)Ds # 4߅c'Q6c=z,TTЈd~:(8Cm9mS"'-ˍ- 6#P)TzFH*7Ǻ}!c b4:Ή &)f qs$ ʹ3f.k[u4 s`Ƞ޺>v*,~3\@Vְ ̕\.mΊJl*yim. 0џukׯe7dNуb(&.i-^y-xy13_qpjؘb]ɶoϺr/0UcoW:3&Mj^OYtƫxVz WY. Vm565kH;vh/CbR${A!.WPtssVu?lxor,ja!JSwɾ*..:|y..U6W=kt× 1Λ=QL?!Hˊ]䒇+X~׊nvA Zq愲^(b{ *| M@F|OiWoR+N(2k]? 5K#+2s-Ij3Q=v*r1OB^Hؾm9--WO۾#f?-V歓16񫈔lSOmLPexo_)N З穽eO3[Va}q:27f^"|߃Gj%٩ p`Qp0gdBs%dںÖ9Yo[8Ӧ%IFmf \i KH\(bM(8atZ%Retɘ3#)"g*ȼ֦Ȁ}e 7e!zkP {| );d>mf E, 돍?Ĭ%K ZO0Id%fרanBBcbR ul'ό,xTQNb| ÖAʦٙSDW)㜑kB>ea"k9ނ3?rnAtm.z )ם`ChZQ $e34`?OwC۽9_aIs$d86!h,~UBHVA*w 1x ИX23i # /JRf-/-喕ܮg@oy$@XXo' w q#} AlPHb[ 0x0la/L.,t42JSO6M7{SȀl׀}4{S*% A.Xŕ(%sq}Oai0bxSSܶt2؆>2A v|~N&CA4pab>#D)$LUwr+U|ɸ5B_ÅLl4 -Zr's0{툎8sYFu1q]|ftA&ArL2ħddMĬ12ptp8/OT{"SˡksM3唆6F9Go|KC Q ^Ւ@o0z+}r/+N.kĿޕoXXPsHAfZ"ɸqܹҶ׳>qJRuѝTj{)(J^GjJr j]k_LWւ}9AZNMC~M"XbEDKˈ|H?yTFiPUoi1ܔmF Ňk٩euLW\0yF1kT|- K և H=ArcG( 醼 ,gq\[J=,$+^4<2X4#fiMmgfbJ M9hHbʗ^*lټ9[W?KsXJ?IR1d Y0QL D]8i3Rb #'l޲I~e>dgX_dUϧj&Xny 3L2I[Ơ* ~,80#J[ NjKZ䘤|Y} s]M>P^&&aVN&G .U޷#**ϕ;,Ғ(хV~F/J_D'G "]t;[Pƚv ss?DYM,@Tvc, %*td۽ԙQOV%E>jl۩Qfx?ftE#-ٿzÏ?[owXe+``0PcgpT;ϝ{oX7lJaJp\UMpOuWU)23b !U Vy {(io?i/fd3Jv t)˱fڠF͙9bd劑?S;8!`t7(ot@S(.^Ͼ{P l=pzZR + ZDl|kϸ"F/?áG,t~<?]JW`"Q@o7Fж2GVf5Ǝ/߿߶d=uLٝ"R{Y@֜\F@'^|+AVkGѐK}p,j)f1,H+v e$w[8{y:/~މl#l+{~阷2Ҷ3 l׎W/ִ:S?OQ˒v,27~L~C %^|(+~ߧ#(}S1{3?QOƀރ@^{Wbu {)&y6hcyEtfM\WgŖ-U!J7bԻszW+ ,|z Z;̧`U;Vi+4(BC6}]aLn$:,m~9w,CrB0FRN7C`n:o}4f2\i[8";{&33g,nNvwU2';T T|hں5g+DX|yr l+~u/kAC \O{/o]+ޟfyVnXfP'v\$Fme +vH|Ԣ]$#e_rPhNj{}J=tky߶+&*9qe{_ϫ2hF-v_JgԶLzzuLL"3o0psQ\<{V}`ܐ ((%]2tg%oҭ0!([`&T_f'eJkw_DEŧ"R_h񆧳=TS*!53V>aу>#WqSVo̢,FVn5\"8|8|^DG\9,Dxgw-rr|UVWzؔg)f#9<.#Eg ?39}CJ6)yK/gc&GnU€PSQdd޾d13صC'UlǏoFNJ&3TGNb6Y&ze#kz䖐QTXDr؛@}eMO¨>/%e)G[Sz23H![=.z>%w̆.eEԵ% u'!>>F6Ly,pv{is:vbO?伟V68DU|·tt)I| npf:=m1a'YE\㹨aJ܇b8xl/d2x+j+21^T>o>A`5GᆰH4i t`q'eP}ع}&QF[0kYoYF9J?&/)][|䢯Ie\r+2Zoa[ҧ3GzMePFd|bbn[g:._||WF n5 ~'i,Rz`<#ؽi5#"8v_`LjW: ^x*77ؾ}"=HCיZۺD6ϛI^>Zް"R$dW62؈q"QfZZ",Z%#@7SZ7vu{y{cx,y&{}%[q*"9cJE %""wKcbKaJdk$0Vy'Wؿi-z i{RJ_V&GaeOHHvYyQDz3?/xҝx?*;w_iQ^*sg'/dPjDd\{AJV6+r"*Q_Y4bѢG\‹ 'UM[Mu3h8|8IJe0c,-8l$e;g ge vߨˍf-h %ަrbaeخ>ƐDZK8u(ƌRWOs*=Ss?Bͩ=.gDwZJ(<3K~>F#6eL̓fT|zL<ϝw+l`^*Qaah<éP 22̻ MNd7ƒb/'S a9KY;ݱe}P/3Ƭ[1hp)ņbܯ'RqB܉5y?0p¢b"NTvG4R+ōtLHhrrMJFmׄvyMU$"#!8Gt:A8a~/++TX*:[>sG…&{;ڶn/aиݱ jGj¨zGձ^~ےjGUĉP.QY8A_ˀisAb^\A'Q6\ !Y}(k#Τ5c(_||.`^&7wƌ$NA0D+g[L׬L #a$֘ ոLT")=E`ق JN,4dwBÀQLaG@mSu1@-,M:u5 S7>dܺ +28%;g׶~B ?I\sGכ 1n?KʰdƔ9n:\xMs h$FS %uv,IL?̜'TA>rchXl<آwaTغpwr}!u IG%y}|xw8,ʲzjz9RWR EVY67H/eh^jDFa%M?ujZxMN\É,>78v("+q.`R:3JMSD5,+5֮h&9h%;ISm=_Oo4gu#(<}RufJ?VJ]ɶ. d5SH^ҿlҦK}]OҘ֛ʎi!ki5uCj*,P:SZ Yc0ldjٺc1~8|T wjm3c\DaU ܺ.>Ə5}*vB@FY. J1L8aE!%-r>%=۟Χ(Eiponը/;+#hȐSVCPC7 f)**}:, +CѮJ# % WGٌPYX/W̫jm'!֖**m܌1 HYUb @ZN"ݹ꧳kO<}H] 00'NTrIu+ĺc4#g"(=q)-1~c1"s-è"0=%3 sa I.CwQvW Seֹ ͞IBl(ӎNӦ`"i]QG)KqsqR#іM >|lVkF@}VK ˃p&g0F9NV)<^@ܖVWQc'10uC>̌*]W{Y0;a6io,oNRK\MhϿ1k/2dSJt(6μO(cJ2uڱ' ,FRr55OYsbR v5Ŋh*ѣszjO :KWؑCS2풗NW_sskֻV-bfM`O6"}8>Q##ϕ&-t,p2+< ҺEtTC>8S^=ӛRXD}A9k};}VQ|0=HQK(c`q"78h~;ˍk>XgڎE]ӵC\ H2{[Lp'_@mu=z]:J["z^mϋ'Fܵ+d޲n& $z=1ѱIB0T/ w-ձ~6V|n5FѶ։fk\5<4(w[6zT[6|S(h Ō=mLƥoRATN߉ 1\{鏯 Gn`=KplgOe(/+7bx-EQ9H]0XV9Tܹty6{0Y|Y:]ڀVG2VٱqcsЭN*~z|LUBFh\;pFGi H(E]X6嶃{XK4GV`H9E_{Q~vUzRGWbN"][me!3p49fCL1.0;n _u̹ >94>.t3+> M8z4j0dLڶ5.B2s>"rȼʽ<5%e/?\Z7$B~8 4TO3<ɚ~K qJ#Hhj^.rCL JyƆH, j FAJFn8p? o8c,.CyI}Ux8zuk[NGc}h@)_2igҷCطF2gk LK(Lo/7xa0I]ɱL!dHFxxUe%ƌrFT hnTU` s1 YHo% ?g-S&ĕK9s #Im20@]0aQdapJy \ɉ泚K0uȦmb a8t4zb73;>x$5_\ [XF}>>ZEL lv)NVw0Žx'POUxꑗB.;4h߅8d6tJ<Jn-̾ۘjdGVz|(,è@,)F5ʽ_R|46iJ@剟`kG${C/O/\c_[ .䜶f,|t4CHD2,Kփ56/T]Rʹ;YỈ\Ge~[Hy?a᜙ 9j(}8xDO}:eĜ q@. ʓ1>`ccX3Pd"4P۔.rW>CrT.C5 V,9yR?u]dvEUI-mQW`;S^Q ү{w}*9sT͛N{?bz1.tQZcgfcm7Fu,q1du\͇<jhaGyYhMX`~.ii0~8I<×@EZIҁYJyq0%(}>Zd/`42iՕJ:V~YޕҤ55m:ۖ3'Tèn:>K[ċjȨr|Z1~ˬؾk~KI6J[,XPXO>H%F9\Veb$)()`t;:3JAKq4>`c981>:pEu-~Ԏ_ _}ÙO5!"uc$RV^ Cw/,Xt!=lomF‘ iII8=OǹK熾2O5-q{0lb>+o Hgwb\<;p")a_ [T@̱Nd@h`_{NdɅ:9v( 8^k2$j( ΊOe&ƼzQ]ɲqҨ&*jS-*ًMThL=3'(" צTĨmNlܴ "oR~^;} I^txܾ.H:Bo_:3kuLSjag'E,:R(9g~ -E8x ]85FeQд,Lb3#}KUU%Xr;&00YmK8 `lY(ҍקSrs%2&wկ6lLeɯ+KN|(TW#171wNVU` `z~܎ H esm`4W*?t9]':T<' t<Ϸ矧=:(g$"@aKkwD̖\K&#_ 6O]_)5_YK_F˥d(w7ƵNOGrYj>rٿyؿUkI,5+175LVߴyuV-գRL&2?+p-lUJߛˡJRSɶ?p[Ymbq??4PXt}J UOl%XYvkK7> NtϨ!nY0X?ob p1`YU_4bR^Ѷ e 3K#>X9gE+0ڊ3(0ptxPW݊^Rf7HyɆeC1H&Y(lچ&/}q_mw>87t!)c.\#=y`F `DHN>g.4-5aj% 434h!k|LFߧ5:ۈ ǚ*lM)+=_L!enZM!)fd\ON! &"Vncu[띷>__b*>p 1a0HtΜa$?A&9 -#+g?6(jٌLctV(\lsS3m K'{0Vfux= lrr ^ޱgֵsy eYuCPLRWV~MDpyn-Lq&,&Vprw!`HdeN\ͨ*124rKxGi]B2Gysԋ5e0yb6dV2]]x*nUֹThΟMEl߱ AC$!Sn׏ [vR93>֖r4pJIǘR-I [wwpxQI-R TSaIߌC`@vNc:wWU6 EI(.Aј,QTZ C2c}65#>79=}6uyPùi|\0 &K6@2黴95d$!,b\U::\X_8* jXTMfp4FZvdYMmkL8z1e$5 !6 LƘ+DL쫍_15κ|zPܩ#8s2UȌ%]Jv mxT3X|x8S}{IhfBNUc1GL3R5--!A!'7+}>[.6;:3Uaz'k6:XVVxԗc[ﲟ'kenY{6}e[$qMAÏIŏ\Qm2_́#ZR,Yoz;jp6,%n_oɭsȺ ǜr&u>~* o&c`kwރf`A/od>)P֠_z MXU1HYNKg)h9},OtofS ]`55/;1iMe@frF39+,69apw^tt2Ps 3S(y/mmc}RaǪ%773]ֽO:?)pV: gg͂ u4tذj 0qdn,pFQ2`vL<?''Piǘ|dV ow֙n=S 0o~HNº߆>oZb4O}atPSBfR*Lt _x<)7 ;/#t9`)j!ʲ<'t٨<321R^;1e"!zߋqTV:FȤ-bXDGdAMhlAq{ca*Dk:ш;^6m+;bYʘK#̑d{:ilp!6/cJq8`}w˘'R+]aru뎯ИW*2y~N&=zmHv<~>7hޭW-ޡ.dzy˷IV3ϽvxJnЁ)qGɣHd~l>##K:m N yZk`\-!X-CsVY/ yF`.T3IwcJE}\cROJ_6z2O૯s*،z,(eFsBTfHG,()'p2߆=,t#clJ[3=#&xC_=cg; `lv Lx|o7zŒG\FBwwX8Nӑ s{"S[`>p3LYVQ\Kάj^:w}? .GÇ##1ߗ`/233O֮M8 K^|FnN28ѷo o6̩c(=O kA'93@ G HՅゴOw[sǫ쿎Wct):ݕJtؓ]\n돲,O=LM[\~qfŗ ;}f%.}0HX93?>~oolDYU0qgw;wd @ b%d}ѻN= {k^ardF"=; ?ۤJQa x~r; %]|E\rҶ>U [kw7r.D`҉? [YM)E7S>ckvT߁`/K''{ X0Kԭd3UvXg=IrHxGnO? qmz3Z(Z\AЎEl=B$㸤tdyvTnk!یtaGs>1 dQ9ױv)%[YVc -dPK1Lu() ygB([5J`-ߵAn*YI [xw9C,)Pu"S:{KƂ2+N_Xqv_COK a ä`_/<<̱K_MV%?ދKU7as Hf^*lG k#2^qڀBK[ƒmҒrg]֗ 3pC|d?LW(?D&H?PC wY%5XCЫ*֣/wc3#%xWdfeL78>Uex˘'9 Iiq4i퍼r`:,#'X8arLt*bh@V(WI]乓~KT;_~@ToE{E`"mҿj\7^s覦nY J$gHphmܸ 'އe^ UGfȿJ'Dp+=%uÚƆZ2H={eόZiLA^V<>(ܩw/saE?u)dFT0!=0FMNXؚ77SF2lY).^ P2"#p:&Xj4.}kbK86Z9_9'd EɁ]z=/πn3~}~X_ r܇$~jin’dhIY|;{'ogzAJhyd jS9>@%#nn ؏>?;rMFjI\rFM(Y(WWM)?Nx~L"}WWw^?b[Agv;r4v| [w oN )Hr:5LD4*JM-T&$ʢ8*ƌݶ ];y::^<{9,W0ssxW0gÕ4vLXi?֠A5,nYKwAr-zJQ~mic/J(15AbBJ[ )c7(jhޥp˲qeQo bz3HIM{eNӇ f{壶_Ԍ3Wlym3e+p*Ci^fc[zX`AQYRKYpNM[1}/:͝E?8nBYf($o7Q>̌b F#X0 Je:%׵J %zJ%wrzq5MZ3]-ylGLbgVR bC(kdZY\J,Na}ѓqdvP:'Aֶ"%99,:G0`W7!h pUXR.͕Qʯ >dtW},ovv5Gԭ? @ mkg;?qa* emߺߣ*~!Q܃N;OXUdSKxjJ,cOK ,'nI $X\hPY-Vw+.ԍw  B=s&LxBB~y睹s̕sssBA=dnM 2NWEl+QAcuWa)7}=:r-/ߒk!z%*c4I?cg}Izu5%%C^lZJrU"J} s(:lݣd(]DJ9r"e s5 n^tkrEF&`P]iI7A/r⩪,$,}Ʈ>%9R%zmin|tb(llmU-WVItHꐹv#~$44e,u_d1QԨV:qmM?VzR9RKL V8zy93v+-s]7wߑq\db邡A(ieH2xP&Э'%j VALKI.j2><!nv~i Nszr:`L|B9ix$j2GB~Nc_7ۿ2&*u4 us 1Ɯr~؞m#Cu0Ѧq9o؆pF+LRjUf "t\UIL?4R1D<0 !A5V5rס @AQ;M8|<ne|FP(E(cwu+P5a!JUg:F 'Ir= jBR#=QRkY/t/EIu^hG {k݆W{@(e"'ᅨ$R4R)U魖w3<'`X2N_Ii&8v(qӚO !TI'?PO r2aA(򣪪'OpeBx|sB{΋ {&oyN!+>U ;8ueoO*{q2ϾhѨ ߅M=iX0-,>ē\ n 'Wsd%}xJ_U'75ވL/ӬG}۵aShx~2HS|U4Wc< -g[PM,; 4 hnO3ɣzȪ܇,Uc U2V,;wbl(wvN;={ 2J%mXd^x.^O}]۶f<:yAVK*>u'=ԣYڏ:~vX.\9]D<8 s-ܜlz4dwYW }KJ $]McN~ک ohEq^7V`{b-O} >ob2l}É1p #2;&0uI\?gz1i@?q0-4XM%ga@TPO"m8Ǐ @cSZ !cjZjmZl0ô֣r!H(2pOM{VmXf@O>7ӧ0vS?PoO/-9Qi!j(ʟ] {~FG(<ãg $jelRfQXq=F\υ=뵰/::8mBg?4-& X@>_PE~nmo7࿑{_57>1H-* 5Ӛ +vJsz 9YmNBNNTr\ÕGDb[oS=QُZgqٰ&֌G6˵ x=4өD^xmswWs[9߉QfYڮ(/|v r>zD ~Nv:sƟ酛f*FE-3TI7trxj9*г뙔d|3C9Rk ?ݣyok7( }@[M>Y"h -2^^+/Ӎa[ o_3[KY\gq~.7 :|Q~I~{^/+L]PT5UsF9@[Gslmkڋ5 jXhlPB{J3 КueW/hQ+PYTJ xr 86j:=1~LB;AJGkjn}cpßQUذ[˘YFQ fIjO3RZT}zL5FT4L-:u8E~RQw*;h\ݤ/^ 'eCt01Uܣ7-"$Vg/e؁6C; h_tb.ڴL~i `zQ76+6f&0 1adpGA#'_"ޜ!hB^e--SB=CAjGF#hyrZ  ׍G.]*`A)e ;cJI@QU<}HaҢ  w eQOVQ[ iz&ZkKM55IgI4s?p$.$mIq6̛G0^K*h7SVuESLHclDOm43 Pk}H*qN-Qư/d)Eb ),zŬ7eod\/ ޼r2PQ_cǎ(?=}|7?^% K0HAPz=-'vz>a(3TzWIfn+GՇ>Llq2Qg d_1M:h@S C.wP?$MXr gLd[23_Ax8΄!;Ʃ 2a=43GeS;==;4i3`K a3c2 Bw0OP_SW[SIboR 7 YPq3fý {8`Ͼ,˕Z$zTaOAS;eʎdz`Ekq $=-1 Og?|Ŀ:0J2*T|hu6x$~SGyN6/鴬ԓr/ϕ-iujIeLZ7u҂ԓ:D}L97xw+w:?ҟd Ljo46AGջZu,Hzk/2aHdBRٞNsp6|FqХҋ],'r'Ã:e zR!LB!+  +,4C$E­޻q^L/LjJ=bNWۻ7VY LS:꩷sz:VUKKsb5^Laѽ 05W;&n/(ܾ Q6+^6}s9W_dleZ–r>|kW!|Y^c"q)ΗO^˧ cI"t-Gԁ(zX iFe,cOZV8 _Ԩ 2l4L_~ ~1Y^ SoU ;pH\1H/ hG#wp%g#(4z%[֧!Z:}pz09@zhWףoCpz댓ۊ:ƴ7'T>\ EOh)RI)qС1eÌʐf*7 a%? ߘA>c=v0dY~#ϔ~˅)oxct <"gU,QtɜJѴږI˿9f+/Xx ʖ3n@4律MI SlR W`=r ixhDm~~øPnu02jxi U?N57b6namϗ~^>\1l45s7Y&gS =ΓRN"  p"~,~R}d"E>gp3/°!1w&5|z_'QH=/*ǻ{w(Rqgoٌ1T0uO^뛤q733FeU{*_(. SWNM;kV/K+{t򛏑BW3꒷{eޜb?~ĕ]w]P$Nl%M z{3?0:u\;.-'^Rwر U{1sa'm+1#)է'P?Jc|IG'(뫄CJ+zYX(arIIa=8 K;7ܣQU ^KQ/\v0>B4KS-u M<"z0ht¢':RE'fpY+:K$ 73˰' f3//PMtQcZވ(fقFdeoペg/˹PLOGO7mVm!2JC44P'BFz񰇶|†)j : Eԙ:K aNoPݘ6:DױMS|7f t8ѳ6sƐH\2F& f;'n ['$%%Wdr's9("Q>vCs.5I]Nq: J9͇I a0vDT`%kAA. rN# mo<(L-ȺdGMٰEt 6"CB SO6#^-ݫvsP< #A1^*SeS RdM/Aa2yTzxO`u,KT#MrM)'+^,-*?8p(%qUG9@XR=Oi˯>3g&'{3 #Ժ Q#nCNF&NGPqZ8KQA_]֘I ;nzC'z'DK1_KoI҈fT>ΕNqt PB+3(rJ K:u}j}zJTb2)#f*H& :~p_TC=Ńc RϙԁRii4W&E#'A]}99Pa0efg٤P.NO_;R#[x7y}IߙW6j)$DFKƝJǟxRu'd'%7ZaI|'k5FXa8srrv;L$H32Z-}B"YH1e|jݻ7p=/>y!Lڛ6mp:OuWD>RĠEUi"'|i&( #lQ܃jY$oWF]0Ro6UKR 9}ҹ1UΑ.yrl:k}v_[c[tҶ;v@ttt+@*?`BOS8oI}{$.j{V\KٌL&=NhaEVʷ#=i63c~ʒ&zm7#1dPiz[I=̘0@l*,{Y:8*ޞ_XZ|"e,65FJ+=2ه} [{o7[to"/ M]n 5,e W/ǀ 8Uғ옯V7-٤ܷI ]T: z> iFIˈpgJ/mXs$'IU6,j4L R$ݖiA*//zɒrq ~ 5pzp<T.ʧ{4XO,76[m1J_e#]KO'clJӳ=ufƓ>0T9©xvwz@k3@s@Rf |~ʘ*J .άW[j0q}'wacbuLV3}3 QG Z,0nuR[ifE=cCLVf.I۸q./#7$oЅi}.Op]$լ^CIk?E)j6;Tr%oex?{1=>%M؃wh8l9gxv\3=ޞVπ~GGVi^|cJJ?~ ?'={}-}@]cӃJ5Y]s jwK;WU H)ΞV\wǸ$Vy`9Y_~[ՊUE#Kз5'KCaQXꯖ#W>F[o+}25u1#;~@p-U-;zkߕ@ր<ԙ EI FJz%1f!l(dٌ5iĮȊǟA6DO4&xc DyQ.LIwnz ǨvŠ_аDŒz0[Kg `P'聝Y6!! s9B`py-#**Shh S PK=3B6Az>@weݵIlچj6è׭ǩ|>+d;|@P؁Fo5(;t6w7{OTse zz2~.*z8~: K([j@^& ڇX1ommGJebsC7=>1ਨA7#2R bI[^Y݄>Q`fzW7a}hHlhb>_`ú[ɖIzV<&ORޓ+ BDGfFO;MG9QM UxW(s뢾6RjC^Qej:9iXz ?'N1?a0&}yI!vXO^+VC1wo|N$ahk7+-(gLIS'cYJئnK=>[z bGIfK𰝴Պ!8%ظ7}PQZ 蛘bЁĐRazWT6!G! bn s>7R6m(WOŊj ZN C~6"F e7erKǏ¤E)bo-e }}kߪxôӠ%>F s26u/C3@IDAT٧=g``**?sU?v]︄ܼ6xS՜3FL$Y p>oN]_3msݩϗ~ձMk]{Vv/C~zӒAPs:X%]iU)*|(ϾKJ>(ry{w/ܿY@+EI|6^|Y2S®5ɷBBh~kaÇX$ҲdYIhte:8~ے‹".Q\osbWgC.N)oL@V44dj̽{kBp̣HqțS:,d^$~pz?~m4ab}X*cO"~_ SҌ=v^#Ve?*z;ŏ>5fw}~?˷FRZnj{nSJy_z7>|g=m|},@,6ko)T^2\S9W{_ş|f̘z{}{STtǴj2|4r7'󮇻շ_2`̍wpP -]Y?M 043=Hew= ذVm64Lv]s%$:ȵkۦ#xI|o|œrn}awjH=GZ7 PGƧ߾5~mU:}X8L%mLosY)5h]_ǯ_ cuŮM[Y_Pb+{Լz_4]\\tbiJ=.Wwψ1E^p8Ds xؓj҆zp$kl]4vSWT=b%6`ȑ?/,FkaZ 7_B,-?RW4 -5E1>x3z$nXPBbDCJqM]x ;k "3C Rݖ,~U 0ﺱH*SXo!u5+J[U4Ʊ03C騩'z!8i(,ȟލu-($O(jP{|6:x\23U`Œ)h3y7͘adt:ېFM* B.fr8/z2i&C2XoԈH8d:)-4 +~_VaȱA LA؏l9_# W؊6"W1)iГ`p63B-׵5ھh8o&;j"OFyf3 51NAϜBVDx/|$_mcÖ9-9 1t 5.i̧C=co-ن@M2o*_6pmǯYMnӠ%ߕ'33[n=E*q]̻q8L=ƛ[ъӌ\9*PK+T jdݜHpVAD- _u&NߒW<.-hh3 Z5|IKJvq¿xqE9x@\^"otMWKF,PujRQP 2M +6B^]5C,E%vm{{вv쒪̹-’ b&rLBIcǎ}<0abTz:>]!Ln4J鯜ۿ^'򃵍 F+W@bG)? łnu? Vbw"wrی~;/j!YyW5V9&H}[,H*pvtySI6w?|^ ˎz[}ZS]Hc5^,83MڪkG0R?OfByDϗ~F x[ 3M~ G_c+kʼn*ƥ!%%-'jz ܇xj>{r_{>')[QhzYK<_ѳTM˸0nji"#?Uҫ|5۵M IUŨ(#Nk*-sAOdOy[L,Hc\L&uX[X^n\q(4k=%g?LE9KZ3马۳; H: .5cjf^|9x(qG{2(~|)Eu=f/\wZNғžSFIی;5fҟqr w EaQ|ax.nW;t=X mE􀥯=[p<~$ЃslOABQ*YzFP( h(5ϗD^qZpEFc-tpJ: w W` Aׁn9JkĊVЧBua\iQ9ZM&e]CG)1z;?33S;,.Y)}'Erz坎KNt9̏=b⇹h =幖Ϳy.HBaЄߩxՓX'2xi빲 uF |wOiɰx6ײr5 %%2Fn+{qL`i6*0X.@.x%ŬGuc+>+*7-jwRˌ1lɮ!eF~ |\I6J K1O!\O{ÉQ^uNk ~g ߋ4\_p MFXaƊ񻟕Kx\8uels^* 6{WWN&)Rn{e1sŐOX83()_^X# mD>Ք/̫STO5Tb:gh^Ls繒+ gzE;\TS' w_q"dէەJBR^N?2]vRa/ItD(eg%U 3=uaDf*w5ۤA!}zCr\ڃ&Y-ZZ -qNC,J?Qמƚ\wЮy1J Pv9Od"A65h@ZOfݔY7:8},CbĄPsj+RSr|/L3Ǟȣ7dY A=X1%pf՛&N\C|]vO_+o0fLXߙsa!﹓iݺy/k 6tH$xd:%i(wQE lމYgJ  SSj=7ՁچAZC*dr~T<\<@pS\O鴣`Eoѵ{ˆwvcz`?'KAߖY'Ȫ,϶3j*gk'CLM_OOT0&-u0c7߂Sim& ɝkVD*I{Ԕ>[FyUMah#g &&EVyz49id,)A/Q-E.n8Ba}}酚 CGe c:~ uw f֠-R Q] ]KT2&̂ B׼&}>ryŘ8fIHÔCA",y&̽ CkVH9 jI:S`SهdC E*Ai>ްyS^\LxEFaQHt FzrXW.QFwل^Zv07€0_IzS j +'p8RN.4$iv&";c3~p;Kn̈Gkڭ.zھ޾E>ɚr kom,cWf-< uiҒ^ezRt{4~d;^Y3# K]1ضk7Ft!pX8sۮ G\u?-=+fP 4 ¢b÷0Vz'I?evQGݍG}q-asZaēʂAӪ@;7 ětZ\עUSvm8_~iQ't>$d#*D5EhŁ B]$hrBa9_ud1hnNI,V1y~* 1T*Sɇ4y g|w>7wwpDg.yָnTe KKg̘ ijA3v }iZN'}U|D`3V%VRϜz+,%3@U>?Qvo}`pPƏb R/sM'R0P8M`vkWܻ6WNI-m`{U$ :號m1oW`kky .V򰌱D$>56 \dT³Ofu.]Fdt1ε\VVy6E|R;EK# 4mb`'"ɀ:)#ABxdH4UCFT>У3#i( RCC#Fbh?*]( fR8{d\ΣΡ:%zSU>t6$U9aP̗0"HE:l5q:.+hZ=v;{ ydo7zsoI9' FN:G6LFbVΪ0w)|Z8lHTgu'Uمm;V()nK˿'\.,6ޭ5pì {qz8RVgMOY+n|-HM؇!'a뚟aK0[*Ӯd^;:j̞y>؃ؽʴ*,y5*kqۢ[q,7,IDP6ܓã)8?/} 7Ϟm{7ѻ+#v#64V Z-[LqyE.]>ȏJ9Z=+sVI!:X(pfٺy][P=!{eRQ.5_r-o`5fZLlS%jSKߝ{+Vy7rWГHd+;ێ9E^K_}1Ub^IU/k@yRk1]m_]ḪTۤ3ƹ"?vV:nOaEUuR|,TLRi7NYЗt>#{亪OEtj_TRꄢ^J;I;Lr\S\GZugVLǢ,2Cs1 ilǤDCyt LyowtF{d1e9O wU1,WbmU‡ɣG&Ü5ڂ L\G7-V_twHW_">.a 44b,'xڂqOWF+ڭdp4"HGEAǼ<=agm]\W Rb9Yڳ~CSXLxv#8DM÷Bˋ`qҳ /1ް?>[s 0m4-چv@nn6IOຉa89t @fQ=*{տ_0)zCZfEAq )H"o˜Cp,'˹ш JFCVV^'.x\Ӎ`oImHzTI^hŽM57~vfP'@Ra_g>^&8' N;H@zMN%D.) Xs]".|?%mnFuKL -+}4dDZP܏ƄuH͂!Zkhm:Ʈ5m B}]} Uj3eKKa?# dY=y``+AzR,Fv4tㅭ1p71tp$o;b63n >>#Zh!nXyd=7݈kû r @] 4"6wV 7wմ z}3P:λat jOh8dʸ^^>q:ܴHDNP O!ufd`oz6،(pb1u aߪA+߃%#H `ih1#Y^iAÌ~4z./,[ˠ]?&yXE #FӖZb3*'Rs}SƓГ:8剡2-2F_h6. dulUޗj`Î{!ϋV9smD@X8Y2[-"mIﰪʠ&:57:tw¸VqPmlaG W2&z;c ޝ=n,,Ķm[akesQ@ZH/vL  ]`w…\W:z(b% L}˱z)| |e^(Osܲ/?lc.qpxF6--]g!=#M`OF$H{3x0v,~%W #z7cldr)P5c&vbKZ[< Hgl?{I~[ Ǧ`ڤqx/ `=tvC ?xn͢^-i"Gvyͺ˻IOݓ)/Km_jY%%TT I_ Wb@)5s9Gв,E.T1uנXGt,dm ?g)JIީby%r\&ՋNNNVbc 7|.ſ[h1vJ}h_yʢ&zV瀬j eQ' Fyq$I@>JZ5JEIm -ʟ~-{)đ Ҏh%oɒ^f. *u8]Y/hpBI^.i\:{(E@ h>twϥUqE)) _%ğٞokONTuTM?nY֢ d2q<<-aLӳ߉ A"1*/(=r]XF *my}Bˏ.5WZ;=t>zEOrekI!'kMN=pM,þq̼)$W`}p7>CS*+DeNcN`OS)[9OGU>!ᰶ>+kɳLyn pGpW̻2||1s\Kyg!s`آvíL#ϧO/֎g GU@[5-Gh0(kwuؐ *uU>P"`D^7qrKʳXt=pI{K;}<|bz}o?6~xy&r֭ƍCH}{,nZyi%99} G^k} ?ҀbNڟ0sl etP>ZogK[j\jW3&*˰%dUelhJ|8PoecN@Eev?_iXR5lT%dR_-;y޼=y8R.ufs.`^^r*uX{YG `^օ~Reب%S/{ھͶ?)#R$zuPd5;w9*XOh#Eax&+ FVeSK2,Hϑ쌬}[UUEboF59m*,9e{JIPvȏgiii7Ԡ"Luc|4r*x鶷7xN= K 7;!\J8ƥːz:UUYFsxZalʨEQn |=b0MC:PfuAJfհ^$7)3oŮ K=g)OLd'={c#Vذs#0'"H?;kƑhoףSn2ue$ vl_2BZ4os \ɀ1hطw&DnA"Ƥ ,[s6Jv@lߴG#,%vw HƓFAU)-iY7L>Á@)Xz#3CXՊ uvR;8kaj#9$P'5 y+ +Zت ݲ"6ԣԈfX7eaN2La!桁cnm5ds:8ػ"tI~2M?vd1 r~(.H>a'2Ғ %@Caj^ٸÕ * rj{ rPGO@:cjNqs?`#BV !O%ڬYH>ͦ}>PRZ {xO!5cVBwad%x@5ҥiH< Ι4ko;{S9Z :(8]V>~z+w4zRbcþ5o6J0~H7+#Xs<9:/GTN*qGb+>!j+1f8 4ިsj3C' \lCP %5uJ-ۉ,޹Csx^ٌ[o_`c jipDmk2NvGimu <:חˆ6nH#AmTL?z J٠f!1_L`/HY_XDsnvEw`;f͜ 3$&+śT6)Ŕ=f ო#cht`7EzJqfT7T}1,TAJETY\/"{J'*j"t3"F9K:ыҴ j ?&FGҋzWO# ?V>}Zz6gD4%Iglܐ"o9'Сp72K&tk_ H$$ Hwm0 K[|`$eW+=;^|Ty,BR܋D{;f_ r.2fq*^Z߿cJefzG}u޶-(kEWϑ{6ݡ{ߒ42ɳ^m[_i bR!ts;2<"tOjǯ{c`(VzKCv۽j%ME*ӎۯR/`)'1ԯ. `#7x@Bi- xJ(ov#Z<5n0n(G|\)Bʪ__6g 2ܢ݆n~aܠhVVf.-kQt \DǷa&f^w$SOshC4gڼLXzDYve. ԇ-<߹R69ϲyD}QeDWi>U6Q 9|AʒDG!ߕ1T~uMi2O'C $=JݥΚIm1yH}4 -4r `1X@ϸ+Dk֞Y9BR 9sJ:ޡ,DE7/l݈#;جЈ8nH/ɓhIcG&5MmFgD3lo_U&,ľ'`DOʪOV1CUաO_0U;#'7l49: 鼤=+G(Œa`Dz<)Ejm,Rf߉Ti*lЪ :f嘴MY[5OL"D3EZa -Rg8N 2@_ ttuHQ܈c)Tj^>=K aOy)bCn:A3iF CI*:ªVe# ӡ+2 )Ő v5͞*dffq@nRN$Z`.V-m ƒ@)[zr!BС^vtlvA>.4, $^өojƑ*V8>4EacCf4L5UHþHJ:)݂x{:#͂ k={Фw!**0 q[qb8j?:rc߮ B[1C? -xfbXX#b@JC s:kE#GԠﭹM #N)`)޿ppm'=K ,7] ` @[*TؖmmT[j)P%@B37L>Ph7ܹs̕1>Y/Ɲ^ 嵠nQ32`e5|ɰҠٟmw}5 $c샅6AJ#Վ|6@=D"5vx+x6KXC^ե`Pax>a Ӯ^" ZQb pMzJ'n0V7NflL/Cc ߅~uek`Wt a뭕J@V%&q5}s8ВN]"ҟG-q.vv&m`YMhՓYg(YFXz1Fi:8}'=$YffVvtl#v߶ xw2@;xlrړn><у#ƥYHaPXu-HǠ!5HGG^hԬ1*[i-Pjh)iW}L<٬YGˌ ^kO3XpkRbݞ9Z}X)8 .bP@;9iYŸ [Ͳb`o~ثx^{-iYI 2xO1c45D!?X仟ըvǛVkM^Dj9,Kvry%k9jSZ@ iZI+f+-))B B[uǁHx;UTS̜t"B{#<*H,<ŚT6-%UdApP\̦Q콳oxfNVI >4d|z:MYxys0yhU&;BN[o56vD1̸={u$;6RZ()))A57ˢ_!T^dTu=ώwvݚ'^޹:xP KF|WCn2,^Mrq?5 ^֏>x׶ɢBTSIi *50,YSmt愕%5;3wV'&sHEhwx2SIz>9p1D\}Q+Nj[3@ތlQgFo@8|$i&q.W_ǨIW6;?k{ʖ0BeoǯvСZpY]yb͟NhbGz); \Dm(UCc5U9G/%(E!: BI.y;H7F5A)*Qߙ:G|8vj8y'ܣ׋E9W/-B帰(}z:E+#Li(azN{֙,A82dY$Me>/N)d?י;mD^j o>A]6c٣{pmǁ8wÃOT?3ȤoXX;rܺ'K91H̷oT4VaSذt1CqIHN`W$LA=id,y y#yeu'"<$^tXKl* ΙY}FS66ӏ(AY0] ڠ1d{XqFD_O?X`V^F28:`/ 8fspӉuXfyarF!:nH =J-)2w;3; ^B4щ=bLB vP GqQ9J9^,$@e*˛^Cg>;Pп$W{G,a_eWs.5{*JW&piH9U_׮GoOwu'B7㠰`޳X @n߀bᚕՀ4w ʲ Bh=z9œ9Ockc/+'duDz 珶11 uS0/*FX?R ܂@m)!*ѭ*ɰŬ'CQ^&,q0&{o=wp\bolWY n>3GSOr%@di_MᐒTQv&ыo|(%-"hiA?l[nT`Ԉ8]X9+ҲKB:S(( HJ:.AZ԰~BBtڴ.d[%)o_뜲jj#: 2.'Z2‹ y ~eAA)c:4!.tC E{:=0ŵ<  `gl;sSKw0}E6# j#؞5hm ZR:} t~/)Ⱥj*,ZAdUŌْ&M_E-aŸ982e.疟 6Q/s"рWNOt ~]OF+@M9=k}?Ђ`̤I~="tFƴn`kdE:>`<0Q@^>k؈16/o+Oy6R 75/.FÕRG ޱ#;vƢgDUyR];/jH}d[*vd%mٱ ]﹯)LRTcyw"٨-@ W78t.֭0yT.SZLrnĄc[<ޞrr"<}vY{?/mGeE":zyUsdeff=t=SkjڔtFzYr1oJ+R>yߥ= w)!r=HZ1@0c*9&b8Qj wTƲ_]Kze[jwlHlHҥt`}՗{oo9I+IԵ7b+@r_r\ՏiZW&&O}Ed s4ckH-χ@.Gui~AVynzyN&O]rZwp 3[+٢'.6ȪA$*c< \5Xs%@IDAT;Rשg;K+_PQjy+G+(i)˟ NjZZ"cy&v؊tXE 4:O<<I} Bk }CsY=*'gQ{v3.Ld{,?AhO+,NbLH`hٚ4w㩙!FE:dBX8yܦ.c^*zo&#Kl>zu:O Ymڧ6p7Ò0!p6kSUˣF Ý?(ۙM@瀁L倗_xq:TL>n39EXa?M#H3 e"n%?ߓH =)SqޓdSUick1bZD^m d촋)i'k!"Rڭ;šI4]fBC~ ӯŦ+Dܬ|9G@Hcc)|t DE^4rɚ߶bH͑Ů^;Q+c@/wγH# VSX5ARr7>0yq~>I;qFj`GSma=F0aaqd^4whd- vXшщtǕE|eiW C!.+ 1gy Eo2ۙ !02uƮ%+xN2+v)+28rܕuZ?##!Ι~fuNOɊ}41!!{8˺J10# Nz6fc5p~q[9F8x;cdž`eWΤ\4^~-6+,48d E+Yw5M{MS0ZYrF =ɏPQoK'1pak`=v$^ʭqB>t2 qi4u@%}z"(Bf׈X{sHK>}'IT϶y7AG{n_J[=rugQQ #ɤBnfu%]tƌm-P;t8$bѣaeq}FjUՎu'E }Bྴ>Vch"KS%(6ctL&EG2aafO*6 4pEˑJnF>2i k .U{t?bT&mO>zEi%Ubh#Fs<$7.of3t,7?žFTHS3z'rP*rd^wxŧIGTڦb[W@'p21 i>(;Ϳ''˯pj:ry|IzW_60#97v"0jwl)Z]]\"ʍZQdh;[8h6)P[9[RBgJZZ9Z/zgX;gqdgaBG Sw|-e%ZGr,⦵wQݟ:,h:QI>b)zr_uJ9aLEMK~}&7\Qlhl,j6*H(oO9vš+6@+_5$P菥N'I+/q{&eWG֛9@+H|:g!;vѧUu)T=ȶzFg#E쯢lZgS]>[էcwdLa͊!|Wg,Ey}vMu/r*vewX_f{@,^| rC>;x ?WÏSWZtrŊ^#xV"I߯muۇomZ wF%m_ﺉPXq,HNCHYmh9N?u}v8z/`׼Q13[7WusT⿴;"Rz]-Ms-Fg%6oI;5fm~Sl@ѾW.'`jr՛'C7*ug`AThb7 T HvH]i,D˧}Hk;wkV/C5wLzыA/GOwkv#^wRe_G?} z}g=zi +on׼n;?24羷y I]4nو7ΦbQI]1zQ7mSO|Cc(;f݌{zaJIb5k!]=} GW^礰)W>\燎O>vbdcw5(" lOn$ˮqg0j>/sg>~ dէmݱ'ܛ'urzeMi"UZZSom["ѹvɏ~~+ߡ^ga$ߦ!q.u9?srG![mGe5zP_~&3]zdޅ'8ۂ 6!8=ifuؗW^7w)8Y+0(Qޝޖ[ޏ;1;Je+12Pr1=pb}&k/* O?Pً`jA:NvVc9DaZ#p3ȩ|y9!ccd@v?_z d9J2j@oV+8bږfQWp!Ω ¼T;2M%yVΜbŚ?v$s1f8|{ŏWhzQ}lB E B?2ӥzR2+Y %ݭ z@"8yZ%X~,Z /'tSDƐ܃qMc2pNîBzvw?C`Ɣ!`ȑt*ĎkOV`Rq8C#F2l*cf8G9g @Ρl|>LB5pwGޡcJ{‘tą5h<cIfc&#GKbu;qؽ}§8%824s5 U[.(KpE8&]pVgfCa z; S'pu Bf#3~9X7}/{k%sı' ^v('v+2v<ʊPDVJOR"[ #s>p/ ?טmFbSq-з5 Xy!f.X;qㄫ'Òuiy2ȴB!m.\xC_~byvB7D|ཷCpyh$;\m\b*?!(` %h1o:\8b2> X1wrn w=>Ǐ iwu$4/7_>ztƀZ,m?}Ы-B '(*0,_иf il͢ {Gʂo6z<*9"V8햾(BS2e &ߠs;m;pը=BȰkghmNih$w1x✶ۻwL/KYbvn][K S Ovx;A@Rx&wa/VBv Y #L9#5?.x0szI_~LVA}{vQR*Nc_ph@_} K*zи@c%<zAޞ %^*/Vu?n8㭝⫫qn:3R{c)֥o87ɞ/4Xͧm3I⯟$zΣ֯^>})f6{|^/}Vܖ*˲?"J^K&ҥ jȻgk PEĒao1Pʥy2eȽH$mN⌷߿3d,c}QwNGͯDʠc̦lV%QVk->+sRGX)m7*n'?Nu;' Ɔ>,FD j |ʤ5 oU03Hla t҆SI\ KBQvXb<Ӟ>'=8X-)0ݻ!ٗjZ e@WOHm3qҭ9tNKJ{mOw?XY?~5 տ +:bZn߁^t#jQjkWOzՓ)Y/F| 2h0t@'F N`Guܾ.MҒbf 50<z񓦴 J, G*XtJ꭛cԘ=kHH@Rq>LoM;s]^bUk{t)Ӹu,J`178i~Y VGS31}ˡV|cBu?1$3LnP%},7G)jk|bK ~8b˴릳Ao#TYW>WLl+AV5T20TzoL D s{5߿g)C0x3FRKDd%Jߛlh"Uh+ u][t_g[/sN͠Hyyg}U2Vry)3T'fU}gR>GyRnZiQ!< 9} ټ23K1CJJ& rш!xME `WUF"#}5, Uџ@}98Љ4t&3+LS"`k`XPK69dHuzn\\TށԽQ_M|O`u4kșjgalKng3yDɭsGFY#5޲ [E e|6("ڽK*3XWJkLv#zLtUщ]5vM K#m}׌sR;-s)¦6T-mDԗq[ڹ[[pߞ|Y_r`)WAK7h8Mx8QRm0tx˃Y}FQu1vl%eFJ.gaoK[ᛴ6nv0nD=)ڶhӾ ݐ- #2~n.LRTVDUK|)G-Ԟg)W э.gu;;z YOZ Taà9՘~56=mS{DԖcvF5z;$*ӳH 2r\R2%Z FDM\LHu c5 2 rq忢QcHF(ƥ 鯥Ց4qK.RPWZhXZ@DUHz;N+TlyxF_uUTOw!Yȳj䘈Jվ5(';iWZ}LӴޗ|7y9-).BN'r-;B-뤫'`?cc\,q1l۱ ;wXdS/n޼s&R7.w?t rkb-a??לWZ- s^ z򝙶v>SNPmֱ[qhtz٭)鴝ծ}A;"ZwʟۭSXsim~}wKou0, rSӟg^}7O zu+vo,i\kI&beg͘ WjaLWO3҈n53Й W_cC7g^"6Fz"VaTLvᅤH&?c,3?篾+]fϽ|yќfBnwƌZGy伧''&hiB4/d=r88)V,bi_Kۣao." q(-I"B(J%G-+#?CsM}@(CbcwbWL ۹#A;kT&\l,CvJL掊Lϻ\Ar^rɱHVE3'>[k4WsCcj͠u|P^WЧQ۪LjUW%*֒F+q5Ri;VeiUζҨcr掠kZiM_+W%zRZ9O3{91OEi휋+%\ lmxe`GU x&%l3OcagUi\x`!aڤ c|mkeܯ[c1Faie+Wb-w_0<(?̎!|V[cɢ粳3ݷƔf[ӻ/9YHMi4c$$f#jގz-11nͯ.CS;Eƈrz'9[ÅqP+r RZƣe  /;X]z!޺yޜhJ/ՙGu rtJ[{`q 膹?j0܃sާ;VE=={7Xӓל^L?z#qe?n,7W/[GV"f tD3 &kNgۺwu3[C>1vp}k;o%VÛ`;B扝H+o'ˊu~Gwpqs<{v`Oi͘ס߫g)R k_z0z9weDD_2`Ͼp5rPO@}UӉ%02c?CYKSK)B ,G5ueZ(M*۪mӷ}ZG>LK究/'\xL2MfQ!iȃ <2hү@U_|(-vz?$&ͷ܎z&bذHrf0C L*4owߣrd&lPAE/iBa WדC2]BPEkK?uK{UǖO]WYn窒 6ŲN,;ZWKC6%)'Pof&n-ONO cX{c-ZҡsР'bLZ!֖p sϾHk,Rqwթ*żJRju֞:.kyICC39&HbY^kpǍ ޒȵe1Z SBI'ߵYF]KG%@'R.oL}kg(R[3߭ԭ[HvQ,[)CK"Ir 3a iYhF+"9t|GЊ)$;"ߓ94҆uk FcԦCͳ8H nگWӎV6|’6Z"eRRQST*Pְ#*"Z+t2`PjZQ7#ϵ}+<9jHe.sq̓϶X5Lf:=;KֶX 5p)moTTT G/;tG>x7h9L2L0Yr~ݷpxј=1zmYvh^2 _ NWҲOLTe eSOΆyctG5ǒ65/V97\A@S"Y7ӓ).qCQQ#Eǵ-YMC[_#6ރ]"rp'9NQGp.,@ KCu*ƀfYk{R;odd!8ڕc`_'4L:c,Ha8Nf'O˻dOtmQTX:cW!T(:AѡHM/hX\qVs}J{g=lkil!o)휴YđDgz1UE#a3zlM7[\Z"gwyΟ/݅C@y?~wV/ g'9r/6Do\Xv%EiCOnzkvN.CN:Qc0x8@wE/O'-]t ~Ea!K2>%dm *z :IsE"¡ӈʀ (" )ěŠ Lf`'R6 &tKe5 <_q"- Ԓ2Pf:uc-.EZ{u+\x?ZL Wb / f͚uNNF`I@Vx:ewuw6 ~\$}E*oYs8k0)j.DGu{_M`tdH s-]3Xk=Xq/gLF`|95`bq|?' @ֲ<;;,V|zi-?#'31}v*]˳ ɻ%tM15=_P 5lmaN+ɶD4-ԝ<or*6SuU9=xFX_I-Sn"ޫxh1;9 $+r9I0c_~~9=:4mQ(L@]ߵ=fnVW1[h|}Pձ~5[O6SB{qk{l aܼ\:ߺ RC8 )tWQ+uG|-_~))g~:41L3J{~dcP63E )ߡQ& cuਡD۷<9(zAEQ)6$Qt`](8c }-i}ƌfUhlE@gZ{bZ!:)iŘ'Z "m،C wɯ|߮A*j&I9 k[ضy ^|9lں^c4rRzNoVҋP୮o% 4NoҪ&^iă䑒t]"zû5W1oNM/`YwDăB֗^xETppeHY.fG|ݻ ̤'L~*kdz.l,O4qR{ΩXDWMs2`[~1_4$#F 7݄b>7WK t3*0~.T%=vǬ_P:먳GsνǢO3šQ|+컰Xe6LFj]JyCt%-͹d̝ߊQsc?q1K'/Ⱥ#TO,wzUBح>wGAVy+һpLc뀭Q%`7V_^͸\WDyh;t?{k E? ɤ;M'#k+"בյQr^+HIZ}dH~qc&7kIz8@s~PVDYQ ~.݊[ 9b"P>-ό#9tPt&OW#.Olu8x*k6+ºl}{q)'J pB9V)Gx8}1pmr:ꫵ d]j1kWc4q>>ޓCe1j]b"k2NeJn.B;#;% ֌њUX9CW-ޫ!w( Gcc=|O$ㄽ3,FGpvxpq [~>M DR-Ǒrg忬'l U\vZr[]kb2lߏHY=z^;ͪ(9H82\ y- -h7P{FXh7z:R3" uhϐ+u($f *2xzc=H]iPQNZZk,=@Y k`K-^-%\üG I~vqLwNЮx=pɃ##o{K 5w76lg u <-&߀?LF/ Wp(@:t@IDAT6dHq9pнȃ -яn,$pZiKQ^COA6}c[4..0qb? GCwK>L:u2mGuǑI4=ss Hmi"}'rz"Of<cρ#8ZtW]7koOMeChHQHS{IQZ>.^^1e}-e#oq65jɖ 를?dɩLj{3.7 ެ ^-A퐓1ki /ZE7_a ]"}:v@oX%Ƨx7c,7Lzli3MșNSV^0r\:S zݯ[,jHuM\EVG<@*|> >ضҷq=N[?В#x=y=cζ}LdS[]>.@ICl^[a.(cL2,];錢RњJD7XSx<&S߾F+f\`I(קCFm :T0`uy~<{!`Lߔ ~R]twRΓAknIg0pI#S퀬RcuOk=Đ4=9j^?gŝX% 3R7Gswĕ" 5 o &3oAMVd%(G  ^/S,ںOy2ViV,U0]ktP|ƴ2Ɖ~Kߜq$J[$֋P4{DIվ WP}{O[[LXMJHfd YBI=$9t$=~tW3n˓ aǎ AXXD|xh˼_>"mgv]@)ks-A|,Ό}-o?[I Gk<&u:-DE'}-'o2iLT}عs; [׊zUvz3Fϙ23^Xڅ/Lľ72ΉK* 90) hՏ^-ObEΕ>QРCi^ӫfo$@!֖XX kqF8joԹ 0z@ȽKG/ßn5l?;.d?*{2'U"1ūS%CwsA xZeeGNkJQ xi+5cfuA3TM6Vs+kJyy@|W)U^]K?YD$,-B *lͧlhT{{Gu'kc&-<¶{^mRGwbڭ .,H6FMw_=rc9-qEG"Ƶڟ Qmz=FKY9fK{-]|փYaOTTYHyloE~H)\hi;/Gs^%yF/22|^SVڴlr[:P kr},힌zs:,z֤g'ŨSX/9ꧥ˼Y[$o%-c֘ٔN@ׯmUF}Z>F)2z+O/4nR=/9GYq!"u ze-:T&FAֻ#?|mn}qt/CYf:р&2&'+26#qg]~'-6d{ tšS=]HjJIPnciGlD(Ի^!r<}Ħ3?kA([b7a=S1! U6? {19KG y%p֛wܜ@K?o8 Bx 'q Vڸ#ca腱"sbO#Wge#aaaCO.58Љ*WEy5HIA'zFcr \]]?1$ |RLKOp=ux;<+ri.)k;;4fwhQ&b /5RL΃Sl,PRHORt0gbB]±Tql 6"'4{ne2zw.^8v`٣܋k=Ry͙7L`WgY# FhAZ Axn[QD:`Ɗ6u3o0|>suU{4zvEU\]epmrq#%|QE8"1Gb )[\iNo>zap5]=M(gWIlӹBu\\h?F`-*ȃ5$ R{ ^~-}Z3!;!);,R^.%HNlj҃+as-!(z`+lVA3AvA>ֶ֮c:E*\M+#-uYzEnUKWӶL/+܍/YÄ? ԡ;D`ѱgU5s{a;E`m#a㯯hޥ#r .6b"/*\R96Z{O{S8 s%r#㮗j1xǪ< {*>DX^eCul,/ KrLI|ͷط? r7b[\2\IUpΈ8a|`>PVy;yH]=pUpa{noڄp,ib6p 'A=c̼ }ޚ8k;p3 T/9!;7,ä;_(Z޾(zܴRˡQs/qp^*?:kHqkPS-a#T9rKŇYEyKXFAc/H P{6^i{>_.mU8_n[h o>X:\S_chװ5ouSenOUyJʶm}E򔳎rF9)W:[xiNFOrx)VʗzSrcl~yĚjA-q]wJ;fd]_}p]n38}b|'ض{} n2z_OG0_`EH9URmj0wrE~Cf:͞hվмz=/ /_n}k5[K~1!c` Vug$)JV;u(FdZt9H9Tx;t;t-B΢[aሹb/0Eypl8@|Ğx>&/LJ?=aoVMB#q(.ɧ n/˼ÓAqf؝pܟpwKk4`ḱ)g{)d$^"~©ע|UFVa$&#-1ߣe54nWn[-(L/ Qלz6hN&ahvO?e_cx }<׷1ס}ȣJ?T@ߌۧ݊x jJ6f!˜>BGu- pJL#R6 <"I.՘6(7T)[V-Z#`٦'E#j!@@B:KS@D3*%6&E5)-\ @Bi1GɚSNnZPѯYz3#e+GʒثV'C!khJ>W۩cA. .~08;3:2ϔi1yY0f35,7_ ^"cu .#$11vB/`݅%+끈Z}_YFS○*/ 6=RZ=kV' ]Jb<"h7[4vW*TK}#"5u3^?z6U;e:9''4(WaҲ՜U 2]5d.)B%1G]ӷO'BJ^0Եv<Q?uI:˷~puv6 }~2jl%m4ԉD3U*S$Nᬒh]Tmt#eĔS*V}U3=gnƻp(#$SR/\>IKi4>jktv𠦲`}v V:9f5ic<ҫ Mnyn[# -c.=X=},NBG+8k W3JiZݻc`K-|VhLd` F1ogM/wSBU\P9O fw3#-[wҌ6=@{M 0M"]kv%b+Pr.Vmb[`к\{f`A-| 99?(/gg4܍*|qRXؽܗ6Qp>^ؼ+ڴBiy!5ߘcH0Ycɺ9oi9 GQ#Gt$'r.پ>mM$"9 .[s0k#S#I`nFIf n썦8[xRD8%'r$LEwnOHOC\iܟFDCq?fJa2>ş¯A~e%s5-ⲭԬRʁH%B:%[?-~ ȡt% ƒ}b15Cg {lέ9" `!\;pۈe~d yo>k 46ްo"O`p>vKdݸw%tǂaM蓏ʉEq \3|FSCĒO-9i̸y=|ť>nBhhxRYw.xI*xLRTME Tq1ɤt ͳ?e##c $7z@4Q֗!p#Ad!ݗiC|iFss{ 4IDSqw}47^SksRpU+;M3nE QsY鏙7n mz)mͥwq:ae(QqO/$"$=s16ר+NңV_G\}M%O)5gp,|%CH=8xSno,zK}/H6ӗu *}>Q#r;<-q',Me Od"5u+y_gvnOszXuܽg'KikO/$\|]"c{+"^ywͼ]IXކ%TfNS}6̍،^m6}֦B\n4ǡ1i>LAcaWMV)ժ4Ԇri=+j?z)YFB))~H¬-kᆪǟ%qKb4۰NnJ^WU)j]v4w=K2R{Ec2'8?5SG!H.o?P̘:d,ΕvJ]5S/oEV-Nժ`XOLYI *}ֈjy&NayKjBhȪG"eK؊9Hկ'I֩-q#yv׏jwCYP_\)O0Czi}V&~YlKTLlHrt[5K/?u_9V''KDQ@Mzv Ţx9&쩢51\"~<6)x"LŠcg?ר,V&>pr_e~Xٶ3F/2rPwǎ> .6pomވ_Am}.Z@(撐Iۦfv/.Erj\)յS(»| //O'R>z1nAԵ-NODӗT\F6D!.>7hn]T}HD7~,h7^*fyq29{^ (}|OQM-atg( }l%[!= N%#~$^O W1Ӱb~y9{Q۟?QS~A{&k;0 2YS6*F#Es;ERRv|ϰ.CA#d tza#Pt<~ZѪC{V?֣6V@\8xcD&UžilHo:bj :/űa~ UE+H!mڻ#pؔ$k%!n3D^>R\߽%~_ۜiӇyiׁsG_dQ"7084/7>*[6=ܫtEлb]q%^S6+د/ē:*|@)߅+}r9{C~<%5u`aHHL͝XTՎR%ex[Wv]Pr:׵ю܍sǜ߃Dַco !]"N&\\˻thWVH+q̘(=r^O+߿j~] b'U!PtY%&&Yr_l=s6@QHX?/]m @KY8-c?/B[Y~2f3' Gaٲ GENB;慿=>r{e%KRE>i/1 zړKΆf _~WsB!Q:\kA'mTx ]46OUyW:y7ջZOPo"G){1?ѳ2wv%n܊=`Ǯm6hdu_*OPƇo''=㧎#׫%_۷7fc-mu-z'c&N>Ccc%;Lb ղH>ӷR.nc"yc8$EaKWKZm1=C>q՛?z:8 Βų4Jܿ2ԡRh䟂~Q ǡCpGa'59\ǖ̞Zrp/,|Yߤ]7:b\읟Yszq`|wZn3фowsD̼bL /3?.‹o6ҝë}5ꃻ)uT_*<7]R%}_i?ũXms߼j֊!flyݥOA &~M|SmK|}e%p0qMH?c!ðm$l{NlYj_iyϩ{p@}ð1ho9/~8)y7GD6{%ZH1e%vSuo. 5 o9-{ DUq!ms;cQQڬC>{ƕtK3ѾP<5GТ>QE~K73T%V7!8K[$_*[;"9kW"QÒ*zzu-Ɵ+W-4-Ψa.V۷l[J)})e^m).r?ڵCuV,\qX ikg3`h6=~nSz2.˥:ټ3)zTM'Dt ]vƈP7 YMu۷D@pJDXmvZZh6شy 6{y;􀽅FPn9G07)c?,E߫>cwhƏfGqaŻa(H=vT+h܃iЖ}ifcG۷h =FVmܒ5 &8H(\JmEÉڮrN)|fF)ˈbxiWnTke(͠PZٻnA&*)^ݰq{"qt6ҎAډt1vlߚ4C׬{Y -θca_3_|Je>T/4*7%ؕ$l*mdjNL~ƞnҗmmv*^ڥMD>% ړ^'R8&ypt M{wtDEjۣL޿оpl9C; ;t[+T梂p;[{S*>n(9XA J=RńXsݐCRn>fWskٽSM-EBG]U4/ŷGmH1_z UU4}J4{J240O5KS+P|.<7Jʩϥx#g_ Z^f2R?=FC{`WcĨ1Z,澃զgo͵`-dK={Ѻn`z_F f3\1DdiW ۛ-/S6Pabf]Y5gk1mز /l$9:O7[kA^Bں5N0 q2kZT=8m ̥)u¥par(um1mA͚XˆWM/,/وT3P.~uB;Z[bco. JP@{/j S(0OGW;'j Cv6SC\ Φrۑ1J9Ի?\'M ҫuغK<XpsQ^SfRm۷p</c'uk4 e)NI.>ok|4i=zϷJrY9KhT!1rX̼nSSSx9K׫:W8[^ySړJWٰPO>*8"gc_{4/#x|v 5p^DmhN8Җ0X~I)<z7Y> ͽ/+T~Yi/7Q=pqt"T J)x28T[5eQ -G4e2)`kkdS±5YWoX t_RoѮC:Ғ ;ɧܘmXWQEQE^.95k0;w;3IyKGǮC?_bU Mb/H>$n^|j} Շz%\"JeVN9haKb}<X8ojAܑ8G-jl|avd/H$|m6SyJ|-3ǡjc yeg.mi箁y7EAaZ\]7 |9(kF4ւEiɷ/ig.qb8\,"sݏOIw?Q h!ѣ m ^&CjkR+>pRS3+qAui2@~Ix <&Gb᜗6|*>}aP6'2ڦ"pv{p&8l()8b(%ܯiJDڒJēXh脔asl?NP^z |7!-`?Em߲5vYpv./A&mSpj; -vM(ݗxx?]: 6lBs(n!&4'qLI:"9O4o,\)L݇Z{Tϼcz#8$QL AweZM i¢BLYkڢF:%sL *(($s7bSi4NfjJ JK% gN# 殤Ŕ8Ug t:DJ+lgN=c'`AHFI3+85}Y_8>S@,v gaJΞ[Kؑx)8G{ޝ1a8p Q'J΋^}/ā;;f'':+w6[KSzC8aM]%=gNN`Z M\urMQ TYDlV{Sה*o@. _~3hMҟĉS@U+[5#FХkKۓ yZ-&4}x 1z?D^ M2'Ϩ!pPeJ;}{$ ^BȽIS 8;Oq|Q7\u$no<RRih.Pi_}A cNW. 'AyOj[SEn+LMEbU֝s5NxJ_ >5#$ I -<.o 5 R0c\CpCʱP֘z qHb0wAU!&6^C)D77f;p.O~*N=vDvl_Tj!07's [(F=jH`U\#'uҥQHd e'iKP,{5 ]X9]0G%kcڈ5|<1vg|?l_\`ԥ۶bLS&܂$d! CrB`N y梨sQ] p+oYǤ-!R-ZS¦=,*~7 "N.WC6Dq!<ԌZg[spuU3d,U=U#{Zß/pH܅mLNmNE7 ~Y^y gRgw /պIR׫g_LiJm6>>#nXB۱ú.x]~BdEv"Lak=8foW`mje8C@%26u]iu' 9ٷХJ8,㮂x.7%J;<`^|]KBk/z`^|LyǕ6B\VY+G~r,L IZP::/pC#HCF4YXU_jPM'x&9$؞#rQO`(^B:ZUӢ[Uy!D9)g>?3\=1Dʨb\ũrRX.N !B/}*"HZ=ӘO8r=KYd[5rؿ_x>RهwdW砾T8=XQK9c롪C78MS)}Fs8p B^URmג~'/gx 'CBS~Z@#j}mD H?)<|>>+_ͥwB {*zj7 $&O\GQOc.gO<"U?Luף֫4GZF'RO쇸dv{XUx(*3KzWOu?I#5^Tpe/]OQD 9o]~spsqx884miv'$$^aq5%C[Ľ(h\2R՝mhO z $ ~e69[Vԭ[ҭk^ k`z{[+dRn$8S_EΩC<8=)1wȬy %&2`{ v$$sEsf\zª_/mM;}'p` lMQDbsGs|F!LŸg"e[rFx`Um7DE4il dG3,jV A8piBc~>)\Iҹ ι/H$Fv$rNbt)2\z3LIX=s͐JIjtauSDZy*ر_-j=MIytvȤ֓ڀĸ!=Տk>0GQv!{S#Zjglы唝&VќX KB&~v@6ptqD#6Pyu%4>fEnD>h<Ǐ"A9f̘ Ʀn3wޏxP-r7ƃ,\{,wN⡇SK³b~?λAcXWYQ d.,ZاNbmQ"֑i$2+w5ʊRb6=85ԍ pKўquB`vڿHnI5Е6\n1\nvd_-+Wy. /UW!hk΁R0W/}KNX bX:sr3QBTkJy5}Vڀ-3K)0Bcp#]sO^T֥L6C`i[$G{t6w/kQ{3[hd.{hH~RK]{M:pPga?˗R/~?@T@I֧o-i}mʼncxxt!uqu|$I*b6;V$M62Rc=e㢅XȮE3_T9Gcc2~nd1EZd$,1'NS)4wIi:\#nqcnATuH丵G½`I֯coυCff Ϥl b&í? 8s]Iֻ=nڊshN⥣wHN-T\kdҸ!p:S6L^Jxsa(lgmM2H\enB6GƢ #\0/)oHT4avP#QIi֭;x`b׌6UW~ &HN/ˋƏ v=ҟp&= Aطw6m{ŽDh_7XU ҥU&8J"pd)%_G$u)NEC,%?xzhxR4E> 1T[VYV}ҩϸW [q/m>gτWDHC)9-ި0Kעgy);u-7>6Wx-_y5$$U'̓2c'#>j7|Y Oj'Spa~cM6Cg%wo )%2={ "'3HЏ9)hKvUE )mݣk;DY=U_x;XRƵE `c1_'$LT{xsO΄Z}nia҇%S:[H3h?ִY2#mZ8ambϞ]hѱ /gS!n>$DF%%gO/gsD+O>{M1f| }&T_>f ޛ uθkWź(r'FL9NCam4XR:wo̚u?"e8<(1eUo_:I`q'Nܝ(t}M*eX:Zw&MN67I0[6sA |oD:Dڼ"Gɪ QLT Iu#]Ou ]pǬYܔgI EY&n>'ERBi o/?Wj6Y#T5) .g$^BxVM!FNJԴJ.Nux'ߛ8htK~ȭL9UZ|Z8DOW#ôOO3$:bK̤„7'\JRTQˀ])N$_8oʵ> u"HH:몳>5n[}zut; g2oZ")h:T|<HgaZ@h\+w/>Jkolgl9q]Wz~{hƹ&"}!\"$Q CM!lXM֢]Zz0uÎ~n_$xtyjXM?4H*.>&Nx]{WCi]9=|/U7Џn~ANppxq`%^]efCJ_hy*u9Ն~H Ҹ$4c0MnDy9x]S)xqX&PbǧSj! ]zԑ[eIT?׻wL8Y}p*P`]֯H\4gk8 U6uPQaTOʪ IdkYIh|Խ1.mT?cH[vy(JYs4yRGdZsFC}SUOxFfIoTX^TxFߌKj꫋q9Hqf6Wpf"qnA%%K}%o5p⢍W[c!y4h}y4HDյƷ_h2o8 nD} _Zl/ҏv$Y5jc$6$R a_M׎-!gݫ5-Ծa> [ѡ7>m܌ؽ6D;JӵeLE g]6zWVLKGRMt"Fވ;̋YVQӢ595ʎ>$ey)J !U F{ԓSciu(J+z%ѢJSbiTriKfؿgX3LL8w#׌ZaOOȘ'R痯W`+mO}]XO'߸xvXEcBGh55)]IvĢv;E1}S6saѱuK AZ9wܷ;\(̹OiWTƎαk m|<\i x7BC(YY{nd ۱ BȻ6cpsb[,!fG]GTgnF ;G'Gb$yRb̴{]_ADe)DErXM}n&䁯 n [PSO= %)^oM;v+(S⮙É"7 TDd˲W?Kl:"%ylM L{.pEsT iv U} S9K;I9p}8 |SkAũz2 _ E+e~$ԡS+' WO8Sa*z8|v7uZtnA>T%3"fnA>GI"(7)nlu i 1عTyشiVG@x~x@_d U7LZO)1\WUDU\WCпG[ йcϦR':&R{> vCi₲ګW aؿFq+}G%`_ây ؼ9O>© K̡˅)r |I5w9%N]qǔ7OշI̳(KV_liwWڣw .Θd KܺN+-%=-N}:=fnd­o/P7w4+{rK~^As2O,v(ӗplh~7 kCI=,2(9\oQE`^.)q$>S&iw*? 9^[E>_B`}G(ڻ V=/*Woi[TO Xq״i$^gkfmΑdջϗtjWk[s*ֶᜑo '**G5| VuRO}?5Z{{j*KBZNҨ5>j|,3;el&q4%sace*'_II}{ A22sͨ6 ~TwkEICǎfK[0$<_~mN?Äh̨jymq AeU5vHF̩4⃣7Ca/?wcĐuO:eb`dM5%vY*3t tU58ycoAJl*:NW^/?-/o{&V^~Cqp,a!:Ρʷ%n<o>$仡G݂l%Fݍ~?-/}b ]<-‚ λRi,D@zb,gcHׯۋXJNgRk@ۛs.t#8p8i'v"'qLv08W48(ɧ;u|d;7_jsNI8?EѴ@q1ڮF=OV32Q(iB1;71rd)~hfjko9Q͘16KEg4x ?U+IЪvd]$Xel|R_Y^֧!yK{ő1BUNWOXp/yoLZqq1yiQx.Nu Uqq'X+/8C[Dv)VSR`2xyv'?z GMJ7wjm-Q|.];w@19~zq$_B\ꪻզ\U$@GF]<0W.rE W`Wc8s76ͥ[—_k~ЮVpw/׾ y9VI'sYֳW \zг{oSAnQUYZzx8Z_C-2{O ^o̼/|Z7U0$8U.Ydim ;d^B7c 8h̝NOBF)M(txB"\brTzQ}EFeG9/f*\= lS*hsI'+/KlljC\Re$%#cp N E4xjcpW"lx\ʳ _/q ˨Gړ?* '>}}u=k7 Jq5CS+db'UnJGPE$X'NT!\PelhUׁV|_.}PȖ* u}PjFMחR9(]3Q!:q>fKZ=TpN?ko9O"֬<ջp!YC8ɳ6LZUS$e+Q}$U);$F(çUYqԷԪL5N:{u=!?TW(flM4k䎃xE4(¼x١π~}jK="@ZBꪯ;ƌUw_m;2+ 6m:PU#|)l^UNNG#8 a]˟ Q/'e&ᩮX?'{bU 6QAXXhM\q=ނ6S;_DɄMS?#%@nH)ldd%lI2wŔq#h: 6rG_T0wȡ~V,G֮8u AC5g-"[baֶ=P3IM{r|XEG2OvDvȌ?Jژ-JA-sk[i㓐K;bՆ}žX 킰6A?jL-8x{b!HN;DƎ +S+|" RGA(!q{%ޚˌ1h(D:1~tj㋤ͦBxy$zQT>%b耞p6ah i ~MHB6`UѽGۢg`"v_/#Fb)֧Ь$=x?wUMMgTHH!$$"M);^Q‡"{o RHϜro {ٳ;;gwvvggfj`f+4ڬGhn½;wAQ"(,|p臍عc?IQbM8B} cm)4m\,AI56&;[X㑛HMc8^#@uHK,GMM SȎV6*?#4 h] bm@FSI^ӂ *#49)~[8ۋa]{azS"&ObY$R:CX$<0;)]z >"'|i!38Zi4.7⎻:T7"Ns+wO7Y{gb4驜r}u0?@#Z4NMVUƾ}0lذvkle+u<kUlkxq:wk*O?MxsBr}sg͹. ^0 3L`GnA6yd6zw"ܾ.oA&eFF6 펴#*PD l1Q.\y!:+jQ8!-ڄO3~Q+iGƙCGJ ʝՑ,~71U^Jŵ{d!7ml ֡ЛoA,YbV=|r=Cz!,1ĜԢ" ||2@ĝ9O(ĦdkNhɤ~C- f|ThnϤ) ߆ 1BMӊgnoqRۈ>+`{UC]u0D<=ʂ&yll XꮢƉdz,u#9t)G'3}Y]}DG7 `q&u΂V&:R'o,(as7wQmWJ~38c M?FYQj7yYj|௫Aߏ#o%y[3y<|O^?:b܈xü+ uB\Gt6"oWPNqz]X g93r#v6\'^{p3iT`7_['*%z.?t-/K-i䬃'Tִ X~. X݁t#O'etN=KWh3ɒ8rɝ֮__ vxwBD̛8]/ RhTu kZ9&t:>9iS۶t 1L9 /|rj ݵ7;o<c ;s }Ԟu$GqY5\]+zq@mGeu5JN x'~ohX⁷磪Q&d޽fѭl#3=H!\]P`';G!L#{Y`5|~WpwwP`G}\*քB􀐁кJ{p9 b}n ̋|%w*E^Pu>| sVc9FYUU%pWs=@i*y}s_w"PwpBr+OW^ze>{y_ZC,|I_vU(,<9v--0FbA}ow+Qdiݪz!"[gSB=y[KM̨,,4W_Be]|OI|{9|qDLl3 C)V;[ xWPrNR8qL7j4 W3, fueI/'²`ݖqzau?&E-Fܱ]hp ӵiK(ʛދLa Eކ p qg1܅d7v8Vn܍t1^44_~ "B0z< W]mL$CK϶jnK~&]h1>LgVו{NbCV!'i{K}5(C[K!=K?t73G/G^r:~J1})޻^3ZrP%d^g.Gjq w 5n0(6:Z(ӊ 6mtqGRD7epz1T!3KҾ oHr٪E9mQVq{l>(iSpL/77y+p NBVUA.Hk0ҟȳ91E>:RA7p 61]/B>9eV-,d6A"Of+`Jɚ| Qd d>Ȕ 9?^+.) YE1 螘{>sCR?Ue2|1M&3,ݬv`y3v6ul6LpCq#O"?.V0wª}=`?xLj-2B~)7S:ixpW G]6ΝAI.|"sɶ+?!dt,aebQf92Eڍu)l)h c/WK:|w%7$\J:!D<| ԭAKZ W=ˤߦisr\:l03B;-,c|'KE -'+Q|P]l'+(,*ü9Ө.dR;ٓGQRYhe5+Yz%F> XẒ}4 JjpjPKRt !mAz`QX|9Hkp:~u=A[>̭á#$ ;FFڝزg토^06C|B"mp09uF(#M-;z,} $ֹBDK%݆b'L/OV<-,[N!^Nme05p;V f} [l믽E>ba``E_|OlҶ<$X q;K cLaAJ+eeEeUuaAfϟ/'ak'7?jD %gPEm۟,\L#cX~euշ$KbPE$\h{74ar9op= I0}IdR.#%9646FA ',9$ n݌_~GSF"kEK @1tP:p + YuH|iۑ`c߀09L `֘Xi y8$ϝ#Bs!l 7>aF'~RThXoQ:ճ C Y[L2WIXl;?@WT7`d L3ɂW [s>OϬ^"@n;u0,0ԙ,zfи=U1$o#AMr Lg"% fT9=̫0&$f#9S)`fA$fZ,ग8` &$-V!5Qƃߩ=#iH&Zxe6B"8_B ?އb1dpQZ#0djFXf8HFBT=1O̿zx%[nER~?yxťD#w"|m({+Nq=bه*SZ/ g򷝂ɓqmW^]nj7w@U?,L;)h 󢟃g|ez"oqP@8 b2rXrxjWGW,`8QO7y[ӜΙPC~Ir]E;v e_&wrr^')Ͻ`է+ѯOku> X9щd5N{ 11m{wnq߃b^JA>/ǃSyNDX(:#lOT؊pcd&Lؽ=Z+|X 4Cs4;4fhp9q}_uq b!1J$X4qPeɤςXIkD 7vݭU^>WE}TȕXq4F`ǂ<-3G ZU5v)<}C)6(NCjRzzeO ^7?hSimJ&D<Ѹ:*o"\<Z%⮧?p\"E xVl=`V"9MUo,ʁq08qZR˕-MD uhj(BBj% SOGȁ$3$HO ``ioT4h59~xv.FMm GҦ%j3Ͻ} )} E= <L[݄#{O(ҴtAcN"Ap>mI֌ȗc$=ge!%#ij6>-rJ9$H(v$tb=B`p_O2뇣۱n+Z\O<c5dqa<--dS~)4=餐 / 8|/|/%୅$m;ub٘LYT1`Nn!Xгf,3h̬IGA +%$!bϭE`9 OeɟUʽ d[.l A9|bPfjlUm$/yX+oK 'y94G)ƯW Y;ӝ7O=z]3'a<}^F$dqU~uKOA X] Lzb{y޵ k7o{-=Ez+WX=ig"VސV]n2܊)+CW6Ҙp*-'1gJ];y#$ Hlڼ]z>w#=W_EIr,F]d~=d$ې^z:OZr 4ʪ$Lij/DVv;Hq`Yy(Ci^\%󲩘zt$+`OByc!$$vljˆ,LZn=Μ8+uYH"ai EMb/$`{H tbϜ>.aGG{ y3SSgO#sfF:h8 iTFV93w6:@|O) t,y"OŢIXmdn&U`cRbh0[hVU KPX^A0 ѧΥt ÇQ\s[gسҽdΥ1ID>{ 2198|6nہrw4^"<ѢIW^~---k{pYIyâ%iU$~y2m} }}?Ó//ZaH US8>֭ 58w.wbULfg 3;'nj xEQێ6 YSΨ3p()e|*04bN>^OV mILzA[9hiROe_c1'_dz5lg"L'ya&rÖIDtȿ.VCӔ^3-Ϡ rq Yڏi|:3w6Ӫ_r%4;8#T0##d}7p),|DA*9OaDl;_['qc'\oc 9565u?,/N #ԁa.(y'm{͛C9>Ay&qmIΝMD"tu!|l؝'fhɸYm91SGDeXrZe,*y.L_Œ(Cb OO\9XPĻ\ EW4\|hʢȩ{o>f԰#w S8FoDC~ aOjt ƆصgHX ?N(G/V}1vo?CdfÓN%&⡹3Ѽ"qrS\ze!̽&MS{?Ό#s) T`7|d#ػ(V7:8S_]w"R2[(;FTwC$LX$1 O_ xiN]׽u evn޵Ӑ hFeS^-J[4O'\8'|]x‚jP s*‹#_3O<-(_;N䓏_~HrNkD6D+|îtEԻ"Ϳכc@!ߚɅOQS}ͿgE:um(]"H'ʇFTGtl E俞uٕI+nJ=e3REbk0i>Df{0qX"ٷ }(HKBi4k264;F4y邜.遺Y=Ñ۟; Бs<+L]D0Ԏ O>>f#sޫ}46݇HP-]<rǎ0nDv#b1u@dQYw[sr1{ikrwgGM[jT-oUyb2}{:֊z0tC,̴5QE.so7زf #k{GH4BB#wWaDad6[vv5)Mp1T@Duȯn&<}LD,Md=l`o&2=YCXc<+8{={ D0rǖ-ɚ@o]p=vn 2WzNH)b׮7 FFH~ Oι 7E*]M My\QMJГF5=u)>8Sj*MǀSp:@|r8GsCI/9% L|7*RNM' u Mϭ5 Q^o‚RGCUaC2PI:b3H𩧏x QG#H{JE)pwIMU&z,2{gA90n@}LH:d@nmR -tPEV@a {[{(Q^C#2 XO VmPiAEw1]ЕtF2}`L/~'uH/r=X$ ;{Ν1/2h Z(x2"[Wc9'JƋO-c3ipM4z*޻Ttj='~)Ch2xTmm/I֫tKtJ$΃)}B$X]|'Xh)̭m1hpDCЃ0"] sZ10^x/ )B×3*ju;Z n&׽a3]zo7vK5Y:ij2lnֶP}o4\*zR]=}"Va e2g+˿@31pZ<qcַ;3gͤ59I(;ȗ3s "frtiSIY((!siks'ڃs, .ع(.S70yLhϿ?W@!fĤER6.z-ۈ9YHÑ WۿtLڢ#QURL4zH!MZb.|XzE<_lwJf'7:X<@S ϓ;'C!=\1}O!*?imzn˘AO 7Hy,NWT>01XE}׎xy:q/en?bN`X9$JI~.rYpniU-梁>"pyXw"AQufӫW.*^?EQ r J?8~ێDEzH*ڜ[];؍H1/,7uAi5V3?D`Ocm;RL!QN!nz+K3s rȗdc&2˚HckrSOƗo{LJc5OaE4VSEdbHd~(-} 2YL^I{=8I pq2 ]\7-2CKڍh! ب]@·̇5W#F>,oJ Xslv *un79=s'WW) ;C, d s SK{ڬǕvs,ta.+2 u$ &:s||r \Z&KkN?FUTD,9zJmڸ '=Ss8:^I{|ūd>h '݃_L'2"C&!y?3|D:и1'$!GC!?xnΜCxtVKn͈ OD 7dm(' tyOܧ0Csg2"wy-Ŧʺ,`pLgWQL$fb/k̦!~}K%S0?k(_[bX ߋZY#tX*KK#Sfhh!$"kW%tD/wWhuzk>G<ԔtBbϝ&%CȧAButD:n?6G.1?(NI;'g'x0o[^# $njfP;'$}\MB}zЈ| ~(?+fbZ,"FqtHN-񍥇g,ȿ@UmšUD޼F}U/ƚ,aQ/s㡻n=.}_񚹩I=&*,-U&=ibg xfʛUiѦm?:sBvX-GpK1֘.'iqLw8a@S{U܎GHsEucoF t4wXUǟ 2NLW:ZvWrm SFڰl7jZ!}7mތ&l/cYžUoؼW:^߂mc %&8Ybh]Q [$mr`A+aF)nlJ*8Gɬ?7o^v|C1e Z30>i> +z*yÍŸyUl*iX+b`y'Wg;]GuT~H/Ѣ$PO$R$wvU$d=u*Ohq4AKs{y+r /W؛L )t(A0&=8?C jjDT΅#+F} Ŗ=0gBͪ-89Mbڍh -29!4I T)W;!dR\䋴  D 1Al4P]sv! p\ (lӁ4#Ԑ?SK`8&wOsO߰Vpu5.]lkJUm^p\ SpL>Μ>;_`eUx"eF>4hwK0X=LmC_FڧfC HpQ@vM?!Ў)'0@|r^Q2[;Po@C&ƌ`A$_K ^~4Xvl%Cq- s1=GTs.مgr1<,;)#DZІ?P\TcaHZ ^tիhOYMkʓ1WFQnsS3< X2dMH~nuH!.es?'G9OLBJ2q\\PLBW?HL N<Glrܵǩsɬ ub}#W{C'Vڦ'i6atQRmxR$I,Z=|((FiY:q:y>N(&}$s+aQc`o:MEF03CMc & _;Z#_F!%֗yĝw!t i3 y^b!Mˇ^\MN;#_OUt_PUsF>Md>b{wB4F.Dd;!$A'P7ƺVN #F4KW.Ve3i/%41<1."_ 9L_'OmEǴWL AywDE뽪KW 毊h*0yS#|g4fb}M1 jÕ*>-8SkwPU^0 f,ڳ;ڽWQ Sk1ζ7> k+]ՕSi.dvv6ݮoo:/g 8Ւ_WI59w4 ^1IG$oUuFGUFA&l٭}qbZ).~j_nWق|&CmX97xeX| djۯ^<ٵk?L5H+5 N@|n^49R oj4k?SL>hy{źl't1zdƶq}飺kG899|V y,:EւJpߔ{ڒ'=5ֽ01l0M1#no0UB[GT5y?WN#]9WټsrQږ|ojʜ2$AT!S{`W )8zgce#]#5AZOgPtuX|jlmQI*5̈Ǩ#| $nwDOЪZQKsl5\1&-KMm2rIηhbW.SQU4T |)3 ^ܮ?}0m RSZr Uc ;+lt%o&H~CНeO7qZ[6.0:E_-C٦ߤ=_|*}=jsd8 _hCCwt2zr@嘒OfI? dqkbxpc\U!70~?v}?h: ~RJuzd^xZ-H2DV|nA )T9X#]4d"1;ӊE= `get̥5J_D/1#[˦jGFuQ>jim:lv=izRXcȻT6iY[J>5hY CЏmI^ wŘDWA2 r7hGsX~B:t졽iBIMKLFMߵ $"[" =ԂoBbe:qqhӸ+.&Ⱥ󶂄$E;ȿi) pA Z8F&"y"4HPOu>W:23fIӛODl\(W%R)4:'cyz>Loy?*0\ ޖg^)軘:x/'D K&|O]'ߙa'շ$C#ң/ 1x_ -1NqNjH,D14IٺW}qgzQ9b|(Qw%_‹o~At 7|?J0ncǧ'.xq`|IBPn,T:{6ŧXFN ?Qw|7#0lQOZ0Y`uyflTUfX + AT' F \LwZ)ulW.|4aĩNjS[ WDG.ŗћqꂨ+s3c\+בȁn==GŸB[ēS1Kb"?KN # $$ ȟH:q N'S Lh:آ-M(+̃9uػ/GNGFn9[ɇ9b?N+[#-ɚ2ۨiRă3 _~5$* ]֭уĠJUĹt37o5p _^Jt1*=!/ o$0LKWMr/6_ ᴌ_hl>aAƧT.:ظRf>C}Rj_`;Jz'WSqs=y!V7AG\|9~0xeZd B BFJxzwƟ,G>#XOtzNu :EUN >N~mǿoķO=jaxF7oD{u0ϯ$o:\/Ϝ9MztD4V*xćbC 'x05Z5('ȑ#xӅbX+{RL(?:53j7tX/63ڤ5s&7F!LǧѯO4 f֘Wxp +5UtHӜ|FmHPiЂݖB2}yEK_7AZ.<}+;mķW~': ѽ2cX>.?E\<:hnƓ=˩h$ `e?[ؒ[[(ei]Vd]>MBL=TF\6ۂc"[5  Ҿ v#wQL*T_QL:wDKek+mL$YZc{HP-1YrsD!l*jvmyEBi#$i/nv퍆>dFu~5#R.]h{Tm/ě=V/K(pǴNX1e*9|Us!N!SNIYx{.r-Ni`y!N-pk.&0́ CAzd_xQ nj/<7f@`R| "FM$⤉Sgb{hX(̜r/>o6, 6H!T?9~`.:I]ŋ;uwttQYʃē'3c<0ʌ`¸3Q}oV{dMx\SU6'h f|E=DqpYlza*s^tbLp&0eDfʸI+K>Q+-iDPFɻx󣅘 n͆FPN&ƓJUu5< IV|-&MPb.&e/$DI&A5 dNdFzoGhϭS72h(WLD`zmX0+L+8,z/3nb1m0xa.WEg*C'b.5׃]G&-zp<)gӱ{e o>t8& \)^/^O 1#Pbro\ Q?%N?og-"tGΛ7O{E3ΧނOذ.:zӐF V*D:_,wf& |vc{gc˱3!YIK7Lw\V@fN!ܬ IN_E|y"B˜L946%0`l"eeY}adU4gnLZ*?Jf"nzpΟDcY*DK_qQcޮFddֳD$#Eia8yT9y?O 87*AY?of:;tPi^GUNE&k>{!EsB[JtO;~r<;m"+pe;KqpwK{TmæYgW_L҈ o/¨gÚu`4 J]#,"31 ?F_Qz,&0&ʘ5)n=T.K|rOv Gz.3DR|धޡQ 7{'TBXZV2zb M-eH/O<0鮝-Ŧp5A=>mMZFo*m-,[xcỎ#-l;߬E0/=g{CTTȪ09T)cs‚H,ʺrBy'@~/o 5u8c%f _zXIz0F0p-6XQIYTYƵ{c9 !C-x඙Yotuv@g Ӏ@죢;-,8 skuR/MI΀ |PTVSt5FSywIxj,{:Kdᑻbi0v@г =\н?C[ovF'ka߁( [f߉ "kvl>6,;؄6֯wt$[Na̠B>ehln=Л鸚JTw 82v#=_^q'*i5  Ihk$ݾ{7|˶Qޣ$ xu<^1Cӏ\U;_l$$2!_5f̞ɾICKIͯW4Tk'ֽGyz|;TFGxN^16y~y]с 8H:Hx⹗u\oEA+ mR]UB=}U;jξ"0b@p|嗄7LZ~:ÿ\˓&mq{+ V!16qѧ^ogRN-@ZLf̹>~=E8][Xf|iu쫥$i1hP̽NЃpvrPz]מw,ڮS,yw PPcHVp%*a|B9 16ONۦževgv'Ş%וId&i3<\:*3Ej $ "6 2%d| vIIPr$u \_S iS]RF'UE!o!@z*mWc+G "Mʒ)>GI?(aۻS?<]G1zA.f Ngd\W'ZoVv,nŔ'"L-~V`l&+YsXWγ+9.Pr%kMB䓋WuFtEO`)Sڣ&I>b ɣ"eh/eGd {ƭiS&룆Ue Xɼs;#F ͷZFAz[ObʭVފ#Gƞ+c+y Y2hݖ-\чb(Tx[o_?E=sy'_Jߝmt=!4~I;g^v:Jށza;g@(p|_8S`u9<[#|kRK}r:y_‡"(nr cF~ _xGg8~!,o$Վ qqȿqع2~ J~?iWJI{emSߡߝ>9z4LnrOw,t|ED}UʻW Lޝmx zه sdzUʏ%ZP>H}ذKd6r=2#w?7=r֖Mhb ?&]eWo#=[5j{4XP6 ?b10tQR yYq Q FQ3xc{dFг՝a~I#==@UQ@ݟWT)I2Fr XU`=֢>JhUMqbLAea mJ"O֓޷l3-%m)Bp`,tҔpr0?-tjk)zቻn 6f\ҵIH|}B97@e}m9!sb$cz5#JatX83Y%oDF3i-cg?涮ؾi= *Z״C=kqDkJ̐yO2:>q^?91啥3' GߟW tX{9xבthjKl _O>-_EO 9o6v9tw4'>ix>C+{p1;]w`qD(ǑB8P ŭfNϥcI"iCHj;׎t(m"DE*Wl[^joVwx7 <;qXA{4L)=N7{pY0j`L2V)8uO@9r֏eH@f1j: m'Oٳ GB 5rΛ)OV!l$(eol iaVP\%ӧW?bܐPx1Ax{g/tXڻzʌA`ݶx AyUbHIgR(ݹ!-E&Rۡgh/.RU("4ZW*uJ܈SGڄ+IqWtB ;uMDdjJKOs}BybMI|ƣ/,_# û4~節Iqq?ZF Y 7K" D %(~y9h)Ă?BIqZڌpt ^{o_u_v 9oxt*ZXs}V_+@л%_:Uqgͽ^*Vٹ~Ί+]4F#twK˵] ~@[ޕ -A-iLd>kQ z1BOG3^=S y6+yyY~*FHp 2Mʚck`x-dA^I9NDC!^{86dKz"H:פSaͲ|Jivyo3{1'ׅ$| ."IRQw܇ޯ~({̼}Z?BCu]WUUuɧQQv;W5dZzOw]5=}PMcDh"C/snfzo !|wy6=;6s̞{z;a8o?JR o|T)ןx풓i[l|K|t؇?|K^:8yFsgq[}?_ȼ[FDeIüyw`B}w]MEa#\~s+ڤU~.'Ё>AC~^̼x"ߓ:`LNs'\:O?Ȣxji X%J'" gO%c 1qzt720j0TWc۾cܛX1p{Q즷gs- d}wa8͵/Ow*b&3ƥ3CcH` T@ 1aCL[aAe pޞ((GEe za_miGel oJ=|i,ĸ{Vi0Gc:k5Aj,z7`=? ؂0Y1aCZ\=˿]F+ = yp U 90n#پR/?Ìp QG%#`*%6!ܝ=zP/QJ*uL8+NUMHr;F^/|RɘeZLk u;YYG#2=I3K:Rs;[Z7掬F#F􋀍VDY !]PJepVfzrsЖTcwmel`h_~IOp 5 %F͈vr>M0M8`#۴{jqbF<^'1bZ ^jhЄBƭB‹9k;)WD;R 'j¼')߯ i{"O&an4PynvгS3Ű1Fi9걧_#Lcׁx>fس}kS{eͳvy"4Usʧkwy#ʳ`KE}ȱ*R啬j$ko<zǟ!|_02y&[(Z%|zg()3h2Pe0E ;Qekx Y ˿@b61ٰ` 7R[U:}7_چZh3y(ܖ@T3vg@iםj]}̕V.}xYmRJ˙hrvK) c_STUbUyoTmqè}WGCKT(b]r]M*sQi^oj]_6nү}jSA!KyYsa;U`K `~[ C|.׾e8~(6dN :ڣ7I}G?0検E ՚`0Ţas+0-~d\ԪjOD.33}bfȰ6Z  ydd K0 y'O 0eCaƭuӯ÷!!C`gם.#SU7qno$cKH;mOPo$ԮQTw{ur;rM_QŲ5o7KiLGOV}zI{YΤ΄_RޘCE :4wzeuۧ{F'=Su uv9{A: Æ߆8߾ nĐ q0Q?ns3[:n_ %?-Q%4RG dDw0IZc&]AiU~J9'uT2UOȭj[eW|'#/  oWXZ9 ~]b"dP̙92hp+IqNBuWsSSkq#ƛdm؂`z}IyO-xƏGPF# 됙øʎ n^͖4Z9 }V$ҏ3Z^!F|`fj!r`E>5lei g-ϼǏe{L?Xy#{T؞e̻F H=Ja= B]TxPQf&[-/X-oyÿ~ڕ@? G"Izp3g4R`^+}쏎“TϡpyRYggj6X.tV+k^Wu:- |B3% ߮UI>)2柖i=v^[{N)O'섄xRq/J**Wކ>ېGW~\y/O?CXRCO1|SPS[[S`^^=SN%a~.E*{y;v;(X4 -؇?Re*uK-O_f1gƝw?}G]`[qzЖXӑh"ij>m6jqA˚5v"q 2V&loajeF81^Hr8pNC_ F܀S}cԾ8E/KD̊13PhmiC#4ufv6BYuRQإF C2CL9 F= -8Nتl4z"Ȳ (Vh Nfl)7.pswb&qF᭗eV4$^9zؾ:ptEZYh$t6%&ln]=N{O؟Xn=u3m7"*5Yƍk8ፋ+{O(Xw M;Y3D:&6N8z0|aoѓJgGkl pfQ16z4rI#ȗl]!za7{K%vmHWTYayu81O LHc0fnum P\Z cN'ADK{gbhdKFu w[B:]ݽk0=z%-LQobx֌[K=B1qh k{ȗTYՅA˚| ?oKT'_蛘tH+KæQO|qa88adziڔqX \XZkzZӑW`dp? KL?p`d%˂]ξD ]H^^w0&?VjK o3gsaAG<iC(h"@x!a01qVT*%0k4[E-È{>(g.O&jyag|d冞 #IUv> zv~6gI$? >_~͊ؔٶfF_ TZ_ƛKKE{U!(r/Jg*0ePpVeQ7YrM혇\v1AbK(cYSi:Ժi*r] Iõun&u-<2p&abjuǃVUb=3ظ !96d`$'ivD JQޣMp CMėkz`[nAMM Fhɇy_|.&]sRW"xzCȜ։H:~ CWZhyˠe|^ywq7 ~a2g @T1X|N>jU}2Tz|kL2iw'a:ssOe1rK5K_h)ytB[Иm^xDz!VHw}JQ'&-OggUwh!˩Ꟗc ?i1t4neAWE\ yLQ%<`^DxR:cϹ+?#GIQJZZzKD*Njx4V[Ew*׮ԿcO~{?N*jh[iiu]d ›|I%uU?g @),Y)޺moAJVsiMjTR ,7?d!"-g-4 wtB@\5a.R*MtRiuGc3Ź䥏`܇PX jJ>Bz4ףG30,7JGS|UW}Jgeu5O-IӒG-?z'tN[VSnt5?m!xp/gX >wt*X!s=xjn~WJ;. t.9R6irM}|w%eHyJ$uTűj롶SO?&k-K@|[^î8)  >ڕ*q>~l&CsD^0{lvm>v6+5TG"wv -5E(:-nB*ne>ִ$|1IT^cE]pѸCxس\"':z?)ؒ+\'>y;Yޥ:&q;mNvsCw4T(.#c>Yh$2UZG1CTq.rbypm#T"(Iz3|0ssÒ+c:RJuk0C i7kǂl8Ux+p(qsw,d7a*!t([z2FaV //w{Ax%gTPiT(Y3U+ @m' P,_<9=}qΞc}V:< EC}5222@n;# u؟A|l"akJ~8 g&4G c:…0GҡȾpF|y&RMlJ0{=ޫTbvX;|PbM/<*?;|vqiF5 H ̔EF&[[8LQT^vm.80&T[wCoh?\JMoF 4'NmԺj$K8g0i JG2h3coѣI'Py~ГÛ_!q{yksk;#ŭ TcQ3m,_RJ/B̼aʩ7~H$Z!ƌL#*=f8ÅJCzZ Ul AB\7Pȿ QX7^;4|ˍHXRDҜ]+zHOO=h6_O?Zxـu,FCSDy=Xtyj.wz(L}_$N ]5UѹX$/z>?I/X.iM "I=cG nzHl|ZkF&OUFch)FPpk [9GT^+:S7MN_Oyj2wqНq*ejۼ{ϫv'}1ͶNbK>.+QM?pݍsNT$H@41ni+TAD 7vRQG2|dDnn"Fa9(;Z)ė_Gn ) :yeM|V { $A6wK ZF+Y^4kK+gG1έZ&<("ДԝXr FM6w^̮.~>N3ґ6T: A.ּ 8Ӭ?y8C?Wh,4e|,:- \xYÏ7/Uz%,=ܠUAfX.%[F_B ccr~ʶ|^gNw/1tv]rAj5]|6>gLI=:KGe3/Ir*Ԯ stL2TF c~]?x# aMZT@!>N1z5z vH&L9x"ߦ'50Go=\|ه8yw=BOR3*xJpH (Ust::y*͸o7V~sYtAq>7{%=hH<H51<{^MDQZX gzV 6A=m quy)=|.kbCG´ vzVfMxdS0Ǒ?ŒBd{#4HЄ/mNj\{/U||S? Е:&cWX2eLrRQf zy )Kb ϕe06^NYQ’RVL) ЛWoXnͺUlC8܇߉ޤQK}+I|DvS}6ޝm)4 WTS\* XΒ-A\I$8c"aW8_{F/r7nCbR ^KhW¬1fu{PN"w 3 Iu*~Bw^k޷65cl&8-?'S]P^U7ބk$;lHMO!oFTЃ3x:j =TG)HD%a5%NwJ۳5h> [ބw 4+FXZ4JLndP1;%A6Iφ3"$Cem$Z$kvwr-_3<3~Ũf7ci g(?`lj#'hB_DzzF1R:HL4C|B6>,iic\>Pױbvbgޭ(Y؃Ti/Je̟}5V+,mlY hd܆+m^_VS1w ?6֯[^y7R8ƗV|0rKjD]ԅN;d "y%I~9 ]yUa V)#1 r)I u@\JW-G6:G+P!B8iWQ>lv"YiI[֕8V#R=[mZ39)վ ɟ8y+몈?t-##3/ԜPU 8""'Xҫ }R Mi^^!2لypuuy.c$Y"7 EkbeO.o7OAǓ(Ysr21ƩH+4*5a2O&cu4`XƭF SO#V@v(,.2%O=<,@-#s] sJ*Ntg\s ~jSMjװO+̫FEX5_0{ˮu:+Usьɰ7a0brnXa1< n0ӯ3d%m<=K^YC6x$V@V#C6ɗaތ'/P[0 ԰)2C0>ܼ. 3;D5KDo!>JUVg|=֬X޹?dug :xh?aX]d.|o7mOzֈmz;HyWNܼk+q%ku<;d=~yضhWX= 4Dhr_O=ezj߱{U٢AOut5/JV.WXƠ>E 5ϭ?L R_isoWW& 9tFz ГԎ^]&BhE_z@4^ BMA+A q5g:IS8x Dc*cajD3zQ46ٓ9¬ldΐ!{>|}?;v-3Au0N'b=0Nπ1NqOHP;i3p"щmǣ}Py"B8v iC j ڽbTJ8Qyc5JT5G;{*KB;{߹6ȵC?NЉ!|#*mMW+) 1:VUT1-F!m j[ER'sdnv䅴UkH5(96J)W]\9uh*$ ɣ!+V.++%aj d\B޼Y&ՃS9I?a)W| oዔTz=r-ʫѲ 6lěsG")碀yKVW/uϝ(>4k dsЇq%/(O'Bʳ3߀YTÅ#{pH!etR1J#09ND2 СQZRx6/=,hHJ"8bu,ξ/7‹Yyj&+Iq;0-v\"FOgP` kAT @)A4 = c`8+^{"}kjz!.=O?t7~ܼpHMNEp{lIoYG<^pܫFQ= -QOz=K ?)ɰ&lyrz:"T  kG`ۡ4z"m jXM!#]"-͑Gq, QK-ǻ~`gs[\uIM .nT6O ~+>%KvBDlMNɇkS*ICp1eDu7^lTM q;];{ $S's'[]\\D_8T«$Q*_s$: 7s }(CI \N0 C Iu2BN=ь37MPHIˀ ?7# ׎}}c̹! -SMaC3: IwwJ%),pj>Z]4[_qlӿA%ߠUӃV#xX;r){/X ݽ^wwc9\ 5gM>o87QhaO=IS# rvF5^5 ={{hœɱzw#0ԐLȚRQūFw;Q0UiL?wcW M>nAɎm)i=Zgn\J s]x&u$pм s>#M6 Yg BuJK _PWۯ-MJ^(4[^T++;-c;x7"$:i[Wu駟O]0jȻUaqx-v|//X* Rag$[BzZjZ7pՔM@8lx,cs@' PL&M19gIq^!ax=J3ƺ°l)IҒ 4ll洮 7vnWfGAgI.GBheAB2!o K+YIBA9>C/ǘOkw7'b7nJyj/Wo/e.K潌cW+(ec&2i(BrJ ӏ^:Ȝ2:kWm86 +VT:ٻa/_-77-d3є73/\C#Զ%Ȼt9e=VN݊1aSxW7:u\[/>3C@>ܜ=x.׻r^ޝQNo^u+b|g'o?j?KcEf1|Ƽچ|X^x_~ozr䇗?1Q]"nFԾ͌s>'dQ8X^qmVDz.߿Go㶸Ns]ϒHIڟVcx)xXrm՗V3ǵ~3^^x1_OdtmCҢs} }c[ٗۮ׽>IxSᏄ͗ϕ%ݫA׹x8_캄tB1H﹪mTZQ2&Ux%*?,њo4lG(q69̼Uܐy!b“{n}J2"Lh ċDy U|YwaCV}QOٴ-(-o0X=a[40/t7G=22Q]W}==iQ{]x"WOUo'xaT6bMM3--p^[ *r$Xy0]vT1JH 3g\*;W4ACyU˃6 c]P#cM;ekj>p>˽mQGkKijVU~**ZͺBNq=Bf;}[G\f&2u #1y/o~(jT>4bQ 礞ڽE(.yO-ʡVbLYǒdWBu($$.Vܫ-IΩ)ol\' 9}҆v($IT>r^dHzeٟBq5y,2mh!qT޳+s|5rM6 nk YiWwSQPYV=t\/M5>BKI]ʗRe[7ęOO=Ŋ]=gM2FzU) J=0ۣ\:fptx?s]dO8JI`;&5\WeZMte̻\ S*cZUQ{9#ĴTzF1 D_)sXk!ul{zzB>Q)J03'V#l&in&<~6Kژ gfٛzkKh%iRhnb5 ЍU76gP-:hdsF^v(*N@ܰP+.-%ll,Hr+zsGzFK]z=#"pY[qZeMp5A_zR60%Y(l04o1qƎMMĈ~A&8Z5)T9`gf9܇G B*N4 Ʌ-,HfXͧK脞oD_:!,46`ӰYش-ٹy !ὰe^baڌ&>?lQ@#}q02C,cuĕk'k*'ze;77n AfFE,a|8J&4q4NE_iTVSK%#I|t$P!]P4 ^g1~Kh4,E.e#V`q@IDAT"D:] M0Xu,H/iShx9|2 &Tnczh>t`?#v 'JMgMվ.{^ɳ#}-OUIqm1\ Kf3ʷg~|J _D $cFhW^5[atRMcI38$GAxWԔ2,4%QG/0մ<,=a:f@] Vg Gce&ۢ7ጂomilZنPQj6Ƕakfd}|dG;rpC#-gD>2%\-qT2$+iaӎ=5IC4FbPBo"f. O2>]O(Uh𡁱.tFt;' 7:AŸ]>Nh($ފV릮SH)nCCu|TAcwYUGU{^{< bLn'8s뜚#\R#} 붬&dnOo1'P^{DXS#Ea=G.*gI{(lݹWjHE94?q4:QOw |_ƒeaƻɝeyR15RǑǔ˷̟@ <`a[<|ڌ~*Fx^u./?zNhkݭÿ9ݭϕםNuUOv1|B`]$H% _l7$yϒKټHI:;2D*|gG%A&>p%0 =kNź3LO&d2Z(8q:qp&-'3qT(-ɥ}<SbI8"Cj%qJ$qcidoȐ0+PB*Ӱ0v؉:ZW7[@2z&2ka|'㿦#qC=#Z)o1 $ycp1ymxl:lo- L)Tw MUF,3{"tM:~ bF ?+iS8oG`t՟V3r )Cz%`Ӯ]IH a?@TzϘ+er6>Vݝ#=Zz&wDWZگ~$V^Sr )KԣM[ox 3$ Q 5t+`[rһ m2cXIcǍbyUs,_%#\!cA$Cos?e\ΐp WOI@C/;WEyR?ym嵠`m 9E JktFɆAGf1D{mZj ֡nje+yJ ޅoMJC);$V;(5%nle4+2!#xc>[;q@ /9g(q\YfB801™c}GpH< KʙԄf4 v]إaO:`kSIJCӟ́N]=]M[# a+CBN*Izݖwݍ 1hFf"t'xS€4ct5fL F;I`;.Jt(R#< __/JG sMuKSO[.C *Md 75B)iN1TT9 -Ugt~*PFps%h,ex?P(QPb.D7@ V+'"R^^w 9G E6;D2Fć&`ogx8z\ 8# 4Ű&i`P,*0،cD~1ر:eLefF:2uyϖ5P2;QxLx ͑Y#zp $nm=0v5]gu6yl*tSbHZ :K?(пaM ؋SR KiSчABԾBXddropAd7: f,Ah燯>#L#+y˴ ^zk5l{ALhKV > `\ IdT2 |edf ѪZ2?o@ֲca,1_3l' !vNcŕ̵QN!=$;sNM>Jn~sHш8.;uɪ9d_߅6m >xsl;A-cY]{y"qϻ,$oGCw.\FiP9z[2Gj\ n&2jN,u^jߐEG1K)Gdq&ξ +ej<,|Rf5Q\ouk|R@~iTe?Ϝ`>н!.6_PoԘylr,M̨3vJupC]sӏDFJ1_uZv.;e3\czFva=|#"Sb1Z!o[^|cb-jkD9c9*cNI~hf$GCM&ϜyKW".Ә4i #Q9h?tM`jk`QS+ӰvZDr>sJsr#;>gHZD.&nPцI-43v^꧖#.91=wMDϳ. ULIpd^ZQژk"֪eH:R8=PRՄe &Q$k31u6l:85> eU-@@E3础9OcHf,L rO\k%sh{BUl͖b|>^` CO!,ͻH؊bэ062q?S*8_vMv6tvToYA^rq>i ǯh2s~FNtwvu\<"e۴iK݌x.G3\X4=sՐ\ 7lD`@_DOo^6{F0ڢuX|)_~-^{!UOWgT z <*P#p~:-xP1$*YOWk̨3+]֛:: f)ΟgOJ"ESF[~K}u=>yH~*zQ]叟Ss@äH[UߟoX+sLԸao5߇_zofRZy j0"l51/7/gVHM>Ŝob=эm80z.VDKsfƎE #!>!ulHuM<uFL\: H*:H;y 1!';'kRx:a$]# LCT~ F&z#&Bf^,nN$3شtqque0>lA%k3R`gA'Nt1"5}k:$y?-: jXdPfѡj1;[#B@i\n.$:k6G[259~~ñP,6`I[>S\;OLB3"G-,<1$[7 ݷFVhK>:kСMbW\썳s鐸t=Ŕu=ؿg?ܼeaN84]Fel:3pʆF^+ n!Mz=gJJyh㥣Ut}̒3vfrq籡t_Fms\5`kQ>)հy(J|A-o)(bnB6Z푔E\ݜIfܫ= p4Rdkj0sTl~倗_!X0Ir2ݼ4K. 4g[y^L6 Dg~Z1g/^yW̅ƍup-c餽 3#O8/[1(*h6a1rC.$"lHbyJ,&c.EGa#L rʳ$*V"0 r*_>UDS,SbQa28]hDᅢH>JJ(Z5vm"bb SN YWeo~1/dRm՗\7>*"zѓ,e݋-RRٰr^Tƭ'K)r ZOk _=I'(Ne'baMp IO5~ /PLՁCDd|ęx[llZ(el㍂Vx6eoÊje0yYXzxDĉju:3k(sOH9#s RS8hK}3|_hᓂ,;|RR||}< r%e%Űupؓ'jDcӂc־[HHM Xj!`eQ[Wk]'`an.i2ֻ}Ս>}z|H:@E9iюEIHt r5sz8$ʇ_Q}o֮?[/t:Oa۱dS2V`zi~yb^P{-N9 GY")(&}/:梦 عǎ'bT8}K꿾.*gNCPA^5u[@me~!L8YÓ9L Z[]w#8zuAٜxqgHèRw̻|z^IĔɧWUs$ȁ6[P13]k;?Ն@Ι*;Cb-WllM8_oYY=dA*Gq=|8"^{ ^A:pp8,mRUeu9N'A>!0c #ޚG҆m @)3ܬQZ_} =FvtHNTsfLCw2 cDŽYԜ2ae>pVO;Z;,7F&QkHy6X:WΞ\FĆc 'Jy+\55.hh-Ֆ0vGO F9vQtXܤd8VVvk{{ C*iNSw>TlO;}=P'u$Qfۨc _^\mff(F̑]xMXEb:q|'pRU[b"mcMFD?(3m9@C/?adDIɧK`!̘H^ww?b(bjjx1ZwY|]ySPm J(G_o-%/=5m^J_G2v\l%JV^ #aÒ{>v,Fq[eFe=_/~ 5R|Pgf|DBF AnOitO(:Hq/pb#ܦZf 49ͭ:xGk >y}:nRj>D֑]s{o |6֓vIɾdVLcFTzd'χ`P&NS}T"Ac%qaʬjQGj~(n&rxT04 _%ɄLtDK7lCUP`h:\ЎFSTCVC 2/݇qw3uVTVn]w7z;,SѨo,Ge-͉d*Elڢ\MJW` ()ä+nfg9>0}|D|8>vg䕴5G]#ρ`uvW,;ʱs `C?SFuq;s^~?lqQ5mHs.A94@>zW#tjÍm!J2/ij6raK*)C ř,QC TrƁci`.c$r(r>#F63}qd+w̴_$m`s9!6# ܘՒJ3J0 7nC`nr!~4X1:=s@A\*1-TZ ԑ PI m0;ԗ >GS#È;ôyepqqWhctk=Shp^ +F|Gڌk0f7dL tUNZdF7G(TEvYfgD97^ _^Z IM]::0#x>r0 l22QW^rBK$vr*mI#yӃ~ψ-96 u qNܫB $yHYJ.ⴴӸ<|pڇ\-o*}fĿW mo jC&`EțƧ_~ oV&ܮo}\μ(<2Btz0eU0|W~*2T<Ƈ.d "p{XBVu[Sb᥂xДEӠme߲J?~~bC]]1 r-_X)疣;G%NQErƨ!z. EPw>YsہK}`U pa=:8P[~(d(߹R]j51o;S EňPQ !6Xu]#ԭ..ʒrYCnxnX&a]v hה"`4L9E"2ǢΪ jS"vdiG/Z޹ɰmrq–pΖ́a݉DJZA@Z̿p \Ș7d~bJ55]s4Р\ >ILM by{x?. IMwDŽf*̼  :\03pA&#-%H(l axO/Љ X(DXNRQ{V,ǯ2IP]LnVVm£/8Cq[q͂e:U\`Eƿ;tyvȒ~nJBr9'?mU LP֓q騥W(}ֱDO{4Neqjr٣`UWR8gqL8[WE?'>#~j`r>t,0"]Օm:q-vZƕx8?/T-dbK䞟PB94gft$XO*EW\o06R,{ Y ]]Μ._y/|HHYMi˗R-6U3H !JzՊy2#hhcH7aFlF]_4US?eqCmGR2TW{\ҶܜL`V?ږ:t/gTψN--CվO\<^r1٧uJe9P 2vTnjmq`9sȾ@6Н?mr2r4;|ૐ;)!w.Y~ L1S[p,6GMtz 衬m@ghfWZDØ2#3RLK|z:!U&FJ5*XXa9 h\a[PcQB^C$i;QF;SL( G&XjE2񉸂Q=:# _a(,-9rrIEcƘ:fz@0_x1*À8+FSU5-#I(ŨV+,D{85Gf4n]5,i2e =8|/*'M pEwUe@MU6}E&{4+/-!n}f| SÌƌS0&j$sc%hTT6j'/OYÏJ:~$',TZKj|I) ż$9ܘO֨ vV&hd_@ sYqggU|Q&هQBz f/C F#pt!HgM8j1rMD--NYv$cۧ@cʳ(kU?NJa} KiQUr^o`UB}X.dp^|7Jb_Je:a-X³ϭ8a!}u৺@!&1ݘq[OeIpW/੬#?6"}IL>Yi`ٯ $yN)6V*jV+$+%E|n6'\ϼB L%N$|Vy0& cs?r hD8ؙ1?scsdW{NuLhZ[chÔе5}5S,w~$:x^НүNp&۵|FWד]!~ܑ{͉m&ނװ S>̇ozY:CIdb~PNk$1C3ZI=`?S3x פ<4I1"uľ8ϙy>*m|m9NJf;t*Ff~)2!hiieaѪpw=oZ˛@ V gę<*NX,%1acပ & Nԓda3-F8#W}|q*8P؊[{ ϑr韩+ 6u-SajdʦƐ%k)ve1ʗ^hoaz fzrDxJ[ĦX+t wL[GG*R2>g"FJ[w ,؇ijHqEɲ(;'+YB_13Јtx#3~Tۨ@]Uh&xzi&`rP |j'AUΨ1Bc-a>l*̧󲱈`uw F2?/v\-&qXSD chc.}F7,]>| .iG+Y[q EF0sȦSқo% I3`sf0Gܧ6paI[/lE|:d"dtb•YsQͭ57/ICa,C0fb$H3}PƼd[]t[ش[8n$}çNF,8vS PHE ڠhu|)|>|}:IRmu=wI]+>xjK*zN5KoYM9=2Q~Gճ7vsG"8d)Ws7=P+]K3];Aۚ50c*c1e$Դ1H'9Vy%|$S}ߴd|l7NKٻòu 2Y^Kܩ7Ö,oySeKnXk' q ֝>X/Xˆ3@ ImpX2NƞSJ)t(>ѲwlZU9k *"?>`W客x LR9B2gήo/߂NZ:&UgxijJ,!bu4zF:>?v㯹XN4|`:!X@q75D2e}q/BB~etV~?mQm#YıYz~3`Q{axD/(w4Y*@{CS5ǰ^{oƯ}4,[˽!YzwClfW%UjDQ.Bh@ΏĎMyǝ`b@t#Oʵ~SοB!7lRpojzFݽ7 h+8w'E)/bnV3J ; Fx8! 8r(Y`1l]A .$2tz22']FLGa62?%UȢ3ۃ#™?R7Ša9)5u0\)w;?hgTk),iFLБgS8+ X.0k\v'DhfZR共*j#R5i{K1`\5$3z#$ 3'!e)O''9@IIgFpP)"eNE+X顳60'X0 ѣ s|INGOkNŻ쪇nFWh$l3.|cu1B hkIw EՌ{b;vl9/S?^[iߢqh=_~gHIMAU~%^NEsgIj)t_{6SDEj43᦭pߞb^[3^O"FVď hod.J'yt6{`EVrC)Qlc>_vme?\\,8/߽{ vSGX12}ncP&z8%E x##L"Js|6aMl#{Ќb+@H>jx,+/{](n6`ʬR7$iъ˛[2zz0<|FQRVvA@g*0vy5mI GoM׶Cƴ$0j(a'.k#M(nB gռfdeV[%#WEaqFv6'0}LTu`y$ClеAaU-J 񽱱`.nZv-)N.2RTyTYR'(筞j_K;ēѤ5C;ĜVLY0ha؝QLۊ4yb]065C8RiB%F$x,v3V p$[B뜚*D!\^nȲ"=6a|udt9:) T3J)ESi>6PB$u MH<m+t1JZV6̍|$#4 N}2biu:8nBmQ5}>jVST:18Hic.bER܊ĶmyoCmjq>R6&@.zr9ud$>=OU"Mӎ;ϦP|]kdq򏂩3_n4R&Gǚa"g7*C G`Œy [UۑV{ 2?YV>2rτ+3p ^吕ApJ)G(;ҿAΌt=WCEm"yppmOGxQկ<[gΩ ZՇ.UCi7J#Fb_H `z?~^ܩ֚]/¸'AοtIGUV̷Qiըřd -rr_ -7QHyωLl`UC\Y84'xqP4۸VD0ݮ޷ȃN ϟkPTTJrD2/rlvZk<|NbJKS(J| r5 Ek"{h4G׾YdN@bQѐ4Eye-Hd264hWZpLE$eɄ/'onM&^KO*Mhi]e MϦ/h6q4Q3'ZXIչb>,8rU"kՋĢAn񨧁/l.NK̈VL4YT 'y]Ԓ\r)=hCC5.BUkmAwwԴgL->J]4 myU7d(``uz:Ϛ䪬a_- W~1o>.p}Z8sEdxy#w|4"£2BxϑMHka@'c)^qEXnŴ;`no_w?|et$eUGf/q?!@k &K3/]"_H<@ʩ<SlQA)$!:~GfnKOw\p>AHSA2 *`.1扽HLDQ)kgdԦ`@Uc#fmX*VVqkachW=SJ/ Tɉ߻CyKkh+Rz:<Xqӭ?>L22zDi-P>Nr:ƌ Exް55󼆎>sğ.D)8*F:ű]UgFKVVbhƖi F04vˇI,-l f+J=-trJθ'{ <9thjii!0hztƎBJiپ SgNpym@_|H(4٨,ʅ'mnge 1~<ˆȟ s~ahb%xFSNz[|vfvJvW,[7g)bvo%FzܱtO3Ϩј:{N;nGFA ?[P89{hFj_gm^2/j ׭d`h-M(fʮ0Gp2 6+a=ƹ9%&DOtFcj;ݷC-mݾeL @CCÔO%'j59_} >rn3Rs͌11j8vx72d0H[kE5( TJrR9Z K\wzN~UAh'Ըkk>%}Ճ)%t"F+#WIC&cׅ?Y|o&"vr,bH%U/? #{0 Td[ΙTU] mk` @:<*Q\_;&0hhҭ t?~TYH< S‰Q'Πτp1ZV$$aT7BF-bjPʽ%r4U =Fe`ɼUxׄ /dhUBҿ9mmcfw_{ z%q|dfM $;|8 :Fa^/qHGTu`\7U4bh̛@5dK#o([ISWmd ==fa8Z|o |]l Hᩗ?Dʘ`ܙT[e"x>aCrA7)b*}wL}p:u5Tc#fm(M;az$l^d 1O#m0wE(.3'?&^U׃Z91->'19TlC@{3h* 77Z`@cU=ܙ7uX+] =3FnFlZ xZ\cM >3@]=· &00#x?1X-ecC)\A@NΒWIvfSKo_'vduE}T"Ŕ'zWf #J s.CF ٫MXt= EdQ+c^NF:@6-9 Yd6U3Ͽ#GEQQ *ܾq[7J6PKt6%n/^D^d)Y QMF.9r .F[Qdh:;oőSg˿hDCE+J<^G\K].ưWbxj:=4=?.i@E:UH!EHMSgiaxT[AHxU$4#їիT6GT{Vya+*̡QWaZ9vmJm릑R4wqs LC9_1hb 0{ qػ )[IDBNO OϾ]lqFG5%1-J3_ݷ_È?548ێ923lZ 9HB ŝ0G Ӱ,Yf"X|*|h4c %Z`&Vd+ƏMn}#%`TK s3q7oQjEW6[7Ɣp%S89C[ed-Ѳg-Bof5eg&Cēȕe:*lH#RhHQ<;3[ ~U/l-9`dD1єtU~wmhq-$I"q(+^܏dH^c d}5G{$p(~Ye(EwLk\F#y;GSiiRMSVבOu)E^_ 4k߶tF!{EYq#2Gy]~ڰ[~ `+MRǏXFzT`J X5˥zr ŧ}+|)/~Ȉ)Y_~9(L}#;J]҇Z9ؗ @x{Lb7ܒ7T")\WrŬTU9O>$)I]Դni18.Ĵ 7~@ϑ8E_h?ʈ׳@uHox1L|u'̢,ʪMky'}x:X6vՕz:U-`R2%.c?uSVoJ}u?L@yՙ&}~ȹُpK_9 Tq2>3G/X"hu S>eLT^q/ghK^ 35s OBȽ#GAgPaQxE,50okU1qV1QJb=9ȌNF0 vK#%zi5*<={ID,Tj:!3aGyC]F!ȇ2nꥦgƭ]Ą _LJ|3+ ѹx{S)eTM`<)1zUQiIIX#ظ#z8.P,(3F2ZyB8>2LiSTvh F.egNL)=WPZ}1 D@d=SusD`pjcFSy2"6;zuy:z?QF2cF1 I_dWRR~l5~Ipur43Ԇ&w+q|IgUlFv63Cv<ќsW3kq:[yZuBIqhSБ)GѪq սƿ2JO;kuK++82bUnN0btt._vo>_|6([3 <N?bdFi0l% /[ /ʓԪkN⦛n[oSNGI?O <UA_8O&ϼҊxZGS27q Vp%ֿs*<ᡣA Qg;d!2_O :y=/AT l\ 3B'`Z`y5i4hFO7ޠMMm"cB Krt!_܅DJDpm˭AB~l#2bˀ?}QOU)e/ Mh;C}cJ:w>`HX9FB/4UNլ+!Jw`-I&@7*˂ooJ#r⛑y3 `"n4瞯}  ڪjwbSz{# ?! DVvro=-1Ic?{52"*QOoyqIanYScɌxcF#;/n7&w}$A$G5'_N3SιLaLjkt6+FԩJJ- J`+P 'TOokL2.KOR%k(\Bp,+c=J@ 2 Q* ߕ(McmhKHGwU6 Dڂʖ7QŁ=-$ y}(KEH%2y/N% m$p^LүSU]iK(Lgzt1쩔TdtE澶}ZmX;64H2r9vw,IfV#]jpej0-lЩצoNߵX;n?L2X.@~'Ý=Xi7&?o{V0Gn;P8F<'IhIaX$VxոH𥕝"mMt6qvӪk4|6pW08r*ƴ%8XӞ{̲Pp)‘]<#ZP.-xȺ דCՋZF2!CYg8v``35(`Vmt4*h=dfm2ed#wތ|CK;]݊J_}XvrrNr4f4y4Vþ(6 INLB[˝;vmI:zh2V2^($7r Z=,C9?9 |ʂQxW 5c<,+BqA&wF):zcC:^$c?{WOeիz_=BgDx(iFd^~a쒿8r^},[/ZLzvcR/__|Fs7q\.P eu0`\WQ,lSZ!Eg-_r$FyQ_&y "E܉l6:FYrRoԛ0ibLM.]oäp p`FږƘsM.#(EڭJȺjqyA96lUW?mqI~ʴ\/& bvsRI#̉rYĴbKK/S -4 䘏Q s9E*ƌ,'Nt8R*QĤ vEs 6-2~LCCJɦwsvsnԉmtD0jl>/>Dz""^WhÕ_oGN0#Hk>nY#`xݟb^})Qs/Ye<7\u2X_›o) 8ʔh>F]JQ=c_~@d2a2x5`zMb$ҖdOa2OJ~ٿ-rk PDQˉ ̙rK_}&]F-o  FL6}9-/} eS-+j!}BfߣmsebXQTaYEڷd/7Ɗ>|]f"|)5<|L_ߦ^_8&i1~ 4Ɛ(/c:Yr?O,S_D=b<0B,Ө撾':JepO/us2M-mj!7#NFlg/\5]ƱJM-et3C)K1cpgt1}ݞ3yB"khA8#ƊUwH9}"/cfPO0T Kf.EI1ww]GCגUHK:,̤Tw ΜΧs9 O=4琿 l@scLX0:x-'xNf7\=TA0#M3XVs8UQ$FN'*?}>?_1C)Ac {fV=GOLu"ŧņH#%+faj- W:w~/\~|^d+9j #₝aaJ3>Ǵy*Xc'etۚhIܺZ,`U\~"*XP-F&Ĩ$/{=I{E){2J0}pzf9ZwbH*+ˇe돟`АHa;f'' bBCT!,`95Dϝwr@|̘D&\N LFܷ',$/@"FP_WbK"d@kwC7eF@˭*ݝɅ)/;_jkzQuڻY]yjRI~iXN6PюTdHRGfUwu{iԌ0\dHLkk!ZV};uv3ﱞkgT*2qtl1 &YdRܛюҾ=n셢&Fwbo\LXN$ð.FPgvq3Gx> "58H^/[a)ccg+oQtTa]3+[6lLv2f8עp,+ yvPԲ|Fo Ϫ0=60%O{;ރfޟtFO:ң 8u Ɍ> s]R2l iAUB 5& 7lJ `_OʏԠ p(X5_zLi#,~Jdafdv;Jҗ1}‡͟~ݾٗIWN@Y.[.TZ.!׾mood{_eB;ɏ4Bu;xq\{6Lύj} +ֵoK{biۣjT;KM'^ɟ('.TdMGAnD^˥dI]m<iW2{'cI:cxw$87qde"a:o54cjLU䚥^9$'"StHRD!xPg/_<عm+}H3Ŏ|o0'ad!ibH;tP:y+VE=j|y*ӦNnV21,N1/S@x'6 ) @3cq8wǬ'r׽(/,0k h6(wZxL-1f\|;:kYCp֩tSL2A ÆΞL&@bՙZ+SPX/2>Hf&͊J-i`f,R4PÑԈOڄv(/ctk)n&PJG;58|hedKP쫩Pl4]cTVy0Vx9^Zq񁧋 ,Gkv6ɨC!>,3I[}.35n4L?d%54FUQ8.u2mlٵEF0Zke^|GL 7A7Bʿ!p"p]/ 614 w0 lf :ov I_\fK|p|ص; v Pׯ)gҙu܏`XFN,LOүsol<:Pb t8_}) qÛSa!-0HcveD$A.OlCٸyqǯb5YS ΜG|p: {kϬ߭^-3tM܀?xy̫`!rSȠQR&A Ga݆ x5Մq<(ۈ_]߱}v>lNjc G}A:)yTDv~>XhŀsO@,ǭH:~-*/"^~ ys1o220R`˸޶֦u?O>FzS)s7dKY '.:g92J}~Ϲfʟ^@g}(}3} H6È(~>zXR \+qմh$WjLt [&$ խ:أ+ {w?za?-bJ)FpQV_(!>ԀA%¬f8$j;;j,r˦/主fL%U(;9|WO'S }۩<N6fX9u0.oiKfѣ~$ 42YظҊhaH@7qU-9zXzd4dBB1j]@WLA5&p1~7q;wPZğzbٵW9ْz T:@aq9yZa\Ȋ)b ("uvj\iPj@%dn BZnA 5`ST*&dЗcsF[)FH\ah?o|nTJhj>2‡¦W۪ [v=5 }{I/Z;ql|#*2U0_Px$8Cah4x>[ n҂3aVjث:d!Y)zw?KМ J3RkБ5liLLjVepvؘa.~he.&]O@֑ ޽ƸDJ9-/fDS:@VvF[nC箍z#d0"OQ2XUUz)]&t 1ҿɾ*jۑwHU.N1?9H/}w^QXY0z) uİQ8v@cZnzt:CL]w)tM=Zm\ʟUͼOX#R8! G:/Ǎ~/@P_yIZoKnF,?}r{yO??AV\9jx(bJ3b3E9AG5DwOm#nt8P>Mem_ϕ!בHJ/k,;tR޶;Կ)Clq:}N.)驖w>?+ F])C_{d#v/ۓV_]7qw}[]^zqK:V,yXz_M 6/.ꅖ-(;fRɖʶ(;" 2v 3S@??_{w{Z5yt``\6GKJot{DZɄK;Թ=ܛPwq!u 3O]nRZiVk16'UʯƆ"a^OJ?SX9nSv ]LX#0Ȭj o8SiP3A#x~ iBÓؑPĕ.]Vb8{N&13SlD)g'ZjzqSCG_;AmR2ZbM#~Ç2/c 0x6 (c6+3\ `}Gq{R~HzI?w夿\8eΞ: Ro‘q5㮻•ӯZfL6Z >A̪mJXZ)eom#h㵗BѓA<`ggSq0AWꋋI!c5i4tLA;LQG},!\#|9ү0@hOk@Q“ӗ2: i]>``{`p3ZA""h8 )Snc:$>Np'cٗ3 2+`WVfK_:ٵm=)}0aEٸbtb7O߯V NÈTպfzvэ[rO=NXF@t3?p=68~=pW~fEV) IOnGdIVSV~T aͻ(ݾ,W}8XRl\@` ꫨ!1W]\+wuDz;o=CS=txOA~N?ć%~$uJS* G)mF `/&Ois}o]KmǾ8Rےzלťl9~ (Gw*m'+YJL8ZZcOA( ȐF:WCGGhUbi v֜=le1p*ZꬒM̬5AgaL=?[CVزs?rŷ~lνl fMd菍w vb ]Q‰5ML91`HTWTm~!?g;$=D0drs$ $-~mQ 1ȅt>~MHZ]FFiS8ɸ9v5%Uo"#1Ak:yoL@Y`̀wY h8ۛ10@WC#PM˱RTBz:9ƻ{\:{xi[5vU%V OY@QF>3SMIY\i'[<4) j۳%v3`m` IlAIp"WSS11sK5%N3(nf (݀pgx8m0bBG0M _(6/̻:(P16͵8sq.9>6&3t$Y2ӿkyz5:HyWϬE'0mjz+w; k/'\<sVbjl}"#GO!CS2 ~L3>`𛭓RexĖ;2.n^ڷ\oXy\E#8Y.vY8e\5*GFEԎW S3'OGw72; h{y+?+3+-+{-b%8)^|GnO(FFQ3nfDli=X#XrvDDi5N~f'6݆TnDןbM4Ahu/ʓ} cE;vaԨ=Gz)gBLbLG:NۆM?!gܞEhƴr(e?Y-sTbO*c.E  u6Yg1ڲrWKmej:Y.Mo?ׅV@v1er2΃^v9M8#,cq@ 1{ܹ4ɗ9o—*L\\$ipNn }o܏^@,V]FPf*sȳ6gsN~g]qw綫}/[GB,3" ˟oTUu|X<Qhr^~g'eQ<=w{dm!A}l#Σޫ[[-]{OVe ]]Nz?G0yR=筞0y}Q?& 7gAߌXU{_0{\8vLj f3B9JSO"q>E' f1%D7:e/iY47[߫k  ?;CYEeJ`ʘI[*j뽮/3KN:pһ-wV1hN? ظ;:H_Uڕ2_j=4dʹLƆ)xOXUMeNJ-M5ͣ}osʽT}}ʳmQ}g.yO[קkiGW,[Tgj,%E߳~yWՁt 5#-n (\7_5%Y+c"㤱0Ͽ}%' =ӱGmRE3Q` .A6>s~2s415 koabi FEs&AY)u]9g~ 7 .Kg,i"K NCuҨ^+IZ[YQs+@IT:JI\sQKP,]4C{RkPOؤ eꮘ@IDATط0R$*gN1v!,r4`aA6uhp(a=Ǝ0R5>1D؈I$ȤqNC#4P Ngq9@9, /f^Gt.;KNMپե9N62tm1SK-K$)"$ď2OЭ/:f%wx &ݏF\K G  +&*MMйN.'tH= 3~>(Ylنc K+0@o` IZjRLr j P^9nD3sY P[ cpM7c[g@YhܛnIL~f5dfdT ]fL;Zi L~z"ݫ|nZ\J!v'sk6h4Nġֻ} +N[oB%㘸x~C4EL"~qK]/T 2гuu|Uc1c⽽̻~6>(} 3*Ӥnͨʦ6N&RFi;cKVֳ,S.O6'3H%ao {}1CP@08z:% ~)vxp$(RTRg.Qc|dO܋Ժ.X} ė޸ C773%ٹrz~| (0ոal'&Lk|7qP,f y{%cM{g\]IGkqDj d# +z*&8WhTi@J_pB>-h՘cϡLDBAWD_`?M:I&F]'1(ȟmgpL `imkGܪ76fԨȤ"f8e̦懆O߄%&7=CU% u8VI_]HAOL knzLْU_`q5 `+ϖ66,mlX4r2n\R_Lz*ʑoϾߘygr1ojښzhmc}%*˙=^6U֎P=Fl_ ~SǫsaH?~yiE]u(E.GIqW <(=f:2SAtbŽw}0YK{"/`Q~5u(~^9resfoO n} '`8iJ/tNy@=/:,[N"Ȫ?rm}rs`@#]=l@!+ /FX:b')X+ +.z:yj'n,kCHxM7*eמQth\|sTP$ƚjGM֋mEf+9}9KtRΩWR",v#ϙ܈}$ucɤ2DJZ]1vs`HubM-0:Hf;:$mW`hS#ı14_}#:skg #Za92\Yž4L񂦳YMH<\3{&) r8{xltaabA=lzh)xS`y1x2f}3 SIԵ RhTt,ݺ NX`[⤛zT4WuY AZlŷ⣏{=H6Wפ_MWvQ,wejKb!̈V'~V!ij{r-b4*_ďoHN.Hݥh?OeA:u>^$uO]l,v rITVp%_K.{S/~$QyW}:ڛ %5OS>~Ͻ )0}p1餣=TXDB tFW#,'N#$`}\f eҾ/wf1GgRGQ;8D.rہL̹fu)B2Jd^h}Czi{R.?z;z6{6;O)亥_w_j]SQ:\(3q!]e@ލ<\pM {|B*m~d;1el4iB5sJN(j jqӭKY\D]dX:y(2J@Cu+ }lN*ɼ Go訟zB/kIyˢ%zS70et8 f~'bb.xh ^pw}kχ2ۧRJБ~,&Ixi=5f4S⑷s'cq_Λmg]AmZ qZ_8OG|J< /"RMwzLC܁ 6;a[-됑ׂ:5"m~ܴ(dэڳ`&P/=wf8iHB-T9WM@s^|]}xWZSF6.xg0y0h ~McƳ0 f,⯾",Evʮ<(m[;=X{/y6002>oZە _~-ḿ#I=@{ PF9N8:Qr$2NaP4רOUؕi?~L mR G20B-b 1*d2S|Q&+{_Sri:}}f4:ljƺh5D^M;4`wj7*绐E5߭GP`gAYـp&@ ij[حɻذ/b`;\H(0AYn0a|O*ךs<#^%nna֭ %zqY3q9g`'N`aB퍵b_8`0CItE#pCt$bKNC r5@=0ѕԎ'Mn QꏪT8z #%ub="\}ƴ?tݰuj6P&,aRrcw"@4q'pxfÅ[5.-)Frv٠s.o1AdhF\V d_|g ذIjS<bPikQc g=ihh#gNṋy5Fz'_>w3w008Vd0X@t0Hb`GzZ-LX>oV%S4UL۝6[j 8PNL%t8Rēe/iͭ @ss+:QJ 阽ѠGف0A~E9^NmXC\2VѷWHăLu(MFCE Zt-QL LkB{z;+dr ja. =ڳzF[vHF  kАc˫Bw8?o!sWΑD\N5~d^y9w? ֟D O~}Q-EB) 2`Bg)L2X ؇ҲTѬSHӽCAe\Sg$:T]O =6m6Oa4h78HZ) %SH}3ڰJYp VM |=ǐsIץg;1.\%Yݣ Z_xNkK:iD9eKRغm;3UU1.αa!|&8y꘽WK&74tg(r16 %:SԽ_7%?P l#z[&n-qj>RxV(:UBɪR46{Z݃ ߢ?u U'KkΪ6Fo9KۍPvhH a쏊 &uԤ~5C- S:**aSX PIUX[RvPO{#:F?CM5&0)&IdB`xl'!T؄HKMä ci,(vς>;SQ)u06c`J rdE lIs ??/(!HI:PjEXb&V*8ɜۑK06N)AeC#K) 1zfIpx-_|zRMdn 12} Zz"2r.CtiWڑLV&^q-~# ðeZ70{5͈zEEco,ƒ=>OcW~o?yqlwoÔS{U_#}R7.D>A9ϴ3)/^8I~'=d8/ooH@0g2k$9PU0wy  g:|==])ݠC_'굗:[?s.ξTt^*Rdc(Cagu4}_ăv`  (,yҖ{'2gUkwI3#[f_<O= OBOm;ICp]3ۂ|Y[Sg%GÒrj]Z{H(.fivcw* jSsUozv~ȳ?>UtK>ýi/Mj]~SMO|WKmhW?޿_9m2әQcUhfbs1S#4C$8Q[~9dyg=f2r䪥mMt830",m.fp$$}|L43(_fӏ#O>? .ޘnEcdD alXιq&HJ?8PbYD%(5+Pil 5Vܖz :!Qa׉q6aa9]\9jBcp[>kS] R&_{`̬]= j; kl#[ɧplO0󣽕u;n}Xx?(Q 7㦛n%VYZA'!3'ن sǢy = xIE@M4C"zOc"b=H?),aD>F]H$M:U\DS`珫y q^Ë?R_T 9wڷi f'& ƎkE _ .3+Xr24;\|uߡh_81C6=6Dy Als5ƜsfSG&0W+#1Fay"99YLn¢st'jɟЁݮésNAMU-@!\%AJ.؉Ӱ̘um*LrbJL;Djvri<'O܅hUg; ~`l*]027͟ãQRG@O)>~"Fp~^iΘԳu shXю޿T Xxi >.g%)Ū_a`(avԩ:hCI~νnX5$}B9S%XFoٴԵHIIe" 2ÃlR>~SԇY?.:S e#}G+/[q;Qk}HêO@ub[VU#TRw [4)kV{H}{$'cBbKcf~QX;|H1gR[( &OBX^R#aCx PoFv.b+f)j0cmeꨆp)=K bO2 D}ciysn(;L##Fb`8?HSI=w&j]p0f'[a@05}+;$QL0 u;sN@Y%e8 򀭳R*qצ~"5̎wwPXF\jn160bT u]{;NP/;afg&Ot0H:Eh>i>f9R ՝㥱n ܔ8v0J3Nz0q5tu*^]S3ܘO]JO%uh=x=Q^Tݶ {7[/6P^y(c sgnYP6᷐~na:Ra̹VԐG 6+MJ_ )m Q1TkQBQdh™p% օ9e |mB֋qcKV:㨱[P_T)Cuu@-%,Hojnsω6Y d GE㺥72{(@Fkҿɺw ]hK9~R@ֿɔ (Zc<ԩ(!"40Eؾs )5($@JJ >xVTa'O7_gD㗟a$Z̉Ś"|4DHH 녻_Uߒv&T1_U/b%i_w*ڍzPNwıNDIro(9 h8+Ǽ0/ìĵϼŤ0bʸfҔ:ib\(l{c]z؊^^Sk>uj_ijb x2 G}Ϟ ՌJ5C+Пd!# F *`VX70jQi>Hg.A0C]WW(G9cN3qlDcO2v+Gi(V\IVi/uu9ֶ4o E5I| kv(6%Q{KMGWW@{Md2m1pFlGPnjQnz"&L::HS0qlGC=4 HQ .8p9QG}T`iO>ٳ6 P8 Vi;SpɅZh(:D]1MYINA8gaN3v҉EqCdu4EaFnqg +LqLH-p߱t)ͨ9To;8|H&Gl=~C8ݡV_(-E.3] ?hxy1;vM!x~JЂv#AAs)q$Z5uªKlM5m!DRU_oEKkSUъ4~zi۶sm}K?m3]Mշˁ s?KM9U]cfhˋO8" :>K#4<|Pwo ʾ#ˤOT@T}Gܲi::}!ڂB#K=J}1'?͜0y(ݳaN/=\d ]d)/ X(YݷJ05m04q Px vn=-GWaKP1 |8򷣠4qg:m:9jSYh;\l# pՂ2* tXFXn?N5KR:uIB&Mأ54PJ?O_KFu0sxavRʹͫHl#U^na1sUTIJfb@ tXfc.Kc)vft$0#Kc :ݘD{_N䤧VQ{-8s>PH\h=qWKp/- /BCLcfs{oR+gѼZ@N\ ᑴI;1M'>Nn7gW{k:ba扵;a݆7;3(܂r*Š 'fV!a Ч5%>Z졺l}֤YE+T|fɠӆ0дlG 3sDjʒ8zɠ:≇YW'v[jH%|=iIF0e^99|5H>?L S ]'{K)MA ǛFE[PB+=v!A"eѿ`;bNjg/^f9&]qV6'Ng] eqwA}FK]>&13g~=lC8992@;A3u\}kڙiFZb72%v˯wS{2vhbjԛ2TI8袿"s~s-Vg.+r%DFY2}_cU_k|W@`V jowOL#eЈ.C/S3ʡ bٗìFI0VVLAchaKsTL &cD@pK P97^= QW"|[CqxNWhfeNυ>)qIOl8*f¡꣨P7b!/QXys`])WǪI=0Y2T oMV lw?ޓgѦC/:ROaGlBy[2Ò UBy2 DJ!&FoEogzY1X@ xq*` FW#Sm47à m쭭n0!P Lw"<65A !t& )05c澡ZHwʂY?I5][@՜״lݏ-H!mmKM]zvqC3[uI FvdJdDp!Xq>籠\?ΐ:7 ̠c/&mXÛ8ǾN92;DJv2(Z:MÔQCQZ}s1H q%l|p,) [ccp^{#tO.ɦ'o}A@ Fj}2ѫ: ±7g !\L.38`|Y>8ZN[]qY ]}CְsOǟx gV0*`&wK To_朗纔6k :+@ֿ i,8ٲԞ]1D*iK,Q()4"#OA2$)[oQS|ucqNZ&1| .ʭ9~`ˬwjߗ *g:>sdWMl%XH'[' CI[{զи( 'ɗ8:rJ-[y Pf8(+})٥bPOX"t ꜔|2]=̩#ȥACOf&( ~ȱC:M #!qR(=_F8HFڇq01Ŵ}>חx1AϢ4(6Dכ54w:\kS_߀<|5AC=Z8jjٯetq:78%%&aDv ‚~R[eBŎu( uou0gr(x匆:aN(ӌb-g\ 8WF%{#رt0 rX31F*,0f{O9L3/:d1-Ű'_icIW#]Rա+U GQ֤*BCUdzoގQ.54 pǫ۠S.Vה6'Nݧ\-4/˥SJ>Lng^qR|w ͽ㼫L?oej!07jV>;ouu5ọs@ۥQB3lwi_jS?.ӟY! t/(^}!Fzr/ds!Y=`@rV#I _zH3.=v*#HM_DLnՇ*񭹡rD.ꩥ"}`f>-p դ ZYq۝ hq `}Z:q+z^uOR_.4#EO}hfpﮇ #XOo:G:̝< CG^Vl.X"l|W0$rOXxWmGbS(c_Qw9fHК:jʟ9ͅ>/{ȱMcF_>L8A=c7޹jD0{~ᅠ眂y :fM^ynksBG]cP2TeI0/Q=x%駞,ܲ#F`Yغi  ޥ˶ Nj)p?Wf"QrllZ1ZODsI&\ tBW { ntQAՔt 'ofl:#m?jyt9*l,?,NsՓ5p\0}z}X(}e^rNf=Hs, #xX^S^I`dyUDZ]{2HuʷPohGJhs2iش)8{Jp.0yk2NAm9M&4vsnVO?yab)v0xi/\:$3<JNS>s9dF>l|ܬ5_]x2fDo| Gj>ps~a?>ߝw@ 6z;Dw_G#2#vI0A]f& ndIE1.ۆLO|Ⓞ"!w(-P(PHݗԷRow^@J@qww熛C}?ܹ6w}><*B\\hcvWݮedxm=%d{y0㰷q{KiWD=o^C{yh# _w+S5Z^V붾VY #x9#Q) TH3".@kdh825$ c* `(Hn@'U«n20pB*vSsЪHjAmߨ]5YA5$&z%֒c|UoK=O͍hKgA샯;>X_tlCΕAAn*K%Ǵ(rrPpd*F29wskNoiO~F233?Tٌ*O4Y]܅rlb8 ≔,hA2}ZܩTYg{:Isě_#ΊbQ<7^~ m\?:!(j"PUQB"R>y hCT4b%;5(;bǂakez3`c+gp/\D€ZQ0+? bEy'$䗕#z UXI:kJ:?H8@ Vxr"j,L/XW&9 d‘{8`\Bz6y frN998L@y-ݼYLik0]Ws JbDL_w9!tPx"qL( JI.a>+*T1 9l*A h)u{ݔ-fwaD:_&3Ч5Up+X53dn?m wOb+XeVcc*, SAf9b#|Џ,*e /GRᑿՔvŸ# n*|ı`l3I_#PWWVoC $6ixXfq~<#/orƎZe ~$uIՓW?ns +8ړAqi݁Ϳ+_ Vd:[E|aX |= >eh+`ijoNBŊQ+BmŗwqXϡվ2 ."|V&{e{{MZ z9$UXt2R YF};4(Jy~N5]KAwܮ[N9';OJY6bLV,p3hJCk0]&LJ| b%*y݄&%SG {'Tu+$6Jة[𽅉-`G %';NGv6plԍ bn]%x%@s 1Vx1M`k<61>XKfqr h0ipwi5ac>X[KQ0>s Ay031讠VE)d9dA֝ s+La.~#xb /RȈ[`{#2 J%PقJM,*z \TF.4eB ¼|Jt"6 I &p )R1̲Ƀ(AFe[l d!W?:$g%(sCk!ݿ~E~rlWn<#Ǩ)ϛ_ {D+]vw)߮/2,)gURVM)|GcZd]0_*\XrV][Ȅ?YQIhd@V@U"&0e})}xo)~y~DyG rzDN iEo9L0sy?zo\o݄QLoҋ0rVn4VC79IJ՝>TFXIFo|I=uϡo̓cRAN5!{#ɟY}H.V# OӯK=f~yNwH'׍~A\R>rϟ2R9AXVDvf~nAo~~?Zg_~2vl`gf};Hx+5=2RP̪£O/=#SZ|fF`{=% ?ֵͪ|E^i=sO$XLQ14hAUɏaTS0V쌲6K9{2QYws\J'U:[s!4r9ٔSji嵬ͧ,9wuj]3iKڊ>D@"߭H/|ظq=:ZX:] `qr? Y=+,QWӈ/Bz^1^y{(B.,^6EP_GyLَ++J̛``a ŵ:I<=&y( \;P}~Tbvv J'Y<,Jվ7}9U1ƟѮ  `EҠhCq3 }ʇFu Wl@IDAT}9R 9y&aJf֦J|=ǡmzcN1- ]l aeBG >;͛'x}^ӕZqlOr4V>,V\U QC򱀢(%89v* 灇DE _ʃGq)ي,SD~`1Q#zVY΄ %O?@G2ImVΆ7/fpY/4Xe 3ga_^DEb}m8UDf->BIUO_A$ {'u[ zߥw0o0NwuX8 <ǟ#1t3gO9o=`U7G/?SY=SW :bL7IBu?EJ I9?tID 齐E~~ISb˾rHS$Տo !o282(cp%ȱYMVIeߛvέ砎d y Vu`tPizl{Ec3(J6v)7Ձ33$D%!^^[IS$Ӳ,Ø WQbي5n$kY'0=|ChoF1'(筥ȸ@o$w!B1r?H6Cdg+] >Ɖj#["shء#Χr1hA7qa"X+d7vG)eYmL2^TB kF! ?r YM%=9k:>{~5z@ +__+Gv\p}y !$L:OU%/,/ ne)oP-egqK='I*8v͠R_Nd@.2?)8WHf7ijЁ}:[[:{ S^'j4|ܷaDP_EFc6*.ۋ{^nEw3Zە>mp~v hR6p&U6^p>d:gbrIvY@8unO)cWߟ`3d};/=I oc+Ϯm :b$Sz##ݟ@s7o18t8cyϤ49ij@ Sޫu58Ho;Kf~{ۖ?  7U>^MPpk&fXh.\͠} p*)T[n#ws{أtI\Sl R~%л%Ԧ2&+@O¶Xq־3\;{/>/xgTc},(7fKFX{{;v5T~1V1f_MzޕY&$aJS NW⽣{02vV&6c?KEI8|=צz7M=NO{RԿ7+#Ȓ]}zD6ZqڿvM=~KKg O!ҸO? >,ϛxZD9IH=Z{ssJS  vH$BF&9Hsi|,Yj'8٫xi߇56CۏcbD8t<* EP`q Y: yUBp]==. ax󟫔uㆎ`+Fy}rݫݗZ!>mvo XfgouG=+\ ن<]~P=)wn;_wK<e1cP?NuT~ug^n|[5ۯAw׷9ivϮ73.yNXg5p(dQkN$xV޽i&uK+qRVvHGTD";%M":c+K=]S2jh-yf51&0jjLV+4"0ЗpH-`A#Vv8PIe)bbR)5`2e3Z(L%Hk-Z2Hihh@ -~~ѓYkƱQwN${o7e'?q}OڸM碶1 Vv$9 LM:08W\1 ܯfRALjJ3חI꟰gq4:bM]~3& AP@ _|H$0{L: ì( ~Ίs nEHI>7lxU3jCo?d+GCK؀S#Of/nݷߎ!L?Z|7.ߢ2Δ+g+s Koqˍ`Ʊ~7doAj4ȧ8DZ:Z-=Hʹ(+!6__c-eVk֮_`wCp'XJX zJ b!p611|uYCUg]Ͽ"p`[s^۩V_CS-͘(Hv,Hɡ Ѧ5Z/3yX,zU|(tIˤ>W[Ʃ/1|jaIG4q^xOw<'}z&ӣp^_hf EO҉\h ~Kei7ߴ*F_Яٽ㍞u+gɆÈ ;㗝NB2y'3a5}9cˏq`_;ʠ>bD (%mcgo1t==;'՝b(/QH _~RȾx\]O{qXV=~?Mrj̪wL-I#3Ethð+Ok4XUX:슧}m5)$,.je,kl%=NmE:`_ 9PRQI:l?b0gڻb9'Xf/ioiNP9ƎZ,pq¯0a1G{r z[~Xj}MASA>Z,v0@,>/Cf DYfNC)f~LTɦog{8s.osnohxlݴ gZaVAJLq{U5h*~a Q\݌cH:2 :=2O;66JB>O.-:bIk >O7=uDzbtK*yn6ibVc*g$񻠡*b'nk&N1Xg*%PmNW"6毉=4S!''0Vx}w߱ޅc}?Tv0Rʠ?ee`1~/U*<-7bo=>,wю2N`A|^Ha_*gÇq86ӅZWG P-;"HXJGCEv9l ~&'UrKSSeK%lzV2y0/^YQwYBF I0xUYbfؐV2=q3L1W50g&aИ߆Rid1M`h=Oye凕ϵ\g[t)ZymՅU0 [2C=;G=[єNv^Osor Y iڷKs kNs@ԣO1*_A̐J ӤcDX0x+&dDQR)I额d8w Ɣ% 1ouDFQ޾ؽcҌ,V~7$=*vVQ7*48?,C~}u5aPjPvI7HOY1jw=Ni?Y n5D6$\Mq~VNdkXZ[ICQA`zO o˖ﱗug72&! R1#?\Az{xZIX *ffk''NhPze*ٷ"cnv!ca'fSeZLG8rЪVpw.Fice(32-A.|xx'reMH̿?{<>\d*UR"{a\+3V%eҗs~oAY[KAcCWUR$]v cCֿzBBC߫ ׫HMeBژY/xJ3rײ{vfsIoAJ{_Fksū{m> X)W ?_<*ܺl>+nd$#hfC=,6Ymp5xW4`{:pvRm d]o?4%X_XB_F.~ ۔tms":ᗖ7SsіCJ[x >B{"_nr/.*۔ oT;cꅖ]ײ#.0&"1wDkN-mshFČRtV݊l1~_lY:G.T{h`Ŋ'mpS'1ydzQeTX[ǒw`kڎI#a$:KjUu3l(GL3Ρ!Rbς<&֖d)էeTS2BS'+& ZϠ>s@9-7Ͽcϥ †wP"1i2ylq # .l5T$oq<YFIb'x:tZwg"vs6նeCXAdT2ΪtC1>! &Շ` Lz:q|e kf͘dk,Ĭ zR|bb1<,;As//_}^2d?M>fX/o7'PQ?"\rtTW Yɸ>lL W*4 *;bq#pwRA}HL<wr}7$sFN$}mغ};]QZVo| imS^QkĖe>XyXz-NЀ)X0>JG}X:qrc>Xtږצ㑝̘pr9F\b9=e''8AO*Č@OAuO}]à% zu%{E~-P3}mU?9DQE&-ΛdnGI#(I ]u(@ڱ4 >AYEgBێz-r^mVM⊜"8vچ[ՅRnlS?۸ڹJL(n\[ܡCbޖ5ڠwYǒgTb,PǫWH n4zj՜lCl ~$nҝ,tԼnQbU].kpŒ1}/XV}6v4&i@[N'XPFx9<i CV; s@RJFű*{&cTla|QJN/ '7&8C:YK{=\PQBR0;b_wU`rSO D!Șk YL04# "5"Y͒|N9+"meXVk0ghkڊ9zT-(>θ6(Vzp03|6z 2h%fE GP>(g|m>n}a^ hd֘2WՋNhH(?G E*uQܰr􀮠Zߗ"9`un̍ )J g,Tr%ŹjJ]/ԫ(anElŊV %O9JKq}}yK$U|ŸmU*{d+qY*=^<~OYQͼyr7Bs"^1^v֟)I<9v5?U6ykь.O83KvvSas?oO~]|Aɍ 4YGjهX:EK_"vP|@Vd~Cˠ?%O뀝7e#hh J<#IGH<01e!lV<.phJeXOډ{O[ qWi0&t2ZLԂ<Lń9@2e<`TD93`ZL2ZB J6IB ӄLϿ3n. BCrF9NTuCE%9)>D1(q,T2OXvydoRg1!{znø[Y hZP&dA&\@rS8>Fd#g r-|YߵSW./5i?T ֢\J錆n%Q *+EI_ѝ2?Fo.:z7yU=?}p d~tgn ePRYY+=.E_PV~~UmrZѡh?]Х]d_.?caHIqSg<{Y9kqyґՈ/}뮹o0sWSny>bʧW⻍{;Q(YɠOlBXR~fT8>ZXhQDvOK fBȦ:J M̫ܕ ER: ]TNyh9!M$c4N쓋 452YAM"{-mxgXeGy~ЅmndEQ`+BXSILV7Ŷ f;gcLΫI܅C pʧk׉QUILEs[sϪ6y}7%jOU9v^@^Rn&`{D>'V$mYe*R!~o9+F9jߧesun†ҏyrPב9ƾ?S*g4K;Fսvz礢wt?to޻o)*'}l߅?"[/9~v.1L>x*iUlJVQΏ@n_(ODjz2.V#X ɱ|ƟY9alfIs+Dr*LV`7_n!\i+,p02u ,GtCU0$iM}51&Jnltdt` |>bd4fFL|a8*?GĊ[p=3NJ#dp$4pUe=RkDsАwP8ͽA9/<եaIPpᅕ+f.8AyU! q}3 BKm ߌ]12"6GYs E?*%N&QPh̿F9r?y%9:uL8zwLpAS'%ݡ$(++˰Gz/`ͺu#=|ZFPi@gS5+nv$` OW4+R8|(ד\1-:$4:0WmXZTj.]<wu WK-xk, 0&˘uSTK)gRy# gH'6|PL̬'1|HeM::B!C OL[cM GAaQ;{v܉'`U !tS%{>_u.MǏsXqA9+Q[D"% ][MYȫ^|ػ{7eW}WU/7'Ip$i*D9""z(yM4R<+$i8@e؉{ ]΂}SY q5rpE.zX"*x{/<ԱE g?RYu (y*:){a_UEclܚfCJ>-FCu+H #۹GuaDL#V(cS-&E65V󖞓IVc<ΓPcVcq,'@nL%m'2_Jq:?4<|,)%_No@x2;<&ڲ\AkX @"֟{В_'NPRȐ֧4”$Sy@(ՑZPNPNǩf̸f*V!+ox |HnߪJ&PFQie ,AxZ"k;μE'[~kK9!aJn0lHjXMN 2ҋ /D9SLō\pGID mSK),0lx7g }"n ʩvHtieΑ> P9ST$͆/*Ih3b h `C B1 ZIdAȮ0=*j'٘yQFMbXJ+ieNV͑ A(Ah6#4{ݐERvp~xp҇*T84);uo*o8I)#s2ﲆUΘ{3aeg6S$c2Hл(~&$p@i2ӣ0 Xht^`d'aܲUؑSUS [H)yp:;b!i03u+RĠvOw/$SzD O!&O+M V?ohŒ3pC[ic?^jxAV鸥P}`0u{~b^Edʸ5ט0k1}37Ŏ䃁4VMe0v NN /wy,dN7UR֞.ŢE퍸adt!zزɍ/5IBI[$ 0S F;t<˽W{Oj* GoI4St;2IjCSYA،C^yu8z*o&%4w:C  n\(*$UUy6І\};O?3o҉pJ@iG_{X1 j?nۛ9Cw~GCb@:&7z&{0ǤQCQʜ 1s_JDZ?\'w 3s#ޅzfSs])wżbp ؍4s1qsɘ#񤝒&H4T z j[}pr,Mw9gk܂|Y{cMwdK&Hk7\& 1cFÏVOi0 \6uSd"s51dr@&O/ _:dP4(r8a~f6a?s E^a l(b6Dಊ2\x jȁmE<ѯtf`S&嘭׭F刻yM}F&RQ&%'m vmL<9Ktr}_=?1UYl']@7q r)BdU.9^J_8@-A}%qwUz^':ո%ޛWbKi} ^b/%~='V9'; de1}unV,#oN M" S ZkkQֈ(a}DۛJ_Wl Xu^>DŽԾwMxw0Qdb$ћWøasH { k r}24-{1g\9SIH{qC)ٟ *39b+1nĕ^HynS| \яC6&e :59x땯3ӧ\]( J5/ ?W!l_8]kj/{Pr 4)glF9QJL$ 7,vsrM'H"zRbr{ا 8D?d*מlS9ξf3(S*KI.k`~Ȋ*?5_7^A!Hb`U*N҂ܖU"Ԕi ] 'b7IMqլqX9D:bA׺"j R*르/݂L.np"޷f92 n=݋WX{ۿAo3uedgW2-Q(࡫kReQVo#D_~z>eŬkfH#P?iyqrRKZj |*hH[C'(h ?z86U`{b&-b%`*pp,8^oLʧSx]{K}*V}Y s2i/*N8"qU_әŨMq[%SVj[ȑT-Ț}v4(.V,ݏ~nꩋŠ*Vw܄Is'( TWR/ 魔mǑ|pիowߢL/Tz믫~+2)ʪ;MO?`捰X%,; PRw/)1{eQ5.MT>QHӫۿ۠*/YbTLI'Gj|,OYc56S6s q_rےcKKEXQw2O55VK{gQJr'Ag:y7ENy+rYjOUgoSߵ >~pf+aϑNgIqr$~ ʾ[QWg):x/'P6e>TZ%TF\hxL\s SEp+R n|7&+A:JڐQ9\lQb K; wӃy5N2؂(H|d χH79a!3 5Î`/:d(v8[!ܫL27 e@' +`QnRVNh{P}lІV OkBy[m)RVZ3{) P^g/08hpU38?(,ΐuF!8zʊ .<yJ? mFyw c܇y>VVdd典P/bFPp0VoWHtpS`?ۄ@G;kTIxK==`sLof*UV65B1۟N"X]y.d Gj ;q-)d{wPeE,87߭%јvp02xb@S'iD3m5xq@"HN/T*|qAj[p}=)~mmOlѦit;/v/+~߱Kن[ DBץl뿱EAVդz`_N\rRoglܴYX% ^9cPO]'O#IL7LhJ Kd^2ȈxXڟ@wy!g]v*U( ;ppf93w#f:%zJp&1ՕuIRP}Mc@COEW^ҚjB./xqn2r2_d{c=ܟXLȣ)xgIOSڒkV Z b+qJ#;uKI2$jy_ylqdtu5YkCcl;tư##Р#Id7+/և|!0VRFT֏ i)څwi(q<7Ldžw"di@Z6"4pJFFpq :Vo0klDҹ(*HF` G:Ω  K++#An]yG"n]Zce@!n&~00 4]z=ز%%8?V/Ffjʣ)*ٍC|.IE 5\-䥐 1a\`򑌙dP&Ϥϑ BPT!>ڤ7JߥgKr?ҩOPAK;}oʵHyTґTP[Jԋ`]d|Oزp*+jKËoϾ GOew7PXT+cOe9,~`%fi[4b3mI mMCd7@&<`dnM9R,b1hj7%p ZC0tEķ3I!Ph&#BsDNa ɑ8-3`ll)kc=qFMHVz3aB\ wVd!ۃ fޒLb Jic_ӻkmG>vc+pчŠGjoT$🃳;cǭKh De?#JegeL&+Rq<؟Y|͘TP)(,5@{s+aL&C91\7ܰs3cn`ˆ-c0t8e7H*OGW0#iΙ@evҝ.kK՞Yh4! E3тg!3^HV:]l.:Em 7HnB)LK} 1֔u;NH_ 2z0sGArŋ Z%W^ xZRw ü@6؏>v? Cݗstf平"(}W_xWjDMN#j^u>P J1ryŗFqk B_'={V93X*j> ft_w"Qz:)W"wlv5Z['%`%XՅ#m(:Ђm7p~R^ *k)y%:M**2e]]3Ubn>̾jڰ;w eZ1w 'W*WA]x?{o!Y5Ta!ά`Ko?IMB(8LojCɽ+c-ǯ$U 8 ?5az6/H`Bc)h%M}1t_=7eoXtWel+y@u$8;n[$N37)9A~ڋw" `μJ32?(,laH({t)$H 6VUTMe*W0.4nd+lg9aN_ 7.Y)3e!DcRcaKД-'iJ5w1NמW|OOx-(vԐg>-IˀVӟJKo_޲u:e Snð;k`_ψA(jgmS+~ϰf孬Q^V~>5x/D ںҗ!#S"I363oDr +eSO!%|r˗L$)Ĝ6|xo{HHk {V+H޻#ugXKk.t{/2]Jܢ/5 zUV]$IZs^ k,UzE}15E†9IBkU|8(r+--o&^yuel݂e/?ouXՍKN Wqp曵l5Ѧ.VQrFZّ+`N(fš32?yRټ2ퟲ]&f@B"i7Z+<xK9K}sQwa ]OvMlb$lxMK5_i]ajE6l%ݎfiX]LT#xjDfϬoL3rN[o!/^t:쮻vq! &ū۾ yK?匭x~y.jTC6ALN6ԝLF~~1*yr;j aq#a[;A1wB\:%I:3%JDpm>z(K!6cy_߭+3U  ʟnS5/bHLL,|dݩk$Rܧ2 )^਄2^kA?z&4Y&H &(=ݺϛŻ&l;w92SaUVL9%i\baeYS޸./V56Y@I$H[g V'RVo`Mjkȟy=?Z+RNҿ@ܝA)}nS֑㕩<zE~缟dvo-ƬyW޻O[.XH Wlڲ&`Xos6/odu=K , ^nlT{h1}܆^}lhv|dUW޽o7}[Ͻ.LjO8{2c`91S?[ /_+W}m H$2QG5ާo YL ox"z*ŮUz>]!!Ms=}h]mWVU5'jKcK)]:ƅ^/qEd.e*&.֤E}{) $9:(+ۑ / 2-͊wor0 ԓrϑVεԚ2rO՗6VNY?(M%Р_]_B{[*X9$}t'|ZctY>Sz|,YF~KJ(g'{~L]u*}W%H= <)\DlaE"ʾ^CB*Bh!tHPTP:^Fѱl{H#${־Gx/}Z{=_Qև}n eeF* l"N@ͱsu ȪLlfdj vs&xY[BVram/%";wA  }Z`d 6RqcRGOO(#+=9nцzX3'.>A?;/s3 .J쎟8C̣kbPD!UYڻOZA@G7'!عh#+0y $3GFj2gD'3YA7HQ1k%Њ%i`= 8a0|f=+@{Ԣlv% ]!?\iؿg^-8˿kKIMs~xWP 㕏Ϛq./M'2 _ 5(ė}J?_$Ir#q"#݅@yʖ H AۑW V}>8 .Xh9Ff͞Øv123Ʋ=mF˨ejnNB{IMU-s H_pxR);bE漮<^ʨʒcg# mDYn #*v/.ʲn Φd'£@vIt0^i?k#44BỶAdQabdSoNM{zqDF 9<Tj"QÚ$/{l徃w (qTr\,L/kzO֒*$%έ]reM ehց`G,Y%^*nÚU(h’9HJؘx3V~ yYYY,*wtTea&Z;\B,6oی**$16IMN`OW[hםE *מAi[8 Z82WIEG+B6P+K( A!>aLH1KX;1c2%́:ТmmZAiQ>n1}w }%vOF`x5gB1##G(/Oa!`&?#%b_>'Ak[V&U EGuYS*׻k=>=W*jq[0oou[PVv`P$ݓ ӻ7`#ߗ #kK;Xg'Cl5(g}ܻEcX|#6ډc *jD5oyLײ-k'~8#篜xT5/dݺ'TnV1*ە-v$EkmҟI^R 15 SM*Ys=/IOB'|w _BU{!TfۯCy2x_c)X$͊: `333P}M , Qn[޾IHܱn,>:I^XndGf¦^riVL8y;n%6Vm8 J&~wGPH(f_$2 .:QfE6=jIdDž JO =gt l[ ? 7,' B]I"b# qIu]%9_G$'㦑HݍǎaQGp%ꦔǟ=Q'2၈d[$OT 7-ƓeLG Xؓ1]FaY&=ޅ?zr~Uv(R#7o1rH8 7Ŭ^sdu9 $yg[qAfKà^tǎݛAmw%Y3^~Yû]Dϻ =f>Bjc6?mݨT7=]\nfo_'c{|6*ƛT.d߾ZRrT5W^1ir9Ul'W_n[ݾ<_q-MB"#c/Z47*X5jG.7R郤oNgYT~/O6479X' 2o+qX_Ǩ;O)mo)2GBȓ~Kbys#R0'2rчiJ&ĺ)ւz="5 q]()>Ci@/bG{狾iU|9s;K05F1޵};ZjyIs^ 7%-ݺvxowVJ s궑bS{_lx0ꩁ+oQ1q=U0z$nn&}k_7Pۘ0Wׅºo} `ǞCitunCsc=&OƮCX +EM7i)C0J,(Q:c%V.y=8V #C [ă_qleǏ1ٙZ~:0jD0ΚDbg ބx0v |xfV6Fr~}=Stqkm%l6dDDga)z:3A\S"+GJה}O?lۘ͡˪9*e@X~UnZן~,*{1*Y j> h)j9tqr Qcu%Շ[PK֏ysm ~updyz//quI *.E`?nSfoǍ"IJp&=Qcܷ+۴g3`A={OޢX+hǓH IBmbcC8S8~;{8OPa c'4RiI TKKAJ1_ç(>Ws7\5Kfe.Tڮ G 0r/z;X982^vǸc2rv[(Y!/vԴ8CCS‡8qVFsH4Vܳ&-\d㣠kaSnǠШzÒ 4K {xOZ究ăɭ$o Z> 'Y=giSF,Vs'9ğ7` :X嫵*cg3ݝ⚱3Yhb$7Hk MWJ&E}U)I(y~~HeNSx8E+ B(]\,'ai[OǬʸ^HU<ڃ`(%5$#J E1}%IAw Z81A=Aܼ B\vJ[Pͼ%ᥣKYZV_, +MޥfVov0QCӪt)/Vvs 7GCd„fmd-_DRE2, $Rf1L " V mcKx݅6 #LL5.TOu2-#( }n(0FV<}<}Zedsg^3z/YԷ9u*]lfGjo5GC1:781c?D{cWQ>f!a㭊* :-Ti7D9j kVg?̷:Z + wܶ@Ql-c2 qR.y(jCk*Hh5"8|Tա|&h6Q*k:ublςiWbeXe;/$K`[me2TkJۗG3@p|YXˍ9\mY;8FvO<Bfjo|zd (meٿKXYZ%FY/.RKm8 W׍m~7_6vm3s,*IL$RDsHJr_Re:2iڲVٞT5IS-Lbې}жg8 O@#ijG %q&xɳlSXoYI_@ZF|C*tO0e'+\o$f՘l[U{p f'r2c63zBb޾Zc2Qyn%[j; 0%zX=7 k:1u4eςw fj)][6b¢7;Mk89!ʗP)a79kZmiŗ+)5 CC˭ _<!CiI`&ϋvz-  y > ~1޿RXt"u>GJՔ\I޶Ύ֔{ } ~i -XHٰzH@gZ`٬ٔ6ƌ}NѿyX!->4 "q`hDjC!u_\ RҎС{/վpĽP|Sc_\\+1j)|̰2箇O}1җnQq\3hJ]OXB2Z-,`3ywTb.8L ӅK- :_u\dekV]w 2zWW>0}؃[^}9^5QO}UiLp+IO7ͦ}t45)խBqMnrM])ɦnOW7^S YFEE˸&C@.7cwkaL79{ü./!}ȳH$kG 5c|jx?XJmm щ݄Sؾm7lz&60|烿qɂ_Ӄ]& *q䕎 1i9?3sfLz{`xX@ɳb%n}= mZӿUL~Lz.*'TYaxL*x#u[?#42UhvC· :XOrysS vΊ=&j892e*rҟ_嵑KCF!ur`ww)* r9q]~Jysp˲>Prr N ύCfjs9 \QWY Җ#xd׽o9޾4T"2ԃU􀳻h{]-8vZQdAr#Ewm- ^cJFP"Ii?/Œy#S~2ɰD5+`ȎQVٙ~AlܡSgmIMTXu`&eo`Vr[OP>9\=iusdi1P>*'0崛{ \|IR܋XILnW;A;}uT , @!L5_N !a^xk%-]LSϜ(o" Qa{"9jL4ra֍7p^Lr#6=9AaC7V#vFE+PDp%$[#:f2Ti@lz,̌PZ ƒ&U|&WVyʆ\v'HsD7ivmſOHH$ UB&ng< ʑʰu_V}13>յ(<<:‹<98u: >Bɤd`܅)NX"rsҩ KFn1sS~* YO>xsGf&+KgԹ9gmЩc% Lr~Ax[޳o!W2FT`?au)ߡE$2Hb Z(*(9V \p)%fvnՁQ^MI'@dW`عzLf!jZS&҅60L~ -a0wc,w)%kBY Z ܊֯cEd(J Έ__vR GW0+AJimk̾1 RoI4s[mh`6'tu+0Dkmfr;(L:enEYffĜL%X%e%,EkZI#XE?*fMEj&+͚+o*M.`Uy y]y$B{aqmd6Ҟ+2xM G!~$ϣw \|ѪmSլ(mVWLPJ_ {aD_X*0ODK#/z&$xь;bٝTGY`oGtSU˩"'۷gh$y6w,ΤH.U}|Up[?AǂbʩǞɡt)\Gyg>ջ}2ssݴG̽v+eO[.9*@ЬDL~^.5urjQ璘eYd_PI1Zettt![.Cρ#=G(1G3myH۲e nJ( QA(~?\ɼ*073Qv%vST #ҴrKLaұկ[." ĮH{&.(:`E}ыw-i8IƋ$zp{15tEVSOI]X_d9chyNEnj_}K]Z|nd-df'L9#OO8pE"Av'Cf^!:(]O'ӀFyݘ8q^ 3;7_4^hn=ߓ oD9{es83Gg~2ڬ`΍dYl z׺7=0~M ^4Mf'ust 1{$N΢4 Dx4TLpEC9[ػVZ}uHbZj<deqiMg])WD)%,.mC rDQ'nfT1!ǰyA2!P'>Y(qlVU3ʉ)p$L&r 0ʵ/l2tη? B[uC}ĕV9&MCLEHo2<+T#w=A5 }QhO̡`8< /YQ\u+xw]$ܖ,S9LZaDdV<erK/+*׃$ $^r-$:׉4C"RO"+ڕ\SnEe,$jwo/BӾ$nE구_s.ZWR6F (`U6`W^|RVp3q牴xs= ?;]|) :g ڿMPş$(}+025TmX]t _ITiaO(H2aJ=b)?t +I{AeS3̂-'X(Vm#\ pBĉL0l7}O™_@:^TSLdi*dQ){I;F,t6҄e-M:hn˅^7&ƔԈ3RFV7q3B Hkȋ$d>FN5=2D|C)Io ȵ-¨W eǷʛIT^PڐNkc( }fvjE5%cѸӧzcZ;NCqrerB&~iy}^ew-W*]@"NJ<uUHz?reJJ'_{}N'Ýd&V)fJR~N'lFx(nW뇩ؿ?/Y}+tf5ɫYю'&\~!O=YC|kLMMXmᆎ l߽ Yu%O_ ЂQTU6TqCBJ*l)kNPJ cn$b_ÍsKi7R5͵k9АƳJ_վdT ?9Y҂Uv"Ÿɬ <{~LP ?/ɋ^T_= 0M }?2V)R&L: S'?E q\4՚861Y=S4QC$"4_@e/I:l.6~:7Q?d<$|?}oI%~$uTϨ<Aj8Q~$3ƞNFI$^upp MHgy8Y#f}*Qy߀306ǃ?;y(q:9fO?-b-8Jlp^Xu$ F ߉o_]zگ#٥im05d5H>cɴbx:[Zw;Rd }(./X*ttӊKqAL2P݇ ^bøa&O2N4`%8ϋY^BP`";Qr ݰfeV碶O[a"Mg XML)Vԕe̎g|zıZ=ɓ$AoVSIyɝh $Ee 5e oV+WRzA+0DVP9.Yw&**˙Y(it;Ildz?ـ̩AE ̘q> VmWXߧ:lEsY3Bbz{9ߵ1SA 3(6ڲqS#/ j.,s,ٖ .NV3ZQV}_s_Zb+mwhIHL7!S##_CHWZoqIl&MbBY_i-3"z~|v( B77ڙ>d 뫄dyoP^:B,C^y-^8}t,u2^z*#Xo[ sX&ܿa>T iA+ ל9 E>OzrIgE-aY}Z#a@vV$cۋfW6`|jAݦ$ϴ w?SU0ij~\cw-?9ַ+l}! t(|>RiZՑ֊YD7 NdV`XT4GEqF"#&!-=zBrr@PB\Hҹ VSoYUWo{s,. ^ 8B4Ԓ ߃ RegrLY2I 8_Yh)g3]'nMnd $Hq\L84ALIfJ0# 29v²|I*DtWڎ$$%MY([2R 8pAT3n iN4}* ~ !PATՔ 4bЮ69*+ApQS烇2HʃvGM){V]*Gρj &d]>R+ko\лbDZ6+cRVN}pre ={6omei,)2j>.c J廒3+faLqؾybFbؔ(v{ahO,RTqU3{(ƉOZ0$[nƩS's V5'r~X]Bm1>: ')39pJOb5%E)#u=ڃC5-]럴h&W{ǀ؅U .%_BF5ρ&LoHp;" (O`~9rLgp>@R9~HXt;A?)W7h.#AJ_yM a(k~Xg9*/<]u_ʅUl6D]wYRAG!f$VU5z|GU*/=%G z!Jyk^>rzD7Ͳ*Ԏ-/#0}%*b_%]y #\/oo, jm+ZƭkǮ=_bڄ)?je^Y(gcGU6LO?nRm;EO Z2 %DCh(w!<>kL12l5OYIJz\u`KxOm#fGL2@IDAT Qbnb d!UOzB.hg&P;-HG1`Uۮ!Fiw}c/u  zzDs*O]vI!DK: e7^'S%`BՆ &g /I;6ã8?_+j|'ۓQbxC4?W_ R˩v֎dPR}2H5lkmOJL}/B@{HqbU3L!,DM-fL{6}?׼r)gB^m8ꨥUE5Ȥ,HUTQjH;%(p1@g!|`NF`hXkJ?ܳlabX7n|4;>*9){wnWF ,S^Tƺec6Si3L>wn=n{߄ OHcxwp\,H́OHJsR著9W&ͺk~oSCA#`" ߘ%qrf$Tl^Dᯅȩ-)n{1ڰ3Xs3+dz껭tO<#G+՟}ﻋD{dw߭e l8ޙ6s' z6$I~¤z%-p68P3^$ʂRI-C8[_--X3g7ަ/PHKhض>#KJL\ˋrHȤt#[*)2B(.@VF%QI>.8 KHB,i)>_rGzPɊ;OP7U|q$)ݳ~'gV1+eQMBd$H˽bFưX/UfH9+nnB(  &3"!z՗eN;̴N.at ]1&0s߽z9x/mVӜcK,|pEמ0)h~G̨q[w 0g/ `m>sϵ#2jl|C'Ѣe n[Gc5c8Im_΍Çӕ׾QbRth;NU GX٣KJ-e*Ƣ tU3[J%$dz1f"U)fPcqxoh*dR]؛ UuGv㖒 KHray%A^><0b47#(lmYSEe{0nHZ܁'|X"N7ѫ89<;нp0#M cC1vd縆E$K.%eۋ`UZmVsZt=1i~JHxaMw#xRF!&p4Cv9wuAga> ч^# ߡX vR9hk$ڮD3CЭmjgTٍQcG#DkmqGwF q>}A+)imՅ|vu yϧח2Cm_z3/Z  M?sd$&%c$.i)8]WL9!ϦmdH$yU$XDӵ&Kzh*($ k_2 P(''{wӁARק˭yؒHF8 h:V#kC-Y^Γx):dT@)ssن,cًu(Sy2ٮ$RUa!C^Y!,f"I Ǖd^+&cb|(i!)`J!#b߾]%P[cH-A0 rDV(meZ4upӂFqcL|RT7 <Zdv-rELBB3-z}zpwu;\9_Z&sd8?5Vh[ )?D'ʣڙBnm'%Z7nVvoLCh)24q55\X뭿{iʥ hר$wႛ{$ۂ︗ ׉l,l~nX(u4_hhCE. }9+L~OMJm(X "w,<ӏ!&Y&VmR,`ne*J8#A=՞?¤3'P5d"Vơpps 9yP_2Kvʬ3}ҶJ]}+44VeDij{.u |V7| ǏP+O[ NO}*yV+u[fK?t-R&|x)ӀsL}r5`|Y_WվD=K% YFW]쒵<~8׺e{ҤIT *+ש<^% ܷ\j|~. Ț.1}MqX>F2c'Sz~ 屛=y3VXװo?ūlGv%.Oɷ`E ⡇%lq7m8HKh_by7f挷7ϿaOh\XXV[/5w!p+h:WT6 = 9r</k2--FҙTƘ9o>}c9sU@Yfl$襗刉V70&eM\m]T*PzRfaN(f߂S|Y]ϵظ0+$r۶&EoI0Ģ++ 7N6/dQ NNRȵe%XZ28D/g0F?`UV~ v& :)- ¨VC": bߋ#YC&XIkhbo;%|@ ™@2 L{U0nVyO=S8uţsĬ' |&v-r ?D42diZILCݶزhLIGQ0*=ذ}tq|arXV;ӷր$%[݀y31-8ƞݴgsAT[ bwo%xr3_ s̛񿩥&"7lihhĺ5_cbب!_}%[}9{II۟ؕT:RQzìˆ _% ^{fUAVV! X@|ٮ ˆB2a5}=5QOIF)U'P~ iʡS]gϲMA' l{{3OSP+]5ĒOAVO2XIdHLJ,ohRy{z8X(Wc^ Hk#}j0pfd29ǐzNNxLʌmsN&g2aC'Q1<&`Q -o݅i"I9 o葉J{d3 ~E;4#(о$Lě+KO6VY+d BWK8eOyj:I?wP~3lnNs =],aKovZFyYxmFhա9ȋt]gsG9ʄHJmг!CLĤ2*XJipԕU۰gcǺUh+O 7q"FE :֎cg$>fih>rA9{yCDUtz-t)DPOet`9I43 YYٸ}TKNd_2| `K ?l4zRrL1=ٜX+aL!DC*N@4׻k77ؤQLgD}- KKԪK\Y&^mWbdK5x-eb+%=uٽ~#eQI)}r+2ǕwqTk]r <@-cT68x~ܲRtqB}X݄{֩\ CUKKK艶P<"ɄD'~YM9\EKjU=CQ/w)I1eT&Z5VٖXu;^k6_K6~nV |ZCnŴI3pY$*UUopYׯ?DCm-+$X*vSN;bˎLpMb%>}mPMS\9=r8FD % ÕmaW<^T 3CvUh|i++Kiwwގ&% f 9ڞ޸ 0v PfoYX\qW@ӴG c@H2p$,>t Y=x;vdOVLV4O?0I␐P> LT-=nsPFk`h=[8hE%nC嗶[aq/ v+ ,Xi1aATnb`'/sVa+nglb`CTA뛣͂͆0\EoYyDOH{^W|W󇯿طc C7% kj7J,G#yaaH 藏w&¶t-#9bO$MxIHKu$YŬ&$! }'} ?2dB@x A-t_<Գ`]eA=s)߳* n JRᄉDPbcb_g]ǾEOL~2߽ t<^G sqY6E+=pqu0_~Z#DrB,6 'aBT$4(:{`մ2Qd3".^ aD5DAAuB L2) q<.}P7{ !ԿP8 wq%+│7188t˲Jr&214@Ϧ8N?}$Dk2\d}Ɠ/bׅ=%aoo*?{UvO^I%$@{ bAUWUwUUPQT.B'BzHキ78@tgms{眿UE Y]6WUU}܅3A9rX2@+Aru뿵9I~J]MKė$9,"CY=CvFGbRttRr둧5u1Y Htc IK=g{j&#5&PU+捁ڳ|ڬcyMG35*.>'%v<Ρ'^FLf=0O+譔j q6>k-LRGֶqF:JVzHӕ 7KՑ a1Ǒ~$b/#J?1 `WC[j^@<) Q\^bj0cn-OLKJf{r-k]6ih w{ ӅA.lD>_ H@ٜYH B%)uHC(&zcʀɋ^:>*ehHxz󴒹*wN`۾C٫Ek9ݓv|1)~'fJf =D<2Vm!<ܳ:Vg0}b&*X%CV1Ī2WgGX.>R 8| dSy|.T5'y% W>̚5dJNGEX$|nL &Ӊ nή42=|ѳg{Y~#y̢cTu^mn'UOĩwTN?ZҾ۩O\Gln֛Ǘ_|Dr} \/2N~ N@&?N|;yi'?^9 VbKɢ!C)HE/ jV~_3keIbc G)zW-^&&=c@if#ɠ(#QH->BY}MVx!뢏eYd͔-ncAO@Ei'l8E<$%&k}}De/#̾~ CGMF흔o)I@4UVVa9Lƣ3eZˢlV->>ˁDzQIbCnEE G2 QӶn` ȖZ Ͽ!ڻ丣3=я}~zPvє'.$Ȣ5U&GK&L aL#*9I􁽻Ȯ`t:d7șdzJ#ΆdF6a/0"cFzػt ޅ&Z db)IʘcaFf6*Xd%QHg 5ds㳯b[o(଀ԆƦa׾&/w}GU3,69đRF9N{^v^d='QVPggf < k"*߉AYް4(/s2"XۇLL0Ymd`sg1Ӹpb;FJR.7dd_oE'I$JOAUƳ ]=^j+--?6raa(J>mȡgyavLf06'ihFBnh!QG.9|'⮥ H_!CB9ˆ11b?B A橓,_"*m.|3}P9^dZvV< ܴg6]Q').,bH8K@1b?*0 }mۯ/'A`'}(^Ţdp 00>,'YF1t;D2%h%לt8pnk#c&L2Y[zT1 E}kIHdvccPtdx0YyM!c{S)_~r{w}su#+_;,DN3)J7VVYC9b/wGp?,xTyv)hW,0mX9/YlVX}"'4y:@_,θa|Msg-(ےMQcȾao,Ajr>tA,y2+rb%;?iy)*<(^,|(|_}jj~L~ُęh#S^K&igk+Rio_3d QWJbEv|G*@Ztć=ξ|# }U,du$d|[$l0]P̙YesKVFdb˞8n aۑ SSs3i*B #)5Ye>ܴbJ!>% 3'2]DkyY= ".\oɨXGoW?iX̹<pXx{̑ŝ9)>!fȼx$1;vVTt|wNo朘c-Bqash} k?f%4Fڐf_QKp*>&Ms+GQ -NYdmRlJw /N=Kd% o߉=byf? ,f3pPT= &`gQy%=ќ+W4pĬ/U&ٙ]̋qla589 "xzّZqˋ 8 m+.2hdW!m7:9Jٛo 3…s9hDFE"?>TܩR p[8W/P@VW)*19ٮ둾hձߴZUm.<`:g 7#x{s!Ip6,3{غkpb{#͝dZtBdمŠ:2irimv&#RՈ?<ӣϘl[9E͑#T58P{ڠN ASI_-ؑ(<ϝr͆C-V@^ng&N^h<Ms) LÚbץfiEff)͒~>q ,1N=R$J6j+Zfւl9 0dsG;"O0W-GnXؾ(̥!=t_L[QGJAlQZipӆPƉw)#02@joo1glgA6-*(p BX8+;w'5I$؂سE0ÜJk 2pGjv/ pU')#+ ׸xB6V{7vCל[QFBIU+u)INزG>! ކ& 8E'c)HhHN=gĩj;JߋbPHMdX%qUŬg~4͑Iכ7~;͒'O<OGi]+&\fxZr[1yTXN" ==g9܃&M& /$g~VSYJY='px k_{lG u{s7]*2;2C9t$WĒ'M.aaf ,ʀ>@$Isr#ȹ .;0.CTT'ݿҤQny'*ZY>Wj(h$-*:e`*W45yb^ƭ ʂb64Raؑ|}homό>|k7α '2I p6nv܎8^d!yqӍ}h~66nS0iv,A>!6JaG&(&'r_YW7R罔e`rzHBjͧo>5q̒r(#iNUz&0%qLXFUkxT dR107ڬ\N1~.n'("M{sa* o<1t4!!5$*:XSXA-dX^В2iֵ\NY<4$?0upG»/)',MV(YA0߂a_X/$)b}\LbTúA+ʉ8W&dK/Y0zZxq*PdV$7?y7:U;zbKĩ3gƼI|е2*w? STTcu1W=10M~W=9Ov  ^>L yp#O =$wIT郤KEI Yʦk`gBVFC3.KmՕlFG FG  W~-5= L=wC& 2 pNV8y5j,s}v'/1ޡwyeFRO)'r߃1dZV[n-tM?U(y)Xk55Jٕ0G xïPgbdd ;/ 6^~ee kO>Iq}y #L9e*I~ReR&՚x{eWXJJ\!o*?+Wx#hA'6AaJ^l=3Sr׳G3Bz԰xXq&Sp_L\. _˙W4MeI Q_N7S Zkaɸcmx罷GlދגvQM44CxH :ښޕɑ=Z+Fv.Ŝ/19tR߆d԰ ʇKsL9vdd]>hiUN ^oͫ*c{;|G_6UYu;P(KM;}oRmU3R-:`>SmG%i!۔e#Y`yEmq}/5ov"a+Ac]}'1ށܲf;1Z0[HGcc-I>uY*HdچI˞B׫61XaG6 |ϔݸ'W!:^pr7Adim pCM]tV$Ĭ$󣬻љw1&t]JЖtG_إgi[[(IpU;Z5R8@a?#hL_^bA61&v`RZncnRݮSo!\ Fi(ߐQFLJޘw' S)g'|S3yZVmg gd0Jbqr`e?x-\L'][ӊ&d[/ !}&U%cG_7ÓڴiyO0T=Sq2e~ITZM0Lafכ)nr|Ou[wkY uW߾[}\-QA}1ڱ ;""#9 rJP+x_y:-@IDAT휨ZIlCd-r\A^:e\Zז\|OERJ+_0,zdgTVIfi( #ΞH9(W6~E tJ~O,_Bܺ^YYYUGZ$iC(>&Mq91ɾ(~W%Д sc[jG,tY?b%I91|[z} &ϘU{G*זVe7F )b=>O#;Q9i5/O?XZZ:LŒ~N͞Ay,FO2n$p-oބ#ٛ{JEJ (jZa|%3 |^]Qo;iGaoIsq";6o3M|b!ީ;yh@`}W0sՒ 7BNb]ϙAU._}DuKOO l m __5 ux%`^(|p3ӽ%0fى &n!QWOssO>Eϵ|x:Y.&:db塤vfh  0Z a־mJ&XAmw1oغ44 DDV5{wŘqH ZSp܄1L *кh_=l ̌J34E% SyƂzPBfw׵aH_͌*4;#߻NtG}}\'&M)N gǰʍ*F1>u@!Yc'fb<gxٷ,&Y|ӏ [wt<]z/8//V\xu 2ȱ {Y8.L F"5f&Ba&/m3ŝfO 6m&hE_=oezJYЧ,XW) x_陘N` ;3?C߹k }jd?9;}f9S">|2q$S^@,h\B#ȩ/Td{0mb0y"RRREmqRXGaJ6%LkOW[C-}9{,hא M@ LKC (f,p,Ͼ}UNҷ;G-#J0~wٷ,ְ=r%{T dhMIIÏdaW[%3z%g;А";qh6esj7c{A,nr1c T* 7^k^k5'yM;~&( ИmmJږvՇ$'s0ɶ}5~K2\/]˶}w𜼄;oeWx/+UY}?h(;cnTK3(;ATQQīz~G47`,+u(ȜUԒ,K⎜QSeq~~diϘ{K}C+7!-y+n+#A&ZPLq YdR-GQFuTh4AS ZjqBnpܩc(UϢQ3hՕmf16D9et7lZ%׹v-jo:֥zH_ch6B ^|8gmUBh"-1ȘX/.f*9Yd>cH`p?6Y"jd2/2d~a!q6Dy>1|x>BE7؂16o=*ZEVzG)˧r3&P(;'Y!Eh-z]6YSn~kr/ aQ/G1f2ر,(E:lP(t6d!7@;*ְɋp )WOo -. s|lcАz={]X2k<4,53#FQU4FD 9J ބ,ID dm-\/G't^FkU>|wӵY+.|CM-z6ʾCb;[TYKKJY[yo"8yD sT Cd}\oQpwNSK> IȚ JV<ȳ2ɤNt3hLAGP2)TyB\e( fojwFy |;  I hG_'Ke- zp4S~8'0;D. oణILxo$dTPJNHJC4Ρ3JgZMY_;NaR33v Ky[< @H{cwub#4eM`evbDDD̹qlEqE QKYǓHyc&سu8r&ҟΘQl,m!ѯ5,wlja(Qp݄8WNzO ξ 3 v@uqW%v.r3$lSРk`_La2ŁK͉mٺ^8GK(x0)Mހt+0ȚEٸ3'N B ꊊ(Ul$B&B衣MUdeOqt} &sA?;kHHGSrT{-wk)]}'|\OC`ߒJq jN &ׯZ!@7S8>x{Ӭ^G1&99ؠ('CYoi`&Z^[C!b^Iw'6cथ" 7n8sݫcT/?o]E9iX@uSWf{ۧ2Zz:2J#sGKD\ @/O* &|_ ?89Z?;7Il"c O'y`biOvˆIq.>X&c"5Z`@fRCW){/ᾇO3ƍZ[ Jg) $ybIi쯌xm隴ȹJvYP5}׼1N8CP&U+lߺE(?}x9ǮIcךr& A ;$X#o6;b\ɪCFf TsJw!a<qeǍlJKE)V0导~~U|vĝ8k'7C:}Guz<Ǐ `+ VmUOF=2V]&$\@eai@҃.صcY娫)qz_eq b_dO>ul[Zak k4) `QK! 4ZI2u:N؍g?{)wߧm OK+} ڲarFYYr=kStzɲ-AMi&Ɓ,*S=:ʽ\ 8!}t5M_V,`FeGv &_8w j:wݪ(o;>c8j2Мp 47?YYHlH Y56Щ/ކO￳nc4]oHi,~" Y䏔l`)U7ƈjEHi9kk謜ޖh} ggZ$/:/Xe~۶cޗ~c"2Fk1G2P/FÅsfRh9GPCeU5nf~7nn&i2f0)/‚2[+1nL;j }|.115o!pȢlmq^d_Y>3˜חe(*IEnYd@;}.U% o9ni7X*ytWR\,++>T$p^"~t+ǡ]rב.dBZՖv+qC^C2ħ&NC8&W1~M_o:+W8|xa2ߺu!¥_ϛF/_B4=}@?ָ񔵾=|cbY͘KۨJT+g ått 6') Cḻ")_>ODe0ʶ;+Wދ)O'1Gp˕j3gNcDj)܉ף ooAr+PGZZ:}m݁oގG7WՁ9?+Taq_ ɧAˍfޘYw\v Oص#͐VKݷ (/F^[d3̻i==)+O\VTQTX՘d>G,|/eNy>9DZ=|ݤ\Ds:FF[1x4f=|kƒ#C[LsCLl"1Eb$" )Ps׻,׹?[X=u\7,)c߲.7z TT?l[=G{&qM&[*u\FnG=7OHJ@r5J6Zst/zanv9WbMzupA" U9ٗ,:U'>&juS޿2vݙUYY=yG^ 蝿3+ jݺv3+ؚka+%YwW?Y_W-U_1P esu͏ӻͺi(OX5:bv?LZ<4ji߇Iӓ %AbdTb3Xr &Ȇ 9o]Ԭ85[.1`rE,xsP Ȉc'qFsa-q7AY^TQy==ZBS2euϡЋɖnT{S*_A,XD.^I ~`\O?40@iE`EvJ|UƎǕ lrR5dљp[)٘ N? ӃQ]s蓏Ql[IX0XoQ fgn7{i5liBϞ6y,"`t_Z`=RDkdDUWf7Mچ܊MƥX:b"gM`U&ёQb_|^XSv1jH"~]~pI֓Zɏ1f`=]cǏVwWWw峇{PI<Ԗwy؏H{z >0}~ո}Wm$]р0'a͸.K4x.cc0KJ=N\]^g[ѽi_zL܆V6LjU@[aJg]4̂2Lk'3!i>~,rspd1%C vn߆cOAbIVx+ɿ$ :lkYY *+WJ3pyyz=d8|0?Ti=-gmc{*x:[+ Ҽr$'a;γxyWX(_RR),ܲs ڡ$iN ,,`";wvt/9 Hfs,Ońx\>fpB9_[~HB=lٳ 5#S+1,E rO8QWX95 oO__T׷c(jd =<\ٱP/R47M u EƤ0Dx3- ->O+FBV1]0n8 9L+6u?ֳ-%ྕw5wO˜&1sY;-MJ6-6n?l_~pt|-O>eܕyH:X%Iu5L7,tлQ>k Y,}ɜyٰ!+.^w0wzuG|N>  ˔Xke˖w>|E)J w>x>| k<:߿^aQ.;E΁?L}pɧ/G4䜾f-æom)<>,F'jZCM58>('#\WgasDWLDVgAa ?5U5j0ϚGLXUU Wk::L0:* dַ2GPF9ys|)60PӎOF$v< rn'0|x$eAtbB=-Y 81At[+Ku-G>`dEۤ|d!ʂTi&:f1ԔR@6*:^\HDZC?_7Wr1S`FP튥 rpuEG`*{cЮ "‚86w^VY&ڶm+h#`& ɅىX0bI,bmI1Q`jT+"w}KV6!=d"=NO ߽ڌ45/(9GCЪ?&H/S&E#ɏ>)& __JϵZ7^|~,_sfB&gÌN |ded$`3*Q4UNk,ɺ ϴCXt݁M!(T ud[ҏwoүKL*xUB~CK>Q) *ȳ=z#ug=w/MO&z\~%x?%dZqfe }zަpr Ο;E5m( J2\QYz^?'X`tOܻ|%? d$FF2CMI<j1!BX7;9C,ɂ՗EQXkwSCo7=go}0elɶ_ofz֧S1ޛ1[\S(g. pnN(dfSd;줠c[԰_f pR `={*@ ?<(5|a2yZ*1갅.JJH)9a݆D<2OcOMJDڥ| M9.x}\Q~4EmT(uGM-ۂr|(0h;rAeRz"5l\ZK g( ub+Őg^gȗv*4"WXe{{VrvIӮvjs%?iOY-j=޶1qyz7چ<9L2wd=u U;o0UhpU`1lwјx;̿M$$пUGGcj;#Q ) "!]kR;bJ#U.UAǓU=ʲvV8 ccbjd]ɪNOO?O S9w'~gKPPv~Z҃^i =48*LUQ.L eX/mj"!8W_;S;I{yL<sweMO:;o NtssCpW}L.!L&$]KX ½hdh5Xo6et$k+";ahkAR1@d4Pa"lJ<8 qCs Ktx#l:?}e:Rd2 L0a , =f 7dg'J8('xa߀Y?u_Mr×2!J3 ۇ Q#HzTW`i[ye,X`'_>Ho:TO?}Nt8V15ÈEAQx2YQQH'!EUU]O_l8Oody\94kּY%Ksay;49RN^c~yXtQmO> {Ba :z)e-/fܠQQ^vx qA z:7ǪAK}_oiTk  d5'Dו`TT L8F:ڗiwLZyյd|}\?maT[o&gTU>{_kDʟYZm/m+  j\iE)/|G,?{2*Y )jii/>OP}d!ܾ#䡶V2(){b-5d$uĨ)+'[ <$[[3os}k@{6mVaڳo4И(9_}|xXLf$POI+qL~Gƥxq:9D]h'ɘ-? Gczy^6V,sR[Ν?yKYc(2>K\>sA%=1R̦Ϯ>YT=e.j+KJz`2MM)Y}q/2L=^̯|Qlv_c҄iw^{9^YUw**e2\6062"!;"Tߑ OmfL\{_t1'. ӣ?b?vTbYӫ]ÂAyKKf}ISf^0>W}"/0d,BFfd#;r9 bSNd #}4+Vǟ$hSԳVJYQA5DE^ii{?r>ZT ')iEjREvojj1sttm$8<|tPdmypJL:fPbb2Z8owxSs;<]e*+5-_Ƹo;y,9'OG|~?*_Va;LJJɧv}x}KN 'ۖ}e]y#=)oa_ƒ+W*J2wҁU#Ǡ{|E)hm]T5&jN.ޞT3e>/.ϸ#2"W-+ag@s݅ӟ؉Sx>E3\6N~%Qagq1M\V~stnӱR[H_-ɣ3s'UfՀlO*9U )VGU7,cyOM3KV́oV 9[v/.:0;j؂d:s>Z߾!HmbwzGs5c$zfs v\~s+<vV]|dڀ( rˮ*tJox ;Ejóg kIn\V,Ĺyz_ɧ܅x)VQ@ळ'p".p (.$lyˆlf{cuUKIB:|Qda\j0ko !"˘P^Z, 7U)d2e#/23.`9'߃} iGEYN,gDu1xf8s.>~X:)7a&#!9{Ny6һ%}\B62{#:r"NИT.1 gh_G0MS@~,qF<մJ AhԈV>g7Z#k{#Q@Kxmzn)E$jEz[GUNVzpX8%V1.=Y{:+<~IS&cYJ܀2w-JwOur3J^ݗbG \]IMKtZj&=4^6H _@kr LA:,=%^Y!TC2ǗUd0-EMU)*(`oCpZv%[m6J嚆 /o&#@Hm2Prq\S&~ UصAu@9B:<ÔB`K\ּ߬$.`>+D_Yn#p}N[(wPL qT )Q *uY_ S񔠳cp6xu4ZamːE':͜71݆cŬl\HΆ9+JӁl}O M1dzd5"jd@omY3y8X@6HzFH~6>s~B8>QLs`BHAvNyrT~ovu,%e(BP,e ܽPˉHɦ%8w5bfMGom!xԩxg0c"Z0QVwN][/V"-`4$)$@ ޾ؒQЩ.uW{̫{::dyz=CXT*A%PqEȑVݫ]{;t&Xl@G?+h n蚯ɜ:~30/**D!R>0$3-;ݶR8CF՝wc+ ʬl+Vf`OIֽI3^ER؈ɸ6&*MD+JJ1;r8r>Vm2>Jw&@.b(ǂMYdsf- `L7joK^/?*",b]k掎UuE~N`2X`ҙZ⇣~OOH;-VsC?cǜrI/56|'͔dA1'0AC+LVu'PyiOZcC ')w 9Q}{zRH&_ewcB)m津L7E@ \UVuXϘv,:;|+2:2f ˣ7zۛj`G֝)GK0uG V^u;d" KHMlOu?;]c *%1:o~7c0% p (ԹPRD,4oX_ iu]3N!sL7`}}OoV<1}pUe;y*o& 1YlXaDe;HfP6#ZCAX밃 @9ix>UOiwdQF\/Xo1=i*/#|To9zܴ3VyԜ|lpXeM@V2Q/GL}5S[1 㣔LVNӒÜ&Nd;J.&-WeNop9윯t.!qeH{|#8A[ XY:<ؒk߲gr CT'nӳ1s׈Cڙ|ȚEpʒJ@Eu^XTuJ^~eooץ 5Y3wf nЭrqq35ӧ>Îa:҄;#vE`|Y#עi$ZtW%n|NkQDj GN\{w$?po˙knA?N6ay5,xW9ތ8p/cUשּׁl~vg/΅)̬ jLPlmhk)X9ry;(5-n8{#cPȶ͆4BhXҙ= `Ӹc# 5̺hT?'9ӰD; rƨ;GN5I1`t^}86(3&"x]M7FyB?o5}-WTN K,#~ak$5dFT0Df2(V9du!Ott?} VGfމI8jIeUz~ڻo»(>L0TWH{Fe3'/3\GۅȈpw>*$DM2Z } ^Z(ae([=5_dNJOHY9w,8a-g֨ ȤF \^~l_T*@ 3=c VNdBRa5vT'}^."#cQeQ1:i'â ^UTRbJ1S7WUSZ-nЏFWi'ա:.YiϺ'-Ր_1|Oٯ|Ud":v|0o-BMSTҦLFP|T_q3޿-\9#pͺdAKS- 9vZgꥋIf$ofws쵘u- *MH KG'!n6OY`=gS1ǙQQ4!;j̙s:Fm,/eǁ*jiw7R/,,!}1W n~;zcK93YZ`u#HYhw)Q^7 ~7@IDAT\PZ̠qWwʡM!:xzU)T[un KOdl%ta&x_7VNʹuH,\ ޵/1ʲC./[}DzytvU;aZSHq>}]5 h#+gxDDѱA=zj!Ҧqaմnj:][AY[D:ItKFg]dF礠pJ2!Antumٞ…þZUout0"9˯p]`[b7P<hb3bZ2R'^ev9p(FGrj3"0j$L:/Twϔ*Nn+ouPd>3=YZ&fy%m#6U`fM2OHw x*2Ig\ie.s|i'ͤ*zs6YHڲ͔m}4 Ӧ_dԷ?.$U( B#؋rB9zڐJ$.vނտ$_d}H FBΡ:p1{jظa栶11<ҟ7.soHMڝpYYT~+on ?Ϩ,/ |NE4+j/2(ً:70xmaVqc+)915e~'N14̢*@#}VdP`vi )g h/]9VHkLOUWy>^? 7>Q ͬq[k4=<ɲs1xÕp:?kcιBqq zfNqҞ+޿v I&PP6ķ?d R¯q3`i716Os٤Ŝ „oaAG L2='NG=W-9 ;׾dvQ,*kRqͳY{/g~kǜW~sXO iSV3Rߚ̌4z;Q1nw.\B=ҏK.Nf'kSkDB2qLq{H~_J?i먉/y\PwQ9> ̭J,\?DIH_5\;#wr0ZV6Zju|f *[-leP7~ 3lN؈>tG&}x4G֎Վ͞ :_d#3L,p>JÜio'n /AB_6uC}Jq *ȾOy2 xzS“s"" EVQX"-~v|hHB>jqm\su~s(A.?jЃЌK*Py.ȳ.es|2K;z<.7Ϳt^82C%/]J] Ժ=[Rw=ne0I?W#ԇoH$7 7-cGZtN\1ROYp o#E *N.Jf-IrGve,u$h-XO/rp>2VۓzE>q,։TJ|u&]C]Oe?<k9?CWu}NVXqgjw^#՟XGϿR\0ҷ#wcѯڈ~C1;_(Z}H:ڌbdn qie܃$F_W)&[ER z*,yHdVO'3Z1:ڃ:[ɏP5z?fc x & )xaGQN,x.M0%- :+HYb\\3u ?Y7`O![d4SO>?GC.oFk|7|}ZHBXɬe'L]°0aTo  ش[db_}@:HE2&?sCS0SC2mWG`__xqI\1,YH Ǧ{i0BD ,q)%'ө?nЗȚCg7`KY×q#_`(±.'c~Zn5բ t RI3p' j?/S( 9N$()%n n[P^\H*@ҞMH9@M ޚ@TH x/:ͤb -)|ԹʫԪ}_ުu8XYt/8s{|v".U-{:oE[f&&ņ;џ m/pXbB>Z`ڊZYwa/rO:^Iw=l5C3a!~{9) aaec6>0 \^KZ<:f.~>ܼ6qoGP:jc kQn:&\ORf_$lA8QYR #NעR}| y8P^Gq6'9jxq!ɀ@Ҽ׹Wm~62[:_ JQv*j~ݼba_`)~AbU 8&K()'1|؈IhN X_J!3*)3m0W9$t12-dE}j-*Hy#"=,dvkLXo.lj ŎB^YwVm/9a_AARw3^H(:TٴVJݿ6Ҕ3E9htG]l)v C؂t|W 2rا {[ZW*ܽ[54ҧNm5Pc29=8AvL ?#f9cAkfD@P ҳgtz=oM](~(Y/ jn@~ b=x{H<سgkk/?3nfl!: zllm`BT3~e9~ w:C \nVs:IwLN9MAVrXV""T,wކ~XCq:9eɸifݟ}'wEm&}֑9}׼:_(r՗'vK%jthj[Mɜg_,z+0g$\Ӭ 5J5ûYWv(>6b; ƕW۵R|6e+;:,CC)7ѯo[f!=$n>IL։D2m?-]WQP`}S?awlܛ/ϼT]5E"x,;ʡh~N?wȺUh."Kq9;ɔ`pdW+rz 59\_[O~8E 54ԇ\$(x Z+ٻVbtQ,v@L2x) 8I\*`$U9HŠ*4 CG[2,Ãa^\q24?gC1%ؙ֓>"Bof2p*vUؙX/,GG6n<)ق0i a`1]o*σYZQC6Ld&C#9mnE| P|j3<7̸R$ڀ#NFUmRV)o3o 'nU7uz!EGkg,L|..?myW?m];iӽ'݇m^_݋<7ԼxB-YX\DGᖙR|<..QC*-Б(|;A~KDPZH$P *Ƈ-2\D<2[[zJ\OU,VFE@V3 .1$ M}-Fa0pvfF`WԊZn^ްdւlZ]8SB~;)ZrnrwlŤebm B-bȩ)@vmtQVL*)4ܮz:zٜ83;7g-ɥ qtz?p7iFj55Jlpt CQfčh8a#ӓmڔ ݻ ǩƨ~Yq5/6"Teh61nZJm">}P|llZ Ԇ@BcBe F7/)jlN`"j=C*?$P֬ l:A#GL K38"lhKO]ZL0 ۗ`+0*ٙNf (;Ra\Ce~0w H%6oރ&cwV+)ݲml7QxH1s:n:O+sCfGe Έ/)#YHwB=Q)$y|?F1ZJ:Y"d瞽Xniq畫I 遜tnN% cP+ޖ1WR~ݔ X2;#^heɽ(ї'Hь̄˼/F}}Wσ!GHFy0㩡:f((O(W2lmm{%.*ǖ_γ|uB`bةPۚ3멃A;ei:(3/yw3w9~'F6JXJ f\v9NDi2N28;9(6D9V;„Ç 11OS]|OuP٤@-?z9g5ֆTm:_h豘Et䙿22VSAa.G O? ό8FʎKlڰIS9TxA?U9 #GƬYvWq#* 2)I]%w03i[I,ߋcXq+vyS:3Z2ea73f99{2k3N ,t+Iijn'Uھ}d-`fדƊq `ҝץxFi$0![K/KCŏjk~5䝠&dF^}~;i]4'k}21`BŷcP)hɆZP7:WD((gFg\ld:I{9lH,|Y<4{6,,p3D>^{' ǓΆ3̥㒔nKYp o.Ya-,N޸!.AtA?`fk;cS]qA6>yX,Ggftu1K6vn1~f^32`ZH,f /*߫ݶYX(#ܳw?bGULU?| N"z@=j-ÒX|~=q*$U&LyNP[F-ĎX'xߥl*ϡ[U28VRKרHzxHpNfu{WPd:δ[pp3~l [J|f yS NJo)+.mXh%Udcذ8^U\OHئQ>/͑s$yō޼smHmx(|']w pV¬%KRFsN=Sk+1ٌ}C(s8r;M[,Q/XW}O@1և ŧe1a5q!J|_ G'>@K:j[⽹o1̆}A"L:Y^&)g3R+p4QJ*:O=K {h!Z,)"h>‚H+6$`Dl(_:QTRn\W<*9TD(;W23Аݳ\\PdfNIoFFfm)DG>{u#鈑écR3!/T9 -'4cֆ;6Y%)8Q#*9;W<$u/ɜbT;2ӯ,ŔP}ܧצ zf3/>4o'9v><)5uפyHtU-a#1yӊ߳yڊ,_?jзf_uL*61Y`KacD]rM#evl;>ed8ONӲ M}"s嚊4wv>WӲY)~@$C=: SoN,[|&(6 eى֧ʥI;J;z˾ATX"9'-0GJ^5>{l߇;FI ^wgy' t̸ M|D~9YEd #fX6w&kĤf!vp{ڻBsTOh(tϓXy?#gܦgM+~e+qŎ~`xWDI3Ø0z-.gdI:[fD3ӊUʬg>NqFNqbO=iPrVi_:YboR:=\;#&a/f\<$ 7R^Icou"oС2d {Cyz~<\n37xu_h~K)˒7r4H.t4ԩ=LjfnZx?J諏^s4t62×/73/7APB֕?YKmF ~%ԩϪ?o㼇f P+L+Y]>tٺ1o]ymfX$&N- hji;rٖ7vdoA uu1RM5 xxi~uo_XH{&eHM<r6K YoR֝ ;Io쥴!hs n~i'S7~ }' ~%È~ X̘Ilz.?!t%ؒ75\ TZ+NeaѢŤKm2R w"އԂ#=`f W{KjR$muw?}k9nڄm&Zy2>/ BP\]Q#fBUdn(4pϟ yy)sGtI|Nm&ٷ]r9|Vg Q"X#`E(݆rw*ct&뀘FS[ %uJ89c`4εdc;JJZr^I=SVcꖯZ ?v"8q Eup_Ufa|Vf & O͞}-C)} G>R]l\{O_`Ҹ!xG8?NxG4VߵWE،A[+6ԓ{fi2᮫ltɜsO>|͟N$DU \S0ሊ YfR(sxS;_k,f , sS^ڹ%;O?LelKaA[ȣȀpĀQӶv)>T \뼏W*n234 nbw%.Ҡ;= 2#A9_e 'Ǫ[$ _*=RڋAª4~`ڈ8$uerjYðIʩ01K΍wchtv'gV6 CZqԒ~^8~.4)o2L%!ff}^zŘt4䑒|-Xg]bp@EzvdE-\|-28H0Y7G;f9N4#[:̚oXC,hc )?[ԅ.m&%#zLCeq&G֕55𙐠 S^il+ 7y;cH74;0އ,S`Xd;C "c;mk6Au`>%,Y  ㍼C)0ep;X`jjk>d2u'ʶ?[QY0UeKJsl?ZIb7WdҴ3@%U_ u+nto9|5uO|i.}oplw:aEMKgBCc0CAw,WĨR1vgǗ.ĺRu-ȧ^%9uWMgwcI!qu*|A(f@2im=7٧ ~9$Zq7ds6?h' ,N#CfO/hUmF=KPe\u+Fj<KBL*dƝy`]w8e$z.+%v謼zNlBâF1@u _>Cdd~cmV&mk@cd #Fvݓ@:ֆFg3#,F䬉HjŦBNZnђ{Y((m5_G dZ0V|DUL%&`5 $]\+#۩1[PıK`Ȟ^>~\{wGu$+/ 5$o_;0NXQGgJAxLFd Z,^sjOjIï)ǁΈneևw @Cdb4ytHR91UXx)Dʎw`` JJJp{la;7͚Cw2NrQL|80`vG3>p7mwފ 9ݞb? ZVgDqlh*VndFMژ6շ'29P_B˸MD 8T?r 6edY]9\N=/ml+ |䣊L庿%X^]CΕuw~nO>DMM>q+~Pcc1zf"G$,*~I)Neį f̶db3#-nJ8V̙tM'= ujE$>]Q^؞=;ʫ\P{Iݟ|4)3Τw|Ckk+ 0O̴̾\ x1}1{*%x#}QC4@VXaӾSo\#fP ^`MJmA'&fY`5t"%z<~351㗼ܼV9L^/XUU2X *m1rs)h|qLFcbK>m{`XΣO>eVV$ލVa؝TX6vX;4#ꞝX?#ܾG,oYO]iػg7r3m}V$ki|%?jje9w`. ":VrNWxzι. e*4p`,OxZfrJyy8k^.D㱩9gq~̚:}AkdS^@Cs#"K罇7l~-9>fƾuG̔^AdY8!G,&2f^2)=cTuW 0ZgwP/OaO*/Z\:\m}RK;̞96{iDm+莗W,Y'}"%h2|(n/^ybuAKI؄h"˾D˱6M;r36n^[Vcī&-}]JD5{+RRPʌ#ɜPپj)vH@1 t;̎|V$3aPD%ƦjX3@:̖lRfrM&Pl}ks^s-JmQ_]E)f[aQXswdȁw ˾Be 5=v~x㖻;=#ޠhF (:Bugd!.7zs۱n(~bFz 3aZ :\%x٠a˱}z[0aMl%& *l`5(i¦1nXWj9w6ÞVkr/ܑlѣ {}AΩisv89qU+ǟ")1oPMFA w:!'Ћy;f_PWG=XtmUU1[0;?X!ȁgY×7 Gvdׁ &a8^nz%Vs^.e-8qO;Ind\JNOaGCM-13WqDzIt/˸.9/M @*}0%U6w?-?{at@-,6vaD`&H8o0X{LVmȬAL? * : @7@  CEE~ 2C36 1p`X ?2!ԝf3[O{{tfɆsijZdž %1ZJ 3Np}k6dzX# r>,E(F'{#)KE`{ȔWVZ`YԽZ]g|WQ-9{?c dkS#qZ6vxsU7zՆ6A/"AeL>>Vhzu`-mb1Ф*btLbhj {}oeȱLSz\,9Y"5^YQs`:mk^SB x3YQ(rFEwJa_Zj7TN'aw`;+O14K3/afXךZhx# ܃OX=v1d6?߸WaB{+:25-9 ]3$S.`L{p)Ŝ0f k͖kVS}K? aGRvն 6 J1go*23rgKuEKOGJ?y"~E37y XuAVg4;WnFr9>܏ö nߠm{vlcȸપR7cGY㭴ϳ54R>X)iT-зv)*ͨ@O~}Ͽ% Ǭ?B7xcnǖA+p%*l @IDATܳ 6uR|~j˒/Lj1~fc[LW)Yf2a7- T%T㤯"6teΛ\S})oRO`_}K)꘢yS1ǧ1;:q"\q34]s뮜<}毢dO?Klgjnd?2U^غc?<$Ó|J8tLZ7Aqdɢ~\JO>]gg8[ vNwKt9̠2Tcw+cOϯH\KغfّHe n~i Lu3n$ d^4a̾="cSV'OGeV6I"Hlkoq3bG%Eکv8a:FVN8S kFG7`Z9'u~WyWs=Un@%i7 2h, kRuQM"DiY%8{F0f^&LD 8 wA*J;X/|3bQҢ!5k m2atvR0"_yEldO9L`:R v6yWaΫox c; <38LC땬V*YZ;>/0bC)jrye@c˗-~j=~X8am p2 i(}^O o=jj({o53tߖ5D 'w%~XL&}}Y[D 6tU~pt%3 K82 !)Bl^gH@:FZ6(V$vщTlXYU4OC㺙7z:Dq)Vk ;xhjlcʙA@3mg{g%Pj:N˩-y6\njk)8fN CXҡZ@mKK;s2n}2FaPr9a uPٯ\.*=Vu)Gu*8F;Ál+M&/ CGqe9H\{4, GnET7jmA GG7Ʈ&6)2Ls=K3 Hʜw(s'Y%vF&vG`9k88tt(pscnYvd"V[mn6yub >76-b e^]CN~/*z!O<|nо}5:?XΌAΆT> *c;oj&0 u;\Wd¼f62ep,*}뉒fU[EV _{V,Z@[M?\^B]p2G"eyjHm{g͠t_L&1{bi܇+GQZgO:~LF d {f\koE?'kr%N:_֬FdOD \W1(5X+.*!*&#L2\ l{Ocz-)5I'31x`u`ݽ ZS>^pQ3V?뻟ǙAFJa&%<S8ڏe}xX?֖uK!coʼns fJhmm87ީl&}<%Ҧ >̋~V9߇N9ҧJ{o툻;PW; gX{;|^'Di+ CSNlzJv~~C܋C=g9_@+ek$E WKHxJ/cڱDi^y77Iw=o^ۮy Rd?r-,&+gނG}@;{ p6%0Cg'OjZ1#54ȸd]¹_y|FA4Np_x%tT,R}l# qN>hM[ہ+1AT ^ƱG`$T,N#+{Z 0ge5 9)Oaګ Cho';PIٰgo+~2DOSb s[-b+w^ @ws/'~;H9~\Bx`}4Bu[GwPoMǭ1n{lLA]oՁU) Q^yi1eyJKË/, 5QdG(z$DjJ*#1o-_88E0PlXQjU#Phe} ѐ8e[cQh1Ltۑĸ LBҀh즆MQGwo]tr,eFgy^v6Ֆu#SW0Q);܊uBj&Fv $A#3{B5'̄  ox>DЯWY[ X1C3X22=㨣 i)!d=:*ĐaA5ێqC yCF[Y 62)zG+mK3:騑Rw$Yg0L،p{;f]e=E7p!NfkX!ÙFWxl8V)Y1H#NzFOD+'D'1ڈ6f&rR'MFGљFmu'YFi FPmk-T#L|(Ga:A뮻 o3ꮎ 58!"P&tjiiWhU.deF:3SQ@J:n?3zww#F,PKBK}ڙәNuBK HqZ$bDHp:y?{Z頑O w>\TBԓ&Αu[,qf5qh 0woM-d"u)rX ܳGƸyΚ\'&{+HNhtY/ft+k!bFj,+QkA38x + A=iLkb3UsMt[ `M%w˨Ĭi啶gs/, #Fu2]xbj{v禩DkT[YۈB?g+a;I Jz2Mϼ2v*$)+N'\0\n@Vi.d1τaox2N)'0v+0gJBgLa2bkϿ9'2"lړudCQI~YbdbM})[P5dH:#ML(aӉ) [@\tnR TU{=/E:I?V́Q#47V]gGX<E!:6`?~<3sUE+"A^ ' $92&@̄  VdRJ[ @3)mK H\ ?.;.|t=-WHf+]6I@e}C+Lg2% g `r^|KM_XlT KƗgR^Uϧ<},f9 :/~*8p4>6UwXO]Nj`^`rLvw_7} F{VŬ*ͧˤG ɳТ՚{1Md.'%w dU"J[M]V~;-ց_# LQ]JH&:`HAbLZ[7%uMG+$Oʶߙtgr?E:dDREC7 }3 O>qc+H YHَi?L N[3&^3Q.,j 0[ YyDo -7RҲ] & a)%l[Lg XK -mU\qxo=S S ],`up8z& W/2 ???Cckj:::dpp!-=֌#fbOŗ_S' "#6lҒӑ{Rޟ>PoeB|c&(|Ldp"ux^_gOU~E3G)j ;U~ݮ~].plU.??{ *hٷ~UJ1$d@6u ??n2ʘMuT@$̚<1t\v1OMA<.zJ$*$<:u LcJOG~Ex' j3^EyԜw 0y?uFrj"Dž[ﵾ6$ ơ>ʠ#w>x;v+&[XTB{"tA詅ZI>W~u#ȏT;K0Ԙ:'RC0K}j #`5`F*FG'71nk檾LF)鬺Ӥ^GVdpBH:vDmlP_8}O Z3KX^= ү5Rʘyf<`Q\7^W#A'D >ؿz!;7/Ncb *cׂ"߁714~LOUo> ޝ@&ЮHDIGD;Oo80XxJp%rqC {1vh_6 AhH}=[^N,fsk٤^kұjU*E-2,m;%(bV0m!H{YԒ|ʸJ[z֔]j~(%\m+i>_bJL0Ǖ䞇碀 r@mi,gݥT^nԶg5 :M`=|t/,ߢTO7Ro_apJ 25^'dumjzV%Ns}Vƛ z`>nSKQ }:||;pյUp&Tu#Z=;uS;z ]^ܐXؔTw1)okQqw45};bTA6&?g.i[2C%I`Bk=wܭ )wG2 ޴ˊkX`ff~IU$DICX}-R0aUL<ȖdďUlޘdy/Ɉ$j7sўٵJU]VO#}E*ُf z 3"iboʶS_~*NѬ<?oZ였/w[)SgʸJq8 #FހS+cn}'w$VRm@YE5|vh Wj6#ҏ֔r0rS b 0 x pB,zagFHMAlڴ~QQг @S-LQEI5r?~YV˪T[#h) @Q=p@hZ702pRcV̘IfL2qQl*bUmu‚eNͤ5V[&#\9XEFNZ2%Z &T`D|Ȥe 8yr '= (1z%:N5~%^j;<X~8}W;:s)h iI $dpvC'˼Y]dlbdjSt7 K09o1ޚ̌)=sѹ >trMGj.B]]1k#Qa|6'G2q`C%)m f4jVu\ȶQ/ eW|u]=HDU#1TL0檑$:󒹬j-ײrM5|dڱ{_isX" ?v2V< W3+x(bH³ګ1$Q5gAh.Yxnިa̡1bŎ[utH;;("gtm[g54~_`OVeY3 B3p&*a6+jG.Can)~msLُ]0Ê$FX)\H3:8v}{ޅW_{z/Dcf3+/ O& "eN:{FF Ym/V|{ׇAx׮)6 nFlVѓ6:BGAl۸|Kdz[_ٳ[ f:Vʭlw 0!rOYѳx>^0kTIeU i4wZJO ǁ=߆e_$B}OdNO7O>D;ՉiJz3)eL1tLQ x8*A:~CԞŚիI!q#չ)XmguN;| 7vg_~$j„ٳfC@/TFRLoض:8}ݖ(HOCZ9Ekꕸۻo޽}B@+gaޖt%%:rMb|c@1 _7<`a}} ަ3p?c8U3.ܓU4 6wf[ZY Øc^硬ZΉ| ׁ`APXumX?XH#;4*󭹱^I 'K˥VY܉'8yEgJ.O3i%_0ňPagO'/{/ " h7T}f='UYUq|?6mPlL$&jI">:^oU&?v cH=;Y鵏QyRRN?^wˊ^ܮSր]H&O'Ig"YkDPnP&UPkt۞OL"Tϱ u5d1BᙓJ0ӥaiU{a7kŴ 2Bغ0Uy&/1\α؄un?~ ROP1FҽU=<'Jra!l;g0rZݢjZCDf&ذ {O&aƍhml 5hOc&NO Iba40:(dv-܆Es#=6'WVK*a&mPR#'N"ɻBSo̲n{ F&DLK>xj(E*tz&nt`LG[/Y[tF;=X#2 ̤~Z KVvnxjLpf0nmMx̊ze r.oAT1H_>a&8 2M,+2oXEw[i2-Cgw3-jُz|}h Hg0]̘Ϛ6*۔>:@R,E%ITҜ/u>\UVogXmΞ%`h #2K|Jկܶyadl&8i/$#6[P׸:&FSkIrX8z~?2'?"lMʉL%A qw"{/;ˎ "!^<L1ښd&V (q{N4JaF?⇅Hm^F@GsS#]& \&|>]\K 뎞z<˗DKS.&z}bdV Yw/XUc k3,xFX^~}Q\VܬX- {eV64ymm~VJ:˨.& 4III:A.ZwށMyoz~z˿XH5[i@`HzrlYupF=~EL:v27߯XǪHH î㍀|me )]+l7y e`ϙC6ju+eqxxIJ(P>a~zX6}G^N>[hu|k/aQ}&=(ՁA[5K1| I@&\(aB@ȱ37EX;nymj{nG$+~+Um&PWXdS0fxԹ&= ߓ &?4#L3=W$u+XA&y0# R᧯@fF6|]wsh6iVRo7=+~tɇxU5; w&k鯪}m|^FlX{jrm8_>xoA[6-52oh}0PM}[93p6m\+OiO}CecX9zDWuʤݏa[i#( C&P9%#'rb&mâ`CˢƖr#sR;չ.i! Q+Hލ2͌QLynA;i9w&"U׻dÚZؑ=ߏr Kf*-.Yͱ?*DV6 ލA>H=9{6K.>J {Ixvx ȟ_+y=BM?]oIf y&$4@>+$Tt!BJhooעZ&Az{&Ӧ18xd O-tR *ؕUtyj&Xf`gawC+c ڛð,:)1!Xd5:=aS~% Ka嚍ʼ"Ue-eFN%SO?߹o x:1^nm {vRV&[>oo**AC-~`O]]J}׎֗d={h|tiUc^M6q@?52˘BRHNNƤIT(w_g> <:*1 f?DI8LlA 2_;|Æ(_0~^Y3K`2?J&n[-g߮,:L"9B!Sڎ3|tĬ{0OMx}63--Cyvz1ҵ_ {:ms{]e'j_ISmJ֓e}$ەj#]h/۱ϰjois5^W࿢҇_t*Ÿ/OݛsmuXFPتHM$T[2z3ugC}R N(g3+y 椏6y(f̝#0M\ K?+oTq6A!lOҽ Mw1\2#B_Ȍ Je\Oio jtʪLNphCavTuA:="j<<ߣnU2$3G%UhQo@zS=VV7vbU+:YvH6 I%iO* SaKʎHwdhfjDCm x7î# "& %A54A1AfQ3l_Ȋ2Xl.:ɠΜ܉>a2~+O[ RՒ*gO#Cj`g >NjUСp42ŧ[p>Iy0v.+C$)R人3sv++S#HL]wFpWw[f<4;&LA4@U)|l}}O( ŲW8D3+f3f?Vߵ3qCp4IaˊJIXG!gY *\kJ%MOBk3)j]0[֮& _zCP Ɍ8̪!C7hL-LIUVbsk 0#}gU;\"Z2m{= JJvbCT <:KX08eV/:M覲B8ZQG@(ǛfD'ē#T!ܷv۽n2t |yuR1 _.\3v[YǙ!zm߾}@J؞~j?)W^Ė @IDAT4b KOo4c'LRٿ>d>#XϬs g_X6Jwc8;`f8W 3XSͫ2#RwU.̈H˞3M*؋Ƥ?dcH=sŸ FGzL 4cK~Zj AuQҗJ֚o粡G&BflK*˵G"_G>J'oOq3 ۛ?8L#H#wÓN!faeNJ&f?D)u>.=]J\a3025U:)S>M(9O-hi x;_ 2**N"0 '[Č!^0p+NcQbP^Z9P}G[R?@/* Ek_|:e~])UBJkwnʘb3]S*E9IXێr\J+##]f=MeE>Fo'Ǖu^H?WW!>r~k?w54GjivCR8Ҫ0 1Q杞~ZT;{[H(KA]:i'M²kD-7`0&VɵU8r,&Oc+i/K0}܆JE{OGn3g+{-sPǑUڹrRFټH\<gw0ZOX~#yjURfL=q5M&Pl& d*뵹5(;B~{Cl?жz[O{%O[Uwѵ+-6ܗ=Z]Tϥϑ$w]&ϒT)J%NWqְJߵߓp }1 NFɴ"=t lfݩt2H}ݎ;7p+6e*3&$\>h0É$3侧IICJkc$ck!\/>BYtQ3v%0(t:m#,j#;}|Rzh* rĠ.OEaLm꾍1){kʱldBFa#|h1f6ڄӓN!` Ac0h$S+*Dy ǎcMP|%j`sV@m}הUK7\4U7H ;ay/ ʄY>zwaxx`N&v&7_ i36!\>*崽s2BJJҘ>, 6C16Rt4ԲZAVV%e-E# R^2MCA@L蕝J mE:b  D-]8CR"mOFsB(ǹ4 ?N=vT#jXb! +ٍwpeea~|ގ7d3xwk}t/XebVH~}]Ncm۩7qd"<| ; [UW7 c;?`WQӚ0%b|n 3cz> I::HAڰu=̳Wߺ8]RX҃KXEeczXe$}p!Xy+}Ss KcjVڱ@C~s'+|T`-b_XW΃`&T6V!}{aa;u-AxSi2OVjPXOxp<| 8 \nJ&RzדNY&+geBGkge4ͬru *3e_MwtVQ<<1ƛ. Aʲ"_ s3|2.fgL,ԫ7U@uT')Ah l̨7 BÏris؏Ó6cuǖ}_ۊǓH~u^OOȞabZUUK)@S/']yZ1H{/8:Jb{pWVٯdKddRoio"g̃?ACO%)ڳixYpt .M7иX|9)+0m 3QK@.d@^X{xֶ!z5`k'Ծף%!.<(N2hu*AQ5Cu %‡Iz-\ `~^P r6.iE&)pv;%3hwHٛX),.<4{>v̝, D[KR]KV>V_ >X`؀rRwߖ_d `uUWV)"oO6.t٧תJv$'>ef]৑q(DYO^* l״J40ktGUEhcքm8pҎWox^6Ќ){jUkt.e:Vuu+q^|ϖ$F9Sx;pS"0֮"J>f;K#C3L@.Ǔ:f) OΤw!vDi 32-$l^Oz|َ(ۏɊjDTR)*I{w۶/I Sr$,Z8ծdb4==dNmͤ1dWBIa6̘fhnTʘ+IhlPR&e0k[2QjIyhhk1,Zw&B|OD_ X4ay o_==%"K9*őv XnDz29GlBij߀kfHKr= $ӕG˾U-{wT$%ϻ܏ɦ>'r }.RI\4X6y4f9'aOZr=5b/jg vnqӞ?5?uI RX(Sh:)Jv樂yqN!UߚshW:s3P N8y* m[`na3FF%𫨧fh)B7PO ttCʼt6P/6c2xtf9hgHFpۍ_.Ƅ;e=h9R78 =;/}AWgl<:UI:tš}hf7m( ]؇Y^ȥK=̚BЙNC[aon}dm [0yGb )vȧZGZ8Q2knGs/"ɷ`Lgݻ?j ȥA2 ieFȇ1e(c0V0 BQm;/;ҲlmYE, aj@A~j!H'0a!W`ox:KR7z6`B/fU gRU%a˚6C 7? KPxG$6: g,S–ut *1{p dTd݌m z-< oO:[`g:sd/ٕԅ5v\kMX1iӦzNI0d*_X=bC oq`O#WO] 7WGH&xsz G Li3gP\2h2yt 1B,Hm}ABcF=3+(t8|rZefB1 6Ѐʲ,Q@e1b<}mlv?C1zڇV*R Z _ M2ߥJnSr/}/J4ڻ 7'UqQd:Z~o}_xU Ye]]Atc dlY0,z& HA?~5nN3q~5;9WC۔ vZ'W穟S< Ȩ:IGwd6xi|ȊaʏzꚰQzbǖn7 Sϡ\7o\8Jַ8r߼ 2iTPJ@VKsBнv:ixՄAdV]VP^?a_oU4+:ťWڰ ).f\0x'8ƻ+ *I/ޥְFi*B*첟TS3S̺2{6v\g͎FS uH|$=aZd:x*།$ g%Z-3仅az=~~K<=λ|ǝOpi3l1|ꃝ ]~iyZJ.?n "da؟ROBMev`M._3_"^HL4Umm0`LNzc]@&U+JףzNqjIL IPO S۟S/(r1!`|ZW֪Sء1`@J(2o fkk5ΥOtj*H(Bʴ@&X7nÏ*\X@ՖS7Z 4$&,c⍷)T4deF݈2aAOMNI#c#pjVٯ}ڱa DD?TW2U\jh'TPN:9y]hXg߼x9ཻg#;`N.8pF{=%R]JS.45u_ʬ.o\wIիXedJ/q`[שׁki1rg|,q9oij򔜃&NefN;aH's_cC=_,嗘;4a2 vۏo.3mM >>g|Ev Z0}hm8Bݽiae8d|_XI<{_MnTˆu=]Su76< Lo}*0rI;Xz1]}y Qm]H)|kӽyQlA1<YUc2iw* = RpCuŧtªC[X_Qʚt~H11$K:RdVՒ:0 tIwD[chjFQX] :wF6;T|63J#>JzFtw HE[D&Kޣ´ػkĢezKTMQu[&rEQ?|&3vRIOr~eTVSs;j2ȨI 2Ԉ140f%MdT(rq~<%~V¬ V,Eȑ\tt SNA+`;^F帝uЇ:z65j鋃uh#)Jk*0,:gk 74 y>֒\QO/o*I/̰g%ek>GGw:o0u%.g _V)&]G*9 PU9&OzR}YQ ۷>LQsg+V{:ef31'bѢoҿ"EU@Jv)Rg4th ӕPRt'wSnGpr#)x!sNt虱 6֬>5m-v inضz}ӄk6cLL$HO@N2-/.HexO:Vv܂s4'0ظXu"0~psI`giB+4gD-KYLn-oU} X1+ aSYD>|Ktd"6ix{-0i(:HfFJ߻ƽ1([􃁾BÕGÍHujy}sc>{'Z$qYEفn1%%8͞)u%9{(l̘̤:lyOD:)|gOqb;G~%K \I%[Ex.Lp;*ӟ~Nkya6u-emEs`XBH7tp4,<]8}" y9y6Bpƛ& נJ-p J J1e 'cͪ'F("Q fq:} h ORqvaf^;Fea |\Y/DY;G=;qO/+YQNA0|,t2A x{Y u~R-y%kz}劔S0f2W A70;;)Ab56&fw~`/PӇdK[†>l1ڷ &6&| e*^(3QZ$>Ҩ]K ]cRJZԺ꼍?FA^.&2YLh3/ni2:Gɉi8`gӾ9ᱣHK{W ET̝wk&*63ӫ˺gHXMƖW0kܤUVm~/>AqӝG @L^my>mhm""ZIqlv-vl}^ޮWL~4i'jW>eO YN?FەS!"@qk(ӻ9 %|u.}s%MWOԩ&Խ;ҍ7`94760yV}n<:m1 >3';NRR $^U=03`@(&rGJ}7`z#DZ38Lz)sv&#Uƶk|ъ8r?޷co~t^6yLR'I}yA=+< k[Rwo{IQ|zRe~^(~^5]jl'E?f߄yO^wt ScزBMk;+2y+eo6VKsc䦧j~n|{y*&&NKA-&*1s`jz ʭ !>:5e,F />&8g`qv=iw2(0Y{[1 Lj8 o˳$mZMZI|7p/}A)Y-4~tn>}vJ?h%M5%9{YM1I.+YH}W^YGi=La`in/3n<\jٞb=mOH5()+d#vDhv*{t#ۗڀ\ku5׶~ cT\dn5Y%췽}U'KK.C{xcr2Fuo*(|uy9{LSeHbBY\;bǒ`?pϟ^ČyO_O*S&%{Z]=ag4mi(Y*M[M.PmTT]mӖ lk Z<5v%۲om\Y^_{EicP{O&<;uSl]y&HJyL2{;KH`9MEI ZZ̓Gy8gGֽPZnuKKAQF:>>ұtP-=LY٦pw߮|jwZ \I&/Iɽt&Y^ eI7)tyXgV~&gJ0LXmhKZGdm W9gUJ| 섵;S{`j`B}v@9T_"V$8 ]D%wVn4_ =5E2.D(rRCNG*icI߱"Brq JAC#qOED5;~-L4ޑ'EcH߻h,)!'Tem)ipL,ma{b=p6PO,F@SJE(dg1%=L{N10hͪ^74!+ DQƤ |(tˆ9*CH!4BޛbYݵZVE]v@@EM:BK1BDܿ? 7oΝ;/a PJM&c3‡{`e+~f|P6_~#1$r="W,H#[eNvBbj:,_n@7sЅg/eQitPcXoO>Nw{b`xga.A0\E2sATxf\ΖaM=;z@gՍr: ɦ WⱌZk/ i DF0l0͛ #픹ʂ`EXP8+H6O۟/b' C՗=YXs^:ϝf읶8RbB D%c<#q2ۄkYe-_Bye?ԝd{k$T.7nվNvK/ ի=_|-"-]lĝS ـAmF4raF+.>d4;Oޚ,g;'ÞwQ{{QU*NM(s+wDH+ Uj($ӓRgJi.~ ٤H*+?;'4SZG8vqsƍ('@UOzа?%Fv]< ٥F9jrG+ˑ_Z_c'[!bdA,av )f?}>Fcf][TA@%OP S)]]SOܞ+ot o1ؚbOw#ϼJǤW_x\J_Zmوm[HߙK[Pic"8}u,z[U2\sp,E*1◃0yt ;-U]̘6U. V$ ,ɼkغk z1\\aL[u7ڽ{Oʾ}Դ4 D`/LVd%u  k[' w_`_HpTT9|JV'sRjyv"|uvXN՘#rCU*ɼ@hh+#CTyVxʣd:J;p !dnY]%+ !;#'#sJ &jA'^kdğM+PIN]\_G~l皚d0h>OKW!}3n*Q`OocUƌp2da ;o&8{glog8wMÓ<{!c/_|cP8\҄_z mزt *}y[Ox1-!Y y/dk)DN^;яm3/A=лge8iM+p14z^lFM9ٹ} 9t>ݥY %Q5+֬[K`}ʫEmI%*J>$'[Q=/3 ;Z_UV#si(vTKu/V/xiX3hh/{m-V&O jǠH_"D :4v9 pMRԵ\ؔO!|ԮRH;8ϬNOVbﲉ3^N7jT u˗*x+j EOT{w{&w-lmɤ|K3g fwXI`=HS#'-xIj;~l|)ozxc\-,ZU.yGTij7P^H-[>58G^omOb U)e_GW-u{"oVw?0s+si+Y9E\8IuzJUۺim;i}T!Ytrm%`%vnYK[M-CL:Zz6ԭ2\"դO~Y_\ZEqfa >̭-)|NrїNF-SaF)u{ldzi(#~wVoA#*H4]џc+9㵿,6"+797m *gPp>{c Yͺ2A`H8sۄiH튤Zd #ۓ8FMԩ h81<(]DZg@uʔT5=zP} =? j,UⒼx1{d,Zso?qS Lm.<1}X+|VZE(//`/ݴ3= Xxgh-JꑛvqyJ` D6b3(1'S0ǍGJ%!Cv<~2N߿́c+"%9Ͽ ̈),3fݮgǏ61$@?l/+'OR*D$zxa[ z-wpwY36<3J݉G 5V*2^Wş~,jAIʪ7cIUdw,PBZ_%XXR%ߤ)c{w܅*l]#V YPs17+*s&%PPQ{N.RXƤe 2gbCd8QۉXo%ѹcRΨ?Ӵ=we{Eq(_2_u[a$ɉklZUt~|<=QC>톾dKb%CDD>_Cm6ULmu1:T2IK l2({8I|CV粲]"s=)p"{ҳi=@ImmW__dDs21l9tso#]Ø;i aBӔU*W 税^\l+%$ɋl_N:ÐLБBHtb,>]h1qK a\C :Z'* 8 (:FJqoBFR*wcx1xqH*1BC}o0f_/h˰A~E=.u&{3 0`9tBsknE~: 8ueab6jƤA~V t %玒LƝ%,sd'As#%CvxZgЬ . Z?d  t:p X#+;m2. /QAsŃ?9מzB1h8v뇬E,D=M)Wb)Ȟ.ʏqK{U}rw}_,YArϿᔭM5|}BPWm2i}Aՠv%̅)&Ѹe e)xIj"y:>R rڵ&bI,ڷM)_J0uϧyK9bmo@1-YxIjY(f&}_lzH>zGRa(}hg![vGʾyԱu7sP=ѮOwϾOk=HIMFtA+opY%>2^(>N0BUz;?;ީi8jRNS-PYՄ{;lb(/lE9}vE`FrOɎ̥I'z 9?0qBqץ5^4ňA8{&+YY6!9N!6{l9.pmqpC/+ 8 M旁&xїW"BLEm1% TR~+ڿ|7. ׍]0='$۸$?aޭ%˾W@D<.g6oڛѝ@Ɵ! 8x1N+݉^a)Is Rީ$g&'5|9|̗İ̭)Ymoc3sZ\}F (_|!5c 1gOxm{7n[kjbbdx*۴,|f1f]qq*ٟ_Yɨqavnnv ud:$lZ߿>+:1}SDYzpی[t,GAvc.?1 bZ껑lth jlI YI>|<=p)\svt? =wNw;1|Evt&s;;]Mw> KB7*t&Lm. H[?}"z[ftG)p_f3ޣ{{lX ~Z#M#sMtlGAL+*Oחp FNЌEߞǒR†@vYn$I2CW5U*k0 0j\[Jd!>En pw3fk9*)]4h(Ŧ~Dԑ0hIjM񯏾nGowOiT 9 Ͳ =|!ۢR VwH;M:x~Ypb'N(bQΙ$Әh`\Q4AscǙwͦ7c-b9;Yׯ)8vF:Ҿqshǿ}=ZՖO5uDVa R'\,'Zs(Zcx!<4Rs`Oޞ.8`0z#Ǐ`ʌa}|]o+ϣ޷aJ01X0n9-n2YVUX q3tfbE8n>x(2C18_N :xi(}]~2Qo:̑|=);4ũSg qg--D >~(tK`#$%g~Do8ga۾R){esqV&:;RtM0y(eA)0zԘ=F={GYZvh&V1ilΊLupS^dr ;2'Xh .Xߏd  څcOJYӀN෺2tau<{}E2yIˠ i<}a(}nje{<#c( .1|&cŗ)u vE#{i.]:#,dJ{t?Ubͪ}*U`54_1z`=W4ڡ$v &c pIe)㏾XAA Ȫ]O~chJmp<,o@yޭ;C 3||7nؠ8\8}[7*_uA 3,8Vo؈f(3-i(d.*6Ъ2s}_ɇ~i rRa уl"GWO[ՈTJjb `뮼?..Z^LgOvܰ1md߹ tjiշq{c`ǭҢ| I¸*ZÊ e Q 2~o{7=0 b$%1$m,s;XG֭Ɣ;d?9;{QJӼQZ#rՖ݋`\mo޾c~EU]#]Ud&m%z@t/}m)b|?c5q!8+v)20;["\}yؘ`Hh? =)\RRqR>/C ÉL~+.<%4v"aEO8Q˗L^ :ǘBظ BG@73Q8|8]- V(Ǚ\3%V0. {PZ)DIn{# *2FЌ\$ЕIg"9~?#jL3Cs&.5h[rI<Ћ_SsMwzصn?r8'v3">9RFFͽLKB=LLplqkA9Yz\;~}s}+gx:gSd#p0*K`N'^|s0QGi~&8޴΢&tކ{#s* {lfE'}+WXT~p`Yn4ɢA>_Xظ6#e!#eu4:C &*jGV|rq4,hzt3Lֶ@VUUOHsb>(;1^ӄ+Fj3x@fx#ZiqWkwtMH!47V#ާ>Zפ$ǭ Z83Q`[6m[ h&7*ں+.#}O>`UA:)\CGvY-RGIrn$}JWCR\gOߦJ_c)a[GIr5qyWP`9NkߪchÒxΌ5O<8rC ҹvr*֭*5mn.sHcDt'P)ކ~XgӐDv3~ ݙ%9+KKs#xJ$FFuB"k(DŽpI\2k*WC:zyCkkcJ9Ȥo@/w*}lHqO1a~$`Fh!4TÐMـҺ=ca0Q:;akochH?U NvVjC[hU>ct |sFFyx; `sC-mhU4o.wyTf,y]{Ț;g )%\1J|f#7螨-#XC]s,# @z : daA4ltKh?*`xXf ^0>[F/l'Y5~ ew|Eɺ0n/kiI>j) kJv\ػ;pmbQV4Bv^%=b,!lI U0F;5o7lw ql  j1LEU,yZaG_ӄ` Q474"6.'T*ճ P$=0#Qp=(JUq^T^!#!hhxM)S(o `æ0zzOpr84#`oH7NjDѷΦ/K҇y}S㑗>л//^Ψj6I6!cKlV3Av;Ul`. JoIQKϖagQY˂itKHä2RU~rx)Oituh"MkJ'+ (ՆV薯yHapPj mwLXAɱxsn%Vr*6j>kA:haJe&M :O`Կ?[o{GABۧ4C^JοQR&|1[)!%Ȉ3ש=$ދ2),.ϑDX՛>/Ǵq~Ni~inu `@(NJ[ #zt4<{ɺ$%*3 P d F]=A~}ˋfLסBPR߈ lٲˆܵk/ƌg()pB ǽ }(d3 @?M \%gVðϞ cYw\7 CF!9"~\1qRGKb9ٿaa 3,J}ᇈ`\-ٰk= eTՔ:8L#q)B< Cݯ2suvmuQƑml*/UqTV.[BѾRmѧO*IuenFcymۈÇ+$|pGW7O{o!hg/AA!$_EE 7^'00UmI#jVWڛ= 4aέ#1x4NS˖ [0 ZJ|a+`vtu"P$҄#lXk̓Dv})+a+e\X`7 dŧT]p_?Ä?H/Ʌ 0ߧ@E{)-Mox*t*t`tDNVkD?BM_"5QZ;z.x=Ȓg7ar*H!:l+glE؂q<|Mqb\LMðcaK\ 2x0!ICN;u&֬]lgO[SG²5~O||%WUYuz=۬fΙ_K:C9 ={73^H=:J[R鏖̥|&DYMzAu2eҶJطwJaa]z4 /C6y(;+C/=@Aʺ^p=Uwdoټ߲.mURgiq0EZx+yy v+rk x*y?o-k `l<.~q-jaT-K1^zr~zjmR$64,df5ul+9TJ}+rutU&YJֱ]j_Xˑmr{ IZArm]S7S}*OXumؑ򎨠guQRz=>zIl"6#Ğd{ vBu^<[մ!r 6ֆ-sݑdSG2ijCEƤx"4Jcd46sV:W$вW,5<0' D'_:vsVRcH<"^V=é?~+z]@<>[ߍnyэF}$GsŔs}Ib}D3g3kݗ'K̏Id1;b}6}(ud%UJ*8Q~)3{F?ͦ{fJv&3qx$7;cAp!ǥ?.!* 9Eq zyQ֯<$&2Ê<.d7b<WDlM2̿m<P'؈M/mUsZRa0=y%A>H7s4k[NKщ`^mLHT ?0T\D`MP4-]? n>=)+gcq|QkRK+C[&boQ–fB#cɂPxi{q<&T9ɨK0҅ 4ћqrE/eov*͠arUy(1T;v,e MवiEW2j߶} NF&T(e܈-[F0 FX0e064:QBmj;$`i[{>.DЌ X&O-Æc_)2@o!Cux~ذ~vl])!U]BCcaKQ2kQ'=c\־7a(R>,/5=蓜b)A`5sqdWo8;ڡIMOrT#R[f4aؚ #5yEdH,lI-|BQ :d(Jq"b #}4ܴr(o_1I[( _R_ w- k[eۄM'cf^pȪ&tRӹ3'N APN)ɰcQKYvz NNDa-8vׯ]:FCTu`(dߙFpg/?yqeh#\Xe D2{Qm蠡={v 5رHDGS+ k߯XNYKOx`cTNt۲-U}})))tT̗WD!9)* jgqXX(!X}y!lC"(kű!-vP=Raq%WȘdl-LVcΣ2DgC%OoXPcppN깄snxtsMU2À}Y:鉱pm,~َGcP$s~G w|ҟ EGV9mUf{kFob:l ٦Taشf=;۱G' JA#?w-SSR oTѩ CƣO8fpU`$$"J)n"蛙#|3Z>~^RYT 1#x}mRjfh*GGr S]?d& c3;. aJ:M:sJKvCau(F^N ޵YEj=kno&dÇ |6GfnڣOVaz?ށC1c(އ}? E@0Pjؕې/V86C„VϢ/ytJ++o'T汯MX^Pط{`o#{H%ߚ> ?d>jlП},{ scǷSTTULL8[rSv~5Μun%cU`cƏp=ioSaҮ$4&xnjG! M bg{mz.sG@ˎoW]>z4 KiAQ%],]h UP i4TS+M/Tž#a$b *CدCjuL/H;]\:oߕcK.H*&venR)]ͧz9Ze6%HCz:GG{.d"'`\Nu Z#T;✠ 娟rGshaOZ,廔/t'$ɳSUYu>͑7W_pswqxR[g6d;y.jؑo!t6v2aU/l;TCB[LxH% a{264J#2{LQz* ߬6ҩ=*[z(n}Mȏ8l?hx)PXR7css ɦRgx s"|3Ptwܺ&5oNh9I<=d![/f&R1Ԇ 8FÈ`f> ox0| RSOK &$HfȺze]7I>ٿqW(M Ʀ44H/校ɶ2c},., !j@ZdIVvj2>bQ^\@/zjX.mkgK6r|N)t¸!dBYV #POO[{v?Ao_Wo%/7n=Hr#@+[W_Ra+18'O\܅2~ߓr+y(@7+Q=tgN$P Ȫ{uUd* >nH}VrEn{^'w_lٱ}}=Z[^(Yzm1㲁* p lRZ̧~+Lz5~%/ <8OTTHz2]x: #FOTm?o`T[Kr~:SvP]CVe@ &_}EJNQNOVV)"Wy lؖm~,o"0֬Udi oqZUW>_]fFEë=H4:Q6;bCf |VJe *e4BB2d>^9d`QәHå 4d#4S~ #?Nu>:xH˳k>")!}\aXD`== DvFq1lrW/O^gg͖?4%Ky ܱzALGȊDžD`UAw5cnj[6CY~"Ss Pi'q OI&K{ ۮGxhm h*Uwן!?n[s&̞0>[KQΫ9wK_,>=uΛ) Mm2>T+u^dL!NIGԪJOڙq銣!NuSbwuI2>2t&6Izb|O4c6߷^_ Jʻֻs:_ ug*/8'90W5,DaT;. $RG@팕BoMG֤Qh cM)NP◪Pu*1tҙieK1RZ N' ]"UvQae&)N0E 8ЋfbzSӮw?SyW]+`Tq38Z@nˌ;uŔ͡,Ǫd$S5@' jk/CFf,%HH==e]=('&)I}>q1)lfEY{ 0x ʛq($Q"cgbc\&GgVU獛(!؅24,3**q1>ڌ|N ;r9&1F9-ރ'>A"/ 6Pzv&Wb9uBiQt')FQ'Wqrz9.w(7+0XU!;GahSȭC~I5Raٓ`׷7GÄXi˚oe׽@IDATBtG(;tN}?"Ѓ2 |rjY?HN%#7G߁)Rͭ߈zC;pAi<>cGEォvS;i+9R-U7o)w(y4 אh8{SR~hmi3XղyIN6VlھwpU\AlHQ^^v{ݍjbLk4 MD&||P4&o޼7Ν;3|;#`Mc!29I킅?rv&S(FHgL:]:ӣ܏eFҺPF}/ħ~Kg?tgf D:g]e]3 Zb<ͯUuXt &q( _'nxyB?ٔ@7~<@c_5p$0Ȍsgl^0&vŒoCfe+YLlhl#4R8wᄎ#ps'R߃,;lI}%104-> bQ&+ V)Yv K)XsOsonrPhh( )ǟ=sr)ÿ﯌i2,x338р%-kаlɾ t~q-WE} ꮀ5싍7M%KOV%|Z`|y|:% ;@eljGY972b|뒵^r֮XAGҙ,l#7:8Auci=ezd&J3~a!K&I~O_O0] ]){bdaİpdRVVJ#r+xx僯-`i.IqIXIyYl̘v7:(9sVEg2:ˆw+ Gs1ŽC;WG)3=s(6)C73d HOVmzַ$fhoo*0i81^Y6:љj0܏Eؿo7za 3љ L]~#|2,Hgȑ$1!ϝ8b: kvOᡌJ;`[IcN"?*e;F3='1l܌VgJIpkuvUbI2ϔOӹR0aCL'#ɼVφ hkvV-E)cQ**tdg;,4S/P2^]ޝgי`V`9ԧ|(-Xe,$kun+a1G5@%O9jWzV,ŤE E.o& ٢t9zPvCxgޫ8wso](^xmh4#/fџZ:s| &lДOs58W8l 4WORх=}CvIaٞθ9wUME2tS7t>|̹tIB5孫͜0s8ЉIOmqc/ΟQw<%3GAmRݑ[XGZ΍tc8ͥT*`|ntfÚU97Ŧ՛y=J;I:] )6('Xm!<ȤPe~Da1Z.UpBunX|'amTO&+9wԋ'ƾ 3keDS[7@}q$-dz|D5 Ț5e0먴**-PajK;{V #?ONf| ]SUdm7޽( Mwla<ػ3CKi]zhe(wBh`;r8Mrj^yڞKtKMl,o'Iٖ.ti,۬7kiÔ|@EM0e,ꔥ z Lok@@:N^%վGC9;Qr3{jz)N ") ڏRt/}a :R-+iɋNt0є]Uz]!uiG;I^NX{~WfomV)ӾMAּ+KMh̨W6}eFpHEZSLQUqœC5Ϣ6 u;l\5}V6}N0#XlL)\|@励wJYx2T?GK}a㪝ҭGs ui;\m R[3iT]&uw0XVF^#`Z BӁձ4&@FŔOTNSU8mӭ;F}7f1nW Y =em]QFp ~~ʹ$as&NWh@:B)S{6c& )Oi'Sjҹd}Q.`aWJ~%"O%Sq[1dk9yr cU\M<Jxuw^/<4|:ۆ2X@1066dHvŧ ɗq&\(ݚ}GϏ<ͥ Z.ƨB`iXd$z81Pctva2RPɯʟ~AG?dH$1(Ia1%Wma8b_㳷#L O ks [\*=Ws7P!l3B15T*l CmX٢4m8Y|.[dec!#4c7ð<}xJ`jLYjJڲ8sUƉ`eOh xً`۔)SCά,c0s߉La#G4ԥ\p[&%3 V ؔt" Xi%iBkC]PXd%}#_|e}a 4tS;=w]tXpU> ;i iԀ1c<>s#*D&w @_bӦ+Ǒ6q8_d3ؘ02'5O<*k8 ]$Aiq(F#gba@È@FVIy5|Ki4'0ݎ/Ә_-K/hI2Rmh[YY2g_l]Ξ Bc{{#ǟd]e{IiQWJ!J&\L`R*,(^Ebr%:\s8$녨gK\ޝG0|t.>` h֖hPy%S2Q3O;@)P$1'1p8o Ʌ|m<"yĴ eyo%^uqqYb1~qJ(LEg[aԨIz2&&;vZܗ;eN3# z%_K„!憵kxo1k}GWCI8~.z$jS#:Lv/D!=1G߽`~Ʈ>MI`HUX%=jWtD(F^n2f?wTec}OP4)+l>Ёøb8M^2<G燘_1fN tilҳTUөY1k{te\5 S=gPLwgؽs+/ӈ9@:`(]xucpP(,J8J8PaĘ1f% $[SQ#N|4 R Mv >d atJ_R64R 9Ѷ*Gkz}v=9n-߁ k:pdBI TY2X/>k'7!N%C֎ ડ$Hd\32J[4X:sok6&CK(FtDQBzBI[I*Lj*cZj뒹JJ:)>2>S?~Rn߳Ίd[o_©0vx  2b<ץ,b+ש P'IN zrNհ1$ʺZR.9N5j'yɱ2֜h9/&:uEnVfqDnΎd:!f8{{ݱ8c1w1!s+5n<֮^?S{`:91h3p,ga-QI"$wCu+V(-ڦɀE_(n^Ĩ vm\qcc*jN"WTddLݭ?A>:vI)Ͽ_C.ƌΞHN:Tst0'c5A,!c=J@aXjV[Ww*ΔRu)*_$VbX~0"hJpfvfwtc]߈p$*Ls֫CCtbXO`:TR NȎ00O° dg6q tfֆe> [*o>BJqU *87u_T{6W-w{=0:}a߼yC,M̳6׏?ԶVvqze~&RQlH4iQDIXjҶJ)v{|')5j9iCUR)ZRN\D^Q5*$Zؾ2դ2*!6+`AeCNɺdG&fQ9NĉBT>r$qT8 SM.C*Mw'`epNBJ%LEoly~YIʃSxN}>f)όOo @ڑeD6KijG;Fꬥsz)ˡu2FY=ԳSSn) Hɸ@; )!Ͻtz>)޽(ʣsr8Gv~5Y4^mk)^㛚RU*ov-1Ka *)';]q:|G:LE$W44K$vcrR/ܧʩT]UCi 83ӼT;nm:RyTu&dO9 GEpK 9 o?l-,/etGk'cχm`HNߢb_y ~=z-9ṿ?Am@ÔhٵwRrp8~l|?}_s+R&Ì3a5I(sl䋯*ۦ7mr.z!΄*hhמyCP[RI~֭_q);ZY81dd{g3֬?^x[[##ÇܶA)]F.5_8a/'@gG/mPP#Y;q\W?6QsOvHzvg*aOg3;1CDٔ{c./E# KdK$mjeqB%?;%^L󷌬>$%$^"{Łf 8gƏI9ueSc4Haiڪ9YXf-0_WiMSaU#T RY'pHe- ͥ0?@|!VLU<Ƌda$Ci_G$$RUMٙkW%1s>/O#1+jt1﫯bj[Z_ # ɇ1CJ#"I_$UP1d  /8v >3KϿHdѣwu,XzٰGƫH:ZHQ硇]G5-)oiDPӔp%eMW7?>.}:2;b`sY{Fv0'#y×Fz5q!wt怶fj|ts׮>uZ_-ɾ-+r { ܺ󥑑PRsʧa'8wΟFpmj]}䷀ywMN/W·'.Z,2.EԆcߚZj^< u?j ' Ǯ;Xb>l|2=֚=$ uWf^*sG0Vפd?4mȹAUy%ӈSnWlN`th~:ad??5}E? >& =F4t`G0 |7jg23gޭ~YH9]9Glgu͏" 7m^8\:& }?󧎮c5}9> ]$h1A:ʁ1_W'JǠ1P5NBGJ#l k==[CFvtL 7G0t+o%y-I/r^ʟk_u7f/S6֩f :7:l$[-׍V2/vuUkzMV巜c[W1Y[WO{I)7T4i>MenTqM$xki.KCY'׈Dwnz~j(:eMijGQ]R;?ջKG/TJwRVivHI`^:^U&y Vܼ\8J]zN[s06o$=:e2Wbq! IFbb,%(w2RsQVeH>4 #PV<3#.e_@=p,d처Rσ5g>TٸH3|B2d$mݙsCx|@::R†$3맍wwA:zLb歌A!ؽs1UBHvw4i̘1vDK&6gK&\I!1,J_pRRaXn=YMơa<?T}0G-.|VuTwt8m]8Fp#Sr&+6MQ1qC @SzvgLJ|Hqzb 5XpB#m(O&̸aٳbYݑ1n79rbleipX_|o*|siɋ] #)и.\A#v̜roLdEɧiJIMG 6v6%-dLxZ PDO>am~-2Uӏ\#%kTL,?i&_O0 rw@c3K#%`Ǝ dߒ~qJ(Iߒ`66̎:=fM}ie_ŌBɶ|/ !~=C}/Er~5ga5ܽ;@cBT8K?|;Xa=%Ip՘x2NZnf@۲BXzFdp nLV ̐j%hЗu04 v8ЗC0[撌앷ަz45 87?$wZ3MCV=}_m K? hHJ!͆@{78z2GyY |I|O db|h̯(,GFF>%)_v÷r8'a!xWq۪J ^d>6ŝ?4aaJǯ6d@i'6O:eMLL T8IiRV=-a0fR$)9QY7^1@YL|o2iN/o7") (ƌQ@VJQB_ V9TDlͥl%>GBЉ3 28 wl0hP1(e <ŔlϪZYۺV/KsF+ȞWΟG9z7W;%%"/=@*lxrL~PC6cs犏>"[G&dD Nz $Hႃ[7 MٞHE HNcZݴw!ٚKΎn5lj$9>֣a{k~ö⦭ٽa6˼a-Оk|3m,YؘKtkQOvXd[cīo*咩 ,ۭ9S”95jRY%쑹e>e(sU-ͨ.5sҩPx)9[g$R uشeM/1P"H9 7e0G5'(M?o믿72]g3$F729/N %ى!euhd*,XjLC=6X/aiu{477wȘɠS*9Ʈ8H[_DgWSX瑐e7j8$_y! w)6d֚]+-,Gȸ\߾+~[PmuX ;thp Ci%e"=Qdw:HAUq q).C&W/Qu HntQBuqWY»{8Jghݱ}ZFivۚK$f+{S8m7[.$.mk)m:3TF/(iZv׏G%١!ZUq^m0al\[!ۻulF_\RKc/U} ՑO0:P팢(dZq\f[US S8):(q7)R+4yU'!{!3t")}fh^U;^Ou hQwgo%vbQ^ WFՠt*Іl>n]a?d$De2j Zv+&yOeA\)kVϫڴ5uMl:~dؘ*#TVW겹erRNy+Iݦ֋ԯ5js#ekܷ͎Ӿ&r?R>}n9K doSr()6\ұm㇒{ K,DSe"{ni nV&6`YEB:07]J9}VeiZ>)f0d_18}ʎC@YPSxY?L XXk('-S/ d#j,j\iǩ"5~$b)]}{K 2ƒ+/L`1:yXUYI9V63A:XX||v8eܠ4S=(M#ðjwpE\G[X[/7B}U'.lk~QG9=Ƅ${ȔYHɹ!H Hÿ3 1تJp",aH aީQ42O-WI,x 9Tꆫ)-mo7.9Qti+Bo=zwБ臈ġͥ`Gt XbYdt\ufm6 V~t65cw'AVOJt,Y,ԣ,}4iɒ=,lqLұ3z2h|{O]i`3G[xtf!\Ly1/"/Q1[; f^j(%I0B=(hcF# 82ޛ?Dgs degԣ3F@#z[ʉy3H|c#d*ߺD;\rkO mS; kcPk+%lܜ=ke4 oR.m|,wlmN3QjwhhcmT5\4d"{xԨQX|N8: a !Q[l۶[;9֪=bRAn2 K:{XH4>uȒ=/}J5 t'~PIl5;=%Bm|=SJ(ƉxBlr*c\D<cNHxL:f2C](qՊ8pp?u pU`r9gNSr[ Pd&4X(]|D Px c6ơ=e' `>JEhNcPƿHXnNοw^d~yjDG'0g;7_3lC{؞0X_uV)wLU% 9n2=4E3r≻C`\_DPޘᔔL5 Hs?yϩZ#L}}{zhb"^ nvr|<'(dr ¾۟FxޡrNoC׾c40Uf3 n)3|θ ̛o|P|OO@bq,Afٽ2SynR0s` vN*6 bzR`P}G3gAhy22 .)Wu>chyƭ׸TmNeډ Pm[HI^OYy%%gD2UW^ eO=XL3_<`ʐ]Sf%i]ggHZUSڥ cWJ'KG'GO)vR𤋮Sm[}[[l%Sfs=^dMS`^_gF%RSGsnu^If w/E%4//Ui Xz!I-}d&Ust)yIg!9<]t5H_M7‘^7Jj񚦼\+9W~8 ;w~ qler~i`\v.40"L[!^Fk\F_to* G'.TJ2ngAY Ȉ5~YJz(N.})WB/_Cu5c< :-͛_Dn[Nv% ݻXᩙp0qG \JFd |aĖ'h"M}imF)b/ഖ1 i'0o3d {U{YVc?U-WqM%Ꜩ?W㑪ot ٨yȾ2| Ǝ   酌Ɋ"itoC!%t,&\A0'% ~{GgXk&M1d`ƨKm8Niww 1$h!q*<)|Qtw#4pdЖ,~ Fm"^y~6431b5ɵNtw[ ڟ92%7~2T-x`sƌE >ɑV7շ_[= v`J-=ҕ=80cO N~w>ݺcĨ1(1C2J i!<xgyolĺ1hѨзgޗc3Z nO?T>ez5)N1P5ϸ ?vu%ɓq ҀiCc6tk+휷O@ݞT&0b ׎V$Y=,N0e.bWS,")l=f|fL/'`|$qOzdK s> aHx N)w|sP ®қWB#b`Cy(n0^/lOO)՗ts ,wjE5*zd\9o»quEٶ4}Th 3.mYd9%:]w\;Vo/ yE8Jg:]:ءlg5_k,Ϡ#Cn V1XA4cW,ȕ03__ frvZ7aR9~A@ޛcNu_uB ;%&j^}`{}$xTV1h|/{mO@.Ӈs+C˻΀qL3Eksj:NJRQ9Rk7l~vWa$s2':sTq16?'s*a8۵7eh]ڙrW+ 藺B<;+l]Љ7{sz`&}vuqM,ЕT)EdB6k,PΐƦF-=D^[=xēwc1ǭjr'KΩNY+PG΍knwĞ K:30%zS{OeMāmK0ᗩ6Ș׌YjdEšt s^L"Ѝ 5OC5WPMӨ`? 9wp\:⣢&5V|cp}/NT)oI{K,] #amw!j1ЏHsrT SՖp[݌6zSTqgx :m-uzHE@@'֡$l6Ƚ|؎4Ia:qgGq&VƘpϳ|&4]MdYջX~fD`wF. {\7rޏ?I:K?Eh*|ԓOVʎu,WGw+٨`14Lؠ=*`&%iכ{j}ݻ-y /0=MVs/K2dv~HI5Bs`rsE^~9,1 q7kV\M^|BJƥV &}j:aNJ=Y:J*ݮ6戾Q:0S ͏>EM%xeLRI9CakpݲJK{̲R ǪO.L}lMv_Nmyщ9z-r]"'qyz^FI+m5ʡ͊@Od_u[Ӽ%?UR:=~r#UNoDC7->)cr$$˦PKZoCR{W|Bs5.݉ll-k) w\-uHHC, \{ !mi!1.7|' i(SP;iOz+ Opk':iTicR5˹o-vYd]ob\I4;/uveڵ~#NǙPo9 A0lڴ4MM*8Ey'I PBü%m{Y%.P}09bdFUڵ3&N\Pʬ@IDATH*@UK?rhcdQI)!73$U|ABzv>7SDŽISߓqHjėۻ\4#D`2= ܩ.vh:1S j9a;1ߗkVyqfvDg}oeȞ4ed`wMH:X`՘4>Ÿ~ezA?-*'cK}8{v j&MF=5CG1#F7 d?Py"65ڕ+.# cא=ٌ\Se"}Î<<적, 0'm1o-^zѶ=E#̞>cv韛qPBot]+lghE%"h'g=RJØ10 :GJJTOS:G_fv&ؾm;Jh^^|Bo2ckm6'FG|fc|.{Ιm_o'y mة= `|^X"(${8V` +"2.fLdsxE `xQAT͸@@x9M;|$(o8ɿ?Cm ֳE҃hJ^8{ Jſ=bѐGQ]LX޶tF'}0*BrQzkt9hsTYqp޿ee 1=?I:RZ=040ųO=?~cD}>t!x_^\*JcflgZJ򹶠oX%#3KYۢ4*x4еDfpXjWgk0V2|b1TZ'LY(O_d߱ S/5ȍ^M,IM2s'aAG6:}zR<~Y9aT|VuqJ]vnyq{^h&_%éZ):|V߲~ [bi=þ=|xd!C^g13fJV:y0Ā;R9lΏcybc9d ]:fN[APPʳ'(RɔP!ԣ. }Lhx?ӓDSHJuE037_~I<4|˜n:,[-^_!HR?**v~;" qtF*Uq G% t `sХw&4es۠ME>xb8CuFӱҿ9WN!vS!P-ruAq)Up̌џ1cWZ}a9+¿'jjj~6!YXD?S!>0M ECm ˪L:eOs .N=ӶP2zng\]8܈;>JѦ Dz w`XNXq+EG_?GhtPX>ˋp>2퐓ī||t`+ ahY4Rfo\< AxosKth?Z'{Nd :]L|E)%+vvKˆsJpoLV8${q. 2*FlX>KQfpjMB>jzB +.eYGPΤޤ:`>@Ye/zB ]ue鰔Nz>I1(}Tgd&+ !U?\XXI X&NH9erOMܴlX$yk&d_sJژd^ @[Af=zk.Z>U%bg&eYJ.Ql",QTEͻu}?r&h_ԝW%l~k-+`oOSrP(r*- /ڈITrhiZr.MXQQ)15gkl-& uG 6]DUdQA{=VTq",;QBU=5[VCiȘطg78)E\+@Tc")uV )J7a1|/&93&HʥŇ0?av) Es^GE@,^$12çaAYJP@p1 {cԻiúuXEiZ4 oQ*f!  vdr^̠@H|Zy?ڐCA=3Y-K `U+ksɳ<,vfGF R٘QqCaE@nUbHmذieۻaPY;)TU:9dSq܇{/RVtڽO)ΐ5w7l;a/{0P{<؇b찳,՘9_)Lc['O2L so93PEY aBFr{-._A0\ܼ=&S_5"ŽA@{k82KF~Z"dFF_1,CFj"Aaܠ[Xu$m 2$WOv/VWW֗ĶZŒ55-uJ/ϾF\fl`m7ceFQ__vԩf$J;zW-S-Vb ~^d: Brl!l;%%_D!X\t?~V o]z-ۑe϶ܘdy9Ϧ68vx|fw8.oaʴ?ae.%cϬ0}1GKylX.ʳz1&a;X-ƴi~Z:Kyч3f&h:14 Ԥαd|궦KK9K'#yP KLp:y+KQq$0W69ąKQ(ã7ٶQtXLUwprE^>ؖv팗^{ QP*t?W@VA%t8{*}FL~.͹H!gp\@'` %vjӋ8w.@d Xvw@f8Ȱ8~Smi/|;oQttw =&?qdBPZT`xm /#A O{TqZHp Q[MjgӮvSQ%5QOrѓ؆u pA:/W Fҙj2tAgʴJh:!>.7mz󚝐v.$2Afp#"YȕqSYv&X;E"FBGF[Z&N KW/葓r݉+=cj{vXSd.xGؙCvfwźcy޲E!<|QYyH#n|=G .4(EmuxN.aLBH!|FI.NNUI ÿ;r/<=yrO*IZZ_r)`|U`@!r$;GK_Ň FCO{^C}.Ȫ*6J95[@RZ[.7MgUØlvjWj.?u\τ 6nENM2w5͒][LԲКqmsiʫcuuYCi}iC@*|?z>GaD#/Ie *0>ţFFA81wC:xCG<$݋o2{ߚ>7mAtd&)LL KNv˖ɤ V1a`2 Ӗ7KrAx"ɀkf2[UKV(c #S KJ*%J1'ȩb4#@%zv6M{I3xh 8jO/U{ W. ; 8NaKRYÇ2059Ѵ1ܮ._10ěJ[ ANB%>EԳg'2cRozغ\`B$CN;1FYOؚWzg fvLp6%Ԍa8E/SzVpbɸO AlզّL\?Ө#+4^`ʔiәk494{K 7sSN] w,V9VM-h@#4:0m '])S:n=;h0wM }R7Ɗ4ӒrTx7YA&k<ڟ͝ 2s͏>@pd~2O66!S8{FW˽{8nݮog^$WȊ "TEEGC?'BBl/uB\ ~G`)(ٙ?d|UБc0z1th矧ccx`WS0Ti+ pȢ~:Lvcߏ,{C2%}[S?gz0CB>sy"";Dֻz.\xzޔw7^FJvIݎyVΝ;+(Ea q1Q]UD L:ĸs$ C:I2N'xgnN(̬,|#R8pad[}6zA RM!`Y=8Hk֬š1{\,[!V05Fs T}',D e>Amgj5 G' Cnj4o)+a(Q>4/bĒϿPXŋ?\FVJKnŴ9wCm]ڛo`9  7,WMkcʪ?yuV`͇*stﮌkn8=LAQ?S 1;Vrda\:GgXo|FPX05kQaz2>9~H!;-4n{.vϛŐof>Co2XX}vn8)e]{w3T=;O>K  ע4H H.sq[:u`U׫lL1#4Yτ ߥ1 B'QƏ`dX:lw8gQ2N]:|(1׬^/gTsjIXE1}CqOs,~Eci W:Za{o cd%ʚхϾOf؋nypq7+@F*dC O'cR|/(f+# ~زx)MYGy/D5 \:rٓL 3{%9\=gH8i $)g8=Pӏs4vM5n(+)[~fLX2 @5hҷ+Nt~ X?}G(̣ԣfMWΫ*ohE4yV8ztC1DJF6u>o:KRGb#0RƲb, 7$5g.zO(u1g)gO.J+ uz؟UGuOW ֗nW#h#fx:1=PXXy`+:}ytdUӁ>-6Vq0>bCY;{,}%*YÏFі8i%G (hş|cqQ)ڟu~F̕O;~L % ]vj:l>j'ߴxu_Y9 9EM[GǴ|J_`Og.tlh<x`=D5 ٙO' |+HR:EVl?O4כ} <69ʆ\LBnva$/hl'ށ!z+yKg1v)|kRmgxo_~,E+aY/sV%l!a0ɭVatj2~l4+3I$%@FX5 & $rj O.UN\Y{tؿpﺾp#5?3* jV={ 5R:@J!K#,I36_:[ܟ:u 6mBvcQI&CNwy"e?qb.n{(.֧2beReX#i\㄰=ՊZG=8d"(.oA Csp!:Ɣ ˺TCYnV1N٨Ëp$lҘ MÚNјVrgsE@R˒wv{دjl8nNHf:]]8L0W>PJ8p1u /cǍ>2bЛ,mB- U<^~ } Âz!0?J'C{Q7/2BJQ7dW!Ugkq,rȼ_W9t(GS-RDb-p3U}Fcu͏f{+I6l?X01(g)3eF&H=S I9RZM)q`Ifnܷg*ahb]9I6dQߘsەÅU螩8v2 9&4nYj)6:O;Z-~=dլ;,, )ޏơ`%KzVlHIr#hȠR[^|UŔ)k,8`,~zmr8t|2t l?#L `Sz:zM@.4(nQwӀf _?e[ hi|]ܐGi<-:54vY:w1_lٗ?ƜLJ!^ "MdVefȮvW&v%ٛdm&w^1pXn ŋQAqoA7YjB@$=-ßF\ѹ /3lg[ilbw~$ K5!r`TUW#@RCRV9eȝ4\Z`뺵c#<Ȗ b~X=LWO`ӖH|Ǻ/9Y) 3$N߲oD-;񔥶3cPf hDrq !ƀtgKP< q1Xeۉp0c4}1cӉRdHuX3NcWYny0X ڨ2|u;Ԉd ܹw\뭜qmKE#jruvR~tSW+:}G4Ã=L0?cPo2GuEaDxa XU+%'̘E!FK%`6Lȴ ~ݸOՈ`_ kH$K]>0+)vm~F0o;P9A%Z flc]ؗHEy!xT @ zOܵ{=@MS _87Vd9Mjb"Sq{FJ(y??%|=bl  %#[ M~d]J}EUw8x VNIMbOI=x~=ۛt{nY70JuvEF6Y4c鈩k4GhRu⌻'jǍj[ә!)9>L+d(s71pJ޼U`U=c5_9:3~ jTNT6ȪG QF0seGcv}#8q,,9QSU8{d &YQC#gbbM~dqr4B?zX nwG~. ["oc ҨNfKS?00Xaoi3c\Fx 3t9+; _B:3 a;N"jۏ ܞ9ބPN 0ωl"%jf O6 %y pDZ7٫Z+ۄJA.'li~.tf+ġs=lLZ`DKk+"#ˆ؄n=zS⸄9h*Ñ=TI"XvdmK];s5҂?k3\ʕyF)5lj@bQ+p{ w`:1x5|R=V];~y 8} THG,"bȚ@rnF ۉ1Yًmشv#IsW7 [W[چ=aU |wܿrSTʃݍc:6[I0;s)|X8E{T0 .Q/%m޲-\UNj>bJ:xa3=l؈_orX̞4#xq~_EᄆEP[:v8~c㺯u 8k؉ylD-TA.Ts[EOmm*Nu\n|ey*}W?ݴA^dg0i|Ҕ)ӌY-mxXN ֮) VI*c:S|;|oi! =>{7m.t9z- "5%čmb ]\SFFtPnrqxჯ%Qe2[eݡoA&]_K4jcNb< "ȥf\I@( (IQvBQ;F k^Y8+ P"u6qUpNfMS˘C{ߎr긏_vYJY]o_5O*ի!\ۮ.S^}vwQ}@ F!ɸFQP9MͿzCI>zv|VWFiatʜ?ʍde8gjU혴Yzho߬=J.V{ilI"3@UT 'ǶP^f:h d'">1)' ă溄auKSz2,i~nyw-&Fခ5B°@:AZ@grzRܽ=8tQ0isGImT JFkG8͸ן e[q^<{#FookQ6Ʒ2{V2ad͛Qk+H:K99xb`߭z CT2(Ls.WI?SUy !SeZCHKd1GZQ Bymy a(gTl!J:x8A)`~N-uRfj"Q]J6'ᾧ_QPS(j ɚ^2Ȁ8a"ALOJxy"1 S~3AR$/ mMZm %&#j9njG)ḙ$_}?d)֎ z3kZr2KIYpH?ۿmg!*QF"̛;G9cXO_8wEmY#\@C!fhߺPc6_&LӦb1X\n2#+X/j#O4-# t?J#"pk,lcTf$VFm'j41qHCI)4ha k6nEedbt t_2H~Q0?RxUNf?/\Uw @&!7#L;'ͯ'$ĜQV"X]*xeq^:~;; Ɍ[Z|x}L9m7d`ػ{+l]~=FMGNA&=Ŷl6_2wiH2b3 ud rS: qn ᇋaoA&CF^@5DF[; 7?cDD{[;1rUdX |@Zv6m2h܊?LjXC9s [mtJYu3`l9})sEs$ڜiR;il-e~@jHIaX^4 |; TPTl Ko`?}=r\:;1Z>2#?Ndr/RyA;ef}9o&b &0[[[J)33f*B"ƒ~uC ɀ$k?z^緟fl e"J:T=^~U8LKyPtm%Qvٶ;=t](zͭg^} o V~7}{' 鰨(G||*B3bBttw; Și$ ?&Tt*+պe t dE Ʋ=8yZ+AS"<)o;c\[qj[h <+`m&é+3 4KiF={$O?£o\M”N*Ŭ6pȥw݌8:ƗdI1YzYU(m(͊_P^qq艏} s<؄hݱ8}j"y=__{n8~Qcg {RtȢ 1>yjkAd\)IT1ŔuryAhȕg%sT'*>+S÷O;ЪT#^gvՔodaRiibl؍c ! |SFj_]f2r옿zbԔ6NvW'e[N }ÏK:T{U?qY_CAxc8><%.=w ҉\8w2TQށȩlUbVͷ1mr Dp2Yϣ9|(]z t g+Z9h lba~ݵw([|ئX[u2*t@ƚM¯]>4uՅ3>`9%1jl`݊rT>/e|2)ǐFz.*U/w; RhO*-bjy? 3;dP[/fӾkz8O bFaJ^z `uϱuo[<rgU^>Dw2] 9v:SFRIĵm$tÐSy8Frb*9^}^F\L$%!6 9=JKYim _jNk+Dx$=e~k#>-}u:$/+>Ûe)';WS7^ϕҟ H/qQd}q)zi#:K:_r}*Z)I}F}^ l硖}\7Ldy7PUyznNQdJRUMrq]'$?^wx\WtZ0 ~>nD )z=twy|0S2Ą[XvC3lC !QaudDN=]+PAXCaxoZk$Xa݉޳g^o\2fDbGԔBNI7׷ݽ'=@J%C<՗c:49P^2XZz͎Ĕ,E`o7Ƥ ؂p G̱mNwhL!0 -3`x8t_b[h$t`_`}(8xae85r>[>4^'/pR/(i:e]/եsR*mk60^[iÆmXZLojZr˖=t90$"DQ9(-ϥ ԕm}iU-d^&:Th;^O @*Ƶ4̪$C{O vh[Vjֈqe^'ESޮH~7, Br]Qp~q@(ת hrd_Lw'SÆ̠D?AkCgXtQY_vo)pqIr"N`Ϙ= |E'_J9fѓ&C̡;r.|x3lxk9` p{t+M y 2/"7n7(+L`$Ҧ3|ǿcx0Z*j:ypv'qlLǕ:bu5*g>N3GQsgxza杋(˸MtC"54bզHayq!_/eH*d'sN޴\<β"CKWHsnd,@Ȑض099t K̚>.NW&jR-uZ/@c@ޅ; ZEX:#=a0 ]W};MQQ8yߍz>9ނ3?)RUj;մ;'Ap/ŶtN"ZР( (k\[ļ ȑ8io_cOWTI j8ƚqz-cNF!A߹#z):D)W@]_b_ tWs6N%(6E-KT:Wktcʏ_A]HXK2q2z 3(Gϑ;9ƚPL{ٜtDŽP_C bSN׵sq5;Y_1\cyRbs6Gc iP¦Z;ʤw_?G>;d?>~N{I\v2oG\ntݮvK6*e#VXGS A}Our5`)a <>'.?V@WS]٬Ͽ3}j U{zs+ף{|G5>k׭k|DOh kƿ+4 Վ@mFk/(ADidvjg;/_z:qkFZc檬S']4@ 1y+8F,/7щ)r%)ܘ㔥ue'N`&/kkR=ABkS=~lI쮬/k/[sWN|1U0sq®-1cH,|4TO&(0ah  l}Ot!vvc%3ڱ ;W\[iva@࢜&Gc4*,R+ SMF턋d=mSJv3Yh,Kc840-#<ہzhM\ȼDIC1rKeyÙG5A i&(7hc:=.+EmUd'›Ҳ!2&Fb۔UlSQM-[q,0 O<FN-ېvC}ƴ^ksc ^ zN:{18F^4RxKh]qr; ыe=ovihg}WdƲ ?){w WlGǎװyd_ c`UHg aoKo̚=O?NδSE t yK+Mcs6 1w{*tȁd< Rj7ߠ#!Wd ^83cKpI@k%\u チ4>ڵ ~5lA5cCc(2/>"o`ڏ1,P<įNY1Ȥ+3'1{, '!Wv*e<{xНDh#{4S C.[{v'"saNA|]ͩ46NT ?20C ŷ_|867 <&/a=k*sqiH9XV9~0eF'[vp{jfl|X(/D$,#U7~7boOaʕdK^wq!=b㲏ޡ18 `Auk$W;XeqgNh0ߋ,rcr8xキwh)NvËO?u*W.*(.|kYő3.ydv0úue8xտ}|!}{C7# + !tZxͿc0CΨSxp.A×,;ѷo? A':43UB/ɂlLi~+_v\S6j}=A.0_>x7n `l G@_%W￰'`|dO{u_Tmf @$KIbU qpfVKߩl:pd)9Ǚ$JdOf؅J+z\u^AWCk.dZ#50,\Nذ.Ƒ}[H9M'V kw:hZ1qj$ hTe.eiHվ&ߪ]Y{ zYjW1uټH7FEVF8`UIK \{y>bGՌX^Jf/~=rP'_1r *8qҶ/')f>_ EI@]/A'}˫])_PA[EYOB>RVwuGϹ6\܇-+@j<եg>//!Z/d#@bKjk܏7X8g^xsyr ^s7R& ?ujiPTYuҐH'|T邎@*i0`pw-I!w6зZrj&K^$O-7\rA#ig # *ɣ1r2_P'+R`C9!gz؈j ſ,] C1LP 60]Ѱ@4ymH,2=PGvES~2Jo?2-W1G㠡[ QGⓈܻj0p=\g #P߸⮆cma큨Nqm4̘};f~72+ޘ3>bϿNxQ*)ƺgs7pQ o*$ٻJ5:=L4Ggö~=QHrB k7#Ɠ0cU &>HH~ /-y$q-(Y2^ZdB3œxZD/FN7xH*aN߀(/́'hN(UwΛal}wHa7+3x9zC]Vo~$%\à!Õ8H{x9+AMh)@9?%2F+{嚗!Ө=k(=מdMGD9r}= *Ȍ޽{+1Sp 5To(:ϲ;>&9MyXtvPRV̻N^h42fkw,z,lCqxC)o7gw݈<BWٳR fAyu1,h8.%3u {(#:rhc;671QhХ1Jnd&آRdM5eAIg‘)4_ģ>&3+}uD ӧY ևV h-![0)u5D.% ^ hJ|DG/Y,HID 'n^tJF-ߛ[6QMy{; 77B烔r`⎖:M';U&;ԍPVZK(a̮cƓG  (6 bMݻ `B%Go*d@zzW78E;#wP32T%o 9i HNJnwÍ@TL@|<(MHC~ 5a.AèЙG {k嬯[몊k; "㮅wDx~mLi;`ue sI.NBc|h9xsŋ}{+FHW;JZ_|F4Z8-fJwOdgaF?`6C=X{fw-=~Ɍ#chlc) NI 2KjH91A.z^njh 2> :J\+}H`\1SbĐ0\LIklzV7X|;bS] ]Pv1-{ܥ$C3caʥ:y\OQЗq,H0*31{av}dsyl߶ ,8 oG%Kc]~eϾ+( FPn%@gV QɓXGdcENEm8r txgW-IDžGGSy:o|CKa Ta՗J07}?w4.0zeu=ۯh2qL4W*㸸 n*'p}ƳԹ6uOT05]J/k *GQM緞Wsux:攔Ũ¢YN1,x'um8b>2S3}M IJAJ\e`ẓ;A{8cbvVvϧDiS3yjieK=ػk7(k؋IL\#Aa4 &s9d][מcN$B:$#|ӵ>}6oZ?ԕKytZ97!Y2涮Liw.yy ơmò+\%.&X5P-S zdo$,8G m YK`ɜ_mZp2%1()9߮ePJkKa⦺*WH~TkUC_N*mu]AT;M1iҗ6GNw#N`2h X,h;koZs0 wyà*qW=|Ķ~QPRaH#kJD :s^( j$;vA/bog'@(;e` <9z5GS:ga?j\;`xJw]K_%}/]eDUI9ײhх"}db[{\U[ڒљKF\-~_+I:޻6ܿP6^s~[>RRfj)eUaup[2>>oZlod)#94!WK\G%)k*}YrYo:FmsI4ph4h#gUF:bŻFu#'{`~vP$X5r5L7+ϽߎpwS8,e̾x#r:vE+L޼f0aܘt8@Zߺ.a[I`J>Nd nP 3'h+> Uڌ44R@2K/װ􎤱Lcp)яɑ^'Xܿ^$U>Ɖ4f\$I>5Pںv6>"Z1JI{!f`({zھ} ֹev{ʀ1(Eihܽ}7'Bޏ2 : {rpm$e=vwfb\s :b/*1bI4d/}>V傁{bc x=fϚKylBjrJ_PU3ekne-Ϧ"vʆz0f?7>Q+~9F@R%ì{ѿ^!h' .I3oSl(3Ʈ'c,3]Y͞,.1E}g!~eHBfg3шC$eFwd@h("0nދc;awPMUM'N!;p'ƎLFh6nފ^z f'@dΞ9WXϝ]I<,[`q]LYzq]#E~ٞ<0/>$jiec E&/$\ݕNy9K%0cFq^YY 6t+FF* ]7wO2`A krZٍ@[N8əܳfth"VQSnz4gaa8w>zL]]~O=("B{"`8E ƈt_C1.[YTᇋCY%j uD#o*'2u&ֻvYJY+)ztRȅw  k6B3ۊmbIwsa!mv"{_nBeqPPw54Bwt顖@U|pv.*Ư% OMZ WdžΦ Ʋ:i&"bxTH|8U,,!ͣFtCةXb_as2{^:x ؿlomA8SEI=W/D :yۍ oey"?%J9|.7cԺ}~t-֢$#+|edU\"Az@'_P8?4݌\!qD UKC2ж't=u"2Ah9_߫러pU>46i.cCZ8:swsAeM-A*:@MkO :zH:bqwƢNMfUWVQ|ę/MMчŏdƧ'g /(njXƎ4cZy8 +;WX3VwCjطH)BIeLD&gOGaQ.ġ$h\J봧'v1ꦌ}L|tp1#t)ϷiD/?˗*62>ua2᳨"NB%'gW`f5;O3̂7zmg K>-:)<7k xA]~; =: NwMX9DPvS"V 3وa?\oZE{MmZp5Y/#-.x865’wV֧R枟C^jR-DMuQ;.tdqSoGz6 8=%&qnƹ|\~ vs 2X$Y=xs$DbKlj'@,e}o(1B/vU|pyeXk8A:oaD}6<Ǜv >+ӽ+CpUvUH)[*(j:$~t:ֱ]?u!Nq8S558s#a8RGa}Hܐ$6Pd[곴e~Ӏ Nn NZͫh"Q+s{h;)/ iWMmrln*:*'/Mo)Uevv+{HK9ɱYRnGO[>:8B-4JR#{:Ar'Ȫ#K9ҹsN8_3[X3İk'd=Xi8Q@q*J! #t>+y1j. u̫ipӻUgn:;9VI&h+[su`$oQC.yup83F\ itKǟ-⨱5NCyw`(N%1eu)eY%r;0v4r:zQ͏ZЈOén?-d# ^j1ل  8S"FO . @Fѧ4v[3Fn*ë;+z)KN2VAMs;VĤ "}& K4fRF1=3덈!z{b<,1$0qfJc1Cc-಼7JIw*""X v;MruvsZ]EDE$o|@~}^;.[~XYfJ2 a{%N2O1flDzuK5!"67+X3X [[=vdEM˃ s~; TbΒh%@š`3Y"  qie&=\یsl'y_ ~]DH(#X@Z#ޣ}m2R],ŷ?|O y1,zK)1fy}PgH,ol1;/N!h*=$EpќӀc1!Q#j+VԄUUȔfXޟ봯C: D=C˻4U'Xc@ Np(Keq,&blo*F--p:u9( i&ADQm>ΧwߵY J0o6vgf`n:4€J?4-{5A-u|θkFhWbI$&/ߵ"6Tz0 %jyի>:OKwR~{wouVO}*+{ж.רa6~ܿ_p͏ڪ -lK~D # :pp`-"\gv>y: 6ۚm4W,* t Ycɴ Vd-H5517~ZSaW\IVr *m1AvЈ+ 7v›>;c#&L|2пL[LBqukH0kda_jߡў ?Y?M+9): #p "hIXJ umRZR`+pu%K\ML{w[Ʋ+u v41?x,>p|^~ز};CdUU}ox3Щ>KQ|O.Ḻi< КF=?|ѥx衇x_~uJП;kÇQDo+-YY ז2\zffj`nGeނsLYLLWa˶]TY0g!TOs/XزdBOC'^+ߠMzP雂}a?7 hK)h]҂A>R%;s H !VQg^ꢚʖV}p`=C:9GOԋT=eV<3,k%Ygg7:4ZHzs?n憟$ޤOû+-OnHs+mc7F gĴ l;ܦD1\^۲pCĎU2 |H؄$8H)cE C!/l.2(=$,ru̯d,;JIOuzF@ػG_{cԋp5tl($7 S2 ݚ;H f\'ռ*96b`&z`{ia{~ #tY6@l%Z~5+Ne`uˎ7E9.?,j1d "YVɉB(e<=G3cmQ=I=Ǩ|?2TڒSu쫴Hvm=տÇ ov;_0z4|K^Ws{%Ζ×?v~W,5cI}>l߾d}M۶`$nM w췹"xQ*kF-7Q|ȧä )(Jjs[Qah +Z,|qv25%g̎V9.߁!x1n8ikB%a>(%Tb &-w.DUx!=YkfgWrgԵt"=zOfluF zdrs8wt ьOGEVKttA0-lbi{ s CWkCa簀_e-q%{QQ6 {+OY__oU`}gy[-_g{ٻc3"ϭNNR2R~>G@IDATQg=]y i1J[)3g#*1g(xk|*}U_Vڋ*e(j"*0mF Ӂt jiOV~K!u;b2ۥ%5Rle%%0\eP]\i=܆lz3q.$(hs}dT$&e?}|NgHs%Li =5 ?x?zO\MyMѧiwպs;FʠZr_xAN4qr|6b:|{9`̹A۴lضw/_y]9|*N|߲~5j\ )59T#g8P6e$xt_HZ1V[N03e@PנK@! WkB\R2c2+?l2(u#|4N~K a vT#:+X(VԳQXH t;?C q1WL-CGa{F7u+mڋK/C = *ʽMH#;k ^A0m mʲ+C.SƖxgz6tc2w%Xsa?|Zݩeq0oa^M*G]1xBY[7^f`2S"A8(#hE)tacGhHpJr ]A\z0dZnA}M)˴ly}7PuG~m}J:w/Elh[oYFk1B5dsk 5Rqσg` sXGPE_TgMkF.u9*:_&O *B}CMy [4 Y(d?!pP=Wۍ,,J/*=SǏapȆ[mFR,Uzf`,UHط}ǐ _/g0ť%Ƿ%%$WkOdUk.S谞=Eޙy]`$ &}1r\ˆ!ؼ{fQ"RUy( › ]_ok}i z<2I75Z>6>TbM,嗟]n.4s팳b9Lƶژ0lPlvPg 댡lKha˭Ng'0s2ð>0o.;5gI~xh 8XaμTa1[>t0Cr*B@g쎝2M=#uIIqRHeXzHU۔z{[珓R8,M@VIbBN##?Pcll rvHGNIY8c#zy2ڄ3Amq.e&%?\H@7ΣAVcpovmyBc{֐^lmT@gr,8vdZ(kLA f Q6H@K2}}@$ų=ߥXH]!u;v>m*~M\s2^cE k˳$3ײWA0b=2]E >Oj^Kkk I^j I#_C: CIyܶc?Qv`tЙ S%Ϟe,oy8?7d- 40#ulnǙ Kk'wDJN̻˺١DZ_GrQ嚺i/؇vegNא+-Tgw KTk*"mpigęRm{UjιY+k_W19f9^)D+{GEݍ 8iO'Y\Xa*@Wh\TpG}}9Vl]U*WgE6Pl hRKE%TR2 VIB#zynVAjkgezӮnެ½ SFuyp M>H (Ն_UUͅRu1'`z@L;AP_^r`O+ލéƨdLRR\FbX_pJ2ܻɰgBpc̍OY}S2Z- 9Z5j ]!,JBLHIBEM*uNa ɸuݼ} `]"j:eHMe r \| ok{@V\Z:! [66BEG+ )&x۹Dƾ L K1``hǁ}T\bݲ̱מyrPM}Ȉp{ʺ!2VV&J!6UՁغXSN8g>ۨkEj6p o7>#cǎfGو:".X&=J[j=4ATSrڻ>*wnĕ&lݲœO=`:(9VKq~&.D[(NoN.N8Gv]r".\R:x?XړZ hi Q3%$D0ȓ)'p e"`0n ՇuAx5<.3W4~Y Z•viYEe$ nO'38)2\n^x?DUUqQx\ wQetTƎI=m9Box̅ (B:{o YoT^>sILj`~VXbY0ʞ;HҧF_tXGJ,y'ڬ6Òar*>;z<ş66 &qibw:/9*V%Q)224h@_]:r3X@0bU @[K^YظCܛ P}>qտNJ4 bWU5..ŨQ!l\U% ~{{]u?*šD R~j*Fv|I4p/?|(Ͽ*%>T5U8e`=')^mu7RubY ~hW>8YG;C+kb]t "! _2p}ma ۷hځɻlmqMN2$ Κ5`έ[+g|PTK%~#Fƶ=GX{31s*ޝT#<2}8J.E6-WmF}*2?kI|'psa@Z6.߀uS_<|`-K=SJMf/VU](/Z1f$Q2_Tban>o-5_]mp0ܻaml%hrp';-qzu-;PM.EmYW23Փbc9 kJJ6 r I,$u14D@O}'@ȴYEvP9(um;{/ן}’$P9(ٳ/\&37S*5u:nX.$ E]DTUޅ23!3A!_-5TV*!ߪH\u?[r)F9q-䐎7ۖ1uyu?' C).=6P#M0}hoy>(> 4>BVOS[7x4x4 <ķC.3/ޝb]Db ԲঀJٽsI(s(ʪm$sGVLe@x P=H2ˠ}wOɣؿo/*g°ە9N:Bϊ[iANI6/aRn $34b+|71)zxw0rCzVϭ $qTUA\ rPiQU_~^$yq@!hͻ4D܍em+o~%G-ODיXj3C*;%pwxloxg"ifq'+lkF6g2'q1r3J_IӔy\5]=t@tB#u„ |U{VTytRO+X¾…4WIr_ 1'1eU7 *;)62 /GE;  ,g]g.W'}Kw^mmł4 >Ng[mUEo/}/ӡ=]Dz!d0eI+H@%E]M5qK)T 0`6m LMlCy]nZRȫӥp@ {*&=/%1 S܍}Yr i vՊ ŝ EOr4R~$#OFFz[0ѭմS7TscFUM#Ibvy0ΗьN~U$iEQ)1] @pr.Q,#ީkRԓǕADKz9K<]5+mƑd9٧L}[هovHig巬"qNzvkAԨx!lzr~RP:]9< ; X:^}W"N% 8A6> 1jf-ېW2biin7 Ps']J]y1^•e*;*:košlZdġWn"E:1I0V*hXժݨTlU?JGvnñjrL:ѸwT m<o/SZr!'3FXmZs8dܟ`?n״c]oWZ?y ʪ /-21´4F[KFF;<a}(J.EXtT|ko_hL=Bրv\IqZ'az38UK{>nX|YQS&2ԃ;=ٿ-Sbgĺ)/u{g/LG9:yEm1%%_k%`.O*0Vo.Z#aĈ8} NGq0| U.TRuyj>c,[=a' w{ @EEgcؼ**rs6*pxZ .i GY7 $fSk1-gϟ?dvRux7Y¡GEu,Y3Oϗ}K@`P7"X2m$SglF2J'DE #ߊg.ոc y}T;c` oA>BةXߨUkPD:DfͺZᔺ>~* hI'(ˤB1 # &&臯?w_F $ $<>&YEp ?5#7~.̻2j8Οb 'V-_Q9}.N2 M%AF V΁'=1%C00%91],31/[Rm˼h|{U~ң+Fxgfˠh*{V|N}%-ֹmX82ãfB.jLб/^8ꔒpuVUϦ suK؞_`9\,>s](u|9kO$-#YEtٴXe!pUX;1݉a#dbs{`ss/u1Z6 J$_k\:?J]'0眍2_} ]w1WjX5.Sr`R: K]^KX|CFbpʂV %%nT)j+XmȐ:M>%O )T%\a'Isq⹉ZsSƺSqbv\g޶,n;N'kCotOq_gNuek*J0ooT~*ƍiE`_ϰMRXXڃ̫oGVTI)IGcP/_$&ҊU:i3W5AI;'g,l7Q^ۼXj*8RA"sRQDt%*<ғJfZ!*_']MK8Mh#Z|yWY#wCv9ygsQפ K>SMa I#SFHേhKj|9?\JWKxcΦ@'2-24N ƫؿ O(>^`/nͰR?-'iTق<\ {M-N8 sa;] l)MpNDj9I151-({7"96et>cΡ(ĐsT5),WXش4)TN\mQNUE[[)vY/Hŧ# LgC~zT>^EsDY+m.ٟ'2~/E檭EK\^?n/RkUMVy@U[XļU|CmuuVDV *u~4b~1m|?6\dҧ!C[:+VjEAjWDJLUQ* TX7[mlv+I&DԢtfn4nTFi(Ŵ".vG+@^%9]5ݳq5a !ʐHsl/[MoA"|j1W1x+QŜKCf+O='n î)_do倸$8Ce/y>z]IՎʯ0| .*#̃Uu(6 t7A@eO6Y^=^έh.O5h$}VDL5N'0 ֯#(Sz2A֭ܘ@З p>%Qs"u:4CA-://wg=BRf!jSWtrؕ쏕kyB Yzr"Փo#/;1ǯ*~N@QtF3Qr, 6'#3=֓>¤J˴kr<60f̚Kbo$aY*}zc6@Vfϸ NӣJ@{7WLh7:OL" ZrRPJ1^` -'0腰BZam1կTU"PaK)-2 y:}hl{SO`]T5`͆-38 L=֣ ƎTD}#vb]ӵO 9\7]&޾wl2͚ls۶u]>֭^NhA9"ÃQc'bւ, pQڈG#QTY'>`ol \AdjR<>8wM,J( ~q=B(YU$&4$UK}۰f76ك~`DA)4Xƾۙc@6*+cl?7X)l3J1lTe$GVYg0^n_I$tK4)?~꘏3  j&g2qݜJϲ33BwATyg5?|QӐn ÆJ4Td4=$nݖfIk\l{05aѶ\0  `>s j]ꐣv/B9$Hƶ!+9(!q&}mhjG;'g*@XxcϦ($sJI(mQ"SS Tɱ$=A5>l .jݣ̼5$%i_]ī, :eݨC#=sz,*gXG;:QhSFM,IW+aY^ԑ8ةD~ DiF +:F05FmܺŸ Q6 .ny=`Yc0{='O&`!x5ڢ3%Y\hcKE1cE9[7J1sAPEkJJ-]}Z䥷CsB RF QZLDO/Z]ׅX+.DzJ+Rx-ߢ5Aܼy3!-A*Mq|Y v'`үou" 'ˁ6 %a_e~DqۻHN!96yh#x<AD闑C"n%vHՅ>0Wd>$`ص/n/Iinݽ~e_1=XuTl4浟&ZU_&]-hjA щ Tpg acj=^.2 j4ܝx8 'O I0qqxԹS- CZ,"QK^ wb>8I7hSmh9Aѧ Zt!f͚whȴ1 /?}./˫Nn4dx~C0U~w>/CeoYi$D]eJm~ G*J$s v@VY3]|긾QϲKA[c;Xd J}%YQ ($ 5*դ-'T?ۚm2ICzz4_*M;jb*RyMytkXL8n2`K G N1MI4tFĤo5%?qGU78AwD9{?>_V^zc iP1X= *_׬(f`['tHvj7Gs> udZtx7̀c-6@ ]lI4t(@RߨB̶_$'<3U”zP:3 `j^{^+]u;En8y=6;ٞ($5m/ ^%K]۞ھ˧g`R]&}AR%xr!jX݇_~\=fu lOz,ʏxPJYyk?*rjkRc}nhY9&9^yi/CDNF>?s宂 ({Y=u_rDF+3Tz @̓kwvbAʍ*U# )XrQf%Joڀl07;do:"oYBf%o|G\ RVبNMud-E9Eg=~۟Ri - \Xdݏ-;u T6 u0/Q*=3腗2e4jœ/W d2I]~{xJL3g`djߙo_)6&-~}n ,5CG"0x3Flm^Mn,v%Ę,W;G{w780qR 9Rfq< eZS16?8y+/|9.\!9Ka޳ l<ـJׯEtda&Ms:pS!lw"k@Tl/kdaĀ0$2iKY$ۍV ܽkv}e~S$||i5\Q˝|\_SЎh`QTX]b~ֵ[4F 1uFgơ"E2ohϪ Т5|fX^S*58MfCaǯ'1h>f(8t#Gc䙴 53;},AbqC5tdmBY0T"yر{>K"F©CHĺ8^wԳl@9K~j5vT8eby |I  hVXi?!ɯ'X]]ϺW7FQx@+ VXijdX;pxPW?OFѳT NjٓVO>Ri? $+<gI<ħ҆y ~FN~}=iإPCNǾ\ۺ_:oٺpLVE IԖfc91sa%g3hBʛ1c`nޙpdnUQ7 98G 99!T6JN쇺]UQ}u Y^ c6c|$^FGoA\9G4S~]TA/s* d] (>,/5S0LG*.L(JѝA ?WZJYqT1Wx}ZD%lf`IzM+Swc#d|5zn볞,haC)GCY%9hIk_-5<<\Ls*ŁT~Si[NYZYc >39 | 3 KJj`=$٘8`@;u{tV+h@HɓTH^cp__}hؤD|٢`=B"Tٳ_M#"30w\۰<Q`un7u sD>:sЅb+gY];\@\}{`6\{g.P 7 ի<*1 Έk^nڽ{?Coݵ~?0>;͉TJ*!6:~>'.?s@H抉%;HD|NXlG˟[W7Wd>UrNeoϭGժw4bQҢ{7-+L cބ!Te3Vo*jIqوb( 7<ϹW_0MhQ~6NXk .8ebLqq[de!y'+@)Ѯ x:kb`t/h]̜<|6njgwNa a;kɼI H9}<[wm (&om%Bhھ%`D ž*@ZGKYwfb¤i ~_mCI ֛{wSqE96ϋ:xԘD*R ^j͂Ki$> Usݪћڎ@VWUU^ j?N~π*(U^ 7ږԑ:t0E* 1>=*Sg+==>tm %h/~v9Nsơ۩:d8{RAUq =3 Q`EMM`/,t?v짠AϚy/rvF7gGz#$H$f"|LY4GUq> f RK=Dۇa#IQu@J&;mbiO& `O@Fr F eSס=h.͗}DTNcIJa]m?t#ٞy a.O؍$yR!briɜ׹uOVr L@V!cHT).ߞ&)6T fY@kw?[1.u`kfl83z-8j!8nV(\0wЧ} Ο=Eq7G]Gfߝ0$` 0jpUXi?˪KFt_f?Ye'E]ƅĞirBKuG_' kewۑrX(%`=oG*˝p' ɓa@xg*-t64CW 曧')8(ӵ.7_Gl%?:O~Ư;C3}+ jŴ_˫oKe5_Ex̓vHX:Ol0¤~3ˣEߺq+j7T֙Nlj+~(h B9jX&eŽ*朂b2'hnNT#SH$@nL+ v"٫ǸU0kD=wMNGs'b{r/Ř֫C3 ;y f&8q4"#;E5n-$ Ssi};fǒOCFST=z0g|ZŻb`hC]q&rHC4Ï>DіEbEU}iq .>^(.HcxHS;ove ./Tť͘ǂ)Hiw᧟?kzŅ Q AB^TQ^ھ)RZ w#nspI ̙3rgΜkxM 56},~?W^MWY ,VaתeL9N˖&C*Ai.VU+$@UqSuy p%S)qU,"SYp:``ɩf}aSֹd;rI›jZQ۸ #]5@ej/6$ޯ^6#" $)B> lt[H($.ƶM{TLrɽھH=XHU*kir΅ck-ׅoKp[6FZ<f5jYA͕s`mSx٣dT+>42jmQ/em-2vP[וzLyPv9d8d̓ td/Qr{wVļ$b"@5E3;W"de5yN %[4zx|5TXv Feu x\\qb]|2jheӒ 3SF 2Sձpa0ecDZۉ=GÖ#2}v%Kau%/EMB&sMLJ42ڋzwfVj5b,%;{79(OIOnڧU(GHP.Fg)WWMi}XZF1frTdKƭR>K/%y̨q +cko*aIqK1hJ/V{3]xdoہӮ]> K^yE Sr.ٖm)4%ލ]B&MņR)G;]bUvݛMw{foay>;s 38vg{8+ ?}zc,wı}Hϯ t n8A_֞sl[׭ jᢹ3cw0x*~U_Àc%6\([M63)_]Vߎ39ht s|%!%= $fB@Nj.O96vdvFз Y`1ji]cic'# ^ZI9POX⻯`DCk>~ٻw' KKР$xP>USt`YW5^wїN |p-]G:_sV 2CVO/oe$Qn֑I/WV@?ZӞ.b߱`Ufh{1!Zs5!_̼s|Ղd{=֊ J[kʌ#ĒcM]Ge,E@$,ۛJB|7˦<`"ώLj*VT1~^S.TK%']>=f;d ÍM7C؊^5 `'Oؐ gPzfƟFJy蛛!!1q5䘩c7Tҟ4> NL ޾eLr~(FCNtTudɺqfp[UUֱ'cvzAja_k:癷'`[]{#C?K;C{)d~ز_8>L+ ȞV@Y}Y;a ;3-0ӧ ~E ;&L ?>L:N2[)I&@齩WE`=zC,L,~ =6eTS@ʇ|]lRP/,CQ;-W{Z~1<#xѧO/E%޸5>UeA*8^m"pjPN' {XbFyM zZXRĀqu3Im)Pxj~1ޣ9U*3) 2]\GIN53+4lZ4c>M5=pxZ\XeH.U{0B>.x^Oh"V㝜<8ۙ߶7Pαc scD e^#FOfT(IPUR)7 8.Q;#9+Iyw;9>S'_b:%<3qjK,F3 e*=KPҝl2'{+|@1 TD:ׁt^m!<8>q02Y\m `ksE [;7:hzxy~,yNܿqc4Tu*(R-,LwV)`\Pv 6֩$C U|j|O**^O#%'=6U n~A-;:W)u}r+Ä_/?6'qGTU6҇j#mj?q_T쳖=2\m`eRh-&_4q.ndҞ}~xx0_>6696#G 9Σ‘D uQqi2* 9EeAҙLZj-!惚"P)&"hBE ]2mJ rlH 0?n<-*Rw%>PK0c%W`mxJ{e;vBs]VϹJQ*?%Yf^EKTw $G<4t8v”1{?>+(Òo{ L0G'{$ֺÚ|Jefo7/׭åw0 L/vƾsvd RNOll^=R qd9PI>Q'€ruV֔P rOữ~X[|y?NTIDmY:vnxєNfVn3 y0enT""ϑՑ"* [Xo۽?SYW٧#|{H2^Œ&Fvbo`pU>RpYjkXHs T-!dtgR2FML/b"0չI@`]7Nl˶I;yE&[mFS32Wn` mpRk^y x)u޹~#KޛR9 ^)CJt8eݕ-[6mAFPX߅ x79aΞ=YUV~[ޔ1:e_oXg %ʯ0Z\\=Џ^/.g[JJxj9]/?6zqtexJ!Uyk|o.Mt 6-ϼ^d8 +R߳'6|ʾ=Rie󦵐Ϥ `Vs舄j9ȉ2"UBϯ"[v6FE_@{GsT,7™2me"$#tP2{a7 aXؓub8ŀEYK"$yew+r+8d݊B^ǣxtr>L`u(ɹ̱H=^Di?;'AGF@X[dVAPe L-;[IxQˎ}|+%dfT̍dcY !9n0e1 *ȃS'υk|m1Zb.DξTTX[\n~=),;J2F wGh{.?g^RY2FZ1=Yo| [pZq Ӌ<K0A^M c#dm UݘVx)q :Kr7){0PRqsvGlwN@pA19FSf $\dh0tPge9x6·31 *O## akk+z*!T6gap%;sl[xv'{kסz-MSz-wrr#k#&Ħ)$a&B3"r&$Bfh5Ăxc;Q^Cf Rn,~As\Dk2X}|د6)䩳 1P_]@)E*f~=-@םlW{TgId_IQ$~ޑ.,Ffb _#ܫ{3`]CR ʖ3IVwh| CJ-|2`AY†dF .ܙ#vX6"?5hNoΞ-^n]y3!p̈́>x;+ۣ)ySeM|mn\?{ .o:skx{̲g&SV) VXOКA];#s JNU>jd0`?.B|bW8ϤWƾ?zLL_o*,]=:{QȂdJg!2'Ϡ#Y|#xχ7^©GЧo ek;%kٓ{ikK5YGGtСEڹѮ ;a&5kG?u$K kkx2Mp~qIgQ,W>zSj@-/7+fbuyV8(4=FYmΝ3##%|%,|Wsk0lmX+%}وa3Zq{mߵf@W&E6%&$R*(+яJ(ZgNy?`qu\>yr\yyi0ӷR%*0D%6~np#SzB{TUUO$뼐54[E ;ƈNcPUc2U@;&) * 5޸^XoiKG]+)ˤ5V{,znXO)kѕu8ɣw#ERG-#Ӻ-0jigX]r:55V%HJ\1dR _73Ic:7-{xg}3]dwWi[ˉь1lTqm<<9uehx㞆 v h$E[WƴPHjxe2O~W6DUXYqUo> H,u{MT_Xԕ1$0idO7׎>ViS6yr?*2*`+>cӖ&)yIj{^sLmك_Go{u}9^x̝ ןvn^ ll䟶gk!!n'sd`A0[nvs\\lH6QwyhjAX0xBǶt2앟W5f~`cӁWb ~h߲"bpfǜii)%yzR*{9%66x)c̞/e )k(6CkX50laLB2a+o`:_Fe097r. w5&\9_|bFq_ O٧N\zkz0S㿪UxSJo ;GrvC3L:KL"8\z>rJq]ѻ \;'Rr(BiV_ eO̜2Eob`;]|ddad g|ACSk6 PZ42(\`Dtz1=$2}![u$;Vkvpܴss ]^B b{''W + B"IFh_9b0ɭĎm[le/y< t!K@ߑY{㋰z$h$ɔ9|'ɢ> t?o+/z"%+߭Xu? |rt`l;'|' kގHDnY 2ϜK@Qi\p2<dpbЫ+}C״GG_re \څ׫O_|շȍ`{Oy@l93qdX^NAZBNҙ > "<]Fĩ3O-%5b-%UCn_K׮ػc3ayg4zff6ad[ 9I~tS6?itf"o'+D&Xvm(sHNDbAv=ⶳfI*3{x4.ٖ RχcݟDDg_}(jcmrRg1auf>k#: l\՞gMICw飥BEyՕ|~;u:m .IJ'jϵZV됑MԶ`mG9? CU@m?"3=TS4)|b-d%6P7N###8fpd ˰$A'K&x1saϊB;s9kVf`nGc ^aA'%2~WI ȤBʨy Б>{dᳩ_)HHJ |1I?=+gTS"\?lctqE1ԫJ'&ysܭ# љoKs^]Ebm=(5˔<: }or ?QwNRny>D C"y/o޳FN^&HY#%^ZU7=m `!\`.*J>&"lJ'׶RaMX+k@@|aA>FǙC[pax%;#%ca;r*1nH s.EcIۍ_U0sAQ:.o, y53-@LRo<; 9h+1^tL$Xy}ȜA7ݨTUsقU>3xFuHx9&HzLE +M4~&++bu x`cwkJ1M`ۜ ͕nvS9|gq{*41!K-y "ϿW_>X2YV 8]fp 魙A|7-L էWKnu{O_|b!l/~XIbW "_ 9Ɠ`áio0e3ߓi=,)f <2#g=\(avw&κn* k ͗?쭕{z g0ktz"_–`Ic?˱zr)w 8ٓkAuCzE]I )Aw=67tO'/"4 N+|ԩt>x,yia.Y ,N3Llg |)tn6CISw/< Enj@(;;:u[ܽ[cKme~L`t@>Hyɿ`}wb_rJL9x6C;}2ViT$7*לP!nd*aLp-^Uso!|wSQ{*p, ?f)lH{SGu{TT6W|p,ph=Uߣ1ɺM 3j؈pq z3̱M bbi"AJʯo&Pܵp&h VpŜR#%Q6ǐ&9H.Wr#*_ߓqhZ!QH.cZb0 1H)2#+4ףRIDt=ЩCXPqJ5)@8?/E7;l%q U>Z]\|W͵Ͽ*g+rRΖaUj7׳}"u,jsE]րɱtj46RS{ NBA M;*Oyɗf-j!/)a:p -}Бpcڎ/uK5fi%p"kok/"d`` ,D؇^1@HpaL``1 : 23cpҍ B;g̖Ϫ Y:z2kҚYAO W)#[V~#GR^p=QWgk PaJp>)I VN1F0 3Ai]{ X,%3zC0stÁc1̝dz|TzҰ\˕[;pOhOBMu-6oL5G0;PyѾȁSnPdscvEfa9R}%I')\frLM|3S>A_=ud>YȹԁF*TJ&ǧc'8}x;VioƬYdHkw  ;)9slvs}|I ܺQZl9X0`;)<=Zk=; rܹ гWJbHXKkA~ 5 kL^'{ DM$~0aCdXb˕NL4`wk>?W]ixif+ ?_j2)%3'_: "F;R0)?Jyy (Ú//+qNg}wuHSfۛɳê;ob]m?QoTV9&G_>*,erȿLkz>*f~ڵӋH% RyX!2. sQWM5v0l e&R{+;̬m ęxJ h[&z9^65}>ACG¹P s/+ҹ.Oa;7*G1:qw( /s"_XY^KMĔRLG@tL,>{ cRahyg9`|ïHbHEe=v 죶o9J9+'X, `Uuax`t&Jzuar)srv'w ¯[G?ftli9e#u6A g*} wz֣ 5G(mt;@ӐMFhfdjS'QU^|OOSh ֑ ;CC׈ X_~=Hw荁AA8a4Q_erj(ǗB>#T#B<D1i{w{?Z&;pѼyJS#޵'Ɗ$ُLb#嗟V#QHt*e&\E;sSڒ2r1nUM/14R'GWj Z{T;/> |;Gᱧtt.ܽiAn*" CHrH~c)S{|40ui ԒkđFf#ZI}پ6sCBJ@7&A94xl9RPp`͵X&+GK|\~-/]6*ۏGFv"tj΍cNNˀ)j)|FDr1ZTÊެU|G =h&^)¤XT*׆L17z %ӯRg;])D FbYErʘ4Zʸ1`ҟ3GRHoj@DԔihyߦ<[~/^#טyb"{w\sjϓk kȯG_[{h1X) *Vվ?d5`}INPE'SQcj ^BXRρyi|glC;.̓=_g"S\r.3^*oim;񵶽/Y wgm:he\Λ? =Lal!C?_jnտPoO-)5"94a4_YO@gfV6/ǥt'+2iK΃uHHQGؾiD#EEϝߎ0A3J'YK/bav&|B81`&byf"9#& RrJpr aDf4wޅO>.*+`2u v¢J:򉧑Zi %3G'$9H g!YbKG:*F&EÚ/jQ3N<n5^ 7ofݵizVha |YkߴPׯߨԙ3g jW89pٿw2/jZ˯?u_R\@.]JOn@IDAT7?ý-XYo"ka;>[뻥28>S־[8GPv6xq6,d_}x$ӧOe*-y=w;qz "~lY|}lk۽egO/J{veyRBfؚ#[l%u8o{Kė0&/ 2 +AȑÇcҐ56Ǣ/0ӕ@ȓ{xCTlÞHNÌ #{L]:kZS S7ͺzqPzw۾8{ d\LctqEXJL2]L0ep|ᴑ(籗(Q'K}SA[нW,srJ%xx_3ů;,?31c?2TzQ9Pp}8bgODDǗv/5j P ;rTUBÑ|8< K」O5V%+L(ݑ>#ot_4Lt& dȩ?EQx׿Wߦomw~yۚ{ ӣL5?ukлÎ N=qnsl.-gOFlUHrSg}{!C{ecvLhYGgueo='R{I2l'HifA\V}1͐Beާ8m&LE\":V7[d}Z]mSs+0#7Sdu륬=k뎲rŚ'@0[jr7t]܍deÕvh'~ o*2C_(7W֮j$w7Ow9nV\5ꟲ6=%kog1$`ܝz|Ϛ`w?iW,3}Yo4W3o4᧴éG=pc_%혼1q^K_|TqV.3cҊ]sWiE6Uf]L+_H[ncmlWN>zAl.:^ͧh  Ffnyuȗq?҆gŋ׭ӥ ~$ޏP\Z߰oWl\/W'J,FN8-~26I]9տ?N$dWL۹ËKC"c`UMwi?ɵ-HJ6mK}U˻E}O|U~Ԝ8\whͿ V$M=I:m,O~FjZeS䣬jՄ/ 2 W:.d=d/s NuL.Ô˃ /|QTe0r%g^TpǝTV"Wm|R~k_U򿦿:_cEb굡^GMME6#KSӻL#".c+ j{I:U}д^Kտw2_@RZMޯlR/S~w<_7ؠ+=ZnD ߽w8 o.gK6[dkw-YV>mfcf ͬa6w;zTﺟ,-ɐd\ۂtˆQ3@E$H5#[LZT*-٢, ]·Z$kG>^w\6PHlώ^n Zb_؟@_gyQ`Z~4BEFihzL‹ d.Q{dIFV@s1bB5QAVI* e1 ]0 \4c{!f 7bZS_J`#g?_D77&nj3,%BQY_ʎڻ">8̾/W%l1u̸평&1 ]u{x6kXQ'02#.S~/ߧxlQu#/{ =hP_)_mY75p-Ѓ8ux7}9%֧/hiLk>e0ŵMZY&!ϯkF-)S*YZ_(v#iY\9??wi3R nBB^dM2jͳE;D]WQM^WLз/YB?i4n)諬~B,AÇ, )i>xj-/2v}g`{3ok]]g<Ą82ּmwJ=s _5IOB%.9_Fkufkv[`EueL gCqy5Ŷu.$s/ {!ЮCaFCd-fB5+TP Ba}9}`a0ʹ B@#.!΀.PXq AakI$Ƿ+ ƖR¯oni}HNa%6瑘Y7\N6T4b0\(;HR$V ?#=}m5eD{̄gUy3ĶȄ(]9uCo^|& 30Pk,@q$ٿkĀJFF. Ohd6qR YVd;_ ZOUUm#{[2u?XD1G2 _QhXۺĤ8 ?g"uޞXfi5#צZc%S1OFJ2τ5X[2DR)&OK_L|9YHe~j~r|瞎I§߮FnzN0aώ_mIhkV2BŦBcMʏ6qdu7wv46VjC)Z=*Skё!q0;Y }{#+LF98&xDbh^Β+h1Qe1ƌa T8 >_ѱ{_^M.d4qP$v˺v. =g C--> CƐ0E.&0#o=n]0d,?c-J^%OkV7֖E_is j۶rwuxFT L2X3T+|^ey| \߯/|xV ozv߯e;L&( gG{cF߁\J2Փ >MO {2݁.mOs[i\*u*XU٪&FTy򛔞7OR\OK֓ءү "0Lr*8v!mJ 巗>yzfb/nsOڑkPǖؗ:OQq2 ESk  w/ FWGu-Ncm5akb8r9}au Ԧ "Rum6^U#؀w*'N/~Hs[nu]n[^{hⰷX/p64c}nXEkM+) bW#W[ %az\7Jv|ұS/ Pv;#ǩ hdc=pH셔HV:mrPq&a9kf~ig/:"KZC,n38U^HKKלt""6! 鍜 բAVfE)=ZP{Sٿ82e?ߘz<}2˭i@oMƌ:2Az 0ҍ`N#BBNSX$ܜ, ݃XD}ya<~8( f\_ 0jǀPwd$Ae{fd}/d]y(LRS > yFoe) o=M:Õ/`UV95شy+ߏxjE9`{8X _^u@GO?IO} =6`ʗ *Jq;| ;pof4Rk[+80p(k'JMbBRsZPRTpFk[l) w>sT ղݧ)+;'u}raDQB ~ڸw͚ugL2s$s׏\IשGh;8w!CwQRBFزa پ39&¢|F7Ll9 ̑1dAʒ4VRR&VoN?0D!rcQU&k p"<ڥ#*k&Q< (ae+K捂 Y+\i-0#K=46d;i&ܐͱGBqtÛ70yL%ɑd* 7>.x(=:t\}`eeD]8[Tm;΄a٧5 ˀI -A(ɤL\<|m} QĻg`jQpO@"v#u[7dR;.)Yi9~*פz>Gtd¥>Jy]GZ~1 L yRv?r>':1h4ZӟvNœS,D>@f^]W]_!^JmQ=uر{ @^d`AaW^#= P`F.όeE$4n/~|*S()6wŦ-k0>\L$k}ؤ 8~6=}&`emNc긅v=J֊ХSG$]Ns=ǝ2ix~'x7g.GaY a6Yg:xGiC כ$$ /2eO#ҕ8q4}Lѭ)e=6n}Ҝ1xM81i;$p\=}Yb5%;=1rNöL~)-) m U"07} c=Mo=Bquve &5,+3p0ǥ'm&NڸI|RT\xW?rLCYU4HPmG @{&]8v!d|uMhAj\Q]G*@Lդ5 S; 1Ҩ) |)^픩e ^GMȑb 2׌4%TioW_2QSh2H5B1 8}_zhú/ѕ-F혌dJ#}%~8=\JiNa ̜6#IV(<<]kEl#(IRn=94.:|\9l""1.'CRR< 9 |pB4+=Շ}?X9t'PVu7&k!ttZoQ-w ZÕa -+\YJxxW~.` :͓]GbRr׵r_\nYY?aԵr\"/,xYUHoH,~s[<ײ_jDkerȵ"c)hoIBQoz;΀3aQ- rfgKlٲH/ ln!q&{o؉ӔJfO?y/+jVtATa_&E96BߔåU!kuKk ҧ*A#kktETTꌨŭnpY S0@H&HZ8 ,V =6l7BWj)VSd#{djO 2R`caShH(kXe9vow+k=5oBͯ('SגAho'OMi)ozS6x'i_5/j`voDY:t"HгB;^dZ^l@VX f@Kkk3tKƋ/M/$H3W2\ 202fdm\厑#"aCjm3N'3(?@?4$ftؿմuגp4.UǭR2J"];sbsxއnJM(ǩ %9l"Y(.(GEmL)ԯ; Y_ܹsxW+md_avқ#bJAs$̕r;>D_ޢ? M4俏0Myr=:ҩa;rR|;lKGP* @ 1 tiBY غu'.;VUTmLE!n82}{?}9Y(;oDzpú!(3mOֶ#A 2V*0(rNNP |1fvYM QK#626drz%.UA)[t :͹za՚UYˀ\XqwK4;Sg~wL,^N$?>>m%;jrPVY%PH}%vٱʤU_B!cۦ ]RnQٓe:YXYA{u*R}:5>;kW0?U:D ]|pr#Fg'M^ [.Ot>?@~):>Y^@߈(\zd<>j%?2uAU]o֔)HK>_A `~*-1]ӱl~ɴ^{r?ϐf= $I$֪V7eßzⷾr:]%L;HjnFOomV@V9X{G"jbX 6Qwl$O!(eW*qs{Wq-"W`r0%$3QzըR|?]gap4&tɷ4ʨRRG59A`:gf%s`ZgLϝTg:veCEy005yC q?!=jBrEʙurǙPB՘ *U撥nٟSٮu䀒ƥg˂GabM66<£U6ԔAgذ%ۅnB#{ad?-<7|A-DOi5 @~NcW&}"IX/Bo%tyU48 頞Gh.1d~;Us/5TDc&)NL1 (zᣘR d(';nq66֐(HPS\lI>n=gh*>a UhKhrĬIh*օ|(gZ 1ba}=O`ǖ݃>߈=_ttާEs>Cgs@(:ZۻŌ̃:y ᶞO<gZ8]Vg̡H`y9 Bo̦.Accd^ZCmK`]5c<97:u׏>D,Nՠѣ/9/so6?rʑ9OC# A Cک8;Zї`~&՗_Ç0zK]Ecf* Kkj>gxީ?= `):wt'(,TO3&g[Ji v4ĝJ;T4]3F ҈^F@+&hAxztSfXs3+&+T'Os`mf&z9OWx"j­_[٧)$kU?avlqNlLEc1 _|1\m =EYh0!u'o(N'KVcmLOϵf;!@'fJ]IvH2p(wB cxbȭ3Jk7OĐ2 }旗 {^1cg¡ Dعjv>Jk2TZT::G]2zt`Re1ve~v'K~7].ӨMä'6Q䥧R~HKKVTs?2ϔ4NkG.ْZb?.$ѱKވΌp9ރw?S2**+S2NPSRla|4?? ަߴN; )jY+LC| u_qdgzb4%3;.g`ѫ;Y0a[:ܙZ '+V4'Yz;Wr^xlp:Y#SE UEJSsx9PSA՗kvWʎ^$S΀k-G_7GUE--0(2 BtQ eYtG˹"7<e(=an]۷qNyFܾ#.IcJpe?EsmXe\qx'Ұ/y/GH#&l|{#g '=# OơQ8F0Զ{p1.׭]}*f3ntHM2{:.tԖ,Ēћ!WJ-ƃwL"o323;&tZ%;;11OgrK:x恻a"}OF5UT=HHJپ 3'RV~xL~}X:O;›|7]rA,X7<(Cלv?TI$z oqtg+ɄaQwǜ1Dvh[[܆/kg0wquݷ_(bi @5 PvП޴'dJT=( /~3OjT=Cb<У/Ng0|ijn7*LEDCDPpeqw<ۦA%ģ qZ3Oi%Ǵc=ѣfYw_3;#יVXq'ӏ+ VKu݅:sk^sprW!+y77,Ն|!ǘaoFlWxڲրo*A˧eY;}H7o8הumPq?&ݜ1mG Zm)ř#Bk';e^S!ൾIX<7cFcOB{<ԋm61amԅ kpWjB/r,`ij# `Dg:=]Fr?0g| μκFh8-f#MzﹺVOVc-հe,?~x\mqgLy'iO']gѕ6nC/Z4wU}<*ϜQ ٳ/1A\l5gr3>y v:zUǦ\ )U4Eg%{R-εwu*븆/C/7:vXJys3\51YEWd7 ءo칪wal tJFNBdq,c<؉ikG imi=>PC$K$iMif4mYDQѡ5&(X j hAEM+%܌) 8d 0L D11 Jpؙ@a ,(w nǑÌx8_xc&" q|( Փh{M)UKsZlø(ؓ1qI2#;wm> ;_|9{D]n7 QJ>FElB!YPpn~fLCNx(=M;s~*ZTcIzPd@1"֬`{?O߯<s5NS)>L9f0CaG#8rRqL@Y7SၱWIm_ݭa\޽Z J> H3OPrrj=N[ӧގ鏌0o Ћ k{ (jvd0WQRZsdjK'.k-h :wqwLZ=sS)4a@9dKK ;)&퇸};N@锹g^3t@?~.xO hFa2;' EW`& rQ(1Gzf#VSƍE^A>k SV982"jp-v J3۞POf yaG)btԀJA~?Jr=cm\\.ܷa1B+cgAx~ԋieLu\~8$>}킮!JeG&L˂l<⧸c)JtOwrXH:_qK0PdǠ YK%Z#e*x.R' 7ԟCR|"܊Q.CiXQK!vqD9=X^ǏkZ̅}(3$shwT[[ѱ8՞UnZG5Q#ǴzL/&#VxP)0-%ެ$LO]ȅ$99&&^}zcԋ 'Wp$L]yLgf)E쨒XlO8 'ѩ}jX jPTޑQxdV S>!0C$OqSPii-؟x.d:2&+Lp*=i(dsTEq5{tEF/ c.5H}u!Nh_Bҡ 8nh1/GbF)BL >l6\؟5]>U˪]R*[ߕnԽEܗ<5̕sWPXH&nܓl~})ƺ ?-C ΗRF&v NFi9ztʆb+;Gǿc ~Cߢ1.ycO!ciŘ75!! ͺM-F w1!4,m 3(!om)b"+&, CL AHi,f<47þ}'ee<UVWVU?AOOҒ2ʈxԜ)` }#|Rk})Oo&pUϾ,6yGJC} Gݯy1ש<ӛc'zFFJU 1JS+/2zSgN(#ЊqMirX` W;ONgB tZk)gM%na4]piad=w;EZk\$P*[mGL[GK3Qzp.,,j k1]6<Ѓ,2b9%gY\Owo54aL_24(9wEui@3e`c' :ƼloVb `YK^, Bruh֩+!^;pm' a[i|_㏔uqU1$ 3j4r6!d0H'UmrHOOyep Kr~q6>|0ܚWYh q7rts}Wj@5}Qv5h nl:Y3 %B*R-A+̖{xtWkS5Dj.O&l@]cy>a5TY'N:r)u|Ϫ<UYVҐ[R%rt1Z=*~3ckE$J,vR>X]j`Ͷ`wR<`bm 3PS̔2~ɩi(=?Yi'oVɥ95θ7 Fj9e0;ǰncs5wcHf9 $/#=eR!h1=BEFÕQWNFxVBƱ#g_|cơ$Fc)D| 5. @$$2΢#vZH0t){o?y`mx"KV$r(k C֣io061ΰt `Cд쭦QHM;:9}52'N7W'LHػNdm:Zek ئquW`߁hX|el cƋbBRGk8|8228s)e  v&2(ل:P2ՑɊՔy!=}6e-Aed"_OH PY,ױ.+An RtblPkGW,g@+1F>Vc8p TY|^ 3AkϿBנ(%Vߗ|>d kd[<[cxw DȞ-o2ިajjNc3٭P09y {Xd&z> hQE䌑̶=L4;8\dhT s_}1YFvud4csJ'|zz淬w ͭ ۅ FXqt6p/]LwFiqa<ЈN{XWC4Ta[QNqCdt v]fڋZ²c Q: |p"쐓`nm{6P4n= c$L7$+hg Pן1WwxOt%[Jg:!%hc Js'UfM u s3 ;4PGΧdl]F̕ )i* sb!w1$6 Ʌ_ *^ΜuM,~c]Sfe+Ω Di{lڈrN+S74)0A5/I.'MNtRƲPG n>FGc:kzLIEm_B C>B~1fxP;2$Ok..}1.e%yT @^7`%d[Mn8Β _׻EFe9M06t":dP%Xg&f6nB'p\< צ}쑣vtl͍adn/d$(Հ޽?kO 8R("g[*_Q \F+:Fp|x>Q̱`:9hr6Kx7⋗'c\ /pLZRf<&FOOǞ2|e;P9@N=뭫ۧJ0zQl m QWebWKs0)'WܮA~fWcԉorE%hl, IGǸxC}3::``/NfW×Dc1u9n8W[~~^[ RRBg8 -!(q׈;6cU% ab|vۗmu,w<ETeK.hha?a=WZr_1vhlF#lg^K,JGW՟=k$,$ GX5'Ȭ,@cY3V.\r8dD?<Tl0,ӟ2WP{ ¶ƕl)yKm6R=!ݶS:[r, ׮BeiLC%>.ǜ~* CE,_4F s;`uxErfi.;[ۏ{~qnwL;&q8xH8Pp-~OL@ߓ9w@Y;Coux_xfLq]UvpO! y ୗŨK#x6[+WEwBՖ\j̫s/@RaK.{C~ע;~HCs9謝48Wo5c3'ThRXgrՎOŊUkN@-~h39=Vd>Μ<TcVHdTUX@^GSj0*/+߮˯\`Uk2ŠW{b`PJk=srtB$fOЩ" k)X<ؓɧaPTѻsrg2*3"#`Y121v3 =p@$rBahv޽f}Ef6~ vAߒcE(_@őFN}Uf]=`<_]Ym\߇@+"C7W[kңX#AWgaHP_7|<#98[̶([7ÂoMy>%(Fzt6iL)j}qOY|FZּ,ZO bH's4TQH<+6mڀ  >$D% 72ȑCѷD $u=ѳ(D[6ɭ?zBqzڿ/ێ]}<2u4نs*a#e;|t7 fƩDP3`['y>6y%f:"#a3?Ӧ3OKw! K֐-W'LƤqm?iMtjm1 bI␧qAa"ۂϒmh[;QRfONK0p5{6yr[[%+8ij0r|Zp6A v4:DP#E8{fT,qV9=2U Z %Y ,"8 ͚tItgv@g_}ͣ(v K'C)iԶ Z0NٶfOo>x3gecú0:Z9_} D\ZLI {\oI%)v,.O?D __ɷk]ҶU]7j_&Vp!oX;0¢ڀX&U`#3N\vIKز5hnņsC *Ҷ,cra #JIWzH5HkKem/Ё{W;c[)j)LY6KGV_ցTˢl99z*jt:vVt0םUjDWqJP}WPkHTZiL>JT 6Nͼ 3ٿ?Þ`>Ψ98Mňw@ ǏٌiXl9QA^\W[;zX /,@fA?S_-DK|[pLwO&, )?Vy3#42u|Lxy6Td[xt8FM,{&6{p>.dI$$Cvyq=% Q@{yn)jMy;$Y/$F>M[{-#MI\VDP_ ڏX"WX|-e`(G7_c&(f뺟88Z[od cʺO@V)8zvg[UW:FRIskj{ʾ*vT:4S2#~|WAzSbԀ^iñӛR:zx[\H# CZFX@9zN!sahZueCq~C[Q<4z3rQz֍8N2'lhx<.dG"3~}2Ǧؐt:Xa#zk^u 9 /4dhxfҀ`PWX|.';85M`3rS魗Oo3E8WDÍrj5uSzcWtRN)CsCq۔[gkb5tdKIdCV䪑"W+|O{8@OKo@C;d ڣA Ƥ^NI#1k;} /_iS'd'[Fi\`Gic?>CU#e#QYSc9SM̓p޷M< __o8S R %7homiBI^ºc_V#ܼ<W(s]u.6c!7<] v6| ;G)QM#Ҁ~%Y9)0''t[;>}r̉#t%`Fz~5J E(P[Y&i,ЛqAcϾ> -IBqf Ocd,puy1\l xvei{)wM0v |ԣ+L(om^ ACM9%K&x|=ׁ߂>]:?^+"{KlV3hL6t*oc,ƫ/܋]wA|4R5 폆a/hV]ȣ(_=pNWX5yވe+K{7qv{_w>+E댔X6|߽[VN5cE/"k[ۤ?,i4I<y|TJRS!栱socVʲR⵱4zzP8 Xӗ\ZpwȺþl,DvF@$F o#?#]|qM*'ӷB*,_b5xwWb^8p-edV*nDl2#P̺XYqV\v4KJLN!c4cӠ(h=<k$ӼLwNh*l޹'χ(;h6G=8Kw.?4A( kad=[򡳃¦1Y 1ʤR{Ȫ0ۘv.y ^|ٚX+<>#ܺbTq asT+[:2k'ѣ/8fmNR,86us2% 2󳑗W [g[8ѩђN{ Ƥ?*I&N%K (q\{nwaáeW.4u*v}r݌r9xP('qv͑Q7^zϯRiƑ!Z)/g51ֹl.%1Y.1և'0>2lȒھu;ڗݿ>pm{y(GOkV_ߦ6gBiaYro5q >NY/i@~ |Am~5~r^*^~XCxݗRHYzHͿamLō #zSO=kw(Y%ni2fDg_U֩}Po)_vه][VQC\>ˤrwS9(: ;z!Ç00 =cL9el:cx -zC&Z#EPUuQc._vUY)b(=u>Mg¬K?K0#0RvBN4u}Yp,T|^MUN9ңvݏ $yVČXt%U۹xM>dXn=&dQbyN#*1k]f du[@髌R2mta7Ad.[=~f6:7;wGXJ-0"Vyc»K@-E-dzڍ~eac¶=ĆюbܤG~0zY!dAyy9lmm/ɫ1/~jT3 (jf/ܭ”lB9L[gdس^<$X {_~u.4?We kj+ʶ!xq笧rض;b줻 $ڈUkW:4eLo_~Ⱦ_(ݾzvW[V*H"1>et : .#oA~x+i8YoCJ͚7`?x CGW#h@fMTUPc(>t`+R2`6 "e]x:ce [7Y )y[v& Cz6^07tIv@ 34(Qp:_ 4N/ 8m:fc)uk7EvFg^̔FȀ 4%Z2Ř6nwb˖ғ铩{RK?u '*i\,@! ^-|PL` F% й,e-,lmľ/lز;,lL4?R>۶3y ՜u`k]/ضjrsEpԭvB_'o2W^V7=l_x'j³R˨VY'Ndif>dq>*{0hx:p=eڎ.Ͽ,[F:W֬K%?z{˛1ZVD﹀o͙͘ՌyS *ӵ胛Е1;z a+EcH|IKƾ1Jg0Io9i^=:WJ] 2cl%ܤ;QDGQl_}X|F^C Ntv0"h^|:|!8)quȢCȩ x_;p5U% jX%jljҞke iO^9Tn^&uS@ (G}Dɪϯ>J_Ν8 ]iZ3d%˘Çf*L4 ts2Zdj*Ŷ(6[ PD˾yaZŦCnlՓhdTѠ utqt`U qwyEZ {VʴΪh%jUe=L[ ':)vғ' Rs|ePڳ,0?r:f;<6H"8&9i2Ɥ!q6DǷ\54Y*+o*JqND)|eu{Z bOe$*''׹JtJC층4Uq;K;yיtf%b\n(g_`<%ut*#At}ao!!=)?r < B:2;c`Th7ctAH _zƙZG]&NF8c_πCڟWvاWrO4*8Yv[sB\W2}zAVkORYԔdu ,GK_-F{^pLWSUUÑCeίSmߧH#Ϋ~n^)Ӎv?ZYff[ MA$Yx2;q۪db5N(.KK8 ࡝T{U(iߝ҂AXu uzk{! r2q!erU6MEЄi3V􋺝Fe*"{BR2hQӊ;> JyAvk8!%^pk"oċo/\y>|#XWbix2:XLSPwOҳqV#z6֝@5.=렜zM4lj*˔JRH]Wo 5rj;LV?~}Ga#Vw/eG]cs>˜^z[^sI8W c^.Q6?pmDua!8</S4a~;)tC47} 3-x8A۹MxΩT7 MIO?[)ډ* pF A$NnTFH1#PJI5r3u?DS%QT{cJ L@F 'KΕwqG/+Vc{w&Ci u SɩsgI{dxyOCR~aҲƄ Jǡ\,h7oLykPm٨0@MB:~^=D`t_l8tdۻ頎Y h[0|Jjh_~Lf!f Μp.- p7vBpP UhܤA,8}Lc2O?_vbp<_|ȀP;x9O>c;L{ "@n)<}^a8B45,SC9 mHQ>y<>1 K_z'N#}Y\b<0I p}m׻'&MrY'upӪ5*~'g$`xˋAl?X_"M .Lekb6ʶRCu)e߆6#?2@W؆_7IT_~6J~җ0Y`ρ\%<\}:@T1GsbrTv, 9~xשxG|T,߫GيgkhٳܜYݮ qeb5];>ٓ1:C!Fs>aC|zE2WsY;d{aIX+Hx@IDAT[dPm6b,M|V}'(.K#ӽ:V7?W^/7fk{LXuQ(/|') ž>X0i<_I)/nZ,a`: Uƀ63}9ؤc--ʾץ9Wk+Pqb >}xTJ>Sj$=3!cW~6~ [#-ں ڼ[Pq/8" FKi /=gNJ ^A^GL|[x}DŜeoiu0Q_s"w2deo (Q<vzFnL#W@ml3.+/Nw|\uuuB&xV+k`M,mkA&V[ +9{ɋߍB㤥FA5Ѩ6)wz%L!w+|{++tB˖FSl,F=& YD{xb^AtVS./;^"VȐ掾eb "j=}WWL490Lbm62ϼ)ƏGFcԑm(wo1He4~TZڿ0Ä-|I~}(f!IW,%y yjS05tTXTsջ3i-8: 7T^('ҁT\ba_ޔRX/;0Z]eJmvtmXԵz`=s2rRc܊l"VBjJDJJQ|bXU*/ʊȸRbF?oփ RP{|HY+; X1ddw+}w!xxK\|{k FΊzV7"S)DiuGʼVQ~[W<-bЏl 0ՀDw_*ȇF/Z@o77]3֟etk̟Skg D̘8 HMDJN%@DsUv*JG'آ1Rzn1$7n6^ ࡔ҄;S׈QEH?ܖWr(]l ֋a|JM0N23 ؃(gD&>k.g1>V,\|Eo)35 ,ߍ:87߄23% q=]-A0G iY8KYd%j6CGZ<:*A`P_g0!|<́[}z"N5uk8̙Wy$;Uy ba(h*%oD!RR+k\  ʢغK ٳfwWN ?5~ݎ`AK[֧_ [l62⿺󥝨T!"SKs?#`v~Qf<=Eg `%UCAVy=IGS bl`~u-c[: Is 6`V&{C ~d{$زqXY riig(\WSCdA50T'`/FId^@I5gv(%'18$ iC0tUoD&(K}Ya"!q(=`iYyn 4 $ҩ2z6vUTթaiƔ~mFZ '4Q\ڍdkq=1\(4ɜf}KIQ%`a0N1inEHg?<3."88+쯼h#%,}4La^Sa[v줌), thR@6C†L~!aO&.fbXth)C%mݨ>`H6qwE{a֝?{&=ˍ6Ӛ_,+Rt%;LٻGV%E8o-}.\JiJRL?jY}_ltx :  bgH1Qc_m]w67{ZTѨ(`Uʠ(#]X&ءo:2=+QY\,VLurvGlAUrjrRJAy%h/)Ul(\)L5Z}# v |?Ң OWoT,P\R\*g fMs"fS#th9bXi'Kdط2@/*$ZL XV28MM &Ww&C>##]US?^.Q3>ydذ) W|/ qi!'3 G67Tzf65O 6jE_2331eT)ރ93ı]Ka;w{5PԝR=ږ=௹^ $5S|7;9}?x/%uvѹSv6X1H:OJ+77r;* U3ot^bʍ9Q ܎c~~ GGUޯ3^J JA.Qʋd` a1%gj$qGG;NdgGݹ_x`vd+Jzz!??U쬞ML̓`Z:갥׎{X`[;M}l&cՔѶdzC   t14U6Ttq`V "$H س XT۶e6lF2j.5 Rr+.>sND>=Dݚ&+-D1A"}3"2AvlNl"Yμ3Å0N>Gi^Q t P^K%}Y(**ކZoDIJGFap\۫'N$C'/1/\>EIG_ 5& `doe(=nVq *)y.'o?F{WVv q1g1_c?Zpݶ=hXbA1%@u|uu<nvZJ:I^~IL:~6HS6'x3򺟽"zUYt։hbp%8 (Y^BV3>e ȗX 5v7M3F1n fHfQYysŏ_INqgkT^.EVufݻ_| !8 0ՆϹi (o6@P`0ʘGՋ0# HYjե}od"?mYK{C 27HÑJ!>ѧHuD~l㚐zce3 ,>D! G%e6Cf[)lj,¿?}:MŢEHmSq~Xt8~[}(V3d{BUq%蒑#(<`;ΏB<QQaEWpKn0)~7^e-ܧލIGBr 3ウ6M֗  my!HN8ʠiL)x>}q`ɼƬ'{R'emƂRJ$+m@֞ڋob S?Rqɉ9XW@S'B:VƄoJ oL~([T` N*w1 f` P_]Ĉebe2rY1~Ųߒ[*E (OY`R=utS8][F]}p@S=Ŋ0kiRTUUdYJr2YlD`9JJksų"UJζE?KpqVe}r\ ^WW~mafv*jlLr'w[ñk40?`ۺ-L㢎hVU1(τӟj"+H[g_)_˲ji35˵j.޵FU&QmSx4 Ȭ鼮KqXbM3+|YGTdi2&qo`Vxi~퍭@POцbD[6f*9U¼쮙*!z,<^LUTLyM*ˋ9Q[RpSwc w&nϲ?b}ۏOޠ8F-k*"?+Y(#1#7>DvD~׃ -o4URc {u7ݎB4Ed`ݦ#$m|6Dew3q$Q4'N?ry֌íڵk_'6_Lԓs1? '6%I#F:H=i(-F9ԫWo^ߣm΃8cF=;^IˇZK54u`?ʘ>3I^`(?Yt0ئ~dN;m5iSn50֬yF&2 c u5M2k=sHF\t%۴]-XG]#Ι3z~CpΣˤ|*F8g-NDE10`$BkA4Դj0 FpY~m1"#[R<nG{Sxc &d)?,Y[F63ɨ)@{iRvڶNAkXfUm-y{`7$2 @KHE#HbCxEeْ/@vz6(yGH?+TիV  kc+Gh1;paVap}Z!ʔ6J{+X`gPX ֦ sTJL*#9:3 =ֶ:-M3ȢifZ*^xy|ͷXpR `چ JÄ 6a5E{P t`a}l&7=x.prcN]rP=yݹ,y]~@G.ȢO"}tDx=Tbb2gd֮ ={1욭^LGR3D)9)wIe:LgSgj/b9`)CfHʼ<4\(fܿ,.dP ĸA[FiʎN;y5`uw8>_JC;кy\CBlZ|uɴTC-k r A 1Ƽ9!avw`=w*4tZۢK9߈ûzHuJ[=k0v@ Û̞BG2xo}`oe(ϥ"3 P!֍|̀53a"\~& d<2ā O>m-yؼ}7VR]؀1ip;ge㳏[.Uy\0ϟ~yT00q5" LECG1tRxڴzV0(/,8WX Q*^ <^VRyߜװk~dfe0,>G5d`59䒥SK~&)ʈY uL7O>Zhc{u\κnT8 >01gP5y&Uq8'Igd u8mHՀ:2,AaT0%fOp58PPu^.b^M[SmG1M' دc`h(`%$ $Cg@M';EE`U?ym?oy,|d" F[?mS1N 4zΧXr<DZA [`GգV2|lc@5^Oƥ@Kk Ul6 %#`v2E[0AX0Paƚ۸^΂* [}&`R*^bJmA PU<}}2-ExG "Yil6hR#``rא];h43ϳ?J57lڼ<}}LaM7o~-׽g=NNL6b쫾yۮ$ӨfJˮWhw+ 0m_eqs65jQ\^Ļ'JI+<Ã<i3?`ESsmSlph)]sb#' 5]S8Ch[Yfip{("np(?D4F8#1ZJc&aݶd##VCGMa^6)ݡĎ5>#ѷؾΟ\;;n#>q nE  or<Ӂ=-SWYk L%W`5=|)GK@ ܑK@T")^yx +}J4榮ơcLNdf^.)zxg$Ֆu9b8zBGWp(v)(#37_C?GJlo=2S|;岿w,C1pSpw!Ѿ=?b]س F"VX' e2>np5`$hrm-^}|!6;ݾD<%!]L1o}=(Q&?'=,jmTD߯Eg~֠q]s|ϣ\!UI-[ KY`_$j9)Klo:s&`j5yki9liܒ (hKU(9vL 1CJ^V䢸0 ~.m$9EUH%3aBvjLeUwle)ǧE&߾Hf{ ,Dž`$GTl ʼnh?>\M2QO *͛رH~z>Z9R",*(J; zv8k ())>>WYOry/~ ??ʔ(Ͼfڝ`ʵϝ=Ie/6?\:8M@J@%`Ȑ!ʫ3gb$u (-.b%x^ftU0߀i?17$2^Oes˥2:͊g6n QѣI>~*ć܅FµLr1,˥|7?a,>!cS[m{`: h`x/+1߼ N2˜=f^Z~~ZXac#@b=LSUIԋ2ӱ+Y4hByˇzcz1ܶf.Zضz?RE&#n$Шoekn/+iFDA0i$|).[ ywm߈[Nǡ@sv ܄pu2Frßct  xY>!;ۚ@²^z2J4&uJ BN ^!/:5X8sB5l y㵍<},dC\>j"msqĉ! }x< o8d~SX >#Gݏ߇,Wݝ7v,2 Pivsxصys{xsҼ+l[s@bFmﳋ Ex*Xrj}O˪ҥyyd|b`\{N3|}@*Q{.r'j~|C4C*a ?n鰥|H[C9GhKB՚J'~c&nZRK?v6oݎ\{@-J#.1cde2]D;8h b]7KK嫿Rw]U~9M}ۑ%`~eا-Ly4˻ ]8&>.7K>~3672?Sw(+b Z,; ǿfwv؄U v]ToYn-bp3&qCF=F _pAM7DSmX+_ `jh2uq١fy '~.PDJbfpu*;3os__Jy$30':RQ [oj.+=<ԷG2hI?*l621Lu+o3GPV@5F*rD ueW>1 h;kT|ʔiq%oW~'7QW:J'IunGoIjqK$F\(G̹8*`>E ζ /xoez@!l}_ݍ6~G1Y%dKQY [j$w#$ܶoNB]."'*M'>^޿rqy2zDBmydU) NS6QH$e6s?mZ^V,v)ʒkǖG&Tn^WDq#YiiHVEWcMPjс\C2KI§8$ӧc(8 ߺPDǗY(w;yL5;~?qO_vHLZQ*Ў%1(uL);ǂu? 0aҤx衇o >o(h~)ÿV P*q՗o{MӥRMm44"k' LөmBJ*˺ =L`](l}14V߽2uTI3n?Q$PZo>f(xxbBO<پcGQJ5pld$„VzmioJ]]w6dX\hx8,;-?ssMduM-2ʘ [PѢdmΒ3v/Ô.3ޔtDMI!s?{Ґ,-Fʹ ;&ezmkMlc~7F%}6ٓGp6/4Q fc쪂1X=lV!~#qݳ7ƓIن f}{Tou cBL $tn8#AcgC{H6|ZԐ )j mosDlE)Z[^V򊐔Cn]@R?;#jH&[zuL\|YǼT-Eł]'o`Ӽ6~ƔObMc'zU`ޮ+82(!<94 @?0> y @=*/.ȯ)/#Qusƀ)$\!ZZBg LnI<{|+W6KL0}`Q]-;-81g}nb Γ͓ `" h:qR<RZ,#T|,ܜ\H ) ny `c!h`:8K>۶mB>ly+RdHC] ),.M2{ynD }v ܬ,ӥݸRޠ|Z_nWMǟ_38Go7x%t3#kH#?%5s IvJE sGʥfWe&=ĻYuŸ>u[TM Ko,3҄`l]3iơC*yzQib!7 k ȯfg"h@ݽ^q_`޿mf<"p:93fYqp:6 SfŢӧ)0>&q}s,wN\u?KCCnjlFpv 71>.O,ʙ.&Eu~]IpFcc!3U8wq6jt_4{20E@ EW/dj:N=zLU'_톫 ~RڕC!!aKw7``GZjΖ5#8Sކ\@l ePgμv.=I^*G`|y0޺ wXe <0Tq&J Gp"$⓲o`/Zs&m2/řB';4eˆsC3+96*}kdJ7G_ XsCw 2'6\gJT6ztZ:uH<{}fY/6AX1e}~AfmSX"z`axOW-Uԧ@#':*^T `j*weډ Vǽ:ؾ ?x{{p/AhoWaxE^eGC{J\O Aw^z.o~2d 1)ȑ#/0D^|g| JFړ '}yTh"hH<[+.C 0iٟO?j92~OqIő4C0U1899KE}zar0r-.~ bPI`ΘU6tNo?٨" &bGdE'W/⻯A?J>S맸gKw*w܆C $l4y+Y,̣sh7{.DC OV 3OXk*PsBk2ύm=b|;L |KfCM2_|0úiѓ88c/@Cae}} ii 2Xk;ѻi W_GB18Qcsx`{:C6.;=PGf+ZʘmQb:G `nwn$pi;k;yrϱ(lΐm(ꇋTD:!rw;c΍*r[^*L:~a~R4THeV'4ӂXU/AoS\O'\"kZ暨e!œpg?f\\)ԖX9{\L&83C¤~>?/4.7cyIk0f{ٷg T0S|o*ٮsu&zeSmcb3"p| _Ϯ$OG%WbZ /$ GJG+ʷJ/rPBwE;pUc`eUYs`wCEam `b=O* 57c7bJ^| 0O<{h#rqWӧ0E+kL0,l<t cz)]F+Sdc <><}0a =S:gMޞI]:̝m6s>-\w_E<=c1u @Yy=y3-?_G1ٗ ! U#9UТvY-kd ߪB3=.UcP$1κc!fUJCuo2-AU!4]Fvaz:_If9Zc"`5πW_EW B]FlB2u5ÀpvF#SȨlUoA^Q܌K4m՗^[$,3rH~J3pAcC:w5k(+( :8UVdi\ `j؈#Thi0 Q~zM/7W** rSQpTbiu[O?D,3#EuYl姾M}+)w@.DZcmj 0Hy 3EQavćL`Z`P,\w5?ı=e+EA4D4'?*ِ\KۻAalv~6;@IDAT.vM R:'Qʑr,tT=  g[Sʥ須2L~0ԉ*!t몶LjjT]IPmV%dCMmF}Vh1azJRV] CS#Lg[Un A1--a&H$1>e]ʝ}73ݫkvZ2κ>z3\#P*\gz2̵X^YM`LtН}G8yR|d Ŕy-Ffls!ˆzژ33)w3JxGgӕAcF8-Yz~$.~6`$99wWL&:\ukpAl`PÒǞQRf2&0_VQqx܋/]!{NDzR"fNd5v A?FVLOaw&rԽP%I572.3Og Qc/''G:e{>s 6Z>rS2G 6ا"O%A* %I2u < ΒtemHcGŷ?P҄E!#2q$fJiCOCI{Hl󧪥EfIyQݰcJ.r9ˍ pA:S!e.U!3((A.S-ZYW4_$xմaRu T7ӊҍD YO{@ ͭZ015D)3*κdŪIϷy-ABC p d t>k~AP&veʑV1sݺt`'I c}'LB@(N.;@f,?eZL2d5Xu:xW t1rK:扈d4pK['X7cE4Cp݃݁=<<V݌sV {dEVQN\#᜕K_&L,%1g#Pk+`7-DBh顺 rK>dPBuZUR N8wk,UEm*a 5=#Ϫi-/CQM5Ȥpv"\Beb> U8@g @ LODŽ9ǽ?FGؽs3\M^/[P*ؼ&o[6_ڑ,@V`IVI MtڑS<ql>Xݒ_1#*̠U+">(w=uɠ͡jfu!Of9ly, Qy='ce! ŏ-ĎSbެ{pB:Nmɝ$)fKg~|2ZOO>y'lVg]n -wd` q`dGcsMaɁV;aӎ=Oڬ)eG2/,V}*O uP Yם^i+GbD^X7j(+/Jৼ薦W2+ )Nӭ}敉HT'`Ё]_SqIdc^˕ݰ+:OcWc?;ЂY[rf[!66~AHOW J$hba>{ -`?+hD= O>]Le.a|?)I1w|l۴E*o^p0ƽQ>jΐBfKuAcpd5۪㍛Y9Xjkᑨ< )?Wx ZɊ}y،{\[YN? Y,yz":mf(q,BJ/v浳DH`Ԣx)#[q +esLzly([xVGM8믷\ [ M(jA 5j/#Sp>=mL"۫}}zv8ןW918Ҋ8[c-}3&WKl,=M!ϻ3fVknj6|9Ǫ?!Qzz:t\^kV!"Hn.Ak/*/#A]bԬpZPO;,u 7 ~bLWqb%9V_d p l2+̱d,Zh{1Q@u6Ұ} <&̀ua~hRŬc"LFxd$5zAf8vLn>2U à!\}Jnjҁԅ${RRyUC>:|a#l q8"{'~){>>̌ٿzy ztZ`jsh4B'|Z^ uNA>dV 3fj0anRMPh6$dihhf,6#F,S- MmrTK'vةpt3`3c*| =a&5vey L,A8W f Eu԰5QVVN N,DY|އ`_g֊^g82e/PZ, g,dV2bdЉl!Q Y?\ۡOƈ7q22n4{Yjz d KSSkfA֖f((LA1]Ew;%րuaA~v"ȦyG8US,U_ ;I ̩|R5*jUaEwM6D|B* Z](=YM u* @iEA{:LAL #7qQ hd sf;+@9]Nfep(:/Q__|Ս$+dՒև@KgswV1?W19Xi#̚+V:G21WQcЯ?&LFes^/@Q d#z:M (0d/d&'XKzzr=*JJYhrٙg}r[4wLA<c`ՠbq3rJ( :x]ߥY̓sp2yy̵}L+$P{d43A蓏J!co t{BЪ>Jmʱ Ssxw`U\[wA:HH{{L{K/&&FM5ͨ ("V Hk"/{ܩΜ^kת~u/*Ċ~ӛ:|xW0ۓw7m"J `V{N q&5V7}Spٕ4 :mR-vLpcYr@,ΚgW5Vucud|(@sIRynliϘ:YuזLJŻ}’*4tdk1N!d۳6B~V>0iI UC2\C${J<)8S֮_5O>-#>oײǞ|xOmxFK>azvIņUUNw/4$Z ɦZcذ&R ֫ Jj֡jǙq!yOfѧ}0'>8:FKդPY#2];!9>(5s#EL`|[0< Ȕ+2,޼VzԼwCQiP:&=5@xO 1SXxbo|LXu1U sUgcSzy6kIγ}t߿K<+TBkhE7|n8y6&\엻!uŊXrj<) F͗x?df$Kr)u VP)O~OiGeyĘߣo^T6WDYQx^%5.ũ}>8М,835q>]PYƼ+瀕zqy"/\BJR㏢ބc"ҧ*9о@m燹H&~V`ࣴe8(+#:f eKBNn6ы?S(}-ZsD&URL&z **eie]~;mmoU䞺Jiq>%a&`ܡMkХJNd jۆ 9 Лp^U 0ii9w^G%ۤ%ÌSI׀=ԶA!:` O0B/Xs5j u՗!mWi9P{iJ"-  W$SRi%sF 3zPLzw&dKv"_"-.ٗ&#VU[mwԓ x],8o@\a-'G@ `0үo98PN`q8r &^i8 @O\w:sW5Vu^ԩ6(B5$SuK׭N'M?` O ݴ Osմ ?g?Z7 `J{c^@zGХԵ kJa0B!B)m.ٻ. @6ZM'媕_sbۍlNȦжQWGf2+ >xosd# .k';? X̀ {c '>~G7콰L; V]F8HӇbn(Z/8S6hʔq(AN7Ga L>FxY3+ڇ1`($lmSGOӶY6W5V9m`uϝ&C8QB5BE NI!yϳnn71eTѓTۈOl4.SVȰʫiEu9sc ҔYZWE/oJ&ȁ_4d3ǚ-[}4 [N]wKx[B>ط yY@{Y|sx/~1YGYָ@vt =͒"bǯ 5=o'SNXeyi&'*Nk2ARg2#18 ^'}==5w¼&mV2&F}sى=?'(y" uЙ\Ed{3k.Ы6K 3(~BBE\Ġ*,f͞MΛ *ipL5*0;W8er{jTӻ0(Q#}]R6X;ߘLbnJvcsS@y'vt8__J|'>!oЕ=r*tr#GfR{jqv{$u{>G{AZ|rer햻ICXWhߦ0<S2R.U3y~ #UoM;kV%럖♹'ǘg*L>hZĻX}wǎcM%0W[6G4?`.!ctvm]xXeEƣcUcm2lFKΫh֑Eă'L2ϓڋAW! dÊ̩7ZKEQ"QT$fJvF }+k;725N" @tcPK^}Z1䩆g;}R}\ У,( h1aoOW!de\_ov@ mj -$< W|^/5z S؝rH.oi)^ G)+Y_4ē!״!N/y62GSM|u"u)T0٥Kq0]$]ffnSͲv"ڭ= cluޑC Ul2nζ1 21dYI};KCʽ(g_cNkuTKu5cryJ\WmBCϠ>qH Dʌ밝d?Zy۷oF^~% 81@.S]\9ֳ{KUjxc$"#B^.ܱUޥOg?02=akflf獕! 菸X:&0kȬj-QUIKPR>ΣdZa ~ SR8Kllh@)\뭷1P\p1>Q")(9 r:K^0XUU/:0(@9*2цN1l] (;Zhߥ>MW&;o;LС#} ;Qn/VF(&L:r\8ٶCdzەW$6`\BIq>jxH,ARF9 b!!=(.p ܻ܋B)V?9P'BfGs ]K"?r3<2  [)yG&Yw#jK|ᡀc&ƩnKqHY=هعٻl3 8"wj9JO}K--??_ѽ$tvr/M;Y:z_!Ik_fьi3\Gǜ2kBI&T•gL8OI41DJUa'Ƌ%) \IƿR > QG#88X jPUbJҙtzu;ѿ2^Ŝq,:]sxA2쒓Jqd6.mמX$V~ǥphiу KX2h,tZ,!zI4L@ѓ\r(5/Dru0YBM Ӎ~)\_-F}?A)/מa;|||xO\d JϘ4]Q֞|~|ݧʻ8V'?"Uy)+?Cyd}Wa; ]y]zd*dnN\k)uiF=|3̥U!qj&D)I`|DVD'aՑT'D7$WWᘳWB6rsнs~ZJHb]|<<=:~g0wrܙ0Ew< 쩜ƒbJ2KK+<9`- UcϜbX;0d^yꄲՀlaq:Ǜ%ܦl:T]Ɇm+*B;&w˶ReT~,:(Dti/uߝ p3@}u NYdJ"d3ZW*w EO_}rb˽6'MX|KbJwueIe9#N*E= *Ǔ-].+M*A,m4 ʾU桺\T?Qd<2g[ǿyFѰ~fmLU$O"uq%>R9_oi›ּ5~.W!ԗĕcP]Yo`1ZƵp'ezWϾ5;#+_,Z@`}&i$[E0ǎ 3s~m9ϭ {8ۣcފrNT1 Ik#*|vm| jcR%%JDvxpNIq4$<ȳtu{5J[@t)S+kUE*ϕJ>K]4/}Pb ZXRgeޥF漯D6bH{_oL%YKJiMrVNr~ke;?Sz%fΘ '~5 0YKvw> ݃$zՎ㠱0kpf=wY`*2&tVC~d/HNPt d-Iv8@~cVF2BkZd+E slZΧf"<* >nUpYkl='͔ߓ hT1+w,N]6EX}ه89°6C-ZQ,b"{wSԔ^fM,X0Sr_v-;?`Mp꨾e͂俁9$pW9xI;1KpTCpz5毭+;o<zY " fglP]\rdB:}Crđ='RI 5W>v9\NhSU_x1 LGGfl 6w|uGØ`{$'k~ j1vW`^G`U5.t@ZJ&ܹC"39ٗ@IZ2Nشi#龍K_ DjYwnZ@w;~ӻ#A-f_΁) f=yL^8&q4<\ȒwxF-([ {c-<˹ `(2ڼK &Z軨 R42,jP^ܐWETN{~ٔ4D=23A#BjH OoeTQf sy_c2(ʐM$z@db*-?uvt8q]Lx(=j~ N_c 3cP@m=lpL 0kCcќ UAl'Pwx+oJ];ȸ?ݭP>mx!~^@5ϝg+ju*6NY͙R!~à%HN?&33V./qΝŦ˰A'u"f)$8dS\fwAWK0]D ;T+nc ǎGR ~el˵9y~ 椞~/JH&M;h1ܡcZSȈcy 뾖2b%S2"Q-aFбSF B]zOw:A"T4wDAU 1Tnkg:rؙGBaI^IѭɴEX#HL;x-QZ:Xe- N:UpjbnC"%')MI% )Kf}=%{Ɋʪ2;qW֬JJ۹sCQI62ʆF%2 df^j%Ek-L}¬Hww7|<7/^ohabTlR,fZE{vF{/ٞVh=q<"m]ґS1LL(h@hܰpѭñSg:$Ci?kցD0;=OR[}E]Rm*zH#HH`WK/iR4ٶvB5˷˿ z!;1s#sqX8 X- 6YOż'|B l# kJrj| xlqZލv{AYGⴹV=qF*c7๧0*<K F<]~8wn&d1z3?0cɣ,|y5Zˆ L7mBfy<Äj zf|MU$!_v5r9Ĕl[3?Bh6Ǩc`Gه?l^Mϸikn?~\ Uk䳛QpE+Ψa_τ ՖCFP&cT`_1v\OM}}۔^=2$^S߉ϖg { Dni;X+63)ml_T׿3?V9zg&J9b VֹLý>R•Ͻn( =E@)Tc4t xvT So~A:(z7tAna)r K54 2 A1?o؋Ic#e5lQV\=e]2RcPC;V*FllGmjc{)ch#֐T@vW,svąF053w˖s4tAd ?˿}1{EK :Id/}׷ReOiNpӶ h.it2ga9[wn {Tʀ3K }|=bzr~2&L_|@G5dTz w¿LfF5 ETB:y_q`]`&kKUMU 5ɚl[w!,OJa@KM΃Gٌ(… z2(SPPHx!XCv}EZ> g428}{W"LGt!&#Ƒcbά{pG(;i) O<<'qdZ3i@ӿ3PXb'u4"@`Oּ 8SJWhI UϫLЫk\Kuzf9SoJ9bYvG<߶{a1`laYˀ6{y tFЫn598{Ve1uHG'C`oܶu++,à}<n^>8M8u, Yw'M5r.6VF _A;ogd&ȜUGdM[] {n^Tpq [Ad6V]挜{PZ ;O[Yyl傼k/\m PinPrc4ΐr7+^^{-E%L{2PdMs{qp."e)~C׈l1ggD%ϱ5=ɮc:Ŕ =E_Ww9;D`:EٰkZ1ZY9Oؑ^{J`f{-1FEI<y VK/Z 2i4%JqbFpNBAU=&ɤ+}'j8P|{0@冭o`kOI:B+]&jXa{QWES 8P4-.3qłuP~\/uc5 Ud-$g=6(x~%'fQҗ #0 GSNl~ұBC)J* 2iXD|JJʴ1z$gd|qqq:RawD w݆B$O\z23),1p26TOʞ1Gon S6wtr# F%xgnوʊ |G8vJʾ~$VQq6T#B CߣN4ƕj ( 4ǣ׆J#b _=A(l{nxX'&P * kF:mTrz '13g઴m mFYu3%@k~~*vb_ice_Rϝx^C/2<ģxɹʡ{SBX`2dx_);}+&$]={}9̵;УLyUa;9^T{.*E٩!0LXHOwFM*%H;?s1OG=c©YX^_ {nxO*vJo;z uL0l3kKuLjYs(]DhCۀ1f*@psXf` * SG6ҜOޮ "oZ串Lg؞zrkyDPA՗I垕kzJ&U%\?^R]<=er?d}pxEl]omބ֢w2 nZmL_ո>N9S;sJYOou_/A2F &= 9 "B#"Zq>{1==(a \[;W-p1Z 9/*dLȧ(킽U+:QQ Ipf; |0R@IDAT}/"Iya!7ڳ,LpYݲBerr sH)l/rj[,mi VW}_\>TrՆP97n*T3>`_H?}4,fR1vle7t`~-W3~6O]FmK]jYO'{uBc]X`U)jì4bҀA2u0)"һOTe#ZnyfpVX))qa|0`[ʳe]ˉ(ACh׶-^BYΚ]zz#ҥdaR,ϽFv3  ʌ1l d@Fv >;+ -\si@k)|F;9;ׄonS! RS[HjnVJ erEa<4c&PLq_'3,țhD+=c1@|HN>elm;=c/W31!=~gGP1oЉcpg.`f4r8 (HU-m"ly>hhU2`턊<w#Kǐ傌ԋ.' %WJ-2 N XbDIUQD@Ι)#PLt(ϼɴ~^ I`׶8IY:K:x"RewGO1R(ZDcd_"HL%fqۓAYEE4ls 0'/BFd&i!a'M qhC`m~e6\322BF|$S];ʍZъZ29ƀ&a{ؾ&F 0EY3뙒"zL,"xmM0- `8Pmz*< ʆ`ʢS*2&ɂil2,۲'R%r )6YKݰ8l$|a^} 'Bw5fIs|,9|oIV%GkٺmIsæaG(tH]d`ㅋaea];. #+B%{R&\S15@䉓8DZV8t4sh zd$pUXVt(G*J_@iY:'E V݃}L9x~Est^#QrTU< _oOϲ]+e`E_7c89)drĞB&I5,*ӗ>UULVѰ$-\% 'Fuu96OX~Zy>ac؉Lc'-+: jnlMk_zuFXMV+̜:c䈑AoC1(E㱊,fͦ||k+kx,䲯h! jz^>XRImڙ{(}XL,5-y+sNcw@_G?0x^VdWXAc[ ~=-c؄ӈ2o=~3"2cTGLW6us"ؽ?=6ҳUdu= 1i#CYl(N>HHHDA0&e[Sg~x<} ts3E@G/>C )عy5.ЦMLaNFU{ $8-`䉓R q2+i(߽㧔BDC7XDEGNyWKjF%Ŏk'|j#!%ӎxۃV#ZLQӰf9WB`IS1H"jKt*XB%陕lG+0lpDIzs @hƛZKfYT.]Sjy csYW'& P$RKcYUY[yn<*'*dyh ŋ9o`5l'gpy*i^XMϫ6|U XpWdq\g_#9oG|%9wuiDS{; GK,a텧]I{IkAH|/ЪM7'+gT˽&(]k-m7^Tx*,5eqg* =[_`2 =I8O*m-onV a#LB_lz*j>Zo.El狏SA,2W~׫G4xĺ"Ϸ jDH$,wK!pIŇ#vڥMS6I)L(M{٭0~p&6!Rƍ#XernW|SwjV kf%K9s2lOf #sE, bs3TqДH@|=6r,u(C 㘕S]Azޚ"dwm,8\f{xϿ!Hb{fE)J898rʼnly5 ઼s 9ȃjW̔e{ зQ]|:xѣ?EC*]w֒4;KyY Ƞ_Y]]5-3dr|Gfs*/b\T[zJ4"cڶbqm&?C=KSN~VrBVÖ=ӏ=7Mf33WSPLĉ# w+u7σWo;ヌdb{(lkaBd JIIIQc 68砂RwM\"kkhydeKҏ$? zzԧ|tRPu_OL$+B_}  C|+3xp6FG-6'ڀ2>7UJ(<mJRυ}Y֭cfnIdbF:RJ8V|2ve+ܿ^2[g̶0JqA ߋj_ۓzW HMlmkT"̑KJ۹R2wAs/ eSV}5"N}3&^ z*){\LbģՃ`=Yk-m4`N g)\K酘0iR/V"NxbYCLSlIq+B=uQ=cF]oل?]l}CʃI0qh\%V&= +ptWL:)1qJ:(/_UyIٸy2y`FNONc1bhV ܄^!'/q%nUTufI=l:JIt0<<ŧ? ˪ nuCDQħc`uiG虴Fz23 ͊UxlȞ=y @r9=uյ>aRӏP|YT9 /rϠ)heaCI鵴)F4&6VP* EKKK@8s&Uy .6+ۏKZȰ\\%!h5FL೪7tgd9*`byLƶqm[QC{[`쌇5Z[jH"@CKhogD}S˭YR6Ewfc 1ug7m =ڇ}sl_ĴtGG]_QOˍ^yse[^=}ŭ_/٧sy1䴿+vgv \ag:;YLf=~ )wP4R"O$ xKE{\YdYhkg4r`T&>k5W| T`"'BGBcA+3#0`]ݯۦb<;}}ݲØ4r8ҋE^Ni~ $%_m;ѕWzmne&CmC)__'!wymN` f 𶅶iCVs>b7bzmZO6]2H7(3`Njހ被  W?| qg,# kP.؅O@[xӳ}=0dz͸g&ܧD`ؖ{&uko ;S&MlY)~=ǞzZq>sD,ٴ e )rawLRʽ7]d:g`݆ +e_ˉ==q mn9,`B; 6=pFl8,c!0$xGI 6Yqu<] [RL)/%i)`MkgrfDL̍n;wl N+T ̳-*k#2A%Lubt(GP7H9ػg?yxUHouC=v ښIn 0lT^1kZE@8lxmZSnU.q<ȼqYm(`8B,$ENNjN>NGB (\&G#QH=: r* Ҏ$ale'c-t2ac+de'eˮ[$@ i~q0Uqݚ|d=hF,>yN[n{ޣcc7"U}DyJ  3h\G>fg`0jtvjrH,P}ƵdY抭E:~'Oe֘4mԚ clA^StℲܫ|(I90_D'@:xdx@5qp1SSdB9[e_"3 z:}><1@U=ڍٸ{ʯT -emic>Ӹg &#1yd?],ǡTO0bQ%= P2 `(ى_}WkTk0dGeJm74n8 *28UQF"6!Uyi:LrxxbSxjZ:QH43=A\9"[7B`桓_V/k c()lO ΝE۶{f?z{W}x?`DJc} 6AnʺәYP_r.>fiBҪq5:HODMFֆtoL.!4&elԮ B9xo?8y,+r|=1[5 h^B|{.&`W䪗~[U#v – ;qٔӪGO?O_e+W>#]I-aCp#e%E@li~;sTnJp)r}{`YVlv ,Ub-3ET6zd%V[{)DrBANfd|`=PTZKV6Lw9N:E(+K:[7jJ[7ӣ<$-Iwh&%S _NI-#v]IJ8uRTR !!-/S) % Q^qCESg9vb,_ SXX"/ږRyhtH2 wXz}ֻ?x<A:3QLA|ŗrꕫφ'iU':u\<{{#HCb.Roنue{wa7ap\dFxTKIQ(\D67^>^ŭGڧטKKyQW^nj+#5$_ -- B^Zwrcu?|'`êyu>բn')Zբ.h~VP-'.LH$I]vWVUi3MMsO<;\Vlb@4\č_%hn%c 4ulk\\AԢSDC3UU^ygxouL7M|)ڷ:V# ).%(x;YtdN\(U&\/o|Y{F_D@ywʥX2(hA>h }A'|h&fFyU/=1%9c&ĄپPIB:.&;wyȩ 1X|>*+A)}z=|Mewȸ* ~`BƁ \zcc*<8x݁~ہؾw7ƎjwtpR ?~l4߮WeʊJ 9tHɢMԥ#_%+;NJ M6&dFVL> wϔO?z¹Z2{&ELJ c& sU`D;╇PFkĜu@.(~9.& 60/0sߞ䍴PyC0ضU:u4{_E}HHB- ޤ"(DP+VދB F\&\b@';}Μ9gZ'о5?v:}؎ 1(L&@$M4cl޴{mF&nL{?q!e_AZYgu;OhRE&g:Gfm*%8~[~ʱcaK&CAÕA98z%#n]jym1|~\>АP&Lcʤg`/UU:`2XAu:ڶ ([W!K'c9 a9jQiIr]#|ڽ+yZۚ1Xm%߹W<7E hKOJYT+Lyvld}LV}\'BcXr.s|4e3޴s3CSh fN5l UL)]Fe>lݲu])V :L^mږy2]^-1N.l_&ˏw G5ŷgcnlC5Q2 okd>3x¼_2]2{ X#n_,R՘w)ZP7ա:7|DGRࣷw[oo|͏Hs8r3Z+Lƴ;O]ZEaBKfÆeu VAxTfƘ|Z{Wrcp6F6||.܎95gܯO<>)ubch<#c#8Ѫv,H3>r}zdwT;5`⨏GkT5ƀ6)Bg\6t5 r%m&DIBpu1~HL$&OT_\\fVD ռ/)P=)3ܕ\,1wF7:A-2].#uǥLخRoȳ.}m6'u%zHNndVX1cƨWLrJklLwb ;d_=WpT-V:gV:6P($H㳡m[i1;FY4 dlo'f SV hQ~m|RrJk.G{K_;MX_zMl۸na%<wu4P:rH@,_ݦ?yQ~%@ҷ,uw[ԥbcpF21plUl_2*S<.]bm;(`÷`چeZ` YE+F@[~.8lU"J"G[֓/z+yad+:׷|Eb^|~8O\Gٴt2yO%ܻOވ(D8yl/٬a ҇z2q֋3 `=}?uHiߜEO=WwFF*ە-s[! U^V9Tˢo)w[V-q,Q?Vp4ӔPֹ9f3IƜjYd~,}a:z@Oc ٝ6jbŒ꛿:P@G&d>l^'Q=){(Sy5@A}d='dkI'`萁e0ޅ]Ėrw{ j6u6!YG?}N^XM F*GI'#SC݂dE@`i=왣q]MÞ]Axޫ&2m=]%Ȯeٌ@B=W1MW|j`jnSGH-.^n]ۈ~Eܪ + 7AA(ZrN”l6֚绡4{gOls61RH99"d{G"Hy?Ϝ9Sd2~ôG`M޳i С={z2>'[{& ءOl[2]Ҍ/f0=:( + jgXPT,LM@Jy\mL@P?!!(`RIޕDD y?z"LEbT RvMʹ1ݐSBk]"[R(Zx8қ݄rбoC:ALl<TKoQWV E$Ʊ^ҭ/L)Xj꧜a4ra![=L2'Rɴ)M_f Cw_o~?V0i"^NLЖ!X'TZ2 و,0٧ ТU.]{bĄGʕw6H~6\y+1h&LNΣa6*sFbF7sXNH~O{b_|xr@UYŧģx[Mzċ <&ָN?Vz@Q&O~27 o *KR[;1iʒ1n]UWSk{`C$7d7^9]q3kZSCXG萭g_N?i L}yRϠϭ3}B`\]ˉUߣ*g\N(уv+5Z7ӎ:dnڰ ?, 7>l6tqC'ᇮ+fhoŎL)107D)MZ:njO#__٭0kgNr)ep)ǛNY۳K0&˽ΐ۰>"6 jyG?$;ue5ūEz]Cy~.!O;0~v f|9ǻd&xխ)n݋rJPѣ i f|_PB߆`a>97nK0jYT`B󞭘LWOseVnuQe|%g-jH,FiH\eцϰLǕ17|hܜ9 U,I S]=[:{ Kp_?-)qI#5؇I[bi7aC5De3SWkh)rLLv\q+nM=q)*!h>'̅q56nj\Oa /ÍkC&%H(:ʺQ5]OXUlWYFݎ?~W e6ڱ?S{ {jY5K:G 'ۯI$nݻ[]P[TZR`CʰQ4XR8#Bݺiw1k+v#O>Vb:ϼNDG`vvvLVU ,;YK#19>hvr RSnp4' f~N}9Gh!ʢud;>l̍I@߁ґxsJ5Pw_ 6FasOO^LHћ.VAu3I_ѹg\jn_ ѣNSh>? :wےr,߮/uJѮ L~vX]VeԪ 2Rgԯ7dyeoՒ/ql8$_K yJf_ǽKZl}msزe_Qg{WoaX*u{ۑ|J[>GJCreKA{泰CV4H~.o'3ْrAf'3BS 8luѣkkd;jOV>L%[ޟ2uᏮZs [AR ~yYʹ'O6búYzdQ^o'P1#%MpټAtA5{ϻNrr %ӽnapV](!)̞ &u~.('HqpM@Ĥ !UKuue,$R⊢M+!-[Q2%{|a[« SY㋅}0侽E$eGZz}C)'XG)s*XY0^@vC)3Fq;ZM=t J`ag WSOG}xWJ^$Y 6~տ+W?u5ަ~~\\\(hJ$C* ^uB {ahdɠ]xq;F~][XoLy .i=P;8 Ql_)ς /wc8/x8j7!kQxc?e=ġ5dEsO~WÉմl|.r>2$oz浸L6eqzPnc*^@)j4H2d8*6S1zh7ʀ)EaSJ_eC- 8Z $=})aesρTX[p8ls7~p 9MGxX0=k7W_D2;p12ۄ⑇&0QFӃlLxcӗAmaMZctjeX;ټ|[6akZcS#AC[0Cl7Fq-̮\Sm.PRy_JǑc 0NCg|8_7= %t,?'/G^-5EnѿPe<@IDATuOwOH_T[67 i-0aa7n C@﮽њ禖Q;v{o}-<_I sČ_H61-0')$̝3L(|0c `8k"@ah@@ړ0v ,.?Z ̞К-Ha&SjnA),Y(:Gtr23mt"ՔN%NյL5JۓXo@!_|Eǯ+ASpk(6H٧KQe0d.ZRԎ(1 F[=>Uɬ&h ?ԑ+ە(& qI21esqûX2]8C}:&ЮcoϢSvV/[KvsuhFS&1QRf(Ѽ].p 5TAvx!̺R$LK|C[c`ݪ &)*Bc\E/td,UeeU2wwn(\z+o~E}w_ H3jfxXb bcbQk %B_/?p'ND=x0O+ÕM n0Dk}7t߸0QH-[8FIuQdF͚Kxlog)ZN~sV` ۱L>}${XTfGZȳ :`ok/)aȤyTSl-tf&og|iIw=vEc8hF7l6_'egcg>ts>Y10Bt=іzg~e9 lXY^X^Rd$AC砮+FU,r Wi/ho\dž&WD8؏S2W=ݹ6GCϏbwUEwڨ,-l_Q+z/.T "-K ܋mqI2jaj;vIx TٽZ)վ>w[ĎN2>^hAiz:m&Y= Cj`s DX_(,,u%AbJ7lv)5 6S'!ARE]m 2¾!eƌv~I[ՕK}wό6ga)dJ|Qff 띶/g׋[΢}Yu@+LHI)i޿6aPJK~Mfd J) 4j@G4ͻM/XȄe gD2W&Y2V #+& Qۻ7d&9]QG.D+Jlb֜Ѫ?>5eON"{Aa `UE| k.%M3C`f(#;ʥ⮒ɶz*CÅʹ b%hH{.ft~%5Ԩ$|נҾ4菤koIA3"I7;Wls 1mUv.tO.y''"ޫ :#L We$i?&L_l׽; veC1""3!_w9ܮZwʿW_/A.%]ܚ( =bΟq݁lX7&Y$(8 D\3F{оw7?N"h֘ܗPQBJ"BAw6?&yLNBPP pԺ@p AmX(F7,zD:A3cW' ,ʀOˊhnf&\+D YC"zaGv5CaK&43/X 2F4ǡJ}F\E 6 )ŝܫ%Ӭk4=8JCD`LU]$*&2_,9w&&@W޴1Aօc腮=z)~ |vt4,WTқ֓  `A0rc ٔQhleT2sc޽hBU "ax{:;݃fak͆3B€0MOֵdw<PqAV)vl]/'C;~ۃz*#e۴ ~PSX@'v K^S1K;Ý`Y0E>M>Og}Bg%D?BlJ`cpq"[=޾E3ߺѧ CSӲzeSφ!#'W-"ıCaDQב@V6vd(G{YWZ1qr/IF8>2`9y|mMLԻ 2BJ#?0KBS^ØCՇ89nEٗfPz|O/zFźKcjJ .'h3ī;P.7ŀ>oގI=p1&z ={j1;HL{!4t&:]?~_3&iG&Wfr:t֨jݖ+JbͿCғX`\Gg^A29Xz- e{^_abB,Ы[_1Yip4wj}Au=n8$BC<3DM`oW|KT<xoSRЄuT~)ܰ9qg0^KE\}u :ىxf“䬴s[5UY\.˸ 9~WZUK޴%%+o6Puo޻{'&"#n6.sէE.@oFBE!J\`wiQQV:N>Ϥz jT[[-+ ȹ->zwZWiM巒"c24/~חW_KYѣGFO KW+p*E,ceػ3$tpx<=] yxLdujN^0₫ʀ5%)fbZe+iT* 4e*+lқw}6dyT ːK*)9sPG-w̢+ GH!k1V4i5Y'2L%c9Zρ,h!&,(oڠ‡>R6ZE]5*)gT$ =YcűQ:a B aD֨|h,laBaISmA x:HL{yy ^͚6QXdX |ͷZ~@YT ZG6[4fd0]naڿ\zR2 4j5}KJv@}Y6d̹" -k&;;7.*u`OVLteQ"5xn1#P4d!MиcF}()Ah(v.:bL롚rfQbʙޤEeڥ\P `zC 0[Vޭ!gQF=_yRK=0gL~pM6Β!ruog+qc*'hɀAu1fu,q05B[`\ij $2JCWZf'#5lJ 'ف>bA{)͡<,N}4/? _Ro s]Pau.V i TI'5`nzJ੧'PA?ǡ#$>C˜ lN}S2дE|ЛoPB5Ƈ0=MPDFdHw9^x D KR^3Ca2ıa?C1k_F h;<HIBJʬ5BR943fP׹|Љ#A>\P~"'[~*]$)&($HRRjK [95kr&H Q< :TU0,JA3<܋2X$ĝe%ÈKL\xt9yxw/#9- uN?䍙qlОC).Y0׌#M`i{gB9A /ep]$AV)~Ae&wW%TkP]juT/6مy$t +߆l/&=+h҅,]ַ6>ҢM VDacˏ굗av~-?@ϞyoBطW'|Wۓ;wv3Ez=K2_;vEY+DNLY* "ʏNU[b ζnJ#Cޡg[-+g#&)laJtbXQ O¿P(>r=dXEI3g(kǻy5aanwm7d[ з_2l0|gؽs'egCV}Fذy Rҥs[̝?cw[3G*HdXO mUƶJTfRT+eL)N@9Op|k[Vsܸc97 Y=~ Y.ƏčLʵǣ~1&dS-!m2Xs.s|kesM9î|>)0L4fB5EMz*&9 wͩ2#L,Ja2ۗ6Ho%[̶dε2&!z=TJ0`b7U|⨀x׸z%r֞; EET (u8ͨ'j)%EjA̗Yn:KWU"c~VeWW7SǢlk>jyknY{w̉SgY:4+VQ~cɀ2i݂@JM,{Sy^:մxfRN}U6:~Ag~=Gٗ; jd*`}eGqn4aro pl/ VW"oٺ~'7”)Ϡ]۶(/.֫)Etkm[cu0<e]Z*ّ|]IV1:LQ;K`O*wd"ς YC Uz Kof:> Oc ;S=8DSt# iOQh߲ ydod$6" .ɠLbXt2HGϝS((dRɨw¼;U1wdnc p>[N>U b/&Ѓ<|[ho~hߩl |7o<7⊴+WO()%ėp~X^^X ^Nki`Uڷl*Ed#% ^[Z`d顠17F:帮c T_/¼9_`ʍ9Җ 6vJVWJTw 6!ʮ г4w7U%>"9|N5(wJ-yOw)Ͻ9qx3F:uR3Y$ oiݴ?A|='0?_kDK>`_S,$Z<1e< uuП&ͲaAN->R Kr Qb|w/ c&xd%2RY%όzhODŽGBQ#Cy'#ђiM0gWgjkxpڏ4Y`۲/ч>eO:7܋KDG&ТǾx)K.<|C?pTK~ HO.SWe˴SB*li-5k`}u*˪>ˇY=w[f~6YB]s[d+| W?OulOJj)De8;m,b<0qҋ_)<6m/P>ksۻËk/MTGi'{E,YDtЁ}}d_tbQaȶXURQ&0i<1i W^ŋҕӚʸI|o,!9]7$}r3&)Emʽ~W>WlMUmL^{2yl4NG&1$5Ɵ&4@-rZ"/=Iy:3uEsp,,ҦBj][D֮Gߝ Iִ 9Pq'"i`e$>:ܤN#>{!dULp~@HhbM\אyi[򬯍P 'mvaZT^WLHe[S7 \=u_1֮z_VL3E~Om8;&/4DӶ_h)NR?05a li<85vPd< zKʻEm\s՛q/̨>y{wkbbd~ihoT(<:g"gro2ȑcuBCYcي8BRaۭg.aL>G q'}S4.\> g`X\r CrQ"zOQv,P⻱KgH=O ^R3 䙔$q<=ڑD7zeGά[n`u8-+.##Rn|ܩȹI(E*_iO/%c8!n/R6vɍ9ڟYɬo-8C `8] =qck7G׊K/c歘Nur&cm|HY:vοcٗvZx| mW>Z`%CJHBaS,T}>$ ^T D._#5PoHM;j`l&܏φ5rQVۊz2/ ,+Gі,;$22-)Y_㻯>OQ3_]^Hl?^w0vC/u5zTwkj#+J`$:Ni;{sA$R(;ٜ̆xV$ ImJV6:QIekA#y$ح)Y\lF:p8td/:w;=,)ޭifܿts';8qNӳ+ D ;=!߲̅s0p`[?g{ah;-ei EU@0>ܓOS%z`27y*uaPȼr0 {цޗaA+-"J)+N[j9{ ͽИD1F, ە" tDm;/L{31aeJWk2ю`.AndX* n> nrk``Ѥߦ탃(t}{)daՋճS^wn)SLrTW}BPBpXkp\4=9sq:: Z#C7ll(9|!zR'xbӊPjs%Wxw(L`m2Qn-aLnO[n vQ# |̌y,l[֬1ҫ7'؞kFr,NhuC.(Fٔl5~Zʕ)厩"`O_͇\Z2z/\"kЧ,L=<ޢ:VaWw;p&-zĹ?Q=Ҵ"M99*'_΃`t;w-ѷqd { 7>Ʉc)>k#렀Lkԓ"vCC} L' a RZ#w'. h8JBB;*fIo02%pQz60}0HLQvEn2Ȃ5%ɢ&02e%?tB;t$*K`C&ݯo0[UF2޿o%=sPo  +1{XPmպ Lrȃ=~{K(kԞa *ڻC%MʝLŨ4r4RoGs?Yqg}>e9O1pOkNV?[&ODvu6vPbD"vU8uR[f_玟>fL60cضUs8߯ATOHŽ};`GfizR6|&3ž;w;M1%? 1c4ٷw&+Ov;|~ÒvH)цX|*f,2ЈNpػۣ @~TWavA~#ݰzu2Tבe p|˝ȴG9}̛S6!O٦ܗN%C 0s]Y"v bA`W~e&a(-!Ӟ{nȤP,~t]*\hddȵiDi'auS^Q׏U9^D>b(RRSwAx!(3* {NP u {eOg~*.+BbR F 4}XԞ~V14r Β ,E,2O#&]]5lp2zp>]kJl#}e$z玵Ӕ+u[LTW/Vp ~1Cbm2icg~uZܜنɇ!UM6P6j;^~Lvdd!B0¨epjD"+zV ud5|), S$q)”ha+2ֱM`& r EeoV9^Y^ֻwTc);eRuRڮb@IG7yݰ֯%ўq4r/ٚݢ%븒%jfLkCMy|~WBH&&mԟEKT5PI:`룛}H!O2GM;]7Q' (.evҳ].:=0o\L{f뀫hS[TB!A}go S?{OwzϾLk`c玬8kJ6 {ǤnEgàpJI+ XmRB?$=#TjDG7_"g{erʊBYH`kv}[Uݾ%H WS̢#9ҧ3ZOŦC u(D|4swU4Ќia&< 1pn@w;exujEJ1=R(Gz/mc*g!c,0|YE&(C{((~ Jd&A/瞣e A T܏m?#)M\h["LJũDJ'A8]t=7AYLjH"J^j57[(A=~fEVncSgF\XO>Gν}0z8| ⓡ|,K۴x14pWbC9k6Mp | 8LðߜÝ&xS5;-9ٺ%?7OГ!4-%+ =;m({lĠ3gN&]zoJg.Dn0ĉgg1͚8D~(oOI[N6V0'?9m:ϸʊ2tNv+6oD [dՏMU@5ӏ&QJ#%RKPv/Qz56~*.\F6M dRRSzWYeOa1 R~ M}x}+VCCЬeKџv/Gij|G*+DYYX=١nH,l<2e⎬ 3 ?=}5Ϙhk('$;IN>~ٸ.L mג9Vm;Uh6\*dJ:9{ :NڍLz w ל჻XOk> &$gUN Q.X0)e?>[3aпʀعz/ (KyKgUyY̥LN8}, ln_c3[x8*,d=]\V,Eч.YtV5^|Mx]7f|NUStȘV*#~ x,{ѳgNJ>6qQLp|3y ^ ]Be~-ukѦ㲤v wS?>KCІknd/ WKKR*I[C8^C0aDC14Sv®e1,r*EMբFx㟴e0QKqGY\FE*+ǨFelOX}hϻg }}̟|F\%2B[K)G[9tXlFDDhSC;y= Kg~`/,*gV6(v7R{aѬtn 1F8QKxWf`dW5:20~ Z4Cuml *2GD0G`x0N@oHÉLr@Reu N>Ah@YP5IGѦcr*NCw$bg?8Ȗ1m=+ٶk'L.^̪tf"5ٷwweǂ)0{gas^ٸN{! *ۖ}˸!_fmyUIQ=YVאGfR(.~nqo?iĚ$Wwsuyee~%lCf[wku K:Uʮ?A&Kji; Q@cǎӳ hϛq)D'.l^+Pd>zVZS\E vn7@0&.L5kx`\-!ߋ#qoSgխI/nȷfFTFcݒZFkJO})uCd\&C+u,Dp墂\d`jgB#D|iץaUV2LEM#cXSTEGC#/&hL{3xźMdT1N=΀z%i YA2dt ZIQeE)]v+ҁ}3@5A9D۵WܹxWPBa'oa/kQ|1Z1X5ޮ8~8 U0D ]GZQϣ]sthMW(6(z_OQdIdߩ%?ۅ-znd֔Ól^zn[ xrfNpm&mm3q *TgvlG&8MƑ.f^F\Z\mG4MZGPHJL"tu'#H!G&Y>ں+$!B=!D@݊(uګu}-ŝ@A"] ɿ\&\BΝ9sf̑Zm$]N1%$Vmd qI=u]R ,Avа/el&{0;fp3c衣CN2i|hIc=aŒV>6Q4#l2A; 9xtΛG) LeI]D3ûmS^tDͳ1 Sc}$[ )۶5l{:fuu2Xûg؁@{:)#NL|ғ3N}Is ;smjjll ԓ m5嵄pT7K42ncCz@IDAT*p&| v\ #>\MQWֿN@ӍsWMI)m1S㟚\kyϮ!/'qh[( *<ǖNHbD3 Ik ()@mڭQD͓q,o:=?Il<#]s4}j1LV;{Fbw;6h$RJڂ7>H--@` ԄK/lq7 KH00};>yq"+eA2=zڿ{Y ]p2<?&LhA}'/o|yL8Eql8Jץsdڗrl~ 5CwPn PaIeO C@$I& , r28С*/Lff;@GTUe"_Y]^#ƺ뎡uIEee4&GSUV*`llKU" =Ęٔ5v2Ξ N: 8 tp}\XzFeRB0*і,ΌmgI >v0XQb"1HT#h`0 ich75Æᣇ)2/q[Ȑ4ci Up&FU<лg[^qٛBwV:N,LO9M/><*U 3-ϓ<_w3< BﳀȎLB#OE;ЉNMXd:~AEd)ᵫa ^0unAo`+n&>I憆+ :q[ "^ߞv}zE<,%BrӲZD&cuf(3fpS:`SN_'N`֓!]:izPQ joiqt puhV(l!`*9D /0 YO>! F +*ߟ<>[8A6#;scZ0\C2m[;:JI$}m +1*%{mz&U9sf #8FhJsԜ ջ|^w`*c]@IJN98G(SvE-Ac8ˈ=Xڋ۵tX ':QHQJkNT R@Sn~P۔jk d:jdS77NO knI!I3pXh%  $2 yL)\-~)(ɔlj=YL6 TD]D!+uBX'$K" C.ةH 2~V(C:r_]9 p3kG+Oٗ@; |r ȶeDDEqBP&/Z勿Bsc cԕ@Y~nHG7'M-B% gPN7Ϙn\y@p`dD^W$/=6pc<'ȒĈZol$:玝DjB"rԜj]wOV2}W;b%d;ٳ F9PJJ~TYdžuT1(d)it8S8-%dOTʕ ;JGRt)Q_QnYuYdw jwn0%\[^?/Aǥ0.cPm?-AOK= WҼyN8us'H`RC뫜YN-k;=Y4=x4PSK_  ENG4`;/ݙE7@AIB) bc RЦMkYQnrja-?L3(w鏴|0vQcHC~ŷ2erx8l ‰*X02d1ٹeH[V(}]1&xC!FM_>B #RY8{ - D6nn.p4iƺx%eDѱ2yn%d {e'g*,Kaa|[أok2/HMi(J&iIqxqLL{ -ru`(ϊ%w" $6>Յ]ds V=nd:w3dl r[UFAz(/`2^z7-v'"/ɍFvmۣnsJ[1ޭ>~_F&p-sv-+ U G])lt"ZG'Jq2U(-.+F' "ҰkWEFLigŠϨGmxMN]heѫL{&i YWС+يz3B['_|d>b\t߮ 0}%nj^y,v(r- W)G_3T "a+OV3d}y|gU=U%1`u]tR VA5/E9olQ֣8-Ģ$E$Y{Iڵ4h^*S}C;[8_wf/)CG\[:VI ><Θ!dϚ񠒥 XMqRGJѮGO^^:.E)2&1S@NRSl6$ۯΰ.U%S!N?5%7bE3cPNGHBJnFǹ;Z]t}L-&W_y(,YU7= K=eqr͸wZ-_Pb9WaA6YsL.j֌\}f)ƌrOw!" b9,:cv9gU,T93O& <32OgSaǂhMۥ~ ~}0zx|,N/RIt7zwE 3SGpTՎO~YsÆQ|؅^n.9U}^},*R6ڷc;_Tj&.;r- ;h6m<Ze|!ǑcKӰ/.u~;06þ=ǹX u5c| Rm8Mjc 4z5PAN'37xU׺[mdpR?yJM7| $g#Jow:_VdۻŸʛBS"80ǔx];C!b.FKAv*=oA +] 3&!#q(Vֶҁ FK7gYh! j[YذfUoL dI,y :p}n~3ZS!cMjιa⃡}mARYk~p!|?eq05-pu`QcNîdcz *}7ե)k/cvO?]*b F)lP+*=){UW10vX `U7V\EJzNk~d0+AwvF!Gu;2r8v s7SbtJUd^gZ'r~}fGpG z@km.rߍ15 GVmk殸ڳ8ʤ3}pW%^Lˇez$@qH.D;=med;gdQF2{Pz|CdE *Y NPSkM̔ČgSБBr,_=uJJ[#, C#zw±uؽu)!J'7_KV8~cPJ)O+VH AжNmlR[s+kMi4JE9v>E>l̔]6pn#G+˥yJo6)gYtZ2sVz̮Rnc_M;?th悸\=0NHq?QgȺ#1ydFg#m@od&M@mbV(3ݲm:LR؊I"99Q11mCDeof HGnԯd^g'giiB;}oGP}ӯb}=dnk+WpyJf[Ìj;T9py9p0(@na_{%p'GSj,cdhRQWܩgBFd_i/% hoSU^nh|䙺ZZ29֠aWàV=v5ANy%NÒO9iATܿ7?T?_Nhӻ?.wlRS!vOZbPL>9<%IBL1h͊*ԟME>W)d|"8R0梫 ry8*ﭼ4k ddE`_AU<$*VOýD*8ZVN=,ĩXԏj$_y>+Qa {O82^܌D0u#0jd:H‰h8mr,` ;]51:>URy`8Abeg}Hpjۣ`SX[ MOQWZQĻBYLŴ2Oljͭ(Y/2BJ9V1)2f/R6etva\s]V5<BʭWX ؠֹ>˲^Oi'+L/W]pϿ{Yy䷱gJ/=ĵ@gb .?ǦOsZ*U:(QҹMeGݫ/c厝xr,*kztA'_|Op SyHu&ܙ%u C8 נZ E:tgNq錑3LHU3Nq?p߬9>}rXEEƿA z^8PPVefe(I'{2GK`:|z.Dˆ>,qD5K;\4Դt\˗E9p`Q3>l1zz՜vT8?uuu"e9(67 htQHŖᅤTiwdƧG5ƏX{fƒǞk\P{C#ՑCę얶Eo vj[7֖VʘF98=M'cIj,ihj:a5xsk?ZνK~6Y^djjS5kTSvo[tZq_}Mke V1I]dYvX1<m;S6^O6V4 =eybݺ_1i{nXWUJ*Ymx8m/6d#'"PV(E|=iX9|x?쭭}O#E }4ԡ*:t5 ]zS)MMV~KWTdcEfUuE J^xEpͿ ]kg\ӃJb_LFNW'lPB'bcJ}j %xv`Ca^R"Ӱ> gk56i2GKd5E2oE^NIHD_ɒ[-1ß_l,UfL :=5@Ue-z7{K*T64T'29pu`! ;jq4j'a-ڵqr ,"#X*nSΫfzcWdƶSÖF6^.d^AU2udٶgfm׃l2dc$M/=5GSq/=yRN=lMʯjdJr`5ûQgL;[ë#k".6j& cn쨑W[Ny &ظwja4%u#(&t'k3xL5`l; /XHDw̚5WݍM-1}tx:jr;gF={ɚRJ~?~1A*ޠ2$voI >l ;z<-]_(klb:~da` 4uյ0CpVy]ejb؜,Y?N73@CAb$W{j'k:<9Y2R3Z`LT <\,ΝdD] YN禠>.v$,#*,s72ݝgUཷ)Et04|/ u`|uq8ة3(4J2fH}; 9prkW+K)[z8#RX}6~2\3ñ}zpA:J&vUV?$#|$rK=f0u7#+[8{3?.u 5c?/`Ic b>X *r$v酅g1Lb|l(ݧ1Hj:뗓T 0D>>Ɩ2JzV$Re_֗q oqPS ܗ1iX ٸe7f;~T3`t1t 튗Ѽ^-B0JJA҄E&ɉ_)N+aC-CY|V!0:9w1 bS_edC>1섡> ʱ"*c:5^6N fc1"NQ6K@r:lXVtL >kW,CXTڊc;ғk̜9?EQiס#22 |-R]{ŹJO0IέG.صw2:n<%&1B@}oA kW~}9J[vӊ.~p.Zcn^0֧L=>NnT7_kPo\%AO2'L|([tҸ ХzNwu>'jϫڔ o_]~tvs;iG>H-kTV0նNi]T%O1pỮȀ ֭Oߙ%vtEcA#\<ܣ^] 5Aa;B /j9jshEd.n l)d1flkt sqr=:3I|ǼV(\IZ2^-垇?~ f}vX0&P3obT#If-C_];PVRB9bc:*ggbC浍"gdjr$5?ÿ/*J2iwD$eK悑 &*9n Q6 d=#1ZvRl3]*YAӐC;$᧕qԹ/c-~+19߱x6o'F:hNI{Jr6.b?QNqMd}RFu#0(S':Fj۷aLҽ0p8_YYAz# [ׯ޵fD, VSz&9"aiL狊3KsM(I8QctU飮*e)hI}xٰ6mjJ RZ\oZPK哝|֔n}y^Fp_1y"{&?X`<`UV0%%'cO9t3z2GȈdk1:јع VQ{9bxsי! FKc{X"s+ uu37ݡT$HϨXFu1f6lq%WWҾMw>eur+ ɍ=) ~[9(: {%NMvX'H$|6uu1e::cH'-确k/f!=chQQӒ<W{yp&*qiOcϽ.z0P%1NU9kv-jc땈Xgڇ8&+%2`<@e *6/^`P C'x7`511XD3;w >mn٠A&8]$Ygq\5``ki988] >ر4+|yxjޔ~oG CxJ$&F~d &CV;\<>\H|.z_,cGꌧ.A2^W~ "0\Z  G[,_֨БD~~ݶ@ᅹ*&4> g=ptDSBt4Ft S 9#&eЩd VduwѣaTqVE=?Yxeر0r~zY./NDH N2j9R8H -%LTͳ|ϓG9QSnǤ\PUI"1\PK7i`mx~Q%Hݩת^ z">>E~!^G~=]ajdP,sV=sB2f_QOz7Ք@;7]t$o:?]\f`OuǑӴ_S 45PFi+ʉвqBÉdz Ճ=_̇)HoffּlSN`D#/iDIXvez 7(#Ɣ"Z :?w>v׍devVb͹GdEJv -fիi,7@wo#,Xb3u3Buu`aޝF2"ׅ!+'"ђ WZƣ%C ]$x]Z5=4twc=?`'ㄒLe;i DV) x^8nXXZ`1X{LH]QeWK8w+2&C.Zc3zS? xx>صk7E{0|g@zzWstken$G]ɬf}wj6;v(I^p1ȤZ 2Qc98Iȼ@מ,-%J/$ >GZ֧‡rm:96vGQX< ADRcx<Ե7&qsgaAUs[7Td< Ď_ )MLJ2|W*t̘4MFsK0`sh)Xn_صHYB@Ы}m.}#p@[n=X_πэ'; mUdԺ'> ;vAA~x"-<'q1pU+Qs2nc2Vwn1y 6]ٌ\Fiq5&u<Ȱ, H|L>},`;I/62հ32Űw* R1L*N1:uQ۷YyVNg e2p)ꕼn&70#ph g2AQXil,,bXH<^3 ^%SL!T4YOcbha~u"t$Cѫ@`UNLEVFIa[U"lz:ImkdCwvR> G: XJajs{%g#ǏVZx"tuVzY|-`ՆMA 3xyvQ"* }^G?ٙ:::vs_34"*|M[޲+/{$[ڨby k- ɪ:{Fl=d8/]a:{#GoBރ~@mu uZjr -"},]1ftja7XX^KࢗzjWi8IP{NJ/p ,hkոr,ynXM2wtªTce±X8e&E#cLX:qb#ۣp%#X;L`bߕM82,;9 Q3q! jz$L+E$}?a]9'03Ookطf5 .A՟X#s 7~ܱUK^[_TRL>a"<P B콦Tέ<cK/SB -M[Cv|5Zb~0=qو'P]S$#ϳ64`8⼡8hIKh԰!tt)7vJNTD |Tǂb9~ T4p܏z]7Lۯ.GQ~6,nD/ضi3q*PwKl<` G;Sgm-DY"owFj2 % bef %_ռwr/q'T *}j1\~Cp<,S&L hιftt,O V˚r,aN j"EC;~7N=ډu!ש!֓>˕t#CcIG.xIygI s_S}rua0g/[Çc}C:r\EŔ%>AHHOG(e9%T_JWRRSy_+ K{RT`AQI%%.#y'oh8q s>HIJgq2<N8NTڙ _At2>m`Mӷח# }TkY:Ur˲چI$s9:WFiټ*_ rɵCKӞ]Z uCsN?vsgNs#~V8 ҆cM5r<"cPe:BhX;q~jHtMzo5 ,Mg{+p/tny9yk;dߔj?N:EYȱF,Eqdڶi/U[g$FtN6nR3 CGNVW-6!ƧeO_Wn$=K"릭ݳڢ{dU XhQJ:,.Cĩd?ʠ䬃ww4/S9wB/s ?HfFI> BA8vE利d̽xC@/ͅ˥0\xl^O9 4Š]&F2elp2klL0v`2 Ĵh1fW1^76Df^4!~Ј,jpt308VNTXh|Z!5t|JcB[Ϲ*Y}0hɸ"6-SGH<ă|ޜgk-+2O&AL@Jd{jG=]|>蕅J#л]~AJsSRN9dԔ՚@p~xQ ނӟk1 MGuj E]:C QJ2SL~'4rJ$ c}&6,焜wsI=ٳkZ&?t<33/=]8 }}ơ;v @]hID32 (}3[Dl|t #zkn +/.!{Ec￳LMU=gQ*lsѡ0\<,gE2& cƛ;s/*eh=;lhO|%g'h7#="#Q®$΃0nȋvG8x~EKVpԀ}+Cܠˊ)z왓W@Gkܹm:ц,cK;G$ftg-f ᗫض^oV(H3Hm`:DF6B>\oN9~a5Y4b~miZ^GSN韅"J2o^q`~u.,r'vxu(hrbbSPb4Sqʠ<4d`׍ qaN.4!ke\ut%q/!+: iMGW"1oNt3 2+)ٿ cX-G\8mzтxnzHzMW>nR 3Zhh}6'!uy6򏴗j{u_Y}/?W>|/<)RC.dPn۴dxx0ƶEɖ\FJۧYya>F 0' jfeXH[(t5 pN5R_REdGz/#o5~:#l_-L;Sꏱk>:!*ACGSʟ{~y> ,>\䙔aMCKkyA6;¿Ǖbb/4&2|YLC0cst> <1t uྵ [Q52*e ȚCgnTLk[9Iq&b˘NvE?MQp m"J3v(naAG;4Ы#>-_}=1-qig"lڣFȣ:{m{*v;uyA_qv:zlm:Y>N rI]S~Z$]O>x^sQh~W_dfr{T9%K%pմknż)āSϨfa"%]x%0p |b)+I H78ٶ_y_'zDeXOMYe,(+:|N6_Xe6t.(ӰxJ.~ҿH{q$}OsΩFir^g:Jm>-4Ϸ8Z5,SSYW.uu 7Wm_?q13NM?qRק#>5@+:n?q5 깩KV9BS)M5T}9dmzX2R=ěGZOƆ޺W^A0-z.ΌWDfLSj` YONFq_Nĵ:C<'-ʊdOTNhݺ 0X)w'G0Ak k_=&FeU5]PsfdR>3(pY43q'62bvyJiRgR GR&90굪2z(hI;c" UXqFg2+\: Lވ1Dt%k4E Ȋ'zi fd%u1AFd9bVxޕGN⒞:ST|CEP&P @H5oqA\Q^]0h1KEVFu#`> S3|t~44Jz`t?zwCaJl |) HfSi$ӳ.Ġw wA6ِ_t1YixiũXK5I,_5c7} f.wg~&KazoJ y-l!]TPϽF1 @I~xk 4JiݣgCuAuO"e\b :{ݨ HFweiXe4\XJyd,lWߜ8qeFO5iݸi#>n¡;aif }'Wf|Ѕ"z,(2% 8r=8 jm2 *ʔ+hGVKJ):X]zէ'Q6Wsѳ[T̓Q]1N?IF'fA铧hC FG32=1e(*Rh\ߒK@`'cvg@i]}e{oͅw;?W<ڣ] 4Gd<\X*&ڞfdfw[93|Ǝc4 }z1Cv]">?I=|k-jj2UKgdzHKC)5=p.n:ٻ#)Hֶty%d֗qFBu&hp?3 gg:sA6:* 0e40dSӗ(Y,fZ)mhͫ,^>#Eq9| IUK5 4ff\oSd B+QCc8^>o|8}6Y\)>gGRRGΝ= H %1 cC>s"UUAG}==Y<{6Ljȥ×kN ̗\G̺R# ]sZ:$By'Ѧ 髄"2'c$劭XN~c,6R Iڻ)_Ŗ?GJlJҌ-k6Oi#wKG׵ksOgնq N *0d@hkJ՞"m@ci'Ĩ~7K}N?/(Qe1|;bbb9eM['oہh:9xj=/ҁa:{M#rQ7g}j8BN+3nj?_b hZ(pmZWÏS"?ĚHd ::ijA$l+ ;-2VϝIKo/†jxdJ=WJ߆ n OuRHBVeV~>[СTZvJ*kMaG7GlVJ$"f% 2з\ØtpKW>Kip԰ f؋ ¾uM%E~ #Ud(Lch]5a4TJ49$y+d'R-|,h!~,B{ /a?Sh eRrzlC=BaI>y~>7 p=]75GA,k>?CaK ۚ:Mkd{ v k>(2ɸ$x& VG 6=<{8 :x(ڕ{:y_NE,#Oo_ L/W/Lv'.^A &+\T(Ye-}z! upp֣76]/5]>K'`S)eLǠQ)Y xg~HFmZlԑFy fqgWoY{+Ga5^,/c.z3dҶ}fxs"Rxplo->˿E-۵W$_߿ K<~/_"J g$Z﬽IY^t 4WQad-[!Ǖs:SEaʲ0jZ_{)KeheQ@zaϹ6YVː;ᾷ_/[HGnS۔M)yOnƠ)1Nכ42FO3~Y^|:ֵ+klE|f8ZxNM>^.9m廹3?sJ`SH!cZDԾnuPY]>nר]kc٧ϐ/99; Dž J.2") FiռROs^8GpT>Mmb ՜v;rQ4kkPUSA8ato})sŇoϦ׭iU'' m`}/@BtqkH ܣb †m1̾9w;Tz$p\ L{Q|4 Re[Dm|~ނr5H|чԓ##ذn6o_SaUoJ/W֝o&=8R YaDo ?,^xAWmO7 Z< E%xo0㍷ѹ;0MEzF<}EsT!UF˘DgԹQNRGz2Tjѭ1֜oܭ4k.EwǑ6Pڭ+~UKy  >Msևb{NxMxLZuگ[`WGD7ɢFCI.9QKc y:9o,\fmPo/ٍ"씑‹oKL7Q*\x99Y{u' WY)jWĞ{]CW ɰ޾'y0C;F]OD~Owɼ)W)=iL+צes2ǟgCȤSԔd搻߀0L>LhȈF6M+pl[ fn(L&k-y##Жrʃ(yΤD(![!L0VV9i '6QQ¶YX DSQlyCJ@U N]Gi):p/* фn#@ҹlkdh9@qԷGh(_KE~E*Mql, 阛A9=KOad-,CFEo''0?e8q&ǍGf`ОJy"zp|<'!吽,#D;I62ԍ6711Q ݇+IIp:%oʍʢٓ0j$uhvQc鮹L,5Wp:!|~2 $z9t= fti+~E+071^cQ9Vd`ߧC,Z1߄,ո;E61j(lm5/(#S GAAGOqab"+1l'IM>| Jn5 ED D0f)[9+5wPHŝLwHfdl}l81۶]}0e!Y';dQ 5$' Vl\fdkS;cĄGpjYxi!Sgt 3x1PAsn.Th7g|Bo/6)/O0љ==: cޚlDn]v Ԗ!5? BṡyLܜ<6 tym*NW8s쨞vn)OL*h[ԝ?}&5<6zjjXϓl]E2eX$yWe>u{hu \ 9cff P7:kpqeG6uh*gܓ *ٶ}b8c4;ʴD( Ӑ +d\ZM{/~6|(ߪ}/Xx gx\ sZQ_{ũXouV,Ɓͫj> JĉX߽6]9iA݊2GgTaj3K&)Y' Y{Sy< SS>QꈳY{Uֽ6Wʊ[#*R;ǫ=y[)R0DZ놵z'pTucɌ"߂u Hzb1ezilܺ,{dC-Xe3m^hx7~}/[Qd?od}OjHn~߿z6^wub/%`{S{.]K. Z8VO> 3?a.Vy_Ȉ҉kW1bHe=3mCEr)v2'ѨQ;ua|adns8NKfI!}/_1Jex9hpCs!KSw; z"}T* >'Oƈჰyt=[h8!΋8s .&L/7Ak>FN4OyKQAEY13Oňq Cx-(z7\.UG5'r%*.&N1\pO==e<%}Zdl$ϼ'ѨexI[J]d,$e|#vA^e/s8]!Ǘ:eV~6w?IWK69M}d]P@9ڪu۶} ccCju#:au7>W2r4SV}l T c;0\sprt-;0uȾ|E-5FeN:=:GЄ``6UUUh#I;BGSNBڴ T]pT<:tJqv(1Js^ݻ#)9/M K yR[({-)8fg:pd2gJ 'LAVOٻy{1?rkVD~ (Cq\ bc)r/>2#Įe^ΔUHc>cgcD8gܔl30磏 >}?0 < At4/ctA50ٳLPőN(UUNeNWAk"7q9Ȳ 5QY^E96,b ~ 05 JjVjb,-*tu:s&5RiNe-%L^|dz jw]5gwe[СJMJ y.O^ɾLk=mdHEiW\ʄ_\fЩc *(9$4.yR;#Fb^вm{0O n66E#'>`3ar݊[o Mp`v=K:EJGYtK.k `mbuQѧgO4kp*Z _2p~2 yq2H77H3^|aKv∑x/1bvޓz& rB:›LH>ax )`1pde)#&2w!V!sWhcq*+Q3k[TUP8 ,Y3%\(uh˫W]Brqȴs_i2&Su{H,gJE" 5L 2d4khU}PaM0 7n:\gkzuA-0yJ=U 'c_`p>ʈwW$!UUꗗ郘ǨuO}=:t*uEUV)Qg.C@J"jNZvyo+([*o-ɹ() go?tRWe/c !@f4  ЩJeqzDHK_zIZG&\*MyBLUTS=+d11i 4 BL]G~hagkQ{ ӊldT } Ij%k9dUZU ;x٣Za (XTM3}_gZع1/]mkМ6(}$Ғ081 zs?7p<|F7c/[0OuW K'M{ `m ûqI}^cםr\I'`2єzc9J%/0ݭݒ1jc%ĵY"жr,UWǤ\>fL Bƕ9;ѬӮ[* +eΜZ"3[`Ł?u77ҥDݵg?y%kC kpݕgeİϧ1ubp&}9|;h2b0̖.iRb?0| Zl2:;޵۽9+dL/r# QJJE+iO!`ҨmZ5}!f<`sd`olH^ӑ|ҮeK4)ї.\1@픉0h06-7GԑX -A1ɻw.+z+Wo5?112&Q٘"5=ѿüp;zUfPg ΀>Cޤ]RR#Eo`ZS^K2Xl{`UH8HoCLuאPY}m͠@ԴQh!w{~r83]ZOCMFul h[W9RWjf\W4@c -wዹs!7^_}d3b}wiYDUPoN5*umW~u[3>|W``R6npfSJטD0d)e-sqE]0ԪuGXe ev4)N&Ftk6\A⛍Qdaaw䀮=Q:-=]$ڳ B߄YO/\!s5TwK 0S.o zf"RhuHdg_ [mBIt5'톇 g#vmSoY1AS__$]LS*ʣcp`jJPodz6[KԊN{*EՁ--.o}{wekiv*̬{FUUYd>Mv/U@>GmY)N&qJ*2 $&ZWnt3&!@ /^GZ~9ٵ?gqgu3FVpoA)Ǽa. z}p'= ˃%ϞLI*j3\m%- vt'ԦCFsƸ;2/+xgNE!O_tLN.paݘ˵6lA~~1ܛ231v(8\&G,ۉ,AwB4JWPج*Pxwh߶]AVy&U۸\ iGPkU [Vq"j]__931y<\r o 쳝^AGFzRlV0nMtyѩ R:2_hwC2t"+גX8`R .,3t+'r Ο;@cBާΝA=7̩| "=tgZ2f8壍ɚ5/T9|8w [2Ħcʣ Rɼ7>ʱ0О_œvHvi^W)r3(ћ' qrv`r֩is,ѳn_Sw!`=:RŁ fw %1i0?o92B`5 @R) lZZ3m\< nCMe .9onj֔R]TPF0G/AΑ`ln\[#*, 7SȑH(HΪMΓc?d&?p-). J}jCRJ@äs'DW),u#;ack/KƆd^D?~CqR`U V)zyM9RS-{=z&*"ϰF)j=qM8IYc)WjKf.%kB3O]nOuDfKQ5K d8)x$ VX0{Y N :^WYUkbGKIߪ3Um;ZVMʿaЈ7Rq22eiYS2Z 3p j<{aca9hj7!#UyD?jQR:| ,*pF9M-SȾDkj;Uqd W~V_f{[Ǡ.Q\),l{KAco?aJ! 7 dS%3І);ߌF QL o~M+25سgT3/ Ϧ@sVP*_cω&-+oS4A^i8g{Z6^;+[E|T4=Ex=6WLFBVq)\rю \`GfCZt;g_7i~dl3`,T0j"&&y|.5}HސE-%s9;5-4#4]DjG@Y=9\eיCH|xkij>K^oooJ @V7$=0AW2Í~tƱ|,YAxwq&%eLE# za|f=ѤISd(f@F-qzr ߑcwGL T-_Ξ ?C͝;QH걏*94%&?M ݫy/0K=#Ƿⓙs9F{p.E4!c gW#}!ԓ>Cl U(<(,uhqn ~f@vi@+ˑSs׎/SE6`\:]jj),~^Y8v(2xWW)G9DT8 U'Wx >ZL5Ft"ep#| œ ,CFG;eK'!EnUhyTx 7ȢӜ"fGx LTJy2h9hN[,.pnct0Ҕ,4ow'$U#r"ԘBЈ2npy.5ڷk*cgϡSP0nڷ=EbbTu&NnF<*ˏM2c3ƪ  9[iWO~UރZV;'T][)GTOY\<N޷Yux_kEU'n*vB߁ .im=C zF^Чڊ,90ay7䤟rYU0p.-Z/ yy쨀&%3g,芠<)d\:r(.*wDM2? 1eL?|[Ա>U:'cOx3[ Jp3c#*mq"- =hޙ9=I==~_|C"uUa*X'':ٵg}LEVm =Ux2bj)V ,tܡ%XSصiAVcݓE3'>>{Xwl}NWU,`XڵklCYnڻ# ~vP*_- ǚۨC(yraKxo[^{`-(/U@͂Lv)`YRYW}9uRE`oޯ󿜧}|D)*Sq!a٢$z˱%,ތ1RO=<=a3@IDAT8.q+M Sx2ǧiKҮWg(ť%JtxVn`^;f,,._ ֕ak7;4bl<|`B[񵝕鯯OU4Ԝ>2/c_ L?~}QAQ9hq_.*krL2RbUuH stE!amRy7x}}n*xIO=-۷"sgo)ǿw 鈁N֊bNJ]P|brEBr}bS+ 3H}'8b\I8r06^#3 E&̝qbL o]+S#2ꁬ.9'ǁ}a|;O{\/ݱYlK ,wסvd?bc&G%*(5jiyo0FR6tJL%b4@dʡM8p1]"^ "Tr:KaGH&{7muSWJ4Qnz8SX [ͧS16`N\Ǭ2jueI4ρ#k< {G-웞pWi(2V"jees)sZ 7[d͐.̃|W|rGqy̹өSWp3?]Clzf<Ⱥ0 Yh;=LiC 7o$`qtxh*|e)ZY6tk1QNȖa52(sD,5o(H004&/%?z-[) \_=sN:NgeE%N\3۟CqK]CѴTƜQy4ZaCF67;'c57lK02,q *}e;^X}9j¼@s_4X^{EꋸD3ƬY)FfM;\N9tp4-sE;SԃĶ)|x7`lf=yRtA&KQnB0ԛ+ Gف&DQג'%];&MŻb` ֠Sn nV99ZU+<^܉tbsIsF* ;9^?cj`x|$#Kc0ğ?}GS Wxg1q18XNM\K6QNxݛ[/ ~EvkfNO#^ INVGEY%N>L\7q` 6l] _[ kгG$+SPtF7uWrfJۇE0& V<Fa=~!CFׁrw,w+hftys8#d`͊_gi KO3TQjn>. ^q81CJ.Ij#/9J:*kTf KkGw.1&zF|PAs s4,>a{WZٞjp0c6oVyIgҼƪSFaq6Wt7mQJ|W ,S՚E7KXW+_\yHIO:eR\224 R_:}tk_:T C>Ό6^!{n&7sT$ʿn2~7nE`VUYufB2s1﫯O|0c_Ywo{G0pJmr_֗vLq@s_OM\#蛅%ؒ6G650Τ=-;)˱4L@ Ie֥%%`V2Yh 9fP)B:2vkie_@$lmF߶VÞmg%(<;{cË׵+'^Ct>>bV>=:=q ޞcx{/N EpNAd,qX s5<5;`qhݞd2b]-NA HLƛ>ŋ x+ A]޸O@5ޗz9vMVdl[&7+;j'cv)l>EG1kՏ_vV*6Y S{Np5bd<胺_FM&A$mPl,Gh|}fBHwɱqt03!]7YtR:.;ӝ^OJ3/=#f̩~]P0o y'{0s(g O], YF]z1뽷yĮ7Jw`~dz(R럏?l8Wqf($ZÓ@ͺL^3tlNxA>6Mx c/<?8n\m<w%^ ߛH:STy`hКQCƔ_ cifZ}C|1g>rߦ]NH\U+"tC](e5ڏ1K+H6!zGavUeXֵ#}DYOHO1r[5 [/i+ ѓroxIQ` 9ZfI/WY$pN>n*rmM=VC=U 'l|[ygc@Lyvm,ﲓSX?ABcL9y`;~-Ydjjv |XΔzJ0A27OgKz ^ --yR|6Ԡif}&Ō2(%0~l#P h["H;+;Lyp %3kp- t;8Xfmw2wgDP9 `W3=%cI)(Vrq(}H`)zs#\]r3γ1בQѯTroڇ`xaF`;Db"MXFjjDJϧUZZX"Y7p&w83SFF-e.đFg?t^DyyM 60vOekkж?HH#J;}%+ k ZU IiXVOֻq/L*`DEJnCF$9t zaw;^G vW* n2{":)1QJC{|q'r\WBՔX7XX`/fn&&1/X֖W?>=wqrtk?m;"/ES9sӦ73^~YSњ٢;xy#8Cf#M#z:_[Ua.8g:Ӯ\1غc:moMHP:C rщ{lj m AUJ||,%L-TdhNpKʯKixbn⿫H qG S֘$%mjlw.k`h<8k. ij{̚.†CG~ x74C~>x]@Xsdޫ8gJՍYj (IC2Crm(?UЙ_-T]SM$!8(UvdR;H9lc~if}<VmeQdLa>񓈿\,Qĝ=TXJ8ٴdʢ0*89ub [~r?U{]!PRhé[60>h.م1p<3pvBxh6sD.`6 6[tr" kC?r ݌‹ڽ-S}tcbf403甎?˹E"{ gcQS&?CՀ޾en)vAƉRsfuo~Rd5纽wظ|(JK86Ac+/>ԭ ;SƿԶi;5俢AK-  1j 6-AQQa4/vr/_y@5BGNB)+#RV\U?U@ֿPQBIC)i 5?d{;OpyL䁸xk0wv8#"Nި[W8Ղl)5'E2%PUאsGw>N;C} 9A'=fcf2J#3zE%~5+}{ګ60źC`(5+{:'xetlD/:2澋R 3ElgE)Ly+}ʜ`[ϖ)g +2;9˜gǗ`M6,ьS`C^`yY50S0Üv/Z]ƕW̉UN={K)+(o6 Y.T3ɹ*`k E1Q&2U-dRYSt`ZMn&6\'mL1ጬ|@G ۶ADشn91,̇בSh ǎ\3{GlQ!Zt5%ݙ1 1^^N?DAbAl頦Qɏ~w.xАJ?O3Qiu,f5Y#RL"O?ܦe_TXT&8О3`Uv˿|$8߂ b8k&.;tG'XΞ?~w< GbŘ.]qu2Ul gۆu-ӟ{؏<w s`H^xQ`5ׯ ˳~5L0639b t@mk&Ha+މ[_ˌzQSFIkh.\Zlݎ5 de^OEێDۨ]oCAګ*}{`qJfPYׯR=l@ H; ߆֣fn2:a*)FkFvdoHiO',Ϟo} Gᅥ>4Rtwn#D:t Z& P-~6|7JM)(ܷKqN)'?*%*<Wll!C(MX})"mp 2:ڽ%z tkhŖO&xbJ1j29j,(-v>dp @3g,|bi*)V:3wrĻ!%? cduko6dHݾqN͑A Fxo>)F&K#"D:󅳶 M-űiU}~83G<3[}ʟ17S^E `mm]W]CT un9urmC0deYe*w, ,2W̬z?A_YnchtJE8rb]{oXHa[n:g9TֻwJieο\ >gN ?+ G }iGŋ*\Ǽ_XW*$VtdrPyf =iKj*stf? ss%Zb>c޾td?߶#$ʸO^T@59vMu-~VPY9]ۻڭ]Ȇg<~ xkfi 6axSDB-Wr۴lU=oGUe}ۘ=lżϕի37U2 ' ' Дؑ|s ӟ~y}*$/7 >rGEÏJ'ǘGh>b4H؎?e{MY+`JL0̢_=CX(sw9n7RGXfJH}frSֽ%2^㨋n?ZO.D~XSߌϠЉ0+S [;;OaA amʡTugChWzxh9 ~a 4khQ>F>&._ڴ0zhK\B"Jk^M5}՗_~INlB KO!NzxLJ Eʶ%K:kSM=S9~̑CgppJ )Z4nj,\PUtRFLkuo1|G" kұ+S$28;#k2'h,{-}c7?A_ZJ"Tkld 4mZM&3wVnj_^gw ڒ91"BJtU@VZ%)N?~mF;s>$lKq%>1} ,Q@炑1̺lQ x ƚ&&_y$ 9W{Mahr0%dzQ8/-wŰGKzEnhR&,\_t0N2ge5[=T;\{9y j䌼xf}# "(9n!J I Ewv](# :d M.s[ERW^B7(@7r<7>8,tc iJ4eC7c?`Rp4, r*l.ۙy /?y %7 &%n_C|2eZmc-{vVhkoJIF9JU,E&\ykR[_gu=9KlɀRW^ ktv<`sNWU2t$lyi|Ě֓hL@3JӦÚs43TS3"J޸N.d ]ڶiMRLy&wsz/"ҽ)F}ϞbP}L,BtS;&‖I.p!!lQp)O SH1USMUݠoRi|T9* 2m?Ƒs15[i«u[j*|3?\k Z:,`b᠘37RK3E ,ٷZ1)s~\t W+?-Z9Πn6"೜AiT]JaZX0/"IOeҭd\ \puoWڄ6":J=9Mؽf$3;k053pN[Q֒rްtܛ; v/3m^ h"砆6)뎓ͺ\Z2b@JD)2.n^̽qXz (Bi0Qr>1 _~5 7цNjʴX6g1"D'%>!X7aW Δu`J-* :+fLP\YYM}M,KH|ҫ{\u%GÕ_qfOч߭JSmQh'#LbUSq5zlx}xRu:E)92fRMm+KjJ29,Ƿ4y,oG%Yg G[Y0Oo L}O~a$A"mcQx}Zɚp"摵CN{Z" 2^eag\ƥ8y^!#L|H ݁y+@ʺé+66* z@lvesr[_Q0̽~"$Es?r1^z /Pq<}mps'W{o񋣓3`)֫x- Bk}c߈Nr7.j9ɜ7S wD̙̿{3;i jNcMUw{-m*^raTWU0p L]م輑|OtgCuCd̟"/~(l}nhzE9ŀ21UQC QonDEG+1vJ0#Nd#co<#J \Ʀ j1hlwa ;mX#<d a%T)㮩J%~ansɀlNa픵w[og>| ~ ovQǧbq ;;4 andڔJ\NF5̱]L∯㒵`Fyӱ) O GړZJ?eZ62vUdpI ΝgWd oĄRs*ޞjxj |0bشj%h~g$%+ УxcA_TRelsy58e%C݊pe|*(ն{ס]PMb¬/ɧ#%f|B`M\8?NTw"N5ڢJz_+^7'4ғv{((`bo_M`v]Ɔj@ƭb;u@`)}W^{n,z쏋H&0KG7~ScbWdsќ{{Dž#$ĊנCǦFvČli*Bɼw_mhKyZJ+5WP aHڻ4~̖GcQ03&p8eͷk Yx'~D-U9q9QS]$x#t1,w+TN'{8Cͷ" e*|^я| Ҫ4Y-|s=}ζnCYX8"Ҩ;j_ۧI\kt2RMO]¹* =`ٿtzwXRg' AدšWȆn#uP iwQa|!GoTfx>%}KJʳ s._۩17`gs$wt7w3:GhY}&F ?E?ب.шCI+ 8S.k (OIM5_0dۯGd,y46!v![˔pKp|V 8 72nppm]1 5D9 O?}4EhA r9*a=1[-KVCcO~s)Y;@-]*O˯7zKYnC81ᖊnAUBW-Խc>TċX77OBZ%?hν<VGWnBt7}4:LDY|^ieBBHP0"tهXn|`**c†2 ߜ9ޜڥD˷qz8hg9hy0cF]([/:!{%;FW٧{Ͼ;~Jͅ$&Of^){fW{~C͌*ȍOZn|~ .beXi'iǏhF޾C; ](e*SXhiP*:SZxV,C]UAϓcus7U(S͋<h2~}lA*tyZLWx."}x>Kex@T-$t% N.gkX'b-2e2է9߽+ kx+A7AÅzaqx7QRTAŢ6 s2۴d<<j݇ᅨ&93y^T(!s}>V]$:y$Ї hß=&ig +20wv!d́2kơgh`[~(Z,WB{vi2׊S?6Vd}…/O\>'h˩0e˂EpY)LcL$XMTΕtw֠}JTߢ!.n-q~-MMaNww 0Lm /ִZ<ӑV?T`'`a hE^4ՠI0: cLQ.L{\y~8iݚߢ28n>"ABM#ɉȧA `p?ۦ&GxοM'l1D|@-R^_a9YʯDɯ_fS[ge+&\-;W_tӶ-{bׁc\S4a-04v~73EbF `BL3.amw3j!Bʕim@ MY#fbXQӗ|G~fZ枖U /&彐 L0Xι\;#/3e4mfip^vlS8s ADD^dzdX\qDǜ_;~ـ+d*ѲJ8̊tvcZ\NGM'ϦCD)AG"(B["])g`kO/z1{bgbO%#&H"#Ln4M>GNG'bi Ԉ022L'$K1t V .#Ð/f[YG׉l߾=Ӣlu2ȉ`w꡼AC)Ր^ u&'}u.Ą gdٻb TŕFjhff Ea)f׸DƦ&{UQEsjhWD:,V[ VS!B?C@T]\)Rgs%%clO1J`k 芖 - re6*z[ ZNY3gי`tM%hЗ=[)Wt"+:߻`1UW&3h^jFѧ\mhKf _GAP<(؈YQO*5IhQ鈸X gl-;!K l9E|=޹u MpOA;1]9oAW~sIqM_!qĽ98wyiB22J 9IGanQ@Vݬ9-c 9^v?=`ͱ;fҧy~Aj$7эͭk:؛1ݥQQ6i)\wx{ % yyEs "VK$ޫ/'|rΊkeSCvFfQBaW kb[$ Q{hݒD=N/_@ѲO*ͅ*(gKN(wW(2DPч$`dž{wc<AJrPsny =njD]p/D_oW#_=NTY "Ӳ)&QuNYJF'YKQ!uChs3LM7Ž҂E5olgi؋ؽy}}'#%%>ݐ˹չ0Щ<Sߑ ;=Pm1-{s6{*,:Z5zL0u?c&m~cFAS ͛gfL)t},TX˥?{ߣi3cqw>\0)Bc,Uo)-1?zPwʻ}V_(c̉ҝ'I_>Zbq}ej* D|V9 nҲe @:2IiRޅcÇIY3|"/| F= ~n⋕~pglj8x݆M5-[3`M5wA1} Uqc}ǟ!1ЭKY)Y|.@}щ vϣu;,77}"מny _NE%C7Ő0<,8E?mLC7NF|)j9l:æNYHd&D")C~#?Jf}`/NR1ʍ|$pE寇~vDڅ #L*CZjd26(a5ߢ }me)q@N'-I1+SEGcl}cϤz*$bPjƙbAAd/yJ}_>i >HG/c ñ?ÿk5jKF J-3Rf$CNە?󱻔 ؟u?m嵵@[ pqqB9 @ &UZC&)-Bɉ#`zc7hǧ~8x 44L2,_ؑ~1DG6rI%8'rh4p۹)\8b:i\vdpJXrXU8҈y¸5x4b#q\M J $xhO̝v F38Mu Msf,`S̡w-)Nd(zCCx!TmahTb̩EYrJ]5JW`\PRZbjFSTAOK1-8WPnMO!F01 (.F^i >\5d/61劀6k+FRj b"0fkmG6}U!Z6Q,gO@IDATR*!O`# Veqqr&(@XG|ItF|b"#kVA಩*D_u{RXU43TV9^J@5SOWNO<I Q+i؜-.{F^u|h{_~Txz_A9L&d{?%'~ٌqeX̭Y5 o/39gy)&{)R_MX_1&j߁}05| xt>yX\&tȇOFAm3 % {^RJ!#D]goἑ,^v&PCaP.ЗaQ~Ce7b$UΊO!>|k`wAa=wQ7^!G;)}uھi-hٓf<^ԫu,+Uvݵz>uPt`k>zh'`cTQ.QݻA"h >N'X+K45ksڳ4?UaΛh^[9W -HP '~;BSoVW[9:בy(MK'Cx*@ PJ8x~M<1i|4xE<%4+_ca?Y*s^_u3+rC6vU-MS'O`]tRV@֫TJo:;} /[h:cX 1ˉ~Gꅻ3HW6dG 19 xcT2&KUyh@ \#KWaSFR9x54r]HCPϙ2c dJAS K@I/SQYIE+pdȠr(U0ixoDZܯf.X^=gg/`}5nBx7bҨA|n.jq|=X\NKWL9'f}Yʐ^v*#̹\OA=kL5x t}')*430hi)%}$O7%(owY}nd)Ѥ0?HKO  t.֙^5y|绹w;9#O>K?yA̚>somg̙9{k/GnC꽴Ww}:T?'miy9EVe{5+k2QؑDLzR@l*F: u sJysnըJ@i[*c~57)QzVUZ*vtM'~.'';^KeY![f\׊>WU-!(Y #L>J\jK޷Y܉ٳaζ4~\u!(e(ǴE)MMQƲƂMmܗ̤,64ȯ3")W-YڅnFum%bҳ1,[PMՁNNQ`h#;w { '%$Pښn 3еMV5%ڙs441&|/?˸އr^U@VU>*]uVUS3*9Yl;i4u#.9u|?4nᄀ~A 6 t7G+2"ܕZw~>x)^z!4Qa* Ia];}})zA=z/aԄ*%iҪ=A` O4:Ё=Xj 9R #LG,Goy=w-ӧ%mo/?E+g!<4 8|~LXݽsEgb۔8;ٴkX! NoJUۧ'rEaO-U(#瞄ERBzÐe;zbapxDyؿFr3CVtz Uȫ_lLeCUgN*ߧ_Kɾ69W`bZ@Tӧ뮾dFbS.蝘v-C {¤UB \~!Ⱦ ]iN`W~HST/>Izuׯ,-)\KA ;:ı= OMr;SI5 r19+%*V^R5IMFdt8vU=L@Cccur0EƼ+ΒQ'x(4~\Rп/r1u$ %0oej'sQ=Iѫ:4L'5j>h*7YO<;Am!.ҪDO?2 "#p/f.GfA6k?Z:VN^ k{UxvWNЪRF>C m$b߹sdvV =sqm`Uik@"hVhu"EaA}/ kOVg.oN>_~_5}.Fb8ЇՎ㔭#sOp\qC{P3p~ZVYe Y%dTx|p?P Gď6uG9:y*OySFc̠PN쮴f{i։у[Ye|~yhӝZMjd >X E;ZRdp%KRXȣOρ~4_+Ҵܙ4{N\V UghPrҿÇ#)) 8F~].`3tw%Jt^|UxṠsbAM+W^2y]Ȕgߪc:ޟn }zCVƉGGqӻNC.PJOU3h_!ieVh\VB-@|J̘<IpQ``\^HMU`:O}52(k-S X붥d+V$7pTVͯܤ{ tVnn:}ԯ9<~H- Uʫq_T{]ѿK絀XhcmY[-@Ѯ+J+zŲ&}mo9U$f$6k[hk-[/*w% U-}S`ei2fJD7MvN.4qϗB,pC1Є!'M0I˰`abPGq+9(9Ǻ ;r*ٷb+LQޔꆮ+> |+c͸|?FY1duiCTlifۮIGyRV_P=&co@C E-q@Tfh{^Sæea$jJoב2buc ng,]$Ŭ[m]H_Vf=k\Y4B.nHKq͖/ejͧ$^(_s6x7JjGt Y02-+S7Qk۪j%J$-ŖL ).B:海E%jiS=i›f|8%b'j?{M=*wH:6|< JrP#mldc+<7= i{sמCkPKFɅpJ†8zRC OB6L2=07 <`$~~ "h0~9 _X4)1 t$yf"'?v s߀E[;g@Ÿ|) U5SE9Z%xMԠ"NTrU1hk [gixw}0(ƹ#G`>Ə .]8'] ؐY{CNTV T<2REꜪx\)(Z o6~ZfW7E先>߷W˾Ϻ ׳R{F=11dp&SSySå9"P|pW ༠_Q)̞ۢ"ؿ0_p3QlHfS4/%Ӝo'EaT:bڔqH?"L/.JJh~:Nޘ5&l,VN[eab ,x:A@R |P !Z8g$s eVlP\v`b)3ȳi?5sFvE/@f?'΀O)yd/~"џjW%.B앹\C?4Ƕյ Z@ӵ n8gڮ݃gˏGѬ\c\8IڃG⹷ӄ\/D}{ +KHgP9gg,%aUXR,*.1&ߐ/)t}/3O-s+8*d$}Th$K th5ؑhR op! m_ ' E2Y[# 6G'k]QyCf=5EfVZ2W_K֙ڳQYA|\4F2\~|1 6b QHHY֩ UuԶِfϫ;2OJPuTFH)No֩pZU4_NsiԸYvFz+ 82sci a8bدZ$Ǐ2įPajmHPp'r)_7߅aSS U?r~}/Z3k>{hg[ [ dOx3דI}f;fQʤBCh_QU$*ɫ.I 5Hz^M[~~dRw\ЦߢD@( ]W -m5&ݬa_ʚEb /Qg{P : 3M2:*@9-y)ZK/>r MȆu׌U@p+Q9[/eDv\ٔXt uu/̛f,cE" 660$304DqE6J1omJ^KJ(&$..̸a;Ύo$՜_ (Ds[Xٺh C@`O~J ՛V"'M\귪&U'cq?nz꺿LTڵ_g,&"X:^+q\S\oTD)Cȫ搾T4deiywJP&?mZ֟BzY z G;csFҭaWUI6tudVdpKMw+ߌcHςds;E &5dLT/dTה-LCuMU'gSH۫P}i)J%`0% Hۈ {t͙OJ!cS.K%X[1o v4oYTjan.D ΝP cVU{B'OoZ:wĀC)ХP[dT$v؊݆_G~-+yTR%*lߴ^(c#ԧOGğ:ȱ3fߘnYFF]Pj)ӏ0_ I;XM';:x"I~g<\p})Srߓ0۽er߽s(b ع' 3?]cLHS1T1#;vf5@٫qNU2l,-q|tn#,=Y.bIF~FT9qOQ5X60rNd.rﹰ5ZRLªqs姭sѠ e?\*O?]R˳oXqQ@;d1QUP4f5OsmƩ3THҸQ} NAWpC*זrPe49i)Hh=q!$xHKYɲӧ]n]r z&1mWL,ykktLRтߘ6ybLeٽbp!>5U~JK9Oi\M~O6=wo( se53XEDҟ#%oJq%gC`ݳ']d[YAVV|6=L1WW5c^ #+hk̜8WFoM4MO7!`uGw0s1<_|47cOb9 h &]Sb:u .Ǟ=րZSM6TsUΡer2,o DYdͪ72O1@wk%#GaRؼMYJ)seup_oMz@ҋQMCmFUӼ1mlk *> [έ[gLau%1}#zyd K^hPP hm-m-I4]%]$e$VJI$&#">LLY 0!-%AIFvn˸ܽ^ܻv2U"O9&t'·f;ggWx6d3>Xts 7{ RQPk ܕӕ Za+u#-,=fϧ4E'1'>PXb *a=rȴ;"xX{v‡Zs@)@HY7|mȸ%L8w A4n-ko )e/.9+a aIFF(ύk6tIaaSn~yi x8&"i1.]0gXa5bway^]vai ?A43u9r<||i > ~Уg/| {!OO9NY$F0t![@-]GQa=P@Z[oeg|XRG!归smDӧѯV0!a̙K)hX3 S |&ٶx;CMgNÄ"UEL1uPiכ5)wFTnR4խ;quS4#gusѭn:JUːY_Jj0{*Ig(0D…гwI>Q0>rZQzX} {ME0# 441Iɯpr9\x?LV*^Y^J:I OoWJ nJܷQxwA3odiL«1gЭQs-27/ y`Ol#zx5Jb/WTbvvL7WՋl-SIѓUB>J61v<ڳJB3w?NwEگ&'O.} CU;/-~ؽKLͽ 7طcl%N1oC0vLݏ(-Ýphj$&f_nm؉'GxG+:tdV1|y~U }t~e~&@G. Ջ:G1-vw?nI?3ݜq غ+U~y {M?s,ZHhz]1ށo =5۾Bشm KSHA77mnG) ^c+ /,BhQ5FjHj?;+,yoO.yI4dۉ00A0M_4&;/ɺT Ki%H?\?epA&4G(nϗ,>Zu\OF#?ퟛS SnA@ֶPEX1B^m"_x-=SpK6]&|MNÐo G㵍Mes}q$"|_x$%^XIOB$f >]jC( ψγJ͖RoɦJ;I>OeͦirN Nr\'ؤQLI#K-zJZ~-dDZ&sq{y:'% Dh^OFլLWFm;m-Z5CgOY{kY¥h2 $]=/q5;Q 1cLͷߡ*2~`8u|l[`hl!`t\P4"Ȍ녨Ӝ`/Jy +lg^(25XCg}=i *+PTxES7fyi1*Z#8vx*iE} ;YxY*NPqPFfh 0n4>:/4/~B95aXt\v "OWhJGd\Žq~6t^^2I=)Ϯ"q׎4Ym`4hXiktWBҌ^A8ۭc҃"ưs {hR2R2EQӑDpgKXȳ+R 9"66.0&0E&58%sɴ˾V=ML@9zJźs+GϠʹ5_+ft5a%vnS'Wt3 v:_/~6$-l÷_ qS.o]Mr$D!.;Wbii&8܂_wPz96󾳴@x#ӂ~hy{Y*G_~_&WϜ݅olgq9f@SNpJ='hq؈Q0/}cg}Lhio9QHp fTp5}j S.io(YvbWhutQ.y ?.?Ca-YıYbzW65đ)o~ÏoGFM뱰j%.bY咲}czÿfPPpPX>Xu="}\G%a)~ioeqR%,PZe%(otm-[@&#IhFu ?]V/EQRwMCaf4L y:L!N7Tsd[TF6Cc k'Wh@x 6U5b S4))۷z:?\ PXBQNpZȺǶ Ә>F".{q-;c>8 5AYĊ>}4*A՚*/q`nbce!Y4W%ژs=L4ԊYӬk0<2%fPCqZm]rdڀ@c,q89:G^Ic"EF/2*IϾ4AmJX[#IƓmg#S4-u .6  DQNծm{;{ ̃QN|!MLC.cjyźsrBGo|Ūʕ$NSa'Vf],^Wl?W&b=0޷K"4-G-=B(I>\gD(y1'4֝kÔ?Nݻ{-NS,ͷޥč:B*B]tbѷ߼ ^zYU7r![֠H'V |WMuwt긢K٤U4O}NrNڳ9/ߣUZsNE8DA85F߅n{lu9>D RCA:r gOurF䚒&3Zscl>nFEVߛH!Hڱ!&v#U#%}Q;¦5"gc?oEXk~5Xr,z!lua8X^MVX$`U(J&X{m,y,7C,[ފ?l<}vDeM9_??zM?'Op/e7r2 ]C` ;^Gy5tvAqA.^&$ݹF{7XjSnGrLzn#}(5 ;9ƎG`h-y5{Š&$_zuL%$O0tmŹ ~j_&e|+r| Ƌ&ӎ}XoRw랁2ρ2l d.KEX{:9vB]W?t(jèFCH aO@_ׄℌ?x t.*%웏Q!>x^ƈ--zk)V%v\Վ)cZq ?0^G&TVVި޴y-=k5z2#qC0~LtRb<ؽէvpo7G-Ӿ-oԿ^qw&_uaoMp?"cX6yX٧ g<}-47AEdٚ t٫?O{ Wi)m\|F8~框{ᖊhO7"@wήoR:NKYw ?y!! h[@LdS%JlkmLw?t@IDAT݄eT\jE%Mjj99! [B6UZZ Xu`a뵞[ppp:Xw Bp\Jc0f,8ElH ed`Hd}TT=K\E=dga$YEFN ΰ36.-1^,<ʪJJ]lQPX upckL`iXBaͤ$u,XL Ptp" e-ٗ‘z¿[wG" }DƝ;$2ck t% uݦh͞_"5WnOs {2s1l FR诈 e@} i8)7܊76CY+~*de%2MAž fK a(`{ژ js=*vSA1RKOZ,/8^]YqAS];bI3M;j麡}{3dfb0!ִ!'7&d#9ج=kPZm=GaVW@Ug7TU < YaIS݌o{Uģeows ˆxצ6wWڌ4EhQ^KQgtDmZd'M t܃ ]|U=齇H@ HGP)bC]P׮ޤJ F:I yLx s{fܹs߼[K2B&K <ۧ5\lՏOwоuwcXo*[5f>"=̮sI F=mÂV\e߿͛7ouj&[Ě*cL{wOsmuQPD%S}|v0MQ~L<^~WHYro#Yb)JMV!ϗ(6btҒdeO=ASN01Ɍ+4=X _AaH_\.EHHIS C%\/O t}f2J[s(ħtgބόY qODž?xx|=ʍ{'| hӻ gؓdB~5F Wv0C 9,461o M 8.H;ܴ^ia37\Iȸ_ ggؽ|)\'8[+Jon 8D7lK~o~ a%h]U,0cR9vag1,#(:m*+@CǠg[@%ԗVitT>>wutFv;_iHlCn :vv19mgҦ=6gQoz>\:0uZGqP\HDlY\Jˆ}37TODV~z 7O"ޱ ,<82Zpա`iPj]c9ff7ΟAW $~t% Δ ºu__ -uc{ɊVY/e"}Zߖϑ0F5sSQe1CX`밑)=ڴJέo=ӷtsmsTI^s:3Iѧ^) ?K8~=[dۆV,yv)o`MKtp 163&(efLeSa!kAn&Qy[ǎۄ* W^Wl )띊.),L?+ȁ_1i. o8׃[Pޫ?PH'e[H'㣫}Z/f"}]k'|r`[aL5~VPXz'4sa~emQbfjbRF|I <ʂwW!_`9R&!&vݛBG+ V.xa󈋋q~4֝dL_/qltKįϹY! <8z$F=> .n]ǙZX3 fa1ڻ8s"}~2|[sn?,\.'~X&@D!8ːG핾~2WòUyqLQ,Kttr8Yh^D@{*R TUQXjt4m[|x&kֺM  / 14:"R dQ1(;5@S -1lk$LB21Tꅛ ˴$Z9*Yy%Ji ]ȑp z([k{15--)٭v7mЎFgK̘6B j:99)AA~1 uѹCy}/2jWچ 7QYO'$Ά(=D3y)\~6;Ë/{It831QJ;qdHN?x8Y4y(QgȞKER~ * ݫ {~q$!E EFmAG$#G/gNb .54^ƜDɃߺQ+icS`FΞm&p9\ӊsщ0gEE 8tOτ+Mzj|(+N/Mw6-VVM.vw0:'M!c`P}B)c鋳O#;#޾5~hg2T]7 6nXCV|&QN%ئ'ɜb1c'X E)>i4|.B6Nvc FyM5`"Wt$#)π[ KS&AE"%ՇOɪ}9x­ ߕ0v\̃|["pw,LYsbG6q qʓ2o:QZ`lCuQ.x\W_ln{g]$3 \Zy.5]$VT+ߡ*&߫(='f7! F#Ww -:Y8XtEy.պd=?wD΀H3&E ne't\bj5]N(pȫ- zo;᥷{ l^C.1 {qGђx`T{+/ic^J٪F *ۏڷYk/{MG(Y[~¼Vm\C)u#;"*oT9wvզ&1N--IK/aW߼D',#)%W s1  {`pš_~ mOSCg234+p>vYtH=`KE"5?y8qYdAKR8XjKGa}=nGF>2aej=kcFoC ukoag9kPp NiELuKۺ@NY>;59cV±~j5ZyBvE=hߒHxnmab薥tS^wZ簭-lX&',1+Eزm ֭X=ѓv`ԍYiT⨀-I{W#;RZw᷍6ePp]odU{5Z<8X%]Oٷ, '{YfJyw2g8{~!3saNǾƒ TcRj8W8kYe5C Ї}ݦ-D- x 0[[߮*!'u5|-7VWD廯sS h@Diyx<} ر]<5d66-ܐɐ+\74뜔1mpwwEp#cuT&}]iGZ4} >Se.#e)sKNo/b 9ߐu'sf#c{_iFGN!̛y2}j#ד>Atu}!eI抪b|4V^$B`s:ZiIߴ- FCŸSv=iJM P& [,&Ԥ hc&u1#ӧմmjk,TQ,t-T+pz1\{MgY28 +Brex*бO>؟>057.)EWPee kiA62wV}R.Eрj3j iw%r~^8sR]:@Ue5󀷇5\3%3+?'F "H|;D.p l84O;LV#)j<.tй@Lg0<4h31<[FHcÔEΝQQ/SY42n\XzⰓ¢^:6v,';/h|[]hNC_Q0c. FId/8e?1C,CGX@VIVl o g #]E !KfʮJ+3BS15-68r`'-fo8Ѱ|`%Hx{"6$.]7LV(5W*\ç|QXeqhG HG Mp8M w܉~Ύrd8^>).cv`++P]L.&Q,TХY`~Џ

.b΀fy/zV0L_u(imNb`$AP2ZmSun ES0 992T/^2?>a+1Ӗ' {ل$j":nFQ k)3)pMwG|I<oUӾU}/BHVѺ]%Lism\~*8,gŁ~JM>#ˣZ"q|I:kYbm^`'p`@> а *M{$hGpk.}cf Y?]|:gyY@ެOw{ώ^9>sH^z_^H[Q3Fn3_p;ӥkÏ>^uo{.~vVlrT\b06z\oyn;_Y\hUTo#?bkJ&~|ZUSҀ+Fy7:q%>~ %{`sUHn qIΗwu| it B{4ew9>5a ~/ov u,`/|L~MQ?=14J?5|U@wZv lV~:=0ūk{f{8wdE<՛X4Xݑ8(y!8Ex-`ٳгL$اop֘\sاd* Yl}>_h3{>_]žhޚ؀ѢGHBOe9ʑ>8L6$ I'eoQΘu18Q0\- .4|O; ߌ+ݗ_g`%d؏ [o`gljrT-"ȎnG{ WOPb|{f/֗_|>=mܝ@+O׾ ;͖6.xi;I@-U)?{&XT翂,6 /u_q"56FPNDS^G䢾'÷x3ޖ,;{+ Z} VjO`ߏKk ~GV)&ٚl?Y7#76L-#GF̺+# ֊g^ϼ >1z5pl䱣v|㏷77Ur=vZx{o.'ЭDf&a]sGc ?̟tc>įw@{/0rfWG|6ɨ4I ǻ,@y8$ރ`+;\I}?A֊g||,]'MA[wŷ'%^r5Ob4"Iy *{A=~j6Jt zhO{Qlue;+xd$T̿΢a?|=K|/k^_~\w;po~%3%~*xGsJ(XNu6:old]s/Kt$M?l{;v\ |GuYTh^oZaя>=|P_o]R|}Bw~byM脷󁊢dR?^9 x~K˟fUqmٵ޷.~_?*Lч9J;TGklIF>?.k#'B3x΂hMfm 9EJk 4')aߎ:>F/(/+nH﹎B?;se@4o~d/Z8Տ+hLݞ"+ݻ@[:@j{"Z\uu9sn9{);Wq>s*:s~o|i(H O+$'{/U?䙘:M߬V߁h729 Fg2r~U)n]s@̛Ԉ1@m w=kNP&vԒxnN\n$_lo-X!tak'`$M#Eܩ2Q8'͕oωq-X8~TA0F:/C1f_ Y5Iq06Qz~de457ʦC&82> uagB`RҘeQڃ% O0<|\V-rbHj su2>ҦC`TMLS z bYMʇ._#N%?0FlNΌgr[)µ )ӻ^ ]Ū4)p/­=͏b"0 IeWmmoT^^es k<+ h狮e|:h-<3!xҒ^1U]js|k}!ڇ"4m# 8F$= Wa=˨\6Ɓzج@JKڜu?ܴ`~ЗjLO%\{ZLsB2?Y =˜Pf_zΘ/s= eɄmaK 624™mR8,Hmٜp__~pAn7pq km3jg% (lra.#I,@UYyŮ_Cas͑g!#Wv{I0Btg{tGsr>'ѼB^exM|#s~JVjicToۿ';rB'LŔ_{=T\Z6Ύc_Yܸvlc70[7LJ@jJS-e>lN_QZw1ֺ7Z>m~ 5 >5nĭI)5I.ۣ^l&'d3fdT9|:)_p[;&BbvDk_{n;p`5j>pfC67L_k#dߓ{< Ol2G}d 9><<1ЗA&'g($a_ 4]qD$fu.t(p*?li]5|7 '.0zp5vnduZ 7.g.*[6\G%atd_-H}Z Qk%<.y%2^">%;.ag:xp''ɠ$|X $IJRs?.&mx*'i\Ѥ(Z6瘯Nⷳwe86ahqyVs+T \ϑW3GGǜ] ΣsKI=Jy-S͂gާu잻%co..S;`әwپ%Va:pmϒxS_ Bw4o}7,t?Y7ty A7+Rk)y=nA峫7w.?;w}-ny_hoV?*p/_Y,yYWU}ǎ!o+?Zhq~R>۱ǿ1ho,Q{v'ۡp#K«>r%V`Z6{i [._ldB)rwGt o;Hm&;vƧPܜ`f<>6|zS2R}+윕~ =8zǧJnܛy}q݈/ˉl3Ndݣb~ʻ}{b'7+wyǑDu+J3lEt8ޝ8yS>Œք:B/='7uzvbj~/5y^<~:$n㛻dgqi{v`WS jLiO1ą)OK 6ʋ2# H̖ʛ8vJ[ u쁔m-&`V{-38읩Eaxq~{J" {Uo`ص8x}XgLFJϯފ9nO;s}0b+nAx7+`$/s5Z'G`kNNgcԵcc!@4k)n*ܵ:9޹8B%nyyTѵu4RUh1s&Ӽ~OnwZ%zKsd(z/Um 2IQU f-xP .egBQ--:3R,!T꘍)rNWDY T kڞBǀɢZ۶0u'(J[R"l\`תBv3jB8Ds KssT<´;־6 OWP}@궔}t̩oY:9yoJl= Tq\499OgꙜ2N(Y؛ 8@$w6ę%<# LdܳZ3cj)pu/y<@-وSD&2%b\Z=#^<9xD(^69ti|R+,8ësc0~'CK@ 9tAuIHٻ-=*6rsǏ <;uVǝ) V@<.%Jx+ea+<~dqYi;oR/rbvӀ%,OGhQH%mpT1կ>A`|+ˎtuú?o?p1<:g8^=^X}v%K2v[ꌒҥ!D_ltcRWg#C4Ys ^5 DvϺxdz&dᨑ䥛dET9uӞqɓi@Qc q_Bx4N;Zw<%.-ʘq?lIt'dz( V =/]Pg?'/2D ~>ZeXw3Z$OhgS=n[{g=5ź_I\l&72N9d ?lp75t"t/̙ĜE"Y|l͛,Y OgJIFKf!suO9,ݾ^8xeY}jIxwfz;.A7dmF.{V 2RUB.u.FgU1tԽz]\$2FW-ݣ?$ۛ!: oBGuױ`yϣy56hg0쳽lOCͮ#ޒ#/7Цk'+ڱ#mf{i\5B6^8hBs5)8׺|9$\hg v\Cs|>T^YF}3 )x~-uifF\ A XVgݭEn4is𤮵1z* X<ճv\[mEh( ZLp-XgJ|3(|Ƿ|b a~q~ &lxËכ v1g'9 nuwYkX0Nړ|=¦b+p]w]6W{FU+⤃G[;dތp˩lJZJ uQ8ujl[Wrvd9T@}_:j*lGpCGpZڻT&`/qHS1h\ }B$N|v7#\ؿvMG˞`3EgNkmcx7VW5 ]&6 }g{ ؀z U&qpfoū<1Y> =Ҟ$: ޮkÍw>=^ }ex{u|:.pq$Ϧãúȁ <'#C.COѾ@8~ʾa3]WGGjgE1==|=yO\Y+o{N7m2ћUv}Lg6>]Z YG t&utߚ=/~1e|55{"nƧ Jo౾ƴV,c 0g]Z[-g~=Yk8~FO}׸@653,cA;`%e:1|/7Ûy Bh/" "xۉc Q.[KH ׾+Z_m|oM1\I;UjƷxqxXde4ZyfV{c*k0z" C/ckڦО>'&'~ Lab =FtMM WFw6pѸ #%;muܾ(|`vβGi ħevW1nO8l m_cbC*/koS_xs%u.nudA_]N^Ms Xެ5鏖?~rg=uahw_9W%u3dEϊFp$Ԗp'?;usn)XNؙ2FjR'F^Gk<V$1>Zf ASZNܔL' pKǺp@g± ƲOsa qoMkJGtoP޵Au kŧ@p9&Eb{BvG+}o<;i11գ'Ŏ,(M#*k7ŲWg/DgԎ)qX&͸a ֲGxx!V86o/Lsz `K̫G@ܣ⮓ᶽ^mɒ^dy8+:~GϷy ?xFs /(ɹKGHIč:y<ϻwd#8-~KVrt1ۼk[ 1vgJ&(ZbA@ hޜ#^xΜ9^ ;u!G~?(ᴷ .}3L\`.aV*>aC}h 6PpȵefHMA{^f׎>Z8KYMcO{C}霂7f[?ֿ/tE֣T:/3{q/o^&0u47?~*IΤ8zorȔ1@d(](2f2%Nн&^w3ԑU?{r v_hqߧKyϲHC ޑsSQ XQ9Ixc67e$49@{jgԽkϥ5ce{5(}ќ%)LJ_{$*֋z~|^hj%x2d#b KJ6|=Vz"?s1w6C _uXrÿǯ 5#ut͝1كy G5kakBztu5fЭZHkݵ/9&N^p.F@uq€ӒJm:߻(WBD;PdԫP(,(U0t0{2{9%A}&@: ~: ƻVkMx/#־gVZp|X;H 6~\Z|rFN_>|h$ Nxϙg` ZSQosG'*Z O|W Nc׼Ǽg Q;zZu$G)(87z(}q=}J.yώKS[pۙcb.|B?x2xAW-''J Ԓ OָoS*ɺ&כ~;3۝ +3A #m:_Un,-jE=F޽ȳg:?ϧ|NC -M&lO:R Dl>: $>Lq2µhz¾3 /~FLcu|[OO~Q]vtd~Զl-i =/@Al盏k.[ {iխ%f2|yp-r1oYj?oK{%c[6ꨣ{}Su=:b'vDj`:Ov2E}kvUlM;f˵O>?y51?DcǎF_kG_EUwJ佶-)/!9UVHgo遛"pN(F>>($; +"Dy?HI'rN`ގs%B\.֤ Ɵ:vb쪇&IIr')D;vN=w OR S8G+&P寿}|_xߪ˷ȿ호E I-/}rw3*_my?p݋9ӻ-otuG?r7NNϟSg%;2T eY(o,|JPJF $RUs*~[Ƽ)fT^AcSCh;CVYP"DrD9! mUǎ8/jo$AOhk60>qx,i LF<|y4u+Klm7zcp`0.y!|g|'2a?x#7_9ۧh2wo+acˡNk< _4_ucygδ33gSwf+wI.HDɱF>$A#@>[A`ذ@ ōV(*r}g33q )#fS˃xnt MZS^}Wf|. C<> +&gJ y2=mt@l? %2Wy Zlcme/<|OGGOY>BATҔɵ=1Tn:8q=Fnk kY\ fc 4a<8C te&8qNci WmtXttQU:Y{:3BzH#`&$ Nd@U.Wi"P l}WQZѪzFɇV* h仆c8U\} ٵVJ;%mhooRb#!C,US%m:ePpP> l֔{)𫹿\^`& s&/knAO*7"{2/*3lY3*UU]RB+5M c}WƬxt k`!s4r0 %ul:<>du)܎7"JcNo5lO9f$1({*P vׄP1w[w{om(e.?&F+gؠGZA2)*/{mgMg]suoV4 +ZSr 3Flk󲮌Aټɜ1eRbФS8&(l'ሳ1;m FǮ993F EسpIV9S6p G A.# ~ye(NjԣΕ  %woISǘ@4s:gAաqө>t_yuU7+ϿE@x$*LP{pvՎe~Ov\pϳKB7t0\cZ'e~ H&D-ST Z﮻Uh򠶶|#:h˽SFIÇK;Ї0-c,څJFC EP(]i7GkCcf}aH'Mtpכl`0ۗz RϴӃ2^O,eL㐿LyVhOh(` 2qR)2,| sL` ]4pOMu<$YM:ҽ[Љ`z*> e>.? `\8uAkK5P!ۏh$ц1Ƣbpqۮn;sVa Q3>lHySc:!^~5t({…izG Ah|s$0V/Ͽܝ,p]:G PDvh._䯃Up`5>Vj(3s$řm%}JuN߹s'STa]o0PU:ȵ(jq,kJi}:2` 5 < $DifK}mN K?\czӹsgN;"HO\BdT UjG<=Ix*G.k̲=ysiD}vѝ5a [Nȶr_tn4k>.^lt42|bkB"OQ$|z'o~'#i'`v6N0[ZG2D,Xk%t7){b NSH(pݤe@tdRec`|_[R}&pQ9&VGk׫$dl:\XdҐvftĞm>lok;H[[iGd~]0o@ur;}Nb| EB|G}jo {(z$=V=qrk> 7Jaխ\ 67V(qM.[,g'\$x܂ ]hO 1Fm񟾯#t6ńv\QwթVwߖaNf*tKgN$Hasbo>Egv*yD9k{nI/vLmȊܰ%?tfOe2t>W?S@G!8l ;na~զޫJM}ѥzW~y췦q2y,r~'yJI1d_TǗXg|'Y"ϫcu>mtom_i{ {^Rqw5Ώx8Fk1젍M$;e$UA`BW؛{-;)%4@>cM"4X<>^"3g5ʳ.L>Ii-1=ƵK}S[;\ޕߢ&%ױG<F%6r`"*IGQ"f /G4U =vSxp+vC}bf#i$t%vT.;VO7YAnBZ>ZgŃgϧSgwo@u]'dT$`g2I2d|z.\D$֩TN3>q/]|7OPHEK EԘ4c0cYFRCՂ1S_ʳ"ιj3Ep9 hЅq?*c"H:  Ekii67C㣆65G@Oc ea,BB03  6c\9s)= ,8ӳޭƒ!cPj Sb pE( .[M srk>hP:A%gG# YANBԠQNc lkJ Da#ւ-i"Je#SϽ7i`h-ȉd.JZmD*3 nPAZY˂8T1 cL2c粉#IF顾Z~fI%q[6rv~J  ]TE; ]UPfl砃ShV UҔNT=Jw-Z@a rOǹ€DirܕLpR4lq,XCSaueU}fNbta O[9faz<L'x鶁VX%y5UYpЁYOҹQ*+3mہOorUfkFc@FQPPi;38}r~H E{ã`!k$+ <>, (O҄IEQ)1[ ~h8&O'1.}_ҹk +H*lor鋭hBA}r*B+h߳qU,*%%@%VY'G۳̱Knk\PZi2i*c1VD6u>5\=gFwDۨvgV4s4,{'Rd"sxrluw|!I =a6^=6ڬrо2u]]0oUn&OVe9gƬGu>rQit`]d8WR Y%[swָg:FQÑV7kV9\]f0Q] onr"Y;X3FQ ` m'rJae-/rJȃC̗qD2CM`V%S)>9n*#(YQꨈa,S'gB>vQ5XZ0 =1t~ ˜s8?NYWFo ;~~.^\w݉} Cb|ҪuEl%ᨪ_s7L~BeC; Yd:ʳ! G!a>r1<; 6G;`ey+`l-TE$5kK-+ЬXQU30w `@E䥩c 2aj!V53!C$$Afg#'tnV.t=>Ou6TbH?mun1L*lE3K\θ}n,wAg!炭mA0vw`."HG:ˎ!;GN҉(1اq)E% yS 5kYBw m*T~_`FLISru8|9F]7[C=nI y4lO=*8Ö{ўm8 !R\pg{{q/t%o Vf 0QZ>c_ ܫVINF .tQ h˙XyiY2ûFpMVЎPX-ՓΜ%qgkғWJW?x{j"_{l дml꾰qt#ٗ֠#ԉK׀RB1_M`x\w`±^e/yki8wA_Ail$0X"SSk- &Kq[ZcVkdiFsχ(s{w'M9N^|yex#A`Uo~%{_B%h_p XǓwaS6*`K+|3Dn-u h5yi"0oOAxWώMa<yTp|%S_ιx }F36=d{Q{d"#I,3=/m AK1 Bޟ}q&e]n2^&Il}n3kPuWJ}"Q+SZ=?ģ XWƇ.`ۊxOVv^fgoooAЀߩ$٦yMLѾ^:&"Oc]W//\'myG_@γ榲=/1O/ܷa"+=&FxD˖ћ[s*R}.ol `=50ǟi 2PaG(e|[E!*)DZG# o|wGޙo| i [=߱(Nm?0S=MS+ Щ~L&s̏KKˬ>`WO/]vp;ią6ljFӦHr/:D:78dge-ߟ Y}t}vE*{4'.gR}(d sy-s:"u <  T`殃IH娦F뜯ED51QZXo#.8.vFլo13ᛊEleRʑXV.Ij9}2|]qs,{2x0[i ښ{`(l^ 73+lձbo׷&keL7++exQ|[FŮxW x!~B|55$|Gifr6]|4?jNJVgy)_<!,ij+@-נŤ,0ԞubhMv]^ Ѡ} ?f;:e6D0߸q$2K=F#SeIGTڏ;N-mK96RFw Ca\^Sv=U1f&{2(V&-]9&Ub{ MOJ,I;ﭝ% vFqZG}EvL6RMG_"?]ieXceV, 64_UYJSq)N~7;;G@+Wb $ ш,|m^|qFA#! q&6O 7'n)SK S[#!u:!ezHNy&>#jS>dɭ;]~Q :+4: ث* D_Б(`_wҭ}lV|/4~jJv@{wZ[>d+tMY֨yL,ŏNL.Xit -U5hKs1\b(/g*l媉Bݧb[5uI9U.3Fl5Ä63R"6T 2 6D|Ӣ?MPRΞϭX,Ď3#akHf,AbKײ{XyҌv\Q ]#F?럄Ͳ8L"HWjF?,Aq;tX pgCdfb*@lvK \V{ Qwcej_Lж`z MCI^At奯BPֵcEhhI#, ҭbbWڞXc7 ̆"mkj3S`,ym帊J{ ACbu*4rtȹ'"ձ7? ms дl97iV|>0UTFsN@QEU:tnBS | Y ,trDU* [&S{Xå` i`=/Y(φ wuJ b *%xaU!IA5NܶV_Y(Gy~Aj; +fe]*n9cw~;>h 4IK.fhn͚90+jeULFlIUry%MNijU-:X(Up&:3OY{^Yn@p!ԎҪ ޣglLLϧeOeA2\7Źh6VtcvӥÌI!nŒCf< doVӖmhFvػ=[2MC5!C Aa[w]?A E&NɹjT*8]'A5ζܢGg`DQ{k(w-٨IG`"l =_twKĐ]6IAn4wBҮG~fjHqyX~2GkDy쳾+dKW[(O.@ t`pHտؠ#FLfR~:* # [=ލmTޗiA*|y;/Le %m rL3mF#_O4 2ϳG Sr]ݣTϱX ``⊀|.kGg/q]XHZtp\;kfZ1À6P#hgETz2d=Dy.:_KjB7w,˱:##'VW(O4?{(.:sL/̑x-/دT:w1=i3JoߏtA?FmpJlGf[2 8[ ]՞4ox+ޡ4;E+6Qph;ՙ%ض\Z g Ln~&K3ͦ+,txURӪv+Q{H3H sE΄b.w\L 9g&Sx0hT}{גwC*HW9H$e^҆?wHqV GVuB^^ Nf  >07uk@!}cs_*s>S,8kI;nZiz{cVʹdЉ=tC2|⾬!vv13w+cCo1oRxPe `kxs-Lt(D]SIn!W# {|A1Hp}9)/Nx=uBqP}bW_|a7PO@!WoKgڔ0ATV?njc7/{n~ݔcd*e$wtQu Rn&$+Oz_!/O.rAyf4*y4|bh|и 7YTyb3&[\q~ =' ;?'A|@eҧשVIȃ7Ҥiy؀{8܎16ρ.ZF*1फ़x X7+|3b4[F75&WA+t*D1RfxDԡ=J0&dN[P]:W'xg$Ȅ.K&#Ag$2m Yk|3OKeD0Yw1C-G 7vRaVֳn>!҇G鄩*Jkˬ:A }c{=V)&(+|;ԵLk&T#wal>yXz P^jLf m9Gȵ*-/mS+=fH٩%iPH=l[h{4%&ص Dk-Ry1lq;=+u:`=Llmd}=W?OX%s :W9q@4n{谑EРkD*Vr۵UhgQ?9*ʡ)͈pwX@ =Hz"_Vg;:v"_?2$ ؋5 ǝEꂂw6XڲgG;`qd'!OtckE{a軵3.UzT?B#S=m/jC KTςd4(Y[mp8EHYE\{+ ԷR.(Ϝ ȒC'`l>M5Gq b {z:Uڙk]瘜w9]p޵^ZU6T=2"c-.F@{i _e %] 3O'01we$ جmт_6XsLԮHQ\b:2.c&IK?'_P.:˞tsr5OK_hYbN]Ts\I{T ;rw{o]_HdHȳ& Vwއ; IP_l ݉O+[cTv6ݢJv|wЎQv4EpD`[2#i\oZ uݹ;>q5 ,-vX iwx1O[lVKD:Gc#C'֋+7Y֣;*)TEPBE6>1Yv҂55Ę >62b=A/WŸ&^x1t^UG,:zҳ/ߗgI=ՏsʧKmƾ}@g9N ;G*15^f''I8o9$TpƐAWv@01efJs='v\'曆W}U|>OFː=ǡ!=s/7bSΐ 4b3Jk<1[->==nada*s0u %YL+q{~=sVgȷqXw@%UZ"xN#hWTe]H/eb_:ok;TBYad@ul.V+JhG*PX VypV9#|8(5T<ޢ^Wc! Tf *dPMpN0Q44[ȺTcH}>EƸk3>ڴFOUxNңJ6< TX' F5VB P٫>gu5%ggE(7ph;3JPej+{8#d@Lu3ԸաlF|Al3In>!-襖$JCC@ajO(7d[F" Q޵B`uEm\Rz`ce͏psF!2AK8y:5j`ݭfoĽ>k 2+HZXKEitd#moW߈[Md,I )4 [sF*(Jp Eϧ&kΡN2FypP++QD!<샽0({-Щ4"YP\HjF @@ld+5¸dw(**G&#F|>IG` xg~F6* eWE>QRS79Mfg ffY\dI{ʪȠ&ySPWΰM2>VPJ<2A^fu &$B`\VU-(g&d/-c ˪qD*~ = A Uj)3Y ?G6йBUzq*,c]F$S"!c8nX+ǿ<bc$p#3aF[q]S;-Siz0}Ou/ab.};N4z,xDfvz2=K2zpwtiH^ ѪgalgGD%c')Q̬iIsL;±Hʕ0 8Oҿ6 ar3f׾ODmH$LYUVTVB:"|ce.:jTf*ȃ8>_ A̠/t apX[ !Am9|#W٤\1 40GgVHHTǙh'ؤ-``Uq9*bL(LsT+˿|G{ 5r<{γ*vH}IN-Tld ~!GQI)*xgp{o!G P]|󺱛Wk8OQV#8Ak+taʠg>s~>YCrI\@#8u=oqA6va!IYE w-;=gq *}Ŋe} f@cTF&7H'X/u$f^S"qF$? QKܻuzT;4ne@_]T:Y@]Rtv-y{ ]XE~^T`~v]v!LuW J=vBr}mj X>,qgH30[gݦѐ~GyHkxahX5W"I/-msC./XX|,3yGfv} _ ⯿s >d/ [A&@9XQ-^ h)Z@bP!kA@+>C }C/[nRWgƂo.cjԏ&( ,FFd(h?/2' 螱ϸ f[T? ߨǩjN|?+cx6`Hv :Ow 60P ߇-zgyFB<'o74mKm#L{.*8rj<im0'xJi(&[YF֩=T>L `|Gd~*иL@a;"N_,.;QPaO uk}&:ze'^;8~^YG̯ mڂ]`bw&{qtoEA62ȯ?* o-KSSܰ0c8vfJt@IDATgi;p=xD& 3hUNL "e Egv&lm#L!)$ Y":1PapDA33!LXD+z{٧rz8>SZ[$dA%+mZ8$]lD1J]atkߦ&|s|a+ ]JڮToqvtǤ0\k>3`snxʋŸW\V޷={t)Y"JE\9fO2B5G!mh] 8;Z /{}=saCyA\ju0uxi{ti5hXoJC(æʙ+F='kK5]ičLYAx$MhmcU~ý0Z:`sRxkk77 p]'#ɴЂvv6 !Y>4fdP xtGD=R`Eg2젢s|bL@KG0ؒ$'a-Nk{jK^x gvq8vD38Slf Xekkaǔ*$)U0ѧbV8oSYI}Y}= s6#>S-&[H`GiR lN}vI~Z_iH%>vd :Lr6*2gmT {wO>&> a?Qj7&l7 s*X90s  sA\o<nL7+Z ^8d=Ϥ;h(vr`V[mjP"]L'p`rj>o%hMJ#a0 C43i`\KTC]s6U Ͻ<&E6+a{î>)>  \&T ^ Ae,,!$^a]zU٩` ̀3k-? BSgʺF jhu*8g)f!HPEv/K'vh`bk!eWKY\ݣ@/3gt0٤G+ϊ-TfQg'T8f[MYjDV|f;勁."(>"gAtv6%0/8_QBCOh`^W??MQ֘]X$iʣ^J8(YdM4Xh|.٪ YY3c[nb\.8IfX3]_m0z^4N`Όc2FËRy{wxx?M#~8l}x[yBhm--&#(mD0Lآk/R̅5`oKTz۷3 /uhtGLsquZrR;߂zU?ҝ6l;ڙJ _зQQ-Eg1;i}nXZz5Xc7f/Og3ko|{MgSs @Ь@: z DŽ ۘv2D;;=")VDĜme+mA0`}ڥrQ?1_ecx^ $OȄ/=#9̂k{18$ EE"p[I*u tԡUYWw[uDVE \]ٹ-7&NSDjOoCJҳ]MQvJ f Km{srGPv'H]~<]yLv\Gʫtf}hiqxKrbvV X%zڶNܠ] ,~Z^$Qpz2]$]'K~IO+}937xgubnP}B&=ԐVAW]beB61;E $h7Չbut2*KAĀ3`FleIր 3:ZLmbW ^+^CH`:|:X}TVl#\&,!66rw΃i5V.R+kibxf;7aaon03bZMsc~i[סE Zb)sNРYے^PYEC+vv>G @2G#1ɨ.~ы6kj]ZG[0kX oj`Uw[0)kNs vA>?\czKVOX_gJ+G9ЁS? wJC_߸3./cNIW`8o ࠿GrKpX5u=PR[?ϮA +6Zi5+{aKЦg=g- Я d;\*(XOM&5ӉAmmbrR|«,s Gȉ1|mRhk0Z}Nc_Ob.000!{K`$1W|!%m! A4[!sfEyT>@kEFXOVV:siVzĔċͲJP0"r%Me b$ 3!]_kN oٛUX^Xq&; V΁|=o1A;$-9u1%j@lhݦl8ǃE3~53Ԏ7bk2Ӥ!Ȥ@b% <m4?h3P-mhÞԇÀzB^7X'f/*ŌDgG lm\Ȥ׸풫c<:G86 :T]?r=;W_H_`ǩwTZf-5J;ÙcĄHة:m[AsmúI=5sY KǖE~X= o[/ jf|CYKe&!AR) y?,y62D:V<,J2gҵJ4l6yk,G09=HD|L;Y3u}Kֽ2y;{GǾ|,4F;\*gVfZ"{MVU8|yhjI74ՌTvlf Vl\ F#Bz[E3<c`FĪ!llYcY2;]r=xB$?3{VV%՜ۢA8ƾ`c<n{-kfsU`kUqg Fi1 F[&}\' u=0bt?T>A|a!O< 6pw4ڳ;#@9*Wu6h`U^,S߼96')hC}M8 cfyL(ј^ξ TVqlrL83]'x~&s S00i#=M<8HV6&LO dvsaiDlgcz9Zei?|>h:g.̹ _ 'i>3 -,me6Ix2O~&EPNАWi"R<%Ͼ?ŸsȲo@Bvx avzy3sEfQVh# S&/~yj$=3T҇4+pd!Ʊŵ f~vZ*Pk$@&*@'2d- V綀qmEvZZ$Ktez*S/; DHv{Dj]yoIpVI? ʽxD΋#8i5}>־0H3 :O=0zh)>t sWZZRwyq%7p;`ٕC{VSI d =;!`-xbЃ6OLFB u !露‘DZ&[O| ]r={C:5[>73H] ndu+z) IE# 0qslMX_|YOrC=otyAt$>7N}]/'9J%}%9;1+Zk:Ľ^F]VIi(gn&ʗ>z_ AZ* B3f,v&M7k _@H?ȖB&jk`g03]K׸ S;̏uJIiF*$6>Dw Pq޿ pص9g1XgrDHMT3"_k*ĠmaCbG=tW龞5@$VӬ + OD؇C V~^^zD?!Ȭ=垝S!;Dҋ`6kb'!Q _΍7#3/7Em2Lο%g=>Dc= t+c a|3~kKt,QvS*SO_;QH I5> `fJ#K:9^ Zev6-o ] ~8X?E/]# ۑD]d~>݂4F56r\f Ǒٍ}Aު}v#]&Y+YwmWl[2+=Oёiݴ e6N1Doҋ_W>ϵ"X☖+9{}2@uTФ6v#sg_?>3_9v r4t̱ 4~?lC^+( )җ#y|c]3i ;\AlG # TI/\o`UА{L'mg*K4@C8 vRn셗Ov.NoVKsư[Qy,L'MbD][˚> '6raU[h\<^?Z[y mpٕ$2 lOu-K%ǸowއfBOP6hNM*s.:X[ӿе*Ė՝ x@Evf/2 e7O$.c9H&?7{ */m+ %mF`?'$&8נegE͇x3сMrH(Z 3Jvʺ! y6Vw (ql ʬޗQ=q׏v&t8oǰffjk.Pq?L5ؼHkW¡[V b_蒈~{=t~*IGpa؉`UrsD-YbT~ҟ2>PjhérN|џ%:TߩOѰfҏ1B#tz-0h4|PO1}`h,x;ueA[$0c,$"Ok䳺zS,&7!Ԁ/teoLZlCxmϴn5G9ELd[ qE,5$iˌMCLwB]vQvʗ.˗Ϥ4ƑkTWwjd-K) ptQ^_'9;:耆G4/צYAQQ=AЛH$`'gjC$5se]e-s&;ˑjM [k. 5Ɩ{vok(Sw]O|[C‹=&WT먞V6&>Xvi6Q!@S7މ{J-2ZROVTBU?ӷv[od~XZ!+( Bx83O2ƨ1S]E6uv79Hu@>c/棾0ED|E$VOUސA>c<@Ew]>r}jXC*}=+x3n݊٩[m Ǯ[MȧŅEWډ7UK2ͿMb;D׵uof,l{Ϊ"ː2;TTόA0($b,{Ģ| cF;=m]' cׁX\',xDZva"辺Yn`1.EXa1;܋MUB{.I\B6@9ToK%~[ףBKifCغEBah+~63mefˀ@SAZW0NpZ l*vuXt5A y8=NfUhCTZ$۫~p60V0"lw^RԈ8fU ­2uR9( sdŰ>ǀ]NO&E #Nd= zR0J9 y 1ka2@wU RgojZ&dÁB)pI=M{e}^0L⚾lO".cҐI!Qls{P ʜd;g km mii@0@e{ȐTbq>Y446jyHܯΨ?Yڞ7h鱻A*g?7GX gy zNz)-'멀!A+f" ܆LO"’vYU#К.֐oJ)8VC'ࠅ6* ;)mt>4 ]T, 85b۟#[XP F}V(Z5$-xIE%M=u1L=#S({cbp/s n @}2:39"&{d 3(=G *ZUͦ4hd^ 'eYxsfq?^T Z5DS'?# k4gY&%b@ONM2g5X&Six~2/# ςcctz{{ho1`71-dWh0rt MDS-&Bǟ}ӰU!ҰV]L$X"AN<p{l&MR|ɕ/}{DRg^ӟ¯9]yrG_ ZB>H]8Vӓؤ|ici. i>cx}WY=BhZYTCӳd<9.XJvQ3>jLo_OϼFm;@]\G<^/Vc Z:Sՠw$)/!EPK+Mm9 {} ;kycqX;eV2X::Par ec2+uIzʉ%*Of1mma碑qF΂,M󻾿f^]U]]LOhVF BHC`;l6a_7a\7# ^/lC,Bh4f^}_22+<c;$9S]]}볡9ёwr6s-ƛh>Ɔ\a+x~:9tI@{Iw3Øe6hc\(D =T_+Hh59b둑nK'mJx@;ZҍSVx$!J{fބW&'ö+e0{: 8nn?}7f1siX?~lL ٥*f Pw:V3tڒ7ݷ_^0IXgntةgQ+/k6tO?# >zPh>/QOp L+gn|7ԯg9ʺ?\;dFa?.lI@0H`g,_<Lj #5C>Y6ynݚHvƘha혵ǯΪZjl&?WR֬ Y3 g3>zh_d$stgڙV0QO3:0`scow ?[_3h 1hz5 ".gvYB=웳l{dUâ͗l6?u?N 9,ٯk^뜇5ܓ馍]͹#x{sXpK 0hm ;%ԺGMR}|=t.[x\{wߙX>/{a=6]KO_(h':k\fK|/sMSw[v8`NtƷ&,1A2+$F϶VD(1﫝|ۭͳ>0Gς?zPzK]I ͣ-Y6ϺK99_bs ?:{0YWg?_g&CV &P"(q|$n{wju\W|=\A0s%A.hl:$ɇ|G[{YIixό`H|+| xܘ?6]}@T4 "YoOJ#0|Mn*iCC cK{ӡ֬}?k_J#rp676`G*ӱfd *GxĴƍny_\IK}ڏf7b)H=u~>|IJNb'*O[N0W*2zz_||SG;齃G^hzz;q ښʛ#ڐٗު%{5@gZAsm?㎵pΠ;>NPi:O<g>oh/ >$G+6\|btVqMN7o:O:-aq2ٚ9wg~a_gWk)8^[Lhq%zVqnO'9cv[!VfẏS84ҺτNi-6cnP& D=m_צw77f1Zvʽ;u#.6BUوy K>}xPSE'r8رO}~\ڍEB+ >󝄅ۛ鉽uP(բČf=̑~} 0mĒpCX,;A423_0= 46E}t$(3eozof:GyVe $x./64 Qup齷~s: uy{1}Y;+n);@KǏWn:wO\w\m\i6,à R RRp25.gn*P)gL mk{pF.@Ik\ <`Qv]׸dܵv2;؞-XYF1l9BPy̚79s l0ޖºPL:h+Ce&)Ɩiݪk Z>IP_@o*>fTe?;Ԃ|XŘ9,Z{wU*Ɵы(_Pg|]0=pz'C>$PqLH[ONF|ra[o@|]<Z0]%4Iu}5ׇdJD4>Lvs:=%a@?%7*Z霡2v3vZd$ -Չa2Eg:Fmր#כm3c4139_vy9}~,P_0Ϳ ӫ SƇ-V]Mn8iF^&t/,p|y>< G ?k!m̠tGRT$C Ej$`}JX 2zEt$E=u$ʎ!;˛srC#_-Za'.^ fRUɐ}TcpT@#ռ*Re ooV/Lʴ}>>o73s>g7O9/Ɂ/@}ݣZrRGr"RqR9EVx<;ӍNN期adi{} -!Y[A@ n>ͱ[co\@Ʃ20״?e1$ogoۺڋ oiNd#^8RvI!9x9tn@3]վ.w|Z5n^ oE"ǜ`.nxURg.yšEck{c]gk}ЭR %Ahf[Ɂn$S51.b\m-= 6(ҳFe=icLu&훵#mL3ϓcqX ~_7xXkaH;^3*u6v xtzPwɹ\sq/~˯ЯZ|(ŗw;q hVU֢rsqaxm9 GTމvL}xGhF PČ\ü {ՠNkpI  *yWAr,e { .dO$?[U&vlӃYU % xhNِ/oi%eC. f&ݼK [w 9 CK4;ZZ~Zo"{@| *=`{ ._ёp}gwIKUI4SngZW|4+x(cıQ9;|tN` Î5djL};$4Zv?lnsRw[J7S fS7hGd2iF }Gz`rkЇ5"媋 wQo ={G{Wfw#ف!dcqxMscF߬d 9=,I tҵ`-#=w\>b L>3.}wU}qtFѾ+/^8;2tTnOm]LuHU!ߌJmU43M. nY8ڒIpQ0%3wS/eޚ `Z=pZ@jكzO@Лp:RsAAA5!? BTAg|'MTiK;pߙ+1*:\ OW[? _L5CTG*g%U/` (YU~|}x_q6kIvd#QTn_l[Zڹ 3[ԑ$'qz^,Hх*ǾECnˆYw\jV>{ǟIq @o!)_8z: qնKa7+D =ÞƟ`k:ÕͰPY+I ;};o?ovdBUzyKϻ7_t ӗ>UxQRxc&ޟ8:sBIa[FyAհnR50$¹!d$Qpɚddqw,Ggoԝ-{s1;2ÞK b_~ctt6oޚ~WީŹZNH ټ Ҟ7?2hD'`Enm%%h-,O!U`rГ|P;ͽV{=pC86ɺăMH?@,/x*% ?x<Ŋi6v ?ΰ˥1=7w|'0j ~qu] I$랖ވ>iadiǺ8jߍʻ 24"g%TE?t㫠{"q}o y@=amD(;{-=AE]^S ? 8`ֲM "\f0L! 7_nx1Ze ]xnzLf|8BneOiJqBYj3daT ޭk  j$֗%CȦZJY X0on@ΕksslzTLڡ!^:\;v0"uzUYGS@ƥu@  & iF~PL6l}+>rwHNUW zw>CDH@(,F ;w Ȋ*?bt4(c#cO}Ms_o޹6xWujuªGtjў%2ƼznGk={:8}Bߵ/:B ]1Rκp+bžp,48}msԳ+7U dHZu^qwc=}wdn7e'̆B. /.h@aeZlǕve-Y+ |8{Gg: =1X.9\2Hu2y݈NlsZ?$6>]Oq,̗Qk*6VOk%&Ux'pwayS4xuͶ  k1q/!iT T :'ZN7&eiWލk"gQ/^;rzͯ,סpG3[Bqًd#=}qZQ|ϙg@qlePKqo4-㜢Rá{vm]k!x=C1k!{kkxY=)˸o>M'q8}L9ʻd<|}4mHbTYڝHT0o9 2g=omf@ u)J]溳"W['y6;Ň_ELlA̓,A)-OU0?xL C^n=֜\y{ / h?-]Җ<>ak! ^κ; !5s,s:s$DZ0gc@ttֲ3gxZv<8~|U6υd6Ǫ7rn>m[CvJ=*-&f2T.]4?ث9]/Tgi t0rx5uԉ}x3+Pώz9VEJ;r^2Úֽ񪼐k= 9*`ӯ_禿Rݿ~ݝ?,whs2gk.}Aކ.צ9Uxr/Kq3bϻ_zetux/Atь柏$_͜2Z}} xBx<,QUIЖ[`==CS PB{/<> T)~or|IN5<[Gƞ;Z6nm߻ub#NldfWj~Kζr89}2r1ThAJ>I[k3;`̝2&/x٥G{<]nUfU{9l>Giгz#&m FLv?-}oXfut75Ǎd" ;#tԣޭ*$Fȅ_$Üs}c_?zhVtqYi># }b8\{Z2!y>l(5hfCxJ҇uxgZ:֑CM<[cACf=A3?֯Dpk3Qux ۳Y{m|\;h$ž-q<Mt!IVy[4SC@$Z'.3=UΜiq {3M;rkwK:Wg qI8\;:8[^iy)Qڂst^XLNC%G!k,hU%̧>ڶک?AsĽc N`5W%9zxt_ڰ@ёٳӮ'j8,[9_U[7ϞT8s8/w4ZWߝapkMs4e &vSd%a0cϑo %Α$&=a8-{Mwz6|l]Άt]m=7i}PqGr8UpdX>!i]U5݇ƹ/<ԙܻkjlku UzGV?I' 6WB1GssеyHZ>A/x R/I>dX>JX+No],Ʈ!◾8O{٬Ů]9ǩN YuupT_J-$^me2bG] pI2TL hJ^>bRw*X~Cs]MlM),}=p`<2~^0[2w,[q39=0ӷݚ |{'6m,}8nB1-J*"e#9^F%\< emsMq;|]Æ>b(3wT׵+Ys_cl`LZQؒ赟xh6im삜|"$c;r=KGGXNtrmOW}co|ąt%oV}kg|j?[mlVjFSAx\dJc^wYzKLæZq|0K*@jL3./Ƕc5r$ ѥp(*[u]Z?8Ygɝ#?_[]#Yy|q?__(^jr xIbQnzx@z:JaQTdjհ>;)>gO=%@)ܫhÙ_(wޜvg;rS"YO+ڨ*7Z#v|_]>"3%9>Dpqط'ᩛ7gJN޵?'o|{q׭NN~޼n+/+[هs7 ?8֚&{@r+{[okL1.>x8M?;WX+(Mu]uhL/6QJq Wt'wY[2.z%ڡvڈⷶrf@zw/lZ}d(}ҽ>-i+X,~/Vs߃ jU2۶1)`zҒ7S] Q_IJf{R̂ϼק%K"-X"Dsq-m|בBuOMGG0Pқ1Kq11CmG 7_Px)u2v^==eG@e Pq3ōH>()[]o<ܨJLv90B`E VU53fK ހy=ѦoG @FDŽA2},ghh2( gmmڒЂrcՔE릵uX9[Rh%JJX`"\V&Pۚ Qc2ߖf(BcYH2"֊a= ._֘ ^Bncz1j܍/wNSᾮ  {NK*Ly~Lϻ}OSuy 9xD\.Ey('d^K 9xUy~'_X d23{yea|Fa$Z?hM`.k̡Ǩ,@Ksÿ́L`O)i|v^ս\vUq 7g0t!O)_|pEKœsZ)aEjSi?#w%K}o׷0|}C #Ӣ % VU8#3i8oѵs7p}s>(WjvG7Nzׯh= ?]Su޳{֒ _WK'"<L؞aFY/2HbG=&}38~sl9r˞ ~ޞ`oUaǓ .O€kڸXkH7j^;@Hoe qd=t,N6Dk ~K];jCjy @Z7~}?Ӄw9ltn9KWwV:{ZjVݰz~Herlh'Yes=V)_)ږn{9|N{ЎCq[5gN{o4k_R+s6ηrȐ?>"| i߹Y1^d GO_6}7jwx$b1='g=^?3RUi7Z}Nߟq{Υ:mRƲ9a< ̫xښwVj~4bD _[}WkoL~@^~ʺLSQ[G%:SKĪ8Q28`ɄkQ@=}%B̭ʨR[X%g0ax{8/lݐ.pV3v CvyG<*U4vFJGXNIXuEʸ[~Q>yth8$Hu!s\2(QU }t= Akf}*t|G3GeoגyTn`s&ݻwrtU NkWh/?ӭn|?!@"a}ІkJ7ڌfߛa6.]#[*1Q۞8^U̩9= 4Ǥ@ >̑SoZ;(-=tK+J*6{Kǟt$'^<ѬDU#ߕ:IphF}LYcnx`]s3}՛KEdcLwpxឮ[ծki:4/%hֶ}ou#t|8Xp.?4xű莃 ]ԣ=zXw1%mET~_v߳gVf&`*ltM}nkn&< #G@U{ss6Vp=[~ә_wÆ=?6n\ PW{6a0?L ז۵Dg|+\в}ގΆ '={\\78WHSױbV~dN;BiK~a˓$cG]e1FoKLO Go|PݵJu,0>v:~t$E#8U{ l όhTǁ@?xH2eðw/B6W@v̱z`Ʋ5>$$#1f~pU#/ݽ<"ЉIu+x@$1&/nݸ=Z{rSoJx7+ M'ܗ$JP'I _l~'fKo̗2:TO|urFl%l vG~G |޳ B*Gt:zHɚZCB"TZ5|7|ӇWP(-_:*wq\E}dz=KX[\|0W%'ma=gasⷛ/ձ=z:tp?`\{*G^.X ,j$'T-/ŊQ;?׽gGb+ 7u ݻoK&wo3ڒp=Ϸf~w} qA*T2c^#Ӆo/eGsvE;1Pd=֙f]}1VKog _aCHN6fݣtlfegݕ|@6䏀/&M4H g)x`Ywt]s]TVKxp}hǑɎ'% o+ߠn` J k阋wO_{?9 SD bl`x|&_}x"wf>N6\[8*}G Jspcd{>]Ȗ=>(,>4y>z+}G7\tI_-,BO.3.\2hI R]G/fgW%.9S@Z|o@H[ܺ;awHhԽ|C4SI/'>`d[ıO-)0\}kt=$a,>(a^#^V퇵XDxuW!%s!8ڀ X` }sƯ̇AiSZS"zp>i3@7P'_<1lkpY}a@ C-F?~ر1;mbUTni,ݲΞyࡀlMrY~ [nDp,fcM O_#Mff9z;<T/bTL;T[g;j1ѣiӹ3NO[W ay6N;0[DvP푠,2'Ep7j_z䩗 =\sC2W)Xgq )E,Z%'Bzqn6WN"D(k ?˕d$7g`iʳs>?e{C@qB'̰Yn| nj,x sri-Bn͠`U$hߠ)JR5G,2-wgr: :ҾrjR?vCȭ6Vnd4A?$|Mdlt H?(`Dy[kv屧yG2xf|TOޘ (|d ujr톇)d.`&@`*Ycny"Ym.u/y׾^N 5e=8qH6U;*Be o>8{ 1.8Krh Lr{ET~2;ƧO IhYU*zRdUV~ΏpAU|8׺j5O)P1ΠWt/'{_LP' *n?vAa9_)e qd}DƑӪ85w|07Yr'd`~0tA zVryg<1g/;0y΅! *yS10Cz&2*kA+:Nh# ;e,R%T-_QbJNvq>vU>2uDQ]"&;n9#ej~3%g`+\#s{8{&?{YH\/DKsDĝ3@w<wGésܙ3pkUo~[ߊ~[v]t˩'̹.wK(1hh& }=T%h}fZ{lm ƙAKTٹ/}wIٱo|LΟ ~zd z3i ;lDv\ei,HE~/c58tA뾂}%U+1wcFhņя?O}ۿ:pp== ɼ@ɓC}[fw :{O $7q2ܚ7G]#F6F3Y:ӧ?tўU0f[ɤƸ>^Hd {}tAU'1L g% *JbȤYMsz wKsNz^i~N'̙|o:pcI /|Utc|~|nOtfѩr2u zY7tϚt؎g673w $nMIkC[<}c`M0 mD0|U$Y#4y.6#֥9'S cLHf3fB6柞0&@OUsڕƘ 9`ˆj7{#9u懹A(A}ݰ*RӷÕh?ia?gtY?}A! ֛c/zZKZgY]+A2Ɯ9o`Nf`X-%da΃%0m8o^b8MX< LEZ g]n&Q5KZʫsi->WpX7҂RgA }6Ѐ$; 6sL?Ka=6]9єޟ/ }9ZM$R5#t#\4A3ւ=ٰ&YLv$ eiֈ⨅yﵜc *u A;ETv99oCd&ghUpZ ḫ S%Ÿzc"Fk$tA[dq{X>w8( ރx:**n ]"l7|uD{m?1}ǿ.Pyk pFE>)xQX#H zC=niFi7.z#{O1GF:|$JNh: \d8+Y sXVAWdxf9/?I-dT;L"0ލkWM=Vֹ*.օ״;DϜ^gANK˯b{ |ǂQh˲'/{onn/2?5_YUlŜxHq$'.vT`zՔc^2xxt{<#>YNWg8Con^q[,~pX6{qxε^ XAJ/lN28wƎ_lX89#ilƟ?YyV2~ձj7~ קϞ:<*pB*[lLc/;&1O4G 768#L8πNՉnؖ]ǁ'{ t{ѕ $GL] l^?}tZz&];{6KsV4K~X YD:&8uc @Cn l漺ڷ:fy5Mv(4͗9OszSPzg̟PuǦjzeWK>dS|v#M;gu5||&xGr4Dtf~ CF nn\(j Q!oWBWއI!oO4B89T"+MxM"ޗZmfce)mB v1zaGsN MjЖN}KNql]WӁ-:սsoAC*DWA;zWnS%i,,ix|4k؅c18P2 !L*r!!$X/o(QA9Rqh4m~+eV-7ONwm3? :ӵHj%5 x(ހ'1a`x=}[8֚55.:4φ3?,If0-4pvŁpu#!WTCKd$y|G8 \9XMyߐ?w(?~%!V]nޒ9-yS]b-_'<, # ^n>qs|3_j }'[^^Gb33߈7ҕ%a.vNx: #/~a%J uy_z[3BUGg<M B~4 [إ#~KS1_b ۷Z7vtSI\f~PEϭw+6죰ڎ'eK;]0ܖ.ttfA `\$I4Ʈd:Bn>|vʹnmafS42vZ~YDXQpXՎ3kӻ %֗k2}o]o'jkM$J6c 8? du6pc¶޸[r}gd2OW ]FHj%KOm ZgIxq})V4FHD#'͆lm-w *@~ttO*6ޖ5f篯Y,K \8! BF-uZ?^u$`9dgm}G+~mk6*zE ESGГ$sqލ~mYם[#J<F/19NYJ{]kV6R{ڃtYIHr [ hhimp)s2";`GO^~ zy/ Qgo޺ Z1{|0 !Ú{ֵ,It]A A( ޜٟ/ 4lt|ؼey2c@IDATHRi vM gѵ:ZeĻ| jDqˌ!UE=9$|b*@Ӎ`>Ij6b8/U|EH\.f-^A6 ;y{!OBH9ijӏw/* PKQ!>ٓH5y {cFCry)>I#(ȎI)mmT} 7m(c> Bi\Pt̟3^)(|UbH ^+>'(E :v.` ^F'2߮Nhc?~hs ͺ͂O* #t/+p<}Ir5R#g^hY=ݾhyxS;Օ i ӍK燳o}km7\j\  ExŌF4D@:b# 7N{7YBŲ,d ^z|ݮ6h"ZV!:_fm-܊PZ/ [;<]WU:Ax̢> ւ@x.)g) zR笏1֋Id<̪e=>/[Cהv{O!Ix=<zN3X"͞ +ǭ}NHA 0tbx#QQa_);g蒞߼8F:9PkM swF1dXk_F2#`XhX97%(G(g`tѽ,d4~[H>SL:W3,~^^bhtny n$8}?tc@>:WYi>s3G 9ϭ[ZCa0$%qv9g$= H:A# HsWŁr|Nn}cA}u&.`:evɅ'F5G=W#!\+V eh^ׇT5 @΀a (hsnt#.OWkOU8A}ӉuYZf5to}TrkcM_!~Fdxt4wOЋÇB_AˁmOg o_>?6M/׿Ozo+u<4}u:tawUzM"eҶOwĖ\m΢19$af隰e/9͈|"p9G+-9"!,U2Qlfx l1FU. Zg rȼ^ufCڕ3s<֤ &ӿF;?c8qJ]wʕ;9Kjʃ$P-5 AݯoYR ;C3 A`o L?*ך4< /td:6Gt~j.K'yd(ADW rZ^H؝c.Br>q9̜ycKX4$ǒVs; %ʤluȨDxS5FHKvpڦ{Twm GoAk9tw߉[>0I =QHZ%Y-' H,VB:#9TXsm[7EkfgK0Y^Kد_K?N|q=-)g{Qi_ߞ?><ঢ়̧ LD"} -3g [9.ϓf_oǠIJ{+'I^LUhO@׮*}t\GA37ҕ3|}<d{s,HpNx21tvÃऍɹ;p }>vhn/םWzj}~pNiWa^>&tΩv=po # E$ycЧxu9^efwu=KW0tTs~l搹42]1]Ç#ϟ2W׋GLvXߝ Jq:>gOH .d?dk?$p\Lx%PNd^CȠ5jcf L;33Z7֕o73.@/mhzIQm#;8ɞɠ$]ؐ|1y ,]Jij]ɳh90[\.ts޻8]]qZp:+_gG%\QY~Ku XhݕDq%]ӏ#~05KXAi Td'ʾxܧ[%:;o|睂NޞnN8<}ѐͷJ09[;zwAlxU+I)4 g D}haU!gWl񶗀8֣kڂY[>PX'ΝPuhh#U?GOΞgԸ*Wc*_O K%qU5E 3r 50yHb1v_=҅͡+ 2  ϓiȱC7ߋ΋IQ^{饃Usln}sG߯OFڐ Vdz{|޺:0} 29oEddqΐ;_ |g3EUXx :緱}oɦ^Ē۵g"$X;Hnƻ#þG3_9KNT`5P0?U`+a505{0jZv;օgO<~W[@\|z?oN{!S3|{-z^ 3\P61oGUbc-ػjxm'f1"[B_5^kLI ł3A`uX?cf4C3NB3گsW/3iuMï!6kgMKرO#>y1l}#tS1ҽ9*b+[ߝg>󨔎? k IVыޘK ȞYMF uHx98*` =FU~ru}-wIp}GZvjrx`2Tb Z#vrlOuF/ƿp5Xz(%K=g/؋# ]$%0kd<1d p)_MZK~0!dw&m{6WSؠtR^ =(VSOg+*_ Z$YC )=;[ \k0^)ؘ|NޖDڼ=>~L:VSLhdf!SNH#X=qjz5<{[Ժ:8}FQ!Z4^ عw=`t 1b x⅑aӳKE-1y^ܛRcAۂ ^.d=ٮRfͫ,;׹U>* t ޞxyNպy4&sӖ}Ӗ) OJ~oߵ5#ّa ;cϞ l^jYJNJ͋c&Dlw2Rm G-n60W1ujld#[S~\{Mm DU,n[˹d9Ζ<PFExG0S w堋PAd}x [|d;a%T%0~fHdL>|L.3iԚ !;[i׵c9` );3&-0{@lovלƵV u}ǽ AP-t h@}9( F gvTx5LZ]۾}(ת !5SC־LZ+?[~4Zidor`޶z -:ovG㊶r<k޹~~OVd5aƭ^3Jȁ@'=5-lܠ8ӃWs̜OC1?:VKNXoij&뛣5T0h*,5[`Ł>>z4Llgd|qUsƅqQ߳̔uɠ o9 m\ેKX'Ocu$œ}=KUc|·i[8p/+ah8S_, Koךh;Z]+/0@j (R ;t^zNO~\:r⥗ꊲgǘ9~tˣZ[BUE z Szg2_gZ( ݵk0R [nӃ6gsTζV$⇇щFF8P4Cht(Ih#xnU"guSѶ z'zi|jZm#0{,dHQ4O OW?~ok?2OߟLO\vj+z}8IK/Oi - UL40In0 Y xkeGxȿ%vp0>;w7ٴ!nd?=rl8$$=I.g&w6 nOv<9=.̂wniG8M8&'?̉ӎ.OWC>'dON2|H GJZiY8f@$m';ztk3iUwcSj΍`蹍Ʌ932hEF3-xn,u-cqk#.r(@A72 ;{b2tՠ ϶*mfbD2 |X4 pGת2}<=7sv{+ZV?2GwXU"if{t? z=mA?Ж%̧ ftl hs'[qT^b5ڞh'vݞ6^99Gr,]TȐ`l';s_B p8*BIdiBc54X_:I2dRr=ӅKTsn!}*@1,2sXֳO`fϑ%;?ʡ6dl>*9E6=Yad{n9cڑv?|(' [;{$ͅAQM'v۩:=+ATׄds;24~v9x.:0-d!3ȣ+gFgf sLZ^ڐl{K__m+P:6w٨)YQݐ#DDHr˪'ڃ1ud]BcԪ#Gwhzō{-CoV %$#'Iǽٍh_#Ygrܡfφc\8h~U6v\9'q 75Zlm 䏺Vpzόf.^zH K?q<בŔF5xn}mWl={EV_Ё>ٻ{2°0nW;jWYv>vT8T $ۘv @r, r*"ȹh(!fȂaKGcvbҾg5)=kӯ2GI"wj[pnfg3mYϽcaZ.1m<ؚ(g$># fX fIɡ>gK3 $|ta*; .S 0^V~Wls,S=GWOޞKڗ?=j:VP13:U$g3 Nk/62?I,tB&G`9{_ :}˟#1tz.u{ڗ>OnȺm}t[B ur@ kYB }@\r}']c5WR(-B ~o?dݖ[/qjZڵfjawsONr߻yc@=KL楞h P+S|U܌?x2]KuBJ~tKTSǎLW ̱3_<\?mi[7}wOtx/ rK?xΌ[|+Z?SIWgMn\GS0kd|TTrgUvbVIV%$Vs(67o)1୑x WF"ߌ_λ}XpmMrQcɞ?ƌLoi44{C@jϖD+{ 9@F"\0YfID>+ [|>Mxc:wfAGӥ[^~s ӷ|kxmk/Nʴ19L<[asvi?>oG/u^c۞J2¼\a>i44yRGRMת|t.SP +6`EIfzk= `#qD/+G"Vхȱ(?ڐ %3pK̇7tvuN20Z'S0b2c4 /,dxŋӷd")t=F~JF1lANy  O6'uvϞy8yJNJ! E[ju#G@t#ZjE M`;v5ש>{絧5s?}<ꨠ 3Ӄtj]%0?xu<nW|Գ++E˵G&V`Ӻex=كD{l9jĔ]i{g3+W{zpth[J`t-bσW&!!L`χKd\˧/ݳ鎍+jO310-Kv_=|Պ0>TGn3Χc/$<PpG|G" Oi>'<13n|(s2KF ,KX{?O=J'RpK|'$B`-7O{mYoi&,3X zwbmaxE"DGMuƠ>LEkϚse?8b[:+*+ab&}UT_1DϜzFxӥvc^p}Ok2U9\R ^v86\ LS"}7eրQKgqp3Q<#|$kG۫1G6L>lUK@c&Uo*6z(aHp 0QRhZTL3%Fk'OP/R+{g]t0/v HS>?v2ϟy+d[}S=ZΌ|}fUgύA>R]2Y`kWkT[n+k8vrYfĝ?uTbWՙs)kFFœ6SQw§2˻?O8GblU=WA#U$b`(s+z (n93\O+t+=UEbaeg_MFX~'9,??_{˿ߖ_eW~4'DR~yo/'owŭud]<]dm(G0zT*MUE͂XUK"վ>Lm@uvp3?O>K\|/v]:n7aq)'cX <G;xSZbI38Z[__>/ 3_X~﷫{ +"fk?xs*Sxqc> >TṮ]k4 tݨB-ЗȘ1|KPl5<=2\:|o]$^ApzDyz;mij9VgR:ǡx p1y{օ=u FfڎV+:SرmZzLhs_е  p3V2=Y>SCy5:n*m%}h唟>&]?NchD+idzɆ? `t tK T^m=FCM[|Zwcbpg'sRTFc743NIا3~+SM3W|,ٗl:7bOgh48c#փOҞ>gqz܉Hw^o0žYуhaUѧgCH[ H.x/푱8я x & B뺞IޙAmkkUmsGV}t=)c^hċpWEq{Խ9.^oUxw&gݷ^#V42@!'o3^+SUsͱW9ڞ ܁W䚭1;zo_뮄K3 'xnC{CpE:QׁNp,WmF)Q[fOo4ɛmO}D 4ޟ?WbC'LJ`KWso>%_>G_4]ݴڲ?E%B8DҝQ鴥N7>NpB9<I"*< گorr>wdz~2 m=Lg.O_~ݹ7^JZKg'9/z[vsB3S IoF|^[}dDɂ]g]+9fN샞"41 êH-߈v>U^(qmvO,_IQ*y=s{Ėzel]+>NU=͜o2Co 6 nM]J({%9St_cFE)}ڸkJs_/}.xV2DWZL,^#`2f<bm3O8{|;΍9k~_^eZg\Y߽<[|7c?D= fFl2|V-5Al8w| Du7Ư t~>|~[׳SUfko,Ms8ß/.~uT' hF6ε/u_%p/a.$=4 :2[K'}Ըt][ޣu8#gH.0\9T@OߩwOtw m-JBA~/O|$,%^O*3ڛa}tis _1t5 Mek#dKk3<ɰґn_G/: `ϖ#\,7;%>^-IT)n]8gˉyg,f%37KF~%1m[:$M(#=Oʍ!~$u|0w\|e:}ݸ=FYj NO[*HGfV"ˆwzlu&VoNtot{)ZLs/=va鐺 ?~#O|YtJdN<2/aL.ɾe7&7b&ߣ~:A|:hqP­elAq5[3Y%}T+h9}ncYwSqoٟ_B9q4u~r1ޤt>ؔɟ{phw^_Wα"V4q {ݺM`;o.ɂFBW{o% w1ܾ۝Xw9[r;nͫM=_':{vG\_v: 'uS]Om;5]I+5;]dV l3f׊%nJHx4;W9fM1:==ͨs*nY%m~㚳O|0@!'unM#XTK}h—փXgD?1S==J|]+*k[mD6 u' u˱o1lsB4{̓O;QrlKr:%gf [}y<|qt >k]:y|GGpՙ*{ݧc{IB/5|w8p:Dҧ{dD | bZ-:%9 pIs Օ`!!m[ٯe{C)R((_Zlms1 ֦C+&|ΙQX;hl? 6k-|H (r^ m!*xT!6m%3~1j`8v!ABۚ(HooEIqJ"'6Z6u4V.kz&S}OJ wfg9#rܥhak$Y8Sj:ޝUo킮AӞz^L@ Gv%7^JXzzΤOɊt7 9wuKG?nhl|Nj1FMA)pr63Ԋjc19;a' (J=b2΄}WU毥6eҔ g`G;?N0Wt߳He ׾;Nӧ_^x |Ib}͏I銆(iNO2t߸}~(z!ڍrJ23GO z[Ky@c.'Yk)Gg[RB(ÿ#cpТ)|1 RB>/*׎SB L&=@/ mo?+_MypzU+:Kq병9Kw"50^`m?Ȥ/}'⋻q?q_7W/˟T8~~?,]A9H,Gɦ׫kQ>mD_]7 /N%9"?m ETu7VrGuiю%۶1sJoE$ӑ*Ң5t;X;M&yf8~,_? 7FZ5\^?ܓ~1N'ar?nьgKNc,NiNhћP{y^{s2y컮)eεo!C:rsu.=D=O[W!=nd-ꇵn/Au{.U[S]jzs@ v4h=Fwn[s'ǐvop1ozg\ ㏾$>0uwHg&w~tk$'1x-:KBGkݑ'U5y6}:LvN +ǒkB2@RƛȞׯg#g2ϱ~a7ޜ*IOg?@py Б5wV j%Ac R.o=+Xup`>6Gpq1I෿fU/G\ɿcxZ h~0 րM@[߃ĝB/>CPW\o-_^/_ `#=7:bޣc;UN@6tjFўù6gc;a8ő&!B}<?G2 ;>{{s d(߿:0cqX7pM|V(?͖Mk#sKhrdҜMZ;P +ٌsiΫ|^ #gֱ^ܘ. .]ג,|?Cm0QOr\e+[{7쵊$|~GX ɒw]KT} E 9ǛF=9^9w|[>̊lG y};/\O_8' M/luhH3ݧ>G}㵁WznrDb~ǖ}&:=Kv+l->_=eĮS[{'[:X&j.}8v?|.xbU򭪺?'?Zq[\'; ǹ] az]p0n>¾pq^t?b>|H||k6᳟jc~ߵ}[K+Y^>=*"8=mIwړ$~_<٩ۛ/"1o w<{}ܞ "!)aO|"7ۿw[Vm_gN VGl m U|\=w,#@~.۟ޟ/1]9M!yfw=7 *_~'Tw#sdFv՞hj\;ˈ'2:3t#3W*8ꇾ}>Uk )O& au B7&V~E嚞z<~dӋ/^r潋gE7{J#g?.??QSdk)~KjN7_zux \'85hMxc:=?CA@辧O>|2? L%ɞdގ]t+;$ I:x7sg=z$@!3{mb)$B K^ˍ5=m&_6:~Љ}xvxQ+luh]!6OYw7KYW{hxEYzN͸6W|%jwLh'=njnS<[K?FM>.ja -:^4+KO >Sٳw};Jrggjf˰&i=GAsƶ/x;e wgp[9RFYw*vcX{/!,ed% u kQÙQhAg픭;wC<9P_-kU-1Ĵ%P^!j^ю\@Jyvhga*o@zS[돢Xٞmmܭʧ1#IpU\D'[{#FּZʢh #LI4Iqvw3׺Q tɠ)3*B2 HK58[.(b۝owdpJa4Wˊ&m5BP1ks "o˓کbk`VC:qGѶ,som1;ЙY2|/V, 2 hϳ ;{aqjw/ii#Kw[{ayS:ѵuzcqzIȹykaʞpvKI3$@UڙoNnkl{+ab pD镽8f@vJ9piUbIJ80"$F8b`-04{LIH1 Tx2Yqڗ{rH?*8RK' "sG>0s=7o^h< {rbY1vt>`L)IYo_#zgO m J㙼/©#ݳ}yܵe;rtƞHn5W z,/Xj 12쇓% 9 ˒Wy\OIG ]e;.OqX ߙ9v O+^鳜j3_zJSLcip?9+d5,~%ݩu$>ǹg9W^Fk^y7Yў^{mJ7Zg9چ Z%p?-+_YsZ$[bpKth4۶aR8OeS*8{KBoO3h[}!o㦗}cRg?;wrsdO-@ӷQ]}gk&ray0/x8FsșȉNnr7}ő57$kKb$]˖`[ n\GM3X{f[?<&_ӫg.v,?9Orʕ>|SΊ$xI'O`_~eΧXF ]G MK[=n5a^m#fp<3xwk'E =&x*rstU#fϮp}_m w7ϵ*7Z'?J6xHA*qho OYTĻ8 =4[wy0Zrw2i|Z')vvD>ItDV`jK7* (ZRs6:OQb{u&b[$] DwSuz䟋˶%̲ؕVʊ9#_j܂~N uyˑ'az 7֞O)"YI㞊ݞ<61xRc'aV$6Ԓ$(}'aN:0ҽ*ֵD5F֓0~Z ik?C =woݡE:slx=D&Q a,݈L9@Fsf1KcɊ7A^olq &B@s*> &΃u noȚn|ďVw(6p|59h(V>OuԱTO'IL`J6.܅hr=|}LJ I\CFVDW\wgnԾvqkW E%6~rL.p4|=*ߛ6GpLo쉧3,sb lۻ괌Xh`8D?cQböV‹/-◖ϖ?|_}g#Uq,7_+! I x`^xLn^eM6Dmr^2=xmٸ|OHAk 76NBu4{ͩCKP`Ƨ_84JCB{'cw|,Ch~&k#DLk9 J`696↰$ ܸXesI(AwZz|h -&DhΗ'c';/Ռ(!"[>d#C껪`Pb{F^mUjB!Zk2gn`/d]IssSK]+U/л|MЯ\ۛRo#v:`"g©mMq؝BM0)3IlݳWMlZh-RZӈBB_.\)#,^S;XRGO:oPn290>8W jY$'9Ě5A qA(Ι]j\I|>DyD>L#`<1Hؗ QHyd&lvL-]U+QCR( 'A|Y{x xvqپv4*`>ru搼_l s/ż|{jZḊPo(+ +r3p}4 ޿qe{vTFU {#˷W^ j]fӎ`i  Ɣ3O24M;qi^xus htJIe÷$A%ܦw0CqZ兿{"=`uSrLI]%}!Gv} t|NIJZ!95A7&p6<𔵄kMz{ ^Jva,LiqE`=9 εto[NU6'xi?uj\ `0^ Ľ?pl+|GO+k_9]%cVN}#uЀAOJj qKNkČ8_4q4yq{}wwO69N}rRG D緫F!>1o>>L7dfdp?Y.4E\ZwtIɸ=Lȴqfrt D;U¢eJ|-aDg2k}$e~:=@Tvd>#}{y{3f磧?mfwe]8XO0lY۽ 1jSݓbV8R[`qȂiXMn'ۺQlfwgArCRmSm^n/x=^}֠rtnf?Nn };dr$6^$GynkPW ;81AltK+I] alL2.!abգߎѼxom;%Im\ _ 4~ۻ! }QLʏ@) n9']vdYpoͿwL9+4PGct:ry/Ѽ؛sP;[dcL2U1y\C[o*2>o~t8z9vO~l:+85E@M~Oɭ(zz?hk: 47oH9Nr|}I2pӑv_kbĕLTbƑvN݂~/} 2n_Ww,{mAzƾytxN̓[B% :n$ g,0:%<^S8BI!]8kSp^2RS(n ц3,gOބu}=tIt*P;ə%\|7i ~M#Uɖ+TJu(u>CنiwyK4I>IV>Wni8H #<9N&6)6ǂ!5:С|*넵=s$ֳi}_M8 SU`_s{fL&غu$ߚx[ O{컭Ӂ9AK,[cלO?Y'$n5U?mxl-ً%K#U7I}f,eoh՝|ʯBݞ7.;-}L>)~˗_<5{7RSO)`VhkG2{#_ͽ[i.|nR+ɆhZ$[>M(xɨMk6.'NKIGL e9m1; {f}Na _ u? wIE:׺Wڊ^^GJW-ѧ٬4LDҹ̑@hA]5} ۗ'SwJ̄/ֱ;|},l>W<,>;t,Ľx5]n|国no߁r혎V7jm $*]gTM?p5YpCLH4A <t)1h#^IM_%mDH}ރuyחSvo tSblPEoSq|\CK{ ރ޶..9NtIlΆwGK\ + ÛRs/};X [x NG,;puo~Ӈ_;q+Vp]U:9 x+~8\/u]hd!k7 um~~U|=lot{ECx,T#i[8\qlJZvrt`-ӏn wJDZYwS9|~;PcMy4?>SwVh@?/)_{[/ {?"sꔍ'Xbat/,=,jQ`X"0yw~:=\itrBh֚Bd7شk|+Ύwֳ%&u뺗^(oepz%H4E|piэl E"3i%A6m-u`eK|؛| f!>6~oΒڣxT[N|&*ܧ׮boGYsȿ־1/tTԏOo}u~WF0hcTU/c >Ъhosw0Np5tyB6b[cXB&0Om|,wLHj 1 3JIkdmղ$$͢E֐bވ bxvc~e9on2\DUB_),=U>$={rLfG]efjKr}.;: v_us8J!-C^Ys~7ژ3U^/j;?|GTL:?^˽yOZ/p猱[9sa_kw/qP?5OR68bpז'Sio|g9xU\mznyߗ_:=Ŕc^cJ_g26/;>Woe4܉ڌ4+Ѣ9n_ %z۵Å7K Afcm  PvF*={y9Wr Dt3y=ж(C2|NehJZܽ/;\[1U-qpyi|h_A;T кZ D?;3y>`e 03)8v'OJ Ka#9Isl ;Q7 |ڣ9[+k(uuHhψe جgi͜7Ymʒ:hJ)%̽gT1;cȦI@@aTPrv)9 8i[@Id';k+*jqNz1c0|_ͥ7m{lzЬ)Zpq&Nk4i"V.ub. ydn:}1>Hat\mޚ3:b|'~l}__]hc*i?̸_ͳG@S 71RÐog{9:woqMe3A=a6s8#S'goFY*aM:nF,^Ӄ0xƪU5K]r >_1]Aи:oGa9sIsXx)\Iu៤rcF~a2 7[7І~lk@H ̫Z,e|ϼ[׿(il=S;=qL]gkG>ӷs |po8ohL.1Ug~O.=b0۾| _O~f>%K:ւeޞ~M÷yop}5~h3m_ó Վ>$g_OGptlZTƋ%h=nE_!{F8u}Ə7G/VN"߼SD2$hzwN>ƣ.hȗ'P?6l.W]KSRc t)'06'܁pHX@_+-=ǝ}g5IʚONd`iO4_Z)%S~hP"T3~g4]v}p JVPHˆnIUj+\=z_ؾ]ǞVͼs*ܕI,cN9$ W*9m|ˁc5ml n?lk&<u |YrMU28{TaDlm_+Jui~(8tkrN^`o%K6n:#6;DmAWE.'O?`C@BGK\K" ڼes]2 _|U][kd`O똀|ܭxvn0`q- ڴOBg>W=Z!Cm ݸsMѷٞ =T]-\۩;M ʞU`nEkk]p |$϶5U_ ^2-v`v@e_:zd|pOѸBA)V og *өvӍdI?+yڻkَ>t ]ly"-9 \3-MhݑY$ *0B,|ӟ]~?x sd >vKfw(J;O^UQLvou`A=.8C1>% `:]`]0q!_x!{p wz5)}Z>Z6SJ=~|Rg;ibel'6Ο;;| \qxOo\b7}?/Żuto3ZxgF/ؘz]?+]3xb$κ8Qq8xESxtP%#KP>$MD3l׊'q2]kn\$zPGIJ"_KZ9!4,}C_ G~䇽˷CXUڳ$=me3>۫.`>j(Sskk98n)h ϝO@S(>Å%'E>> @aq'-˩ ח{q-izgy//}#{r?y h8QMbw$!E*.66%hA9 7^O w!PZHUƽw/,a91;+˒{5,\?pui3Ʒ2F\sNʨuFǒ]bR)kcO 8Buz`j+}>wJL/Y>E/i%~#Ê.dhl 4?=yX_.#Yg^w;^qyŵro[7֚hs2!? b>|պv<o V "ˆgVP Bٷ#Y;uوF7adVsr2*(|\Uw['ZgT^h_R=]y8WU004eޮ CQד4Ps0zxw[.Ņs!&0%^9Z'7w=՜UkpZA^[έ#)ƞ-y ϞUpAp3f\'O7ZJww㍒yU-?/,GOzJoh*˵7d2rV[/8>ɴFÏɺWI_گδu&]r(dhYF~3tf&?8b}#'[˓(h }z3ϼ{O䏎_Ͽ T?}uCg;nӂK,aG 73dЀך #E 猪 |@6`w.KΘd>#mD;Jd TK軪H|c4#ʉ_=ŪiGMIaXpR2Be [8σ$`GL7sm<:!pOCs?*lW5+' 2V%03N->^`;>.QBH{&XJI o Z3(tuNU0V̙n ʂ*;>ű,G{kcK1.sxe6uJ>{JNN)ٱiTkcx Wu=f鉂!d^wgsW3Sپ7t<̛gmL'c<:[{3Qh`1Л8&e2.3kC^G;pa#>>G`ɜ=w.f_9tմ/?zwy'=#zӖ@O\G`7d_ӏ~=asFA JtԌ`;џȮxtO_-ˆuw(;G(]6X9 x ->4NC%5nt?[=}w%7ۯ0ɁH$ =xso(q@Y>n+\?9AHed .U 'UL>R#F/tv5.?%npmzy ̏Sb{V2kFo+W8ټYk://ݴfNx8z< kM&*#ͱNGw(]0qw'\g3;,G_$ChFwaS"MrP"ihA"ImlhdwlWrJ%Y 9| ty: ے?\q~O=Nv6+6:Fh!XOtc3w{UV#ɈdUsʅZE,h8˯F>]6p#uSX7z{Wf0{ٖm>^$uK&Gfd|⦮!Tɟ{fitVOF {$|l"~d|̓5nF4 d;ykvo XwtpmޏoK"v R'Gm 6čo<?jAx~/M(||&#G +}W6mMdIY^K+s.8>`|CÓuljl^+Zz~ysm[ACH}u(ohlHN!F"n}26ƒvx:Pc)|In{WWv6qh u*ħ~YkȄA炙C|HCU_:/*iWI x||k#w/K˗+u-huBJjo[#+qQ'J"U| vl9ܾ#8N.ϮCY`i*52lxunb?^a%F{qǃ(VUqCԼL<" IvZ9G-~kW]{Izpt9JTB-zpTrG>d{x2Y /zY _|-9=wa'B?wb D:"OB_$~f|qGW혣WU"ȇ@\ƛWNU.u C 茸QE$qroJrr)Ƿ.'8'w5ؾw)xړ ҙƦd3El(( t] F!D2I>n@Zw%)>f|ADqQj]-j}E09}ɧ8ѡS\}οu20cqW/?`$ֆ?Nz8=9Lȓ/{yO;:NX 7^ӧOgn[^s_h=ںoBlAr⥫mlljtء6(Eֺ.iMrrYwe&>EPA2Ŷ6(ަa¥ᕩZ֮"f!VIW`VešZF!@8awN;meGl 8oOA7B#6i_4m֪5R+!#3F5EF!VS&)B9DPﲖ2}6 j~|D$bbAGAi-O6_p^ȗ.u>YlOY8ܵ{kZզ˝;Ov=2bhkәxI )x?<Ýk\=L\`%c^6&ÀvI9V} 1j-7έpFۉ-OcQTjG:ˢqmgo =Yl}^pŌ\u=gQ;r=*MbkbrR<Ց0K!5ʀTc~#Fe@(Rvh~{؇[Adih&H&-acT1vP}K794w//Wn4:g;\6?x?~3TR,]+ڹ{y_Տtw \}˯7sIa GIPLfU{7۟]?oUaKs{~`885lzg?~|gag ª2$ wRcbV;~ڗ3:c?|ܕfxW23Hڹnѣu)Ue-){ IK?e ǫPw#3w,i$^.;ۗeLcE2' 12Uj1LwTmncV#ST҇OA=;^@'MS ~џ W?AJ6e8c}.8~t$]:RHpshMw8 6e68Y_9WƸy pI >'3%we5+@<];Ǔh̉պ0FiV~'negs&sn^[AMj q2z|~D|A}GFN0zVM*zةShߌ^#'R9;(1cm::O8rZyF"e ieƴteչs$BWIWZ>Ko?C 3\xi[6IXg9|Z%}Sr]tYP~( $gh$Dq!wu۠rPq=9}`g+O%XT`=ZEO%;|D ꈚm;+~ph_s4뙽鮜%k;|,eް?лSЇlhoz xn.7á0ox|'9B#:;{vs|fmuÍ&1lٝրķ­m[uIOILI>QjE;g?m,;Νrp0YuWj4gt9o:ˍt`EWiw֥G3'r7h#wy9^|E[s{3}Yu:~ U:Sg<ņKmhdrs)-Ӄ Zs 9,鵜9;]Y ŎؒȃH'S q/a[ =av7 q4<9v%O|# [yF"6wc7кW}VcV?E7?~jM>< ёFpU`Aa$, msNހ5OPe2xiѽ,g WMF|Ԛӳxx?  U{dTs/l-?f6rp"*M>w4ֻ=/ t$P}f6?K֧si{k> uފ;xtt+(?}3cbG-+/]tNoՍ_If/&m]n8({~,?lO~t .|{}v9lz*ۿ8JߡiۧR &>Gc1NJ'#d6b5m7qO8q._#l{l Itdt) %֜C=KaM(2rK🸋>RבO{蘝tĉppr+߰}g 8W̋|sX ݊V]NC%al;}Vr(Ko\\s]k;@"d.z?޾nu Q$pO׹#@7xӖ-:`W͆zpWϥ`$ػdU 7I69"uμoܼqGqDF{> n~ .E_|9ßE|&: q iA'-JU]8Xfk}O?w%+xx*#nx#Q3={ӄQ+?F~D]|wN~ӳ}kϏv3ӣi;[nܨzE`?,޴QR7?~̾gW)v58#F(N*n 7|o|}o-|wU7Gk̮HZW_,Ή=e`[i''Uiҁ#'?3\̧rfpi+ 3Nvpoοa~xL9ڃ#UN FA!ܔQB Aj|57A0@btjgN:mBH}89Le_F1K$&}J-*$,{P?3(y 4]5*͝ڟqLb|zkҊc8%a2+ c mUtS1aչdǴ(Vл`[EFaM+^n} ٓ[O & )K |lӈ<GƣVR*o=U0?L/JɄ U1ݚla 0]npi{x'9I L8Ix\`RZ$apN>\eIq.6-Q UpCo-H2`oZDkGO7)[.Տ  '9 JXQ%lz-E8ⶣ裞q k/a[nӪD_Q[_z{䓏~solywO~o/pYr=p}pvݿVv{2nxiҦ< #}iY.RƮ|t(D1FJ|h &̓j?o%>)!QOV6S<ص{8[/=yc"2PE:|~>rOt?J5锻E;nbxZ;Qy/;7 a*+9UGns@Co]݋fŀ?SϪjhGవ8 '۽;9]wCpf; 9ۄrNz.]ϦsKnS%$4fn𾣒t$s}]<x?Cֈ?l/?9OfϿ@^/oֵޱo_epn:/$8gY69̗V ,p/_}Uuus%ϯ\tw(?^p#љ[W(gu >nfG9Vӿ+sOytMyq47۵;;zKifN&:2ȣ6v q_XBk,=x:o|i?1iv˯d:+;CI#I \ k0aٷ{1h;|>3͗ϮŶ\?|gmJYfm5H+˗{T>0\=t>{Nud@mo\X~t gѾkZ/B+?v*fkG%'} +A7h|7 /t ڕ(87]Sb =CA}FwFv_tŷ:cGKsƻKG<ΧBLз/v&$}W>Ƿy::y3O>QIB֥3K" V>5'x(H9E.1nydG| pw1rve>OJx ˯~ݯ|^tv|?u(r?]BњZ,~=[n.>Ytdi556wh9Ók@֭¦!`wj`(@'8>]2Yb$QY$7J rDlxQ ן'xw;Yȭs3?볒/[g ཯z%s\$l|'kͽ̯.rq'PEd!GSY}Uo[qvj-[t$uW%Dтyړ:~wg=j_LB#2i ߪ}?q,{sd~SȾ]4c4otkWk]%3~dz_PG0^%y[U<fe ?|-(aN|HxJ>z c? 9O[<~=Tݾ%۷\2>®hIq 4M{PL5|ss$ܽur߻>lٷ-ḳKګ' m12^vo[GŭrWſϒF\go:qU.53[467_ţ'gFֈ5SD$eS|Xf_e fqn)VmnDkW >" xКg.xA wq_PnK`h's l)E-) sZ5NzLkמÝnU߉ fCcpm\lBKLl;YB/b`[O1⻝]BHXHDKVq}D~늡7ku^g֚]F˳*w8?O7,oT-lh7u=fQ6 E/N<>, ~Ńy8= E_+ߩfk;c -pKFCpn%&Aճ5ϲFo>ǔ  Q;%/ 7В=Ɖ <ϤVy VA+~ fm [o$?ј6+JPv4I{0~3xϺk ߝLϛ`is'AFA1ctY?`LGP5:a<~\zGАjǭ[d^|vmyB<yKܻ|7k*zZ/$0hh R.(743S%cߎ=e4Kʚ>TVGp0;C/uǑW.,/}G_= MbȀe:og4rG/|6sm49@^ el ^p#{"ޠ5؎h^Pxsbn = tPZia}[p$93 }TR`4gdp2*KfG_^eSu)a,u᳂رIpñhu [A2qU豞'e񷊦CvLYOUk,ɉz׌!W^9Ggss!*{=]goar{%djrm_s"1RsC@xρoςu{Ǚ·9XM}`iݳOLxrR0߇tSKVy,C?vF6Ys[/k~fjXώ]dj(D˶i|}#dZv+߹sC%Hk9~}3lҎ)1$ZǏ+ooF4/c%h`0;?ꟼ?miͿrͿD;JBF-Z;dq+C$2پ7_z%=<립g\-cx0z?&eɣWH# &AIX9m_:jQ0;--e2͏LuѪUxU?d@AUV%6c;W\#|[{=˒ ^txxUKRR:Dk~+#k{& xǣG1x=0o{G9o7i> mxP<8}w732~' {'hc3Zؤ`=_ub`b?tu$#InMѻ nGFOv<ȯ#n^gl{=3Uu;Z{}1сl_f'lFWl݇;YFg9-]3±.?Docv7xG/[ 6aّ$d w,{d(B)V|nSȚ @$w |{/˾D#bWIŋ |أ#58Gz4WrlֆFck8? sӷ?i|PenZ:;BQ&IpGW&/|Vz}]"1CO=U'DZ=ZKuU@7Lzz{аZt魪d0=+`1Q|*l^\\%!'Mex"Y|Kup|0xOt>J^@A=~֑-pgBFy+/_d[{>OIE.ck*.v3Tnl=gJ gc|`=RGhgou\"Zwd/ ]^,1;0s0g#gIMk;zր-o&KaCǦS<+PR(FwS~>{%}N:Qw#3<Kјf-j ;_~_G32E[xk;q'cΑxp?^7ۇ1$ uu w:Ҙ7/C|3AJ-e16$Pq#{ȇ[J76En;$5ţ'u!3pk#לC9hjKSpjZ VXΎq*~ C `!~7&v[y)nconYOJ޺~Ҳ]K$ ĹZp'yCӵCDzCTlI?|2ĸ;7"Tt{ &aP9JJ$ZxBtNi qch&Qyr^@iO*@)2eū[Q31d{;-sV'HO}ns@(?2cdW mu=M+b#ɝ@c6{zTr(vxB\A?65iy7meFҮX3F``r򙳯P6LSSc`@s_>?oD=熽S!޳::Z=2 .Ǎ=xD&ʹ y4W0YEgk,hFI t "BϊpP956C*VP -Iѭcxh#= {CGUY/G0ڦu2G^c}4|lAa%b ,Ce }zJ4W%&2q&/?׵Z\ϑ*;j?Qn$];k~o< 72^HփsnBSм^>[_Z3'kQ{ @췖ieT0ikǛ[z8N=U= O/\-n:@<+S.8hksϙP{?)h8?nqí;({DпQ;ppa|_/0~U8"гYMN#<W}6QG覄17~H{1ΏO;B* eح"U WS C) HM43=T=)&k%'H跊8FL-dèKf5r^OhfhشWw';~L,Ƃu?/wbEoͣxxBhJ«Wʎ7'Kʜo;X0i`=e[ ^ vxXş2);1 *Z7!};/Wpyh$JY L_`ftɎBF|xD߾>ۡx*<|x[p$[pmNhy&z$vrN7 =CjLk[@y<D˾ˡ'F'?' x ?eu#PS0x?( ]ycm-?D1^4%<9 9N &Y< mQ7ܛjRIbG538\pĽmOHTӁ=]ϖ&[왖 HsІ+ڵ# #> uk[/h|?8c6C8#8 /r Ult`Wm:zP:-/XH8=_E3` a0Ѳ;`ed0_χ - ǼT=ypl)/֮^4I}_tz,\L-\ZTwr:q7YZQ/XӮ# qz. fdخt39p]NrfcxaD}Bk^=/b]Ӂ4xMW fAkPa\K-)$ul8銄&tR._njsO= ֪B:.hoϖt踭[o updtKN&>> g?"'m[p/']/8?x@nsGМW=FoUQ챻uk mpz{_L@IDATÞF| B.0ǯx@vd5kLycTz؝[X%4xrUs:< _WA&u*zV JNymF> kmcWCd˷7c p߁ g-y.`Lo+PCN0k4)AS*ӯ^YhgkfnSXbI$[tgE v~d~iO_OzodƑכ g믿5 .7':>0HA/-dz+yյu-~5]&G 9Vl*+r|+Gk[SOq=(!wkW^9A7>$n_.m芤?w;Ӈⓟi/T +~(+ع?Y*88^~t,.'u⧝\߄0…e [뺈O ]Cj OϞLNOQN2I'vh{[hvW=ƦYMZ!M'ux]O9~.gݷaf9KgϜ&WUzF"aXT1F,K*Bx![@8~huLݛ-V感zD1uLN9Zg%Vt_6mIP$Ս3uus=H8w>[]Lᬸʵ2ֽo]b?yOG-|S[If гx t v4#e~eI 7z3ߟIZ~x'~zU СV ~_$"hm%;_F xX(?/$HHّw׋z#.^E$`9Zv@Ntp}Rܳ>yM M"6lJY8$JA9hesV *#nx>գ: |uQxn< UlC*@9iS]_BՎka*6{%sPYo'G y㖭NXۉ1(s%c˦ĺ ][[H0;AcOH`ra 43 P "{%@b3 ID+efjCdh=2 b n(8ZrR)O(a(;g9{* Q%F1NIqv*>USP5&ūLyxy$7I01#X eh5?F7?sc7,(4`HȯJw6 . :|isÝ_XffzЄF娛j-дJY+c#M#DЏ8nz?JYq6ﭪU(%ag_fj% g_\vT8P0lk]!6:r 8y=^[̂ 95ji3ikcj3_.|o,LgABρ{^-[1vf&޳*mo8<=Z8q4XXem엔"3!.FX =o.J 8}8y%8]ڵ)#ڶob( rN_(p4e ; o%";»>?B h_Suh1$ `Hf߭KP#1Ή8~$-&eCo@y3@/.kt-gc2C9ws*evϞ fKϚ=_ 4(GMF>^B t{UE~ D>WEUkvȍ>] u{:aJ3!>A& /=#9G(,m6v&{c d1'5D%Uu29Y7۶)ΝPd\.f}-d07*q`cnfrDvԁFET,NPXyRv)H5 !%g PmF QMLwG  /?𲞦\K~.: o}eZg_?[^KUߝ0ٶ|_J|;˻pa_7}7+ vK \mxgK4q"qa V?*7?xfǎT yZ^zcZoOG ډE:[sl>ًh/856O[ã;Nd+)nMMX܉foZi]]A)7K_:fk 1+h r @35ҏ$G6 Oܙc%tycwujIs֎Q%`Afy>q鄏/B+r#22k{0,{Ys' Jmfrn<팸-'E?d#yyJ‘#K5pr3GPڧJ\JśK c4-A'IY9/08o-N 4>XճcE L\z_8YJ?ƃɂgSnb ?[mթDA[Siα/k[0dFәpt0_+hzۺ֗4|=w&[r+':+'`Se wВ2m u'as6^BWh1 6祹Zzά&}Ozacpdhsv8,u)oZ{J_l#ѻ#ʚ(s^NV9kL<h0.lj y`y]βrLY2U%֒Z+OlڵfKyߩXY. [[v<j~x$÷9pL=3k;pgHdUvkl/Y̢Z?\7>>vرttkvEíI Hp$$ӹ$ eOZOk|toӜSpHh|ͱ|bv4HI*uxү6x-}ߝ8--0dH;(pX=J]{翕n׿.;+,צ[8-/Soo{;yq yo1/׿&FK =<򷽦3a̯Fgk#ȸ(zZw#^3 ~VٳC@~x= rVzӎ`0'zV<&Gj^֊G{9j9,Od'N6&-IaZ)F8Mq{OT6Nb$F莡Q.׬乍׵==fK-Ig˓xN$8MF>vB#x7کCp:\/l˥l݇eCX}]ы| '1^\]y}_E\'TZb?ۯ]\j:x1zصG_˗S T BŶ(fzCnhL~r ޓ-KߑO,#:@6tK͍tޕOxų| Y#l< (t!t3L:ur Yu~#&_FW"/2\$pg`x: kn~OyVu =|m5dGkE 7#Z f+DԺ@ZBST6|rrp4T&z2T@7~xO7kyR?gI&G|3^:ա;g@' d|M=<}XQvA 6Q}/qsWa ju3yp/Lk! wf{=j=|g4i кKIZ}P+g$V㔴?X~ֽuvV񮍊Кީ*}}pMxxL08m_,F {V.et xFl86rtdxPĵVÉ{~IKj<Ҹw~rsAzb hMbp@8zpX0*s3y@4/6jxc nOIW]s]_$԰0Ɔ8Б r زpy^鵛; I dsp^k{sCg <|ILnmBO&;gw6vun\ٓLu%֣Gzk$ɹscKd=F cXUIJ8{ iKX>/> IYsf{S`"AU]";!`M`KNHƷl%xON)bip{+O>WTh٭gX^$DNP'N(h9bIV~m8wnUe'N:}tf_ܫxK/Cyy*{g,N%w}:oJ# '6v``[ ]';Z g[OmM.kI*!ؙ{Dõ:yp9?߹kO Un)L1= ~|pЁ5& @;\~$NQa}p6wXQ<E 8I9M%>kW $\K"=U8,ЕtaR}yg>9~\t{A0<|m[L|8Zª>Wq|olCkPa=&m1kyq0ݳvHr g`n'x!R:#*<n+R"J J?5Yvk)$2yH~,##ȅ"t[\)0 X|kw/+>MNN4J3 B>e=˹B%xy3)sX9! TN-C qHDŽK K1`b{crΝS/"^ ͜=a7Ƽt7Hh i/$pS/(o,N==1(dth1 lGS=;$fN5IG %QlWw!$5)82 oL cgPL$1Bc"y *}&xLE[[(v!7HyeF7`U(1煮OVWRȧ m[ܙ?HX4C$8*q8!fMh Q*"8t $Kk֬) 8$h h)Y/zhHih;CҏJ.wDK CU8$ T20Yx!܄2O'&4Zpo3{ 9m zqڸWv2&axb5|KrO@XLa #`{Ɵ4YʨO)0Ho~e,tEGue: ;68cwSU8{gMo\=ixw_<ٞ!2#woG k5GC;#06^>9gZI3td/olѷǎͶ{DÛ~vW=a[r9^:A`[g [ :l1~ztjY}|~ ? t%{o61Ѩ#{hq.fpͮ9١>S~US+qݷFv^rcw׿6_+_Z\%?7 f"m ~]iz>4{rNoSwXU¡~K/d p?${h/[z=D;M5I=+6< gKjE=>sǟ^X~__zk[˝ 8+^X2[&> Sz2c $nl޹npy//NhNbIΕkw^{,}d[t~7v J8ݽэ峫dCt9I2=Fpzd[K7s}`I'ז ~•WI|ѹ嗾Wmb?z-1?!['뾀0[晧g?\Iӛo_jƧ?xGx勗m%,U0/ uϜ\n=+h.||IbQh΀htptUg\~ʧ[b*o[7 ֫|lw'0WK}NGLc\vgQC[_{syxuIٖ 2yu6F@k_>tΥy|+n,,_ysx| 6?ĵݑ%k]gOWzk5_\λutWu'i O?Z@E"oCػx':6ÖdWpKdT:Dۙxo t!vj6A:0}8t> k\N7: #{L)a*cqO[;?͏g*Y(ԽMObLpеۻoc.<~HsݳywzvWֲ]YgP߳#A:x3yiswwGWCb*W\ήGn:lY5 g)]_c?kJ'ڈCg];j ~UE9pl:7X_BlvWϪU>Ic$gN6:|@?@֞rl.IQbR NaJIƏ^BW 97v*rS&%;xEo+}g9 ڰIWgႮ/^;Yy:Sg5o~z&"a11Cu3}X*ݻ!yj["I2dE#U󱀹X r r SUV:6hll{~v?0t+nM|Lx޹jl  ck^x~xDk|L۷EǏN-h]_cOL`znvP1PU0(OߓWH9Uyt$`v';a }ܹǩn`/h/M(?Ml=xt|fm󎦂HŰZ}&Y+Mst+< o%t5 :ݕ^Un䞷΍sB1y:R21q4A;;\ \h F>6@҇aJVz ]%o+ur{hg'|Z6HsׁB@ym8樹ه5DtVÞ64D(i-H M?7ረ zEM&@F)&pӝG&S:wV+qPe #3v.d5P `j%P 0D 1(Y9WE }|$u$g- ' k#[FPǁ7=IInWak/)Ap&`d/dqXN}uy7?ҹ܇-)\, f|ٿK~2)"e0'~e54)q s &/isq|JLZ`s#/s`^ps]yAЅwko+§xQϬ <ֵ'C!|g|5~lh\YpT7vq/`#iQT}^*t̸:{H8T Qe|O^,g^r̳#!ck0Gq-R@bN: mQ]e2!ZO 1ēe,H\NF*m]qa!;Fᆿ71pUʜ`,KʭlzD=q R 6K:i1Roxo;q" Kr=sF]"yqqx_^8wtÏ?>|=\UoYpj#ye:8tY@;A6Ad`7͜_+ >&xسmJN =zښ$4ҍn {h t]N %]jm#ģ׮xHwqaH1zr畄w{=HY$%C$)޾ aC5.>={&>$3^.+A ܓw3o^Ȕr6;snN-xWkx&Jn@-idϽ^G˅ ?&XA5>mI_pv8Za9X{O@ym oWt`휢qt.A={Φ78lnLs9$7i<27_d}lpu$g^C 9!trp5o_[ |}YRҽ>lsLL߁Z֡bl=MX]9K2/a5 .XDJIX/j{&+I-ve9=$mkm8:({I-gڂ5^%hx.9w~+o~n+9  ȾƞK3f0qV;ꋎ?g_ UG m0kU6ׯ\Mcpf]ogcɼ6odzp`Ý5*V}inN+Vƍ]W=lh{??8n_n?#!H>ռ]泫}8:A›vZuvTЊʖl!,w؅~f3^`9VaNһ{xWcyF2׃)2Z5 'gǟ]^-{7˥,!DO7w]|}6<҈Ӿ=,vOKiIZ`i=vW1~/+η~d?k`5 GŜgKB*a(rTE~;Gt\sZwjs|#->lyX{F 3&W-I5c*lBtMBͮ4+.,~b5N1A`n5d X 8KU(kkjX?;2ؓWk_ [ٕ[6= .ln1_gWy%=]\9-W^-zFE,;,.:RSv)ڝo,K l^t1;w^Tx/&gGp k"]4{۵ pSv-}Kƺ fxof͇mEffQPJvCm4Cp.+ܛ^ml7^H/g5C>ȆU?ڨVp{*3U3?/]Z% ml }@R'1e0kא 3ȩ`Jv*qw4X^y:I:_.:e@}ENh*J'V\7*^%4>.0$90HxT?ڏ"#~GR m:V*<5x ~9:Wp tFu^D3;u#?/OҵT; O zhOd;wkIWz)Op:7:5' kl,hK>6 ļ:Oc|.U{6W]̊YT'cO7._Ml!.?#Ű]Hc_|J/vߕ+ςl9g ]:t٣w;, Y0eb.p~x5vo9PVa6r OnI9jcG.nfC+k7 ŔT:$Qw>Q,{?|3tm;[LW*:/>t=u۵56 ?lx;c\ WH`ט/o:θ!}s{/qp|m8\ EV\wBa;v xyCYb.wgbeֹج9FIwvۃͮu#}dz XP[s^4-?p|k_{},쭃c{'t5?Uan(elM  #Bk QNCB`JDWH_ W/|g0b;_V?Wo!g k_lL9l3e`"@@xs̫Ϳv G,%#y%-ya`S|鎰w7& wgc%`/-jvp4[٘%ng}bS-ښ PBٙ*[B ^9TC0lsNXrUshʸ!0iprR)ݰ=^٘:d9?O(|8jvM[pPb *)}z:/jߑ[e (o!W۝[e3 !rK_\sS ZW8<#ž[4lmq\׵q|9xgdW:9x6;͘H <ٌ'Uyx(S lo=9b?qyj(Zs;o $pP= )7j-dﴣRIjG߾-5g3"QΜ酶n\8qr kΰ{g/ SOܸzS'GM$ign׉S/-eI'˷ߨRt`XKWkdv9tg9*kwHF΁q2@0lM9 F0F!(pC:p,sFo9{Oe ?g֘5'f @ߺq1Q ~r GYpݐA }rW/mmDtFhd(~|q$Ȫfhɼ;k`<>73,-JnCwuWygvwaa ؒ%e[XKO[08`  ;f'LOtsy5BbaTW{|TeD>3d/|N5XA /G`eMm* { (aHZt1t9:l3nnj zWAI~3 T<κmb#8U@g93@T>ʿ;W_ w.gMڑ!C<# ^]%3 +uD9 drB%I]1_K<]9d>cps6g<6Jd-H:w' ?cSuKvL3u|+MPa!9q&0@A ~!t*g}$*=38#僚#ụ9^~3 hqP.F q.=l#v6ߕQ3'ȷkW{BpԍN<]8&1r78+xmeFO;-,js;utiQ2a?ѫ r&?9\QGt?z Wx#,%z?yȡ~>v0i_u6B\Pghik#jUpĦ"p]%#NKOu+a ] x>S<9_aCy@xN91uڋK jy*@IDATڇuL%^acN:؃2*tNh58c{[Aʺx$C4ЩKZѡ Ȳüa,wQ}}Z '*mmB px7K,FvKw]&.[] nzډGȳ[;g,tfevj<\TNMNљz>e#XVƤiPbu2ʻ?5#* WS̪·CȈk:鳤39iD+j0bW6&i]pbSw8<ؽgc\\OWWtf{TJC 񠭫TU ALfwx{o!dd-;0wM~7Sku:0=3?+{f:]?⒬A93YqNLeL "ݍTM9U ]d <8lGD8= ZsOV$VYݻIˍi t4ɱxUFf~<u|t:Z_y6]w?kt=#QL=-v8Jcia ~|T7^ 6PsMuu;iysV]#8yly|}R'Q38_R'#@>2nĆ}tkd1VZ{T6ǀ͑:I?p+ekR/gVS4buw g' >"@#PV:yz>TKM[$΀}"3)$  x3$ ޻AU#k)g[Y!lYcqSۄ&Qo%|qw#SX`&oWy->P\ TZitv<0un g d~&R>Q|6OGn#? CIg-aj{GT(8e.o]s/]zV"ȔY:u 9γ6;PT6nXS+tNg&'6~@lv5 Oy Scvt$J&lw,r'Bs̗H|Nl,e3.ԐWG߾W{eRteJvIN&[LOG!=]t5WT7Lܩ9~6'X :੅ƏPJ Ks'8qLzJMblftxm/ i䈎+HcW6P!'y+MV֛£8G<A0Rృd'?A&f9 kTzY3 iDxEG[Z1.oG]iPhetfgߌZ*R*m%=)c`^0RuØBoB(_["X<@z a9αL0{o)rE93YRjFY@&Og]Z'6 ,VVp$V +s g3Y ?S>z]N^z@*f huF3fvVdA\QT:#Y!*q̄| Cpt.NM7!LO !Q9f:K1%T3pq<<_fJ( IXH/ 6UܥکWsZxP ۞veghVitUNah >I܀t2N_F21Lۘ.*"_"S¹ML9'/nY|k%PhG5hn}Ow[ ʺ*i320>x9fXEP1 6iW rrb,]LSS88 :U803KTeK ; qH5(Tg]lʘgE U+4n2suD6S-?SS/)h<@3ADfL =\X<< ,QqLlY*(-{`s6ÅK F ˃U8‰tTrm9yLY 5+_:m9(PS*Hu0?;\"| G s"ƨyVL˃va@{KW=umѣA9GXFO;QD!(m f9E>|63UU27XKSCIO?̛C#'瓵W[yy?m0aBx8xv^T8zZ'6gm $m:Uq<"x&~X`셰71Ezj q6+$ OV5qLL$PO5J{u_YNk ?_a(G.}\P\mbt:YwMpw :]֞%^WSdݶ~?<ZiV3QO\'A6eaNMr!vG%p:xy$vu3W#C Tq)]8+'_vO9= X-o]k=6cqL\sCC> f=gS'UcUș<׫˽v!{OogfSaK950}wiBqn_4xwi cmp#Ftuu1Fx: `-Ax_O~& ҃ ~(s;"Yn*K:Qgz[㩳Zh)xY!q_}G6/Ԯ\xjNǕin}Ov>5B[9}t!8GLeRЈyd e}m$dVcG/a@YdeQdEu@|)؃G3$p&qC`+O9c& &~aSnm]hF>ݩrt2F)X>u3_||g=ux04I`j\70JgLQLwb&M4z*A{{$q)}^fgRmm5:qJDV%J~gGP|RosߖP2A_B60x.="I|k57e&&9~#7d^:rU=\u_+#6$2 kw捍 =uF /@X{155u &0N?]6C:i //s6LOhB-嵏=,_>y:%Kv[=}b9(~S&pѦR&3Ԑq&,KZ@{{clb+5Y 6 qBcE: ,~vts fHȎ= Ekwh7v؉oB H I@zVXtƼ sYxzo0N$]@{cMM t56m3M(s 'vď +`l)3a٠p!=sߗȋ nBtxbr^ؖ.'1$IGb++oLbo~mυ;dtqW[CG%:ǼT (k[^ 8ǚ[ۣc5ExdVZ֟},N٥KkSmյt;K̃2]ULn v Me#R 6ULj2'\Ħ& ɞlƥƤ ffm` c6Cy0ϓ p7DA$6Wjmy ~p /& g3({`PI Q=Vv"{q/m2 a#@ 2Qߋ2}+{d;b<=f\-meTwH:p}V gkSOR˩g cdsg' 2^}>#T9dgWOh:WetoL{Nʣ I0KFJOuVaG?V?F TCSA* 3biZ%pSߝ~8bG ΰOƙ@W 聏vzp*ohSqP =p;Li.-̅Pcl=<ǩK"w.΄F01 lr13l*vɊzѕe-ߊ.{;[a4R"y TAV,>y#{?]J^8QpTE\LzQɑx fZE, \ /.˿(%0 5 GbʹY9@g[j|r>Z#ڜyoI/WƉI;:F$IRݻY&a; `,h4TleѶ,Ʋ%>(#VŒ[V mF@v<Ҭ6 s2@Z[wa;Zxr?^2c^q,e * ƘZc 8i;rF^+Q!s7X}p t?V4f~KX`^=&h+ 8bYh:wU9$~-t°)WKb=d}+vR ]!0;("M2/ Gx\NˋkWK EZgE{R`(ɲIFbMޯB>Mu.|_Z!õ(*ed0楋=& iSAC)vf$V {d,!6"kՒNϕhF, \$/8jjz%p޽[^ThNO$ĴsO( 驗>_]_1 565'&J3M#@;,C; }腏ylU9ge@i8@Y{~<ఁcrXK Œuh\S1~l"c*ct"'@ڨh#3<' u [ 据$ 3,m=iE)2)d2xH f{uVߢX9c= M=} GQ WP]"}j 4bw|(^lbr$:Q/u֮#Zg : =ҿ5d[kuyx*SRxb/u|D%`+x2pG580 @O,3 ]{hksL0{:t\uL*/X|DRmν6L!x<ϬM^\LT_4 }9TDh;2RKJStҞA^Sݗ]'qґu]|<煡tL8X/nׇ\Oq)Ia`-K,#I{6|uA^D'Te.yu Tc#H,Q'p`84 NyL{]g%4,=Y9Cs1х|?[ƦAR8=qd}B $8]p^Z6R=V:ia3_'3%p=BG ƄU0ToQ&gg9N#h?3܏-`7g^I#.C$-qE& ߡM =Kv$TQE42|uO:SY&xpa 1tGg:d^uk+ԎH,Tf{Ԣ`^[_Wῖf%,M}wl}𳳿Η{ VØp}LͧXagb~3*e|  hkҽ-1IK _qDcc?ŕؔdLM[FؔIuTjLGRJ$PVE}m=OOqS؃=A%w:R$祏QT|MA8 ?"IXA>Bߓ E?]-< 6757<ƮTB0ڈIbk}SjlVѓN8 : g-/\Ư4EW,Y`YSH( -P.[ZjU]Fz#yNv]Am8v:OM 9 !~[͛WPMIݤ}++R@D~zCV_L?l|%g'rznً|4!HMs~=&tjCO÷'$ק?H>d3Fghw:xέvp6o~9~*;רo2pv3QZoܴ;mdVso6We⦅v(>VQY[C6~[QYowv)sߌ0mpcڱqHH&~g ~C׻m(F7s j C/_ yXL i Ft>~IQoz NAy dV1m3$a,WfP0YY<8n}>'.ylEjӗ`7:=G$ {~cjh}nkkX[N)+Xmuo{sMX",n3Bτ kqL^$77cG-_Q|dǐլS?c:(7)[Sk[GaS|gB!l1e\ceqX$eS&er"q1⭵<4Mo:B-V=ɧOr5&4 @7iޜj9u};$P|sf.CQ$QċygJ%V_0TϞ1@|Uq62GXx$[%z< .O41]09OLĖM2MMaW015j)̜ B,?z2%t.}է_0.Fwњ1p: 'ACQ],f xlyhEE,T  met0˸,*5whN0ol&W̟aj[-^\;UU=+{AVs 9w۠ 3$lB' bfOxpgu|,oGo0 KoFd9?z0]r-CyN0vjڪbJ*?3f/_M$X0CD?Os'+1&F)O}/tZMj`W**V;:2s,LY&`=΃9CZ`_M0Y~8PUn<'Ո 4ҖĀ6\e5#Le-0=((')!dy4,C1 1x{v *&m8,! j 5\٘&w-a>: :G$x߹7VQl˝|rp͔Ԉ. kqx:jƗʶ WjlQf30nYm0sD5@ZF<|Z ` v^ !`͜wh~8ΡmojdwuuqNdЧ ]d:PzOd x GG@1; < vrVᙶ6p'1952GxȷUI:f +=-5t xs6h5I& X; g(n'KY[80h+ lVQZ'] 琣`t}fX7ϷV:M}n:Tuv l{榐m0Y@Cx_N$Wc)3|١ʹ*K5uJѱ.ty<$n. sP$/l!ߜc qXǗIe8MJQ%@mc+WnWP%LKXoeY:|/a(/QGtb41 e"l7aGjiWYxTӆ=4Swk&tپ G3pϛ_ζ+6a:k@y=\~HJ+y JS$`++wk D1&^q<-1{|}I iG6հ'7iH#%Y2C.-C ƊJ6X|sPdb![\4a|z::.|P{>Q{k*kb1:usB/oGOgU{@H<[cp\npaDW@ ~ 2Iq~44tuKC6婍$p x§b'%U|\ԁ"\4fxIYywBOʓnZ]rlr0GՈ btMi[!'Do.*`? Ҷ^MvwxNX&FW^KG,~1ݾ>znsР|7'{C:/gSq3ϕHgD}'&.VKԿc\+UD|bR,؎K-=WRJ:"P_Vnfk~FwHJJ] 4ʒlK-M/q]*+Raiu&35[QbRNfq_;1GiKw L'Mh(+p( l} jחHU=A:P&|K<߿GQcW "YC\ )p|θ_]'1?z"jۡ}r;=ޤ'gl ZSUDo`ڭ*hQˊh ]C_>,$PwR;+ڴU.Y|:9Up =VL2vfRmpAjH>l[m;Ԡ.9>mnt^jw-;xQwSL%?H]FxGQZl;zSVwRY]^L8{ɦiׅ/G}=r!{X2ꚚbͬJx=obj6#0xD[}^͢ <*shLw_S"!x5vN}ث~p[d!N;%,;C#J )QאkSVr&E1E}AtgKN( nm@CSKY\9!Ch߲|m[H.] O~Uwv pz/Lws.sjr-NC#O75`gd/Jp Od{; ۂ0 I& 3S 2`3Hnnd-:BsVtV7TXc.ζf>5VlS/%bk36Fc錸z/=ETG=x!3[+8q^:2~a9tJDG+("S9P7k@W/DƖ[E=G#ڲV˧NL:xcw4S%@–bjF,AǧDe% ct,φ=GA2 cy!gb/*؄FMt"aUfdQf(tih٠gdtݯ8XBp^JPyx^oCVH*1qJeJ!3upGr3g|FJ(W̙r-U(~[I (UBpȸ H**]izfR0u⚊<b`qDSQSG!/N5ȺWQUz+Äd@QY' I@iK~qn[-nXe#O89/$D5ȟf胷y;qO(KІ $ h6,O[m=e8 |a-Ƌ5l<2w/1=.#ӷaHCH`F5a󹴲hE.svtx>G9̚WcާTv61kUqNe&m ue9'L)V'c.F\C\Ӂe{8|脰4t? ؂KgN㮾nġ$tW/: TԐ3Q5oXjrg*|L2͎- sYFvYc\}$5o<EL8@ Hfwyenj lG੼Q6 _t( iF~A;:5 ;VVCN2"Msyeؼc "@aB h)fLctJv$) M'C˱E $D׳Uc( PI<$Uyr>`.lYinj[l@0qτC`k`+XAWb#V k$ßqmߧ,0v׬"\^ Dp@ŀ+pjR{&|lqh`:_lpoec昷zHˀ0iSk')"C*>:ȭzWK3Ki@_ 0;!q{FA>O?|x7}|&2!5K@:VvTW z|8yq\עK԰@]\L8Z*L6ah)uH0pQ.._Y7D7}|7?ߗZVM"쫿J* 6p.+u_Mya;* $'Iot>:28hͯ}9;:;']Р=q!?vEe}81N 5kT;@;t]HV@fɋKHFR6{ʷ$\s8ۡy2SoW@&Wֲfh2v|!/mmիOoIp|u@nj$S3yD싏pxLԒ#]{YAg]C+40 9&}fMlbOK5x‡(0V$*Ƭ';vy@w녳kR{TUP]1{2lCmO m<`:|Sӓill4lae93fE5l?AӗcܕQ^} sOg)%}o.xkmިCԀGGÎSiV^476 efhYYV0ܹϗqx|ts}{9=I<338b o;*B`"!c'^Z$,풙Z[4mGx_(G"4V@Q_Wbb1DЃ]3bxd_5LHg$SyvI4e nrj*M;tG{˸ !\,siyeNB?Li#ҋ˝};NtWynVq}+ly c 1ng'&LO7/XĶ7G+c(`nr&.lI*p'2G*3iV)M7Ҧ=:RoM [|@ Ս^: Y&˛ V-L /CQ7vdyp@[DI8Vt}ZŅ01S~f^6+(l=\K+t]OɊ`yWX+k;OP]}#v;FWOw$y7aaK\]x:EG~*mmNo_OٹK<яX& vΔ7IAUfzr(xh5kM~x0*2r5&p٧TfITVc 79⬏=b3=iV9r|Jb ӳ>̥Y*ũjmYCVuFUGӋ !W46I8/Velp NfR&9g:*$&VF&"om"dRk6ʙ G`ͶB򔀌s2`6ٙY /P՝1N ȴED3HBEm:6 h"#SiԷu(eg׫ܪH^LeC&UT(v- 2+(,8}8wG ;kNkG{#@vCԅGdiX!AB2Sr퉂B0`O P9=03V*(O,pgĪc2*u[Kg8sAF\jaDV0ᛱbfQm{s @bīPY*S9E\+U$pfBzV $>s[9AhQ^ ,d*۩Du)8. Ec5  h/hDRAQ9/ R՞cWy6s)z^ԫdigG4;ɱ{)/E \dX5 ]5 z1hpyP]`հdAΑE9[{OdŕI7^^gaIls3 x|;( s%F!A h\'dUF  8J`!SA9T|Ii"2<^(OPH70($<}:x i9ol灋G' h+jlEV'SEǶV?Qn9pd:mpqLI qT>t<{F ;_$?*f/-Y>9kkO4Q du(?g&5sW%~, U58  Y* VԷ:]5:`l2]x*oJq% cF`\:@!pR>>, Т84 }=ͼɜF voo#N q8Ed͍-z# );^uF _PD7=o=3,߼\71Yp:T iDE oBuBְom`j!s =5 |zOQYIÐ%HJ??^:MN8Z x>Wnx*p1oV5& s߅T셹rFG={֜C1>5&~2&[3 ӎq룴15t䥎檱>,g/nu2*twGOkY*ONOcHM39ЦάGKVVtuw'/ԋFSz.z'jci`TС*i2s\wT Dԣè zBɜ"Ng Ex]8+cɳɪ^&37Vtp^:\ aP*VJ6+ .Qf1A. eO.… Νq|j dr*ʺHa h'ßǦB7 ʼ r3Z{Hs)8S/^lEO@ % øY2z\wq"62ld0C{V$9H\w\,hEg:iL61g"tW{i噇[BيusLG_7u7:k:hCcn E*iY9|U85A~9?d/-=8nKt.rT)vgZQi^߅aj6% xy}5yu&WUF}Rg$[hzo+m#?wla+d[GyrK$8׵0>yЅ`yoCb7g,^&<50LHⅼCކ [ߍϔ3yOPA!qx}>7_3dMBZ;#c|YޤGg ?)W1&K٩.,(.dOzZh7<-Y7x>ue*w^"U3:g[o7haPI; a[ol"DzL,){Vd^YaXɄ˳ PuVS6JȭH>[.e-bec[2֠? :/8o~ Sd$ӽVVeVQt)7NuYzy0k"c~ܭVY g)bHtzLhSt_w@7g<ڠXib["Hlu>G巕͝$MNN$\@_c=*Nrڅ_0Jۚy|OA|o= u}$H |CO u̠=[zq2Q:x<8ǥtp$oM.601)-.Ln )a!CNG-vϰ#:݇k6>a= %1U6a|0fN , ~G`<#O{C:3)40_R҂}orDM[=#`?0%%)iqq1wu*ZHvMe4 \eGyT:-%>{i~nF|vXn[UU<8x+g1ձL(6SӜ'ͧ>"+<ZT;T PWƭE C3 T2BuCOd$ko7mpSoc$TgEz(J<_Yy Od5T4B_|64-dPοڴ:(2 r0 BS%ȱL }"sxx'}~VȜӁ0CpHX]=%v9-$,מz>I-T7Xia~8R5-ԞG\gim2v 37c404pE:Ǔ`'xWkFPu@0o=}iA==6A>a.޹Y89`Fws844ZEP1BfԼG&0V4+spV {.2Nxϓ7sP&#78+pfgUxmuMq6 衞KhTwx)kL u{rD,(HG`V%mx[{Q"ٝ! ۝ 0t5&Rd|-bshkjQ*`b*(D Z =*'}tKJa"LldBԃ]]AcFc Dtr+i{fx/j)ԊF|3IW~Zh3ЍCLE>&BВTƧ EK#H!/1"J&L:  c*_!Ptύ@%u}+CAYAYY53eEPLOx`eYXn4!X<lT;ltp`PE> ]gT,483A1dmH "?*aQFHb:LqEC[\NٓůԬAeΞWH3:tUW "St,h ¡/8rOmVU6mFj'[7( v& |dsTX.Naqv\Ȗ[<" to|uc>4 L0Bf S r"8 'JbȎ]pD ' i Th``͚EG<#DY^jqAx#Op%*by(#O\֐ɖ OhAʆY%$>>u616>_ixȧ"3o~ji2L+0?3Qpsiʲy'R?\ԩcv/$|t}ՇU|9[r )Yȴ QG$` r~j TY鋿+ Mu#]y[_#3; t+CY DƳX^!; XnWpDQՃ&y(ͪ,pD+RoO:gbC?c"H# $O1|hu~kkVӍfAl;g|UwʆEa2aҁuo˺ uofIPCalj&S)nLئ&'3Vw!}w2tp0FT7KX˯`gؚ:M?>sؙ[AƇ7Kx]/?dzڳS30H'bvEEby3Gݷ)Һ '{ 3UfOH|PIC2OSϐ3eMDKpG|poMeGa*Y%' +Z r|m(iߗj_GL+)յ~<'ܡ׾dh H#9h8k*'9>3yW#,Noedy,>l3[=!3>n ]R#kE'ܗ,8b# mq1f= 4o9hVx?.B q=YI$822k\;묔m=|E%3M)0;7I<߈:xsge~&xe=CgPe; 3ͺO%,`1nv{ ^cU4> &+BZZ eq%m07\&Z쭟f|iWzᇿMr1M?DKe\&o;:-i\~M?woƮvV~DǾ:\2T <2tLZ>ZvDw%.TQOh/'Xϴ 2=\pF=4\7 {.\LQWt,чtE~g:_ 3MRڗN=joigv8ac4RVZ^n^jR١ǙI)CsއRI:쳕J]E* &xfĀs[#7?$k:q5!%>(kd$nÎC+)(kt-}b]-4u`wr)^.j==b y[!$-umlt<}vz;FM&Hd#@!A $I?0 ð GVlZHS,;ޑwٝ{0D|GKާ|ooZ$^-FK+̢1olUJ{yOOoϷk%P@J_2#Kwjsw-'՗>m9XÙ2vfőv߀´IA5ЭWln0 ߵ}Оv؏sZ.%NJJ_C9:2&9ދxTwݠpm݊ϡWEK,(!\K؎| |[